diff --git a/packages/cloudflare/src/index.ts b/packages/cloudflare/src/index.ts index 8efcc8439809..a5bb99d40818 100644 --- a/packages/cloudflare/src/index.ts +++ b/packages/cloudflare/src/index.ts @@ -107,6 +107,7 @@ export { CloudflareClient } from './client'; export { getDefaultIntegrations } from './sdk'; export { fetchIntegration } from './integrations/fetch'; +export { vercelAIIntegration } from './integrations/tracing/vercelai'; export { instrumentD1WithSentry } from './d1'; diff --git a/packages/cloudflare/src/integrations/tracing/vercelai.ts b/packages/cloudflare/src/integrations/tracing/vercelai.ts new file mode 100644 index 000000000000..c513568997ab --- /dev/null +++ b/packages/cloudflare/src/integrations/tracing/vercelai.ts @@ -0,0 +1,51 @@ +/** + * This is a copy of the Vercel AI integration from the node SDK. + * + * The only difference is that it does not use `@opentelemetry/instrumentation` + * because Cloudflare Workers do not support it. + * + * Therefore, we cannot automatically patch setting `experimental_telemetry: { isEnabled: true }` + * and users have to manually set this to get spans. + */ + +import type { IntegrationFn } from '@sentry/core'; +import { addVercelAiProcessors, defineIntegration } from '@sentry/core'; + +const INTEGRATION_NAME = 'VercelAI'; + +const _vercelAIIntegration = (() => { + return { + name: INTEGRATION_NAME, + setup(client) { + addVercelAiProcessors(client); + }, + }; +}) satisfies IntegrationFn; + +/** + * Adds Sentry tracing instrumentation for the [ai](https://www.npmjs.com/package/ai) library. + * This integration is not enabled by default, you need to manually add it. + * + * For more information, see the [`ai` documentation](https://sdk.vercel.ai/docs/ai-sdk-core/telemetry). + * + * You need to enable collecting spans for a specific call by setting + * `experimental_telemetry.isEnabled` to `true` in the first argument of the function call. + * + * ```javascript + * const result = await generateText({ + * model: openai('gpt-4-turbo'), + * experimental_telemetry: { isEnabled: true }, + * }); + * ``` + * + * If you want to collect inputs and outputs for a specific call, you must specifically opt-in to each + * function call by setting `experimental_telemetry.recordInputs` and `experimental_telemetry.recordOutputs` + * to `true`. + * + * ```javascript + * const result = await generateText({ + * model: openai('gpt-4-turbo'), + * experimental_telemetry: { isEnabled: true, recordInputs: true, recordOutputs: true }, + * }); + */ +export const vercelAIIntegration = defineIntegration(_vercelAIIntegration); diff --git a/packages/cloudflare/src/utils/addOriginToSpan.ts b/packages/cloudflare/src/utils/addOriginToSpan.ts new file mode 100644 index 000000000000..2a23710fa7cf --- /dev/null +++ b/packages/cloudflare/src/utils/addOriginToSpan.ts @@ -0,0 +1,8 @@ +import type { Span } from '@opentelemetry/api'; +import type { SpanOrigin } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; + +/** Adds an origin to an OTEL Span. */ +export function addOriginToSpan(span: Span, origin: SpanOrigin): void { + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, origin); +} diff --git a/packages/cloudflare/src/utils/commonjs.ts b/packages/cloudflare/src/utils/commonjs.ts new file mode 100644 index 000000000000..23a9b97f9fc1 --- /dev/null +++ b/packages/cloudflare/src/utils/commonjs.ts @@ -0,0 +1,8 @@ +/** Detect CommonJS. */ +export function isCjs(): boolean { + try { + return typeof module !== 'undefined' && typeof module.exports !== 'undefined'; + } catch { + return false; + } +} diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts index 3dd8bd66d023..7997bd3345a0 100644 --- a/packages/core/src/client.ts +++ b/packages/core/src/client.ts @@ -498,7 +498,8 @@ export abstract class Client { ): void; /** - * Register a callback for whenever a span is ended. + * Register a callback for after a span is ended. + * NOTE: The span cannot be mutated anymore in this callback. * Receives the span as argument. * @returns {() => void} A function that, when executed, removes the registered callback. */ diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index b4f09d89f381..7551478c9c88 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -123,6 +123,7 @@ export { captureFeedback } from './feedback'; export type { ReportDialogOptions } from './report-dialog'; export { _INTERNAL_captureLog, _INTERNAL_flushLogsBuffer, _INTERNAL_captureSerializedLog } from './logs/exports'; export { consoleLoggingIntegration } from './logs/console-integration'; +export { addVercelAiProcessors } from './utils/vercel-ai'; export type { FeatureFlag } from './utils/featureFlags'; export { diff --git a/packages/node/src/integrations/tracing/vercelai/ai_sdk_attributes.ts b/packages/core/src/utils/vercel-ai-attributes.ts similarity index 100% rename from packages/node/src/integrations/tracing/vercelai/ai_sdk_attributes.ts rename to packages/core/src/utils/vercel-ai-attributes.ts diff --git a/packages/core/src/utils/vercel-ai.ts b/packages/core/src/utils/vercel-ai.ts new file mode 100644 index 000000000000..2a653addd805 --- /dev/null +++ b/packages/core/src/utils/vercel-ai.ts @@ -0,0 +1,221 @@ +import type { Client } from '../client'; +import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../semanticAttributes'; +import type { Event } from '../types-hoist/event'; +import type { Span, SpanAttributes, SpanJSON, SpanOrigin } from '../types-hoist/span'; +import { spanToJSON } from './spanUtils'; +import { + AI_MODEL_ID_ATTRIBUTE, + AI_MODEL_PROVIDER_ATTRIBUTE, + AI_PROMPT_ATTRIBUTE, + AI_PROMPT_MESSAGES_ATTRIBUTE, + AI_PROMPT_TOOLS_ATTRIBUTE, + AI_RESPONSE_TEXT_ATTRIBUTE, + AI_RESPONSE_TOOL_CALLS_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 './vercel-ai-attributes'; + +function addOriginToSpan(span: Span, origin: SpanOrigin): void { + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, origin); +} + +/** + * Post-process spans emitted by the Vercel AI SDK. + * This is supposed to be used in `client.on('spanStart', ...) + */ +function onVercelAiSpanStart(span: Span): void { + const { data: attributes, description: name } = spanToJSON(span); + + if (!name) { + 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') { + processToolCallSpan(span, attributes); + 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]; + if (typeof aiModelId !== 'string' || typeof aiModelProvider !== 'string' || !aiModelId || !aiModelProvider) { + return; + } + + processGenerateSpan(span, name, attributes); +} + +const vercelAiEventProcessor = Object.assign( + (event: Event): Event => { + if (event.type === 'transaction' && event.spans) { + for (const span of event.spans) { + // this mutates spans in-place + processEndedVercelAiSpan(span); + } + } + return event; + }, + { id: 'VercelAiEventProcessor' }, +); + +/** + * Post-process spans emitted by the Vercel AI SDK. + */ +function processEndedVercelAiSpan(span: SpanJSON): void { + const { data: attributes, origin } = span; + + if (origin !== 'auto.vercelai.otel') { + return; + } + + renameAttributeKey(attributes, AI_USAGE_COMPLETION_TOKENS_ATTRIBUTE, GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE); + renameAttributeKey(attributes, AI_USAGE_PROMPT_TOKENS_ATTRIBUTE, GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE); + + if ( + typeof attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE] === 'number' && + typeof attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE] === 'number' + ) { + attributes['gen_ai.usage.total_tokens'] = + attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE] + attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]; + } + + // Rename AI SDK attributes to standardized gen_ai attributes + renameAttributeKey(attributes, AI_PROMPT_MESSAGES_ATTRIBUTE, 'gen_ai.request.messages'); + renameAttributeKey(attributes, AI_RESPONSE_TEXT_ATTRIBUTE, 'gen_ai.response.text'); + renameAttributeKey(attributes, AI_RESPONSE_TOOL_CALLS_ATTRIBUTE, 'gen_ai.response.tool_calls'); + renameAttributeKey(attributes, AI_PROMPT_TOOLS_ATTRIBUTE, 'gen_ai.request.available_tools'); +} + +/** + * Renames an attribute key in the provided attributes object if the old key exists. + * This function safely handles null and undefined values. + */ +function renameAttributeKey(attributes: Record, oldKey: string, newKey: string): void { + if (attributes[oldKey] != null) { + attributes[newKey] = attributes[oldKey]; + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete attributes[oldKey]; + } +} + +function processToolCallSpan(span: Span, attributes: SpanAttributes): void { + 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]}`); +} + +function processGenerateSpan(span: Span, name: string, attributes: SpanAttributes): void { + addOriginToSpan(span, 'auto.vercelai.otel'); + + const nameWthoutAi = name.replace('ai.', ''); + span.setAttribute('ai.pipeline.name', nameWthoutAi); + 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_FUNCTION_ID_ATTRIBUTE]; + if (functionId && typeof functionId === 'string' && name.split('.').length - 1 === 1) { + span.updateName(`${nameWthoutAi} ${functionId}`); + span.setAttribute('ai.pipeline.name', functionId); + } + + if (attributes[AI_PROMPT_ATTRIBUTE]) { + span.setAttribute('gen_ai.prompt', attributes[AI_PROMPT_ATTRIBUTE]); + } + if (attributes[AI_MODEL_ID_ATTRIBUTE] && !attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]) { + 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, 'gen_ai.invoke_agent'); + 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, 'gen_ai.invoke_agent'); + 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, 'gen_ai.invoke_agent'); + 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, 'gen_ai.invoke_agent'); + 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, 'gen_ai.invoke_agent'); + 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, 'gen_ai.invoke_agent'); + 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; + } +} + +/** + * Add event processors to the given client to process Vercel AI spans. + */ +export function addVercelAiProcessors(client: Client): void { + client.on('spanStart', onVercelAiSpanStart); + // Note: We cannot do this on `spanEnd`, because the span cannot be mutated anymore at this point + client.addEventProcessor(vercelAiEventProcessor); +} diff --git a/packages/nextjs/src/index.types.ts b/packages/nextjs/src/index.types.ts index c630d545061c..04b73ea4c83e 100644 --- a/packages/nextjs/src/index.types.ts +++ b/packages/nextjs/src/index.types.ts @@ -22,6 +22,9 @@ export declare function init( export declare const linkedErrorsIntegration: typeof clientSdk.linkedErrorsIntegration; export declare const contextLinesIntegration: typeof clientSdk.contextLinesIntegration; +// Different implementation in server and worker +export declare const vercelAIIntegration: typeof serverSdk.vercelAIIntegration; + export declare const getDefaultIntegrations: (options: Options) => Integration[]; export declare const defaultStackParser: StackParser; diff --git a/packages/node/src/integrations/tracing/vercelai/index.ts b/packages/node/src/integrations/tracing/vercelai/index.ts index 8ba6cb5af905..9ee5fb29f11d 100644 --- a/packages/node/src/integrations/tracing/vercelai/index.ts +++ b/packages/node/src/integrations/tracing/vercelai/index.ts @@ -1,27 +1,7 @@ -/* eslint-disable @typescript-eslint/no-dynamic-delete */ -/* eslint-disable complexity */ import type { Client, IntegrationFn } from '@sentry/core'; -import { defineIntegration, SEMANTIC_ATTRIBUTE_SENTRY_OP, spanToJSON } from '@sentry/core'; +import { addVercelAiProcessors, defineIntegration } from '@sentry/core'; import { generateInstrumentOnce } from '../../../otel/instrument'; -import { addOriginToSpan } from '../../../utils/addOriginToSpan'; import type { modulesIntegration } from '../../modules'; -import { - AI_MODEL_ID_ATTRIBUTE, - AI_MODEL_PROVIDER_ATTRIBUTE, - AI_PROMPT_ATTRIBUTE, - AI_PROMPT_MESSAGES_ATTRIBUTE, - AI_PROMPT_TOOLS_ATTRIBUTE, - AI_RESPONSE_TEXT_ATTRIBUTE, - AI_RESPONSE_TOOL_CALLS_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 './ai_sdk_attributes'; import { INTEGRATION_NAME } from './constants'; import { SentryVercelAiInstrumentation } from './instrumentation'; import type { VercelAiOptions } from './types'; @@ -47,175 +27,14 @@ const _vercelAIIntegration = ((options: VercelAiOptions = {}) => { instrumentation = instrumentVercelAi(); }, afterAllSetup(client) { - function registerProcessors(): void { - client.on('spanStart', span => { - const { data: attributes, description: name } = spanToJSON(span); - - if (!name) { - 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]; - if (typeof aiModelId !== 'string' || typeof aiModelProvider !== 'string' || !aiModelId || !aiModelProvider) { - return; - } - - addOriginToSpan(span, 'auto.vercelai.otel'); - - const nameWthoutAi = name.replace('ai.', ''); - span.setAttribute('ai.pipeline.name', nameWthoutAi); - 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_FUNCTION_ID_ATTRIBUTE]; - if (functionId && typeof functionId === 'string' && name.split('.').length - 1 === 1) { - span.updateName(`${nameWthoutAi} ${functionId}`); - span.setAttribute('ai.pipeline.name', functionId); - } - - if (attributes[AI_PROMPT_ATTRIBUTE]) { - span.setAttribute('gen_ai.prompt', attributes[AI_PROMPT_ATTRIBUTE]); - } - if (attributes[AI_MODEL_ID_ATTRIBUTE] && !attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]) { - 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, 'gen_ai.invoke_agent'); - 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, 'gen_ai.invoke_agent'); - 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, 'gen_ai.invoke_agent'); - 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, 'gen_ai.invoke_agent'); - 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, 'gen_ai.invoke_agent'); - 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, 'gen_ai.invoke_agent'); - 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 => { - if (event.type === 'transaction' && event.spans?.length) { - for (const span of event.spans) { - const { data: attributes, description: name } = span; - - if (!name || span.origin !== 'auto.vercelai.otel') { - continue; - } - - renameAttributeKey( - attributes, - AI_USAGE_COMPLETION_TOKENS_ATTRIBUTE, - GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE, - ); - renameAttributeKey(attributes, AI_USAGE_PROMPT_TOKENS_ATTRIBUTE, GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE); - if ( - typeof attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE] === 'number' && - typeof attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE] === 'number' - ) { - attributes['gen_ai.usage.total_tokens'] = - attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE] + attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]; - } - - // Rename AI SDK attributes to standardized gen_ai attributes - renameAttributeKey(attributes, AI_PROMPT_MESSAGES_ATTRIBUTE, 'gen_ai.request.messages'); - renameAttributeKey(attributes, AI_RESPONSE_TEXT_ATTRIBUTE, 'gen_ai.response.text'); - renameAttributeKey(attributes, AI_RESPONSE_TOOL_CALLS_ATTRIBUTE, 'gen_ai.response.tool_calls'); - renameAttributeKey(attributes, AI_PROMPT_TOOLS_ATTRIBUTE, 'gen_ai.request.available_tools'); - } - } - - return event; - }); - } - // Auto-detect if we should force the integration when running with 'ai' package available // Note that this can only be detected if the 'Modules' integration is available, and running in CJS mode const shouldForce = options.force ?? shouldForceIntegration(client); if (shouldForce) { - registerProcessors(); + addVercelAiProcessors(client); } else { - instrumentation?.callWhenPatched(registerProcessors); + instrumentation?.callWhenPatched(() => addVercelAiProcessors(client)); } }, }; @@ -223,6 +42,7 @@ const _vercelAIIntegration = ((options: VercelAiOptions = {}) => { /** * Adds Sentry tracing instrumentation for the [ai](https://www.npmjs.com/package/ai) library. + * This integration is not enabled by default, you need to manually add it. * * For more information, see the [`ai` documentation](https://sdk.vercel.ai/docs/ai-sdk-core/telemetry). * @@ -235,17 +55,14 @@ const _vercelAIIntegration = ((options: VercelAiOptions = {}) => { * }); * ``` * - * The integration automatically detects when to force registration in CommonJS environments - * when the 'ai' package is available. You can still manually set the `force` option if needed. - * - * By default this integration adds tracing support to all `ai` function calls. If you need to disable - * collecting spans for a specific call, you can do so by setting `experimental_telemetry.isEnabled` to - * `false` in the first argument of the function call. + * This integration adds tracing support to all `ai` function calls. + * You need to opt-in to collecting spans for a specific call, + * you can do so by setting `experimental_telemetry.isEnabled` to `true` in the first argument of the function call. * * ```javascript * const result = await generateText({ * model: openai('gpt-4-turbo'), - * experimental_telemetry: { isEnabled: false }, + * experimental_telemetry: { isEnabled: true }, * }); * ``` * @@ -260,14 +77,3 @@ const _vercelAIIntegration = ((options: VercelAiOptions = {}) => { * }); */ export const vercelAIIntegration = defineIntegration(_vercelAIIntegration); - -/** - * Renames an attribute key in the provided attributes object if the old key exists. - * This function safely handles null and undefined values. - */ -function renameAttributeKey(attributes: Record, oldKey: string, newKey: string): void { - if (attributes[oldKey] != null) { - attributes[newKey] = attributes[oldKey]; - delete attributes[oldKey]; - } -} diff --git a/packages/sveltekit/src/index.types.ts b/packages/sveltekit/src/index.types.ts index 03c63041e726..df788559d2f5 100644 --- a/packages/sveltekit/src/index.types.ts +++ b/packages/sveltekit/src/index.types.ts @@ -46,6 +46,9 @@ export declare function wrapLoadWithSentry any>(orig export declare const linkedErrorsIntegration: typeof clientSdk.linkedErrorsIntegration; export declare const contextLinesIntegration: typeof clientSdk.contextLinesIntegration; +// Different implementation in server and worker +export declare const vercelAIIntegration: typeof serverSdk.vercelAIIntegration; + export declare const getDefaultIntegrations: (options: Options) => Integration[]; export declare const defaultStackParser: StackParser; diff --git a/packages/sveltekit/src/worker/index.ts b/packages/sveltekit/src/worker/index.ts index e49a493fb0b8..8e4645741456 100644 --- a/packages/sveltekit/src/worker/index.ts +++ b/packages/sveltekit/src/worker/index.ts @@ -84,6 +84,7 @@ export { instrumentSupabaseClient, zodErrorsIntegration, featureFlagsIntegration, + vercelAIIntegration, type FeatureFlagsIntegration, } from '@sentry/cloudflare'; diff --git a/packages/vercel-edge/src/index.ts b/packages/vercel-edge/src/index.ts index 303d40144ec3..5325d1e62391 100644 --- a/packages/vercel-edge/src/index.ts +++ b/packages/vercel-edge/src/index.ts @@ -98,5 +98,6 @@ export { VercelEdgeClient } from './client'; export { getDefaultIntegrations, init } from './sdk'; export { winterCGFetchIntegration } from './integrations/wintercg-fetch'; +export { vercelAIIntegration } from './integrations/tracing/vercelai'; export * as logger from './logs/exports'; diff --git a/packages/vercel-edge/src/integrations/tracing/vercelai.ts b/packages/vercel-edge/src/integrations/tracing/vercelai.ts new file mode 100644 index 000000000000..c513568997ab --- /dev/null +++ b/packages/vercel-edge/src/integrations/tracing/vercelai.ts @@ -0,0 +1,51 @@ +/** + * This is a copy of the Vercel AI integration from the node SDK. + * + * The only difference is that it does not use `@opentelemetry/instrumentation` + * because Cloudflare Workers do not support it. + * + * Therefore, we cannot automatically patch setting `experimental_telemetry: { isEnabled: true }` + * and users have to manually set this to get spans. + */ + +import type { IntegrationFn } from '@sentry/core'; +import { addVercelAiProcessors, defineIntegration } from '@sentry/core'; + +const INTEGRATION_NAME = 'VercelAI'; + +const _vercelAIIntegration = (() => { + return { + name: INTEGRATION_NAME, + setup(client) { + addVercelAiProcessors(client); + }, + }; +}) satisfies IntegrationFn; + +/** + * Adds Sentry tracing instrumentation for the [ai](https://www.npmjs.com/package/ai) library. + * This integration is not enabled by default, you need to manually add it. + * + * For more information, see the [`ai` documentation](https://sdk.vercel.ai/docs/ai-sdk-core/telemetry). + * + * You need to enable collecting spans for a specific call by setting + * `experimental_telemetry.isEnabled` to `true` in the first argument of the function call. + * + * ```javascript + * const result = await generateText({ + * model: openai('gpt-4-turbo'), + * experimental_telemetry: { isEnabled: true }, + * }); + * ``` + * + * If you want to collect inputs and outputs for a specific call, you must specifically opt-in to each + * function call by setting `experimental_telemetry.recordInputs` and `experimental_telemetry.recordOutputs` + * to `true`. + * + * ```javascript + * const result = await generateText({ + * model: openai('gpt-4-turbo'), + * experimental_telemetry: { isEnabled: true, recordInputs: true, recordOutputs: true }, + * }); + */ +export const vercelAIIntegration = defineIntegration(_vercelAIIntegration);