| 1 | // Window-level failure routing. The desktop bridge rejects a bound Go call with the bare |
| 2 | // error string, so a stackless rejection is an ordinary backend error that is |
| 3 | // contained and toasted; only faults carrying a stack reach the crash overlay. |
| 4 | |
| 5 | import { |
| 6 | buildCrashPayload, |
| 7 | globalCrashReportReason, |
| 8 | isOpaqueScriptErrorEvent, |
| 9 | normalizeCrashError, |
| 10 | opaqueScriptFingerprintHint, |
| 11 | paintCrashOverlay, |
| 12 | reportCrash, |
| 13 | shouldReportGlobalCrashEvent, |
| 14 | } from "./crash"; |
| 15 | import { recordFrontendDiagnostic } from "./frontendDiagnosticBridge"; |
| 16 | |
| 17 | export const RECOVERABLE_ERROR_EVENT = "reasonix:recoverable-error"; |
| 18 | |
| 19 | export type RecoverableErrorDetail = { message: string }; |
| 20 | |
| 21 | export function isRecoverableRejectionReason(reason: unknown): boolean { |
| 22 | if (typeof reason !== "object" || reason === null) return true; |
| 23 | const stack = (reason as { stack?: unknown }).stack; |
| 24 | return typeof stack !== "string" || stack.trim() === ""; |
| 25 | } |
| 26 | |
| 27 | export function onRecoverableError(cb: (detail: RecoverableErrorDetail) => void): () => void { |
| 28 | const handler = (e: Event) => cb((e as CustomEvent<RecoverableErrorDetail>).detail); |
| 29 | window.addEventListener(RECOVERABLE_ERROR_EVENT, handler); |
| 30 | return () => window.removeEventListener(RECOVERABLE_ERROR_EVENT, handler); |
| 31 | } |
| 32 | |
| 33 | function containRecoverableRejection(e: PromiseRejectionEvent): boolean { |
| 34 | if (!isRecoverableRejectionReason(e.reason)) return false; |
| 35 | e.preventDefault(); |
| 36 | recordFrontendDiagnostic("runtime", "unhandled-backend-rejection", { status: "error" }); |
| 37 | const detail: RecoverableErrorDetail = { message: normalizeCrashError(e.reason).errorMessage }; |
| 38 | window.dispatchEvent(new CustomEvent(RECOVERABLE_ERROR_EVENT, { detail })); |
| 39 | return true; |
| 40 | } |
| 41 | |
| 42 | export function installGlobalCrashHandlers() { |
| 43 | window.addEventListener("error", (e) => { |
| 44 | if (!shouldReportGlobalCrashEvent(e)) return; |
| 45 | const payload = buildCrashPayload("window.error", globalCrashReportReason(e)); |
| 46 | if (isOpaqueScriptErrorEvent(e)) payload.fingerprintHint = opaqueScriptFingerprintHint(); |
| 47 | paintCrashOverlay(payload); |
| 48 | }); |
| 49 | window.addEventListener("unhandledrejection", (e) => { |
| 50 | if (!shouldReportGlobalCrashEvent(e) || containRecoverableRejection(e)) return; |
| 51 | reportCrash("unhandledrejection", e.reason); |
| 52 | }); |
| 53 | } |
| 54 |