diff --git a/packages/analytics-js-common/src/constants/errors.ts b/packages/analytics-js-common/src/constants/errors.ts new file mode 100644 index 0000000000..0336cc3e2f --- /dev/null +++ b/packages/analytics-js-common/src/constants/errors.ts @@ -0,0 +1,4 @@ +const FAILED_REQUEST_ERR_MSG_PREFIX = 'The request failed'; +const ERROR_MESSAGES_TO_BE_FILTERED = [FAILED_REQUEST_ERR_MSG_PREFIX]; + +export { FAILED_REQUEST_ERR_MSG_PREFIX, ERROR_MESSAGES_TO_BE_FILTERED }; diff --git a/packages/analytics-js-plugins/__tests__/errorReporting/utils.test.ts b/packages/analytics-js-plugins/__tests__/errorReporting/utils.test.ts index 8e9d3f89ba..f6555bed2a 100644 --- a/packages/analytics-js-plugins/__tests__/errorReporting/utils.test.ts +++ b/packages/analytics-js-plugins/__tests__/errorReporting/utils.test.ts @@ -12,6 +12,7 @@ import { getBugsnagErrorEvent, getErrorDeliveryPayload, getConfigForPayloadCreation, + isAllowedToBeNotified, } from '../../src/errorReporting/utils'; jest.mock('@rudderstack/analytics-js-common/utilities/uuId', () => ({ @@ -548,4 +549,21 @@ describe('Error Reporting utilities', () => { }); }); }); + + describe('isAllowedToBeNotified', () => { + it('should return true for Error argument value', () => { + const result = isAllowedToBeNotified({ message: 'dummy error' }); + expect(result).toBeTruthy(); + }); + + it('should return true for Error argument value', () => { + const result = isAllowedToBeNotified({ message: 'The request failed' }); + expect(result).toBeFalsy(); + }); + + it('should return true if message is not defined', () => { + const result = isAllowedToBeNotified({ name: 'dummy error' }); + expect(result).toBeTruthy(); + }); + }); }); diff --git a/packages/analytics-js-plugins/src/errorReporting/constants.ts b/packages/analytics-js-plugins/src/errorReporting/constants.ts index da3c9ebfa1..9765e672c6 100644 --- a/packages/analytics-js-plugins/src/errorReporting/constants.ts +++ b/packages/analytics-js-plugins/src/errorReporting/constants.ts @@ -22,6 +22,7 @@ const REQUEST_TIMEOUT_MS = 10 * 1000; // 10 seconds const NOTIFIER_NAME = 'RudderStack JavaScript SDK Error Notifier'; const SDK_GITHUB_URL = 'https://github.com/rudderlabs/rudder-sdk-js'; const SOURCE_NAME = 'js'; +const ERROR_REPORTING_PLUGIN = 'ErrorReportingPlugin'; export { SDK_FILE_NAME_PREFIXES, @@ -31,4 +32,5 @@ export { NOTIFIER_NAME, SDK_GITHUB_URL, SOURCE_NAME, + ERROR_REPORTING_PLUGIN, }; diff --git a/packages/analytics-js-plugins/src/errorReporting/event/event.ts b/packages/analytics-js-plugins/src/errorReporting/event/event.ts index 31cd6e00a3..97e97d16b1 100644 --- a/packages/analytics-js-plugins/src/errorReporting/event/event.ts +++ b/packages/analytics-js-plugins/src/errorReporting/event/event.ts @@ -2,8 +2,10 @@ import type { ErrorState } from '@rudderstack/analytics-js-common/types/ErrorHan import type { ILogger } from '@rudderstack/analytics-js-common/types/Logger'; import ErrorStackParser from 'error-stack-parser'; import type { Exception, Stackframe } from '@rudderstack/analytics-js-common/types/Metrics'; +import { stringifyWithoutCircular } from '@rudderstack/analytics-js-common/utilities/json'; import type { FrameType, IErrorFormat } from '../types'; import { hasStack, isError } from './utils'; +import { ERROR_REPORTING_PLUGIN } from '../constants'; const normaliseFunctionName = (name: string) => /^global code$/i.test(name) ? 'global code' : name; @@ -64,71 +66,24 @@ const hasNecessaryFields = (error: any) => (typeof error.name === 'string' || typeof error.errorClass === 'string') && (typeof error.message === 'string' || typeof error.errorMessage === 'string'); -const normaliseError = ( - maybeError: any, - tolerateNonErrors: boolean, - component: string, - logger?: ILogger, -) => { +const normaliseError = (maybeError: any, component: string, logger?: ILogger) => { let error; let internalFrames = 0; - const createAndLogInputError = (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; - }; - - // In some cases: - // - // - the promise rejection handler (both in the browser and node) - // - the node uncaughtException handler - // - // We are really limited in what we can do to get a stacktrace. So we use the - // tolerateNonErrors option to ensure that the resulting error communicates as - // such. - if (!tolerateNonErrors) { - if (isError(maybeError)) { - error = maybeError; - } else { - error = createAndLogInputError(typeof maybeError); - internalFrames += 2; - } + if (isError(maybeError)) { + error = maybeError; + } else if (typeof maybeError === 'object' && hasNecessaryFields(maybeError)) { + error = new Error(maybeError.message || maybeError.errorMessage); + error.name = maybeError.name || maybeError.errorClass; + internalFrames += 1; } else { - switch (typeof maybeError) { - case 'string': - case 'number': - case 'boolean': - error = new Error(String(maybeError)); - internalFrames += 1; - break; - case 'function': - error = createAndLogInputError('function'); - internalFrames += 2; - break; - case 'object': - if (maybeError !== null && isError(maybeError)) { - error = maybeError; - } else if (maybeError !== null && hasNecessaryFields(maybeError)) { - error = new Error(maybeError.message || maybeError.errorMessage); - error.name = maybeError.name || maybeError.errorClass; - internalFrames += 1; - } else { - error = createAndLogInputError(maybeError === null ? 'null' : 'unsupported object'); - internalFrames += 2; - } - break; - default: - error = createAndLogInputError('nothing'); - internalFrames += 2; - } + logger?.warn( + `${ERROR_REPORTING_PLUGIN}:: ${component} received a non-error: ${stringifyWithoutCircular(error)}`, + ); + error = undefined; } - 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; @@ -136,8 +91,7 @@ const normaliseError = ( if (hasStack(e)) { 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 + // will only have one extra internal frame from this function internalFrames = 1; } } @@ -160,12 +114,10 @@ class ErrorFormat implements IErrorFormat { errorFramesToSkip = 0, logger?: ILogger, ) { - const [error, internalFrames] = normaliseError( - maybeError, - tolerateNonErrors, - component, - logger, - ); + const [error, internalFrames] = normaliseError(maybeError, component, logger); + if (!error) { + return undefined; + } let event; try { const stacktrace = getStacktrace( diff --git a/packages/analytics-js-plugins/src/errorReporting/index.ts b/packages/analytics-js-plugins/src/errorReporting/index.ts index e4ebc2a33d..61e7baa584 100644 --- a/packages/analytics-js-plugins/src/errorReporting/index.ts +++ b/packages/analytics-js-plugins/src/errorReporting/index.ts @@ -19,6 +19,7 @@ import { isRudderSDKError, getBugsnagErrorEvent, getErrorDeliveryPayload, + isAllowedToBeNotified, } from './utils'; import { REQUEST_TIMEOUT_MS } from './constants'; import { ErrorFormat } from './event/event'; @@ -83,6 +84,10 @@ const ErrorReporting = (): ExtensionPlugin => ({ logger, ); + if (!errorPayload || !isAllowedToBeNotified(errorPayload.errors[0])) { + return; + } + // filter errors if (!isRudderSDKError(errorPayload.errors[0])) { return; diff --git a/packages/analytics-js-plugins/src/errorReporting/utils.ts b/packages/analytics-js-plugins/src/errorReporting/utils.ts index 67064228ce..ec2786e373 100644 --- a/packages/analytics-js-plugins/src/errorReporting/utils.ts +++ b/packages/analytics-js-plugins/src/errorReporting/utils.ts @@ -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'; import { APP_STATE_EXCLUDE_KEYS, DEV_HOSTS, @@ -142,6 +143,24 @@ const getBugsnagErrorEvent = ( ], }); +/** + * A function to determine whether the error should be promoted to notify or not + * @param {Error} error + * @returns + */ +const isAllowedToBeNotified = (event: any) => { + const errorMessage = event.message; + if (errorMessage && typeof errorMessage === 'string') { + return !ERROR_MESSAGES_TO_BE_FILTERED.some(e => errorMessage.includes(e)); + } + return true; +}; + +/** + * 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; @@ -188,4 +207,5 @@ export { isRudderSDKError, getErrorDeliveryPayload, getErrorContext, + isAllowedToBeNotified, }; diff --git a/packages/analytics-js/__tests__/components/configManager/commonUtil.test.ts b/packages/analytics-js/__tests__/components/configManager/commonUtil.test.ts index 4e36ca8496..077a3f5bb2 100644 --- a/packages/analytics-js/__tests__/components/configManager/commonUtil.test.ts +++ b/packages/analytics-js/__tests__/components/configManager/commonUtil.test.ts @@ -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(); }); @@ -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(); }); diff --git a/packages/analytics-js/__tests__/services/ErrorHandler/processError.test.ts b/packages/analytics-js/__tests__/services/ErrorHandler/processError.test.ts index af249ebdd4..302ec0fbe9 100644 --- a/packages/analytics-js/__tests__/services/ErrorHandler/processError.test.ts +++ b/packages/analytics-js/__tests__/services/ErrorHandler/processError.test.ts @@ -1,7 +1,6 @@ import { processError, getNormalizedErrorForUnhandledError, - isAllowedToBeNotified, } from '../../../src/services/ErrorHandler/processError'; jest.mock('../../../src/components/utilities/event', () => { @@ -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'; @@ -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(); - }); -}); diff --git a/packages/analytics-js/src/components/configManager/util/commonUtil.ts b/packages/analytics-js/src/components/configManager/util/commonUtil.ts index 2d91600692..ac7e15ad6e 100644 --- a/packages/analytics-js/src/components/configManager/util/commonUtil.ts +++ b/packages/analytics-js/src/components/configManager/util/commonUtil.ts @@ -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); }; diff --git a/packages/analytics-js/src/constants/errors.ts b/packages/analytics-js/src/constants/errors.ts deleted file mode 100644 index deabc8d64a..0000000000 --- a/packages/analytics-js/src/constants/errors.ts +++ /dev/null @@ -1,8 +0,0 @@ -const FAILED_REQUEST_ERR_MSG_PREFIX = 'The request failed'; -const UNHANDLEDEXCEPTION_FOR_NON_ERROR_OBJECT = 'unhandledException handler received a non-error'; -const ERROR_MESSAGES_TO_BE_FILTERED = [ - FAILED_REQUEST_ERR_MSG_PREFIX, - UNHANDLEDEXCEPTION_FOR_NON_ERROR_OBJECT, -]; - -export { FAILED_REQUEST_ERR_MSG_PREFIX, ERROR_MESSAGES_TO_BE_FILTERED }; diff --git a/packages/analytics-js/src/services/ErrorHandler/ErrorHandler.ts b/packages/analytics-js/src/services/ErrorHandler/ErrorHandler.ts index 110c8e7a45..405d91e5d8 100644 --- a/packages/analytics-js/src/services/ErrorHandler/ErrorHandler.ts +++ b/packages/analytics-js/src/services/ErrorHandler/ErrorHandler.ts @@ -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 @@ -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', diff --git a/packages/analytics-js/src/services/ErrorHandler/processError.ts b/packages/analytics-js/src/services/ErrorHandler/processError.ts index 8b329763ce..bd5588bffd 100644 --- a/packages/analytics-js/src/services/ErrorHandler/processError.ts +++ b/packages/analytics-js/src/services/ErrorHandler/processError.ts @@ -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'; /** @@ -39,44 +38,29 @@ const getNormalizedErrorForUnhandledError = (error: SDKError): SDKError | undefi 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; } 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 }; diff --git a/packages/analytics-js/src/services/HttpClient/xhr/xhrRequestHandler.ts b/packages/analytics-js/src/services/HttpClient/xhr/xhrRequestHandler.ts index d201af0c6d..d6aee3e4c9 100644 --- a/packages/analytics-js/src/services/HttpClient/xhr/xhrRequestHandler.ts +++ b/packages/analytics-js/src/services/HttpClient/xhr/xhrRequestHandler.ts @@ -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'; 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,