optimizely-sdk 3.1.1 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/optimizely.rb +169 -94
- data/lib/optimizely/audience.rb +7 -7
- data/lib/optimizely/bucketer.rb +16 -17
- data/lib/optimizely/config/datafile_project_config.rb +411 -0
- data/lib/optimizely/config_manager/async_scheduler.rb +91 -0
- data/lib/optimizely/config_manager/http_project_config_manager.rb +282 -0
- data/lib/optimizely/config_manager/project_config_manager.rb +24 -0
- data/lib/optimizely/config_manager/static_project_config_manager.rb +43 -0
- data/lib/optimizely/decision_service.rb +142 -52
- data/lib/optimizely/event_builder.rb +21 -28
- data/lib/optimizely/exceptions.rb +17 -1
- data/lib/optimizely/helpers/constants.rb +19 -0
- data/lib/optimizely/notification_center.rb +1 -0
- data/lib/optimizely/optimizely_factory.rb +73 -0
- data/lib/optimizely/project_config.rb +29 -434
- data/lib/optimizely/version.rb +1 -1
- metadata +12 -6
data/lib/optimizely/audience.rb
CHANGED
@@ -24,7 +24,7 @@ module Optimizely
|
|
24
24
|
module Audience
|
25
25
|
module_function
|
26
26
|
|
27
|
-
def user_in_experiment?(config, experiment, attributes)
|
27
|
+
def user_in_experiment?(config, experiment, attributes, logger)
|
28
28
|
# Determine for given experiment if user satisfies the audiences for the experiment.
|
29
29
|
#
|
30
30
|
# config - Representation of the Optimizely project config.
|
@@ -36,7 +36,7 @@ module Optimizely
|
|
36
36
|
|
37
37
|
audience_conditions = experiment['audienceConditions'] || experiment['audienceIds']
|
38
38
|
|
39
|
-
|
39
|
+
logger.log(
|
40
40
|
Logger::DEBUG,
|
41
41
|
format(
|
42
42
|
Helpers::Constants::AUDIENCE_EVALUATION_LOGS['EVALUATING_AUDIENCES_COMBINED'],
|
@@ -47,7 +47,7 @@ module Optimizely
|
|
47
47
|
|
48
48
|
# Return true if there are no audiences
|
49
49
|
if audience_conditions.empty?
|
50
|
-
|
50
|
+
logger.log(
|
51
51
|
Logger::INFO,
|
52
52
|
format(
|
53
53
|
Helpers::Constants::AUDIENCE_EVALUATION_LOGS['AUDIENCE_EVALUATION_RESULT_COMBINED'],
|
@@ -60,7 +60,7 @@ module Optimizely
|
|
60
60
|
|
61
61
|
attributes ||= {}
|
62
62
|
|
63
|
-
custom_attr_condition_evaluator = CustomAttributeConditionEvaluator.new(attributes,
|
63
|
+
custom_attr_condition_evaluator = CustomAttributeConditionEvaluator.new(attributes, logger)
|
64
64
|
|
65
65
|
evaluate_custom_attr = lambda do |condition|
|
66
66
|
return custom_attr_condition_evaluator.evaluate(condition)
|
@@ -71,7 +71,7 @@ module Optimizely
|
|
71
71
|
return nil unless audience
|
72
72
|
|
73
73
|
audience_conditions = audience['conditions']
|
74
|
-
|
74
|
+
logger.log(
|
75
75
|
Logger::DEBUG,
|
76
76
|
format(
|
77
77
|
Helpers::Constants::AUDIENCE_EVALUATION_LOGS['EVALUATING_AUDIENCE'],
|
@@ -83,7 +83,7 @@ module Optimizely
|
|
83
83
|
audience_conditions = JSON.parse(audience_conditions) if audience_conditions.is_a?(String)
|
84
84
|
result = ConditionTreeEvaluator.evaluate(audience_conditions, evaluate_custom_attr)
|
85
85
|
result_str = result.nil? ? 'UNKNOWN' : result.to_s.upcase
|
86
|
-
|
86
|
+
logger.log(
|
87
87
|
Logger::INFO,
|
88
88
|
format(Helpers::Constants::AUDIENCE_EVALUATION_LOGS['AUDIENCE_EVALUATION_RESULT'], audience_id, result_str)
|
89
89
|
)
|
@@ -94,7 +94,7 @@ module Optimizely
|
|
94
94
|
|
95
95
|
eval_result ||= false
|
96
96
|
|
97
|
-
|
97
|
+
logger.log(
|
98
98
|
Logger::INFO,
|
99
99
|
format(
|
100
100
|
Helpers::Constants::AUDIENCE_EVALUATION_LOGS['AUDIENCE_EVALUATION_RESULT_COMBINED'],
|
data/lib/optimizely/bucketer.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
#
|
4
|
-
# Copyright 2016-2017, Optimizely and contributors
|
4
|
+
# Copyright 2016-2017, 2019 Optimizely and contributors
|
5
5
|
#
|
6
6
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
7
|
# you may not use this file except in compliance with the License.
|
@@ -28,18 +28,17 @@ module Optimizely
|
|
28
28
|
MAX_TRAFFIC_VALUE = 10_000
|
29
29
|
UNSIGNED_MAX_32_BIT_VALUE = 0xFFFFFFFF
|
30
30
|
|
31
|
-
def initialize(
|
32
|
-
# Bucketer init method to set bucketing seed and
|
33
|
-
#
|
34
|
-
|
35
|
-
|
31
|
+
def initialize(logger)
|
32
|
+
# Bucketer init method to set bucketing seed and logger.
|
33
|
+
# logger - Optional component which provides a log method to log messages.
|
34
|
+
@logger = logger
|
36
35
|
@bucket_seed = HASH_SEED
|
37
|
-
@config = config
|
38
36
|
end
|
39
37
|
|
40
|
-
def bucket(experiment, bucketing_id, user_id)
|
38
|
+
def bucket(project_config, experiment, bucketing_id, user_id)
|
41
39
|
# Determines ID of variation to be shown for a given experiment key and user ID.
|
42
40
|
#
|
41
|
+
# project_config - Instance of ProjectConfig
|
43
42
|
# experiment - Experiment for which visitor is to be bucketed.
|
44
43
|
# bucketing_id - String A customer-assigned value used to generate the bucketing key
|
45
44
|
# user_id - String ID for user.
|
@@ -52,19 +51,19 @@ module Optimizely
|
|
52
51
|
experiment_key = experiment['key']
|
53
52
|
group_id = experiment['groupId']
|
54
53
|
if group_id
|
55
|
-
group =
|
54
|
+
group = project_config.group_id_map.fetch(group_id)
|
56
55
|
if Helpers::Group.random_policy?(group)
|
57
56
|
traffic_allocations = group.fetch('trafficAllocation')
|
58
57
|
bucketed_experiment_id = find_bucket(bucketing_id, user_id, group_id, traffic_allocations)
|
59
58
|
# return if the user is not bucketed into any experiment
|
60
59
|
unless bucketed_experiment_id
|
61
|
-
@
|
60
|
+
@logger.log(Logger::INFO, "User '#{user_id}' is in no experiment.")
|
62
61
|
return nil
|
63
62
|
end
|
64
63
|
|
65
64
|
# return if the user is bucketed into a different experiment than the one specified
|
66
65
|
if bucketed_experiment_id != experiment_id
|
67
|
-
@
|
66
|
+
@logger.log(
|
68
67
|
Logger::INFO,
|
69
68
|
"User '#{user_id}' is not in experiment '#{experiment_key}' of group #{group_id}."
|
70
69
|
)
|
@@ -72,7 +71,7 @@ module Optimizely
|
|
72
71
|
end
|
73
72
|
|
74
73
|
# continue bucketing if the user is bucketed into the experiment specified
|
75
|
-
@
|
74
|
+
@logger.log(
|
76
75
|
Logger::INFO,
|
77
76
|
"User '#{user_id}' is in experiment '#{experiment_key}' of group #{group_id}."
|
78
77
|
)
|
@@ -82,9 +81,9 @@ module Optimizely
|
|
82
81
|
traffic_allocations = experiment['trafficAllocation']
|
83
82
|
variation_id = find_bucket(bucketing_id, user_id, experiment_id, traffic_allocations)
|
84
83
|
if variation_id && variation_id != ''
|
85
|
-
variation =
|
84
|
+
variation = project_config.get_variation_from_id(experiment_key, variation_id)
|
86
85
|
variation_key = variation ? variation['key'] : nil
|
87
|
-
@
|
86
|
+
@logger.log(
|
88
87
|
Logger::INFO,
|
89
88
|
"User '#{user_id}' is in variation '#{variation_key}' of experiment '#{experiment_key}'."
|
90
89
|
)
|
@@ -93,13 +92,13 @@ module Optimizely
|
|
93
92
|
|
94
93
|
# Handle the case when the traffic range is empty due to sticky bucketing
|
95
94
|
if variation_id == ''
|
96
|
-
@
|
95
|
+
@logger.log(
|
97
96
|
Logger::DEBUG,
|
98
97
|
'Bucketed into an empty traffic range. Returning nil.'
|
99
98
|
)
|
100
99
|
end
|
101
100
|
|
102
|
-
@
|
101
|
+
@logger.log(Logger::INFO, "User '#{user_id}' is in no variation.")
|
103
102
|
nil
|
104
103
|
end
|
105
104
|
|
@@ -114,7 +113,7 @@ module Optimizely
|
|
114
113
|
# Returns entity ID corresponding to the provided bucket value or nil if no match is found.
|
115
114
|
bucketing_key = format(BUCKETING_ID_TEMPLATE, bucketing_id: bucketing_id, entity_id: parent_id)
|
116
115
|
bucket_value = generate_bucket_value(bucketing_key)
|
117
|
-
@
|
116
|
+
@logger.log(Logger::DEBUG, "Assigned bucket #{bucket_value} to user '#{user_id}' "\
|
118
117
|
"with bucketing ID: '#{bucketing_id}'.")
|
119
118
|
|
120
119
|
traffic_allocations.each do |traffic_allocation|
|
@@ -0,0 +1,411 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright 2019, Optimizely and contributors
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
#
|
17
|
+
require 'json'
|
18
|
+
require 'optimizely/project_config'
|
19
|
+
require 'optimizely/helpers/constants'
|
20
|
+
require 'optimizely/helpers/validator'
|
21
|
+
module Optimizely
|
22
|
+
class DatafileProjectConfig < ProjectConfig
|
23
|
+
# Representation of the Optimizely project config.
|
24
|
+
RUNNING_EXPERIMENT_STATUS = ['Running'].freeze
|
25
|
+
RESERVED_ATTRIBUTE_PREFIX = '$opt_'
|
26
|
+
|
27
|
+
attr_reader :account_id
|
28
|
+
attr_reader :attributes
|
29
|
+
attr_reader :audiences
|
30
|
+
attr_reader :typed_audiences
|
31
|
+
attr_reader :events
|
32
|
+
attr_reader :experiments
|
33
|
+
attr_reader :feature_flags
|
34
|
+
attr_reader :groups
|
35
|
+
attr_reader :project_id
|
36
|
+
# Boolean - denotes if Optimizely should remove the last block of visitors' IP address before storing event data
|
37
|
+
attr_reader :anonymize_ip
|
38
|
+
attr_reader :bot_filtering
|
39
|
+
attr_reader :revision
|
40
|
+
attr_reader :rollouts
|
41
|
+
attr_reader :version
|
42
|
+
|
43
|
+
attr_reader :attribute_key_map
|
44
|
+
attr_reader :audience_id_map
|
45
|
+
attr_reader :event_key_map
|
46
|
+
attr_reader :experiment_feature_map
|
47
|
+
attr_reader :experiment_id_map
|
48
|
+
attr_reader :experiment_key_map
|
49
|
+
attr_reader :feature_flag_key_map
|
50
|
+
attr_reader :feature_variable_key_map
|
51
|
+
attr_reader :group_id_map
|
52
|
+
attr_reader :rollout_id_map
|
53
|
+
attr_reader :rollout_experiment_key_map
|
54
|
+
attr_reader :variation_id_map
|
55
|
+
attr_reader :variation_id_to_variable_usage_map
|
56
|
+
attr_reader :variation_key_map
|
57
|
+
|
58
|
+
def initialize(datafile, logger, error_handler)
|
59
|
+
# ProjectConfig init method to fetch and set project config data
|
60
|
+
#
|
61
|
+
# datafile - JSON string representing the project
|
62
|
+
|
63
|
+
config = JSON.parse(datafile)
|
64
|
+
|
65
|
+
@error_handler = error_handler
|
66
|
+
@logger = logger
|
67
|
+
@version = config['version']
|
68
|
+
|
69
|
+
raise InvalidDatafileVersionError, @version unless Helpers::Constants::SUPPORTED_VERSIONS.value?(@version)
|
70
|
+
|
71
|
+
@account_id = config['accountId']
|
72
|
+
@attributes = config.fetch('attributes', [])
|
73
|
+
@audiences = config.fetch('audiences', [])
|
74
|
+
@typed_audiences = config.fetch('typedAudiences', [])
|
75
|
+
@events = config.fetch('events', [])
|
76
|
+
@experiments = config['experiments']
|
77
|
+
@feature_flags = config.fetch('featureFlags', [])
|
78
|
+
@groups = config.fetch('groups', [])
|
79
|
+
@project_id = config['projectId']
|
80
|
+
@anonymize_ip = config.key?('anonymizeIP') ? config['anonymizeIP'] : false
|
81
|
+
@bot_filtering = config['botFiltering']
|
82
|
+
@revision = config['revision']
|
83
|
+
@rollouts = config.fetch('rollouts', [])
|
84
|
+
|
85
|
+
# Utility maps for quick lookup
|
86
|
+
@attribute_key_map = generate_key_map(@attributes, 'key')
|
87
|
+
@event_key_map = generate_key_map(@events, 'key')
|
88
|
+
@group_id_map = generate_key_map(@groups, 'id')
|
89
|
+
@group_id_map.each do |key, group|
|
90
|
+
exps = group.fetch('experiments')
|
91
|
+
exps.each do |exp|
|
92
|
+
@experiments.push(exp.merge('groupId' => key))
|
93
|
+
end
|
94
|
+
end
|
95
|
+
@experiment_key_map = generate_key_map(@experiments, 'key')
|
96
|
+
@experiment_id_map = generate_key_map(@experiments, 'id')
|
97
|
+
@audience_id_map = generate_key_map(@audiences, 'id')
|
98
|
+
@audience_id_map = @audience_id_map.merge(generate_key_map(@typed_audiences, 'id')) unless @typed_audiences.empty?
|
99
|
+
@variation_id_map = {}
|
100
|
+
@variation_key_map = {}
|
101
|
+
@variation_id_to_variable_usage_map = {}
|
102
|
+
@variation_id_to_experiment_map = {}
|
103
|
+
@experiment_key_map.each_value do |exp|
|
104
|
+
# Excludes experiments from rollouts
|
105
|
+
variations = exp.fetch('variations')
|
106
|
+
variations.each do |variation|
|
107
|
+
variation_id = variation['id']
|
108
|
+
@variation_id_to_experiment_map[variation_id] = exp
|
109
|
+
end
|
110
|
+
end
|
111
|
+
@rollout_id_map = generate_key_map(@rollouts, 'id')
|
112
|
+
# split out the experiment key map for rollouts
|
113
|
+
@rollout_experiment_key_map = {}
|
114
|
+
@rollout_id_map.each_value do |rollout|
|
115
|
+
exps = rollout.fetch('experiments')
|
116
|
+
@rollout_experiment_key_map = @rollout_experiment_key_map.merge(generate_key_map(exps, 'key'))
|
117
|
+
end
|
118
|
+
@all_experiments = @experiment_key_map.merge(@rollout_experiment_key_map)
|
119
|
+
@all_experiments.each do |key, exp|
|
120
|
+
variations = exp.fetch('variations')
|
121
|
+
variations.each do |variation|
|
122
|
+
variation_id = variation['id']
|
123
|
+
variation['featureEnabled'] = variation['featureEnabled'] == true
|
124
|
+
variation_variables = variation['variables']
|
125
|
+
next if variation_variables.nil?
|
126
|
+
|
127
|
+
@variation_id_to_variable_usage_map[variation_id] = generate_key_map(variation_variables, 'id')
|
128
|
+
end
|
129
|
+
@variation_id_map[key] = generate_key_map(variations, 'id')
|
130
|
+
@variation_key_map[key] = generate_key_map(variations, 'key')
|
131
|
+
end
|
132
|
+
@feature_flag_key_map = generate_key_map(@feature_flags, 'key')
|
133
|
+
@experiment_feature_map = {}
|
134
|
+
@feature_variable_key_map = {}
|
135
|
+
@feature_flag_key_map.each do |key, feature_flag|
|
136
|
+
@feature_variable_key_map[key] = generate_key_map(feature_flag['variables'], 'key')
|
137
|
+
feature_flag['experimentIds'].each do |experiment_id|
|
138
|
+
@experiment_feature_map[experiment_id] = [feature_flag['id']]
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def self.create(datafile, logger, error_handler, skip_json_validation)
|
144
|
+
# Looks up and sets datafile and config based on response body.
|
145
|
+
#
|
146
|
+
# datafile - JSON string representing the Optimizely project.
|
147
|
+
# logger - Provides a logger instance.
|
148
|
+
# error_handler - Provides a handle_error method to handle exceptions.
|
149
|
+
# skip_json_validation - Optional boolean param which allows skipping JSON schema
|
150
|
+
# validation upon object invocation. By default JSON schema validation will be performed.
|
151
|
+
# Returns instance of DatafileProjectConfig, nil otherwise.
|
152
|
+
if !skip_json_validation && !Helpers::Validator.datafile_valid?(datafile)
|
153
|
+
default_logger = SimpleLogger.new
|
154
|
+
default_logger.log(Logger::ERROR, InvalidInputError.new('datafile').message)
|
155
|
+
return nil
|
156
|
+
end
|
157
|
+
|
158
|
+
begin
|
159
|
+
config = new(datafile, logger, error_handler)
|
160
|
+
rescue StandardError => e
|
161
|
+
default_logger = SimpleLogger.new
|
162
|
+
error_msg = e.class == InvalidDatafileVersionError ? e.message : InvalidInputError.new('datafile').message
|
163
|
+
error_to_handle = e.class == InvalidDatafileVersionError ? InvalidDatafileVersionError : InvalidInputError
|
164
|
+
default_logger.log(Logger::ERROR, error_msg)
|
165
|
+
error_handler.handle_error error_to_handle
|
166
|
+
return nil
|
167
|
+
end
|
168
|
+
|
169
|
+
config
|
170
|
+
end
|
171
|
+
|
172
|
+
def experiment_running?(experiment)
|
173
|
+
# Determine if experiment corresponding to given key is running
|
174
|
+
#
|
175
|
+
# experiment - Experiment
|
176
|
+
#
|
177
|
+
# Returns true if experiment is running
|
178
|
+
RUNNING_EXPERIMENT_STATUS.include?(experiment['status'])
|
179
|
+
end
|
180
|
+
|
181
|
+
def get_experiment_from_key(experiment_key)
|
182
|
+
# Retrieves experiment ID for a given key
|
183
|
+
#
|
184
|
+
# experiment_key - String key representing the experiment
|
185
|
+
#
|
186
|
+
# Returns Experiment or nil if not found
|
187
|
+
|
188
|
+
experiment = @experiment_key_map[experiment_key]
|
189
|
+
return experiment if experiment
|
190
|
+
|
191
|
+
@logger.log Logger::ERROR, "Experiment key '#{experiment_key}' is not in datafile."
|
192
|
+
@error_handler.handle_error InvalidExperimentError
|
193
|
+
nil
|
194
|
+
end
|
195
|
+
|
196
|
+
def get_experiment_key(experiment_id)
|
197
|
+
# Retrieves experiment key for a given ID.
|
198
|
+
#
|
199
|
+
# experiment_id - String ID representing the experiment.
|
200
|
+
#
|
201
|
+
# Returns String key.
|
202
|
+
|
203
|
+
experiment = @experiment_id_map[experiment_id]
|
204
|
+
return experiment['key'] unless experiment.nil?
|
205
|
+
|
206
|
+
@logger.log Logger::ERROR, "Experiment id '#{experiment_id}' is not in datafile."
|
207
|
+
@error_handler.handle_error InvalidExperimentError
|
208
|
+
nil
|
209
|
+
end
|
210
|
+
|
211
|
+
def get_event_from_key(event_key)
|
212
|
+
# Get event for the provided event key.
|
213
|
+
#
|
214
|
+
# event_key - Event key for which event is to be determined.
|
215
|
+
#
|
216
|
+
# Returns Event corresponding to the provided event key.
|
217
|
+
|
218
|
+
event = @event_key_map[event_key]
|
219
|
+
return event if event
|
220
|
+
|
221
|
+
@logger.log Logger::ERROR, "Event '#{event_key}' is not in datafile."
|
222
|
+
@error_handler.handle_error InvalidEventError
|
223
|
+
nil
|
224
|
+
end
|
225
|
+
|
226
|
+
def get_audience_from_id(audience_id)
|
227
|
+
# Get audience for the provided audience ID
|
228
|
+
#
|
229
|
+
# audience_id - ID of the audience
|
230
|
+
#
|
231
|
+
# Returns the audience
|
232
|
+
|
233
|
+
audience = @audience_id_map[audience_id]
|
234
|
+
return audience if audience
|
235
|
+
|
236
|
+
@logger.log Logger::ERROR, "Audience '#{audience_id}' is not in datafile."
|
237
|
+
@error_handler.handle_error InvalidAudienceError
|
238
|
+
nil
|
239
|
+
end
|
240
|
+
|
241
|
+
def get_variation_from_id(experiment_key, variation_id)
|
242
|
+
# Get variation given experiment key and variation ID
|
243
|
+
#
|
244
|
+
# experiment_key - Key representing parent experiment of variation
|
245
|
+
# variation_id - ID of the variation
|
246
|
+
#
|
247
|
+
# Returns the variation or nil if not found
|
248
|
+
|
249
|
+
variation_id_map = @variation_id_map[experiment_key]
|
250
|
+
if variation_id_map
|
251
|
+
variation = variation_id_map[variation_id]
|
252
|
+
return variation if variation
|
253
|
+
|
254
|
+
@logger.log Logger::ERROR, "Variation id '#{variation_id}' is not in datafile."
|
255
|
+
@error_handler.handle_error InvalidVariationError
|
256
|
+
return nil
|
257
|
+
end
|
258
|
+
|
259
|
+
@logger.log Logger::ERROR, "Experiment key '#{experiment_key}' is not in datafile."
|
260
|
+
@error_handler.handle_error InvalidExperimentError
|
261
|
+
nil
|
262
|
+
end
|
263
|
+
|
264
|
+
def get_variation_id_from_key(experiment_key, variation_key)
|
265
|
+
# Get variation ID given experiment key and variation key
|
266
|
+
#
|
267
|
+
# experiment_key - Key representing parent experiment of variation
|
268
|
+
# variation_key - Key of the variation
|
269
|
+
#
|
270
|
+
# Returns ID of the variation
|
271
|
+
|
272
|
+
variation_key_map = @variation_key_map[experiment_key]
|
273
|
+
if variation_key_map
|
274
|
+
variation = variation_key_map[variation_key]
|
275
|
+
return variation['id'] if variation
|
276
|
+
|
277
|
+
@logger.log Logger::ERROR, "Variation key '#{variation_key}' is not in datafile."
|
278
|
+
@error_handler.handle_error InvalidVariationError
|
279
|
+
return nil
|
280
|
+
end
|
281
|
+
|
282
|
+
@logger.log Logger::ERROR, "Experiment key '#{experiment_key}' is not in datafile."
|
283
|
+
@error_handler.handle_error InvalidExperimentError
|
284
|
+
nil
|
285
|
+
end
|
286
|
+
|
287
|
+
def get_whitelisted_variations(experiment_key)
|
288
|
+
# Retrieves whitelisted variations for a given experiment Key
|
289
|
+
#
|
290
|
+
# experiment_key - String Key representing the experiment
|
291
|
+
#
|
292
|
+
# Returns whitelisted variations for the experiment or nil
|
293
|
+
|
294
|
+
experiment = @experiment_key_map[experiment_key]
|
295
|
+
return experiment['forcedVariations'] if experiment
|
296
|
+
|
297
|
+
@logger.log Logger::ERROR, "Experiment key '#{experiment_key}' is not in datafile."
|
298
|
+
@error_handler.handle_error InvalidExperimentError
|
299
|
+
end
|
300
|
+
|
301
|
+
def get_attribute_id(attribute_key)
|
302
|
+
# Get attribute ID for the provided attribute key.
|
303
|
+
#
|
304
|
+
# Args:
|
305
|
+
# Attribute key for which attribute is to be fetched.
|
306
|
+
#
|
307
|
+
# Returns:
|
308
|
+
# Attribute ID corresponding to the provided attribute key.
|
309
|
+
attribute = @attribute_key_map[attribute_key]
|
310
|
+
has_reserved_prefix = attribute_key.to_s.start_with?(RESERVED_ATTRIBUTE_PREFIX)
|
311
|
+
unless attribute.nil?
|
312
|
+
if has_reserved_prefix
|
313
|
+
@logger.log(Logger::WARN, "Attribute '#{attribute_key}' unexpectedly has reserved prefix '#{RESERVED_ATTRIBUTE_PREFIX}'; "\
|
314
|
+
'using attribute ID instead of reserved attribute name.')
|
315
|
+
end
|
316
|
+
return attribute['id']
|
317
|
+
end
|
318
|
+
return attribute_key if has_reserved_prefix
|
319
|
+
|
320
|
+
@logger.log Logger::ERROR, "Attribute key '#{attribute_key}' is not in datafile."
|
321
|
+
@error_handler.handle_error InvalidAttributeError
|
322
|
+
nil
|
323
|
+
end
|
324
|
+
|
325
|
+
def variation_id_exists?(experiment_id, variation_id)
|
326
|
+
# Determines if a given experiment ID / variation ID pair exists in the datafile
|
327
|
+
#
|
328
|
+
# experiment_id - String experiment ID
|
329
|
+
# variation_id - String variation ID
|
330
|
+
#
|
331
|
+
# Returns true if variation is in datafile
|
332
|
+
|
333
|
+
experiment_key = get_experiment_key(experiment_id)
|
334
|
+
variation_id_map = @variation_id_map[experiment_key]
|
335
|
+
if variation_id_map
|
336
|
+
variation = variation_id_map[variation_id]
|
337
|
+
return true if variation
|
338
|
+
|
339
|
+
@logger.log Logger::ERROR, "Variation ID '#{variation_id}' is not in datafile."
|
340
|
+
@error_handler.handle_error InvalidVariationError
|
341
|
+
end
|
342
|
+
|
343
|
+
false
|
344
|
+
end
|
345
|
+
|
346
|
+
def get_feature_flag_from_key(feature_flag_key)
|
347
|
+
# Retrieves the feature flag with the given key
|
348
|
+
#
|
349
|
+
# feature_flag_key - String feature key
|
350
|
+
#
|
351
|
+
# Returns feature flag if found, otherwise nil
|
352
|
+
feature_flag = @feature_flag_key_map[feature_flag_key]
|
353
|
+
return feature_flag if feature_flag
|
354
|
+
|
355
|
+
@logger.log Logger::ERROR, "Feature flag key '#{feature_flag_key}' is not in datafile."
|
356
|
+
nil
|
357
|
+
end
|
358
|
+
|
359
|
+
def get_feature_variable(feature_flag, variable_key)
|
360
|
+
# Retrieves the variable with the given key for the given feature
|
361
|
+
#
|
362
|
+
# feature_flag - The feature flag for which we are retrieving the variable
|
363
|
+
# variable_key - String variable key
|
364
|
+
#
|
365
|
+
# Returns variable if found, otherwise nil
|
366
|
+
feature_flag_key = feature_flag['key']
|
367
|
+
variable = @feature_variable_key_map[feature_flag_key][variable_key]
|
368
|
+
return variable if variable
|
369
|
+
|
370
|
+
@logger.log Logger::ERROR, "No feature variable was found for key '#{variable_key}' in feature flag "\
|
371
|
+
"'#{feature_flag_key}'."
|
372
|
+
nil
|
373
|
+
end
|
374
|
+
|
375
|
+
def get_rollout_from_id(rollout_id)
|
376
|
+
# Retrieves the rollout with the given ID
|
377
|
+
#
|
378
|
+
# rollout_id - String rollout ID
|
379
|
+
#
|
380
|
+
# Returns the rollout if found, otherwise nil
|
381
|
+
rollout = @rollout_id_map[rollout_id]
|
382
|
+
return rollout if rollout
|
383
|
+
|
384
|
+
@logger.log Logger::ERROR, "Rollout with ID '#{rollout_id}' is not in the datafile."
|
385
|
+
nil
|
386
|
+
end
|
387
|
+
|
388
|
+
def feature_experiment?(experiment_id)
|
389
|
+
# Determines if given experiment is a feature test.
|
390
|
+
#
|
391
|
+
# experiment_id - String experiment ID
|
392
|
+
#
|
393
|
+
# Returns true if experiment belongs to any feature,
|
394
|
+
# false otherwise.
|
395
|
+
@experiment_feature_map.key?(experiment_id)
|
396
|
+
end
|
397
|
+
|
398
|
+
private
|
399
|
+
|
400
|
+
def generate_key_map(array, key)
|
401
|
+
# Helper method to generate map from key to hash in array of hashes
|
402
|
+
#
|
403
|
+
# array - Array consisting of hash
|
404
|
+
# key - Key in each hash which will be key in the map
|
405
|
+
#
|
406
|
+
# Returns map mapping key to hash
|
407
|
+
|
408
|
+
Hash[array.map { |obj| [obj[key], obj] }]
|
409
|
+
end
|
410
|
+
end
|
411
|
+
end
|