From 7a7d5b1f6d658832af1e129644197c0ed10f04c4 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Thu, 27 Nov 2025 14:46:26 +0100 Subject: [PATCH 1/2] feat(browser): Expose langgraph instrumentation --- .../tracing/ai-providers/langgraph/init.js | 9 ++++++ .../tracing/ai-providers/langgraph/mocks.js | 30 +++++++++++++++++++ .../tracing/ai-providers/langgraph/subject.js | 16 ++++++++++ .../tracing/ai-providers/langgraph/test.ts | 30 +++++++++++++++++++ .../utils/generatePlugin.ts | 1 + packages/browser/rollup.bundle.config.mjs | 1 + packages/browser/src/index.ts | 1 + .../index.instrumentlanggraph.ts | 1 + .../browser/src/utils/lazyLoadIntegration.ts | 1 + 9 files changed, 90 insertions(+) create mode 100644 dev-packages/browser-integration-tests/suites/tracing/ai-providers/langgraph/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/ai-providers/langgraph/mocks.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/ai-providers/langgraph/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/ai-providers/langgraph/test.ts create mode 100644 packages/browser/src/integrations-bundle/index.instrumentlanggraph.ts diff --git a/dev-packages/browser-integration-tests/suites/tracing/ai-providers/langgraph/init.js b/dev-packages/browser-integration-tests/suites/tracing/ai-providers/langgraph/init.js new file mode 100644 index 000000000000..d90a3acf6157 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/ai-providers/langgraph/init.js @@ -0,0 +1,9 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + tracesSampleRate: 1, + debug: true, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/ai-providers/langgraph/mocks.js b/dev-packages/browser-integration-tests/suites/tracing/ai-providers/langgraph/mocks.js new file mode 100644 index 000000000000..8f959fff9388 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/ai-providers/langgraph/mocks.js @@ -0,0 +1,30 @@ +// Mock LangGraph graph for browser testing + +export class MockStateGraph { + compile(options = {}) { + const compiledGraph = { + name: options.name, + graph_name: options.name, + lc_kwargs: { + name: options.name, + }, + builder: { + nodes: {}, + }, + invoke: async (input) => { + const messages = input?.messages; + return { + messages: [ + ...messages, + { + role: 'assistant', + content: 'Mock response from LangGraph', + }, + ], + }; + }, + }; + + return compiledGraph; + } +} diff --git a/dev-packages/browser-integration-tests/suites/tracing/ai-providers/langgraph/subject.js b/dev-packages/browser-integration-tests/suites/tracing/ai-providers/langgraph/subject.js new file mode 100644 index 000000000000..70741f5d111f --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/ai-providers/langgraph/subject.js @@ -0,0 +1,16 @@ +import { MockStateGraph } from './mocks.js'; +import { instrumentLangGraph } from '@sentry/browser'; + +// Test that manual instrumentation doesn't crash the browser +// The instrumentation automatically creates spans +// Test both agent creation and invocation + +const graph = new MockStateGraph(); +instrumentLangGraph(graph, { recordInputs: false, recordOutputs: false }); +const compiledGraph = graph.compile({ name: 'mock-graph' }); + +const response = await compiledGraph.invoke({ + messages: [{ role: 'user', content: 'What is the capital of France?' }], +}); + +console.log('Received response', response); diff --git a/dev-packages/browser-integration-tests/suites/tracing/ai-providers/langgraph/test.ts b/dev-packages/browser-integration-tests/suites/tracing/ai-providers/langgraph/test.ts new file mode 100644 index 000000000000..51fb1b69da31 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/ai-providers/langgraph/test.ts @@ -0,0 +1,30 @@ +import { expect } from '@playwright/test'; +import { sentryTest } from '../../../../utils/fixtures'; +import { envelopeRequestParser, waitForTransactionRequest } from '../../../../utils/helpers'; + +// 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 crash in the browser +// and that gen_ai transactions are sent. + +sentryTest('manual LangGraph instrumentation sends gen_ai transactions', async ({ getLocalTestUrl, page }) => { + const transactionPromise = waitForTransactionRequest(page, event => { + return !!event.transaction?.includes('mock-graph'); + }); + + const url = await getLocalTestUrl({ testDir: __dirname }); + await page.goto(url); + + const req = await transactionPromise; + + const eventData = envelopeRequestParser(req); + + // Verify it's a gen_ai transaction + expect(eventData.transaction).toBe('create_agent mock-graph'); + expect(eventData.contexts?.trace?.op).toBe('gen_ai.create_agent'); + expect(eventData.contexts?.trace?.origin).toBe('auto.ai.langgraph'); + expect(eventData.contexts?.trace?.data).toMatchObject({ + 'gen_ai.operation.name': 'create_agent', + 'gen_ai.agent.name': 'mock-graph', + }); +}); diff --git a/dev-packages/browser-integration-tests/utils/generatePlugin.ts b/dev-packages/browser-integration-tests/utils/generatePlugin.ts index 6e3ef99aa7ea..548dfb2a6150 100644 --- a/dev-packages/browser-integration-tests/utils/generatePlugin.ts +++ b/dev-packages/browser-integration-tests/utils/generatePlugin.ts @@ -40,6 +40,7 @@ const IMPORTED_INTEGRATION_CDN_BUNDLE_PATHS: Record = { instrumentAnthropicAiClient: 'instrumentanthropicaiclient', instrumentOpenAiClient: 'instrumentopenaiclient', instrumentGoogleGenAIClient: 'instrumentgooglegenaiclient', + instrumentLangGraph: 'instrumentlanggraph', // technically, this is not an integration, but let's add it anyway for simplicity makeMultiplexedTransport: 'multiplexedtransport', }; diff --git a/packages/browser/rollup.bundle.config.mjs b/packages/browser/rollup.bundle.config.mjs index 4893e66f49ef..13bdee685821 100644 --- a/packages/browser/rollup.bundle.config.mjs +++ b/packages/browser/rollup.bundle.config.mjs @@ -16,6 +16,7 @@ const reexportedPluggableIntegrationFiles = [ 'instrumentanthropicaiclient', 'instrumentopenaiclient', 'instrumentgooglegenaiclient', + 'instrumentlanggraph', ]; browserPluggableIntegrationFiles.forEach(integrationName => { diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index 03416fa41af7..a58714f312d7 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -66,6 +66,7 @@ export { instrumentAnthropicAiClient, instrumentOpenAiClient, instrumentGoogleGenAIClient, + instrumentLangGraph, logger, } from '@sentry/core'; export type { Span, FeatureFlagsIntegration } from '@sentry/core'; diff --git a/packages/browser/src/integrations-bundle/index.instrumentlanggraph.ts b/packages/browser/src/integrations-bundle/index.instrumentlanggraph.ts new file mode 100644 index 000000000000..c7a8c0e9e591 --- /dev/null +++ b/packages/browser/src/integrations-bundle/index.instrumentlanggraph.ts @@ -0,0 +1 @@ +export { instrumentLangGraph } from '@sentry/core'; diff --git a/packages/browser/src/utils/lazyLoadIntegration.ts b/packages/browser/src/utils/lazyLoadIntegration.ts index 6d5e48542f56..8eeb6b95b66b 100644 --- a/packages/browser/src/utils/lazyLoadIntegration.ts +++ b/packages/browser/src/utils/lazyLoadIntegration.ts @@ -24,6 +24,7 @@ const LazyLoadableIntegrations = { instrumentAnthropicAiClient: 'instrumentanthropicaiclient', instrumentOpenAiClient: 'instrumentopenaiclient', instrumentGoogleGenAIClient: 'instrumentgooglegenaiclient', + instrumentLangGraph: 'instrumentlanggraph', } as const; const WindowWithMaybeIntegration = WINDOW as { From b2d6c40363f90ba8ad4054735eca752c9e8f5d6f Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Thu, 27 Nov 2025 14:55:18 +0100 Subject: [PATCH 2/2] improve test --- .../tracing/ai-providers/langgraph/mocks.js | 3 +- .../tracing/ai-providers/langgraph/test.ts | 33 ++++++++++++++----- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/dev-packages/browser-integration-tests/suites/tracing/ai-providers/langgraph/mocks.js b/dev-packages/browser-integration-tests/suites/tracing/ai-providers/langgraph/mocks.js index 8f959fff9388..54792b827a43 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/ai-providers/langgraph/mocks.js +++ b/dev-packages/browser-integration-tests/suites/tracing/ai-providers/langgraph/mocks.js @@ -1,5 +1,4 @@ // Mock LangGraph graph for browser testing - export class MockStateGraph { compile(options = {}) { const compiledGraph = { @@ -11,7 +10,7 @@ export class MockStateGraph { builder: { nodes: {}, }, - invoke: async (input) => { + invoke: async input => { const messages = input?.messages; return { messages: [ diff --git a/dev-packages/browser-integration-tests/suites/tracing/ai-providers/langgraph/test.ts b/dev-packages/browser-integration-tests/suites/tracing/ai-providers/langgraph/test.ts index 51fb1b69da31..1feabd48c8d2 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/ai-providers/langgraph/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/ai-providers/langgraph/test.ts @@ -8,23 +8,38 @@ import { envelopeRequestParser, waitForTransactionRequest } from '../../../../ut // and that gen_ai transactions are sent. sentryTest('manual LangGraph instrumentation sends gen_ai transactions', async ({ getLocalTestUrl, page }) => { - const transactionPromise = waitForTransactionRequest(page, event => { - return !!event.transaction?.includes('mock-graph'); + const createTransactionPromise = waitForTransactionRequest(page, event => { + return !!event.transaction?.includes('create_agent mock-graph'); + }); + + const invokeTransactionPromise = waitForTransactionRequest(page, event => { + return !!event.transaction?.includes('invoke_agent mock-graph'); }); const url = await getLocalTestUrl({ testDir: __dirname }); await page.goto(url); - const req = await transactionPromise; + const createReq = await createTransactionPromise; + const invokeReq = await invokeTransactionPromise; - const eventData = envelopeRequestParser(req); + const createEventData = envelopeRequestParser(createReq); + const invokeEventData = envelopeRequestParser(invokeReq); - // Verify it's a gen_ai transaction - expect(eventData.transaction).toBe('create_agent mock-graph'); - expect(eventData.contexts?.trace?.op).toBe('gen_ai.create_agent'); - expect(eventData.contexts?.trace?.origin).toBe('auto.ai.langgraph'); - expect(eventData.contexts?.trace?.data).toMatchObject({ + // Verify create_agent transaction + expect(createEventData.transaction).toBe('create_agent mock-graph'); + expect(createEventData.contexts?.trace?.op).toBe('gen_ai.create_agent'); + expect(createEventData.contexts?.trace?.origin).toBe('auto.ai.langgraph'); + expect(createEventData.contexts?.trace?.data).toMatchObject({ 'gen_ai.operation.name': 'create_agent', 'gen_ai.agent.name': 'mock-graph', }); + + // Verify invoke_agent transaction + expect(invokeEventData.transaction).toBe('invoke_agent mock-graph'); + expect(invokeEventData.contexts?.trace?.op).toBe('gen_ai.invoke_agent'); + expect(invokeEventData.contexts?.trace?.origin).toBe('auto.ai.langgraph'); + expect(invokeEventData.contexts?.trace?.data).toMatchObject({ + 'gen_ai.operation.name': 'invoke_agent', + 'gen_ai.agent.name': 'mock-graph', + }); });