Skip to content

Commit 314f52f

Browse files
feat(server-utils): Emit low-cardinality gen_ai inference span names when streaming
When span streaming is on, inference spans use `{operation} {model}` or `{operation}` if the model is missing, and execute_tool drops the tool name from the span name. Static lifecycle names are unchanged. Co-Authored-By: Cursor Grok 4.6 <cursoragent@cursor.com>
1 parent 1718e70 commit 314f52f

20 files changed

Lines changed: 510 additions & 34 deletions

File tree

MIGRATION.md

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -617,17 +617,21 @@ These changes are not caught by TypeScript. If you filter, group, or alert on sp
617617

618618
Affected SDKs: All SDKs.
619619

620-
With [span streaming](#span-streaming-is-now-the-default) enabled(the default), span names are now **low cardinality**, following the [Sentry span name conventions](https://getsentry.github.io/sentry-conventions/names/).
620+
With [span streaming](#span-streaming-is-now-the-default) enabled (the default), span names are now **low cardinality**, following the [Sentry span name conventions](https://getsentry.github.io/sentry-conventions/names/).
621621

622-
In v11, this affects `pageload` and `graphql` spans. Further ops will follow in future releases.
622+
In v11, this affects `pageload`, `graphql`, and `gen_ai` spans. Further ops will follow in future releases.
623623
If you [opt out of span streaming](#opting-out-of-span-streaming), span names remain unchanged.
624624

625625
The following span names were adjusted:
626626

627-
| Span op | Before | After |
628-
| ---------- | --------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
629-
| `pageload` | The parameterized route, or the raw URL path if the SDK couldn't resolve one (`/users/123`) | The parameterized route, or `Pageload` if the SDK has none |
630-
| `graphql` | The graphql phase and, for operations, the operation name (`query GetUser`, `graphql.parse`, `graphql.resolve user.0.name`) | The operation type, or the processing type where there is none (`GraphQL query`, `GraphQL parse`, `GraphQL resolve`) |
627+
| Span op | Before | After |
628+
| ---------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
629+
| `pageload` | The parameterized route, or the raw URL path if the SDK couldn't resolve one (`/users/123`) | The parameterized route, or `Pageload` if the SDK has none |
630+
| `graphql` | The graphql phase and, for operations, the operation name (`query GetUser`, `graphql.parse`, `graphql.resolve user.0.name`) | The operation type, or the processing type where there is none (`GraphQL query`, `GraphQL parse`, `GraphQL resolve`) |
631+
| `gen_ai.chat`, `gen_ai.embeddings`, `gen_ai.generate_content`, … | `{operation} {model}`, or `{operation} unknown` if the model is missing (`chat unknown`) | `{operation} {model}`, or `{operation}` if the model is missing (`chat`). Instrumented methods always have an operation, so the convention fallback `Generative AI model operation` is unused today. |
632+
| `gen_ai.execute_tool` | `execute_tool {tool name}` (`execute_tool getWeather`) | `execute_tool`; the tool name stays on `gen_ai.tool.name` |
633+
634+
Resolved low-cardinality values are kept in both lifecycles: a known model stays in the name (`chat gpt-4`).
631635

632636
Some consequences to be aware of:
633637

@@ -639,7 +643,7 @@ For the same reason, `useOperationNameForRootSpan` no longer renames the enclosi
639643

640644
Child spans of a pageload span carry its name in their `sentry.segment.name` attribute, so that changes with it. If you group or filter spans by segment name in dashboards or alerts, update those references.
641645

642-
`ignoreSpans` is evaluated when a span **starts**, at which point a pageload span without a resolved route is already named `'Pageload'`, so filters matching a URL path no longer apply to it. Match on attributes instead:
646+
`ignoreSpans` is evaluated when a span **starts**. Filters matching a raw URL path no longer apply to an unresolved pageload (`'Pageload'`), filters matching `chat unknown` no longer apply to a streamed chat span (`'chat'`), and filters matching `execute_tool getWeather` no longer apply to a streamed tool span (`'execute_tool'`). Match on attributes instead:
643647

644648
```js
645649
Sentry.init({

dev-packages/node-integration-tests/suites/tracing/vercelai/span-streaming-v4/test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ describe('Vercel AI integration (streaming v4)', () => {
114114
// Sixth span - execute_tool
115115
// Note: gen_ai.tool.description is NOT present when genAI recording disabled because ai.prompt.tools is not recorded
116116
expect.objectContaining({
117-
name: 'execute_tool getWeather',
117+
name: 'execute_tool',
118118
status: 'ok',
119119
attributes: expect.objectContaining({
120120
[GEN_AI_TOOL_CALL_ID_ATTRIBUTE]: attr('call-1'),
@@ -222,7 +222,7 @@ describe('Vercel AI integration (streaming v4)', () => {
222222
}),
223223
// Sixth span - execute_tool with description and input/output
224224
expect.objectContaining({
225-
name: 'execute_tool getWeather',
225+
name: 'execute_tool',
226226
status: 'ok',
227227
attributes: expect.objectContaining({
228228
[GEN_AI_TOOL_CALL_ID_ATTRIBUTE]: attr('call-1'),
@@ -263,7 +263,7 @@ describe('Vercel AI integration (streaming v4)', () => {
263263
}),
264264
}),
265265
expect.objectContaining({
266-
name: 'execute_tool getWeather',
266+
name: 'execute_tool',
267267
status: 'error',
268268
attributes: expect.objectContaining({
269269
[GEN_AI_TOOL_CALL_ID_ATTRIBUTE]: attr('call-1'),

dev-packages/node-integration-tests/suites/tracing/vercelai/span-streaming-v6/test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ describe('Vercel AI integration (streaming, v6)', () => {
111111
// Sixth span - execute_tool
112112
// Note: gen_ai.tool.description is NOT present when genAI recording disabled because ai.prompt.tools is not recorded
113113
expect.objectContaining({
114-
name: 'execute_tool getWeather',
114+
name: 'execute_tool',
115115
status: 'ok',
116116
attributes: expect.objectContaining({
117117
[GEN_AI_TOOL_CALL_ID_ATTRIBUTE]: attr('call-1'),
@@ -219,7 +219,7 @@ describe('Vercel AI integration (streaming, v6)', () => {
219219
}),
220220
// Sixth span - execute_tool with description and input/output
221221
expect.objectContaining({
222-
name: 'execute_tool getWeather',
222+
name: 'execute_tool',
223223
status: 'ok',
224224
attributes: expect.objectContaining({
225225
[GEN_AI_TOOL_CALL_ID_ATTRIBUTE]: attr('call-1'),
@@ -259,7 +259,7 @@ describe('Vercel AI integration (streaming, v6)', () => {
259259
}),
260260
}),
261261
expect.objectContaining({
262-
name: 'execute_tool getWeather',
262+
name: 'execute_tool',
263263
status: 'error',
264264
attributes: expect.objectContaining({
265265
[GEN_AI_TOOL_CALL_ID_ATTRIBUTE]: attr('call-1'),

docs/migration/v11-end-state.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,23 @@ Sentry.init({
475475
476476
In Node, Bun, Vercel Edge and Cloudflare you can also set the `SENTRY_TRACE_LIFECYCLE=static` environment variable instead. The static lifecycle only exists for backwards compatibility and is planned for removal in a future major version, so treat this as a temporary measure.
477477
478+
### Span name changes
479+
480+
Affected SDKs: All SDKs.
481+
482+
With [span streaming](#span-streaming-is-now-the-default) enabled (the default), span names are now **low cardinality**, following the [Sentry span name conventions](https://getsentry.github.io/sentry-conventions/names/). If you [opt out of span streaming](#opting-out-of-span-streaming), span names remain unchanged.
483+
484+
| Span op | Before | After |
485+
| ---------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
486+
| `pageload` | The parameterized route, or the raw URL path if the SDK couldn't resolve one (`/users/123`) | The parameterized route, or `Pageload` if the SDK has none |
487+
| `graphql` | The graphql phase and, for operations, the operation name (`query GetUser`, `graphql.parse`, `graphql.resolve user.0.name`) | The operation type, or the processing type where there is none (`GraphQL query`, `GraphQL parse`, `GraphQL resolve`) |
488+
| `gen_ai.chat`, `gen_ai.embeddings`, `gen_ai.generate_content`, … | `{operation} {model}`, or `{operation} unknown` if the model is missing (`chat unknown`) | `{operation} {model}`, or `{operation}` if the model is missing (`chat`). Instrumented methods always have an operation, so the convention fallback `Generative AI model operation` is unused today. |
489+
| `gen_ai.execute_tool` | `execute_tool {tool name}` (`execute_tool getWeather`) | `execute_tool`; the tool name stays on `gen_ai.tool.name` |
490+
491+
Resolved low-cardinality values are kept in both lifecycles: a known model stays in the name (`chat gpt-4`).
492+
493+
`ignoreSpans` is evaluated at span start. Filters matching `chat unknown` no longer apply to a streamed chat span named `'chat'`; match on `gen_ai.request.model` instead. Filters matching `execute_tool getWeather` no longer apply to a streamed tool span named `'execute_tool'`; match on `gen_ai.tool.name` instead.
494+
478495
### The `enableLogs` option was removed
479496
480497
Affected SDKs: All SDKs.

packages/server-utils/src/ai/anthropic-ai/index.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
/* eslint-disable typescript-eslint/no-deprecated */
22
import {
33
captureException,
4+
GEN_AI_INFERENCE_SPAN_NAME_FALLBACK,
5+
getClient,
6+
hasSpanStreamingEnabled,
47
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
58
SPAN_STATUS_ERROR,
69
startSpan,
@@ -189,8 +192,15 @@ function handleStreamingRequest<T extends unknown[], R>(
189192
isStreamingMethod: boolean,
190193
): R | Promise<R> {
191194
const model = requestAttributes[GEN_AI_REQUEST_MODEL] ?? 'unknown';
195+
const client = getClient();
192196
const spanConfig = {
193-
name: `${operationName} ${model}`,
197+
// With span streaming, omit the `'unknown'` model sentinel so the name stays low-cardinality.
198+
name:
199+
(typeof model === 'string' && model !== 'unknown') || !(client && hasSpanStreamingEnabled(client))
200+
? `${operationName} ${model}`
201+
: operationName !== 'unknown'
202+
? operationName
203+
: GEN_AI_INFERENCE_SPAN_NAME_FALLBACK,
194204
op: getGenAiSpanOp(operationName),
195205
attributes: requestAttributes as Record<string, SpanAttributeValue>,
196206
};
@@ -273,6 +283,7 @@ function instrumentMethod<T extends unknown[], R>(
273283
const operationName = instrumentedMethod.operation || 'unknown';
274284
const requestAttributes = extractRequestAttributes(args, operationName);
275285
const model = requestAttributes[GEN_AI_REQUEST_MODEL] ?? 'unknown';
286+
const client = getClient();
276287

277288
const params = typeof args[0] === 'object' ? (args[0] as Record<string, unknown>) : undefined;
278289
const isStreamRequested = Boolean(params?.stream);
@@ -296,7 +307,13 @@ function instrumentMethod<T extends unknown[], R>(
296307

297308
const instrumentedPromise = startSpan(
298309
{
299-
name: `${operationName} ${model}`,
310+
// With span streaming, omit the `'unknown'` model sentinel so the name stays low-cardinality.
311+
name:
312+
(typeof model === 'string' && model !== 'unknown') || !(client && hasSpanStreamingEnabled(client))
313+
? `${operationName} ${model}`
314+
: operationName !== 'unknown'
315+
? operationName
316+
: GEN_AI_INFERENCE_SPAN_NAME_FALLBACK,
300317
op: getGenAiSpanOp(operationName),
301318
attributes: requestAttributes as Record<string, SpanAttributeValue>,
302319
},

packages/server-utils/src/ai/google-genai/index.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@
22
/* eslint-disable max-lines */
33
import {
44
captureException,
5+
GEN_AI_INFERENCE_SPAN_NAME_FALLBACK,
6+
getClient,
7+
handleCallbackErrors,
8+
hasSpanStreamingEnabled,
59
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
610
SPAN_STATUS_ERROR,
711
startSpan,
812
startSpanManual,
9-
handleCallbackErrors,
1013
stringify,
1114
} from '@sentry/core';
1215
import type { Span, SpanAttributeValue } from '@sentry/core';
@@ -270,13 +273,21 @@ function instrumentMethod<T extends unknown[], R>(
270273
const params = args[0] as Record<string, unknown> | undefined;
271274
const requestAttributes = extractRequestAttributes(operationName, params, context);
272275
const model = requestAttributes[GEN_AI_REQUEST_MODEL] ?? 'unknown';
276+
const client = getClient();
277+
// With span streaming, omit the `'unknown'` model sentinel so the name stays low-cardinality.
278+
const spanName =
279+
(typeof model === 'string' && model !== 'unknown') || !(client && hasSpanStreamingEnabled(client))
280+
? `${operationName} ${model}`
281+
: operationName !== 'unknown'
282+
? operationName
283+
: GEN_AI_INFERENCE_SPAN_NAME_FALLBACK;
273284

274285
// Check if this is a streaming method
275286
if (instrumentedMethod.streaming) {
276287
// Use startSpanManual for streaming methods to control span lifecycle
277288
return startSpanManual(
278289
{
279-
name: `${operationName} ${model}`,
290+
name: spanName,
280291
op: getGenAiSpanOp(operationName),
281292
attributes: requestAttributes,
282293
},
@@ -305,7 +316,7 @@ function instrumentMethod<T extends unknown[], R>(
305316
// Single span for both sync and async operations
306317
return startSpan(
307318
{
308-
name: `${operationName} ${model}`,
319+
name: spanName,
309320
op: getGenAiSpanOp(operationName),
310321
attributes: requestAttributes,
311322
},

packages/server-utils/src/ai/langchain/embeddings.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import {
22
captureException,
3+
getClient,
4+
hasSpanStreamingEnabled,
35
SEMANTIC_ATTRIBUTE_SENTRY_OP,
46
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
57
startSpan,
@@ -75,13 +77,18 @@ export function _INTERNAL_getLangChainEmbeddingsSpanOptions(
7577
const { recordInputs } = resolveAIRecordingOptions(options);
7678
const attributes = extractEmbeddingAttributes(instance);
7779
const modelName = attributes[GEN_AI_REQUEST_MODEL] || 'unknown';
80+
const client = getClient();
7881

7982
if (recordInputs && input != null) {
8083
attributes[GEN_AI_EMBEDDINGS_INPUT] = stringify(input, String);
8184
}
8285

8386
return {
84-
name: `embeddings ${modelName}`,
87+
// With span streaming, omit the `'unknown'` model sentinel so the name stays low-cardinality.
88+
name:
89+
(typeof modelName === 'string' && modelName !== 'unknown') || !(client && hasSpanStreamingEnabled(client))
90+
? `embeddings ${modelName}`
91+
: 'embeddings',
8592
op: GEN_AI_EMBEDDINGS_OPERATION_ATTRIBUTE,
8693
attributes: attributes as Record<string, SpanAttributeValue>,
8794
};

packages/server-utils/src/ai/langchain/index.ts

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
/* eslint-disable max-lines */
22
import {
33
captureException,
4+
GEN_AI_INFERENCE_SPAN_NAME_FALLBACK,
5+
getClient,
6+
hasSpanStreamingEnabled,
47
SEMANTIC_ATTRIBUTE_SENTRY_OP,
58
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
69
SPAN_STATUS_ERROR,
@@ -101,11 +104,19 @@ export function createLangChainCallbackHandler(options: LangChainOptions = {}):
101104
metadata,
102105
);
103106
const modelName = attributes[GEN_AI_REQUEST_MODEL];
104-
const operationName = attributes[GEN_AI_OPERATION_NAME];
107+
const operationName =
108+
typeof attributes[GEN_AI_OPERATION_NAME] === 'string' ? attributes[GEN_AI_OPERATION_NAME] : 'unknown';
109+
const client = getClient();
105110

106111
startSpanManual(
107112
{
108-
name: `${operationName} ${modelName}`,
113+
// With span streaming, omit the `'unknown'` model sentinel so the name stays low-cardinality.
114+
name:
115+
(typeof modelName === 'string' && modelName !== 'unknown') || !(client && hasSpanStreamingEnabled(client))
116+
? `${operationName} ${modelName}`
117+
: operationName !== 'unknown'
118+
? operationName
119+
: GEN_AI_INFERENCE_SPAN_NAME_FALLBACK,
109120
op: 'gen_ai.chat',
110121
attributes: {
111122
...getAgentNameFromMetadata(metadata),
@@ -146,11 +157,19 @@ export function createLangChainCallbackHandler(options: LangChainOptions = {}):
146157
}
147158

148159
const modelName = attributes[GEN_AI_REQUEST_MODEL];
149-
const operationName = attributes[GEN_AI_OPERATION_NAME];
160+
const operationName =
161+
typeof attributes[GEN_AI_OPERATION_NAME] === 'string' ? attributes[GEN_AI_OPERATION_NAME] : 'unknown';
162+
const client = getClient();
150163

151164
startSpanManual(
152165
{
153-
name: `${operationName} ${modelName}`,
166+
// With span streaming, omit the `'unknown'` model sentinel so the name stays low-cardinality.
167+
name:
168+
(typeof modelName === 'string' && modelName !== 'unknown') || !(client && hasSpanStreamingEnabled(client))
169+
? `${operationName} ${modelName}`
170+
: operationName !== 'unknown'
171+
? operationName
172+
: GEN_AI_INFERENCE_SPAN_NAME_FALLBACK,
154173
op: 'gen_ai.chat',
155174
attributes: {
156175
...getAgentNameFromMetadata(metadata),
@@ -302,9 +321,13 @@ export function createLangChainCallbackHandler(options: LangChainOptions = {}):
302321
attributes[GEN_AI_TOOL_CALL_ARGUMENTS] = input;
303322
}
304323

324+
const client = getClient();
325+
305326
startSpanManual(
306327
{
307-
name: `execute_tool ${toolName}`,
328+
// With span streaming, the name follows the `{operation}` inference template. The tool
329+
// name stays available on `gen_ai.tool.name`.
330+
name: client && hasSpanStreamingEnabled(client) ? 'execute_tool' : `execute_tool ${toolName}`,
308331
op: 'gen_ai.execute_tool',
309332
attributes: {
310333
...attributes,

packages/server-utils/src/ai/langgraph/utils.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
/* eslint-disable typescript-eslint/no-deprecated */
22
import {
33
captureException,
4+
getClient,
5+
hasSpanStreamingEnabled,
46
SEMANTIC_ATTRIBUTE_SENTRY_OP,
57
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
68
SPAN_STATUS_ERROR,
@@ -114,10 +116,14 @@ export function wrapToolsWithSpans(tools: unknown[], options: LangGraphOptions,
114116
}
115117
}
116118

119+
const client = getClient();
120+
117121
return startSpan(
118122
{
119123
op: GEN_AI_EXECUTE_TOOL_OPERATION_ATTRIBUTE,
120-
name: `execute_tool ${toolName}`,
124+
// With span streaming, the name follows the `{operation}` inference template. The tool
125+
// name stays available on `gen_ai.tool.name`.
126+
name: client && hasSpanStreamingEnabled(client) ? 'execute_tool' : `execute_tool ${toolName}`,
121127
attributes: spanAttributes,
122128
},
123129
async span => {

0 commit comments

Comments
 (0)