diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/test.ts b/dev-packages/node-integration-tests/suites/tracing/langchain/test.ts index e1ead1a89fe2..513537ad9a26 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/langchain/test.ts @@ -33,7 +33,6 @@ describe('LangChain integration', () => { createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => { test('creates langchain related spans with genAI recording disabled', async () => { await createRunner() - .ignore('event') .expect({ transaction: { transaction: 'main' } }) .expect({ span: container => { @@ -90,7 +89,6 @@ describe('LangChain integration', () => { test('does not create duplicate spans from double module patching', async () => { await createRunner() - .ignore('event') .expect({ transaction: { transaction: 'main' } }) .expect({ span: container => { @@ -111,7 +109,6 @@ describe('LangChain integration', () => { createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument-with-pii.mjs', (createRunner, test) => { test('creates langchain related spans with genAI recording enabled', async () => { await createRunner() - .ignore('event') .expect({ transaction: { transaction: 'main' } }) .expect({ span: container => { @@ -170,7 +167,6 @@ describe('LangChain integration', () => { createEsmAndCjsTests(__dirname, 'scenario-tools.mjs', 'instrument.mjs', (createRunner, test) => { test('creates langchain spans with tool calls', async () => { await createRunner() - .ignore('event') .expect({ transaction: { transaction: 'main' } }) .expect({ span: container => { @@ -201,7 +197,6 @@ describe('LangChain integration', () => { createEsmTests(__dirname, 'scenario-openai-before-langchain.mjs', 'instrument.mjs', (createRunner, test) => { test('demonstrates timing issue with duplicate spans', async () => { await createRunner() - .ignore('event') .expect({ transaction: { transaction: 'main' } }) .expect({ span: container => { @@ -236,7 +231,6 @@ describe('LangChain integration', () => { (createRunner, test) => { test('extracts system instructions from messages', async () => { await createRunner() - .ignore('event') .expect({ transaction: { transaction: 'main' } }) .expect({ span: container => { @@ -260,7 +254,6 @@ describe('LangChain integration', () => { createEsmAndCjsTests(__dirname, 'scenario-chain.mjs', 'instrument.mjs', (createRunner, test) => { test('uses runName for chain spans instead of unknown_chain', async () => { await createRunner() - .ignore('event') .expect({ transaction: { transaction: 'main' } }) .expect({ span: container => { @@ -306,7 +299,6 @@ describe('LangChain integration', () => { createEsmAndCjsTests(__dirname, 'scenario-embeddings.mjs', 'instrument.mjs', (createRunner, test) => { test('creates embedding spans with genAI recording disabled', async () => { await createRunner() - .ignore('event') .expect({ transaction: { transaction: 'main' } }) .expect({ span: container => { @@ -343,7 +335,6 @@ describe('LangChain integration', () => { test('does not create duplicate embedding spans from double module patching', async () => { await createRunner() - .ignore('event') .expect({ transaction: { transaction: 'main' } }) .expect({ span: container => { @@ -362,7 +353,6 @@ describe('LangChain integration', () => { createEsmAndCjsTests(__dirname, 'scenario-embeddings.mjs', 'instrument-with-pii.mjs', (createRunner, test) => { test('creates embedding spans with genAI recording enabled', async () => { await createRunner() - .ignore('event') .expect({ transaction: { transaction: 'main' } }) .expect({ span: container => { @@ -403,7 +393,6 @@ describe('LangChain integration', () => { createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument-span-streaming.mjs', (createRunner, test) => { test('creates langchain related spans with span streaming enabled', async () => { await createRunner() - .ignore('event') .expect({ span: container => { const sonnetSpan = container.items.find(span => span.name === 'chat claude-3-5-sonnet-20241022'); diff --git a/packages/server-utils/src/ai/langchain/embeddings.ts b/packages/server-utils/src/ai/langchain/embeddings.ts index 23ba6fe07f69..cf052e76a684 100644 --- a/packages/server-utils/src/ai/langchain/embeddings.ts +++ b/packages/server-utils/src/ai/langchain/embeddings.ts @@ -1,10 +1,4 @@ -import { - captureException, - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - startSpan, - stringify, -} from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startSpan, stringify } from '@sentry/core'; import type { SpanAttributeValue } from '@sentry/core'; import { GEN_AI_EMBEDDINGS_INPUT, @@ -99,12 +93,9 @@ export function instrumentEmbeddingMethod( return new Proxy(originalMethod, { apply(target, thisArg, args: unknown[]): Promise { return startSpan(_INTERNAL_getLangChainEmbeddingsSpanOptions(thisArg, args[0], options), () => { - return Reflect.apply(target, thisArg, args).then(undefined, error => { - captureException(error, { - mechanism: { handled: false, type: 'auto.ai.langchain' }, - }); - throw error; - }); + // On rejection `startSpan` marks the span failed and rethrows to the caller, so we don't + // record the error ourselves. + return Reflect.apply(target, thisArg, args); }); }, }); diff --git a/packages/server-utils/src/ai/langchain/index.ts b/packages/server-utils/src/ai/langchain/index.ts index 1f6ba8754f79..f7d0de9d4b61 100644 --- a/packages/server-utils/src/ai/langchain/index.ts +++ b/packages/server-utils/src/ai/langchain/index.ts @@ -1,6 +1,5 @@ /* eslint-disable max-lines */ import { - captureException, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, @@ -184,19 +183,14 @@ export function createLangChainCallbackHandler(options: LangChainOptions = {}): }, // LLM Error Handler - note: handleLLMError with capital LLM - handleLLMError(error: Error, runId: string) { + handleLLMError(_error: Error, runId: string) { + // The error is surfaced to the caller (invoke() rejects), so we only mark the span failed and + // do not record it. const span = spanMap.get(runId); if (span?.isRecording()) { span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); exitSpan(runId); } - - captureException(error, { - mechanism: { - handled: false, - type: `${LANGCHAIN_ORIGIN}.llm_error_handler`, - }, - }); }, // Chain Start Handler @@ -258,19 +252,14 @@ export function createLangChainCallbackHandler(options: LangChainOptions = {}): }, // Chain Error Handler - handleChainError(error: Error, runId: string) { + handleChainError(_error: Error, runId: string) { + // The error is surfaced to the caller (invoke() rejects), so we only mark the span failed and + // do not record it. const span = spanMap.get(runId); if (span?.isRecording()) { span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); exitSpan(runId); } - - captureException(error, { - mechanism: { - handled: false, - type: `${LANGCHAIN_ORIGIN}.chain_error_handler`, - }, - }); }, // Tool Start Handler @@ -336,19 +325,14 @@ export function createLangChainCallbackHandler(options: LangChainOptions = {}): }, // Tool Error Handler - handleToolError(error: Error, runId: string) { + handleToolError(_error: Error, runId: string) { + // The error is surfaced to the caller (invoke() rejects), so we only mark the span failed and + // do not record it. const span = spanMap.get(runId); if (span?.isRecording()) { span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); exitSpan(runId); } - - captureException(error, { - mechanism: { - handled: false, - type: `${LANGCHAIN_ORIGIN}.tool_error_handler`, - }, - }); }, // LangChain BaseCallbackHandler required methods diff --git a/packages/server-utils/src/integrations/langchain.ts b/packages/server-utils/src/integrations/langchain.ts index 562894882c82..b6a43579de0f 100644 --- a/packages/server-utils/src/integrations/langchain.ts +++ b/packages/server-utils/src/integrations/langchain.ts @@ -89,13 +89,13 @@ function instrumentChatModels(options: LangChainOptions): void { } } -// Embeddings don't use the callback system. Wrap the method in its own span +// Embeddings don't use the callback system. Wrap the method in its own span. +// Embedding errors reject to the caller, so we only open the span (which bindTracingChannelToSpan +// still marks failed on error) and do not capture them. function instrumentEmbeddings(options: LangChainOptions): void { for (const channelName of langchainEmbeddingsChannels) { - bindTracingChannelToSpan( - diagnosticsChannel.tracingChannel(channelName), - data => createEmbeddingsSpan(data, options), - { captureError: () => ({ mechanism: { handled: false, type: 'auto.ai.langchain' } }) }, + bindTracingChannelToSpan(diagnosticsChannel.tracingChannel(channelName), data => + createEmbeddingsSpan(data, options), ); } } diff --git a/packages/server-utils/test/ai/lib/tracing/langchain-embeddings.test.ts b/packages/server-utils/test/ai/lib/tracing/langchain-embeddings.test.ts index 361c47675ceb..bf3ce75bfbf8 100644 --- a/packages/server-utils/test/ai/lib/tracing/langchain-embeddings.test.ts +++ b/packages/server-utils/test/ai/lib/tracing/langchain-embeddings.test.ts @@ -84,7 +84,7 @@ describe('instrumentEmbeddingMethod', () => { expect(capturedSpanConfig!.attributes[GEN_AI_EMBEDDINGS_INPUT]).toBe('["doc1","doc2"]'); }); - it('captures exception on failure', async () => { + it('rethrows the error to the caller without capturing it', async () => { const error = new Error('API error'); const original = vi.fn().mockRejectedValue(error); const wrapped = instrumentEmbeddingMethod(original); @@ -92,9 +92,7 @@ describe('instrumentEmbeddingMethod', () => { const instance = { constructor: { name: 'OpenAIEmbeddings' }, model: 'error-model' }; await expect(wrapped.call(instance, 'test')).rejects.toThrow('API error'); - expect(captureException).toHaveBeenCalledWith(error, { - mechanism: { handled: false, type: 'auto.ai.langchain' }, - }); + expect(captureException).not.toHaveBeenCalled(); }); it('infers system from class name', async () => {