@@ -46,7 +46,8 @@ class DecisionService
4646 DECISION_SOURCES = {
4747 'EXPERIMENT' => 'experiment' ,
4848 'FEATURE_TEST' => 'feature-test' ,
49- 'ROLLOUT' => 'rollout'
49+ 'ROLLOUT' => 'rollout' ,
50+ 'HOLDOUT' => 'holdout'
5051 } . freeze
5152
5253 def initialize ( logger , cmab_service , user_profile_service = nil )
@@ -169,6 +170,107 @@ def get_variation_for_feature(project_config, feature_flag, user_context, decide
169170 get_variations_for_feature_list ( project_config , [ feature_flag ] , user_context , decide_options ) . first
170171 end
171172
173+ def get_decision_for_flag ( feature_flag , user_context , project_config , decide_options = [ ] , user_profile_tracker = nil , decide_reasons = nil )
174+ # Get the decision for a single feature flag.
175+ # Processes holdouts, experiments, and rollouts in that order.
176+ #
177+ # feature_flag - The feature flag to get a decision for
178+ # user_context - The user context
179+ # project_config - The project config
180+ # decide_options - Array of decide options
181+ # user_profile_tracker - The user profile tracker
182+ # decide_reasons - Array of decision reasons to merge
183+ #
184+ # Returns a DecisionResult for the feature flag
185+
186+ reasons = decide_reasons ? decide_reasons . dup : [ ]
187+ user_id = user_context . user_id
188+
189+ # Check holdouts
190+ holdouts = project_config . get_holdouts_for_flag ( feature_flag [ 'key' ] )
191+ holdouts . each do |holdout |
192+ holdout_decision = get_variation_for_holdout ( holdout , user_context , project_config )
193+ reasons . push ( *holdout_decision . reasons )
194+
195+ if holdout_decision . decision
196+ message = "The user '#{ user_id } ' is bucketed into holdout '#{ holdout [ 'key' ] } ' for feature flag '#{ feature_flag [ 'key' ] } '."
197+ @logger . log ( Logger ::INFO , message )
198+ reasons . push ( message )
199+ return DecisionResult . new ( holdout_decision . decision , false , reasons )
200+ end
201+ end
202+
203+ # Check if the feature flag has an experiment and the user is bucketed into that experiment
204+ experiment_decision = get_variation_for_feature_experiment ( project_config , feature_flag , user_context , user_profile_tracker , decide_options )
205+ reasons . push ( *experiment_decision . reasons )
206+
207+ if experiment_decision . decision
208+ return DecisionResult . new ( experiment_decision . decision , experiment_decision . error , reasons )
209+ end
210+
211+ # Check if the feature flag has a rollout and the user is bucketed into that rollout
212+ rollout_decision = get_variation_for_feature_rollout ( project_config , feature_flag , user_context )
213+ reasons . push ( *rollout_decision . reasons )
214+
215+ if rollout_decision . decision
216+ message = "The user '#{ user_id } ' is bucketed into a rollout for feature flag '#{ feature_flag [ 'key' ] } '."
217+ @logger . log ( Logger ::INFO , message )
218+ reasons . push ( message )
219+ return DecisionResult . new ( rollout_decision . decision , rollout_decision . error , reasons )
220+ else
221+ message = "The user '#{ user_id } ' is not bucketed into a rollout for feature flag '#{ feature_flag [ 'key' ] } '."
222+ @logger . log ( Logger ::INFO , message )
223+ reasons . push ( message )
224+ default_decision = Decision . new ( nil , nil , DECISION_SOURCES [ 'ROLLOUT' ] , nil )
225+ return DecisionResult . new ( nil , false , reasons )
226+ end
227+ end
228+
229+ def get_variation_for_holdout ( holdout , user_context , project_config )
230+ # Get the variation for holdout
231+ #
232+ # holdout - The holdout configuration
233+ # user_context - The user context
234+ # project_config - The project config
235+ #
236+ # Returns a DecisionResult for the holdout
237+
238+ decide_reasons = [ ]
239+ user_id = user_context . user_id
240+ attributes = user_context . user_attributes
241+ bucketing_id , bucketing_id_reasons = get_bucketing_id ( user_id , attributes )
242+ decide_reasons . push ( *bucketing_id_reasons )
243+
244+ # Check audience conditions
245+ user_meets_audience_conditions , reasons_received = Audience . user_meets_audience_conditions? ( project_config , holdout , user_context , @logger )
246+ decide_reasons . push ( *reasons_received )
247+
248+ unless user_meets_audience_conditions
249+ message = "User '#{ user_id } ' does not meet the conditions for holdout '#{ holdout [ 'key' ] } '."
250+ @logger . log ( Logger ::DEBUG , message )
251+ decide_reasons . push ( message )
252+ return DecisionResult . new ( nil , false , decide_reasons )
253+ end
254+
255+ # Bucket user into holdout variation
256+ variation , bucket_reasons = @bucketer . bucket ( project_config , holdout , bucketing_id , user_id )
257+ decide_reasons . push ( *bucket_reasons )
258+
259+ if variation
260+ message = "The user '#{ user_id } ' is bucketed into variation '#{ variation [ 'key' ] } ' of holdout '#{ holdout [ 'key' ] } '."
261+ @logger . log ( Logger ::INFO , message )
262+ decide_reasons . push ( message )
263+
264+ holdout_decision = Decision . new ( holdout , variation , DECISION_SOURCES [ 'HOLDOUT' ] , nil )
265+ return DecisionResult . new ( holdout_decision , false , decide_reasons )
266+ else
267+ message = "The user '#{ user_id } ' is not bucketed into holdout '#{ holdout [ 'key' ] } '."
268+ @logger . log ( Logger ::DEBUG , message )
269+ decide_reasons . push ( message )
270+ return DecisionResult . new ( nil , false , decide_reasons )
271+ end
272+ end
273+
172274 def get_variations_for_feature_list ( project_config , feature_flags , user_context , decide_options = [ ] )
173275 # Returns the list of experiment/variation the user is bucketed in for the given list of features.
174276 #
0 commit comments