Files
supabase/apps/studio/lib/error-reporting.ts
Jordi Enric ec26943390 feat: improve db overload debugging UX (#43564)
When the dashboard hits a DB connection timeout, users currently see a
raw error message with no
path forward. This PR adds an inline troubleshooting system that detects
known error types and
surfaces contextual next steps — restart the DB, read the docs, or debug
with AI.

##  Changes

- New ErrorDisplay component (packages/ui-patterns) — styled error card
with a title, monospace error
block, optional troubleshooting slot, and a "Contact support" link that
always renders. Accepts
  typed supportFormParams to pre-fill the support form.

- Error classification in handleError (data/fetchers.ts) — on every API
error, the message is tested
against ERROR_PATTERNS. If matched, handleError throws a typed subclass
(ConnectionTimeoutError
extends ResponseError) instead of a plain ResponseError. Stack traces
now show the exact error
  class. All existing instanceof ResponseError checks continue to work.

- ErrorMatcher component — reads errorType from the thrown class
instance, does an O(1) lookup into
ERROR_MAPPINGS, and renders the matching troubleshooting accordion as
children of ErrorDisplay.
  Falls back to plain ErrorDisplay for unclassified errors.

- Connection timeout mapping — first error type wired up, with three
troubleshooting steps: restart
the database, link to the docs, and "Debug with AI" (opens the AI
assistant sidebar with a
  pre-filled prompt).

- Telemetry — three new typed events track when the troubleshooter is
shown, when accordion steps are
   toggled, and which CTAs are clicked.

##  Adding a new error type

  1. Add a class to types/api-errors.ts
  2. Add { pattern, ErrorClass } to data/error-patterns.ts
  3. Create a troubleshooting component in errorMappings/
  4. Add an entry to error-mappings.tsx
2026-03-16 11:22:30 +01:00

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
}
handleUnknownAPIResponseError(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 handleUnknownAPIResponseError(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)
})
}