diff --git a/apps/docs/app/error.tsx b/apps/docs/app/error.tsx index 716873821f0..3c3392fa749 100644 --- a/apps/docs/app/error.tsx +++ b/apps/docs/app/error.tsx @@ -7,7 +7,7 @@ import { Button } from 'ui' const ErrorPage = ({ error }) => { useEffect(() => { - Sentry.captureException(error) + Sentry.captureException(error, { tags: { globalErrorBoundary: true } }) }, [error]) return ( diff --git a/apps/docs/app/global-error.tsx b/apps/docs/app/global-error.tsx index 52071703471..60e69040c0f 100644 --- a/apps/docs/app/global-error.tsx +++ b/apps/docs/app/global-error.tsx @@ -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 ( diff --git a/apps/docs/instrumentation-client.ts b/apps/docs/instrumentation-client.ts index c51465428fc..52afede50c1 100644 --- a/apps/docs/instrumentation-client.ts +++ b/apps/docs/instrumentation-client.ts @@ -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 diff --git a/apps/docs/lib/sentry-client.test.ts b/apps/docs/lib/sentry-client.test.ts new file mode 100644 index 00000000000..ba7529af432 --- /dev/null +++ b/apps/docs/lib/sentry-client.test.ts @@ -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() + } + ) +}) diff --git a/apps/docs/lib/sentry-client.ts b/apps/docs/lib/sentry-client.ts new file mode 100644 index 00000000000..5105425c2eb --- /dev/null +++ b/apps/docs/lib/sentry-client.ts @@ -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 +} diff --git a/apps/docs/next.config.mjs b/apps/docs/next.config.mjs index edafab3c420..1f522ef0604 100644 --- a/apps/docs/next.config.mjs +++ b/apps/docs/next.config.mjs @@ -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, diff --git a/apps/docs/turbo.jsonc b/apps/docs/turbo.jsonc index 3bd522d3235..187126ea25b 100644 --- a/apps/docs/turbo.jsonc +++ b/apps/docs/turbo.jsonc @@ -85,6 +85,7 @@ "SITE_NAME", "SUPABASE_SECRET_KEY", ], + "passThroughEnv": ["SENTRY_AUTH_TOKEN"], "inputs": ["$TURBO_DEFAULT$"], "outputs": [".next/**", "!.next/cache/**"], },