From e6798ab4cb152aa0184fad5dd40a1373f9c9aadc Mon Sep 17 00:00:00 2001 From: Alexander Grattan <51346343+agrattan0820@users.noreply.github.com> Date: Thu, 5 Jun 2025 16:53:39 -0400 Subject: [PATCH 1/6] fix(sveltekit): Export `vercelAIIntegration` from `@sentry/node` (#16496) Closes #16494 --- packages/sveltekit/src/server/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/sveltekit/src/server/index.ts b/packages/sveltekit/src/server/index.ts index 8cc952bfb7e7..717dd7387c98 100644 --- a/packages/sveltekit/src/server/index.ts +++ b/packages/sveltekit/src/server/index.ts @@ -123,6 +123,7 @@ export { logger, consoleLoggingIntegration, createSentryWinstonTransport, + vercelAIIntegration, } from '@sentry/node'; // We can still leave this for the carrier init and type exports From 4f9c89ef1f5eac8b0bfeb5e410211c96032ae51b Mon Sep 17 00:00:00 2001 From: Daniel Griesser Date: Fri, 6 Jun 2025 09:32:07 +0200 Subject: [PATCH 2/6] chore: Add external contributor to CHANGELOG.md (#16498) This PR adds the external contributor to the CHANGELOG.md file, so that they are credited for their contribution. See #16496 Co-authored-by: AbhiPrasad <18689448+AbhiPrasad@users.noreply.github.com> --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1823b8c57462..c56e4c00ba52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +Work in this release was contributed by @agrattan0820. Thank you for your contribution! + ## 9.27.0 - feat(node): Expand how vercel ai input/outputs can be set ([#16455](https://github.com/getsentry/sentry-javascript/pull/16455)) From e0400020d9acddc4b9291d88006edf1c77311bc0 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Fri, 6 Jun 2025 13:46:28 +0200 Subject: [PATCH 3/6] feat(nestjs): Stop creating spans for `TracingInterceptor` (#16501) --- .../nestjs-fastify/tests/transactions.test.ts | 52 +++---------------- .../sentry-nest-instrumentation.ts | 8 ++- 2 files changed, 12 insertions(+), 48 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/nestjs-fastify/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-fastify/tests/transactions.test.ts index 1ac095f0d137..2b8c555d7322 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-fastify/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/tests/transactions.test.ts @@ -112,36 +112,6 @@ test('Sends an API route transaction', async ({ baseURL }) => { op: 'request_context.nestjs', origin: 'auto.http.otel.nestjs', }, - { - span_id: expect.stringMatching(/[a-f0-9]{16}/), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - data: { - 'sentry.origin': 'auto.middleware.nestjs', - 'sentry.op': 'middleware.nestjs', - }, - description: 'SentryTracingInterceptor', - parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - status: 'ok', - op: 'middleware.nestjs', - origin: 'auto.middleware.nestjs', - }, - { - span_id: expect.stringMatching(/[a-f0-9]{16}/), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - data: { - 'sentry.origin': 'auto.middleware.nestjs', - 'sentry.op': 'middleware.nestjs', - }, - description: 'SentryTracingInterceptor', - parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - status: 'ok', - op: 'middleware.nestjs', - origin: 'auto.middleware.nestjs', - }, { span_id: expect.stringMatching(/[a-f0-9]{16}/), trace_id: expect.stringMatching(/[a-f0-9]{32}/), @@ -183,29 +153,19 @@ test('Sends an API route transaction', async ({ baseURL }) => { status: 'ok', origin: 'manual', }, - { - span_id: expect.stringMatching(/[a-f0-9]{16}/), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - data: { - 'sentry.origin': 'auto.middleware.nestjs', - 'sentry.op': 'middleware.nestjs', - }, - description: 'Interceptors - After Route', - parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - status: 'ok', - op: 'middleware.nestjs', - origin: 'auto.middleware.nestjs', - }, ]), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), transaction: 'GET /test-transaction', - type: 'transaction', transaction_info: { source: 'route', }, + type: 'transaction', }), ); + + const spanDescriptions = transactionEvent.spans.map(span => span.description); + expect(spanDescriptions).not.toContain('SentryTracingInterceptor'); }); test('API route transaction includes nest middleware span. Spans created in and after middleware are nested correctly', async ({ diff --git a/packages/nestjs/src/integrations/sentry-nest-instrumentation.ts b/packages/nestjs/src/integrations/sentry-nest-instrumentation.ts index b333e2157ed0..fff9f92616f3 100644 --- a/packages/nestjs/src/integrations/sentry-nest-instrumentation.ts +++ b/packages/nestjs/src/integrations/sentry-nest-instrumentation.ts @@ -183,8 +183,12 @@ export class SentryNestInstrumentation extends InstrumentationBase { const parentSpan = getActiveSpan(); let afterSpan: Span | undefined; - // Check that we can reasonably assume that the target is an interceptor. - if (!context || !next || typeof next.handle !== 'function') { + if ( + !context || + !next || + typeof next.handle !== 'function' || // Check that we can reasonably assume that the target is an interceptor. + target.name === 'SentryTracingInterceptor' // We don't want to trace this internal interceptor + ) { return originalIntercept.apply(thisArgIntercept, argsIntercept); } From 3e47adda337a79ceca1ab5e93d671d3cdbfe3cf8 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Sun, 8 Jun 2025 18:47:39 -0400 Subject: [PATCH 4/6] feat(node): Update vercel ai spans as per new conventions (#16497) resolves https://github.com/getsentry/sentry-javascript/issues/16454 As per https://www.notion.so/sentry/WIP-Span-layout-of-AI-agents-1fa8b10e4b5d80f0a83dd251a5ea4ab1, we update the span names and ops to better match OpenTelemetry. This should make them more easily accessible to the new agents module view we are building. Now instead of a span name like `generateText.doGenerate`, we will show a name like `generate_text gpt-4o-mini` (which is the OTEL convention for the gen ai span name). Another example, tool calls will show the tool name so you'll get `execute_tool getWeather` instead of `ai.toolCall`. --- .../suites/tracing/vercelai/scenario.mjs | 30 +++ .../suites/tracing/vercelai/test.ts | 194 ++++++++++++++++-- .../{attributes.ts => ai_sdk_attributes.ts} | 0 .../integrations/tracing/vercelai/index.ts | 169 ++++++++------- 4 files changed, 300 insertions(+), 93 deletions(-) rename packages/node/src/integrations/tracing/vercelai/{attributes.ts => ai_sdk_attributes.ts} (100%) diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/scenario.mjs b/dev-packages/node-integration-tests/suites/tracing/vercelai/scenario.mjs index 9df798eed59e..9bfdd4a9793a 100644 --- a/dev-packages/node-integration-tests/suites/tracing/vercelai/scenario.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/scenario.mjs @@ -1,6 +1,7 @@ import * as Sentry from '@sentry/node'; import { generateText } from 'ai'; import { MockLanguageModelV1 } from 'ai/test'; +import { z } from 'zod'; async function run() { await Sentry.startSpan({ op: 'function', name: 'main' }, async () => { @@ -30,6 +31,35 @@ async function run() { prompt: 'Where is the second span?', }); + // This span should include tool calls and tool results + await generateText({ + model: new MockLanguageModelV1({ + doGenerate: async () => ({ + rawCall: { rawPrompt: null, rawSettings: {} }, + finishReason: 'tool-calls', + usage: { promptTokens: 15, completionTokens: 25 }, + text: 'Tool call completed!', + toolCalls: [ + { + toolCallType: 'function', + toolCallId: 'call-1', + toolName: 'getWeather', + args: '{ "location": "San Francisco" }', + }, + ], + }), + }), + tools: { + getWeather: { + parameters: z.object({ location: z.string() }), + execute: async args => { + return `Weather in ${args.location}: Sunny, 72°F`; + }, + }, + }, + prompt: 'What is the weather in San Francisco?', + }); + // This span should not be captured because we've disabled telemetry await generateText({ experimental_telemetry: { isEnabled: false }, 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 7876dbccb440..fdeec051389f 100644 --- a/dev-packages/node-integration-tests/suites/tracing/vercelai/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/test.ts @@ -26,11 +26,11 @@ describe('Vercel AI integration', () => { 'gen_ai.usage.output_tokens': 20, 'gen_ai.usage.total_tokens': 30, 'operation.name': 'ai.generateText', - 'sentry.op': 'ai.pipeline.generateText', + 'sentry.op': 'ai.pipeline.generate_text', 'sentry.origin': 'auto.vercelai.otel', }, description: 'generateText', - op: 'ai.pipeline.generateText', + op: 'ai.pipeline.generate_text', origin: 'auto.vercelai.otel', status: 'ok', }), @@ -38,7 +38,7 @@ describe('Vercel AI integration', () => { expect.objectContaining({ data: { 'sentry.origin': 'auto.vercelai.otel', - 'sentry.op': 'ai.run.doGenerate', + 'sentry.op': 'gen_ai.generate_text', 'operation.name': 'ai.generateText.doGenerate', 'ai.operationId': 'ai.generateText.doGenerate', 'ai.model.provider': 'mock-provider', @@ -59,8 +59,8 @@ describe('Vercel AI integration', () => { 'gen_ai.response.model': 'mock-model-id', 'gen_ai.usage.total_tokens': 30, }, - description: 'generateText.doGenerate', - op: 'ai.run.doGenerate', + description: 'generate_text mock-model-id', + op: 'gen_ai.generate_text', origin: 'auto.vercelai.otel', status: 'ok', }), @@ -83,11 +83,11 @@ describe('Vercel AI integration', () => { 'gen_ai.usage.output_tokens': 20, 'gen_ai.usage.total_tokens': 30, 'operation.name': 'ai.generateText', - 'sentry.op': 'ai.pipeline.generateText', + 'sentry.op': 'ai.pipeline.generate_text', 'sentry.origin': 'auto.vercelai.otel', }, description: 'generateText', - op: 'ai.pipeline.generateText', + op: 'ai.pipeline.generate_text', origin: 'auto.vercelai.otel', status: 'ok', }), @@ -95,7 +95,7 @@ describe('Vercel AI integration', () => { expect.objectContaining({ data: { 'sentry.origin': 'auto.vercelai.otel', - 'sentry.op': 'ai.run.doGenerate', + 'sentry.op': 'gen_ai.generate_text', 'operation.name': 'ai.generateText.doGenerate', 'ai.operationId': 'ai.generateText.doGenerate', 'ai.model.provider': 'mock-provider', @@ -119,8 +119,79 @@ describe('Vercel AI integration', () => { 'gen_ai.response.model': 'mock-model-id', 'gen_ai.usage.total_tokens': 30, }, - description: 'generateText.doGenerate', - op: 'ai.run.doGenerate', + description: 'generate_text mock-model-id', + op: 'gen_ai.generate_text', + origin: 'auto.vercelai.otel', + status: 'ok', + }), + // Fifth span - tool call generateText span + expect.objectContaining({ + data: { + 'ai.model.id': 'mock-model-id', + 'ai.model.provider': 'mock-provider', + 'ai.operationId': 'ai.generateText', + 'ai.pipeline.name': 'generateText', + 'ai.response.finishReason': 'tool-calls', + 'ai.settings.maxRetries': 2, + 'ai.settings.maxSteps': 1, + 'ai.streaming': false, + 'gen_ai.response.model': 'mock-model-id', + 'gen_ai.usage.input_tokens': 15, + 'gen_ai.usage.output_tokens': 25, + 'gen_ai.usage.total_tokens': 40, + 'operation.name': 'ai.generateText', + 'sentry.op': 'ai.pipeline.generate_text', + 'sentry.origin': 'auto.vercelai.otel', + }, + description: 'generateText', + op: 'ai.pipeline.generate_text', + origin: 'auto.vercelai.otel', + status: 'ok', + }), + // Sixth span - tool call doGenerate span + expect.objectContaining({ + data: { + 'ai.model.id': 'mock-model-id', + 'ai.model.provider': 'mock-provider', + 'ai.operationId': 'ai.generateText.doGenerate', + 'ai.pipeline.name': 'generateText.doGenerate', + 'ai.response.finishReason': 'tool-calls', + 'ai.response.id': expect.any(String), + 'ai.response.model': 'mock-model-id', + 'ai.response.timestamp': expect.any(String), + 'ai.settings.maxRetries': 2, + 'ai.streaming': false, + 'gen_ai.request.model': 'mock-model-id', + 'gen_ai.response.finish_reasons': ['tool-calls'], + 'gen_ai.response.id': expect.any(String), + 'gen_ai.response.model': 'mock-model-id', + 'gen_ai.system': 'mock-provider', + 'gen_ai.usage.input_tokens': 15, + 'gen_ai.usage.output_tokens': 25, + 'gen_ai.usage.total_tokens': 40, + 'operation.name': 'ai.generateText.doGenerate', + 'sentry.op': 'gen_ai.generate_text', + 'sentry.origin': 'auto.vercelai.otel', + }, + description: 'generate_text mock-model-id', + op: 'gen_ai.generate_text', + origin: 'auto.vercelai.otel', + status: 'ok', + }), + // Seventh span - tool call execution span + expect.objectContaining({ + data: { + 'ai.operationId': 'ai.toolCall', + 'ai.toolCall.id': 'call-1', + 'ai.toolCall.name': 'getWeather', + 'gen_ai.tool.call.id': 'call-1', + 'gen_ai.tool.name': 'getWeather', + 'operation.name': 'ai.toolCall', + 'sentry.op': 'gen_ai.execute_tool', + 'sentry.origin': 'auto.vercelai.otel', + }, + description: 'execute_tool getWeather', + op: 'gen_ai.execute_tool', origin: 'auto.vercelai.otel', status: 'ok', }), @@ -149,11 +220,11 @@ describe('Vercel AI integration', () => { 'gen_ai.usage.output_tokens': 20, 'gen_ai.usage.total_tokens': 30, 'operation.name': 'ai.generateText', - 'sentry.op': 'ai.pipeline.generateText', + 'sentry.op': 'ai.pipeline.generate_text', 'sentry.origin': 'auto.vercelai.otel', }, description: 'generateText', - op: 'ai.pipeline.generateText', + op: 'ai.pipeline.generate_text', origin: 'auto.vercelai.otel', status: 'ok', }), @@ -182,11 +253,11 @@ describe('Vercel AI integration', () => { 'gen_ai.usage.output_tokens': 20, 'gen_ai.usage.total_tokens': 30, 'operation.name': 'ai.generateText.doGenerate', - 'sentry.op': 'ai.run.doGenerate', + 'sentry.op': 'gen_ai.generate_text', 'sentry.origin': 'auto.vercelai.otel', }, - description: 'generateText.doGenerate', - op: 'ai.run.doGenerate', + description: 'generate_text mock-model-id', + op: 'gen_ai.generate_text', origin: 'auto.vercelai.otel', status: 'ok', }), @@ -209,11 +280,11 @@ describe('Vercel AI integration', () => { 'gen_ai.usage.output_tokens': 20, 'gen_ai.usage.total_tokens': 30, 'operation.name': 'ai.generateText', - 'sentry.op': 'ai.pipeline.generateText', + 'sentry.op': 'ai.pipeline.generate_text', 'sentry.origin': 'auto.vercelai.otel', }, description: 'generateText', - op: 'ai.pipeline.generateText', + op: 'ai.pipeline.generate_text', origin: 'auto.vercelai.otel', status: 'ok', }), @@ -221,7 +292,7 @@ describe('Vercel AI integration', () => { expect.objectContaining({ data: { 'sentry.origin': 'auto.vercelai.otel', - 'sentry.op': 'ai.run.doGenerate', + 'sentry.op': 'gen_ai.generate_text', 'operation.name': 'ai.generateText.doGenerate', 'ai.operationId': 'ai.generateText.doGenerate', 'ai.model.provider': 'mock-provider', @@ -245,8 +316,91 @@ describe('Vercel AI integration', () => { 'gen_ai.response.model': 'mock-model-id', 'gen_ai.usage.total_tokens': 30, }, - description: 'generateText.doGenerate', - op: 'ai.run.doGenerate', + description: 'generate_text mock-model-id', + op: 'gen_ai.generate_text', + origin: 'auto.vercelai.otel', + status: 'ok', + }), + // Fifth span - tool call generateText span (should include prompts when sendDefaultPii: true) + expect.objectContaining({ + data: { + 'ai.model.id': 'mock-model-id', + 'ai.model.provider': 'mock-provider', + 'ai.operationId': 'ai.generateText', + 'ai.pipeline.name': 'generateText', + 'ai.prompt': '{"prompt":"What is the weather in San Francisco?"}', + 'ai.response.finishReason': 'tool-calls', + 'ai.response.text': 'Tool call completed!', + 'ai.response.toolCalls': expect.any(String), + 'ai.settings.maxRetries': 2, + 'ai.settings.maxSteps': 1, + 'ai.streaming': false, + 'gen_ai.prompt': '{"prompt":"What is the weather in San Francisco?"}', + 'gen_ai.response.model': 'mock-model-id', + 'gen_ai.usage.input_tokens': 15, + 'gen_ai.usage.output_tokens': 25, + 'gen_ai.usage.total_tokens': 40, + 'operation.name': 'ai.generateText', + 'sentry.op': 'ai.pipeline.generate_text', + 'sentry.origin': 'auto.vercelai.otel', + }, + description: 'generateText', + op: 'ai.pipeline.generate_text', + origin: 'auto.vercelai.otel', + status: 'ok', + }), + // Sixth span - tool call doGenerate span (should include prompts when sendDefaultPii: true) + expect.objectContaining({ + data: { + 'ai.model.id': 'mock-model-id', + 'ai.model.provider': 'mock-provider', + 'ai.operationId': 'ai.generateText.doGenerate', + 'ai.pipeline.name': 'generateText.doGenerate', + 'ai.prompt.format': expect.any(String), + 'ai.prompt.messages': expect.any(String), + 'ai.prompt.toolChoice': expect.any(String), + 'ai.prompt.tools': expect.any(Array), + 'ai.response.finishReason': 'tool-calls', + 'ai.response.id': expect.any(String), + 'ai.response.model': 'mock-model-id', + 'ai.response.text': 'Tool call completed!', + 'ai.response.timestamp': expect.any(String), + 'ai.response.toolCalls': expect.any(String), + 'ai.settings.maxRetries': 2, + 'ai.streaming': false, + 'gen_ai.request.model': 'mock-model-id', + 'gen_ai.response.finish_reasons': ['tool-calls'], + 'gen_ai.response.id': expect.any(String), + 'gen_ai.response.model': 'mock-model-id', + 'gen_ai.system': 'mock-provider', + 'gen_ai.usage.input_tokens': 15, + 'gen_ai.usage.output_tokens': 25, + 'gen_ai.usage.total_tokens': 40, + 'operation.name': 'ai.generateText.doGenerate', + 'sentry.op': 'gen_ai.generate_text', + 'sentry.origin': 'auto.vercelai.otel', + }, + description: 'generate_text mock-model-id', + op: 'gen_ai.generate_text', + origin: 'auto.vercelai.otel', + status: 'ok', + }), + // Seventh span - tool call execution span + expect.objectContaining({ + data: { + 'ai.operationId': 'ai.toolCall', + 'ai.toolCall.args': expect.any(String), + 'ai.toolCall.id': 'call-1', + 'ai.toolCall.name': 'getWeather', + 'ai.toolCall.result': expect.any(String), + 'gen_ai.tool.call.id': 'call-1', + 'gen_ai.tool.name': 'getWeather', + 'operation.name': 'ai.toolCall', + 'sentry.op': 'gen_ai.execute_tool', + 'sentry.origin': 'auto.vercelai.otel', + }, + description: 'execute_tool getWeather', + op: 'gen_ai.execute_tool', origin: 'auto.vercelai.otel', status: 'ok', }), diff --git a/packages/node/src/integrations/tracing/vercelai/attributes.ts b/packages/node/src/integrations/tracing/vercelai/ai_sdk_attributes.ts similarity index 100% rename from packages/node/src/integrations/tracing/vercelai/attributes.ts rename to packages/node/src/integrations/tracing/vercelai/ai_sdk_attributes.ts diff --git a/packages/node/src/integrations/tracing/vercelai/index.ts b/packages/node/src/integrations/tracing/vercelai/index.ts index 44bc2dca915f..2c5faf04acef 100644 --- a/packages/node/src/integrations/tracing/vercelai/index.ts +++ b/packages/node/src/integrations/tracing/vercelai/index.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-dynamic-delete */ /* eslint-disable complexity */ import type { IntegrationFn } from '@sentry/core'; import { defineIntegration, SEMANTIC_ATTRIBUTE_SENTRY_OP, spanToJSON } from '@sentry/core'; @@ -7,12 +8,15 @@ import { AI_MODEL_ID_ATTRIBUTE, AI_MODEL_PROVIDER_ATTRIBUTE, AI_PROMPT_ATTRIBUTE, + AI_TELEMETRY_FUNCTION_ID_ATTRIBUTE, + AI_TOOL_CALL_ID_ATTRIBUTE, + AI_TOOL_CALL_NAME_ATTRIBUTE, AI_USAGE_COMPLETION_TOKENS_ATTRIBUTE, AI_USAGE_PROMPT_TOKENS_ATTRIBUTE, GEN_AI_RESPONSE_MODEL_ATTRIBUTE, GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE, GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE, -} from './attributes'; +} from './ai_sdk_attributes'; import { INTEGRATION_NAME } from './constants'; import { SentryVercelAiInstrumentation } from './instrumentation'; import type { VercelAiOptions } from './types'; @@ -37,81 +41,30 @@ const _vercelAIIntegration = ((options: VercelAiOptions = {}) => { return; } + // Tool call spans + // https://ai-sdk.dev/docs/ai-sdk-core/telemetry#tool-call-spans + if ( + attributes[AI_TOOL_CALL_NAME_ATTRIBUTE] && + attributes[AI_TOOL_CALL_ID_ATTRIBUTE] && + name === 'ai.toolCall' + ) { + addOriginToSpan(span, 'auto.vercelai.otel'); + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'gen_ai.execute_tool'); + span.setAttribute('gen_ai.tool.call.id', attributes[AI_TOOL_CALL_ID_ATTRIBUTE]); + span.setAttribute('gen_ai.tool.name', attributes[AI_TOOL_CALL_NAME_ATTRIBUTE]); + span.updateName(`execute_tool ${attributes[AI_TOOL_CALL_NAME_ATTRIBUTE]}`); + return; + } + + // The AI and Provider must be defined for generate, stream, and embed spans. // The id of the model const aiModelId = attributes[AI_MODEL_ID_ATTRIBUTE]; - // the provider of the model const aiModelProvider = attributes[AI_MODEL_PROVIDER_ATTRIBUTE]; - - // both of these must be defined for the integration to work if (typeof aiModelId !== 'string' || typeof aiModelProvider !== 'string' || !aiModelId || !aiModelProvider) { return; } - let isPipelineSpan = false; - - switch (name) { - case 'ai.generateText': { - span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'ai.pipeline.generateText'); - isPipelineSpan = true; - break; - } - case 'ai.generateText.doGenerate': { - span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'ai.run.doGenerate'); - break; - } - case 'ai.streamText': { - span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'ai.pipeline.streamText'); - isPipelineSpan = true; - break; - } - case 'ai.streamText.doStream': { - span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'ai.run.doStream'); - break; - } - case 'ai.generateObject': { - span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'ai.pipeline.generateObject'); - isPipelineSpan = true; - break; - } - case 'ai.generateObject.doGenerate': { - span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'ai.run.doGenerate'); - break; - } - case 'ai.streamObject': { - span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'ai.pipeline.streamObject'); - isPipelineSpan = true; - break; - } - case 'ai.streamObject.doStream': { - span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'ai.run.doStream'); - break; - } - case 'ai.embed': { - span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'ai.pipeline.embed'); - isPipelineSpan = true; - break; - } - case 'ai.embed.doEmbed': { - span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'ai.embeddings'); - break; - } - case 'ai.embedMany': { - span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'ai.pipeline.embedMany'); - isPipelineSpan = true; - break; - } - case 'ai.embedMany.doEmbed': { - span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'ai.embeddings'); - break; - } - case 'ai.toolCall': - case 'ai.stream.firstChunk': - case 'ai.stream.finish': - span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'ai.run'); - break; - } - addOriginToSpan(span, 'auto.vercelai.otel'); const nameWthoutAi = name.replace('ai.', ''); @@ -119,9 +72,9 @@ const _vercelAIIntegration = ((options: VercelAiOptions = {}) => { span.updateName(nameWthoutAi); // If a Telemetry name is set and it is a pipeline span, use that as the operation name - const functionId = attributes['ai.telemetry.functionId']; - if (functionId && typeof functionId === 'string' && isPipelineSpan) { - span.updateName(functionId); + const functionId = attributes[AI_TELEMETRY_FUNCTION_ID_ATTRIBUTE]; + if (functionId && typeof functionId === 'string' && name.split('.').length - 1 === 1) { + span.updateName(`${nameWthoutAi} ${functionId}`); span.setAttribute('ai.pipeline.name', functionId); } @@ -132,6 +85,78 @@ const _vercelAIIntegration = ((options: VercelAiOptions = {}) => { span.setAttribute(GEN_AI_RESPONSE_MODEL_ATTRIBUTE, attributes[AI_MODEL_ID_ATTRIBUTE]); } span.setAttribute('ai.streaming', name.includes('stream')); + + // Generate Spans + if (name === 'ai.generateText') { + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'ai.pipeline.generate_text'); + return; + } + + if (name === 'ai.generateText.doGenerate') { + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'gen_ai.generate_text'); + span.updateName(`generate_text ${attributes[AI_MODEL_ID_ATTRIBUTE]}`); + return; + } + + if (name === 'ai.streamText') { + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'ai.pipeline.stream_text'); + return; + } + + if (name === 'ai.streamText.doStream') { + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'gen_ai.stream_text'); + span.updateName(`stream_text ${attributes[AI_MODEL_ID_ATTRIBUTE]}`); + return; + } + + if (name === 'ai.generateObject') { + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'ai.pipeline.generate_object'); + return; + } + + if (name === 'ai.generateObject.doGenerate') { + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'gen_ai.generate_object'); + span.updateName(`generate_object ${attributes[AI_MODEL_ID_ATTRIBUTE]}`); + return; + } + + if (name === 'ai.streamObject') { + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'ai.pipeline.stream_object'); + return; + } + + if (name === 'ai.streamObject.doStream') { + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'gen_ai.stream_object'); + span.updateName(`stream_object ${attributes[AI_MODEL_ID_ATTRIBUTE]}`); + return; + } + + if (name === 'ai.embed') { + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'ai.pipeline.embed'); + return; + } + + if (name === 'ai.embed.doEmbed') { + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'gen_ai.embed'); + span.updateName(`embed ${attributes[AI_MODEL_ID_ATTRIBUTE]}`); + return; + } + + if (name === 'ai.embedMany') { + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'ai.pipeline.embed_many'); + return; + } + + if (name === 'ai.embedMany.doEmbed') { + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'gen_ai.embed_many'); + span.updateName(`embed_many ${attributes[AI_MODEL_ID_ATTRIBUTE]}`); + return; + } + + if (name.startsWith('ai.stream')) { + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'ai.run'); + return; + } }); client.addEventProcessor(event => { @@ -145,12 +170,10 @@ const _vercelAIIntegration = ((options: VercelAiOptions = {}) => { if (attributes[AI_USAGE_COMPLETION_TOKENS_ATTRIBUTE] != undefined) { attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE] = attributes[AI_USAGE_COMPLETION_TOKENS_ATTRIBUTE]; - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete attributes[AI_USAGE_COMPLETION_TOKENS_ATTRIBUTE]; } if (attributes[AI_USAGE_PROMPT_TOKENS_ATTRIBUTE] != undefined) { attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE] = attributes[AI_USAGE_PROMPT_TOKENS_ATTRIBUTE]; - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete attributes[AI_USAGE_PROMPT_TOKENS_ATTRIBUTE]; } if ( From 48f525380379a6e73ce2fda2bfe2ec60921c535f Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Tue, 10 Jun 2025 12:45:14 +0200 Subject: [PATCH 5/6] test(sveltekit): Pin `@sveltejs/kit@2.21.2` to unblock CI (#16529) While we're investigating https://github.com/sveltejs/kit/issues/13869 and https://github.com/getsentry/sentry-javascript/issues/16507, let's pin to the last working SvelteKit version for now so the repo CI is unblocked. --- .../test-applications/sveltekit-2-svelte-5/package.json | 2 +- .../e2e-tests/test-applications/sveltekit-2/package.json | 2 +- .../test-applications/sveltekit-cloudflare-pages/package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/package.json b/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/package.json index 4fa8983ae153..cbf60795d4a0 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/package.json +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/package.json @@ -23,7 +23,7 @@ "@sentry-internal/test-utils": "link:../../../test-utils", "@sentry/core": "latest || *", "@sveltejs/adapter-auto": "^3.0.0", - "@sveltejs/kit": "^2.0.0", + "@sveltejs/kit": "2.21.2", "@sveltejs/vite-plugin-svelte": "^3.0.0", "svelte": "^5.0.0-next.115", "svelte-check": "^3.6.0", diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/package.json b/dev-packages/e2e-tests/test-applications/sveltekit-2/package.json index 91bc78290199..54599c04d9d1 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2/package.json +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/package.json @@ -23,7 +23,7 @@ "@sentry/core": "latest || *", "@sveltejs/adapter-auto": "^3.0.0", "@sveltejs/adapter-node": "^2.0.0", - "@sveltejs/kit": "^2.16.0", + "@sveltejs/kit": "2.21.2", "@sveltejs/vite-plugin-svelte": "^3.0.0", "svelte": "^4.2.8", "svelte-check": "^3.6.0", diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/package.json b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/package.json index 51fe00136f06..87d3c20c5c7a 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/package.json +++ b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/package.json @@ -20,7 +20,7 @@ "devDependencies": { "@playwright/test": "^1.45.3", "@sveltejs/adapter-cloudflare": "^5.0.3", - "@sveltejs/kit": "^2.17.2", + "@sveltejs/kit": "2.21.2", "@sveltejs/vite-plugin-svelte": "^5.0.3", "svelte": "^5.20.2", "svelte-check": "^4.1.4", From 3825f7d574ab7c1ba8e6f04f7520d974518b3921 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Tue, 10 Jun 2025 14:28:44 +0200 Subject: [PATCH 6/6] meta(changelog): Update changelog for 9.28.0 --- CHANGELOG.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c56e4c00ba52..fe5144bbb94b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,22 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +## 9.28.0 + +### Important Changes + +- **feat(nestjs): Stop creating spans for `TracingInterceptor` ([#16501](https://github.com/getsentry/sentry-javascript/pull/16501))** + +With this change we stop creating spans for `TracingInterceptor` as this interceptor only serves as an internal helper and adds noise for the user. + +- **feat(node): Update vercel ai spans as per new conventions ([#16497](https://github.com/getsentry/sentry-javascript/pull/16497))** + +This feature ships updates to the span names and ops to better match OpenTelemetry. This should make them more easily accessible to the new agents module view we are building. + +### Other Changes + +- fix(sveltekit): Export `vercelAIIntegration` from `@sentry/node` ([#16496](https://github.com/getsentry/sentry-javascript/pull/16496)) + Work in this release was contributed by @agrattan0820. Thank you for your contribution! ## 9.27.0