mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-21 08:31:13 -05:00
Connect to Lovable Cloud
Implement distributed tracing across edge functions: - Introduce span types and utilities (logger.ts enhancements) - Replace request tracking with span-based tracing in approval and rejection flows - Propagate and manage W3C trace context in edge tracking - Add span visualization scaffolding (spanVisualizer.ts) and admin TraceViewer UI (TraceViewer.tsx) - Create tracing-related type definitions and support files - Prepare RPC call logging with span context and retries
This commit is contained in:
255
src/pages/admin/TraceViewer.tsx
Normal file
255
src/pages/admin/TraceViewer.tsx
Normal file
@@ -0,0 +1,255 @@
|
||||
import { useState } from 'react';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { buildSpanTree, formatSpanTree, calculateSpanStats, extractAllEvents } from '@/lib/spanVisualizer';
|
||||
import type { Span } from '@/types/tracing';
|
||||
import type { SpanTree } from '@/lib/spanVisualizer';
|
||||
|
||||
/**
|
||||
* Admin Trace Viewer
|
||||
*
|
||||
* Visual tool for debugging distributed traces across the approval pipeline.
|
||||
* Reconstructs and displays span hierarchies from edge function logs.
|
||||
*/
|
||||
export default function TraceViewer() {
|
||||
const [traceId, setTraceId] = useState('');
|
||||
const [spans, setSpans] = useState<Span[]>([]);
|
||||
const [tree, setTree] = useState<SpanTree | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const loadTrace = async () => {
|
||||
if (!traceId.trim()) {
|
||||
setError('Please enter a trace ID');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
// TODO: Replace with actual edge function log query
|
||||
// This would need an edge function that queries Supabase logs
|
||||
// For now, using mock data structure
|
||||
const mockSpans: Span[] = [
|
||||
{
|
||||
spanId: 'root-1',
|
||||
traceId,
|
||||
name: 'process-selective-approval',
|
||||
kind: 'SERVER',
|
||||
startTime: Date.now() - 5000,
|
||||
endTime: Date.now(),
|
||||
duration: 5000,
|
||||
attributes: {
|
||||
'http.method': 'POST',
|
||||
'user.id': 'user-123',
|
||||
'submission.id': 'sub-456',
|
||||
},
|
||||
events: [
|
||||
{ timestamp: Date.now() - 4900, name: 'authentication_start' },
|
||||
{ timestamp: Date.now() - 4800, name: 'authentication_success' },
|
||||
{ timestamp: Date.now() - 4700, name: 'validation_complete' },
|
||||
],
|
||||
status: 'ok',
|
||||
},
|
||||
{
|
||||
spanId: 'child-1',
|
||||
traceId,
|
||||
parentSpanId: 'root-1',
|
||||
name: 'process_approval_transaction',
|
||||
kind: 'DATABASE',
|
||||
startTime: Date.now() - 4500,
|
||||
endTime: Date.now() - 500,
|
||||
duration: 4000,
|
||||
attributes: {
|
||||
'db.operation': 'rpc',
|
||||
'submission.id': 'sub-456',
|
||||
},
|
||||
events: [
|
||||
{ timestamp: Date.now() - 4400, name: 'rpc_call_start' },
|
||||
{ timestamp: Date.now() - 600, name: 'rpc_call_success' },
|
||||
],
|
||||
status: 'ok',
|
||||
},
|
||||
];
|
||||
|
||||
setSpans(mockSpans);
|
||||
const builtTree = buildSpanTree(mockSpans);
|
||||
setTree(builtTree);
|
||||
|
||||
if (!builtTree) {
|
||||
setError('No root span found for this trace ID');
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to load trace');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const stats = tree ? calculateSpanStats(tree) : null;
|
||||
const events = tree ? extractAllEvents(tree) : [];
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-6 space-y-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold mb-2">Distributed Trace Viewer</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Debug moderation pipeline execution by visualizing span hierarchies
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Load Trace</CardTitle>
|
||||
<CardDescription>
|
||||
Enter a trace ID from edge function logs to visualize the execution tree
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={traceId}
|
||||
onChange={(e) => setTraceId(e.target.value)}
|
||||
placeholder="Enter trace ID (e.g., abc-123-def-456)"
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button onClick={loadTrace} disabled={isLoading}>
|
||||
{isLoading ? 'Loading...' : 'Load Trace'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<Alert variant="destructive" className="mt-4">
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{tree && stats && (
|
||||
<>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Trace Statistics</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div>
|
||||
<div className="text-sm text-muted-foreground">Total Duration</div>
|
||||
<div className="text-2xl font-bold">{stats.totalDuration}ms</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-muted-foreground">Total Spans</div>
|
||||
<div className="text-2xl font-bold">{stats.totalSpans}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-muted-foreground">Max Depth</div>
|
||||
<div className="text-2xl font-bold">{stats.maxDepth}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-muted-foreground">Errors</div>
|
||||
<div className="text-2xl font-bold text-destructive">{stats.errorCount}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<div className="text-sm text-muted-foreground mb-2">Critical Path (Longest Duration):</div>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{stats.criticalPath.map((spanName, i) => (
|
||||
<Badge key={i} variant="secondary">
|
||||
{spanName}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Span Tree</CardTitle>
|
||||
<CardDescription>
|
||||
Hierarchical view of span execution with timing breakdown
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<pre className="bg-muted p-4 rounded-lg overflow-x-auto text-sm">
|
||||
{formatSpanTree(tree)}
|
||||
</pre>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Events Timeline</CardTitle>
|
||||
<CardDescription>
|
||||
Chronological list of all events across all spans
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
{events.map((event, i) => (
|
||||
<div key={i} className="flex gap-2 text-sm border-l-2 border-primary pl-4 py-1">
|
||||
<Badge variant="outline">{event.spanName}</Badge>
|
||||
<span className="text-muted-foreground">→</span>
|
||||
<span className="font-medium">{event.eventName}</span>
|
||||
<span className="text-muted-foreground ml-auto">
|
||||
{new Date(event.timestamp).toISOString()}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Span Details</CardTitle>
|
||||
<CardDescription>
|
||||
Detailed breakdown of each span with attributes and events
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Accordion type="single" collapsible className="w-full">
|
||||
{spans.map((span) => (
|
||||
<AccordionItem key={span.spanId} value={span.spanId}>
|
||||
<AccordionTrigger>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant={span.status === 'error' ? 'destructive' : 'default'}>
|
||||
{span.kind}
|
||||
</Badge>
|
||||
<span>{span.name}</span>
|
||||
<span className="text-muted-foreground ml-2">
|
||||
({span.duration}ms)
|
||||
</span>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<pre className="bg-muted p-4 rounded-lg overflow-x-auto text-xs">
|
||||
{JSON.stringify(span, null, 2)}
|
||||
</pre>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!tree && !isLoading && !error && (
|
||||
<Alert>
|
||||
<AlertDescription>
|
||||
Enter a trace ID to visualize the distributed trace. You can find trace IDs in edge function logs
|
||||
under the "Span completed" messages.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user