From ed84b29c6b24611838c481fdf21980d11f7d88fd Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 21 Apr 2026 02:19:14 +0000 Subject: [PATCH] feat(errors): fallback handlers for unhandled rejections and errors Sentry's global integrations natively capture window.onerror / unhandledrejection (renderer) and uncaughtException / unhandledRejection (main). When a DSN is configured we let Sentry own those paths to avoid double-capturing. When it isn't (dev, unconfigured prod), we attach minimal listeners that route through reportError so stray promise rejections and uncaught errors at least hit the console and the facade. This closes the one gap where a bare click-handler promise rejection (no try/catch, not a useMutation) would otherwise be invisible. --- js/src/lib/sentry.ts | 49 +++++++++++++++++++++++++++---------------- js/src/main/sentry.ts | 47 ++++++++++++++++++++++++----------------- 2 files changed, 59 insertions(+), 37 deletions(-) diff --git a/js/src/lib/sentry.ts b/js/src/lib/sentry.ts index 284a35c..41b6812 100644 --- a/js/src/lib/sentry.ts +++ b/js/src/lib/sentry.ts @@ -1,28 +1,41 @@ import * as Sentry from "@sentry/electron/renderer"; import { appConfig } from "@/config/env"; -import { installErrorSinks } from "@/lib/errors"; +import { installErrorSinks, reportError } from "@/lib/errors"; /** - * Initialise Sentry for a renderer process. No-ops when `sentryDsn` is empty - * so dev builds and unconfigured envs stay quiet. + * Initialise renderer error handling. When a DSN is configured, Sentry's + * own global handlers capture uncaught exceptions and unhandled promise + * rejections. Otherwise we install a minimal fallback so stray rejections + * aren't silent in dev / unconfigured envs. */ export function initSentryRenderer(): void { - if (!appConfig.sentryDsn) return; + if (appConfig.sentryDsn) { + Sentry.init({ + dsn: appConfig.sentryDsn, + tracesSampleRate: 0, + }); - Sentry.init({ - dsn: appConfig.sentryDsn, - tracesSampleRate: 0, + installErrorSinks({ + capture: (err, context) => + Sentry.captureException(err, { extra: context }), + breadcrumb: (err, context) => + Sentry.addBreadcrumb({ + category: "error", + level: "error", + message: err instanceof Error ? err.message : String(err), + data: context, + }), + }); + return; + } + + window.addEventListener("unhandledrejection", (event) => { + reportError(event.reason, { scope: "unhandledrejection" }); }); - - installErrorSinks({ - capture: (err, context) => - Sentry.captureException(err, { extra: context }), - breadcrumb: (err, context) => - Sentry.addBreadcrumb({ - category: "error", - level: "error", - message: err instanceof Error ? err.message : String(err), - data: context, - }), + window.addEventListener("error", (event) => { + reportError(event.error ?? new Error(event.message), { + scope: "window.error", + }); }); } + diff --git a/js/src/main/sentry.ts b/js/src/main/sentry.ts index 8b724ed..ce8aa8c 100644 --- a/js/src/main/sentry.ts +++ b/js/src/main/sentry.ts @@ -1,30 +1,39 @@ import { app } from "electron"; import * as Sentry from "@sentry/electron/main"; import { appConfig } from "@/config/env"; -import { installErrorSinks } from "@/lib/errors"; +import { installErrorSinks, reportError } from "@/lib/errors"; /** - * Initialise Sentry for the main process. Captures uncaught exceptions from - * the Node side and the crash reporter. Safe to call before `app.whenReady`. + * Initialise main-process error handling. Sentry owns uncaught-exception + * capture when a DSN is configured; otherwise we install a minimal Node + * fallback so rejections aren't silent. Safe to call before `app.whenReady`. */ export function initSentryMain(): void { - if (!appConfig.sentryDsn) return; + if (appConfig.sentryDsn) { + Sentry.init({ + dsn: appConfig.sentryDsn, + tracesSampleRate: 0, + release: app.getVersion(), + }); - Sentry.init({ - dsn: appConfig.sentryDsn, - tracesSampleRate: 0, - release: app.getVersion(), + installErrorSinks({ + capture: (err, context) => + Sentry.captureException(err, { extra: context }), + breadcrumb: (err, context) => + Sentry.addBreadcrumb({ + category: "error", + level: "error", + message: err instanceof Error ? err.message : String(err), + data: context, + }), + }); + return; + } + + process.on("uncaughtException", (err) => { + reportError(err, { scope: "uncaughtException" }); }); - - installErrorSinks({ - capture: (err, context) => - Sentry.captureException(err, { extra: context }), - breadcrumb: (err, context) => - Sentry.addBreadcrumb({ - category: "error", - level: "error", - message: err instanceof Error ? err.message : String(err), - data: context, - }), + process.on("unhandledRejection", (reason) => { + reportError(reason, { scope: "unhandledRejection" }); }); }