Skip to content

Commit 9f5bb63

Browse files
Alda/bucketing (#54)
* Changed the bucketing ID terminology to bucketing key--this is more appropriate as this value is used for the key of the murmur function. Also,the bucketing ID keyword will be used for the user-assigned bucketing value. * Added the bucketingId parameter to the bucketer. * Get the bucketing ID from the attributes if one of the keys is the reserved bucketing ID. * Fixed the bucketing tests with the addition of the bucketing ID. * Included tests for bucket method in the bucketer class for bucketing ID. * Added additional bucketing ID tests for invalid experiment keys and grouped experiments. * Added bucketingID tests for getVariation. * Fixed a getVariation test. * Added bucketing ID tests for getVariation in the decision service. * Changed the bucketing ID reserved keyword name. * Added a test for bucketing ID in an impression event. * Disabled the log in the test. * Removed generated snippet file. * Fixed nits from PR. * Added bucketing id as an attribute payload with null id in events. * Adding local host and port for Charles debugging. Will remove later. * Disabled https verification for debugging. Will remove. * Revert "Disabled https verification for debugging. Will remove." This reverts commit eeeda2e. * Revert "Adding local host and port for Charles debugging. Will remove later." This reverts commit 08f52b1. * Added a test to make sure that the conversion event contains the bucketin ID if it is part of the attributes map.
1 parent fed569c commit 9f5bb63

File tree

7 files changed

+444
-74
lines changed

7 files changed

+444
-74
lines changed

src/Optimizely/Bucketer.php

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -64,42 +64,43 @@ public function __construct(LoggerInterface $logger)
6464
/**
6565
* Generate a hash value to be used in determining which variation the user will be put in.
6666
*
67-
* @param $bucketingId string ID to be used to bucket the user in the experiment.
67+
* @param $bucketingKey string Value used for the key of the murmur hash.
6868
*
6969
* @return integer Unsigned value denoting the hash value for the user.
7070
*/
71-
private function generateHashCode($bucketingId)
71+
private function generateHashCode($bucketingKey)
7272
{
73-
return murmurhash3_int($bucketingId, Bucketer::$HASH_SEED) & Bucketer::$UNSIGNED_MAX_32_BIT_VALUE;
73+
return murmurhash3_int($bucketingKey, Bucketer::$HASH_SEED) & Bucketer::$UNSIGNED_MAX_32_BIT_VALUE;
7474
}
7575

7676
/**
7777
* Generate an integer to be used in bucketing user to a particular variation.
7878
*
79-
* @param $bucketingId string ID to be used to bucket the user in the experiment.
79+
* @param $bucketingKey string Value used for the key of the murmur hash.
8080
*
8181
* @return integer Value in the closed range [0, 9999] denoting the bucket the user belongs to.
8282
*/
83-
protected function generateBucketValue($bucketingId)
83+
protected function generateBucketValue($bucketingKey)
8484
{
85-
$hashCode = $this->generateHashCode($bucketingId);
85+
$hashCode = $this->generateHashCode($bucketingKey);
8686
$ratio = $hashCode / Bucketer::$MAX_HASH_VALUE;
8787
return floor($ratio * Bucketer::$MAX_TRAFFIC_VALUE);
8888
}
8989

9090
/**
91+
* @param $bucketingId string A customer-assigned value used to create the key for the murmur hash.
9192
* @param $userId string ID for user.
9293
* @param $parentId mixed ID representing Experiment or Group.
9394
* @param $trafficAllocations array Traffic allocations for variation or experiment.
9495
*
9596
* @return string ID representing experiment or variation.
9697
*/
97-
private function findBucket($userId, $parentId, $trafficAllocations)
98+
private function findBucket($bucketingId, $userId, $parentId, $trafficAllocations)
9899
{
99-
// Generate bucketing ID based on combination of user ID and experiment ID or group ID.
100-
$bucketingId = $userId.$parentId;
101-
$bucketingNumber = $this->generateBucketValue($bucketingId);
102-
$this->_logger->log(Logger::DEBUG, sprintf('Assigned bucket %s to user "%s".', $bucketingNumber, $userId));
100+
// Generate the bucketing key based on combination of user ID and experiment ID or group ID.
101+
$bucketingKey = $bucketingId.$parentId;
102+
$bucketingNumber = $this->generateBucketValue($bucketingKey);
103+
$this->_logger->log(Logger::DEBUG, sprintf('Assigned bucket %s to user "%s" with bucketing ID "%s".', $bucketingNumber, $userId, $bucketingId));
103104

104105
forEach ($trafficAllocations as $trafficAllocation)
105106
{
@@ -117,11 +118,12 @@ private function findBucket($userId, $parentId, $trafficAllocations)
117118
*
118119
* @param $config ProjectConfig Configuration for the project.
119120
* @param $experiment Experiment Experiment in which user is to be bucketed.
121+
* @param $bucketingId string A customer-assigned value used to create the key for the murmur hash.
120122
* @param $userId string User identifier.
121123
*
122124
* @return Variation Variation which will be shown to the user.
123125
*/
124-
public function bucket(ProjectConfig $config, Experiment $experiment, $userId)
126+
public function bucket(ProjectConfig $config, Experiment $experiment, $bucketingId, $userId)
125127
{
126128
if (is_null($experiment->getKey())) {
127129
return new Variation();
@@ -135,7 +137,7 @@ public function bucket(ProjectConfig $config, Experiment $experiment, $userId)
135137
return new Variation();
136138
}
137139

138-
$userExperimentId = $this->findBucket($userId, $group->getId(), $group->getTrafficAllocation());
140+
$userExperimentId = $this->findBucket($bucketingId, $userId, $group->getId(), $group->getTrafficAllocation());
139141
if (empty($userExperimentId)) {
140142
$this->_logger->log(Logger::INFO, sprintf('User "%s" is in no experiment.', $userId));
141143
return new Variation();
@@ -157,7 +159,7 @@ public function bucket(ProjectConfig $config, Experiment $experiment, $userId)
157159
}
158160

159161
// Bucket user if not in whitelist and in group (if any).
160-
$variationId = $this->findBucket($userId, $experiment->getId(), $experiment->getTrafficAllocation());
162+
$variationId = $this->findBucket($bucketingId, $userId, $experiment->getId(), $experiment->getTrafficAllocation());
161163
if (!empty($variationId)) {
162164
$variation = $config->getVariationFromId($experiment->getKey(), $variationId);
163165
$this->_logger->log(Logger::INFO,

src/Optimizely/DecisionService/DecisionService.php

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
use Optimizely\UserProfile\UserProfileUtils;
3030
use Optimizely\Utils\Validator;
3131

32+
define("RESERVED_ATTRIBUTE_KEY_BUCKETING_ID", "Optimizely Bucketing ID");
33+
3234
/**
3335
* Optimizely's decision service that determines which variation of an experiment the user will be allocated to.
3436
*
@@ -79,14 +81,23 @@ public function __construct(LoggerInterface $logger, ProjectConfig $projectConfi
7981
/**
8082
* Determine which variation to show the user.
8183
*
82-
* @param $experiment Experiment Experiment to get the variation for.
83-
* @param $userId string User identifier.
84-
* @param $attributes array Attributes of the user.
84+
* @param $experiment Experiment Experiment to get the variation for.
85+
* @param $userId string User identifier.
86+
* @param $attributes array Attributes of the user.
8587
*
8688
* @return Variation Variation which the user is bucketed into.
8789
*/
8890
public function getVariation(Experiment $experiment, $userId, $attributes = null)
8991
{
92+
// by default, the bucketing ID should be the user ID
93+
$bucketingId = $userId;
94+
95+
// If the bucketing ID key is defined in attributes, then use that in place of the userID for the murmur hash key
96+
if (!empty($attributes[RESERVED_ATTRIBUTE_KEY_BUCKETING_ID])) {
97+
$bucketingId = $attributes[RESERVED_ATTRIBUTE_KEY_BUCKETING_ID];
98+
$this->_logger->log(Logger::DEBUG, sprintf('Setting the bucketing ID to "%s".', $bucketingId));
99+
}
100+
90101
if (!$experiment->isExperimentRunning()) {
91102
$this->_logger->log(Logger::INFO, sprintf('Experiment "%s" is not running.', $experiment->getKey()));
92103
return null;
@@ -96,7 +107,7 @@ public function getVariation(Experiment $experiment, $userId, $attributes = null
96107
$forcedVariation = $this->_projectConfig->getForcedVariation($experiment->getKey(), $userId);
97108
if (!is_null($forcedVariation)) {
98109
return $forcedVariation;
99-
}
110+
}
100111

101112
// check if the user has been whitelisted
102113
$variation = $this->getWhitelistedVariation($experiment, $userId);
@@ -125,7 +136,7 @@ public function getVariation(Experiment $experiment, $userId, $attributes = null
125136
return null;
126137
}
127138

128-
$variation = $this->_bucketer->bucket($this->_projectConfig, $experiment, $userId);
139+
$variation = $this->_bucketer->bucket($this->_projectConfig, $experiment, $bucketingId, $userId);
129140
if (!is_null($variation)) {
130141
$this->saveVariation($experiment, $variation, $userProfile);
131142
}

src/Optimizely/Event/Builder/EventBuilder.php

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
use Optimizely\ProjectConfig;
2424
use Optimizely\Utils\EventTagUtils;
2525

26+
define("RESERVED_ATTRIBUTE_KEY_BUCKETING_ID_EVENT_PARAM_KEY", "optimizely_bucketing_id");
27+
2628
class EventBuilder
2729
{
2830
/**
@@ -102,15 +104,25 @@ private function setCommonParams($config, $userId, $attributes)
102104

103105
forEach ($attributes as $attributeKey => $attributeValue) {
104106
if ($attributeValue) {
105-
$attributeEntity = $config->getAttribute($attributeKey);
106-
if (!is_null($attributeEntity->getKey())) {
107+
// check for reserved attributes
108+
if (strcmp($attributeKey , RESERVED_ATTRIBUTE_KEY_BUCKETING_ID) == 0) {
107109
array_push($this->_eventParams[USER_FEATURES], [
108-
'id' => $attributeEntity->getId(),
109-
'name' => $attributeKey,
110+
'name' => RESERVED_ATTRIBUTE_KEY_BUCKETING_ID_EVENT_PARAM_KEY,
110111
'type' => 'custom',
111112
'value' => $attributeValue,
112113
'shouldIndex' => true
113114
]);
115+
} else {
116+
$attributeEntity = $config->getAttribute($attributeKey);
117+
if (!is_null($attributeEntity->getKey())) {
118+
array_push($this->_eventParams[USER_FEATURES], [
119+
'id' => $attributeEntity->getId(),
120+
'name' => $attributeKey,
121+
'type' => 'custom',
122+
'value' => $attributeValue,
123+
'shouldIndex' => true
124+
]);
125+
}
114126
}
115127
}
116128
}

0 commit comments

Comments
 (0)