Skip to content

Commit f111aa5

Browse files
rashidspmikeproeng37
authored andcommitted
refac(config): Moves forced variations mapping into DecisionService. (#174)
1 parent 0094ed7 commit f111aa5

File tree

6 files changed

+298
-295
lines changed

6 files changed

+298
-295
lines changed

lib/optimizely.rb

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,16 @@ def get_variation(experiment_key, user_id, attributes = nil)
173173
# @return [Boolean] indicates if the set completed successfully.
174174

175175
def set_forced_variation(experiment_key, user_id, variation_key)
176-
@config.set_forced_variation(experiment_key, user_id, variation_key)
176+
unless @is_valid
177+
@logger.log(Logger::ERROR, InvalidDatafileError.new('set_forced_variation').message)
178+
return nil
179+
end
180+
181+
input_values = {experiment_key: experiment_key, user_id: user_id}
182+
input_values[:variation_key] = variation_key unless variation_key.nil?
183+
return false unless Optimizely::Helpers::Validator.inputs_valid?(input_values, @logger, Logger::ERROR)
184+
185+
@decision_service.set_forced_variation(@config, experiment_key, user_id, variation_key)
177186
end
178187

179188
# Gets the forced variation for a given user and experiment.
@@ -184,8 +193,20 @@ def set_forced_variation(experiment_key, user_id, variation_key)
184193
# @return [String] The forced variation key.
185194

186195
def get_forced_variation(experiment_key, user_id)
196+
unless @is_valid
197+
@logger.log(Logger::ERROR, InvalidDatafileError.new('get_forced_variation').message)
198+
return nil
199+
end
200+
201+
return nil unless Optimizely::Helpers::Validator.inputs_valid?(
202+
{
203+
experiment_key: experiment_key,
204+
user_id: user_id
205+
}, @logger, Logger::ERROR
206+
)
207+
187208
forced_variation_key = nil
188-
forced_variation = @config.get_forced_variation(experiment_key, user_id)
209+
forced_variation = @decision_service.get_forced_variation(@config, experiment_key, user_id)
189210
forced_variation_key = forced_variation['key'] if forced_variation
190211

191212
forced_variation_key

lib/optimizely/decision_service.rb

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ class DecisionService
3333

3434
attr_reader :bucketer
3535

36+
# Hash of user IDs to a Hash of experiments to variations.
37+
# This contains all the forced variations set by the user by calling setForcedVariation.
38+
attr_reader :forced_variation_map
39+
3640
Decision = Struct.new(:experiment, :variation, :source)
3741

3842
DECISION_SOURCES = {
@@ -44,6 +48,7 @@ def initialize(logger, user_profile_service = nil)
4448
@logger = logger
4549
@user_profile_service = user_profile_service
4650
@bucketer = Bucketer.new(logger)
51+
@forced_variation_map = {}
4752
end
4853

4954
def get_variation(project_config, experiment_key, user_id, attributes = nil)
@@ -70,7 +75,7 @@ def get_variation(project_config, experiment_key, user_id, attributes = nil)
7075
end
7176

7277
# Check if a forced variation is set for the user
73-
forced_variation = project_config.get_forced_variation(experiment_key, user_id)
78+
forced_variation = get_forced_variation(project_config, experiment_key, user_id)
7479
return forced_variation['id'] if forced_variation
7580

7681
# Check if user is in a white-listed variation
@@ -266,6 +271,86 @@ def get_variation_for_feature_rollout(project_config, feature_flag, user_id, att
266271
nil
267272
end
268273

274+
def set_forced_variation(project_config, experiment_key, user_id, variation_key)
275+
# Sets a Hash of user IDs to a Hash of experiments to forced variations.
276+
#
277+
# project_config - Instance of ProjectConfig
278+
# experiment_key - String Key for experiment
279+
# user_id - String ID for user.
280+
# variation_key - String Key for variation. If null, then clear the existing experiment-to-variation mapping
281+
#
282+
# Returns a boolean value that indicates if the set completed successfully
283+
284+
experiment = project_config.get_experiment_from_key(experiment_key)
285+
experiment_id = experiment['id'] if experiment
286+
# check if the experiment exists in the datafile
287+
return false if experiment_id.nil? || experiment_id.empty?
288+
289+
# clear the forced variation if the variation key is null
290+
if variation_key.nil?
291+
@forced_variation_map[user_id].delete(experiment_id) if @forced_variation_map.key? user_id
292+
@logger.log(Logger::DEBUG, "Variation mapped to experiment '#{experiment_key}' has been removed for user "\
293+
"'#{user_id}'.")
294+
return true
295+
end
296+
297+
variation_id = project_config.get_variation_id_from_key(experiment_key, variation_key)
298+
299+
# check if the variation exists in the datafile
300+
unless variation_id
301+
# this case is logged in get_variation_id_from_key
302+
return false
303+
end
304+
305+
@forced_variation_map[user_id] = {} unless @forced_variation_map.key? user_id
306+
@forced_variation_map[user_id][experiment_id] = variation_id
307+
@logger.log(Logger::DEBUG, "Set variation '#{variation_id}' for experiment '#{experiment_id}' and "\
308+
"user '#{user_id}' in the forced variation map.")
309+
true
310+
end
311+
312+
def get_forced_variation(project_config, experiment_key, user_id)
313+
# Gets the forced variation for the given user and experiment.
314+
#
315+
# project_config - Instance of ProjectConfig
316+
# experiment_key - String Key for experiment
317+
# user_id - String ID for user
318+
#
319+
# Returns Variation The variation which the given user and experiment should be forced into
320+
321+
unless @forced_variation_map.key? user_id
322+
@logger.log(Logger::DEBUG, "User '#{user_id}' is not in the forced variation map.")
323+
return nil
324+
end
325+
326+
experiment_to_variation_map = @forced_variation_map[user_id]
327+
experiment = project_config.get_experiment_from_key(experiment_key)
328+
experiment_id = experiment['id'] if experiment
329+
# check for nil and empty string experiment ID
330+
# this case is logged in get_experiment_from_key
331+
return nil if experiment_id.nil? || experiment_id.empty?
332+
333+
unless experiment_to_variation_map.key? experiment_id
334+
@logger.log(Logger::DEBUG, "No experiment '#{experiment_key}' mapped to user '#{user_id}' "\
335+
'in the forced variation map.')
336+
return nil
337+
end
338+
339+
variation_id = experiment_to_variation_map[experiment_id]
340+
variation_key = ''
341+
variation = project_config.get_variation_from_id(experiment_key, variation_id)
342+
variation_key = variation['key'] if variation
343+
344+
# check if the variation exists in the datafile
345+
# this case is logged in get_variation_from_id
346+
return nil if variation_key.empty?
347+
348+
@logger.log(Logger::DEBUG, "Variation '#{variation_key}' is mapped to experiment '#{experiment_key}' "\
349+
"and user '#{user_id}' in the forced variation map")
350+
351+
variation
352+
end
353+
269354
private
270355

271356
def get_whitelisted_variation_id(project_config, experiment_key, user_id)

lib/optimizely/project_config.rb

Lines changed: 0 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,6 @@ class ProjectConfig
5959
attr_reader :variation_id_to_variable_usage_map
6060
attr_reader :variation_key_map
6161

62-
# Hash of user IDs to a Hash
63-
# of experiments to variations. This contains all the forced variations
64-
# set by the user by calling setForcedVariation (it is not the same as the
65-
# whitelisting forcedVariations data structure in the Experiments class).
66-
attr_reader :forced_variation_map
67-
6862
def initialize(datafile, logger, error_handler)
6963
# ProjectConfig init method to fetch and set project config data
7064
#
@@ -108,7 +102,6 @@ def initialize(datafile, logger, error_handler)
108102
@audience_id_map = @audience_id_map.merge(generate_key_map(@typed_audiences, 'id')) unless @typed_audiences.empty?
109103
@variation_id_map = {}
110104
@variation_key_map = {}
111-
@forced_variation_map = {}
112105
@variation_id_to_variable_usage_map = {}
113106
@variation_id_to_experiment_map = {}
114107
@experiment_key_map.each_value do |exp|
@@ -280,95 +273,6 @@ def get_whitelisted_variations(experiment_key)
280273
@error_handler.handle_error InvalidExperimentError
281274
end
282275

283-
def get_forced_variation(experiment_key, user_id)
284-
# Gets the forced variation for the given user and experiment.
285-
#
286-
# experiment_key - String Key for experiment.
287-
# user_id - String ID for user
288-
#
289-
# Returns Variation The variation which the given user and experiment should be forced into.
290-
291-
return nil unless Optimizely::Helpers::Validator.inputs_valid?(
292-
{
293-
experiment_key: experiment_key,
294-
user_id: user_id
295-
}, @logger, Logger::DEBUG
296-
)
297-
298-
unless @forced_variation_map.key? user_id
299-
@logger.log(Logger::DEBUG, "User '#{user_id}' is not in the forced variation map.")
300-
return nil
301-
end
302-
303-
experiment_to_variation_map = @forced_variation_map[user_id]
304-
experiment = get_experiment_from_key(experiment_key)
305-
experiment_id = experiment['id'] if experiment
306-
# check for nil and empty string experiment ID
307-
# this case is logged in get_experiment_from_key
308-
return nil if experiment_id.nil? || experiment_id.empty?
309-
310-
unless experiment_to_variation_map.key? experiment_id
311-
@logger.log(Logger::DEBUG, "No experiment '#{experiment_key}' mapped to user '#{user_id}' "\
312-
'in the forced variation map.')
313-
return nil
314-
end
315-
316-
variation_id = experiment_to_variation_map[experiment_id]
317-
variation_key = ''
318-
variation = get_variation_from_id(experiment_key, variation_id)
319-
variation_key = variation['key'] if variation
320-
321-
# check if the variation exists in the datafile
322-
# this case is logged in get_variation_from_id
323-
return nil if variation_key.empty?
324-
325-
@logger.log(Logger::DEBUG, "Variation '#{variation_key}' is mapped to experiment '#{experiment_key}' "\
326-
"and user '#{user_id}' in the forced variation map")
327-
328-
variation
329-
end
330-
331-
def set_forced_variation(experiment_key, user_id, variation_key)
332-
# Sets a Hash of user IDs to a Hash of experiments to forced variations.
333-
#
334-
# experiment_key - String Key for experiment.
335-
# user_id - String ID for user.
336-
# variation_key - String Key for variation. If null, then clear the existing experiment-to-variation mapping.
337-
#
338-
# Returns a boolean value that indicates if the set completed successfully.
339-
340-
input_values = {experiment_key: experiment_key, user_id: user_id}
341-
input_values[:variation_key] = variation_key unless variation_key.nil?
342-
return false unless Optimizely::Helpers::Validator.inputs_valid?(input_values, @logger, Logger::DEBUG)
343-
344-
experiment = get_experiment_from_key(experiment_key)
345-
experiment_id = experiment['id'] if experiment
346-
# check if the experiment exists in the datafile
347-
return false if experiment_id.nil? || experiment_id.empty?
348-
349-
# clear the forced variation if the variation key is null
350-
if variation_key.nil?
351-
@forced_variation_map[user_id].delete(experiment_id) if @forced_variation_map.key? user_id
352-
@logger.log(Logger::DEBUG, "Variation mapped to experiment '#{experiment_key}' has been removed for user "\
353-
"'#{user_id}'.")
354-
return true
355-
end
356-
357-
variation_id = get_variation_id_from_key(experiment_key, variation_key)
358-
359-
# check if the variation exists in the datafile
360-
unless variation_id
361-
# this case is logged in get_variation_id_from_key
362-
return false
363-
end
364-
365-
@forced_variation_map[user_id] = {} unless @forced_variation_map.key? user_id
366-
@forced_variation_map[user_id][experiment_id] = variation_id
367-
@logger.log(Logger::DEBUG, "Set variation '#{variation_id}' for experiment '#{experiment_id}' and "\
368-
"user '#{user_id}' in the forced variation map.")
369-
true
370-
end
371-
372276
def get_attribute_id(attribute_key)
373277
# Get attribute ID for the provided attribute key.
374278
#

spec/decision_service_spec.rb

Lines changed: 88 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
end
4141

4242
it 'should return the correct variation ID for a given user for whom a variation has been forced' do
43-
config.set_forced_variation('test_experiment', 'test_user', 'variation')
43+
decision_service.set_forced_variation(config, 'test_experiment', 'test_user', 'variation')
4444
expect(decision_service.get_variation(config, 'test_experiment', 'test_user')).to eq('111129')
4545
# Setting forced variation should short circuit whitelist check, bucketing and audience evaluation
4646
expect(decision_service).not_to have_received(:get_whitelisted_variation_id)
@@ -53,7 +53,7 @@
5353
'browser_type' => 'firefox',
5454
Optimizely::Helpers::Constants::CONTROL_ATTRIBUTES['BUCKETING_ID'] => 'pid'
5555
}
56-
config.set_forced_variation('test_experiment_with_audience', 'test_user', 'control_with_audience')
56+
decision_service.set_forced_variation(config, 'test_experiment_with_audience', 'test_user', 'control_with_audience')
5757
expect(decision_service.get_variation(config, 'test_experiment_with_audience', 'test_user', user_attributes)).to eq('122228')
5858
# Setting forced variation should short circuit whitelist check, bucketing and audience evaluation
5959
expect(decision_service).not_to have_received(:get_whitelisted_variation_id)
@@ -713,4 +713,90 @@
713713
expect(spy_logger).not_to have_received(:log)
714714
end
715715
end
716+
717+
# Only those log messages have been asserted, which are directly logged in these methods.
718+
# Messages that are logged in some internal function calls, are asserted in their respective function test cases.
719+
describe 'get_forced_variation' do
720+
user_id = 'test_user'
721+
invalid_experiment_key = 'invalid_experiment'
722+
valid_experiment = {id: '111127', key: 'test_experiment'}
723+
724+
# User ID is not defined in the forced variation map
725+
it 'should log a message and return nil when user is not in forced variation map' do
726+
expect(decision_service.get_forced_variation(config, valid_experiment[:key], user_id)).to eq(nil)
727+
expect(spy_logger).to have_received(:log).with(Logger::DEBUG,
728+
"User '#{user_id}' is not in the forced variation map.")
729+
end
730+
# Experiment key does not exist in the datafile
731+
it 'should return nil when experiment key is not in datafile' do
732+
expect(decision_service.get_forced_variation(config, invalid_experiment_key, user_id)).to eq(nil)
733+
end
734+
end
735+
736+
# Only those log messages have been asserted, which are directly logged in these methods.
737+
# Messages that are logged in some internal function calls, are asserted in their respective function test cases.
738+
describe 'set_forced_variation' do
739+
user_id = 'test_user'
740+
invalid_experiment_key = 'invalid_experiment'
741+
invalid_variation_key = 'invalid_variation'
742+
valid_experiment = {id: '111127', key: 'test_experiment'}
743+
valid_variation = {id: '111128', key: 'control'}
744+
745+
# Experiment key does not exist in the datafile
746+
it 'return nil when experiment key is not in datafile' do
747+
expect(decision_service.set_forced_variation(config, invalid_experiment_key, user_id, valid_variation[:key])).to eq(false)
748+
end
749+
# Variation key does not exist in the datafile
750+
it 'return false when variation_key is not in datafile' do
751+
expect(decision_service.set_forced_variation(config, valid_experiment[:key], user_id, invalid_variation_key)).to eq(false)
752+
end
753+
end
754+
755+
describe 'set/get forced variations multiple calls' do
756+
user_id = 'test_user'
757+
user_id_2 = 'test_user_2'
758+
valid_experiment = {id: '111127', key: 'test_experiment'}
759+
valid_variation = {id: '111128', key: 'control'}
760+
valid_variation_2 = {id: '111129', key: 'variation'}
761+
valid_experiment_2 = {id: '122227', key: 'test_experiment_with_audience'}
762+
valid_variation_for_exp_2 = {id: '122228', key: 'control_with_audience'}
763+
# Call set variation with different variations on one user/experiment to confirm that each set is expected.
764+
it 'should set and return expected variations when different variations are set and removed for one user/experiment' do
765+
expect(decision_service.set_forced_variation(config, valid_experiment[:key], user_id, valid_variation[:key])).to eq(true)
766+
variation = decision_service.get_forced_variation(config, valid_experiment[:key], user_id)
767+
expect(variation['id']).to eq(valid_variation[:id])
768+
expect(variation['key']).to eq(valid_variation[:key])
769+
770+
expect(decision_service.set_forced_variation(config, valid_experiment[:key], user_id, valid_variation_2[:key])).to eq(true)
771+
variation = decision_service.get_forced_variation(config, valid_experiment[:key], user_id)
772+
expect(variation['id']).to eq(valid_variation_2[:id])
773+
expect(variation['key']).to eq(valid_variation_2[:key])
774+
end
775+
776+
# Set variation on multiple experiments for one user.
777+
it 'should set and return expected variations when variation is set for multiple experiments for one user' do
778+
expect(decision_service.set_forced_variation(config, valid_experiment[:key], user_id, valid_variation[:key])).to eq(true)
779+
variation = decision_service.get_forced_variation(config, valid_experiment[:key], user_id)
780+
expect(variation['id']).to eq(valid_variation[:id])
781+
expect(variation['key']).to eq(valid_variation[:key])
782+
783+
expect(decision_service.set_forced_variation(config, valid_experiment_2[:key], user_id, valid_variation_for_exp_2[:key])).to eq(true)
784+
variation = decision_service.get_forced_variation(config, valid_experiment_2[:key], user_id)
785+
expect(variation['id']).to eq(valid_variation_for_exp_2[:id])
786+
expect(variation['key']).to eq(valid_variation_for_exp_2[:key])
787+
end
788+
789+
# Set variations for multiple users.
790+
it 'should set and return expected variations when variations are set for multiple users' do
791+
expect(decision_service.set_forced_variation(config, valid_experiment[:key], user_id, valid_variation[:key])).to eq(true)
792+
variation = decision_service.get_forced_variation(config, valid_experiment[:key], user_id)
793+
expect(variation['id']).to eq(valid_variation[:id])
794+
expect(variation['key']).to eq(valid_variation[:key])
795+
796+
expect(decision_service.set_forced_variation(config, valid_experiment[:key], user_id_2, valid_variation[:key])).to eq(true)
797+
variation = decision_service.get_forced_variation(config, valid_experiment[:key], user_id_2)
798+
expect(variation['id']).to eq(valid_variation[:id])
799+
expect(variation['key']).to eq(valid_variation[:key])
800+
end
801+
end
716802
end

0 commit comments

Comments
 (0)