import { flushIfDev, startInactiveSpan, type Span } from '@/remote/monitor'; export type EntryRoute = | 'camera-full' | 'camera-cropped' | 'camera-roll' | 'text-input' | 'suggestion'; export type SpanName = | 'photo-capture' | 'image-crop' | 'image-crop-bounding-box' | 'image-persist' | 'image-encode' | 'camera-roll-pick' | 'staged-log-creation' | 'food-detection' | 'food-detection-ai' | 'ai-gemini' | 'ai-claude' | 'ai-openai' | 'ingredient-detection-ai' | 'candidate-switch-ai' | 'database-persist' | 'background-image-encode' | 'background-food-reco' | 'background-nutrition-reco' | 'photo-upload-url' | 'photo-upload-bucket' | 'final-render'; const SPAN_OPS: Record = { 'photo-capture': 'ui.action', 'image-crop': 'file', 'image-crop-bounding-box': 'file', 'image-persist': 'file', 'image-encode': 'serialize', 'camera-roll-pick': 'ui.action', 'staged-log-creation': 'function', 'food-detection': 'function', 'food-detection-ai': 'ai.inference', 'ai-gemini': 'ai.inference', 'ai-claude': 'ai.inference', 'ai-openai': 'ai.inference', 'ingredient-detection-ai': 'http.client', 'candidate-switch-ai': 'ai.inference', 'database-persist': 'db', 'background-image-encode': 'serialize', 'background-food-reco': 'queue.task', 'background-nutrition-reco': 'queue.task', 'photo-upload-url': 'http.client', 'photo-upload-bucket': 'http.client', 'final-render': 'ui.render', }; type TraceState = { transaction: Span; entryRoute: EntryRoute; spans: Map; startedAt: number; }; const traces = new Map(); const TRACE_TTL_MS = 5 * 60 * 1000; export function startTrace(logId: string, entryRoute: EntryRoute): void { if (traces.has(logId)) { cancelTrace(logId); } const transaction = startInactiveSpan({ name: 'add_log', op: 'user.flow', forceTransaction: true, attributes: { entry_route: entryRoute, log_id: logId }, }); if (!transaction) return; traces.set(logId, { transaction, entryRoute, spans: new Map(), startedAt: Date.now(), }); setTimeout(() => { if (traces.has(logId)) { cancelTrace(logId); } }, TRACE_TTL_MS); } export function forkTrace(parentLogId: string, newLogId: string): void { const parent = traces.get(parentLogId); if (!parent) return; const transaction = startInactiveSpan({ name: 'add_log', op: 'user.flow', forceTransaction: true, attributes: { entry_route: parent.entryRoute, log_id: newLogId, forked_from: parentLogId, }, }); if (!transaction) return; traces.set(newLogId, { transaction, entryRoute: parent.entryRoute, spans: new Map(), startedAt: Date.now(), }); parent.transaction.setAttributes({ spawned_items: 'true' }); } export function startSpan(logId: string, spanName: SpanName): void { const trace = traces.get(logId); if (!trace) return; if (trace.spans.has(spanName)) return; const span = startInactiveSpan({ name: `add_log.${spanName.replace(/-/g, '_')}`, op: SPAN_OPS[spanName], parentSpan: trace.transaction, attributes: { entry_route: trace.entryRoute, log_id: logId }, }); if (span) trace.spans.set(spanName, span); } const FLUSH_ON_END: SpanName[] = ['food-detection', 'food-detection-ai', 'ingredient-detection-ai']; export async function endSpan(logId: string, spanName: SpanName, error?: unknown): Promise { const trace = traces.get(logId); if (!trace) return; const span = trace.spans.get(spanName); if (!span) return; if (error) { span.setStatus({ code: 2, message: error instanceof Error ? error.message : String(error) }); } else { span.setStatus({ code: 1 }); } span.end(); trace.spans.delete(spanName); if (FLUSH_ON_END.includes(spanName)) { await flushIfDev(); } } export async function endTrace(logId: string, success: boolean = true): Promise { const trace = traces.get(logId); if (!trace) return; trace.transaction.setStatus({ code: success ? 1 : 2 }); trace.transaction.setAttributes({ success: String(success) }); trace.transaction.end(); traces.delete(logId); await flushIfDev(); } export async function cancelTrace(logId: string): Promise { const trace = traces.get(logId); if (!trace) return; trace.spans.forEach(span => { span.setStatus({ code: 2, message: 'cancelled' }); span.end(); }); trace.transaction.setStatus({ code: 2, message: 'cancelled' }); trace.transaction.setAttributes({ success: 'false', cancelled: 'true' }); trace.transaction.end(); traces.delete(logId); await flushIfDev(); } export function getTraceEntryRoute(logId: string): EntryRoute | undefined { return traces.get(logId)?.entryRoute; } export function hasActiveTrace(logId: string): boolean { return traces.has(logId); } export async function cancelAllTraces(): Promise { const ids = Array.from(traces.keys()); for (const logId of ids) await cancelTrace(logId); }