Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 92 additions & 12 deletions src/utils/logger.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,29 @@
const IS_DEV = import.meta.env.DEV;
/**
* 개발 환경 여부 — debug/info 로그 출력 기준
*
* ## 왜 import.meta.env.DEV를 쓰지 않는가
*
* Vite의 `import.meta.env.DEV`는 실행 **명령(command)** 을 기준으로 결정된다.
* - `vite` (dev server 실행) → DEV = true
* - `vite build` → DEV = false ← --mode development를 붙여도 마찬가지
*
* Vite는 빌드 시 `import.meta.env.*`를 정적 문자열로 치환(static replace)하는데,
* DEV/PROD의 치환 기준이 mode가 아닌 command이기 때문에 build 산출물에서는
* 항상 false가 된다.
*
* 결과적으로 `pnpm run build:local` (--mode development) 로 빌드한
* 확장 프로그램에서 debugLog/infoLog가 전부 묵음이 되는 버그가 발생한다.
*
* ## 왜 import.meta.env.MODE를 쓰는가
*
* `import.meta.env.MODE`는 `--mode` 플래그 값을 그대로 반영한다.
* - `vite build --mode development` → MODE = "development" ✓
* - `vite build --mode production` → MODE = "production"
* - `vite build` (기본값) → MODE = "production"
*
* 이를 통해 build:local 환경에서 debug 로그가 정상 출력된다.
*/
const IS_DEV = import.meta.env.MODE === 'development';

const MAX_STRING_LENGTH = 400;
const MAX_ARRAY_LENGTH = 20;
Expand All @@ -7,6 +32,8 @@ const MAX_DEPTH = 4;
const REDACTED = "[REDACTED]";
const TRUNCATED_ARRAY_META_KEY = "__truncated_items__";
const TRUNCATED_OBJECT_META_KEY = "__truncated_keys__";
const ERROR_ACCESSING_PROPERTY = "[Error accessing property]";
const UNINSPECTABLE_OBJECT = "[Uninspectable Object]";

const EMAIL_PATTERN =
/\b([A-Z0-9._%+-])([A-Z0-9._%+-]*)(@[A-Z0-9.-]+\.[A-Z]{2,})\b/gi;
Expand All @@ -23,8 +50,29 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
return false;
}

const prototype = Object.getPrototypeOf(value);
return prototype === Object.prototype || prototype === null;
try {
const prototype = Object.getPrototypeOf(value);
return prototype === Object.prototype || prototype === null;
} catch {
return false;
}
}

function getObjectKeys(value: object, isPlain: boolean): string[] {
try {
return isPlain ? Object.keys(value) : Object.getOwnPropertyNames(value);
} catch {
return [];
}
}

function getObjectTypeName(value: object): string | null {
try {
const typeName = value.constructor?.name;
return typeName && typeName !== "Object" ? typeName : null;
} catch {
return null;
}
}

