import { trace } from "@opentelemetry/api"; import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http"; import { BatchSpanProcessor, type ReadableSpan, type SpanExporter, type SpanProcessor, } from "@opentelemetry/sdk-trace-base"; import { normalizeBaseUrl } from "../baseUrl"; const DEFAULT_BACKEND_BASE_URL = "https://backend.assistant-api.com"; const AI_SPAN_PREFIXES = ["gen_ai.", "ai.", "llm."]; export type AssistantCloudTraceExportOptions = { apiKey: string; baseUrl?: string; headers?: Record; }; export type AssistantCloudSpanProcessorOptions = { filter?: (span: ReadableSpan) => boolean; }; export function assistantCloudTraceExportOptions({ apiKey, baseUrl = DEFAULT_BACKEND_BASE_URL, headers, }: AssistantCloudTraceExportOptions): { url: string; headers: Record; } { if (!apiKey) throw new Error("An Assistant Cloud API key is required"); return { url: `${normalizeBaseUrl(baseUrl)}/v1/traces`, headers: { Authorization: `Bearer ${apiKey}`, ...headers, }, }; } export function createAssistantCloudTraceExporter( options: AssistantCloudTraceExportOptions, ): SpanExporter { return new OTLPTraceExporter(assistantCloudTraceExportOptions(options)); } export function isAssistantCloudSpan(span: ReadableSpan): boolean { return ( typeof span.attributes["gen_ai.operation.name"] === "string" || AI_SPAN_PREFIXES.some((prefix) => span.name.startsWith(prefix)) ); } export function createAssistantCloudSpanProcessor( exporter: SpanExporter, options: AssistantCloudSpanProcessorOptions = {}, ): SpanProcessor { const processor = new BatchSpanProcessor(exporter); const filter = options.filter ?? isAssistantCloudSpan; return { onStart: (span, parentContext) => processor.onStart(span, parentContext), onEnd: (span) => { if (filter(span)) processor.onEnd(span); }, forceFlush: () => processor.forceFlush(), shutdown: () => processor.shutdown(), }; } export function assistantCloudTraceMetadata(): { traceId?: string } { const spanContext = trace.getActiveSpan()?.spanContext(); return spanContext && trace.isSpanContextValid(spanContext) ? { traceId: spanContext.traceId } : {}; } export function withAssistantCloudTraceMetadata< Part extends { type: string } = { type: string }, >( messageMetadata?: (options: { part: Part; }) => Record | undefined, ): (options: { part: Part }) => Record | undefined { return (options) => { const metadata = messageMetadata?.(options); if (options.part.type !== "start") return metadata; const trace = assistantCloudTraceMetadata(); return trace.traceId === undefined ? metadata : { ...metadata, ...trace }; }; }