Skip to content

Commit 0c4875e

Browse files
authored
fix(core): Classify custom AggregateErrors as exception groups (#19053)
This PR fixes a bug where classes extending from `AggregateError` would not correctly be identified as exception groups in Sentry because the `is_exception_group` flag was missing in the error's mechanism.
1 parent 53879ea commit 0c4875e

File tree

4 files changed

+131
-5
lines changed

4 files changed

+131
-5
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
class CustomAggregateError extends AggregateError {
2+
constructor(errors, message, options) {
3+
super(errors, message, options);
4+
this.name = 'CustomAggregateError';
5+
}
6+
}
7+
8+
const aggregateError = new CustomAggregateError(
9+
[new Error('error 1', { cause: new Error('error 1 cause') }), new Error('error 2')],
10+
'custom aggregate error',
11+
{
12+
cause: new Error('aggregate cause'),
13+
},
14+
);
15+
16+
Sentry.captureException(aggregateError);
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { expect } from '@playwright/test';
2+
import { sentryTest } from '../../../../utils/fixtures';
3+
import { envelopeRequestParser, waitForErrorRequestOnUrl } from '../../../../utils/helpers';
4+
5+
sentryTest('captures custom AggregateErrors', async ({ getLocalTestUrl, page }) => {
6+
const url = await getLocalTestUrl({ testDir: __dirname });
7+
const req = await waitForErrorRequestOnUrl(page, url);
8+
const eventData = envelopeRequestParser(req);
9+
10+
expect(eventData.exception?.values).toHaveLength(5); // CustomAggregateError + 3 embedded errors + 1 aggregate cause
11+
12+
// Verify the embedded errors come first
13+
expect(eventData.exception?.values).toEqual([
14+
expect.objectContaining({
15+
mechanism: { exception_id: 4, handled: true, parent_id: 0, source: 'errors[1]', type: 'chained' },
16+
type: 'Error',
17+
value: 'error 2',
18+
}),
19+
expect.objectContaining({
20+
mechanism: { exception_id: 3, handled: true, parent_id: 2, source: 'cause', type: 'chained' },
21+
type: 'Error',
22+
value: 'error 1 cause',
23+
}),
24+
expect.objectContaining({
25+
mechanism: { exception_id: 2, handled: true, parent_id: 0, source: 'errors[0]', type: 'chained' },
26+
type: 'Error',
27+
value: 'error 1',
28+
}),
29+
expect.objectContaining({
30+
mechanism: { exception_id: 1, handled: true, parent_id: 0, source: 'cause', type: 'chained' },
31+
type: 'Error',
32+
value: 'aggregate cause',
33+
}),
34+
expect.objectContaining({
35+
mechanism: { exception_id: 0, handled: true, type: 'generic', is_exception_group: true },
36+
type: 'CustomAggregateError',
37+
value: 'custom aggregate error',
38+
}),
39+
]);
40+
});

packages/core/src/utils/aggregate-errors.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ function aggregateExceptionsFromError(
5656

5757
// Recursively call this function in order to walk down a chain of errors
5858
if (isInstanceOf(error[key], Error)) {
59-
applyExceptionGroupFieldsForParentException(exception, exceptionId);
59+
applyExceptionGroupFieldsForParentException(exception, exceptionId, error);
6060
const newException = exceptionFromErrorImplementation(parser, error[key] as Error);
6161
const newExceptionId = newExceptions.length;
6262
applyExceptionGroupFieldsForChildException(newException, key, newExceptionId, exceptionId);
@@ -74,10 +74,10 @@ function aggregateExceptionsFromError(
7474

7575
// This will create exception grouping for AggregateErrors
7676
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AggregateError
77-
if (Array.isArray(error.errors)) {
77+
if (isExceptionGroup(error)) {
7878
error.errors.forEach((childError, i) => {
7979
if (isInstanceOf(childError, Error)) {
80-
applyExceptionGroupFieldsForParentException(exception, exceptionId);
80+
applyExceptionGroupFieldsForParentException(exception, exceptionId, error);
8181
const newException = exceptionFromErrorImplementation(parser, childError as Error);
8282
const newExceptionId = newExceptions.length;
8383
applyExceptionGroupFieldsForChildException(newException, `errors[${i}]`, newExceptionId, exceptionId);
@@ -98,12 +98,20 @@ function aggregateExceptionsFromError(
9898
return newExceptions;
9999
}
100100

101-
function applyExceptionGroupFieldsForParentException(exception: Exception, exceptionId: number): void {
101+
function isExceptionGroup(error: ExtendedError): error is ExtendedError & { errors: unknown[] } {
102+
return Array.isArray(error.errors);
103+
}
104+
105+
function applyExceptionGroupFieldsForParentException(
106+
exception: Exception,
107+
exceptionId: number,
108+
error: ExtendedError,
109+
): void {
102110
exception.mechanism = {
103111
handled: true,
104112
type: 'auto.core.linked_errors',
113+
...(isExceptionGroup(error) && { is_exception_group: true }),
105114
...exception.mechanism,
106-
...(exception.type === 'AggregateError' && { is_exception_group: true }),
107115
exception_id: exceptionId,
108116
};
109117
}

packages/core/test/lib/utils/aggregate-errors.test.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,16 @@ class FakeAggregateError extends Error {
2020
}
2121
}
2222

23+
class CustomAggregateError extends FakeAggregateError {
24+
public cause?: Error;
25+
26+
constructor(errors: Error[], message: string, cause?: Error) {
27+
super(errors, message);
28+
this.name = 'CustomAggregateError';
29+
this.cause = cause;
30+
}
31+
}
32+
2333
describe('applyAggregateErrorsToEvent()', () => {
2434
test('should not do anything if event does not contain an exception', () => {
2535
const event: Event = { exception: undefined };
@@ -316,4 +326,56 @@ describe('applyAggregateErrorsToEvent()', () => {
316326
},
317327
});
318328
});
329+
330+
test('marks custom AggregateErrors as exception groups', () => {
331+
const customAggregateError = new CustomAggregateError(
332+
[new Error('Nested Error 1')],
333+
'my CustomAggregateError',
334+
new Error('Aggregate Cause'),
335+
);
336+
337+
const event: Event = { exception: { values: [exceptionFromError(stackParser, customAggregateError)] } };
338+
const eventHint: EventHint = { originalException: customAggregateError };
339+
340+
applyAggregateErrorsToEvent(exceptionFromError, stackParser, 'cause', 100, event, eventHint);
341+
342+
expect(event).toStrictEqual({
343+
exception: {
344+
values: [
345+
{
346+
mechanism: {
347+
exception_id: 2,
348+
handled: true,
349+
parent_id: 0,
350+
source: 'errors[0]',
351+
type: 'chained',
352+
},
353+
type: 'Error',
354+
value: 'Nested Error 1',
355+
},
356+
{
357+
mechanism: {
358+
exception_id: 1,
359+
handled: true,
360+
parent_id: 0,
361+
source: 'cause',
362+
type: 'chained',
363+
},
364+
type: 'Error',
365+
value: 'Aggregate Cause',
366+
},
367+
{
368+
mechanism: {
369+
exception_id: 0,
370+
handled: true,
371+
type: 'instrument',
372+
is_exception_group: true,
373+
},
374+
type: 'CustomAggregateError',
375+
value: 'my CustomAggregateError',
376+
},
377+
],
378+
},
379+
});
380+
});
319381
});

0 commit comments

Comments
 (0)