Skip to content
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();
});
});
});
2 changes: 1 addition & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, 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
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 Down Expand Up @@ -50,8 +50,31 @@ export class SentryLangGraphInstrumentation extends InstrumentationBase<LangGrap
this._patch.bind(this),
exports => exports,
),
new InstrumentationNodeModuleFile(
/**
* In CJS, LangGraph packages re-export from dist/prebuilt/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/prebuilt/index.cjs',
supportedVersions,
this._patch.bind(this),
exports => exports,
),
new InstrumentationNodeModuleFile(
/**
* ESM builds use dist/prebuilt/index.js (without .cjs extension)
* This catches ESM imports that resolve through the main package
*/
'@langchain/langgraph/dist/prebuilt/index.js',
supportedVersions,
this._patch.bind(this),
exports => exports,
),
],
);

return module;
}

Expand Down Expand Up @@ -83,6 +106,17 @@ export class SentryLangGraphInstrumentation extends InstrumentationBase<LangGrap
);
}

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

return exports;
}
}
Loading