mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-24 10:51: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:
@@ -75,11 +75,31 @@ export async function invokeWithTracking<T = any>(
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
||||
|
||||
// Generate W3C Trace Context header
|
||||
const effectiveTraceId = context.traceId || crypto.randomUUID();
|
||||
const spanId = crypto.randomUUID();
|
||||
const traceparent = `00-${effectiveTraceId}-${spanId}-01`;
|
||||
|
||||
// Add breadcrumb with trace context
|
||||
breadcrumb.apiCall(
|
||||
`/functions/${functionName}`,
|
||||
'POST',
|
||||
undefined,
|
||||
{ traceId: effectiveTraceId, spanId }
|
||||
);
|
||||
|
||||
try {
|
||||
const { data, error } = await supabase.functions.invoke<T>(functionName, {
|
||||
body: { ...payload, clientRequestId: context.requestId },
|
||||
body: {
|
||||
...payload,
|
||||
clientRequestId: context.requestId,
|
||||
traceId: effectiveTraceId,
|
||||
},
|
||||
signal: controller.signal,
|
||||
headers: customHeaders,
|
||||
headers: {
|
||||
...customHeaders,
|
||||
'traceparent': traceparent,
|
||||
},
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
@@ -103,7 +123,15 @@ export async function invokeWithTracking<T = any>(
|
||||
}
|
||||
);
|
||||
|
||||
return { data: result, error: null, requestId, duration, attempts: attemptCount, status: 200 };
|
||||
return {
|
||||
data: result,
|
||||
error: null,
|
||||
requestId,
|
||||
duration,
|
||||
attempts: attemptCount,
|
||||
status: 200,
|
||||
traceId,
|
||||
};
|
||||
} catch (error: unknown) {
|
||||
// Handle AbortError specifically
|
||||
if (error instanceof Error && error.name === 'AbortError') {
|
||||
|
||||
150
src/lib/spanVisualizer.ts
Normal file
150
src/lib/spanVisualizer.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* Span Visualizer
|
||||
* Reconstructs span trees from logs for debugging distributed traces
|
||||
*/
|
||||
|
||||
import type { Span } from '@/types/tracing';
|
||||
|
||||
export interface SpanTree {
|
||||
span: Span;
|
||||
children: SpanTree[];
|
||||
totalDuration: number;
|
||||
selfDuration: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build span tree from flat span logs
|
||||
*/
|
||||
export function buildSpanTree(spans: Span[]): SpanTree | null {
|
||||
const spanMap = new Map<string, Span>();
|
||||
const childrenMap = new Map<string, Span[]>();
|
||||
|
||||
// Index spans
|
||||
for (const span of spans) {
|
||||
spanMap.set(span.spanId, span);
|
||||
|
||||
if (span.parentSpanId) {
|
||||
if (!childrenMap.has(span.parentSpanId)) {
|
||||
childrenMap.set(span.parentSpanId, []);
|
||||
}
|
||||
childrenMap.get(span.parentSpanId)!.push(span);
|
||||
}
|
||||
}
|
||||
|
||||
// Find root span
|
||||
const rootSpan = spans.find(s => !s.parentSpanId);
|
||||
if (!rootSpan) return null;
|
||||
|
||||
// Build tree recursively
|
||||
function buildTree(span: Span): SpanTree {
|
||||
const children = childrenMap.get(span.spanId) || [];
|
||||
const childTrees = children.map(buildTree);
|
||||
|
||||
const totalDuration = span.duration || 0;
|
||||
const childrenDuration = childTrees.reduce((sum, child) => sum + child.totalDuration, 0);
|
||||
const selfDuration = totalDuration - childrenDuration;
|
||||
|
||||
return {
|
||||
span,
|
||||
children: childTrees,
|
||||
totalDuration,
|
||||
selfDuration,
|
||||
};
|
||||
}
|
||||
|
||||
return buildTree(rootSpan);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format span tree as ASCII art
|
||||
*/
|
||||
export function formatSpanTree(tree: SpanTree, indent: number = 0): string {
|
||||
const prefix = ' '.repeat(indent);
|
||||
const status = tree.span.status === 'error' ? '❌' : tree.span.status === 'ok' ? '✅' : '⏳';
|
||||
const line = `${prefix}${status} ${tree.span.name} (${tree.span.duration}ms / self: ${tree.selfDuration}ms)`;
|
||||
|
||||
const childLines = tree.children.map(child => formatSpanTree(child, indent + 1));
|
||||
|
||||
return [line, ...childLines].join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate span statistics for a tree
|
||||
*/
|
||||
export function calculateSpanStats(tree: SpanTree): {
|
||||
totalSpans: number;
|
||||
errorCount: number;
|
||||
maxDepth: number;
|
||||
totalDuration: number;
|
||||
criticalPath: string[];
|
||||
} {
|
||||
let totalSpans = 0;
|
||||
let errorCount = 0;
|
||||
let maxDepth = 0;
|
||||
|
||||
function traverse(node: SpanTree, depth: number) {
|
||||
totalSpans++;
|
||||
if (node.span.status === 'error') errorCount++;
|
||||
maxDepth = Math.max(maxDepth, depth);
|
||||
|
||||
node.children.forEach(child => traverse(child, depth + 1));
|
||||
}
|
||||
|
||||
traverse(tree, 0);
|
||||
|
||||
// Find critical path (longest duration path)
|
||||
function findCriticalPath(node: SpanTree): string[] {
|
||||
if (node.children.length === 0) {
|
||||
return [node.span.name];
|
||||
}
|
||||
|
||||
const longestChild = node.children.reduce((longest, child) =>
|
||||
child.totalDuration > longest.totalDuration ? child : longest
|
||||
);
|
||||
|
||||
return [node.span.name, ...findCriticalPath(longestChild)];
|
||||
}
|
||||
|
||||
return {
|
||||
totalSpans,
|
||||
errorCount,
|
||||
maxDepth,
|
||||
totalDuration: tree.totalDuration,
|
||||
criticalPath: findCriticalPath(tree),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract all events from a span tree
|
||||
*/
|
||||
export function extractAllEvents(tree: SpanTree): Array<{
|
||||
spanName: string;
|
||||
eventName: string;
|
||||
timestamp: number;
|
||||
attributes?: Record<string, unknown>;
|
||||
}> {
|
||||
const events: Array<{
|
||||
spanName: string;
|
||||
eventName: string;
|
||||
timestamp: number;
|
||||
attributes?: Record<string, unknown>;
|
||||
}> = [];
|
||||
|
||||
function traverse(node: SpanTree) {
|
||||
node.span.events.forEach(event => {
|
||||
events.push({
|
||||
spanName: node.span.name,
|
||||
eventName: event.name,
|
||||
timestamp: event.timestamp,
|
||||
attributes: event.attributes,
|
||||
});
|
||||
});
|
||||
|
||||
node.children.forEach(child => traverse(child));
|
||||
}
|
||||
|
||||
traverse(tree);
|
||||
|
||||
// Sort by timestamp
|
||||
return events.sort((a, b) => a.timestamp - b.timestamp);
|
||||
}
|
||||
Reference in New Issue
Block a user