From 691d17b5545a8b176fcc98c396eb6d50a38282a2 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Fri, 10 Apr 2026 13:45:39 +0900 Subject: [PATCH 1/4] feat(core): Add `enableTruncation` option to LangChain integration This PR adds an `enableTruncation` option to the LangChain integration that allows users to disable input message truncation. It defaults to `true` to preserve existing behavior. Also fixes missing truncation for LLM string prompts in extractLLMRequestAttributes and refactors to use the shared getTruncatedJsonString/getJsonString utilities. Closes: #20138 --- .../langchain/instrument-no-truncation.mjs | 22 ++++++++ .../langchain/scenario-no-truncation.mjs | 52 +++++++++++++++++++ .../suites/tracing/langchain/test.ts | 28 ++++++++++ packages/core/.oxlintrc.json | 6 +++ packages/core/src/tracing/langchain/index.ts | 3 ++ packages/core/src/tracing/langchain/types.ts | 6 +++ packages/core/src/tracing/langchain/utils.ts | 18 +++++-- .../test/lib/tracing/langchain-utils.test.ts | 2 +- 8 files changed, 131 insertions(+), 6 deletions(-) create mode 100644 dev-packages/node-integration-tests/suites/tracing/langchain/instrument-no-truncation.mjs create mode 100644 dev-packages/node-integration-tests/suites/tracing/langchain/scenario-no-truncation.mjs diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-no-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-no-truncation.mjs new file mode 100644 index 000000000000..7795259f074a --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-no-truncation.mjs @@ -0,0 +1,22 @@ +import * as Sentry from '@sentry/node'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + sendDefaultPii: true, + transport: loggingTransport, + integrations: [ + Sentry.langchainIntegration({ + enableTruncation: false, + }), + ], + beforeSendTransaction: event => { + // Filter out mock express server transactions + if (event.transaction.includes('/v1/messages') || event.transaction.includes('/v1/embeddings')) { + return null; + } + return event; + }, +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/scenario-no-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/langchain/scenario-no-truncation.mjs new file mode 100644 index 000000000000..dace9942c674 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/langchain/scenario-no-truncation.mjs @@ -0,0 +1,52 @@ +import { ChatAnthropic } from '@langchain/anthropic'; +import * as Sentry from '@sentry/node'; +import express from 'express'; + +function startMockAnthropicServer() { + const app = express(); + app.use(express.json({ limit: '10mb' })); + + app.post('/v1/messages', (req, res) => { + res.json({ + id: 'msg_no_truncation_test', + type: 'message', + role: 'assistant', + content: [{ type: 'text', text: 'Response' }], + model: req.body.model, + stop_reason: 'end_turn', + stop_sequence: null, + usage: { input_tokens: 10, output_tokens: 5 }, + }); + }); + + 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 () => { + const model = new ChatAnthropic({ + model: 'claude-3-5-sonnet-20241022', + apiKey: 'mock-api-key', + clientOptions: { + baseURL: baseUrl, + }, + }); + + // Long content that would normally be truncated + const longContent = 'A'.repeat(50_000); + await model.invoke([{ role: 'user', content: longContent }]); + }); + + await Sentry.flush(2000); + + server.close(); +} + +run(); 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 39127c7e3055..133230a027d1 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/langchain/test.ts @@ -549,4 +549,32 @@ describe('LangChain integration', () => { .completed(); }); }); + + const longContent = 'A'.repeat(50_000); + + const EXPECTED_TRANSACTION_NO_TRUNCATION = { + transaction: 'main', + spans: expect.arrayContaining([ + expect.objectContaining({ + data: expect.objectContaining({ + [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: expect.stringContaining(longContent), + }), + }), + ]), + }; + + createEsmAndCjsTests( + __dirname, + 'scenario-no-truncation.mjs', + 'instrument-no-truncation.mjs', + (createRunner, test) => { + test('does not truncate input messages when enableTruncation is false', async () => { + await createRunner() + .ignore('event') + .expect({ transaction: EXPECTED_TRANSACTION_NO_TRUNCATION }) + .start() + .completed(); + }); + }, + ); }); diff --git a/packages/core/.oxlintrc.json b/packages/core/.oxlintrc.json index 05b86efa950b..df7465d97fa2 100644 --- a/packages/core/.oxlintrc.json +++ b/packages/core/.oxlintrc.json @@ -16,6 +16,12 @@ "rules": { "sdk/no-unsafe-random-apis": "off" } + }, + { + "files": ["src/tracing/langchain/utils.ts"], + "rules": { + "max-lines": "off" + } } ], "ignorePatterns": ["rollup.npm.config.mjs"] diff --git a/packages/core/src/tracing/langchain/index.ts b/packages/core/src/tracing/langchain/index.ts index 16257acebbd7..7acc35400c99 100644 --- a/packages/core/src/tracing/langchain/index.ts +++ b/packages/core/src/tracing/langchain/index.ts @@ -34,6 +34,7 @@ import { */ export function createLangChainCallbackHandler(options: LangChainOptions = {}): LangChainCallbackHandler { const { recordInputs, recordOutputs } = resolveAIRecordingOptions(options); + const enableTruncation = options.enableTruncation ?? true; // Internal state - single instance tracks all spans const spanMap = new Map(); @@ -89,6 +90,7 @@ export function createLangChainCallbackHandler(options: LangChainOptions = {}): llm as LangChainSerialized, prompts, recordInputs, + enableTruncation, invocationParams, metadata, ); @@ -127,6 +129,7 @@ export function createLangChainCallbackHandler(options: LangChainOptions = {}): llm as LangChainSerialized, messages as LangChainMessage[][], recordInputs, + enableTruncation, invocationParams, metadata, ); diff --git a/packages/core/src/tracing/langchain/types.ts b/packages/core/src/tracing/langchain/types.ts index 7379de764817..1c066269aba5 100644 --- a/packages/core/src/tracing/langchain/types.ts +++ b/packages/core/src/tracing/langchain/types.ts @@ -13,6 +13,12 @@ export interface LangChainOptions { * @default false (respects sendDefaultPii option) */ recordOutputs?: boolean; + + /** + * Enable or disable truncation of recorded input messages. + * Defaults to `true`. + */ + enableTruncation?: boolean; } /** diff --git a/packages/core/src/tracing/langchain/utils.ts b/packages/core/src/tracing/langchain/utils.ts index 924739485948..1227889f210d 100644 --- a/packages/core/src/tracing/langchain/utils.ts +++ b/packages/core/src/tracing/langchain/utils.ts @@ -26,8 +26,7 @@ import { GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE, } from '../ai/gen-ai-attributes'; import { isContentMedia, stripInlineMediaFromSingleMessage } from '../ai/mediaStripping'; -import { truncateGenAiMessages } from '../ai/messageTruncation'; -import { extractSystemInstructions } from '../ai/utils'; +import { extractSystemInstructions, getJsonString, getTruncatedJsonString } from '../ai/utils'; import { LANGCHAIN_ORIGIN, ROLE_MAP } from './constants'; import type { LangChainLLMResult, LangChainMessage, LangChainSerialized } from './types'; @@ -284,6 +283,7 @@ export function extractLLMRequestAttributes( llm: LangChainSerialized, prompts: string[], recordInputs: boolean, + enableTruncation: boolean, invocationParams?: Record, langSmithMetadata?: Record, ): Record { @@ -295,7 +295,11 @@ export function extractLLMRequestAttributes( if (recordInputs && Array.isArray(prompts) && prompts.length > 0) { setIfDefined(attrs, GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE, prompts.length); const messages = prompts.map(p => ({ role: 'user', content: p })); - setIfDefined(attrs, GEN_AI_INPUT_MESSAGES_ATTRIBUTE, asString(messages)); + setIfDefined( + attrs, + GEN_AI_INPUT_MESSAGES_ATTRIBUTE, + enableTruncation ? getTruncatedJsonString(messages) : getJsonString(messages), + ); } return attrs; @@ -314,6 +318,7 @@ export function extractChatModelRequestAttributes( llm: LangChainSerialized, langChainMessages: LangChainMessage[][], recordInputs: boolean, + enableTruncation: boolean, invocationParams?: Record, langSmithMetadata?: Record, ): Record { @@ -334,8 +339,11 @@ export function extractChatModelRequestAttributes( const filteredLength = Array.isArray(filteredMessages) ? filteredMessages.length : 0; setIfDefined(attrs, GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE, filteredLength); - const truncated = truncateGenAiMessages(filteredMessages as unknown[]); - setIfDefined(attrs, GEN_AI_INPUT_MESSAGES_ATTRIBUTE, asString(truncated)); + setIfDefined( + attrs, + GEN_AI_INPUT_MESSAGES_ATTRIBUTE, + enableTruncation ? getTruncatedJsonString(filteredMessages) : getJsonString(filteredMessages), + ); } return attrs; diff --git a/packages/core/test/lib/tracing/langchain-utils.test.ts b/packages/core/test/lib/tracing/langchain-utils.test.ts index 98724c8902d4..18807631c404 100644 --- a/packages/core/test/lib/tracing/langchain-utils.test.ts +++ b/packages/core/test/lib/tracing/langchain-utils.test.ts @@ -237,7 +237,7 @@ describe('extractChatModelRequestAttributes with multimodal content', () => { ], ]; - const attrs = extractChatModelRequestAttributes(serialized, messages, true); + const attrs = extractChatModelRequestAttributes(serialized, messages, true, true); const inputMessages = attrs[GEN_AI_INPUT_MESSAGES_ATTRIBUTE] as string | undefined; expect(inputMessages).toBeDefined(); From d9f8735d1a7d22e9265b0c88a3c9e70c6ed57470 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Fri, 10 Apr 2026 14:21:58 +0900 Subject: [PATCH 2/4] Fix langChainIntegration casing in instrument-no-truncation.mjs --- .../suites/tracing/langchain/instrument-no-truncation.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-no-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-no-truncation.mjs index 7795259f074a..dd4ee25fea16 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-no-truncation.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-no-truncation.mjs @@ -8,7 +8,7 @@ Sentry.init({ sendDefaultPii: true, transport: loggingTransport, integrations: [ - Sentry.langchainIntegration({ + Sentry.langChainIntegration({ enableTruncation: false, }), ], From dbc45f09ff676a86087822b8d7a8325d81e82bd1 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Fri, 10 Apr 2026 14:37:46 +0900 Subject: [PATCH 3/4] Bump size limit --- .size-limit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.size-limit.js b/.size-limit.js index 1e6e8d951464..4100751f2c40 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -276,7 +276,7 @@ module.exports = [ path: createCDNPath('bundle.tracing.replay.logs.metrics.min.js'), gzip: false, brotli: false, - limit: '250 KB', + limit: '251 KB', }, { name: 'CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed', From 3973f21a81a78995452beba11c3ffc66c271159e Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Fri, 10 Apr 2026 12:02:23 +0200 Subject: [PATCH 4/4] update --- .size-limit.js | 2 +- .../suites/tracing/langchain/instrument-no-truncation.mjs | 2 ++ .../suites/tracing/langchain/scenario-no-truncation.mjs | 6 +++++- .../suites/tracing/langchain/test.ts | 7 ++++++- 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/.size-limit.js b/.size-limit.js index 4100751f2c40..1e6e8d951464 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -276,7 +276,7 @@ module.exports = [ path: createCDNPath('bundle.tracing.replay.logs.metrics.min.js'), gzip: false, brotli: false, - limit: '251 KB', + limit: '250 KB', }, { name: 'CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed', diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-no-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-no-truncation.mjs index dd4ee25fea16..027299eeacad 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-no-truncation.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-no-truncation.mjs @@ -10,6 +10,8 @@ Sentry.init({ integrations: [ Sentry.langChainIntegration({ enableTruncation: false, + recordInputs: true, + recordOutputs: true, }), ], beforeSendTransaction: event => { diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/scenario-no-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/langchain/scenario-no-truncation.mjs index dace9942c674..bb8f5fc35325 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/scenario-no-truncation.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/langchain/scenario-no-truncation.mjs @@ -41,7 +41,11 @@ async function run() { // Long content that would normally be truncated const longContent = 'A'.repeat(50_000); - await model.invoke([{ role: 'user', content: longContent }]); + await model.invoke([ + { role: 'user', content: longContent }, + { role: 'assistant', content: 'Some reply' }, + { role: 'user', content: 'Follow-up question' }, + ]); }); await Sentry.flush(2000); 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 133230a027d1..434001c92965 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/langchain/test.ts @@ -557,7 +557,12 @@ describe('LangChain integration', () => { spans: expect.arrayContaining([ expect.objectContaining({ data: expect.objectContaining({ - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: expect.stringContaining(longContent), + [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: JSON.stringify([ + { role: 'user', content: longContent }, + { role: 'assistant', content: 'Some reply' }, + { role: 'user', content: 'Follow-up question' }, + ]), + [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 3, }), }), ]),