Skip to content

Commit 41dfc75

Browse files
RulaKhaledcursoragentclaude
committed
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}` when the model is missing or empty. `gen_ai.execute_tool` drops the tool name from the span name, which stays on `gen_ai.tool.name`. Static lifecycle names are unchanged. Every instrumented method supplies an operation, so the convention fallback `Generative AI model operation` is unreachable and is not wired up at any site. Co-Authored-By: Cursor Grok 4.6 <cursoragent@cursor.com> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent c159f0b commit 41dfc75

19 files changed

Lines changed: 514 additions & 71 deletions

File tree

‎MIGRATION.md‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -778,7 +778,7 @@ The LangGraph instrumentation no longer emits `gen_ai.create_agent` spans when a
778778

779779
Affected SDKs: All SDKs.
780780

781-
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/).
781+
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/).
782782

783783
If you [opt out of span streaming](#opting-out-of-span-streaming), span names remain unchanged.
784784

@@ -789,9 +789,13 @@ The following span names were adjusted:
789789
| `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 |
790790
| `router` | Framework-specific, sometimes containing the raw URL (`/users/123`, `SvelteKit Route Change`) | The span's `http.route`, or `Router` if the SDK has none |
791791
| `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`) |
792+
| `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`) |
793+
| `gen_ai.execute_tool` | `execute_tool {tool name}` (`execute_tool getWeather`) | `execute_tool`; the tool name stays on `gen_ai.tool.name` |
792794
| `resource.*` | The resource URL, relative to the page origin for same-origin resources (`/assets/app.js`) | The resource domain (`cdn.example.com`), or `Resource` if the SDK has none |
793795
| `mcp.notification.client_to_server`, `mcp.notification.server_to_client` | The notification method name (`notifications/tools/list_changed`) | The notification method name, or `MCP notification` if the message carries none |
794796

797+
Resolved low-cardinality values are kept in both lifecycles: a known model stays in the name (`chat gpt-4`).
798+
795799
Resource spans now also carry a `url.domain` attribute holding that domain. The full URL remains available on `url.full`.
796800

797801
Some consequences to be aware of:
@@ -809,6 +813,7 @@ Child spans of a service or root span carry its name in their `sentry.segment.na
809813
`ignoreSpans` is evaluated when a span **starts**, at which point a span might not yet have its final name. For example, an unresolved pageload span name is named `'Pageload'` and might receive its final, resolved route name later.
810814
`ignoreSpans` filters matching a URL path no longer apply to them.
811815
Another example where filters might need adjustments are `resource.*` spans where their name now only includes the domain the resource was taken from.
816+
Likewise, 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'`).
812817

813818
Match on attributes instead:
814819

‎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'),

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

