Skip to content

Commit b88aa39

Browse files
committed
feat(core): Instrument LangGraph Agent
1 parent f9e714f commit b88aa39

File tree

8 files changed

+171
-2
lines changed

8 files changed

+171
-2
lines changed

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
"test:watch": "yarn test --watch"
1414
},
1515
"dependencies": {
16-
"@sentry/cloudflare": "10.25.0"
16+
"@sentry/cloudflare": "10.25.0",
17+
"@langchain/langgraph": "^1.0.1"
1718
},
1819
"devDependencies": {
1920
"@cloudflare/workers-types": "^4.20250922.0",
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { END, MessagesAnnotation, START, StateGraph } from '@langchain/langgraph';
2+
import * as Sentry from '@sentry/cloudflare';
3+
4+
interface Env {
5+
SENTRY_DSN: string;
6+
}
7+
8+
export default Sentry.withSentry(
9+
(env: Env) => ({
10+
dsn: env.SENTRY_DSN,
11+
tracesSampleRate: 1.0,
12+
sendDefaultPii: true,
13+
}),
14+
{
15+
async fetch(_request, _env, _ctx) {
16+
// Define simple mock LLM function
17+
const mockLlm = (): {
18+
messages: {
19+
role: string;
20+
content: string;
21+
response_metadata: {
22+
model_name: string;
23+
finish_reason: string;
24+
tokenUsage: { promptTokens: number; completionTokens: number; totalTokens: number };
25+
};
26+
tool_calls: never[];
27+
}[];
28+
} => {
29+
return {
30+
messages: [
31+
{
32+
role: 'assistant',
33+
content: 'Mock response from LangGraph agent',
34+
response_metadata: {
35+
model_name: 'mock-model',
36+
finish_reason: 'stop',
37+
tokenUsage: {
38+
promptTokens: 20,
39+
completionTokens: 10,
40+
totalTokens: 30,
41+
},
42+
},
43+
tool_calls: [],
44+
},
45+
],
46+
};
47+
};
48+
49+
// Create and instrument the graph
50+
const graph = new StateGraph(MessagesAnnotation)
51+
.addNode('agent', mockLlm)
52+
.addEdge(START, 'agent')
53+
.addEdge('agent', END);
54+
55+
Sentry.instrumentLangGraph(graph, { recordInputs: true, recordOutputs: true });
56+
57+
const compiled = graph.compile({ name: 'weather_assistant' });
58+
59+
await compiled.invoke({
60+
messages: [{ role: 'user', content: 'What is the weather in SF?' }],
61+
});
62+
63+
return new Response(JSON.stringify({ success: true }));
64+
},
65+
},
66+
);
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { expect, it } from 'vitest';
2+
import { createRunner } from '../../../runner';
3+
4+
// These tests are not exhaustive because the instrumentation is
5+
// already tested in the node integration tests and we merely
6+
// want to test that the instrumentation does not break in our
7+
// cloudflare SDK.
8+
9+
it('traces langgraph compile and invoke operations', async ({ signal }) => {
10+
const runner = createRunner(__dirname)
11+
.ignore('event')
12+
.expect(envelope => {
13+
const transactionEvent = envelope[1]?.[0]?.[1] as any;
14+
15+
expect(transactionEvent.transaction).toBe('GET /');
16+
17+
// Check create_agent span
18+
const createAgentSpan = transactionEvent.spans.find((span: any) => span.op === 'gen_ai.create_agent');
19+
expect(createAgentSpan).toMatchObject({
20+
data: {
21+
'gen_ai.operation.name': 'create_agent',
22+
'sentry.op': 'gen_ai.create_agent',
23+
'sentry.origin': 'auto.ai.langgraph',
24+
'gen_ai.agent.name': 'weather_assistant',
25+
},
26+
description: 'create_agent weather_assistant',
27+
op: 'gen_ai.create_agent',
28+
origin: 'auto.ai.langgraph',
29+
});
30+
31+
// Check invoke_agent span
32+
const invokeAgentSpan = transactionEvent.spans.find((span: any) => span.op === 'gen_ai.invoke_agent');
33+
expect(invokeAgentSpan).toMatchObject({
34+
data: expect.objectContaining({
35+
'gen_ai.operation.name': 'invoke_agent',
36+
'sentry.op': 'gen_ai.invoke_agent',
37+
'sentry.origin': 'auto.ai.langgraph',
38+
'gen_ai.agent.name': 'weather_assistant',
39+
'gen_ai.pipeline.name': 'weather_assistant',
40+
'gen_ai.request.messages': '[{"role":"user","content":"What is the weather in SF?"}]',
41+
'gen_ai.response.model': 'mock-model',
42+
'gen_ai.usage.input_tokens': 20,
43+
'gen_ai.usage.output_tokens': 10,
44+
'gen_ai.usage.total_tokens': 30,
45+
}),
46+
description: 'invoke_agent weather_assistant',
47+
op: 'gen_ai.invoke_agent',
48+
origin: 'auto.ai.langgraph',
49+
});
50+
51+
// Verify tools are captured
52+
if (invokeAgentSpan.data['gen_ai.request.available_tools']) {
53+
expect(invokeAgentSpan.data['gen_ai.request.available_tools']).toMatch(/get_weather/);
54+
}
55+
})
56+
.start(signal);
57+
await runner.makeRequest('get', '/');
58+
await runner.completed();
59+
});
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"name": "worker-name",
3+
"compatibility_date": "2025-06-17",
4+
"main": "index.ts",
5+
"compatibility_flags": ["nodejs_compat"],
6+
}

