Skip to content

Commit

Permalink
feat: Support AggregateErrors in LinkedErrors integration (#8463)
Browse files Browse the repository at this point in the history
  • Loading branch information
lforst committed Jul 6, 2023
1 parent 5854132 commit 63da54c
Show file tree
Hide file tree
Showing 3 changed files with 302 additions and 24 deletions.
25 changes: 25 additions & 0 deletions packages/types/src/mechanism.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,29 @@ export interface Mechanism {
* to recreate the stacktrace.
*/
synthetic?: boolean;

/**
* Describes the source of the exception, in the case that this is a derived (linked or aggregate) error.
*
* This should be populated with the name of the property where the exception was found on the parent exception.
* E.g. "cause", "errors[0]", "errors[1]"
*/
source?: string;

/**
* Indicates whether the exception is an `AggregateException`.
*/
is_exception_group?: boolean;

/**
* An identifier for the exception inside the `event.exception.values` array. This identifier is referenced to via the
* `parent_id` attribute to link and aggregate errors.
*/
exception_id?: number;

/**
* References another exception via the `exception_id` field to indicate that this excpetion is a child of that
* exception in the case of aggregate or linked errors.
*/
parent_id?: number;
}
115 changes: 95 additions & 20 deletions packages/utils/src/aggregate-errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,28 @@ export function applyAggregateErrorsToEvent(
limit: number,
event: Event,
hint?: EventHint,
): Event | null {
): void {
if (!event.exception || !event.exception.values || !hint || !isInstanceOf(hint.originalException, Error)) {
return event;
return;
}

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

event.exception.values = [...linkedErrors, ...event.exception.values];

return event;
// We only create exception grouping if there is an exception in the event.
if (originalException) {
event.exception.values = aggregateExceptionsFromError(
exceptionFromErrorImplementation,
parser,
limit,
hint.originalException as ExtendedError,
key,
event.exception.values,
originalException,
0,
);
}
}

function aggregateExceptionsFromError(
Expand All @@ -36,15 +42,84 @@ function aggregateExceptionsFromError(
limit: number,
error: ExtendedError,
key: string,
stack: Exception[] = [],
prevExceptions: Exception[],
exception: Exception,
exceptionId: number,
): Exception[] {
if (!isInstanceOf(error[key], Error) || stack.length >= limit) {
return stack;
if (prevExceptions.length >= limit + 1) {
return prevExceptions;
}

let newExceptions = [...prevExceptions];

if (isInstanceOf(error[key], Error)) {
applyExceptionGroupFieldsForParentException(exception, exceptionId);
const newException = exceptionFromErrorImplementation(parser, error[key]);
const newExceptionId = newExceptions.length;
applyExceptionGroupFieldsForChildException(newException, key, newExceptionId, exceptionId);
newExceptions = aggregateExceptionsFromError(
exceptionFromErrorImplementation,
parser,
limit,
error[key],
key,
[newException, ...newExceptions],
newException,
newExceptionId,
);
}

// This will create exception grouping for AggregateErrors
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AggregateError
if (Array.isArray(error.errors)) {
error.errors.forEach((childError, i) => {
if (isInstanceOf(childError, Error)) {
applyExceptionGroupFieldsForParentException(exception, exceptionId);
const newException = exceptionFromErrorImplementation(parser, childError);
const newExceptionId = newExceptions.length;
applyExceptionGroupFieldsForChildException(newException, `errors[${i}]`, newExceptionId, exceptionId);
newExceptions = aggregateExceptionsFromError(
exceptionFromErrorImplementation,
parser,
limit,
childError,
key,
[newException, ...newExceptions],
newException,
newExceptionId,
);
}
});
}

const exception = exceptionFromErrorImplementation(parser, error[key]);
return aggregateExceptionsFromError(exceptionFromErrorImplementation, parser, limit, error[key], key, [
exception,
...stack,
]);
return newExceptions;
}

function applyExceptionGroupFieldsForParentException(exception: Exception, exceptionId: number): void {
// Don't know if this default makes sense. The protocol requires us to set these values so we pick *some* default.
exception.mechanism = exception.mechanism || { type: 'generic', handled: true };

exception.mechanism = {
...exception.mechanism,
is_exception_group: true,
exception_id: exceptionId,
};
}

function applyExceptionGroupFieldsForChildException(
exception: Exception,
source: string,
exceptionId: number,
parentId: number | undefined,
): void {
// Don't know if this default makes sense. The protocol requires us to set these values so we pick *some* default.
exception.mechanism = exception.mechanism || { type: 'generic', handled: true };

exception.mechanism = {
...exception.mechanism,
type: 'chained',
source,
exception_id: exceptionId,
parent_id: parentId,
};
}
186 changes: 182 additions & 4 deletions packages/utils/test/aggregate-errors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { applyAggregateErrorsToEvent, createStackParser } from '../src/index';

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

describe('applyAggregateErrorsToEvent()', () => {
Expand Down Expand Up @@ -46,28 +46,63 @@ describe('applyAggregateErrorsToEvent()', () => {
test('should recursively walk the original exception based on the `key` option and add them as exceptions to the event', () => {
const key = 'cause';
const originalException: ExtendedError = new Error('Root Error');
const event: Event = { exception: { values: [exceptionFromError(stackParser, originalException)] } };
originalException[key] = new Error('Nested Error 1');
originalException[key][key] = new Error('Nested Error 2');

const event: Event = { exception: { values: [exceptionFromError(stackParser, originalException)] } };
const eventHint: EventHint = { originalException };

applyAggregateErrorsToEvent(exceptionFromError, stackParser, key, 100, event, eventHint);
expect(event).toStrictEqual({
exception: {
values: [
{
value: 'Nested Error 2',
mechanism: {
exception_id: 2,
handled: true,
parent_id: 1,
source: 'cause',
type: 'chained',
},
},
{
value: 'Nested Error 1',
mechanism: {
exception_id: 1,
handled: true,
parent_id: 0,
is_exception_group: true,
source: 'cause',
type: 'chained',
},
},
{
value: 'Root Error',
mechanism: {
exception_id: 0,
handled: true,
is_exception_group: true,
type: 'instrument',
},
},
],
},
});
});

test('should not modify event if there are no attached errors', () => {
const originalException: ExtendedError = new Error('Some Error');

const event: Event = { exception: { values: [exceptionFromError(stackParser, originalException)] } };
const eventHint: EventHint = { originalException };

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

// no changes
expect(event).toStrictEqual({ exception: { values: [exceptionFromError(stackParser, originalException)] } });
});

test('should allow to limit number of attached errors', () => {
const key = 'cause';
const originalException: ExtendedError = new Error('Root Error');
Expand All @@ -89,9 +124,152 @@ describe('applyAggregateErrorsToEvent()', () => {
// Last exception in list should be the root exception
expect(event.exception?.values?.[event.exception?.values.length - 1]).toStrictEqual({
value: 'Root Error',
mechanism: {
exception_id: 0,
handled: true,
is_exception_group: true,
type: 'instrument',
},
});
});

test.todo('should recursively walk AggregateErrors and add them as exceptions to the event');
test.todo('should recursively walk mixed errors (Aggregate errors and based on `key`)');
test('should keep the original mechanism type for the root exception', () => {
const fakeAggregateError: ExtendedError = new Error('Root Error');
fakeAggregateError.errors = [new Error('Nested Error 1'), new Error('Nested Error 2')];

const event: Event = { exception: { values: [exceptionFromError(stackParser, fakeAggregateError)] } };
const eventHint: EventHint = { originalException: fakeAggregateError };

applyAggregateErrorsToEvent(exceptionFromError, stackParser, 'cause', 100, event, eventHint);
expect(event.exception?.values?.[event.exception.values.length - 1].mechanism?.type).toBe('instrument');
});

test('should recursively walk mixed errors (Aggregate errors and based on `key`)', () => {
const chainedError: ExtendedError = new Error('Nested Error 3');
chainedError.cause = new Error('Nested Error 4');
const fakeAggregateError2: ExtendedError = new Error('AggregateError2');
fakeAggregateError2.errors = [new Error('Nested Error 2'), chainedError];
const fakeAggregateError1: ExtendedError = new Error('AggregateError1');
fakeAggregateError1.errors = [new Error('Nested Error 1'), fakeAggregateError2];

const event: Event = { exception: { values: [exceptionFromError(stackParser, fakeAggregateError1)] } };
const eventHint: EventHint = { originalException: fakeAggregateError1 };

applyAggregateErrorsToEvent(exceptionFromError, stackParser, 'cause', 100, event, eventHint);
expect(event).toStrictEqual({
exception: {
values: [
{
mechanism: {
exception_id: 5,
handled: true,
parent_id: 4,
source: 'cause',
type: 'chained',
},
value: 'Nested Error 4',
},
{
mechanism: {
exception_id: 4,
handled: true,
is_exception_group: true,
parent_id: 2,
source: 'errors[1]',
type: 'chained',
},
value: 'Nested Error 3',
},
{
mechanism: {
exception_id: 3,
handled: true,
parent_id: 2,
source: 'errors[0]',
type: 'chained',
},
value: 'Nested Error 2',
},
{
mechanism: {
exception_id: 2,
handled: true,
is_exception_group: true,
parent_id: 0,
source: 'errors[1]',
type: 'chained',
},
value: 'AggregateError2',
},
{
mechanism: {
exception_id: 1,
handled: true,
parent_id: 0,
source: 'errors[0]',
type: 'chained',
},
value: 'Nested Error 1',
},
{
mechanism: {
exception_id: 0,
handled: true,
is_exception_group: true,
type: 'instrument',
},
value: 'AggregateError1',
},
],
},
});
});

test('should keep the original mechanism type for the root exception', () => {
const key = 'cause';
const originalException: ExtendedError = new Error('Root Error');
originalException[key] = new Error('Nested Error 1');
originalException[key][key] = new Error('Nested Error 2');

const event: Event = { exception: { values: [exceptionFromError(stackParser, originalException)] } };
const eventHint: EventHint = { originalException };

applyAggregateErrorsToEvent(exceptionFromError, stackParser, key, 100, event, eventHint);
expect(event).toStrictEqual({
exception: {
values: [
{
value: 'Nested Error 2',
mechanism: {
exception_id: 2,
handled: true,
parent_id: 1,
source: 'cause',
type: 'chained',
},
},
{
value: 'Nested Error 1',
mechanism: {
exception_id: 1,
handled: true,
parent_id: 0,
is_exception_group: true,
source: 'cause',
type: 'chained',
},
},
{
value: 'Root Error',
mechanism: {
exception_id: 0,
handled: true,
is_exception_group: true,
type: 'instrument',
},
},
],
},
});
});
});

0 comments on commit 63da54c

Please sign in to comment.