UNPKG

@orchard9ai/error-handling

Version:

Federated error handling package with go-core-http-toolkit format support and logging integration

429 lines (370 loc) 11.3 kB
import { ErrorHandler } from '../core/ErrorHandler.js'; import { parseUnknownError } from '../utils/errorParsers.js'; import type { ErrorContext, DisplayError, ErrorHandlerConfig } from '../types/index.js'; /** * Global error handler configuration */ export interface GlobalErrorHandlerConfig { /** Whether to enable global error handling */ enabled?: boolean; /** Whether to show toast notifications for errors */ showToasts?: boolean; /** Whether to log errors to console */ logToConsole?: boolean; /** Custom error handler callback */ onError?: (error: Error, displayError: DisplayError, context: ErrorContext) => void; /** Custom unhandled rejection handler */ onUnhandledRejection?: (reason: any, promise: Promise<any>, displayError: DisplayError) => void; /** Error handler configuration */ errorHandlerConfig?: Partial<ErrorHandlerConfig>; /** Errors to ignore (by message or error type) */ ignoredErrors?: string[]; /** Maximum number of errors to handle per minute (rate limiting) */ maxErrorsPerMinute?: number; } /** * Error statistics for monitoring */ export interface ErrorStats { totalErrors: number; errorsByType: Record<string, number>; errorsByMinute: number[]; lastError?: { message: string; timestamp: Date; context?: ErrorContext; }; } /** * Global error handler for catching unhandled errors */ export class GlobalErrorHandler { private config: GlobalErrorHandlerConfig & { enabled: boolean; showToasts: boolean; logToConsole: boolean; errorHandlerConfig: Partial<ErrorHandlerConfig>; ignoredErrors: string[]; maxErrorsPerMinute: number; }; private errorHandler: ErrorHandler; private errorCounts: number[]; private stats: ErrorStats; private originalErrorHandler?: OnErrorEventHandler | null; private originalRejectionHandler?: ((this: WindowEventHandlers, ev: PromiseRejectionEvent) => any) | null; private cleanupInterval?: number; constructor(config: GlobalErrorHandlerConfig = {}) { this.config = { enabled: true, showToasts: true, logToConsole: true, onError: config.onError, onUnhandledRejection: config.onUnhandledRejection, errorHandlerConfig: {}, ignoredErrors: [ 'Script error.', 'Non-Error promise rejection captured', 'ResizeObserver loop limit exceeded' ], maxErrorsPerMinute: 10, ...config }; this.errorHandler = new ErrorHandler({ loggerName: 'global-error-handler', ...this.config.errorHandlerConfig }); this.errorCounts = []; this.stats = { totalErrors: 0, errorsByType: {}, errorsByMinute: [] }; if (this.config.enabled) { this.install(); } } /** * Install global error handlers */ install(): void { if (typeof window === 'undefined') { return; // Server-side rendering } // Store original handlers this.originalErrorHandler = window.onerror; this.originalRejectionHandler = window.onunhandledrejection; // Install window error handler window.onerror = (message, source, lineno, colno, error) => { this.handleWindowError(message, source, lineno, colno, error); // Call original handler if it exists if (this.originalErrorHandler) { return this.originalErrorHandler(message, source, lineno, colno, error); } return false; }; // Install unhandled promise rejection handler window.onunhandledrejection = (event) => { this.handleUnhandledRejection(event); // Call original handler if it exists if (this.originalRejectionHandler) { return this.originalRejectionHandler.call(window, event); } }; // Clean up error counts every minute this.cleanupInterval = window.setInterval(() => { this.cleanupErrorCounts(); }, 60000); } /** * Uninstall global error handlers */ uninstall(): void { if (typeof window === 'undefined') { return; } // Clear interval if (this.cleanupInterval) { window.clearInterval(this.cleanupInterval); this.cleanupInterval = undefined; } // Restore original handlers window.onerror = this.originalErrorHandler || null; window.onunhandledrejection = this.originalRejectionHandler || null; this.originalErrorHandler = null; this.originalRejectionHandler = null; } /** * Handle window errors */ private handleWindowError( message: string | Event, source?: string, lineno?: number, colno?: number, error?: Error ): void { // Check rate limiting if (!this.checkRateLimit()) { return; } // Check if error should be ignored const errorMessage = typeof message === 'string' ? message : error?.message || 'Unknown error'; if (this.shouldIgnoreError(errorMessage)) { return; } // Create context const context: ErrorContext = { component: 'global-error-handler', action: 'window-error', metadata: { source, lineno, colno, userAgent: navigator.userAgent, url: window.location.href } }; // Handle the error const actualError = error || new Error(errorMessage); const displayError = this.processError(actualError, context); // Update statistics this.updateStats(actualError, context); // Call custom handler if (this.config.onError) { this.config.onError(actualError, displayError, context); } // Show toast if enabled if (this.config.showToasts) { this.showErrorToast(displayError); } // Log to console if enabled if (this.config.logToConsole) { console.error('Global Error:', actualError, { context, displayError }); } } /** * Handle unhandled promise rejections */ private handleUnhandledRejection(event: PromiseRejectionEvent): void { // Check rate limiting if (!this.checkRateLimit()) { return; } // Check if error should be ignored const reason = event.reason; const errorMessage = reason instanceof Error ? reason.message : String(reason); if (this.shouldIgnoreError(errorMessage)) { return; } // Create context const context: ErrorContext = { component: 'global-error-handler', action: 'unhandled-rejection', metadata: { promiseString: event.promise.toString(), reasonType: typeof reason, userAgent: navigator.userAgent, url: window.location.href } }; // Convert reason to error const error = reason instanceof Error ? reason : new Error(String(reason)); const displayError = this.processError(error, context); // Update statistics this.updateStats(error, context); // Call custom handler if (this.config.onUnhandledRejection) { this.config.onUnhandledRejection(reason, event.promise, displayError); } // Call general error handler if (this.config.onError) { this.config.onError(error, displayError, context); } // Show toast if enabled if (this.config.showToasts) { this.showErrorToast(displayError); } // Log to console if enabled if (this.config.logToConsole) { console.error('Unhandled Promise Rejection:', reason, { context, displayError }); } // Prevent default browser behavior event.preventDefault(); } /** * Process error through error handler */ private processError(error: Error, context: ErrorContext): DisplayError { const apiError = parseUnknownError(error); return this.errorHandler.handleApiError(apiError, context); } /** * Check if error should be ignored */ private shouldIgnoreError(message: string): boolean { return this.config.ignoredErrors.some(ignored => message.includes(ignored) || message === ignored ); } /** * Check rate limiting */ private checkRateLimit(): boolean { const now = Date.now(); const oneMinuteAgo = now - 60000; // Remove old error counts this.errorCounts = this.errorCounts.filter(timestamp => timestamp > oneMinuteAgo); // Check if we've exceeded the limit if (this.errorCounts.length >= this.config.maxErrorsPerMinute) { return false; } // Add current error this.errorCounts.push(now); return true; } /** * Update error statistics */ private updateStats(error: Error, context: ErrorContext): void { this.stats.totalErrors++; const errorType = error.constructor.name; this.stats.errorsByType[errorType] = (this.stats.errorsByType[errorType] || 0) + 1; this.stats.lastError = { message: error.message, timestamp: new Date(), context }; // Track errors per minute const minute = Math.floor(Date.now() / 60000); if (this.stats.errorsByMinute.length === 0 || this.stats.errorsByMinute[this.stats.errorsByMinute.length - 1] !== minute) { this.stats.errorsByMinute.push(minute); } } /** * Clean up old error counts */ private cleanupErrorCounts(): void { const oneMinuteAgo = Date.now() - 60000; this.errorCounts = this.errorCounts.filter(timestamp => timestamp > oneMinuteAgo); } /** * Show error toast */ private showErrorToast(displayError: DisplayError): void { // Integration point for toast notifications // Apps should configure this via onError callback } /** * Get error statistics */ getStats(): ErrorStats { return { ...this.stats }; } /** * Reset error statistics */ resetStats(): void { this.stats = { totalErrors: 0, errorsByType: {}, errorsByMinute: [] }; this.errorCounts = []; } /** * Update configuration */ configure(config: Partial<GlobalErrorHandlerConfig>): void { this.config = { ...this.config, ...config }; if (config.enabled !== undefined) { if (config.enabled) { this.install(); } else { this.uninstall(); } } } /** * Add error to ignore list */ addIgnoredError(pattern: string): void { if (!this.config.ignoredErrors.includes(pattern)) { this.config.ignoredErrors.push(pattern); } } /** * Remove error from ignore list */ removeIgnoredError(pattern: string): void { const index = this.config.ignoredErrors.indexOf(pattern); if (index > -1) { this.config.ignoredErrors.splice(index, 1); } } } /** * Global error handler instance */ let globalErrorHandler: GlobalErrorHandler | null = null; /** * Get or create global error handler */ export function getGlobalErrorHandler(): GlobalErrorHandler { if (!globalErrorHandler) { globalErrorHandler = new GlobalErrorHandler(); } return globalErrorHandler; } /** * Configure global error handling */ export function configureGlobalErrorHandling(config: GlobalErrorHandlerConfig): GlobalErrorHandler { globalErrorHandler = new GlobalErrorHandler(config); return globalErrorHandler; } /** * Install global error handling with default configuration */ export function installGlobalErrorHandling(config?: GlobalErrorHandlerConfig): GlobalErrorHandler { return configureGlobalErrorHandling({ enabled: true, ...config }); }