Skip to content
Merged
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
6 changes: 5 additions & 1 deletion MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -823,7 +823,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 @@ -836,12 +836,15 @@ The following span names were adjusted:
| `http.server` | The request method and route, or the raw URL path if the SDK couldn't resolve one (`GET /users/123`) | `GET /users/:id` when a route is known, otherwise just the request method (`GET`) |
| `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`) |
| `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.server` | The method and its target, including the resource URI (`resources/read file:///docs/api.md`) | The method alone for resource methods (`resources/read`). Tool and prompt names are unchanged (`tools/call get-weather`) |
| `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 |

`navigation.redirect` spans are started through the same code path as navigation spans, so they get the same names.

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

`http.server` requests that resolve to a route are **unchanged** — those names were already low cardinality. Only requests the SDK cannot parameterize are affected.
Expand All @@ -863,6 +866,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 or navigation span is named `'Pageload'`/`'Navigation'` 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'`).

Match on attributes instead:

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
26 changes: 20 additions & 6 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
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
11 changes: 9 additions & 2 deletions packages/server-utils/src/ai/workers-ai/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import {
_INTERNAL_shouldSkipAiProviderWrapping,
getClient,
hasSpanStreamingEnabled,
isObjectLike,
SPAN_STATUS_ERROR,
startSpan,
Expand Down Expand Up @@ -42,7 +44,8 @@ function instrumentRun(

const operationName = getOperationName(inputs);
const requestAttributes = extractRequestAttributes(model, inputs, operationName);
const modelName = typeof model === 'string' ? model : 'unknown';
const modelName = typeof model === 'string' && model ? model : 'unknown';
const client = getClient();

const isStreamRequested =
!!inputs && typeof inputs === 'object' && (inputs as { stream?: unknown }).stream === true;
Expand All @@ -52,7 +55,11 @@ 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:
modelName !== 'unknown' || !(client && hasSpanStreamingEnabled(client))
? `${operationName} ${modelName}`
: operationName,
Comment thread
cursor[bot] marked this conversation as resolved.
op: `gen_ai.${operationName}`,
attributes: requestAttributes,
};
Expand Down
6 changes: 5 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,8 @@ import type { IntegrationFn, Span, SpanAttributeValue } from '@sentry/core';
import {
_INTERNAL_shouldSkipAiProviderWrapping,
defineIntegration,
getClient,
hasSpanStreamingEnabled,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
startInactiveSpan,
} from '@sentry/core';
Expand Down Expand Up @@ -99,9 +101,11 @@ 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: model !== 'unknown' || !(client && hasSpanStreamingEnabled(client)) ? `${operation} ${model}` : operation,
Comment thread
RulaKhaled marked this conversation as resolved.
op: getGenAiSpanOp(operation),
attributes: attributes as Record<string, SpanAttributeValue>,
});
Expand Down
6 changes: 5 additions & 1 deletion packages/server-utils/src/integrations/google-genai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {
_INTERNAL_shouldSkipAiProviderWrapping,
defineIntegration,
getActiveSpan,
getClient,
hasSpanStreamingEnabled,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
spanToJSON,
startInactiveSpan,
Expand Down Expand Up @@ -108,9 +110,11 @@ function createGenAiSpan(
const attributes = extractRequestAttributes(operation, params, data.self);
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: model !== 'unknown' || !(client && hasSpanStreamingEnabled(client)) ? `${operation} ${model}` : operation,
op: getGenAiSpanOp(operation),
attributes,
});
Expand Down
6 changes: 5 additions & 1 deletion packages/server-utils/src/integrations/openai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import type { IntegrationFn, Span, SpanAttributeValue } from '@sentry/core';
import {
_INTERNAL_shouldSkipAiProviderWrapping,
defineIntegration,
getClient,
hasSpanStreamingEnabled,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
startInactiveSpan,
} from '@sentry/core';
Expand Down Expand Up @@ -82,9 +84,11 @@ function createGenAiSpan(data: OpenAiChatChannelContext, operation: string, opti
const attributes = extractRequestAttributes(args, operation);
attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] = ORIGIN;
const model = (params?.model as string) || 'unknown';
const client = getClient();

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