## Summary Since #47293, an API failure on a signup / org-creation / project-creation form emitted `dashboard_error_created` twice: `useTrackFunnelError` fired the origin-tagged event and the global `ToastErrorTracker` independently fired the legacy untagged `source:'toast'` event for the same toast, each behind its own 10% sampling draw. I verified the twin rate empirically at 8-11% of origin-tagged funnel toasts, exactly the floor for two independent 10% draws, meaning the twin co-fires for effectively every funnel error ([Hex thread](https://app.hex.tech/supabase/thread/019f3bc1-3a5c-7200-9122-8e3439bfbe8c)). Any consumer counting funnel errors without an `origin IS NOT NULL` filter saw ~2x inflation. The fix makes `ToastErrorTracker` the sole emitter of `source:'toast'` events, so the duplicate is unrepresentable rather than suppressed. Funnel call sites pass the id returned by `toast.error()` into `trackFunnelError`, which registers the funnel properties against that toast id instead of firing its own event – the tracker then emits a single `dashboard_error_created` enriched with `origin` / `errorCategory` / `errorReason` / `errorCode` for registered toasts, and the plain untagged event otherwise. The `'toast'` overload of `trackFunnelError` requires the toast id, so a missed pairing is a compile error rather than a silent double count. Registration is unconditional and there's only one sampling draw, so suppression can't lose a sampling race. `'form'`-sourced funnel events are unchanged. ## Changes - `lib/toast-errors.tsx`: toast-id → funnel-properties registry (`registerFunnelErrorToast`); `ToastErrorTracker` emits one (optionally enriched) event per error toast under a single 10% draw, deleting entries once consumed - `lib/telemetry/use-track-funnel-error.ts`: overloaded signature – `'toast'` requires the id returned by `toast.error()` (type-enforced), `'form'` keeps direct emission with its own sampling - Update the 7 funnel `toast.error` call sites in `NewOrgForm`, `SignUpForm`, and `pages/new/[slug]` to pass the toast id - Component tests for the tracker (previously uncovered), including an end-to-end test through `useTrackFunnelError` - Code hygiene (also flagged by CodeRabbit): all four `dashboard_error_created` emitters (toast, form, `AlertError`, `ErrorMatcher`) independently encoded the 10% draw – downstream analysis assumes a uniform sampling multiplier across sources, so one site drifting would silently skew comparisons. The rate and the draw now live in one place (`isDashboardErrorSampled()` in `lib/telemetry/error-sampling.ts`). No behavior change. - Mount `ToastErrorTracker` in the TanStack root (`routes/__root.tsx`), mirroring `pages/_app.tsx`. The TanStack tree mounted `Toaster` but never the tracker, so untagged toast error telemetry has never fired in that flavour – and with the tracker now the sole emitter, the missing mount would have silently dropped funnel toast events there too. Side effect once the TanStack flavour ships: untagged `source:'toast'` volume from it goes from zero to normal. ## Testing Component-tested (`apps/studio/lib/toast-errors.test.tsx`): - [x] Unregistered error toast fires exactly one untagged `dashboard_error_created {source:'toast'}` - [x] Registered funnel toast fires exactly one event, enriched with `origin`/`errorCategory`/`errorReason`/`errorCode` - [x] `useTrackFunnelError` with a toast id routes through the tracker as a single enriched event - [x] Non-error toasts ignored; the 10% sampling gate still applies Full Studio unit suite passes (392 files / 4371 tests), plus typecheck and lint. Also verified end-to-end in a local browser (TanStack flavour, sample rate temporarily forced to 1): a failed signup produced exactly one `dashboard_error_created` with `{source:'toast', origin:'signup', errorCategory:'api', errorReason:'email_already_registered', errorCode:403}` and no untagged twin (two independent trials); an unregistered error toast produced exactly one plain `{source:'toast'}`; a client-side validation failure produced exactly one `{source:'form', origin:'signup', errorCategory:'validation', errorReason:'email_invalid'}`; success toasts produced nothing. Post-deploy I'll re-run the twin-rate query from the Hex thread; the untagged-twin rate on funnel pages should decay to ~0 as stale bundles reload over 2-3 days. ## Notes - Origin-tagged funnel toast events now ride the tracker's single 10% draw instead of their own independent draw – statistically identical volume, but the event fires on the tracker's next effect rather than synchronously at the call site (irrelevant for PostHog) - Registration must happen in the same synchronous block as `toast.error()` (documented on the `TrackFunnelError` type) – all current call sites comply - The invalid Postgres version toast in `pages/new/[slug].tsx` (~line 416) needs no special-casing: unregistered toasts keep the plain untagged event, so its telemetry is preserved - Heads-up for `dashboard_error_created` consumers: overall untagged `source:'toast'` volume will dip slightly after this deploys, since funnel-page twins disappear. A volume monitor seeing that drop is this fix landing, not a tracking regression (same class as the intended GROWTH-893 sampling-unification drop). ## Linear - fixes GROWTH-965 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Enhanced error telemetry for organization creation, sign-up, payment, and project-creation flows by associating failures with toast identifiers and enriched funnel context. * Standardized dashboard error sampling logic across error handling components for consistency. * **Tests** * Added comprehensive test coverage for toast error tracking, including funnel registration, deduplication, filtering, and sampling behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Alaister Young <10985857+alaister@users.noreply.github.com>
Error Handling
ErrorMatcher displays a typed API error. If the error was classified by handleError (i.e. it is an instance of a known error class), it shows matching troubleshooting steps. Otherwise it shows a generic error card.
Classification happens in the data layer — handleError in data/fetchers.ts matches the error message against patterns and throws the appropriate error subclass (e.g. ConnectionTimeoutError). The component never does regex matching itself.
The title always comes from the caller — the same error type can appear on different pages with different titles.
Usage
import { ErrorMatcher } from 'components/interfaces/ErrorHandling/ErrorMatcher'
{
isError && (
<ErrorMatcher title="Failed to load tables" error={error} supportFormParams={{ projectRef }} />
)
}
Pass the full error object from React Query — not error.message. This lets ErrorMatcher check the error class and show the right troubleshooting steps.
Props
| Prop | Type | Description |
|---|---|---|
title |
string |
Displayed in the error card header. Set by the caller. |
error |
string | { message: string } |
The error from React Query (pass the full object, not .message). |
supportFormParams |
Partial<SupportFormUrlKeys> |
Typed params for the support form URL (projectRef, category…). |
className |
string? |
Extra classes on the card. |
supportFormParams is typed as Partial<SupportFormUrlKeys> — autocomplete shows all available fields (projectRef, orgSlug, category, subject, message, error, sid). The URL is built by createSupportFormUrl() from SupportForm.utils.tsx.
Adding a new error mapping
1. Add the error class to types/api-errors.ts
export type KnownErrorType = 'connection-timeout' | 'your-error'
export class YourError extends ResponseError {
readonly errorType = 'your-error' as const
}
export type ClassifiedError = ConnectionTimeoutError | FailedToRetrieveProjectsError | YourError
2. Add a pattern entry to data/error-patterns.ts
import { YourError } from 'types/api-errors'
export const ERROR_PATTERNS: ErrorPattern[] = [
// existing...
{
pattern: /YOUR_ERROR_PATTERN/i,
ErrorClass: YourError,
},
]
handleError picks this up automatically — any matching API error will be thrown as a YourError instance.
3. Create errorMappings/YourError.tsx
import { SIDEBAR_KEYS } from 'components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider'
import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state'
import { useSidebarManagerSnapshot } from 'state/sidebar-manager-state'
import { TroubleshootingAccordion } from '../TroubleshootingAccordion'
import {
FixWithAITroubleshootingSection,
TroubleshootingGuideSection,
} from '../TroubleshootingSections'
const ERROR_TYPE = 'your-error'
const BUILD_PROMPT = () => `Describe the issue for the AI assistant.`
export function YourErrorTroubleshooting() {
const { openSidebar } = useSidebarManagerSnapshot()
const aiSnap = useAiAssistantStateSnapshot()
return (
<TroubleshootingAccordion
errorType={ERROR_TYPE}
stepTitles={{ 1: 'Troubleshooting guide', 2: 'Debug with AI' }}
>
<TroubleshootingGuideSection
number={1}
errorType={ERROR_TYPE}
href="https://supabase.com/docs/guides/..."
/>
<FixWithAITroubleshootingSection
number={2}
errorType={ERROR_TYPE}
buildPrompt={BUILD_PROMPT}
onDebugWithAI={(prompt) => {
openSidebar(SIDEBAR_KEYS.AI_ASSISTANT)
aiSnap.newChat({ initialMessage: prompt })
}}
/>
</TroubleshootingAccordion>
)
}
4. Add it to error-mappings.tsx
import { YourErrorTroubleshooting } from './errorMappings/YourError'
export const ERROR_MAPPINGS: Record<KnownErrorType, ErrorMapping> = {
// existing...
'your-error': {
id: 'your-error',
Troubleshooting: YourErrorTroubleshooting,
},
}
That's it. ErrorMatcher picks it up automatically.
Available section components
| Component | Props |
|---|---|
RestartDatabaseTroubleshootingSection |
number, errorType, onRestartProject? |
TroubleshootingGuideSection |
number, errorType, href, title?, description? |
FixWithAITroubleshootingSection |
number, errorType, buildPrompt, onDebugWithAI? |