From a0e7e58020116ca4f7b00b507ba2da6ba546413e Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Fri, 10 Apr 2026 12:14:50 +0900 Subject: [PATCH 1/4] feat(core): Add `enableTruncation` option to Google GenAI integration This PR adds an `enableTruncation` option to the Google GenAI integration that allows users to disable input message truncation. It defaults to `true` to preserve existing behavior. Also refactors the truncation to use the shared `getTruncatedJsonString`/`getJsonString` utilities instead of calling `truncateGenAiMessages` directly. Closes: #20137 --- .../google-genai/instrument-no-truncation.mjs | 17 ++++++++ .../google-genai/scenario-no-truncation.mjs | 43 +++++++++++++++++++ .../suites/tracing/google-genai/test.ts | 31 +++++++++++++ .../core/src/tracing/google-genai/index.ts | 24 ++++++++--- .../core/src/tracing/google-genai/types.ts | 5 +++ 5 files changed, 114 insertions(+), 6 deletions(-) create mode 100644 dev-packages/node-integration-tests/suites/tracing/google-genai/instrument-no-truncation.mjs create mode 100644 dev-packages/node-integration-tests/suites/tracing/google-genai/scenario-no-truncation.mjs 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 new file mode 100644 index 000000000000..be5288b429d6 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/google-genai/instrument-no-truncation.mjs @@ -0,0 +1,17 @@ +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, + sendDefaultPii: false, + transport: loggingTransport, + integrations: [ + Sentry.googleGenAIIntegration({ + recordInputs: true, + recordOutputs: true, + enableTruncation: false, + }), + ], +}); 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 new file mode 100644 index 000000000000..9abf870963d5 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/google-genai/scenario-no-truncation.mjs @@ -0,0 +1,43 @@ +import { instrumentGoogleGenAIClient } from '@sentry/core'; +import * as Sentry from '@sentry/node'; + +class MockGoogleGenerativeAI { + constructor(config) { + this.apiKey = config.apiKey; + this.models = { + generateContent: this._generateContent.bind(this), + }; + } + + async _generateContent() { + await new Promise(resolve => setTimeout(resolve, 10)); + return { + response: { + text: () => 'Response', + usageMetadata: { promptTokenCount: 10, candidatesTokenCount: 5, totalTokenCount: 15 }, + candidates: [ + { + content: { parts: [{ text: 'Response' }], role: 'model' }, + finishReason: 'STOP', + }, + ], + }, + }; + } +} + +async function run() { + await Sentry.startSpan({ op: 'function', name: 'main' }, async () => { + const mockClient = new MockGoogleGenerativeAI({ apiKey: 'mock-api-key' }); + const client = instrumentGoogleGenAIClient(mockClient, { enableTruncation: false, recordInputs: true }); + + // 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 }] }], + }); + }); +} + +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 5d79cdf94202..a3ba05a1757e 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 @@ -653,4 +653,35 @@ describe('Google GenAI integration', () => { .completed(); }); }); + + const longContent = 'A'.repeat(50_000); + + const EXPECTED_TRANSACTION_NO_TRUNCATION = { + transaction: 'main', + spans: expect.arrayContaining([ + expect.objectContaining({ + data: expect.objectContaining({ + [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: JSON.stringify([ + { role: 'user', content: [{ type: 'text', text: longContent }] }, + ]), + [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 1, + }), + }), + ]), + }; + + 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: EXPECTED_TRANSACTION_NO_TRUNCATION }) + .start() + .completed(); + }); + }, + ); }); diff --git a/packages/core/src/tracing/google-genai/index.ts b/packages/core/src/tracing/google-genai/index.ts index ac06d39a5784..51ca11f612fa 100644 --- a/packages/core/src/tracing/google-genai/index.ts +++ b/packages/core/src/tracing/google-genai/index.ts @@ -27,9 +27,14 @@ import { GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE, GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE, } from '../ai/gen-ai-attributes'; -import { truncateGenAiMessages } from '../ai/messageTruncation'; import type { InstrumentedMethodEntry } from '../ai/utils'; -import { buildMethodPath, extractSystemInstructions, resolveAIRecordingOptions } from '../ai/utils'; +import { + buildMethodPath, + extractSystemInstructions, + getJsonString, + getTruncatedJsonString, + resolveAIRecordingOptions, +} from '../ai/utils'; import { GOOGLE_GENAI_METHOD_REGISTRY, GOOGLE_GENAI_SYSTEM_NAME } from './constants'; import { instrumentStream } from './streaming'; import type { Candidate, ContentPart, GoogleGenAIOptions, GoogleGenAIResponse } from './types'; @@ -134,7 +139,12 @@ function extractRequestAttributes( * This is only recorded if recordInputs is true. * Handles different parameter formats for different Google GenAI methods. */ -function addPrivateRequestAttributes(span: Span, params: Record, isEmbeddings: boolean): void { +function addPrivateRequestAttributes( + span: Span, + params: Record, + isEmbeddings: boolean, + enableTruncation: boolean, +): void { if (isEmbeddings) { const contents = params.contents; if (contents != null) { @@ -184,7 +194,9 @@ function addPrivateRequestAttributes(span: Span, params: Record const filteredLength = Array.isArray(filteredMessages) ? filteredMessages.length : 0; span.setAttributes({ [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: filteredLength, - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: JSON.stringify(truncateGenAiMessages(filteredMessages as unknown[])), + [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: enableTruncation + ? getTruncatedJsonString(filteredMessages) + : getJsonString(filteredMessages), }); } } @@ -285,7 +297,7 @@ function instrumentMethod( async (span: Span) => { try { if (options.recordInputs && params) { - addPrivateRequestAttributes(span, params, isEmbeddings); + addPrivateRequestAttributes(span, params, isEmbeddings, options.enableTruncation ?? true); } const stream = await target.apply(context, args); return instrumentStream(stream, span, Boolean(options.recordOutputs)) as R; @@ -313,7 +325,7 @@ function instrumentMethod( }, (span: Span) => { if (options.recordInputs && params) { - addPrivateRequestAttributes(span, params, isEmbeddings); + addPrivateRequestAttributes(span, params, isEmbeddings, options.enableTruncation ?? true); } return handleCallbackErrors( diff --git a/packages/core/src/tracing/google-genai/types.ts b/packages/core/src/tracing/google-genai/types.ts index abfb8141ce31..69f1e279fbd0 100644 --- a/packages/core/src/tracing/google-genai/types.ts +++ b/packages/core/src/tracing/google-genai/types.ts @@ -9,6 +9,11 @@ export interface GoogleGenAIOptions { * Enable or disable output recording. */ recordOutputs?: boolean; + /** + * Enable or disable truncation of recorded input messages. + * Defaults to `true`. + */ + enableTruncation?: boolean; } /** From 13b59585244c7684cdcd5352ebe83c96f3f0f650 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Fri, 10 Apr 2026 14:37:46 +0900 Subject: [PATCH 2/4] Bump size limit --- .size-limit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.size-limit.js b/.size-limit.js index 1e6e8d951464..4100751f2c40 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -276,7 +276,7 @@ module.exports = [ path: createCDNPath('bundle.tracing.replay.logs.metrics.min.js'), gzip: false, brotli: false, - limit: '250 KB', + limit: '251 KB', }, { name: 'CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed', From 20c2f15bfa41cd9a21c13ce3a6ac3457ea39b338 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Fri, 10 Apr 2026 17:04:44 +0900 Subject: [PATCH 3/4] Fix test assertion to use Google GenAI parts format instead of content format --- .../suites/tracing/google-genai/test.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) 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 a3ba05a1757e..ff9a872f849c 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 @@ -661,9 +661,7 @@ describe('Google GenAI integration', () => { spans: expect.arrayContaining([ expect.objectContaining({ data: expect.objectContaining({ - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: JSON.stringify([ - { role: 'user', content: [{ type: 'text', text: longContent }] }, - ]), + [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: JSON.stringify([{ role: 'user', parts: [{ text: longContent }] }]), [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 1, }), }), From c93411f717830f12d46e963b513c03ab10b66324 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Fri, 10 Apr 2026 12:13:25 +0200 Subject: [PATCH 4/4] updates --- .size-limit.js | 2 +- .../tracing/google-genai/scenario-no-truncation.mjs | 6 +++++- .../suites/tracing/google-genai/test.ts | 8 ++++++-- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/.size-limit.js b/.size-limit.js index 4100751f2c40..1e6e8d951464 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -276,7 +276,7 @@ module.exports = [ path: createCDNPath('bundle.tracing.replay.logs.metrics.min.js'), gzip: false, brotli: false, - limit: '251 KB', + limit: '250 KB', }, { name: 'CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed', 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 index 9abf870963d5..13b271a23878 100644 --- 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 @@ -35,7 +35,11 @@ async function run() { const longContent = 'A'.repeat(50_000); await client.models.generateContent({ model: 'gemini-1.5-flash', - contents: [{ role: 'user', parts: [{ text: longContent }] }], + contents: [ + { role: 'user', parts: [{ text: longContent }] }, + { role: 'model', parts: [{ text: 'Some reply' }] }, + { role: 'user', parts: [{ text: 'Follow-up question' }] }, + ], }); }); } 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 ff9a872f849c..b6271a03f4fc 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 @@ -661,8 +661,12 @@ describe('Google GenAI integration', () => { spans: expect.arrayContaining([ expect.objectContaining({ data: expect.objectContaining({ - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: JSON.stringify([{ role: 'user', parts: [{ text: longContent }] }]), - [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 1, + [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: JSON.stringify([ + { role: 'user', parts: [{ text: longContent }] }, + { role: 'model', parts: [{ text: 'Some reply' }] }, + { role: 'user', parts: [{ text: 'Follow-up question' }] }, + ]), + [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 3, }), }), ]),