mirror of
https://github.com/supabase/supabase.git
synced 2026-05-23 01:39:34 +08:00
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
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
|
|
}
|
|
|
|
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)
|
|
})
|
|
}
|