Skip to content
Draft
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { ChatAnthropic } from '@langchain/anthropic';
import { HumanMessage, SystemMessage } from '@langchain/core/messages';
import { createReactAgent } from '@langchain/langgraph/prebuilt';
import * as Sentry from '@sentry/node';
import express from 'express';

function startMockAnthropicServer() {
const app = express();
app.use(express.json());

app.post('/v1/messages', (req, res) => {
const model = req.body.model;

// Simulate basic response
res.json({
id: 'msg_react_agent_123',
type: 'message',
role: 'assistant',
content: [
{
type: 'text',
text: 'Mock response from Anthropic!',
},
],
model: model,
stop_reason: 'end_turn',
stop_sequence: null,
usage: {
input_tokens: 10,
output_tokens: 15,
},
});
});

return new Promise(resolve => {
const server = app.listen(0, () => {
resolve(server);
});
});
}

async function run() {
const server = await startMockAnthropicServer();
const baseUrl = `http://localhost:${server.address().port}`;

await Sentry.startSpan({ op: 'function', name: 'main' }, async () => {
// Create mocked LLM instance
const llm = new ChatAnthropic({
model: 'claude-3-5-sonnet-20241022',
apiKey: 'mock-api-key',
clientOptions: {
baseURL: baseUrl,
},
});

// Create a simple react agent with no tools
const agent = createReactAgent({ llm, tools: [] });

// Test: basic invocation
await agent.invoke({
messages: [new SystemMessage('You are a helpful assistant.'), new HumanMessage('What is the weather today?')],
});
});

await Sentry.flush(2000);

server.close();
}

run();
Original file line number Diff line number Diff line change
Expand Up @@ -205,4 +205,44 @@ describe('LangGraph integration', () => {
await createRunner().ignore('event').expect({ transaction: EXPECTED_TRANSACTION_WITH_TOOLS }).start().completed();
});
});

const EXPECTED_TRANSACTION_REACT_AGENT = {
transaction: 'main',
spans: expect.arrayContaining([
// create_agent span
expect.objectContaining({
data: expect.objectContaining({
'gen_ai.operation.name': 'create_agent',
'sentry.op': 'gen_ai.create_agent',
'sentry.origin': 'auto.ai.langgraph',
}),
description: expect.stringContaining('create_agent'),
op: 'gen_ai.create_agent',
origin: 'auto.ai.langgraph',
status: 'ok',
}),
// invoke_agent span
expect.objectContaining({
data: expect.objectContaining({
'gen_ai.operation.name': 'invoke_agent',
'sentry.op': 'gen_ai.invoke_agent',
'sentry.origin': 'auto.ai.langgraph',
}),
description: expect.stringContaining('invoke_agent'),
op: 'gen_ai.invoke_agent',
origin: 'auto.ai.langgraph',
status: 'ok',
}),
]),
};

createEsmAndCjsTests(__dirname, 'agent-scenario.mjs', 'instrument.mjs', (createRunner, test) => {
test('should instrument LangGraph createReactAgent with default PII settings', async () => {
await createRunner()
.ignore('event')
.expect({ transaction: EXPECTED_TRANSACTION_REACT_AGENT })
.start()
.completed();
});
});
});
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,8 @@
"gauge/strip-ansi": "6.0.1",
"wide-align/string-width": "4.2.3",
"cliui/wrap-ansi": "7.0.0",
"sucrase": "getsentry/sucrase#es2020-polyfills"
"sucrase": "getsentry/sucrase#es2020-polyfills",
"import-in-the-middle": "^2.0.1"
},
"version": "0.0.0",
"name": "sentry-javascript",
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,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, instrumentLangGraph } from './tracing/langgraph';
export { instrumentStateGraphCompile, instrumentCreateReactAgent, 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';
Expand Down
52 changes: 52 additions & 0 deletions packages/core/src/tracing/langgraph/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,58 @@ function instrumentCompiledGraphInvoke(
}) as (...args: unknown[]) => Promise<unknown>;
}

/**
* Instruments createReactAgent to create spans for agent creation and invocation
*
* Creates a `gen_ai.create_agent` span when createReactAgent() is called
*/
export function instrumentCreateReactAgent(
originalCreateReactAgent: (...args: unknown[]) => CompiledGraph,
options: LangGraphOptions,
): (...args: unknown[]) => CompiledGraph {
return new Proxy(originalCreateReactAgent, {
apply(target, thisArg, args: unknown[]): CompiledGraph {
return startSpan(
{
op: 'gen_ai.create_agent',
name: 'create_agent',
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: LANGGRAPH_ORIGIN,
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.create_agent',
[GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'create_agent',
},
},
span => {
try {
const compiledGraph = Reflect.apply(target, thisArg, args);
const compiledOptions = args.length > 0 ? (args[0] as Record<string, unknown>) : {};
const originalInvoke = compiledGraph.invoke;
if (originalInvoke && typeof originalInvoke === 'function') {
compiledGraph.invoke = instrumentCompiledGraphInvoke(
originalInvoke.bind(compiledGraph) as (...args: unknown[]) => Promise<unknown>,
compiledGraph,
compiledOptions,
options,
) as typeof originalInvoke;
}

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;
}
},
);
},
}) as (...args: unknown[]) => CompiledGraph;
}

