Skip to content
Merged
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
32 changes: 25 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 { debug, captureException, SPAN_STATUS_ERROR, getAsyncContextStrategy, getMainCarrier } from '@sentry/core';
import { DEBUG_BUILD } from './debug-build';
import { ERROR_TYPE } 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,25 @@ 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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bug: The falsy check in getErrorInfo incorrectly maps an Error with an empty message to 'unknown_error' instead of an empty string.
Severity: LOW

Suggested Fix

Replace the ternary operator's falsy check with a nullish coalescing operator (??) to correctly handle empty strings, which are falsy. For example: const message = raw ?? 'unknown_error';. This ensures that an empty string message is preserved.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: packages/server-utils/src/tracing-channel.ts#L204

Potential issue: The `getErrorInfo` function uses a falsy check (`raw ? ...`) to
determine the error message. Since an empty string (`''`) is a falsy value in
JavaScript, if an error is thrown with an empty message (e.g., `new Error('')`), its
message will be incorrectly reported as `'unknown_error'`. This is a regression from the
previous behavior which would have correctly preserved the empty string. While creating
errors with empty messages is uncommon, it is a valid edge case that could be triggered
by third-party libraries or dynamic error creation, leading to less accurate tracing
information.

Did we get this right? 👍 / 👎 to inform future reviews.

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

return {
message,
attributes: {
[ERROR_TYPE]: type,
},
};
}
74 changes: 74 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,80 @@ 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');
});

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');
});

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');
});

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');
});
});
});

describe('captureError', () => {
Expand Down
Loading