Files
gridpilot.gg/apps/website/components/errors/ErrorAnalyticsDashboard.tsx
2026-01-07 22:05:53 +01:00

611 lines
24 KiB
TypeScript

'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;
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',
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>
{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();
},
};
}