From 02d2f5dd9b05759a9cff82fc2222ce2594062f8a Mon Sep 17 00:00:00 2001 From: Sai Kumar Battinoju Date: Tue, 31 Dec 2024 19:54:58 +0530 Subject: [PATCH 01/53] feat: move error reporting functionality to the core module --- .../__mocks__/ErrorHandler.ts | 9 +- .../__mocks__/HttpClient.ts | 1 + .../src/services/ExternalSrcLoader/types.ts | 13 +- .../src/types/ErrorHandler.ts | 17 +- .../src/types/HttpClient.ts | 1 + .../analytics-js-common/src/types/Metrics.ts | 22 +- .../src/types/PluginsManager.ts | 2 - .../analytics-js-common/src/types/Source.ts | 1 + .../analytics-js-plugins/__mocks__/state.ts | 1 - .../deviceModeTransformation/index.test.ts | 2 +- .../__tests__/xhrQueue/index.test.ts | 2 +- .../analytics-js-plugins/rollup.config.mjs | 2 - .../src/bugsnag/constants.ts | 53 -- .../analytics-js-plugins/src/bugsnag/index.ts | 63 -- .../src/bugsnag/logMessages.ts | 23 - .../analytics-js-plugins/src/bugsnag/utils.ts | 217 ------- .../src/errorReporting/event/event.ts | 106 --- .../src/errorReporting/event/utils.ts | 18 - .../src/errorReporting/index.ts | 134 ---- .../src/errorReporting/logMessages.ts | 3 - .../src/errorReporting/types.ts | 12 - packages/analytics-js-plugins/src/index.ts | 2 - .../src/shared-chunks/common.ts | 1 - packages/analytics-js/.size-limit.mjs | 8 +- .../__mocks__/remotePlugins/Bugsnag.ts | 10 - .../__mocks__/remotePlugins/ErrorReporting.ts | 10 - .../services/ErrorHandler/utils.test.ts | 609 ++++++++++++++++++ packages/analytics-js/rollup.config.mjs | 9 +- .../analytics-js/src/app/RudderAnalytics.ts | 2 - .../CapabilitiesManager.ts | 4 +- .../detection/adBlockers.ts | 5 +- .../components/capabilitiesManager/types.ts | 2 +- .../components/configManager/ConfigManager.ts | 9 +- .../src/components/core/Analytics.ts | 3 +- .../components/eventManager/EventManager.ts | 4 +- .../eventRepository/EventRepository.ts | 8 +- .../pluginsManager/PluginsManager.ts | 5 - .../bundledBuildPluginImports.ts | 4 - .../pluginsManager/defaultPluginsList.ts | 2 - .../federatedModulesBuildPluginImports.ts | 4 - .../components/pluginsManager/pluginNames.ts | 4 +- .../analytics-js/src/constants/logMessages.ts | 21 +- .../src/services/ErrorHandler/ErrorHandler.ts | 217 ++----- .../src/services/ErrorHandler/constant.ts | 36 +- .../src/services/ErrorHandler}/constants.ts | 9 +- .../services/ErrorHandler}/event/LICENSE.txt | 0 .../src/services/ErrorHandler/event/event.ts | 101 +++ .../src/services/ErrorHandler/event/types.ts | 6 + .../src/services/ErrorHandler/processError.ts | 66 -- .../src/services/ErrorHandler}/utils.ts | 162 ++--- .../src/services/HttpClient/HttpClient.ts | 12 +- .../src/types/remote-plugins.d.ts | 2 - 52 files changed, 960 insertions(+), 1079 deletions(-) rename packages/{analytics-js-plugins => analytics-js-common}/__mocks__/HttpClient.ts (96%) delete mode 100644 packages/analytics-js-plugins/src/bugsnag/constants.ts delete mode 100644 packages/analytics-js-plugins/src/bugsnag/index.ts delete mode 100644 packages/analytics-js-plugins/src/bugsnag/logMessages.ts delete mode 100644 packages/analytics-js-plugins/src/bugsnag/utils.ts delete mode 100644 packages/analytics-js-plugins/src/errorReporting/event/event.ts delete mode 100644 packages/analytics-js-plugins/src/errorReporting/event/utils.ts delete mode 100644 packages/analytics-js-plugins/src/errorReporting/index.ts delete mode 100644 packages/analytics-js-plugins/src/errorReporting/logMessages.ts delete mode 100644 packages/analytics-js-plugins/src/errorReporting/types.ts delete mode 100644 packages/analytics-js/__mocks__/remotePlugins/Bugsnag.ts delete mode 100644 packages/analytics-js/__mocks__/remotePlugins/ErrorReporting.ts create mode 100644 packages/analytics-js/__tests__/services/ErrorHandler/utils.test.ts rename packages/{analytics-js-plugins/src/errorReporting => analytics-js/src/services/ErrorHandler}/constants.ts (79%) rename packages/{analytics-js-plugins/src/errorReporting => analytics-js/src/services/ErrorHandler}/event/LICENSE.txt (100%) create mode 100644 packages/analytics-js/src/services/ErrorHandler/event/event.ts create mode 100644 packages/analytics-js/src/services/ErrorHandler/event/types.ts delete mode 100644 packages/analytics-js/src/services/ErrorHandler/processError.ts rename packages/{analytics-js-plugins/src/errorReporting => analytics-js/src/services/ErrorHandler}/utils.ts (50%) diff --git a/packages/analytics-js-common/__mocks__/ErrorHandler.ts b/packages/analytics-js-common/__mocks__/ErrorHandler.ts index 4f5f91ac6..a57ce3c08 100644 --- a/packages/analytics-js-common/__mocks__/ErrorHandler.ts +++ b/packages/analytics-js-common/__mocks__/ErrorHandler.ts @@ -1,14 +1,11 @@ -import type { IErrorHandler, PreLoadErrorData } from '../src/types/ErrorHandler'; -import { BufferQueue } from './BufferQueue'; +import type { IErrorHandler } from '../src/types/ErrorHandler'; +import { defaultHttpClient } from './HttpClient'; // Mock all the methods of the ErrorHandler class class ErrorHandler implements IErrorHandler { onError = jest.fn(); leaveBreadcrumb = jest.fn(); - notifyError = jest.fn(); - init = jest.fn(); - attachErrorListeners = jest.fn(); - errorBuffer = new BufferQueue(); + httpClient = defaultHttpClient; } const defaultErrorHandler = new ErrorHandler(); diff --git a/packages/analytics-js-plugins/__mocks__/HttpClient.ts b/packages/analytics-js-common/__mocks__/HttpClient.ts similarity index 96% rename from packages/analytics-js-plugins/__mocks__/HttpClient.ts rename to packages/analytics-js-common/__mocks__/HttpClient.ts index 46a62d81f..c78322ea2 100644 --- a/packages/analytics-js-plugins/__mocks__/HttpClient.ts +++ b/packages/analytics-js-common/__mocks__/HttpClient.ts @@ -10,6 +10,7 @@ class HttpClient implements IHttpClient { getAsyncData = jest.fn(); setAuthHeader = jest.fn(); resetAuthHeader = jest.fn(); + init = jest.fn(); } const defaultHttpClient = new HttpClient(); diff --git a/packages/analytics-js-common/src/services/ExternalSrcLoader/types.ts b/packages/analytics-js-common/src/services/ExternalSrcLoader/types.ts index 11975ebaa..31a5ecf29 100644 --- a/packages/analytics-js-common/src/services/ExternalSrcLoader/types.ts +++ b/packages/analytics-js-common/src/services/ExternalSrcLoader/types.ts @@ -1,4 +1,4 @@ -import type { ErrorState } from '../../types/ErrorHandler'; +import type { ErrorState, IErrorHandler } from '../../types/ErrorHandler'; import type { ILogger } from '../../types/Logger'; export interface IExternalSourceLoadConfig { @@ -11,16 +11,7 @@ export interface IExternalSourceLoadConfig { } export interface IExternalSrcLoader { - errorHandler?: { - onError( - error: unknown, - context?: string, - customMessage?: string, - shouldAlwaysThrow?: boolean, - ): void; - leaveBreadcrumb(breadcrumb: string): void; - notifyError(error: Error, errorState: ErrorState): void; - }; + errorHandler?: IErrorHandler; logger?: ILogger; timeout: number; loadJSFile(config: IExternalSourceLoadConfig): void; diff --git a/packages/analytics-js-common/src/types/ErrorHandler.ts b/packages/analytics-js-common/src/types/ErrorHandler.ts index 6b8e3b1cd..e2953a006 100644 --- a/packages/analytics-js-common/src/types/ErrorHandler.ts +++ b/packages/analytics-js-common/src/types/ErrorHandler.ts @@ -1,26 +1,13 @@ -import type { IPluginEngine } from './PluginEngine'; import type { ILogger } from './Logger'; -import type { BufferQueue } from '../services/BufferQueue/BufferQueue'; import type { IHttpClient } from './HttpClient'; -import type { IExternalSrcLoader } from '../services/ExternalSrcLoader/types'; export type SDKError = unknown | Error | ErrorEvent | Event | PromiseRejectionEvent; export interface IErrorHandler { + httpClient: IHttpClient; logger?: ILogger; - pluginEngine?: IPluginEngine; - errorBuffer: BufferQueue; - init(httpClient: IHttpClient, externalSrcLoader: IExternalSrcLoader): void; - onError( - error: SDKError, - context?: string, - customMessage?: string, - shouldAlwaysThrow?: boolean, - errorType?: string, - ): void; + onError(error: SDKError, context?: string, customMessage?: string, errorType?: string): void; leaveBreadcrumb(breadcrumb: string): void; - notifyError(error: Error, errorState: ErrorState): void; - attachErrorListeners(): void; } export type ErrorState = { diff --git a/packages/analytics-js-common/src/types/HttpClient.ts b/packages/analytics-js-common/src/types/HttpClient.ts index 43f8d68c9..f528e8501 100644 --- a/packages/analytics-js-common/src/types/HttpClient.ts +++ b/packages/analytics-js-common/src/types/HttpClient.ts @@ -65,4 +65,5 @@ export interface IHttpClient { getAsyncData(config: IAsyncRequestConfig): void; setAuthHeader(value: string, noBto?: boolean): void; resetAuthHeader(): void; + init(errorHandler: IErrorHandler): void; } diff --git a/packages/analytics-js-common/src/types/Metrics.ts b/packages/analytics-js-common/src/types/Metrics.ts index f54ca78bb..2fda29cf5 100644 --- a/packages/analytics-js-common/src/types/Metrics.ts +++ b/packages/analytics-js-common/src/types/Metrics.ts @@ -13,16 +13,16 @@ export type MetricServicePayload = { }; export type ErrorEventPayload = { + payloadVersion: string; notifier: { name: string; version: string; url: string; }; - events: ErrorEventType[]; + events: ErrorEvent[]; }; -export type ErrorEventType = { - payloadVersion: string; +export type ErrorEvent = { exceptions: Exception[]; severity: string; unhandled: boolean; @@ -30,6 +30,7 @@ export type ErrorEventType = { app: { version: string; releaseStage: string; + type: string; }; device: { locale?: string; @@ -41,12 +42,12 @@ export type ErrorEventType = { clientIp: string; }; breadcrumbs: Breadcrumb[] | []; - context: string; metaData: { [index: string]: any; }; user: { id: string; + name: string; }; }; @@ -54,12 +55,6 @@ export type GeneratedEventType = { errors: Exception[]; }; -export interface Exception { - message: string; - errorClass: string; - type: string; - stacktrace: Stackframe[]; -} export interface Stackframe { file: string; method?: string; @@ -68,3 +63,10 @@ export interface Stackframe { code?: Record; inProject?: boolean; } + +export interface Exception { + message: string; + errorClass: string; + type: string; + stacktrace: Stackframe[]; +} diff --git a/packages/analytics-js-common/src/types/PluginsManager.ts b/packages/analytics-js-common/src/types/PluginsManager.ts index 5792131d7..7739d58a5 100644 --- a/packages/analytics-js-common/src/types/PluginsManager.ts +++ b/packages/analytics-js-common/src/types/PluginsManager.ts @@ -13,11 +13,9 @@ export interface IPluginsManager { export type PluginName = | 'BeaconQueue' - | 'Bugsnag' | 'CustomConsentManager' | 'DeviceModeDestinations' | 'DeviceModeTransformation' - | 'ErrorReporting' | 'ExternalAnonymousId' | 'GoogleLinker' | 'IubendaConsentManager' diff --git a/packages/analytics-js-common/src/types/Source.ts b/packages/analytics-js-common/src/types/Source.ts index ec5a8e698..da8a64f34 100644 --- a/packages/analytics-js-common/src/types/Source.ts +++ b/packages/analytics-js-common/src/types/Source.ts @@ -15,5 +15,6 @@ export type SourceConfig = { export type Source = { id: string; config?: SourceConfig; + name: string; workspaceId: string; }; diff --git a/packages/analytics-js-plugins/__mocks__/state.ts b/packages/analytics-js-plugins/__mocks__/state.ts index a53a58ebb..05869b933 100644 --- a/packages/analytics-js-plugins/__mocks__/state.ts +++ b/packages/analytics-js-plugins/__mocks__/state.ts @@ -138,7 +138,6 @@ const defaultStateValues: ApplicationState = { reporting: { isErrorReportingEnabled: signal(false), isMetricsReportingEnabled: signal(false), - isErrorReportingPluginLoaded: signal(false), breadcrumbs: signal([]), }, session: { diff --git a/packages/analytics-js-plugins/__tests__/deviceModeTransformation/index.test.ts b/packages/analytics-js-plugins/__tests__/deviceModeTransformation/index.test.ts index 80b8c43a2..fce91955a 100644 --- a/packages/analytics-js-plugins/__tests__/deviceModeTransformation/index.test.ts +++ b/packages/analytics-js-plugins/__tests__/deviceModeTransformation/index.test.ts @@ -5,6 +5,7 @@ import { defaultErrorHandler } from '@rudderstack/analytics-js-common/__mocks__/ import { defaultLogger } from '@rudderstack/analytics-js-common/__mocks__/Logger'; import { defaultStoreManager } from '@rudderstack/analytics-js-common/__mocks__/StoreManager'; import type { ExtensionPoint } from '@rudderstack/analytics-js-common/types/PluginEngine'; +import { defaultHttpClient } from '@rudderstack/analytics-js-common/__mocks__/HttpClient'; import * as utils from '../../src/deviceModeTransformation/utilities'; import { DeviceModeTransformation } from '../../src/deviceModeTransformation'; import { @@ -15,7 +16,6 @@ import { } from '../../__fixtures__/fixtures'; import { server } from '../../__fixtures__/msw.server'; import { resetState, state } from '../../__mocks__/state'; -import { defaultHttpClient } from '../../__mocks__/HttpClient'; import { defaultPluginsManager } from '../../__mocks__/PluginsManager'; import type { RetryQueue } from '../../src/utilities/retryQueue/RetryQueue'; import type { QueueItem, QueueItemData } from '../../src/types/plugins'; diff --git a/packages/analytics-js-plugins/__tests__/xhrQueue/index.test.ts b/packages/analytics-js-plugins/__tests__/xhrQueue/index.test.ts index 2bfdf81e5..c48fb5107 100644 --- a/packages/analytics-js-plugins/__tests__/xhrQueue/index.test.ts +++ b/packages/analytics-js-plugins/__tests__/xhrQueue/index.test.ts @@ -6,12 +6,12 @@ import type { RudderEvent } from '@rudderstack/analytics-js-common/types/Event'; import { defaultLogger } from '@rudderstack/analytics-js-common/__mocks__/Logger'; import { defaultErrorHandler } from '@rudderstack/analytics-js-common/__mocks__/ErrorHandler'; import type { ExtensionPoint } from '@rudderstack/analytics-js-common/types/PluginEngine'; +import { defaultHttpClient } from '@rudderstack/analytics-js-common/__mocks__/HttpClient'; import type { RetryQueue } from '../../src/utilities/retryQueue/RetryQueue'; import type { QueueItem, QueueItemData } from '../../src/types/plugins'; import { resetState, state } from '../../__mocks__/state'; import { XhrQueue } from '../../src/xhrQueue'; import { Schedule } from '../../src/utilities/retryQueue/Schedule'; -import { defaultHttpClient } from '../../__mocks__/HttpClient'; jest.mock('@rudderstack/analytics-js-common/utilities/timestamp', () => ({ ...jest.requireActual('@rudderstack/analytics-js-common/utilities/timestamp'), diff --git a/packages/analytics-js-plugins/rollup.config.mjs b/packages/analytics-js-plugins/rollup.config.mjs index de66d8555..4f32c29e8 100644 --- a/packages/analytics-js-plugins/rollup.config.mjs +++ b/packages/analytics-js-plugins/rollup.config.mjs @@ -37,11 +37,9 @@ const isNpmPackageBuild = moduleType === 'npm'; const isCDNPackageBuild = moduleType === 'cdn'; const pluginsMap = { './BeaconQueue': './src/beaconQueue/index.ts', - './Bugsnag': './src/bugsnag/index.ts', './CustomConsentManager': './src/customConsentManager/index.ts', './DeviceModeDestinations': './src/deviceModeDestinations/index.ts', './DeviceModeTransformation': './src/deviceModeTransformation/index.ts', - './ErrorReporting': './src/errorReporting/index.ts', './ExternalAnonymousId': './src/externalAnonymousId/index.ts', './GoogleLinker': './src/googleLinker/index.ts', './IubendaConsentManager': './src/iubendaConsentManager/index.ts', diff --git a/packages/analytics-js-plugins/src/bugsnag/constants.ts b/packages/analytics-js-plugins/src/bugsnag/constants.ts deleted file mode 100644 index 5b45e385e..000000000 --- a/packages/analytics-js-plugins/src/bugsnag/constants.ts +++ /dev/null @@ -1,53 +0,0 @@ -const BUGSNAG_LIB_INSTANCE_GLOBAL_KEY_NAME = 'bugsnag'; // For version 6 and below -const BUGSNAG_LIB_V7_INSTANCE_GLOBAL_KEY_NAME = 'Bugsnag'; -const GLOBAL_LIBRARY_OBJECT_NAMES = [ - BUGSNAG_LIB_V7_INSTANCE_GLOBAL_KEY_NAME, - BUGSNAG_LIB_INSTANCE_GLOBAL_KEY_NAME, -]; -const BUGSNAG_CDN_URL = '__RS_BUGSNAG_SDK_URL__'; -const ERROR_REPORT_PROVIDER_NAME_BUGSNAG = 'rs-bugsnag'; -// This API key token is parsed in the CI pipeline -const API_KEY = '__RS_BUGSNAG_API_KEY__'; -const BUGSNAG_VALID_MAJOR_VERSION = '6'; -const SDK_LOAD_POLL_INTERVAL_MS = 100; // ms -const MAX_WAIT_FOR_SDK_LOAD_MS = 100 * SDK_LOAD_POLL_INTERVAL_MS; // ms - -// Errors from the below scripts are NOT allowed to reach Bugsnag -const SDK_FILE_NAME_PREFIXES = (): string[] => [ - 'rsa', // Prefix for all the SDK scripts including plugins and module federated chunks -]; - -const DEV_HOSTS = ['www.test-host.com', 'localhost', '127.0.0.1', '[::1]']; - -// List of keys to exclude from the metadata -// Potential PII or sensitive data -const APP_STATE_EXCLUDE_KEYS = [ - 'userId', - 'userTraits', - 'groupId', - 'groupTraits', - 'anonymousId', - 'config', - 'instance', // destination instance objects - 'eventBuffer', // pre-load event buffer (may contain PII) - 'traits', - 'authToken', -]; - -const BUGSNAG_PLUGIN = 'BugsnagPlugin'; - -export { - BUGSNAG_LIB_INSTANCE_GLOBAL_KEY_NAME, - BUGSNAG_LIB_V7_INSTANCE_GLOBAL_KEY_NAME, - GLOBAL_LIBRARY_OBJECT_NAMES, - BUGSNAG_CDN_URL, - ERROR_REPORT_PROVIDER_NAME_BUGSNAG, - API_KEY, - BUGSNAG_VALID_MAJOR_VERSION, - MAX_WAIT_FOR_SDK_LOAD_MS, - SDK_FILE_NAME_PREFIXES, - SDK_LOAD_POLL_INTERVAL_MS, - DEV_HOSTS, - APP_STATE_EXCLUDE_KEYS, - BUGSNAG_PLUGIN, -}; diff --git a/packages/analytics-js-plugins/src/bugsnag/index.ts b/packages/analytics-js-plugins/src/bugsnag/index.ts deleted file mode 100644 index 337867cc0..000000000 --- a/packages/analytics-js-plugins/src/bugsnag/index.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -/* eslint-disable no-param-reassign */ -import type { ApplicationState } from '@rudderstack/analytics-js-common/types/ApplicationState'; -import type { ILogger } from '@rudderstack/analytics-js-common/types/Logger'; -import type { IExternalSrcLoader } from '@rudderstack/analytics-js-common/services/ExternalSrcLoader/types'; -import type { ExtensionPlugin } from '@rudderstack/analytics-js-common/types/PluginEngine'; -import type { PluginName } from '@rudderstack/analytics-js-common/types/PluginsManager'; -import type { BugsnagLib } from '../types/plugins'; -import { BUGSNAG_API_KEY_VALIDATION_ERROR, BUGSNAG_SDK_URL_ERROR } from './logMessages'; -import { API_KEY } from './constants'; -import { initBugsnagClient, loadBugsnagSDK, isApiKeyValid } from './utils'; - -const pluginName: PluginName = 'Bugsnag'; - -const Bugsnag = (): ExtensionPlugin => ({ - name: pluginName, - deps: [], - initialize: (state: ApplicationState) => { - state.plugins.loadedPlugins.value = [...state.plugins.loadedPlugins.value, pluginName]; - }, - errorReportingProvider: { - init: ( - state: ApplicationState, - externalSrcLoader: IExternalSrcLoader, - logger?: ILogger, - ): Promise => - new Promise((resolve, reject) => { - // If API key token is not parsed or invalid, don't proceed to initialize the client - if (!isApiKeyValid(API_KEY)) { - reject(new Error(BUGSNAG_API_KEY_VALIDATION_ERROR(API_KEY))); - return; - } - - // If SDK URL is empty, don't proceed to initialize the client - // eslint-disable-next-line no-constant-condition - // @ts-expect-error we're dynamically filling this value during build - // eslint-disable-next-line no-constant-condition - if (!'__RS_BUGSNAG_SDK_URL__') { - reject(new Error(BUGSNAG_SDK_URL_ERROR)); - return; - } - - loadBugsnagSDK(externalSrcLoader, logger); - - initBugsnagClient(state, resolve, reject, logger); - }), - notify: ( - client: BugsnagLib.Client, - error: Error, - state: ApplicationState, - logger?: ILogger, - ): void => { - client.notify(error); - }, - breadcrumb: (client: BugsnagLib.Client, message: string, logger?: ILogger): void => { - client?.leaveBreadcrumb(message); - }, - }, -}); - -export { Bugsnag }; - -export default Bugsnag; diff --git a/packages/analytics-js-plugins/src/bugsnag/logMessages.ts b/packages/analytics-js-plugins/src/bugsnag/logMessages.ts deleted file mode 100644 index 5b3825843..000000000 --- a/packages/analytics-js-plugins/src/bugsnag/logMessages.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { LOG_CONTEXT_SEPARATOR } from '../shared-chunks/common'; - -const BUGSNAG_API_KEY_VALIDATION_ERROR = (apiKey: string): string => - `The Bugsnag API key (${apiKey}) is invalid or not provided.`; - -const BUGSNAG_SDK_LOAD_TIMEOUT_ERROR = (timeout: number): string => - `A timeout ${timeout} ms occurred while trying to load the Bugsnag SDK.`; - -const BUGSNAG_SDK_LOAD_ERROR = (context: string): string => - `${context}${LOG_CONTEXT_SEPARATOR}Failed to load the Bugsnag SDK.`; - -const BUGSNAG_SDK_URL_ERROR = 'The Bugsnag SDK URL is invalid. Failed to load the Bugsnag SDK.'; - -const FAILED_TO_FILTER_ERROR = (context: string): string => - `${context}${LOG_CONTEXT_SEPARATOR}Failed to filter the error.`; - -export { - BUGSNAG_API_KEY_VALIDATION_ERROR, - BUGSNAG_SDK_LOAD_TIMEOUT_ERROR, - BUGSNAG_SDK_LOAD_ERROR, - BUGSNAG_SDK_URL_ERROR, - FAILED_TO_FILTER_ERROR, -}; diff --git a/packages/analytics-js-plugins/src/bugsnag/utils.ts b/packages/analytics-js-plugins/src/bugsnag/utils.ts deleted file mode 100644 index e1c159dca..000000000 --- a/packages/analytics-js-plugins/src/bugsnag/utils.ts +++ /dev/null @@ -1,217 +0,0 @@ -import type { ApplicationState } from '@rudderstack/analytics-js-common/types/ApplicationState'; -import type { IExternalSrcLoader } from '@rudderstack/analytics-js-common/services/ExternalSrcLoader/types'; -import type { ILogger } from '@rudderstack/analytics-js-common/types/Logger'; -import type { BugsnagLib } from '../types/plugins'; -import { - BUGSNAG_SDK_LOAD_ERROR, - BUGSNAG_SDK_LOAD_TIMEOUT_ERROR, - FAILED_TO_FILTER_ERROR, -} from './logMessages'; -import { - API_KEY, - APP_STATE_EXCLUDE_KEYS, - BUGSNAG_CDN_URL, - BUGSNAG_LIB_INSTANCE_GLOBAL_KEY_NAME, - BUGSNAG_PLUGIN, - BUGSNAG_VALID_MAJOR_VERSION, - DEV_HOSTS, - ERROR_REPORT_PROVIDER_NAME_BUGSNAG, - GLOBAL_LIBRARY_OBJECT_NAMES, - MAX_WAIT_FOR_SDK_LOAD_MS, - SDK_FILE_NAME_PREFIXES, - SDK_LOAD_POLL_INTERVAL_MS, -} from './constants'; -import { CDN_INT_DIR, stringifyWithoutCircular } from '../shared-chunks/common'; - -const isValidVersion = (globalLibInstance: any) => { - // For version 7 - // eslint-disable-next-line no-underscore-dangle - let version = globalLibInstance?._client?._notifier?.version; - - // For versions older than 7 - if (!version) { - const tempInstance = globalLibInstance({ - apiKey: API_KEY, - releaseStage: 'version-test', - // eslint-disable-next-line func-names, object-shorthand - beforeSend: function () { - return false; - }, - }); - version = tempInstance.notifier?.version; - } - - return version && version.charAt(0) === BUGSNAG_VALID_MAJOR_VERSION; -}; - -const isRudderSDKError = (event: BugsnagLib.Report) => { - const errorOrigin = event.stacktrace?.[0]?.file; - - if (!errorOrigin || typeof errorOrigin !== 'string') { - return false; - } - - // Prefix folder for all the destination SDK scripts - const isDestinationIntegrationBundle = errorOrigin.includes(CDN_INT_DIR); - const srcFileName = errorOrigin.substring(errorOrigin.lastIndexOf('/') + 1); - - return ( - isDestinationIntegrationBundle || - SDK_FILE_NAME_PREFIXES().some( - prefix => srcFileName.startsWith(prefix) && srcFileName.endsWith('.js'), - ) - ); -}; - -const getAppStateForMetadata = (state: ApplicationState): Record | undefined => { - const stateStr = stringifyWithoutCircular(state, false, APP_STATE_EXCLUDE_KEYS); - return stateStr !== null ? JSON.parse(stateStr) : undefined; -}; - -const enhanceErrorEventMutator = (state: ApplicationState, event: BugsnagLib.Report): void => { - event.updateMetaData('source', { - snippetVersion: (globalThis as typeof window).RudderSnippetVersion, - }); - event.updateMetaData('state', getAppStateForMetadata(state) ?? {}); - - const { errorMessage } = event; - // eslint-disable-next-line no-param-reassign - event.context = errorMessage; - - // Hack for easily grouping the script load errors - // on the dashboard - if (errorMessage.includes('error in script loading')) { - // eslint-disable-next-line no-param-reassign - event.context = 'Script load failures'; - } - - // eslint-disable-next-line no-param-reassign - event.severity = 'error'; -}; - -const onError = - (state: ApplicationState, logger?: ILogger): BugsnagLib.BeforeSend => - (event: BugsnagLib.Report): boolean => { - try { - // Discard the event if it's not originated at the SDK - if (!isRudderSDKError(event)) { - return false; - } - - enhanceErrorEventMutator(state, event); - - return true; - } catch { - logger?.error(FAILED_TO_FILTER_ERROR(BUGSNAG_PLUGIN)); - // Drop the error event if it couldn't be filtered as - // it is most likely a non-SDK error - return false; - } - }; - -const getReleaseStage = () => { - const host = globalThis.location.hostname; - return host && DEV_HOSTS.includes(host) ? 'development' : '__RS_BUGSNAG_RELEASE_STAGE__'; -}; - -const getGlobalBugsnagLibInstance = () => (globalThis as any)[BUGSNAG_LIB_INSTANCE_GLOBAL_KEY_NAME]; - -const getNewClient = (state: ApplicationState, logger?: ILogger): BugsnagLib.Client => { - const globalBugsnagLibInstance = getGlobalBugsnagLibInstance(); - - const clientConfig: BugsnagLib.IConfig = { - apiKey: API_KEY, - appVersion: state.context.app.value.version, - metaData: { - SDK: { - name: 'JS', - installType: state.context.app.value.installType, - }, - }, - beforeSend: onError(state, logger), - autoCaptureSessions: false, // auto capture sessions is disabled - collectUserIp: false, // collecting user's IP is disabled - // enabledBreadcrumbTypes: ['error', 'log', 'user'], // for v7 and above - maxEvents: 100, - maxBreadcrumbs: 40, - releaseStage: getReleaseStage(), - user: { - // Combination of source, session and visit ids - id: `${state.source.value?.id ?? (state.lifecycle.writeKey.value as string)}..${state.session.sessionInfo.value?.id ?? 'NA'}..${state.autoTrack?.pageLifecycle?.visitId?.value ?? 'NA'}`, - }, - logger, - networkBreadcrumbsEnabled: false, - }; - - const client: BugsnagLib.Client = globalBugsnagLibInstance(clientConfig); - - return client; -}; - -const isApiKeyValid = (apiKey: string): boolean => { - const isAPIKeyValid = !(apiKey.startsWith('{{') || apiKey.endsWith('}}') || apiKey.length === 0); - return isAPIKeyValid; -}; - -const loadBugsnagSDK = (externalSrcLoader: IExternalSrcLoader, logger?: ILogger) => { - const isNotLoaded = GLOBAL_LIBRARY_OBJECT_NAMES.every( - globalKeyName => !(globalThis as any)[globalKeyName], - ); - - if (!isNotLoaded) { - return; - } - - externalSrcLoader.loadJSFile({ - url: BUGSNAG_CDN_URL, - id: ERROR_REPORT_PROVIDER_NAME_BUGSNAG, - callback: id => { - if (!id) { - logger?.error(BUGSNAG_SDK_LOAD_ERROR(BUGSNAG_PLUGIN)); - } - }, - }); -}; - -const initBugsnagClient = ( - state: ApplicationState, - promiseResolve: (value: BugsnagLib.Client) => void, - promiseReject: (reason?: Error) => void, - logger?: ILogger, - time = 0, -): void => { - const globalBugsnagLibInstance = getGlobalBugsnagLibInstance(); - if (typeof globalBugsnagLibInstance === 'function') { - if (isValidVersion(globalBugsnagLibInstance)) { - const client = getNewClient(state, logger); - promiseResolve(client); - } - } else if (time >= MAX_WAIT_FOR_SDK_LOAD_MS) { - promiseReject(new Error(BUGSNAG_SDK_LOAD_TIMEOUT_ERROR(MAX_WAIT_FOR_SDK_LOAD_MS))); - } else { - // Try to initialize the client after a delay - (globalThis as typeof window).setTimeout( - initBugsnagClient, - SDK_LOAD_POLL_INTERVAL_MS, - state, - promiseResolve, - promiseReject, - logger, - time + SDK_LOAD_POLL_INTERVAL_MS, - ); - } -}; - -export { - isValidVersion, - getNewClient, - isApiKeyValid, - loadBugsnagSDK, - getGlobalBugsnagLibInstance, - initBugsnagClient, - getReleaseStage, - isRudderSDKError, - enhanceErrorEventMutator, - onError, - getAppStateForMetadata, -}; diff --git a/packages/analytics-js-plugins/src/errorReporting/event/event.ts b/packages/analytics-js-plugins/src/errorReporting/event/event.ts deleted file mode 100644 index 796f52d0e..000000000 --- a/packages/analytics-js-plugins/src/errorReporting/event/event.ts +++ /dev/null @@ -1,106 +0,0 @@ -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 type { FrameType, IErrorFormat } from '../types'; -import { hasStack, isError } from './utils'; -import { ERROR_REPORTING_PLUGIN } from '../constants'; -import { stringifyWithoutCircular } from '../../shared-chunks/common'; - -const normaliseFunctionName = (name: string) => - /^global code$/i.test(name) ? 'global code' : name; - -// takes a stacktrace.js style stackframe (https://github.com/stacktracejs/stackframe) -// and returns a Bugsnag compatible stackframe (https://docs.bugsnag.com/api/error-reporting/#json-payload) -const formatStackframe = (frame: FrameType): Stackframe => { - const f = { - file: frame.fileName, - method: normaliseFunctionName(frame.functionName), - lineNumber: frame.lineNumber, - columnNumber: frame.columnNumber, - code: undefined, - inProject: undefined, - }; - // Some instances result in no file: - // - calling notify() from chrome's terminal results in no file/method. - // - non-error exception thrown from global code in FF - // This adds one. - if (f.lineNumber > -1 && !f.file && !f.method) { - f.file = 'global code'; - } - return f; -}; - -const ensureString = (str: any) => (typeof str === 'string' ? str : ''); - -function createBugsnagError( - errorClass: string, - errorMessage: string, - stacktrace: any[], -): Exception { - return { - errorClass: ensureString(errorClass), - message: ensureString(errorMessage), - type: 'browserjs', - stacktrace: stacktrace.reduce((accum: Stackframe[], frame: FrameType) => { - const f = formatStackframe(frame); - // don't include a stackframe if none of its properties are defined - try { - if (JSON.stringify(f) === '{}') return accum; - return accum.concat(f); - } catch (e) { - return accum; - } - }, []), - }; -} - -// Helpers - -const getStacktrace = (error: any) => { - if (hasStack(error)) return ErrorStackParser.parse(error); - return []; -}; - -const normaliseError = (maybeError: any, component: string, logger?: ILogger) => { - let error; - - if (isError(maybeError)) { - error = maybeError; - } else { - logger?.warn( - `${ERROR_REPORTING_PLUGIN}:: ${component} received a non-error: ${stringifyWithoutCircular(error)}`, - ); - error = undefined; - } - - if (error && !hasStack(error)) { - error = undefined; - } - - return error; -}; - -class ErrorFormat implements IErrorFormat { - errors: Exception[]; - - constructor(errorClass: string, errorMessage: string, stacktrace: any[]) { - this.errors = [createBugsnagError(errorClass, errorMessage, stacktrace)]; - } - static create(maybeError: any, component: string, logger?: ILogger) { - const error = normaliseError(maybeError, component, logger); - if (!error) { - return undefined; - } - let event; - try { - const stacktrace = getStacktrace(error); - event = new ErrorFormat(error.name, error.message, stacktrace); - } catch (e) { - event = new ErrorFormat(error.name, error.message, []); - } - - return event; - } -} - -export { ErrorFormat }; diff --git a/packages/analytics-js-plugins/src/errorReporting/event/utils.ts b/packages/analytics-js-plugins/src/errorReporting/event/utils.ts deleted file mode 100644 index 5e3f9acf8..000000000 --- a/packages/analytics-js-plugins/src/errorReporting/event/utils.ts +++ /dev/null @@ -1,18 +0,0 @@ -const hasStack = (err: any) => - !!err && - (!!err.stack || !!err.stacktrace || !!err['opera#sourceloc']) && - typeof (err.stack || err.stacktrace || err['opera#sourceloc']) === 'string' && - err.stack !== `${err.name}: ${err.message}`; - -const isError = (value: any) => { - switch (Object.prototype.toString.call(value)) { - case '[object Error]': - case '[object Exception]': - case '[object DOMException]': - return true; - default: - return value instanceof Error; - } -}; - -export { hasStack, isError }; diff --git a/packages/analytics-js-plugins/src/errorReporting/index.ts b/packages/analytics-js-plugins/src/errorReporting/index.ts deleted file mode 100644 index 543301ca1..000000000 --- a/packages/analytics-js-plugins/src/errorReporting/index.ts +++ /dev/null @@ -1,134 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -/* eslint-disable no-param-reassign */ -import type { - ApplicationState, - BreadcrumbMetaData, -} from '@rudderstack/analytics-js-common/types/ApplicationState'; -import type { - ExtensionPlugin, - IPluginEngine, -} from '@rudderstack/analytics-js-common/types/PluginEngine'; -import type { ILogger } from '@rudderstack/analytics-js-common/types/Logger'; -import type { PluginName } from '@rudderstack/analytics-js-common/types/PluginsManager'; -import type { ErrorState, SDKError } from '@rudderstack/analytics-js-common/types/ErrorHandler'; -import type { IHttpClient } from '@rudderstack/analytics-js-common/types/HttpClient'; -import type { IExternalSrcLoader } from '@rudderstack/analytics-js-common/services/ExternalSrcLoader/types'; -import { - createNewBreadcrumb, - getConfigForPayloadCreation, - isRudderSDKError, - getBugsnagErrorEvent, - getErrorDeliveryPayload, - isAllowedToBeNotified, -} from './utils'; -import { REQUEST_TIMEOUT_MS } from './constants'; -import { ErrorFormat } from './event/event'; -import { INVALID_SOURCE_CONFIG_ERROR } from './logMessages'; - -const pluginName: PluginName = 'ErrorReporting'; - -const ErrorReporting = (): ExtensionPlugin => ({ - name: pluginName, - deps: [], - initialize: (state: ApplicationState) => { - state.plugins.loadedPlugins.value = [...state.plugins.loadedPlugins.value, pluginName]; - state.reporting.isErrorReportingPluginLoaded.value = true; - if (state.reporting.breadcrumbs?.value) { - state.reporting.breadcrumbs.value = [createNewBreadcrumb('Error Reporting Plugin Loaded')]; - } - }, - errorReporting: { - // This extension point is deprecated - // TODO: Remove this in the next major release - init: ( - state: ApplicationState, - pluginEngine: IPluginEngine, - externalSrcLoader: IExternalSrcLoader, - logger?: ILogger, - isInvokedFromLatestCore?: boolean, - ) => { - if (isInvokedFromLatestCore) { - return undefined; - } - if (!state.source.value?.config || !state.source.value?.id) { - return Promise.reject(new Error(INVALID_SOURCE_CONFIG_ERROR)); - } - - return pluginEngine.invokeSingle( - 'errorReportingProvider.init', - state, - externalSrcLoader, - logger, - ); - }, - notify: ( - pluginEngine: IPluginEngine, // Only kept for backward compatibility - client: any, // Only kept for backward compatibility - error: SDKError, - state: ApplicationState, - logger?: ILogger, - httpClient?: IHttpClient, - errorState?: ErrorState, - ): void => { - if (httpClient) { - const { component, normalizedError } = getConfigForPayloadCreation( - error, - errorState?.severityReason.type as string, - ); - - // Generate the error payload - const errorPayload = ErrorFormat.create(normalizedError, component, logger); - - if (!errorPayload || !isAllowedToBeNotified(errorPayload.errors[0])) { - return; - } - - // filter errors - if (!isRudderSDKError(errorPayload.errors[0])) { - return; - } - - // enrich error payload - const bugsnagPayload = getBugsnagErrorEvent(errorPayload, errorState as ErrorState, state); - - // send it to metrics service - httpClient?.getAsyncData({ - url: state.metrics.metricsServiceUrl.value as string, - options: { - method: 'POST', - data: getErrorDeliveryPayload(bugsnagPayload, state), - sendRawData: true, - }, - isRawResponse: true, - timeout: REQUEST_TIMEOUT_MS, - callback: (result: any, details: any) => { - // do nothing - }, - }); - } else { - pluginEngine.invokeSingle('errorReportingProvider.notify', client, error, state, logger); - } - }, - breadcrumb: ( - pluginEngine: IPluginEngine, // Only kept for backward compatibility - client: any, // Only kept for backward compatibility - message: string, - logger?: ILogger, // Only kept for backward compatibility - state?: ApplicationState, - metaData?: BreadcrumbMetaData, - ): void => { - if (state) { - state.reporting.breadcrumbs.value = [ - ...state.reporting.breadcrumbs.value, - createNewBreadcrumb(message, metaData), - ]; - } else { - pluginEngine.invokeSingle('errorReportingProvider.breadcrumb', client, message, logger); - } - }, - }, -}); - -export { ErrorReporting }; - -export default ErrorReporting; diff --git a/packages/analytics-js-plugins/src/errorReporting/logMessages.ts b/packages/analytics-js-plugins/src/errorReporting/logMessages.ts deleted file mode 100644 index 36db30ec0..000000000 --- a/packages/analytics-js-plugins/src/errorReporting/logMessages.ts +++ /dev/null @@ -1,3 +0,0 @@ -const INVALID_SOURCE_CONFIG_ERROR = `Invalid source configuration or source id.`; - -export { INVALID_SOURCE_CONFIG_ERROR }; diff --git a/packages/analytics-js-plugins/src/errorReporting/types.ts b/packages/analytics-js-plugins/src/errorReporting/types.ts deleted file mode 100644 index dc77d3769..000000000 --- a/packages/analytics-js-plugins/src/errorReporting/types.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { Exception } from '@rudderstack/analytics-js-common/types/Metrics'; - -export interface IErrorFormat { - errors: Exception[]; -} - -export type FrameType = { - fileName: string; - functionName: string; - lineNumber: number; - columnNumber: number; -}; diff --git a/packages/analytics-js-plugins/src/index.ts b/packages/analytics-js-plugins/src/index.ts index d63b16825..32017dc8a 100644 --- a/packages/analytics-js-plugins/src/index.ts +++ b/packages/analytics-js-plugins/src/index.ts @@ -1,9 +1,7 @@ export { default as BeaconQueue } from './beaconQueue'; -export { default as Bugsnag } from './bugsnag'; export { default as CustomConsentManager } from './customConsentManager'; export { default as DeviceModeDestinations } from './deviceModeDestinations'; export { default as DeviceModeTransformation } from './deviceModeTransformation'; -export { default as ErrorReporting } from './errorReporting'; export { default as ExternalAnonymousId } from './externalAnonymousId'; export { default as GoogleLinker } from './googleLinker'; export { default as IubendaConsentManager } from './iubendaConsentManager'; diff --git a/packages/analytics-js-plugins/src/shared-chunks/common.ts b/packages/analytics-js-plugins/src/shared-chunks/common.ts index afa9c2b7f..2afb4e29a 100644 --- a/packages/analytics-js-plugins/src/shared-chunks/common.ts +++ b/packages/analytics-js-plugins/src/shared-chunks/common.ts @@ -36,6 +36,5 @@ export { } from '@rudderstack/analytics-js-common/utilities/object'; export { CDN_INT_DIR } from '@rudderstack/analytics-js-common/constants/urls'; export { METRICS_PAYLOAD_VERSION } from '@rudderstack/analytics-js-common/constants/metrics'; -export { ERROR_MESSAGES_TO_BE_FILTERED } from '@rudderstack/analytics-js-common/constants/errors'; export { onPageLeave } from '@rudderstack/analytics-js-common/utilities/page'; export { QueueStatuses } from '@rudderstack/analytics-js-common/constants/QueueStatuses'; diff --git a/packages/analytics-js/.size-limit.mjs b/packages/analytics-js/.size-limit.mjs index 9dc1495ea..98882d766 100644 --- a/packages/analytics-js/.size-limit.mjs +++ b/packages/analytics-js/.size-limit.mjs @@ -30,24 +30,24 @@ export default [ name: 'Core - Modern - NPM (ESM)', path: 'dist/npm/modern/esm/index.mjs', import: '*', - limit: '25 KiB', + limit: '27.5 KiB', }, { name: 'Core - Modern - NPM (CJS)', path: 'dist/npm/modern/cjs/index.cjs', import: '*', - limit: '25.5 KiB', + limit: '27.5 KiB', }, { name: 'Core - Modern - NPM (UMD)', path: 'dist/npm/modern/umd/index.js', import: '*', - limit: '25 KiB', + limit: '27.5 KiB', }, { name: 'Core - Modern - CDN', path: 'dist/cdn/modern/iife/rsa.min.js', - limit: '25.5 KiB', + limit: '27.5 KiB', }, { name: 'Core (Bundled) - Legacy - NPM (ESM)', diff --git a/packages/analytics-js/__mocks__/remotePlugins/Bugsnag.ts b/packages/analytics-js/__mocks__/remotePlugins/Bugsnag.ts deleted file mode 100644 index 2af495db6..000000000 --- a/packages/analytics-js/__mocks__/remotePlugins/Bugsnag.ts +++ /dev/null @@ -1,10 +0,0 @@ -const Bugsnag = () => ({ - name: 'Bugsnag', - errorReportingProvider: { - init: jest.fn(() => {}), - notify: jest.fn(() => {}), - breadcrumb: jest.fn(() => {}), - }, -}); - -export default Bugsnag; diff --git a/packages/analytics-js/__mocks__/remotePlugins/ErrorReporting.ts b/packages/analytics-js/__mocks__/remotePlugins/ErrorReporting.ts deleted file mode 100644 index 49f9f01d5..000000000 --- a/packages/analytics-js/__mocks__/remotePlugins/ErrorReporting.ts +++ /dev/null @@ -1,10 +0,0 @@ -const ErrorReporting = () => ({ - name: 'ErrorReporting', - errorReporting: { - init: jest.fn(() => {}), - notify: jest.fn(() => {}), - breadcrumb: jest.fn(() => {}), - }, -}); - -export default ErrorReporting; diff --git a/packages/analytics-js/__tests__/services/ErrorHandler/utils.test.ts b/packages/analytics-js/__tests__/services/ErrorHandler/utils.test.ts new file mode 100644 index 000000000..3eeb96a7a --- /dev/null +++ b/packages/analytics-js/__tests__/services/ErrorHandler/utils.test.ts @@ -0,0 +1,609 @@ +/* eslint-disable max-classes-per-file */ +import { signal } from '@preact/signals-core'; +import type { ErrorEventPayload } from '@rudderstack/analytics-js-common/types/Metrics'; +import { mergeDeepRight } from '@rudderstack/analytics-js-common/utilities/object'; +import { state } from '../../../src/state'; +import * as errorReportingConstants from '../../../src/services/ErrorHandler/constant'; +import { + createNewBreadcrumb, + getAppStateForMetadata, + getBugsnagErrorEvent, + getErrorDeliveryPayload, + getReleaseStage, + getURLWithoutQueryString, + isAllowedToBeNotified, + isRudderSDKError, +} from '../../../src/services/ErrorHandler/utils'; + +jest.mock('@rudderstack/analytics-js-common/utilities/uuId', () => ({ + generateUUID: jest.fn().mockReturnValue('test_uuid'), +})); + +const DEFAULT_STATE_DATA = { + autoTrack: { + enabled: false, + pageLifecycle: { + enabled: false, + }, + }, + capabilities: { + isAdBlocked: false, + isBeaconAvailable: false, + isCryptoAvailable: false, + isIE11: false, + isLegacyDOM: false, + isOnline: true, + isUaCHAvailable: false, + storage: { + isCookieStorageAvailable: false, + isLocalStorageAvailable: false, + isSessionStorageAvailable: false, + }, + }, + consents: { + data: {}, + enabled: false, + initialized: false, + postConsent: {}, + preConsent: { + enabled: false, + }, + resolutionStrategy: 'and', + }, + context: { + app: { + installType: 'cdn', + name: 'RudderLabs JavaScript SDK', + namespace: 'com.rudderlabs.javascript', + version: 'dev-snapshot', + }, + device: null, + library: { + name: 'RudderLabs JavaScript SDK', + version: 'dev-snapshot', + }, + locale: null, + network: null, + os: { + name: '', + version: '', + }, + screen: { + density: 0, + height: 0, + innerHeight: 0, + innerWidth: 0, + width: 0, + }, + userAgent: '', + }, + dataPlaneEvents: { + deliveryEnabled: true, + }, + lifecycle: { + initialized: false, + loaded: false, + logLevel: 'ERROR', + readyCallbacks: [], + }, + loadOptions: { + beaconQueueOptions: {}, + bufferDataPlaneEventsUntilReady: false, + configUrl: 'https://api.rudderstack.com', + dataPlaneEventsBufferTimeout: 1000, + destinationsQueueOptions: {}, + integrations: { + All: true, + }, + loadIntegration: true, + lockIntegrationsVersion: false, + lockPluginsVersion: false, + logLevel: 'ERROR', + plugins: [], + polyfillIfRequired: true, + queueOptions: {}, + sameSiteCookie: 'Lax', + sendAdblockPageOptions: {}, + sessions: { + autoTrack: true, + timeout: 1800000, + }, + storage: { + cookie: {}, + encryption: { + version: 'v3', + }, + migrate: true, + }, + uaChTrackLevel: 'none', + useBeacon: false, + useGlobalIntegrationsConfigInEvents: false, + useServerSideCookies: false, + }, + metrics: { + dropped: 0, + queued: 0, + retries: 0, + sent: 0, + triggered: 0, + }, + nativeDestinations: { + activeDestinations: [], + clientDestinationsReady: false, + configuredDestinations: [], + failedDestinations: [], + initializedDestinations: [], + integrationsConfig: {}, + loadIntegration: true, + loadOnlyIntegrations: {}, + }, + plugins: { + activePlugins: [], + failedPlugins: [], + loadedPlugins: [], + pluginsToLoadFromConfig: [], + ready: false, + totalPluginsToLoad: 0, + }, + reporting: { + breadcrumbs: [], + isErrorReportingEnabled: false, + isErrorReportingPluginLoaded: false, + isMetricsReportingEnabled: false, + }, + serverCookies: { + isEnabledServerSideCookies: false, + }, + session: { + initialReferrer: '', + initialReferringDomain: '', + }, + source: { + id: 'dummy-source-id', + workspaceId: 'dummy-workspace-id', + }, + storage: { + entries: {}, + migrate: false, + trulyAnonymousTracking: false, + }, +}; + +describe('Error Reporting utilities', () => { + describe('createNewBreadcrumb', () => { + it('should create and return a breadcrumb', () => { + const msg = 'sample message'; + const breadcrumb = createNewBreadcrumb(msg); + + expect(breadcrumb).toStrictEqual({ + metaData: {}, + type: 'manual', + timestamp: expect.any(Date), + name: msg, + }); + }); + + it('should create and return a breadcrumb with empty meta data if not provided', () => { + const msg = 'sample message'; + const breadcrumb = createNewBreadcrumb(msg); + + expect(breadcrumb.metaData).toStrictEqual({}); + }); + }); + + describe('getURLWithoutQueryString', () => { + it('should return url without query param ', () => { + (window as any).location.href = 'https://www.test-host.com?key1=1234&key2=true'; + const urlWithoutSearchParam = getURLWithoutQueryString(); + expect(urlWithoutSearchParam).toEqual('https://www.test-host.com/'); + }); + }); + describe('getReleaseStage', () => { + let windowSpy: any; + let locationSpy: any; + + beforeEach(() => { + windowSpy = jest.spyOn(window, 'window', 'get'); + locationSpy = jest.spyOn(globalThis, 'location', 'get'); + }); + + afterEach(() => { + windowSpy.mockRestore(); + locationSpy.mockRestore(); + }); + + const testCaseData = [ + ['localhost', 'development'], + ['127.0.0.1', 'development'], + ['www.test-host.com', 'development'], + ['[::1]', 'development'], + ['', '__RS_BUGSNAG_RELEASE_STAGE__'], + ['www.validhost.com', '__RS_BUGSNAG_RELEASE_STAGE__'], + ]; + + it.each(testCaseData)( + 'if window host name is "%s" then it should return the release stage as "%s" ', + (hostName, expectedReleaseStage) => { + locationSpy.mockImplementation(() => ({ + hostname: hostName, + })); + + expect(getReleaseStage()).toBe(expectedReleaseStage); + }, + ); + }); + + describe('isRudderSDKError', () => { + const testCaseData: any[] = [ + ['https://invalid-domain.com/rsa.min.js', true], + ['https://invalid-domain.com/rss.min.js', false], + ['https://invalid-domain.com/rsa-plugins-Beacon.min.js', true], + ['https://invalid-domain.com/Amplitude.min.js', false], + ['https://invalid-domain.com/js-integrations/Amplitude.min.js', true], + ['https://invalid-domain.com/js-integrations/Qualaroo.min.js', true], + ['https://invalid-domain.com/mjs-integrations/Qualaroo.min.js', false], + ['https://invalid-domain.com/test.js', false], + ['https://invalid-domain.com/rsa.css', false], + [undefined, false], + [null, false], + [1, false], + ['', false], + ['asdf.com', false], + ]; + + it.each(testCaseData)( + 'if script src is "%s" then it should return the value as "%s" ', + (scriptSrc: string, expectedValue: boolean) => { + // Bugsnag error event object structure + const event = { + stacktrace: [ + { + file: scriptSrc, + }, + ], + }; + + expect(isRudderSDKError(event)).toBe(expectedValue); + }, + ); + }); + + describe('getAppStateForMetadata', () => { + const origAppStateExcludes = errorReportingConstants.APP_STATE_EXCLUDE_KEYS; + + afterEach(() => { + Object.defineProperty(errorReportingConstants, 'APP_STATE_EXCLUDE_KEYS', { + value: origAppStateExcludes, + writable: true, + }); + }); + + // Here we are just exploring different combinations of data where + // the signals could be buried inside objects, arrays, nested objects, etc. + const tcData: any[][] = [ + [ + { + name: 'test', + value: 123, + someKey1: [1, 2, 3], + someKey2: { + key1: 'value1', + key2: 'value2', + }, + someKey3: 2.5, + testSignal: signal('test'), + }, + { + name: 'test', + value: 123, + someKey1: [1, 2, 3], + someKey2: { + key1: 'value1', + key2: 'value2', + }, + someKey3: 2.5, + testSignal: 'test', + }, + undefined, + ], + [ + { + name: 'test', + someKey: { + key1: 'value1', + key2: signal('value2'), + }, + someKey2: { + key1: 'value1', + key2: { + key3: signal('value3'), + }, + }, + someKey3: [signal('value1'), signal('value2'), 1, 3], + someKey4: [ + { + key1: signal('value1'), + key2: signal('value2'), + }, + 'asdf', + 1, + { + key3: 'value3', + key4: 'value4', + }, + ], + }, + { + name: 'test', + someKey: { + key1: 'value1', + key2: 'value2', + }, + someKey2: { + key1: 'value1', + key2: { + key3: 'value3', + }, + }, + someKey3: ['value1', 'value2', 1, 3], + someKey4: [ + { + key1: 'value1', + key2: 'value2', + }, + 'asdf', + 1, + { + key3: 'value3', + key4: 'value4', + }, + ], + }, + [], + ], + [ + { + someKey: signal({ + key1: 'value1', + key2: signal('value2'), + key3: [signal('value1'), signal('value2'), undefined, null], + key4: true, + key7: { + key1: signal('value1'), + key2: signal('value2'), + key3: 'asdf', + key4: signal('value4'), + }, + key5: signal([signal('value1'), signal('value2'), 1, 3]), + KEY6: 123, + }), + }, + { + someKey: { + key1: 'value1', + key2: 'value2', + key3: ['value1', 'value2', null, null], + key7: { + key1: 'value1', + key2: 'value2', + key3: 'asdf', + }, + key5: ['value1', 'value2', 1, 3], + KEY6: 123, + }, + }, + ['key4', 'key6'], // excluded keys + ], + [ + { + // eslint-disable-next-line compat/compat + someKey: BigInt(123), + }, + {}, + [], + ], + ]; + + it.each(tcData)('should convert signals to JSON %#', (input, expected, excludes) => { + Object.defineProperty(errorReportingConstants, 'APP_STATE_EXCLUDE_KEYS', { + value: excludes, + writable: true, + }); + + expect(getAppStateForMetadata(input)).toEqual(expected); + }); + }); + + describe('getBugsnagErrorEvent', () => { + it('should return enhanced error event payload', () => { + state.session.sessionInfo.value = { id: 123 }; + state.autoTrack.pageLifecycle.visitId.value = 'test-visit-id'; + + const newError = new Error(); + const normalizedError = Object.create(newError, { + message: { value: 'ReferenceError: testUndefinedFn is not defined' }, + stack: { + value: `ReferenceError: testUndefinedFn is not defined at Analytics.page (http://localhost:3001/cdn/modern/iife/rsa.js:1610:3) at RudderAnalytics.page (http://localhost:3001/cdn/modern/iife/rsa.js:1666:84)`, + }, + }); + const errorState = { + severity: 'error', + unhandled: false, + severityReason: { type: 'handledException' }, + }; + const errorPayload = ErrorFormat.create(normalizedError, 'notify()') as ErrorFormat; + + (window as any).RudderSnippetVersion = 'sample_snippet_version'; + const enhancedError = getBugsnagErrorEvent(errorPayload, errorState, state); + console.log(JSON.stringify(enhancedError)); + const expectedOutcome = { + notifier: { + name: 'RudderStack JavaScript SDK Error Notifier', + version: 'dev-snapshot', + url: 'https://github.com/rudderlabs/rudder-sdk-js', + }, + events: [ + { + payloadVersion: '5', + exceptions: [ + { + errorClass: 'Error', + message: 'ReferenceError: testUndefinedFn is not defined', + type: 'browserjs', + stacktrace: [ + { + file: 'ReferenceError: testUndefinedFn is not defined at Analytics.page http://localhost:3001/cdn/modern/iife/rsa.js:1610:3 at RudderAnalytics.page http://localhost:3001/cdn/modern/iife/rsa.js', + lineNumber: 1666, + columnNumber: 84, + code: undefined, + inProject: undefined, + method: undefined, + }, + ], + }, + ], + severity: 'error', + unhandled: false, + severityReason: { + type: 'handledException', + }, + app: { + version: 'dev-snapshot', + releaseStage: 'development', + }, + device: { + userAgent: '', + time: expect.any(Date), + }, + request: { + url: 'https://www.test-host.com/', + clientIp: '[NOT COLLECTED]', + }, + breadcrumbs: [], + context: 'ReferenceError: testUndefinedFn is not defined', + metaData: { + sdk: { + name: 'JS', + installType: 'cdn', + }, + state: mergeDeepRight(DEFAULT_STATE_DATA, { + autoTrack: { + pageLifecycle: { + visitId: 'test-visit-id', + }, + }, + session: { + sessionInfo: { id: 123 }, + }, + }), + source: { + snippetVersion: 'sample_snippet_version', + }, + }, + user: { + id: 'dummy-source-id..123..test-visit-id', + }, + }, + ], + }; + expect(enhancedError).toEqual(expectedOutcome); + }); + }); + + describe('getErrorDeliveryPayload', () => { + it('should return error delivery payload', () => { + const enhancedErrorPayload = { + notifier: { + name: 'Rudderstack JavaScript SDK Error Notifier', + version: 'sample_version', + url: 'https://github.com/rudderlabs/rudder-sdk-js', + }, + events: [ + { + payloadVersion: '5', + exceptions: [ + { + errorClass: 'Error', + errorMessage: 'ReferenceError: testUndefinedFn is not defined', + type: 'browserjs', + stacktrace: [ + { + file: 'ReferenceError: testUndefinedFn is not defined at Analytics.page http://localhost:3001/cdn/modern/iife/rsa.js:1610:3 at RudderAnalytics.page http://localhost:3001/cdn/modern/iife/rsa.js', + lineNumber: 1666, + columnNumber: 84, + code: undefined, + inProject: undefined, + method: undefined, + }, + ], + }, + ], + severity: 'error', + unhandled: false, + severityReason: { + type: 'handledException', + }, + app: { + version: 'dev-snapshot', + releaseStage: 'development', + }, + device: { + userAgent: '', + time: expect.any(Date), + }, + request: { + url: 'https://www.test-host.com/', + clientIp: '[NOT COLLECTED]', + }, + breadcrumbs: [], + context: 'ReferenceError: testUndefinedFn is not defined', + metaData: { + sdk: { + name: 'JS', + installType: 'cdn', + }, + state: DEFAULT_STATE_DATA, + source: { + snippetVersion: 'sample_snippet_version', + }, + }, + user: { + id: 'sample-write-key', + }, + }, + ], + } as unknown as ErrorEventPayload; + + const deliveryPayload = getErrorDeliveryPayload(enhancedErrorPayload, state); + expect(deliveryPayload).toEqual( + JSON.stringify({ + version: '1', + message_id: 'test_uuid', + source: { + name: 'js', + sdk_version: 'dev-snapshot', + install_type: 'cdn', + }, + errors: enhancedErrorPayload, + }), + ); + }); + }); + + 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/rollup.config.mjs b/packages/analytics-js/rollup.config.mjs index 2a1a2e8ea..5058e2bd9 100644 --- a/packages/analytics-js/rollup.config.mjs +++ b/packages/analytics-js/rollup.config.mjs @@ -81,10 +81,6 @@ const getExternalsConfig = () => { externalGlobalsConfig['@rudderstack/analytics-js-plugins/beaconQueue'] = '{}'; } - if (!bundledPluginsList.includes('Bugsnag')) { - externalGlobalsConfig['@rudderstack/analytics-js-plugins/bugsnag'] = '{}'; - } - if (!bundledPluginsList.includes('CustomConsentManager')) { externalGlobalsConfig['@rudderstack/analytics-js-plugins/customConsentManager'] = '{}'; } @@ -97,10 +93,6 @@ const getExternalsConfig = () => { externalGlobalsConfig['@rudderstack/analytics-js-plugins/deviceModeTransformation'] = '{}'; } - if (!bundledPluginsList.includes('ErrorReporting')) { - externalGlobalsConfig['@rudderstack/analytics-js-plugins/errorReporting'] = '{}'; - } - if (!bundledPluginsList.includes('ExternalAnonymousId')) { externalGlobalsConfig['@rudderstack/analytics-js-plugins/externalAnonymousId'] = '{}'; } @@ -195,6 +187,7 @@ export function getDefaultConfig(distName) { __RS_BUGSNAG_API_KEY__: process.env.BUGSNAG_API_KEY || '{{__RS_BUGSNAG_API_KEY__}}', __RS_BUGSNAG_RELEASE_STAGE__: process.env.BUGSNAG_RELEASE_STAGE || 'production', __RS_BUGSNAG_SDK_URL__: bugsnagSDKUrl, + __REPOSITORY_URL__: pkg.repository.url, }), resolve({ jsnext: true, diff --git a/packages/analytics-js/src/app/RudderAnalytics.ts b/packages/analytics-js/src/app/RudderAnalytics.ts index 6d94926a7..f969f7c89 100644 --- a/packages/analytics-js/src/app/RudderAnalytics.ts +++ b/packages/analytics-js/src/app/RudderAnalytics.ts @@ -36,7 +36,6 @@ import type { IAnalytics } from '../components/core/IAnalytics'; import { Analytics } from '../components/core/Analytics'; import { defaultLogger } from '../services/Logger/Logger'; import { PAGE_UNLOAD_ON_BEACON_DISABLED_WARNING } from '../constants/logMessages'; -import { defaultErrorHandler } from '../services/ErrorHandler'; import { state } from '../state'; // TODO: add analytics restart/reset mechanism @@ -65,7 +64,6 @@ class RudderAnalytics implements IRudderAnalytics { return RudderAnalytics.globalSingleton; // END-NO-SONAR-SCAN } - defaultErrorHandler.attachErrorListeners(); this.setDefaultInstanceKey = this.setDefaultInstanceKey.bind(this); this.getAnalyticsInstance = this.getAnalyticsInstance.bind(this); diff --git a/packages/analytics-js/src/components/capabilitiesManager/CapabilitiesManager.ts b/packages/analytics-js/src/components/capabilitiesManager/CapabilitiesManager.ts index a04b2c9a3..dfadd1072 100644 --- a/packages/analytics-js/src/components/capabilitiesManager/CapabilitiesManager.ts +++ b/packages/analytics-js/src/components/capabilitiesManager/CapabilitiesManager.ts @@ -37,10 +37,10 @@ import { debounce } from '../utilities/globals'; // TODO: replace direct calls to detection methods with state values when possible class CapabilitiesManager implements ICapabilitiesManager { logger?: ILogger; - errorHandler?: IErrorHandler; + errorHandler: IErrorHandler; externalSrcLoader: IExternalSrcLoader; - constructor(errorHandler?: IErrorHandler, logger?: ILogger) { + constructor(errorHandler: IErrorHandler, logger?: ILogger) { this.logger = logger; this.errorHandler = errorHandler; this.externalSrcLoader = new ExternalSrcLoader(this.errorHandler, this.logger); diff --git a/packages/analytics-js/src/components/capabilitiesManager/detection/adBlockers.ts b/packages/analytics-js/src/components/capabilitiesManager/detection/adBlockers.ts index 204140ef4..82140c365 100644 --- a/packages/analytics-js/src/components/capabilitiesManager/detection/adBlockers.ts +++ b/packages/analytics-js/src/components/capabilitiesManager/detection/adBlockers.ts @@ -3,7 +3,7 @@ import type { ILogger } from '@rudderstack/analytics-js-common/types/Logger'; import { HttpClient } from '../../../services/HttpClient/HttpClient'; import { state } from '../../../state'; -const detectAdBlockers = (errorHandler?: IErrorHandler, logger?: ILogger): void => { +const detectAdBlockers = (errorHandler: IErrorHandler, logger?: ILogger): void => { // Apparently, '?view=ad' is a query param that is blocked by majority of adblockers // Use source config URL here as it is very unlikely to be blocked by adblockers @@ -13,7 +13,8 @@ const detectAdBlockers = (errorHandler?: IErrorHandler, logger?: ILogger): void const baseUrl = new URL(state.lifecycle.sourceConfigUrl.value as string); const url = `${baseUrl.origin}${baseUrl.pathname}?view=ad`; - const httpClient = new HttpClient(errorHandler, logger); + const httpClient = new HttpClient(logger); + httpClient.init(errorHandler); httpClient.setAuthHeader(state.lifecycle.writeKey.value as string); httpClient.getAsyncData({ diff --git a/packages/analytics-js/src/components/capabilitiesManager/types.ts b/packages/analytics-js/src/components/capabilitiesManager/types.ts index a705cd9f3..8e2eacf5c 100644 --- a/packages/analytics-js/src/components/capabilitiesManager/types.ts +++ b/packages/analytics-js/src/components/capabilitiesManager/types.ts @@ -4,7 +4,7 @@ import type { IExternalSrcLoader } from '@rudderstack/analytics-js-common/servic export interface ICapabilitiesManager { logger?: ILogger; - errorHandler?: IErrorHandler; + errorHandler: IErrorHandler; externalSrcLoader: IExternalSrcLoader; init(): void; detectBrowserCapabilities(): void; diff --git a/packages/analytics-js/src/components/configManager/ConfigManager.ts b/packages/analytics-js/src/components/configManager/ConfigManager.ts index 315f2ab5a..bff7e5f12 100644 --- a/packages/analytics-js/src/components/configManager/ConfigManager.ts +++ b/packages/analytics-js/src/components/configManager/ConfigManager.ts @@ -120,9 +120,9 @@ class ConfigManager implements IConfigManager { /** * Handle errors */ - onError(error: unknown, customMessage?: string, shouldAlwaysThrow?: boolean) { + onError(error: unknown, customMessage?: string) { if (this.hasErrorHandler) { - this.errorHandler?.onError(error, CONFIG_MANAGER, customMessage, shouldAlwaysThrow); + this.errorHandler?.onError(error, CONFIG_MANAGER, customMessage); } else { throw error; } @@ -148,12 +148,12 @@ class ConfigManager implements IConfigManager { res = response; } } catch (err) { - this.onError(err, SOURCE_CONFIG_RESOLUTION_ERROR, true); + this.onError(err, SOURCE_CONFIG_RESOLUTION_ERROR); return; } if (!isValidSourceConfig(res)) { - this.onError(new Error(SOURCE_CONFIG_RESOLUTION_ERROR), undefined, true); + this.onError(new Error(SOURCE_CONFIG_RESOLUTION_ERROR)); return; } @@ -174,6 +174,7 @@ class ConfigManager implements IConfigManager { // set source related information in state state.source.value = { config: res.source.config, + name: res.source.name, id: res.source.id, workspaceId: res.source.workspaceId, }; diff --git a/packages/analytics-js/src/components/core/Analytics.ts b/packages/analytics-js/src/components/core/Analytics.ts index 8421dda6c..991a76230 100644 --- a/packages/analytics-js/src/components/core/Analytics.ts +++ b/packages/analytics-js/src/components/core/Analytics.ts @@ -99,6 +99,7 @@ class Analytics implements IAnalytics { this.externalSrcLoader = new ExternalSrcLoader(this.errorHandler, this.logger); this.capabilitiesManager = new CapabilitiesManager(this.errorHandler, this.logger); this.httpClient = defaultHttpClient; + this.httpClient.init(this.errorHandler); } /** @@ -246,6 +247,7 @@ class Analytics implements IAnalytics { this.eventRepository = new EventRepository( this.pluginsManager, this.storeManager, + this.httpClient, this.errorHandler, this.logger, ); @@ -272,7 +274,6 @@ class Analytics implements IAnalytics { * Initialize the storage and event queue */ onPluginsReady() { - this.errorHandler.init(this.httpClient, this.externalSrcLoader); // Initialize storage this.storeManager?.init(); this.userSessionManager?.init(); diff --git a/packages/analytics-js/src/components/eventManager/EventManager.ts b/packages/analytics-js/src/components/eventManager/EventManager.ts index 7b98dea55..d30f546f7 100644 --- a/packages/analytics-js/src/components/eventManager/EventManager.ts +++ b/packages/analytics-js/src/components/eventManager/EventManager.ts @@ -68,9 +68,9 @@ class EventManager implements IEventManager { * Handles error * @param error The error object */ - onError(error: unknown, customMessage?: string, shouldAlwaysThrow?: boolean): void { + onError(error: unknown, customMessage?: string): void { if (this.errorHandler) { - this.errorHandler.onError(error, EVENT_MANAGER, customMessage, shouldAlwaysThrow); + this.errorHandler.onError(error, EVENT_MANAGER, customMessage); } else { throw error; } diff --git a/packages/analytics-js/src/components/eventRepository/EventRepository.ts b/packages/analytics-js/src/components/eventRepository/EventRepository.ts index 0964d58aa..d89c9096e 100644 --- a/packages/analytics-js/src/components/eventRepository/EventRepository.ts +++ b/packages/analytics-js/src/components/eventRepository/EventRepository.ts @@ -18,7 +18,6 @@ import { NATIVE_DEST_PLUGIN_ENQUEUE_ERROR, NATIVE_DEST_PLUGIN_INITIALIZE_ERROR, } from '../../constants/logMessages'; -import { HttpClient } from '../../services/HttpClient'; import { state } from '../../state'; import type { IEventRepository } from './types'; import { @@ -51,13 +50,14 @@ class EventRepository implements IEventRepository { constructor( pluginsManager: IPluginsManager, storeManager: IStoreManager, + httpClient: IHttpClient, errorHandler?: IErrorHandler, logger?: ILogger, ) { this.pluginsManager = pluginsManager; this.errorHandler = errorHandler; + this.httpClient = httpClient; this.logger = logger; - this.httpClient = new HttpClient(errorHandler, logger); this.storeManager = storeManager; this.onError = this.onError.bind(this); } @@ -213,9 +213,9 @@ class EventRepository implements IEventRepository { * @param customMessage a message * @param shouldAlwaysThrow if it should throw or use logger */ - onError(error: unknown, customMessage?: string, shouldAlwaysThrow?: boolean): void { + onError(error: unknown, customMessage?: string): void { if (this.errorHandler) { - this.errorHandler.onError(error, EVENT_REPOSITORY, customMessage, shouldAlwaysThrow); + this.errorHandler.onError(error, EVENT_REPOSITORY, customMessage); } else { throw error; } diff --git a/packages/analytics-js/src/components/pluginsManager/PluginsManager.ts b/packages/analytics-js/src/components/pluginsManager/PluginsManager.ts index 4501099fc..2252773b6 100644 --- a/packages/analytics-js/src/components/pluginsManager/PluginsManager.ts +++ b/packages/analytics-js/src/components/pluginsManager/PluginsManager.ts @@ -113,11 +113,6 @@ class PluginsManager implements IPluginsManager { supportedPlugins: Object.values(DataPlaneEventsTransportToPluginNameMap), shouldAddMissingPlugins: true, }, - { - configurationStatus: () => state.reporting.isErrorReportingEnabled.value, - configurationStatusStr: 'Error reporting is enabled', - supportedPlugins: ['ErrorReporting', 'Bugsnag'] as PluginName[], // TODO: Remove deprecated plugin- Bugsnag - }, { configurationStatus: () => getNonCloudDestinations(state.nativeDestinations.configuredDestinations.value).length > 0, diff --git a/packages/analytics-js/src/components/pluginsManager/bundledBuildPluginImports.ts b/packages/analytics-js/src/components/pluginsManager/bundledBuildPluginImports.ts index 233c09fae..32d51bf0d 100644 --- a/packages/analytics-js/src/components/pluginsManager/bundledBuildPluginImports.ts +++ b/packages/analytics-js/src/components/pluginsManager/bundledBuildPluginImports.ts @@ -1,9 +1,7 @@ import { BeaconQueue } from '@rudderstack/analytics-js-plugins/beaconQueue'; -import { Bugsnag } from '@rudderstack/analytics-js-plugins/bugsnag'; import { CustomConsentManager } from '@rudderstack/analytics-js-plugins/customConsentManager'; import { DeviceModeDestinations } from '@rudderstack/analytics-js-plugins/deviceModeDestinations'; import { DeviceModeTransformation } from '@rudderstack/analytics-js-plugins/deviceModeTransformation'; -import { ErrorReporting } from '@rudderstack/analytics-js-plugins/errorReporting'; import { ExternalAnonymousId } from '@rudderstack/analytics-js-plugins/externalAnonymousId'; import { GoogleLinker } from '@rudderstack/analytics-js-plugins/googleLinker'; import { IubendaConsentManager } from '@rudderstack/analytics-js-plugins/iubendaConsentManager'; @@ -21,11 +19,9 @@ import type { PluginMap } from './types'; */ const getBundledBuildPluginImports = (): PluginMap => ({ BeaconQueue, - Bugsnag, CustomConsentManager, DeviceModeDestinations, DeviceModeTransformation, - ErrorReporting, ExternalAnonymousId, GoogleLinker, IubendaConsentManager, diff --git a/packages/analytics-js/src/components/pluginsManager/defaultPluginsList.ts b/packages/analytics-js/src/components/pluginsManager/defaultPluginsList.ts index e232c9c22..016ae0c58 100644 --- a/packages/analytics-js/src/components/pluginsManager/defaultPluginsList.ts +++ b/packages/analytics-js/src/components/pluginsManager/defaultPluginsList.ts @@ -5,11 +5,9 @@ import type { PluginName } from '@rudderstack/analytics-js-common/types/PluginsM */ const defaultOptionalPluginsList: PluginName[] = [ 'BeaconQueue', - 'Bugsnag', 'CustomConsentManager', 'DeviceModeDestinations', 'DeviceModeTransformation', - 'ErrorReporting', 'ExternalAnonymousId', 'GoogleLinker', 'IubendaConsentManager', diff --git a/packages/analytics-js/src/components/pluginsManager/federatedModulesBuildPluginImports.ts b/packages/analytics-js/src/components/pluginsManager/federatedModulesBuildPluginImports.ts index cd5105c8e..879a7b179 100644 --- a/packages/analytics-js/src/components/pluginsManager/federatedModulesBuildPluginImports.ts +++ b/packages/analytics-js/src/components/pluginsManager/federatedModulesBuildPluginImports.ts @@ -12,16 +12,12 @@ const getFederatedModuleImport = ( switch (pluginName) { case 'BeaconQueue': return () => import('rudderAnalyticsRemotePlugins/BeaconQueue'); - case 'Bugsnag': - return () => import('rudderAnalyticsRemotePlugins/Bugsnag'); case 'CustomConsentManager': return () => import('rudderAnalyticsRemotePlugins/CustomConsentManager'); case 'DeviceModeDestinations': return () => import('rudderAnalyticsRemotePlugins/DeviceModeDestinations'); case 'DeviceModeTransformation': return () => import('rudderAnalyticsRemotePlugins/DeviceModeTransformation'); - case 'ErrorReporting': - return () => import('rudderAnalyticsRemotePlugins/ErrorReporting'); case 'ExternalAnonymousId': return () => import('rudderAnalyticsRemotePlugins/ExternalAnonymousId'); case 'GoogleLinker': diff --git a/packages/analytics-js/src/components/pluginsManager/pluginNames.ts b/packages/analytics-js/src/components/pluginsManager/pluginNames.ts index 3814c1634..6120ecd9b 100644 --- a/packages/analytics-js/src/components/pluginsManager/pluginNames.ts +++ b/packages/analytics-js/src/components/pluginsManager/pluginNames.ts @@ -10,11 +10,9 @@ const localPluginNames: PluginName[] = []; */ const pluginNamesList: PluginName[] = [ 'BeaconQueue', - 'Bugsnag', // deprecated 'CustomConsentManager', 'DeviceModeDestinations', 'DeviceModeTransformation', - 'ErrorReporting', 'ExternalAnonymousId', 'GoogleLinker', 'IubendaConsentManager', @@ -27,6 +25,6 @@ const pluginNamesList: PluginName[] = [ 'XhrQueue', ]; -const deprecatedPluginsList = ['Bugsnag']; +const deprecatedPluginsList = ['Bugsnag', 'ErrorReporting']; export { localPluginNames, pluginNamesList, deprecatedPluginsList }; diff --git a/packages/analytics-js/src/constants/logMessages.ts b/packages/analytics-js/src/constants/logMessages.ts index 0020c5fdb..55cbdd869 100644 --- a/packages/analytics-js/src/constants/logMessages.ts +++ b/packages/analytics-js/src/constants/logMessages.ts @@ -8,6 +8,7 @@ import type { DeliveryType, StorageStrategy, } from '@rudderstack/analytics-js-common/types/LoadOptions'; +import type { Nullable } from '@rudderstack/analytics-js-common/types/Nullable'; // CONSTANT const SOURCE_CONFIG_OPTION_ERROR = `"getSourceConfig" must be a function. Please make sure that it is defined and returns a valid source configuration object.`; @@ -32,11 +33,17 @@ const UNSUPPORTED_CONSENT_MANAGER_ERROR = ( consentManagersToPluginNameMap, )}".`; -const REPORTING_PLUGIN_INIT_FAILURE_ERROR = (context: string): string => - `${context}${LOG_CONTEXT_SEPARATOR}Failed to initialize the error reporting plugin.`; +const NON_ERROR_WARNING = (context: string, errStr: Nullable): string => + `${context}${LOG_CONTEXT_SEPARATOR}Received a non-error: ${errStr}.`; -const NOTIFY_FAILURE_ERROR = (context: string): string => - `${context}${LOG_CONTEXT_SEPARATOR}Failed to notify the error.`; +const FAILED_ATTACH_LISTENERS_ERROR = (context: string): string => + `${context}${LOG_CONTEXT_SEPARATOR}Failed to attach global error listeners.`; + +const BREADCRUMB_ERROR = (context: string): string => + `${context}${LOG_CONTEXT_SEPARATOR}Failed to log breadcrumb.`; + +const HANDLE_ERROR_FAILURE = (context: string): string => + `${context}${LOG_CONTEXT_SEPARATOR}Failed to handle the error.`; const PLUGIN_NAME_MISSING_ERROR = (context: string): string => `${context}${LOG_CONTEXT_SEPARATOR}Plugin name is missing.`; @@ -267,8 +274,7 @@ export { TIMEOUT_NOT_RECOMMENDED_WARNING, INVALID_SESSION_ID_WARNING, DEPRECATED_PLUGIN_WARNING, - REPORTING_PLUGIN_INIT_FAILURE_ERROR, - NOTIFY_FAILURE_ERROR, + HANDLE_ERROR_FAILURE, PLUGIN_NAME_MISSING_ERROR, PLUGIN_ALREADY_EXISTS_ERROR, PLUGIN_NOT_FOUND_ERROR, @@ -317,4 +323,7 @@ export { SERVER_SIDE_COOKIE_FEATURE_OVERRIDE_WARNING, BAD_COOKIES_WARNING, PAGE_UNLOAD_ON_BEACON_DISABLED_WARNING, + BREADCRUMB_ERROR, + NON_ERROR_WARNING, + FAILED_ATTACH_LISTENERS_ERROR, }; diff --git a/packages/analytics-js/src/services/ErrorHandler/ErrorHandler.ts b/packages/analytics-js/src/services/ErrorHandler/ErrorHandler.ts index 7d2fbc5f4..8aea3e9a7 100644 --- a/packages/analytics-js/src/services/ErrorHandler/ErrorHandler.ts +++ b/packages/analytics-js/src/services/ErrorHandler/ErrorHandler.ts @@ -1,106 +1,61 @@ -import type { IPluginEngine } from '@rudderstack/analytics-js-common/types/PluginEngine'; -import { removeDoubleSpaces } from '@rudderstack/analytics-js-common/utilities/string'; -import { isTypeOfError } from '@rudderstack/analytics-js-common/utilities/checks'; +import { isUndefined } from '@rudderstack/analytics-js-common/utilities/checks'; import { ErrorType, type ErrorState, type IErrorHandler, - type PreLoadErrorData, type SDKError, } from '@rudderstack/analytics-js-common/types/ErrorHandler'; import type { ILogger } from '@rudderstack/analytics-js-common/types/Logger'; import { ERROR_HANDLER } from '@rudderstack/analytics-js-common/constants/loggerContexts'; import { LOG_CONTEXT_SEPARATOR } from '@rudderstack/analytics-js-common/constants/logMessages'; -import { BufferQueue } from '@rudderstack/analytics-js-common/services/BufferQueue/BufferQueue'; import type { IHttpClient } from '@rudderstack/analytics-js-common/types/HttpClient'; -import type { IExternalSrcLoader } from '@rudderstack/analytics-js-common/services/ExternalSrcLoader/types'; import { MANUAL_ERROR_IDENTIFIER } from '@rudderstack/analytics-js-common/utilities/errors'; import { - NOTIFY_FAILURE_ERROR, - REPORTING_PLUGIN_INIT_FAILURE_ERROR, + BREADCRUMB_ERROR, + FAILED_ATTACH_LISTENERS_ERROR, + HANDLE_ERROR_FAILURE, } from '../../constants/logMessages'; import { state } from '../../state'; -import { defaultPluginEngine } from '../PluginEngine'; import { defaultLogger } from '../Logger'; -import { getNormalizedErrorForUnhandledError, processError } from './processError'; +import { + createNewBreadcrumb, + getBugsnagErrorEvent, + getErrInstance, + getErrorDeliveryPayload, + isAllowedToBeNotified, + isRudderSDKError, +} from './utils'; +import { createBugsnagException, normalizeError } from './event/event'; +import { defaultHttpClient } from '../HttpClient'; /** * A service to handle errors */ class ErrorHandler implements IErrorHandler { + httpClient: IHttpClient; logger?: ILogger; - pluginEngine?: IPluginEngine; - httpClient?: IHttpClient; - errReportingClient?: any; - errorBuffer: BufferQueue; // If no logger is passed errors will be thrown as unhandled error - constructor(logger?: ILogger, pluginEngine?: IPluginEngine) { + constructor(httpClient: IHttpClient, logger?: ILogger) { + this.httpClient = httpClient; this.logger = logger; - this.pluginEngine = pluginEngine; - this.errorBuffer = new BufferQueue(); - this.attachEffect(); - } - - attachEffect() { - if (state.reporting.isErrorReportingPluginLoaded.value === true) { - while (this.errorBuffer.size() > 0) { - const errorToProcess = this.errorBuffer.dequeue(); - - if (errorToProcess) { - // send it to the plugin - this.notifyError(errorToProcess.error, errorToProcess.errorState); - } - } - } + this.attachErrorListeners(); } attachErrorListeners() { if ('addEventListener' in (globalThis as typeof window)) { (globalThis as typeof window).addEventListener('error', (event: ErrorEvent | Event) => { - this.onError(event, undefined, undefined, undefined, ErrorType.UNHANDLEDEXCEPTION); + this.onError(event, undefined, undefined, ErrorType.UNHANDLEDEXCEPTION); }); (globalThis as typeof window).addEventListener( 'unhandledrejection', (event: PromiseRejectionEvent) => { - this.onError(event, undefined, undefined, undefined, ErrorType.UNHANDLEDREJECTION); + this.onError(event, undefined, undefined, ErrorType.UNHANDLEDREJECTION); }, ); } else { - this.logger?.debug(`Failed to attach global error listeners.`); - } - } - - init(httpClient: IHttpClient, externalSrcLoader: IExternalSrcLoader) { - this.httpClient = httpClient; - // Below lines are only kept for backward compatibility - // TODO: Remove this in the next major release - if (!this.pluginEngine) { - return; - } - - try { - const extPoint = 'errorReporting.init'; - const errReportingInitVal = this.pluginEngine.invokeSingle( - extPoint, - state, - this.pluginEngine, - externalSrcLoader, - this.logger, - true, - ); - if (errReportingInitVal instanceof Promise) { - errReportingInitVal - .then((client: any) => { - this.errReportingClient = client; - }) - .catch(err => { - this.logger?.error(REPORTING_PLUGIN_INIT_FAILURE_ERROR(ERROR_HANDLER), err); - }); - } - } catch (err) { - this.onError(err, ERROR_HANDLER); + this.logger?.error(FAILED_ATTACH_LISTENERS_ERROR(ERROR_HANDLER)); } } @@ -108,69 +63,60 @@ class ErrorHandler implements IErrorHandler { error: SDKError, context = '', customMessage = '', - shouldAlwaysThrow = false, errorType = ErrorType.HANDLEDEXCEPTION, ) { - let normalizedError; - let errorMessage; - if (errorType === ErrorType.HANDLEDEXCEPTION) { - errorMessage = processError(error); - - // If no error message after we normalize, then we swallow/ignore the errors - if (!errorMessage) { + try { + const errInstance = getErrInstance(error, errorType); + const normalizedError = normalizeError(errInstance, this.logger); + if (isUndefined(normalizedError)) { return; } - errorMessage = removeDoubleSpaces( - `${context}${LOG_CONTEXT_SEPARATOR}${customMessage} ${errorMessage}`, - ); + const errorMsgPrefix = `${context}${LOG_CONTEXT_SEPARATOR}${customMessage} `; + const bsException = createBugsnagException(normalizedError, errorMsgPrefix); - normalizedError = new Error(errorMessage); - if (isTypeOfError(error)) { - normalizedError = Object.create(error, { - message: { value: errorMessage }, - }); + // filter errors + if (!isRudderSDKError(bsException)) { + return; } - } else { - normalizedError = getNormalizedErrorForUnhandledError(error); - } - const isErrorReportingEnabled = state.reporting.isErrorReportingEnabled.value; - const isErrorReportingPluginLoaded = state.reporting.isErrorReportingPluginLoaded.value; - try { - if (isErrorReportingEnabled) { + if (state.reporting.isErrorReportingEnabled.value && isAllowedToBeNotified(bsException)) { const errorState: ErrorState = { severity: 'error', unhandled: errorType !== ErrorType.HANDLEDEXCEPTION, severityReason: { type: errorType }, }; - if (!isErrorReportingPluginLoaded) { - // buffer the error - this.errorBuffer.enqueue({ - error: normalizedError, - errorState, - }); - } else if (normalizedError) { - this.notifyError(normalizedError, errorState); - } + // enrich error payload + const bugsnagPayload = getBugsnagErrorEvent(bsException, errorState, state); + + // send it to metrics service + this.httpClient?.getAsyncData({ + url: state.metrics.metricsServiceUrl.value as string, + options: { + method: 'POST', + data: getErrorDeliveryPayload(bugsnagPayload, state), + sendRawData: true, + }, + isRawResponse: true, + }); } - } catch (e) { - this.logger?.error(NOTIFY_FAILURE_ERROR(ERROR_HANDLER), e); - } - if (errorType === ErrorType.HANDLEDEXCEPTION) { - if (this.logger) { - this.logger.error(errorMessage); - - if (shouldAlwaysThrow) { - throw normalizedError; - } - } else { - throw normalizedError; + // Log only the handled exceptions to the console + // Unhandled exceptions are already logged by the browser + if (errorType === ErrorType.HANDLEDEXCEPTION) { + this.logger?.error( + Object.create(normalizedError, { + message: { value: bsException.message }, + }), + ); + // Log special errors thrown by the SDK + } else if ((error as any).error?.stack?.includes(MANUAL_ERROR_IDENTIFIER)) { + this.logger?.error('An unknown error occurred:', (error as ErrorEvent).error?.message); } - } else if ((error as any).error?.stack?.includes(MANUAL_ERROR_IDENTIFIER)) { - this.logger?.error('An unknown error occurred:', (error as ErrorEvent).error?.message); + } catch (err) { + // If an error occurs while handling an error, log it + this.logger?.error(HANDLE_ERROR_FAILURE(ERROR_HANDLER), err); } } @@ -181,48 +127,17 @@ class ErrorHandler implements IErrorHandler { * @param {string} breadcrumb breadcrumbs message */ leaveBreadcrumb(breadcrumb: string) { - if (this.pluginEngine) { - try { - this.pluginEngine.invokeSingle( - 'errorReporting.breadcrumb', - this.pluginEngine, // deprecated parameter - this.errReportingClient, // deprecated parameter - breadcrumb, - this.logger, - state, - ); - } catch (err) { - this.onError(err, ERROR_HANDLER, 'errorReporting.breadcrumb'); - } - } - } - - /** - * Send handled errors to external error monitoring service via a plugin - * - * @param {Error} error Error instance from handled error - */ - notifyError(error: SDKError, errorState: ErrorState) { - if (this.pluginEngine && this.httpClient) { - try { - this.pluginEngine.invokeSingle( - 'errorReporting.notify', - this.pluginEngine, // deprecated parameter - this.errReportingClient, // deprecated parameter - error, - state, - this.logger, - this.httpClient, - errorState, - ); - } catch (err) { - // Not calling onError here as we don't want to go into infinite loop - this.logger?.error(NOTIFY_FAILURE_ERROR(ERROR_HANDLER), err); - } + try { + state.reporting.breadcrumbs.value = [ + ...state.reporting.breadcrumbs.value, + createNewBreadcrumb(breadcrumb), + ]; + } catch (err) { + this.onError(err, BREADCRUMB_ERROR(ERROR_HANDLER)); } } } -const defaultErrorHandler = new ErrorHandler(defaultLogger, defaultPluginEngine); +const defaultErrorHandler = new ErrorHandler(defaultHttpClient, defaultLogger); export { ErrorHandler, defaultErrorHandler }; diff --git a/packages/analytics-js/src/services/ErrorHandler/constant.ts b/packages/analytics-js/src/services/ErrorHandler/constant.ts index eec8a8b7c..1b480fc55 100644 --- a/packages/analytics-js/src/services/ErrorHandler/constant.ts +++ b/packages/analytics-js/src/services/ErrorHandler/constant.ts @@ -1,3 +1,35 @@ -const LOAD_ORIGIN = 'RS_JS_SDK'; +// Errors from the below scripts are NOT allowed to reach Bugsnag +const SDK_FILE_NAME_PREFIXES = (): string[] => [ + 'rsa', // Prefix for all the SDK scripts including plugins and module federated chunks +]; -export { LOAD_ORIGIN }; +const DEV_HOSTS = ['www.test-host.com', 'localhost', '127.0.0.1', '[::1]']; + +// List of keys to exclude from the metadata +// Potential PII or sensitive data +const APP_STATE_EXCLUDE_KEYS = [ + 'userId', + 'userTraits', + 'groupId', + 'groupTraits', + 'anonymousId', + 'config', + 'instance', // destination instance objects + 'eventBuffer', // pre-load event buffer (may contain PII) + 'traits', + 'authToken', +]; +const REQUEST_TIMEOUT_MS = 10 * 1000; // 10 seconds +const NOTIFIER_NAME = 'RudderStack JavaScript SDK'; +const SDK_GITHUB_URL = '__REPOSITORY_URL__'; +const SOURCE_NAME = 'js'; + +export { + SDK_FILE_NAME_PREFIXES, + DEV_HOSTS, + APP_STATE_EXCLUDE_KEYS, + REQUEST_TIMEOUT_MS, + NOTIFIER_NAME, + SDK_GITHUB_URL, + SOURCE_NAME, +}; diff --git a/packages/analytics-js-plugins/src/errorReporting/constants.ts b/packages/analytics-js/src/services/ErrorHandler/constants.ts similarity index 79% rename from packages/analytics-js-plugins/src/errorReporting/constants.ts rename to packages/analytics-js/src/services/ErrorHandler/constants.ts index 6fa61398c..2a7b7e1c1 100644 --- a/packages/analytics-js-plugins/src/errorReporting/constants.ts +++ b/packages/analytics-js/src/services/ErrorHandler/constants.ts @@ -20,10 +20,11 @@ const APP_STATE_EXCLUDE_KEYS = [ 'authToken', ]; 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 NOTIFIER_NAME = 'RudderStack JavaScript SDK'; +const SDK_GITHUB_URL = '__REPOSITORY_URL__'; const SOURCE_NAME = 'js'; -const ERROR_REPORTING_PLUGIN = 'ErrorReportingPlugin'; + +const ERROR_MESSAGES_TO_BE_FILTERED: string[] = []; export { SDK_FILE_NAME_PREFIXES, @@ -33,5 +34,5 @@ export { NOTIFIER_NAME, SDK_GITHUB_URL, SOURCE_NAME, - ERROR_REPORTING_PLUGIN, + ERROR_MESSAGES_TO_BE_FILTERED, }; diff --git a/packages/analytics-js-plugins/src/errorReporting/event/LICENSE.txt b/packages/analytics-js/src/services/ErrorHandler/event/LICENSE.txt similarity index 100% rename from packages/analytics-js-plugins/src/errorReporting/event/LICENSE.txt rename to packages/analytics-js/src/services/ErrorHandler/event/LICENSE.txt diff --git a/packages/analytics-js/src/services/ErrorHandler/event/event.ts b/packages/analytics-js/src/services/ErrorHandler/event/event.ts new file mode 100644 index 000000000..6a19884da --- /dev/null +++ b/packages/analytics-js/src/services/ErrorHandler/event/event.ts @@ -0,0 +1,101 @@ +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 { ERROR_HANDLER } from '@rudderstack/analytics-js-common/constants/loggerContexts'; +import { stringifyWithoutCircular } from '@rudderstack/analytics-js-common/utilities/json'; +import { isString } from '@rudderstack/analytics-js-common/utilities/checks'; +import { NON_ERROR_WARNING } from '../../../constants/logMessages'; +import type { FrameType } from './types'; + +const GLOBAL_CODE = 'global code'; + +const normaliseFunctionName = (name: string) => (/^global code$/i.test(name) ? GLOBAL_CODE : name); + +/** + * Takes a stacktrace.js style stackframe (https://github.com/stacktracejs/stackframe) + * and returns a Bugsnag compatible stackframe (https://docs.bugsnag.com/api/error-reporting/#json-payload) + * @param frame + * @returns + */ +const formatStackframe = (frame: FrameType): Stackframe => { + const f = { + file: frame.fileName, + method: normaliseFunctionName(frame.functionName), + lineNumber: frame.lineNumber, + columnNumber: frame.columnNumber, + }; + // Some instances result in no file: + // - calling notify() from chrome's terminal results in no file/method. + // - non-error exception thrown from global code in FF + // This adds one. + if (f.lineNumber > -1 && !f.file && !f.method) { + f.file = GLOBAL_CODE; + } + return f; +}; + +const ensureString = (str: any) => (isString(str) ? str : ''); + +function createException( + errorClass: string, + errorMessage: string, + msgPrefix: string, + stacktrace: any[], +): Exception { + return { + errorClass: ensureString(errorClass), + message: `${msgPrefix}${ensureString(errorMessage)}`, + type: 'browserjs', + stacktrace: stacktrace.reduce((accum: Stackframe[], frame: FrameType) => { + const f = formatStackframe(frame); + // don't include a stackframe if none of its properties are defined + try { + if (JSON.stringify(f) === '{}') return accum; + return accum.concat(f); + } catch { + return accum; + } + }, []), + }; +} + +const hasStack = (err: any) => + !!err && + (!!err.stack || !!err.stacktrace || !!err['opera#sourceloc']) && + typeof (err.stack || err.stacktrace || err['opera#sourceloc']) === 'string' && + err.stack !== `${err.name}: ${err.message}`; + +const isError = (value: any) => { + switch (Object.prototype.toString.call(value)) { + case '[object Error]': + case '[object Exception]': + case '[object DOMException]': + return true; + default: + return value instanceof Error; + } +}; + +const normalizeError = (maybeError: any, logger?: ILogger): any | undefined => { + let error; + + if (isError(maybeError) && hasStack(maybeError)) { + error = maybeError; + } else { + logger?.warn(NON_ERROR_WARNING(ERROR_HANDLER, stringifyWithoutCircular(error))); + error = undefined; + } + + return error; +}; + +const createBugsnagException = (error: any, msgPrefix: string): Exception => { + try { + const stacktrace = ErrorStackParser.parse(error); + return createException(error.name, error.message, msgPrefix, stacktrace); + } catch { + return createException(error.name, error.message, msgPrefix, []); + } +}; + +export { normalizeError, createBugsnagException }; diff --git a/packages/analytics-js/src/services/ErrorHandler/event/types.ts b/packages/analytics-js/src/services/ErrorHandler/event/types.ts new file mode 100644 index 000000000..90dbeb2fd --- /dev/null +++ b/packages/analytics-js/src/services/ErrorHandler/event/types.ts @@ -0,0 +1,6 @@ +export type FrameType = { + fileName: string; + functionName: string; + lineNumber: number; + columnNumber: number; +}; diff --git a/packages/analytics-js/src/services/ErrorHandler/processError.ts b/packages/analytics-js/src/services/ErrorHandler/processError.ts deleted file mode 100644 index bd5588bff..000000000 --- a/packages/analytics-js/src/services/ErrorHandler/processError.ts +++ /dev/null @@ -1,66 +0,0 @@ -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 { LOAD_ORIGIN } from './constant'; - -/** - * Utility method to normalise errors - */ -const processError = (error: SDKError): string => { - let errorMessage; - - try { - if (isString(error)) { - errorMessage = error; - } else if (error instanceof Error) { - errorMessage = error.message; - } else if (error instanceof ErrorEvent) { - errorMessage = error.message; - } else { - errorMessage = (error as any).message - ? (error as any).message - : stringifyWithoutCircular(error as Record); - } - } catch (e) { - errorMessage = `Unknown error: ${(e as Error).message}`; - } - - return errorMessage; -}; - -const getNormalizedErrorForUnhandledError = (error: SDKError): SDKError | undefined => { - try { - if ( - error instanceof Error || - error instanceof ErrorEvent || - (error instanceof PromiseRejectionEvent && error.reason) - ) { - 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 error; - } catch (e) { - return e; - } -}; - -export { processError, getNormalizedErrorForUnhandledError }; diff --git a/packages/analytics-js-plugins/src/errorReporting/utils.ts b/packages/analytics-js/src/services/ErrorHandler/utils.ts similarity index 50% rename from packages/analytics-js-plugins/src/errorReporting/utils.ts rename to packages/analytics-js/src/services/ErrorHandler/utils.ts index ef8fadc3b..0c77d2041 100644 --- a/packages/analytics-js-plugins/src/errorReporting/utils.ts +++ b/packages/analytics-js/src/services/ErrorHandler/utils.ts @@ -1,7 +1,6 @@ import type { ApplicationState, Breadcrumb, - BreadcrumbMetaData, } from '@rudderstack/analytics-js-common/types/ApplicationState'; import { ErrorType, @@ -11,55 +10,43 @@ import { import { clone } from 'ramda'; import type { ErrorEventPayload, + Exception, MetricServicePayload, } from '@rudderstack/analytics-js-common/types/Metrics'; +import { stringifyWithoutCircular } from '@rudderstack/analytics-js-common/utilities/json'; +import { CDN_INT_DIR } from '@rudderstack/analytics-js-common/constants/urls'; +import { generateUUID } from '@rudderstack/analytics-js-common/utilities/uuId'; +import { METRICS_PAYLOAD_VERSION } from '@rudderstack/analytics-js-common/constants/metrics'; import { APP_STATE_EXCLUDE_KEYS, DEV_HOSTS, + ERROR_MESSAGES_TO_BE_FILTERED, + NOTIFIER_NAME, SDK_FILE_NAME_PREFIXES, SDK_GITHUB_URL, - NOTIFIER_NAME, SOURCE_NAME, } from './constants'; -import { - CDN_INT_DIR, - ERROR_MESSAGES_TO_BE_FILTERED, - generateUUID, - METRICS_PAYLOAD_VERSION, - stringifyWithoutCircular, -} from '../shared-chunks/common'; -import type { ErrorFormat } from './event/event'; -const getConfigForPayloadCreation = (err: SDKError, errorType: string) => { +const getErrInstance = (err: SDKError, errorType: string) => { switch (errorType) { case ErrorType.UNHANDLEDEXCEPTION: { const { error } = err as ErrorEvent; - return { - component: 'unhandledException handler', - normalizedError: error || err, - }; + return error || err; } case ErrorType.UNHANDLEDREJECTION: { - const error = err as PromiseRejectionEvent; - return { - component: 'unhandledrejection handler', - normalizedError: error.reason, - }; + return (err as PromiseRejectionEvent).reason; } case ErrorType.HANDLEDEXCEPTION: default: - return { - component: 'notify()', - normalizedError: err, - }; + return err; } }; -const createNewBreadcrumb = (message: string, metaData?: BreadcrumbMetaData): Breadcrumb => ({ +const createNewBreadcrumb = (message: string): Breadcrumb => ({ type: 'manual', name: message, timestamp: new Date(), - metaData: metaData ?? {}, + metaData: {}, }); const getReleaseStage = () => { @@ -77,88 +64,76 @@ const getURLWithoutQueryString = () => { return url[0]; }; -const getErrorContext = (event: any) => { - const { message } = event; - let context = message; - - // Hack for easily grouping the script load errors - // on the dashboard - if (message.includes('Error in loading a third-party script')) { - context = 'Script load failures'; - } - return context; -}; - const getBugsnagErrorEvent = ( - payload: ErrorFormat, + exception: Exception, errorState: ErrorState, state: ApplicationState, -): ErrorEventPayload => ({ - notifier: { - name: NOTIFIER_NAME, - version: state.context.app.value.version, - url: SDK_GITHUB_URL, - }, - events: [ - { - payloadVersion: '5', - exceptions: clone(payload.errors), - severity: errorState.severity, - unhandled: errorState.unhandled, - severityReason: errorState.severityReason, - app: { - version: state.context.app.value.version, - releaseStage: getReleaseStage(), - }, - device: { - locale: state.context.locale.value ?? undefined, - userAgent: state.context.userAgent.value ?? undefined, - time: new Date(), - }, - request: { - url: getURLWithoutQueryString() as string, - clientIp: '[NOT COLLECTED]', - }, - breadcrumbs: clone(state.reporting.breadcrumbs.value), - context: getErrorContext(payload.errors[0]), - metaData: { - sdk: { - name: 'JS', - installType: state.context.app.value.installType, +): ErrorEventPayload => { + const { context, lifecycle, session, source, reporting, autoTrack } = state; + const { app, locale, userAgent, timezone, screen, library } = context; + + return { + payloadVersion: '5', + notifier: { + name: NOTIFIER_NAME, + version: app.value.version, + url: SDK_GITHUB_URL, + }, + events: [ + { + exceptions: [clone(exception)], + severity: errorState.severity, + unhandled: errorState.unhandled, + severityReason: errorState.severityReason, + app: { + version: app.value.version, + releaseStage: getReleaseStage(), + type: app.value.installType, }, - state: getAppStateForMetadata(state) ?? {}, - source: { - snippetVersion: (globalThis as typeof window).RudderSnippetVersion, + device: { + locale: locale.value ?? undefined, + userAgent: userAgent.value ?? undefined, + time: new Date(), + }, + request: { + url: getURLWithoutQueryString() as string, + clientIp: '[NOT COLLECTED]', + }, + breadcrumbs: clone(reporting.breadcrumbs.value), + metaData: { + app: { + snippetVersion: library.value.snippetVersion, + }, + device: { ...screen.value, timezone: timezone.value }, + // Add rest of the state groups as metadata + // so that they show up as separate tabs in the dashboard + ...getAppStateForMetadata(state), + }, + user: { + // Combination of source, session and visit ids + id: `${source.value?.id ?? (lifecycle.writeKey.value as string)}..${session.sessionInfo.value?.id ?? 'NA'}..${autoTrack?.pageLifecycle?.visitId?.value ?? 'NA'}`, + name: source.value?.name ?? 'NA', }, }, - user: { - // Combination of source, session and visit ids - id: `${state.source.value?.id ?? (state.lifecycle.writeKey.value as string)}..${state.session.sessionInfo.value?.id ?? 'NA'}..${state.autoTrack?.pageLifecycle?.visitId?.value ?? 'NA'}`, - }, - }, - ], -}); + ], + }; +}; /** * A function to determine whether the error should be promoted to notify or not - * @param {Error} error + * @param {Error} exception * @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; -}; +const isAllowedToBeNotified = (exception: Exception) => + !ERROR_MESSAGES_TO_BE_FILTERED.some(e => exception.message.includes(e)); /** * A function to determine if the error is from Rudder SDK - * @param {Error} event + * @param {Error} exception * @returns */ -const isRudderSDKError = (event: any) => { - const errorOrigin = event.stacktrace?.[0]?.file; +const isRudderSDKError = (exception: Exception) => { + const errorOrigin = exception.stacktrace?.[0]?.file; if (!errorOrigin || typeof errorOrigin !== 'string') { return false; @@ -194,7 +169,7 @@ const getErrorDeliveryPayload = (payload: ErrorEventPayload, state: ApplicationS }; export { - getConfigForPayloadCreation, + getErrInstance, createNewBreadcrumb, getReleaseStage, getAppStateForMetadata, @@ -202,6 +177,5 @@ export { getURLWithoutQueryString, isRudderSDKError, getErrorDeliveryPayload, - getErrorContext, isAllowedToBeNotified, }; diff --git a/packages/analytics-js/src/services/HttpClient/HttpClient.ts b/packages/analytics-js/src/services/HttpClient/HttpClient.ts index ee3f7e3ad..45fd65045 100644 --- a/packages/analytics-js/src/services/HttpClient/HttpClient.ts +++ b/packages/analytics-js/src/services/HttpClient/HttpClient.ts @@ -9,7 +9,6 @@ import type { IErrorHandler } from '@rudderstack/analytics-js-common/types/Error import type { ILogger } from '@rudderstack/analytics-js-common/types/Logger'; import { toBase64 } from '@rudderstack/analytics-js-common/utilities/string'; import { HTTP_CLIENT } from '@rudderstack/analytics-js-common/constants/loggerContexts'; -import { defaultErrorHandler } from '../ErrorHandler'; import { defaultLogger } from '../Logger'; import { responseTextToJson } from './xhr/xhrResponseHandler'; import { createXhrRequestOptions, xhrRequest } from './xhr/xhrRequestHandler'; @@ -25,13 +24,16 @@ class HttpClient implements IHttpClient { basicAuthHeader?: string; hasErrorHandler = false; - constructor(errorHandler?: IErrorHandler, logger?: ILogger) { - this.errorHandler = errorHandler; + constructor(logger?: ILogger) { this.logger = logger; - this.hasErrorHandler = Boolean(this.errorHandler); this.onError = this.onError.bind(this); } + init(errorHandler: IErrorHandler) { + this.errorHandler = errorHandler; + this.hasErrorHandler = true; + } + /** * Implement requests in a blocking way */ @@ -107,6 +109,6 @@ class HttpClient implements IHttpClient { } } -const defaultHttpClient = new HttpClient(defaultErrorHandler, defaultLogger); +const defaultHttpClient = new HttpClient(defaultLogger); export { HttpClient, defaultHttpClient }; diff --git a/packages/analytics-js/src/types/remote-plugins.d.ts b/packages/analytics-js/src/types/remote-plugins.d.ts index c6f49d12b..4891446ad 100644 --- a/packages/analytics-js/src/types/remote-plugins.d.ts +++ b/packages/analytics-js/src/types/remote-plugins.d.ts @@ -1,9 +1,7 @@ declare module 'rudderAnalyticsRemotePlugins/BeaconQueue'; -declare module 'rudderAnalyticsRemotePlugins/Bugsnag'; declare module 'rudderAnalyticsRemotePlugins/CustomConsentManager'; declare module 'rudderAnalyticsRemotePlugins/DeviceModeDestinations'; declare module 'rudderAnalyticsRemotePlugins/DeviceModeTransformation'; -declare module 'rudderAnalyticsRemotePlugins/ErrorReporting'; declare module 'rudderAnalyticsRemotePlugins/ExternalAnonymousId'; declare module 'rudderAnalyticsRemotePlugins/GoogleLinker'; declare module 'rudderAnalyticsRemotePlugins/IubendaConsentManager'; From b93cb5a6d02747b7333b5e74cf79e998cfe99537 Mon Sep 17 00:00:00 2001 From: Sai Kumar Battinoju Date: Thu, 2 Jan 2025 10:49:43 +0530 Subject: [PATCH 02/53] chore: remove unnecessary console statement in test suite --- .../analytics-js/__tests__/services/ErrorHandler/utils.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/analytics-js/__tests__/services/ErrorHandler/utils.test.ts b/packages/analytics-js/__tests__/services/ErrorHandler/utils.test.ts index 3eeb96a7a..3d09f662b 100644 --- a/packages/analytics-js/__tests__/services/ErrorHandler/utils.test.ts +++ b/packages/analytics-js/__tests__/services/ErrorHandler/utils.test.ts @@ -435,7 +435,6 @@ describe('Error Reporting utilities', () => { (window as any).RudderSnippetVersion = 'sample_snippet_version'; const enhancedError = getBugsnagErrorEvent(errorPayload, errorState, state); - console.log(JSON.stringify(enhancedError)); const expectedOutcome = { notifier: { name: 'RudderStack JavaScript SDK Error Notifier', From 3026f5f69172e0f1b1c814a83bb19e223dbdcd72 Mon Sep 17 00:00:00 2001 From: Sai Kumar Battinoju Date: Thu, 2 Jan 2025 10:50:05 +0530 Subject: [PATCH 03/53] chore: delete duplicate constants file --- .../src/services/ErrorHandler/constant.ts | 35 ------------------- 1 file changed, 35 deletions(-) delete mode 100644 packages/analytics-js/src/services/ErrorHandler/constant.ts diff --git a/packages/analytics-js/src/services/ErrorHandler/constant.ts b/packages/analytics-js/src/services/ErrorHandler/constant.ts deleted file mode 100644 index 1b480fc55..000000000 --- a/packages/analytics-js/src/services/ErrorHandler/constant.ts +++ /dev/null @@ -1,35 +0,0 @@ -// Errors from the below scripts are NOT allowed to reach Bugsnag -const SDK_FILE_NAME_PREFIXES = (): string[] => [ - 'rsa', // Prefix for all the SDK scripts including plugins and module federated chunks -]; - -const DEV_HOSTS = ['www.test-host.com', 'localhost', '127.0.0.1', '[::1]']; - -// List of keys to exclude from the metadata -// Potential PII or sensitive data -const APP_STATE_EXCLUDE_KEYS = [ - 'userId', - 'userTraits', - 'groupId', - 'groupTraits', - 'anonymousId', - 'config', - 'instance', // destination instance objects - 'eventBuffer', // pre-load event buffer (may contain PII) - 'traits', - 'authToken', -]; -const REQUEST_TIMEOUT_MS = 10 * 1000; // 10 seconds -const NOTIFIER_NAME = 'RudderStack JavaScript SDK'; -const SDK_GITHUB_URL = '__REPOSITORY_URL__'; -const SOURCE_NAME = 'js'; - -export { - SDK_FILE_NAME_PREFIXES, - DEV_HOSTS, - APP_STATE_EXCLUDE_KEYS, - REQUEST_TIMEOUT_MS, - NOTIFIER_NAME, - SDK_GITHUB_URL, - SOURCE_NAME, -}; From 9b0f51434380deac5a79bf914ed662141e809909 Mon Sep 17 00:00:00 2001 From: Sai Kumar Battinoju Date: Thu, 2 Jan 2025 10:51:02 +0530 Subject: [PATCH 04/53] chore: restore error messages filtering logic --- packages/analytics-js/src/services/ErrorHandler/constants.ts | 3 --- packages/analytics-js/src/services/ErrorHandler/utils.ts | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/analytics-js/src/services/ErrorHandler/constants.ts b/packages/analytics-js/src/services/ErrorHandler/constants.ts index 2a7b7e1c1..1b480fc55 100644 --- a/packages/analytics-js/src/services/ErrorHandler/constants.ts +++ b/packages/analytics-js/src/services/ErrorHandler/constants.ts @@ -24,8 +24,6 @@ const NOTIFIER_NAME = 'RudderStack JavaScript SDK'; const SDK_GITHUB_URL = '__REPOSITORY_URL__'; const SOURCE_NAME = 'js'; -const ERROR_MESSAGES_TO_BE_FILTERED: string[] = []; - export { SDK_FILE_NAME_PREFIXES, DEV_HOSTS, @@ -34,5 +32,4 @@ export { NOTIFIER_NAME, SDK_GITHUB_URL, SOURCE_NAME, - ERROR_MESSAGES_TO_BE_FILTERED, }; diff --git a/packages/analytics-js/src/services/ErrorHandler/utils.ts b/packages/analytics-js/src/services/ErrorHandler/utils.ts index 0c77d2041..61acd94ef 100644 --- a/packages/analytics-js/src/services/ErrorHandler/utils.ts +++ b/packages/analytics-js/src/services/ErrorHandler/utils.ts @@ -17,10 +17,10 @@ import { stringifyWithoutCircular } from '@rudderstack/analytics-js-common/utili import { CDN_INT_DIR } from '@rudderstack/analytics-js-common/constants/urls'; import { generateUUID } from '@rudderstack/analytics-js-common/utilities/uuId'; import { METRICS_PAYLOAD_VERSION } from '@rudderstack/analytics-js-common/constants/metrics'; +import { ERROR_MESSAGES_TO_BE_FILTERED } from '@rudderstack/analytics-js-common/constants/errors'; import { APP_STATE_EXCLUDE_KEYS, DEV_HOSTS, - ERROR_MESSAGES_TO_BE_FILTERED, NOTIFIER_NAME, SDK_FILE_NAME_PREFIXES, SDK_GITHUB_URL, From 3fd7a5aa4b035408ba06c1c5de29364138f1c88b Mon Sep 17 00:00:00 2001 From: Sai Kumar Battinoju Date: Thu, 2 Jan 2025 10:51:44 +0530 Subject: [PATCH 05/53] refactor: move code around to the right places --- .../src/utilities/checks.ts | 17 ++++++++--- .../src/utilities/errors.ts | 17 ++++++++--- .../src/services/ErrorHandler/event/event.ts | 28 ++++--------------- 3 files changed, 32 insertions(+), 30 deletions(-) diff --git a/packages/analytics-js-common/src/utilities/checks.ts b/packages/analytics-js-common/src/utilities/checks.ts index fc973a34c..310a0f353 100644 --- a/packages/analytics-js-common/src/utilities/checks.ts +++ b/packages/analytics-js-common/src/utilities/checks.ts @@ -65,11 +65,20 @@ const isDefinedNotNullAndNotEmptyString = (value: any): boolean => isDefinedAndNotNull(value) && value !== ''; /** - * Determines if the input is an instance of Error - * @param obj input value - * @returns true if the input is an instance of Error and false otherwise + * Determines if the input is of type error + * @param value input value + * @returns true if the input is of type error else false */ -const isTypeOfError = (obj: any): obj is Error => obj instanceof Error; +const isTypeOfError = (value: any): boolean => { + switch (Object.prototype.toString.call(value)) { + case '[object Error]': + case '[object Exception]': + case '[object DOMException]': + return true; + default: + return value instanceof Error; + } +}; export { isFunction, diff --git a/packages/analytics-js-common/src/utilities/errors.ts b/packages/analytics-js-common/src/utilities/errors.ts index e9fac17a8..691f256ea 100644 --- a/packages/analytics-js-common/src/utilities/errors.ts +++ b/packages/analytics-js-common/src/utilities/errors.ts @@ -1,7 +1,13 @@ import { isTypeOfError } from './checks'; import { stringifyWithoutCircular } from './json'; -const MANUAL_ERROR_IDENTIFIER = '[MANUAL ERROR]'; +const MANUAL_ERROR_IDENTIFIER = '[MANUALLY DISPATCHED ERROR]'; + +const hasStack = (err: any) => + !!err && + (!!err.stack || !!err.stacktrace || !!err['opera#sourceloc']) && + typeof (err.stack || err.stacktrace || err['opera#sourceloc']) === 'string' && + err.stack !== `${err.name}: ${err.message}`; /** * Get mutated error with issue prepended to error message @@ -20,10 +26,13 @@ const getMutatedError = (err: any, issue: string): Error => { }; const dispatchErrorEvent = (error: any) => { - if (isTypeOfError(error)) { - error.stack = `${error.stack ?? ''}\n${MANUAL_ERROR_IDENTIFIER}`; + if (isTypeOfError(error) && hasStack(error)) { + let stack = error.stack ?? error.stacktrace ?? error['opera#sourceloc'] ?? ''; + stack = `${stack}\n${MANUAL_ERROR_IDENTIFIER}`; + // eslint-disable-next-line no-param-reassign + error.stack = stack; } (globalThis as typeof window).dispatchEvent(new ErrorEvent('error', { error })); }; -export { getMutatedError, dispatchErrorEvent, MANUAL_ERROR_IDENTIFIER }; +export { getMutatedError, dispatchErrorEvent, MANUAL_ERROR_IDENTIFIER, hasStack }; diff --git a/packages/analytics-js/src/services/ErrorHandler/event/event.ts b/packages/analytics-js/src/services/ErrorHandler/event/event.ts index 6a19884da..08d06779c 100644 --- a/packages/analytics-js/src/services/ErrorHandler/event/event.ts +++ b/packages/analytics-js/src/services/ErrorHandler/event/event.ts @@ -3,13 +3,14 @@ import ErrorStackParser from 'error-stack-parser'; import type { Exception, Stackframe } from '@rudderstack/analytics-js-common/types/Metrics'; import { ERROR_HANDLER } from '@rudderstack/analytics-js-common/constants/loggerContexts'; import { stringifyWithoutCircular } from '@rudderstack/analytics-js-common/utilities/json'; -import { isString } from '@rudderstack/analytics-js-common/utilities/checks'; +import { isString, isTypeOfError } from '@rudderstack/analytics-js-common/utilities/checks'; +import { hasStack } from '@rudderstack/analytics-js-common/utilities/errors'; import { NON_ERROR_WARNING } from '../../../constants/logMessages'; import type { FrameType } from './types'; const GLOBAL_CODE = 'global code'; -const normaliseFunctionName = (name: string) => (/^global code$/i.test(name) ? GLOBAL_CODE : name); +const normalizeFunctionName = (name: string) => (/^global code$/i.test(name) ? GLOBAL_CODE : name); /** * Takes a stacktrace.js style stackframe (https://github.com/stacktracejs/stackframe) @@ -20,7 +21,7 @@ const normaliseFunctionName = (name: string) => (/^global code$/i.test(name) ? G const formatStackframe = (frame: FrameType): Stackframe => { const f = { file: frame.fileName, - method: normaliseFunctionName(frame.functionName), + method: normalizeFunctionName(frame.functionName), lineNumber: frame.lineNumber, columnNumber: frame.columnNumber, }; @@ -59,30 +60,13 @@ function createException( }; } -const hasStack = (err: any) => - !!err && - (!!err.stack || !!err.stacktrace || !!err['opera#sourceloc']) && - typeof (err.stack || err.stacktrace || err['opera#sourceloc']) === 'string' && - err.stack !== `${err.name}: ${err.message}`; - -const isError = (value: any) => { - switch (Object.prototype.toString.call(value)) { - case '[object Error]': - case '[object Exception]': - case '[object DOMException]': - return true; - default: - return value instanceof Error; - } -}; - const normalizeError = (maybeError: any, logger?: ILogger): any | undefined => { let error; - if (isError(maybeError) && hasStack(maybeError)) { + if (isTypeOfError(maybeError) && hasStack(maybeError)) { error = maybeError; } else { - logger?.warn(NON_ERROR_WARNING(ERROR_HANDLER, stringifyWithoutCircular(error))); + logger?.warn(NON_ERROR_WARNING(ERROR_HANDLER, stringifyWithoutCircular(maybeError))); error = undefined; } From 65e3190dd8e5d0194715feee408b1a4174dc132b Mon Sep 17 00:00:00 2001 From: Sai Kumar Battinoju Date: Thu, 2 Jan 2025 13:16:08 +0530 Subject: [PATCH 06/53] chore: remove unnecessary test suites --- .../__tests__/bugsnag/index.test.ts | 110 --- .../__tests__/bugsnag/utils.test.ts | 806 ------------------ .../__tests__/errorReporting/index.test.ts | 251 ------ .../__tests__/errorReporting/utils.test.ts | 688 --------------- 4 files changed, 1855 deletions(-) delete mode 100644 packages/analytics-js-plugins/__tests__/bugsnag/index.test.ts delete mode 100644 packages/analytics-js-plugins/__tests__/bugsnag/utils.test.ts delete mode 100644 packages/analytics-js-plugins/__tests__/errorReporting/index.test.ts delete mode 100644 packages/analytics-js-plugins/__tests__/errorReporting/utils.test.ts diff --git a/packages/analytics-js-plugins/__tests__/bugsnag/index.test.ts b/packages/analytics-js-plugins/__tests__/bugsnag/index.test.ts deleted file mode 100644 index 1064d9ea2..000000000 --- a/packages/analytics-js-plugins/__tests__/bugsnag/index.test.ts +++ /dev/null @@ -1,110 +0,0 @@ -import type { ExtensionPoint } from '@rudderstack/analytics-js-common/types/PluginEngine'; -import { Bugsnag } from '../../src/bugsnag'; -import * as bugsnagConstants from '../../src/bugsnag/constants'; -import { resetState, state } from '../../__mocks__/state'; - -describe('Plugin - Bugsnag', () => { - const origApiKey = bugsnagConstants.API_KEY; - - const mountBugsnagSDK = () => { - (window as any).bugsnag = jest.fn(() => ({ - notifier: { version: '6.0.0' }, - leaveBreadcrumb: jest.fn(), - notify: jest.fn(), - })); - return (window as any).bugsnag(); - }; - - beforeEach(() => { - resetState(); - delete (window as any).bugsnag; - - Object.defineProperty(bugsnagConstants, 'API_KEY', { - value: origApiKey, - writable: true, - }); - }); - - it('should add Bugsnag plugin in the loaded plugin list', () => { - Bugsnag().initialize?.(state); - expect(state.plugins.loadedPlugins.value.includes('Bugsnag')).toBe(true); - }); - - it('should reject the promise if the Api Key is not valid', async () => { - Object.defineProperty(bugsnagConstants, 'API_KEY', { - value: '{{ dummy api key }}', - writable: true, - }); - - // eslint-disable-next-line @typescript-eslint/dot-notation - const pluginInitPromise = (Bugsnag().errorReportingProvider as ExtensionPoint)?.init?.(); - - await expect(pluginInitPromise).rejects.toThrow( - 'The Bugsnag API key ({{ dummy api key }}) is invalid or not provided.', - ); - }); - - it('should reject the promise if the Bugsnag client could not be initialized', async () => { - jest.useFakeTimers(); - jest.setSystemTime(0); - - const mockExtSrcLoader = { - loadJSFile: jest.fn(() => Promise.resolve()), - }; - - const pluginInitPromise = (Bugsnag().errorReportingProvider as ExtensionPoint)?.init?.( - state, - mockExtSrcLoader, - ); - - // Advance timers to trigger the timeout - jest.advanceTimersByTime(10000); - - await expect(pluginInitPromise).rejects.toThrow( - 'A timeout 10000 ms occurred while trying to load the Bugsnag SDK.', - ); - - jest.useRealTimers(); - }); - - it('should initialize the Bugsnag SDK and return the client instance', async () => { - jest.useFakeTimers(); - jest.setSystemTime(0); - - const mockExtSrcLoader = { - loadJSFile: jest.fn(() => Promise.resolve()), - }; - - const pluginInitPromise = (Bugsnag().errorReportingProvider as ExtensionPoint)?.init?.( - state, - mockExtSrcLoader, - ); - - jest.advanceTimersByTime(1); - mountBugsnagSDK(); - - jest.runAllTimers(); - - await expect(pluginInitPromise).resolves.toBeDefined(); - }); - - it('should notify the client', () => { - const bsClient = mountBugsnagSDK(); - - const mockError = new Error('Test Error'); - - (Bugsnag().errorReportingProvider as ExtensionPoint)?.notify?.(bsClient, mockError); - - expect(bsClient.notify).toHaveBeenCalledWith(mockError); - }); - - it('should leave a breadcrumb', () => { - const bsClient = mountBugsnagSDK(); - - const mockMessage = 'Test Breadcrumb'; - - (Bugsnag().errorReportingProvider as ExtensionPoint)?.breadcrumb?.(bsClient, mockMessage); - - expect(bsClient.leaveBreadcrumb).toHaveBeenCalledWith(mockMessage); - }); -}); diff --git a/packages/analytics-js-plugins/__tests__/bugsnag/utils.test.ts b/packages/analytics-js-plugins/__tests__/bugsnag/utils.test.ts deleted file mode 100644 index 855073690..000000000 --- a/packages/analytics-js-plugins/__tests__/bugsnag/utils.test.ts +++ /dev/null @@ -1,806 +0,0 @@ -import { signal } from '@preact/signals-core'; -import { ExternalSrcLoader } from '@rudderstack/analytics-js-common/services/ExternalSrcLoader'; -import { defaultErrorHandler } from '@rudderstack/analytics-js-common/__mocks__/ErrorHandler'; -import { defaultLogger } from '@rudderstack/analytics-js-common/__mocks__/Logger'; -import * as bugsnagConstants from '../../src/bugsnag/constants'; -import { - isApiKeyValid, - getGlobalBugsnagLibInstance, - getReleaseStage, - isValidVersion, - isRudderSDKError, - enhanceErrorEventMutator, - initBugsnagClient, - loadBugsnagSDK, - onError, - getAppStateForMetadata, -} from '../../src/bugsnag/utils'; -import { server } from '../../__fixtures__/msw.server'; -import type { BugsnagLib } from '../../src/types/plugins'; -import { resetState, state } from '../../__mocks__/state'; - -beforeEach(() => { - window.RudderSnippetVersion = '3.0.0'; - resetState(); - state.lifecycle.writeKey.value = 'dummy-write-key'; -}); - -afterEach(() => { - window.RudderSnippetVersion = undefined; -}); - -const DEFAULT_STATE_DATA = { - autoTrack: { - enabled: false, - pageLifecycle: { - enabled: false, - }, - }, - capabilities: { - isAdBlocked: false, - isBeaconAvailable: false, - isCryptoAvailable: false, - isIE11: false, - isLegacyDOM: false, - isOnline: true, - isUaCHAvailable: false, - storage: { - isCookieStorageAvailable: false, - isLocalStorageAvailable: false, - isSessionStorageAvailable: false, - }, - }, - consents: { - data: {}, - enabled: false, - initialized: false, - postConsent: {}, - preConsent: { - enabled: false, - }, - resolutionStrategy: 'and', - }, - context: { - app: { - installType: 'cdn', - name: 'RudderLabs JavaScript SDK', - namespace: 'com.rudderlabs.javascript', - version: 'dev-snapshot', - }, - device: null, - library: { - name: 'RudderLabs JavaScript SDK', - version: 'dev-snapshot', - }, - locale: null, - network: null, - os: { - name: '', - version: '', - }, - screen: { - density: 0, - height: 0, - innerHeight: 0, - innerWidth: 0, - width: 0, - }, - userAgent: '', - }, - dataPlaneEvents: { - deliveryEnabled: true, - }, - lifecycle: { - initialized: false, - loaded: false, - logLevel: 'ERROR', - readyCallbacks: [], - writeKey: 'dummy-write-key', - }, - loadOptions: { - beaconQueueOptions: {}, - bufferDataPlaneEventsUntilReady: false, - configUrl: 'https://api.rudderstack.com', - dataPlaneEventsBufferTimeout: 1000, - destinationsQueueOptions: {}, - integrations: { - All: true, - }, - loadIntegration: true, - lockIntegrationsVersion: false, - lockPluginsVersion: false, - logLevel: 'ERROR', - plugins: [], - polyfillIfRequired: true, - queueOptions: {}, - sameSiteCookie: 'Lax', - sendAdblockPageOptions: {}, - sessions: { - autoTrack: true, - timeout: 1800000, - }, - storage: { - cookie: {}, - encryption: { - version: 'v3', - }, - migrate: true, - }, - uaChTrackLevel: 'none', - useBeacon: false, - useGlobalIntegrationsConfigInEvents: false, - useServerSideCookies: false, - }, - metrics: { - dropped: 0, - queued: 0, - retries: 0, - sent: 0, - triggered: 0, - }, - nativeDestinations: { - activeDestinations: [], - clientDestinationsReady: false, - configuredDestinations: [], - failedDestinations: [], - initializedDestinations: [], - integrationsConfig: {}, - loadIntegration: true, - loadOnlyIntegrations: {}, - }, - plugins: { - activePlugins: [], - failedPlugins: [], - loadedPlugins: [], - pluginsToLoadFromConfig: [], - ready: false, - totalPluginsToLoad: 0, - }, - reporting: { - breadcrumbs: [], - isErrorReportingEnabled: false, - isErrorReportingPluginLoaded: false, - isMetricsReportingEnabled: false, - }, - serverCookies: { - isEnabledServerSideCookies: false, - }, - session: { - initialReferrer: '', - initialReferringDomain: '', - sessionInfo: {}, - }, - source: { - id: 'dummy-source-id', - workspaceId: 'dummy-workspace-id', - }, - storage: { - entries: {}, - migrate: false, - trulyAnonymousTracking: false, - }, -}; - -describe('Bugsnag utilities', () => { - describe('isApiKeyValid', () => { - it('should return true for a valid API key', () => { - const apiKey = '1234567890abcdef'; - expect(isApiKeyValid(apiKey)).toBe(true); - }); - - it('should return false for an invalid API key', () => { - const apiKey = '{{invalid-api-key}}'; - expect(isApiKeyValid(apiKey)).toBe(false); - }); - - it('should return false for an invalid API key', () => { - const apiKey = ''; - expect(isApiKeyValid(apiKey)).toBe(false); - }); - }); - - describe('getGlobalBugsnagLibInstance', () => { - it('should return the global Bugsnag instance if defined on the window object', () => { - const bsObj = { - version: '1.2.3', - }; - (window as any).bugsnag = bsObj; - - expect(getGlobalBugsnagLibInstance()).toBe(bsObj); - - delete (window as any).bugsnag; - }); - - it('should return undefined if the global Bugsnag instance is not defined on the window object', () => { - expect(getGlobalBugsnagLibInstance()).toBe(undefined); - }); - }); - - describe('getReleaseStage', () => { - let windowSpy: any; - let locationSpy: any; - - beforeEach(() => { - windowSpy = jest.spyOn(window, 'window', 'get'); - locationSpy = jest.spyOn(globalThis, 'location', 'get'); - }); - - afterEach(() => { - windowSpy.mockRestore(); - locationSpy.mockRestore(); - }); - - const testCaseData = [ - ['localhost', 'development'], - ['127.0.0.1', 'development'], - ['www.test-host.com', 'development'], - ['[::1]', 'development'], - ['', '__RS_BUGSNAG_RELEASE_STAGE__'], - ['www.validhost.com', '__RS_BUGSNAG_RELEASE_STAGE__'], - ]; - - it.each(testCaseData)( - 'if window host name is "%s" then it should return the release stage as "%s" ', - (hostName, expectedReleaseStage) => { - locationSpy.mockImplementation(() => ({ - hostname: hostName, - })); - - expect(getReleaseStage()).toBe(expectedReleaseStage); - }, - ); - }); - - describe('isValidVersion', () => { - it('should return true if bugsnag version 6 is present in window scope', () => { - (window as any).bugsnag = jest.fn(() => ({ notifier: { version: '6.0.0' } })); - - expect(isValidVersion((window as any).bugsnag)).toBe(true); - - delete (window as any).bugsnag; - }); - - it('should return false if bugsnag version 7 is present in window scope', () => { - (window as any).bugsnag = { _client: { _notifier: { version: '7.0.0' } } }; - - expect(isValidVersion((window as any).bugsnag)).toBe(false); - - delete (window as any).bugsnag; - }); - - it('should return false if bugsnag version 4 is present in window scope', () => { - (window as any).bugsnag = jest.fn(() => ({ notifier: { version: '4.0.0' } })); - - expect(isValidVersion((window as any).bugsnag)).toBe(false); - - delete (window as any).bugsnag; - }); - }); - - describe('isRudderSDKError', () => { - const testCaseData: any[][] = [ - ['https://invalid-domain.com/rsa.min.js', true], - ['https://invalid-domain.com/rss.min.js', false], - ['https://invalid-domain.com/rsa-plugins-Beacon.min.js', true], - ['https://invalid-domain.com/Amplitude.min.js', false], - ['https://invalid-domain.com/js-integrations/Amplitude.min.js', true], - ['https://invalid-domain.com/js-integrations/Qualaroo.min.js', true], - ['https://invalid-domain.com/test.js', false], - ['https://invalid-domain.com/rsa.css', false], - [undefined, false], - [null, false], - [1, false], - ['', false], - ['asdf.com', false], - ]; - - it.each(testCaseData)( - 'if script src is "%s" then it should return the value as "%s" ', - (scriptSrc: any, expectedValue: boolean) => { - // Bugsnag error event object structure - const event = { - stacktrace: [ - { - file: scriptSrc, - }, - ], - } as unknown as BugsnagLib.Report; - - expect(isRudderSDKError(event)).toBe(expectedValue); - }, - ); - }); - - describe('enhanceErrorEventMutator', () => { - it('should return the enhanced error event object', () => { - const event = { - metaData: {}, - stacktrace: [ - { - file: 'https://invalid-domain.com/rsa.min.js', - }, - ], - updateMetaData(key: string, value: any) { - // @ts-expect-error ignore for testing - this.metaData[key] = value; - }, - errorMessage: 'test error message', - } as unknown as BugsnagLib.Report; - - enhanceErrorEventMutator(state, event); - - expect(event.metaData).toEqual({ - source: { - snippetVersion: '3.0.0', - }, - state: DEFAULT_STATE_DATA, - }); - - expect(event.context).toBe('test error message'); - expect(event.severity).toBe('error'); - }); - - it('should return the enhanced error event object if the error is for script loads', () => { - const event = { - metaData: {}, - stacktrace: [ - { - file: 'https://invalid-domain.com/rsa.min.js', - }, - ], - updateMetaData(key: string, value: any) { - // @ts-expect-error ignore for testing - this.metaData[key] = value; - }, - errorMessage: 'error in script loading "https://invalid-domain.com/rsa.min.js"', - } as unknown as BugsnagLib.Report; - - enhanceErrorEventMutator(state, event); - - expect(event.metaData).toEqual({ - source: { - snippetVersion: '3.0.0', - }, - state: DEFAULT_STATE_DATA, - }); - - expect(event.context).toBe('Script load failures'); - expect(event.severity).toBe('error'); - }); - }); - - describe('initBugsnagClient', () => { - const mountBugsnagSDK = () => { - (window as any).bugsnag = jest.fn(() => ({ notifier: { version: '6.0.0' } })); - }; - - beforeEach(() => { - jest.useFakeTimers(); - jest.setSystemTime(0); - }); - - afterEach(() => { - delete (window as any).bugsnag; - resetState(); - - jest.useRealTimers(); - }); - - it('should resolve the promise immediately if the bugsnag SDK is already loaded', async () => { - mountBugsnagSDK(); - - const bsClient = await new Promise((resolve, reject) => { - initBugsnagClient(state, resolve, reject); - }); - - expect(bsClient).toBeDefined(); - }); - - it('should resolve the promise after some time when the bugsnag SDK is loaded', async () => { - const bsClientPromise: Promise = new Promise((resolve, reject) => { - initBugsnagClient(state, resolve, reject); - }); - - state.session.sessionInfo.value = { id: 123 }; - state.autoTrack.pageLifecycle.visitId.value = 'test-visit-id'; - - // Advance time and mount the Bugsnag SDK - jest.advanceTimersByTime(1); - mountBugsnagSDK(); - - // Run all timers to trigger the promise resolution - jest.runAllTimers(); - - const bsClient: BugsnagLib.Client = await bsClientPromise; - - expect(bsClient).toBeDefined(); // returns a mocked Bugsnag client - - // First call is the version check - expect((window as any).bugsnag).toHaveBeenCalledTimes(2); - expect((window as any).bugsnag).toHaveBeenNthCalledWith(2, { - apiKey: '__RS_BUGSNAG_API_KEY__', - appVersion: 'dev-snapshot', - metaData: { - SDK: { - name: 'JS', - installType: 'cdn', - }, - }, - autoCaptureSessions: false, - collectUserIp: false, - maxEvents: 100, - maxBreadcrumbs: 40, - releaseStage: 'development', - user: { - id: 'dummy-source-id..123..test-visit-id', - }, - networkBreadcrumbsEnabled: false, - beforeSend: expect.any(Function), - logger: undefined, - }); - }); - - it('should return bugsnag client with write key as user id if source id is not available', async () => { - // @ts-expect-error source id is not defined for the test case - state.source.value = { id: undefined }; - state.lifecycle.writeKey = signal('dummy-write-key'); - state.session.sessionInfo.value = { id: 123 }; - state.autoTrack.pageLifecycle.visitId.value = 'test-visit-id'; - - const bsClientPromise: Promise = new Promise((resolve, reject) => { - initBugsnagClient(state, resolve, reject); - }); - - // Advance time and mount the Bugsnag SDK - jest.advanceTimersByTime(1); - mountBugsnagSDK(); - - // Run all timers to trigger the promise resolution - jest.runAllTimers(); - - await bsClientPromise; - - // First call is the version check - expect((window as any).bugsnag).toHaveBeenCalledTimes(2); - expect((window as any).bugsnag).toHaveBeenNthCalledWith(2, { - apiKey: '__RS_BUGSNAG_API_KEY__', - appVersion: 'dev-snapshot', - metaData: { - SDK: { - name: 'JS', - installType: 'cdn', - }, - }, - autoCaptureSessions: false, - collectUserIp: false, - maxEvents: 100, - maxBreadcrumbs: 40, - releaseStage: 'development', - user: { - id: 'dummy-write-key..123..test-visit-id', - }, - networkBreadcrumbsEnabled: false, - beforeSend: expect.any(Function), - logger: undefined, - }); - }); - - it('should reject the promise if the Bugsnag SDK is not loaded', async () => { - const bsClientPromise = new Promise((resolve, reject) => { - initBugsnagClient(state, resolve, reject); - }); - - // Advance time to trigger timeout - jest.advanceTimersByTime(10000); // 10 seconds - - await expect(bsClientPromise).rejects.toThrow( - 'A timeout 10000 ms occurred while trying to load the Bugsnag SDK.', - ); - }); - }); - - describe('loadBugsnagSDK', () => { - let insertBeforeSpy: any; - - const extSrcLoader = new ExternalSrcLoader(defaultErrorHandler, defaultLogger); - - beforeAll(() => { - server.listen(); - }); - - afterAll(() => { - server.close(); - }); - - beforeEach(() => { - insertBeforeSpy = jest.spyOn(document.head, 'insertBefore'); - - jest.useFakeTimers(); - jest.setSystemTime(0); - }); - - afterEach(() => { - insertBeforeSpy.mockRestore(); - if (document.head.firstChild) { - document.head.removeChild(document.head.firstChild as ChildNode); - } - delete (window as any).Bugsnag; - delete (window as any).bugsnag; - - jest.useRealTimers(); - }); - - it('should not load Bugsnag SDK if it (<=v6) is already loaded', () => { - (window as any).bugsnag = jest.fn(() => ({ notifier: { version: '6.0.0' } })); - - loadBugsnagSDK(extSrcLoader, defaultLogger); - - expect(insertBeforeSpy).not.toHaveBeenCalled(); - }); - - it('should not load Bugsnag SDK if it (>v6) is already loaded', () => { - (window as any).Bugsnag = { _client: { _notifier: { version: '7.0.0' } } }; - - loadBugsnagSDK(extSrcLoader, defaultLogger); - - expect(insertBeforeSpy).not.toHaveBeenCalled(); - }); - - it('should attempt to load Bugsnag SDK if not already loaded', () => { - loadBugsnagSDK(extSrcLoader); - - // Run all timers to trigger the script load - jest.runAllTimers(); - - expect(insertBeforeSpy).toHaveBeenCalled(); - }); - - it('should invoke error handler and log error if Bugsnag SDK could not be loaded', done => { - loadBugsnagSDK(extSrcLoader, defaultLogger); - - // Advance the timer to trigger the script load and result in error - jest.advanceTimersByTimeAsync(1).then(() => { - expect(defaultErrorHandler.onError).toHaveBeenCalledWith( - new Error( - `Failed to load the script with the id "rs-bugsnag" from URL "__RS_BUGSNAG_SDK_URL__".`, - ), - 'ExternalSrcLoader', - ); - expect(defaultLogger.error).toHaveBeenCalledWith( - `BugsnagPlugin:: Failed to load the Bugsnag SDK.`, - ); - done(); - }); - }); - }); - - describe('onError', () => { - it('should return a function', () => { - expect(typeof onError(state)).toBe('function'); - }); - - it('should return a function that returns false if the error is not from RudderStack SDK', () => { - const error = { - stacktrace: [ - { - file: 'https://invalid-domain.com/not-rsa.min.js', - }, - ], - } as unknown as BugsnagLib.Report; - - const onErrorFn = onError(state); - - expect(onErrorFn(error)).toBe(false); - }); - - it('should return a function that returns true and enhances the error event if the error is from RudderStack SDK', () => { - const error = { - stacktrace: [ - { - file: 'https://invalid-domain.com/rsa.min.js', - }, - ], - errorMessage: 'error in script loading "https://invalid-domain.com/rsa.min.js"', - updateMetaData: jest.fn(), - } as any; - - const onErrorFn = onError(state); - - expect(onErrorFn(error)).toBe(true); - expect(error.updateMetaData).toHaveBeenCalledTimes(2); - expect(error.updateMetaData).toHaveBeenNthCalledWith(1, 'source', { - snippetVersion: '3.0.0', - }); - expect(error.updateMetaData).toHaveBeenNthCalledWith(2, 'state', DEFAULT_STATE_DATA); - expect(error.severity).toBe('error'); - expect(error.context).toBe('Script load failures'); - }); - - it('should return a function that returns false if processing the event results in any unhandled exception', () => { - // Not defining `updateMetaData` on the error object to simulate an unhandled exception - const error = { - stacktrace: [ - { - file: 'https://invalid-domain.com/rsa.min.js', - }, - ], - errorMessage: 'error in script loading "https://invalid-domain.com/rsa.min.js"', - } as any; - - const onErrorFn = onError(state); - - expect(onErrorFn(error)).toBe(false); - }); - - it('should log error and return false if the error could not be filtered', () => { - const error = { - stacktrace: [ - { - file: 'https://invalid-domain.com/rsa.min.js', - }, - ], - errorMessage: 'error in script loading "https://invalid-domain.com/rsa.min.js"', - - // Simulate an unhandled exception - updateMetaData: jest.fn(() => { - throw new Error('Failed to update metadata.'); - }), - } as any; - - const onErrorFn = onError(state, defaultLogger); - - expect(onErrorFn(error)).toBe(false); - expect(defaultLogger.error).toHaveBeenCalledWith( - 'BugsnagPlugin:: Failed to filter the error.', - ); - }); - }); - - describe('getAppStateForMetadata', () => { - const origAppStateExcludes = bugsnagConstants.APP_STATE_EXCLUDE_KEYS; - - beforeEach(() => { - Object.defineProperty(bugsnagConstants, 'APP_STATE_EXCLUDE_KEYS', { - value: origAppStateExcludes, - writable: true, - }); - }); - - // Here we are just exploring different combinations of data where - // the signals could be buried inside objects, arrays, nested objects, etc. - const tcData: any[][] = [ - [ - { - name: 'test', - value: 123, - someKey1: [1, 2, 3], - someKey2: { - key1: 'value1', - key2: 'value2', - }, - someKey3: 2.5, - testSignal: signal('test'), - }, - { - name: 'test', - value: 123, - someKey1: [1, 2, 3], - someKey2: { - key1: 'value1', - key2: 'value2', - }, - someKey3: 2.5, - testSignal: 'test', - }, - undefined, - ], - [ - { - name: 'test', - someKey: { - key1: 'value1', - key2: signal('value2'), - }, - someKey2: { - key1: 'value1', - key2: { - key3: signal('value3'), - }, - }, - someKey3: [signal('value1'), signal('value2'), 1, 3], - someKey4: [ - { - key1: signal('value1'), - key2: signal('value2'), - }, - 'asdf', - 1, - { - key3: 'value3', - key4: 'value4', - }, - ], - }, - { - name: 'test', - someKey: { - key1: 'value1', - key2: 'value2', - }, - someKey2: { - key1: 'value1', - key2: { - key3: 'value3', - }, - }, - someKey3: ['value1', 'value2', 1, 3], - someKey4: [ - { - key1: 'value1', - key2: 'value2', - }, - 'asdf', - 1, - { - key3: 'value3', - key4: 'value4', - }, - ], - }, - [], - ], - [ - { - someKey: signal({ - key1: 'value1', - key2: signal('value2'), - key3: [signal('value1'), signal('value2'), undefined, null], - key4: true, - key7: { - key1: signal('value1'), - key2: signal('value2'), - key3: 'asdf', - key4: signal('value4'), - }, - key5: signal([signal('value1'), signal('value2'), 1, 3]), - KEY6: 123, - }), - }, - { - someKey: { - key1: 'value1', - key2: 'value2', - key3: ['value1', 'value2', null, null], - key7: { - key1: 'value1', - key2: 'value2', - key3: 'asdf', - }, - key5: ['value1', 'value2', 1, 3], - KEY6: 123, - }, - }, - ['key4', 'key6'], // excluded keys - ], - [ - { - // We're intentionally adding BigInt values - // here to test if they are converted to strings - // eslint-disable-next-line compat/compat - someKey: BigInt(123), - }, - undefined, - [], - ], - ]; - - it.each(tcData)('should convert signals to JSON %#', (input, expected, excludes) => { - Object.defineProperty(bugsnagConstants, 'APP_STATE_EXCLUDE_KEYS', { - value: excludes, - writable: true, - }); - - expect(getAppStateForMetadata(input)).toEqual(expected); - }); - }); -}); diff --git a/packages/analytics-js-plugins/__tests__/errorReporting/index.test.ts b/packages/analytics-js-plugins/__tests__/errorReporting/index.test.ts deleted file mode 100644 index f0bed5e53..000000000 --- a/packages/analytics-js-plugins/__tests__/errorReporting/index.test.ts +++ /dev/null @@ -1,251 +0,0 @@ -import { signal } from '@preact/signals-core'; -import { clone } from 'ramda'; -import type { IHttpClient } from '@rudderstack/analytics-js-common/types/HttpClient'; -import { defaultLogger } from '@rudderstack/analytics-js-common/__mocks__/Logger'; -import type { ExtensionPoint } from '@rudderstack/analytics-js-common/types/PluginEngine'; -import { ErrorReporting } from '../../src/errorReporting'; - -describe('Plugin - ErrorReporting', () => { - const originalState = { - plugins: { - loadedPlugins: signal([]), - }, - lifecycle: { - writeKey: signal('dummy-write-key'), - }, - reporting: { - isErrorReportingPluginLoaded: signal(false), - breadcrumbs: signal([]), - }, - context: { - locale: signal('en-GB'), - userAgent: signal('sample user agent'), - app: signal({ version: 'sample_version', installType: 'sample_installType' }), - }, - source: signal({ - id: 'test-source-id', - config: {}, - }), - session: { - sessionInfo: signal({ id: 'test-session-id' }), - }, - autoTrack: { - pageLifecycle: { - visitId: signal('test-visit-id'), - }, - }, - }; - - let state: any; - - // Deprecated code - const mockPluginEngine = { - invokeSingle: jest.fn(() => Promise.resolve()), - }; - const mockExtSrcLoader = { - loadJSFile: jest.fn(() => Promise.resolve()), - }; - const mockErrReportingProviderClient = { - notify: jest.fn(), - leaveBreadcrumb: jest.fn(), - }; - // End of deprecated code - - beforeEach(() => { - state = clone(originalState); - }); - - it('should add ErrorReporting plugin in the loaded plugin list', () => { - ErrorReporting()?.initialize?.(state); - expect(state.plugins.loadedPlugins.value.includes('ErrorReporting')).toBe(true); - expect(state.reporting.isErrorReportingPluginLoaded.value).toBe(true); - expect(state.reporting.breadcrumbs.value[0].name).toBe('Error Reporting Plugin Loaded'); - }); - - it('should not invoke error reporting provider plugin on init if request is coming from latest core SDK', () => { - (ErrorReporting()?.errorReporting as ExtensionPoint)?.init?.( - {}, - mockPluginEngine, - mockExtSrcLoader, - defaultLogger, - true, - ); - expect(mockPluginEngine.invokeSingle).not.toHaveBeenCalled(); - }); - - it('should not invoke error reporting provider plugin on init if sourceConfig do not have required parameters', () => { - (ErrorReporting()?.errorReporting as ExtensionPoint)?.init?.( - state, - mockPluginEngine, - mockExtSrcLoader, - defaultLogger, - true, - ); - expect(mockPluginEngine.invokeSingle).not.toHaveBeenCalled(); - }); - - it('should not invoke error reporting provider plugin on init if request is coming from old core SDK', () => { - (ErrorReporting()?.errorReporting as ExtensionPoint)?.init?.( - state, - mockPluginEngine, - mockExtSrcLoader, - defaultLogger, - ); - expect(mockPluginEngine.invokeSingle).toHaveBeenCalledTimes(1); - expect(mockPluginEngine.invokeSingle).toHaveBeenCalledWith( - 'errorReportingProvider.init', - state, - mockExtSrcLoader, - defaultLogger, - ); - }); - - it('should invoke the error reporting provider plugin on notify if httpClient is not provided', () => { - const dummyError = new Error('dummy error'); - (ErrorReporting()?.errorReporting as ExtensionPoint)?.notify?.( - mockPluginEngine, - mockErrReportingProviderClient, - dummyError, - state, - defaultLogger, - ); - expect(mockPluginEngine.invokeSingle).toHaveBeenCalledTimes(1); - expect(mockPluginEngine.invokeSingle).toHaveBeenCalledWith( - 'errorReportingProvider.notify', - mockErrReportingProviderClient, - dummyError, - state, - defaultLogger, - ); - }); - - it('should not send data to metrics service if the error message contains certain', () => { - state.lifecycle = { - writeKey: signal('sample-write-key'), - }; - state.metrics = { - metricsServiceUrl: signal('https://test.com'), - }; - const mockHttpClient = { - getAsyncData: jest.fn(), - setAuthHeader: jest.fn(), - } as unknown as IHttpClient; - const newError = new Error(); - const normalizedError = Object.create(newError, { - message: { value: 'The request failed due to timeout' }, - stack: { - value: `The request failed due to timeout at Analytics.page (http://localhost:3001/cdn/modern/iife/rsa.js:1610:3) at RudderAnalytics.page (http://localhost:3001/cdn/modern/iife/rsa.js:1666:84)`, - }, - }); - (ErrorReporting()?.errorReporting as ExtensionPoint)?.notify?.( - {}, - undefined, - normalizedError, - state, - undefined, - mockHttpClient, - { - severity: 'error', - unhandled: false, - severityReason: { type: 'handledException' }, - }, - ); - - expect(mockHttpClient.getAsyncData).not.toHaveBeenCalled(); - }); - - it('should send data to metrics service on notify when httpClient is provided', () => { - state.lifecycle = { - writeKey: signal('sample-write-key'), - }; - state.metrics = { - metricsServiceUrl: signal('https://test.com'), - }; - const mockHttpClient = { - getAsyncData: jest.fn(), - setAuthHeader: jest.fn(), - } as unknown as IHttpClient; - const newError = new Error(); - const normalizedError = Object.create(newError, { - message: { value: 'ReferenceError: testUndefinedFn is not defined' }, - stack: { - value: `ReferenceError: testUndefinedFn is not defined at Analytics.page (http://localhost:3001/cdn/modern/iife/rsa.js:1610:3) at RudderAnalytics.page (http://localhost:3001/cdn/modern/iife/rsa.js:1666:84)`, - }, - }); - (ErrorReporting()?.errorReporting as ExtensionPoint)?.notify?.( - {}, - undefined, - normalizedError, - state, - undefined, - mockHttpClient, - { - severity: 'error', - unhandled: false, - severityReason: { type: 'handledException' }, - }, - ); - - expect(mockHttpClient.getAsyncData).toHaveBeenCalled(); - }); - - it('should not notify if the error is not an SDK error', () => { - const mockHttpClient = { - getAsyncData: jest.fn(), - setAuthHeader: jest.fn(), - } as unknown as IHttpClient; - const newError = new Error(); - const normalizedError = Object.create(newError, { - message: { value: 'ReferenceError: testUndefinedFn is not defined' }, - stack: { - value: `ReferenceError: testUndefinedFn is not defined at Abcd.page (http://localhost:3001/cdn/modern/iife/abc.js:1610:3) at xyz.page (http://localhost:3001/cdn/modern/iife/abc.js:1666:84)`, - }, - }); - (ErrorReporting()?.errorReporting as ExtensionPoint)?.notify?.( - {}, - undefined, - normalizedError, - state, - undefined, - mockHttpClient, - { - severity: 'error', - unhandled: false, - severityReason: { type: 'handledException' }, - }, - ); - - expect(mockHttpClient.getAsyncData).not.toHaveBeenCalled(); - }); - - it('should add a new breadcrumb', () => { - const breadcrumbLength = state.reporting.breadcrumbs.value.length; - (ErrorReporting()?.errorReporting as ExtensionPoint)?.breadcrumb?.( - {}, - undefined, - 'dummy breadcrumb', - undefined, - state, - ); - - expect(state.reporting.breadcrumbs.value.length).toBe(breadcrumbLength + 1); - expect(mockPluginEngine.invokeSingle).not.toHaveBeenCalled(); - }); - - it('should invoke the error reporting provider plugin on new breadcrumb if state is not provided', () => { - (ErrorReporting()?.errorReporting as ExtensionPoint)?.breadcrumb?.( - mockPluginEngine, - mockErrReportingProviderClient, - 'dummy breadcrumb', - defaultLogger, - ); - - expect(mockPluginEngine.invokeSingle).toHaveBeenCalledTimes(1); - expect(mockPluginEngine.invokeSingle).toHaveBeenCalledWith( - 'errorReportingProvider.breadcrumb', - mockErrReportingProviderClient, - 'dummy breadcrumb', - defaultLogger, - ); - }); -}); diff --git a/packages/analytics-js-plugins/__tests__/errorReporting/utils.test.ts b/packages/analytics-js-plugins/__tests__/errorReporting/utils.test.ts deleted file mode 100644 index 59c883c6f..000000000 --- a/packages/analytics-js-plugins/__tests__/errorReporting/utils.test.ts +++ /dev/null @@ -1,688 +0,0 @@ -/* eslint-disable max-classes-per-file */ -import { signal } from '@preact/signals-core'; -import type { ErrorEventPayload } from '@rudderstack/analytics-js-common/types/Metrics'; -import { mergeDeepRight } from '@rudderstack/analytics-js-common/utilities/object'; -import { ErrorFormat } from '../../src/errorReporting/event/event'; -import * as errorReportingConstants from '../../src/errorReporting/constants'; -import { - getReleaseStage, - isRudderSDKError, - getAppStateForMetadata, - getErrorContext, - createNewBreadcrumb, - getURLWithoutQueryString, - getBugsnagErrorEvent, - getErrorDeliveryPayload, - getConfigForPayloadCreation, - isAllowedToBeNotified, -} from '../../src/errorReporting/utils'; -import { state } from '../../__mocks__/state'; - -jest.mock('@rudderstack/analytics-js-common/utilities/uuId', () => ({ - generateUUID: jest.fn().mockReturnValue('test_uuid'), -})); - -const DEFAULT_STATE_DATA = { - autoTrack: { - enabled: false, - pageLifecycle: { - enabled: false, - }, - }, - capabilities: { - isAdBlocked: false, - isBeaconAvailable: false, - isCryptoAvailable: false, - isIE11: false, - isLegacyDOM: false, - isOnline: true, - isUaCHAvailable: false, - storage: { - isCookieStorageAvailable: false, - isLocalStorageAvailable: false, - isSessionStorageAvailable: false, - }, - }, - consents: { - data: {}, - enabled: false, - initialized: false, - postConsent: {}, - preConsent: { - enabled: false, - }, - resolutionStrategy: 'and', - }, - context: { - app: { - installType: 'cdn', - name: 'RudderLabs JavaScript SDK', - namespace: 'com.rudderlabs.javascript', - version: 'dev-snapshot', - }, - device: null, - library: { - name: 'RudderLabs JavaScript SDK', - version: 'dev-snapshot', - }, - locale: null, - network: null, - os: { - name: '', - version: '', - }, - screen: { - density: 0, - height: 0, - innerHeight: 0, - innerWidth: 0, - width: 0, - }, - userAgent: '', - }, - dataPlaneEvents: { - deliveryEnabled: true, - }, - lifecycle: { - initialized: false, - loaded: false, - logLevel: 'ERROR', - readyCallbacks: [], - }, - loadOptions: { - beaconQueueOptions: {}, - bufferDataPlaneEventsUntilReady: false, - configUrl: 'https://api.rudderstack.com', - dataPlaneEventsBufferTimeout: 1000, - destinationsQueueOptions: {}, - integrations: { - All: true, - }, - loadIntegration: true, - lockIntegrationsVersion: false, - lockPluginsVersion: false, - logLevel: 'ERROR', - plugins: [], - polyfillIfRequired: true, - queueOptions: {}, - sameSiteCookie: 'Lax', - sendAdblockPageOptions: {}, - sessions: { - autoTrack: true, - timeout: 1800000, - }, - storage: { - cookie: {}, - encryption: { - version: 'v3', - }, - migrate: true, - }, - uaChTrackLevel: 'none', - useBeacon: false, - useGlobalIntegrationsConfigInEvents: false, - useServerSideCookies: false, - }, - metrics: { - dropped: 0, - queued: 0, - retries: 0, - sent: 0, - triggered: 0, - }, - nativeDestinations: { - activeDestinations: [], - clientDestinationsReady: false, - configuredDestinations: [], - failedDestinations: [], - initializedDestinations: [], - integrationsConfig: {}, - loadIntegration: true, - loadOnlyIntegrations: {}, - }, - plugins: { - activePlugins: [], - failedPlugins: [], - loadedPlugins: [], - pluginsToLoadFromConfig: [], - ready: false, - totalPluginsToLoad: 0, - }, - reporting: { - breadcrumbs: [], - isErrorReportingEnabled: false, - isErrorReportingPluginLoaded: false, - isMetricsReportingEnabled: false, - }, - serverCookies: { - isEnabledServerSideCookies: false, - }, - session: { - initialReferrer: '', - initialReferringDomain: '', - }, - source: { - id: 'dummy-source-id', - workspaceId: 'dummy-workspace-id', - }, - storage: { - entries: {}, - migrate: false, - trulyAnonymousTracking: false, - }, -}; - -describe('Error Reporting utilities', () => { - describe('createNewBreadcrumb', () => { - it('should create and return a breadcrumb', () => { - const msg = 'sample message'; - const breadcrumb = createNewBreadcrumb(msg); - - expect(breadcrumb).toStrictEqual({ - metaData: {}, - type: 'manual', - timestamp: expect.any(Date), - name: msg, - }); - }); - - it('should create and return a breadcrumb with empty meta data if not provided', () => { - const msg = 'sample message'; - const breadcrumb = createNewBreadcrumb(msg); - - expect(breadcrumb.metaData).toStrictEqual({}); - }); - }); - - describe('getURLWithoutQueryString', () => { - it('should return url without query param ', () => { - (window as any).location.href = 'https://www.test-host.com?key1=1234&key2=true'; - const urlWithoutSearchParam = getURLWithoutQueryString(); - expect(urlWithoutSearchParam).toEqual('https://www.test-host.com/'); - }); - }); - describe('getReleaseStage', () => { - let windowSpy: any; - let locationSpy: any; - - beforeEach(() => { - windowSpy = jest.spyOn(window, 'window', 'get'); - locationSpy = jest.spyOn(globalThis, 'location', 'get'); - }); - - afterEach(() => { - windowSpy.mockRestore(); - locationSpy.mockRestore(); - }); - - const testCaseData = [ - ['localhost', 'development'], - ['127.0.0.1', 'development'], - ['www.test-host.com', 'development'], - ['[::1]', 'development'], - ['', '__RS_BUGSNAG_RELEASE_STAGE__'], - ['www.validhost.com', '__RS_BUGSNAG_RELEASE_STAGE__'], - ]; - - it.each(testCaseData)( - 'if window host name is "%s" then it should return the release stage as "%s" ', - (hostName, expectedReleaseStage) => { - locationSpy.mockImplementation(() => ({ - hostname: hostName, - })); - - expect(getReleaseStage()).toBe(expectedReleaseStage); - }, - ); - }); - - describe('isRudderSDKError', () => { - const testCaseData: any[] = [ - ['https://invalid-domain.com/rsa.min.js', true], - ['https://invalid-domain.com/rss.min.js', false], - ['https://invalid-domain.com/rsa-plugins-Beacon.min.js', true], - ['https://invalid-domain.com/Amplitude.min.js', false], - ['https://invalid-domain.com/js-integrations/Amplitude.min.js', true], - ['https://invalid-domain.com/js-integrations/Qualaroo.min.js', true], - ['https://invalid-domain.com/mjs-integrations/Qualaroo.min.js', false], - ['https://invalid-domain.com/test.js', false], - ['https://invalid-domain.com/rsa.css', false], - [undefined, false], - [null, false], - [1, false], - ['', false], - ['asdf.com', false], - ]; - - it.each(testCaseData)( - 'if script src is "%s" then it should return the value as "%s" ', - (scriptSrc: string, expectedValue: boolean) => { - // Bugsnag error event object structure - const event = { - stacktrace: [ - { - file: scriptSrc, - }, - ], - }; - - expect(isRudderSDKError(event)).toBe(expectedValue); - }, - ); - }); - - describe('getErrorContext', () => { - it('should return error context', () => { - const event = { - metadata: {}, - stacktrace: [ - { - file: 'https://invalid-domain.com/rsa.min.js', - }, - ], - message: 'test error message', - }; - - const context = getErrorContext(event); - - expect(context).toBe('test error message'); - }); - - it('should return the enhanced error event object if the error is for script loads', () => { - const event = { - metadata: {}, - stacktrace: [ - { - file: 'https://invalid-domain.com/rsa.min.js', - }, - ], - message: 'Error in loading a third-party script "https://invalid-domain.com/rsa.min.js"', - }; - - const context = getErrorContext(event); - - expect(context).toBe('Script load failures'); - }); - }); - - describe('getAppStateForMetadata', () => { - const origAppStateExcludes = errorReportingConstants.APP_STATE_EXCLUDE_KEYS; - - afterEach(() => { - Object.defineProperty(errorReportingConstants, 'APP_STATE_EXCLUDE_KEYS', { - value: origAppStateExcludes, - writable: true, - }); - }); - - // Here we are just exploring different combinations of data where - // the signals could be buried inside objects, arrays, nested objects, etc. - const tcData: any[][] = [ - [ - { - name: 'test', - value: 123, - someKey1: [1, 2, 3], - someKey2: { - key1: 'value1', - key2: 'value2', - }, - someKey3: 2.5, - testSignal: signal('test'), - }, - { - name: 'test', - value: 123, - someKey1: [1, 2, 3], - someKey2: { - key1: 'value1', - key2: 'value2', - }, - someKey3: 2.5, - testSignal: 'test', - }, - undefined, - ], - [ - { - name: 'test', - someKey: { - key1: 'value1', - key2: signal('value2'), - }, - someKey2: { - key1: 'value1', - key2: { - key3: signal('value3'), - }, - }, - someKey3: [signal('value1'), signal('value2'), 1, 3], - someKey4: [ - { - key1: signal('value1'), - key2: signal('value2'), - }, - 'asdf', - 1, - { - key3: 'value3', - key4: 'value4', - }, - ], - }, - { - name: 'test', - someKey: { - key1: 'value1', - key2: 'value2', - }, - someKey2: { - key1: 'value1', - key2: { - key3: 'value3', - }, - }, - someKey3: ['value1', 'value2', 1, 3], - someKey4: [ - { - key1: 'value1', - key2: 'value2', - }, - 'asdf', - 1, - { - key3: 'value3', - key4: 'value4', - }, - ], - }, - [], - ], - [ - { - someKey: signal({ - key1: 'value1', - key2: signal('value2'), - key3: [signal('value1'), signal('value2'), undefined, null], - key4: true, - key7: { - key1: signal('value1'), - key2: signal('value2'), - key3: 'asdf', - key4: signal('value4'), - }, - key5: signal([signal('value1'), signal('value2'), 1, 3]), - KEY6: 123, - }), - }, - { - someKey: { - key1: 'value1', - key2: 'value2', - key3: ['value1', 'value2', null, null], - key7: { - key1: 'value1', - key2: 'value2', - key3: 'asdf', - }, - key5: ['value1', 'value2', 1, 3], - KEY6: 123, - }, - }, - ['key4', 'key6'], // excluded keys - ], - [ - { - // eslint-disable-next-line compat/compat - someKey: BigInt(123), - }, - {}, - [], - ], - ]; - - it.each(tcData)('should convert signals to JSON %#', (input, expected, excludes) => { - Object.defineProperty(errorReportingConstants, 'APP_STATE_EXCLUDE_KEYS', { - value: excludes, - writable: true, - }); - - expect(getAppStateForMetadata(input)).toEqual(expected); - }); - }); - - describe('getBugsnagErrorEvent', () => { - it('should return enhanced error event payload', () => { - state.session.sessionInfo.value = { id: 123 }; - state.autoTrack.pageLifecycle.visitId.value = 'test-visit-id'; - - const newError = new Error(); - const normalizedError = Object.create(newError, { - message: { value: 'ReferenceError: testUndefinedFn is not defined' }, - stack: { - value: `ReferenceError: testUndefinedFn is not defined at Analytics.page (http://localhost:3001/cdn/modern/iife/rsa.js:1610:3) at RudderAnalytics.page (http://localhost:3001/cdn/modern/iife/rsa.js:1666:84)`, - }, - }); - const errorState = { - severity: 'error', - unhandled: false, - severityReason: { type: 'handledException' }, - }; - const errorPayload = ErrorFormat.create(normalizedError, 'notify()') as ErrorFormat; - - (window as any).RudderSnippetVersion = 'sample_snippet_version'; - const enhancedError = getBugsnagErrorEvent(errorPayload, errorState, state); - console.log(JSON.stringify(enhancedError)); - const expectedOutcome = { - notifier: { - name: 'RudderStack JavaScript SDK Error Notifier', - version: 'dev-snapshot', - url: 'https://github.com/rudderlabs/rudder-sdk-js', - }, - events: [ - { - payloadVersion: '5', - exceptions: [ - { - errorClass: 'Error', - message: 'ReferenceError: testUndefinedFn is not defined', - type: 'browserjs', - stacktrace: [ - { - file: 'ReferenceError: testUndefinedFn is not defined at Analytics.page http://localhost:3001/cdn/modern/iife/rsa.js:1610:3 at RudderAnalytics.page http://localhost:3001/cdn/modern/iife/rsa.js', - lineNumber: 1666, - columnNumber: 84, - code: undefined, - inProject: undefined, - method: undefined, - }, - ], - }, - ], - severity: 'error', - unhandled: false, - severityReason: { - type: 'handledException', - }, - app: { - version: 'dev-snapshot', - releaseStage: 'development', - }, - device: { - userAgent: '', - time: expect.any(Date), - }, - request: { - url: 'https://www.test-host.com/', - clientIp: '[NOT COLLECTED]', - }, - breadcrumbs: [], - context: 'ReferenceError: testUndefinedFn is not defined', - metaData: { - sdk: { - name: 'JS', - installType: 'cdn', - }, - state: mergeDeepRight(DEFAULT_STATE_DATA, { - autoTrack: { - pageLifecycle: { - visitId: 'test-visit-id', - }, - }, - session: { - sessionInfo: { id: 123 }, - }, - }), - source: { - snippetVersion: 'sample_snippet_version', - }, - }, - user: { - id: 'dummy-source-id..123..test-visit-id', - }, - }, - ], - }; - expect(enhancedError).toEqual(expectedOutcome); - }); - }); - - describe('getErrorDeliveryPayload', () => { - it('should return error delivery payload', () => { - const enhancedErrorPayload = { - notifier: { - name: 'Rudderstack JavaScript SDK Error Notifier', - version: 'sample_version', - url: 'https://github.com/rudderlabs/rudder-sdk-js', - }, - events: [ - { - payloadVersion: '5', - exceptions: [ - { - errorClass: 'Error', - errorMessage: 'ReferenceError: testUndefinedFn is not defined', - type: 'browserjs', - stacktrace: [ - { - file: 'ReferenceError: testUndefinedFn is not defined at Analytics.page http://localhost:3001/cdn/modern/iife/rsa.js:1610:3 at RudderAnalytics.page http://localhost:3001/cdn/modern/iife/rsa.js', - lineNumber: 1666, - columnNumber: 84, - code: undefined, - inProject: undefined, - method: undefined, - }, - ], - }, - ], - severity: 'error', - unhandled: false, - severityReason: { - type: 'handledException', - }, - app: { - version: 'dev-snapshot', - releaseStage: 'development', - }, - device: { - userAgent: '', - time: expect.any(Date), - }, - request: { - url: 'https://www.test-host.com/', - clientIp: '[NOT COLLECTED]', - }, - breadcrumbs: [], - context: 'ReferenceError: testUndefinedFn is not defined', - metaData: { - sdk: { - name: 'JS', - installType: 'cdn', - }, - state: DEFAULT_STATE_DATA, - source: { - snippetVersion: 'sample_snippet_version', - }, - }, - user: { - id: 'sample-write-key', - }, - }, - ], - } as unknown as ErrorEventPayload; - - const deliveryPayload = getErrorDeliveryPayload(enhancedErrorPayload, state); - expect(deliveryPayload).toEqual( - JSON.stringify({ - version: '1', - message_id: 'test_uuid', - source: { - name: 'js', - sdk_version: 'dev-snapshot', - install_type: 'cdn', - }, - errors: enhancedErrorPayload, - }), - ); - }); - }); - - describe('getConfigForPayloadCreation', () => { - it('should return the config for payload creation in case of unhandled errors', () => { - const error = new ErrorEvent('test error'); - const config = getConfigForPayloadCreation(error, 'unhandledException'); - expect(config).toEqual({ - component: 'unhandledException handler', - normalizedError: error, - }); - }); - - it('should return the config for payload creation in case of unhandledPromiseRejection', () => { - // eslint-disable-next-line compat/compat - const error = new PromiseRejectionEvent('test error', { - promise: Promise.resolve(), - reason: 'test error', - }); - const config = getConfigForPayloadCreation(error, 'unhandledPromiseRejection'); - expect(config).toEqual({ - component: 'unhandledrejection handler', - normalizedError: 'test error', - }); - }); - - it('should return the config for payload creation in case of handled errors', () => { - const error = new Error('test error'); - const config = getConfigForPayloadCreation(error, 'handledException'); - expect(config).toEqual({ - component: 'notify()', - normalizedError: error, - }); - }); - - it('should return the config for payload creation even if the error type is a random value', () => { - const error = new Error('test error'); - const config = getConfigForPayloadCreation(error, 'randomValue'); - expect(config).toEqual({ - component: 'notify()', - normalizedError: error, - }); - }); - }); - - 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(); - }); - }); -}); From 9a8df2902f7017c0ac3dc1dabe36154d98710c66 Mon Sep 17 00:00:00 2001 From: Sai Kumar Battinoju Date: Thu, 2 Jan 2025 13:59:20 +0530 Subject: [PATCH 07/53] fix: error filtering and logging --- .../src/types/ApplicationState.ts | 1 - .../src/utilities/errors.ts | 48 +- .../detection/adBlockers.test.ts | 8 +- .../configManager/ConfigManager.test.ts | 1 + .../eventManager/EventManager.test.ts | 3 +- .../eventRepository/EventRepository.test.ts | 69 ++- .../pluginsManager/PluginsManager.test.ts | 15 +- .../UserSessionManager.test.ts | 2 + .../ErrorHandler/ErrorHandler.test.ts | 460 +++++++----------- .../ErrorHandler/processError.test.ts | 117 ----- .../services/ErrorHandler/utils.test.ts | 37 +- .../services/HttpClient/HttpClient.test.ts | 7 +- packages/analytics-js/project.json | 10 +- .../analytics-js/src/constants/logMessages.ts | 2 +- .../src/services/ErrorHandler/ErrorHandler.ts | 33 +- .../src/services/ErrorHandler/event/event.ts | 4 +- .../src/services/ErrorHandler/utils.ts | 4 +- .../src/state/slices/reporting.ts | 1 - 18 files changed, 327 insertions(+), 495 deletions(-) delete mode 100644 packages/analytics-js/__tests__/services/ErrorHandler/processError.test.ts diff --git a/packages/analytics-js-common/src/types/ApplicationState.ts b/packages/analytics-js-common/src/types/ApplicationState.ts index f1b0181ab..20152d920 100644 --- a/packages/analytics-js-common/src/types/ApplicationState.ts +++ b/packages/analytics-js-common/src/types/ApplicationState.ts @@ -145,7 +145,6 @@ export type Breadcrumb = { export type ReportingState = { isErrorReportingEnabled: Signal; isMetricsReportingEnabled: Signal; - isErrorReportingPluginLoaded: Signal; breadcrumbs: Signal; }; diff --git a/packages/analytics-js-common/src/utilities/errors.ts b/packages/analytics-js-common/src/utilities/errors.ts index 691f256ea..9efdf1501 100644 --- a/packages/analytics-js-common/src/utilities/errors.ts +++ b/packages/analytics-js-common/src/utilities/errors.ts @@ -1,13 +1,19 @@ import { isTypeOfError } from './checks'; import { stringifyWithoutCircular } from './json'; -const MANUAL_ERROR_IDENTIFIER = '[MANUALLY DISPATCHED ERROR]'; +const MANUAL_ERROR_IDENTIFIER = '[SDK DISPATCHED ERROR]'; -const hasStack = (err: any) => - !!err && - (!!err.stack || !!err.stacktrace || !!err['opera#sourceloc']) && - typeof (err.stack || err.stacktrace || err['opera#sourceloc']) === 'string' && - err.stack !== `${err.name}: ${err.message}`; +const getStacktrace = (err: any): string | undefined => { + const { stack, stacktrace, name, message } = err; + const operaSourceloc = err['opera#sourceloc']; + + const stackString = stack ?? stacktrace ?? operaSourceloc; + + if (!!stackString && typeof stackString === 'string' && stack !== `${name}: ${message}`) { + return stackString; + } + return undefined; +}; /** * Get mutated error with issue prepended to error message @@ -26,13 +32,31 @@ const getMutatedError = (err: any, issue: string): Error => { }; const dispatchErrorEvent = (error: any) => { - if (isTypeOfError(error) && hasStack(error)) { - let stack = error.stack ?? error.stacktrace ?? error['opera#sourceloc'] ?? ''; - stack = `${stack}\n${MANUAL_ERROR_IDENTIFIER}`; - // eslint-disable-next-line no-param-reassign - error.stack = stack; + if (isTypeOfError(error)) { + const errStack = getStacktrace(error); + if (errStack) { + const { stack, stacktrace } = error; + const operaSourceloc = error['opera#sourceloc']; + + switch (errStack) { + case stack: + // eslint-disable-next-line no-param-reassign + error.stack = `${stack}\n${MANUAL_ERROR_IDENTIFIER}`; + break; + case stacktrace: + // eslint-disable-next-line no-param-reassign + error.stacktrace = `${stacktrace}\n${MANUAL_ERROR_IDENTIFIER}`; + break; + case operaSourceloc: + default: + // eslint-disable-next-line no-param-reassign + error['opera#sourceloc'] = `${operaSourceloc}\n${MANUAL_ERROR_IDENTIFIER}`; + break; + } + } } + (globalThis as typeof window).dispatchEvent(new ErrorEvent('error', { error })); }; -export { getMutatedError, dispatchErrorEvent, MANUAL_ERROR_IDENTIFIER, hasStack }; +export { getMutatedError, dispatchErrorEvent, MANUAL_ERROR_IDENTIFIER, getStacktrace }; diff --git a/packages/analytics-js/__tests__/components/capabilitiesManager/detection/adBlockers.test.ts b/packages/analytics-js/__tests__/components/capabilitiesManager/detection/adBlockers.test.ts index 75c1a1b46..988c835aa 100644 --- a/packages/analytics-js/__tests__/components/capabilitiesManager/detection/adBlockers.test.ts +++ b/packages/analytics-js/__tests__/components/capabilitiesManager/detection/adBlockers.test.ts @@ -1,4 +1,5 @@ import { effect } from '@preact/signals-core'; +import { defaultErrorHandler } from '@rudderstack/analytics-js-common/__mocks__/ErrorHandler'; import { detectAdBlockers } from '../../../../src/components/capabilitiesManager/detection/adBlockers'; import { state, resetState } from '../../../../src/state'; @@ -12,6 +13,7 @@ jest.mock('../../../../src/services/HttpClient/HttpClient', () => { __esModule: true, ...originalModule, HttpClient: jest.fn().mockImplementation(() => ({ + init: jest.fn(), setAuthHeader: jest.fn(), getAsyncData: jest.fn().mockImplementation(({ url, callback }) => { callback(undefined, { @@ -36,7 +38,7 @@ describe('detectAdBlockers', () => { responseURL: 'https://example.com/some/path/?view=ad', }; - detectAdBlockers(); + detectAdBlockers(defaultErrorHandler); effect(() => { expect(state.capabilities.isAdBlocked.value).toBe(true); @@ -52,7 +54,7 @@ describe('detectAdBlockers', () => { responseURL: 'data:text/css;charset=UTF-8;base64,dGVtcA==', }; - detectAdBlockers(); + detectAdBlockers(defaultErrorHandler); effect(() => { expect(state.capabilities.isAdBlocked.value).toBe(true); @@ -68,7 +70,7 @@ describe('detectAdBlockers', () => { responseURL: 'https://example.com/some/path/?view=ad', }; - detectAdBlockers(); + detectAdBlockers(defaultErrorHandler); effect(() => { expect(state.capabilities.isAdBlocked.value).toBe(false); diff --git a/packages/analytics-js/__tests__/components/configManager/ConfigManager.test.ts b/packages/analytics-js/__tests__/components/configManager/ConfigManager.test.ts index 9f1fc0915..2c92b8daa 100644 --- a/packages/analytics-js/__tests__/components/configManager/ConfigManager.test.ts +++ b/packages/analytics-js/__tests__/components/configManager/ConfigManager.test.ts @@ -151,6 +151,7 @@ describe('ConfigManager', () => { id: dummySourceConfigResponse.source.id, config: dummySourceConfigResponse.source.config, workspaceId: dummySourceConfigResponse.source.workspaceId, + name: dummySourceConfigResponse.source.name, }; state.lifecycle.dataPlaneUrl.value = sampleDataPlaneUrl; diff --git a/packages/analytics-js/__tests__/components/eventManager/EventManager.test.ts b/packages/analytics-js/__tests__/components/eventManager/EventManager.test.ts index bd76d974b..ac408c6c4 100644 --- a/packages/analytics-js/__tests__/components/eventManager/EventManager.test.ts +++ b/packages/analytics-js/__tests__/components/eventManager/EventManager.test.ts @@ -43,11 +43,10 @@ describe('EventManager', () => { }, }); - expect(mockErrorHandler.onError).toBeCalledWith( + expect(mockErrorHandler.onError).toHaveBeenCalledWith( new Error('Failed to generate the event object.'), 'EventManager', undefined, - undefined, ); }); diff --git a/packages/analytics-js/__tests__/components/eventRepository/EventRepository.test.ts b/packages/analytics-js/__tests__/components/eventRepository/EventRepository.test.ts index 1584ecc82..599dd96e7 100644 --- a/packages/analytics-js/__tests__/components/eventRepository/EventRepository.test.ts +++ b/packages/analytics-js/__tests__/components/eventRepository/EventRepository.test.ts @@ -6,6 +6,7 @@ import type { } from '@rudderstack/analytics-js-common/types/Destination'; import type { RudderEvent } from '@rudderstack/analytics-js-common/types/Event'; import type { IErrorHandler } from '@rudderstack/analytics-js-common/types/ErrorHandler'; +import { defaultHttpClient } from '../../../src/services/HttpClient'; import { EventRepository } from '../../../src/components/eventRepository'; import { state, resetState } from '../../../src/state'; import { PluginsManager } from '../../../src/components/pluginsManager'; @@ -90,7 +91,11 @@ describe('EventRepository', () => { }); it('should invoke appropriate plugins start on init', () => { - const eventRepository = new EventRepository(defaultPluginsManager, defaultStoreManager); + const eventRepository = new EventRepository( + defaultPluginsManager, + defaultStoreManager, + defaultHttpClient, + ); const spy = jest.spyOn(defaultPluginsManager, 'invokeSingle'); eventRepository.init(); @@ -127,7 +132,11 @@ describe('EventRepository', () => { }); it('should start the destinations events queue when the client destinations are ready', () => { - const eventRepository = new EventRepository(mockPluginsManager, defaultStoreManager); + const eventRepository = new EventRepository( + mockPluginsManager, + defaultStoreManager, + defaultHttpClient, + ); eventRepository.init(); @@ -137,7 +146,11 @@ describe('EventRepository', () => { }); it('should start the dataplane events queue when no hybrid destinations are present', () => { - const eventRepository = new EventRepository(mockPluginsManager, defaultStoreManager); + const eventRepository = new EventRepository( + mockPluginsManager, + defaultStoreManager, + defaultHttpClient, + ); state.nativeDestinations.activeDestinations.value = [ { @@ -164,7 +177,11 @@ describe('EventRepository', () => { }); it('should start the dataplane events queue when hybrid destinations are present and bufferDataPlaneEventsUntilReady is false', () => { - const eventRepository = new EventRepository(mockPluginsManager, defaultStoreManager); + const eventRepository = new EventRepository( + mockPluginsManager, + defaultStoreManager, + defaultHttpClient, + ); state.nativeDestinations.activeDestinations.value = activeDestinationsWithHybridMode; @@ -176,7 +193,11 @@ describe('EventRepository', () => { }); it('should start the dataplane events queue when hybrid destinations are present and bufferDataPlaneEventsUntilReady is true and client destinations are ready after some time', done => { - const eventRepository = new EventRepository(mockPluginsManager, defaultStoreManager); + const eventRepository = new EventRepository( + mockPluginsManager, + defaultStoreManager, + defaultHttpClient, + ); state.nativeDestinations.activeDestinations.value = activeDestinationsWithHybridMode; @@ -195,7 +216,11 @@ describe('EventRepository', () => { }); it('should start the dataplane events queue when hybrid destinations are present and bufferDataPlaneEventsUntilReady is true and client destinations are not ready until buffer timeout expires', done => { - const eventRepository = new EventRepository(mockPluginsManager, defaultStoreManager); + const eventRepository = new EventRepository( + mockPluginsManager, + defaultStoreManager, + defaultHttpClient, + ); state.nativeDestinations.activeDestinations.value = activeDestinationsWithHybridMode; @@ -213,7 +238,11 @@ describe('EventRepository', () => { }); it('should pass the enqueued event to both dataplane and destinations events queues', () => { - const eventRepository = new EventRepository(mockPluginsManager, defaultStoreManager); + const eventRepository = new EventRepository( + mockPluginsManager, + defaultStoreManager, + defaultHttpClient, + ); eventRepository.init(); @@ -246,7 +275,11 @@ describe('EventRepository', () => { }); it('should invoke event callback function if provided', () => { - const eventRepository = new EventRepository(mockPluginsManager, defaultStoreManager); + const eventRepository = new EventRepository( + mockPluginsManager, + defaultStoreManager, + defaultHttpClient, + ); eventRepository.init(); @@ -268,6 +301,7 @@ describe('EventRepository', () => { const eventRepository = new EventRepository( mockPluginsManager, defaultStoreManager, + defaultHttpClient, mockErrorHandler, ); @@ -283,12 +317,15 @@ describe('EventRepository', () => { new Error('test error'), 'EventRepository', 'API Callback Invocation Failed', - undefined, ); }); it('should buffer the data plane events if the pre-consent event delivery strategy is set to buffer', () => { - const eventRepository = new EventRepository(mockPluginsManager, defaultStoreManager); + const eventRepository = new EventRepository( + mockPluginsManager, + defaultStoreManager, + defaultHttpClient, + ); state.consents.preConsent.value = { enabled: true, @@ -307,7 +344,11 @@ describe('EventRepository', () => { describe('resume', () => { it('should resume events processing on resume', () => { - const eventRepository = new EventRepository(mockPluginsManager, defaultStoreManager); + const eventRepository = new EventRepository( + mockPluginsManager, + defaultStoreManager, + defaultHttpClient, + ); eventRepository.init(); eventRepository.resume(); @@ -315,7 +356,11 @@ describe('EventRepository', () => { }); it('should clear the events queue if discardPreConsentEvents is set to true', () => { - const eventRepository = new EventRepository(mockPluginsManager, defaultStoreManager); + const eventRepository = new EventRepository( + mockPluginsManager, + defaultStoreManager, + defaultHttpClient, + ); state.consents.postConsent.value.discardPreConsentEvents = true; diff --git a/packages/analytics-js/__tests__/components/pluginsManager/PluginsManager.test.ts b/packages/analytics-js/__tests__/components/pluginsManager/PluginsManager.test.ts index d39502057..e74f74581 100644 --- a/packages/analytics-js/__tests__/components/pluginsManager/PluginsManager.test.ts +++ b/packages/analytics-js/__tests__/components/pluginsManager/PluginsManager.test.ts @@ -64,20 +64,7 @@ describe('PluginsManager', () => { state.reporting.isErrorReportingEnabled.value = true; expect(pluginsManager.getPluginsToLoadBasedOnConfig().sort()).toEqual( - ['ErrorReporting', 'Bugsnag', 'ExternalAnonymousId', 'GoogleLinker'].sort(), - ); - }); - - it('should filter the error reporting plugins if they are not configured through the plugins input', () => { - state.reporting.isErrorReportingEnabled.value = true; - state.plugins.pluginsToLoadFromConfig.value = []; - - expect(pluginsManager.getPluginsToLoadBasedOnConfig().sort()).toEqual([]); - - // Expect a warning for user not explicitly configuring it - expect(defaultLogger.warn).toHaveBeenCalledTimes(1); - expect(defaultLogger.warn).toHaveBeenCalledWith( - "PluginsManager:: Error reporting is enabled, but ['ErrorReporting', 'Bugsnag'] plugins were not configured to load. Ignore if this was intentional. Otherwise, consider adding them to the 'plugins' load API option.", + ['ExternalAnonymousId', 'GoogleLinker'].sort(), ); }); diff --git a/packages/analytics-js/__tests__/components/userSessionManager/UserSessionManager.test.ts b/packages/analytics-js/__tests__/components/userSessionManager/UserSessionManager.test.ts index f7b0571ea..baa870104 100644 --- a/packages/analytics-js/__tests__/components/userSessionManager/UserSessionManager.test.ts +++ b/packages/analytics-js/__tests__/components/userSessionManager/UserSessionManager.test.ts @@ -84,6 +84,8 @@ describe('User session manager', () => { clientDataStoreLS = defaultStoreManager.getStore('clientDataInLocalStorage') as Store; clientDataStoreSession = defaultStoreManager.getStore('clientDataInSessionStorage') as Store; + defaultHttpClient.init(defaultErrorHandler); + clearStorage(); resetState(); diff --git a/packages/analytics-js/__tests__/services/ErrorHandler/ErrorHandler.test.ts b/packages/analytics-js/__tests__/services/ErrorHandler/ErrorHandler.test.ts index 34167240f..0bc64e8d4 100644 --- a/packages/analytics-js/__tests__/services/ErrorHandler/ErrorHandler.test.ts +++ b/packages/analytics-js/__tests__/services/ErrorHandler/ErrorHandler.test.ts @@ -1,329 +1,223 @@ -import type { SDKError } from '@rudderstack/analytics-js-common/types/ErrorHandler'; -import type { IExternalSrcLoader } from '@rudderstack/analytics-js-common/services/ExternalSrcLoader/types'; -import { defaultHttpClient } from '../../../src/services/HttpClient'; -import { defaultLogger } from '../../../src/services/Logger'; -import { defaultPluginEngine } from '../../../src/services/PluginEngine'; +import { defaultHttpClient } from '@rudderstack/analytics-js-common/__mocks__/HttpClient'; +import { defaultLogger } from '@rudderstack/analytics-js-common/__mocks__/Logger'; +import { resetState, state } from '../../../src/state'; import { ErrorHandler } from '../../../src/services/ErrorHandler'; -import * as processError from '../../../src/services/ErrorHandler/processError'; -import { state, resetState } from '../../../src/state'; - -jest.mock('../../../src/services/Logger', () => { - const originalModule = jest.requireActual('../../../src/services/Logger'); - - return { - __esModule: true, - ...originalModule, - defaultLogger: { - error: jest.fn((): void => {}), - }, - }; -}); - -jest.mock('../../../src/services/PluginEngine', () => { - const originalModule = jest.requireActual('../../../src/services/PluginEngine'); - - return { - __esModule: true, - ...originalModule, - defaultPluginEngine: { - invokeSingle: jest.fn((): void => {}), - }, - }; -}); - -jest.mock('../../../src/services/ErrorHandler/processError', () => { - const originalModule = jest.requireActual('../../../src/services/ErrorHandler/processError'); - - return { - __esModule: true, - ...originalModule, - processError: jest.fn((error: SDKError): string => error.message || error || ''), - }; -}); - -const extSrcLoader = {} as IExternalSrcLoader; describe('ErrorHandler', () => { let errorHandlerInstance: ErrorHandler; beforeEach(() => { resetState(); - state.reporting.isErrorReportingPluginLoaded.value = false; - errorHandlerInstance = new ErrorHandler(defaultLogger, defaultPluginEngine); - errorHandlerInstance.init(defaultHttpClient, extSrcLoader); - }); - - it('should leaveBreadcrumb if plugin engine is provided', () => { - errorHandlerInstance.leaveBreadcrumb('breadcrumb'); - expect(defaultPluginEngine.invokeSingle).toHaveBeenCalledTimes(2); - expect(defaultPluginEngine.invokeSingle).toHaveBeenCalledWith( - 'errorReporting.breadcrumb', - defaultPluginEngine, - undefined, - 'breadcrumb', - defaultLogger, - state, - ); + errorHandlerInstance = new ErrorHandler(defaultHttpClient, defaultLogger); }); - it('should notifyError if plugin engine is provided', () => { - errorHandlerInstance.notifyError(new Error('notify'), { - severity: 'error', - unhandled: false, - severityReason: { type: 'handledException' }, + describe('leaveBreadcrumb', () => { + it('should leave breadcrumb message', () => { + errorHandlerInstance.leaveBreadcrumb('sample breadcrumb message'); + expect(state.reporting.breadcrumbs.value.length).toBe(1); + expect(state.reporting.breadcrumbs.value).toEqual([ + { + type: 'manual', + metaData: {}, + name: 'sample breadcrumb message', + timestamp: expect.any(Date), + }, + ]); }); - expect(defaultPluginEngine.invokeSingle).toHaveBeenCalledTimes(2); - expect(defaultPluginEngine.invokeSingle).toHaveBeenCalledWith( - 'errorReporting.notify', - defaultPluginEngine, - undefined, - expect.any(Error), - state, - defaultLogger, - defaultHttpClient, - { - severity: 'error', - unhandled: false, - severityReason: { type: 'handledException' }, - }, - ); - }); - it('should log error for Errors with context and custom message if logger exists', () => { - state.reporting.isErrorReportingEnabled.value = true; - state.reporting.isErrorReportingPluginLoaded.value = true; - errorHandlerInstance.onError(new Error('dummy error'), 'Unit test', 'dummy custom message'); - - expect(defaultPluginEngine.invokeSingle).toHaveBeenCalledTimes(2); - expect(defaultPluginEngine.invokeSingle).toHaveBeenCalledWith( - 'errorReporting.notify', - defaultPluginEngine, - undefined, - expect.any(Error), - state, - defaultLogger, - defaultHttpClient, - { - severity: 'error', - unhandled: false, - severityReason: { type: 'handledException' }, - }, - ); + it('should append breadcrumb messages', () => { + errorHandlerInstance.leaveBreadcrumb('sample breadcrumb message 1'); + errorHandlerInstance.leaveBreadcrumb('sample breadcrumb message 2'); + expect(state.reporting.breadcrumbs.value.length).toBe(2); + expect(state.reporting.breadcrumbs.value).toEqual([ + { + type: 'manual', + metaData: {}, + name: 'sample breadcrumb message 1', + timestamp: expect.any(Date), + }, + { + type: 'manual', + metaData: {}, + name: 'sample breadcrumb message 2', + timestamp: expect.any(Date), + }, + ]); + }); - expect(defaultLogger.error).toHaveBeenCalledTimes(1); - expect(defaultLogger.error).toHaveBeenCalledWith( - 'Unit test:: dummy custom message dummy error', - ); - }); + it('should handle error if leaveBreadcrumb fails', () => { + const onErrorSpy = jest.spyOn(errorHandlerInstance, 'onError'); - it('should log error for messages with context and custom message if logger exists', () => { - state.reporting.isErrorReportingEnabled.value = true; - state.reporting.isErrorReportingPluginLoaded.value = true; - errorHandlerInstance.onError('dummy error', 'Unit test', 'dummy custom message'); + // @ts-expect-error cause an error for testing + state.reporting.breadcrumbs.value = null; - expect(defaultPluginEngine.invokeSingle).toHaveBeenCalledTimes(2); - expect(defaultPluginEngine.invokeSingle).toHaveBeenCalledWith( - 'errorReporting.notify', - defaultPluginEngine, - undefined, - expect.any(Error), - state, - defaultLogger, - defaultHttpClient, - { - severity: 'error', - unhandled: false, - severityReason: { type: 'handledException' }, - }, - ); + errorHandlerInstance.leaveBreadcrumb('sample breadcrumb message'); - expect(defaultLogger.error).toHaveBeenCalledTimes(1); - expect(defaultLogger.error).toHaveBeenCalledWith( - 'Unit test:: dummy custom message dummy error', - ); - }); - - it('should log and throw for messages with context and custom message if logger exists and shouldAlwaysThrow', () => { - try { - state.reporting.isErrorReportingEnabled.value = true; - state.reporting.isErrorReportingPluginLoaded.value = true; - errorHandlerInstance.onError('dummy error', 'Unit test', 'dummy custom message', true); - } catch (err) { - expect(defaultPluginEngine.invokeSingle).toHaveBeenCalledTimes(2); - expect(defaultPluginEngine.invokeSingle).toHaveBeenCalledWith( - 'errorReporting.notify', - defaultPluginEngine, - undefined, + expect(onErrorSpy).toHaveBeenCalledTimes(1); + expect(onErrorSpy).toHaveBeenCalledWith( expect.any(Error), - state, - defaultLogger, - defaultHttpClient, - { - severity: 'error', - unhandled: false, - severityReason: { type: 'handledException' }, - }, + 'ErrorHandler:: Failed to log breadcrumb.', ); - expect(defaultLogger.error).toHaveBeenCalledTimes(1); - expect(defaultLogger.error).toHaveBeenCalledWith( - 'Unit test:: dummy custom message dummy error', - ); - expect(err.message).toStrictEqual('Unit test:: dummy custom message dummy error'); - } + onErrorSpy.mockRestore(); + }); }); - it('should throw error for Errors with context and custom message if logger does not exist', () => { - errorHandlerInstance = new ErrorHandler(); - try { - errorHandlerInstance.onError(new Error('dummy error'), 'Unit test', 'dummy custom message'); - } catch (err) { - expect(defaultPluginEngine.invokeSingle).toHaveBeenCalledTimes(1); - expect(defaultLogger.error).toHaveBeenCalledTimes(0); - expect(err.message).toStrictEqual('Unit test:: dummy custom message dummy error'); - } - }); + describe('onError', () => { + it('should skip processing if error is not a valid error', () => { + errorHandlerInstance.onError({}); + + expect(defaultLogger.warn).toHaveBeenCalledTimes(1); + expect(defaultLogger.warn).toHaveBeenCalledWith('ErrorHandler:: Ignoring a non-error: {}.'); - it('should throw error for messages with context and custom message if logger does not exist', () => { - errorHandlerInstance = new ErrorHandler(); - try { - errorHandlerInstance.onError('dummy error', 'Unit test', 'dummy custom message'); - } catch (err) { - expect(defaultPluginEngine.invokeSingle).toHaveBeenCalledTimes(1); + // It should not be logged to the console expect(defaultLogger.error).toHaveBeenCalledTimes(0); - expect(err.message).toStrictEqual('Unit test:: dummy custom message dummy error'); - } - }); + }); - it('should swallow Errors based on processError logic', () => { - errorHandlerInstance.onError(''); + it('should skip errors if they are not originated from the sdk', () => { + // For this error, the stacktrace would not contain the sdk file names + errorHandlerInstance.onError(new Error('dummy error')); - expect(defaultPluginEngine.invokeSingle).toHaveBeenCalledTimes(1); - expect(defaultLogger.error).toHaveBeenCalledTimes(0); - }); + // It should not be logged to the console + expect(defaultLogger.error).toHaveBeenCalledTimes(0); + }); - it('should log error on notifyError if invoking plugin results in an exception', () => { - // Hard code the presence of the error reporting client - errorHandlerInstance.errReportingClient = {}; + it('should handle errors even if they are not originated from the sdk but installation type is NPM', () => { + // Set the installation type to NPM + // @ts-expect-error need to set the value for testing + state.context.app.value.installType = 'npm'; - defaultPluginEngine.invokeSingle = jest.fn(() => { - throw new Error('dummy error'); - }); + errorHandlerInstance.onError(new Error('dummy error'), 'Test'); - errorHandlerInstance.notifyError(new Error('notify'), { - severity: 'error', - unhandled: false, - severityReason: { type: 'handledException' }, + expect(defaultLogger.error).toHaveBeenCalledTimes(1); + expect(defaultLogger.error).toHaveBeenCalledWith(new Error('Test:: dummy error')); }); - expect(defaultLogger.error).toHaveBeenCalledTimes(1); - expect(defaultLogger.error).toHaveBeenCalledWith( - 'ErrorHandler:: Failed to notify the error.', - new Error('dummy error'), - ); - }); + it('should not log unhandled errors to the console', () => { + // @ts-expect-error not using the enum value for testing + errorHandlerInstance.onError( + new Error('dummy error'), + 'Test', + undefined, + 'unhandledException', + ); - it('should invoke onError on leaveBreadcrumb if invoking plugin results in an exception', () => { - defaultPluginEngine.invokeSingle = jest.fn(() => { - throw new Error('dummy error'); + expect(defaultLogger.error).toHaveBeenCalledTimes(0); }); - const onErrorSpy = jest.spyOn(errorHandlerInstance, 'onError'); - errorHandlerInstance.leaveBreadcrumb('breadcrumb'); + it('should log unhandled errors that are explicitly dispatched by the SDK', () => { + const error = new Error('dummy error'); + // Explicitly mark the error as dispatched by the SDK + error.stack += '[SDK DISPATCHED ERROR]'; + const errorEvent = new ErrorEvent('error', { error }); - expect(onErrorSpy).toHaveBeenCalledTimes(1); - expect(onErrorSpy).toHaveBeenCalledWith( - expect.any(Error), - 'ErrorHandler', - 'errorReporting.breadcrumb', - ); - onErrorSpy.mockRestore(); - }); + // @ts-expect-error not using the enum value for testing + errorHandlerInstance.onError(errorEvent, 'Test', undefined, 'unhandledException'); - it('should invoke getNormalizedErrorForUnhandledError fn to normalize unhandled error types', () => { - const getNormalizedErrorForUnhandledErrorSpy = jest.spyOn( - processError, - 'getNormalizedErrorForUnhandledError', - ); - errorHandlerInstance.onError( - new ErrorEvent('error'), - undefined, - undefined, - undefined, - 'unhandledException', - ); - expect(getNormalizedErrorForUnhandledErrorSpy).toHaveBeenCalled(); - }); + expect(defaultLogger.error).toHaveBeenCalledTimes(1); + expect(defaultLogger.error).toHaveBeenCalledWith(new Error('Test:: dummy error')); + }); - describe('init', () => { - it('reporting client should not be defined if the plugin engine is not supplied', () => { - errorHandlerInstance = new ErrorHandler(defaultLogger); - errorHandlerInstance.init(defaultHttpClient); + it('should not notify errors if error reporting is disabled', () => { + state.reporting.isErrorReportingEnabled.value = false; + errorHandlerInstance.onError(new Error('dummy error')); - expect(errorHandlerInstance.httpClient).not.toBeUndefined(); + expect(defaultHttpClient.getAsyncData).toHaveBeenCalledTimes(0); }); - }); - it('should attach error listeners', () => { - const unhandledRejectionListener = jest.spyOn(window, 'addEventListener'); - errorHandlerInstance.attachErrorListeners(); - expect(unhandledRejectionListener).toHaveBeenCalledTimes(2); - expect(unhandledRejectionListener).toHaveBeenCalledWith('error', expect.any(Function)); - expect(unhandledRejectionListener).toHaveBeenCalledWith( - 'unhandledrejection', - expect.any(Function), - ); - }); + it('should not notify errors if the error message is not allowed to be notified', () => { + state.reporting.isErrorReportingEnabled.value = true; + // "The request failed" is one of the messages that should not be notified + errorHandlerInstance.onError(new Error('The request failed due to some issue')); - it('should notify buffered errors once Error reporting plugin is loaded', () => { - errorHandlerInstance.notifyError = jest.fn(); - errorHandlerInstance.errorBuffer.enqueue({ - error: new Error('dummy error'), - errorState: { - severity: 'error', - unhandled: false, - severityReason: { type: 'handledException' }, - }, + expect(defaultHttpClient.getAsyncData).toHaveBeenCalledTimes(0); }); - state.reporting.isErrorReportingPluginLoaded.value = true; - setTimeout(() => { - expect(errorHandlerInstance.attachEffect).toHaveBeenCalledTimes(1); - expect(errorHandlerInstance.errorBuffer.size()).toBe(0); - expect(errorHandlerInstance.notifyError).toHaveBeenCalledTimes(1); - }, 1); - }); - it('should enqueue errors if Error reporting plugin is not loaded', () => { - errorHandlerInstance.errorBuffer.enqueue = jest.fn(); - state.reporting.isErrorReportingEnabled.value = true; - errorHandlerInstance.onError(new Error('dummy error')); - expect(errorHandlerInstance.errorBuffer.enqueue).toHaveBeenCalledTimes(1); - }); + it.skip('should notify errors if error reporting is enabled and the error message is allowed to be notified', () => { + state.reporting.isErrorReportingEnabled.value = true; + state.metrics.metricsServiceUrl.value = 'https://dummy.dataplane.com/rsaMetrics'; - it('should not invoke the plugin if Error reporting plugin is not loaded', () => { - errorHandlerInstance.attachEffect(); - expect(defaultPluginEngine.invokeSingle).toHaveBeenCalledTimes(1); - }); + // @ts-expect-error Ensure that error is notified. Force set the install type to NPM + state.context.app.value.installType = 'npm'; + + errorHandlerInstance.onError(new Error('dummy error')); - it('should log error in case unhandled error occurs during processing or notifying the error', () => { - state.reporting.isErrorReportingEnabled.value = true; - state.reporting.isErrorReportingPluginLoaded.value = true; - const dummyError = new Error('dummy error'); - errorHandlerInstance.notifyError = jest.fn(() => { - throw dummyError; + const notifyPayload = { + version: '1', + message_id: expect.any(String), + source: { + name: 'js', + sdk_version: '1.0.0', + write_key: '', + install_type: 'npm', + }, + errors: { + payloadVersion: '5', + notifier: { + name: 'RudderStack JavaScript SDK', + version: '1.0.0', + url: 'https://', + }, + events: [ + { + exceptions: [ + { + message: 'dummy error', + errorClass: 'Error', + type: 'browserjs', + stacktrace: expect.any(String), + }, + ], + severity: 'error', + unhandled: false, + severityReason: { type: 'handledException' }, + app: { + version: '1.0.0', + releaseStage: 'production', + type: 'npm', + }, + device: { + locale: undefined, + userAgent: undefined, + time: expect.any(Date), + }, + request: { + url: '', + clientIp: '[NOT COLLECTED]', + }, + breadcrumbs: [], + metaData: { + app: { + snippetVersion: undefined, + }, + device: { + density: 0, + width: 0, + height: 0, + innerWidth: 0, + innerHeight: 0, + timezone: undefined, + }, + }, + user: { + id: '', + name: '', + }, + }, + ], + }, + }; + + expect(defaultHttpClient.getAsyncData).toHaveBeenCalledTimes(1); + expect(defaultHttpClient.getAsyncData).toHaveBeenCalledWith({ + url: 'https://dummy.dataplane.com/rsaMetrics', + options: { + method: 'POST', + data: JSON.stringify(notifyPayload), + sendRawData: true, + }, + isRawResponse: true, + }); }); - errorHandlerInstance.logger.error = jest.fn(); - errorHandlerInstance.onError( - new Error('test error'), - undefined, - undefined, - undefined, - 'unhandledException', - ); - expect(errorHandlerInstance.logger.error).toHaveBeenCalledTimes(1); - expect(errorHandlerInstance.logger.error).toHaveBeenCalledWith( - 'ErrorHandler:: Failed to notify the error.', - dummyError, - ); }); }); diff --git a/packages/analytics-js/__tests__/services/ErrorHandler/processError.test.ts b/packages/analytics-js/__tests__/services/ErrorHandler/processError.test.ts deleted file mode 100644 index 302ec0fbe..000000000 --- a/packages/analytics-js/__tests__/services/ErrorHandler/processError.test.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { - processError, - getNormalizedErrorForUnhandledError, -} from '../../../src/services/ErrorHandler/processError'; - -jest.mock('../../../src/components/utilities/event', () => { - const originalModule = jest.requireActual('../../../src/components/utilities/event'); - - return { - __esModule: true, - ...originalModule, - isEvent: jest.fn((event): boolean => event && Boolean(event.target)), - }; -}); - -describe('ErrorHandler - process error', () => { - it('should not throw error for non supported argument value', () => { - const msg = processError(null); - expect(msg).toStrictEqual("Unknown error: Cannot read properties of null (reading 'message')"); - }); - - it('should process string argument value', () => { - const msg = processError('dummy error'); - expect(msg).toStrictEqual('dummy error'); - }); - - it('should process Error argument value', () => { - const msg = processError(new Error('dummy error')); - expect(msg).toStrictEqual('dummy error'); - }); - - it('should process ErrorEvent argument value', () => { - const msg = processError(new ErrorEvent('dummy error', { message: 'dummy error' })); - expect(msg).toStrictEqual('dummy error'); - }); - - it('should process any other type of argument value', () => { - const msg = processError({ foo: 'bar' }); - expect(msg).toStrictEqual('{"foo":"bar"}'); - }); - - it('should process any other type of argument value with message property', () => { - const msg = processError({ foo: 'bar', message: 'dummy error' }); - expect(msg).toStrictEqual('dummy error'); - }); -}); - -describe('ErrorHandler - getNormalizedErrorForUnhandledError', () => { - it('should return error instance for Error argument value', () => { - const error = new Error('dummy error'); - const normalizedError = getNormalizedErrorForUnhandledError(error); - expect(normalizedError).toStrictEqual(error); - }); - - it('should return error instance for ErrorEvent argument value', () => { - const error = new ErrorEvent('dummy error'); - const normalizedError = getNormalizedErrorForUnhandledError(error); - expect(normalizedError).toStrictEqual(error); - }); - - it('should return error instance for PromiseRejectionEvent argument value', () => { - const error = new PromiseRejectionEvent('dummy error'); - const normalizedError = getNormalizedErrorForUnhandledError(error); - expect(normalizedError).toStrictEqual(error); - }); - - it('should return undefined for Event argument value', () => { - const event = new Event('dummyError'); - const targetElement = document.createElement('div'); - targetElement.id = 'targetElement'; - document.body.appendChild(targetElement); - - let normalizedError; - - targetElement.addEventListener('dummyError', e => { - normalizedError = getNormalizedErrorForUnhandledError(e); - }); - targetElement.dispatchEvent(event); - expect(normalizedError).toBeUndefined(); - }); - - it('should return undefined for Event argument value with non SDK script target', () => { - const event = new Event('dummyError'); - const targetElement = document.createElement('script'); - targetElement.id = 'targetElement'; - document.body.appendChild(targetElement); - - let normalizedError; - - targetElement.addEventListener('dummyError', e => { - normalizedError = getNormalizedErrorForUnhandledError(e); - }); - targetElement.dispatchEvent(event); - expect(normalizedError).toBeUndefined(); - }); - - 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'; - targetElement.dataset.isnonnativesdk = 'true'; - targetElement.id = 'dummy'; - targetElement.src = 'dummy'; - document.body.appendChild(targetElement); - - let normalizedError; - - targetElement.addEventListener('dummyError', e => { - normalizedError = getNormalizedErrorForUnhandledError(e); - }); - targetElement.dispatchEvent(event); - console.log('normalizedError', normalizedError); - expect(normalizedError?.message).toStrictEqual( - 'Error in loading a third-party script from URL https://www.test-host.com/dummy with ID dummy.', - ); - }); -}); diff --git a/packages/analytics-js/__tests__/services/ErrorHandler/utils.test.ts b/packages/analytics-js/__tests__/services/ErrorHandler/utils.test.ts index 3d09f662b..20d16504a 100644 --- a/packages/analytics-js/__tests__/services/ErrorHandler/utils.test.ts +++ b/packages/analytics-js/__tests__/services/ErrorHandler/utils.test.ts @@ -1,9 +1,9 @@ /* eslint-disable max-classes-per-file */ import { signal } from '@preact/signals-core'; -import type { ErrorEventPayload } from '@rudderstack/analytics-js-common/types/Metrics'; +import type { ErrorEventPayload, Exception } from '@rudderstack/analytics-js-common/types/Metrics'; import { mergeDeepRight } from '@rudderstack/analytics-js-common/utilities/object'; import { state } from '../../../src/state'; -import * as errorReportingConstants from '../../../src/services/ErrorHandler/constant'; +import * as errorReportingConstants from '../../../src/services/ErrorHandler/constants'; import { createNewBreadcrumb, getAppStateForMetadata, @@ -12,7 +12,7 @@ import { getReleaseStage, getURLWithoutQueryString, isAllowedToBeNotified, - isRudderSDKError, + isSDKError, } from '../../../src/services/ErrorHandler/utils'; jest.mock('@rudderstack/analytics-js-common/utilities/uuId', () => ({ @@ -52,15 +52,15 @@ const DEFAULT_STATE_DATA = { }, context: { app: { - installType: 'cdn', + installType: '__MODULE_TYPE__', name: 'RudderLabs JavaScript SDK', namespace: 'com.rudderlabs.javascript', - version: 'dev-snapshot', + version: '__PACKAGE_VERSION__', }, device: null, library: { name: 'RudderLabs JavaScript SDK', - version: 'dev-snapshot', + version: '__PACKAGE_VERSION__', }, locale: null, network: null, @@ -233,7 +233,7 @@ describe('Error Reporting utilities', () => { ); }); - describe('isRudderSDKError', () => { + describe('isSDKError', () => { const testCaseData: any[] = [ ['https://invalid-domain.com/rsa.min.js', true], ['https://invalid-domain.com/rss.min.js', false], @@ -263,7 +263,7 @@ describe('Error Reporting utilities', () => { ], }; - expect(isRudderSDKError(event)).toBe(expectedValue); + expect(isSDKError(event)).toBe(expectedValue); }, ); }); @@ -415,7 +415,7 @@ describe('Error Reporting utilities', () => { }); describe('getBugsnagErrorEvent', () => { - it('should return enhanced error event payload', () => { + it.skip('should return enhanced error event payload', () => { state.session.sessionInfo.value = { id: 123 }; state.autoTrack.pageLifecycle.visitId.value = 'test-visit-id'; @@ -580,8 +580,8 @@ describe('Error Reporting utilities', () => { message_id: 'test_uuid', source: { name: 'js', - sdk_version: 'dev-snapshot', - install_type: 'cdn', + sdk_version: '__PACKAGE_VERSION__', + install_type: '__MODULE_TYPE__', }, errors: enhancedErrorPayload, }), @@ -590,19 +590,16 @@ describe('Error Reporting utilities', () => { }); describe('isAllowedToBeNotified', () => { - it('should return true for Error argument value', () => { - const result = isAllowedToBeNotified({ message: 'dummy error' }); + it('should return true if the error is allowed to be notified', () => { + const result = isAllowedToBeNotified({ message: 'dummy error' } as unknown as Exception); expect(result).toBeTruthy(); }); - it('should return true for Error argument value', () => { - const result = isAllowedToBeNotified({ message: 'The request failed' }); + it('should return false if the error is not allowed to be notified', () => { + const result = isAllowedToBeNotified({ + message: 'The request failed', + } as unknown as Exception); 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/__tests__/services/HttpClient/HttpClient.test.ts b/packages/analytics-js/__tests__/services/HttpClient/HttpClient.test.ts index 2e7349d8d..f480ae9f8 100644 --- a/packages/analytics-js/__tests__/services/HttpClient/HttpClient.test.ts +++ b/packages/analytics-js/__tests__/services/HttpClient/HttpClient.test.ts @@ -38,7 +38,8 @@ describe('HttpClient', () => { }); beforeEach(() => { - clientInstance = new HttpClient(defaultErrorHandler, defaultLogger); + clientInstance = new HttpClient(defaultLogger); + clientInstance.init(defaultErrorHandler); }); afterEach(() => { @@ -88,8 +89,8 @@ describe('HttpClient', () => { }); }); - it('should fire and forget getAsyncData', async () => { - const response = await clientInstance.getAsyncData({ + it('should fire and forget getAsyncData', () => { + const response = clientInstance.getAsyncData({ url: `${dummyDataplaneHost}/jsonSample`, }); expect(response).toBeUndefined(); diff --git a/packages/analytics-js/project.json b/packages/analytics-js/project.json index 4adfc485a..b29514b5d 100644 --- a/packages/analytics-js/project.json +++ b/packages/analytics-js/project.json @@ -5,13 +5,6 @@ "projectType": "library", "tags": ["type:sdk", "scope:analytics-v3"], "targets": { - "prepare-test-mocks": { - "executor": "nx:run-commands", - "options": { - "command": "sed 's/var rudderanalytics/rudderanalytics/g' packages/analytics-js/dist/cdn/legacy/iife/rsa.min.js > packages/analytics-js/__mocks__/cdnSDK.js" - }, - "dependsOn": ["build:browser"] - }, "test": { "executor": "@nx/jest:jest", "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], @@ -27,8 +20,7 @@ "ci": true, "codeCoverage": true } - }, - "dependsOn": ["prepare-test-mocks"] + } }, "lint": { "executor": "@nx/eslint:lint", diff --git a/packages/analytics-js/src/constants/logMessages.ts b/packages/analytics-js/src/constants/logMessages.ts index 55cbdd869..04355f619 100644 --- a/packages/analytics-js/src/constants/logMessages.ts +++ b/packages/analytics-js/src/constants/logMessages.ts @@ -34,7 +34,7 @@ const UNSUPPORTED_CONSENT_MANAGER_ERROR = ( )}".`; const NON_ERROR_WARNING = (context: string, errStr: Nullable): string => - `${context}${LOG_CONTEXT_SEPARATOR}Received a non-error: ${errStr}.`; + `${context}${LOG_CONTEXT_SEPARATOR}Ignoring a non-error: ${errStr}.`; const FAILED_ATTACH_LISTENERS_ERROR = (context: string): string => `${context}${LOG_CONTEXT_SEPARATOR}Failed to attach global error listeners.`; diff --git a/packages/analytics-js/src/services/ErrorHandler/ErrorHandler.ts b/packages/analytics-js/src/services/ErrorHandler/ErrorHandler.ts index 8aea3e9a7..92880f1e0 100644 --- a/packages/analytics-js/src/services/ErrorHandler/ErrorHandler.ts +++ b/packages/analytics-js/src/services/ErrorHandler/ErrorHandler.ts @@ -9,7 +9,10 @@ import type { ILogger } from '@rudderstack/analytics-js-common/types/Logger'; import { ERROR_HANDLER } from '@rudderstack/analytics-js-common/constants/loggerContexts'; import { LOG_CONTEXT_SEPARATOR } from '@rudderstack/analytics-js-common/constants/logMessages'; import type { IHttpClient } from '@rudderstack/analytics-js-common/types/HttpClient'; -import { MANUAL_ERROR_IDENTIFIER } from '@rudderstack/analytics-js-common/utilities/errors'; +import { + getStacktrace, + MANUAL_ERROR_IDENTIFIER, +} from '@rudderstack/analytics-js-common/utilities/errors'; import { BREADCRUMB_ERROR, FAILED_ATTACH_LISTENERS_ERROR, @@ -23,7 +26,7 @@ import { getErrInstance, getErrorDeliveryPayload, isAllowedToBeNotified, - isRudderSDKError, + isSDKError, } from './utils'; import { createBugsnagException, normalizeError } from './event/event'; import { defaultHttpClient } from '../HttpClient'; @@ -45,13 +48,13 @@ class ErrorHandler implements IErrorHandler { attachErrorListeners() { if ('addEventListener' in (globalThis as typeof window)) { (globalThis as typeof window).addEventListener('error', (event: ErrorEvent | Event) => { - this.onError(event, undefined, undefined, ErrorType.UNHANDLEDEXCEPTION); + this.onError(event, ERROR_HANDLER, undefined, ErrorType.UNHANDLEDEXCEPTION); }); (globalThis as typeof window).addEventListener( 'unhandledrejection', (event: PromiseRejectionEvent) => { - this.onError(event, undefined, undefined, ErrorType.UNHANDLEDREJECTION); + this.onError(event, ERROR_HANDLER, undefined, ErrorType.UNHANDLEDREJECTION); }, ); } else { @@ -72,11 +75,19 @@ class ErrorHandler implements IErrorHandler { return; } - const errorMsgPrefix = `${context}${LOG_CONTEXT_SEPARATOR}${customMessage} `; + const errorMsgPrefix = `${context}${LOG_CONTEXT_SEPARATOR}${customMessage}`; const bsException = createBugsnagException(normalizedError, errorMsgPrefix); - // filter errors - if (!isRudderSDKError(bsException)) { + const stacktrace = getStacktrace(normalizedError); + const isSdkDispatched = stacktrace?.includes(MANUAL_ERROR_IDENTIFIER); + + // Filter errors that are not originated in the SDK. + // However, in case of NPM installations, since we cannot differentiate between SDK and application errors, we should report all errors. + if ( + !isSDKError(bsException) && + state.context.app.value.installType !== 'npm' && + !isSdkDispatched + ) { return; } @@ -102,17 +113,13 @@ class ErrorHandler implements IErrorHandler { }); } - // Log only the handled exceptions to the console - // Unhandled exceptions are already logged by the browser - if (errorType === ErrorType.HANDLEDEXCEPTION) { + // Log handled errors and errors dispatched by the SDK + if (errorType === ErrorType.HANDLEDEXCEPTION || isSdkDispatched) { this.logger?.error( Object.create(normalizedError, { message: { value: bsException.message }, }), ); - // Log special errors thrown by the SDK - } else if ((error as any).error?.stack?.includes(MANUAL_ERROR_IDENTIFIER)) { - this.logger?.error('An unknown error occurred:', (error as ErrorEvent).error?.message); } } catch (err) { // If an error occurs while handling an error, log it diff --git a/packages/analytics-js/src/services/ErrorHandler/event/event.ts b/packages/analytics-js/src/services/ErrorHandler/event/event.ts index 08d06779c..9232a51c6 100644 --- a/packages/analytics-js/src/services/ErrorHandler/event/event.ts +++ b/packages/analytics-js/src/services/ErrorHandler/event/event.ts @@ -4,7 +4,7 @@ import type { Exception, Stackframe } from '@rudderstack/analytics-js-common/typ import { ERROR_HANDLER } from '@rudderstack/analytics-js-common/constants/loggerContexts'; import { stringifyWithoutCircular } from '@rudderstack/analytics-js-common/utilities/json'; import { isString, isTypeOfError } from '@rudderstack/analytics-js-common/utilities/checks'; -import { hasStack } from '@rudderstack/analytics-js-common/utilities/errors'; +import { getStacktrace } from '@rudderstack/analytics-js-common/utilities/errors'; import { NON_ERROR_WARNING } from '../../../constants/logMessages'; import type { FrameType } from './types'; @@ -63,7 +63,7 @@ function createException( const normalizeError = (maybeError: any, logger?: ILogger): any | undefined => { let error; - if (isTypeOfError(maybeError) && hasStack(maybeError)) { + if (isTypeOfError(maybeError) && !!getStacktrace(maybeError)) { error = maybeError; } else { logger?.warn(NON_ERROR_WARNING(ERROR_HANDLER, stringifyWithoutCircular(maybeError))); diff --git a/packages/analytics-js/src/services/ErrorHandler/utils.ts b/packages/analytics-js/src/services/ErrorHandler/utils.ts index 61acd94ef..da6a0a673 100644 --- a/packages/analytics-js/src/services/ErrorHandler/utils.ts +++ b/packages/analytics-js/src/services/ErrorHandler/utils.ts @@ -132,7 +132,7 @@ const isAllowedToBeNotified = (exception: Exception) => * @param {Error} exception * @returns */ -const isRudderSDKError = (exception: Exception) => { +const isSDKError = (exception: Exception) => { const errorOrigin = exception.stacktrace?.[0]?.file; if (!errorOrigin || typeof errorOrigin !== 'string') { @@ -175,7 +175,7 @@ export { getAppStateForMetadata, getBugsnagErrorEvent, getURLWithoutQueryString, - isRudderSDKError, + isSDKError, getErrorDeliveryPayload, isAllowedToBeNotified, }; diff --git a/packages/analytics-js/src/state/slices/reporting.ts b/packages/analytics-js/src/state/slices/reporting.ts index 9377a3666..6e4ecff5c 100644 --- a/packages/analytics-js/src/state/slices/reporting.ts +++ b/packages/analytics-js/src/state/slices/reporting.ts @@ -4,7 +4,6 @@ import type { ReportingState } from '@rudderstack/analytics-js-common/types/Appl const reportingState: ReportingState = { isErrorReportingEnabled: signal(false), isMetricsReportingEnabled: signal(false), - isErrorReportingPluginLoaded: signal(false), breadcrumbs: signal([]), }; From c34b0a2f2678cf883ee9d113e673eeb9baf4356d Mon Sep 17 00:00:00 2001 From: Sai Kumar Battinoju Date: Thu, 2 Jan 2025 15:40:56 +0530 Subject: [PATCH 08/53] chore: fix test target --- packages/analytics-js/package.json | 6 +++--- packages/analytics-js/project.json | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/analytics-js/package.json b/packages/analytics-js/package.json index f4017fb3a..b16249d77 100644 --- a/packages/analytics-js/package.json +++ b/packages/analytics-js/package.json @@ -112,6 +112,7 @@ "build": "npm run build:browser && npm run build:package", "build:modern": "npm run build:browser:modern && npm run build:package:modern", "build:browser": "rollup -c --environment VERSION:$npm_package_version,UGLIFY,PROD_DEBUG,ENV:prod", + "build:browser:dev": "rollup -c --environment PROD_DEBUG", "build:browser:modern": "BROWSERSLIST_ENV=modern npm run build:browser", "build:package": "npm run build:npm && npm run build:npm:bundled && npm run build:npm:content-script", "build:package:modern": "npm run build:npm:modern && npm run build:npm:bundled:modern && npm run build:npm:content-script:modern", @@ -121,9 +122,8 @@ "build:npm:bundled:modern": "BUNDLED_PLUGINS=all npm run build:npm:modern", "build:npm:content-script": "BUNDLED_PLUGINS=all NO_EXTERNAL_HOST=true npm run build:npm", "build:npm:content-script:modern": "BUNDLED_PLUGINS=all NO_EXTERNAL_HOST=true npm run build:npm:modern", - "build:browser:test": "ONLY_IIFE=true npm run build:browser && sed 's/var rudderanalytics/rudderanalytics/g' dist/cdn/legacy/iife/rsa.min.js > __mocks__/cdnSDK.js", - "test": "npm run build:browser:test && nx test --maxWorkers=50%", - "test:ci": "npm run build:browser:test && nx test --configuration=ci --runInBand --maxWorkers=1 --forceExit", + "test": "nx test --maxWorkers=50%", + "test:ci": "nx test --configuration=ci --runInBand --maxWorkers=1 --forceExit", "check:lint": "nx lint", "check:lint:ci": "nx lint --configuration=ci", "check:size:build": "npm run build:browser && npm run build:browser:modern && npm run build:package && npm run build:package:modern", diff --git a/packages/analytics-js/project.json b/packages/analytics-js/project.json index b29514b5d..fd9d24317 100644 --- a/packages/analytics-js/project.json +++ b/packages/analytics-js/project.json @@ -20,7 +20,8 @@ "ci": true, "codeCoverage": true } - } + }, + "dependsOn": ["build:browser:dev"] }, "lint": { "executor": "@nx/eslint:lint", From d40d8cf02322ad66c278d3335c292e2b2f244303 Mon Sep 17 00:00:00 2001 From: Sai Kumar Battinoju Date: Thu, 2 Jan 2025 16:24:47 +0530 Subject: [PATCH 09/53] test: add tests for errorhandler --- jest/jest.setup-dom.js | 20 ++- .../ErrorHandler/ErrorHandler.test.ts | 130 ++++++++---------- .../analytics-js/src/constants/logMessages.ts | 4 - .../src/services/ErrorHandler/ErrorHandler.ts | 28 ++-- 4 files changed, 87 insertions(+), 95 deletions(-) diff --git a/jest/jest.setup-dom.js b/jest/jest.setup-dom.js index 11be1cf08..ba3545178 100644 --- a/jest/jest.setup-dom.js +++ b/jest/jest.setup-dom.js @@ -8,9 +8,23 @@ global.window.innerHeight = 1024; global.window.__BUNDLE_ALL_PLUGINS__ = false; global.window.__IS_LEGACY_BUILD__ = false; global.window.__IS_DYNAMIC_CUSTOM_BUNDLE__ = false; -global.PromiseRejectionEvent = function (reason) { - this.reason = reason; -}; + +// Only define the mock if it's not already defined (e.g., in a real browser) +if (typeof PromiseRejectionEvent === 'undefined') { + // Mock class (very minimal) + class PromiseRejectionEvent extends Event { + constructor(type, eventInitDict) { + super(type, eventInitDict); + this.promise = eventInitDict?.promise; + this.reason = eventInitDict?.reason; + } + } + + // Attach it to the global object so tests can use it. + global.PromiseRejectionEvent = PromiseRejectionEvent; + // If you rely on "window" instead: + // global.window.PromiseRejectionEvent = PromiseRejectionEvent; +} // TODO: remove once we use globalThis in analytics v1.1 too // Setup mocking for window.navigator diff --git a/packages/analytics-js/__tests__/services/ErrorHandler/ErrorHandler.test.ts b/packages/analytics-js/__tests__/services/ErrorHandler/ErrorHandler.test.ts index 0bc64e8d4..47ea394df 100644 --- a/packages/analytics-js/__tests__/services/ErrorHandler/ErrorHandler.test.ts +++ b/packages/analytics-js/__tests__/services/ErrorHandler/ErrorHandler.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable compat/compat */ import { defaultHttpClient } from '@rudderstack/analytics-js-common/__mocks__/HttpClient'; import { defaultLogger } from '@rudderstack/analytics-js-common/__mocks__/Logger'; import { resetState, state } from '../../../src/state'; @@ -11,6 +12,39 @@ describe('ErrorHandler', () => { errorHandlerInstance = new ErrorHandler(defaultHttpClient, defaultLogger); }); + it('should attach error listeners for unhandled errors', () => { + const onErrorSpy = jest.spyOn(errorHandlerInstance, 'onError'); + + // Dispatch an unhandled error + const errorEvent = new ErrorEvent('error', { error: new Error('dummy error') }); + (globalThis as typeof window).dispatchEvent(errorEvent); + + expect(onErrorSpy).toHaveBeenCalledTimes(1); + expect(onErrorSpy).toHaveBeenCalledWith( + errorEvent, + 'ErrorHandler', + undefined, + 'unhandledException', + ); + + onErrorSpy.mockReset(); + + // Dispatch an unhandled rejection + const promiseRejectionEvent = new PromiseRejectionEvent('unhandledrejection', { + reason: new Error('dummy error'), + promise: Promise.resolve(), + }); + (globalThis as typeof window).dispatchEvent(promiseRejectionEvent); + + expect(onErrorSpy).toHaveBeenCalledTimes(1); + expect(onErrorSpy).toHaveBeenCalledWith( + promiseRejectionEvent, + 'ErrorHandler', + undefined, + 'unhandledPromiseRejection', + ); + }); + describe('leaveBreadcrumb', () => { it('should leave breadcrumb message', () => { errorHandlerInstance.leaveBreadcrumb('sample breadcrumb message'); @@ -94,11 +128,11 @@ describe('ErrorHandler', () => { }); it('should not log unhandled errors to the console', () => { - // @ts-expect-error not using the enum value for testing errorHandlerInstance.onError( new Error('dummy error'), 'Test', undefined, + // @ts-expect-error not using the enum value for testing 'unhandledException', ); @@ -133,91 +167,47 @@ describe('ErrorHandler', () => { expect(defaultHttpClient.getAsyncData).toHaveBeenCalledTimes(0); }); - it.skip('should notify errors if error reporting is enabled and the error message is allowed to be notified', () => { + it('should notify errors if error reporting is enabled and the error message is allowed to be notified', () => { state.reporting.isErrorReportingEnabled.value = true; + state.lifecycle.writeKey.value = 'dummy-write-key'; state.metrics.metricsServiceUrl.value = 'https://dummy.dataplane.com/rsaMetrics'; // @ts-expect-error Ensure that error is notified. Force set the install type to NPM state.context.app.value.installType = 'npm'; - errorHandlerInstance.onError(new Error('dummy error')); - - const notifyPayload = { - version: '1', - message_id: expect.any(String), - source: { - name: 'js', - sdk_version: '1.0.0', - write_key: '', - install_type: 'npm', - }, - errors: { - payloadVersion: '5', - notifier: { - name: 'RudderStack JavaScript SDK', - version: '1.0.0', - url: 'https://', - }, - events: [ - { - exceptions: [ - { - message: 'dummy error', - errorClass: 'Error', - type: 'browserjs', - stacktrace: expect.any(String), - }, - ], - severity: 'error', - unhandled: false, - severityReason: { type: 'handledException' }, - app: { - version: '1.0.0', - releaseStage: 'production', - type: 'npm', - }, - device: { - locale: undefined, - userAgent: undefined, - time: expect.any(Date), - }, - request: { - url: '', - clientIp: '[NOT COLLECTED]', - }, - breadcrumbs: [], - metaData: { - app: { - snippetVersion: undefined, - }, - device: { - density: 0, - width: 0, - height: 0, - innerWidth: 0, - innerHeight: 0, - timezone: undefined, - }, - }, - user: { - id: '', - name: '', - }, - }, - ], - }, - }; + const error = new Error('dummy error'); + error.stack = + 'Error: Test:: dummy error\n at Object. (https://example.com/sample.js:1:1)'; + errorHandlerInstance.onError(error, 'Test'); expect(defaultHttpClient.getAsyncData).toHaveBeenCalledTimes(1); expect(defaultHttpClient.getAsyncData).toHaveBeenCalledWith({ url: 'https://dummy.dataplane.com/rsaMetrics', options: { method: 'POST', - data: JSON.stringify(notifyPayload), + data: expect.any(String), sendRawData: true, }, isRawResponse: true, }); }); + + it('should log error if an error occurs while handling an error', () => { + // @ts-expect-error Ensure that error is notified + state.context.app.value.installType = 'npm'; + state.reporting.isErrorReportingEnabled.value = true; + + defaultHttpClient.getAsyncData.mockImplementationOnce(() => { + throw new Error('Failed to notify error'); + }); + + errorHandlerInstance.onError(new Error('dummy error'), 'Test'); + + expect(defaultLogger.error).toHaveBeenCalledTimes(1); + expect(defaultLogger.error).toHaveBeenCalledWith( + 'ErrorHandler:: Failed to handle the error.', + new Error('Failed to notify error'), + ); + }); }); }); diff --git a/packages/analytics-js/src/constants/logMessages.ts b/packages/analytics-js/src/constants/logMessages.ts index 04355f619..5509a4ccd 100644 --- a/packages/analytics-js/src/constants/logMessages.ts +++ b/packages/analytics-js/src/constants/logMessages.ts @@ -36,9 +36,6 @@ const UNSUPPORTED_CONSENT_MANAGER_ERROR = ( const NON_ERROR_WARNING = (context: string, errStr: Nullable): string => `${context}${LOG_CONTEXT_SEPARATOR}Ignoring a non-error: ${errStr}.`; -const FAILED_ATTACH_LISTENERS_ERROR = (context: string): string => - `${context}${LOG_CONTEXT_SEPARATOR}Failed to attach global error listeners.`; - const BREADCRUMB_ERROR = (context: string): string => `${context}${LOG_CONTEXT_SEPARATOR}Failed to log breadcrumb.`; @@ -325,5 +322,4 @@ export { PAGE_UNLOAD_ON_BEACON_DISABLED_WARNING, BREADCRUMB_ERROR, NON_ERROR_WARNING, - FAILED_ATTACH_LISTENERS_ERROR, }; diff --git a/packages/analytics-js/src/services/ErrorHandler/ErrorHandler.ts b/packages/analytics-js/src/services/ErrorHandler/ErrorHandler.ts index 92880f1e0..e7ad84785 100644 --- a/packages/analytics-js/src/services/ErrorHandler/ErrorHandler.ts +++ b/packages/analytics-js/src/services/ErrorHandler/ErrorHandler.ts @@ -13,11 +13,7 @@ import { getStacktrace, MANUAL_ERROR_IDENTIFIER, } from '@rudderstack/analytics-js-common/utilities/errors'; -import { - BREADCRUMB_ERROR, - FAILED_ATTACH_LISTENERS_ERROR, - HANDLE_ERROR_FAILURE, -} from '../../constants/logMessages'; +import { BREADCRUMB_ERROR, HANDLE_ERROR_FAILURE } from '../../constants/logMessages'; import { state } from '../../state'; import { defaultLogger } from '../Logger'; import { @@ -46,20 +42,16 @@ class ErrorHandler implements IErrorHandler { } attachErrorListeners() { - if ('addEventListener' in (globalThis as typeof window)) { - (globalThis as typeof window).addEventListener('error', (event: ErrorEvent | Event) => { - this.onError(event, ERROR_HANDLER, undefined, ErrorType.UNHANDLEDEXCEPTION); - }); + (globalThis as typeof window).addEventListener('error', (event: ErrorEvent | Event) => { + this.onError(event, ERROR_HANDLER, undefined, ErrorType.UNHANDLEDEXCEPTION); + }); - (globalThis as typeof window).addEventListener( - 'unhandledrejection', - (event: PromiseRejectionEvent) => { - this.onError(event, ERROR_HANDLER, undefined, ErrorType.UNHANDLEDREJECTION); - }, - ); - } else { - this.logger?.error(FAILED_ATTACH_LISTENERS_ERROR(ERROR_HANDLER)); - } + (globalThis as typeof window).addEventListener( + 'unhandledrejection', + (event: PromiseRejectionEvent) => { + this.onError(event, ERROR_HANDLER, undefined, ErrorType.UNHANDLEDREJECTION); + }, + ); } onError( From 702aeb6522a8783009822fd06a4328c3a1e8bb28 Mon Sep 17 00:00:00 2001 From: Sai Kumar Battinoju Date: Thu, 2 Jan 2025 17:13:35 +0530 Subject: [PATCH 10/53] test: add more test cases for coverage --- .../services/ErrorHandler/utils.test.ts | 566 ++++++++++-------- 1 file changed, 302 insertions(+), 264 deletions(-) diff --git a/packages/analytics-js/__tests__/services/ErrorHandler/utils.test.ts b/packages/analytics-js/__tests__/services/ErrorHandler/utils.test.ts index 20d16504a..e3ad40f4a 100644 --- a/packages/analytics-js/__tests__/services/ErrorHandler/utils.test.ts +++ b/packages/analytics-js/__tests__/services/ErrorHandler/utils.test.ts @@ -1,13 +1,14 @@ +/* eslint-disable compat/compat */ /* eslint-disable max-classes-per-file */ import { signal } from '@preact/signals-core'; import type { ErrorEventPayload, Exception } from '@rudderstack/analytics-js-common/types/Metrics'; -import { mergeDeepRight } from '@rudderstack/analytics-js-common/utilities/object'; -import { state } from '../../../src/state'; +import { state, resetState } from '../../../src/state'; import * as errorReportingConstants from '../../../src/services/ErrorHandler/constants'; import { createNewBreadcrumb, getAppStateForMetadata, getBugsnagErrorEvent, + getErrInstance, getErrorDeliveryPayload, getReleaseStage, getURLWithoutQueryString, @@ -19,157 +20,11 @@ jest.mock('@rudderstack/analytics-js-common/utilities/uuId', () => ({ generateUUID: jest.fn().mockReturnValue('test_uuid'), })); -const DEFAULT_STATE_DATA = { - autoTrack: { - enabled: false, - pageLifecycle: { - enabled: false, - }, - }, - capabilities: { - isAdBlocked: false, - isBeaconAvailable: false, - isCryptoAvailable: false, - isIE11: false, - isLegacyDOM: false, - isOnline: true, - isUaCHAvailable: false, - storage: { - isCookieStorageAvailable: false, - isLocalStorageAvailable: false, - isSessionStorageAvailable: false, - }, - }, - consents: { - data: {}, - enabled: false, - initialized: false, - postConsent: {}, - preConsent: { - enabled: false, - }, - resolutionStrategy: 'and', - }, - context: { - app: { - installType: '__MODULE_TYPE__', - name: 'RudderLabs JavaScript SDK', - namespace: 'com.rudderlabs.javascript', - version: '__PACKAGE_VERSION__', - }, - device: null, - library: { - name: 'RudderLabs JavaScript SDK', - version: '__PACKAGE_VERSION__', - }, - locale: null, - network: null, - os: { - name: '', - version: '', - }, - screen: { - density: 0, - height: 0, - innerHeight: 0, - innerWidth: 0, - width: 0, - }, - userAgent: '', - }, - dataPlaneEvents: { - deliveryEnabled: true, - }, - lifecycle: { - initialized: false, - loaded: false, - logLevel: 'ERROR', - readyCallbacks: [], - }, - loadOptions: { - beaconQueueOptions: {}, - bufferDataPlaneEventsUntilReady: false, - configUrl: 'https://api.rudderstack.com', - dataPlaneEventsBufferTimeout: 1000, - destinationsQueueOptions: {}, - integrations: { - All: true, - }, - loadIntegration: true, - lockIntegrationsVersion: false, - lockPluginsVersion: false, - logLevel: 'ERROR', - plugins: [], - polyfillIfRequired: true, - queueOptions: {}, - sameSiteCookie: 'Lax', - sendAdblockPageOptions: {}, - sessions: { - autoTrack: true, - timeout: 1800000, - }, - storage: { - cookie: {}, - encryption: { - version: 'v3', - }, - migrate: true, - }, - uaChTrackLevel: 'none', - useBeacon: false, - useGlobalIntegrationsConfigInEvents: false, - useServerSideCookies: false, - }, - metrics: { - dropped: 0, - queued: 0, - retries: 0, - sent: 0, - triggered: 0, - }, - nativeDestinations: { - activeDestinations: [], - clientDestinationsReady: false, - configuredDestinations: [], - failedDestinations: [], - initializedDestinations: [], - integrationsConfig: {}, - loadIntegration: true, - loadOnlyIntegrations: {}, - }, - plugins: { - activePlugins: [], - failedPlugins: [], - loadedPlugins: [], - pluginsToLoadFromConfig: [], - ready: false, - totalPluginsToLoad: 0, - }, - reporting: { - breadcrumbs: [], - isErrorReportingEnabled: false, - isErrorReportingPluginLoaded: false, - isMetricsReportingEnabled: false, - }, - serverCookies: { - isEnabledServerSideCookies: false, - }, - session: { - initialReferrer: '', - initialReferringDomain: '', - }, - source: { - id: 'dummy-source-id', - workspaceId: 'dummy-workspace-id', - }, - storage: { - entries: {}, - migrate: false, - trulyAnonymousTracking: false, - }, -}; - describe('Error Reporting utilities', () => { + beforeEach(() => { + resetState(); + }); + describe('createNewBreadcrumb', () => { it('should create and return a breadcrumb', () => { const msg = 'sample message'; @@ -182,13 +37,6 @@ describe('Error Reporting utilities', () => { name: msg, }); }); - - it('should create and return a breadcrumb with empty meta data if not provided', () => { - const msg = 'sample message'; - const breadcrumb = createNewBreadcrumb(msg); - - expect(breadcrumb.metaData).toStrictEqual({}); - }); }); describe('getURLWithoutQueryString', () => { @@ -198,6 +46,7 @@ describe('Error Reporting utilities', () => { expect(urlWithoutSearchParam).toEqual('https://www.test-host.com/'); }); }); + describe('getReleaseStage', () => { let windowSpy: any; let locationSpy: any; @@ -252,18 +101,18 @@ describe('Error Reporting utilities', () => { ]; it.each(testCaseData)( - 'if script src is "%s" then it should return the value as "%s" ', - (scriptSrc: string, expectedValue: boolean) => { + 'if file path is "%s" then it should return the value as "%s" ', + (filePath: string, expectedValue: boolean) => { // Bugsnag error event object structure const event = { stacktrace: [ { - file: scriptSrc, + file: filePath, }, ], }; - expect(isSDKError(event)).toBe(expectedValue); + expect(isSDKError(event as unknown as Exception)).toBe(expectedValue); }, ); }); @@ -415,48 +264,86 @@ describe('Error Reporting utilities', () => { }); describe('getBugsnagErrorEvent', () => { - it.skip('should return enhanced error event payload', () => { + it('should return the error event payload', () => { state.session.sessionInfo.value = { id: 123 }; + // @ts-expect-error setting the value for testing + state.context.app.value.installType = 'cdn'; state.autoTrack.pageLifecycle.visitId.value = 'test-visit-id'; - - const newError = new Error(); - const normalizedError = Object.create(newError, { - message: { value: 'ReferenceError: testUndefinedFn is not defined' }, - stack: { - value: `ReferenceError: testUndefinedFn is not defined at Analytics.page (http://localhost:3001/cdn/modern/iife/rsa.js:1610:3) at RudderAnalytics.page (http://localhost:3001/cdn/modern/iife/rsa.js:1666:84)`, + // @ts-expect-error setting the value for testing + state.context.library.value.snippetVersion = 'sample_snippet_version'; + state.context.locale.value = 'en-US'; + state.context.userAgent.value = + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3'; + state.source.value = { + id: 'dummy-source-id', + name: 'dummy-source-name', + workspaceId: 'dummy-workspace-id', + }; + state.reporting.breadcrumbs.value = [ + { + metaData: {}, + name: 'sample breadcrumb message', + timestamp: new Date(), + type: 'manual', }, - }); + { + metaData: {}, + name: 'sample breadcrumb message 2', + timestamp: new Date(), + type: 'manual', + }, + ]; + + state.context.screen.value = { + density: 1, + width: 2, + height: 3, + innerWidth: 4, + innerHeight: 5, + }; + const errorState = { severity: 'error', unhandled: false, severityReason: { type: 'handledException' }, }; - const errorPayload = ErrorFormat.create(normalizedError, 'notify()') as ErrorFormat; - (window as any).RudderSnippetVersion = 'sample_snippet_version'; - const enhancedError = getBugsnagErrorEvent(errorPayload, errorState, state); + const exception = { + errorClass: 'Error', + message: 'dummy message', + type: 'browserjs', + stacktrace: [ + { + file: 'https://example.com/sample.js', + method: 'Object.', + lineNumber: 1, + columnNumber: 1, + }, + ], + }; + + const bsErrorEvent = getBugsnagErrorEvent(exception, errorState, state); + const expectedOutcome = { + payloadVersion: '5', notifier: { - name: 'RudderStack JavaScript SDK Error Notifier', - version: 'dev-snapshot', - url: 'https://github.com/rudderlabs/rudder-sdk-js', + name: 'RudderStack JavaScript SDK', + version: '__PACKAGE_VERSION__', + url: '__REPOSITORY_URL__', }, events: [ { - payloadVersion: '5', exceptions: [ { errorClass: 'Error', - message: 'ReferenceError: testUndefinedFn is not defined', + message: 'dummy message', type: 'browserjs', stacktrace: [ { - file: 'ReferenceError: testUndefinedFn is not defined at Analytics.page http://localhost:3001/cdn/modern/iife/rsa.js:1610:3 at RudderAnalytics.page http://localhost:3001/cdn/modern/iife/rsa.js', - lineNumber: 1666, - columnNumber: 84, - code: undefined, - inProject: undefined, - method: undefined, + file: 'https://example.com/sample.js', + method: 'Object.', + lineNumber: 1, + columnNumber: 1, }, ], }, @@ -467,113 +354,231 @@ describe('Error Reporting utilities', () => { type: 'handledException', }, app: { - version: 'dev-snapshot', + version: '__PACKAGE_VERSION__', releaseStage: 'development', + type: 'cdn', }, device: { - userAgent: '', + locale: 'en-US', + userAgent: + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3', time: expect.any(Date), }, request: { url: 'https://www.test-host.com/', clientIp: '[NOT COLLECTED]', }, - breadcrumbs: [], - context: 'ReferenceError: testUndefinedFn is not defined', + breadcrumbs: [ + { + metaData: {}, + name: 'sample breadcrumb message', + timestamp: expect.any(Date), + type: 'manual', + }, + { + metaData: {}, + name: 'sample breadcrumb message 2', + timestamp: expect.any(Date), + type: 'manual', + }, + ], metaData: { - sdk: { - name: 'JS', - installType: 'cdn', + app: { + snippetVersion: 'sample_snippet_version', + }, + device: { + density: 1, + width: 2, + height: 3, + innerWidth: 4, + innerHeight: 5, + }, + autoTrack: { + enabled: false, + pageLifecycle: { + enabled: false, + visitId: 'test-visit-id', + }, + }, + capabilities: { + isAdBlocked: false, + isBeaconAvailable: false, + isCryptoAvailable: false, + isIE11: false, + isLegacyDOM: false, + isOnline: true, + isUaCHAvailable: false, + storage: { + isCookieStorageAvailable: false, + isLocalStorageAvailable: false, + isSessionStorageAvailable: false, + }, + }, + consents: { + data: {}, + enabled: false, + initialized: false, + postConsent: {}, + preConsent: { + enabled: false, + }, + resolutionStrategy: 'and', + }, + context: { + app: { + installType: 'cdn', + name: 'RudderLabs JavaScript SDK', + namespace: 'com.rudderlabs.javascript', + version: '__PACKAGE_VERSION__', + }, + device: null, + library: { + name: 'RudderLabs JavaScript SDK', + snippetVersion: 'sample_snippet_version', + version: '__PACKAGE_VERSION__', + }, + locale: 'en-US', + network: null, + os: { + name: '', + version: '', + }, + screen: { + density: 1, + height: 3, + innerHeight: 5, + innerWidth: 4, + width: 2, + }, + userAgent: + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3', + }, + dataPlaneEvents: { + deliveryEnabled: true, }, - state: mergeDeepRight(DEFAULT_STATE_DATA, { - autoTrack: { - pageLifecycle: { - visitId: 'test-visit-id', + lifecycle: { + initialized: false, + integrationsCDNPath: 'https://cdn.rudderlabs.com/v3/modern/js-integrations', + pluginsCDNPath: 'https://cdn.rudderlabs.com/v3/modern/plugins', + loaded: false, + logLevel: 'ERROR', + readyCallbacks: [], + }, + loadOptions: { + beaconQueueOptions: {}, + bufferDataPlaneEventsUntilReady: false, + configUrl: 'https://api.rudderstack.com', + dataPlaneEventsBufferTimeout: 10000, + destinationsQueueOptions: {}, + integrations: { + All: true, + }, + loadIntegration: true, + lockIntegrationsVersion: false, + lockPluginsVersion: false, + logLevel: 'ERROR', + plugins: [], + polyfillIfRequired: true, + queueOptions: {}, + sameSiteCookie: 'Lax', + sendAdblockPageOptions: {}, + sessions: { + autoTrack: true, + timeout: 1800000, + }, + storage: { + cookie: {}, + encryption: { + version: 'v3', }, + migrate: true, }, - session: { - sessionInfo: { id: 123 }, + uaChTrackLevel: 'none', + useBeacon: false, + useGlobalIntegrationsConfigInEvents: false, + useServerSideCookies: false, + }, + metrics: { + dropped: 0, + queued: 0, + retries: 0, + sent: 0, + triggered: 0, + }, + nativeDestinations: { + activeDestinations: [], + clientDestinationsReady: false, + configuredDestinations: [], + failedDestinations: [], + initializedDestinations: [], + integrationsConfig: {}, + loadIntegration: true, + loadOnlyIntegrations: {}, + }, + plugins: { + activePlugins: [], + failedPlugins: [], + loadedPlugins: [], + pluginsToLoadFromConfig: [], + ready: false, + totalPluginsToLoad: 0, + }, + reporting: { + breadcrumbs: [ + { + metaData: {}, + name: 'sample breadcrumb message', + timestamp: expect.any(String), + type: 'manual', + }, + { + metaData: {}, + name: 'sample breadcrumb message 2', + timestamp: expect.any(String), + type: 'manual', + }, + ], + isErrorReportingEnabled: false, + isMetricsReportingEnabled: false, + }, + serverCookies: { + isEnabledServerSideCookies: false, + }, + session: { + initialReferrer: '', + initialReferringDomain: '', + sessionInfo: { + id: 123, }, - }), + }, source: { - snippetVersion: 'sample_snippet_version', + id: 'dummy-source-id', + name: 'dummy-source-name', + workspaceId: 'dummy-workspace-id', + }, + storage: { + entries: {}, + migrate: false, + trulyAnonymousTracking: false, }, }, user: { id: 'dummy-source-id..123..test-visit-id', + name: 'dummy-source-name', }, }, ], }; - expect(enhancedError).toEqual(expectedOutcome); + + expect(bsErrorEvent).toEqual(expectedOutcome); }); }); describe('getErrorDeliveryPayload', () => { it('should return error delivery payload', () => { - const enhancedErrorPayload = { - notifier: { - name: 'Rudderstack JavaScript SDK Error Notifier', - version: 'sample_version', - url: 'https://github.com/rudderlabs/rudder-sdk-js', - }, - events: [ - { - payloadVersion: '5', - exceptions: [ - { - errorClass: 'Error', - errorMessage: 'ReferenceError: testUndefinedFn is not defined', - type: 'browserjs', - stacktrace: [ - { - file: 'ReferenceError: testUndefinedFn is not defined at Analytics.page http://localhost:3001/cdn/modern/iife/rsa.js:1610:3 at RudderAnalytics.page http://localhost:3001/cdn/modern/iife/rsa.js', - lineNumber: 1666, - columnNumber: 84, - code: undefined, - inProject: undefined, - method: undefined, - }, - ], - }, - ], - severity: 'error', - unhandled: false, - severityReason: { - type: 'handledException', - }, - app: { - version: 'dev-snapshot', - releaseStage: 'development', - }, - device: { - userAgent: '', - time: expect.any(Date), - }, - request: { - url: 'https://www.test-host.com/', - clientIp: '[NOT COLLECTED]', - }, - breadcrumbs: [], - context: 'ReferenceError: testUndefinedFn is not defined', - metaData: { - sdk: { - name: 'JS', - installType: 'cdn', - }, - state: DEFAULT_STATE_DATA, - source: { - snippetVersion: 'sample_snippet_version', - }, - }, - user: { - id: 'sample-write-key', - }, - }, - ], - } as unknown as ErrorEventPayload; + const errorEventPayload = {} as unknown as ErrorEventPayload; - const deliveryPayload = getErrorDeliveryPayload(enhancedErrorPayload, state); + const deliveryPayload = getErrorDeliveryPayload(errorEventPayload, state); expect(deliveryPayload).toEqual( JSON.stringify({ version: '1', @@ -583,7 +588,7 @@ describe('Error Reporting utilities', () => { sdk_version: '__PACKAGE_VERSION__', install_type: '__MODULE_TYPE__', }, - errors: enhancedErrorPayload, + errors: errorEventPayload, }), ); }); @@ -602,4 +607,37 @@ describe('Error Reporting utilities', () => { expect(result).toBeFalsy(); }); }); + + describe('getErrInstance', () => { + it('should return the same error instance for handled errors', () => { + const error = new Error('dummy error'); + const errorType = 'handledException'; + const result = getErrInstance(error, errorType); + expect(result).toEqual(error); + }); + + it('should return the internal error instance for unhandled errors', () => { + const errorEvent = new ErrorEvent('error', { error: new Error('dummy error') }); + const errorType = 'unhandledException'; + const result = getErrInstance(errorEvent, errorType); + expect(result).toEqual(errorEvent.error); + }); + + it('should return the same error event instance if the internal error is not present', () => { + const errorEvent = new ErrorEvent('error'); + const errorType = 'unhandledException'; + const result = getErrInstance(errorEvent, errorType); + expect(result).toEqual(errorEvent); + }); + + it('should return the internal reason instance for unhandled promise rejections', () => { + const errorEvent = new PromiseRejectionEvent('error', { + reason: new Error('dummy error'), + promise: Promise.resolve(), + }); + const errorType = 'unhandledPromiseRejection'; + const result = getErrInstance(errorEvent, errorType); + expect(result).toEqual(errorEvent.reason); + }); + }); }); From d5d6dff8fa725ec29ad7707cc22e604db957922d Mon Sep 17 00:00:00 2001 From: Sai Kumar Battinoju Date: Thu, 2 Jan 2025 17:25:03 +0530 Subject: [PATCH 11/53] refactor: remove the need to create new http client --- .../CapabilitiesManager.test.ts | 12 ++++- .../detection/adBlockers.test.ts | 47 +++++++------------ .../CapabilitiesManager.ts | 11 +++-- .../detection/adBlockers.ts | 10 +--- .../components/capabilitiesManager/types.ts | 4 +- .../src/components/core/Analytics.ts | 6 ++- 6 files changed, 45 insertions(+), 45 deletions(-) diff --git a/packages/analytics-js/__tests__/components/capabilitiesManager/CapabilitiesManager.test.ts b/packages/analytics-js/__tests__/components/capabilitiesManager/CapabilitiesManager.test.ts index acf6be360..6aa091f02 100644 --- a/packages/analytics-js/__tests__/components/capabilitiesManager/CapabilitiesManager.test.ts +++ b/packages/analytics-js/__tests__/components/capabilitiesManager/CapabilitiesManager.test.ts @@ -1,4 +1,5 @@ import type { ILogger } from '@rudderstack/analytics-js-common/types/Logger'; +import { defaultHttpClient } from '../../../src/services/HttpClient'; import { isLegacyJSEngine } from '../../../src/components/capabilitiesManager/detection'; import type { ICapabilitiesManager } from '../../../src/components/capabilitiesManager/types'; import { defaultErrorHandler } from '../../../src/services/ErrorHandler'; @@ -39,7 +40,11 @@ describe('CapabilitiesManager', () => { describe('prepareBrowserCapabilities', () => { beforeEach(() => { - capabilitiesManager = new CapabilitiesManager(defaultErrorHandler, mockLogger); + capabilitiesManager = new CapabilitiesManager( + defaultHttpClient, + defaultErrorHandler, + mockLogger, + ); }); afterEach(() => { @@ -98,7 +103,10 @@ describe('CapabilitiesManager', () => { state.lifecycle.writeKey.value = 'sample-write-key'; state.loadOptions.value.polyfillIfRequired = true; - const tempCapabilitiesManager = new CapabilitiesManager(defaultErrorHandler); + const tempCapabilitiesManager = new CapabilitiesManager( + defaultHttpClient, + defaultErrorHandler, + ); isLegacyJSEngine.mockReturnValue(true); tempCapabilitiesManager.externalSrcLoader = { diff --git a/packages/analytics-js/__tests__/components/capabilitiesManager/detection/adBlockers.test.ts b/packages/analytics-js/__tests__/components/capabilitiesManager/detection/adBlockers.test.ts index 988c835aa..f314f5e1a 100644 --- a/packages/analytics-js/__tests__/components/capabilitiesManager/detection/adBlockers.test.ts +++ b/packages/analytics-js/__tests__/components/capabilitiesManager/detection/adBlockers.test.ts @@ -1,44 +1,33 @@ import { effect } from '@preact/signals-core'; -import { defaultErrorHandler } from '@rudderstack/analytics-js-common/__mocks__/ErrorHandler'; import { detectAdBlockers } from '../../../../src/components/capabilitiesManager/detection/adBlockers'; import { state, resetState } from '../../../../src/state'; - -let errObj; -let xhrObj; - -jest.mock('../../../../src/services/HttpClient/HttpClient', () => { - const originalModule = jest.requireActual('../../../../src/services/HttpClient/HttpClient'); - - return { - __esModule: true, - ...originalModule, - HttpClient: jest.fn().mockImplementation(() => ({ - init: jest.fn(), - setAuthHeader: jest.fn(), - getAsyncData: jest.fn().mockImplementation(({ url, callback }) => { - callback(undefined, { - error: errObj, - xhr: xhrObj, - }); - }), - })), - }; -}); +import { defaultHttpClient } from '../../../../src/services/HttpClient'; describe('detectAdBlockers', () => { beforeEach(() => { resetState(); }); + let errObj: Error | undefined; + let xhrObj: XMLHttpRequest; + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + defaultHttpClient.getAsyncData = jest.fn().mockImplementation(({ callback, ...rest }) => { + callback(undefined, { + error: errObj, + xhr: xhrObj, + }); + }); + it('should detect adBlockers if the request is blocked', done => { state.lifecycle.sourceConfigUrl.value = 'https://example.com/some/path/'; errObj = new Error('Request blocked'); xhrObj = { responseURL: 'https://example.com/some/path/?view=ad', - }; + } as unknown as XMLHttpRequest; - detectAdBlockers(defaultErrorHandler); + detectAdBlockers(defaultHttpClient); effect(() => { expect(state.capabilities.isAdBlocked.value).toBe(true); @@ -52,9 +41,9 @@ describe('detectAdBlockers', () => { errObj = undefined; xhrObj = { responseURL: 'data:text/css;charset=UTF-8;base64,dGVtcA==', - }; + } as unknown as XMLHttpRequest; - detectAdBlockers(defaultErrorHandler); + detectAdBlockers(defaultHttpClient); effect(() => { expect(state.capabilities.isAdBlocked.value).toBe(true); @@ -68,9 +57,9 @@ describe('detectAdBlockers', () => { errObj = undefined; xhrObj = { responseURL: 'https://example.com/some/path/?view=ad', - }; + } as unknown as XMLHttpRequest; - detectAdBlockers(defaultErrorHandler); + detectAdBlockers(defaultHttpClient); effect(() => { expect(state.capabilities.isAdBlocked.value).toBe(false); diff --git a/packages/analytics-js/src/components/capabilitiesManager/CapabilitiesManager.ts b/packages/analytics-js/src/components/capabilitiesManager/CapabilitiesManager.ts index dfadd1072..e2d1dd664 100644 --- a/packages/analytics-js/src/components/capabilitiesManager/CapabilitiesManager.ts +++ b/packages/analytics-js/src/components/capabilitiesManager/CapabilitiesManager.ts @@ -12,6 +12,7 @@ import { CAPABILITIES_MANAGER } from '@rudderstack/analytics-js-common/constants import { getTimezone } from '@rudderstack/analytics-js-common/utilities/timezone'; import { isValidURL } from '@rudderstack/analytics-js-common/utilities/url'; import { isDefinedAndNotNull } from '@rudderstack/analytics-js-common/utilities/checks'; +import type { IHttpClient } from '@rudderstack/analytics-js-common/types/HttpClient'; import { INVALID_POLYFILL_URL_WARNING, POLYFILL_SCRIPT_LOAD_ERROR, @@ -36,13 +37,15 @@ import { debounce } from '../utilities/globals'; // TODO: replace direct calls to detection methods with state values when possible class CapabilitiesManager implements ICapabilitiesManager { - logger?: ILogger; + httpClient: IHttpClient; errorHandler: IErrorHandler; + logger?: ILogger; externalSrcLoader: IExternalSrcLoader; - constructor(errorHandler: IErrorHandler, logger?: ILogger) { - this.logger = logger; + constructor(httpClient: IHttpClient, errorHandler: IErrorHandler, logger?: ILogger) { + this.httpClient = httpClient; this.errorHandler = errorHandler; + this.logger = logger; this.externalSrcLoader = new ExternalSrcLoader(this.errorHandler, this.logger); this.onError = this.onError.bind(this); this.onReady = this.onReady.bind(this); @@ -106,7 +109,7 @@ class CapabilitiesManager implements ICapabilitiesManager { state.loadOptions.value.sendAdblockPage === true && state.lifecycle.sourceConfigUrl.value !== undefined ) { - detectAdBlockers(this.errorHandler, this.logger); + detectAdBlockers(this.httpClient); } }); } diff --git a/packages/analytics-js/src/components/capabilitiesManager/detection/adBlockers.ts b/packages/analytics-js/src/components/capabilitiesManager/detection/adBlockers.ts index 82140c365..e9f2a9b9f 100644 --- a/packages/analytics-js/src/components/capabilitiesManager/detection/adBlockers.ts +++ b/packages/analytics-js/src/components/capabilitiesManager/detection/adBlockers.ts @@ -1,9 +1,7 @@ -import type { IErrorHandler } from '@rudderstack/analytics-js-common/types/ErrorHandler'; -import type { ILogger } from '@rudderstack/analytics-js-common/types/Logger'; -import { HttpClient } from '../../../services/HttpClient/HttpClient'; +import type { IHttpClient } from '@rudderstack/analytics-js-common/types/HttpClient'; import { state } from '../../../state'; -const detectAdBlockers = (errorHandler: IErrorHandler, logger?: ILogger): void => { +const detectAdBlockers = (httpClient: IHttpClient): void => { // Apparently, '?view=ad' is a query param that is blocked by majority of adblockers // Use source config URL here as it is very unlikely to be blocked by adblockers @@ -13,10 +11,6 @@ const detectAdBlockers = (errorHandler: IErrorHandler, logger?: ILogger): void = const baseUrl = new URL(state.lifecycle.sourceConfigUrl.value as string); const url = `${baseUrl.origin}${baseUrl.pathname}?view=ad`; - const httpClient = new HttpClient(logger); - httpClient.init(errorHandler); - httpClient.setAuthHeader(state.lifecycle.writeKey.value as string); - httpClient.getAsyncData({ url, options: { diff --git a/packages/analytics-js/src/components/capabilitiesManager/types.ts b/packages/analytics-js/src/components/capabilitiesManager/types.ts index 8e2eacf5c..ce64d0a04 100644 --- a/packages/analytics-js/src/components/capabilitiesManager/types.ts +++ b/packages/analytics-js/src/components/capabilitiesManager/types.ts @@ -1,10 +1,12 @@ import type { ILogger } from '@rudderstack/analytics-js-common/types/Logger'; import type { IErrorHandler } from '@rudderstack/analytics-js-common/types/ErrorHandler'; import type { IExternalSrcLoader } from '@rudderstack/analytics-js-common/services/ExternalSrcLoader/types'; +import type { IHttpClient } from '@rudderstack/analytics-js-common/types/HttpClient'; export interface ICapabilitiesManager { - logger?: ILogger; + httpClient: IHttpClient; errorHandler: IErrorHandler; + logger?: ILogger; externalSrcLoader: IExternalSrcLoader; init(): void; detectBrowserCapabilities(): void; diff --git a/packages/analytics-js/src/components/core/Analytics.ts b/packages/analytics-js/src/components/core/Analytics.ts index 991a76230..b695518db 100644 --- a/packages/analytics-js/src/components/core/Analytics.ts +++ b/packages/analytics-js/src/components/core/Analytics.ts @@ -97,9 +97,13 @@ class Analytics implements IAnalytics { this.errorHandler = defaultErrorHandler; this.logger = defaultLogger; this.externalSrcLoader = new ExternalSrcLoader(this.errorHandler, this.logger); - this.capabilitiesManager = new CapabilitiesManager(this.errorHandler, this.logger); this.httpClient = defaultHttpClient; this.httpClient.init(this.errorHandler); + this.capabilitiesManager = new CapabilitiesManager( + this.httpClient, + this.errorHandler, + this.logger, + ); } /** From d1b9ac4e7f33e0e77d4bdd8f567e113c81855429 Mon Sep 17 00:00:00 2001 From: Sai Kumar Battinoju Date: Thu, 2 Jan 2025 19:41:01 +0530 Subject: [PATCH 12/53] test: refactor browser test suite --- .../analytics-js/__tests__/browser.test.ts | 176 +++++++++--------- .../analytics-js/__tests__/nativeSdkLoader.js | 5 +- 2 files changed, 93 insertions(+), 88 deletions(-) diff --git a/packages/analytics-js/__tests__/browser.test.ts b/packages/analytics-js/__tests__/browser.test.ts index cbbfa6c60..c86353cbb 100644 --- a/packages/analytics-js/__tests__/browser.test.ts +++ b/packages/analytics-js/__tests__/browser.test.ts @@ -1,13 +1,8 @@ +/* eslint-disable import/no-dynamic-require */ /* eslint-disable global-require */ import { loadingSnippet } from './nativeSdkLoader'; -const pathToSdk = '../dist/cdn/legacy/iife/rsa.min.js'; - -function wait(time: number) { - return new Promise(resolve => { - setTimeout(resolve, time); - }); -} +const pathToSdk = '../dist/cdn/legacy/iife/rsa.js'; describe('Test suite for the SDK', () => { const xhrMock: any = { @@ -41,15 +36,6 @@ describe('Test suite for the SDK', () => { const originalXMLHttpRequest = window.XMLHttpRequest; - beforeEach(async () => { - window.XMLHttpRequest = jest.fn(() => xhrMock); - - loadingSnippet(); - - require(pathToSdk); - await wait(500); - }); - afterEach(() => { jest.resetModules(); jest.clearAllMocks(); @@ -60,93 +46,113 @@ describe('Test suite for the SDK', () => { window.XMLHttpRequest = originalXMLHttpRequest; }); - it('should process the buffered API calls when SDK script is loaded', async () => { - // Only done for this case to test the - // API calls queuing functionality - jest.resetModules(); - rudderanalytics.page(); - require(pathToSdk); - await wait(500); + describe('preload buffer', () => { + it('should process the buffered API calls when SDK script is loaded', done => { + // Mocking the xhr function + window.XMLHttpRequest = jest.fn(() => xhrMock) as unknown as typeof XMLHttpRequest; - expect(window.rudderanalytics.push).not.toBe(Array.prototype.push); + loadingSnippet(); + window.rudderanalytics?.page(); + window.rudderanalytics?.ready(() => { + expect((window.rudderanalytics as any).push).not.toBe(Array.prototype.push); - // one source config endpoint call and one implicit page call - // Refer to above 'beforeEach' - expect(xhrMock.send).toHaveBeenCalledTimes(2); - }); + // one source config endpoint call and one implicit page call + expect(xhrMock.send).toHaveBeenCalledTimes(2); - it('should make network requests when event APIs are invoked', () => { - rudderanalytics.page(); - rudderanalytics.track('test-event'); - rudderanalytics.identify('jest-user'); - rudderanalytics.group('jest-group'); - rudderanalytics.alias('new-jest-user', 'jest-user'); + done(); + }); - // one source config endpoint call and above API requests - expect(xhrMock.send).toHaveBeenCalledTimes(6); + require(pathToSdk); + }); }); - describe('getAnonymousId', () => { - it('should return a new UUID when no prior persisted dat is present', () => { - const anonId = rudderanalytics.getAnonymousId(); + describe('api', () => { + beforeEach(done => { + // Mocking the xhr function + window.XMLHttpRequest = jest.fn(() => xhrMock) as unknown as typeof XMLHttpRequest; - const uuidRegEx = /^[a-z0-9]{8}-[a-z0-9]{4}-4[a-z0-9]{3}-[a-z0-9]{4}-[a-z0-9]{12}$/; - expect(anonId).toMatch(uuidRegEx); - }); - - it('should persist the anonymous ID generated by the SDK', () => { - const anonIdRes1 = rudderanalytics.getAnonymousId(); + loadingSnippet(); - // SDK remembers the previously generated anonymous ID and returns the same value - const anonIdRes2 = rudderanalytics.getAnonymousId(); + window.rudderanalytics?.ready(() => { + done(); + }); - expect(anonIdRes1).toEqual(anonIdRes2); + require(pathToSdk); }); - }); - - describe('reset', () => { - it('should clear al the persisted data expect for anonymous ID when the flag is not set', () => { - // Make identify and group API calls to let the SDK persist - // user (ID and traits) and group data (ID and traits) - rudderanalytics.identify(userId, userTraits); - rudderanalytics.group(groupUserId, groupTraits); - const anonId = 'jest-anon-ID'; - rudderanalytics.setAnonymousId(anonId); + it('should make network requests when event APIs are invoked', () => { + window.rudderanalytics?.page(); + window.rudderanalytics?.track('test-event'); + window.rudderanalytics?.identify('jest-user'); + window.rudderanalytics?.group('jest-group'); + window.rudderanalytics?.alias('new-jest-user', 'jest-user'); - // SDK clears all the persisted data except for anonymous ID - rudderanalytics.reset(); - - // SDK remembers the previously generated anonymous ID and returns the same value - const anonIdRes = rudderanalytics.getAnonymousId(); - - expect(anonId).toEqual(anonIdRes); - expect(rudderanalytics.getUserId()).toEqual(''); - expect(rudderanalytics.getUserTraits()).toEqual({}); - expect(rudderanalytics.getGroupId()).toEqual(''); - expect(rudderanalytics.getGroupTraits()).toEqual({}); + // one source config endpoint call and individual event requests + expect(xhrMock.send).toHaveBeenCalledTimes(6); }); - it('should clear all the persisted data include anonymous ID when the flag is set', () => { - // Make identify and group API calls to let the SDK persist - // user (ID and traits) and group data (ID and traits) - rudderanalytics.identify(userId, userTraits); - rudderanalytics.group(groupUserId, groupTraits); + describe('getAnonymousId', () => { + it('should return a new UUID when no prior persisted data is present', () => { + const anonId = window.rudderanalytics?.getAnonymousId(); - const anonId = 'jest-anon-ID'; - rudderanalytics.setAnonymousId(anonId); + const uuidRegEx = /^[\da-f]{8}-[\da-f]{4}-4[\da-f]{3}-[\da-f]{4}-[\da-f]{12}$/i; + expect(anonId).toMatch(uuidRegEx); + }); - // SDK clears all the persisted data - rudderanalytics.reset(true); + it('should persist the anonymous ID generated by the SDK', () => { + const anonIdRes1 = window.rudderanalytics?.getAnonymousId(); - // SDK remembers the previously generated anonymous ID and returns the same value - const anonIdRes = rudderanalytics.getAnonymousId(); + // SDK remembers the previously generated anonymous ID and returns the same value + const anonIdRes2 = window.rudderanalytics?.getAnonymousId(); + + expect(anonIdRes1).toEqual(anonIdRes2); + }); + }); - expect(anonId).not.toEqual(anonIdRes); - expect(rudderanalytics.getUserId()).toEqual(''); - expect(rudderanalytics.getUserTraits()).toEqual({}); - expect(rudderanalytics.getGroupId()).toEqual(''); - expect(rudderanalytics.getGroupTraits()).toEqual({}); + describe('reset', () => { + it('should clear all the persisted data except for anonymous ID when the flag is not set', () => { + // Make identify and group API calls to let the SDK persist + // user (ID and traits) and group data (ID and traits) + window.rudderanalytics?.identify(userId, userTraits); + window.rudderanalytics?.group(groupUserId, groupTraits); + + const anonId = 'jest-anon-ID'; + window.rudderanalytics?.setAnonymousId(anonId); + + // SDK clears all the persisted data except for anonymous ID + window.rudderanalytics?.reset(); + + // SDK remembers the previously generated anonymous ID and returns the same value + const anonIdRes = window.rudderanalytics?.getAnonymousId(); + + expect(anonId).toEqual(anonIdRes); + expect(window.rudderanalytics?.getUserId()).toEqual(''); + expect(window.rudderanalytics?.getUserTraits()).toEqual({}); + expect(window.rudderanalytics?.getGroupId()).toEqual(''); + expect(window.rudderanalytics?.getGroupTraits()).toEqual({}); + }); + + it('should clear all the persisted data include anonymous ID when the flag is set', () => { + // Make identify and group API calls to let the SDK persist + // user (ID and traits) and group data (ID and traits) + window.rudderanalytics?.identify(userId, userTraits); + window.rudderanalytics?.group(groupUserId, groupTraits); + + const anonId = 'jest-anon-ID'; + window.rudderanalytics?.setAnonymousId(anonId); + + // SDK clears all the persisted data + window.rudderanalytics?.reset(true); + + // SDK remembers the previously generated anonymous ID and returns the same value + const anonIdRes = window.rudderanalytics?.getAnonymousId(); + + expect(anonId).not.toEqual(anonIdRes); + expect(window.rudderanalytics?.getUserId()).toEqual(''); + expect(window.rudderanalytics?.getUserTraits()).toEqual({}); + expect(window.rudderanalytics?.getGroupId()).toEqual(''); + expect(window.rudderanalytics?.getGroupTraits()).toEqual({}); + }); }); }); }); diff --git a/packages/analytics-js/__tests__/nativeSdkLoader.js b/packages/analytics-js/__tests__/nativeSdkLoader.js index 23292730c..faf6bdfe8 100644 --- a/packages/analytics-js/__tests__/nativeSdkLoader.js +++ b/packages/analytics-js/__tests__/nativeSdkLoader.js @@ -1,4 +1,4 @@ -function loadingSnippet() { +function loadingSnippet(loadOptions) { (function () { 'use strict'; window.RudderSnippetVersion = '3.0.10'; @@ -56,8 +56,7 @@ function loadingSnippet() { } }; window.rudderAnalyticsMount(); - var loadOptions = {}; - window.rudderanalytics.load('WRITE_KEY', 'https://some.reallookingdataplane.url'); + window.rudderanalytics.load('WRITE_KEY', 'https://some.reallookingdataplane.url', loadOptions ?? {}); } } })(); From a30f5261a79223a17cc25d84974ec7f82e89f98a Mon Sep 17 00:00:00 2001 From: Sai Kumar Battinoju Date: Thu, 2 Jan 2025 20:01:03 +0530 Subject: [PATCH 13/53] chore: adjust size limits --- packages/analytics-js-plugins/.size-limit.mjs | 12 +++--- packages/analytics-js/.size-limit.mjs | 38 +++++++++---------- packages/analytics-v1.1/.size-limit.js | 4 +- 3 files changed, 27 insertions(+), 27 deletions(-) diff --git a/packages/analytics-js-plugins/.size-limit.mjs b/packages/analytics-js-plugins/.size-limit.mjs index 0aa8752a6..67aa6a442 100644 --- a/packages/analytics-js-plugins/.size-limit.mjs +++ b/packages/analytics-js-plugins/.size-limit.mjs @@ -8,19 +8,19 @@ export default [ path: 'dist/cdn/legacy/plugins/rsa-plugins.js', limit: '0.5 KiB', }, - { - name: 'Plugins - Legacy - CDN', - path: 'dist/cdn/legacy/plugins/rsa-plugins-*.min.js', - limit: '16 KiB', - }, { name: 'Plugins Module Federation Mapping - Modern - CDN', path: 'dist/cdn/modern/plugins/rsa-plugins.js', limit: '0.5 KiB', }, + { + name: 'Plugins - Legacy - CDN', + path: 'dist/cdn/legacy/plugins/rsa-plugins-*.min.js', + limit: '14 KiB', + }, { name: 'Plugins - Modern - CDN', path: 'dist/cdn/modern/plugins/rsa-plugins-*.min.js', - limit: '7.5 KiB', + limit: '6 KiB', }, ]; diff --git a/packages/analytics-js/.size-limit.mjs b/packages/analytics-js/.size-limit.mjs index 98882d766..53047e0fe 100644 --- a/packages/analytics-js/.size-limit.mjs +++ b/packages/analytics-js/.size-limit.mjs @@ -7,42 +7,42 @@ export default [ name: 'Core - Legacy - NPM (ESM)', path: 'dist/npm/legacy/esm/index.mjs', import: '*', - limit: '49 KiB', + limit: '48 KiB', }, { name: 'Core - Legacy - NPM (CJS)', path: 'dist/npm/legacy/cjs/index.cjs', import: '*', - limit: '49.1 KiB', + limit: '48 KiB', }, { name: 'Core - Legacy - NPM (UMD)', path: 'dist/npm/legacy/umd/index.js', import: '*', - limit: '49 KiB', + limit: '48 KiB', }, { name: 'Core - Legacy - CDN', path: 'dist/cdn/legacy/iife/rsa.min.js', - limit: '49 KiB', + limit: '47.5 KiB', }, { name: 'Core - Modern - NPM (ESM)', path: 'dist/npm/modern/esm/index.mjs', import: '*', - limit: '27.5 KiB', + limit: '27 KiB', }, { name: 'Core - Modern - NPM (CJS)', path: 'dist/npm/modern/cjs/index.cjs', import: '*', - limit: '27.5 KiB', + limit: '27 KiB', }, { name: 'Core - Modern - NPM (UMD)', path: 'dist/npm/modern/umd/index.js', import: '*', - limit: '27.5 KiB', + limit: '27 KiB', }, { name: 'Core - Modern - CDN', @@ -53,72 +53,72 @@ export default [ name: 'Core (Bundled) - Legacy - NPM (ESM)', path: 'dist/npm/legacy/bundled/esm/index.mjs', import: '*', - limit: '49 KiB', + limit: '48 KiB', }, { name: 'Core (Bundled) - Legacy - NPM (CJS)', path: 'dist/npm/legacy/bundled/cjs/index.cjs', import: '*', - limit: '49.1 KiB', + limit: '48 KiB', }, { name: 'Core (Bundled) - Legacy - NPM (UMD)', path: 'dist/npm/legacy/bundled/umd/index.js', import: '*', - limit: '49 KiB', + limit: '48 KiB', }, { name: 'Core (Bundled) - Modern - NPM (ESM)', path: 'dist/npm/modern/bundled/esm/index.mjs', import: '*', - limit: '40 KiB', + limit: '39 KiB', }, { name: 'Core (Bundled) - Modern - NPM (CJS)', path: 'dist/npm/modern/bundled/cjs/index.cjs', import: '*', - limit: '40.5 KiB', + limit: '39 KiB', }, { name: 'Core (Bundled) - Modern - NPM (UMD)', path: 'dist/npm/modern/bundled/umd/index.js', import: '*', - limit: '40 KiB', + limit: '39 KiB', }, { name: 'Core (Content Script) - Legacy - NPM (ESM)', path: 'dist/npm/legacy/content-script/esm/index.mjs', import: '*', - limit: '48.5 KiB', + limit: '48 KiB', }, { name: 'Core (Content Script) - Legacy - NPM (CJS)', path: 'dist/npm/legacy/content-script/cjs/index.cjs', import: '*', - limit: '48.5 KiB', + limit: '48 KiB', }, { name: 'Core (Content Script) - Legacy - NPM (UMD)', path: 'dist/npm/legacy/content-script/umd/index.js', import: '*', - limit: '48.5 KiB', + limit: '48 KiB', }, { name: 'Core (Content Script) - Modern - NPM (ESM)', path: 'dist/npm/modern/content-script/esm/index.mjs', import: '*', - limit: '39.5 KiB', + limit: '39 KiB', }, { name: 'Core (Content Script) - Modern - NPM (CJS)', path: 'dist/npm/modern/content-script/cjs/index.cjs', import: '*', - limit: '40 KiB', + limit: '39 KiB', }, { name: 'Core (Content Script) - Modern - NPM (UMD)', path: 'dist/npm/modern/content-script/umd/index.js', import: '*', - limit: '39.5 KiB', + limit: '39 KiB', }, ]; diff --git a/packages/analytics-v1.1/.size-limit.js b/packages/analytics-v1.1/.size-limit.js index f70166cf3..abd60fdde 100644 --- a/packages/analytics-v1.1/.size-limit.js +++ b/packages/analytics-v1.1/.size-limit.js @@ -40,12 +40,12 @@ module.exports = [ limit: '30 KiB', }, { - name: 'Core - Legacy - CDN', + name: 'Core (v1.1) - Legacy - CDN', path: 'dist/cdn/legacy/rudder-analytics.min.js', limit: '32.5 KiB', }, { - name: 'Core - Modern - CDN', + name: 'Core (v1.1) - Modern - CDN', path: 'dist/cdn/modern/rudder-analytics.min.js', limit: '32 KiB', }, From d2e051e2c6675fd5d84247cb63f64a7cc996333e Mon Sep 17 00:00:00 2001 From: Sai Kumar Battinoju Date: Thu, 2 Jan 2025 21:31:32 +0530 Subject: [PATCH 14/53] test: improve browser test suite --- .../analytics-js/__tests__/browser.test.ts | 117 +++++++++++------- .../analytics-js/__tests__/nativeSdkLoader.js | 4 +- 2 files changed, 75 insertions(+), 46 deletions(-) diff --git a/packages/analytics-js/__tests__/browser.test.ts b/packages/analytics-js/__tests__/browser.test.ts index c86353cbb..e63eea2f4 100644 --- a/packages/analytics-js/__tests__/browser.test.ts +++ b/packages/analytics-js/__tests__/browser.test.ts @@ -2,44 +2,72 @@ /* eslint-disable global-require */ import { loadingSnippet } from './nativeSdkLoader'; -const pathToSdk = '../dist/cdn/legacy/iife/rsa.js'; - describe('Test suite for the SDK', () => { + const WRITE_KEY = 'write-key'; + const DATA_PLANE_URL = 'https://example.dataplane.com'; + + const MOCK_SOURCE_CONFIGURATION = { + updatedAt: new Date().toISOString(), + source: { + name: 'source-name', + id: 'source-id', + workspaceId: 'workspace-id', + writeKey: WRITE_KEY, + updatedAt: new Date().toISOString(), + config: { + statsCollection: { + errors: { + enabled: false, + }, + metrics: { + enabled: false, + }, + }, + }, + enabled: true, + destinations: [], + }, + }; + const xhrMock: any = { open: jest.fn(), setRequestHeader: jest.fn(), onload: jest.fn(), onreadystatechange: jest.fn(), - responseText: JSON.stringify({ - source: { - config: {}, - id: 'id', - destinations: [], - }, - }), + responseText: JSON.stringify(MOCK_SOURCE_CONFIGURATION), status: 200, + send: jest.fn(() => xhrMock.onload()), }; - xhrMock.send = jest.fn(() => xhrMock.onload()); + const USER_ID = 'user-id'; + const USER_TRAITS = { + 'user-trait-key-1': 'user-trait-value-1', + 'user-trait-key-2': 'user-trait-value-2', + }; - const userId = 'jest-user-id'; - const userTraits = { - 'jest-user-trait-key-1': 'jest-user-trait-value-1', - 'jest-user-trait-key-2': 'jest-user-trait-value-2', + const USER_GROUP_ID = 'group-id'; + const USER_GROUP_TRAITS = { + 'group-trait-key-1': 'group-trait-value-1', + 'group-trait-key-2': 'group-trait-value-2', }; - const groupUserId = 'jest-group-id'; - const groupTraits = { - 'jest-group-trait-key-1': 'jest-group-trait-value-1', - 'jest-group-trait-key-2': 'jest-group-trait-value-2', + const SDK_PATH = '../dist/cdn/legacy/iife/rsa.js'; + + const loadAndWaitForSDK = async () => { + const readyPromise = new Promise(resolve => { + // eslint-disable-next-line sonarjs/no-nested-functions + window.rudderanalytics?.ready(() => resolve(true)); + }); + + require(SDK_PATH); + + await readyPromise; }; const originalXMLHttpRequest = window.XMLHttpRequest; afterEach(() => { jest.resetModules(); - jest.clearAllMocks(); - jest.restoreAllMocks(); window.rudderanalytics = undefined; @@ -47,45 +75,46 @@ describe('Test suite for the SDK', () => { }); describe('preload buffer', () => { - it('should process the buffered API calls when SDK script is loaded', done => { + it('should process the buffered API calls when SDK script is loaded', async () => { // Mocking the xhr function window.XMLHttpRequest = jest.fn(() => xhrMock) as unknown as typeof XMLHttpRequest; - loadingSnippet(); + loadingSnippet(WRITE_KEY, DATA_PLANE_URL); + window.rudderanalytics?.page(); - window.rudderanalytics?.ready(() => { - expect((window.rudderanalytics as any).push).not.toBe(Array.prototype.push); + window.rudderanalytics?.track('test-event'); - // one source config endpoint call and one implicit page call - expect(xhrMock.send).toHaveBeenCalledTimes(2); + await loadAndWaitForSDK(); - done(); - }); + expect((window.rudderanalytics as any).push).not.toBe(Array.prototype.push); - require(pathToSdk); + // one source configuration request, one page request, and one track request + expect(xhrMock.send).toHaveBeenCalledTimes(3); }); }); describe('api', () => { - beforeEach(done => { + beforeEach(async () => { // Mocking the xhr function window.XMLHttpRequest = jest.fn(() => xhrMock) as unknown as typeof XMLHttpRequest; - loadingSnippet(); + loadingSnippet(WRITE_KEY, DATA_PLANE_URL); - window.rudderanalytics?.ready(() => { - done(); - }); + await loadAndWaitForSDK(); + + window.rudderanalytics?.reset(); + }); - require(pathToSdk); + afterEach(() => { + window.XMLHttpRequest = originalXMLHttpRequest; }); it('should make network requests when event APIs are invoked', () => { window.rudderanalytics?.page(); window.rudderanalytics?.track('test-event'); - window.rudderanalytics?.identify('jest-user'); - window.rudderanalytics?.group('jest-group'); - window.rudderanalytics?.alias('new-jest-user', 'jest-user'); + window.rudderanalytics?.identify(USER_ID, USER_TRAITS); + window.rudderanalytics?.group(USER_GROUP_ID, USER_GROUP_TRAITS); + window.rudderanalytics?.alias('new-user-id', USER_ID); // one source config endpoint call and individual event requests expect(xhrMock.send).toHaveBeenCalledTimes(6); @@ -113,10 +142,10 @@ describe('Test suite for the SDK', () => { it('should clear all the persisted data except for anonymous ID when the flag is not set', () => { // Make identify and group API calls to let the SDK persist // user (ID and traits) and group data (ID and traits) - window.rudderanalytics?.identify(userId, userTraits); - window.rudderanalytics?.group(groupUserId, groupTraits); + window.rudderanalytics?.identify(USER_ID, USER_TRAITS); + window.rudderanalytics?.group(USER_GROUP_ID, USER_GROUP_TRAITS); - const anonId = 'jest-anon-ID'; + const anonId = 'anon-ID'; window.rudderanalytics?.setAnonymousId(anonId); // SDK clears all the persisted data except for anonymous ID @@ -135,10 +164,10 @@ describe('Test suite for the SDK', () => { it('should clear all the persisted data include anonymous ID when the flag is set', () => { // Make identify and group API calls to let the SDK persist // user (ID and traits) and group data (ID and traits) - window.rudderanalytics?.identify(userId, userTraits); - window.rudderanalytics?.group(groupUserId, groupTraits); + window.rudderanalytics?.identify(USER_ID, USER_TRAITS); + window.rudderanalytics?.group(USER_GROUP_ID, USER_GROUP_TRAITS); - const anonId = 'jest-anon-ID'; + const anonId = 'anon-ID'; window.rudderanalytics?.setAnonymousId(anonId); // SDK clears all the persisted data diff --git a/packages/analytics-js/__tests__/nativeSdkLoader.js b/packages/analytics-js/__tests__/nativeSdkLoader.js index faf6bdfe8..0dc747497 100644 --- a/packages/analytics-js/__tests__/nativeSdkLoader.js +++ b/packages/analytics-js/__tests__/nativeSdkLoader.js @@ -1,4 +1,4 @@ -function loadingSnippet(loadOptions) { +function loadingSnippet(writeKey, dpUrl, loadOptions) { (function () { 'use strict'; window.RudderSnippetVersion = '3.0.10'; @@ -56,7 +56,7 @@ function loadingSnippet(loadOptions) { } }; window.rudderAnalyticsMount(); - window.rudderanalytics.load('WRITE_KEY', 'https://some.reallookingdataplane.url', loadOptions ?? {}); + window.rudderanalytics.load(writeKey, dpUrl, loadOptions ?? {}); } } })(); From 059388afdbfb5dd2ee6f0a41df44d42bd49df22b Mon Sep 17 00:00:00 2001 From: Sai Kumar Battinoju Date: Thu, 2 Jan 2025 22:01:04 +0530 Subject: [PATCH 15/53] test: improve code coverage --- .../__tests__/utilities/errors.test.ts | 86 +++++++++++++++++-- 1 file changed, 79 insertions(+), 7 deletions(-) diff --git a/packages/analytics-js-common/__tests__/utilities/errors.test.ts b/packages/analytics-js-common/__tests__/utilities/errors.test.ts index d1dab5619..dd8d4ba16 100644 --- a/packages/analytics-js-common/__tests__/utilities/errors.test.ts +++ b/packages/analytics-js-common/__tests__/utilities/errors.test.ts @@ -1,18 +1,90 @@ -import { dispatchErrorEvent } from '../../src/utilities/errors'; +import { dispatchErrorEvent, getStacktrace } from '../../src/utilities/errors'; describe('Errors - utilities', () => { describe('dispatchErrorEvent', () => { + const dispatchEventMock = jest.fn(); + const originalDispatchEvent = globalThis.dispatchEvent; + + beforeEach(() => { + globalThis.dispatchEvent = dispatchEventMock; + }); + + afterEach(() => { + globalThis.dispatchEvent = originalDispatchEvent; + }); + it('should dispatch an error event', () => { - const dispatchEvent = jest.fn(); - const originalDispatchEvent = globalThis.dispatchEvent; + const error = new Error('Test error'); + + dispatchErrorEvent(error); + + expect(dispatchEventMock).toHaveBeenCalledWith(new ErrorEvent('error', { error })); + expect((error.stack as string).endsWith('[SDK DISPATCHED ERROR]')).toBeTruthy(); + }); - globalThis.dispatchEvent = dispatchEvent; + it('should decorate stacktrace before dispatching error event', () => { const error = new Error('Test error'); + // @ts-expect-error need to set the value for testing + error.stacktrace = error.stack; + delete error.stack; + dispatchErrorEvent(error); - expect(dispatchEvent).toHaveBeenCalledWith(new ErrorEvent('error', { error })); - // Cleanup - globalThis.dispatchEvent = originalDispatchEvent; + // @ts-expect-error need to check the stacktrace property + expect((error.stacktrace as string).endsWith('[SDK DISPATCHED ERROR]')).toBeTruthy(); + }); + + it('should decorate opera sourceloc before dispatching error event', () => { + const error = new Error('Test error'); + // @ts-expect-error need to set the value for testing + error['opera#sourceloc'] = error.stack; + delete error.stack; + + dispatchErrorEvent(error); + + // @ts-expect-error need to check the opera sourceloc property + expect((error['opera#sourceloc'] as string).endsWith('[SDK DISPATCHED ERROR]')).toBeTruthy(); + }); + }); + + describe('getStacktrace', () => { + it('should return stack if it is a string', () => { + const error = new Error('Test error'); + expect(getStacktrace(error)).toBe(error.stack); + }); + + it('should return stacktrace if it is a string', () => { + const error = new Error('Test error'); + // @ts-expect-error need to set the value for testing + error.stacktrace = error.stack; + delete error.stack; + + // @ts-expect-error need to check the stacktrace property + expect(getStacktrace(error)).toBe(error.stacktrace); + }); + + it('should return opera sourceloc if it is a string', () => { + const error = new Error('Test error'); + // @ts-expect-error need to set the value for testing + error['opera#sourceloc'] = error.stack; + delete error.stack; + + // @ts-expect-error need to check the opera sourceloc property + expect(getStacktrace(error)).toBe(error['opera#sourceloc']); + }); + + it('should return undefined if none of the properties are strings', () => { + const error = new Error('Test error'); + delete error.stack; + + expect(getStacktrace(error)).toBeUndefined(); + }); + + it('should return undefined if stack is the same as name and message', () => { + const error = new Error('Test error'); + error.stack = `${error.name}: ${error.message}`; + + expect(getStacktrace(error)).toBeUndefined(); }); }); }); From a3d856194ae98eb560d706289b68e2577c0618d7 Mon Sep 17 00:00:00 2001 From: Sai Kumar Battinoju Date: Fri, 3 Jan 2025 08:17:59 +0530 Subject: [PATCH 16/53] chore: use sonarqube scan --- .github/workflows/unit-tests-and-lint.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/unit-tests-and-lint.yml b/.github/workflows/unit-tests-and-lint.yml index f2599bf36..7ee9228ba 100644 --- a/.github/workflows/unit-tests-and-lint.yml +++ b/.github/workflows/unit-tests-and-lint.yml @@ -61,8 +61,8 @@ jobs: run: | ./scripts/fix-reports-path-in-github-runner.sh - - name: SonarCloud Scan - uses: SonarSource/sonarcloud-github-action@master + - name: SonarQube Scan + uses: SonarSource/sonarqube-scan-action@v4 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} From 69ac89b6ff9dfd427f105b19aaa46aeecbc70e6c Mon Sep 17 00:00:00 2001 From: Sai Kumar Battinoju Date: Fri, 3 Jan 2025 08:38:02 +0530 Subject: [PATCH 17/53] chore: minor improvements --- packages/analytics-js/__tests__/browser.test.ts | 1 + .../__tests__/components/utilities/event.test.ts | 8 -------- 2 files changed, 1 insertion(+), 8 deletions(-) delete mode 100644 packages/analytics-js/__tests__/components/utilities/event.test.ts diff --git a/packages/analytics-js/__tests__/browser.test.ts b/packages/analytics-js/__tests__/browser.test.ts index e63eea2f4..51b5475bd 100644 --- a/packages/analytics-js/__tests__/browser.test.ts +++ b/packages/analytics-js/__tests__/browser.test.ts @@ -68,6 +68,7 @@ describe('Test suite for the SDK', () => { afterEach(() => { jest.resetModules(); + jest.clearAllMocks(); window.rudderanalytics = undefined; diff --git a/packages/analytics-js/__tests__/components/utilities/event.test.ts b/packages/analytics-js/__tests__/components/utilities/event.test.ts deleted file mode 100644 index 57138ce20..000000000 --- a/packages/analytics-js/__tests__/components/utilities/event.test.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { isEvent } from '../../../src/components/utilities/event'; - -describe('Common Utils - Event', () => { - it('should check if is Event or Error', () => { - expect(isEvent(new Event('dummyEvent'))).toBeTruthy(); - expect(isEvent(new Error('dummyEvent'))).toBeFalsy(); - }); -}); From cb0f336207de1819847db904c7fa7bae2ac62054 Mon Sep 17 00:00:00 2001 From: Sai Kumar Battinoju Date: Fri, 3 Jan 2025 08:46:03 +0530 Subject: [PATCH 18/53] chore: minor improvements 2 --- .../components/capabilitiesManager/CapabilitiesManager.ts | 6 +----- .../src/components/eventRepository/EventRepository.ts | 1 - 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/analytics-js/src/components/capabilitiesManager/CapabilitiesManager.ts b/packages/analytics-js/src/components/capabilitiesManager/CapabilitiesManager.ts index e2d1dd664..10de4c547 100644 --- a/packages/analytics-js/src/components/capabilitiesManager/CapabilitiesManager.ts +++ b/packages/analytics-js/src/components/capabilitiesManager/CapabilitiesManager.ts @@ -205,11 +205,7 @@ class CapabilitiesManager implements ICapabilitiesManager { * @param error The error object */ onError(error: unknown): void { - if (this.errorHandler) { - this.errorHandler.onError(error, CAPABILITIES_MANAGER); - } else { - throw error; - } + this.errorHandler.onError(error, CAPABILITIES_MANAGER); } } diff --git a/packages/analytics-js/src/components/eventRepository/EventRepository.ts b/packages/analytics-js/src/components/eventRepository/EventRepository.ts index d89c9096e..9ad9a6f8f 100644 --- a/packages/analytics-js/src/components/eventRepository/EventRepository.ts +++ b/packages/analytics-js/src/components/eventRepository/EventRepository.ts @@ -211,7 +211,6 @@ class EventRepository implements IEventRepository { * Handles error * @param error The error object * @param customMessage a message - * @param shouldAlwaysThrow if it should throw or use logger */ onError(error: unknown, customMessage?: string): void { if (this.errorHandler) { From 115ab6a2f5b4bcfdaf926fc38f4b35087e91f78c Mon Sep 17 00:00:00 2001 From: Sai Kumar Battinoju Date: Fri, 3 Jan 2025 12:35:58 +0530 Subject: [PATCH 19/53] fix: error handling --- .../__mocks__/ErrorHandler.ts | 2 + .../__mocks__/HttpClient.ts | 4 +- .../__mocks__/PluginEngine.ts | 0 .../__mocks__/PluginsManager.ts | 0 .../analytics-js-common/__mocks__/Store.ts | 24 ++++-- .../__mocks__/StoreManager.ts | 3 +- .../src/component-cookie/index.ts | 2 +- .../ExternalSrcLoader/ExternalSrcLoader.ts | 16 ++-- .../src/services/ExternalSrcLoader/types.ts | 6 +- .../src/types/ErrorHandler.ts | 2 +- .../src/types/HttpClient.ts | 3 +- .../analytics-js-common/src/types/Store.ts | 14 ++-- .../analytics-js-plugins/__mocks__/state.ts | 1 + .../deviceModeTransformation/index.test.ts | 2 +- .../ketchConsentManager/utils.test.ts | 1 - .../src/iubendaConsentManager/utils.ts | 2 + .../src/ketchConsentManager/utils.ts | 2 + .../src/utilities/retryQueue/RetryQueue.ts | 6 ++ .../components/configManager/cdnPaths.test.ts | 73 ++++++++++++++----- .../eventManager/EventManager.test.ts | 65 ++++++++++------- .../UserSessionManager.test.ts | 9 ++- .../userSessionManager/utils.test.ts | 6 +- .../components/utilities/consent.test.ts | 9 ++- .../PluginEngine/PluginEngine.test.ts | 3 +- .../services/StoreManager/Store.test.ts | 22 ++++++ .../analytics-js/src/app/RudderAnalytics.ts | 2 +- .../CapabilitiesManager.ts | 4 +- .../components/capabilitiesManager/types.ts | 2 +- .../components/configManager/ConfigManager.ts | 61 +++++++++------- .../src/components/configManager/types.ts | 4 +- .../components/configManager/util/cdnPaths.ts | 25 +++++-- .../configManager/util/commonUtil.ts | 10 +-- .../src/components/core/Analytics.ts | 4 +- .../components/eventManager/EventManager.ts | 14 ++-- .../eventManager/RudderEventFactory.ts | 4 +- .../src/components/eventManager/utilities.ts | 26 ++++--- .../eventRepository/EventRepository.ts | 14 ++-- .../pluginsManager/PluginsManager.ts | 12 +-- .../userSessionManager/UserSessionManager.ts | 36 ++++----- .../components/userSessionManager/utils.ts | 8 +- .../src/components/utilities/consent.ts | 6 +- .../analytics-js/src/constants/logMessages.ts | 8 +- .../src/services/ErrorHandler/ErrorHandler.ts | 4 +- .../src/services/ErrorHandler/event/event.ts | 4 +- .../src/services/HttpClient/HttpClient.ts | 12 +-- .../HttpClient/xhr/xhrResponseHandler.ts | 11 +-- .../src/services/PluginEngine/PluginEngine.ts | 18 ++--- .../src/services/StoreManager/Store.ts | 26 +++---- .../src/services/StoreManager/StoreManager.ts | 18 ++--- 49 files changed, 347 insertions(+), 263 deletions(-) rename packages/{analytics-js-plugins => analytics-js-common}/__mocks__/PluginEngine.ts (100%) rename packages/{analytics-js-plugins => analytics-js-common}/__mocks__/PluginsManager.ts (100%) diff --git a/packages/analytics-js-common/__mocks__/ErrorHandler.ts b/packages/analytics-js-common/__mocks__/ErrorHandler.ts index a57ce3c08..3b3833052 100644 --- a/packages/analytics-js-common/__mocks__/ErrorHandler.ts +++ b/packages/analytics-js-common/__mocks__/ErrorHandler.ts @@ -1,11 +1,13 @@ import type { IErrorHandler } from '../src/types/ErrorHandler'; import { defaultHttpClient } from './HttpClient'; +import { defaultLogger } from './Logger'; // Mock all the methods of the ErrorHandler class class ErrorHandler implements IErrorHandler { onError = jest.fn(); leaveBreadcrumb = jest.fn(); httpClient = defaultHttpClient; + logger = defaultLogger; } const defaultErrorHandler = new ErrorHandler(); diff --git a/packages/analytics-js-common/__mocks__/HttpClient.ts b/packages/analytics-js-common/__mocks__/HttpClient.ts index c78322ea2..958e8ee19 100644 --- a/packages/analytics-js-common/__mocks__/HttpClient.ts +++ b/packages/analytics-js-common/__mocks__/HttpClient.ts @@ -1,10 +1,12 @@ import type { IErrorHandler } from '@rudderstack/analytics-js-common/types/ErrorHandler'; import type { IHttpClient } from '@rudderstack/analytics-js-common/types/HttpClient'; import type { ILogger } from '@rudderstack/analytics-js-common/types/Logger'; +import { defaultErrorHandler } from './ErrorHandler'; +import { defaultLogger } from './Logger'; class HttpClient implements IHttpClient { errorHandler?: IErrorHandler; - logger?: ILogger; + logger: ILogger = defaultLogger; hasErrorHandler = false; getData = jest.fn(); getAsyncData = jest.fn(); diff --git a/packages/analytics-js-plugins/__mocks__/PluginEngine.ts b/packages/analytics-js-common/__mocks__/PluginEngine.ts similarity index 100% rename from packages/analytics-js-plugins/__mocks__/PluginEngine.ts rename to packages/analytics-js-common/__mocks__/PluginEngine.ts diff --git a/packages/analytics-js-plugins/__mocks__/PluginsManager.ts b/packages/analytics-js-common/__mocks__/PluginsManager.ts similarity index 100% rename from packages/analytics-js-plugins/__mocks__/PluginsManager.ts rename to packages/analytics-js-common/__mocks__/PluginsManager.ts diff --git a/packages/analytics-js-common/__mocks__/Store.ts b/packages/analytics-js-common/__mocks__/Store.ts index d5ee148bf..1887cad96 100644 --- a/packages/analytics-js-common/__mocks__/Store.ts +++ b/packages/analytics-js-common/__mocks__/Store.ts @@ -1,23 +1,33 @@ -import type { IStore, IStoreConfig } from '../src/types/Store'; +import type { IPluginsManager } from '@rudderstack/analytics-js-common/types/PluginsManager'; +import type { IStorage, IStore, IStoreConfig } from '../src/types/Store'; import { defaultInMemoryStorage, defaultLocalStorage } from './Storage'; +import { defaultPluginsManager } from './PluginsManager'; +import { defaultLogger } from './Logger'; +import { defaultErrorHandler } from './ErrorHandler'; // Mock all the methods of the Store class class Store implements IStore { - constructor(config: IStoreConfig, engine?: any) { + constructor(config: IStoreConfig, engine: IStorage, pluginsManager: IPluginsManager) { this.id = config.id; this.name = config.name; this.isEncrypted = config.isEncrypted ?? false; this.validKeys = config.validKeys ?? {}; this.engine = engine ?? defaultLocalStorage; this.originalEngine = this.engine; + this.errorHandler = config.errorHandler; + this.logger = config.logger; + this.pluginsManager = pluginsManager; } id = 'test'; name = 'test'; isEncrypted = false; validKeys: Record; - engine = defaultLocalStorage; - originalEngine = defaultLocalStorage; + engine: IStorage = defaultLocalStorage; + originalEngine: IStorage = defaultLocalStorage; + errorHandler; + logger; + pluginsManager; createValidKey = (key: string) => { return [this.name, this.id, key].join('.'); }; @@ -51,6 +61,10 @@ class Store implements IStore { getOriginalEngine = () => this.originalEngine; } -const defaultStore = new Store({ id: 'test', name: 'test' }); +const defaultStore = new Store( + { id: 'test', name: 'test', errorHandler: defaultErrorHandler, logger: defaultLogger }, + defaultLocalStorage, + defaultPluginsManager, +); export { Store, defaultStore }; diff --git a/packages/analytics-js-common/__mocks__/StoreManager.ts b/packages/analytics-js-common/__mocks__/StoreManager.ts index da6498f14..822f0954d 100644 --- a/packages/analytics-js-common/__mocks__/StoreManager.ts +++ b/packages/analytics-js-common/__mocks__/StoreManager.ts @@ -1,4 +1,5 @@ import type { IStoreConfig, IStoreManager } from '../src/types/Store'; +import { defaultPluginsManager } from './PluginsManager'; import { defaultCookieStorage, defaultInMemoryStorage, defaultLocalStorage } from './Storage'; import { defaultStore, Store } from './Store'; @@ -21,7 +22,7 @@ class StoreManager implements IStoreManager { break; } - return new Store(config, storageEngine); + return new Store(config, storageEngine, defaultPluginsManager); }; getStore = jest.fn(() => defaultStore); initializeStorageState = jest.fn(); diff --git a/packages/analytics-js-common/src/component-cookie/index.ts b/packages/analytics-js-common/src/component-cookie/index.ts index 70388e626..1ecbb062d 100644 --- a/packages/analytics-js-common/src/component-cookie/index.ts +++ b/packages/analytics-js-common/src/component-cookie/index.ts @@ -22,7 +22,7 @@ const encode = (value: any, logger?: ILogger): string | undefined => { const decode = (value: string): string | undefined => { try { return decodeURIComponent(value); - } catch (err) { + } catch { // Do nothing as non-RS SDK cookies may not be URI encoded return undefined; } diff --git a/packages/analytics-js-common/src/services/ExternalSrcLoader/ExternalSrcLoader.ts b/packages/analytics-js-common/src/services/ExternalSrcLoader/ExternalSrcLoader.ts index a1454190c..9b6b44344 100644 --- a/packages/analytics-js-common/src/services/ExternalSrcLoader/ExternalSrcLoader.ts +++ b/packages/analytics-js-common/src/services/ExternalSrcLoader/ExternalSrcLoader.ts @@ -10,20 +10,18 @@ import { jsFileLoader } from './jsFileLoader'; * Service to load external resources/files */ class ExternalSrcLoader implements IExternalSrcLoader { - errorHandler?: IErrorHandler; - logger?: ILogger; - hasErrorHandler = false; + errorHandler: IErrorHandler; + logger: ILogger; timeout: number; constructor( - errorHandler?: IErrorHandler, - logger?: ILogger, + errorHandler: IErrorHandler, + logger: ILogger, timeout = DEFAULT_EXT_SRC_LOAD_TIMEOUT_MS, ) { this.errorHandler = errorHandler; this.logger = logger; this.timeout = timeout; - this.hasErrorHandler = Boolean(this.errorHandler); this.onError = this.onError.bind(this); } @@ -52,11 +50,7 @@ class ExternalSrcLoader implements IExternalSrcLoader { * Handle errors */ onError(error: unknown) { - if (this.hasErrorHandler) { - this.errorHandler?.onError(error, EXTERNAL_SRC_LOADER); - } else { - throw error; - } + this.errorHandler?.onError(error, EXTERNAL_SRC_LOADER); } } diff --git a/packages/analytics-js-common/src/services/ExternalSrcLoader/types.ts b/packages/analytics-js-common/src/services/ExternalSrcLoader/types.ts index 31a5ecf29..f1182fbf7 100644 --- a/packages/analytics-js-common/src/services/ExternalSrcLoader/types.ts +++ b/packages/analytics-js-common/src/services/ExternalSrcLoader/types.ts @@ -1,4 +1,4 @@ -import type { ErrorState, IErrorHandler } from '../../types/ErrorHandler'; +import type { IErrorHandler } from '../../types/ErrorHandler'; import type { ILogger } from '../../types/Logger'; export interface IExternalSourceLoadConfig { @@ -11,8 +11,8 @@ export interface IExternalSourceLoadConfig { } export interface IExternalSrcLoader { - errorHandler?: IErrorHandler; - logger?: ILogger; + errorHandler: IErrorHandler; + logger: ILogger; timeout: number; loadJSFile(config: IExternalSourceLoadConfig): void; } diff --git a/packages/analytics-js-common/src/types/ErrorHandler.ts b/packages/analytics-js-common/src/types/ErrorHandler.ts index e2953a006..739feff0f 100644 --- a/packages/analytics-js-common/src/types/ErrorHandler.ts +++ b/packages/analytics-js-common/src/types/ErrorHandler.ts @@ -5,7 +5,7 @@ export type SDKError = unknown | Error | ErrorEvent | Event | PromiseRejectionEv export interface IErrorHandler { httpClient: IHttpClient; - logger?: ILogger; + logger: ILogger; onError(error: SDKError, context?: string, customMessage?: string, errorType?: string): void; leaveBreadcrumb(breadcrumb: string): void; } diff --git a/packages/analytics-js-common/src/types/HttpClient.ts b/packages/analytics-js-common/src/types/HttpClient.ts index f528e8501..b61d48353 100644 --- a/packages/analytics-js-common/src/types/HttpClient.ts +++ b/packages/analytics-js-common/src/types/HttpClient.ts @@ -56,9 +56,8 @@ export type HTTPClientMethod = export interface IHttpClient { errorHandler?: IErrorHandler; - logger?: ILogger; + logger: ILogger; basicAuthHeader?: string; - hasErrorHandler: boolean; getData( config: IRequestConfig, ): Promise<{ data: T | string | undefined; details?: ResponseDetails }>; diff --git a/packages/analytics-js-common/src/types/Store.ts b/packages/analytics-js-common/src/types/Store.ts index 1ab176bd2..ff3c79312 100644 --- a/packages/analytics-js-common/src/types/Store.ts +++ b/packages/analytics-js-common/src/types/Store.ts @@ -12,16 +12,16 @@ export interface IStoreConfig { isEncrypted?: boolean; validKeys?: Record; noCompoundKey?: boolean; - errorHandler?: IErrorHandler; - logger?: ILogger; + errorHandler: IErrorHandler; + logger: ILogger; type?: StorageType; } export interface IStoreManager { stores?: Record; isInitialized?: boolean; - errorHandler?: IErrorHandler; - logger?: ILogger; + errorHandler: IErrorHandler; + logger: ILogger; init(): void; initializeStorageState(): void; setStore(storeConfig: IStoreConfig): IStore; @@ -37,9 +37,9 @@ export interface IStore { originalEngine: IStorage; noKeyValidation?: boolean; noCompoundKey?: boolean; - errorHandler?: IErrorHandler; - logger?: ILogger; - pluginsManager?: IPluginsManager; + errorHandler: IErrorHandler; + logger: ILogger; + pluginsManager: IPluginsManager; createValidKey(key: string): string | undefined; swapQueueStoreToInMemoryEngine(): void; set(key: string, value: any): void; diff --git a/packages/analytics-js-plugins/__mocks__/state.ts b/packages/analytics-js-plugins/__mocks__/state.ts index 05869b933..881eb3fc5 100644 --- a/packages/analytics-js-plugins/__mocks__/state.ts +++ b/packages/analytics-js-plugins/__mocks__/state.ts @@ -154,6 +154,7 @@ const defaultStateValues: ApplicationState = { source: signal({ id: 'dummy-source-id', workspaceId: 'dummy-workspace-id', + name: 'dummy-source-name', }), storage: { encryptionPluginName: signal(undefined), diff --git a/packages/analytics-js-plugins/__tests__/deviceModeTransformation/index.test.ts b/packages/analytics-js-plugins/__tests__/deviceModeTransformation/index.test.ts index fce91955a..e5036f8c7 100644 --- a/packages/analytics-js-plugins/__tests__/deviceModeTransformation/index.test.ts +++ b/packages/analytics-js-plugins/__tests__/deviceModeTransformation/index.test.ts @@ -6,6 +6,7 @@ import { defaultLogger } from '@rudderstack/analytics-js-common/__mocks__/Logger import { defaultStoreManager } from '@rudderstack/analytics-js-common/__mocks__/StoreManager'; import type { ExtensionPoint } from '@rudderstack/analytics-js-common/types/PluginEngine'; import { defaultHttpClient } from '@rudderstack/analytics-js-common/__mocks__/HttpClient'; +import { defaultPluginsManager } from '@rudderstack/analytics-js-common/__mocks__/PluginsManager'; import * as utils from '../../src/deviceModeTransformation/utilities'; import { DeviceModeTransformation } from '../../src/deviceModeTransformation'; import { @@ -16,7 +17,6 @@ import { } from '../../__fixtures__/fixtures'; import { server } from '../../__fixtures__/msw.server'; import { resetState, state } from '../../__mocks__/state'; -import { defaultPluginsManager } from '../../__mocks__/PluginsManager'; import type { RetryQueue } from '../../src/utilities/retryQueue/RetryQueue'; import type { QueueItem, QueueItemData } from '../../src/types/plugins'; diff --git a/packages/analytics-js-plugins/__tests__/ketchConsentManager/utils.test.ts b/packages/analytics-js-plugins/__tests__/ketchConsentManager/utils.test.ts index af3eb2a7c..4b5b30d91 100644 --- a/packages/analytics-js-plugins/__tests__/ketchConsentManager/utils.test.ts +++ b/packages/analytics-js-plugins/__tests__/ketchConsentManager/utils.test.ts @@ -6,7 +6,6 @@ import { getConsentData, getKetchConsentData, } from '../../src/ketchConsentManager/utils'; -import { defaultPluginsManager } from '../../__mocks__/PluginsManager'; describe('KetchConsentManager - Utils', () => { beforeEach(() => { diff --git a/packages/analytics-js-plugins/src/iubendaConsentManager/utils.ts b/packages/analytics-js-plugins/src/iubendaConsentManager/utils.ts index a9ac8eba7..7b9bc78ea 100644 --- a/packages/analytics-js-plugins/src/iubendaConsentManager/utils.ts +++ b/packages/analytics-js-plugins/src/iubendaConsentManager/utils.ts @@ -51,6 +51,8 @@ const getIubendaConsentData = ( id: IUBENDA_CONSENT_MANAGER_PLUGIN, name: IUBENDA_CONSENT_MANAGER_PLUGIN, type: COOKIE_STORAGE, + errorHandler: storeManager?.errorHandler, + logger: storeManager?.logger, }); rawConsentCookieData = dataStore?.engine.getItem(getIubendaCookieName(logger)); } catch (err) { diff --git a/packages/analytics-js-plugins/src/ketchConsentManager/utils.ts b/packages/analytics-js-plugins/src/ketchConsentManager/utils.ts index 9677beef4..bc25a4fc5 100644 --- a/packages/analytics-js-plugins/src/ketchConsentManager/utils.ts +++ b/packages/analytics-js-plugins/src/ketchConsentManager/utils.ts @@ -25,6 +25,8 @@ const getKetchConsentData = ( id: KETCH_CONSENT_MANAGER_PLUGIN, name: KETCH_CONSENT_MANAGER_PLUGIN, type: COOKIE_STORAGE, + errorHandler: storeManager?.errorHandler, + logger: storeManager?.logger, }); rawConsentCookieData = dataStore?.engine.getItem(KETCH_CONSENT_COOKIE_NAME_V1); } catch (err) { diff --git a/packages/analytics-js-plugins/src/utilities/retryQueue/RetryQueue.ts b/packages/analytics-js-plugins/src/utilities/retryQueue/RetryQueue.ts index cc4f30727..debc5b911 100644 --- a/packages/analytics-js-plugins/src/utilities/retryQueue/RetryQueue.ts +++ b/packages/analytics-js-plugins/src/utilities/retryQueue/RetryQueue.ts @@ -134,6 +134,8 @@ class RetryQueue implements IQueue { name: this.name, validKeys: QueueStatuses, type: storageType, + errorHandler: this.storeManager.errorHandler, + logger: this.storeManager.logger, }); this.setDefaultQueueEntries(); @@ -583,6 +585,8 @@ class RetryQueue implements IQueue { name: this.name, validKeys: QueueStatuses, type: LOCAL_STORAGE, + errorHandler: this.storeManager.errorHandler, + logger: this.storeManager.logger, }); const our = { queue: (this.getStorageEntry(QueueStatuses.QUEUE) ?? []) as QueueItem[], @@ -770,6 +774,8 @@ class RetryQueue implements IQueue { name, validKeys: QueueStatuses, type: LOCAL_STORAGE, + errorHandler: this.storeManager.errorHandler, + logger: this.storeManager.logger, }), ); } diff --git a/packages/analytics-js/__tests__/components/configManager/cdnPaths.test.ts b/packages/analytics-js/__tests__/components/configManager/cdnPaths.test.ts index 1f2e91288..55040ca68 100644 --- a/packages/analytics-js/__tests__/components/configManager/cdnPaths.test.ts +++ b/packages/analytics-js/__tests__/components/configManager/cdnPaths.test.ts @@ -4,7 +4,8 @@ import { getPluginsCDNPath, } from '../../../src/components/configManager/util/cdnPaths'; import { getSDKUrl } from '../../../src/components/configManager/util/commonUtil'; -import { DEST_SDK_BASE_URL, SDK_CDN_BASE_URL } from '../../../src/constants/urls'; +import { SDK_CDN_BASE_URL } from '../../../src/constants/urls'; +import { defaultLogger } from '../../../src/services/Logger'; jest.mock('../../../src/components/configManager/util/commonUtil.ts', () => { const originalModule = jest.requireActual( @@ -33,26 +34,45 @@ describe('CDN path utilities', () => { }); it('should return custom url if valid url is provided', () => { - const integrationsCDNPath = getIntegrationsCDNPath(dummyVersion, false, dummyCustomURL); + const integrationsCDNPath = getIntegrationsCDNPath( + dummyVersion, + false, + dummyCustomURL, + defaultLogger, + ); expect(integrationsCDNPath).toBe(dummyCustomURL); }); it('should throw error if invalid custom url is provided', () => { - const integrationsCDNPath = () => getIntegrationsCDNPath(dummyVersion, false, '/'); - expect(integrationsCDNPath).toThrow( - 'Failed to load the SDK as the base URL for integrations is not valid.', + const errorSpy = jest.spyOn(defaultLogger, 'error'); + const integrationsCDNPath = getIntegrationsCDNPath(dummyVersion, false, '/', defaultLogger); + expect(integrationsCDNPath).toBeNull(); + expect(errorSpy).toHaveBeenCalledWith( + 'ConfigManager:: The base URL "/" for integrations is not valid.', ); + + errorSpy.mockRestore(); }); it('should return script src path if script src exists and integrations version is not locked', () => { - const integrationsCDNPath = getIntegrationsCDNPath(dummyVersion, false, undefined); + const integrationsCDNPath = getIntegrationsCDNPath( + dummyVersion, + false, + undefined, + defaultLogger, + ); expect(integrationsCDNPath).toBe( 'https://www.dummy.url/fromScript/v3/modern/js-integrations', ); }); it('should return script src path with versioned folder if script src exists and integrations version is locked', () => { - const integrationsCDNPath = getIntegrationsCDNPath(dummyVersion, true, undefined); + const integrationsCDNPath = getIntegrationsCDNPath( + dummyVersion, + true, + undefined, + defaultLogger, + ); expect(integrationsCDNPath).toBe( 'https://www.dummy.url/fromScript/3.x.x/modern/js-integrations', ); @@ -61,14 +81,24 @@ describe('CDN path utilities', () => { it('should return default path if no script src exists and integrations version is not locked', () => { getSDKUrl.mockImplementation(() => undefined); - const integrationsCDNPath = getIntegrationsCDNPath(dummyVersion, false, undefined); + const integrationsCDNPath = getIntegrationsCDNPath( + dummyVersion, + false, + undefined, + defaultLogger, + ); expect(integrationsCDNPath).toBe('https://cdn.rudderlabs.com/v3/modern/js-integrations'); }); it('should return default path with versioned folder if no script src exists and integrations version is locked', () => { getSDKUrl.mockImplementation(() => undefined); - const integrationsCDNPath = getIntegrationsCDNPath(dummyVersion, true, undefined); + const integrationsCDNPath = getIntegrationsCDNPath( + dummyVersion, + true, + undefined, + defaultLogger, + ); expect(integrationsCDNPath).toBe(`${SDK_CDN_BASE_URL}/${dummyVersion}/modern/${CDN_INT_DIR}`); }); }); @@ -87,38 +117,47 @@ describe('CDN path utilities', () => { }); it('should return plugins CDN URL if a valid custom URL is provided', () => { - const pluginsCDNPath = getPluginsCDNPath(dummyVersion, false, dummyCustomURL); + const pluginsCDNPath = getPluginsCDNPath(dummyVersion, false, dummyCustomURL, defaultLogger); expect(pluginsCDNPath).toBe('https://www.dummy.url/plugins'); }); it('should throw error if invalid custom url is provided', () => { - const pluginsCDNPath = () => getPluginsCDNPath(dummyVersion, false, 'htp:/some.broken.url'); - expect(pluginsCDNPath).toThrow( - 'Failed to load the SDK as the base URL for plugins is not valid.', + const errorSpy = jest.spyOn(defaultLogger, 'error'); + const pluginsCDNPath = getPluginsCDNPath( + dummyVersion, + false, + 'htp:/some.broken.url', + defaultLogger, ); + expect(pluginsCDNPath).toBeNull(); + expect(errorSpy).toHaveBeenCalledWith( + 'ConfigManager:: The base URL "htp:/some.broken.url" for plugins is not valid.', + ); + + errorSpy.mockRestore(); }); it('should return script src path if script src exists and plugins version is not locked', () => { - const pluginsCDNPath = getPluginsCDNPath(dummyVersion, false); + const pluginsCDNPath = getPluginsCDNPath(dummyVersion, false, undefined, defaultLogger); expect(pluginsCDNPath).toBe('https://www.dummy.url/fromScript/v3/modern/plugins'); }); it('should return script src path with versioned folder if script src exists and plugins version is locked', () => { - const pluginsCDNPath = getPluginsCDNPath(dummyVersion, true); + const pluginsCDNPath = getPluginsCDNPath(dummyVersion, true, undefined, defaultLogger); expect(pluginsCDNPath).toBe('https://www.dummy.url/fromScript/3.x.x/modern/plugins'); }); it('should return default path if no script src exists and plugins version is not locked', () => { getSDKUrl.mockImplementation(() => undefined); - const pluginsCDNPath = getPluginsCDNPath(dummyVersion, false, undefined); + const pluginsCDNPath = getPluginsCDNPath(dummyVersion, false, undefined, defaultLogger); expect(pluginsCDNPath).toBe('https://cdn.rudderlabs.com/v3/modern/plugins'); }); it('should return default path if no script src exists but plugins version is locked', () => { getSDKUrl.mockImplementation(() => undefined); - const pluginsCDNPath = getPluginsCDNPath(dummyVersion, true, undefined); + const pluginsCDNPath = getPluginsCDNPath(dummyVersion, true, undefined, defaultLogger); expect(pluginsCDNPath).toBe('https://cdn.rudderlabs.com/3.x.x/modern/plugins'); }); }); diff --git a/packages/analytics-js/__tests__/components/eventManager/EventManager.test.ts b/packages/analytics-js/__tests__/components/eventManager/EventManager.test.ts index ac408c6c4..b363ec7f3 100644 --- a/packages/analytics-js/__tests__/components/eventManager/EventManager.test.ts +++ b/packages/analytics-js/__tests__/components/eventManager/EventManager.test.ts @@ -1,10 +1,14 @@ import type { IErrorHandler } from '@rudderstack/analytics-js-common/types/ErrorHandler'; -import { EventManager } from '@rudderstack/analytics-js/components/eventManager/EventManager'; -import { EventRepository } from '@rudderstack/analytics-js/components/eventRepository/EventRepository'; -import { UserSessionManager } from '@rudderstack/analytics-js/components/userSessionManager/UserSessionManager'; -import { PluginEngine } from '@rudderstack/analytics-js/services/PluginEngine/PluginEngine'; -import { StoreManager } from '@rudderstack/analytics-js/services/StoreManager/StoreManager'; -import { PluginsManager } from '@rudderstack/analytics-js/components/pluginsManager/PluginsManager'; +import type { IHttpClient } from '@rudderstack/analytics-js-common/types/HttpClient'; +import type { ILogger } from '@rudderstack/analytics-js-common/types/Logger'; +import { EventManager } from '../../../src/components/eventManager/EventManager'; +import { EventRepository } from '../../../src/components/eventRepository/EventRepository'; +import { UserSessionManager } from '../../../src/components/userSessionManager/UserSessionManager'; +import { PluginEngine } from '../../../src/services/PluginEngine/PluginEngine'; +import { StoreManager } from '../../../src/services/StoreManager/StoreManager'; +import { PluginsManager } from '../../../src/components/pluginsManager/PluginsManager'; +import { defaultLogger } from '../../../src/services/Logger'; +import { defaultHttpClient } from '../../../src/services/HttpClient'; describe('EventManager', () => { class MockErrorHandler implements IErrorHandler { @@ -12,21 +16,40 @@ describe('EventManager', () => { leaveBreadcrumb = jest.fn(); notifyError = jest.fn(); init = jest.fn(); + httpClient: IHttpClient = defaultHttpClient; + logger: ILogger = defaultLogger; } const mockErrorHandler = new MockErrorHandler(); - const pluginEngine = new PluginEngine(); - const pluginsManager = new PluginsManager(pluginEngine, mockErrorHandler); - const storeManager = new StoreManager(pluginsManager, mockErrorHandler); - const eventRepository = new EventRepository(pluginsManager, storeManager, mockErrorHandler); - const userSessionManager = new UserSessionManager(); - const eventManager = new EventManager(eventRepository, userSessionManager, mockErrorHandler); + const pluginEngine = new PluginEngine(defaultLogger); + const pluginsManager = new PluginsManager(pluginEngine, mockErrorHandler, defaultLogger); + const storeManager = new StoreManager(pluginsManager, mockErrorHandler, defaultLogger); + const eventRepository = new EventRepository( + pluginsManager, + storeManager, + defaultHttpClient, + mockErrorHandler, + defaultLogger, + ); + const userSessionManager = new UserSessionManager( + pluginsManager, + storeManager, + defaultHttpClient, + mockErrorHandler, + defaultLogger, + ); + const eventManager = new EventManager( + eventRepository, + userSessionManager, + mockErrorHandler, + defaultLogger, + ); describe('init', () => { it('should initialize on init', () => { const eventRepositoryInitSpy = jest.spyOn(eventRepository, 'init'); eventManager.init(); - expect(eventRepositoryInitSpy).toBeCalled(); + expect(eventRepositoryInitSpy).toHaveBeenCalled(); eventRepositoryInitSpy.mockRestore(); }); @@ -49,27 +72,13 @@ describe('EventManager', () => { undefined, ); }); - - it('should throw an exception if the event data is invalid and error handler is not defined', () => { - const eventManager = new EventManager(eventRepository, userSessionManager); - expect(() => { - eventManager.addEvent({ - // @ts-ignore - type: 'test', - event: 'test', - properties: { - test: 'test', - }, - }); - }).toThrowError('Failed to generate the event object.'); - }); }); describe('resume', () => { it('should resume on resume', () => { const eventRepositoryResumeSpy = jest.spyOn(eventRepository, 'resume'); eventManager.resume(); - expect(eventRepositoryResumeSpy).toBeCalled(); + expect(eventRepositoryResumeSpy).toHaveBeenCalled(); eventRepositoryResumeSpy.mockRestore(); }); diff --git a/packages/analytics-js/__tests__/components/userSessionManager/UserSessionManager.test.ts b/packages/analytics-js/__tests__/components/userSessionManager/UserSessionManager.test.ts index baa870104..bbac6abf8 100644 --- a/packages/analytics-js/__tests__/components/userSessionManager/UserSessionManager.test.ts +++ b/packages/analytics-js/__tests__/components/userSessionManager/UserSessionManager.test.ts @@ -91,11 +91,11 @@ describe('User session manager', () => { state.storage.entries.value = entriesWithOnlyCookieStorage; userSessionManager = new UserSessionManager( - defaultErrorHandler, - defaultLogger, defaultPluginsManager, defaultStoreManager, defaultHttpClient, + defaultErrorHandler, + defaultLogger, ); }); @@ -298,10 +298,11 @@ describe('User session manager', () => { storeManager.init(); userSessionManager = new UserSessionManager( - defaultErrorHandler, - defaultLogger, mockPluginsManager, storeManager, + defaultHttpClient, + defaultErrorHandler, + defaultLogger, ); setDataInCookieStorageEngine(customData); diff --git a/packages/analytics-js/__tests__/components/userSessionManager/utils.test.ts b/packages/analytics-js/__tests__/components/userSessionManager/utils.test.ts index af86ca91f..3fd3b9608 100644 --- a/packages/analytics-js/__tests__/components/userSessionManager/utils.test.ts +++ b/packages/analytics-js/__tests__/components/userSessionManager/utils.test.ts @@ -44,7 +44,7 @@ describe('Utility: User session manager', () => { describe('generateManualTrackingSession:', () => { it('should return newly generated manual session', () => { const sessionId = 1234567890; - const outcome = generateManualTrackingSession(sessionId); + const outcome = generateManualTrackingSession(sessionId, defaultLogger); expect(outcome).toEqual({ manualTrack: true, id: sessionId, @@ -52,7 +52,7 @@ describe('Utility: User session manager', () => { }); }); it('should return newly generated manual session if session id is not provided', () => { - const outcome = generateManualTrackingSession(); + const outcome = generateManualTrackingSession(undefined, defaultLogger); expect(outcome).toEqual({ manualTrack: true, id: expect.any(Number), @@ -62,6 +62,7 @@ describe('Utility: User session manager', () => { it('should print a error message if the provided session id is not a number', () => { const sessionId = '1234567890'; defaultLogger.warn = jest.fn(); + // @ts-expect-error testing invalid input generateManualTrackingSession(sessionId, defaultLogger); expect(defaultLogger.warn).toHaveBeenCalledWith( `UserSessionManager:: The provided session ID (${sessionId}) is either invalid, not a positive integer, or not at least "${MIN_SESSION_ID_LENGTH}" digits long. A new session ID will be auto-generated instead.`, @@ -91,6 +92,7 @@ describe('Utility: User session manager', () => { const outcome3 = isStorageTypeValidForStoringData('memoryStorage'); const outcome4 = isStorageTypeValidForStoringData('sessionStorage'); const outcome5 = isStorageTypeValidForStoringData('none'); + // @ts-expect-error testing invalid input const outcome6 = isStorageTypeValidForStoringData('random'); expect(outcome1).toEqual(true); expect(outcome2).toEqual(true); diff --git a/packages/analytics-js/__tests__/components/utilities/consent.test.ts b/packages/analytics-js/__tests__/components/utilities/consent.test.ts index b96994888..861412abf 100644 --- a/packages/analytics-js/__tests__/components/utilities/consent.test.ts +++ b/packages/analytics-js/__tests__/components/utilities/consent.test.ts @@ -1,9 +1,10 @@ -import { resetState, state } from '@rudderstack/analytics-js/state'; +import { resetState, state } from '../../../src/state'; import { getUserSelectedConsentManager, getValidPostConsentOptions, getConsentManagementData, } from '../../../src/components/utilities/consent'; +import { defaultLogger } from '../../../src/services/Logger'; describe('consent utilties', () => { beforeEach(() => { @@ -139,7 +140,7 @@ describe('consent utilties', () => { deniedConsentIds: [], }, }; - const validOptions = getConsentManagementData(); + const validOptions = getConsentManagementData(undefined, defaultLogger); expect(validOptions).toEqual(expectedOutcome); }); @@ -162,7 +163,7 @@ describe('consent utilties', () => { provider: 'oneTrust', }; - const validOptions = getConsentManagementData(consentOptions); + const validOptions = getConsentManagementData(consentOptions, defaultLogger); expect(validOptions).toEqual(expectedOutcome); }); @@ -182,7 +183,7 @@ describe('consent utilties', () => { }, }; - const validOptions = getConsentManagementData(consentOptions); + const validOptions = getConsentManagementData(consentOptions, defaultLogger); expect(validOptions).toEqual(expectedOutcome); }); }); diff --git a/packages/analytics-js/__tests__/services/PluginEngine/PluginEngine.test.ts b/packages/analytics-js/__tests__/services/PluginEngine/PluginEngine.test.ts index e474766b6..25cc831e9 100644 --- a/packages/analytics-js/__tests__/services/PluginEngine/PluginEngine.test.ts +++ b/packages/analytics-js/__tests__/services/PluginEngine/PluginEngine.test.ts @@ -1,5 +1,6 @@ import type { ExtensionPlugin } from '@rudderstack/analytics-js-common/types/PluginEngine'; import { PluginEngine } from '../../../src/services/PluginEngine/PluginEngine'; +import { defaultLogger } from '../../../src/services/Logger'; const mockPlugin1: ExtensionPlugin = { name: 'p1', @@ -34,7 +35,7 @@ describe('PluginEngine', () => { let pluginEngineTestInstance: PluginEngine; beforeEach(() => { - pluginEngineTestInstance = new PluginEngine(); + pluginEngineTestInstance = new PluginEngine(defaultLogger); pluginEngineTestInstance.register(mockPlugin1); pluginEngineTestInstance.register(mockPlugin2); pluginEngineTestInstance.register(mockPlugin3); diff --git a/packages/analytics-js/__tests__/services/StoreManager/Store.test.ts b/packages/analytics-js/__tests__/services/StoreManager/Store.test.ts index c2f91298f..8b44eb3b8 100644 --- a/packages/analytics-js/__tests__/services/StoreManager/Store.test.ts +++ b/packages/analytics-js/__tests__/services/StoreManager/Store.test.ts @@ -1,6 +1,10 @@ import { QueueStatuses } from '@rudderstack/analytics-js-common/constants/QueueStatuses'; import { Store } from '../../../src/services/StoreManager/Store'; import { getStorageEngine } from '../../../src/services/StoreManager/storages/storageEngine'; +import { defaultErrorHandler } from '../../../src/services/ErrorHandler'; +import { defaultLogger } from '../../../src/services/Logger'; +import { PluginsManager } from '../../../src/components/pluginsManager'; +import { PluginEngine } from '../../../src/services/PluginEngine'; describe('Store', () => { let store: Store; @@ -22,6 +26,9 @@ describe('Store', () => { }, }; + const pluginEngine = new PluginEngine(defaultLogger); + const pluginsManager = new PluginsManager(pluginEngine, defaultErrorHandler, defaultLogger); + beforeEach(() => { engine.clear(); store = new Store( @@ -29,8 +36,11 @@ describe('Store', () => { name: 'name', id: 'id', validKeys: QueueStatuses, + errorHandler: defaultErrorHandler, + logger: defaultLogger, }, getStorageEngine('localStorage'), + pluginsManager, ); }); @@ -81,8 +91,11 @@ describe('Store', () => { { name: 'name', id: 'id', + errorHandler: defaultErrorHandler, + logger: defaultLogger, }, getStorageEngine('localStorage'), + pluginsManager, ); expect(store.createValidKey('test')).toStrictEqual('name.id.test'); }); @@ -95,8 +108,11 @@ describe('Store', () => { name: 'name', id: 'id', validKeys: { nope: 'wrongKey' }, + errorHandler: defaultErrorHandler, + logger: defaultLogger, }, getStorageEngine('localStorage'), + pluginsManager, ); expect(store.createValidKey('test')).toBeUndefined(); }); @@ -107,8 +123,11 @@ describe('Store', () => { { name: 'name', id: 'id', + errorHandler: defaultErrorHandler, + logger: defaultLogger, }, getStorageEngine('localStorage'), + pluginsManager, ); expect(store.createValidKey('queue')).toStrictEqual('name.id.queue'); }); @@ -133,8 +152,11 @@ describe('Store', () => { name: 'name', id: 'id', validKeys: QueueStatuses, + errorHandler: defaultErrorHandler, + logger: defaultLogger, }, lsProxy, + pluginsManager, ); Object.keys(QueueStatuses).forEach(keyValue => { diff --git a/packages/analytics-js/src/app/RudderAnalytics.ts b/packages/analytics-js/src/app/RudderAnalytics.ts index f969f7c89..8060df0bc 100644 --- a/packages/analytics-js/src/app/RudderAnalytics.ts +++ b/packages/analytics-js/src/app/RudderAnalytics.ts @@ -276,7 +276,7 @@ class RudderAnalytics implements IRudderAnalytics { } }); } else { - // throw warning if beacon is disabled + // log warning if beacon is disabled this.logger.warn(PAGE_UNLOAD_ON_BEACON_DISABLED_WARNING(RSA)); } } diff --git a/packages/analytics-js/src/components/capabilitiesManager/CapabilitiesManager.ts b/packages/analytics-js/src/components/capabilitiesManager/CapabilitiesManager.ts index 10de4c547..8c6da6c46 100644 --- a/packages/analytics-js/src/components/capabilitiesManager/CapabilitiesManager.ts +++ b/packages/analytics-js/src/components/capabilitiesManager/CapabilitiesManager.ts @@ -39,10 +39,10 @@ import { debounce } from '../utilities/globals'; class CapabilitiesManager implements ICapabilitiesManager { httpClient: IHttpClient; errorHandler: IErrorHandler; - logger?: ILogger; + logger: ILogger; externalSrcLoader: IExternalSrcLoader; - constructor(httpClient: IHttpClient, errorHandler: IErrorHandler, logger?: ILogger) { + constructor(httpClient: IHttpClient, errorHandler: IErrorHandler, logger: ILogger) { this.httpClient = httpClient; this.errorHandler = errorHandler; this.logger = logger; diff --git a/packages/analytics-js/src/components/capabilitiesManager/types.ts b/packages/analytics-js/src/components/capabilitiesManager/types.ts index ce64d0a04..4a3040ae5 100644 --- a/packages/analytics-js/src/components/capabilitiesManager/types.ts +++ b/packages/analytics-js/src/components/capabilitiesManager/types.ts @@ -6,7 +6,7 @@ import type { IHttpClient } from '@rudderstack/analytics-js-common/types/HttpCli export interface ICapabilitiesManager { httpClient: IHttpClient; errorHandler: IErrorHandler; - logger?: ILogger; + logger: ILogger; externalSrcLoader: IExternalSrcLoader; init(): void; detectBrowserCapabilities(): void; diff --git a/packages/analytics-js/src/components/configManager/ConfigManager.ts b/packages/analytics-js/src/components/configManager/ConfigManager.ts index bff7e5f12..90a3245d1 100644 --- a/packages/analytics-js/src/components/configManager/ConfigManager.ts +++ b/packages/analytics-js/src/components/configManager/ConfigManager.ts @@ -10,6 +10,7 @@ import type { Destination } from '@rudderstack/analytics-js-common/types/Destina import type { ILogger } from '@rudderstack/analytics-js-common/types/Logger'; import { CONFIG_MANAGER } from '@rudderstack/analytics-js-common/constants/loggerContexts'; import type { IntegrationOpts } from '@rudderstack/analytics-js-common/types/Integration'; +import type { Nullable } from '@rudderstack/analytics-js-common/types/Nullable'; import { isValidSourceConfig } from './util/validate'; import { SOURCE_CONFIG_FETCH_ERROR, @@ -35,15 +36,13 @@ import { METRICS_SERVICE_ENDPOINT } from './constants'; class ConfigManager implements IConfigManager { httpClient: IHttpClient; - errorHandler?: IErrorHandler; - logger?: ILogger; - hasErrorHandler = false; + errorHandler: IErrorHandler; + logger: ILogger; - constructor(httpClient: IHttpClient, errorHandler?: IErrorHandler, logger?: ILogger) { + constructor(httpClient: IHttpClient, errorHandler: IErrorHandler, logger: ILogger) { this.errorHandler = errorHandler; this.logger = logger; this.httpClient = httpClient; - this.hasErrorHandler = Boolean(this.errorHandler); this.onError = this.onError.bind(this); this.processConfig = this.processConfig.bind(this); @@ -51,7 +50,7 @@ class ConfigManager implements IConfigManager { attachEffects() { effect(() => { - this.logger?.setMinLogLevel(state.lifecycle.logLevel.value); + this.logger.setMinLogLevel(state.lifecycle.logLevel.value); }); } @@ -60,8 +59,6 @@ class ConfigManager implements IConfigManager { * config related information in global state */ init() { - this.attachEffects(); - const { logLevel, configUrl, @@ -72,22 +69,38 @@ class ConfigManager implements IConfigManager { integrations, } = state.loadOptions.value; - state.lifecycle.activeDataplaneUrl.value = removeTrailingSlashes( - state.lifecycle.dataPlaneUrl.value, - ) as string; - // determine the path to fetch integration SDK from const intgCdnUrl = getIntegrationsCDNPath( APP_VERSION, lockIntegrationsVersion as boolean, destSDKBaseURL, + this.logger, ); - // determine the path to fetch remote plugins from - const pluginsCDNPath = getPluginsCDNPath( - APP_VERSION, - lockPluginsVersion as boolean, - pluginsSDKBaseURL, - ); + + if (intgCdnUrl === null) { + return; + } + + let pluginsCDNPath: Nullable | undefined; + if (!__BUNDLE_ALL_PLUGINS__) { + // determine the path to fetch remote plugins from + pluginsCDNPath = getPluginsCDNPath( + APP_VERSION, + lockPluginsVersion as boolean, + pluginsSDKBaseURL, + this.logger, + ); + } + + if (pluginsCDNPath === null) { + return; + } + + this.attachEffects(); + + state.lifecycle.activeDataplaneUrl.value = removeTrailingSlashes( + state.lifecycle.dataPlaneUrl.value, + ) as string; updateStorageStateFromLoadOptions(this.logger); updateConsentsStateFromLoadOptions(this.logger); @@ -121,11 +134,7 @@ class ConfigManager implements IConfigManager { * Handle errors */ onError(error: unknown, customMessage?: string) { - if (this.hasErrorHandler) { - this.errorHandler?.onError(error, CONFIG_MANAGER, customMessage); - } else { - throw error; - } + this.errorHandler.onError(error, CONFIG_MANAGER, customMessage); } /** @@ -159,7 +168,7 @@ class ConfigManager implements IConfigManager { // Log error and abort if source is disabled if (res.source.enabled === false) { - this.logger?.error(SOURCE_DISABLED_ERROR); + this.logger.error(SOURCE_DISABLED_ERROR); return; } @@ -201,8 +210,10 @@ class ConfigManager implements IConfigManager { const sourceConfigFunc = state.loadOptions.value.getSourceConfig; if (sourceConfigFunc) { if (!isFunction(sourceConfigFunc)) { - throw new Error(SOURCE_CONFIG_OPTION_ERROR); + this.logger.error(SOURCE_CONFIG_OPTION_ERROR(CONFIG_MANAGER)); + return; } + // Fetch source config from the function const res = sourceConfigFunc(); diff --git a/packages/analytics-js/src/components/configManager/types.ts b/packages/analytics-js/src/components/configManager/types.ts index c097829bf..0c02d205b 100644 --- a/packages/analytics-js/src/components/configManager/types.ts +++ b/packages/analytics-js/src/components/configManager/types.ts @@ -77,8 +77,8 @@ export type SourceConfigResponse = { export interface IConfigManager { httpClient: IHttpClient; - errorHandler?: IErrorHandler; - logger?: ILogger; + errorHandler: IErrorHandler; + logger: ILogger; init: () => void; getConfig: () => void; processConfig: () => void; diff --git a/packages/analytics-js/src/components/configManager/util/cdnPaths.ts b/packages/analytics-js/src/components/configManager/util/cdnPaths.ts index 0cc0070b1..137b696b3 100644 --- a/packages/analytics-js/src/components/configManager/util/cdnPaths.ts +++ b/packages/analytics-js/src/components/configManager/util/cdnPaths.ts @@ -1,5 +1,8 @@ import { CDN_INT_DIR, CDN_PLUGINS_DIR } from '@rudderstack/analytics-js-common/constants/urls'; import { isValidURL } from '@rudderstack/analytics-js-common/utilities/url'; +import type { ILogger } from '@rudderstack/analytics-js-common/types/Logger'; +import type { Nullable } from '@rudderstack/analytics-js-common/types/Nullable'; +import { CONFIG_MANAGER } from '@rudderstack/analytics-js-common/constants/loggerContexts'; import { BUILD_TYPE, CDN_ARCH_VERSION_DIR, @@ -16,13 +19,15 @@ const getSDKComponentBaseURL = ( baseURL: string, currentVersion: string, lockVersion: boolean, - customURL?: string, -) => { + customURL: string | undefined, + logger: ILogger, +): Nullable => { let sdkComponentURL = ''; if (customURL) { if (!isValidURL(customURL)) { - throw new Error(COMPONENT_BASE_URL_ERROR(componentType)); + logger.error(COMPONENT_BASE_URL_ERROR(CONFIG_MANAGER, componentType, customURL)); + return null; } return removeTrailingSlashes(customURL) as string; @@ -46,13 +51,15 @@ const getSDKComponentBaseURL = ( * @param currentVersion * @param lockIntegrationsVersion * @param customIntegrationsCDNPath + * @param logger Logger instance * @returns */ const getIntegrationsCDNPath = ( currentVersion: string, lockIntegrationsVersion: boolean, - customIntegrationsCDNPath?: string, -): string => + customIntegrationsCDNPath: string | undefined, + logger: ILogger, +): Nullable => getSDKComponentBaseURL( 'integrations', CDN_INT_DIR, @@ -60,6 +67,7 @@ const getIntegrationsCDNPath = ( currentVersion, lockIntegrationsVersion, customIntegrationsCDNPath, + logger, ); /** @@ -67,13 +75,15 @@ const getIntegrationsCDNPath = ( * @param currentVersion Current SDK version * @param lockPluginsVersion Flag to lock the plugins version * @param customPluginsCDNPath URL to load the plugins from + * @param logger Logger instance * @returns Final plugins CDN path */ const getPluginsCDNPath = ( currentVersion: string, lockPluginsVersion: boolean, - customPluginsCDNPath?: string, -): string => + customPluginsCDNPath: string | undefined, + logger: ILogger, +): Nullable => getSDKComponentBaseURL( 'plugins', CDN_PLUGINS_DIR, @@ -81,6 +91,7 @@ const getPluginsCDNPath = ( currentVersion, lockPluginsVersion, customPluginsCDNPath, + logger, ); export { getIntegrationsCDNPath, getPluginsCDNPath }; diff --git a/packages/analytics-js/src/components/configManager/util/commonUtil.ts b/packages/analytics-js/src/components/configManager/util/commonUtil.ts index 139663988..bd2c5a641 100644 --- a/packages/analytics-js/src/components/configManager/util/commonUtil.ts +++ b/packages/analytics-js/src/components/configManager/util/commonUtil.ts @@ -81,7 +81,7 @@ const updateReportingState = (res: SourceConfigResponse): void => { state.reporting.isMetricsReportingEnabled.value = isMetricsReportingEnabled(res.source.config); }; -const getServerSideCookiesStateData = (logger?: ILogger) => { +const getServerSideCookiesStateData = (logger: ILogger) => { const { useServerSideCookies, dataServiceEndpoint, @@ -160,7 +160,7 @@ const getServerSideCookiesStateData = (logger?: ILogger) => { }; }; -const updateStorageStateFromLoadOptions = (logger?: ILogger): void => { +const updateStorageStateFromLoadOptions = (logger: ILogger): void => { const { storage: storageOptsFromLoad } = state.loadOptions.value; let storageType = storageOptsFromLoad?.type; if (isDefined(storageType) && !isValidStorageType(storageType)) { @@ -222,7 +222,7 @@ const updateStorageStateFromLoadOptions = (logger?: ILogger): void => { }); }; -const updateConsentsStateFromLoadOptions = (logger?: ILogger): void => { +const updateConsentsStateFromLoadOptions = (logger: ILogger): void => { const { provider, consentManagerPluginName, initialized, enabled, consentsData } = getConsentManagementData(state.loadOptions.value.consentManagement, logger); @@ -316,7 +316,7 @@ const updateConsentsState = (resp: SourceConfigResponse): void => { }); }; -const updateDataPlaneEventsStateFromLoadOptions = (logger?: ILogger) => { +const updateDataPlaneEventsStateFromLoadOptions = (logger: ILogger) => { if (state.dataPlaneEvents.deliveryEnabled.value) { const defaultEventsQueuePluginName: PluginName = 'XhrQueue'; let eventsQueuePluginName: PluginName = defaultEventsQueuePluginName; @@ -342,7 +342,7 @@ const getSourceConfigURL = ( writeKey: string, lockIntegrationsVersion: boolean, lockPluginsVersion: boolean, - logger?: ILogger, + logger: ILogger, ): string => { const defSearchParams = new URLSearchParams({ p: MODULE_TYPE, diff --git a/packages/analytics-js/src/components/core/Analytics.ts b/packages/analytics-js/src/components/core/Analytics.ts index b695518db..f58de6669 100644 --- a/packages/analytics-js/src/components/core/Analytics.ts +++ b/packages/analytics-js/src/components/core/Analytics.ts @@ -242,11 +242,11 @@ class Analytics implements IAnalytics { this.storeManager = new StoreManager(this.pluginsManager, this.errorHandler, this.logger); this.configManager = new ConfigManager(this.httpClient, this.errorHandler, this.logger); this.userSessionManager = new UserSessionManager( - this.errorHandler, - this.logger, this.pluginsManager, this.storeManager, this.httpClient, + this.errorHandler, + this.logger, ); this.eventRepository = new EventRepository( this.pluginsManager, diff --git a/packages/analytics-js/src/components/eventManager/EventManager.ts b/packages/analytics-js/src/components/eventManager/EventManager.ts index d30f546f7..b3c7fcc2b 100644 --- a/packages/analytics-js/src/components/eventManager/EventManager.ts +++ b/packages/analytics-js/src/components/eventManager/EventManager.ts @@ -14,8 +14,8 @@ import type { IUserSessionManager } from '../userSessionManager/types'; class EventManager implements IEventManager { eventRepository: IEventRepository; userSessionManager: IUserSessionManager; - errorHandler?: IErrorHandler; - logger?: ILogger; + errorHandler: IErrorHandler; + logger: ILogger; eventFactory: RudderEventFactory; /** @@ -28,8 +28,8 @@ class EventManager implements IEventManager { constructor( eventRepository: IEventRepository, userSessionManager: IUserSessionManager, - errorHandler?: IErrorHandler, - logger?: ILogger, + errorHandler: IErrorHandler, + logger: ILogger, ) { this.eventRepository = eventRepository; this.userSessionManager = userSessionManager; @@ -69,11 +69,7 @@ class EventManager implements IEventManager { * @param error The error object */ onError(error: unknown, customMessage?: string): void { - if (this.errorHandler) { - this.errorHandler.onError(error, EVENT_MANAGER, customMessage); - } else { - throw error; - } + this.errorHandler.onError(error, EVENT_MANAGER, customMessage); } } diff --git a/packages/analytics-js/src/components/eventManager/RudderEventFactory.ts b/packages/analytics-js/src/components/eventManager/RudderEventFactory.ts index 48843568f..b2ab84066 100644 --- a/packages/analytics-js/src/components/eventManager/RudderEventFactory.ts +++ b/packages/analytics-js/src/components/eventManager/RudderEventFactory.ts @@ -6,9 +6,9 @@ import type { RudderContext, RudderEvent } from '@rudderstack/analytics-js-commo import { getEnrichedEvent, getUpdatedPageProperties } from './utilities'; class RudderEventFactory { - logger?: ILogger; + logger: ILogger; - constructor(logger?: ILogger) { + constructor(logger: ILogger) { this.logger = logger; } diff --git a/packages/analytics-js/src/components/eventManager/utilities.ts b/packages/analytics-js/src/components/eventManager/utilities.ts index 3ea0e9140..93595ac37 100644 --- a/packages/analytics-js/src/components/eventManager/utilities.ts +++ b/packages/analytics-js/src/components/eventManager/utilities.ts @@ -97,7 +97,7 @@ const getUpdatedPageProperties = ( const checkForReservedElementsInObject = ( obj: Nullable | RudderContext | undefined, parentKeyPath: string, - logger?: ILogger, + logger: ILogger, ): void => { if (isObjectLiteralAndNotNull(obj)) { Object.keys(obj as object).forEach(property => { @@ -105,7 +105,7 @@ const checkForReservedElementsInObject = ( RESERVED_ELEMENTS.includes(property) || RESERVED_ELEMENTS.includes(property.toLowerCase()) ) { - logger?.warn( + logger.warn( RESERVED_KEYWORD_WARNING(EVENT_MANAGER, property, parentKeyPath, RESERVED_ELEMENTS), ); } @@ -118,7 +118,7 @@ const checkForReservedElementsInObject = ( * @param rudderEvent Generated rudder event * @param logger Logger instance */ -const checkForReservedElements = (rudderEvent: RudderEvent, logger?: ILogger): void => { +const checkForReservedElements = (rudderEvent: RudderEvent, logger: ILogger): void => { // properties, traits, contextualTraits are either undefined or object const { properties, traits, context } = rudderEvent; const { traits: contextualTraits } = context; @@ -159,7 +159,7 @@ const updateTopLevelEventElements = (rudderEvent: RudderEvent, options: ApiOptio const getMergedContext = ( rudderContext: RudderContext, options: ApiOptions, - logger?: ILogger, + logger: ILogger, ): RudderContext => { let context = rudderContext; Object.keys(options).forEach(key => { @@ -179,7 +179,7 @@ const getMergedContext = ( ...tempContext, }); } else { - logger?.warn(INVALID_CONTEXT_OBJECT_WARNING(EVENT_MANAGER)); + logger.warn(INVALID_CONTEXT_OBJECT_WARNING(EVENT_MANAGER)); } } }); @@ -191,12 +191,16 @@ const getMergedContext = ( * @param rudderEvent Generated rudder event * @param options API options */ -const processOptions = (rudderEvent: RudderEvent, options?: Nullable): void => { +const processOptions = ( + rudderEvent: RudderEvent, + options: Nullable | undefined, + logger: ILogger, +): void => { // Only allow object type for options if (isObjectLiteralAndNotNull(options)) { updateTopLevelEventElements(rudderEvent, options as ApiOptions); // eslint-disable-next-line no-param-reassign - rudderEvent.context = getMergedContext(rudderEvent.context, options as ApiOptions); + rudderEvent.context = getMergedContext(rudderEvent.context, options as ApiOptions, logger); } }; @@ -230,9 +234,9 @@ const getEventIntegrationsConfig = (integrationsConfig?: IntegrationOpts) => { */ const getEnrichedEvent = ( rudderEvent: Partial, - options?: Nullable, - pageProps?: ApiObject, - logger?: ILogger, + options: Nullable | undefined, + pageProps: ApiObject | undefined, + logger: ILogger, ): RudderEvent => { const commonEventData = { channel: CHANNEL, @@ -321,7 +325,7 @@ const getEnrichedEvent = ( processedEvent.properties = null; } - processOptions(processedEvent, options); + processOptions(processedEvent, options, logger); checkForReservedElements(processedEvent, logger); // Update the integrations config for the event diff --git a/packages/analytics-js/src/components/eventRepository/EventRepository.ts b/packages/analytics-js/src/components/eventRepository/EventRepository.ts index 9ad9a6f8f..c01dfa3ca 100644 --- a/packages/analytics-js/src/components/eventRepository/EventRepository.ts +++ b/packages/analytics-js/src/components/eventRepository/EventRepository.ts @@ -31,8 +31,8 @@ import { getFinalEvent, shouldBufferEventsForPreConsent } from './utils'; * Event repository class responsible for queuing events for further processing and delivery */ class EventRepository implements IEventRepository { - errorHandler?: IErrorHandler; - logger?: ILogger; + errorHandler: IErrorHandler; + logger: ILogger; pluginsManager: IPluginsManager; httpClient: IHttpClient; storeManager: IStoreManager; @@ -51,8 +51,8 @@ class EventRepository implements IEventRepository { pluginsManager: IPluginsManager, storeManager: IStoreManager, httpClient: IHttpClient, - errorHandler?: IErrorHandler, - logger?: ILogger, + errorHandler: IErrorHandler, + logger: ILogger, ) { this.pluginsManager = pluginsManager; this.errorHandler = errorHandler; @@ -213,11 +213,7 @@ class EventRepository implements IEventRepository { * @param customMessage a message */ onError(error: unknown, customMessage?: string): void { - if (this.errorHandler) { - this.errorHandler.onError(error, EVENT_REPOSITORY, customMessage); - } else { - throw error; - } + this.errorHandler.onError(error, EVENT_REPOSITORY, customMessage); } } diff --git a/packages/analytics-js/src/components/pluginsManager/PluginsManager.ts b/packages/analytics-js/src/components/pluginsManager/PluginsManager.ts index 2252773b6..100c3ca94 100644 --- a/packages/analytics-js/src/components/pluginsManager/PluginsManager.ts +++ b/packages/analytics-js/src/components/pluginsManager/PluginsManager.ts @@ -34,10 +34,10 @@ import type { PluginsGroup } from './types'; // TODO: add timeout error mechanism for marking remote plugins that failed to load as failed in state class PluginsManager implements IPluginsManager { engine: IPluginEngine; - errorHandler?: IErrorHandler; - logger?: ILogger; + errorHandler: IErrorHandler; + logger: ILogger; - constructor(engine: IPluginEngine, errorHandler?: IErrorHandler, logger?: ILogger) { + constructor(engine: IPluginEngine, errorHandler: IErrorHandler, logger: ILogger) { this.engine = engine; this.errorHandler = errorHandler; @@ -339,11 +339,7 @@ class PluginsManager implements IPluginsManager { * Handle errors */ onError(error: unknown, customMessage?: string): void { - if (this.errorHandler) { - this.errorHandler.onError(error, PLUGINS_MANAGER, customMessage); - } else { - throw error; - } + this.errorHandler.onError(error, PLUGINS_MANAGER, customMessage); } } diff --git a/packages/analytics-js/src/components/userSessionManager/UserSessionManager.ts b/packages/analytics-js/src/components/userSessionManager/UserSessionManager.ts index 0b774725f..8da57f2c8 100644 --- a/packages/analytics-js/src/components/userSessionManager/UserSessionManager.ts +++ b/packages/analytics-js/src/components/userSessionManager/UserSessionManager.ts @@ -73,19 +73,19 @@ import type { import { isPositiveInteger } from '../utilities/number'; class UserSessionManager implements IUserSessionManager { - storeManager?: IStoreManager; - pluginsManager?: IPluginsManager; - errorHandler?: IErrorHandler; - httpClient?: IHttpClient; - logger?: ILogger; + storeManager: IStoreManager; + pluginsManager: IPluginsManager; + errorHandler: IErrorHandler; + httpClient: IHttpClient; + logger: ILogger; serverSideCookieDebounceFuncs: Record; constructor( - errorHandler?: IErrorHandler, - logger?: ILogger, - pluginsManager?: IPluginsManager, - storeManager?: IStoreManager, - httpClient?: IHttpClient, + pluginsManager: IPluginsManager, + storeManager: IStoreManager, + httpClient: IHttpClient, + errorHandler: IErrorHandler, + logger: ILogger, ) { this.storeManager = storeManager; this.pluginsManager = pluginsManager; @@ -257,7 +257,7 @@ class UserSessionManager implements IUserSessionManager { let timeout: number; const configuredSessionTimeout = state.loadOptions.value.sessions?.timeout; if (!isPositiveInteger(configuredSessionTimeout)) { - this.logger?.warn( + this.logger.warn( TIMEOUT_NOT_NUMBER_WARNING( USER_SESSION_MANAGER, configuredSessionTimeout, @@ -270,13 +270,13 @@ class UserSessionManager implements IUserSessionManager { } if (timeout === 0) { - this.logger?.warn(TIMEOUT_ZERO_WARNING(USER_SESSION_MANAGER)); + this.logger.warn(TIMEOUT_ZERO_WARNING(USER_SESSION_MANAGER)); autoTrack = false; } // In case user provides a timeout value greater than 0 but less than 10 seconds SDK will show a warning // and will proceed with it if (timeout > 0 && timeout < MIN_SESSION_TIMEOUT_MS) { - this.logger?.warn( + this.logger.warn( TIMEOUT_NOT_RECOMMENDED_WARNING(USER_SESSION_MANAGER, timeout, MIN_SESSION_TIMEOUT_MS), ); } @@ -288,11 +288,7 @@ class UserSessionManager implements IUserSessionManager { * @param error The error object */ onError(error: unknown, customMessage?: string): void { - if (this.errorHandler) { - this.errorHandler.onError(error, USER_SESSION_MANAGER, customMessage); - } else { - throw error; - } + this.errorHandler.onError(error, USER_SESSION_MANAGER, customMessage); } /** @@ -371,14 +367,14 @@ class UserSessionManager implements IUserSessionManager { const before = stringifyWithoutCircular(cData.value, false, []); const after = stringifyWithoutCircular(cookieValue, false, []); if (after !== before) { - this.logger?.error(FAILED_SETTING_COOKIE_FROM_SERVER_ERROR(cData.name)); + this.logger.error(FAILED_SETTING_COOKIE_FROM_SERVER_ERROR(cData.name)); if (cb) { cb(cData.name, cData.value); } } }); } else { - this.logger?.error(DATA_SERVER_REQUEST_FAIL_ERROR(details?.xhr?.status)); + this.logger.error(DATA_SERVER_REQUEST_FAIL_ERROR(details?.xhr?.status)); cookiesData.forEach(each => { if (cb) { cb(each.name, each.value); diff --git a/packages/analytics-js/src/components/userSessionManager/utils.ts b/packages/analytics-js/src/components/userSessionManager/utils.ts index 81b492365..1188a1ffa 100644 --- a/packages/analytics-js/src/components/userSessionManager/utils.ts +++ b/packages/analytics-js/src/components/userSessionManager/utils.ts @@ -36,15 +36,13 @@ const generateSessionId = (): number => Date.now(); * @param logger logger * @returns */ -const isManualSessionIdValid = (sessionId?: number, logger?: ILogger): boolean => { +const isManualSessionIdValid = (sessionId: number | undefined, logger: ILogger): boolean => { if ( !sessionId || !isPositiveInteger(sessionId) || !hasMinLength(MIN_SESSION_ID_LENGTH, sessionId) ) { - logger?.warn( - INVALID_SESSION_ID_WARNING(USER_SESSION_MANAGER, sessionId, MIN_SESSION_ID_LENGTH), - ); + logger.warn(INVALID_SESSION_ID_WARNING(USER_SESSION_MANAGER, sessionId, MIN_SESSION_ID_LENGTH)); return false; } return true; @@ -73,7 +71,7 @@ const generateAutoTrackingSession = (sessionTimeout?: number): SessionInfo => { * @param logger Logger module * @returns SessionInfo */ -const generateManualTrackingSession = (id?: number, logger?: ILogger): SessionInfo => { +const generateManualTrackingSession = (id: number | undefined, logger: ILogger): SessionInfo => { const sessionId: number = isManualSessionIdValid(id, logger) ? (id as number) : generateSessionId(); diff --git a/packages/analytics-js/src/components/utilities/consent.ts b/packages/analytics-js/src/components/utilities/consent.ts index db276d2af..a17568194 100644 --- a/packages/analytics-js/src/components/utilities/consent.ts +++ b/packages/analytics-js/src/components/utilities/consent.ts @@ -92,12 +92,12 @@ const isValidConsentsData = (value: Consents | undefined): value is Consents => */ const getConsentManagerInfo = ( consentManagementOpts: ConsentManagementOptions, - logger?: ILogger, + logger: ILogger, ) => { let { provider }: { provider?: ConsentManagementProvider } = consentManagementOpts; const consentManagerPluginName = provider ? ConsentManagersToPluginNameMap[provider] : undefined; if (provider && !consentManagerPluginName) { - logger?.error( + logger.error( UNSUPPORTED_CONSENT_MANAGER_ERROR(CONFIG_MANAGER, provider, ConsentManagersToPluginNameMap), ); @@ -115,7 +115,7 @@ const getConsentManagerInfo = ( */ const getConsentManagementData = ( consentManagementOpts: ConsentManagementOptions | undefined, - logger?: ILogger, + logger: ILogger, ) => { let consentManagerPluginName: PluginName | undefined; let allowedConsentIds: Consents = []; diff --git a/packages/analytics-js/src/constants/logMessages.ts b/packages/analytics-js/src/constants/logMessages.ts index 5509a4ccd..5d773b604 100644 --- a/packages/analytics-js/src/constants/logMessages.ts +++ b/packages/analytics-js/src/constants/logMessages.ts @@ -11,7 +11,6 @@ import type { import type { Nullable } from '@rudderstack/analytics-js-common/types/Nullable'; // CONSTANT -const SOURCE_CONFIG_OPTION_ERROR = `"getSourceConfig" must be a function. Please make sure that it is defined and returns a valid source configuration object.`; const DATA_PLANE_URL_ERROR = `Failed to load the SDK as the data plane URL could not be determined. Please check that the data plane URL is set correctly and try again.`; const SOURCE_CONFIG_RESOLUTION_ERROR = `Unable to process/parse source configuration response.`; const SOURCE_DISABLED_ERROR = `The source is disabled. Please enable the source in the dashboard to send events.`; @@ -20,8 +19,11 @@ const EVENT_OBJECT_GENERATION_ERROR = `Failed to generate the event object.`; const PLUGIN_EXT_POINT_MISSING_ERROR = `Failed to invoke plugin because the extension point name is missing.`; const PLUGIN_EXT_POINT_INVALID_ERROR = `Failed to invoke plugin because the extension point name is invalid.`; -const COMPONENT_BASE_URL_ERROR = (component: string): string => - `Failed to load the SDK as the base URL for ${component} is not valid.`; +const SOURCE_CONFIG_OPTION_ERROR = (context: string): string => + `${context}${LOG_CONTEXT_SEPARATOR}The "getSourceConfig" load API option must be a function return a valid source configuration object.`; + +const COMPONENT_BASE_URL_ERROR = (context: string, component: string, url?: string): string => + `${context}${LOG_CONTEXT_SEPARATOR}The base URL "${url}" for ${component} is not valid.`; // ERROR const UNSUPPORTED_CONSENT_MANAGER_ERROR = ( diff --git a/packages/analytics-js/src/services/ErrorHandler/ErrorHandler.ts b/packages/analytics-js/src/services/ErrorHandler/ErrorHandler.ts index e7ad84785..68c53680d 100644 --- a/packages/analytics-js/src/services/ErrorHandler/ErrorHandler.ts +++ b/packages/analytics-js/src/services/ErrorHandler/ErrorHandler.ts @@ -32,10 +32,10 @@ import { defaultHttpClient } from '../HttpClient'; */ class ErrorHandler implements IErrorHandler { httpClient: IHttpClient; - logger?: ILogger; + logger: ILogger; // If no logger is passed errors will be thrown as unhandled error - constructor(httpClient: IHttpClient, logger?: ILogger) { + constructor(httpClient: IHttpClient, logger: ILogger) { this.httpClient = httpClient; this.logger = logger; this.attachErrorListeners(); diff --git a/packages/analytics-js/src/services/ErrorHandler/event/event.ts b/packages/analytics-js/src/services/ErrorHandler/event/event.ts index 9232a51c6..382bcbf67 100644 --- a/packages/analytics-js/src/services/ErrorHandler/event/event.ts +++ b/packages/analytics-js/src/services/ErrorHandler/event/event.ts @@ -60,13 +60,13 @@ function createException( }; } -const normalizeError = (maybeError: any, logger?: ILogger): any | undefined => { +const normalizeError = (maybeError: any, logger: ILogger): any => { let error; if (isTypeOfError(maybeError) && !!getStacktrace(maybeError)) { error = maybeError; } else { - logger?.warn(NON_ERROR_WARNING(ERROR_HANDLER, stringifyWithoutCircular(maybeError))); + logger.warn(NON_ERROR_WARNING(ERROR_HANDLER, stringifyWithoutCircular(maybeError))); error = undefined; } diff --git a/packages/analytics-js/src/services/HttpClient/HttpClient.ts b/packages/analytics-js/src/services/HttpClient/HttpClient.ts index 45fd65045..08ff9760a 100644 --- a/packages/analytics-js/src/services/HttpClient/HttpClient.ts +++ b/packages/analytics-js/src/services/HttpClient/HttpClient.ts @@ -20,18 +20,16 @@ import { createXhrRequestOptions, xhrRequest } from './xhr/xhrRequestHandler'; */ class HttpClient implements IHttpClient { errorHandler?: IErrorHandler; - logger?: ILogger; + logger: ILogger; basicAuthHeader?: string; - hasErrorHandler = false; - constructor(logger?: ILogger) { + constructor(logger: ILogger) { this.logger = logger; this.onError = this.onError.bind(this); } init(errorHandler: IErrorHandler) { this.errorHandler = errorHandler; - this.hasErrorHandler = true; } /** @@ -86,11 +84,7 @@ class HttpClient implements IHttpClient { * Handle errors */ onError(error: unknown) { - if (this.hasErrorHandler) { - this.errorHandler?.onError(error, HTTP_CLIENT); - } else { - throw error; - } + this.errorHandler?.onError(error, HTTP_CLIENT); } /** diff --git a/packages/analytics-js/src/services/HttpClient/xhr/xhrResponseHandler.ts b/packages/analytics-js/src/services/HttpClient/xhr/xhrResponseHandler.ts index 5d3d2cd8d..3c6a22d31 100644 --- a/packages/analytics-js/src/services/HttpClient/xhr/xhrResponseHandler.ts +++ b/packages/analytics-js/src/services/HttpClient/xhr/xhrResponseHandler.ts @@ -1,22 +1,17 @@ -import { isFunction } from '@rudderstack/analytics-js-common/utilities/checks'; import { getMutatedError } from '@rudderstack/analytics-js-common/utilities/errors'; /** * Utility to parse XHR JSON response */ const responseTextToJson = ( - responseText?: string, - onError?: (message: Error | unknown) => void, + responseText: string, + onError: (message: unknown) => void, ): T | undefined => { try { return JSON.parse(responseText || ''); } catch (err) { const error = getMutatedError(err, 'Failed to parse response data'); - if (isFunction(onError)) { - onError(error); - } else { - throw error; - } + onError(error); } return undefined; diff --git a/packages/analytics-js/src/services/PluginEngine/PluginEngine.ts b/packages/analytics-js/src/services/PluginEngine/PluginEngine.ts index 0225d0250..9b0c3a696 100644 --- a/packages/analytics-js/src/services/PluginEngine/PluginEngine.ts +++ b/packages/analytics-js/src/services/PluginEngine/PluginEngine.ts @@ -29,9 +29,9 @@ class PluginEngine implements IPluginEngine { byName: Record = {}; cache: Record = {}; config: PluginEngineConfig = { throws: true }; - logger?: ILogger; + logger: ILogger; - constructor(options: PluginEngineConfig = {}, logger?: ILogger) { + constructor(logger: ILogger, options: PluginEngineConfig = {}) { this.config = { throws: true, ...options, @@ -46,7 +46,7 @@ class PluginEngine implements IPluginEngine { if (this.config.throws) { throw new Error(errorMessage); } else { - this.logger?.error(errorMessage, plugin); + this.logger.error(errorMessage, plugin); } } @@ -55,7 +55,7 @@ class PluginEngine implements IPluginEngine { if (this.config.throws) { throw new Error(errorMessage); } else { - this.logger?.error(errorMessage); + this.logger.error(errorMessage); } } @@ -86,7 +86,7 @@ class PluginEngine implements IPluginEngine { if (this.config.throws) { throw new Error(errorMessage); } else { - this.logger?.error(errorMessage); + this.logger.error(errorMessage); } } @@ -97,7 +97,7 @@ class PluginEngine implements IPluginEngine { if (this.config.throws) { throw new Error(errorMessage); } else { - this.logger?.error(errorMessage); + this.logger.error(errorMessage); } } @@ -119,7 +119,7 @@ class PluginEngine implements IPluginEngine { if (plugin.deps?.some(dependency => !this.byName[dependency])) { // If deps not exist, then not load it. const notExistDeps = plugin.deps.filter(dependency => !this.byName[dependency]); - this.logger?.error(PLUGIN_DEPS_ERROR(PLUGIN_ENGINE, plugin.name, notExistDeps)); + this.logger.error(PLUGIN_DEPS_ERROR(PLUGIN_ENGINE, plugin.name, notExistDeps)); return false; } return lifeCycleName === '.' ? true : hasValueByPath(plugin, lifeCycleName); @@ -175,7 +175,7 @@ class PluginEngine implements IPluginEngine { if (throws) { throw err; } else { - this.logger?.error( + this.logger.error( PLUGIN_INVOCATION_ERROR(PLUGIN_ENGINE, extensionPointName, plugin.name), err, ); @@ -195,6 +195,6 @@ class PluginEngine implements IPluginEngine { } } -const defaultPluginEngine = new PluginEngine({ throws: true }, defaultLogger); +const defaultPluginEngine = new PluginEngine(defaultLogger, { throws: true }); export { PluginEngine, defaultPluginEngine }; diff --git a/packages/analytics-js/src/services/StoreManager/Store.ts b/packages/analytics-js/src/services/StoreManager/Store.ts index 744ab3e6a..68160615a 100644 --- a/packages/analytics-js/src/services/StoreManager/Store.ts +++ b/packages/analytics-js/src/services/StoreManager/Store.ts @@ -8,8 +8,6 @@ import type { IPluginsManager } from '@rudderstack/analytics-js-common/types/Plu import type { Nullable } from '@rudderstack/analytics-js-common/types/Nullable'; import { LOCAL_STORAGE, MEMORY_STORAGE } from '@rudderstack/analytics-js-common/constants/storages'; import { getMutatedError } from '@rudderstack/analytics-js-common/utilities/errors'; -import { defaultLogger } from '../Logger'; -import { defaultErrorHandler } from '../ErrorHandler'; import { isStorageQuotaExceeded } from '../../components/capabilitiesManager/detection'; import { BAD_COOKIES_WARNING, @@ -31,12 +29,11 @@ class Store implements IStore { originalEngine: IStorage; noKeyValidation?: boolean; noCompoundKey?: boolean; - errorHandler?: IErrorHandler; - hasErrorHandler = false; - logger?: ILogger; - pluginsManager?: IPluginsManager; + errorHandler: IErrorHandler; + logger: ILogger; + pluginsManager: IPluginsManager; - constructor(config: IStoreConfig, engine?: IStorage, pluginsManager?: IPluginsManager) { + constructor(config: IStoreConfig, engine: IStorage, pluginsManager: IPluginsManager) { this.id = config.id; this.name = config.name; this.isEncrypted = config.isEncrypted ?? false; @@ -45,9 +42,8 @@ class Store implements IStore { this.noKeyValidation = Object.keys(this.validKeys).length === 0; this.noCompoundKey = config.noCompoundKey; this.originalEngine = this.engine; - this.errorHandler = config.errorHandler ?? defaultErrorHandler; - this.hasErrorHandler = Boolean(this.errorHandler); - this.logger = config.logger ?? defaultLogger; + this.errorHandler = config.errorHandler; + this.logger = config.logger; this.pluginsManager = pluginsManager; } @@ -113,7 +109,7 @@ class Store implements IStore { ); } catch (err) { if (isStorageQuotaExceeded(err)) { - this.logger?.warn(STORAGE_QUOTA_EXCEEDED_WARNING(`Store ${this.id}`)); + this.logger.warn(STORAGE_QUOTA_EXCEEDED_WARNING(`Store ${this.id}`)); // switch to inMemory engine this.swapQueueStoreToInMemoryEngine(); // and save it there @@ -149,7 +145,7 @@ class Store implements IStore { // A hack for warning the users of potential partial SDK version migrations if (isString(decryptedValue) && decryptedValue.startsWith('RudderEncrypt:')) { - this.logger?.warn(BAD_COOKIES_WARNING(key)); + this.logger.warn(BAD_COOKIES_WARNING(key)); } return null; @@ -215,11 +211,7 @@ class Store implements IStore { * Handle errors */ onError(error: unknown) { - if (this.hasErrorHandler) { - this.errorHandler?.onError(error, `Store ${this.id}`); - } else { - throw error; - } + this.errorHandler?.onError(error, `Store ${this.id}`); } } diff --git a/packages/analytics-js/src/services/StoreManager/StoreManager.ts b/packages/analytics-js/src/services/StoreManager/StoreManager.ts index 26d8ee108..5a1dd5417 100644 --- a/packages/analytics-js/src/services/StoreManager/StoreManager.ts +++ b/packages/analytics-js/src/services/StoreManager/StoreManager.ts @@ -40,15 +40,13 @@ import { getStorageTypeFromPreConsentIfApplicable } from './utils'; class StoreManager implements IStoreManager { stores: Record = {}; isInitialized = false; - errorHandler?: IErrorHandler; - logger?: ILogger; - pluginsManager?: IPluginsManager; - hasErrorHandler = false; + errorHandler: IErrorHandler; + logger: ILogger; + pluginsManager: IPluginsManager; - constructor(pluginsManager?: IPluginsManager, errorHandler?: IErrorHandler, logger?: ILogger) { + constructor(pluginsManager: IPluginsManager, errorHandler: IErrorHandler, logger: ILogger) { this.errorHandler = errorHandler; this.logger = logger; - this.hasErrorHandler = Boolean(this.errorHandler); this.pluginsManager = pluginsManager; this.onError = this.onError.bind(this); } @@ -109,6 +107,8 @@ class StoreManager implements IStoreManager { isEncrypted: true, noCompoundKey: true, type: storageType, + errorHandler: this.errorHandler, + logger: this.logger, }); } }); @@ -220,11 +220,7 @@ class StoreManager implements IStoreManager { * Handle errors */ onError(error: unknown) { - if (this.hasErrorHandler) { - this.errorHandler?.onError(error, STORE_MANAGER); - } else { - throw error; - } + this.errorHandler?.onError(error, STORE_MANAGER); } } From 733387fdc2901b3cc48a8a82ac977dc0c025f347 Mon Sep 17 00:00:00 2001 From: Sai Kumar Battinoju Date: Fri, 3 Jan 2025 13:04:11 +0530 Subject: [PATCH 20/53] refactor: address ai bot review comments --- .../eventManager/EventManager.test.ts | 2 -- .../CapabilitiesManager.ts | 2 +- .../configManager/util/commonUtil.ts | 18 +++++++------- .../src/components/core/Analytics.ts | 2 +- .../pluginsManager/PluginsManager.ts | 24 ++++++++++--------- .../src/services/ErrorHandler/ErrorHandler.ts | 4 ++-- .../src/services/StoreManager/Store.ts | 2 +- .../src/services/StoreManager/StoreManager.ts | 2 +- 8 files changed, 27 insertions(+), 29 deletions(-) diff --git a/packages/analytics-js/__tests__/components/eventManager/EventManager.test.ts b/packages/analytics-js/__tests__/components/eventManager/EventManager.test.ts index b363ec7f3..826b4ab8c 100644 --- a/packages/analytics-js/__tests__/components/eventManager/EventManager.test.ts +++ b/packages/analytics-js/__tests__/components/eventManager/EventManager.test.ts @@ -14,8 +14,6 @@ describe('EventManager', () => { class MockErrorHandler implements IErrorHandler { onError = jest.fn(); leaveBreadcrumb = jest.fn(); - notifyError = jest.fn(); - init = jest.fn(); httpClient: IHttpClient = defaultHttpClient; logger: ILogger = defaultLogger; } diff --git a/packages/analytics-js/src/components/capabilitiesManager/CapabilitiesManager.ts b/packages/analytics-js/src/components/capabilitiesManager/CapabilitiesManager.ts index 8c6da6c46..386d5654a 100644 --- a/packages/analytics-js/src/components/capabilitiesManager/CapabilitiesManager.ts +++ b/packages/analytics-js/src/components/capabilitiesManager/CapabilitiesManager.ts @@ -125,7 +125,7 @@ class CapabilitiesManager implements ICapabilitiesManager { if (isValidURL(customPolyfillUrl)) { polyfillUrl = customPolyfillUrl; } else { - this.logger?.warn(INVALID_POLYFILL_URL_WARNING(CAPABILITIES_MANAGER, customPolyfillUrl)); + this.logger.warn(INVALID_POLYFILL_URL_WARNING(CAPABILITIES_MANAGER, customPolyfillUrl)); } } diff --git a/packages/analytics-js/src/components/configManager/util/commonUtil.ts b/packages/analytics-js/src/components/configManager/util/commonUtil.ts index bd2c5a641..a5ff9a2ca 100644 --- a/packages/analytics-js/src/components/configManager/util/commonUtil.ts +++ b/packages/analytics-js/src/components/configManager/util/commonUtil.ts @@ -140,7 +140,7 @@ const getServerSideCookiesStateData = (logger: ILogger) => { dataServiceHost !== removeLeadingPeriod(providedCookieDomain as string) ) { sscEnabled = false; - logger?.warn( + logger.warn( SERVER_SIDE_COOKIE_FEATURE_OVERRIDE_WARNING( CONFIG_MANAGER, providedCookieDomain, @@ -164,9 +164,7 @@ const updateStorageStateFromLoadOptions = (logger: ILogger): void => { const { storage: storageOptsFromLoad } = state.loadOptions.value; let storageType = storageOptsFromLoad?.type; if (isDefined(storageType) && !isValidStorageType(storageType)) { - logger?.warn( - STORAGE_TYPE_VALIDATION_WARNING(CONFIG_MANAGER, storageType, DEFAULT_STORAGE_TYPE), - ); + logger.warn(STORAGE_TYPE_VALIDATION_WARNING(CONFIG_MANAGER, storageType, DEFAULT_STORAGE_TYPE)); storageType = DEFAULT_STORAGE_TYPE; } @@ -176,7 +174,7 @@ const updateStorageStateFromLoadOptions = (logger: ILogger): void => { if (!isUndefined(storageEncryptionVersion) && isUndefined(encryptionPluginName)) { // set the default encryption plugin - logger?.warn( + logger.warn( UNSUPPORTED_STORAGE_ENCRYPTION_VERSION_WARNING( CONFIG_MANAGER, storageEncryptionVersion, @@ -196,7 +194,7 @@ const updateStorageStateFromLoadOptions = (logger: ILogger): void => { storageEncryptionVersion === DEFAULT_STORAGE_ENCRYPTION_VERSION; if (configuredMigrationValue === true && finalMigrationVal !== configuredMigrationValue) { - logger?.warn( + logger.warn( STORAGE_DATA_MIGRATION_OVERRIDE_WARNING( CONFIG_MANAGER, storageEncryptionVersion, @@ -235,7 +233,7 @@ const updateConsentsStateFromLoadOptions = (logger: ILogger): void => { if (isDefined(storageStrategy) && !StorageStrategies.includes(storageStrategy)) { storageStrategy = DEFAULT_PRE_CONSENT_STORAGE_STRATEGY; - logger?.warn( + logger.warn( UNSUPPORTED_PRE_CONSENT_STORAGE_STRATEGY( CONFIG_MANAGER, preConsentOpts?.storage?.strategy, @@ -250,7 +248,7 @@ const updateConsentsStateFromLoadOptions = (logger: ILogger): void => { if (isDefined(eventsDeliveryType) && !deliveryTypes.includes(eventsDeliveryType)) { eventsDeliveryType = DEFAULT_PRE_CONSENT_EVENTS_DELIVERY_TYPE; - logger?.warn( + logger.warn( UNSUPPORTED_PRE_CONSENT_EVENTS_DELIVERY_TYPE( CONFIG_MANAGER, preConsentOpts?.events?.delivery, @@ -327,7 +325,7 @@ const updateDataPlaneEventsStateFromLoadOptions = (logger: ILogger) => { } else { eventsQueuePluginName = defaultEventsQueuePluginName; - logger?.warn(UNSUPPORTED_BEACON_API_WARNING(CONFIG_MANAGER)); + logger.warn(UNSUPPORTED_BEACON_API_WARNING(CONFIG_MANAGER)); } } @@ -377,7 +375,7 @@ const getSourceConfigURL = ( searchParams = configUrlInstance.searchParams; hash = configUrlInstance.hash; } else { - logger?.warn(INVALID_CONFIG_URL_WARNING(CONFIG_MANAGER, configUrl)); + logger.warn(INVALID_CONFIG_URL_WARNING(CONFIG_MANAGER, configUrl)); } return `${origin}${pathname}?${searchParams}${hash}`; diff --git a/packages/analytics-js/src/components/core/Analytics.ts b/packages/analytics-js/src/components/core/Analytics.ts index f58de6669..609dd445f 100644 --- a/packages/analytics-js/src/components/core/Analytics.ts +++ b/packages/analytics-js/src/components/core/Analytics.ts @@ -133,7 +133,7 @@ class Analytics implements IAnalytics { }); // set log level as early as possible - this.logger?.setMinLogLevel(state.loadOptions.value.logLevel ?? POST_LOAD_LOG_LEVEL); + this.logger.setMinLogLevel(state.loadOptions.value.logLevel ?? POST_LOAD_LOG_LEVEL); // Expose state to global objects setExposedGlobal('state', state, writeKey); diff --git a/packages/analytics-js/src/components/pluginsManager/PluginsManager.ts b/packages/analytics-js/src/components/pluginsManager/PluginsManager.ts index 100c3ca94..524fb3f25 100644 --- a/packages/analytics-js/src/components/pluginsManager/PluginsManager.ts +++ b/packages/analytics-js/src/components/pluginsManager/PluginsManager.ts @@ -13,7 +13,10 @@ import type { ILogger } from '@rudderstack/analytics-js-common/types/Logger'; import type { Nullable } from '@rudderstack/analytics-js-common/types/Nullable'; import { PLUGINS_MANAGER } from '@rudderstack/analytics-js-common/constants/loggerContexts'; import { isDefined, isFunction } from '@rudderstack/analytics-js-common/utilities/checks'; -import { generateMisconfiguredPluginsWarning } from '../../constants/logMessages'; +import { + DEPRECATED_PLUGIN_WARNING, + generateMisconfiguredPluginsWarning, +} from '../../constants/logMessages'; import { setExposedGlobal } from '../utilities/globals'; import { state } from '../../state'; import { @@ -21,7 +24,7 @@ import { StorageEncryptionVersionsToPluginNameMap, DataPlaneEventsTransportToPluginNameMap, } from '../configManager/constants'; -import { pluginNamesList } from './pluginNames'; +import { deprecatedPluginsList, pluginNamesList } from './pluginNames'; import { getMandatoryPluginsMap, pluginsInventory, @@ -95,15 +98,14 @@ class PluginsManager implements IPluginsManager { return []; } - // TODO: Uncomment below lines after removing deprecated plugin // Filter deprecated plugins - // pluginsToLoadFromConfig = pluginsToLoadFromConfig.filter(pluginName => { - // if (deprecatedPluginsList.includes(pluginName)) { - // this.logger?.warn(DEPRECATED_PLUGIN_WARNING(PLUGINS_MANAGER, pluginName)); - // return false; - // } - // return true; - // }); + pluginsToLoadFromConfig = pluginsToLoadFromConfig.filter(pluginName => { + if (deprecatedPluginsList.includes(pluginName)) { + this.logger.warn(DEPRECATED_PLUGIN_WARNING(PLUGINS_MANAGER, pluginName)); + return false; + } + return true; + }); const pluginGroupsToProcess: PluginsGroup[] = [ { @@ -198,7 +200,7 @@ class PluginsManager implements IPluginsManager { pluginsToLoadFromConfig.push(...missingPlugins); } - this.logger?.warn( + this.logger.warn( generateMisconfiguredPluginsWarning( PLUGINS_MANAGER, group.configurationStatusStr, diff --git a/packages/analytics-js/src/services/ErrorHandler/ErrorHandler.ts b/packages/analytics-js/src/services/ErrorHandler/ErrorHandler.ts index 68c53680d..f4b36145d 100644 --- a/packages/analytics-js/src/services/ErrorHandler/ErrorHandler.ts +++ b/packages/analytics-js/src/services/ErrorHandler/ErrorHandler.ts @@ -107,7 +107,7 @@ class ErrorHandler implements IErrorHandler { // Log handled errors and errors dispatched by the SDK if (errorType === ErrorType.HANDLEDEXCEPTION || isSdkDispatched) { - this.logger?.error( + this.logger.error( Object.create(normalizedError, { message: { value: bsException.message }, }), @@ -115,7 +115,7 @@ class ErrorHandler implements IErrorHandler { } } catch (err) { // If an error occurs while handling an error, log it - this.logger?.error(HANDLE_ERROR_FAILURE(ERROR_HANDLER), err); + this.logger.error(HANDLE_ERROR_FAILURE(ERROR_HANDLER), err); } } diff --git a/packages/analytics-js/src/services/StoreManager/Store.ts b/packages/analytics-js/src/services/StoreManager/Store.ts index 68160615a..0605d9da7 100644 --- a/packages/analytics-js/src/services/StoreManager/Store.ts +++ b/packages/analytics-js/src/services/StoreManager/Store.ts @@ -211,7 +211,7 @@ class Store implements IStore { * Handle errors */ onError(error: unknown) { - this.errorHandler?.onError(error, `Store ${this.id}`); + this.errorHandler.onError(error, `Store ${this.id}`); } } diff --git a/packages/analytics-js/src/services/StoreManager/StoreManager.ts b/packages/analytics-js/src/services/StoreManager/StoreManager.ts index 5a1dd5417..b63ddc3eb 100644 --- a/packages/analytics-js/src/services/StoreManager/StoreManager.ts +++ b/packages/analytics-js/src/services/StoreManager/StoreManager.ts @@ -192,7 +192,7 @@ class StoreManager implements IStoreManager { } if (finalStorageType !== storageType) { - this.logger?.warn( + this.logger.warn( STORAGE_UNAVAILABLE_WARNING(STORE_MANAGER, sessionKey, storageType, finalStorageType), ); } From f51cb955847c68c13f033f4635371f88cf3389e2 Mon Sep 17 00:00:00 2001 From: Sai Kumar Battinoju Date: Fri, 3 Jan 2025 13:08:06 +0530 Subject: [PATCH 21/53] test: add missing unit test for source configuration data --- .../components/configManager/ConfigManager.test.ts | 13 +++++++++++++ packages/analytics-js/src/constants/logMessages.ts | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/analytics-js/__tests__/components/configManager/ConfigManager.test.ts b/packages/analytics-js/__tests__/components/configManager/ConfigManager.test.ts index 2c92b8daa..fc2d42b8c 100644 --- a/packages/analytics-js/__tests__/components/configManager/ConfigManager.test.ts +++ b/packages/analytics-js/__tests__/components/configManager/ConfigManager.test.ts @@ -146,6 +146,19 @@ describe('ConfigManager', () => { expect(configManagerInstance.processConfig).toHaveBeenCalled(); }); + it('should log an error if getSourceConfig load option is not a function', () => { + // @ts-expect-error Testing invalid input + state.loadOptions.value.getSourceConfig = dummySourceConfigResponse; + configManagerInstance.processConfig = jest.fn(); + + configManagerInstance.getConfig(); + + expect(defaultLogger.error).toHaveBeenCalledTimes(1); + expect(defaultLogger.error).toHaveBeenCalledWith( + 'ConfigManager:: The "getSourceConfig" load API option must be a function that returns valid source configuration data.', + ); + }); + it('should update source, destination, lifecycle and reporting state with proper values', () => { const expectedSourceState = { id: dummySourceConfigResponse.source.id, diff --git a/packages/analytics-js/src/constants/logMessages.ts b/packages/analytics-js/src/constants/logMessages.ts index 5d773b604..3b8991eab 100644 --- a/packages/analytics-js/src/constants/logMessages.ts +++ b/packages/analytics-js/src/constants/logMessages.ts @@ -20,7 +20,7 @@ const PLUGIN_EXT_POINT_MISSING_ERROR = `Failed to invoke plugin because the exte const PLUGIN_EXT_POINT_INVALID_ERROR = `Failed to invoke plugin because the extension point name is invalid.`; const SOURCE_CONFIG_OPTION_ERROR = (context: string): string => - `${context}${LOG_CONTEXT_SEPARATOR}The "getSourceConfig" load API option must be a function return a valid source configuration object.`; + `${context}${LOG_CONTEXT_SEPARATOR}The "getSourceConfig" load API option must be a function that returns valid source configuration data.`; const COMPONENT_BASE_URL_ERROR = (context: string, component: string, url?: string): string => `${context}${LOG_CONTEXT_SEPARATOR}The base URL "${url}" for ${component} is not valid.`; From c85269931758a4bada25c39c15bb4e545f046de1 Mon Sep 17 00:00:00 2001 From: Sai Kumar Battinoju Date: Fri, 3 Jan 2025 13:20:18 +0530 Subject: [PATCH 22/53] chore: address ai bot review comments --- .../ExternalSrcLoader/ExternalSrcLoader.ts | 2 +- .../pluginsManager/PluginsManager.test.ts | 63 ++++++++++--------- .../src/services/StoreManager/StoreManager.ts | 2 +- 3 files changed, 34 insertions(+), 33 deletions(-) diff --git a/packages/analytics-js-common/src/services/ExternalSrcLoader/ExternalSrcLoader.ts b/packages/analytics-js-common/src/services/ExternalSrcLoader/ExternalSrcLoader.ts index 9b6b44344..60823685a 100644 --- a/packages/analytics-js-common/src/services/ExternalSrcLoader/ExternalSrcLoader.ts +++ b/packages/analytics-js-common/src/services/ExternalSrcLoader/ExternalSrcLoader.ts @@ -50,7 +50,7 @@ class ExternalSrcLoader implements IExternalSrcLoader { * Handle errors */ onError(error: unknown) { - this.errorHandler?.onError(error, EXTERNAL_SRC_LOADER); + this.errorHandler.onError(error, EXTERNAL_SRC_LOADER); } } diff --git a/packages/analytics-js/__tests__/components/pluginsManager/PluginsManager.test.ts b/packages/analytics-js/__tests__/components/pluginsManager/PluginsManager.test.ts index e74f74581..94374212d 100644 --- a/packages/analytics-js/__tests__/components/pluginsManager/PluginsManager.test.ts +++ b/packages/analytics-js/__tests__/components/pluginsManager/PluginsManager.test.ts @@ -18,6 +18,18 @@ describe('PluginsManager', () => { }); describe('getPluginsToLoadBasedOnConfig', () => { + /** + * Compare function to sort strings alphabetically using localeCompare. + * + * @param {string} a + * @param {string} b + * @returns {number} Negative if a < b, positive if a > b, zero if equal + */ + const alphabeticalCompare = (a: string, b: string) => + // Using "undefined" locale so that JavaScript decides the best locale. + // The { sensitivity: 'base' } option makes it case-insensitive + a.localeCompare(b); + beforeEach(() => { resetState(); @@ -34,16 +46,16 @@ describe('PluginsManager', () => { it('should return the default optional plugins if no plugins were configured in the state', () => { // All other plugins require some state variables to be set which by default are not set - expect(pluginsManager.getPluginsToLoadBasedOnConfig().sort()).toEqual( - ['ExternalAnonymousId', 'GoogleLinker'].sort(), + expect(pluginsManager.getPluginsToLoadBasedOnConfig().sort(alphabeticalCompare)).toEqual( + ['ExternalAnonymousId', 'GoogleLinker'].sort(alphabeticalCompare), ); }); it('should not filter the data plane queue plugin if it is automatically configured', () => { state.dataPlaneEvents.eventsQueuePluginName.value = 'XhrQueue'; - expect(pluginsManager.getPluginsToLoadBasedOnConfig().sort()).toEqual( - ['XhrQueue', 'ExternalAnonymousId', 'GoogleLinker'].sort(), + expect(pluginsManager.getPluginsToLoadBasedOnConfig().sort(alphabeticalCompare)).toEqual( + ['XhrQueue', 'ExternalAnonymousId', 'GoogleLinker'].sort(alphabeticalCompare), ); }); @@ -63,8 +75,8 @@ describe('PluginsManager', () => { it('should not filter the error reporting plugins if it is configured to load by default', () => { state.reporting.isErrorReportingEnabled.value = true; - expect(pluginsManager.getPluginsToLoadBasedOnConfig().sort()).toEqual( - ['ExternalAnonymousId', 'GoogleLinker'].sort(), + expect(pluginsManager.getPluginsToLoadBasedOnConfig().sort(alphabeticalCompare)).toEqual( + ['ExternalAnonymousId', 'GoogleLinker'].sort(alphabeticalCompare), ); }); @@ -78,13 +90,13 @@ describe('PluginsManager', () => { }, ]; - expect(pluginsManager.getPluginsToLoadBasedOnConfig().sort()).toEqual( + expect(pluginsManager.getPluginsToLoadBasedOnConfig().sort(alphabeticalCompare)).toEqual( [ 'DeviceModeDestinations', 'NativeDestinationQueue', 'ExternalAnonymousId', 'GoogleLinker', - ].sort(), + ].sort(alphabeticalCompare), ); }); @@ -99,7 +111,7 @@ describe('PluginsManager', () => { ]; state.plugins.pluginsToLoadFromConfig.value = []; - expect(pluginsManager.getPluginsToLoadBasedOnConfig().sort()).toEqual([]); + expect(pluginsManager.getPluginsToLoadBasedOnConfig().sort(alphabeticalCompare)).toEqual([]); // Expect a warning for user not explicitly configuring it expect(defaultLogger.warn).toHaveBeenCalledTimes(1); @@ -120,7 +132,7 @@ describe('PluginsManager', () => { // Only DeviceModeDestinations is configured state.plugins.pluginsToLoadFromConfig.value = ['DeviceModeDestinations']; - expect(pluginsManager.getPluginsToLoadBasedOnConfig().sort()).toEqual([ + expect(pluginsManager.getPluginsToLoadBasedOnConfig().sort(alphabeticalCompare)).toEqual([ 'DeviceModeDestinations', ]); @@ -147,14 +159,14 @@ describe('PluginsManager', () => { }, ]; - expect(pluginsManager.getPluginsToLoadBasedOnConfig().sort()).toEqual( + expect(pluginsManager.getPluginsToLoadBasedOnConfig().sort(alphabeticalCompare)).toEqual( [ 'DeviceModeTransformation', 'DeviceModeDestinations', 'NativeDestinationQueue', 'ExternalAnonymousId', 'GoogleLinker', - ].sort(), + ].sort(alphabeticalCompare), ); }); @@ -173,8 +185,8 @@ describe('PluginsManager', () => { 'NativeDestinationQueue', ]; - expect(pluginsManager.getPluginsToLoadBasedOnConfig().sort()).toEqual( - ['DeviceModeDestinations', 'NativeDestinationQueue'].sort(), + expect(pluginsManager.getPluginsToLoadBasedOnConfig().sort(alphabeticalCompare)).toEqual( + ['DeviceModeDestinations', 'NativeDestinationQueue'].sort(alphabeticalCompare), ); // Expect a warning for user not explicitly configuring it @@ -187,8 +199,8 @@ describe('PluginsManager', () => { it('should not filter storage encryption plugin if it is configured to load by default', () => { state.storage.encryptionPluginName.value = 'StorageEncryption'; - expect(pluginsManager.getPluginsToLoadBasedOnConfig().sort()).toEqual( - ['StorageEncryption', 'ExternalAnonymousId', 'GoogleLinker'].sort(), + expect(pluginsManager.getPluginsToLoadBasedOnConfig().sort(alphabeticalCompare)).toEqual( + ['StorageEncryption', 'ExternalAnonymousId', 'GoogleLinker'].sort(alphabeticalCompare), ); }); @@ -196,7 +208,7 @@ describe('PluginsManager', () => { state.storage.encryptionPluginName.value = 'StorageEncryption'; state.plugins.pluginsToLoadFromConfig.value = []; - expect(pluginsManager.getPluginsToLoadBasedOnConfig().sort()).toEqual([]); + expect(pluginsManager.getPluginsToLoadBasedOnConfig().sort(alphabeticalCompare)).toEqual([]); // Expect a warning for user not explicitly configuring it expect(defaultLogger.warn).toHaveBeenCalledTimes(1); @@ -208,8 +220,8 @@ describe('PluginsManager', () => { it('should not filter storage migrator plugin if it is configured to load by default', () => { state.storage.migrate.value = true; - expect(pluginsManager.getPluginsToLoadBasedOnConfig().sort()).toEqual( - ['StorageMigrator', 'ExternalAnonymousId', 'GoogleLinker'].sort(), + expect(pluginsManager.getPluginsToLoadBasedOnConfig().sort(alphabeticalCompare)).toEqual( + ['StorageMigrator', 'ExternalAnonymousId', 'GoogleLinker'].sort(alphabeticalCompare), ); }); @@ -217,7 +229,7 @@ describe('PluginsManager', () => { state.storage.migrate.value = true; state.plugins.pluginsToLoadFromConfig.value = []; - expect(pluginsManager.getPluginsToLoadBasedOnConfig().sort()).toEqual([]); + expect(pluginsManager.getPluginsToLoadBasedOnConfig().sort(alphabeticalCompare)).toEqual([]); // Expect a warning for user not explicitly configuring it expect(defaultLogger.warn).toHaveBeenCalledTimes(1); @@ -225,16 +237,5 @@ describe('PluginsManager', () => { "PluginsManager:: Storage migration is enabled, but 'StorageMigrator' plugin was not configured to load. Ignore if this was intentional. Otherwise, consider adding it to the 'plugins' load API option.", ); }); - - it('should not log any warning if logger is not supplied', () => { - pluginsManager = new PluginsManager(defaultPluginEngine, defaultErrorHandler); - - // Checking only for the migration plugin - state.storage.migrate.value = true; - state.plugins.pluginsToLoadFromConfig.value = []; - - expect(pluginsManager.getPluginsToLoadBasedOnConfig().sort()).toEqual([]); - expect(defaultLogger.warn).not.toHaveBeenCalled(); - }); }); }); diff --git a/packages/analytics-js/src/services/StoreManager/StoreManager.ts b/packages/analytics-js/src/services/StoreManager/StoreManager.ts index b63ddc3eb..35005b52b 100644 --- a/packages/analytics-js/src/services/StoreManager/StoreManager.ts +++ b/packages/analytics-js/src/services/StoreManager/StoreManager.ts @@ -220,7 +220,7 @@ class StoreManager implements IStoreManager { * Handle errors */ onError(error: unknown) { - this.errorHandler?.onError(error, STORE_MANAGER); + this.errorHandler.onError(error, STORE_MANAGER); } } From cb8e1d8c8273bc8da0e9d64d5f1baaaae9a652ff Mon Sep 17 00:00:00 2001 From: Sai Kumar Battinoju Date: Fri, 3 Jan 2025 14:05:53 +0530 Subject: [PATCH 23/53] fix: user and context details in the error payload --- packages/analytics-js-common/src/types/Metrics.ts | 2 +- .../__tests__/services/ErrorHandler/ErrorHandler.test.ts | 4 ++-- .../__tests__/services/ErrorHandler/utils.test.ts | 5 ++++- .../analytics-js/src/services/ErrorHandler/ErrorHandler.ts | 6 +----- packages/analytics-js/src/services/ErrorHandler/utils.ts | 6 ++++-- 5 files changed, 12 insertions(+), 11 deletions(-) diff --git a/packages/analytics-js-common/src/types/Metrics.ts b/packages/analytics-js-common/src/types/Metrics.ts index 2fda29cf5..b31d5fde9 100644 --- a/packages/analytics-js-common/src/types/Metrics.ts +++ b/packages/analytics-js-common/src/types/Metrics.ts @@ -42,12 +42,12 @@ export type ErrorEvent = { clientIp: string; }; breadcrumbs: Breadcrumb[] | []; + context: string; metaData: { [index: string]: any; }; user: { id: string; - name: string; }; }; diff --git a/packages/analytics-js/__tests__/services/ErrorHandler/ErrorHandler.test.ts b/packages/analytics-js/__tests__/services/ErrorHandler/ErrorHandler.test.ts index 47ea394df..851491b7e 100644 --- a/packages/analytics-js/__tests__/services/ErrorHandler/ErrorHandler.test.ts +++ b/packages/analytics-js/__tests__/services/ErrorHandler/ErrorHandler.test.ts @@ -124,7 +124,7 @@ describe('ErrorHandler', () => { errorHandlerInstance.onError(new Error('dummy error'), 'Test'); expect(defaultLogger.error).toHaveBeenCalledTimes(1); - expect(defaultLogger.error).toHaveBeenCalledWith(new Error('Test:: dummy error')); + expect(defaultLogger.error).toHaveBeenCalledWith('Test:: dummy error'); }); it('should not log unhandled errors to the console', () => { @@ -149,7 +149,7 @@ describe('ErrorHandler', () => { errorHandlerInstance.onError(errorEvent, 'Test', undefined, 'unhandledException'); expect(defaultLogger.error).toHaveBeenCalledTimes(1); - expect(defaultLogger.error).toHaveBeenCalledWith(new Error('Test:: dummy error')); + expect(defaultLogger.error).toHaveBeenCalledWith('Test:: dummy error'); }); it('should not notify errors if error reporting is disabled', () => { diff --git a/packages/analytics-js/__tests__/services/ErrorHandler/utils.test.ts b/packages/analytics-js/__tests__/services/ErrorHandler/utils.test.ts index e3ad40f4a..d06b7677a 100644 --- a/packages/analytics-js/__tests__/services/ErrorHandler/utils.test.ts +++ b/packages/analytics-js/__tests__/services/ErrorHandler/utils.test.ts @@ -382,6 +382,7 @@ describe('Error Reporting utilities', () => { type: 'manual', }, ], + context: 'dummy message', metaData: { app: { snippetVersion: 'sample_snippet_version', @@ -561,10 +562,12 @@ describe('Error Reporting utilities', () => { migrate: false, trulyAnonymousTracking: false, }, + user: { + name: 'dummy-source-name', + }, }, user: { id: 'dummy-source-id..123..test-visit-id', - name: 'dummy-source-name', }, }, ], diff --git a/packages/analytics-js/src/services/ErrorHandler/ErrorHandler.ts b/packages/analytics-js/src/services/ErrorHandler/ErrorHandler.ts index f4b36145d..e3774e25f 100644 --- a/packages/analytics-js/src/services/ErrorHandler/ErrorHandler.ts +++ b/packages/analytics-js/src/services/ErrorHandler/ErrorHandler.ts @@ -107,11 +107,7 @@ class ErrorHandler implements IErrorHandler { // Log handled errors and errors dispatched by the SDK if (errorType === ErrorType.HANDLEDEXCEPTION || isSdkDispatched) { - this.logger.error( - Object.create(normalizedError, { - message: { value: bsException.message }, - }), - ); + this.logger.error(bsException.message); } } catch (err) { // If an error occurs while handling an error, log it diff --git a/packages/analytics-js/src/services/ErrorHandler/utils.ts b/packages/analytics-js/src/services/ErrorHandler/utils.ts index da6a0a673..c5d850f30 100644 --- a/packages/analytics-js/src/services/ErrorHandler/utils.ts +++ b/packages/analytics-js/src/services/ErrorHandler/utils.ts @@ -100,19 +100,21 @@ const getBugsnagErrorEvent = ( clientIp: '[NOT COLLECTED]', }, breadcrumbs: clone(reporting.breadcrumbs.value), + context: exception.message, metaData: { app: { snippetVersion: library.value.snippetVersion, }, device: { ...screen.value, timezone: timezone.value }, + user: { + name: source.value?.name ?? 'NA', + }, // Add rest of the state groups as metadata // so that they show up as separate tabs in the dashboard ...getAppStateForMetadata(state), }, user: { - // Combination of source, session and visit ids id: `${source.value?.id ?? (lifecycle.writeKey.value as string)}..${session.sessionInfo.value?.id ?? 'NA'}..${autoTrack?.pageLifecycle?.visitId?.value ?? 'NA'}`, - name: source.value?.name ?? 'NA', }, }, ], From 5c1fbe4c5c05a2c5e24907541009afc3421cdf46 Mon Sep 17 00:00:00 2001 From: Sai Kumar Battinoju Date: Fri, 3 Jan 2025 14:51:16 +0530 Subject: [PATCH 24/53] chore: remove invalid test case --- .../components/configManager/commonUtil.test.ts | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/packages/analytics-js/__tests__/components/configManager/commonUtil.test.ts b/packages/analytics-js/__tests__/components/configManager/commonUtil.test.ts index ff30f7960..4d66612c9 100644 --- a/packages/analytics-js/__tests__/components/configManager/commonUtil.test.ts +++ b/packages/analytics-js/__tests__/components/configManager/commonUtil.test.ts @@ -655,19 +655,6 @@ describe('Config Manager Common Utilities', () => { ); }); - it('should return default source config URL if invalid source config URL is provided and no logger is supplied', () => { - // Mock console.warn - const consoleWarnMock = jest.spyOn(console, 'warn').mockImplementation(() => {}); - - const sourceConfigURL = getSourceConfigURL('invalid-url', 'writekey', true, true); - - expect(sourceConfigURL).toBe( - 'https://api.rudderstack.com/sourceConfig/?p=__MODULE_TYPE__&v=__PACKAGE_VERSION__&build=modern&writeKey=writekey&lockIntegrationsVersion=true&lockPluginsVersion=true', - ); - - expect(consoleWarnMock).not.toHaveBeenCalled(); - }); - it('should return the source config URL with default endpoint appended if no endpoint is present', () => { const sourceConfigURL = getSourceConfigURL('https://www.dummy.url', 'writekey', false, false); From a590ed8e845a7f4bf1a72210013485540bb49ab8 Mon Sep 17 00:00:00 2001 From: Sai Kumar Battinoju Date: Fri, 3 Jan 2025 16:12:15 +0530 Subject: [PATCH 25/53] test: fix failing tests --- .../CapabilitiesManager.test.ts | 32 +----------- .../configManager/commonUtil.test.ts | 49 +++++++++++++------ .../components/eventManager/utilities.test.ts | 12 ----- 3 files changed, 34 insertions(+), 59 deletions(-) diff --git a/packages/analytics-js/__tests__/components/capabilitiesManager/CapabilitiesManager.test.ts b/packages/analytics-js/__tests__/components/capabilitiesManager/CapabilitiesManager.test.ts index 6aa091f02..f81b9cf85 100644 --- a/packages/analytics-js/__tests__/components/capabilitiesManager/CapabilitiesManager.test.ts +++ b/packages/analytics-js/__tests__/components/capabilitiesManager/CapabilitiesManager.test.ts @@ -1,4 +1,5 @@ import type { ILogger } from '@rudderstack/analytics-js-common/types/Logger'; +import { defaultLogger } from '../../../src/services/Logger'; import { defaultHttpClient } from '../../../src/services/HttpClient'; import { isLegacyJSEngine } from '../../../src/components/capabilitiesManager/detection'; import type { ICapabilitiesManager } from '../../../src/components/capabilitiesManager/types'; @@ -98,37 +99,6 @@ describe('CapabilitiesManager', () => { ); }); - it('should use default polyfill URL but not log any warning if custom URL and logger are not provided', () => { - state.loadOptions.value.polyfillURL = 'invalid-url'; - state.lifecycle.writeKey.value = 'sample-write-key'; - state.loadOptions.value.polyfillIfRequired = true; - - const tempCapabilitiesManager = new CapabilitiesManager( - defaultHttpClient, - defaultErrorHandler, - ); - - isLegacyJSEngine.mockReturnValue(true); - tempCapabilitiesManager.externalSrcLoader = { - loadJSFile: jest.fn(), - } as any; - - tempCapabilitiesManager.prepareBrowserCapabilities(); - - expect(tempCapabilitiesManager.externalSrcLoader.loadJSFile).toHaveBeenCalledWith({ - url: 'https://somevalid.polyfill.url&callback=RS_polyfillCallback_sample-write-key', - id: 'rudderstackPolyfill', - async: true, - timeout: 10000, - callback: expect.any(Function), - }); - - // mock console.warn - const consoleWarn = jest.spyOn(console, 'warn').mockImplementation(() => {}); - - expect(consoleWarn).not.toHaveBeenCalled(); - }); - it('should not load polyfills if default polyfill URL is invalid', () => { state.loadOptions.value.polyfillURL = 'invalid-url'; state.lifecycle.writeKey.value = 'sample-write-key'; diff --git a/packages/analytics-js/__tests__/components/configManager/commonUtil.test.ts b/packages/analytics-js/__tests__/components/configManager/commonUtil.test.ts index 4d66612c9..eaace0def 100644 --- a/packages/analytics-js/__tests__/components/configManager/commonUtil.test.ts +++ b/packages/analytics-js/__tests__/components/configManager/commonUtil.test.ts @@ -59,7 +59,7 @@ describe('Config Manager Common Utilities', () => { removeScriptElement(); }); - const testCases = [ + const testCases: any[][] = [ // expected, input [ 'https://www.dummy.url/fromScript/v3/rsa.min.js', @@ -83,12 +83,15 @@ describe('Config Manager Common Utilities', () => { [undefined, null], ]; - test.each(testCases)('should return %s when the script src is %s', (expected, input) => { - createScriptElement(input as string); + test.each(testCases)( + 'should return %s when the script src is %s', + (expected: any, input: any) => { + createScriptElement(input as string); - const sdkURL = getSDKUrl(); - expect(sdkURL).toBe(expected); - }); + const sdkURL = getSDKUrl(); + expect(sdkURL).toBe(expected); + }, + ); }); describe('updateReportingState', () => { @@ -108,11 +111,10 @@ describe('Config Manager Common Utilities', () => { }, } as SourceConfigResponse; - updateReportingState(mockSourceConfig, mockLogger); + updateReportingState(mockSourceConfig); expect(state.reporting.isErrorReportingEnabled.value).toBe(true); expect(state.reporting.isMetricsReportingEnabled.value).toBe(true); - expect(mockLogger.warn).not.toHaveBeenCalled(); }); it('should update reporting state with the data from source config even if error reporting provider is not specified', () => { @@ -131,11 +133,10 @@ describe('Config Manager Common Utilities', () => { }, } as SourceConfigResponse; - updateReportingState(mockSourceConfig, mockLogger); + updateReportingState(mockSourceConfig); expect(state.reporting.isErrorReportingEnabled.value).toBe(true); expect(state.reporting.isMetricsReportingEnabled.value).toBe(true); - expect(mockLogger.warn).not.toHaveBeenCalled(); }); }); @@ -151,7 +152,7 @@ describe('Config Manager Common Utilities', () => { }, }; - updateStorageStateFromLoadOptions(); + updateStorageStateFromLoadOptions(mockLogger); expect(state.storage.encryptionPluginName.value).toBe('StorageEncryption'); expect(state.storage.migrate.value).toBe(true); @@ -168,6 +169,7 @@ describe('Config Manager Common Utilities', () => { it('should log a warning if the specified storage type is not valid', () => { state.loadOptions.value.storage = { + // @ts-expect-error testing invalid value type: 'random-type', }; @@ -182,6 +184,7 @@ describe('Config Manager Common Utilities', () => { it('should log a warning if the encryption version is not supported', () => { state.loadOptions.value.storage = { encryption: { + // @ts-expect-error testing invalid value version: 'v2', }, }; @@ -332,7 +335,7 @@ describe('Config Manager Common Utilities', () => { }, }; - updateConsentsStateFromLoadOptions(); + updateConsentsStateFromLoadOptions(mockLogger); expect(state.consents.activeConsentManagerPluginName.value).toBe('OneTrustConsentManager'); expect(state.consents.preConsent.value).toStrictEqual({ @@ -354,6 +357,7 @@ describe('Config Manager Common Utilities', () => { it('should log an error if the specified consent manager is not supported', () => { state.loadOptions.value.consentManagement = { enabled: true, + // @ts-expect-error testing invalid value provider: 'randomManager', }; @@ -374,6 +378,7 @@ describe('Config Manager Common Utilities', () => { state.loadOptions.value.preConsent = { enabled: true, storage: { + // @ts-expect-error testing invalid value strategy: 'random-strategy', }, events: { @@ -409,6 +414,7 @@ describe('Config Manager Common Utilities', () => { strategy: 'none', }, events: { + // @ts-expect-error testing invalid value delivery: 'random-delivery', }, }; @@ -447,7 +453,7 @@ describe('Config Manager Common Utilities', () => { deniedConsentIds: ['consent2'], }; - updateConsentsStateFromLoadOptions(); + updateConsentsStateFromLoadOptions(mockLogger); expect(state.consents.preConsent.value).toStrictEqual({ enabled: false, @@ -475,7 +481,7 @@ describe('Config Manager Common Utilities', () => { enabled: false, }; - updateConsentsStateFromLoadOptions(); + updateConsentsStateFromLoadOptions(mockLogger); expect(state.consents.preConsent.value).toStrictEqual({ enabled: false, @@ -544,7 +550,7 @@ describe('Config Manager Common Utilities', () => { state.consents.provider.value = 'ketch'; const mockSourceConfig = { consentManagementMetadata: 'random-metadata', - } as SourceConfigResponse; + } as unknown as SourceConfigResponse; updateConsentsState(mockSourceConfig); @@ -575,6 +581,7 @@ describe('Config Manager Common Utilities', () => { }); it('should not update the resolution strategy to state if the provider is not supported', () => { + // @ts-expect-error testing invalid value state.consents.provider.value = 'random-provider'; const mockSourceConfig = { consentManagementMetadata: { @@ -656,7 +663,13 @@ describe('Config Manager Common Utilities', () => { }); it('should return the source config URL with default endpoint appended if no endpoint is present', () => { - const sourceConfigURL = getSourceConfigURL('https://www.dummy.url', 'writekey', false, false); + const sourceConfigURL = getSourceConfigURL( + 'https://www.dummy.url', + 'writekey', + false, + false, + mockLogger, + ); expect(sourceConfigURL).toBe( 'https://www.dummy.url/sourceConfig/?p=__MODULE_TYPE__&v=__PACKAGE_VERSION__&build=modern&writeKey=writekey&lockIntegrationsVersion=false&lockPluginsVersion=false', @@ -669,6 +682,7 @@ describe('Config Manager Common Utilities', () => { 'writekey', false, false, + mockLogger, ); expect(sourceConfigURL).toBe( @@ -682,6 +696,7 @@ describe('Config Manager Common Utilities', () => { 'writekey', false, false, + mockLogger, ); expect(sourceConfigURL).toBe( @@ -695,6 +710,7 @@ describe('Config Manager Common Utilities', () => { 'writekey', false, false, + mockLogger, ); expect(sourceConfigURL).toBe( @@ -708,6 +724,7 @@ describe('Config Manager Common Utilities', () => { 'writekey', false, false, + mockLogger, ); expect(sourceConfigURL).toBe( diff --git a/packages/analytics-js/__tests__/components/eventManager/utilities.test.ts b/packages/analytics-js/__tests__/components/eventManager/utilities.test.ts index 9f2b0adc9..799bd7555 100644 --- a/packages/analytics-js/__tests__/components/eventManager/utilities.test.ts +++ b/packages/analytics-js/__tests__/components/eventManager/utilities.test.ts @@ -388,18 +388,6 @@ describe('Event Manager - Utilities', () => { expect(mockLogger.warn).not.toHaveBeenCalled(); }); - it('should not log a warn message if the logger is not provided', () => { - const obj = { - anonymousId: sampleAnonId, - originalTimestamp: sampleOriginalTimestamp, - nonReservedKey: 123, - } as ApiObject; - - checkForReservedElementsInObject(obj, defaultParentKeyPath); - - expect(mockLogger.warn).not.toHaveBeenCalled(); - }); - it('should not log a warn message if the object is not provided', () => { checkForReservedElementsInObject(undefined, defaultParentKeyPath, mockLogger); From 75f05c6be57b2bba5319b473cc22d1765e890129 Mon Sep 17 00:00:00 2001 From: Sai Kumar Battinoju Date: Fri, 3 Jan 2025 19:46:18 +0530 Subject: [PATCH 26/53] fix: restore user name in the payload type --- packages/analytics-js-common/src/types/Metrics.ts | 1 + .../__tests__/services/ErrorHandler/utils.test.ts | 4 +--- packages/analytics-js/src/services/ErrorHandler/utils.ts | 4 +--- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/analytics-js-common/src/types/Metrics.ts b/packages/analytics-js-common/src/types/Metrics.ts index b31d5fde9..189f30947 100644 --- a/packages/analytics-js-common/src/types/Metrics.ts +++ b/packages/analytics-js-common/src/types/Metrics.ts @@ -48,6 +48,7 @@ export type ErrorEvent = { }; user: { id: string; + name: string; }; }; diff --git a/packages/analytics-js/__tests__/services/ErrorHandler/utils.test.ts b/packages/analytics-js/__tests__/services/ErrorHandler/utils.test.ts index d06b7677a..54746ffbe 100644 --- a/packages/analytics-js/__tests__/services/ErrorHandler/utils.test.ts +++ b/packages/analytics-js/__tests__/services/ErrorHandler/utils.test.ts @@ -562,12 +562,10 @@ describe('Error Reporting utilities', () => { migrate: false, trulyAnonymousTracking: false, }, - user: { - name: 'dummy-source-name', - }, }, user: { id: 'dummy-source-id..123..test-visit-id', + name: 'dummy-source-name', }, }, ], diff --git a/packages/analytics-js/src/services/ErrorHandler/utils.ts b/packages/analytics-js/src/services/ErrorHandler/utils.ts index c5d850f30..97a03aaf1 100644 --- a/packages/analytics-js/src/services/ErrorHandler/utils.ts +++ b/packages/analytics-js/src/services/ErrorHandler/utils.ts @@ -106,15 +106,13 @@ const getBugsnagErrorEvent = ( snippetVersion: library.value.snippetVersion, }, device: { ...screen.value, timezone: timezone.value }, - user: { - name: source.value?.name ?? 'NA', - }, // Add rest of the state groups as metadata // so that they show up as separate tabs in the dashboard ...getAppStateForMetadata(state), }, user: { id: `${source.value?.id ?? (lifecycle.writeKey.value as string)}..${session.sessionInfo.value?.id ?? 'NA'}..${autoTrack?.pageLifecycle?.visitId?.value ?? 'NA'}`, + name: source.value?.name ?? 'NA', }, }, ], From 7e0d8153f66a314b477fc407b45b260deee12d22 Mon Sep 17 00:00:00 2001 From: Sai Kumar Battinoju Date: Fri, 3 Jan 2025 20:33:00 +0530 Subject: [PATCH 27/53] chore: replace deprecated jest apis --- .../components/core/Analytics.test.ts | 2 +- .../eventRepository/EventRepository.test.ts | 30 +++++++++---------- .../StoreManager/StoreManager.test.ts | 2 +- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/packages/analytics-js/__tests__/components/core/Analytics.test.ts b/packages/analytics-js/__tests__/components/core/Analytics.test.ts index f10e2ff0f..58525d952 100644 --- a/packages/analytics-js/__tests__/components/core/Analytics.test.ts +++ b/packages/analytics-js/__tests__/components/core/Analytics.test.ts @@ -245,7 +245,7 @@ describe('Core - Analytics', () => { const onReadySpy = jest.spyOn(analytics, 'onReady'); state.lifecycle.status.value = 'ready'; analytics.onDestinationsReady(); - expect(onReadySpy).not.toBeCalled(); + expect(onReadySpy).not.toHaveBeenCalled(); onReadySpy.mockRestore(); }); diff --git a/packages/analytics-js/__tests__/components/eventRepository/EventRepository.test.ts b/packages/analytics-js/__tests__/components/eventRepository/EventRepository.test.ts index 599dd96e7..1e725716b 100644 --- a/packages/analytics-js/__tests__/components/eventRepository/EventRepository.test.ts +++ b/packages/analytics-js/__tests__/components/eventRepository/EventRepository.test.ts @@ -142,7 +142,7 @@ describe('EventRepository', () => { state.nativeDestinations.clientDestinationsReady.value = true; - expect(mockDestinationsEventsQueue.start).toBeCalledTimes(1); + expect(mockDestinationsEventsQueue.start).toHaveBeenCalledTimes(1); }); it('should start the dataplane events queue when no hybrid destinations are present', () => { @@ -173,7 +173,7 @@ describe('EventRepository', () => { eventRepository.init(); - expect(mockDataplaneEventsQueue.start).toBeCalledTimes(1); + expect(mockDataplaneEventsQueue.start).toHaveBeenCalledTimes(1); }); it('should start the dataplane events queue when hybrid destinations are present and bufferDataPlaneEventsUntilReady is false', () => { @@ -189,7 +189,7 @@ describe('EventRepository', () => { eventRepository.init(); - expect(mockDataplaneEventsQueue.start).toBeCalledTimes(1); + expect(mockDataplaneEventsQueue.start).toHaveBeenCalledTimes(1); }); it('should start the dataplane events queue when hybrid destinations are present and bufferDataPlaneEventsUntilReady is true and client destinations are ready after some time', done => { @@ -206,11 +206,11 @@ describe('EventRepository', () => { eventRepository.init(); - expect(mockDataplaneEventsQueue.start).not.toBeCalled(); + expect(mockDataplaneEventsQueue.start).not.toHaveBeenCalled(); setTimeout(() => { state.nativeDestinations.clientDestinationsReady.value = true; - expect(mockDataplaneEventsQueue.start).toBeCalledTimes(1); + expect(mockDataplaneEventsQueue.start).toHaveBeenCalledTimes(1); done(); }, 500); }); @@ -229,10 +229,10 @@ describe('EventRepository', () => { eventRepository.init(); - expect(mockDataplaneEventsQueue.start).not.toBeCalled(); + expect(mockDataplaneEventsQueue.start).not.toHaveBeenCalled(); setTimeout(() => { - expect(mockDataplaneEventsQueue.start).toBeCalledTimes(1); + expect(mockDataplaneEventsQueue.start).toHaveBeenCalledTimes(1); done(); }, state.loadOptions.value.dataPlaneEventsBufferTimeout + 50); }); @@ -286,8 +286,8 @@ describe('EventRepository', () => { const mockEventCallback = jest.fn(); eventRepository.enqueue(testEvent, mockEventCallback); - expect(mockEventCallback).toBeCalledTimes(1); - expect(mockEventCallback).toBeCalledWith({ + expect(mockEventCallback).toHaveBeenCalledTimes(1); + expect(mockEventCallback).toHaveBeenCalledWith({ ...testEvent, integrations: { All: true }, }); @@ -312,8 +312,8 @@ describe('EventRepository', () => { }); eventRepository.enqueue(testEvent, mockEventCallback); - expect(mockErrorHandler.onError).toBeCalledTimes(1); - expect(mockErrorHandler.onError).toBeCalledWith( + expect(mockErrorHandler.onError).toHaveBeenCalledTimes(1); + expect(mockErrorHandler.onError).toHaveBeenCalledWith( new Error('test error'), 'EventRepository', 'API Callback Invocation Failed', @@ -339,7 +339,7 @@ describe('EventRepository', () => { eventRepository.init(); - expect(mockDataplaneEventsQueue.start).not.toBeCalled(); + expect(mockDataplaneEventsQueue.start).not.toHaveBeenCalled(); }); describe('resume', () => { @@ -352,7 +352,7 @@ describe('EventRepository', () => { eventRepository.init(); eventRepository.resume(); - expect(mockDataplaneEventsQueue.start).toBeCalled(); + expect(mockDataplaneEventsQueue.start).toHaveBeenCalled(); }); it('should clear the events queue if discardPreConsentEvents is set to true', () => { @@ -368,8 +368,8 @@ describe('EventRepository', () => { eventRepository.resume(); - expect(mockDataplaneEventsQueue.clear).toBeCalled(); - expect(mockDestinationsEventsQueue.clear).toBeCalled(); + expect(mockDataplaneEventsQueue.clear).toHaveBeenCalled(); + expect(mockDestinationsEventsQueue.clear).toHaveBeenCalled(); }); }); }); diff --git a/packages/analytics-js/__tests__/services/StoreManager/StoreManager.test.ts b/packages/analytics-js/__tests__/services/StoreManager/StoreManager.test.ts index a28fffa93..1c1724e7b 100644 --- a/packages/analytics-js/__tests__/services/StoreManager/StoreManager.test.ts +++ b/packages/analytics-js/__tests__/services/StoreManager/StoreManager.test.ts @@ -202,7 +202,7 @@ describe('StoreManager', () => { state.loadOptions.value.storage.entries = loadOptionWithEntry; storeManager.initClientDataStores(); expect(state.storage.entries.value).toEqual(entriesWithInMemoryFallback); - expect(logger.warn).toBeCalledTimes(8); + expect(logger.warn).toHaveBeenCalledTimes(8); }); it('should construct the appropriate storage entry state if the pre-consent storage strategy is set to none', () => { From 46063876117c211773fe18143b99914f82d2096b Mon Sep 17 00:00:00 2001 From: Sai Kumar Battinoju Date: Tue, 7 Jan 2025 20:24:18 +0530 Subject: [PATCH 28/53] chore: avoid fixing paths in reports --- .github/workflows/unit-tests-and-lint.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/unit-tests-and-lint.yml b/.github/workflows/unit-tests-and-lint.yml index 7ee9228ba..6b5e1ac0f 100644 --- a/.github/workflows/unit-tests-and-lint.yml +++ b/.github/workflows/unit-tests-and-lint.yml @@ -58,6 +58,7 @@ jobs: npm run check:lint:ci - name: Fix filesystem paths in generated reports + if: false run: | ./scripts/fix-reports-path-in-github-runner.sh From 6e6dba82f7a796bacfc437899ee6397e1e63b3b7 Mon Sep 17 00:00:00 2001 From: Sai Kumar Battinoju Date: Tue, 7 Jan 2025 20:46:23 +0530 Subject: [PATCH 29/53] chore: fix reports paths to relative --- .github/workflows/unit-tests-and-lint.yml | 2 +- package.json | 1 + scripts/fix-reports-path-in-github-runner.sh | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/unit-tests-and-lint.yml b/.github/workflows/unit-tests-and-lint.yml index 6b5e1ac0f..46936056d 100644 --- a/.github/workflows/unit-tests-and-lint.yml +++ b/.github/workflows/unit-tests-and-lint.yml @@ -33,7 +33,7 @@ jobs: env: HUSKY: 0 run: | - npm run setup:ci + npm run setup:test - name: Check for affected projects id: check_affected diff --git a/package.json b/package.json index 73b3aa5aa..43de0a7fb 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "scripts": { "setup": "npm i --include=optional && npm run build:package:modern", "setup:ci": "npm ci && npm i @nx/nx-linux-x64-gnu && npm run build:package:modern", + "setup:test": "npm ci && npm i @nx/nx-linux-x64-gnu", "clean": "nx run-many -t clean && nx reset && git clean -xdf node_modules", "clean:cache": "rimraf -rf ./node_modules/.cache && rimraf -rf ./.nx/cache", "start": "nx run-many --targets=start --parallel=3 --projects=@rudderstack/analytics-js-integrations,@rudderstack/analytics-js-plugins,@rudderstack/analytics-js", diff --git a/scripts/fix-reports-path-in-github-runner.sh b/scripts/fix-reports-path-in-github-runner.sh index 6dab2ddde..1f0602cda 100755 --- a/scripts/fix-reports-path-in-github-runner.sh +++ b/scripts/fix-reports-path-in-github-runner.sh @@ -1,6 +1,6 @@ #!/bin/bash # Path variables -defaultPrefixToReplace="/github/workspace" +defaultPrefixToReplace="" defaultAbsolutePathPrefix="home/runner/work/rudder-sdk-js/rudder-sdk-js" selfHostedAbsolutePathPrefix="runner/_work/rudder-sdk-js/rudder-sdk-js" absolutePathPrefix="$selfHostedAbsolutePathPrefix" From 10bf1a8540beb07541b8acea0cc0736bbf0d829f Mon Sep 17 00:00:00 2001 From: Sai Kumar Battinoju Date: Tue, 7 Jan 2025 21:01:38 +0530 Subject: [PATCH 30/53] chore: fix report paths --- .github/workflows/unit-tests-and-lint.yml | 1 - scripts/fix-reports-path-in-github-runner.sh | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/unit-tests-and-lint.yml b/.github/workflows/unit-tests-and-lint.yml index 46936056d..b7c0435a2 100644 --- a/.github/workflows/unit-tests-and-lint.yml +++ b/.github/workflows/unit-tests-and-lint.yml @@ -58,7 +58,6 @@ jobs: npm run check:lint:ci - name: Fix filesystem paths in generated reports - if: false run: | ./scripts/fix-reports-path-in-github-runner.sh diff --git a/scripts/fix-reports-path-in-github-runner.sh b/scripts/fix-reports-path-in-github-runner.sh index 1f0602cda..f0f970a69 100755 --- a/scripts/fix-reports-path-in-github-runner.sh +++ b/scripts/fix-reports-path-in-github-runner.sh @@ -1,8 +1,8 @@ #!/bin/bash # Path variables defaultPrefixToReplace="" -defaultAbsolutePathPrefix="home/runner/work/rudder-sdk-js/rudder-sdk-js" -selfHostedAbsolutePathPrefix="runner/_work/rudder-sdk-js/rudder-sdk-js" +defaultAbsolutePathPrefix="home/runner/work/rudder-sdk-js/rudder-sdk-js/" +selfHostedAbsolutePathPrefix="runner/_work/rudder-sdk-js/rudder-sdk-js/" absolutePathPrefix="$selfHostedAbsolutePathPrefix" # List of package folders projectFolderNames=("analytics-js" "analytics-js-common" "analytics-js-integrations" "analytics-js-plugins" "analytics-js-service-worker" "analytics-v1.1" "analytics-js-cookies") From 073c1c69707a1a65275d336298b12ccae546125a Mon Sep 17 00:00:00 2001 From: Sai Kumar Battinoju Date: Thu, 9 Jan 2025 14:58:40 +0530 Subject: [PATCH 31/53] fix: error message prefix --- .../__tests__/services/ErrorHandler/ErrorHandler.test.ts | 4 ++-- .../analytics-js/src/services/ErrorHandler/ErrorHandler.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/analytics-js/__tests__/services/ErrorHandler/ErrorHandler.test.ts b/packages/analytics-js/__tests__/services/ErrorHandler/ErrorHandler.test.ts index 851491b7e..c85fff839 100644 --- a/packages/analytics-js/__tests__/services/ErrorHandler/ErrorHandler.test.ts +++ b/packages/analytics-js/__tests__/services/ErrorHandler/ErrorHandler.test.ts @@ -121,10 +121,10 @@ describe('ErrorHandler', () => { // @ts-expect-error need to set the value for testing state.context.app.value.installType = 'npm'; - errorHandlerInstance.onError(new Error('dummy error'), 'Test'); + errorHandlerInstance.onError(new Error('dummy error'), 'Test', 'Custom Message'); expect(defaultLogger.error).toHaveBeenCalledTimes(1); - expect(defaultLogger.error).toHaveBeenCalledWith('Test:: dummy error'); + expect(defaultLogger.error).toHaveBeenCalledWith('Test:: Custom Message dummy error'); }); it('should not log unhandled errors to the console', () => { diff --git a/packages/analytics-js/src/services/ErrorHandler/ErrorHandler.ts b/packages/analytics-js/src/services/ErrorHandler/ErrorHandler.ts index e3774e25f..ce19fb405 100644 --- a/packages/analytics-js/src/services/ErrorHandler/ErrorHandler.ts +++ b/packages/analytics-js/src/services/ErrorHandler/ErrorHandler.ts @@ -67,7 +67,7 @@ class ErrorHandler implements IErrorHandler { return; } - const errorMsgPrefix = `${context}${LOG_CONTEXT_SEPARATOR}${customMessage}`; + const errorMsgPrefix = `${context}${LOG_CONTEXT_SEPARATOR}${customMessage ? `${customMessage} ` : ''}`; const bsException = createBugsnagException(normalizedError, errorMsgPrefix); const stacktrace = getStacktrace(normalizedError); From 96c79cc62819311f133fd41811fceea37ede445d Mon Sep 17 00:00:00 2001 From: Sai Kumar Battinoju Date: Thu, 9 Jan 2025 18:44:08 +0530 Subject: [PATCH 32/53] fix: allow errors with simple stack trace --- .../analytics-js-common/__tests__/utilities/errors.test.ts | 7 ------- packages/analytics-js-common/src/utilities/errors.ts | 4 ++-- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/packages/analytics-js-common/__tests__/utilities/errors.test.ts b/packages/analytics-js-common/__tests__/utilities/errors.test.ts index dd8d4ba16..3b54d1e82 100644 --- a/packages/analytics-js-common/__tests__/utilities/errors.test.ts +++ b/packages/analytics-js-common/__tests__/utilities/errors.test.ts @@ -79,12 +79,5 @@ describe('Errors - utilities', () => { expect(getStacktrace(error)).toBeUndefined(); }); - - it('should return undefined if stack is the same as name and message', () => { - const error = new Error('Test error'); - error.stack = `${error.name}: ${error.message}`; - - expect(getStacktrace(error)).toBeUndefined(); - }); }); }); diff --git a/packages/analytics-js-common/src/utilities/errors.ts b/packages/analytics-js-common/src/utilities/errors.ts index 9efdf1501..f6f69e6f8 100644 --- a/packages/analytics-js-common/src/utilities/errors.ts +++ b/packages/analytics-js-common/src/utilities/errors.ts @@ -4,12 +4,12 @@ import { stringifyWithoutCircular } from './json'; const MANUAL_ERROR_IDENTIFIER = '[SDK DISPATCHED ERROR]'; const getStacktrace = (err: any): string | undefined => { - const { stack, stacktrace, name, message } = err; + const { stack, stacktrace } = err; const operaSourceloc = err['opera#sourceloc']; const stackString = stack ?? stacktrace ?? operaSourceloc; - if (!!stackString && typeof stackString === 'string' && stack !== `${name}: ${message}`) { + if (!!stackString && typeof stackString === 'string') { return stackString; } return undefined; From cd9b9e944865d5e7f0575b7127ebce23f1193f09 Mon Sep 17 00:00:00 2001 From: Sai Kumar Battinoju Date: Thu, 9 Jan 2025 18:44:55 +0530 Subject: [PATCH 33/53] fix: add custom message separator --- .../__tests__/services/ErrorHandler/ErrorHandler.test.ts | 2 +- packages/analytics-js/src/services/ErrorHandler/ErrorHandler.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/analytics-js/__tests__/services/ErrorHandler/ErrorHandler.test.ts b/packages/analytics-js/__tests__/services/ErrorHandler/ErrorHandler.test.ts index c85fff839..a54688ba0 100644 --- a/packages/analytics-js/__tests__/services/ErrorHandler/ErrorHandler.test.ts +++ b/packages/analytics-js/__tests__/services/ErrorHandler/ErrorHandler.test.ts @@ -124,7 +124,7 @@ describe('ErrorHandler', () => { errorHandlerInstance.onError(new Error('dummy error'), 'Test', 'Custom Message'); expect(defaultLogger.error).toHaveBeenCalledTimes(1); - expect(defaultLogger.error).toHaveBeenCalledWith('Test:: Custom Message dummy error'); + expect(defaultLogger.error).toHaveBeenCalledWith('Test:: Custom Message - dummy error'); }); it('should not log unhandled errors to the console', () => { diff --git a/packages/analytics-js/src/services/ErrorHandler/ErrorHandler.ts b/packages/analytics-js/src/services/ErrorHandler/ErrorHandler.ts index ce19fb405..8b2842bbd 100644 --- a/packages/analytics-js/src/services/ErrorHandler/ErrorHandler.ts +++ b/packages/analytics-js/src/services/ErrorHandler/ErrorHandler.ts @@ -67,7 +67,7 @@ class ErrorHandler implements IErrorHandler { return; } - const errorMsgPrefix = `${context}${LOG_CONTEXT_SEPARATOR}${customMessage ? `${customMessage} ` : ''}`; + const errorMsgPrefix = `${context}${LOG_CONTEXT_SEPARATOR}${customMessage ? `${customMessage} - ` : ''}`; const bsException = createBugsnagException(normalizedError, errorMsgPrefix); const stacktrace = getStacktrace(normalizedError); From f623d4bf7cc0abfcff7cfbd6b36fd23d32934313 Mon Sep 17 00:00:00 2001 From: Sai Kumar Battinoju Date: Thu, 9 Jan 2025 18:45:25 +0530 Subject: [PATCH 34/53] fix: filter only unhandled errors --- .../__tests__/services/ErrorHandler/ErrorHandler.test.ts | 5 +++-- .../analytics-js/src/services/ErrorHandler/ErrorHandler.ts | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/analytics-js/__tests__/services/ErrorHandler/ErrorHandler.test.ts b/packages/analytics-js/__tests__/services/ErrorHandler/ErrorHandler.test.ts index a54688ba0..74d98f577 100644 --- a/packages/analytics-js/__tests__/services/ErrorHandler/ErrorHandler.test.ts +++ b/packages/analytics-js/__tests__/services/ErrorHandler/ErrorHandler.test.ts @@ -108,9 +108,10 @@ describe('ErrorHandler', () => { expect(defaultLogger.error).toHaveBeenCalledTimes(0); }); - it('should skip errors if they are not originated from the sdk', () => { + it('should skip unhandled errors if they are not originated from the sdk', () => { // For this error, the stacktrace would not contain the sdk file names - errorHandlerInstance.onError(new Error('dummy error')); + // @ts-expect-error not using the enum value for testing + errorHandlerInstance.onError(new Error('dummy error'), '', '', 'unhandledException'); // It should not be logged to the console expect(defaultLogger.error).toHaveBeenCalledTimes(0); diff --git a/packages/analytics-js/src/services/ErrorHandler/ErrorHandler.ts b/packages/analytics-js/src/services/ErrorHandler/ErrorHandler.ts index 8b2842bbd..9a355232b 100644 --- a/packages/analytics-js/src/services/ErrorHandler/ErrorHandler.ts +++ b/packages/analytics-js/src/services/ErrorHandler/ErrorHandler.ts @@ -73,12 +73,13 @@ class ErrorHandler implements IErrorHandler { const stacktrace = getStacktrace(normalizedError); const isSdkDispatched = stacktrace?.includes(MANUAL_ERROR_IDENTIFIER); - // Filter errors that are not originated in the SDK. + // Filter unhandled errors that are not originated in the SDK. // However, in case of NPM installations, since we cannot differentiate between SDK and application errors, we should report all errors. if ( !isSDKError(bsException) && state.context.app.value.installType !== 'npm' && - !isSdkDispatched + !isSdkDispatched && + errorType !== ErrorType.HANDLEDEXCEPTION ) { return; } From 56afb078e693827d2795d4ae37306aa75b8b6916 Mon Sep 17 00:00:00 2001 From: Sai Kumar Battinoju Date: Thu, 9 Jan 2025 19:14:49 +0530 Subject: [PATCH 35/53] fix: callback invocations --- .../src/constants/loggerContexts.ts | 8 +- .../eventRepository/EventRepository.test.ts | 77 ++++++++++++------- .../src/components/core/Analytics.ts | 31 +++++--- .../eventRepository/EventRepository.ts | 28 +++++-- .../analytics-js/src/constants/logMessages.ts | 13 ++-- 5 files changed, 101 insertions(+), 56 deletions(-) diff --git a/packages/analytics-js-common/src/constants/loggerContexts.ts b/packages/analytics-js-common/src/constants/loggerContexts.ts index 65583c977..43acbba1d 100644 --- a/packages/analytics-js-common/src/constants/loggerContexts.ts +++ b/packages/analytics-js-common/src/constants/loggerContexts.ts @@ -1,3 +1,4 @@ +const API_SUFFIX = 'API'; const CAPABILITIES_MANAGER = 'CapabilitiesManager'; const CONFIG_MANAGER = 'ConfigManager'; const EVENT_MANAGER = 'EventManager'; @@ -6,8 +7,8 @@ const USER_SESSION_MANAGER = 'UserSessionManager'; const ERROR_HANDLER = 'ErrorHandler'; const PLUGIN_ENGINE = 'PluginEngine'; const STORE_MANAGER = 'StoreManager'; -const READY_API = 'readyApi'; -const LOAD_CONFIGURATION = 'LoadConfiguration'; +const READY_API = `Ready${API_SUFFIX}`; +const LOAD_API = `Load${API_SUFFIX}`; const EVENT_REPOSITORY = 'EventRepository'; const EXTERNAL_SRC_LOADER = 'ExternalSrcLoader'; const HTTP_CLIENT = 'HttpClient'; @@ -24,10 +25,11 @@ export { PLUGIN_ENGINE, STORE_MANAGER, READY_API, - LOAD_CONFIGURATION, + LOAD_API, EVENT_REPOSITORY, EXTERNAL_SRC_LOADER, HTTP_CLIENT, RSA, ANALYTICS_CORE, + API_SUFFIX, }; diff --git a/packages/analytics-js/__tests__/components/eventRepository/EventRepository.test.ts b/packages/analytics-js/__tests__/components/eventRepository/EventRepository.test.ts index 1e725716b..7d6376e66 100644 --- a/packages/analytics-js/__tests__/components/eventRepository/EventRepository.test.ts +++ b/packages/analytics-js/__tests__/components/eventRepository/EventRepository.test.ts @@ -5,14 +5,13 @@ import type { DestinationConfig, } from '@rudderstack/analytics-js-common/types/Destination'; import type { RudderEvent } from '@rudderstack/analytics-js-common/types/Event'; -import type { IErrorHandler } from '@rudderstack/analytics-js-common/types/ErrorHandler'; +import { defaultErrorHandler } from '@rudderstack/analytics-js-common/__mocks__/ErrorHandler'; +import { defaultLogger } from '@rudderstack/analytics-js-common/__mocks__/Logger'; import { defaultHttpClient } from '../../../src/services/HttpClient'; import { EventRepository } from '../../../src/components/eventRepository'; import { state, resetState } from '../../../src/state'; import { PluginsManager } from '../../../src/components/pluginsManager'; import { defaultPluginEngine } from '../../../src/services/PluginEngine'; -import { defaultErrorHandler } from '../../../src/services/ErrorHandler'; -import { defaultLogger } from '../../../src/services/Logger'; import { StoreManager } from '../../../src/services/StoreManager'; describe('EventRepository', () => { @@ -22,7 +21,11 @@ describe('EventRepository', () => { defaultLogger, ); - const defaultStoreManager = new StoreManager(defaultPluginsManager); + const defaultStoreManager = new StoreManager( + defaultPluginsManager, + defaultErrorHandler, + defaultLogger, + ); const mockDestinationsEventsQueue = { scheduleTimeoutActive: false, @@ -95,38 +98,40 @@ describe('EventRepository', () => { defaultPluginsManager, defaultStoreManager, defaultHttpClient, + defaultErrorHandler, + defaultLogger, ); const spy = jest.spyOn(defaultPluginsManager, 'invokeSingle'); eventRepository.init(); - expect(spy).nthCalledWith( + expect(spy).toHaveBeenNthCalledWith( 1, 'dataplaneEventsQueue.init', state, expect.objectContaining({}), defaultStoreManager, - undefined, - undefined, + defaultErrorHandler, + defaultLogger, ); - expect(spy).nthCalledWith( + expect(spy).toHaveBeenNthCalledWith( 2, 'transformEvent.init', state, defaultPluginsManager, expect.objectContaining({}), defaultStoreManager, - undefined, - undefined, + defaultErrorHandler, + defaultLogger, ); - expect(spy).nthCalledWith( + expect(spy).toHaveBeenNthCalledWith( 3, 'destinationsEventsQueue.init', state, defaultPluginsManager, defaultStoreManager, undefined, - undefined, - undefined, + defaultErrorHandler, + defaultLogger, ); spy.mockRestore(); }); @@ -136,6 +141,8 @@ describe('EventRepository', () => { mockPluginsManager, defaultStoreManager, defaultHttpClient, + defaultErrorHandler, + defaultLogger, ); eventRepository.init(); @@ -150,6 +157,8 @@ describe('EventRepository', () => { mockPluginsManager, defaultStoreManager, defaultHttpClient, + defaultErrorHandler, + defaultLogger, ); state.nativeDestinations.activeDestinations.value = [ @@ -181,6 +190,8 @@ describe('EventRepository', () => { mockPluginsManager, defaultStoreManager, defaultHttpClient, + defaultErrorHandler, + defaultLogger, ); state.nativeDestinations.activeDestinations.value = activeDestinationsWithHybridMode; @@ -197,6 +208,8 @@ describe('EventRepository', () => { mockPluginsManager, defaultStoreManager, defaultHttpClient, + defaultErrorHandler, + defaultLogger, ); state.nativeDestinations.activeDestinations.value = activeDestinationsWithHybridMode; @@ -220,6 +233,8 @@ describe('EventRepository', () => { mockPluginsManager, defaultStoreManager, defaultHttpClient, + defaultErrorHandler, + defaultLogger, ); state.nativeDestinations.activeDestinations.value = activeDestinationsWithHybridMode; @@ -242,6 +257,8 @@ describe('EventRepository', () => { mockPluginsManager, defaultStoreManager, defaultHttpClient, + defaultErrorHandler, + defaultLogger, ); eventRepository.init(); @@ -249,7 +266,7 @@ describe('EventRepository', () => { const invokeSingleSpy = jest.spyOn(mockPluginsManager, 'invokeSingle'); eventRepository.enqueue(testEvent); - expect(invokeSingleSpy).nthCalledWith( + expect(invokeSingleSpy).toHaveBeenNthCalledWith( 1, 'dataplaneEventsQueue.enqueue', state, @@ -258,17 +275,17 @@ describe('EventRepository', () => { ...testEvent, integrations: { All: true }, }, - undefined, - undefined, + defaultErrorHandler, + defaultLogger, ); - expect(invokeSingleSpy).nthCalledWith( + expect(invokeSingleSpy).toHaveBeenNthCalledWith( 2, 'destinationsEventsQueue.enqueue', state, mockDestinationsEventsQueue, testEvent, - undefined, - undefined, + defaultErrorHandler, + defaultLogger, ); invokeSingleSpy.mockRestore(); @@ -279,6 +296,8 @@ describe('EventRepository', () => { mockPluginsManager, defaultStoreManager, defaultHttpClient, + defaultErrorHandler, + defaultLogger, ); eventRepository.init(); @@ -294,15 +313,12 @@ describe('EventRepository', () => { }); it('should handle error if event callback function throws', () => { - const mockErrorHandler = { - onError: jest.fn(), - } as unknown as IErrorHandler; - const eventRepository = new EventRepository( mockPluginsManager, defaultStoreManager, defaultHttpClient, - mockErrorHandler, + defaultErrorHandler, + defaultLogger, ); eventRepository.init(); @@ -312,11 +328,10 @@ describe('EventRepository', () => { }); eventRepository.enqueue(testEvent, mockEventCallback); - expect(mockErrorHandler.onError).toHaveBeenCalledTimes(1); - expect(mockErrorHandler.onError).toHaveBeenCalledWith( + expect(defaultLogger.error).toHaveBeenCalledTimes(1); + expect(defaultLogger.error).toHaveBeenCalledWith( + 'TrackAPI:: The callback threw an exception', new Error('test error'), - 'EventRepository', - 'API Callback Invocation Failed', ); }); @@ -325,6 +340,8 @@ describe('EventRepository', () => { mockPluginsManager, defaultStoreManager, defaultHttpClient, + defaultErrorHandler, + defaultLogger, ); state.consents.preConsent.value = { @@ -348,6 +365,8 @@ describe('EventRepository', () => { mockPluginsManager, defaultStoreManager, defaultHttpClient, + defaultErrorHandler, + defaultLogger, ); eventRepository.init(); @@ -360,6 +379,8 @@ describe('EventRepository', () => { mockPluginsManager, defaultStoreManager, defaultHttpClient, + defaultErrorHandler, + defaultLogger, ); state.consents.postConsent.value.discardPreConsentEvents = true; diff --git a/packages/analytics-js/src/components/core/Analytics.ts b/packages/analytics-js/src/components/core/Analytics.ts index 609dd445f..e7a79089b 100644 --- a/packages/analytics-js/src/components/core/Analytics.ts +++ b/packages/analytics-js/src/components/core/Analytics.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ import { ExternalSrcLoader } from '@rudderstack/analytics-js-common/services/ExternalSrcLoader'; import { batch, effect } from '@preact/signals-core'; -import { isFunction, isNull } from '@rudderstack/analytics-js-common/utilities/checks'; +import { isDefined, isFunction, isNull } from '@rudderstack/analytics-js-common/utilities/checks'; import type { IHttpClient } from '@rudderstack/analytics-js-common/types/HttpClient'; import { clone } from 'ramda'; import type { ILogger } from '@rudderstack/analytics-js-common/types/Logger'; @@ -16,10 +16,12 @@ import type { AnonymousIdOptions, ConsentOptions, LoadOptions, + OnLoadedCallback, } from '@rudderstack/analytics-js-common/types/LoadOptions'; import type { ApiCallback } from '@rudderstack/analytics-js-common/types/EventApi'; import { ANALYTICS_CORE, + LOAD_API, READY_API, } from '@rudderstack/analytics-js-common/constants/loggerContexts'; import { @@ -61,9 +63,9 @@ import { } from '../../constants/app'; import { DATA_PLANE_URL_VALIDATION_ERROR, - READY_API_CALLBACK_ERROR, - READY_CALLBACK_INVOKE_ERROR, + INVALID_CALLBACK_FN_ERROR, WRITE_KEY_VALIDATION_ERROR, + CALLBACK_INVOKE_ERROR, } from '../../constants/logMessages'; import type { IAnalytics } from './IAnalytics'; import { getConsentManagementData, getValidPostConsentOptions } from '../utilities/consent'; @@ -319,11 +321,20 @@ class Analytics implements IAnalytics { // Process any preloaded events this.processDataInPreloadBuffer(); - // TODO: we need to avoid passing the window object to the callback function - // as this will prevent us from supporting multiple SDK instances in the same page // Execute onLoaded callback if provided in load options - if (isFunction(state.loadOptions.value.onLoaded)) { - state.loadOptions.value.onLoaded((globalThis as typeof window).rudderanalytics); + const onLoadedCallbackFn = state.loadOptions.value.onLoaded; + if (isDefined(onLoadedCallbackFn)) { + if (isFunction(onLoadedCallbackFn)) { + // TODO: we need to avoid passing the window object to the callback function + // as this will prevent us from supporting multiple SDK instances in the same page + try { + (onLoadedCallbackFn as OnLoadedCallback)((globalThis as typeof window).rudderanalytics); + } catch (err) { + this.logger.error(CALLBACK_INVOKE_ERROR(LOAD_API), err); + } + } else { + this.logger.error(INVALID_CALLBACK_FN_ERROR(LOAD_API)); + } } // Set lifecycle state @@ -348,7 +359,7 @@ class Analytics implements IAnalytics { try { callback(); } catch (err) { - this.errorHandler.onError(err, ANALYTICS_CORE, READY_CALLBACK_INVOKE_ERROR); + this.logger.error(CALLBACK_INVOKE_ERROR(READY_API), err); } }); @@ -459,7 +470,7 @@ class Analytics implements IAnalytics { this.errorHandler.leaveBreadcrumb(`New ${type} invocation`); if (!isFunction(callback)) { - this.logger.error(READY_API_CALLBACK_ERROR(READY_API)); + this.logger.error(INVALID_CALLBACK_FN_ERROR(READY_API)); return; } @@ -472,7 +483,7 @@ class Analytics implements IAnalytics { try { callback(); } catch (err) { - this.errorHandler.onError(err, ANALYTICS_CORE, READY_CALLBACK_INVOKE_ERROR); + this.logger.error(CALLBACK_INVOKE_ERROR(READY_API), err); } } else { state.eventBuffer.readyCallbacksArray.value = [ diff --git a/packages/analytics-js/src/components/eventRepository/EventRepository.ts b/packages/analytics-js/src/components/eventRepository/EventRepository.ts index c01dfa3ca..dec5f4251 100644 --- a/packages/analytics-js/src/components/eventRepository/EventRepository.ts +++ b/packages/analytics-js/src/components/eventRepository/EventRepository.ts @@ -8,13 +8,18 @@ import type { ILogger } from '@rudderstack/analytics-js-common/types/Logger'; import type { RudderEvent } from '@rudderstack/analytics-js-common/types/Event'; import type { ApiCallback } from '@rudderstack/analytics-js-common/types/EventApi'; import { isHybridModeDestination } from '@rudderstack/analytics-js-common/utilities/destinations'; -import { EVENT_REPOSITORY } from '@rudderstack/analytics-js-common/constants/loggerContexts'; +import { + API_SUFFIX, + EVENT_REPOSITORY, +} from '@rudderstack/analytics-js-common/constants/loggerContexts'; import type { Destination } from '@rudderstack/analytics-js-common/types/Destination'; +import { isDefined, isFunction } from '@rudderstack/analytics-js-common/utilities/checks'; import { - API_CALLBACK_INVOKE_ERROR, + CALLBACK_INVOKE_ERROR, DATAPLANE_PLUGIN_ENQUEUE_ERROR, DATAPLANE_PLUGIN_INITIALIZE_ERROR, DMT_PLUGIN_INITIALIZE_ERROR, + INVALID_CALLBACK_FN_ERROR, NATIVE_DEST_PLUGIN_ENQUEUE_ERROR, NATIVE_DEST_PLUGIN_INITIALIZE_ERROR, } from '../../constants/logMessages'; @@ -198,12 +203,19 @@ class EventRepository implements IEventRepository { } // Invoke the callback if it exists - try { - // Using the event sent to the data plane queue here - // to ensure the mutated (if any) event is sent to the callback - callback?.(dpQEvent); - } catch (error) { - this.onError(error, API_CALLBACK_INVOKE_ERROR); + if (isDefined(callback)) { + const apiName = `${event.type.charAt(0).toUpperCase()}${event.type.slice(1)}${API_SUFFIX}`; + if (isFunction(callback)) { + try { + // Using the event sent to the data plane queue here + // to ensure the mutated (if any) event is sent to the callback + callback?.(dpQEvent); + } catch (error) { + this.logger.error(CALLBACK_INVOKE_ERROR(apiName), error); + } + } else { + this.logger.error(INVALID_CALLBACK_FN_ERROR(apiName)); + } } } diff --git a/packages/analytics-js/src/constants/logMessages.ts b/packages/analytics-js/src/constants/logMessages.ts index 3b8991eab..8761b3c88 100644 --- a/packages/analytics-js/src/constants/logMessages.ts +++ b/packages/analytics-js/src/constants/logMessages.ts @@ -78,8 +78,8 @@ const WRITE_KEY_VALIDATION_ERROR = (context: string, writeKey: string): string = const DATA_PLANE_URL_VALIDATION_ERROR = (context: string, dataPlaneUrl: string): string => `${context}${LOG_CONTEXT_SEPARATOR}The data plane URL "${dataPlaneUrl}" is invalid. It must be a valid URL string. Please check that the data plane URL is correct and try again.`; -const READY_API_CALLBACK_ERROR = (context: string): string => - `${context}${LOG_CONTEXT_SEPARATOR}The provided callback is not a function.`; +const INVALID_CALLBACK_FN_ERROR = (context: string): string => + `${context}${LOG_CONTEXT_SEPARATOR}The provided callback is not invokable.`; const XHR_DELIVERY_ERROR = ( prefix: string, @@ -199,9 +199,9 @@ const STORAGE_UNAVAILABLE_WARNING = ( ): string => `${context}${LOG_CONTEXT_SEPARATOR}The storage type "${selectedStorageType}" is not available for entry "${entry}". The SDK will initialize the entry with "${finalStorageType}" storage type instead.`; -const READY_CALLBACK_INVOKE_ERROR = `Failed to invoke the ready callback`; +const CALLBACK_INVOKE_ERROR = (context: string): string => + `${context}${LOG_CONTEXT_SEPARATOR}The callback threw an exception`; -const API_CALLBACK_INVOKE_ERROR = `API Callback Invocation Failed`; const NATIVE_DEST_PLUGIN_INITIALIZE_ERROR = `NativeDestinationQueuePlugin initialization failed`; const DATAPLANE_PLUGIN_INITIALIZE_ERROR = `XhrQueuePlugin initialization failed`; const DMT_PLUGIN_INITIALIZE_ERROR = `DeviceModeTransformationPlugin initialization failed`; @@ -288,7 +288,7 @@ export { DATA_PLANE_URL_ERROR, WRITE_KEY_VALIDATION_ERROR, DATA_PLANE_URL_VALIDATION_ERROR, - READY_API_CALLBACK_ERROR, + INVALID_CALLBACK_FN_ERROR, XHR_DELIVERY_ERROR, XHR_REQUEST_ERROR, XHR_SEND_ERROR, @@ -299,8 +299,6 @@ export { PLUGIN_EXT_POINT_MISSING_ERROR, PLUGIN_EXT_POINT_INVALID_ERROR, STORAGE_TYPE_VALIDATION_WARNING, - READY_CALLBACK_INVOKE_ERROR, - API_CALLBACK_INVOKE_ERROR, INVALID_CONFIG_URL_WARNING, POLYFILL_SCRIPT_LOAD_ERROR, UNSUPPORTED_PRE_CONSENT_STORAGE_STRATEGY, @@ -324,4 +322,5 @@ export { PAGE_UNLOAD_ON_BEACON_DISABLED_WARNING, BREADCRUMB_ERROR, NON_ERROR_WARNING, + CALLBACK_INVOKE_ERROR, }; From 85f5ac05683663ca1ddead19b3cff006d4619931 Mon Sep 17 00:00:00 2001 From: Sai Kumar Battinoju Date: Thu, 9 Jan 2025 21:05:46 +0530 Subject: [PATCH 36/53] fix: remove unwanted error handling logic --- .../eventManager/EventManager.test.ts | 19 --- .../eventManager/RudderEventFactory.test.ts | 10 -- .../services/HttpClient/HttpClient.test.ts | 57 ++----- .../CapabilitiesManager.ts | 8 +- .../components/configManager/ConfigManager.ts | 4 +- .../components/eventManager/EventManager.ts | 17 +-- .../eventManager/RudderEventFactory.ts | 6 +- .../eventRepository/EventRepository.ts | 140 ++++++------------ .../pluginsManager/PluginsManager.ts | 11 +- .../analytics-js/src/constants/logMessages.ts | 18 +-- .../src/services/HttpClient/HttpClient.ts | 2 - .../src/services/StoreManager/StoreManager.ts | 8 - 12 files changed, 71 insertions(+), 229 deletions(-) diff --git a/packages/analytics-js/__tests__/components/eventManager/EventManager.test.ts b/packages/analytics-js/__tests__/components/eventManager/EventManager.test.ts index 826b4ab8c..d2ca25482 100644 --- a/packages/analytics-js/__tests__/components/eventManager/EventManager.test.ts +++ b/packages/analytics-js/__tests__/components/eventManager/EventManager.test.ts @@ -53,25 +53,6 @@ describe('EventManager', () => { }); }); - describe('addEvent', () => { - it('should raise error if the event data is invalid', () => { - eventManager.addEvent({ - // @ts-ignore - type: 'test', - event: 'test', - properties: { - test: 'test', - }, - }); - - expect(mockErrorHandler.onError).toHaveBeenCalledWith( - new Error('Failed to generate the event object.'), - 'EventManager', - undefined, - ); - }); - }); - describe('resume', () => { it('should resume on resume', () => { const eventRepositoryResumeSpy = jest.spyOn(eventRepository, 'resume'); diff --git a/packages/analytics-js/__tests__/components/eventManager/RudderEventFactory.test.ts b/packages/analytics-js/__tests__/components/eventManager/RudderEventFactory.test.ts index 9b9b76887..4c614ec83 100644 --- a/packages/analytics-js/__tests__/components/eventManager/RudderEventFactory.test.ts +++ b/packages/analytics-js/__tests__/components/eventManager/RudderEventFactory.test.ts @@ -503,14 +503,4 @@ describe('RudderEventFactory', () => { event: null, }); }); - - it('should not generate any event if the event type is not supported', () => { - const apiEvent = { - type: 'test', - } as unknown as APIEvent; - - const testEvent = rudderEventFactory.create(apiEvent); - - expect(testEvent).toBeUndefined(); - }); }); diff --git a/packages/analytics-js/__tests__/services/HttpClient/HttpClient.test.ts b/packages/analytics-js/__tests__/services/HttpClient/HttpClient.test.ts index f480ae9f8..bf7fc49c3 100644 --- a/packages/analytics-js/__tests__/services/HttpClient/HttpClient.test.ts +++ b/packages/analytics-js/__tests__/services/HttpClient/HttpClient.test.ts @@ -1,35 +1,10 @@ import type { ResponseDetails } from '@rudderstack/analytics-js-common/types/HttpClient'; +import { defaultLogger } from '@rudderstack/analytics-js-common/__mocks__/Logger'; +import { defaultErrorHandler } from '@rudderstack/analytics-js-common/__mocks__/ErrorHandler'; import { HttpClient } from '../../../src/services/HttpClient'; -import { defaultErrorHandler } from '../../../src/services/ErrorHandler'; -import { defaultLogger } from '../../../src/services/Logger'; import { server } from '../../../__fixtures__/msw.server'; import { dummyDataplaneHost } from '../../../__fixtures__/fixtures'; -jest.mock('../../../src/services/Logger', () => { - const originalModule = jest.requireActual('../../../src/services/Logger'); - - return { - __esModule: true, - ...originalModule, - defaultLogger: { - error: jest.fn((): void => {}), - warn: jest.fn((): void => {}), - }, - }; -}); - -jest.mock('../../../src/services/ErrorHandler', () => { - const originalModule = jest.requireActual('../../../src/services/ErrorHandler'); - - return { - __esModule: true, - ...originalModule, - defaultErrorHandler: { - onError: jest.fn((): void => {}), - }, - }; -}); - describe('HttpClient', () => { let clientInstance: HttpClient; @@ -125,13 +100,11 @@ describe('HttpClient', () => { }); it('should handle 400 range errors in getAsyncData requests', done => { - const callback = (response: any, reject: ResponseDetails) => { + const callback = (data: any, details: ResponseDetails) => { const errResult = new Error( 'The request failed with status: 404, Not Found for URL: https://dummy.dataplane.host.com/404ErrorSample.', ); - expect(reject.error).toEqual(errResult); - expect(defaultErrorHandler.onError).toHaveBeenCalledTimes(1); - expect(defaultErrorHandler.onError).toHaveBeenCalledWith(errResult, 'HttpClient'); + expect(details.error).toEqual(errResult); done(); }; clientInstance.getAsyncData({ @@ -145,12 +118,10 @@ describe('HttpClient', () => { url: `${dummyDataplaneHost}/404ErrorSample`, }); expect(response.data).toBeUndefined(); - expect(defaultErrorHandler.onError).toHaveBeenCalledTimes(1); - expect(defaultErrorHandler.onError).toHaveBeenCalledWith( + expect(response.details?.error).toEqual( new Error( 'The request failed with status: 404, Not Found for URL: https://dummy.dataplane.host.com/404ErrorSample.', ), - 'HttpClient', ); }); @@ -160,8 +131,6 @@ describe('HttpClient', () => { 'The request failed with status: 500, Internal Server Error for URL: https://dummy.dataplane.host.com/500ErrorSample.', ); expect(reject.error).toEqual(errResult); - expect(defaultErrorHandler.onError).toHaveBeenCalledTimes(1); - expect(defaultErrorHandler.onError).toHaveBeenCalledWith(errResult, 'HttpClient'); done(); }; clientInstance.getAsyncData({ @@ -175,12 +144,10 @@ describe('HttpClient', () => { url: `${dummyDataplaneHost}/500ErrorSample`, }); expect(response.data).toBeUndefined(); - expect(defaultErrorHandler.onError).toHaveBeenCalledTimes(1); - expect(defaultErrorHandler.onError).toHaveBeenCalledWith( + expect(response.details?.error).toEqual( new Error( 'The request failed with status: 500, Internal Server Error for URL: https://dummy.dataplane.host.com/500ErrorSample.', ), - 'HttpClient', ); }); @@ -189,12 +156,10 @@ describe('HttpClient', () => { url: `${dummyDataplaneHost}/noConnectionSample`, }); expect(response.data).toBeUndefined(); - expect(defaultErrorHandler.onError).toHaveBeenCalledTimes(1); - expect(defaultErrorHandler.onError).toHaveBeenCalledWith( + expect(response.details?.error).toEqual( new Error( 'The request failed due to timeout or no connection (error) for URL: https://dummy.dataplane.host.com/noConnectionSample.', ), - 'HttpClient', ); }); @@ -233,13 +198,9 @@ describe('HttpClient', () => { }); it('should handle if input data contains non-stringifiable values', done => { - const callback = (response: any) => { + const callback = (response: any, details: ResponseDetails) => { expect(response).toBeUndefined(); - expect(defaultErrorHandler.onError).toHaveBeenCalledTimes(1); - expect(defaultErrorHandler.onError).toHaveBeenCalledWith( - new Error('Failed to prepare data for the request.'), - 'HttpClient', - ); + expect(details.error).toEqual(new Error('Failed to prepare data for the request.')); done(); }; clientInstance.getAsyncData({ diff --git a/packages/analytics-js/src/components/capabilitiesManager/CapabilitiesManager.ts b/packages/analytics-js/src/components/capabilitiesManager/CapabilitiesManager.ts index 386d5654a..014eec61d 100644 --- a/packages/analytics-js/src/components/capabilitiesManager/CapabilitiesManager.ts +++ b/packages/analytics-js/src/components/capabilitiesManager/CapabilitiesManager.ts @@ -52,12 +52,8 @@ class CapabilitiesManager implements ICapabilitiesManager { } init() { - try { - this.prepareBrowserCapabilities(); - this.attachWindowListeners(); - } catch (err) { - this.onError(err); - } + this.prepareBrowserCapabilities(); + this.attachWindowListeners(); } /** diff --git a/packages/analytics-js/src/components/configManager/ConfigManager.ts b/packages/analytics-js/src/components/configManager/ConfigManager.ts index 90a3245d1..ebbb2731d 100644 --- a/packages/analytics-js/src/components/configManager/ConfigManager.ts +++ b/packages/analytics-js/src/components/configManager/ConfigManager.ts @@ -4,7 +4,7 @@ import type { ResponseDetails, } from '@rudderstack/analytics-js-common/types/HttpClient'; import { batch, effect } from '@preact/signals-core'; -import { isFunction, isString } from '@rudderstack/analytics-js-common/utilities/checks'; +import { isFunction, isNull, isString } from '@rudderstack/analytics-js-common/utilities/checks'; import type { IErrorHandler } from '@rudderstack/analytics-js-common/types/ErrorHandler'; import type { Destination } from '@rudderstack/analytics-js-common/types/Destination'; import type { ILogger } from '@rudderstack/analytics-js-common/types/Logger'; @@ -77,7 +77,7 @@ class ConfigManager implements IConfigManager { this.logger, ); - if (intgCdnUrl === null) { + if (isNull(intgCdnUrl)) { return; } diff --git a/packages/analytics-js/src/components/eventManager/EventManager.ts b/packages/analytics-js/src/components/eventManager/EventManager.ts index b3c7fcc2b..fc76c2b2a 100644 --- a/packages/analytics-js/src/components/eventManager/EventManager.ts +++ b/packages/analytics-js/src/components/eventManager/EventManager.ts @@ -1,8 +1,6 @@ import type { ILogger } from '@rudderstack/analytics-js-common/types/Logger'; import type { IErrorHandler } from '@rudderstack/analytics-js-common/types/ErrorHandler'; import type { APIEvent } from '@rudderstack/analytics-js-common/types/EventApi'; -import { EVENT_MANAGER } from '@rudderstack/analytics-js-common/constants/loggerContexts'; -import { EVENT_OBJECT_GENERATION_ERROR } from '../../constants/logMessages'; import type { IEventManager } from './types'; import { RudderEventFactory } from './RudderEventFactory'; import type { IEventRepository } from '../eventRepository/types'; @@ -36,7 +34,6 @@ class EventManager implements IEventManager { this.errorHandler = errorHandler; this.logger = logger; this.eventFactory = new RudderEventFactory(this.logger); - this.onError = this.onError.bind(this); } /** @@ -57,19 +54,7 @@ class EventManager implements IEventManager { addEvent(event: APIEvent) { this.userSessionManager.refreshSession(); const rudderEvent = this.eventFactory.create(event); - if (rudderEvent) { - this.eventRepository.enqueue(rudderEvent, event.callback); - } else { - this.onError(new Error(EVENT_OBJECT_GENERATION_ERROR)); - } - } - - /** - * Handles error - * @param error The error object - */ - onError(error: unknown, customMessage?: string): void { - this.errorHandler.onError(error, EVENT_MANAGER, customMessage); + this.eventRepository.enqueue(rudderEvent, event.callback); } } diff --git a/packages/analytics-js/src/components/eventManager/RudderEventFactory.ts b/packages/analytics-js/src/components/eventManager/RudderEventFactory.ts index b2ab84066..263da80d9 100644 --- a/packages/analytics-js/src/components/eventManager/RudderEventFactory.ts +++ b/packages/analytics-js/src/components/eventManager/RudderEventFactory.ts @@ -129,7 +129,7 @@ class RudderEventFactory { * @param event API event parameters object * @returns A RudderEvent object */ - create(event: APIEvent): RudderEvent | undefined { + create(event: APIEvent): RudderEvent { let eventObj: RudderEvent | undefined; switch (event.type) { case 'page': @@ -150,10 +150,8 @@ class RudderEventFactory { eventObj = this.generateAliasEvent(event.to as string, event.from, event.options); break; case 'group': - eventObj = this.generateGroupEvent(event.groupId, event.traits, event.options); - break; default: - // Do nothing + eventObj = this.generateGroupEvent(event.groupId, event.traits, event.options); break; } return eventObj; diff --git a/packages/analytics-js/src/components/eventRepository/EventRepository.ts b/packages/analytics-js/src/components/eventRepository/EventRepository.ts index dec5f4251..c52632722 100644 --- a/packages/analytics-js/src/components/eventRepository/EventRepository.ts +++ b/packages/analytics-js/src/components/eventRepository/EventRepository.ts @@ -8,21 +8,10 @@ import type { ILogger } from '@rudderstack/analytics-js-common/types/Logger'; import type { RudderEvent } from '@rudderstack/analytics-js-common/types/Event'; import type { ApiCallback } from '@rudderstack/analytics-js-common/types/EventApi'; import { isHybridModeDestination } from '@rudderstack/analytics-js-common/utilities/destinations'; -import { - API_SUFFIX, - EVENT_REPOSITORY, -} from '@rudderstack/analytics-js-common/constants/loggerContexts'; +import { API_SUFFIX } from '@rudderstack/analytics-js-common/constants/loggerContexts'; import type { Destination } from '@rudderstack/analytics-js-common/types/Destination'; import { isDefined, isFunction } from '@rudderstack/analytics-js-common/utilities/checks'; -import { - CALLBACK_INVOKE_ERROR, - DATAPLANE_PLUGIN_ENQUEUE_ERROR, - DATAPLANE_PLUGIN_INITIALIZE_ERROR, - DMT_PLUGIN_INITIALIZE_ERROR, - INVALID_CALLBACK_FN_ERROR, - NATIVE_DEST_PLUGIN_ENQUEUE_ERROR, - NATIVE_DEST_PLUGIN_INITIALIZE_ERROR, -} from '../../constants/logMessages'; +import { CALLBACK_INVOKE_ERROR, INVALID_CALLBACK_FN_ERROR } from '../../constants/logMessages'; import { state } from '../../state'; import type { IEventRepository } from './types'; import { @@ -64,53 +53,40 @@ class EventRepository implements IEventRepository { this.httpClient = httpClient; this.logger = logger; this.storeManager = storeManager; - this.onError = this.onError.bind(this); } /** * Initializes the event repository */ init(): void { - try { - this.dataplaneEventsQueue = this.pluginsManager.invokeSingle( - `${DATA_PLANE_QUEUE_EXT_POINT_PREFIX}.init`, - state, - this.httpClient, - this.storeManager, - this.errorHandler, - this.logger, - ); - } catch (e) { - this.onError(e, DATAPLANE_PLUGIN_INITIALIZE_ERROR); - } - - try { - this.dmtEventsQueue = this.pluginsManager.invokeSingle( - `${DMT_EXT_POINT_PREFIX}.init`, - state, - this.pluginsManager, - this.httpClient, - this.storeManager, - this.errorHandler, - this.logger, - ); - } catch (e) { - this.onError(e, DMT_PLUGIN_INITIALIZE_ERROR); - } - - try { - this.destinationsEventsQueue = this.pluginsManager.invokeSingle( - `${DESTINATIONS_QUEUE_EXT_POINT_PREFIX}.init`, - state, - this.pluginsManager, - this.storeManager, - this.dmtEventsQueue, - this.errorHandler, - this.logger, - ); - } catch (e) { - this.onError(e, NATIVE_DEST_PLUGIN_INITIALIZE_ERROR); - } + this.dataplaneEventsQueue = this.pluginsManager.invokeSingle( + `${DATA_PLANE_QUEUE_EXT_POINT_PREFIX}.init`, + state, + this.httpClient, + this.storeManager, + this.errorHandler, + this.logger, + ); + + this.dmtEventsQueue = this.pluginsManager.invokeSingle( + `${DMT_EXT_POINT_PREFIX}.init`, + state, + this.pluginsManager, + this.httpClient, + this.storeManager, + this.errorHandler, + this.logger, + ); + + this.destinationsEventsQueue = this.pluginsManager.invokeSingle( + `${DESTINATIONS_QUEUE_EXT_POINT_PREFIX}.init`, + state, + this.pluginsManager, + this.storeManager, + this.dmtEventsQueue, + this.errorHandler, + this.logger, + ); // Start the queue once the client destinations are ready effect(() => { @@ -173,34 +149,25 @@ class EventRepository implements IEventRepository { * @param callback API callback function */ enqueue(event: RudderEvent, callback?: ApiCallback): void { - let dpQEvent; - try { - dpQEvent = getFinalEvent(event, state); - this.pluginsManager.invokeSingle( - `${DATA_PLANE_QUEUE_EXT_POINT_PREFIX}.enqueue`, - state, - this.dataplaneEventsQueue, - dpQEvent, - this.errorHandler, - this.logger, - ); - } catch (e) { - this.onError(e, DATAPLANE_PLUGIN_ENQUEUE_ERROR); - } - - try { - const dQEvent = clone(event); - this.pluginsManager.invokeSingle( - `${DESTINATIONS_QUEUE_EXT_POINT_PREFIX}.enqueue`, - state, - this.destinationsEventsQueue, - dQEvent, - this.errorHandler, - this.logger, - ); - } catch (e) { - this.onError(e, NATIVE_DEST_PLUGIN_ENQUEUE_ERROR); - } + const dpQEvent = getFinalEvent(event, state); + this.pluginsManager.invokeSingle( + `${DATA_PLANE_QUEUE_EXT_POINT_PREFIX}.enqueue`, + state, + this.dataplaneEventsQueue, + dpQEvent, + this.errorHandler, + this.logger, + ); + + const dQEvent = clone(event); + this.pluginsManager.invokeSingle( + `${DESTINATIONS_QUEUE_EXT_POINT_PREFIX}.enqueue`, + state, + this.destinationsEventsQueue, + dQEvent, + this.errorHandler, + this.logger, + ); // Invoke the callback if it exists if (isDefined(callback)) { @@ -218,15 +185,6 @@ class EventRepository implements IEventRepository { } } } - - /** - * Handles error - * @param error The error object - * @param customMessage a message - */ - onError(error: unknown, customMessage?: string): void { - this.errorHandler.onError(error, EVENT_REPOSITORY, customMessage); - } } export { EventRepository }; diff --git a/packages/analytics-js/src/components/pluginsManager/PluginsManager.ts b/packages/analytics-js/src/components/pluginsManager/PluginsManager.ts index 524fb3f25..0882d5786 100644 --- a/packages/analytics-js/src/components/pluginsManager/PluginsManager.ts +++ b/packages/analytics-js/src/components/pluginsManager/PluginsManager.ts @@ -16,6 +16,7 @@ import { isDefined, isFunction } from '@rudderstack/analytics-js-common/utilitie import { DEPRECATED_PLUGIN_WARNING, generateMisconfiguredPluginsWarning, + UNKNOWN_PLUGINS_WARNING, } from '../../constants/logMessages'; import { setExposedGlobal } from '../utilities/globals'; import { state } from '../../state'; @@ -230,15 +231,7 @@ class PluginsManager implements IPluginsManager { }); if (failedPlugins.length > 0) { - this.onError( - new Error( - `Ignoring loading of unknown plugins: ${failedPlugins.join( - ',', - )}. Mandatory plugins: ${Object.keys(getMandatoryPluginsMap()).join( - ',', - )}. Load option plugins: ${state.plugins.pluginsToLoadFromConfig.value.join(',')}`, - ), - ); + this.logger.warn(UNKNOWN_PLUGINS_WARNING(PLUGINS_MANAGER, failedPlugins)); } batch(() => { diff --git a/packages/analytics-js/src/constants/logMessages.ts b/packages/analytics-js/src/constants/logMessages.ts index 8761b3c88..7a49a55fa 100644 --- a/packages/analytics-js/src/constants/logMessages.ts +++ b/packages/analytics-js/src/constants/logMessages.ts @@ -15,7 +15,6 @@ const DATA_PLANE_URL_ERROR = `Failed to load the SDK as the data plane URL could const SOURCE_CONFIG_RESOLUTION_ERROR = `Unable to process/parse source configuration response.`; const SOURCE_DISABLED_ERROR = `The source is disabled. Please enable the source in the dashboard to send events.`; const XHR_PAYLOAD_PREP_ERROR = `Failed to prepare data for the request.`; -const EVENT_OBJECT_GENERATION_ERROR = `Failed to generate the event object.`; const PLUGIN_EXT_POINT_MISSING_ERROR = `Failed to invoke plugin because the extension point name is missing.`; const PLUGIN_EXT_POINT_INVALID_ERROR = `Failed to invoke plugin because the extension point name is invalid.`; @@ -202,13 +201,6 @@ const STORAGE_UNAVAILABLE_WARNING = ( const CALLBACK_INVOKE_ERROR = (context: string): string => `${context}${LOG_CONTEXT_SEPARATOR}The callback threw an exception`; -const NATIVE_DEST_PLUGIN_INITIALIZE_ERROR = `NativeDestinationQueuePlugin initialization failed`; -const DATAPLANE_PLUGIN_INITIALIZE_ERROR = `XhrQueuePlugin initialization failed`; -const DMT_PLUGIN_INITIALIZE_ERROR = `DeviceModeTransformationPlugin initialization failed`; - -const NATIVE_DEST_PLUGIN_ENQUEUE_ERROR = `NativeDestinationQueuePlugin event enqueue failed`; -const DATAPLANE_PLUGIN_ENQUEUE_ERROR = `XhrQueuePlugin event enqueue failed`; - const INVALID_CONFIG_URL_WARNING = (context: string, configUrl: string | undefined): string => `${context}${LOG_CONTEXT_SEPARATOR}The provided source config URL "${configUrl}" is invalid. Using the default source config URL instead.`; @@ -260,6 +252,9 @@ const BAD_COOKIES_WARNING = (key: string) => const PAGE_UNLOAD_ON_BEACON_DISABLED_WARNING = (context: string) => `${context}${LOG_CONTEXT_SEPARATOR}Page Unloaded event can only be tracked when the Beacon transport is active. Please enable "useBeacon" load API option.`; +const UNKNOWN_PLUGINS_WARNING = (context: string, unknownPlugins: string[]) => + `${context}${LOG_CONTEXT_SEPARATOR}Ignoring unknown plugins: ${unknownPlugins.join(', ')}.`; + export { UNSUPPORTED_CONSENT_MANAGER_ERROR, UNSUPPORTED_ERROR_REPORTING_PROVIDER_WARNING, @@ -295,7 +290,6 @@ export { XHR_PAYLOAD_PREP_ERROR, STORE_DATA_SAVE_ERROR, STORE_DATA_FETCH_ERROR, - EVENT_OBJECT_GENERATION_ERROR, PLUGIN_EXT_POINT_MISSING_ERROR, PLUGIN_EXT_POINT_INVALID_ERROR, STORAGE_TYPE_VALIDATION_WARNING, @@ -304,11 +298,6 @@ export { UNSUPPORTED_PRE_CONSENT_STORAGE_STRATEGY, UNSUPPORTED_PRE_CONSENT_EVENTS_DELIVERY_TYPE, SOURCE_CONFIG_RESOLUTION_ERROR, - NATIVE_DEST_PLUGIN_INITIALIZE_ERROR, - DATAPLANE_PLUGIN_INITIALIZE_ERROR, - DMT_PLUGIN_INITIALIZE_ERROR, - NATIVE_DEST_PLUGIN_ENQUEUE_ERROR, - DATAPLANE_PLUGIN_ENQUEUE_ERROR, DATA_SERVER_URL_INVALID_ERROR, DATA_SERVER_REQUEST_FAIL_ERROR, FAILED_SETTING_COOKIE_FROM_SERVER_ERROR, @@ -323,4 +312,5 @@ export { BREADCRUMB_ERROR, NON_ERROR_WARNING, CALLBACK_INVOKE_ERROR, + UNKNOWN_PLUGINS_WARNING, }; diff --git a/packages/analytics-js/src/services/HttpClient/HttpClient.ts b/packages/analytics-js/src/services/HttpClient/HttpClient.ts index 08ff9760a..18525cf83 100644 --- a/packages/analytics-js/src/services/HttpClient/HttpClient.ts +++ b/packages/analytics-js/src/services/HttpClient/HttpClient.ts @@ -51,7 +51,6 @@ class HttpClient implements IHttpClient { details: data, }; } catch (reason) { - this.onError((reason as ResponseDetails).error ?? reason); return { data: undefined, details: reason as ResponseDetails }; } } @@ -73,7 +72,6 @@ class HttpClient implements IHttpClient { } }) .catch((data: ResponseDetails) => { - this.onError(data.error ?? data); if (!isFireAndForget) { callback(undefined, data); } diff --git a/packages/analytics-js/src/services/StoreManager/StoreManager.ts b/packages/analytics-js/src/services/StoreManager/StoreManager.ts index 35005b52b..5d14aacb1 100644 --- a/packages/analytics-js/src/services/StoreManager/StoreManager.ts +++ b/packages/analytics-js/src/services/StoreManager/StoreManager.ts @@ -48,7 +48,6 @@ class StoreManager implements IStoreManager { this.errorHandler = errorHandler; this.logger = logger; this.pluginsManager = pluginsManager; - this.onError = this.onError.bind(this); } /** @@ -215,13 +214,6 @@ class StoreManager implements IStoreManager { getStore(id: StoreId): Store | undefined { return this.stores[id]; } - - /** - * Handle errors - */ - onError(error: unknown) { - this.errorHandler.onError(error, STORE_MANAGER); - } } export { StoreManager }; From 7fd54ba70acac2faff1a98c7b879c161a7a1b3de Mon Sep 17 00:00:00 2001 From: Sai Kumar Battinoju Date: Thu, 9 Jan 2025 22:26:39 +0530 Subject: [PATCH 37/53] test: add more test cases for coverage --- .../pluginsManager/PluginsManager.test.ts | 46 +++++++++++++++---- 1 file changed, 38 insertions(+), 8 deletions(-) diff --git a/packages/analytics-js/__tests__/components/pluginsManager/PluginsManager.test.ts b/packages/analytics-js/__tests__/components/pluginsManager/PluginsManager.test.ts index 94374212d..c67991c8e 100644 --- a/packages/analytics-js/__tests__/components/pluginsManager/PluginsManager.test.ts +++ b/packages/analytics-js/__tests__/components/pluginsManager/PluginsManager.test.ts @@ -1,18 +1,13 @@ +import { defaultErrorHandler } from '@rudderstack/analytics-js-common/__mocks__/ErrorHandler'; +import { defaultLogger } from '@rudderstack/analytics-js-common/__mocks__/Logger'; +import { defaultPluginEngine } from '@rudderstack/analytics-js-common/__mocks__/PluginEngine'; import { PluginsManager } from '../../../src/components/pluginsManager'; -import { defaultErrorHandler } from '../../../src/services/ErrorHandler'; -import { defaultLogger } from '../../../src/services/Logger'; -import { defaultPluginEngine } from '../../../src/services/PluginEngine'; import { state, resetState } from '../../../src/state'; import { defaultOptionalPluginsList } from '../../../src/components/pluginsManager/defaultPluginsList'; let pluginsManager: PluginsManager; describe('PluginsManager', () => { - beforeAll(() => { - defaultLogger.warn = jest.fn(); - defaultLogger.error = jest.fn(); - }); - afterAll(() => { jest.clearAllMocks(); }); @@ -237,5 +232,40 @@ describe('PluginsManager', () => { "PluginsManager:: Storage migration is enabled, but 'StorageMigrator' plugin was not configured to load. Ignore if this was intentional. Otherwise, consider adding it to the 'plugins' load API option.", ); }); + + it('should log a warning if deprecated plugins are configured', () => { + state.plugins.pluginsToLoadFromConfig.value = [ + 'ErrorReporting', + 'Bugsnag', + 'StorageMigrator', + ]; + + expect(pluginsManager.getPluginsToLoadBasedOnConfig().sort(alphabeticalCompare)).toEqual([]); + + // Expect a warning for user not explicitly configuring it + expect(defaultLogger.warn).toHaveBeenCalledTimes(2); + expect(defaultLogger.warn).toHaveBeenCalledWith( + 'PluginsManager:: ErrorReporting plugin is deprecated. Please exclude it from the load API options.', + ); + expect(defaultLogger.warn).toHaveBeenCalledWith( + 'PluginsManager:: Bugsnag plugin is deprecated. Please exclude it from the load API options.', + ); + }); + + it('should log a warning if unknown plugins are configured', () => { + state.plugins.pluginsToLoadFromConfig.value = [ + 'UnknownPlugin1', + 'GoogleLinker', + 'UnknownPlugin2', + ]; + + pluginsManager.setActivePlugins(); + + // Expect a warning for user not explicitly configuring it + expect(defaultLogger.warn).toHaveBeenCalledTimes(1); + expect(defaultLogger.warn).toHaveBeenCalledWith( + 'PluginsManager:: Ignoring unknown plugins: UnknownPlugin1, UnknownPlugin2.', + ); + }); }); }); From 1b0b0dbc7ce3ade523713ca0f59b5de5eaf97574 Mon Sep 17 00:00:00 2001 From: Sai Kumar Battinoju Date: Thu, 9 Jan 2025 22:39:54 +0530 Subject: [PATCH 38/53] test: add more test cases for coverage --- .../components/core/Analytics.test.ts | 72 +++++++++++++++++++ .../src/components/core/Analytics.ts | 2 +- 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/packages/analytics-js/__tests__/components/core/Analytics.test.ts b/packages/analytics-js/__tests__/components/core/Analytics.test.ts index 58525d952..7c115ddd9 100644 --- a/packages/analytics-js/__tests__/components/core/Analytics.test.ts +++ b/packages/analytics-js/__tests__/components/core/Analytics.test.ts @@ -224,6 +224,7 @@ describe('Core - Analytics', () => { expect(state.lifecycle.loaded.value).toBeTruthy(); expect(state.lifecycle.status.value).toBe('loaded'); }); + it('should dispatch RSA initialised event', () => { const dispatchEventSpy = jest.spyOn(window.document, 'dispatchEvent'); state.loadOptions.value.onLoaded = jest.fn(); @@ -233,6 +234,32 @@ describe('Core - Analytics', () => { analyticsInstance: undefined, }); }); + + it('should log an error if the onLoaded callback is not a function', () => { + const errorSpy = jest.spyOn(analytics.logger, 'error'); + // @ts-expect-error testing invalid callback + state.loadOptions.value.onLoaded = true; + + analytics.onInitialized(); + + expect(errorSpy).toHaveBeenCalledTimes(1); + expect(errorSpy).toHaveBeenCalledWith('LoadAPI:: The provided callback is not invokable.'); + }); + + it('should log an error if the onLoaded callback throws an error', () => { + const errorSpy = jest.spyOn(analytics.logger, 'error'); + state.loadOptions.value.onLoaded = () => { + throw new Error('Test error'); + }; + + analytics.onInitialized(); + + expect(errorSpy).toHaveBeenCalledTimes(1); + expect(errorSpy).toHaveBeenCalledWith( + 'LoadAPI:: The callback threw an exception', + new Error('Test error'), + ); + }); }); describe('onDestinationsReady', () => { @@ -259,6 +286,22 @@ describe('Core - Analytics', () => { expect(callback).toHaveBeenCalledTimes(2); }); + it('should log an error if a ready callback throws an error', () => { + const errorSpy = jest.spyOn(analytics.logger, 'error'); + const callback = () => { + throw new Error('Test error'); + }; + state.eventBuffer.readyCallbacksArray.value = [callback, jest.fn()]; + + analytics.onReady(); + + expect(errorSpy).toHaveBeenCalledTimes(1); + expect(errorSpy).toHaveBeenCalledWith( + 'ReadyAPI:: The callback threw an exception', + new Error('Test error'), + ); + }); + it('should ignore calls with no function callback', () => { const leaveBreadcrumbSpy = jest.spyOn(analytics.errorHandler, 'leaveBreadcrumb'); const errorSpy = jest.spyOn(analytics.logger, 'error'); @@ -309,6 +352,35 @@ describe('Core - Analytics', () => { analyticsInstance: undefined, }); }); + + it('should log an error if the provided callback is not a function', () => { + state.lifecycle.loaded.value = true; + + const errorSpy = jest.spyOn(analytics.logger, 'error'); + // @ts-expect-error testing invalid callback + analytics.ready(true); + + expect(errorSpy).toHaveBeenCalledTimes(1); + expect(errorSpy).toHaveBeenCalledWith('ReadyAPI:: The provided callback is not invokable.'); + }); + + it('should log an error if the provided callback throws an error', () => { + state.lifecycle.loaded.value = true; + state.lifecycle.status.value = 'readyExecuted'; + + const errorSpy = jest.spyOn(analytics.logger, 'error'); + const callback = () => { + throw new Error('Test error'); + }; + + analytics.ready(callback); + + expect(errorSpy).toHaveBeenCalledTimes(1); + expect(errorSpy).toHaveBeenCalledWith( + 'ReadyAPI:: The callback threw an exception', + new Error('Test error'), + ); + }); }); describe('page', () => { diff --git a/packages/analytics-js/src/components/core/Analytics.ts b/packages/analytics-js/src/components/core/Analytics.ts index e7a79089b..38ab66dc8 100644 --- a/packages/analytics-js/src/components/core/Analytics.ts +++ b/packages/analytics-js/src/components/core/Analytics.ts @@ -328,7 +328,7 @@ class Analytics implements IAnalytics { // TODO: we need to avoid passing the window object to the callback function // as this will prevent us from supporting multiple SDK instances in the same page try { - (onLoadedCallbackFn as OnLoadedCallback)((globalThis as typeof window).rudderanalytics); + onLoadedCallbackFn((globalThis as typeof window).rudderanalytics); } catch (err) { this.logger.error(CALLBACK_INVOKE_ERROR(LOAD_API), err); } From e67f70ef5f88e3a209ba5cf62d221dd1803e0647 Mon Sep 17 00:00:00 2001 From: Sai Kumar Battinoju Date: Thu, 9 Jan 2025 23:31:09 +0530 Subject: [PATCH 39/53] test: add more test cases for improving coverage of error handler --- .../services/ErrorHandler/utils.test.ts | 72 +++++++++++++++++++ .../src/services/ErrorHandler/ErrorHandler.ts | 9 +-- .../src/services/ErrorHandler/event/event.ts | 2 +- .../src/services/ErrorHandler/utils.ts | 34 ++++++--- .../analytics-js/src/state/slices/context.ts | 2 +- 5 files changed, 103 insertions(+), 16 deletions(-) diff --git a/packages/analytics-js/__tests__/services/ErrorHandler/utils.test.ts b/packages/analytics-js/__tests__/services/ErrorHandler/utils.test.ts index 54746ffbe..16c5b865c 100644 --- a/packages/analytics-js/__tests__/services/ErrorHandler/utils.test.ts +++ b/packages/analytics-js/__tests__/services/ErrorHandler/utils.test.ts @@ -8,10 +8,12 @@ import { createNewBreadcrumb, getAppStateForMetadata, getBugsnagErrorEvent, + getDeviceDetails, getErrInstance, getErrorDeliveryPayload, getReleaseStage, getURLWithoutQueryString, + getUserDetails, isAllowedToBeNotified, isSDKError, } from '../../../src/services/ErrorHandler/utils'; @@ -115,6 +117,13 @@ describe('Error Reporting utilities', () => { expect(isSDKError(event as unknown as Exception)).toBe(expectedValue); }, ); + + // Test when stacktrace is empty array + const exception = { + stacktrace: [], + }; + + expect(isSDKError(exception as unknown as Exception)).toBe(false); }); describe('getAppStateForMetadata', () => { @@ -641,4 +650,67 @@ describe('Error Reporting utilities', () => { expect(result).toEqual(errorEvent.reason); }); }); + + describe('getUserDetails', () => { + it('should return user details for the given source, session, lifecycle, and autoTrack', () => { + state.source.value = { + id: 'dummy-source-id', + name: 'dummy-source-name', + workspaceId: 'dummy-workspace-id', + }; + state.session.sessionInfo.value = { id: 123 }; + state.autoTrack.pageLifecycle.visitId.value = 'test-visit-id'; + + const userDetails = getUserDetails( + state.source, + state.session, + state.lifecycle, + state.autoTrack, + ); + expect(userDetails).toEqual({ + id: 'dummy-source-id..123..test-visit-id', + name: 'dummy-source-name', + }); + }); + + it('should use fallback values if the required values are not present', () => { + state.lifecycle.writeKey.value = 'dummy-write-key'; + + const userDetails = getUserDetails( + state.source, + state.session, + state.lifecycle, + state.autoTrack, + ); + expect(userDetails).toEqual({ + id: 'dummy-write-key..NA..NA', + name: 'NA', + }); + }); + }); + + describe('getDeviceDetails', () => { + it('should return device details for the given locale and userAgent', () => { + state.context.locale.value = 'en-US'; + state.context.userAgent.value = + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3'; + + const deviceDetails = getDeviceDetails(state.context.locale, state.context.userAgent); + expect(deviceDetails).toEqual({ + locale: 'en-US', + userAgent: + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3', + time: expect.any(Date), + }); + }); + + it('should use fallback values if the required values are not present', () => { + const deviceDetails = getDeviceDetails(state.context.locale, state.context.userAgent); + expect(deviceDetails).toEqual({ + locale: 'NA', + userAgent: 'NA', + time: expect.any(Date), + }); + }); + }); }); diff --git a/packages/analytics-js/src/services/ErrorHandler/ErrorHandler.ts b/packages/analytics-js/src/services/ErrorHandler/ErrorHandler.ts index 9a355232b..06105406f 100644 --- a/packages/analytics-js/src/services/ErrorHandler/ErrorHandler.ts +++ b/packages/analytics-js/src/services/ErrorHandler/ErrorHandler.ts @@ -67,11 +67,12 @@ class ErrorHandler implements IErrorHandler { return; } - const errorMsgPrefix = `${context}${LOG_CONTEXT_SEPARATOR}${customMessage ? `${customMessage} - ` : ''}`; + const customMsgVal = customMessage ? `${customMessage} - ` : ''; + const errorMsgPrefix = `${context}${LOG_CONTEXT_SEPARATOR}${customMsgVal}`; const bsException = createBugsnagException(normalizedError, errorMsgPrefix); - const stacktrace = getStacktrace(normalizedError); - const isSdkDispatched = stacktrace?.includes(MANUAL_ERROR_IDENTIFIER); + const stacktrace = getStacktrace(normalizedError) as string; + const isSdkDispatched = stacktrace.includes(MANUAL_ERROR_IDENTIFIER); // Filter unhandled errors that are not originated in the SDK. // However, in case of NPM installations, since we cannot differentiate between SDK and application errors, we should report all errors. @@ -95,7 +96,7 @@ class ErrorHandler implements IErrorHandler { const bugsnagPayload = getBugsnagErrorEvent(bsException, errorState, state); // send it to metrics service - this.httpClient?.getAsyncData({ + this.httpClient.getAsyncData({ url: state.metrics.metricsServiceUrl.value as string, options: { method: 'POST', diff --git a/packages/analytics-js/src/services/ErrorHandler/event/event.ts b/packages/analytics-js/src/services/ErrorHandler/event/event.ts index 382bcbf67..f3543fca3 100644 --- a/packages/analytics-js/src/services/ErrorHandler/event/event.ts +++ b/packages/analytics-js/src/services/ErrorHandler/event/event.ts @@ -63,7 +63,7 @@ function createException( const normalizeError = (maybeError: any, logger: ILogger): any => { let error; - if (isTypeOfError(maybeError) && !!getStacktrace(maybeError)) { + if (isTypeOfError(maybeError) && isString(getStacktrace(maybeError))) { error = maybeError; } else { logger.warn(NON_ERROR_WARNING(ERROR_HANDLER, stringifyWithoutCircular(maybeError))); diff --git a/packages/analytics-js/src/services/ErrorHandler/utils.ts b/packages/analytics-js/src/services/ErrorHandler/utils.ts index 97a03aaf1..4881c73b4 100644 --- a/packages/analytics-js/src/services/ErrorHandler/utils.ts +++ b/packages/analytics-js/src/services/ErrorHandler/utils.ts @@ -64,6 +64,25 @@ const getURLWithoutQueryString = () => { return url[0]; }; +const getUserDetails = ( + source: ApplicationState['source'], + session: ApplicationState['session'], + lifecycle: ApplicationState['lifecycle'], + autoTrack: ApplicationState['autoTrack'], +) => ({ + id: `${source.value?.id ?? (lifecycle.writeKey.value as string)}..${session.sessionInfo.value.id ?? 'NA'}..${autoTrack.pageLifecycle.visitId.value ?? 'NA'}`, + name: source.value?.name ?? 'NA', +}); + +const getDeviceDetails = ( + locale: ApplicationState['context']['locale'], + userAgent: ApplicationState['context']['userAgent'], +) => ({ + locale: locale.value ?? 'NA', + userAgent: userAgent.value ?? 'NA', + time: new Date(), +}); + const getBugsnagErrorEvent = ( exception: Exception, errorState: ErrorState, @@ -90,11 +109,7 @@ const getBugsnagErrorEvent = ( releaseStage: getReleaseStage(), type: app.value.installType, }, - device: { - locale: locale.value ?? undefined, - userAgent: userAgent.value ?? undefined, - time: new Date(), - }, + device: getDeviceDetails(locale, userAgent), request: { url: getURLWithoutQueryString() as string, clientIp: '[NOT COLLECTED]', @@ -110,10 +125,7 @@ const getBugsnagErrorEvent = ( // so that they show up as separate tabs in the dashboard ...getAppStateForMetadata(state), }, - user: { - id: `${source.value?.id ?? (lifecycle.writeKey.value as string)}..${session.sessionInfo.value?.id ?? 'NA'}..${autoTrack?.pageLifecycle?.visitId?.value ?? 'NA'}`, - name: source.value?.name ?? 'NA', - }, + user: getUserDetails(source, session, lifecycle, autoTrack), }, ], }; @@ -133,7 +145,7 @@ const isAllowedToBeNotified = (exception: Exception) => * @returns */ const isSDKError = (exception: Exception) => { - const errorOrigin = exception.stacktrace?.[0]?.file; + const errorOrigin = exception.stacktrace[0]?.file; if (!errorOrigin || typeof errorOrigin !== 'string') { return false; @@ -178,4 +190,6 @@ export { isSDKError, getErrorDeliveryPayload, isAllowedToBeNotified, + getUserDetails, // for testing + getDeviceDetails, // for testing }; diff --git a/packages/analytics-js/src/state/slices/context.ts b/packages/analytics-js/src/state/slices/context.ts index 385b00fa4..2fbe4c122 100644 --- a/packages/analytics-js/src/state/slices/context.ts +++ b/packages/analytics-js/src/state/slices/context.ts @@ -15,7 +15,7 @@ const contextState: ContextState = { version: APP_VERSION, snippetVersion: (globalThis as typeof window).RudderSnippetVersion, }), - userAgent: signal(''), + userAgent: signal(null), device: signal(null), network: signal(null), os: signal({ From 7a74983f3dcd3d4d9adcd6b5e2acef54cfe92df4 Mon Sep 17 00:00:00 2001 From: Sai Kumar Battinoju Date: Fri, 10 Jan 2025 11:38:12 +0530 Subject: [PATCH 40/53] fix: add missing event properties --- packages/analytics-js-common/src/utilities/errors.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/analytics-js-common/src/utilities/errors.ts b/packages/analytics-js-common/src/utilities/errors.ts index f6f69e6f8..a9e03ba22 100644 --- a/packages/analytics-js-common/src/utilities/errors.ts +++ b/packages/analytics-js-common/src/utilities/errors.ts @@ -56,7 +56,9 @@ const dispatchErrorEvent = (error: any) => { } } - (globalThis as typeof window).dispatchEvent(new ErrorEvent('error', { error })); + (globalThis as typeof window).dispatchEvent( + new ErrorEvent('error', { error, bubbles: true, cancelable: true, composed: true }), + ); }; export { getMutatedError, dispatchErrorEvent, MANUAL_ERROR_IDENTIFIER, getStacktrace }; From a59a002b04f4f4ff113bf533b5f4212b41bde55b Mon Sep 17 00:00:00 2001 From: Sai Kumar Battinoju Date: Fri, 10 Jan 2025 12:22:08 +0530 Subject: [PATCH 41/53] test: add more test cases for improving coverage of error handler --- .../services/ErrorHandler/event.test.ts | 235 ++++++++++++++++++ .../src/components/core/Analytics.ts | 1 - .../src/services/ErrorHandler/event/event.ts | 10 +- 3 files changed, 243 insertions(+), 3 deletions(-) create mode 100644 packages/analytics-js/__tests__/services/ErrorHandler/event.test.ts diff --git a/packages/analytics-js/__tests__/services/ErrorHandler/event.test.ts b/packages/analytics-js/__tests__/services/ErrorHandler/event.test.ts new file mode 100644 index 000000000..bd4f23889 --- /dev/null +++ b/packages/analytics-js/__tests__/services/ErrorHandler/event.test.ts @@ -0,0 +1,235 @@ +import { defaultLogger } from '@rudderstack/analytics-js-common/__mocks__/Logger'; +import { + createBugsnagException, + createException, + ensureString, + formatStackframe, + normalizeError, + normalizeFunctionName, +} from '../../../src/services/ErrorHandler/event/event'; + +describe('event', () => { + describe('normalizeFunctionName', () => { + const testCases: any[][] = [ + ['functionName', 'functionName'], + ['global code', 'global code'], + ['GLOBAL CODE', 'global code'], + ['Global Code', 'global code'], + ['Global code', 'global code'], + ['global Code', 'global code'], + ['globalcode', 'globalcode'], + ['global code with something extra', 'global code with something extra'], + ['', ''], + ]; + + it.each(testCases)('if function name is %p then return %p', (input, output) => { + expect(normalizeFunctionName(input)).toBe(output); + }); + }); + + describe('ensureString', () => { + const testCases: any[][] = [ + ['string', 'string'], + [123, ''], + [undefined, ''], + [null, ''], + [{}, ''], + [[], ''], + [true, ''], + [false, ''], + ]; + + it.each(testCases)('if input is %p then return %p', (input, output) => { + expect(ensureString(input)).toBe(output); + }); + }); + + describe('formatStackframe', () => { + const testCases: any[][] = [ + [ + { + fileName: 'file.js', + functionName: 'functionName', + lineNumber: 1, + columnNumber: 2, + }, + { + file: 'file.js', + method: 'functionName', + lineNumber: 1, + columnNumber: 2, + }, + ], + [ + { + fileName: '', + functionName: '', + lineNumber: 1, + columnNumber: 1, + }, + { + file: 'global code', + method: '', + lineNumber: 1, + columnNumber: 1, + }, + ], + [ + { + fileName: '', + functionName: 'Global Code', + lineNumber: -1, + columnNumber: -1, + }, + { + file: '', + method: 'global code', + lineNumber: -1, + columnNumber: -1, + }, + ], + ]; + + it.each(testCases)('if stack frame is %p then return %p', (input, output) => { + expect(formatStackframe(input)).toEqual(output); + }); + }); + + describe('createException', () => { + it('should return exception object with valid stacktrace', () => { + const errorClass = 'errorClass'; + const errorMessage = 'errorMessage'; + const msgPrefix = 'msgPrefix'; + const stacktrace = [ + { + fileName: 'file.js', + functionName: 'functionName', + lineNumber: 1, + columnNumber: 2, + }, + // should be ignored + {}, + // stack frame with a circular reference, should be ignored + { + // will be replaced by circular reference below + fileName: 'xyz', + functionName: 'functionName', + lineNumber: 1, + columnNumber: 2, + }, + { + fileName: 'file2.js', + functionName: 'functionName2', + lineNumber: 3, + columnNumber: 4, + }, + ]; + + // create circular reference + (stacktrace[2] as any).fileName = stacktrace; + + const expectedOutput = { + errorClass, + message: `${msgPrefix}${errorMessage}`, + type: 'browserjs', + stacktrace: [ + { + file: 'file.js', + method: 'functionName', + lineNumber: 1, + columnNumber: 2, + }, + { + file: 'file2.js', + method: 'functionName2', + lineNumber: 3, + columnNumber: 4, + }, + ], + }; + expect(createException(errorClass, errorMessage, msgPrefix, stacktrace)).toEqual( + expectedOutput, + ); + }); + }); + + describe('normalizeError', () => { + it('should return error object if it is an instance of Error and has stacktrace', () => { + const error = new Error('error message'); + error.stack = 'stacktrace'; + + expect(normalizeError(error, defaultLogger)).toEqual(error); + expect(defaultLogger.warn).not.toHaveBeenCalled(); + }); + + it('should return undefined and log warning if error is not an instance of Error', () => { + const error = 'error message'; + + expect(normalizeError(error, defaultLogger)).toBeUndefined(); + expect(defaultLogger.warn).toHaveBeenCalledTimes(1); + expect(defaultLogger.warn).toHaveBeenCalledWith( + 'ErrorHandler:: Ignoring a non-error: "error message".', + ); + }); + + it('should return undefined and log warning if error does not have stacktrace', () => { + const error = new Error('error message'); + error.stack = undefined; + + expect(normalizeError(error, defaultLogger)).toBeUndefined(); + expect(defaultLogger.warn).toHaveBeenCalledTimes(1); + expect(defaultLogger.warn).toHaveBeenCalledWith('ErrorHandler:: Ignoring a non-error: {}.'); + }); + }); + + describe('createBugsnagException', () => { + it('should return exception object with a valid stacktrace', () => { + const error = new Error('error message'); + // override stack property to a custom stack trace with proper function names etc. + error.stack = `Error: error message + at functionName (file.js:1:2) + at functionName2 (file2.js:3:4)`; + + const msgPrefix = 'msgPrefix'; + + const expectedOutput = { + errorClass: 'Error', + message: `${msgPrefix}error message`, + type: 'browserjs', + stacktrace: [ + { + file: 'file.js', + method: 'functionName', + lineNumber: 1, + columnNumber: 2, + }, + { + file: 'file2.js', + method: 'functionName2', + lineNumber: 3, + columnNumber: 4, + }, + ], + }; + + expect(createBugsnagException(error, msgPrefix)).toEqual(expectedOutput); + }); + + it('should return exception object with an empty stack trace if the stack trace information is not parsable', () => { + const error = new Error('error message'); + // override stack property to cause an exception while parsing + error.stack = undefined; + + const msgPrefix = 'msgPrefix'; + + const expectedOutput = { + errorClass: 'Error', + message: `${msgPrefix}error message`, + type: 'browserjs', + stacktrace: [], + }; + + expect(createBugsnagException(error, msgPrefix)).toEqual(expectedOutput); + }); + }); +}); diff --git a/packages/analytics-js/src/components/core/Analytics.ts b/packages/analytics-js/src/components/core/Analytics.ts index 38ab66dc8..0130a2a07 100644 --- a/packages/analytics-js/src/components/core/Analytics.ts +++ b/packages/analytics-js/src/components/core/Analytics.ts @@ -16,7 +16,6 @@ import type { AnonymousIdOptions, ConsentOptions, LoadOptions, - OnLoadedCallback, } from '@rudderstack/analytics-js-common/types/LoadOptions'; import type { ApiCallback } from '@rudderstack/analytics-js-common/types/EventApi'; import { diff --git a/packages/analytics-js/src/services/ErrorHandler/event/event.ts b/packages/analytics-js/src/services/ErrorHandler/event/event.ts index f3543fca3..a2da3603b 100644 --- a/packages/analytics-js/src/services/ErrorHandler/event/event.ts +++ b/packages/analytics-js/src/services/ErrorHandler/event/event.ts @@ -26,7 +26,6 @@ const formatStackframe = (frame: FrameType): Stackframe => { columnNumber: frame.columnNumber, }; // Some instances result in no file: - // - calling notify() from chrome's terminal results in no file/method. // - non-error exception thrown from global code in FF // This adds one. if (f.lineNumber > -1 && !f.file && !f.method) { @@ -82,4 +81,11 @@ const createBugsnagException = (error: any, msgPrefix: string): Exception => { } }; -export { normalizeError, createBugsnagException }; +export { + normalizeError, + createBugsnagException, + formatStackframe, // for testing + ensureString, // for testing + createException, // for testing + normalizeFunctionName, // for testing +}; From c9c64533d023e934802c3a3234c72d5693e3b56e Mon Sep 17 00:00:00 2001 From: Sai Kumar Battinoju Date: Fri, 10 Jan 2025 13:33:58 +0530 Subject: [PATCH 42/53] test: add more test cases for improving coverage in multiple modules --- .../CapabilitiesManager.test.ts | 18 ++- .../configManager/ConfigManager.test.ts | 113 +++++++++++++++++- .../components/core/Analytics.test.ts | 20 +++- .../eventRepository/EventRepository.test.ts | 19 +++ .../services/ErrorHandler/utils.test.ts | 1 - .../services/StoreManager/Store.test.ts | 11 ++ .../components/configManager/ConfigManager.ts | 2 +- .../eventRepository/EventRepository.ts | 2 +- .../analytics-js/src/constants/logMessages.ts | 8 +- .../src/state/slices/lifecycle.ts | 3 +- .../src/state/slices/loadOptions.ts | 1 - 11 files changed, 182 insertions(+), 16 deletions(-) diff --git a/packages/analytics-js/__tests__/components/capabilitiesManager/CapabilitiesManager.test.ts b/packages/analytics-js/__tests__/components/capabilitiesManager/CapabilitiesManager.test.ts index f81b9cf85..c71c84e90 100644 --- a/packages/analytics-js/__tests__/components/capabilitiesManager/CapabilitiesManager.test.ts +++ b/packages/analytics-js/__tests__/components/capabilitiesManager/CapabilitiesManager.test.ts @@ -1,5 +1,4 @@ import type { ILogger } from '@rudderstack/analytics-js-common/types/Logger'; -import { defaultLogger } from '../../../src/services/Logger'; import { defaultHttpClient } from '../../../src/services/HttpClient'; import { isLegacyJSEngine } from '../../../src/components/capabilitiesManager/detection'; import type { ICapabilitiesManager } from '../../../src/components/capabilitiesManager/types'; @@ -117,5 +116,22 @@ describe('CapabilitiesManager', () => { expect(capabilitiesManager.externalSrcLoader.loadJSFile).not.toHaveBeenCalled(); expect(capabilitiesManager.onReady).toHaveBeenCalled(); }); + + it('should initiate adblockers detection if configured', () => { + state.loadOptions.value.sendAdblockPage = true; + state.lifecycle.sourceConfigUrl.value = 'https://www.dummy.url'; + + const getAsyncDataSpy = jest.spyOn(defaultHttpClient, 'getAsyncData'); + + capabilitiesManager.init(); + + expect(getAsyncDataSpy).toHaveBeenCalledTimes(1); + expect(getAsyncDataSpy).toHaveBeenCalledWith({ + url: 'https://www.dummy.url/?view=ad', + options: expect.any(Object), + callback: expect.any(Function), + isRawResponse: true, + }); + }); }); }); diff --git a/packages/analytics-js/__tests__/components/configManager/ConfigManager.test.ts b/packages/analytics-js/__tests__/components/configManager/ConfigManager.test.ts index fc2d42b8c..55d508132 100644 --- a/packages/analytics-js/__tests__/components/configManager/ConfigManager.test.ts +++ b/packages/analytics-js/__tests__/components/configManager/ConfigManager.test.ts @@ -8,7 +8,7 @@ import { state, resetState } from '../../../src/state'; import { getSDKUrl } from '../../../src/components/configManager/util/commonUtil'; import { server } from '../../../__fixtures__/msw.server'; import { dummySourceConfigResponse } from '../../../__fixtures__/fixtures'; -import { +import type { ConfigResponseDestinationItem, SourceConfigResponse, } from '../../../src/components/configManager/types'; @@ -53,8 +53,6 @@ jest.mock('../../../src/components/configManager/util/commonUtil.ts', () => { describe('ConfigManager', () => { let configManagerInstance: ConfigManager; - const errorMsg = - 'The write key " " is invalid. It must be a non-empty string. Please check that the write key is correct and try again.'; const sampleWriteKey = '2LoR1TbVG2bcISXvy7DamldfkgO'; const sampleDataPlaneUrl = 'https://www.dummy.url'; const sampleDestSDKUrl = 'https://www.sample.url/integrations'; @@ -181,9 +179,36 @@ describe('ConfigManager', () => { }); it('should call the onError method of errorHandler for undefined sourceConfig response', () => { - configManagerInstance.processConfig(undefined); + configManagerInstance.processConfig(); - expect(defaultErrorHandler.onError).toHaveBeenCalled(); + expect(defaultErrorHandler.onError).toHaveBeenCalledTimes(1); + expect(defaultErrorHandler.onError).toHaveBeenCalledWith( + new Error('Failed to fetch the source config'), + 'ConfigManager', + undefined, + ); + }); + + it('should handle error if the source config response is not parsable', () => { + configManagerInstance.processConfig('{"key": "value"'); + + expect(defaultErrorHandler.onError).toHaveBeenCalledTimes(1); + expect(defaultErrorHandler.onError).toHaveBeenCalledWith( + new SyntaxError("Expected ',' or '}' after property value in JSON at position 15"), + 'ConfigManager', + 'Unable to process/parse source configuration response', + ); + }); + + it('should handle error if the source config response is not valid', () => { + configManagerInstance.processConfig({ key: 'value' }); + + expect(defaultErrorHandler.onError).toHaveBeenCalledTimes(1); + expect(defaultErrorHandler.onError).toHaveBeenCalledWith( + new Error('Unable to process/parse source configuration response'), + 'ConfigManager', + undefined, + ); }); it('should log error and abort if source is disabled', () => { @@ -274,4 +299,82 @@ describe('ConfigManager', () => { expect(state.serverCookies.dataServiceUrl.value).toBeUndefined(); expect(state.serverCookies.isEnabledServerSideCookies.value).toBe(false); }); + + it('should log an error and exit if the provided integrations CDN URL is invalid', () => { + state.loadOptions.value.destSDKBaseURL = 'invalid-url'; + const getConfigSpy = jest.spyOn(configManagerInstance, 'getConfig'); + + configManagerInstance.init(); + + expect(defaultLogger.error).toHaveBeenCalledWith( + 'ConfigManager:: The base URL "invalid-url" for integrations is not valid.', + ); + expect(getConfigSpy).not.toHaveBeenCalled(); + }); + + it('should not determine plugins CDN path if __BUNDLE_ALL_PLUGINS__ is true', () => { + state.loadOptions.value.destSDKBaseURL = sampleDestSDKUrl; + + // @ts-expect-error Testing global variable + // eslint-disable-next-line no-underscore-dangle + global.window.__BUNDLE_ALL_PLUGINS__ = true; + + configManagerInstance.init(); + + expect(state.lifecycle.pluginsCDNPath.value).toBeUndefined(); + + // @ts-expect-error Testing global variable + // eslint-disable-next-line no-underscore-dangle + global.window.__BUNDLE_ALL_PLUGINS__ = false; + }); + + it('should log an error and exit if the provided plugins CDN URL is invalid', () => { + state.loadOptions.value.destSDKBaseURL = sampleDestSDKUrl; + state.loadOptions.value.pluginsSDKBaseURL = 'invalid-url'; + const getConfigSpy = jest.spyOn(configManagerInstance, 'getConfig'); + + configManagerInstance.init(); + + expect(defaultLogger.error).toHaveBeenCalledWith( + 'ConfigManager:: The base URL "invalid-url" for plugins is not valid.', + ); + expect(getConfigSpy).not.toHaveBeenCalled(); + }); + + it('should log an error if getSourceConfig load option is not a function', () => { + // @ts-expect-error Testing for invalid input + state.loadOptions.value.getSourceConfig = 'dummySourceConfigResponse'; + + configManagerInstance.getConfig(); + + expect(defaultLogger.error).toHaveBeenCalledWith( + 'ConfigManager:: The "getSourceConfig" load API option must be a function that returns valid source configuration data.', + ); + }); + + it('should fetch configuration from getSourceConfig load option even when it returns a promise', done => { + state.loadOptions.value.getSourceConfig = () => Promise.resolve(dummySourceConfigResponse); + + configManagerInstance.getConfig(); + + effect(() => { + if (state.lifecycle.status.value === 'configured') { + done(); + } + }); + }); + + it('should handle promise rejection errors from getSourceConfig function', done => { + // @ts-expect-error Testing invalid input + state.loadOptions.value.getSourceConfig = () => Promise.reject(new Error('Some error')); + + configManagerInstance.onError = jest.fn(); + + configManagerInstance.getConfig(); + + setTimeout(() => { + expect(configManagerInstance.onError).toHaveBeenCalled(); + done(); + }, 1); + }); }); diff --git a/packages/analytics-js/__tests__/components/core/Analytics.test.ts b/packages/analytics-js/__tests__/components/core/Analytics.test.ts index 7c115ddd9..d55015118 100644 --- a/packages/analytics-js/__tests__/components/core/Analytics.test.ts +++ b/packages/analytics-js/__tests__/components/core/Analytics.test.ts @@ -120,15 +120,33 @@ describe('Core - Analytics', () => { describe('load', () => { const sampleDataPlaneUrl = 'https://www.dummy.url'; it('should load the analytics script with the given options', () => { + state.loadOptions.value.logLevel = 'WARN'; + const startLifecycleSpy = jest.spyOn(analytics, 'startLifecycle'); const setMinLogLevelSpy = jest.spyOn(analytics.logger, 'setMinLogLevel'); + analytics.load(dummyWriteKey, sampleDataPlaneUrl, { logLevel: 'ERROR' }); + expect(state.lifecycle.status.value).toBe('browserCapabilitiesReady'); expect(startLifecycleSpy).toHaveBeenCalledTimes(1); - expect(setMinLogLevelSpy).toHaveBeenCalledWith('ERROR'); + // Once in load API and then in config manager + expect(setMinLogLevelSpy).toHaveBeenCalledTimes(2); + expect(setMinLogLevelSpy).toHaveBeenNthCalledWith(1, 'ERROR'); expect(setExposedGlobal).toHaveBeenCalledWith('state', state, dummyWriteKey); }); + it('should set the log level if it is not configured', () => { + state.loadOptions.value.logLevel = undefined; + const setMinLogLevelSpy = jest.spyOn(analytics.logger, 'setMinLogLevel'); + + analytics.load(dummyWriteKey, sampleDataPlaneUrl); + + expect(state.lifecycle.status.value).toBe('browserCapabilitiesReady'); + // Once in load API and then in config manager + expect(setMinLogLevelSpy).toHaveBeenCalledTimes(2); + expect(setMinLogLevelSpy).toHaveBeenNthCalledWith(1, 'ERROR'); + }); + it('should not load if the write key is invalid', () => { const startLifecycleSpy = jest.spyOn(analytics, 'startLifecycle'); const errorSpy = jest.spyOn(analytics.logger, 'error'); diff --git a/packages/analytics-js/__tests__/components/eventRepository/EventRepository.test.ts b/packages/analytics-js/__tests__/components/eventRepository/EventRepository.test.ts index 7d6376e66..1842ce7f3 100644 --- a/packages/analytics-js/__tests__/components/eventRepository/EventRepository.test.ts +++ b/packages/analytics-js/__tests__/components/eventRepository/EventRepository.test.ts @@ -312,6 +312,25 @@ describe('EventRepository', () => { }); }); + it('should log an error if the event callback function is not a function', () => { + const eventRepository = new EventRepository( + mockPluginsManager, + defaultStoreManager, + defaultHttpClient, + defaultErrorHandler, + defaultLogger, + ); + + eventRepository.init(); + + eventRepository.enqueue(testEvent, 'invalid-callback' as any); + + expect(defaultLogger.error).toHaveBeenCalledTimes(1); + expect(defaultLogger.error).toHaveBeenCalledWith( + 'TrackAPI:: The provided callback is not invokable.', + ); + }); + it('should handle error if event callback function throws', () => { const eventRepository = new EventRepository( mockPluginsManager, diff --git a/packages/analytics-js/__tests__/services/ErrorHandler/utils.test.ts b/packages/analytics-js/__tests__/services/ErrorHandler/utils.test.ts index 16c5b865c..e6b354c5c 100644 --- a/packages/analytics-js/__tests__/services/ErrorHandler/utils.test.ts +++ b/packages/analytics-js/__tests__/services/ErrorHandler/utils.test.ts @@ -486,7 +486,6 @@ describe('Error Reporting utilities', () => { loadIntegration: true, lockIntegrationsVersion: false, lockPluginsVersion: false, - logLevel: 'ERROR', plugins: [], polyfillIfRequired: true, queueOptions: {}, diff --git a/packages/analytics-js/__tests__/services/StoreManager/Store.test.ts b/packages/analytics-js/__tests__/services/StoreManager/Store.test.ts index 8b44eb3b8..b24fc0a68 100644 --- a/packages/analytics-js/__tests__/services/StoreManager/Store.test.ts +++ b/packages/analytics-js/__tests__/services/StoreManager/Store.test.ts @@ -63,6 +63,17 @@ describe('Store', () => { engine.setItem('name.id.queue', '[{]}'); expect(store.get(QueueStatuses.QUEUE)).toBeNull(); }); + + it('should log a warning if the underlying cookie value is a legacy encrypted value', () => { + const spy = jest.spyOn(defaultLogger, 'warn'); + engine.setItem('name.id.queue', '"RudderEncrypt:encryptedValue"'); + + store.get('queue'); + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith( + 'The cookie data for queue seems to be encrypted using SDK versions < v3. The data is dropped. This can potentially stem from using SDK versions < v3 on other sites or web pages that can share cookies with this webpage. We recommend using the same SDK (v3) version everywhere or avoid disabling the storage data migration.', + ); + }); }); describe('.set', () => { diff --git a/packages/analytics-js/src/components/configManager/ConfigManager.ts b/packages/analytics-js/src/components/configManager/ConfigManager.ts index ebbb2731d..e66489ebd 100644 --- a/packages/analytics-js/src/components/configManager/ConfigManager.ts +++ b/packages/analytics-js/src/components/configManager/ConfigManager.ts @@ -145,7 +145,7 @@ class ConfigManager implements IConfigManager { // TODO: add retry logic with backoff based on rejectionDetails.xhr.status // We can use isErrRetryable utility method if (!response) { - this.onError(SOURCE_CONFIG_FETCH_ERROR(details?.error)); + this.onError(new Error(SOURCE_CONFIG_FETCH_ERROR)); return; } diff --git a/packages/analytics-js/src/components/eventRepository/EventRepository.ts b/packages/analytics-js/src/components/eventRepository/EventRepository.ts index c52632722..fa2345ec8 100644 --- a/packages/analytics-js/src/components/eventRepository/EventRepository.ts +++ b/packages/analytics-js/src/components/eventRepository/EventRepository.ts @@ -176,7 +176,7 @@ class EventRepository implements IEventRepository { try { // Using the event sent to the data plane queue here // to ensure the mutated (if any) event is sent to the callback - callback?.(dpQEvent); + callback(dpQEvent); } catch (error) { this.logger.error(CALLBACK_INVOKE_ERROR(apiName), error); } diff --git a/packages/analytics-js/src/constants/logMessages.ts b/packages/analytics-js/src/constants/logMessages.ts index 7a49a55fa..1de2dbe6d 100644 --- a/packages/analytics-js/src/constants/logMessages.ts +++ b/packages/analytics-js/src/constants/logMessages.ts @@ -11,8 +11,9 @@ import type { import type { Nullable } from '@rudderstack/analytics-js-common/types/Nullable'; // CONSTANT -const DATA_PLANE_URL_ERROR = `Failed to load the SDK as the data plane URL could not be determined. Please check that the data plane URL is set correctly and try again.`; -const SOURCE_CONFIG_RESOLUTION_ERROR = `Unable to process/parse source configuration response.`; +const DATA_PLANE_URL_ERROR = + 'Failed to load the SDK as the data plane URL could not be determined. Please check that the data plane URL is set correctly and try again.'; +const SOURCE_CONFIG_RESOLUTION_ERROR = `Unable to process/parse source configuration response`; const SOURCE_DISABLED_ERROR = `The source is disabled. Please enable the source in the dashboard to send events.`; const XHR_PAYLOAD_PREP_ERROR = `Failed to prepare data for the request.`; const PLUGIN_EXT_POINT_MISSING_ERROR = `Failed to invoke plugin because the extension point name is missing.`; @@ -68,8 +69,7 @@ const PLUGIN_INVOCATION_ERROR = ( const STORAGE_UNAVAILABILITY_ERROR_PREFIX = (context: string, storageType: StorageType): string => `${context}${LOG_CONTEXT_SEPARATOR}The "${storageType}" storage type is `; -const SOURCE_CONFIG_FETCH_ERROR = (reason: Error | undefined): string => - `Failed to fetch the source config. Reason: ${reason}`; +const SOURCE_CONFIG_FETCH_ERROR = 'Failed to fetch the source config'; const WRITE_KEY_VALIDATION_ERROR = (context: string, writeKey: string): string => `${context}${LOG_CONTEXT_SEPARATOR}The write key "${writeKey}" is invalid. It must be a non-empty string. Please check that the write key is correct and try again.`; diff --git a/packages/analytics-js/src/state/slices/lifecycle.ts b/packages/analytics-js/src/state/slices/lifecycle.ts index 4a2f69018..e2a9ac300 100644 --- a/packages/analytics-js/src/state/slices/lifecycle.ts +++ b/packages/analytics-js/src/state/slices/lifecycle.ts @@ -1,5 +1,6 @@ import { signal } from '@preact/signals-core'; import type { LifecycleState } from '@rudderstack/analytics-js-common/types/ApplicationState'; +import { POST_LOAD_LOG_LEVEL } from '../../services/Logger'; import { DEST_SDK_BASE_URL, PLUGINS_BASE_URL } from '../../constants/urls'; const lifecycleState: LifecycleState = { @@ -9,7 +10,7 @@ const lifecycleState: LifecycleState = { sourceConfigUrl: signal(undefined), status: signal(undefined), initialized: signal(false), - logLevel: signal('ERROR'), + logLevel: signal(POST_LOAD_LOG_LEVEL), loaded: signal(false), readyCallbacks: signal([]), writeKey: signal(undefined), diff --git a/packages/analytics-js/src/state/slices/loadOptions.ts b/packages/analytics-js/src/state/slices/loadOptions.ts index 8d9759353..b776a1466 100644 --- a/packages/analytics-js/src/state/slices/loadOptions.ts +++ b/packages/analytics-js/src/state/slices/loadOptions.ts @@ -11,7 +11,6 @@ import { DEFAULT_CONFIG_BE_URL } from '../../constants/urls'; import { DEFAULT_STORAGE_ENCRYPTION_VERSION } from '../../components/configManager/constants'; const defaultLoadOptions: LoadOptions = { - logLevel: 'ERROR', configUrl: DEFAULT_CONFIG_BE_URL, loadIntegration: true, sessions: { From ef841ae03cc8f23c3b64cf465f5ea462f8571ce4 Mon Sep 17 00:00:00 2001 From: Sai Kumar Battinoju Date: Fri, 10 Jan 2025 14:16:33 +0530 Subject: [PATCH 43/53] test: add more test cases for improving coverage in config manager --- .../configManager/ConfigManager.test.ts | 19 +++++++++++++++++-- .../components/configManager/ConfigManager.ts | 19 ++++++++++++++----- .../src/components/configManager/types.ts | 10 ++++++++-- 3 files changed, 39 insertions(+), 9 deletions(-) diff --git a/packages/analytics-js/__tests__/components/configManager/ConfigManager.test.ts b/packages/analytics-js/__tests__/components/configManager/ConfigManager.test.ts index 55d508132..a6e86903b 100644 --- a/packages/analytics-js/__tests__/components/configManager/ConfigManager.test.ts +++ b/packages/analytics-js/__tests__/components/configManager/ConfigManager.test.ts @@ -1,5 +1,6 @@ import { effect, signal } from '@preact/signals-core'; import { http, HttpResponse } from 'msw'; +import type { ResponseDetails } from '@rudderstack/analytics-js-common/types/HttpClient'; import { defaultHttpClient } from '../../../src/services/HttpClient'; import { defaultErrorHandler } from '../../../src/services/ErrorHandler'; import { defaultLogger } from '../../../src/services/Logger'; @@ -178,8 +179,8 @@ describe('ConfigManager', () => { ); }); - it('should call the onError method of errorHandler for undefined sourceConfig response', () => { - configManagerInstance.processConfig(); + it('should handle error for undefined source config response', () => { + configManagerInstance.processConfig(undefined); expect(defaultErrorHandler.onError).toHaveBeenCalledTimes(1); expect(defaultErrorHandler.onError).toHaveBeenCalledWith( @@ -189,6 +190,19 @@ describe('ConfigManager', () => { ); }); + it('should handle error for source config request failures', () => { + configManagerInstance.processConfig(undefined, { + error: new Error('Request failed'), + } as unknown as ResponseDetails); + + expect(defaultErrorHandler.onError).toHaveBeenCalledTimes(1); + expect(defaultErrorHandler.onError).toHaveBeenCalledWith( + new Error('Request failed'), + 'ConfigManager', + 'Failed to fetch the source config', + ); + }); + it('should handle error if the source config response is not parsable', () => { configManagerInstance.processConfig('{"key": "value"'); @@ -201,6 +215,7 @@ describe('ConfigManager', () => { }); it('should handle error if the source config response is not valid', () => { + // @ts-expect-error Testing invalid input configManagerInstance.processConfig({ key: 'value' }); expect(defaultErrorHandler.onError).toHaveBeenCalledTimes(1); diff --git a/packages/analytics-js/src/components/configManager/ConfigManager.ts b/packages/analytics-js/src/components/configManager/ConfigManager.ts index e66489ebd..4e84aa9d3 100644 --- a/packages/analytics-js/src/components/configManager/ConfigManager.ts +++ b/packages/analytics-js/src/components/configManager/ConfigManager.ts @@ -4,7 +4,12 @@ import type { ResponseDetails, } from '@rudderstack/analytics-js-common/types/HttpClient'; import { batch, effect } from '@preact/signals-core'; -import { isFunction, isNull, isString } from '@rudderstack/analytics-js-common/utilities/checks'; +import { + isDefined, + isFunction, + isNull, + isString, +} from '@rudderstack/analytics-js-common/utilities/checks'; import type { IErrorHandler } from '@rudderstack/analytics-js-common/types/ErrorHandler'; import type { Destination } from '@rudderstack/analytics-js-common/types/Destination'; import type { ILogger } from '@rudderstack/analytics-js-common/types/Logger'; @@ -141,11 +146,15 @@ class ConfigManager implements IConfigManager { * A callback function that is executed once we fetch the source config response. * Use to construct and store information that are dependent on the sourceConfig. */ - processConfig(response?: SourceConfigResponse | string, details?: ResponseDetails) { + processConfig(response: SourceConfigResponse | string | undefined, details?: ResponseDetails) { // TODO: add retry logic with backoff based on rejectionDetails.xhr.status // We can use isErrRetryable utility method - if (!response) { - this.onError(new Error(SOURCE_CONFIG_FETCH_ERROR)); + if (!isDefined(response)) { + if (isDefined(details)) { + this.onError((details as ResponseDetails).error, SOURCE_CONFIG_FETCH_ERROR); + } else { + this.onError(new Error(SOURCE_CONFIG_FETCH_ERROR)); + } return; } @@ -154,7 +163,7 @@ class ConfigManager implements IConfigManager { if (isString(response)) { res = JSON.parse(response); } else { - res = response; + res = response as SourceConfigResponse; } } catch (err) { this.onError(err, SOURCE_CONFIG_RESOLUTION_ERROR); diff --git a/packages/analytics-js/src/components/configManager/types.ts b/packages/analytics-js/src/components/configManager/types.ts index 0c02d205b..eba72a504 100644 --- a/packages/analytics-js/src/components/configManager/types.ts +++ b/packages/analytics-js/src/components/configManager/types.ts @@ -1,7 +1,10 @@ import type { DestinationConfig } from '@rudderstack/analytics-js-common/types/Destination'; import type { Nullable } from '@rudderstack/analytics-js-common/types/Nullable'; import type { StatsCollection } from '@rudderstack/analytics-js-common/types/Source'; -import type { IHttpClient } from '@rudderstack/analytics-js-common/types/HttpClient'; +import type { + IHttpClient, + ResponseDetails, +} from '@rudderstack/analytics-js-common/types/HttpClient'; import type { IErrorHandler } from '@rudderstack/analytics-js-common/types/ErrorHandler'; import type { ILogger } from '@rudderstack/analytics-js-common/types/Logger'; import type { ConsentManagementMetadata } from '@rudderstack/analytics-js-common/types/Consent'; @@ -81,5 +84,8 @@ export interface IConfigManager { logger: ILogger; init: () => void; getConfig: () => void; - processConfig: () => void; + processConfig: ( + response: SourceConfigResponse | string | undefined, + details?: ResponseDetails, + ) => void; } From 0ecb545c12a5b706f04fe8fc044d7650a466ee38 Mon Sep 17 00:00:00 2001 From: Sai Kumar Battinoju Date: Fri, 10 Jan 2025 14:40:19 +0530 Subject: [PATCH 44/53] test: add more test cases for improving coverage in plugin engine --- .../PluginEngine/PluginEngine.test.ts | 98 ++++++++++++++++++- .../src/services/PluginEngine/PluginEngine.ts | 4 + 2 files changed, 99 insertions(+), 3 deletions(-) diff --git a/packages/analytics-js/__tests__/services/PluginEngine/PluginEngine.test.ts b/packages/analytics-js/__tests__/services/PluginEngine/PluginEngine.test.ts index 25cc831e9..072fe0c6a 100644 --- a/packages/analytics-js/__tests__/services/PluginEngine/PluginEngine.test.ts +++ b/packages/analytics-js/__tests__/services/PluginEngine/PluginEngine.test.ts @@ -1,10 +1,11 @@ import type { ExtensionPlugin } from '@rudderstack/analytics-js-common/types/PluginEngine'; +import { defaultLogger } from '@rudderstack/analytics-js-common/__mocks__/Logger'; import { PluginEngine } from '../../../src/services/PluginEngine/PluginEngine'; -import { defaultLogger } from '../../../src/services/Logger'; const mockPlugin1: ExtensionPlugin = { name: 'p1', foo: 'bar1', + initialize: jest.fn(), ext: { form: { processMeta(meta: string[]) { @@ -41,8 +42,6 @@ describe('PluginEngine', () => { pluginEngineTestInstance.register(mockPlugin3); }); - afterEach(() => {}); - it('should retrieve all registered plugins', () => { expect(pluginEngineTestInstance.getPlugins().length).toEqual(3); }); @@ -56,6 +55,40 @@ describe('PluginEngine', () => { expect(pluginEngineTestInstance.getPlugins().length).toEqual(4); }); + it('should throw error for missing plugin name if configured', () => { + // @ts-expect-error Testing for missing name + expect(() => { + pluginEngineTestInstance.register({}); + }).toThrow(new Error('PluginEngine:: Plugin name is missing.')); + }); + + it('should log an error for missing plugin name', () => { + // Temporarily mutate the config + pluginEngineTestInstance.config.throws = false; + + // @ts-expect-error Testing for missing name + pluginEngineTestInstance.register({}); + + expect(defaultLogger.error).toHaveBeenCalledTimes(1); + expect(defaultLogger.error).toHaveBeenCalledWith('PluginEngine:: Plugin name is missing.', {}); + }); + + it('should throw error for already registered plugin name if configured', () => { + expect(() => { + pluginEngineTestInstance.register({ name: 'p1' }); + }).toThrow(new Error('PluginEngine:: Plugin "p1" already exists.')); + }); + + it('should log an error for already registered plugin name', () => { + // Temporarily mutate the config + pluginEngineTestInstance.config.throws = false; + + pluginEngineTestInstance.register({ name: 'p1' }); + + expect(defaultLogger.error).toHaveBeenCalledTimes(1); + expect(defaultLogger.error).toHaveBeenCalledWith('PluginEngine:: Plugin "p1" already exists.'); + }); + it('should invoke multiple plugins on functions', () => { const meta = ['m0']; pluginEngineTestInstance.invokeMultiple('ext.form.processMeta', meta); @@ -90,6 +123,50 @@ describe('PluginEngine', () => { expect(pluginEngineTestInstance.getPlugin('p2')).toBeUndefined(); }); + it('should throw an error if the plugin to unregister does not exist', () => { + expect(() => { + pluginEngineTestInstance.unregister('p0'); + }).toThrow(new Error('PluginEngine:: Plugin "p0" not found.')); + }); + + it('should log an error if the plugin to unregister does not exist', () => { + // Temporarily mutate the config + pluginEngineTestInstance.config.throws = false; + + pluginEngineTestInstance.unregister('p0'); + + expect(defaultLogger.error).toHaveBeenCalledTimes(1); + expect(defaultLogger.error).toHaveBeenCalledWith('PluginEngine:: Plugin "p0" not found.'); + }); + + it('should throw an error if the plugin to unregister is found in byName but already registered', () => { + // Temporarily mutate the plugins array + pluginEngineTestInstance.plugins = [mockPlugin2, mockPlugin3]; + + expect(() => { + pluginEngineTestInstance.unregister('p1'); + }).toThrow( + new Error( + 'PluginEngine:: Plugin "p1" not found in plugins but found in byName. This indicates a bug in the plugin engine. Please report this issue to the development team.', + ), + ); + }); + + it('should log an error if the plugin to unregister is found in byName but already registered', () => { + // Temporarily mutate the plugins array + pluginEngineTestInstance.plugins = [mockPlugin2, mockPlugin3]; + + // Temporarily mutate the config + pluginEngineTestInstance.config.throws = false; + + pluginEngineTestInstance.unregister('p1'); + + expect(defaultLogger.error).toHaveBeenCalledTimes(1); + expect(defaultLogger.error).toHaveBeenCalledWith( + 'PluginEngine:: Plugin "p1" not found in plugins but found in byName. This indicates a bug in the plugin engine. Please report this issue to the development team.', + ); + }); + it('should not load if deps do not exist', () => { pluginEngineTestInstance.register({ name: 'p4', deps: ['p5'] }); expect(pluginEngineTestInstance.getPlugins().map(p => p.name)).toStrictEqual([ @@ -195,6 +272,21 @@ describe('PluginEngine', () => { pluginEngineTestInstance.invokeMultiple('fail'); }); + it('should throw an error if extension point is not provided', () => { + expect(() => { + pluginEngineTestInstance.invoke(); + }).toThrow(new Error('Failed to invoke plugin because the extension point name is missing.')); + }); + + it('should throw an error if extension point is invalid', () => { + // Temporarily mutate the config + pluginEngineTestInstance.config.throws = undefined; + + expect(() => { + pluginEngineTestInstance.invoke('!'); + }).toThrow(new Error('Failed to invoke plugin because the extension point name is invalid.')); + }); + it('should register 1000 plugins in less than 200ms', () => { const time1 = Date.now(); for (let i = 0; i < 1000; i++) { diff --git a/packages/analytics-js/src/services/PluginEngine/PluginEngine.ts b/packages/analytics-js/src/services/PluginEngine/PluginEngine.ts index 9b0c3a696..37ba01fed 100644 --- a/packages/analytics-js/src/services/PluginEngine/PluginEngine.ts +++ b/packages/analytics-js/src/services/PluginEngine/PluginEngine.ts @@ -47,6 +47,7 @@ class PluginEngine implements IPluginEngine { throw new Error(errorMessage); } else { this.logger.error(errorMessage, plugin); + return; } } @@ -56,6 +57,7 @@ class PluginEngine implements IPluginEngine { throw new Error(errorMessage); } else { this.logger.error(errorMessage); + return; } } @@ -87,6 +89,7 @@ class PluginEngine implements IPluginEngine { throw new Error(errorMessage); } else { this.logger.error(errorMessage); + return; } } @@ -98,6 +101,7 @@ class PluginEngine implements IPluginEngine { throw new Error(errorMessage); } else { this.logger.error(errorMessage); + return; } } From 8451e777c023f880912115093f8feb55c93079bf Mon Sep 17 00:00:00 2001 From: Sai Kumar Battinoju Date: Fri, 10 Jan 2025 15:01:39 +0530 Subject: [PATCH 45/53] test: address ai bot review comments --- .../services/ErrorHandler/utils.test.ts | 18 ++++++------- .../src/components/core/Analytics.ts | 24 +++++++---------- .../eventRepository/EventRepository.ts | 19 +++----------- .../src/components/utilities/callbacks.ts | 26 +++++++++++++++++++ 4 files changed, 48 insertions(+), 39 deletions(-) create mode 100644 packages/analytics-js/src/components/utilities/callbacks.ts diff --git a/packages/analytics-js/__tests__/services/ErrorHandler/utils.test.ts b/packages/analytics-js/__tests__/services/ErrorHandler/utils.test.ts index e6b354c5c..4beba5cdd 100644 --- a/packages/analytics-js/__tests__/services/ErrorHandler/utils.test.ts +++ b/packages/analytics-js/__tests__/services/ErrorHandler/utils.test.ts @@ -604,16 +604,16 @@ describe('Error Reporting utilities', () => { }); describe('isAllowedToBeNotified', () => { - it('should return true if the error is allowed to be notified', () => { - const result = isAllowedToBeNotified({ message: 'dummy error' } as unknown as Exception); - expect(result).toBeTruthy(); - }); + const testCases = [ + ['dummy error', true, 'should allow generic errors'], + ['The request failed', false, 'should not allow request failures'], + ['', true, 'should allow empty messages'], + ['Network request failed', true, 'should allow network errors'], + ]; - it('should return false if the error is not allowed to be notified', () => { - const result = isAllowedToBeNotified({ - message: 'The request failed', - } as unknown as Exception); - expect(result).toBeFalsy(); + test.each(testCases)('%s -> %s (%s)', (message, expected, testName) => { + const result = isAllowedToBeNotified({ message } as unknown as Exception); + expect(result).toBe(expected); }); }); diff --git a/packages/analytics-js/src/components/core/Analytics.ts b/packages/analytics-js/src/components/core/Analytics.ts index 0130a2a07..97aefb852 100644 --- a/packages/analytics-js/src/components/core/Analytics.ts +++ b/packages/analytics-js/src/components/core/Analytics.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ import { ExternalSrcLoader } from '@rudderstack/analytics-js-common/services/ExternalSrcLoader'; import { batch, effect } from '@preact/signals-core'; -import { isDefined, isFunction, isNull } from '@rudderstack/analytics-js-common/utilities/checks'; +import { isFunction, isNull } from '@rudderstack/analytics-js-common/utilities/checks'; import type { IHttpClient } from '@rudderstack/analytics-js-common/types/HttpClient'; import { clone } from 'ramda'; import type { ILogger } from '@rudderstack/analytics-js-common/types/Logger'; @@ -69,6 +69,7 @@ import { import type { IAnalytics } from './IAnalytics'; import { getConsentManagementData, getValidPostConsentOptions } from '../utilities/consent'; import { dispatchSDKEvent, isDataPlaneUrlValid, isWriteKeyValid } from './utilities'; +import { safelyInvokeCallback } from '../utilities/callbacks'; /* * Analytics class with lifecycle based on state ad user triggered events @@ -322,19 +323,14 @@ class Analytics implements IAnalytics { // Execute onLoaded callback if provided in load options const onLoadedCallbackFn = state.loadOptions.value.onLoaded; - if (isDefined(onLoadedCallbackFn)) { - if (isFunction(onLoadedCallbackFn)) { - // TODO: we need to avoid passing the window object to the callback function - // as this will prevent us from supporting multiple SDK instances in the same page - try { - onLoadedCallbackFn((globalThis as typeof window).rudderanalytics); - } catch (err) { - this.logger.error(CALLBACK_INVOKE_ERROR(LOAD_API), err); - } - } else { - this.logger.error(INVALID_CALLBACK_FN_ERROR(LOAD_API)); - } - } + // TODO: we need to avoid passing the window object to the callback function + // as this will prevent us from supporting multiple SDK instances in the same page + safelyInvokeCallback( + onLoadedCallbackFn, + [(globalThis as typeof window).rudderanalytics], + LOAD_API, + this.logger, + ); // Set lifecycle state batch(() => { diff --git a/packages/analytics-js/src/components/eventRepository/EventRepository.ts b/packages/analytics-js/src/components/eventRepository/EventRepository.ts index fa2345ec8..b2698e5d2 100644 --- a/packages/analytics-js/src/components/eventRepository/EventRepository.ts +++ b/packages/analytics-js/src/components/eventRepository/EventRepository.ts @@ -10,8 +10,6 @@ import type { ApiCallback } from '@rudderstack/analytics-js-common/types/EventAp import { isHybridModeDestination } from '@rudderstack/analytics-js-common/utilities/destinations'; import { API_SUFFIX } from '@rudderstack/analytics-js-common/constants/loggerContexts'; import type { Destination } from '@rudderstack/analytics-js-common/types/Destination'; -import { isDefined, isFunction } from '@rudderstack/analytics-js-common/utilities/checks'; -import { CALLBACK_INVOKE_ERROR, INVALID_CALLBACK_FN_ERROR } from '../../constants/logMessages'; import { state } from '../../state'; import type { IEventRepository } from './types'; import { @@ -20,6 +18,7 @@ import { DMT_EXT_POINT_PREFIX, } from './constants'; import { getFinalEvent, shouldBufferEventsForPreConsent } from './utils'; +import { safelyInvokeCallback } from '../utilities/callbacks'; /** * Event repository class responsible for queuing events for further processing and delivery @@ -170,20 +169,8 @@ class EventRepository implements IEventRepository { ); // Invoke the callback if it exists - if (isDefined(callback)) { - const apiName = `${event.type.charAt(0).toUpperCase()}${event.type.slice(1)}${API_SUFFIX}`; - if (isFunction(callback)) { - try { - // Using the event sent to the data plane queue here - // to ensure the mutated (if any) event is sent to the callback - callback(dpQEvent); - } catch (error) { - this.logger.error(CALLBACK_INVOKE_ERROR(apiName), error); - } - } else { - this.logger.error(INVALID_CALLBACK_FN_ERROR(apiName)); - } - } + const apiName = `${event.type.charAt(0).toUpperCase()}${event.type.slice(1)}${API_SUFFIX}`; + safelyInvokeCallback(callback, [dpQEvent], apiName, this.logger); } } diff --git a/packages/analytics-js/src/components/utilities/callbacks.ts b/packages/analytics-js/src/components/utilities/callbacks.ts new file mode 100644 index 000000000..7d48f27a7 --- /dev/null +++ b/packages/analytics-js/src/components/utilities/callbacks.ts @@ -0,0 +1,26 @@ +import type { ILogger } from '@rudderstack/analytics-js-common/types/Logger'; +import { isDefined, isFunction } from '@rudderstack/analytics-js-common/utilities/checks'; +import { CALLBACK_INVOKE_ERROR, INVALID_CALLBACK_FN_ERROR } from '../../constants/logMessages'; + +const safelyInvokeCallback = ( + callback: unknown, + args: unknown[], + apiName: string, + logger: ILogger, +): void => { + if (!isDefined(callback)) { + return; + } + + if (isFunction(callback)) { + try { + callback(...args); + } catch (error) { + logger.error(CALLBACK_INVOKE_ERROR(apiName), error); + } + } else { + logger.error(INVALID_CALLBACK_FN_ERROR(apiName)); + } +}; + +export { safelyInvokeCallback }; From 12ff2cafd699788ea6f9b37f49ea88790dddea4c Mon Sep 17 00:00:00 2001 From: Sai Kumar Battinoju Date: Fri, 10 Jan 2025 19:58:01 +0530 Subject: [PATCH 46/53] feat: move data plane events processing to core --- .eslintrc.json | 1 - jest/jest.polyfills.js | 2 - .../__tests__/utilities/url.test.ts | 2 +- .../src/types/ApplicationState.ts | 2 - .../src/types/LoadOptions.ts | 9 +- .../src/types/PluginsManager.ts | 4 +- .../analytics-js-plugins/__mocks__/state.ts | 3 - .../__tests__/beaconQueue/.gitkeep | 0 .../__tests__/xhrQueue/index.test.ts | 307 -------------- .../__tests__/xhrQueue/utilities.test.ts | 394 ------------------ .../analytics-js-plugins/rollup.config.mjs | 2 - .../src/beaconQueue/constants.ts | 27 -- .../src/beaconQueue/index.ts | 150 ------- .../src/beaconQueue/logMessages.ts | 24 -- .../src/beaconQueue/types.ts | 12 - .../src/beaconQueue/utilities.ts | 71 ---- packages/analytics-js-plugins/src/index.ts | 2 - .../src/xhrQueue/constants.ts | 23 - .../src/xhrQueue/index.ts | 163 -------- .../src/xhrQueue/logMessages.ts | 6 - .../src/xhrQueue/types.ts | 16 - .../src/xhrQueue/utilities.ts | 108 ----- .../__mocks__/remotePlugins/BeaconQueue.ts | 9 - .../__mocks__/remotePlugins/XhrQueue.ts | 9 - .../__tests__/app/RudderAnalytics.test.ts | 19 +- .../analytics-js/__tests__/browser.test.ts | 4 +- .../configManager/commonUtil.test.ts | 46 -- .../components/core/Analytics.test.ts | 12 +- .../pluginsManager/PluginsManager.test.ts | 21 - .../services/ErrorHandler/utils.test.ts | 5 +- packages/analytics-js/public/index.html | 5 - packages/analytics-js/rollup.config.mjs | 8 - .../analytics-js/src/app/RudderAnalytics.ts | 55 +-- .../CapabilitiesManager.ts | 2 - .../capabilitiesManager/detection/browser.ts | 6 +- .../capabilitiesManager/detection/dom.ts | 1 - .../capabilitiesManager/detection/index.ts | 2 +- .../components/configManager/ConfigManager.ts | 2 - .../src/components/configManager/constants.ts | 5 - .../configManager/util/commonUtil.ts | 24 -- .../pluginsManager/PluginsManager.ts | 8 - .../bundledBuildPluginImports.ts | 4 - .../pluginsManager/defaultPluginsList.ts | 2 - .../federatedModulesBuildPluginImports.ts | 4 - .../components/pluginsManager/pluginNames.ts | 4 +- .../src/components/utilities/loadOptions.ts | 1 + .../analytics-js/src/constants/logMessages.ts | 8 - .../src/state/slices/capabilities.ts | 1 - .../src/state/slices/dataPlaneEvents.ts | 1 - .../src/state/slices/loadOptions.ts | 2 - .../src/types/remote-plugins.d.ts | 2 - .../sanity-suite/public/v1.1/index-cdn.html | 2 +- .../sanity-suite/public/v1.1/index-local.html | 2 +- .../sanity-suite/public/v1.1/index-npm.html | 2 +- .../public/v1.1/integrations/index-cdn.html | 2 +- .../public/v1.1/integrations/index-local.html | 2 +- .../public/v1.1/integrations/index-npm.html | 2 +- .../public/v1.1/manualLoadCall/index-cdn.html | 2 +- .../v1.1/manualLoadCall/index-local.html | 2 +- .../public/v1.1/manualLoadCall/index-npm.html | 2 +- 60 files changed, 53 insertions(+), 1565 deletions(-) delete mode 100644 packages/analytics-js-plugins/__tests__/beaconQueue/.gitkeep delete mode 100644 packages/analytics-js-plugins/__tests__/xhrQueue/index.test.ts delete mode 100644 packages/analytics-js-plugins/__tests__/xhrQueue/utilities.test.ts delete mode 100644 packages/analytics-js-plugins/src/beaconQueue/constants.ts delete mode 100644 packages/analytics-js-plugins/src/beaconQueue/index.ts delete mode 100644 packages/analytics-js-plugins/src/beaconQueue/logMessages.ts delete mode 100644 packages/analytics-js-plugins/src/beaconQueue/types.ts delete mode 100644 packages/analytics-js-plugins/src/beaconQueue/utilities.ts delete mode 100644 packages/analytics-js-plugins/src/xhrQueue/constants.ts delete mode 100644 packages/analytics-js-plugins/src/xhrQueue/index.ts delete mode 100644 packages/analytics-js-plugins/src/xhrQueue/logMessages.ts delete mode 100644 packages/analytics-js-plugins/src/xhrQueue/types.ts delete mode 100644 packages/analytics-js-plugins/src/xhrQueue/utilities.ts delete mode 100644 packages/analytics-js/__mocks__/remotePlugins/BeaconQueue.ts delete mode 100644 packages/analytics-js/__mocks__/remotePlugins/XhrQueue.ts diff --git a/.eslintrc.json b/.eslintrc.json index 80605e3e8..ce2b07fd7 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -27,7 +27,6 @@ "CustomEvent", "requestAnimationFrame", "cancelAnimationFrame", - "navigator.sendBeacon", "Uint8Array", "Set", "atob" diff --git a/jest/jest.polyfills.js b/jest/jest.polyfills.js index da6191c12..4bc209a45 100644 --- a/jest/jest.polyfills.js +++ b/jest/jest.polyfills.js @@ -14,5 +14,3 @@ global.Math.random = () => 0.5; // Suppress Console output from tested code to terminal console.warn = jest.fn(); console.error = jest.fn(); -// Mock browsers sendBeacon utility -navigator.sendBeacon = jest.fn(); diff --git a/packages/analytics-js-common/__tests__/utilities/url.test.ts b/packages/analytics-js-common/__tests__/utilities/url.test.ts index 965e4adeb..ff0dd5b3e 100644 --- a/packages/analytics-js-common/__tests__/utilities/url.test.ts +++ b/packages/analytics-js-common/__tests__/utilities/url.test.ts @@ -27,7 +27,7 @@ describe('utilities - url', () => { [false, undefined], [ true, - 'https://polyfill-fastly.io/v3/polyfill.min.js?version=3.111.0&features=URL%2CPromise%2CNumber.isNaN%2CNumber.isInteger%2CArray.from%2CArray.prototype.find%2CArray.prototype.includes%2CString.prototype.endsWith%2CString.prototype.startsWith%2CString.prototype.includes%2CString.prototype.replaceAll%2CString.fromCodePoint%2CObject.entries%2CObject.values%2CObject.assign%2CObject.fromEntries%2CElement.prototype.dataset%2CTextEncoder%2CrequestAnimationFrame%2CCustomEvent%2Cnavigator.sendBeacon%2CArrayBuffer%2CSet', + 'https://polyfill-fastly.io/v3/polyfill.min.js?version=3.111.0&features=URL%2CPromise%2CNumber.isNaN%2CNumber.isInteger%2CArray.from%2CArray.prototype.find%2CArray.prototype.includes%2CString.prototype.endsWith%2CString.prototype.startsWith%2CString.prototype.includes%2CString.prototype.replaceAll%2CString.fromCodePoint%2CObject.entries%2CObject.values%2CObject.assign%2CObject.fromEntries%2CElement.prototype.dataset%2CTextEncoder%2CrequestAnimationFrame%2CCustomEvent%2CArrayBuffer%2CSet', ], ]; diff --git a/packages/analytics-js-common/src/types/ApplicationState.ts b/packages/analytics-js-common/src/types/ApplicationState.ts index 20152d920..4264bfc2c 100644 --- a/packages/analytics-js-common/src/types/ApplicationState.ts +++ b/packages/analytics-js-common/src/types/ApplicationState.ts @@ -28,7 +28,6 @@ export type CapabilitiesState = { isCookieStorageAvailable: Signal; isSessionStorageAvailable: Signal; }; - isBeaconAvailable: Signal; isLegacyDOM: Signal; isUaCHAvailable: Signal; isCryptoAvailable: Signal; @@ -107,7 +106,6 @@ export type MetricsState = { }; export type DataPlaneEventsState = { - eventsQueuePluginName: Signal; readonly deliveryEnabled: Signal; }; diff --git a/packages/analytics-js-common/src/types/LoadOptions.ts b/packages/analytics-js-common/src/types/LoadOptions.ts index ad615b2f8..057471c9a 100644 --- a/packages/analytics-js-common/src/types/LoadOptions.ts +++ b/packages/analytics-js-common/src/types/LoadOptions.ts @@ -46,8 +46,6 @@ export type BeaconQueueOpts = { flushQueueInterval?: number; }; -export type EventsTransportMode = 'xhr' | 'beacon'; - export type BatchOpts = { // Whether to enable batching enabled: boolean; @@ -138,7 +136,13 @@ export type LoadOptions = { secureCookie?: boolean; // defaults to false. destSDKBaseURL?: string; // defaults to https://cdn.rudderlabs.com/latest/v3/modern/js-integrations pluginsSDKBaseURL?: string; // defaults to https://cdn.rudderlabs.com/latest/v3/modern/plugins + /** + * @deprecated + */ useBeacon?: boolean; // defaults to false. + /** + * @deprecated Use queueOptions instead + */ beaconQueueOptions?: BeaconQueueOpts; destinationsQueueOptions?: DestinationsQueueOpts; anonymousIdOptions?: AnonymousIdOptions; @@ -161,7 +165,6 @@ export type LoadOptions = { storage?: StorageOpts; preConsent?: PreConsentOptions; // transport mechanism to be used for sending batched requests - transportMode?: EventsTransportMode; // Unused for now. This will deprecate the useBeacon and beaconQueueOptions consentManagement?: ConsentManagementOptions; sameDomainCookiesOnly?: boolean; externalAnonymousIdCookieName?: string; diff --git a/packages/analytics-js-common/src/types/PluginsManager.ts b/packages/analytics-js-common/src/types/PluginsManager.ts index 7739d58a5..8de784a75 100644 --- a/packages/analytics-js-common/src/types/PluginsManager.ts +++ b/packages/analytics-js-common/src/types/PluginsManager.ts @@ -12,7 +12,6 @@ export interface IPluginsManager { } export type PluginName = - | 'BeaconQueue' | 'CustomConsentManager' | 'DeviceModeDestinations' | 'DeviceModeTransformation' @@ -24,5 +23,4 @@ export type PluginName = | 'OneTrustConsentManager' | 'StorageEncryption' | 'StorageEncryptionLegacy' - | 'StorageMigrator' - | 'XhrQueue'; + | 'StorageMigrator'; diff --git a/packages/analytics-js-plugins/__mocks__/state.ts b/packages/analytics-js-plugins/__mocks__/state.ts index 881eb3fc5..4ef5c1446 100644 --- a/packages/analytics-js-plugins/__mocks__/state.ts +++ b/packages/analytics-js-plugins/__mocks__/state.ts @@ -11,7 +11,6 @@ const defaultStateValues: ApplicationState = { isCookieStorageAvailable: signal(false), isSessionStorageAvailable: signal(false), }, - isBeaconAvailable: signal(false), isLegacyDOM: signal(false), isUaCHAvailable: signal(false), isCryptoAvailable: signal(false), @@ -88,8 +87,6 @@ const defaultStateValues: ApplicationState = { sameSiteCookie: 'Lax', polyfillIfRequired: true, integrations: { All: true }, - useBeacon: false, - beaconQueueOptions: {}, destinationsQueueOptions: {}, queueOptions: {}, lockIntegrationsVersion: false, diff --git a/packages/analytics-js-plugins/__tests__/beaconQueue/.gitkeep b/packages/analytics-js-plugins/__tests__/beaconQueue/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/analytics-js-plugins/__tests__/xhrQueue/index.test.ts b/packages/analytics-js-plugins/__tests__/xhrQueue/index.test.ts deleted file mode 100644 index c48fb5107..000000000 --- a/packages/analytics-js-plugins/__tests__/xhrQueue/index.test.ts +++ /dev/null @@ -1,307 +0,0 @@ -/* eslint-disable import/no-extraneous-dependencies */ -import { batch } from '@preact/signals-core'; -import { mergeDeepRight } from '@rudderstack/analytics-js-common/utilities/object'; -import { defaultStoreManager } from '@rudderstack/analytics-js-common/__mocks__/StoreManager'; -import type { RudderEvent } from '@rudderstack/analytics-js-common/types/Event'; -import { defaultLogger } from '@rudderstack/analytics-js-common/__mocks__/Logger'; -import { defaultErrorHandler } from '@rudderstack/analytics-js-common/__mocks__/ErrorHandler'; -import type { ExtensionPoint } from '@rudderstack/analytics-js-common/types/PluginEngine'; -import { defaultHttpClient } from '@rudderstack/analytics-js-common/__mocks__/HttpClient'; -import type { RetryQueue } from '../../src/utilities/retryQueue/RetryQueue'; -import type { QueueItem, QueueItemData } from '../../src/types/plugins'; -import { resetState, state } from '../../__mocks__/state'; -import { XhrQueue } from '../../src/xhrQueue'; -import { Schedule } from '../../src/utilities/retryQueue/Schedule'; - -jest.mock('@rudderstack/analytics-js-common/utilities/timestamp', () => ({ - ...jest.requireActual('@rudderstack/analytics-js-common/utilities/timestamp'), - getCurrentTimeFormatted: jest.fn(() => 'sample_timestamp'), -})); - -jest.mock('@rudderstack/analytics-js-common/utilities/uuId', () => ({ - ...jest.requireActual('@rudderstack/analytics-js-common/utilities/uuId'), - generateUUID: jest.fn(() => 'sample_uuid'), -})); - -describe('XhrQueue', () => { - beforeAll(() => { - resetState(); - batch(() => { - state.lifecycle.writeKey.value = 'sampleWriteKey'; - state.lifecycle.activeDataplaneUrl.value = 'https://sampleurl.com'; - state.loadOptions.value.queueOptions = { - minRetryDelay: 1000, - maxRetryDelay: 360000, - backoffFactor: 2, - maxAttempts: 10, - maxItems: 100, - }; - }); - }); - - it('should add itself to the loaded plugins list on initialized', () => { - XhrQueue()?.initialize?.(state); - - expect(state.plugins.loadedPlugins.value).toContain('XhrQueue'); - }); - - it('should return a queue object on init', () => { - const queue = (XhrQueue()?.dataplaneEventsQueue as ExtensionPoint).init?.( - state, - defaultHttpClient, - defaultStoreManager, - defaultErrorHandler, - defaultLogger, - ) as RetryQueue; - - expect(queue).toBeDefined(); - expect(queue.name).toBe('rudder_sampleWriteKey'); - }); - - it('should add item in queue on enqueue', () => { - const queue = (XhrQueue()?.dataplaneEventsQueue as ExtensionPoint).init?.( - state, - defaultHttpClient, - defaultStoreManager, - ) as RetryQueue; - - const addItemSpy = jest.spyOn(queue, 'addItem'); - - const event = { - type: 'track', - event: 'test', - userId: 'test', - properties: { - test: 'test', - }, - anonymousId: 'sampleAnonId', - messageId: 'test', - originalTimestamp: 'test', - } as unknown as RudderEvent; - - (XhrQueue()?.dataplaneEventsQueue as ExtensionPoint).enqueue?.(state, queue, event); - - expect(addItemSpy).toHaveBeenCalledWith({ - url: 'https://sampleurl.com/v1/track', - headers: { - AnonymousId: 'c2FtcGxlQW5vbklk', // Base64 encoded anonymousId - }, - event: mergeDeepRight(event, { sentAt: 'sample_timestamp' }), - }); - - addItemSpy.mockRestore(); - }); - - it('should process queue item on start', () => { - // Mock getAsyncData to return a successful response - - defaultHttpClient.getAsyncData.mockImplementation(({ callback }) => { - callback?.(true); - }); - const queue = (XhrQueue()?.dataplaneEventsQueue as ExtensionPoint).init?.( - state, - defaultHttpClient, - defaultStoreManager, - ) as RetryQueue; - - const event = { - type: 'track', - event: 'test', - userId: 'test', - properties: { - test: 'test', - }, - anonymousId: 'sampleAnonId', - messageId: 'test', - originalTimestamp: 'test', - } as unknown as RudderEvent; - - const queueProcessCbSpy = jest.spyOn(queue, 'processQueueCb'); - - (XhrQueue()?.dataplaneEventsQueue as ExtensionPoint).enqueue?.(state, queue, event); - - // Explicitly start the queue to process the item - // In actual implementation, this is done based on the state signals - queue.start(); - - expect(queueProcessCbSpy).toHaveBeenCalledWith( - { - url: 'https://sampleurl.com/v1/track', - headers: { - AnonymousId: 'c2FtcGxlQW5vbklk', // Base64 encoded anonymousId - }, - event: mergeDeepRight(event, { sentAt: 'sample_timestamp' }), - }, - expect.any(Function), - 0, - 10, - true, - ); - - // Item is successfully processed and removed from queue - expect((queue.getStorageEntry('queue') as QueueItem[]).length).toBe(0); - - queueProcessCbSpy.mockRestore(); - }); - - it('should log error on retryable failure and requeue the item', () => { - // Mock getAsyncData to return a retryable failure - - defaultHttpClient.getAsyncData.mockImplementation(({ callback }) => { - callback?.(false, { error: 'some error', xhr: { status: 429 } }); - }); - const queue = (XhrQueue()?.dataplaneEventsQueue as ExtensionPoint).init?.( - state, - defaultHttpClient, - defaultStoreManager, - undefined, - defaultLogger, - ) as RetryQueue; - - const schedule = new Schedule(); - // Override the timestamp generation function to return a fixed value - schedule.now = () => 1; - - queue.schedule = schedule; - - const event = { - type: 'track', - event: 'test', - userId: 'test', - properties: { - test: 'test', - }, - anonymousId: 'sampleAnonId', - messageId: 'test', - originalTimestamp: 'test', - } as unknown as RudderEvent; - - (XhrQueue()?.dataplaneEventsQueue as ExtensionPoint).enqueue?.(state, queue, event); - - // Explicitly start the queue to process the item - // In actual implementation, this is done based on the state signals - queue.start(); - - expect(defaultLogger.error).toHaveBeenCalledWith( - 'XhrQueuePlugin:: Failed to deliver event(s) to https://sampleurl.com/v1/track. It/they will be retried.', - ); - - // The element is requeued - expect(queue.getStorageEntry('queue')).toStrictEqual([ - { - item: { - url: 'https://sampleurl.com/v1/track', - headers: { - AnonymousId: 'c2FtcGxlQW5vbklk', // Base64 encoded anonymousId - }, - event: mergeDeepRight(event, { sentAt: 'sample_timestamp' }), - }, - attemptNumber: 1, - id: 'sample_uuid', - time: 1 + 1000 * 2 ** 1, // this is the delay calculation in RetryQueue - type: 'Single', - }, - ]); - }); - - it('should queue and process events when running in batch mode', () => { - batch(() => { - state.loadOptions.value.queueOptions = { - minRetryDelay: 1000, - maxRetryDelay: 360000, - backoffFactor: 2, - maxAttempts: 10, - maxItems: 100, - batch: { - enabled: true, - maxSize: 1024, - maxItems: 2, - }, - }; - }); - - const queue = (XhrQueue()?.dataplaneEventsQueue as ExtensionPoint).init?.( - state, - defaultHttpClient, - defaultStoreManager, - undefined, - defaultLogger, - ) as RetryQueue; - const queueProcessCbSpy = jest.spyOn(queue, 'processQueueCb'); - - const schedule = new Schedule(); - // Override the timestamp generation function to return a fixed value - schedule.now = () => 1; - - queue.schedule = schedule; - - const event = { - type: 'track', - event: 'test', - userId: 'test', - properties: { - test: 'test', - }, - anonymousId: 'sampleAnonId', - messageId: 'test', - originalTimestamp: 'test', - } as unknown as RudderEvent; - - const event2 = { - type: 'track', - event: 'test2', - userId: 'test2', - properties: { - test2: 'test2', - }, - anonymousId: 'sampleAnonId', - messageId: 'test2', - originalTimestamp: 'test2', - } as unknown as RudderEvent; - - (XhrQueue()?.dataplaneEventsQueue as ExtensionPoint).enqueue?.(state, queue, event); - (XhrQueue()?.dataplaneEventsQueue as ExtensionPoint).enqueue?.(state, queue, event2); - - // Explicitly start the queue to process the item - // In actual implementation, this is done based on the state signals - queue.start(); - - expect(queueProcessCbSpy).toHaveBeenCalledWith( - [ - { - url: 'https://sampleurl.com/v1/track', - headers: { - AnonymousId: 'c2FtcGxlQW5vbklk', // Base64 encoded anonymousId - }, - event: mergeDeepRight(event, { sentAt: 'sample_timestamp' }), - }, - { - url: 'https://sampleurl.com/v1/track', - headers: { - AnonymousId: 'c2FtcGxlQW5vbklk', // Base64 encoded anonymousId - }, - event: mergeDeepRight(event2, { sentAt: 'sample_timestamp' }), - }, - ], - expect.any(Function), - 0, - 10, - true, - ); - - expect(defaultHttpClient.getAsyncData).toHaveBeenCalledWith({ - url: 'https://sampleurl.com/v1/batch', - options: { - method: 'POST', - headers: { - AnonymousId: 'c2FtcGxlQW5vbklk', // Base64 encoded anonymousId - }, - sendRawData: true, - data: '{"batch":[{"type":"track","event":"test","userId":"test","properties":{"test":"test"},"anonymousId":"sampleAnonId","messageId":"test","originalTimestamp":"test","sentAt":"sample_timestamp"},{"type":"track","event":"test2","userId":"test2","properties":{"test2":"test2"},"anonymousId":"sampleAnonId","messageId":"test2","originalTimestamp":"test2","sentAt":"sample_timestamp"}],"sentAt":"sample_timestamp"}', - }, - isRawResponse: true, - timeout: 30000, - callback: expect.any(Function), - }); - }); -}); diff --git a/packages/analytics-js-plugins/__tests__/xhrQueue/utilities.test.ts b/packages/analytics-js-plugins/__tests__/xhrQueue/utilities.test.ts deleted file mode 100644 index c8a4957f3..000000000 --- a/packages/analytics-js-plugins/__tests__/xhrQueue/utilities.test.ts +++ /dev/null @@ -1,394 +0,0 @@ -import type { RudderEvent } from '@rudderstack/analytics-js-common/types/Event'; -import type { ResponseDetails } from '@rudderstack/analytics-js-common/types/HttpClient'; -import { getCurrentTimeFormatted } from '@rudderstack/analytics-js-common/utilities/timestamp'; -import { defaultLogger } from '@rudderstack/analytics-js-common/__mocks__/Logger'; -import type { ApiObject } from '@rudderstack/analytics-js-common/types/ApiObject'; -import { - getNormalizedQueueOptions, - getDeliveryUrl, - getBatchDeliveryUrl, - logErrorOnFailure, - getRequestInfo, - getBatchDeliveryPayload, -} from '../../src/xhrQueue/utilities'; -import { resetState, state } from '../../__mocks__/state'; - -jest.mock('@rudderstack/analytics-js-common/utilities/timestamp', () => ({ - getCurrentTimeFormatted: () => '2021-01-01T00:00:00.000Z', -})); - -describe('xhrQueue Plugin Utilities', () => { - describe('getNormalizedQueueOptions', () => { - it('should return default queue options if input queue options is empty object', () => { - const queueOptions = getNormalizedQueueOptions({}); - - expect(queueOptions).toEqual({ - maxRetryDelay: 360000, - minRetryDelay: 1000, - backoffFactor: 2, - maxAttempts: 10, - maxItems: 100, - }); - }); - - it('should return default queue options if input queue options is null', () => { - // @ts-expect-error Testing for null - const queueOptions = getNormalizedQueueOptions(null); - - expect(queueOptions).toEqual({ - maxRetryDelay: 360000, - minRetryDelay: 1000, - backoffFactor: 2, - maxAttempts: 10, - maxItems: 100, - }); - }); - - it('should return default queue options if input queue options is undefined', () => { - // @ts-expect-error Testing for undefined - const queueOptions = getNormalizedQueueOptions(undefined); - - expect(queueOptions).toEqual({ - maxRetryDelay: 360000, - minRetryDelay: 1000, - backoffFactor: 2, - maxAttempts: 10, - maxItems: 100, - }); - }); - - it('should return queue options with default values for missing fields', () => { - const queueOptions = getNormalizedQueueOptions({ - maxRetryDelay: 720000, - minRetryDelay: 3000, - maxAttempts: 100, - }); - - expect(queueOptions).toEqual({ - maxRetryDelay: 720000, - minRetryDelay: 3000, - backoffFactor: 2, - maxAttempts: 100, - maxItems: 100, - }); - }); - }); - - describe('getDeliveryUrl', () => { - it('should return delivery url if valid dataplane url and event type are provided', () => { - const deliveryUrl = getDeliveryUrl('https://test.com', 'track'); - - expect(deliveryUrl).toEqual('https://test.com/v1/track'); - }); - - it('should return delivery url if even if dataplane url contains extra slashes', () => { - const deliveryUrl = getDeliveryUrl('https://test.com/', 'track'); - - expect(deliveryUrl).toEqual('https://test.com/v1/track'); - }); - - it('should return delivery url if dataplane url contains additional path components', () => { - const deliveryUrl = getDeliveryUrl('https://test.com/some/path////', 'track'); - - expect(deliveryUrl).toEqual('https://test.com/some/path/v1/track'); - }); - }); - - describe('getBatchDeliveryUrl', () => { - it('should return batch delivery url if valid dataplane url and event type are provided', () => { - const deliveryUrl = getBatchDeliveryUrl('https://test.com'); - - expect(deliveryUrl).toEqual('https://test.com/v1/batch'); - }); - - it('should return batch delivery url if even if dataplane url contains extra slashes', () => { - const deliveryUrl = getBatchDeliveryUrl('https://test.com/'); - - expect(deliveryUrl).toEqual('https://test.com/v1/batch'); - }); - - it('should return batch delivery url if dataplane url contains additional path components', () => { - const deliveryUrl = getBatchDeliveryUrl('https://test.com/some/path///'); - - expect(deliveryUrl).toEqual('https://test.com/some/path/v1/batch'); - }); - }); - - describe('logErrorOnFailure', () => { - it('should not log error if there is no error', () => { - const details = { - response: {}, - } as ResponseDetails; - - logErrorOnFailure(details, 'https://test.com/v1/page', false, 1, 10, defaultLogger); - - expect(defaultLogger.error).not.toHaveBeenCalled(); - }); - - it('should log an error for delivery failure', () => { - const details = { - error: {}, - } as ResponseDetails; - - logErrorOnFailure(details, 'https://test.com/v1/page', false, 1, 10, defaultLogger); - - expect(defaultLogger.error).toHaveBeenCalledWith( - 'XhrQueuePlugin:: Failed to deliver event(s) to https://test.com/v1/page. The event(s) will be dropped.', - ); - }); - - it('should log an error for retryable network failure', () => { - const details = { - error: {}, - xhr: { - status: 429, - }, - } as ResponseDetails; - - logErrorOnFailure(details, 'https://test.com/v1/page', true, 1, 10, defaultLogger); - - expect(defaultLogger.error).toHaveBeenCalledWith( - 'XhrQueuePlugin:: Failed to deliver event(s) to https://test.com/v1/page. It/they will be retried. Retry attempt 1 of 10.', - ); - - // Retryable error but it's the first attempt - // @ts-expect-error Needed to set the status for testing - (details.xhr as XMLHttpRequest).status = 429; - - logErrorOnFailure(details, 'https://test.com/v1/page', true, 0, 10, defaultLogger); - - expect(defaultLogger.error).toHaveBeenCalledWith( - 'XhrQueuePlugin:: Failed to deliver event(s) to https://test.com/v1/page. It/they will be retried.', - ); - - // 500 error - // @ts-expect-error Needed to set the status for testing - (details.xhr as XMLHttpRequest).status = 500; - - logErrorOnFailure(details, 'https://test.com/v1/page', true, 1, 10, defaultLogger); - - expect(defaultLogger.error).toHaveBeenCalledWith( - 'XhrQueuePlugin:: Failed to deliver event(s) to https://test.com/v1/page. It/they will be retried. Retry attempt 1 of 10.', - ); - - // 5xx error - // @ts-expect-error Needed to set the status for testing - (details.xhr as XMLHttpRequest).status = 501; - - logErrorOnFailure(details, 'https://test.com/v1/page', true, 1, 10, defaultLogger); - - expect(defaultLogger.error).toHaveBeenCalledWith( - 'XhrQueuePlugin:: Failed to deliver event(s) to https://test.com/v1/page. It/they will be retried. Retry attempt 1 of 10.', - ); - - // 600 error - // @ts-expect-error Needed to set the status for testing - (details.xhr as XMLHttpRequest).status = 600; - - logErrorOnFailure(details, 'https://test.com/v1/page', true, 1, 10, defaultLogger); - - expect(defaultLogger.error).toHaveBeenCalledWith( - 'XhrQueuePlugin:: Failed to deliver event(s) to https://test.com/v1/page. The event(s) will be dropped.', - ); - - // Retryable error but exhausted all tries - // @ts-expect-error Needed to set the status for testing - (details.xhr as XMLHttpRequest).status = 520; - - logErrorOnFailure(details, 'https://test.com/v1/page', false, 10, 10, defaultLogger); - - expect(defaultLogger.error).toHaveBeenCalledWith( - 'XhrQueuePlugin:: Failed to deliver event(s) to https://test.com/v1/page. Retries exhausted (10). The event(s) will be dropped.', - ); - }); - }); - - describe('getRequestInfo', () => { - beforeEach(() => { - resetState(); - }); - - it('should return request info for single event queue item', () => { - const queueItemData = { - event: { - type: 'track', - properties: { - prop1: 'value1', - }, - } as unknown as RudderEvent, - url: 'https://test.com/v1/track', - headers: { - AnonymousId: 'anonymous-id', - }, - }; - - const requestInfo = getRequestInfo(queueItemData, state, defaultLogger); - - expect(requestInfo).toEqual({ - url: 'https://test.com/v1/track', - headers: { - AnonymousId: 'anonymous-id', - }, - data: '{"type":"track","properties":{"prop1":"value1"},"sentAt":"2021-01-01T00:00:00.000Z"}', - }); - }); - - it('should return request info for batch queue item', () => { - const queueItemData = [ - { - event: { - type: 'track', - properties: { - prop1: 'value1', - }, - } as unknown as RudderEvent, - url: 'https://test.com/v1/track', - headers: { - AnonymousId: 'anonymous-id1', - }, - }, - { - event: { - type: 'track', - properties: { - prop2: 'value2', - }, - } as unknown as RudderEvent, - url: 'https://test.com/v1/track', - headers: { - AnonymousId: 'anonymous-id2', - }, - }, - ]; - - state.lifecycle.activeDataplaneUrl.value = 'https://test.dataplaneurl.com/'; - - const requestInfo = getRequestInfo(queueItemData, state, defaultLogger); - - expect(requestInfo).toEqual({ - url: 'https://test.dataplaneurl.com/v1/batch', - headers: { - AnonymousId: 'anonymous-id1', - }, - data: '{"batch":[{"type":"track","properties":{"prop1":"value1"},"sentAt":"2021-01-01T00:00:00.000Z"},{"type":"track","properties":{"prop2":"value2"},"sentAt":"2021-01-01T00:00:00.000Z"}],"sentAt":"2021-01-01T00:00:00.000Z"}', - }); - }); - }); - - describe('getBatchDeliveryPayload', () => { - const currentTime = getCurrentTimeFormatted(); - it('should return stringified batch event payload', () => { - const events = [ - { - channel: 'test', - type: 'track', - anonymousId: 'test', - properties: { - test: 'test', - }, - } as unknown as RudderEvent, - { - channel: 'test', - type: 'track', - anonymousId: 'test', - properties: { - test1: 'test1', - }, - } as unknown as RudderEvent, - ]; - - expect(getBatchDeliveryPayload(events, currentTime, defaultLogger)).toBe( - '{"batch":[{"channel":"test","type":"track","anonymousId":"test","properties":{"test":"test"}},{"channel":"test","type":"track","anonymousId":"test","properties":{"test1":"test1"}}],"sentAt":"2021-01-01T00:00:00.000Z"}', - ); - }); - - it('should return stringified event payload filtering the null values', () => { - const events = [ - { - channel: 'test', - type: 'track', - anonymousId: 'test', - userId: null, - properties: { - test: 'test', - test2: null, - }, - } as unknown as RudderEvent, - { - channel: 'test', - type: 'track', - anonymousId: 'test', - groupId: null, - properties: { - test1: 'test1', - test3: { - test4: null, - }, - }, - } as unknown as RudderEvent, - ]; - expect(getBatchDeliveryPayload(events, currentTime, defaultLogger)).toBe( - '{"batch":[{"channel":"test","type":"track","anonymousId":"test","properties":{"test":"test"}},{"channel":"test","type":"track","anonymousId":"test","properties":{"test1":"test1","test3":{}}}],"sentAt":"2021-01-01T00:00:00.000Z"}', - ); - }); - - it('should return string with circular dependencies replaced with static string', () => { - const events: RudderEvent[] = [ - { - channel: 'test', - type: 'track', - anonymousId: 'test', - userId: null, - properties: { - test: 'test', - test2: null, - }, - } as unknown as RudderEvent, - { - channel: 'test', - type: 'track', - anonymousId: 'test', - groupId: null, - properties: { - test1: 'test1', - test3: { - test4: null, - }, - }, - } as unknown as RudderEvent, - ]; - const event2 = events[1] as RudderEvent; - - // Create a circular reference - // @ts-expect-error Testing for circular reference - (event2.properties as ApiObject).test5 = event2; - - expect(getBatchDeliveryPayload(events, currentTime, defaultLogger)).toContain( - '[Circular Reference]', - ); - }); - - it('should return null if the payload cannot be stringified', () => { - const events = [ - { - channel: 'test', - type: 'track', - anonymousId: 'test', - properties: { - someBigInt: BigInt(9007199254740991), - }, - } as unknown as RudderEvent, - { - channel: 'test', - type: 'track', - anonymousId: 'test', - properties: { - test1: 'test1', - }, - } as unknown as RudderEvent, - ]; - - expect(getBatchDeliveryPayload(events, currentTime, defaultLogger)).toBeNull(); - }); - }); -}); diff --git a/packages/analytics-js-plugins/rollup.config.mjs b/packages/analytics-js-plugins/rollup.config.mjs index 4f32c29e8..3ed82b608 100644 --- a/packages/analytics-js-plugins/rollup.config.mjs +++ b/packages/analytics-js-plugins/rollup.config.mjs @@ -36,7 +36,6 @@ const moduleType = process.env.MODULE_TYPE || 'cdn'; const isNpmPackageBuild = moduleType === 'npm'; const isCDNPackageBuild = moduleType === 'cdn'; const pluginsMap = { - './BeaconQueue': './src/beaconQueue/index.ts', './CustomConsentManager': './src/customConsentManager/index.ts', './DeviceModeDestinations': './src/deviceModeDestinations/index.ts', './DeviceModeTransformation': './src/deviceModeTransformation/index.ts', @@ -49,7 +48,6 @@ const pluginsMap = { './StorageEncryption': './src/storageEncryption/index.ts', './StorageEncryptionLegacy': './src/storageEncryptionLegacy/index.ts', './StorageMigrator': './src/storageMigrator/index.ts', - './XhrQueue': './src/xhrQueue/index.ts', }; const bugsnagSDKUrl = 'https://d2wy8f7a9ursnm.cloudfront.net/v6/bugsnag.min.js'; diff --git a/packages/analytics-js-plugins/src/beaconQueue/constants.ts b/packages/analytics-js-plugins/src/beaconQueue/constants.ts deleted file mode 100644 index c7a117b4c..000000000 --- a/packages/analytics-js-plugins/src/beaconQueue/constants.ts +++ /dev/null @@ -1,27 +0,0 @@ -const DEFAULT_BEACON_QUEUE_MAX_SIZE = 10; -const DEFAULT_BEACON_QUEUE_FLUSH_INTERVAL_MS = 10 * 60 * 1000; // 10 minutes - -// Limit of the Beacon transfer mechanism on the browsers -const MAX_BATCH_PAYLOAD_SIZE_BYTES = 64 * 1024; // 64 KB - -const DEFAULT_BEACON_QUEUE_OPTIONS = { - maxItems: DEFAULT_BEACON_QUEUE_MAX_SIZE, - flushQueueInterval: DEFAULT_BEACON_QUEUE_FLUSH_INTERVAL_MS, -}; - -const REQUEST_TIMEOUT_MS = 10 * 1000; // 10 seconds - -const DATA_PLANE_API_VERSION = 'v1'; - -const QUEUE_NAME = 'rudder_beacon'; - -const BEACON_QUEUE_PLUGIN = 'BeaconQueuePlugin'; - -export { - MAX_BATCH_PAYLOAD_SIZE_BYTES, - DEFAULT_BEACON_QUEUE_OPTIONS, - REQUEST_TIMEOUT_MS, - DATA_PLANE_API_VERSION, - QUEUE_NAME, - BEACON_QUEUE_PLUGIN, -}; diff --git a/packages/analytics-js-plugins/src/beaconQueue/index.ts b/packages/analytics-js-plugins/src/beaconQueue/index.ts deleted file mode 100644 index cb28def22..000000000 --- a/packages/analytics-js-plugins/src/beaconQueue/index.ts +++ /dev/null @@ -1,150 +0,0 @@ -/* eslint-disable no-param-reassign */ -import type { ApplicationState } from '@rudderstack/analytics-js-common/types/ApplicationState'; -import type { IHttpClient } from '@rudderstack/analytics-js-common/types/HttpClient'; -import type { IStoreManager } from '@rudderstack/analytics-js-common/types/Store'; -import type { IErrorHandler } from '@rudderstack/analytics-js-common/types/ErrorHandler'; -import type { ILogger } from '@rudderstack/analytics-js-common/types/Logger'; -import type { - BeaconQueueOpts, - QueueOpts, -} from '@rudderstack/analytics-js-common/types/LoadOptions'; -import type { RudderEvent } from '@rudderstack/analytics-js-common/types/Event'; -import type { ExtensionPlugin } from '@rudderstack/analytics-js-common/types/PluginEngine'; -import type { PluginName } from '@rudderstack/analytics-js-common/types/PluginsManager'; -import type { DoneCallback, IQueue } from '../types/plugins'; -import { - getNormalizedBeaconQueueOptions, - getDeliveryUrl, - getBatchDeliveryPayload, -} from './utilities'; -import { BEACON_QUEUE_PLUGIN, MAX_BATCH_PAYLOAD_SIZE_BYTES, QUEUE_NAME } from './constants'; -import type { BeaconQueueBatchItemData, BeaconQueueItemData } from './types'; -import { - BEACON_PLUGIN_EVENTS_QUEUE_DEBUG, - BEACON_QUEUE_SEND_ERROR, - BEACON_QUEUE_DELIVERY_ERROR, -} from './logMessages'; -import { RetryQueue } from '../utilities/retryQueue/RetryQueue'; -import { - getCurrentTimeFormatted, - getFinalEventForDeliveryMutator, - LOCAL_STORAGE, - validateEventPayloadSize, -} from '../shared-chunks/common'; - -const pluginName: PluginName = 'BeaconQueue'; - -const BeaconQueue = (): ExtensionPlugin => ({ - name: pluginName, - deps: [], - initialize: (state: ApplicationState) => { - state.plugins.loadedPlugins.value = [...state.plugins.loadedPlugins.value, pluginName]; - }, - dataplaneEventsQueue: { - /** - * Initialize the queue for delivery - * @param state Application state - * @param httpClient http client instance - * @param storeManager Store Manager instance - * @param errorHandler Error handler instance - * @param logger Logger instance - * @returns BeaconItemsQueue instance - */ - init( - state: ApplicationState, - httpClient: IHttpClient, - storeManager: IStoreManager, - errorHandler?: IErrorHandler, - logger?: ILogger, - ): IQueue { - const writeKey = state.lifecycle.writeKey.value as string; - const dataplaneUrl = state.lifecycle.activeDataplaneUrl.value as string; - const url = getDeliveryUrl(dataplaneUrl, writeKey); - - const finalQOpts: BeaconQueueOpts = getNormalizedBeaconQueueOptions( - state.loadOptions.value.beaconQueueOptions ?? {}, - ); - - const queueProcessCallback = (itemData: BeaconQueueBatchItemData, done: DoneCallback) => { - logger?.debug(BEACON_PLUGIN_EVENTS_QUEUE_DEBUG(BEACON_QUEUE_PLUGIN)); - const currentTime = getCurrentTimeFormatted(); - const finalEvents = itemData.map((queueItemData: BeaconQueueItemData) => - getFinalEventForDeliveryMutator(queueItemData.event, currentTime), - ); - const data = getBatchDeliveryPayload(finalEvents, currentTime, logger); - - if (data) { - try { - const isEnqueuedInBeacon = navigator.sendBeacon(url, data); - if (!isEnqueuedInBeacon) { - logger?.error(BEACON_QUEUE_SEND_ERROR(BEACON_QUEUE_PLUGIN)); - } - - done(null, isEnqueuedInBeacon); - } catch (err) { - errorHandler?.onError(err, BEACON_QUEUE_PLUGIN, BEACON_QUEUE_DELIVERY_ERROR(url)); - // Remove the item from queue - done(null); - } - } else { - // Mark the item as done so that it can be removed from the queue - done(null); - } - }; - - const eventsQueue = new RetryQueue( - `${QUEUE_NAME}_${writeKey}`, - { - batch: { - enabled: true, - flushInterval: finalQOpts.flushQueueInterval, - maxSize: MAX_BATCH_PAYLOAD_SIZE_BYTES, // set the hard limit - maxItems: finalQOpts.maxItems, - }, - } as QueueOpts, - queueProcessCallback, - storeManager, - LOCAL_STORAGE, - logger, - (itemData: BeaconQueueItemData[]): number => { - const currentTime = getCurrentTimeFormatted(); - const events = itemData.map((queueItemData: BeaconQueueItemData) => queueItemData.event); - // type casting to Blob as we know that the event has already been validated prior to enqueue - return (getBatchDeliveryPayload(events, currentTime, logger) as Blob).size; - }, - ); - - return eventsQueue; - }, - - /** - * Add event to the queue for delivery - * @param state Application state - * @param eventsQueue IQueue instance - * @param event RudderEvent object - * @param errorHandler Error handler instance - * @param logger Logger instance - * @returns none - */ - enqueue( - state: ApplicationState, - eventsQueue: IQueue, - event: RudderEvent, - errorHandler?: IErrorHandler, - logger?: ILogger, - ): void { - // sentAt is only added here for the validation step - // It'll be updated to the latest timestamp during actual delivery - event.sentAt = getCurrentTimeFormatted(); - validateEventPayloadSize(event, logger); - - eventsQueue.addItem({ - event, - } as BeaconQueueItemData); - }, - }, -}); - -export { BeaconQueue }; - -export default BeaconQueue; diff --git a/packages/analytics-js-plugins/src/beaconQueue/logMessages.ts b/packages/analytics-js-plugins/src/beaconQueue/logMessages.ts deleted file mode 100644 index 47728d398..000000000 --- a/packages/analytics-js-plugins/src/beaconQueue/logMessages.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { LOG_CONTEXT_SEPARATOR } from '../shared-chunks/common'; - -const BEACON_PLUGIN_EVENTS_QUEUE_DEBUG = (context: string): string => - `${context}${LOG_CONTEXT_SEPARATOR}Sending events to data plane.`; - -const BEACON_QUEUE_STRING_CONVERSION_FAILURE_ERROR = (context: string): string => - `${context}${LOG_CONTEXT_SEPARATOR}Failed to convert events batch object to string.`; - -const BEACON_QUEUE_BLOB_CONVERSION_FAILURE_ERROR = (context: string): string => - `${context}${LOG_CONTEXT_SEPARATOR}Failed to convert events batch object to Blob.`; - -const BEACON_QUEUE_SEND_ERROR = (context: string): string => - `${context}${LOG_CONTEXT_SEPARATOR}Failed to send events batch data to the browser's beacon queue. The events will be dropped.`; - -const BEACON_QUEUE_DELIVERY_ERROR = (url: string): string => - `Failed to send events batch data to the browser's beacon queue for URL ${url}.`; - -export { - BEACON_PLUGIN_EVENTS_QUEUE_DEBUG, - BEACON_QUEUE_STRING_CONVERSION_FAILURE_ERROR, - BEACON_QUEUE_BLOB_CONVERSION_FAILURE_ERROR, - BEACON_QUEUE_SEND_ERROR, - BEACON_QUEUE_DELIVERY_ERROR, -}; diff --git a/packages/analytics-js-plugins/src/beaconQueue/types.ts b/packages/analytics-js-plugins/src/beaconQueue/types.ts deleted file mode 100644 index d20c401fc..000000000 --- a/packages/analytics-js-plugins/src/beaconQueue/types.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { RudderEvent } from '@rudderstack/analytics-js-common/types/Event'; - -export type BeaconQueueItemData = { - event: RudderEvent; -}; - -export type BeaconQueueBatchItemData = BeaconQueueItemData[]; - -export type BeaconBatchData = { - batch: RudderEvent[]; - sentAt: string; -}; diff --git a/packages/analytics-js-plugins/src/beaconQueue/utilities.ts b/packages/analytics-js-plugins/src/beaconQueue/utilities.ts deleted file mode 100644 index 238477aab..000000000 --- a/packages/analytics-js-plugins/src/beaconQueue/utilities.ts +++ /dev/null @@ -1,71 +0,0 @@ -import type { RudderEvent } from '@rudderstack/analytics-js-common/types/Event'; -import type { ILogger } from '@rudderstack/analytics-js-common/types/Logger'; -import type { BeaconQueueOpts } from '@rudderstack/analytics-js-common/types/LoadOptions'; -import { - BEACON_QUEUE_STRING_CONVERSION_FAILURE_ERROR, - BEACON_QUEUE_BLOB_CONVERSION_FAILURE_ERROR, -} from './logMessages'; -import { - BEACON_QUEUE_PLUGIN, - DATA_PLANE_API_VERSION, - DEFAULT_BEACON_QUEUE_OPTIONS, -} from './constants'; -import type { BeaconBatchData } from './types'; -import { - mergeDeepRight, - removeDuplicateSlashes, - stringifyWithoutCircular, -} from '../shared-chunks/common'; - -/** - * Utility to get the stringified event payload as Blob - * @param events RudderEvent object array - * @param logger Logger instance - * @returns stringified events payload as Blob, undefined if error occurs. - */ -const getBatchDeliveryPayload = ( - events: RudderEvent[], - currentTime: string, - logger?: ILogger, -): Blob | undefined => { - const data: BeaconBatchData = { - batch: events, - sentAt: currentTime, - }; - - try { - const blobPayload = stringifyWithoutCircular(data, true); - const blobOptions: BlobPropertyBag = { type: 'text/plain' }; - - if (blobPayload) { - return new Blob([blobPayload], blobOptions); - } - logger?.error(BEACON_QUEUE_STRING_CONVERSION_FAILURE_ERROR(BEACON_QUEUE_PLUGIN)); - } catch (err) { - logger?.error(BEACON_QUEUE_BLOB_CONVERSION_FAILURE_ERROR(BEACON_QUEUE_PLUGIN), err); - } - return undefined; -}; - -const getNormalizedBeaconQueueOptions = (queueOpts: BeaconQueueOpts): BeaconQueueOpts => - mergeDeepRight(DEFAULT_BEACON_QUEUE_OPTIONS, queueOpts); - -const getDeliveryUrl = (dataplaneUrl: string, writeKey: string): string => { - const dpUrl = new URL(dataplaneUrl); - return new URL( - removeDuplicateSlashes( - [ - dpUrl.pathname, - '/', - 'beacon', - '/', - DATA_PLANE_API_VERSION, - '/', - `batch?writeKey=${writeKey}`, - ].join(''), - ), - dpUrl, - ).href; -}; - -export { getBatchDeliveryPayload, getDeliveryUrl, getNormalizedBeaconQueueOptions }; diff --git a/packages/analytics-js-plugins/src/index.ts b/packages/analytics-js-plugins/src/index.ts index 32017dc8a..076ca2bd2 100644 --- a/packages/analytics-js-plugins/src/index.ts +++ b/packages/analytics-js-plugins/src/index.ts @@ -1,4 +1,3 @@ -export { default as BeaconQueue } from './beaconQueue'; export { default as CustomConsentManager } from './customConsentManager'; export { default as DeviceModeDestinations } from './deviceModeDestinations'; export { default as DeviceModeTransformation } from './deviceModeTransformation'; @@ -11,4 +10,3 @@ export { default as KetchConsentManager } from './ketchConsentManager'; export { default as StorageEncryption } from './storageEncryption'; export { default as StorageEncryptionLegacy } from './storageEncryptionLegacy'; export { default as StorageMigrator } from './storageMigrator'; -export { default as XhrQueue } from './xhrQueue'; diff --git a/packages/analytics-js-plugins/src/xhrQueue/constants.ts b/packages/analytics-js-plugins/src/xhrQueue/constants.ts deleted file mode 100644 index 4716e1cb8..000000000 --- a/packages/analytics-js-plugins/src/xhrQueue/constants.ts +++ /dev/null @@ -1,23 +0,0 @@ -const DEFAULT_RETRY_QUEUE_OPTIONS = { - maxRetryDelay: 360000, - minRetryDelay: 1000, - backoffFactor: 2, - maxAttempts: 10, - maxItems: 100, -}; - -const REQUEST_TIMEOUT_MS = 30 * 1000; // 30 seconds - -const DATA_PLANE_API_VERSION = 'v1'; - -const QUEUE_NAME = 'rudder'; - -const XHR_QUEUE_PLUGIN = 'XhrQueuePlugin'; - -export { - DEFAULT_RETRY_QUEUE_OPTIONS, - REQUEST_TIMEOUT_MS, - DATA_PLANE_API_VERSION, - QUEUE_NAME, - XHR_QUEUE_PLUGIN, -}; diff --git a/packages/analytics-js-plugins/src/xhrQueue/index.ts b/packages/analytics-js-plugins/src/xhrQueue/index.ts deleted file mode 100644 index 097b82c19..000000000 --- a/packages/analytics-js-plugins/src/xhrQueue/index.ts +++ /dev/null @@ -1,163 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -/* eslint-disable no-param-reassign */ -import type { ExtensionPlugin } from '@rudderstack/analytics-js-common/types/PluginEngine'; -import type { ApplicationState } from '@rudderstack/analytics-js-common/types/ApplicationState'; -import type { IHttpClient } from '@rudderstack/analytics-js-common/types/HttpClient'; -import type { IErrorHandler } from '@rudderstack/analytics-js-common/types/ErrorHandler'; -import type { ILogger } from '@rudderstack/analytics-js-common/types/Logger'; -import type { IStoreManager } from '@rudderstack/analytics-js-common/types/Store'; -import type { QueueOpts } from '@rudderstack/analytics-js-common/types/LoadOptions'; -import type { RudderEvent } from '@rudderstack/analytics-js-common/types/Event'; -import type { PluginName } from '@rudderstack/analytics-js-common/types/PluginsManager'; -import { - getNormalizedQueueOptions, - getDeliveryUrl, - logErrorOnFailure, - getRequestInfo, - getBatchDeliveryPayload, -} from './utilities'; -import type { DoneCallback, IQueue, QueueItemData } from '../types/plugins'; -import { RetryQueue } from '../utilities/retryQueue/RetryQueue'; -import { QUEUE_NAME, REQUEST_TIMEOUT_MS } from './constants'; -import type { XHRRetryQueueItemData, XHRQueueItemData } from './types'; -import { - getCurrentTimeFormatted, - isErrRetryable, - LOCAL_STORAGE, - toBase64, - validateEventPayloadSize, -} from '../shared-chunks/common'; - -const pluginName: PluginName = 'XhrQueue'; - -const XhrQueue = (): ExtensionPlugin => ({ - name: pluginName, - deps: [], - initialize: (state: ApplicationState) => { - state.plugins.loadedPlugins.value = [...state.plugins.loadedPlugins.value, pluginName]; - }, - dataplaneEventsQueue: { - /** - * Initialize the queue for delivery - * @param state Application state - * @param httpClient http client instance - * @param storeManager Store Manager instance - * @param errorHandler Error handler instance - * @param logger Logger instance - * @returns RetryQueue instance - */ - init( - state: ApplicationState, - httpClient: IHttpClient, - storeManager: IStoreManager, - errorHandler?: IErrorHandler, - logger?: ILogger, - ): IQueue { - const writeKey = state.lifecycle.writeKey.value as string; - httpClient.setAuthHeader(writeKey); - - const finalQOpts = getNormalizedQueueOptions( - state.loadOptions.value.queueOptions as QueueOpts, - ); - - const eventsQueue = new RetryQueue( - // adding write key to the queue name to avoid conflicts - `${QUEUE_NAME}_${writeKey}`, - finalQOpts, - ( - itemData: QueueItemData, - done: DoneCallback, - attemptNumber?: number, - maxRetryAttempts?: number, - willBeRetried?: boolean, - ) => { - const { data, url, headers } = getRequestInfo( - itemData as XHRRetryQueueItemData, - state, - logger, - ); - - httpClient.getAsyncData({ - url, - options: { - method: 'POST', - headers, - data: data as string, - sendRawData: true, - }, - isRawResponse: true, - timeout: REQUEST_TIMEOUT_MS, - callback: (result, details) => { - // null means item will not be requeued - const queueErrResp = isErrRetryable(details) ? details : null; - - logErrorOnFailure( - details, - url, - willBeRetried, - attemptNumber, - maxRetryAttempts, - logger, - ); - - done(queueErrResp, result); - }, - }); - }, - storeManager, - LOCAL_STORAGE, - logger, - (itemData: XHRQueueItemData[]): number => { - const currentTime = getCurrentTimeFormatted(); - const events = itemData.map((queueItemData: XHRQueueItemData) => queueItemData.event); - // type casting to string as we know that the event has already been validated prior to enqueue - return (getBatchDeliveryPayload(events, currentTime, logger) as string)?.length; - }, - ); - - return eventsQueue; - }, - - /** - * Add event to the queue for delivery - * @param state Application state - * @param eventsQueue RetryQueue instance - * @param event RudderEvent object - * @param errorHandler Error handler instance - * @param logger Logger instance - * @returns none - */ - enqueue( - state: ApplicationState, - eventsQueue: IQueue, - event: RudderEvent, - errorHandler?: IErrorHandler, - logger?: ILogger, - ): void { - // sentAt is only added here for the validation step - // It'll be updated to the latest timestamp during actual delivery - event.sentAt = getCurrentTimeFormatted(); - validateEventPayloadSize(event, logger); - - const dataplaneUrl = state.lifecycle.activeDataplaneUrl.value as string; - const url = getDeliveryUrl(dataplaneUrl, event.type); - // Other default headers are added by the HttpClient - // Auth header is added during initialization - const headers = { - // To maintain event ordering while using the HTTP API as per is documentation, - // make sure to include anonymousId as a header - AnonymousId: toBase64(event.anonymousId), - }; - - eventsQueue.addItem({ - url, - headers, - event, - } as XHRRetryQueueItemData); - }, - }, -}); - -export { XhrQueue }; - -export default XhrQueue; diff --git a/packages/analytics-js-plugins/src/xhrQueue/logMessages.ts b/packages/analytics-js-plugins/src/xhrQueue/logMessages.ts deleted file mode 100644 index a655a9122..000000000 --- a/packages/analytics-js-plugins/src/xhrQueue/logMessages.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { LOG_CONTEXT_SEPARATOR } from '../shared-chunks/common'; - -const EVENT_DELIVERY_FAILURE_ERROR_PREFIX = (context: string, url: string): string => - `${context}${LOG_CONTEXT_SEPARATOR}Failed to deliver event(s) to ${url}.`; - -export { EVENT_DELIVERY_FAILURE_ERROR_PREFIX }; diff --git a/packages/analytics-js-plugins/src/xhrQueue/types.ts b/packages/analytics-js-plugins/src/xhrQueue/types.ts deleted file mode 100644 index 28a089623..000000000 --- a/packages/analytics-js-plugins/src/xhrQueue/types.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { RudderEvent } from '@rudderstack/analytics-js-common/types/Event'; - -export type XHRQueueItemData = { - url: string; - headers: Record; - event: RudderEvent; -}; - -export type XHRQueueBatchItemData = XHRQueueItemData[]; - -export type XHRRetryQueueItemData = XHRQueueItemData | XHRQueueBatchItemData; - -export type XHRBatchPayload = { - batch: RudderEvent[]; - sentAt: string; -}; diff --git a/packages/analytics-js-plugins/src/xhrQueue/utilities.ts b/packages/analytics-js-plugins/src/xhrQueue/utilities.ts deleted file mode 100644 index 7cb0d6961..000000000 --- a/packages/analytics-js-plugins/src/xhrQueue/utilities.ts +++ /dev/null @@ -1,108 +0,0 @@ -import type { QueueOpts } from '@rudderstack/analytics-js-common/types/LoadOptions'; -import type { ResponseDetails } from '@rudderstack/analytics-js-common/types/HttpClient'; -import type { ILogger } from '@rudderstack/analytics-js-common/types/Logger'; -import type { ApplicationState } from '@rudderstack/analytics-js-common/types/ApplicationState'; -import type { RudderEvent } from '@rudderstack/analytics-js-common/types/Event'; -import type { Nullable } from '@rudderstack/analytics-js-common/types/Nullable'; -import { clone } from 'ramda'; -import { DATA_PLANE_API_VERSION, DEFAULT_RETRY_QUEUE_OPTIONS, XHR_QUEUE_PLUGIN } from './constants'; -import type { XHRRetryQueueItemData, XHRQueueItemData, XHRBatchPayload } from './types'; -import { EVENT_DELIVERY_FAILURE_ERROR_PREFIX } from './logMessages'; -import { - getCurrentTimeFormatted, - getDeliveryPayload, - getFinalEventForDeliveryMutator, - isErrRetryable, - isUndefined, - mergeDeepRight, - removeDuplicateSlashes, - stringifyWithoutCircular, -} from '../shared-chunks/common'; - -const getBatchDeliveryPayload = ( - events: RudderEvent[], - currentTime: string, - logger?: ILogger, -): Nullable => { - const batchPayload: XHRBatchPayload = { batch: events, sentAt: currentTime }; - return stringifyWithoutCircular(batchPayload, true, undefined, logger); -}; - -const getNormalizedQueueOptions = (queueOpts: QueueOpts): QueueOpts => - mergeDeepRight(DEFAULT_RETRY_QUEUE_OPTIONS, queueOpts); - -const getDeliveryUrl = (dataplaneUrl: string, endpoint: string): string => { - const dpUrl = new URL(dataplaneUrl); - return new URL( - removeDuplicateSlashes([dpUrl.pathname, '/', DATA_PLANE_API_VERSION, '/', endpoint].join('')), - dpUrl, - ).href; -}; - -const getBatchDeliveryUrl = (dataplaneUrl: string): string => getDeliveryUrl(dataplaneUrl, 'batch'); - -const logErrorOnFailure = ( - details: ResponseDetails | undefined, - url: string, - willBeRetried?: boolean, - attemptNumber?: number, - maxRetryAttempts?: number, - logger?: ILogger, -) => { - if (isUndefined(details?.error) || isUndefined(logger)) { - return; - } - - const isRetryableFailure = isErrRetryable(details); - let errMsg = EVENT_DELIVERY_FAILURE_ERROR_PREFIX(XHR_QUEUE_PLUGIN, url); - const dropMsg = `The event(s) will be dropped.`; - if (isRetryableFailure) { - if (willBeRetried) { - errMsg = `${errMsg} It/they will be retried.`; - if ((attemptNumber as number) > 0) { - errMsg = `${errMsg} Retry attempt ${attemptNumber} of ${maxRetryAttempts}.`; - } - } else { - errMsg = `${errMsg} Retries exhausted (${maxRetryAttempts}). ${dropMsg}`; - } - } else { - errMsg = `${errMsg} ${dropMsg}`; - } - logger?.error(errMsg); -}; - -const getRequestInfo = ( - itemData: XHRRetryQueueItemData, - state: ApplicationState, - logger?: ILogger, -) => { - let data; - let headers; - let url: string; - const currentTime = getCurrentTimeFormatted(); - if (Array.isArray(itemData)) { - const finalEvents = itemData.map((queueItemData: XHRQueueItemData) => - getFinalEventForDeliveryMutator(queueItemData.event, currentTime), - ); - data = getBatchDeliveryPayload(finalEvents, currentTime, logger); - headers = itemData[0] ? clone(itemData[0].headers) : {}; - url = getBatchDeliveryUrl(state.lifecycle.activeDataplaneUrl.value as string); - } else { - const { url: eventUrl, event, headers: eventHeaders } = itemData; - const finalEvent = getFinalEventForDeliveryMutator(event, currentTime); - - data = getDeliveryPayload(finalEvent, logger); - headers = clone(eventHeaders); - url = eventUrl; - } - return { data, headers, url }; -}; - -export { - getNormalizedQueueOptions, - getDeliveryUrl, - logErrorOnFailure, - getBatchDeliveryUrl, - getRequestInfo, - getBatchDeliveryPayload, -}; diff --git a/packages/analytics-js/__mocks__/remotePlugins/BeaconQueue.ts b/packages/analytics-js/__mocks__/remotePlugins/BeaconQueue.ts deleted file mode 100644 index dc856bdd6..000000000 --- a/packages/analytics-js/__mocks__/remotePlugins/BeaconQueue.ts +++ /dev/null @@ -1,9 +0,0 @@ -const BeaconQueue = () => ({ - name: 'BeaconQueue', - dataplaneEventsQueue: { - init: jest.fn(() => {}), - enqueue: jest.fn(() => {}), - }, -}); - -export default BeaconQueue; diff --git a/packages/analytics-js/__mocks__/remotePlugins/XhrQueue.ts b/packages/analytics-js/__mocks__/remotePlugins/XhrQueue.ts deleted file mode 100644 index f7451beef..000000000 --- a/packages/analytics-js/__mocks__/remotePlugins/XhrQueue.ts +++ /dev/null @@ -1,9 +0,0 @@ -const XhrQueue = () => ({ - name: 'XhrQueue', - dataplaneEventsQueue: { - init: jest.fn(() => {}), - enqueue: jest.fn(() => {}), - }, -}); - -export default XhrQueue; diff --git a/packages/analytics-js/__tests__/app/RudderAnalytics.test.ts b/packages/analytics-js/__tests__/app/RudderAnalytics.test.ts index 09c3c7480..c57e076a7 100644 --- a/packages/analytics-js/__tests__/app/RudderAnalytics.test.ts +++ b/packages/analytics-js/__tests__/app/RudderAnalytics.test.ts @@ -783,27 +783,10 @@ describe('Core - Rudder Analytics Facade', () => { expect(bufferedEvents).toEqual([]); }); - it('should track Page Loaded event irrespective of useBeacon load option', () => { - const bufferedEvents: PreloadedEventCall[] = []; - rudderAnalyticsInstance.trackPageLifecycleEvents(bufferedEvents, { - useBeacon: false, - autoTrack: { - pageLifecycle: { - enabled: true, - }, - }, - }); - - expect(bufferedEvents).toEqual([ - ['track', 'Page Loaded', {}, { originalTimestamp: expect.any(String) }], - ]); - }); - - it('should track Page Unloaded event if useBeacon is set to true and trackPageLifecycle feature is enabled', () => { + it('should track Page Unloaded event if trackPageLifecycle feature is enabled', () => { const bufferedEvents: PreloadedEventCall[] = []; rudderAnalyticsInstance.track = jest.fn(); rudderAnalyticsInstance.trackPageLifecycleEvents(bufferedEvents, { - useBeacon: true, autoTrack: { pageLifecycle: { enabled: true, diff --git a/packages/analytics-js/__tests__/browser.test.ts b/packages/analytics-js/__tests__/browser.test.ts index 51b5475bd..24dfa51c9 100644 --- a/packages/analytics-js/__tests__/browser.test.ts +++ b/packages/analytics-js/__tests__/browser.test.ts @@ -76,7 +76,7 @@ describe('Test suite for the SDK', () => { }); describe('preload buffer', () => { - it('should process the buffered API calls when SDK script is loaded', async () => { + it.skip('should process the buffered API calls when SDK script is loaded', async () => { // Mocking the xhr function window.XMLHttpRequest = jest.fn(() => xhrMock) as unknown as typeof XMLHttpRequest; @@ -110,7 +110,7 @@ describe('Test suite for the SDK', () => { window.XMLHttpRequest = originalXMLHttpRequest; }); - it('should make network requests when event APIs are invoked', () => { + it.skip('should make network requests when event APIs are invoked', () => { window.rudderanalytics?.page(); window.rudderanalytics?.track('test-event'); window.rudderanalytics?.identify(USER_ID, USER_TRAITS); diff --git a/packages/analytics-js/__tests__/components/configManager/commonUtil.test.ts b/packages/analytics-js/__tests__/components/configManager/commonUtil.test.ts index eaace0def..085876ee8 100644 --- a/packages/analytics-js/__tests__/components/configManager/commonUtil.test.ts +++ b/packages/analytics-js/__tests__/components/configManager/commonUtil.test.ts @@ -6,7 +6,6 @@ import { updateStorageStateFromLoadOptions, updateConsentsStateFromLoadOptions, updateConsentsState, - updateDataPlaneEventsStateFromLoadOptions, getSourceConfigURL, } from '../../../src/components/configManager/util/commonUtil'; import { @@ -604,51 +603,6 @@ describe('Config Manager Common Utilities', () => { }); }); - describe('updateDataPlaneEventsStateFromLoadOptions', () => { - beforeEach(() => { - resetState(); - }); - - it('should not set the events queue plugin name if events delivery is disabled', () => { - state.dataPlaneEvents.deliveryEnabled.value = false; - - updateDataPlaneEventsStateFromLoadOptions(mockLogger); - - expect(state.dataPlaneEvents.eventsQueuePluginName.value).toBeUndefined(); - }); - - it('should set the events queue plugin name to XhrQueue by default', () => { - updateDataPlaneEventsStateFromLoadOptions(mockLogger); - - expect(state.dataPlaneEvents.eventsQueuePluginName.value).toMatch('XhrQueue'); - }); - - it('should set the events queue plugin name to BeaconQueue if beacon transport is selected', () => { - state.loadOptions.value.useBeacon = true; - - // Force set the beacon availability - state.capabilities.isBeaconAvailable.value = true; - - updateDataPlaneEventsStateFromLoadOptions(mockLogger); - - expect(state.dataPlaneEvents.eventsQueuePluginName.value).toMatch('BeaconQueue'); - }); - - it('should set the events queue plugin name to XhrQueue if beacon transport is selected but not available', () => { - state.loadOptions.value.useBeacon = true; - - // Force set the beacon availability to false - state.capabilities.isBeaconAvailable.value = false; - - updateDataPlaneEventsStateFromLoadOptions(mockLogger); - - expect(state.dataPlaneEvents.eventsQueuePluginName.value).toMatch('XhrQueue'); - expect(mockLogger.warn).toHaveBeenCalledWith( - 'ConfigManager:: The Beacon API is not supported by your browser. The events will be sent using XHR instead.', - ); - }); - }); - describe('getSourceConfigURL', () => { it('should return default source config URL if invalid source config URL is provided', () => { const sourceConfigURL = getSourceConfigURL('invalid-url', 'writekey', true, true, mockLogger); diff --git a/packages/analytics-js/__tests__/components/core/Analytics.test.ts b/packages/analytics-js/__tests__/components/core/Analytics.test.ts index d55015118..b1719b43b 100644 --- a/packages/analytics-js/__tests__/components/core/Analytics.test.ts +++ b/packages/analytics-js/__tests__/components/core/Analytics.test.ts @@ -77,30 +77,30 @@ describe('Core - Analytics', () => { state.lifecycle.status.value = 'configured'; expect(onConfiguredSpy).toHaveBeenCalledTimes(1); - expect(state.lifecycle.status.value).toBe('pluginsLoading'); + expect(state.lifecycle.status.value).toBe('readyExecuted'); state.lifecycle.status.value = 'pluginsLoading'; expect(onConfiguredSpy).toHaveBeenCalledTimes(1); expect(state.lifecycle.status.value).toBe('pluginsLoading'); state.lifecycle.status.value = 'pluginsReady'; - expect(onPluginsReadySpy).toHaveBeenCalledTimes(1); + expect(onPluginsReadySpy).toHaveBeenCalledTimes(2); expect(state.lifecycle.status.value).toBe('readyExecuted'); state.lifecycle.status.value = 'initialized'; - expect(onInitializedSpy).toHaveBeenCalledTimes(2); + expect(onInitializedSpy).toHaveBeenCalledTimes(3); expect(state.lifecycle.status.value).toBe('readyExecuted'); state.lifecycle.status.value = 'loaded'; - expect(loadDestinationsSpy).toHaveBeenCalledTimes(3); + expect(loadDestinationsSpy).toHaveBeenCalledTimes(4); expect(state.lifecycle.status.value).toBe('readyExecuted'); state.lifecycle.status.value = 'destinationsReady'; - expect(onDestinationsReadySpy).toHaveBeenCalledTimes(4); + expect(onDestinationsReadySpy).toHaveBeenCalledTimes(5); expect(state.lifecycle.status.value).toBe('readyExecuted'); state.lifecycle.status.value = 'ready'; - expect(onReadySpy).toHaveBeenCalledTimes(5); + expect(onReadySpy).toHaveBeenCalledTimes(6); expect(state.lifecycle.status.value).toBe('readyExecuted'); }); diff --git a/packages/analytics-js/__tests__/components/pluginsManager/PluginsManager.test.ts b/packages/analytics-js/__tests__/components/pluginsManager/PluginsManager.test.ts index c67991c8e..fcbb2ef95 100644 --- a/packages/analytics-js/__tests__/components/pluginsManager/PluginsManager.test.ts +++ b/packages/analytics-js/__tests__/components/pluginsManager/PluginsManager.test.ts @@ -46,27 +46,6 @@ describe('PluginsManager', () => { ); }); - it('should not filter the data plane queue plugin if it is automatically configured', () => { - state.dataPlaneEvents.eventsQueuePluginName.value = 'XhrQueue'; - - expect(pluginsManager.getPluginsToLoadBasedOnConfig().sort(alphabeticalCompare)).toEqual( - ['XhrQueue', 'ExternalAnonymousId', 'GoogleLinker'].sort(alphabeticalCompare), - ); - }); - - it('should add the data plane queue plugin if it is not configured through the plugins input', () => { - state.plugins.pluginsToLoadFromConfig.value = []; - state.dataPlaneEvents.eventsQueuePluginName.value = 'XhrQueue'; - - expect(pluginsManager.getPluginsToLoadBasedOnConfig()).toEqual(['XhrQueue']); - - // Expect a warning for user not explicitly configuring it - expect(defaultLogger.warn).toHaveBeenCalledTimes(1); - expect(defaultLogger.warn).toHaveBeenCalledWith( - "PluginsManager:: Data plane events delivery is enabled, but 'XhrQueue' plugin was not configured to load. So, the plugin will be loaded automatically.", - ); - }); - it('should not filter the error reporting plugins if it is configured to load by default', () => { state.reporting.isErrorReportingEnabled.value = true; diff --git a/packages/analytics-js/__tests__/services/ErrorHandler/utils.test.ts b/packages/analytics-js/__tests__/services/ErrorHandler/utils.test.ts index 4beba5cdd..ea50dc49a 100644 --- a/packages/analytics-js/__tests__/services/ErrorHandler/utils.test.ts +++ b/packages/analytics-js/__tests__/services/ErrorHandler/utils.test.ts @@ -88,7 +88,7 @@ describe('Error Reporting utilities', () => { const testCaseData: any[] = [ ['https://invalid-domain.com/rsa.min.js', true], ['https://invalid-domain.com/rss.min.js', false], - ['https://invalid-domain.com/rsa-plugins-Beacon.min.js', true], + ['https://invalid-domain.com/rsa-plugins-StorageMigrator.min.js', true], ['https://invalid-domain.com/Amplitude.min.js', false], ['https://invalid-domain.com/js-integrations/Amplitude.min.js', true], ['https://invalid-domain.com/js-integrations/Qualaroo.min.js', true], @@ -412,7 +412,6 @@ describe('Error Reporting utilities', () => { }, capabilities: { isAdBlocked: false, - isBeaconAvailable: false, isCryptoAvailable: false, isIE11: false, isLegacyDOM: false, @@ -475,7 +474,6 @@ describe('Error Reporting utilities', () => { readyCallbacks: [], }, loadOptions: { - beaconQueueOptions: {}, bufferDataPlaneEventsUntilReady: false, configUrl: 'https://api.rudderstack.com', dataPlaneEventsBufferTimeout: 10000, @@ -503,7 +501,6 @@ describe('Error Reporting utilities', () => { migrate: true, }, uaChTrackLevel: 'none', - useBeacon: false, useGlobalIntegrationsConfigInEvents: false, useServerSideCookies: false, }, diff --git a/packages/analytics-js/public/index.html b/packages/analytics-js/public/index.html index 029e693b8..c495b27aa 100644 --- a/packages/analytics-js/public/index.html +++ b/packages/analytics-js/public/index.html @@ -112,10 +112,6 @@ // maxSize: 5 * 1024, // 5KB // }, // }, - // useBeacon: true, - // beaconQueueOptions: { - // maxItems: 5, - // }, // sessions: { // autoTrack: true, // timeout: 30 * 1000 @@ -163,7 +159,6 @@ // plugins: [ // 'StorageEncryption', // 'StorageMigrator', - // 'XhrQueue' // ] // autoTrack:{ // enabled: true, diff --git a/packages/analytics-js/rollup.config.mjs b/packages/analytics-js/rollup.config.mjs index 5058e2bd9..400dc5a86 100644 --- a/packages/analytics-js/rollup.config.mjs +++ b/packages/analytics-js/rollup.config.mjs @@ -77,10 +77,6 @@ const getExternalsConfig = () => { } if (isDynamicCustomBuild) { - if (!bundledPluginsList.includes('BeaconQueue')) { - externalGlobalsConfig['@rudderstack/analytics-js-plugins/beaconQueue'] = '{}'; - } - if (!bundledPluginsList.includes('CustomConsentManager')) { externalGlobalsConfig['@rudderstack/analytics-js-plugins/customConsentManager'] = '{}'; } @@ -128,10 +124,6 @@ const getExternalsConfig = () => { if (!bundledPluginsList.includes('StorageMigrator')) { externalGlobalsConfig['@rudderstack/analytics-js-plugins/storageMigrator'] = '{}'; } - - if (!bundledPluginsList.includes('XhrQueue') && bundledPluginsList.includes('BeaconQueue')) { - externalGlobalsConfig['@rudderstack/analytics-js-plugins/xhrQueue'] = '{}'; - } } return externalGlobalsConfig; diff --git a/packages/analytics-js/src/app/RudderAnalytics.ts b/packages/analytics-js/src/app/RudderAnalytics.ts index 8060df0bc..89efaf6f5 100644 --- a/packages/analytics-js/src/app/RudderAnalytics.ts +++ b/packages/analytics-js/src/app/RudderAnalytics.ts @@ -35,7 +35,6 @@ import { getExposedGlobal, setExposedGlobal } from '../components/utilities/glob import type { IAnalytics } from '../components/core/IAnalytics'; import { Analytics } from '../components/core/Analytics'; import { defaultLogger } from '../services/Logger/Logger'; -import { PAGE_UNLOAD_ON_BEACON_DISABLED_WARNING } from '../constants/logMessages'; import { state } from '../state'; // TODO: add analytics restart/reset mechanism @@ -189,7 +188,7 @@ class RudderAnalytics implements IRudderAnalytics { preloadedEventsArray: PreloadedEventCall[], loadOptions?: Partial, ) { - const { autoTrack, useBeacon } = loadOptions ?? {}; + const { autoTrack } = loadOptions ?? {}; const { enabled: autoTrackEnabled = false, options: autoTrackOptions = {}, @@ -213,7 +212,7 @@ class RudderAnalytics implements IRudderAnalytics { return; } this.trackPageLoadedEvent(events, options, preloadedEventsArray); - this.setupPageUnloadTracking(events, useBeacon, options); + this.setupPageUnloadTracking(events, options); } /** @@ -246,39 +245,29 @@ class RudderAnalytics implements IRudderAnalytics { /** * Setup page unload tracking if enabled * @param events - * @param useBeacon * @param options */ - setupPageUnloadTracking( - events: PageLifecycleEvents[], - useBeacon: boolean | undefined, - options: ApiOptions, - ) { + setupPageUnloadTracking(events: PageLifecycleEvents[], options: ApiOptions) { if (events.length === 0 || events.includes(PageLifecycleEvents.UNLOADED)) { - if (useBeacon === true) { - onPageLeave((isAccessible: boolean) => { - if (isAccessible === false && state.lifecycle.loaded.value) { - const pageUnloadedTimestamp = Date.now(); - const visitDuration = - pageUnloadedTimestamp - - (state.autoTrack.pageLifecycle.pageLoadedTimestamp.value as number); - - this.track( - PageLifecycleEvents.UNLOADED, - { - visitDuration, - }, - { - ...options, - originalTimestamp: getFormattedTimestamp(new Date(pageUnloadedTimestamp)), - }, - ); - } - }); - } else { - // log warning if beacon is disabled - this.logger.warn(PAGE_UNLOAD_ON_BEACON_DISABLED_WARNING(RSA)); - } + onPageLeave((isAccessible: boolean) => { + if (isAccessible === false && state.lifecycle.loaded.value) { + const pageUnloadedTimestamp = Date.now(); + const visitDuration = + pageUnloadedTimestamp - + (state.autoTrack.pageLifecycle.pageLoadedTimestamp.value as number); + + this.track( + PageLifecycleEvents.UNLOADED, + { + visitDuration, + }, + { + ...options, + originalTimestamp: getFormattedTimestamp(new Date(pageUnloadedTimestamp)), + }, + ); + } + }); } } diff --git a/packages/analytics-js/src/components/capabilitiesManager/CapabilitiesManager.ts b/packages/analytics-js/src/components/capabilitiesManager/CapabilitiesManager.ts index 014eec61d..87c1ca69e 100644 --- a/packages/analytics-js/src/components/capabilitiesManager/CapabilitiesManager.ts +++ b/packages/analytics-js/src/components/capabilitiesManager/CapabilitiesManager.ts @@ -25,7 +25,6 @@ import type { ICapabilitiesManager } from './types'; import { POLYFILL_LOAD_TIMEOUT, POLYFILL_SCRIPT_ID, POLYFILL_URL } from './polyfill'; import { getScreenDetails, - hasBeacon, hasCrypto, hasUAClientHints, isIE11, @@ -80,7 +79,6 @@ class CapabilitiesManager implements ICapabilitiesManager { ); // Browser feature detection details - state.capabilities.isBeaconAvailable.value = hasBeacon(); state.capabilities.isUaCHAvailable.value = hasUAClientHints(); state.capabilities.isCryptoAvailable.value = hasCrypto(); state.capabilities.isIE11.value = isIE11(); diff --git a/packages/analytics-js/src/components/capabilitiesManager/detection/browser.ts b/packages/analytics-js/src/components/capabilitiesManager/detection/browser.ts index 5a2589c82..55ed3e894 100644 --- a/packages/analytics-js/src/components/capabilitiesManager/detection/browser.ts +++ b/packages/analytics-js/src/components/capabilitiesManager/detection/browser.ts @@ -16,10 +16,6 @@ const hasCrypto = (): boolean => // eslint-disable-next-line compat/compat -- We are checking for the existence of navigator.userAgentData const hasUAClientHints = (): boolean => !isNullOrUndefined(globalThis.navigator.userAgentData); -const hasBeacon = (): boolean => - !isNullOrUndefined(globalThis.navigator.sendBeacon) && - isFunction(globalThis.navigator.sendBeacon); - const isIE11 = (): boolean => Boolean(globalThis.navigator.userAgent.match(/Trident.*rv:11\./)); -export { isBrowser, isNode, hasCrypto, hasUAClientHints, hasBeacon, isIE11 }; +export { isBrowser, isNode, hasCrypto, hasUAClientHints, isIE11 }; diff --git a/packages/analytics-js/src/components/capabilitiesManager/detection/dom.ts b/packages/analytics-js/src/components/capabilitiesManager/detection/dom.ts index 9373ddace..cf51db9ae 100644 --- a/packages/analytics-js/src/components/capabilitiesManager/detection/dom.ts +++ b/packages/analytics-js/src/components/capabilitiesManager/detection/dom.ts @@ -32,7 +32,6 @@ const legacyJSEngineRequiredPolyfills: Record boolean> = { requestAnimationFrame: () => !isFunction(globalThis.requestAnimationFrame) || !isFunction(globalThis.cancelAnimationFrame), CustomEvent: () => !isFunction(globalThis.CustomEvent), - 'navigator.sendBeacon': () => !isFunction(globalThis.navigator.sendBeacon), // Note, the polyfill service serves both ArrayBuffer and Uint8Array under the same feature name, "ArrayBuffer". ArrayBuffer: () => !isFunction(globalThis.Uint8Array), Set: () => !isFunction(globalThis.Set), diff --git a/packages/analytics-js/src/components/capabilitiesManager/detection/index.ts b/packages/analytics-js/src/components/capabilitiesManager/detection/index.ts index 947698321..88b6b4a9f 100644 --- a/packages/analytics-js/src/components/capabilitiesManager/detection/index.ts +++ b/packages/analytics-js/src/components/capabilitiesManager/detection/index.ts @@ -1,5 +1,5 @@ export { detectAdBlockers } from './adBlockers'; -export { isBrowser, isNode, hasCrypto, hasUAClientHints, hasBeacon, isIE11 } from './browser'; +export { isBrowser, isNode, hasCrypto, hasUAClientHints, isIE11 } from './browser'; export { getUserAgentClientHint } from './clientHint'; export { isDatasetAvailable, legacyJSEngineRequiredPolyfills, isLegacyJSEngine } from './dom'; export { getScreenDetails } from './screen'; diff --git a/packages/analytics-js/src/components/configManager/ConfigManager.ts b/packages/analytics-js/src/components/configManager/ConfigManager.ts index 4e84aa9d3..cf8d4f995 100644 --- a/packages/analytics-js/src/components/configManager/ConfigManager.ts +++ b/packages/analytics-js/src/components/configManager/ConfigManager.ts @@ -33,7 +33,6 @@ import { getSourceConfigURL, updateConsentsState, updateConsentsStateFromLoadOptions, - updateDataPlaneEventsStateFromLoadOptions, updateReportingState, updateStorageStateFromLoadOptions, } from './util/commonUtil'; @@ -109,7 +108,6 @@ class ConfigManager implements IConfigManager { updateStorageStateFromLoadOptions(this.logger); updateConsentsStateFromLoadOptions(this.logger); - updateDataPlaneEventsStateFromLoadOptions(this.logger); // set application lifecycle state in global state batch(() => { diff --git a/packages/analytics-js/src/components/configManager/constants.ts b/packages/analytics-js/src/components/configManager/constants.ts index 85984be8e..ee432fd77 100644 --- a/packages/analytics-js/src/components/configManager/constants.ts +++ b/packages/analytics-js/src/components/configManager/constants.ts @@ -15,11 +15,6 @@ export const StorageEncryptionVersionsToPluginNameMap: Record = { - [DEFAULT_DATA_PLANE_EVENTS_TRANSPORT]: 'XhrQueue', - beacon: 'BeaconQueue', -}; - const DEFAULT_DATA_SERVICE_ENDPOINT = 'rsaRequest'; const METRICS_SERVICE_ENDPOINT = 'rsaMetrics'; diff --git a/packages/analytics-js/src/components/configManager/util/commonUtil.ts b/packages/analytics-js/src/components/configManager/util/commonUtil.ts index a5ff9a2ca..21c8a6590 100644 --- a/packages/analytics-js/src/components/configManager/util/commonUtil.ts +++ b/packages/analytics-js/src/components/configManager/util/commonUtil.ts @@ -21,7 +21,6 @@ import type { ConsentResolutionStrategy, } from '@rudderstack/analytics-js-common/types/Consent'; import { clone } from 'ramda'; -import type { PluginName } from '@rudderstack/analytics-js-common/types/PluginsManager'; import { isValidURL, removeDuplicateSlashes } from '@rudderstack/analytics-js-common/utilities/url'; import { removeLeadingPeriod } from '@rudderstack/analytics-js-common/utilities/string'; import { MODULE_TYPE, APP_VERSION } from '../../../constants/app'; @@ -31,7 +30,6 @@ import { INVALID_CONFIG_URL_WARNING, STORAGE_DATA_MIGRATION_OVERRIDE_WARNING, STORAGE_TYPE_VALIDATION_WARNING, - UNSUPPORTED_BEACON_API_WARNING, UNSUPPORTED_PRE_CONSENT_EVENTS_DELIVERY_TYPE, UNSUPPORTED_PRE_CONSENT_STORAGE_STRATEGY, UNSUPPORTED_STORAGE_ENCRYPTION_VERSION_WARNING, @@ -314,27 +312,6 @@ const updateConsentsState = (resp: SourceConfigResponse): void => { }); }; -const updateDataPlaneEventsStateFromLoadOptions = (logger: ILogger) => { - if (state.dataPlaneEvents.deliveryEnabled.value) { - const defaultEventsQueuePluginName: PluginName = 'XhrQueue'; - let eventsQueuePluginName: PluginName = defaultEventsQueuePluginName; - - if (state.loadOptions.value.useBeacon) { - if (state.capabilities.isBeaconAvailable.value) { - eventsQueuePluginName = 'BeaconQueue'; - } else { - eventsQueuePluginName = defaultEventsQueuePluginName; - - logger.warn(UNSUPPORTED_BEACON_API_WARNING(CONFIG_MANAGER)); - } - } - - batch(() => { - state.dataPlaneEvents.eventsQueuePluginName.value = eventsQueuePluginName; - }); - } -}; - const getSourceConfigURL = ( configUrl: string | undefined, writeKey: string, @@ -387,6 +364,5 @@ export { updateStorageStateFromLoadOptions, updateConsentsStateFromLoadOptions, updateConsentsState, - updateDataPlaneEventsStateFromLoadOptions, getSourceConfigURL, }; diff --git a/packages/analytics-js/src/components/pluginsManager/PluginsManager.ts b/packages/analytics-js/src/components/pluginsManager/PluginsManager.ts index 0882d5786..72cd07c0a 100644 --- a/packages/analytics-js/src/components/pluginsManager/PluginsManager.ts +++ b/packages/analytics-js/src/components/pluginsManager/PluginsManager.ts @@ -23,7 +23,6 @@ import { state } from '../../state'; import { ConsentManagersToPluginNameMap, StorageEncryptionVersionsToPluginNameMap, - DataPlaneEventsTransportToPluginNameMap, } from '../configManager/constants'; import { deprecatedPluginsList, pluginNamesList } from './pluginNames'; import { @@ -109,13 +108,6 @@ class PluginsManager implements IPluginsManager { }); const pluginGroupsToProcess: PluginsGroup[] = [ - { - configurationStatus: () => isDefined(state.dataPlaneEvents.eventsQueuePluginName.value), - configurationStatusStr: 'Data plane events delivery is enabled', - activePluginName: state.dataPlaneEvents.eventsQueuePluginName.value, - supportedPlugins: Object.values(DataPlaneEventsTransportToPluginNameMap), - shouldAddMissingPlugins: true, - }, { configurationStatus: () => getNonCloudDestinations(state.nativeDestinations.configuredDestinations.value).length > 0, diff --git a/packages/analytics-js/src/components/pluginsManager/bundledBuildPluginImports.ts b/packages/analytics-js/src/components/pluginsManager/bundledBuildPluginImports.ts index 32d51bf0d..b0576a1c9 100644 --- a/packages/analytics-js/src/components/pluginsManager/bundledBuildPluginImports.ts +++ b/packages/analytics-js/src/components/pluginsManager/bundledBuildPluginImports.ts @@ -1,4 +1,3 @@ -import { BeaconQueue } from '@rudderstack/analytics-js-plugins/beaconQueue'; import { CustomConsentManager } from '@rudderstack/analytics-js-plugins/customConsentManager'; import { DeviceModeDestinations } from '@rudderstack/analytics-js-plugins/deviceModeDestinations'; import { DeviceModeTransformation } from '@rudderstack/analytics-js-plugins/deviceModeTransformation'; @@ -11,14 +10,12 @@ import { OneTrustConsentManager } from '@rudderstack/analytics-js-plugins/oneTru import { StorageEncryption } from '@rudderstack/analytics-js-plugins/storageEncryption'; import { StorageEncryptionLegacy } from '@rudderstack/analytics-js-plugins/storageEncryptionLegacy'; import { StorageMigrator } from '@rudderstack/analytics-js-plugins/storageMigrator'; -import { XhrQueue } from '@rudderstack/analytics-js-plugins/xhrQueue'; import type { PluginMap } from './types'; /** * Map plugin names to direct code imports from plugins package */ const getBundledBuildPluginImports = (): PluginMap => ({ - BeaconQueue, CustomConsentManager, DeviceModeDestinations, DeviceModeTransformation, @@ -31,7 +28,6 @@ const getBundledBuildPluginImports = (): PluginMap => ({ StorageEncryption, StorageEncryptionLegacy, StorageMigrator, - XhrQueue, }); export { getBundledBuildPluginImports }; diff --git a/packages/analytics-js/src/components/pluginsManager/defaultPluginsList.ts b/packages/analytics-js/src/components/pluginsManager/defaultPluginsList.ts index 016ae0c58..b80e9f260 100644 --- a/packages/analytics-js/src/components/pluginsManager/defaultPluginsList.ts +++ b/packages/analytics-js/src/components/pluginsManager/defaultPluginsList.ts @@ -4,7 +4,6 @@ import type { PluginName } from '@rudderstack/analytics-js-common/types/PluginsM * Plugins to be loaded in the plugins loadOption is not defined */ const defaultOptionalPluginsList: PluginName[] = [ - 'BeaconQueue', 'CustomConsentManager', 'DeviceModeDestinations', 'DeviceModeTransformation', @@ -17,7 +16,6 @@ const defaultOptionalPluginsList: PluginName[] = [ 'StorageEncryption', 'StorageEncryptionLegacy', 'StorageMigrator', - 'XhrQueue', ]; export { defaultOptionalPluginsList }; diff --git a/packages/analytics-js/src/components/pluginsManager/federatedModulesBuildPluginImports.ts b/packages/analytics-js/src/components/pluginsManager/federatedModulesBuildPluginImports.ts index 879a7b179..7a660c6b0 100644 --- a/packages/analytics-js/src/components/pluginsManager/federatedModulesBuildPluginImports.ts +++ b/packages/analytics-js/src/components/pluginsManager/federatedModulesBuildPluginImports.ts @@ -10,8 +10,6 @@ const getFederatedModuleImport = ( pluginName: PluginName, ): (() => Promise) | undefined => { switch (pluginName) { - case 'BeaconQueue': - return () => import('rudderAnalyticsRemotePlugins/BeaconQueue'); case 'CustomConsentManager': return () => import('rudderAnalyticsRemotePlugins/CustomConsentManager'); case 'DeviceModeDestinations': @@ -36,8 +34,6 @@ const getFederatedModuleImport = ( return () => import('rudderAnalyticsRemotePlugins/StorageEncryptionLegacy'); case 'StorageMigrator': return () => import('rudderAnalyticsRemotePlugins/StorageMigrator'); - case 'XhrQueue': - return () => import('rudderAnalyticsRemotePlugins/XhrQueue'); default: return undefined; } diff --git a/packages/analytics-js/src/components/pluginsManager/pluginNames.ts b/packages/analytics-js/src/components/pluginsManager/pluginNames.ts index 6120ecd9b..76ec7dcc4 100644 --- a/packages/analytics-js/src/components/pluginsManager/pluginNames.ts +++ b/packages/analytics-js/src/components/pluginsManager/pluginNames.ts @@ -9,7 +9,6 @@ const localPluginNames: PluginName[] = []; * List of plugin names that are loaded as dynamic imports in modern builds */ const pluginNamesList: PluginName[] = [ - 'BeaconQueue', 'CustomConsentManager', 'DeviceModeDestinations', 'DeviceModeTransformation', @@ -22,9 +21,8 @@ const pluginNamesList: PluginName[] = [ 'StorageEncryption', 'StorageEncryptionLegacy', 'StorageMigrator', - 'XhrQueue', ]; -const deprecatedPluginsList = ['Bugsnag', 'ErrorReporting']; +const deprecatedPluginsList = ['Bugsnag', 'ErrorReporting', 'XhrQueue', 'BeaconQueue']; export { localPluginNames, pluginNamesList, deprecatedPluginsList }; diff --git a/packages/analytics-js/src/components/utilities/loadOptions.ts b/packages/analytics-js/src/components/utilities/loadOptions.ts index f70d11948..44790addd 100644 --- a/packages/analytics-js/src/components/utilities/loadOptions.ts +++ b/packages/analytics-js/src/components/utilities/loadOptions.ts @@ -1,3 +1,4 @@ +/* eslint-disable sonarjs/deprecation */ import { clone } from 'ramda'; import { isNonEmptyObject, diff --git a/packages/analytics-js/src/constants/logMessages.ts b/packages/analytics-js/src/constants/logMessages.ts index 1de2dbe6d..35c10e116 100644 --- a/packages/analytics-js/src/constants/logMessages.ts +++ b/packages/analytics-js/src/constants/logMessages.ts @@ -160,9 +160,6 @@ const RESERVED_KEYWORD_WARNING = ( const INVALID_CONTEXT_OBJECT_WARNING = (logContext: string): string => `${logContext}${LOG_CONTEXT_SEPARATOR}Please make sure that the "context" property in the event API's "options" argument is a valid object literal with key-value pairs.`; -const UNSUPPORTED_BEACON_API_WARNING = (context: string): string => - `${context}${LOG_CONTEXT_SEPARATOR}The Beacon API is not supported by your browser. The events will be sent using XHR instead.`; - const TIMEOUT_NOT_NUMBER_WARNING = ( context: string, timeout: number | undefined, @@ -249,9 +246,6 @@ const INVALID_POLYFILL_URL_WARNING = ( const BAD_COOKIES_WARNING = (key: string) => `The cookie data for ${key} seems to be encrypted using SDK versions < v3. The data is dropped. This can potentially stem from using SDK versions < v3 on other sites or web pages that can share cookies with this webpage. We recommend using the same SDK (v3) version everywhere or avoid disabling the storage data migration.`; -const PAGE_UNLOAD_ON_BEACON_DISABLED_WARNING = (context: string) => - `${context}${LOG_CONTEXT_SEPARATOR}Page Unloaded event can only be tracked when the Beacon transport is active. Please enable "useBeacon" load API option.`; - const UNKNOWN_PLUGINS_WARNING = (context: string, unknownPlugins: string[]) => `${context}${LOG_CONTEXT_SEPARATOR}Ignoring unknown plugins: ${unknownPlugins.join(', ')}.`; @@ -262,7 +256,6 @@ export { STORAGE_DATA_MIGRATION_OVERRIDE_WARNING, RESERVED_KEYWORD_WARNING, INVALID_CONTEXT_OBJECT_WARNING, - UNSUPPORTED_BEACON_API_WARNING, TIMEOUT_NOT_NUMBER_WARNING, TIMEOUT_ZERO_WARNING, TIMEOUT_NOT_RECOMMENDED_WARNING, @@ -308,7 +301,6 @@ export { COMPONENT_BASE_URL_ERROR, SERVER_SIDE_COOKIE_FEATURE_OVERRIDE_WARNING, BAD_COOKIES_WARNING, - PAGE_UNLOAD_ON_BEACON_DISABLED_WARNING, BREADCRUMB_ERROR, NON_ERROR_WARNING, CALLBACK_INVOKE_ERROR, diff --git a/packages/analytics-js/src/state/slices/capabilities.ts b/packages/analytics-js/src/state/slices/capabilities.ts index 45fdc1d8b..bf2396001 100644 --- a/packages/analytics-js/src/state/slices/capabilities.ts +++ b/packages/analytics-js/src/state/slices/capabilities.ts @@ -8,7 +8,6 @@ const capabilitiesState: CapabilitiesState = { isCookieStorageAvailable: signal(false), isSessionStorageAvailable: signal(false), }, - isBeaconAvailable: signal(false), isLegacyDOM: signal(false), isUaCHAvailable: signal(false), isCryptoAvailable: signal(false), diff --git a/packages/analytics-js/src/state/slices/dataPlaneEvents.ts b/packages/analytics-js/src/state/slices/dataPlaneEvents.ts index 82800cfed..6811cd71e 100644 --- a/packages/analytics-js/src/state/slices/dataPlaneEvents.ts +++ b/packages/analytics-js/src/state/slices/dataPlaneEvents.ts @@ -3,7 +3,6 @@ import type { DataPlaneEventsState } from '@rudderstack/analytics-js-common/type import type { PluginName } from '@rudderstack/analytics-js-common/types/PluginsManager'; const dataPlaneEventsState: DataPlaneEventsState = { - eventsQueuePluginName: signal(undefined), deliveryEnabled: signal(true), // Delivery should always happen }; diff --git a/packages/analytics-js/src/state/slices/loadOptions.ts b/packages/analytics-js/src/state/slices/loadOptions.ts index b776a1466..e6b071f8f 100644 --- a/packages/analytics-js/src/state/slices/loadOptions.ts +++ b/packages/analytics-js/src/state/slices/loadOptions.ts @@ -20,8 +20,6 @@ const defaultLoadOptions: LoadOptions = { sameSiteCookie: 'Lax', polyfillIfRequired: true, integrations: DEFAULT_INTEGRATIONS_CONFIG, - useBeacon: false, - beaconQueueOptions: {}, destinationsQueueOptions: {}, queueOptions: {}, lockIntegrationsVersion: false, diff --git a/packages/analytics-js/src/types/remote-plugins.d.ts b/packages/analytics-js/src/types/remote-plugins.d.ts index 4891446ad..dc4f5b854 100644 --- a/packages/analytics-js/src/types/remote-plugins.d.ts +++ b/packages/analytics-js/src/types/remote-plugins.d.ts @@ -1,4 +1,3 @@ -declare module 'rudderAnalyticsRemotePlugins/BeaconQueue'; declare module 'rudderAnalyticsRemotePlugins/CustomConsentManager'; declare module 'rudderAnalyticsRemotePlugins/DeviceModeDestinations'; declare module 'rudderAnalyticsRemotePlugins/DeviceModeTransformation'; @@ -11,4 +10,3 @@ declare module 'rudderAnalyticsRemotePlugins/OneTrustConsentManager'; declare module 'rudderAnalyticsRemotePlugins/StorageEncryption'; declare module 'rudderAnalyticsRemotePlugins/StorageEncryptionLegacy'; declare module 'rudderAnalyticsRemotePlugins/StorageMigrator'; -declare module 'rudderAnalyticsRemotePlugins/XhrQueue'; diff --git a/packages/sanity-suite/public/v1.1/index-cdn.html b/packages/sanity-suite/public/v1.1/index-cdn.html index 7e66d1d29..3abd3b219 100644 --- a/packages/sanity-suite/public/v1.1/index-cdn.html +++ b/packages/sanity-suite/public/v1.1/index-cdn.html @@ -107,7 +107,7 @@ src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-Fy6S3B9q64WdZWQUiU+q4/2Lc9npb8tCaSX9FK7E8HnRr0Jz8D6OP9dO5Vg3Q9ct" crossorigin="anonymous"> - +