Skip to content

Commit 3f133f6

Browse files
committed
feat(core): Automatically disable truncation when span streaming is enabled in Anthropic AI integration
When span streaming is enabled, the `enableTruncation` option now defaults to `false` unless the user has explicitly set it. Closes: #20222
1 parent 2af59be commit 3f133f6

File tree

5 files changed

+140
-3
lines changed

5 files changed

+140
-3
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
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: true,
9+
transport: loggingTransport,
10+
traceLifecycle: 'stream',
11+
integrations: [
12+
Sentry.anthropicAIIntegration({
13+
enableTruncation: true,
14+
}),
15+
],
16+
});
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
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: true,
9+
transport: loggingTransport,
10+
traceLifecycle: 'stream',
11+
});
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import Anthropic from '@anthropic-ai/sdk';
2+
import * as Sentry from '@sentry/node';
3+
import express from 'express';
4+
5+
function startMockAnthropicServer() {
6+
const app = express();
7+
app.use(express.json({ limit: '10mb' }));
8+
9+
app.post('/anthropic/v1/messages', (req, res) => {
10+
res.send({
11+
id: 'msg_streaming_test',
12+
type: 'message',
13+
model: req.body.model,
14+
role: 'assistant',
15+
content: [{ type: 'text', text: 'Response' }],
16+
stop_reason: 'end_turn',
17+
stop_sequence: null,
18+
usage: { input_tokens: 10, output_tokens: 5 },
19+
});
20+
});
21+
22+
return new Promise(resolve => {
23+
const server = app.listen(0, () => {
24+
resolve(server);
25+
});
26+
});
27+
}
28+
29+
async function run() {
30+
const server = await startMockAnthropicServer();
31+
32+
await Sentry.startSpan({ op: 'function', name: 'main' }, async () => {
33+
const client = new Anthropic({
34+
apiKey: 'mock-api-key',
35+
baseURL: `http://localhost:${server.address().port}/anthropic`,
36+
});
37+
38+
// Long content that would normally be truncated
39+
const longContent = 'A'.repeat(50_000);
40+
await client.messages.create({
41+
model: 'claude-3-haiku-20240307',
42+
max_tokens: 100,
43+
messages: [{ role: 'user', content: longContent }],
44+
});
45+
});
46+
47+
// Flush is required when span streaming is enabled to ensure streamed spans are sent before the process exits
48+
await Sentry.flush(2000);
49+
50+
server.close();
51+
}
52+
53+
run();

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

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -844,4 +844,53 @@ describe('Anthropic integration', () => {
844844
});
845845
},
846846
);
847+
848+
const streamingLongContent = 'A'.repeat(50_000);
849+
850+
createEsmAndCjsTests(__dirname, 'scenario-streaming.mjs', 'instrument-streaming.mjs', (createRunner, test) => {
851+
test('automatically disables truncation when span streaming is enabled', async () => {
852+
await createRunner()
853+
.expect({
854+
span: container => {
855+
const spans = container.items;
856+
857+
const expectedMessages = JSON.stringify([{ role: 'user', content: streamingLongContent }]);
858+
const chatSpan = spans.find(
859+
s => s.attributes?.[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value === expectedMessages,
860+
);
861+
expect(chatSpan).toBeDefined();
862+
},
863+
})
864+
.start()
865+
.completed();
866+
});
867+
});
868+
869+
createEsmAndCjsTests(
870+
__dirname,
871+
'scenario-streaming.mjs',
872+
'instrument-streaming-with-truncation.mjs',
873+
(createRunner, test) => {
874+
test('respects explicit enableTruncation: true even when span streaming is enabled', async () => {
875+
await createRunner()
876+
.expect({
877+
span: container => {
878+
const spans = container.items;
879+
880+
// With explicit enableTruncation: true, content should be truncated despite streaming.
881+
// Find the chat span by matching the start of the truncated content (the 'A' repeated messages).
882+
const chatSpan = spans.find(s =>
883+
s.attributes?.[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value?.startsWith('[{"role":"user","content":"AAAA'),
884+
);
885+
expect(chatSpan).toBeDefined();
886+
expect(chatSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value.length).toBeLessThan(
887+
streamingLongContent.length,
888+
);
889+
},
890+
})
891+
.start()
892+
.completed();
893+
});
894+
},
895+
);
847896
});

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

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import { getClient } from '../../currentScopes';
12
import { captureException } from '../../exports';
23
import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../../semanticAttributes';
34
import { SPAN_STATUS_ERROR } from '../../tracing';
5+
import { hasSpanStreamingEnabled } from '../../tracing/spans/hasSpanStreamingEnabled';
46
import { startSpan, startSpanManual } from '../../tracing/trace';
57
import type { Span, SpanAttributeValue } from '../../types-hoist/span';
68
import {
@@ -206,7 +208,9 @@ function handleStreamingRequest<T extends unknown[], R>(
206208
originalResult = originalMethod.apply(context, args) as Promise<R>;
207209

208210
if (options.recordInputs && params) {
209-
addPrivateRequestAttributes(span, params, options.enableTruncation ?? true);
211+
const client = getClient();
212+
const enableTruncation = options.enableTruncation ?? !(client && hasSpanStreamingEnabled(client));
213+
addPrivateRequestAttributes(span, params, enableTruncation);
210214
}
211215

212216
return (async () => {
@@ -228,7 +232,9 @@ function handleStreamingRequest<T extends unknown[], R>(
228232
return startSpanManual(spanConfig, span => {
229233
try {
230234
if (options.recordInputs && params) {
231-
addPrivateRequestAttributes(span, params, options.enableTruncation ?? true);
235+
const client = getClient();
236+
const enableTruncation = options.enableTruncation ?? !(client && hasSpanStreamingEnabled(client));
237+
addPrivateRequestAttributes(span, params, enableTruncation);
232238
}
233239
const messageStream = target.apply(context, args);
234240
return instrumentMessageStream(messageStream, span, options.recordOutputs ?? false);
@@ -289,7 +295,9 @@ function instrumentMethod<T extends unknown[], R>(
289295
originalResult = target.apply(context, args) as Promise<R>;
290296

291297
if (options.recordInputs && params) {
292-
addPrivateRequestAttributes(span, params, options.enableTruncation ?? true);
298+
const client = getClient();
299+
const enableTruncation = options.enableTruncation ?? !(client && hasSpanStreamingEnabled(client));
300+
addPrivateRequestAttributes(span, params, enableTruncation);
293301
}
294302

295303
return originalResult.then(

0 commit comments

Comments
 (0)