Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Support AggregateErrors in LinkedErrors integration #8463

Merged
merged 2 commits into from
Jul 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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',
},
},
],
},
});
});
});