diff --git a/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-no-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-no-truncation.mjs deleted file mode 100644 index 1fa73dda80a2..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-no-truncation.mjs +++ /dev/null @@ -1,24 +0,0 @@ -import * as Sentry from '@sentry/node'; -import { loggingTransport } from '@sentry-internal/node-integration-tests'; - -Sentry.init({ - dsn: 'https://public@dsn.ingest.sentry.io/1337', - release: '1.0', - tracesSampleRate: 1.0, - dataCollection: { genAI: { inputs: false, outputs: false } }, - transport: loggingTransport, - integrations: [ - Sentry.anthropicAIIntegration({ - recordInputs: true, - recordOutputs: true, - enableTruncation: false, - }), - ], - beforeSendTransaction: event => { - // Filter out mock express server transactions - if (event.transaction.includes('/anthropic/v1/')) { - return null; - } - return event; - }, -}); diff --git a/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-streaming-with-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-streaming-with-truncation.mjs deleted file mode 100644 index 048f3de408e2..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-streaming-with-truncation.mjs +++ /dev/null @@ -1,16 +0,0 @@ -import * as Sentry from '@sentry/node'; -import { loggingTransport } from '@sentry-internal/node-integration-tests'; - -Sentry.init({ - dsn: 'https://public@dsn.ingest.sentry.io/1337', - release: '1.0', - tracesSampleRate: 1.0, - dataCollection: { genAI: { inputs: true, outputs: true } }, - transport: loggingTransport, - traceLifecycle: 'stream', - integrations: [ - Sentry.anthropicAIIntegration({ - enableTruncation: true, - }), - ], -}); diff --git a/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-with-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-with-truncation.mjs deleted file mode 100644 index c593efd41fdb..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-with-truncation.mjs +++ /dev/null @@ -1,18 +0,0 @@ -import * as Sentry from '@sentry/node'; -import { loggingTransport } from '@sentry-internal/node-integration-tests'; - -Sentry.init({ - dsn: 'https://public@dsn.ingest.sentry.io/1337', - release: '1.0', - tracesSampleRate: 1.0, - dataCollection: { genAI: { inputs: true, outputs: true } }, - transport: loggingTransport, - integrations: [Sentry.anthropicAIIntegration({ enableTruncation: true })], - beforeSendTransaction: event => { - // Filter out mock express server transactions - if (event.transaction.includes('/anthropic/v1/')) { - return null; - } - return event; - }, -}); diff --git a/dev-packages/node-integration-tests/suites/tracing/anthropic/scenario-media-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/anthropic/scenario-media-stripping.mjs similarity index 96% rename from dev-packages/node-integration-tests/suites/tracing/anthropic/scenario-media-truncation.mjs rename to dev-packages/node-integration-tests/suites/tracing/anthropic/scenario-media-stripping.mjs index 48f337e2b23c..92f609875dc8 100644 --- a/dev-packages/node-integration-tests/suites/tracing/anthropic/scenario-media-truncation.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/anthropic/scenario-media-stripping.mjs @@ -36,7 +36,6 @@ async function run() { }); // Send the image showing the number 3 - // Put the image in the last message so it doesn't get dropped await client.messages.create({ model: 'claude-3-haiku-20240307', max_tokens: 1024, diff --git a/dev-packages/node-integration-tests/suites/tracing/anthropic/scenario-message-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/anthropic/scenario-message-truncation.mjs deleted file mode 100644 index 27aa8494f693..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/anthropic/scenario-message-truncation.mjs +++ /dev/null @@ -1,75 +0,0 @@ -import Anthropic from '@anthropic-ai/sdk'; -import * as Sentry from '@sentry/node'; -import express from 'express'; - -function startMockAnthropicServer() { - const app = express(); - app.use(express.json({ limit: '10mb' })); - - app.post('/anthropic/v1/messages', (req, res) => { - res.send({ - id: 'msg-truncation-test', - type: 'message', - role: 'assistant', - content: [{ type: 'text', text: 'Response to truncated messages' }], - model: req.body.model, - stop_reason: 'end_turn', - stop_sequence: null, - usage: { input_tokens: 10, output_tokens: 15 }, - }); - }); - - return new Promise(resolve => { - const server = app.listen(0, () => { - resolve(server); - }); - }); -} - -async function run() { - const server = await startMockAnthropicServer(); - - await Sentry.startSpan({ op: 'function', name: 'main' }, async () => { - const client = new Anthropic({ - apiKey: 'mock-api-key', - baseURL: `http://localhost:${server.address().port}/anthropic`, - }); - - // Test 1: Given an array of messages only the last message should be kept - // The last message should be truncated to fit within the 20KB limit - const largeContent1 = 'A'.repeat(15000); // ~15KB - const largeContent2 = 'B'.repeat(15000); // ~15KB - const largeContent3 = 'C'.repeat(25000) + 'D'.repeat(25000); // ~50KB (will be truncated, only C's remain) - - await client.messages.create({ - model: 'claude-3-haiku-20240307', - max_tokens: 100, - messages: [ - { role: 'user', content: largeContent1 }, - { role: 'assistant', content: largeContent2 }, - { role: 'user', content: largeContent3 }, - ], - temperature: 0.7, - }); - - // Test 2: Given an array of messages only the last message should be kept - // The last message is small, so it should be kept intact - const smallContent = 'This is a small message that fits within the limit'; - await client.messages.create({ - model: 'claude-3-haiku-20240307', - max_tokens: 100, - messages: [ - { role: 'user', content: largeContent1 }, - { role: 'assistant', content: largeContent2 }, - { role: 'user', content: smallContent }, - ], - temperature: 0.7, - }); - }); - - await Sentry.flush(2000); - - server.close(); -} - -run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/anthropic/scenario-no-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/anthropic/scenario-no-truncation.mjs deleted file mode 100644 index f66cee978733..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/anthropic/scenario-no-truncation.mjs +++ /dev/null @@ -1,64 +0,0 @@ -import Anthropic from '@anthropic-ai/sdk'; -import * as Sentry from '@sentry/node'; -import express from 'express'; - -function startMockAnthropicServer() { - const app = express(); - app.use(express.json({ limit: '10mb' })); - - app.post('/anthropic/v1/messages', (req, res) => { - res.send({ - id: 'msg-no-truncation-test', - type: 'message', - role: 'assistant', - content: [{ type: 'text', text: 'Response' }], - model: req.body.model, - stop_reason: 'end_turn', - stop_sequence: null, - usage: { input_tokens: 10, output_tokens: 5 }, - }); - }); - - return new Promise(resolve => { - const server = app.listen(0, () => { - resolve(server); - }); - }); -} - -async function run() { - const server = await startMockAnthropicServer(); - - await Sentry.startSpan({ op: 'function', name: 'main' }, async () => { - const client = new Anthropic({ - apiKey: 'mock-api-key', - baseURL: `http://localhost:${server.address().port}/anthropic`, - }); - - // Multiple messages with long content (would normally be truncated and popped to last message only) - const longContent = 'A'.repeat(50_000); - await client.messages.create({ - model: 'claude-3-haiku-20240307', - max_tokens: 100, - messages: [ - { role: 'user', content: longContent }, - { role: 'assistant', content: 'Some reply' }, - { role: 'user', content: 'Follow-up question' }, - ], - }); - - // Long string input (messagesFromParams wraps it in an array) - const longStringInput = 'B'.repeat(50_000); - await client.messages.create({ - model: 'claude-3-haiku-20240307', - max_tokens: 100, - input: longStringInput, - }); - }); - - await Sentry.flush(2000); - - server.close(); -} - -run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/anthropic/test.ts b/dev-packages/node-integration-tests/suites/tracing/anthropic/test.ts index 43dd96dfce5c..f4bf696ad527 100644 --- a/dev-packages/node-integration-tests/suites/tracing/anthropic/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/anthropic/test.ts @@ -601,109 +601,56 @@ describe('Anthropic integration', () => { }); }); - createEsmAndCjsTests( - __dirname, - 'scenario-message-truncation.mjs', - 'instrument-with-truncation.mjs', - (createRunner, test) => { - test('truncates messages when they exceed byte limit - keeps only last message and crops it', async () => { - await createRunner() - .ignore('event') - .expect({ - transaction: { - transaction: 'main', - }, - }) - .expect({ - span: container => { - expect(container.items).toHaveLength(2); - const smallMsgValue = JSON.stringify([ - { role: 'user', content: 'This is a small message that fits within the limit' }, - ]); - const truncatedSpan = container.items.find(span => - getStringAttributeValue(span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value)?.match( - /^\[\{"role":"user","content":"C+"\}\]$/, - ), - ); - expect(truncatedSpan).toBeDefined(); - expect(truncatedSpan!.name).toBe('chat claude-3-haiku-20240307'); - expect(truncatedSpan!.status).toBe('ok'); - expect(truncatedSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('chat'); - expect(truncatedSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); - expect(truncatedSpan!.attributes['sentry.origin'].value).toBe('auto.ai.anthropic'); - expect(truncatedSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); - expect(truncatedSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('claude-3-haiku-20240307'); - - const smallMessageSpan = container.items.find( - span => span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value === smallMsgValue, - ); - expect(smallMessageSpan).toBeDefined(); - expect(smallMessageSpan!.name).toBe('chat claude-3-haiku-20240307'); - expect(smallMessageSpan!.status).toBe('ok'); - expect(smallMessageSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('chat'); - expect(smallMessageSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); - expect(smallMessageSpan!.attributes['sentry.origin'].value).toBe('auto.ai.anthropic'); - expect(smallMessageSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); - expect(smallMessageSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe( - 'claude-3-haiku-20240307', - ); - }, - }) - .start() - .completed(); - }); - }, - ); - - createEsmAndCjsTests( - __dirname, - 'scenario-media-truncation.mjs', - 'instrument-with-truncation.mjs', - (createRunner, test) => { - test('truncates media attachment, keeping all other details', async () => { - const expectedMediaMessages = JSON.stringify([ - { - role: 'user', - content: [ - { - type: 'image', - source: { - type: 'base64', - media_type: 'image/png', - data: '[Blob substitute]', - }, + createEsmAndCjsTests(__dirname, 'scenario-media-stripping.mjs', 'instrument-with-pii.mjs', (createRunner, test) => { + test('strips media attachment, keeping all other messages and details', async () => { + const expectedMediaMessages = JSON.stringify([ + { + role: 'user', + content: 'what number is this?', + }, + { + role: 'user', + content: [ + { + type: 'image', + source: { + type: 'base64', + media_type: 'image/png', + data: '[Blob substitute]', }, - ], - }, - ]); - await createRunner() - .ignore('event') - .expect({ - transaction: { - transaction: 'main', }, - }) - .expect({ - span: container => { - expect(container.items).toHaveLength(1); - const [firstSpan] = container.items; + ], + }, + ]); + await createRunner() + .ignore('event') + .expect({ + transaction: { + transaction: 'main', + }, + }) + .expect({ + span: container => { + expect(container.items).toHaveLength(1); + const [firstSpan] = container.items; - // [0] messages.create with media attachment — image data replaced, other fields preserved - expect(firstSpan!.name).toBe('chat claude-3-haiku-20240307'); - expect(firstSpan!.status).toBe('ok'); - expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toBe(expectedMediaMessages); - expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('chat'); - expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); - expect(firstSpan!.attributes['sentry.origin'].value).toBe('auto.ai.anthropic'); - expect(firstSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); - expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('claude-3-haiku-20240307'); - }, - }) - .start() - .completed(); - }); - }, - ); + // messages.create with media attachment — image data replaced, all other messages/fields preserved + expect(firstSpan!.name).toBe('chat claude-3-haiku-20240307'); + expect(firstSpan!.status).toBe('ok'); + expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toBe(expectedMediaMessages); + expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('chat'); + expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + expect(firstSpan!.attributes['sentry.origin'].value).toBe( + isOrchestrionEnabled() ? 'auto.ai.orchestrion.anthropic' : 'auto.ai.anthropic', + ); + expect(firstSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); + expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('claude-3-haiku-20240307'); + }, + }) + .start() + .completed(); + }); + }); createEsmAndCjsTests( __dirname, @@ -734,61 +681,14 @@ describe('Anthropic integration', () => { }, ); - const longContent = 'A'.repeat(50_000); - const longStringInput = 'B'.repeat(50_000); - - const EXPECTED_TRANSACTION_NO_TRUNCATION = { - transaction: 'main', - }; - - createEsmAndCjsTests( - __dirname, - 'scenario-no-truncation.mjs', - 'instrument-no-truncation.mjs', - (createRunner, test) => { - test('does not truncate input messages when enableTruncation is false', async () => { - const expectedAllMessages = JSON.stringify([ - { role: 'user', content: longContent }, - { role: 'assistant', content: 'Some reply' }, - { role: 'user', content: 'Follow-up question' }, - ]); - const expectedLongString = JSON.stringify([longStringInput]); - await createRunner() - .ignore('event') - .expect({ transaction: EXPECTED_TRANSACTION_NO_TRUNCATION }) - .expect({ - span: container => { - expect(container.items).toHaveLength(2); - const conversationSpan = container.items.find( - span => span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value === expectedAllMessages, - ); - expect(conversationSpan).toBeDefined(); - - const longStringSpan = container.items.find( - span => span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value === expectedLongString, - ); - expect(longStringSpan).toBeDefined(); - }, - }) - .start() - .completed(); - }); - }, - ); - - const streamingLongContent = 'A'.repeat(50_000); - createEsmAndCjsTests(__dirname, 'scenario-span-streaming.mjs', 'instrument-streaming.mjs', (createRunner, test) => { - test('automatically disables truncation when span streaming is enabled', async () => { + test('records full gen_ai input messages when span streaming is enabled', async () => { + const longContent = 'A'.repeat(50_000); await createRunner() .expect({ span: container => { - const spans = container.items; - - const chatSpan = spans.find(s => - getStringAttributeValue(s.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value)?.includes( - streamingLongContent, - ), + const chatSpan = container.items.find(s => + getStringAttributeValue(s.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value)?.includes(longContent), ); expect(chatSpan).toBeDefined(); }, @@ -797,34 +697,4 @@ describe('Anthropic integration', () => { .completed(); }); }); - - createEsmAndCjsTests( - __dirname, - 'scenario-span-streaming.mjs', - 'instrument-streaming-with-truncation.mjs', - (createRunner, test) => { - test('respects explicit enableTruncation: true even when span streaming is enabled', async () => { - await createRunner() - .expect({ - span: container => { - const spans = container.items; - - // With explicit enableTruncation: true, content should be truncated despite streaming. - // Find the chat span by matching the start of the truncated content (the 'A' repeated messages). - const chatSpan = spans.find(s => - getStringAttributeValue(s.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value)?.startsWith( - '[{"role":"user","content":"AAAA', - ), - ); - expect(chatSpan).toBeDefined(); - expect( - (getStringAttributeValue(chatSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value) ?? '').length, - ).toBeLessThan(streamingLongContent.length); - }, - }) - .start() - .completed(); - }); - }, - ); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/google-genai/instrument-no-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/google-genai/instrument-no-truncation.mjs deleted file mode 100644 index 2b1f9cfd1dca..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/google-genai/instrument-no-truncation.mjs +++ /dev/null @@ -1,24 +0,0 @@ -import * as Sentry from '@sentry/node'; -import { loggingTransport } from '@sentry-internal/node-integration-tests'; - -Sentry.init({ - dsn: 'https://public@dsn.ingest.sentry.io/1337', - release: '1.0', - tracesSampleRate: 1.0, - dataCollection: { genAI: { inputs: false, outputs: false } }, - transport: loggingTransport, - integrations: [ - Sentry.googleGenAIIntegration({ - recordInputs: true, - recordOutputs: true, - enableTruncation: false, - }), - ], - beforeSendTransaction: event => { - // Filter out mock express server transactions - if (event.transaction.includes('/v1beta/')) { - return null; - } - return event; - }, -}); diff --git a/dev-packages/node-integration-tests/suites/tracing/google-genai/instrument-streaming-with-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/google-genai/instrument-streaming-with-truncation.mjs deleted file mode 100644 index 0e148493344a..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/google-genai/instrument-streaming-with-truncation.mjs +++ /dev/null @@ -1,16 +0,0 @@ -import * as Sentry from '@sentry/node'; -import { loggingTransport } from '@sentry-internal/node-integration-tests'; - -Sentry.init({ - dsn: 'https://public@dsn.ingest.sentry.io/1337', - release: '1.0', - tracesSampleRate: 1.0, - dataCollection: { genAI: { inputs: true, outputs: true } }, - transport: loggingTransport, - traceLifecycle: 'stream', - integrations: [ - Sentry.googleGenAIIntegration({ - enableTruncation: true, - }), - ], -}); diff --git a/dev-packages/node-integration-tests/suites/tracing/google-genai/instrument-with-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/google-genai/instrument-with-truncation.mjs deleted file mode 100644 index 7cccb1ccb487..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/google-genai/instrument-with-truncation.mjs +++ /dev/null @@ -1,18 +0,0 @@ -import * as Sentry from '@sentry/node'; -import { loggingTransport } from '@sentry-internal/node-integration-tests'; - -Sentry.init({ - dsn: 'https://public@dsn.ingest.sentry.io/1337', - release: '1.0', - tracesSampleRate: 1.0, - dataCollection: { genAI: { inputs: true, outputs: true } }, - transport: loggingTransport, - integrations: [Sentry.googleGenAIIntegration({ enableTruncation: true })], - beforeSendTransaction: event => { - // Filter out mock express server transactions - if (event.transaction.includes('/v1beta/')) { - return null; - } - return event; - }, -}); diff --git a/dev-packages/node-integration-tests/suites/tracing/google-genai/scenario-message-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/google-genai/scenario-message-truncation.mjs deleted file mode 100644 index d3cb34648f4a..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/google-genai/scenario-message-truncation.mjs +++ /dev/null @@ -1,81 +0,0 @@ -import { GoogleGenAI } from '@google/genai'; -import * as Sentry from '@sentry/node'; -import express from 'express'; - -function startMockGoogleGenAIServer() { - const app = express(); - app.use(express.json({ limit: '10mb' })); - - app.post('/v1beta/models/:model\\:generateContent', (req, res) => { - res.send({ - candidates: [ - { - content: { parts: [{ text: 'Response to truncated messages' }], role: 'model' }, - finishReason: 'stop', - index: 0, - }, - ], - usageMetadata: { promptTokenCount: 10, candidatesTokenCount: 15, totalTokenCount: 25 }, - }); - }); - - return new Promise(resolve => { - const server = app.listen(0, () => { - resolve(server); - }); - }); -} - -async function run() { - const server = await startMockGoogleGenAIServer(); - - await Sentry.startSpan({ op: 'function', name: 'main' }, async () => { - const client = new GoogleGenAI({ - apiKey: 'mock-api-key', - httpOptions: { baseUrl: `http://localhost:${server.address().port}` }, - }); - - // Test 1: Given an array of messages only the last message should be kept - // The last message should be truncated to fit within the 20KB limit - const largeContent1 = 'A'.repeat(15000); // ~15KB - const largeContent2 = 'B'.repeat(15000); // ~15KB - const largeContent3 = 'C'.repeat(25000) + 'D'.repeat(25000); // ~50KB (will be truncated, only C's remain) - - await client.models.generateContent({ - model: 'gemini-1.5-flash', - config: { - temperature: 0.7, - topP: 0.9, - maxOutputTokens: 100, - }, - contents: [ - { role: 'user', parts: [{ text: largeContent1 }] }, - { role: 'model', parts: [{ text: largeContent2 }] }, - { role: 'user', parts: [{ text: largeContent3 }] }, - ], - }); - - // Test 2: Given an array of messages only the last message should be kept - // The last message is small, so it should be kept intact - const smallContent = 'This is a small message that fits within the limit'; - await client.models.generateContent({ - model: 'gemini-1.5-flash', - config: { - temperature: 0.7, - topP: 0.9, - maxOutputTokens: 100, - }, - contents: [ - { role: 'user', parts: [{ text: largeContent1 }] }, - { role: 'model', parts: [{ text: largeContent2 }] }, - { role: 'user', parts: [{ text: smallContent }] }, - ], - }); - }); - - await Sentry.flush(2000); - - server.close(); -} - -run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/google-genai/scenario-no-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/google-genai/scenario-no-truncation.mjs deleted file mode 100644 index 67ece6759577..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/google-genai/scenario-no-truncation.mjs +++ /dev/null @@ -1,55 +0,0 @@ -import { GoogleGenAI } from '@google/genai'; -import * as Sentry from '@sentry/node'; -import express from 'express'; - -function startMockGoogleGenAIServer() { - const app = express(); - app.use(express.json({ limit: '10mb' })); - - app.post('/v1beta/models/:model\\:generateContent', (req, res) => { - res.send({ - candidates: [ - { - content: { parts: [{ text: 'Response' }], role: 'model' }, - finishReason: 'stop', - index: 0, - }, - ], - usageMetadata: { promptTokenCount: 10, candidatesTokenCount: 5, totalTokenCount: 15 }, - }); - }); - - return new Promise(resolve => { - const server = app.listen(0, () => { - resolve(server); - }); - }); -} - -async function run() { - const server = await startMockGoogleGenAIServer(); - - await Sentry.startSpan({ op: 'function', name: 'main' }, async () => { - const client = new GoogleGenAI({ - apiKey: 'mock-api-key', - httpOptions: { baseUrl: `http://localhost:${server.address().port}` }, - }); - - // Long content that would normally be truncated - const longContent = 'A'.repeat(50_000); - await client.models.generateContent({ - model: 'gemini-1.5-flash', - contents: [ - { role: 'user', parts: [{ text: longContent }] }, - { role: 'model', parts: [{ text: 'Some reply' }] }, - { role: 'user', parts: [{ text: 'Follow-up question' }] }, - ], - }); - }); - - await Sentry.flush(2000); - - server.close(); -} - -run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/google-genai/test.ts b/dev-packages/node-integration-tests/suites/tracing/google-genai/test.ts index 81b983ea4231..73e4ba1932a5 100644 --- a/dev-packages/node-integration-tests/suites/tracing/google-genai/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/google-genai/test.ts @@ -346,50 +346,6 @@ describe('Google GenAI integration', () => { }); }); - createEsmAndCjsTests( - __dirname, - 'scenario-message-truncation.mjs', - 'instrument-with-truncation.mjs', - (createRunner, test) => { - test('truncates messages when they exceed byte limit - keeps only last message and crops it', async () => { - await createRunner() - .ignore('event') - .expect({ transaction: { transaction: 'main' } }) - .expect({ - span: container => { - expect(container.items).toHaveLength(2); - const truncatedSpan = container.items.find(span => - getStringAttributeValue(span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value)?.match( - /^\[\{"role":"user","parts":\[\{"text":"C+"\}\]\}\]$/, - ), - ); - expect(truncatedSpan).toBeDefined(); - expect(truncatedSpan!.name).toBe('generate_content gemini-1.5-flash'); - expect(truncatedSpan!.status).toBe('ok'); - expect(truncatedSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('generate_content'); - - const smallMessageSpan = container.items.find( - span => - span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value === - JSON.stringify([ - { - role: 'user', - parts: [{ text: 'This is a small message that fits within the limit' }], - }, - ]), - ); - expect(smallMessageSpan).toBeDefined(); - expect(smallMessageSpan!.name).toBe('generate_content gemini-1.5-flash'); - expect(smallMessageSpan!.status).toBe('ok'); - expect(smallMessageSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('generate_content'); - }, - }) - .start() - .completed(); - }); - }, - ); - createEsmAndCjsTests( __dirname, 'scenario-system-instructions.mjs', @@ -504,88 +460,20 @@ describe('Google GenAI integration', () => { }); }); - const longContent = 'A'.repeat(50_000); - - createEsmAndCjsTests( - __dirname, - 'scenario-no-truncation.mjs', - 'instrument-no-truncation.mjs', - (createRunner, test) => { - test('does not truncate input messages when enableTruncation is false', async () => { - await createRunner() - .ignore('event') - .expect({ transaction: { transaction: 'main' } }) - .expect({ - span: container => { - expect(container.items).toHaveLength(1); - const [firstSpan] = container.items; - - // [0] generate_content with full (non-truncated) input messages - expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('generate_content'); - expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toBe( - JSON.stringify([ - { role: 'user', parts: [{ text: longContent }] }, - { role: 'model', parts: [{ text: 'Some reply' }] }, - { role: 'user', parts: [{ text: 'Follow-up question' }] }, - ]), - ); - }, - }) - .start() - .completed(); - }); - }, - ); - - const streamingLongContent = 'A'.repeat(50_000); - createEsmAndCjsTests(__dirname, 'scenario-span-streaming.mjs', 'instrument-streaming.mjs', (createRunner, test) => { - test('automatically disables truncation when span streaming is enabled', async () => { + test('records full gen_ai input messages when span streaming is enabled', async () => { + const longContent = 'A'.repeat(50_000); await createRunner() .expect({ span: container => { - const spans = container.items; - - const chatSpan = spans.find(s => - getStringAttributeValue(s.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value)?.includes( - streamingLongContent, - ), + const generateContentSpan = container.items.find(s => + getStringAttributeValue(s.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value)?.includes(longContent), ); - expect(chatSpan).toBeDefined(); + expect(generateContentSpan).toBeDefined(); }, }) .start() .completed(); }); }); - - createEsmAndCjsTests( - __dirname, - 'scenario-span-streaming.mjs', - 'instrument-streaming-with-truncation.mjs', - (createRunner, test) => { - test('respects explicit enableTruncation: true even when span streaming is enabled', async () => { - await createRunner() - .expect({ - span: container => { - const spans = container.items; - - // With explicit enableTruncation: true, content should be truncated despite streaming. - // Find the chat span by matching the start of the truncated content (the 'A' repeated messages). - const chatSpan = spans.find(s => - getStringAttributeValue(s.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value)?.startsWith( - '[{"role":"user","parts":[{"text":"AAAA', - ), - ); - expect(chatSpan).toBeDefined(); - expect( - (getStringAttributeValue(chatSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value) ?? '').length, - ).toBeLessThan(streamingLongContent.length); - }, - }) - .start() - .completed(); - }); - }, - ); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-no-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-no-truncation.mjs deleted file mode 100644 index 0a647f326044..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-no-truncation.mjs +++ /dev/null @@ -1,24 +0,0 @@ -import * as Sentry from '@sentry/node'; -import { loggingTransport } from '@sentry-internal/node-integration-tests'; - -Sentry.init({ - dsn: 'https://public@dsn.ingest.sentry.io/1337', - release: '1.0', - tracesSampleRate: 1.0, - dataCollection: { genAI: { inputs: true, outputs: true } }, - transport: loggingTransport, - integrations: [ - Sentry.langChainIntegration({ - enableTruncation: false, - recordInputs: true, - recordOutputs: true, - }), - ], - beforeSendTransaction: event => { - // Filter out mock express server transactions - if (event.transaction.includes('/v1/messages') || event.transaction.includes('/v1/embeddings')) { - return null; - } - return event; - }, -}); diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-streaming-with-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-streaming-with-truncation.mjs deleted file mode 100644 index 2a2365407b79..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-streaming-with-truncation.mjs +++ /dev/null @@ -1,16 +0,0 @@ -import * as Sentry from '@sentry/node'; -import { loggingTransport } from '@sentry-internal/node-integration-tests'; - -Sentry.init({ - dsn: 'https://public@dsn.ingest.sentry.io/1337', - release: '1.0', - tracesSampleRate: 1.0, - dataCollection: { genAI: { inputs: true, outputs: true } }, - transport: loggingTransport, - traceLifecycle: 'stream', - integrations: [ - Sentry.langChainIntegration({ - enableTruncation: true, - }), - ], -}); diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-with-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-with-truncation.mjs deleted file mode 100644 index cb677ddd9926..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-with-truncation.mjs +++ /dev/null @@ -1,18 +0,0 @@ -import * as Sentry from '@sentry/node'; -import { loggingTransport } from '@sentry-internal/node-integration-tests'; - -Sentry.init({ - dsn: 'https://public@dsn.ingest.sentry.io/1337', - release: '1.0', - tracesSampleRate: 1.0, - dataCollection: { genAI: { inputs: true, outputs: true } }, - transport: loggingTransport, - integrations: [Sentry.langChainIntegration({ enableTruncation: true })], - beforeSendTransaction: event => { - // Filter out mock express server transactions - if (event.transaction.includes('/v1/messages') || event.transaction.includes('/v1/embeddings')) { - return null; - } - return event; - }, -}); diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/scenario-message-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/langchain/scenario-message-truncation.mjs deleted file mode 100644 index 9e5e59f264ca..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/scenario-message-truncation.mjs +++ /dev/null @@ -1,82 +0,0 @@ -import { ChatAnthropic } from '@langchain/anthropic'; -import * as Sentry from '@sentry/node'; -import express from 'express'; - -function startMockAnthropicServer() { - const app = express(); - app.use(express.json()); - - app.post('/v1/messages', (req, res) => { - const model = req.body.model; - - res.json({ - id: 'msg_truncation_test', - type: 'message', - role: 'assistant', - content: [ - { - type: 'text', - text: 'Response to truncated messages', - }, - ], - model: model, - stop_reason: 'end_turn', - stop_sequence: null, - usage: { - input_tokens: 10, - output_tokens: 15, - }, - }); - }); - - return new Promise(resolve => { - const server = app.listen(0, () => { - resolve(server); - }); - }); -} - -async function run() { - const server = await startMockAnthropicServer(); - const baseUrl = `http://localhost:${server.address().port}`; - - await Sentry.startSpan({ op: 'function', name: 'main' }, async () => { - const model = new ChatAnthropic({ - model: 'claude-3-5-sonnet-20241022', - apiKey: 'mock-api-key', - clientOptions: { - baseURL: baseUrl, - }, - }); - - const largeContent1 = 'A'.repeat(15000); // ~15KB - const largeContent2 = 'B'.repeat(15000); // ~15KB - const largeContent3 = 'C'.repeat(25000) + 'D'.repeat(25000); // ~50KB (will be truncated, only C's remain) - - // Test 1: Create one very large string that gets truncated to only include Cs - await model.invoke(largeContent3); - - // Test 2: Create an array of messages that gets truncated to only include the last message - // The last message should be truncated to fit within the 20KB limit (result should again contain only Cs) - await model.invoke([ - { role: 'system', content: largeContent1 }, - { role: 'user', content: largeContent2 }, - { role: 'user', content: largeContent3 }, - ]); - - // Test 3: Given an array of messages only the last message should be kept - // The last message is small, so it should be kept intact - const smallContent = 'This is a small message that fits within the limit'; - await model.invoke([ - { role: 'system', content: largeContent1 }, - { role: 'user', content: largeContent2 }, - { role: 'user', content: smallContent }, - ]); - }); - - await Sentry.flush(2000); - - server.close(); -} - -run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/scenario-no-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/langchain/scenario-no-truncation.mjs deleted file mode 100644 index bb8f5fc35325..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/scenario-no-truncation.mjs +++ /dev/null @@ -1,56 +0,0 @@ -import { ChatAnthropic } from '@langchain/anthropic'; -import * as Sentry from '@sentry/node'; -import express from 'express'; - -function startMockAnthropicServer() { - const app = express(); - app.use(express.json({ limit: '10mb' })); - - app.post('/v1/messages', (req, res) => { - res.json({ - id: 'msg_no_truncation_test', - type: 'message', - role: 'assistant', - content: [{ type: 'text', text: 'Response' }], - model: req.body.model, - stop_reason: 'end_turn', - stop_sequence: null, - usage: { input_tokens: 10, output_tokens: 5 }, - }); - }); - - return new Promise(resolve => { - const server = app.listen(0, () => { - resolve(server); - }); - }); -} - -async function run() { - const server = await startMockAnthropicServer(); - const baseUrl = `http://localhost:${server.address().port}`; - - await Sentry.startSpan({ op: 'function', name: 'main' }, async () => { - const model = new ChatAnthropic({ - model: 'claude-3-5-sonnet-20241022', - apiKey: 'mock-api-key', - clientOptions: { - baseURL: baseUrl, - }, - }); - - // Long content that would normally be truncated - const longContent = 'A'.repeat(50_000); - await model.invoke([ - { role: 'user', content: longContent }, - { role: 'assistant', content: 'Some reply' }, - { role: 'user', content: 'Follow-up question' }, - ]); - }); - - await Sentry.flush(2000); - - server.close(); -} - -run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/test.ts b/dev-packages/node-integration-tests/suites/tracing/langchain/test.ts index d2b94f200c62..7f090db64c68 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/langchain/test.ts @@ -197,59 +197,6 @@ describe('LangChain integration', () => { }); }); - createEsmAndCjsTests( - __dirname, - 'scenario-message-truncation.mjs', - 'instrument-with-truncation.mjs', - (createRunner, test) => { - test('truncates messages when they exceed byte limit', async () => { - await createRunner() - .ignore('event') - .expect({ transaction: { transaction: 'main' } }) - .expect({ - span: container => { - expect(container.items).toHaveLength(3); - // The string-input span has no system message (and therefore no system instructions), - // while the array-input span does — use that to distinguish the two truncated spans. - const truncatedContent = /^\[\{"role":"user","content":"C+"\}\]$/; - const stringInputSpan = container.items.find( - span => - span.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE] === undefined && - getStringAttributeValue(span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value)?.match( - truncatedContent, - ), - ); - expect(stringInputSpan).toBeDefined(); - expect(stringInputSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); - expect(stringInputSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toMatch(truncatedContent); - - const arrayInputSpan = container.items.find( - span => - span.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE] !== undefined && - getStringAttributeValue(span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value)?.match( - truncatedContent, - ), - ); - expect(arrayInputSpan).toBeDefined(); - expect(arrayInputSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); - expect(arrayInputSpan!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE]).toBeDefined(); - - const smallMessageSpan = container.items.find( - span => - span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value === - JSON.stringify([{ role: 'user', content: 'This is a small message that fits within the limit' }]), - ); - expect(smallMessageSpan).toBeDefined(); - expect(smallMessageSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); - expect(smallMessageSpan!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE]).toBeDefined(); - }, - }) - .start() - .completed(); - }); - }, - ); - createEsmTests(__dirname, 'scenario-openai-before-langchain.mjs', 'instrument.mjs', (createRunner, test) => { test('demonstrates timing issue with duplicate spans', async () => { await createRunner() @@ -454,52 +401,14 @@ describe('LangChain integration', () => { }); }); - const longContent = 'A'.repeat(50_000); - - createEsmAndCjsTests( - __dirname, - 'scenario-no-truncation.mjs', - 'instrument-no-truncation.mjs', - (createRunner, test) => { - test('does not truncate input messages when enableTruncation is false', async () => { - await createRunner() - .ignore('event') - .expect({ transaction: { transaction: 'main' } }) - .expect({ - span: container => { - expect(container.items).toHaveLength(1); - const [firstSpan] = container.items; - - // [0] chat with full (untruncated) input messages - expect(firstSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); - expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toBe( - JSON.stringify([ - { role: 'user', content: longContent }, - { role: 'assistant', content: 'Some reply' }, - { role: 'user', content: 'Follow-up question' }, - ]), - ); - }, - }) - .start() - .completed(); - }); - }, - ); - - const streamingLongContent = 'A'.repeat(50_000); - createEsmAndCjsTests(__dirname, 'scenario-span-streaming.mjs', 'instrument-streaming.mjs', (createRunner, test) => { - test('automatically disables truncation when span streaming is enabled', async () => { + test('records full gen_ai input messages when span streaming is enabled', async () => { + const longContent = 'A'.repeat(50_000); await createRunner() .expect({ span: container => { - const spans = container.items; - - const chatSpan = spans.find(s => - getStringAttributeValue(s.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value)?.includes( - streamingLongContent, - ), + const chatSpan = container.items.find(s => + getStringAttributeValue(s.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value)?.includes(longContent), ); expect(chatSpan).toBeDefined(); }, @@ -508,33 +417,4 @@ describe('LangChain integration', () => { .completed(); }); }); - - createEsmAndCjsTests( - __dirname, - 'scenario-span-streaming.mjs', - 'instrument-streaming-with-truncation.mjs', - (createRunner, test) => { - test('respects explicit enableTruncation: true even when span streaming is enabled', async () => { - await createRunner() - .expect({ - span: container => { - const spans = container.items; - - // With explicit enableTruncation: true, content should be truncated despite streaming. - const chatSpan = spans.find(s => - getStringAttributeValue(s.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value)?.startsWith( - '[{"role":"user","content":"AAAA', - ), - ); - expect(chatSpan).toBeDefined(); - expect( - (getStringAttributeValue(chatSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value) ?? '').length, - ).toBeLessThan(streamingLongContent.length); - }, - }) - .start() - .completed(); - }); - }, - ); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/v1/instrument-with-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/langchain/v1/instrument-with-truncation.mjs deleted file mode 100644 index b773dbad2e6b..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/v1/instrument-with-truncation.mjs +++ /dev/null @@ -1,18 +0,0 @@ -import * as Sentry from '@sentry/node'; -import { loggingTransport } from '@sentry-internal/node-integration-tests'; - -Sentry.init({ - dsn: 'https://public@dsn.ingest.sentry.io/1337', - release: '1.0', - tracesSampleRate: 1.0, - dataCollection: { genAI: { inputs: true, outputs: true } }, - transport: loggingTransport, - integrations: [Sentry.langChainIntegration({ enableTruncation: true })], - beforeSendTransaction: event => { - // Filter out mock express server transactions - if (event.transaction.includes('/v1/messages') || event.transaction.includes('/v1/chat/completions')) { - return null; - } - return event; - }, -}); diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/v1/scenario-message-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/langchain/v1/scenario-message-truncation.mjs deleted file mode 100644 index 9e5e59f264ca..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/v1/scenario-message-truncation.mjs +++ /dev/null @@ -1,82 +0,0 @@ -import { ChatAnthropic } from '@langchain/anthropic'; -import * as Sentry from '@sentry/node'; -import express from 'express'; - -function startMockAnthropicServer() { - const app = express(); - app.use(express.json()); - - app.post('/v1/messages', (req, res) => { - const model = req.body.model; - - res.json({ - id: 'msg_truncation_test', - type: 'message', - role: 'assistant', - content: [ - { - type: 'text', - text: 'Response to truncated messages', - }, - ], - model: model, - stop_reason: 'end_turn', - stop_sequence: null, - usage: { - input_tokens: 10, - output_tokens: 15, - }, - }); - }); - - return new Promise(resolve => { - const server = app.listen(0, () => { - resolve(server); - }); - }); -} - -async function run() { - const server = await startMockAnthropicServer(); - const baseUrl = `http://localhost:${server.address().port}`; - - await Sentry.startSpan({ op: 'function', name: 'main' }, async () => { - const model = new ChatAnthropic({ - model: 'claude-3-5-sonnet-20241022', - apiKey: 'mock-api-key', - clientOptions: { - baseURL: baseUrl, - }, - }); - - const largeContent1 = 'A'.repeat(15000); // ~15KB - const largeContent2 = 'B'.repeat(15000); // ~15KB - const largeContent3 = 'C'.repeat(25000) + 'D'.repeat(25000); // ~50KB (will be truncated, only C's remain) - - // Test 1: Create one very large string that gets truncated to only include Cs - await model.invoke(largeContent3); - - // Test 2: Create an array of messages that gets truncated to only include the last message - // The last message should be truncated to fit within the 20KB limit (result should again contain only Cs) - await model.invoke([ - { role: 'system', content: largeContent1 }, - { role: 'user', content: largeContent2 }, - { role: 'user', content: largeContent3 }, - ]); - - // Test 3: Given an array of messages only the last message should be kept - // The last message is small, so it should be kept intact - const smallContent = 'This is a small message that fits within the limit'; - await model.invoke([ - { role: 'system', content: largeContent1 }, - { role: 'user', content: largeContent2 }, - { role: 'user', content: smallContent }, - ]); - }); - - await Sentry.flush(2000); - - server.close(); -} - -run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/v1/test.ts b/dev-packages/node-integration-tests/suites/tracing/langchain/v1/test.ts index 9d5c5e7ba1a3..42c79716b86a 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/v1/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/langchain/v1/test.ts @@ -12,12 +12,11 @@ import { GEN_AI_RESPONSE_TEXT_ATTRIBUTE, GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE, GEN_AI_SYSTEM_ATTRIBUTE, - GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE, GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE, GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE, GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE, } from '../../../../../../packages/core/src/tracing/ai/gen-ai-attributes'; -import { conditionalTest, getStringAttributeValue, isOrchestrionEnabled } from '../../../../utils'; +import { conditionalTest, isOrchestrionEnabled } from '../../../../utils'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../../utils/runner'; import { createEsmTests } from '../../../../utils/runner/createEsmAndCjsTests'; @@ -213,70 +212,6 @@ conditionalTest({ min: 20 })('LangChain integration (v1)', () => { }, ); - createEsmAndCjsTests( - __dirname, - 'scenario-message-truncation.mjs', - 'instrument-with-truncation.mjs', - (createRunner, test) => { - test('truncates messages when they exceed byte limit', async () => { - await createRunner() - .ignore('event') - .expect({ transaction: { transaction: 'main' } }) - .expect({ - span: container => { - expect(container.items).toHaveLength(3); - // The string-input span has no system message (and therefore no system instructions), - // while the array-input span does — use that to distinguish the two truncated spans. - const truncatedContent = /^\[\{"role":"user","content":"C+"\}\]$/; - const stringInputSpan = container.items.find( - span => - span.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE] === undefined && - getStringAttributeValue(span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value)?.match( - truncatedContent, - ), - ); - expect(stringInputSpan).toBeDefined(); - expect(stringInputSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); - expect(stringInputSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toMatch(truncatedContent); - - const arrayInputSpan = container.items.find( - span => - span.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE] !== undefined && - getStringAttributeValue(span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value)?.match( - truncatedContent, - ), - ); - expect(arrayInputSpan).toBeDefined(); - expect(arrayInputSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); - expect(arrayInputSpan!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE].value).toMatch( - /^\[\{"type":"text","content":"A+"\}\]$/, - ); - - const smallMessageSpan = container.items.find( - span => - span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value === - JSON.stringify([{ role: 'user', content: 'This is a small message that fits within the limit' }]), - ); - expect(smallMessageSpan).toBeDefined(); - expect(smallMessageSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); - expect(smallMessageSpan!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE].value).toMatch( - /^\[\{"type":"text","content":"A+"\}\]$/, - ); - }, - }) - .start() - .completed(); - }); - }, - { - additionalDependencies: { - langchain: '^1.0.0', - '@langchain/core': '^1.0.0', - '@langchain/anthropic': '^1.0.0', - }, - }, - ); - createEsmTests( __dirname, 'scenario-openai-before-langchain.mjs', diff --git a/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument-no-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument-no-truncation.mjs deleted file mode 100644 index c3933a5ea129..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument-no-truncation.mjs +++ /dev/null @@ -1,17 +0,0 @@ -import * as Sentry from '@sentry/node'; -import { loggingTransport } from '@sentry-internal/node-integration-tests'; - -Sentry.init({ - dsn: 'https://public@dsn.ingest.sentry.io/1337', - release: '1.0', - tracesSampleRate: 1.0, - dataCollection: { genAI: { inputs: true, outputs: true } }, - transport: loggingTransport, - integrations: [ - Sentry.langGraphIntegration({ - recordInputs: true, - recordOutputs: true, - enableTruncation: false, - }), - ], -}); diff --git a/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument-streaming-with-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument-streaming-with-truncation.mjs deleted file mode 100644 index c76ba5928e20..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument-streaming-with-truncation.mjs +++ /dev/null @@ -1,16 +0,0 @@ -import * as Sentry from '@sentry/node'; -import { loggingTransport } from '@sentry-internal/node-integration-tests'; - -Sentry.init({ - dsn: 'https://public@dsn.ingest.sentry.io/1337', - release: '1.0', - tracesSampleRate: 1.0, - dataCollection: { genAI: { inputs: true, outputs: true } }, - transport: loggingTransport, - traceLifecycle: 'stream', - integrations: [ - Sentry.langGraphIntegration({ - enableTruncation: true, - }), - ], -}); diff --git a/dev-packages/node-integration-tests/suites/tracing/langgraph/scenario-no-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/langgraph/scenario-no-truncation.mjs deleted file mode 100644 index 982e7a69de53..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/langgraph/scenario-no-truncation.mjs +++ /dev/null @@ -1,46 +0,0 @@ -import { END, MessagesAnnotation, START, StateGraph } from '@langchain/langgraph'; -import * as Sentry from '@sentry/node'; - -async function run() { - await Sentry.startSpan({ op: 'function', name: 'langgraph-test' }, async () => { - const mockLlm = () => { - return { - messages: [ - { - role: 'assistant', - content: 'Mock LLM response', - response_metadata: { - model_name: 'mock-model', - finish_reason: 'stop', - tokenUsage: { - promptTokens: 20, - completionTokens: 10, - totalTokens: 30, - }, - }, - }, - ], - }; - }; - - const graph = new StateGraph(MessagesAnnotation) - .addNode('agent', mockLlm) - .addEdge(START, 'agent') - .addEdge('agent', END) - .compile({ name: 'weather_assistant' }); - - // Multiple messages with long content (would normally be truncated and popped to last message only) - const longContent = 'A'.repeat(50_000); - await graph.invoke({ - messages: [ - { role: 'user', content: longContent }, - { role: 'assistant', content: 'Some reply' }, - { role: 'user', content: 'Follow-up question' }, - ], - }); - }); - - await Sentry.flush(2000); -} - -run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts b/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts index ed104fa52933..97d01cb151a8 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts @@ -278,91 +278,6 @@ describe('LangGraph integration', () => { }); }); - const longContent = 'A'.repeat(50_000); - - createEsmAndCjsTests( - __dirname, - 'scenario-no-truncation.mjs', - 'instrument-no-truncation.mjs', - (createRunner, test) => { - test('does not truncate input messages when enableTruncation is false', async () => { - await createRunner() - .ignore('event') - .expect({ transaction: { transaction: 'langgraph-test' } }) - .expect({ - span: container => { - const expectedMessages = JSON.stringify([ - { role: 'user', content: longContent }, - { role: 'assistant', content: 'Some reply' }, - { role: 'user', content: 'Follow-up question' }, - ]); - - expect(container.items).toHaveLength(2); - const invokeAgentSpan = container.items.find( - span => span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value === expectedMessages, - ); - - expect(invokeAgentSpan).toBeDefined(); - expect(invokeAgentSpan!.name).toBe('invoke_agent weather_assistant'); - }, - }) - .start() - .completed(); - }); - }, - ); - - const streamingLongContent = 'A'.repeat(50_000); - - createEsmAndCjsTests(__dirname, 'scenario-span-streaming.mjs', 'instrument-streaming.mjs', (createRunner, test) => { - test('automatically disables truncation when span streaming is enabled', async () => { - await createRunner() - .expect({ - span: container => { - const spans = container.items; - - const chatSpan = spans.find(s => - getStringAttributeValue(s.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value)?.includes( - streamingLongContent, - ), - ); - expect(chatSpan).toBeDefined(); - }, - }) - .start() - .completed(); - }); - }); - - createEsmAndCjsTests( - __dirname, - 'scenario-span-streaming.mjs', - 'instrument-streaming-with-truncation.mjs', - (createRunner, test) => { - test('respects explicit enableTruncation: true even when span streaming is enabled', async () => { - await createRunner() - .expect({ - span: container => { - const spans = container.items; - - // With explicit enableTruncation: true, content should be truncated despite streaming. - const chatSpan = spans.find(s => - getStringAttributeValue(s.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value)?.startsWith( - '[{"role":"user","content":"AAAA', - ), - ); - expect(chatSpan).toBeDefined(); - expect( - (getStringAttributeValue(chatSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value) ?? '').length, - ).toBeLessThan(streamingLongContent.length); - }, - }) - .start() - .completed(); - }); - }, - ); - // createReactAgent tests. // Spans are asserted order-independently: the span-array order is not a protocol guarantee (Sentry // rebuilds the tree from `parent_span_id`), and the provider emits tree order while the OTel exporter @@ -490,4 +405,21 @@ describe('LangGraph integration', () => { .completed(); }); }); + + createEsmAndCjsTests(__dirname, 'scenario-span-streaming.mjs', 'instrument-streaming.mjs', (createRunner, test) => { + test('records full gen_ai input messages when span streaming is enabled', async () => { + const longContent = 'A'.repeat(50_000); + await createRunner() + .expect({ + span: container => { + const chatSpan = container.items.find(s => + getStringAttributeValue(s.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value)?.includes(longContent), + ); + expect(chatSpan).toBeDefined(); + }, + }) + .start() + .completed(); + }); + }); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/instrument-no-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/openai/instrument-no-truncation.mjs deleted file mode 100644 index 442163b75b6e..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/openai/instrument-no-truncation.mjs +++ /dev/null @@ -1,23 +0,0 @@ -import * as Sentry from '@sentry/node'; -import { loggingTransport } from '@sentry-internal/node-integration-tests'; - -Sentry.init({ - dsn: 'https://public@dsn.ingest.sentry.io/1337', - release: '1.0', - tracesSampleRate: 1.0, - dataCollection: { genAI: { inputs: false, outputs: false } }, - transport: loggingTransport, - integrations: [ - Sentry.openAIIntegration({ - recordInputs: true, - recordOutputs: true, - enableTruncation: false, - }), - ], - beforeSendTransaction: event => { - if (event.transaction.includes('/openai/')) { - return null; - } - return event; - }, -}); diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/instrument-streaming-with-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/openai/instrument-streaming-with-truncation.mjs deleted file mode 100644 index c61dffa4c1f1..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/openai/instrument-streaming-with-truncation.mjs +++ /dev/null @@ -1,16 +0,0 @@ -import * as Sentry from '@sentry/node'; -import { loggingTransport } from '@sentry-internal/node-integration-tests'; - -Sentry.init({ - dsn: 'https://public@dsn.ingest.sentry.io/1337', - release: '1.0', - tracesSampleRate: 1.0, - dataCollection: { genAI: { inputs: true, outputs: true } }, - transport: loggingTransport, - traceLifecycle: 'stream', - integrations: [ - Sentry.openAIIntegration({ - enableTruncation: true, - }), - ], -}); diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/instrument-with-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/openai/instrument-with-truncation.mjs deleted file mode 100644 index cbd50b7a2262..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/openai/instrument-with-truncation.mjs +++ /dev/null @@ -1,17 +0,0 @@ -import * as Sentry from '@sentry/node'; -import { loggingTransport } from '@sentry-internal/node-integration-tests'; - -Sentry.init({ - dsn: 'https://public@dsn.ingest.sentry.io/1337', - release: '1.0', - tracesSampleRate: 1.0, - dataCollection: { genAI: { inputs: true, outputs: true } }, - transport: loggingTransport, - integrations: [Sentry.openAIIntegration({ enableTruncation: true })], - beforeSendTransaction: event => { - if (event.transaction.includes('/openai/')) { - return null; - } - return event; - }, -}); diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/scenario-no-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/openai/scenario-no-truncation.mjs deleted file mode 100644 index c5fe61c1ab66..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/openai/scenario-no-truncation.mjs +++ /dev/null @@ -1,87 +0,0 @@ -import * as Sentry from '@sentry/node'; -import express from 'express'; -import OpenAI from 'openai'; - -function startMockServer() { - const app = express(); - app.use(express.json({ limit: '10mb' })); - - app.post('/openai/chat/completions', (req, res) => { - res.send({ - id: 'chatcmpl-mock123', - object: 'chat.completion', - created: 1677652288, - model: req.body.model, - choices: [ - { - index: 0, - message: { role: 'assistant', content: 'Hello!' }, - finish_reason: 'stop', - }, - ], - usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 }, - }); - }); - - app.post('/openai/responses', (req, res) => { - res.send({ - id: 'resp_mock456', - object: 'response', - created_at: 1677652290, - model: req.body.model, - output: [ - { - type: 'message', - id: 'msg_mock_output_1', - status: 'completed', - role: 'assistant', - content: [{ type: 'output_text', text: 'Response text', annotations: [] }], - }, - ], - output_text: 'Response text', - status: 'completed', - usage: { input_tokens: 5, output_tokens: 3, total_tokens: 8 }, - }); - }); - - return new Promise(resolve => { - const server = app.listen(0, () => { - resolve(server); - }); - }); -} - -async function run() { - const server = await startMockServer(); - - await Sentry.startSpan({ op: 'function', name: 'main' }, async () => { - const client = new OpenAI({ - baseURL: `http://localhost:${server.address().port}/openai`, - apiKey: 'mock-api-key', - }); - - // Multiple messages with long content (would normally be truncated and popped to last message only) - const longContent = 'A'.repeat(50_000); - await client.chat.completions.create({ - model: 'gpt-4', - messages: [ - { role: 'user', content: longContent }, - { role: 'assistant', content: 'Some reply' }, - { role: 'user', content: 'Follow-up question' }, - ], - }); - - // Responses API with long string input (would normally be truncated) - const longStringInput = 'B'.repeat(50_000); - await client.responses.create({ - model: 'gpt-4', - input: longStringInput, - }); - }); - - // Flush is required when span streaming is enabled to ensure streamed spans are sent before the process exits - await Sentry.flush(); - server.close(); -} - -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 bbb93952136a..ba4b9aec346b 100644 --- a/dev-packages/node-integration-tests/suites/tracing/openai/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/openai/test.ts @@ -722,61 +722,6 @@ describe('OpenAI integration', () => { }); }); - const longContent = 'A'.repeat(50_000); - - createEsmAndCjsTests( - __dirname, - 'scenario-no-truncation.mjs', - 'instrument-no-truncation.mjs', - (createRunner, test) => { - test('does not truncate input messages when enableTruncation is false', async () => { - await createRunner() - .ignore('event') - .expect({ - transaction: { - transaction: 'main', - }, - }) - .expect({ - span: container => { - expect(container.items).toHaveLength(2); - const chatCompletionSpan = container.items.find( - span => span.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]?.value === 'chatcmpl-mock123', - ); - expect(chatCompletionSpan).toBeDefined(); - expect(chatCompletionSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ - type: 'string', - value: 'chatcmpl-mock123', - }); - expect(chatCompletionSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toMatchObject({ - type: 'string', - value: JSON.stringify([ - { role: 'user', content: longContent }, - { role: 'assistant', content: 'Some reply' }, - { role: 'user', content: 'Follow-up question' }, - ]), - }); - - const responsesSpan = container.items.find( - span => span.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]?.value === 'resp_mock456', - ); - expect(responsesSpan).toBeDefined(); - expect(responsesSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ - type: 'string', - value: 'resp_mock456', - }); - expect(responsesSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toMatchObject({ - type: 'string', - value: 'B'.repeat(50_000), - }); - }, - }) - .start() - .completed(); - }); - }, - ); - createEsmAndCjsTests(__dirname, 'scenario-embeddings.mjs', 'instrument.mjs', (createRunner, test) => { test('creates openai related spans with genAI recording disabled', async () => { await createRunner() @@ -1143,146 +1088,6 @@ describe('OpenAI integration', () => { }); }); - createEsmAndCjsTests( - __dirname, - 'truncation/scenario-message-truncation-completions.mjs', - 'instrument-with-truncation.mjs', - (createRunner, test) => { - test('truncates messages when they exceed byte limit - keeps only last message and crops it', async () => { - await createRunner() - .ignore('event') - .expect({ - transaction: { - transaction: 'main', - }, - }) - .expect({ - span: container => { - expect(container.items).toHaveLength(2); - const truncatedMessageSpan = container.items.find(span => - getStringAttributeValue(span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value)?.match( - /^\[\{"role":"user","content":"C+"\}\]$/, - ), - ); - expect(truncatedMessageSpan).toBeDefined(); - expect(truncatedMessageSpan!.name).toBe('chat gpt-3.5-turbo'); - expect(truncatedMessageSpan!.status).toBe('ok'); - expect(truncatedMessageSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ - type: 'string', - value: 'chat', - }); - expect(truncatedMessageSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ - type: 'string', - value: 'gen_ai.chat', - }); - expect(truncatedMessageSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ - type: 'string', - value: 'auto.ai.openai', - }); - expect(truncatedMessageSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ - type: 'string', - value: 'openai', - }); - expect(truncatedMessageSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ - type: 'string', - value: 'gpt-3.5-turbo', - }); - expect(truncatedMessageSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toMatch( - /^\[\{"role":"user","content":"C+"\}\]$/, - ); - expect(truncatedMessageSpan!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE].value).toMatch( - /^\[\{"type":"text","content":"A+"\}\]$/, - ); - - const smallMessageSpan = container.items.find( - span => - span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value === - JSON.stringify([{ role: 'user', content: 'This is a small message that fits within the limit' }]), - ); - expect(smallMessageSpan).toBeDefined(); - expect(smallMessageSpan!.name).toBe('chat gpt-3.5-turbo'); - expect(smallMessageSpan!.status).toBe('ok'); - expect(smallMessageSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ - type: 'string', - value: 'chat', - }); - expect(smallMessageSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ - type: 'string', - value: 'gen_ai.chat', - }); - expect(smallMessageSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ - type: 'string', - value: 'auto.ai.openai', - }); - expect(smallMessageSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ - type: 'string', - value: 'openai', - }); - expect(smallMessageSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ - type: 'string', - value: 'gpt-3.5-turbo', - }); - expect(smallMessageSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ - type: 'string', - value: JSON.stringify([ - { role: 'user', content: 'This is a small message that fits within the limit' }, - ]), - }); - expect(smallMessageSpan!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE].value).toMatch( - /^\[\{"type":"text","content":"A+"\}\]$/, - ); - }, - }) - .start() - .completed(); - }); - }, - ); - - createEsmAndCjsTests( - __dirname, - 'truncation/scenario-message-truncation-responses.mjs', - 'instrument-with-truncation.mjs', - (createRunner, test) => { - test('truncates string inputs when they exceed byte limit', async () => { - await createRunner() - .ignore('event') - .expect({ - transaction: { - transaction: 'main', - }, - }) - .expect({ - span: container => { - expect(container.items).toHaveLength(1); - const [firstSpan] = container.items; - - // [0] long A-string input is truncated - expect(firstSpan!.name).toBe('chat gpt-3.5-turbo'); - expect(firstSpan!.status).toBe('ok'); - expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); - expect(firstSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ - type: 'string', - value: 'gen_ai.chat', - }); - expect(firstSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ - type: 'string', - value: 'auto.ai.openai', - }); - expect(firstSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); - expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ - type: 'string', - value: 'gpt-3.5-turbo', - }); - expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toMatch(/^A+$/); - }, - }) - .start() - .completed(); - }); - }, - ); - // Test for conversation ID support (Conversations API and previous_response_id) createEsmAndCjsTests(__dirname, 'scenario-conversation.mjs', 'instrument.mjs', (createRunner, test) => { test('captures conversation ID from Conversations API and previous_response_id', async () => { @@ -1563,7 +1368,7 @@ describe('OpenAI integration', () => { }); }); - createEsmAndCjsTests(__dirname, 'scenario-vision.mjs', 'instrument-with-truncation.mjs', (createRunner, test) => { + createEsmAndCjsTests(__dirname, 'scenario-vision.mjs', 'instrument-with-pii.mjs', (createRunner, test) => { test('redacts inline base64 image data in vision requests', async () => { await createRunner() .ignore('event') @@ -1617,27 +1422,22 @@ describe('OpenAI integration', () => { }); }); - const streamingLongContent = 'A'.repeat(50_000); - const streamingLongString = 'B'.repeat(50_000); - createEsmAndCjsTests(__dirname, 'scenario-span-streaming.mjs', 'instrument-streaming.mjs', (createRunner, test) => { - test('automatically disables truncation when span streaming is enabled', async () => { + test('records full gen_ai input messages when span streaming is enabled', async () => { + const longContent = 'A'.repeat(50_000); + const longStringInput = 'B'.repeat(50_000); await createRunner() .expect({ span: container => { const spans = container.items; const chatSpan = spans.find(s => - getStringAttributeValue(s.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value)?.includes( - streamingLongContent, - ), + getStringAttributeValue(s.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value)?.includes(longContent), ); expect(chatSpan).toBeDefined(); const responsesSpan = spans.find(s => - getStringAttributeValue(s.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value)?.includes( - streamingLongString, - ), + getStringAttributeValue(s.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value)?.includes(longStringInput), ); expect(responsesSpan).toBeDefined(); }, @@ -1646,44 +1446,4 @@ describe('OpenAI integration', () => { .completed(); }); }); - - createEsmAndCjsTests( - __dirname, - 'scenario-span-streaming.mjs', - 'instrument-streaming-with-truncation.mjs', - (createRunner, test) => { - test('respects explicit enableTruncation: true even when span streaming is enabled', async () => { - await createRunner() - .expect({ - span: container => { - const spans = container.items; - - // With explicit enableTruncation: true, content should be truncated despite streaming. - // Truncation keeps only the last message (50k 'A's) and crops it to the byte limit. - const chatSpan = spans.find(s => - getStringAttributeValue(s.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value)?.startsWith( - '[{"role":"user","content":"AAAA', - ), - ); - expect(chatSpan).toBeDefined(); - expect( - (getStringAttributeValue(chatSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value) ?? '').length, - ).toBeLessThan(streamingLongContent.length); - - // The responses API string input (50k 'B's) should also be truncated. - const responsesSpan = spans.find(s => - getStringAttributeValue(s.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value)?.startsWith('BBB'), - ); - expect(responsesSpan).toBeDefined(); - expect( - (getStringAttributeValue(responsesSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value) ?? '') - .length, - ).toBeLessThan(streamingLongString.length); - }, - }) - .start() - .completed(); - }); - }, - ); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/truncation/scenario-message-truncation-completions.mjs b/dev-packages/node-integration-tests/suites/tracing/openai/truncation/scenario-message-truncation-completions.mjs deleted file mode 100644 index f443ab3a47fe..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/openai/truncation/scenario-message-truncation-completions.mjs +++ /dev/null @@ -1,81 +0,0 @@ -import { instrumentOpenAiClient } from '@sentry/core'; -import * as Sentry from '@sentry/node'; - -class MockOpenAI { - constructor(config) { - this.apiKey = config.apiKey; - - this.chat = { - completions: { - create: async params => { - // Simulate processing time - await new Promise(resolve => setTimeout(resolve, 10)); - - return { - id: 'chatcmpl-completions-truncation-test', - object: 'chat.completion', - created: 1677652288, - model: params.model, - system_fingerprint: 'fp_44709d6fcb', - choices: [ - { - index: 0, - message: { - role: 'assistant', - content: 'Response to truncated messages', - }, - finish_reason: 'stop', - }, - ], - usage: { - prompt_tokens: 10, - completion_tokens: 15, - total_tokens: 25, - }, - }; - }, - }, - }; - } -} - -async function run() { - await Sentry.startSpan({ op: 'function', name: 'main' }, async () => { - const mockClient = new MockOpenAI({ - apiKey: 'mock-api-key', - }); - - const client = instrumentOpenAiClient(mockClient, { enableTruncation: true, recordInputs: true }); - - // Test 1: Given an array of messages only the last message should be kept - // The last message should be truncated to fit within the 20KB limit - const largeContent1 = 'A'.repeat(15000); // ~15KB - const largeContent2 = 'B'.repeat(15000); // ~15KB - const largeContent3 = 'C'.repeat(25000) + 'D'.repeat(25000); // ~50KB (will be truncated, only C's remain) - - await client.chat.completions.create({ - model: 'gpt-3.5-turbo', - messages: [ - { role: 'system', content: largeContent1 }, - { role: 'user', content: largeContent2 }, - { role: 'user', content: largeContent3 }, - ], - temperature: 0.7, - }); - - // Test 2: Given an array of messages only the last message should be kept - // The last message is small, so it should be kept intact - const smallContent = 'This is a small message that fits within the limit'; - await client.chat.completions.create({ - model: 'gpt-3.5-turbo', - messages: [ - { role: 'system', content: largeContent1 }, - { role: 'user', content: largeContent2 }, - { role: 'user', content: smallContent }, - ], - temperature: 0.7, - }); - }); -} - -run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/truncation/scenario-message-truncation-responses.mjs b/dev-packages/node-integration-tests/suites/tracing/openai/truncation/scenario-message-truncation-responses.mjs deleted file mode 100644 index 692f5fd87ca7..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/openai/truncation/scenario-message-truncation-responses.mjs +++ /dev/null @@ -1,93 +0,0 @@ -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, { enableTruncation: true, recordInputs: true }); - - // 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/vercelai/instrument-no-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/vercelai/instrument-no-truncation.mjs deleted file mode 100644 index 64adb199d9c6..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/vercelai/instrument-no-truncation.mjs +++ /dev/null @@ -1,17 +0,0 @@ -import * as Sentry from '@sentry/node'; -import { loggingTransport } from '@sentry-internal/node-integration-tests'; - -Sentry.init({ - dsn: 'https://public@dsn.ingest.sentry.io/1337', - release: '1.0', - tracesSampleRate: 1.0, - dataCollection: { genAI: { inputs: true, outputs: true } }, - transport: loggingTransport, - integrations: [ - Sentry.vercelAIIntegration({ - recordInputs: true, - recordOutputs: true, - enableTruncation: false, - }), - ], -}); diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/instrument-with-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/vercelai/instrument-with-truncation.mjs deleted file mode 100644 index 0147e17d7b5f..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/vercelai/instrument-with-truncation.mjs +++ /dev/null @@ -1,11 +0,0 @@ -import * as Sentry from '@sentry/node'; -import { loggingTransport } from '@sentry-internal/node-integration-tests'; - -Sentry.init({ - dsn: 'https://public@dsn.ingest.sentry.io/1337', - release: '1.0', - tracesSampleRate: 1.0, - dataCollection: { genAI: { inputs: true, outputs: true } }, - transport: loggingTransport, - integrations: [Sentry.vercelAIIntegration({ enableTruncation: true })], -}); diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/scenario-message-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/vercelai/scenario-message-truncation.mjs deleted file mode 100644 index 6c3f7327e64b..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/vercelai/scenario-message-truncation.mjs +++ /dev/null @@ -1,51 +0,0 @@ -import * as Sentry from '@sentry/node'; -import { generateText } from 'ai'; -import { MockLanguageModelV1 } from 'ai/test'; - -async function run() { - await Sentry.startSpan({ op: 'function', name: 'main' }, async () => { - const largeContent1 = 'A'.repeat(15000); // ~15KB - const largeContent2 = 'B'.repeat(15000); // ~15KB - const largeContent3 = 'C'.repeat(25000) + 'D'.repeat(25000); // ~50KB (will be truncated) - - // Test 1: Messages array with large last message that gets truncated - // Only the last message should be kept, and it should be truncated to only Cs - await generateText({ - experimental_telemetry: { isEnabled: true }, - model: new MockLanguageModelV1({ - doGenerate: async () => ({ - rawCall: { rawPrompt: null, rawSettings: {} }, - finishReason: 'stop', - usage: { promptTokens: 10, completionTokens: 5 }, - text: 'Response to truncated messages', - }), - }), - messages: [ - { role: 'user', content: largeContent1 }, - { role: 'assistant', content: largeContent2 }, - { role: 'user', content: largeContent3 }, - ], - }); - - // Test 2: Messages array where last message is small and kept intact - const smallContent = 'This is a small message that fits within the limit'; - await generateText({ - experimental_telemetry: { isEnabled: true }, - model: new MockLanguageModelV1({ - doGenerate: async () => ({ - rawCall: { rawPrompt: null, rawSettings: {} }, - finishReason: 'stop', - usage: { promptTokens: 10, completionTokens: 5 }, - text: 'Response to small message', - }), - }), - messages: [ - { role: 'user', content: largeContent1 }, - { role: 'assistant', content: largeContent2 }, - { role: 'user', content: smallContent }, - ], - }); - }); -} - -run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/scenario-no-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/vercelai/scenario-no-truncation.mjs deleted file mode 100644 index 415c13ef9acf..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/vercelai/scenario-no-truncation.mjs +++ /dev/null @@ -1,28 +0,0 @@ -import * as Sentry from '@sentry/node'; -import { generateText } from 'ai'; -import { MockLanguageModelV1 } from 'ai/test'; - -async function run() { - await Sentry.startSpan({ op: 'function', name: 'main' }, async () => { - // Multiple messages with long content (would normally be truncated and popped to last message only) - const longContent = 'A'.repeat(50_000); - await generateText({ - experimental_telemetry: { isEnabled: true }, - model: new MockLanguageModelV1({ - doGenerate: async () => ({ - rawCall: { rawPrompt: null, rawSettings: {} }, - finishReason: 'stop', - usage: { promptTokens: 10, completionTokens: 5 }, - text: 'Response', - }), - }), - messages: [ - { role: 'user', content: longContent }, - { role: 'assistant', content: 'Some reply' }, - { role: 'user', content: 'Follow-up question' }, - ], - }); - }); -} - -run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/span-streaming-v4/instrument-with-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/vercelai/span-streaming-v4/instrument-with-truncation.mjs deleted file mode 100644 index 3961dba5ba9f..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/vercelai/span-streaming-v4/instrument-with-truncation.mjs +++ /dev/null @@ -1,16 +0,0 @@ -import * as Sentry from '@sentry/node'; -import { loggingTransport } from '@sentry-internal/node-integration-tests'; - -Sentry.init({ - dsn: 'https://public@dsn.ingest.sentry.io/1337', - release: '1.0', - tracesSampleRate: 1.0, - dataCollection: { genAI: { inputs: true, outputs: true } }, - transport: loggingTransport, - traceLifecycle: 'stream', - integrations: [ - Sentry.vercelAIIntegration({ - enableTruncation: true, - }), - ], -}); diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/span-streaming-v4/scenario-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/vercelai/span-streaming-v4/scenario-truncation.mjs deleted file mode 100644 index ebe0becaad35..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/vercelai/span-streaming-v4/scenario-truncation.mjs +++ /dev/null @@ -1,27 +0,0 @@ -import * as Sentry from '@sentry/node'; -import { generateText } from 'ai'; -import { MockLanguageModelV1 } from 'ai/test'; - -async function run() { - await Sentry.startSpan({ op: 'function', name: 'main' }, async () => { - // Single long message so truncation must crop it - const longContent = 'A'.repeat(50_000); - await generateText({ - experimental_telemetry: { isEnabled: true }, - model: new MockLanguageModelV1({ - doGenerate: async () => ({ - rawCall: { rawPrompt: null, rawSettings: {} }, - finishReason: 'stop', - usage: { promptTokens: 10, completionTokens: 5 }, - text: 'Response', - }), - }), - messages: [{ role: 'user', content: longContent }], - }); - }); - - // Flush is required when span streaming is enabled to ensure streamed spans are sent before the process exits - await Sentry.flush(2000); -} - -run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/span-streaming-v4/test.ts b/dev-packages/node-integration-tests/suites/tracing/vercelai/span-streaming-v4/test.ts index f522f7766822..205a1a662ab2 100644 --- a/dev-packages/node-integration-tests/suites/tracing/vercelai/span-streaming-v4/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/span-streaming-v4/test.ts @@ -18,7 +18,7 @@ import { GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE, } from '../../../../../../packages/core/src/tracing/ai/gen-ai-attributes'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../../utils/runner'; -import { getStringAttributeValue, isOrchestrionEnabled } from '../../../../utils'; +import { isOrchestrionEnabled } from '../../../../utils'; /** * Helper to match a typed attribute value in a SerializedStreamedSpan. @@ -303,50 +303,4 @@ describe('Vercel AI integration (streaming v4)', () => { await createRunner().ignore('event').expect({ span: EXPECTED_SPANS_ERROR_IN_TOOL }).start().completed(); }); }); - - const streamingLongContent = 'A'.repeat(50_000); - - createEsmAndCjsTests(__dirname, 'scenario-truncation.mjs', 'instrument.mjs', (createRunner, test) => { - test('automatically disables truncation when span streaming is enabled', async () => { - await createRunner() - .expect({ - span: container => { - const spans = container.items; - - const chatSpan = spans.find(s => - getStringAttributeValue(s.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value)?.includes( - streamingLongContent, - ), - ); - expect(chatSpan).toBeDefined(); - }, - }) - .start() - .completed(); - }); - }); - - createEsmAndCjsTests(__dirname, 'scenario-truncation.mjs', 'instrument-with-truncation.mjs', (createRunner, test) => { - test('respects explicit enableTruncation: true even when span streaming is enabled', async () => { - await createRunner() - .expect({ - span: container => { - const spans = container.items; - - // With explicit enableTruncation: true, content should be truncated despite streaming. - const chatSpan = spans.find(s => - getStringAttributeValue(s.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value)?.startsWith( - '[{"role":"user","content":"AAAA', - ), - ); - expect(chatSpan).toBeDefined(); - expect( - (getStringAttributeValue(chatSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value) ?? '').length, - ).toBeLessThan(streamingLongContent.length); - }, - }) - .start() - .completed(); - }); - }); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/test.ts b/dev-packages/node-integration-tests/suites/tracing/vercelai/test.ts index 1c79760227c9..099ea20d8a5d 100644 --- a/dev-packages/node-integration-tests/suites/tracing/vercelai/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/test.ts @@ -478,53 +478,6 @@ describe('Vercel AI integration (v4)', () => { }, ); - createEsmAndCjsTests( - __dirname, - 'scenario-message-truncation.mjs', - 'instrument-with-truncation.mjs', - (createRunner, test) => { - test('truncates messages when they exceed byte limit', async () => { - await createRunner() - .ignore('event') - .expect({ transaction: { transaction: 'main' } }) - .expect({ - span: container => { - expect(container.items).toHaveLength(4); - const truncatedInvokeAgentSpan = container.items.find( - span => - span.name === 'invoke_agent' && - getStringAttributeValue(span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value)?.match( - /^\[.*"(?:text|content)":"C+".*\]$/, - ), - ); - expect(truncatedInvokeAgentSpan).toBeDefined(); - expect(truncatedInvokeAgentSpan!.name).toBe('invoke_agent'); - expect(truncatedInvokeAgentSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); - expect(truncatedInvokeAgentSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toMatch( - /^\[.*"(?:text|content)":"C+".*\]$/, - ); - - const smallMessageInvokeAgentSpan = container.items.find( - span => - span.name === 'invoke_agent' && - getStringAttributeValue(span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value)?.includes( - 'This is a small message that fits within the limit', - ), - ); - expect(smallMessageInvokeAgentSpan).toBeDefined(); - expect(smallMessageInvokeAgentSpan!.name).toBe('invoke_agent'); - expect(smallMessageInvokeAgentSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); - expect(smallMessageInvokeAgentSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toContain( - 'This is a small message that fits within the limit', - ); - }, - }) - .start() - .completed(); - }); - }, - ); - createEsmAndCjsTests(__dirname, 'scenario-embeddings.mjs', 'instrument.mjs', (createRunner, test) => { test('creates embedding related spans with genAI recording disabled', async () => { await createRunner() @@ -615,52 +568,6 @@ describe('Vercel AI integration (v4)', () => { }); }); - const longContent = 'A'.repeat(50_000); - - createEsmAndCjsTests( - __dirname, - 'scenario-no-truncation.mjs', - 'instrument-no-truncation.mjs', - (createRunner, test) => { - test('does not truncate input messages when enableTruncation is false', async () => { - await createRunner() - .expect({ transaction: { transaction: 'main' } }) - .expect({ - span: container => { - expect(container.items).toHaveLength(2); - const invokeAgentSpan = container.items.find( - span => - span.name === 'invoke_agent' && - span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value === - JSON.stringify([ - { role: 'user', content: longContent }, - { role: 'assistant', content: 'Some reply' }, - { role: 'user', content: 'Follow-up question' }, - ]), - ); - expect(invokeAgentSpan).toBeDefined(); - expect(invokeAgentSpan!.name).toBe('invoke_agent'); - expect(invokeAgentSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); - expect(invokeAgentSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toBe( - JSON.stringify([ - { role: 'user', content: longContent }, - { role: 'assistant', content: 'Some reply' }, - { role: 'user', content: 'Follow-up question' }, - ]), - ); - - const generateContentSpan = container.items.find(span => span.name === 'generate_content mock-model-id'); - expect(generateContentSpan).toBeDefined(); - expect(generateContentSpan!.name).toBe('generate_content mock-model-id'); - expect(generateContentSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); - }, - }) - .start() - .completed(); - }); - }, - ); - createEsmAndCjsTests(__dirname, 'scenario-stream-text.mjs', 'instrument.mjs', (createRunner, test) => { test('creates ai spans for streamText (doStream)', async () => { await createRunner() diff --git a/packages/cloudflare/src/integrations/tracing/vercelai.ts b/packages/cloudflare/src/integrations/tracing/vercelai.ts index 9d28d41d5bb0..4ed5704f8b20 100644 --- a/packages/cloudflare/src/integrations/tracing/vercelai.ts +++ b/packages/cloudflare/src/integrations/tracing/vercelai.ts @@ -14,12 +14,6 @@ import { addVercelAiProcessors, defineIntegration } from '@sentry/core'; const INTEGRATION_NAME = 'VercelAI'; interface VercelAiOptions { - /** - * Enable or disable truncation of recorded input messages. - * Defaults to `true`. - */ - enableTruncation?: boolean; - // `recordInputs`/`recordOutputs` are intentionally omitted: this entrypoint only post-processes // spans the AI SDK already emitted, so it cannot decide whether inputs/outputs are recorded. // Control this per call via `experimental_telemetry.recordInputs`/`recordOutputs`, or use the diff --git a/packages/core/src/server-exports.ts b/packages/core/src/server-exports.ts index 8288320bbed9..8af5242426b9 100644 --- a/packages/core/src/server-exports.ts +++ b/packages/core/src/server-exports.ts @@ -57,7 +57,7 @@ export type { // AI instrumentation is only supported in server runtimes, so these exports are kept out of the browser entry to // avoid shipping the AI tracing code in browser bundles. export { addVercelAiProcessors, getProviderMetadataAttributes } from './tracing/vercel-ai'; -export { getTruncatedJsonString, shouldEnableTruncation, resolveAIRecordingOptions } from './tracing/ai/utils'; +export { getGenAiMessagesJsonString, resolveAIRecordingOptions } from './tracing/ai/utils'; export { GEN_AI_REQUEST_MODEL_ATTRIBUTE, GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE } from './tracing/ai/gen-ai-attributes'; export { _INTERNAL_getSpanContextForToolCallId, _INTERNAL_cleanupToolCallSpanContext } from './tracing/vercel-ai/utils'; export { toolCallSpanContextMap as _INTERNAL_toolCallSpanContextMap } from './tracing/vercel-ai/constants'; diff --git a/packages/core/src/tracing/ai/mediaStripping.ts b/packages/core/src/tracing/ai/mediaStripping.ts index cb8e5d7b959e..7f0988bd5e94 100644 --- a/packages/core/src/tracing/ai/mediaStripping.ts +++ b/packages/core/src/tracing/ai/mediaStripping.ts @@ -195,3 +195,78 @@ export function stripInlineMediaFromSingleMessage(part: ContentMedia): ContentMe } return strip; } + +/** + * Message with the OpenAI/Anthropic `content: [...]` array format. + */ +type ContentArrayMessage = { + [key: string]: unknown; + content: unknown[]; +}; + +/** + * Message with the Google GenAI `parts: [...]` format. + */ +type PartsMessage = { + [key: string]: unknown; + parts: unknown[]; +}; + +/** + * Check if a message has the OpenAI/Anthropic content array format. + */ +function isContentArrayMessage(message: unknown): message is ContentArrayMessage { + return message !== null && typeof message === 'object' && 'content' in message && Array.isArray(message.content); +} + +/** + * Check if a message has the Google GenAI parts format. + */ +function isPartsMessage(message: unknown): message is PartsMessage { + return ( + message !== null && + typeof message === 'object' && + 'parts' in message && + Array.isArray((message as PartsMessage).parts) && + (message as PartsMessage).parts.length > 0 + ); +} + +/** + * Strip inline media from an array of messages, returning a new array. + * + * This does NOT mutate the input, because the actual API/client still needs the real media. + * Recurses into OpenAI/Anthropic `content: [...]` arrays and Google GenAI `parts: [...]`, replacing + * inline binary/base64 data with a placeholder while preserving all other structure. + */ +export function stripInlineMediaFromMessages(messages: unknown[]): unknown[] { + return messages.map(message => { + let newMessage: Record | undefined = undefined; + if (!!message && typeof message === 'object') { + if (isContentArrayMessage(message)) { + newMessage = { + ...message, + content: stripInlineMediaFromMessages(message.content), + }; + } else if ('content' in message && isContentMedia(message.content)) { + newMessage = { + ...message, + content: stripInlineMediaFromSingleMessage(message.content), + }; + } + if (isPartsMessage(message)) { + newMessage = { + // might have to strip content AND parts + ...(newMessage ?? message), + parts: stripInlineMediaFromMessages(message.parts), + }; + } + if (isContentMedia(newMessage)) { + newMessage = stripInlineMediaFromSingleMessage(newMessage); + } else if (isContentMedia(message)) { + newMessage = stripInlineMediaFromSingleMessage(message); + } + } + return newMessage ?? message; + }); +} diff --git a/packages/core/src/tracing/ai/messageTruncation.ts b/packages/core/src/tracing/ai/messageTruncation.ts deleted file mode 100644 index 779cf332855b..000000000000 --- a/packages/core/src/tracing/ai/messageTruncation.ts +++ /dev/null @@ -1,409 +0,0 @@ -import { isContentMedia, stripInlineMediaFromSingleMessage } from './mediaStripping'; - -/** - * Default maximum size in bytes for GenAI messages. - * Messages exceeding this limit will be truncated. - */ -export const DEFAULT_GEN_AI_MESSAGES_BYTE_LIMIT = 20000; - -/** - * Message format used by OpenAI and Anthropic APIs. - */ -type ContentMessage = { - [key: string]: unknown; - content: string; -}; - -/** - * One block inside OpenAI / Anthropic `content: [...]` arrays (text, image_url, etc.). - */ -type ContentArrayBlock = { - [key: string]: unknown; - type: string; -}; - -/** - * Message format used by OpenAI and Anthropic APIs for media. - */ -type ContentArrayMessage = { - [key: string]: unknown; - content: ContentArrayBlock[]; -}; - -/** - * Message format used by Google GenAI API. - * Parts can be strings or objects with a text property. - */ -type PartsMessage = { - [key: string]: unknown; - parts: Array; -}; - -/** - * A part in a Google GenAI message that contains text. - */ -type TextPart = string | { text: string }; - -/** - * A part in a Google GenAI that contains media. - */ -type MediaPart = { - type: string; - content: string; -}; - -/** - * One element of an array-based message: OpenAI/Anthropic `content[]` or Google `parts`. - */ -type ArrayMessageItem = TextPart | MediaPart | ContentArrayBlock; - -/** - * Calculate the UTF-8 byte length of a string. - */ -const utf8Bytes = (text: string): number => { - return new TextEncoder().encode(text).length; -}; - -/** - * Calculate the UTF-8 byte length of a value's JSON representation. - */ -const jsonBytes = (value: unknown): number => { - return utf8Bytes(JSON.stringify(value)); -}; - -/** - * Truncate a string to fit within maxBytes (inclusive) when encoded as UTF-8. - * Uses binary search for efficiency with multi-byte characters. - * - * @param text - The string to truncate - * @param maxBytes - Maximum byte length (inclusive, UTF-8 encoded) - * @returns Truncated string whose UTF-8 byte length is at most maxBytes - */ -function truncateTextByBytes(text: string, maxBytes: number): string { - if (utf8Bytes(text) <= maxBytes) { - return text; - } - - let low = 0; - let high = text.length; - let bestFit = ''; - - while (low <= high) { - const mid = Math.floor((low + high) / 2); - const candidate = text.slice(0, mid); - const byteSize = utf8Bytes(candidate); - - if (byteSize <= maxBytes) { - bestFit = candidate; - low = mid + 1; - } else { - high = mid - 1; - } - } - - return bestFit; -} - -/** - * Extract text content from a message item. - * Handles plain strings and objects with a text property. - * - * @returns The text content - */ -function getItemText(item: ArrayMessageItem): string { - if (typeof item === 'string') { - return item; - } - if ('text' in item && typeof item.text === 'string') { - return item.text; - } - return ''; -} - -/** - * Create a new item with updated text content while preserving the original structure. - * - * @param item - Original item (string or object) - * @param text - New text content - * @returns New item with updated text - */ -function withItemText(item: ArrayMessageItem, text: string): ArrayMessageItem { - if (typeof item === 'string') { - return text; - } - return { ...item, text }; -} - -/** - * Check if a message has the OpenAI/Anthropic content format. - */ -function isContentMessage(message: unknown): message is ContentMessage { - return ( - message !== null && - typeof message === 'object' && - 'content' in message && - typeof (message as ContentMessage).content === 'string' - ); -} - -/** - * Check if a message has the OpenAI/Anthropic content array format. - */ -function isContentArrayMessage(message: unknown): message is ContentArrayMessage { - return message !== null && typeof message === 'object' && 'content' in message && Array.isArray(message.content); -} - -/** - * Check if a message has the Google GenAI parts format. - */ -function isPartsMessage(message: unknown): message is PartsMessage { - return ( - message !== null && - typeof message === 'object' && - 'parts' in message && - Array.isArray((message as PartsMessage).parts) && - (message as PartsMessage).parts.length > 0 - ); -} - -/** - * Truncate a message with `content: string` format (OpenAI/Anthropic). - * - * @param message - Message with content property - * @param maxBytes - Maximum byte limit - * @returns Array with truncated message, or empty array if it doesn't fit - */ -function truncateContentMessage(message: ContentMessage, maxBytes: number): unknown[] { - // Calculate overhead (message structure without content) - const emptyMessage = { ...message, content: '' }; - const overhead = jsonBytes(emptyMessage); - const availableForContent = maxBytes - overhead; - - if (availableForContent <= 0) { - return []; - } - - const truncatedContent = truncateTextByBytes(message.content, availableForContent); - return [{ ...message, content: truncatedContent }]; -} - -/** - * Extracts the array items and their key from an array-based message. - * Returns `null` key if neither `parts` nor `content` is a valid array. - */ -function getArrayItems(message: PartsMessage | ContentArrayMessage): { - key: 'parts' | 'content' | null; - items: ArrayMessageItem[]; -} { - if ('parts' in message && Array.isArray(message.parts)) { - return { key: 'parts', items: message.parts }; - } - if ('content' in message && Array.isArray(message.content)) { - return { key: 'content', items: message.content }; - } - return { key: null, items: [] }; -} - -/** - * Truncate a message with an array-based format. - * Handles both `parts: [...]` (Google GenAI) and `content: [...]` (OpenAI/Anthropic multimodal). - * Keeps as many complete items as possible, only truncating the first item if needed. - * - * @param message - Message with parts or content array - * @param maxBytes - Maximum byte limit - * @returns Array with truncated message, or empty array if it doesn't fit - */ -function truncateArrayMessage(message: PartsMessage | ContentArrayMessage, maxBytes: number): unknown[] { - const { key, items } = getArrayItems(message); - - if (key === null || items.length === 0) { - return []; - } - - // Calculate overhead by creating empty text items - const emptyItems = items.map(item => withItemText(item, '')); - const overhead = jsonBytes({ ...message, [key]: emptyItems }); - let remainingBytes = maxBytes - overhead; - - if (remainingBytes <= 0) { - return []; - } - - // Include items until we run out of space - const includedItems: ArrayMessageItem[] = []; - - for (const item of items) { - const text = getItemText(item); - const textSize = utf8Bytes(text); - - if (textSize <= remainingBytes) { - // Item fits: include it as-is - includedItems.push(item); - remainingBytes -= textSize; - } else if (includedItems.length === 0) { - // First item doesn't fit: truncate it - const truncated = truncateTextByBytes(text, remainingBytes); - if (truncated) { - includedItems.push(withItemText(item, truncated)); - } - break; - } else { - // Subsequent item doesn't fit: stop here - break; - } - } - - /* c8 ignore start - * for type safety only, algorithm guarantees SOME text included */ - if (includedItems.length <= 0) { - return []; - } else { - /* c8 ignore stop */ - return [{ ...message, [key]: includedItems }]; - } -} - -/** - * Truncate a single message to fit within maxBytes. - * - * Supports three message formats: - * - OpenAI/Anthropic: `{ ..., content: string }` - * - Vercel AI/OpenAI multimodal: `{ ..., content: Array<{type, text?, ...}> }` - * - Google GenAI: `{ ..., parts: Array }` - * - * @param message - The message to truncate - * @param maxBytes - Maximum byte limit for the message - * @returns Array containing the truncated message, or empty array if truncation fails - */ -function truncateSingleMessage(message: unknown, maxBytes: number): unknown[] { - if (!message) return []; - - // Handle plain strings (e.g., embeddings input) - if (typeof message === 'string') { - const truncated = truncateTextByBytes(message, maxBytes); - return truncated ? [truncated] : []; - } - - if (typeof message !== 'object') { - return []; - } - - if (isContentMessage(message)) { - return truncateContentMessage(message, maxBytes); - } - - if (isContentArrayMessage(message) || isPartsMessage(message)) { - return truncateArrayMessage(message, maxBytes); - } - - // Unknown message format: cannot truncate safely - return []; -} - -/** - * Strip the inline media from message arrays. - * - * This returns a stripped message. We do NOT want to mutate the data in place, - * because of course we still want the actual API/client to handle the media. - */ -function stripInlineMediaFromMessages(messages: unknown[]): unknown[] { - const stripped = messages.map(message => { - let newMessage: Record | undefined = undefined; - if (!!message && typeof message === 'object') { - if (isContentArrayMessage(message)) { - newMessage = { - ...message, - content: stripInlineMediaFromMessages(message.content), - }; - } else if ('content' in message && isContentMedia(message.content)) { - newMessage = { - ...message, - content: stripInlineMediaFromSingleMessage(message.content), - }; - } - if (isPartsMessage(message)) { - newMessage = { - // might have to strip content AND parts - ...(newMessage ?? message), - parts: stripInlineMediaFromMessages(message.parts), - }; - } - if (isContentMedia(newMessage)) { - newMessage = stripInlineMediaFromSingleMessage(newMessage); - } else if (isContentMedia(message)) { - newMessage = stripInlineMediaFromSingleMessage(message); - } - } - return newMessage ?? message; - }); - return stripped; -} - -/** - * Truncate an array of messages to fit within a byte limit. - * - * Strategy: - * - Always keeps only the last (newest) message - * - Strips inline media from the message - * - Truncates the message content if it exceeds the byte limit - * - * @param messages - Array of messages to truncate - * @param maxBytes - Maximum total byte limit for the message - * @returns Array containing only the last message (possibly truncated) - * - * @example - * ```ts - * const messages = [msg1, msg2, msg3, msg4]; // newest is msg4 - * const truncated = truncateMessagesByBytes(messages, 10000); - * // Returns [msg4] (truncated if needed) - * ``` - */ -function truncateMessagesByBytes(messages: unknown[], maxBytes: number): unknown[] { - // Early return for empty or invalid input - if (!Array.isArray(messages) || messages.length === 0) { - return messages; - } - - // The result is always a single-element array that callers wrap with - // JSON.stringify([message]), so subtract the 2-byte array wrapper ("[" and "]") - // to ensure the final serialized value stays under the limit. - const effectiveMaxBytes = maxBytes - 2; - - // Always keep only the last message - const lastMessage = messages[messages.length - 1]; - - // Strip inline media from the single message - const stripped = stripInlineMediaFromMessages([lastMessage]); - const strippedMessage = stripped[0]; - - // Check if it fits - const messageBytes = jsonBytes(strippedMessage); - if (messageBytes <= effectiveMaxBytes) { - return stripped; - } - - // Truncate the single message if needed - return truncateSingleMessage(strippedMessage, effectiveMaxBytes); -} - -/** - * Truncate GenAI messages using the default byte limit. - * - * Convenience wrapper around `truncateMessagesByBytes` with the default limit. - * - * @param messages - Array of messages to truncate - * @returns Truncated array of messages - */ -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/tracing/ai/utils.ts b/packages/core/src/tracing/ai/utils.ts index ff9e072bf271..2e35203f5662 100644 --- a/packages/core/src/tracing/ai/utils.ts +++ b/packages/core/src/tracing/ai/utils.ts @@ -16,7 +16,7 @@ import { GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE, GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE, } from './gen-ai-attributes'; -import { truncateGenAiMessages, truncateGenAiStringInput } from './messageTruncation'; +import { stripInlineMediaFromMessages } from './mediaStripping'; export interface AIRecordingOptions { recordInputs?: boolean; @@ -56,22 +56,6 @@ export function resolveAIRecordingOptions(options? } as T & Required; } -/** - * Resolves whether truncation should be enabled. - * If the user explicitly set `enableTruncation`, that value is used. - * Otherwise, truncation is disabled because gen_ai spans are always sent through the v2 span path - * (full span streaming via `traceLifecycle: 'stream'`, or extraction into a v2 span envelope for - * static transactions). That path is not subject to the transaction payload-size limits that - * truncation works around, so the full message data can be retained. - */ -export function shouldEnableTruncation(enableTruncation: boolean | undefined): boolean { - if (enableTruncation !== undefined) { - return enableTruncation; - } - - return !getClient(); -} - /** * Build method path from current traversal */ @@ -186,20 +170,22 @@ export function endStreamSpan(span: Span, state: StreamResponseState, recordOutp } /** - * Get the truncated JSON string for a string, an array of messages, or an object. + * Serialize a string, an array of messages, or an object to a JSON string for span attributes, + * stripping inline media (base64 blobs, data URIs, etc.) from message arrays so large binary + * payloads never end up in span attributes. * - * @param value - The value to truncate and serialize - * @returns The truncated JSON string + * @param value - The value to serialize + * @returns The JSON string */ -export function getTruncatedJsonString(value: T | T[]): string { +export function getGenAiMessagesJsonString(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 truncateGenAiStringInput(value); + return value; } - // Both truncation (media stripping recurses the value) and `JSON.stringify` can throw on - // circular refs or non-serializable values (e.g. BigInt); never let that crash instrumentation. + // Media stripping recurses the value and `JSON.stringify` can throw on circular refs or + // non-serializable values (e.g. BigInt); never let that crash instrumentation. try { - return JSON.stringify(Array.isArray(value) ? truncateGenAiMessages(value) : value); + return JSON.stringify(Array.isArray(value) ? stripInlineMediaFromMessages(value) : value); } catch { return '[unserializable]'; } diff --git a/packages/core/src/tracing/anthropic-ai/index.ts b/packages/core/src/tracing/anthropic-ai/index.ts index 64cd105905bc..af398887f6d9 100644 --- a/packages/core/src/tracing/anthropic-ai/index.ts +++ b/packages/core/src/tracing/anthropic-ai/index.ts @@ -21,12 +21,7 @@ import { GEN_AI_SYSTEM_ATTRIBUTE, } from '../ai/gen-ai-attributes'; import type { InstrumentedMethodEntry } from '../ai/utils'; -import { - resolveAIRecordingOptions, - setTokenUsageAttributes, - shouldEnableTruncation, - wrapPromiseWithMethods, -} from '../ai/utils'; +import { resolveAIRecordingOptions, setTokenUsageAttributes, wrapPromiseWithMethods } from '../ai/utils'; import { ANTHROPIC_METHOD_REGISTRY } from './constants'; import { instrumentAsyncIterableStream, instrumentMessageStream } from './streaming'; import type { AnthropicAiOptions, AnthropicAiResponse, AnthropicAiStreamingEvent, ContentBlock } from './types'; @@ -85,13 +80,9 @@ export function extractRequestAttributes( * Add private request attributes to spans. * This is only recorded if recordInputs is true. */ -export function addPrivateRequestAttributes( - span: Span, - params: Record, - enableTruncation: boolean, -): void { +export function addPrivateRequestAttributes(span: Span, params: Record): void { const messages = messagesFromParams(params); - setMessagesAttribute(span, messages, enableTruncation); + setMessagesAttribute(span, messages); if ('prompt' in params) { span.setAttributes({ [GEN_AI_PROMPT_ATTRIBUTE]: JSON.stringify(params.prompt) }); @@ -222,7 +213,7 @@ function handleStreamingRequest( originalResult = target.apply(invocationThis, args) as Promise; if (options.recordInputs && params) { - addPrivateRequestAttributes(span, params, shouldEnableTruncation(options.enableTruncation)); + addPrivateRequestAttributes(span, params); } return (async () => { @@ -244,7 +235,7 @@ function handleStreamingRequest( return startSpanManual(spanConfig, span => { try { if (options.recordInputs && params) { - addPrivateRequestAttributes(span, params, shouldEnableTruncation(options.enableTruncation)); + addPrivateRequestAttributes(span, params); } // The helper synchronously delegates to `create`; suppress that one internal call so it // does not produce a duplicate child span (see the dedup gate in `instrumentMethod`). @@ -323,7 +314,7 @@ function instrumentMethod( originalResult = target.apply(invocationThis, args) as Promise; if (options.recordInputs && params) { - addPrivateRequestAttributes(span, params, shouldEnableTruncation(options.enableTruncation)); + addPrivateRequestAttributes(span, params); } return originalResult.then( diff --git a/packages/core/src/tracing/anthropic-ai/types.ts b/packages/core/src/tracing/anthropic-ai/types.ts index dd61ff63e264..ba281ef82a0d 100644 --- a/packages/core/src/tracing/anthropic-ai/types.ts +++ b/packages/core/src/tracing/anthropic-ai/types.ts @@ -9,11 +9,6 @@ export interface AnthropicAiOptions { * Enable or disable output recording. */ recordOutputs?: boolean; - /** - * Enable or disable truncation of recorded input messages. - * Defaults to `true`. - */ - enableTruncation?: boolean; } export type Message = { diff --git a/packages/core/src/tracing/anthropic-ai/utils.ts b/packages/core/src/tracing/anthropic-ai/utils.ts index 5989247c111c..541af57b8743 100644 --- a/packages/core/src/tracing/anthropic-ai/utils.ts +++ b/packages/core/src/tracing/anthropic-ai/utils.ts @@ -3,14 +3,13 @@ import { SPAN_STATUS_ERROR } from '../../tracing'; import type { Span } from '../../types/span'; import type { SpanStatusType } from '../../types/spanStatus'; import { GEN_AI_INPUT_MESSAGES_ATTRIBUTE, GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE } from '../ai/gen-ai-attributes'; -import { extractSystemInstructions, getTruncatedJsonString } from '../ai/utils'; -import { stringify } from '../../utils/string'; +import { extractSystemInstructions, getGenAiMessagesJsonString } from '../ai/utils'; import type { AnthropicAiResponse } from './types'; /** - * Set the input messages attribute, extracting system instructions before truncation. + * Set the messages attribute, extracting system instructions first. */ -export function setMessagesAttribute(span: Span, messages: unknown, enableTruncation: boolean): void { +export function setMessagesAttribute(span: Span, messages: unknown): void { if (Array.isArray(messages) && messages.length === 0) { return; } @@ -24,9 +23,7 @@ export function setMessagesAttribute(span: Span, messages: unknown, enableTrunca } span.setAttributes({ - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: enableTruncation - ? getTruncatedJsonString(filteredMessages) - : stringify(filteredMessages), + [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: getGenAiMessagesJsonString(filteredMessages), }); } diff --git a/packages/core/src/tracing/google-genai/index.ts b/packages/core/src/tracing/google-genai/index.ts index 1909a0903764..f46ed48ccb57 100644 --- a/packages/core/src/tracing/google-genai/index.ts +++ b/packages/core/src/tracing/google-genai/index.ts @@ -27,13 +27,11 @@ import { GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE, } from '../ai/gen-ai-attributes'; import type { InstrumentedMethodEntry } from '../ai/utils'; -import { stringify } from '../../utils/string'; import { buildMethodPath, extractSystemInstructions, - getTruncatedJsonString, + getGenAiMessagesJsonString, resolveAIRecordingOptions, - shouldEnableTruncation, } from '../ai/utils'; import { GOOGLE_GENAI_METHOD_REGISTRY, GOOGLE_GENAI_SYSTEM_NAME } from './constants'; import { instrumentStream } from './streaming'; @@ -139,12 +137,7 @@ export function extractRequestAttributes( * This is only recorded if recordInputs is true. * Handles different parameter formats for different Google GenAI methods. */ -export function addPrivateRequestAttributes( - span: Span, - params: Record, - operationName: string, - enableTruncation: boolean, -): void { +export function addPrivateRequestAttributes(span: Span, params: Record, operationName: string): void { if (operationName === 'embeddings') { const contents = params.contents; if (contents != null) { @@ -192,9 +185,7 @@ export function addPrivateRequestAttributes( } span.setAttributes({ - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: enableTruncation - ? getTruncatedJsonString(filteredMessages) - : stringify(filteredMessages), + [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: getGenAiMessagesJsonString(filteredMessages), }); } } @@ -295,12 +286,7 @@ function instrumentMethod( async (span: Span) => { try { if (options.recordInputs && params) { - addPrivateRequestAttributes( - span, - params, - operationName, - shouldEnableTruncation(options.enableTruncation), - ); + addPrivateRequestAttributes(span, params, operationName); } const stream = await target.apply(context, args); return instrumentStream(stream, span, Boolean(options.recordOutputs)) as R; @@ -328,7 +314,7 @@ function instrumentMethod( }, (span: Span) => { if (options.recordInputs && params) { - addPrivateRequestAttributes(span, params, operationName, shouldEnableTruncation(options.enableTruncation)); + addPrivateRequestAttributes(span, params, operationName); } return handleCallbackErrors( diff --git a/packages/core/src/tracing/google-genai/types.ts b/packages/core/src/tracing/google-genai/types.ts index 35ca728a4a60..d1729dd68f36 100644 --- a/packages/core/src/tracing/google-genai/types.ts +++ b/packages/core/src/tracing/google-genai/types.ts @@ -9,11 +9,6 @@ export interface GoogleGenAIOptions { * Enable or disable output recording. */ recordOutputs?: boolean; - /** - * Enable or disable truncation of recorded input messages. - * Defaults to `true`. - */ - enableTruncation?: boolean; } /** diff --git a/packages/core/src/tracing/langchain/index.ts b/packages/core/src/tracing/langchain/index.ts index 621ae76acdd5..9397d1f60d34 100644 --- a/packages/core/src/tracing/langchain/index.ts +++ b/packages/core/src/tracing/langchain/index.ts @@ -12,7 +12,7 @@ import { GEN_AI_TOOL_NAME_ATTRIBUTE, GEN_AI_TOOL_OUTPUT_ATTRIBUTE, } from '../ai/gen-ai-attributes'; -import { resolveAIRecordingOptions, shouldEnableTruncation } from '../ai/utils'; +import { resolveAIRecordingOptions } from '../ai/utils'; import { LANGCHAIN_ORIGIN } from './constants'; import type { LangChainCallbackHandler, @@ -38,7 +38,6 @@ import { */ export function createLangChainCallbackHandler(options: LangChainOptions = {}): LangChainCallbackHandler { const { recordInputs, recordOutputs } = resolveAIRecordingOptions(options); - const enableTruncation = shouldEnableTruncation(options.enableTruncation); // Internal state - single instance tracks all spans const spanMap = new Map(); @@ -94,7 +93,6 @@ export function createLangChainCallbackHandler(options: LangChainOptions = {}): llm as LangChainSerialized, prompts, recordInputs, - enableTruncation, invocationParams, metadata, ); @@ -134,7 +132,6 @@ export function createLangChainCallbackHandler(options: LangChainOptions = {}): llm as LangChainSerialized, messages as LangChainMessage[][], recordInputs, - enableTruncation, invocationParams, metadata, ); diff --git a/packages/core/src/tracing/langchain/types.ts b/packages/core/src/tracing/langchain/types.ts index aae03680c0d1..c624fae7f918 100644 --- a/packages/core/src/tracing/langchain/types.ts +++ b/packages/core/src/tracing/langchain/types.ts @@ -13,12 +13,6 @@ export interface LangChainOptions { * @default false (respects `dataCollection.genAI.outputs`, or `sendDefaultPii` when `dataCollection` is not configured) */ recordOutputs?: boolean; - - /** - * Enable or disable truncation of recorded input messages. - * Defaults to `true`. - */ - enableTruncation?: boolean; } /** diff --git a/packages/core/src/tracing/langchain/utils.ts b/packages/core/src/tracing/langchain/utils.ts index db5fffa0e000..62c770d8511f 100644 --- a/packages/core/src/tracing/langchain/utils.ts +++ b/packages/core/src/tracing/langchain/utils.ts @@ -27,7 +27,7 @@ import { GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE, } from '../ai/gen-ai-attributes'; import { isContentMedia, stripInlineMediaFromSingleMessage } from '../ai/mediaStripping'; -import { extractSystemInstructions, getTruncatedJsonString } from '../ai/utils'; +import { extractSystemInstructions } from '../ai/utils'; import { LANGCHAIN_ORIGIN, ROLE_MAP } from './constants'; import type { LangChainLLMResult, LangChainMessage, LangChainSerialized } from './types'; @@ -277,7 +277,6 @@ export function extractLLMRequestAttributes( llm: LangChainSerialized, prompts: string[], recordInputs: boolean, - enableTruncation: boolean, invocationParams?: Record, langSmithMetadata?: Record, ): Record { @@ -288,11 +287,7 @@ export function extractLLMRequestAttributes( if (recordInputs && Array.isArray(prompts) && prompts.length > 0) { const messages = prompts.map(p => ({ role: 'user', content: p })); - setIfDefined( - attrs, - GEN_AI_INPUT_MESSAGES_ATTRIBUTE, - enableTruncation ? getTruncatedJsonString(messages) : stringify(messages), - ); + setIfDefined(attrs, GEN_AI_INPUT_MESSAGES_ATTRIBUTE, stringify(messages)); } return attrs; @@ -311,7 +306,6 @@ export function extractChatModelRequestAttributes( llm: LangChainSerialized, langChainMessages: LangChainMessage[][], recordInputs: boolean, - enableTruncation: boolean, invocationParams?: Record, langSmithMetadata?: Record, ): Record { @@ -329,11 +323,7 @@ export function extractChatModelRequestAttributes( setIfDefined(attrs, GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE, systemInstructions); } - setIfDefined( - attrs, - GEN_AI_INPUT_MESSAGES_ATTRIBUTE, - enableTruncation ? getTruncatedJsonString(filteredMessages) : stringify(filteredMessages), - ); + setIfDefined(attrs, GEN_AI_INPUT_MESSAGES_ATTRIBUTE, stringify(filteredMessages)); } return attrs; diff --git a/packages/core/src/tracing/langgraph/index.ts b/packages/core/src/tracing/langgraph/index.ts index ea6653e28bd1..a68d40d545b0 100644 --- a/packages/core/src/tracing/langgraph/index.ts +++ b/packages/core/src/tracing/langgraph/index.ts @@ -12,12 +12,7 @@ import { GEN_AI_REQUEST_MODEL_ATTRIBUTE, GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE, } from '../ai/gen-ai-attributes'; -import { - extractSystemInstructions, - getTruncatedJsonString, - resolveAIRecordingOptions, - shouldEnableTruncation, -} from '../ai/utils'; +import { extractSystemInstructions, resolveAIRecordingOptions } from '../ai/utils'; import { stringify } from '../../utils/string'; import type { SpanAttributeValue } from '../../types/span'; import { createLangChainCallbackHandler } from '../langchain'; @@ -222,11 +217,8 @@ export function instrumentCompiledGraphInvoke( span.setAttribute(GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE, systemInstructions); } - const enableTruncation = shouldEnableTruncation(options.enableTruncation); span.setAttributes({ - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: enableTruncation - ? getTruncatedJsonString(filteredMessages) - : stringify(filteredMessages), + [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: stringify(filteredMessages), }); } diff --git a/packages/core/src/tracing/langgraph/types.ts b/packages/core/src/tracing/langgraph/types.ts index 021099f369b1..b16f9718c69e 100644 --- a/packages/core/src/tracing/langgraph/types.ts +++ b/packages/core/src/tracing/langgraph/types.ts @@ -7,11 +7,6 @@ export interface LangGraphOptions { * Enable or disable output recording. */ recordOutputs?: boolean; - /** - * Enable or disable truncation of recorded input messages. - * Defaults to `true`. - */ - enableTruncation?: boolean; } /** diff --git a/packages/core/src/tracing/openai/index.ts b/packages/core/src/tracing/openai/index.ts index c15b10856f8e..c5888a9e13bb 100644 --- a/packages/core/src/tracing/openai/index.ts +++ b/packages/core/src/tracing/openai/index.ts @@ -15,13 +15,11 @@ import { GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE, } from '../ai/gen-ai-attributes'; import type { InstrumentedMethodEntry } from '../ai/utils'; -import { stringify } from '../../utils/string'; import { buildMethodPath, extractSystemInstructions, - getTruncatedJsonString, + getGenAiMessagesJsonString, resolveAIRecordingOptions, - shouldEnableTruncation, wrapPromiseWithMethods, } from '../ai/utils'; import { OPENAI_METHOD_REGISTRY } from './constants'; @@ -79,12 +77,7 @@ export function extractRequestAttributes(args: unknown[], operationName: string) } // Extract and record AI request inputs, if present. This is intentionally separate from response attributes. -export function addRequestAttributes( - span: Span, - params: Record, - operationName: string, - enableTruncation: boolean, -): void { +export function addRequestAttributes(span: Span, params: Record, operationName: string): void { // Store embeddings input on a separate attribute and do not truncate it if (operationName === 'embeddings' && 'input' in params) { const input = params.input; @@ -125,10 +118,7 @@ export function addRequestAttributes( span.setAttribute(GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE, systemInstructions); } - span.setAttribute( - GEN_AI_INPUT_MESSAGES_ATTRIBUTE, - enableTruncation ? getTruncatedJsonString(filteredMessages) : stringify(filteredMessages), - ); + span.setAttribute(GEN_AI_INPUT_MESSAGES_ATTRIBUTE, getGenAiMessagesJsonString(filteredMessages)); } /** @@ -164,7 +154,7 @@ function instrumentMethod( originalResult = originalMethod.apply(context, args); if (options.recordInputs && params) { - addRequestAttributes(span, params, operationName, shouldEnableTruncation(options.enableTruncation)); + addRequestAttributes(span, params, operationName); } // Return async processing @@ -202,7 +192,7 @@ function instrumentMethod( originalResult = originalMethod.apply(context, args); if (options.recordInputs && params) { - addRequestAttributes(span, params, operationName, shouldEnableTruncation(options.enableTruncation)); + addRequestAttributes(span, params, operationName); } return originalResult.then( diff --git a/packages/core/src/tracing/openai/types.ts b/packages/core/src/tracing/openai/types.ts index 794c7ca49f8a..dd6872bb691b 100644 --- a/packages/core/src/tracing/openai/types.ts +++ b/packages/core/src/tracing/openai/types.ts @@ -22,11 +22,6 @@ export interface OpenAiOptions { * Enable or disable output recording. */ recordOutputs?: boolean; - /** - * Enable or disable truncation of recorded input messages. - * Defaults to `true`. - */ - enableTruncation?: boolean; } export interface OpenAiClient { diff --git a/packages/core/src/tracing/vercel-ai/index.ts b/packages/core/src/tracing/vercel-ai/index.ts index c905ac980614..67292c412c33 100644 --- a/packages/core/src/tracing/vercel-ai/index.ts +++ b/packages/core/src/tracing/vercel-ai/index.ts @@ -2,7 +2,6 @@ import type { Client } from '../../client'; import { getClient } from '../../currentScopes'; import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../../semanticAttributes'; -import { shouldEnableTruncation } from '../ai/utils'; import type { Event } from '../../types/event'; import type { Span, SpanAttributes, SpanAttributeValue, SpanJSON, StreamedSpanJSON } from '../../types/span'; import { spanToJSON } from '../../utils/spanUtils'; @@ -86,13 +85,7 @@ function onVercelAiSpanStart(span: Span): void { return; } - const client = getClient(); - const integration = client?.getIntegrationByName('VercelAI') as - | { options?: { enableTruncation?: boolean } } - | undefined; - const enableTruncation = shouldEnableTruncation(integration?.options?.enableTruncation); - - processGenerateSpan(span, name, attributes, enableTruncation); + processGenerateSpan(span, name, attributes); } function vercelAiEventProcessor(event: Event): Event { @@ -426,7 +419,7 @@ function processToolCallSpan(span: Span, attributes: SpanAttributes): void { } } -function processGenerateSpan(span: Span, name: string, attributes: SpanAttributes, enableTruncation: boolean): void { +function processGenerateSpan(span: Span, name: string, attributes: SpanAttributes): void { span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto.vercelai.otel'); const nameWthoutAi = name.replace('ai.', ''); @@ -438,7 +431,7 @@ function processGenerateSpan(span: Span, name: string, attributes: SpanAttribute span.setAttribute('gen_ai.function_id', functionId); } - requestMessagesFromPrompt(span, attributes, enableTruncation); + requestMessagesFromPrompt(span, attributes); if (attributes[AI_MODEL_ID_ATTRIBUTE] && !attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]) { span.setAttribute(GEN_AI_RESPONSE_MODEL_ATTRIBUTE, attributes[AI_MODEL_ID_ATTRIBUTE]); diff --git a/packages/core/src/tracing/vercel-ai/utils.ts b/packages/core/src/tracing/vercel-ai/utils.ts index d5c01f6302a8..91f111ad2d1d 100644 --- a/packages/core/src/tracing/vercel-ai/utils.ts +++ b/packages/core/src/tracing/vercel-ai/utils.ts @@ -9,8 +9,7 @@ import { GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE, GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE, } from '../ai/gen-ai-attributes'; -import { extractSystemInstructions, getTruncatedJsonString } from '../ai/utils'; -import { stringify } from '../../utils/string'; +import { extractSystemInstructions, getGenAiMessagesJsonString } from '../ai/utils'; import { toolCallSpanContextMap } from './constants'; import type { TokenSummary, ToolCallSpanContext } from './types'; import { AI_PROMPT_ATTRIBUTE, AI_PROMPT_MESSAGES_ATTRIBUTE } from './vercel-ai-attributes'; @@ -221,7 +220,7 @@ export function convertUserInputToMessagesFormat(userInput: string): { role: str * Generate a request.messages JSON array from the prompt field in the * invoke_agent op */ -export function requestMessagesFromPrompt(span: Span, attributes: SpanAttributes, enableTruncation: boolean): void { +export function requestMessagesFromPrompt(span: Span, attributes: SpanAttributes): void { if ( typeof attributes[AI_PROMPT_ATTRIBUTE] === 'string' && !attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE] && @@ -240,7 +239,7 @@ export function requestMessagesFromPrompt(span: Span, attributes: SpanAttributes span.setAttribute(GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE, systemInstructions); } - const messagesJson = enableTruncation ? getTruncatedJsonString(filteredMessages) : stringify(filteredMessages); + const messagesJson = getGenAiMessagesJsonString(filteredMessages); span.setAttributes({ [AI_PROMPT_ATTRIBUTE]: messagesJson, @@ -260,16 +259,7 @@ export function requestMessagesFromPrompt(span: Span, attributes: SpanAttributes span.setAttribute(GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE, systemInstructions); } - // `extractSystemInstructions` returns the original array reference unchanged when no - // system message is extracted. When truncation is also disabled, re-serializing would - // reproduce the SDK's own input string, so we reuse it instead of allocating a second - // full-size copy of the payload (matters for large prompts in memory-constrained runtimes). - const messagesJson = - !enableTruncation && filteredMessages === messages - ? originalMessagesJson - : enableTruncation - ? getTruncatedJsonString(filteredMessages) - : stringify(filteredMessages); + const messagesJson = getGenAiMessagesJsonString(filteredMessages); span.setAttributes({ [AI_PROMPT_MESSAGES_ATTRIBUTE]: messagesJson, diff --git a/packages/core/src/tracing/workers-ai/index.ts b/packages/core/src/tracing/workers-ai/index.ts index c6be4824305a..8463d524c191 100644 --- a/packages/core/src/tracing/workers-ai/index.ts +++ b/packages/core/src/tracing/workers-ai/index.ts @@ -2,7 +2,7 @@ import { SPAN_STATUS_ERROR } from '../../tracing'; import { startSpan, startSpanManual } from '../../tracing/trace'; import type { Span } from '../../types/span'; import { isObjectLike } from '../../utils/is'; -import { resolveAIRecordingOptions, shouldEnableTruncation } from '../ai/utils'; +import { resolveAIRecordingOptions } from '../ai/utils'; import { instrumentWorkersAiStream } from './streaming'; import type { WorkersAiOptions } from './types'; import { addRequestAttributes, addResponseAttributes, extractRequestAttributes, getOperationName } from './utils'; @@ -65,7 +65,7 @@ function instrumentRun( } if (options.recordInputs) { - addRequestAttributes(span, inputs, operationName, shouldEnableTruncation(options.enableTruncation)); + addRequestAttributes(span, inputs, operationName); } return originalResult.then(result => { @@ -85,7 +85,7 @@ function instrumentRun( const originalResult = originalRun.apply(context, args) as Promise; if (options.recordInputs) { - addRequestAttributes(span, inputs, operationName, shouldEnableTruncation(options.enableTruncation)); + addRequestAttributes(span, inputs, operationName); } return originalResult.then(result => { diff --git a/packages/core/src/tracing/workers-ai/types.ts b/packages/core/src/tracing/workers-ai/types.ts index 33334a6ed1bf..5f4781b12e9c 100644 --- a/packages/core/src/tracing/workers-ai/types.ts +++ b/packages/core/src/tracing/workers-ai/types.ts @@ -1,12 +1,6 @@ import type { AIRecordingOptions } from '../ai/utils'; -export interface WorkersAiOptions extends AIRecordingOptions { - /** - * Enable or disable truncation of recorded input messages. - * Defaults to `true`. - */ - enableTruncation?: boolean; -} +export interface WorkersAiOptions extends AIRecordingOptions {} /** * Minimal shape of the Cloudflare Workers AI binding (`env.AI`). diff --git a/packages/core/src/tracing/workers-ai/utils.ts b/packages/core/src/tracing/workers-ai/utils.ts index 42ad16328982..13c7d803fdeb 100644 --- a/packages/core/src/tracing/workers-ai/utils.ts +++ b/packages/core/src/tracing/workers-ai/utils.ts @@ -20,8 +20,7 @@ import { GEN_AI_RESPONSE_TEXT_ATTRIBUTE, GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE, } from '../ai/gen-ai-attributes'; -import { extractSystemInstructions, getTruncatedJsonString, setTokenUsageAttributes } from '../ai/utils'; -import { stringify } from '../../utils/string'; +import { extractSystemInstructions, getGenAiMessagesJsonString, setTokenUsageAttributes } from '../ai/utils'; import { WORKERS_AI_ORIGIN, WORKERS_AI_PROVIDER_NAME } from './constants'; import type { WorkersAiInput, WorkersAiOutput } from './types'; @@ -89,12 +88,7 @@ export function extractRequestAttributes( * Record the request inputs (messages/prompt/embeddings input) on the span. * Only called when `recordInputs` is enabled. */ -export function addRequestAttributes( - span: Span, - inputs: unknown, - operationName: string, - enableTruncation: boolean, -): void { +export function addRequestAttributes(span: Span, inputs: unknown, operationName: string): void { if (!inputs || typeof inputs !== 'object') { return; } @@ -124,10 +118,7 @@ export function addRequestAttributes( span.setAttribute(GEN_AI_SYSTEM_INSTRUCTIONS, systemInstructions); } - span.setAttribute( - GEN_AI_INPUT_MESSAGES, - enableTruncation ? getTruncatedJsonString(filteredMessages) : stringify(filteredMessages), - ); + span.setAttribute(GEN_AI_INPUT_MESSAGES, getGenAiMessagesJsonString(filteredMessages)); } /** diff --git a/packages/core/test/lib/tracing/ai-message-truncation.test.ts b/packages/core/test/lib/tracing/ai-media-stripping.test.ts similarity index 56% rename from packages/core/test/lib/tracing/ai-message-truncation.test.ts rename to packages/core/test/lib/tracing/ai-media-stripping.test.ts index 951dc9be6a15..cc2f0b868529 100644 --- a/packages/core/test/lib/tracing/ai-message-truncation.test.ts +++ b/packages/core/test/lib/tracing/ai-media-stripping.test.ts @@ -1,18 +1,15 @@ import { describe, expect, it } from 'vitest'; -import { truncateGenAiMessages, truncateGenAiStringInput } from '../../../src/tracing/ai/messageTruncation'; -import { getTruncatedJsonString } from '../../../src/tracing/ai/utils'; +import { stripInlineMediaFromMessages } from '../../../src/tracing/ai/mediaStripping'; +import { getGenAiMessagesJsonString } from '../../../src/tracing/ai/utils'; -describe('message truncation utilities', () => { - describe('truncateGenAiMessages', () => { +describe('media stripping utilities', () => { + describe('stripInlineMediaFromMessages', () => { it('leaves empty/non-array/small messages alone', () => { - // @ts-expect-error - exercising invalid type code path - expect(truncateGenAiMessages(null)).toBe(null); - expect(truncateGenAiMessages([])).toStrictEqual([]); - expect(truncateGenAiMessages([{ text: 'hello' }])).toStrictEqual([{ text: 'hello' }]); - expect(truncateGenAiStringInput('hello')).toBe('hello'); + expect(stripInlineMediaFromMessages([])).toStrictEqual([]); + expect(stripInlineMediaFromMessages([{ text: 'hello' }])).toStrictEqual([{ text: 'hello' }]); }); - it('strips inline media from messages', () => { + it('strips inline media from messages, keeping all messages', () => { const b64 = Buffer.from('lots of data\n').toString('base64'); const removed = '[Blob substitute]'; const messages = [ @@ -93,12 +90,38 @@ describe('message truncation utilities', () => { // indented json makes for better diffs in test output const messagesJson = JSON.stringify(messages, null, 2); - const result = truncateGenAiMessages(messages); + const result = stripInlineMediaFromMessages(messages); // original messages objects must not be mutated expect(JSON.stringify(messages, null, 2)).toBe(messagesJson); - // only the last message should be kept (with media stripped) + // all messages are kept, with inline media stripped expect(result).toStrictEqual([ + { + role: 'user', + content: [ + { + type: 'image', + source: { + type: 'base64', + media_type: 'image/png', + data: removed, + }, + }, + ], + }, + { + role: 'user', + content: { + image_url: removed, + }, + }, + { + role: 'agent', + type: 'image', + content: { + b64_json: removed, + }, + }, { role: 'system', inlineData: { @@ -170,7 +193,7 @@ describe('message truncation utilities', () => { ]; const messagesJson = JSON.stringify(messages, null, 2); - const result = truncateGenAiMessages(messages); + const result = stripInlineMediaFromMessages(messages); // original messages must not be mutated expect(JSON.stringify(messages, null, 2)).toBe(messagesJson); @@ -212,7 +235,7 @@ describe('message truncation utilities', () => { }, ]; - const result = truncateGenAiMessages(messages); + const result = stripInlineMediaFromMessages(messages); expect(result).toStrictEqual([ { @@ -256,7 +279,7 @@ describe('message truncation utilities', () => { }, ]; - const result = truncateGenAiMessages(messages); + const result = stripInlineMediaFromMessages(messages); expect(result).toStrictEqual([ { @@ -301,7 +324,7 @@ describe('message truncation utilities', () => { ]; const messagesJson = JSON.stringify(messages, null, 2); - const result = truncateGenAiMessages(messages); + const result = stripInlineMediaFromMessages(messages); expect(JSON.stringify(messages, null, 2)).toBe(messagesJson); @@ -347,7 +370,7 @@ describe('message truncation utilities', () => { ]; const messagesJson = JSON.stringify(messages, null, 2); - const result = truncateGenAiMessages(messages); + const result = stripInlineMediaFromMessages(messages); expect(JSON.stringify(messages, null, 2)).toBe(messagesJson); @@ -389,7 +412,7 @@ describe('message truncation utilities', () => { }, ]; - const result = truncateGenAiMessages(messages); + const result = stripInlineMediaFromMessages(messages); expect(result).toStrictEqual([ { @@ -407,222 +430,41 @@ describe('message truncation utilities', () => { }, ]); }); + }); - const humongous = 'this is a long string '.repeat(10_000); - const giant = 'this is a long string '.repeat(1_000); - const big = 'this is a long string '.repeat(100); - - it('keeps only the last message without truncation when it fits the limit', () => { - // Multiple messages that together exceed 20KB, but last message is small - const messages = [ - { content: `1 ${humongous}` }, - { content: `2 ${humongous}` }, - { content: `3 ${big}` }, // last message - small enough to fit - ]; - - const result = truncateGenAiMessages(messages); - - // Should only keep the last message, unchanged - expect(result).toStrictEqual([{ content: `3 ${big}` }]); - }); - - it('keeps only the last message with truncation when it does not fit the limit', () => { - const messages = [{ content: `1 ${humongous}` }, { content: `2 ${humongous}` }, { content: `3 ${humongous}` }]; - const result = truncateGenAiMessages(messages); - const truncLen = 20_000 - 2 - JSON.stringify({ content: '' }).length; - expect(result).toStrictEqual([{ content: `3 ${humongous}`.substring(0, truncLen) }]); - }); - - it('drops if last message cannot be safely truncated', () => { - const messages = [ - { content: `1 ${humongous}` }, - { content: `2 ${humongous}` }, - { what_even_is_this: `? ${humongous}` }, - ]; - const result = truncateGenAiMessages(messages); - expect(result).toStrictEqual([]); - }); - - it('fully drops message if content cannot be made to fit', () => { - const messages = [{ some_other_field: humongous, content: 'hello' }]; - expect(truncateGenAiMessages(messages)).toStrictEqual([]); - }); - - it('truncates if the message content string will not fit', () => { - const messages = [{ content: `2 ${humongous}` }]; - const result = truncateGenAiMessages(messages); - const truncLen = 20_000 - 2 - JSON.stringify({ content: '' }).length; - expect(result).toStrictEqual([{ content: `2 ${humongous}`.substring(0, truncLen) }]); - }); - - it('fully drops message if first part overhead does not fit', () => { - const messages = [ - { - parts: [{ some_other_field: humongous }], - }, - ]; - expect(truncateGenAiMessages(messages)).toStrictEqual([]); - }); - - it('fully drops message if overhead too large', () => { - const messages = [ - { - some_other_field: humongous, - parts: [], - }, - ]; - expect(truncateGenAiMessages(messages)).toStrictEqual([]); - }); - - it('truncates if the first message part will not fit', () => { - const messages = [ - { - parts: [`2 ${humongous}`, { some_other_field: 'no text here' }], - }, - ]; - - const result = truncateGenAiMessages(messages); - - // interesting (unexpected?) edge case effect of this truncation. - // subsequent messages count towards truncation overhead limit, - // but are not included, even without their text. This is an edge - // case that seems unlikely in normal usage. - const truncLen = - 20_000 - - 2 - - JSON.stringify({ - parts: ['', { some_other_field: 'no text here', text: '' }], - }).length; - - expect(result).toStrictEqual([ - { - parts: [`2 ${humongous}`.substring(0, truncLen)], - }, - ]); - }); + describe('getGenAiMessagesJsonString', () => { + it('returns a fallback instead of throwing on circular references', () => { + const circular: Record = { role: 'user', content: 'hi' }; + circular.self = circular; - it('truncates if the first message part will not fit, text object', () => { - const messages = [ - { - parts: [{ text: `2 ${humongous}` }], - }, - ]; - const result = truncateGenAiMessages(messages); - const truncLen = - 20_000 - - 2 - - JSON.stringify({ - parts: [{ text: '' }], - }).length; - expect(result).toStrictEqual([ - { - parts: [ - { - text: `2 ${humongous}`.substring(0, truncLen), - }, - ], - }, - ]); + expect(getGenAiMessagesJsonString(circular)).toBe('[unserializable]'); + expect(getGenAiMessagesJsonString([circular])).toBe('[unserializable]'); }); - it('drops if subsequent message part will not fit, text object', () => { - const messages = [ - { - parts: [ - { text: `1 ${big}` }, - { some_other_field: 'ok' }, - { text: `2 ${big}` }, - { text: `3 ${big}` }, - { text: `4 ${giant}` }, - { text: `5 ${giant}` }, - { text: `6 ${big}` }, - { text: `7 ${big}` }, - { text: `8 ${big}` }, - ], - }, - ]; - const result = truncateGenAiMessages(messages); - expect(result).toStrictEqual([ - { - parts: [{ text: `1 ${big}` }, { some_other_field: 'ok' }, { text: `2 ${big}` }, { text: `3 ${big}` }], - }, - ]); + it('returns strings as-is and serializes objects', () => { + expect(getGenAiMessagesJsonString('hello')).toBe('hello'); + expect(getGenAiMessagesJsonString({ a: 1 })).toBe('{"a":1}'); }); - it('truncates content array message when first text item does not fit', () => { + it('strips inline media from message arrays while keeping all messages', () => { + const b64 = Buffer.from('lots of data\n').toString('base64'); const messages = [ + { role: 'user', content: 'first message' }, { role: 'user', - content: [{ type: 'text', text: `2 ${humongous}` }], + content: [{ type: 'image_url', image_url: { url: `data:image/png;base64,${b64}` } }], }, ]; - const result = truncateGenAiMessages(messages); - const truncLen = - 20_000 - - 2 - - JSON.stringify({ - role: 'user', - content: [{ type: 'text', text: '' }], - }).length; - expect(result).toStrictEqual([ - { - role: 'user', - content: [{ type: 'text', text: `2 ${humongous}`.substring(0, truncLen) }], - }, - ]); - }); - it('drops subsequent content array items that do not fit', () => { - const messages = [ - { - role: 'assistant', - content: [ - { type: 'text', text: `1 ${big}` }, - { type: 'image_url', url: 'https://example.com/img.png' }, - { type: 'text', text: `2 ${big}` }, - { type: 'text', text: `3 ${big}` }, - { type: 'text', text: `4 ${giant}` }, - { type: 'text', text: `5 ${giant}` }, - ], - }, - ]; - const result = truncateGenAiMessages(messages); - expect(result).toStrictEqual([ - { - role: 'assistant', - content: [ - { type: 'text', text: `1 ${big}` }, - { type: 'image_url', url: 'https://example.com/img.png' }, - { type: 'text', text: `2 ${big}` }, - { type: 'text', text: `3 ${big}` }, - ], - }, - ]); - }); + const result = getGenAiMessagesJsonString(messages); - it('drops content array message if overhead is too large', () => { - const messages = [ - { - some_other_field: humongous, - content: [{ type: 'text', text: 'hello' }], - }, - ]; - expect(truncateGenAiMessages(messages)).toStrictEqual([]); + expect(result).toBe( + JSON.stringify([ + { role: 'user', content: 'first message' }, + { role: 'user', content: [{ type: 'image_url', image_url: { url: '[Blob substitute]' } }] }, + ]), + ); + expect(result).not.toContain(b64); }); }); }); - -describe('getTruncatedJsonString', () => { - it('returns a fallback instead of throwing on circular references', () => { - const circular: Record = { role: 'user', content: 'hi' }; - circular.self = circular; - - expect(getTruncatedJsonString(circular)).toBe('[unserializable]'); - expect(getTruncatedJsonString([circular])).toBe('[unserializable]'); - }); - - it('serializes normal values as before', () => { - expect(getTruncatedJsonString('hello')).toBe('hello'); - expect(getTruncatedJsonString({ a: 1 })).toBe('{"a":1}'); - }); -}); diff --git a/packages/core/test/lib/tracing/ai/utils.test.ts b/packages/core/test/lib/tracing/ai/utils.test.ts index b46a047c3838..c310bd4a0475 100644 --- a/packages/core/test/lib/tracing/ai/utils.test.ts +++ b/packages/core/test/lib/tracing/ai/utils.test.ts @@ -1,10 +1,6 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { getCurrentScope, getGlobalScope, getIsolationScope, setCurrentClient } from '../../../../src'; -import { - resolveAIRecordingOptions, - shouldEnableTruncation, - wrapPromiseWithMethods, -} from '../../../../src/tracing/ai/utils'; +import { resolveAIRecordingOptions, wrapPromiseWithMethods } from '../../../../src/tracing/ai/utils'; import { getDefaultTestClientOptions, TestClient } from '../../../mocks/client'; describe('resolveAIRecordingOptions', () => { @@ -87,51 +83,6 @@ describe('resolveAIRecordingOptions', () => { }); }); -describe('shouldEnableTruncation', () => { - beforeEach(() => { - getCurrentScope().clear(); - getIsolationScope().clear(); - getGlobalScope().clear(); - getCurrentScope().setClient(undefined); - }); - - afterEach(() => { - getCurrentScope().clear(); - getIsolationScope().clear(); - getGlobalScope().clear(); - }); - - function setupClient(options: Parameters[0] = {}): void { - const client = new TestClient(getDefaultTestClientOptions({ tracesSampleRate: 1, ...options })); - setCurrentClient(client); - client.init(); - } - - it('defaults to true when no client is set', () => { - expect(shouldEnableTruncation(undefined)).toBe(true); - }); - - it('defaults to false with a default client', () => { - setupClient(); - expect(shouldEnableTruncation(undefined)).toBe(false); - }); - - it('defaults to false when span streaming is enabled (traceLifecycle: stream)', () => { - setupClient({ traceLifecycle: 'stream' }); - expect(shouldEnableTruncation(undefined)).toBe(false); - }); - - it('explicit enableTruncation: true overrides the default', () => { - setupClient(); - expect(shouldEnableTruncation(true)).toBe(true); - }); - - it('explicit enableTruncation: false overrides the default', () => { - setupClient(); - expect(shouldEnableTruncation(false)).toBe(false); - }); -}); - describe('wrapPromiseWithMethods', () => { /** * Creates a mock APIPromise that mimics the behavior of OpenAI/Anthropic SDK APIPromise. diff --git a/packages/core/test/lib/tracing/langchain-utils.test.ts b/packages/core/test/lib/tracing/langchain-utils.test.ts index f39e01b5c625..2882af83fc01 100644 --- a/packages/core/test/lib/tracing/langchain-utils.test.ts +++ b/packages/core/test/lib/tracing/langchain-utils.test.ts @@ -241,7 +241,7 @@ describe('extractChatModelRequestAttributes with multimodal content', () => { ], ]; - const attrs = extractChatModelRequestAttributes(serialized, messages, true, true); + const attrs = extractChatModelRequestAttributes(serialized, messages, true); const inputMessages = attrs[GEN_AI_INPUT_MESSAGES_ATTRIBUTE] as string | undefined; expect(inputMessages).toBeDefined(); diff --git a/packages/core/test/lib/tracing/vercel-ai-request-messages.test.ts b/packages/core/test/lib/tracing/vercel-ai-request-messages.test.ts index 25e9ed53f1eb..961d145ca2e6 100644 --- a/packages/core/test/lib/tracing/vercel-ai-request-messages.test.ts +++ b/packages/core/test/lib/tracing/vercel-ai-request-messages.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { getTruncatedJsonString } from '../../../src/tracing/ai/utils'; -import { stringify } from '../../../src/utils/string'; +import { getGenAiMessagesJsonString } from '../../../src/tracing/ai/utils'; import { GEN_AI_INPUT_MESSAGES_ATTRIBUTE, GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE, @@ -27,22 +26,20 @@ function createRecordingSpan(): { span: Span; recorded: Record } describe('requestMessagesFromPrompt (ai.prompt.messages string branch)', () => { - it('reuses the original string verbatim when no system message and truncation is off', () => { + it('serializes all messages when there is no system message', () => { const { span, recorded } = createRecordingSpan(); - // Deliberately non-canonical whitespace. Re-serializing (JSON.stringify(JSON.parse(x))) - // would strip it, so a byte-identical result proves the original string was reused. - const original = '[ { "role": "user", "content": "hello world" } ]'; - const attributes = { [AI_PROMPT_MESSAGES_ATTRIBUTE]: original } as unknown as SpanAttributes; + const messages = [{ role: 'user', content: 'hello world' }]; + const attributes = { [AI_PROMPT_MESSAGES_ATTRIBUTE]: JSON.stringify(messages) } as unknown as SpanAttributes; - requestMessagesFromPrompt(span, attributes, /* enableTruncation */ false); + requestMessagesFromPrompt(span, attributes); - expect(recorded[AI_PROMPT_MESSAGES_ATTRIBUTE]).toBe(original); - expect(recorded[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBe(original); + expect(recorded[AI_PROMPT_MESSAGES_ATTRIBUTE]).toBe(getGenAiMessagesJsonString(messages)); + expect(recorded[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBe(getGenAiMessagesJsonString(messages)); expect(recorded[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE]).toBeUndefined(); }); - it('extracts the system message and re-serializes the remainder when truncation is off', () => { + it('extracts the system message and serializes the remainder', () => { const { span, recorded } = createRecordingSpan(); const original = JSON.stringify([ @@ -51,31 +48,32 @@ describe('requestMessagesFromPrompt (ai.prompt.messages string branch)', () => { ]); const attributes = { [AI_PROMPT_MESSAGES_ATTRIBUTE]: original } as unknown as SpanAttributes; - requestMessagesFromPrompt(span, attributes, false); + requestMessagesFromPrompt(span, attributes); expect(recorded[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE]).toBe(JSON.stringify([{ type: 'text', content: 'be nice' }])); - // System message removed; output is the SDK's own serialization of just the remainder. - expect(recorded[AI_PROMPT_MESSAGES_ATTRIBUTE]).toBe(stringify([{ role: 'user', content: 'hello' }])); + // System message removed; output is just the remainder. + expect(recorded[AI_PROMPT_MESSAGES_ATTRIBUTE]).toBe( + getGenAiMessagesJsonString([{ role: 'user', content: 'hello' }]), + ); expect(recorded[AI_PROMPT_MESSAGES_ATTRIBUTE]).not.toBe(original); }); - it('keeps the truncation path untouched when truncation is on', () => { + it('keeps all messages and strips inline media', () => { const { span, recorded } = createRecordingSpan(); + const b64 = Buffer.from('lots of data\n').toString('base64'); const messages = [ { role: 'user', content: 'first' }, - { role: 'user', content: 'second' }, + { role: 'user', content: [{ type: 'image_url', image_url: { url: `data:image/png;base64,${b64}` } }] }, ]; - const original = JSON.stringify(messages); - const attributes = { [AI_PROMPT_MESSAGES_ATTRIBUTE]: original } as unknown as SpanAttributes; + const attributes = { [AI_PROMPT_MESSAGES_ATTRIBUTE]: JSON.stringify(messages) } as unknown as SpanAttributes; - requestMessagesFromPrompt(span, attributes, /* enableTruncation */ true); + requestMessagesFromPrompt(span, attributes); - // Output must equal the SDK's own truncated serialization (and therefore differ from the - // input), proving the fast-path reuse did NOT short-circuit the truncation branch. - expect(recorded[AI_PROMPT_MESSAGES_ATTRIBUTE]).toBe(getTruncatedJsonString(messages)); - expect(recorded[AI_PROMPT_MESSAGES_ATTRIBUTE]).not.toBe(original); - // Original (pre-truncation) message count is still reported. + expect(recorded[AI_PROMPT_MESSAGES_ATTRIBUTE]).toBe(getGenAiMessagesJsonString(messages)); + expect(recorded[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toContain('first'); + expect(recorded[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toContain('[Blob substitute]'); + expect(recorded[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).not.toContain(b64); }); it('does not throw and sets no attributes for malformed JSON', () => { @@ -83,7 +81,7 @@ describe('requestMessagesFromPrompt (ai.prompt.messages string branch)', () => { const attributes = { [AI_PROMPT_MESSAGES_ATTRIBUTE]: '{ not json' } as unknown as SpanAttributes; - expect(() => requestMessagesFromPrompt(span, attributes, false)).not.toThrow(); + expect(() => requestMessagesFromPrompt(span, attributes)).not.toThrow(); expect(Object.keys(recorded)).toHaveLength(0); }); }); diff --git a/packages/core/test/lib/utils/anthropic-utils.test.ts b/packages/core/test/lib/utils/anthropic-utils.test.ts index 59b6dd63fad2..7079e0f8f014 100644 --- a/packages/core/test/lib/utils/anthropic-utils.test.ts +++ b/packages/core/test/lib/utils/anthropic-utils.test.ts @@ -96,24 +96,23 @@ describe('anthropic-ai-utils', () => { }; const span = mock as unknown as Span; - it('sets length along with truncated value', () => { + it('sets the full message value without truncation', () => { const content = 'A'.repeat(200_000); - setMessagesAttribute(span, [{ role: 'user', content }], true); - const result = [{ role: 'user', content: 'A'.repeat(19970) }]; + setMessagesAttribute(span, [{ role: 'user', content }]); expect(mock.attributes).toStrictEqual({ - 'gen_ai.input.messages': JSON.stringify(result), + 'gen_ai.input.messages': JSON.stringify([{ role: 'user', content }]), }); }); - it('sets length to 1 for non-array input', () => { - setMessagesAttribute(span, { content: 'hello, world' }, true); + it('serializes non-array input as-is', () => { + setMessagesAttribute(span, { content: 'hello, world' }); expect(mock.attributes).toStrictEqual({ 'gen_ai.input.messages': '{"content":"hello, world"}', }); }); it('ignores empty array', () => { - setMessagesAttribute(span, [], true); + setMessagesAttribute(span, []); expect(mock.attributes).toStrictEqual({ 'gen_ai.input.messages': '{"content":"hello, world"}', }); diff --git a/packages/server-utils/src/integrations/tracing-channel/anthropic.ts b/packages/server-utils/src/integrations/tracing-channel/anthropic.ts index a6425437c7f7..06641a682e8a 100644 --- a/packages/server-utils/src/integrations/tracing-channel/anthropic.ts +++ b/packages/server-utils/src/integrations/tracing-channel/anthropic.ts @@ -12,7 +12,6 @@ import { instrumentMessageStream, resolveAIRecordingOptions, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - shouldEnableTruncation, startInactiveSpan, waitForTracingChannelBinding, } from '@sentry/core'; @@ -113,7 +112,6 @@ function createGenAiSpan( const params = typeof args[0] === 'object' && args[0] !== null ? (args[0] as Record) : undefined; const { recordInputs } = resolveAIRecordingOptions(options); - const enableTruncation = shouldEnableTruncation(options.enableTruncation); const attributes = extractAnthropicRequestAttributes(args, methodPath, operation); const model = (attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE] as string) || 'unknown'; @@ -126,7 +124,7 @@ function createGenAiSpan( }); if (recordInputs && params) { - addAnthropicRequestAttributes(span, params, enableTruncation); + addAnthropicRequestAttributes(span, params); } return span; diff --git a/packages/server-utils/src/integrations/tracing-channel/google-genai.ts b/packages/server-utils/src/integrations/tracing-channel/google-genai.ts index 312e4716cf61..93f9c6a313e2 100644 --- a/packages/server-utils/src/integrations/tracing-channel/google-genai.ts +++ b/packages/server-utils/src/integrations/tracing-channel/google-genai.ts @@ -12,7 +12,6 @@ import { instrumentGoogleGenAIStream, resolveAIRecordingOptions, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - shouldEnableTruncation, spanToJSON, startInactiveSpan, waitForTracingChannelBinding, @@ -116,7 +115,6 @@ function createGenAiSpan( const params = args[0] as Record | undefined; const { recordInputs } = resolveAIRecordingOptions(options); - const enableTruncation = shouldEnableTruncation(options.enableTruncation); const attributes = extractGoogleGenAIRequestAttributes(operation, params, data.self); const model = (attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE] as string) || 'unknown'; @@ -129,7 +127,7 @@ function createGenAiSpan( }); if (recordInputs && params) { - addGoogleGenAIRequestAttributes(span, params, operation, enableTruncation); + addGoogleGenAIRequestAttributes(span, params, operation); } return span; diff --git a/packages/server-utils/src/integrations/tracing-channel/openai.ts b/packages/server-utils/src/integrations/tracing-channel/openai.ts index b172c6924300..6692745d37ef 100644 --- a/packages/server-utils/src/integrations/tracing-channel/openai.ts +++ b/packages/server-utils/src/integrations/tracing-channel/openai.ts @@ -10,7 +10,6 @@ import { instrumentOpenAiStream, resolveAIRecordingOptions, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - shouldEnableTruncation, startInactiveSpan, waitForTracingChannelBinding, } from '@sentry/core'; @@ -91,7 +90,6 @@ function createGenAiSpan(data: OpenAiChatChannelContext, operation: string, opti const params = args[0] as Record | undefined; const { recordInputs } = resolveAIRecordingOptions(options); - const enableTruncation = shouldEnableTruncation(options.enableTruncation); const attributes = extractOpenAiRequestAttributes(args, operation); attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] = ORIGIN; @@ -104,7 +102,7 @@ function createGenAiSpan(data: OpenAiChatChannelContext, operation: string, opti }); if (recordInputs && params) { - addOpenAiRequestAttributes(span, params, operation, enableTruncation); + addOpenAiRequestAttributes(span, params, operation); } return span; diff --git a/packages/server-utils/src/vercel-ai/index.ts b/packages/server-utils/src/vercel-ai/index.ts index 64296801c322..dda2be5f43a1 100644 --- a/packages/server-utils/src/vercel-ai/index.ts +++ b/packages/server-utils/src/vercel-ai/index.ts @@ -16,12 +16,6 @@ export interface VercelAiOptions { * Integration-level options take precedence over global `dataCollection` config. */ recordOutputs?: boolean; - - /** - * Enable or disable truncation of recorded input messages. - * Defaults to `true`. - */ - enableTruncation?: boolean; } const _vercelAiIntegration = ((options: VercelAiOptions = {}) => { diff --git a/packages/server-utils/src/vercel-ai/vercel-ai-dc-subscriber.ts b/packages/server-utils/src/vercel-ai/vercel-ai-dc-subscriber.ts index bafc3b51bbd9..b6bdd4133289 100644 --- a/packages/server-utils/src/vercel-ai/vercel-ai-dc-subscriber.ts +++ b/packages/server-utils/src/vercel-ai/vercel-ai-dc-subscriber.ts @@ -32,11 +32,10 @@ import { GEN_AI_CONVERSATION_ID_ATTRIBUTE, GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE, getClient, + getGenAiMessagesJsonString, getProviderMetadataAttributes, - getTruncatedJsonString, isObjectLike, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - shouldEnableTruncation, SPAN_STATUS_ERROR, spanToJSON, spanToTraceContext, @@ -206,7 +205,6 @@ export type VercelAiTracingChannelFactory = (name: string) => export interface VercelAiChannelOptions { recordInputs?: boolean; recordOutputs?: boolean; - enableTruncation?: boolean; } /** @@ -366,7 +364,7 @@ export function createSpanFromMessage( return undefined; } - const { recordInputs, enableTruncation } = getRecordingOptions(event, channelOptions); + const { recordInputs } = getRecordingOptions(event, channelOptions); const provider = asString(event.provider); const modelId = asString(event.modelId); const callId = asString(event.callId); @@ -392,9 +390,9 @@ export function createSpanFromMessage( // `generateObject` builds the same `invoke_agent` span as `generateText` (non-streaming); its // distinct `ai.generateObject` operationId rides on `event.operationId`. The JSON-schema attribute // the OTel path derives from the SDK's Zod schema is not reconstructed on the channel path. - return buildInvokeAgentSpan(event, baseAttributes, recordInputs, enableTruncation, callId, type === 'streamText'); + return buildInvokeAgentSpan(event, baseAttributes, recordInputs, callId, type === 'streamText'); case 'languageModelCall': - return buildModelCallSpan(event, baseAttributes, recordInputs, enableTruncation, callId, modelId); + return buildModelCallSpan(event, baseAttributes, recordInputs, callId, modelId); case 'executeTool': return buildToolSpan(event, recordInputs); case 'embed': @@ -427,7 +425,6 @@ function buildInvokeAgentSpan( event: Record, baseAttributes: SpanAttributes, recordInputs: boolean, - enableTruncation: boolean, callId: string | undefined, isStream: boolean, ): Span { @@ -441,7 +438,7 @@ function buildInvokeAgentSpan( [VERCEL_AI_OPERATION_ID_ATTRIBUTE]: operationId, [GEN_AI_RESPONSE_STREAMING]: isStream, ...(functionId ? { [GEN_AI_FUNCTION_ID]: functionId } : {}), - ...(recordInputs ? buildInputMessageAttributes(event, enableTruncation) : {}), + ...(recordInputs ? buildInputMessageAttributes(event) : {}), }); if (isStream && callId) { invokeAgentSpanByCallId.set(callId, span); @@ -454,7 +451,6 @@ function buildModelCallSpan( event: Record, baseAttributes: SpanAttributes, recordInputs: boolean, - enableTruncation: boolean, callId: string | undefined, modelId: string | undefined, ): Span { @@ -465,7 +461,7 @@ function buildModelCallSpan( return startGenAiSpan(GEN_AI_GENERATE_CONTENT_OPERATION, modelId, { ...baseAttributes, [VERCEL_AI_OPERATION_ID_ATTRIBUTE]: operationId, - ...(recordInputs ? buildInputMessageAttributes(event, enableTruncation) : {}), + ...(recordInputs ? buildInputMessageAttributes(event) : {}), ...(recordInputs && Array.isArray(event.tools) ? { [GEN_AI_REQUEST_AVAILABLE_TOOLS]: stringify(event.tools) } : {}), }); } @@ -689,14 +685,12 @@ function getRecordingOptions( ): { recordInputs: boolean; recordOutputs: boolean; - enableTruncation: boolean; } { const genAI = getClient()?.getDataCollectionOptions().genAI; return { recordInputs: resolveRecording(channelOptions.recordInputs, event.recordInputs, genAI?.inputs), recordOutputs: resolveRecording(channelOptions.recordOutputs, event.recordOutputs, genAI?.outputs), - enableTruncation: shouldEnableTruncation(channelOptions.enableTruncation), }; } @@ -720,10 +714,7 @@ function resolveRecording(integrationOption: unknown, perCallOption: unknown, gl return globalDefault === true; } -function buildInputMessageAttributes( - event: Record, - enableTruncation: boolean, -): Record { +function buildInputMessageAttributes(event: Record): Record { const attributes: Record = {}; // `ai` >= 7 forbids system messages in `messages`/`prompt` and exposes the system prompt as a @@ -738,7 +729,7 @@ function buildInputMessageAttributes( // simpler `prompt` field is used. const messages = event.messages ?? event.prompt; if (messages !== undefined) { - attributes[GEN_AI_INPUT_MESSAGES] = enableTruncation ? getTruncatedJsonString(messages) : stringify(messages); + attributes[GEN_AI_INPUT_MESSAGES] = getGenAiMessagesJsonString(messages); } return attributes; diff --git a/packages/vercel-edge/src/integrations/tracing/vercelai.ts b/packages/vercel-edge/src/integrations/tracing/vercelai.ts index 7f5fbddf09fa..6f65c4f2c11f 100644 --- a/packages/vercel-edge/src/integrations/tracing/vercelai.ts +++ b/packages/vercel-edge/src/integrations/tracing/vercelai.ts @@ -14,12 +14,6 @@ import { addVercelAiProcessors, defineIntegration } from '@sentry/core'; const INTEGRATION_NAME = 'VercelAI' as const; interface VercelAiOptions { - /** - * Enable or disable truncation of recorded input messages. - * Defaults to `true`. - */ - enableTruncation?: boolean; - // `recordInputs`/`recordOutputs` are intentionally omitted: this entrypoint only post-processes // spans the AI SDK already emitted (no OTel patch or tracing channel in the edge runtime), so it // cannot decide whether inputs/outputs are recorded. Control this per call via