4242require_relative 'optimizely/odp/lru_cache'
4343require_relative 'optimizely/odp/odp_manager'
4444require_relative 'optimizely/helpers/sdk_settings'
45+ require_relative 'optimizely/user_profile_tracker'
4546
4647module Optimizely
4748 class Project
@@ -172,65 +173,18 @@ def create_user_context(user_id, attributes = nil)
172173 OptimizelyUserContext . new ( self , user_id , attributes )
173174 end
174175
175- def decide ( user_context , key , decide_options = [ ] )
176- # raising on user context as it is internal and not provided directly by the user.
177- raise if user_context . class != OptimizelyUserContext
178-
179- reasons = [ ]
180-
181- # check if SDK is ready
182- unless is_valid
183- @logger . log ( Logger ::ERROR , InvalidProjectConfigError . new ( 'decide' ) . message )
184- reasons . push ( OptimizelyDecisionMessage ::SDK_NOT_READY )
185- return OptimizelyDecision . new ( flag_key : key , user_context : user_context , reasons : reasons )
186- end
187-
188- # validate that key is a string
189- unless key . is_a? ( String )
190- @logger . log ( Logger ::ERROR , 'Provided key is invalid' )
191- reasons . push ( format ( OptimizelyDecisionMessage ::FLAG_KEY_INVALID , key ) )
192- return OptimizelyDecision . new ( flag_key : key , user_context : user_context , reasons : reasons )
193- end
194-
195- # validate that key maps to a feature flag
196- config = project_config
197- feature_flag = config . get_feature_flag_from_key ( key )
198- unless feature_flag
199- @logger . log ( Logger ::ERROR , "No feature flag was found for key '#{ key } '." )
200- reasons . push ( format ( OptimizelyDecisionMessage ::FLAG_KEY_INVALID , key ) )
201- return OptimizelyDecision . new ( flag_key : key , user_context : user_context , reasons : reasons )
202- end
203-
204- # merge decide_options and default_decide_options
205- if decide_options . is_a? Array
206- decide_options += @default_decide_options
207- else
208- @logger . log ( Logger ::DEBUG , 'Provided decide options is not an array. Using default decide options.' )
209- decide_options = @default_decide_options
210- end
211-
176+ def create_optimizely_decision ( user_context , flag_key , decision , reasons , decide_options , config )
212177 # Create Optimizely Decision Result.
213178 user_id = user_context . user_id
214179 attributes = user_context . user_attributes
215180 variation_key = nil
216181 feature_enabled = false
217182 rule_key = nil
218- flag_key = key
219183 all_variables = { }
220184 decision_event_dispatched = false
185+ feature_flag = config . get_feature_flag_from_key ( flag_key )
221186 experiment = nil
222187 decision_source = Optimizely ::DecisionService ::DECISION_SOURCES [ 'ROLLOUT' ]
223- context = Optimizely ::OptimizelyUserContext ::OptimizelyDecisionContext . new ( key , nil )
224- variation , reasons_received = @decision_service . validated_forced_decision ( config , context , user_context )
225- reasons . push ( *reasons_received )
226-
227- if variation
228- decision = Optimizely ::DecisionService ::Decision . new ( nil , variation , Optimizely ::DecisionService ::DECISION_SOURCES [ 'FEATURE_TEST' ] )
229- else
230- decision , reasons_received = @decision_service . get_variation_for_feature ( config , feature_flag , user_context , decide_options )
231- reasons . push ( *reasons_received )
232- end
233-
234188 # Send impression event if Decision came from a feature test and decide options doesn't include disableDecisionEvent
235189 if decision . is_a? ( Optimizely ::DecisionService ::Decision )
236190 experiment = decision . experiment
@@ -249,7 +203,7 @@ def decide(user_context, key, decide_options = [])
249203 # Generate all variables map if decide options doesn't include excludeVariables
250204 unless decide_options . include? OptimizelyDecideOption ::EXCLUDE_VARIABLES
251205 feature_flag [ 'variables' ] . each do |variable |
252- variable_value = get_feature_variable_for_variation ( key , feature_enabled , variation , variable , user_id )
206+ variable_value = get_feature_variable_for_variation ( flag_key , feature_enabled , variation , variable , user_id )
253207 all_variables [ variable [ 'key' ] ] = Helpers ::VariableType . cast_value_to_type ( variable_value , variable [ 'type' ] , @logger )
254208 end
255209 end
@@ -281,6 +235,47 @@ def decide(user_context, key, decide_options = [])
281235 )
282236 end
283237
238+ def decide ( user_context , key , decide_options = [ ] )
239+ # raising on user context as it is internal and not provided directly by the user.
240+ raise if user_context . class != OptimizelyUserContext
241+
242+ reasons = [ ]
243+
244+ # check if SDK is ready
245+ unless is_valid
246+ @logger . log ( Logger ::ERROR , InvalidProjectConfigError . new ( 'decide' ) . message )
247+ reasons . push ( OptimizelyDecisionMessage ::SDK_NOT_READY )
248+ return OptimizelyDecision . new ( flag_key : key , user_context : user_context , reasons : reasons )
249+ end
250+
251+ # validate that key is a string
252+ unless key . is_a? ( String )
253+ @logger . log ( Logger ::ERROR , 'Provided key is invalid' )
254+ reasons . push ( format ( OptimizelyDecisionMessage ::FLAG_KEY_INVALID , key ) )
255+ return OptimizelyDecision . new ( flag_key : key , user_context : user_context , reasons : reasons )
256+ end
257+
258+ # validate that key maps to a feature flag
259+ config = project_config
260+ feature_flag = config . get_feature_flag_from_key ( key )
261+ unless feature_flag
262+ @logger . log ( Logger ::ERROR , "No feature flag was found for key '#{ key } '." )
263+ reasons . push ( format ( OptimizelyDecisionMessage ::FLAG_KEY_INVALID , key ) )
264+ return OptimizelyDecision . new ( flag_key : key , user_context : user_context , reasons : reasons )
265+ end
266+
267+ # merge decide_options and default_decide_options
268+ if decide_options . is_a? Array
269+ decide_options += @default_decide_options
270+ else
271+ @logger . log ( Logger ::DEBUG , 'Provided decide options is not an array. Using default decide options.' )
272+ decide_options = @default_decide_options
273+ end
274+
275+ decide_options . delete ( OptimizelyDecideOption ::ENABLED_FLAGS_ONLY ) if decide_options . include? ( OptimizelyDecideOption ::ENABLED_FLAGS_ONLY )
276+ decide_for_keys ( user_context , [ key ] , decide_options , true ) [ key ]
277+ end
278+
284279 def decide_all ( user_context , decide_options = [ ] )
285280 # raising on user context as it is internal and not provided directly by the user.
286281 raise if user_context . class != OptimizelyUserContext
@@ -298,7 +293,7 @@ def decide_all(user_context, decide_options = [])
298293 decide_for_keys ( user_context , keys , decide_options )
299294 end
300295
301- def decide_for_keys ( user_context , keys , decide_options = [ ] )
296+ def decide_for_keys ( user_context , keys , decide_options = [ ] , ignore_default_options = false ) # rubocop:disable Style/OptionalBooleanParameter
302297 # raising on user context as it is internal and not provided directly by the user.
303298 raise if user_context . class != OptimizelyUserContext
304299
@@ -308,13 +303,79 @@ def decide_for_keys(user_context, keys, decide_options = [])
308303 return { }
309304 end
310305
311- enabled_flags_only = ( !decide_options . nil? && ( decide_options . include? OptimizelyDecideOption ::ENABLED_FLAGS_ONLY ) ) || ( @default_decide_options . include? OptimizelyDecideOption ::ENABLED_FLAGS_ONLY )
306+ # merge decide_options and default_decide_options
307+ unless ignore_default_options
308+ if decide_options . is_a? ( Array )
309+ decide_options += @default_decide_options
310+ else
311+ @logger . log ( Logger ::DEBUG , 'Provided decide options is not an array. Using default decide options.' )
312+ decide_options = @default_decide_options
313+ end
314+ end
315+
316+ # enabled_flags_only = (!decide_options.nil? && (decide_options.include? OptimizelyDecideOption::ENABLED_FLAGS_ONLY)) || (@default_decide_options.include? OptimizelyDecideOption::ENABLED_FLAGS_ONLY)
312317
313318 decisions = { }
319+ valid_keys = [ ]
320+ decision_reasons_dict = { }
321+ config = project_config
322+ return decisions unless config
323+
324+ flags_without_forced_decision = [ ]
325+ flag_decisions = { }
326+
314327 keys . each do |key |
315- decision = decide ( user_context , key , decide_options )
316- decisions [ key ] = decision unless enabled_flags_only && !decision . enabled
328+ # Retrieve the feature flag from the project's feature flag key map
329+ feature_flag = config . feature_flag_key_map [ key ]
330+
331+ # If the feature flag is nil, create a default OptimizelyDecision and move to the next key
332+ if feature_flag . nil?
333+ decisions [ key ] = OptimizelyDecision . new ( nil , false , nil , nil , key , user_context , [ ] )
334+ next
335+ end
336+ valid_keys . push ( key )
337+ decision_reasons = [ ]
338+ decision_reasons_dict [ key ] = decision_reasons
339+
340+ config = project_config
341+ context = Optimizely ::OptimizelyUserContext ::OptimizelyDecisionContext . new ( key , nil )
342+ variation , reasons_received = @decision_service . validated_forced_decision ( config , context , user_context )
343+ decision_reasons_dict [ key ] . push ( *reasons_received )
344+ if variation
345+ decision = Optimizely ::DecisionService ::Decision . new ( nil , variation , Optimizely ::DecisionService ::DECISION_SOURCES [ 'FEATURE_TEST' ] )
346+ flag_decisions [ key ] = decision
347+ else
348+ flags_without_forced_decision . push ( feature_flag )
349+ end
317350 end
351+ decision_list = @decision_service . get_variations_for_feature_list ( config , flags_without_forced_decision , user_context , decide_options )
352+
353+ flags_without_forced_decision . each_with_index do |flag , i |
354+ decision = decision_list [ i ] [ 0 ]
355+ reasons = decision_list [ i ] [ 1 ]
356+ flag_key = flag [ 'key' ]
357+ flag_decisions [ flag_key ] = decision
358+ decision_reasons_dict [ flag_key ] ||= [ ]
359+ decision_reasons_dict [ flag_key ] . push ( *reasons )
360+ end
361+ valid_keys . each do |key |
362+ flag_decision = flag_decisions [ key ]
363+ decision_reasons = decision_reasons_dict [ key ]
364+ optimizely_decision = create_optimizely_decision (
365+ user_context ,
366+ key ,
367+ flag_decision ,
368+ decision_reasons ,
369+ decide_options ,
370+ config
371+ )
372+
373+ enabled_flags_only_missing = !decide_options . include? ( OptimizelyDecideOption ::ENABLED_FLAGS_ONLY )
374+ is_enabled = optimizely_decision . enabled
375+
376+ decisions [ key ] = optimizely_decision if enabled_flags_only_missing || is_enabled
377+ end
378+
318379 decisions
319380 end
320381
@@ -959,7 +1020,10 @@ def get_variation_with_config(experiment_key, user_id, attributes, config)
9591020 return nil unless user_inputs_valid? ( attributes )
9601021
9611022 user_context = OptimizelyUserContext . new ( self , user_id , attributes , identify : false )
962- variation_id , = @decision_service . get_variation ( config , experiment_id , user_context )
1023+ user_profile_tracker = UserProfileTracker . new ( user_id , @user_profile_service , @logger )
1024+ user_profile_tracker . load_user_profile
1025+ variation_id , = @decision_service . get_variation ( config , experiment_id , user_context , user_profile_tracker )
1026+ user_profile_tracker . save_user_profile
9631027 variation = config . get_variation_from_id ( experiment_key , variation_id ) unless variation_id . nil?
9641028 variation_key = variation [ 'key' ] if variation
9651029 decision_notification_type = if config . feature_experiment? ( experiment_id )
0 commit comments