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(integrations): Capture error cause with captureErrorCause in ExtraErrorData integration #9914

Merged
merged 6 commits into from
Dec 22, 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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 28 additions & 4 deletions packages/integrations/src/extraerrordata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,29 @@ import { DEBUG_BUILD } from './debug-build';
const INTEGRATION_NAME = 'ExtraErrorData';

interface ExtraErrorDataOptions {
/**
* The object depth up to which to capture data on error objects.
*/
depth: number;

/**
* Whether to capture error causes.
*
* More information: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause
*/
captureErrorCause: boolean;
}

const extraErrorDataIntegration = ((options: Partial<ExtraErrorDataOptions> = {}) => {
const depth = options.depth || 3;

// TODO(v8): Flip the default for this option to true
const captureErrorCause = options.captureErrorCause || false;

return {
name: INTEGRATION_NAME,
processEvent(event, hint) {
return _enhanceEventWithErrorData(event, hint, depth);
return _enhanceEventWithErrorData(event, hint, depth, captureErrorCause);
},
};
}) satisfies IntegrationFn;
Expand All @@ -25,13 +38,18 @@ const extraErrorDataIntegration = ((options: Partial<ExtraErrorDataOptions> = {}
// eslint-disable-next-line deprecation/deprecation
export const ExtraErrorData = convertIntegrationFnToClass(INTEGRATION_NAME, extraErrorDataIntegration);

function _enhanceEventWithErrorData(event: Event, hint: EventHint = {}, depth: number): Event {
function _enhanceEventWithErrorData(
event: Event,
hint: EventHint = {},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
hint: EventHint = {},
hint: EventHint | undefined = {},

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional values should be at the end of the arguments list. I thought we had a rule for this?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parameter is not optional. It just takes undefined and falls back to {}. Optional parameters are marked with a ? like this hint?: EventHint. Additionally, your suggestion doesn't do anything because TS already knows to allow undefined as type for parameters with default.

depth: number,
captureErrorCause: boolean,
): Event {
if (!hint.originalException || !isError(hint.originalException)) {
return event;
}
const exceptionName = (hint.originalException as ExtendedError).name || hint.originalException.constructor.name;

const errorData = _extractErrorData(hint.originalException as ExtendedError);
const errorData = _extractErrorData(hint.originalException as ExtendedError, captureErrorCause);

if (errorData) {
const contexts: Contexts = {
Expand Down Expand Up @@ -59,7 +77,7 @@ function _enhanceEventWithErrorData(event: Event, hint: EventHint = {}, depth: n
/**
* Extract extra information from the Error object
*/
function _extractErrorData(error: ExtendedError): Record<string, unknown> | null {
function _extractErrorData(error: ExtendedError, captureErrorCause: boolean): Record<string, unknown> | null {
// We are trying to enhance already existing event, so no harm done if it won't succeed
try {
const nativeKeys = [
Expand All @@ -85,6 +103,12 @@ function _extractErrorData(error: ExtendedError): Record<string, unknown> | null
extraErrorInfo[key] = isError(value) ? value.toString() : value;
}

// Error.cause is a standard property that is non enumerable, we therefore need to access it separately.
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause
if (captureErrorCause && error.cause !== undefined) {
extraErrorInfo.cause = isError(error.cause) ? error.cause.toString() : error.cause;
}

// Check if someone attached `toJSON` method to grab even more properties (eg. axios is doing that)
if (typeof error.toJSON === 'function') {
const serializedError = error.toJSON() as Record<string, unknown>;
Expand Down
50 changes: 50 additions & 0 deletions packages/integrations/test/extraerrordata.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,4 +178,54 @@ describe('ExtraErrorData()', () => {
},
});
});

it('captures Error causes when captureErrorCause = true', () => {
// Error.cause is only available from node 16 upwards
const nodeMajorVersion = parseInt(process.versions.node.split('.')[0]);
if (nodeMajorVersion < 16) {
return;
}

const extraErrorDataWithCauseCapture = new ExtraErrorData({ captureErrorCause: true });

// @ts-expect-error The typing .d.ts library we have installed isn't aware of Error.cause yet
const error = new Error('foo', { cause: { woot: 'foo' } }) as ExtendedError;

const enhancedEvent = extraErrorDataWithCauseCapture.processEvent(event, {
originalException: error,
});

expect(enhancedEvent.contexts).toEqual({
Error: {
cause: {
woot: 'foo',
},
},
});
});

it("doesn't capture Error causes when captureErrorCause != true", () => {
// Error.cause is only available from node 16 upwards
const nodeMajorVersion = parseInt(process.versions.node.split('.')[0]);
if (nodeMajorVersion < 16) {
return;
}

const extraErrorDataWithoutCauseCapture = new ExtraErrorData();

// @ts-expect-error The typing .d.ts library we have installed isn't aware of Error.cause yet
const error = new Error('foo', { cause: { woot: 'foo' } }) as ExtendedError;

const enhancedEvent = extraErrorDataWithoutCauseCapture.processEvent(event, {
originalException: error,
});

expect(enhancedEvent.contexts).not.toEqual({
Error: {
cause: {
woot: 'foo',
},
},
});
});
});