289 lines
7.9 KiB
TypeScript
289 lines
7.9 KiB
TypeScript
'use client';
|
|
|
|
import { DevErrorPanel } from '@/components/shared/DevErrorPanel';
|
|
import { ErrorDisplay } from '@/components/shared/ErrorDisplay';
|
|
import { ApiError } from '@/lib/gateways/api/base/ApiError';
|
|
import { getGlobalErrorHandler } from '@/lib/infrastructure/GlobalErrorHandler';
|
|
import React, { Component, ErrorInfo, ReactNode, useState, version } from 'react';
|
|
|
|
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;
|
|
}
|