Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
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;
},
streamGenAiSpans: true,
});
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ async function run() {
apiKey: 'mock-api-key',
});

const client = instrumentAnthropicAiClient(mockClient);
const client = instrumentAnthropicAiClient(mockClient, { enableTruncation: true, recordInputs: true });

// Send the image showing the number 3
// Put the image in the last message so it doesn't get dropped
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ async function run() {
apiKey: 'mock-api-key',
});

const client = instrumentAnthropicAiClient(mockClient);
const client = instrumentAnthropicAiClient(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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -606,7 +606,7 @@ describe('Anthropic integration', () => {
createEsmAndCjsTests(
__dirname,
'scenario-message-truncation.mjs',
'instrument-with-pii.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()
Expand Down Expand Up @@ -659,51 +659,56 @@ describe('Anthropic integration', () => {
},
);

createEsmAndCjsTests(__dirname, 'scenario-media-truncation.mjs', 'instrument-with-pii.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-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]',
},
},
},
],
},
]);
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');
expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE].value).toBe(2);
},
})
.start()
.completed();
});
});
// [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');
expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE].value).toBe(2);
},
})
.start()
.completed();
});
},
);

createEsmAndCjsTests(
__dirname,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
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;
},
streamGenAiSpans: true,
});
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ async function run() {
apiKey: 'mock-api-key',
});

const client = instrumentGoogleGenAIClient(mockClient);
const client = instrumentGoogleGenAIClient(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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,7 @@ describe('Google GenAI integration', () => {
createEsmAndCjsTests(
__dirname,
'scenario-message-truncation.mjs',
'instrument-with-pii.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()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
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;
},
streamGenAiSpans: true,
});
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ describe('LangChain integration', () => {
createEsmAndCjsTests(
__dirname,
'scenario-message-truncation.mjs',
'instrument-with-pii.mjs',
'instrument-with-truncation.mjs',
(createRunner, test) => {
test('truncates messages when they exceed byte limit', async () => {
await createRunner()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
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;
},
streamGenAiSpans: true,
});
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ conditionalTest({ min: 20 })('LangChain integration (v1)', () => {
createEsmAndCjsTests(
__dirname,
'scenario-message-truncation.mjs',
'instrument-with-pii.mjs',
'instrument-with-truncation.mjs',
(createRunner, test) => {
test('truncates messages when they exceed byte limit', async () => {
await createRunner()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
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;
},
streamGenAiSpans: true,
});
Original file line number Diff line number Diff line change
Expand Up @@ -1174,7 +1174,7 @@ describe('OpenAI integration', () => {
createEsmAndCjsTests(
__dirname,
'truncation/scenario-message-truncation-completions.mjs',
'instrument-with-pii.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()
Expand Down Expand Up @@ -1278,7 +1278,7 @@ describe('OpenAI integration', () => {
createEsmAndCjsTests(
__dirname,
'truncation/scenario-message-truncation-responses.mjs',
'instrument-with-pii.mjs',
'instrument-with-truncation.mjs',
(createRunner, test) => {
test('truncates string inputs when they exceed byte limit', async () => {
await createRunner()
Expand Down Expand Up @@ -1603,7 +1603,7 @@ describe('OpenAI integration', () => {
});
});

createEsmAndCjsTests(__dirname, 'scenario-vision.mjs', 'instrument-with-pii.mjs', (createRunner, test) => {
createEsmAndCjsTests(__dirname, 'scenario-vision.mjs', 'instrument-with-truncation.mjs', (createRunner, test) => {
test('redacts inline base64 image data in vision requests', async () => {
await createRunner()
.ignore('event')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ async function run() {
apiKey: 'mock-api-key',
});

const client = instrumentOpenAiClient(mockClient);
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ async function run() {
apiKey: 'mock-api-key',
});

const client = instrumentOpenAiClient(mockClient);
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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
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 })],
streamGenAiSpans: true,
});
Original file line number Diff line number Diff line change
Expand Up @@ -446,7 +446,7 @@ describe('Vercel AI integration', () => {
createEsmAndCjsTests(
__dirname,
'scenario-message-truncation.mjs',
'instrument-with-pii.mjs',
'instrument-with-truncation.mjs',
(createRunner, test) => {
test('truncates messages when they exceed byte limit', async () => {
await createRunner()
Expand Down
15 changes: 13 additions & 2 deletions packages/core/src/tracing/ai/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,22 @@ export function resolveAIRecordingOptions<T extends AIRecordingOptions>(options?
/**
* 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.
* Otherwise, truncation is disabled whenever gen_ai spans are sent through the span streaming / v2
* span path, i.e. full span streaming (`traceLifecycle: 'stream'`) or `streamGenAiSpans`. 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;
}

const client = getClient();
return enableTruncation ?? !(client && hasSpanStreamingEnabled(client));
if (!client) {
return true;
}

return !hasSpanStreamingEnabled(client) && !client.getOptions().streamGenAiSpans;
}

/**
Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/types/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -569,6 +569,11 @@ export interface ClientOptions<TO extends BaseTransportOptions = BaseTransportOp
*
* This enables streaming gen_ai spans, avoiding payload size limits of usual transactions.
*
* Because the v2 span format is not subject to the transaction payload-size limits that gen_ai message
* truncation exists to work around, enabling this option also disables gen_ai input truncation (and the
* inline-media redaction that rides along with it) by default. Set `enableTruncation: true` on the
* respective AI integration to opt back into truncation.
*
* @default false
*/
streamGenAiSpans?: boolean;
Expand Down
Loading
Loading