Skip to content

Commit 61b7d52

Browse files
nicohrubecclaude
andauthored
ref(langgraph)!: Drop gen_ai.create_agent spans (#22840)
Drops the `gen_ai.create_agent` spans from the LangGraph instrumentation, which are no longer something we should be emitting given our new protocol. Fixes #22335 Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent fa8babb commit 61b7d52

7 files changed

Lines changed: 93 additions & 211 deletions

File tree

MIGRATION.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,12 @@ Affected SDKs: All SDKs.
256256

257257
If you reference these attributes in custom instrumentation, `beforeSendSpan`, dashboards, or alerts, update them to the new names.
258258

259+
### LangGraph no longer emits `create_agent` spans
260+
261+
Affected SDKs: All server-side SDKs.
262+
263+
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.
264+
259265
### `thirdPartyErrorFilterIntegration` filters internal frames by default
260266

261267
Affected SDKs: All SDKs.

dev-packages/cloudflare-integration-tests/suites/tracing/langgraph/test.ts

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -29,28 +29,11 @@ it('traces langgraph compile and invoke operations', async ({ signal }) => {
2929
const container = envelope[1]?.[1]?.[1] as any;
3030
expect(container).toBeDefined();
3131

32-
expect(container.items).toHaveLength(2);
32+
expect(container.items).toHaveLength(1);
3333
expect(container.items.map((span: SerializedStreamedSpan) => span.name).sort()).toEqual([
34-
'create_agent weather_assistant',
3534
'invoke_agent weather_assistant',
3635
]);
3736

38-
const createAgentSpan = container.items.find(
39-
(span: SerializedStreamedSpan) => span.name === 'create_agent weather_assistant',
40-
);
41-
expect(createAgentSpan).toBeDefined();
42-
expect(createAgentSpan!.status).toBe('ok');
43-
expect(createAgentSpan!.attributes[GEN_AI_OPERATION_NAME]).toEqual({
44-
type: 'string',
45-
value: 'create_agent',
46-
});
47-
expect(createAgentSpan!.attributes['sentry.op']).toEqual({ type: 'string', value: 'gen_ai.create_agent' });
48-
expect(createAgentSpan!.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.ai.langgraph' });
49-
expect(createAgentSpan!.attributes[GEN_AI_AGENT_NAME]).toEqual({
50-
type: 'string',
51-
value: 'weather_assistant',
52-
});
53-
5437
const invokeAgentSpan = container.items.find(
5538
(span: SerializedStreamedSpan) => span.name === 'invoke_agent weather_assistant',
5639
);

dev-packages/deno-integration-tests/suites/orchestrion-langgraph/test.ts

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import { tracingChannel } from 'node:diagnostics_channel';
44
import type { DenoClient } from '@sentry/deno';
55
import { init, startSpan } from '@sentry/deno';
66
import { assert } from 'https://deno.land/std@0.212.0/assert/assert.ts';
7-
import { assertExists } from 'https://deno.land/std@0.212.0/assert/assert_exists.ts';
87
import { assertEquals } from 'https://deno.land/std@0.212.0/assert/assert_equals.ts';
8+
import { assertExists } from 'https://deno.land/std@0.212.0/assert/assert_exists.ts';
99
import { resetGlobals, transactionSink, withTimeout } from '../../src/index.ts';
1010

1111
Deno.test('langgraph instrumentation: included in default integrations (Deno 2.8.0+)', () => {
@@ -15,7 +15,7 @@ Deno.test('langgraph instrumentation: included in default integrations (Deno 2.8
1515
assert(names.includes('LangGraph'), `LangGraph should be in defaults, got ${names.join(', ')}`);
1616
});
1717

18-
Deno.test('langgraph instrumentation: orchestrion stateGraphCompile channel produces a nested create_agent span', async () => {
18+
Deno.test('langgraph instrumentation: orchestrion stateGraphCompile channel wraps the compiled graph invoke', async () => {
1919
resetGlobals();
2020
const sink = transactionSink();
2121
init({
@@ -27,13 +27,17 @@ Deno.test('langgraph instrumentation: orchestrion stateGraphCompile channel prod
2727

2828
const channel = tracingChannel('orchestrion:@langchain/langgraph:stateGraphCompile');
2929

30-
// `arguments[0]` is the compile options; `name` names the agent span.
30+
const originalInvoke = () => Promise.resolve('result');
31+
const compiledGraph = { invoke: originalInvoke };
32+
// `arguments[0]` is the compile options; `name` names the wrapped invoke_agent span.
3133
const ctx: Record<string, unknown> = { arguments: [{ name: 'my-agent' }] };
3234

33-
startSpan({ name: 'parent', op: 'test' }, () => {
35+
await startSpan({ name: 'parent', op: 'test' }, async () => {
3436
channel.start.runStores(ctx, () => undefined);
35-
ctx.result = {};
37+
ctx.result = compiledGraph;
3638
channel.end.publish(ctx);
39+
assert(compiledGraph.invoke !== originalInvoke, "compiled graph's invoke should be wrapped");
40+
await compiledGraph.invoke();
3741
});
3842

3943
const parent = await withTimeout(
@@ -42,13 +46,13 @@ Deno.test('langgraph instrumentation: orchestrion stateGraphCompile channel prod
4246
"'parent' transaction",
4347
);
4448

45-
const aiSpan = parent.spans?.find(s => s.op === 'gen_ai.create_agent');
49+
const invokeAgentSpan = parent.spans?.find(s => s.op === 'gen_ai.invoke_agent');
4650
assertExists(
47-
aiSpan,
48-
`expected a gen_ai.create_agent child span, got ops: ${parent.spans?.map(s => s.op).join(', ')}`,
51+
invokeAgentSpan,
52+
`expected a gen_ai.invoke_agent child span, got ops: ${parent.spans?.map(s => s.op).join(', ')}`,
4953
);
50-
assertEquals(aiSpan!.description, 'create_agent my-agent');
51-
assertEquals(aiSpan!.data?.['gen_ai.operation.name'], 'create_agent');
52-
assertEquals(aiSpan!.data?.['gen_ai.agent.name'], 'my-agent');
53-
assertEquals(aiSpan!.data?.['sentry.origin'], 'auto.ai.langgraph');
54+
assertEquals(invokeAgentSpan!.description, 'invoke_agent my-agent');
55+
assertEquals(invokeAgentSpan!.data?.['gen_ai.operation.name'], 'invoke_agent');
56+
assertEquals(invokeAgentSpan!.data?.['gen_ai.agent.name'], 'my-agent');
57+
assertEquals(invokeAgentSpan!.data?.['sentry.origin'], 'auto.ai.langgraph');
5458
});

dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts

Lines changed: 7 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -31,21 +31,12 @@ describe('LangGraph integration', () => {
3131
.expect({ transaction: { transaction: 'langgraph-test' } })
3232
.expect({
3333
span: container => {
34-
expect(container.items).toHaveLength(3);
34+
expect(container.items).toHaveLength(2);
3535
expect(container.items.map(span => span.name).sort()).toEqual([
36-
'create_agent weather_assistant',
3736
'invoke_agent weather_assistant',
3837
'invoke_agent weather_assistant',
3938
]);
4039

41-
const createAgentSpan = container.items.find(span => span.name === 'create_agent weather_assistant');
42-
expect(createAgentSpan).toBeDefined();
43-
expect(createAgentSpan!.status).toBe('ok');
44-
expect(createAgentSpan!.attributes['sentry.op'].value).toBe('gen_ai.create_agent');
45-
expect(createAgentSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langgraph');
46-
expect(createAgentSpan!.attributes[GEN_AI_OPERATION_NAME].value).toBe('create_agent');
47-
expect(createAgentSpan!.attributes[GEN_AI_AGENT_NAME].value).toBe('weather_assistant');
48-
4940
const invokeAgentSpans = container.items.filter(span => span.name === 'invoke_agent weather_assistant');
5041
expect(invokeAgentSpans).toHaveLength(2);
5142
for (const span of invokeAgentSpans) {
@@ -70,11 +61,7 @@ describe('LangGraph integration', () => {
7061
.expect({ transaction: { transaction: 'langgraph-test' } })
7162
.expect({
7263
span: container => {
73-
expect(container.items).toHaveLength(3);
74-
const createAgentSpan = container.items.find(span => span.name === 'create_agent weather_assistant');
75-
expect(createAgentSpan).toBeDefined();
76-
expect(createAgentSpan!.status).toBe('ok');
77-
expect(createAgentSpan!.attributes['sentry.op'].value).toBe('gen_ai.create_agent');
64+
expect(container.items).toHaveLength(2);
7865

7966
const weatherTodaySpan = container.items.find(span =>
8067
getStringAttributeValue(span.attributes[GEN_AI_INPUT_MESSAGES]?.value)?.includes(
@@ -110,20 +97,12 @@ describe('LangGraph integration', () => {
11097
.expect({ transaction: { transaction: 'langgraph-tools-test' } })
11198
.expect({
11299
span: container => {
113-
expect(container.items).toHaveLength(4);
100+
expect(container.items).toHaveLength(2);
114101
expect(container.items.map(span => span.name).sort()).toEqual([
115-
'create_agent tool_agent',
116-
'create_agent tool_calling_agent',
117102
'invoke_agent tool_agent',
118103
'invoke_agent tool_calling_agent',
119104
]);
120105

121-
const toolAgentSpan = container.items.find(span => span.name === 'create_agent tool_agent');
122-
expect(toolAgentSpan).toBeDefined();
123-
expect(toolAgentSpan!.status).toBe('ok');
124-
expect(toolAgentSpan!.attributes['sentry.op'].value).toBe('gen_ai.create_agent');
125-
expect(toolAgentSpan!.attributes[GEN_AI_AGENT_NAME].value).toBe('tool_agent');
126-
127106
const toolAgentInvokeSpan = container.items.find(span => span.name === 'invoke_agent tool_agent');
128107
expect(toolAgentInvokeSpan).toBeDefined();
129108
expect(toolAgentInvokeSpan!.status).toBe('ok');
@@ -138,12 +117,6 @@ describe('LangGraph integration', () => {
138117
expect(toolAgentInvokeSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS].value).toBe(15);
139118
expect(toolAgentInvokeSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS].value).toBe(40);
140119

141-
const toolCallingAgentSpan = container.items.find(span => span.name === 'create_agent tool_calling_agent');
142-
expect(toolCallingAgentSpan).toBeDefined();
143-
expect(toolCallingAgentSpan!.status).toBe('ok');
144-
expect(toolCallingAgentSpan!.attributes['sentry.op'].value).toBe('gen_ai.create_agent');
145-
expect(toolCallingAgentSpan!.attributes[GEN_AI_AGENT_NAME].value).toBe('tool_calling_agent');
146-
147120
const toolCallingInvokeSpan = container.items.find(span => span.name === 'invoke_agent tool_calling_agent');
148121
expect(toolCallingInvokeSpan).toBeDefined();
149122
expect(toolCallingInvokeSpan!.status).toBe('ok');
@@ -170,11 +143,7 @@ describe('LangGraph integration', () => {
170143
.expect({ transaction: { transaction: 'langgraph-thread-id-test' } })
171144
.expect({
172145
span: container => {
173-
expect(container.items).toHaveLength(4);
174-
const createAgentSpan = container.items.find(span => span.name === 'create_agent thread_test_agent');
175-
expect(createAgentSpan).toBeDefined();
176-
expect(createAgentSpan!.status).toBe('ok');
177-
expect(createAgentSpan!.attributes['sentry.op'].value).toBe('gen_ai.create_agent');
146+
expect(container.items).toHaveLength(3);
178147

179148
const firstThreadSpan = container.items.find(
180149
span => span.attributes[GEN_AI_CONVERSATION_ID]?.value === 'thread_abc123_session_1',
@@ -215,7 +184,7 @@ describe('LangGraph integration', () => {
215184
.expect({ transaction: { transaction: 'main' } })
216185
.expect({
217186
span: container => {
218-
expect(container.items).toHaveLength(2);
187+
expect(container.items).toHaveLength(1);
219188
const invokeAgentSpan = container.items.find(span => span.name === 'invoke_agent test-agent');
220189

221190
expect(invokeAgentSpan).toBeDefined();
@@ -247,12 +216,7 @@ describe('LangGraph integration', () => {
247216
})
248217
.expect({
249218
span: container => {
250-
expect(container.items).toHaveLength(3);
251-
const createAgentSpan = container.items.find(span => span.name === 'create_agent resume_agent');
252-
expect(createAgentSpan).toBeDefined();
253-
expect(createAgentSpan!.status).toBe('ok');
254-
expect(createAgentSpan!.attributes['sentry.op'].value).toBe('gen_ai.create_agent');
255-
expect(createAgentSpan!.attributes[GEN_AI_AGENT_NAME].value).toBe('resume_agent');
219+
expect(container.items).toHaveLength(2);
256220

257221
const invokeAgentSpan = container.items.find(
258222
span => span.attributes[GEN_AI_CONVERSATION_ID]?.value === 'resume-thread-1',
@@ -290,7 +254,7 @@ describe('LangGraph integration', () => {
290254
{ role: 'user', content: 'Follow-up question' },
291255
]);
292256

293-
expect(container.items).toHaveLength(2);
257+
expect(container.items).toHaveLength(1);
294258
const invokeAgentSpan = container.items.find(
295259
span => span.attributes[GEN_AI_INPUT_MESSAGES]?.value === expectedMessages,
296260
);

packages/core/src/server-exports.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,6 @@ export {
9898
instrumentCreateReactAgent,
9999
instrumentStateGraph,
100100
instrumentCompiledGraphInvoke,
101-
_INTERNAL_getLangGraphCreateAgentSpanOptions,
102101
} from './tracing/langgraph';
103102
export { wrapToolsWithSpans, extractLLMFromParams, extractAgentNameFromParams } from './tracing/langgraph/utils';
104103
export { LANGGRAPH_INTEGRATION_NAME } from './tracing/langgraph/constants';

packages/core/src/tracing/langgraph/index.ts

Lines changed: 17 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import {
2020
shouldEnableTruncation,
2121
} from '../ai/utils';
2222
import { stringify } from '../../utils/string';
23-
import type { SpanAttributeValue } from '../../types/span';
2423
import { createLangChainCallbackHandler } from '../langchain';
2524
import type { BaseChatModel, LangChainMessage } from '../langchain/types';
2625
import { normalizeLangChainMessages } from '../langchain/utils';
@@ -41,40 +40,8 @@ let _insideCreateReactAgent = false;
4140
const SENTRY_PATCHED = '__sentry_patched__';
4241

4342
/**
44-
* Builds the span options for a LangGraph `create_agent` span.
45-
*
46-
* @internal Exported so the diagnostics-channel (orchestrion) instrumentation can open the same span
47-
* as the prototype-patching path below without re-declaring the semantic attribute keys.
48-
*/
49-
export function _INTERNAL_getLangGraphCreateAgentSpanOptions(agentName?: string): {
50-
op: string;
51-
name: string;
52-
attributes: Record<string, SpanAttributeValue>;
53-
} {
54-
const attributes: Record<string, SpanAttributeValue> = {
55-
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: LANGGRAPH_ORIGIN,
56-
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.create_agent',
57-
[GEN_AI_OPERATION_NAME]: 'create_agent',
58-
};
59-
60-
if (agentName) {
61-
attributes[GEN_AI_AGENT_NAME] = agentName;
62-
}
63-
64-
return {
65-
op: 'gen_ai.create_agent',
66-
name: agentName ? `create_agent ${agentName}` : 'create_agent',
67-
attributes,
68-
};
69-
}
70-
71-
/**
72-
* Instruments StateGraph's compile method to create spans for agent creation and invocation
73-
*
74-
* Wraps the compile() method to:
75-
* - Create a `gen_ai.create_agent` span when compile() is called
76-
* - Automatically wrap the invoke() method on the returned compiled graph with a `gen_ai.invoke_agent` span
77-
*
43+
* Instruments StateGraph's compile method to wrap the returned compiled graph's invoke() with a
44+
* `gen_ai.invoke_agent` span.
7845
*/
7946
export function instrumentStateGraphCompile(
8047
originalCompile: (...args: unknown[]) => CompiledGraph,
@@ -93,42 +60,23 @@ export function instrumentStateGraphCompile(
9360
return Reflect.apply(target, thisArg, args);
9461
}
9562

96-
return startSpan(_INTERNAL_getLangGraphCreateAgentSpanOptions(), span => {
97-
try {
98-
const compiledGraph = Reflect.apply(target, thisArg, args);
99-
const compileOptions = args.length > 0 ? (args[0] as Record<string, unknown>) : {};
63+
const compiledGraph = Reflect.apply(target, thisArg, args);
64+
const compileOptions = args.length > 0 ? (args[0] as Record<string, unknown>) : {};
10065

101-
// Extract graph name
102-
if (compileOptions?.name && typeof compileOptions.name === 'string') {
103-
span.setAttribute(GEN_AI_AGENT_NAME, compileOptions.name);
104-
span.updateName(`create_agent ${compileOptions.name}`);
105-
}
106-
107-
// Instrument agent invoke method on the compiled graph
108-
const originalInvoke = compiledGraph.invoke;
109-
if (originalInvoke && typeof originalInvoke === 'function') {
110-
compiledGraph.invoke = instrumentCompiledGraphInvoke(
111-
originalInvoke.bind(compiledGraph) as (...args: unknown[]) => Promise<unknown>,
112-
compiledGraph,
113-
compileOptions,
114-
options,
115-
undefined,
116-
sentryHandler,
117-
);
118-
}
66+
// Instrument agent invoke method on the compiled graph
67+
const originalInvoke = compiledGraph.invoke;
68+
if (originalInvoke && typeof originalInvoke === 'function') {
69+
compiledGraph.invoke = instrumentCompiledGraphInvoke(
70+
originalInvoke.bind(compiledGraph) as (...args: unknown[]) => Promise<unknown>,
71+
compiledGraph,
72+
compileOptions,
73+
options,
74+
undefined,
75+
sentryHandler,
76+
);
77+
}
11978

120-
return compiledGraph;
121-
} catch (error) {
122-
span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' });
123-
captureException(error, {
124-
mechanism: {
125-
handled: false,
126-
type: 'auto.ai.langgraph.error',
127-
},
128-
});
129-
throw error;
130-
}
131-
});
79+
return compiledGraph;
13280
},
13381
});
13482

0 commit comments

Comments
 (0)