Skip to content

Commit 468e038

Browse files
feat(core): Add enableTruncation option to Anthropic AI integration (#20181)
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 --------- Co-authored-by: Nicolas Hrubec <nico.hrubec@sentry.io> Co-authored-by: Nicolas Hrubec <nicolas.hrubec@outlook.com>
1 parent c9782ae commit 468e038

File tree

7 files changed

+138
-11
lines changed

7 files changed

+138
-11
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import * as Sentry from '@sentry/node';
2+
import { loggingTransport } from '@sentry-internal/node-integration-tests';
3+
4+
Sentry.init({
5+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
6+
release: '1.0',
7+
tracesSampleRate: 1.0,
8+
sendDefaultPii: false,
9+
transport: loggingTransport,
10+
integrations: [
11+
Sentry.anthropicAIIntegration({
12+
recordInputs: true,
13+
recordOutputs: true,
14+
enableTruncation: false,
15+
}),
16+
],
17+
beforeSendTransaction: event => {
18+
// Filter out mock express server transactions
19+
if (event.transaction.includes('/anthropic/v1/')) {
20+
return null;
21+
}
22+
return event;
23+
},
24+
});
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { instrumentAnthropicAiClient } from '@sentry/core';
2+
import * as Sentry from '@sentry/node';
3+
4+
class MockAnthropic {
5+
constructor(config) {
6+
this.apiKey = config.apiKey;
7+
this.messages = {
8+
create: this._messagesCreate.bind(this),
9+
};
10+
}
11+
12+
async _messagesCreate(params) {
13+
await new Promise(resolve => setTimeout(resolve, 10));
14+
return {
15+
id: 'msg-no-truncation-test',
16+
type: 'message',
17+
role: 'assistant',
18+
content: [{ type: 'text', text: 'Response' }],
19+
model: params.model,
20+
stop_reason: 'end_turn',
21+
stop_sequence: null,
22+
usage: { input_tokens: 10, output_tokens: 5 },
23+
};
24+
}
25+
}
26+
27+
async function run() {
28+
await Sentry.startSpan({ op: 'function', name: 'main' }, async () => {
29+
const mockClient = new MockAnthropic({ apiKey: 'mock-api-key' });
30+
const client = instrumentAnthropicAiClient(mockClient, { enableTruncation: false, recordInputs: true });
31+
32+
// Multiple messages with long content (would normally be truncated and popped to last message only)
33+
const longContent = 'A'.repeat(50_000);
34+
await client.messages.create({
35+
model: 'claude-3-haiku-20240307',
36+
max_tokens: 100,
37+
messages: [
38+
{ role: 'user', content: longContent },
39+
{ role: 'assistant', content: 'Some reply' },
40+
{ role: 'user', content: 'Follow-up question' },
41+
],
42+
});
43+
44+
// Long string input (messagesFromParams wraps it in an array)
45+
const longStringInput = 'B'.repeat(50_000);
46+
await client.messages.create({
47+
model: 'claude-3-haiku-20240307',
48+
max_tokens: 100,
49+
input: longStringInput,
50+
});
51+
});
52+
}
53+
54+
run();

dev-packages/node-integration-tests/suites/tracing/anthropic/test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -802,4 +802,46 @@ describe('Anthropic integration', () => {
802802
});
803803
},
804804
);
805+
806+
const longContent = 'A'.repeat(50_000);
807+
const longStringInput = 'B'.repeat(50_000);
808+
809+
const EXPECTED_TRANSACTION_NO_TRUNCATION = {
810+
transaction: 'main',
811+
spans: expect.arrayContaining([
812+
// Multiple messages should all be preserved (no popping to last message only)
813+
expect.objectContaining({
814+
data: expect.objectContaining({
815+
[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: JSON.stringify([
816+
{ role: 'user', content: longContent },
817+
{ role: 'assistant', content: 'Some reply' },
818+
{ role: 'user', content: 'Follow-up question' },
819+
]),
820+
[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 3,
821+
}),
822+
}),
823+
// Long string input should not be truncated (messagesFromParams wraps it in an array)
824+
expect.objectContaining({
825+
data: expect.objectContaining({
826+
[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: JSON.stringify([longStringInput]),
827+
[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 1,
828+
}),
829+
}),
830+
]),
831+
};
832+
833+
createEsmAndCjsTests(
834+
__dirname,
835+
'scenario-no-truncation.mjs',
836+
'instrument-no-truncation.mjs',
837+
(createRunner, test) => {
838+
test('does not truncate input messages when enableTruncation is false', async () => {
839+
await createRunner()
840+
.ignore('event')
841+
.expect({ transaction: EXPECTED_TRANSACTION_NO_TRUNCATION })
842+
.start()
843+
.completed();
844+
});
845+
},
846+
);
805847
});

packages/core/src/tracing/anthropic-ai/index.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,9 @@ function extractRequestAttributes(args: unknown[], methodPath: string, operation
7272
* Add private request attributes to spans.
7373
* This is only recorded if recordInputs is true.
7474
*/
75-
function addPrivateRequestAttributes(span: Span, params: Record<string, unknown>): void {
75+
function addPrivateRequestAttributes(span: Span, params: Record<string, unknown>, enableTruncation: boolean): void {
7676
const messages = messagesFromParams(params);
77-
setMessagesAttribute(span, messages);
77+
setMessagesAttribute(span, messages, enableTruncation);
7878

7979
if ('prompt' in params) {
8080
span.setAttributes({ [GEN_AI_PROMPT_ATTRIBUTE]: JSON.stringify(params.prompt) });
@@ -206,7 +206,7 @@ function handleStreamingRequest<T extends unknown[], R>(
206206
originalResult = originalMethod.apply(context, args) as Promise<R>;
207207

208208
if (options.recordInputs && params) {
209-
addPrivateRequestAttributes(span, params);
209+
addPrivateRequestAttributes(span, params, options.enableTruncation ?? true);
210210
}
211211

212212
return (async () => {
@@ -228,7 +228,7 @@ function handleStreamingRequest<T extends unknown[], R>(
228228
return startSpanManual(spanConfig, span => {
229229
try {
230230
if (options.recordInputs && params) {
231-
addPrivateRequestAttributes(span, params);
231+
addPrivateRequestAttributes(span, params, options.enableTruncation ?? true);
232232
}
233233
const messageStream = target.apply(context, args);
234234
return instrumentMessageStream(messageStream, span, options.recordOutputs ?? false);
@@ -289,7 +289,7 @@ function instrumentMethod<T extends unknown[], R>(
289289
originalResult = target.apply(context, args) as Promise<R>;
290290

291291
if (options.recordInputs && params) {
292-
addPrivateRequestAttributes(span, params);
292+
addPrivateRequestAttributes(span, params, options.enableTruncation ?? true);
293293
}
294294

295295
return originalResult.then(

packages/core/src/tracing/anthropic-ai/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ export interface AnthropicAiOptions {
99
* Enable or disable output recording.
1010
*/
1111
recordOutputs?: boolean;
12+
/**
13+
* Enable or disable truncation of recorded input messages.
14+
* Defaults to `true`.
15+
*/
16+
enableTruncation?: boolean;
1217
}
1318

1419
export type Message = {

packages/core/src/tracing/anthropic-ai/utils.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@ import {
77
GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE,
88
GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE,
99
} from '../ai/gen-ai-attributes';
10-
import { extractSystemInstructions, getTruncatedJsonString } from '../ai/utils';
10+
import { extractSystemInstructions, getJsonString, getTruncatedJsonString } from '../ai/utils';
1111
import type { AnthropicAiResponse } from './types';
1212

1313
/**
1414
* Set the messages and messages original length attributes.
1515
* Extracts system instructions before truncation.
1616
*/
17-
export function setMessagesAttribute(span: Span, messages: unknown): void {
17+
export function setMessagesAttribute(span: Span, messages: unknown, enableTruncation: boolean): void {
1818
if (Array.isArray(messages) && messages.length === 0) {
1919
return;
2020
}
@@ -29,7 +29,9 @@ export function setMessagesAttribute(span: Span, messages: unknown): void {
2929

3030
const filteredLength = Array.isArray(filteredMessages) ? filteredMessages.length : 1;
3131
span.setAttributes({
32-
[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: getTruncatedJsonString(filteredMessages),
32+
[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: enableTruncation
33+
? getTruncatedJsonString(filteredMessages)
34+
: getJsonString(filteredMessages),
3335
[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: filteredLength,
3436
});
3537
}

packages/core/test/lib/utils/anthropic-utils.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ describe('anthropic-ai-utils', () => {
9898

9999
it('sets length along with truncated value', () => {
100100
const content = 'A'.repeat(200_000);
101-
setMessagesAttribute(span, [{ role: 'user', content }]);
101+
setMessagesAttribute(span, [{ role: 'user', content }], true);
102102
const result = [{ role: 'user', content: 'A'.repeat(19970) }];
103103
expect(mock.attributes).toStrictEqual({
104104
'sentry.sdk_meta.gen_ai.input.messages.original_length': 1,
@@ -107,15 +107,15 @@ describe('anthropic-ai-utils', () => {
107107
});
108108

109109
it('sets length to 1 for non-array input', () => {
110-
setMessagesAttribute(span, { content: 'hello, world' });
110+
setMessagesAttribute(span, { content: 'hello, world' }, true);
111111
expect(mock.attributes).toStrictEqual({
112112
'sentry.sdk_meta.gen_ai.input.messages.original_length': 1,
113113
'gen_ai.input.messages': '{"content":"hello, world"}',
114114
});
115115
});
116116

117117
it('ignores empty array', () => {
118-
setMessagesAttribute(span, []);
118+
setMessagesAttribute(span, [], true);
119119
expect(mock.attributes).toStrictEqual({
120120
'sentry.sdk_meta.gen_ai.input.messages.original_length': 1,
121121
'gen_ai.input.messages': '{"content":"hello, world"}',

0 commit comments

Comments
 (0)