Skip to content

Commit

Permalink
refactor(bucketing): update variation bucketing based on feature-flag
Browse files Browse the repository at this point in the history
  • Loading branch information
rohitesh-wingify committed Jun 27, 2023
1 parent d2228f5 commit 7ad0938
Show file tree
Hide file tree
Showing 11 changed files with 648 additions and 31 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.50.0] - 2023-06-27

### Changed

- Update bucketing logic so that variation remains unchanged even after changing campaign traffic distribution. Note: it will not cater the case when user was not part of campaign earlier but started becoming part of campaign after changing the campaign traffic distribution and vice versa.

## [1.46.0] - 2023-06-06

### Added
Expand Down
30 changes: 24 additions & 6 deletions src/Core/Bucketer.php
Original file line number Diff line number Diff line change
Expand Up @@ -77,16 +77,32 @@ public static function getBucketVariationId($campaign, $variationName)
* @param bool $disableLogs optional: disable logs if True
* @return array|null
*/
public static function getBucket($userId, $campaign, $disableLogs = false)
public static function getBucket($userId, $campaign, $is_new_bucketing_enabled, $disableLogs = false)
{
// if bucketing to be done
list($bucketVal, $hashValue) = self::getBucketVal($userId, $campaign, $disableLogs);
// if bucketing to be done - check first if user is part of campaign
list($bucketVal, $hashValue) = self::getBucketVal($userId, $campaign, $is_new_bucketing_enabled, $disableLogs);
$isUserPart = self::isUserPartofCampaign($bucketVal, $campaign['percentTraffic']);
LoggerService::log(Logger::INFO, 'USER_CAMPAIGN_ELIGIBILITY', ['{userId}' => $userId, '{status}' => $isUserPart ? 'eligible' : 'not eligible', '{campaignKey}' => $campaign['key']], self::CLASSNAME, $disableLogs);
if (!$isUserPart) {
return null;
}
$multiplier = self::getMultiplier($campaign['percentTraffic'], $disableLogs);

// based on bucketing algo flag, determine bucket value
if(!$is_new_bucketing_enabled || (isset($campaign["isOB"]) && $campaign["isOB"])){
// old algo
$multiplier = self::getMultiplier($campaign['percentTraffic'], $disableLogs);
list($bucketVal, $hashValue) = self::getBucketVal($userId, $campaign, $is_new_bucketing_enabled, $disableLogs);

// log for type of algo
LoggerService::log(Logger::INFO, 'Using Old Algo!');
} else {
// new algo
$multiplier = 1;
list($bucketVal, $hashValue) = self::getBucketVal($userId, null, $is_new_bucketing_enabled, $disableLogs);

// log for type of algo
LoggerService::log(Logger::INFO, 'Using New Algo!');
}

$rangeForVariations = self::getRangeForVariations($bucketVal, $multiplier);

Expand Down Expand Up @@ -142,11 +158,13 @@ public static function getCampaignUsingRange($rangeForCampaigns, $campaigns)
* Copyright 2016-2019, Optimizely, used under Apache 2.0 License.
* Source - https://github.com/optimizely/php-sdk/blob/master/src/Optimizely/Bucketer.php
*/
public static function getBucketVal($userId, $campaign = [], $disableLogs = false)
public static function getBucketVal($userId, $campaign = [], $is_new_bucketing_enabled, $disableLogs = false)
{
if (isset($campaign["isBucketingSeedEnabled"]) && $campaign["isBucketingSeedEnabled"]) {
# if (isset($campaign["isBucketingSeedEnabled"]) && $campaign["isBucketingSeedEnabled"]) {
if ($campaign!=null && ($is_new_bucketing_enabled || (isset($campaign["isBucketingSeedEnabled"]) && $campaign["isBucketingSeedEnabled"]))) {
$userId = $campaign["id"] . '_' . $userId;
}

$code = self::getmurmurHash_Int($userId);
$range = $code / self::$MAX_VALUE;
if ($range < 0) {
Expand Down
50 changes: 39 additions & 11 deletions src/Core/VariationDecider.php
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,13 @@ public function fetchVariationData($userStorageObj, $campaign, $userId, $options
$decision['isUserWhitelisted'] = false;
$decision['fromUserStorageService'] = false;

# get new bucketing enabled flag from settings
if ($this->settings!=null && isset($this->settings["isNB"]) && $this->settings["isNB"]) {
$is_new_bucketing_enabled = true;
} else {
$is_new_bucketing_enabled = false;
}

// VWO generated UUID based on passed UserId and Account ID
if (isset($this->accountId)) {
$decision['vwoUserId'] = UuidUtil::get($userId, $this->accountId);
Expand All @@ -125,7 +132,7 @@ public function fetchVariationData($userStorageObj, $campaign, $userId, $options
}

//check for whitelisting if applied and get Variation Info
$bucketInfo = CampaignUtil::findVariationFromWhiteListing($campaign, $userId, $options);
$bucketInfo = CampaignUtil::findVariationFromWhiteListing($campaign, $userId, $options, $is_new_bucketing_enabled);
// do murmur operations and get Variation for the userId
if ($bucketInfo == null) {
if (isset($campaign['isAlwaysCheckSegment'])) {
Expand All @@ -139,7 +146,7 @@ public function fetchVariationData($userStorageObj, $campaign, $userId, $options
}

$isPresegmentation = ValidationsUtil::checkPreSegmentation($campaign, $userId, $options);
$isPresegmentationAndTrafficPassed = $isPresegmentation && self::isUserPartOfCampaign($userId, $campaign['percentTraffic']);
$isPresegmentationAndTrafficPassed = $isPresegmentation && self::isUserPartOfCampaign($userId, $campaign['percentTraffic'], $campaign, $is_new_bucketing_enabled);
if ($isPresegmentationAndTrafficPassed && $isCampaignPartOfGroup) {
$groupCampaigns = CampaignUtil::getGroupCampaigns($this->settings, $groupId);

Expand All @@ -161,7 +168,7 @@ public function fetchVariationData($userStorageObj, $campaign, $userId, $options
return null;
}

$eligibleCampaigns = self::getEligibleCampaigns($userId, $groupCampaigns, $campaign, $options);
$eligibleCampaigns = self::getEligibleCampaigns($userId, $groupCampaigns, $campaign, $options, $is_new_bucketing_enabled);
$megAlgoNumber = isset($this->settings["groups"][$groupId]["et"]) ? $this->settings["groups"][$groupId]["et"] : self::RandomAlgo ;

$nonEligibleCampaignsKey = self::getNonEligibleCampaignsKey($eligibleCampaigns, $groupCampaigns);
Expand Down Expand Up @@ -201,7 +208,7 @@ public function fetchVariationData($userStorageObj, $campaign, $userId, $options
self::CLASSNAME
);
if ($winnerCampaign && $winnerCampaign["id"] == $campaign["id"]) {
$bucketInfo = Bucketer::getBucket($userId, $campaign);
$bucketInfo = Bucketer::getBucket($userId, $campaign, $is_new_bucketing_enabled);
if ($bucketInfo == null) {
return $bucketInfo;
} else {
Expand Down Expand Up @@ -396,11 +403,11 @@ public function userStorageSet($userStorageObj, $userId, $campaignKey, $variatio
* @param array $options contains variables for segmentation
* @return array eligible campaigns from which winner campaign is to be selected
*/
private static function getEligibleCampaigns($userId, $groupCampaigns, $calledCampaign, $options)
private static function getEligibleCampaigns($userId, $groupCampaigns, $calledCampaign, $options, $is_new_bucketing_enabled)
{
$eligibleCampaigns = [];
foreach ($groupCampaigns as $campaign) {
if ($calledCampaign["id"] == $campaign["id"] || ValidationsUtil::checkPreSegmentation($campaign, $userId, $options, true) && self::isUserPartOfCampaign($userId, $campaign['percentTraffic'])) {
if ($calledCampaign["id"] == $campaign["id"] || ValidationsUtil::checkPreSegmentation($campaign, $userId, $options, true) && self::isUserPartOfCampaign($userId, $campaign['percentTraffic'], $campaign, $is_new_bucketing_enabled)) {
$eligibleCampaigns[] = $campaign;
}
}
Expand All @@ -414,9 +421,9 @@ private static function getEligibleCampaigns($userId, $groupCampaigns, $calledCa
* @param int|float $percentTraffic traffic for a campaign in which user is participating
* @return bool
*/
private static function isUserPartOfCampaign($userId, $percentTraffic)
private static function isUserPartOfCampaign($userId, $percentTraffic, $campaign, $is_new_bucketing_enabled)
{
list($bucketVal, $hashValue) = Bucketer::getBucketVal($userId, [], true);
list($bucketVal, $hashValue) = Bucketer::getBucketVal($userId, $campaign, $is_new_bucketing_enabled, true);
return Bucketer::isUserPartofCampaign($bucketVal, $percentTraffic);
}

Expand All @@ -429,6 +436,13 @@ private static function isUserPartOfCampaign($userId, $percentTraffic)
*/
private static function findWinnerCampaign($userId, $eligibleCampaigns, $megAlgoNumber, $groupId, $settingsFile)
{
# get new bucketing enabled flag from settings
if ($settingsFile!=null && isset($settingsFile["isNB"]) && $settingsFile["isNB"]) {
$is_new_bucketing_enabled = true;
} else {
$is_new_bucketing_enabled = false;
}

if (count($eligibleCampaigns) == 1) {
return $eligibleCampaigns[0];
} else {
Expand All @@ -438,7 +452,7 @@ private static function findWinnerCampaign($userId, $eligibleCampaigns, $megAlgo
//Allocate new range for campaigns
$eligibleCampaigns = Bucketer::addRangesToCampaigns($eligibleCampaigns);
//Now retrieve the campaign from the modified_campaign_for_whitelisting
list($bucketVal, $hashValue) = Bucketer::getBucketVal($userId, [], true);
list($bucketVal, $hashValue) = Bucketer::getBucketVal($userId, [], false, true);
return Bucketer::getCampaignUsingRange($bucketVal, $eligibleCampaigns);
} else {
$winnerCampaign = null;
Expand Down Expand Up @@ -545,9 +559,16 @@ private static function getNonEligibleCampaignsKey($eligibleCampaigns, $groupCam
*/
private function checkWhitelistingOrStorageForGroupedCampaigns($userStorageObj, $userId, $calledCampaign, $groupCampaigns, $groupName, $options)
{
# get new bucketing enabled flag from settings
if ($this->settings!=null && isset($this->settings["isNB"]) && $this->settings["isNB"]) {
$is_new_bucketing_enabled = true;
} else {
$is_new_bucketing_enabled = false;
}

foreach ($groupCampaigns as $campaign) {
if ($calledCampaign["id"] != $campaign["id"]) {
$targetedVariation = CampaignUtil::findVariationFromWhiteListing($campaign, $userId, $options, true);
$targetedVariation = CampaignUtil::findVariationFromWhiteListing($campaign, $userId, $options, $is_new_bucketing_enabled, true);
if ($targetedVariation) {
LoggerService::log(
Logger::INFO,
Expand Down Expand Up @@ -651,7 +672,14 @@ private function getVariationIfPreSegmentationApplied($isPreSegmentation, $campa
return $bucketInfo;
}

$bucketInfo = Bucketer::getBucket($userId, $campaign);
# get new bucketing enabled flag from settings
if ($this->settings!=null && isset($this->settings["isNB"]) && $this->settings["isNB"]) {
$is_new_bucketing_enabled = true;
} else {
$is_new_bucketing_enabled = false;
}

$bucketInfo = Bucketer::getBucket($userId, $campaign, $is_new_bucketing_enabled);
LoggerService::log(
Logger::INFO,
'USER_VARIATION_ALLOCATION_STATUS',
Expand Down
12 changes: 6 additions & 6 deletions src/Utils/Campaign.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,12 @@ public static function makeRanges($settings = [])
* @param bool $disableLogs disable logs if True
* @return array|null
*/
public static function findVariationFromWhiteListing($campaign, $userId, $options, $disableLogs = false)
public static function findVariationFromWhiteListing($campaign, $userId, $options, $is_new_bucketing_enabled, $disableLogs = false)
{
$bucketInfo = null;
if (isset($campaign['isForcedVariationEnabled']) && $campaign['isForcedVariationEnabled'] == true) {
$variationTargetingVariables = CommonUtil::getValueFromOptions($options, 'variationTargetingVariables');
$bucketInfo = self::getForcedBucket($campaign, $userId, $variationTargetingVariables, $disableLogs);
$bucketInfo = self::getForcedBucket($campaign, $userId, $variationTargetingVariables, $is_new_bucketing_enabled, $disableLogs);
$status = $bucketInfo != null ? 'satisfy' : "didn't satisfy";

LoggerService::log(
Expand Down Expand Up @@ -103,7 +103,7 @@ public static function findVariationFromWhiteListing($campaign, $userId, $option
* @param bool $disableLogs disable logs if True
* @return array|null
*/
private static function getForcedBucket($campaign, $userId, $variationTargetingVariables, $disableLogs = false)
private static function getForcedBucket($campaign, $userId, $variationTargetingVariables, $is_new_bucketing_enabled, $disableLogs = false)
{
if (isset($campaign["isUserListEnabled"]) && $campaign["isUserListEnabled"]) {
$variationTargetingVariables['_vwoUserId'] = UuidUtil::get($userId, AccountUtil::instance()->getAccountId(), true);
Expand Down Expand Up @@ -169,7 +169,7 @@ private static function getForcedBucket($campaign, $userId, $variationTargetingV
if ($totalValidVariations == 1) {
return $validVariations[0];
} elseif ($totalValidVariations > 1) {
return self::evaluateBestVariation($validVariations, $totalVariationTraffic, $userId, $campaign, $disableLogs);
return self::evaluateBestVariation($validVariations, $totalVariationTraffic, $userId, $campaign, $is_new_bucketing_enabled, $disableLogs);
}
return null;
}
Expand All @@ -182,13 +182,13 @@ private static function getForcedBucket($campaign, $userId, $variationTargetingV
* @param bool $disableLogs
* @return null| array of variation
*/
private static function evaluateBestVariation($validVariations, $totalVariationTraffic, $userId, $campaign, $disableLogs = false)
private static function evaluateBestVariation($validVariations, $totalVariationTraffic, $userId, $campaign, $is_new_bucketing_enabled, $disableLogs = false)
{
//scale and assign ranges to the variations
$validVariations = self::scaleVariations($validVariations, $totalVariationTraffic);
$validVariations = Bucketer::addRangesToVariations($validVariations, $campaign['key']);
//find murmur
list($bucketVal, $hashValue) = Bucketer::getBucketVal($userId, $campaign, $disableLogs);
list($bucketVal, $hashValue) = Bucketer::getBucketVal($userId, $campaign, $is_new_bucketing_enabled, $disableLogs);
//get range according to murmur
$rangeForVariation = Bucketer::getRangeForVariations($bucketVal);
//get variation
Expand Down
2 changes: 1 addition & 1 deletion src/Utils/ImpressionBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class ImpressionBuilder
/**
* sdk version for api hit
*/
const SDK_VERSION = '1.46.0';
const SDK_VERSION = '1.50.0';
/**
* sdk langauge for api hit
*/
Expand Down
Loading

0 comments on commit 7ad0938

Please sign in to comment.