From b802211049d0b35d855a6cc8e4c5b323ef06701d Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Fri, 26 Jun 2026 09:55:24 -0400 Subject: [PATCH] ref(server-utils): Simplify error info extraction and set error attributes on span Collapse the two branches of the thrown/rejected value handler into a single pass that derives the status message and `error.type` once, and set `error.type` / `sentry.status.message` attributes on the span alongside the error status. Add assertions covering Error instances, thrown primitives, bare error-like objects, and falsy throws. --- packages/server-utils/src/tracing-channel.ts | 33 ++++++-- .../server-utils/test/tracing-channel.test.ts | 78 +++++++++++++++++++ 2 files changed, 104 insertions(+), 7 deletions(-) diff --git a/packages/server-utils/src/tracing-channel.ts b/packages/server-utils/src/tracing-channel.ts index a0d449eb1489..4381732d3dd1 100644 --- a/packages/server-utils/src/tracing-channel.ts +++ b/packages/server-utils/src/tracing-channel.ts @@ -3,6 +3,7 @@ import type { AsyncLocalStorage } from 'node:async_hooks'; import type { ExclusiveEventHintOrCaptureContext, Span } from '@sentry/core'; import { _INTERNAL_getTracingChannelBinding, debug, captureException, SPAN_STATUS_ERROR } from '@sentry/core'; import { DEBUG_BUILD } from './debug-build'; +import { ERROR_TYPE, SENTRY_STATUS_MESSAGE } from '@sentry/conventions/attributes'; export type TracingChannelPayloadWithSpan = TData & { _sentrySpan?: Span; @@ -103,7 +104,9 @@ export function bindTracingChannelToSpan( captureException(data.error, getErrorHint(data.error)); } - span.setStatus({ code: SPAN_STATUS_ERROR, message: getErrorMessage(data.error) }); + const { message, attributes } = getErrorInfo(data.error); + span.setStatus({ code: SPAN_STATUS_ERROR, message }); + span.setAttributes(attributes); }, asyncEnd(data) { endBoundSpan(data, beforeSpanEnd); @@ -186,10 +189,26 @@ function endBoundSpan( span.end(); } -/** Best-effort short message for a span status: an error-like's `message`, otherwise its string form. */ -function getErrorMessage(error: unknown): string { - if (error && typeof error === 'object' && 'message' in error && typeof error.message === 'string') { - return error.message; - } - return String(error); +type ErrorInfo = { + message: string; + attributes: Record; +}; + +/** + * Best-effort message and attribute extraction for thrown/rejected values. + */ +function getErrorInfo(error: unknown): ErrorInfo { + const isObject = !!error && typeof error === 'object'; + const raw = isObject ? ('message' in error ? error.message : undefined) : error; + + const message = raw ? String(raw) : 'unknown_error'; + const type = isObject && 'name' in error ? String(error.name) : 'unknown'; + + return { + message, + attributes: { + [ERROR_TYPE]: type, + [SENTRY_STATUS_MESSAGE]: message, + }, + }; } diff --git a/packages/server-utils/test/tracing-channel.test.ts b/packages/server-utils/test/tracing-channel.test.ts index 53cb4b098ec1..90ad4543d328 100644 --- a/packages/server-utils/test/tracing-channel.test.ts +++ b/packages/server-utils/test/tracing-channel.test.ts @@ -370,6 +370,84 @@ describe('bindTracingChannelToSpan', () => { expect(spanToJSON(span).status).toBe('callback-sync-throw'); expect(captureExceptionSpy).not.toHaveBeenCalled(); }); + + describe('error status and attributes', () => { + it('derives the type from `name` and the status message from `message` for an Error instance', () => { + const { channel, span } = setup('test:lifecycle:error-attrs-error'); + + expect(() => + channel.traceSync( + () => { + throw new TypeError('bad input'); + }, + { operation: 'read' }, + ), + ).toThrow('bad input'); + + const { status, data } = spanToJSON(span); + expect(status).toBe('bad input'); + expect(data['error.type']).toBe('TypeError'); + expect(data['sentry.status.message']).toBe('bad input'); + }); + + it('stringifies a thrown primitive and marks the type unknown', () => { + const { channel, span } = setup('test:lifecycle:error-attrs-string'); + + expect(() => + channel.traceSync( + () => { + throw 'plain failure'; + }, + { operation: 'read' }, + ), + ).toThrow('plain failure'); + + const { status, data } = spanToJSON(span); + expect(status).toBe('plain failure'); + expect(data['error.type']).toBe('unknown'); + expect(data['sentry.status.message']).toBe('plain failure'); + }); + + it('falls back to unknown_error for an error-like object without `name` or `message`', () => { + const { channel, span } = setup('test:lifecycle:error-attrs-bare'); + + expect(() => + channel.traceSync( + () => { + throw { code: 500 }; + }, + { operation: 'read' }, + ), + ).toThrow(); + + const { status, data } = spanToJSON(span); + expect(status).toBe('unknown_error'); + expect(data['error.type']).toBe('unknown'); + expect(data['sentry.status.message']).toBe('unknown_error'); + }); + + it('falls back to unknown_error when a falsy value is thrown', () => { + const { channel, span } = setup('test:lifecycle:error-attrs-falsy'); + + let threw = false; + try { + channel.traceSync( + () => { + throw 0; + }, + { operation: 'read' }, + ); + } catch { + threw = true; + } + + expect(threw).toBe(true); + const { status, data } = spanToJSON(span); + expect(status).toBe('unknown_error'); + expect(data['error.type']).toBe('unknown'); + expect(data['sentry.status.message']).toBe('unknown_error'); + }); + }); }); describe('captureError', () => {