mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 01:15:03 +03:00
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:
1 parent
74daa990bc
commit
15f80e5f9d
7 files changed
+99
-32
No files matched your search
@@ -7,7 +7,7 @@ import { Button } from 'ui'
|
||||
|
||||
const ErrorPage = ({ error }) => {
|
||||
useEffect(() => {
|
||||
Sentry.captureException(error)
|
||||
Sentry.captureException(error, { tags: { globalErrorBoundary: true } })
|
||||
}, [error])
|
||||
|
||||
return (
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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
|
||||
@@ -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()
|
||||
}
|
||||
)
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -85,6 +85,7 @@
|
||||
"SITE_NAME",
|
||||
"SUPABASE_SECRET_KEY",
|
||||
],
|
||||
"passThroughEnv": ["SENTRY_AUTH_TOKEN"],
|
||||
"inputs": ["$TURBO_DEFAULT$"],
|
||||
"outputs": [".next/**", "!.next/cache/**"],
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user