From b18c1401bb18794dd577355082b855fe60ee1a91 Mon Sep 17 00:00:00 2001 From: RulaKhaled Date: Tue, 25 Aug 2026 15:32:38 +0200 Subject: [PATCH 1/3] feat(server-utils): Emit low-cardinality gen_ai agent span names when streaming Fixes #23524 Co-Authored-By: Cursor --- MIGRATION.md | 15 ++-- .../suites/tracing/langchain/test.ts | 22 ++++++ .../server-utils/src/ai/langchain/index.ts | 8 ++- .../vercel-ai/vercel-ai-dc-subscriber.ts | 7 +- .../tracing/langchain-invoke-agent.test.ts | 69 +++++++++++++++++++ .../tracing/langgraph-invoke-agent.test.ts | 56 +++++++++++++++ .../vercel-ai/invoke-agent-span-names.test.ts | 61 ++++++++++++++++ 7 files changed, 230 insertions(+), 8 deletions(-) create mode 100644 packages/server-utils/test/ai/lib/tracing/langchain-invoke-agent.test.ts create mode 100644 packages/server-utils/test/ai/lib/tracing/langgraph-invoke-agent.test.ts create mode 100644 packages/server-utils/test/integrations/vercel-ai/invoke-agent-span-names.test.ts diff --git a/MIGRATION.md b/MIGRATION.md index 9a7fafc9f9c4..734d62c9f7c2 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -619,15 +619,16 @@ 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/). -In v11, this affects `pageload` and `graphql` spans. Further ops will follow in future releases. +In v11, this affects `pageload`, `graphql`, and `gen_ai.invoke_agent` spans. Further ops will follow in future releases. If you [opt out of span streaming](#opting-out-of-span-streaming), span names remain unchanged. The following span names were adjusted: -| 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`) | +| 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.invoke_agent`, `gen_ai.handoff` | `{operation} {agent}` (`invoke_agent weather_assistant`), `chain {chainName}` (`chain format_prompt`), or `{operation} {functionId}` (`invoke_agent weather_agent`) | `{operation} {agent}` when the agent name is known, or `{operation}` if the SDK has none (`invoke_agent`). Chain names stay on `langchain.chain.name`; function ids stay on `gen_ai.function.id` | Some consequences to be aware of: @@ -639,7 +640,9 @@ For the same reason, `useOperationNameForRootSpan` no longer renames the enclosi 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. -`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: +Resolved low-cardinality values are kept in both lifecycles: a known agent name stays in the name (`invoke_agent weather_assistant`). + +`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. Filters matching `chain format_prompt` or `invoke_agent weather_agent` no longer apply to a streamed agent span (`'invoke_agent'`). Match on attributes instead: ```js Sentry.init({ diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/test.ts b/dev-packages/node-integration-tests/suites/tracing/langchain/test.ts index e1ead1a89fe2..f2bfd36a0690 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/langchain/test.ts @@ -420,4 +420,26 @@ describe('LangChain integration', () => { .completed(); }); }); + + createEsmAndCjsTests(__dirname, 'scenario-chain.mjs', 'instrument-span-streaming.mjs', (createRunner, test) => { + test('uses invoke_agent for chain spans when span streaming is enabled', async () => { + await createRunner() + .ignore('event') + .expect({ + span: container => { + const chainSpans = container.items.filter( + span => span.attributes['sentry.op']?.value === 'gen_ai.invoke_agent', + ); + expect(chainSpans.map(span => span.name).sort()).toEqual(['invoke_agent', 'invoke_agent', 'invoke_agent']); + expect(chainSpans.map(span => span.attributes['langchain.chain.name']?.value).sort()).toEqual([ + 'format_prompt', + 'parse_output', + 'unknown_chain', + ]); + }, + }) + .start() + .completed(); + }); + }); }); diff --git a/packages/server-utils/src/ai/langchain/index.ts b/packages/server-utils/src/ai/langchain/index.ts index 1f6ba8754f79..fc0281d29a67 100644 --- a/packages/server-utils/src/ai/langchain/index.ts +++ b/packages/server-utils/src/ai/langchain/index.ts @@ -1,6 +1,8 @@ /* eslint-disable max-lines */ import { captureException, + getClient, + hasSpanStreamingEnabled, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, @@ -227,9 +229,13 @@ export function createLangChainCallbackHandler(options: LangChainOptions = {}): attributes['langchain.chain.inputs'] = JSON.stringify(inputs); } + const client = getClient(); + startSpanManual( { - name: `chain ${chainName}`, + // With span streaming, the name follows the `{operation}` agent template. The chain + // name stays available on `langchain.chain.name`. + name: client && hasSpanStreamingEnabled(client) ? 'invoke_agent' : `chain ${chainName}`, op: 'gen_ai.invoke_agent', attributes: { ...attributes, diff --git a/packages/server-utils/src/integrations/vercel-ai/vercel-ai-dc-subscriber.ts b/packages/server-utils/src/integrations/vercel-ai/vercel-ai-dc-subscriber.ts index 449fa12005e8..02390c274bc4 100644 --- a/packages/server-utils/src/integrations/vercel-ai/vercel-ai-dc-subscriber.ts +++ b/packages/server-utils/src/integrations/vercel-ai/vercel-ai-dc-subscriber.ts @@ -26,6 +26,7 @@ import { _INTERNAL_skipAiProviderWrapping, captureException, getClient, + hasSpanStreamingEnabled, isObjectLike, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, @@ -429,7 +430,11 @@ function buildInvokeAgentSpan( if (callId) { operationIdByCallId.set(callId, { operationId, isStream }); } - const span = startGenAiSpan(GEN_AI_INVOKE_AGENT_OPERATION, functionId, { + // With span streaming, the name follows the `{operation}` agent template. `functionId` is not + // `gen_ai.agent.name` — it stays available on `gen_ai.function.id`. + const client = getClient(); + const nameSuffix = client && hasSpanStreamingEnabled(client) ? undefined : functionId; + const span = startGenAiSpan(GEN_AI_INVOKE_AGENT_OPERATION, nameSuffix, { ...baseAttributes, [VERCEL_AI_OPERATION_ID_ATTRIBUTE]: operationId, [GEN_AI_RESPONSE_STREAMING]: isStream, diff --git a/packages/server-utils/test/ai/lib/tracing/langchain-invoke-agent.test.ts b/packages/server-utils/test/ai/lib/tracing/langchain-invoke-agent.test.ts new file mode 100644 index 000000000000..a7361120d6c2 --- /dev/null +++ b/packages/server-utils/test/ai/lib/tracing/langchain-invoke-agent.test.ts @@ -0,0 +1,69 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { getMainCarrier, setCurrentClient, spanToStaticSpanJSON } from '@sentry/core'; +import type { Span } from '@sentry/core'; +import { createLangChainCallbackHandler } from '../../../../src/ai/langchain'; +import { getDefaultTestClientOptions, TestClient } from '../../../mocks/client'; + +describe('LangChain invoke_agent span names', () => { + beforeEach(() => { + getMainCarrier().__SENTRY__ = undefined; + }); + + afterEach(() => { + getMainCarrier().__SENTRY__ = undefined; + }); + + function setupClient(traceLifecycle: 'static' | 'stream'): Span[] { + const client = new TestClient( + getDefaultTestClientOptions({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + tracesSampleRate: 1, + traceLifecycle, + }), + ); + setCurrentClient(client); + client.init(); + + const endedSpans: Span[] = []; + client.on('spanEnd', span => endedSpans.push(span)); + return endedSpans; + } + + function runChain(runName?: string, chain: { name?: string } = {}): void { + const handler = createLangChainCallbackHandler(); + handler.handleChainStart?.( + chain, + { topic: 'weather' }, + 'run-1', + undefined, + undefined, + undefined, + undefined, + runName, + ); + handler.handleChainEnd?.({ ok: true }, 'run-1'); + } + + it('keeps `chain {chainName}` in static mode', () => { + const endedSpans = setupClient('static'); + runChain('format_prompt'); + + expect(spanToStaticSpanJSON(endedSpans[0]!).description).toBe('chain format_prompt'); + }); + + it('keeps `chain unknown_chain` when the chain name is missing in static mode', () => { + const endedSpans = setupClient('static'); + runChain(); + + expect(spanToStaticSpanJSON(endedSpans[0]!).description).toBe('chain unknown_chain'); + }); + + it('uses `invoke_agent` when span streaming is enabled', () => { + const endedSpans = setupClient('stream'); + runChain('format_prompt'); + + const span = spanToStaticSpanJSON(endedSpans[0]!); + expect(span.description).toBe('invoke_agent'); + expect(span.data?.['langchain.chain.name']).toBe('format_prompt'); + }); +}); diff --git a/packages/server-utils/test/ai/lib/tracing/langgraph-invoke-agent.test.ts b/packages/server-utils/test/ai/lib/tracing/langgraph-invoke-agent.test.ts new file mode 100644 index 000000000000..1e8a204bd85e --- /dev/null +++ b/packages/server-utils/test/ai/lib/tracing/langgraph-invoke-agent.test.ts @@ -0,0 +1,56 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { getMainCarrier, setCurrentClient, spanToStaticSpanJSON } from '@sentry/core'; +import type { Span } from '@sentry/core'; +import { instrumentCompiledGraphInvoke } from '../../../../src/ai/langgraph'; +import type { CompiledGraph } from '../../../../src/ai/langgraph/types'; +import { getDefaultTestClientOptions, TestClient } from '../../../mocks/client'; + +describe('LangGraph invoke_agent span names', () => { + beforeEach(() => { + getMainCarrier().__SENTRY__ = undefined; + }); + + afterEach(() => { + getMainCarrier().__SENTRY__ = undefined; + }); + + function setupClient(traceLifecycle: 'static' | 'stream'): Span[] { + const client = new TestClient( + getDefaultTestClientOptions({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + tracesSampleRate: 1, + traceLifecycle, + }), + ); + setCurrentClient(client); + client.init(); + + const endedSpans: Span[] = []; + client.on('spanEnd', span => endedSpans.push(span)); + return endedSpans; + } + + async function invokeGraph(compileOptions: Record): Promise { + const invoke = instrumentCompiledGraphInvoke( + async () => ({ messages: [] }), + {} as CompiledGraph, + compileOptions, + {}, + ); + await invoke({}); + } + + it('names the span `{operation} {agent}` when an agent name is present', async () => { + const endedSpans = setupClient('stream'); + await invokeGraph({ name: 'weather_assistant' }); + + expect(spanToStaticSpanJSON(endedSpans[0]!).description).toBe('invoke_agent weather_assistant'); + }); + + it('uses the operation name when the agent name is missing and span streaming is enabled', async () => { + const endedSpans = setupClient('stream'); + await invokeGraph({}); + + expect(spanToStaticSpanJSON(endedSpans[0]!).description).toBe('invoke_agent'); + }); +}); diff --git a/packages/server-utils/test/integrations/vercel-ai/invoke-agent-span-names.test.ts b/packages/server-utils/test/integrations/vercel-ai/invoke-agent-span-names.test.ts new file mode 100644 index 000000000000..0fd89564ccf8 --- /dev/null +++ b/packages/server-utils/test/integrations/vercel-ai/invoke-agent-span-names.test.ts @@ -0,0 +1,61 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { GEN_AI_FUNCTION_ID } from '@sentry/conventions/attributes'; +import { getMainCarrier, setCurrentClient, spanToStaticSpanJSON } from '@sentry/core'; +import type { Span } from '@sentry/core'; +import { createSpanFromMessage } from '../../../src/integrations/vercel-ai/vercel-ai-dc-subscriber'; +import { getDefaultTestClientOptions, TestClient } from '../../mocks/client'; + +describe('Vercel AI invoke_agent span names', () => { + beforeEach(() => { + getMainCarrier().__SENTRY__ = undefined; + }); + + afterEach(() => { + getMainCarrier().__SENTRY__ = undefined; + }); + + function setupClient(traceLifecycle: 'static' | 'stream'): Span[] { + const client = new TestClient( + getDefaultTestClientOptions({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + tracesSampleRate: 1, + traceLifecycle, + }), + ); + setCurrentClient(client); + client.init(); + + const endedSpans: Span[] = []; + client.on('spanEnd', span => endedSpans.push(span)); + return endedSpans; + } + + function startInvokeAgentSpan(functionId?: string): void { + const span = createSpanFromMessage( + { + type: 'generateText', + event: functionId ? { functionId } : {}, + } as Parameters[0], + {} as Parameters[1], + ); + span?.end(); + } + + it('keeps `invoke_agent {functionId}` in static mode', () => { + const endedSpans = setupClient('static'); + startInvokeAgentSpan('weather_agent'); + + const span = spanToStaticSpanJSON(endedSpans[0]!); + expect(span.description).toBe('invoke_agent weather_agent'); + expect(span.data?.[GEN_AI_FUNCTION_ID]).toBe('weather_agent'); + }); + + it('uses `invoke_agent` when span streaming is enabled', () => { + const endedSpans = setupClient('stream'); + startInvokeAgentSpan('weather_agent'); + + const span = spanToStaticSpanJSON(endedSpans[0]!); + expect(span.description).toBe('invoke_agent'); + expect(span.data?.[GEN_AI_FUNCTION_ID]).toBe('weather_agent'); + }); +}); From 00717964068e713859d24df6c858e3547413e93b Mon Sep 17 00:00:00 2001 From: RulaKhaled Date: Tue, 25 Aug 2026 16:03:15 +0200 Subject: [PATCH 2/3] docs: Document gen_ai agent span names in the v11 end-state guide --- MIGRATION.md | 15 ++++++--------- docs/migration/v11-end-state.md | 14 ++++++++++++++ 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index 734d62c9f7c2..9a7fafc9f9c4 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -619,16 +619,15 @@ 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/). -In v11, this affects `pageload`, `graphql`, and `gen_ai.invoke_agent` spans. Further ops will follow in future releases. +In v11, this affects `pageload` and `graphql` spans. Further ops will follow in future releases. If you [opt out of span streaming](#opting-out-of-span-streaming), span names remain unchanged. The following span names were adjusted: -| 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.invoke_agent`, `gen_ai.handoff` | `{operation} {agent}` (`invoke_agent weather_assistant`), `chain {chainName}` (`chain format_prompt`), or `{operation} {functionId}` (`invoke_agent weather_agent`) | `{operation} {agent}` when the agent name is known, or `{operation}` if the SDK has none (`invoke_agent`). Chain names stay on `langchain.chain.name`; function ids stay on `gen_ai.function.id` | +| 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`) | Some consequences to be aware of: @@ -640,9 +639,7 @@ For the same reason, `useOperationNameForRootSpan` no longer renames the enclosi 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. -Resolved low-cardinality values are kept in both lifecycles: a known agent name stays in the name (`invoke_agent weather_assistant`). - -`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. Filters matching `chain format_prompt` or `invoke_agent weather_agent` no longer apply to a streamed agent span (`'invoke_agent'`). Match on attributes instead: +`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: ```js Sentry.init({ diff --git a/docs/migration/v11-end-state.md b/docs/migration/v11-end-state.md index f80b801c28b3..b630cb643059 100644 --- a/docs/migration/v11-end-state.md +++ b/docs/migration/v11-end-state.md @@ -475,6 +475,20 @@ 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 | +| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `gen_ai.invoke_agent`, `gen_ai.handoff` | `{operation} {agent}` (`invoke_agent weather_assistant`), `chain {chainName}` (`chain format_prompt`), or `{operation} {functionId}` (`invoke_agent weather_agent`) | `{operation} {agent}` when the agent name is known, or `{operation}` if the SDK has none (`invoke_agent`). Chain names stay on `langchain.chain.name`; function ids stay on `gen_ai.function.id` | + +Resolved low-cardinality values are kept in both lifecycles: a known agent name stays in the name (`invoke_agent weather_assistant`). + +`ignoreSpans` is evaluated at span start. Filters matching `chain format_prompt` or `invoke_agent weather_agent` no longer apply to a streamed agent span named `'invoke_agent'`; match on `langchain.chain.name` or `gen_ai.function.id` instead. + ### The `enableLogs` option was removed Affected SDKs: All SDKs. From 19076c5a06cab1323233866df90517dc005e05b4 Mon Sep 17 00:00:00 2001 From: RulaKhaled Date: Tue, 25 Aug 2026 16:22:23 +0200 Subject: [PATCH 3/3] fix(server-utils): Set gen_ai.operation.name on LangChain chain spans Streamed invoke_agent names should be derivable from attributes, not only from the span name. Co-Authored-By: Cursor --- .../node-integration-tests/suites/tracing/langchain/test.ts | 4 ++++ packages/server-utils/src/ai/langchain/index.ts | 1 + .../test/ai/lib/tracing/langchain-invoke-agent.test.ts | 3 +++ 3 files changed, 8 insertions(+) diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/test.ts b/dev-packages/node-integration-tests/suites/tracing/langchain/test.ts index f2bfd36a0690..dc4d7b315e2d 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/langchain/test.ts @@ -276,6 +276,7 @@ describe('LangChain integration', () => { expect(formatPromptSpan).toBeDefined(); expect(formatPromptSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); expect(formatPromptSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); + expect(formatPromptSpan!.attributes[GEN_AI_OPERATION_NAME].value).toBe('invoke_agent'); expect(formatPromptSpan!.attributes['langchain.chain.name'].value).toBe('format_prompt'); const chatSpan = container.items.find(span => span.name === 'chat claude-3-5-sonnet-20241022'); @@ -431,6 +432,9 @@ describe('LangChain integration', () => { span => span.attributes['sentry.op']?.value === 'gen_ai.invoke_agent', ); expect(chainSpans.map(span => span.name).sort()).toEqual(['invoke_agent', 'invoke_agent', 'invoke_agent']); + for (const span of chainSpans) { + expect(span.attributes[GEN_AI_OPERATION_NAME]?.value).toBe('invoke_agent'); + } expect(chainSpans.map(span => span.attributes['langchain.chain.name']?.value).sort()).toEqual([ 'format_prompt', 'parse_output', diff --git a/packages/server-utils/src/ai/langchain/index.ts b/packages/server-utils/src/ai/langchain/index.ts index fc0281d29a67..513b12dfd57a 100644 --- a/packages/server-utils/src/ai/langchain/index.ts +++ b/packages/server-utils/src/ai/langchain/index.ts @@ -222,6 +222,7 @@ export function createLangChainCallbackHandler(options: LangChainOptions = {}): const chainName = runName || chain.name || 'unknown_chain'; const attributes: Record = { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.langchain', + [GEN_AI_OPERATION_NAME]: 'invoke_agent', 'langchain.chain.name': chainName, }; diff --git a/packages/server-utils/test/ai/lib/tracing/langchain-invoke-agent.test.ts b/packages/server-utils/test/ai/lib/tracing/langchain-invoke-agent.test.ts index a7361120d6c2..23759a0feb13 100644 --- a/packages/server-utils/test/ai/lib/tracing/langchain-invoke-agent.test.ts +++ b/packages/server-utils/test/ai/lib/tracing/langchain-invoke-agent.test.ts @@ -1,4 +1,5 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { GEN_AI_OPERATION_NAME } from '@sentry/conventions/attributes'; import { getMainCarrier, setCurrentClient, spanToStaticSpanJSON } from '@sentry/core'; import type { Span } from '@sentry/core'; import { createLangChainCallbackHandler } from '../../../../src/ai/langchain'; @@ -49,6 +50,7 @@ describe('LangChain invoke_agent span names', () => { runChain('format_prompt'); expect(spanToStaticSpanJSON(endedSpans[0]!).description).toBe('chain format_prompt'); + expect(spanToStaticSpanJSON(endedSpans[0]!).data?.[GEN_AI_OPERATION_NAME]).toBe('invoke_agent'); }); it('keeps `chain unknown_chain` when the chain name is missing in static mode', () => { @@ -64,6 +66,7 @@ describe('LangChain invoke_agent span names', () => { const span = spanToStaticSpanJSON(endedSpans[0]!); expect(span.description).toBe('invoke_agent'); + expect(span.data?.[GEN_AI_OPERATION_NAME]).toBe('invoke_agent'); expect(span.data?.['langchain.chain.name']).toBe('format_prompt'); }); });