dev experience

This commit is contained in:
2026-01-01 16:40:14 +01:00
parent 17d715f259
commit df7e5db5ba
12 changed files with 3745 additions and 6 deletions

View File

@@ -0,0 +1,365 @@
'use client';
import { useState, useEffect } from 'react';
import { Bug, X, Settings, Shield, Activity } from 'lucide-react';
import { getGlobalErrorHandler } from '@/lib/infrastructure/GlobalErrorHandler';
import { getGlobalApiLogger } from '@/lib/infrastructure/ApiRequestLogger';
interface DebugModeToggleProps {
/**
* Whether to show the toggle (auto-detected from environment)
*/
show?: boolean;
}
/**
* Debug Mode Toggle Component
* Provides a floating interface to control debug features and view real-time metrics
*/
export function DebugModeToggle({ show }: DebugModeToggleProps) {
const [isOpen, setIsOpen] = useState(false);
const [debugEnabled, setDebugEnabled] = useState(false);
const [metrics, setMetrics] = useState({
errors: 0,
apiRequests: 0,
apiFailures: 0,
});
const isDev = process.env.NODE_ENV === 'development';
const shouldShow = show ?? isDev;
useEffect(() => {
if (!shouldShow) return;
// Load debug state from localStorage
const saved = localStorage.getItem('gridpilot_debug_enabled');
if (saved === 'true') {
setDebugEnabled(true);
initializeDebugFeatures();
}
// Update metrics every 2 seconds
const interval = setInterval(updateMetrics, 2000);
return () => clearInterval(interval);
}, [shouldShow]);
useEffect(() => {
// Save debug state
if (shouldShow) {
localStorage.setItem('gridpilot_debug_enabled', debugEnabled.toString());
}
}, [debugEnabled, shouldShow]);
const updateMetrics = () => {
if (!debugEnabled) return;
const globalHandler = getGlobalErrorHandler();
const apiLogger = getGlobalApiLogger();
const errorStats = globalHandler.getStats();
const apiStats = apiLogger.getStats();
setMetrics({
errors: errorStats.total,
apiRequests: apiStats.total,
apiFailures: apiStats.failed,
});
};
const initializeDebugFeatures = () => {
const globalHandler = getGlobalErrorHandler();
const apiLogger = getGlobalApiLogger();
// Initialize global error handler
globalHandler.initialize();
// Override fetch with logging
if (!(window as any).__GRIDPILOT_FETCH_LOGGED__) {
const loggedFetch = apiLogger.createLoggedFetch();
window.fetch = loggedFetch as any;
(window as any).__GRIDPILOT_FETCH_LOGGED__ = true;
}
// Expose to window for easy access
(window as any).__GRIDPILOT_GLOBAL_HANDLER__ = globalHandler;
(window as any).__GRIDPILOT_API_LOGGER__ = apiLogger;
console.log('%c[DEBUG MODE] Enabled', 'color: #00ff88; font-weight: bold; font-size: 14px;');
console.log('Available globals:', {
__GRIDPILOT_GLOBAL_HANDLER__: globalHandler,
__GRIDPILOT_API_LOGGER__: apiLogger,
__GRIDPILOT_REACT_ERRORS__: (window as any).__GRIDPILOT_REACT_ERRORS__ || [],
});
};
const toggleDebug = () => {
const newEnabled = !debugEnabled;
setDebugEnabled(newEnabled);
if (newEnabled) {
initializeDebugFeatures();
} else {
// Disable debug features
const globalHandler = getGlobalErrorHandler();
globalHandler.destroy();
console.log('%c[DEBUG MODE] Disabled', 'color: #ff4444; font-weight: bold; font-size: 14px;');
}
};
const triggerTestError = () => {
if (!debugEnabled) return;
// Trigger a test API error
const testError = new Error('This is a test error for debugging');
(testError as any).type = 'TEST_ERROR';
const globalHandler = getGlobalErrorHandler();
globalHandler.report(testError, { test: true, timestamp: Date.now() });
console.log('%c[TEST] Error triggered', 'color: #ffaa00; font-weight: bold;', testError);
};
const triggerTestApiCall = async () => {
if (!debugEnabled) return;
try {
// This will fail and be logged
await fetch('https://httpstat.us/500');
} catch (error) {
// Already logged by interceptor
console.log('%c[TEST] API call completed', 'color: #00aaff; font-weight: bold;');
}
};
const clearAllLogs = () => {
const globalHandler = getGlobalErrorHandler();
const apiLogger = getGlobalApiLogger();
globalHandler.clearHistory();
apiLogger.clearHistory();
setMetrics({ errors: 0, apiRequests: 0, apiFailures: 0 });
console.log('%c[DEBUG] All logs cleared', 'color: #00ff88; font-weight: bold;');
};
const copyDebugInfo = async () => {
const globalHandler = getGlobalErrorHandler();
const apiLogger = getGlobalApiLogger();
const debugInfo = {
timestamp: new Date().toISOString(),
environment: {
mode: process.env.NODE_ENV,
appMode: process.env.NEXT_PUBLIC_GRIDPILOT_MODE,
version: process.env.NEXT_PUBLIC_APP_VERSION,
},
browser: {
userAgent: navigator.userAgent,
language: navigator.language,
platform: navigator.platform,
},
errors: globalHandler.getStats(),
api: apiLogger.getStats(),
reactErrors: (window as any).__GRIDPILOT_REACT_ERRORS__ || [],
};
try {
await navigator.clipboard.writeText(JSON.stringify(debugInfo, null, 2));
console.log('%c[DEBUG] Debug info copied to clipboard', 'color: #00ff88; font-weight: bold;');
} catch (err) {
console.error('Failed to copy:', err);
}
};
if (!shouldShow) {
return null;
}
return (
<div className="fixed bottom-4 left-4 z-50">
{/* Main Toggle Button */}
{!isOpen && (
<button
onClick={() => setIsOpen(true)}
className={`p-3 rounded-full shadow-lg transition-all ${
debugEnabled
? 'bg-green-600 hover:bg-green-700 text-white'
: 'bg-iron-gray hover:bg-charcoal-outline text-gray-300'
}`}
title={debugEnabled ? 'Debug Mode Active' : 'Enable Debug Mode'}
>
<Bug className="w-5 h-5" />
</button>
)}
{/* Debug Panel */}
{isOpen && (
<div className="w-80 bg-deep-graphite border border-charcoal-outline rounded-xl shadow-2xl overflow-hidden">
{/* Header */}
<div className="flex items-center justify-between px-3 py-2 bg-iron-gray/50 border-b border-charcoal-outline">
<div className="flex items-center gap-2">
<Bug className="w-4 h-4 text-green-400" />
<span className="text-sm font-semibold text-white">Debug Control</span>
</div>
<button
onClick={() => setIsOpen(false)}
className="p-1 hover:bg-charcoal-outline rounded"
>
<X className="w-4 h-4 text-gray-400" />
</button>
</div>
{/* Content */}
<div className="p-3 space-y-3">
{/* Debug Toggle */}
<div className="flex items-center justify-between bg-iron-gray/30 p-2 rounded border border-charcoal-outline">
<div className="flex items-center gap-2">
<Shield className={`w-4 h-4 ${debugEnabled ? 'text-green-400' : 'text-gray-500'}`} />
<span className="text-sm font-medium">Debug Mode</span>
</div>
<button
onClick={toggleDebug}
className={`px-3 py-1 rounded text-xs font-bold transition-colors ${
debugEnabled
? 'bg-green-600 text-white hover:bg-green-700'
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
}`}
>
{debugEnabled ? 'ON' : 'OFF'}
</button>
</div>
{/* Metrics */}
{debugEnabled && (
<div className="grid grid-cols-3 gap-2">
<div className="bg-iron-gray border border-charcoal-outline rounded p-2 text-center">
<div className="text-[10px] text-gray-500">Errors</div>
<div className="text-lg font-bold text-red-400">{metrics.errors}</div>
</div>
<div className="bg-iron-gray border border-charcoal-outline rounded p-2 text-center">
<div className="text-[10px] text-gray-500">API</div>
<div className="text-lg font-bold text-blue-400">{metrics.apiRequests}</div>
</div>
<div className="bg-iron-gray border border-charcoal-outline rounded p-2 text-center">
<div className="text-[10px] text-gray-500">Failures</div>
<div className="text-lg font-bold text-yellow-400">{metrics.apiFailures}</div>
</div>
</div>
)}
{/* Actions */}
{debugEnabled && (
<div className="space-y-2">
<div className="text-xs font-semibold text-gray-400">Test Actions</div>
<div className="grid grid-cols-2 gap-2">
<button
onClick={triggerTestError}
className="px-2 py-1.5 bg-red-600 hover:bg-red-700 text-white rounded text-xs font-medium"
>
Test Error
</button>
<button
onClick={triggerTestApiCall}
className="px-2 py-1.5 bg-blue-600 hover:bg-blue-700 text-white rounded text-xs font-medium"
>
Test API
</button>
</div>
<div className="text-xs font-semibold text-gray-400 mt-2">Utilities</div>
<div className="grid grid-cols-2 gap-2">
<button
onClick={copyDebugInfo}
className="px-2 py-1.5 bg-iron-gray hover:bg-charcoal-outline text-gray-300 rounded text-xs font-medium border border-charcoal-outline"
>
Copy Info
</button>
<button
onClick={clearAllLogs}
className="px-2 py-1.5 bg-iron-gray hover:bg-charcoal-outline text-gray-300 rounded text-xs font-medium border border-charcoal-outline"
>
Clear Logs
</button>
</div>
</div>
)}
{/* Quick Links */}
{debugEnabled && (
<div className="space-y-1">
<div className="text-xs font-semibold text-gray-400">Quick Access</div>
<div className="text-[10px] text-gray-500 font-mono space-y-0.5">
<div> window.__GRIDPILOT_GLOBAL_HANDLER__</div>
<div> window.__GRIDPILOT_API_LOGGER__</div>
<div> window.__GRIDPILOT_REACT_ERRORS__</div>
</div>
</div>
)}
{/* Status */}
<div className="text-[10px] text-gray-500 text-center pt-2 border-t border-charcoal-outline">
{debugEnabled ? 'Debug features active' : 'Debug mode disabled'}
{isDev && ' • Development Environment'}
</div>
</div>
</div>
)}
</div>
);
}
/**
* Hook for programmatic debug control
*/
export function useDebugMode() {
const [debugEnabled, setDebugEnabled] = useState(false);
useEffect(() => {
const saved = localStorage.getItem('gridpilot_debug_enabled');
setDebugEnabled(saved === 'true');
}, []);
const enable = () => {
setDebugEnabled(true);
localStorage.setItem('gridpilot_debug_enabled', 'true');
// Initialize debug features
const globalHandler = getGlobalErrorHandler();
globalHandler.initialize();
const apiLogger = getGlobalApiLogger();
if (!(window as any).__GRIDPILOT_FETCH_LOGGED__) {
const loggedFetch = apiLogger.createLoggedFetch();
window.fetch = loggedFetch as any;
(window as any).__GRIDPILOT_FETCH_LOGGED__ = true;
}
(window as any).__GRIDPILOT_GLOBAL_HANDLER__ = globalHandler;
(window as any).__GRIDPILOT_API_LOGGER__ = apiLogger;
};
const disable = () => {
setDebugEnabled(false);
localStorage.setItem('gridpilot_debug_enabled', 'false');
const globalHandler = getGlobalErrorHandler();
globalHandler.destroy();
};
const toggle = () => {
if (debugEnabled) {
disable();
} else {
enable();
}
};
return {
enabled: debugEnabled,
enable,
disable,
toggle,
};
}

