From 9e3782b29768f2187016cff210411663630b5e7a Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Tue, 6 Oct 2026 11:10:40 +0200 Subject: [PATCH 1/6] test(node): Port LangGraph tests to span streaming Co-Authored-By: GPT-6 --- .../tracing/langgraph/instrument-agent.mjs | 8 -- .../langgraph/instrument-span-streaming.mjs | 11 -- .../tracing/langgraph/instrument-with-pii.mjs | 1 - .../suites/tracing/langgraph/instrument.mjs | 1 - .../suites/tracing/langgraph/test.ts | 120 +++++++----------- 5 files changed, 46 insertions(+), 95 deletions(-) delete mode 100644 dev-packages/node-integration-tests/suites/tracing/langgraph/instrument-span-streaming.mjs diff --git a/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument-agent.mjs b/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument-agent.mjs index b080c8d5ec03..1c507ba84b9c 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument-agent.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument-agent.mjs @@ -2,17 +2,9 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', tracesSampleRate: 1.0, dataCollection: { genAI: { inputs: true, outputs: true } }, transport: loggingTransport, - beforeSendTransaction: event => { - // Filter out mock express server transactions - if (event.transaction && event.transaction.includes('/v1/messages')) { - return null; - } - return event; - }, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument-span-streaming.mjs b/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument-span-streaming.mjs deleted file mode 100644 index 657bed0a3a8c..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument-span-streaming.mjs +++ /dev/null @@ -1,11 +0,0 @@ -import * as Sentry from '@sentry/node'; -import { loggingTransport } from '@sentry-internal/node-integration-tests'; - -Sentry.init({ - dsn: 'https://public@dsn.ingest.sentry.io/1337', - release: '1.0', - tracesSampleRate: 1.0, - dataCollection: { genAI: { inputs: true, outputs: true } }, - transport: loggingTransport, - traceLifecycle: 'stream', -}); diff --git a/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument-with-pii.mjs b/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument-with-pii.mjs index bdf24d6bbd0e..1c507ba84b9c 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument-with-pii.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument-with-pii.mjs @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', tracesSampleRate: 1.0, diff --git a/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument.mjs index 42052d281304..cc192fb89834 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument.mjs @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', tracesSampleRate: 1.0, diff --git a/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts b/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts index 92b94bf07853..a0cebbb81534 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts @@ -28,16 +28,18 @@ describe('LangGraph integration', () => { createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => { test('should instrument LangGraph with default PII settings', async () => { await createRunner() - .expect({ transaction: { transaction: 'langgraph-test' } }) .expect({ span: container => { - expect(container.items).toHaveLength(2); - expect(container.items.map(span => span.name).sort()).toEqual([ + const segment = container.items.find(span => span.is_segment && span.name === 'langgraph-test'); + expect(segment).toBeDefined(); + const spans = container.items.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.langgraph'); + expect(spans).toHaveLength(2); + expect(spans.map(span => span.name).sort()).toEqual([ 'invoke_agent weather_assistant', 'invoke_agent weather_assistant', ]); - const invokeAgentSpans = container.items.filter(span => span.name === 'invoke_agent weather_assistant'); + const invokeAgentSpans = spans.filter(span => span.name === 'invoke_agent weather_assistant'); expect(invokeAgentSpans).toHaveLength(2); for (const span of invokeAgentSpans) { expect(span.status).toBe('ok'); @@ -57,12 +59,14 @@ describe('LangGraph integration', () => { createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument-with-pii.mjs', (createRunner, test) => { test('should instrument LangGraph with genAI recording enabled', async () => { await createRunner() - .expect({ transaction: { transaction: 'langgraph-test' } }) .expect({ span: container => { - expect(container.items).toHaveLength(2); + const segment = container.items.find(span => span.is_segment && span.name === 'langgraph-test'); + expect(segment).toBeDefined(); + const spans = container.items.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.langgraph'); + expect(spans).toHaveLength(2); - const weatherTodaySpan = container.items.find(span => + const weatherTodaySpan = spans.find(span => getStringAttributeValue(span.attributes[GEN_AI_INPUT_MESSAGES]?.value)?.includes( 'What is the weather today?', ), @@ -73,7 +77,7 @@ describe('LangGraph integration', () => { expect(weatherTodaySpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); expect(weatherTodaySpan!.attributes['sentry.origin'].value).toBe('auto.ai.langgraph'); - const weatherDetailsSpan = container.items.find(span => + const weatherDetailsSpan = spans.find(span => getStringAttributeValue(span.attributes[GEN_AI_INPUT_MESSAGES]?.value)?.includes( 'Tell me about the weather', ), @@ -92,16 +96,18 @@ describe('LangGraph integration', () => { createEsmAndCjsTests(__dirname, 'scenario-tools.mjs', 'instrument-with-pii.mjs', (createRunner, test) => { test('should capture tools from LangGraph agent', { timeout: 30000 }, async () => { await createRunner() - .expect({ transaction: { transaction: 'langgraph-tools-test' } }) .expect({ span: container => { - expect(container.items).toHaveLength(2); - expect(container.items.map(span => span.name).sort()).toEqual([ + const segment = container.items.find(span => span.is_segment && span.name === 'langgraph-tools-test'); + expect(segment).toBeDefined(); + const spans = container.items.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.langgraph'); + expect(spans).toHaveLength(2); + expect(spans.map(span => span.name).sort()).toEqual([ 'invoke_agent tool_agent', 'invoke_agent tool_calling_agent', ]); - const toolAgentInvokeSpan = container.items.find(span => span.name === 'invoke_agent tool_agent'); + const toolAgentInvokeSpan = spans.find(span => span.name === 'invoke_agent tool_agent'); expect(toolAgentInvokeSpan).toBeDefined(); expect(toolAgentInvokeSpan!.status).toBe('ok'); expect(toolAgentInvokeSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); @@ -115,7 +121,7 @@ describe('LangGraph integration', () => { expect(toolAgentInvokeSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS].value).toBe(15); expect(toolAgentInvokeSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS].value).toBe(40); - const toolCallingInvokeSpan = container.items.find(span => span.name === 'invoke_agent tool_calling_agent'); + const toolCallingInvokeSpan = spans.find(span => span.name === 'invoke_agent tool_calling_agent'); expect(toolCallingInvokeSpan).toBeDefined(); expect(toolCallingInvokeSpan!.status).toBe('ok'); expect(toolCallingInvokeSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); @@ -137,12 +143,14 @@ describe('LangGraph integration', () => { createEsmAndCjsTests(__dirname, 'scenario-thread-id.mjs', 'instrument.mjs', (createRunner, test) => { test('should capture thread_id as gen_ai.conversation.id', async () => { await createRunner() - .expect({ transaction: { transaction: 'langgraph-thread-id-test' } }) .expect({ span: container => { - expect(container.items).toHaveLength(3); + const segment = container.items.find(span => span.is_segment && span.name === 'langgraph-thread-id-test'); + expect(segment).toBeDefined(); + const spans = container.items.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.langgraph'); + expect(spans).toHaveLength(3); - const firstThreadSpan = container.items.find( + const firstThreadSpan = spans.find( span => span.attributes[GEN_AI_CONVERSATION_ID]?.value === 'thread_abc123_session_1', ); expect(firstThreadSpan).toBeDefined(); @@ -150,14 +158,14 @@ describe('LangGraph integration', () => { expect(firstThreadSpan!.status).toBe('ok'); expect(firstThreadSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); - const secondThreadSpan = container.items.find( + const secondThreadSpan = spans.find( span => span.attributes[GEN_AI_CONVERSATION_ID]?.value === 'thread_xyz789_session_2', ); expect(secondThreadSpan).toBeDefined(); expect(secondThreadSpan!.name).toBe('invoke_agent thread_test_agent'); expect(secondThreadSpan!.status).toBe('ok'); - const noThreadSpan = container.items.find( + const noThreadSpan = spans.find( span => span.name === 'invoke_agent thread_test_agent' && span.attributes[GEN_AI_CONVERSATION_ID] === undefined, ); @@ -177,11 +185,15 @@ describe('LangGraph integration', () => { (createRunner, test) => { test('extracts system instructions from messages', async () => { await createRunner() - .expect({ transaction: { transaction: 'main' } }) .expect({ span: container => { - expect(container.items).toHaveLength(1); - const invokeAgentSpan = container.items.find(span => span.name === 'invoke_agent test-agent'); + const segment = container.items.find(span => span.is_segment && span.name === 'main'); + expect(segment).toBeDefined(); + const spans = container.items.filter( + span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.langgraph', + ); + expect(spans).toHaveLength(1); + const invokeAgentSpan = spans.find(span => span.name === 'invoke_agent test-agent'); expect(invokeAgentSpan).toBeDefined(); expect(invokeAgentSpan!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS].value).toBe( @@ -199,21 +211,14 @@ describe('LangGraph integration', () => { createEsmAndCjsTests(__dirname, 'scenario-resume.mjs', 'instrument.mjs', (createRunner, test) => { test('should not throw when invoke is called with null input (resume scenario)', async () => { await createRunner() - .expect({ - transaction: { - transaction: 'langgraph-resume-test', - contexts: { - trace: expect.objectContaining({ - status: 'ok', - }), - }, - }, - }) .expect({ span: container => { - expect(container.items).toHaveLength(2); + const segment = container.items.find(span => span.is_segment && span.name === 'langgraph-resume-test'); + expect(segment?.status).toBe('ok'); + const spans = container.items.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.langgraph'); + expect(spans).toHaveLength(2); - const invokeAgentSpan = container.items.find( + const invokeAgentSpan = spans.find( span => span.attributes[GEN_AI_CONVERSATION_ID]?.value === 'resume-thread-1', ); expect(invokeAgentSpan).toBeDefined(); @@ -230,28 +235,6 @@ describe('LangGraph integration', () => { }); }); - createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument-span-streaming.mjs', (createRunner, test) => { - test('creates langgraph related spans with span streaming enabled', async () => { - await createRunner() - .expect({ - span: container => { - const weatherTodaySpan = container.items.find(span => - getStringAttributeValue(span.attributes[GEN_AI_INPUT_MESSAGES]?.value)?.includes( - 'What is the weather today?', - ), - ); - expect(weatherTodaySpan).toBeDefined(); - expect(weatherTodaySpan!.name).toBe('invoke_agent weather_assistant'); - expect(weatherTodaySpan!.status).toBe('ok'); - expect(weatherTodaySpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); - expect(weatherTodaySpan!.attributes['sentry.origin'].value).toBe('auto.ai.langgraph'); - }, - }) - .start() - .completed(); - }); - }); - // createReactAgent tests. // Spans are asserted order-independently: the span-array order is not a protocol guarantee (Sentry // rebuilds the tree from `parent_span_id`), and the provider emits tree order while the OTel exporter @@ -259,15 +242,11 @@ describe('LangGraph integration', () => { createEsmAndCjsTests(__dirname, 'agent-scenario.mjs', 'instrument-agent.mjs', (createRunner, test) => { test('should instrument createReactAgent with agent and chat spans', { timeout: 30000 }, async () => { await createRunner() - .expect({ - transaction: event => { - const spans = event.spans ?? []; - expect(event.transaction).toBe('main'); - expect(spans).toContainEqual(expect.objectContaining({ op: 'http.client' })); - }, - }) .expect({ span: container => { + const segment = container.items.find(span => span.is_segment && span.name === 'main'); + expect(segment).toBeDefined(); + expect(container.items.find(span => span.attributes[SENTRY_OP]?.value === 'http.client')).toBeDefined(); const spans = container.items; expect(spans).toContainEqual( expect.objectContaining({ @@ -301,15 +280,11 @@ describe('LangGraph integration', () => { createEsmAndCjsTests(__dirname, 'agent-tools-scenario.mjs', 'instrument-agent.mjs', (createRunner, test) => { test('should create tool execution spans for createReactAgent with tools', { timeout: 30000 }, async () => { await createRunner() - .expect({ - transaction: event => { - const spans = event.spans ?? []; - expect(event.transaction).toBe('main'); - expect(spans.filter(span => span.op === 'http.client')).toHaveLength(3); - }, - }) .expect({ span: container => { + const segment = container.items.find(span => span.is_segment && span.name === 'main'); + expect(segment).toBeDefined(); + expect(container.items.filter(span => span.attributes[SENTRY_OP]?.value === 'http.client')).toHaveLength(3); const spans = container.items; expect(spans).toContainEqual( expect.objectContaining({ @@ -354,13 +329,10 @@ describe('LangGraph integration', () => { createEsmAndCjsTests(__dirname, 'scenario-stategraph-chat.mjs', 'instrument-agent.mjs', (createRunner, test) => { test('auto-injects langchain handler for plain StateGraph and emits chat spans', { timeout: 30000 }, async () => { await createRunner() - .expect({ - transaction: event => { - expect(event.transaction).toBe('main'); - }, - }) .expect({ span: container => { + const segment = container.items.find(span => span.is_segment && span.name === 'main'); + expect(segment).toBeDefined(); const chatSpans = container.items.filter(s => s.attributes[SENTRY_OP]?.value === 'gen_ai.chat'); expect(chatSpans).toHaveLength(1); expect(chatSpans[0]?.attributes[GEN_AI_AGENT_NAME]?.value).toBe('plain_assistant'); From 6aaaf8531f23814490fa498426bd254d6f720580 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Tue, 6 Oct 2026 11:25:09 +0200 Subject: [PATCH 2/6] test(node): Remove redundant langgraph assertions Co-Authored-By: GPT-6 --- .../suites/tracing/langgraph/test.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts b/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts index a0cebbb81534..6086160c0996 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts @@ -71,7 +71,6 @@ describe('LangGraph integration', () => { 'What is the weather today?', ), ); - expect(weatherTodaySpan).toBeDefined(); expect(weatherTodaySpan!.name).toBe('invoke_agent weather_assistant'); expect(weatherTodaySpan!.status).toBe('ok'); expect(weatherTodaySpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); @@ -82,7 +81,6 @@ describe('LangGraph integration', () => { 'Tell me about the weather', ), ); - expect(weatherDetailsSpan).toBeDefined(); expect(weatherDetailsSpan!.name).toBe('invoke_agent weather_assistant'); expect(weatherDetailsSpan!.status).toBe('ok'); expect(weatherDetailsSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); @@ -108,7 +106,6 @@ describe('LangGraph integration', () => { ]); const toolAgentInvokeSpan = spans.find(span => span.name === 'invoke_agent tool_agent'); - expect(toolAgentInvokeSpan).toBeDefined(); expect(toolAgentInvokeSpan!.status).toBe('ok'); expect(toolAgentInvokeSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); expect(toolAgentInvokeSpan!.attributes[GEN_AI_TOOL_DEFINITIONS].value).toContain('get_weather'); @@ -122,7 +119,6 @@ describe('LangGraph integration', () => { expect(toolAgentInvokeSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS].value).toBe(40); const toolCallingInvokeSpan = spans.find(span => span.name === 'invoke_agent tool_calling_agent'); - expect(toolCallingInvokeSpan).toBeDefined(); expect(toolCallingInvokeSpan!.status).toBe('ok'); expect(toolCallingInvokeSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); expect(toolCallingInvokeSpan!.attributes[GEN_AI_INPUT_MESSAGES].value).toContain('San Francisco'); @@ -153,7 +149,6 @@ describe('LangGraph integration', () => { const firstThreadSpan = spans.find( span => span.attributes[GEN_AI_CONVERSATION_ID]?.value === 'thread_abc123_session_1', ); - expect(firstThreadSpan).toBeDefined(); expect(firstThreadSpan!.name).toBe('invoke_agent thread_test_agent'); expect(firstThreadSpan!.status).toBe('ok'); expect(firstThreadSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); @@ -161,7 +156,6 @@ describe('LangGraph integration', () => { const secondThreadSpan = spans.find( span => span.attributes[GEN_AI_CONVERSATION_ID]?.value === 'thread_xyz789_session_2', ); - expect(secondThreadSpan).toBeDefined(); expect(secondThreadSpan!.name).toBe('invoke_agent thread_test_agent'); expect(secondThreadSpan!.status).toBe('ok'); @@ -169,7 +163,6 @@ describe('LangGraph integration', () => { span => span.name === 'invoke_agent thread_test_agent' && span.attributes[GEN_AI_CONVERSATION_ID] === undefined, ); - expect(noThreadSpan).toBeDefined(); expect(noThreadSpan!.status).toBe('ok'); }, }) @@ -195,7 +188,6 @@ describe('LangGraph integration', () => { expect(spans).toHaveLength(1); const invokeAgentSpan = spans.find(span => span.name === 'invoke_agent test-agent'); - expect(invokeAgentSpan).toBeDefined(); expect(invokeAgentSpan!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS].value).toBe( JSON.stringify([{ type: 'text', content: 'You are a helpful assistant' }]), ); @@ -221,7 +213,6 @@ describe('LangGraph integration', () => { const invokeAgentSpan = spans.find( span => span.attributes[GEN_AI_CONVERSATION_ID]?.value === 'resume-thread-1', ); - expect(invokeAgentSpan).toBeDefined(); expect(invokeAgentSpan!.name).toBe('invoke_agent resume_agent'); expect(invokeAgentSpan!.status).toBe('ok'); expect(invokeAgentSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); From 68fc23f1fc73d4af384598f1d288b4810b33d3d9 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Tue, 6 Oct 2026 11:49:36 +0200 Subject: [PATCH 3/6] test(node): Collect LangGraph spans across envelopes Co-Authored-By: GPT-6 --- .../suites/tracing/langgraph/test.ts | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts b/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts index 6086160c0996..068144c86760 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts @@ -1,3 +1,4 @@ +import type { SerializedStreamedSpanContainer } from '@sentry/core'; import { afterAll, describe, expect } from 'vitest'; import { GEN_AI_AGENT_NAME, @@ -232,13 +233,17 @@ describe('LangGraph integration', () => { // emits finish order (the `http.client` that the chat span wraps finishes before the chat span itself). createEsmAndCjsTests(__dirname, 'agent-scenario.mjs', 'instrument-agent.mjs', (createRunner, test) => { test('should instrument createReactAgent with agent and chat spans', { timeout: 30000 }, async () => { + const allSpans: SerializedStreamedSpanContainer['items'] = []; + await createRunner() + .unordered() .expect({ span: container => { - const segment = container.items.find(span => span.is_segment && span.name === 'main'); + allSpans.push(...container.items); + const segment = allSpans.find(span => span.is_segment && span.name === 'main'); expect(segment).toBeDefined(); - expect(container.items.find(span => span.attributes[SENTRY_OP]?.value === 'http.client')).toBeDefined(); - const spans = container.items; + expect(allSpans.find(span => span.attributes[SENTRY_OP]?.value === 'http.client')).toBeDefined(); + const spans = allSpans; expect(spans).toContainEqual( expect.objectContaining({ name: 'invoke_agent helpful_assistant', @@ -270,13 +275,17 @@ describe('LangGraph integration', () => { // createReactAgent with tools - verifies tool execution spans (asserted order-independently, see above). createEsmAndCjsTests(__dirname, 'agent-tools-scenario.mjs', 'instrument-agent.mjs', (createRunner, test) => { test('should create tool execution spans for createReactAgent with tools', { timeout: 30000 }, async () => { + const allSpans: SerializedStreamedSpanContainer['items'] = []; + await createRunner() + .unordered() .expect({ span: container => { - const segment = container.items.find(span => span.is_segment && span.name === 'main'); + allSpans.push(...container.items); + const segment = allSpans.find(span => span.is_segment && span.name === 'main'); expect(segment).toBeDefined(); - expect(container.items.filter(span => span.attributes[SENTRY_OP]?.value === 'http.client')).toHaveLength(3); - const spans = container.items; + expect(allSpans.filter(span => span.attributes[SENTRY_OP]?.value === 'http.client')).toHaveLength(3); + const spans = allSpans; expect(spans).toContainEqual( expect.objectContaining({ status: 'ok', @@ -319,12 +328,16 @@ describe('LangGraph integration', () => { createEsmAndCjsTests(__dirname, 'scenario-stategraph-chat.mjs', 'instrument-agent.mjs', (createRunner, test) => { test('auto-injects langchain handler for plain StateGraph and emits chat spans', { timeout: 30000 }, async () => { + const allSpans: SerializedStreamedSpanContainer['items'] = []; + await createRunner() + .unordered() .expect({ span: container => { - const segment = container.items.find(span => span.is_segment && span.name === 'main'); + allSpans.push(...container.items); + const segment = allSpans.find(span => span.is_segment && span.name === 'main'); expect(segment).toBeDefined(); - const chatSpans = container.items.filter(s => s.attributes[SENTRY_OP]?.value === 'gen_ai.chat'); + const chatSpans = allSpans.filter(s => s.attributes[SENTRY_OP]?.value === 'gen_ai.chat'); expect(chatSpans).toHaveLength(1); expect(chatSpans[0]?.attributes[GEN_AI_AGENT_NAME]?.value).toBe('plain_assistant'); }, From 35061aa44d23a830fe8edf34aa55a998633c7115 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Tue, 6 Oct 2026 12:06:50 +0200 Subject: [PATCH 4/6] test(node): Use the SENTRY_ORIGIN convention in langgraph tests Co-Authored-By: GPT-6 --- .../node-integration-tests/suites/tracing/langgraph/test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts b/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts index 068144c86760..837733156c5a 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts @@ -45,7 +45,7 @@ describe('LangGraph integration', () => { for (const span of invokeAgentSpans) { expect(span.status).toBe('ok'); expect(span.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); - expect(span.attributes['sentry.origin'].value).toBe('auto.ai.langgraph'); + expect(span.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langgraph'); expect(span.attributes[GEN_AI_OPERATION_NAME].value).toBe('invoke_agent'); expect(span.attributes[GEN_AI_AGENT_NAME].value).toBe('weather_assistant'); expect(span.attributes[GEN_AI_PIPELINE_NAME].value).toBe('weather_assistant'); @@ -75,7 +75,7 @@ describe('LangGraph integration', () => { expect(weatherTodaySpan!.name).toBe('invoke_agent weather_assistant'); expect(weatherTodaySpan!.status).toBe('ok'); expect(weatherTodaySpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); - expect(weatherTodaySpan!.attributes['sentry.origin'].value).toBe('auto.ai.langgraph'); + expect(weatherTodaySpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langgraph'); const weatherDetailsSpan = spans.find(span => getStringAttributeValue(span.attributes[GEN_AI_INPUT_MESSAGES]?.value)?.includes( @@ -217,7 +217,7 @@ describe('LangGraph integration', () => { expect(invokeAgentSpan!.name).toBe('invoke_agent resume_agent'); expect(invokeAgentSpan!.status).toBe('ok'); expect(invokeAgentSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); - expect(invokeAgentSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langgraph'); + expect(invokeAgentSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langgraph'); expect(invokeAgentSpan!.attributes[GEN_AI_AGENT_NAME].value).toBe('resume_agent'); expect(invokeAgentSpan!.attributes[GEN_AI_PIPELINE_NAME].value).toBe('resume_agent'); }, From 4af19964d1feca2e1dc8f69e61934ab52db4454d Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Tue, 6 Oct 2026 12:12:47 +0200 Subject: [PATCH 5/6] test(node): Use convention constants for langgraph span attributes Co-Authored-By: GPT-6 --- .../suites/tracing/langgraph/test.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts b/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts index 837733156c5a..0f5a10dc1188 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts @@ -44,7 +44,7 @@ describe('LangGraph integration', () => { expect(invokeAgentSpans).toHaveLength(2); for (const span of invokeAgentSpans) { expect(span.status).toBe('ok'); - expect(span.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(span.attributes[SENTRY_OP].value).toBe('gen_ai.invoke_agent'); expect(span.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langgraph'); expect(span.attributes[GEN_AI_OPERATION_NAME].value).toBe('invoke_agent'); expect(span.attributes[GEN_AI_AGENT_NAME].value).toBe('weather_assistant'); @@ -74,7 +74,7 @@ describe('LangGraph integration', () => { ); expect(weatherTodaySpan!.name).toBe('invoke_agent weather_assistant'); expect(weatherTodaySpan!.status).toBe('ok'); - expect(weatherTodaySpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(weatherTodaySpan!.attributes[SENTRY_OP].value).toBe('gen_ai.invoke_agent'); expect(weatherTodaySpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langgraph'); const weatherDetailsSpan = spans.find(span => @@ -84,7 +84,7 @@ describe('LangGraph integration', () => { ); expect(weatherDetailsSpan!.name).toBe('invoke_agent weather_assistant'); expect(weatherDetailsSpan!.status).toBe('ok'); - expect(weatherDetailsSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(weatherDetailsSpan!.attributes[SENTRY_OP].value).toBe('gen_ai.invoke_agent'); }, }) .start() @@ -108,7 +108,7 @@ describe('LangGraph integration', () => { const toolAgentInvokeSpan = spans.find(span => span.name === 'invoke_agent tool_agent'); expect(toolAgentInvokeSpan!.status).toBe('ok'); - expect(toolAgentInvokeSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(toolAgentInvokeSpan!.attributes[SENTRY_OP].value).toBe('gen_ai.invoke_agent'); expect(toolAgentInvokeSpan!.attributes[GEN_AI_TOOL_DEFINITIONS].value).toContain('get_weather'); expect(toolAgentInvokeSpan!.attributes[GEN_AI_INPUT_MESSAGES].value).toContain('What is the weather?'); expect(toolAgentInvokeSpan!.attributes[GEN_AI_RESPONSE_MODEL].value).toBe('gpt-4-0613'); @@ -121,7 +121,7 @@ describe('LangGraph integration', () => { const toolCallingInvokeSpan = spans.find(span => span.name === 'invoke_agent tool_calling_agent'); expect(toolCallingInvokeSpan!.status).toBe('ok'); - expect(toolCallingInvokeSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(toolCallingInvokeSpan!.attributes[SENTRY_OP].value).toBe('gen_ai.invoke_agent'); expect(toolCallingInvokeSpan!.attributes[GEN_AI_INPUT_MESSAGES].value).toContain('San Francisco'); expect(toolCallingInvokeSpan!.attributes[GEN_AI_RESPONSE_MODEL].value).toBe('gpt-4-0613'); expect(toolCallingInvokeSpan!.attributes[GEN_AI_RESPONSE_TEXT].value).toMatch(/"role":"tool"/); @@ -152,7 +152,7 @@ describe('LangGraph integration', () => { ); expect(firstThreadSpan!.name).toBe('invoke_agent thread_test_agent'); expect(firstThreadSpan!.status).toBe('ok'); - expect(firstThreadSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(firstThreadSpan!.attributes[SENTRY_OP].value).toBe('gen_ai.invoke_agent'); const secondThreadSpan = spans.find( span => span.attributes[GEN_AI_CONVERSATION_ID]?.value === 'thread_xyz789_session_2', @@ -216,7 +216,7 @@ describe('LangGraph integration', () => { ); expect(invokeAgentSpan!.name).toBe('invoke_agent resume_agent'); expect(invokeAgentSpan!.status).toBe('ok'); - expect(invokeAgentSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(invokeAgentSpan!.attributes[SENTRY_OP].value).toBe('gen_ai.invoke_agent'); expect(invokeAgentSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langgraph'); expect(invokeAgentSpan!.attributes[GEN_AI_AGENT_NAME].value).toBe('resume_agent'); expect(invokeAgentSpan!.attributes[GEN_AI_PIPELINE_NAME].value).toBe('resume_agent'); From d7a89de47db9ac175935af8dbfa478808814d5e5 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Wed, 7 Oct 2026 09:54:18 +0200 Subject: [PATCH 6/6] test(node): Assert LangGraph spans after envelope selection Co-Authored-By: GPT-6 --- .../suites/tracing/langgraph/test.ts | 146 +++++++++--------- 1 file changed, 73 insertions(+), 73 deletions(-) diff --git a/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts b/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts index 0f5a10dc1188..98faddab467e 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts @@ -233,117 +233,117 @@ describe('LangGraph integration', () => { // emits finish order (the `http.client` that the chat span wraps finishes before the chat span itself). createEsmAndCjsTests(__dirname, 'agent-scenario.mjs', 'instrument-agent.mjs', (createRunner, test) => { test('should instrument createReactAgent with agent and chat spans', { timeout: 30000 }, async () => { - const allSpans: SerializedStreamedSpanContainer['items'] = []; + let receivedSpans: SerializedStreamedSpanContainer['items'] = []; await createRunner() .unordered() .expect({ span: container => { - allSpans.push(...container.items); - const segment = allSpans.find(span => span.is_segment && span.name === 'main'); - expect(segment).toBeDefined(); - expect(allSpans.find(span => span.attributes[SENTRY_OP]?.value === 'http.client')).toBeDefined(); - const spans = allSpans; - expect(spans).toContainEqual( - expect.objectContaining({ - name: 'invoke_agent helpful_assistant', - status: 'ok', - attributes: expect.objectContaining({ - [GEN_AI_OPERATION_NAME]: expect.objectContaining({ value: 'invoke_agent' }), - [SENTRY_OP]: expect.objectContaining({ value: 'gen_ai.invoke_agent' }), - [SENTRY_ORIGIN]: expect.objectContaining({ value: 'auto.ai.langgraph' }), - [GEN_AI_AGENT_NAME]: expect.objectContaining({ value: 'helpful_assistant' }), - [GEN_AI_PIPELINE_NAME]: expect.objectContaining({ value: 'helpful_assistant' }), - }), - }), - ); - expect(spans).toContainEqual( - expect.objectContaining({ - attributes: expect.objectContaining({ - [SENTRY_OP]: expect.objectContaining({ value: 'gen_ai.chat' }), - [GEN_AI_AGENT_NAME]: expect.objectContaining({ value: 'helpful_assistant' }), - }), - }), - ); + expect(container.items.find(span => span.is_segment && span.name === 'main')).toBeDefined(); + receivedSpans = container.items; }, }) .start() .completed(); + + expect(receivedSpans.find(span => span.attributes[SENTRY_OP]?.value === 'http.client')).toBeDefined(); + const spans = receivedSpans; + expect(spans).toContainEqual( + expect.objectContaining({ + name: 'invoke_agent helpful_assistant', + status: 'ok', + attributes: expect.objectContaining({ + [GEN_AI_OPERATION_NAME]: expect.objectContaining({ value: 'invoke_agent' }), + [SENTRY_OP]: expect.objectContaining({ value: 'gen_ai.invoke_agent' }), + [SENTRY_ORIGIN]: expect.objectContaining({ value: 'auto.ai.langgraph' }), + [GEN_AI_AGENT_NAME]: expect.objectContaining({ value: 'helpful_assistant' }), + [GEN_AI_PIPELINE_NAME]: expect.objectContaining({ value: 'helpful_assistant' }), + }), + }), + ); + expect(spans).toContainEqual( + expect.objectContaining({ + attributes: expect.objectContaining({ + [SENTRY_OP]: expect.objectContaining({ value: 'gen_ai.chat' }), + [GEN_AI_AGENT_NAME]: expect.objectContaining({ value: 'helpful_assistant' }), + }), + }), + ); }); }); // createReactAgent with tools - verifies tool execution spans (asserted order-independently, see above). createEsmAndCjsTests(__dirname, 'agent-tools-scenario.mjs', 'instrument-agent.mjs', (createRunner, test) => { test('should create tool execution spans for createReactAgent with tools', { timeout: 30000 }, async () => { - const allSpans: SerializedStreamedSpanContainer['items'] = []; + let receivedSpans: SerializedStreamedSpanContainer['items'] = []; await createRunner() .unordered() .expect({ span: container => { - allSpans.push(...container.items); - const segment = allSpans.find(span => span.is_segment && span.name === 'main'); - expect(segment).toBeDefined(); - expect(allSpans.filter(span => span.attributes[SENTRY_OP]?.value === 'http.client')).toHaveLength(3); - const spans = allSpans; - expect(spans).toContainEqual( - expect.objectContaining({ - status: 'ok', - attributes: expect.objectContaining({ - [SENTRY_OP]: expect.objectContaining({ value: 'gen_ai.invoke_agent' }), - [GEN_AI_OPERATION_NAME]: expect.objectContaining({ value: 'invoke_agent' }), - [GEN_AI_AGENT_NAME]: expect.objectContaining({ value: 'math_assistant' }), - }), - }), - ); - expect(spans).toContainEqual( - expect.objectContaining({ - name: 'execute_tool add', - status: 'ok', - attributes: expect.objectContaining({ - [SENTRY_OP]: expect.objectContaining({ value: 'gen_ai.execute_tool' }), - [GEN_AI_OPERATION_NAME]: expect.objectContaining({ value: 'execute_tool' }), - [GEN_AI_TOOL_NAME]: expect.objectContaining({ value: 'add' }), - }), - }), - ); - expect(spans).toContainEqual( - expect.objectContaining({ - name: 'execute_tool multiply', - status: 'ok', - attributes: expect.objectContaining({ - [SENTRY_OP]: expect.objectContaining({ value: 'gen_ai.execute_tool' }), - [GEN_AI_OPERATION_NAME]: expect.objectContaining({ value: 'execute_tool' }), - [GEN_AI_TOOL_NAME]: expect.objectContaining({ value: 'multiply' }), - }), - }), - ); - expect(spans.filter(span => span.attributes[SENTRY_OP]?.value === 'gen_ai.chat')).toHaveLength(3); + expect(container.items.find(span => span.is_segment && span.name === 'main')).toBeDefined(); + receivedSpans = container.items; }, }) .start() .completed(); + + expect(receivedSpans.filter(span => span.attributes[SENTRY_OP]?.value === 'http.client')).toHaveLength(3); + const spans = receivedSpans; + expect(spans).toContainEqual( + expect.objectContaining({ + status: 'ok', + attributes: expect.objectContaining({ + [SENTRY_OP]: expect.objectContaining({ value: 'gen_ai.invoke_agent' }), + [GEN_AI_OPERATION_NAME]: expect.objectContaining({ value: 'invoke_agent' }), + [GEN_AI_AGENT_NAME]: expect.objectContaining({ value: 'math_assistant' }), + }), + }), + ); + expect(spans).toContainEqual( + expect.objectContaining({ + name: 'execute_tool add', + status: 'ok', + attributes: expect.objectContaining({ + [SENTRY_OP]: expect.objectContaining({ value: 'gen_ai.execute_tool' }), + [GEN_AI_OPERATION_NAME]: expect.objectContaining({ value: 'execute_tool' }), + [GEN_AI_TOOL_NAME]: expect.objectContaining({ value: 'add' }), + }), + }), + ); + expect(spans).toContainEqual( + expect.objectContaining({ + name: 'execute_tool multiply', + status: 'ok', + attributes: expect.objectContaining({ + [SENTRY_OP]: expect.objectContaining({ value: 'gen_ai.execute_tool' }), + [GEN_AI_OPERATION_NAME]: expect.objectContaining({ value: 'execute_tool' }), + [GEN_AI_TOOL_NAME]: expect.objectContaining({ value: 'multiply' }), + }), + }), + ); + expect(spans.filter(span => span.attributes[SENTRY_OP]?.value === 'gen_ai.chat')).toHaveLength(3); }); }); createEsmAndCjsTests(__dirname, 'scenario-stategraph-chat.mjs', 'instrument-agent.mjs', (createRunner, test) => { test('auto-injects langchain handler for plain StateGraph and emits chat spans', { timeout: 30000 }, async () => { - const allSpans: SerializedStreamedSpanContainer['items'] = []; + let receivedSpans: SerializedStreamedSpanContainer['items'] = []; await createRunner() .unordered() .expect({ span: container => { - allSpans.push(...container.items); - const segment = allSpans.find(span => span.is_segment && span.name === 'main'); - expect(segment).toBeDefined(); - const chatSpans = allSpans.filter(s => s.attributes[SENTRY_OP]?.value === 'gen_ai.chat'); - expect(chatSpans).toHaveLength(1); - expect(chatSpans[0]?.attributes[GEN_AI_AGENT_NAME]?.value).toBe('plain_assistant'); + expect(container.items.find(span => span.is_segment && span.name === 'main')).toBeDefined(); + receivedSpans = container.items; }, }) .start() .completed(); + + const chatSpans = receivedSpans.filter(s => s.attributes[SENTRY_OP]?.value === 'gen_ai.chat'); + expect(chatSpans).toHaveLength(1); + expect(chatSpans[0]?.attributes[GEN_AI_AGENT_NAME]?.value).toBe('plain_assistant'); }); }); });