fix(docs): restore browser crash reporting to sentry (#50231)

## Problem

Docs discarded every browser exception because its third-party
stack-frame filter never returned a value from its predicate. Errors
captured by the page error boundaries were discarded too.

## Fix

Use Studio's Sentry SDK tagging approach with a matching webpack
application key, retaining page-crashing exceptions even when their
frames are classified as third-party. Preserve consent and platform
checks, and pass the source-map upload token through Turbo.

## How to test

- Run `pnpm --filter docs run test:local:unwatch
lib/sentry-client.test.ts` with the documented local Supabase
prerequisites satisfied. Eleven filter regression checks passed locally
using an isolated Vitest configuration.
- On a production-mode preview with the docs DSN configured, accept
telemetry consent and trigger a temporary client render error. Verify
that the docs Sentry project receives it with `globalErrorBoundary:
true` and readable stack traces.
- Verify that third-party-only errors are filtered and declining consent
suppresses browser reports.

Prettier, focused filter/test TypeScript checks, and an in-memory
transport check using the real Sentry SDK passed. Full app typecheck and
lint are blocked locally by existing dependency/generated-file drift;
the standard docs suite requires unavailable Docker access. Live Sentry
ingestion and source-map uploads still need deployment verification.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **Bug Fixes**
- Improved error monitoring to distinguish documentation app boundary
crashes from other exceptions.
- Reduced noise in error reports by filtering third-party-only errors
and respecting platform and consent settings.
- Preserved reporting for first-party failures and critical application
crashes.

- **Chores**
- Improved Sentry build and deployment configuration for more consistent
error tracking.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Jordi Enric authored and GitHub committed 2026-09-11 12:48:33 +02:00
1 parent 74daa990bc
commit 15f80e5f9d
7 files changed
+99 -32

No files matched your search

+1 -1
View File
@@ -7,7 +7,7 @@ import { Button } from 'ui'
const ErrorPage = ({ error }) => {
useEffect(() => {
Sentry.captureException(error)
Sentry.captureException(error, { tags: { globalErrorBoundary: true } })
}, [error])
return (
+1 -1
View File
@@ -6,7 +6,7 @@ import { useEffect } from 'react'
export default function GlobalError({ error }: { error: Error & { digest?: string } }) {
useEffect(() => {
Sentry.captureException(error)
Sentry.captureException(error, { tags: { globalErrorBoundary: true } })
}, [error])
return (
+11 -30
View File
@@ -4,7 +4,9 @@
import * as Sentry from '@sentry/nextjs'
import { hasConsented, IS_PLATFORM } from 'common'
import { IS_DEV } from './lib/constants'
import { filterSentryEvent } from './lib/sentry-client'
if (!IS_DEV) {
Sentry.init({
@@ -13,6 +15,14 @@ if (!IS_DEV) {
// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: false,
integrations: (defaultIntegrations) => [
...defaultIntegrations,
Sentry.thirdPartyErrorFilterIntegration({
filterKeys: ['supabase-docs'],
behaviour: 'apply-tag-if-exclusively-contains-third-party-frames',
}),
],
ignoreErrors: [
// [Charis 2025-05-05]
// We should fix hydration problems but let's not make this a blocker for
@@ -21,38 +31,9 @@ if (!IS_DEV) {
],
beforeSend(event) {
if (!IS_PLATFORM || !hasConsented()) {
return null
}
const frames = event.exception?.values?.[0].stacktrace?.frames || []
if (isThirdPartyError(frames)) {
return null
}
return event
return filterSentryEvent(event, { isPlatform: IS_PLATFORM, hasConsent: hasConsented() })
},
})
}
// We want to ignore errors not originating from docs app static files
// (such as errors from browser extensions). Those errors come from files
// not starting with 'app:///_next'.
//
// However, there is a complication because the Sentry code that sends
// the error shows up in the stack trace, and that _does_ start with
// 'app:///_next'. It is always the first frame in the stack trace,
// and has a specific pre_context comment that we can use for filtering.
function isThirdPartyError(frames: Sentry.StackFrame[] | undefined) {
if (!frames) return false
function isSentryFrame(frame: Sentry.StackFrame, index: number) {
return index === 0 && frame.pre_context?.[0]?.includes('sentry.javascript')
}
return !frames.some((frame, index) => {
frame.abs_path?.startsWith('app:///_next') && !isSentryFrame(frame, index)
})
}
export const onRouterTransitionStart = Sentry.captureRouterTransitionStart
+67
View File
@@ -0,0 +1,67 @@
import type { ErrorEvent } from '@sentry/nextjs'
import { describe, expect, it } from 'vitest'
import { filterSentryEvent } from './sentry-client'
describe('filterSentryEvent', () => {
it('forwards first-party exceptions', () => {
const event: ErrorEvent = {
type: undefined,
exception: {
values: [
{ stacktrace: { frames: [{ filename: 'https://supabase.com/docs/_next/app.js' }] } },
],
},
}
expect(filterSentryEvent(event, { isPlatform: true, hasConsent: true })).toBe(event)
})
it('forwards errors without stack frames', () => {
const event: ErrorEvent = {
type: undefined,
exception: { values: [{ value: 'Page crashed' }] },
}
expect(filterSentryEvent(event, { isPlatform: true, hasConsent: true })).toBe(event)
})
it.each([true, 'true'])('drops third-party-only errors tagged %s', (tag) => {
expect(
filterSentryEvent(
{ type: undefined, tags: { third_party_code: tag } },
{ isPlatform: true, hasConsent: true }
)
).toBeNull()
})
it.each([true, 'true'])('retains error-boundary crashes tagged %s', (tag) => {
const event: ErrorEvent = {
type: undefined,
tags: { third_party_code: true, globalErrorBoundary: tag },
}
expect(filterSentryEvent(event, { isPlatform: true, hasConsent: true })).toBe(event)
})
it('does not forward errors outside the platform', () => {
expect(
filterSentryEvent({ type: undefined }, { isPlatform: false, hasConsent: true })
).toBeNull()
})
it('retains errors explicitly tagged as first-party', () => {
const event: ErrorEvent = { type: undefined, tags: { third_party_code: false } }
expect(filterSentryEvent(event, { isPlatform: true, hasConsent: true })).toBe(event)
})
it.each([
{},
{ globalErrorBoundary: true },
{ globalErrorBoundary: 'true', third_party_code: true },
])(
'does not forward errors without permission to report, including boundary crashes: %j',
(tags) => {
expect(
filterSentryEvent({ type: undefined, tags }, { isPlatform: true, hasConsent: false })
).toBeNull()
}
)
})
+15
View File
@@ -0,0 +1,15 @@
import type { ErrorEvent } from '@sentry/nextjs'
export function filterSentryEvent(
event: ErrorEvent,
{ isPlatform, hasConsent }: { isPlatform: boolean; hasConsent: boolean }
) {
if (!isPlatform || !hasConsent) return null
const isErrorBoundaryCrash =
event.tags?.globalErrorBoundary === true || event.tags?.globalErrorBoundary === 'true'
const isThirdPartyOnly =
event.tags?.third_party_code === true || event.tags?.third_party_code === 'true'
return isThirdPartyOnly && !isErrorBoundaryCrash ? null : event
}
+3
View File
@@ -223,6 +223,9 @@ export default withSentryConfig(configExport, {
org: 'supabase',
project: 'docs',
unstable_sentryWebpackPluginOptions: {
applicationKey: 'supabase-docs',
},
// Only print logs for uploading source maps in CI
silent: !process.env.CI,
+1
View File
@@ -85,6 +85,7 @@
"SITE_NAME",
"SUPABASE_SECRET_KEY",
],
"passThroughEnv": ["SENTRY_AUTH_TOKEN"],
"inputs": ["$TURBO_DEFAULT$"],
"outputs": [".next/**", "!.next/cache/**"],
},