View File

@@ -3,7 +3,7 @@
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId';
import { useNotifications } from '@/components/notifications/NotificationProvider';
import type { NotificationVariant } from '@/components/notifications/notificationTypes';
import { Wrench, ChevronDown, ChevronUp, X, MessageSquare, Activity, LogIn } from 'lucide-react';
import { Wrench, ChevronDown, ChevronUp, X, MessageSquare, Activity, LogIn, Play } from 'lucide-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { ApiConnectionMonitor } from '@/lib/api/base/ApiConnectionMonitor';
@@ -16,6 +16,7 @@ import { UrgencySection } from './sections/UrgencySection';
import { NotificationSendSection } from './sections/NotificationSendSection';
import { APIStatusSection } from './sections/APIStatusSection';
import { LoginSection } from './sections/LoginSection';
import { ReplaySection } from './sections/ReplaySection';
// Import types
import type { DemoNotificationType, DemoUrgency, LoginMode } from './types';
@@ -410,6 +411,16 @@ export default function DevToolbar() {
onLogout={handleLogout}
/>
</Accordion>
{/* Replay Section - Accordion */}
<Accordion
title="Error Replay"
icon={<Play className="w-4 h-4 text-gray-400" />}
isOpen={openAccordion === 'replay'}
onToggle={() => setOpenAccordion(openAccordion === 'replay' ? null : 'replay')}
>
<ReplaySection />
</Accordion>
</div>
)}

