From 9b18d3daf94e299f3e32df5f651a249340394b54 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Tue, 25 Aug 2026 17:08:35 +0200 Subject: [PATCH] fix(server-utils): Don't capture caller-handled LangGraph errors LangGraph's compiled-graph invoke and tool-node wrappers called captureException with handled: false and then rethrew. The error rejects invoke() to the caller, so the instrumentation recorded an "unhandled crash" for an error the application handles, the same issue fixed for the client wrappers (#23024) and LangChain. 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. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../suites/tracing/langgraph/test.ts | 10 ---------- packages/server-utils/src/ai/langgraph/index.ts | 9 ++------- packages/server-utils/src/ai/langgraph/utils.ts | 9 ++------- 3 files changed, 4 insertions(+), 24 deletions(-) diff --git a/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts b/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts index 7b04ee85f65d..3f85267b154e 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts @@ -27,7 +27,6 @@ describe('LangGraph integration', () => { createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => { test('should instrument LangGraph with default PII settings', async () => { await createRunner() - .ignore('event') .expect({ transaction: { transaction: 'langgraph-test' } }) .expect({ span: container => { @@ -57,7 +56,6 @@ describe('LangGraph integration', () => { createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument-with-pii.mjs', (createRunner, test) => { test('should instrument LangGraph with genAI recording enabled', async () => { await createRunner() - .ignore('event') .expect({ transaction: { transaction: 'langgraph-test' } }) .expect({ span: container => { @@ -93,7 +91,6 @@ describe('LangGraph integration', () => { createEsmAndCjsTests(__dirname, 'scenario-tools.mjs', 'instrument-with-pii.mjs', (createRunner, test) => { test('should capture tools from LangGraph agent', { timeout: 30000 }, async () => { await createRunner() - .ignore('event') .expect({ transaction: { transaction: 'langgraph-tools-test' } }) .expect({ span: container => { @@ -139,7 +136,6 @@ describe('LangGraph integration', () => { createEsmAndCjsTests(__dirname, 'scenario-thread-id.mjs', 'instrument.mjs', (createRunner, test) => { test('should capture thread_id as gen_ai.conversation.id', async () => { await createRunner() - .ignore('event') .expect({ transaction: { transaction: 'langgraph-thread-id-test' } }) .expect({ span: container => { @@ -180,7 +176,6 @@ describe('LangGraph integration', () => { (createRunner, test) => { test('extracts system instructions from messages', async () => { await createRunner() - .ignore('event') .expect({ transaction: { transaction: 'main' } }) .expect({ span: container => { @@ -203,7 +198,6 @@ describe('LangGraph integration', () => { createEsmAndCjsTests(__dirname, 'scenario-resume.mjs', 'instrument.mjs', (createRunner, test) => { test('should not throw when invoke is called with null input (resume scenario)', async () => { await createRunner() - .ignore('event') .expect({ transaction: { transaction: 'langgraph-resume-test', @@ -238,7 +232,6 @@ describe('LangGraph integration', () => { createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument-span-streaming.mjs', (createRunner, test) => { test('creates langgraph related spans with span streaming enabled', async () => { await createRunner() - .ignore('event') .expect({ span: container => { const weatherTodaySpan = container.items.find(span => @@ -265,7 +258,6 @@ describe('LangGraph integration', () => { createEsmAndCjsTests(__dirname, 'agent-scenario.mjs', 'instrument-agent.mjs', (createRunner, test) => { test('should instrument createReactAgent with agent and chat spans', { timeout: 30000 }, async () => { await createRunner() - .ignore('event') .expect({ transaction: event => { const spans = event.spans ?? []; @@ -308,7 +300,6 @@ describe('LangGraph integration', () => { createEsmAndCjsTests(__dirname, 'agent-tools-scenario.mjs', 'instrument-agent.mjs', (createRunner, test) => { test('should create tool execution spans for createReactAgent with tools', { timeout: 30000 }, async () => { await createRunner() - .ignore('event') .expect({ transaction: event => { const spans = event.spans ?? []; @@ -364,7 +355,6 @@ describe('LangGraph integration', () => { createEsmAndCjsTests(__dirname, 'scenario-stategraph-chat.mjs', 'instrument-agent.mjs', (createRunner, test) => { test('auto-injects langchain handler for plain StateGraph and emits chat spans', { timeout: 30000 }, async () => { await createRunner() - .ignore('event') .expect({ transaction: event => { expect(event.transaction).toBe('main'); diff --git a/packages/server-utils/src/ai/langgraph/index.ts b/packages/server-utils/src/ai/langgraph/index.ts index 5a12a972dc9d..ec1e91fa3d66 100644 --- a/packages/server-utils/src/ai/langgraph/index.ts +++ b/packages/server-utils/src/ai/langgraph/index.ts @@ -1,6 +1,5 @@ /* eslint-disable typescript-eslint/no-deprecated */ import { - captureException, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, @@ -183,13 +182,9 @@ export function instrumentCompiledGraphInvoke( return result; } catch (error) { + // The error is rethrown to the caller (invoke() rejects), so we only mark the span failed + // and do not record it. span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); - captureException(error, { - mechanism: { - handled: false, - type: 'auto.ai.langgraph.error', - }, - }); throw error; } }, diff --git a/packages/server-utils/src/ai/langgraph/utils.ts b/packages/server-utils/src/ai/langgraph/utils.ts index 36c70cc3bd9b..ea1da8b8815a 100644 --- a/packages/server-utils/src/ai/langgraph/utils.ts +++ b/packages/server-utils/src/ai/langgraph/utils.ts @@ -1,6 +1,5 @@ /* eslint-disable typescript-eslint/no-deprecated */ import { - captureException, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, @@ -141,13 +140,9 @@ export function wrapToolsWithSpans(tools: unknown[], options: LangGraphOptions, return result; } catch (error) { + // The error is rethrown to the caller (invoke() rejects), so we only mark the span + // failed and do not record it. span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); - captureException(error, { - mechanism: { - handled: false, - type: 'auto.ai.langgraph.error', - }, - }); throw error; } },