Skip to content

Commit 1253af6

Browse files
committed
Squashed commit of the following:
commit 75224a4 Author: Owais <oakbani@folio3.com> Date: Thu Oct 26 18:14:31 2017 +0500 :memo: Final changes commit 75fa44d Author: Owais <oakbani@folio3.com> Date: Thu Oct 26 16:29:35 2017 +0500 # Conflicts: # tests/ProjectConfigTest.php # tests/UtilsTests/ValidatorTest.php commit a443056 Author: Owais <oakbani@folio3.com> Date: Thu Oct 26 12:21:16 2017 +0500 Partial fixes commit e03926b Author: Owais <oakbani@folio3.com> Date: Tue Oct 24 17:07:39 2017 +0500 Squashed commit of the following: commit 24569b8 Author: Owais <oakbani@folio3.com> Date: Fri Oct 20 18:18:55 2017 +0500 :pen: Fixed Code Coverage commit a6fa1e6 Author: Owais <oakbani@folio3.com> Date: Tue Oct 24 16:46:44 2017 +0500 :pen2: All Unit tests completed commit 72173d0 Author: Owais <oakbani@folio3.com> Date: Mon Oct 23 20:36:21 2017 +0500 :pen: 90% Unit tests done commit 9051dda Author: Owais <oakbani@folio3.com> Date: Mon Oct 23 18:41:04 2017 +0500 :pen: Unit tests done for feature experiment commit 8a5722d Author: Owais <oakbani@folio3.com> Date: Mon Oct 23 13:13:53 2017 +0500 :pen: Implementation First pass done commit 92dd219 Author: Owais <oakbani@folio3.com> Date: Fri Oct 20 17:32:04 2017 +0500 :pen: Feature Flag bucketing to Experiment done :memo: Todo: Rollout Logic commit ad15a2c Author: Owais <oakbani@folio3.com> Date: Fri Oct 20 12:52:47 2017 +0500 :pen: indentation commit dc41bf9 Author: Owais <oakbani@folio3.com> Date: Thu Oct 19 17:36:46 2017 +0500 :pencil2: Added Logger to Validator :pencil2: Added method defs commit 547b7ad Author: Owais <oakbani@folio3.com> Date: Thu Oct 19 17:11:22 2017 +0500 Feature Flags models and parsing from new v4 file
1 parent e60d1c5 commit 1253af6

File tree

16 files changed

+2054
-323
lines changed

16 files changed

+2054
-323
lines changed

src/Optimizely/DecisionService/DecisionService.php

Lines changed: 171 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
use Optimizely\UserProfile\UserProfile;
2929
use Optimizely\UserProfile\UserProfileUtils;
3030
use Optimizely\Utils\Validator;
31+
use Optimizely\Entity\FeatureFlag;
32+
use Optimizely\Entity\Rollout;
3133