View File

@@ -0,0 +1,160 @@
import { useState, useEffect } from 'react';
import { Play, Copy, Trash2, Download, Clock } from 'lucide-react';
import { getGlobalReplaySystem } from '@/lib/infrastructure/ErrorReplay';
interface ReplayEntry {
id: string;
timestamp: string;
error: string;
type: string;
}
export function ReplaySection() {
const [replays, setReplays] = useState<ReplayEntry[]>([]);
const [selectedReplay, setSelectedReplay] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
loadReplays();
}, []);
const loadReplays = () => {
const system = getGlobalReplaySystem();
const index = system.getReplayIndex();
setReplays(index);
};
const handleReplay = async (replayId: string) => {
setLoading(true);
try {
const system = getGlobalReplaySystem();
await system.replay(replayId);
} catch (error) {
console.error('Replay failed:', error);
} finally {
setLoading(false);
}
};
const handleExport = (replayId: string) => {
const system = getGlobalReplaySystem();
const data = system.exportReplay(replayId, 'json');
const blob = new Blob([data], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `replay_${replayId}.json`;
a.click();
URL.revokeObjectURL(url);
};
const handleCopy = async (replayId: string) => {
const system = getGlobalReplaySystem();
const data = system.exportReplay(replayId, 'json');
try {
await navigator.clipboard.writeText(data);
console.log('Replay data copied to clipboard');
} catch (err) {
console.error('Failed to copy:', err);
}
};
const handleDelete = (replayId: string) => {
if (confirm('Delete this replay?')) {
const system = getGlobalReplaySystem();
system.deleteReplay(replayId);
loadReplays();
}
};
const handleClearAll = () => {
if (confirm('Clear all replays?')) {
const system = getGlobalReplaySystem();
system.clearAll();
loadReplays();
}
};
return (
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-xs font-semibold text-gray-400">Error Replay</span>
<div className="flex gap-1">
<button
onClick={loadReplays}
className="p-1 hover:bg-charcoal-outline rounded"
title="Refresh"
>
<Clock className="w-3 h-3 text-gray-400" />
</button>
<button
onClick={handleClearAll}
className="p-1 hover:bg-charcoal-outline rounded"
title="Clear All"
>
<Trash2 className="w-3 h-3 text-red-400" />
</button>
</div>
</div>
{replays.length === 0 ? (
<div className="text-xs text-gray-500 text-center py-2">
No replays available
</div>
) : (
<div className="space-y-1 max-h-48 overflow-auto">
{replays.map((replay) => (
<div
key={replay.id}
className="bg-deep-graphite border border-charcoal-outline rounded p-2 text-xs"
>
<div className="flex items-start justify-between gap-2 mb-1">
<div className="flex-1 min-w-0">
<div className="font-mono text-red-400 font-bold truncate">
{replay.type}
</div>
<div className="text-gray-300 truncate">{replay.error}</div>
<div className="text-gray-500 text-[10px]">
{new Date(replay.timestamp).toLocaleTimeString()}
</div>
</div>
</div>
<div className="flex gap-1 mt-1">
<button
onClick={() => handleReplay(replay.id)}
disabled={loading}
className="flex items-center gap-1 px-2 py-1 bg-green-600 hover:bg-green-700 text-white rounded"
>
<Play className="w-3 h-3" />
Replay
</button>
<button
onClick={() => handleExport(replay.id)}
className="flex items-center gap-1 px-2 py-1 bg-iron-gray hover:bg-charcoal-outline text-gray-300 rounded border border-charcoal-outline"
>
<Download className="w-3 h-3" />
Export
</button>
<button
onClick={() => handleCopy(replay.id)}
className="flex items-center gap-1 px-2 py-1 bg-iron-gray hover:bg-charcoal-outline text-gray-300 rounded border border-charcoal-outline"
>
<Copy className="w-3 h-3" />
Copy
</button>
<button
onClick={() => handleDelete(replay.id)}
className="flex items-center gap-1 px-2 py-1 bg-iron-gray hover:bg-charcoal-outline text-gray-300 rounded border border-charcoal-outline"
>
<Trash2 className="w-3 h-3" />
</button>
</div>
</div>
))}
</div>
)}
</div>
);
}

