From 39a73f154c776c507f7af127a8c8accb8915821f Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Tue, 25 Aug 2026 17:04:11 +0200 Subject: [PATCH 1/3] fix(server-utils): Don't capture caller-handled LangChain errors LangChain's callback error handlers (handleLLMError, handleChainError, handleToolError) and the embeddings wrapper called captureException with handled: false. These errors reject invoke()/embedQuery() to the caller, so the instrumentation recorded an "unhandled crash" for an error the application handles, the same issue fixed for the OpenAI, Anthropic and Google GenAI client wrappers in #23024. The span is still marked failed and the error still propagates; only the event goes away. The integration tests masked these with .ignore('event'); those masks are removed so the tests no longer hide instrumentation-level captures. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../suites/tracing/langchain/test.ts | 11 ------ .../src/ai/langchain/embeddings.ts | 17 +++------- .../server-utils/src/ai/langchain/index.ts | 34 +++++-------------- 3 files changed, 13 insertions(+), 49 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 e1ead1a89fe2..513537ad9a26 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 => { @@ -201,7 +197,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 => { @@ -236,7 +231,6 @@ describe('LangChain integration', () => { (createRunner, test) => { test('extracts system instructions from messages', async () => { await createRunner() - .ignore('event') .expect({ transaction: { transaction: 'main' } }) .expect({ span: container => { @@ -260,7 +254,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 => { @@ -306,7 +299,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 => { @@ -343,7 +335,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 => { @@ -362,7 +353,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 => { @@ -403,7 +393,6 @@ describe('LangChain integration', () => { createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument-span-streaming.mjs', (createRunner, test) => { test('creates langchain related spans with span streaming enabled', async () => { await createRunner() - .ignore('event') .expect({ span: container => { const sonnetSpan = container.items.find(span => span.name === 'chat claude-3-5-sonnet-20241022'); diff --git a/packages/server-utils/src/ai/langchain/embeddings.ts b/packages/server-utils/src/ai/langchain/embeddings.ts index 23ba6fe07f69..cf052e76a684 100644 --- a/packages/server-utils/src/ai/langchain/embeddings.ts +++ b/packages/server-utils/src/ai/langchain/embeddings.ts @@ -1,10 +1,4 @@ -import { - captureException, - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - startSpan, - stringify, -} from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startSpan, stringify } from '@sentry/core'; import type { SpanAttributeValue } from '@sentry/core'; import { GEN_AI_EMBEDDINGS_INPUT, @@ -99,12 +93,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); }); }, }); diff --git a/packages/server-utils/src/ai/langchain/index.ts b/packages/server-utils/src/ai/langchain/index.ts index 1f6ba8754f79..f7d0de9d4b61 100644 --- a/packages/server-utils/src/ai/langchain/index.ts +++ b/packages/server-utils/src/ai/langchain/index.ts @@ -1,6 +1,5 @@ /* eslint-disable max-lines */ import { - captureException, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, @@ -184,19 +183,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 @@ -258,19 +252,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 @@ -336,19 +325,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 From 797b1664483fbf2e702562219345cbb8f55e4b49 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Wed, 26 Aug 2026 10:44:42 +0200 Subject: [PATCH 2/3] test(server-utils): Assert embeddings instrumentation rethrows without capturing The embeddings wrapper no longer records caller-handled errors, so the unit test now asserts the error propagates and captureException is not called. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../test/ai/lib/tracing/langchain-embeddings.test.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/server-utils/test/ai/lib/tracing/langchain-embeddings.test.ts b/packages/server-utils/test/ai/lib/tracing/langchain-embeddings.test.ts index 361c47675ceb..bf3ce75bfbf8 100644 --- a/packages/server-utils/test/ai/lib/tracing/langchain-embeddings.test.ts +++ b/packages/server-utils/test/ai/lib/tracing/langchain-embeddings.test.ts @@ -84,7 +84,7 @@ describe('instrumentEmbeddingMethod', () => { expect(capturedSpanConfig!.attributes[GEN_AI_EMBEDDINGS_INPUT]).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); @@ -92,9 +92,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 () => { From 5f0ccb272236ba3c5bdd05d4105517ff75ae9bda Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Wed, 26 Aug 2026 11:12:52 +0200 Subject: [PATCH 3/3] fix(server-utils): Stop capturing caller-handled errors in LangChain embeddings auto-instrumentation The Node diagnostics-channel path for embedQuery/embedDocuments passed captureError with handled: false into bindTracingChannelToSpan, so it still recorded caller-handled embedding errors as unhandled crashes even after the manual instrumentEmbeddingMethod wrapper stopped. Drop captureError; the span is still marked failed on error and the rejection propagates to the caller. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/server-utils/src/integrations/langchain.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/server-utils/src/integrations/langchain.ts b/packages/server-utils/src/integrations/langchain.ts index 562894882c82..b6a43579de0f 100644 --- a/packages/server-utils/src/integrations/langchain.ts +++ b/packages/server-utils/src/integrations/langchain.ts @@ -89,13 +89,13 @@ function instrumentChatModels(options: LangChainOptions): void { } } -// Embeddings don't use the callback system. Wrap the method in its own span +// Embeddings don't use the callback system. Wrap the method in its own span. +// Embedding errors reject to the caller, so we only open the span (which bindTracingChannelToSpan +// still marks failed on error) and do not capture them. function instrumentEmbeddings(options: LangChainOptions): void { for (const channelName of langchainEmbeddingsChannels) { - bindTracingChannelToSpan( - diagnosticsChannel.tracingChannel(channelName), - data => createEmbeddingsSpan(data, options), - { captureError: () => ({ mechanism: { handled: false, type: 'auto.ai.langchain' } }) }, + bindTracingChannelToSpan(diagnosticsChannel.tracingChannel(channelName), data => + createEmbeddingsSpan(data, options), ); } }