diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/scenario-message-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/openai/scenario-message-truncation-completions.mjs similarity index 97% rename from dev-packages/node-integration-tests/suites/tracing/openai/scenario-message-truncation.mjs rename to dev-packages/node-integration-tests/suites/tracing/openai/scenario-message-truncation-completions.mjs index 5623d3763657..96684ed9ec4f 100644 --- a/dev-packages/node-integration-tests/suites/tracing/openai/scenario-message-truncation.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/openai/scenario-message-truncation-completions.mjs @@ -12,7 +12,7 @@ class MockOpenAI { await new Promise(resolve => setTimeout(resolve, 10)); return { - id: 'chatcmpl-truncation-test', + id: 'chatcmpl-completions-truncation-test', object: 'chat.completion', created: 1677652288, model: params.model, diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/scenario-message-truncation-responses.mjs b/dev-packages/node-integration-tests/suites/tracing/openai/scenario-message-truncation-responses.mjs new file mode 100644 index 000000000000..aebd3341eb33 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/openai/scenario-message-truncation-responses.mjs @@ -0,0 +1,93 @@ +import { instrumentOpenAiClient } from '@sentry/core'; +import * as Sentry from '@sentry/node'; + +class MockOpenAI { + constructor(config) { + this.apiKey = config.apiKey; + + this.responses = { + create: async params => { + // Simulate processing time + await new Promise(resolve => setTimeout(resolve, 10)); + + return { + id: 'chatcmpl-responses-truncation-test', + object: 'response', + created_at: 1677652288, + status: 'completed', + error: null, + incomplete_details: null, + instructions: null, + max_output_tokens: null, + model: params.model, + output: [ + { + type: 'message', + id: 'message-123', + status: 'completed', + role: 'assistant', + content: [ + { + type: 'output_text', + text: 'Response to truncated messages', + annotations: [], + }, + ], + }, + ], + parallel_tool_calls: true, + previous_response_id: null, + reasoning: { + effort: null, + summary: null, + }, + store: true, + temperature: params.temperature, + text: { + format: { + type: 'text', + }, + }, + tool_choice: 'auto', + tools: [], + top_p: 1.0, + truncation: 'disabled', + usage: { + input_tokens: 10, + input_tokens_details: { + cached_tokens: 0, + }, + output_tokens: 15, + output_tokens_details: { + reasoning_tokens: 0, + }, + total_tokens: 25, + }, + user: null, + metadata: {}, + }; + }, + }; + } +} + +async function run() { + await Sentry.startSpan({ op: 'function', name: 'main' }, async () => { + const mockClient = new MockOpenAI({ + apiKey: 'mock-api-key', + }); + + const client = instrumentOpenAiClient(mockClient); + + // Create 1 large message that gets truncated to fit within the 20KB limit + const largeContent = 'A'.repeat(25000) + 'B'.repeat(25000); // ~50KB gets truncated to include only As + + await client.responses.create({ + model: 'gpt-3.5-turbo', + input: largeContent, + temperature: 0.7, + }); + }); +} + +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/test.ts b/dev-packages/node-integration-tests/suites/tracing/openai/test.ts index 218e3c7ee61f..5cbb27df73bf 100644 --- a/dev-packages/node-integration-tests/suites/tracing/openai/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/openai/test.ts @@ -400,7 +400,7 @@ describe('OpenAI integration', () => { createEsmAndCjsTests( __dirname, - 'scenario-message-truncation.mjs', + 'scenario-message-truncation-completions.mjs', 'instrument-with-pii.mjs', (createRunner, test) => { test('truncates messages when they exceed byte limit - keeps only last message and crops it', async () => { @@ -433,4 +433,40 @@ describe('OpenAI integration', () => { }); }, ); + + createEsmAndCjsTests( + __dirname, + 'scenario-message-truncation-responses.mjs', + 'instrument-with-pii.mjs', + (createRunner, test) => { + test('truncates string inputs when they exceed byte limit', async () => { + await createRunner() + .ignore('event') + .expect({ + transaction: { + transaction: 'main', + spans: expect.arrayContaining([ + expect.objectContaining({ + data: expect.objectContaining({ + 'gen_ai.operation.name': 'responses', + 'sentry.op': 'gen_ai.responses', + 'sentry.origin': 'auto.ai.openai', + 'gen_ai.system': 'openai', + 'gen_ai.request.model': 'gpt-3.5-turbo', + // Messages should be present and should include truncated string input (contains only As) + 'gen_ai.request.messages': expect.stringMatching(/^A+$/), + }), + description: 'responses gpt-3.5-turbo', + op: 'gen_ai.responses', + origin: 'auto.ai.openai', + status: 'ok', + }), + ]), + }, + }) + .start() + .completed(); + }); + }, + ); }); diff --git a/packages/core/src/utils/ai/messageTruncation.ts b/packages/core/src/utils/ai/messageTruncation.ts index 64d186f927b8..945761f6220c 100644 --- a/packages/core/src/utils/ai/messageTruncation.ts +++ b/packages/core/src/utils/ai/messageTruncation.ts @@ -294,3 +294,13 @@ export function truncateMessagesByBytes(messages: unknown[], maxBytes: number): export function truncateGenAiMessages(messages: unknown[]): unknown[] { return truncateMessagesByBytes(messages, DEFAULT_GEN_AI_MESSAGES_BYTE_LIMIT); } + +/** + * Truncate GenAI string input using the default byte limit. + * + * @param input - The string to truncate + * @returns Truncated string + */ +export function truncateGenAiStringInput(input: string): string { + return truncateTextByBytes(input, DEFAULT_GEN_AI_MESSAGES_BYTE_LIMIT); +} diff --git a/packages/core/src/utils/ai/utils.ts b/packages/core/src/utils/ai/utils.ts index 00e147a16e5f..4a7a14eea554 100644 --- a/packages/core/src/utils/ai/utils.ts +++ b/packages/core/src/utils/ai/utils.ts @@ -7,7 +7,7 @@ import { GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE, GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE, } from './gen-ai-attributes'; -import { truncateGenAiMessages } from './messageTruncation'; +import { truncateGenAiMessages, truncateGenAiStringInput } from './messageTruncation'; /** * Maps AI method paths to Sentry operation name */ @@ -95,7 +95,7 @@ export function setTokenUsageAttributes( export function getTruncatedJsonString(value: T | T[]): string { if (typeof value === 'string') { // Some values are already JSON strings, so we don't need to duplicate the JSON parsing - return value; + return truncateGenAiStringInput(value); } if (Array.isArray(value)) { // truncateGenAiMessages returns an array of strings, so we need to stringify it