Skip to content

Commit e45920a

Browse files
committed
feat(core): Instrument LangGraph Agent
1 parent ea20d8d commit e45920a

File tree

22 files changed

+958
-1
lines changed

22 files changed

+958
-1
lines changed

.size-limit.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ module.exports = [
240240
import: createImport('init'),
241241
ignore: [...builtinModules, ...nodePrefixedBuiltinModules],
242242
gzip: true,
243-
limit: '158 KB',
243+
limit: '159 KB',
244244
},
245245
{
246246
name: '@sentry/node - without tracing',

dev-packages/node-integration-tests/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"@hono/node-server": "^1.19.4",
3232
"@langchain/anthropic": "^0.3.10",
3333
"@langchain/core": "^0.3.28",
34+
"@langchain/langgraph": "^0.2.32",
3435
"@nestjs/common": "^11",
3536
"@nestjs/core": "^11",
3637
"@nestjs/platform-express": "^11",
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import * as Sentry from '@sentry/node';
2+
import { loggingTransport } from '@sentry-internal/node-integration-tests';
3+
4+
Sentry.init({
5+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
6+
release: '1.0',
7+
tracesSampleRate: 1.0,
8+
sendDefaultPii: true,
9+
transport: loggingTransport,
10+
});
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import * as Sentry from '@sentry/node';
2+
import { loggingTransport } from '@sentry-internal/node-integration-tests';
3+
4+
Sentry.init({
5+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
6+
release: '1.0',
7+
tracesSampleRate: 1.0,
8+
sendDefaultPii: false,
9+
transport: loggingTransport,
10+
});
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { tool } from '@langchain/core/tools';
2+
import { END, MessagesAnnotation, START, StateGraph } from '@langchain/langgraph';
3+
import { ToolNode } from '@langchain/langgraph/prebuilt';
4+
import * as Sentry from '@sentry/node';
5+
import { z } from 'zod';
6+
7+
async function run() {
8+
await Sentry.startSpan({ op: 'function', name: 'langgraph-tools-test' }, async () => {
9+
// Define tools
10+
const getWeatherTool = tool(
11+
async ({ city }) => {
12+
return JSON.stringify({ city, temperature: 72, condition: 'sunny' });
13+
},
14+
{
15+
name: 'get_weather',
16+
description: 'Get the current weather for a given city',
17+
schema: z.object({
18+
city: z.string().describe('The city to get weather for'),
19+
}),
20+
},
21+
);
22+
23+
const getTimeTool = tool(
24+
async () => {
25+
return new Date().toISOString();
26+
},
27+
{
28+
name: 'get_time',
29+
description: 'Get the current time',
30+
schema: z.object({}),
31+
},
32+
);
33+
34+
const tools = [getWeatherTool, getTimeTool];
35+
const toolNode = new ToolNode(tools);
36+
37+
// Define mock LLM function that returns without tool calls
38+
const mockLlm = () => {
39+
return {
40+
messages: [
41+
{
42+
role: 'assistant',
43+
content: 'Response without calling tools',
44+
response_metadata: {
45+
model_name: 'gpt-4-0613',
46+
finish_reason: 'stop',
47+
tokenUsage: {
48+
promptTokens: 25,
49+
completionTokens: 15,
50+
totalTokens: 40,
51+
},
52+
},
53+
tool_calls: [],
54+
},
55+
],
56+
};
57+
};
58+
59+
// Routing function - check if there are tool calls
60+
const shouldContinue = state => {
61+
const messages = state.messages;
62+
const lastMessage = messages[messages.length - 1];
63+
64+
// If the last message has tool_calls, route to tools, otherwise end
65+
if (lastMessage.tool_calls && lastMessage.tool_calls.length > 0) {
66+
return 'tools';
67+
}
68+
return END;
69+
};
70+
71+
// Create graph with conditional edge to tools
72+
const graph = new StateGraph(MessagesAnnotation)
73+
.addNode('agent', mockLlm)
74+
.addNode('tools', toolNode)
75+
.addEdge(START, 'agent')
76+
.addConditionalEdges('agent', shouldContinue, {
77+
tools: 'tools',
78+
[END]: END,
79+
})
80+
.addEdge('tools', 'agent')
81+
.compile({ name: 'tool_agent' });
82+
83+
// Simple invocation - won't call tools since mockLlm returns empty tool_calls
84+
await graph.invoke({
85+
messages: [{ role: 'user', content: 'What is the weather?' }],
86+
});
87+
});
88+
89+
await Sentry.flush(2000);
90+
}
91+
92+
run();
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { END, MessagesAnnotation, START, StateGraph } from '@langchain/langgraph';
2+
import * as Sentry from '@sentry/node';
3+
4+
async function run() {
5+
await Sentry.startSpan({ op: 'function', name: 'langgraph-test' }, async () => {
6+
// Define a simple mock LLM function
7+
const mockLlm = () => {
8+
return {
9+
messages: [
10+
{
11+
role: 'assistant',
12+
content: 'Mock LLM response',
13+
response_metadata: {
14+
model_name: 'mock-model',
15+
finish_reason: 'stop',
16+
tokenUsage: {
17+
promptTokens: 20,
18+
completionTokens: 10,
19+
totalTokens: 30,
20+
},
21+
},
22+
},
23+
],
24+
};
25+
};
26+
27+
// Create and compile the graph
28+
const graph = new StateGraph(MessagesAnnotation)
29+
.addNode('agent', mockLlm)
30+
.addEdge(START, 'agent')
31+
.addEdge('agent', END)
32+
.compile({ name: 'weather_assistant' });
33+
34+
// Test: basic invocation
35+
await graph.invoke({
36+
messages: [{ role: 'user', content: 'What is the weather today?' }],
37+
});
38+
39+
// Test: invocation with multiple messages
40+
await graph.invoke({
41+
messages: [
42+
{ role: 'user', content: 'Hello' },
43+
{ role: 'assistant', content: 'Hi there!' },
44+
{ role: 'user', content: 'Tell me about the weather' },
45+
],
46+
});
47+
});
48+
49+
await Sentry.flush(2000);
50+
}
51+
52+
run();
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import { afterAll, describe, expect } from 'vitest';
2+
import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner';
3+
4+
describe('LangGraph integration', () => {
5+
afterAll(() => {
6+
cleanupChildProcesses();
7+
});
8+
9+
const EXPECTED_TRANSACTION_DEFAULT_PII_FALSE = {
10+
transaction: 'langgraph-test',
11+
spans: expect.arrayContaining([
12+
// create_agent span
13+
expect.objectContaining({
14+
data: {
15+
'gen_ai.operation.name': 'create_agent',
16+
'sentry.op': 'gen_ai.create_agent',
17+
'sentry.origin': 'auto.ai.langgraph',
18+
'gen_ai.agent.name': 'weather_assistant',
19+
},
20+
description: 'create_agent weather_assistant',
21+
op: 'gen_ai.create_agent',
22+
origin: 'auto.ai.langgraph',
23+
status: 'ok',
24+
}),
25+
// First invoke_agent span
26+
expect.objectContaining({
27+
data: expect.objectContaining({
28+
'gen_ai.operation.name': 'invoke_agent',
29+
'sentry.op': 'gen_ai.invoke_agent',
30+
'sentry.origin': 'auto.ai.langgraph',
31+
'gen_ai.agent.name': 'weather_assistant',
32+
'gen_ai.pipeline.name': 'weather_assistant',
33+
}),
34+
description: 'invoke_agent weather_assistant',
35+
op: 'gen_ai.invoke_agent',
36+
origin: 'auto.ai.langgraph',
37+
status: 'ok',
38+
}),
39+
// Second invoke_agent span
40+
expect.objectContaining({
41+
data: expect.objectContaining({
42+
'gen_ai.operation.name': 'invoke_agent',
43+
'sentry.op': 'gen_ai.invoke_agent',
44+
'sentry.origin': 'auto.ai.langgraph',
45+
'gen_ai.agent.name': 'weather_assistant',
46+
'gen_ai.pipeline.name': 'weather_assistant',
47+
}),
48+
description: 'invoke_agent weather_assistant',
49+
op: 'gen_ai.invoke_agent',
50+
origin: 'auto.ai.langgraph',
51+
status: 'ok',
52+
}),
53+
]),
54+
};
55+
56+
const EXPECTED_TRANSACTION_DEFAULT_PII_TRUE = {
57+
transaction: 'langgraph-test',
58+
spans: expect.arrayContaining([
59+
// create_agent span (PII enabled doesn't affect this span)
60+
expect.objectContaining({
61+
data: {
62+
'gen_ai.operation.name': 'create_agent',
63+
'sentry.op': 'gen_ai.create_agent',
64+
'sentry.origin': 'auto.ai.langgraph',
65+
'gen_ai.agent.name': 'weather_assistant',
66+
},
67+
description: 'create_agent weather_assistant',
68+
op: 'gen_ai.create_agent',
69+
origin: 'auto.ai.langgraph',
70+
status: 'ok',
71+
}),
72+
// First invoke_agent span with PII
73+
expect.objectContaining({
74+
data: expect.objectContaining({
75+
'gen_ai.operation.name': 'invoke_agent',
76+
'sentry.op': 'gen_ai.invoke_agent',
77+
'sentry.origin': 'auto.ai.langgraph',
78+
'gen_ai.agent.name': 'weather_assistant',
79+
'gen_ai.pipeline.name': 'weather_assistant',
80+
'gen_ai.request.messages': expect.stringContaining('What is the weather today?'),
81+
}),
82+
description: 'invoke_agent weather_assistant',
83+
op: 'gen_ai.invoke_agent',
84+
origin: 'auto.ai.langgraph',
85+
status: 'ok',
86+
}),
87+
// Second invoke_agent span with PII and multiple messages
88+
expect.objectContaining({
89+
data: expect.objectContaining({
90+
'gen_ai.operation.name': 'invoke_agent',
91+
'sentry.op': 'gen_ai.invoke_agent',
92+
'sentry.origin': 'auto.ai.langgraph',
93+
'gen_ai.agent.name': 'weather_assistant',
94+
'gen_ai.pipeline.name': 'weather_assistant',
95+
'gen_ai.request.messages': expect.stringContaining('Tell me about the weather'),
96+
}),
97+
description: 'invoke_agent weather_assistant',
98+
op: 'gen_ai.invoke_agent',
99+
origin: 'auto.ai.langgraph',
100+
status: 'ok',
101+
}),
102+
]),
103+
};
104+
105+
const EXPECTED_TRANSACTION_WITH_TOOLS = {
106+
transaction: 'langgraph-tools-test',
107+
spans: expect.arrayContaining([
108+
// create_agent span
109+
expect.objectContaining({
110+
data: {
111+
'gen_ai.operation.name': 'create_agent',
112+
'sentry.op': 'gen_ai.create_agent',
113+
'sentry.origin': 'auto.ai.langgraph',
114+
'gen_ai.agent.name': 'tool_agent',
115+
},
116+
description: 'create_agent tool_agent',
117+
op: 'gen_ai.create_agent',
118+
origin: 'auto.ai.langgraph',
119+
status: 'ok',
120+
}),
121+
// invoke_agent span with tools
122+
expect.objectContaining({
123+
data: expect.objectContaining({
124+
'gen_ai.operation.name': 'invoke_agent',
125+
'sentry.op': 'gen_ai.invoke_agent',
126+
'sentry.origin': 'auto.ai.langgraph',
127+
'gen_ai.agent.name': 'tool_agent',
128+
'gen_ai.pipeline.name': 'tool_agent',
129+
'gen_ai.request.available_tools': expect.stringContaining('get_weather'),
130+
'gen_ai.request.messages': expect.stringContaining('What is the weather?'),
131+
'gen_ai.response.model': 'gpt-4-0613',
132+
'gen_ai.response.finish_reasons': ['stop'],
133+
'gen_ai.response.text': expect.stringContaining('Response without calling tools'),
134+
'gen_ai.usage.input_tokens': 25,
135+
'gen_ai.usage.output_tokens': 15,
136+
'gen_ai.usage.total_tokens': 40,
137+
}),
138+
description: 'invoke_agent tool_agent',
139+
op: 'gen_ai.invoke_agent',
140+
origin: 'auto.ai.langgraph',
141+
status: 'ok',
142+
}),
143+
]),
144+
};
145+
146+
createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => {
147+
test('should instrument LangGraph with default PII settings', async () => {
148+
await createRunner()
149+
.ignore('event')
150+
.expect({ transaction: EXPECTED_TRANSACTION_DEFAULT_PII_FALSE })
151+
.start()
152+
.completed();
153+
});
154+
});
155+
156+
createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument-with-pii.mjs', (createRunner, test) => {
157+
test('should instrument LangGraph with sendDefaultPii: true', async () => {
158+
await createRunner()
159+
.ignore('event')
160+
.expect({ transaction: EXPECTED_TRANSACTION_DEFAULT_PII_TRUE })
161+
.start()
162+
.completed();
163+
});
164+
});
165+
166+
createEsmAndCjsTests(__dirname, 'scenario-tools.mjs', 'instrument-with-pii.mjs', (createRunner, test) => {
167+
test('should capture tools from LangGraph agent', { timeout: 30000 }, async () => {
168+
await createRunner().ignore('event').expect({ transaction: EXPECTED_TRANSACTION_WITH_TOOLS }).start().completed();
169+
});
170+
});
171+
});

packages/astro/src/index.server.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ export {
9595
onUnhandledRejectionIntegration,
9696
openAIIntegration,
9797
langChainIntegration,
98+
langgraphIntegration,
9899
parameterize,
99100
pinoIntegration,
100101
postgresIntegration,

packages/aws-serverless/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ export {
5858
onUnhandledRejectionIntegration,
5959
openAIIntegration,
6060
langChainIntegration,
61+
langgraphIntegration,
6162
modulesIntegration,
6263
contextLinesIntegration,
6364
nodeContextIntegration,

packages/bun/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ export {
7878
onUnhandledRejectionIntegration,
7979
openAIIntegration,
8080
langChainIntegration,
81+
langgraphIntegration,
8182
modulesIntegration,
8283
contextLinesIntegration,
8384
nodeContextIntegration,

0 commit comments

Comments
 (0)