Skip to content

Commit 3a30a00

Browse files
authored
feat(OptimizelyConfig): Add new fields to OptimizelyConfig (#285)
## Summary The following new public properties are added to OptimizelyConfig: - sdkKey - environmentKey - attributes - audiences - events - experimentRules and deliveryRules to OptimizelyFeature - audiences to OptimizelyExperiment ## Test plan All FSC tests OPTIMIZELY_CONFIG_V2 tests should pass. https://travis-ci.com/github/optimizely/fullstack-sdk-compatibility-suite/builds/235036023
1 parent bc46127 commit 3a30a00

6 files changed

+1209
-31
lines changed

lib/optimizely/condition_tree_evaluator.rb

+2
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ module ConditionTreeEvaluator
2828
NOT_CONDITION => :not_evaluator
2929
}.freeze
3030

31+
OPERATORS = [AND_CONDITION, OR_CONDITION, NOT_CONDITION].freeze
32+
3133
module_function
3234

3335
def evaluate(conditions, leaf_evaluator)

lib/optimizely/config/datafile_project_config.rb

+12-2
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,8 @@ def initialize(datafile, logger, error_handler)
8787
@anonymize_ip = config.key?('anonymizeIP') ? config['anonymizeIP'] : false
8888
@bot_filtering = config['botFiltering']
8989
@revision = config['revision']
90-
@sdk_key = config.fetch('sdkKey', nil)
91-
@environment_key = config.fetch('environmentKey', nil)
90+
@sdk_key = config.fetch('sdkKey', '')
91+
@environment_key = config.fetch('environmentKey', '')
9292
@rollouts = config.fetch('rollouts', [])
9393
@send_flag_decisions = config.fetch('sendFlagDecisions', false)
9494

@@ -482,6 +482,16 @@ def feature_experiment?(experiment_id)
482482
@experiment_feature_map.key?(experiment_id)
483483
end
484484

485+
def rollout_experiment?(experiment_id)
486+
# Determines if given experiment is a rollout test.
487+
#
488+
# experiment_id - String experiment ID
489+
#
490+
# Returns true if experiment belongs to any rollout,
491+
# false otherwise.
492+
@rollout_experiment_id_map.key?(experiment_id)
493+
end
494+
485495
private
486496

487497
def generate_key_map(array, key)

lib/optimizely/optimizely_config.rb

+177-25
Original file line numberDiff line numberDiff line change
@@ -16,56 +16,109 @@
1616
#
1717

1818
module Optimizely
19+
require 'json'
1920
class OptimizelyConfig
21+
include Optimizely::ConditionTreeEvaluator
2022
def initialize(project_config)
2123
@project_config = project_config
24+
@rollouts = @project_config.rollouts
25+
@audiences = []
26+
audience_id_lookup_dict = {}
27+
28+
@project_config.typed_audiences.each do |typed_audience|
29+
@audiences.push(
30+
'id' => typed_audience['id'],
31+
'name' => typed_audience['name'],
32+
'conditions' => typed_audience['conditions'].to_json
33+
)
34+
audience_id_lookup_dict[typed_audience['id']] = typed_audience['id']
35+
end
36+
37+
@project_config.audiences.each do |audience|
38+
next unless !audience_id_lookup_dict.key?(audience['id']) && (audience['id'] != '$opt_dummy_audience')
39+
40+
@audiences.push(
41+
'id' => audience['id'],
42+
'name' => audience['name'],
43+
'conditions' => audience['conditions']
44+
)
45+
end
2246
end
2347

2448
def config
2549
experiments_map_object = experiments_map
26-
features_map = get_features_map(experiments_map_object)
50+
features_map = get_features_map(experiments_id_map)
2751
config = {
52+
'sdkKey' => @project_config.sdk_key,
2853
'datafile' => @project_config.datafile,
54+
# This experimentsMap is for experiments of legacy projects only.
55+
# For flag projects, experiment keys are not guaranteed to be unique
56+
# across multiple flags, so this map may not include all experiments
57+
# when keys conflict. Use experimentRules and deliveryRules instead.
2958
'experimentsMap' => experiments_map_object,
3059
'featuresMap' => features_map,
31-
'revision' => @project_config.revision
60+
'revision' => @project_config.revision,
61+
'attributes' => get_attributes_list(@project_config.attributes),
62+
'audiences' => @audiences,
63+
'events' => get_events_list(@project_config.events),
64+
'environmentKey' => @project_config.environment_key
3265
}
33-
config['sdkKey'] = @project_config.sdk_key if @project_config.sdk_key
34-
config['environmentKey'] = @project_config.environment_key if @project_config.environment_key
3566
config
3667
end
3768

3869
private
3970

