Skip to main content
Glama
errorHandler.ts10.8 kB
/** * Enterprise Error Handler * Centralized error handling with categorization, recovery strategies, and user-friendly messages */ import { logger, LogLevel } from './logger'; export enum ErrorCategory { NETWORK = 'network', WEBSOCKET = 'websocket', DEBUGGER = 'debugger', PERMISSION = 'permission', VALIDATION = 'validation', TIMEOUT = 'timeout', UNKNOWN = 'unknown' } export enum ErrorSeverity { LOW = 'low', MEDIUM = 'medium', HIGH = 'high', CRITICAL = 'critical' } export interface AppError { id: string; category: ErrorCategory; severity: ErrorSeverity; code: string; message: string; technicalMessage: string; userMessage: string; timestamp: Date; context?: any; recoverable: boolean; recoveryAction?: () => Promise<void>; } export interface ErrorRecoveryStrategy { canHandle(error: AppError): boolean; recover(error: AppError): Promise<boolean>; maxRetries?: number; retryDelay?: number; } export class ErrorHandler { private static instance: ErrorHandler; private errorHistory: AppError[] = []; private recoveryStrategies: ErrorRecoveryStrategy[] = []; private maxErrorHistory = 100; private errorCounts: Map<string, number> = new Map(); private constructor() { this.setupDefaultRecoveryStrategies(); } static getInstance(): ErrorHandler { if (!ErrorHandler.instance) { ErrorHandler.instance = new ErrorHandler(); } return ErrorHandler.instance; } private setupDefaultRecoveryStrategies(): void { // WebSocket reconnection strategy this.addRecoveryStrategy({ canHandle: (error: AppError) => error.category === ErrorCategory.WEBSOCKET && error.severity !== ErrorSeverity.CRITICAL, recover: async (error: AppError): Promise<boolean> => { logger.info('error-handler', 'Attempting WebSocket reconnection recovery', { errorId: error.id }); // This would be implemented by the connection manager return false; // Let connection manager handle reconnection }, maxRetries: 3, retryDelay: 2000 }); // Permission request strategy this.addRecoveryStrategy({ canHandle: (error: AppError) => error.category === ErrorCategory.PERMISSION, recover: async (error: AppError): Promise<boolean> => { try { logger.info('error-handler', 'Requesting missing permissions'); await chrome.permissions.request({ permissions: ['debugger', 'tabs'] }); return true; } catch (err) { logger.error('error-handler', 'Failed to request permissions', err as Error); return false; } } }); // Debugger reconnection strategy this.addRecoveryStrategy({ canHandle: (error: AppError) => error.category === ErrorCategory.DEBUGGER, recover: async (error: AppError): Promise<boolean> => { logger.info('error-handler', 'Attempting debugger reconnection'); try { // Implementation would depend on specific debugger context return false; } catch (err) { return false; } } }); } createError( category: ErrorCategory, code: string, technicalMessage: string, context?: any, severity: ErrorSeverity = ErrorSeverity.MEDIUM ): AppError { const errorId = this.generateErrorId(); const userMessage = this.generateUserMessage(category, code, technicalMessage); const recoverable = this.isRecoverable(category, code); const error: AppError = { id: errorId, category, severity, code, message: technicalMessage, technicalMessage, userMessage, timestamp: new Date(), context, recoverable }; this.recordError(error); return error; } private generateErrorId(): string { return `error_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } private generateUserMessage(category: ErrorCategory, code: string, technicalMessage: string): string { const messageMap: Record<string, string> = { [`${ErrorCategory.NETWORK}_connection_failed`]: 'Impossible de se connecter au serveur. Vérifiez votre connexion Internet et réessayez.', [`${ErrorCategory.WEBSOCKET}_connection_closed`]: 'La connexion avec le serveur a été interrompue. Tentative de reconnexion automatique...', [`${ErrorCategory.DEBUGGER}_attach_failed`]: 'Impossible d\'attacher le débogueur à l\'onglet. Assurez-vous que l\'onglet est accessible.', [`${ErrorCategory.PERMISSION}_missing`]: 'L\'extension nécessite des permissions supplémentaires. Veuillez les accéder dans les paramètres.', [`${ErrorCategory.TIMEOUT}_operation`]: 'L\'opération a pris trop de temps. Veuillez réessayer.', [`${ErrorCategory.VALIDATION}_invalid_url`]: 'L\'URL fournie n\'est pas valide. Veuillez vérifier et corriger.', default: 'Une erreur est survenue. Veuillez réessayer ou contacter le support si le problème persiste.' }; const key = `${category}_${code}`; return messageMap[key] || messageMap.default; } private isRecoverable(category: ErrorCategory, code: string): boolean { const nonRecoverableErrors = [ `${ErrorCategory.PERMISSION}_denied`, `${ErrorCategory.DEBUGGER}_not_supported`, `${ErrorCategory.VALIDATION}_malformed_request` ]; return !nonRecoverableErrors.includes(`${category}_${code}`); } private recordError(error: AppError): void { this.errorHistory.push(error); // Keep only recent errors if (this.errorHistory.length > this.maxErrorHistory) { this.errorHistory.shift(); } // Track error frequency const errorKey = `${error.category}_${error.code}`; const currentCount = this.errorCounts.get(errorKey) || 0; this.errorCounts.set(errorKey, currentCount + 1); // Log the error logger.error('app-error', error.technicalMessage, undefined, { errorId: error.id, category: error.category, code: error.code, severity: error.severity, context: error.context, frequency: currentCount + 1 }); // Check for error patterns that might indicate systemic issues this.checkForErrorPatterns(error); } private checkForErrorPatterns(error: AppError): void { const errorKey = `${error.category}_${error.code}`; const frequency = this.errorCounts.get(errorKey) || 0; if (frequency >= 5 && frequency % 5 === 0) { logger.warn('error-pattern', `Recurring error detected: ${errorKey}`, { frequency, severity: error.severity, lastOccurrence: error.timestamp }); } if (error.severity === ErrorSeverity.CRITICAL) { logger.critical('critical-error', 'Critical error occurred', undefined, { errorId: error.id, category: error.category, code: error.code, timestamp: error.timestamp }); } } async handleError(error: Error | AppError, context?: any): Promise<AppError> { let appError: AppError; if (this.isAppError(error)) { appError = { ...error, context: { ...error.context, ...context } }; } else { appError = this.createError( ErrorCategory.UNKNOWN, 'unexpected_error', error.message, { originalError: error, ...context }, ErrorSeverity.MEDIUM ); } // Attempt recovery if the error is recoverable if (appError.recoverable) { await this.attemptRecovery(appError); } return appError; } private isAppError(error: any): error is AppError { return error && typeof error === 'object' && 'id' in error && 'category' in error; } private async attemptRecovery(error: AppError): Promise<void> { for (const strategy of this.recoveryStrategies) { if (strategy.canHandle(error)) { const maxRetries = strategy.maxRetries || 1; const retryDelay = strategy.retryDelay || 1000; for (let attempt = 1; attempt <= maxRetries; attempt++) { try { logger.info('error-recovery', `Attempting recovery (attempt ${attempt}/${maxRetries})`, { errorId: error.id, strategy: strategy.constructor.name }); const recovered = await strategy.recover(error); if (recovered) { logger.info('error-recovery', 'Recovery successful', { errorId: error.id }); return; } } catch (recoveryError) { logger.error('error-recovery', 'Recovery attempt failed', recoveryError as Error, { errorId: error.id, attempt }); } if (attempt < maxRetries) { await new Promise(resolve => setTimeout(resolve, retryDelay)); } } } } } addRecoveryStrategy(strategy: ErrorRecoveryStrategy): void { this.recoveryStrategies.push(strategy); logger.debug('error-handler', 'Recovery strategy added', { strategy: strategy.constructor.name }); } getErrorHistory(category?: ErrorCategory, severity?: ErrorSeverity): AppError[] { return this.errorHistory.filter(error => (!category || error.category === category) && (!severity || error.severity === severity) ); } getErrorStats(): { totalErrors: number; errorsByCategory: Record<ErrorCategory, number>; errorsBySeverity: Record<ErrorSeverity, number>; recentErrors: AppError[]; } { const errorsByCategory = Object.values(ErrorCategory).reduce((acc, category) => { acc[category] = this.errorHistory.filter(error => error.category === category).length; return acc; }, {} as Record<ErrorCategory, number>); const errorsBySeverity = Object.values(ErrorSeverity).reduce((acc, severity) => { acc[severity] = this.errorHistory.filter(error => error.severity === severity).length; return acc; }, {} as Record<ErrorSeverity, number>); const recentErrors = this.errorHistory.slice(-10); return { totalErrors: this.errorHistory.length, errorsByCategory, errorsBySeverity, recentErrors }; } clearErrorHistory(): void { this.errorHistory = []; this.errorCounts.clear(); logger.info('error-handler', 'Error history cleared'); } exportErrorReport(): string { const stats = this.getErrorStats(); const report = { timestamp: new Date().toISOString(), stats, errors: this.errorHistory, patterns: Array.from(this.errorCounts.entries()).map(([key, count]) => ({ error: key, frequency: count })) }; return JSON.stringify(report, null, 2); } } export const errorHandler = ErrorHandler.getInstance();

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/DeamonDev888/Browser-Manager-MCP-Server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server