Files
gridpilot.gg/apps/website/components/errors/EnhancedErrorBoundary.tsx
2026-01-15 19:55:46 +01:00

289 lines
7.9 KiB
TypeScript

'use client';
import React, { Component, ReactNode, ErrorInfo, useState, version } from 'react';
import { ApiError } from '@/lib/api/base/ApiError';
import { getGlobalErrorHandler } from '@/lib/infrastructure/GlobalErrorHandler';
import { DevErrorPanel } from './DevErrorPanel';
import { ErrorDisplay } from '@/components/shared/state/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;
}
interface GridPilotWindow extends Window {
__GRIDPILOT_REACT_ERRORS__?: Array<{
error: Error;
errorInfo: ErrorInfo;
timestamp: string;
componentStack?: string;
}>;
}
/**
* 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 Record<string, unknown>).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 Record<string, unknown>).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
if (typeof window !== 'undefined') {
const gpWindow = window as unknown as GridPilotWindow;
const reactErrors = gpWindow.__GRIDPILOT_REACT_ERRORS__ || [];
gpWindow.__GRIDPILOT_REACT_ERRORS__ = [
...reactErrors,
{
error,
errorInfo,
timestamp: new Date().toISOString(),
componentStack: errorInfo.componentStack || undefined,
}
];
}
// Report to global error handler with enhanced context
const enhancedContext = {
...this.props.context,
source: 'react_error_boundary',
componentStack: errorInfo.componentStack,
reactVersion: 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);
}
// 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();
}
}
/**
* 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;
}
/**
* 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: version,
component: this.getComponentName(errorInfo),
timestamp: new Date().toISOString(),
});
console.log('Props:', this.props);
console.log('State:', this.state);
console.groupEnd();
}
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] = useState<Error | ApiError | null>(null);
const [errorInfo, setErrorInfo] = useState<ErrorInfo | null>(null);
const [isDev] = 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;
}