From 63c7f232cb3f3515351fdf24bdcd32117fe8dc70 Mon Sep 17 00:00:00 2001 From: Sai Kumar Battinoju <88789928+saikumarrs@users.noreply.github.com> Date: Tue, 21 Jan 2025 13:20:13 +0530 Subject: [PATCH] feat: make error handler and logger mandatory in all components (#2007) * feat: improve user callback execution * feat: make error handler and logger mandatory in all components --- .../__mocks__/HttpClient.ts | 11 +- .../analytics-js-common/__mocks__/Store.ts | 24 ++- .../__mocks__/StoreManager.ts | 3 +- .../__tests__/utilities/errors.test.ts | 79 +++++++++- .../ExternalSrcLoader/ExternalSrcLoader.ts | 16 +- .../src/services/ExternalSrcLoader/types.ts | 4 +- .../src/types/HttpClient.ts | 5 +- .../analytics-js-common/src/types/Store.ts | 14 +- .../src/utilities/errors.ts | 51 ++++-- .../ketchConsentManager/utils.test.ts | 1 - .../src/iubendaConsentManager/utils.ts | 2 + .../src/ketchConsentManager/utils.ts | 2 + .../src/utilities/retryQueue/RetryQueue.ts | 6 + .../CapabilitiesManager.test.ts | 52 +++---- .../detection/adBlockers.test.ts | 47 +++--- .../configManager/ConfigManager.test.ts | 140 ++++++++++++++++- .../components/configManager/cdnPaths.test.ts | 73 +++++++-- .../configManager/commonUtil.test.ts | 62 ++++---- .../eventManager/EventManager.test.ts | 86 +++++----- .../eventManager/RudderEventFactory.test.ts | 17 +- .../components/eventManager/utilities.test.ts | 12 -- .../eventRepository/EventRepository.test.ts | 57 ++++--- .../pluginsManager/PluginsManager.test.ts | 131 +++++++++------- .../UserSessionManager.test.ts | 13 +- .../userSessionManager/utils.test.ts | 6 +- .../components/utilities/consent.test.ts | 11 +- .../ErrorHandler/ErrorHandler.test.ts | 2 +- .../services/HttpClient/HttpClient.test.ts | 61 ++------ .../PluginEngine/PluginEngine.test.ts | 99 +++++++++++- .../services/StoreManager/Store.test.ts | 33 ++++ .../analytics-js/src/app/RudderAnalytics.ts | 2 +- .../CapabilitiesManager.ts | 27 ++-- .../detection/adBlockers.ts | 10 +- .../components/capabilitiesManager/types.ts | 4 +- .../components/configManager/ConfigManager.ts | 86 ++++++---- .../src/components/configManager/types.ts | 14 +- .../components/configManager/util/cdnPaths.ts | 25 ++- .../configManager/util/commonUtil.ts | 28 ++-- .../src/components/core/Analytics.ts | 13 +- .../components/eventManager/EventManager.ts | 29 +--- .../eventManager/RudderEventFactory.ts | 10 +- .../src/components/eventManager/utilities.ts | 26 ++-- .../eventRepository/EventRepository.ts | 147 ++++++------------ .../pluginsManager/PluginsManager.ts | 27 +--- .../userSessionManager/UserSessionManager.ts | 36 ++--- .../components/userSessionManager/utils.ts | 8 +- .../src/components/utilities/consent.ts | 6 +- .../analytics-js/src/constants/logMessages.ts | 53 ++++--- .../src/services/HttpClient/HttpClient.ts | 17 +- .../HttpClient/xhr/xhrResponseHandler.ts | 11 +- .../src/services/PluginEngine/PluginEngine.ts | 22 +-- .../src/services/StoreManager/Store.ts | 26 ++-- .../src/services/StoreManager/StoreManager.ts | 26 +--- .../src/state/slices/lifecycle.ts | 3 +- packages/analytics-v1.1/.size-limit.js | 4 +- 55 files changed, 1026 insertions(+), 754 deletions(-) diff --git a/packages/analytics-js-common/__mocks__/HttpClient.ts b/packages/analytics-js-common/__mocks__/HttpClient.ts index 85d404b3c1..e5e3c9eeef 100644 --- a/packages/analytics-js-common/__mocks__/HttpClient.ts +++ b/packages/analytics-js-common/__mocks__/HttpClient.ts @@ -1,16 +1,17 @@ -import type { IErrorHandler } from '../src/types/ErrorHandler'; -import type { IHttpClient } from '../src/types/HttpClient'; -import type { ILogger } from '../src/types/Logger'; +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 { defaultLogger } from './Logger'; class HttpClient implements IHttpClient { errorHandler?: IErrorHandler; - logger?: ILogger; + logger: ILogger = defaultLogger; hasErrorHandler = false; - init = jest.fn(); getData = jest.fn(); getAsyncData = jest.fn(); setAuthHeader = jest.fn(); resetAuthHeader = jest.fn(); + init = jest.fn(); } const defaultHttpClient = new HttpClient(); diff --git a/packages/analytics-js-common/__mocks__/Store.ts b/packages/analytics-js-common/__mocks__/Store.ts index d5ee148bf9..1887cad96c 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 da6498f147..822f0954db 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/__tests__/utilities/errors.test.ts b/packages/analytics-js-common/__tests__/utilities/errors.test.ts index d1dab56190..3b54d1e825 100644 --- a/packages/analytics-js-common/__tests__/utilities/errors.test.ts +++ b/packages/analytics-js-common/__tests__/utilities/errors.test.ts @@ -1,18 +1,83 @@ -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); - globalThis.dispatchEvent = dispatchEvent; + expect(dispatchEventMock).toHaveBeenCalledWith(new ErrorEvent('error', { error })); + expect((error.stack as string).endsWith('[SDK DISPATCHED ERROR]')).toBeTruthy(); + }); + + 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(); }); }); }); diff --git a/packages/analytics-js-common/src/services/ExternalSrcLoader/ExternalSrcLoader.ts b/packages/analytics-js-common/src/services/ExternalSrcLoader/ExternalSrcLoader.ts index a1454190cc..60823685a4 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 d7763d414b..f1182fbf7c 100644 --- a/packages/analytics-js-common/src/services/ExternalSrcLoader/types.ts +++ b/packages/analytics-js-common/src/services/ExternalSrcLoader/types.ts @@ -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/HttpClient.ts b/packages/analytics-js-common/src/types/HttpClient.ts index 9efbe103ff..b61d483530 100644 --- a/packages/analytics-js-common/src/types/HttpClient.ts +++ b/packages/analytics-js-common/src/types/HttpClient.ts @@ -56,14 +56,13 @@ export type HTTPClientMethod = export interface IHttpClient { errorHandler?: IErrorHandler; - logger?: ILogger; + logger: ILogger; basicAuthHeader?: string; - hasErrorHandler: boolean; - init: (errorHandler: IErrorHandler) => void; getData( config: IRequestConfig, ): Promise<{ data: T | string | undefined; details?: ResponseDetails }>; 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/Store.ts b/packages/analytics-js-common/src/types/Store.ts index 1ab176bd29..ff3c79312e 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-common/src/utilities/errors.ts b/packages/analytics-js-common/src/utilities/errors.ts index 0929aa1cca..a9e03ba229 100644 --- a/packages/analytics-js-common/src/utilities/errors.ts +++ b/packages/analytics-js-common/src/utilities/errors.ts @@ -1,7 +1,19 @@ import { isTypeOfError } from './checks'; import { stringifyWithoutCircular } from './json'; -const MANUAL_ERROR_IDENTIFIER = '[MANUAL ERROR]'; +const MANUAL_ERROR_IDENTIFIER = '[SDK DISPATCHED ERROR]'; + +const getStacktrace = (err: any): string | undefined => { + const { stack, stacktrace } = err; + const operaSourceloc = err['opera#sourceloc']; + + const stackString = stack ?? stacktrace ?? operaSourceloc; + + if (!!stackString && typeof stackString === 'string') { + return stackString; + } + return undefined; +}; /** * Get mutated error with issue prepended to error message @@ -21,21 +33,32 @@ const getMutatedError = (err: any, issue: string): Error => { const dispatchErrorEvent = (error: any) => { if (isTypeOfError(error)) { - error.stack = `${error.stack ?? ''}\n${MANUAL_ERROR_IDENTIFIER}`; - } - (globalThis as typeof window).dispatchEvent(new ErrorEvent('error', { error })); -}; - -const getStacktrace = (err: any): string | undefined => { - const { stack, stacktrace } = err; - const operaSourceloc = err['opera#sourceloc']; + const errStack = getStacktrace(error); + if (errStack) { + const { stack, stacktrace } = error; + const operaSourceloc = error['opera#sourceloc']; - const stackString = stack ?? stacktrace ?? operaSourceloc; - - if (!!stackString && typeof stackString === 'string') { - return stackString; + 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; + } + } } - return undefined; + + (globalThis as typeof window).dispatchEvent( + new ErrorEvent('error', { error, bubbles: true, cancelable: true, composed: true }), + ); }; export { getMutatedError, dispatchErrorEvent, MANUAL_ERROR_IDENTIFIER, getStacktrace }; diff --git a/packages/analytics-js-plugins/__tests__/ketchConsentManager/utils.test.ts b/packages/analytics-js-plugins/__tests__/ketchConsentManager/utils.test.ts index c034547a80..4b5b30d91f 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 '@rudderstack/analytics-js-common/__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 a9ac8eba7d..7b9bc78ea8 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 9677beef4c..bc25a4fc5c 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 cc4f307273..debc5b911b 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/capabilitiesManager/CapabilitiesManager.test.ts b/packages/analytics-js/__tests__/components/capabilitiesManager/CapabilitiesManager.test.ts index acf6be360d..c71c84e903 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(() => { @@ -93,34 +98,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(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'; @@ -139,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/capabilitiesManager/detection/adBlockers.test.ts b/packages/analytics-js/__tests__/components/capabilitiesManager/detection/adBlockers.test.ts index 45191f1c96..f314f5e1a7 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: any; -let xhrObj: any; - -jest.mock('../../../../src/services/HttpClient/HttpClient', () => { - const originalModule = jest.requireActual('../../../../src/services/HttpClient/HttpClient'); - - return { - __esModule: true, - ...originalModule, - HttpClient: jest.fn().mockImplementation(() => ({ - setAuthHeader: jest.fn(), - init: 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/__tests__/components/configManager/ConfigManager.test.ts b/packages/analytics-js/__tests__/components/configManager/ConfigManager.test.ts index 3701f45c33..4a1f9e8681 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'; @@ -53,8 +54,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'; @@ -68,7 +67,6 @@ describe('ConfigManager', () => { }); beforeEach(() => { - defaultHttpClient.init(defaultErrorHandler); configManagerInstance = new ConfigManager( defaultHttpClient, defaultErrorHandler, @@ -147,6 +145,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, @@ -168,10 +179,51 @@ describe('ConfigManager', () => { ); }); - it('should call the onError method of errorHandler for undefined sourceConfig response', () => { + it('should handle error for undefined source config response', () => { configManagerInstance.processConfig(undefined); - 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 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"'); + + 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', () => { + // @ts-expect-error Testing invalid input + 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', () => { @@ -262,4 +314,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/configManager/cdnPaths.test.ts b/packages/analytics-js/__tests__/components/configManager/cdnPaths.test.ts index 1f2e912889..55040ca682 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/configManager/commonUtil.test.ts b/packages/analytics-js/__tests__/components/configManager/commonUtil.test.ts index ff30f79602..eaace0def2 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: { @@ -655,21 +662,14 @@ 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); + 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', @@ -682,6 +682,7 @@ describe('Config Manager Common Utilities', () => { 'writekey', false, false, + mockLogger, ); expect(sourceConfigURL).toBe( @@ -695,6 +696,7 @@ describe('Config Manager Common Utilities', () => { 'writekey', false, false, + mockLogger, ); expect(sourceConfigURL).toBe( @@ -708,6 +710,7 @@ describe('Config Manager Common Utilities', () => { 'writekey', false, false, + mockLogger, ); expect(sourceConfigURL).toBe( @@ -721,6 +724,7 @@ describe('Config Manager Common Utilities', () => { 'writekey', false, false, + mockLogger, ); expect(sourceConfigURL).toBe( diff --git a/packages/analytics-js/__tests__/components/eventManager/EventManager.test.ts b/packages/analytics-js/__tests__/components/eventManager/EventManager.test.ts index e50310d984..d2ca254822 100644 --- a/packages/analytics-js/__tests__/components/eventManager/EventManager.test.ts +++ b/packages/analytics-js/__tests__/components/eventManager/EventManager.test.ts @@ -1,75 +1,63 @@ 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 { onError = jest.fn(); 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(); }); }); - 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).toBeCalledWith( - new Error('Failed to generate the event object.'), - '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/eventManager/RudderEventFactory.test.ts b/packages/analytics-js/__tests__/components/eventManager/RudderEventFactory.test.ts index 9b9b768875..f4d8bbcb50 100644 --- a/packages/analytics-js/__tests__/components/eventManager/RudderEventFactory.test.ts +++ b/packages/analytics-js/__tests__/components/eventManager/RudderEventFactory.test.ts @@ -7,8 +7,9 @@ import type { OSInfo, ScreenInfo, } from '@rudderstack/analytics-js-common/types/EventContext'; -import { state } from '@rudderstack/analytics-js/state'; -import { RudderEventFactory } from '@rudderstack/analytics-js/components/eventManager/RudderEventFactory'; +import { defaultLogger } from '@rudderstack/analytics-js-common/__mocks__/Logger'; +import { state } from '../../../src/state'; +import { RudderEventFactory } from '../../../src/components/eventManager/RudderEventFactory'; jest.mock('@rudderstack/analytics-js-common/utilities/timestamp', () => ({ getCurrentTimeFormatted: jest.fn().mockReturnValue('2020-01-01T00:00:00.000Z'), @@ -19,7 +20,7 @@ jest.mock('@rudderstack/analytics-js-common/utilities/uuId', () => ({ })); describe('RudderEventFactory', () => { - const rudderEventFactory = new RudderEventFactory(); + const rudderEventFactory = new RudderEventFactory(defaultLogger); beforeEach(() => { batch(() => { @@ -503,14 +504,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__/components/eventManager/utilities.test.ts b/packages/analytics-js/__tests__/components/eventManager/utilities.test.ts index 9f2b0adc98..799bd75554 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); diff --git a/packages/analytics-js/__tests__/components/eventRepository/EventRepository.test.ts b/packages/analytics-js/__tests__/components/eventRepository/EventRepository.test.ts index 448aedf2cb..4637de788b 100644 --- a/packages/analytics-js/__tests__/components/eventRepository/EventRepository.test.ts +++ b/packages/analytics-js/__tests__/components/eventRepository/EventRepository.test.ts @@ -7,6 +7,7 @@ import type { import type { RudderEvent } from '@rudderstack/analytics-js-common/types/Event'; 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'; @@ -20,7 +21,11 @@ describe('EventRepository', () => { defaultLogger, ); - const defaultStoreManager = new StoreManager(defaultPluginsManager); + const defaultStoreManager = new StoreManager( + defaultPluginsManager, + defaultErrorHandler, + defaultLogger, + ); const mockDestinationsEventsQueue = { scheduleTimeoutActive: false, @@ -92,13 +97,14 @@ describe('EventRepository', () => { const eventRepository = new EventRepository( defaultPluginsManager, defaultStoreManager, + defaultHttpClient, defaultErrorHandler, defaultLogger, ); const spy = jest.spyOn(defaultPluginsManager, 'invokeSingle'); eventRepository.init(); - expect(spy).nthCalledWith( + expect(spy).toHaveBeenNthCalledWith( 1, 'dataplaneEventsQueue.init', state, @@ -107,7 +113,7 @@ describe('EventRepository', () => { defaultErrorHandler, defaultLogger, ); - expect(spy).nthCalledWith( + expect(spy).toHaveBeenNthCalledWith( 2, 'transformEvent.init', state, @@ -117,7 +123,7 @@ describe('EventRepository', () => { defaultErrorHandler, defaultLogger, ); - expect(spy).nthCalledWith( + expect(spy).toHaveBeenNthCalledWith( 3, 'destinationsEventsQueue.init', state, @@ -134,6 +140,7 @@ describe('EventRepository', () => { const eventRepository = new EventRepository( mockPluginsManager, defaultStoreManager, + defaultHttpClient, defaultErrorHandler, defaultLogger, ); @@ -142,13 +149,14 @@ 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', () => { const eventRepository = new EventRepository( mockPluginsManager, defaultStoreManager, + defaultHttpClient, defaultErrorHandler, defaultLogger, ); @@ -174,13 +182,14 @@ 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', () => { const eventRepository = new EventRepository( mockPluginsManager, defaultStoreManager, + defaultHttpClient, defaultErrorHandler, defaultLogger, ); @@ -191,13 +200,14 @@ 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 => { const eventRepository = new EventRepository( mockPluginsManager, defaultStoreManager, + defaultHttpClient, defaultErrorHandler, defaultLogger, ); @@ -209,11 +219,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); }); @@ -222,6 +232,7 @@ describe('EventRepository', () => { const eventRepository = new EventRepository( mockPluginsManager, defaultStoreManager, + defaultHttpClient, defaultErrorHandler, defaultLogger, ); @@ -233,10 +244,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); }); @@ -245,6 +256,7 @@ describe('EventRepository', () => { const eventRepository = new EventRepository( mockPluginsManager, defaultStoreManager, + defaultHttpClient, defaultErrorHandler, defaultLogger, ); @@ -254,7 +266,7 @@ describe('EventRepository', () => { const invokeSingleSpy = jest.spyOn(mockPluginsManager, 'invokeSingle'); eventRepository.enqueue(testEvent); - expect(invokeSingleSpy).nthCalledWith( + expect(invokeSingleSpy).toHaveBeenNthCalledWith( 1, 'dataplaneEventsQueue.enqueue', state, @@ -266,7 +278,7 @@ describe('EventRepository', () => { defaultErrorHandler, defaultLogger, ); - expect(invokeSingleSpy).nthCalledWith( + expect(invokeSingleSpy).toHaveBeenNthCalledWith( 2, 'destinationsEventsQueue.enqueue', state, @@ -283,6 +295,7 @@ describe('EventRepository', () => { const eventRepository = new EventRepository( mockPluginsManager, defaultStoreManager, + defaultHttpClient, defaultErrorHandler, defaultLogger, ); @@ -292,8 +305,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 }, }); @@ -303,6 +316,7 @@ describe('EventRepository', () => { const eventRepository = new EventRepository( mockPluginsManager, defaultStoreManager, + defaultHttpClient, defaultErrorHandler, defaultLogger, ); @@ -321,11 +335,13 @@ describe('EventRepository', () => { const eventRepository = new EventRepository( mockPluginsManager, defaultStoreManager, + defaultHttpClient, defaultErrorHandler, defaultLogger, ); eventRepository.init(); + const mockEventCallback = jest.fn(() => { throw new Error('test error'); }); @@ -342,6 +358,7 @@ describe('EventRepository', () => { const eventRepository = new EventRepository( mockPluginsManager, defaultStoreManager, + defaultHttpClient, defaultErrorHandler, defaultLogger, ); @@ -358,7 +375,7 @@ describe('EventRepository', () => { eventRepository.init(); - expect(mockDataplaneEventsQueue.start).not.toBeCalled(); + expect(mockDataplaneEventsQueue.start).not.toHaveBeenCalled(); }); describe('resume', () => { @@ -366,19 +383,21 @@ describe('EventRepository', () => { const eventRepository = new EventRepository( mockPluginsManager, defaultStoreManager, + defaultHttpClient, defaultErrorHandler, defaultLogger, ); eventRepository.init(); eventRepository.resume(); - expect(mockDataplaneEventsQueue.start).toBeCalled(); + expect(mockDataplaneEventsQueue.start).toHaveBeenCalled(); }); it('should clear the events queue if discardPreConsentEvents is set to true', () => { const eventRepository = new EventRepository( mockPluginsManager, defaultStoreManager, + defaultHttpClient, defaultErrorHandler, defaultLogger, ); @@ -389,8 +408,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__/components/pluginsManager/PluginsManager.test.ts b/packages/analytics-js/__tests__/components/pluginsManager/PluginsManager.test.ts index 64a60200a3..c67991c8e5 100644 --- a/packages/analytics-js/__tests__/components/pluginsManager/PluginsManager.test.ts +++ b/packages/analytics-js/__tests__/components/pluginsManager/PluginsManager.test.ts @@ -1,27 +1,30 @@ -import type { - Destination, - DestinationConfig, -} from '@rudderstack/analytics-js-common/types/Destination'; +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(); }); 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(); @@ -31,24 +34,23 @@ describe('PluginsManager', () => { }); it('should return empty array if plugins list is set to undefined in the state', () => { - // @ts-expect-error needed for testing state.plugins.pluginsToLoadFromConfig.value = undefined; - expect(pluginsManager.getPluginsToLoadBasedOnConfig()).toEqual(expect.arrayContaining([])); + expect(pluginsManager.getPluginsToLoadBasedOnConfig()).toEqual([]); }); 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()).toEqual( - expect.arrayContaining(['ExternalAnonymousId', 'GoogleLinker']), + 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()).toEqual( - expect.arrayContaining(['XhrQueue', 'ExternalAnonymousId', 'GoogleLinker']), + expect(pluginsManager.getPluginsToLoadBasedOnConfig().sort(alphabeticalCompare)).toEqual( + ['XhrQueue', 'ExternalAnonymousId', 'GoogleLinker'].sort(alphabeticalCompare), ); }); @@ -56,9 +58,7 @@ describe('PluginsManager', () => { state.plugins.pluginsToLoadFromConfig.value = []; state.dataPlaneEvents.eventsQueuePluginName.value = 'XhrQueue'; - expect(pluginsManager.getPluginsToLoadBasedOnConfig()).toEqual( - expect.arrayContaining(['XhrQueue']), - ); + expect(pluginsManager.getPluginsToLoadBasedOnConfig()).toEqual(['XhrQueue']); // Expect a warning for user not explicitly configuring it expect(defaultLogger.warn).toHaveBeenCalledTimes(1); @@ -67,23 +67,31 @@ 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(alphabeticalCompare)).toEqual( + ['ExternalAnonymousId', 'GoogleLinker'].sort(alphabeticalCompare), + ); + }); + it('should not filter the device mode destination plugins if they are configured', () => { // Non-empty array state.nativeDestinations.configuredDestinations.value = [ { config: { connectionMode: 'device', - } as unknown as DestinationConfig, - } as unknown as Destination, + }, + }, ]; - expect(pluginsManager.getPluginsToLoadBasedOnConfig()).toEqual( - expect.arrayContaining([ + expect(pluginsManager.getPluginsToLoadBasedOnConfig().sort(alphabeticalCompare)).toEqual( + [ 'DeviceModeDestinations', 'NativeDestinationQueue', 'ExternalAnonymousId', 'GoogleLinker', - ]), + ].sort(alphabeticalCompare), ); }); @@ -93,12 +101,12 @@ describe('PluginsManager', () => { { config: { connectionMode: 'device', - } as unknown as DestinationConfig, - } as unknown as Destination, + }, + }, ]; state.plugins.pluginsToLoadFromConfig.value = []; - expect(pluginsManager.getPluginsToLoadBasedOnConfig()).toEqual(expect.arrayContaining([])); + expect(pluginsManager.getPluginsToLoadBasedOnConfig().sort(alphabeticalCompare)).toEqual([]); // Expect a warning for user not explicitly configuring it expect(defaultLogger.warn).toHaveBeenCalledTimes(1); @@ -113,15 +121,15 @@ describe('PluginsManager', () => { { config: { connectionMode: 'device', - } as unknown as DestinationConfig, - } as unknown as Destination, + }, + }, ]; // Only DeviceModeDestinations is configured state.plugins.pluginsToLoadFromConfig.value = ['DeviceModeDestinations']; - expect(pluginsManager.getPluginsToLoadBasedOnConfig()).toEqual( - expect.arrayContaining(['DeviceModeDestinations']), - ); + expect(pluginsManager.getPluginsToLoadBasedOnConfig().sort(alphabeticalCompare)).toEqual([ + 'DeviceModeDestinations', + ]); // Expect a warning for user not explicitly configuring it expect(defaultLogger.warn).toHaveBeenCalledTimes(1); @@ -136,24 +144,24 @@ describe('PluginsManager', () => { { config: { connectionMode: 'device', - } as unknown as DestinationConfig, - } as unknown as Destination, + }, + }, { config: { connectionMode: 'device', - } as unknown as DestinationConfig, + }, shouldApplyDeviceModeTransformation: true, - } as unknown as Destination, + }, ]; - expect(pluginsManager.getPluginsToLoadBasedOnConfig()).toEqual( - expect.arrayContaining([ + expect(pluginsManager.getPluginsToLoadBasedOnConfig().sort(alphabeticalCompare)).toEqual( + [ 'DeviceModeTransformation', 'DeviceModeDestinations', 'NativeDestinationQueue', 'ExternalAnonymousId', 'GoogleLinker', - ]), + ].sort(alphabeticalCompare), ); }); @@ -163,17 +171,17 @@ describe('PluginsManager', () => { { config: { connectionMode: 'device', - } as unknown as DestinationConfig, + }, shouldApplyDeviceModeTransformation: true, - } as unknown as Destination, + }, ]; state.plugins.pluginsToLoadFromConfig.value = [ 'DeviceModeDestinations', 'NativeDestinationQueue', ]; - expect(pluginsManager.getPluginsToLoadBasedOnConfig()).toEqual( - expect.arrayContaining(['DeviceModeDestinations', 'NativeDestinationQueue']), + expect(pluginsManager.getPluginsToLoadBasedOnConfig().sort(alphabeticalCompare)).toEqual( + ['DeviceModeDestinations', 'NativeDestinationQueue'].sort(alphabeticalCompare), ); // Expect a warning for user not explicitly configuring it @@ -186,8 +194,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()).toEqual( - expect.arrayContaining(['StorageEncryption', 'ExternalAnonymousId', 'GoogleLinker']), + expect(pluginsManager.getPluginsToLoadBasedOnConfig().sort(alphabeticalCompare)).toEqual( + ['StorageEncryption', 'ExternalAnonymousId', 'GoogleLinker'].sort(alphabeticalCompare), ); }); @@ -195,7 +203,7 @@ describe('PluginsManager', () => { state.storage.encryptionPluginName.value = 'StorageEncryption'; state.plugins.pluginsToLoadFromConfig.value = []; - expect(pluginsManager.getPluginsToLoadBasedOnConfig()).toEqual(expect.arrayContaining([])); + expect(pluginsManager.getPluginsToLoadBasedOnConfig().sort(alphabeticalCompare)).toEqual([]); // Expect a warning for user not explicitly configuring it expect(defaultLogger.warn).toHaveBeenCalledTimes(1); @@ -207,8 +215,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()).toEqual( - expect.arrayContaining(['StorageMigrator', 'ExternalAnonymousId', 'GoogleLinker']), + expect(pluginsManager.getPluginsToLoadBasedOnConfig().sort(alphabeticalCompare)).toEqual( + ['StorageMigrator', 'ExternalAnonymousId', 'GoogleLinker'].sort(alphabeticalCompare), ); }); @@ -216,7 +224,7 @@ describe('PluginsManager', () => { state.storage.migrate.value = true; state.plugins.pluginsToLoadFromConfig.value = []; - expect(pluginsManager.getPluginsToLoadBasedOnConfig()).toEqual(expect.arrayContaining([])); + expect(pluginsManager.getPluginsToLoadBasedOnConfig().sort(alphabeticalCompare)).toEqual([]); // Expect a warning for user not explicitly configuring it expect(defaultLogger.warn).toHaveBeenCalledTimes(1); @@ -232,7 +240,7 @@ describe('PluginsManager', () => { 'StorageMigrator', ]; - expect(pluginsManager.getPluginsToLoadBasedOnConfig()).toEqual(expect.arrayContaining([])); + expect(pluginsManager.getPluginsToLoadBasedOnConfig().sort(alphabeticalCompare)).toEqual([]); // Expect a warning for user not explicitly configuring it expect(defaultLogger.warn).toHaveBeenCalledTimes(2); @@ -244,15 +252,20 @@ describe('PluginsManager', () => { ); }); - it('should not log any warning if logger is not supplied', () => { - pluginsManager = new PluginsManager(defaultPluginEngine, defaultErrorHandler); + it('should log a warning if unknown plugins are configured', () => { + state.plugins.pluginsToLoadFromConfig.value = [ + 'UnknownPlugin1', + 'GoogleLinker', + 'UnknownPlugin2', + ]; - // Checking only for the migration plugin - state.storage.migrate.value = true; - state.plugins.pluginsToLoadFromConfig.value = []; + pluginsManager.setActivePlugins(); - expect(pluginsManager.getPluginsToLoadBasedOnConfig()).toEqual(expect.arrayContaining([])); - expect(defaultLogger.warn).not.toHaveBeenCalled(); + // Expect a warning for user not explicitly configuring it + expect(defaultLogger.warn).toHaveBeenCalledTimes(1); + expect(defaultLogger.warn).toHaveBeenCalledWith( + 'PluginsManager:: Ignoring unknown plugins: UnknownPlugin1, UnknownPlugin2.', + ); }); }); }); diff --git a/packages/analytics-js/__tests__/components/userSessionManager/UserSessionManager.test.ts b/packages/analytics-js/__tests__/components/userSessionManager/UserSessionManager.test.ts index 176ee09375..bbac6abf81 100644 --- a/packages/analytics-js/__tests__/components/userSessionManager/UserSessionManager.test.ts +++ b/packages/analytics-js/__tests__/components/userSessionManager/UserSessionManager.test.ts @@ -84,18 +84,18 @@ describe('User session manager', () => { clientDataStoreLS = defaultStoreManager.getStore('clientDataInLocalStorage') as Store; clientDataStoreSession = defaultStoreManager.getStore('clientDataInSessionStorage') as Store; + defaultHttpClient.init(defaultErrorHandler); + clearStorage(); resetState(); - defaultHttpClient.init(defaultErrorHandler); - 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 af86ca91fc..3fd3b96081 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 b969948888..4088b5de69 100644 --- a/packages/analytics-js/__tests__/components/utilities/consent.test.ts +++ b/packages/analytics-js/__tests__/components/utilities/consent.test.ts @@ -1,11 +1,12 @@ -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', () => { +describe('consent utilities', () => { beforeEach(() => { resetState(); }); @@ -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/ErrorHandler/ErrorHandler.test.ts b/packages/analytics-js/__tests__/services/ErrorHandler/ErrorHandler.test.ts index 400461052a..74d98f5778 100644 --- a/packages/analytics-js/__tests__/services/ErrorHandler/ErrorHandler.test.ts +++ b/packages/analytics-js/__tests__/services/ErrorHandler/ErrorHandler.test.ts @@ -143,7 +143,7 @@ describe('ErrorHandler', () => { 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 += '[MANUAL ERROR]'; + error.stack += '[SDK DISPATCHED ERROR]'; const errorEvent = new ErrorEvent('error', { error }); // @ts-expect-error not using the enum value for testing diff --git a/packages/analytics-js/__tests__/services/HttpClient/HttpClient.test.ts b/packages/analytics-js/__tests__/services/HttpClient/HttpClient.test.ts index 74689141fe..bf7fc49c3f 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; @@ -89,8 +64,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(); @@ -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/__tests__/services/PluginEngine/PluginEngine.test.ts b/packages/analytics-js/__tests__/services/PluginEngine/PluginEngine.test.ts index e474766b6f..072fe0c6a5 100644 --- a/packages/analytics-js/__tests__/services/PluginEngine/PluginEngine.test.ts +++ b/packages/analytics-js/__tests__/services/PluginEngine/PluginEngine.test.ts @@ -1,9 +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'; const mockPlugin1: ExtensionPlugin = { name: 'p1', foo: 'bar1', + initialize: jest.fn(), ext: { form: { processMeta(meta: string[]) { @@ -34,14 +36,12 @@ describe('PluginEngine', () => { let pluginEngineTestInstance: PluginEngine; beforeEach(() => { - pluginEngineTestInstance = new PluginEngine(); + pluginEngineTestInstance = new PluginEngine(defaultLogger); pluginEngineTestInstance.register(mockPlugin1); pluginEngineTestInstance.register(mockPlugin2); pluginEngineTestInstance.register(mockPlugin3); }); - afterEach(() => {}); - it('should retrieve all registered plugins', () => { expect(pluginEngineTestInstance.getPlugins().length).toEqual(3); }); @@ -55,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); @@ -89,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([ @@ -194,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/__tests__/services/StoreManager/Store.test.ts b/packages/analytics-js/__tests__/services/StoreManager/Store.test.ts index c2f91298f8..b24fc0a68e 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, ); }); @@ -53,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', () => { @@ -81,8 +102,11 @@ describe('Store', () => { { name: 'name', id: 'id', + errorHandler: defaultErrorHandler, + logger: defaultLogger, }, getStorageEngine('localStorage'), + pluginsManager, ); expect(store.createValidKey('test')).toStrictEqual('name.id.test'); }); @@ -95,8 +119,11 @@ describe('Store', () => { name: 'name', id: 'id', validKeys: { nope: 'wrongKey' }, + errorHandler: defaultErrorHandler, + logger: defaultLogger, }, getStorageEngine('localStorage'), + pluginsManager, ); expect(store.createValidKey('test')).toBeUndefined(); }); @@ -107,8 +134,11 @@ describe('Store', () => { { name: 'name', id: 'id', + errorHandler: defaultErrorHandler, + logger: defaultLogger, }, getStorageEngine('localStorage'), + pluginsManager, ); expect(store.createValidKey('queue')).toStrictEqual('name.id.queue'); }); @@ -133,8 +163,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 6d94926a7f..4112f0c605 100644 --- a/packages/analytics-js/src/app/RudderAnalytics.ts +++ b/packages/analytics-js/src/app/RudderAnalytics.ts @@ -278,7 +278,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 dfadd1072e..014eec61d5 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,25 +37,23 @@ 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); } init() { - try { - this.prepareBrowserCapabilities(); - this.attachWindowListeners(); - } catch (err) { - this.onError(err); - } + this.prepareBrowserCapabilities(); + this.attachWindowListeners(); } /** @@ -106,7 +105,7 @@ class CapabilitiesManager implements ICapabilitiesManager { state.loadOptions.value.sendAdblockPage === true && state.lifecycle.sourceConfigUrl.value !== undefined ) { - detectAdBlockers(this.errorHandler, this.logger); + detectAdBlockers(this.httpClient); } }); } @@ -122,7 +121,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)); } } @@ -202,11 +201,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/capabilitiesManager/detection/adBlockers.ts b/packages/analytics-js/src/components/capabilitiesManager/detection/adBlockers.ts index 82140c3651..e9f2a9b9fe 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 8e2eacf5c5..4a3040ae5d 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/configManager/ConfigManager.ts b/packages/analytics-js/src/components/configManager/ConfigManager.ts index 89cc9c4e1d..4e84aa9d30 100644 --- a/packages/analytics-js/src/components/configManager/ConfigManager.ts +++ b/packages/analytics-js/src/components/configManager/ConfigManager.ts @@ -4,12 +4,18 @@ 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 { + 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'; 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 +41,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 +55,7 @@ class ConfigManager implements IConfigManager { attachEffects() { effect(() => { - this.logger?.setMinLogLevel(state.lifecycle.logLevel.value); + this.logger.setMinLogLevel(state.lifecycle.logLevel.value); }); } @@ -60,8 +64,6 @@ class ConfigManager implements IConfigManager { * config related information in global state */ init() { - this.attachEffects(); - const { logLevel, configUrl, @@ -72,22 +74,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 (isNull(intgCdnUrl)) { + 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); @@ -120,23 +138,23 @@ class ConfigManager implements IConfigManager { /** * Handle errors */ - onError(error: unknown, customMessage?: string, shouldAlwaysThrow?: boolean) { - if (this.hasErrorHandler) { - this.errorHandler?.onError(error, CONFIG_MANAGER, customMessage); - } else { - throw error; - } + onError(error: unknown, customMessage?: string) { + this.errorHandler.onError(error, CONFIG_MANAGER, customMessage); } /** * 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(SOURCE_CONFIG_FETCH_ERROR(details?.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; } @@ -145,21 +163,21 @@ 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, 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; } // 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 +219,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 c097829bf6..eba72a5046 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'; @@ -77,9 +80,12 @@ export type SourceConfigResponse = { export interface IConfigManager { httpClient: IHttpClient; - errorHandler?: IErrorHandler; - logger?: ILogger; + errorHandler: IErrorHandler; + logger: ILogger; init: () => void; getConfig: () => void; - processConfig: () => void; + processConfig: ( + response: SourceConfigResponse | string | undefined, + details?: ResponseDetails, + ) => void; } diff --git a/packages/analytics-js/src/components/configManager/util/cdnPaths.ts b/packages/analytics-js/src/components/configManager/util/cdnPaths.ts index 0cc0070b18..137b696b3b 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 1396639883..a5ff9a2ca0 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, @@ -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, @@ -160,13 +160,11 @@ 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)) { - 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, @@ -222,7 +220,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); @@ -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, @@ -316,7 +314,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; @@ -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)); } } @@ -342,7 +340,7 @@ const getSourceConfigURL = ( writeKey: string, lockIntegrationsVersion: boolean, lockPluginsVersion: boolean, - logger?: ILogger, + logger: ILogger, ): string => { const defSearchParams = new URLSearchParams({ p: MODULE_TYPE, @@ -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 e4734a2978..8c92580c4e 100644 --- a/packages/analytics-js/src/components/core/Analytics.ts +++ b/packages/analytics-js/src/components/core/Analytics.ts @@ -98,9 +98,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, + ); } /** @@ -130,7 +134,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); @@ -239,15 +243,16 @@ 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, this.storeManager, + this.httpClient, this.errorHandler, this.logger, ); diff --git a/packages/analytics-js/src/components/eventManager/EventManager.ts b/packages/analytics-js/src/components/eventManager/EventManager.ts index ae82714e5d..fc76c2b2a1 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'; @@ -14,8 +12,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,15 +26,14 @@ class EventManager implements IEventManager { constructor( eventRepository: IEventRepository, userSessionManager: IUserSessionManager, - errorHandler?: IErrorHandler, - logger?: ILogger, + errorHandler: IErrorHandler, + logger: ILogger, ) { this.eventRepository = eventRepository; this.userSessionManager = userSessionManager; this.errorHandler = errorHandler; this.logger = logger; this.eventFactory = new RudderEventFactory(this.logger); - this.onError = this.onError.bind(this); } /** @@ -57,23 +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, shouldAlwaysThrow?: boolean): void { - if (this.errorHandler) { - this.errorHandler.onError(error, EVENT_MANAGER, customMessage); - } else { - throw error; - } + 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 48843568f5..263da80d90 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; } @@ -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/eventManager/utilities.ts b/packages/analytics-js/src/components/eventManager/utilities.ts index 3ea0e9140e..93595ac37b 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 aa8345a8b8..b2698e5d2f 100644 --- a/packages/analytics-js/src/components/eventRepository/EventRepository.ts +++ b/packages/analytics-js/src/components/eventRepository/EventRepository.ts @@ -8,19 +8,8 @@ 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 { - DATAPLANE_PLUGIN_ENQUEUE_ERROR, - DATAPLANE_PLUGIN_INITIALIZE_ERROR, - DMT_PLUGIN_INITIALIZE_ERROR, - 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 { @@ -54,62 +43,49 @@ 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(logger); - this.httpClient.init(errorHandler); 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(() => { @@ -172,53 +148,30 @@ 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 const apiName = `${event.type.charAt(0).toUpperCase()}${event.type.slice(1)}${API_SUFFIX}`; safelyInvokeCallback(callback, [dpQEvent], apiName, this.logger); } - - /** - * 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, shouldAlwaysThrow?: boolean): void { - if (this.errorHandler) { - this.errorHandler.onError(error, EVENT_REPOSITORY, customMessage); - } else { - throw error; - } - } } export { EventRepository }; diff --git a/packages/analytics-js/src/components/pluginsManager/PluginsManager.ts b/packages/analytics-js/src/components/pluginsManager/PluginsManager.ts index 1aae6780c6..0882d5786a 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'; @@ -37,10 +38,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; @@ -101,7 +102,7 @@ class PluginsManager implements IPluginsManager { // Filter deprecated plugins pluginsToLoadFromConfig = pluginsToLoadFromConfig.filter(pluginName => { if (deprecatedPluginsList.includes(pluginName)) { - this.logger?.warn(DEPRECATED_PLUGIN_WARNING(PLUGINS_MANAGER, pluginName)); + this.logger.warn(DEPRECATED_PLUGIN_WARNING(PLUGINS_MANAGER, pluginName)); return false; } return true; @@ -200,7 +201,7 @@ class PluginsManager implements IPluginsManager { pluginsToLoadFromConfig.push(...missingPlugins); } - this.logger?.warn( + this.logger.warn( generateMisconfiguredPluginsWarning( PLUGINS_MANAGER, group.configurationStatusStr, @@ -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(() => { @@ -341,11 +334,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 0b774725fb..8da57f2c85 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 81b4923650..1188a1ffa1 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 db276d2af3..a175681944 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 f711da0971..056105a927 100644 --- a/packages/analytics-js/src/constants/logMessages.ts +++ b/packages/analytics-js/src/constants/logMessages.ts @@ -11,17 +11,19 @@ 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 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 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 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.`; // ERROR const UNSUPPORTED_CONSENT_MANAGER_ERROR = ( @@ -42,9 +44,6 @@ const BREADCRUMB_ERROR = (context: string): string => const HANDLE_ERROR_FAILURE = (context: string): string => `${context}${LOG_CONTEXT_SEPARATOR}Failed to handle the error.`; -const NOTIFY_FAILURE_ERROR = (context: string): string => - `${context}${LOG_CONTEXT_SEPARATOR}Failed to notify the error.`; - const PLUGIN_NAME_MISSING_ERROR = (context: string): string => `${context}${LOG_CONTEXT_SEPARATOR}Plugin name is missing.`; @@ -70,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.`; @@ -117,6 +115,16 @@ const STORAGE_TYPE_VALIDATION_WARNING = ( ): string => `${context}${LOG_CONTEXT_SEPARATOR}The storage type "${storageType}" is not supported. Please choose one of the following supported types: "${SUPPORTED_STORAGE_TYPES}". The default type "${defaultStorageType}" will be used instead.`; +const UNSUPPORTED_ERROR_REPORTING_PROVIDER_WARNING = ( + context: string, + selectedErrorReportingProvider: string | undefined, + errorReportingProvidersToPluginNameMap: Record, + defaultProvider: string, +): string => + `${context}${LOG_CONTEXT_SEPARATOR}The error reporting provider "${selectedErrorReportingProvider}" is not supported. Please choose one of the following supported providers: "${Object.keys( + errorReportingProvidersToPluginNameMap, + )}". The default provider "${defaultProvider}" will be used instead.`; + const UNSUPPORTED_STORAGE_ENCRYPTION_VERSION_WARNING = ( context: string, selectedStorageEncryptionVersion: string | undefined, @@ -193,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.`; @@ -251,8 +252,12 @@ 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, UNSUPPORTED_STORAGE_ENCRYPTION_VERSION_WARNING, STORAGE_DATA_MIGRATION_OVERRIDE_WARNING, RESERVED_KEYWORD_WARNING, @@ -263,7 +268,7 @@ export { TIMEOUT_NOT_RECOMMENDED_WARNING, INVALID_SESSION_ID_WARNING, DEPRECATED_PLUGIN_WARNING, - NOTIFY_FAILURE_ERROR, + HANDLE_ERROR_FAILURE, PLUGIN_NAME_MISSING_ERROR, PLUGIN_ALREADY_EXISTS_ERROR, PLUGIN_NOT_FOUND_ERROR, @@ -284,7 +289,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, @@ -293,11 +297,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, @@ -309,9 +308,9 @@ export { SERVER_SIDE_COOKIE_FEATURE_OVERRIDE_WARNING, BAD_COOKIES_WARNING, PAGE_UNLOAD_ON_BEACON_DISABLED_WARNING, - NON_ERROR_WARNING, BREADCRUMB_ERROR, - HANDLE_ERROR_FAILURE, + NON_ERROR_WARNING, CALLBACK_INVOKE_ERROR, + UNKNOWN_PLUGINS_WARNING, INVALID_CALLBACK_FN_ERROR, }; diff --git a/packages/analytics-js/src/services/HttpClient/HttpClient.ts b/packages/analytics-js/src/services/HttpClient/HttpClient.ts index 6c79d76f1b..18525cf831 100644 --- a/packages/analytics-js/src/services/HttpClient/HttpClient.ts +++ b/packages/analytics-js/src/services/HttpClient/HttpClient.ts @@ -20,19 +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.hasErrorHandler = Boolean(this.errorHandler); this.onError = this.onError.bind(this); } init(errorHandler: IErrorHandler) { this.errorHandler = errorHandler; - this.hasErrorHandler = true; } /** @@ -54,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 }; } } @@ -76,7 +72,6 @@ class HttpClient implements IHttpClient { } }) .catch((data: ResponseDetails) => { - this.onError(data.error ?? data); if (!isFireAndForget) { callback(undefined, data); } @@ -87,11 +82,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); } /** @@ -110,6 +101,6 @@ class HttpClient implements IHttpClient { } } -const defaultHttpClient: HttpClient = new HttpClient(defaultLogger); +const defaultHttpClient = new HttpClient(defaultLogger); export { HttpClient, defaultHttpClient }; diff --git a/packages/analytics-js/src/services/HttpClient/xhr/xhrResponseHandler.ts b/packages/analytics-js/src/services/HttpClient/xhr/xhrResponseHandler.ts index 5d3d2cd8d0..3c6a22d31d 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 0225d0250b..37ba01fed7 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,8 @@ class PluginEngine implements IPluginEngine { if (this.config.throws) { throw new Error(errorMessage); } else { - this.logger?.error(errorMessage, plugin); + this.logger.error(errorMessage, plugin); + return; } } @@ -55,7 +56,8 @@ class PluginEngine implements IPluginEngine { if (this.config.throws) { throw new Error(errorMessage); } else { - this.logger?.error(errorMessage); + this.logger.error(errorMessage); + return; } } @@ -86,7 +88,8 @@ class PluginEngine implements IPluginEngine { if (this.config.throws) { throw new Error(errorMessage); } else { - this.logger?.error(errorMessage); + this.logger.error(errorMessage); + return; } } @@ -97,7 +100,8 @@ class PluginEngine implements IPluginEngine { if (this.config.throws) { throw new Error(errorMessage); } else { - this.logger?.error(errorMessage); + this.logger.error(errorMessage); + return; } } @@ -119,7 +123,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 +179,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 +199,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 744ab3e6af..0605d9da72 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 26d8ee1089..5d14aacb18 100644 --- a/packages/analytics-js/src/services/StoreManager/StoreManager.ts +++ b/packages/analytics-js/src/services/StoreManager/StoreManager.ts @@ -40,17 +40,14 @@ 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 +106,8 @@ class StoreManager implements IStoreManager { isEncrypted: true, noCompoundKey: true, type: storageType, + errorHandler: this.errorHandler, + logger: this.logger, }); } }); @@ -192,7 +191,7 @@ class StoreManager implements IStoreManager { } if (finalStorageType !== storageType) { - this.logger?.warn( + this.logger.warn( STORAGE_UNAVAILABLE_WARNING(STORE_MANAGER, sessionKey, storageType, finalStorageType), ); } @@ -215,17 +214,6 @@ class StoreManager implements IStoreManager { getStore(id: StoreId): Store | undefined { return this.stores[id]; } - - /** - * Handle errors - */ - onError(error: unknown) { - if (this.hasErrorHandler) { - this.errorHandler?.onError(error, STORE_MANAGER); - } else { - throw error; - } - } } export { StoreManager }; diff --git a/packages/analytics-js/src/state/slices/lifecycle.ts b/packages/analytics-js/src/state/slices/lifecycle.ts index 4a2f69018b..ccb44790be 100644 --- a/packages/analytics-js/src/state/slices/lifecycle.ts +++ b/packages/analytics-js/src/state/slices/lifecycle.ts @@ -1,6 +1,7 @@ import { signal } from '@preact/signals-core'; import type { LifecycleState } from '@rudderstack/analytics-js-common/types/ApplicationState'; import { DEST_SDK_BASE_URL, PLUGINS_BASE_URL } from '../../constants/urls'; +import { POST_LOAD_LOG_LEVEL } from '../../services/Logger'; const lifecycleState: LifecycleState = { activeDataplaneUrl: signal(undefined), @@ -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-v1.1/.size-limit.js b/packages/analytics-v1.1/.size-limit.js index f70166cf3c..abd60fdde9 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', },