dev experience
This commit is contained in:
379
apps/website/components/errors/EnhancedErrorBoundary.tsx
Normal file
379
apps/website/components/errors/EnhancedErrorBoundary.tsx
Normal file
@@ -0,0 +1,379 @@
|
||||
'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 {
|
||||
return {
|
||||
hasError: true,
|
||||
error,
|
||||
errorInfo: null,
|
||||
isDev: process.env.NODE_ENV === 'development',
|
||||
};
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
|
||||
// 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> {
|
||||
return (props: P) => (
|
||||
<EnhancedErrorBoundary {...options}>
|
||||
<Component {...props} />
|
||||
</EnhancedErrorBoundary>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user