diff --git a/packages/core/src/tracing/vercel-ai/index.ts b/packages/core/src/tracing/vercel-ai/index.ts index 7d72b3de3ae7..f9abcb096dc8 100644 --- a/packages/core/src/tracing/vercel-ai/index.ts +++ b/packages/core/src/tracing/vercel-ai/index.ts @@ -513,10 +513,20 @@ function processGenerateSpan(span: Span, name: string, attributes: SpanAttribute } } +const CLIENTS_WITH_VERCEL_AI_PROCESSORS = new WeakSet(); + /** * Add event processors to the given client to process Vercel AI spans. + * + * Idempotent: both the integration and the Next.js SDK register these, and duplicate hooks would + * process every span twice. */ export function addVercelAiProcessors(client: Client): void { + if (CLIENTS_WITH_VERCEL_AI_PROCESSORS.has(client)) { + return; + } + CLIENTS_WITH_VERCEL_AI_PROCESSORS.add(client); + client.on('spanStart', onVercelAiSpanStart); // Note: We cannot do this on `spanEnd`, because the span cannot be mutated anymore at this point client.addEventProcessor(Object.assign(vercelAiEventProcessor, { id: 'VercelAiEventProcessor' })); diff --git a/packages/nextjs/src/server/index.ts b/packages/nextjs/src/server/index.ts index 203b0f348a97..d4038fa0e6e1 100644 --- a/packages/nextjs/src/server/index.ts +++ b/packages/nextjs/src/server/index.ts @@ -4,6 +4,7 @@ import { HTTP_TARGET, URL_QUERY } from '@sentry/conventions/attributes'; import type { EventProcessor } from '@sentry/core'; import { + addVercelAiProcessors, applySdkMetadata, debug, getClient, @@ -210,6 +211,14 @@ export function init(options: NodeOptions): NodeClient | undefined { const client = nodeInit(opts); + // Next.js bundles `ai`, so the integration can neither patch the module nor detect it via `Modules` + // (which only sees the app's own `package.json`, missing workspace and transitive deps). Register + // the processors here instead — after init, so an explicitly constructed `vercelAIIntegration()` + // can't override the default back to the broken behavior. + if (client?.getIntegrationByName('VercelAI')) { + addVercelAiProcessors(client); + } + client?.on('beforeSampling', ({ spanAttributes }, samplingDecision) => { // There are situations where the Next.js Node.js server forwards requests for the Edge Runtime server (e.g. in // middleware) and this causes spans for Sentry ingest requests to be created. These are not exempt from our tracing