public/js/lib/errors.js
// Global error capture, installed at page load (§83): what an error-tracking
// SDK such as Sentry does first, minus the network upload.
export const captured = [];
const listeners = new Set();
export const onCaptured = (fn) => (listeners.add(fn), () => listeners.delete(fn));
function record(kind, detail) {
captured.unshift({ kind, at: performance.now(), ...detail });
captured.length = Math.min(captured.length, 30);
listeners.forEach((fn) => fn(captured));
}
addEventListener("error", (e) =>
record("error", { message: e.message, where: `${e.filename?.split("/").pop()}:${e.lineno}:${e.colno}`, stack: e.error?.stack }),
);
addEventListener("unhandledrejection", (e) =>
record("unhandledrejection", { message: String(e.reason?.message ?? e.reason), stack: e.reason?.stack }),
);
// Fired by the browser whenever the page's CSP blocks something.
addEventListener("securitypolicyviolation", (e) =>
record("csp", { message: `${e.effectiveDirective} blocked ${e.blockedURI || "inline"}`, where: `${e.sourceFile?.split("/").pop() ?? "?"}:${e.lineNumber}` }),
);
// Deprecations and browser interventions, reported by the browser itself.
if ("ReportingObserver" in window) {
new ReportingObserver((reports) => {
// csp-violation reports duplicate the event above; they show the other channel.
for (const r of reports) {
const b = r.body ?? {};
record(`report:${r.type}`, { message: b.message ?? `${b.effectiveDirective ?? ""} ${b.blockedURL ?? ""}`.trim() });
}
}, { buffered: true }).observe();
}