From a008245b951e802ff168b44d32141e38003de5c2 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Fri, 10 Apr 2026 11:50:38 +0900 Subject: [PATCH 1/3] feat(core): Add `enableTruncation` option to Anthropic AI integration This PR adds an `enableTruncation` option to the Anthropic AI integration that allows users to disable input message truncation. It defaults to `true` to preserve existing behavior. Closes: #20136 --- .../anthropic/instrument-no-truncation.mjs | 24 +++++++++ .../anthropic/scenario-no-truncation.mjs | 50 +++++++++++++++++++ .../suites/tracing/anthropic/test.ts | 38 ++++++++++++++ .../core/src/tracing/anthropic-ai/index.ts | 10 ++-- .../core/src/tracing/anthropic-ai/types.ts | 5 ++ .../core/src/tracing/anthropic-ai/utils.ts | 8 +-- .../test/lib/utils/anthropic-utils.test.ts | 6 +-- 7 files changed, 130 insertions(+), 11 deletions(-) create mode 100644 dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-no-truncation.mjs create mode 100644 dev-packages/node-integration-tests/suites/tracing/anthropic/scenario-no-truncation.mjs diff --git a/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-no-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-no-truncation.mjs new file mode 100644 index 000000000000..ce15aad4e8e1 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-no-truncation.mjs @@ -0,0 +1,24 @@ +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: false, + transport: loggingTransport, + integrations: [ + Sentry.anthropicAIIntegration({ + recordInputs: true, + recordOutputs: true, + enableTruncation: false, + }), + ], + beforeSendTransaction: event => { + // Filter out mock express server transactions + if (event.transaction.includes('/anthropic/v1/')) { + return null; + } + return event; + }, +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/anthropic/scenario-no-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/anthropic/scenario-no-truncation.mjs new file mode 100644 index 000000000000..3b59af2660a7 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/anthropic/scenario-no-truncation.mjs @@ -0,0 +1,50 @@ +import { instrumentAnthropicAiClient } from '@sentry/core'; +import * as Sentry from '@sentry/node'; + +class MockAnthropic { + constructor(config) { + this.apiKey = config.apiKey; + this.messages = { + create: this._messagesCreate.bind(this), + }; + } + + async _messagesCreate(params) { + await new Promise(resolve => setTimeout(resolve, 10)); + return { + id: 'msg-no-truncation-test', + type: 'message', + role: 'assistant', + content: [{ type: 'text', text: 'Response' }], + model: params.model, + stop_reason: 'end_turn', + stop_sequence: null, + usage: { input_tokens: 10, output_tokens: 5 }, + }; + } +} + +async function run() { + await Sentry.startSpan({ op: 'function', name: 'main' }, async () => { + const mockClient = new MockAnthropic({ apiKey: 'mock-api-key' }); + const client = instrumentAnthropicAiClient(mockClient, { enableTruncation: false, recordInputs: true }); + + // Long array messages (would normally be truncated) + const longContent = 'A'.repeat(50_000); + await client.messages.create({ + model: 'claude-3-haiku-20240307', + max_tokens: 100, + messages: [{ role: 'user', content: longContent }], + }); + + // Long string input (should not be wrapped in quotes) + const longStringInput = 'B'.repeat(50_000); + await client.messages.create({ + model: 'claude-3-haiku-20240307', + max_tokens: 100, + input: longStringInput, + }); + }); +} + +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/anthropic/test.ts b/dev-packages/node-integration-tests/suites/tracing/anthropic/test.ts index 3241adfc161d..f9b13cb1725a 100644 --- a/dev-packages/node-integration-tests/suites/tracing/anthropic/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/anthropic/test.ts @@ -802,4 +802,42 @@ describe('Anthropic integration', () => { }); }, ); + + const longContent = 'A'.repeat(50_000); + const longStringInput = 'B'.repeat(50_000); + + const EXPECTED_TRANSACTION_NO_TRUNCATION = { + transaction: 'main', + spans: expect.arrayContaining([ + // Long array messages should not be truncated + expect.objectContaining({ + data: expect.objectContaining({ + [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: JSON.stringify([{ role: 'user', content: longContent }]), + [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 1, + }), + }), + // Long string input should not be truncated or wrapped in quotes + expect.objectContaining({ + data: expect.objectContaining({ + [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: longStringInput, + [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 1, + }), + }), + ]), + }; + + 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/src/tracing/anthropic-ai/index.ts b/packages/core/src/tracing/anthropic-ai/index.ts index 14cc44d6d6be..323c7feb2bb2 100644 --- a/packages/core/src/tracing/anthropic-ai/index.ts +++ b/packages/core/src/tracing/anthropic-ai/index.ts @@ -72,9 +72,9 @@ function extractRequestAttributes(args: unknown[], methodPath: string, operation * Add private request attributes to spans. * This is only recorded if recordInputs is true. */ -function addPrivateRequestAttributes(span: Span, params: Record): void { +function addPrivateRequestAttributes(span: Span, params: Record, enableTruncation: boolean): void { const messages = messagesFromParams(params); - setMessagesAttribute(span, messages); + setMessagesAttribute(span, messages, enableTruncation); if ('prompt' in params) { span.setAttributes({ [GEN_AI_PROMPT_ATTRIBUTE]: JSON.stringify(params.prompt) }); @@ -206,7 +206,7 @@ function handleStreamingRequest( originalResult = originalMethod.apply(context, args) as Promise; if (options.recordInputs && params) { - addPrivateRequestAttributes(span, params); + addPrivateRequestAttributes(span, params, options.enableTruncation ?? true); } return (async () => { @@ -228,7 +228,7 @@ function handleStreamingRequest( return startSpanManual(spanConfig, span => { try { if (options.recordInputs && params) { - addPrivateRequestAttributes(span, params); + addPrivateRequestAttributes(span, params, options.enableTruncation ?? true); } const messageStream = target.apply(context, args); return instrumentMessageStream(messageStream, span, options.recordOutputs ?? false); @@ -289,7 +289,7 @@ function instrumentMethod( originalResult = target.apply(context, args) as Promise; if (options.recordInputs && params) { - addPrivateRequestAttributes(span, params); + addPrivateRequestAttributes(span, params, options.enableTruncation ?? true); } return originalResult.then( diff --git a/packages/core/src/tracing/anthropic-ai/types.ts b/packages/core/src/tracing/anthropic-ai/types.ts index ba281ef82a0d..dd61ff63e264 100644 --- a/packages/core/src/tracing/anthropic-ai/types.ts +++ b/packages/core/src/tracing/anthropic-ai/types.ts @@ -9,6 +9,11 @@ export interface AnthropicAiOptions { * Enable or disable output recording. */ recordOutputs?: boolean; + /** + * Enable or disable truncation of recorded input messages. + * Defaults to `true`. + */ + enableTruncation?: boolean; } export type Message = { diff --git a/packages/core/src/tracing/anthropic-ai/utils.ts b/packages/core/src/tracing/anthropic-ai/utils.ts index b70d9adcfa67..20e2ff15bafb 100644 --- a/packages/core/src/tracing/anthropic-ai/utils.ts +++ b/packages/core/src/tracing/anthropic-ai/utils.ts @@ -7,14 +7,14 @@ import { GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE, GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE, } from '../ai/gen-ai-attributes'; -import { extractSystemInstructions, getTruncatedJsonString } from '../ai/utils'; +import { extractSystemInstructions, getJsonString, getTruncatedJsonString } from '../ai/utils'; import type { AnthropicAiResponse } from './types'; /** * Set the messages and messages original length attributes. * Extracts system instructions before truncation. */ -export function setMessagesAttribute(span: Span, messages: unknown): void { +export function setMessagesAttribute(span: Span, messages: unknown, enableTruncation: boolean): void { if (Array.isArray(messages) && messages.length === 0) { return; } @@ -29,7 +29,9 @@ export function setMessagesAttribute(span: Span, messages: unknown): void { const filteredLength = Array.isArray(filteredMessages) ? filteredMessages.length : 1; span.setAttributes({ - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: getTruncatedJsonString(filteredMessages), + [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: enableTruncation + ? getTruncatedJsonString(filteredMessages) + : getJsonString(filteredMessages), [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: filteredLength, }); } diff --git a/packages/core/test/lib/utils/anthropic-utils.test.ts b/packages/core/test/lib/utils/anthropic-utils.test.ts index 012ff9e6ccb6..8712d1f96407 100644 --- a/packages/core/test/lib/utils/anthropic-utils.test.ts +++ b/packages/core/test/lib/utils/anthropic-utils.test.ts @@ -98,7 +98,7 @@ describe('anthropic-ai-utils', () => { it('sets length along with truncated value', () => { const content = 'A'.repeat(200_000); - setMessagesAttribute(span, [{ role: 'user', content }]); + setMessagesAttribute(span, [{ role: 'user', content }], true); const result = [{ role: 'user', content: 'A'.repeat(19970) }]; expect(mock.attributes).toStrictEqual({ 'sentry.sdk_meta.gen_ai.input.messages.original_length': 1, @@ -107,7 +107,7 @@ describe('anthropic-ai-utils', () => { }); it('sets length to 1 for non-array input', () => { - setMessagesAttribute(span, { content: 'hello, world' }); + setMessagesAttribute(span, { content: 'hello, world' }, true); expect(mock.attributes).toStrictEqual({ 'sentry.sdk_meta.gen_ai.input.messages.original_length': 1, 'gen_ai.input.messages': '{"content":"hello, world"}', @@ -115,7 +115,7 @@ describe('anthropic-ai-utils', () => { }); it('ignores empty array', () => { - setMessagesAttribute(span, []); + setMessagesAttribute(span, [], true); expect(mock.attributes).toStrictEqual({ 'sentry.sdk_meta.gen_ai.input.messages.original_length': 1, 'gen_ai.input.messages': '{"content":"hello, world"}', From 2ca93d03086f75c9a495b2a84b31f8654fb4fd6f Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Fri, 10 Apr 2026 12:23:18 +0900 Subject: [PATCH 2/3] Fix incorrect test assertion for string input in Anthropic no-truncation scenario messagesFromParams wraps string input values into an array, so the expected attribute value is JSON.stringify([longStringInput]), not the raw string. --- .../suites/tracing/anthropic/scenario-no-truncation.mjs | 2 +- .../node-integration-tests/suites/tracing/anthropic/test.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dev-packages/node-integration-tests/suites/tracing/anthropic/scenario-no-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/anthropic/scenario-no-truncation.mjs index 3b59af2660a7..48ed5813a315 100644 --- a/dev-packages/node-integration-tests/suites/tracing/anthropic/scenario-no-truncation.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/anthropic/scenario-no-truncation.mjs @@ -37,7 +37,7 @@ async function run() { messages: [{ role: 'user', content: longContent }], }); - // Long string input (should not be wrapped in quotes) + // Long string input (messagesFromParams wraps it in an array) const longStringInput = 'B'.repeat(50_000); await client.messages.create({ model: 'claude-3-haiku-20240307', diff --git a/dev-packages/node-integration-tests/suites/tracing/anthropic/test.ts b/dev-packages/node-integration-tests/suites/tracing/anthropic/test.ts index f9b13cb1725a..036ae368e6da 100644 --- a/dev-packages/node-integration-tests/suites/tracing/anthropic/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/anthropic/test.ts @@ -816,10 +816,10 @@ describe('Anthropic integration', () => { [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 1, }), }), - // Long string input should not be truncated or wrapped in quotes + // Long string input should not be truncated (messagesFromParams wraps it in an array) expect.objectContaining({ data: expect.objectContaining({ - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: longStringInput, + [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: JSON.stringify([longStringInput]), [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 1, }), }), From 85f9cd2932995b5430e69d6c4a461c38da6e3a03 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Fri, 10 Apr 2026 14:37:46 +0900 Subject: [PATCH 3/3] 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',