Skip to content

Commit 63da54c

Browse files
authored
feat: Support AggregateErrors in LinkedErrors integration (#8463)
1 parent 5854132 commit 63da54c

File tree

3 files changed

+302
-24
lines changed

3 files changed

+302
-24
lines changed

packages/types/src/mechanism.ts

+25
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,29 @@ export interface Mechanism {
2929
* to recreate the stacktrace.
3030
*/
3131
synthetic?: boolean;
32+
33+
/**
34+
* Describes the source of the exception, in the case that this is a derived (linked or aggregate) error.
35+
*
36+
* This should be populated with the name of the property where the exception was found on the parent exception.
37+
* E.g. "cause", "errors[0]", "errors[1]"
38+
*/
39+
source?: string;
40+
41+
/**
42+
* Indicates whether the exception is an `AggregateException`.
43+
*/
44+
is_exception_group?: boolean;
45+
46+
/**
47+
* An identifier for the exception inside the `event.exception.values` array. This identifier is referenced to via the
48+
* `parent_id` attribute to link and aggregate errors.
49+
*/
50+
exception_id?: number;
51+
52+
/**
53+
* References another exception via the `exception_id` field to indicate that this excpetion is a child of that
54+
* exception in the case of aggregate or linked errors.
55+
*/
56+
parent_id?: number;
3257
}

packages/utils/src/aggregate-errors.ts

+95-20
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,28 @@ export function applyAggregateErrorsToEvent(
1212
limit: number,
1313
event: Event,
1414
hint?: EventHint,
15-
): Event | null {
15+
): void {
1616
if (!event.exception || !event.exception.values || !hint || !isInstanceOf(hint.originalException, Error)) {
17-
return event;
17+
return;
1818
}
1919

20-
const linkedErrors = aggregateExceptionsFromError(
21-
exceptionFromErrorImplementation,
22-
parser,
23-
limit,
24-
hint.originalException as ExtendedError,
25-
key,
26-
);
20+
// Generally speaking the last item in `event.exception.values` is the exception originating from the original Error
21+
const originalException: Exception | undefined =
22+
event.exception.values.length > 0 ? event.exception.values[event.exception.values.length - 1] : undefined;
2723

28-
event.exception.values = [...linkedErrors, ...event.exception.values];
29-
30-
return event;
24+
// We only create exception grouping if there is an exception in the event.
25+
if (originalException) {
26+
event.exception.values = aggregateExceptionsFromError(
27+
exceptionFromErrorImplementation,
28+
parser,
29+
limit,
30+
hint.originalException as ExtendedError,
31+
key,
32+
event.exception.values,
33+
originalException,
34+
0,
35+
);
36+
}
3137
}
3238

3339
function aggregateExceptionsFromError(
@@ -36,15 +42,84 @@ function aggregateExceptionsFromError(
3642
limit: number,
3743
error: ExtendedError,
3844
key: string,
39-
stack: Exception[] = [],
45+
prevExceptions: Exception[],
46+
exception: Exception,
47+
exceptionId: number,
4048
): Exception[] {
41-
if (!isInstanceOf(error[key], Error) || stack.length >= limit) {
42-
return stack;
49+
if (prevExceptions.length >= limit + 1) {
50+
return prevExceptions;
51+
}
52+
53+
let newExceptions = [...prevExceptions];
54+
55+
if (isInstanceOf(error[key], Error)) {
56+
applyExceptionGroupFieldsForParentException(exception, exceptionId);
57+
const newException = exceptionFromErrorImplementation(parser, error[key]);
58+
const newExceptionId = newExceptions.length;
59+
applyExceptionGroupFieldsForChildException(newException, key, newExceptionId, exceptionId);
60+
newExceptions = aggregateExceptionsFromError(
61+
exceptionFromErrorImplementation,
62+
parser,
63+
limit,
64+
error[key],
65+
key,
66+
[newException, ...newExceptions],
67+
newException,
68+
newExceptionId,
69+
);
70+
}
71+
72+
// This will create exception grouping for AggregateErrors
73+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AggregateError
74+
if (Array.isArray(error.errors)) {
75+
error.errors.forEach((childError, i) => {
76+
if (isInstanceOf(childError, Error)) {
77+
applyExceptionGroupFieldsForParentException(exception, exceptionId);
78+
const newException = exceptionFromErrorImplementation(parser, childError);
79+
const newExceptionId = newExceptions.length;
80+
applyExceptionGroupFieldsForChildException(newException, `errors[${i}]`, newExceptionId, exceptionId);
81+
newExceptions = aggregateExceptionsFromError(
82+
exceptionFromErrorImplementation,
83+
parser,
84+
limit,
85+
childError,
86+
key,
87+
[newException, ...newExceptions],
88+
newException,
89+
newExceptionId,
90+
);
91+
}
92+
});
4393
}
4494

45-
const exception = exceptionFromErrorImplementation(parser, error[key]);
46-
return aggregateExceptionsFromError(exceptionFromErrorImplementation, parser, limit, error[key], key, [
47-
exception,
48-
...stack,
49-
]);
95+
return newExceptions;
96+
}
97+
98+
function applyExceptionGroupFieldsForParentException(exception: Exception, exceptionId: number): void {
99+
// Don't know if this default makes sense. The protocol requires us to set these values so we pick *some* default.
100+
exception.mechanism = exception.mechanism || { type: 'generic', handled: true };
101+
102+
exception.mechanism = {
103+
...exception.mechanism,
104+
is_exception_group: true,
105+
exception_id: exceptionId,
106+
};
107+
}
108+
109+
function applyExceptionGroupFieldsForChildException(
110+
exception: Exception,
111+
source: string,
112+
exceptionId: number,
113+
parentId: number | undefined,
114+
): void {
115+
// Don't know if this default makes sense. The protocol requires us to set these values so we pick *some* default.
116+
exception.mechanism = exception.mechanism || { type: 'generic', handled: true };
117+
118+
exception.mechanism = {
119+
...exception.mechanism,
120+
type: 'chained',
121+
source,
122+
exception_id: exceptionId,
123+
parent_id: parentId,
124+
};
50125
}

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

+182-4
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { applyAggregateErrorsToEvent, createStackParser } from '../src/index';
44

55
const stackParser = createStackParser([0, line => ({ filename: line })]);
66
const exceptionFromError = (_stackParser: StackParser, ex: Error): Exception => {
7-
return { value: ex.message };
7+
return { value: ex.message, mechanism: { type: 'instrument', handled: true } };
88
};
99

1010
describe('applyAggregateErrorsToEvent()', () => {
@@ -46,28 +46,63 @@ describe('applyAggregateErrorsToEvent()', () => {
4646
test('should recursively walk the original exception based on the `key` option and add them as exceptions to the event', () => {
4747
const key = 'cause';
4848
const originalException: ExtendedError = new Error('Root Error');
49-
const event: Event = { exception: { values: [exceptionFromError(stackParser, originalException)] } };
5049
originalException[key] = new Error('Nested Error 1');
5150
originalException[key][key] = new Error('Nested Error 2');
51+
52+
const event: Event = { exception: { values: [exceptionFromError(stackParser, originalException)] } };
5253
const eventHint: EventHint = { originalException };
54+
5355
applyAggregateErrorsToEvent(exceptionFromError, stackParser, key, 100, event, eventHint);
5456
expect(event).toStrictEqual({
5557
exception: {
5658
values: [
5759
{
5860
value: 'Nested Error 2',
61+
mechanism: {
62+
exception_id: 2,
63+
handled: true,
64+
parent_id: 1,
65+
source: 'cause',
66+
type: 'chained',
67+
},
5968
},
6069
{
6170
value: 'Nested Error 1',
71+
mechanism: {
72+
exception_id: 1,
73+
handled: true,
74+
parent_id: 0,
75+
is_exception_group: true,
76+
source: 'cause',
77+
type: 'chained',
78+
},
6279
},
6380
{
6481
value: 'Root Error',
82+
mechanism: {
83+
exception_id: 0,
84+
handled: true,
85+
is_exception_group: true,
86+
type: 'instrument',
87+
},
6588
},
6689
],
6790
},
6891
});
6992
});
7093

94+
test('should not modify event if there are no attached errors', () => {
95+
const originalException: ExtendedError = new Error('Some Error');
96+
97+
const event: Event = { exception: { values: [exceptionFromError(stackParser, originalException)] } };
98+
const eventHint: EventHint = { originalException };
99+
100+
applyAggregateErrorsToEvent(exceptionFromError, stackParser, 'cause', 100, event, eventHint);
101+
102+
// no changes
103+
expect(event).toStrictEqual({ exception: { values: [exceptionFromError(stackParser, originalException)] } });
104+
});
105+
71106
test('should allow to limit number of attached errors', () => {
72107
const key = 'cause';
73108
const originalException: ExtendedError = new Error('Root Error');
@@ -89,9 +124,152 @@ describe('applyAggregateErrorsToEvent()', () => {
89124
// Last exception in list should be the root exception
90125
expect(event.exception?.values?.[event.exception?.values.length - 1]).toStrictEqual({
91126
value: 'Root Error',
127+
mechanism: {
128+
exception_id: 0,
129+
handled: true,
130+
is_exception_group: true,
131+
type: 'instrument',
132+
},
92133
});
93134
});
94135

95-
test.todo('should recursively walk AggregateErrors and add them as exceptions to the event');
96-
test.todo('should recursively walk mixed errors (Aggregate errors and based on `key`)');
136+
test('should keep the original mechanism type for the root exception', () => {
137+
const fakeAggregateError: ExtendedError = new Error('Root Error');
138+
fakeAggregateError.errors = [new Error('Nested Error 1'), new Error('Nested Error 2')];
139+
140+
const event: Event = { exception: { values: [exceptionFromError(stackParser, fakeAggregateError)] } };
141+
const eventHint: EventHint = { originalException: fakeAggregateError };
142+
143+
applyAggregateErrorsToEvent(exceptionFromError, stackParser, 'cause', 100, event, eventHint);
144+
expect(event.exception?.values?.[event.exception.values.length - 1].mechanism?.type).toBe('instrument');
145+
});
146+
147+
test('should recursively walk mixed errors (Aggregate errors and based on `key`)', () => {
148+
const chainedError: ExtendedError = new Error('Nested Error 3');
149+
chainedError.cause = new Error('Nested Error 4');
150+
const fakeAggregateError2: ExtendedError = new Error('AggregateError2');
151+
fakeAggregateError2.errors = [new Error('Nested Error 2'), chainedError];
152+
const fakeAggregateError1: ExtendedError = new Error('AggregateError1');
153+
fakeAggregateError1.errors = [new Error('Nested Error 1'), fakeAggregateError2];
154+
155+
const event: Event = { exception: { values: [exceptionFromError(stackParser, fakeAggregateError1)] } };
156+
const eventHint: EventHint = { originalException: fakeAggregateError1 };
157+
158+
applyAggregateErrorsToEvent(exceptionFromError, stackParser, 'cause', 100, event, eventHint);
159+
expect(event).toStrictEqual({
160+
exception: {
161+
values: [
162+
{
163+
mechanism: {
164+
exception_id: 5,
165+
handled: true,
166+
parent_id: 4,
167+
source: 'cause',
168+
type: 'chained',
169+
},
170+
value: 'Nested Error 4',
171+
},
172+
{
173+
mechanism: {
174+
exception_id: 4,
175+
handled: true,
176+
is_exception_group: true,
177+
parent_id: 2,
178+
source: 'errors[1]',
179+
type: 'chained',
180+
},
181+
value: 'Nested Error 3',
182+
},
183+
{
184+
mechanism: {
185+
exception_id: 3,
186+
handled: true,
187+
parent_id: 2,
188+
source: 'errors[0]',
189+
type: 'chained',
190+
},
191+
value: 'Nested Error 2',
192+
},
193+
{
194+
mechanism: {
195+
exception_id: 2,
196+
handled: true,
197+
is_exception_group: true,
198+
parent_id: 0,
199+
source: 'errors[1]',
200+
type: 'chained',
201+
},
202+
value: 'AggregateError2',
203+
},
204+
{
205+
mechanism: {
206+
exception_id: 1,
207+
handled: true,
208+
parent_id: 0,
209+
source: 'errors[0]',
210+
type: 'chained',
211+
},
212+
value: 'Nested Error 1',
213+
},
214+
{
215+
mechanism: {
216+
exception_id: 0,
217+
handled: true,
218+
is_exception_group: true,
219+
type: 'instrument',
220+
},
221+
value: 'AggregateError1',
222+
},
223+
],
224+
},
225+
});
226+
});
227+
228+
test('should keep the original mechanism type for the root exception', () => {
229+
const key = 'cause';
230+
const originalException: ExtendedError = new Error('Root Error');
231+
originalException[key] = new Error('Nested Error 1');
232+
originalException[key][key] = new Error('Nested Error 2');
233+
234+
const event: Event = { exception: { values: [exceptionFromError(stackParser, originalException)] } };
235+
const eventHint: EventHint = { originalException };
236+
237+
applyAggregateErrorsToEvent(exceptionFromError, stackParser, key, 100, event, eventHint);
238+
expect(event).toStrictEqual({
239+
exception: {
240+
values: [
241+
{
242+
value: 'Nested Error 2',
243+
mechanism: {
244+
exception_id: 2,
245+
handled: true,
246+
parent_id: 1,
247+
source: 'cause',
248+
type: 'chained',
249+
},
250+
},
251+
{
252+
value: 'Nested Error 1',
253+
mechanism: {
254+
exception_id: 1,
255+
handled: true,
256+
parent_id: 0,
257+
is_exception_group: true,
258+
source: 'cause',
259+
type: 'chained',
260+
},
261+
},
262+
{
263+
value: 'Root Error',
264+
mechanism: {
265+
exception_id: 0,
266+
handled: true,
267+
is_exception_group: true,
268+
type: 'instrument',
269+
},
270+
},
271+
],
272+
},
273+
});
274+
});
97275
});

0 commit comments

Comments
 (0)