Skip to content

Commit 86dc30a

Browse files
authored
feat(core): Add enableTruncation option to OpenAI integration (#20167)
This PR adds an `enableTruncation` option to the OpenAI integration that allows users to disable input message truncation. It defaults to `true` to preserve existing behavior. Closes: #20135
1 parent 7a59841 commit 86dc30a

File tree

6 files changed

+170
-5
lines changed

6 files changed

+170
-5
lines changed
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
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.openAIIntegration({
12+
recordInputs: true,
13+
recordOutputs: true,
14+
enableTruncation: false,
15+
}),
16+
],
17+
beforeSendTransaction: event => {
18+
if (event.transaction.includes('/openai/')) {
19+
return null;
20+
}
21+
return event;
22+
},
23+
});
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import * as Sentry from '@sentry/node';
2+
import express from 'express';
3+
import OpenAI from 'openai';
4+
5+
function startMockServer() {
6+
const app = express();
7+
app.use(express.json({ limit: '10mb' }));
8+
9+
app.post('/openai/chat/completions', (req, res) => {
10+
res.send({
11+
id: 'chatcmpl-mock123',
12+
object: 'chat.completion',
13+
created: 1677652288,
14+
model: req.body.model,
15+
choices: [
16+
{
17+
index: 0,
18+
message: { role: 'assistant', content: 'Hello!' },
19+
finish_reason: 'stop',
20+
},
21+
],
22+
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
23+
});
24+
});
25+
26+
app.post('/openai/responses', (req, res) => {
27+
res.send({
28+
id: 'resp_mock456',
29+
object: 'response',
30+
created_at: 1677652290,
31+
model: req.body.model,
32+
output: [
33+
{
34+
type: 'message',
35+
id: 'msg_mock_output_1',
36+
status: 'completed',
37+
role: 'assistant',
38+
content: [{ type: 'output_text', text: 'Response text', annotations: [] }],
39+
},
40+
],
41+
output_text: 'Response text',
42+
status: 'completed',
43+
usage: { input_tokens: 5, output_tokens: 3, total_tokens: 8 },
44+
});
45+
});
46+
47+
return new Promise(resolve => {
48+
const server = app.listen(0, () => {
49+
resolve(server);
50+
});
51+
});
52+
}
53+
54+
async function run() {
55+
const server = await startMockServer();
56+
57+
await Sentry.startSpan({ op: 'function', name: 'main' }, async () => {
58+
const client = new OpenAI({
59+
baseURL: `http://localhost:${server.address().port}/openai`,
60+
apiKey: 'mock-api-key',
61+
});
62+
63+
// Chat completion with long content (would normally be truncated)
64+
const longContent = 'A'.repeat(50_000);
65+
await client.chat.completions.create({
66+
model: 'gpt-4',
67+
messages: [{ role: 'user', content: longContent }],
68+
});
69+
70+
// Responses API with long string input (would normally be truncated)
71+
const longStringInput = 'B'.repeat(50_000);
72+
await client.responses.create({
73+
model: 'gpt-4',
74+
input: longStringInput,
75+
});
76+
});
77+
78+
server.close();
79+
}
80+
81+
run();

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

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,43 @@ describe('OpenAI integration', () => {
345345
});
346346
});
347347

