diff --git a/src/lib/sentry.ts b/src/lib/sentry.ts index ff9a12f..dccca5c 100644 --- a/src/lib/sentry.ts +++ b/src/lib/sentry.ts @@ -47,6 +47,35 @@ export function initSentry(router: SentryRouter) { replaysOnErrorSampleRate: 1.0, enableLogs: true, + + // Drop noise that's never actionable, so it doesn't bury real errors or + // burn quota: intentional fetch cancellation plus errors injected by the + // user's environment rather than our code. + ignoreErrors: [ + // Intentional fetch/query cancellation. + "AbortError", + "The user aborted a request", + "signal is aborted without reason", + // Benign layout-loop notices the browser emits under load. + "ResizeObserver loop limit exceeded", + "ResizeObserver loop completed with undelivered notifications", + // Translating proxies (Yandex/Google Translate) rewrite the DOM and trip the History API. + "Failed to execute 'replaceState' on 'History'", + // Sandboxed-iframe / privacy-mode storage access denied. + "The operation is insecure", + ], + denyUrls: [ + /^chrome-extension:\/\//i, + /^moz-extension:\/\//i, + /^safari-web-extension:\/\//i, + ], + beforeSend(event, hint) { + // Intentional cancellation (a query/fetch aborted mid-flight) is expected + // noise — never report it, however the SDK wrapped it. + const exception = hint.originalException as { name?: string } | undefined + if (exception?.name === "AbortError") return null + return event + }, }) initialized = true diff --git a/src/main.tsx b/src/main.tsx index ff6a496..9d221a4 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -23,6 +23,30 @@ import { FeatureFlagProvider } from "./lib/feature-flags" import { initSentry } from "./lib/sentry" import { routeTree } from "./routeTree.gen" +// A `vite:preloadError` fires when a dynamic import for a code-split chunk +// fails — almost always because a newer build was deployed and this tab is now +// stale (it references content-hashed chunk filenames the deploy has replaced). +// Prompt to reload rather than force-reloading: a forced reload discards unsaved +// work (e.g. a half-filled login/registration form), and because the router uses +// `defaultPreload: "intent"` this event also fires for speculative preloads +// (hovering a stale link), where auto-reloading would be jarring. +// `preventDefault()` stops Vite re-throwing the failed import. Shown once so a +// burst of failures can't stack it. +let updateToastShown = false +window.addEventListener("vite:preloadError", (event) => { + event.preventDefault() + if (updateToastShown) return + updateToastShown = true + toast.info("A new version is available", { + description: "Reload to get the latest update.", + duration: Infinity, + action: { + label: "Reload", + onClick: () => window.location.reload(), + }, + }) +}) + const queryClient = new QueryClient({ defaultOptions: { queries: {