mirror of
https://github.com/supabase/supabase.git
synced 2026-07-01 04:14:25 +08:00
Sentry was flooded with un-actionable events: browser extension errors,
network blips, React hydration
noise from DOM-manipulating extensions, and CriticalError issues that
were impossible to route because
they grouped by message string rather than type. This made it hard to
spot real regressions.
## Changes:
- Improved client-side Sentry signal quality in
apps/studio/instrumentation-client.ts by replacing
custom third-party stack filtering with
Sentry.thirdPartyErrorFilterIntegration, plus stricter
allowUrls filtering for Supabase/app frames.
- Added build-time Sentry bundle annotation in
apps/studio/next.config.js via
unstable_sentryWebpackPluginOptions.applicationKey = 'supabase-studio'
to support reliable third-party
frame filtering.
- Expanded and reorganized ignoreErrors rules across client/server/edge
Sentry configs to suppress known
non-actionable noise (Next.js navigation internals, network/transient
chunk failures, extension/DOM-
manipulation noise, hydration-noise patterns).
- Refactored critical error reporting in
apps/studio/lib/error-reporting.ts from captureMessage to
captureException with scoped tags (critical=true, context=<action>) and
synthetic CriticalError
exceptions for better alerting/grouping.
- Updated tests in apps/studio/lib/error-reporting.test.ts to match the
new Sentry API usage (withScope
+ captureException) and assert on exception objects/tags behavior.
- 100% sampling (codeSampleRate = 1) for normal/useful errors.
- 1% sampling (codeSampleRate = 0.01) only for explicitly noisy classes:
- Failed to construct 'URL': Invalid URL
- Session error detected
- chunk-load failures (ChunkLoadError, Loading chunk ... failed, Loading
CSS chunk ... failed)
- Sent events are tagged with codeSampleRate so you can filter/segment
in Sentry dashboards.
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
117 lines
3.4 KiB
TypeScript
117 lines
3.4 KiB
TypeScript
import * as Sentry from '@sentry/nextjs'
|
|
import { ResponseError } from 'types'
|
|
|
|
type CaptureMessageOptions = {
|
|
context: string
|
|
message: string
|
|
}
|
|
|
|
const WHITELIST_ERRORS = [
|
|
// Common validation errors
|
|
'email must be an email',
|
|
'Password is known to be weak and easy to guess, please choose a different one',
|
|
// Authentication errors
|
|
'A user with this email already exists',
|
|
'Password should contain at least one character of each',
|
|
'You attempted to send email to an inactive recipient',
|
|
'New password should be different from the old password',
|
|
'Invalid TOTP code entered',
|
|
'No SSO provider assigned for this domain',
|
|
// Project creation errors
|
|
'The following organization members have reached their maximum limits for the number of active free projects',
|
|
'db_pass must be longer than or equal to 4 characters',
|
|
'There are overdue invoices in the organization(s)',
|
|
'name should not contain a . string',
|
|
'Project creation in the Supabase dashboard is disabled for this Vercel-managed organization.',
|
|
'Your account, which is handled by the Fly Supabase extension, cannot access this endpoint.',
|
|
'already exists in your organization.',
|
|
]
|
|
|
|
/**
|
|
* Captures a critical error to Sentry, filtering out whitelisted errors.
|
|
*
|
|
* @param error - The error object (ResponseError, Error, or any object with a message property)
|
|
* @param context - The context/action that failed (e.g., 'reset password', 'sign up', 'create project')
|
|
* Attached as the `context` tag on the Sentry event.
|
|
*/
|
|
export function captureCriticalError(
|
|
error: ResponseError | Error | { message: string },
|
|
context: string
|
|
): void {
|
|
if (!error.message) {
|
|
return
|
|
}
|
|
|
|
if (error instanceof ResponseError) {
|
|
handleResponseError(error, context)
|
|
return
|
|
}
|
|
if (error instanceof Error) {
|
|
handleError(error, context)
|
|
return
|
|
}
|
|
|
|
handleUnknownError(error, context)
|
|
}
|
|
|
|
function handleResponseError(error: ResponseError, context: string) {
|
|
const { code, message, requestPathname } = error
|
|
if (!requestPathname || !code) {
|
|
captureMessage({
|
|
message: `Response Error (no code or requestPathname) w/ message: ${error.message}`,
|
|
context,
|
|
})
|
|
return
|
|
}
|
|
|
|
if (code >= 500) {
|
|
// Only capture 5XX errors as critical errors
|
|
captureMessage({
|
|
context,
|
|
message: `requestPathname ${requestPathname} w/ message: ${message}`,
|
|
})
|
|
return
|
|
}
|
|
}
|
|
|
|
function handleError(error: Error, context: string) {
|
|
if (!error.message) {
|
|
return
|
|
}
|
|
|
|
captureMessage({
|
|
message: error.message,
|
|
context,
|
|
})
|
|
}
|
|
|
|
function handleUnknownError(error: unknown, context: string) {
|
|
if (
|
|
error &&
|
|
typeof error === 'object' &&
|
|
'message' in error &&
|
|
typeof error.message === 'string'
|
|
) {
|
|
captureMessage({
|
|
message: error.message,
|
|
context,
|
|
})
|
|
}
|
|
}
|
|
|
|
function captureMessage({ message, context }: CaptureMessageOptions) {
|
|
if (WHITELIST_ERRORS.some((whitelisted) => message.includes(whitelisted))) {
|
|
return
|
|
}
|
|
// Use captureException (vs captureMessage) so these appear as exceptions in Sentry
|
|
// and can have dedicated alert rules. Grouping is still by message since all
|
|
// CriticalErrors share the same synthetic stack trace.
|
|
Sentry.withScope((scope) => {
|
|
scope.setTag('critical', 'true')
|
|
scope.setTag('context', context)
|
|
const error = new Error(message)
|
|
error.name = `CriticalError`
|
|
Sentry.captureException(error)
|
|
})
|
|
}
|