Files
gridpilot.gg/apps/website/components/errors/EnhancedErrorBoundary.tsx
2026-01-04 23:02:28 +01:00

405 lines
13 KiB
TypeScript

'use client';
import React, { Component, ReactNode, ErrorInfo } from 'react';
import { ApiError } from '@/lib/api/base/ApiError';
import { getGlobalErrorHandler } from '@/lib/infrastructure/GlobalErrorHandler';
import { DevErrorPanel } from './DevErrorPanel';
import { ErrorDisplay } from './ErrorDisplay';
interface Props {
children: ReactNode;
fallback?: ReactNode;
onError?: (error: Error, errorInfo: ErrorInfo) => void;
onReset?: () => void;
/**
* Whether to show the enhanced dev overlay
*/
enableDevOverlay?: boolean;
/**
* Additional context to include with errors
*/
context?: Record<string, unknown>;
}
interface State {
hasError: boolean;
error: Error | ApiError | null;
errorInfo: ErrorInfo | null;
isDev: boolean;
}
/**
* Enhanced React Error Boundary with maximum developer transparency
* Integrates with GlobalErrorHandler and provides detailed debugging info
*/
export class EnhancedErrorBoundary extends Component<Props, State> {
private globalErrorHandler: ReturnType<typeof getGlobalErrorHandler>;
constructor(props: Props) {
super(props);
this.state = {
hasError: false,
error: null,
errorInfo: null,
isDev: process.env.NODE_ENV === 'development',
};
this.globalErrorHandler = getGlobalErrorHandler();
}
static getDerivedStateFromError(error: Error): State {
// Don't catch Next.js navigation errors (redirect, notFound, etc.)
if (error && typeof error === 'object' && 'digest' in error) {
const digest = (error as any).digest;
if (typeof digest === 'string' && (
digest.startsWith('NEXT_REDIRECT') ||
digest.startsWith('NEXT_NOT_FOUND')
)) {
// Re-throw Next.js navigation errors so they can be handled properly
throw error;
}
}
return {
hasError: true,
error,
errorInfo: null,
isDev: process.env.NODE_ENV === 'development',
};
}
componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
// Don't catch Next.js navigation errors (redirect, notFound, etc.)
if (error && typeof error === 'object' && 'digest' in error) {
const digest = (error as any).digest;
if (typeof digest === 'string' && (
digest.startsWith('NEXT_REDIRECT') ||
digest.startsWith('NEXT_NOT_FOUND')
)) {
// Re-throw Next.js navigation errors so they can be handled properly
throw error;
}
}
// Add to React error history
const reactErrors = (window as any).__GRIDPILOT_REACT_ERRORS__ || [];
reactErrors.push({
error,
errorInfo,
timestamp: new Date().toISOString(),
componentStack: errorInfo.componentStack,
});
(window as any).__GRIDPILOT_REACT_ERRORS__ = reactErrors;
// Report to global error handler with enhanced context
const enhancedContext = {
...this.props.context,
source: 'react_error_boundary',
componentStack: errorInfo.componentStack,
reactVersion: React.version,
componentName: this.getComponentName(errorInfo),
};
// Use global error handler for maximum transparency
this.globalErrorHandler.report(error, enhancedContext);
// Call custom error handler if provided
if (this.props.onError) {
this.props.onError(error, errorInfo);
}
// Show dev overlay if enabled
if (this.props.enableDevOverlay && this.state.isDev) {
// The global handler will show the overlay, but we can add additional React-specific info
this.showReactDevOverlay(error, errorInfo);
}
// Log to console with maximum detail
if (this.state.isDev) {
this.logReactErrorWithMaximumDetail(error, errorInfo);
}
}
componentDidMount(): void {
// Initialize global error handler if not already done
if (this.props.enableDevOverlay && this.state.isDev) {
this.globalErrorHandler.initialize();
}
}
componentWillUnmount(): void {
// Clean up if needed
}
/**
* Extract component name from error info
*/
private getComponentName(errorInfo: ErrorInfo): string | undefined {
try {
const stack = errorInfo.componentStack;
if (stack) {
const match = stack.match(/at (\w+)/);
return match ? match[1] : undefined;
}
} catch {
// Ignore
}
return undefined;
}
/**
* Show React-specific dev overlay
*/
private showReactDevOverlay(error: Error, errorInfo: ErrorInfo): void {
const existingOverlay = document.getElementById('gridpilot-react-overlay');
if (existingOverlay) {
this.updateReactDevOverlay(existingOverlay, error, errorInfo);
return;
}
const overlay = document.createElement('div');
overlay.id = 'gridpilot-react-overlay';
overlay.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 90%;
max-width: 800px;
max-height: 80vh;
background: #1a1a1a;
color: #fff;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 12px;
z-index: 999998;
overflow: auto;
padding: 20px;
border: 3px solid #ff6600;
border-radius: 8px;
box-shadow: 0 0 40px rgba(255, 102, 0, 0.6);
`;
this.updateReactDevOverlay(overlay, error, errorInfo);
document.body.appendChild(overlay);
// Add keyboard shortcut to dismiss
const dismissHandler = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
overlay.remove();
document.removeEventListener('keydown', dismissHandler);
}
};
document.addEventListener('keydown', dismissHandler);
}
/**
* Update React dev overlay
*/
private updateReactDevOverlay(overlay: HTMLElement, error: Error, errorInfo: ErrorInfo): void {
overlay.innerHTML = `
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 15px;">
<h2 style="color: #ff6600; margin: 0; font-size: 18px;">⚛️ React Component Error</h2>
<button onclick="this.parentElement.parentElement.remove()"
style="background: #ff6600; color: white; border: none; padding: 6px 12px; cursor: pointer; border-radius: 4px; font-weight: bold;">
CLOSE
</button>
</div>
<div style="background: #000; padding: 12px; border-radius: 4px; margin-bottom: 15px; border-left: 3px solid #ff6600;">
<div style="color: #ff6600; font-weight: bold; margin-bottom: 5px;">Error Message</div>
<div style="color: #fff;">${error.message}</div>
</div>
<div style="background: #000; padding: 12px; border-radius: 4px; margin-bottom: 15px; border-left: 3px solid #00aaff;">
<div style="color: #00aaff; font-weight: bold; margin-bottom: 5px;">Component Stack Trace</div>
<pre style="margin: 0; white-space: pre-wrap; color: #888;">${errorInfo.componentStack || 'No component stack available'}</pre>
</div>
<div style="background: #000; padding: 12px; border-radius: 4px; margin-bottom: 15px; border-left: 3px solid #ffaa00;">
<div style="color: #ffaa00; font-weight: bold; margin-bottom: 5px;">JavaScript Stack Trace</div>
<pre style="margin: 0; white-space: pre-wrap; color: #888; overflow-x: auto;">${error.stack || 'No stack trace available'}</pre>
</div>
<div style="background: #000; padding: 12px; border-radius: 4px; margin-bottom: 15px; border-left: 3px solid #00ff88;">
<div style="color: #00ff88; font-weight: bold; margin-bottom: 5px;">React Information</div>
<div style="line-height: 1.6; color: #888;">
<div>React Version: ${React.version}</div>
<div>Error Boundary: Active</div>
<div>Timestamp: ${new Date().toLocaleTimeString()}</div>
</div>
</div>
<div style="background: #000; padding: 12px; border-radius: 4px; border-left: 3px solid #aa00ff;">
<div style="color: #aa00ff; font-weight: bold; margin-bottom: 5px;">Quick Actions</div>
<div style="display: flex; gap: 8px; flex-wrap: wrap;">
<button onclick="navigator.clipboard.writeText(\`${error.message}\n\nComponent Stack:\n${errorInfo.componentStack}\n\nStack:\n${error.stack}\`)"
style="background: #0066cc; color: white; border: none; padding: 6px 10px; cursor: pointer; border-radius: 4px; font-size: 11px;">
📋 Copy Details
</button>
<button onclick="window.location.reload()"
style="background: #cc6600; color: white; border: none; padding: 6px 10px; cursor: pointer; border-radius: 4px; font-size: 11px;">
🔄 Reload
</button>
</div>
</div>
<div style="margin-top: 15px; padding: 10px; background: #222; border-radius: 4px; border-left: 3px solid #888; font-size: 11px; color: #888;">
💡 This React error boundary caught a component rendering error. Check the console for additional details from the global error handler.
</div>
`;
}
/**
* Log React error with maximum detail
*/
private logReactErrorWithMaximumDetail(error: Error, errorInfo: ErrorInfo): void {
console.groupCollapsed('%c[REACT ERROR BOUNDARY] Component Rendering Failed',
'color: #ff6600; font-weight: bold; font-size: 14px;'
);
console.log('Error Details:', {
message: error.message,
name: error.name,
stack: error.stack,
});
console.log('Component Stack:', errorInfo.componentStack);
console.log('React Context:', {
reactVersion: React.version,
component: this.getComponentName(errorInfo),
timestamp: new Date().toISOString(),
});
console.log('Props:', this.props);
console.log('State:', this.state);
// Show component hierarchy if available
try {
const hierarchy = this.getComponentHierarchy();
if (hierarchy) {
console.log('Component Hierarchy:', hierarchy);
}
} catch {
// Ignore hierarchy extraction errors
}
console.groupEnd();
}
/**
* Attempt to extract component hierarchy (for debugging)
*/
private getComponentHierarchy(): string | null {
// This is a simplified version - in practice, you might want to use React DevTools
// or other methods to get the full component tree
return null;
}
resetError = (): void => {
this.setState({ hasError: false, error: null, errorInfo: null });
if (this.props.onReset) {
this.props.onReset();
}
};
render(): ReactNode {
if (this.state.hasError && this.state.error) {
if (this.props.fallback) {
return this.props.fallback;
}
// Show different UI based on environment
if (this.state.isDev) {
return (
<DevErrorPanel
error={this.state.error instanceof ApiError ? this.state.error : new ApiError(
this.state.error.message,
'UNKNOWN_ERROR',
{
timestamp: new Date().toISOString(),
source: 'react_error_boundary',
},
this.state.error
)}
onReset={this.resetError}
/>
);
}
return (
<ErrorDisplay
error={this.state.error instanceof ApiError ? this.state.error : new ApiError(
this.state.error.message,
'UNKNOWN_ERROR',
{
timestamp: new Date().toISOString(),
source: 'react_error_boundary',
},
this.state.error
)}
onRetry={this.resetError}
/>
);
}
return this.props.children;
}
}
/**
* Hook-based alternative for functional components
*/
export function useEnhancedErrorBoundary() {
const [error, setError] = React.useState<Error | ApiError | null>(null);
const [errorInfo, setErrorInfo] = React.useState<ErrorInfo | null>(null);
const [isDev] = React.useState(process.env.NODE_ENV === 'development');
const handleError = (err: Error, info: ErrorInfo) => {
setError(err);
setErrorInfo(info);
// Report to global handler
const globalHandler = getGlobalErrorHandler();
globalHandler.report(err, {
source: 'react_hook_boundary',
componentStack: info.componentStack,
});
};
const reset = () => {
setError(null);
setErrorInfo(null);
};
return {
error,
errorInfo,
isDev,
handleError,
reset,
ErrorBoundary: ({ children, enableDevOverlay }: { children: ReactNode; enableDevOverlay?: boolean }) => (
<EnhancedErrorBoundary
onError={handleError}
enableDevOverlay={enableDevOverlay}
>
{children}
</EnhancedErrorBoundary>
),
};
}
/**
* Higher-order component wrapper for easy usage
*/
export function withEnhancedErrorBoundary<P extends object>(
Component: React.ComponentType<P>,
options: Omit<Props, 'children'> = {}
): React.FC<P> {
const WrappedComponent = (props: P) => (
<EnhancedErrorBoundary {...options}>
<Component {...props} />
</EnhancedErrorBoundary>
);
WrappedComponent.displayName = `withEnhancedErrorBoundary(${Component.displayName || Component.name || 'Component'})`;
return WrappedComponent;
}