diff --git a/apps/studio/components/interfaces/ErrorHandling/ErrorMatcher.tsx b/apps/studio/components/interfaces/ErrorHandling/ErrorMatcher.tsx index a094881fb62..29c17fd6d62 100644 --- a/apps/studio/components/interfaces/ErrorHandling/ErrorMatcher.tsx +++ b/apps/studio/components/interfaces/ErrorHandling/ErrorMatcher.tsx @@ -3,6 +3,7 @@ import { ErrorDisplay, SupportFormParams } from 'ui-patterns/ErrorDisplay/ErrorDisplay' import { getMappingForError } from './ErrorMatcher.utils' +import { categorizeError } from '@/lib/telemetry/categorizeError' import { useTrack } from '@/lib/telemetry/track' interface ErrorMatcherProps { @@ -29,7 +30,7 @@ export function ErrorMatcher({ title, error, supportFormParams, className }: Err if (Math.random() < 0.1) { track('dashboard_error_created', { source: 'error_display', - errorType: mapping?.id, + ...categorizeError(error), hasTroubleshooting: !!mapping, }) } diff --git a/apps/studio/components/ui/AlertError.tsx b/apps/studio/components/ui/AlertError.tsx index 8b1d859225a..c90a81704d7 100644 --- a/apps/studio/components/ui/AlertError.tsx +++ b/apps/studio/components/ui/AlertError.tsx @@ -4,6 +4,7 @@ import { Button } from 'ui' import { Admonition } from 'ui-patterns/admonition' import { SupportLink } from '@/components/interfaces/Support/SupportLink' +import { categorizeError } from '@/lib/telemetry/categorizeError' import { useTrack } from '@/lib/telemetry/track' export interface AlertErrorProps { @@ -73,6 +74,7 @@ export const AlertError = ({ if (Math.random() < 0.1) { track('dashboard_error_created', { source: 'admonition', + ...categorizeError(error), }) } } diff --git a/apps/studio/lib/toast-errors.tsx b/apps/studio/lib/toast-errors.tsx index 8dd031588f3..b0cd4653876 100644 --- a/apps/studio/lib/toast-errors.tsx +++ b/apps/studio/lib/toast-errors.tsx @@ -1,6 +1,7 @@ import { useEffect, useRef } from 'react' import { useSonner } from 'sonner' +import { categorizeError } from '@/lib/telemetry/categorizeError' import { useTrack } from '@/lib/telemetry/track' export const ToastErrorTracker = () => { @@ -13,8 +14,13 @@ export const ToastErrorTracker = () => { if (toast.type === 'error' && !trackRef.current.has(toast.id)) { trackRef.current.add(toast.id) if (Math.random() < 0.1) { + // Only the rendered title is available here and it may contain PII, so + // categorizeError extracts a PII-free code/type and never the text. + // Non-string titles (ReactNode) yield `errorType: 'unknown'`. + const title = typeof toast.title === 'string' ? toast.title : '' track('dashboard_error_created', { source: 'toast', + ...categorizeError(title), }) } } diff --git a/packages/common/telemetry-constants.ts b/packages/common/telemetry-constants.ts index 45de2d9d97a..434509c9677 100644 --- a/packages/common/telemetry-constants.ts +++ b/packages/common/telemetry-constants.ts @@ -2821,9 +2821,17 @@ export interface DashboardErrorCreatedEvent { */ source?: 'admonition' | 'toast' | 'error_display' /** - * Type of error matched (for error_display source) + * Normalized, PII-free error category derived from the error + * (e.g. 'connection-timeout', 'unauthorized', 'forbidden', 'not-found', + * 'server-error', 'network-error', 'timeout', 'canceled', 'unknown'). + * Never contains the raw error message. */ errorType?: string + /** + * HTTP status code (e.g. 403, 500) when available, or a transport category + * ('network', 'timeout') when there is no status. PII-free. + */ + errorCode?: number | string /** * Whether troubleshooting steps are available (for error_display source) */