/** * Enhanced Global Error Handler for Maximum Developer Transparency * Captures all uncaught errors, promise rejections, and React errors */ import { ApiError } from '../api/base/ApiError'; import { getGlobalErrorReporter } from './EnhancedErrorReporter'; import { ConsoleLogger } from './logging/ConsoleLogger'; import { getGlobalReplaySystem } from './ErrorReplay'; export interface GlobalErrorHandlerOptions { /** * Enable detailed error overlays in development */ showDevOverlay?: boolean; /** * Log all errors to console with maximum detail */ verboseLogging?: boolean; /** * Capture stack traces with enhanced context */ captureEnhancedStacks?: boolean; /** * Report to external services (Sentry, etc.) */ reportToExternal?: boolean; /** * Custom error filter to ignore certain errors */ errorFilter?: (error: Error) => boolean; } export class GlobalErrorHandler { private options: GlobalErrorHandlerOptions; private logger: ConsoleLogger; private errorReporter: ReturnType; private errorHistory: Array<{ error: Error | ApiError; timestamp: string; context?: unknown; stackEnhanced?: string; }> = []; private readonly MAX_HISTORY = 100; private isInitialized = false; constructor(options: GlobalErrorHandlerOptions = {}) { this.options = { showDevOverlay: options.showDevOverlay ?? process.env.NODE_ENV === 'development', verboseLogging: options.verboseLogging ?? process.env.NODE_ENV === 'development', captureEnhancedStacks: options.captureEnhancedStacks ?? process.env.NODE_ENV === 'development', reportToExternal: options.reportToExternal ?? process.env.NODE_ENV === 'production', errorFilter: options.errorFilter, }; this.logger = new ConsoleLogger(); this.errorReporter = getGlobalErrorReporter(); } /** * Initialize global error handlers */ initialize(): void { if (this.isInitialized) { console.warn('[GlobalErrorHandler] Already initialized'); return; } // Only initialize in browser environment if (typeof window === 'undefined') { if (this.options.verboseLogging) { this.logger.info('Global error handler skipped (server-side)'); } return; } // Handle uncaught JavaScript errors window.addEventListener('error', this.handleWindowError); // Handle unhandled promise rejections window.addEventListener('unhandledrejection', this.handleUnhandledRejection); // Override console.error to capture framework errors this.overrideConsoleError(); // React error boundary fallback this.setupReactErrorHandling(); this.isInitialized = true; if (this.options.verboseLogging) { this.logger.info('Global error handler initialized', { devOverlay: this.options.showDevOverlay, verboseLogging: this.options.verboseLogging, enhancedStacks: this.options.captureEnhancedStacks, }); } } /** * Handle window errors (uncaught JavaScript errors) */ private handleWindowError = (event: ErrorEvent): void => { const error = event.error; // Apply error filter if provided if (this.options.errorFilter && !this.options.errorFilter(error)) { return; } const enhancedContext = this.captureEnhancedContext('window_error', { filename: event.filename, lineno: event.lineno, colno: event.colno, message: event.message, }); // Log with maximum detail this.logErrorWithMaximumDetail(error, enhancedContext); // Store in history this.addToHistory(error, enhancedContext); // Report to external if enabled if (this.options.reportToExternal) { this.reportToExternal(error, enhancedContext); } // Auto-capture for replay in development if (this.options.showDevOverlay) { const replaySystem = getGlobalReplaySystem(); replaySystem.autoCapture(error, enhancedContext); } // Prevent default error logging in dev to avoid duplicates if (this.options.showDevOverlay) { event.preventDefault(); } }; /** * Handle unhandled promise rejections */ private handleUnhandledRejection = (event: PromiseRejectionEvent): void => { const error = event.reason; // Apply error filter if provided if (this.options.errorFilter && !this.options.errorFilter(error)) { return; } const enhancedContext = this.captureEnhancedContext('unhandled_promise', { promise: event.promise, reason: typeof error === 'string' ? error : error?.message || 'Unknown promise rejection', }); // Log with maximum detail this.logErrorWithMaximumDetail(error, enhancedContext); // Store in history this.addToHistory(error, enhancedContext); // Report to external if enabled if (this.options.reportToExternal) { this.reportToExternal(error, enhancedContext); } // Prevent default logging if (this.options.showDevOverlay) { event.preventDefault(); } }; /** * Override console.error to capture framework errors */ private overrideConsoleError(): void { const originalError = console.error; console.error = (...args: unknown[]): void => { // Call original first originalError.apply(console, args); // Try to extract error from arguments const error = args.find(arg => arg instanceof Error) || args.find(arg => typeof arg === 'object' && arg !== null && 'message' in arg) || new Error(args.map(a => String(a)).join(' ')); if (error instanceof Error) { const enhancedContext = this.captureEnhancedContext('console_error', { originalArgs: args, }); // Store in history this.addToHistory(error, enhancedContext); // No overlay - just enhanced console logging } }; } /** * Setup React-specific error handling */ private setupReactErrorHandling(): void { // This will be used by React Error Boundaries // We'll provide a global registry for React errors (window as any).__GRIDPILOT_REACT_ERRORS__ = []; } /** * Capture enhanced context with stack trace and environment info */ private captureEnhancedContext(type: string, additionalContext: Record = {}): Record { const stack = new Error().stack; return { type, timestamp: new Date().toISOString(), userAgent: navigator.userAgent, url: window.location.href, language: navigator.language, platform: navigator.platform, viewport: { width: window.innerWidth, height: window.innerHeight, }, screen: { width: window.screen.width, height: window.screen.height, }, memory: (performance as any).memory ? { usedJSHeapSize: (performance as any).memory.usedJSHeapSize, totalJSHeapSize: (performance as any).memory.totalJSHeapSize, } : null, connection: (navigator as any).connection ? { effectiveType: (navigator as any).connection.effectiveType, downlink: (navigator as any).connection.downlink, rtt: (navigator as any).connection.rtt, } : null, ...additionalContext, enhancedStack: this.options.captureEnhancedStacks ? this.enhanceStackTrace(stack) : undefined, }; } /** * Enhance stack trace with additional context */ private enhanceStackTrace(stack?: string): string | undefined { if (!stack) return undefined; // Add source map information if available const lines = stack.split('\n').slice(1); // Remove first line (error message) const enhanced = lines.map(line => { // Try to extract file and line info const match = line.match(/at (.+) \((.+):(\d+):(\d+)\)/) || line.match(/at (.+):(\d+):(\d+)/); if (match) { const func = match[1] || 'anonymous'; const file = match[2] || match[1]; const lineNum = match[3] || match[2]; const colNum = match[4] || match[3]; // Add source map comment if in development if (process.env.NODE_ENV === 'development' && file && file.includes('.js')) { return `at ${func} (${file}:${lineNum}:${colNum}) [Source Map: ${file}.map]`; } return `at ${func} (${file}:${lineNum}:${colNum})`; } return line.trim(); }); return enhanced.join('\n'); } /** * Log error with maximum detail */ private logErrorWithMaximumDetail(error: Error | ApiError, context: Record): void { if (!this.options.verboseLogging) return; const isApiError = error instanceof ApiError; // Group all related information console.groupCollapsed(`%c[GLOBAL ERROR] ${error.name || 'Error'}: ${error.message}`, 'color: #ff4444; font-weight: bold; font-size: 14px;' ); // Error details console.log('Error Details:', { name: error.name, message: error.message, stack: error.stack, type: isApiError ? error.type : 'N/A', severity: isApiError ? error.getSeverity() : 'error', retryable: isApiError ? error.isRetryable() : 'N/A', connectivity: isApiError ? error.isConnectivityIssue() : 'N/A', }); // Context information console.log('Context:', context); // Enhanced stack trace if (context.enhancedStack) { console.log('Enhanced Stack Trace:\n' + context.enhancedStack); } // API-specific information if (isApiError && error.context) { console.log('API Context:', error.context); } // Environment information console.log('Environment:', { mode: process.env.NODE_ENV, nextPublicMode: process.env.NEXT_PUBLIC_GRIDPILOT_MODE, version: process.env.NEXT_PUBLIC_APP_VERSION, buildTime: process.env.NEXT_PUBLIC_BUILD_TIME, }); // Performance metrics if (context.memory && typeof context.memory === 'object' && 'usedJSHeapSize' in context.memory && 'totalJSHeapSize' in context.memory) { const memory = context.memory as { usedJSHeapSize: number; totalJSHeapSize: number }; console.log('Memory Usage:', { used: `${(memory.usedJSHeapSize / 1024 / 1024).toFixed(2)} MB`, total: `${(memory.totalJSHeapSize / 1024 / 1024).toFixed(2)} MB`, }); } // Network information if (context.connection) { console.log('Network:', context.connection); } // Error history (last 5 errors) if (this.errorHistory.length > 0) { console.log('Recent Error History:', this.errorHistory.slice(-5)); } console.groupEnd(); // Also log to our logger this.logger.error(error.message, error, context); } /** * Report error to external services */ private reportToExternal(error: Error | ApiError, context: Record): void { // This is a placeholder for external error reporting (Sentry, LogRocket, etc.) // In a real implementation, you would send structured data to your error tracking service if (this.options.verboseLogging) { console.log('[EXTERNAL REPORT] Would send to error tracking service:', { error: { name: error.name, message: error.message, stack: error.stack, type: error instanceof ApiError ? error.type : undefined, }, context, timestamp: new Date().toISOString(), }); } } /** * Add error to history */ private addToHistory(error: Error | ApiError, context: Record): void { const entry = { error, timestamp: new Date().toISOString(), context, stackEnhanced: context.enhancedStack as string | undefined, }; this.errorHistory.push(entry); // Keep only last N errors if (this.errorHistory.length > this.MAX_HISTORY) { this.errorHistory = this.errorHistory.slice(-this.MAX_HISTORY); } } /** * Get error history */ getErrorHistory(): Array<{ error: Error | ApiError; timestamp: string; context?: unknown; stackEnhanced?: string }> { return [...this.errorHistory]; } /** * Clear error history */ clearHistory(): void { this.errorHistory = []; if (this.options.verboseLogging) { this.logger.info('Error history cleared'); } } /** * Get statistics about errors */ getStats(): { total: number; byType: Record; recent: Array<{ timestamp: string; message: string; type: string }>; } { const stats = { total: this.errorHistory.length, byType: {} as Record, recent: this.errorHistory.slice(-10).map(entry => ({ timestamp: entry.timestamp, message: entry.error.message, type: entry.error instanceof ApiError ? entry.error.type : entry.error.name || 'Error', })), }; this.errorHistory.forEach(entry => { const type = entry.error instanceof ApiError ? entry.error.type : entry.error.name || 'Error'; stats.byType[type] = (stats.byType[type] || 0) + 1; }); return stats; } /** * Manually report an error */ report(error: Error | ApiError, additionalContext: Record = {}): void { const context = this.captureEnhancedContext('manual_report', additionalContext); this.logErrorWithMaximumDetail(error, context); this.addToHistory(error, context); // Auto-capture for replay in development if (this.options.showDevOverlay) { const replaySystem = getGlobalReplaySystem(); replaySystem.autoCapture(error, context); } if (this.options.reportToExternal) { this.reportToExternal(error, context); } } /** * Destroy the error handler and remove all listeners */ destroy(): void { if (typeof window !== 'undefined') { window.removeEventListener('error', this.handleWindowError); window.removeEventListener('unhandledrejection', this.handleUnhandledRejection); // Restore original console.error if ((console as any)._originalError) { console.error = (console as any)._originalError; } } this.isInitialized = false; if (this.options.verboseLogging) { this.logger.info('Global error handler destroyed'); } } } /** * Global instance accessor */ let globalErrorHandlerInstance: GlobalErrorHandler | null = null; export function getGlobalErrorHandler(): GlobalErrorHandler { if (!globalErrorHandlerInstance) { globalErrorHandlerInstance = new GlobalErrorHandler(); } return globalErrorHandlerInstance; } /** * Initialize global error handling */ export function initializeGlobalErrorHandling(options?: GlobalErrorHandlerOptions): GlobalErrorHandler { const handler = new GlobalErrorHandler(options); handler.initialize(); globalErrorHandlerInstance = handler; return handler; } /** * React hook for manual error reporting */ export function useGlobalErrorHandler() { const handler = getGlobalErrorHandler(); return { report: (error: Error | ApiError, context?: Record) => handler.report(error, context), getHistory: () => handler.getErrorHistory(), getStats: () => handler.getStats(), clearHistory: () => handler.clearHistory(), }; }