diff --git a/packages/core/src/tracing/vercel-ai/index.ts b/packages/core/src/tracing/vercel-ai/index.ts index 1d8a27e8e3aa..5458ace456c5 100644 --- a/packages/core/src/tracing/vercel-ai/index.ts +++ b/packages/core/src/tracing/vercel-ai/index.ts @@ -266,6 +266,12 @@ function processEndedVercelAiSpan(span: SpanJSON): void { return; } + // The Vercel AI SDK sets span status to raw error message strings. + // Any such value should be normalized to a SpanStatusType value. We pick internal_error as it is the most generic. + if (span.status && span.status !== 'ok') { + span.status = 'internal_error'; + } + 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); renameAttributeKey(attributes, AI_USAGE_CACHED_INPUT_TOKENS_ATTRIBUTE, GEN_AI_USAGE_INPUT_TOKENS_CACHED_ATTRIBUTE); diff --git a/packages/core/test/lib/tracing/vercel-ai-span-status.test.ts b/packages/core/test/lib/tracing/vercel-ai-span-status.test.ts new file mode 100644 index 000000000000..dbba343eca42 --- /dev/null +++ b/packages/core/test/lib/tracing/vercel-ai-span-status.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, it } from 'vitest'; +import { addVercelAiProcessors } from '../../../src/tracing/vercel-ai'; +import type { SpanJSON } from '../../../src/types-hoist/span'; +import { getDefaultTestClientOptions, TestClient } from '../../mocks/client'; + +describe('vercel-ai span status normalization', () => { + function processSpan(status: string): string | undefined { + const options = getDefaultTestClientOptions({ tracesSampleRate: 1.0 }); + const client = new TestClient(options); + client.init(); + addVercelAiProcessors(client); + + const span: SpanJSON = { + description: 'test', + span_id: 'test-span-id', + trace_id: 'test-trace-id', + start_timestamp: 1000, + timestamp: 2000, + origin: 'auto.vercelai.otel', + status, + data: {}, + }; + + const eventProcessor = client['_eventProcessors'].find(p => p.id === 'VercelAiEventProcessor'); + const processedEvent = eventProcessor!({ type: 'transaction' as const, spans: [span] }, {}); + return (processedEvent as { spans?: SpanJSON[] })?.spans?.[0]?.status; + } + + it('normalizes raw error message status to internal_error', () => { + expect(processSpan("FileNotFoundError: The file '/nonexistent/file.txt' does not exist")).toBe('internal_error'); + }); + + it('preserves ok status', () => { + expect(processSpan('ok')).toBe('ok'); + }); +});