diff --git a/dev-packages/cloudflare-integration-tests/package.json b/dev-packages/cloudflare-integration-tests/package.json index aac6e9c96945..c791a224a2cc 100644 --- a/dev-packages/cloudflare-integration-tests/package.json +++ b/dev-packages/cloudflare-integration-tests/package.json @@ -13,7 +13,8 @@ "test:watch": "yarn test --watch" }, "dependencies": { - "@sentry/cloudflare": "10.25.0" + "@sentry/cloudflare": "10.25.0", + "@langchain/langgraph": "^1.0.1" }, "devDependencies": { "@cloudflare/workers-types": "^4.20250922.0", diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/langgraph/index.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/langgraph/index.ts new file mode 100644 index 000000000000..6837a14be111 --- /dev/null +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/langgraph/index.ts @@ -0,0 +1,66 @@ +import { END, MessagesAnnotation, START, StateGraph } from '@langchain/langgraph'; +import * as Sentry from '@sentry/cloudflare'; + +interface Env { + SENTRY_DSN: string; +} + +export default Sentry.withSentry( + (env: Env) => ({ + dsn: env.SENTRY_DSN, + tracesSampleRate: 1.0, + sendDefaultPii: true, + }), + { + async fetch(_request, _env, _ctx) { + // Define simple mock LLM function + const mockLlm = (): { + messages: { + role: string; + content: string; + response_metadata: { + model_name: string; + finish_reason: string; + tokenUsage: { promptTokens: number; completionTokens: number; totalTokens: number }; + }; + tool_calls: never[]; + }[]; + } => { + return { + messages: [ + { + role: 'assistant', + content: 'Mock response from LangGraph agent', + response_metadata: { + model_name: 'mock-model', + finish_reason: 'stop', + tokenUsage: { + promptTokens: 20, + completionTokens: 10, + totalTokens: 30, + }, + }, + tool_calls: [], + }, + ], + }; + }; + + // Create and instrument the graph + const graph = new StateGraph(MessagesAnnotation) + .addNode('agent', mockLlm) + .addEdge(START, 'agent') + .addEdge('agent', END); + + Sentry.instrumentLangGraph(graph, { recordInputs: true, recordOutputs: true }); + + const compiled = graph.compile({ name: 'weather_assistant' }); + + await compiled.invoke({ + messages: [{ role: 'user', content: 'What is the weather in SF?' }], + }); + + return new Response(JSON.stringify({ success: true })); + }, + }, +); diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/langgraph/test.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/langgraph/test.ts new file mode 100644 index 000000000000..33023b30fa55 --- /dev/null +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/langgraph/test.ts @@ -0,0 +1,59 @@ +import { expect, it } from 'vitest'; +import { createRunner } from '../../../runner'; + +// These tests are not exhaustive because the instrumentation is +// already tested in the node integration tests and we merely +// want to test that the instrumentation does not break in our +// cloudflare SDK. + +it('traces langgraph compile and invoke operations', async ({ signal }) => { + const runner = createRunner(__dirname) + .ignore('event') + .expect(envelope => { + const transactionEvent = envelope[1]?.[0]?.[1] as any; + + expect(transactionEvent.transaction).toBe('GET /'); + + // Check create_agent span + const createAgentSpan = transactionEvent.spans.find((span: any) => span.op === 'gen_ai.create_agent'); + expect(createAgentSpan).toMatchObject({ + data: { + 'gen_ai.operation.name': 'create_agent', + 'sentry.op': 'gen_ai.create_agent', + 'sentry.origin': 'auto.ai.langgraph', + 'gen_ai.agent.name': 'weather_assistant', + }, + description: 'create_agent weather_assistant', + op: 'gen_ai.create_agent', + origin: 'auto.ai.langgraph', + }); + + // Check invoke_agent span + const invokeAgentSpan = transactionEvent.spans.find((span: any) => span.op === 'gen_ai.invoke_agent'); + expect(invokeAgentSpan).toMatchObject({ + data: expect.objectContaining({ + 'gen_ai.operation.name': 'invoke_agent', + 'sentry.op': 'gen_ai.invoke_agent', + 'sentry.origin': 'auto.ai.langgraph', + 'gen_ai.agent.name': 'weather_assistant', + 'gen_ai.pipeline.name': 'weather_assistant', + 'gen_ai.request.messages': '[{"role":"user","content":"What is the weather in SF?"}]', + 'gen_ai.response.model': 'mock-model', + 'gen_ai.usage.input_tokens': 20, + 'gen_ai.usage.output_tokens': 10, + 'gen_ai.usage.total_tokens': 30, + }), + description: 'invoke_agent weather_assistant', + op: 'gen_ai.invoke_agent', + origin: 'auto.ai.langgraph', + }); + + // Verify tools are captured + if (invokeAgentSpan.data['gen_ai.request.available_tools']) { + expect(invokeAgentSpan.data['gen_ai.request.available_tools']).toMatch(/get_weather/); + } + }) + .start(signal); + await runner.makeRequest('get', '/'); + await runner.completed(); +}); diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/langgraph/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/tracing/langgraph/wrangler.jsonc new file mode 100644 index 000000000000..d6be01281f0c --- /dev/null +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/langgraph/wrangler.jsonc @@ -0,0 +1,6 @@ +{ + "name": "worker-name", + "compatibility_date": "2025-06-17", + "main": "index.ts", + "compatibility_flags": ["nodejs_compat"], +} diff --git a/packages/cloudflare/src/index.ts b/packages/cloudflare/src/index.ts index 36de54816030..827c45327689 100644 --- a/packages/cloudflare/src/index.ts +++ b/packages/cloudflare/src/index.ts @@ -102,6 +102,7 @@ export { growthbookIntegration, logger, metrics, + instrumentLangGraph, } from '@sentry/core'; export { withSentry } from './handler'; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index ed3dbe4750d7..014a411d0265 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -152,7 +152,7 @@ export type { GoogleGenAIResponse } from './tracing/google-genai/types'; export { createLangChainCallbackHandler } from './tracing/langchain'; export { LANGCHAIN_INTEGRATION_NAME } from './tracing/langchain/constants'; export type { LangChainOptions, LangChainIntegration } from './tracing/langchain/types'; -export { instrumentStateGraphCompile } from './tracing/langgraph'; +export { instrumentStateGraphCompile, instrumentLangGraph } from './tracing/langgraph'; export { LANGGRAPH_INTEGRATION_NAME } from './tracing/langgraph/constants'; export type { LangGraphOptions, LangGraphIntegration, CompiledGraph } from './tracing/langgraph/types'; export type { OpenAiClient, OpenAiOptions, InstrumentedMethod } from './tracing/openai/types'; diff --git a/packages/core/src/tracing/langgraph/index.ts b/packages/core/src/tracing/langgraph/index.ts index 65d315bf3f63..5601cddf458b 100644 --- a/packages/core/src/tracing/langgraph/index.ts +++ b/packages/core/src/tracing/langgraph/index.ts @@ -155,3 +155,38 @@ function instrumentCompiledGraphInvoke( }, }) as (...args: unknown[]) => Promise; } + +/** + * Directly instruments a StateGraph instance to add tracing spans + * + * This function can be used to manually instrument LangGraph StateGraph instances + * in environments where automatic instrumentation is not available or desired. + * + * @param stateGraph - The StateGraph instance to instrument + * @param options - Optional configuration for recording inputs/outputs + * + * @example + * ```typescript + * import { instrumentLangGraph } from '@sentry/cloudflare'; + * import { StateGraph } from '@langchain/langgraph'; + * + * const graph = new StateGraph(MessagesAnnotation) + * .addNode('agent', mockLlm) + * .addEdge(START, 'agent') + * .addEdge('agent', END); + * + * instrumentLangGraph(graph, { recordInputs: true, recordOutputs: true }); + * const compiled = graph.compile({ name: 'my_agent' }); + * ``` + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function instrumentLangGraph any }>( + stateGraph: T, + options?: LangGraphOptions, +): T { + const _options: LangGraphOptions = options || {}; + + stateGraph.compile = instrumentStateGraphCompile(stateGraph.compile.bind(stateGraph), _options); + + return stateGraph; +} diff --git a/packages/vercel-edge/src/index.ts b/packages/vercel-edge/src/index.ts index 4f5017bb8f6c..8ece38279732 100644 --- a/packages/vercel-edge/src/index.ts +++ b/packages/vercel-edge/src/index.ts @@ -71,6 +71,7 @@ export { // eslint-disable-next-line deprecation/deprecation inboundFiltersIntegration, instrumentOpenAiClient, + instrumentLangGraph, instrumentGoogleGenAIClient, instrumentAnthropicAiClient, eventFiltersIntegration, diff --git a/yarn.lock b/yarn.lock index 0b2a0f0f31dc..082e1a032283 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4931,6 +4931,13 @@ zod "^3.25.32" zod-to-json-schema "^3.22.3" +"@langchain/langgraph-checkpoint@^1.0.0": + version "1.0.0" + resolved "https://registry.npmjs.org/@langchain/langgraph-checkpoint/-/langgraph-checkpoint-1.0.0.tgz#ece2ede439d0d0b0b532c4be7817fd5029afe4f8" + integrity sha512-xrclBGvNCXDmi0Nz28t3vjpxSH6UYx6w5XAXSiiB1WEdc2xD2iY/a913I3x3a31XpInUW/GGfXXfePfaghV54A== + dependencies: + uuid "^10.0.0" + "@langchain/langgraph-checkpoint@~0.0.17": version "0.0.18" resolved "https://registry.npmjs.org/@langchain/langgraph-checkpoint/-/langgraph-checkpoint-0.0.18.tgz#2f7a9cdeda948ccc8d312ba9463810709d71d0b8" @@ -4948,6 +4955,15 @@ p-retry "4" uuid "^9.0.0" +"@langchain/langgraph-sdk@~1.0.0": + version "1.0.0" + resolved "https://registry.npmjs.org/@langchain/langgraph-sdk/-/langgraph-sdk-1.0.0.tgz#16faca6cc426432dee9316428d0aecd94e5b7989" + integrity sha512-g25ti2W7Dl5wUPlNK+0uIGbeNFqf98imhHlbdVVKTTkDYLhi/pI1KTgsSSkzkeLuBIfvt2b0q6anQwCs7XBlbw== + dependencies: + p-queue "^6.6.2" + p-retry "4" + uuid "^9.0.0" + "@langchain/langgraph@^0.2.32": version "0.2.74" resolved "https://registry.npmjs.org/@langchain/langgraph/-/langgraph-0.2.74.tgz#37367a1e8bafda3548037a91449a69a84f285def" @@ -4958,6 +4974,15 @@ uuid "^10.0.0" zod "^3.23.8" +"@langchain/langgraph@^1.0.1": + version "1.0.2" + resolved "https://registry.npmjs.org/@langchain/langgraph/-/langgraph-1.0.2.tgz#62de931edac0dd850daf708bd6f8f3835cf25a5e" + integrity sha512-syxzzWTnmpCL+RhUEvalUeOXFoZy/KkzHa2Da2gKf18zsf9Dkbh3rfnRDrTyUGS1XSTejq07s4rg1qntdEDs2A== + dependencies: + "@langchain/langgraph-checkpoint" "^1.0.0" + "@langchain/langgraph-sdk" "~1.0.0" + uuid "^10.0.0" + "@leichtgewicht/ip-codec@^2.0.1": version "2.0.4" resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b"