93 lines
3.5 KiB
TypeScript
93 lines
3.5 KiB
TypeScript
// Browser-side logger with levels, timestamps and a ring buffer.
|
|
//
|
|
// Level: NEXT_PUBLIC_LOG_LEVEL at build time, overridable per browser with
|
|
// localStorage.setItem("iotp_log_level", "debug"). The last 300 entries are
|
|
// kept in window.__iotpLogs and can be copied with window.__iotpDumpLogs().
|
|
|
|
export type LogLevel = "debug" | "info" | "warn" | "error"
|
|
|
|
const ORDER: Record<LogLevel, number> = { debug: 10, info: 20, warn: 30, error: 40 }
|
|
const BUFFER_SIZE = 300
|
|
|
|
interface LogEntry {
|
|
ts: string
|
|
level: LogLevel
|
|
scope: string
|
|
message: string
|
|
data?: unknown
|
|
}
|
|
|
|
declare global {
|
|
interface Window {
|
|
__iotpLogs?: LogEntry[]
|
|
__iotpDumpLogs?: () => string
|
|
}
|
|
}
|
|
|
|
function configuredLevel(): LogLevel {
|
|
let level = (process.env.NEXT_PUBLIC_LOG_LEVEL || (process.env.NODE_ENV === "production" ? "info" : "debug")) as LogLevel
|
|
if (typeof window !== "undefined") {
|
|
try {
|
|
const override = window.localStorage.getItem("iotp_log_level") as LogLevel | null
|
|
if (override && override in ORDER) level = override
|
|
} catch { /* storage may be unavailable */ }
|
|
}
|
|
return level in ORDER ? level : "info"
|
|
}
|
|
|
|
function buffer(): LogEntry[] {
|
|
if (typeof window === "undefined") return []
|
|
if (!window.__iotpLogs) {
|
|
window.__iotpLogs = []
|
|
window.__iotpDumpLogs = () =>
|
|
(window.__iotpLogs || [])
|
|
.map((e) => `${e.ts} ${e.level.toUpperCase().padEnd(5)} [${e.scope}] ${e.message}${e.data !== undefined ? " " + safeJson(e.data) : ""}`)
|
|
.join("\n")
|
|
}
|
|
return window.__iotpLogs
|
|
}
|
|
|
|
function safeJson(v: unknown): string {
|
|
try { return JSON.stringify(v) } catch { return String(v) }
|
|
}
|
|
|
|
function emit(level: LogLevel, scope: string, message: string, data?: unknown) {
|
|
if (ORDER[level] < ORDER[configuredLevel()]) return
|
|
const ts = new Date().toISOString()
|
|
const buf = buffer()
|
|
buf.push({ ts, level, scope, message, data })
|
|
if (buf.length > BUFFER_SIZE) buf.splice(0, buf.length - BUFFER_SIZE)
|
|
const line = `[iotp ${ts.slice(11, 23)}] [${scope}] ${message}`
|
|
const fn = level === "error" ? console.error : level === "warn" ? console.warn : level === "info" ? console.info : console.debug
|
|
if (data !== undefined) fn(line, data)
|
|
else fn(line)
|
|
}
|
|
|
|
export function getLogger(scope: string) {
|
|
return {
|
|
debug: (message: string, data?: unknown) => emit("debug", scope, message, data),
|
|
info: (message: string, data?: unknown) => emit("info", scope, message, data),
|
|
warn: (message: string, data?: unknown) => emit("warn", scope, message, data),
|
|
error: (message: string, data?: unknown) => emit("error", scope, message, data),
|
|
}
|
|
}
|
|
|
|
// Describe any thrown value for logs and user messages.
|
|
export function describeError(err: unknown): string {
|
|
if (err instanceof Error) return `${err.name}: ${err.message}`
|
|
if (typeof err === "string") return err
|
|
return safeJson(err)
|
|
}
|
|
|
|
// Global handlers so uncaught browser errors land in the buffer too.
|
|
export function installGlobalErrorLogging() {
|
|
if (typeof window === "undefined") return
|
|
const w = window as Window & { __iotpGlobalInstalled?: boolean }
|
|
if (w.__iotpGlobalInstalled) return
|
|
w.__iotpGlobalInstalled = true
|
|
const log = getLogger("window")
|
|
window.addEventListener("error", (ev) => log.error(`Uncaught: ${ev.message}`, { file: ev.filename, line: ev.lineno }))
|
|
window.addEventListener("unhandledrejection", (ev) => log.error(`Unhandled rejection: ${describeError(ev.reason)}`))
|
|
log.debug(`Logging active at level "${configuredLevel()}"; call window.__iotpDumpLogs() to copy the buffer`)
|
|
}
|