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
7 changes: 6 additions & 1 deletion MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -778,7 +778,7 @@ The LangGraph instrumentation no longer emits `gen_ai.create_agent` spans when a

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/).
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.

Expand All @@ -789,9 +789,13 @@ The following span names were adjusted:
| `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 |
| `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 |
| `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`) |
| `gen_ai.execute_tool` | `execute_tool {tool name}` (`execute_tool getWeather`) | `execute_tool`; the tool name stays on `gen_ai.tool.name` |
| `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 |
| `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 |

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

Resource spans now also carry a `url.domain` attribute holding that domain. The full URL remains available on `url.full`.

Some consequences to be aware of:
Expand All @@ -809,6 +813,7 @@ Child spans of a service or root span carry its name in their `sentry.segment.na
`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.
`ignoreSpans` filters matching a URL path no longer apply to them.
Another example where filters might need adjustments are `resource.*` spans where their name now only includes the domain the resource was taken from.
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'`).

Match on attributes instead:

Expand Down
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
67 changes: 33 additions & 34 deletions packages/server-utils/src/ai/anthropic-ai/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
/* eslint-disable typescript-eslint/no-deprecated */
import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, startSpan, startSpanManual } from '@sentry/core';
import {
getClient,
hasSpanStreamingEnabled,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
SPAN_STATUS_ERROR,
startSpan,
startSpanManual,
} from '@sentry/core';
import type { Span, SpanAttributeValue } from '@sentry/core';
import {
GEN_AI_OPERATION_NAME,
Expand Down Expand Up @@ -170,21 +177,12 @@ function handleStreamingRequest<T extends unknown[], R>(
target: (...args: T) => R | Promise<R>,
invocationThis: unknown,
args: T,
requestAttributes: Record<string, unknown>,
operationName: string,
methodPath: string,
spanConfig: { name: string; op: string; attributes: Record<string, SpanAttributeValue> },
params: Record<string, unknown> | undefined,
options: AnthropicAiOptions,
isStreamRequested: boolean,
isStreamingMethod: boolean,
): R | Promise<R> {
const model = requestAttributes[GEN_AI_REQUEST_MODEL] ?? 'unknown';
const spanConfig = {
name: `${operationName} ${model}`,
op: getGenAiSpanOp(operationName),
attributes: requestAttributes as Record<string, SpanAttributeValue>,
};

// messages.stream() always returns a sync MessageStream, even with stream: true param
if (isStreamRequested && !isStreamingMethod) {
let originalResult!: Promise<R>;
Expand Down Expand Up @@ -262,7 +260,17 @@ 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 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 spanConfig = {
name:
(typeof model === 'string' && model !== 'unknown') || !(client && hasSpanStreamingEnabled(client))
? `${operationName} ${model}`
: operationName,
op: getGenAiSpanOp(operationName),
attributes: requestAttributes as Record<string, SpanAttributeValue>,
};

const params = typeof args[0] === 'object' ? (args[0] as Record<string, unknown>) : undefined;
const isStreamRequested = Boolean(params?.stream);
Expand All @@ -272,9 +280,7 @@ function instrumentMethod<T extends unknown[], R>(
target,
invocationThis,
args,
requestAttributes,
operationName,
methodPath,
spanConfig,
params,
options,
isStreamRequested,
Expand All @@ -284,25 +290,18 @@ function instrumentMethod<T extends unknown[], R>(

let originalResult!: Promise<R>;

const instrumentedPromise = startSpan(
{
name: `${operationName} ${model}`,
op: getGenAiSpanOp(operationName),
attributes: requestAttributes as Record<string, SpanAttributeValue>,
},
span => {
originalResult = target.apply(invocationThis, args) as Promise<R>;

if (options.recordInputs && params) {
addPrivateRequestAttributes(span, params);
}

return originalResult.then(result => {
addResponseAttributes(span, result as AnthropicAiResponse, options.recordOutputs);
return result;
});
},
);
const instrumentedPromise = startSpan(spanConfig, span => {
originalResult = target.apply(invocationThis, args) as Promise<R>;

if (options.recordInputs && params) {
addPrivateRequestAttributes(span, params);
}

return originalResult.then(result => {
addResponseAttributes(span, result as AnthropicAiResponse, options.recordOutputs);
return result;
});
});

return wrapPromiseWithMethods(originalResult, instrumentedPromise);
},
Expand Down
16 changes: 12 additions & 4 deletions packages/server-utils/src/ai/google-genai/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
/* eslint-disable typescript-eslint/no-deprecated */
/* eslint-disable max-lines */
import {
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 @@ -268,14 +270,20 @@ function instrumentMethod<T extends unknown[], R>(
const operationName = instrumentedMethod.operation || 'unknown';
const params = args[0] as Record<string, unknown> | undefined;
const requestAttributes = extractRequestAttributes(operationName, params, context);
const model = requestAttributes[GEN_AI_REQUEST_MODEL] ?? 'unknown';
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;

// 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 All @@ -297,7 +305,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
16 changes: 14 additions & 2 deletions packages/server-utils/src/ai/langchain/embeddings.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startSpan, stringify } from '@sentry/core';
import {
getClient,
hasSpanStreamingEnabled,
SEMANTIC_ATTRIBUTE_SENTRY_OP,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
startSpan,
stringify,
} from '@sentry/core';
import type { SpanAttributeValue } from '@sentry/core';
import {
GEN_AI_EMBEDDINGS_INPUT,
Expand Down Expand Up @@ -69,13 +76,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
32 changes: 25 additions & 7 deletions packages/server-utils/src/ai/langchain/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
/* eslint-disable max-lines */
import {
getClient,
hasSpanStreamingEnabled,
SEMANTIC_ATTRIBUTE_SENTRY_OP,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
SPAN_STATUS_ERROR,
Expand Down Expand Up @@ -99,12 +101,18 @@ export function createLangChainCallbackHandler(options: LangChainOptions = {}):
invocationParams,
metadata,
);
const modelName = attributes[GEN_AI_REQUEST_MODEL];
const operationName = attributes[GEN_AI_OPERATION_NAME];
const modelName = attributes[GEN_AI_REQUEST_MODEL] || 'unknown';
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,
Comment thread
cursor[bot] marked this conversation as resolved.
op: 'gen_ai.chat',
attributes: {
...getAgentNameFromMetadata(metadata),
Expand Down Expand Up @@ -144,12 +152,18 @@ export function createLangChainCallbackHandler(options: LangChainOptions = {}):
attributes[GEN_AI_TOOL_DEFINITIONS] = toolDefsJson;
}

const modelName = attributes[GEN_AI_REQUEST_MODEL];
const operationName = attributes[GEN_AI_OPERATION_NAME];
const modelName = attributes[GEN_AI_REQUEST_MODEL] || 'unknown';
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,
op: 'gen_ai.chat',
attributes: {
...getAgentNameFromMetadata(metadata),
Expand Down Expand Up @@ -291,9 +305,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,5 +1,7 @@
/* eslint-disable typescript-eslint/no-deprecated */
import {
getClient,
hasSpanStreamingEnabled,
SEMANTIC_ATTRIBUTE_SENTRY_OP,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
SPAN_STATUS_ERROR,
Expand Down Expand Up @@ -113,10 +115,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
9 changes: 8 additions & 1 deletion packages/server-utils/src/ai/openai/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/* eslint-disable typescript-eslint/no-deprecated */
import { DEBUG_BUILD } from '../../debug-build';
import {
getClient,
hasSpanStreamingEnabled,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
SPAN_STATUS_ERROR,
startSpan,
Expand Down Expand Up @@ -143,9 +145,14 @@ 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:
model !== 'unknown' || !(client && hasSpanStreamingEnabled(client))
? `${operationName} ${model}`
: operationName,
op: getGenAiSpanOp(operationName),
attributes: requestAttributes as Record<string, SpanAttributeValue>,
};
Expand Down
Loading
Loading