Skip to content

Commit 8637cdc

Browse files
committed
feat(cloudflare/vercel-edge): Add manual instrumentation for LangGraph
1 parent 5594f84 commit 8637cdc

File tree

9 files changed

+203
-4
lines changed

9 files changed

+203
-4
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"test:watch": "yarn test --watch"
1414
},
1515
"dependencies": {
16+
"@langchain/langgraph": "^1.0.1",
1617
"@sentry/cloudflare": "10.22.0"
1718
},
1819
"devDependencies": {
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: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"name": "worker-name",
3+
"compatibility_date": "2025-06-17",
4+
"main": "index.ts",
5+
"compatibility_flags": ["nodejs_compat"]
6+
}
7+

packages/cloudflare/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export type {
2121
export type { CloudflareOptions } from './client';
2222

2323
export {
24+
instrumentLangGraph,
2425
addEventProcessor,
2526
addBreadcrumb,
2627
addIntegration,

packages/core/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ export type { GoogleGenAIResponse } from './utils/google-genai/types';
147147
export { createLangChainCallbackHandler } from './utils/langchain';
148148
export { LANGCHAIN_INTEGRATION_NAME } from './utils/langchain/constants';
149149
export type { LangChainOptions, LangChainIntegration } from './utils/langchain/types';
150-
export { instrumentStateGraphCompile } from './tracing/langgraph';
150+
export { instrumentStateGraphCompile, instrumentLangGraph } from './tracing/langgraph';
151151
export { LANGGRAPH_INTEGRATION_NAME } from './tracing/langgraph/constants';
152152
export type { LangGraphOptions, LangGraphIntegration, CompiledGraph } from './tracing/langgraph/types';
153153
export type { OpenAiClient, OpenAiOptions, InstrumentedMethod } from './utils/openai/types';

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

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,45 @@ function instrumentCompiledGraphInvoke(
130130
}) as (...args: unknown[]) => Promise<unknown>;
131131
}
132132