/**
* Directly instruments a StateGraph instance to add tracing spans
*
Expand Down
2 changes: 1 addition & 1 deletion packages/node-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@
"@apm-js-collab/tracing-hooks": "^0.3.1",
"@sentry/core": "10.32.1",
"@sentry/opentelemetry": "10.32.1",
"import-in-the-middle": "^2"
"import-in-the-middle": "^2.0.1"
},
"devDependencies": {
"@apm-js-collab/code-transformer": "^0.8.2",
Expand Down
2 changes: 1 addition & 1 deletion packages/node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@
"@sentry/core": "10.32.1",
"@sentry/node-core": "10.32.1",
"@sentry/opentelemetry": "10.32.1",
"import-in-the-middle": "^2",
"import-in-the-middle": "^2.0.1",
"minimatch": "^9.0.0"
},
"devDependencies": {
Expand Down
76 changes: 53 additions & 23 deletions packages/node/src/integrations/tracing/langgraph/instrumentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
InstrumentationNodeModuleFile,
} from '@opentelemetry/instrumentation';
import type { CompiledGraph, LangGraphOptions } from '@sentry/core';
import { getClient, instrumentStateGraphCompile, SDK_VERSION } from '@sentry/core';
import { getClient, instrumentCreateReactAgent, instrumentStateGraphCompile, SDK_VERSION } from '@sentry/core';

const supportedVersions = ['>=0.0.0 <2.0.0'];

Expand All @@ -18,6 +18,7 @@ type LangGraphInstrumentationOptions = InstrumentationConfig & LangGraphOptions;
interface PatchedModuleExports {
[key: string]: unknown;
StateGraph?: abstract new (...args: unknown[]) => unknown;
createReactAgent?: (...args: unknown[]) => CompiledGraph;
}

/**
Expand All @@ -31,34 +32,50 @@ export class SentryLangGraphInstrumentation extends InstrumentationBase<LangGrap
/**
* Initializes the instrumentation by defining the modules to be patched.
*/
public init(): InstrumentationModuleDefinition {
const module = new InstrumentationNodeModuleDefinition(
'@langchain/langgraph',
supportedVersions,
this._patch.bind(this),
exports => exports,
[
new InstrumentationNodeModuleFile(
/**
* In CJS, LangGraph packages re-export from dist/index.cjs files.
* Patching only the root module sometimes misses the real implementation or
* gets overwritten when that file is loaded. We add a file-level patch so that
* _patch runs again on the concrete implementation
*/
'@langchain/langgraph/dist/index.cjs',
supportedVersions,
this._patch.bind(this),
exports => exports,
),
],
);
return module;
public init(): InstrumentationModuleDefinition[] {
return [
new InstrumentationNodeModuleDefinition(
'@langchain/langgraph',
supportedVersions,
this._patch.bind(this),
exports => exports,
[
new InstrumentationNodeModuleFile(
/**
* In CJS, LangGraph packages re-export from dist/index.cjs files.
* Patching only the root module sometimes misses the real implementation or
* gets overwritten when that file is loaded. We add a file-level patch so that
* _patch runs again on the concrete implementation
*/
'@langchain/langgraph/dist/index.cjs',
supportedVersions,
this._patch.bind(this),
exports => exports,
),
],
),
new InstrumentationNodeModuleDefinition(
'@langchain/langgraph/prebuilt',
supportedVersions,
this._patch.bind(this),
exports => exports,
[
new InstrumentationNodeModuleFile(
'@langchain/langgraph/dist/prebuilt/index.js',
supportedVersions,
this._patch.bind(this),
exports => exports,
),
],
),
];
}

/**
* Core patch logic applying instrumentation to the LangGraph module.
*/
private _patch(exports: PatchedModuleExports): PatchedModuleExports | void {
console.log('SentryLangGraphInstrumentation _patch');
const client = getClient();
const defaultPii = Boolean(client?.getOptions().sendDefaultPii);

Expand All @@ -73,6 +90,7 @@ export class SentryLangGraphInstrumentation extends InstrumentationBase<LangGrap

// Patch StateGraph.compile to instrument both compile() and invoke()
if (exports.StateGraph && typeof exports.StateGraph === 'function') {
console.log('SentryLangGraphInstrumentation _patch StateGraph');
const StateGraph = exports.StateGraph as {
prototype: Record<string, unknown>;
};
Expand All @@ -83,6 +101,18 @@ export class SentryLangGraphInstrumentation extends InstrumentationBase<LangGrap
);
}

// Patch createReactAgent to instrument the agent creation and invocation
if (exports.createReactAgent && typeof exports.createReactAgent === 'function') {
console.log('SentryLangGraphInstrumentation _patch createReactAgent');
const originalCreateReactAgent = exports.createReactAgent;
Object.defineProperty(exports, 'createReactAgent', {
value: instrumentCreateReactAgent(originalCreateReactAgent as (...args: unknown[]) => CompiledGraph, options),
writable: true,
enumerable: true,
configurable: true,
});
}

return exports;
}
}
8 changes: 4 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -19238,10 +19238,10 @@ import-fresh@^3.2.1:
parent-module "^1.0.0"
resolve-from "^4.0.0"

import-in-the-middle@^2, import-in-the-middle@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/import-in-the-middle/-/import-in-the-middle-2.0.0.tgz#295948cee94d0565314824c6bd75379d13e5b1a5"
integrity sha512-yNZhyQYqXpkT0AKq3F3KLasUSK4fHvebNH5hOsKQw2dhGSALvQ4U0BqUc5suziKvydO5u5hgN2hy1RJaho8U5A==
import-in-the-middle@^2.0.0, import-in-the-middle@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/import-in-the-middle/-/import-in-the-middle-2.0.1.tgz#8d1aa2db18374f2c811de2aa4756ebd6e9859243"
integrity sha512-bruMpJ7xz+9jwGzrwEhWgvRrlKRYCRDBrfU+ur3FcasYXLJDxTruJ//8g2Noj+QFyRBeqbpj8Bhn4Fbw6HjvhA==
dependencies:
acorn "^8.14.0"
acorn-import-attributes "^1.9.5"
Expand Down
Loading