/** * ZCLAW Error Handling Utilities * * Centralized error reporting, notification, and tracking system. */ import { v4 as uuidv4 } from 'uuid'; import { AppError, classifyError, ErrorCategory, ErrorSeverity, } from './error-types'; // === Types === export interface StoredError extends AppError { dismissed: boolean; reported: boolean; stack?: string; context?: Record; } // === Error Store === interface ErrorStore { errors: StoredError[]; addError: (error: AppError) => void; dismissError: (id: string) => void; dismissAll: () => void; markReported: (id: string) => void; getUndismissedErrors: () => StoredError[]; getErrorCount: () => number; getErrorsByCategory: (category: ErrorCategory) => StoredError[]; getErrorsBySeverity: (severity: ErrorSeverity) => StoredError[]; } // === Global Error Store === let errorStore: ErrorStore = { errors: [], addError: () => {}, dismissError: () => {}, dismissAll: () => {}, markReported: () => {}, getUndismissedErrors: () => [], getErrorCount: () => 0, getErrorsByCategory: () => [], getErrorsBySeverity: () => [], }; // === Initialize Store === function initErrorStore(): void { errorStore = { errors: [], addError: (error: AppError) => { // Dedup: skip if same title+message already exists and undismissed const isDuplicate = errorStore.errors.some( (e) => !e.dismissed && e.title === error.title && e.message === error.message ); if (isDuplicate) return; const storedError: StoredError = { ...error, dismissed: false, reported: false, }; // Cap at 50 errors to prevent unbounded growth errorStore.errors = [storedError, ...errorStore.errors].slice(0, 50); // Notify listeners notifyErrorListeners(error); }, dismissError(id: string): void { const error = errorStore.errors.find(e => e.id === id); if (error) { errorStore.errors = errorStore.errors.map(e => e.id === id ? { ...e, dismissed: true } : e ); } }, dismissAll(): void { errorStore.errors = errorStore.errors.map(e => ({ ...e, dismissed: true })); }, markReported(id: string): void { const error = errorStore.errors.find(e => e.id === id); if (error) { errorStore.errors = errorStore.errors.map(e => e.id === id ? { ...e, reported: true } : e ); } }, getUndismissedErrors(): StoredError[] { return errorStore.errors.filter(e => !e.dismissed); }, getErrorCount(): number { return errorStore.errors.filter(e => !e.dismissed).length; }, getErrorsByCategory(category: ErrorCategory): StoredError[] { return errorStore.errors.filter(e => e.category === category && !e.dismissed); }, getErrorsBySeverity(severity: ErrorSeverity): StoredError[] { return errorStore.errors.filter(e => e.severity === severity && !e.dismissed); }, }; } // === Error Listeners === type ErrorListener = (error: AppError) => void; const errorListeners: Set = new Set(); function addErrorListener(listener: ErrorListener): () => void { errorListeners.add(listener); return () => errorListeners.delete(listener); } function notifyErrorListeners(error: AppError): void { errorListeners.forEach(listener => { try { listener(error); } catch (e) { console.error('[ErrorHandling] Listener error:', e); } }); } // Initialize on first import initErrorStore(); // === Public API === /** * Report an error to the centralized error handling system. */ export function reportError( error: unknown, context?: { componentStack?: string; errorName?: string; errorMessage?: string; } ): AppError { const appError = classifyError(error); // Add context information if provided if (context) { const technicalDetails = [ context.componentStack && `Component Stack:\n${context.componentStack}`, context.errorName && `Error Name: ${context.errorName}`, context.errorMessage && `Error Message: ${context.errorMessage}`, ].filter(Boolean).join('\n\n'); if (technicalDetails) { (appError as { technicalDetails?: string }).technicalDetails = technicalDetails; } } errorStore.addError(appError); // Log to console in development if (import.meta.env.DEV) { console.error('[ErrorHandling] Error reported:', { id: appError.id, category: appError.category, severity: appError.severity, title: appError.title, message: appError.message, }); } return appError; } /** * Report an error from an API response. */ export function reportApiError( response: Response, endpoint: string, method: string = 'GET' ): AppError { const status = response.status; let category: ErrorCategory = 'server'; let severity: ErrorSeverity = 'medium'; let title = 'API Error'; let message = `Request to ${endpoint} failed with status ${status}`; let recoverySteps: { description: string }[] = []; if (status === 401) { category = 'auth'; severity = 'high'; title = 'Authentication Required'; message = 'Your session has expired. Please authenticate again.'; recoverySteps = [ { description: 'Click "Reconnect" to authenticate' }, { description: 'Check your API key in settings' }, ]; } else if (status === 403) { category = 'permission'; severity = 'medium'; title = 'Permission Denied'; message = 'You do not have permission to perform this action.'; recoverySteps = [ { description: 'Contact your administrator for access' }, { description: 'Check your RBAC configuration' }, ]; } else if (status === 404) { category = 'client'; severity = 'low'; title = 'Not Found'; message = `The requested resource was not found: ${endpoint}`; recoverySteps = [ { description: 'Verify the resource exists' }, { description: 'Check the URL is correct' }, ]; } else if (status === 422) { category = 'validation'; severity = 'low'; title = 'Validation Error'; message = 'The request data is invalid.'; recoverySteps = [ { description: 'Check your input data format' }, { description: 'Verify required fields are provided' }, ]; } else if (status === 429) { category = 'client'; severity = 'medium'; title = 'Rate Limited'; message = 'Too many requests. Please wait before trying again.'; recoverySteps = [ { description: 'Wait a moment before retrying' }, { description: 'Reduce request frequency' }, ]; } else if (status >= 500) { category = 'server'; severity = 'high'; title = 'Server Error'; message = 'The server encountered an error processing your request.'; recoverySteps = [ { description: 'Try again in a few moments' }, { description: 'Contact support if the problem persists' }, ]; } const appError: AppError = { id: uuidv4(), category, severity, title, message, technicalDetails: `${method} ${endpoint}\nStatus: ${status}\nResponse: ${response.statusText}`, recoverable: status !== 500 || status < 400, recoverySteps, timestamp: new Date(), originalError: response, }; errorStore.addError(appError); return appError; } /** * Report a network error. */ export function reportNetworkError( error: Error, url?: string ): AppError { return reportError(error, { errorMessage: url ? `URL: ${url}\n${error.message}` : error.message, }); } /** * Report a WebSocket error. */ export function reportWebSocketError( event: CloseEvent | ErrorEvent, url: string ): AppError { const code = 'code' in event ? event.code : 0; const reason = 'reason' in event ? event.reason : 'Unknown'; return reportError( new Error(`WebSocket error: ${reason} (code: ${code})`), { errorMessage: `WebSocket URL: ${url}\nCode: ${code}\nReason: ${reason}`, } ); } /** * Dismiss an error by ID. */ export function dismissError(id: string): void { errorStore.dismissError(id); } /** * Dismiss all active errors. */ export function dismissAllErrors(): void { errorStore.dismissAll(); } /** * Dismiss all active errors (alias for dismissAllErrors). */ export function dismissAll(): void { errorStore.dismissAll(); } /** * Mark an error as reported. */ export function markErrorReported(id: string): void { errorStore.markReported(id); } /** * Get all active (non-dismissed) errors. */ export function getActiveErrors(): StoredError[] { return errorStore.getUndismissedErrors(); } /** * Get all undismissed errors (alias for getActiveErrors). */ export function getUndismissedErrors(): StoredError[] { return errorStore.getUndismissedErrors(); } /** * Get the count of active errors. */ export function getActiveErrorCount(): number { return errorStore.getErrorCount(); } /** * Get errors filtered by category. */ export function getErrorsByCategory(category: ErrorCategory): StoredError[] { return errorStore.getErrorsByCategory(category); } /** * Get errors filtered by severity. */ export function getErrorsBySeverity(severity: ErrorSeverity): StoredError[] { return errorStore.getErrorsBySeverity(severity); } /** * Subscribe to error events. */ export function subscribeToErrors(listener: ErrorListener): () => void { return addErrorListener(listener); } /** * Check if there are any critical errors. */ export function hasCriticalErrors(): boolean { return errorStore.getErrorsBySeverity('critical').length > 0; } /** * Check if there are any high severity errors. */ export function hasHighSeverityErrors(): boolean { const highSeverity = ['high', 'critical']; return errorStore.errors.some(e => highSeverity.includes(e.severity) && !e.dismissed); } // === Types === interface CloseEvent { code?: number; reason?: string; wasClean?: boolean; } interface ErrorEvent { code?: number; reason?: string; message?: string; }