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

fix: error filtering of non-errors #1811

Merged
merged 7 commits into from
Jul 31, 2024
Merged
Show file tree
Hide file tree
Changes from 4 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
saikumarrs marked this conversation as resolved.
Show resolved Hide resolved
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
getBugsnagErrorEvent,
getErrorDeliveryPayload,
getConfigForPayloadCreation,
isAllowedToBeNotified,
} from '../../src/errorReporting/utils';

jest.mock('@rudderstack/analytics-js-common/utilities/uuId', () => ({
Expand Down Expand Up @@ -115,6 +116,14 @@ describe('Error Reporting utilities', () => {
expect(isRudderSDKError(event)).toBe(expectedValue);
},
);

it('should return false if error message contains value that is not allowed to be notified', () => {
const event = {
message: 'The request failed',
};

expect(isRudderSDKError(event)).toBe(false);
});
});

describe('getErrorContext', () => {
Expand Down Expand Up @@ -548,4 +557,21 @@ describe('Error Reporting utilities', () => {
});
});
});

describe('isAllowedToBeNotified', () => {
it('should return true for Error argument value', () => {
const result = isAllowedToBeNotified('dummy error');
expect(result).toBeTruthy();
});

it('should return false for Error argument value', () => {
const result = isAllowedToBeNotified('The request failed');
expect(result).toBeFalsy();
});

it('should return false for Error argument value', () => {
const result = isAllowedToBeNotified('unhandledException handler received a non-error');
expect(result).toBeFalsy();
});
});
});
27 changes: 11 additions & 16 deletions packages/analytics-js-plugins/src/errorReporting/event/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,14 +73,10 @@ const normaliseError = (
let error;
saikumarrs marked this conversation as resolved.
Show resolved Hide resolved
let internalFrames = 0;

const createAndLogInputError = (reason: string) => {
const logInputError = (reason: string) => {
const verb = component === 'error cause' ? 'was' : 'received';
if (logger) logger.warn(`${component} ${verb} a non-error: "${reason}"`);
const err = new Error(
`${component} ${verb} a non-error. See "${component}" tab for more detail.`,
);
err.name = 'InvalidError';
return err;
return undefined;
};

// In some cases:
Expand All @@ -95,8 +91,7 @@ const normaliseError = (
if (isError(maybeError)) {
error = maybeError;
} else {
error = createAndLogInputError(typeof maybeError);
internalFrames += 2;
error = logInputError(typeof maybeError);
}
} else {
switch (typeof maybeError) {
Expand All @@ -107,8 +102,7 @@ const normaliseError = (
internalFrames += 1;
break;
case 'function':
error = createAndLogInputError('function');
internalFrames += 2;
error = logInputError('function');
break;
case 'object':
if (maybeError !== null && isError(maybeError)) {
Expand All @@ -118,17 +112,15 @@ const normaliseError = (
error.name = maybeError.name || maybeError.errorClass;
internalFrames += 1;
} else {
error = createAndLogInputError(maybeError === null ? 'null' : 'unsupported object');
internalFrames += 2;
error = logInputError(maybeError === null ? 'null' : 'unsupported object');
}
break;
default:
error = createAndLogInputError('nothing');
internalFrames += 2;
error = logInputError('nothing');
}
}

if (!hasStack(error)) {
if (error && !hasStack(error)) {
// in IE10/11 a new Error() doesn't have a stacktrace until you throw it, so try that here
try {
throw error;
Expand All @@ -137,7 +129,7 @@ const normaliseError = (
error = e;
// if the error only got a stacktrace after we threw it here, we know it
// will only have one extra internal frame from this function, regardless
// of whether it went through createAndLogInputError() or not
// of whether it went through logInputError() or not
saikumarrs marked this conversation as resolved.
Show resolved Hide resolved
internalFrames = 1;
}
}
Expand Down Expand Up @@ -166,6 +158,9 @@ class ErrorFormat implements IErrorFormat {
component,
logger,
);
if (!error) {
return undefined;
}
let event;
try {
const stacktrace = getStacktrace(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ const ErrorReporting = (): ExtensionPlugin => ({
);

// filter errors
if (!isRudderSDKError(errorPayload.errors[0])) {
if (!errorPayload || !isRudderSDKError(errorPayload.errors[0])) {
return;
}

Expand Down
19 changes: 19 additions & 0 deletions packages/analytics-js-plugins/src/errorReporting/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import type {
import { generateUUID } from '@rudderstack/analytics-js-common/utilities/uuId';
import { METRICS_PAYLOAD_VERSION } from '@rudderstack/analytics-js-common/constants/metrics';
import { stringifyWithoutCircular } from '@rudderstack/analytics-js-common/utilities/json';
import { ERROR_MESSAGES_TO_BE_FILTERED } from '@rudderstack/analytics-js-common/constants/errors';
saikumarrs marked this conversation as resolved.
Show resolved Hide resolved
import {
APP_STATE_EXCLUDE_KEYS,
DEV_HOSTS,
Expand Down Expand Up @@ -142,8 +143,25 @@ const getBugsnagErrorEvent = (
],
});

/**
* A function to determine whether the error should be promoted to notify or not
* @param {Error} error
* @returns
*/
const isAllowedToBeNotified = (errorMessage: string) =>
!ERROR_MESSAGES_TO_BE_FILTERED.some(e => errorMessage.includes(e));

/**
* A function to determine if the error is from Rudder SDK
* @param {Error} event
* @returns
*/
const isRudderSDKError = (event: any) => {
const errorOrigin = event.stacktrace?.[0]?.file;
const errorMessage = event.message;
if (errorMessage && typeof errorMessage === 'string' && !isAllowedToBeNotified(errorMessage)) {
saikumarrs marked this conversation as resolved.
Show resolved Hide resolved
return false;
}

if (!errorOrigin || typeof errorOrigin !== 'string') {
return false;
Expand Down Expand Up @@ -188,4 +206,5 @@ export {
isRudderSDKError,
getErrorDeliveryPayload,
getErrorContext,
isAllowedToBeNotified,
};
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ describe('Config Manager Common Utilities', () => {

updateReportingState(mockSourceConfig, mockLogger);

// expect(state.reporting.isErrorReportingEnabled.value).toBe(true); // TODO: uncomment this line when error reporting is enabled
expect(state.reporting.isErrorReportingEnabled.value).toBe(true);
expect(state.reporting.isMetricsReportingEnabled.value).toBe(true);
expect(mockLogger.warn).not.toHaveBeenCalled();
});
Expand All @@ -133,7 +133,7 @@ describe('Config Manager Common Utilities', () => {

updateReportingState(mockSourceConfig, mockLogger);

// expect(state.reporting.isErrorReportingEnabled.value).toBe(true); // TODO: uncomment this line when error reporting is enabled
expect(state.reporting.isErrorReportingEnabled.value).toBe(true);
expect(state.reporting.isMetricsReportingEnabled.value).toBe(true);
expect(mockLogger.warn).not.toHaveBeenCalled();
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import {
processError,
getNormalizedErrorForUnhandledError,
isAllowedToBeNotified,
} from '../../../src/services/ErrorHandler/processError';

jest.mock('../../../src/components/utilities/event', () => {
Expand Down Expand Up @@ -95,7 +94,7 @@ describe('ErrorHandler - getNormalizedErrorForUnhandledError', () => {
expect(normalizedError).toBeUndefined();
});

it.skip('should return error instance for Event argument value with SDK script target', () => {
it('should return error instance for Event argument value with SDK script target', () => {
const event = new Event('dummyError');
const targetElement = document.createElement('script');
targetElement.dataset.loader = 'RS_JS_SDK';
Expand All @@ -116,30 +115,3 @@ describe('ErrorHandler - getNormalizedErrorForUnhandledError', () => {
);
});
});

describe('ErrorHandler - isAllowedToBeNotified', () => {
it('should return true for Error argument value', () => {
const result = isAllowedToBeNotified(new Error('dummy error'));
expect(result).toBeTruthy();
});

it('should return true for Error argument value', () => {
const result = isAllowedToBeNotified(new Error('The request failed'));
expect(result).toBeFalsy();
});

it('should return true for ErrorEvent argument value', () => {
const result = isAllowedToBeNotified(new ErrorEvent('dummy error'));
expect(result).toBeTruthy();
});

it('should return true for PromiseRejectionEvent argument value', () => {
const result = isAllowedToBeNotified(new PromiseRejectionEvent('dummy error'));
expect(result).toBeTruthy();
});

it('should return true for PromiseRejectionEvent argument value', () => {
const result = isAllowedToBeNotified(new PromiseRejectionEvent('The request failed'));
expect(result).toBeFalsy();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,8 @@ const getSDKUrl = (): string | undefined => {
* @param logger Logger instance
*/
const updateReportingState = (res: SourceConfigResponse): void => {
state.reporting.isErrorReportingEnabled.value = false;
// TODO: Enable this once the error reporting is tested properly
// state.reporting.isErrorReportingEnabled.value =
// isErrorReportingEnabled(res.source.config) && !isSDKRunningInChromeExtension();
state.reporting.isErrorReportingEnabled.value =
isErrorReportingEnabled(res.source.config) && !isSDKRunningInChromeExtension();
state.reporting.isMetricsReportingEnabled.value = isMetricsReportingEnabled(res.source.config);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,7 @@ import {
import { state } from '../../state';
import { defaultPluginEngine } from '../PluginEngine';
import { defaultLogger } from '../Logger';
import {
isAllowedToBeNotified,
getNormalizedErrorForUnhandledError,
processError,
} from './processError';
import { getNormalizedErrorForUnhandledError, processError } from './processError';

/**
* A service to handle errors
Expand Down Expand Up @@ -204,7 +200,7 @@ class ErrorHandler implements IErrorHandler {
* @param {Error} error Error instance from handled error
*/
notifyError(error: SDKError, errorState: ErrorState) {
if (this.pluginEngine && this.httpClient && isAllowedToBeNotified(error)) {
if (this.pluginEngine && this.httpClient) {
try {
this.pluginEngine.invokeSingle(
'errorReporting.notify',
Expand Down
58 changes: 21 additions & 37 deletions packages/analytics-js/src/services/ErrorHandler/processError.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { stringifyWithoutCircular } from '@rudderstack/analytics-js-common/utilities/json';
import { isString } from '@rudderstack/analytics-js-common/utilities/checks';
import type { ErrorTarget, SDKError } from '@rudderstack/analytics-js-common/types/ErrorHandler';
import { ERROR_MESSAGES_TO_BE_FILTERED } from '../../constants/errors';
import { LOAD_ORIGIN } from './constant';

/**
Expand Down Expand Up @@ -39,44 +38,29 @@
return error;
}
// TODO: remove this block once all device mode integrations start using the v3 script loader module (TS)
// if (error instanceof Event) {
// const eventTarget = error.target as ErrorTarget;
// // Discard all the non-script loading errors
// if (eventTarget && eventTarget.localName !== 'script') {
// return undefined;
// }
// // Discard script errors that are not originated at SDK or from native SDKs
// if (
// eventTarget?.dataset &&
// (eventTarget.dataset.loader !== LOAD_ORIGIN ||
// eventTarget.dataset.isnonnativesdk !== 'true')
// ) {
// return undefined;
// }
// const errorMessage = `Error in loading a third-party script from URL ${eventTarget?.src} with ID ${eventTarget?.id}.`;
// return Object.create(error, {
// message: { value: errorMessage },
// });
// }
return undefined;
if (error instanceof Event) {
const eventTarget = error.target as ErrorTarget;
// Discard all the non-script loading errors
if (eventTarget && eventTarget.localName !== 'script') {
return undefined;
}
// Discard script errors that are not originated at SDK or from native SDKs
if (
eventTarget?.dataset &&
(eventTarget.dataset.loader !== LOAD_ORIGIN ||
eventTarget.dataset.isnonnativesdk !== 'true')
) {
return undefined;
}
const errorMessage = `Error in loading a third-party script from URL ${eventTarget?.src} with ID ${eventTarget?.id}.`;
return Object.create(error, {
message: { value: errorMessage },
});
}
return error;

Check warning on line 60 in packages/analytics-js/src/services/ErrorHandler/processError.ts

View check run for this annotation

Codecov / codecov/patch

packages/analytics-js/src/services/ErrorHandler/processError.ts#L60

Added line #L60 was not covered by tests
} catch (e) {
return e;
}
};

/**
* A function to determine whether the error should be promoted to notify or not
* @param {Error} error
* @returns
*/
const isAllowedToBeNotified = (error: SDKError) => {
if ((error instanceof Error || error instanceof ErrorEvent) && error.message) {
return !ERROR_MESSAGES_TO_BE_FILTERED.some(e => error.message.includes(e));
}
if (error instanceof PromiseRejectionEvent && typeof error.reason === 'string') {
return !ERROR_MESSAGES_TO_BE_FILTERED.some(e => error.reason.includes(e));
}
return true;
};

export { processError, isAllowedToBeNotified, getNormalizedErrorForUnhandledError };
export { processError, getNormalizedErrorForUnhandledError };
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import type {
} from '@rudderstack/analytics-js-common/types/HttpClient';
import type { ILogger } from '@rudderstack/analytics-js-common/types/Logger';
import { getMutatedError } from '@rudderstack/analytics-js-common/utilities/errors';
import { FAILED_REQUEST_ERR_MSG_PREFIX } from '@rudderstack/analytics-js-common/constants/errors';
saikumarrs marked this conversation as resolved.
Show resolved Hide resolved
import { DEFAULT_XHR_TIMEOUT_MS } from '../../../constants/timeouts';
import { FAILED_REQUEST_ERR_MSG_PREFIX } from '../../../constants/errors';
import {
XHR_PAYLOAD_PREP_ERROR,
XHR_DELIVERY_ERROR,
Expand Down