From 6f9769e198d1d08656c241b7b5aeed46a6bbb4e9 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Wed, 29 Jul 2026 18:02:11 +0200 Subject: [PATCH 1/4] ref(langgraph)!: Drop gen_ai.create_agent spans MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The LangGraph instrumentation no longer emits `gen_ai.create_agent` spans on graph compile. These spans are no longer relevant for Sentry's AI product, and removing them for v11 keeps the leftover out of the major. The span was emitted from two call sites sharing the `_INTERNAL_getLangGraphCreateAgentSpanOptions` builder: the core `instrumentStateGraphCompile` proxy and the orchestrion `stateGraphCompile` channel. Both now only wrap the compiled graph's `invoke`. The `insideCreateReactAgent` suppression is kept — it prevents duplicate `invoke_agent` spans, independent of the create_agent span. Co-Authored-By: Claude Opus 4.8 (1M context) --- MIGRATION.md | 6 + .../suites/tracing/langgraph/test.ts | 19 +-- .../suites/orchestrion-langgraph/test.ts | 30 +++-- .../suites/tracing/langgraph/test.ts | 50 ++------ packages/core/src/server-exports.ts | 1 - packages/core/src/tracing/langgraph/index.ts | 86 +++---------- .../integrations/tracing-channel/langgraph.ts | 114 +++++++----------- 7 files changed, 95 insertions(+), 211 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index 438b2572f682..ff01d0d8a266 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -256,6 +256,12 @@ Affected SDKs: All SDKs. If you reference these attributes in custom instrumentation, `beforeSendSpan`, dashboards, or alerts, update them to the new names. +### LangGraph no longer emits `create_agent` spans + +Affected SDKs: All server-side SDKs. + +The LangGraph instrumentation no longer emits `gen_ai.create_agent` spans when a graph is compiled. `gen_ai.invoke_agent` and `gen_ai.execute_tool` spans are unaffected. If you reference `create_agent` spans in dashboards or alerts, update them accordingly. + ### `thirdPartyErrorFilterIntegration` filters internal frames by default Affected SDKs: All SDKs. diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/langgraph/test.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/langgraph/test.ts index 6d3cffa6d14e..6615c79430b4 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/langgraph/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/langgraph/test.ts @@ -29,28 +29,11 @@ it('traces langgraph compile and invoke operations', async ({ signal }) => { const container = envelope[1]?.[1]?.[1] as any; expect(container).toBeDefined(); - expect(container.items).toHaveLength(2); + expect(container.items).toHaveLength(1); expect(container.items.map((span: SerializedStreamedSpan) => span.name).sort()).toEqual([ - 'create_agent weather_assistant', 'invoke_agent weather_assistant', ]); - const createAgentSpan = container.items.find( - (span: SerializedStreamedSpan) => span.name === 'create_agent weather_assistant', - ); - expect(createAgentSpan).toBeDefined(); - expect(createAgentSpan!.status).toBe('ok'); - expect(createAgentSpan!.attributes[GEN_AI_OPERATION_NAME]).toEqual({ - type: 'string', - value: 'create_agent', - }); - expect(createAgentSpan!.attributes['sentry.op']).toEqual({ type: 'string', value: 'gen_ai.create_agent' }); - expect(createAgentSpan!.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.ai.langgraph' }); - expect(createAgentSpan!.attributes[GEN_AI_AGENT_NAME]).toEqual({ - type: 'string', - value: 'weather_assistant', - }); - const invokeAgentSpan = container.items.find( (span: SerializedStreamedSpan) => span.name === 'invoke_agent weather_assistant', ); diff --git a/dev-packages/deno-integration-tests/suites/orchestrion-langgraph/test.ts b/dev-packages/deno-integration-tests/suites/orchestrion-langgraph/test.ts index 5fd109a5a52b..dc1fed18b0d8 100644 --- a/dev-packages/deno-integration-tests/suites/orchestrion-langgraph/test.ts +++ b/dev-packages/deno-integration-tests/suites/orchestrion-langgraph/test.ts @@ -15,7 +15,7 @@ Deno.test('langgraph instrumentation: included in default integrations (Deno 2.8 assert(names.includes('LangGraph'), `LangGraph should be in defaults, got ${names.join(', ')}`); }); -Deno.test('langgraph instrumentation: orchestrion stateGraphCompile channel produces a nested create_agent span', async () => { +Deno.test('langgraph instrumentation: orchestrion stateGraphCompile channel wraps invoke without a create_agent span', async () => { resetGlobals(); const sink = transactionSink(); init({ @@ -27,12 +27,24 @@ Deno.test('langgraph instrumentation: orchestrion stateGraphCompile channel prod const channel = tracingChannel('orchestrion:@langchain/langgraph:stateGraphCompile'); - // `arguments[0]` is the compile options; `name` names the agent span. + // The subscriber replaces `invoke` on the compiled graph, so it needs to be a settable property. + let invoke = () => Promise.resolve('result'); + const compiledGraph = { + get invoke() { + return invoke; + }, + set invoke(fn) { + invoke = fn; + }, + }; + // `arguments[0]` is the compile options. const ctx: Record = { arguments: [{ name: 'my-agent' }] }; + const originalInvoke = compiledGraph.invoke; + startSpan({ name: 'parent', op: 'test' }, () => { channel.start.runStores(ctx, () => undefined); - ctx.result = {}; + ctx.result = compiledGraph; channel.end.publish(ctx); }); @@ -42,13 +54,7 @@ Deno.test('langgraph instrumentation: orchestrion stateGraphCompile channel prod "'parent' transaction", ); - const aiSpan = parent.spans?.find(s => s.op === 'gen_ai.create_agent'); - assertExists( - aiSpan, - `expected a gen_ai.create_agent child span, got ops: ${parent.spans?.map(s => s.op).join(', ')}`, - ); - assertEquals(aiSpan!.description, 'create_agent my-agent'); - assertEquals(aiSpan!.data?.['gen_ai.operation.name'], 'create_agent'); - assertEquals(aiSpan!.data?.['gen_ai.agent.name'], 'my-agent'); - assertEquals(aiSpan!.data?.['sentry.origin'], 'auto.ai.langgraph'); + const createAgentSpan = parent.spans?.find(s => s.op === 'gen_ai.create_agent'); + assertEquals(createAgentSpan, undefined, 'no gen_ai.create_agent span should be produced'); + assert(compiledGraph.invoke !== originalInvoke, "compiled graph's invoke should be wrapped"); }); 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 16d51d5f1962..91481ae3d82c 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts @@ -31,21 +31,12 @@ describe('LangGraph integration', () => { .expect({ transaction: { transaction: 'langgraph-test' } }) .expect({ span: container => { - expect(container.items).toHaveLength(3); + expect(container.items).toHaveLength(2); expect(container.items.map(span => span.name).sort()).toEqual([ - 'create_agent weather_assistant', 'invoke_agent weather_assistant', 'invoke_agent weather_assistant', ]); - const createAgentSpan = container.items.find(span => span.name === 'create_agent weather_assistant'); - expect(createAgentSpan).toBeDefined(); - expect(createAgentSpan!.status).toBe('ok'); - expect(createAgentSpan!.attributes['sentry.op'].value).toBe('gen_ai.create_agent'); - expect(createAgentSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langgraph'); - expect(createAgentSpan!.attributes[GEN_AI_OPERATION_NAME].value).toBe('create_agent'); - expect(createAgentSpan!.attributes[GEN_AI_AGENT_NAME].value).toBe('weather_assistant'); - const invokeAgentSpans = container.items.filter(span => span.name === 'invoke_agent weather_assistant'); expect(invokeAgentSpans).toHaveLength(2); for (const span of invokeAgentSpans) { @@ -70,11 +61,7 @@ describe('LangGraph integration', () => { .expect({ transaction: { transaction: 'langgraph-test' } }) .expect({ span: container => { - expect(container.items).toHaveLength(3); - const createAgentSpan = container.items.find(span => span.name === 'create_agent weather_assistant'); - expect(createAgentSpan).toBeDefined(); - expect(createAgentSpan!.status).toBe('ok'); - expect(createAgentSpan!.attributes['sentry.op'].value).toBe('gen_ai.create_agent'); + expect(container.items).toHaveLength(2); const weatherTodaySpan = container.items.find(span => getStringAttributeValue(span.attributes[GEN_AI_INPUT_MESSAGES]?.value)?.includes( @@ -110,20 +97,12 @@ describe('LangGraph integration', () => { .expect({ transaction: { transaction: 'langgraph-tools-test' } }) .expect({ span: container => { - expect(container.items).toHaveLength(4); + expect(container.items).toHaveLength(2); expect(container.items.map(span => span.name).sort()).toEqual([ - 'create_agent tool_agent', - 'create_agent tool_calling_agent', 'invoke_agent tool_agent', 'invoke_agent tool_calling_agent', ]); - const toolAgentSpan = container.items.find(span => span.name === 'create_agent tool_agent'); - expect(toolAgentSpan).toBeDefined(); - expect(toolAgentSpan!.status).toBe('ok'); - expect(toolAgentSpan!.attributes['sentry.op'].value).toBe('gen_ai.create_agent'); - expect(toolAgentSpan!.attributes[GEN_AI_AGENT_NAME].value).toBe('tool_agent'); - const toolAgentInvokeSpan = container.items.find(span => span.name === 'invoke_agent tool_agent'); expect(toolAgentInvokeSpan).toBeDefined(); expect(toolAgentInvokeSpan!.status).toBe('ok'); @@ -138,12 +117,6 @@ describe('LangGraph integration', () => { expect(toolAgentInvokeSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS].value).toBe(15); expect(toolAgentInvokeSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS].value).toBe(40); - const toolCallingAgentSpan = container.items.find(span => span.name === 'create_agent tool_calling_agent'); - expect(toolCallingAgentSpan).toBeDefined(); - expect(toolCallingAgentSpan!.status).toBe('ok'); - expect(toolCallingAgentSpan!.attributes['sentry.op'].value).toBe('gen_ai.create_agent'); - expect(toolCallingAgentSpan!.attributes[GEN_AI_AGENT_NAME].value).toBe('tool_calling_agent'); - const toolCallingInvokeSpan = container.items.find(span => span.name === 'invoke_agent tool_calling_agent'); expect(toolCallingInvokeSpan).toBeDefined(); expect(toolCallingInvokeSpan!.status).toBe('ok'); @@ -170,11 +143,7 @@ describe('LangGraph integration', () => { .expect({ transaction: { transaction: 'langgraph-thread-id-test' } }) .expect({ span: container => { - expect(container.items).toHaveLength(4); - const createAgentSpan = container.items.find(span => span.name === 'create_agent thread_test_agent'); - expect(createAgentSpan).toBeDefined(); - expect(createAgentSpan!.status).toBe('ok'); - expect(createAgentSpan!.attributes['sentry.op'].value).toBe('gen_ai.create_agent'); + expect(container.items).toHaveLength(3); const firstThreadSpan = container.items.find( span => span.attributes[GEN_AI_CONVERSATION_ID]?.value === 'thread_abc123_session_1', @@ -215,7 +184,7 @@ describe('LangGraph integration', () => { .expect({ transaction: { transaction: 'main' } }) .expect({ span: container => { - expect(container.items).toHaveLength(2); + expect(container.items).toHaveLength(1); const invokeAgentSpan = container.items.find(span => span.name === 'invoke_agent test-agent'); expect(invokeAgentSpan).toBeDefined(); @@ -247,12 +216,7 @@ describe('LangGraph integration', () => { }) .expect({ span: container => { - expect(container.items).toHaveLength(3); - const createAgentSpan = container.items.find(span => span.name === 'create_agent resume_agent'); - expect(createAgentSpan).toBeDefined(); - expect(createAgentSpan!.status).toBe('ok'); - expect(createAgentSpan!.attributes['sentry.op'].value).toBe('gen_ai.create_agent'); - expect(createAgentSpan!.attributes[GEN_AI_AGENT_NAME].value).toBe('resume_agent'); + expect(container.items).toHaveLength(2); const invokeAgentSpan = container.items.find( span => span.attributes[GEN_AI_CONVERSATION_ID]?.value === 'resume-thread-1', @@ -290,7 +254,7 @@ describe('LangGraph integration', () => { { role: 'user', content: 'Follow-up question' }, ]); - expect(container.items).toHaveLength(2); + expect(container.items).toHaveLength(1); const invokeAgentSpan = container.items.find( span => span.attributes[GEN_AI_INPUT_MESSAGES]?.value === expectedMessages, ); diff --git a/packages/core/src/server-exports.ts b/packages/core/src/server-exports.ts index 14486692830e..de095f47afb7 100644 --- a/packages/core/src/server-exports.ts +++ b/packages/core/src/server-exports.ts @@ -98,7 +98,6 @@ export { instrumentCreateReactAgent, instrumentStateGraph, instrumentCompiledGraphInvoke, - _INTERNAL_getLangGraphCreateAgentSpanOptions, } from './tracing/langgraph'; export { wrapToolsWithSpans, extractLLMFromParams, extractAgentNameFromParams } from './tracing/langgraph/utils'; export { LANGGRAPH_INTEGRATION_NAME } from './tracing/langgraph/constants'; diff --git a/packages/core/src/tracing/langgraph/index.ts b/packages/core/src/tracing/langgraph/index.ts index c5cdd753f79c..9a5963579990 100644 --- a/packages/core/src/tracing/langgraph/index.ts +++ b/packages/core/src/tracing/langgraph/index.ts @@ -20,7 +20,6 @@ import { shouldEnableTruncation, } from '../ai/utils'; import { stringify } from '../../utils/string'; -import type { SpanAttributeValue } from '../../types/span'; import { createLangChainCallbackHandler } from '../langchain'; import type { BaseChatModel, LangChainMessage } from '../langchain/types'; import { normalizeLangChainMessages } from '../langchain/utils'; @@ -41,40 +40,8 @@ let _insideCreateReactAgent = false; const SENTRY_PATCHED = '__sentry_patched__'; /** - * Builds the span options for a LangGraph `create_agent` span. - * - * @internal Exported so the diagnostics-channel (orchestrion) instrumentation can open the same span - * as the prototype-patching path below without re-declaring the semantic attribute keys. - */ -export function _INTERNAL_getLangGraphCreateAgentSpanOptions(agentName?: string): { - op: string; - name: string; - attributes: Record; -} { - const attributes: Record = { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: LANGGRAPH_ORIGIN, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.create_agent', - [GEN_AI_OPERATION_NAME]: 'create_agent', - }; - - if (agentName) { - attributes[GEN_AI_AGENT_NAME] = agentName; - } - - return { - op: 'gen_ai.create_agent', - name: agentName ? `create_agent ${agentName}` : 'create_agent', - attributes, - }; -} - -/** - * Instruments StateGraph's compile method to create spans for agent creation and invocation - * - * Wraps the compile() method to: - * - Create a `gen_ai.create_agent` span when compile() is called - * - Automatically wrap the invoke() method on the returned compiled graph with a `gen_ai.invoke_agent` span - * + * Instruments StateGraph's compile method to wrap the returned compiled graph's invoke() with a + * `gen_ai.invoke_agent` span. */ export function instrumentStateGraphCompile( originalCompile: (...args: unknown[]) => CompiledGraph, @@ -93,42 +60,23 @@ export function instrumentStateGraphCompile( return Reflect.apply(target, thisArg, args); } - return startSpan(_INTERNAL_getLangGraphCreateAgentSpanOptions(), span => { - try { - const compiledGraph = Reflect.apply(target, thisArg, args); - const compileOptions = args.length > 0 ? (args[0] as Record) : {}; + const compiledGraph = Reflect.apply(target, thisArg, args); + const compileOptions = args.length > 0 ? (args[0] as Record) : {}; - // Extract graph name - if (compileOptions?.name && typeof compileOptions.name === 'string') { - span.setAttribute(GEN_AI_AGENT_NAME, compileOptions.name); - span.updateName(`create_agent ${compileOptions.name}`); - } - - // Instrument agent invoke method on the compiled graph - const originalInvoke = compiledGraph.invoke; - if (originalInvoke && typeof originalInvoke === 'function') { - compiledGraph.invoke = instrumentCompiledGraphInvoke( - originalInvoke.bind(compiledGraph) as (...args: unknown[]) => Promise, - compiledGraph, - compileOptions, - options, - undefined, - sentryHandler, - ); - } + // Instrument agent invoke method on the compiled graph + const originalInvoke = compiledGraph.invoke; + if (originalInvoke && typeof originalInvoke === 'function') { + compiledGraph.invoke = instrumentCompiledGraphInvoke( + originalInvoke.bind(compiledGraph) as (...args: unknown[]) => Promise, + compiledGraph, + compileOptions, + options, + undefined, + sentryHandler, + ); + } - return compiledGraph; - } catch (error) { - span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); - captureException(error, { - mechanism: { - handled: false, - type: 'auto.ai.langgraph.error', - }, - }); - throw error; - } - }); + return compiledGraph; }, }); diff --git a/packages/server-utils/src/integrations/tracing-channel/langgraph.ts b/packages/server-utils/src/integrations/tracing-channel/langgraph.ts index 524dd1174060..871985e138ec 100644 --- a/packages/server-utils/src/integrations/tracing-channel/langgraph.ts +++ b/packages/server-utils/src/integrations/tracing-channel/langgraph.ts @@ -1,7 +1,6 @@ import * as diagnosticsChannel from 'node:diagnostics_channel'; import type { CompiledGraph, IntegrationFn, LangGraphOptions } from '@sentry/core'; import { - _INTERNAL_getLangGraphCreateAgentSpanOptions, createLangChainCallbackHandler, debug, defineIntegration, @@ -10,13 +9,10 @@ import { instrumentCompiledGraphInvoke, LANGGRAPH_INTEGRATION_NAME, resolveAIRecordingOptions, - startInactiveSpan, - waitForTracingChannelBinding, wrapToolsWithSpans, } from '@sentry/core'; import { DEBUG_BUILD } from '../../debug-build'; import { CHANNELS } from '../../orchestrion/channels'; -import { bindTracingChannelToSpan } from '../../tracing-channel'; // Same name as the OTel integration by design, so the OTel 'LangGraph' integration is // deduplicated out of the default set. @@ -34,8 +30,8 @@ interface CreateReactAgentChannelContext { let subscribed = false; -// `createReactAgent` compiles a `StateGraph` internally; suppress the `create_agent` span for that -// nested compile so a react agent gets a single `invoke_agent` span, matching the OTel path. +// `createReactAgent` compiles a `StateGraph` internally. When set, the compile subscriber skips that +// nested graph so its `invoke` is wrapped once (by the createReactAgent handler), not twice. let insideCreateReactAgent = false; const _langGraphIntegration = ((options: LangGraphOptions = {}) => { @@ -51,70 +47,52 @@ const _langGraphIntegration = ((options: LangGraphOptions = {}) => { const resolvedOptions = resolveAIRecordingOptions(options); const sentryHandler = createLangChainCallbackHandler(resolvedOptions); - // `bindTracingChannelToSpan` needs the async-context binding that `initOpenTelemetry()` registers - // after `setupOnce` runs, so wait for it before subscribing. - waitForTracingChannelBinding(() => { - // StateGraph.compile → `create_agent` span, then wrap the returned graph's `invoke`. - DEBUG_BUILD && - debug.log(`[orchestrion:langgraph] subscribing to channel "${CHANNELS.LANGGRAPH_STATE_GRAPH_COMPILE}"`); - bindTracingChannelToSpan( - diagnosticsChannel.tracingChannel(CHANNELS.LANGGRAPH_STATE_GRAPH_COMPILE), - data => { - if (insideCreateReactAgent) { - return undefined; - } - const compileOptions = getFirstArgObject(data.arguments); - const name = typeof compileOptions?.name === 'string' ? compileOptions.name : undefined; - - return startInactiveSpan(_INTERNAL_getLangGraphCreateAgentSpanOptions(name)); - }, - { - beforeSpanEnd: (_span, data) => { - wrapCompiledGraphInvoke( - data.result, - getFirstArgObject(data.arguments) ?? {}, - resolvedOptions, - null, - sentryHandler, - ); - }, - }, - ); - - // createReactAgent has no `create_agent` span of its own; it only wraps tools and the returned - // graph's `invoke`. Tools are wrapped at `start` (before the agent runs), invoke at `end`. - DEBUG_BUILD && - debug.log(`[orchestrion:langgraph] subscribing to channel "${CHANNELS.LANGGRAPH_CREATE_REACT_AGENT}"`); - const reactAgentChannel = diagnosticsChannel.tracingChannel( - CHANNELS.LANGGRAPH_CREATE_REACT_AGENT, - ); - reactAgentChannel.start.subscribe(message => { - // `createReactAgent` runs synchronously and compiles a `StateGraph` internally, so the flag - // must be on for the duration and off by `end`. It's set here (never in a branch that can - // throw) and cleared in both `end` and `error`, so it can neither stick on across calls nor - // stay off during this call's nested compile. Tool wrapping is guarded for the same reason. - insideCreateReactAgent = true; - try { - const { arguments: args } = message as CreateReactAgentChannelContext; - const params = getFirstArgObject(args); - if (params && Array.isArray(params.tools) && params.tools.length > 0) { - wrapToolsWithSpans(params.tools, resolvedOptions, extractAgentNameFromParams(args) ?? undefined); - } - } catch (error) { - DEBUG_BUILD && debug.error('[orchestrion:langgraph] failed to wrap createReactAgent tools', error); + // StateGraph.compile returns synchronously; wrap the returned graph's `invoke` at `end`. + DEBUG_BUILD && + debug.log(`[orchestrion:langgraph] subscribing to channel "${CHANNELS.LANGGRAPH_STATE_GRAPH_COMPILE}"`); + diagnosticsChannel + .tracingChannel(CHANNELS.LANGGRAPH_STATE_GRAPH_COMPILE) + .end.subscribe(message => { + if (insideCreateReactAgent) { + return; } + const { arguments: args, result } = message as CompileChannelContext; + wrapCompiledGraphInvoke(result, getFirstArgObject(args) ?? {}, resolvedOptions, null, sentryHandler); }); - reactAgentChannel.end.subscribe(message => { - insideCreateReactAgent = false; - const { arguments: args, result } = message as CreateReactAgentChannelContext; - const agentName = extractAgentNameFromParams(args) ?? undefined; - const compileOptions = agentName ? { name: agentName } : {}; - wrapCompiledGraphInvoke(result, compileOptions, resolvedOptions, extractLLMFromParams(args), sentryHandler); - }); - // Make sure a thrown `createReactAgent` doesn't leave the suppression flag stuck on. - reactAgentChannel.error.subscribe(() => { - insideCreateReactAgent = false; - }); + + // createReactAgent only wraps tools and the returned graph's `invoke`. Tools are wrapped at + // `start` (before the agent runs), invoke at `end`. + DEBUG_BUILD && + debug.log(`[orchestrion:langgraph] subscribing to channel "${CHANNELS.LANGGRAPH_CREATE_REACT_AGENT}"`); + const reactAgentChannel = diagnosticsChannel.tracingChannel( + CHANNELS.LANGGRAPH_CREATE_REACT_AGENT, + ); + reactAgentChannel.start.subscribe(message => { + // `createReactAgent` runs synchronously and compiles a `StateGraph` internally, so the flag + // must be on for the duration and off by `end`. It's set here (never in a branch that can + // throw) and cleared in both `end` and `error`, so it can neither stick on across calls nor + // stay off during this call's nested compile. Tool wrapping is guarded for the same reason. + insideCreateReactAgent = true; + try { + const { arguments: args } = message as CreateReactAgentChannelContext; + const params = getFirstArgObject(args); + if (params && Array.isArray(params.tools) && params.tools.length > 0) { + wrapToolsWithSpans(params.tools, resolvedOptions, extractAgentNameFromParams(args) ?? undefined); + } + } catch (error) { + DEBUG_BUILD && debug.error('[orchestrion:langgraph] failed to wrap createReactAgent tools', error); + } + }); + reactAgentChannel.end.subscribe(message => { + insideCreateReactAgent = false; + const { arguments: args, result } = message as CreateReactAgentChannelContext; + const agentName = extractAgentNameFromParams(args) ?? undefined; + const compileOptions = agentName ? { name: agentName } : {}; + wrapCompiledGraphInvoke(result, compileOptions, resolvedOptions, extractLLMFromParams(args), sentryHandler); + }); + // Make sure a thrown `createReactAgent` doesn't leave the suppression flag stuck on. + reactAgentChannel.error.subscribe(() => { + insideCreateReactAgent = false; }); }, }; From 4edfe3ebe6738949f7e79e993c9a2aa0fddd1191 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Wed, 29 Jul 2026 18:06:10 +0200 Subject: [PATCH 2/4] test(langgraph): Describe deno compile test by current behavior MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename and refocus the orchestrion stateGraphCompile test to assert what it does now — wrapping the compiled graph's invoke — instead of framing it around the removed create_agent span. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../suites/orchestrion-langgraph/test.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/dev-packages/deno-integration-tests/suites/orchestrion-langgraph/test.ts b/dev-packages/deno-integration-tests/suites/orchestrion-langgraph/test.ts index dc1fed18b0d8..09c25445bd80 100644 --- a/dev-packages/deno-integration-tests/suites/orchestrion-langgraph/test.ts +++ b/dev-packages/deno-integration-tests/suites/orchestrion-langgraph/test.ts @@ -4,8 +4,6 @@ import { tracingChannel } from 'node:diagnostics_channel'; import type { DenoClient } from '@sentry/deno'; import { init, startSpan } from '@sentry/deno'; import { assert } from 'https://deno.land/std@0.212.0/assert/assert.ts'; -import { assertExists } from 'https://deno.land/std@0.212.0/assert/assert_exists.ts'; -import { assertEquals } from 'https://deno.land/std@0.212.0/assert/assert_equals.ts'; import { resetGlobals, transactionSink, withTimeout } from '../../src/index.ts'; Deno.test('langgraph instrumentation: included in default integrations (Deno 2.8.0+)', () => { @@ -15,7 +13,7 @@ Deno.test('langgraph instrumentation: included in default integrations (Deno 2.8 assert(names.includes('LangGraph'), `LangGraph should be in defaults, got ${names.join(', ')}`); }); -Deno.test('langgraph instrumentation: orchestrion stateGraphCompile channel wraps invoke without a create_agent span', async () => { +Deno.test('langgraph instrumentation: orchestrion stateGraphCompile channel wraps the compiled graph invoke', async () => { resetGlobals(); const sink = transactionSink(); init({ @@ -48,13 +46,11 @@ Deno.test('langgraph instrumentation: orchestrion stateGraphCompile channel wrap channel.end.publish(ctx); }); - const parent = await withTimeout( + await withTimeout( sink.waitFor(t => t.transaction === 'parent'), 5000, "'parent' transaction", ); - const createAgentSpan = parent.spans?.find(s => s.op === 'gen_ai.create_agent'); - assertEquals(createAgentSpan, undefined, 'no gen_ai.create_agent span should be produced'); assert(compiledGraph.invoke !== originalInvoke, "compiled graph's invoke should be wrapped"); }); From c6d84232ec9aeb2bc72388f3a5d12a98f2d9189f Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Wed, 29 Jul 2026 18:07:31 +0200 Subject: [PATCH 3/4] test(langgraph): Simplify deno compile test graph mock A plain writable `invoke` property is enough; the getter/setter was unnecessary. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../suites/orchestrion-langgraph/test.ts | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/dev-packages/deno-integration-tests/suites/orchestrion-langgraph/test.ts b/dev-packages/deno-integration-tests/suites/orchestrion-langgraph/test.ts index 09c25445bd80..7b8d2203ae00 100644 --- a/dev-packages/deno-integration-tests/suites/orchestrion-langgraph/test.ts +++ b/dev-packages/deno-integration-tests/suites/orchestrion-langgraph/test.ts @@ -25,21 +25,11 @@ Deno.test('langgraph instrumentation: orchestrion stateGraphCompile channel wrap const channel = tracingChannel('orchestrion:@langchain/langgraph:stateGraphCompile'); - // The subscriber replaces `invoke` on the compiled graph, so it needs to be a settable property. - let invoke = () => Promise.resolve('result'); - const compiledGraph = { - get invoke() { - return invoke; - }, - set invoke(fn) { - invoke = fn; - }, - }; + const originalInvoke = () => Promise.resolve('result'); + const compiledGraph = { invoke: originalInvoke }; // `arguments[0]` is the compile options. const ctx: Record = { arguments: [{ name: 'my-agent' }] }; - const originalInvoke = compiledGraph.invoke; - startSpan({ name: 'parent', op: 'test' }, () => { channel.start.runStores(ctx, () => undefined); ctx.result = compiledGraph; From 7af8bf093c605055198f789df1e153431c0c3b84 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Thu, 30 Jul 2026 09:10:47 +0200 Subject: [PATCH 4/4] test(langgraph): Assert invoke_agent span in deno compile test Call the wrapped invoke and assert the invoke_agent span it opens, instead of only checking that the property changed. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../suites/orchestrion-langgraph/test.ts | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/dev-packages/deno-integration-tests/suites/orchestrion-langgraph/test.ts b/dev-packages/deno-integration-tests/suites/orchestrion-langgraph/test.ts index 7b8d2203ae00..ec4d010110f4 100644 --- a/dev-packages/deno-integration-tests/suites/orchestrion-langgraph/test.ts +++ b/dev-packages/deno-integration-tests/suites/orchestrion-langgraph/test.ts @@ -4,6 +4,8 @@ import { tracingChannel } from 'node:diagnostics_channel'; import type { DenoClient } from '@sentry/deno'; import { init, startSpan } from '@sentry/deno'; import { assert } from 'https://deno.land/std@0.212.0/assert/assert.ts'; +import { assertEquals } from 'https://deno.land/std@0.212.0/assert/assert_equals.ts'; +import { assertExists } from 'https://deno.land/std@0.212.0/assert/assert_exists.ts'; import { resetGlobals, transactionSink, withTimeout } from '../../src/index.ts'; Deno.test('langgraph instrumentation: included in default integrations (Deno 2.8.0+)', () => { @@ -27,20 +29,30 @@ Deno.test('langgraph instrumentation: orchestrion stateGraphCompile channel wrap const originalInvoke = () => Promise.resolve('result'); const compiledGraph = { invoke: originalInvoke }; - // `arguments[0]` is the compile options. + // `arguments[0]` is the compile options; `name` names the wrapped invoke_agent span. const ctx: Record = { arguments: [{ name: 'my-agent' }] }; - startSpan({ name: 'parent', op: 'test' }, () => { + await startSpan({ name: 'parent', op: 'test' }, async () => { channel.start.runStores(ctx, () => undefined); ctx.result = compiledGraph; channel.end.publish(ctx); + assert(compiledGraph.invoke !== originalInvoke, "compiled graph's invoke should be wrapped"); + await compiledGraph.invoke(); }); - await withTimeout( + const parent = await withTimeout( sink.waitFor(t => t.transaction === 'parent'), 5000, "'parent' transaction", ); - assert(compiledGraph.invoke !== originalInvoke, "compiled graph's invoke should be wrapped"); + const invokeAgentSpan = parent.spans?.find(s => s.op === 'gen_ai.invoke_agent'); + assertExists( + invokeAgentSpan, + `expected a gen_ai.invoke_agent child span, got ops: ${parent.spans?.map(s => s.op).join(', ')}`, + ); + assertEquals(invokeAgentSpan!.description, 'invoke_agent my-agent'); + assertEquals(invokeAgentSpan!.data?.['gen_ai.operation.name'], 'invoke_agent'); + assertEquals(invokeAgentSpan!.data?.['gen_ai.agent.name'], 'my-agent'); + assertEquals(invokeAgentSpan!.data?.['sentry.origin'], 'auto.ai.langgraph'); });