Skip to content

Commit c6129f7

Browse files
ozayr-zaviarzashraf1985
authored andcommitted
feat: Duplicate experiment key issue with multi feature flag (#282)
1 parent b394e9c commit c6129f7

7 files changed

+243
-103
lines changed

lib/optimizely.rb

+9-6
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# frozen_string_literal: true
22

33
#
4-
# Copyright 2016-2020, Optimizely and contributors
4+
# Copyright 2016-2021, Optimizely and contributors
55
#
66
# Licensed under the Apache License, Version 2.0 (the "License");
77
# you may not use this file except in compliance with the License.
@@ -877,12 +877,14 @@ def get_variation_with_config(experiment_key, user_id, attributes, config)
877877
experiment = config.get_experiment_from_key(experiment_key)
878878
return nil if experiment.nil?
879879

880+
experiment_id = experiment['id']
881+
880882
return nil unless user_inputs_valid?(attributes)
881883

882-
variation_id, = @decision_service.get_variation(config, experiment_key, user_id, attributes)
884+
variation_id, = @decision_service.get_variation(config, experiment_id, user_id, attributes)
883885
variation = config.get_variation_from_id(experiment_key, variation_id) unless variation_id.nil?
884886
variation_key = variation['key'] if variation
885-
decision_notification_type = if config.feature_experiment?(experiment['id'])
887+
decision_notification_type = if config.feature_experiment?(experiment_id)
886888
Helpers::Constants::DECISION_NOTIFICATION_TYPES['FEATURE_TEST']
887889
else
888890
Helpers::Constants::DECISION_NOTIFICATION_TYPES['AB_TEST']
@@ -1078,10 +1080,11 @@ def send_impression(config, experiment, variation_key, flag_key, rule_key, enabl
10781080
}
10791081
end
10801082

1083+
experiment_id = experiment['id']
10811084
experiment_key = experiment['key']
10821085

10831086
variation_id = ''
1084-
variation_id = config.get_variation_id_from_key(experiment_key, variation_key) if experiment_key != ''
1087+
variation_id = config.get_variation_id_from_key_by_experiment_id(experiment_id, variation_key) if experiment_id != ''
10851088

10861089
metadata = {
10871090
flag_key: flag_key,
@@ -1097,9 +1100,9 @@ def send_impression(config, experiment, variation_key, flag_key, rule_key, enabl
10971100

10981101
@logger.log(Logger::INFO, "Activating user '#{user_id}' in experiment '#{experiment_key}'.")
10991102

1100-
experiment = nil if experiment_key == ''
1103+
experiment = nil if experiment_id == ''
11011104
variation = nil
1102-
variation = config.get_variation_from_id(experiment_key, variation_id) unless experiment.nil?
1105+
variation = config.get_variation_from_id_by_experiment_id(experiment_id, variation_id) unless experiment.nil?
11031106
log_event = EventFactory.create_log_event(user_event, @logger)
11041107
@notification_center.send_notifications(
11051108
NotificationCenter::NOTIFICATION_TYPES[:ACTIVATE],

lib/optimizely/bucketer.rb

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# frozen_string_literal: true
22

33
#
4-
# Copyright 2016-2017, 2019-2020 Optimizely and contributors
4+
# Copyright 2016-2017, 2019-2021 Optimizely and contributors
55
#
66
# Licensed under the Apache License, Version 2.0 (the "License");
77
# you may not use this file except in compliance with the License.
@@ -88,7 +88,7 @@ def bucket(project_config, experiment, bucketing_id, user_id)
8888
decide_reasons.push(*find_bucket_reasons)
8989

9090
if variation_id && variation_id != ''
91-
variation = project_config.get_variation_from_id(experiment_key, variation_id)
91+
variation = project_config.get_variation_from_id_by_experiment_id(experiment_id, variation_id)
9292
return variation, decide_reasons
9393
end
9494

lib/optimizely/config/datafile_project_config.rb

+80-13
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,12 @@ class DatafileProjectConfig < ProjectConfig
5252
attr_reader :feature_variable_key_map
5353
attr_reader :group_id_map
5454
attr_reader :rollout_id_map
55-
attr_reader :rollout_experiment_key_map
55+
attr_reader :rollout_experiment_id_map
5656
attr_reader :variation_id_map
5757
attr_reader :variation_id_to_variable_usage_map
5858
attr_reader :variation_key_map
59+
attr_reader :variation_id_map_by_experiment_id
60+
attr_reader :variation_key_map_by_experiment_id
5961

6062
def initialize(datafile, logger, error_handler)
6163
# ProjectConfig init method to fetch and set project config data
@@ -113,9 +115,11 @@ def initialize(datafile, logger, error_handler)
113115
@audience_id_map = @audience_id_map.merge(generate_key_map(@typed_audiences, 'id')) unless @typed_audiences.empty?
114116
@variation_id_map = {}
115117
@variation_key_map = {}
118+
@variation_id_map_by_experiment_id = {}
119+
@variation_key_map_by_experiment_id = {}
116120
@variation_id_to_variable_usage_map = {}
117121
@variation_id_to_experiment_map = {}
118-
@experiment_key_map.each_value do |exp|
122+
@experiment_id_map.each_value do |exp|
119123
# Excludes experiments from rollouts
120124
variations = exp.fetch('variations')
121125
variations.each do |variation|
@@ -125,13 +129,13 @@ def initialize(datafile, logger, error_handler)
125129
end
126130
@rollout_id_map = generate_key_map(@rollouts, 'id')
127131
# split out the experiment key map for rollouts
128-
@rollout_experiment_key_map = {}
132+
@rollout_experiment_id_map = {}
129133
@rollout_id_map.each_value do |rollout|
130134
exps = rollout.fetch('experiments')
131-
@rollout_experiment_key_map = @rollout_experiment_key_map.merge(generate_key_map(exps, 'key'))
135+
@rollout_experiment_id_map = @rollout_experiment_id_map.merge(generate_key_map(exps, 'id'))
132136
end
133-
@all_experiments = @experiment_key_map.merge(@rollout_experiment_key_map)
134-
@all_experiments.each do |key, exp|
137+
@all_experiments = @experiment_id_map.merge(@rollout_experiment_id_map)
138+
@all_experiments.each do |id, exp|
135139
variations = exp.fetch('variations')
136140
variations.each do |variation|
137141
variation_id = variation['id']
@@ -141,8 +145,10 @@ def initialize(datafile, logger, error_handler)
141145

142146
@variation_id_to_variable_usage_map[variation_id] = generate_key_map(variation_variables, 'id')
143147
end
144-
@variation_id_map[key] = generate_key_map(variations, 'id')
145-
@variation_key_map[key] = generate_key_map(variations, 'key')
148+
@variation_id_map[exp['key']] = generate_key_map(variations, 'id')
149+
@variation_key_map[exp['key']] = generate_key_map(variations, 'key')
150+
@variation_id_map_by_experiment_id[id] = generate_key_map(variations, 'id')
151+
@variation_key_map_by_experiment_id[id] = generate_key_map(variations, 'key')
146152
end
147153
@feature_flag_key_map = generate_key_map(@feature_flags, 'key')
148154
@experiment_feature_map = {}
@@ -209,6 +215,21 @@ def get_experiment_from_key(experiment_key)
209215
nil
210216
end
211217

218+
def get_experiment_from_id(experiment_id)
219+
# Retrieves experiment ID for a given key
220+
#
221+
# experiment_id - String id representing the experiment
222+
#
223+
# Returns Experiment or nil if not found
224+
225+
experiment = @experiment_id_map[experiment_id]
226+
return experiment if experiment
227+
228+
@logger.log Logger::ERROR, "Experiment id '#{experiment_id}' is not in datafile."
229+
@error_handler.handle_error InvalidExperimentError
230+
nil
231+
end
232+
212233
def get_experiment_key(experiment_id)
213234
# Retrieves experiment key for a given ID.
214235
#
@@ -277,6 +298,52 @@ def get_variation_from_id(experiment_key, variation_id)
277298
nil
278299
end
279300

301+
def get_variation_from_id_by_experiment_id(experiment_id, variation_id)
302+
# Get variation given experiment ID and variation ID
303+
#
304+
# experiment_id - ID representing parent experiment of variation
305+
# variation_id - ID of the variation
306+
#
307+
# Returns the variation or nil if not found
308+
309+
variation_id_map_by_experiment_id = @variation_id_map_by_experiment_id[experiment_id]
310+
if variation_id_map_by_experiment_id
311+
variation = variation_id_map_by_experiment_id[variation_id]
312+
return variation if variation
313+
314+
@logger.log Logger::ERROR, "Variation id '#{variation_id}' is not in datafile."
315+
@error_handler.handle_error InvalidVariationError
316+
return nil
317+
end
318+
319+
@logger.log Logger::ERROR, "Experiment id '#{experiment_id}' is not in datafile."
320+
@error_handler.handle_error InvalidExperimentError
321+
nil
322+
end
323+
324+
def get_variation_id_from_key_by_experiment_id(experiment_id, variation_key)
325+
# Get variation given experiment ID and variation key
326+
#
327+
# experiment_id - ID representing parent experiment of variation
328+
# variation_key - Key of the variation
329+
#
330+
# Returns the variation or nil if not found
331+
332+
variation_key_map = @variation_key_map_by_experiment_id[experiment_id]
333+
if variation_key_map
334+
variation = variation_key_map[variation_key]
335+
return variation['id'] if variation
336+
337+
@logger.log Logger::ERROR, "Variation key '#{variation_key}' is not in datafile."
338+
@error_handler.handle_error InvalidVariationError
339+
return nil
340+
end
341+
342+
@logger.log Logger::ERROR, "Experiment id '#{experiment_id}' is not in datafile."
343+
@error_handler.handle_error InvalidExperimentError
344+
nil
345+
end
346+
280347
def get_variation_id_from_key(experiment_key, variation_key)
281348
# Get variation ID given experiment key and variation key
282349
#
@@ -300,17 +367,17 @@ def get_variation_id_from_key(experiment_key, variation_key)
300367
nil
301368
end
302369

303-
def get_whitelisted_variations(experiment_key)
304-
# Retrieves whitelisted variations for a given experiment Key
370+
def get_whitelisted_variations(experiment_id)
371+
# Retrieves whitelisted variations for a given experiment id
305372
#
306-
# experiment_key - String Key representing the experiment
373+
# experiment_id - String id representing the experiment
307374
#
308375
# Returns whitelisted variations for the experiment or nil
309376

310-
experiment = @experiment_key_map[experiment_key]
377+
experiment = @experiment_id_map[experiment_id]
311378
return experiment['forcedVariations'] if experiment
312379

313-
@logger.log Logger::ERROR, "Experiment key '#{experiment_key}' is not in datafile."
380+
@logger.log Logger::ERROR, "Experiment ID '#{experiment_id}' is not in datafile."
314381
@error_handler.handle_error InvalidExperimentError
315382
end
316383

lib/optimizely/decision_service.rb

+20-20
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# frozen_string_literal: true
22

33
#
4-
# Copyright 2017-2020, Optimizely and contributors
4+
# Copyright 2017-2021, Optimizely and contributors
55
#
66
# Licensed under the Apache License, Version 2.0 (the "License");
77
# you may not use this file except in compliance with the License.
@@ -52,11 +52,11 @@ def initialize(logger, user_profile_service = nil)
5252
@forced_variation_map = {}
5353
end
5454

55-
def get_variation(project_config, experiment_key, user_id, attributes = nil, decide_options = [])
55+
def get_variation(project_config, experiment_id, user_id, attributes = nil, decide_options = [])
5656
# Determines variation into which user will be bucketed.
5757
#
5858
# project_config - project_config - Instance of ProjectConfig
59-
# experiment_key - Experiment for which visitor variation needs to be determined
59+
# experiment_id - Experiment for which visitor variation needs to be determined
6060
# user_id - String ID for user
6161
# attributes - Hash representing user attributes
6262
#
@@ -68,10 +68,10 @@ def get_variation(project_config, experiment_key, user_id, attributes = nil, dec
6868
bucketing_id, bucketing_id_reasons = get_bucketing_id(user_id, attributes)
6969
decide_reasons.push(*bucketing_id_reasons)
7070
# Check to make sure experiment is active
71-
experiment = project_config.get_experiment_from_key(experiment_key)
71+
experiment = project_config.get_experiment_from_id(experiment_id)
7272
return nil, decide_reasons if experiment.nil?
7373

74-
experiment_id = experiment['id']
74+
experiment_key = experiment['key']
7575
unless project_config.experiment_running?(experiment)
7676
message = "Experiment '#{experiment_key}' is not running."
7777
@logger.log(Logger::INFO, message)
@@ -80,12 +80,12 @@ def get_variation(project_config, experiment_key, user_id, attributes = nil, dec
8080
end
8181

8282
# Check if a forced variation is set for the user
83-
forced_variation, reasons_received = get_forced_variation(project_config, experiment_key, user_id)
83+
forced_variation, reasons_received = get_forced_variation(project_config, experiment['key'], user_id)
8484
decide_reasons.push(*reasons_received)
8585
return forced_variation['id'], decide_reasons if forced_variation
8686

8787
# Check if user is in a white-listed variation
88-
whitelisted_variation_id, reasons_received = get_whitelisted_variation_id(project_config, experiment_key, user_id)
88+
whitelisted_variation_id, reasons_received = get_whitelisted_variation_id(project_config, experiment_id, user_id)
8989
decide_reasons.push(*reasons_received)
9090
return whitelisted_variation_id, decide_reasons if whitelisted_variation_id
9191

@@ -122,7 +122,7 @@ def get_variation(project_config, experiment_key, user_id, attributes = nil, dec
122122
message = ''
123123
if variation_id
124124
variation_key = variation['key']
125-
message = "User '#{user_id}' is in variation '#{variation_key}' of experiment '#{experiment_key}'."
125+
message = "User '#{user_id}' is in variation '#{variation_key}' of experiment '#{experiment_id}'."
126126
else
127127
message = "User '#{user_id}' is in no variation."
128128
end
@@ -186,13 +186,13 @@ def get_variation_for_feature_experiment(project_config, feature_flag, user_id,
186186
return nil, decide_reasons
187187
end
188188

189-
experiment_key = experiment['key']
190-
variation_id, reasons_received = get_variation(project_config, experiment_key, user_id, attributes, decide_options)
189+
experiment_id = experiment['id']
190+
variation_id, reasons_received = get_variation(project_config, experiment_id, user_id, attributes, decide_options)
191191
decide_reasons.push(*reasons_received)
192192

193193
next unless variation_id
194194

195-
variation = project_config.variation_id_map[experiment_key][variation_id]
195+
variation = project_config.get_variation_from_id_by_experiment_id(experiment_id, variation_id)
196196

197197
return Decision.new(experiment, variation, DECISION_SOURCES['FEATURE_TEST']), decide_reasons
198198
end
@@ -315,7 +315,7 @@ def set_forced_variation(project_config, experiment_key, user_id, variation_key)
315315
return true
316316
end
317317

318-
variation_id = project_config.get_variation_id_from_key(experiment_key, variation_key)
318+
variation_id = project_config.get_variation_id_from_key_by_experiment_id(experiment_id, variation_key)
319319

320320
# check if the variation exists in the datafile
321321
unless variation_id
@@ -334,7 +334,7 @@ def get_forced_variation(project_config, experiment_key, user_id)
334334
# Gets the forced variation for the given user and experiment.
335335
#
336336
# project_config - Instance of ProjectConfig
337-
# experiment_key - String Key for experiment
337+
# experiment_key - String key for experiment
338338
# user_id - String ID for user
339339
#
340340
# Returns Variation The variation which the given user and experiment should be forced into
@@ -354,22 +354,22 @@ def get_forced_variation(project_config, experiment_key, user_id)
354354
return nil, decide_reasons if experiment_id.nil? || experiment_id.empty?
355355

356356
unless experiment_to_variation_map.key? experiment_id
357-
message = "No experiment '#{experiment_key}' mapped to user '#{user_id}' in the forced variation map."
357+
message = "No experiment '#{experiment_id}' mapped to user '#{user_id}' in the forced variation map."
358358
@logger.log(Logger::DEBUG, message)
359359
decide_reasons.push(message)
360360
return nil, decide_reasons
361361
end
362362

363363
variation_id = experiment_to_variation_map[experiment_id]
364364
variation_key = ''
365-
variation = project_config.get_variation_from_id(experiment_key, variation_id)
365+
variation = project_config.get_variation_from_id_by_experiment_id(experiment_id, variation_id)
366366
variation_key = variation['key'] if variation
367367

368368
# check if the variation exists in the datafile
369369
# this case is logged in get_variation_from_id
370370
return nil, decide_reasons if variation_key.empty?
371371

372-
message = "Variation '#{variation_key}' is mapped to experiment '#{experiment_key}' and user '#{user_id}' in the forced variation map"
372+
message = "Variation '#{variation_key}' is mapped to experiment '#{experiment_id}' and user '#{user_id}' in the forced variation map"
373373
@logger.log(Logger::DEBUG, message)
374374
decide_reasons.push(message)
375375

@@ -378,7 +378,7 @@ def get_forced_variation(project_config, experiment_key, user_id)
378378

379379
private
380380

381-
def get_whitelisted_variation_id(project_config, experiment_key, user_id)
381+
def get_whitelisted_variation_id(project_config, experiment_id, user_id)
382382
# Determine if a user is whitelisted into a variation for the given experiment and return the ID of that variation
383383
#
384384
# project_config - project_config - Instance of ProjectConfig
@@ -387,23 +387,23 @@ def get_whitelisted_variation_id(project_config, experiment_key, user_id)
387387
#
388388
# Returns variation ID into which user_id is whitelisted (nil if no variation)
389389

390-
whitelisted_variations = project_config.get_whitelisted_variations(experiment_key)
390+
whitelisted_variations = project_config.get_whitelisted_variations(experiment_id)
391391

392392
return nil, nil unless whitelisted_variations
393393

394394
whitelisted_variation_key = whitelisted_variations[user_id]
395395

396396
return nil, nil unless whitelisted_variation_key
397397

398-
whitelisted_variation_id = project_config.get_variation_id_from_key(experiment_key, whitelisted_variation_key)
398+
whitelisted_variation_id = project_config.get_variation_id_from_key_by_experiment_id(experiment_id, whitelisted_variation_key)
399399

400400
unless whitelisted_variation_id
401401
message = "User '#{user_id}' is whitelisted into variation '#{whitelisted_variation_key}', which is not in the datafile."
402402
@logger.log(Logger::INFO, message)
403403
return nil, message
404404
end
405405

406-
message = "User '#{user_id}' is whitelisted into variation '#{whitelisted_variation_key}' of experiment '#{experiment_key}'."
406+
message = "User '#{user_id}' is whitelisted into variation '#{whitelisted_variation_key}' of experiment '#{experiment_id}'."
407407
@logger.log(Logger::INFO, message)
408408

409409
[whitelisted_variation_id, message]

0 commit comments

Comments
 (0)