@@ -730,102 +730,21 @@ def get_decision_for_flag(
730730 'reasons' : reasons
731731 }
732732
733- # Check if the feature flag has an experiment and the user is bucketed into that experiment
734- if feature_flag .experimentIds :
735- # Iterate through experiments to find a match
736- for experiment_id in feature_flag .experimentIds :
737- experiment = project_config .get_experiment_from_id (experiment_id )
738- if not experiment :
739- continue
740-
741- # Check for forced decision
742- optimizely_decision_context = OptimizelyUserContext .OptimizelyDecisionContext (
743- feature_flag .key , experiment .key
744- )
745- forced_decision_variation , forced_reasons = self .validated_forced_decision (
746- project_config , optimizely_decision_context , user_context
747- )
748- reasons .extend (forced_reasons )
749-
750- if forced_decision_variation :
751- decision = Decision (
752- experiment , forced_decision_variation , enums .DecisionSources .FEATURE_TEST , None
753- )
754- return {
755- 'decision' : decision ,
756- 'error' : False ,
757- 'reasons' : reasons
758- }
759-
760- # Get variation through normal bucketing
761- variation_result = self .get_variation (
762- project_config , experiment , user_context , user_profile_tracker , reasons , decide_options
763- )
764- cmab_uuid = variation_result ['cmab_uuid' ]
765- variation_reasons = variation_result ['reasons' ]
766- decision_variation = variation_result ['variation' ]
767- error = variation_result ['error' ]
768- reasons .extend (variation_reasons )
769-
770- # If there's an error, return immediately
771- if error :
772- decision = Decision (experiment , None , enums .DecisionSources .FEATURE_TEST , cmab_uuid )
773- return {
774- 'decision' : decision ,
775- 'error' : True ,
776- 'reasons' : reasons
777- }
778-
779- # If user is bucketed into a variation, return the decision
780- if decision_variation :
781- self .logger .debug (
782- f'User "{ user_id } " '
783- f'bucketed into experiment "{ experiment .key } " of feature "{ feature_flag .key } ".'
784- )
785- decision = Decision (
786- experiment , decision_variation , enums .DecisionSources .FEATURE_TEST , cmab_uuid
787- )
788- return {
789- 'decision' : decision ,
790- 'error' : False ,
791- 'reasons' : reasons
792- }
793-
794- # Check if the feature flag has a rollout and the user is bucketed into that rollout
795- rollout_decision , rollout_reasons = self .get_variation_for_rollout (
796- project_config , feature_flag , user_context
797- )
798- reasons .extend (rollout_reasons )
799-
800- if rollout_decision and rollout_decision .variation :
801- # Check if this was a forced decision (last reason contains "forced decision map")
802- is_forced_decision = reasons and 'forced decision map' in reasons [- 1 ] if reasons else False
803-
804- if not is_forced_decision :
805- # Only add the "bucketed into rollout" message for normal bucketing
806- message = (
807- f"The user '{ user_id } ' is bucketed into a rollout "
808- f"for feature flag '{ feature_flag .key } '."
809- )
810- self .logger .info (message )
811- reasons .append (message )
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 ]
812738
813- return {
814- 'decision' : rollout_decision ,
815- 'error' : False ,
816- 'reasons' : reasons
817- }
818- else :
819- message = (
820- f"The user '{ user_id } ' is not bucketed into a rollout "
821- f"for feature flag '{ feature_flag .key } '."
822- )
823- self .logger .info (message )
824- return {
825- 'decision' : Decision (None , None , enums .DecisionSources .ROLLOUT , None ),
826- 'error' : False ,
827- 'reasons' : reasons
828- }
739+ # Merge reasons
740+ if fallback_result .get ('reasons' ):
741+ reasons .extend (fallback_result ['reasons' ])
742+
743+ return {
744+ 'decision' : fallback_result ['decision' ],
745+ 'error' : fallback_result .get ('error' , False ),
746+ 'reasons' : reasons
747+ }
829748
830749 def get_variation_for_holdout (
831750 self ,
@@ -905,9 +824,9 @@ def get_variation_for_holdout(
905824 self .logger .info (message )
906825 decide_reasons .append (message )
907826
908- # Create Decision for holdout - pass holdout dict as experiment , source is HOLDOUT
827+ # Create Decision for holdout - experiment is None , source is HOLDOUT
909828 holdout_decision : Decision = Decision (
910- experiment = holdout , # type: ignore[arg-type]
829+ experiment = None ,
911830 variation = variation ,
912831 source = enums .DecisionSources .HOLDOUT ,
913832 cmab_uuid = None
@@ -1012,24 +931,100 @@ def get_variations_for_feature_list(
1012931 - 'error': Boolean indicating if an error occurred during the decision process.
1013932 - 'reasons': List of log messages representing decision making for each feature.
1014933 """
1015- ignore_ups = False
934+ decide_reasons : list [str ] = []
935+
1016936 if options :
1017937 ignore_ups = OptimizelyDecideOption .IGNORE_USER_PROFILE_SERVICE in options
938+ else :
939+ ignore_ups = False
1018940
1019941 user_profile_tracker : Optional [UserProfileTracker ] = None
1020942 if self .user_profile_service is not None and not ignore_ups :
1021- user_id = user_context .user_id
1022- user_profile_tracker = UserProfileTracker (user_id , self .user_profile_service , self .logger )
1023- user_profile_tracker .load_user_profile ([], None )
943+ user_profile_tracker = UserProfileTracker (user_context .user_id , self .user_profile_service , self .logger )
944+ user_profile_tracker .load_user_profile (decide_reasons , None )
1024945
1025946 decisions = []
1026- for feature_flag in features :
1027- decision = self .get_decision_for_flag (
1028- feature_flag , user_context , project_config , options , user_profile_tracker
1029- )
1030- decisions .append (decision )
1031947
1032- if user_profile_tracker :
948+ 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+
952+ # Check if the feature flag is under an experiment
953+ if feature .experimentIds :
954+ for experiment_id in feature .experimentIds :
955+ experiment = project_config .get_experiment_from_id (experiment_id )
956+ decision_variation : Optional [Union [entities .Variation , VariationDict ]] = None
957+
958+ if experiment :
959+ optimizely_decision_context = OptimizelyUserContext .OptimizelyDecisionContext (
960+ feature .key , experiment .key )
961+ forced_decision_variation , reasons_received = self .validated_forced_decision (
962+ project_config , optimizely_decision_context , user_context )
963+ feature_reasons .extend (reasons_received )
964+
965+ if forced_decision_variation :
966+ decision_variation = forced_decision_variation
967+ cmab_uuid = None
968+ error = False
969+ else :
970+ variation_result = self .get_variation (
971+ project_config , experiment , user_context , user_profile_tracker , feature_reasons , options
972+ )
973+ cmab_uuid = variation_result ['cmab_uuid' ]
974+ variation_reasons = variation_result ['reasons' ]
975+ decision_variation = variation_result ['variation' ]
976+ error = variation_result ['error' ]
977+ feature_reasons .extend (variation_reasons )
978+
979+ if error :
980+ decision = Decision (experiment , None , enums .DecisionSources .FEATURE_TEST , cmab_uuid )
981+ decision_result : DecisionResult = {
982+ 'decision' : decision ,
983+ 'error' : True ,
984+ 'reasons' : feature_reasons
985+ }
986+ decisions .append (decision_result )
987+ experiment_decision_found = True
988+ break
989+
990+ if decision_variation :
991+ self .logger .debug (
992+ f'User "{ user_context .user_id } " '
993+ f'bucketed into experiment "{ experiment .key } " of feature "{ feature .key } ".'
994+ )
995+ decision = Decision (experiment , decision_variation ,
996+ enums .DecisionSources .FEATURE_TEST , cmab_uuid )
997+ decision_result = {
998+ 'decision' : decision ,
999+ 'error' : False ,
1000+ 'reasons' : feature_reasons
1001+ }
1002+ decisions .append (decision_result )
1003+ experiment_decision_found = True # Mark that a decision was found
1004+ break # Stop after the first successful experiment decision
1005+
1006+ # Only process rollout if no experiment decision was found and no error
1007+ if not experiment_decision_found :
1008+ rollout_decision , rollout_reasons = self .get_variation_for_rollout (project_config ,
1009+ feature ,
1010+ user_context )
1011+ if rollout_reasons :
1012+ feature_reasons .extend (rollout_reasons )
1013+ if rollout_decision :
1014+ self .logger .debug (f'User "{ user_context .user_id } " '
1015+ f'bucketed into rollout for feature "{ feature .key } ".' )
1016+ else :
1017+ self .logger .debug (f'User "{ user_context .user_id } " '
1018+ f'not bucketed into any rollout for feature "{ feature .key } ".' )
1019+
1020+ decision_result = {
1021+ 'decision' : rollout_decision ,
1022+ 'error' : False ,
1023+ 'reasons' : feature_reasons
1024+ }
1025+ decisions .append (decision_result )
1026+
1027+ if self .user_profile_service is not None and user_profile_tracker is not None and ignore_ups is False :
10331028 user_profile_tracker .save_user_profile ()
10341029
10351030 return decisions
0 commit comments