Lines changed: 40 additions & 44 deletions
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_ORIGIN,
57
SPAN_STATUS_ERROR,
68
startSpan,
@@ -180,21 +182,13 @@ function handleStreamingRequest<T extends unknown[], R>(
180182
target: (...args: T) => R | Promise<R>,
181183
invocationThis: unknown,
182184
args: T,
183-
requestAttributes: Record<string, unknown>,
184-
operationName: string,
185+
spanConfig: { name: string; op: string; attributes: Record<string, SpanAttributeValue> },
185186
methodPath: string,
186187
params: Record<string, unknown> | undefined,
187188
options: AnthropicAiOptions,
188189
isStreamRequested: boolean,
189190
isStreamingMethod: boolean,
190191
): R | Promise<R> {
191-
const model = requestAttributes[GEN_AI_REQUEST_MODEL] ?? 'unknown';
192-
const spanConfig = {
193-
name: `${operationName} ${model}`,
194-
op: getGenAiSpanOp(operationName),
195-
attributes: requestAttributes as Record<string, SpanAttributeValue>,
196-
};
197-
198192
// messages.stream() always returns a sync MessageStream, even with stream: true param
199193
if (isStreamRequested && !isStreamingMethod) {
200194
let originalResult!: Promise<R>;
@@ -272,7 +266,17 @@ function instrumentMethod<T extends unknown[], R>(
272266

273267
const operationName = instrumentedMethod.operation || 'unknown';
274268
const requestAttributes = extractRequestAttributes(args, operationName);
275-
const model = requestAttributes[GEN_AI_REQUEST_MODEL] ?? 'unknown';
269+
const model = requestAttributes[GEN_AI_REQUEST_MODEL] || 'unknown';
270+
const client = getClient();
271+
// With span streaming, omit the `'unknown'` model sentinel so the name stays low-cardinality.
272+
const spanConfig = {
273+
name:
274+
(typeof model === 'string' && model !== 'unknown') || !(client && hasSpanStreamingEnabled(client))
275+
? `${operationName} ${model}`
276+
: operationName,
277+
op: getGenAiSpanOp(operationName),
278+
attributes: requestAttributes as Record<string, SpanAttributeValue>,
279+
};
276280

277281
const params = typeof args[0] === 'object' ? (args[0] as Record<string, unknown>) : undefined;
278282
const isStreamRequested = Boolean(params?.stream);
@@ -282,8 +286,7 @@ function instrumentMethod<T extends unknown[], R>(
282286
target,
283287
invocationThis,
284288
args,
285-
requestAttributes,
286-
operationName,
289+
spanConfig,
287290
methodPath,
288291
params,
289292
options,
@@ -294,39 +297,32 @@ function instrumentMethod<T extends unknown[], R>(
294297

295298
let originalResult!: Promise<R>;
296299

297-
const instrumentedPromise = startSpan(
298-
{
299-
name: `${operationName} ${model}`,
300-
op: getGenAiSpanOp(operationName),
301-
attributes: requestAttributes as Record<string, SpanAttributeValue>,
302-
},
303-
span => {
304-
originalResult = target.apply(invocationThis, args) as Promise<R>;
305-
306-
if (options.recordInputs && params) {
307-
addPrivateRequestAttributes(span, params);
308-
}
309-
310-
return originalResult.then(
311-
result => {
312-
addResponseAttributes(span, result as AnthropicAiResponse, options.recordOutputs);
313-
return result;
314-
},
315-
error => {
316-
captureException(error, {
317-
mechanism: {
318-
handled: false,
319-
type: 'auto.ai.anthropic',
320-
data: {
321-
function: methodPath,
322-
},
300+
const instrumentedPromise = startSpan(spanConfig, span => {
301+
originalResult = target.apply(invocationThis, args) as Promise<R>;
302+
303+
if (options.recordInputs && params) {
304+
addPrivateRequestAttributes(span, params);
305+
}
306+
307+
return originalResult.then(
308+
result => {
309+
addResponseAttributes(span, result as AnthropicAiResponse, options.recordOutputs);
310+
return result;
311+
},
312+
error => {
313+
captureException(error, {
314+
mechanism: {
315+
handled: false,
316+
type: 'auto.ai.anthropic',
317+
data: {
318+
function: methodPath,
323319
},
324-
});
325-
throw error;
326-
},
327-
);
328-
},
329-
);
320+
},
321+
});
322+
throw error;
323+
},
324+
);
325+
});
330326

331327
return wrapPromiseWithMethods(originalResult, instrumentedPromise, 'auto.ai.anthropic');
332328
},

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

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22
/* eslint-disable max-lines */
33
import {
44
captureException,
5+
getClient,
6+
handleCallbackErrors,
7+
hasSpanStreamingEnabled,
58
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
69
SPAN_STATUS_ERROR,
710
startSpan,
811
startSpanManual,
9-
handleCallbackErrors,
1012
stringify,
1113
} from '@sentry/core';
1214
import type { Span, SpanAttributeValue } from '@sentry/core';
@@ -269,14 +271,20 @@ function instrumentMethod<T extends unknown[], R>(
269271
const operationName = instrumentedMethod.operation || 'unknown';
270272
const params = args[0] as Record<string, unknown> | undefined;
271273
const requestAttributes = extractRequestAttributes(operationName, params, context);
272-
const model = requestAttributes[GEN_AI_REQUEST_MODEL] ?? 'unknown';
274+
const model = requestAttributes[GEN_AI_REQUEST_MODEL] || 'unknown';
275+
const client = getClient();
276+
// With span streaming, omit the `'unknown'` model sentinel so the name stays low-cardinality.
277+
const spanName =
278+
(typeof model === 'string' && model !== 'unknown') || !(client && hasSpanStreamingEnabled(client))
279+
? `${operationName} ${model}`
280+
: operationName;
273281

274282
// Check if this is a streaming method
275283
if (instrumentedMethod.streaming) {
276284
// Use startSpanManual for streaming methods to control span lifecycle
277285
return startSpanManual(
278286
{
279-
name: `${operationName} ${model}`,
287+
name: spanName,
280288
op: getGenAiSpanOp(operationName),
281289
attributes: requestAttributes,
282290
},
@@ -305,7 +313,7 @@ function instrumentMethod<T extends unknown[], R>(
305313
// Single span for both sync and async operations
306314
return startSpan(
307315
{
308-
name: `${operationName} ${model}`,
316+
name: spanName,
309317
op: getGenAiSpanOp(operationName),
310318
attributes: requestAttributes,
311319
},

‎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: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
/* eslint-disable max-lines */
22
import {
33
captureException,
4+
getClient,
5+
hasSpanStreamingEnabled,
46
SEMANTIC_ATTRIBUTE_SENTRY_OP,
57
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
68
SPAN_STATUS_ERROR,
@@ -101,11 +103,17 @@ export function createLangChainCallbackHandler(options: LangChainOptions = {}):
101103
metadata,
102104
);
103105
const modelName = attributes[GEN_AI_REQUEST_MODEL];
104-
const operationName = attributes[GEN_AI_OPERATION_NAME];
106+
const operationName =
107+
typeof attributes[GEN_AI_OPERATION_NAME] === 'string' ? attributes[GEN_AI_OPERATION_NAME] : 'unknown';
108+
const client = getClient();
105109

106110
startSpanManual(
107111
{
108-
name: `${operationName} ${modelName}`,
112+
// With span streaming, omit the `'unknown'` model sentinel so the name stays low-cardinality.
113+
name:
114+
(typeof modelName === 'string' && modelName !== 'unknown') || !(client && hasSpanStreamingEnabled(client))
115+
? `${operationName} ${modelName}`
116+
: operationName,
109117
op: 'gen_ai.chat',
110118
attributes: {
111119
...getAgentNameFromMetadata(metadata),
@@ -146,11 +154,17 @@ export function createLangChainCallbackHandler(options: LangChainOptions = {}):
146154
}
147155

148156
const modelName = attributes[GEN_AI_REQUEST_MODEL];
149-
const operationName = attributes[GEN_AI_OPERATION_NAME];
157+
const operationName =
158+
typeof attributes[GEN_AI_OPERATION_NAME] === 'string' ? attributes[GEN_AI_OPERATION_NAME] : 'unknown';
159+
const client = getClient();
150160

151161
startSpanManual(
152162
{
153-
name: `${operationName} ${modelName}`,
163+
// With span streaming, omit the `'unknown'` model sentinel so the name stays low-cardinality.
164+
name:
165+
(typeof modelName === 'string' && modelName !== 'unknown') || !(client && hasSpanStreamingEnabled(client))
166+
? `${operationName} ${modelName}`
167+
: operationName,
154168
op: 'gen_ai.chat',
155169
attributes: {
156170
...getAgentNameFromMetadata(metadata),
@@ -302,9 +316,13 @@ export function createLangChainCallbackHandler(options: LangChainOptions = {}):
302316
attributes[GEN_AI_TOOL_CALL_ARGUMENTS] = input;
303317
}
304318

319+
const client = getClient();
320+
305321
startSpanManual(
306322
{
307-
name: `execute_tool ${toolName}`,
323+
// With span streaming, the name follows the `{operation}` inference template. The tool
324+
// name stays available on `gen_ai.tool.name`.
325+
name: client && hasSpanStreamingEnabled(client) ? 'execute_tool' : `execute_tool ${toolName}`,
308326
op: 'gen_ai.execute_tool',
309327
attributes: {
310328
...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 => {

‎packages/server-utils/src/ai/openai/index.ts‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
import { DEBUG_BUILD } from '../../debug-build';
33
import {
44
captureException,
5+
getClient,
6+
hasSpanStreamingEnabled,
57
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
68
SPAN_STATUS_ERROR,
79
startSpan,
@@ -144,9 +146,14 @@ function instrumentMethod<T extends unknown[], R>(
144146

145147
const params = args[0] as Record<string, unknown> | undefined;
146148
const isStreamRequested = params && typeof params === 'object' && params.stream === true;
149+
const client = getClient();
147150

148151
const spanConfig = {
149-
name: `${operationName} ${model}`,
152+
// With span streaming, omit the `'unknown'` model sentinel so the name stays low-cardinality.
153+
name:
154+
model !== 'unknown' || !(client && hasSpanStreamingEnabled(client))
155+
? `${operationName} ${model}`
156+
: operationName,
150157
op: getGenAiSpanOp(operationName),
151158
attributes: requestAttributes as Record<string, SpanAttributeValue>,
152159
};

0 commit comments

Comments
 (0)