import { toast } from 'sonner'; import { logger } from './logger'; import { supabase } from '@/integrations/supabase/client'; import { breadcrumbManager } from './errorBreadcrumbs'; import { captureEnvironmentContext } from './environmentContext'; export type ErrorContext = { action: string; userId?: string; metadata?: Record; duration?: number; // Optional: milliseconds the operation took }; export class AppError extends Error { constructor( message: string, public code: string, public userMessage?: string ) { super(message); this.name = 'AppError'; } } /** * Check if error is a Supabase connection/API error */ export function isSupabaseConnectionError(error: unknown): boolean { if (error && typeof error === 'object') { const supabaseError = error as { code?: string; status?: number; message?: string }; // Connection timeout errors if (supabaseError.code === 'PGRST301') return true; // Timeout if (supabaseError.code === 'PGRST000') return true; // Connection error // 5xx server errors if (supabaseError.status && supabaseError.status >= 500) return true; // Database connection errors (08xxx codes) if (supabaseError.code?.startsWith('08')) return true; } // Network fetch errors if (error instanceof TypeError) { const message = error.message.toLowerCase(); if (message.includes('fetch') || message.includes('network') || message.includes('failed to fetch')) { return true; } } return false; } export const handleError = ( error: unknown, context: ErrorContext ): string => { // Generate or use existing error ID const errorId = (context.metadata?.requestId as string) || crypto.randomUUID(); const shortErrorId = errorId.slice(0, 8); // Check if this is a connection error and dispatch event if (isSupabaseConnectionError(error)) { window.dispatchEvent(new CustomEvent('api-connectivity-down')); } // Enhanced error message and stack extraction let errorMessage: string; let stack: string | undefined; let errorName = 'UnknownError'; let supabaseErrorDetails: Record | undefined; if (error instanceof Error) { errorMessage = error instanceof AppError ? error.userMessage || error.message : error.message; stack = error.stack; errorName = error.name; // Check if Error instance has attached Supabase metadata if ((error as any).supabaseCode) { supabaseErrorDetails = { code: (error as any).supabaseCode, details: (error as any).supabaseDetails, hint: (error as any).supabaseHint }; } } else if (error && typeof error === 'object') { // Handle Supabase errors (plain objects with message/code/details) const supabaseError = error as { message?: string; code?: string; details?: string; hint?: string; stack?: string; }; errorMessage = supabaseError.message || 'An unexpected error occurred'; errorName = 'SupabaseError'; // Capture Supabase error details for metadata supabaseErrorDetails = { code: supabaseError.code, details: supabaseError.details, hint: supabaseError.hint }; // Try to extract stack from object if (supabaseError.stack && typeof supabaseError.stack === 'string') { stack = supabaseError.stack; } else if (supabaseError.code || supabaseError.details || supabaseError.hint) { // Create synthetic stack trace for Supabase errors to aid debugging const stackParts = [ `SupabaseError: ${errorMessage}`, supabaseError.code ? ` Code: ${supabaseError.code}` : null, supabaseError.details ? ` Details: ${supabaseError.details}` : null, supabaseError.hint ? ` Hint: ${supabaseError.hint}` : null, ` at ${context.action}`, ` Reference ID: ${errorId}` ].filter(Boolean); stack = stackParts.join('\n'); } } else if (typeof error === 'string') { errorMessage = error; // Generate synthetic stack trace for string errors stack = new Error().stack?.replace(/^Error\n/, `StringError: ${error}\n`); } else { errorMessage = 'An unexpected error occurred'; // Generate synthetic stack trace for unknown error types stack = new Error().stack?.replace(/^Error\n/, `UnknownError: ${String(error)}\n`); } // Log to console/monitoring with enhanced debugging logger.error('Error occurred', { ...context, error: errorMessage, stack, errorId, errorName, errorType: typeof error, errorConstructor: error?.constructor?.name, hasStack: !!stack, isSyntheticStack: !!(error && typeof error === 'object' && !(error instanceof Error) && stack), supabaseError: supabaseErrorDetails, }); // Additional debug logging when stack is missing if (!stack) { console.error('[handleError] Error without stack trace:', { type: typeof error, constructor: error?.constructor?.name, error: error, context, errorId }); } // Log to database with breadcrumbs (non-blocking) try { const envContext = captureEnvironmentContext(); const breadcrumbs = breadcrumbManager.getAll(); // Fire-and-forget database logging supabase.rpc('log_request_metadata', { p_request_id: errorId, p_user_id: context.userId || undefined, p_endpoint: context.action, p_method: 'ERROR', p_status_code: 500, p_error_type: errorName, p_error_message: errorMessage, p_error_stack: stack, p_user_agent: navigator.userAgent, p_breadcrumbs: JSON.stringify({ breadcrumbs, isRetry: context.metadata?.isRetry || false, attempt: context.metadata?.attempt, retriesExhausted: context.metadata?.retriesExhausted || false, supabaseError: supabaseErrorDetails, metadata: context.metadata }), p_timezone: envContext.timezone, p_referrer: document.referrer || undefined, p_duration_ms: context.duration, }).then(({ error: dbError }) => { if (dbError) { logger.error('Failed to log error to database', { dbError }); } }); } catch (logError) { logger.error('Failed to capture error context', { logError }); } // Show user-friendly toast with error ID (skip for retry attempts) const isRetry = context.metadata?.isRetry === true || context.metadata?.attempt; if (!isRetry) { toast.error(context.action, { description: `${errorMessage}\n\nReference ID: ${shortErrorId}`, duration: 5000, }); } return errorId; }; export const handleSuccess = ( title: string, description?: string ): void => { toast.success(title, { description, duration: 3000 }); }; export const handleInfo = ( title: string, description?: string ): void => { toast.info(title, { description, duration: 4000 }); }; /** * Handle non-critical errors (background failures) that should be logged * to the database WITHOUT showing user toasts * Use this for fire-and-forget operations where user shouldn't be interrupted */ export const handleNonCriticalError = ( error: unknown, context: ErrorContext ): string => { const errorId = crypto.randomUUID(); const shortErrorId = errorId.slice(0, 8); const errorMessage = error instanceof AppError ? error.userMessage || error.message : error instanceof Error ? error.message : 'An unexpected error occurred'; // Log to console/monitoring (same as handleError) logger.error('Non-critical error occurred', { ...context, error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, errorId, severity: 'low', }); // Log to database with breadcrumbs (non-blocking, fire-and-forget) try { const envContext = captureEnvironmentContext(); const breadcrumbs = breadcrumbManager.getAll(); supabase.rpc('log_request_metadata', { p_request_id: errorId, p_user_id: context.userId || undefined, p_endpoint: context.action, p_method: 'NON_CRITICAL_ERROR', p_status_code: 500, p_error_type: error instanceof Error ? error.name : 'UnknownError', p_error_message: errorMessage, p_error_stack: error instanceof Error ? error.stack : undefined, p_user_agent: navigator.userAgent, p_breadcrumbs: JSON.stringify({ breadcrumbs, metadata: context.metadata // Include metadata for debugging }), p_timezone: envContext.timezone, p_referrer: document.referrer || undefined, p_duration_ms: context.duration, }).then(({ error: dbError }) => { if (dbError) { logger.error('Failed to log non-critical error to database', { dbError }); } }); } catch (logError) { logger.error('Failed to capture non-critical error context', { logError }); } // NO TOAST - This is the key difference from handleError() return errorId; }; /** * Type-safe error message extraction utility * Use this instead of `error: any` in catch blocks */ export function getErrorMessage(error: unknown): string { if (error instanceof Error) { return error.message; } if (typeof error === 'string') { return error; } if (error && typeof error === 'object' && 'message' in error) { return String(error.message); } return 'An unexpected error occurred'; } /** * Type guard to check if error has a code property */ export function hasErrorCode(error: unknown): error is { code: string } { return ( error !== null && typeof error === 'object' && 'code' in error && typeof (error as { code: unknown }).code === 'string' ); } /** * Helper to wrap async operations with automatic duration tracking * Use this for operations where you want to track how long they took before failing */ export async function withErrorTiming( fn: () => Promise, errorContext: Omit ): Promise { const start = performance.now(); try { return await fn(); } catch (error) { const duration = Math.round(performance.now() - start); handleError(error, { ...errorContext, duration }); throw error; } }