vwo-fme-node-sdk
Version:
VWO Node/JavaScript SDK for Feature Management and Experimentation
316 lines (294 loc) • 12.9 kB
text/typescript
/**
* 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 { Constants } from '../constants';
import { CampaignTypeEnum } from '../enums/CampaignTypeEnum';
import { InfoLogMessagesEnum } from '../enums/log-messages';
import { CampaignModel } from '../models/campaign/CampaignModel';
import { VariationModel } from '../models/campaign/VariationModel';
import { SettingsModel } from '../models/settings/SettingsModel';
import { LogManager } from '../packages/logger';
import { buildMessage } from './LogMessageUtil';
/**
* Sets the variation allocation for a given campaign based on its type.
* If the campaign type is ROLLOUT or PERSONALIZE, it handles the campaign using `_handleRolloutCampaign`.
* Otherwise, it assigns range values to each variation in the campaign.
* @param {CampaignModel} campaign - The campaign for which to set the variation allocation.
*/
export function setVariationAllocation(campaign: CampaignModel): void {
// Check if the campaign type is ROLLOUT or PERSONALIZE
if (campaign.getType() === CampaignTypeEnum.ROLLOUT || campaign.getType() === CampaignTypeEnum.PERSONALIZE) {
_handleRolloutCampaign(campaign);
} else {
let currentAllocation = 0;
// Iterate over each variation in the campaign
campaign.getVariations().forEach((variation) => {
// Assign range values to the variation and update the current allocation
const stepFactor = assignRangeValues(variation, currentAllocation);
currentAllocation += stepFactor;
// Log the range allocation for debugging
LogManager.Instance.info(
buildMessage(InfoLogMessagesEnum.VARIATION_RANGE_ALLOCATION, {
variationKey: variation.getKey(),
campaignKey: campaign.getKey(),
variationWeight: variation.getWeight(),
startRange: variation.getStartRangeVariation(),
endRange: variation.getEndRangeVariation(),
}),
);
});
}
}
/**
* Assigns start and end range values to a variation based on its weight.
* @param {VariationModel} data - The variation model to assign range values.
* @param {number} currentAllocation - The current allocation value before this variation.
* @returns {number} The step factor calculated from the variation's weight.
*/
export function assignRangeValues(data: VariationModel, currentAllocation: number) {
// Calculate the bucket range based on the variation's weight
const stepFactor: number = _getVariationBucketRange(data.getWeight());
// Set the start and end range of the variation
if (stepFactor) {
data.setStartRange(currentAllocation + 1);
data.setEndRange(currentAllocation + stepFactor);
} else {
data.setStartRange(-1);
data.setEndRange(-1);
}
return stepFactor;
}
/**
* Scales the weights of variations to sum up to 100%.
* @param {any[]} variations - The list of variations to scale.
*/
export function scaleVariationWeights(variations: any) {
// Calculate the total weight of all variations
const totalWeight = variations.reduce((acc, variation) => {
return acc + variation.weight;
}, 0);
// If total weight is zero, assign equal weight to each variation
if (!totalWeight) {
const equalWeight = 100 / variations.length;
variations.forEach((variation) => (variation.weight = equalWeight));
} else {
// Scale each variation's weight to make the total 100%
variations.forEach((variation) => (variation.weight = (variation.weight / totalWeight) * 100));
}
}
/**
* Generates a bucketing seed based on user ID, campaign, and optional group ID.
* @param {string} userId - The user ID.
* @param {any} campaign - The campaign object.
* @param {string} [groupId] - The optional group ID.
* @returns {string} The bucketing seed.
*/
export function getBucketingSeed(userId: string, campaign: CampaignModel, groupId: number) {
// Return a seed combining group ID and user ID if group ID is provided
if (groupId) {
return `${groupId}_${userId}`;
}
const isRolloutOrPersonalize =
campaign.getType() === CampaignTypeEnum.ROLLOUT || campaign.getType() === CampaignTypeEnum.PERSONALIZE;
// get salt
const salt = isRolloutOrPersonalize ? campaign.getVariations()[0].getSalt() : campaign.getSalt();
// get bucket key
const bucketKey = salt ? `${salt}_${userId}` : `${campaign.getId()}_${userId}`;
// Return a seed combining campaign ID and user ID otherwise
return bucketKey;
}
/**
* Retrieves a variation by its ID within a specific campaign identified by its key.
* @param {SettingsModel} settings - The settings model containing all campaigns.
* @param {string} campaignKey - The key of the campaign.
* @param {string} variationId - The ID of the variation to retrieve.
* @returns {VariationModel | null} The found variation model or null if not found.
*/
export function getVariationFromCampaignKey(settings: SettingsModel, campaignKey: string, variationId: number) {
// Find the campaign by its key
const campaign: CampaignModel = settings.getCampaigns().find((campaign: CampaignModel) => {
return campaign.getKey() === campaignKey;
});
if (campaign) {
// Find the variation by its ID within the found campaign
const variation: VariationModel = campaign.getVariations().find((variation: VariationModel) => {
return variation.getId() === variationId;
});
if (variation) {
// Return a new instance of VariationModel based on the found variation
return new VariationModel().modelFromDictionary(variation);
}
}
return null;
}
/**
* Sets the allocation ranges for a list of campaigns.
* @param {CampaignModel[]} campaigns - The list of campaigns to set allocations for.
*/
export function setCampaignAllocation(campaigns: any[]) {
let stepFactor = 0;
for (let i = 0, currentAllocation = 0; i < campaigns.length; i++) {
const campaign = campaigns[i];
// Assign range values to each campaign and update the current allocation
stepFactor = assignRangeValuesMEG(campaign, currentAllocation);
currentAllocation += stepFactor;
}
}
/**
* Determines if a campaign is part of a group.
* @param {SettingsModel} settings - The settings model containing group associations.
* @param {string} campaignId - The ID of the campaign to check.
* @param {any} [variationId=null] - The optional variation ID.
* @returns {Object} An object containing the group ID and name if the campaign is part of a group, otherwise an empty object.
*/
export function getGroupDetailsIfCampaignPartOfIt(settings: SettingsModel, campaignId: any, variationId: any = null) {
/**
* If variationId is null, that means that campaign is testing campaign
* If variationId is not null, that means that campaign is personalization campaign and we need to append variationId to campaignId using _
* then check if the current campaign is part of any group
*/
let campaignToCheck = campaignId.toString();
// check if variationId is not null
if (variationId !== null) {
// if variationId is not null, then append it to the campaignId like campaignId_variationId
campaignToCheck = `${campaignId}_${variationId}`.toString();
}
if (
settings.getCampaignGroups() &&
Object.prototype.hasOwnProperty.call(settings.getCampaignGroups(), campaignToCheck)
) {
return {
groupId: settings.getCampaignGroups()[campaignToCheck],
groupName: settings.getGroups()[settings.getCampaignGroups()[campaignToCheck]].name,
};
}
return {};
}
/**
* Retrieves campaigns by a specific group ID.
* @param {SettingsModel} settings - The settings model containing all groups.
* @param {any} groupId - The ID of the group.
* @returns {Array} An array of campaigns associated with the specified group ID.
*/
export function getCampaignsByGroupId(settings: SettingsModel, groupId: number) {
const group = settings.getGroups()[groupId];
if (group) {
return group.campaigns; // Return the campaigns associated with the group
} else {
return []; // Return an empty array if the group ID is not found
}
}
/**
* Retrieves feature keys from a list of campaign IDs.
* @param {SettingsModel} settings - The settings model containing all features.
* @param {any} campaignIdWithVariation - An array of campaign IDs and variation IDs in format campaignId_variationId.
* @returns {Array} An array of feature keys associated with the provided campaign IDs.
*/
export function getFeatureKeysFromCampaignIds(settings: SettingsModel, campaignIdWithVariation: any) {
const featureKeys = [];
for (const campaign of campaignIdWithVariation) {
// split key with _ to separate campaignId and variationId
const [campaignId, variationId] = campaign.split('_').map(Number);
settings.getFeatures().forEach((feature) => {
// check if feature already exists in the featureKeys array
if (featureKeys.indexOf(feature.getKey()) !== -1) {
return;
}
feature.getRules().forEach((rule) => {
if (rule.getCampaignId() === campaignId) {
// Check if variationId is provided and matches the rule's variationId
if (variationId !== undefined && variationId !== null) {
// Add feature key if variationId matches
if (rule.getVariationId() === variationId) {
featureKeys.push(feature.getKey());
}
} else {
// Add feature key if no variationId is provided
featureKeys.push(feature.getKey());
}
}
});
});
}
return featureKeys;
}
/**
* Retrieves campaign IDs from a specific feature key.
* @param {SettingsModel} settings - The settings model containing all features.
* @param {string} featureKey - The key of the feature.
* @returns {Array} An array of campaign IDs associated with the specified feature key.
*/
export function getCampaignIdsFromFeatureKey(settings: SettingsModel, featureKey: string) {
const campaignIds = [];
settings.getFeatures().forEach((feature) => {
if (feature.getKey() === featureKey) {
feature.getRules().forEach((rule) => {
campaignIds.push(rule.getCampaignId()); // Add campaign ID if feature key matches
});
}
});
return campaignIds;
}
/**
* Assigns range values to a campaign based on its weight.
* @param {any} data - The campaign data containing weight.
* @param {number} currentAllocation - The current allocation value before this campaign.
* @returns {number} The step factor calculated from the campaign's weight.
*/
export function assignRangeValuesMEG(data: any, currentAllocation: number) {
const stepFactor: number = _getVariationBucketRange(data.weight);
if (stepFactor) {
data.startRangeVariation = currentAllocation + 1; // Set the start range
data.endRangeVariation = currentAllocation + stepFactor; // Set the end range
} else {
data.startRangeVariation = -1; // Set invalid range if step factor is zero
data.endRangeVariation = -1;
}
return stepFactor;
}
/**
* Calculates the bucket range for a variation based on its weight.
* @param {number} variationWeight - The weight of the variation.
* @returns {number} The calculated bucket range.
*/
function _getVariationBucketRange(variationWeight: number) {
if (!variationWeight || variationWeight === 0) {
return 0; // Return zero if weight is invalid or zero
}
const startRange = Math.ceil(variationWeight * 100);
return Math.min(startRange, Constants.MAX_TRAFFIC_VALUE); // Ensure the range does not exceed the max traffic value
}
/**
* Handles the rollout campaign by setting start and end ranges for all variations.
* @param {CampaignModel} campaign - The campaign to handle.
*/
function _handleRolloutCampaign(campaign: CampaignModel): void {
// Set start and end ranges for all variations in the campaign
for (let i = 0; i < campaign.getVariations().length; i++) {
const variation = campaign.getVariations()[i];
const endRange = campaign.getVariations()[i].getWeight() * 100;
variation.setStartRange(1);
variation.setEndRange(endRange);
LogManager.Instance.info(
buildMessage(InfoLogMessagesEnum.VARIATION_RANGE_ALLOCATION, {
variationKey: variation.getKey(),
campaignKey: campaign.getKey(),
variationWeight: variation.getWeight(),
startRange: 1,
endRange,
}),
);
}
}