mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 12:31:26 -05:00
236 lines
8.7 KiB
TypeScript
236 lines
8.7 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
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';
|
|
import { supabase } from '@/lib/supabaseClient';
|
|
|
|
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) {
|
|
// Use breadcrumbs from error object if already fetched, otherwise they'll be empty
|
|
const breadcrumbs = error.request_breadcrumbs || [];
|
|
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}${error.duration_ms != null ? `\nDuration: ${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>
|
|
{error.duration_ms != null && (
|
|
<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">
|
|
{breadcrumbs && breadcrumbs.length > 0 ? (
|
|
<div className="space-y-2">
|
|
{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>
|
|
);
|
|
}
|