From 10ff6036cf5ab1fde6fe411fa26b65059ea2c129 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Wed, 26 Aug 2026 11:36:38 +0200 Subject: [PATCH] fix(v10/core): Don't capture caller-handled LangChain errors Backport of: #23593 --- .../suites/tracing/langchain/test.ts | 10 ------ .../core/src/tracing/langchain/embeddings.ts | 10 ++---- packages/core/src/tracing/langchain/index.ts | 34 +++++-------------- .../lib/tracing/langchain-embeddings.test.ts | 6 ++-- .../integrations/tracing-channel/langchain.ts | 8 ++--- 5 files changed, 18 insertions(+), 50 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 ed342ce9d1a2..d15f134f21c8 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/langchain/test.ts @@ -33,7 +33,6 @@ describe('LangChain integration', () => { createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => { test('creates langchain related spans with genAI recording disabled', async () => { await createRunner() - .ignore('event') .expect({ transaction: { transaction: 'main' } }) .expect({ span: container => { @@ -90,7 +89,6 @@ describe('LangChain integration', () => { test('does not create duplicate spans from double module patching', async () => { await createRunner() - .ignore('event') .expect({ transaction: { transaction: 'main' } }) .expect({ span: container => { @@ -111,7 +109,6 @@ describe('LangChain integration', () => { createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument-with-pii.mjs', (createRunner, test) => { test('creates langchain related spans with genAI recording enabled', async () => { await createRunner() - .ignore('event') .expect({ transaction: { transaction: 'main' } }) .expect({ span: container => { @@ -170,7 +167,6 @@ describe('LangChain integration', () => { createEsmAndCjsTests(__dirname, 'scenario-tools.mjs', 'instrument.mjs', (createRunner, test) => { test('creates langchain spans with tool calls', async () => { await createRunner() - .ignore('event') .expect({ transaction: { transaction: 'main' } }) .expect({ span: container => { @@ -250,7 +246,6 @@ describe('LangChain integration', () => { createEsmTests(__dirname, 'scenario-openai-before-langchain.mjs', 'instrument.mjs', (createRunner, test) => { test('demonstrates timing issue with duplicate spans', async () => { await createRunner() - .ignore('event') .expect({ transaction: { transaction: 'main' } }) .expect({ span: container => { @@ -287,7 +282,6 @@ describe('LangChain integration', () => { (createRunner, test) => { test('extracts system instructions from messages', async () => { await createRunner() - .ignore('event') .expect({ transaction: { transaction: 'main' } }) .expect({ span: container => { @@ -311,7 +305,6 @@ 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() - .ignore('event') .expect({ transaction: { transaction: 'main' } }) .expect({ span: container => { @@ -357,7 +350,6 @@ describe('LangChain integration', () => { createEsmAndCjsTests(__dirname, 'scenario-embeddings.mjs', 'instrument.mjs', (createRunner, test) => { test('creates embedding spans with genAI recording disabled', async () => { await createRunner() - .ignore('event') .expect({ transaction: { transaction: 'main' } }) .expect({ span: container => { @@ -394,7 +386,6 @@ describe('LangChain integration', () => { test('does not create duplicate embedding spans from double module patching', async () => { await createRunner() - .ignore('event') .expect({ transaction: { transaction: 'main' } }) .expect({ span: container => { @@ -413,7 +404,6 @@ describe('LangChain integration', () => { createEsmAndCjsTests(__dirname, 'scenario-embeddings.mjs', 'instrument-with-pii.mjs', (createRunner, test) => { test('creates embedding spans with genAI recording enabled', async () => { await createRunner() - .ignore('event') .expect({ transaction: { transaction: 'main' } }) .expect({ span: container => { diff --git a/packages/core/src/tracing/langchain/embeddings.ts b/packages/core/src/tracing/langchain/embeddings.ts index f6f70280e2ac..f15c98bb879d 100644 --- a/packages/core/src/tracing/langchain/embeddings.ts +++ b/packages/core/src/tracing/langchain/embeddings.ts @@ -1,4 +1,3 @@ -import { captureException } from '../../exports'; import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../../semanticAttributes'; import { startSpan } from '../../tracing/trace'; import type { SpanAttributeValue } from '../../types/span'; @@ -93,12 +92,9 @@ export function instrumentEmbeddingMethod( return new Proxy(originalMethod, { apply(target, thisArg, args: unknown[]): Promise { return startSpan(_INTERNAL_getLangChainEmbeddingsSpanOptions(thisArg, args[0], options), () => { - return Reflect.apply(target, thisArg, args).then(undefined, error => { - captureException(error, { - mechanism: { handled: false, type: 'auto.ai.langchain' }, - }); - throw error; - }); + // On rejection `startSpan` marks the span failed and rethrows to the caller, so we don't + // record the error ourselves. + return Reflect.apply(target, thisArg, args); }); }, }) as (...args: unknown[]) => Promise; diff --git a/packages/core/src/tracing/langchain/index.ts b/packages/core/src/tracing/langchain/index.ts index 621ae76acdd5..3443c9e55f86 100644 --- a/packages/core/src/tracing/langchain/index.ts +++ b/packages/core/src/tracing/langchain/index.ts @@ -1,5 +1,4 @@ /* eslint-disable max-lines */ -import { captureException } from '../../exports'; import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../../semanticAttributes'; import { SPAN_STATUS_ERROR } from '../../tracing'; import { startSpanManual } from '../../tracing/trace'; @@ -183,19 +182,14 @@ export function createLangChainCallbackHandler(options: LangChainOptions = {}): }, // LLM Error Handler - note: handleLLMError with capital LLM - handleLLMError(error: Error, runId: string) { + handleLLMError(_error: Error, runId: string) { + // The error is surfaced to the caller (invoke() rejects), so we only mark the span failed and + // do not record it. const span = spanMap.get(runId); if (span?.isRecording()) { span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); exitSpan(runId); } - - captureException(error, { - mechanism: { - handled: false, - type: `${LANGCHAIN_ORIGIN}.llm_error_handler`, - }, - }); }, // Chain Start Handler @@ -257,19 +251,14 @@ export function createLangChainCallbackHandler(options: LangChainOptions = {}): }, // Chain Error Handler - handleChainError(error: Error, runId: string) { + handleChainError(_error: Error, runId: string) { + // The error is surfaced to the caller (invoke() rejects), so we only mark the span failed and + // do not record it. const span = spanMap.get(runId); if (span?.isRecording()) { span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); exitSpan(runId); } - - captureException(error, { - mechanism: { - handled: false, - type: `${LANGCHAIN_ORIGIN}.chain_error_handler`, - }, - }); }, // Tool Start Handler @@ -335,19 +324,14 @@ export function createLangChainCallbackHandler(options: LangChainOptions = {}): }, // Tool Error Handler - handleToolError(error: Error, runId: string) { + handleToolError(_error: Error, runId: string) { + // The error is surfaced to the caller (invoke() rejects), so we only mark the span failed and + // do not record it. const span = spanMap.get(runId); if (span?.isRecording()) { span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); exitSpan(runId); } - - captureException(error, { - mechanism: { - handled: false, - type: `${LANGCHAIN_ORIGIN}.tool_error_handler`, - }, - }); }, // LangChain BaseCallbackHandler required methods diff --git a/packages/core/test/lib/tracing/langchain-embeddings.test.ts b/packages/core/test/lib/tracing/langchain-embeddings.test.ts index f1bed062b4b2..af605fcfaf6f 100644 --- a/packages/core/test/lib/tracing/langchain-embeddings.test.ts +++ b/packages/core/test/lib/tracing/langchain-embeddings.test.ts @@ -75,7 +75,7 @@ describe('instrumentEmbeddingMethod', () => { expect(capturedSpanConfig!.attributes[GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE]).toBe('["doc1","doc2"]'); }); - it('captures exception on failure', async () => { + it('rethrows the error to the caller without capturing it', async () => { const error = new Error('API error'); const original = vi.fn().mockRejectedValue(error); const wrapped = instrumentEmbeddingMethod(original); @@ -83,9 +83,7 @@ describe('instrumentEmbeddingMethod', () => { const instance = { constructor: { name: 'OpenAIEmbeddings' }, model: 'error-model' }; await expect(wrapped.call(instance, 'test')).rejects.toThrow('API error'); - expect(captureException).toHaveBeenCalledWith(error, { - mechanism: { handled: false, type: 'auto.ai.langchain' }, - }); + expect(captureException).not.toHaveBeenCalled(); }); it('infers system from class name', async () => { diff --git a/packages/server-utils/src/integrations/tracing-channel/langchain.ts b/packages/server-utils/src/integrations/tracing-channel/langchain.ts index 008d52638241..a7663e7c3758 100644 --- a/packages/server-utils/src/integrations/tracing-channel/langchain.ts +++ b/packages/server-utils/src/integrations/tracing-channel/langchain.ts @@ -92,10 +92,10 @@ const _langChainChannelIntegration = ((options: LangChainOptions = {}) => { waitForTracingChannelBinding(() => { for (const channelName of langchainEmbeddingsChannels) { DEBUG_BUILD && debug.log(`[orchestrion:langchain] subscribing to channel "${channelName}"`); - bindTracingChannelToSpan( - diagnosticsChannel.tracingChannel(channelName), - data => createEmbeddingsSpan(data, options), - { captureError: () => ({ mechanism: { handled: false, type: 'auto.ai.langchain' } }) }, + // Embedding errors reject to the caller, so we only open the span (which + // bindTracingChannelToSpan still marks failed on error) and do not capture them. + bindTracingChannelToSpan(diagnosticsChannel.tracingChannel(channelName), data => + createEmbeddingsSpan(data, options), ); } });