Files
supabase/apps/studio/data/error-patterns.test.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

63 lines
2.1 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import { ERROR_PATTERNS } from './error-patterns'
// Representative sample messages for each error class.
// Keep this in sync when adding new patterns — the test will fail if you don't.
const PATTERN_SAMPLES: Record<string, { matches: string[]; nonMatches: string[] }> = {
ConnectionTimeoutError: {
matches: [
'CONNECTION TERMINATED DUE TO CONNECTION TIMEOUT',
'connection terminated due to connection timeout',
'ERROR: FAILED TO RUN SQL QUERY: CONNECTION TERMINATED DUE TO CONNECTION TIMEOUT.',
'Connection Terminated Due To Connection Timeout', // extra whitespace
],
nonMatches: [
'connection timeout',
'connection terminated',
'query timed out',
'idle connection timeout',
'',
],
},
}
describe('ERROR_PATTERNS registry', () => {
it('has a PATTERN_SAMPLES entry for every registered pattern (keep samples in sync)', () => {
for (const { ErrorClass } of ERROR_PATTERNS) {
expect(
PATTERN_SAMPLES,
`Add a PATTERN_SAMPLES entry for '${ErrorClass.name}'`
).toHaveProperty(ErrorClass.name)
}
})
describe('per-pattern match correctness', () => {
for (const { ErrorClass, pattern } of ERROR_PATTERNS) {
const samples = PATTERN_SAMPLES[ErrorClass.name]
if (!samples) continue
describe(ErrorClass.name, () => {
it.each(samples.matches)('matches: %s', (msg) => {
expect(pattern.test(msg)).toBe(true)
})
it.each(samples.nonMatches)('does not match: %s', (msg) => {
expect(pattern.test(msg)).toBe(false)
})
})
}
})
describe('no message matches more than one pattern', () => {
const allSamples = Object.entries(PATTERN_SAMPLES).flatMap(([className, { matches }]) =>
matches.map((msg) => ({ msg, sourceClass: className }))
)
it.each(allSamples)('$sourceClass sample "$msg" matches exactly one pattern', ({ msg }) => {
const matched = ERROR_PATTERNS.filter(({ pattern }) => pattern.test(msg))
expect(matched.length).toBe(1)
})
})
})