635 lines
26 KiB
TypeScript
635 lines
26 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState, useEffect } from 'react';
|
|
import { getGlobalErrorHandler } from '@/lib/infrastructure/GlobalErrorHandler';
|
|
import { getGlobalApiLogger } from '@/lib/infrastructure/ApiRequestLogger';
|
|
import { getErrorAnalyticsStats, type ErrorStats } from '@/lib/services/error/ErrorAnalyticsService';
|
|
import {
|
|
Activity,
|
|
AlertTriangle,
|
|
Clock,
|
|
Copy,
|
|
RefreshCw,
|
|
Bug,
|
|
Globe,
|
|
Cpu,
|
|
FileText,
|
|
Trash2,
|
|
Download,
|
|
Search,
|
|
ChevronDown,
|
|
Zap,
|
|
Terminal
|
|
} from 'lucide-react';
|
|
import { Box } from '@/ui/Box';
|
|
import { Text } from '@/ui/Text';
|
|
import { Stack } from '@/ui/Stack';
|
|
import { Icon } from '@/ui/Icon';
|
|
import { IconButton } from '@/ui/IconButton';
|
|
import { Badge } from '@/ui/Badge';
|
|
import { Button } from '@/ui/Button';
|
|
import { Input } from '@/ui/Input';
|
|
|
|
interface ErrorAnalyticsDashboardProps {
|
|
/**
|
|
* Auto-refresh interval in milliseconds
|
|
*/
|
|
refreshInterval?: number;
|
|
/**
|
|
* Whether to show in production (default: false)
|
|
*/
|
|
showInProduction?: boolean;
|
|
}
|
|
|
|
function formatDuration(duration: number): string {
|
|
return duration.toFixed(2) + 'ms';
|
|
}
|
|
|
|
function formatPercentage(value: number, total: number): string {
|
|
if (total === 0) return '0%';
|
|
return ((value / total) * 100).toFixed(1) + '%';
|
|
}
|
|
|
|
function formatMemory(bytes: number): string {
|
|
return (bytes / 1024 / 1024).toFixed(1) + 'MB';
|
|
}
|
|
|
|
interface PerformanceWithMemory extends Performance {
|
|
memory?: {
|
|
usedJSHeapSize: number;
|
|
totalJSHeapSize: number;
|
|
jsHeapSizeLimit: number;
|
|
};
|
|
}
|
|
|
|
interface NavigatorWithConnection extends Navigator {
|
|
connection?: {
|
|
effectiveType: string;
|
|
downlink: number;
|
|
rtt: number;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 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';
|
|
const shouldShow = isDev || showInProduction;
|
|
|
|
const perf = typeof performance !== 'undefined' ? performance as PerformanceWithMemory : null;
|
|
const nav = typeof navigator !== 'undefined' ? navigator as NavigatorWithConnection : null;
|
|
|
|
useEffect(() => {
|
|
if (!shouldShow) return;
|
|
|
|
const update = () => {
|
|
setStats(getErrorAnalyticsStats());
|
|
};
|
|
|
|
update();
|
|
|
|
if (refreshInterval > 0) {
|
|
const interval = setInterval(update, refreshInterval);
|
|
return () => clearInterval(interval);
|
|
}
|
|
}, [refreshInterval, shouldShow]);
|
|
|
|
if (!shouldShow) {
|
|
return null;
|
|
}
|
|
|
|
const updateStatsManual = () => {
|
|
setStats(getErrorAnalyticsStats());
|
|
};
|
|
|
|
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();
|
|
updateStatsManual();
|
|
}
|
|
};
|
|
|
|
const filteredRecentErrors = stats?.recentErrors.filter(error =>
|
|
searchTerm === '' ||
|
|
error.message.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
error.type.toLowerCase().includes(searchTerm.toLowerCase())
|
|
) || [];
|
|
|
|
if (!isExpanded) {
|
|
return (
|
|
<Box position="fixed" bottom="4" left="4" zIndex={50}>
|
|
<IconButton
|
|
icon={Activity}
|
|
onClick={() => setIsExpanded(true)}
|
|
variant="secondary"
|
|
title="Open Error Analytics"
|
|
size="lg"
|
|
color="rgb(239, 68, 68)"
|
|
/>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Box
|
|
position="fixed"
|
|
bottom="4"
|
|
left="4"
|
|
zIndex={50}
|
|
w="96"
|
|
bg="bg-deep-graphite"
|
|
border
|
|
borderColor="border-charcoal-outline"
|
|
rounded="xl"
|
|
shadow="2xl"
|
|
overflow="hidden"
|
|
display="flex"
|
|
flexDirection="col"
|
|
maxHeight="80vh"
|
|
>
|
|
{/* Header */}
|
|
<Box display="flex" alignItems="center" justifyContent="between" px={4} py={3} bg="bg-iron-gray/50" borderBottom borderColor="border-charcoal-outline">
|
|
<Box display="flex" alignItems="center" gap={2}>
|
|
<Icon icon={Activity} size={4} color="rgb(239, 68, 68)" />
|
|
<Text size="sm" weight="semibold" color="text-white">Error Analytics</Text>
|
|
{isDev && (
|
|
<Badge variant="danger" size="xs">
|
|
DEV
|
|
</Badge>
|
|
)}
|
|
</Box>
|
|
<Box display="flex" alignItems="center" gap={1}>
|
|
<IconButton
|
|
icon={RefreshCw}
|
|
onClick={updateStatsManual}
|
|
variant="ghost"
|
|
size="sm"
|
|
title="Refresh"
|
|
/>
|
|
<IconButton
|
|
icon={ChevronDown}
|
|
onClick={() => setIsExpanded(false)}
|
|
variant="ghost"
|
|
size="sm"
|
|
title="Minimize"
|
|
/>
|
|
</Box>
|
|
</Box>
|
|
|
|
{/* Tabs */}
|
|
<Box display="flex" borderBottom borderColor="border-charcoal-outline" bg="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 => (
|
|
<Box
|
|
key={tab.id}
|
|
as="button"
|
|
type="button"
|
|
onClick={() => setSelectedTab(tab.id as 'errors' | 'api' | 'environment' | 'raw')}
|
|
display="flex"
|
|
flexGrow={1}
|
|
alignItems="center"
|
|
justifyContent="center"
|
|
gap={1}
|
|
px={2}
|
|
py={2}
|
|
cursor="pointer"
|
|
transition
|
|
bg={selectedTab === tab.id ? 'bg-deep-graphite' : ''}
|
|
borderBottom={selectedTab === tab.id}
|
|
borderColor={selectedTab === tab.id ? 'border-red-400' : ''}
|
|
borderWidth={selectedTab === tab.id ? '2px' : '0'}
|
|
>
|
|
<Icon icon={tab.icon} size={3} color={selectedTab === tab.id ? 'text-white' : 'text-gray-400'} />
|
|
<Text
|
|
size="xs"
|
|
weight="medium"
|
|
color={selectedTab === tab.id ? 'text-white' : 'text-gray-400'}
|
|
>
|
|
{tab.label}
|
|
</Text>
|
|
</Box>
|
|
))}
|
|
</Box>
|
|
|
|
{/* Content */}
|
|
<Box flexGrow={1} overflow="auto" p={4}>
|
|
<Stack gap={4}>
|
|
{/* Search Bar */}
|
|
{selectedTab === 'errors' && (
|
|
<Input
|
|
type="text"
|
|
placeholder="Search errors..."
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
icon={<Icon icon={Search} size={3} color="rgb(107, 114, 128)" />}
|
|
/>
|
|
)}
|
|
|
|
{/* Errors Tab */}
|
|
{selectedTab === 'errors' && stats && (
|
|
<Stack gap={4}>
|
|
{/* Error Summary */}
|
|
<Box display="grid" gridCols={2} gap={2}>
|
|
<Box bg="bg-iron-gray" border borderColor="border-charcoal-outline" rounded="md" p={3}>
|
|
<Text size="xs" color="text-gray-500" block>Total Errors</Text>
|
|
<Text size="xl" weight="bold" color="text-red-400">{stats.totalErrors}</Text>
|
|
</Box>
|
|
<Box bg="bg-iron-gray" border borderColor="border-charcoal-outline" rounded="md" p={3}>
|
|
<Text size="xs" color="text-gray-500" block>Error Types</Text>
|
|
<Text size="xl" weight="bold" color="text-warning-amber">
|
|
{Object.keys(stats.errorsByType).length}
|
|
</Text>
|
|
</Box>
|
|
</Box>
|
|
|
|
{/* Error Types Breakdown */}
|
|
{Object.keys(stats.errorsByType).length > 0 && (
|
|
<Box bg="bg-iron-gray" border borderColor="border-charcoal-outline" rounded="md" p={3}>
|
|
<Box display="flex" alignItems="center" gap={2} mb={2}>
|
|
<Icon icon={Bug} size={3} color="rgb(156, 163, 175)" />
|
|
<Text size="xs" weight="semibold" color="text-gray-400">Error Types</Text>
|
|
</Box>
|
|
<Stack gap={1} maxHeight="8rem" overflow="auto">
|
|
{Object.entries(stats.errorsByType).map(([type, count]) => (
|
|
<Box key={type} display="flex" justifyContent="between">
|
|
<Text size="xs" color="text-gray-300">{type}</Text>
|
|
<Text size="xs" color="text-red-400" font="mono">{count}</Text>
|
|
</Box>
|
|
))}
|
|
</Stack>
|
|
</Box>
|
|
)}
|
|
|
|
{/* Recent Errors */}
|
|
{filteredRecentErrors.length > 0 && (
|
|
<Box bg="bg-iron-gray" border borderColor="border-charcoal-outline" rounded="md" p={3}>
|
|
<Box display="flex" alignItems="center" gap={2} mb={2}>
|
|
<Icon icon={AlertTriangle} size={3} color="rgb(156, 163, 175)" />
|
|
<Text size="xs" weight="semibold" color="text-gray-400">Recent Errors</Text>
|
|
</Box>
|
|
<Stack gap={2} maxHeight="16rem" overflow="auto">
|
|
{filteredRecentErrors.map((error, idx) => (
|
|
<Box key={idx} bg="bg-deep-graphite" border borderColor="border-charcoal-outline" rounded="md" p={2}>
|
|
<Box display="flex" justifyContent="between" alignItems="start" gap={2} mb={1}>
|
|
<Text size="xs" font="mono" weight="bold" color="text-red-400" truncate>{error.type}</Text>
|
|
<Text size="xs" color="text-gray-500" fontSize="10px">
|
|
{new Date(error.timestamp).toLocaleTimeString()}
|
|
</Text>
|
|
</Box>
|
|
<Text size="xs" color="text-gray-300" block mb={1}>{error.message}</Text>
|
|
<Button
|
|
variant="ghost"
|
|
onClick={() => copyToClipboard(error)}
|
|
size="sm"
|
|
p={0}
|
|
minHeight="0"
|
|
icon={<Icon icon={Copy} size={3} />}
|
|
>
|
|
<Text size="xs" color="text-gray-500" fontSize="10px">Copy Details</Text>
|
|
</Button>
|
|
</Box>
|
|
))}
|
|
</Stack>
|
|
</Box>
|
|
)}
|
|
|
|
{/* Error Timeline */}
|
|
{stats.errorsByTime.length > 0 && (
|
|
<Box bg="bg-iron-gray" border borderColor="border-charcoal-outline" rounded="md" p={3}>
|
|
<Box display="flex" alignItems="center" gap={2} mb={2}>
|
|
<Icon icon={Clock} size={3} color="rgb(156, 163, 175)" />
|
|
<Text size="xs" weight="semibold" color="text-gray-400">Last 10 Minutes</Text>
|
|
</Box>
|
|
<Stack gap={1} maxHeight="8rem" overflow="auto">
|
|
{stats.errorsByTime.map((point, idx) => (
|
|
<Box key={idx} display="flex" justifyContent="between">
|
|
<Text size="xs" color="text-gray-500">{point.time}</Text>
|
|
<Text size="xs" color="text-red-400" font="mono">{point.count} errors</Text>
|
|
</Box>
|
|
))}
|
|
</Stack>
|
|
</Box>
|
|
)}
|
|
</Stack>
|
|
)}
|
|
|
|
{/* API Tab */}
|
|
{selectedTab === 'api' && stats && (
|
|
<Stack gap={4}>
|
|
{/* API Summary */}
|
|
<Box display="grid" gridCols={2} gap={2}>
|
|
<Box bg="bg-iron-gray" border borderColor="border-charcoal-outline" rounded="md" p={3}>
|
|
<Text size="xs" color="text-gray-500" block>Total Requests</Text>
|
|
<Text size="xl" weight="bold" color="text-primary-blue">{stats.apiStats.totalRequests}</Text>
|
|
</Box>
|
|
<Box bg="bg-iron-gray" border borderColor="border-charcoal-outline" rounded="md" p={3}>
|
|
<Text size="xs" color="text-gray-500" block>Success Rate</Text>
|
|
<Text size="xl" weight="bold" color="text-performance-green">
|
|
{formatPercentage(stats.apiStats.successful, stats.apiStats.totalRequests)}
|
|
</Text>
|
|
</Box>
|
|
</Box>
|
|
|
|
{/* API Stats */}
|
|
<Box bg="bg-iron-gray" border borderColor="border-charcoal-outline" rounded="md" p={3}>
|
|
<Box display="flex" alignItems="center" gap={2} mb={2}>
|
|
<Icon icon={Globe} size={3} color="rgb(156, 163, 175)" />
|
|
<Text size="xs" weight="semibold" color="text-gray-400">API Metrics</Text>
|
|
</Box>
|
|
<Stack gap={1}>
|
|
<Box display="flex" justifyContent="between">
|
|
<Text size="xs" color="text-gray-500">Successful</Text>
|
|
<Text size="xs" color="text-performance-green" font="mono">{stats.apiStats.successful}</Text>
|
|
</Box>
|
|
<Box display="flex" justifyContent="between">
|
|
<Text size="xs" color="text-gray-500">Failed</Text>
|
|
<Text size="xs" color="text-red-400" font="mono">{stats.apiStats.failed}</Text>
|
|
</Box>
|
|
<Box display="flex" justifyContent="between">
|
|
<Text size="xs" color="text-gray-500">Avg Duration</Text>
|
|
<Text size="xs" color="text-warning-amber" font="mono">{formatDuration(stats.apiStats.averageDuration)}</Text>
|
|
</Box>
|
|
</Stack>
|
|
</Box>
|
|
|
|
{/* Slowest Requests */}
|
|
{stats.apiStats.slowestRequests.length > 0 && (
|
|
<Box bg="bg-iron-gray" border borderColor="border-charcoal-outline" rounded="md" p={3}>
|
|
<Box display="flex" alignItems="center" gap={2} mb={2}>
|
|
<Icon icon={Zap} size={3} color="rgb(156, 163, 175)" />
|
|
<Text size="xs" weight="semibold" color="text-gray-400">Slowest Requests</Text>
|
|
</Box>
|
|
<Stack gap={1} maxHeight="10rem" overflow="auto">
|
|
{stats.apiStats.slowestRequests.map((req, idx) => (
|
|
<Box key={idx} display="flex" justifyContent="between" bg="bg-deep-graphite" p={1.5} rounded="sm" border borderColor="border-charcoal-outline">
|
|
<Text size="xs" color="text-gray-300" truncate flexGrow={1}>{req.url}</Text>
|
|
<Text size="xs" color="text-red-400" font="mono" ml={2}>{formatDuration(req.duration)}</Text>
|
|
</Box>
|
|
))}
|
|
</Stack>
|
|
</Box>
|
|
)}
|
|
</Stack>
|
|
)}
|
|
|
|
{/* Environment Tab */}
|
|
{selectedTab === 'environment' && stats && (
|
|
<Stack gap={4}>
|
|
{/* Environment Info */}
|
|
<Box bg="bg-iron-gray" border borderColor="border-charcoal-outline" rounded="md" p={3}>
|
|
<Box display="flex" alignItems="center" gap={2} mb={2}>
|
|
<Icon icon={Cpu} size={3} color="rgb(156, 163, 175)" />
|
|
<Text size="xs" weight="semibold" color="text-gray-400">Environment</Text>
|
|
</Box>
|
|
<Stack gap={1}>
|
|
<Box display="flex" justifyContent="between">
|
|
<Text size="xs" color="text-gray-500">Node Environment</Text>
|
|
<Text size="xs" font="mono" weight="bold" color={stats.environment.mode === 'development' ? 'text-performance-green' : 'text-warning-amber'}>
|
|
{stats.environment.mode}
|
|
</Text>
|
|
</Box>
|
|
{stats.environment.version && (
|
|
<Box display="flex" justifyContent="between">
|
|
<Text size="xs" color="text-gray-500">Version</Text>
|
|
<Text size="xs" color="text-gray-300" font="mono">{stats.environment.version}</Text>
|
|
</Box>
|
|
)}
|
|
{stats.environment.buildTime && (
|
|
<Box display="flex" justifyContent="between">
|
|
<Text size="xs" color="text-gray-500">Build Time</Text>
|
|
<Text size="xs" color="text-gray-500" font="mono" fontSize="10px">{stats.environment.buildTime}</Text>
|
|
</Box>
|
|
)}
|
|
</Stack>
|
|
</Box>
|
|
|
|
{/* Browser Info */}
|
|
<Box bg="bg-iron-gray" border borderColor="border-charcoal-outline" rounded="md" p={3}>
|
|
<Box display="flex" alignItems="center" gap={2} mb={2}>
|
|
<Icon icon={Globe} size={3} color="rgb(156, 163, 175)" />
|
|
<Text size="xs" weight="semibold" color="text-gray-400">Browser</Text>
|
|
</Box>
|
|
<Stack gap={1}>
|
|
<Box display="flex" justifyContent="between">
|
|
<Text size="xs" color="text-gray-500">User Agent</Text>
|
|
<Text size="xs" color="text-gray-300" truncate maxWidth="150px">{navigator.userAgent}</Text>
|
|
</Box>
|
|
<Box display="flex" justifyContent="between">
|
|
<Text size="xs" color="text-gray-500">Language</Text>
|
|
<Text size="xs" color="text-gray-300" font="mono">{navigator.language}</Text>
|
|
</Box>
|
|
<Box display="flex" justifyContent="between">
|
|
<Text size="xs" color="text-gray-500">Platform</Text>
|
|
<Text size="xs" color="text-gray-300" font="mono">{navigator.platform}</Text>
|
|
</Box>
|
|
</Stack>
|
|
</Box>
|
|
|
|
{/* Performance */}
|
|
<Box bg="bg-iron-gray" border borderColor="border-charcoal-outline" rounded="md" p={3}>
|
|
<Box display="flex" alignItems="center" gap={2} mb={2}>
|
|
<Icon icon={Activity} size={3} color="rgb(156, 163, 175)" />
|
|
<Text size="xs" weight="semibold" color="text-gray-400">Performance</Text>
|
|
</Box>
|
|
<Stack gap={1}>
|
|
<Box display="flex" justifyContent="between">
|
|
<Text size="xs" color="text-gray-500">Viewport</Text>
|
|
<Text size="xs" color="text-gray-300" font="mono">{window.innerWidth}x{window.innerHeight}</Text>
|
|
</Box>
|
|
<Box display="flex" justifyContent="between">
|
|
<Text size="xs" color="text-gray-500">Screen</Text>
|
|
<Text size="xs" color="text-gray-300" font="mono">{window.screen.width}x{window.screen.height}</Text>
|
|
</Box>
|
|
{perf?.memory && (
|
|
<Box display="flex" justifyContent="between">
|
|
<Text size="xs" color="text-gray-500">JS Heap</Text>
|
|
<Text size="xs" color="text-gray-300" font="mono">
|
|
{formatMemory(perf.memory.usedJSHeapSize)}
|
|
</Text>
|
|
</Box>
|
|
)}
|
|
</Stack>
|
|
</Box>
|
|
|
|
{/* Connection */}
|
|
{nav?.connection && (
|
|
<Box bg="bg-iron-gray" border borderColor="border-charcoal-outline" rounded="md" p={3}>
|
|
<Box display="flex" alignItems="center" gap={2} mb={2}>
|
|
<Icon icon={Zap} size={3} color="rgb(156, 163, 175)" />
|
|
<Text size="xs" weight="semibold" color="text-gray-400">Network</Text>
|
|
</Box>
|
|
<Stack gap={1}>
|
|
<Box display="flex" justifyContent="between">
|
|
<Text size="xs" color="text-gray-500">Type</Text>
|
|
<Text size="xs" color="text-gray-300" font="mono">{nav.connection.effectiveType}</Text>
|
|
</Box>
|
|
<Box display="flex" justifyContent="between">
|
|
<Text size="xs" color="text-gray-500">Downlink</Text>
|
|
<Text size="xs" color="text-gray-300" font="mono">{nav.connection.downlink}Mbps</Text>
|
|
</Box>
|
|
<Box display="flex" justifyContent="between">
|
|
<Text size="xs" color="text-gray-500">RTT</Text>
|
|
<Text size="xs" color="text-gray-300" font="mono">{nav.connection.rtt}ms</Text>
|
|
</Box>
|
|
</Stack>
|
|
</Box>
|
|
)}
|
|
</Stack>
|
|
)}
|
|
|
|
{/* Raw Data Tab */}
|
|
{selectedTab === 'raw' && stats && (
|
|
<Stack gap={3}>
|
|
<Box bg="bg-iron-gray" border borderColor="border-charcoal-outline" rounded="md" p={3}>
|
|
<Box display="flex" alignItems="center" gap={2} mb={2}>
|
|
<Icon icon={FileText} size={3} color="rgb(156, 163, 175)" />
|
|
<Text size="xs" weight="semibold" color="text-gray-400">Export Options</Text>
|
|
</Box>
|
|
<Box display="flex" gap={2}>
|
|
<Button
|
|
variant="primary"
|
|
onClick={exportAllData}
|
|
fullWidth
|
|
size="sm"
|
|
icon={<Icon icon={Download} size={3} />}
|
|
>
|
|
Export JSON
|
|
</Button>
|
|
<Button
|
|
variant="secondary"
|
|
onClick={() => copyToClipboard(stats)}
|
|
fullWidth
|
|
size="sm"
|
|
icon={<Icon icon={Copy} size={3} />}
|
|
>
|
|
Copy Stats
|
|
</Button>
|
|
</Box>
|
|
</Box>
|
|
|
|
<Box bg="bg-iron-gray" border borderColor="border-charcoal-outline" rounded="md" p={3}>
|
|
<Box display="flex" alignItems="center" gap={2} mb={2}>
|
|
<Icon icon={Trash2} size={3} color="rgb(156, 163, 175)" />
|
|
<Text size="xs" weight="semibold" color="text-gray-400">Maintenance</Text>
|
|
</Box>
|
|
<Button
|
|
variant="danger"
|
|
onClick={clearAllData}
|
|
fullWidth
|
|
size="sm"
|
|
icon={<Icon icon={Trash2} size={3} />}
|
|
>
|
|
Clear All Logs
|
|
</Button>
|
|
</Box>
|
|
|
|
<Box bg="bg-iron-gray" border borderColor="border-charcoal-outline" rounded="md" p={3}>
|
|
<Box display="flex" alignItems="center" gap={2} mb={2}>
|
|
<Icon icon={Terminal} size={3} color="rgb(156, 163, 175)" />
|
|
<Text size="xs" weight="semibold" color="text-gray-400">Console Commands</Text>
|
|
</Box>
|
|
<Stack gap={1} fontSize="10px">
|
|
<Text color="text-gray-400" font="mono">• window.__GRIDPILOT_GLOBAL_HANDLER__</Text>
|
|
<Text color="text-gray-400" font="mono">• window.__GRIDPILOT_API_LOGGER__</Text>
|
|
<Text color="text-gray-400" font="mono">• window.__GRIDPILOT_REACT_ERRORS__</Text>
|
|
</Stack>
|
|
</Box>
|
|
</Stack>
|
|
)}
|
|
</Stack>
|
|
</Box>
|
|
|
|
{/* Footer */}
|
|
<Box px={4} py={2} bg="bg-iron-gray/30" borderTop borderColor="border-charcoal-outline" display="flex" justifyContent="between" alignItems="center">
|
|
<Text size="xs" color="text-gray-500" fontSize="10px">Auto-refresh: {refreshInterval}ms</Text>
|
|
{copied && <Text size="xs" color="text-performance-green" fontSize="10px">Copied!</Text>}
|
|
</Box>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 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();
|
|
},
|
|
};
|
|
}
|