packages/cloudflare/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ export {
102102
growthbookIntegration,
103103
logger,
104104
metrics,
105+
instrumentLangGraph,
105106
} from '@sentry/core';
106107

107108
export { withSentry } from './handler';

packages/core/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ export type { GoogleGenAIResponse } from './tracing/google-genai/types';
152152
export { createLangChainCallbackHandler } from './tracing/langchain';
153153
export { LANGCHAIN_INTEGRATION_NAME } from './tracing/langchain/constants';
154154
export type { LangChainOptions, LangChainIntegration } from './tracing/langchain/types';
155-
export { instrumentStateGraphCompile } from './tracing/langgraph';
155+
export { instrumentStateGraphCompile, instrumentLangGraph } from './tracing/langgraph';
156156
export { LANGGRAPH_INTEGRATION_NAME } from './tracing/langgraph/constants';
157157
export type { LangGraphOptions, LangGraphIntegration, CompiledGraph } from './tracing/langgraph/types';
158158
export type { OpenAiClient, OpenAiOptions, InstrumentedMethod } from './tracing/openai/types';

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

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,3 +155,38 @@ function instrumentCompiledGraphInvoke(
155155
},
156156
}) as (...args: unknown[]) => Promise<unknown>;
157157
}
158+
159+
/**
160+
* Directly instruments a StateGraph instance to add tracing spans
161+
*
162+
* This function can be used to manually instrument LangGraph StateGraph instances
163+
* in environments where automatic instrumentation is not available or desired.
164+
*
165+
* @param stateGraph - The StateGraph instance to instrument
166+
* @param options - Optional configuration for recording inputs/outputs
167+
*
168+
* @example
169+
* ```typescript
170+
* import { instrumentLangGraph } from '@sentry/cloudflare';
171+
* import { StateGraph } from '@langchain/langgraph';
172+
*
173+
* const graph = new StateGraph(MessagesAnnotation)
174+
* .addNode('agent', mockLlm)
175+
* .addEdge(START, 'agent')
176+
* .addEdge('agent', END);
177+
*
178+
* instrumentLangGraph(graph, { recordInputs: true, recordOutputs: true });
179+
* const compiled = graph.compile({ name: 'my_agent' });
180+
* ```
181+
*/
182+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
183+
export function instrumentLangGraph<T extends { compile: (...args: any[]) => any }>(
184+
stateGraph: T,
185+
options?: LangGraphOptions,
186+
): T {
187+
const _options: LangGraphOptions = options || {};
188+
189+
stateGraph.compile = instrumentStateGraphCompile(stateGraph.compile.bind(stateGraph), _options);
190+
191+
return stateGraph;
192+
}

packages/vercel-edge/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ export {
7171
// eslint-disable-next-line deprecation/deprecation
7272
inboundFiltersIntegration,
7373
instrumentOpenAiClient,
74+
instrumentLangGraph,
7475
instrumentGoogleGenAIClient,
7576
instrumentAnthropicAiClient,
7677
eventFiltersIntegration,

0 commit comments

Comments
 (0)