Files
kandev/apps/web/lib/logger/intercept.ts
T
2026-05-01 15:18:18 +01:00

117 lines
3.2 KiB
TypeScript

/**
* Console + window-error interceptor that mirrors entries into the in-memory
* log ring buffer (see `./buffer.ts`). Idempotent — safe to call multiple
* times. Designed to be installed once from the top-level client boot.
*/
import { getLogBuffer, type LogEntry, type LogLevel } from "./buffer";
let installed = false;
const LEVELS: LogLevel[] = ["debug", "info", "warn", "error"];
function safeStringify(arg: unknown): unknown {
if (
typeof arg === "string" ||
typeof arg === "number" ||
typeof arg === "boolean" ||
arg === null
) {
return arg;
}
if (arg instanceof Error) {
return { name: arg.name, message: arg.message, stack: arg.stack };
}
try {
JSON.stringify(arg);
return arg;
} catch {
return String(arg);
}
}
function extractMessage(first: unknown): string {
if (typeof first === "string") return first;
if (first instanceof Error) return first.message;
return String(first ?? "");
}
function buildEntry(level: LogLevel, source: string, args: unknown[]): LogEntry {
const message = extractMessage(args[0]);
// Preserve the originating Error's stack on the entry so the bundle keeps it
// even though we serialize args by value below.
const stack = args[0] instanceof Error ? args[0].stack : undefined;
return {
timestamp: new Date().toISOString(),
level,
source,
message,
args: args.length > 1 ? args.slice(1).map(safeStringify) : undefined,
stack,
};
}
export function installConsoleInterceptor(): void {
if (installed) return;
if (typeof window === "undefined") return;
installed = true;
const buffer = getLogBuffer();
for (const level of LEVELS) {
const original = console[level]?.bind(console);
if (!original) continue;
console[level] = (...args: unknown[]) => {
try {
buffer.push(buildEntry(level, "console", args));
} catch {
// never let interceptor break the host console call
}
original(...args);
};
}
window.addEventListener("error", (event) => {
buffer.push({
timestamp: new Date().toISOString(),
level: "error",
source: "window.onerror",
message: event.message ?? "Uncaught error",
args: [
{
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
error:
event.error instanceof Error
? { name: event.error.name, stack: event.error.stack }
: undefined,
},
],
stack: event.error instanceof Error ? event.error.stack : undefined,
});
});
window.addEventListener("unhandledrejection", (event) => {
const reason = event.reason;
const message =
reason instanceof Error ? reason.message : String(reason ?? "Unhandled rejection");
buffer.push({
timestamp: new Date().toISOString(),
level: "error",
source: "unhandledrejection",
message,
args:
reason instanceof Error
? [{ name: reason.name, stack: reason.stack }]
: [safeStringify(reason)],
stack: reason instanceof Error ? reason.stack : undefined,
});
});
}
/** Exposed for tests. */
export function _resetInstalledForTesting(): void {
installed = false;
}