40-
def experiments_map
41-
feature_variables_map = @project_config.feature_flags.reduce({}) do |result_map, feature|
42-
result_map.update(feature['id'] => feature['variables'])
43-
end
71+
def experiments_id_map
72+
feature_variables_map = feature_variable_map
73+
audiences_id_map = audiences_map
4474
@project_config.experiments.reduce({}) do |experiments_map, experiment|
75+
feature_id = @project_config.experiment_feature_map.fetch(experiment['id'], []).first
4576
experiments_map.update(
46-
experiment['key'] => {
77+
experiment['id'] => {
4778
'id' => experiment['id'],
4879
'key' => experiment['key'],
49-
'variationsMap' => experiment['variations'].reduce({}) do |variations_map, variation|
50-
variation_object = {
51-
'id' => variation['id'],
52-
'key' => variation['key'],
53-
'variablesMap' => get_merged_variables_map(variation, experiment['id'], feature_variables_map)
54-
}
55-
variation_object['featureEnabled'] = variation['featureEnabled'] if @project_config.feature_experiment?(experiment['id'])
56-
variations_map.update(variation['key'] => variation_object)
57-
end
80+
'variationsMap' => get_variation_map(feature_id, experiment, feature_variables_map),
81+
'audiences' => replace_ids_with_names(experiment.fetch('audienceConditions', []), audiences_id_map) || ''
5882
}
5983
)
6084
end
6185
end
6286

87+
def audiences_map
88+
@audiences.reduce({}) do |audiences_map, optly_audience|
89+
audiences_map.update(optly_audience['id'] => optly_audience['name'])
90+
end
91+
end
92+
93+
def experiments_map
94+
experiments_id_map.values.reduce({}) do |experiments_key_map, experiment|
95+
experiments_key_map.update(experiment['key'] => experiment)
96+
end
97+
end
98+
99+
def feature_variable_map
100+
@project_config.feature_flags.reduce({}) do |result_map, feature|
101+
result_map.update(feature['id'] => feature['variables'])
102+
end
103+
end
104+
105+
def get_variation_map(feature_id, experiment, feature_variables_map)
106+
experiment['variations'].reduce({}) do |variations_map, variation|
107+
variation_object = {
108+
'id' => variation['id'],
109+
'key' => variation['key'],
110+
'featureEnabled' => variation['featureEnabled'],
111+
'variablesMap' => get_merged_variables_map(variation, feature_id, feature_variables_map)
112+
}
113+
variations_map.update(variation['key'] => variation_object)
114+
end
115+
end
116+
63117
# Merges feature key and type from feature variables to variation variables.
64-
def get_merged_variables_map(variation, experiment_id, feature_variables_map)
65-
feature_ids = @project_config.experiment_feature_map[experiment_id]
66-
return {} unless feature_ids
118+
def get_merged_variables_map(variation, feature_id, feature_variables_map)
119+
return {} unless feature_id
67120

68-
experiment_feature_variables = feature_variables_map[feature_ids[0]]
121+
feature_variables = feature_variables_map[feature_id]
69122
# temporary variation variables map to get values to merge.
70123
temp_variables_id_map = {}
71124
if variation['variables']
@@ -78,7 +131,7 @@ def get_merged_variables_map(variation, experiment_id, feature_variables_map)
78131
)
79132
end
80133
end
81-
experiment_feature_variables.reduce({}) do |variables_map, feature_variable|
134+
feature_variables.reduce({}) do |variables_map, feature_variable|
82135
variation_variable = temp_variables_id_map[feature_variable['id']]
83136
variable_value = variation['featureEnabled'] && variation_variable ? variation_variable['value'] : feature_variable['defaultValue']
84137
variables_map.update(
@@ -94,13 +147,15 @@ def get_merged_variables_map(variation, experiment_id, feature_variables_map)
94147

95148
def get_features_map(all_experiments_map)
96149
@project_config.feature_flags.reduce({}) do |features_map, feature|
150+
delivery_rules = get_delivery_rules(@rollouts, feature['rolloutId'], feature['id'])
97151
features_map.update(
98152
feature['key'] => {
99153
'id' => feature['id'],
100154
'key' => feature['key'],
155+
# This experimentsMap is deprecated. Use experimentRules and deliveryRules instead.
101156
'experimentsMap' => feature['experimentIds'].reduce({}) do |experiments_map, experiment_id|
102157
experiment_key = @project_config.experiment_id_map[experiment_id]['key']
103-
experiments_map.update(experiment_key => all_experiments_map[experiment_key])
158+
experiments_map.update(experiment_key => experiments_id_map[experiment_id])
104159
end,
105160
'variablesMap' => feature['variables'].reduce({}) do |variables, variable|
106161
variables.update(
@@ -111,10 +166,107 @@ def get_features_map(all_experiments_map)
111166
'value' => variable['defaultValue']
112167
}
113168
)
114-
end
169+
end,
170+
'experimentRules' => feature['experimentIds'].reduce([]) do |experiments_map, experiment_id|
171+
experiments_map.push(all_experiments_map[experiment_id])
172+
end,
173+
'deliveryRules' => delivery_rules
115174
}
116175
)
117176
end
118177
end
178+
179+
def get_attributes_list(attributes)
180+
attributes.map do |attribute|
181+
{
182+
'id' => attribute['id'],
183+
'key' => attribute['key']
184+
}
185+
end
186+
end
187+
188+
def get_events_list(events)
189+
events.map do |event|
190+
{
191+
'id' => event['id'],
192+
'key' => event['key'],
193+
'experimentIds' => event['experimentIds']
194+
}
195+
end
196+
end
197+
198+
def lookup_name_from_id(audience_id, audiences_map)
199+
audiences_map[audience_id] || audience_id
200+
end
201+
202+
def stringify_conditions(conditions, audiences_map)
203+
operand = 'OR'
204+
conditions_str = ''
205+
length = conditions.length()
206+
return '' if length.zero?
207+
return '"' + lookup_name_from_id(conditions[0], audiences_map) + '"' if length == 1 && !OPERATORS.include?(conditions[0])
208+
209+
# Edge cases for lengths 0, 1 or 2
210+
if length == 2 && OPERATORS.include?(conditions[0]) && !conditions[1].is_a?(Array) && !OPERATORS.include?(conditions[1])
211+
return '"' + lookup_name_from_id(conditions[1], audiences_map) + '"' if conditions[0] != 'not'
212+
213+
return conditions[0].upcase + ' "' + lookup_name_from_id(conditions[1], audiences_map) + '"'
214+
215+
end
216+
if length > 1
217+
(0..length - 1).each do |n|
218+
# Operand is handled here and made Upper Case
219+
if OPERATORS.include?(conditions[n])
220+
operand = conditions[n].upcase
221+
# Check if element is a list or not
222+
elsif conditions[n].is_a?(Array)
223+
# Check if at the end or not to determine where to add the operand
224+
# Recursive call to call stringify on embedded list
225+
conditions_str += if n + 1 < length
226+
'(' + stringify_conditions(conditions[n], audiences_map) + ') '
227+
else
228+
operand + ' (' + stringify_conditions(conditions[n], audiences_map) + ')'
229+
end
230+
# If the item is not a list, we process as an audience ID and retrieve the name
231+
else
232+
audience_name = lookup_name_from_id(conditions[n], audiences_map)
233+
unless audience_name.nil?
234+
# Below handles all cases for one ID or greater
235+
conditions_str += if n + 1 < length - 1
236+
'"' + audience_name + '" ' + operand + ' '
237+
elsif n + 1 == length
238+
operand + ' "' + audience_name + '"'
239+
else
240+
'"' + audience_name + '" '
241+
end
242+
end
243+
end
244+
end
245+
end
246+
conditions_str || ''
247+
end
248+
249+
def replace_ids_with_names(conditions, audiences_map)
250+
!conditions.empty? ? stringify_conditions(conditions, audiences_map) : ''
251+
end
252+
253+
def get_delivery_rules(rollouts, rollout_id, feature_id)
254+
audiences_id_map = audiences_map
255+
feature_variables_map = feature_variable_map
256+
rollout = rollouts.select { |selected_rollout| selected_rollout['id'] == rollout_id }
257+
if rollout.any?
258+
rollout = rollout[0]
259+
experiments = rollout['experiments']
260+
return experiments.map do |experiment|
261+
{
262+
'id' => experiment['id'],
263+
'key' => experiment['key'],
264+
'variationsMap' => get_variation_map(feature_id, experiment, feature_variables_map),
265+
'audiences' => replace_ids_with_names(experiment.fetch('audienceConditions', []), audiences_id_map) || ''
266+
}
267+
end
268+
end
269+
[]
270+
end
119271
end
120272
end

spec/config/datafile_project_config_spec.rb

+12
Original file line numberDiff line numberDiff line change
@@ -1061,4 +1061,16 @@
10611061
expect(config.feature_experiment?(experiment['id'])).to eq(false)
10621062
end
10631063
end
1064+
1065+
describe '#rollout_experiment' do
1066+
let(:config) { Optimizely::DatafileProjectConfig.new(config_body_JSON, logger, error_handler) }
1067+
1068+
it 'should return true if the experiment is a rollout test' do
1069+
expect(config.rollout_experiment?('177770')).to eq(true)
1070+
end
1071+
1072+
it 'should return false if the experiment is not a rollout test' do
1073+
expect(config.rollout_experiment?('177771')).to eq(false)
1074+
end
1075+
end
10641076
end

0 commit comments

Comments
 (0)