mirror of
https://github.com/supabase/supabase.git
synced 2026-09-06 01:49:12 +08: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>
137 lines
5.4 KiB
TypeScript
137 lines
5.4 KiB
TypeScript
import type { AnyRouter } from '@tanstack/react-router'
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
const sentryMocks = vi.hoisted(() => ({
|
|
init: vi.fn(),
|
|
tanstackRouterBrowserTracingIntegration: vi.fn(() => ({
|
|
name: 'TanStackRouterBrowserTracing',
|
|
})),
|
|
// Imported at module scope by lib/sentry-client-options.ts, so the mock
|
|
// must provide it even though the TanStack init never enables it.
|
|
thirdPartyErrorFilterIntegration: vi.fn(() => ({ name: 'ThirdPartyErrorsFilter' })),
|
|
}))
|
|
|
|
vi.mock('@sentry/react', () => sentryMocks)
|
|
|
|
// The integration only needs a router reference to hook navigation events, and
|
|
// it is mocked here — a stub stands in for the real router at this boundary.
|
|
const fakeRouter = { subscribe: vi.fn() } as unknown as AnyRouter
|
|
|
|
// sentry.tanstack.ts keeps a module-level `initialized` flag, so each test
|
|
// imports a fresh copy of the module.
|
|
async function loadInitializer() {
|
|
vi.resetModules()
|
|
const { initSentryTanStackClient } = await import('./sentry.tanstack')
|
|
return initSentryTanStackClient
|
|
}
|
|
|
|
describe('initSentryTanStackClient', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals()
|
|
vi.unstubAllEnvs()
|
|
})
|
|
|
|
it('does not initialize Sentry during SSR/prerender (no window)', async () => {
|
|
const initSentryTanStackClient = await loadInitializer()
|
|
vi.stubGlobal('window', undefined)
|
|
|
|
initSentryTanStackClient(fakeRouter)
|
|
|
|
expect(sentryMocks.init).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('initializes Sentry in the browser with the shared client options', async () => {
|
|
vi.stubEnv('NEXT_PUBLIC_SENTRY_DSN', 'https://public@sentry.example.com/1')
|
|
const initSentryTanStackClient = await loadInitializer()
|
|
|
|
initSentryTanStackClient(fakeRouter)
|
|
|
|
expect(sentryMocks.init).toHaveBeenCalledTimes(1)
|
|
expect(sentryMocks.init).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
dsn: 'https://public@sentry.example.com/1',
|
|
tracesSampleRate: 0.02,
|
|
})
|
|
)
|
|
})
|
|
|
|
it('passes an undefined dsn when NEXT_PUBLIC_SENTRY_DSN is unset (disabled-client no-op)', async () => {
|
|
vi.stubEnv('NEXT_PUBLIC_SENTRY_DSN', undefined)
|
|
const initSentryTanStackClient = await loadInitializer()
|
|
|
|
initSentryTanStackClient(fakeRouter)
|
|
|
|
// `Sentry.init` without a dsn creates a disabled client, so calling init
|
|
// unconditionally is safe for local/self-hosted builds.
|
|
expect(sentryMocks.init).toHaveBeenCalledTimes(1)
|
|
expect(sentryMocks.init).toHaveBeenCalledWith(expect.objectContaining({ dsn: undefined }))
|
|
})
|
|
|
|
it('only initializes once across repeated calls', async () => {
|
|
const initSentryTanStackClient = await loadInitializer()
|
|
|
|
initSentryTanStackClient(fakeRouter)
|
|
initSentryTanStackClient(fakeRouter)
|
|
|
|
expect(sentryMocks.init).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('still initializes in the browser after an earlier SSR call', async () => {
|
|
const initSentryTanStackClient = await loadInitializer()
|
|
|
|
// An SSR call must not trip the idempotency guard for the browser call.
|
|
vi.stubGlobal('window', undefined)
|
|
initSentryTanStackClient(fakeRouter)
|
|
expect(sentryMocks.init).not.toHaveBeenCalled()
|
|
|
|
vi.unstubAllGlobals()
|
|
initSentryTanStackClient(fakeRouter)
|
|
expect(sentryMocks.init).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('wires the TanStack Router browser tracing integration for the given router', async () => {
|
|
const initSentryTanStackClient = await loadInitializer()
|
|
|
|
initSentryTanStackClient(fakeRouter)
|
|
|
|
expect(sentryMocks.tanstackRouterBrowserTracingIntegration).toHaveBeenCalledWith(fakeRouter)
|
|
|
|
const [options] = sentryMocks.init.mock.calls[0]
|
|
// `integrations` is the function form: Sentry.init calls it with the
|
|
// default integrations (browserSession, globalHandlers, …) and installs
|
|
// whatever it returns, so the defaults must survive the merge.
|
|
expect(options.integrations).toBeTypeOf('function')
|
|
const defaultIntegrations = [{ name: 'BrowserSession' }, { name: 'GlobalHandlers' }]
|
|
const integrations = options.integrations(defaultIntegrations)
|
|
|
|
// Defaults passed in by Sentry.init survive the merge.
|
|
expect(integrations).toContainEqual({ name: 'BrowserSession' })
|
|
expect(integrations).toContainEqual({ name: 'GlobalHandlers' })
|
|
expect(integrations).toContainEqual({ name: 'TanStackRouterBrowserTracing' })
|
|
// The Vite build runs no Sentry bundler plugin, so frames carry no
|
|
// applicationKey metadata — the third-party filter must stay off or every
|
|
// event would be tagged third_party_code=true and dropped by beforeSend.
|
|
expect(sentryMocks.thirdPartyErrorFilterIntegration).not.toHaveBeenCalled()
|
|
expect(integrations).not.toContainEqual({ name: 'ThirdPartyErrorsFilter' })
|
|
})
|
|
|
|
it('passes the Vercel commit SHA as the release so session envelopes are sent', async () => {
|
|
// The SDK silently drops session envelopes when the client has no release
|
|
// (`Client.sendSession` early-returns) — without this, Release Health
|
|
// sends no /envelope traffic at all on the TanStack build. The Next build
|
|
// instead gets its release injected at build time by withSentryConfig.
|
|
vi.stubEnv('NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA', 'abc123commit')
|
|
const initSentryTanStackClient = await loadInitializer()
|
|
|
|
initSentryTanStackClient(fakeRouter)
|
|
|
|
expect(sentryMocks.init).toHaveBeenCalledWith(
|
|
expect.objectContaining({ release: 'abc123commit' })
|
|
)
|
|
})
|
|
})
|