mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 02:51:12 -05:00
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
256 lines
9.0 KiB
TypeScript
256 lines
9.0 KiB
TypeScript
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>
|
|
);
|
|
}
|