Files
gridpilot.gg/apps/website/lib/infrastructure/GlobalErrorHandler.ts
2026-01-16 01:00:03 +01:00

517 lines
16 KiB
TypeScript

/**
* 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<typeof getGlobalErrorReporter>;
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;
}
// 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<string, unknown> = {}): Record<string, unknown> {
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<string, unknown>): 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<string, unknown>): 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<string, unknown>): 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<string, number>;
recent: Array<{ timestamp: string; message: string; type: string }>;
} {
const stats = {
total: this.errorHistory.length,
byType: {} as Record<string, number>,
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<string, unknown> = {}): 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<string, unknown>) => handler.report(error, context),
getHistory: () => handler.getErrorHistory(),
getStats: () => handler.getStats(),
clearHistory: () => handler.clearHistory(),
};
}