Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 26 additions & 7 deletions packages/server-utils/src/tracing-channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 extends object> = TData & {
_sentrySpan?: Span;
Expand Down Expand Up @@ -103,7 +104,9 @@ export function bindTracingChannelToSpan<TData extends object>(
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);
Expand Down Expand Up @@ -186,10 +189,26 @@ function endBoundSpan<TData extends object>(
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<string, string>;
};

/**
* 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';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty Error message misclassified

Low Severity

getErrorInfo uses raw ? String(raw) : 'unknown_error', so when an Error has an empty message (e.g. new Error()), the span status and sentry.status.message become unknown_error instead of the empty string from .message, unlike the previous getErrorMessage logic and unlike nearby MySQL tracing-channel handling.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b802211. Configure here.

const type = isObject && 'name' in error ? String(error.name) : 'unknown';

return {
message,
attributes: {
[ERROR_TYPE]: type,
[SENTRY_STATUS_MESSAGE]: message,
},
};
}
78 changes: 78 additions & 0 deletions packages/server-utils/test/tracing-channel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
Loading