/** * Enhanced Global Error Handler for Maximum Developer Transparency * Captures all uncaught errors, promise rejections, and React errors */ import { ApiError } from '../gateways/api/base/ApiError'; import { getGlobalReplaySystem } from './ErrorReplay'; import { ConsoleLogger } from './logging/ConsoleLogger'; 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 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(); } /** * 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; } // Check if this is a network/CORS error (expected in some cases) if (error instanceof TypeError && error.message.includes('fetch')) { this.logger.warn('Network error detected', { type: 'network_error', message: error.message, url: event.filename, line: event.lineno, col: event.colno, }); return; // Don't prevent default for network errors } const enhancedContext = this.captureEnhancedContext('window_error', { filename: event.filename, lineno: event.lineno, colno: event.colno, message: event.message, }); // Log with appropriate detail this.logErrorWithMaximumDetail(error, enhancedContext); // Store in history this.addToHistory(error, enhancedContext); // Report to external if enabled (but not for network errors) 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; } // Check if this is a network error (expected) if (error instanceof TypeError && error.message.includes('fetch')) { this.logger.warn('Unhandled promise rejection - network error', { type: 'network_error', message: error.message, }); return; } const enhancedContext = this.captureEnhancedContext('unhandled_promise', { promise: event.promise, reason: typeof error === 'string' ? error : error?.message || 'Unknown promise rejection', }); // Log with appropriate detail this.logErrorWithMaximumDetail(error, enhancedContext); // Store in history this.addToHistory(error, enhancedContext); // Report to external if enabled (but not for network errors) 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 { __GRIDPILOT_REACT_ERRORS__?: unknown[] }).__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 unknown as { memory?: { usedJSHeapSize: number; totalJSHeapSize: number } }).memory ? { usedJSHeapSize: (performance as unknown as { memory: { usedJSHeapSize: number; totalJSHeapSize: number } }).memory.usedJSHeapSize, totalJSHeapSize: (performance as unknown as { memory: { usedJSHeapSize: number; totalJSHeapSize: number } }).memory.totalJSHeapSize, } : null, connection: (navigator as unknown as { connection?: { effectiveType: string; downlink: number; rtt: number } }).connection ? { effectiveType: (navigator as unknown as { connection: { effectiveType: string; downlink: number; rtt: number } }).connection.effectiveType, downlink: (navigator as unknown as { connection: { effectiveType: string; downlink: number; rtt: number } }).connection.downlink, rtt: (navigator as unknown as { connection: { effectiveType: string; downlink: number; rtt: number } }).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 appropriate detail */ private logErrorWithMaximumDetail(error: Error | ApiError, context: Record): void { if (!this.options.verboseLogging) return; const isApiError = error instanceof ApiError; const isWarning = isApiError && error.getSeverity() === 'warn'; if (isWarning) { // Simplified warning output this.logger.warn(error.message, context); return; } // Full error details for actual errors this.logger.error(error.message, error, context); // In development, show additional details if (process.env.NODE_ENV === 'development') { console.groupCollapsed(`%c[ERROR DETAIL] ${error.name || 'Error'}`, 'color: #ff4444; font-weight: bold; font-size: 14px;'); console.log('%cError Details:', 'color: #666; font-weight: bold;', { name: error.name, message: error.message, type: isApiError ? error.type : 'N/A', severity: isApiError ? error.getSeverity() : 'error', retryable: isApiError ? error.isRetryable() : 'N/A', connectivity: isApiError ? error.isConnectivityIssue() : 'N/A', }); console.log('%cContext:', 'color: #666; font-weight: bold;', context); if (isApiError && error.context?.developerHint) { console.log('%c💡 Developer Hint:', 'color: #00aaff; font-weight: bold;', error.context.developerHint); } console.groupEnd(); } } /** * 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); // Check if this is a network error (don't report to external services) const isNetworkError = error.message.includes('fetch') || error.message.includes('Failed to fetch') || error.message.includes('NetworkError'); if (isNetworkError) { this.logger.warn(`Manual error report: ${error.message}`, context); } else { 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); } // Only report non-network errors to external services if (this.options.reportToExternal && !isNetworkError) { 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 unknown as { _originalError?: typeof console.error })._originalError) { console.error = (console as unknown as { _originalError: typeof console.error })._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(), }; }