From 1e31f047d1ce29367448d18e0c48342861c5d117 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Sun, 12 Apr 2026 01:42:46 +0900 Subject: [PATCH 1/3] feat(core): Automatically disable truncation when span streaming is enabled in Google GenAI integration When span streaming is enabled, the `enableTruncation` option now defaults to `false` unless the user has explicitly set it. Closes: #20223 --- .../instrument-streaming-with-truncation.mjs | 16 ++++++ .../google-genai/instrument-streaming.mjs | 11 ++++ .../google-genai/scenario-span-streaming.mjs | 51 +++++++++++++++++++ .../suites/tracing/google-genai/test.ts | 50 ++++++++++++++++++ .../core/src/tracing/google-genai/index.ts | 10 +++- 5 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 dev-packages/node-integration-tests/suites/tracing/google-genai/instrument-streaming-with-truncation.mjs create mode 100644 dev-packages/node-integration-tests/suites/tracing/google-genai/instrument-streaming.mjs create mode 100644 dev-packages/node-integration-tests/suites/tracing/google-genai/scenario-span-streaming.mjs 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 new file mode 100644 index 000000000000..e706163aea04 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/google-genai/instrument-streaming-with-truncation.mjs @@ -0,0 +1,16 @@ +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: true, + transport: loggingTransport, + traceLifecycle: 'stream', + integrations: [ + Sentry.googleGenAIIntegration({ + enableTruncation: true, + }), + ], +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/google-genai/instrument-streaming.mjs b/dev-packages/node-integration-tests/suites/tracing/google-genai/instrument-streaming.mjs new file mode 100644 index 000000000000..48a860c510c5 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/google-genai/instrument-streaming.mjs @@ -0,0 +1,11 @@ +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: true, + transport: loggingTransport, + traceLifecycle: 'stream', +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/google-genai/scenario-span-streaming.mjs b/dev-packages/node-integration-tests/suites/tracing/google-genai/scenario-span-streaming.mjs new file mode 100644 index 000000000000..f5b2656b5cb0 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/google-genai/scenario-span-streaming.mjs @@ -0,0 +1,51 @@ +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.json({ + candidates: [ + { + content: { parts: [{ text: 'Response' }], role: 'model' }, + finishReason: 'STOP', + }, + ], + 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 }] }], + }); + }); + + // Flush is required when span streaming is enabled to ensure streamed spans are sent before the process exits + 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 b6271a03f4fc..9839ef5fa2c0 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 @@ -686,4 +686,54 @@ describe('Google GenAI integration', () => { }); }, ); + + 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 => + 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. + // Find the chat span by matching the start of the truncated content (the 'A' repeated messages). + const chatSpan = spans.find(s => + s.attributes?.[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value?.startsWith( + '[{"role":"user","parts":[{"text":"AAAA', + ), + ); + expect(chatSpan).toBeDefined(); + expect(chatSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value.length).toBeLessThan( + streamingLongContent.length, + ); + }, + }) + .start() + .completed(); + }); + }, + ); }); diff --git a/packages/core/src/tracing/google-genai/index.ts b/packages/core/src/tracing/google-genai/index.ts index 51ca11f612fa..6babe4c1a238 100644 --- a/packages/core/src/tracing/google-genai/index.ts +++ b/packages/core/src/tracing/google-genai/index.ts @@ -1,7 +1,9 @@ /* eslint-disable max-lines */ +import { getClient } from '../../currentScopes'; import { captureException } from '../../exports'; import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../../semanticAttributes'; import { SPAN_STATUS_ERROR } from '../../tracing'; +import { hasSpanStreamingEnabled } from '../../tracing/spans/hasSpanStreamingEnabled'; import { startSpan, startSpanManual } from '../../tracing/trace'; import type { Span, SpanAttributeValue } from '../../types-hoist/span'; import { handleCallbackErrors } from '../../utils/handleCallbackErrors'; @@ -297,7 +299,9 @@ function instrumentMethod( async (span: Span) => { try { if (options.recordInputs && params) { - addPrivateRequestAttributes(span, params, isEmbeddings, options.enableTruncation ?? true); + const client = getClient(); + const enableTruncation = options.enableTruncation ?? !(client && hasSpanStreamingEnabled(client)); + addPrivateRequestAttributes(span, params, isEmbeddings, enableTruncation); } const stream = await target.apply(context, args); return instrumentStream(stream, span, Boolean(options.recordOutputs)) as R; @@ -325,7 +329,9 @@ function instrumentMethod( }, (span: Span) => { if (options.recordInputs && params) { - addPrivateRequestAttributes(span, params, isEmbeddings, options.enableTruncation ?? true); + const client = getClient(); + const enableTruncation = options.enableTruncation ?? !(client && hasSpanStreamingEnabled(client)); + addPrivateRequestAttributes(span, params, isEmbeddings, enableTruncation); } return handleCallbackErrors( From 61fe1da9a74a11d050099b8adf561b5c12f6c901 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Sun, 12 Apr 2026 01:56:19 +0900 Subject: [PATCH 2/3] Add shouldEnableTruncation helper --- packages/core/src/tracing/ai/utils.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/core/src/tracing/ai/utils.ts b/packages/core/src/tracing/ai/utils.ts index d3cce644dbc1..05502b249efb 100644 --- a/packages/core/src/tracing/ai/utils.ts +++ b/packages/core/src/tracing/ai/utils.ts @@ -3,6 +3,7 @@ */ import { captureException } from '../../exports'; import { getClient } from '../../currentScopes'; +import { hasSpanStreamingEnabled } from '../spans/hasSpanStreamingEnabled'; import type { Span } from '../../types-hoist/span'; import { isThenable } from '../../utils/is'; import { @@ -56,6 +57,16 @@ 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 when span streaming is active. + */ +export function shouldEnableTruncation(enableTruncation: boolean | undefined): boolean { + const client = getClient(); + return enableTruncation ?? !(client && hasSpanStreamingEnabled(client)); +} + /** * Build method path from current traversal */ From f7c722d733807af7e9f45f3edd8383ef8b37f154 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Sun, 12 Apr 2026 02:01:32 +0900 Subject: [PATCH 3/3] Use shouldEnableTruncation in Google GenAI integration --- packages/core/src/tracing/google-genai/index.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/core/src/tracing/google-genai/index.ts b/packages/core/src/tracing/google-genai/index.ts index 6babe4c1a238..7d5f6023f271 100644 --- a/packages/core/src/tracing/google-genai/index.ts +++ b/packages/core/src/tracing/google-genai/index.ts @@ -1,9 +1,7 @@ /* eslint-disable max-lines */ -import { getClient } from '../../currentScopes'; import { captureException } from '../../exports'; import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../../semanticAttributes'; import { SPAN_STATUS_ERROR } from '../../tracing'; -import { hasSpanStreamingEnabled } from '../../tracing/spans/hasSpanStreamingEnabled'; import { startSpan, startSpanManual } from '../../tracing/trace'; import type { Span, SpanAttributeValue } from '../../types-hoist/span'; import { handleCallbackErrors } from '../../utils/handleCallbackErrors'; @@ -36,6 +34,7 @@ import { getJsonString, getTruncatedJsonString, resolveAIRecordingOptions, + shouldEnableTruncation, } from '../ai/utils'; import { GOOGLE_GENAI_METHOD_REGISTRY, GOOGLE_GENAI_SYSTEM_NAME } from './constants'; import { instrumentStream } from './streaming'; @@ -299,9 +298,12 @@ function instrumentMethod( async (span: Span) => { try { if (options.recordInputs && params) { - const client = getClient(); - const enableTruncation = options.enableTruncation ?? !(client && hasSpanStreamingEnabled(client)); - addPrivateRequestAttributes(span, params, isEmbeddings, enableTruncation); + addPrivateRequestAttributes( + span, + params, + isEmbeddings, + shouldEnableTruncation(options.enableTruncation), + ); } const stream = await target.apply(context, args); return instrumentStream(stream, span, Boolean(options.recordOutputs)) as R; @@ -329,9 +331,7 @@ function instrumentMethod( }, (span: Span) => { if (options.recordInputs && params) { - const client = getClient(); - const enableTruncation = options.enableTruncation ?? !(client && hasSpanStreamingEnabled(client)); - addPrivateRequestAttributes(span, params, isEmbeddings, enableTruncation); + addPrivateRequestAttributes(span, params, isEmbeddings, shouldEnableTruncation(options.enableTruncation)); } return handleCallbackErrors(