Skip to content

Commit 4f0c089

Browse files
authored
Merge pull request #20195 from getsentry/nh/vercel-enable-truncation
feat(core): Add `enableTruncation` option to Vercel AI integration
2 parents 4a5f90b + 43ce2ac commit 4f0c089

File tree

12 files changed

+240
-14
lines changed

12 files changed

+240
-14
lines changed
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
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+
integrations: [
11+
Sentry.vercelAIIntegration({
12+
recordInputs: true,
13+
recordOutputs: true,
14+
enableTruncation: false,
15+
}),
16+
],
17+
});
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.vercelAIIntegration({
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: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import * as Sentry from '@sentry/node';
2+
import { generateText } from 'ai';
3+
import { MockLanguageModelV1 } from 'ai/test';
4+
5+
async function run() {
6+
await Sentry.startSpan({ op: 'function', name: 'main' }, async () => {
7+
// Multiple messages with long content (would normally be truncated and popped to last message only)
8+
const longContent = 'A'.repeat(50_000);
9+
await generateText({
10+
experimental_telemetry: { isEnabled: true },
11+
model: new MockLanguageModelV1({
12+
doGenerate: async () => ({
13+
rawCall: { rawPrompt: null, rawSettings: {} },
14+
finishReason: 'stop',
15+
usage: { promptTokens: 10, completionTokens: 5 },
16+
text: 'Response',
17+
}),
18+
}),
19+
messages: [
20+
{ role: 'user', content: longContent },
21+
{ role: 'assistant', content: 'Some reply' },
22+
{ role: 'user', content: 'Follow-up question' },
23+
],
24+
});
25+
});
26+
}
27+
28+
run();
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import * as Sentry from '@sentry/node';
2+
import { generateText } from 'ai';
3+
import { MockLanguageModelV1 } from 'ai/test';
4+
5+
async function run() {
6+
await Sentry.startSpan({ op: 'function', name: 'main' }, async () => {
7+
const longContent = 'A'.repeat(50_000);
8+
await generateText({
9+
experimental_telemetry: { isEnabled: true },
10+
model: new MockLanguageModelV1({
11+
doGenerate: async () => ({
12+
rawCall: { rawPrompt: null, rawSettings: {} },
13+
finishReason: 'stop',
14+
usage: { promptTokens: 10, completionTokens: 5 },
15+
text: 'Response',
16+
}),
17+
}),
18+
messages: [
19+
{ role: 'user', content: longContent },
20+
{ role: 'assistant', content: 'Some reply' },
21+
{ role: 'user', content: 'Follow-up question' },
22+
],
23+
});
24+
});
25+
26+
// Flush is required when span streaming is enabled to ensure streamed spans are sent before the process exits
27+
await Sentry.flush(2000);
28+
}
29+
30+
run();

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

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -950,4 +950,83 @@ describe('Vercel AI integration', () => {
950950
.completed();
951951
});
952952
});
953+
954+
const longContent = 'A'.repeat(50_000);
955+
956+
createEsmAndCjsTests(
957+
__dirname,
958+
'scenario-no-truncation.mjs',
959+
'instrument-no-truncation.mjs',
960+
(createRunner, test) => {
961+
test('does not truncate input messages when enableTruncation is false', async () => {
962+
await createRunner()
963+
.expect({
964+
transaction: {
965+
transaction: 'main',
966+
spans: expect.arrayContaining([
967+
// Multiple messages should all be preserved (no popping to last message only)
968+
expect.objectContaining({
969+
data: expect.objectContaining({
970+
[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: JSON.stringify([
971+
{ role: 'user', content: longContent },
972+
{ role: 'assistant', content: 'Some reply' },
973+
{ role: 'user', content: 'Follow-up question' },
974+
]),
975+
[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 3,
976+
}),
977+
}),
978+
]),
979+
},
980+
})
981+
.start()
982+
.completed();
983+
});
984+
},
985+
);
986+
987+
const streamingLongContent = 'A'.repeat(50_000);
988+
989+
createEsmAndCjsTests(__dirname, 'scenario-streaming.mjs', 'instrument-streaming.mjs', (createRunner, test) => {
990+
test('automatically disables truncation when span streaming is enabled', async () => {
991+
await createRunner()
992+
.expect({
993+
span: container => {
994+
const spans = container.items;
995+
996+
const chatSpan = spans.find(s =>
997+
s.attributes?.[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value?.includes(streamingLongContent),
998+
);
999+
expect(chatSpan).toBeDefined();
1000+
},
1001+
})
1002+
.start()
1003+
.completed();
1004+
});
1005+
});
1006+
1007+
createEsmAndCjsTests(
1008+
__dirname,
1009+
'scenario-streaming.mjs',
1010+
'instrument-streaming-with-truncation.mjs',
1011+
(createRunner, test) => {
1012+
test('respects explicit enableTruncation: true even when span streaming is enabled', async () => {
1013+
await createRunner()
1014+
.expect({
1015+
span: container => {
1016+
const spans = container.items;
1017+
1018+
// With explicit enableTruncation: true, truncation keeps only the last message
1019+
// and drops the long content. The result should NOT contain the full 50k 'A' string.
1020+
const chatSpan = spans.find(s =>
1021+
s.attributes?.[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value?.includes('Follow-up question'),
1022+
);
1023+
expect(chatSpan).toBeDefined();
1024+
expect(chatSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).not.toContain(streamingLongContent);
1025+
},
1026+
})
1027+
.start()
1028+
.completed();
1029+
});
1030+
},
1031+
);
9531032
});

packages/cloudflare/src/integrations/tracing/vercelai.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,18 @@ import { addVercelAiProcessors, defineIntegration } from '@sentry/core';
1313

1414
const INTEGRATION_NAME = 'VercelAI';
1515

16-
const _vercelAIIntegration = (() => {
16+
interface VercelAiOptions {
17+
/**
18+
* Enable or disable truncation of recorded input messages.
19+
* Defaults to `true`.
20+
*/
21+
enableTruncation?: boolean;
22+
}
23+
24+
const _vercelAIIntegration = ((options: VercelAiOptions = {}) => {
1725
return {
1826
name: INTEGRATION_NAME,
27+
options,
1928
setup(client) {
2029
addVercelAiProcessors(client);
2130
},

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

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
/* eslint-disable max-lines */
22
import type { Client } from '../../client';
3+
import { getClient } from '../../currentScopes';
34
import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../../semanticAttributes';
5+
import { shouldEnableTruncation } from '../ai/utils';
46
import type { Event } from '../../types-hoist/event';
57
import type { Span, SpanAttributes, SpanAttributeValue, SpanJSON } from '../../types-hoist/span';
68
import { spanToJSON } from '../../utils/spanUtils';
@@ -114,7 +116,13 @@ function onVercelAiSpanStart(span: Span): void {
114116
return;
115117
}
116118

117-
processGenerateSpan(span, name, attributes);
119+
const client = getClient();
120+
const integration = client?.getIntegrationByName('VercelAI') as
121+
| { options?: { enableTruncation?: boolean } }
122+
| undefined;
123+
const enableTruncation = shouldEnableTruncation(integration?.options?.enableTruncation);
124+
125+
processGenerateSpan(span, name, attributes, enableTruncation);
118126
}
119127

120128
function vercelAiEventProcessor(event: Event): Event {
@@ -396,7 +404,7 @@ function processToolCallSpan(span: Span, attributes: SpanAttributes): void {
396404
}
397405
}
398406

399-
function processGenerateSpan(span: Span, name: string, attributes: SpanAttributes): void {
407+
function processGenerateSpan(span: Span, name: string, attributes: SpanAttributes, enableTruncation: boolean): void {
400408
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto.vercelai.otel');
401409

402410
const nameWthoutAi = name.replace('ai.', '');
@@ -408,7 +416,7 @@ function processGenerateSpan(span: Span, name: string, attributes: SpanAttribute
408416
span.setAttribute('gen_ai.function_id', functionId);
409417
}
410418

411-
requestMessagesFromPrompt(span, attributes);
419+
requestMessagesFromPrompt(span, attributes, enableTruncation);
412420

413421
if (attributes[AI_MODEL_ID_ATTRIBUTE] && !attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]) {
414422
span.setAttribute(GEN_AI_RESPONSE_MODEL_ATTRIBUTE, attributes[AI_MODEL_ID_ATTRIBUTE]);

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

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE,
1717
GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE,
1818
} from '../ai/gen-ai-attributes';
19-
import { extractSystemInstructions, getTruncatedJsonString } from '../ai/utils';
19+
import { extractSystemInstructions, getJsonString, getTruncatedJsonString } from '../ai/utils';
2020
import { toolCallSpanContextMap } from './constants';
2121
import type { TokenSummary, ToolCallSpanContext } from './types';
2222
import { AI_PROMPT_ATTRIBUTE, AI_PROMPT_MESSAGES_ATTRIBUTE } from './vercel-ai-attributes';
@@ -227,7 +227,7 @@ export function convertUserInputToMessagesFormat(userInput: string): { role: str
227227
* Generate a request.messages JSON array from the prompt field in the
228228
* invoke_agent op
229229
*/
230-
export function requestMessagesFromPrompt(span: Span, attributes: SpanAttributes): void {
230+
export function requestMessagesFromPrompt(span: Span, attributes: SpanAttributes, enableTruncation: boolean): void {
231231
if (
232232
typeof attributes[AI_PROMPT_ATTRIBUTE] === 'string' &&
233233
!attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE] &&
@@ -247,11 +247,13 @@ export function requestMessagesFromPrompt(span: Span, attributes: SpanAttributes
247247
}
248248

249249
const filteredLength = Array.isArray(filteredMessages) ? filteredMessages.length : 0;
250-
const truncatedMessages = getTruncatedJsonString(filteredMessages);
250+
const messagesJson = enableTruncation
251+
? getTruncatedJsonString(filteredMessages)
252+
: getJsonString(filteredMessages);
251253

252254
span.setAttributes({
253-
[AI_PROMPT_ATTRIBUTE]: truncatedMessages,
254-
[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: truncatedMessages,
255+
[AI_PROMPT_ATTRIBUTE]: messagesJson,
256+
[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: messagesJson,
255257
[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: filteredLength,
256258
});
257259
}
@@ -268,11 +270,13 @@ export function requestMessagesFromPrompt(span: Span, attributes: SpanAttributes
268270
}
269271

270272
const filteredLength = Array.isArray(filteredMessages) ? filteredMessages.length : 0;
271-
const truncatedMessages = getTruncatedJsonString(filteredMessages);
273+
const messagesJson = enableTruncation
274+
? getTruncatedJsonString(filteredMessages)
275+
: getJsonString(filteredMessages);
272276

273277
span.setAttributes({
274-
[AI_PROMPT_MESSAGES_ATTRIBUTE]: truncatedMessages,
275-
[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: truncatedMessages,
278+
[AI_PROMPT_MESSAGES_ATTRIBUTE]: messagesJson,
279+
[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: messagesJson,
276280
[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: filteredLength,
277281
});
278282
}

packages/deno/src/integrations/tracing/vercelai.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,18 @@ import { addVercelAiProcessors, defineIntegration } from '@sentry/core';
77

88
const INTEGRATION_NAME = 'VercelAI';
99

10-
const _vercelAIIntegration = (() => {
10+
interface VercelAiOptions {
11+
/**
12+
* Enable or disable truncation of recorded input messages.
13+
* Defaults to `true`.
14+
*/
15+
enableTruncation?: boolean;
16+
}
17+
18+
const _vercelAIIntegration = ((options: VercelAiOptions = {}) => {
1119
return {
1220
name: INTEGRATION_NAME,
21+
options,
1322
setup(client) {
1423
addVercelAiProcessors(client);
1524
},

0 commit comments

Comments
 (0)