Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +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 { 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 { 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+)', () => {
Expand All @@ -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 the compiled graph invoke', async () => {
resetGlobals();
const sink = transactionSink();
init({
Expand All @@ -27,13 +27,17 @@ 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.
const originalInvoke = () => Promise.resolve('result');
const compiledGraph = { invoke: originalInvoke };
// `arguments[0]` is the compile options; `name` names the wrapped invoke_agent span.
const ctx: Record<string, unknown> = { arguments: [{ name: 'my-agent' }] };

startSpan({ name: 'parent', op: 'test' }, () => {
await startSpan({ name: 'parent', op: 'test' }, async () => {
channel.start.runStores(ctx, () => undefined);
ctx.result = {};
ctx.result = compiledGraph;
channel.end.publish(ctx);
assert(compiledGraph.invoke !== originalInvoke, "compiled graph's invoke should be wrapped");
await compiledGraph.invoke();
});

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

const aiSpan = parent.spans?.find(s => s.op === 'gen_ai.create_agent');
const invokeAgentSpan = parent.spans?.find(s => s.op === 'gen_ai.invoke_agent');
assertExists(
aiSpan,
`expected a gen_ai.create_agent child span, got ops: ${parent.spans?.map(s => s.op).join(', ')}`,
invokeAgentSpan,
`expected a gen_ai.invoke_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');
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');
});
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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(
Expand Down Expand Up @@ -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');
Expand All @@ -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');
Expand All @@ -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',
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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,
);
Expand Down
1 change: 0 additions & 1 deletion packages/core/src/server-exports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
86 changes: 17 additions & 69 deletions packages/core/src/tracing/langgraph/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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<string, SpanAttributeValue>;
} {
const attributes: Record<string, SpanAttributeValue> = {
[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,
Expand All @@ -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<string, unknown>) : {};
const compiledGraph = Reflect.apply(target, thisArg, args);
const compileOptions = args.length > 0 ? (args[0] as Record<string, unknown>) : {};

// 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<unknown>,
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<unknown>,
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;
},
});

Expand Down
Loading
Loading