diff --git a/frontend/src/utils/logger.ts b/frontend/src/utils/logger.ts new file mode 100644 index 0000000..50c620c --- /dev/null +++ b/frontend/src/utils/logger.ts @@ -0,0 +1,94 @@ +/** + * @ai-summary Centralized frontend logger with level filtering and sanitization + * @ai-context Use this instead of console.log for consistent logging + */ + +type LogLevel = 'debug' | 'info' | 'warn' | 'error'; + +const LEVELS: Record = { + debug: 0, + info: 1, + warn: 2, + error: 3, +}; + +const validLevels: LogLevel[] = ['debug', 'info', 'warn', 'error']; + +const rawLevel = ( + import.meta.env.VITE_LOG_LEVEL || 'info' +).toLowerCase() as LogLevel; +const LOG_LEVEL: LogLevel = validLevels.includes(rawLevel) ? rawLevel : 'info'; + +// Warn once on startup if invalid level +if (import.meta.env.VITE_LOG_LEVEL && rawLevel !== LOG_LEVEL) { + console.warn( + `[Logger] Invalid VITE_LOG_LEVEL "${import.meta.env.VITE_LOG_LEVEL}", using "info"` + ); +} + +const shouldLog = (level: LogLevel): boolean => LEVELS[level] >= LEVELS[LOG_LEVEL]; + +// Sanitize sensitive data from logs +const sensitiveKeys = [ + 'token', + 'password', + 'secret', + 'authorization', + 'apikey', + 'credential', +]; + +const sanitize = (meta?: object): object | undefined => { + if (!meta) return undefined; + const sanitized = { ...meta }; + for (const key of Object.keys(sanitized)) { + if (sensitiveKeys.some((s) => key.toLowerCase().includes(s))) { + (sanitized as Record)[key] = '[REDACTED]'; + } + } + return sanitized; +}; + +export const logger = { + debug: (msg: string, meta?: object): void => { + if (shouldLog('debug')) { + try { + console.debug(`[DEBUG] ${msg}`, sanitize(meta) ?? ''); + } catch { + /* Fail silently */ + } + } + }, + + info: (msg: string, meta?: object): void => { + if (shouldLog('info')) { + try { + console.log(`[INFO] ${msg}`, sanitize(meta) ?? ''); + } catch { + /* Fail silently */ + } + } + }, + + warn: (msg: string, meta?: object): void => { + if (shouldLog('warn')) { + try { + console.warn(`[WARN] ${msg}`, sanitize(meta) ?? ''); + } catch { + /* Fail silently */ + } + } + }, + + error: (msg: string, meta?: object): void => { + if (shouldLog('error')) { + try { + console.error(`[ERROR] ${msg}`, sanitize(meta) ?? ''); + } catch { + /* Fail silently */ + } + } + }, +}; + +export default logger; diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts index dd0d605..5c5dda9 100644 --- a/frontend/src/vite-env.d.ts +++ b/frontend/src/vite-env.d.ts @@ -5,6 +5,7 @@ interface ImportMetaEnv { readonly VITE_AUTH0_DOMAIN: string readonly VITE_AUTH0_CLIENT_ID: string readonly VITE_AUTH0_AUDIENCE: string + readonly VITE_LOG_LEVEL?: string } interface ImportMeta {