12
12
# limitations under the License.
13
13
14
14
from collections import namedtuple
15
+
15
16
from six import string_types
16
17
17
18
from . import bucketer
21
22
from .helpers import validator
22
23
from .user_profile import UserProfile
23
24
24
-
25
25
Decision = namedtuple ('Decision' , 'experiment variation source' )
26
26
27
27
@@ -211,7 +211,7 @@ def get_stored_variation(self, project_config, experiment, user_profile):
211
211
if variation_id :
212
212
variation = project_config .get_variation_from_id (experiment .key , variation_id )
213
213
if variation :
214
- message = 'Found a stored decision. User "%s" is in variation "%s" of experiment "%s".' \
214
+ message = 'Found a stored decision. User "%s" is in variation "%s" of experiment "%s".' \
215
215
% (user_id , variation .key , experiment .key )
216
216
self .logger .info (
217
217
message
@@ -221,7 +221,7 @@ def get_stored_variation(self, project_config, experiment, user_profile):
221
221
return None
222
222
223
223
def get_variation (
224
- self , project_config , experiment , user_id , attributes , ignore_user_profile = False
224
+ self , project_config , experiment , user_context , ignore_user_profile = False
225
225
):
226
226
""" Top-level function to help determine variation user should be put in.
227
227
@@ -234,14 +234,17 @@ def get_variation(
234
234
Args:
235
235
project_config: Instance of ProjectConfig.
236
236
experiment: Experiment for which user variation needs to be determined.
237
- user_id: ID for user.
238
- attributes: Dict representing user attributes.
237
+ user_context: contains user id and attributes
239
238
ignore_user_profile: True to ignore the user profile lookup. Defaults to False.
240
239
241
240
Returns:
242
241
Variation user should see. None if user is not in experiment or experiment is not running
243
242
And an array of log messages representing decision making.
244
243
"""
244
+
245
+ user_id = user_context .user_id
246
+ attributes = user_context .get_user_attributes ()
247
+
245
248
decide_reasons = []
246
249
# Check if experiment is running
247
250
if not experiment_helper .is_experiment_running (experiment ):
@@ -323,110 +326,174 @@ def get_variation(
323
326
decide_reasons .append (message )
324
327
return None , decide_reasons
325
328
326
- def get_variation_for_rollout (self , project_config , rollout , user_id , attributes = None ):
329
+ def get_variation_for_rollout (self , project_config , rollout , user , options ):
327
330
""" Determine which experiment/variation the user is in for a given rollout.
328
331
Returns the variation of the first experiment the user qualifies for.
329
332
330
333
Args:
331
334
project_config: Instance of ProjectConfig.
332
335
rollout: Rollout for which we are getting the variation.
333
- user_id : ID for user.
334
- attributes: Dict representing user attributes .
336
+ user : ID and attributes for user.
337
+ options: Decide options .
335
338
336
339
Returns:
337
340
Decision namedtuple consisting of experiment and variation for the user and
338
341
array of log messages representing decision making.
339
342
"""
343
+ user_id = user .user_id
344
+ attributes = user .get_user_attributes ()
340
345
decide_reasons = []
341
- # Go through each experiment in order and try to get the variation for the user
342
- if rollout and len (rollout .experiments ) > 0 :
343
- for idx in range (len (rollout .experiments ) - 1 ):
344
- logging_key = str (idx + 1 )
345
- rollout_rule = project_config .get_experiment_from_id (rollout .experiments [idx ].get ('id' ))
346
-
347
- # Check if user meets audience conditions for targeting rule
348
- audience_conditions = rollout_rule .get_audience_conditions_or_ids ()
349
- user_meets_audience_conditions , reasons_received = audience_helper .does_user_meet_audience_conditions (
350
- project_config ,
351
- audience_conditions ,
352
- enums .RolloutRuleAudienceEvaluationLogs ,
353
- logging_key ,
354
- attributes ,
355
- self .logger )
346
+ rollout_rules = project_config .get_rollout_experiments_map (rollout )
347
+
348
+ if rollout and len (rollout_rules ) > 0 :
349
+ index = 0
350
+ while index < len (rollout_rules ):
351
+ decision_response , reasons_received = self .get_variation_from_delivery_rule (project_config ,
352
+ rollout_rules [index ].key ,
353
+ rollout_rules , index , user ,
354
+ options )
356
355
decide_reasons += reasons_received
357
- if not user_meets_audience_conditions :
358
- message = 'User "{}" does not meet conditions for targeting rule {}.' .format (user_id , logging_key )
359
- self .logger .debug (
360
- message
361
- )
362
- decide_reasons .append (message )
363
- continue
364
- message = 'User "{}" meets audience conditions for targeting rule {}.' .format (user_id , idx + 1 )
356
+
357
+ if decision_response :
358
+ variation , skip_to_everyone_else = decision_response
359
+
360
+ if variation :
361
+ rule = rollout_rules [index ]
362
+ feature_decision = Decision (experiment = rule , variation = variation ,
363
+ source = enums .DecisionSources .ROLLOUT )
364
+
365
+ return feature_decision , decide_reasons
366
+
367
+ # the last rule is special for "Everyone Else"
368
+ index = len (rollout_rules ) - 1 if skip_to_everyone_else else index + 1
369
+
370
+ return None , decide_reasons
371
+
372
+ def get_variation_from_experiment_rule (self , config , flag_key , rule , user , options ):
373
+ """ Checks for experiment rule if decision is forced and returns it.
374
+ Otherwise returns a regular decision.
375
+
376
+ Args:
377
+ config: Instance of ProjectConfig.
378
+ flag_key: Key of the flag.
379
+ rule: Experiment rule.
380
+ user: ID and attributes for user.
381
+ options: Decide options.
382
+
383
+ Returns:
384
+ Decision namedtuple consisting of experiment and variation for the user and
385
+ array of log messages representing decision making.
386
+ """
387
+ decide_reasons = []
388
+
389
+ # check forced decision first
390
+ forced_decision_variation , reasons_received = user .find_validated_forced_decision (flag_key , rule .key , options )
391
+ decide_reasons += reasons_received
392
+
393
+ if forced_decision_variation :
394
+ return forced_decision_variation , decide_reasons
395
+
396
+ # regular decision
397
+ decision_variation , variation_reasons = self .get_variation (config , rule , user , options )
398
+ decide_reasons += variation_reasons
399
+ return decision_variation , decide_reasons
400
+
401
+ def get_variation_from_delivery_rule (self , config , flag_key , rules , rule_index , user , options ):
402
+ """ Checks for delivery rule if decision is forced and returns it.
403
+ Otherwise returns a regular decision.
404
+
405
+ Args:
406
+ config: Instance of ProjectConfig.
407
+ flag_key: Key of the flag.
408
+ rules: Experiment rule.
409
+ user: ID and attributes for user.
410
+ options: Decide options.
411
+
412
+ Returns:
413
+ If forced decision, it returns namedtuple consisting of forced_decision_variation and skip_to_everyone_else
414
+ and decision reason log messages.
415
+
416
+ If regular decision it returns a tuple of bucketed_variation and skip_to_everyone_else
417
+ and decision reason log messages
418
+ """
419
+ decide_reasons = []
420
+ skip_to_everyone_else = False
421
+ bucketed_variation = None
422
+
423
+ # check forced decision first
424
+ rule = rules [rule_index ]
425
+ forced_decision_variation , reasons_received = user .find_validated_forced_decision (flag_key , rule .key , options )
426
+ decide_reasons += reasons_received
427
+
428
+ if forced_decision_variation :
429
+ return (forced_decision_variation , skip_to_everyone_else ), decide_reasons
430
+
431
+ # regular decision
432
+ user_id = user .user_id
433
+ attributes = user .get_user_attributes ()
434
+ bucketing_id = self ._get_bucketing_id (user_id , attributes )
435
+
436
+ everyone_else = (rule_index == len (rules ) - 1 )
437
+ logging_key = "Everyone Else" if everyone_else else str (rule_index + 1 )
438
+
439
+ rollout_rule = config .get_experiment_from_id (rule .id )
440
+ audience_conditions = rollout_rule .get_audience_conditions_or_ids ()
441
+
442
+ audience_decision_response , reasons_received_audience = audience_helper .does_user_meet_audience_conditions (
443
+ config , audience_conditions , enums .RolloutRuleAudienceEvaluationLogs , logging_key , attributes , self .logger )
444
+ # TODO - add regular logger here, and add log to reasons
445
+ decide_reasons += reasons_received_audience
446
+
447
+ if audience_decision_response :
448
+
449
+ message = 'User "{}" meets conditions for targeting rule {}.' .format (user_id , logging_key )
450
+ self .logger .debug (message )
451
+ decide_reasons .append (message )
452
+
453
+ bucketed_variation , bucket_reasons = self .bucketer .bucket (config , rollout_rule , user_id ,
454
+ bucketing_id ) # used this from existing, now old code
455
+ decide_reasons .append (bucket_reasons )
456
+
457
+ if bucketed_variation :
458
+ message = 'User "{}" bucketed into a targeting rule {}.' .format (user_id , logging_key )
459
+ self .logger .debug (message )
460
+ decide_reasons .append (message )
461
+
462
+ elif not everyone_else :
463
+ # skip this logging for EveryoneElse since this has a message not for everyone_else
464
+ message = 'User "{}" not bucketed into a targeting rule {}.' .format (user_id ,
465
+ logging_key )
365
466
self .logger .debug (message )
366
467
decide_reasons .append (message )
367
- # Determine bucketing ID to be used
368
- bucketing_id , bucket_reasons = self ._get_bucketing_id (user_id , attributes )
369
- decide_reasons += bucket_reasons
370
- variation , reasons = self .bucketer .bucket (project_config , rollout_rule , user_id , bucketing_id )
371
- decide_reasons += reasons
372
- if variation :
373
- message = 'User "{}" is in the traffic group of targeting rule {}.' .format (user_id , logging_key )
374
- self .logger .debug (
375
- message
376
- )
377
- decide_reasons .append (message )
378
- return Decision (rollout_rule , variation , enums .DecisionSources .ROLLOUT ), decide_reasons
379
- else :
380
- message = 'User "{}" is not in the traffic group for targeting rule {}. ' \
381
- 'Checking "Everyone Else" rule now.' .format (user_id , logging_key )
382
- # Evaluate no further rules
383
- self .logger .debug (
384
- message
385
- )
386
- decide_reasons .append (message )
387
- break
388
-
389
- # Evaluate last rule i.e. "Everyone Else" rule
390
- everyone_else_rule = project_config .get_experiment_from_id (rollout .experiments [- 1 ].get ('id' ))
391
- audience_conditions = everyone_else_rule .get_audience_conditions_or_ids ()
392
- audience_eval , audience_reasons = audience_helper .does_user_meet_audience_conditions (
393
- project_config ,
394
- audience_conditions ,
395
- enums .RolloutRuleAudienceEvaluationLogs ,
396
- 'Everyone Else' ,
397
- attributes ,
398
- self .logger
399
- )
400
- decide_reasons += audience_reasons
401
- if audience_eval :
402
- # Determine bucketing ID to be used
403
- bucketing_id , bucket_id_reasons = self ._get_bucketing_id (user_id , attributes )
404
- decide_reasons += bucket_id_reasons
405
- variation , bucket_reasons = self .bucketer .bucket (
406
- project_config , everyone_else_rule , user_id , bucketing_id )
407
- decide_reasons += bucket_reasons
408
- if variation :
409
- message = 'User "{}" meets conditions for targeting rule "Everyone Else".' .format (user_id )
410
- self .logger .debug (message )
411
- decide_reasons .append (message )
412
- return Decision (everyone_else_rule , variation , enums .DecisionSources .ROLLOUT ,), decide_reasons
413
468
414
- return Decision (None , None , enums .DecisionSources .ROLLOUT ), decide_reasons
469
+ # skip the rest of rollout rules to the everyone-else rule if audience matches but not bucketed.
470
+ skip_to_everyone_else = True
415
471
416
- def get_variation_for_feature (self , project_config , feature , user_id , attributes = None , ignore_user_profile = False ):
472
+ else :
473
+ message = 'User "{}" does not meet conditions for targeting rule {}.' .format (user_id , logging_key )
474
+ self .logger .debug (message )
475
+ decide_reasons .append (message )
476
+
477
+ return (bucketed_variation , skip_to_everyone_else ), decide_reasons
478
+
479
+ def get_variation_for_feature (self , project_config , feature , user_context , ignore_user_profile = False ):
417
480
""" Returns the experiment/variation the user is bucketed in for the given feature.
418
481
419
482
Args:
420
483
project_config: Instance of ProjectConfig.
421
484
feature: Feature for which we are determining if it is enabled or not for the given user.
422
- user_id: ID for user.
485
+ user: user context for user.
423
486
attributes: Dict representing user attributes.
424
487
ignore_user_profile: True if we should bypass the user profile service
425
488
426
489
Returns:
427
490
Decision namedtuple consisting of experiment and variation for the user.
428
491
"""
492
+ user_id = user_context .user_id
493
+ attributes = user_context .get_user_attributes ()
494
+
429
495
decide_reasons = []
496
+
430
497
bucketing_id , reasons = self ._get_bucketing_id (user_id , attributes )
431
498
decide_reasons += reasons
432
499
@@ -436,15 +503,15 @@ def get_variation_for_feature(self, project_config, feature, user_id, attributes
436
503
for experiment in feature .experimentIds :
437
504
experiment = project_config .get_experiment_from_id (experiment )
438
505
if experiment :
439
- variation , variation_reasons = self .get_variation (
440
- project_config , experiment , user_id , attributes , ignore_user_profile )
506
+ variation , variation_reasons = self .get_variation_from_experiment_rule (
507
+ project_config , feature . key , experiment , user_context , ignore_user_profile )
441
508
decide_reasons += variation_reasons
442
509
if variation :
443
510
return Decision (experiment , variation , enums .DecisionSources .FEATURE_TEST ), decide_reasons
444
511
445
512
# Next check if user is part of a rollout
446
513
if feature .rolloutId :
447
514
rollout = project_config .get_rollout_from_id (feature .rolloutId )
448
- return self .get_variation_for_rollout (project_config , rollout , user_id , attributes )
515
+ return self .get_variation_for_rollout (project_config , rollout , user_context , ignore_user_profile )
449
516
else :
450
517
return Decision (None , None , enums .DecisionSources .ROLLOUT ), decide_reasons
0 commit comments