Skip to content

Commit 388dd46

Browse files
committed
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
1 parent 2af59be commit 388dd46

File tree

5 files changed

+137
-2
lines changed

5 files changed

+137
-2
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import * as Sentry from '@sentry/node';
2+
import { loggingTransport } from '@sentry-internal/node-integration-tests';
3+
4+
Sentry.init({
5+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
6+
release: '1.0',
7+
tracesSampleRate: 1.0,
8+
sendDefaultPii: true,
9+
transport: loggingTransport,
10+
traceLifecycle: 'stream',
11+
integrations: [
12+
Sentry.googleGenAIIntegration({
13+
enableTruncation: true,
14+
}),
15+
],
16+
});
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import * as Sentry from '@sentry/node';
2+
import { loggingTransport } from '@sentry-internal/node-integration-tests';
3+
4+
Sentry.init({
5+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
6+
release: '1.0',
7+
tracesSampleRate: 1.0,
8+
sendDefaultPii: true,
9+
transport: loggingTransport,
10+
traceLifecycle: 'stream',
11+
});
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { GoogleGenAI } from '@google/genai';
2+
import * as Sentry from '@sentry/node';
3+
import express from 'express';
4+
5+
function startMockGoogleGenAIServer() {
6+
const app = express();
7+
app.use(express.json({ limit: '10mb' }));
8+
9+
app.post('/v1beta/models/:model\\:generateContent', (req, res) => {
10+
res.json({
11+
candidates: [
12+
{
13+
content: { parts: [{ text: 'Response' }], role: 'model' },
14+
finishReason: 'STOP',
15+
},
16+
],
17+
usageMetadata: { promptTokenCount: 10, candidatesTokenCount: 5, totalTokenCount: 15 },
18+
});
19+
});
20+
21+
return new Promise(resolve => {
22+
const server = app.listen(0, () => {
23+
resolve(server);
24+
});
25+
});
26+
}
27+
28+
async function run() {
29+
const server = await startMockGoogleGenAIServer();
30+
31+
await Sentry.startSpan({ op: 'function', name: 'main' }, async () => {
32+
const client = new GoogleGenAI({
33+
apiKey: 'mock-api-key',
34+
httpOptions: { baseUrl: `http://localhost:${server.address().port}` },
35+
});
36+
37+
// Long content that would normally be truncated
38+
const longContent = 'A'.repeat(50_000);
39+
await client.models.generateContent({
40+
model: 'gemini-1.5-flash',
41+
contents: [{ role: 'user', parts: [{ text: longContent }] }],
42+
});
43+
});
44+
45+
// Flush is required when span streaming is enabled to ensure streamed spans are sent before the process exits
46+
await Sentry.flush(2000);
47+
48+
server.close();
49+
}
50+
51+
run();

dev-packages/node-integration-tests/suites/tracing/google-genai/test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -686,4 +686,55 @@ describe('Google GenAI integration', () => {
686686
});
687687
},
688688
);
689+
690+
const streamingLongContent = 'A'.repeat(50_000);
691+
692+
createEsmAndCjsTests(__dirname, 'scenario-span-streaming.mjs', 'instrument-streaming.mjs', (createRunner, test) => {
693+
test('automatically disables truncation when span streaming is enabled', async () => {
694+
await createRunner()
695+
.expect({
696+
span: container => {
697+
const spans = container.items;
698+
699+
const expectedMessages = JSON.stringify([{ role: 'user', parts: [{ text: streamingLongContent }] }]);
700+
const chatSpan = spans.find(
701+
s => s.attributes?.[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value === expectedMessages,
702+
);
703+
expect(chatSpan).toBeDefined();
704+
},
705+
})
706+
.start()
707+
.completed();
708+
});
709+
});
710+
711+
createEsmAndCjsTests(
712+
__dirname,
713+
'scenario-span-streaming.mjs',
714+
'instrument-streaming-with-truncation.mjs',
715+
(createRunner, test) => {
716+
test('respects explicit enableTruncation: true even when span streaming is enabled', async () => {
717+
await createRunner()
718+
.expect({
719+
span: container => {
720+
const spans = container.items;
721+
722+
// With explicit enableTruncation: true, content should be truncated despite streaming.
723+
// Find the chat span by matching the start of the truncated content (the 'A' repeated messages).
724+
const chatSpan = spans.find(s =>
725+
s.attributes?.[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value?.startsWith(
726+
'[{"role":"user","parts":[{"text":"AAAA',
727+
),
728+
);
729+
expect(chatSpan).toBeDefined();
730+
expect(chatSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value.length).toBeLessThan(
731+
streamingLongContent.length,
732+
);
733+
},
734+
})
735+
.start()
736+
.completed();
737+
});
738+
},
739+
);
689740
});

packages/core/src/tracing/google-genai/index.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
/* eslint-disable max-lines */
2+
import { getClient } from '../../currentScopes';
23
import { captureException } from '../../exports';
34
import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../../semanticAttributes';
45
import { SPAN_STATUS_ERROR } from '../../tracing';
6+
import { hasSpanStreamingEnabled } from '../../tracing/spans/hasSpanStreamingEnabled';
57
import { startSpan, startSpanManual } from '../../tracing/trace';
68
import type { Span, SpanAttributeValue } from '../../types-hoist/span';
79
import { handleCallbackErrors } from '../../utils/handleCallbackErrors';
@@ -297,7 +299,9 @@ function instrumentMethod<T extends unknown[], R>(
297299
async (span: Span) => {
298300
try {
299301
if (options.recordInputs && params) {
300-
addPrivateRequestAttributes(span, params, isEmbeddings, options.enableTruncation ?? true);
302+
const client = getClient();
303+
const enableTruncation = options.enableTruncation ?? !(client && hasSpanStreamingEnabled(client));
304+
addPrivateRequestAttributes(span, params, isEmbeddings, enableTruncation);
301305
}
302306
const stream = await target.apply(context, args);
303307
return instrumentStream(stream, span, Boolean(options.recordOutputs)) as R;
@@ -325,7 +329,9 @@ function instrumentMethod<T extends unknown[], R>(
325329
},
326330
(span: Span) => {
327331
if (options.recordInputs && params) {
328-
addPrivateRequestAttributes(span, params, isEmbeddings, options.enableTruncation ?? true);
332+
const client = getClient();
333+
const enableTruncation = options.enableTruncation ?? !(client && hasSpanStreamingEnabled(client));
334+
addPrivateRequestAttributes(span, params, isEmbeddings, enableTruncation);
329335
}
330336

331337
return handleCallbackErrors(

0 commit comments

Comments
 (0)