From 6529e432cb0aecb66c9984f7acdf0fc920f22127 Mon Sep 17 00:00:00 2001 From: Amr Gaber Date: Sun, 28 Jun 2026 18:47:30 -0500 Subject: [PATCH] fix: harden against stale chunks, Sentry noise, and a floating auth promise MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - main.tsx: add a `vite:preloadError` listener that prompts a one-shot "new version available" toast (sonner) when a deploy replaces a chunk an open tab is importing. Prompt rather than force-reload — `defaultPreload: "intent"` fires the event on hover, and a forced reload would discard unsaved work. - lib/sentry.ts: add `beforeSend` + `ignoreErrors`/`denyUrls` to drop never-actionable noise (intentional AbortError cancellation, browser extensions, translating proxies, sandboxed storage) so it doesn't bury real errors or burn quota. - lib/auth.tsx: guard the fire-and-forget `checkAuth()` with `.catch` reporting to Sentry (it already catches internally; this prevents a future refactor from leaking an unhandled rejection). --- src/lib/auth.tsx | 6 +++++- src/lib/sentry.ts | 29 +++++++++++++++++++++++++++++ src/main.tsx | 24 ++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 1 deletion(-) diff --git a/src/lib/auth.tsx b/src/lib/auth.tsx index 83dbd52..3b0fe93 100644 --- a/src/lib/auth.tsx +++ b/src/lib/auth.tsx @@ -9,6 +9,7 @@ import { } from "react" import { api, setOnUnauthorized } from "@/api/api" import { resetAnalytics } from "@/lib/analytics" +import { captureException } from "@/lib/sentry" import { clearCachedConsents, fetchConsents, @@ -105,7 +106,10 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { }, [clearState]) useEffect(() => { - checkAuth() + // checkAuth handles its own errors internally; this .catch is a guard so a + // future refactor can't leak an unhandled rejection (a bare `void` would + // silence the floating-promise lint but not a real runtime rejection). + checkAuth().catch((error: unknown) => captureException(error)) }, [checkAuth]) useEffect(() => { diff --git a/src/lib/sentry.ts b/src/lib/sentry.ts index 5f1cbf9..bc63a1c 100644 --- a/src/lib/sentry.ts +++ b/src/lib/sentry.ts @@ -48,6 +48,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 7f5c960..71167f1 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -3,6 +3,7 @@ import { RouterProvider, createRouter } from "@tanstack/react-router" import { StrictMode } from "react" import { createRoot } from "react-dom/client" import { reactErrorHandler } from "@sentry/react" +import { toast } from "sonner" // @ts-expect-error -- fontsource CSS-only import, no types import "@fontsource-variable/geist" import { BrandLockup } from "./components/brand-lockup" @@ -16,6 +17,29 @@ import { AuthProvider, useAuth } from "./lib/auth" 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, 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() const router = createRouter({