You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
142 lines
4.7 KiB
142 lines
4.7 KiB
/** |
|
* Centralized logging utility. |
|
* |
|
* Level matrix: |
|
* dev (default) → info / warn / error (quiet console; relay/query traces off) |
|
* dev + opt-in → debug / info / warn / error (`imwald-debug` / `jumble-debug` `true`, or `VITE_DEBUG=true`) |
|
* production → warn / error only (bare console — no timestamp string built) |
|
* |
|
* Opt out of debug in dev: `localStorage.setItem('imwald-debug', 'false')` then reload. |
|
* Force on: `VITE_DEBUG=true` or localStorage … `'true'` (with dev, enables debug the same as default). |
|
*/ |
|
|
|
type LogLevel = 'debug' | 'info' | 'warn' | 'error' |
|
|
|
const LEVELS: LogLevel[] = ['debug', 'info', 'warn', 'error'] |
|
|
|
class Logger { |
|
private readonly isDev = import.meta.env.DEV |
|
private enableDebug: boolean |
|
private minLevel: LogLevel |
|
|
|
constructor() { |
|
const explicitOff = |
|
localStorage.getItem('imwald-debug') === 'false' || |
|
localStorage.getItem('jumble-debug') === 'false' |
|
const explicitOn = |
|
localStorage.getItem('imwald-debug') === 'true' || |
|
localStorage.getItem('jumble-debug') === 'true' || |
|
import.meta.env.VITE_DEBUG === 'true' |
|
// `npm run dev`: quiet by default; opt in via localStorage / `VITE_DEBUG` / `imwaldDebug.enable()`. |
|
this.enableDebug = this.isDev && explicitOn && !explicitOff |
|
|
|
// In production only warn/error reach the console — info is noise for end-users. |
|
this.minLevel = this.enableDebug ? 'debug' : this.isDev ? 'info' : 'warn' |
|
} |
|
|
|
private shouldLog(level: LogLevel): boolean { |
|
return LEVELS.indexOf(level) >= LEVELS.indexOf(this.minLevel) |
|
} |
|
|
|
private getCallerInfo(): string { |
|
const stack = new Error().stack |
|
if (!stack) return 'unknown' |
|
for (const line of stack.split('\n').slice(3)) { |
|
const m = line.match(/at\s+(.+?)\s+\((.+?):(\d+):(\d+)\)/) |
|
if (m) { |
|
const fileName = m[2].split('/').pop()?.replace(/\.[tj]sx?$/, '') ?? 'unknown' |
|
return `${fileName}:${m[1]}` |
|
} |
|
} |
|
return 'unknown' |
|
} |
|
|
|
private prefix(level: LogLevel): string { |
|
const ts = new Date().toISOString().substring(11, 23) |
|
const caller = this.enableDebug ? ` [${this.getCallerInfo()}]` : '' |
|
return `[${ts}] [${level.toUpperCase()}]${caller}` |
|
} |
|
|
|
debug(message: string, ...args: unknown[]): void { |
|
if (!this.enableDebug) return |
|
console.log(`${this.prefix('debug')} ${message}`, ...args) |
|
} |
|
|
|
info(message: string, ...args: unknown[]): void { |
|
if (!this.shouldLog('info')) return |
|
// In production this branch is never reached (minLevel === 'warn'). |
|
console.log(`${this.prefix('info')} ${message}`, ...args) |
|
} |
|
|
|
warn(message: string, ...args: unknown[]): void { |
|
if (!this.shouldLog('warn')) return |
|
if (this.isDev) { |
|
console.warn(`${this.prefix('warn')} ${message}`, ...args) |
|
} else { |
|
// In production: no string-building overhead; browser devtools add their own timestamp. |
|
console.warn(message, ...args) |
|
} |
|
} |
|
|
|
error(message: string, ...args: unknown[]): void { |
|
if (!this.shouldLog('error')) return |
|
if (this.isDev) { |
|
console.error(`${this.prefix('error')} ${message}`, ...args) |
|
} else { |
|
console.error(message, ...args) |
|
} |
|
} |
|
|
|
/** Dev-only performance marker. */ |
|
perf(message: string, ...args: unknown[]): void { |
|
if (!this.isDev) return |
|
console.log(`[PERF] ${message}`, ...args) |
|
} |
|
|
|
/** Run `fn` inside a console group (debug mode only). */ |
|
group(label: string, fn: () => void): void { |
|
if (!this.enableDebug) { fn(); return } |
|
console.group(label) |
|
fn() |
|
console.groupEnd() |
|
} |
|
|
|
/** Raw dev-only log — no formatting. */ |
|
dev(message: string, ...args: unknown[]): void { |
|
if (this.isDev) console.log(message, ...args) |
|
} |
|
|
|
setDebugMode(enabled: boolean): void { |
|
this.enableDebug = enabled |
|
this.minLevel = enabled ? 'debug' : this.isDev ? 'info' : 'warn' |
|
localStorage.setItem('imwald-debug', String(enabled)) |
|
localStorage.setItem('jumble-debug', String(enabled)) |
|
} |
|
|
|
isDebugEnabled(): boolean { |
|
return this.enableDebug |
|
} |
|
|
|
/** Component-scoped debug log (debug mode only). */ |
|
component(componentName: string, message: string, ...args: unknown[]): void { |
|
if (!this.enableDebug) return |
|
console.log(`${this.prefix('debug')} [${componentName}] ${message}`, ...args) |
|
} |
|
|
|
/** Component-scoped perf log (dev only). */ |
|
perfComponent(componentName: string, operation: string, ...args: unknown[]): void { |
|
if (!this.isDev) return |
|
console.log(`[PERF] [${componentName}] ${operation}`, ...args) |
|
} |
|
} |
|
|
|
// Create singleton instance |
|
const logger = new Logger() |
|
|
|
// Expose debug toggle for development |
|
if (import.meta.env.DEV) { |
|
;(window as any).imwaldLogger = logger |
|
;(window as any).jumbleLogger = logger |
|
} |
|
|
|
export default logger
|
|
|