dev experience
This commit is contained in:
657
apps/website/lib/infrastructure/GlobalErrorHandler.ts
Normal file
657
apps/website/lib/infrastructure/GlobalErrorHandler.ts
Normal file
@@ -0,0 +1,657 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
// Show dev overlay if enabled
|
||||
if (this.options.showDevOverlay) {
|
||||
this.showDevOverlay(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);
|
||||
|
||||
// Show dev overlay if enabled
|
||||
if (this.options.showDevOverlay) {
|
||||
this.showDevOverlay(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);
|
||||
|
||||
// Show dev overlay if enabled
|
||||
if (this.options.showDevOverlay) {
|
||||
this.showDevOverlay(error, enhancedContext);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<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 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.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<string, unknown>): 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show development overlay with error details
|
||||
*/
|
||||
private showDevOverlay(error: Error | ApiError, context: Record<string, unknown>): void {
|
||||
// Check if overlay already exists
|
||||
const existingOverlay = document.getElementById('gridpilot-error-overlay');
|
||||
if (existingOverlay) {
|
||||
// Update existing overlay
|
||||
this.updateDevOverlay(existingOverlay, error, context);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create new overlay
|
||||
const overlay = document.createElement('div');
|
||||
overlay.id = 'gridpilot-error-overlay';
|
||||
overlay.style.cssText = `
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.95);
|
||||
color: #fff;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 12px;
|
||||
z-index: 999999;
|
||||
overflow: auto;
|
||||
padding: 20px;
|
||||
border: 4px solid #ff4444;
|
||||
box-shadow: 0 0 50px rgba(255, 68, 68, 0.5);
|
||||
`;
|
||||
|
||||
this.updateDevOverlay(overlay, error, context);
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
// Add keyboard shortcut to dismiss
|
||||
const dismissHandler = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape' || e.key === 'Enter') {
|
||||
overlay.remove();
|
||||
document.removeEventListener('keydown', dismissHandler);
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', dismissHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update existing dev overlay
|
||||
*/
|
||||
private updateDevOverlay(overlay: HTMLElement, error: Error | ApiError, context: Record<string, unknown>): void {
|
||||
const isApiError = error instanceof ApiError;
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
|
||||
overlay.innerHTML = `
|
||||
<div style="max-width: 1200px; margin: 0 auto;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 20px;">
|
||||
<div>
|
||||
<h1 style="color: #ff4444; margin: 0 0 10px 0; font-size: 24px;">
|
||||
🚨 UNCAUGHT ERROR - DEVELOPMENT MODE
|
||||
</h1>
|
||||
<div style="color: #888;">${timestamp} | Press ESC or ENTER to dismiss</div>
|
||||
</div>
|
||||
<button onclick="this.parentElement.parentElement.remove()"
|
||||
style="background: #ff4444; color: white; border: none; padding: 8px 16px; cursor: pointer; border-radius: 4px; font-weight: bold;">
|
||||
CLOSE
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 20px;">
|
||||
<div style="background: #1a1a1a; padding: 15px; border-radius: 8px; border: 1px solid #333;">
|
||||
<h3 style="color: #ffaa00; margin-top: 0;">Error Information</h3>
|
||||
<div style="line-height: 1.6;">
|
||||
<div><strong>Type:</strong> <span style="color: #ff4444;">${isApiError ? error.type : error.name}</span></div>
|
||||
<div><strong>Message:</strong> ${error.message}</div>
|
||||
${isApiError ? `<div><strong>Severity:</strong> ${error.getSeverity()}</div>` : ''}
|
||||
${isApiError ? `<div><strong>Retryable:</strong> ${error.isRetryable()}</div>` : ''}
|
||||
${isApiError ? `<div><strong>Connectivity:</strong> ${error.isConnectivityIssue()}</div>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="background: #1a1a1a; padding: 15px; border-radius: 8px; border: 1px solid #333;">
|
||||
<h3 style="color: #00aaff; margin-top: 0;">Environment</h3>
|
||||
<div style="line-height: 1.6;">
|
||||
<div><strong>Mode:</strong> ${process.env.NODE_ENV}</div>
|
||||
<div><strong>App Mode:</strong> ${process.env.NEXT_PUBLIC_GRIDPILOT_MODE || 'pre-launch'}</div>
|
||||
<div><strong>URL:</strong> ${window.location.href}</div>
|
||||
<div><strong>User Agent:</strong> ${navigator.userAgent}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${isApiError && error.context ? `
|
||||
<div style="background: #1a1a1a; padding: 15px; border-radius: 8px; border: 1px solid #333; margin-bottom: 20px;">
|
||||
<h3 style="color: #00ff88; margin-top: 0;">API Context</h3>
|
||||
<pre style="background: #000; padding: 10px; border-radius: 4px; overflow-x: auto; margin: 0;">${JSON.stringify(error.context, null, 2)}</pre>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<div style="background: #1a1a1a; padding: 15px; border-radius: 8px; border: 1px solid #333; margin-bottom: 20px;">
|
||||
<h3 style="color: #ff4444; margin-top: 0;">Stack Trace</h3>
|
||||
<pre style="background: #000; padding: 10px; border-radius: 4px; overflow-x: auto; margin: 0; white-space: pre-wrap;">${context.enhancedStack || error.stack || 'No stack trace available'}</pre>
|
||||
</div>
|
||||
|
||||
<div style="background: #1a1a1a; padding: 15px; border-radius: 8px; border: 1px solid #333; margin-bottom: 20px;">
|
||||
<h3 style="color: #ffaa00; margin-top: 0;">Additional Context</h3>
|
||||
<pre style="background: #000; padding: 10px; border-radius: 4px; overflow-x: auto; margin: 0;">${JSON.stringify(context, null, 2)}</pre>
|
||||
</div>
|
||||
|
||||
<div style="background: #1a1a1a; padding: 15px; border-radius: 8px; border: 1px solid #333;">
|
||||
<h3 style="color: #00aaff; margin-top: 0;">Quick Actions</h3>
|
||||
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
|
||||
<button onclick="navigator.clipboard.writeText(\`${error.message}\n\nStack:\n${error.stack}\n\nContext:\n${JSON.stringify(context, null, 2)}\`)"
|
||||
style="background: #0066cc; color: white; border: none; padding: 8px 12px; cursor: pointer; border-radius: 4px;">
|
||||
📋 Copy Error Details
|
||||
</button>
|
||||
<button onclick="window.location.reload()"
|
||||
style="background: #cc6600; color: white; border: none; padding: 8px 12px; cursor: pointer; border-radius: 4px;">
|
||||
🔄 Reload Page
|
||||
</button>
|
||||
<button onclick="console.clear(); console.log('Error details:', ${JSON.stringify({ error: error.message, stack: error.stack, context }).replace(/"/g, '"')})"
|
||||
style="background: #6600cc; color: white; border: none; padding: 8px 12px; cursor: pointer; border-radius: 4px;">
|
||||
📝 Log to Console
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 20px; padding: 15px; background: #222; border-radius: 4px; border-left: 4px solid #ffaa00;">
|
||||
<strong>💡 Tip:</strong> This overlay only appears in development mode. In production, errors are logged silently and handled gracefully.
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
this.logErrorWithMaximumDetail(error, context);
|
||||
this.addToHistory(error, context);
|
||||
|
||||
if (this.options.showDevOverlay) {
|
||||
this.showDevOverlay(error, context);
|
||||
|
||||
// Auto-capture for replay
|
||||
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 {
|
||||
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;
|
||||
}
|
||||
|
||||
// Remove overlay if exists
|
||||
const overlay = document.getElementById('gridpilot-error-overlay');
|
||||
if (overlay) {
|
||||
overlay.remove();
|
||||
}
|
||||
|
||||
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(),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user