vwo-fme-node-sdk
Version:
VWO Node/JavaScript SDK for Feature Management and Experimentation
226 lines (200 loc) • 8.3 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 { DecisionMaker } from '../packages/decision-maker';
import { LogManager } from '../packages/logger';
import { SegmentationManager } from '../packages/segmentation-evaluator';
import { Constants } from '../constants';
import { VariationModel } from '../models/campaign/VariationModel';
import { CampaignTypeEnum } from '../enums/CampaignTypeEnum';
import { DebugLogMessagesEnum, InfoLogMessagesEnum } from '../enums/log-messages';
import { CampaignModel } from '../models/campaign/CampaignModel';
import { ContextModel } from '../models/user/ContextModel';
import { isObject } from '../utils/DataTypeUtil';
import { buildMessage } from '../utils/LogMessageUtil';
interface ICampaignDecisionService {
isUserPartOfCampaign(userId: any, campaign: CampaignModel): boolean;
getVariation(variations: Array<VariationModel>, bucketValue: number): VariationModel;
checkInRange(variation: VariationModel, bucketValue: number): VariationModel;
bucketUserToVariation(userId: any, accountId: any, campaign: CampaignModel): VariationModel;
getPreSegmentationDecision(campaign: CampaignModel, context: ContextModel): Promise<any>;
getVariationAlloted(userId: any, accountId: any, campaign: CampaignModel): VariationModel;
}
export class CampaignDecisionService implements ICampaignDecisionService {
/**
* Calculate if this user should become part of the campaign or not
*
* @param {String} userId the unique ID assigned to a user
* @param {Object} campaign fot getting the value of traffic allotted to the campaign
*
* @return {Boolean} if User is a part of Campaign or not
*/
isUserPartOfCampaign(userId: any, campaign: CampaignModel): boolean {
// if (!ValidateUtil.isValidValue(userId) || !campaign) {
// return false;
// }
if (!campaign || !userId) {
return false;
}
// check if campaign is rollout or personalize
const isRolloutOrPersonalize =
campaign.getType() === CampaignTypeEnum.ROLLOUT || campaign.getType() === CampaignTypeEnum.PERSONALIZE;
// get salt
const salt = isRolloutOrPersonalize ? campaign.getVariations()[0].getSalt() : campaign.getSalt();
// get traffic allocation
const trafficAllocation = isRolloutOrPersonalize ? campaign.getVariations()[0].getWeight() : campaign.getTraffic();
// get bucket key
const bucketKey = salt ? `${salt}_${userId}` : `${campaign.getId()}_${userId}`;
// get bucket value for user
const valueAssignedToUser = new DecisionMaker().getBucketValueForUser(bucketKey);
// check if user is part of campaign
const isUserPart = valueAssignedToUser !== 0 && valueAssignedToUser <= trafficAllocation;
LogManager.Instance.info(
buildMessage(InfoLogMessagesEnum.USER_PART_OF_CAMPAIGN, {
userId,
notPart: isUserPart ? '' : 'not',
campaignKey:
campaign.getType() === CampaignTypeEnum.AB
? campaign.getKey()
: campaign.getName() + '_' + campaign.getRuleKey(),
}),
);
return isUserPart;
}
/**
* Returns the Variation by checking the Start and End Bucket Allocations of each Variation
*
* @param {Object} campaign which contains the variations
* @param {Number} bucketValue the bucket Value of the user
*
* @return {Object|null} variation data allotted to the user or null if not
*/
getVariation(variations: Array<VariationModel>, bucketValue: number): VariationModel {
for (let i = 0; i < variations.length; i++) {
const variation = variations[i];
if (bucketValue >= variation.getStartRangeVariation() && bucketValue <= variation.getEndRangeVariation()) {
return variation;
}
}
return null;
}
checkInRange(variation: VariationModel, bucketValue: number): VariationModel {
if (bucketValue >= variation.getStartRangeVariation() && bucketValue <= variation.getEndRangeVariation()) {
return variation;
}
}
/**
* Validates the User ID and generates Variation into which the User is bucketed in.
*
* @param {String} userId the unique ID assigned to User
* @param {Object} campaign the Campaign of which User is a part of
*
* @return {Object|null} variation data into which user is bucketed in or null if not
*/
bucketUserToVariation(userId: any, accountId: any, campaign: CampaignModel): VariationModel {
let multiplier;
if (!campaign || !userId) {
return null;
}
if (campaign.getTraffic()) {
multiplier = 1;
}
const percentTraffic = campaign.getTraffic();
// get salt
const salt = campaign.getSalt();
// get bucket key
const bucketKey = salt ? `${salt}_${accountId}_${userId}` : `${campaign.getId()}_${accountId}_${userId}`;
// get hash value
const hashValue = new DecisionMaker().generateHashValue(bucketKey);
const bucketValue = new DecisionMaker().generateBucketValue(hashValue, Constants.MAX_TRAFFIC_VALUE, multiplier);
LogManager.Instance.debug(
buildMessage(DebugLogMessagesEnum.USER_BUCKET_TO_VARIATION, {
userId,
campaignKey: campaign.getKey(),
percentTraffic,
bucketValue,
hashValue,
}),
);
return this.getVariation(campaign.getVariations(), bucketValue);
}
async getPreSegmentationDecision(campaign: CampaignModel, context: ContextModel): Promise<boolean> {
// validate segmentation
const campaignType = campaign.getType();
let segments = {};
if (campaignType === CampaignTypeEnum.ROLLOUT || campaignType === CampaignTypeEnum.PERSONALIZE) {
segments = campaign.getVariations()[0].getSegments();
} else if (campaignType === CampaignTypeEnum.AB) {
segments = campaign.getSegments();
}
if (isObject(segments) && !Object.keys(segments).length) {
LogManager.Instance.info(
buildMessage(InfoLogMessagesEnum.SEGMENTATION_SKIP, {
userId: context.getId(),
campaignKey:
campaign.getType() === CampaignTypeEnum.AB
? campaign.getKey()
: campaign.getName() + '_' + campaign.getRuleKey(),
}),
);
return true;
} else {
const preSegmentationResult = await SegmentationManager.Instance.validateSegmentation(
segments,
context.getCustomVariables(),
);
if (!preSegmentationResult) {
LogManager.Instance.info(
buildMessage(InfoLogMessagesEnum.SEGMENTATION_STATUS, {
userId: context.getId(),
campaignKey:
campaign.getType() === CampaignTypeEnum.AB
? campaign.getKey()
: campaign.getName() + '_' + campaign.getRuleKey(),
status: 'failed',
}),
);
return false;
}
LogManager.Instance.info(
buildMessage(InfoLogMessagesEnum.SEGMENTATION_STATUS, {
userId: context.getId(),
campaignKey:
campaign.getType() === CampaignTypeEnum.AB
? campaign.getKey()
: campaign.getName() + '_' + campaign.getRuleKey(),
status: 'passed',
}),
);
return true;
}
}
getVariationAlloted(userId: any, accountId: any, campaign: CampaignModel): VariationModel {
const isUserPart = this.isUserPartOfCampaign(userId, campaign);
if (campaign.getType() === CampaignTypeEnum.ROLLOUT || campaign.getType() === CampaignTypeEnum.PERSONALIZE) {
if (isUserPart) {
return campaign.getVariations()[0];
} else {
return null;
}
} else {
if (isUserPart) {
return this.bucketUserToVariation(userId, accountId, campaign);
} else {
return null;
}
}
}
}