@@ -714,8 +714,8 @@ def get_decision_for_flag(
714714 reasons .extend (holdout_decision ['reasons' ])
715715
716716 decision = holdout_decision ['decision' ]
717- # Check if user was bucketed into holdout (has a variation )
718- if decision . variation is None :
717+ # Check if user was bucketed into holdout (has a decision )
718+ if decision is None :
719719 continue
720720
721721 message = (
@@ -730,21 +730,102 @@ 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 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 )
742812
743- return {
744- 'decision' : fallback_result ['decision' ],
745- 'error' : fallback_result .get ('error' , False ),
746- 'reasons' : reasons
747- }
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+ }
748829
749830 def get_variation_for_holdout (
750831 self ,
@@ -931,100 +1012,24 @@ def get_variations_for_feature_list(
9311012 - 'error': Boolean indicating if an error occurred during the decision process.
9321013 - 'reasons': List of log messages representing decision making for each feature.
9331014 """
934- decide_reasons : list [str ] = []
935-
1015+ ignore_ups = False
9361016 if options :
9371017 ignore_ups = OptimizelyDecideOption .IGNORE_USER_PROFILE_SERVICE in options
938- else :
939- ignore_ups = False
9401018
9411019 user_profile_tracker : Optional [UserProfileTracker ] = None
942- if self .user_profile_service is not None and not ignore_ups :
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 )
1020+ if not ignore_ups and self .user_profile_service :
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 )
9451024
9461025 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 )
9471031
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 :
1032+ if user_profile_tracker :
10281033 user_profile_tracker .save_user_profile ()
10291034
10301035 return decisions
0 commit comments