348+
const longContent = 'A'.repeat(50_000);
349+
350+
const EXPECTED_TRANSACTION_NO_TRUNCATION = {
351+
transaction: 'main',
352+
spans: expect.arrayContaining([
353+
// Chat completion with long content should not be truncated
354+
expect.objectContaining({
355+
data: expect.objectContaining({
356+
[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: JSON.stringify([{ role: 'user', content: longContent }]),
357+
[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 1,
358+
}),
359+
}),
360+
// Responses API long string input should not be truncated or wrapped in quotes
361+
expect.objectContaining({
362+
data: expect.objectContaining({
363+
[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: 'B'.repeat(50_000),
364+
[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 1,
365+
}),
366+
}),
367+
]),
368+
};
369+
370+
createEsmAndCjsTests(
371+
__dirname,
372+
'scenario-no-truncation.mjs',
373+
'instrument-no-truncation.mjs',
374+
(createRunner, test) => {
375+
test('does not truncate input messages when enableTruncation is false', async () => {
376+
await createRunner()
377+
.ignore('event')
378+
.expect({ transaction: EXPECTED_TRANSACTION_NO_TRUNCATION })
379+
.start()
380+
.completed();
381+
});
382+
},
383+
);
384+
348385
const EXPECTED_TRANSACTION_DEFAULT_PII_FALSE_EMBEDDINGS = {
349386
transaction: 'main',
350387
spans: expect.arrayContaining([

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,17 @@ export function endStreamSpan(span: Span, state: StreamResponseState, recordOutp
169169
span.end();
170170
}
171171

172+
/**
173+
* Serialize a value to a JSON string without truncation.
174+
* Strings are returned as-is, arrays and objects are JSON-stringified.
175+
*/
176+
export function getJsonString<T>(value: T | T[]): string {
177+
if (typeof value === 'string') {
178+
return value;
179+
}
180+
return JSON.stringify(value);
181+
}
182+
172183
/**
173184
* Get the truncated JSON string for a string or array of strings.
174185
*

packages/core/src/tracing/openai/index.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import type { InstrumentedMethodEntry } from '../ai/utils';
1919
import {
2020
buildMethodPath,
2121
extractSystemInstructions,
22+
getJsonString,
2223
getTruncatedJsonString,
2324
resolveAIRecordingOptions,
2425
wrapPromiseWithMethods,
@@ -78,7 +79,12 @@ function extractRequestAttributes(args: unknown[], operationName: string): Recor
7879
}
7980

8081
// Extract and record AI request inputs, if present. This is intentionally separate from response attributes.
81-
function addRequestAttributes(span: Span, params: Record<string, unknown>, operationName: string): void {
82+
function addRequestAttributes(
83+
span: Span,
84+
params: Record<string, unknown>,
85+
operationName: string,
86+
enableTruncation: boolean,
87+
): void {
8288
// Store embeddings input on a separate attribute and do not truncate it
8389
if (operationName === 'embeddings' && 'input' in params) {
8490
const input = params.input;
@@ -119,8 +125,10 @@ function addRequestAttributes(span: Span, params: Record<string, unknown>, opera
119125
span.setAttribute(GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE, systemInstructions);
120126
}
121127

122-
const truncatedInput = getTruncatedJsonString(filteredMessages);
123-
span.setAttribute(GEN_AI_INPUT_MESSAGES_ATTRIBUTE, truncatedInput);
128+
span.setAttribute(
129+
GEN_AI_INPUT_MESSAGES_ATTRIBUTE,
130+
enableTruncation ? getTruncatedJsonString(filteredMessages) : getJsonString(filteredMessages),
131+
);
124132

125133
if (Array.isArray(filteredMessages)) {
126134
span.setAttribute(GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE, filteredMessages.length);
@@ -162,7 +170,7 @@ function instrumentMethod<T extends unknown[], R>(
162170
originalResult = originalMethod.apply(context, args);
163171

164172
if (options.recordInputs && params) {
165-
addRequestAttributes(span, params, operationName);
173+
addRequestAttributes(span, params, operationName, options.enableTruncation ?? true);
166174
}
167175

168176
// Return async processing
@@ -200,7 +208,7 @@ function instrumentMethod<T extends unknown[], R>(
200208
originalResult = originalMethod.apply(context, args);
201209

202210
if (options.recordInputs && params) {
203-
addRequestAttributes(span, params, operationName);
211+
addRequestAttributes(span, params, operationName, options.enableTruncation ?? true);
204212
}
205213

206214
return originalResult.then(

packages/core/src/tracing/openai/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ export interface OpenAiOptions {
2222
* Enable or disable output recording.
2323
*/
2424
recordOutputs?: boolean;
25+
/**
26+
* Enable or disable truncation of recorded input messages.
27+
* Defaults to `true`.
28+
*/
29+
enableTruncation?: boolean;
2530
}
2631

2732
export interface OpenAiClient {

0 commit comments

Comments
 (0)