function sanitizeString(value: string): string {
Expand Down Expand Up @@ -79,6 +127,10 @@ function sanitizeValue(
return sanitizeString(value.toString());
}

if (value instanceof RegExp) {
return sanitizeString(value.toString());
}

if (value instanceof Error) {
return getErrorLogDetails(value);
}
Expand Down Expand Up @@ -115,23 +167,51 @@ function sanitizeValue(

seen.add(value);

if (!isPlainObject(value)) {
return sanitizeString(String(value));
}
const plainObject = isPlainObject(value);

// non-plain 객체(커스텀 클래스, chrome API 객체 등)는 isPlainObject를 통과 못 해
// String() 변환 시 "[object Object]"가 되어 디버깅 불가.
// Object.getOwnPropertyNames로 non-enumerable 포함 own property를 추출한다.
// (예: chrome.runtime.lastError.message는 non-enumerable이라 Object.keys에 안 잡힘)
const allKeys = getObjectKeys(value as object, plainObject);

const keys = allKeys.slice(0, MAX_OBJECT_KEYS);
const sanitizedObject = keys.reduce<Record<string, unknown>>((acc, key) => {
if (SENSITIVE_KEY_PATTERN.test(key)) {
acc[key] = REDACTED;
return acc;
}

try {
acc[key] = sanitizeValue(
(value as Record<string, unknown>)[key],
depth + 1,
seen,
);
} catch {
acc[key] = ERROR_ACCESSING_PROPERTY;
}

const entries = Object.entries(value).slice(0, MAX_OBJECT_KEYS);
const sanitizedObject = entries.reduce<Record<string, unknown>>((acc, [key, entryValue]) => {
acc[key] = SENSITIVE_KEY_PATTERN.test(key)
? REDACTED
: sanitizeValue(entryValue, depth + 1, seen);
return acc;
}, {});
Comment on lines +179 to 196
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

로깅 대상 객체가 Proxy이거나 특정 상황에서 예외를 던지는 getter를 가지고 있을 경우(예: 일부 브라우저 내부 객체), 속성 접근 시 로거 자체가 중단될 수 있습니다. 안전한 로깅을 위해 속성 값을 읽어올 때 try-catch로 보호하고, 민감한 키인 경우 접근 자체를 건너뛰도록 개선하는 것이 좋습니다.

Suggested change
const sanitizedObject = keys.reduce<Record<string, unknown>>((acc, key) => {
acc[key] = SENSITIVE_KEY_PATTERN.test(key)
? REDACTED
: sanitizeValue(entryValue, depth + 1, seen);
: sanitizeValue((value as Record<string, unknown>)[key], depth + 1, seen);
return acc;
}, {});
const sanitizedObject = keys.reduce<Record<string, unknown>>((acc, key) => {
if (SENSITIVE_KEY_PATTERN.test(key)) {
acc[key] = REDACTED;
return acc;
}
try {
acc[key] = sanitizeValue((value as Record<string, unknown>)[key], depth + 1, seen);
} catch (e) {
acc[key] = "[Error accessing property]";
}
return acc;
}, {});

const omittedCount = Object.keys(value).length - entries.length;

const omittedCount = allKeys.length - keys.length;
if (omittedCount > 0) {
sanitizedObject[TRUNCATED_OBJECT_META_KEY] = omittedCount;
}

// 클래스 인스턴스라면 타입 힌트 추가
if (!plainObject) {
const typeName = getObjectTypeName(value as object);
if (typeName) {
sanitizedObject["[type]"] = typeName;
}
}

if (allKeys.length === 0 && Object.keys(sanitizedObject).length === 0) {
return getObjectTypeName(value as object) ?? UNINSPECTABLE_OBJECT;
}
Comment on lines +204 to +213
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

빈 일반 객체({})를 로깅할 때 [Uninspectable Object]가 반환되는 버그가 있습니다. plainObjecttrue인 경우에는 이 폴백 로직을 타지 않고 빈 객체({})를 반환해야 합니다. 또한 getObjectTypeName을 중복 호출하지 않도록 변수에 저장하여 재사용하는 것이 좋습니다.

Suggested change
if (!plainObject) {
const typeName = getObjectTypeName(value as object);
if (typeName) {
sanitizedObject["[type]"] = typeName;
}
}
if (allKeys.length === 0 && Object.keys(sanitizedObject).length === 0) {
return getObjectTypeName(value as object) ?? UNINSPECTABLE_OBJECT;
}
let typeName: string | null = null;
if (!plainObject) {
typeName = getObjectTypeName(value as object);
if (typeName) {
sanitizedObject["[type]"] = typeName;
}
}
if (!plainObject && allKeys.length === 0 && Object.keys(sanitizedObject).length === 0) {
return typeName ?? UNINSPECTABLE_OBJECT;
}

Comment on lines +211 to +213
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Return empty plain objects as objects

sanitizeValue now converts any object with zero own keys into the "[Uninspectable Object]" string, which regresses normal {} logging behavior and can violate the Record<string, unknown> contract of getErrorLogDetails when a plain empty object is thrown. In that case callers that spread the result (e.g. response-parse error logging paths) can get string-character keys instead of a structured object, so this fallback should be limited to non-plain/uninspectable host objects only.

Useful? React with 👍 / 👎.


return sanitizedObject;
}

Expand Down
Loading