mirror of
https://github.com/supabase/supabase.git
synced 2026-10-08 19:05:06 +03:00
Stacked on #47657 (base is `alaister/tanstack-migration-fixes`; retarget to `master` once that merges). The TanStack runtime never ran `Sentry.init` — `instrumentation-client.ts` is a Next-convention file nothing imports under TanStack Start, so every `Sentry.captureException` on that build (including the `routes/__root.tsx` error-boundary / `routerErrorComponent` reports) was a silent no-op. - **Shared config source**: the entire client config moves verbatim from `instrumentation-client.ts` into `lib/sentry-client-options.ts` (`buildSentryClientOptions`). Both runtimes build from it, so Next and TanStack can't drift — the builds differ only in two explicit knobs. - **TanStack init**: `sentry.tanstack.ts` initializes `@sentry/react` from `getRouter()` (TanStack Start's real client bootstrap — the earliest point with the router instance), wiring `tanstackRouterBrowserTracingIntegration(router)`. Window-guarded + idempotent; `router.tsx` is TanStack-only so the Next build is untouched. (Named without `.client.` — Start's import-protection fails the build for `*.client.*` in the server graph.) - **Third-party error filter is intentionally Next-only**: without the bundler-injected `applicationKey` metadata (only `withSentryConfig` provides it), the SDK tags *every* event `third_party_code: true` and `beforeSend` would drop them all — recreating the silent no-op with a DSN set. Follow-up: add `@sentry/vite-plugin` moduleMetadata, then enable. - **DSN-less builds stay crash-free**: `vite.config.ts` inlines `undefined` for unset `NEXT_PUBLIC_SENTRY_DSN`/`NEXT_PUBLIC_SENTRY_ENVIRONMENT` (a literal `process.env.*` in the bundle is the exact `process is not defined` class #47657 fixed). No-DSN → disabled client, plus the existing `IS_PLATFORM`/consent gates. - Tests: `instrumentation-client.test.ts` moved to `lib/sentry-client-options.test.ts` with all 36 assertions kept, plus integration-gating and Next/TanStack parity tests. `tsc` clean; full `vite build --mode test` passes. Follow-up (separate): server-side Sentry for the Start handler (`server.ts` entry + `@sentry/node`-style init). ## To test - **Locally (no DSN set)**: load the TanStack build — no Sentry network requests, no console errors, and crucially no `ReferenceError: process is not defined` (the define fallback). Forcing an error must not POST to any `/envelope` endpoint. - **On a preview/deploy (DSN set, telemetry consent accepted)**: throw a test error (e.g. crash a route component) → a POST to `o…ingest.sentry.io/api/…/envelope/` fires, and the event lands in Sentry with a `codeSampleRate` tag and **no** `third_party_code` tag. Navigation spans named after TanStack routes appear when the 2% pageload trace samples in. - **Next build regression check**: the Next dev/preview still reports errors exactly as before (`instrumentation-client.ts` now builds its options from the same shared source). --- ### Review feedback: Sentry `/envelope` never fires on TanStack (Joshen) Root-caused: `@sentry/core`'s `Client.sendSession` silently drops the session when the client has no `release`. The Next build gets a release injected by `withSentryConfig` (the Vercel commit SHA); the Vite build runs no Sentry bundler plugin, so it had no release → session envelopes were discarded before transport → zero `/envelope` traffic (errors/transactions are separate). Fix: inject `release: NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA` on the TanStack build (vite.config re-exposes `VERCEL_GIT_COMMIT_SHA` under the `NEXT_PUBLIC_` name, same SHA the Next release resolves to). Also switched `integrations` to the function form so defaults are preserved by contract (not just by current SDK behavior). 45 unit tests green. **To test (deploys only — the SHA is unset locally, so this can't be reproduced on a local dev build):** on this PR's Vercel preview with a DSN + telemetry consent, load any page and watch the Network tab for a POST to `…ingest.sentry.io/…/envelope/` — a session envelope should now fire on load, matching the Next build. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Improved client-side error and performance monitoring for the Studio app across both router setups. * Added support for passing release/version information into monitoring data. * **Bug Fixes** * Reduced noisy error reporting by better filtering common browser, extension, cancellation, and load-related issues. * Prevented browser bundles from referencing missing environment values at runtime. * Made monitoring initialization safer in server-rendered and client-only environments. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Alaister Young <10985857+alaister@users.noreply.github.com>
112 lines
4.6 KiB
TypeScript
112 lines
4.6 KiB
TypeScript
import type { QueryClient } from '@tanstack/react-query'
|
|
import { createRouter } from '@tanstack/react-router'
|
|
import { setupRouterSsrQueryIntegration } from '@tanstack/react-router-ssr-query'
|
|
|
|
import { routeTree } from './routeTree.gen'
|
|
import { initSentryTanStackClient } from './sentry.tanstack'
|
|
import { getQueryClient } from '@/data/query-client'
|
|
import { BASE_PATH, IS_PLATFORM } from '@/lib/constants'
|
|
import { parseSearch, stringifySearch } from '@/lib/router-search-params'
|
|
|
|
export interface RouterContext {
|
|
queryClient: QueryClient
|
|
}
|
|
|
|
// Skew protection: pin this browser session to the deployment that served it,
|
|
// so a long-lived dashboard session never 404s on a lazily-loaded JS chunk or
|
|
// gets a surprise version change mid-session. It's a session cookie (no
|
|
// expiry) — cleared when the tab/window closes, so the next visit gets the
|
|
// latest deploy. Scoped to BASE_PATH so the version check (hit at the root
|
|
// `/api/...`, outside this Path) stays unpinned and can still detect new
|
|
// deploys; the "Refresh" toast clears it before reloading (see
|
|
// use-check-latest-deploy). No-op unless we're on a Vercel deploy with Skew
|
|
// Protection enabled.
|
|
function pinDeploymentForSession() {
|
|
if (typeof document === 'undefined') return
|
|
if (!IS_PLATFORM) return
|
|
if (process.env.NEXT_PUBLIC_VERCEL_SKEW_PROTECTION_ENABLED !== '1') return
|
|
const deploymentId = process.env.NEXT_PUBLIC_VERCEL_DEPLOYMENT_ID
|
|
if (!deploymentId) return
|
|
if (document.cookie.includes('__vdpl=')) return
|
|
document.cookie = `__vdpl=${deploymentId}; Path=${BASE_PATH || '/'}; SameSite=Lax; Secure`
|
|
}
|
|
|
|
// Backstop for the pin above: if a lazily-loaded chunk 404s — most likely the
|
|
// pinned deployment aged out of Skew Protection's Maximum Age, so its hashed
|
|
// chunks are gone — Vite emits `vite:preloadError`. Drop the pin and reload so
|
|
// we land on the latest deployment (a plain reload wouldn't recover, since the
|
|
// cookie would just re-pin to the dead deployment). A short time-window guard
|
|
// prevents a reload loop if the latest deployment is itself broken.
|
|
function registerChunkErrorBackstop() {
|
|
if (typeof window === 'undefined') return
|
|
window.addEventListener('vite:preloadError', (event) => {
|
|
const KEY = 'studio:chunk-error-reload-at'
|
|
let last = 0
|
|
try {
|
|
last = Number(sessionStorage.getItem(KEY) || 0)
|
|
} catch {
|
|
// sessionStorage unavailable — fall through and attempt a reload anyway.
|
|
}
|
|
// Reloaded very recently → likely a loop; let Vite surface the error.
|
|
if (Date.now() - last < 10_000) return
|
|
event.preventDefault()
|
|
try {
|
|
sessionStorage.setItem(KEY, String(Date.now()))
|
|
} catch {
|
|
// ignore — worst case we lose loop protection for this reload.
|
|
}
|
|
document.cookie = `__vdpl=; Path=${BASE_PATH || '/'}; Max-Age=0`
|
|
window.location.reload()
|
|
})
|
|
}
|
|
|
|
function getContext(): RouterContext {
|
|
return {
|
|
queryClient: getQueryClient(),
|
|
}
|
|
}
|
|
|
|
export function getRouter() {
|
|
pinDeploymentForSession()
|
|
registerChunkErrorBackstop()
|
|
|
|
const context = getContext()
|
|
|
|
const router = createRouter({
|
|
routeTree,
|
|
context,
|
|
scrollRestoration: true,
|
|
defaultPreload: 'intent',
|
|
// Next-style search params (plain strings, repeated keys → arrays)
|
|
// instead of TanStack's JSON defaults, which coerce "2"→2/"true"→true
|
|
// and JSON-quote strings on write. The whole app — including the
|
|
// next/router compat shim and nuqs — expects the Next semantics.
|
|
parseSearch,
|
|
stringifySearch,
|
|
// Inlined via Vite's `define` at build time; stays undefined (= app at `/`)
|
|
// unless NEXT_PUBLIC_BASE_PATH is set. Must agree with Vite `base`
|
|
basepath: process.env.NEXT_PUBLIC_BASE_PATH || undefined,
|
|
})
|
|
|
|
// Sentry: nothing loads Next's convention files (instrumentation-client.ts)
|
|
// under TanStack Start, so init happens here — the earliest point with
|
|
// access to the router instance, which the tracing integration needs.
|
|
// No-op on the server and when no DSN is configured (see module).
|
|
initSentryTanStackClient(router)
|
|
|
|
// @tanstack/react-router-ssr-query@1.166.12 pulls in @tanstack/query-core@5.100
|
|
// as a peer, but our app pins react-query to 5.83. The QueryClient class is
|
|
// structurally identical between the two, but TS treats them as nominally
|
|
// distinct types because each version has its own `#private` field.
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
setupRouterSsrQueryIntegration({ router, queryClient: context.queryClient as any })
|
|
|
|
return router
|
|
}
|
|
|
|
declare module '@tanstack/react-router' {
|
|
interface Register {
|
|
router: ReturnType<typeof getRouter>
|
|
}
|
|
}
|