3234
// This value was decided between App Backend, Audience, and Oasis teams, but may possibly change.
3335
// We decided to prefix the reserved keyword with '$' because it is a symbol that is not
@@ -93,14 +95,8 @@ public function __construct(LoggerInterface $logger, ProjectConfig $projectConfi
9395
*/
9496
public function getVariation(Experiment $experiment, $userId, $attributes = null)
9597
{
96-
// by default, the bucketing ID should be the user ID
97-
$bucketingId = $userId;
98-
99-
// If the bucketing ID key is defined in attributes, then use that in place of the userID for the murmur hash key
100-
if (!empty($attributes[RESERVED_ATTRIBUTE_KEY_BUCKETING_ID])) {
101-
$bucketingId = $attributes[RESERVED_ATTRIBUTE_KEY_BUCKETING_ID];
102-
$this->_logger->log(Logger::DEBUG, sprintf('Setting the bucketing ID to "%s".', $bucketingId));
103-
}
98+
99+
$bucketingId = $this->getBucketingId($userId, $attributes);
104100

105101
if (!$experiment->isExperimentRunning()) {
106102
$this->_logger->log(Logger::INFO, sprintf('Experiment "%s" is not running.', $experiment->getKey()));
@@ -147,6 +143,173 @@ public function getVariation(Experiment $experiment, $userId, $attributes = null
147143
return $variation;
148144
}
149145

146+
/**
147+
* Gets the Bucketing Id for Bucketing
148+
* @param string $userId
149+
* @param array $userAttributes
150+
* @return string
151+
*/
152+
private function getBucketingId($userId, $userAttributes){
153+
// by default, the bucketing ID should be the user ID
154+
$bucketingId = $userId;
155+
156+
// If the bucketing ID key is defined in userAttributes, then use that in place of the userID for the murmur hash key
157+
if (!empty($userAttributes[RESERVED_ATTRIBUTE_KEY_BUCKETING_ID])) {
158+
$bucketingId = $userAttributes[RESERVED_ATTRIBUTE_KEY_BUCKETING_ID];
159+
$this->_logger->log(Logger::DEBUG, sprintf('Setting the bucketing ID to "%s".', $bucketingId));
160+
}
161+
162+
return $bucketingId;
163+
}
164+
165+
/**
166+
* Get the variation the user is bucketed into for the given FeatureFlag
167+
* @param FeatureFlag $featureFlag The feature flag the user wants to access
168+
* @param string $userId user id
169+
* @param array $userAttributes user attributes
170+
* @return array/null {"experiment" : Experiment, "variation": Variation } / null
171+
*/
172+
public function getVariationForFeature(FeatureFlag $featureFlag, $userId, $userAttributes){
173+
//Evaluate in this order:
174+
//1. Attempt to bucket user into all experiments in the feature flag.
175+
//2. Attempt to bucket user into rollout in the feature flag.
176+
177+
// Check if the feature flag is under an experiment and the the user is bucketed into one of these experiments
178+
$result = $this->getVariationForFeatureExperiment($featureFlag, $userId, $userAttributes);
179+
if($result)
180+
return $result;
181+
182+
// Check if the feature flag has rollout and the user is bucketed into one of it's rules
183+
$variation = $this->getVariationForFeatureRollout($featureFlag, $userId, $userAttributes);
184+
if($variation){
185+
$this->_logger->log(Logger::INFO,
186+
"User '{$userId}' is bucketed into a rollout for feature flag '{$featureFlag->getKey()}'."
187+
);
188+
189+
return array(
190+
"experiment" => null,
191+
"variation" => $variation);
192+
193+
} else{
194+
$this->_logger->log(Logger::INFO,
195+
"User '{$userId}' is not bucketed into a rollout for feature flag '{$featureFlag->getKey()}'."
196+
);
197+
198+
return null;
199+
}
200+
}
201+
202+
/**
203+
* Get the variation if the user is bucketed for one of the experiments on this feature flag
204+
* @param FeatureFlag $featureFlag The feature flag the user wants to access
205+
* @param string $userId user id
206+
* @param array $userAttributes user userAttributes
207+
* @return array/null {"experiment" : Experiment, "variation": Variation } / null
208+
*/
209+
public function getVariationForFeatureExperiment(FeatureFlag $featureFlag, $userId, $userAttributes){
210+
$feature_flag_key = $featureFlag->getKey();
211+
$experimentIds = $featureFlag->getExperimentIds();
212+
//Check if there are any experiment ids inside feature flag
213+
if(empty($experimentIds))
214+
{
215+
$this->_logger->log(Logger::DEBUG,
216+
"The feature flag '{$feature_flag_key}' is not used in any experiments.");
217+
return null;
218+
}
219+
220+
// Evaluate each experiment id and return the first bucketed experiment variation
221+
foreach($experimentIds as $experiment_id){
222+
$experiment = $this->_projectConfig->getExperimentFromId($experiment_id);
223+
if( $experiment == new Experiment()){
224+
// Error logged and exception thrown in ProjectConfig-getExperimentFromId
225+
continue;
226+
}
227+
228+
$variation = $this->getVariation($experiment, $userId, $userAttributes);
229+
if($variation instanceof Variation && $variation != new Variation){
230+
$this->_logger->log(Logger::INFO,
231+
"The user '{$userId}' is bucketed into experiment '{$experiment->getKey()}' of feature '{$feature_flag_key}'.");
232+
return array(
233+
"experiment"=> $experiment,
234+
"variation" => $variation
235+
);
236+
}
237+
}
238+
239+
$this->_logger->log(Logger::INFO,
240+
"The user '{$userId}' is not bucketed into any of the experiments on the feature '{$feature_flag_key}'.");
241+
242+
return null;
243+
}
244+
245+
/**
246+
* Get the variation if the user is bucketed for one of the rollouts on this feature flag
247+
* Evaluate the user for rules in priority order by seeing if the user satisfies the audience.
248+
* Fall back onto the everyone else rule if the user is ever excluded from a rule due to traffic allocation.
249+
* @param FeatureFlag $featureFlag The feature flag the user wants to access
250+
* @param string $userId user id
251+
* @param array $userAttributes user userAttributes
252+
* @return Variation/null
253+
*/
254+
public function getVariationForFeatureRollout(FeatureFlag $featureFlag, $userId, $userAttributes){
255+
$bucketing_id = $this->getBucketingId($userId, $userAttributes);
256+
$feature_flag_key = $featureFlag->getKey();
257+
$rollout_id = $featureFlag->getRolloutId();
258+
if(empty($rollout_id)){
259+
$this->_logger->log(Logger::DEBUG,
260+
"Feature flag '{$feature_flag_key}' is not used in a rollout.");
261+
return null;
262+
}
263+
$rollout = $this->_projectConfig->getRolloutFromId($rollout_id);
264+
if($rollout == new Rollout()){
265+
// Error logged and thrown in getRolloutFromId
266+
return null;
267+
}
268+
269+
$rolloutRules = $rollout->getExperiments();
270+
if(sizeof($rolloutRules) == 0)
271+
return null;
272+
273+
// Evaluate all rollout rules except for last one
274+
for($i=0; $i<sizeof($rolloutRules)-1; $i++){
275+
$experiment = $rolloutRules[$i];
276+
277+
// Evaluate if user meets the audience condition of this rollout rule
278+
if (!Validator::isUserInExperiment($this->_projectConfig, $experiment, $userAttributes)) {
279+
$this->_logger->log(
280+
Logger::DEBUG,
281+
sprintf("User '%s' did not meet the audience conditions to be in rollout rule '%s'.", $userId, $experiment->getKey())
282+
);
283+
// Evaluate this user for the next rule
284+
continue;
285+
}
286+
287+
$this->_logger->log(Logger::DEBUG,
288+
sprintf("Attempting to bucket user '{$userId}' into rollout rule '%s'.", $experiment->getKey()));
289+
290+
// Evaluate if user satisfies the traffic allocation for this rollout rule
291+
$variation = $this->_bucketer->bucket($this->_projectConfig, $experiment, $bucketing_id, $userId);
292+
if($variation && $variation != new Variation()){
293+
return $variation;
294+
} else {
295+
$this->_logger->log(Logger::DEBUG,
296+
"User '{$userId}' was excluded due to traffic allocation. Checking 'Eveyrone Else' rule now.");
297+
break;
298+
}
299+
}
300+
301+
// Evaluate Everyone Else Rule / Last Rule now
302+
$experiment = $rolloutRules[sizeof($rolloutRules)-1];
303+
$variation = $this->_bucketer->bucket($this->_projectConfig, $experiment, $bucketing_id, $userId);
304+
if($variation && $variation != new Variation()){
305+
return $variation;
306+
} else {
307+
$this->_logger->log(Logger::DEBUG,
308+
"User '{$userId}' was excluded from the 'Everyone Else' rule for feature flag");
309+
return null;
310+
}
311+
}
312+
150313
/**
151314
* Determine variation the user has been forced into.
152315
*
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
<?php
2+
/**
3+
* Copyright 2017, Optimizely
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
namespace Optimizely\Entity;
19+
20+
use Optimizely\Utils\ConfigParser;
21+
22+
class FeatureFlag{
23+
24+
/**
25+
* variable to hold feature flag ID
26+
* @var String
27+
*/
28+
private $_id;
29+
30+
/**
31+
* variable to hold feature flag key
32+
* @var String
33+
*/
34+
private $_key;
35+
36+
/**
37+
* The ID of the rollout that is attached to this feature flag
38+
* @var String
39+
*/
40+
private $_rolloutId;
41+
42+
/**
43+
* A list of the IDs of the experiments the feature flag is attached to. If there are multiple expeirments,
44+
* they must be in the same mutually exclusive group.
45+
* @var [String]
46+
*/
47+
private $_experimentIds;
48+
49+
/**
50+
* A list of the feature variables that are part of this feature
51+
* @var [FeatureVariable]
52+
*/
53+
private $_variables;
54+
55+
public function __construct($id = null, $key = null, $rolloutId = null, $experimentIds = null, $variables = []){
56+
$this->_id = $id;
57+
$this->_key = $key;
58+
$this->_rolloutId = $rolloutId;
59+
$this->_experimentIds = $experimentIds;
60+
$this->_variables = ConfigParser::generateMap($variables, null, FeatureVariable::class);
61+
}
62+
63+
/**
64+
* @return String feature flag ID
65+
*/
66+
public function getId(){
67+
return $this->_id;
68+
}
69+
70+
/**
71+
* @param String $id feature flag ID
72+
*/
73+
public function setId($id){
74+
$this->_id = $id;
75+
}
76+
77+
/**
78+
* @return String feature flag key
79+
*/
80+
public function getKey(){
81+
return $this->_key;
82+
}
83+
84+
/**
85+
* @param String $key feature flag key
86+
*/
87+
public function setKey($key){
88+
$this->_key = $key;
89+
}
90+
91+
/**
92+
* @return String attached rollout ID
93+
*/
94+
public function getRolloutId(){
95+
return $this->_rolloutId;
96+
}
97+
98+
/**
99+
* @param String $rolloutId attached rollout ID
100+
*/
101+
public function setRolloutId($rolloutId){
102+
$this->_rolloutId = $rolloutId;
103+
}
104+
105+
/**
106+
* @return [String] attached experiment IDs
107+
*/
108+
public function getExperimentIds(){
109+
return $this->_experimentIds;
110+
}
111+
112+
/**
113+
* @param [String] $experimentIds attached experiment IDs
114+
*/
115+
public function setExperimentIds($experimentIds){
116+
$this->_experimentIds = $experimentIds;
117+
}
118+
119+
/**
120+
* @return [FeatureVariable] feature variables that are part of this feature
121+
*/
122+
public function getVariables(){
123+
return $this->_variables;
124+
}
125+
126+
/**
127+
* @param [FeatureVariable] $variables feature variables that are part of this feature
128+
*/
129+
public function setVariables($variables){
130+
$this->_variables = ConfigParser::generateMap($variables, null, FeatureVariable::class);
131+
}
132+
}

0 commit comments

Comments
 (0)