2020use Monolog \Logger ;
2121use Optimizely \Bucketer ;
2222use Optimizely \Entity \Experiment ;
23+ use Optimizely \Entity \FeatureFlag ;
24+ use Optimizely \Entity \Rollout ;
2325use Optimizely \Entity \Variation ;
2426use Optimizely \Logger \LoggerInterface ;
2527use Optimizely \ProjectConfig ;
3739 *
3840 * The decision service contains all logic around how a user decision is made. This includes all of the following (in order):
3941 * 1. Checking experiment status.
40- * 2. Checking whitelisting.
41- * 3. Check sticky bucketing.
42- * 4. Checking audience targeting.
43- * 5. Using Murmurhash3 to bucket the user.
42+ * 2. Checking force bucketing
43+ * 3. Checking whitelisting.
44+ * 4. Check sticky bucketing.
45+ * 5. Checking audience targeting.
46+ * 6. Using Murmurhash3 to bucket the user.
4447 *
4548 * @package Optimizely
4649 */
@@ -79,6 +82,28 @@ public function __construct(LoggerInterface $logger, ProjectConfig $projectConfi
7982 $ this ->_userProfileService = $ userProfileService ;
8083 }
8184
85+ /**
86+ * Gets the ID for Bucketing
87+ * @param string $userId user ID
88+ * @param array $userAttributes user attributes
89+ *
90+ * @return string the bucketing ID assigned to user
91+ */
92+ private function getBucketingId ($ userId , $ userAttributes )
93+ {
94+ // By default, the bucketing ID should be the user ID
95+ $ bucketingId = $ userId ;
96+
97+ // If the bucketing ID key is defined in userAttributes, then use that in
98+ // place of the userID for the murmur hash key
99+ if (!empty ($ userAttributes [RESERVED_ATTRIBUTE_KEY_BUCKETING_ID ])) {
100+ $ bucketingId = $ userAttributes [RESERVED_ATTRIBUTE_KEY_BUCKETING_ID ];
101+ $ this ->_logger ->log (Logger::DEBUG , sprintf ('Setting the bucketing ID to "%s". ' , $ bucketingId ));
102+ }
103+
104+ return $ bucketingId ;
105+ }
106+
82107 /**
83108 * Determine which variation to show the user.
84109 *
@@ -90,14 +115,8 @@ public function __construct(LoggerInterface $logger, ProjectConfig $projectConfi
90115 */
91116 public function getVariation (Experiment $ experiment , $ userId , $ attributes = null )
92117 {
93- // by default, the bucketing ID should be the user ID
94- $ bucketingId = $ userId ;
95-
96- // If the bucketing ID key is defined in attributes, then use that in place of the userID for the murmur hash key
97- if (!empty ($ attributes [RESERVED_ATTRIBUTE_KEY_BUCKETING_ID ])) {
98- $ bucketingId = $ attributes [RESERVED_ATTRIBUTE_KEY_BUCKETING_ID ];
99- $ this ->_logger ->log (Logger::DEBUG , sprintf ('Setting the bucketing ID to "%s". ' , $ bucketingId ));
100- }
118+
119+ $ bucketingId = $ this ->getBucketingId ($ userId , $ attributes );
101120
102121 if (!$ experiment ->isExperimentRunning ()) {
103122 $ this ->_logger ->log (Logger::INFO , sprintf ('Experiment "%s" is not running. ' , $ experiment ->getKey ()));
@@ -144,6 +163,174 @@ public function getVariation(Experiment $experiment, $userId, $attributes = null
144163 return $ variation ;
145164 }
146165
166+ /**
167+ * Get the variation the user is bucketed into for the given FeatureFlag
168+ * @param FeatureFlag $featureFlag The feature flag the user wants to access
169+ * @param string $userId user ID
170+ * @param array $userAttributes user attributes
171+ * @return Decision if getVariationForFeatureExperiment or getVariationForFeatureRollout returns a Decision
172+ * null otherwise
173+ */
174+ public function getVariationForFeature (FeatureFlag $ featureFlag , $ userId , $ userAttributes )
175+ {
176+ //Evaluate in this order:
177+ //1. Attempt to bucket user into experiment using feature flag.
178+ //2. Attempt to bucket user into rollout using the feature flag.
179+
180+ // Check if the feature flag is under an experiment and the the user is bucketed into one of these experiments
181+ $ decision = $ this ->getVariationForFeatureExperiment ($ featureFlag , $ userId , $ userAttributes );
182+ if ($ decision ) {
183+ return $ decision ;
184+ }
185+
186+ // Check if the feature flag has rollout and the user is bucketed into one of it's rules
187+ $ decision = $ this ->getVariationForFeatureRollout ($ featureFlag , $ userId , $ userAttributes );
188+ if ($ decision ) {
189+ $ this ->_logger ->log (
190+ Logger::INFO ,
191+ "User ' {$ userId }' is bucketed into rollout for feature flag ' {$ featureFlag ->getKey ()}'. "
192+ );
193+
194+ return $ decision ;
195+ }
196+
197+ $ this ->_logger ->log (
198+ Logger::INFO ,
199+ "User ' {$ userId }' is not bucketed into rollout for feature flag ' {$ featureFlag ->getKey ()}'. "
200+ );
201+
202+ return null ;
203+ }
204+
205+ /**
206+ * Get the variation if the user is bucketed for one of the experiments on this feature flag
207+ * @param FeatureFlag $featureFlag The feature flag the user wants to access
208+ * @param string $userId user id
209+ * @param array $userAttributes user userAttributes
210+ * @return Decision if a variation is returned for the user
211+ * null if feature flag is not used in any experiments or no variation is returned for the user
212+ */
213+ public function getVariationForFeatureExperiment (FeatureFlag $ featureFlag , $ userId , $ userAttributes )
214+ {
215+ $ feature_flag_key = $ featureFlag ->getKey ();
216+ $ experimentIds = $ featureFlag ->getExperimentIds ();
217+
218+ // Check if there are any experiment IDs inside feature flag
219+ if (empty ($ experimentIds )) {
220+ $ this ->_logger ->log (
221+ Logger::DEBUG ,
222+ "The feature flag ' {$ feature_flag_key }' is not used in any experiments. "
223+ );
224+ return null ;
225+ }
226+
227+ // Evaluate each experiment ID and return the first bucketed experiment variation
228+ foreach ($ experimentIds as $ experiment_id ) {
229+ $ experiment = $ this ->_projectConfig ->getExperimentFromId ($ experiment_id );
230+ if ($ experiment && !($ experiment ->getKey ())) {
231+ // Error logged and exception thrown in ProjectConfig-getExperimentFromId
232+ continue ;
233+ }
234+
235+ $ variation = $ this ->getVariation ($ experiment , $ userId , $ userAttributes );
236+ if ($ variation && $ variation ->getKey ()) {
237+ $ this ->_logger ->log (
238+ Logger::INFO ,
239+ "The user ' {$ userId }' is bucketed into experiment ' {$ experiment ->getKey ()}' of feature ' {$ feature_flag_key }'. "
240+ );
241+
242+ return new FeatureDecision ($ experiment ->getId (), $ variation ->getId (), FeatureDecision::DECISION_SOURCE_EXPERIMENT );
243+ }
244+ }
245+
246+ $ this ->_logger ->log (
247+ Logger::INFO ,
248+ "The user ' {$ userId }' is not bucketed into any of the experiments using the feature ' {$ feature_flag_key }'. "
249+ );
250+
251+ return null ;
252+ }
253+
254+ /**
255+ * Get the variation if the user is bucketed into rollout for this feature flag
256+ * Evaluate the user for rules in priority order by seeing if the user satisfies the audience.
257+ * Fall back onto the everyone else rule if the user is ever excluded from a rule due to traffic allocation.
258+ * @param FeatureFlag $featureFlag The feature flag the user wants to access
259+ * @param string $userId user id
260+ * @param array $userAttributes user userAttributes
261+ * @return Decision if a variation is returned for the user
262+ * null if feature flag is not used in a rollout or
263+ * no rollout found against the rollout ID or
264+ * no variation is returned for the user
265+ */
266+ public function getVariationForFeatureRollout (FeatureFlag $ featureFlag , $ userId , $ userAttributes )
267+ {
268+ $ bucketing_id = $ this ->getBucketingId ($ userId , $ userAttributes );
269+ $ feature_flag_key = $ featureFlag ->getKey ();
270+ $ rollout_id = $ featureFlag ->getRolloutId ();
271+ if (empty ($ rollout_id )) {
272+ $ this ->_logger ->log (
273+ Logger::DEBUG ,
274+ "Feature flag ' {$ feature_flag_key }' is not used in a rollout. "
275+ );
276+ return null ;
277+ }
278+ $ rollout = $ this ->_projectConfig ->getRolloutFromId ($ rollout_id );
279+ if ($ rollout && !($ rollout ->getId ())) {
280+ // Error logged and thrown in getRolloutFromId
281+ return null ;
282+ }
283+
284+ $ rolloutRules = $ rollout ->getExperiments ();
285+ if (sizeof ($ rolloutRules ) == 0 ) {
286+ return null ;
287+ }
288+
289+ // Evaluate all rollout rules except for last one
290+ for ($ i = 0 ; $ i < sizeof ($ rolloutRules ) - 1 ; $ i ++) {
291+ $ experiment = $ rolloutRules [$ i ];
292+
293+ // Evaluate if user meets the audience condition of this rollout rule
294+ if (!Validator::isUserInExperiment ($ this ->_projectConfig , $ experiment , $ userAttributes )) {
295+ $ this ->_logger ->log (
296+ Logger::DEBUG ,
297+ sprintf ("User '%s' did not meet the audience conditions to be in rollout rule '%s'. " , $ userId , $ experiment ->getKey ())
298+ );
299+ // Evaluate this user for the next rule
300+ continue ;
301+ }
302+
303+ $ this ->_logger ->log (
304+ Logger::DEBUG ,
305+ sprintf ("Attempting to bucket user ' {$ userId }' into rollout rule '%s'. " , $ experiment ->getKey ())
306+ );
307+
308+ // Evaluate if user satisfies the traffic allocation for this rollout rule
309+ $ variation = $ this ->_bucketer ->bucket ($ this ->_projectConfig , $ experiment , $ bucketing_id , $ userId );
310+ if ($ variation && $ variation ->getKey ()) {
311+ return new FeatureDecision ($ experiment ->getId (), $ variation ->getId (), FeatureDecision::DECISION_SOURCE_ROLLOUT );
312+ } else {
313+ $ this ->_logger ->log (
314+ Logger::DEBUG ,
315+ "User ' {$ userId }' was excluded due to traffic allocation. Checking 'Everyone Else' rule now. "
316+ );
317+ break ;
318+ }
319+ }
320+ // Evaluate Everyone Else Rule / Last Rule now
321+ $ experiment = $ rolloutRules [sizeof ($ rolloutRules )-1 ];
322+ $ variation = $ this ->_bucketer ->bucket ($ this ->_projectConfig , $ experiment , $ bucketing_id , $ userId );
323+ if ($ variation && $ variation ->getKey ()) {
324+ return new FeatureDecision ($ experiment ->getId (), $ variation ->getId (), FeatureDecision::DECISION_SOURCE_ROLLOUT );
325+ } else {
326+ $ this ->_logger ->log (
327+ Logger::DEBUG ,
328+ "User ' {$ userId }' was excluded from the 'Everyone Else' rule for feature flag "
329+ );
330+ return null ;
331+ }
332+ }
333+
147334 /**
148335 * Determine variation the user has been forced into.
149336 *
0 commit comments