From 29f97f5dde85ce319e2b2f462d0c598edaefbcbd Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Wed, 22 Jul 2026 15:19:31 +0200 Subject: [PATCH 1/4] feat(core)!: Remove `streamGenAiSpans` flag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removes the `streamGenAiSpans` client option. The behavior it gated (extracting gen_ai spans from transactions into a v2 span envelope container) was already the default and is now unconditional — there is no longer a way to opt out. For static transactions, `extractGenAiSpansFromEvent` always pulls gen_ai spans into a v2 span container. For full span streaming (`traceLifecycle: 'stream'`), the existing `hasSpanStreamingEnabled` guard still short-circuits so spans are not extracted twice. Because gen_ai spans now always take the v2 path (not subject to the transaction payload-size limits that truncation works around), `shouldEnableTruncation` simplifies to `!getClient()`. The `enableTruncation` escape hatch is intentionally left in place for a follow-up. Test suites that set `streamGenAiSpans: false` to assert on `transaction.spans` are migrated to assert on the v2 span container; redundant `streamGenAiSpans: true` config lines are removed. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../suites/tracing/anthropic-ai/index.ts | 1 - .../suites/tracing/google-genai/index.ts | 1 - .../suites/tracing/langchain/index.ts | 1 - .../suites/tracing/langgraph/index.ts | 1 - .../suites/tracing/openai/index.ts | 1 - .../suites/tracing/workers-ai/index.ts | 3 - .../suites/tracing/workers-ai/test.ts | 104 +++++++----------- .../cloudflare-vercelai-v7-als/src/index.ts | 1 - .../tests/index.test.ts | 26 +++-- .../src/index.ts | 1 - .../tests/index.test.ts | 63 ++++------- .../nextjs-15/sentry.server.config.ts | 1 - .../nextjs-16/sentry.server.config.ts | 1 - .../anthropic/instrument-no-truncation.mjs | 1 - .../instrument-streaming-with-truncation.mjs | 1 - .../anthropic/instrument-streaming.mjs | 1 - .../anthropic/instrument-with-options.mjs | 1 - .../tracing/anthropic/instrument-with-pii.mjs | 1 - .../anthropic/instrument-with-truncation.mjs | 1 - .../suites/tracing/anthropic/instrument.mjs | 1 - .../google-genai/instrument-no-truncation.mjs | 1 - .../instrument-streaming-with-truncation.mjs | 1 - .../google-genai/instrument-streaming.mjs | 1 - .../google-genai/instrument-with-options.mjs | 1 - .../google-genai/instrument-with-pii.mjs | 1 - .../instrument-with-truncation.mjs | 1 - .../tracing/google-genai/instrument.mjs | 1 - .../langchain/instrument-no-truncation.mjs | 1 - .../instrument-streaming-with-truncation.mjs | 1 - .../langchain/instrument-streaming.mjs | 1 - .../tracing/langchain/instrument-with-pii.mjs | 1 - .../langchain/instrument-with-truncation.mjs | 1 - .../suites/tracing/langchain/instrument.mjs | 1 - .../langchain/v1/instrument-with-pii.mjs | 1 - .../v1/instrument-with-truncation.mjs | 1 - .../tracing/langchain/v1/instrument.mjs | 1 - .../tracing/langgraph/instrument-agent.mjs | 2 - .../langgraph/instrument-no-truncation.mjs | 1 - .../instrument-streaming-with-truncation.mjs | 1 - .../langgraph/instrument-streaming.mjs | 1 - .../tracing/langgraph/instrument-with-pii.mjs | 1 - .../suites/tracing/langgraph/instrument.mjs | 1 - .../suites/tracing/langgraph/test.ts | 96 +++++++++------- .../openai/instrument-no-truncation.mjs | 1 - .../tracing/openai/instrument-root-span.mjs | 1 - .../instrument-streaming-with-truncation.mjs | 1 - .../tracing/openai/instrument-streaming.mjs | 1 - .../openai/instrument-with-options.mjs | 1 - .../tracing/openai/instrument-with-pii.mjs | 1 - .../openai/instrument-with-truncation.mjs | 1 - .../suites/tracing/openai/instrument.mjs | 1 - .../openai-tool-calls/instrument-with-pii.mjs | 1 - .../openai/openai-tool-calls/instrument.mjs | 1 - .../openai/v6/instrument-root-span.mjs | 1 - .../openai/v6/instrument-with-options.mjs | 1 - .../tracing/openai/v6/instrument-with-pii.mjs | 1 - .../suites/tracing/openai/v6/instrument.mjs | 1 - .../vercelai/instrument-no-truncation.mjs | 1 - .../tracing/vercelai/instrument-with-pii.mjs | 1 - .../vercelai/instrument-with-truncation.mjs | 1 - .../suites/tracing/vercelai/instrument.mjs | 1 - .../vercelai/v5/instrument-with-pii.mjs | 1 - .../suites/tracing/vercelai/v5/instrument.mjs | 1 - packages/core/src/tracing/ai/utils.ts | 16 +-- .../src/tracing/spans/extractGenAiSpans.ts | 1 - packages/core/src/types/options.ts | 14 --- .../core/test/lib/tracing/ai/utils.test.ts | 16 +-- .../tracing/spans/extractGenAiSpans.test.ts | 10 +- 68 files changed, 147 insertions(+), 261 deletions(-) diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/anthropic-ai/index.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/anthropic-ai/index.ts index 384fba1965dd..c28839548208 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/anthropic-ai/index.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/anthropic-ai/index.ts @@ -27,7 +27,6 @@ export default Sentry.withSentry( (env: Env) => ({ dsn: env.SENTRY_DSN, tracesSampleRate: 1.0, - streamGenAiSpans: true, }), { async fetch(_request, _env, _ctx) { diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/google-genai/index.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/google-genai/index.ts index 635de9ebe95f..31f4c29fe859 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/google-genai/index.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/google-genai/index.ts @@ -45,7 +45,6 @@ export default Sentry.withSentry( (env: Env) => ({ dsn: env.SENTRY_DSN, tracesSampleRate: 1.0, - streamGenAiSpans: true, }), { async fetch(_request, _env, _ctx) { diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/langchain/index.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/langchain/index.ts index bae6245a01ad..006027d1b3a9 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/langchain/index.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/langchain/index.ts @@ -30,7 +30,6 @@ export default Sentry.withSentry( (env: Env) => ({ dsn: env.SENTRY_DSN, tracesSampleRate: 1.0, - streamGenAiSpans: true, }), { async fetch(_request, _env, _ctx) { diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/langgraph/index.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/langgraph/index.ts index 2c021727fcc6..8bf8f8acd720 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/langgraph/index.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/langgraph/index.ts @@ -10,7 +10,6 @@ export default Sentry.withSentry( dsn: env.SENTRY_DSN, tracesSampleRate: 1.0, dataCollection: { genAI: { inputs: true, outputs: true } }, - streamGenAiSpans: true, }), { async fetch(_request, _env, _ctx) { diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/openai/index.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/openai/index.ts index 9ef8eabc831e..0da42f9ea10b 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/openai/index.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/openai/index.ts @@ -32,7 +32,6 @@ export default Sentry.withSentry( (env: Env) => ({ dsn: env.SENTRY_DSN, tracesSampleRate: 1.0, - streamGenAiSpans: true, }), { async fetch(_request, _env, _ctx) { diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/index.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/index.ts index 9064ea1cbd4e..0cf287055c99 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/index.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/index.ts @@ -12,9 +12,6 @@ export default Sentry.withSentry( (env: Env) => ({ dsn: env.SENTRY_DSN, tracesSampleRate: 1.0, - // Keep gen_ai spans embedded in the transaction (instead of streamed as a - // separate envelope container) so they can be asserted on `transaction.spans`. - streamGenAiSpans: false, }), { async fetch(request) { diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/test.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/test.ts index e5c835f37508..080a6dcb9d6e 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/test.ts @@ -22,40 +22,29 @@ it('traces a basic Workers AI text generation request', async ({ signal }) => { .ignore('event') .expect(envelope => { const transactionEvent = envelope[1]?.[0]?.[1] as any; + expect(transactionEvent.transaction).toBe('GET /'); - // The transaction event is framework-generated and carries non-deterministic fields - // (random ports, ids, timestamps, sdk version), so we assert the stable subset. - expect(transactionEvent).toEqual( + const container = envelope[1]?.[1]?.[1] as any; + expect(container).toBeDefined(); + expect(container.items).toHaveLength(1); + + expect(container.items[0]).toEqual( expect.objectContaining({ - type: 'transaction', - transaction: 'GET /', - transaction_info: { source: 'route' }, - contexts: expect.objectContaining({ - trace: expect.objectContaining({ - op: 'http.server', - origin: 'auto.http.cloudflare', - status: 'ok', - }), - }), - spans: [ - expect.objectContaining({ - description: 'chat @cf/meta/llama-3.1-8b-instruct', - op: 'gen_ai.chat', - origin: 'auto.ai.cloudflare.workers_ai', - data: { - 'sentry.origin': 'auto.ai.cloudflare.workers_ai', - 'sentry.op': 'gen_ai.chat', - [GEN_AI_PROVIDER_NAME]: 'cloudflare.workers_ai', - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: '@cf/meta/llama-3.1-8b-instruct', - [GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]: 0.7, - [GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE]: 100, - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 12, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 7, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 19, - }, - }), - ], + name: 'chat @cf/meta/llama-3.1-8b-instruct', + status: 'ok', + is_segment: false, + attributes: { + 'sentry.origin': { value: 'auto.ai.cloudflare.workers_ai', type: 'string' }, + 'sentry.op': { value: 'gen_ai.chat', type: 'string' }, + [GEN_AI_PROVIDER_NAME]: { value: 'cloudflare.workers_ai', type: 'string' }, + [GEN_AI_OPERATION_NAME_ATTRIBUTE]: { value: 'chat', type: 'string' }, + [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: { value: '@cf/meta/llama-3.1-8b-instruct', type: 'string' }, + [GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]: { value: 0.7, type: 'double' }, + [GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE]: { value: 100, type: 'integer' }, + [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: { value: 12, type: 'integer' }, + [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: { value: 7, type: 'integer' }, + [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: { value: 19, type: 'integer' }, + }, }), ); }) @@ -69,38 +58,29 @@ it('traces a streaming Workers AI text generation request', async ({ signal }) = .ignore('event') .expect(envelope => { const transactionEvent = envelope[1]?.[0]?.[1] as any; + expect(transactionEvent.transaction).toBe('GET /stream'); - expect(transactionEvent).toEqual( + const container = envelope[1]?.[1]?.[1] as any; + expect(container).toBeDefined(); + expect(container.items).toHaveLength(1); + + expect(container.items[0]).toEqual( expect.objectContaining({ - type: 'transaction', - transaction: 'GET /stream', - transaction_info: { source: 'url' }, - contexts: expect.objectContaining({ - trace: expect.objectContaining({ - op: 'http.server', - origin: 'auto.http.cloudflare', - status: 'ok', - }), - }), - spans: [ - expect.objectContaining({ - description: 'chat @cf/meta/llama-3.1-8b-instruct', - op: 'gen_ai.chat', - origin: 'auto.ai.cloudflare.workers_ai', - data: { - 'sentry.origin': 'auto.ai.cloudflare.workers_ai', - 'sentry.op': 'gen_ai.chat', - [GEN_AI_PROVIDER_NAME]: 'cloudflare.workers_ai', - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: '@cf/meta/llama-3.1-8b-instruct', - [GEN_AI_REQUEST_STREAM_ATTRIBUTE]: true, - [GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]: true, - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 12, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 7, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 19, - }, - }), - ], + name: 'chat @cf/meta/llama-3.1-8b-instruct', + status: 'ok', + is_segment: false, + attributes: { + 'sentry.origin': { value: 'auto.ai.cloudflare.workers_ai', type: 'string' }, + 'sentry.op': { value: 'gen_ai.chat', type: 'string' }, + [GEN_AI_PROVIDER_NAME]: { value: 'cloudflare.workers_ai', type: 'string' }, + [GEN_AI_OPERATION_NAME_ATTRIBUTE]: { value: 'chat', type: 'string' }, + [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: { value: '@cf/meta/llama-3.1-8b-instruct', type: 'string' }, + [GEN_AI_REQUEST_STREAM_ATTRIBUTE]: { value: true, type: 'boolean' }, + [GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]: { value: true, type: 'boolean' }, + [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: { value: 12, type: 'integer' }, + [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: { value: 7, type: 'integer' }, + [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: { value: 19, type: 'integer' }, + }, }), ); }) diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7-als/src/index.ts b/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7-als/src/index.ts index f4110426ef99..c310afdf5658 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7-als/src/index.ts +++ b/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7-als/src/index.ts @@ -7,7 +7,6 @@ export default Sentry.withSentry( dsn: env.E2E_TEST_DSN, environment: 'qa', tunnel: 'http://localhost:3031/', - streamGenAiSpans: false, tracesSampleRate: 1.0, integrations: [Sentry.vercelAIIntegration()], }), diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7-als/tests/index.test.ts b/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7-als/tests/index.test.ts index 8aea274ca40d..078452236a44 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7-als/tests/index.test.ts +++ b/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7-als/tests/index.test.ts @@ -1,23 +1,33 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; +import { getSpanOp, waitForRequest } from '@sentry-internal/test-utils'; test('does not capture Vercel AI v7 spans without nodejs_compat', async ({ baseURL }) => { - const transactionPromise = waitForTransaction('cloudflare-vercelai-v7-als', txn => { - return txn.transaction === 'GET /generate'; + // The transaction envelope also carries any extracted gen_ai spans as a span v2 container item, + // so we wait for the whole envelope and assert neither place contains AI spans. + const envelopePromise = waitForRequest('cloudflare-vercelai-v7-als', ({ envelope }) => { + const transactionItem = envelope[1].find(([header]) => header.type === 'transaction'); + return (transactionItem?.[1] as any)?.transaction === 'GET /generate'; }); const response = await fetch(`${baseURL}/generate`); expect(response.status).toBe(200); - const transaction = await transactionPromise; + const { envelope } = await envelopePromise; + const transaction = envelope[1].find(([header]) => header.type === 'transaction')?.[1] as any; expect(transaction.transaction).toBe('GET /generate'); expect(transaction.contexts?.trace?.op).toBe('http.server'); - // v7 uses diagnostics_channel which is not available with nodejs_als, - // so no AI spans should be present. - const aiSpans = (transaction.spans || []).filter( + // v7 uses diagnostics_channel which is not available with nodejs_als, so no AI spans should be + // present — neither embedded in the transaction nor streamed as a span v2 container item. + const embeddedAiSpans = (transaction.spans || []).filter( (span: any) => span.op?.startsWith('gen_ai.') || span.description?.includes('generateText'), ); - expect(aiSpans).toHaveLength(0); + expect(embeddedAiSpans).toHaveLength(0); + + const streamedGenAiSpans = envelope[1] + .filter(([header]) => header.type === 'span') + .flatMap(([, payload]) => (payload as any).items ?? []) + .filter((span: any) => getSpanOp(span)?.startsWith('gen_ai.')); + expect(streamedGenAiSpans).toHaveLength(0); }); diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7-compat/src/index.ts b/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7-compat/src/index.ts index 91c6eae65f81..8425fcf0eac6 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7-compat/src/index.ts +++ b/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7-compat/src/index.ts @@ -8,7 +8,6 @@ export default Sentry.withSentry( environment: 'qa', tunnel: 'http://localhost:3031/', tracesSampleRate: 1.0, - streamGenAiSpans: false, integrations: [Sentry.vercelAIIntegration()], }), { diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7-compat/tests/index.test.ts b/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7-compat/tests/index.test.ts index 4b1a983c629b..3ca6c9342870 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7-compat/tests/index.test.ts +++ b/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7-compat/tests/index.test.ts @@ -1,57 +1,40 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; +import { getSpanOp, waitForStreamedSpans } from '@sentry-internal/test-utils'; test('captures Vercel AI v7 spans with nodejs_compat using tracing channels', async ({ baseURL }) => { - const transactionPromise = waitForTransaction('cloudflare-vercelai-v7-compat', txn => { - return txn.transaction === 'GET /generate'; - }); + // gen_ai spans are extracted into a separate span v2 envelope item + const genAiSpansPromise = waitForStreamedSpans('cloudflare-vercelai-v7-compat', spans => + spans.some(span => getSpanOp(span) === 'gen_ai.invoke_agent'), + ); const response = await fetch(`${baseURL}/generate`); expect(response.status).toBe(200); - const transaction = await transactionPromise; - - expect(transaction.transaction).toBe('GET /generate'); - expect(transaction.contexts?.trace?.op).toBe('http.server'); - expect(transaction.spans).toHaveLength(2); + const genAiSpans = await genAiSpansPromise; - expect(transaction.spans).toEqual( + expect(genAiSpans).toEqual( expect.arrayContaining([ expect.objectContaining({ - description: 'invoke_agent', - op: 'gen_ai.invoke_agent', - origin: 'auto.vercelai.channel', - parent_span_id: expect.any(String), - span_id: expect.any(String), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: expect.any(String), - data: expect.objectContaining({ - 'gen_ai.operation.name': 'invoke_agent', - 'gen_ai.usage.input_tokens': 10, - 'gen_ai.usage.output_tokens': 20, - 'gen_ai.usage.total_tokens': 30, - 'sentry.op': 'gen_ai.invoke_agent', - 'sentry.origin': 'auto.vercelai.channel', + name: 'invoke_agent', + attributes: expect.objectContaining({ + 'gen_ai.operation.name': { value: 'invoke_agent', type: 'string' }, + 'gen_ai.usage.input_tokens': { value: 10, type: 'integer' }, + 'gen_ai.usage.output_tokens': { value: 20, type: 'integer' }, + 'gen_ai.usage.total_tokens': { value: 30, type: 'integer' }, + 'sentry.op': { value: 'gen_ai.invoke_agent', type: 'string' }, + 'sentry.origin': { value: 'auto.vercelai.channel', type: 'string' }, }), }), expect.objectContaining({ - description: 'generate_content mock-model-id', - op: 'gen_ai.generate_content', - origin: 'auto.vercelai.channel', - parent_span_id: expect.any(String), - span_id: expect.any(String), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: expect.any(String), - data: expect.objectContaining({ - 'gen_ai.operation.name': 'generate_content', - 'gen_ai.usage.input_tokens': 10, - 'gen_ai.usage.output_tokens': 20, - 'gen_ai.usage.total_tokens': 30, - 'sentry.op': 'gen_ai.generate_content', - 'sentry.origin': 'auto.vercelai.channel', + name: 'generate_content mock-model-id', + attributes: expect.objectContaining({ + 'gen_ai.operation.name': { value: 'generate_content', type: 'string' }, + 'gen_ai.usage.input_tokens': { value: 10, type: 'integer' }, + 'gen_ai.usage.output_tokens': { value: 20, type: 'integer' }, + 'gen_ai.usage.total_tokens': { value: 30, type: 'integer' }, + 'sentry.op': { value: 'gen_ai.generate_content', type: 'string' }, + 'sentry.origin': { value: 'auto.vercelai.channel', type: 'string' }, }), }), ]), diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/sentry.server.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-15/sentry.server.config.ts index a5e60d7787c6..f15dc320f937 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-15/sentry.server.config.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/sentry.server.config.ts @@ -11,5 +11,4 @@ Sentry.init({ bufferSize: 1000, }, integrations: [Sentry.vercelAIIntegration()], - streamGenAiSpans: true, }); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/sentry.server.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/sentry.server.config.ts index 8b9eaa651f6d..9712d67e65e2 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16/sentry.server.config.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16/sentry.server.config.ts @@ -9,7 +9,6 @@ Sentry.init({ dataCollection: { userInfo: true }, // debug: true, integrations: [Sentry.vercelAIIntegration(), Sentry.nodeRuntimeMetricsIntegration({ collectionIntervalMs: 1_000 })], - streamGenAiSpans: true, // Verify Log type is available beforeSendLog(log: Log) { return log; diff --git a/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-no-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-no-truncation.mjs index db17234a62c9..1fa73dda80a2 100644 --- a/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-no-truncation.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-no-truncation.mjs @@ -21,5 +21,4 @@ Sentry.init({ } return event; }, - streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-streaming-with-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-streaming-with-truncation.mjs index f5bc3383d644..048f3de408e2 100644 --- a/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-streaming-with-truncation.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-streaming-with-truncation.mjs @@ -13,5 +13,4 @@ Sentry.init({ enableTruncation: true, }), ], - streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-streaming.mjs b/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-streaming.mjs index 1e13bd68fe06..657bed0a3a8c 100644 --- a/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-streaming.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-streaming.mjs @@ -8,5 +8,4 @@ Sentry.init({ dataCollection: { genAI: { inputs: true, outputs: true } }, transport: loggingTransport, traceLifecycle: 'stream', - streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-with-options.mjs b/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-with-options.mjs index 9818715bf760..7ab9939ce1f7 100644 --- a/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-with-options.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-with-options.mjs @@ -20,5 +20,4 @@ Sentry.init({ } return event; }, - streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-with-pii.mjs b/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-with-pii.mjs index 208730fdc71c..398e7d549ce4 100644 --- a/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-with-pii.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-with-pii.mjs @@ -14,5 +14,4 @@ Sentry.init({ } return event; }, - streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-with-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-with-truncation.mjs index 2adcdd009640..c593efd41fdb 100644 --- a/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-with-truncation.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-with-truncation.mjs @@ -15,5 +15,4 @@ Sentry.init({ } return event; }, - streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument.mjs index 2ee8a8fce693..21631b0899a3 100644 --- a/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument.mjs @@ -14,5 +14,4 @@ Sentry.init({ } return event; }, - streamGenAiSpans: true, }); 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 index 5daa0a199d49..2b1f9cfd1dca 100644 --- 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 @@ -21,5 +21,4 @@ Sentry.init({ } return event; }, - streamGenAiSpans: true, }); 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 index 88f1e7f0ebff..0e148493344a 100644 --- 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 @@ -13,5 +13,4 @@ Sentry.init({ enableTruncation: true, }), ], - streamGenAiSpans: 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 index 1e13bd68fe06..657bed0a3a8c 100644 --- 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 @@ -8,5 +8,4 @@ Sentry.init({ dataCollection: { genAI: { inputs: true, outputs: true } }, transport: loggingTransport, traceLifecycle: 'stream', - streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/google-genai/instrument-with-options.mjs b/dev-packages/node-integration-tests/suites/tracing/google-genai/instrument-with-options.mjs index 63a4584f2519..65c88a9108ea 100644 --- a/dev-packages/node-integration-tests/suites/tracing/google-genai/instrument-with-options.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/google-genai/instrument-with-options.mjs @@ -20,5 +20,4 @@ Sentry.init({ } return event; }, - streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/google-genai/instrument-with-pii.mjs b/dev-packages/node-integration-tests/suites/tracing/google-genai/instrument-with-pii.mjs index 1f9f31b35bcc..e9a8a751fded 100644 --- a/dev-packages/node-integration-tests/suites/tracing/google-genai/instrument-with-pii.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/google-genai/instrument-with-pii.mjs @@ -14,5 +14,4 @@ Sentry.init({ } return event; }, - streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/google-genai/instrument-with-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/google-genai/instrument-with-truncation.mjs index 895e7d2ffbad..7cccb1ccb487 100644 --- a/dev-packages/node-integration-tests/suites/tracing/google-genai/instrument-with-truncation.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/google-genai/instrument-with-truncation.mjs @@ -15,5 +15,4 @@ Sentry.init({ } return event; }, - streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/google-genai/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/google-genai/instrument.mjs index effe80f853e9..407194162e2e 100644 --- a/dev-packages/node-integration-tests/suites/tracing/google-genai/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/google-genai/instrument.mjs @@ -14,5 +14,4 @@ Sentry.init({ } return event; }, - streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-no-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-no-truncation.mjs index d8f1eb096f93..0a647f326044 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-no-truncation.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-no-truncation.mjs @@ -21,5 +21,4 @@ Sentry.init({ } return event; }, - streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-streaming-with-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-streaming-with-truncation.mjs index 3774b2665391..2a2365407b79 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-streaming-with-truncation.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-streaming-with-truncation.mjs @@ -13,5 +13,4 @@ Sentry.init({ enableTruncation: true, }), ], - streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-streaming.mjs b/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-streaming.mjs index 1e13bd68fe06..657bed0a3a8c 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-streaming.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-streaming.mjs @@ -8,5 +8,4 @@ Sentry.init({ dataCollection: { genAI: { inputs: true, outputs: true } }, transport: loggingTransport, traceLifecycle: 'stream', - streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-with-pii.mjs b/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-with-pii.mjs index 47c720f58d1d..36ff67811fd0 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-with-pii.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-with-pii.mjs @@ -14,5 +14,4 @@ Sentry.init({ } return event; }, - streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-with-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-with-truncation.mjs index f402bce82071..cb677ddd9926 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-with-truncation.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-with-truncation.mjs @@ -15,5 +15,4 @@ Sentry.init({ } return event; }, - streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/langchain/instrument.mjs index 84be44cf74ad..0bfc489afdab 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/langchain/instrument.mjs @@ -14,5 +14,4 @@ Sentry.init({ } return event; }, - streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/v1/instrument-with-pii.mjs b/dev-packages/node-integration-tests/suites/tracing/langchain/v1/instrument-with-pii.mjs index f76143531c55..18e541e6547f 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/v1/instrument-with-pii.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/langchain/v1/instrument-with-pii.mjs @@ -14,5 +14,4 @@ Sentry.init({ } return event; }, - streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/v1/instrument-with-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/langchain/v1/instrument-with-truncation.mjs index fd4ea4c13751..b773dbad2e6b 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/v1/instrument-with-truncation.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/langchain/v1/instrument-with-truncation.mjs @@ -15,5 +15,4 @@ Sentry.init({ } return event; }, - streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/v1/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/langchain/v1/instrument.mjs index e00ed754f8c6..ad89287dca0f 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/v1/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/langchain/v1/instrument.mjs @@ -14,5 +14,4 @@ Sentry.init({ } return event; }, - streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument-agent.mjs b/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument-agent.mjs index 12f7f0db96a2..c969422030d6 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument-agent.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument-agent.mjs @@ -6,8 +6,6 @@ Sentry.init({ release: '1.0', tracesSampleRate: 1.0, dataCollection: { genAI: { inputs: true, outputs: true } }, - // This suite asserts on gen_ai spans embedded in the transaction, so opt out of span streaming. - streamGenAiSpans: false, transport: loggingTransport, beforeSendTransaction: event => { // Filter out mock express server transactions diff --git a/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument-no-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument-no-truncation.mjs index f869054e9d3e..c3933a5ea129 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument-no-truncation.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument-no-truncation.mjs @@ -14,5 +14,4 @@ Sentry.init({ enableTruncation: false, }), ], - streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument-streaming-with-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument-streaming-with-truncation.mjs index 3650f937ce19..c76ba5928e20 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument-streaming-with-truncation.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument-streaming-with-truncation.mjs @@ -13,5 +13,4 @@ Sentry.init({ enableTruncation: true, }), ], - streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument-streaming.mjs b/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument-streaming.mjs index 1e13bd68fe06..657bed0a3a8c 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument-streaming.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument-streaming.mjs @@ -8,5 +8,4 @@ Sentry.init({ dataCollection: { genAI: { inputs: true, outputs: true } }, transport: loggingTransport, traceLifecycle: 'stream', - streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument-with-pii.mjs b/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument-with-pii.mjs index 4f7d6be20fc3..1c507ba84b9c 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument-with-pii.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument-with-pii.mjs @@ -7,5 +7,4 @@ Sentry.init({ tracesSampleRate: 1.0, dataCollection: { genAI: { inputs: true, outputs: true } }, transport: loggingTransport, - streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument.mjs index c4cc37267b72..cc192fb89834 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument.mjs @@ -7,5 +7,4 @@ Sentry.init({ tracesSampleRate: 1.0, dataCollection: { genAI: { inputs: false, outputs: false } }, transport: loggingTransport, - streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts b/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts index 817aef2923ca..f293ba29218e 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts @@ -377,27 +377,31 @@ describe('LangGraph integration', () => { transaction: event => { const spans = event.spans ?? []; expect(event.transaction).toBe('main'); - expect(spans).toHaveLength(3); + expect(spans).toContainEqual(expect.objectContaining({ op: 'http.client' })); + }, + }) + .expect({ + span: container => { + const spans = container.items; expect(spans).toContainEqual( expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'invoke_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.invoke_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.langgraph', - [GEN_AI_AGENT_NAME_ATTRIBUTE]: 'helpful_assistant', - [GEN_AI_PIPELINE_NAME_ATTRIBUTE]: 'helpful_assistant', - }), - description: 'invoke_agent helpful_assistant', - op: 'gen_ai.invoke_agent', - origin: 'auto.ai.langgraph', + name: 'invoke_agent helpful_assistant', status: 'ok', + attributes: expect.objectContaining({ + [GEN_AI_OPERATION_NAME_ATTRIBUTE]: expect.objectContaining({ value: 'invoke_agent' }), + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: expect.objectContaining({ value: 'gen_ai.invoke_agent' }), + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: expect.objectContaining({ value: 'auto.ai.langgraph' }), + [GEN_AI_AGENT_NAME_ATTRIBUTE]: expect.objectContaining({ value: 'helpful_assistant' }), + [GEN_AI_PIPELINE_NAME_ATTRIBUTE]: expect.objectContaining({ value: 'helpful_assistant' }), + }), }), ); - expect(spans).toContainEqual(expect.objectContaining({ op: 'http.client' })); expect(spans).toContainEqual( expect.objectContaining({ - data: expect.objectContaining({ [GEN_AI_AGENT_NAME_ATTRIBUTE]: 'helpful_assistant' }), - op: 'gen_ai.chat', + attributes: expect.objectContaining({ + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: expect.objectContaining({ value: 'gen_ai.chat' }), + [GEN_AI_AGENT_NAME_ATTRIBUTE]: expect.objectContaining({ value: 'helpful_assistant' }), + }), }), ); }, @@ -416,43 +420,49 @@ describe('LangGraph integration', () => { transaction: event => { const spans = event.spans ?? []; expect(event.transaction).toBe('main'); - expect(spans).toHaveLength(9); + expect(spans.filter(span => span.op === 'http.client')).toHaveLength(3); + }, + }) + .expect({ + span: container => { + const spans = container.items; expect(spans).toContainEqual( expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'invoke_agent', - [GEN_AI_AGENT_NAME_ATTRIBUTE]: 'math_assistant', - }), - op: 'gen_ai.invoke_agent', status: 'ok', + attributes: expect.objectContaining({ + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: expect.objectContaining({ value: 'gen_ai.invoke_agent' }), + [GEN_AI_OPERATION_NAME_ATTRIBUTE]: expect.objectContaining({ value: 'invoke_agent' }), + [GEN_AI_AGENT_NAME_ATTRIBUTE]: expect.objectContaining({ value: 'math_assistant' }), + }), }), ); expect(spans).toContainEqual( expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'execute_tool', - [GEN_AI_TOOL_NAME_ATTRIBUTE]: 'add', - 'gen_ai.tool.type': 'function', - }), - description: 'execute_tool add', - op: 'gen_ai.execute_tool', + name: 'execute_tool add', status: 'ok', + attributes: expect.objectContaining({ + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: expect.objectContaining({ value: 'gen_ai.execute_tool' }), + [GEN_AI_OPERATION_NAME_ATTRIBUTE]: expect.objectContaining({ value: 'execute_tool' }), + [GEN_AI_TOOL_NAME_ATTRIBUTE]: expect.objectContaining({ value: 'add' }), + 'gen_ai.tool.type': expect.objectContaining({ value: 'function' }), + }), }), ); expect(spans).toContainEqual( expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'execute_tool', - [GEN_AI_TOOL_NAME_ATTRIBUTE]: 'multiply', - 'gen_ai.tool.type': 'function', - }), - description: 'execute_tool multiply', - op: 'gen_ai.execute_tool', + name: 'execute_tool multiply', status: 'ok', + attributes: expect.objectContaining({ + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: expect.objectContaining({ value: 'gen_ai.execute_tool' }), + [GEN_AI_OPERATION_NAME_ATTRIBUTE]: expect.objectContaining({ value: 'execute_tool' }), + [GEN_AI_TOOL_NAME_ATTRIBUTE]: expect.objectContaining({ value: 'multiply' }), + 'gen_ai.tool.type': expect.objectContaining({ value: 'function' }), + }), }), ); - expect(spans.filter(span => span.op === 'http.client')).toHaveLength(3); - expect(spans.filter(span => span.op === 'gen_ai.chat')).toHaveLength(3); + expect( + spans.filter(span => span.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]?.value === 'gen_ai.chat'), + ).toHaveLength(3); }, }) .start() @@ -466,12 +476,16 @@ describe('LangGraph integration', () => { .ignore('event') .expect({ transaction: event => { - const spans = event.spans ?? []; - const chatSpans = spans.filter(s => s.op === 'gen_ai.chat'); + expect(event.transaction).toBe('main'); + }, + }) + .expect({ + span: container => { + const chatSpans = container.items.filter( + s => s.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]?.value === 'gen_ai.chat', + ); expect(chatSpans).toHaveLength(1); - expect(chatSpans[0]?.data).toMatchObject({ - [GEN_AI_AGENT_NAME_ATTRIBUTE]: 'plain_assistant', - }); + expect(chatSpans[0]?.attributes[GEN_AI_AGENT_NAME_ATTRIBUTE]?.value).toBe('plain_assistant'); }, }) .start() diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/instrument-no-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/openai/instrument-no-truncation.mjs index 11ae3216f624..442163b75b6e 100644 --- a/dev-packages/node-integration-tests/suites/tracing/openai/instrument-no-truncation.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/openai/instrument-no-truncation.mjs @@ -20,5 +20,4 @@ Sentry.init({ } return event; }, - streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/instrument-root-span.mjs b/dev-packages/node-integration-tests/suites/tracing/openai/instrument-root-span.mjs index c4cc37267b72..cc192fb89834 100644 --- a/dev-packages/node-integration-tests/suites/tracing/openai/instrument-root-span.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/openai/instrument-root-span.mjs @@ -7,5 +7,4 @@ Sentry.init({ tracesSampleRate: 1.0, dataCollection: { genAI: { inputs: false, outputs: false } }, transport: loggingTransport, - streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/instrument-streaming-with-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/openai/instrument-streaming-with-truncation.mjs index e31ae664dc52..c61dffa4c1f1 100644 --- a/dev-packages/node-integration-tests/suites/tracing/openai/instrument-streaming-with-truncation.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/openai/instrument-streaming-with-truncation.mjs @@ -13,5 +13,4 @@ Sentry.init({ enableTruncation: true, }), ], - streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/instrument-streaming.mjs b/dev-packages/node-integration-tests/suites/tracing/openai/instrument-streaming.mjs index 1e13bd68fe06..657bed0a3a8c 100644 --- a/dev-packages/node-integration-tests/suites/tracing/openai/instrument-streaming.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/openai/instrument-streaming.mjs @@ -8,5 +8,4 @@ Sentry.init({ dataCollection: { genAI: { inputs: true, outputs: true } }, transport: loggingTransport, traceLifecycle: 'stream', - streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/instrument-with-options.mjs b/dev-packages/node-integration-tests/suites/tracing/openai/instrument-with-options.mjs index a1461f7b0633..fcc876ff8a72 100644 --- a/dev-packages/node-integration-tests/suites/tracing/openai/instrument-with-options.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/openai/instrument-with-options.mjs @@ -19,5 +19,4 @@ Sentry.init({ } return event; }, - streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/instrument-with-pii.mjs b/dev-packages/node-integration-tests/suites/tracing/openai/instrument-with-pii.mjs index 3ce66a394943..e6d09440d3f8 100644 --- a/dev-packages/node-integration-tests/suites/tracing/openai/instrument-with-pii.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/openai/instrument-with-pii.mjs @@ -13,5 +13,4 @@ Sentry.init({ } return event; }, - streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/instrument-with-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/openai/instrument-with-truncation.mjs index abb1e1efdc28..cbd50b7a2262 100644 --- a/dev-packages/node-integration-tests/suites/tracing/openai/instrument-with-truncation.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/openai/instrument-with-truncation.mjs @@ -14,5 +14,4 @@ Sentry.init({ } return event; }, - streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/openai/instrument.mjs index 8dc2049189b4..39221a5e1cf5 100644 --- a/dev-packages/node-integration-tests/suites/tracing/openai/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/openai/instrument.mjs @@ -13,5 +13,4 @@ Sentry.init({ } return event; }, - streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/openai-tool-calls/instrument-with-pii.mjs b/dev-packages/node-integration-tests/suites/tracing/openai/openai-tool-calls/instrument-with-pii.mjs index 4f7d6be20fc3..1c507ba84b9c 100644 --- a/dev-packages/node-integration-tests/suites/tracing/openai/openai-tool-calls/instrument-with-pii.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/openai/openai-tool-calls/instrument-with-pii.mjs @@ -7,5 +7,4 @@ Sentry.init({ tracesSampleRate: 1.0, dataCollection: { genAI: { inputs: true, outputs: true } }, transport: loggingTransport, - streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/openai-tool-calls/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/openai/openai-tool-calls/instrument.mjs index c4cc37267b72..cc192fb89834 100644 --- a/dev-packages/node-integration-tests/suites/tracing/openai/openai-tool-calls/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/openai/openai-tool-calls/instrument.mjs @@ -7,5 +7,4 @@ Sentry.init({ tracesSampleRate: 1.0, dataCollection: { genAI: { inputs: false, outputs: false } }, transport: loggingTransport, - streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/v6/instrument-root-span.mjs b/dev-packages/node-integration-tests/suites/tracing/openai/v6/instrument-root-span.mjs index c4cc37267b72..cc192fb89834 100644 --- a/dev-packages/node-integration-tests/suites/tracing/openai/v6/instrument-root-span.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/openai/v6/instrument-root-span.mjs @@ -7,5 +7,4 @@ Sentry.init({ tracesSampleRate: 1.0, dataCollection: { genAI: { inputs: false, outputs: false } }, transport: loggingTransport, - streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/v6/instrument-with-options.mjs b/dev-packages/node-integration-tests/suites/tracing/openai/v6/instrument-with-options.mjs index 21716c6812d2..8b67890a613c 100644 --- a/dev-packages/node-integration-tests/suites/tracing/openai/v6/instrument-with-options.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/openai/v6/instrument-with-options.mjs @@ -20,5 +20,4 @@ Sentry.init({ } return event; }, - streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/v6/instrument-with-pii.mjs b/dev-packages/node-integration-tests/suites/tracing/openai/v6/instrument-with-pii.mjs index 3ce66a394943..e6d09440d3f8 100644 --- a/dev-packages/node-integration-tests/suites/tracing/openai/v6/instrument-with-pii.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/openai/v6/instrument-with-pii.mjs @@ -13,5 +13,4 @@ Sentry.init({ } return event; }, - streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/v6/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/openai/v6/instrument.mjs index 8dc2049189b4..39221a5e1cf5 100644 --- a/dev-packages/node-integration-tests/suites/tracing/openai/v6/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/openai/v6/instrument.mjs @@ -13,5 +13,4 @@ Sentry.init({ } return event; }, - streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/instrument-no-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/vercelai/instrument-no-truncation.mjs index a5f66ed4ceba..64adb199d9c6 100644 --- a/dev-packages/node-integration-tests/suites/tracing/vercelai/instrument-no-truncation.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/instrument-no-truncation.mjs @@ -14,5 +14,4 @@ Sentry.init({ enableTruncation: false, }), ], - streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/instrument-with-pii.mjs b/dev-packages/node-integration-tests/suites/tracing/vercelai/instrument-with-pii.mjs index 4f7d6be20fc3..1c507ba84b9c 100644 --- a/dev-packages/node-integration-tests/suites/tracing/vercelai/instrument-with-pii.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/instrument-with-pii.mjs @@ -7,5 +7,4 @@ Sentry.init({ tracesSampleRate: 1.0, dataCollection: { genAI: { inputs: true, outputs: true } }, transport: loggingTransport, - streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/instrument-with-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/vercelai/instrument-with-truncation.mjs index 7afab89bece4..0147e17d7b5f 100644 --- a/dev-packages/node-integration-tests/suites/tracing/vercelai/instrument-with-truncation.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/instrument-with-truncation.mjs @@ -8,5 +8,4 @@ Sentry.init({ dataCollection: { genAI: { inputs: true, outputs: true } }, transport: loggingTransport, integrations: [Sentry.vercelAIIntegration({ enableTruncation: true })], - streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/vercelai/instrument.mjs index dfe8424bb811..46a27dd03b74 100644 --- a/dev-packages/node-integration-tests/suites/tracing/vercelai/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/instrument.mjs @@ -6,5 +6,4 @@ Sentry.init({ release: '1.0', tracesSampleRate: 1.0, transport: loggingTransport, - streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/v5/instrument-with-pii.mjs b/dev-packages/node-integration-tests/suites/tracing/vercelai/v5/instrument-with-pii.mjs index 29e017aa78f4..7a021162097c 100644 --- a/dev-packages/node-integration-tests/suites/tracing/vercelai/v5/instrument-with-pii.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/v5/instrument-with-pii.mjs @@ -8,5 +8,4 @@ Sentry.init({ // inputs and outputs are enabeld by default when opting into dataCollection dataCollection: {}, transport: loggingTransport, - streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/v5/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/vercelai/v5/instrument.mjs index dfe8424bb811..46a27dd03b74 100644 --- a/dev-packages/node-integration-tests/suites/tracing/vercelai/v5/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/v5/instrument.mjs @@ -6,5 +6,4 @@ Sentry.init({ release: '1.0', tracesSampleRate: 1.0, transport: loggingTransport, - streamGenAiSpans: true, }); diff --git a/packages/core/src/tracing/ai/utils.ts b/packages/core/src/tracing/ai/utils.ts index fd762e61fbe3..ff9e072bf271 100644 --- a/packages/core/src/tracing/ai/utils.ts +++ b/packages/core/src/tracing/ai/utils.ts @@ -3,7 +3,6 @@ */ import { captureException } from '../../exports'; import { getClient } from '../../currentScopes'; -import { hasSpanStreamingEnabled } from '../spans/hasSpanStreamingEnabled'; import type { Span } from '../../types/span'; import { isThenable } from '../../utils/is'; import { @@ -60,22 +59,17 @@ export function resolveAIRecordingOptions(options? /** * Resolves whether truncation should be enabled. * If the user explicitly set `enableTruncation`, that value is used. - * 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. `streamGenAiSpans` is opt-out (on unless explicitly set to `false`). + * Otherwise, truncation is disabled because gen_ai spans are always sent through the v2 span path + * (full span streaming via `traceLifecycle: 'stream'`, or extraction into a v2 span envelope for + * static transactions). 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(); - if (!client) { - return true; - } - - return !hasSpanStreamingEnabled(client) && client.getOptions().streamGenAiSpans === false; + return !getClient(); } /** diff --git a/packages/core/src/tracing/spans/extractGenAiSpans.ts b/packages/core/src/tracing/spans/extractGenAiSpans.ts index 73bce236f7f0..05aaa3ff3e12 100644 --- a/packages/core/src/tracing/spans/extractGenAiSpans.ts +++ b/packages/core/src/tracing/spans/extractGenAiSpans.ts @@ -20,7 +20,6 @@ export function extractGenAiSpansFromEvent(event: Event, client: Client): SpanCo event.type !== 'transaction' || !event.spans?.length || !event.sdkProcessingMetadata?.hasGenAiSpans || - client.getOptions().streamGenAiSpans === false || hasSpanStreamingEnabled(client) ) { return undefined; diff --git a/packages/core/src/types/options.ts b/packages/core/src/types/options.ts index 3d55c5f17498..39dd00f38e0f 100644 --- a/packages/core/src/types/options.ts +++ b/packages/core/src/types/options.ts @@ -564,20 +564,6 @@ export interface ClientOptions { expect(shouldEnableTruncation(undefined)).toBe(true); }); - it('defaults to false with a default client (streamGenAiSpans is opt-out)', () => { + it('defaults to false with a default client', () => { setupClient(); expect(shouldEnableTruncation(undefined)).toBe(false); }); - it('defaults to true when streamGenAiSpans is explicitly disabled', () => { - setupClient({ streamGenAiSpans: false }); - expect(shouldEnableTruncation(undefined)).toBe(true); - }); - - it('defaults to false when streamGenAiSpans is enabled', () => { - setupClient({ streamGenAiSpans: true }); - expect(shouldEnableTruncation(undefined)).toBe(false); - }); - it('defaults to false when span streaming is enabled (traceLifecycle: stream)', () => { setupClient({ traceLifecycle: 'stream' }); expect(shouldEnableTruncation(undefined)).toBe(false); }); - it('explicit enableTruncation: true overrides streamGenAiSpans', () => { - setupClient({ streamGenAiSpans: true }); + it('explicit enableTruncation: true overrides the default', () => { + setupClient(); expect(shouldEnableTruncation(true)).toBe(true); }); diff --git a/packages/core/test/lib/tracing/spans/extractGenAiSpans.test.ts b/packages/core/test/lib/tracing/spans/extractGenAiSpans.test.ts index de47457144d3..f21e164dccba 100644 --- a/packages/core/test/lib/tracing/spans/extractGenAiSpans.test.ts +++ b/packages/core/test/lib/tracing/spans/extractGenAiSpans.test.ts @@ -120,7 +120,7 @@ describe('extractGenAiSpansFromEvent', () => { expect(extractGenAiSpansFromEvent(event, makeClient())).toBeUndefined(); }); - it('extracts gen_ai spans by default when streamGenAiSpans is unset', () => { + it('extracts gen_ai spans by default', () => { const event = makeTransactionEvent([makeSpanJSON({ op: 'gen_ai.chat', span_id: 'genai001' })], true); const result = extractGenAiSpansFromEvent(event, makeClient()); @@ -130,14 +130,6 @@ describe('extractGenAiSpansFromEvent', () => { expect(event.spans).toHaveLength(0); }); - it('returns undefined when streamGenAiSpans is explicitly disabled', () => { - const event = makeTransactionEvent([makeSpanJSON({ op: 'gen_ai.chat' })]); - const client = makeClient({ streamGenAiSpans: false }); - - expect(extractGenAiSpansFromEvent(event, client)).toBeUndefined(); - expect(event.spans).toHaveLength(1); - }); - it('returns undefined when span streaming is enabled', () => { const event = makeTransactionEvent([makeSpanJSON({ op: 'gen_ai.chat' })]); const client = makeClient({ traceLifecycle: 'stream' }); From 4fa02f8afd551cf81680ced4d21b2448bbf517ad Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Thu, 23 Jul 2026 12:06:32 +0200 Subject: [PATCH 2/4] feat(core)!: Remove `sentry.sdk_meta.gen_ai.input.messages.original_length` attribute This internal SDK-meta span attribute only ever signaled how many gen_ai input messages existed before truncation dropped some. It carries no meaning for consumers on its own and is being removed ahead of the truncation removal. Text truncation itself is unchanged by this commit. Co-Authored-By: Claude Opus 4.8 (1M context) --- MIGRATION.md | 1 + .../suites/tracing/anthropic/test.ts | 6 --- .../suites/tracing/google-genai/test.ts | 4 -- .../suites/tracing/langchain/test.ts | 20 ++++---- .../suites/tracing/langchain/v1/test.ts | 19 ++++--- .../suites/tracing/langgraph/test.ts | 2 - .../tracing/openai/openai-tool-calls/test.ts | 17 ------- .../suites/tracing/openai/test.ts | 49 ------------------- .../suites/tracing/openai/v6/test.ts | 35 ------------- .../vercelai/span-streaming-v4/test.ts | 2 - .../vercelai/span-streaming-v6/test.ts | 2 - .../suites/tracing/vercelai/test.ts | 8 --- packages/core/src/server-exports.ts | 6 +-- .../core/src/tracing/ai/gen-ai-attributes.ts | 5 -- .../core/src/tracing/anthropic-ai/utils.ts | 11 +---- .../core/src/tracing/google-genai/index.ts | 3 -- packages/core/src/tracing/langchain/utils.ts | 5 -- packages/core/src/tracing/langgraph/index.ts | 3 -- packages/core/src/tracing/openai/index.ts | 7 --- packages/core/src/tracing/vercel-ai/utils.ts | 6 --- packages/core/src/tracing/workers-ai/utils.ts | 6 --- .../vercel-ai-request-messages.test.ts | 4 -- .../test/lib/utils/anthropic-utils.test.ts | 3 -- .../src/vercel-ai/vercel-ai-dc-subscriber.ts | 3 -- 24 files changed, 26 insertions(+), 201 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index fd633ac0d9ea..d37a78304379 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -393,6 +393,7 @@ Sentry.init({ ### AI integrations - The `enableTruncation` and `streamGenAiSpans` flags were removed. The new default is no truncation and to always stream gen AI spans. +- The internal `sentry.sdk_meta.gen_ai.input.messages.original_length` span attribute was removed. - (Vercel AI) The internal JSON-stringify workaround for array span attributes was removed. - AI integrations are no longer available in the browser SDK. They remain available in the server-side SDKs. diff --git a/dev-packages/node-integration-tests/suites/tracing/anthropic/test.ts b/dev-packages/node-integration-tests/suites/tracing/anthropic/test.ts index 5546aa8abd66..43dd96dfce5c 100644 --- a/dev-packages/node-integration-tests/suites/tracing/anthropic/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/anthropic/test.ts @@ -1,7 +1,6 @@ import { afterAll, describe, expect } from 'vitest'; import { GEN_AI_INPUT_MESSAGES_ATTRIBUTE, - GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE, GEN_AI_OPERATION_NAME_ATTRIBUTE, GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE, GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE, @@ -634,7 +633,6 @@ describe('Anthropic integration', () => { expect(truncatedSpan!.attributes['sentry.origin'].value).toBe('auto.ai.anthropic'); expect(truncatedSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); expect(truncatedSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('claude-3-haiku-20240307'); - expect(truncatedSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE].value).toBe(3); const smallMessageSpan = container.items.find( span => span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value === smallMsgValue, @@ -649,7 +647,6 @@ describe('Anthropic integration', () => { expect(smallMessageSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe( 'claude-3-haiku-20240307', ); - expect(smallMessageSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE].value).toBe(3); }, }) .start() @@ -700,7 +697,6 @@ describe('Anthropic integration', () => { 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() @@ -767,13 +763,11 @@ describe('Anthropic integration', () => { span => span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value === expectedAllMessages, ); expect(conversationSpan).toBeDefined(); - expect(conversationSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE].value).toBe(3); const longStringSpan = container.items.find( span => span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value === expectedLongString, ); expect(longStringSpan).toBeDefined(); - expect(longStringSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE].value).toBe(1); }, }) .start() 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 3332ff1d862c..81b983ea4231 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 @@ -3,7 +3,6 @@ import { GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE, GEN_AI_INPUT_MESSAGES_ATTRIBUTE, GEN_AI_OPERATION_NAME_ATTRIBUTE, - GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE, GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE, GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE, GEN_AI_REQUEST_MODEL_ATTRIBUTE, @@ -368,7 +367,6 @@ describe('Google GenAI integration', () => { expect(truncatedSpan!.name).toBe('generate_content gemini-1.5-flash'); expect(truncatedSpan!.status).toBe('ok'); expect(truncatedSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('generate_content'); - expect(truncatedSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE].value).toBe(3); const smallMessageSpan = container.items.find( span => @@ -384,7 +382,6 @@ describe('Google GenAI integration', () => { expect(smallMessageSpan!.name).toBe('generate_content gemini-1.5-flash'); expect(smallMessageSpan!.status).toBe('ok'); expect(smallMessageSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('generate_content'); - expect(smallMessageSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE].value).toBe(3); }, }) .start() @@ -532,7 +529,6 @@ describe('Google GenAI integration', () => { { role: 'user', parts: [{ text: 'Follow-up question' }] }, ]), ); - expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE].value).toBe(3); }, }) .start() diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/test.ts b/dev-packages/node-integration-tests/suites/tracing/langchain/test.ts index ed342ce9d1a2..d2b94f200c62 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/langchain/test.ts @@ -3,7 +3,6 @@ import { GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE, GEN_AI_EMBEDDINGS_OPERATION_ATTRIBUTE, GEN_AI_INPUT_MESSAGES_ATTRIBUTE, - GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE, GEN_AI_OPERATION_NAME_ATTRIBUTE, GEN_AI_REQUEST_DIMENSIONS_ATTRIBUTE, GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE, @@ -210,20 +209,25 @@ describe('LangChain integration', () => { .expect({ span: container => { expect(container.items).toHaveLength(3); + // The string-input span has no system message (and therefore no system instructions), + // while the array-input span does — use that to distinguish the two truncated spans. + const truncatedContent = /^\[\{"role":"user","content":"C+"\}\]$/; const stringInputSpan = container.items.find( - span => span.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]?.value === 1, + span => + span.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE] === undefined && + getStringAttributeValue(span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value)?.match( + truncatedContent, + ), ); expect(stringInputSpan).toBeDefined(); expect(stringInputSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); - expect(stringInputSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toMatch( - /^\[\{"role":"user","content":"C+"\}\]$/, - ); + expect(stringInputSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toMatch(truncatedContent); const arrayInputSpan = container.items.find( span => - span.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]?.value === 2 && + span.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE] !== undefined && getStringAttributeValue(span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value)?.match( - /^\[\{"role":"user","content":"C+"\}\]$/, + truncatedContent, ), ); expect(arrayInputSpan).toBeDefined(); @@ -237,7 +241,6 @@ describe('LangChain integration', () => { ); expect(smallMessageSpan).toBeDefined(); expect(smallMessageSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); - expect(smallMessageSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE].value).toBe(2); expect(smallMessageSpan!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE]).toBeDefined(); }, }) @@ -476,7 +479,6 @@ describe('LangChain integration', () => { { role: 'user', content: 'Follow-up question' }, ]), ); - expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE].value).toBe(3); }, }) .start() diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/v1/test.ts b/dev-packages/node-integration-tests/suites/tracing/langchain/v1/test.ts index b555e48229e4..9d5c5e7ba1a3 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/v1/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/langchain/v1/test.ts @@ -1,7 +1,6 @@ import { afterAll, expect } from 'vitest'; import { GEN_AI_INPUT_MESSAGES_ATTRIBUTE, - GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE, GEN_AI_OPERATION_NAME_ATTRIBUTE, GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE, GEN_AI_REQUEST_MODEL_ATTRIBUTE, @@ -226,20 +225,25 @@ conditionalTest({ min: 20 })('LangChain integration (v1)', () => { .expect({ span: container => { expect(container.items).toHaveLength(3); + // The string-input span has no system message (and therefore no system instructions), + // while the array-input span does — use that to distinguish the two truncated spans. + const truncatedContent = /^\[\{"role":"user","content":"C+"\}\]$/; const stringInputSpan = container.items.find( - span => span.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]?.value === 1, + span => + span.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE] === undefined && + getStringAttributeValue(span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value)?.match( + truncatedContent, + ), ); expect(stringInputSpan).toBeDefined(); expect(stringInputSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); - expect(stringInputSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toMatch( - /^\[\{"role":"user","content":"C+"\}\]$/, - ); + expect(stringInputSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toMatch(truncatedContent); const arrayInputSpan = container.items.find( span => - span.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]?.value === 2 && + span.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE] !== undefined && getStringAttributeValue(span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value)?.match( - /^\[\{"role":"user","content":"C+"\}\]$/, + truncatedContent, ), ); expect(arrayInputSpan).toBeDefined(); @@ -255,7 +259,6 @@ conditionalTest({ min: 20 })('LangChain integration (v1)', () => { ); expect(smallMessageSpan).toBeDefined(); expect(smallMessageSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); - expect(smallMessageSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE].value).toBe(2); expect(smallMessageSpan!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE].value).toMatch( /^\[\{"type":"text","content":"A+"\}\]$/, ); diff --git a/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts b/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts index f293ba29218e..ed104fa52933 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts @@ -4,7 +4,6 @@ import { GEN_AI_AGENT_NAME_ATTRIBUTE, GEN_AI_CONVERSATION_ID_ATTRIBUTE, GEN_AI_INPUT_MESSAGES_ATTRIBUTE, - GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE, GEN_AI_OPERATION_NAME_ATTRIBUTE, GEN_AI_PIPELINE_NAME_ATTRIBUTE, GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE, @@ -305,7 +304,6 @@ describe('LangGraph integration', () => { expect(invokeAgentSpan).toBeDefined(); expect(invokeAgentSpan!.name).toBe('invoke_agent weather_assistant'); - expect(invokeAgentSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE].value).toBe(3); }, }) .start() diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/openai-tool-calls/test.ts b/dev-packages/node-integration-tests/suites/tracing/openai/openai-tool-calls/test.ts index 0895e6dffd19..7caf0d745237 100644 --- a/dev-packages/node-integration-tests/suites/tracing/openai/openai-tool-calls/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/openai/openai-tool-calls/test.ts @@ -2,7 +2,6 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from ' import { afterAll, describe, expect } from 'vitest'; import { GEN_AI_INPUT_MESSAGES_ATTRIBUTE, - GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE, GEN_AI_OPERATION_NAME_ATTRIBUTE, GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE, GEN_AI_REQUEST_MODEL_ATTRIBUTE, @@ -355,10 +354,6 @@ describe('OpenAI Tool Calls integration', () => { type: 'string', value: 'gpt-4', }); - expect(chatToolsSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ - type: 'integer', - value: 1, - }); expect(chatToolsSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ type: 'string', value: '[{"role":"user","content":"What is the weather like in Paris today?"}]', @@ -430,10 +425,6 @@ describe('OpenAI Tool Calls integration', () => { type: 'boolean', value: true, }); - expect(streamingChatToolsSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ - type: 'integer', - value: 1, - }); expect(streamingChatToolsSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ type: 'string', value: '[{"role":"user","content":"What is the weather like in Paris today?"}]', @@ -502,10 +493,6 @@ describe('OpenAI Tool Calls integration', () => { value: 'gpt-4', }); expect(responsesToolsSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toBeUndefined(); - expect(responsesToolsSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ - type: 'integer', - value: 1, - }); expect(responsesToolsSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ type: 'string', value: '[{"role":"user","content":"What is the weather like in Paris today?"}]', @@ -573,10 +560,6 @@ describe('OpenAI Tool Calls integration', () => { type: 'boolean', value: true, }); - expect(streamingResponsesToolsSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ - type: 'integer', - value: 1, - }); expect(streamingResponsesToolsSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ type: 'string', value: '[{"role":"user","content":"What is the weather like in Paris today?"}]', diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/test.ts b/dev-packages/node-integration-tests/suites/tracing/openai/test.ts index 3f102d360e40..bbb93952136a 100644 --- a/dev-packages/node-integration-tests/suites/tracing/openai/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/openai/test.ts @@ -4,7 +4,6 @@ import { GEN_AI_CONVERSATION_ID_ATTRIBUTE, GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE, GEN_AI_INPUT_MESSAGES_ATTRIBUTE, - GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE, GEN_AI_OPERATION_NAME_ATTRIBUTE, GEN_AI_REQUEST_DIMENSIONS_ATTRIBUTE, GEN_AI_REQUEST_ENCODING_FORMAT_ATTRIBUTE, @@ -368,10 +367,6 @@ describe('OpenAI integration', () => { type: 'double', value: 0.7, }); - expect(chatCompletionSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ - type: 'integer', - value: 1, - }); expect(chatCompletionSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ type: 'string', value: '[{"role":"user","content":"What is the capital of France?"}]', @@ -432,10 +427,6 @@ describe('OpenAI integration', () => { type: 'string', value: 'gpt-3.5-turbo', }); - expect(responsesSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ - type: 'integer', - value: 1, - }); expect(responsesSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ type: 'string', value: 'Translate this to French: Hello', @@ -497,10 +488,6 @@ describe('OpenAI integration', () => { type: 'string', value: 'error-model', }); - expect(nonStreamingErrorSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ - type: 'integer', - value: 1, - }); expect(nonStreamingErrorSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ type: 'string', value: '[{"role":"user","content":"This will fail"}]', @@ -540,10 +527,6 @@ describe('OpenAI integration', () => { type: 'boolean', value: true, }); - expect(streamingChatCompletionSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ - type: 'integer', - value: 1, - }); expect(streamingChatCompletionSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ type: 'string', value: '[{"role":"user","content":"Tell me about streaming"}]', @@ -615,10 +598,6 @@ describe('OpenAI integration', () => { type: 'boolean', value: true, }); - expect(streamingResponsesSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ - type: 'integer', - value: 1, - }); expect(streamingResponsesSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ type: 'string', value: 'Test streaming responses API', @@ -675,10 +654,6 @@ describe('OpenAI integration', () => { type: 'boolean', value: true, }); - expect(streamingErrorSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ - type: 'integer', - value: 1, - }); expect(streamingErrorSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ type: 'string', value: '[{"role":"user","content":"This will fail"}]', @@ -781,10 +756,6 @@ describe('OpenAI integration', () => { { role: 'user', content: 'Follow-up question' }, ]), }); - expect(chatCompletionSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toMatchObject({ - type: 'integer', - value: 3, - }); const responsesSpan = container.items.find( span => span.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]?.value === 'resp_mock456', @@ -798,10 +769,6 @@ describe('OpenAI integration', () => { type: 'string', value: 'B'.repeat(50_000), }); - expect(responsesSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toMatchObject({ - type: 'integer', - value: 1, - }); }, }) .start() @@ -1220,10 +1187,6 @@ describe('OpenAI integration', () => { type: 'string', value: 'gpt-3.5-turbo', }); - expect(truncatedMessageSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ - type: 'integer', - value: 2, - }); expect(truncatedMessageSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toMatch( /^\[\{"role":"user","content":"C+"\}\]$/, ); @@ -1265,10 +1228,6 @@ describe('OpenAI integration', () => { { role: 'user', content: 'This is a small message that fits within the limit' }, ]), }); - expect(smallMessageSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ - type: 'integer', - value: 2, - }); expect(smallMessageSpan!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE].value).toMatch( /^\[\{"type":"text","content":"A+"\}\]$/, ); @@ -1315,10 +1274,6 @@ describe('OpenAI integration', () => { type: 'string', value: 'gpt-3.5-turbo', }); - expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ - type: 'integer', - value: 1, - }); expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toMatch(/^A+$/); }, }) @@ -1627,10 +1582,6 @@ describe('OpenAI integration', () => { expect(span!.status).toBe('ok'); expect(span!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); expect(span!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-4o' }); - expect(span!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ - type: 'integer', - value: 1, - }); expect(span!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toContain('[Blob substitute]'); } }, diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/v6/test.ts b/dev-packages/node-integration-tests/suites/tracing/openai/v6/test.ts index 0530d1575845..0c44b5b02be7 100644 --- a/dev-packages/node-integration-tests/suites/tracing/openai/v6/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/openai/v6/test.ts @@ -3,7 +3,6 @@ import { afterAll, describe, expect } from 'vitest'; import { GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE, GEN_AI_INPUT_MESSAGES_ATTRIBUTE, - GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE, GEN_AI_OPERATION_NAME_ATTRIBUTE, GEN_AI_REQUEST_DIMENSIONS_ATTRIBUTE, GEN_AI_REQUEST_ENCODING_FORMAT_ATTRIBUTE, @@ -381,10 +380,6 @@ describe('OpenAI integration (V6)', () => { type: 'double', value: 0.7, }); - expect(chatCompletionSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ - type: 'integer', - value: 1, - }); expect(chatCompletionSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ type: 'string', value: '[{"role":"user","content":"What is the capital of France?"}]', @@ -445,10 +440,6 @@ describe('OpenAI integration (V6)', () => { type: 'string', value: 'gpt-3.5-turbo', }); - expect(responsesSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ - type: 'integer', - value: 1, - }); expect(responsesSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ type: 'string', value: 'Translate this to French: Hello', @@ -510,10 +501,6 @@ describe('OpenAI integration (V6)', () => { type: 'string', value: 'error-model', }); - expect(nonStreamingErrorSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ - type: 'integer', - value: 1, - }); expect(nonStreamingErrorSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ type: 'string', value: '[{"role":"user","content":"This will fail"}]', @@ -553,10 +540,6 @@ describe('OpenAI integration (V6)', () => { type: 'boolean', value: true, }); - expect(streamingChatCompletionSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ - type: 'integer', - value: 1, - }); expect(streamingChatCompletionSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ type: 'string', value: '[{"role":"user","content":"Tell me about streaming"}]', @@ -628,10 +611,6 @@ describe('OpenAI integration (V6)', () => { type: 'boolean', value: true, }); - expect(streamingResponsesSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ - type: 'integer', - value: 1, - }); expect(streamingResponsesSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ type: 'string', value: 'Test streaming responses API', @@ -688,10 +667,6 @@ describe('OpenAI integration (V6)', () => { type: 'boolean', value: true, }); - expect(streamingErrorSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ - type: 'integer', - value: 1, - }); expect(streamingErrorSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ type: 'string', value: '[{"role":"user","content":"This will fail"}]', @@ -738,10 +713,6 @@ describe('OpenAI integration (V6)', () => { ); expect(chatCompletionSpan).toBeDefined(); expect(chatCompletionSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toBeUndefined(); - expect(chatCompletionSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toMatchObject({ - type: 'integer', - value: 1, - }); expect(chatCompletionSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toMatchObject({ type: 'string', value: expect.any(String), @@ -763,12 +734,6 @@ describe('OpenAI integration (V6)', () => { type: 'boolean', value: true, }); - expect( - streamingChatCompletionSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE], - ).toMatchObject({ - type: 'integer', - value: 1, - }); expect(streamingChatCompletionSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toMatchObject({ type: 'string', value: expect.any(String), diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/span-streaming-v4/test.ts b/dev-packages/node-integration-tests/suites/tracing/vercelai/span-streaming-v4/test.ts index f28559c4b9e9..f522f7766822 100644 --- a/dev-packages/node-integration-tests/suites/tracing/vercelai/span-streaming-v4/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/span-streaming-v4/test.ts @@ -2,7 +2,6 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from ' import { afterAll, describe, expect } from 'vitest'; import { GEN_AI_INPUT_MESSAGES_ATTRIBUTE, - GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE, GEN_AI_OPERATION_NAME_ATTRIBUTE, GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE, GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE, @@ -141,7 +140,6 @@ describe('Vercel AI integration (streaming v4)', () => { name: 'invoke_agent', status: 'ok', attributes: expect.objectContaining({ - [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: attr(1), [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: attr('[{"role":"user","content":"Where is the first span?"}]'), [GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE]: attr( '[{"role":"assistant","parts":[{"type":"text","content":"First span here!"}],"finish_reason":"stop"}]', diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/span-streaming-v6/test.ts b/dev-packages/node-integration-tests/suites/tracing/vercelai/span-streaming-v6/test.ts index 2e8ccc0c1602..68e6660524e4 100644 --- a/dev-packages/node-integration-tests/suites/tracing/vercelai/span-streaming-v6/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/span-streaming-v6/test.ts @@ -2,7 +2,6 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from ' import { afterAll, describe, expect } from 'vitest'; import { GEN_AI_INPUT_MESSAGES_ATTRIBUTE, - GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE, GEN_AI_OPERATION_NAME_ATTRIBUTE, GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE, GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE, @@ -135,7 +134,6 @@ describe('Vercel AI integration (streaming, v6)', () => { name: 'invoke_agent', status: 'ok', attributes: expect.objectContaining({ - [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: attr(1), [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: attr('[{"role":"user","content":"Where is the first span?"}]'), [GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE]: attr( '[{"role":"assistant","parts":[{"type":"text","content":"First span here!"}],"finish_reason":"stop"}]', diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/test.ts b/dev-packages/node-integration-tests/suites/tracing/vercelai/test.ts index b759495545cc..1c79760227c9 100644 --- a/dev-packages/node-integration-tests/suites/tracing/vercelai/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/test.ts @@ -3,7 +3,6 @@ import { afterAll, describe, expect } from 'vitest'; import { GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE, GEN_AI_INPUT_MESSAGES_ATTRIBUTE, - GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE, GEN_AI_OPERATION_NAME_ATTRIBUTE, GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE, GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE, @@ -501,9 +500,6 @@ describe('Vercel AI integration (v4)', () => { expect(truncatedInvokeAgentSpan).toBeDefined(); expect(truncatedInvokeAgentSpan!.name).toBe('invoke_agent'); expect(truncatedInvokeAgentSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); - expect(truncatedInvokeAgentSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE].value).toBe( - 3, - ); expect(truncatedInvokeAgentSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toMatch( /^\[.*"(?:text|content)":"C+".*\]$/, ); @@ -518,9 +514,6 @@ describe('Vercel AI integration (v4)', () => { expect(smallMessageInvokeAgentSpan).toBeDefined(); expect(smallMessageInvokeAgentSpan!.name).toBe('invoke_agent'); expect(smallMessageInvokeAgentSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); - expect( - smallMessageInvokeAgentSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE].value, - ).toBe(3); expect(smallMessageInvokeAgentSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toContain( 'This is a small message that fits within the limit', ); @@ -655,7 +648,6 @@ describe('Vercel AI integration (v4)', () => { { role: 'user', content: 'Follow-up question' }, ]), ); - expect(invokeAgentSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE].value).toBe(3); const generateContentSpan = container.items.find(span => span.name === 'generate_content mock-model-id'); expect(generateContentSpan).toBeDefined(); diff --git a/packages/core/src/server-exports.ts b/packages/core/src/server-exports.ts index 32b54720a83e..8288320bbed9 100644 --- a/packages/core/src/server-exports.ts +++ b/packages/core/src/server-exports.ts @@ -58,11 +58,7 @@ export type { // avoid shipping the AI tracing code in browser bundles. export { addVercelAiProcessors, getProviderMetadataAttributes } from './tracing/vercel-ai'; export { getTruncatedJsonString, shouldEnableTruncation, resolveAIRecordingOptions } from './tracing/ai/utils'; -export { - GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE, - GEN_AI_REQUEST_MODEL_ATTRIBUTE, - GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE, -} from './tracing/ai/gen-ai-attributes'; +export { GEN_AI_REQUEST_MODEL_ATTRIBUTE, GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE } from './tracing/ai/gen-ai-attributes'; export { _INTERNAL_getSpanContextForToolCallId, _INTERNAL_cleanupToolCallSpanContext } from './tracing/vercel-ai/utils'; export { toolCallSpanContextMap as _INTERNAL_toolCallSpanContextMap } from './tracing/vercel-ai/constants'; export { diff --git a/packages/core/src/tracing/ai/gen-ai-attributes.ts b/packages/core/src/tracing/ai/gen-ai-attributes.ts index 16cdfa17b4b3..526715ac9c2f 100644 --- a/packages/core/src/tracing/ai/gen-ai-attributes.ts +++ b/packages/core/src/tracing/ai/gen-ai-attributes.ts @@ -115,11 +115,6 @@ export const GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE = 'gen_ai.usage.total_tokens'; */ export const GEN_AI_OPERATION_NAME_ATTRIBUTE = 'gen_ai.operation.name'; -/** - * Original length of messages array, used to indicate truncations had occured - */ -export const GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE = 'sentry.sdk_meta.gen_ai.input.messages.original_length'; - /** * The prompt messages * Only recorded when recordInputs is enabled diff --git a/packages/core/src/tracing/anthropic-ai/utils.ts b/packages/core/src/tracing/anthropic-ai/utils.ts index 5fbde951343f..5989247c111c 100644 --- a/packages/core/src/tracing/anthropic-ai/utils.ts +++ b/packages/core/src/tracing/anthropic-ai/utils.ts @@ -2,18 +2,13 @@ import { captureException } from '../../exports'; import { SPAN_STATUS_ERROR } from '../../tracing'; import type { Span } from '../../types/span'; import type { SpanStatusType } from '../../types/spanStatus'; -import { - GEN_AI_INPUT_MESSAGES_ATTRIBUTE, - GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE, - GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE, -} from '../ai/gen-ai-attributes'; +import { GEN_AI_INPUT_MESSAGES_ATTRIBUTE, GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE } from '../ai/gen-ai-attributes'; import { extractSystemInstructions, getTruncatedJsonString } from '../ai/utils'; import { stringify } from '../../utils/string'; import type { AnthropicAiResponse } from './types'; /** - * Set the messages and messages original length attributes. - * Extracts system instructions before truncation. + * Set the input messages attribute, extracting system instructions before truncation. */ export function setMessagesAttribute(span: Span, messages: unknown, enableTruncation: boolean): void { if (Array.isArray(messages) && messages.length === 0) { @@ -28,12 +23,10 @@ export function setMessagesAttribute(span: Span, messages: unknown, enableTrunca }); } - const filteredLength = Array.isArray(filteredMessages) ? filteredMessages.length : 1; span.setAttributes({ [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: enableTruncation ? getTruncatedJsonString(filteredMessages) : stringify(filteredMessages), - [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: filteredLength, }); } diff --git a/packages/core/src/tracing/google-genai/index.ts b/packages/core/src/tracing/google-genai/index.ts index 68e4d414586a..1909a0903764 100644 --- a/packages/core/src/tracing/google-genai/index.ts +++ b/packages/core/src/tracing/google-genai/index.ts @@ -8,7 +8,6 @@ import { handleCallbackErrors } from '../../utils/handleCallbackErrors'; import { GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE, GEN_AI_INPUT_MESSAGES_ATTRIBUTE, - GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE, GEN_AI_OPERATION_NAME_ATTRIBUTE, GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE, GEN_AI_REQUEST_FREQUENCY_PENALTY_ATTRIBUTE, @@ -192,9 +191,7 @@ export function addPrivateRequestAttributes( span.setAttribute(GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE, systemInstructions); } - const filteredLength = Array.isArray(filteredMessages) ? filteredMessages.length : 0; span.setAttributes({ - [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: filteredLength, [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: enableTruncation ? getTruncatedJsonString(filteredMessages) : stringify(filteredMessages), diff --git a/packages/core/src/tracing/langchain/utils.ts b/packages/core/src/tracing/langchain/utils.ts index 0f95464788da..db5fffa0e000 100644 --- a/packages/core/src/tracing/langchain/utils.ts +++ b/packages/core/src/tracing/langchain/utils.ts @@ -4,7 +4,6 @@ import { stringify } from '../../utils/string'; import { GEN_AI_AGENT_NAME_ATTRIBUTE, GEN_AI_INPUT_MESSAGES_ATTRIBUTE, - GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE, GEN_AI_OPERATION_NAME_ATTRIBUTE, GEN_AI_REQUEST_FREQUENCY_PENALTY_ATTRIBUTE, GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE, @@ -288,7 +287,6 @@ export function extractLLMRequestAttributes( const attrs = baseRequestAttributes(system, modelName, llm, invocationParams, langSmithMetadata); if (recordInputs && Array.isArray(prompts) && prompts.length > 0) { - setIfDefined(attrs, GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE, prompts.length); const messages = prompts.map(p => ({ role: 'user', content: p })); setIfDefined( attrs, @@ -331,9 +329,6 @@ export function extractChatModelRequestAttributes( setIfDefined(attrs, GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE, systemInstructions); } - const filteredLength = Array.isArray(filteredMessages) ? filteredMessages.length : 0; - setIfDefined(attrs, GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE, filteredLength); - setIfDefined( attrs, GEN_AI_INPUT_MESSAGES_ATTRIBUTE, diff --git a/packages/core/src/tracing/langgraph/index.ts b/packages/core/src/tracing/langgraph/index.ts index bcbd120b38e1..ea6653e28bd1 100644 --- a/packages/core/src/tracing/langgraph/index.ts +++ b/packages/core/src/tracing/langgraph/index.ts @@ -5,7 +5,6 @@ import { GEN_AI_AGENT_NAME_ATTRIBUTE, GEN_AI_CONVERSATION_ID_ATTRIBUTE, GEN_AI_INPUT_MESSAGES_ATTRIBUTE, - GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE, GEN_AI_INVOKE_AGENT_OPERATION_ATTRIBUTE, GEN_AI_OPERATION_NAME_ATTRIBUTE, GEN_AI_PIPELINE_NAME_ATTRIBUTE, @@ -224,12 +223,10 @@ export function instrumentCompiledGraphInvoke( } const enableTruncation = shouldEnableTruncation(options.enableTruncation); - const filteredLength = Array.isArray(filteredMessages) ? filteredMessages.length : 0; span.setAttributes({ [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: enableTruncation ? getTruncatedJsonString(filteredMessages) : stringify(filteredMessages), - [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: filteredLength, }); } diff --git a/packages/core/src/tracing/openai/index.ts b/packages/core/src/tracing/openai/index.ts index 821e9c68e0ff..c15b10856f8e 100644 --- a/packages/core/src/tracing/openai/index.ts +++ b/packages/core/src/tracing/openai/index.ts @@ -8,7 +8,6 @@ import { debug } from '../../utils/debug-logger'; import { GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE, GEN_AI_INPUT_MESSAGES_ATTRIBUTE, - GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE, GEN_AI_OPERATION_NAME_ATTRIBUTE, GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE, GEN_AI_REQUEST_MODEL_ATTRIBUTE, @@ -130,12 +129,6 @@ export function addRequestAttributes( GEN_AI_INPUT_MESSAGES_ATTRIBUTE, enableTruncation ? getTruncatedJsonString(filteredMessages) : stringify(filteredMessages), ); - - if (Array.isArray(filteredMessages)) { - span.setAttribute(GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE, filteredMessages.length); - } else { - span.setAttribute(GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE, 1); - } } /** diff --git a/packages/core/src/tracing/vercel-ai/utils.ts b/packages/core/src/tracing/vercel-ai/utils.ts index b57a43666202..d5c01f6302a8 100644 --- a/packages/core/src/tracing/vercel-ai/utils.ts +++ b/packages/core/src/tracing/vercel-ai/utils.ts @@ -2,7 +2,6 @@ import type { TraceContext } from '../../types/context'; import type { Span, SpanAttributes, SpanJSON } from '../../types/span'; import { GEN_AI_INPUT_MESSAGES_ATTRIBUTE, - GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE, GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE, GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE, GEN_AI_TOOL_DESCRIPTION_ATTRIBUTE, @@ -241,13 +240,11 @@ export function requestMessagesFromPrompt(span: Span, attributes: SpanAttributes span.setAttribute(GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE, systemInstructions); } - const filteredLength = Array.isArray(filteredMessages) ? filteredMessages.length : 0; const messagesJson = enableTruncation ? getTruncatedJsonString(filteredMessages) : stringify(filteredMessages); span.setAttributes({ [AI_PROMPT_ATTRIBUTE]: messagesJson, [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: messagesJson, - [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: filteredLength, }); } } else if (typeof attributes[AI_PROMPT_MESSAGES_ATTRIBUTE] === 'string') { @@ -263,8 +260,6 @@ export function requestMessagesFromPrompt(span: Span, attributes: SpanAttributes span.setAttribute(GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE, systemInstructions); } - const filteredLength = Array.isArray(filteredMessages) ? filteredMessages.length : 0; - // `extractSystemInstructions` returns the original array reference unchanged when no // system message is extracted. When truncation is also disabled, re-serializing would // reproduce the SDK's own input string, so we reuse it instead of allocating a second @@ -279,7 +274,6 @@ export function requestMessagesFromPrompt(span: Span, attributes: SpanAttributes span.setAttributes({ [AI_PROMPT_MESSAGES_ATTRIBUTE]: messagesJson, [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: messagesJson, - [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: filteredLength, }); } // eslint-disable-next-line no-empty diff --git a/packages/core/src/tracing/workers-ai/utils.ts b/packages/core/src/tracing/workers-ai/utils.ts index 23eac0eaa6fc..42ad16328982 100644 --- a/packages/core/src/tracing/workers-ai/utils.ts +++ b/packages/core/src/tracing/workers-ai/utils.ts @@ -15,7 +15,6 @@ import { import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../../semanticAttributes'; import type { Span, SpanAttributeValue } from '../../types/span'; import { - GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE, GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE, GEN_AI_REQUEST_STREAM_ATTRIBUTE, GEN_AI_RESPONSE_TEXT_ATTRIBUTE, @@ -129,11 +128,6 @@ export function addRequestAttributes( GEN_AI_INPUT_MESSAGES, enableTruncation ? getTruncatedJsonString(filteredMessages) : stringify(filteredMessages), ); - - span.setAttribute( - GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE, - Array.isArray(filteredMessages) ? filteredMessages.length : 1, - ); } /** diff --git a/packages/core/test/lib/tracing/vercel-ai-request-messages.test.ts b/packages/core/test/lib/tracing/vercel-ai-request-messages.test.ts index b2230c917344..25e9ed53f1eb 100644 --- a/packages/core/test/lib/tracing/vercel-ai-request-messages.test.ts +++ b/packages/core/test/lib/tracing/vercel-ai-request-messages.test.ts @@ -3,7 +3,6 @@ import { getTruncatedJsonString } from '../../../src/tracing/ai/utils'; import { stringify } from '../../../src/utils/string'; import { GEN_AI_INPUT_MESSAGES_ATTRIBUTE, - GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE, GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE, } from '../../../src/tracing/ai/gen-ai-attributes'; import { requestMessagesFromPrompt } from '../../../src/tracing/vercel-ai/utils'; @@ -40,7 +39,6 @@ describe('requestMessagesFromPrompt (ai.prompt.messages string branch)', () => { expect(recorded[AI_PROMPT_MESSAGES_ATTRIBUTE]).toBe(original); expect(recorded[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBe(original); - expect(recorded[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toBe(1); expect(recorded[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE]).toBeUndefined(); }); @@ -59,7 +57,6 @@ describe('requestMessagesFromPrompt (ai.prompt.messages string branch)', () => { // System message removed; output is the SDK's own serialization of just the remainder. expect(recorded[AI_PROMPT_MESSAGES_ATTRIBUTE]).toBe(stringify([{ role: 'user', content: 'hello' }])); expect(recorded[AI_PROMPT_MESSAGES_ATTRIBUTE]).not.toBe(original); - expect(recorded[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toBe(1); }); it('keeps the truncation path untouched when truncation is on', () => { @@ -79,7 +76,6 @@ describe('requestMessagesFromPrompt (ai.prompt.messages string branch)', () => { expect(recorded[AI_PROMPT_MESSAGES_ATTRIBUTE]).toBe(getTruncatedJsonString(messages)); expect(recorded[AI_PROMPT_MESSAGES_ATTRIBUTE]).not.toBe(original); // Original (pre-truncation) message count is still reported. - expect(recorded[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toBe(2); }); it('does not throw and sets no attributes for malformed JSON', () => { diff --git a/packages/core/test/lib/utils/anthropic-utils.test.ts b/packages/core/test/lib/utils/anthropic-utils.test.ts index 6880310c2064..59b6dd63fad2 100644 --- a/packages/core/test/lib/utils/anthropic-utils.test.ts +++ b/packages/core/test/lib/utils/anthropic-utils.test.ts @@ -101,7 +101,6 @@ describe('anthropic-ai-utils', () => { setMessagesAttribute(span, [{ role: 'user', content }], true); const result = [{ role: 'user', content: 'A'.repeat(19970) }]; expect(mock.attributes).toStrictEqual({ - 'sentry.sdk_meta.gen_ai.input.messages.original_length': 1, 'gen_ai.input.messages': JSON.stringify(result), }); }); @@ -109,7 +108,6 @@ describe('anthropic-ai-utils', () => { it('sets length to 1 for non-array input', () => { setMessagesAttribute(span, { content: 'hello, world' }, true); expect(mock.attributes).toStrictEqual({ - 'sentry.sdk_meta.gen_ai.input.messages.original_length': 1, 'gen_ai.input.messages': '{"content":"hello, world"}', }); }); @@ -117,7 +115,6 @@ describe('anthropic-ai-utils', () => { it('ignores empty array', () => { setMessagesAttribute(span, [], true); expect(mock.attributes).toStrictEqual({ - 'sentry.sdk_meta.gen_ai.input.messages.original_length': 1, 'gen_ai.input.messages': '{"content":"hello, world"}', }); }); diff --git a/packages/server-utils/src/vercel-ai/vercel-ai-dc-subscriber.ts b/packages/server-utils/src/vercel-ai/vercel-ai-dc-subscriber.ts index 0b9e7f39e101..bafc3b51bbd9 100644 --- a/packages/server-utils/src/vercel-ai/vercel-ai-dc-subscriber.ts +++ b/packages/server-utils/src/vercel-ai/vercel-ai-dc-subscriber.ts @@ -30,7 +30,6 @@ import type { Span, SpanAttributes } from '@sentry/core'; import { captureException, GEN_AI_CONVERSATION_ID_ATTRIBUTE, - GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE, GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE, getClient, getProviderMetadataAttributes, @@ -740,8 +739,6 @@ function buildInputMessageAttributes( const messages = event.messages ?? event.prompt; if (messages !== undefined) { attributes[GEN_AI_INPUT_MESSAGES] = enableTruncation ? getTruncatedJsonString(messages) : stringify(messages); - // The original (pre-truncation) message count, so the product can show how many were dropped. - attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE] = Array.isArray(messages) ? messages.length : 1; } return attributes; From e541815fd2d6e6cdcc7f92dcc58cd2967e4f65b9 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Thu, 23 Jul 2026 12:09:12 +0200 Subject: [PATCH 3/4] feat(core)!: Remove `enableTruncation` flag and gen_ai text truncation Removes the `enableTruncation` integration option and the gen_ai message text-truncation machinery. Now that gen_ai spans always ride the v2 span path (far larger size limits than legacy transactions), the byte-limit cropping and "keep only the last message" reduction is dead weight. Inline media stripping is kept and is now unconditional: previously media was only stripped on the truncation path, so with truncation off inline base64 could leak into span attributes for openai/anthropic/google-genai/ vercel-ai/workers-ai. `getGenAiMessagesJsonString` now always strips media before serializing, keeping all messages. Media-stripping logic is consolidated into `mediaStripping.ts`; `messageTruncation.ts` is deleted. Truncation-specific test scenarios and instruments are removed; image-stripping tests are re-pointed at the default recording instruments, and full span-streaming coverage is kept via non-truncation streaming tests. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../anthropic/instrument-no-truncation.mjs | 24 - .../instrument-streaming-with-truncation.mjs | 16 - .../anthropic/instrument-with-truncation.mjs | 18 - ...ation.mjs => scenario-media-stripping.mjs} | 1 - .../anthropic/scenario-message-truncation.mjs | 75 ---- .../anthropic/scenario-no-truncation.mjs | 64 --- .../suites/tracing/anthropic/test.ts | 230 +++------- .../google-genai/instrument-no-truncation.mjs | 24 - .../instrument-streaming-with-truncation.mjs | 16 - .../instrument-with-truncation.mjs | 18 - .../scenario-message-truncation.mjs | 81 ---- .../google-genai/scenario-no-truncation.mjs | 55 --- .../suites/tracing/google-genai/test.ts | 122 +----- .../langchain/instrument-no-truncation.mjs | 24 - .../instrument-streaming-with-truncation.mjs | 16 - .../langchain/instrument-with-truncation.mjs | 18 - .../langchain/scenario-message-truncation.mjs | 82 ---- .../langchain/scenario-no-truncation.mjs | 56 --- .../suites/tracing/langchain/test.ts | 128 +----- .../v1/instrument-with-truncation.mjs | 18 - .../v1/scenario-message-truncation.mjs | 82 ---- .../suites/tracing/langchain/v1/test.ts | 67 +-- .../langgraph/instrument-no-truncation.mjs | 17 - .../instrument-streaming-with-truncation.mjs | 16 - .../langgraph/scenario-no-truncation.mjs | 46 -- .../suites/tracing/langgraph/test.ts | 102 +---- .../openai/instrument-no-truncation.mjs | 23 - .../instrument-streaming-with-truncation.mjs | 16 - .../openai/instrument-with-truncation.mjs | 17 - .../tracing/openai/scenario-no-truncation.mjs | 87 ---- .../suites/tracing/openai/test.ts | 252 +---------- ...cenario-message-truncation-completions.mjs | 81 ---- .../scenario-message-truncation-responses.mjs | 93 ---- .../vercelai/instrument-no-truncation.mjs | 17 - .../vercelai/instrument-with-truncation.mjs | 11 - .../vercelai/scenario-message-truncation.mjs | 51 --- .../vercelai/scenario-no-truncation.mjs | 28 -- .../instrument-with-truncation.mjs | 16 - .../span-streaming-v4/scenario-truncation.mjs | 27 -- .../vercelai/span-streaming-v4/test.ts | 48 +- .../suites/tracing/vercelai/test.ts | 93 ---- .../src/integrations/tracing/vercelai.ts | 6 - packages/core/src/server-exports.ts | 2 +- .../core/src/tracing/ai/mediaStripping.ts | 75 ++++ .../core/src/tracing/ai/messageTruncation.ts | 409 ------------------ packages/core/src/tracing/ai/utils.ts | 36 +- .../core/src/tracing/anthropic-ai/index.ts | 21 +- .../core/src/tracing/anthropic-ai/types.ts | 5 - .../core/src/tracing/anthropic-ai/utils.ts | 11 +- .../core/src/tracing/google-genai/index.ts | 24 +- .../core/src/tracing/google-genai/types.ts | 5 - packages/core/src/tracing/langchain/index.ts | 5 +- packages/core/src/tracing/langchain/types.ts | 6 - packages/core/src/tracing/langchain/utils.ts | 16 +- packages/core/src/tracing/langgraph/index.ts | 12 +- packages/core/src/tracing/langgraph/types.ts | 5 - packages/core/src/tracing/openai/index.ts | 20 +- packages/core/src/tracing/openai/types.ts | 5 - packages/core/src/tracing/vercel-ai/index.ts | 13 +- packages/core/src/tracing/vercel-ai/utils.ts | 18 +- packages/core/src/tracing/workers-ai/index.ts | 6 +- packages/core/src/tracing/workers-ai/types.ts | 8 +- packages/core/src/tracing/workers-ai/utils.ts | 15 +- ...ion.test.ts => ai-media-stripping.test.ts} | 284 +++--------- .../core/test/lib/tracing/ai/utils.test.ts | 51 +-- .../test/lib/tracing/langchain-utils.test.ts | 2 +- .../vercel-ai-request-messages.test.ts | 48 +- .../test/lib/utils/anthropic-utils.test.ts | 13 +- .../integrations/tracing-channel/anthropic.ts | 4 +- .../tracing-channel/google-genai.ts | 4 +- .../integrations/tracing-channel/openai.ts | 4 +- packages/server-utils/src/vercel-ai/index.ts | 6 - .../src/vercel-ai/vercel-ai-dc-subscriber.ts | 25 +- .../src/integrations/tracing/vercelai.ts | 6 - 74 files changed, 315 insertions(+), 3131 deletions(-) delete mode 100644 dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-no-truncation.mjs delete mode 100644 dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-streaming-with-truncation.mjs delete mode 100644 dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-with-truncation.mjs rename dev-packages/node-integration-tests/suites/tracing/anthropic/{scenario-media-truncation.mjs => scenario-media-stripping.mjs} (96%) delete mode 100644 dev-packages/node-integration-tests/suites/tracing/anthropic/scenario-message-truncation.mjs delete mode 100644 dev-packages/node-integration-tests/suites/tracing/anthropic/scenario-no-truncation.mjs delete mode 100644 dev-packages/node-integration-tests/suites/tracing/google-genai/instrument-no-truncation.mjs delete mode 100644 dev-packages/node-integration-tests/suites/tracing/google-genai/instrument-streaming-with-truncation.mjs delete mode 100644 dev-packages/node-integration-tests/suites/tracing/google-genai/instrument-with-truncation.mjs delete mode 100644 dev-packages/node-integration-tests/suites/tracing/google-genai/scenario-message-truncation.mjs delete mode 100644 dev-packages/node-integration-tests/suites/tracing/google-genai/scenario-no-truncation.mjs delete mode 100644 dev-packages/node-integration-tests/suites/tracing/langchain/instrument-no-truncation.mjs delete mode 100644 dev-packages/node-integration-tests/suites/tracing/langchain/instrument-streaming-with-truncation.mjs delete mode 100644 dev-packages/node-integration-tests/suites/tracing/langchain/instrument-with-truncation.mjs delete mode 100644 dev-packages/node-integration-tests/suites/tracing/langchain/scenario-message-truncation.mjs delete mode 100644 dev-packages/node-integration-tests/suites/tracing/langchain/scenario-no-truncation.mjs delete mode 100644 dev-packages/node-integration-tests/suites/tracing/langchain/v1/instrument-with-truncation.mjs delete mode 100644 dev-packages/node-integration-tests/suites/tracing/langchain/v1/scenario-message-truncation.mjs delete mode 100644 dev-packages/node-integration-tests/suites/tracing/langgraph/instrument-no-truncation.mjs delete mode 100644 dev-packages/node-integration-tests/suites/tracing/langgraph/instrument-streaming-with-truncation.mjs delete mode 100644 dev-packages/node-integration-tests/suites/tracing/langgraph/scenario-no-truncation.mjs delete mode 100644 dev-packages/node-integration-tests/suites/tracing/openai/instrument-no-truncation.mjs delete mode 100644 dev-packages/node-integration-tests/suites/tracing/openai/instrument-streaming-with-truncation.mjs delete mode 100644 dev-packages/node-integration-tests/suites/tracing/openai/instrument-with-truncation.mjs delete mode 100644 dev-packages/node-integration-tests/suites/tracing/openai/scenario-no-truncation.mjs delete mode 100644 dev-packages/node-integration-tests/suites/tracing/openai/truncation/scenario-message-truncation-completions.mjs delete mode 100644 dev-packages/node-integration-tests/suites/tracing/openai/truncation/scenario-message-truncation-responses.mjs delete mode 100644 dev-packages/node-integration-tests/suites/tracing/vercelai/instrument-no-truncation.mjs delete mode 100644 dev-packages/node-integration-tests/suites/tracing/vercelai/instrument-with-truncation.mjs delete mode 100644 dev-packages/node-integration-tests/suites/tracing/vercelai/scenario-message-truncation.mjs delete mode 100644 dev-packages/node-integration-tests/suites/tracing/vercelai/scenario-no-truncation.mjs delete mode 100644 dev-packages/node-integration-tests/suites/tracing/vercelai/span-streaming-v4/instrument-with-truncation.mjs delete mode 100644 dev-packages/node-integration-tests/suites/tracing/vercelai/span-streaming-v4/scenario-truncation.mjs delete mode 100644 packages/core/src/tracing/ai/messageTruncation.ts rename packages/core/test/lib/tracing/{ai-message-truncation.test.ts => ai-media-stripping.test.ts} (56%) diff --git a/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-no-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-no-truncation.mjs deleted file mode 100644 index 1fa73dda80a2..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-no-truncation.mjs +++ /dev/null @@ -1,24 +0,0 @@ -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: false, outputs: false } }, - transport: loggingTransport, - integrations: [ - Sentry.anthropicAIIntegration({ - recordInputs: true, - recordOutputs: true, - enableTruncation: false, - }), - ], - beforeSendTransaction: event => { - // Filter out mock express server transactions - if (event.transaction.includes('/anthropic/v1/')) { - return null; - } - return event; - }, -}); diff --git a/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-streaming-with-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-streaming-with-truncation.mjs deleted file mode 100644 index 048f3de408e2..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-streaming-with-truncation.mjs +++ /dev/null @@ -1,16 +0,0 @@ -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, - traceLifecycle: 'stream', - integrations: [ - Sentry.anthropicAIIntegration({ - enableTruncation: true, - }), - ], -}); diff --git a/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-with-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-with-truncation.mjs deleted file mode 100644 index c593efd41fdb..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-with-truncation.mjs +++ /dev/null @@ -1,18 +0,0 @@ -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; - }, -}); diff --git a/dev-packages/node-integration-tests/suites/tracing/anthropic/scenario-media-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/anthropic/scenario-media-stripping.mjs similarity index 96% rename from dev-packages/node-integration-tests/suites/tracing/anthropic/scenario-media-truncation.mjs rename to dev-packages/node-integration-tests/suites/tracing/anthropic/scenario-media-stripping.mjs index 48f337e2b23c..92f609875dc8 100644 --- a/dev-packages/node-integration-tests/suites/tracing/anthropic/scenario-media-truncation.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/anthropic/scenario-media-stripping.mjs @@ -36,7 +36,6 @@ async function run() { }); // Send the image showing the number 3 - // Put the image in the last message so it doesn't get dropped await client.messages.create({ model: 'claude-3-haiku-20240307', max_tokens: 1024, diff --git a/dev-packages/node-integration-tests/suites/tracing/anthropic/scenario-message-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/anthropic/scenario-message-truncation.mjs deleted file mode 100644 index 27aa8494f693..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/anthropic/scenario-message-truncation.mjs +++ /dev/null @@ -1,75 +0,0 @@ -import Anthropic from '@anthropic-ai/sdk'; -import * as Sentry from '@sentry/node'; -import express from 'express'; - -function startMockAnthropicServer() { - const app = express(); - app.use(express.json({ limit: '10mb' })); - - app.post('/anthropic/v1/messages', (req, res) => { - res.send({ - id: 'msg-truncation-test', - type: 'message', - role: 'assistant', - content: [{ type: 'text', text: 'Response to truncated messages' }], - model: req.body.model, - stop_reason: 'end_turn', - stop_sequence: null, - usage: { input_tokens: 10, output_tokens: 15 }, - }); - }); - - return new Promise(resolve => { - const server = app.listen(0, () => { - resolve(server); - }); - }); -} - -async function run() { - const server = await startMockAnthropicServer(); - - await Sentry.startSpan({ op: 'function', name: 'main' }, async () => { - const client = new Anthropic({ - apiKey: 'mock-api-key', - baseURL: `http://localhost:${server.address().port}/anthropic`, - }); - - // 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 - const largeContent1 = 'A'.repeat(15000); // ~15KB - const largeContent2 = 'B'.repeat(15000); // ~15KB - const largeContent3 = 'C'.repeat(25000) + 'D'.repeat(25000); // ~50KB (will be truncated, only C's remain) - - await client.messages.create({ - model: 'claude-3-haiku-20240307', - max_tokens: 100, - messages: [ - { role: 'user', content: largeContent1 }, - { role: 'assistant', content: largeContent2 }, - { role: 'user', content: largeContent3 }, - ], - temperature: 0.7, - }); - - // Test 2: Given an array of messages only the last message should be kept - // The last message is small, so it should be kept intact - const smallContent = 'This is a small message that fits within the limit'; - await client.messages.create({ - model: 'claude-3-haiku-20240307', - max_tokens: 100, - messages: [ - { role: 'user', content: largeContent1 }, - { role: 'assistant', content: largeContent2 }, - { role: 'user', content: smallContent }, - ], - temperature: 0.7, - }); - }); - - await Sentry.flush(2000); - - server.close(); -} - -run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/anthropic/scenario-no-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/anthropic/scenario-no-truncation.mjs deleted file mode 100644 index f66cee978733..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/anthropic/scenario-no-truncation.mjs +++ /dev/null @@ -1,64 +0,0 @@ -import Anthropic from '@anthropic-ai/sdk'; -import * as Sentry from '@sentry/node'; -import express from 'express'; - -function startMockAnthropicServer() { - const app = express(); - app.use(express.json({ limit: '10mb' })); - - app.post('/anthropic/v1/messages', (req, res) => { - res.send({ - id: 'msg-no-truncation-test', - type: 'message', - role: 'assistant', - content: [{ type: 'text', text: 'Response' }], - model: req.body.model, - stop_reason: 'end_turn', - stop_sequence: null, - usage: { input_tokens: 10, output_tokens: 5 }, - }); - }); - - return new Promise(resolve => { - const server = app.listen(0, () => { - resolve(server); - }); - }); -} - -async function run() { - const server = await startMockAnthropicServer(); - - await Sentry.startSpan({ op: 'function', name: 'main' }, async () => { - const client = new Anthropic({ - apiKey: 'mock-api-key', - baseURL: `http://localhost:${server.address().port}/anthropic`, - }); - - // Multiple messages with long content (would normally be truncated and popped to last message only) - const longContent = 'A'.repeat(50_000); - await client.messages.create({ - model: 'claude-3-haiku-20240307', - max_tokens: 100, - messages: [ - { role: 'user', content: longContent }, - { role: 'assistant', content: 'Some reply' }, - { role: 'user', content: 'Follow-up question' }, - ], - }); - - // Long string input (messagesFromParams wraps it in an array) - const longStringInput = 'B'.repeat(50_000); - await client.messages.create({ - model: 'claude-3-haiku-20240307', - max_tokens: 100, - input: longStringInput, - }); - }); - - await Sentry.flush(2000); - - server.close(); -} - -run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/anthropic/test.ts b/dev-packages/node-integration-tests/suites/tracing/anthropic/test.ts index 43dd96dfce5c..8f96dd044277 100644 --- a/dev-packages/node-integration-tests/suites/tracing/anthropic/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/anthropic/test.ts @@ -601,109 +601,54 @@ describe('Anthropic integration', () => { }); }); - createEsmAndCjsTests( - __dirname, - 'scenario-message-truncation.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() - .ignore('event') - .expect({ - transaction: { - transaction: 'main', - }, - }) - .expect({ - span: container => { - expect(container.items).toHaveLength(2); - const smallMsgValue = JSON.stringify([ - { role: 'user', content: 'This is a small message that fits within the limit' }, - ]); - const truncatedSpan = container.items.find(span => - getStringAttributeValue(span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value)?.match( - /^\[\{"role":"user","content":"C+"\}\]$/, - ), - ); - expect(truncatedSpan).toBeDefined(); - expect(truncatedSpan!.name).toBe('chat claude-3-haiku-20240307'); - expect(truncatedSpan!.status).toBe('ok'); - expect(truncatedSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('chat'); - expect(truncatedSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); - expect(truncatedSpan!.attributes['sentry.origin'].value).toBe('auto.ai.anthropic'); - expect(truncatedSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); - expect(truncatedSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('claude-3-haiku-20240307'); - - const smallMessageSpan = container.items.find( - span => span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value === smallMsgValue, - ); - expect(smallMessageSpan).toBeDefined(); - expect(smallMessageSpan!.name).toBe('chat claude-3-haiku-20240307'); - expect(smallMessageSpan!.status).toBe('ok'); - expect(smallMessageSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('chat'); - expect(smallMessageSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); - expect(smallMessageSpan!.attributes['sentry.origin'].value).toBe('auto.ai.anthropic'); - expect(smallMessageSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); - expect(smallMessageSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe( - 'claude-3-haiku-20240307', - ); - }, - }) - .start() - .completed(); - }); - }, - ); - - 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]', - }, + createEsmAndCjsTests(__dirname, 'scenario-media-stripping.mjs', 'instrument-with-pii.mjs', (createRunner, test) => { + test('strips media attachment, keeping all other messages and details', async () => { + const expectedMediaMessages = JSON.stringify([ + { + role: 'user', + content: 'what number is this?', + }, + { + 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'); - }, - }) - .start() - .completed(); - }); - }, - ); + // messages.create with media attachment — image data replaced, all other messages/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'); + }, + }) + .start() + .completed(); + }); + }); createEsmAndCjsTests( __dirname, @@ -734,61 +679,14 @@ describe('Anthropic integration', () => { }, ); - const longContent = 'A'.repeat(50_000); - const longStringInput = 'B'.repeat(50_000); - - const EXPECTED_TRANSACTION_NO_TRUNCATION = { - transaction: 'main', - }; - - createEsmAndCjsTests( - __dirname, - 'scenario-no-truncation.mjs', - 'instrument-no-truncation.mjs', - (createRunner, test) => { - test('does not truncate input messages when enableTruncation is false', async () => { - const expectedAllMessages = JSON.stringify([ - { role: 'user', content: longContent }, - { role: 'assistant', content: 'Some reply' }, - { role: 'user', content: 'Follow-up question' }, - ]); - const expectedLongString = JSON.stringify([longStringInput]); - await createRunner() - .ignore('event') - .expect({ transaction: EXPECTED_TRANSACTION_NO_TRUNCATION }) - .expect({ - span: container => { - expect(container.items).toHaveLength(2); - const conversationSpan = container.items.find( - span => span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value === expectedAllMessages, - ); - expect(conversationSpan).toBeDefined(); - - const longStringSpan = container.items.find( - span => span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value === expectedLongString, - ); - expect(longStringSpan).toBeDefined(); - }, - }) - .start() - .completed(); - }); - }, - ); - - 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 () => { + test('records full gen_ai input messages when span streaming is enabled', async () => { + const longContent = 'A'.repeat(50_000); await createRunner() .expect({ span: container => { - const spans = container.items; - - const chatSpan = spans.find(s => - getStringAttributeValue(s.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value)?.includes( - streamingLongContent, - ), + const chatSpan = container.items.find(s => + getStringAttributeValue(s.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value)?.includes(longContent), ); expect(chatSpan).toBeDefined(); }, @@ -797,34 +695,4 @@ describe('Anthropic integration', () => { .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 => - getStringAttributeValue(s.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value)?.startsWith( - '[{"role":"user","content":"AAAA', - ), - ); - expect(chatSpan).toBeDefined(); - expect( - (getStringAttributeValue(chatSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value) ?? '').length, - ).toBeLessThan(streamingLongContent.length); - }, - }) - .start() - .completed(); - }); - }, - ); }); 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 deleted file mode 100644 index 2b1f9cfd1dca..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/google-genai/instrument-no-truncation.mjs +++ /dev/null @@ -1,24 +0,0 @@ -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: false, outputs: false } }, - transport: loggingTransport, - integrations: [ - Sentry.googleGenAIIntegration({ - recordInputs: true, - recordOutputs: true, - enableTruncation: false, - }), - ], - beforeSendTransaction: event => { - // Filter out mock express server transactions - if (event.transaction.includes('/v1beta/')) { - return null; - } - return event; - }, -}); 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 deleted file mode 100644 index 0e148493344a..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/google-genai/instrument-streaming-with-truncation.mjs +++ /dev/null @@ -1,16 +0,0 @@ -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, - traceLifecycle: 'stream', - integrations: [ - Sentry.googleGenAIIntegration({ - enableTruncation: true, - }), - ], -}); diff --git a/dev-packages/node-integration-tests/suites/tracing/google-genai/instrument-with-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/google-genai/instrument-with-truncation.mjs deleted file mode 100644 index 7cccb1ccb487..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/google-genai/instrument-with-truncation.mjs +++ /dev/null @@ -1,18 +0,0 @@ -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; - }, -}); diff --git a/dev-packages/node-integration-tests/suites/tracing/google-genai/scenario-message-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/google-genai/scenario-message-truncation.mjs deleted file mode 100644 index d3cb34648f4a..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/google-genai/scenario-message-truncation.mjs +++ /dev/null @@ -1,81 +0,0 @@ -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.send({ - candidates: [ - { - content: { parts: [{ text: 'Response to truncated messages' }], role: 'model' }, - finishReason: 'stop', - index: 0, - }, - ], - usageMetadata: { promptTokenCount: 10, candidatesTokenCount: 15, totalTokenCount: 25 }, - }); - }); - - 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}` }, - }); - - // 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 - const largeContent1 = 'A'.repeat(15000); // ~15KB - const largeContent2 = 'B'.repeat(15000); // ~15KB - const largeContent3 = 'C'.repeat(25000) + 'D'.repeat(25000); // ~50KB (will be truncated, only C's remain) - - await client.models.generateContent({ - model: 'gemini-1.5-flash', - config: { - temperature: 0.7, - topP: 0.9, - maxOutputTokens: 100, - }, - contents: [ - { role: 'user', parts: [{ text: largeContent1 }] }, - { role: 'model', parts: [{ text: largeContent2 }] }, - { role: 'user', parts: [{ text: largeContent3 }] }, - ], - }); - - // Test 2: Given an array of messages only the last message should be kept - // The last message is small, so it should be kept intact - const smallContent = 'This is a small message that fits within the limit'; - await client.models.generateContent({ - model: 'gemini-1.5-flash', - config: { - temperature: 0.7, - topP: 0.9, - maxOutputTokens: 100, - }, - contents: [ - { role: 'user', parts: [{ text: largeContent1 }] }, - { role: 'model', parts: [{ text: largeContent2 }] }, - { role: 'user', parts: [{ text: smallContent }] }, - ], - }); - }); - - await Sentry.flush(2000); - - server.close(); -} - -run(); 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 deleted file mode 100644 index 67ece6759577..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/google-genai/scenario-no-truncation.mjs +++ /dev/null @@ -1,55 +0,0 @@ -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.send({ - candidates: [ - { - content: { parts: [{ text: 'Response' }], role: 'model' }, - finishReason: 'stop', - index: 0, - }, - ], - 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 }] }, - { role: 'model', parts: [{ text: 'Some reply' }] }, - { role: 'user', parts: [{ text: 'Follow-up question' }] }, - ], - }); - }); - - 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 81b983ea4231..73e4ba1932a5 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 @@ -346,50 +346,6 @@ describe('Google GenAI integration', () => { }); }); - createEsmAndCjsTests( - __dirname, - 'scenario-message-truncation.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() - .ignore('event') - .expect({ transaction: { transaction: 'main' } }) - .expect({ - span: container => { - expect(container.items).toHaveLength(2); - const truncatedSpan = container.items.find(span => - getStringAttributeValue(span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value)?.match( - /^\[\{"role":"user","parts":\[\{"text":"C+"\}\]\}\]$/, - ), - ); - expect(truncatedSpan).toBeDefined(); - expect(truncatedSpan!.name).toBe('generate_content gemini-1.5-flash'); - expect(truncatedSpan!.status).toBe('ok'); - expect(truncatedSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('generate_content'); - - const smallMessageSpan = container.items.find( - span => - span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value === - JSON.stringify([ - { - role: 'user', - parts: [{ text: 'This is a small message that fits within the limit' }], - }, - ]), - ); - expect(smallMessageSpan).toBeDefined(); - expect(smallMessageSpan!.name).toBe('generate_content gemini-1.5-flash'); - expect(smallMessageSpan!.status).toBe('ok'); - expect(smallMessageSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('generate_content'); - }, - }) - .start() - .completed(); - }); - }, - ); - createEsmAndCjsTests( __dirname, 'scenario-system-instructions.mjs', @@ -504,88 +460,20 @@ describe('Google GenAI integration', () => { }); }); - const longContent = 'A'.repeat(50_000); - - 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: { transaction: 'main' } }) - .expect({ - span: container => { - expect(container.items).toHaveLength(1); - const [firstSpan] = container.items; - - // [0] generate_content with full (non-truncated) input messages - expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('generate_content'); - expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toBe( - JSON.stringify([ - { role: 'user', parts: [{ text: longContent }] }, - { role: 'model', parts: [{ text: 'Some reply' }] }, - { role: 'user', parts: [{ text: 'Follow-up question' }] }, - ]), - ); - }, - }) - .start() - .completed(); - }); - }, - ); - - 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 () => { + test('records full gen_ai input messages when span streaming is enabled', async () => { + const longContent = 'A'.repeat(50_000); await createRunner() .expect({ span: container => { - const spans = container.items; - - const chatSpan = spans.find(s => - getStringAttributeValue(s.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value)?.includes( - streamingLongContent, - ), + const generateContentSpan = container.items.find(s => + getStringAttributeValue(s.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value)?.includes(longContent), ); - expect(chatSpan).toBeDefined(); + expect(generateContentSpan).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 => - getStringAttributeValue(s.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value)?.startsWith( - '[{"role":"user","parts":[{"text":"AAAA', - ), - ); - expect(chatSpan).toBeDefined(); - expect( - (getStringAttributeValue(chatSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value) ?? '').length, - ).toBeLessThan(streamingLongContent.length); - }, - }) - .start() - .completed(); - }); - }, - ); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-no-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-no-truncation.mjs deleted file mode 100644 index 0a647f326044..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-no-truncation.mjs +++ /dev/null @@ -1,24 +0,0 @@ -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: false, - recordInputs: true, - recordOutputs: true, - }), - ], - beforeSendTransaction: event => { - // Filter out mock express server transactions - if (event.transaction.includes('/v1/messages') || event.transaction.includes('/v1/embeddings')) { - return null; - } - return event; - }, -}); diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-streaming-with-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-streaming-with-truncation.mjs deleted file mode 100644 index 2a2365407b79..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-streaming-with-truncation.mjs +++ /dev/null @@ -1,16 +0,0 @@ -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, - traceLifecycle: 'stream', - integrations: [ - Sentry.langChainIntegration({ - enableTruncation: true, - }), - ], -}); diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-with-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-with-truncation.mjs deleted file mode 100644 index cb677ddd9926..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-with-truncation.mjs +++ /dev/null @@ -1,18 +0,0 @@ -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; - }, -}); diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/scenario-message-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/langchain/scenario-message-truncation.mjs deleted file mode 100644 index 9e5e59f264ca..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/scenario-message-truncation.mjs +++ /dev/null @@ -1,82 +0,0 @@ -import { ChatAnthropic } from '@langchain/anthropic'; -import * as Sentry from '@sentry/node'; -import express from 'express'; - -function startMockAnthropicServer() { - const app = express(); - app.use(express.json()); - - app.post('/v1/messages', (req, res) => { - const model = req.body.model; - - res.json({ - id: 'msg_truncation_test', - type: 'message', - role: 'assistant', - content: [ - { - type: 'text', - text: 'Response to truncated messages', - }, - ], - model: model, - stop_reason: 'end_turn', - stop_sequence: null, - usage: { - input_tokens: 10, - output_tokens: 15, - }, - }); - }); - - return new Promise(resolve => { - const server = app.listen(0, () => { - resolve(server); - }); - }); -} - -async function run() { - const server = await startMockAnthropicServer(); - const baseUrl = `http://localhost:${server.address().port}`; - - await Sentry.startSpan({ op: 'function', name: 'main' }, async () => { - const model = new ChatAnthropic({ - model: 'claude-3-5-sonnet-20241022', - apiKey: 'mock-api-key', - clientOptions: { - baseURL: baseUrl, - }, - }); - - const largeContent1 = 'A'.repeat(15000); // ~15KB - const largeContent2 = 'B'.repeat(15000); // ~15KB - const largeContent3 = 'C'.repeat(25000) + 'D'.repeat(25000); // ~50KB (will be truncated, only C's remain) - - // Test 1: Create one very large string that gets truncated to only include Cs - await model.invoke(largeContent3); - - // Test 2: Create an array of messages that gets truncated to only include the last message - // The last message should be truncated to fit within the 20KB limit (result should again contain only Cs) - await model.invoke([ - { role: 'system', content: largeContent1 }, - { role: 'user', content: largeContent2 }, - { role: 'user', content: largeContent3 }, - ]); - - // Test 3: Given an array of messages only the last message should be kept - // The last message is small, so it should be kept intact - const smallContent = 'This is a small message that fits within the limit'; - await model.invoke([ - { role: 'system', content: largeContent1 }, - { role: 'user', content: largeContent2 }, - { role: 'user', content: smallContent }, - ]); - }); - - await Sentry.flush(2000); - - server.close(); -} - -run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/scenario-no-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/langchain/scenario-no-truncation.mjs deleted file mode 100644 index bb8f5fc35325..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/scenario-no-truncation.mjs +++ /dev/null @@ -1,56 +0,0 @@ -import { ChatAnthropic } from '@langchain/anthropic'; -import * as Sentry from '@sentry/node'; -import express from 'express'; - -function startMockAnthropicServer() { - const app = express(); - app.use(express.json({ limit: '10mb' })); - - app.post('/v1/messages', (req, res) => { - res.json({ - id: 'msg_no_truncation_test', - type: 'message', - role: 'assistant', - content: [{ type: 'text', text: 'Response' }], - model: req.body.model, - stop_reason: 'end_turn', - stop_sequence: null, - usage: { input_tokens: 10, output_tokens: 5 }, - }); - }); - - return new Promise(resolve => { - const server = app.listen(0, () => { - resolve(server); - }); - }); -} - -async function run() { - const server = await startMockAnthropicServer(); - const baseUrl = `http://localhost:${server.address().port}`; - - await Sentry.startSpan({ op: 'function', name: 'main' }, async () => { - const model = new ChatAnthropic({ - model: 'claude-3-5-sonnet-20241022', - apiKey: 'mock-api-key', - clientOptions: { - baseURL: baseUrl, - }, - }); - - // Long content that would normally be truncated - const longContent = 'A'.repeat(50_000); - await model.invoke([ - { role: 'user', content: longContent }, - { role: 'assistant', content: 'Some reply' }, - { role: 'user', content: 'Follow-up question' }, - ]); - }); - - await Sentry.flush(2000); - - server.close(); -} - -run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/test.ts b/dev-packages/node-integration-tests/suites/tracing/langchain/test.ts index d2b94f200c62..7f090db64c68 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/langchain/test.ts @@ -197,59 +197,6 @@ describe('LangChain integration', () => { }); }); - createEsmAndCjsTests( - __dirname, - 'scenario-message-truncation.mjs', - 'instrument-with-truncation.mjs', - (createRunner, test) => { - test('truncates messages when they exceed byte limit', async () => { - await createRunner() - .ignore('event') - .expect({ transaction: { transaction: 'main' } }) - .expect({ - span: container => { - expect(container.items).toHaveLength(3); - // The string-input span has no system message (and therefore no system instructions), - // while the array-input span does — use that to distinguish the two truncated spans. - const truncatedContent = /^\[\{"role":"user","content":"C+"\}\]$/; - const stringInputSpan = container.items.find( - span => - span.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE] === undefined && - getStringAttributeValue(span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value)?.match( - truncatedContent, - ), - ); - expect(stringInputSpan).toBeDefined(); - expect(stringInputSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); - expect(stringInputSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toMatch(truncatedContent); - - const arrayInputSpan = container.items.find( - span => - span.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE] !== undefined && - getStringAttributeValue(span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value)?.match( - truncatedContent, - ), - ); - expect(arrayInputSpan).toBeDefined(); - expect(arrayInputSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); - expect(arrayInputSpan!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE]).toBeDefined(); - - const smallMessageSpan = container.items.find( - span => - span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value === - JSON.stringify([{ role: 'user', content: 'This is a small message that fits within the limit' }]), - ); - expect(smallMessageSpan).toBeDefined(); - expect(smallMessageSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); - expect(smallMessageSpan!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE]).toBeDefined(); - }, - }) - .start() - .completed(); - }); - }, - ); - createEsmTests(__dirname, 'scenario-openai-before-langchain.mjs', 'instrument.mjs', (createRunner, test) => { test('demonstrates timing issue with duplicate spans', async () => { await createRunner() @@ -454,52 +401,14 @@ describe('LangChain integration', () => { }); }); - const longContent = 'A'.repeat(50_000); - - 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: { transaction: 'main' } }) - .expect({ - span: container => { - expect(container.items).toHaveLength(1); - const [firstSpan] = container.items; - - // [0] chat with full (untruncated) input messages - expect(firstSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); - expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toBe( - JSON.stringify([ - { role: 'user', content: longContent }, - { role: 'assistant', content: 'Some reply' }, - { role: 'user', content: 'Follow-up question' }, - ]), - ); - }, - }) - .start() - .completed(); - }); - }, - ); - - 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 () => { + test('records full gen_ai input messages when span streaming is enabled', async () => { + const longContent = 'A'.repeat(50_000); await createRunner() .expect({ span: container => { - const spans = container.items; - - const chatSpan = spans.find(s => - getStringAttributeValue(s.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value)?.includes( - streamingLongContent, - ), + const chatSpan = container.items.find(s => + getStringAttributeValue(s.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value)?.includes(longContent), ); expect(chatSpan).toBeDefined(); }, @@ -508,33 +417,4 @@ describe('LangChain integration', () => { .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. - const chatSpan = spans.find(s => - getStringAttributeValue(s.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value)?.startsWith( - '[{"role":"user","content":"AAAA', - ), - ); - expect(chatSpan).toBeDefined(); - expect( - (getStringAttributeValue(chatSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value) ?? '').length, - ).toBeLessThan(streamingLongContent.length); - }, - }) - .start() - .completed(); - }); - }, - ); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/v1/instrument-with-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/langchain/v1/instrument-with-truncation.mjs deleted file mode 100644 index b773dbad2e6b..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/v1/instrument-with-truncation.mjs +++ /dev/null @@ -1,18 +0,0 @@ -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; - }, -}); diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/v1/scenario-message-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/langchain/v1/scenario-message-truncation.mjs deleted file mode 100644 index 9e5e59f264ca..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/v1/scenario-message-truncation.mjs +++ /dev/null @@ -1,82 +0,0 @@ -import { ChatAnthropic } from '@langchain/anthropic'; -import * as Sentry from '@sentry/node'; -import express from 'express'; - -function startMockAnthropicServer() { - const app = express(); - app.use(express.json()); - - app.post('/v1/messages', (req, res) => { - const model = req.body.model; - - res.json({ - id: 'msg_truncation_test', - type: 'message', - role: 'assistant', - content: [ - { - type: 'text', - text: 'Response to truncated messages', - }, - ], - model: model, - stop_reason: 'end_turn', - stop_sequence: null, - usage: { - input_tokens: 10, - output_tokens: 15, - }, - }); - }); - - return new Promise(resolve => { - const server = app.listen(0, () => { - resolve(server); - }); - }); -} - -async function run() { - const server = await startMockAnthropicServer(); - const baseUrl = `http://localhost:${server.address().port}`; - - await Sentry.startSpan({ op: 'function', name: 'main' }, async () => { - const model = new ChatAnthropic({ - model: 'claude-3-5-sonnet-20241022', - apiKey: 'mock-api-key', - clientOptions: { - baseURL: baseUrl, - }, - }); - - const largeContent1 = 'A'.repeat(15000); // ~15KB - const largeContent2 = 'B'.repeat(15000); // ~15KB - const largeContent3 = 'C'.repeat(25000) + 'D'.repeat(25000); // ~50KB (will be truncated, only C's remain) - - // Test 1: Create one very large string that gets truncated to only include Cs - await model.invoke(largeContent3); - - // Test 2: Create an array of messages that gets truncated to only include the last message - // The last message should be truncated to fit within the 20KB limit (result should again contain only Cs) - await model.invoke([ - { role: 'system', content: largeContent1 }, - { role: 'user', content: largeContent2 }, - { role: 'user', content: largeContent3 }, - ]); - - // Test 3: Given an array of messages only the last message should be kept - // The last message is small, so it should be kept intact - const smallContent = 'This is a small message that fits within the limit'; - await model.invoke([ - { role: 'system', content: largeContent1 }, - { role: 'user', content: largeContent2 }, - { role: 'user', content: smallContent }, - ]); - }); - - await Sentry.flush(2000); - - server.close(); -} - -run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/v1/test.ts b/dev-packages/node-integration-tests/suites/tracing/langchain/v1/test.ts index 9d5c5e7ba1a3..42c79716b86a 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/v1/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/langchain/v1/test.ts @@ -12,12 +12,11 @@ import { GEN_AI_RESPONSE_TEXT_ATTRIBUTE, GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE, GEN_AI_SYSTEM_ATTRIBUTE, - GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE, GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE, GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE, GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE, } from '../../../../../../packages/core/src/tracing/ai/gen-ai-attributes'; -import { conditionalTest, getStringAttributeValue, isOrchestrionEnabled } from '../../../../utils'; +import { conditionalTest, isOrchestrionEnabled } from '../../../../utils'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../../utils/runner'; import { createEsmTests } from '../../../../utils/runner/createEsmAndCjsTests'; @@ -213,70 +212,6 @@ conditionalTest({ min: 20 })('LangChain integration (v1)', () => { }, ); - createEsmAndCjsTests( - __dirname, - 'scenario-message-truncation.mjs', - 'instrument-with-truncation.mjs', - (createRunner, test) => { - test('truncates messages when they exceed byte limit', async () => { - await createRunner() - .ignore('event') - .expect({ transaction: { transaction: 'main' } }) - .expect({ - span: container => { - expect(container.items).toHaveLength(3); - // The string-input span has no system message (and therefore no system instructions), - // while the array-input span does — use that to distinguish the two truncated spans. - const truncatedContent = /^\[\{"role":"user","content":"C+"\}\]$/; - const stringInputSpan = container.items.find( - span => - span.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE] === undefined && - getStringAttributeValue(span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value)?.match( - truncatedContent, - ), - ); - expect(stringInputSpan).toBeDefined(); - expect(stringInputSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); - expect(stringInputSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toMatch(truncatedContent); - - const arrayInputSpan = container.items.find( - span => - span.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE] !== undefined && - getStringAttributeValue(span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value)?.match( - truncatedContent, - ), - ); - expect(arrayInputSpan).toBeDefined(); - expect(arrayInputSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); - expect(arrayInputSpan!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE].value).toMatch( - /^\[\{"type":"text","content":"A+"\}\]$/, - ); - - const smallMessageSpan = container.items.find( - span => - span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value === - JSON.stringify([{ role: 'user', content: 'This is a small message that fits within the limit' }]), - ); - expect(smallMessageSpan).toBeDefined(); - expect(smallMessageSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); - expect(smallMessageSpan!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE].value).toMatch( - /^\[\{"type":"text","content":"A+"\}\]$/, - ); - }, - }) - .start() - .completed(); - }); - }, - { - additionalDependencies: { - langchain: '^1.0.0', - '@langchain/core': '^1.0.0', - '@langchain/anthropic': '^1.0.0', - }, - }, - ); - createEsmTests( __dirname, 'scenario-openai-before-langchain.mjs', diff --git a/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument-no-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument-no-truncation.mjs deleted file mode 100644 index c3933a5ea129..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument-no-truncation.mjs +++ /dev/null @@ -1,17 +0,0 @@ -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.langGraphIntegration({ - recordInputs: true, - recordOutputs: true, - enableTruncation: false, - }), - ], -}); diff --git a/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument-streaming-with-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument-streaming-with-truncation.mjs deleted file mode 100644 index c76ba5928e20..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument-streaming-with-truncation.mjs +++ /dev/null @@ -1,16 +0,0 @@ -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, - traceLifecycle: 'stream', - integrations: [ - Sentry.langGraphIntegration({ - enableTruncation: true, - }), - ], -}); diff --git a/dev-packages/node-integration-tests/suites/tracing/langgraph/scenario-no-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/langgraph/scenario-no-truncation.mjs deleted file mode 100644 index 982e7a69de53..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/langgraph/scenario-no-truncation.mjs +++ /dev/null @@ -1,46 +0,0 @@ -import { END, MessagesAnnotation, START, StateGraph } from '@langchain/langgraph'; -import * as Sentry from '@sentry/node'; - -async function run() { - await Sentry.startSpan({ op: 'function', name: 'langgraph-test' }, async () => { - const mockLlm = () => { - return { - messages: [ - { - role: 'assistant', - content: 'Mock LLM response', - response_metadata: { - model_name: 'mock-model', - finish_reason: 'stop', - tokenUsage: { - promptTokens: 20, - completionTokens: 10, - totalTokens: 30, - }, - }, - }, - ], - }; - }; - - const graph = new StateGraph(MessagesAnnotation) - .addNode('agent', mockLlm) - .addEdge(START, 'agent') - .addEdge('agent', END) - .compile({ name: 'weather_assistant' }); - - // Multiple messages with long content (would normally be truncated and popped to last message only) - const longContent = 'A'.repeat(50_000); - await graph.invoke({ - messages: [ - { role: 'user', content: longContent }, - { role: 'assistant', content: 'Some reply' }, - { role: 'user', content: 'Follow-up question' }, - ], - }); - }); - - await Sentry.flush(2000); -} - -run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts b/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts index ed104fa52933..97d01cb151a8 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts @@ -278,91 +278,6 @@ describe('LangGraph integration', () => { }); }); - const longContent = 'A'.repeat(50_000); - - 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: { transaction: 'langgraph-test' } }) - .expect({ - span: container => { - const expectedMessages = JSON.stringify([ - { role: 'user', content: longContent }, - { role: 'assistant', content: 'Some reply' }, - { role: 'user', content: 'Follow-up question' }, - ]); - - expect(container.items).toHaveLength(2); - const invokeAgentSpan = container.items.find( - span => span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value === expectedMessages, - ); - - expect(invokeAgentSpan).toBeDefined(); - expect(invokeAgentSpan!.name).toBe('invoke_agent weather_assistant'); - }, - }) - .start() - .completed(); - }); - }, - ); - - 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 => - getStringAttributeValue(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. - const chatSpan = spans.find(s => - getStringAttributeValue(s.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value)?.startsWith( - '[{"role":"user","content":"AAAA', - ), - ); - expect(chatSpan).toBeDefined(); - expect( - (getStringAttributeValue(chatSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value) ?? '').length, - ).toBeLessThan(streamingLongContent.length); - }, - }) - .start() - .completed(); - }); - }, - ); - // createReactAgent tests. // Spans are asserted order-independently: the span-array order is not a protocol guarantee (Sentry // rebuilds the tree from `parent_span_id`), and the provider emits tree order while the OTel exporter @@ -490,4 +405,21 @@ describe('LangGraph integration', () => { .completed(); }); }); + + createEsmAndCjsTests(__dirname, 'scenario-span-streaming.mjs', 'instrument-streaming.mjs', (createRunner, test) => { + test('records full gen_ai input messages when span streaming is enabled', async () => { + const longContent = 'A'.repeat(50_000); + await createRunner() + .expect({ + span: container => { + const chatSpan = container.items.find(s => + getStringAttributeValue(s.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value)?.includes(longContent), + ); + expect(chatSpan).toBeDefined(); + }, + }) + .start() + .completed(); + }); + }); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/instrument-no-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/openai/instrument-no-truncation.mjs deleted file mode 100644 index 442163b75b6e..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/openai/instrument-no-truncation.mjs +++ /dev/null @@ -1,23 +0,0 @@ -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: false, outputs: false } }, - transport: loggingTransport, - integrations: [ - Sentry.openAIIntegration({ - recordInputs: true, - recordOutputs: true, - enableTruncation: false, - }), - ], - beforeSendTransaction: event => { - if (event.transaction.includes('/openai/')) { - return null; - } - return event; - }, -}); diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/instrument-streaming-with-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/openai/instrument-streaming-with-truncation.mjs deleted file mode 100644 index c61dffa4c1f1..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/openai/instrument-streaming-with-truncation.mjs +++ /dev/null @@ -1,16 +0,0 @@ -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, - traceLifecycle: 'stream', - integrations: [ - Sentry.openAIIntegration({ - enableTruncation: true, - }), - ], -}); diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/instrument-with-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/openai/instrument-with-truncation.mjs deleted file mode 100644 index cbd50b7a2262..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/openai/instrument-with-truncation.mjs +++ /dev/null @@ -1,17 +0,0 @@ -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; - }, -}); diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/scenario-no-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/openai/scenario-no-truncation.mjs deleted file mode 100644 index c5fe61c1ab66..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/openai/scenario-no-truncation.mjs +++ /dev/null @@ -1,87 +0,0 @@ -import * as Sentry from '@sentry/node'; -import express from 'express'; -import OpenAI from 'openai'; - -function startMockServer() { - const app = express(); - app.use(express.json({ limit: '10mb' })); - - app.post('/openai/chat/completions', (req, res) => { - res.send({ - id: 'chatcmpl-mock123', - object: 'chat.completion', - created: 1677652288, - model: req.body.model, - choices: [ - { - index: 0, - message: { role: 'assistant', content: 'Hello!' }, - finish_reason: 'stop', - }, - ], - usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 }, - }); - }); - - app.post('/openai/responses', (req, res) => { - res.send({ - id: 'resp_mock456', - object: 'response', - created_at: 1677652290, - model: req.body.model, - output: [ - { - type: 'message', - id: 'msg_mock_output_1', - status: 'completed', - role: 'assistant', - content: [{ type: 'output_text', text: 'Response text', annotations: [] }], - }, - ], - output_text: 'Response text', - status: 'completed', - usage: { input_tokens: 5, output_tokens: 3, total_tokens: 8 }, - }); - }); - - return new Promise(resolve => { - const server = app.listen(0, () => { - resolve(server); - }); - }); -} - -async function run() { - const server = await startMockServer(); - - await Sentry.startSpan({ op: 'function', name: 'main' }, async () => { - const client = new OpenAI({ - baseURL: `http://localhost:${server.address().port}/openai`, - apiKey: 'mock-api-key', - }); - - // Multiple messages with long content (would normally be truncated and popped to last message only) - const longContent = 'A'.repeat(50_000); - await client.chat.completions.create({ - model: 'gpt-4', - messages: [ - { role: 'user', content: longContent }, - { role: 'assistant', content: 'Some reply' }, - { role: 'user', content: 'Follow-up question' }, - ], - }); - - // Responses API with long string input (would normally be truncated) - const longStringInput = 'B'.repeat(50_000); - await client.responses.create({ - model: 'gpt-4', - input: longStringInput, - }); - }); - - // Flush is required when span streaming is enabled to ensure streamed spans are sent before the process exits - await Sentry.flush(); - server.close(); -} - -run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/test.ts b/dev-packages/node-integration-tests/suites/tracing/openai/test.ts index bbb93952136a..ba4b9aec346b 100644 --- a/dev-packages/node-integration-tests/suites/tracing/openai/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/openai/test.ts @@ -722,61 +722,6 @@ describe('OpenAI integration', () => { }); }); - const longContent = 'A'.repeat(50_000); - - 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: { - transaction: 'main', - }, - }) - .expect({ - span: container => { - expect(container.items).toHaveLength(2); - const chatCompletionSpan = container.items.find( - span => span.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]?.value === 'chatcmpl-mock123', - ); - expect(chatCompletionSpan).toBeDefined(); - expect(chatCompletionSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ - type: 'string', - value: 'chatcmpl-mock123', - }); - expect(chatCompletionSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toMatchObject({ - type: 'string', - value: JSON.stringify([ - { role: 'user', content: longContent }, - { role: 'assistant', content: 'Some reply' }, - { role: 'user', content: 'Follow-up question' }, - ]), - }); - - const responsesSpan = container.items.find( - span => span.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]?.value === 'resp_mock456', - ); - expect(responsesSpan).toBeDefined(); - expect(responsesSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ - type: 'string', - value: 'resp_mock456', - }); - expect(responsesSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toMatchObject({ - type: 'string', - value: 'B'.repeat(50_000), - }); - }, - }) - .start() - .completed(); - }); - }, - ); - createEsmAndCjsTests(__dirname, 'scenario-embeddings.mjs', 'instrument.mjs', (createRunner, test) => { test('creates openai related spans with genAI recording disabled', async () => { await createRunner() @@ -1143,146 +1088,6 @@ describe('OpenAI integration', () => { }); }); - createEsmAndCjsTests( - __dirname, - 'truncation/scenario-message-truncation-completions.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() - .ignore('event') - .expect({ - transaction: { - transaction: 'main', - }, - }) - .expect({ - span: container => { - expect(container.items).toHaveLength(2); - const truncatedMessageSpan = container.items.find(span => - getStringAttributeValue(span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value)?.match( - /^\[\{"role":"user","content":"C+"\}\]$/, - ), - ); - expect(truncatedMessageSpan).toBeDefined(); - expect(truncatedMessageSpan!.name).toBe('chat gpt-3.5-turbo'); - expect(truncatedMessageSpan!.status).toBe('ok'); - expect(truncatedMessageSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ - type: 'string', - value: 'chat', - }); - expect(truncatedMessageSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ - type: 'string', - value: 'gen_ai.chat', - }); - expect(truncatedMessageSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ - type: 'string', - value: 'auto.ai.openai', - }); - expect(truncatedMessageSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ - type: 'string', - value: 'openai', - }); - expect(truncatedMessageSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ - type: 'string', - value: 'gpt-3.5-turbo', - }); - expect(truncatedMessageSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toMatch( - /^\[\{"role":"user","content":"C+"\}\]$/, - ); - expect(truncatedMessageSpan!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE].value).toMatch( - /^\[\{"type":"text","content":"A+"\}\]$/, - ); - - const smallMessageSpan = container.items.find( - span => - span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value === - JSON.stringify([{ role: 'user', content: 'This is a small message that fits within the limit' }]), - ); - expect(smallMessageSpan).toBeDefined(); - expect(smallMessageSpan!.name).toBe('chat gpt-3.5-turbo'); - expect(smallMessageSpan!.status).toBe('ok'); - expect(smallMessageSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ - type: 'string', - value: 'chat', - }); - expect(smallMessageSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ - type: 'string', - value: 'gen_ai.chat', - }); - expect(smallMessageSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ - type: 'string', - value: 'auto.ai.openai', - }); - expect(smallMessageSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ - type: 'string', - value: 'openai', - }); - expect(smallMessageSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ - type: 'string', - value: 'gpt-3.5-turbo', - }); - expect(smallMessageSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ - type: 'string', - value: JSON.stringify([ - { role: 'user', content: 'This is a small message that fits within the limit' }, - ]), - }); - expect(smallMessageSpan!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE].value).toMatch( - /^\[\{"type":"text","content":"A+"\}\]$/, - ); - }, - }) - .start() - .completed(); - }); - }, - ); - - createEsmAndCjsTests( - __dirname, - 'truncation/scenario-message-truncation-responses.mjs', - 'instrument-with-truncation.mjs', - (createRunner, test) => { - test('truncates string inputs when they exceed byte limit', async () => { - await createRunner() - .ignore('event') - .expect({ - transaction: { - transaction: 'main', - }, - }) - .expect({ - span: container => { - expect(container.items).toHaveLength(1); - const [firstSpan] = container.items; - - // [0] long A-string input is truncated - expect(firstSpan!.name).toBe('chat gpt-3.5-turbo'); - expect(firstSpan!.status).toBe('ok'); - expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); - expect(firstSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ - type: 'string', - value: 'gen_ai.chat', - }); - expect(firstSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ - type: 'string', - value: 'auto.ai.openai', - }); - expect(firstSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); - expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ - type: 'string', - value: 'gpt-3.5-turbo', - }); - expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toMatch(/^A+$/); - }, - }) - .start() - .completed(); - }); - }, - ); - // Test for conversation ID support (Conversations API and previous_response_id) createEsmAndCjsTests(__dirname, 'scenario-conversation.mjs', 'instrument.mjs', (createRunner, test) => { test('captures conversation ID from Conversations API and previous_response_id', async () => { @@ -1563,7 +1368,7 @@ describe('OpenAI integration', () => { }); }); - createEsmAndCjsTests(__dirname, 'scenario-vision.mjs', 'instrument-with-truncation.mjs', (createRunner, test) => { + createEsmAndCjsTests(__dirname, 'scenario-vision.mjs', 'instrument-with-pii.mjs', (createRunner, test) => { test('redacts inline base64 image data in vision requests', async () => { await createRunner() .ignore('event') @@ -1617,27 +1422,22 @@ describe('OpenAI integration', () => { }); }); - const streamingLongContent = 'A'.repeat(50_000); - const streamingLongString = 'B'.repeat(50_000); - createEsmAndCjsTests(__dirname, 'scenario-span-streaming.mjs', 'instrument-streaming.mjs', (createRunner, test) => { - test('automatically disables truncation when span streaming is enabled', async () => { + test('records full gen_ai input messages when span streaming is enabled', async () => { + const longContent = 'A'.repeat(50_000); + const longStringInput = 'B'.repeat(50_000); await createRunner() .expect({ span: container => { const spans = container.items; const chatSpan = spans.find(s => - getStringAttributeValue(s.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value)?.includes( - streamingLongContent, - ), + getStringAttributeValue(s.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value)?.includes(longContent), ); expect(chatSpan).toBeDefined(); const responsesSpan = spans.find(s => - getStringAttributeValue(s.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value)?.includes( - streamingLongString, - ), + getStringAttributeValue(s.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value)?.includes(longStringInput), ); expect(responsesSpan).toBeDefined(); }, @@ -1646,44 +1446,4 @@ describe('OpenAI integration', () => { .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. - // Truncation keeps only the last message (50k 'A's) and crops it to the byte limit. - const chatSpan = spans.find(s => - getStringAttributeValue(s.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value)?.startsWith( - '[{"role":"user","content":"AAAA', - ), - ); - expect(chatSpan).toBeDefined(); - expect( - (getStringAttributeValue(chatSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value) ?? '').length, - ).toBeLessThan(streamingLongContent.length); - - // The responses API string input (50k 'B's) should also be truncated. - const responsesSpan = spans.find(s => - getStringAttributeValue(s.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value)?.startsWith('BBB'), - ); - expect(responsesSpan).toBeDefined(); - expect( - (getStringAttributeValue(responsesSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value) ?? '') - .length, - ).toBeLessThan(streamingLongString.length); - }, - }) - .start() - .completed(); - }); - }, - ); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/truncation/scenario-message-truncation-completions.mjs b/dev-packages/node-integration-tests/suites/tracing/openai/truncation/scenario-message-truncation-completions.mjs deleted file mode 100644 index f443ab3a47fe..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/openai/truncation/scenario-message-truncation-completions.mjs +++ /dev/null @@ -1,81 +0,0 @@ -import { instrumentOpenAiClient } from '@sentry/core'; -import * as Sentry from '@sentry/node'; - -class MockOpenAI { - constructor(config) { - this.apiKey = config.apiKey; - - this.chat = { - completions: { - create: async params => { - // Simulate processing time - await new Promise(resolve => setTimeout(resolve, 10)); - - return { - id: 'chatcmpl-completions-truncation-test', - object: 'chat.completion', - created: 1677652288, - model: params.model, - system_fingerprint: 'fp_44709d6fcb', - choices: [ - { - index: 0, - message: { - role: 'assistant', - content: 'Response to truncated messages', - }, - finish_reason: 'stop', - }, - ], - usage: { - prompt_tokens: 10, - completion_tokens: 15, - total_tokens: 25, - }, - }; - }, - }, - }; - } -} - -async function run() { - await Sentry.startSpan({ op: 'function', name: 'main' }, async () => { - const mockClient = new MockOpenAI({ - apiKey: 'mock-api-key', - }); - - 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 - const largeContent1 = 'A'.repeat(15000); // ~15KB - const largeContent2 = 'B'.repeat(15000); // ~15KB - const largeContent3 = 'C'.repeat(25000) + 'D'.repeat(25000); // ~50KB (will be truncated, only C's remain) - - await client.chat.completions.create({ - model: 'gpt-3.5-turbo', - messages: [ - { role: 'system', content: largeContent1 }, - { role: 'user', content: largeContent2 }, - { role: 'user', content: largeContent3 }, - ], - temperature: 0.7, - }); - - // Test 2: Given an array of messages only the last message should be kept - // The last message is small, so it should be kept intact - const smallContent = 'This is a small message that fits within the limit'; - await client.chat.completions.create({ - model: 'gpt-3.5-turbo', - messages: [ - { role: 'system', content: largeContent1 }, - { role: 'user', content: largeContent2 }, - { role: 'user', content: smallContent }, - ], - temperature: 0.7, - }); - }); -} - -run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/truncation/scenario-message-truncation-responses.mjs b/dev-packages/node-integration-tests/suites/tracing/openai/truncation/scenario-message-truncation-responses.mjs deleted file mode 100644 index 692f5fd87ca7..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/openai/truncation/scenario-message-truncation-responses.mjs +++ /dev/null @@ -1,93 +0,0 @@ -import { instrumentOpenAiClient } from '@sentry/core'; -import * as Sentry from '@sentry/node'; - -class MockOpenAI { - constructor(config) { - this.apiKey = config.apiKey; - - this.responses = { - create: async params => { - // Simulate processing time - await new Promise(resolve => setTimeout(resolve, 10)); - - return { - id: 'chatcmpl-responses-truncation-test', - object: 'response', - created_at: 1677652288, - status: 'completed', - error: null, - incomplete_details: null, - instructions: null, - max_output_tokens: null, - model: params.model, - output: [ - { - type: 'message', - id: 'message-123', - status: 'completed', - role: 'assistant', - content: [ - { - type: 'output_text', - text: 'Response to truncated messages', - annotations: [], - }, - ], - }, - ], - parallel_tool_calls: true, - previous_response_id: null, - reasoning: { - effort: null, - summary: null, - }, - store: true, - temperature: params.temperature, - text: { - format: { - type: 'text', - }, - }, - tool_choice: 'auto', - tools: [], - top_p: 1.0, - truncation: 'disabled', - usage: { - input_tokens: 10, - input_tokens_details: { - cached_tokens: 0, - }, - output_tokens: 15, - output_tokens_details: { - reasoning_tokens: 0, - }, - total_tokens: 25, - }, - user: null, - metadata: {}, - }; - }, - }; - } -} - -async function run() { - await Sentry.startSpan({ op: 'function', name: 'main' }, async () => { - const mockClient = new MockOpenAI({ - apiKey: 'mock-api-key', - }); - - 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 - - await client.responses.create({ - model: 'gpt-3.5-turbo', - input: largeContent, - temperature: 0.7, - }); - }); -} - -run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/instrument-no-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/vercelai/instrument-no-truncation.mjs deleted file mode 100644 index 64adb199d9c6..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/vercelai/instrument-no-truncation.mjs +++ /dev/null @@ -1,17 +0,0 @@ -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({ - recordInputs: true, - recordOutputs: true, - enableTruncation: false, - }), - ], -}); diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/instrument-with-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/vercelai/instrument-with-truncation.mjs deleted file mode 100644 index 0147e17d7b5f..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/vercelai/instrument-with-truncation.mjs +++ /dev/null @@ -1,11 +0,0 @@ -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 })], -}); diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/scenario-message-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/vercelai/scenario-message-truncation.mjs deleted file mode 100644 index 6c3f7327e64b..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/vercelai/scenario-message-truncation.mjs +++ /dev/null @@ -1,51 +0,0 @@ -import * as Sentry from '@sentry/node'; -import { generateText } from 'ai'; -import { MockLanguageModelV1 } from 'ai/test'; - -async function run() { - await Sentry.startSpan({ op: 'function', name: 'main' }, async () => { - const largeContent1 = 'A'.repeat(15000); // ~15KB - const largeContent2 = 'B'.repeat(15000); // ~15KB - const largeContent3 = 'C'.repeat(25000) + 'D'.repeat(25000); // ~50KB (will be truncated) - - // Test 1: Messages array with large last message that gets truncated - // Only the last message should be kept, and it should be truncated to only Cs - await generateText({ - experimental_telemetry: { isEnabled: true }, - model: new MockLanguageModelV1({ - doGenerate: async () => ({ - rawCall: { rawPrompt: null, rawSettings: {} }, - finishReason: 'stop', - usage: { promptTokens: 10, completionTokens: 5 }, - text: 'Response to truncated messages', - }), - }), - messages: [ - { role: 'user', content: largeContent1 }, - { role: 'assistant', content: largeContent2 }, - { role: 'user', content: largeContent3 }, - ], - }); - - // Test 2: Messages array where last message is small and kept intact - const smallContent = 'This is a small message that fits within the limit'; - await generateText({ - experimental_telemetry: { isEnabled: true }, - model: new MockLanguageModelV1({ - doGenerate: async () => ({ - rawCall: { rawPrompt: null, rawSettings: {} }, - finishReason: 'stop', - usage: { promptTokens: 10, completionTokens: 5 }, - text: 'Response to small message', - }), - }), - messages: [ - { role: 'user', content: largeContent1 }, - { role: 'assistant', content: largeContent2 }, - { role: 'user', content: smallContent }, - ], - }); - }); -} - -run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/scenario-no-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/vercelai/scenario-no-truncation.mjs deleted file mode 100644 index 415c13ef9acf..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/vercelai/scenario-no-truncation.mjs +++ /dev/null @@ -1,28 +0,0 @@ -import * as Sentry from '@sentry/node'; -import { generateText } from 'ai'; -import { MockLanguageModelV1 } from 'ai/test'; - -async function run() { - await Sentry.startSpan({ op: 'function', name: 'main' }, async () => { - // Multiple messages with long content (would normally be truncated and popped to last message only) - const longContent = 'A'.repeat(50_000); - await generateText({ - experimental_telemetry: { isEnabled: true }, - model: new MockLanguageModelV1({ - doGenerate: async () => ({ - rawCall: { rawPrompt: null, rawSettings: {} }, - finishReason: 'stop', - usage: { promptTokens: 10, completionTokens: 5 }, - text: 'Response', - }), - }), - messages: [ - { role: 'user', content: longContent }, - { role: 'assistant', content: 'Some reply' }, - { role: 'user', content: 'Follow-up question' }, - ], - }); - }); -} - -run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/span-streaming-v4/instrument-with-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/vercelai/span-streaming-v4/instrument-with-truncation.mjs deleted file mode 100644 index 3961dba5ba9f..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/vercelai/span-streaming-v4/instrument-with-truncation.mjs +++ /dev/null @@ -1,16 +0,0 @@ -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, - traceLifecycle: 'stream', - integrations: [ - Sentry.vercelAIIntegration({ - enableTruncation: true, - }), - ], -}); diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/span-streaming-v4/scenario-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/vercelai/span-streaming-v4/scenario-truncation.mjs deleted file mode 100644 index ebe0becaad35..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/vercelai/span-streaming-v4/scenario-truncation.mjs +++ /dev/null @@ -1,27 +0,0 @@ -import * as Sentry from '@sentry/node'; -import { generateText } from 'ai'; -import { MockLanguageModelV1 } from 'ai/test'; - -async function run() { - await Sentry.startSpan({ op: 'function', name: 'main' }, async () => { - // Single long message so truncation must crop it - const longContent = 'A'.repeat(50_000); - await generateText({ - experimental_telemetry: { isEnabled: true }, - model: new MockLanguageModelV1({ - doGenerate: async () => ({ - rawCall: { rawPrompt: null, rawSettings: {} }, - finishReason: 'stop', - usage: { promptTokens: 10, completionTokens: 5 }, - text: 'Response', - }), - }), - messages: [{ role: 'user', content: longContent }], - }); - }); - - // Flush is required when span streaming is enabled to ensure streamed spans are sent before the process exits - await Sentry.flush(2000); -} - -run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/span-streaming-v4/test.ts b/dev-packages/node-integration-tests/suites/tracing/vercelai/span-streaming-v4/test.ts index f522f7766822..205a1a662ab2 100644 --- a/dev-packages/node-integration-tests/suites/tracing/vercelai/span-streaming-v4/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/span-streaming-v4/test.ts @@ -18,7 +18,7 @@ import { GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE, } from '../../../../../../packages/core/src/tracing/ai/gen-ai-attributes'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../../utils/runner'; -import { getStringAttributeValue, isOrchestrionEnabled } from '../../../../utils'; +import { isOrchestrionEnabled } from '../../../../utils'; /** * Helper to match a typed attribute value in a SerializedStreamedSpan. @@ -303,50 +303,4 @@ describe('Vercel AI integration (streaming v4)', () => { await createRunner().ignore('event').expect({ span: EXPECTED_SPANS_ERROR_IN_TOOL }).start().completed(); }); }); - - const streamingLongContent = 'A'.repeat(50_000); - - createEsmAndCjsTests(__dirname, 'scenario-truncation.mjs', 'instrument.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 => - getStringAttributeValue(s.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value)?.includes( - streamingLongContent, - ), - ); - expect(chatSpan).toBeDefined(); - }, - }) - .start() - .completed(); - }); - }); - - createEsmAndCjsTests(__dirname, 'scenario-truncation.mjs', 'instrument-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. - const chatSpan = spans.find(s => - getStringAttributeValue(s.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value)?.startsWith( - '[{"role":"user","content":"AAAA', - ), - ); - expect(chatSpan).toBeDefined(); - expect( - (getStringAttributeValue(chatSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value) ?? '').length, - ).toBeLessThan(streamingLongContent.length); - }, - }) - .start() - .completed(); - }); - }); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/test.ts b/dev-packages/node-integration-tests/suites/tracing/vercelai/test.ts index 1c79760227c9..099ea20d8a5d 100644 --- a/dev-packages/node-integration-tests/suites/tracing/vercelai/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/test.ts @@ -478,53 +478,6 @@ describe('Vercel AI integration (v4)', () => { }, ); - createEsmAndCjsTests( - __dirname, - 'scenario-message-truncation.mjs', - 'instrument-with-truncation.mjs', - (createRunner, test) => { - test('truncates messages when they exceed byte limit', async () => { - await createRunner() - .ignore('event') - .expect({ transaction: { transaction: 'main' } }) - .expect({ - span: container => { - expect(container.items).toHaveLength(4); - const truncatedInvokeAgentSpan = container.items.find( - span => - span.name === 'invoke_agent' && - getStringAttributeValue(span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value)?.match( - /^\[.*"(?:text|content)":"C+".*\]$/, - ), - ); - expect(truncatedInvokeAgentSpan).toBeDefined(); - expect(truncatedInvokeAgentSpan!.name).toBe('invoke_agent'); - expect(truncatedInvokeAgentSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); - expect(truncatedInvokeAgentSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toMatch( - /^\[.*"(?:text|content)":"C+".*\]$/, - ); - - const smallMessageInvokeAgentSpan = container.items.find( - span => - span.name === 'invoke_agent' && - getStringAttributeValue(span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value)?.includes( - 'This is a small message that fits within the limit', - ), - ); - expect(smallMessageInvokeAgentSpan).toBeDefined(); - expect(smallMessageInvokeAgentSpan!.name).toBe('invoke_agent'); - expect(smallMessageInvokeAgentSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); - expect(smallMessageInvokeAgentSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toContain( - 'This is a small message that fits within the limit', - ); - }, - }) - .start() - .completed(); - }); - }, - ); - createEsmAndCjsTests(__dirname, 'scenario-embeddings.mjs', 'instrument.mjs', (createRunner, test) => { test('creates embedding related spans with genAI recording disabled', async () => { await createRunner() @@ -615,52 +568,6 @@ describe('Vercel AI integration (v4)', () => { }); }); - const longContent = 'A'.repeat(50_000); - - 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() - .expect({ transaction: { transaction: 'main' } }) - .expect({ - span: container => { - expect(container.items).toHaveLength(2); - const invokeAgentSpan = container.items.find( - span => - span.name === 'invoke_agent' && - span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value === - JSON.stringify([ - { role: 'user', content: longContent }, - { role: 'assistant', content: 'Some reply' }, - { role: 'user', content: 'Follow-up question' }, - ]), - ); - expect(invokeAgentSpan).toBeDefined(); - expect(invokeAgentSpan!.name).toBe('invoke_agent'); - expect(invokeAgentSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); - expect(invokeAgentSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toBe( - JSON.stringify([ - { role: 'user', content: longContent }, - { role: 'assistant', content: 'Some reply' }, - { role: 'user', content: 'Follow-up question' }, - ]), - ); - - const generateContentSpan = container.items.find(span => span.name === 'generate_content mock-model-id'); - expect(generateContentSpan).toBeDefined(); - expect(generateContentSpan!.name).toBe('generate_content mock-model-id'); - expect(generateContentSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); - }, - }) - .start() - .completed(); - }); - }, - ); - createEsmAndCjsTests(__dirname, 'scenario-stream-text.mjs', 'instrument.mjs', (createRunner, test) => { test('creates ai spans for streamText (doStream)', async () => { await createRunner() diff --git a/packages/cloudflare/src/integrations/tracing/vercelai.ts b/packages/cloudflare/src/integrations/tracing/vercelai.ts index 9d28d41d5bb0..4ed5704f8b20 100644 --- a/packages/cloudflare/src/integrations/tracing/vercelai.ts +++ b/packages/cloudflare/src/integrations/tracing/vercelai.ts @@ -14,12 +14,6 @@ import { addVercelAiProcessors, defineIntegration } from '@sentry/core'; const INTEGRATION_NAME = 'VercelAI'; interface VercelAiOptions { - /** - * Enable or disable truncation of recorded input messages. - * Defaults to `true`. - */ - enableTruncation?: boolean; - // `recordInputs`/`recordOutputs` are intentionally omitted: this entrypoint only post-processes // spans the AI SDK already emitted, so it cannot decide whether inputs/outputs are recorded. // Control this per call via `experimental_telemetry.recordInputs`/`recordOutputs`, or use the diff --git a/packages/core/src/server-exports.ts b/packages/core/src/server-exports.ts index 8288320bbed9..8af5242426b9 100644 --- a/packages/core/src/server-exports.ts +++ b/packages/core/src/server-exports.ts @@ -57,7 +57,7 @@ export type { // AI instrumentation is only supported in server runtimes, so these exports are kept out of the browser entry to // avoid shipping the AI tracing code in browser bundles. export { addVercelAiProcessors, getProviderMetadataAttributes } from './tracing/vercel-ai'; -export { getTruncatedJsonString, shouldEnableTruncation, resolveAIRecordingOptions } from './tracing/ai/utils'; +export { getGenAiMessagesJsonString, resolveAIRecordingOptions } from './tracing/ai/utils'; export { GEN_AI_REQUEST_MODEL_ATTRIBUTE, GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE } from './tracing/ai/gen-ai-attributes'; export { _INTERNAL_getSpanContextForToolCallId, _INTERNAL_cleanupToolCallSpanContext } from './tracing/vercel-ai/utils'; export { toolCallSpanContextMap as _INTERNAL_toolCallSpanContextMap } from './tracing/vercel-ai/constants'; diff --git a/packages/core/src/tracing/ai/mediaStripping.ts b/packages/core/src/tracing/ai/mediaStripping.ts index cb8e5d7b959e..7f0988bd5e94 100644 --- a/packages/core/src/tracing/ai/mediaStripping.ts +++ b/packages/core/src/tracing/ai/mediaStripping.ts @@ -195,3 +195,78 @@ export function stripInlineMediaFromSingleMessage(part: ContentMedia): ContentMe } return strip; } + +/** + * Message with the OpenAI/Anthropic `content: [...]` array format. + */ +type ContentArrayMessage = { + [key: string]: unknown; + content: unknown[]; +}; + +/** + * Message with the Google GenAI `parts: [...]` format. + */ +type PartsMessage = { + [key: string]: unknown; + parts: unknown[]; +}; + +/** + * Check if a message has the OpenAI/Anthropic content array format. + */ +function isContentArrayMessage(message: unknown): message is ContentArrayMessage { + return message !== null && typeof message === 'object' && 'content' in message && Array.isArray(message.content); +} + +/** + * Check if a message has the Google GenAI parts format. + */ +function isPartsMessage(message: unknown): message is PartsMessage { + return ( + message !== null && + typeof message === 'object' && + 'parts' in message && + Array.isArray((message as PartsMessage).parts) && + (message as PartsMessage).parts.length > 0 + ); +} + +/** + * Strip inline media from an array of messages, returning a new array. + * + * This does NOT mutate the input, because the actual API/client still needs the real media. + * Recurses into OpenAI/Anthropic `content: [...]` arrays and Google GenAI `parts: [...]`, replacing + * inline binary/base64 data with a placeholder while preserving all other structure. + */ +export function stripInlineMediaFromMessages(messages: unknown[]): unknown[] { + return messages.map(message => { + let newMessage: Record | undefined = undefined; + if (!!message && typeof message === 'object') { + if (isContentArrayMessage(message)) { + newMessage = { + ...message, + content: stripInlineMediaFromMessages(message.content), + }; + } else if ('content' in message && isContentMedia(message.content)) { + newMessage = { + ...message, + content: stripInlineMediaFromSingleMessage(message.content), + }; + } + if (isPartsMessage(message)) { + newMessage = { + // might have to strip content AND parts + ...(newMessage ?? message), + parts: stripInlineMediaFromMessages(message.parts), + }; + } + if (isContentMedia(newMessage)) { + newMessage = stripInlineMediaFromSingleMessage(newMessage); + } else if (isContentMedia(message)) { + newMessage = stripInlineMediaFromSingleMessage(message); + } + } + return newMessage ?? message; + }); +} diff --git a/packages/core/src/tracing/ai/messageTruncation.ts b/packages/core/src/tracing/ai/messageTruncation.ts deleted file mode 100644 index 779cf332855b..000000000000 --- a/packages/core/src/tracing/ai/messageTruncation.ts +++ /dev/null @@ -1,409 +0,0 @@ -import { isContentMedia, stripInlineMediaFromSingleMessage } from './mediaStripping'; - -/** - * Default maximum size in bytes for GenAI messages. - * Messages exceeding this limit will be truncated. - */ -export const DEFAULT_GEN_AI_MESSAGES_BYTE_LIMIT = 20000; - -/** - * Message format used by OpenAI and Anthropic APIs. - */ -type ContentMessage = { - [key: string]: unknown; - content: string; -}; - -/** - * One block inside OpenAI / Anthropic `content: [...]` arrays (text, image_url, etc.). - */ -type ContentArrayBlock = { - [key: string]: unknown; - type: string; -}; - -/** - * Message format used by OpenAI and Anthropic APIs for media. - */ -type ContentArrayMessage = { - [key: string]: unknown; - content: ContentArrayBlock[]; -}; - -/** - * Message format used by Google GenAI API. - * Parts can be strings or objects with a text property. - */ -type PartsMessage = { - [key: string]: unknown; - parts: Array; -}; - -/** - * A part in a Google GenAI message that contains text. - */ -type TextPart = string | { text: string }; - -/** - * A part in a Google GenAI that contains media. - */ -type MediaPart = { - type: string; - content: string; -}; - -/** - * One element of an array-based message: OpenAI/Anthropic `content[]` or Google `parts`. - */ -type ArrayMessageItem = TextPart | MediaPart | ContentArrayBlock; - -/** - * Calculate the UTF-8 byte length of a string. - */ -const utf8Bytes = (text: string): number => { - return new TextEncoder().encode(text).length; -}; - -/** - * Calculate the UTF-8 byte length of a value's JSON representation. - */ -const jsonBytes = (value: unknown): number => { - return utf8Bytes(JSON.stringify(value)); -}; - -/** - * Truncate a string to fit within maxBytes (inclusive) when encoded as UTF-8. - * Uses binary search for efficiency with multi-byte characters. - * - * @param text - The string to truncate - * @param maxBytes - Maximum byte length (inclusive, UTF-8 encoded) - * @returns Truncated string whose UTF-8 byte length is at most maxBytes - */ -function truncateTextByBytes(text: string, maxBytes: number): string { - if (utf8Bytes(text) <= maxBytes) { - return text; - } - - let low = 0; - let high = text.length; - let bestFit = ''; - - while (low <= high) { - const mid = Math.floor((low + high) / 2); - const candidate = text.slice(0, mid); - const byteSize = utf8Bytes(candidate); - - if (byteSize <= maxBytes) { - bestFit = candidate; - low = mid + 1; - } else { - high = mid - 1; - } - } - - return bestFit; -} - -/** - * Extract text content from a message item. - * Handles plain strings and objects with a text property. - * - * @returns The text content - */ -function getItemText(item: ArrayMessageItem): string { - if (typeof item === 'string') { - return item; - } - if ('text' in item && typeof item.text === 'string') { - return item.text; - } - return ''; -} - -/** - * Create a new item with updated text content while preserving the original structure. - * - * @param item - Original item (string or object) - * @param text - New text content - * @returns New item with updated text - */ -function withItemText(item: ArrayMessageItem, text: string): ArrayMessageItem { - if (typeof item === 'string') { - return text; - } - return { ...item, text }; -} - -/** - * Check if a message has the OpenAI/Anthropic content format. - */ -function isContentMessage(message: unknown): message is ContentMessage { - return ( - message !== null && - typeof message === 'object' && - 'content' in message && - typeof (message as ContentMessage).content === 'string' - ); -} - -/** - * Check if a message has the OpenAI/Anthropic content array format. - */ -function isContentArrayMessage(message: unknown): message is ContentArrayMessage { - return message !== null && typeof message === 'object' && 'content' in message && Array.isArray(message.content); -} - -/** - * Check if a message has the Google GenAI parts format. - */ -function isPartsMessage(message: unknown): message is PartsMessage { - return ( - message !== null && - typeof message === 'object' && - 'parts' in message && - Array.isArray((message as PartsMessage).parts) && - (message as PartsMessage).parts.length > 0 - ); -} - -/** - * Truncate a message with `content: string` format (OpenAI/Anthropic). - * - * @param message - Message with content property - * @param maxBytes - Maximum byte limit - * @returns Array with truncated message, or empty array if it doesn't fit - */ -function truncateContentMessage(message: ContentMessage, maxBytes: number): unknown[] { - // Calculate overhead (message structure without content) - const emptyMessage = { ...message, content: '' }; - const overhead = jsonBytes(emptyMessage); - const availableForContent = maxBytes - overhead; - - if (availableForContent <= 0) { - return []; - } - - const truncatedContent = truncateTextByBytes(message.content, availableForContent); - return [{ ...message, content: truncatedContent }]; -} - -/** - * Extracts the array items and their key from an array-based message. - * Returns `null` key if neither `parts` nor `content` is a valid array. - */ -function getArrayItems(message: PartsMessage | ContentArrayMessage): { - key: 'parts' | 'content' | null; - items: ArrayMessageItem[]; -} { - if ('parts' in message && Array.isArray(message.parts)) { - return { key: 'parts', items: message.parts }; - } - if ('content' in message && Array.isArray(message.content)) { - return { key: 'content', items: message.content }; - } - return { key: null, items: [] }; -} - -/** - * Truncate a message with an array-based format. - * Handles both `parts: [...]` (Google GenAI) and `content: [...]` (OpenAI/Anthropic multimodal). - * Keeps as many complete items as possible, only truncating the first item if needed. - * - * @param message - Message with parts or content array - * @param maxBytes - Maximum byte limit - * @returns Array with truncated message, or empty array if it doesn't fit - */ -function truncateArrayMessage(message: PartsMessage | ContentArrayMessage, maxBytes: number): unknown[] { - const { key, items } = getArrayItems(message); - - if (key === null || items.length === 0) { - return []; - } - - // Calculate overhead by creating empty text items - const emptyItems = items.map(item => withItemText(item, '')); - const overhead = jsonBytes({ ...message, [key]: emptyItems }); - let remainingBytes = maxBytes - overhead; - - if (remainingBytes <= 0) { - return []; - } - - // Include items until we run out of space - const includedItems: ArrayMessageItem[] = []; - - for (const item of items) { - const text = getItemText(item); - const textSize = utf8Bytes(text); - - if (textSize <= remainingBytes) { - // Item fits: include it as-is - includedItems.push(item); - remainingBytes -= textSize; - } else if (includedItems.length === 0) { - // First item doesn't fit: truncate it - const truncated = truncateTextByBytes(text, remainingBytes); - if (truncated) { - includedItems.push(withItemText(item, truncated)); - } - break; - } else { - // Subsequent item doesn't fit: stop here - break; - } - } - - /* c8 ignore start - * for type safety only, algorithm guarantees SOME text included */ - if (includedItems.length <= 0) { - return []; - } else { - /* c8 ignore stop */ - return [{ ...message, [key]: includedItems }]; - } -} - -/** - * Truncate a single message to fit within maxBytes. - * - * Supports three message formats: - * - OpenAI/Anthropic: `{ ..., content: string }` - * - Vercel AI/OpenAI multimodal: `{ ..., content: Array<{type, text?, ...}> }` - * - Google GenAI: `{ ..., parts: Array }` - * - * @param message - The message to truncate - * @param maxBytes - Maximum byte limit for the message - * @returns Array containing the truncated message, or empty array if truncation fails - */ -function truncateSingleMessage(message: unknown, maxBytes: number): unknown[] { - if (!message) return []; - - // Handle plain strings (e.g., embeddings input) - if (typeof message === 'string') { - const truncated = truncateTextByBytes(message, maxBytes); - return truncated ? [truncated] : []; - } - - if (typeof message !== 'object') { - return []; - } - - if (isContentMessage(message)) { - return truncateContentMessage(message, maxBytes); - } - - if (isContentArrayMessage(message) || isPartsMessage(message)) { - return truncateArrayMessage(message, maxBytes); - } - - // Unknown message format: cannot truncate safely - return []; -} - -/** - * Strip the inline media from message arrays. - * - * This returns a stripped message. We do NOT want to mutate the data in place, - * because of course we still want the actual API/client to handle the media. - */ -function stripInlineMediaFromMessages(messages: unknown[]): unknown[] { - const stripped = messages.map(message => { - let newMessage: Record | undefined = undefined; - if (!!message && typeof message === 'object') { - if (isContentArrayMessage(message)) { - newMessage = { - ...message, - content: stripInlineMediaFromMessages(message.content), - }; - } else if ('content' in message && isContentMedia(message.content)) { - newMessage = { - ...message, - content: stripInlineMediaFromSingleMessage(message.content), - }; - } - if (isPartsMessage(message)) { - newMessage = { - // might have to strip content AND parts - ...(newMessage ?? message), - parts: stripInlineMediaFromMessages(message.parts), - }; - } - if (isContentMedia(newMessage)) { - newMessage = stripInlineMediaFromSingleMessage(newMessage); - } else if (isContentMedia(message)) { - newMessage = stripInlineMediaFromSingleMessage(message); - } - } - return newMessage ?? message; - }); - return stripped; -} - -/** - * Truncate an array of messages to fit within a byte limit. - * - * Strategy: - * - Always keeps only the last (newest) message - * - Strips inline media from the message - * - Truncates the message content if it exceeds the byte limit - * - * @param messages - Array of messages to truncate - * @param maxBytes - Maximum total byte limit for the message - * @returns Array containing only the last message (possibly truncated) - * - * @example - * ```ts - * const messages = [msg1, msg2, msg3, msg4]; // newest is msg4 - * const truncated = truncateMessagesByBytes(messages, 10000); - * // Returns [msg4] (truncated if needed) - * ``` - */ -function truncateMessagesByBytes(messages: unknown[], maxBytes: number): unknown[] { - // Early return for empty or invalid input - if (!Array.isArray(messages) || messages.length === 0) { - return messages; - } - - // The result is always a single-element array that callers wrap with - // JSON.stringify([message]), so subtract the 2-byte array wrapper ("[" and "]") - // to ensure the final serialized value stays under the limit. - const effectiveMaxBytes = maxBytes - 2; - - // Always keep only the last message - const lastMessage = messages[messages.length - 1]; - - // Strip inline media from the single message - const stripped = stripInlineMediaFromMessages([lastMessage]); - const strippedMessage = stripped[0]; - - // Check if it fits - const messageBytes = jsonBytes(strippedMessage); - if (messageBytes <= effectiveMaxBytes) { - return stripped; - } - - // Truncate the single message if needed - return truncateSingleMessage(strippedMessage, effectiveMaxBytes); -} - -/** - * Truncate GenAI messages using the default byte limit. - * - * Convenience wrapper around `truncateMessagesByBytes` with the default limit. - * - * @param messages - Array of messages to truncate - * @returns Truncated array of messages - */ -export function truncateGenAiMessages(messages: unknown[]): unknown[] { - return truncateMessagesByBytes(messages, DEFAULT_GEN_AI_MESSAGES_BYTE_LIMIT); -} - -/** - * Truncate GenAI string input using the default byte limit. - * - * @param input - The string to truncate - * @returns Truncated string - */ -export function truncateGenAiStringInput(input: string): string { - return truncateTextByBytes(input, DEFAULT_GEN_AI_MESSAGES_BYTE_LIMIT); -} diff --git a/packages/core/src/tracing/ai/utils.ts b/packages/core/src/tracing/ai/utils.ts index ff9e072bf271..2e35203f5662 100644 --- a/packages/core/src/tracing/ai/utils.ts +++ b/packages/core/src/tracing/ai/utils.ts @@ -16,7 +16,7 @@ import { GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE, GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE, } from './gen-ai-attributes'; -import { truncateGenAiMessages, truncateGenAiStringInput } from './messageTruncation'; +import { stripInlineMediaFromMessages } from './mediaStripping'; export interface AIRecordingOptions { recordInputs?: boolean; @@ -56,22 +56,6 @@ 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 because gen_ai spans are always sent through the v2 span path - * (full span streaming via `traceLifecycle: 'stream'`, or extraction into a v2 span envelope for - * static transactions). 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; - } - - return !getClient(); -} - /** * Build method path from current traversal */ @@ -186,20 +170,22 @@ export function endStreamSpan(span: Span, state: StreamResponseState, recordOutp } /** - * Get the truncated JSON string for a string, an array of messages, or an object. + * Serialize a string, an array of messages, or an object to a JSON string for span attributes, + * stripping inline media (base64 blobs, data URIs, etc.) from message arrays so large binary + * payloads never end up in span attributes. * - * @param value - The value to truncate and serialize - * @returns The truncated JSON string + * @param value - The value to serialize + * @returns The JSON string */ -export function getTruncatedJsonString(value: T | T[]): string { +export function getGenAiMessagesJsonString(value: T | T[]): string { if (typeof value === 'string') { // Some values are already JSON strings, so we don't need to duplicate the JSON parsing - return truncateGenAiStringInput(value); + return value; } - // Both truncation (media stripping recurses the value) and `JSON.stringify` can throw on - // circular refs or non-serializable values (e.g. BigInt); never let that crash instrumentation. + // Media stripping recurses the value and `JSON.stringify` can throw on circular refs or + // non-serializable values (e.g. BigInt); never let that crash instrumentation. try { - return JSON.stringify(Array.isArray(value) ? truncateGenAiMessages(value) : value); + return JSON.stringify(Array.isArray(value) ? stripInlineMediaFromMessages(value) : value); } catch { return '[unserializable]'; } diff --git a/packages/core/src/tracing/anthropic-ai/index.ts b/packages/core/src/tracing/anthropic-ai/index.ts index 64cd105905bc..af398887f6d9 100644 --- a/packages/core/src/tracing/anthropic-ai/index.ts +++ b/packages/core/src/tracing/anthropic-ai/index.ts @@ -21,12 +21,7 @@ import { GEN_AI_SYSTEM_ATTRIBUTE, } from '../ai/gen-ai-attributes'; import type { InstrumentedMethodEntry } from '../ai/utils'; -import { - resolveAIRecordingOptions, - setTokenUsageAttributes, - shouldEnableTruncation, - wrapPromiseWithMethods, -} from '../ai/utils'; +import { resolveAIRecordingOptions, setTokenUsageAttributes, wrapPromiseWithMethods } from '../ai/utils'; import { ANTHROPIC_METHOD_REGISTRY } from './constants'; import { instrumentAsyncIterableStream, instrumentMessageStream } from './streaming'; import type { AnthropicAiOptions, AnthropicAiResponse, AnthropicAiStreamingEvent, ContentBlock } from './types'; @@ -85,13 +80,9 @@ export function extractRequestAttributes( * Add private request attributes to spans. * This is only recorded if recordInputs is true. */ -export function addPrivateRequestAttributes( - span: Span, - params: Record, - enableTruncation: boolean, -): void { +export function addPrivateRequestAttributes(span: Span, params: Record): void { const messages = messagesFromParams(params); - setMessagesAttribute(span, messages, enableTruncation); + setMessagesAttribute(span, messages); if ('prompt' in params) { span.setAttributes({ [GEN_AI_PROMPT_ATTRIBUTE]: JSON.stringify(params.prompt) }); @@ -222,7 +213,7 @@ function handleStreamingRequest( originalResult = target.apply(invocationThis, args) as Promise; if (options.recordInputs && params) { - addPrivateRequestAttributes(span, params, shouldEnableTruncation(options.enableTruncation)); + addPrivateRequestAttributes(span, params); } return (async () => { @@ -244,7 +235,7 @@ function handleStreamingRequest( return startSpanManual(spanConfig, span => { try { if (options.recordInputs && params) { - addPrivateRequestAttributes(span, params, shouldEnableTruncation(options.enableTruncation)); + addPrivateRequestAttributes(span, params); } // The helper synchronously delegates to `create`; suppress that one internal call so it // does not produce a duplicate child span (see the dedup gate in `instrumentMethod`). @@ -323,7 +314,7 @@ function instrumentMethod( originalResult = target.apply(invocationThis, args) as Promise; if (options.recordInputs && params) { - addPrivateRequestAttributes(span, params, shouldEnableTruncation(options.enableTruncation)); + addPrivateRequestAttributes(span, params); } return originalResult.then( diff --git a/packages/core/src/tracing/anthropic-ai/types.ts b/packages/core/src/tracing/anthropic-ai/types.ts index dd61ff63e264..ba281ef82a0d 100644 --- a/packages/core/src/tracing/anthropic-ai/types.ts +++ b/packages/core/src/tracing/anthropic-ai/types.ts @@ -9,11 +9,6 @@ export interface AnthropicAiOptions { * Enable or disable output recording. */ recordOutputs?: boolean; - /** - * Enable or disable truncation of recorded input messages. - * Defaults to `true`. - */ - enableTruncation?: boolean; } export type Message = { diff --git a/packages/core/src/tracing/anthropic-ai/utils.ts b/packages/core/src/tracing/anthropic-ai/utils.ts index 5989247c111c..541af57b8743 100644 --- a/packages/core/src/tracing/anthropic-ai/utils.ts +++ b/packages/core/src/tracing/anthropic-ai/utils.ts @@ -3,14 +3,13 @@ import { SPAN_STATUS_ERROR } from '../../tracing'; import type { Span } from '../../types/span'; import type { SpanStatusType } from '../../types/spanStatus'; import { GEN_AI_INPUT_MESSAGES_ATTRIBUTE, GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE } from '../ai/gen-ai-attributes'; -import { extractSystemInstructions, getTruncatedJsonString } from '../ai/utils'; -import { stringify } from '../../utils/string'; +import { extractSystemInstructions, getGenAiMessagesJsonString } from '../ai/utils'; import type { AnthropicAiResponse } from './types'; /** - * Set the input messages attribute, extracting system instructions before truncation. + * Set the messages attribute, extracting system instructions first. */ -export function setMessagesAttribute(span: Span, messages: unknown, enableTruncation: boolean): void { +export function setMessagesAttribute(span: Span, messages: unknown): void { if (Array.isArray(messages) && messages.length === 0) { return; } @@ -24,9 +23,7 @@ export function setMessagesAttribute(span: Span, messages: unknown, enableTrunca } span.setAttributes({ - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: enableTruncation - ? getTruncatedJsonString(filteredMessages) - : stringify(filteredMessages), + [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: getGenAiMessagesJsonString(filteredMessages), }); } diff --git a/packages/core/src/tracing/google-genai/index.ts b/packages/core/src/tracing/google-genai/index.ts index 1909a0903764..f46ed48ccb57 100644 --- a/packages/core/src/tracing/google-genai/index.ts +++ b/packages/core/src/tracing/google-genai/index.ts @@ -27,13 +27,11 @@ import { GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE, } from '../ai/gen-ai-attributes'; import type { InstrumentedMethodEntry } from '../ai/utils'; -import { stringify } from '../../utils/string'; import { buildMethodPath, extractSystemInstructions, - getTruncatedJsonString, + getGenAiMessagesJsonString, resolveAIRecordingOptions, - shouldEnableTruncation, } from '../ai/utils'; import { GOOGLE_GENAI_METHOD_REGISTRY, GOOGLE_GENAI_SYSTEM_NAME } from './constants'; import { instrumentStream } from './streaming'; @@ -139,12 +137,7 @@ export function extractRequestAttributes( * This is only recorded if recordInputs is true. * Handles different parameter formats for different Google GenAI methods. */ -export function addPrivateRequestAttributes( - span: Span, - params: Record, - operationName: string, - enableTruncation: boolean, -): void { +export function addPrivateRequestAttributes(span: Span, params: Record, operationName: string): void { if (operationName === 'embeddings') { const contents = params.contents; if (contents != null) { @@ -192,9 +185,7 @@ export function addPrivateRequestAttributes( } span.setAttributes({ - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: enableTruncation - ? getTruncatedJsonString(filteredMessages) - : stringify(filteredMessages), + [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: getGenAiMessagesJsonString(filteredMessages), }); } } @@ -295,12 +286,7 @@ function instrumentMethod( async (span: Span) => { try { if (options.recordInputs && params) { - addPrivateRequestAttributes( - span, - params, - operationName, - shouldEnableTruncation(options.enableTruncation), - ); + addPrivateRequestAttributes(span, params, operationName); } const stream = await target.apply(context, args); return instrumentStream(stream, span, Boolean(options.recordOutputs)) as R; @@ -328,7 +314,7 @@ function instrumentMethod( }, (span: Span) => { if (options.recordInputs && params) { - addPrivateRequestAttributes(span, params, operationName, shouldEnableTruncation(options.enableTruncation)); + addPrivateRequestAttributes(span, params, operationName); } return handleCallbackErrors( diff --git a/packages/core/src/tracing/google-genai/types.ts b/packages/core/src/tracing/google-genai/types.ts index 35ca728a4a60..d1729dd68f36 100644 --- a/packages/core/src/tracing/google-genai/types.ts +++ b/packages/core/src/tracing/google-genai/types.ts @@ -9,11 +9,6 @@ export interface GoogleGenAIOptions { * Enable or disable output recording. */ recordOutputs?: boolean; - /** - * Enable or disable truncation of recorded input messages. - * Defaults to `true`. - */ - enableTruncation?: boolean; } /** diff --git a/packages/core/src/tracing/langchain/index.ts b/packages/core/src/tracing/langchain/index.ts index 621ae76acdd5..9397d1f60d34 100644 --- a/packages/core/src/tracing/langchain/index.ts +++ b/packages/core/src/tracing/langchain/index.ts @@ -12,7 +12,7 @@ import { GEN_AI_TOOL_NAME_ATTRIBUTE, GEN_AI_TOOL_OUTPUT_ATTRIBUTE, } from '../ai/gen-ai-attributes'; -import { resolveAIRecordingOptions, shouldEnableTruncation } from '../ai/utils'; +import { resolveAIRecordingOptions } from '../ai/utils'; import { LANGCHAIN_ORIGIN } from './constants'; import type { LangChainCallbackHandler, @@ -38,7 +38,6 @@ import { */ export function createLangChainCallbackHandler(options: LangChainOptions = {}): LangChainCallbackHandler { const { recordInputs, recordOutputs } = resolveAIRecordingOptions(options); - const enableTruncation = shouldEnableTruncation(options.enableTruncation); // Internal state - single instance tracks all spans const spanMap = new Map(); @@ -94,7 +93,6 @@ export function createLangChainCallbackHandler(options: LangChainOptions = {}): llm as LangChainSerialized, prompts, recordInputs, - enableTruncation, invocationParams, metadata, ); @@ -134,7 +132,6 @@ export function createLangChainCallbackHandler(options: LangChainOptions = {}): llm as LangChainSerialized, messages as LangChainMessage[][], recordInputs, - enableTruncation, invocationParams, metadata, ); diff --git a/packages/core/src/tracing/langchain/types.ts b/packages/core/src/tracing/langchain/types.ts index aae03680c0d1..c624fae7f918 100644 --- a/packages/core/src/tracing/langchain/types.ts +++ b/packages/core/src/tracing/langchain/types.ts @@ -13,12 +13,6 @@ export interface LangChainOptions { * @default false (respects `dataCollection.genAI.outputs`, or `sendDefaultPii` when `dataCollection` is not configured) */ recordOutputs?: boolean; - - /** - * Enable or disable truncation of recorded input messages. - * Defaults to `true`. - */ - enableTruncation?: boolean; } /** diff --git a/packages/core/src/tracing/langchain/utils.ts b/packages/core/src/tracing/langchain/utils.ts index db5fffa0e000..62c770d8511f 100644 --- a/packages/core/src/tracing/langchain/utils.ts +++ b/packages/core/src/tracing/langchain/utils.ts @@ -27,7 +27,7 @@ import { GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE, } from '../ai/gen-ai-attributes'; import { isContentMedia, stripInlineMediaFromSingleMessage } from '../ai/mediaStripping'; -import { extractSystemInstructions, getTruncatedJsonString } from '../ai/utils'; +import { extractSystemInstructions } from '../ai/utils'; import { LANGCHAIN_ORIGIN, ROLE_MAP } from './constants'; import type { LangChainLLMResult, LangChainMessage, LangChainSerialized } from './types'; @@ -277,7 +277,6 @@ export function extractLLMRequestAttributes( llm: LangChainSerialized, prompts: string[], recordInputs: boolean, - enableTruncation: boolean, invocationParams?: Record, langSmithMetadata?: Record, ): Record { @@ -288,11 +287,7 @@ export function extractLLMRequestAttributes( if (recordInputs && Array.isArray(prompts) && prompts.length > 0) { const messages = prompts.map(p => ({ role: 'user', content: p })); - setIfDefined( - attrs, - GEN_AI_INPUT_MESSAGES_ATTRIBUTE, - enableTruncation ? getTruncatedJsonString(messages) : stringify(messages), - ); + setIfDefined(attrs, GEN_AI_INPUT_MESSAGES_ATTRIBUTE, stringify(messages)); } return attrs; @@ -311,7 +306,6 @@ export function extractChatModelRequestAttributes( llm: LangChainSerialized, langChainMessages: LangChainMessage[][], recordInputs: boolean, - enableTruncation: boolean, invocationParams?: Record, langSmithMetadata?: Record, ): Record { @@ -329,11 +323,7 @@ export function extractChatModelRequestAttributes( setIfDefined(attrs, GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE, systemInstructions); } - setIfDefined( - attrs, - GEN_AI_INPUT_MESSAGES_ATTRIBUTE, - enableTruncation ? getTruncatedJsonString(filteredMessages) : stringify(filteredMessages), - ); + setIfDefined(attrs, GEN_AI_INPUT_MESSAGES_ATTRIBUTE, stringify(filteredMessages)); } return attrs; diff --git a/packages/core/src/tracing/langgraph/index.ts b/packages/core/src/tracing/langgraph/index.ts index ea6653e28bd1..a68d40d545b0 100644 --- a/packages/core/src/tracing/langgraph/index.ts +++ b/packages/core/src/tracing/langgraph/index.ts @@ -12,12 +12,7 @@ import { GEN_AI_REQUEST_MODEL_ATTRIBUTE, GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE, } from '../ai/gen-ai-attributes'; -import { - extractSystemInstructions, - getTruncatedJsonString, - resolveAIRecordingOptions, - shouldEnableTruncation, -} from '../ai/utils'; +import { extractSystemInstructions, resolveAIRecordingOptions } from '../ai/utils'; import { stringify } from '../../utils/string'; import type { SpanAttributeValue } from '../../types/span'; import { createLangChainCallbackHandler } from '../langchain'; @@ -222,11 +217,8 @@ export function instrumentCompiledGraphInvoke( span.setAttribute(GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE, systemInstructions); } - const enableTruncation = shouldEnableTruncation(options.enableTruncation); span.setAttributes({ - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: enableTruncation - ? getTruncatedJsonString(filteredMessages) - : stringify(filteredMessages), + [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: stringify(filteredMessages), }); } diff --git a/packages/core/src/tracing/langgraph/types.ts b/packages/core/src/tracing/langgraph/types.ts index 021099f369b1..b16f9718c69e 100644 --- a/packages/core/src/tracing/langgraph/types.ts +++ b/packages/core/src/tracing/langgraph/types.ts @@ -7,11 +7,6 @@ export interface LangGraphOptions { * Enable or disable output recording. */ recordOutputs?: boolean; - /** - * Enable or disable truncation of recorded input messages. - * Defaults to `true`. - */ - enableTruncation?: boolean; } /** diff --git a/packages/core/src/tracing/openai/index.ts b/packages/core/src/tracing/openai/index.ts index c15b10856f8e..c5888a9e13bb 100644 --- a/packages/core/src/tracing/openai/index.ts +++ b/packages/core/src/tracing/openai/index.ts @@ -15,13 +15,11 @@ import { GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE, } from '../ai/gen-ai-attributes'; import type { InstrumentedMethodEntry } from '../ai/utils'; -import { stringify } from '../../utils/string'; import { buildMethodPath, extractSystemInstructions, - getTruncatedJsonString, + getGenAiMessagesJsonString, resolveAIRecordingOptions, - shouldEnableTruncation, wrapPromiseWithMethods, } from '../ai/utils'; import { OPENAI_METHOD_REGISTRY } from './constants'; @@ -79,12 +77,7 @@ export function extractRequestAttributes(args: unknown[], operationName: string) } // Extract and record AI request inputs, if present. This is intentionally separate from response attributes. -export function addRequestAttributes( - span: Span, - params: Record, - operationName: string, - enableTruncation: boolean, -): void { +export function addRequestAttributes(span: Span, params: Record, operationName: string): void { // Store embeddings input on a separate attribute and do not truncate it if (operationName === 'embeddings' && 'input' in params) { const input = params.input; @@ -125,10 +118,7 @@ export function addRequestAttributes( span.setAttribute(GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE, systemInstructions); } - span.setAttribute( - GEN_AI_INPUT_MESSAGES_ATTRIBUTE, - enableTruncation ? getTruncatedJsonString(filteredMessages) : stringify(filteredMessages), - ); + span.setAttribute(GEN_AI_INPUT_MESSAGES_ATTRIBUTE, getGenAiMessagesJsonString(filteredMessages)); } /** @@ -164,7 +154,7 @@ function instrumentMethod( originalResult = originalMethod.apply(context, args); if (options.recordInputs && params) { - addRequestAttributes(span, params, operationName, shouldEnableTruncation(options.enableTruncation)); + addRequestAttributes(span, params, operationName); } // Return async processing @@ -202,7 +192,7 @@ function instrumentMethod( originalResult = originalMethod.apply(context, args); if (options.recordInputs && params) { - addRequestAttributes(span, params, operationName, shouldEnableTruncation(options.enableTruncation)); + addRequestAttributes(span, params, operationName); } return originalResult.then( diff --git a/packages/core/src/tracing/openai/types.ts b/packages/core/src/tracing/openai/types.ts index 794c7ca49f8a..dd6872bb691b 100644 --- a/packages/core/src/tracing/openai/types.ts +++ b/packages/core/src/tracing/openai/types.ts @@ -22,11 +22,6 @@ export interface OpenAiOptions { * Enable or disable output recording. */ recordOutputs?: boolean; - /** - * Enable or disable truncation of recorded input messages. - * Defaults to `true`. - */ - enableTruncation?: boolean; } export interface OpenAiClient { diff --git a/packages/core/src/tracing/vercel-ai/index.ts b/packages/core/src/tracing/vercel-ai/index.ts index c905ac980614..67292c412c33 100644 --- a/packages/core/src/tracing/vercel-ai/index.ts +++ b/packages/core/src/tracing/vercel-ai/index.ts @@ -2,7 +2,6 @@ import type { Client } from '../../client'; import { getClient } from '../../currentScopes'; import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../../semanticAttributes'; -import { shouldEnableTruncation } from '../ai/utils'; import type { Event } from '../../types/event'; import type { Span, SpanAttributes, SpanAttributeValue, SpanJSON, StreamedSpanJSON } from '../../types/span'; import { spanToJSON } from '../../utils/spanUtils'; @@ -86,13 +85,7 @@ function onVercelAiSpanStart(span: Span): void { return; } - const client = getClient(); - const integration = client?.getIntegrationByName('VercelAI') as - | { options?: { enableTruncation?: boolean } } - | undefined; - const enableTruncation = shouldEnableTruncation(integration?.options?.enableTruncation); - - processGenerateSpan(span, name, attributes, enableTruncation); + processGenerateSpan(span, name, attributes); } function vercelAiEventProcessor(event: Event): Event { @@ -426,7 +419,7 @@ function processToolCallSpan(span: Span, attributes: SpanAttributes): void { } } -function processGenerateSpan(span: Span, name: string, attributes: SpanAttributes, enableTruncation: boolean): void { +function processGenerateSpan(span: Span, name: string, attributes: SpanAttributes): void { span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto.vercelai.otel'); const nameWthoutAi = name.replace('ai.', ''); @@ -438,7 +431,7 @@ function processGenerateSpan(span: Span, name: string, attributes: SpanAttribute span.setAttribute('gen_ai.function_id', functionId); } - requestMessagesFromPrompt(span, attributes, enableTruncation); + requestMessagesFromPrompt(span, attributes); if (attributes[AI_MODEL_ID_ATTRIBUTE] && !attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]) { span.setAttribute(GEN_AI_RESPONSE_MODEL_ATTRIBUTE, attributes[AI_MODEL_ID_ATTRIBUTE]); diff --git a/packages/core/src/tracing/vercel-ai/utils.ts b/packages/core/src/tracing/vercel-ai/utils.ts index d5c01f6302a8..91f111ad2d1d 100644 --- a/packages/core/src/tracing/vercel-ai/utils.ts +++ b/packages/core/src/tracing/vercel-ai/utils.ts @@ -9,8 +9,7 @@ import { GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE, GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE, } from '../ai/gen-ai-attributes'; -import { extractSystemInstructions, getTruncatedJsonString } from '../ai/utils'; -import { stringify } from '../../utils/string'; +import { extractSystemInstructions, getGenAiMessagesJsonString } from '../ai/utils'; import { toolCallSpanContextMap } from './constants'; import type { TokenSummary, ToolCallSpanContext } from './types'; import { AI_PROMPT_ATTRIBUTE, AI_PROMPT_MESSAGES_ATTRIBUTE } from './vercel-ai-attributes'; @@ -221,7 +220,7 @@ export function convertUserInputToMessagesFormat(userInput: string): { role: str * Generate a request.messages JSON array from the prompt field in the * invoke_agent op */ -export function requestMessagesFromPrompt(span: Span, attributes: SpanAttributes, enableTruncation: boolean): void { +export function requestMessagesFromPrompt(span: Span, attributes: SpanAttributes): void { if ( typeof attributes[AI_PROMPT_ATTRIBUTE] === 'string' && !attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE] && @@ -240,7 +239,7 @@ export function requestMessagesFromPrompt(span: Span, attributes: SpanAttributes span.setAttribute(GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE, systemInstructions); } - const messagesJson = enableTruncation ? getTruncatedJsonString(filteredMessages) : stringify(filteredMessages); + const messagesJson = getGenAiMessagesJsonString(filteredMessages); span.setAttributes({ [AI_PROMPT_ATTRIBUTE]: messagesJson, @@ -260,16 +259,7 @@ export function requestMessagesFromPrompt(span: Span, attributes: SpanAttributes span.setAttribute(GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE, systemInstructions); } - // `extractSystemInstructions` returns the original array reference unchanged when no - // system message is extracted. When truncation is also disabled, re-serializing would - // reproduce the SDK's own input string, so we reuse it instead of allocating a second - // full-size copy of the payload (matters for large prompts in memory-constrained runtimes). - const messagesJson = - !enableTruncation && filteredMessages === messages - ? originalMessagesJson - : enableTruncation - ? getTruncatedJsonString(filteredMessages) - : stringify(filteredMessages); + const messagesJson = getGenAiMessagesJsonString(filteredMessages); span.setAttributes({ [AI_PROMPT_MESSAGES_ATTRIBUTE]: messagesJson, diff --git a/packages/core/src/tracing/workers-ai/index.ts b/packages/core/src/tracing/workers-ai/index.ts index c6be4824305a..8463d524c191 100644 --- a/packages/core/src/tracing/workers-ai/index.ts +++ b/packages/core/src/tracing/workers-ai/index.ts @@ -2,7 +2,7 @@ import { SPAN_STATUS_ERROR } from '../../tracing'; import { startSpan, startSpanManual } from '../../tracing/trace'; import type { Span } from '../../types/span'; import { isObjectLike } from '../../utils/is'; -import { resolveAIRecordingOptions, shouldEnableTruncation } from '../ai/utils'; +import { resolveAIRecordingOptions } from '../ai/utils'; import { instrumentWorkersAiStream } from './streaming'; import type { WorkersAiOptions } from './types'; import { addRequestAttributes, addResponseAttributes, extractRequestAttributes, getOperationName } from './utils'; @@ -65,7 +65,7 @@ function instrumentRun( } if (options.recordInputs) { - addRequestAttributes(span, inputs, operationName, shouldEnableTruncation(options.enableTruncation)); + addRequestAttributes(span, inputs, operationName); } return originalResult.then(result => { @@ -85,7 +85,7 @@ function instrumentRun( const originalResult = originalRun.apply(context, args) as Promise; if (options.recordInputs) { - addRequestAttributes(span, inputs, operationName, shouldEnableTruncation(options.enableTruncation)); + addRequestAttributes(span, inputs, operationName); } return originalResult.then(result => { diff --git a/packages/core/src/tracing/workers-ai/types.ts b/packages/core/src/tracing/workers-ai/types.ts index 33334a6ed1bf..5f4781b12e9c 100644 --- a/packages/core/src/tracing/workers-ai/types.ts +++ b/packages/core/src/tracing/workers-ai/types.ts @@ -1,12 +1,6 @@ import type { AIRecordingOptions } from '../ai/utils'; -export interface WorkersAiOptions extends AIRecordingOptions { - /** - * Enable or disable truncation of recorded input messages. - * Defaults to `true`. - */ - enableTruncation?: boolean; -} +export interface WorkersAiOptions extends AIRecordingOptions {} /** * Minimal shape of the Cloudflare Workers AI binding (`env.AI`). diff --git a/packages/core/src/tracing/workers-ai/utils.ts b/packages/core/src/tracing/workers-ai/utils.ts index 42ad16328982..13c7d803fdeb 100644 --- a/packages/core/src/tracing/workers-ai/utils.ts +++ b/packages/core/src/tracing/workers-ai/utils.ts @@ -20,8 +20,7 @@ import { GEN_AI_RESPONSE_TEXT_ATTRIBUTE, GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE, } from '../ai/gen-ai-attributes'; -import { extractSystemInstructions, getTruncatedJsonString, setTokenUsageAttributes } from '../ai/utils'; -import { stringify } from '../../utils/string'; +import { extractSystemInstructions, getGenAiMessagesJsonString, setTokenUsageAttributes } from '../ai/utils'; import { WORKERS_AI_ORIGIN, WORKERS_AI_PROVIDER_NAME } from './constants'; import type { WorkersAiInput, WorkersAiOutput } from './types'; @@ -89,12 +88,7 @@ export function extractRequestAttributes( * Record the request inputs (messages/prompt/embeddings input) on the span. * Only called when `recordInputs` is enabled. */ -export function addRequestAttributes( - span: Span, - inputs: unknown, - operationName: string, - enableTruncation: boolean, -): void { +export function addRequestAttributes(span: Span, inputs: unknown, operationName: string): void { if (!inputs || typeof inputs !== 'object') { return; } @@ -124,10 +118,7 @@ export function addRequestAttributes( span.setAttribute(GEN_AI_SYSTEM_INSTRUCTIONS, systemInstructions); } - span.setAttribute( - GEN_AI_INPUT_MESSAGES, - enableTruncation ? getTruncatedJsonString(filteredMessages) : stringify(filteredMessages), - ); + span.setAttribute(GEN_AI_INPUT_MESSAGES, getGenAiMessagesJsonString(filteredMessages)); } /** diff --git a/packages/core/test/lib/tracing/ai-message-truncation.test.ts b/packages/core/test/lib/tracing/ai-media-stripping.test.ts similarity index 56% rename from packages/core/test/lib/tracing/ai-message-truncation.test.ts rename to packages/core/test/lib/tracing/ai-media-stripping.test.ts index 951dc9be6a15..cc2f0b868529 100644 --- a/packages/core/test/lib/tracing/ai-message-truncation.test.ts +++ b/packages/core/test/lib/tracing/ai-media-stripping.test.ts @@ -1,18 +1,15 @@ import { describe, expect, it } from 'vitest'; -import { truncateGenAiMessages, truncateGenAiStringInput } from '../../../src/tracing/ai/messageTruncation'; -import { getTruncatedJsonString } from '../../../src/tracing/ai/utils'; +import { stripInlineMediaFromMessages } from '../../../src/tracing/ai/mediaStripping'; +import { getGenAiMessagesJsonString } from '../../../src/tracing/ai/utils'; -describe('message truncation utilities', () => { - describe('truncateGenAiMessages', () => { +describe('media stripping utilities', () => { + describe('stripInlineMediaFromMessages', () => { it('leaves empty/non-array/small messages alone', () => { - // @ts-expect-error - exercising invalid type code path - expect(truncateGenAiMessages(null)).toBe(null); - expect(truncateGenAiMessages([])).toStrictEqual([]); - expect(truncateGenAiMessages([{ text: 'hello' }])).toStrictEqual([{ text: 'hello' }]); - expect(truncateGenAiStringInput('hello')).toBe('hello'); + expect(stripInlineMediaFromMessages([])).toStrictEqual([]); + expect(stripInlineMediaFromMessages([{ text: 'hello' }])).toStrictEqual([{ text: 'hello' }]); }); - it('strips inline media from messages', () => { + it('strips inline media from messages, keeping all messages', () => { const b64 = Buffer.from('lots of data\n').toString('base64'); const removed = '[Blob substitute]'; const messages = [ @@ -93,12 +90,38 @@ describe('message truncation utilities', () => { // indented json makes for better diffs in test output const messagesJson = JSON.stringify(messages, null, 2); - const result = truncateGenAiMessages(messages); + const result = stripInlineMediaFromMessages(messages); // original messages objects must not be mutated expect(JSON.stringify(messages, null, 2)).toBe(messagesJson); - // only the last message should be kept (with media stripped) + // all messages are kept, with inline media stripped expect(result).toStrictEqual([ + { + role: 'user', + content: [ + { + type: 'image', + source: { + type: 'base64', + media_type: 'image/png', + data: removed, + }, + }, + ], + }, + { + role: 'user', + content: { + image_url: removed, + }, + }, + { + role: 'agent', + type: 'image', + content: { + b64_json: removed, + }, + }, { role: 'system', inlineData: { @@ -170,7 +193,7 @@ describe('message truncation utilities', () => { ]; const messagesJson = JSON.stringify(messages, null, 2); - const result = truncateGenAiMessages(messages); + const result = stripInlineMediaFromMessages(messages); // original messages must not be mutated expect(JSON.stringify(messages, null, 2)).toBe(messagesJson); @@ -212,7 +235,7 @@ describe('message truncation utilities', () => { }, ]; - const result = truncateGenAiMessages(messages); + const result = stripInlineMediaFromMessages(messages); expect(result).toStrictEqual([ { @@ -256,7 +279,7 @@ describe('message truncation utilities', () => { }, ]; - const result = truncateGenAiMessages(messages); + const result = stripInlineMediaFromMessages(messages); expect(result).toStrictEqual([ { @@ -301,7 +324,7 @@ describe('message truncation utilities', () => { ]; const messagesJson = JSON.stringify(messages, null, 2); - const result = truncateGenAiMessages(messages); + const result = stripInlineMediaFromMessages(messages); expect(JSON.stringify(messages, null, 2)).toBe(messagesJson); @@ -347,7 +370,7 @@ describe('message truncation utilities', () => { ]; const messagesJson = JSON.stringify(messages, null, 2); - const result = truncateGenAiMessages(messages); + const result = stripInlineMediaFromMessages(messages); expect(JSON.stringify(messages, null, 2)).toBe(messagesJson); @@ -389,7 +412,7 @@ describe('message truncation utilities', () => { }, ]; - const result = truncateGenAiMessages(messages); + const result = stripInlineMediaFromMessages(messages); expect(result).toStrictEqual([ { @@ -407,222 +430,41 @@ describe('message truncation utilities', () => { }, ]); }); + }); - const humongous = 'this is a long string '.repeat(10_000); - const giant = 'this is a long string '.repeat(1_000); - const big = 'this is a long string '.repeat(100); - - it('keeps only the last message without truncation when it fits the limit', () => { - // Multiple messages that together exceed 20KB, but last message is small - const messages = [ - { content: `1 ${humongous}` }, - { content: `2 ${humongous}` }, - { content: `3 ${big}` }, // last message - small enough to fit - ]; - - const result = truncateGenAiMessages(messages); - - // Should only keep the last message, unchanged - expect(result).toStrictEqual([{ content: `3 ${big}` }]); - }); - - it('keeps only the last message with truncation when it does not fit the limit', () => { - const messages = [{ content: `1 ${humongous}` }, { content: `2 ${humongous}` }, { content: `3 ${humongous}` }]; - const result = truncateGenAiMessages(messages); - const truncLen = 20_000 - 2 - JSON.stringify({ content: '' }).length; - expect(result).toStrictEqual([{ content: `3 ${humongous}`.substring(0, truncLen) }]); - }); - - it('drops if last message cannot be safely truncated', () => { - const messages = [ - { content: `1 ${humongous}` }, - { content: `2 ${humongous}` }, - { what_even_is_this: `? ${humongous}` }, - ]; - const result = truncateGenAiMessages(messages); - expect(result).toStrictEqual([]); - }); - - it('fully drops message if content cannot be made to fit', () => { - const messages = [{ some_other_field: humongous, content: 'hello' }]; - expect(truncateGenAiMessages(messages)).toStrictEqual([]); - }); - - it('truncates if the message content string will not fit', () => { - const messages = [{ content: `2 ${humongous}` }]; - const result = truncateGenAiMessages(messages); - const truncLen = 20_000 - 2 - JSON.stringify({ content: '' }).length; - expect(result).toStrictEqual([{ content: `2 ${humongous}`.substring(0, truncLen) }]); - }); - - it('fully drops message if first part overhead does not fit', () => { - const messages = [ - { - parts: [{ some_other_field: humongous }], - }, - ]; - expect(truncateGenAiMessages(messages)).toStrictEqual([]); - }); - - it('fully drops message if overhead too large', () => { - const messages = [ - { - some_other_field: humongous, - parts: [], - }, - ]; - expect(truncateGenAiMessages(messages)).toStrictEqual([]); - }); - - it('truncates if the first message part will not fit', () => { - const messages = [ - { - parts: [`2 ${humongous}`, { some_other_field: 'no text here' }], - }, - ]; - - const result = truncateGenAiMessages(messages); - - // interesting (unexpected?) edge case effect of this truncation. - // subsequent messages count towards truncation overhead limit, - // but are not included, even without their text. This is an edge - // case that seems unlikely in normal usage. - const truncLen = - 20_000 - - 2 - - JSON.stringify({ - parts: ['', { some_other_field: 'no text here', text: '' }], - }).length; - - expect(result).toStrictEqual([ - { - parts: [`2 ${humongous}`.substring(0, truncLen)], - }, - ]); - }); + describe('getGenAiMessagesJsonString', () => { + it('returns a fallback instead of throwing on circular references', () => { + const circular: Record = { role: 'user', content: 'hi' }; + circular.self = circular; - it('truncates if the first message part will not fit, text object', () => { - const messages = [ - { - parts: [{ text: `2 ${humongous}` }], - }, - ]; - const result = truncateGenAiMessages(messages); - const truncLen = - 20_000 - - 2 - - JSON.stringify({ - parts: [{ text: '' }], - }).length; - expect(result).toStrictEqual([ - { - parts: [ - { - text: `2 ${humongous}`.substring(0, truncLen), - }, - ], - }, - ]); + expect(getGenAiMessagesJsonString(circular)).toBe('[unserializable]'); + expect(getGenAiMessagesJsonString([circular])).toBe('[unserializable]'); }); - it('drops if subsequent message part will not fit, text object', () => { - const messages = [ - { - parts: [ - { text: `1 ${big}` }, - { some_other_field: 'ok' }, - { text: `2 ${big}` }, - { text: `3 ${big}` }, - { text: `4 ${giant}` }, - { text: `5 ${giant}` }, - { text: `6 ${big}` }, - { text: `7 ${big}` }, - { text: `8 ${big}` }, - ], - }, - ]; - const result = truncateGenAiMessages(messages); - expect(result).toStrictEqual([ - { - parts: [{ text: `1 ${big}` }, { some_other_field: 'ok' }, { text: `2 ${big}` }, { text: `3 ${big}` }], - }, - ]); + it('returns strings as-is and serializes objects', () => { + expect(getGenAiMessagesJsonString('hello')).toBe('hello'); + expect(getGenAiMessagesJsonString({ a: 1 })).toBe('{"a":1}'); }); - it('truncates content array message when first text item does not fit', () => { + it('strips inline media from message arrays while keeping all messages', () => { + const b64 = Buffer.from('lots of data\n').toString('base64'); const messages = [ + { role: 'user', content: 'first message' }, { role: 'user', - content: [{ type: 'text', text: `2 ${humongous}` }], + content: [{ type: 'image_url', image_url: { url: `data:image/png;base64,${b64}` } }], }, ]; - const result = truncateGenAiMessages(messages); - const truncLen = - 20_000 - - 2 - - JSON.stringify({ - role: 'user', - content: [{ type: 'text', text: '' }], - }).length; - expect(result).toStrictEqual([ - { - role: 'user', - content: [{ type: 'text', text: `2 ${humongous}`.substring(0, truncLen) }], - }, - ]); - }); - it('drops subsequent content array items that do not fit', () => { - const messages = [ - { - role: 'assistant', - content: [ - { type: 'text', text: `1 ${big}` }, - { type: 'image_url', url: 'https://example.com/img.png' }, - { type: 'text', text: `2 ${big}` }, - { type: 'text', text: `3 ${big}` }, - { type: 'text', text: `4 ${giant}` }, - { type: 'text', text: `5 ${giant}` }, - ], - }, - ]; - const result = truncateGenAiMessages(messages); - expect(result).toStrictEqual([ - { - role: 'assistant', - content: [ - { type: 'text', text: `1 ${big}` }, - { type: 'image_url', url: 'https://example.com/img.png' }, - { type: 'text', text: `2 ${big}` }, - { type: 'text', text: `3 ${big}` }, - ], - }, - ]); - }); + const result = getGenAiMessagesJsonString(messages); - it('drops content array message if overhead is too large', () => { - const messages = [ - { - some_other_field: humongous, - content: [{ type: 'text', text: 'hello' }], - }, - ]; - expect(truncateGenAiMessages(messages)).toStrictEqual([]); + expect(result).toBe( + JSON.stringify([ + { role: 'user', content: 'first message' }, + { role: 'user', content: [{ type: 'image_url', image_url: { url: '[Blob substitute]' } }] }, + ]), + ); + expect(result).not.toContain(b64); }); }); }); - -describe('getTruncatedJsonString', () => { - it('returns a fallback instead of throwing on circular references', () => { - const circular: Record = { role: 'user', content: 'hi' }; - circular.self = circular; - - expect(getTruncatedJsonString(circular)).toBe('[unserializable]'); - expect(getTruncatedJsonString([circular])).toBe('[unserializable]'); - }); - - it('serializes normal values as before', () => { - expect(getTruncatedJsonString('hello')).toBe('hello'); - expect(getTruncatedJsonString({ a: 1 })).toBe('{"a":1}'); - }); -}); diff --git a/packages/core/test/lib/tracing/ai/utils.test.ts b/packages/core/test/lib/tracing/ai/utils.test.ts index b46a047c3838..c310bd4a0475 100644 --- a/packages/core/test/lib/tracing/ai/utils.test.ts +++ b/packages/core/test/lib/tracing/ai/utils.test.ts @@ -1,10 +1,6 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { getCurrentScope, getGlobalScope, getIsolationScope, setCurrentClient } from '../../../../src'; -import { - resolveAIRecordingOptions, - shouldEnableTruncation, - wrapPromiseWithMethods, -} from '../../../../src/tracing/ai/utils'; +import { resolveAIRecordingOptions, wrapPromiseWithMethods } from '../../../../src/tracing/ai/utils'; import { getDefaultTestClientOptions, TestClient } from '../../../mocks/client'; describe('resolveAIRecordingOptions', () => { @@ -87,51 +83,6 @@ describe('resolveAIRecordingOptions', () => { }); }); -describe('shouldEnableTruncation', () => { - beforeEach(() => { - getCurrentScope().clear(); - getIsolationScope().clear(); - getGlobalScope().clear(); - getCurrentScope().setClient(undefined); - }); - - afterEach(() => { - getCurrentScope().clear(); - getIsolationScope().clear(); - getGlobalScope().clear(); - }); - - function setupClient(options: Parameters[0] = {}): void { - const client = new TestClient(getDefaultTestClientOptions({ tracesSampleRate: 1, ...options })); - setCurrentClient(client); - client.init(); - } - - it('defaults to true when no client is set', () => { - expect(shouldEnableTruncation(undefined)).toBe(true); - }); - - it('defaults to false with a default client', () => { - setupClient(); - expect(shouldEnableTruncation(undefined)).toBe(false); - }); - - it('defaults to false when span streaming is enabled (traceLifecycle: stream)', () => { - setupClient({ traceLifecycle: 'stream' }); - expect(shouldEnableTruncation(undefined)).toBe(false); - }); - - it('explicit enableTruncation: true overrides the default', () => { - setupClient(); - expect(shouldEnableTruncation(true)).toBe(true); - }); - - it('explicit enableTruncation: false overrides the default', () => { - setupClient(); - expect(shouldEnableTruncation(false)).toBe(false); - }); -}); - describe('wrapPromiseWithMethods', () => { /** * Creates a mock APIPromise that mimics the behavior of OpenAI/Anthropic SDK APIPromise. diff --git a/packages/core/test/lib/tracing/langchain-utils.test.ts b/packages/core/test/lib/tracing/langchain-utils.test.ts index f39e01b5c625..2882af83fc01 100644 --- a/packages/core/test/lib/tracing/langchain-utils.test.ts +++ b/packages/core/test/lib/tracing/langchain-utils.test.ts @@ -241,7 +241,7 @@ describe('extractChatModelRequestAttributes with multimodal content', () => { ], ]; - const attrs = extractChatModelRequestAttributes(serialized, messages, true, true); + const attrs = extractChatModelRequestAttributes(serialized, messages, true); const inputMessages = attrs[GEN_AI_INPUT_MESSAGES_ATTRIBUTE] as string | undefined; expect(inputMessages).toBeDefined(); diff --git a/packages/core/test/lib/tracing/vercel-ai-request-messages.test.ts b/packages/core/test/lib/tracing/vercel-ai-request-messages.test.ts index 25e9ed53f1eb..961d145ca2e6 100644 --- a/packages/core/test/lib/tracing/vercel-ai-request-messages.test.ts +++ b/packages/core/test/lib/tracing/vercel-ai-request-messages.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { getTruncatedJsonString } from '../../../src/tracing/ai/utils'; -import { stringify } from '../../../src/utils/string'; +import { getGenAiMessagesJsonString } from '../../../src/tracing/ai/utils'; import { GEN_AI_INPUT_MESSAGES_ATTRIBUTE, GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE, @@ -27,22 +26,20 @@ function createRecordingSpan(): { span: Span; recorded: Record } describe('requestMessagesFromPrompt (ai.prompt.messages string branch)', () => { - it('reuses the original string verbatim when no system message and truncation is off', () => { + it('serializes all messages when there is no system message', () => { const { span, recorded } = createRecordingSpan(); - // Deliberately non-canonical whitespace. Re-serializing (JSON.stringify(JSON.parse(x))) - // would strip it, so a byte-identical result proves the original string was reused. - const original = '[ { "role": "user", "content": "hello world" } ]'; - const attributes = { [AI_PROMPT_MESSAGES_ATTRIBUTE]: original } as unknown as SpanAttributes; + const messages = [{ role: 'user', content: 'hello world' }]; + const attributes = { [AI_PROMPT_MESSAGES_ATTRIBUTE]: JSON.stringify(messages) } as unknown as SpanAttributes; - requestMessagesFromPrompt(span, attributes, /* enableTruncation */ false); + requestMessagesFromPrompt(span, attributes); - expect(recorded[AI_PROMPT_MESSAGES_ATTRIBUTE]).toBe(original); - expect(recorded[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBe(original); + expect(recorded[AI_PROMPT_MESSAGES_ATTRIBUTE]).toBe(getGenAiMessagesJsonString(messages)); + expect(recorded[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBe(getGenAiMessagesJsonString(messages)); expect(recorded[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE]).toBeUndefined(); }); - it('extracts the system message and re-serializes the remainder when truncation is off', () => { + it('extracts the system message and serializes the remainder', () => { const { span, recorded } = createRecordingSpan(); const original = JSON.stringify([ @@ -51,31 +48,32 @@ describe('requestMessagesFromPrompt (ai.prompt.messages string branch)', () => { ]); const attributes = { [AI_PROMPT_MESSAGES_ATTRIBUTE]: original } as unknown as SpanAttributes; - requestMessagesFromPrompt(span, attributes, false); + requestMessagesFromPrompt(span, attributes); expect(recorded[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE]).toBe(JSON.stringify([{ type: 'text', content: 'be nice' }])); - // System message removed; output is the SDK's own serialization of just the remainder. - expect(recorded[AI_PROMPT_MESSAGES_ATTRIBUTE]).toBe(stringify([{ role: 'user', content: 'hello' }])); + // System message removed; output is just the remainder. + expect(recorded[AI_PROMPT_MESSAGES_ATTRIBUTE]).toBe( + getGenAiMessagesJsonString([{ role: 'user', content: 'hello' }]), + ); expect(recorded[AI_PROMPT_MESSAGES_ATTRIBUTE]).not.toBe(original); }); - it('keeps the truncation path untouched when truncation is on', () => { + it('keeps all messages and strips inline media', () => { const { span, recorded } = createRecordingSpan(); + const b64 = Buffer.from('lots of data\n').toString('base64'); const messages = [ { role: 'user', content: 'first' }, - { role: 'user', content: 'second' }, + { role: 'user', content: [{ type: 'image_url', image_url: { url: `data:image/png;base64,${b64}` } }] }, ]; - const original = JSON.stringify(messages); - const attributes = { [AI_PROMPT_MESSAGES_ATTRIBUTE]: original } as unknown as SpanAttributes; + const attributes = { [AI_PROMPT_MESSAGES_ATTRIBUTE]: JSON.stringify(messages) } as unknown as SpanAttributes; - requestMessagesFromPrompt(span, attributes, /* enableTruncation */ true); + requestMessagesFromPrompt(span, attributes); - // Output must equal the SDK's own truncated serialization (and therefore differ from the - // input), proving the fast-path reuse did NOT short-circuit the truncation branch. - expect(recorded[AI_PROMPT_MESSAGES_ATTRIBUTE]).toBe(getTruncatedJsonString(messages)); - expect(recorded[AI_PROMPT_MESSAGES_ATTRIBUTE]).not.toBe(original); - // Original (pre-truncation) message count is still reported. + expect(recorded[AI_PROMPT_MESSAGES_ATTRIBUTE]).toBe(getGenAiMessagesJsonString(messages)); + expect(recorded[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toContain('first'); + expect(recorded[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toContain('[Blob substitute]'); + expect(recorded[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).not.toContain(b64); }); it('does not throw and sets no attributes for malformed JSON', () => { @@ -83,7 +81,7 @@ describe('requestMessagesFromPrompt (ai.prompt.messages string branch)', () => { const attributes = { [AI_PROMPT_MESSAGES_ATTRIBUTE]: '{ not json' } as unknown as SpanAttributes; - expect(() => requestMessagesFromPrompt(span, attributes, false)).not.toThrow(); + expect(() => requestMessagesFromPrompt(span, attributes)).not.toThrow(); expect(Object.keys(recorded)).toHaveLength(0); }); }); diff --git a/packages/core/test/lib/utils/anthropic-utils.test.ts b/packages/core/test/lib/utils/anthropic-utils.test.ts index 59b6dd63fad2..7079e0f8f014 100644 --- a/packages/core/test/lib/utils/anthropic-utils.test.ts +++ b/packages/core/test/lib/utils/anthropic-utils.test.ts @@ -96,24 +96,23 @@ describe('anthropic-ai-utils', () => { }; const span = mock as unknown as Span; - it('sets length along with truncated value', () => { + it('sets the full message value without truncation', () => { const content = 'A'.repeat(200_000); - setMessagesAttribute(span, [{ role: 'user', content }], true); - const result = [{ role: 'user', content: 'A'.repeat(19970) }]; + setMessagesAttribute(span, [{ role: 'user', content }]); expect(mock.attributes).toStrictEqual({ - 'gen_ai.input.messages': JSON.stringify(result), + 'gen_ai.input.messages': JSON.stringify([{ role: 'user', content }]), }); }); - it('sets length to 1 for non-array input', () => { - setMessagesAttribute(span, { content: 'hello, world' }, true); + it('serializes non-array input as-is', () => { + setMessagesAttribute(span, { content: 'hello, world' }); expect(mock.attributes).toStrictEqual({ 'gen_ai.input.messages': '{"content":"hello, world"}', }); }); it('ignores empty array', () => { - setMessagesAttribute(span, [], true); + setMessagesAttribute(span, []); expect(mock.attributes).toStrictEqual({ 'gen_ai.input.messages': '{"content":"hello, world"}', }); diff --git a/packages/server-utils/src/integrations/tracing-channel/anthropic.ts b/packages/server-utils/src/integrations/tracing-channel/anthropic.ts index a6425437c7f7..06641a682e8a 100644 --- a/packages/server-utils/src/integrations/tracing-channel/anthropic.ts +++ b/packages/server-utils/src/integrations/tracing-channel/anthropic.ts @@ -12,7 +12,6 @@ import { instrumentMessageStream, resolveAIRecordingOptions, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - shouldEnableTruncation, startInactiveSpan, waitForTracingChannelBinding, } from '@sentry/core'; @@ -113,7 +112,6 @@ function createGenAiSpan( const params = typeof args[0] === 'object' && args[0] !== null ? (args[0] as Record) : undefined; const { recordInputs } = resolveAIRecordingOptions(options); - const enableTruncation = shouldEnableTruncation(options.enableTruncation); const attributes = extractAnthropicRequestAttributes(args, methodPath, operation); const model = (attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE] as string) || 'unknown'; @@ -126,7 +124,7 @@ function createGenAiSpan( }); if (recordInputs && params) { - addAnthropicRequestAttributes(span, params, enableTruncation); + addAnthropicRequestAttributes(span, params); } return span; diff --git a/packages/server-utils/src/integrations/tracing-channel/google-genai.ts b/packages/server-utils/src/integrations/tracing-channel/google-genai.ts index 312e4716cf61..93f9c6a313e2 100644 --- a/packages/server-utils/src/integrations/tracing-channel/google-genai.ts +++ b/packages/server-utils/src/integrations/tracing-channel/google-genai.ts @@ -12,7 +12,6 @@ import { instrumentGoogleGenAIStream, resolveAIRecordingOptions, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - shouldEnableTruncation, spanToJSON, startInactiveSpan, waitForTracingChannelBinding, @@ -116,7 +115,6 @@ function createGenAiSpan( const params = args[0] as Record | undefined; const { recordInputs } = resolveAIRecordingOptions(options); - const enableTruncation = shouldEnableTruncation(options.enableTruncation); const attributes = extractGoogleGenAIRequestAttributes(operation, params, data.self); const model = (attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE] as string) || 'unknown'; @@ -129,7 +127,7 @@ function createGenAiSpan( }); if (recordInputs && params) { - addGoogleGenAIRequestAttributes(span, params, operation, enableTruncation); + addGoogleGenAIRequestAttributes(span, params, operation); } return span; diff --git a/packages/server-utils/src/integrations/tracing-channel/openai.ts b/packages/server-utils/src/integrations/tracing-channel/openai.ts index b172c6924300..6692745d37ef 100644 --- a/packages/server-utils/src/integrations/tracing-channel/openai.ts +++ b/packages/server-utils/src/integrations/tracing-channel/openai.ts @@ -10,7 +10,6 @@ import { instrumentOpenAiStream, resolveAIRecordingOptions, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - shouldEnableTruncation, startInactiveSpan, waitForTracingChannelBinding, } from '@sentry/core'; @@ -91,7 +90,6 @@ function createGenAiSpan(data: OpenAiChatChannelContext, operation: string, opti const params = args[0] as Record | undefined; const { recordInputs } = resolveAIRecordingOptions(options); - const enableTruncation = shouldEnableTruncation(options.enableTruncation); const attributes = extractOpenAiRequestAttributes(args, operation); attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] = ORIGIN; @@ -104,7 +102,7 @@ function createGenAiSpan(data: OpenAiChatChannelContext, operation: string, opti }); if (recordInputs && params) { - addOpenAiRequestAttributes(span, params, operation, enableTruncation); + addOpenAiRequestAttributes(span, params, operation); } return span; diff --git a/packages/server-utils/src/vercel-ai/index.ts b/packages/server-utils/src/vercel-ai/index.ts index 64296801c322..dda2be5f43a1 100644 --- a/packages/server-utils/src/vercel-ai/index.ts +++ b/packages/server-utils/src/vercel-ai/index.ts @@ -16,12 +16,6 @@ export interface VercelAiOptions { * Integration-level options take precedence over global `dataCollection` config. */ recordOutputs?: boolean; - - /** - * Enable or disable truncation of recorded input messages. - * Defaults to `true`. - */ - enableTruncation?: boolean; } const _vercelAiIntegration = ((options: VercelAiOptions = {}) => { diff --git a/packages/server-utils/src/vercel-ai/vercel-ai-dc-subscriber.ts b/packages/server-utils/src/vercel-ai/vercel-ai-dc-subscriber.ts index bafc3b51bbd9..b6bdd4133289 100644 --- a/packages/server-utils/src/vercel-ai/vercel-ai-dc-subscriber.ts +++ b/packages/server-utils/src/vercel-ai/vercel-ai-dc-subscriber.ts @@ -32,11 +32,10 @@ import { GEN_AI_CONVERSATION_ID_ATTRIBUTE, GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE, getClient, + getGenAiMessagesJsonString, getProviderMetadataAttributes, - getTruncatedJsonString, isObjectLike, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - shouldEnableTruncation, SPAN_STATUS_ERROR, spanToJSON, spanToTraceContext, @@ -206,7 +205,6 @@ export type VercelAiTracingChannelFactory = (name: string) => export interface VercelAiChannelOptions { recordInputs?: boolean; recordOutputs?: boolean; - enableTruncation?: boolean; } /** @@ -366,7 +364,7 @@ export function createSpanFromMessage( return undefined; } - const { recordInputs, enableTruncation } = getRecordingOptions(event, channelOptions); + const { recordInputs } = getRecordingOptions(event, channelOptions); const provider = asString(event.provider); const modelId = asString(event.modelId); const callId = asString(event.callId); @@ -392,9 +390,9 @@ export function createSpanFromMessage( // `generateObject` builds the same `invoke_agent` span as `generateText` (non-streaming); its // distinct `ai.generateObject` operationId rides on `event.operationId`. The JSON-schema attribute // the OTel path derives from the SDK's Zod schema is not reconstructed on the channel path. - return buildInvokeAgentSpan(event, baseAttributes, recordInputs, enableTruncation, callId, type === 'streamText'); + return buildInvokeAgentSpan(event, baseAttributes, recordInputs, callId, type === 'streamText'); case 'languageModelCall': - return buildModelCallSpan(event, baseAttributes, recordInputs, enableTruncation, callId, modelId); + return buildModelCallSpan(event, baseAttributes, recordInputs, callId, modelId); case 'executeTool': return buildToolSpan(event, recordInputs); case 'embed': @@ -427,7 +425,6 @@ function buildInvokeAgentSpan( event: Record, baseAttributes: SpanAttributes, recordInputs: boolean, - enableTruncation: boolean, callId: string | undefined, isStream: boolean, ): Span { @@ -441,7 +438,7 @@ function buildInvokeAgentSpan( [VERCEL_AI_OPERATION_ID_ATTRIBUTE]: operationId, [GEN_AI_RESPONSE_STREAMING]: isStream, ...(functionId ? { [GEN_AI_FUNCTION_ID]: functionId } : {}), - ...(recordInputs ? buildInputMessageAttributes(event, enableTruncation) : {}), + ...(recordInputs ? buildInputMessageAttributes(event) : {}), }); if (isStream && callId) { invokeAgentSpanByCallId.set(callId, span); @@ -454,7 +451,6 @@ function buildModelCallSpan( event: Record, baseAttributes: SpanAttributes, recordInputs: boolean, - enableTruncation: boolean, callId: string | undefined, modelId: string | undefined, ): Span { @@ -465,7 +461,7 @@ function buildModelCallSpan( return startGenAiSpan(GEN_AI_GENERATE_CONTENT_OPERATION, modelId, { ...baseAttributes, [VERCEL_AI_OPERATION_ID_ATTRIBUTE]: operationId, - ...(recordInputs ? buildInputMessageAttributes(event, enableTruncation) : {}), + ...(recordInputs ? buildInputMessageAttributes(event) : {}), ...(recordInputs && Array.isArray(event.tools) ? { [GEN_AI_REQUEST_AVAILABLE_TOOLS]: stringify(event.tools) } : {}), }); } @@ -689,14 +685,12 @@ function getRecordingOptions( ): { recordInputs: boolean; recordOutputs: boolean; - enableTruncation: boolean; } { const genAI = getClient()?.getDataCollectionOptions().genAI; return { recordInputs: resolveRecording(channelOptions.recordInputs, event.recordInputs, genAI?.inputs), recordOutputs: resolveRecording(channelOptions.recordOutputs, event.recordOutputs, genAI?.outputs), - enableTruncation: shouldEnableTruncation(channelOptions.enableTruncation), }; } @@ -720,10 +714,7 @@ function resolveRecording(integrationOption: unknown, perCallOption: unknown, gl return globalDefault === true; } -function buildInputMessageAttributes( - event: Record, - enableTruncation: boolean, -): Record { +function buildInputMessageAttributes(event: Record): Record { const attributes: Record = {}; // `ai` >= 7 forbids system messages in `messages`/`prompt` and exposes the system prompt as a @@ -738,7 +729,7 @@ function buildInputMessageAttributes( // simpler `prompt` field is used. const messages = event.messages ?? event.prompt; if (messages !== undefined) { - attributes[GEN_AI_INPUT_MESSAGES] = enableTruncation ? getTruncatedJsonString(messages) : stringify(messages); + attributes[GEN_AI_INPUT_MESSAGES] = getGenAiMessagesJsonString(messages); } return attributes; diff --git a/packages/vercel-edge/src/integrations/tracing/vercelai.ts b/packages/vercel-edge/src/integrations/tracing/vercelai.ts index 7f5fbddf09fa..6f65c4f2c11f 100644 --- a/packages/vercel-edge/src/integrations/tracing/vercelai.ts +++ b/packages/vercel-edge/src/integrations/tracing/vercelai.ts @@ -14,12 +14,6 @@ import { addVercelAiProcessors, defineIntegration } from '@sentry/core'; const INTEGRATION_NAME = 'VercelAI' as const; interface VercelAiOptions { - /** - * Enable or disable truncation of recorded input messages. - * Defaults to `true`. - */ - enableTruncation?: boolean; - // `recordInputs`/`recordOutputs` are intentionally omitted: this entrypoint only post-processes // spans the AI SDK already emitted (no OTel patch or tracing channel in the edge runtime), so it // cannot decide whether inputs/outputs are recorded. Control this per call via From e626567707c2f995923c6b4b59fb79a4b0d6fc14 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Thu, 23 Jul 2026 14:03:38 +0200 Subject: [PATCH 4/4] test(node): Fix anthropic media test origin assertion under orchestrion The migrated media-stripping test hard-coded `auto.ai.anthropic`, which is `auto.ai.orchestrion.anthropic` under the orchestrion runtime. Gate it on `isOrchestrionEnabled()` like the rest of the suite. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../node-integration-tests/suites/tracing/anthropic/test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dev-packages/node-integration-tests/suites/tracing/anthropic/test.ts b/dev-packages/node-integration-tests/suites/tracing/anthropic/test.ts index 8f96dd044277..f4bf696ad527 100644 --- a/dev-packages/node-integration-tests/suites/tracing/anthropic/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/anthropic/test.ts @@ -640,7 +640,9 @@ describe('Anthropic integration', () => { 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['sentry.origin'].value).toBe( + isOrchestrionEnabled() ? 'auto.ai.orchestrion.anthropic' : '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'); },