@@ -707,7 +707,7 @@ def get_decision_for_flag(
707707 reasons = decide_reasons .copy () if decide_reasons else []
708708 user_id = user_context .user_id
709709
710- # Check holdouts
710+ # Check holdouts first (they take precedence)
711711 holdouts = project_config .get_holdouts_for_flag (feature_flag .key )
712712 for holdout in holdouts :
713713 holdout_decision = self .get_variation_for_holdout (holdout , user_context , project_config )
@@ -730,21 +730,84 @@ def get_decision_for_flag(
730730 'reasons' : reasons
731731 }
732732
733- # If no holdout decision, fall back to existing experiment/rollout logic
734- # Use get_variations_for_feature_list which handles experiments and rollouts
735- fallback_result = self .get_variations_for_feature_list (
736- project_config , [feature_flag ], user_context , decide_options
737- )[0 ]
738-
739- # Merge reasons
740- if fallback_result .get ('reasons' ):
741- reasons .extend (fallback_result ['reasons' ])
733+ # Check if the feature flag is under an experiment
734+ if feature_flag .experimentIds :
735+ for experiment_id in feature_flag .experimentIds :
736+ experiment = project_config .get_experiment_from_id (experiment_id )
737+ decision_variation : Optional [Union [entities .Variation , VariationDict ]] = None
738+
739+ if experiment :
740+ optimizely_decision_context = OptimizelyUserContext .OptimizelyDecisionContext (
741+ feature_flag .key , experiment .key )
742+ forced_decision_variation , reasons_received = self .validated_forced_decision (
743+ project_config , optimizely_decision_context , user_context )
744+ reasons .extend (reasons_received )
745+
746+ if forced_decision_variation :
747+ decision_variation = forced_decision_variation
748+ cmab_uuid = None
749+ error = False
750+ else :
751+ variation_result = self .get_variation (
752+ project_config , experiment , user_context , user_profile_tracker , reasons , decide_options
753+ )
754+ cmab_uuid = variation_result ['cmab_uuid' ]
755+ variation_reasons = variation_result ['reasons' ]
756+ decision_variation = variation_result ['variation' ]
757+ error = variation_result ['error' ]
758+ reasons .extend (variation_reasons )
759+
760+ if error :
761+ # If there's an error (e.g., CMAB error), return immediately without falling back to rollout
762+ decision = Decision (experiment , None , enums .DecisionSources .FEATURE_TEST , cmab_uuid )
763+ return {
764+ 'decision' : decision ,
765+ 'error' : True ,
766+ 'reasons' : reasons
767+ }
768+
769+ if decision_variation :
770+ self .logger .debug (
771+ f'User "{ user_context .user_id } " '
772+ f'bucketed into experiment "{ experiment .key } " of feature "{ feature_flag .key } ".'
773+ )
774+ decision = Decision (experiment , decision_variation ,
775+ enums .DecisionSources .FEATURE_TEST , cmab_uuid )
776+ return {
777+ 'decision' : decision ,
778+ 'error' : False ,
779+ 'reasons' : reasons
780+ }
781+
782+ # Fall back to rollout
783+ rollout_decision , rollout_reasons = self .get_variation_for_rollout (project_config ,
784+ feature_flag ,
785+ user_context )
786+ reasons .extend (rollout_reasons )
787+
788+ if rollout_decision and rollout_decision .variation :
789+ # Check if this was a forced decision (last reason contains "forced decision map")
790+ is_forced_decision = reasons and 'forced decision map' in reasons [- 1 ] if reasons else False
791+
792+ if not is_forced_decision :
793+ # Only add the "bucketed into rollout" message for normal bucketing
794+ message = f"The user '{ user_id } ' is bucketed into a rollout for feature flag '{ feature_flag .key } '."
795+ self .logger .info (message )
796+ reasons .append (message )
742797
743- return {
744- 'decision' : fallback_result ['decision' ],
745- 'error' : fallback_result .get ('error' , False ),
746- 'reasons' : reasons
747- }
798+ return {
799+ 'decision' : rollout_decision ,
800+ 'error' : False ,
801+ 'reasons' : reasons
802+ }
803+ else :
804+ message = f"The user '{ user_id } ' is not bucketed into a rollout for feature flag '{ feature_flag .key } '."
805+ self .logger .info (message )
806+ return {
807+ 'decision' : Decision (None , None , enums .DecisionSources .ROLLOUT , None ),
808+ 'error' : False ,
809+ 'reasons' : reasons
810+ }
748811
749812 def get_variation_for_holdout (
750813 self ,
@@ -946,116 +1009,10 @@ def get_variations_for_feature_list(
9461009 decisions = []
9471010
9481011 for feature in features :
949- feature_reasons = decide_reasons .copy ()
950- experiment_decision_found = False # Track if an experiment decision was made for the feature
951- holdout_decision_found = False # Track if a holdout decision was made for the feature
952- user_id = user_context .user_id
953-
954- # Check holdouts first (they take precedence over experiments and rollouts)
955- holdouts = project_config .get_holdouts_for_flag (feature .key )
956- for holdout in holdouts :
957- holdout_decision_result = self .get_variation_for_holdout (holdout , user_context , project_config )
958- feature_reasons .extend (holdout_decision_result ['reasons' ])
959-
960- decision = holdout_decision_result ['decision' ]
961- # Check if user was bucketed into holdout (has a variation)
962- if decision .variation is None :
963- continue
964-
965- message = (
966- f"The user '{ user_id } ' is bucketed into holdout '{ holdout ['key' ]} ' "
967- f"for feature flag '{ feature .key } '."
968- )
969- self .logger .info (message )
970- feature_reasons .append (message )
971-
972- decision_result : DecisionResult = {
973- 'decision' : holdout_decision_result ['decision' ],
974- 'error' : False ,
975- 'reasons' : feature_reasons
976- }
977- decisions .append (decision_result )
978- holdout_decision_found = True
979- break
980-
981- # If holdout decision found, skip experiment and rollout evaluation
982- if holdout_decision_found :
983- continue
984-
985- # Check if the feature flag is under an experiment
986- if feature .experimentIds :
987- for experiment_id in feature .experimentIds :
988- experiment = project_config .get_experiment_from_id (experiment_id )
989- decision_variation : Optional [Union [entities .Variation , VariationDict ]] = None
990-
991- if experiment :
992- optimizely_decision_context = OptimizelyUserContext .OptimizelyDecisionContext (
993- feature .key , experiment .key )
994- forced_decision_variation , reasons_received = self .validated_forced_decision (
995- project_config , optimizely_decision_context , user_context )
996- feature_reasons .extend (reasons_received )
997-
998- if forced_decision_variation :
999- decision_variation = forced_decision_variation
1000- cmab_uuid = None
1001- error = False
1002- else :
1003- variation_result = self .get_variation (
1004- project_config , experiment , user_context , user_profile_tracker , feature_reasons , options
1005- )
1006- cmab_uuid = variation_result ['cmab_uuid' ]
1007- variation_reasons = variation_result ['reasons' ]
1008- decision_variation = variation_result ['variation' ]
1009- error = variation_result ['error' ]
1010- feature_reasons .extend (variation_reasons )
1011-
1012- if error :
1013- decision = Decision (experiment , None , enums .DecisionSources .FEATURE_TEST , cmab_uuid )
1014- decision_result = {
1015- 'decision' : decision ,
1016- 'error' : True ,
1017- 'reasons' : feature_reasons
1018- }
1019- decisions .append (decision_result )
1020- experiment_decision_found = True
1021- break
1022-
1023- if decision_variation :
1024- self .logger .debug (
1025- f'User "{ user_context .user_id } " '
1026- f'bucketed into experiment "{ experiment .key } " of feature "{ feature .key } ".'
1027- )
1028- decision = Decision (experiment , decision_variation ,
1029- enums .DecisionSources .FEATURE_TEST , cmab_uuid )
1030- decision_result = {
1031- 'decision' : decision ,
1032- 'error' : False ,
1033- 'reasons' : feature_reasons
1034- }
1035- decisions .append (decision_result )
1036- experiment_decision_found = True # Mark that a decision was found
1037- break # Stop after the first successful experiment decision
1038-
1039- # Only process rollout if no experiment decision was found and no error
1040- if not experiment_decision_found :
1041- rollout_decision , rollout_reasons = self .get_variation_for_rollout (project_config ,
1042- feature ,
1043- user_context )
1044- if rollout_reasons :
1045- feature_reasons .extend (rollout_reasons )
1046- if rollout_decision :
1047- self .logger .debug (f'User "{ user_context .user_id } " '
1048- f'bucketed into rollout for feature "{ feature .key } ".' )
1049- else :
1050- self .logger .debug (f'User "{ user_context .user_id } " '
1051- f'not bucketed into any rollout for feature "{ feature .key } ".' )
1052-
1053- decision_result = {
1054- 'decision' : rollout_decision ,
1055- 'error' : False ,
1056- 'reasons' : feature_reasons
1057- }
1058- decisions .append (decision_result )
1012+ decision = self .get_decision_for_flag (
1013+ feature , user_context , project_config , options , user_profile_tracker , decide_reasons
1014+ )
1015+ decisions .append (decision )
10591016
10601017 if self .user_profile_service is not None and user_profile_tracker is not None and ignore_ups is False :
10611018 user_profile_tracker .save_user_profile ()
0 commit comments