From 3ba7799630bd15a2155d3cc11af80c492424bde7 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Tue, 6 Oct 2026 11:08:00 +0200 Subject: [PATCH 1/8] test(node): Port LangChain tests to span streaming Co-Authored-By: GPT-6 --- .../langchain/instrument-span-streaming.mjs | 11 - .../tracing/langchain/instrument-with-pii.mjs | 8 - .../suites/tracing/langchain/instrument.mjs | 8 - .../suites/tracing/langchain/test.ts | 204 ++++++------------ .../langchain/v1/instrument-with-pii.mjs | 8 - .../tracing/langchain/v1/instrument.mjs | 8 - .../suites/tracing/langchain/v1/test.ts | 75 ++++--- 7 files changed, 114 insertions(+), 208 deletions(-) delete mode 100644 dev-packages/node-integration-tests/suites/tracing/langchain/instrument-span-streaming.mjs diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-span-streaming.mjs b/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-span-streaming.mjs deleted file mode 100644 index 657bed0a3a8c..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/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/langchain/instrument-with-pii.mjs b/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-with-pii.mjs index e60b44f864d7..1c507ba84b9c 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-with-pii.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-with-pii.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.includes('/v1/messages') || event.transaction.includes('/v1/embeddings')) { - return null; - } - return event; - }, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/langchain/instrument.mjs index c81bb4dc1322..cc192fb89834 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/langchain/instrument.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: false, outputs: false } }, transport: loggingTransport, - beforeSendTransaction: event => { - // Filter out mock express server transactions - if (event.transaction.includes('/v1/messages') || event.transaction.includes('/v1/embeddings')) { - return null; - } - return event; - }, }); 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 1d74840579bc..c88dd623094e 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/langchain/test.ts @@ -17,6 +17,7 @@ import { GEN_AI_USAGE_INPUT_TOKENS, GEN_AI_USAGE_OUTPUT_TOKENS, GEN_AI_USAGE_TOTAL_TOKENS, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { GEN_AI_EMBEDDINGS } from '@sentry/conventions/op'; import { @@ -34,17 +35,19 @@ describe('LangChain integration', () => { createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => { test('creates langchain related spans with genAI recording disabled', async () => { await createRunner() - .expect({ transaction: { transaction: 'main' } }) .expect({ span: container => { - expect(container.items).toHaveLength(3); - expect(container.items.map(span => span.name).sort()).toEqual([ + 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.langchain'); + expect(spans).toHaveLength(3); + expect(spans.map(span => span.name).sort()).toEqual([ 'chat claude-3-5-sonnet-20241022', 'chat claude-3-opus-20240229', 'chat error-model', ]); - const sonnetSpan = container.items.find(span => span.name === 'chat claude-3-5-sonnet-20241022'); + const sonnetSpan = spans.find(span => span.name === 'chat claude-3-5-sonnet-20241022'); expect(sonnetSpan).toBeDefined(); expect(sonnetSpan!.status).toBe('ok'); expect(sonnetSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); @@ -61,7 +64,7 @@ describe('LangChain integration', () => { expect(sonnetSpan!.attributes[GEN_AI_RESPONSE_MODEL]).toBeDefined(); expect(sonnetSpan!.attributes[GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE]).toBeDefined(); - const opusSpan = container.items.find(span => span.name === 'chat claude-3-opus-20240229'); + const opusSpan = spans.find(span => span.name === 'chat claude-3-opus-20240229'); expect(opusSpan).toBeDefined(); expect(opusSpan!.status).toBe('ok'); expect(opusSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); @@ -75,7 +78,7 @@ describe('LangChain integration', () => { expect(opusSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS].value).toBe(15); expect(opusSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS].value).toBe(25); - const errorSpan = container.items.find(span => span.name === 'chat error-model'); + const errorSpan = spans.find(span => span.name === 'chat error-model'); expect(errorSpan).toBeDefined(); expect(errorSpan!.status).toBe('error'); expect(errorSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); @@ -87,40 +90,24 @@ describe('LangChain integration', () => { .start() .completed(); }); - - test('does not create duplicate spans from double module patching', async () => { - await createRunner() - .expect({ transaction: { transaction: 'main' } }) - .expect({ - span: container => { - // The scenario makes 3 LangChain calls (2 successful + 1 error). - // Without the dedup guard, the file-level and module-level hooks - // both patch the same prototype, producing 6 spans instead of 3. - expect(container.items).toHaveLength(3); - for (const span of container.items) { - expect(span.attributes['sentry.op'].value).toBe('gen_ai.chat'); - } - }, - }) - .start() - .completed(); - }); }); createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument-with-pii.mjs', (createRunner, test) => { test('creates langchain related spans with genAI recording enabled', async () => { await createRunner() - .expect({ transaction: { transaction: 'main' } }) .expect({ span: container => { - expect(container.items).toHaveLength(3); - expect(container.items.map(span => span.name).sort()).toEqual([ + 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.langchain'); + expect(spans).toHaveLength(3); + expect(spans.map(span => span.name).sort()).toEqual([ 'chat claude-3-5-sonnet-20241022', 'chat claude-3-opus-20240229', 'chat error-model', ]); - const sonnetSpan = container.items.find(span => span.name === 'chat claude-3-5-sonnet-20241022'); + const sonnetSpan = spans.find(span => span.name === 'chat claude-3-5-sonnet-20241022'); expect(sonnetSpan).toBeDefined(); expect(sonnetSpan!.status).toBe('ok'); expect(sonnetSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); @@ -138,7 +125,7 @@ describe('LangChain integration', () => { expect(sonnetSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS].value).toBe(15); expect(sonnetSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS].value).toBe(25); - const opusSpan = container.items.find(span => span.name === 'chat claude-3-opus-20240229'); + const opusSpan = spans.find(span => span.name === 'chat claude-3-opus-20240229'); expect(opusSpan).toBeDefined(); expect(opusSpan!.status).toBe('ok'); expect(opusSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); @@ -152,7 +139,7 @@ describe('LangChain integration', () => { expect(opusSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS].value).toBe(15); expect(opusSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS].value).toBe(25); - const errorSpan = container.items.find(span => span.name === 'chat error-model'); + const errorSpan = spans.find(span => span.name === 'chat error-model'); expect(errorSpan).toBeDefined(); expect(errorSpan!.status).toBe('error'); expect(errorSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); @@ -168,11 +155,13 @@ describe('LangChain integration', () => { createEsmAndCjsTests(__dirname, 'scenario-tools.mjs', 'instrument.mjs', (createRunner, test) => { test('creates langchain spans with tool calls', async () => { await createRunner() - .expect({ transaction: { transaction: 'main' } }) .expect({ span: container => { - expect(container.items).toHaveLength(1); - const [firstSpan] = container.items; + 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.langchain'); + expect(spans).toHaveLength(1); + const [firstSpan] = spans; // [0] chat with tool_use stop reason expect(firstSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); @@ -198,20 +187,22 @@ describe('LangChain integration', () => { createEsmTests(__dirname, 'scenario-openai-before-langchain.mjs', 'instrument.mjs', (createRunner, test) => { test('demonstrates timing issue with duplicate spans', async () => { await createRunner() - .expect({ transaction: { transaction: 'main' } }) .expect({ span: container => { - expect(container.items).toHaveLength(2); - const anthropicSpan = container.items.find( - span => span.attributes['sentry.origin'].value === 'auto.ai.anthropic', + 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.langchain' || + span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.anthropic', ); + expect(spans).toHaveLength(2); + const anthropicSpan = spans.find(span => span.attributes['sentry.origin'].value === 'auto.ai.anthropic'); expect(anthropicSpan).toBeDefined(); expect(anthropicSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); // LangChain call is instrumented by LangChain. - const langchainSpan = container.items.find( - span => span.attributes['sentry.origin'].value === 'auto.ai.langchain', - ); + const langchainSpan = spans.find(span => span.attributes['sentry.origin'].value === 'auto.ai.langchain'); expect(langchainSpan).toBeDefined(); expect(langchainSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); @@ -232,11 +223,15 @@ describe('LangChain 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 [firstSpan] = container.items; + 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.langchain', + ); + expect(spans).toHaveLength(1); + const [firstSpan] = spans; // [0] chat with extracted system instructions expect(firstSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); @@ -255,18 +250,20 @@ describe('LangChain integration', () => { createEsmAndCjsTests(__dirname, 'scenario-chain.mjs', 'instrument.mjs', (createRunner, test) => { test('uses runName for chain spans instead of unknown_chain', async () => { await createRunner() - .expect({ transaction: { transaction: 'main' } }) .expect({ span: container => { - expect(container.items).toHaveLength(4); - expect(container.items.map(span => span.name).sort()).toEqual([ - 'chain format_prompt', - 'chain parse_output', - 'chain unknown_chain', + 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.langchain'); + expect(spans).toHaveLength(4); + expect(spans.map(span => span.name).sort()).toEqual([ 'chat claude-3-5-sonnet-20241022', + 'invoke_agent', + 'invoke_agent format_prompt', + 'invoke_agent parse_output', ]); - const formatPromptSpan = container.items.find(span => span.name === 'chain format_prompt'); + const formatPromptSpan = spans.find(span => span.name === 'invoke_agent format_prompt'); expect(formatPromptSpan).toBeDefined(); expect(formatPromptSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); expect(formatPromptSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); @@ -274,21 +271,23 @@ describe('LangChain integration', () => { expect(formatPromptSpan!.attributes[GEN_AI_PIPELINE_NAME].value).toBe('format_prompt'); expect(formatPromptSpan!.attributes['langchain.chain.name']).toBeUndefined(); - const chatSpan = container.items.find(span => span.name === 'chat claude-3-5-sonnet-20241022'); + const chatSpan = spans.find(span => span.name === 'chat claude-3-5-sonnet-20241022'); expect(chatSpan).toBeDefined(); expect(chatSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); expect(chatSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); - const parseOutputSpan = container.items.find(span => span.name === 'chain parse_output'); + const parseOutputSpan = spans.find(span => span.name === 'invoke_agent parse_output'); expect(parseOutputSpan).toBeDefined(); expect(parseOutputSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); expect(parseOutputSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); + expect(parseOutputSpan!.attributes[GEN_AI_OPERATION_NAME].value).toBe('invoke_agent'); expect(parseOutputSpan!.attributes[GEN_AI_PIPELINE_NAME].value).toBe('parse_output'); expect(parseOutputSpan!.attributes['langchain.chain.name']).toBeUndefined(); - const unknownChainSpan = container.items.find(span => span.name === 'chain unknown_chain'); + const unknownChainSpan = spans.find(span => span.name === 'invoke_agent'); expect(unknownChainSpan).toBeDefined(); expect(unknownChainSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(unknownChainSpan!.attributes[GEN_AI_OPERATION_NAME].value).toBe('invoke_agent'); expect(unknownChainSpan!.attributes[GEN_AI_PIPELINE_NAME]).toBeUndefined(); expect(unknownChainSpan!.attributes['langchain.chain.name']).toBeUndefined(); }, @@ -305,17 +304,19 @@ describe('LangChain integration', () => { createEsmAndCjsTests(__dirname, 'scenario-embeddings.mjs', 'instrument.mjs', (createRunner, test) => { test('creates embedding spans with genAI recording disabled', async () => { await createRunner() - .expect({ transaction: { transaction: 'main' } }) .expect({ span: container => { - expect(container.items).toHaveLength(3); - expect(container.items.map(span => span.name).sort()).toEqual([ + 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.langchain'); + expect(spans).toHaveLength(3); + expect(spans.map(span => span.name).sort()).toEqual([ 'embeddings error-model', 'embeddings text-embedding-3-small', 'embeddings text-embedding-3-small', ]); - const successfulSpans = container.items.filter( + const successfulSpans = spans.filter( span => span.name === 'embeddings text-embedding-3-small' && span.status === 'ok', ); expect(successfulSpans).toHaveLength(2); @@ -328,7 +329,7 @@ describe('LangChain integration', () => { expect(span.attributes[GEN_AI_REQUEST_DIMENSIONS_ATTRIBUTE].value).toBe(1536); } - const errorSpan = container.items.find(span => span.name === 'embeddings error-model'); + const errorSpan = spans.find(span => span.name === 'embeddings error-model'); expect(errorSpan).toBeDefined(); expect(errorSpan!.status).toBe('error'); expect(errorSpan!.attributes['sentry.op'].value).toBe(GEN_AI_EMBEDDINGS); @@ -338,46 +339,30 @@ describe('LangChain integration', () => { .start() .completed(); }); - - test('does not create duplicate embedding spans from double module patching', async () => { - await createRunner() - .expect({ transaction: { transaction: 'main' } }) - .expect({ - span: container => { - // The scenario makes 3 embedding calls (2 successful + 1 error). - expect(container.items).toHaveLength(3); - for (const span of container.items) { - expect(span.attributes['sentry.op'].value).toBe(GEN_AI_EMBEDDINGS); - } - }, - }) - .start() - .completed(); - }); }); createEsmAndCjsTests(__dirname, 'scenario-embeddings.mjs', 'instrument-with-pii.mjs', (createRunner, test) => { test('creates embedding spans with genAI recording enabled', async () => { await createRunner() - .expect({ transaction: { transaction: 'main' } }) .expect({ span: container => { - expect(container.items).toHaveLength(3); - expect(container.items.map(span => span.name).sort()).toEqual([ + 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.langchain'); + expect(spans).toHaveLength(3); + expect(spans.map(span => span.name).sort()).toEqual([ 'embeddings error-model', 'embeddings text-embedding-3-small', 'embeddings text-embedding-3-small', ]); - const querySpan = container.items.find( - span => span.attributes[GEN_AI_EMBEDDINGS_INPUT]?.value === 'Hello world', - ); + const querySpan = spans.find(span => span.attributes[GEN_AI_EMBEDDINGS_INPUT]?.value === 'Hello world'); expect(querySpan).toBeDefined(); expect(querySpan!.name).toBe('embeddings text-embedding-3-small'); expect(querySpan!.status).toBe('ok'); expect(querySpan!.attributes[GEN_AI_REQUEST_DIMENSIONS_ATTRIBUTE].value).toBe(1536); - const documentsSpan = container.items.find( + const documentsSpan = spans.find( span => span.attributes[GEN_AI_EMBEDDINGS_INPUT]?.value === JSON.stringify(['First document', 'Second document']), @@ -386,7 +371,7 @@ describe('LangChain integration', () => { expect(documentsSpan!.name).toBe('embeddings text-embedding-3-small'); expect(documentsSpan!.status).toBe('ok'); - const errorSpan = container.items.find(span => span.name === 'embeddings error-model'); + const errorSpan = spans.find(span => span.name === 'embeddings error-model'); expect(errorSpan).toBeDefined(); expect(errorSpan!.status).toBe('error'); }, @@ -395,59 +380,4 @@ describe('LangChain integration', () => { .completed(); }); }); - - createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument-span-streaming.mjs', (createRunner, test) => { - test('creates langchain related spans with span streaming enabled', async () => { - await createRunner() - .expect({ - span: container => { - const sonnetSpan = container.items.find(span => span.name === 'chat claude-3-5-sonnet-20241022'); - expect(sonnetSpan).toBeDefined(); - expect(sonnetSpan!.status).toBe('ok'); - expect(sonnetSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); - expect(sonnetSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); - expect(sonnetSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); - expect(sonnetSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('claude-3-5-sonnet-20241022'); - expect(sonnetSpan!.attributes[GEN_AI_INPUT_MESSAGES]).toBeDefined(); - }, - }) - .start() - .completed(); - }); - }); - - createEsmAndCjsTests(__dirname, 'scenario-chain.mjs', 'instrument-span-streaming.mjs', (createRunner, test) => { - test('leads chain span names with the operation 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', - ); - // The `unknown_chain` sentinel is dropped, so that span falls back to the bare operation. - expect(chainSpans.map(span => span.name).sort()).toEqual([ - 'invoke_agent', - 'invoke_agent format_prompt', - 'invoke_agent parse_output', - ]); - for (const span of chainSpans) { - expect(span.attributes[GEN_AI_OPERATION_NAME]?.value).toBe('invoke_agent'); - } - expect( - chainSpans - .map(span => span.attributes[GEN_AI_PIPELINE_NAME]?.value) - .filter(Boolean) - .sort(), - ).toEqual(['format_prompt', 'parse_output']); - const unnamedChainSpan = chainSpans.find(span => span.name === 'invoke_agent'); - expect(unnamedChainSpan).toBeDefined(); - expect(unnamedChainSpan!.attributes[GEN_AI_PIPELINE_NAME]).toBeUndefined(); - expect(unnamedChainSpan!.attributes['langchain.chain.name']).toBeUndefined(); - }, - }) - .start() - .completed(); - }); - }); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/v1/instrument-with-pii.mjs b/dev-packages/node-integration-tests/suites/tracing/langchain/v1/instrument-with-pii.mjs index a220d64b148a..1c507ba84b9c 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/v1/instrument-with-pii.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/langchain/v1/instrument-with-pii.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.includes('/v1/messages') || event.transaction.includes('/v1/chat/completions')) { - return null; - } - return event; - }, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/v1/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/langchain/v1/instrument.mjs index 8a2ba440b758..cc192fb89834 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/v1/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/langchain/v1/instrument.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: false, outputs: false } }, transport: loggingTransport, - beforeSendTransaction: event => { - // Filter out mock express server transactions - if (event.transaction.includes('/v1/messages') || event.transaction.includes('/v1/chat/completions')) { - return null; - } - return event; - }, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/v1/test.ts b/dev-packages/node-integration-tests/suites/tracing/langchain/v1/test.ts index c451bfe7bd2d..0a695ef563e5 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/v1/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/langchain/v1/test.ts @@ -14,6 +14,7 @@ import { GEN_AI_USAGE_INPUT_TOKENS, GEN_AI_USAGE_OUTPUT_TOKENS, GEN_AI_USAGE_TOTAL_TOKENS, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE } from '../../../../../../packages/server-utils/src/ai/core/gen-ai-attributes'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../../utils/runner'; @@ -32,17 +33,21 @@ describe('LangChain integration (v1)', () => { test('creates langchain related spans with genAI recording disabled', async () => { await createRunner() .ignore('event') - .expect({ transaction: { transaction: 'main' } }) .expect({ span: container => { - expect(container.items).toHaveLength(3); - expect(container.items.map(span => span.name).sort()).toEqual([ + 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.langchain', + ); + expect(spans).toHaveLength(3); + expect(spans.map(span => span.name).sort()).toEqual([ 'chat claude-3-5-sonnet-20241022', 'chat claude-3-opus-20240229', 'chat error-model', ]); - const sonnetSpan = container.items.find(span => span.name === 'chat claude-3-5-sonnet-20241022'); + const sonnetSpan = spans.find(span => span.name === 'chat claude-3-5-sonnet-20241022'); expect(sonnetSpan).toBeDefined(); expect(sonnetSpan!.status).toBe('ok'); expect(sonnetSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); @@ -59,7 +64,7 @@ describe('LangChain integration (v1)', () => { expect(sonnetSpan!.attributes[GEN_AI_RESPONSE_MODEL]).toBeDefined(); expect(sonnetSpan!.attributes[GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE]).toBeDefined(); - const opusSpan = container.items.find(span => span.name === 'chat claude-3-opus-20240229'); + const opusSpan = spans.find(span => span.name === 'chat claude-3-opus-20240229'); expect(opusSpan).toBeDefined(); expect(opusSpan!.status).toBe('ok'); expect(opusSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); @@ -73,7 +78,7 @@ describe('LangChain integration (v1)', () => { expect(opusSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS].value).toBe(15); expect(opusSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS].value).toBe(25); - const errorSpan = container.items.find(span => span.name === 'chat error-model'); + const errorSpan = spans.find(span => span.name === 'chat error-model'); expect(errorSpan).toBeDefined(); expect(errorSpan!.status).toBe('error'); expect(errorSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); @@ -103,17 +108,21 @@ describe('LangChain integration (v1)', () => { test('creates langchain related spans with genAI recording enabled', async () => { await createRunner() .ignore('event') - .expect({ transaction: { transaction: 'main' } }) .expect({ span: container => { - expect(container.items).toHaveLength(3); - expect(container.items.map(span => span.name).sort()).toEqual([ + 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.langchain', + ); + expect(spans).toHaveLength(3); + expect(spans.map(span => span.name).sort()).toEqual([ 'chat claude-3-5-sonnet-20241022', 'chat claude-3-opus-20240229', 'chat error-model', ]); - const sonnetSpan = container.items.find(span => span.name === 'chat claude-3-5-sonnet-20241022'); + const sonnetSpan = spans.find(span => span.name === 'chat claude-3-5-sonnet-20241022'); expect(sonnetSpan).toBeDefined(); expect(sonnetSpan!.status).toBe('ok'); expect(sonnetSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); @@ -131,7 +140,7 @@ describe('LangChain integration (v1)', () => { expect(sonnetSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS].value).toBe(15); expect(sonnetSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS].value).toBe(25); - const opusSpan = container.items.find(span => span.name === 'chat claude-3-opus-20240229'); + const opusSpan = spans.find(span => span.name === 'chat claude-3-opus-20240229'); expect(opusSpan).toBeDefined(); expect(opusSpan!.status).toBe('ok'); expect(opusSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); @@ -145,7 +154,7 @@ describe('LangChain integration (v1)', () => { expect(opusSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS].value).toBe(15); expect(opusSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS].value).toBe(25); - const errorSpan = container.items.find(span => span.name === 'chat error-model'); + const errorSpan = spans.find(span => span.name === 'chat error-model'); expect(errorSpan).toBeDefined(); expect(errorSpan!.status).toBe('error'); expect(errorSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); @@ -174,11 +183,15 @@ describe('LangChain integration (v1)', () => { test('creates langchain spans with tool calls', async () => { await createRunner() .ignore('event') - .expect({ transaction: { transaction: 'main' } }) .expect({ span: container => { - expect(container.items).toHaveLength(1); - const [firstSpan] = container.items; + 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.langchain', + ); + expect(spans).toHaveLength(1); + const [firstSpan] = spans; // [0] chat with tool_use stop reason expect(firstSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); @@ -217,19 +230,21 @@ describe('LangChain integration (v1)', () => { test('demonstrates timing issue with duplicate spans', async () => { await createRunner() .ignore('event') - .expect({ transaction: { transaction: 'main' } }) .expect({ span: container => { - expect(container.items).toHaveLength(2); - const anthropicSpan = container.items.find( - span => span.attributes['sentry.origin'].value === 'auto.ai.anthropic', + 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.langchain' || + span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.anthropic', ); + expect(spans).toHaveLength(2); + const anthropicSpan = spans.find(span => span.attributes['sentry.origin'].value === 'auto.ai.anthropic'); expect(anthropicSpan).toBeDefined(); expect(anthropicSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); - const langchainSpan = container.items.find( - span => span.attributes['sentry.origin'].value === 'auto.ai.langchain', - ); + const langchainSpan = spans.find(span => span.attributes['sentry.origin'].value === 'auto.ai.langchain'); expect(langchainSpan).toBeDefined(); expect(langchainSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); @@ -258,17 +273,21 @@ describe('LangChain integration (v1)', () => { test('creates langchain spans using initChatModel with OpenAI', async () => { await createRunner() .ignore('event') - .expect({ transaction: { transaction: 'main' } }) .expect({ span: container => { - expect(container.items).toHaveLength(3); - expect(container.items.map(span => span.name).sort()).toEqual([ + 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.langchain', + ); + expect(spans).toHaveLength(3); + expect(spans.map(span => span.name).sort()).toEqual([ 'chat error-model', 'chat gpt-3.5-turbo', 'chat gpt-4o', ]); - const gpt4oSpan = container.items.find(span => span.name === 'chat gpt-4o'); + const gpt4oSpan = spans.find(span => span.name === 'chat gpt-4o'); expect(gpt4oSpan).toBeDefined(); expect(gpt4oSpan!.status).toBe('ok'); expect(gpt4oSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); @@ -285,7 +304,7 @@ describe('LangChain integration (v1)', () => { expect(gpt4oSpan!.attributes[GEN_AI_RESPONSE_MODEL].value).toBe('gpt-4o'); expect(gpt4oSpan!.attributes[GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE].value).toBe('stop'); - const gpt35Span = container.items.find(span => span.name === 'chat gpt-3.5-turbo'); + const gpt35Span = spans.find(span => span.name === 'chat gpt-3.5-turbo'); expect(gpt35Span).toBeDefined(); expect(gpt35Span!.status).toBe('ok'); expect(gpt35Span!.attributes['sentry.op'].value).toBe('gen_ai.chat'); @@ -299,7 +318,7 @@ describe('LangChain integration (v1)', () => { expect(gpt35Span!.attributes[GEN_AI_RESPONSE_MODEL].value).toBe('gpt-3.5-turbo'); expect(gpt35Span!.attributes[GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE].value).toBe('stop'); - const errorSpan = container.items.find(span => span.name === 'chat error-model'); + const errorSpan = spans.find(span => span.name === 'chat error-model'); expect(errorSpan).toBeDefined(); expect(errorSpan!.status).toBe('error'); expect(errorSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); From fb768a768d63bd4d5006209b39e5715c4028d2ee Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Tue, 6 Oct 2026 11:25:04 +0200 Subject: [PATCH 2/8] test(node): Remove redundant langchain assertions Co-Authored-By: GPT-6 --- .../suites/tracing/langchain/test.ts | 16 ---------------- .../suites/tracing/langchain/v1/test.ts | 11 ----------- 2 files changed, 27 deletions(-) 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 c88dd623094e..c90b0dc4d49e 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/langchain/test.ts @@ -48,7 +48,6 @@ describe('LangChain integration', () => { ]); const sonnetSpan = spans.find(span => span.name === 'chat claude-3-5-sonnet-20241022'); - expect(sonnetSpan).toBeDefined(); expect(sonnetSpan!.status).toBe('ok'); expect(sonnetSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); expect(sonnetSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); @@ -65,7 +64,6 @@ describe('LangChain integration', () => { expect(sonnetSpan!.attributes[GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE]).toBeDefined(); const opusSpan = spans.find(span => span.name === 'chat claude-3-opus-20240229'); - expect(opusSpan).toBeDefined(); expect(opusSpan!.status).toBe('ok'); expect(opusSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); expect(opusSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); @@ -79,7 +77,6 @@ describe('LangChain integration', () => { expect(opusSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS].value).toBe(25); const errorSpan = spans.find(span => span.name === 'chat error-model'); - expect(errorSpan).toBeDefined(); expect(errorSpan!.status).toBe('error'); expect(errorSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); expect(errorSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); @@ -108,7 +105,6 @@ describe('LangChain integration', () => { ]); const sonnetSpan = spans.find(span => span.name === 'chat claude-3-5-sonnet-20241022'); - expect(sonnetSpan).toBeDefined(); expect(sonnetSpan!.status).toBe('ok'); expect(sonnetSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); expect(sonnetSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); @@ -126,7 +122,6 @@ describe('LangChain integration', () => { expect(sonnetSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS].value).toBe(25); const opusSpan = spans.find(span => span.name === 'chat claude-3-opus-20240229'); - expect(opusSpan).toBeDefined(); expect(opusSpan!.status).toBe('ok'); expect(opusSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); expect(opusSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('claude-3-opus-20240229'); @@ -140,7 +135,6 @@ describe('LangChain integration', () => { expect(opusSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS].value).toBe(25); const errorSpan = spans.find(span => span.name === 'chat error-model'); - expect(errorSpan).toBeDefined(); expect(errorSpan!.status).toBe('error'); expect(errorSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); expect(errorSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('error-model'); @@ -198,12 +192,10 @@ describe('LangChain integration', () => { ); expect(spans).toHaveLength(2); const anthropicSpan = spans.find(span => span.attributes['sentry.origin'].value === 'auto.ai.anthropic'); - expect(anthropicSpan).toBeDefined(); expect(anthropicSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); // LangChain call is instrumented by LangChain. const langchainSpan = spans.find(span => span.attributes['sentry.origin'].value === 'auto.ai.langchain'); - expect(langchainSpan).toBeDefined(); expect(langchainSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); // Third call (not present): Direct Anthropic call made AFTER LangChain import @@ -264,7 +256,6 @@ describe('LangChain integration', () => { ]); const formatPromptSpan = spans.find(span => span.name === 'invoke_agent format_prompt'); - 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'); @@ -272,12 +263,10 @@ describe('LangChain integration', () => { expect(formatPromptSpan!.attributes['langchain.chain.name']).toBeUndefined(); const chatSpan = spans.find(span => span.name === 'chat claude-3-5-sonnet-20241022'); - expect(chatSpan).toBeDefined(); expect(chatSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); expect(chatSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); const parseOutputSpan = spans.find(span => span.name === 'invoke_agent parse_output'); - expect(parseOutputSpan).toBeDefined(); expect(parseOutputSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); expect(parseOutputSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); expect(parseOutputSpan!.attributes[GEN_AI_OPERATION_NAME].value).toBe('invoke_agent'); @@ -285,7 +274,6 @@ describe('LangChain integration', () => { expect(parseOutputSpan!.attributes['langchain.chain.name']).toBeUndefined(); const unknownChainSpan = spans.find(span => span.name === 'invoke_agent'); - expect(unknownChainSpan).toBeDefined(); expect(unknownChainSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); expect(unknownChainSpan!.attributes[GEN_AI_OPERATION_NAME].value).toBe('invoke_agent'); expect(unknownChainSpan!.attributes[GEN_AI_PIPELINE_NAME]).toBeUndefined(); @@ -330,7 +318,6 @@ describe('LangChain integration', () => { } const errorSpan = spans.find(span => span.name === 'embeddings error-model'); - expect(errorSpan).toBeDefined(); expect(errorSpan!.status).toBe('error'); expect(errorSpan!.attributes['sentry.op'].value).toBe(GEN_AI_EMBEDDINGS); expect(errorSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('openai'); @@ -357,7 +344,6 @@ describe('LangChain integration', () => { ]); const querySpan = spans.find(span => span.attributes[GEN_AI_EMBEDDINGS_INPUT]?.value === 'Hello world'); - expect(querySpan).toBeDefined(); expect(querySpan!.name).toBe('embeddings text-embedding-3-small'); expect(querySpan!.status).toBe('ok'); expect(querySpan!.attributes[GEN_AI_REQUEST_DIMENSIONS_ATTRIBUTE].value).toBe(1536); @@ -367,12 +353,10 @@ describe('LangChain integration', () => { span.attributes[GEN_AI_EMBEDDINGS_INPUT]?.value === JSON.stringify(['First document', 'Second document']), ); - expect(documentsSpan).toBeDefined(); expect(documentsSpan!.name).toBe('embeddings text-embedding-3-small'); expect(documentsSpan!.status).toBe('ok'); const errorSpan = spans.find(span => span.name === 'embeddings error-model'); - expect(errorSpan).toBeDefined(); expect(errorSpan!.status).toBe('error'); }, }) diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/v1/test.ts b/dev-packages/node-integration-tests/suites/tracing/langchain/v1/test.ts index 0a695ef563e5..f6bce8def5f2 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/v1/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/langchain/v1/test.ts @@ -48,7 +48,6 @@ describe('LangChain integration (v1)', () => { ]); const sonnetSpan = spans.find(span => span.name === 'chat claude-3-5-sonnet-20241022'); - expect(sonnetSpan).toBeDefined(); expect(sonnetSpan!.status).toBe('ok'); expect(sonnetSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); expect(sonnetSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); @@ -65,7 +64,6 @@ describe('LangChain integration (v1)', () => { expect(sonnetSpan!.attributes[GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE]).toBeDefined(); const opusSpan = spans.find(span => span.name === 'chat claude-3-opus-20240229'); - expect(opusSpan).toBeDefined(); expect(opusSpan!.status).toBe('ok'); expect(opusSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); expect(opusSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); @@ -79,7 +77,6 @@ describe('LangChain integration (v1)', () => { expect(opusSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS].value).toBe(25); const errorSpan = spans.find(span => span.name === 'chat error-model'); - expect(errorSpan).toBeDefined(); expect(errorSpan!.status).toBe('error'); expect(errorSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); expect(errorSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); @@ -123,7 +120,6 @@ describe('LangChain integration (v1)', () => { ]); const sonnetSpan = spans.find(span => span.name === 'chat claude-3-5-sonnet-20241022'); - expect(sonnetSpan).toBeDefined(); expect(sonnetSpan!.status).toBe('ok'); expect(sonnetSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); expect(sonnetSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); @@ -141,7 +137,6 @@ describe('LangChain integration (v1)', () => { expect(sonnetSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS].value).toBe(25); const opusSpan = spans.find(span => span.name === 'chat claude-3-opus-20240229'); - expect(opusSpan).toBeDefined(); expect(opusSpan!.status).toBe('ok'); expect(opusSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); expect(opusSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('claude-3-opus-20240229'); @@ -155,7 +150,6 @@ describe('LangChain integration (v1)', () => { expect(opusSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS].value).toBe(25); const errorSpan = spans.find(span => span.name === 'chat error-model'); - expect(errorSpan).toBeDefined(); expect(errorSpan!.status).toBe('error'); expect(errorSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); expect(errorSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('error-model'); @@ -241,11 +235,9 @@ describe('LangChain integration (v1)', () => { ); expect(spans).toHaveLength(2); const anthropicSpan = spans.find(span => span.attributes['sentry.origin'].value === 'auto.ai.anthropic'); - expect(anthropicSpan).toBeDefined(); expect(anthropicSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); const langchainSpan = spans.find(span => span.attributes['sentry.origin'].value === 'auto.ai.langchain'); - expect(langchainSpan).toBeDefined(); expect(langchainSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); // Third call (not present): Direct Anthropic call made AFTER LangChain import @@ -288,7 +280,6 @@ describe('LangChain integration (v1)', () => { ]); const gpt4oSpan = spans.find(span => span.name === 'chat gpt-4o'); - expect(gpt4oSpan).toBeDefined(); expect(gpt4oSpan!.status).toBe('ok'); expect(gpt4oSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); expect(gpt4oSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); @@ -305,7 +296,6 @@ describe('LangChain integration (v1)', () => { expect(gpt4oSpan!.attributes[GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE].value).toBe('stop'); const gpt35Span = spans.find(span => span.name === 'chat gpt-3.5-turbo'); - expect(gpt35Span).toBeDefined(); expect(gpt35Span!.status).toBe('ok'); expect(gpt35Span!.attributes['sentry.op'].value).toBe('gen_ai.chat'); expect(gpt35Span!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); @@ -319,7 +309,6 @@ describe('LangChain integration (v1)', () => { expect(gpt35Span!.attributes[GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE].value).toBe('stop'); const errorSpan = spans.find(span => span.name === 'chat error-model'); - expect(errorSpan).toBeDefined(); expect(errorSpan!.status).toBe('error'); expect(errorSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); expect(errorSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); From 3e3412c92f8f2b24353921842b0acbaf9c5f55a8 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Tue, 6 Oct 2026 11:53:30 +0200 Subject: [PATCH 3/8] test(node): Collect langchain spans across envelopes Co-Authored-By: GPT-6 --- .../suites/tracing/langchain/test.ts | 67 ++++++++++++++----- .../suites/tracing/langchain/v1/test.ts | 49 +++++++++----- 2 files changed, 80 insertions(+), 36 deletions(-) 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 c90b0dc4d49e..00008628922d 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/langchain/test.ts @@ -1,3 +1,4 @@ +import type { SerializedStreamedSpanContainer } from '@sentry/core'; import { afterAll, describe, expect } from 'vitest'; import { GEN_AI_EMBEDDINGS_INPUT, @@ -34,12 +35,16 @@ describe('LangChain integration', () => { createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => { test('creates langchain related spans with genAI recording disabled', 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 spans = container.items.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.langchain'); + const spans = allSpans.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.langchain'); expect(spans).toHaveLength(3); expect(spans.map(span => span.name).sort()).toEqual([ 'chat claude-3-5-sonnet-20241022', @@ -91,12 +96,16 @@ describe('LangChain integration', () => { createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument-with-pii.mjs', (createRunner, test) => { test('creates langchain related spans with genAI recording enabled', 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 spans = container.items.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.langchain'); + const spans = allSpans.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.langchain'); expect(spans).toHaveLength(3); expect(spans.map(span => span.name).sort()).toEqual([ 'chat claude-3-5-sonnet-20241022', @@ -148,12 +157,16 @@ describe('LangChain integration', () => { createEsmAndCjsTests(__dirname, 'scenario-tools.mjs', 'instrument.mjs', (createRunner, test) => { test('creates langchain spans with tool calls', 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 spans = container.items.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.langchain'); + const spans = allSpans.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.langchain'); expect(spans).toHaveLength(1); const [firstSpan] = spans; @@ -180,12 +193,16 @@ describe('LangChain integration', () => { createEsmTests(__dirname, 'scenario-openai-before-langchain.mjs', 'instrument.mjs', (createRunner, test) => { test('demonstrates timing issue with duplicate spans', 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 spans = container.items.filter( + const spans = allSpans.filter( span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.langchain' || span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.anthropic', @@ -214,14 +231,16 @@ describe('LangChain integration', () => { 'instrument-with-pii.mjs', (createRunner, test) => { test('extracts system instructions from messages', 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 spans = container.items.filter( - span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.langchain', - ); + const spans = allSpans.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.langchain'); expect(spans).toHaveLength(1); const [firstSpan] = spans; @@ -241,12 +260,16 @@ describe('LangChain integration', () => { createEsmAndCjsTests(__dirname, 'scenario-chain.mjs', 'instrument.mjs', (createRunner, test) => { test('uses runName for chain spans instead of unknown_chain', 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 spans = container.items.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.langchain'); + const spans = allSpans.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.langchain'); expect(spans).toHaveLength(4); expect(spans.map(span => span.name).sort()).toEqual([ 'chat claude-3-5-sonnet-20241022', @@ -291,12 +314,16 @@ describe('LangChain integration', () => { createEsmAndCjsTests(__dirname, 'scenario-embeddings.mjs', 'instrument.mjs', (createRunner, test) => { test('creates embedding spans with genAI recording disabled', 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 spans = container.items.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.langchain'); + const spans = allSpans.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.langchain'); expect(spans).toHaveLength(3); expect(spans.map(span => span.name).sort()).toEqual([ 'embeddings error-model', @@ -330,12 +357,16 @@ describe('LangChain integration', () => { createEsmAndCjsTests(__dirname, 'scenario-embeddings.mjs', 'instrument-with-pii.mjs', (createRunner, test) => { test('creates embedding spans with genAI recording enabled', 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 spans = container.items.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.langchain'); + const spans = allSpans.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.langchain'); expect(spans).toHaveLength(3); expect(spans.map(span => span.name).sort()).toEqual([ 'embeddings error-model', diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/v1/test.ts b/dev-packages/node-integration-tests/suites/tracing/langchain/v1/test.ts index f6bce8def5f2..a95466779068 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/v1/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/langchain/v1/test.ts @@ -1,3 +1,4 @@ +import type { SerializedStreamedSpanContainer } from '@sentry/core'; import { afterAll, describe, expect } from 'vitest'; import { GEN_AI_INPUT_MESSAGES, @@ -31,15 +32,17 @@ describe('LangChain integration (v1)', () => { 'instrument.mjs', (createRunner, test) => { test('creates langchain related spans with genAI recording disabled', async () => { + const allSpans: SerializedStreamedSpanContainer['items'] = []; + await createRunner() + .unordered() .ignore('event') .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 spans = container.items.filter( - span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.langchain', - ); + const spans = allSpans.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.langchain'); expect(spans).toHaveLength(3); expect(spans.map(span => span.name).sort()).toEqual([ 'chat claude-3-5-sonnet-20241022', @@ -103,15 +106,17 @@ describe('LangChain integration (v1)', () => { 'instrument-with-pii.mjs', (createRunner, test) => { test('creates langchain related spans with genAI recording enabled', async () => { + const allSpans: SerializedStreamedSpanContainer['items'] = []; + await createRunner() + .unordered() .ignore('event') .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 spans = container.items.filter( - span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.langchain', - ); + const spans = allSpans.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.langchain'); expect(spans).toHaveLength(3); expect(spans.map(span => span.name).sort()).toEqual([ 'chat claude-3-5-sonnet-20241022', @@ -175,15 +180,17 @@ describe('LangChain integration (v1)', () => { 'instrument.mjs', (createRunner, test) => { test('creates langchain spans with tool calls', async () => { + const allSpans: SerializedStreamedSpanContainer['items'] = []; + await createRunner() + .unordered() .ignore('event') .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 spans = container.items.filter( - span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.langchain', - ); + const spans = allSpans.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.langchain'); expect(spans).toHaveLength(1); const [firstSpan] = spans; @@ -222,13 +229,17 @@ describe('LangChain integration (v1)', () => { 'instrument.mjs', (createRunner, test) => { test('demonstrates timing issue with duplicate spans', async () => { + const allSpans: SerializedStreamedSpanContainer['items'] = []; + await createRunner() + .unordered() .ignore('event') .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 spans = container.items.filter( + const spans = allSpans.filter( span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.langchain' || span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.anthropic', @@ -263,15 +274,17 @@ describe('LangChain integration (v1)', () => { 'instrument.mjs', (createRunner, test) => { test('creates langchain spans using initChatModel with OpenAI', async () => { + const allSpans: SerializedStreamedSpanContainer['items'] = []; + await createRunner() + .unordered() .ignore('event') .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 spans = container.items.filter( - span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.langchain', - ); + const spans = allSpans.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.langchain'); expect(spans).toHaveLength(3); expect(spans.map(span => span.name).sort()).toEqual([ 'chat error-model', From c5fe38177aee0a84ada934cdaeef3c525f838c65 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Tue, 6 Oct 2026 12:06:45 +0200 Subject: [PATCH 4/8] test(node): Use the SENTRY_ORIGIN convention in langchain tests Co-Authored-By: GPT-6 --- .../suites/tracing/langchain/test.ts | 22 +++++++++---------- .../suites/tracing/langchain/v1/test.ts | 20 ++++++++--------- 2 files changed, 21 insertions(+), 21 deletions(-) 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 00008628922d..c4066536c577 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/langchain/test.ts @@ -55,7 +55,7 @@ describe('LangChain integration', () => { const sonnetSpan = spans.find(span => span.name === 'chat claude-3-5-sonnet-20241022'); expect(sonnetSpan!.status).toBe('ok'); expect(sonnetSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); - expect(sonnetSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); + expect(sonnetSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); expect(sonnetSpan!.attributes[GEN_AI_OPERATION_NAME].value).toBe('chat'); expect(sonnetSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); expect(sonnetSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('claude-3-5-sonnet-20241022'); @@ -71,7 +71,7 @@ describe('LangChain integration', () => { const opusSpan = spans.find(span => span.name === 'chat claude-3-opus-20240229'); expect(opusSpan!.status).toBe('ok'); expect(opusSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); - expect(opusSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); + expect(opusSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); expect(opusSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); expect(opusSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('claude-3-opus-20240229'); expect(opusSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE].value).toBe(0.9); @@ -84,7 +84,7 @@ describe('LangChain integration', () => { const errorSpan = spans.find(span => span.name === 'chat error-model'); expect(errorSpan!.status).toBe('error'); expect(errorSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); - expect(errorSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); + expect(errorSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); expect(errorSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); expect(errorSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('error-model'); }, @@ -116,7 +116,7 @@ describe('LangChain integration', () => { const sonnetSpan = spans.find(span => span.name === 'chat claude-3-5-sonnet-20241022'); expect(sonnetSpan!.status).toBe('ok'); expect(sonnetSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); - expect(sonnetSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); + expect(sonnetSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); expect(sonnetSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); expect(sonnetSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('claude-3-5-sonnet-20241022'); expect(sonnetSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE].value).toBe(0.7); @@ -174,7 +174,7 @@ describe('LangChain integration', () => { expect(firstSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); expect(firstSpan!.status).toBe('ok'); expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); - expect(firstSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); + expect(firstSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); expect(firstSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('claude-3-5-sonnet-20241022'); expect(firstSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE].value).toBe(0.7); @@ -208,11 +208,11 @@ describe('LangChain integration', () => { span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.anthropic', ); expect(spans).toHaveLength(2); - const anthropicSpan = spans.find(span => span.attributes['sentry.origin'].value === 'auto.ai.anthropic'); + const anthropicSpan = spans.find(span => span.attributes[SENTRY_ORIGIN].value === 'auto.ai.anthropic'); expect(anthropicSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); // LangChain call is instrumented by LangChain. - const langchainSpan = spans.find(span => span.attributes['sentry.origin'].value === 'auto.ai.langchain'); + const langchainSpan = spans.find(span => span.attributes[SENTRY_ORIGIN].value === 'auto.ai.langchain'); expect(langchainSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); // Third call (not present): Direct Anthropic call made AFTER LangChain import @@ -280,18 +280,18 @@ describe('LangChain integration', () => { const formatPromptSpan = spans.find(span => span.name === 'invoke_agent format_prompt'); expect(formatPromptSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); - expect(formatPromptSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); + expect(formatPromptSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); expect(formatPromptSpan!.attributes[GEN_AI_OPERATION_NAME].value).toBe('invoke_agent'); expect(formatPromptSpan!.attributes[GEN_AI_PIPELINE_NAME].value).toBe('format_prompt'); expect(formatPromptSpan!.attributes['langchain.chain.name']).toBeUndefined(); const chatSpan = spans.find(span => span.name === 'chat claude-3-5-sonnet-20241022'); expect(chatSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); - expect(chatSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); + expect(chatSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); const parseOutputSpan = spans.find(span => span.name === 'invoke_agent parse_output'); expect(parseOutputSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); - expect(parseOutputSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); + expect(parseOutputSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); expect(parseOutputSpan!.attributes[GEN_AI_OPERATION_NAME].value).toBe('invoke_agent'); expect(parseOutputSpan!.attributes[GEN_AI_PIPELINE_NAME].value).toBe('parse_output'); expect(parseOutputSpan!.attributes['langchain.chain.name']).toBeUndefined(); @@ -337,7 +337,7 @@ describe('LangChain integration', () => { expect(successfulSpans).toHaveLength(2); for (const span of successfulSpans) { expect(span.attributes['sentry.op'].value).toBe(GEN_AI_EMBEDDINGS); - expect(span.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); + expect(span.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); expect(span.attributes[GEN_AI_OPERATION_NAME].value).toBe('embeddings'); expect(span.attributes[GEN_AI_PROVIDER_NAME].value).toBe('openai'); expect(span.attributes[GEN_AI_REQUEST_MODEL].value).toBe('text-embedding-3-small'); diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/v1/test.ts b/dev-packages/node-integration-tests/suites/tracing/langchain/v1/test.ts index a95466779068..c92f3c899b91 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/v1/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/langchain/v1/test.ts @@ -53,7 +53,7 @@ describe('LangChain integration (v1)', () => { const sonnetSpan = spans.find(span => span.name === 'chat claude-3-5-sonnet-20241022'); expect(sonnetSpan!.status).toBe('ok'); expect(sonnetSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); - expect(sonnetSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); + expect(sonnetSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); expect(sonnetSpan!.attributes[GEN_AI_OPERATION_NAME].value).toBe('chat'); expect(sonnetSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); expect(sonnetSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('claude-3-5-sonnet-20241022'); @@ -69,7 +69,7 @@ describe('LangChain integration (v1)', () => { const opusSpan = spans.find(span => span.name === 'chat claude-3-opus-20240229'); expect(opusSpan!.status).toBe('ok'); expect(opusSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); - expect(opusSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); + expect(opusSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); expect(opusSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); expect(opusSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('claude-3-opus-20240229'); expect(opusSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE].value).toBe(0.9); @@ -82,7 +82,7 @@ describe('LangChain integration (v1)', () => { const errorSpan = spans.find(span => span.name === 'chat error-model'); expect(errorSpan!.status).toBe('error'); expect(errorSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); - expect(errorSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); + expect(errorSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); expect(errorSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); expect(errorSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('error-model'); }, @@ -127,7 +127,7 @@ describe('LangChain integration (v1)', () => { const sonnetSpan = spans.find(span => span.name === 'chat claude-3-5-sonnet-20241022'); expect(sonnetSpan!.status).toBe('ok'); expect(sonnetSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); - expect(sonnetSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); + expect(sonnetSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); expect(sonnetSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); expect(sonnetSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('claude-3-5-sonnet-20241022'); expect(sonnetSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE].value).toBe(0.7); @@ -198,7 +198,7 @@ describe('LangChain integration (v1)', () => { expect(firstSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); expect(firstSpan!.status).toBe('ok'); expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); - expect(firstSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); + expect(firstSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); expect(firstSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('claude-3-5-sonnet-20241022'); expect(firstSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE].value).toBe(0.7); @@ -245,10 +245,10 @@ describe('LangChain integration (v1)', () => { span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.anthropic', ); expect(spans).toHaveLength(2); - const anthropicSpan = spans.find(span => span.attributes['sentry.origin'].value === 'auto.ai.anthropic'); + const anthropicSpan = spans.find(span => span.attributes[SENTRY_ORIGIN].value === 'auto.ai.anthropic'); expect(anthropicSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); - const langchainSpan = spans.find(span => span.attributes['sentry.origin'].value === 'auto.ai.langchain'); + const langchainSpan = spans.find(span => span.attributes[SENTRY_ORIGIN].value === 'auto.ai.langchain'); expect(langchainSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); // Third call (not present): Direct Anthropic call made AFTER LangChain import @@ -295,7 +295,7 @@ describe('LangChain integration (v1)', () => { const gpt4oSpan = spans.find(span => span.name === 'chat gpt-4o'); expect(gpt4oSpan!.status).toBe('ok'); expect(gpt4oSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); - expect(gpt4oSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); + expect(gpt4oSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); expect(gpt4oSpan!.attributes[GEN_AI_OPERATION_NAME].value).toBe('chat'); expect(gpt4oSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('openai'); expect(gpt4oSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('gpt-4o'); @@ -311,7 +311,7 @@ describe('LangChain integration (v1)', () => { const gpt35Span = spans.find(span => span.name === 'chat gpt-3.5-turbo'); expect(gpt35Span!.status).toBe('ok'); expect(gpt35Span!.attributes['sentry.op'].value).toBe('gen_ai.chat'); - expect(gpt35Span!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); + expect(gpt35Span!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); expect(gpt35Span!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('openai'); expect(gpt35Span!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('gpt-3.5-turbo'); expect(gpt35Span!.attributes[GEN_AI_REQUEST_TEMPERATURE].value).toBe(0.5); @@ -324,7 +324,7 @@ describe('LangChain integration (v1)', () => { const errorSpan = spans.find(span => span.name === 'chat error-model'); expect(errorSpan!.status).toBe('error'); expect(errorSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); - expect(errorSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); + expect(errorSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); expect(errorSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('openai'); expect(errorSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('error-model'); }, From f3c1054a7574c4f9618a4c836588158b8e658a8b Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Tue, 6 Oct 2026 12:12:42 +0200 Subject: [PATCH 5/8] test(node): Use convention constants for langchain span attributes Co-Authored-By: GPT-6 --- .../suites/tracing/langchain/test.ts | 32 ++++++++++--------- .../suites/tracing/langchain/v1/test.ts | 17 +++++----- 2 files changed, 26 insertions(+), 23 deletions(-) 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 c4066536c577..9d13b6170ae3 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/langchain/test.ts @@ -18,6 +18,8 @@ import { GEN_AI_USAGE_INPUT_TOKENS, GEN_AI_USAGE_OUTPUT_TOKENS, GEN_AI_USAGE_TOTAL_TOKENS, + LANGCHAIN_CHAIN_NAME, + SENTRY_OP, SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { GEN_AI_EMBEDDINGS } from '@sentry/conventions/op'; @@ -54,7 +56,7 @@ describe('LangChain integration', () => { const sonnetSpan = spans.find(span => span.name === 'chat claude-3-5-sonnet-20241022'); expect(sonnetSpan!.status).toBe('ok'); - expect(sonnetSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + expect(sonnetSpan!.attributes[SENTRY_OP].value).toBe('gen_ai.chat'); expect(sonnetSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); expect(sonnetSpan!.attributes[GEN_AI_OPERATION_NAME].value).toBe('chat'); expect(sonnetSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); @@ -70,7 +72,7 @@ describe('LangChain integration', () => { const opusSpan = spans.find(span => span.name === 'chat claude-3-opus-20240229'); expect(opusSpan!.status).toBe('ok'); - expect(opusSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + expect(opusSpan!.attributes[SENTRY_OP].value).toBe('gen_ai.chat'); expect(opusSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); expect(opusSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); expect(opusSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('claude-3-opus-20240229'); @@ -83,7 +85,7 @@ describe('LangChain integration', () => { const errorSpan = spans.find(span => span.name === 'chat error-model'); expect(errorSpan!.status).toBe('error'); - expect(errorSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + expect(errorSpan!.attributes[SENTRY_OP].value).toBe('gen_ai.chat'); expect(errorSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); expect(errorSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); expect(errorSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('error-model'); @@ -115,7 +117,7 @@ describe('LangChain integration', () => { const sonnetSpan = spans.find(span => span.name === 'chat claude-3-5-sonnet-20241022'); expect(sonnetSpan!.status).toBe('ok'); - expect(sonnetSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + expect(sonnetSpan!.attributes[SENTRY_OP].value).toBe('gen_ai.chat'); expect(sonnetSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); expect(sonnetSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); expect(sonnetSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('claude-3-5-sonnet-20241022'); @@ -173,7 +175,7 @@ describe('LangChain integration', () => { // [0] chat with tool_use stop reason expect(firstSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); expect(firstSpan!.status).toBe('ok'); - expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + expect(firstSpan!.attributes[SENTRY_OP].value).toBe('gen_ai.chat'); expect(firstSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); expect(firstSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('claude-3-5-sonnet-20241022'); @@ -246,7 +248,7 @@ describe('LangChain integration', () => { // [0] chat with extracted system instructions expect(firstSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); - expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + expect(firstSpan!.attributes[SENTRY_OP].value).toBe('gen_ai.chat'); expect(firstSpan!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS].value).toBe( JSON.stringify([{ type: 'text', content: 'You are a helpful assistant' }]), ); @@ -279,28 +281,28 @@ describe('LangChain integration', () => { ]); const formatPromptSpan = spans.find(span => span.name === 'invoke_agent format_prompt'); - expect(formatPromptSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + 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[GEN_AI_PIPELINE_NAME].value).toBe('format_prompt'); - expect(formatPromptSpan!.attributes['langchain.chain.name']).toBeUndefined(); + expect(formatPromptSpan!.attributes[LANGCHAIN_CHAIN_NAME]).toBeUndefined(); const chatSpan = spans.find(span => span.name === 'chat claude-3-5-sonnet-20241022'); - expect(chatSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + expect(chatSpan!.attributes[SENTRY_OP].value).toBe('gen_ai.chat'); expect(chatSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); const parseOutputSpan = spans.find(span => span.name === 'invoke_agent parse_output'); - expect(parseOutputSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(parseOutputSpan!.attributes[SENTRY_OP].value).toBe('gen_ai.invoke_agent'); expect(parseOutputSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); expect(parseOutputSpan!.attributes[GEN_AI_OPERATION_NAME].value).toBe('invoke_agent'); expect(parseOutputSpan!.attributes[GEN_AI_PIPELINE_NAME].value).toBe('parse_output'); - expect(parseOutputSpan!.attributes['langchain.chain.name']).toBeUndefined(); + expect(parseOutputSpan!.attributes[LANGCHAIN_CHAIN_NAME]).toBeUndefined(); const unknownChainSpan = spans.find(span => span.name === 'invoke_agent'); - expect(unknownChainSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(unknownChainSpan!.attributes[SENTRY_OP].value).toBe('gen_ai.invoke_agent'); expect(unknownChainSpan!.attributes[GEN_AI_OPERATION_NAME].value).toBe('invoke_agent'); expect(unknownChainSpan!.attributes[GEN_AI_PIPELINE_NAME]).toBeUndefined(); - expect(unknownChainSpan!.attributes['langchain.chain.name']).toBeUndefined(); + expect(unknownChainSpan!.attributes[LANGCHAIN_CHAIN_NAME]).toBeUndefined(); }, }) .start() @@ -336,7 +338,7 @@ describe('LangChain integration', () => { ); expect(successfulSpans).toHaveLength(2); for (const span of successfulSpans) { - expect(span.attributes['sentry.op'].value).toBe(GEN_AI_EMBEDDINGS); + expect(span.attributes[SENTRY_OP].value).toBe(GEN_AI_EMBEDDINGS); expect(span.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); expect(span.attributes[GEN_AI_OPERATION_NAME].value).toBe('embeddings'); expect(span.attributes[GEN_AI_PROVIDER_NAME].value).toBe('openai'); @@ -346,7 +348,7 @@ describe('LangChain integration', () => { const errorSpan = spans.find(span => span.name === 'embeddings error-model'); expect(errorSpan!.status).toBe('error'); - expect(errorSpan!.attributes['sentry.op'].value).toBe(GEN_AI_EMBEDDINGS); + expect(errorSpan!.attributes[SENTRY_OP].value).toBe(GEN_AI_EMBEDDINGS); expect(errorSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('openai'); }, }) diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/v1/test.ts b/dev-packages/node-integration-tests/suites/tracing/langchain/v1/test.ts index c92f3c899b91..e17ef7a549e8 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/v1/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/langchain/v1/test.ts @@ -15,6 +15,7 @@ import { GEN_AI_USAGE_INPUT_TOKENS, GEN_AI_USAGE_OUTPUT_TOKENS, GEN_AI_USAGE_TOTAL_TOKENS, + SENTRY_OP, SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE } from '../../../../../../packages/server-utils/src/ai/core/gen-ai-attributes'; @@ -52,7 +53,7 @@ describe('LangChain integration (v1)', () => { const sonnetSpan = spans.find(span => span.name === 'chat claude-3-5-sonnet-20241022'); expect(sonnetSpan!.status).toBe('ok'); - expect(sonnetSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + expect(sonnetSpan!.attributes[SENTRY_OP].value).toBe('gen_ai.chat'); expect(sonnetSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); expect(sonnetSpan!.attributes[GEN_AI_OPERATION_NAME].value).toBe('chat'); expect(sonnetSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); @@ -68,7 +69,7 @@ describe('LangChain integration (v1)', () => { const opusSpan = spans.find(span => span.name === 'chat claude-3-opus-20240229'); expect(opusSpan!.status).toBe('ok'); - expect(opusSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + expect(opusSpan!.attributes[SENTRY_OP].value).toBe('gen_ai.chat'); expect(opusSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); expect(opusSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); expect(opusSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('claude-3-opus-20240229'); @@ -81,7 +82,7 @@ describe('LangChain integration (v1)', () => { const errorSpan = spans.find(span => span.name === 'chat error-model'); expect(errorSpan!.status).toBe('error'); - expect(errorSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + expect(errorSpan!.attributes[SENTRY_OP].value).toBe('gen_ai.chat'); expect(errorSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); expect(errorSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); expect(errorSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('error-model'); @@ -126,7 +127,7 @@ describe('LangChain integration (v1)', () => { const sonnetSpan = spans.find(span => span.name === 'chat claude-3-5-sonnet-20241022'); expect(sonnetSpan!.status).toBe('ok'); - expect(sonnetSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + expect(sonnetSpan!.attributes[SENTRY_OP].value).toBe('gen_ai.chat'); expect(sonnetSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); expect(sonnetSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); expect(sonnetSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('claude-3-5-sonnet-20241022'); @@ -197,7 +198,7 @@ describe('LangChain integration (v1)', () => { // [0] chat with tool_use stop reason expect(firstSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); expect(firstSpan!.status).toBe('ok'); - expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + expect(firstSpan!.attributes[SENTRY_OP].value).toBe('gen_ai.chat'); expect(firstSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); expect(firstSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('claude-3-5-sonnet-20241022'); @@ -294,7 +295,7 @@ describe('LangChain integration (v1)', () => { const gpt4oSpan = spans.find(span => span.name === 'chat gpt-4o'); expect(gpt4oSpan!.status).toBe('ok'); - expect(gpt4oSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + expect(gpt4oSpan!.attributes[SENTRY_OP].value).toBe('gen_ai.chat'); expect(gpt4oSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); expect(gpt4oSpan!.attributes[GEN_AI_OPERATION_NAME].value).toBe('chat'); expect(gpt4oSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('openai'); @@ -310,7 +311,7 @@ describe('LangChain integration (v1)', () => { const gpt35Span = spans.find(span => span.name === 'chat gpt-3.5-turbo'); expect(gpt35Span!.status).toBe('ok'); - expect(gpt35Span!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + expect(gpt35Span!.attributes[SENTRY_OP].value).toBe('gen_ai.chat'); expect(gpt35Span!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); expect(gpt35Span!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('openai'); expect(gpt35Span!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('gpt-3.5-turbo'); @@ -323,7 +324,7 @@ describe('LangChain integration (v1)', () => { const errorSpan = spans.find(span => span.name === 'chat error-model'); expect(errorSpan!.status).toBe('error'); - expect(errorSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + expect(errorSpan!.attributes[SENTRY_OP].value).toBe('gen_ai.chat'); expect(errorSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); expect(errorSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('openai'); expect(errorSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('error-model'); From 6c4e3b8a3cbfcb05a7cd4e57eab5d85a8ac43def Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Wed, 7 Oct 2026 09:42:53 +0200 Subject: [PATCH 6/8] test(node): Assert LangChain spans after envelope selection Co-Authored-By: GPT-6 --- .../suites/tracing/langchain/test.ts | 479 +++++++++--------- .../suites/tracing/langchain/v1/test.ts | 355 +++++++------ 2 files changed, 411 insertions(+), 423 deletions(-) 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 9d13b6170ae3..bc88d62b17f8 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/langchain/test.ts @@ -37,193 +37,189 @@ describe('LangChain integration', () => { createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => { test('creates langchain related spans with genAI recording disabled', 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 spans = allSpans.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.langchain'); - expect(spans).toHaveLength(3); - expect(spans.map(span => span.name).sort()).toEqual([ - 'chat claude-3-5-sonnet-20241022', - 'chat claude-3-opus-20240229', - 'chat error-model', - ]); - - const sonnetSpan = spans.find(span => span.name === 'chat claude-3-5-sonnet-20241022'); - expect(sonnetSpan!.status).toBe('ok'); - expect(sonnetSpan!.attributes[SENTRY_OP].value).toBe('gen_ai.chat'); - expect(sonnetSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); - expect(sonnetSpan!.attributes[GEN_AI_OPERATION_NAME].value).toBe('chat'); - expect(sonnetSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); - expect(sonnetSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('claude-3-5-sonnet-20241022'); - expect(sonnetSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE].value).toBe(0.7); - expect(sonnetSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS].value).toBe(100); - expect(sonnetSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS].value).toBe(10); - expect(sonnetSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS].value).toBe(15); - expect(sonnetSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS].value).toBe(25); - expect(sonnetSpan!.attributes[GEN_AI_RESPONSE_ID]).toBeDefined(); - expect(sonnetSpan!.attributes[GEN_AI_RESPONSE_MODEL]).toBeDefined(); - expect(sonnetSpan!.attributes[GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE]).toBeDefined(); - - const opusSpan = spans.find(span => span.name === 'chat claude-3-opus-20240229'); - expect(opusSpan!.status).toBe('ok'); - expect(opusSpan!.attributes[SENTRY_OP].value).toBe('gen_ai.chat'); - expect(opusSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); - expect(opusSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); - expect(opusSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('claude-3-opus-20240229'); - expect(opusSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE].value).toBe(0.9); - expect(opusSpan!.attributes[GEN_AI_REQUEST_TOP_P].value).toBe(0.95); - expect(opusSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS].value).toBe(200); - expect(opusSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS].value).toBe(10); - expect(opusSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS].value).toBe(15); - expect(opusSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS].value).toBe(25); - - const errorSpan = spans.find(span => span.name === 'chat error-model'); - expect(errorSpan!.status).toBe('error'); - expect(errorSpan!.attributes[SENTRY_OP].value).toBe('gen_ai.chat'); - expect(errorSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); - expect(errorSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); - expect(errorSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('error-model'); + expect(container.items.find(span => span.is_segment && span.name === 'main')).toBeDefined(); + receivedSpans = container.items; }, }) .start() .completed(); + + const spans = receivedSpans.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.langchain'); + expect(spans).toHaveLength(3); + expect(spans.map(span => span.name).sort()).toEqual([ + 'chat claude-3-5-sonnet-20241022', + 'chat claude-3-opus-20240229', + 'chat error-model', + ]); + + const sonnetSpan = spans.find(span => span.name === 'chat claude-3-5-sonnet-20241022'); + expect(sonnetSpan!.status).toBe('ok'); + expect(sonnetSpan!.attributes[SENTRY_OP].value).toBe('gen_ai.chat'); + expect(sonnetSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); + expect(sonnetSpan!.attributes[GEN_AI_OPERATION_NAME].value).toBe('chat'); + expect(sonnetSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); + expect(sonnetSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('claude-3-5-sonnet-20241022'); + expect(sonnetSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE].value).toBe(0.7); + expect(sonnetSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS].value).toBe(100); + expect(sonnetSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS].value).toBe(10); + expect(sonnetSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS].value).toBe(15); + expect(sonnetSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS].value).toBe(25); + expect(sonnetSpan!.attributes[GEN_AI_RESPONSE_ID]).toBeDefined(); + expect(sonnetSpan!.attributes[GEN_AI_RESPONSE_MODEL]).toBeDefined(); + expect(sonnetSpan!.attributes[GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE]).toBeDefined(); + + const opusSpan = spans.find(span => span.name === 'chat claude-3-opus-20240229'); + expect(opusSpan!.status).toBe('ok'); + expect(opusSpan!.attributes[SENTRY_OP].value).toBe('gen_ai.chat'); + expect(opusSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); + expect(opusSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); + expect(opusSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('claude-3-opus-20240229'); + expect(opusSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE].value).toBe(0.9); + expect(opusSpan!.attributes[GEN_AI_REQUEST_TOP_P].value).toBe(0.95); + expect(opusSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS].value).toBe(200); + expect(opusSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS].value).toBe(10); + expect(opusSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS].value).toBe(15); + expect(opusSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS].value).toBe(25); + + const errorSpan = spans.find(span => span.name === 'chat error-model'); + expect(errorSpan!.status).toBe('error'); + expect(errorSpan!.attributes[SENTRY_OP].value).toBe('gen_ai.chat'); + expect(errorSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); + expect(errorSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); + expect(errorSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('error-model'); }); }); createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument-with-pii.mjs', (createRunner, test) => { test('creates langchain related spans with genAI recording enabled', 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 spans = allSpans.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.langchain'); - expect(spans).toHaveLength(3); - expect(spans.map(span => span.name).sort()).toEqual([ - 'chat claude-3-5-sonnet-20241022', - 'chat claude-3-opus-20240229', - 'chat error-model', - ]); - - const sonnetSpan = spans.find(span => span.name === 'chat claude-3-5-sonnet-20241022'); - expect(sonnetSpan!.status).toBe('ok'); - expect(sonnetSpan!.attributes[SENTRY_OP].value).toBe('gen_ai.chat'); - expect(sonnetSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); - expect(sonnetSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); - expect(sonnetSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('claude-3-5-sonnet-20241022'); - expect(sonnetSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE].value).toBe(0.7); - expect(sonnetSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS].value).toBe(100); - expect(sonnetSpan!.attributes[GEN_AI_INPUT_MESSAGES]).toBeDefined(); - expect(sonnetSpan!.attributes[GEN_AI_RESPONSE_TEXT]).toBeDefined(); - expect(sonnetSpan!.attributes[GEN_AI_RESPONSE_ID]).toBeDefined(); - expect(sonnetSpan!.attributes[GEN_AI_RESPONSE_MODEL]).toBeDefined(); - expect(sonnetSpan!.attributes[GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE]).toBeDefined(); - expect(sonnetSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS].value).toBe(10); - expect(sonnetSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS].value).toBe(15); - expect(sonnetSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS].value).toBe(25); - - const opusSpan = spans.find(span => span.name === 'chat claude-3-opus-20240229'); - expect(opusSpan!.status).toBe('ok'); - expect(opusSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); - expect(opusSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('claude-3-opus-20240229'); - expect(opusSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE].value).toBe(0.9); - expect(opusSpan!.attributes[GEN_AI_REQUEST_TOP_P].value).toBe(0.95); - expect(opusSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS].value).toBe(200); - expect(opusSpan!.attributes[GEN_AI_INPUT_MESSAGES]).toBeDefined(); - expect(opusSpan!.attributes[GEN_AI_RESPONSE_TEXT]).toBeDefined(); - expect(opusSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS].value).toBe(10); - expect(opusSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS].value).toBe(15); - expect(opusSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS].value).toBe(25); - - const errorSpan = spans.find(span => span.name === 'chat error-model'); - expect(errorSpan!.status).toBe('error'); - expect(errorSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); - expect(errorSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('error-model'); - expect(errorSpan!.attributes[GEN_AI_INPUT_MESSAGES]).toBeDefined(); + expect(container.items.find(span => span.is_segment && span.name === 'main')).toBeDefined(); + receivedSpans = container.items; }, }) .start() .completed(); + + const spans = receivedSpans.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.langchain'); + expect(spans).toHaveLength(3); + expect(spans.map(span => span.name).sort()).toEqual([ + 'chat claude-3-5-sonnet-20241022', + 'chat claude-3-opus-20240229', + 'chat error-model', + ]); + + const sonnetSpan = spans.find(span => span.name === 'chat claude-3-5-sonnet-20241022'); + expect(sonnetSpan!.status).toBe('ok'); + expect(sonnetSpan!.attributes[SENTRY_OP].value).toBe('gen_ai.chat'); + expect(sonnetSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); + expect(sonnetSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); + expect(sonnetSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('claude-3-5-sonnet-20241022'); + expect(sonnetSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE].value).toBe(0.7); + expect(sonnetSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS].value).toBe(100); + expect(sonnetSpan!.attributes[GEN_AI_INPUT_MESSAGES]).toBeDefined(); + expect(sonnetSpan!.attributes[GEN_AI_RESPONSE_TEXT]).toBeDefined(); + expect(sonnetSpan!.attributes[GEN_AI_RESPONSE_ID]).toBeDefined(); + expect(sonnetSpan!.attributes[GEN_AI_RESPONSE_MODEL]).toBeDefined(); + expect(sonnetSpan!.attributes[GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE]).toBeDefined(); + expect(sonnetSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS].value).toBe(10); + expect(sonnetSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS].value).toBe(15); + expect(sonnetSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS].value).toBe(25); + + const opusSpan = spans.find(span => span.name === 'chat claude-3-opus-20240229'); + expect(opusSpan!.status).toBe('ok'); + expect(opusSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); + expect(opusSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('claude-3-opus-20240229'); + expect(opusSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE].value).toBe(0.9); + expect(opusSpan!.attributes[GEN_AI_REQUEST_TOP_P].value).toBe(0.95); + expect(opusSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS].value).toBe(200); + expect(opusSpan!.attributes[GEN_AI_INPUT_MESSAGES]).toBeDefined(); + expect(opusSpan!.attributes[GEN_AI_RESPONSE_TEXT]).toBeDefined(); + expect(opusSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS].value).toBe(10); + expect(opusSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS].value).toBe(15); + expect(opusSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS].value).toBe(25); + + const errorSpan = spans.find(span => span.name === 'chat error-model'); + expect(errorSpan!.status).toBe('error'); + expect(errorSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); + expect(errorSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('error-model'); + expect(errorSpan!.attributes[GEN_AI_INPUT_MESSAGES]).toBeDefined(); }); }); createEsmAndCjsTests(__dirname, 'scenario-tools.mjs', 'instrument.mjs', (createRunner, test) => { test('creates langchain spans with tool calls', 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 spans = allSpans.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.langchain'); - expect(spans).toHaveLength(1); - const [firstSpan] = spans; - - // [0] chat with tool_use stop reason - expect(firstSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); - expect(firstSpan!.status).toBe('ok'); - expect(firstSpan!.attributes[SENTRY_OP].value).toBe('gen_ai.chat'); - expect(firstSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); - expect(firstSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); - expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('claude-3-5-sonnet-20241022'); - expect(firstSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE].value).toBe(0.7); - expect(firstSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS].value).toBe(150); - expect(firstSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS].value).toBe(20); - expect(firstSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS].value).toBe(30); - expect(firstSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS].value).toBe(50); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE].value).toBe('tool_use'); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_TOOL_CALLS]).toBeDefined(); + expect(container.items.find(span => span.is_segment && span.name === 'main')).toBeDefined(); + receivedSpans = container.items; }, }) .start() .completed(); + + const spans = receivedSpans.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.langchain'); + expect(spans).toHaveLength(1); + const [firstSpan] = spans; + + // [0] chat with tool_use stop reason + expect(firstSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); + expect(firstSpan!.status).toBe('ok'); + expect(firstSpan!.attributes[SENTRY_OP].value).toBe('gen_ai.chat'); + expect(firstSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); + expect(firstSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); + expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('claude-3-5-sonnet-20241022'); + expect(firstSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE].value).toBe(0.7); + expect(firstSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS].value).toBe(150); + expect(firstSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS].value).toBe(20); + expect(firstSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS].value).toBe(30); + expect(firstSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS].value).toBe(50); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE].value).toBe('tool_use'); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_TOOL_CALLS]).toBeDefined(); }); }); createEsmTests(__dirname, 'scenario-openai-before-langchain.mjs', 'instrument.mjs', (createRunner, test) => { test('demonstrates timing issue with duplicate spans', 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 spans = allSpans.filter( - span => - span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.langchain' || - span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.anthropic', - ); - expect(spans).toHaveLength(2); - const anthropicSpan = spans.find(span => span.attributes[SENTRY_ORIGIN].value === 'auto.ai.anthropic'); - expect(anthropicSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); - - // LangChain call is instrumented by LangChain. - const langchainSpan = spans.find(span => span.attributes[SENTRY_ORIGIN].value === 'auto.ai.langchain'); - expect(langchainSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); - - // Third call (not present): Direct Anthropic call made AFTER LangChain import - // is NOT instrumented, which demonstrates the skip mechanism works for NEW - // clients. We should only have ONE Anthropic span (the first one), not two. + expect(container.items.find(span => span.is_segment && span.name === 'main')).toBeDefined(); + receivedSpans = container.items; }, }) .start() .completed(); + + const spans = receivedSpans.filter( + span => + span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.langchain' || + span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.anthropic', + ); + expect(spans).toHaveLength(2); + const anthropicSpan = spans.find(span => span.attributes[SENTRY_ORIGIN].value === 'auto.ai.anthropic'); + expect(anthropicSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); + + // LangChain call is instrumented by LangChain. + const langchainSpan = spans.find(span => span.attributes[SENTRY_ORIGIN].value === 'auto.ai.langchain'); + expect(langchainSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); }); }); @@ -233,80 +229,80 @@ describe('LangChain integration', () => { 'instrument-with-pii.mjs', (createRunner, test) => { test('extracts system instructions from messages', 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 spans = allSpans.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.langchain'); - expect(spans).toHaveLength(1); - const [firstSpan] = spans; - - // [0] chat with extracted system instructions - expect(firstSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); - expect(firstSpan!.attributes[SENTRY_OP].value).toBe('gen_ai.chat'); - expect(firstSpan!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS].value).toBe( - JSON.stringify([{ type: 'text', content: 'You are a helpful assistant' }]), - ); + expect(container.items.find(span => span.is_segment && span.name === 'main')).toBeDefined(); + receivedSpans = container.items; }, }) .start() .completed(); + + const spans = receivedSpans.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.langchain'); + expect(spans).toHaveLength(1); + const [firstSpan] = spans; + + // [0] chat with extracted system instructions + expect(firstSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); + expect(firstSpan!.attributes[SENTRY_OP].value).toBe('gen_ai.chat'); + expect(firstSpan!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS].value).toBe( + JSON.stringify([{ type: 'text', content: 'You are a helpful assistant' }]), + ); }); }, ); createEsmAndCjsTests(__dirname, 'scenario-chain.mjs', 'instrument.mjs', (createRunner, test) => { test('uses runName for chain spans instead of unknown_chain', 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 spans = allSpans.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.langchain'); - expect(spans).toHaveLength(4); - expect(spans.map(span => span.name).sort()).toEqual([ - 'chat claude-3-5-sonnet-20241022', - 'invoke_agent', - 'invoke_agent format_prompt', - 'invoke_agent parse_output', - ]); - - const formatPromptSpan = spans.find(span => span.name === 'invoke_agent format_prompt'); - 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[GEN_AI_PIPELINE_NAME].value).toBe('format_prompt'); - expect(formatPromptSpan!.attributes[LANGCHAIN_CHAIN_NAME]).toBeUndefined(); - - const chatSpan = spans.find(span => span.name === 'chat claude-3-5-sonnet-20241022'); - expect(chatSpan!.attributes[SENTRY_OP].value).toBe('gen_ai.chat'); - expect(chatSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); - - const parseOutputSpan = spans.find(span => span.name === 'invoke_agent parse_output'); - expect(parseOutputSpan!.attributes[SENTRY_OP].value).toBe('gen_ai.invoke_agent'); - expect(parseOutputSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); - expect(parseOutputSpan!.attributes[GEN_AI_OPERATION_NAME].value).toBe('invoke_agent'); - expect(parseOutputSpan!.attributes[GEN_AI_PIPELINE_NAME].value).toBe('parse_output'); - expect(parseOutputSpan!.attributes[LANGCHAIN_CHAIN_NAME]).toBeUndefined(); - - const unknownChainSpan = spans.find(span => span.name === 'invoke_agent'); - expect(unknownChainSpan!.attributes[SENTRY_OP].value).toBe('gen_ai.invoke_agent'); - expect(unknownChainSpan!.attributes[GEN_AI_OPERATION_NAME].value).toBe('invoke_agent'); - expect(unknownChainSpan!.attributes[GEN_AI_PIPELINE_NAME]).toBeUndefined(); - expect(unknownChainSpan!.attributes[LANGCHAIN_CHAIN_NAME]).toBeUndefined(); + expect(container.items.find(span => span.is_segment && span.name === 'main')).toBeDefined(); + receivedSpans = container.items; }, }) .start() .completed(); + + const spans = receivedSpans.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.langchain'); + expect(spans).toHaveLength(4); + expect(spans.map(span => span.name).sort()).toEqual([ + 'chat claude-3-5-sonnet-20241022', + 'invoke_agent', + 'invoke_agent format_prompt', + 'invoke_agent parse_output', + ]); + + const formatPromptSpan = spans.find(span => span.name === 'invoke_agent format_prompt'); + 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[GEN_AI_PIPELINE_NAME].value).toBe('format_prompt'); + expect(formatPromptSpan!.attributes[LANGCHAIN_CHAIN_NAME]).toBeUndefined(); + + const chatSpan = spans.find(span => span.name === 'chat claude-3-5-sonnet-20241022'); + expect(chatSpan!.attributes[SENTRY_OP].value).toBe('gen_ai.chat'); + expect(chatSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); + + const parseOutputSpan = spans.find(span => span.name === 'invoke_agent parse_output'); + expect(parseOutputSpan!.attributes[SENTRY_OP].value).toBe('gen_ai.invoke_agent'); + expect(parseOutputSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); + expect(parseOutputSpan!.attributes[GEN_AI_OPERATION_NAME].value).toBe('invoke_agent'); + expect(parseOutputSpan!.attributes[GEN_AI_PIPELINE_NAME].value).toBe('parse_output'); + expect(parseOutputSpan!.attributes[LANGCHAIN_CHAIN_NAME]).toBeUndefined(); + + const unknownChainSpan = spans.find(span => span.name === 'invoke_agent'); + expect(unknownChainSpan!.attributes[SENTRY_OP].value).toBe('gen_ai.invoke_agent'); + expect(unknownChainSpan!.attributes[GEN_AI_OPERATION_NAME].value).toBe('invoke_agent'); + expect(unknownChainSpan!.attributes[GEN_AI_PIPELINE_NAME]).toBeUndefined(); + expect(unknownChainSpan!.attributes[LANGCHAIN_CHAIN_NAME]).toBeUndefined(); }); }); @@ -316,85 +312,84 @@ describe('LangChain integration', () => { createEsmAndCjsTests(__dirname, 'scenario-embeddings.mjs', 'instrument.mjs', (createRunner, test) => { test('creates embedding spans with genAI recording disabled', 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 spans = allSpans.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.langchain'); - expect(spans).toHaveLength(3); - expect(spans.map(span => span.name).sort()).toEqual([ - 'embeddings error-model', - 'embeddings text-embedding-3-small', - 'embeddings text-embedding-3-small', - ]); - - const successfulSpans = spans.filter( - span => span.name === 'embeddings text-embedding-3-small' && span.status === 'ok', - ); - expect(successfulSpans).toHaveLength(2); - for (const span of successfulSpans) { - expect(span.attributes[SENTRY_OP].value).toBe(GEN_AI_EMBEDDINGS); - expect(span.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); - expect(span.attributes[GEN_AI_OPERATION_NAME].value).toBe('embeddings'); - expect(span.attributes[GEN_AI_PROVIDER_NAME].value).toBe('openai'); - expect(span.attributes[GEN_AI_REQUEST_MODEL].value).toBe('text-embedding-3-small'); - expect(span.attributes[GEN_AI_REQUEST_DIMENSIONS_ATTRIBUTE].value).toBe(1536); - } - - const errorSpan = spans.find(span => span.name === 'embeddings error-model'); - expect(errorSpan!.status).toBe('error'); - expect(errorSpan!.attributes[SENTRY_OP].value).toBe(GEN_AI_EMBEDDINGS); - expect(errorSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('openai'); + expect(container.items.find(span => span.is_segment && span.name === 'main')).toBeDefined(); + receivedSpans = container.items; }, }) .start() .completed(); + + const spans = receivedSpans.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.langchain'); + expect(spans).toHaveLength(3); + expect(spans.map(span => span.name).sort()).toEqual([ + 'embeddings error-model', + 'embeddings text-embedding-3-small', + 'embeddings text-embedding-3-small', + ]); + + const successfulSpans = spans.filter( + span => span.name === 'embeddings text-embedding-3-small' && span.status === 'ok', + ); + expect(successfulSpans).toHaveLength(2); + for (const span of successfulSpans) { + expect(span.attributes[SENTRY_OP].value).toBe(GEN_AI_EMBEDDINGS); + expect(span.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); + expect(span.attributes[GEN_AI_OPERATION_NAME].value).toBe('embeddings'); + expect(span.attributes[GEN_AI_PROVIDER_NAME].value).toBe('openai'); + expect(span.attributes[GEN_AI_REQUEST_MODEL].value).toBe('text-embedding-3-small'); + expect(span.attributes[GEN_AI_REQUEST_DIMENSIONS_ATTRIBUTE].value).toBe(1536); + } + + const errorSpan = spans.find(span => span.name === 'embeddings error-model'); + expect(errorSpan!.status).toBe('error'); + expect(errorSpan!.attributes[SENTRY_OP].value).toBe(GEN_AI_EMBEDDINGS); + expect(errorSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('openai'); }); }); createEsmAndCjsTests(__dirname, 'scenario-embeddings.mjs', 'instrument-with-pii.mjs', (createRunner, test) => { test('creates embedding spans with genAI recording enabled', 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 spans = allSpans.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.langchain'); - expect(spans).toHaveLength(3); - expect(spans.map(span => span.name).sort()).toEqual([ - 'embeddings error-model', - 'embeddings text-embedding-3-small', - 'embeddings text-embedding-3-small', - ]); - - const querySpan = spans.find(span => span.attributes[GEN_AI_EMBEDDINGS_INPUT]?.value === 'Hello world'); - expect(querySpan!.name).toBe('embeddings text-embedding-3-small'); - expect(querySpan!.status).toBe('ok'); - expect(querySpan!.attributes[GEN_AI_REQUEST_DIMENSIONS_ATTRIBUTE].value).toBe(1536); - - const documentsSpan = spans.find( - span => - span.attributes[GEN_AI_EMBEDDINGS_INPUT]?.value === - JSON.stringify(['First document', 'Second document']), - ); - expect(documentsSpan!.name).toBe('embeddings text-embedding-3-small'); - expect(documentsSpan!.status).toBe('ok'); - - const errorSpan = spans.find(span => span.name === 'embeddings error-model'); - expect(errorSpan!.status).toBe('error'); + expect(container.items.find(span => span.is_segment && span.name === 'main')).toBeDefined(); + receivedSpans = container.items; }, }) .start() .completed(); + + const spans = receivedSpans.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.langchain'); + expect(spans).toHaveLength(3); + expect(spans.map(span => span.name).sort()).toEqual([ + 'embeddings error-model', + 'embeddings text-embedding-3-small', + 'embeddings text-embedding-3-small', + ]); + + const querySpan = spans.find(span => span.attributes[GEN_AI_EMBEDDINGS_INPUT]?.value === 'Hello world'); + expect(querySpan!.name).toBe('embeddings text-embedding-3-small'); + expect(querySpan!.status).toBe('ok'); + expect(querySpan!.attributes[GEN_AI_REQUEST_DIMENSIONS_ATTRIBUTE].value).toBe(1536); + + const documentsSpan = spans.find( + span => + span.attributes[GEN_AI_EMBEDDINGS_INPUT]?.value === JSON.stringify(['First document', 'Second document']), + ); + expect(documentsSpan!.name).toBe('embeddings text-embedding-3-small'); + expect(documentsSpan!.status).toBe('ok'); + + const errorSpan = spans.find(span => span.name === 'embeddings error-model'); + expect(errorSpan!.status).toBe('error'); }); }); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/v1/test.ts b/dev-packages/node-integration-tests/suites/tracing/langchain/v1/test.ts index e17ef7a549e8..2eb71ad12c29 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/v1/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/langchain/v1/test.ts @@ -33,63 +33,63 @@ describe('LangChain integration (v1)', () => { 'instrument.mjs', (createRunner, test) => { test('creates langchain related spans with genAI recording disabled', async () => { - const allSpans: SerializedStreamedSpanContainer['items'] = []; + let receivedSpans: SerializedStreamedSpanContainer['items'] = []; await createRunner() .unordered() .ignore('event') .expect({ span: container => { - allSpans.push(...container.items); - const segment = allSpans.find(span => span.is_segment && span.name === 'main'); - expect(segment).toBeDefined(); - const spans = allSpans.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.langchain'); - expect(spans).toHaveLength(3); - expect(spans.map(span => span.name).sort()).toEqual([ - 'chat claude-3-5-sonnet-20241022', - 'chat claude-3-opus-20240229', - 'chat error-model', - ]); - - const sonnetSpan = spans.find(span => span.name === 'chat claude-3-5-sonnet-20241022'); - expect(sonnetSpan!.status).toBe('ok'); - expect(sonnetSpan!.attributes[SENTRY_OP].value).toBe('gen_ai.chat'); - expect(sonnetSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); - expect(sonnetSpan!.attributes[GEN_AI_OPERATION_NAME].value).toBe('chat'); - expect(sonnetSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); - expect(sonnetSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('claude-3-5-sonnet-20241022'); - expect(sonnetSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE].value).toBe(0.7); - expect(sonnetSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS].value).toBe(100); - expect(sonnetSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS].value).toBe(10); - expect(sonnetSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS].value).toBe(15); - expect(sonnetSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS].value).toBe(25); - expect(sonnetSpan!.attributes[GEN_AI_RESPONSE_ID]).toBeDefined(); - expect(sonnetSpan!.attributes[GEN_AI_RESPONSE_MODEL]).toBeDefined(); - expect(sonnetSpan!.attributes[GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE]).toBeDefined(); - - const opusSpan = spans.find(span => span.name === 'chat claude-3-opus-20240229'); - expect(opusSpan!.status).toBe('ok'); - expect(opusSpan!.attributes[SENTRY_OP].value).toBe('gen_ai.chat'); - expect(opusSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); - expect(opusSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); - expect(opusSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('claude-3-opus-20240229'); - expect(opusSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE].value).toBe(0.9); - expect(opusSpan!.attributes[GEN_AI_REQUEST_TOP_P].value).toBe(0.95); - expect(opusSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS].value).toBe(200); - expect(opusSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS].value).toBe(10); - expect(opusSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS].value).toBe(15); - expect(opusSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS].value).toBe(25); - - const errorSpan = spans.find(span => span.name === 'chat error-model'); - expect(errorSpan!.status).toBe('error'); - expect(errorSpan!.attributes[SENTRY_OP].value).toBe('gen_ai.chat'); - expect(errorSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); - expect(errorSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); - expect(errorSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('error-model'); + expect(container.items.find(span => span.is_segment && span.name === 'main')).toBeDefined(); + receivedSpans = container.items; }, }) .start() .completed(); + + const spans = receivedSpans.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.langchain'); + expect(spans).toHaveLength(3); + expect(spans.map(span => span.name).sort()).toEqual([ + 'chat claude-3-5-sonnet-20241022', + 'chat claude-3-opus-20240229', + 'chat error-model', + ]); + + const sonnetSpan = spans.find(span => span.name === 'chat claude-3-5-sonnet-20241022'); + expect(sonnetSpan!.status).toBe('ok'); + expect(sonnetSpan!.attributes[SENTRY_OP].value).toBe('gen_ai.chat'); + expect(sonnetSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); + expect(sonnetSpan!.attributes[GEN_AI_OPERATION_NAME].value).toBe('chat'); + expect(sonnetSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); + expect(sonnetSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('claude-3-5-sonnet-20241022'); + expect(sonnetSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE].value).toBe(0.7); + expect(sonnetSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS].value).toBe(100); + expect(sonnetSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS].value).toBe(10); + expect(sonnetSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS].value).toBe(15); + expect(sonnetSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS].value).toBe(25); + expect(sonnetSpan!.attributes[GEN_AI_RESPONSE_ID]).toBeDefined(); + expect(sonnetSpan!.attributes[GEN_AI_RESPONSE_MODEL]).toBeDefined(); + expect(sonnetSpan!.attributes[GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE]).toBeDefined(); + + const opusSpan = spans.find(span => span.name === 'chat claude-3-opus-20240229'); + expect(opusSpan!.status).toBe('ok'); + expect(opusSpan!.attributes[SENTRY_OP].value).toBe('gen_ai.chat'); + expect(opusSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); + expect(opusSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); + expect(opusSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('claude-3-opus-20240229'); + expect(opusSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE].value).toBe(0.9); + expect(opusSpan!.attributes[GEN_AI_REQUEST_TOP_P].value).toBe(0.95); + expect(opusSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS].value).toBe(200); + expect(opusSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS].value).toBe(10); + expect(opusSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS].value).toBe(15); + expect(opusSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS].value).toBe(25); + + const errorSpan = spans.find(span => span.name === 'chat error-model'); + expect(errorSpan!.status).toBe('error'); + expect(errorSpan!.attributes[SENTRY_OP].value).toBe('gen_ai.chat'); + expect(errorSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); + expect(errorSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); + expect(errorSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('error-model'); }); }, { @@ -107,63 +107,63 @@ describe('LangChain integration (v1)', () => { 'instrument-with-pii.mjs', (createRunner, test) => { test('creates langchain related spans with genAI recording enabled', async () => { - const allSpans: SerializedStreamedSpanContainer['items'] = []; + let receivedSpans: SerializedStreamedSpanContainer['items'] = []; await createRunner() .unordered() .ignore('event') .expect({ span: container => { - allSpans.push(...container.items); - const segment = allSpans.find(span => span.is_segment && span.name === 'main'); - expect(segment).toBeDefined(); - const spans = allSpans.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.langchain'); - expect(spans).toHaveLength(3); - expect(spans.map(span => span.name).sort()).toEqual([ - 'chat claude-3-5-sonnet-20241022', - 'chat claude-3-opus-20240229', - 'chat error-model', - ]); - - const sonnetSpan = spans.find(span => span.name === 'chat claude-3-5-sonnet-20241022'); - expect(sonnetSpan!.status).toBe('ok'); - expect(sonnetSpan!.attributes[SENTRY_OP].value).toBe('gen_ai.chat'); - expect(sonnetSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); - expect(sonnetSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); - expect(sonnetSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('claude-3-5-sonnet-20241022'); - expect(sonnetSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE].value).toBe(0.7); - expect(sonnetSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS].value).toBe(100); - expect(sonnetSpan!.attributes[GEN_AI_INPUT_MESSAGES]).toBeDefined(); - expect(sonnetSpan!.attributes[GEN_AI_RESPONSE_TEXT]).toBeDefined(); - expect(sonnetSpan!.attributes[GEN_AI_RESPONSE_ID]).toBeDefined(); - expect(sonnetSpan!.attributes[GEN_AI_RESPONSE_MODEL]).toBeDefined(); - expect(sonnetSpan!.attributes[GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE]).toBeDefined(); - expect(sonnetSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS].value).toBe(10); - expect(sonnetSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS].value).toBe(15); - expect(sonnetSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS].value).toBe(25); - - const opusSpan = spans.find(span => span.name === 'chat claude-3-opus-20240229'); - expect(opusSpan!.status).toBe('ok'); - expect(opusSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); - expect(opusSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('claude-3-opus-20240229'); - expect(opusSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE].value).toBe(0.9); - expect(opusSpan!.attributes[GEN_AI_REQUEST_TOP_P].value).toBe(0.95); - expect(opusSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS].value).toBe(200); - expect(opusSpan!.attributes[GEN_AI_INPUT_MESSAGES]).toBeDefined(); - expect(opusSpan!.attributes[GEN_AI_RESPONSE_TEXT]).toBeDefined(); - expect(opusSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS].value).toBe(10); - expect(opusSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS].value).toBe(15); - expect(opusSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS].value).toBe(25); - - const errorSpan = spans.find(span => span.name === 'chat error-model'); - expect(errorSpan!.status).toBe('error'); - expect(errorSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); - expect(errorSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('error-model'); - expect(errorSpan!.attributes[GEN_AI_INPUT_MESSAGES]).toBeDefined(); + expect(container.items.find(span => span.is_segment && span.name === 'main')).toBeDefined(); + receivedSpans = container.items; }, }) .start() .completed(); + + const spans = receivedSpans.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.langchain'); + expect(spans).toHaveLength(3); + expect(spans.map(span => span.name).sort()).toEqual([ + 'chat claude-3-5-sonnet-20241022', + 'chat claude-3-opus-20240229', + 'chat error-model', + ]); + + const sonnetSpan = spans.find(span => span.name === 'chat claude-3-5-sonnet-20241022'); + expect(sonnetSpan!.status).toBe('ok'); + expect(sonnetSpan!.attributes[SENTRY_OP].value).toBe('gen_ai.chat'); + expect(sonnetSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); + expect(sonnetSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); + expect(sonnetSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('claude-3-5-sonnet-20241022'); + expect(sonnetSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE].value).toBe(0.7); + expect(sonnetSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS].value).toBe(100); + expect(sonnetSpan!.attributes[GEN_AI_INPUT_MESSAGES]).toBeDefined(); + expect(sonnetSpan!.attributes[GEN_AI_RESPONSE_TEXT]).toBeDefined(); + expect(sonnetSpan!.attributes[GEN_AI_RESPONSE_ID]).toBeDefined(); + expect(sonnetSpan!.attributes[GEN_AI_RESPONSE_MODEL]).toBeDefined(); + expect(sonnetSpan!.attributes[GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE]).toBeDefined(); + expect(sonnetSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS].value).toBe(10); + expect(sonnetSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS].value).toBe(15); + expect(sonnetSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS].value).toBe(25); + + const opusSpan = spans.find(span => span.name === 'chat claude-3-opus-20240229'); + expect(opusSpan!.status).toBe('ok'); + expect(opusSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); + expect(opusSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('claude-3-opus-20240229'); + expect(opusSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE].value).toBe(0.9); + expect(opusSpan!.attributes[GEN_AI_REQUEST_TOP_P].value).toBe(0.95); + expect(opusSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS].value).toBe(200); + expect(opusSpan!.attributes[GEN_AI_INPUT_MESSAGES]).toBeDefined(); + expect(opusSpan!.attributes[GEN_AI_RESPONSE_TEXT]).toBeDefined(); + expect(opusSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS].value).toBe(10); + expect(opusSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS].value).toBe(15); + expect(opusSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS].value).toBe(25); + + const errorSpan = spans.find(span => span.name === 'chat error-model'); + expect(errorSpan!.status).toBe('error'); + expect(errorSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); + expect(errorSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('error-model'); + expect(errorSpan!.attributes[GEN_AI_INPUT_MESSAGES]).toBeDefined(); }); }, { @@ -181,38 +181,38 @@ describe('LangChain integration (v1)', () => { 'instrument.mjs', (createRunner, test) => { test('creates langchain spans with tool calls', async () => { - const allSpans: SerializedStreamedSpanContainer['items'] = []; + let receivedSpans: SerializedStreamedSpanContainer['items'] = []; await createRunner() .unordered() .ignore('event') .expect({ span: container => { - allSpans.push(...container.items); - const segment = allSpans.find(span => span.is_segment && span.name === 'main'); - expect(segment).toBeDefined(); - const spans = allSpans.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.langchain'); - expect(spans).toHaveLength(1); - const [firstSpan] = spans; - - // [0] chat with tool_use stop reason - expect(firstSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); - expect(firstSpan!.status).toBe('ok'); - expect(firstSpan!.attributes[SENTRY_OP].value).toBe('gen_ai.chat'); - expect(firstSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); - expect(firstSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); - expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('claude-3-5-sonnet-20241022'); - expect(firstSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE].value).toBe(0.7); - expect(firstSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS].value).toBe(150); - expect(firstSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS].value).toBe(20); - expect(firstSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS].value).toBe(30); - expect(firstSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS].value).toBe(50); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE].value).toBe('tool_use'); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_TOOL_CALLS]).toBeDefined(); + expect(container.items.find(span => span.is_segment && span.name === 'main')).toBeDefined(); + receivedSpans = container.items; }, }) .start() .completed(); + + const spans = receivedSpans.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.langchain'); + expect(spans).toHaveLength(1); + const [firstSpan] = spans; + + // [0] chat with tool_use stop reason + expect(firstSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); + expect(firstSpan!.status).toBe('ok'); + expect(firstSpan!.attributes[SENTRY_OP].value).toBe('gen_ai.chat'); + expect(firstSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); + expect(firstSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); + expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('claude-3-5-sonnet-20241022'); + expect(firstSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE].value).toBe(0.7); + expect(firstSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS].value).toBe(150); + expect(firstSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS].value).toBe(20); + expect(firstSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS].value).toBe(30); + expect(firstSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS].value).toBe(50); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE].value).toBe('tool_use'); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_TOOL_CALLS]).toBeDefined(); }); }, { @@ -230,34 +230,31 @@ describe('LangChain integration (v1)', () => { 'instrument.mjs', (createRunner, test) => { test('demonstrates timing issue with duplicate spans', async () => { - const allSpans: SerializedStreamedSpanContainer['items'] = []; + let receivedSpans: SerializedStreamedSpanContainer['items'] = []; await createRunner() .unordered() .ignore('event') .expect({ span: container => { - allSpans.push(...container.items); - const segment = allSpans.find(span => span.is_segment && span.name === 'main'); - expect(segment).toBeDefined(); - const spans = allSpans.filter( - span => - span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.langchain' || - span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.anthropic', - ); - expect(spans).toHaveLength(2); - const anthropicSpan = spans.find(span => span.attributes[SENTRY_ORIGIN].value === 'auto.ai.anthropic'); - expect(anthropicSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); - - const langchainSpan = spans.find(span => span.attributes[SENTRY_ORIGIN].value === 'auto.ai.langchain'); - expect(langchainSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); - - // Third call (not present): Direct Anthropic call made AFTER LangChain import - // is NOT instrumented, demonstrating the skip mechanism works for NEW clients. + expect(container.items.find(span => span.is_segment && span.name === 'main')).toBeDefined(); + receivedSpans = container.items; }, }) .start() .completed(); + + const spans = receivedSpans.filter( + span => + span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.langchain' || + span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.anthropic', + ); + expect(spans).toHaveLength(2); + const anthropicSpan = spans.find(span => span.attributes[SENTRY_ORIGIN].value === 'auto.ai.anthropic'); + expect(anthropicSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); + + const langchainSpan = spans.find(span => span.attributes[SENTRY_ORIGIN].value === 'auto.ai.langchain'); + expect(langchainSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); }); }, { @@ -275,63 +272,59 @@ describe('LangChain integration (v1)', () => { 'instrument.mjs', (createRunner, test) => { test('creates langchain spans using initChatModel with OpenAI', async () => { - const allSpans: SerializedStreamedSpanContainer['items'] = []; + let receivedSpans: SerializedStreamedSpanContainer['items'] = []; await createRunner() .unordered() .ignore('event') .expect({ span: container => { - allSpans.push(...container.items); - const segment = allSpans.find(span => span.is_segment && span.name === 'main'); - expect(segment).toBeDefined(); - const spans = allSpans.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.langchain'); - expect(spans).toHaveLength(3); - expect(spans.map(span => span.name).sort()).toEqual([ - 'chat error-model', - 'chat gpt-3.5-turbo', - 'chat gpt-4o', - ]); - - const gpt4oSpan = spans.find(span => span.name === 'chat gpt-4o'); - expect(gpt4oSpan!.status).toBe('ok'); - expect(gpt4oSpan!.attributes[SENTRY_OP].value).toBe('gen_ai.chat'); - expect(gpt4oSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); - expect(gpt4oSpan!.attributes[GEN_AI_OPERATION_NAME].value).toBe('chat'); - expect(gpt4oSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('openai'); - expect(gpt4oSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('gpt-4o'); - expect(gpt4oSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE].value).toBe(0.7); - expect(gpt4oSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS].value).toBe(100); - expect(gpt4oSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS].value).toBe(8); - expect(gpt4oSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS].value).toBe(12); - expect(gpt4oSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS].value).toBe(20); - expect(gpt4oSpan!.attributes[GEN_AI_RESPONSE_ID]).toBeDefined(); - expect(gpt4oSpan!.attributes[GEN_AI_RESPONSE_MODEL].value).toBe('gpt-4o'); - expect(gpt4oSpan!.attributes[GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE].value).toBe('stop'); - - const gpt35Span = spans.find(span => span.name === 'chat gpt-3.5-turbo'); - expect(gpt35Span!.status).toBe('ok'); - expect(gpt35Span!.attributes[SENTRY_OP].value).toBe('gen_ai.chat'); - expect(gpt35Span!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); - expect(gpt35Span!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('openai'); - expect(gpt35Span!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('gpt-3.5-turbo'); - expect(gpt35Span!.attributes[GEN_AI_REQUEST_TEMPERATURE].value).toBe(0.5); - expect(gpt35Span!.attributes[GEN_AI_USAGE_INPUT_TOKENS].value).toBe(8); - expect(gpt35Span!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS].value).toBe(12); - expect(gpt35Span!.attributes[GEN_AI_USAGE_TOTAL_TOKENS].value).toBe(20); - expect(gpt35Span!.attributes[GEN_AI_RESPONSE_MODEL].value).toBe('gpt-3.5-turbo'); - expect(gpt35Span!.attributes[GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE].value).toBe('stop'); - - const errorSpan = spans.find(span => span.name === 'chat error-model'); - expect(errorSpan!.status).toBe('error'); - expect(errorSpan!.attributes[SENTRY_OP].value).toBe('gen_ai.chat'); - expect(errorSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); - expect(errorSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('openai'); - expect(errorSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('error-model'); + expect(container.items.find(span => span.is_segment && span.name === 'main')).toBeDefined(); + receivedSpans = container.items; }, }) .start() .completed(); + + const spans = receivedSpans.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.langchain'); + expect(spans).toHaveLength(3); + expect(spans.map(span => span.name).sort()).toEqual(['chat error-model', 'chat gpt-3.5-turbo', 'chat gpt-4o']); + + const gpt4oSpan = spans.find(span => span.name === 'chat gpt-4o'); + expect(gpt4oSpan!.status).toBe('ok'); + expect(gpt4oSpan!.attributes[SENTRY_OP].value).toBe('gen_ai.chat'); + expect(gpt4oSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); + expect(gpt4oSpan!.attributes[GEN_AI_OPERATION_NAME].value).toBe('chat'); + expect(gpt4oSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('openai'); + expect(gpt4oSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('gpt-4o'); + expect(gpt4oSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE].value).toBe(0.7); + expect(gpt4oSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS].value).toBe(100); + expect(gpt4oSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS].value).toBe(8); + expect(gpt4oSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS].value).toBe(12); + expect(gpt4oSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS].value).toBe(20); + expect(gpt4oSpan!.attributes[GEN_AI_RESPONSE_ID]).toBeDefined(); + expect(gpt4oSpan!.attributes[GEN_AI_RESPONSE_MODEL].value).toBe('gpt-4o'); + expect(gpt4oSpan!.attributes[GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE].value).toBe('stop'); + + const gpt35Span = spans.find(span => span.name === 'chat gpt-3.5-turbo'); + expect(gpt35Span!.status).toBe('ok'); + expect(gpt35Span!.attributes[SENTRY_OP].value).toBe('gen_ai.chat'); + expect(gpt35Span!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); + expect(gpt35Span!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('openai'); + expect(gpt35Span!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('gpt-3.5-turbo'); + expect(gpt35Span!.attributes[GEN_AI_REQUEST_TEMPERATURE].value).toBe(0.5); + expect(gpt35Span!.attributes[GEN_AI_USAGE_INPUT_TOKENS].value).toBe(8); + expect(gpt35Span!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS].value).toBe(12); + expect(gpt35Span!.attributes[GEN_AI_USAGE_TOTAL_TOKENS].value).toBe(20); + expect(gpt35Span!.attributes[GEN_AI_RESPONSE_MODEL].value).toBe('gpt-3.5-turbo'); + expect(gpt35Span!.attributes[GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE].value).toBe('stop'); + + const errorSpan = spans.find(span => span.name === 'chat error-model'); + expect(errorSpan!.status).toBe('error'); + expect(errorSpan!.attributes[SENTRY_OP].value).toBe('gen_ai.chat'); + expect(errorSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); + expect(errorSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('openai'); + expect(errorSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('error-model'); }); }, { From 021d90f93b582a9d79e875045eea6877665ac606 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Wed, 7 Oct 2026 15:11:04 +0200 Subject: [PATCH 7/8] test(node): Collect LangChain timing scenario spans until main ends Co-Authored-By: GPT-6 --- .../suites/tracing/langchain/test.ts | 35 +++++++++---------- .../suites/tracing/langchain/v1/test.ts | 34 ++++++++---------- 2 files changed, 31 insertions(+), 38 deletions(-) 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 f1fcbe70486c..48767041abf9 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/langchain/test.ts @@ -180,26 +180,23 @@ describe('LangChain integration', () => { createEsmTests(__dirname, 'scenario-openai-before-langchain.mjs', 'instrument.mjs', (createRunner, test) => { test('demonstrates timing issue with duplicate spans', async () => { - await createRunner() - .expect({ - span: container => { - expect(container.items.find(span => span.is_segment && span.name === 'main')).toBeDefined(); - const spans = container.items.filter( - span => - span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.langchain' || - span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.anthropic', - ); - expect(spans).toHaveLength(2); - const anthropicSpan = spans.find(span => span.attributes[SENTRY_ORIGIN].value === 'auto.ai.anthropic'); - expect(anthropicSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); + const runner = createRunner(); + const spansPromise = runner.collectStreamedSpansUntilSegment('main'); - // LangChain call is instrumented by LangChain. - const langchainSpan = spans.find(span => span.attributes[SENTRY_ORIGIN].value === 'auto.ai.langchain'); - expect(langchainSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); - }, - }) - .start() - .completed(); + await runner.start().completed(); + + const spans = (await spansPromise).filter( + span => + span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.langchain' || + span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.anthropic', + ); + expect(spans).toHaveLength(2); + const anthropicSpan = spans.find(span => span.attributes[SENTRY_ORIGIN].value === 'auto.ai.anthropic'); + expect(anthropicSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); + + // LangChain call is instrumented by LangChain. + const langchainSpan = spans.find(span => span.attributes[SENTRY_ORIGIN].value === 'auto.ai.langchain'); + expect(langchainSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); }); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/v1/test.ts b/dev-packages/node-integration-tests/suites/tracing/langchain/v1/test.ts index e8619ce63dea..c1d76eff53b6 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/v1/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/langchain/v1/test.ts @@ -255,26 +255,22 @@ describe('LangChain integration (v1)', () => { 'instrument.mjs', (createRunner, test) => { test('demonstrates timing issue with duplicate spans', async () => { - await createRunner() - .ignore('event') - .expect({ - span: container => { - expect(container.items.find(span => span.is_segment && span.name === 'main')).toBeDefined(); - const spans = container.items.filter( - span => - span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.langchain' || - span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.anthropic', - ); - expect(spans).toHaveLength(2); - const anthropicSpan = spans.find(span => span.attributes[SENTRY_ORIGIN].value === 'auto.ai.anthropic'); - expect(anthropicSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); + const runner = createRunner().ignore('event'); + const spansPromise = runner.collectStreamedSpansUntilSegment('main'); - const langchainSpan = spans.find(span => span.attributes[SENTRY_ORIGIN].value === 'auto.ai.langchain'); - expect(langchainSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); - }, - }) - .start() - .completed(); + await runner.start().completed(); + + const spans = (await spansPromise).filter( + span => + span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.langchain' || + span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.anthropic', + ); + expect(spans).toHaveLength(2); + const anthropicSpan = spans.find(span => span.attributes[SENTRY_ORIGIN].value === 'auto.ai.anthropic'); + expect(anthropicSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); + + const langchainSpan = spans.find(span => span.attributes[SENTRY_ORIGIN].value === 'auto.ai.langchain'); + expect(langchainSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); }); }, { From 1021de17ebc15358d637c7948e7ef16fe5a950fe Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Wed, 7 Oct 2026 15:22:02 +0200 Subject: [PATCH 8/8] test(node): Collect LangChain v1 spans across Deno envelopes Co-Authored-By: GPT-6 --- .../suites/tracing/langchain/v1/test.ts | 372 ++++++++---------- 1 file changed, 174 insertions(+), 198 deletions(-) diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/v1/test.ts b/dev-packages/node-integration-tests/suites/tracing/langchain/v1/test.ts index c1d76eff53b6..41158cb85db1 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/v1/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/langchain/v1/test.ts @@ -33,60 +33,56 @@ describe('LangChain integration (v1)', () => { 'instrument.mjs', (createRunner, test) => { test('creates langchain related spans with genAI recording disabled', async () => { - await createRunner() - .ignore('event') - .expect({ - span: container => { - expect(container.items.find(span => span.is_segment && span.name === 'main')).toBeDefined(); - const spans = container.items.filter( - span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.langchain', - ); - expect(spans).toHaveLength(3); - expect(spans.map(span => span.name).sort()).toEqual([ - 'chat claude-3-5-sonnet-20241022', - 'chat claude-3-opus-20240229', - 'chat error-model', - ]); + const runner = createRunner().ignore('event'); + const spansPromise = runner.collectStreamedSpansUntilSegment('main'); - const sonnetSpan = spans.find(span => span.name === 'chat claude-3-5-sonnet-20241022'); - expect(sonnetSpan!.status).toBe('ok'); - expect(sonnetSpan!.attributes[SENTRY_OP].value).toBe('gen_ai.chat'); - expect(sonnetSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); - expect(sonnetSpan!.attributes[GEN_AI_OPERATION_NAME].value).toBe('chat'); - expect(sonnetSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); - expect(sonnetSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('claude-3-5-sonnet-20241022'); - expect(sonnetSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE].value).toBe(0.7); - expect(sonnetSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS].value).toBe(100); - expect(sonnetSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS].value).toBe(10); - expect(sonnetSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS].value).toBe(15); - expect(sonnetSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS].value).toBe(25); - expect(sonnetSpan!.attributes[GEN_AI_RESPONSE_ID]).toBeDefined(); - expect(sonnetSpan!.attributes[GEN_AI_RESPONSE_MODEL]).toBeDefined(); - expect(sonnetSpan!.attributes[GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE]).toBeDefined(); + await runner.start().completed(); - const opusSpan = spans.find(span => span.name === 'chat claude-3-opus-20240229'); - expect(opusSpan!.status).toBe('ok'); - expect(opusSpan!.attributes[SENTRY_OP].value).toBe('gen_ai.chat'); - expect(opusSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); - expect(opusSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); - expect(opusSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('claude-3-opus-20240229'); - expect(opusSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE].value).toBe(0.9); - expect(opusSpan!.attributes[GEN_AI_REQUEST_TOP_P].value).toBe(0.95); - expect(opusSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS].value).toBe(200); - expect(opusSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS].value).toBe(10); - expect(opusSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS].value).toBe(15); - expect(opusSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS].value).toBe(25); + const spans = (await spansPromise).filter( + span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.langchain', + ); + expect(spans).toHaveLength(3); + expect(spans.map(span => span.name).sort()).toEqual([ + 'chat claude-3-5-sonnet-20241022', + 'chat claude-3-opus-20240229', + 'chat error-model', + ]); - const errorSpan = spans.find(span => span.name === 'chat error-model'); - expect(errorSpan!.status).toBe('error'); - expect(errorSpan!.attributes[SENTRY_OP].value).toBe('gen_ai.chat'); - expect(errorSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); - expect(errorSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); - expect(errorSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('error-model'); - }, - }) - .start() - .completed(); + const sonnetSpan = spans.find(span => span.name === 'chat claude-3-5-sonnet-20241022'); + expect(sonnetSpan!.status).toBe('ok'); + expect(sonnetSpan!.attributes[SENTRY_OP].value).toBe('gen_ai.chat'); + expect(sonnetSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); + expect(sonnetSpan!.attributes[GEN_AI_OPERATION_NAME].value).toBe('chat'); + expect(sonnetSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); + expect(sonnetSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('claude-3-5-sonnet-20241022'); + expect(sonnetSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE].value).toBe(0.7); + expect(sonnetSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS].value).toBe(100); + expect(sonnetSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS].value).toBe(10); + expect(sonnetSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS].value).toBe(15); + expect(sonnetSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS].value).toBe(25); + expect(sonnetSpan!.attributes[GEN_AI_RESPONSE_ID]).toBeDefined(); + expect(sonnetSpan!.attributes[GEN_AI_RESPONSE_MODEL]).toBeDefined(); + expect(sonnetSpan!.attributes[GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE]).toBeDefined(); + + const opusSpan = spans.find(span => span.name === 'chat claude-3-opus-20240229'); + expect(opusSpan!.status).toBe('ok'); + expect(opusSpan!.attributes[SENTRY_OP].value).toBe('gen_ai.chat'); + expect(opusSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); + expect(opusSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); + expect(opusSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('claude-3-opus-20240229'); + expect(opusSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE].value).toBe(0.9); + expect(opusSpan!.attributes[GEN_AI_REQUEST_TOP_P].value).toBe(0.95); + expect(opusSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS].value).toBe(200); + expect(opusSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS].value).toBe(10); + expect(opusSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS].value).toBe(15); + expect(opusSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS].value).toBe(25); + + const errorSpan = spans.find(span => span.name === 'chat error-model'); + expect(errorSpan!.status).toBe('error'); + expect(errorSpan!.attributes[SENTRY_OP].value).toBe('gen_ai.chat'); + expect(errorSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); + expect(errorSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); + expect(errorSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('error-model'); }); }, { @@ -104,60 +100,56 @@ describe('LangChain integration (v1)', () => { 'instrument-with-pii.mjs', (createRunner, test) => { test('creates langchain related spans with genAI recording enabled', async () => { - await createRunner() - .ignore('event') - .expect({ - span: container => { - expect(container.items.find(span => span.is_segment && span.name === 'main')).toBeDefined(); - const spans = container.items.filter( - span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.langchain', - ); - expect(spans).toHaveLength(3); - expect(spans.map(span => span.name).sort()).toEqual([ - 'chat claude-3-5-sonnet-20241022', - 'chat claude-3-opus-20240229', - 'chat error-model', - ]); + const runner = createRunner().ignore('event'); + const spansPromise = runner.collectStreamedSpansUntilSegment('main'); - const sonnetSpan = spans.find(span => span.name === 'chat claude-3-5-sonnet-20241022'); - expect(sonnetSpan!.status).toBe('ok'); - expect(sonnetSpan!.attributes[SENTRY_OP].value).toBe('gen_ai.chat'); - expect(sonnetSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); - expect(sonnetSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); - expect(sonnetSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('claude-3-5-sonnet-20241022'); - expect(sonnetSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE].value).toBe(0.7); - expect(sonnetSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS].value).toBe(100); - expect(sonnetSpan!.attributes[GEN_AI_INPUT_MESSAGES]).toBeDefined(); - expect(sonnetSpan!.attributes[GEN_AI_RESPONSE_TEXT]).toBeDefined(); - expect(sonnetSpan!.attributes[GEN_AI_RESPONSE_ID]).toBeDefined(); - expect(sonnetSpan!.attributes[GEN_AI_RESPONSE_MODEL]).toBeDefined(); - expect(sonnetSpan!.attributes[GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE]).toBeDefined(); - expect(sonnetSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS].value).toBe(10); - expect(sonnetSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS].value).toBe(15); - expect(sonnetSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS].value).toBe(25); + await runner.start().completed(); + + const spans = (await spansPromise).filter( + span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.langchain', + ); + expect(spans).toHaveLength(3); + expect(spans.map(span => span.name).sort()).toEqual([ + 'chat claude-3-5-sonnet-20241022', + 'chat claude-3-opus-20240229', + 'chat error-model', + ]); + + const sonnetSpan = spans.find(span => span.name === 'chat claude-3-5-sonnet-20241022'); + expect(sonnetSpan!.status).toBe('ok'); + expect(sonnetSpan!.attributes[SENTRY_OP].value).toBe('gen_ai.chat'); + expect(sonnetSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); + expect(sonnetSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); + expect(sonnetSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('claude-3-5-sonnet-20241022'); + expect(sonnetSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE].value).toBe(0.7); + expect(sonnetSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS].value).toBe(100); + expect(sonnetSpan!.attributes[GEN_AI_INPUT_MESSAGES]).toBeDefined(); + expect(sonnetSpan!.attributes[GEN_AI_RESPONSE_TEXT]).toBeDefined(); + expect(sonnetSpan!.attributes[GEN_AI_RESPONSE_ID]).toBeDefined(); + expect(sonnetSpan!.attributes[GEN_AI_RESPONSE_MODEL]).toBeDefined(); + expect(sonnetSpan!.attributes[GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE]).toBeDefined(); + expect(sonnetSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS].value).toBe(10); + expect(sonnetSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS].value).toBe(15); + expect(sonnetSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS].value).toBe(25); - const opusSpan = spans.find(span => span.name === 'chat claude-3-opus-20240229'); - expect(opusSpan!.status).toBe('ok'); - expect(opusSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); - expect(opusSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('claude-3-opus-20240229'); - expect(opusSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE].value).toBe(0.9); - expect(opusSpan!.attributes[GEN_AI_REQUEST_TOP_P].value).toBe(0.95); - expect(opusSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS].value).toBe(200); - expect(opusSpan!.attributes[GEN_AI_INPUT_MESSAGES]).toBeDefined(); - expect(opusSpan!.attributes[GEN_AI_RESPONSE_TEXT]).toBeDefined(); - expect(opusSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS].value).toBe(10); - expect(opusSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS].value).toBe(15); - expect(opusSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS].value).toBe(25); + const opusSpan = spans.find(span => span.name === 'chat claude-3-opus-20240229'); + expect(opusSpan!.status).toBe('ok'); + expect(opusSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); + expect(opusSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('claude-3-opus-20240229'); + expect(opusSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE].value).toBe(0.9); + expect(opusSpan!.attributes[GEN_AI_REQUEST_TOP_P].value).toBe(0.95); + expect(opusSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS].value).toBe(200); + expect(opusSpan!.attributes[GEN_AI_INPUT_MESSAGES]).toBeDefined(); + expect(opusSpan!.attributes[GEN_AI_RESPONSE_TEXT]).toBeDefined(); + expect(opusSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS].value).toBe(10); + expect(opusSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS].value).toBe(15); + expect(opusSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS].value).toBe(25); - const errorSpan = spans.find(span => span.name === 'chat error-model'); - expect(errorSpan!.status).toBe('error'); - expect(errorSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); - expect(errorSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('error-model'); - expect(errorSpan!.attributes[GEN_AI_INPUT_MESSAGES]).toBeDefined(); - }, - }) - .start() - .completed(); + const errorSpan = spans.find(span => span.name === 'chat error-model'); + expect(errorSpan!.status).toBe('error'); + expect(errorSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); + expect(errorSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('error-model'); + expect(errorSpan!.attributes[GEN_AI_INPUT_MESSAGES]).toBeDefined(); }); }, { @@ -175,35 +167,31 @@ describe('LangChain integration (v1)', () => { 'instrument.mjs', (createRunner, test) => { test('creates langchain spans with tool calls', async () => { - await createRunner() - .ignore('event') - .expect({ - span: container => { - expect(container.items.find(span => span.is_segment && span.name === 'main')).toBeDefined(); - const spans = container.items.filter( - span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.langchain', - ); - expect(spans).toHaveLength(1); - const [firstSpan] = spans; + const runner = createRunner().ignore('event'); + const spansPromise = runner.collectStreamedSpansUntilSegment('main'); + + await runner.start().completed(); - // [0] chat with tool_use stop reason - expect(firstSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); - expect(firstSpan!.status).toBe('ok'); - expect(firstSpan!.attributes[SENTRY_OP].value).toBe('gen_ai.chat'); - expect(firstSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); - expect(firstSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); - expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('claude-3-5-sonnet-20241022'); - expect(firstSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE].value).toBe(0.7); - expect(firstSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS].value).toBe(150); - expect(firstSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS].value).toBe(20); - expect(firstSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS].value).toBe(30); - expect(firstSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS].value).toBe(50); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE].value).toBe('tool_use'); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_TOOL_CALLS]).toBeDefined(); - }, - }) - .start() - .completed(); + const spans = (await spansPromise).filter( + span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.langchain', + ); + expect(spans).toHaveLength(1); + const [firstSpan] = spans; + + // [0] chat with tool_use stop reason + expect(firstSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); + expect(firstSpan!.status).toBe('ok'); + expect(firstSpan!.attributes[SENTRY_OP].value).toBe('gen_ai.chat'); + expect(firstSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); + expect(firstSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); + expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('claude-3-5-sonnet-20241022'); + expect(firstSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE].value).toBe(0.7); + expect(firstSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS].value).toBe(150); + expect(firstSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS].value).toBe(20); + expect(firstSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS].value).toBe(30); + expect(firstSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS].value).toBe(50); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE].value).toBe('tool_use'); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_TOOL_CALLS]).toBeDefined(); }); }, { @@ -221,23 +209,19 @@ describe('LangChain integration (v1)', () => { 'instrument.mjs', (createRunner, test) => { test('derives gen_ai.conversation.id from the invoke config and lets a scope id win', async () => { - await createRunner() - .ignore('event') - .expect({ - span: container => { - expect(container.items.find(span => span.is_segment && span.name === 'main')).toBeDefined(); - const spans = container.items.filter( - span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.langchain', - ); - expect(spans).toHaveLength(4); - const ids = spans - .sort((a, b) => a.start_timestamp - b.start_timestamp) - .map(span => span.attributes[GEN_AI_CONVERSATION_ID]?.value); - expect(ids).toEqual(['thread_from_config', 'session_from_config', 'conversation_from_scope', undefined]); - }, - }) - .start() - .completed(); + const runner = createRunner().ignore('event'); + const spansPromise = runner.collectStreamedSpansUntilSegment('main'); + + await runner.start().completed(); + + const spans = (await spansPromise).filter( + span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.langchain', + ); + expect(spans).toHaveLength(4); + const ids = spans + .sort((a, b) => a.start_timestamp - b.start_timestamp) + .map(span => span.attributes[GEN_AI_CONVERSATION_ID]?.value); + expect(ids).toEqual(['thread_from_config', 'session_from_config', 'conversation_from_scope', undefined]); }); }, { @@ -288,60 +272,52 @@ describe('LangChain integration (v1)', () => { 'instrument.mjs', (createRunner, test) => { test('creates langchain spans using initChatModel with OpenAI', async () => { - await createRunner() - .ignore('event') - .expect({ - span: container => { - expect(container.items.find(span => span.is_segment && span.name === 'main')).toBeDefined(); - const spans = container.items.filter( - span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.langchain', - ); - expect(spans).toHaveLength(3); - expect(spans.map(span => span.name).sort()).toEqual([ - 'chat error-model', - 'chat gpt-3.5-turbo', - 'chat gpt-4o', - ]); + const runner = createRunner().ignore('event'); + const spansPromise = runner.collectStreamedSpansUntilSegment('main'); + + await runner.start().completed(); + + const spans = (await spansPromise).filter( + span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.langchain', + ); + expect(spans).toHaveLength(3); + expect(spans.map(span => span.name).sort()).toEqual(['chat error-model', 'chat gpt-3.5-turbo', 'chat gpt-4o']); - const gpt4oSpan = spans.find(span => span.name === 'chat gpt-4o'); - expect(gpt4oSpan!.status).toBe('ok'); - expect(gpt4oSpan!.attributes[SENTRY_OP].value).toBe('gen_ai.chat'); - expect(gpt4oSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); - expect(gpt4oSpan!.attributes[GEN_AI_OPERATION_NAME].value).toBe('chat'); - expect(gpt4oSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('openai'); - expect(gpt4oSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('gpt-4o'); - expect(gpt4oSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE].value).toBe(0.7); - expect(gpt4oSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS].value).toBe(100); - expect(gpt4oSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS].value).toBe(8); - expect(gpt4oSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS].value).toBe(12); - expect(gpt4oSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS].value).toBe(20); - expect(gpt4oSpan!.attributes[GEN_AI_RESPONSE_ID]).toBeDefined(); - expect(gpt4oSpan!.attributes[GEN_AI_RESPONSE_MODEL].value).toBe('gpt-4o'); - expect(gpt4oSpan!.attributes[GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE].value).toBe('stop'); + const gpt4oSpan = spans.find(span => span.name === 'chat gpt-4o'); + expect(gpt4oSpan!.status).toBe('ok'); + expect(gpt4oSpan!.attributes[SENTRY_OP].value).toBe('gen_ai.chat'); + expect(gpt4oSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); + expect(gpt4oSpan!.attributes[GEN_AI_OPERATION_NAME].value).toBe('chat'); + expect(gpt4oSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('openai'); + expect(gpt4oSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('gpt-4o'); + expect(gpt4oSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE].value).toBe(0.7); + expect(gpt4oSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS].value).toBe(100); + expect(gpt4oSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS].value).toBe(8); + expect(gpt4oSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS].value).toBe(12); + expect(gpt4oSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS].value).toBe(20); + expect(gpt4oSpan!.attributes[GEN_AI_RESPONSE_ID]).toBeDefined(); + expect(gpt4oSpan!.attributes[GEN_AI_RESPONSE_MODEL].value).toBe('gpt-4o'); + expect(gpt4oSpan!.attributes[GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE].value).toBe('stop'); - const gpt35Span = spans.find(span => span.name === 'chat gpt-3.5-turbo'); - expect(gpt35Span!.status).toBe('ok'); - expect(gpt35Span!.attributes[SENTRY_OP].value).toBe('gen_ai.chat'); - expect(gpt35Span!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); - expect(gpt35Span!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('openai'); - expect(gpt35Span!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('gpt-3.5-turbo'); - expect(gpt35Span!.attributes[GEN_AI_REQUEST_TEMPERATURE].value).toBe(0.5); - expect(gpt35Span!.attributes[GEN_AI_USAGE_INPUT_TOKENS].value).toBe(8); - expect(gpt35Span!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS].value).toBe(12); - expect(gpt35Span!.attributes[GEN_AI_USAGE_TOTAL_TOKENS].value).toBe(20); - expect(gpt35Span!.attributes[GEN_AI_RESPONSE_MODEL].value).toBe('gpt-3.5-turbo'); - expect(gpt35Span!.attributes[GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE].value).toBe('stop'); + const gpt35Span = spans.find(span => span.name === 'chat gpt-3.5-turbo'); + expect(gpt35Span!.status).toBe('ok'); + expect(gpt35Span!.attributes[SENTRY_OP].value).toBe('gen_ai.chat'); + expect(gpt35Span!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); + expect(gpt35Span!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('openai'); + expect(gpt35Span!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('gpt-3.5-turbo'); + expect(gpt35Span!.attributes[GEN_AI_REQUEST_TEMPERATURE].value).toBe(0.5); + expect(gpt35Span!.attributes[GEN_AI_USAGE_INPUT_TOKENS].value).toBe(8); + expect(gpt35Span!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS].value).toBe(12); + expect(gpt35Span!.attributes[GEN_AI_USAGE_TOTAL_TOKENS].value).toBe(20); + expect(gpt35Span!.attributes[GEN_AI_RESPONSE_MODEL].value).toBe('gpt-3.5-turbo'); + expect(gpt35Span!.attributes[GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE].value).toBe('stop'); - const errorSpan = spans.find(span => span.name === 'chat error-model'); - expect(errorSpan!.status).toBe('error'); - expect(errorSpan!.attributes[SENTRY_OP].value).toBe('gen_ai.chat'); - expect(errorSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); - expect(errorSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('openai'); - expect(errorSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('error-model'); - }, - }) - .start() - .completed(); + const errorSpan = spans.find(span => span.name === 'chat error-model'); + expect(errorSpan!.status).toBe('error'); + expect(errorSpan!.attributes[SENTRY_OP].value).toBe('gen_ai.chat'); + expect(errorSpan!.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain'); + expect(errorSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('openai'); + expect(errorSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('error-model'); }); }, {