TypeScript · 2814 bytes Raw Blame History
1 // Frontend error bridge. Installs global handlers that forward JS
2 // errors into Rust tracing (which writes them to
3 // `~/Library/Logs/claudex/claudex.log.<date>`) so a silent React
4 // crash doesn't leave us flying blind.
5 //
6 // What gets captured:
7 // * `window.onerror` — uncaught exceptions from synchronous code
8 // * `window.addEventListener("unhandledrejection")` — rejected
9 // promises with no `.catch()`
10 // * `console.error` / `console.warn` — anything React's error
11 // boundary subsystem funnels through the console, plus our own
12 // explicit `console.error` calls
13 //
14 // Install once from the app entrypoint, before React mounts.
15
16 import { logFrontend, type LogLevel } from "@/lib/ipc/client";
17
18 let installed = false;
19
20 export function installDebugBridge(): void {
21 if (installed) return;
22 installed = true;
23
24 // Raw sync errors — message + stack end up on disk.
25 window.addEventListener("error", (ev) => {
26 const err = ev.error as unknown;
27 const message = formatMessage(ev.message, err);
28 const stack = extractStack(err);
29 void logFrontend("error", "window.onerror", message, stack);
30 });
31
32 // Unhandled promise rejections.
33 window.addEventListener("unhandledrejection", (ev) => {
34 const reason = ev.reason as unknown;
35 const message = formatMessage("unhandledrejection", reason);
36 const stack = extractStack(reason);
37 void logFrontend("error", "unhandledrejection", message, stack);
38 });
39
40 // Shadow console.error / console.warn so anything that goes to
41 // devtools also shows up in the log file. We keep the original
42 // so the devtools output is unchanged.
43 wrapConsole("error");
44 wrapConsole("warn");
45 }
46
47 function wrapConsole(level: "error" | "warn") {
48 const original = console[level].bind(console);
49 const mapped: LogLevel = level;
50 console[level] = (...args: unknown[]) => {
51 original(...args);
52 try {
53 const message = args.map(stringify).join(" ");
54 const stack = args.map(extractStack).find((s): s is string => !!s);
55 void logFrontend(mapped, `console.${level}`, message, stack);
56 } catch {
57 // Never let logging fail noisily.
58 }
59 };
60 }
61
62 function formatMessage(fallback: string, err: unknown): string {
63 if (err instanceof Error) return err.message || fallback;
64 if (typeof err === "string") return err;
65 if (err == null) return fallback;
66 try {
67 return JSON.stringify(err);
68 } catch {
69 return fallback;
70 }
71 }
72
73 function extractStack(err: unknown): string | undefined {
74 if (err instanceof Error && typeof err.stack === "string") return err.stack;
75 return undefined;
76 }
77
78 function stringify(v: unknown): string {
79 if (v instanceof Error) return `${v.name}: ${v.message}`;
80 if (typeof v === "string") return v;
81 try {
82 return JSON.stringify(v);
83 } catch {
84 return String(v);
85 }
86 }