UNPKG

vwo-fme-node-sdk

Version:

VWO Node/JavaScript SDK for Feature Management and Experimentation

434 lines (386 loc) 15 kB
/** * Copyright 2024-2025 Wingify Software Pvt. Ltd. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { dynamic } from './types/Common'; import { ILogManager, LogManager } from './packages/logger'; import { NetworkManager } from './packages/network-layer'; import { SegmentationManager } from './packages/segmentation-evaluator'; import { Storage } from './packages/storage'; import { IVWOClient, VWOClient } from './VWOClient'; import { SettingsModel } from './models/settings/SettingsModel'; import { SettingsService } from './services/SettingsService'; import { DebugLogMessagesEnum, ErrorLogMessagesEnum, InfoLogMessagesEnum } from './enums/log-messages'; import { IVWOOptions } from './models/VWOOptionsModel'; import { isNumber } from './utils/DataTypeUtil'; import { cloneObject } from './utils/FunctionUtil'; import { buildMessage } from './utils/LogMessageUtil'; import { Deferred } from './utils/PromiseUtil'; import { setSettingsAndAddCampaignsToRules } from './utils/SettingsUtil'; import { getRandomUUID } from './utils/UuidUtil'; import { BatchEventsQueue } from './services/BatchEventsQueue'; import { BatchEventsDispatcher } from './utils/BatchEventsDispatcher'; import { UsageStatsUtil } from './utils/UsageStatsUtil'; export interface IVWOBuilder { settings: Record<any, any>; // Holds the configuration settings for the VWO client storage: Storage; // Interface for storage management logManager: ILogManager; // Manages logging across the VWO SDK isSettingsFetchInProgress: boolean; // Flag to check if settings fetch is in progress vwoInstance: IVWOClient; build(settings: Record<any, any>): IVWOClient; // Builds and returns a new VWOClient instance fetchSettings(): Promise<Record<any, any>>; // Asynchronously fetches settings from the server setSettingsService(): this; // Sets up the settings manager with provided options getSettings(force: boolean): Promise<Record<any, any>>; // Fetches settings, optionally forcing a refresh setStorage(): this; // Sets up the storage connector based on provided options setNetworkManager(): this; // Configures the network manager with client and mode // initBatching(): this; // Initializes event batching with provided configuration // setAnalyticsCallback(): this; // Configures the analytics callback based on provided options initPolling(): this; // Sets up polling for settings at a specified interval setLogger(): this; // Sets up the logger with specified options setSegmentation(): this; // Configures the segmentation evaluator with provided options initUsageStats(): this; // Initializes usage statistics for the SDK } export class VWOBuilder implements IVWOBuilder { readonly sdkKey: string; readonly options: IVWOOptions; private settingFileManager: SettingsService; settings: Record<any, any>; storage: Storage; logManager: ILogManager; originalSettings: dynamic; isSettingsFetchInProgress: boolean; vwoInstance: IVWOClient; batchEventsQueue: BatchEventsQueue; constructor(options: IVWOOptions) { this.options = options; } /** * Sets the network manager with the provided client and development mode options. * @returns {this} The instance of this builder. */ setNetworkManager(): this { const networkInstance = NetworkManager.Instance; // Attach the network client from options networkInstance.attachClient(this.options?.network?.client, this.options?.retryConfig); LogManager.Instance.debug( buildMessage(DebugLogMessagesEnum.SERVICE_INITIALIZED, { service: `Network Layer`, }), ); // Set the development mode based on options networkInstance.getConfig().setDevelopmentMode(this.options?.isDevelopmentMode); return this; } initBatching(): this { if (this.options.batchEventData) { if (this.settingFileManager.isGatewayServiceProvided) { LogManager.Instance.info(buildMessage(InfoLogMessagesEnum.GATEWAY_AND_BATCH_EVENTS_CONFIG_MISMATCH)); return this; } if ( (!isNumber(this.options.batchEventData.eventsPerRequest) || this.options.batchEventData.eventsPerRequest <= 0) && (!isNumber(this.options.batchEventData.requestTimeInterval) || this.options.batchEventData.requestTimeInterval <= 0) ) { LogManager.Instance.error( 'Invalid batch events config, should be an object, eventsPerRequest should be a number greater than 0 and requestTimeInterval should be a number greater than 0', ); return this; } this.batchEventsQueue = new BatchEventsQueue( Object.assign({}, this.options.batchEventData, { dispatcher: ( events: Record<string, any>[], callback: (error: Error | null, data: Record<string, any>) => void, ) => BatchEventsDispatcher.dispatch( { ev: events, }, callback, Object.assign( {}, { a: this.options.accountId, env: this.options.sdkKey, }, ), ), }), ); this.batchEventsQueue.flushAndClearTimer.bind(this.batchEventsQueue); } return this; } /** * Sets the segmentation evaluator with the provided segmentation options. * @returns {this} The instance of this builder. */ setSegmentation(): this { SegmentationManager.Instance.attachEvaluator(this.options?.segmentation); LogManager.Instance.debug( buildMessage(DebugLogMessagesEnum.SERVICE_INITIALIZED, { service: `Segmentation Evaluator`, }), ); return this; } /** * Fetches settings asynchronously, ensuring no parallel fetches. * @param {boolean} [force=false] - Force fetch ignoring cache. * @returns {Promise<SettingsModel>} A promise that resolves to the fetched settings. */ fetchSettings(force?: boolean): Promise<Record<any, any>> { const deferredObject = new Deferred(); // Check if a fetch operation is already in progress if (!this.isSettingsFetchInProgress) { this.isSettingsFetchInProgress = true; this.settingFileManager.getSettings(force).then((settings: Record<any, any>) => { // if force is false, update original settings, if true the request is from polling and no need to update original settings if (!force) { this.originalSettings = settings; } this.isSettingsFetchInProgress = false; deferredObject.resolve(settings); }); return deferredObject.promise; } else { // Avoid parallel fetches by recursively calling fetchSettings return this.fetchSettings(force); } } /** * Gets the settings, fetching them if not cached or if forced. * @param {boolean} [force=false] - Force fetch ignoring cache. * @returns {Promise<SettingsModel>} A promise that resolves to the settings. */ getSettings(force?: boolean): Promise<Record<any, any>> { const deferredObject = new Deferred(); try { // Use cached settings if available and not forced to fetch if (!force && this.settings) { LogManager.Instance.info('Using already fetched and cached settings'); deferredObject.resolve(this.settings); } else { // Fetch settings if not cached or forced this.fetchSettings(force).then((settings: Record<any, any>) => { deferredObject.resolve(settings); }); } } catch (err) { LogManager.Instance.error('Failed to fetch settings. Error: ' + err); deferredObject.resolve({}); } return deferredObject.promise; } /** * Sets the storage connector based on the provided storage options. * @returns {this} The instance of this builder. */ setStorage(): this { if (this.options.storage) { // Attach the storage connector from options this.storage = Storage.Instance.attachConnector(this.options.storage); } else if (typeof process.env === 'undefined' && typeof window !== 'undefined' && window.localStorage) { // eslint-disable-next-line @typescript-eslint/no-var-requires const { BrowserStorageConnector } = require('./packages/storage/connectors/BrowserStorageConnector'); // Pass clientStorage config to BrowserStorageConnector this.storage = Storage.Instance.attachConnector( new BrowserStorageConnector({ ...this.options.clientStorage, alwaysUseCachedSettings: this.options.clientStorage?.alwaysUseCachedSettings, ttl: this.options.clientStorage?.ttl, }), ); LogManager.Instance.debug( buildMessage(DebugLogMessagesEnum.SERVICE_INITIALIZED, { service: this.options?.clientStorage?.provider === sessionStorage ? `Session Storage` : `Local Storage`, }), ); } else { // Set storage to null if no storage options provided this.storage = null; } return this; } /** * Sets the settings manager with the provided options. * @returns {this} The instance of this builder. */ setSettingsService(): this { this.settingFileManager = new SettingsService(this.options); return this; } /** * Sets the logger with the provided logger options. * @returns {this} The instance of this builder. */ setLogger(): this { this.logManager = new LogManager(this.options.logger || {}); LogManager.Instance.debug( buildMessage(DebugLogMessagesEnum.SERVICE_INITIALIZED, { service: `Logger`, }), ); return this; } /** * Sets the analytics callback with the provided analytics options. * @returns {this} The instance of this builder. */ /* setAnalyticsCallback(): this { if (!isObject(this.options.analyticsEvent)) { // TODO: add logging here return this; } if (!isFunction(this.options.analyticsEvent.eventCallback)) { // TODO: add logging here return this; } if ( this.options.analyticsEvent.isBatchingSupported && !isBoolean(this.options.analyticsEvent.isBatchingSupported) ) { // TODO:- add logging here return this; } // AnalyticsEvent.Instance.attachCallback( // this.options.analyticsEvent.eventCallback, // this.options.analyticsEvent.isBatchingSupported // ); return this; } */ /** * Generates a random user ID based on the provided API key. * @returns {string} The generated random user ID. */ getRandomUserId(): string { const apiName = 'getRandomUserId'; try { LogManager.Instance.debug( buildMessage(DebugLogMessagesEnum.API_CALLED, { apiName, }), ); return getRandomUUID(this.options.sdkKey); } catch (err) { LogManager.Instance.error( buildMessage(ErrorLogMessagesEnum.API_THROW_ERROR, { apiName, err, }), ); } } /** * Initializes the batching with the provided batch events options. * @returns {this} The instance of this builder. */ /* initBatching(): this { if (!isObject(this.options.batchEvents)) { // TODO:- add logging here return this; } if ( isObject(this.options.batchEvents) && (!( (isNumber(this.options.batchEvents.eventsPerRequest) && this.options.batchEvents.eventsPerRequest > 0 && this.options.batchEvents.eventsPerRequest <= Constants.MAX_EVENTS_PER_REQUEST) || (isNumber(this.options.batchEvents.requestTimeInterval) && this.options.batchEvents.requestTimeInterval >= 1) ) || !isFunction(this.options.batchEvents.flushCallback)) ) { LogManager.Instance.error('Invalid batchEvents config'); // throw new Error('Invalid batchEvents config'); return this; } // BatchEventsQueue.Instance.setBatchConfig(this.options.batchEvents, this.options.sdkKey); // TODO return this; } */ /** * Initializes the polling with the provided poll interval. * @returns {this} The instance of this builder. */ initPolling(): this { if (!this.options.pollInterval) { return this; } if (this.options.pollInterval && !isNumber(this.options.pollInterval)) { LogManager.Instance.error( buildMessage(ErrorLogMessagesEnum.INIT_OPTIONS_INVALID, { key: 'pollInterval', correctType: 'number', }), ); return this; } if (this.options.pollInterval && this.options.pollInterval < 1000) { LogManager.Instance.error( buildMessage(ErrorLogMessagesEnum.INIT_OPTIONS_INVALID, { key: 'pollInterval', correctType: 'number', }), ); return this; } this.checkAndPoll(); return this; } /** * Initializes usage statistics for the SDK. * @returns {this} The instance of this builder. */ initUsageStats(): this { if (this.options.isUsageStatsDisabled) { return this; } UsageStatsUtil.getInstance().setUsageStats(this.options); return this; } /** * Builds a new VWOClient instance with the provided settings. * @param {SettingsModel} settings - The settings for the VWOClient. * @returns {VWOClient} The new VWOClient instance. */ build(settings: Record<any, any>): IVWOClient { this.vwoInstance = new VWOClient(settings, this.options); return this.vwoInstance; } /** * Checks and polls for settings updates at the provided interval. */ checkAndPoll(): void { const pollingInterval = this.options.pollInterval; setInterval(() => { this.getSettings(true) .then((latestSettings: SettingsModel) => { const lastSettings = JSON.stringify(this.originalSettings); const stringifiedLatestSettings = JSON.stringify(latestSettings); if (stringifiedLatestSettings !== lastSettings) { this.originalSettings = latestSettings; const clonedSettings = cloneObject(latestSettings); LogManager.Instance.info(InfoLogMessagesEnum.POLLING_SET_SETTINGS); setSettingsAndAddCampaignsToRules(clonedSettings, this.vwoInstance); } else { LogManager.Instance.info(InfoLogMessagesEnum.POLLING_NO_CHANGE_IN_SETTINGS); } }) .catch(() => { LogManager.Instance.error(ErrorLogMessagesEnum.POLLING_FETCH_SETTINGS_FAILED); }); }, pollingInterval); } }