From b4607526588378803a7466ef672bea48d5625fe3 Mon Sep 17 00:00:00 2001 From: RulaKhaled Date: Tue, 6 Oct 2026 01:17:52 +0300 Subject: [PATCH] fix(server-utils): Instrument the Vercel AI experimental_decide channel event ai 7.0.128 renamed experimental_evaluate to experimental_decide, and the tracing channel now publishes type "experimental_decide". The subscriber only matched the old name, so it opened no gen_ai.evaluate span. Co-Authored-By: Claude Opus 5.5 --- .../vercel-ai/vercel-ai-dc-subscriber.ts | 7 ++- .../integrations/vercel-ai/evaluate.test.ts | 57 +++++++++++++++++++ 2 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 packages/server-utils/test/integrations/vercel-ai/evaluate.test.ts diff --git a/packages/server-utils/src/integrations/vercel-ai/vercel-ai-dc-subscriber.ts b/packages/server-utils/src/integrations/vercel-ai/vercel-ai-dc-subscriber.ts index 2ea07e96251c..6923694b3a0c 100644 --- a/packages/server-utils/src/integrations/vercel-ai/vercel-ai-dc-subscriber.ts +++ b/packages/server-utils/src/integrations/vercel-ai/vercel-ai-dc-subscriber.ts @@ -217,7 +217,8 @@ export type ChannelEventType = | 'embed' | 'embedMany' | 'rerank' - | 'experimental_evaluate'; + | 'experimental_evaluate' + | 'experimental_decide'; /** * The context object the AI SDK passes through one tracing-channel call. It is the same object @@ -451,7 +452,9 @@ export function createSpanFromMessage( } case 'rerank': return startGenAiSpan('rerank', modelId, baseAttributes); + // `ai` 7.0.128 renamed `experimental_evaluate` to `experimental_decide`; older 7.x still publishes the old name. case 'experimental_evaluate': + case 'experimental_decide': return startGenAiSpan('evaluate', modelId, { ...baseAttributes, ...(recordInputs @@ -654,7 +657,7 @@ function getOutputMessages( result: Record, finishReason: string | undefined, ): string | undefined { - if (type === 'experimental_evaluate') { + if (type === 'experimental_evaluate' || type === 'experimental_decide') { return stringify([{ type: 'evaluation', answers: withProviderConfidence(result) }]); } // `languageModelCall` exposes the response as a `content` parts array; top-level results expose diff --git a/packages/server-utils/test/integrations/vercel-ai/evaluate.test.ts b/packages/server-utils/test/integrations/vercel-ai/evaluate.test.ts new file mode 100644 index 000000000000..0101b19cf69b --- /dev/null +++ b/packages/server-utils/test/integrations/vercel-ai/evaluate.test.ts @@ -0,0 +1,57 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { getMainCarrier, setCurrentClient, spanToStaticSpanJSON } from '@sentry/core'; +import type { Span } from '@sentry/core'; +import { createSpanFromMessage, enrichSpanOnEnd } from '../../../src/integrations/vercel-ai/vercel-ai-dc-subscriber'; +import { getDefaultTestClientOptions, TestClient } from '../../mocks/client'; + +type Message = Parameters[0]; + +describe('Vercel AI evaluate spans', () => { + let endedSpans: Span[]; + + beforeEach(() => { + getMainCarrier().__SENTRY__ = undefined; + const client = new TestClient( + getDefaultTestClientOptions({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + tracesSampleRate: 1, + }), + ); + setCurrentClient(client); + client.init(); + endedSpans = []; + client.on('spanEnd', span => endedSpans.push(span)); + }); + + afterEach(() => { + getMainCarrier().__SENTRY__ = undefined; + }); + + // `ai` 7.0.128 renamed the channel event from `experimental_evaluate` to `experimental_decide`. + it.each(['experimental_evaluate', 'experimental_decide'] as const)('creates an evaluate span for `%s`', type => { + const message = { + type, + event: { + provider: 'gateway', + modelId: 'typesafe-ai/jev', + state: 'I want a refund', + questions: { wantsRefund: { type: 'boolean', instructions: 'Is a refund requested?' } }, + recordInputs: true, + recordOutputs: true, + }, + result: { answers: { wantsRefund: { type: 'boolean', probability: 0.99 } } }, + } as unknown as Message; + + const span = createSpanFromMessage(message, {} as Parameters[1])!; + expect(span).toBeDefined(); + enrichSpanOnEnd(span, message, {} as Parameters[2]); + span.end(); + + const json = spanToStaticSpanJSON(endedSpans[0]!); + expect(json.description).toBe('evaluate typesafe-ai/jev'); + expect(json.op).toBe('gen_ai.evaluate'); + expect(JSON.parse(json.data?.['gen_ai.output.messages'] as string)).toEqual([ + { type: 'evaluation', answers: { wantsRefund: { type: 'boolean', probability: 0.99 } } }, + ]); + }); +});