/** * Copyright 2024-2026 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, getFormattedErrorMessage, getRandomNumber, } from './FunctionUtil'; import { getRandomUUID, 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 { LogLevelEnum } from '../packages/logger'; import { 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 { Deferred } from './PromiseUtil'; import { UsageStatsUtil } from './UsageStatsUtil'; import { IRetryConfig } from '../packages/network-layer/client/NetworkClient'; import { EventEnum } from '../enums/EventEnum'; import { ContextModel } from '../models/user/ContextModel'; import { DebuggerCategoryEnum } from '../enums/DebuggerCategoryEnum'; import { sendDebugEventToWingify } from './DebuggerServiceUtil'; import { ApiEnum } from '../enums/ApiEnum'; import { CampaignTypeEnum } from '../enums/CampaignTypeEnum'; import { ServiceContainer } from '../services/ServiceContainer'; import { SDKMetaUtil } from './SDKMetaUtil'; /** * Constructs the settings path with API key and account ID. * @param {string} sdkKey - The API key. * @param {any} accountId - The account identifier. * @returns {Record} - The settings path including API key, random number, and account ID. */ export function getSettingsPath(sdkKey: string, accountId: string | number): Record { const path: Record = { 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} - The tracking path for the event. */ export function getTrackEventPath(event: string, accountId: string, userId: string): Record { const path: Record = { 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: SDKMetaUtil.getInstance().getSdkName(), // SDK name constant 'sdk-v': SDKMetaUtil.getInstance().getVersion(), // 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 Wingify servers. * @param {SettingsService} settingsService - The settings service instance. * @param {String} eventName * @param {String} visitorUserAgent - The visitor user agent. * @param {String} ipAddress - The visitor IP address. * @param {Boolean} isUsageStatsEvent - Whether the event is a usage stats event. * @param {Number} usageStatsAccountId - The usage stats account ID. * @returns {Record} - The properties for the event. */ export function getEventsBaseProperties( settingsService: SettingsService, eventName: string, visitorUserAgent: string = '', ipAddress: string = '', isUsageStatsEvent: boolean = false, usageStatsAccountId: number = null, ): Record { const properties = Object.assign({ en: eventName, a: settingsService.accountId.toString(), eTime: getCurrentUnixTimestampInMillis(), random: getRandomNumber(), p: 'FS', visitor_ua: visitorUserAgent, visitor_ip: ipAddress, sn: SDKMetaUtil.getInstance().getSdkName(), sv: SDKMetaUtil.getInstance().getVersion(), }); if (!isUsageStatsEvent) { // set env key for standard sdk events properties.env = settingsService.sdkKey; } else { // set account id for internal usage stats event properties.a = usageStatsAccountId; } // if browser environment, then add the user agent and ip address to the properties if (typeof window !== 'undefined') { // use typeof check so referencing `window` stays safe in non-browser environments // add the dh (default header) to the properties // true means - ignore custom ua and ip address and fetch it from http request properties.dh = 1; } return properties; } /** * Builds generic payload required by all the different tracking calls. * @param {SettingsService} settingsService - The settings service instance. * @param {String} userId user id * @param {String} eventName event name * @param {String} visitorUserAgent - The visitor user agent. * @param {String} ipAddress - The visitor IP address. * @param {Boolean} isUsageStatsEvent - Whether the event is a usage stats event. * @param {Number} usageStatsAccountId - The usage stats account ID. * @param {Boolean} shouldGenerateUUID - Whether to generate a UUID. * @returns {Record} - The payload for the event. */ export function _getEventBasePayload( settingsService: SettingsService, userId: string | number, eventName: string, visitorUserAgent = '', ipAddress = '', isUsageStatsEvent = false, usageStatsAccountId: number = null, shouldGenerateUUID = true, ): Record { let accountId = settingsService.accountId; if (isUsageStatsEvent) { // set account id for internal usage stats event accountId = usageStatsAccountId; } let uuid: string; if (shouldGenerateUUID) { uuid = getUUID(userId.toString(), accountId.toString()); } else { uuid = userId.toString(); } const props: { [key: string]: any; id?: string | number; variation?: string | number; isFirst?: number; isCustomEvent?: boolean; data?: Record; product?: string; } = { vwo_sdkName: SDKMetaUtil.getInstance().getSdkName(), vwo_sdkVersion: SDKMetaUtil.getInstance().getVersion(), }; if (!isUsageStatsEvent) { // set env key for standard sdk events props.vwo_envKey = settingsService.sdkKey; } const properties: Record = { d: { msgId: `${uuid}-${getCurrentUnixTimestampInMillis()}`, visId: uuid, sessionId: getCurrentUnixTimestamp(), visitor_ua: visitorUserAgent, visitor_ip: ipAddress, event: { props: props, name: eventName, time: getCurrentUnixTimestampInMillis(), }, }, }; if (!isUsageStatsEvent) { // set visitor props for standard sdk events properties.d.visitor = { props: { vwo_fs_environment: settingsService.sdkKey, }, }; } return properties; } /** * Builds payload to track the visitor. * @param {ServiceContainer} serviceContainer - The service container instance. * @param {String} eventName - The name of the event. * @param {Number} campaignId - The campaign ID. * @param {Number} variationId - The variation ID. * @param {ContextModel} context - The context model instance. * @returns {Record} - The payload for the event. */ export function getTrackUserPayloadData( serviceContainer: ServiceContainer, eventName: string, campaignId: number, variationId: number, context: ContextModel, ): Record { const userId = context.getId(); const visitorUserAgent = context.getUserAgent(); const ipAddress = context.getIpAddress(); const customVariables = context.getCustomVariables(); const postSegmentationVariables = context.getPostSegmentationVariables(); const properties = _getEventBasePayload( serviceContainer.getSettingsService(), userId, eventName, visitorUserAgent, ipAddress, ); if (context.getSessionId() !== 0) { properties.d.sessionId = context.getSessionId(); } // if uuid is present in the context, use it, otherwise generate a new one if (context.getUuid()) { properties.d.msgId = `${context.getUuid()}-${getCurrentUnixTimestampInMillis()}`; properties.d.visId = context.getUuid(); } properties.d.event.props.id = campaignId; properties.d.event.props.variation = variationId; properties.d.event.props.isFirst = 1; // Add post-segmentation variables if they exist in custom variables if ( postSegmentationVariables && postSegmentationVariables.length > 0 && customVariables && Object.keys(customVariables).length > 0 ) { for (const key of postSegmentationVariables) { if (customVariables[key]) { properties.d.visitor.props[key] = customVariables[key]; } } } // Add IP address as a standard attribute if available if (ipAddress) { properties.d.visitor.props.ip = ipAddress; } serviceContainer.getLogManager().debug( buildMessage(DebugLogMessagesEnum.IMPRESSION_FOR_TRACK_USER, { accountId: serviceContainer.getSettingsService().accountId.toString(), userId, campaignId, }), ); return properties; } /** * Constructs the payload data for tracking goals with custom event properties. * @param {ServiceContainer} serviceContainer - The service container instance. * @param {string} eventName - Name of the event. * @param {any} eventProperties - Custom properties for the event. * @param {ContextModel} context - The context model instance. * @returns {any} - The constructed payload data. */ export function getTrackGoalPayloadData( serviceContainer: ServiceContainer, eventName: string, eventProperties: Record, context: ContextModel, ): Record { const properties = _getEventBasePayload( serviceContainer.getSettingsService(), context.getId(), eventName, context.getUserAgent(), context.getIpAddress(), ); if (context.getSessionId() !== 0) { properties.d.sessionId = context.getSessionId(); } // if uuid is present in the context, use it, otherwise generate a new one if (context.getUuid()) { properties.d.msgId = `${context.getUuid()}-${getCurrentUnixTimestampInMillis()}`; properties.d.visId = context.getUuid(); } 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]; } } serviceContainer.getLogManager().debug( buildMessage(DebugLogMessagesEnum.IMPRESSION_FOR_TRACK_GOAL, { eventName, accountId: serviceContainer.getSettingsService().accountId.toString(), userId: context.getId(), }), ); return properties; } /** * Constructs the payload data for syncing multiple visitor attributes. * @param {ServiceContainer} serviceContainer - The service container instance. * @param {string} eventName - Event name. * @param {Record} attributes - Key-value map of attributes. * @param {ContextModel} context - The context model instance. * @returns {Record} - Payload object to be sent in the request. */ export function getAttributePayloadData( serviceContainer: ServiceContainer, eventName: string, attributes: Record, context: ContextModel, ): Record { const properties = _getEventBasePayload( serviceContainer.getSettingsService(), context.getId(), eventName, context.getUserAgent(), context.getIpAddress(), ); if (context.getSessionId() !== 0) { properties.d.sessionId = context.getSessionId(); } // if uuid is present in the context, use it, otherwise generate a new one if (context.getUuid()) { properties.d.msgId = `${context.getUuid()}-${getCurrentUnixTimestampInMillis()}`; properties.d.visId = context.getUuid(); } properties.d.event.props.isCustomEvent = true; // Mark as a custom event properties.d.event.props[Constants.FS_ENVIRONMENT_KEY] = serviceContainer.getSettingsService().sdkKey; // 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; } serviceContainer.getLogManager().debug( buildMessage(DebugLogMessagesEnum.IMPRESSION_FOR_SYNC_VISITOR_PROP, { eventName, accountId: serviceContainer.getSettingsService().accountId.toString(), userId: context.getId(), }), ); return properties; } /** * Sends a POST API request with the specified properties and payload. * @param {ServiceContainer} serviceContainer - The service container instance. * @param {any} properties - Properties for the request. * @param {any} payload - Payload for the request. * @param {string} userId - User ID. */ export async function sendPostApiRequest( serviceContainer: ServiceContainer, properties: any, payload: any, userId: string, eventProperties: any = {}, campaignInfo: any = {}, ): Promise { const retryConfig: IRetryConfig = serviceContainer.getNetworkManager().getRetryConfig(); const isGatewayServiceConfigured = Boolean(serviceContainer.getWingifyOptions()?.gatewayService); const headers: Record = {}; 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; const request: RequestModel = new RequestModel( serviceContainer.getSettingsService().getCollectionHostname(), HttpMethodEnum.POST, serviceContainer.getUpdatedEndpointWithCollectionPrefix(UrlEnum.EVENTS, isGatewayServiceConfigured), properties, payload, headers, serviceContainer.getSettingsService().protocol, serviceContainer.getSettingsService().port, retryConfig, ); request.setEventName(properties.en); request.setUuid(payload.d.visId); let apiName: string; let extraDataForMessage: string; if (properties.en === EventEnum.VARIATION_SHOWN) { apiName = ApiEnum.GET_FLAG; if ( campaignInfo.campaignType === CampaignTypeEnum.ROLLOUT || campaignInfo.campaignType === CampaignTypeEnum.PERSONALIZE ) { extraDataForMessage = `feature: ${campaignInfo.featureKey}, rule: ${campaignInfo.variationName}`; } else { extraDataForMessage = `feature: ${campaignInfo.featureKey}, rule: ${campaignInfo.campaignKey} and variation: ${campaignInfo.variationName}`; } request.setCampaignId(payload.d.event.props.id); } else if (properties.en != EventEnum.VARIATION_SHOWN) { if (properties.en === EventEnum.SYNC_VISITOR_PROP) { apiName = ApiEnum.SET_ATTRIBUTE; extraDataForMessage = apiName; } else if ( properties.en !== EventEnum.DEBUGGER_EVENT && properties.en !== EventEnum.LOG_EVENT && properties.en !== EventEnum.INIT_CALLED ) { apiName = ApiEnum.TRACK_EVENT; extraDataForMessage = `event: ${properties.en}`; } if (Object.keys(eventProperties).length > 0) { request.setEventProperties(eventProperties); } } await serviceContainer .getNetworkManager() .post(request) .then((response: ResponseModel) => { // if attempt is more than 0 if (response.getTotalAttempts() > 0) { const debugEventProps = createNetWorkAndRetryDebugEvent(response, payload, apiName, extraDataForMessage); debugEventProps.uuid = request.getUuid(); // send debug event sendDebugEventToWingify(serviceContainer, debugEventProps); } serviceContainer.getLogManager().info( buildMessage(InfoLogMessagesEnum.NETWORK_CALL_SUCCESS, { event: properties.en, endPoint: UrlEnum.EVENTS, accountId: serviceContainer.getSettingsService().accountId.toString(), userId: userId, uuid: payload.d.visId, }), ); }) .catch((err: ResponseModel) => { const debugEventProps = createNetWorkAndRetryDebugEvent(err, payload, apiName, extraDataForMessage); debugEventProps.uuid = request.getUuid(); sendDebugEventToWingify(serviceContainer, debugEventProps); serviceContainer.getLogManager().errorLog( 'NETWORK_CALL_FAILED', { method: HttpMethodEnum.POST, err: getFormattedErrorMessage(err), }, {}, false, ); }); } /** * Constructs the payload for a messaging event. * @param {SettingsService} settingsService - The settings service instance. * @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( settingsService: SettingsService, messageType: string, message: string, eventName: string, extraData: any = {}, ): Record { const userId = settingsService.accountId + '_' + settingsService.sdkKey; const properties = _getEventBasePayload(settingsService, userId, eventName); properties.d.event.props[Constants.FS_ENVIRONMENT_KEY] = settingsService.sdkKey; // Set environment key properties.d.event.props.product = Constants.PRODUCT_NAME; const data = { type: messageType, content: { title: message, dateTime: getCurrentUnixTimestampInMillis(), }, metaInfo: { ...extraData }, }; properties.d.event.props.data = data; return properties; } /** * Constructs the payload for init called event. * @param {SettingsService} settingsService - The settings service instance. * @param eventName - The name of the event. * @param settingsFetchTime - Time taken to fetch settings in milliseconds. * @param sdkInitTime - Time taken to initialize the SDK in milliseconds. * @returns The constructed payload with required fields. */ export function getSDKInitEventPayload( settingsService: SettingsService, eventName: string, settingsFetchTime?: number, sdkInitTime?: number, ): Record { const userId = settingsService.accountId + '_' + settingsService.sdkKey; const properties = _getEventBasePayload(settingsService, userId, eventName); // Set the required fields as specified properties.d.event.props[Constants.FS_ENVIRONMENT_KEY] = settingsService.sdkKey; properties.d.event.props.product = Constants.PRODUCT_NAME; const data = { isSDKInitialized: true, settingsFetchTime: settingsFetchTime, sdkInitTime: sdkInitTime, }; properties.d.event.props.data = data; return properties; } /** * Constructs the payload for sdk usage stats event. * @param {SettingsService} settingsService - The settings service instance. * @param eventName - The name of the event. * @param settingsFetchTime - Time taken to fetch settings in milliseconds. * @param sdkInitTime - Time taken to initialize the SDK in milliseconds. * @returns The constructed payload with required fields. */ export function getSDKUsageStatsEventPayload( settingsService: SettingsService, eventName: string, usageStatsAccountId: number, usageStatsUtil: UsageStatsUtil, ): Record { const userId = settingsService.accountId + '_' + settingsService.sdkKey; const properties = _getEventBasePayload(settingsService, userId, eventName, '', '', true, usageStatsAccountId); // Set the required fields as specified properties.d.event.props.product = Constants.PRODUCT_NAME; properties.d.event.props.vwoMeta = usageStatsUtil.getUsageStats(); return properties; } /** * Constructs the payload for debugger event. * @param {SettingsService} settingsService - The settings service instance. * @param eventProps - The properties for the event. * @returns The constructed payload. */ export function getDebuggerEventPayload( settingsService: SettingsService, eventProps: Record = {}, ): Record { let uuid: string; const accountId = settingsService.accountId.toString(); const sdkKey = settingsService.sdkKey; // generate uuid if not present if (!eventProps.uuid) { uuid = getUUID(accountId + '_' + sdkKey, accountId); eventProps.uuid = uuid; } else { uuid = eventProps.uuid; } // create standard event payload const properties = _getEventBasePayload(settingsService, uuid, EventEnum.DEBUGGER_EVENT, '', '', false, null, false); properties.d.event.props = {}; // add session id to the event props if not present if (eventProps.sId) { properties.d.sessionId = eventProps.sId; } else { eventProps.sId = properties.d.sessionId; } // add a safety check for apiName if (!eventProps.an) { eventProps.an = EventEnum.DEBUGGER_EVENT; } // add all debugger props inside vwoMeta properties.d.event.props.vwoMeta = { ...eventProps, a: settingsService.accountId.toString(), product: Constants.PRODUCT_NAME, sn: SDKMetaUtil.getInstance().getSdkName(), sv: SDKMetaUtil.getInstance().getVersion(), 'src-v': Constants.SDK_NAME + '-' + Constants.SDK_VERSION, eventId: getRandomUUID(settingsService.sdkKey), }; return properties; } /** * Sends an event to Wingify (generic event sender). * @param {NetworkManager} networkManager - The network manager instance. * @param {SettingsService} settingsService - The settings service instance. * @param properties - Query parameters for the request. * @param payload - The payload for the request. * @param eventName - The name of the event to send. * @returns A promise that resolves to the response from the server. */ export async function sendEvent( serviceContainer: ServiceContainer, properties: Record, payload: Record, eventName: string, ): Promise { // Create a new deferred object to manage promise resolution const deferredObject = new Deferred(); const isGatewayServiceConfigured = Boolean(serviceContainer.getWingifyOptions()?.gatewayService); const retryConfig: IRetryConfig = serviceContainer.getNetworkManager().getRetryConfig(); // disable retry for event (no retry for generic events) if (eventName === EventEnum.DEBUGGER_EVENT) retryConfig.shouldRetry = false; try { // Create a new request model instance with the provided parameters const request: RequestModel = new RequestModel( serviceContainer.getSettingsService().getCollectionHostname(), HttpMethodEnum.POST, serviceContainer.getUpdatedEndpointWithCollectionPrefix(UrlEnum.EVENTS, isGatewayServiceConfigured), properties, payload, null, serviceContainer.getSettingsService().protocol, serviceContainer.getSettingsService().port, retryConfig, ); request.setEventName(properties.en); // Perform the network POST request serviceContainer .getNetworkManager() .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; } } /** * Creates a network and retry debug event. * @param response The response model. * @param payload The payload for the request. * @param apiName The name of the API. * @param extraData Extra data for the message. * @param isBatchingDebugEvent Whether the debug event was triggered due to batching. * @returns The debug event properties. */ export function createNetWorkAndRetryDebugEvent( response: ResponseModel, payload: any, apiName: string, extraData: string, ) { try { // set category, if call got success then category is retry, otherwise network let category = DebuggerCategoryEnum.RETRY; let msg_t = Constants.NETWORK_CALL_SUCCESS_WITH_RETRIES; let msg = buildMessage(InfoLogMessagesEnum.NETWORK_CALL_SUCCESS_WITH_RETRIES, { extraData: extraData, attempts: response.getTotalAttempts(), err: getFormattedErrorMessage(response.getError()), }); let lt = LogLevelEnum.INFO.toString(); if (response.getStatusCode() !== 200) { category = DebuggerCategoryEnum.NETWORK; msg_t = Constants.NETWORK_CALL_FAILURE_AFTER_MAX_RETRIES; msg = buildMessage(ErrorLogMessagesEnum.NETWORK_CALL_FAILURE_AFTER_MAX_RETRIES, { extraData: extraData, attempts: response.getTotalAttempts(), err: getFormattedErrorMessage(response.getError()), }); lt = LogLevelEnum.ERROR.toString(); } const debugEventProps: Record = { cg: category, msg_t: msg_t, msg: msg, lt: lt, }; if (apiName) { debugEventProps.an = apiName; } if (payload?.d?.sessionId) { debugEventProps.sId = payload.d.sessionId; } else { debugEventProps.sId = getCurrentUnixTimestamp(); } return debugEventProps; } catch (err) { return { cg: DebuggerCategoryEnum.NETWORK, an: apiName, msg_t: 'NETWORK_CALL_FAILED', msg: buildMessage(ErrorLogMessagesEnum.NETWORK_CALL_FAILED, { method: extraData, err: getFormattedErrorMessage(err), }), lt: LogLevelEnum.ERROR.toString(), sId: getCurrentUnixTimestamp(), }; } } /** * Creates payload for holdout variation shown event. * Similar to getTrackUserPayloadData but specifically for holdouts. * @param {SettingsModel} settings - The settings model containing configuration. * @param {string} eventName - The event name. * @param {number} holdoutId - The holdout ID (used as campaignId). * @param {number} variationId - The variation ID (1 if IN holdout, 2 if NOT IN holdout). * @param {ContextModel} context - The user context model containing user-specific data. * @param {number} featureId - The feature ID. * @returns {Record} - The holdout payload data. */ export function createHoldoutPayload( serviceContainer, eventName: string, holdoutId: number, variationId: number, context: ContextModel, featureId: number, ): Record { const userId = context.getId(); const properties = _getEventBasePayload( serviceContainer.getSettingsService(), userId, eventName, context?.getUserAgent(), context?.getIpAddress(), ); properties.d.event.props.id = holdoutId; properties.d.event.props.variation = variationId; properties.d.event.props.isFirst = 1; properties.d.event.props.fId = featureId; return properties; }