133+
/**
134+
* Instrument a LangGraph StateGraph instance for manual instrumentation
135+
* Use this for Cloudflare Workers, Vercel Edge, and other environments without OpenTelemetry
136+
*
137+
* @example
138+
* ```typescript
139+
* import * as Sentry from '@sentry/cloudflare';
140+
* import { StateGraph, START, END, MessagesAnnotation } from '@langchain/langgraph';
141+
*
142+
* // Create and instrument the graph
143+
* const graph = new StateGraph(MessagesAnnotation)
144+
* .addNode('agent', agentFn)
145+
* .addEdge(START, 'agent')
146+
* .addEdge('agent', END);
147+
*
148+
* Sentry.instrumentLangGraph(graph, {
149+
* recordInputs: true,
150+
* recordOutputs: true,
151+
* });
152+
*
153+
* const compiled = graph.compile({ name: 'weather_assistant' });
154+
*
155+
* await compiled.invoke({
156+
* messages: [{ role: 'user', content: 'What is the weather in SF?' }],
157+
* });
158+
* ```
159+
*/
160+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
161+
export function instrumentLangGraph<T extends { compile: (...args: any[]) => any }>(
162+
stateGraph: T,
163+
options?: LangGraphOptions,
164+
): T {
165+
const _options: LangGraphOptions = options || {};
166+
167+
stateGraph.compile = instrumentStateGraphCompile(stateGraph.compile.bind(stateGraph), _options);
168+
169+
return stateGraph;
170+
}
171+
133172
/**
134173
* Extract tools from compiled graph structure
135174
*

packages/vercel-edge/src/index.ts

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

yarn.lock

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4926,6 +4926,13 @@
49264926
zod "^3.25.32"
49274927
zod-to-json-schema "^3.22.3"
49284928

4929+
"@langchain/langgraph-checkpoint@^1.0.0":
4930+
version "1.0.0"
4931+
resolved "https://registry.npmjs.org/@langchain/langgraph-checkpoint/-/langgraph-checkpoint-1.0.0.tgz#ece2ede439d0d0b0b532c4be7817fd5029afe4f8"
4932+
integrity sha512-xrclBGvNCXDmi0Nz28t3vjpxSH6UYx6w5XAXSiiB1WEdc2xD2iY/a913I3x3a31XpInUW/GGfXXfePfaghV54A==
4933+
dependencies:
4934+
uuid "^10.0.0"
4935+
49294936
"@langchain/langgraph-checkpoint@~0.0.17":
49304937
version "0.0.18"
49314938
resolved "https://registry.npmjs.org/@langchain/langgraph-checkpoint/-/langgraph-checkpoint-0.0.18.tgz#2f7a9cdeda948ccc8d312ba9463810709d71d0b8"
@@ -4943,6 +4950,15 @@
49434950
p-retry "4"
49444951
uuid "^9.0.0"
49454952

4953+
"@langchain/langgraph-sdk@~1.0.0":
4954+
version "1.0.0"
4955+
resolved "https://registry.npmjs.org/@langchain/langgraph-sdk/-/langgraph-sdk-1.0.0.tgz#16faca6cc426432dee9316428d0aecd94e5b7989"
4956+
integrity sha512-g25ti2W7Dl5wUPlNK+0uIGbeNFqf98imhHlbdVVKTTkDYLhi/pI1KTgsSSkzkeLuBIfvt2b0q6anQwCs7XBlbw==
4957+
dependencies:
4958+
p-queue "^6.6.2"
4959+
p-retry "4"
4960+
uuid "^9.0.0"
4961+
49464962
"@langchain/langgraph@^0.2.32":
49474963
version "0.2.74"
49484964
resolved "https://registry.npmjs.org/@langchain/langgraph/-/langgraph-0.2.74.tgz#37367a1e8bafda3548037a91449a69a84f285def"
@@ -4953,6 +4969,15 @@
49534969
uuid "^10.0.0"
49544970
zod "^3.23.8"
49554971

4972+
"@langchain/langgraph@^1.0.1":
4973+
version "1.0.1"
4974+
resolved "https://registry.npmjs.org/@langchain/langgraph/-/langgraph-1.0.1.tgz#d0be714653e8a27665f86ea795c5c34189455406"
4975+
integrity sha512-7y8OTDLrHrpJ55Y5x7c7zU2BbqNllXwxM106Xrd+NaQB5CpEb4hbUfIwe4XmhhscKPwvhXAq3tjeUxw9MCiurQ==
4976+
dependencies:
4977+
"@langchain/langgraph-checkpoint" "^1.0.0"
4978+
"@langchain/langgraph-sdk" "~1.0.0"
4979+
uuid "^10.0.0"
4980+
49564981
"@leichtgewicht/ip-codec@^2.0.1":
49574982
version "2.0.4"
49584983
resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b"
@@ -28465,7 +28490,7 @@ string-template@~0.2.1:
2846528490

2846628491
[email protected], "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
2846728492
version "4.2.3"
28468-
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
28493+
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
2846928494
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
2847028495
dependencies:
2847128496
emoji-regex "^8.0.0"
@@ -28575,7 +28600,7 @@ stringify-object@^3.2.1:
2857528600

2857628601
[email protected], strip-ansi@^6.0.0, strip-ansi@^6.0.1:
2857728602
version "6.0.1"
28578-
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
28603+
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
2857928604
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
2858028605
dependencies:
2858128606
ansi-regex "^5.0.1"
@@ -31570,7 +31595,7 @@ [email protected]:
3157031595

3157131596
[email protected], wrap-ansi@^7.0.0:
3157231597
version "7.0.0"
31573-
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
31598+
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
3157431599
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
3157531600
dependencies:
3157631601
ansi-styles "^4.0.0"

0 commit comments

Comments
 (0)