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:
gpt-engineer-app[bot]
2025-11-10 14:40:44 +00:00
parent 1551a2f08d
commit 96adb2b15e
7 changed files with 848 additions and 27 deletions

View File

@@ -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
View 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);
}