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
30 changes: 30 additions & 0 deletions packages/core/src/utils/aggregate-errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,13 +95,43 @@ function aggregateExceptionsFromError(
});
}

// SuppressedError (explicit resource management) wraps two errors: `error` (the new one thrown during disposal)
// and `suppressed` (the original one it replaced).
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SuppressedError

if (isSuppressedError(error)) {
['error', 'suppressed'].forEach(property => {
const childError = error[property];
if (isError(childError)) {
applyExceptionGroupFieldsForParentException(exception, exceptionId, error);
const newException = exceptionFromErrorImplementation(parser, childError);
const newExceptionId = newExceptions.length;
applyExceptionGroupFieldsForChildException(newException, property, newExceptionId, exceptionId);
newExceptions = aggregateExceptionsFromError(
exceptionFromErrorImplementation,
parser,
limit,
childError as ExtendedError,
key,
[newException, ...newExceptions],
newException,
newExceptionId,
);
}
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Feat is missing an integration test

Medium Severity

Flagged because the review rules require feat PRs to include an integration or E2E test. Coverage here is unit-only via FakeSuppressedError, so native using / SuppressedError never runs through LinkedErrors. A Node test gated with test.runIf(supports({ min: 22 })) would close that gap.

Fix in Cursor Fix in Web

Triggered by project rule: PR Review Guidelines for Cursor Bot

Reviewed by Cursor Bugbot for commit 91c0b88. Configure here.


return newExceptions;
}

function isExceptionGroup(error: ExtendedError): error is ExtendedError & { errors: unknown[] } {
return Array.isArray(error.errors);
}

function isSuppressedError(error: ExtendedError): boolean {
return 'error' in error && 'suppressed' in error;
}

function applyExceptionGroupFieldsForParentException(
exception: Exception,
exceptionId: number,
Expand Down
84 changes: 84 additions & 0 deletions packages/core/test/lib/utils/aggregate-errors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,18 @@ class CustomAggregateError extends FakeAggregateError {
}
}

class FakeSuppressedError extends Error {
public error: unknown;
public suppressed: unknown;

constructor(error: unknown, suppressed: unknown, message = 'An error was suppressed during disposal.') {
super(message);
this.name = 'SuppressedError';
this.error = error;
this.suppressed = suppressed;
}
}

describe('applyAggregateErrorsToEvent()', () => {
test('should not do anything if event does not contain an exception', () => {
const event: Event = { exception: undefined };
Expand Down Expand Up @@ -482,4 +494,76 @@ describe('applyAggregateErrorsToEvent()', () => {
},
});
});

test('adds both the `error` and the `suppressed` error of a SuppressedError', () => {
const suppressedError = new FakeSuppressedError(new Error('Error from dispose'), new Error('Original error'));
const event: Event = { exception: { values: [exceptionFromError(stackParser, suppressedError)] } };
const eventHint: EventHint = { originalException: suppressedError };

applyAggregateErrorsToEvent(exceptionFromError, stackParser, 'cause', 100, event, eventHint);

expect(event).toStrictEqual({
exception: {
values: [
{
mechanism: { exception_id: 2, handled: true, parent_id: 0, source: 'suppressed', type: 'chained' },
type: 'Error',
value: 'Original error',
},
{
mechanism: { exception_id: 1, handled: true, parent_id: 0, source: 'error', type: 'chained' },
type: 'Error',
value: 'Error from dispose',
},
{
mechanism: { exception_id: 0, handled: true, type: 'instrument' },
type: 'SuppressedError',
value: 'An error was suppressed during disposal.',
},
],
},
});
});

test('walks nested SuppressedErrors (several disposers throwing)', () => {
const inner = new FakeSuppressedError(new Error('dispose 2 failed'), new Error('Original error'));
const outer = new FakeSuppressedError(new Error('dispose 1 failed'), inner);
const event: Event = { exception: { values: [exceptionFromError(stackParser, outer)] } };

applyAggregateErrorsToEvent(exceptionFromError, stackParser, 'cause', 100, event, { originalException: outer });

expect(event.exception?.values?.map(e => e.value)).toEqual([
'Original error',
'dispose 2 failed',
'An error was suppressed during disposal.',
'dispose 1 failed',
'An error was suppressed during disposal.',
]);
});

test('skips `error` / `suppressed` values that are not Errors', () => {
const suppressedError = new FakeSuppressedError('just a string', new Error('Original error'));
const event: Event = { exception: { values: [exceptionFromError(stackParser, suppressedError)] } };

applyAggregateErrorsToEvent(exceptionFromError, stackParser, 'cause', 100, event, {
originalException: suppressedError,
});

expect(event.exception?.values?.map(e => e.value)).toEqual([
'Original error',
'An error was suppressed during disposal.',
]);
});

test('does not treat an error that only has an `error` property as a SuppressedError', () => {
const notSuppressed: ExtendedError = new Error('Something else');
notSuppressed.error = new Error('unrelated');
const event: Event = { exception: { values: [exceptionFromError(stackParser, notSuppressed)] } };

applyAggregateErrorsToEvent(exceptionFromError, stackParser, 'cause', 100, event, {
originalException: notSuppressed,
});

expect(event.exception?.values).toHaveLength(1);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Feat lacks integration coverage

Medium Severity

This feat only adds unit tests around applyAggregateErrorsToEvent with a fake SuppressedError. The testing conventions require at least one integration or E2E test for feat PRs. An end-to-end captureException case with a native SuppressedError, matching the existing AggregateError suites, is missing.

Fix in Cursor Fix in Web

Triggered by project rule: PR Review Guidelines for Cursor Bot

Reviewed by Cursor Bugbot for commit 1834d47. Configure here.

});