View 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>
);
}

View File

@@ -0,0 +1,617 @@
'use client';
import { useState, useEffect } from 'react';
import { getGlobalErrorHandler } from '@/lib/infrastructure/GlobalErrorHandler';
import { getGlobalApiLogger } from '@/lib/infrastructure/ApiRequestLogger';
import { ApiError } from '@/lib/api/base/ApiError';
import {
Activity,
AlertTriangle,
Clock,
Copy,
RefreshCw,
Terminal,
Database,
Zap,
Bug,
Shield,
Globe,
Cpu,
FileText,
Trash2,
Download,
Search
} from 'lucide-react';
interface ErrorAnalyticsDashboardProps {
/**
* Auto-refresh interval in milliseconds
*/
refreshInterval?: number;
/**
* Whether to show in production (default: false)
*/
showInProduction?: boolean;
}
interface ErrorStats {
totalErrors: number;
errorsByType: Record<string, number>;
errorsByTime: Array<{ time: string; count: number }>;
recentErrors: Array<{
timestamp: string;
message: string;
type: string;
context?: unknown;
}>;
apiStats: {
totalRequests: number;
successful: number;
failed: number;
averageDuration: number;
slowestRequests: Array<{ url: string; duration: number }>;
};
environment: {
mode: string;
appMode: string;
version?: string;
buildTime?: string;
};
}
/**
* Comprehensive Error Analytics Dashboard
* Shows real-time error statistics, API metrics, and environment details
*/
export function ErrorAnalyticsDashboard({
refreshInterval = 5000,
showInProduction = false
}: ErrorAnalyticsDashboardProps) {
const [stats, setStats] = useState<ErrorStats | null>(null);
const [isExpanded, setIsExpanded] = useState(true);
const [selectedTab, setSelectedTab] = useState<'errors' | 'api' | 'environment' | 'raw'>('errors');
const [searchTerm, setSearchTerm] = useState('');
const [copied, setCopied] = useState(false);
const isDev = process.env.NODE_ENV === 'development';
// Don't show in production unless explicitly enabled
if (!isDev && !showInProduction) {
return null;
}
useEffect(() => {
updateStats();
if (refreshInterval > 0) {
const interval = setInterval(updateStats, refreshInterval);
return () => clearInterval(interval);
}
}, [refreshInterval]);
const updateStats = () => {
const globalHandler = getGlobalErrorHandler();
const apiLogger = getGlobalApiLogger();
const errorHistory = globalHandler.getErrorHistory();
const errorStats = globalHandler.getStats();
const apiHistory = apiLogger.getHistory();
const apiStats = apiLogger.getStats();
// Group errors by time (last 10 minutes)
const timeGroups = new Map<string, number>();
const now = Date.now();
const tenMinutesAgo = now - (10 * 60 * 1000);
errorHistory.forEach(entry => {
const entryTime = new Date(entry.timestamp).getTime();
if (entryTime >= tenMinutesAgo) {
const timeKey = new Date(entry.timestamp).toLocaleTimeString();
timeGroups.set(timeKey, (timeGroups.get(timeKey) || 0) + 1);
}
});
const errorsByTime = Array.from(timeGroups.entries())
.map(([time, count]) => ({ time, count }))
.sort((a, b) => a.time.localeCompare(b.time));
const recentErrors = errorHistory.slice(-10).reverse().map(entry => ({
timestamp: entry.timestamp,
message: entry.error.message,
type: entry.error instanceof ApiError ? entry.error.type : entry.error.name || 'Error',
context: entry.context,
}));
const slowestRequests = apiLogger.getSlowestRequests(5).map(log => ({
url: log.url,
duration: log.response?.duration || 0,
}));
setStats({
totalErrors: errorStats.total,
errorsByType: errorStats.byType,
errorsByTime,
recentErrors,
apiStats: {
totalRequests: apiStats.total,
successful: apiStats.successful,
failed: apiStats.failed,
averageDuration: apiStats.averageDuration,
slowestRequests,
},
environment: {
mode: process.env.NODE_ENV || 'unknown',
appMode: process.env.NEXT_PUBLIC_GRIDPILOT_MODE || 'pre-launch',
version: process.env.NEXT_PUBLIC_APP_VERSION,
buildTime: process.env.NEXT_PUBLIC_BUILD_TIME,
},
});
};
const copyToClipboard = async (data: unknown) => {
try {
await navigator.clipboard.writeText(JSON.stringify(data, null, 2));
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (err) {
// Silent failure
}
};
const exportAllData = () => {
const globalHandler = getGlobalErrorHandler();
const apiLogger = getGlobalApiLogger();
const exportData = {
timestamp: new Date().toISOString(),
errors: globalHandler.getErrorHistory(),
apiRequests: apiLogger.getHistory(),
stats: stats,
};
const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `gridpilot-error-report-${Date.now()}.json`;
a.click();
URL.revokeObjectURL(url);
};
const clearAllData = () => {
if (confirm('Are you sure you want to clear all error and API logs?')) {
const globalHandler = getGlobalErrorHandler();
const apiLogger = getGlobalApiLogger();
globalHandler.clearHistory();
apiLogger.clearHistory();
updateStats();
}
};
const filteredRecentErrors = stats?.recentErrors.filter(error =>
searchTerm === '' ||
error.message.toLowerCase().includes(searchTerm.toLowerCase()) ||
error.type.toLowerCase().includes(searchTerm.toLowerCase())
) || [];
if (!isExpanded) {
return (
<button
onClick={() => setIsExpanded(true)}
className="fixed bottom-4 left-4 z-50 p-3 bg-iron-gray border border-charcoal-outline rounded-full shadow-lg hover:bg-charcoal-outline transition-colors"
title="Open Error Analytics"
>
<Activity className="w-5 h-5 text-red-400" />
</button>
);
}
return (
<div className="fixed bottom-4 left-4 z-50 w-96 max-h-[80vh] bg-deep-graphite border border-charcoal-outline rounded-xl shadow-2xl overflow-hidden flex flex-col">
{/* Header */}
<div className="flex items-center justify-between px-4 py-3 bg-iron-gray/50 border-b border-charcoal-outline">
<div className="flex items-center gap-2">
<Activity className="w-4 h-4 text-red-400" />
<span className="text-sm font-semibold text-white">Error Analytics</span>
{isDev && (
<span className="px-1.5 py-0.5 text-[10px] font-medium bg-red-500/20 text-red-400 rounded">
DEV
</span>
)}
</div>
<div className="flex items-center gap-1">
<button
onClick={updateStats}
className="p-1.5 hover:bg-charcoal-outline rounded transition-colors"
title="Refresh"
>
<RefreshCw className="w-3 h-3 text-gray-400" />
</button>
<button
onClick={() => setIsExpanded(false)}
className="p-1.5 hover:bg-charcoal-outline rounded transition-colors"
title="Minimize"
>
<span className="text-gray-400 text-xs font-bold">_</span>
</button>
</div>
</div>
{/* Tabs */}
<div className="flex border-b border-charcoal-outline bg-iron-gray/30">
{[
{ id: 'errors', label: 'Errors', icon: AlertTriangle },
{ id: 'api', label: 'API', icon: Globe },
{ id: 'environment', label: 'Env', icon: Cpu },
{ id: 'raw', label: 'Raw', icon: FileText },
].map(tab => (
<button
key={tab.id}
onClick={() => setSelectedTab(tab.id as any)}
className={`flex-1 flex items-center justify-center gap-1 px-2 py-2 text-xs font-medium transition-colors ${
selectedTab === tab.id
? 'bg-deep-graphite text-white border-b-2 border-red-400'
: 'text-gray-400 hover:bg-charcoal-outline hover:text-gray-200'
}`}
>
<tab.icon className="w-3 h-3" />
{tab.label}
</button>
))}
</div>
{/* Content */}
<div className="flex-1 overflow-auto p-4 space-y-4">
{/* Search Bar */}
{selectedTab === 'errors' && (
<div className="relative">
<input
type="text"
placeholder="Search errors..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full bg-iron-gray border border-charcoal-outline rounded px-3 py-2 pl-8 text-xs text-white placeholder-gray-500 focus:outline-none focus:border-red-400"
/>
<Search className="w-3 h-3 text-gray-500 absolute left-2.5 top-2.5" />
</div>
)}
{/* Errors Tab */}
{selectedTab === 'errors' && stats && (
<div className="space-y-4">
{/* Error Summary */}
<div className="grid grid-cols-2 gap-2">
<div className="bg-iron-gray border border-charcoal-outline rounded p-3">
<div className="text-xs text-gray-500">Total Errors</div>
<div className="text-xl font-bold text-red-400">{stats.totalErrors}</div>
</div>
<div className="bg-iron-gray border border-charcoal-outline rounded p-3">
<div className="text-xs text-gray-500">Error Types</div>
<div className="text-xl font-bold text-yellow-400">
{Object.keys(stats.errorsByType).length}
</div>
</div>
</div>
{/* Error Types Breakdown */}
{Object.keys(stats.errorsByType).length > 0 && (
<div className="bg-iron-gray border border-charcoal-outline rounded p-3">
<div className="text-xs font-semibold text-gray-400 mb-2 flex items-center gap-2">
<Bug className="w-3 h-3" /> Error Types
</div>
<div className="space-y-1 max-h-32 overflow-auto">
{Object.entries(stats.errorsByType).map(([type, count]) => (
<div key={type} className="flex justify-between text-xs">
<span className="text-gray-300">{type}</span>
<span className="text-red-400 font-mono">{count}</span>
</div>
))}
</div>
</div>
)}
{/* Recent Errors */}
{filteredRecentErrors.length > 0 && (
<div className="bg-iron-gray border border-charcoal-outline rounded p-3">
<div className="text-xs font-semibold text-gray-400 mb-2 flex items-center gap-2">
<AlertTriangle className="w-3 h-3" /> Recent Errors
</div>
<div className="space-y-2 max-h-64 overflow-auto">
{filteredRecentErrors.map((error, idx) => (
<div key={idx} className="bg-deep-graphite border border-charcoal-outline rounded p-2 text-xs">
<div className="flex justify-between items-start gap-2 mb-1">
<span className="font-mono text-red-400 font-bold">{error.type}</span>
<span className="text-gray-500 text-[10px]">
{new Date(error.timestamp).toLocaleTimeString()}
</span>
</div>
<div className="text-gray-300 break-words mb-1">{error.message}</div>
<button
onClick={() => copyToClipboard(error)}
className="text-[10px] text-gray-500 hover:text-gray-300 flex items-center gap-1"
>
<Copy className="w-3 h-3" /> Copy Details
</button>
</div>
))}
</div>
</div>
)}
{/* Error Timeline */}
{stats.errorsByTime.length > 0 && (
<div className="bg-iron-gray border border-charcoal-outline rounded p-3">
<div className="text-xs font-semibold text-gray-400 mb-2 flex items-center gap-2">
<Clock className="w-3 h-3" /> Last 10 Minutes
</div>
<div className="space-y-1 max-h-32 overflow-auto">
{stats.errorsByTime.map((point, idx) => (
<div key={idx} className="flex justify-between text-xs">
<span className="text-gray-500">{point.time}</span>
<span className="text-red-400 font-mono">{point.count} errors</span>
</div>
))}
</div>
</div>
)}
</div>
)}
{/* API Tab */}
{selectedTab === 'api' && stats && (
<div className="space-y-4">
{/* API Summary */}
<div className="grid grid-cols-2 gap-2">
<div className="bg-iron-gray border border-charcoal-outline rounded p-3">
<div className="text-xs text-gray-500">Total Requests</div>
<div className="text-xl font-bold text-blue-400">{stats.apiStats.totalRequests}</div>
</div>
<div className="bg-iron-gray border border-charcoal-outline rounded p-3">
<div className="text-xs text-gray-500">Success Rate</div>
<div className="text-xl font-bold text-green-400">
{stats.apiStats.totalRequests > 0
? ((stats.apiStats.successful / stats.apiStats.totalRequests) * 100).toFixed(1)
: 0}%
</div>
</div>
</div>
{/* API Stats */}
<div className="bg-iron-gray border border-charcoal-outline rounded p-3">
<div className="text-xs font-semibold text-gray-400 mb-2 flex items-center gap-2">
<Globe className="w-3 h-3" /> API Metrics
</div>
<div className="space-y-1 text-xs">
<div className="flex justify-between">
<span className="text-gray-500">Successful</span>
<span className="text-green-400 font-mono">{stats.apiStats.successful}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500">Failed</span>
<span className="text-red-400 font-mono">{stats.apiStats.failed}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500">Avg Duration</span>
<span className="text-yellow-400 font-mono">{stats.apiStats.averageDuration.toFixed(2)}ms</span>
</div>
</div>
</div>
{/* Slowest Requests */}
{stats.apiStats.slowestRequests.length > 0 && (
<div className="bg-iron-gray border border-charcoal-outline rounded p-3">
<div className="text-xs font-semibold text-gray-400 mb-2 flex items-center gap-2">
<Zap className="w-3 h-3" /> Slowest Requests
</div>
<div className="space-y-1 max-h-40 overflow-auto">
{stats.apiStats.slowestRequests.map((req, idx) => (
<div key={idx} className="flex justify-between text-xs bg-deep-graphite p-1.5 rounded border border-charcoal-outline">
<span className="text-gray-300 truncate flex-1">{req.url}</span>
<span className="text-red-400 font-mono ml-2">{req.duration.toFixed(2)}ms</span>
</div>
))}
</div>
</div>
)}
</div>
)}
{/* Environment Tab */}
{selectedTab === 'environment' && stats && (
<div className="space-y-4">
{/* Environment Info */}
<div className="bg-iron-gray border border-charcoal-outline rounded p-3">
<div className="text-xs font-semibold text-gray-400 mb-2 flex items-center gap-2">
<Cpu className="w-3 h-3" /> Environment
</div>
<div className="space-y-1 text-xs">
<div className="flex justify-between">
<span className="text-gray-500">Node Environment</span>
<span className={`font-mono font-bold ${
stats.environment.mode === 'development' ? 'text-green-400' : 'text-yellow-400'
}`}>{stats.environment.mode}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500">App Mode</span>
<span className="text-blue-400 font-mono">{stats.environment.appMode}</span>
</div>
{stats.environment.version && (
<div className="flex justify-between">
<span className="text-gray-500">Version</span>
<span className="text-gray-300 font-mono">{stats.environment.version}</span>
</div>
)}
{stats.environment.buildTime && (
<div className="flex justify-between">
<span className="text-gray-500">Build Time</span>
<span className="text-gray-500 font-mono text-[10px]">{stats.environment.buildTime}</span>
</div>
)}
</div>
</div>
{/* Browser Info */}
<div className="bg-iron-gray border border-charcoal-outline rounded p-3">
<div className="text-xs font-semibold text-gray-400 mb-2 flex items-center gap-2">
<Globe className="w-3 h-3" /> Browser
</div>
<div className="space-y-1 text-xs">
<div className="flex justify-between">
<span className="text-gray-500">User Agent</span>
<span className="text-gray-300 text-[9px] truncate max-w-[150px]">{navigator.userAgent}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500">Language</span>
<span className="text-gray-300 font-mono">{navigator.language}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500">Platform</span>
<span className="text-gray-300 font-mono">{navigator.platform}</span>
</div>
</div>
</div>
{/* Performance */}
<div className="bg-iron-gray border border-charcoal-outline rounded p-3">
<div className="text-xs font-semibold text-gray-400 mb-2 flex items-center gap-2">
<Activity className="w-3 h-3" /> Performance
</div>
<div className="space-y-1 text-xs">
<div className="flex justify-between">
<span className="text-gray-500">Viewport</span>
<span className="text-gray-300 font-mono">{window.innerWidth}x{window.innerHeight}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500">Screen</span>
<span className="text-gray-300 font-mono">{window.screen.width}x{window.screen.height}</span>
</div>
{(performance as any).memory && (
<div className="flex justify-between">
<span className="text-gray-500">JS Heap</span>
<span className="text-gray-300 font-mono">
{((performance as any).memory.usedJSHeapSize / 1024 / 1024).toFixed(1)}MB
</span>
</div>
)}
</div>
</div>
{/* Connection */}
{(navigator as any).connection && (
<div className="bg-iron-gray border border-charcoal-outline rounded p-3">
<div className="text-xs font-semibold text-gray-400 mb-2 flex items-center gap-2">
<Zap className="w-3 h-3" /> Network
</div>
<div className="space-y-1 text-xs">
<div className="flex justify-between">
<span className="text-gray-500">Type</span>
<span className="text-gray-300 font-mono">{(navigator as any).connection.effectiveType}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500">Downlink</span>
<span className="text-gray-300 font-mono">{(navigator as any).connection.downlink}Mbps</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500">RTT</span>
<span className="text-gray-300 font-mono">{(navigator as any).connection.rtt}ms</span>
</div>
</div>
</div>
)}
</div>
)}
{/* Raw Data Tab */}
{selectedTab === 'raw' && stats && (
<div className="space-y-3">
<div className="bg-iron-gray border border-charcoal-outline rounded p-3">
<div className="text-xs font-semibold text-gray-400 mb-2 flex items-center gap-2">
<FileText className="w-3 h-3" /> Export Options
</div>
<div className="flex gap-2">
<button
onClick={exportAllData}
className="flex-1 flex items-center justify-center gap-1 px-3 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded text-xs"
>
<Download className="w-3 h-3" /> Export JSON
</button>
<button
onClick={() => copyToClipboard(stats)}
className="flex-1 flex items-center justify-center gap-1 px-3 py-2 bg-iron-gray hover:bg-charcoal-outline text-gray-300 rounded text-xs border border-charcoal-outline"
>
<Copy className="w-3 h-3" /> Copy Stats
</button>
</div>
</div>
<div className="bg-iron-gray border border-charcoal-outline rounded p-3">
<div className="text-xs font-semibold text-gray-400 mb-2 flex items-center gap-2">
<Trash2 className="w-3 h-3" /> Maintenance
</div>
<button
onClick={clearAllData}
className="w-full flex items-center justify-center gap-1 px-3 py-2 bg-red-600 hover:bg-red-700 text-white rounded text-xs"
>
<Trash2 className="w-3 h-3" /> Clear All Logs
</button>
</div>
<div className="bg-iron-gray border border-charcoal-outline rounded p-3">
<div className="text-xs font-semibold text-gray-400 mb-2 flex items-center gap-2">
<Terminal className="w-3 h-3" /> Console Commands
</div>
<div className="space-y-1 text-[10px] font-mono text-gray-400">
<div> window.__GRIDPILOT_GLOBAL_HANDLER__</div>
<div> window.__GRIDPILOT_API_LOGGER__</div>
<div> window.__GRIDPILOT_REACT_ERRORS__</div>
</div>
</div>
</div>
)}
</div>
{/* Footer */}
<div className="px-4 py-2 bg-iron-gray/30 border-t border-charcoal-outline text-[10px] text-gray-500 flex justify-between items-center">
<span>Auto-refresh: {refreshInterval}ms</span>
{copied && <span className="text-green-400">Copied!</span>}
</div>
</div>
);
}
/**
* Hook for accessing error analytics programmatically
*/
export function useErrorAnalytics() {
const globalHandler = getGlobalErrorHandler();
const apiLogger = getGlobalApiLogger();
return {
getStats: () => {
const errorStats = globalHandler.getStats();
const apiStats = apiLogger.getStats();
return {
errors: errorStats,
api: apiStats,
};
},
getRecentErrors: (limit = 10) => {
return globalHandler.getErrorHistory().slice(-limit);
},
getSlowestApiRequests: (limit = 5) => {
return apiLogger.getSlowestRequests(limit);
},
exportData: () => {
return {
timestamp: new Date().toISOString(),
errors: globalHandler.getErrorHistory(),
apiRequests: apiLogger.getHistory(),
};
},
clearAll: () => {
globalHandler.clearHistory();
apiLogger.clearHistory();
},
};
}