UNPKG

vwo-fme-node-sdk

Version:

VWO Node/JavaScript SDK for Feature Management and Experimentation

435 lines (392 loc) 14.6 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 { getCurrentUnixTimestamp, getCurrentUnixTimestampInMillis, getRandomNumber } from './FunctionUtil'; import { getUUID } from './UuidUtil'; import { Constants } from '../constants'; import { HeadersEnum } from '../enums/HeadersEnum'; import { HttpMethodEnum } from '../enums/HttpMethodEnum'; import { UrlEnum } from '../enums/UrlEnum'; import { DebugLogMessagesEnum, ErrorLogMessagesEnum, InfoLogMessagesEnum } from '../enums/log-messages'; import { SettingsModel } from '../models/settings/SettingsModel'; import { LogManager } from '../packages/logger'; import { NetworkManager, RequestModel, ResponseModel } from '../packages/network-layer'; import { SettingsService } from '../services/SettingsService'; import { dynamic } from '../types/Common'; import { isObject } from './DataTypeUtil'; import { buildMessage } from './LogMessageUtil'; import { UrlUtil } from './UrlUtil'; import { Deferred } from './PromiseUtil'; import { HTTPS } from '../constants/Url'; import { UsageStatsUtil } from './UsageStatsUtil'; import { IRetryConfig } from '../packages/network-layer/client/NetworkClient'; /** * Constructs the settings path with API key and account ID. * @param {string} sdkKey - The API key. * @param {any} accountId - The account identifier. * @returns {Record<string, dynamic>} - The settings path including API key, random number, and account ID. */ export function getSettingsPath(sdkKey: string, accountId: string | number): Record<string, dynamic> { const path: Record<string, dynamic> = { i: `${sdkKey}`, // Inject API key r: Math.random(), // Random number for cache busting a: accountId, // Account ID }; return path; } /** * Constructs the tracking path for an event. * @param {string} event - The event type. * @param {string} accountId - The account identifier. * @param {string} userId - The user identifier. * @returns {Record<string, dynamic>} - The tracking path for the event. */ export function getTrackEventPath(event: string, accountId: string, userId: string): Record<string, dynamic> { const path: Record<string, dynamic> = { event_type: event, // Type of the event account_id: accountId, // Account ID uId: userId, // User ID u: getUUID(userId, accountId), // UUID generated for the user sdk: Constants.SDK_NAME, // SDK name constant 'sdk-v': Constants.SDK_VERSION, // SDK version random: getRandomNumber(), // Random number for uniqueness ap: Constants.PLATFORM, // Application platform sId: getCurrentUnixTimestamp(), // Session ID ed: JSON.stringify({ p: 'server' }), // Additional encoded data }; return path; } /** * Builds generic properties for different tracking calls required by VWO servers. * @param {Object} configObj * @param {String} eventName * @returns properties */ export function getEventsBaseProperties( eventName: string, visitorUserAgent: string = '', ipAddress: string = '', ): Record<string, any> { const sdkKey = SettingsService.Instance.sdkKey; const properties = Object.assign({ en: eventName, a: SettingsService.Instance.accountId, env: sdkKey, eTime: getCurrentUnixTimestampInMillis(), random: getRandomNumber(), p: 'FS', visitor_ua: visitorUserAgent, visitor_ip: ipAddress, }); properties.url = Constants.HTTPS_PROTOCOL + UrlUtil.getBaseUrl() + UrlEnum.EVENTS; return properties; } /** * Builds generic payload required by all the different tracking calls. * @param {Object} settings settings file * @param {String} userId user id * @param {String} eventName event name * @returns properties */ export function _getEventBasePayload( settings: SettingsModel, userId: string | number, eventName: string, visitorUserAgent = '', ipAddress = '', ): Record<string, any> { const uuid = getUUID(userId.toString(), SettingsService.Instance.accountId.toString()); const sdkKey = SettingsService.Instance.sdkKey; const props: { vwo_sdkName: string; vwo_sdkVersion: string; vwo_envKey: string; id?: string | number; variation?: string | number; isFirst?: number; isCustomEvent?: boolean; data?: Record<string, any>; product?: string; } = { vwo_sdkName: Constants.SDK_NAME, vwo_sdkVersion: Constants.SDK_VERSION, vwo_envKey: sdkKey, }; const properties = { d: { msgId: `${uuid}-${getCurrentUnixTimestampInMillis()}`, visId: uuid, sessionId: getCurrentUnixTimestamp(), visitor_ua: visitorUserAgent, visitor_ip: ipAddress, event: { props: props, name: eventName, time: getCurrentUnixTimestampInMillis(), }, visitor: { props: { vwo_fs_environment: sdkKey, }, }, }, }; return properties; } /** * Builds payload to track the visitor. * @param {Object} configObj * @param {String} userId * @param {String} eventName * @param {String} campaignId * @param {Number} variationId * @returns track-user payload */ export function getTrackUserPayloadData( settings: SettingsModel, userId: string | number, eventName: string, campaignId: number, variationId: number, visitorUserAgent: string = '', ipAddress: string = '', ): Record<string, any> { const properties = _getEventBasePayload(settings, userId, eventName, visitorUserAgent, ipAddress); properties.d.event.props.id = campaignId; properties.d.event.props.variation = variationId; properties.d.event.props.isFirst = 1; // add usageStats as a new meta key to properties.d.events.props.vwoMeta if (Object.keys(UsageStatsUtil.getInstance().getUsageStats()).length > 0) { properties.d.event.props.vwoMeta = UsageStatsUtil.getInstance().getUsageStats(); } LogManager.Instance.debug( buildMessage(DebugLogMessagesEnum.IMPRESSION_FOR_TRACK_USER, { accountId: settings.getAccountId(), userId, campaignId, }), ); return properties; } /** * Constructs the payload data for tracking goals with custom event properties. * @param {any} settings - Configuration settings. * @param {any} userId - User identifier. * @param {string} eventName - Name of the event. * @param {any} eventProperties - Custom properties for the event. * @param {string} [visitorUserAgent=''] - Visitor's user agent. * @param {string} [ipAddress=''] - Visitor's IP address. * @returns {any} - The constructed payload data. */ export function getTrackGoalPayloadData( settings: SettingsModel, userId: string | number, eventName: string, eventProperties: Record<string, any>, visitorUserAgent: string = '', ipAddress: string = '', ): Record<string, any> { const properties = _getEventBasePayload(settings, userId, eventName, visitorUserAgent, ipAddress); properties.d.event.props.isCustomEvent = true; // Mark as a custom event properties.d.event.props.variation = 1; // Temporary value for variation properties.d.event.props.id = 1; // Temporary value for ID // Add custom event properties if provided if (eventProperties && isObject(eventProperties) && Object.keys(eventProperties).length > 0) { for (const prop in eventProperties) { properties.d.event.props[prop] = eventProperties[prop]; } } LogManager.Instance.debug( buildMessage(DebugLogMessagesEnum.IMPRESSION_FOR_TRACK_GOAL, { eventName, accountId: settings.getAccountId(), userId, }), ); return properties; } /** * Constructs the payload data for syncing multiple visitor attributes. * @param {SettingsModel} settings - Configuration settings. * @param {string | number} userId - User ID. * @param {string} eventName - Event name. * @param {Record<string, any>} attributes - Key-value map of attributes. * @param {string} [visitorUserAgent=''] - Visitor's User-Agent (optional). * @param {string} [ipAddress=''] - Visitor's IP Address (optional). * @returns {Record<string, any>} - Payload object to be sent in the request. */ export function getAttributePayloadData( settings: SettingsModel, userId: string | number, eventName: string, attributes: Record<string, any>, visitorUserAgent: string = '', ipAddress: string = '', ): Record<string, any> { const properties = _getEventBasePayload(settings, userId, eventName, visitorUserAgent, ipAddress); properties.d.event.props.isCustomEvent = true; // Mark as a custom event properties.d.event.props[Constants.VWO_FS_ENVIRONMENT] = settings.getSdkkey(); // Set environment key // Iterate over the attributes map and append to the visitor properties for (const [key, value] of Object.entries(attributes)) { properties.d.visitor.props[key] = value; } LogManager.Instance.debug( buildMessage(DebugLogMessagesEnum.IMPRESSION_FOR_SYNC_VISITOR_PROP, { eventName, accountId: settings.getAccountId(), userId, }), ); return properties; } /** * Sends a POST API request with the specified properties and payload. * @param {any} properties - Properties for the request. * @param {any} payload - Payload for the request. * @param {string} userId - User ID. */ export async function sendPostApiRequest(properties: any, payload: any, userId: string): Promise<void> { const networkManager = NetworkManager.Instance; networkManager.attachClient(); const retryConfig: IRetryConfig = networkManager.getRetryConfig(); const headers: Record<string, string> = {}; const userAgent = payload.d.visitor_ua; // Extract user agent from payload const ipAddress = payload.d.visitor_ip; // Extract IP address from payload // Set headers if available if (userAgent) headers[HeadersEnum.USER_AGENT] = userAgent; if (ipAddress) headers[HeadersEnum.IP] = ipAddress; let baseUrl = UrlUtil.getBaseUrl(); baseUrl = UrlUtil.getUpdatedBaseUrl(baseUrl); const request: RequestModel = new RequestModel( baseUrl, HttpMethodEnum.POST, UrlEnum.EVENTS, properties, payload, headers, SettingsService.Instance.protocol, SettingsService.Instance.port, retryConfig, ); await NetworkManager.Instance.post(request) .then(() => { // clear usage stats only if network call is successful if (Object.keys(UsageStatsUtil.getInstance().getUsageStats()).length > 0) { UsageStatsUtil.getInstance().clearUsageStats(); } LogManager.Instance.info( buildMessage(InfoLogMessagesEnum.NETWORK_CALL_SUCCESS, { event: properties.en, endPoint: UrlEnum.EVENTS, accountId: SettingsService.Instance.accountId, userId: userId, uuid: payload.d.visId, }), ); }) .catch((err: ResponseModel) => { LogManager.Instance.error( buildMessage(ErrorLogMessagesEnum.NETWORK_CALL_FAILED, { method: HttpMethodEnum.POST, err: isObject(err) ? JSON.stringify(err) : err, }), ); }); } // Flag to determine if the SDK should wait for a network response. let shouldWaitForTrackingCalls = false; /** * Checks if the SDK should wait for a network response. * @returns {boolean} - True if the SDK should wait for a network response, false otherwise. */ export function getShouldWaitForTrackingCalls(): boolean { return shouldWaitForTrackingCalls; } /** * Sets the value to determine if the SDK should wait for a network response. * @param value - The value to set. */ export function setShouldWaitForTrackingCalls(value: boolean): void { shouldWaitForTrackingCalls = value; } /** * Constructs the payload for a messaging event. * @param messageType - The type of the message. * @param message - The message to send. * @param eventName - The name of the event. * @returns The constructed payload. */ export function getMessagingEventPayload(messageType: string, message: string, eventName: string): Record<string, any> { const userId = SettingsService.Instance.accountId + '_' + SettingsService.Instance.sdkKey; const properties = _getEventBasePayload(null, userId, eventName, null, null); properties.d.event.props[Constants.VWO_FS_ENVIRONMENT] = SettingsService.Instance.sdkKey; // Set environment key properties.d.event.props.product = 'fme'; const data = { type: messageType, content: { title: message, dateTime: getCurrentUnixTimestampInMillis(), }, }; properties.d.event.props.data = data; return properties; } /** * Sends a messaging event to DACDN * @param properties - Query parameters for the request. * @param payload - The payload for the request. * @returns A promise that resolves to the response from DACDN. */ export async function sendMessagingEvent(properties: Record<string, any>, payload: Record<string, any>): Promise<any> { // Create a new deferred object to manage promise resolution const deferredObject = new Deferred(); // Singleton instance of the network manager const networkInstance = NetworkManager.Instance; const retryConfig: IRetryConfig = networkInstance.getRetryConfig(); // disable retry for messaging event retryConfig.shouldRetry = false; let baseUrl = UrlUtil.getBaseUrl(); baseUrl = UrlUtil.getUpdatedBaseUrl(baseUrl); try { // Create a new request model instance with the provided parameters const request: RequestModel = new RequestModel( baseUrl, HttpMethodEnum.POST, UrlEnum.EVENTS, properties, payload, null, HTTPS, null, retryConfig, ); // Perform the network GET request networkInstance .post(request) .then((response: ResponseModel) => { // Resolve the deferred object with the data from the response deferredObject.resolve(response.getData()); }) .catch((err: ResponseModel) => { // Reject the deferred object with the error response deferredObject.reject(err); }); return deferredObject.promise; } catch (err) { // Resolve the promise with false as fallback deferredObject.resolve(false); return deferredObject.promise; } }