From 5ddec042397927434473a0e819d4ad8e5d83d734 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Wed, 18 Mar 2026 15:52:48 +0100 Subject: [PATCH 1/5] align --- .../suites/tracing/google-genai/test.ts | 4 ++-- .../src/tracing/anthropic-ai/streaming.ts | 5 ++-- .../core/src/tracing/anthropic-ai/utils.ts | 24 ++++++++++++++++++- .../src/tracing/google-genai/streaming.ts | 2 +- packages/core/src/tracing/langchain/index.ts | 6 ++--- 5 files changed, 32 insertions(+), 9 deletions(-) diff --git a/dev-packages/node-integration-tests/suites/tracing/google-genai/test.ts b/dev-packages/node-integration-tests/suites/tracing/google-genai/test.ts index 993984cc6b3d..1a66906e0b04 100644 --- a/dev-packages/node-integration-tests/suites/tracing/google-genai/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/google-genai/test.ts @@ -376,7 +376,7 @@ describe('Google GenAI integration', () => { description: 'generate_content blocked-model', op: 'gen_ai.generate_content', origin: 'auto.ai.google_genai', - status: 'internal_error', + status: 'permission_denied', }), // Fifth span - error handling for streaming expect.objectContaining({ @@ -475,7 +475,7 @@ describe('Google GenAI integration', () => { description: 'generate_content blocked-model', op: 'gen_ai.generate_content', origin: 'auto.ai.google_genai', - status: 'internal_error', + status: 'permission_denied', }), // Fifth span - error handling for streaming with PII expect.objectContaining({ diff --git a/packages/core/src/tracing/anthropic-ai/streaming.ts b/packages/core/src/tracing/anthropic-ai/streaming.ts index 86f5c25baa8a..b42f34324207 100644 --- a/packages/core/src/tracing/anthropic-ai/streaming.ts +++ b/packages/core/src/tracing/anthropic-ai/streaming.ts @@ -11,6 +11,7 @@ import { } from '../ai/gen-ai-attributes'; import { setTokenUsageAttributes } from '../ai/utils'; import type { AnthropicAiStreamingEvent } from './types'; +import { mapAnthropicErrorToStatus } from './utils'; /** * State object used to accumulate information from a stream of Anthropic AI events. @@ -59,7 +60,7 @@ function isErrorEvent(event: AnthropicAiStreamingEvent, span: Span): boolean { // If the event is an error, set the span status and capture the error // These error events are not rejected by the API by default, but are sent as metadata of the response if (event.type === 'error') { - span.setStatus({ code: SPAN_STATUS_ERROR, message: event.error?.type ?? 'internal_error' }); + span.setStatus({ code: SPAN_STATUS_ERROR, message: mapAnthropicErrorToStatus(event.error?.type) }); captureException(event.error, { mechanism: { handled: false, @@ -377,7 +378,7 @@ export function instrumentMessageStream }); if (span.isRecording()) { - span.setStatus({ code: SPAN_STATUS_ERROR, message: 'stream_error' }); + span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); span.end(); } }); diff --git a/packages/core/src/tracing/anthropic-ai/utils.ts b/packages/core/src/tracing/anthropic-ai/utils.ts index b9cf31b4aeea..222d3d30c987 100644 --- a/packages/core/src/tracing/anthropic-ai/utils.ts +++ b/packages/core/src/tracing/anthropic-ai/utils.ts @@ -41,13 +41,35 @@ export function setMessagesAttribute(span: Span, messages: unknown): void { }); } +const ANTHROPIC_ERROR_TYPE_TO_SPAN_STATUS: Record = { + invalid_request_error: 'invalid_argument', + authentication_error: 'unauthenticated', + permission_error: 'permission_denied', + not_found_error: 'not_found', + request_too_large: 'failed_precondition', + rate_limit_error: 'resource_exhausted', + api_error: 'internal_error', + overloaded_error: 'unavailable', +}; + +/** + * Map an Anthropic API error type to a SpanStatusType value. + * @see https://docs.anthropic.com/en/api/errors#error-shapes + */ +export function mapAnthropicErrorToStatus(errorType: string | undefined): string { + if (!errorType) { + return 'internal_error'; + } + return ANTHROPIC_ERROR_TYPE_TO_SPAN_STATUS[errorType] || 'internal_error'; +} + /** * Capture error information from the response * @see https://docs.anthropic.com/en/api/errors#error-shapes */ export function handleResponseError(span: Span, response: AnthropicAiResponse): void { if (response.error) { - span.setStatus({ code: SPAN_STATUS_ERROR, message: response.error.type || 'internal_error' }); + span.setStatus({ code: SPAN_STATUS_ERROR, message: mapAnthropicErrorToStatus(response.error.type) }); captureException(response.error, { mechanism: { diff --git a/packages/core/src/tracing/google-genai/streaming.ts b/packages/core/src/tracing/google-genai/streaming.ts index b9462e8c90dd..a2a7909292e4 100644 --- a/packages/core/src/tracing/google-genai/streaming.ts +++ b/packages/core/src/tracing/google-genai/streaming.ts @@ -46,7 +46,7 @@ function isErrorChunk(chunk: GoogleGenAIResponse, span: Span): boolean { const feedback = chunk?.promptFeedback; if (feedback?.blockReason) { const message = feedback.blockReasonMessage ?? feedback.blockReason; - span.setStatus({ code: SPAN_STATUS_ERROR, message: `Content blocked: ${message}` }); + span.setStatus({ code: SPAN_STATUS_ERROR, message: 'permission_denied' }); captureException(`Content blocked: ${message}`, { mechanism: { handled: false, type: 'auto.ai.google_genai' }, }); diff --git a/packages/core/src/tracing/langchain/index.ts b/packages/core/src/tracing/langchain/index.ts index 54b581af9f2d..0984131e4296 100644 --- a/packages/core/src/tracing/langchain/index.ts +++ b/packages/core/src/tracing/langchain/index.ts @@ -171,7 +171,7 @@ export function createLangChainCallbackHandler(options: LangChainOptions = {}): handleLLMError(error: Error, runId: string) { const span = spanMap.get(runId); if (span?.isRecording()) { - span.setStatus({ code: SPAN_STATUS_ERROR, message: 'llm_error' }); + span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); exitSpan(runId); } @@ -239,7 +239,7 @@ export function createLangChainCallbackHandler(options: LangChainOptions = {}): handleChainError(error: Error, runId: string) { const span = spanMap.get(runId); if (span?.isRecording()) { - span.setStatus({ code: SPAN_STATUS_ERROR, message: 'chain_error' }); + span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); exitSpan(runId); } @@ -298,7 +298,7 @@ export function createLangChainCallbackHandler(options: LangChainOptions = {}): handleToolError(error: Error, runId: string) { const span = spanMap.get(runId); if (span?.isRecording()) { - span.setStatus({ code: SPAN_STATUS_ERROR, message: 'tool_error' }); + span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); exitSpan(runId); } From 68eddb2bfba0651d27597c15331fbdfd1ec6c39b Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Wed, 18 Mar 2026 16:51:29 +0100 Subject: [PATCH 2/5] . --- .../suites/tracing/google-genai/test.ts | 4 ++-- packages/core/src/tracing/anthropic-ai/streaming.ts | 4 ++-- packages/core/src/tracing/anthropic-ai/utils.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/dev-packages/node-integration-tests/suites/tracing/google-genai/test.ts b/dev-packages/node-integration-tests/suites/tracing/google-genai/test.ts index 1a66906e0b04..993984cc6b3d 100644 --- a/dev-packages/node-integration-tests/suites/tracing/google-genai/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/google-genai/test.ts @@ -376,7 +376,7 @@ describe('Google GenAI integration', () => { description: 'generate_content blocked-model', op: 'gen_ai.generate_content', origin: 'auto.ai.google_genai', - status: 'permission_denied', + status: 'internal_error', }), // Fifth span - error handling for streaming expect.objectContaining({ @@ -475,7 +475,7 @@ describe('Google GenAI integration', () => { description: 'generate_content blocked-model', op: 'gen_ai.generate_content', origin: 'auto.ai.google_genai', - status: 'permission_denied', + status: 'internal_error', }), // Fifth span - error handling for streaming with PII expect.objectContaining({ diff --git a/packages/core/src/tracing/anthropic-ai/streaming.ts b/packages/core/src/tracing/anthropic-ai/streaming.ts index b42f34324207..940ec53e8030 100644 --- a/packages/core/src/tracing/anthropic-ai/streaming.ts +++ b/packages/core/src/tracing/anthropic-ai/streaming.ts @@ -11,7 +11,7 @@ import { } from '../ai/gen-ai-attributes'; import { setTokenUsageAttributes } from '../ai/utils'; import type { AnthropicAiStreamingEvent } from './types'; -import { mapAnthropicErrorToStatus } from './utils'; +import { mapAnthropicErrorToStatusMessage } from './utils'; /** * State object used to accumulate information from a stream of Anthropic AI events. @@ -60,7 +60,7 @@ function isErrorEvent(event: AnthropicAiStreamingEvent, span: Span): boolean { // If the event is an error, set the span status and capture the error // These error events are not rejected by the API by default, but are sent as metadata of the response if (event.type === 'error') { - span.setStatus({ code: SPAN_STATUS_ERROR, message: mapAnthropicErrorToStatus(event.error?.type) }); + span.setStatus({ code: SPAN_STATUS_ERROR, message: mapAnthropicErrorToStatusMessage(event.error?.type) }); captureException(event.error, { mechanism: { handled: false, diff --git a/packages/core/src/tracing/anthropic-ai/utils.ts b/packages/core/src/tracing/anthropic-ai/utils.ts index 222d3d30c987..93d86546db1d 100644 --- a/packages/core/src/tracing/anthropic-ai/utils.ts +++ b/packages/core/src/tracing/anthropic-ai/utils.ts @@ -56,7 +56,7 @@ const ANTHROPIC_ERROR_TYPE_TO_SPAN_STATUS: Record = { * Map an Anthropic API error type to a SpanStatusType value. * @see https://docs.anthropic.com/en/api/errors#error-shapes */ -export function mapAnthropicErrorToStatus(errorType: string | undefined): string { +export function mapAnthropicErrorToStatusMessage(errorType: string | undefined): string { if (!errorType) { return 'internal_error'; } @@ -69,7 +69,7 @@ export function mapAnthropicErrorToStatus(errorType: string | undefined): string */ export function handleResponseError(span: Span, response: AnthropicAiResponse): void { if (response.error) { - span.setStatus({ code: SPAN_STATUS_ERROR, message: mapAnthropicErrorToStatus(response.error.type) }); + span.setStatus({ code: SPAN_STATUS_ERROR, message: mapAnthropicErrorToStatusMessage(response.error.type) }); captureException(response.error, { mechanism: { From af37b75792d7c0b7f5e4ee9c7f75c52b474e888c Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Wed, 18 Mar 2026 17:14:47 +0100 Subject: [PATCH 3/5] update --- packages/core/src/tracing/google-genai/streaming.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/tracing/google-genai/streaming.ts b/packages/core/src/tracing/google-genai/streaming.ts index a2a7909292e4..d3f6598b8fd7 100644 --- a/packages/core/src/tracing/google-genai/streaming.ts +++ b/packages/core/src/tracing/google-genai/streaming.ts @@ -46,7 +46,7 @@ function isErrorChunk(chunk: GoogleGenAIResponse, span: Span): boolean { const feedback = chunk?.promptFeedback; if (feedback?.blockReason) { const message = feedback.blockReasonMessage ?? feedback.blockReason; - span.setStatus({ code: SPAN_STATUS_ERROR, message: 'permission_denied' }); + span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); captureException(`Content blocked: ${message}`, { mechanism: { handled: false, type: 'auto.ai.google_genai' }, }); From 1a21d5ad62b6db233ac4a36de95261711711937e Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Thu, 19 Mar 2026 10:13:14 +0100 Subject: [PATCH 4/5] better types --- packages/core/src/tracing/anthropic-ai/utils.ts | 5 +++-- packages/core/src/types-hoist/spanStatus.ts | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/core/src/tracing/anthropic-ai/utils.ts b/packages/core/src/tracing/anthropic-ai/utils.ts index 93d86546db1d..e2cadcec331b 100644 --- a/packages/core/src/tracing/anthropic-ai/utils.ts +++ b/packages/core/src/tracing/anthropic-ai/utils.ts @@ -1,6 +1,7 @@ import { captureException } from '../../exports'; import { SPAN_STATUS_ERROR } from '../../tracing'; import type { Span } from '../../types-hoist/span'; +import type { SpanStatusType } from '../../types-hoist/spanStatus'; import { GEN_AI_INPUT_MESSAGES_ATTRIBUTE, GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE, @@ -41,7 +42,7 @@ export function setMessagesAttribute(span: Span, messages: unknown): void { }); } -const ANTHROPIC_ERROR_TYPE_TO_SPAN_STATUS: Record = { +const ANTHROPIC_ERROR_TYPE_TO_SPAN_STATUS: Record = { invalid_request_error: 'invalid_argument', authentication_error: 'unauthenticated', permission_error: 'permission_denied', @@ -56,7 +57,7 @@ const ANTHROPIC_ERROR_TYPE_TO_SPAN_STATUS: Record = { * Map an Anthropic API error type to a SpanStatusType value. * @see https://docs.anthropic.com/en/api/errors#error-shapes */ -export function mapAnthropicErrorToStatusMessage(errorType: string | undefined): string { +export function mapAnthropicErrorToStatusMessage(errorType: string | undefined): SpanStatusType { if (!errorType) { return 'internal_error'; } diff --git a/packages/core/src/types-hoist/spanStatus.ts b/packages/core/src/types-hoist/spanStatus.ts index 8347d37adb1a..151530b5cb69 100644 --- a/packages/core/src/types-hoist/spanStatus.ts +++ b/packages/core/src/types-hoist/spanStatus.ts @@ -1,4 +1,4 @@ -type SpanStatusType = +export type SpanStatusType = /** The operation completed successfully. */ | 'ok' /** Deadline expired before operation could complete. */ From bcf542f5aa97b22b5357ab3825551f6bc36db270 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Thu, 19 Mar 2026 10:20:49 +0100 Subject: [PATCH 5/5] add unit tests --- .../test/lib/utils/anthropic-utils.test.ts | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/packages/core/test/lib/utils/anthropic-utils.test.ts b/packages/core/test/lib/utils/anthropic-utils.test.ts index 797bb9bc8186..b912af40e35e 100644 --- a/packages/core/test/lib/utils/anthropic-utils.test.ts +++ b/packages/core/test/lib/utils/anthropic-utils.test.ts @@ -1,8 +1,34 @@ import { describe, expect, it } from 'vitest'; -import { messagesFromParams, setMessagesAttribute, shouldInstrument } from '../../../src/tracing/anthropic-ai/utils'; +import { + mapAnthropicErrorToStatusMessage, + messagesFromParams, + setMessagesAttribute, + shouldInstrument, +} from '../../../src/tracing/anthropic-ai/utils'; import type { Span } from '../../../src/types-hoist/span'; describe('anthropic-ai-utils', () => { + describe('mapAnthropicErrorToStatusMessage', () => { + it('maps known Anthropic error types to SpanStatusType values', () => { + expect(mapAnthropicErrorToStatusMessage('invalid_request_error')).toBe('invalid_argument'); + expect(mapAnthropicErrorToStatusMessage('authentication_error')).toBe('unauthenticated'); + expect(mapAnthropicErrorToStatusMessage('permission_error')).toBe('permission_denied'); + expect(mapAnthropicErrorToStatusMessage('not_found_error')).toBe('not_found'); + expect(mapAnthropicErrorToStatusMessage('request_too_large')).toBe('failed_precondition'); + expect(mapAnthropicErrorToStatusMessage('rate_limit_error')).toBe('resource_exhausted'); + expect(mapAnthropicErrorToStatusMessage('api_error')).toBe('internal_error'); + expect(mapAnthropicErrorToStatusMessage('overloaded_error')).toBe('unavailable'); + }); + + it('falls back to internal_error for unknown error types', () => { + expect(mapAnthropicErrorToStatusMessage('some_new_error')).toBe('internal_error'); + }); + + it('falls back to internal_error for undefined', () => { + expect(mapAnthropicErrorToStatusMessage(undefined)).toBe('internal_error'); + }); + }); + describe('shouldInstrument', () => { it('should instrument known methods', () => { expect(shouldInstrument('models.get')).toBe(true);