Files
supabase/apps/studio/sentry.tanstack.ts
Alaister Young 74bc0a8e27 fix(studio): initialize Sentry on the TanStack build (captures were silent no-ops) (#47666)
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>
2026-07-09 18:41:03 +08:00

67 lines
3.5 KiB
TypeScript

// Sentry client init for the TanStack Start (Vite) build.
//
// NOTE: deliberately not named `sentry.client.tanstack.ts` — TanStack Start's
// import-protection denies `**/*.client.*` modules in the server bundle, and
// this module is imported from router.tsx (shared between client and server).
// It is isomorphic by design: the `typeof window` guard below makes it a
// no-op on the server.
//
// The Next build initializes Sentry via instrumentation-client.ts — a Next
// convention file that nothing loads under TanStack Start. Without this init
// every `Sentry.captureException` in the TanStack runtime (including the
// globalErrorBoundary / routerErrorComponent captures in routes/__root.tsx)
// would be a silent no-op.
//
// Called from `getRouter()` (router.tsx) — the earliest point in the TanStack
// client bootstrap with access to the router instance, which
// `tanstackRouterBrowserTracingIntegration` needs at init time so the
// pageload span is captured, not just later navigations.
//
// Imports `@sentry/react` directly (not `@sentry/nextjs`): this module never
// runs on the Next build, and the real `@sentry/nextjs` doesn't export the
// TanStack Router integration. Under Vite both ids resolve to the same
// `@sentry/react` instance anyway (vite.config.ts aliases `@sentry/nextjs`
// to compat/sentry-nextjs.ts), so app code capturing via `@sentry/nextjs`
// reports through the client initialized here.
import * as Sentry from '@sentry/react'
import type { AnyRouter } from '@tanstack/react-router'
import { buildSentryClientOptions } from '@/lib/sentry-client-options'
let isInitialized = false
export function initSentryTanStackClient(router: AnyRouter) {
// Client-only: getRouter() also runs during SSR/prerender, and the TanStack
// build has no server-side Sentry story yet (the Next build's
// sentry.server.config.ts equivalent would live in a custom server entry).
if (typeof window === 'undefined') return
// getRouter() is called once per pageload today; keep the guard so a future
// second call can't double-init the client.
if (isInitialized) return
isInitialized = true
// No-ops cleanly when NEXT_PUBLIC_SENTRY_DSN is unset (local/self-hosted):
// `init` without a dsn creates a disabled client, and beforeSend drops
// everything when !IS_PLATFORM regardless.
Sentry.init(
buildSentryClientOptions({
// The Vite build doesn't run a Sentry bundler plugin, so stack frames
// carry no `supabase-studio` applicationKey metadata. Without the
// metadata the integration would tag EVERY event third_party_code=true
// and beforeSend would drop them all. Leave it off until the Vite build
// annotates frames (@sentry/vite-plugin moduleMetadata).
includeThirdPartyErrorFilter: false,
extraIntegrations: [Sentry.tanstackRouterBrowserTracingIntegration(router)],
// Without a release the SDK silently drops session envelopes
// (`Client.sendSession` early-returns), so Release Health sends nothing
// on this build. The Next build gets its release injected at build time
// by withSentryConfig, which resolves to the Vercel commit SHA; inline
// the same SHA here (vite.config.ts re-exposes VERCEL_GIT_COMMIT_SHA
// under the NEXT_PUBLIC_ name) so both builds report the same release.
// Unset outside Vercel (local/self-hosted), where sessions don't matter —
// so session envelopes only fire on deploys, not on a local dev build.
release: process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA,
})
)
}