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.
This commit is contained in:
Claude
2026-04-21 02:19:28 +00:00
parent cc16310d12
commit ed84b29c6b
2 changed files with 59 additions and 37 deletions
+31 -18
View File
@@ -1,28 +1,41 @@
import * as Sentry from "@sentry/electron/renderer"; import * as Sentry from "@sentry/electron/renderer";
import { appConfig } from "@/config/env"; 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 * Initialise renderer error handling. When a DSN is configured, Sentry's
* so dev builds and unconfigured envs stay quiet. * 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 { export function initSentryRenderer(): void {
if (!appConfig.sentryDsn) return; if (appConfig.sentryDsn) {
Sentry.init({
dsn: appConfig.sentryDsn,
tracesSampleRate: 0,
});
Sentry.init({ installErrorSinks({
dsn: appConfig.sentryDsn, capture: (err, context) =>
tracesSampleRate: 0, 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" });
}); });
window.addEventListener("error", (event) => {
installErrorSinks({ reportError(event.error ?? new Error(event.message), {
capture: (err, context) => scope: "window.error",
Sentry.captureException(err, { extra: context }), });
breadcrumb: (err, context) =>
Sentry.addBreadcrumb({
category: "error",
level: "error",
message: err instanceof Error ? err.message : String(err),
data: context,
}),
}); });
} }
+28 -19
View File
@@ -1,30 +1,39 @@
import { app } from "electron"; import { app } from "electron";
import * as Sentry from "@sentry/electron/main"; import * as Sentry from "@sentry/electron/main";
import { appConfig } from "@/config/env"; 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 * Initialise main-process error handling. Sentry owns uncaught-exception
* the Node side and the crash reporter. Safe to call before `app.whenReady`. * 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 { export function initSentryMain(): void {
if (!appConfig.sentryDsn) return; if (appConfig.sentryDsn) {
Sentry.init({
dsn: appConfig.sentryDsn,
tracesSampleRate: 0,
release: app.getVersion(),
});
Sentry.init({ installErrorSinks({
dsn: appConfig.sentryDsn, capture: (err, context) =>
tracesSampleRate: 0, Sentry.captureException(err, { extra: context }),
release: app.getVersion(), 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" });
}); });
process.on("unhandledRejection", (reason) => {
installErrorSinks({ reportError(reason, { scope: "unhandledRejection" });
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,
}),
}); });
} }