From 683d45b5ccef852cc9d43c2a12157d7a90ce0cfd Mon Sep 17 00:00:00 2001 From: Alvaro Date: Thu, 9 Oct 2025 00:37:21 +0200 Subject: [PATCH] feat(ai): rootSpan flag for telemetry options Adds a new flag `rootSpan` to `experimental_telemetry` that instructs to create a root span (detached from any parent) during the execution. Defaults to false keeping the current behavior. The reason to propose this is that currently I'm having a hard time getting telemetry to show up correctly in langfuse while using effect. Because the stream returned by stream operations exists outside the lifecycle of the calling function, traces get reported out of order, and ultimately certain attributes that should be made available to the root span never make it. So forcing the ai traces to be isolated isi the only way I've been able to sort of solve it. --- .changeset/clean-deers-pull.md | 5 +++++ packages/ai/src/embed/embed-many.ts | 1 + packages/ai/src/embed/embed.ts | 1 + packages/ai/src/generate-object/generate-object.ts | 1 + packages/ai/src/generate-object/stream-object.ts | 1 + packages/ai/src/generate-text/generate-text.ts | 1 + packages/ai/src/generate-text/stream-text.ts | 1 + packages/ai/src/telemetry/record-span.ts | 4 +++- packages/ai/src/telemetry/telemetry-settings.ts | 5 +++++ 9 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 .changeset/clean-deers-pull.md diff --git a/.changeset/clean-deers-pull.md b/.changeset/clean-deers-pull.md new file mode 100644 index 000000000000..aaf3d0170b2a --- /dev/null +++ b/.changeset/clean-deers-pull.md @@ -0,0 +1,5 @@ +--- +'ai': patch +--- + +feat(ai): rootSpan flag for telemetry options diff --git a/packages/ai/src/embed/embed-many.ts b/packages/ai/src/embed/embed-many.ts index 1fbe717a880e..173f76590d64 100644 --- a/packages/ai/src/embed/embed-many.ts +++ b/packages/ai/src/embed/embed-many.ts @@ -121,6 +121,7 @@ Only applicable for HTTP-based providers. }, }), tracer, + root: telemetry?.rootSpan, fn: async span => { const [maxEmbeddingsPerCall, supportsParallelCalls] = await Promise.all([ model.maxEmbeddingsPerCall, diff --git a/packages/ai/src/embed/embed.ts b/packages/ai/src/embed/embed.ts index 6c3027ad1d03..cbaae2a20123 100644 --- a/packages/ai/src/embed/embed.ts +++ b/packages/ai/src/embed/embed.ts @@ -104,6 +104,7 @@ Only applicable for HTTP-based providers. }, }), tracer, + root: telemetry?.rootSpan, fn: async span => { const { embedding, usage, response, providerMetadata } = await retry(() => // nested spans to align with the embedMany telemetry data: diff --git a/packages/ai/src/generate-object/generate-object.ts b/packages/ai/src/generate-object/generate-object.ts index f181ee5143d1..be74ebc11117 100644 --- a/packages/ai/src/generate-object/generate-object.ts +++ b/packages/ai/src/generate-object/generate-object.ts @@ -298,6 +298,7 @@ Default and recommended: 'auto' (best mode for the model). }, }), tracer, + root: telemetry?.rootSpan, fn: async span => { let result: string; let finishReason: FinishReason; diff --git a/packages/ai/src/generate-object/stream-object.ts b/packages/ai/src/generate-object/stream-object.ts index f0780eff7eb0..579f1dd567ce 100644 --- a/packages/ai/src/generate-object/stream-object.ts +++ b/packages/ai/src/generate-object/stream-object.ts @@ -491,6 +491,7 @@ class DefaultStreamObjectResult }, }), tracer, + root: telemetry?.rootSpan, endWhenDone: false, fn: async rootSpan => { const standardizedPrompt = await standardizePrompt({ diff --git a/packages/ai/src/generate-text/generate-text.ts b/packages/ai/src/generate-text/generate-text.ts index 755cac79d466..36479c8178d4 100644 --- a/packages/ai/src/generate-text/generate-text.ts +++ b/packages/ai/src/generate-text/generate-text.ts @@ -328,6 +328,7 @@ A function that attempts to repair a tool call that failed to parse. }, }), tracer, + root: telemetry?.rootSpan, fn: async span => { const initialMessages = initialPrompt.messages; const responseMessages: Array = []; diff --git a/packages/ai/src/generate-text/stream-text.ts b/packages/ai/src/generate-text/stream-text.ts index 75dc4b206d7e..f52cdf7b083b 100644 --- a/packages/ai/src/generate-text/stream-text.ts +++ b/packages/ai/src/generate-text/stream-text.ts @@ -1057,6 +1057,7 @@ class DefaultStreamTextResult }, }), tracer, + root: telemetry?.rootSpan, endWhenDone: false, fn: async rootSpanArg => { rootSpan = rootSpanArg; diff --git a/packages/ai/src/telemetry/record-span.ts b/packages/ai/src/telemetry/record-span.ts index ba2efef9a6c9..e20ec8f87391 100644 --- a/packages/ai/src/telemetry/record-span.ts +++ b/packages/ai/src/telemetry/record-span.ts @@ -3,17 +3,19 @@ import { Attributes, Span, Tracer, SpanStatusCode } from '@opentelemetry/api'; export function recordSpan({ name, tracer, + root, attributes, fn, endWhenDone = true, }: { name: string; tracer: Tracer; + root?: boolean; attributes: Attributes; fn: (span: Span) => Promise; endWhenDone?: boolean; }) { - return tracer.startActiveSpan(name, { attributes }, async span => { + return tracer.startActiveSpan(name, { attributes, root }, async span => { try { const result = await fn(span); diff --git a/packages/ai/src/telemetry/telemetry-settings.ts b/packages/ai/src/telemetry/telemetry-settings.ts index 6151d4621976..baa9a38a78dd 100644 --- a/packages/ai/src/telemetry/telemetry-settings.ts +++ b/packages/ai/src/telemetry/telemetry-settings.ts @@ -41,4 +41,9 @@ export type TelemetrySettings = { * A custom tracer to use for the telemetry data. */ tracer?: Tracer; + + /** + * Whether to create a root span for the operation. + */ + rootSpan?: boolean; };