517 lines
16 KiB
TypeScript
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(),
|
|
};
|
|
} |