Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ describe('Vercel AI integration (streaming v4)', () => {
// Sixth span - execute_tool
// Note: gen_ai.tool.description is NOT present when genAI recording disabled because ai.prompt.tools is not recorded
expect.objectContaining({
name: 'execute_tool getWeather',
name: 'execute_tool',
status: 'ok',
attributes: expect.objectContaining({
[GEN_AI_TOOL_CALL_ID_ATTRIBUTE]: attr('call-1'),
Expand Down Expand Up @@ -222,7 +222,7 @@ describe('Vercel AI integration (streaming v4)', () => {
}),
// Sixth span - execute_tool with description and input/output
expect.objectContaining({
name: 'execute_tool getWeather',
name: 'execute_tool',
status: 'ok',
attributes: expect.objectContaining({
[GEN_AI_TOOL_CALL_ID_ATTRIBUTE]: attr('call-1'),
Expand Down Expand Up @@ -263,7 +263,7 @@ describe('Vercel AI integration (streaming v4)', () => {
}),
}),
expect.objectContaining({
name: 'execute_tool getWeather',
name: 'execute_tool',
status: 'error',
attributes: expect.objectContaining({
[GEN_AI_TOOL_CALL_ID_ATTRIBUTE]: attr('call-1'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ describe('Vercel AI integration (streaming, v6)', () => {
// Sixth span - execute_tool
// Note: gen_ai.tool.description is NOT present when genAI recording disabled because ai.prompt.tools is not recorded
expect.objectContaining({
name: 'execute_tool getWeather',
name: 'execute_tool',
status: 'ok',
attributes: expect.objectContaining({
[GEN_AI_TOOL_CALL_ID_ATTRIBUTE]: attr('call-1'),
Expand Down Expand Up @@ -219,7 +219,7 @@ describe('Vercel AI integration (streaming, v6)', () => {
}),
// Sixth span - execute_tool with description and input/output
expect.objectContaining({
name: 'execute_tool getWeather',
name: 'execute_tool',
status: 'ok',
attributes: expect.objectContaining({
[GEN_AI_TOOL_CALL_ID_ATTRIBUTE]: attr('call-1'),
Expand Down Expand Up @@ -259,7 +259,7 @@ describe('Vercel AI integration (streaming, v6)', () => {
}),
}),
expect.objectContaining({
name: 'execute_tool getWeather',
name: 'execute_tool',
status: 'error',
attributes: expect.objectContaining({
[GEN_AI_TOOL_CALL_ID_ATTRIBUTE]: attr('call-1'),
Expand Down
17 changes: 17 additions & 0 deletions docs/migration/v11-end-state.md
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,23 @@ Sentry.init({

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.

### Span name changes

Affected SDKs: All SDKs.

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.

| Span op | Before | After |
| ---------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `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 |
| `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`) |
| `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. |
| `gen_ai.execute_tool` | `execute_tool {tool name}` (`execute_tool getWeather`) | `execute_tool`; the tool name stays on `gen_ai.tool.name` |

Resolved low-cardinality values are kept in both lifecycles: a known model stays in the name (`chat gpt-4`).

`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.

### The `enableLogs` option was removed

Affected SDKs: All SDKs.
Expand Down
21 changes: 19 additions & 2 deletions packages/server-utils/src/ai/anthropic-ai/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
/* eslint-disable typescript-eslint/no-deprecated */
import {
captureException,
GEN_AI_INFERENCE_SPAN_NAME_FALLBACK,
getClient,
hasSpanStreamingEnabled,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
SPAN_STATUS_ERROR,
startSpan,
Expand Down Expand Up @@ -189,8 +192,15 @@ function handleStreamingRequest<T extends unknown[], R>(
isStreamingMethod: boolean,
): R | Promise<R> {
const model = requestAttributes[GEN_AI_REQUEST_MODEL] ?? 'unknown';

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible for this to be an empty string? If so, we could have a span name like 'chat ' (with a trailing space). I think if this is || instead, it dodges the issue.

Suggested change
const model = requestAttributes[GEN_AI_REQUEST_MODEL] ?? 'unknown';
const model = requestAttributes[GEN_AI_REQUEST_MODEL] || 'unknown';

const client = getClient();
const spanConfig = {
name: `${operationName} ${model}`,
// With span streaming, omit the `'unknown'` model sentinel so the name stays low-cardinality.
name:
(typeof model === 'string' && model !== 'unknown') || !(client && hasSpanStreamingEnabled(client))
? `${operationName} ${model}`
: operationName !== 'unknown'
? operationName
: GEN_AI_INFERENCE_SPAN_NAME_FALLBACK,
Comment on lines +197 to +203

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

low: This nearly-identical ternary is repeated quite a lot in this patch (eg, this file again on line 310, packages/server-utils/src/ai/google-genai/index.ts line 277, etc). Seems like a good opportunity for factoring out. That could reduce bundle size a bit, but more importantly, would make the code more readable and unlikely to drift.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, I think the operationName !== 'unknown' ? operationName : GEN_AI_INFERENCE_SPAN_NAME_FALLBACK branch is unreachable. Every entry in packages/server-utils/src/ai/anthropic-ai/constants.ts has an operation name, so the || 'unknown' never gets executed. Probably we could tighten up the types and make the fallback unnecessary. (Doesn't have to be in this PR, but could be something to throw a clanker at, see if it can let typescript inference prove that the fallback is unnecessary.)

op: getGenAiSpanOp(operationName),
attributes: requestAttributes as Record<string, SpanAttributeValue>,
};
Expand Down Expand Up @@ -273,6 +283,7 @@ function instrumentMethod<T extends unknown[], R>(
const operationName = instrumentedMethod.operation || 'unknown';
const requestAttributes = extractRequestAttributes(args, operationName);
const model = requestAttributes[GEN_AI_REQUEST_MODEL] ?? 'unknown';
const client = getClient();

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

const instrumentedPromise = startSpan(
{

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This object creation is identical to the one on line 196, so we could probably use a helper function to do both in one place.

name: `${operationName} ${model}`,
// With span streaming, omit the `'unknown'` model sentinel so the name stays low-cardinality.
name:
(typeof model === 'string' && model !== 'unknown') || !(client && hasSpanStreamingEnabled(client))
? `${operationName} ${model}`
: operationName !== 'unknown'
? operationName
: GEN_AI_INFERENCE_SPAN_NAME_FALLBACK,
op: getGenAiSpanOp(operationName),
attributes: requestAttributes as Record<string, SpanAttributeValue>,
},
Expand Down
17 changes: 14 additions & 3 deletions packages/server-utils/src/ai/google-genai/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@
/* eslint-disable max-lines */
import {
captureException,
GEN_AI_INFERENCE_SPAN_NAME_FALLBACK,
getClient,
handleCallbackErrors,
hasSpanStreamingEnabled,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
SPAN_STATUS_ERROR,
startSpan,
startSpanManual,
handleCallbackErrors,
stringify,
} from '@sentry/core';
import type { Span, SpanAttributeValue } from '@sentry/core';
Expand Down Expand Up @@ -270,13 +273,21 @@ function instrumentMethod<T extends unknown[], R>(
const params = args[0] as Record<string, unknown> | undefined;
const requestAttributes = extractRequestAttributes(operationName, params, context);
const model = requestAttributes[GEN_AI_REQUEST_MODEL] ?? 'unknown';
const client = getClient();
// With span streaming, omit the `'unknown'` model sentinel so the name stays low-cardinality.
const spanName =
(typeof model === 'string' && model !== 'unknown') || !(client && hasSpanStreamingEnabled(client))
? `${operationName} ${model}`
: operationName !== 'unknown'
? operationName
: GEN_AI_INFERENCE_SPAN_NAME_FALLBACK;

// Check if this is a streaming method
if (instrumentedMethod.streaming) {
// Use startSpanManual for streaming methods to control span lifecycle
return startSpanManual(
{
name: `${operationName} ${model}`,
name: spanName,
op: getGenAiSpanOp(operationName),
attributes: requestAttributes,
},
Expand Down Expand Up @@ -305,7 +316,7 @@ function instrumentMethod<T extends unknown[], R>(
// Single span for both sync and async operations
return startSpan(
{
name: `${operationName} ${model}`,
name: spanName,
op: getGenAiSpanOp(operationName),
attributes: requestAttributes,
},
Expand Down
9 changes: 8 additions & 1 deletion packages/server-utils/src/ai/langchain/embeddings.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import {
captureException,
getClient,
hasSpanStreamingEnabled,
SEMANTIC_ATTRIBUTE_SENTRY_OP,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
startSpan,
Expand Down Expand Up @@ -75,13 +77,18 @@ export function _INTERNAL_getLangChainEmbeddingsSpanOptions(
const { recordInputs } = resolveAIRecordingOptions(options);
const attributes = extractEmbeddingAttributes(instance);
const modelName = attributes[GEN_AI_REQUEST_MODEL] || 'unknown';
const client = getClient();

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

return {
name: `embeddings ${modelName}`,
// With span streaming, omit the `'unknown'` model sentinel so the name stays low-cardinality.
name:
(typeof modelName === 'string' && modelName !== 'unknown') || !(client && hasSpanStreamingEnabled(client))
? `embeddings ${modelName}`
: 'embeddings',
op: GEN_AI_EMBEDDINGS_OPERATION_ATTRIBUTE,
attributes: attributes as Record<string, SpanAttributeValue>,
};
Expand Down
33 changes: 28 additions & 5 deletions packages/server-utils/src/ai/langchain/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
/* eslint-disable max-lines */
import {
captureException,
GEN_AI_INFERENCE_SPAN_NAME_FALLBACK,
getClient,
hasSpanStreamingEnabled,
SEMANTIC_ATTRIBUTE_SENTRY_OP,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
SPAN_STATUS_ERROR,
Expand Down Expand Up @@ -101,11 +104,19 @@ export function createLangChainCallbackHandler(options: LangChainOptions = {}):
metadata,
);
const modelName = attributes[GEN_AI_REQUEST_MODEL];
const operationName = attributes[GEN_AI_OPERATION_NAME];
const operationName =
typeof attributes[GEN_AI_OPERATION_NAME] === 'string' ? attributes[GEN_AI_OPERATION_NAME] : 'unknown';
const client = getClient();

startSpanManual(
{
name: `${operationName} ${modelName}`,
// With span streaming, omit the `'unknown'` model sentinel so the name stays low-cardinality.
name:
(typeof modelName === 'string' && modelName !== 'unknown') || !(client && hasSpanStreamingEnabled(client))
? `${operationName} ${modelName}`
: operationName !== 'unknown'
? operationName
: GEN_AI_INFERENCE_SPAN_NAME_FALLBACK,
op: 'gen_ai.chat',
attributes: {
...getAgentNameFromMetadata(metadata),
Expand Down Expand Up @@ -146,11 +157,19 @@ export function createLangChainCallbackHandler(options: LangChainOptions = {}):
}

const modelName = attributes[GEN_AI_REQUEST_MODEL];
const operationName = attributes[GEN_AI_OPERATION_NAME];
const operationName =
typeof attributes[GEN_AI_OPERATION_NAME] === 'string' ? attributes[GEN_AI_OPERATION_NAME] : 'unknown';
const client = getClient();

startSpanManual(
{
name: `${operationName} ${modelName}`,
// With span streaming, omit the `'unknown'` model sentinel so the name stays low-cardinality.
name:
(typeof modelName === 'string' && modelName !== 'unknown') || !(client && hasSpanStreamingEnabled(client))
? `${operationName} ${modelName}`
: operationName !== 'unknown'
? operationName
: GEN_AI_INFERENCE_SPAN_NAME_FALLBACK,
op: 'gen_ai.chat',
attributes: {
...getAgentNameFromMetadata(metadata),
Expand Down Expand Up @@ -302,9 +321,13 @@ export function createLangChainCallbackHandler(options: LangChainOptions = {}):
attributes[GEN_AI_TOOL_CALL_ARGUMENTS] = input;
}

const client = getClient();

startSpanManual(
{
name: `execute_tool ${toolName}`,
// With span streaming, the name follows the `{operation}` inference template. The tool
// name stays available on `gen_ai.tool.name`.
name: client && hasSpanStreamingEnabled(client) ? 'execute_tool' : `execute_tool ${toolName}`,
op: 'gen_ai.execute_tool',
attributes: {
...attributes,
Expand Down
8 changes: 7 additions & 1 deletion packages/server-utils/src/ai/langgraph/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/* eslint-disable typescript-eslint/no-deprecated */
import {
captureException,
getClient,
hasSpanStreamingEnabled,
SEMANTIC_ATTRIBUTE_SENTRY_OP,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
SPAN_STATUS_ERROR,
Expand Down Expand Up @@ -114,10 +116,14 @@ export function wrapToolsWithSpans(tools: unknown[], options: LangGraphOptions,
}
}

const client = getClient();

return startSpan(
{
op: GEN_AI_EXECUTE_TOOL_OPERATION_ATTRIBUTE,
name: `execute_tool ${toolName}`,
// With span streaming, the name follows the `{operation}` inference template. The tool
// name stays available on `gen_ai.tool.name`.
name: client && hasSpanStreamingEnabled(client) ? 'execute_tool' : `execute_tool ${toolName}`,
attributes: spanAttributes,
},
async span => {
Expand Down
12 changes: 11 additions & 1 deletion packages/server-utils/src/ai/openai/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
import { DEBUG_BUILD } from '../../debug-build';
import {
captureException,
GEN_AI_INFERENCE_SPAN_NAME_FALLBACK,
getClient,
hasSpanStreamingEnabled,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
SPAN_STATUS_ERROR,
startSpan,
Expand Down Expand Up @@ -144,9 +147,16 @@ function instrumentMethod<T extends unknown[], R>(

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

const spanConfig = {
name: `${operationName} ${model}`,
// With span streaming, omit the `'unknown'` model sentinel so the name stays low-cardinality.
name:
(typeof model === 'string' && model !== 'unknown') || !(client && hasSpanStreamingEnabled(client))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the typeof model === 'string' doing any work here? It seems like it's guarded by line 146 above, right?

? `${operationName} ${model}`
: operationName !== 'unknown'
? operationName
: GEN_AI_INFERENCE_SPAN_NAME_FALLBACK,
op: getGenAiSpanOp(operationName),
attributes: requestAttributes as Record<string, SpanAttributeValue>,
};
Expand Down
12 changes: 11 additions & 1 deletion packages/server-utils/src/ai/workers-ai/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import {
_INTERNAL_shouldSkipAiProviderWrapping,
GEN_AI_INFERENCE_SPAN_NAME_FALLBACK,
getClient,
hasSpanStreamingEnabled,
isObjectLike,
SPAN_STATUS_ERROR,
startSpan,
Expand Down Expand Up @@ -43,6 +46,7 @@ function instrumentRun(
const operationName = getOperationName(inputs);
const requestAttributes = extractRequestAttributes(model, inputs, operationName);
const modelName = typeof model === 'string' ? model : 'unknown';
const client = getClient();

const isStreamRequested =
!!inputs && typeof inputs === 'object' && (inputs as { stream?: unknown }).stream === true;
Expand All @@ -52,7 +56,13 @@ function instrumentRun(
(runOptions.returnRawResponse === true || runOptions.websocket === true);

const spanConfig = {
name: `${operationName} ${modelName}`,
// With span streaming, omit the `'unknown'` model sentinel so the name stays low-cardinality.
name:
(typeof modelName === 'string' && modelName !== 'unknown') || !(client && hasSpanStreamingEnabled(client))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, modelName must be a string at this point, so the typeof seems unnecessary.

? `${operationName} ${modelName}`
: operationName !== 'unknown'
? operationName
: GEN_AI_INFERENCE_SPAN_NAME_FALLBACK,
op: `gen_ai.${operationName}`,
attributes: requestAttributes,
};
Expand Down
12 changes: 11 additions & 1 deletion packages/server-utils/src/integrations/anthropic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import type { IntegrationFn, Span, SpanAttributeValue } from '@sentry/core';
import {
_INTERNAL_shouldSkipAiProviderWrapping,
defineIntegration,
GEN_AI_INFERENCE_SPAN_NAME_FALLBACK,
getClient,
hasSpanStreamingEnabled,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
startInactiveSpan,
} from '@sentry/core';
Expand Down Expand Up @@ -99,9 +102,16 @@ function createGenAiSpan(
const attributes = extractRequestAttributes(args, operation);
const model = (attributes[GEN_AI_REQUEST_MODEL] as string) || 'unknown';
attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] = ORIGIN;
const client = getClient();

const span = startInactiveSpan({
name: `${operation} ${model}`,
// With span streaming, omit the `'unknown'` model sentinel so the name stays low-cardinality.
name:
(typeof model === 'string' && model !== 'unknown') || !(client && hasSpanStreamingEnabled(client))
? `${operation} ${model}`
: operation !== 'unknown'
? operation
: GEN_AI_INFERENCE_SPAN_NAME_FALLBACK,
op: getGenAiSpanOp(operation),
attributes: attributes as Record<string, SpanAttributeValue>,
});
Expand Down
Loading
Loading