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..54792b827a43 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/ai-providers/langgraph/mocks.js @@ -0,0 +1,29 @@ +// 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..1feabd48c8d2 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/ai-providers/langgraph/test.ts @@ -0,0 +1,45 @@ +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 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 createReq = await createTransactionPromise; + const invokeReq = await invokeTransactionPromise; + + const createEventData = envelopeRequestParser(createReq); + const invokeEventData = envelopeRequestParser(invokeReq); + + // 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', + }); +}); 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 {