Files
thrilltrack-explorer/src/components/admin/ErrorDetailsModal.tsx
2025-11-03 21:36:08 +00:00

231 lines
8.4 KiB
TypeScript

import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Copy, ExternalLink } from 'lucide-react';
import { format } from 'date-fns';
import { toast } from 'sonner';
interface Breadcrumb {
timestamp: string;
category: string;
message: string;
level?: string;
sequence_order?: number;
}
interface ErrorDetails {
request_id: string;
created_at: string;
error_type: string;
error_message: string;
error_stack?: string;
endpoint: string;
method: string;
status_code: number;
duration_ms: number;
user_id?: string;
request_breadcrumbs?: Breadcrumb[];
user_agent?: string;
client_version?: string;
timezone?: string;
referrer?: string;
ip_address_hash?: string;
}
interface ErrorDetailsModalProps {
error: ErrorDetails;
onClose: () => void;
}
export function ErrorDetailsModal({ error, onClose }: ErrorDetailsModalProps) {
const copyErrorId = () => {
navigator.clipboard.writeText(error.request_id);
toast.success('Error ID copied to clipboard');
};
const copyErrorReport = () => {
const report = `
Error Report
============
Request ID: ${error.request_id}
Timestamp: ${format(new Date(error.created_at), 'PPpp')}
Type: ${error.error_type}
Endpoint: ${error.endpoint}
Method: ${error.method}
Status: ${error.status_code}
Duration: ${error.duration_ms}ms
Error Message:
${error.error_message}
${error.error_stack ? `Stack Trace:\n${error.error_stack}` : ''}
`.trim();
navigator.clipboard.writeText(report);
toast.success('Error report copied to clipboard');
};
return (
<Dialog open onOpenChange={onClose}>
<DialogContent className="max-w-4xl max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
Error Details
<Badge variant="destructive">{error.error_type}</Badge>
</DialogTitle>
</DialogHeader>
<Tabs defaultValue="overview" className="w-full">
<TabsList>
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="stack">Stack Trace</TabsTrigger>
<TabsTrigger value="breadcrumbs">Breadcrumbs</TabsTrigger>
<TabsTrigger value="environment">Environment</TabsTrigger>
</TabsList>
<TabsContent value="overview" className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium">Request ID</label>
<div className="flex items-center gap-2">
<code className="text-sm bg-muted px-2 py-1 rounded">
{error.request_id}
</code>
<Button size="sm" variant="ghost" onClick={copyErrorId}>
<Copy className="w-4 h-4" />
</Button>
</div>
</div>
<div>
<label className="text-sm font-medium">Timestamp</label>
<p className="text-sm">{format(new Date(error.created_at), 'PPpp')}</p>
</div>
<div>
<label className="text-sm font-medium">Endpoint</label>
<p className="text-sm font-mono">{error.endpoint}</p>
</div>
<div>
<label className="text-sm font-medium">Method</label>
<Badge variant="outline">{error.method}</Badge>
</div>
<div>
<label className="text-sm font-medium">Status Code</label>
<p className="text-sm">{error.status_code}</p>
</div>
<div>
<label className="text-sm font-medium">Duration</label>
<p className="text-sm">{error.duration_ms}ms</p>
</div>
{error.user_id && (
<div>
<label className="text-sm font-medium">User ID</label>
<a
href={`/admin/users?search=${error.user_id}`}
className="text-sm text-primary hover:underline flex items-center gap-1"
>
{error.user_id.slice(0, 8)}...
<ExternalLink className="w-3 h-3" />
</a>
</div>
)}
</div>
<div>
<label className="text-sm font-medium">Error Message</label>
<div className="bg-muted p-4 rounded-lg mt-2">
<p className="text-sm font-mono">{error.error_message}</p>
</div>
</div>
</TabsContent>
<TabsContent value="stack">
{error.error_stack ? (
<pre className="bg-muted p-4 rounded-lg overflow-x-auto text-xs">
{error.error_stack}
</pre>
) : (
<p className="text-muted-foreground">No stack trace available</p>
)}
</TabsContent>
<TabsContent value="breadcrumbs">
{error.request_breadcrumbs && error.request_breadcrumbs.length > 0 ? (
<div className="space-y-2">
{error.request_breadcrumbs
.sort((a, b) => (a.sequence_order || 0) - (b.sequence_order || 0))
.map((crumb, index) => (
<div key={index} className="border-l-2 border-primary pl-4 py-2">
<div className="flex items-center gap-2 mb-1">
<Badge variant="outline" className="text-xs">
{crumb.category}
</Badge>
<Badge variant={crumb.level === 'error' ? 'destructive' : 'secondary'} className="text-xs">
{crumb.level || 'info'}
</Badge>
<span className="text-xs text-muted-foreground">
{format(new Date(crumb.timestamp), 'HH:mm:ss.SSS')}
</span>
</div>
<p className="text-sm">{crumb.message}</p>
</div>
))}
</div>
) : (
<p className="text-muted-foreground">No breadcrumbs recorded</p>
)}
</TabsContent>
<TabsContent value="environment">
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
{error.user_agent && (
<div>
<label className="text-sm font-medium">User Agent</label>
<p className="text-xs font-mono break-all">{error.user_agent}</p>
</div>
)}
{error.client_version && (
<div>
<label className="text-sm font-medium">Client Version</label>
<p className="text-sm">{error.client_version}</p>
</div>
)}
{error.timezone && (
<div>
<label className="text-sm font-medium">Timezone</label>
<p className="text-sm">{error.timezone}</p>
</div>
)}
{error.referrer && (
<div>
<label className="text-sm font-medium">Referrer</label>
<p className="text-xs font-mono break-all">{error.referrer}</p>
</div>
)}
{error.ip_address_hash && (
<div>
<label className="text-sm font-medium">IP Hash</label>
<p className="text-xs font-mono">{error.ip_address_hash}</p>
</div>
)}
</div>
{!error.user_agent && !error.client_version && !error.timezone && !error.referrer && !error.ip_address_hash && (
<p className="text-muted-foreground">No environment data available</p>
)}
</div>
</TabsContent>
</Tabs>
<div className="flex justify-end gap-2">
<Button variant="outline" onClick={copyErrorReport}>
<Copy className="w-4 h-4 mr-2" />
Copy Report
</Button>
<Button onClick={onClose}>Close</Button>
</div>
</DialogContent>
</Dialog>
);
}