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..13b271a23878 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/google-genai/scenario-no-truncation.mjs @@ -0,0 +1,47 @@ +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 }] }, + { role: 'model', parts: [{ text: 'Some reply' }] }, + { role: 'user', parts: [{ text: 'Follow-up question' }] }, + ], + }); + }); +} + +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..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 @@ -653,4 +653,37 @@ 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', parts: [{ text: longContent }] }, + { role: 'model', parts: [{ text: 'Some reply' }] }, + { role: 'user', parts: [{ text: 'Follow-up question' }] }, + ]), + [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 3, + }), + }), + ]), + }; + + 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; } /**