Skip to content

Commit 2a182e2

Browse files
committed
fix(core): Identify custom AggregateErrors as exception groups
1 parent aa986f5 commit 2a182e2

4 files changed

Lines changed: 156 additions & 4 deletions

File tree

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: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
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+
{
15+
mechanism: { exception_id: 4, handled: true, parent_id: 0, source: 'errors[1]', type: 'chained' },
16+
stacktrace: {
17+
frames: [
18+
{ colno: 12, filename: 'http://sentry-test.io/subject.bundle.js', function: '?', in_app: true, lineno: 14 },
19+
{ colno: 5, filename: 'http://sentry-test.io/subject.bundle.js', function: '?', in_app: true, lineno: 10 },
20+
],
21+
},
22+
type: 'Error',
23+
value: 'error 2',
24+
},
25+
{
26+
mechanism: { exception_id: 3, handled: true, parent_id: 2, source: 'cause', type: 'chained' },
27+
stacktrace: {
28+
frames: [
29+
{ colno: 12, filename: 'http://sentry-test.io/subject.bundle.js', function: '?', in_app: true, lineno: 14 },
30+
{ colno: 10, filename: 'http://sentry-test.io/subject.bundle.js', function: '?', in_app: true, lineno: 9 },
31+
],
32+
},
33+
type: 'Error',
34+
value: 'error 1 cause',
35+
},
36+
{
37+
mechanism: { exception_id: 2, handled: true, parent_id: 0, source: 'errors[0]', type: 'chained' },
38+
stacktrace: {
39+
frames: [
40+
{ colno: 12, filename: 'http://sentry-test.io/subject.bundle.js', function: '?', in_app: true, lineno: 14 },
41+
{ colno: 50, filename: 'http://sentry-test.io/subject.bundle.js', function: '?', in_app: true, lineno: 8 },
42+
],
43+
},
44+
type: 'Error',
45+
value: 'error 1',
46+
},
47+
{
48+
mechanism: { exception_id: 1, handled: true, parent_id: 0, source: 'cause', type: 'chained' },
49+
stacktrace: {
50+
frames: [
51+
{ colno: 12, filename: 'http://sentry-test.io/subject.bundle.js', function: '?', in_app: true, lineno: 14 },
52+
{ colno: 10, filename: 'http://sentry-test.io/subject.bundle.js', function: '?', in_app: true, lineno: 11 },
53+
],
54+
},
55+
type: 'Error',
56+
value: 'aggregate cause',
57+
},
58+
{
59+
mechanism: { exception_id: 0, handled: true, type: 'generic', is_exception_group: true },
60+
stacktrace: {
61+
frames: [
62+
{ colno: 12, filename: 'http://sentry-test.io/subject.bundle.js', function: '?', in_app: true, lineno: 14 },
63+
{ colno: 24, filename: 'http://sentry-test.io/subject.bundle.js', function: '?', in_app: true, lineno: 8 },
64+
],
65+
},
66+
type: 'CustomAggregateError',
67+
value: 'custom aggregate error',
68+
},
69+
]);
70+
});

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

Lines changed: 8 additions & 4 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);
@@ -77,7 +77,7 @@ function aggregateExceptionsFromError(
7777
if (Array.isArray(error.errors)) {
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,16 @@ function aggregateExceptionsFromError(
9898
return newExceptions;
9999
}
100100

101-
function applyExceptionGroupFieldsForParentException(exception: Exception, exceptionId: number): void {
101+
function applyExceptionGroupFieldsForParentException(
102+
exception: Exception,
103+
exceptionId: number,
104+
error: ExtendedError,
105+
): void {
102106
exception.mechanism = {
103107
handled: true,
104108
type: 'auto.core.linked_errors',
109+
...(Array.isArray(error.errors) && { is_exception_group: true }),
105110
...exception.mechanism,
106-
...(exception.type === 'AggregateError' && { is_exception_group: true }),
107111
exception_id: exceptionId,
108112
};
109113
}

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)