dev experience
This commit is contained in:
365
apps/website/components/dev/DebugModeToggle.tsx
Normal file
365
apps/website/components/dev/DebugModeToggle.tsx
Normal 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,
|
||||
};
|
||||
}
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
160
apps/website/components/dev/sections/ReplaySection.tsx
Normal file
160
apps/website/components/dev/sections/ReplaySection.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
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>
|
||||
);
|
||||
}
|
||||
617
apps/website/components/errors/ErrorAnalyticsDashboard.tsx
Normal file
617
apps/website/components/errors/ErrorAnalyticsDashboard.tsx
Normal 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();
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user