optimizely-sdk 5.0.0 → 5.0.1
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/LICENSE +202 -202
- data/lib/optimizely/audience.rb +127 -127
- data/lib/optimizely/bucketer.rb +156 -156
- data/lib/optimizely/condition_tree_evaluator.rb +123 -123
- data/lib/optimizely/config/datafile_project_config.rb +558 -558
- data/lib/optimizely/config/proxy_config.rb +34 -34
- data/lib/optimizely/config_manager/async_scheduler.rb +95 -95
- data/lib/optimizely/config_manager/http_project_config_manager.rb +340 -340
- data/lib/optimizely/config_manager/project_config_manager.rb +25 -25
- data/lib/optimizely/config_manager/static_project_config_manager.rb +55 -55
- data/lib/optimizely/decide/optimizely_decide_option.rb +28 -28
- data/lib/optimizely/decide/optimizely_decision.rb +60 -60
- data/lib/optimizely/decide/optimizely_decision_message.rb +26 -26
- data/lib/optimizely/decision_service.rb +563 -563
- data/lib/optimizely/error_handler.rb +39 -39
- data/lib/optimizely/event/batch_event_processor.rb +235 -235
- data/lib/optimizely/event/entity/conversion_event.rb +44 -44
- data/lib/optimizely/event/entity/decision.rb +38 -38
- data/lib/optimizely/event/entity/event_batch.rb +86 -86
- data/lib/optimizely/event/entity/event_context.rb +50 -50
- data/lib/optimizely/event/entity/impression_event.rb +48 -48
- data/lib/optimizely/event/entity/snapshot.rb +33 -33
- data/lib/optimizely/event/entity/snapshot_event.rb +48 -48
- data/lib/optimizely/event/entity/user_event.rb +22 -22
- data/lib/optimizely/event/entity/visitor.rb +36 -36
- data/lib/optimizely/event/entity/visitor_attribute.rb +38 -38
- data/lib/optimizely/event/event_factory.rb +156 -156
- data/lib/optimizely/event/event_processor.rb +25 -25
- data/lib/optimizely/event/forwarding_event_processor.rb +44 -44
- data/lib/optimizely/event/user_event_factory.rb +88 -88
- data/lib/optimizely/event_builder.rb +221 -221
- data/lib/optimizely/event_dispatcher.rb +69 -69
- data/lib/optimizely/exceptions.rb +193 -193
- data/lib/optimizely/helpers/constants.rb +459 -459
- data/lib/optimizely/helpers/date_time_utils.rb +30 -30
- data/lib/optimizely/helpers/event_tag_utils.rb +132 -132
- data/lib/optimizely/helpers/group.rb +31 -31
- data/lib/optimizely/helpers/http_utils.rb +68 -68
- data/lib/optimizely/helpers/sdk_settings.rb +61 -61
- data/lib/optimizely/helpers/validator.rb +236 -236
- data/lib/optimizely/helpers/variable_type.rb +67 -67
- data/lib/optimizely/logger.rb +46 -46
- data/lib/optimizely/notification_center.rb +174 -174
- data/lib/optimizely/notification_center_registry.rb +71 -71
- data/lib/optimizely/odp/lru_cache.rb +114 -114
- data/lib/optimizely/odp/odp_config.rb +102 -102
- data/lib/optimizely/odp/odp_event.rb +75 -75
- data/lib/optimizely/odp/odp_event_api_manager.rb +70 -70
- data/lib/optimizely/odp/odp_event_manager.rb +286 -286
- data/lib/optimizely/odp/odp_manager.rb +159 -159
- data/lib/optimizely/odp/odp_segment_api_manager.rb +122 -122
- data/lib/optimizely/odp/odp_segment_manager.rb +97 -97
- data/lib/optimizely/optimizely_config.rb +273 -273
- data/lib/optimizely/optimizely_factory.rb +184 -184
- data/lib/optimizely/optimizely_user_context.rb +238 -238
- data/lib/optimizely/params.rb +31 -31
- data/lib/optimizely/project_config.rb +99 -99
- data/lib/optimizely/semantic_version.rb +166 -166
- data/lib/optimizely/user_condition_evaluator.rb +391 -391
- data/lib/optimizely/user_profile_service.rb +35 -35
- data/lib/optimizely/version.rb +21 -21
- data/lib/optimizely.rb +1262 -1262
- metadata +7 -5
@@ -1,558 +1,558 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
# Copyright 2019-2022, 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 :datafile, :account_id, :attributes, :audiences, :typed_audiences, :events,
|
28
|
-
:experiments, :feature_flags, :groups, :project_id, :bot_filtering, :revision,
|
29
|
-
:sdk_key, :environment_key, :rollouts, :version, :send_flag_decisions,
|
30
|
-
:attribute_key_map, :audience_id_map, :event_key_map, :experiment_feature_map,
|
31
|
-
:experiment_id_map, :experiment_key_map, :feature_flag_key_map, :feature_variable_key_map,
|
32
|
-
:group_id_map, :rollout_id_map, :rollout_experiment_id_map, :variation_id_map,
|
33
|
-
:variation_id_to_variable_usage_map, :variation_key_map, :variation_id_map_by_experiment_id,
|
34
|
-
:variation_key_map_by_experiment_id, :flag_variation_map, :integration_key_map, :integrations,
|
35
|
-
:public_key_for_odp, :host_for_odp, :all_segments
|
36
|
-
# Boolean - denotes if Optimizely should remove the last block of visitors' IP address before storing event data
|
37
|
-
attr_reader :anonymize_ip
|
38
|
-
|
39
|
-
def initialize(datafile, logger, error_handler)
|
40
|
-
# ProjectConfig init method to fetch and set project config data
|
41
|
-
#
|
42
|
-
# datafile - JSON string representing the project
|
43
|
-
super()
|
44
|
-
|
45
|
-
config = JSON.parse(datafile)
|
46
|
-
|
47
|
-
@datafile = datafile
|
48
|
-
@error_handler = error_handler
|
49
|
-
@logger = logger
|
50
|
-
@version = config['version']
|
51
|
-
|
52
|
-
raise InvalidDatafileVersionError, @version unless Helpers::Constants::SUPPORTED_VERSIONS.value?(@version)
|
53
|
-
|
54
|
-
@account_id = config['accountId']
|
55
|
-
@attributes = config.fetch('attributes', [])
|
56
|
-
@audiences = config.fetch('audiences', [])
|
57
|
-
@typed_audiences = config.fetch('typedAudiences', [])
|
58
|
-
@events = config.fetch('events', [])
|
59
|
-
@experiments = config['experiments']
|
60
|
-
@feature_flags = config.fetch('featureFlags', [])
|
61
|
-
@groups = config.fetch('groups', [])
|
62
|
-
@project_id = config['projectId']
|
63
|
-
@anonymize_ip = config.key?('anonymizeIP') ? config['anonymizeIP'] : false
|
64
|
-
@bot_filtering = config['botFiltering']
|
65
|
-
@revision = config['revision']
|
66
|
-
@sdk_key = config.fetch('sdkKey', '')
|
67
|
-
@environment_key = config.fetch('environmentKey', '')
|
68
|
-
@rollouts = config.fetch('rollouts', [])
|
69
|
-
@send_flag_decisions = config.fetch('sendFlagDecisions', false)
|
70
|
-
@integrations = config.fetch('integrations', [])
|
71
|
-
|
72
|
-
# Json type is represented in datafile as a subtype of string for the sake of backwards compatibility.
|
73
|
-
# Converting it to a first-class json type while creating Project Config
|
74
|
-
@feature_flags.each do |feature_flag|
|
75
|
-
feature_flag['variables'].each do |variable|
|
76
|
-
if variable['type'] == 'string' && variable['subType'] == 'json'
|
77
|
-
variable['type'] = 'json'
|
78
|
-
variable.delete('subType')
|
79
|
-
end
|
80
|
-
end
|
81
|
-
end
|
82
|
-
|
83
|
-
# Utility maps for quick lookup
|
84
|
-
@attribute_key_map = generate_key_map(@attributes, 'key')
|
85
|
-
@event_key_map = generate_key_map(@events, 'key')
|
86
|
-
@group_id_map = generate_key_map(@groups, 'id')
|
87
|
-
@group_id_map.each do |key, group|
|
88
|
-
exps = group.fetch('experiments')
|
89
|
-
exps.each do |exp|
|
90
|
-
@experiments.push(exp.merge('groupId' => key))
|
91
|
-
end
|
92
|
-
end
|
93
|
-
@experiment_key_map = generate_key_map(@experiments, 'key')
|
94
|
-
@experiment_id_map = generate_key_map(@experiments, 'id')
|
95
|
-
@audience_id_map = generate_key_map(@audiences, 'id')
|
96
|
-
@integration_key_map = generate_key_map(@integrations, 'key', first_value: true)
|
97
|
-
@audience_id_map = @audience_id_map.merge(generate_key_map(@typed_audiences, 'id')) unless @typed_audiences.empty?
|
98
|
-
@variation_id_map = {}
|
99
|
-
@variation_key_map = {}
|
100
|
-
@variation_id_map_by_experiment_id = {}
|
101
|
-
@variation_key_map_by_experiment_id = {}
|
102
|
-
@variation_id_to_variable_usage_map = {}
|
103
|
-
@variation_id_to_experiment_map = {}
|
104
|
-
@flag_variation_map = {}
|
105
|
-
|
106
|
-
@experiment_id_map.each_value do |exp|
|
107
|
-
# Excludes experiments from rollouts
|
108
|
-
variations = exp.fetch('variations')
|
109
|
-
variations.each do |variation|
|
110
|
-
variation_id = variation['id']
|
111
|
-
@variation_id_to_experiment_map[variation_id] = exp
|
112
|
-
end
|
113
|
-
end
|
114
|
-
@rollout_id_map = generate_key_map(@rollouts, 'id')
|
115
|
-
# split out the experiment key map for rollouts
|
116
|
-
@rollout_experiment_id_map = {}
|
117
|
-
@rollout_id_map.each_value do |rollout|
|
118
|
-
exps = rollout.fetch('experiments')
|
119
|
-
@rollout_experiment_id_map = @rollout_experiment_id_map.merge(generate_key_map(exps, 'id'))
|
120
|
-
end
|
121
|
-
|
122
|
-
if (odp_integration = @integration_key_map&.fetch('odp', nil))
|
123
|
-
@public_key_for_odp = odp_integration['publicKey']
|
124
|
-
@host_for_odp = odp_integration['host']
|
125
|
-
end
|
126
|
-
|
127
|
-
@all_segments = []
|
128
|
-
@audience_id_map.each_value do |audience|
|
129
|
-
@all_segments.concat Audience.get_segments(audience['conditions'])
|
130
|
-
end
|
131
|
-
|
132
|
-
@flag_variation_map = generate_feature_variation_map(@feature_flags)
|
133
|
-
@all_experiments = @experiment_id_map.merge(@rollout_experiment_id_map)
|
134
|
-
@all_experiments.each do |id, exp|
|
135
|
-
variations = exp.fetch('variations')
|
136
|
-
variations.each do |variation|
|
137
|
-
variation_id = variation['id']
|
138
|
-
variation['featureEnabled'] = variation['featureEnabled'] == true
|
139
|
-
variation_variables = variation['variables']
|
140
|
-
next if variation_variables.nil?
|
141
|
-
|
142
|
-
@variation_id_to_variable_usage_map[variation_id] = generate_key_map(variation_variables, 'id')
|
143
|
-
end
|
144
|
-
@variation_id_map[exp['key']] = generate_key_map(variations, 'id')
|
145
|
-
@variation_key_map[exp['key']] = generate_key_map(variations, 'key')
|
146
|
-
@variation_id_map_by_experiment_id[id] = generate_key_map(variations, 'id')
|
147
|
-
@variation_key_map_by_experiment_id[id] = generate_key_map(variations, 'key')
|
148
|
-
end
|
149
|
-
@feature_flag_key_map = generate_key_map(@feature_flags, 'key')
|
150
|
-
@experiment_feature_map = {}
|
151
|
-
@feature_variable_key_map = {}
|
152
|
-
@feature_flag_key_map.each do |key, feature_flag|
|
153
|
-
@feature_variable_key_map[key] = generate_key_map(feature_flag['variables'], 'key')
|
154
|
-
feature_flag['experimentIds'].each do |experiment_id|
|
155
|
-
@experiment_feature_map[experiment_id] = [feature_flag['id']]
|
156
|
-
end
|
157
|
-
end
|
158
|
-
end
|
159
|
-
|
160
|
-
def get_rules_for_flag(feature_flag)
|
161
|
-
# Retrieves rules for a given feature flag
|
162
|
-
#
|
163
|
-
# feature_flag - String key representing the feature_flag
|
164
|
-
#
|
165
|
-
# Returns rules in feature flag
|
166
|
-
rules = feature_flag['experimentIds'].map { |exp_id| @experiment_id_map[exp_id] }
|
167
|
-
rollout = feature_flag['rolloutId'].empty? ? nil : @rollout_id_map[feature_flag['rolloutId']]
|
168
|
-
|
169
|
-
if rollout
|
170
|
-
rollout_experiments = rollout.fetch('experiments')
|
171
|
-
rollout_experiments.each do |exp|
|
172
|
-
rules.push(exp)
|
173
|
-
end
|
174
|
-
end
|
175
|
-
rules
|
176
|
-
end
|
177
|
-
|
178
|
-
def self.create(datafile, logger, error_handler, skip_json_validation)
|
179
|
-
# Looks up and sets datafile and config based on response body.
|
180
|
-
#
|
181
|
-
# datafile - JSON string representing the Optimizely project.
|
182
|
-
# logger - Provides a logger instance.
|
183
|
-
# error_handler - Provides a handle_error method to handle exceptions.
|
184
|
-
# skip_json_validation - Optional boolean param which allows skipping JSON schema
|
185
|
-
# validation upon object invocation. By default JSON schema validation will be performed.
|
186
|
-
# Returns instance of DatafileProjectConfig, nil otherwise.
|
187
|
-
logger ||= SimpleLogger.new
|
188
|
-
if !skip_json_validation && !Helpers::Validator.datafile_valid?(datafile)
|
189
|
-
logger.log(Logger::ERROR, InvalidInputError.new('datafile').message)
|
190
|
-
return nil
|
191
|
-
end
|
192
|
-
|
193
|
-
begin
|
194
|
-
config = new(datafile, logger, error_handler)
|
195
|
-
rescue StandardError => e
|
196
|
-
error_to_handle = e.instance_of?(InvalidDatafileVersionError) ? e : InvalidInputError.new('datafile')
|
197
|
-
error_msg = error_to_handle.message
|
198
|
-
|
199
|
-
logger.log(Logger::ERROR, error_msg)
|
200
|
-
error_handler.handle_error error_to_handle
|
201
|
-
return nil
|
202
|
-
end
|
203
|
-
|
204
|
-
config
|
205
|
-
end
|
206
|
-
|
207
|
-
def experiment_running?(experiment)
|
208
|
-
# Determine if experiment corresponding to given key is running
|
209
|
-
#
|
210
|
-
# experiment - Experiment
|
211
|
-
#
|
212
|
-
# Returns true if experiment is running
|
213
|
-
RUNNING_EXPERIMENT_STATUS.include?(experiment['status'])
|
214
|
-
end
|
215
|
-
|
216
|
-
def get_experiment_from_key(experiment_key)
|
217
|
-
# Retrieves experiment ID for a given key
|
218
|
-
#
|
219
|
-
# experiment_key - String key representing the experiment
|
220
|
-
#
|
221
|
-
# Returns Experiment or nil if not found
|
222
|
-
|
223
|
-
experiment = @experiment_key_map[experiment_key]
|
224
|
-
return experiment if experiment
|
225
|
-
|
226
|
-
invalid_experiment_error = InvalidExperimentError.new(experiment_key: experiment_key)
|
227
|
-
@logger.log Logger::ERROR, invalid_experiment_error.message
|
228
|
-
@error_handler.handle_error invalid_experiment_error
|
229
|
-
nil
|
230
|
-
end
|
231
|
-
|
232
|
-
def get_experiment_from_id(experiment_id)
|
233
|
-
# Retrieves experiment ID for a given key
|
234
|
-
#
|
235
|
-
# experiment_id - String id representing the experiment
|
236
|
-
#
|
237
|
-
# Returns Experiment or nil if not found
|
238
|
-
|
239
|
-
experiment = @experiment_id_map[experiment_id]
|
240
|
-
return experiment if experiment
|
241
|
-
|
242
|
-
invalid_experiment_error = InvalidExperimentError.new(experiment_id: experiment_id)
|
243
|
-
@logger.log Logger::ERROR, invalid_experiment_error.message
|
244
|
-
@error_handler.handle_error invalid_experiment_error
|
245
|
-
nil
|
246
|
-
end
|
247
|
-
|
248
|
-
def get_experiment_key(experiment_id)
|
249
|
-
# Retrieves experiment key for a given ID.
|
250
|
-
#
|
251
|
-
# experiment_id - String ID representing the experiment.
|
252
|
-
#
|
253
|
-
# Returns String key.
|
254
|
-
|
255
|
-
experiment = @experiment_id_map[experiment_id]
|
256
|
-
return experiment['key'] unless experiment.nil?
|
257
|
-
|
258
|
-
invalid_experiment_error = InvalidExperimentError.new(experiment_id: experiment_id)
|
259
|
-
@logger.log Logger::ERROR, invalid_experiment_error.message
|
260
|
-
@error_handler.handle_error invalid_experiment_error
|
261
|
-
nil
|
262
|
-
end
|
263
|
-
|
264
|
-
def get_event_from_key(event_key)
|
265
|
-
# Get event for the provided event key.
|
266
|
-
#
|
267
|
-
# event_key - Event key for which event is to be determined.
|
268
|
-
#
|
269
|
-
# Returns Event corresponding to the provided event key.
|
270
|
-
|
271
|
-
event = @event_key_map[event_key]
|
272
|
-
return event if event
|
273
|
-
|
274
|
-
invalid_event_error = InvalidEventError.new(event_key)
|
275
|
-
@logger.log Logger::ERROR, invalid_event_error.message
|
276
|
-
@error_handler.handle_error invalid_event_error
|
277
|
-
nil
|
278
|
-
end
|
279
|
-
|
280
|
-
def get_audience_from_id(audience_id)
|
281
|
-
# Get audience for the provided audience ID
|
282
|
-
#
|
283
|
-
# audience_id - ID of the audience
|
284
|
-
#
|
285
|
-
# Returns the audience
|
286
|
-
|
287
|
-
audience = @audience_id_map[audience_id]
|
288
|
-
return audience if audience
|
289
|
-
|
290
|
-
invalid_audience_error = InvalidAudienceError.new(audience_id)
|
291
|
-
@logger.log Logger::ERROR, invalid_audience_error.message
|
292
|
-
@error_handler.handle_error invalid_audience_error
|
293
|
-
nil
|
294
|
-
end
|
295
|
-
|
296
|
-
def get_variation_from_flag(flag_key, target_value, attribute)
|
297
|
-
variations = @flag_variation_map[flag_key]
|
298
|
-
return variations.select { |variation| variation[attribute] == target_value }.first if variations
|
299
|
-
|
300
|
-
nil
|
301
|
-
end
|
302
|
-
|
303
|
-
def get_variation_from_id(experiment_key, variation_id)
|
304
|
-
# Get variation given experiment key and variation ID
|
305
|
-
#
|
306
|
-
# experiment_key - Key representing parent experiment of variation
|
307
|
-
# variation_id - ID of the variation
|
308
|
-
#
|
309
|
-
# Returns the variation or nil if not found
|
310
|
-
|
311
|
-
variation_id_map = @variation_id_map[experiment_key]
|
312
|
-
if variation_id_map
|
313
|
-
variation = variation_id_map[variation_id]
|
314
|
-
return variation if variation
|
315
|
-
|
316
|
-
invalid_variation_error = InvalidVariationError.new(variation_id: variation_id)
|
317
|
-
@logger.log Logger::ERROR, invalid_variation_error.message
|
318
|
-
@error_handler.handle_error invalid_variation_error
|
319
|
-
return nil
|
320
|
-
end
|
321
|
-
|
322
|
-
invalid_experiment_error = InvalidExperimentError.new(experiment_key: experiment_key)
|
323
|
-
@logger.log Logger::ERROR, invalid_experiment_error.message
|
324
|
-
@error_handler.handle_error invalid_experiment_error
|
325
|
-
nil
|
326
|
-
end
|
327
|
-
|
328
|
-
def get_variation_from_id_by_experiment_id(experiment_id, variation_id)
|
329
|
-
# Get variation given experiment ID and variation ID
|
330
|
-
#
|
331
|
-
# experiment_id - ID representing parent experiment of variation
|
332
|
-
# variation_id - ID of the variation
|
333
|
-
#
|
334
|
-
# Returns the variation or nil if not found
|
335
|
-
|
336
|
-
variation_id_map_by_experiment_id = @variation_id_map_by_experiment_id[experiment_id]
|
337
|
-
if variation_id_map_by_experiment_id
|
338
|
-
variation = variation_id_map_by_experiment_id[variation_id]
|
339
|
-
return variation if variation
|
340
|
-
|
341
|
-
invalid_variation_error = InvalidVariationError.new(variation_id: variation_id)
|
342
|
-
@logger.log Logger::ERROR, invalid_variation_error.message
|
343
|
-
@error_handler.handle_error invalid_variation_error
|
344
|
-
return nil
|
345
|
-
end
|
346
|
-
|
347
|
-
invalid_experiment_error = InvalidExperimentError.new(experiment_id: experiment_id)
|
348
|
-
@logger.log Logger::ERROR, invalid_experiment_error.message
|
349
|
-
@error_handler.handle_error invalid_experiment_error
|
350
|
-
nil
|
351
|
-
end
|
352
|
-
|
353
|
-
def get_variation_id_from_key_by_experiment_id(experiment_id, variation_key)
|
354
|
-
# Get variation given experiment ID and variation key
|
355
|
-
#
|
356
|
-
# experiment_id - ID representing parent experiment of variation
|
357
|
-
# variation_key - Key of the variation
|
358
|
-
#
|
359
|
-
# Returns the variation or nil if not found
|
360
|
-
|
361
|
-
variation_key_map = @variation_key_map_by_experiment_id[experiment_id]
|
362
|
-
if variation_key_map
|
363
|
-
variation = variation_key_map[variation_key]
|
364
|
-
return variation['id'] if variation
|
365
|
-
|
366
|
-
invalid_variation_error = InvalidVariationError.new(variation_key: variation_key)
|
367
|
-
@logger.log Logger::ERROR, invalid_variation_error.message
|
368
|
-
@error_handler.handle_error invalid_variation_error
|
369
|
-
return nil
|
370
|
-
end
|
371
|
-
|
372
|
-
invalid_experiment_error = InvalidExperimentError.new(experiment_id: experiment_id)
|
373
|
-
@logger.log Logger::ERROR, invalid_experiment_error.message
|
374
|
-
@error_handler.handle_error invalid_experiment_error
|
375
|
-
nil
|
376
|
-
end
|
377
|
-
|
378
|
-
def get_variation_id_from_key(experiment_key, variation_key)
|
379
|
-
# Get variation ID given experiment key and variation key
|
380
|
-
#
|
381
|
-
# experiment_key - Key representing parent experiment of variation
|
382
|
-
# variation_key - Key of the variation
|
383
|
-
#
|
384
|
-
# Returns ID of the variation
|
385
|
-
|
386
|
-
variation_key_map = @variation_key_map[experiment_key]
|
387
|
-
if variation_key_map
|
388
|
-
variation = variation_key_map[variation_key]
|
389
|
-
return variation['id'] if variation
|
390
|
-
|
391
|
-
invalid_variation_error = InvalidVariationError.new(variation_key: variation_key)
|
392
|
-
@logger.log Logger::ERROR, invalid_variation_error.message
|
393
|
-
@error_handler.handle_error invalid_variation_error
|
394
|
-
return nil
|
395
|
-
end
|
396
|
-
|
397
|
-
invalid_experiment_error = InvalidExperimentError.new(experiment_key: experiment_key)
|
398
|
-
@logger.log Logger::ERROR, invalid_experiment_error.message
|
399
|
-
@error_handler.handle_error invalid_experiment_error
|
400
|
-
nil
|
401
|
-
end
|
402
|
-
|
403
|
-
def get_whitelisted_variations(experiment_id)
|
404
|
-
# Retrieves whitelisted variations for a given experiment id
|
405
|
-
#
|
406
|
-
# experiment_id - String id representing the experiment
|
407
|
-
#
|
408
|
-
# Returns whitelisted variations for the experiment or nil
|
409
|
-
|
410
|
-
experiment = @experiment_id_map[experiment_id]
|
411
|
-
return experiment['forcedVariations'] if experiment
|
412
|
-
|
413
|
-
invalid_experiment_error = InvalidExperimentError.new(experiment_id: experiment_id)
|
414
|
-
@logger.log Logger::ERROR, invalid_experiment_error.message
|
415
|
-
@error_handler.handle_error invalid_experiment_error
|
416
|
-
end
|
417
|
-
|
418
|
-
def get_attribute_id(attribute_key)
|
419
|
-
# Get attribute ID for the provided attribute key.
|
420
|
-
#
|
421
|
-
# Args:
|
422
|
-
# Attribute key for which attribute is to be fetched.
|
423
|
-
#
|
424
|
-
# Returns:
|
425
|
-
# Attribute ID corresponding to the provided attribute key.
|
426
|
-
attribute = @attribute_key_map[attribute_key]
|
427
|
-
has_reserved_prefix = attribute_key.to_s.start_with?(RESERVED_ATTRIBUTE_PREFIX)
|
428
|
-
unless attribute.nil?
|
429
|
-
if has_reserved_prefix
|
430
|
-
@logger.log(Logger::WARN, "Attribute '#{attribute_key}' unexpectedly has reserved prefix '#{RESERVED_ATTRIBUTE_PREFIX}'; "\
|
431
|
-
'using attribute ID instead of reserved attribute name.')
|
432
|
-
end
|
433
|
-
return attribute['id']
|
434
|
-
end
|
435
|
-
return attribute_key if has_reserved_prefix
|
436
|
-
|
437
|
-
invalid_attribute_error = InvalidAttributeError.new(attribute_key)
|
438
|
-
@logger.log Logger::ERROR, invalid_attribute_error.message
|
439
|
-
@error_handler.handle_error invalid_attribute_error
|
440
|
-
nil
|
441
|
-
end
|
442
|
-
|
443
|
-
def variation_id_exists?(experiment_id, variation_id)
|
444
|
-
# Determines if a given experiment ID / variation ID pair exists in the datafile
|
445
|
-
#
|
446
|
-
# experiment_id - String experiment ID
|
447
|
-
# variation_id - String variation ID
|
448
|
-
#
|
449
|
-
# Returns true if variation is in datafile
|
450
|
-
|
451
|
-
experiment_key = get_experiment_key(experiment_id)
|
452
|
-
variation_id_map = @variation_id_map[experiment_key]
|
453
|
-
if variation_id_map
|
454
|
-
variation = variation_id_map[variation_id]
|
455
|
-
return true if variation
|
456
|
-
|
457
|
-
invalid_variation_error = InvalidVariationError.new(variation_id: variation_id)
|
458
|
-
@logger.log Logger::ERROR, invalid_variation_error.message
|
459
|
-
@error_handler.handle_error invalid_variation_error
|
460
|
-
end
|
461
|
-
|
462
|
-
false
|
463
|
-
end
|
464
|
-
|
465
|
-
def get_feature_flag_from_key(feature_flag_key)
|
466
|
-
# Retrieves the feature flag with the given key
|
467
|
-
#
|
468
|
-
# feature_flag_key - String feature key
|
469
|
-
#
|
470
|
-
# Returns feature flag if found, otherwise nil
|
471
|
-
feature_flag = @feature_flag_key_map[feature_flag_key]
|
472
|
-
return feature_flag if feature_flag
|
473
|
-
|
474
|
-
@logger.log Logger::ERROR, "Feature flag key '#{feature_flag_key}' is not in datafile."
|
475
|
-
nil
|
476
|
-
end
|
477
|
-
|
478
|
-
def get_feature_variable(feature_flag, variable_key)
|
479
|
-
# Retrieves the variable with the given key for the given feature
|
480
|
-
#
|
481
|
-
# feature_flag - The feature flag for which we are retrieving the variable
|
482
|
-
# variable_key - String variable key
|
483
|
-
#
|
484
|
-
# Returns variable if found, otherwise nil
|
485
|
-
feature_flag_key = feature_flag['key']
|
486
|
-
variable = @feature_variable_key_map[feature_flag_key][variable_key]
|
487
|
-
return variable if variable
|
488
|
-
|
489
|
-
@logger.log Logger::ERROR, "No feature variable was found for key '#{variable_key}' in feature flag "\
|
490
|
-
"'#{feature_flag_key}'."
|
491
|
-
nil
|
492
|
-
end
|
493
|
-
|
494
|
-
def get_rollout_from_id(rollout_id)
|
495
|
-
# Retrieves the rollout with the given ID
|
496
|
-
#
|
497
|
-
# rollout_id - String rollout ID
|
498
|
-
#
|
499
|
-
# Returns the rollout if found, otherwise nil
|
500
|
-
rollout = @rollout_id_map[rollout_id]
|
501
|
-
return rollout if rollout
|
502
|
-
|
503
|
-
@logger.log Logger::ERROR, "Rollout with ID '#{rollout_id}' is not in the datafile."
|
504
|
-
nil
|
505
|
-
end
|
506
|
-
|
507
|
-
def feature_experiment?(experiment_id)
|
508
|
-
# Determines if given experiment is a feature test.
|
509
|
-
#
|
510
|
-
# experiment_id - String experiment ID
|
511
|
-
#
|
512
|
-
# Returns true if experiment belongs to any feature,
|
513
|
-
# false otherwise.
|
514
|
-
@experiment_feature_map.key?(experiment_id)
|
515
|
-
end
|
516
|
-
|
517
|
-
def rollout_experiment?(experiment_id)
|
518
|
-
# Determines if given experiment is a rollout test.
|
519
|
-
#
|
520
|
-
# experiment_id - String experiment ID
|
521
|
-
#
|
522
|
-
# Returns true if experiment belongs to any rollout,
|
523
|
-
# false otherwise.
|
524
|
-
@rollout_experiment_id_map.key?(experiment_id)
|
525
|
-
end
|
526
|
-
|
527
|
-
private
|
528
|
-
|
529
|
-
def generate_feature_variation_map(feature_flags)
|
530
|
-
flag_variation_map = {}
|
531
|
-
feature_flags.each do |flag|
|
532
|
-
variations = []
|
533
|
-
get_rules_for_flag(flag).each do |rule|
|
534
|
-
rule['variations'].each do |rule_variation|
|
535
|
-
variations.push(rule_variation) if variations.select { |variation| variation['id'] == rule_variation['id'] }.empty?
|
536
|
-
end
|
537
|
-
end
|
538
|
-
flag_variation_map[flag['key']] = variations
|
539
|
-
end
|
540
|
-
flag_variation_map
|
541
|
-
end
|
542
|
-
|
543
|
-
def generate_key_map(array, key, first_value: false)
|
544
|
-
# Helper method to generate map from key to hash in array of hashes
|
545
|
-
#
|
546
|
-
# array - Array consisting of hash
|
547
|
-
# key - Key in each hash which will be key in the map
|
548
|
-
# first_value - Determines which value to save if there are duplicate keys. By default the last instance of the key
|
549
|
-
# will be saved. Set to true to save the first key/value encountered.
|
550
|
-
#
|
551
|
-
# Returns map mapping key to hash
|
552
|
-
|
553
|
-
array
|
554
|
-
.group_by { |obj| obj[key] }
|
555
|
-
.transform_values { |group| first_value ? group.first : group.last }
|
556
|
-
end
|
557
|
-
end
|
558
|
-
end
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright 2019-2022, 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 :datafile, :account_id, :attributes, :audiences, :typed_audiences, :events,
|
28
|
+
:experiments, :feature_flags, :groups, :project_id, :bot_filtering, :revision,
|
29
|
+
:sdk_key, :environment_key, :rollouts, :version, :send_flag_decisions,
|
30
|
+
:attribute_key_map, :audience_id_map, :event_key_map, :experiment_feature_map,
|
31
|
+
:experiment_id_map, :experiment_key_map, :feature_flag_key_map, :feature_variable_key_map,
|
32
|
+
:group_id_map, :rollout_id_map, :rollout_experiment_id_map, :variation_id_map,
|
33
|
+
:variation_id_to_variable_usage_map, :variation_key_map, :variation_id_map_by_experiment_id,
|
34
|
+
:variation_key_map_by_experiment_id, :flag_variation_map, :integration_key_map, :integrations,
|
35
|
+
:public_key_for_odp, :host_for_odp, :all_segments
|
36
|
+
# Boolean - denotes if Optimizely should remove the last block of visitors' IP address before storing event data
|
37
|
+
attr_reader :anonymize_ip
|
38
|
+
|
39
|
+
def initialize(datafile, logger, error_handler)
|
40
|
+
# ProjectConfig init method to fetch and set project config data
|
41
|
+
#
|
42
|
+
# datafile - JSON string representing the project
|
43
|
+
super()
|
44
|
+
|
45
|
+
config = JSON.parse(datafile)
|
46
|
+
|
47
|
+
@datafile = datafile
|
48
|
+
@error_handler = error_handler
|
49
|
+
@logger = logger
|
50
|
+
@version = config['version']
|
51
|
+
|
52
|
+
raise InvalidDatafileVersionError, @version unless Helpers::Constants::SUPPORTED_VERSIONS.value?(@version)
|
53
|
+
|
54
|
+
@account_id = config['accountId']
|
55
|
+
@attributes = config.fetch('attributes', [])
|
56
|
+
@audiences = config.fetch('audiences', [])
|
57
|
+
@typed_audiences = config.fetch('typedAudiences', [])
|
58
|
+
@events = config.fetch('events', [])
|
59
|
+
@experiments = config['experiments']
|
60
|
+
@feature_flags = config.fetch('featureFlags', [])
|
61
|
+
@groups = config.fetch('groups', [])
|
62
|
+
@project_id = config['projectId']
|
63
|
+
@anonymize_ip = config.key?('anonymizeIP') ? config['anonymizeIP'] : false
|
64
|
+
@bot_filtering = config['botFiltering']
|
65
|
+
@revision = config['revision']
|
66
|
+
@sdk_key = config.fetch('sdkKey', '')
|
67
|
+
@environment_key = config.fetch('environmentKey', '')
|
68
|
+
@rollouts = config.fetch('rollouts', [])
|
69
|
+
@send_flag_decisions = config.fetch('sendFlagDecisions', false)
|
70
|
+
@integrations = config.fetch('integrations', [])
|
71
|
+
|
72
|
+
# Json type is represented in datafile as a subtype of string for the sake of backwards compatibility.
|
73
|
+
# Converting it to a first-class json type while creating Project Config
|
74
|
+
@feature_flags.each do |feature_flag|
|
75
|
+
feature_flag['variables'].each do |variable|
|
76
|
+
if variable['type'] == 'string' && variable['subType'] == 'json'
|
77
|
+
variable['type'] = 'json'
|
78
|
+
variable.delete('subType')
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Utility maps for quick lookup
|
84
|
+
@attribute_key_map = generate_key_map(@attributes, 'key')
|
85
|
+
@event_key_map = generate_key_map(@events, 'key')
|
86
|
+
@group_id_map = generate_key_map(@groups, 'id')
|
87
|
+
@group_id_map.each do |key, group|
|
88
|
+
exps = group.fetch('experiments')
|
89
|
+
exps.each do |exp|
|
90
|
+
@experiments.push(exp.merge('groupId' => key))
|
91
|
+
end
|
92
|
+
end
|
93
|
+
@experiment_key_map = generate_key_map(@experiments, 'key')
|
94
|
+
@experiment_id_map = generate_key_map(@experiments, 'id')
|
95
|
+
@audience_id_map = generate_key_map(@audiences, 'id')
|
96
|
+
@integration_key_map = generate_key_map(@integrations, 'key', first_value: true)
|
97
|
+
@audience_id_map = @audience_id_map.merge(generate_key_map(@typed_audiences, 'id')) unless @typed_audiences.empty?
|
98
|
+
@variation_id_map = {}
|
99
|
+
@variation_key_map = {}
|
100
|
+
@variation_id_map_by_experiment_id = {}
|
101
|
+
@variation_key_map_by_experiment_id = {}
|
102
|
+
@variation_id_to_variable_usage_map = {}
|
103
|
+
@variation_id_to_experiment_map = {}
|
104
|
+
@flag_variation_map = {}
|
105
|
+
|
106
|
+
@experiment_id_map.each_value do |exp|
|
107
|
+
# Excludes experiments from rollouts
|
108
|
+
variations = exp.fetch('variations')
|
109
|
+
variations.each do |variation|
|
110
|
+
variation_id = variation['id']
|
111
|
+
@variation_id_to_experiment_map[variation_id] = exp
|
112
|
+
end
|
113
|
+
end
|
114
|
+
@rollout_id_map = generate_key_map(@rollouts, 'id')
|
115
|
+
# split out the experiment key map for rollouts
|
116
|
+
@rollout_experiment_id_map = {}
|
117
|
+
@rollout_id_map.each_value do |rollout|
|
118
|
+
exps = rollout.fetch('experiments')
|
119
|
+
@rollout_experiment_id_map = @rollout_experiment_id_map.merge(generate_key_map(exps, 'id'))
|
120
|
+
end
|
121
|
+
|
122
|
+
if (odp_integration = @integration_key_map&.fetch('odp', nil))
|
123
|
+
@public_key_for_odp = odp_integration['publicKey']
|
124
|
+
@host_for_odp = odp_integration['host']
|
125
|
+
end
|
126
|
+
|
127
|
+
@all_segments = []
|
128
|
+
@audience_id_map.each_value do |audience|
|
129
|
+
@all_segments.concat Audience.get_segments(audience['conditions'])
|
130
|
+
end
|
131
|
+
|
132
|
+
@flag_variation_map = generate_feature_variation_map(@feature_flags)
|
133
|
+
@all_experiments = @experiment_id_map.merge(@rollout_experiment_id_map)
|
134
|
+
@all_experiments.each do |id, exp|
|
135
|
+
variations = exp.fetch('variations')
|
136
|
+
variations.each do |variation|
|
137
|
+
variation_id = variation['id']
|
138
|
+
variation['featureEnabled'] = variation['featureEnabled'] == true
|
139
|
+
variation_variables = variation['variables']
|
140
|
+
next if variation_variables.nil?
|
141
|
+
|
142
|
+
@variation_id_to_variable_usage_map[variation_id] = generate_key_map(variation_variables, 'id')
|
143
|
+
end
|
144
|
+
@variation_id_map[exp['key']] = generate_key_map(variations, 'id')
|
145
|
+
@variation_key_map[exp['key']] = generate_key_map(variations, 'key')
|
146
|
+
@variation_id_map_by_experiment_id[id] = generate_key_map(variations, 'id')
|
147
|
+
@variation_key_map_by_experiment_id[id] = generate_key_map(variations, 'key')
|
148
|
+
end
|
149
|
+
@feature_flag_key_map = generate_key_map(@feature_flags, 'key')
|
150
|
+
@experiment_feature_map = {}
|
151
|
+
@feature_variable_key_map = {}
|
152
|
+
@feature_flag_key_map.each do |key, feature_flag|
|
153
|
+
@feature_variable_key_map[key] = generate_key_map(feature_flag['variables'], 'key')
|
154
|
+
feature_flag['experimentIds'].each do |experiment_id|
|
155
|
+
@experiment_feature_map[experiment_id] = [feature_flag['id']]
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def get_rules_for_flag(feature_flag)
|
161
|
+
# Retrieves rules for a given feature flag
|
162
|
+
#
|
163
|
+
# feature_flag - String key representing the feature_flag
|
164
|
+
#
|
165
|
+
# Returns rules in feature flag
|
166
|
+
rules = feature_flag['experimentIds'].map { |exp_id| @experiment_id_map[exp_id] }
|
167
|
+
rollout = feature_flag['rolloutId'].empty? ? nil : @rollout_id_map[feature_flag['rolloutId']]
|
168
|
+
|
169
|
+
if rollout
|
170
|
+
rollout_experiments = rollout.fetch('experiments')
|
171
|
+
rollout_experiments.each do |exp|
|
172
|
+
rules.push(exp)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
rules
|
176
|
+
end
|
177
|
+
|
178
|
+
def self.create(datafile, logger, error_handler, skip_json_validation)
|
179
|
+
# Looks up and sets datafile and config based on response body.
|
180
|
+
#
|
181
|
+
# datafile - JSON string representing the Optimizely project.
|
182
|
+
# logger - Provides a logger instance.
|
183
|
+
# error_handler - Provides a handle_error method to handle exceptions.
|
184
|
+
# skip_json_validation - Optional boolean param which allows skipping JSON schema
|
185
|
+
# validation upon object invocation. By default JSON schema validation will be performed.
|
186
|
+
# Returns instance of DatafileProjectConfig, nil otherwise.
|
187
|
+
logger ||= SimpleLogger.new
|
188
|
+
if !skip_json_validation && !Helpers::Validator.datafile_valid?(datafile)
|
189
|
+
logger.log(Logger::ERROR, InvalidInputError.new('datafile').message)
|
190
|
+
return nil
|
191
|
+
end
|
192
|
+
|
193
|
+
begin
|
194
|
+
config = new(datafile, logger, error_handler)
|
195
|
+
rescue StandardError => e
|
196
|
+
error_to_handle = e.instance_of?(InvalidDatafileVersionError) ? e : InvalidInputError.new('datafile')
|
197
|
+
error_msg = error_to_handle.message
|
198
|
+
|
199
|
+
logger.log(Logger::ERROR, error_msg)
|
200
|
+
error_handler.handle_error error_to_handle
|
201
|
+
return nil
|
202
|
+
end
|
203
|
+
|
204
|
+
config
|
205
|
+
end
|
206
|
+
|
207
|
+
def experiment_running?(experiment)
|
208
|
+
# Determine if experiment corresponding to given key is running
|
209
|
+
#
|
210
|
+
# experiment - Experiment
|
211
|
+
#
|
212
|
+
# Returns true if experiment is running
|
213
|
+
RUNNING_EXPERIMENT_STATUS.include?(experiment['status'])
|
214
|
+
end
|
215
|
+
|
216
|
+
def get_experiment_from_key(experiment_key)
|
217
|
+
# Retrieves experiment ID for a given key
|
218
|
+
#
|
219
|
+
# experiment_key - String key representing the experiment
|
220
|
+
#
|
221
|
+
# Returns Experiment or nil if not found
|
222
|
+
|
223
|
+
experiment = @experiment_key_map[experiment_key]
|
224
|
+
return experiment if experiment
|
225
|
+
|
226
|
+
invalid_experiment_error = InvalidExperimentError.new(experiment_key: experiment_key)
|
227
|
+
@logger.log Logger::ERROR, invalid_experiment_error.message
|
228
|
+
@error_handler.handle_error invalid_experiment_error
|
229
|
+
nil
|
230
|
+
end
|
231
|
+
|
232
|
+
def get_experiment_from_id(experiment_id)
|
233
|
+
# Retrieves experiment ID for a given key
|
234
|
+
#
|
235
|
+
# experiment_id - String id representing the experiment
|
236
|
+
#
|
237
|
+
# Returns Experiment or nil if not found
|
238
|
+
|
239
|
+
experiment = @experiment_id_map[experiment_id]
|
240
|
+
return experiment if experiment
|
241
|
+
|
242
|
+
invalid_experiment_error = InvalidExperimentError.new(experiment_id: experiment_id)
|
243
|
+
@logger.log Logger::ERROR, invalid_experiment_error.message
|
244
|
+
@error_handler.handle_error invalid_experiment_error
|
245
|
+
nil
|
246
|
+
end
|
247
|
+
|
248
|
+
def get_experiment_key(experiment_id)
|
249
|
+
# Retrieves experiment key for a given ID.
|
250
|
+
#
|
251
|
+
# experiment_id - String ID representing the experiment.
|
252
|
+
#
|
253
|
+
# Returns String key.
|
254
|
+
|
255
|
+
experiment = @experiment_id_map[experiment_id]
|
256
|
+
return experiment['key'] unless experiment.nil?
|
257
|
+
|
258
|
+
invalid_experiment_error = InvalidExperimentError.new(experiment_id: experiment_id)
|
259
|
+
@logger.log Logger::ERROR, invalid_experiment_error.message
|
260
|
+
@error_handler.handle_error invalid_experiment_error
|
261
|
+
nil
|
262
|
+
end
|
263
|
+
|
264
|
+
def get_event_from_key(event_key)
|
265
|
+
# Get event for the provided event key.
|
266
|
+
#
|
267
|
+
# event_key - Event key for which event is to be determined.
|
268
|
+
#
|
269
|
+
# Returns Event corresponding to the provided event key.
|
270
|
+
|
271
|
+
event = @event_key_map[event_key]
|
272
|
+
return event if event
|
273
|
+
|
274
|
+
invalid_event_error = InvalidEventError.new(event_key)
|
275
|
+
@logger.log Logger::ERROR, invalid_event_error.message
|
276
|
+
@error_handler.handle_error invalid_event_error
|
277
|
+
nil
|
278
|
+
end
|
279
|
+
|
280
|
+
def get_audience_from_id(audience_id)
|
281
|
+
# Get audience for the provided audience ID
|
282
|
+
#
|
283
|
+
# audience_id - ID of the audience
|
284
|
+
#
|
285
|
+
# Returns the audience
|
286
|
+
|
287
|
+
audience = @audience_id_map[audience_id]
|
288
|
+
return audience if audience
|
289
|
+
|
290
|
+
invalid_audience_error = InvalidAudienceError.new(audience_id)
|
291
|
+
@logger.log Logger::ERROR, invalid_audience_error.message
|
292
|
+
@error_handler.handle_error invalid_audience_error
|
293
|
+
nil
|
294
|
+
end
|
295
|
+
|
296
|
+
def get_variation_from_flag(flag_key, target_value, attribute)
|
297
|
+
variations = @flag_variation_map[flag_key]
|
298
|
+
return variations.select { |variation| variation[attribute] == target_value }.first if variations
|
299
|
+
|
300
|
+
nil
|
301
|
+
end
|
302
|
+
|
303
|
+
def get_variation_from_id(experiment_key, variation_id)
|
304
|
+
# Get variation given experiment key and variation ID
|
305
|
+
#
|
306
|
+
# experiment_key - Key representing parent experiment of variation
|
307
|
+
# variation_id - ID of the variation
|
308
|
+
#
|
309
|
+
# Returns the variation or nil if not found
|
310
|
+
|
311
|
+
variation_id_map = @variation_id_map[experiment_key]
|
312
|
+
if variation_id_map
|
313
|
+
variation = variation_id_map[variation_id]
|
314
|
+
return variation if variation
|
315
|
+
|
316
|
+
invalid_variation_error = InvalidVariationError.new(variation_id: variation_id)
|
317
|
+
@logger.log Logger::ERROR, invalid_variation_error.message
|
318
|
+
@error_handler.handle_error invalid_variation_error
|
319
|
+
return nil
|
320
|
+
end
|
321
|
+
|
322
|
+
invalid_experiment_error = InvalidExperimentError.new(experiment_key: experiment_key)
|
323
|
+
@logger.log Logger::ERROR, invalid_experiment_error.message
|
324
|
+
@error_handler.handle_error invalid_experiment_error
|
325
|
+
nil
|
326
|
+
end
|
327
|
+
|
328
|
+
def get_variation_from_id_by_experiment_id(experiment_id, variation_id)
|
329
|
+
# Get variation given experiment ID and variation ID
|
330
|
+
#
|
331
|
+
# experiment_id - ID representing parent experiment of variation
|
332
|
+
# variation_id - ID of the variation
|
333
|
+
#
|
334
|
+
# Returns the variation or nil if not found
|
335
|
+
|
336
|
+
variation_id_map_by_experiment_id = @variation_id_map_by_experiment_id[experiment_id]
|
337
|
+
if variation_id_map_by_experiment_id
|
338
|
+
variation = variation_id_map_by_experiment_id[variation_id]
|
339
|
+
return variation if variation
|
340
|
+
|
341
|
+
invalid_variation_error = InvalidVariationError.new(variation_id: variation_id)
|
342
|
+
@logger.log Logger::ERROR, invalid_variation_error.message
|
343
|
+
@error_handler.handle_error invalid_variation_error
|
344
|
+
return nil
|
345
|
+
end
|
346
|
+
|
347
|
+
invalid_experiment_error = InvalidExperimentError.new(experiment_id: experiment_id)
|
348
|
+
@logger.log Logger::ERROR, invalid_experiment_error.message
|
349
|
+
@error_handler.handle_error invalid_experiment_error
|
350
|
+
nil
|
351
|
+
end
|
352
|
+
|
353
|
+
def get_variation_id_from_key_by_experiment_id(experiment_id, variation_key)
|
354
|
+
# Get variation given experiment ID and variation key
|
355
|
+
#
|
356
|
+
# experiment_id - ID representing parent experiment of variation
|
357
|
+
# variation_key - Key of the variation
|
358
|
+
#
|
359
|
+
# Returns the variation or nil if not found
|
360
|
+
|
361
|
+
variation_key_map = @variation_key_map_by_experiment_id[experiment_id]
|
362
|
+
if variation_key_map
|
363
|
+
variation = variation_key_map[variation_key]
|
364
|
+
return variation['id'] if variation
|
365
|
+
|
366
|
+
invalid_variation_error = InvalidVariationError.new(variation_key: variation_key)
|
367
|
+
@logger.log Logger::ERROR, invalid_variation_error.message
|
368
|
+
@error_handler.handle_error invalid_variation_error
|
369
|
+
return nil
|
370
|
+
end
|
371
|
+
|
372
|
+
invalid_experiment_error = InvalidExperimentError.new(experiment_id: experiment_id)
|
373
|
+
@logger.log Logger::ERROR, invalid_experiment_error.message
|
374
|
+
@error_handler.handle_error invalid_experiment_error
|
375
|
+
nil
|
376
|
+
end
|
377
|
+
|
378
|
+
def get_variation_id_from_key(experiment_key, variation_key)
|
379
|
+
# Get variation ID given experiment key and variation key
|
380
|
+
#
|
381
|
+
# experiment_key - Key representing parent experiment of variation
|
382
|
+
# variation_key - Key of the variation
|
383
|
+
#
|
384
|
+
# Returns ID of the variation
|
385
|
+
|
386
|
+
variation_key_map = @variation_key_map[experiment_key]
|
387
|
+
if variation_key_map
|
388
|
+
variation = variation_key_map[variation_key]
|
389
|
+
return variation['id'] if variation
|
390
|
+
|
391
|
+
invalid_variation_error = InvalidVariationError.new(variation_key: variation_key)
|
392
|
+
@logger.log Logger::ERROR, invalid_variation_error.message
|
393
|
+
@error_handler.handle_error invalid_variation_error
|
394
|
+
return nil
|
395
|
+
end
|
396
|
+
|
397
|
+
invalid_experiment_error = InvalidExperimentError.new(experiment_key: experiment_key)
|
398
|
+
@logger.log Logger::ERROR, invalid_experiment_error.message
|
399
|
+
@error_handler.handle_error invalid_experiment_error
|
400
|
+
nil
|
401
|
+
end
|
402
|
+
|
403
|
+
def get_whitelisted_variations(experiment_id)
|
404
|
+
# Retrieves whitelisted variations for a given experiment id
|
405
|
+
#
|
406
|
+
# experiment_id - String id representing the experiment
|
407
|
+
#
|
408
|
+
# Returns whitelisted variations for the experiment or nil
|
409
|
+
|
410
|
+
experiment = @experiment_id_map[experiment_id]
|
411
|
+
return experiment['forcedVariations'] if experiment
|
412
|
+
|
413
|
+
invalid_experiment_error = InvalidExperimentError.new(experiment_id: experiment_id)
|
414
|
+
@logger.log Logger::ERROR, invalid_experiment_error.message
|
415
|
+
@error_handler.handle_error invalid_experiment_error
|
416
|
+
end
|
417
|
+
|
418
|
+
def get_attribute_id(attribute_key)
|
419
|
+
# Get attribute ID for the provided attribute key.
|
420
|
+
#
|
421
|
+
# Args:
|
422
|
+
# Attribute key for which attribute is to be fetched.
|
423
|
+
#
|
424
|
+
# Returns:
|
425
|
+
# Attribute ID corresponding to the provided attribute key.
|
426
|
+
attribute = @attribute_key_map[attribute_key]
|
427
|
+
has_reserved_prefix = attribute_key.to_s.start_with?(RESERVED_ATTRIBUTE_PREFIX)
|
428
|
+
unless attribute.nil?
|
429
|
+
if has_reserved_prefix
|
430
|
+
@logger.log(Logger::WARN, "Attribute '#{attribute_key}' unexpectedly has reserved prefix '#{RESERVED_ATTRIBUTE_PREFIX}'; "\
|
431
|
+
'using attribute ID instead of reserved attribute name.')
|
432
|
+
end
|
433
|
+
return attribute['id']
|
434
|
+
end
|
435
|
+
return attribute_key if has_reserved_prefix
|
436
|
+
|
437
|
+
invalid_attribute_error = InvalidAttributeError.new(attribute_key)
|
438
|
+
@logger.log Logger::ERROR, invalid_attribute_error.message
|
439
|
+
@error_handler.handle_error invalid_attribute_error
|
440
|
+
nil
|
441
|
+
end
|
442
|
+
|
443
|
+
def variation_id_exists?(experiment_id, variation_id)
|
444
|
+
# Determines if a given experiment ID / variation ID pair exists in the datafile
|
445
|
+
#
|
446
|
+
# experiment_id - String experiment ID
|
447
|
+
# variation_id - String variation ID
|
448
|
+
#
|
449
|
+
# Returns true if variation is in datafile
|
450
|
+
|
451
|
+
experiment_key = get_experiment_key(experiment_id)
|
452
|
+
variation_id_map = @variation_id_map[experiment_key]
|
453
|
+
if variation_id_map
|
454
|
+
variation = variation_id_map[variation_id]
|
455
|
+
return true if variation
|
456
|
+
|
457
|
+
invalid_variation_error = InvalidVariationError.new(variation_id: variation_id)
|
458
|
+
@logger.log Logger::ERROR, invalid_variation_error.message
|
459
|
+
@error_handler.handle_error invalid_variation_error
|
460
|
+
end
|
461
|
+
|
462
|
+
false
|
463
|
+
end
|
464
|
+
|
465
|
+
def get_feature_flag_from_key(feature_flag_key)
|
466
|
+
# Retrieves the feature flag with the given key
|
467
|
+
#
|
468
|
+
# feature_flag_key - String feature key
|
469
|
+
#
|
470
|
+
# Returns feature flag if found, otherwise nil
|
471
|
+
feature_flag = @feature_flag_key_map[feature_flag_key]
|
472
|
+
return feature_flag if feature_flag
|
473
|
+
|
474
|
+
@logger.log Logger::ERROR, "Feature flag key '#{feature_flag_key}' is not in datafile."
|
475
|
+
nil
|
476
|
+
end
|
477
|
+
|
478
|
+
def get_feature_variable(feature_flag, variable_key)
|
479
|
+
# Retrieves the variable with the given key for the given feature
|
480
|
+
#
|
481
|
+
# feature_flag - The feature flag for which we are retrieving the variable
|
482
|
+
# variable_key - String variable key
|
483
|
+
#
|
484
|
+
# Returns variable if found, otherwise nil
|
485
|
+
feature_flag_key = feature_flag['key']
|
486
|
+
variable = @feature_variable_key_map[feature_flag_key][variable_key]
|
487
|
+
return variable if variable
|
488
|
+
|
489
|
+
@logger.log Logger::ERROR, "No feature variable was found for key '#{variable_key}' in feature flag "\
|
490
|
+
"'#{feature_flag_key}'."
|
491
|
+
nil
|
492
|
+
end
|
493
|
+
|
494
|
+
def get_rollout_from_id(rollout_id)
|
495
|
+
# Retrieves the rollout with the given ID
|
496
|
+
#
|
497
|
+
# rollout_id - String rollout ID
|
498
|
+
#
|
499
|
+
# Returns the rollout if found, otherwise nil
|
500
|
+
rollout = @rollout_id_map[rollout_id]
|
501
|
+
return rollout if rollout
|
502
|
+
|
503
|
+
@logger.log Logger::ERROR, "Rollout with ID '#{rollout_id}' is not in the datafile."
|
504
|
+
nil
|
505
|
+
end
|
506
|
+
|
507
|
+
def feature_experiment?(experiment_id)
|
508
|
+
# Determines if given experiment is a feature test.
|
509
|
+
#
|
510
|
+
# experiment_id - String experiment ID
|
511
|
+
#
|
512
|
+
# Returns true if experiment belongs to any feature,
|
513
|
+
# false otherwise.
|
514
|
+
@experiment_feature_map.key?(experiment_id)
|
515
|
+
end
|
516
|
+
|
517
|
+
def rollout_experiment?(experiment_id)
|
518
|
+
# Determines if given experiment is a rollout test.
|
519
|
+
#
|
520
|
+
# experiment_id - String experiment ID
|
521
|
+
#
|
522
|
+
# Returns true if experiment belongs to any rollout,
|
523
|
+
# false otherwise.
|
524
|
+
@rollout_experiment_id_map.key?(experiment_id)
|
525
|
+
end
|
526
|
+
|
527
|
+
private
|
528
|
+
|
529
|
+
def generate_feature_variation_map(feature_flags)
|
530
|
+
flag_variation_map = {}
|
531
|
+
feature_flags.each do |flag|
|
532
|
+
variations = []
|
533
|
+
get_rules_for_flag(flag).each do |rule|
|
534
|
+
rule['variations'].each do |rule_variation|
|
535
|
+
variations.push(rule_variation) if variations.select { |variation| variation['id'] == rule_variation['id'] }.empty?
|
536
|
+
end
|
537
|
+
end
|
538
|
+
flag_variation_map[flag['key']] = variations
|
539
|
+
end
|
540
|
+
flag_variation_map
|
541
|
+
end
|
542
|
+
|
543
|
+
def generate_key_map(array, key, first_value: false)
|
544
|
+
# Helper method to generate map from key to hash in array of hashes
|
545
|
+
#
|
546
|
+
# array - Array consisting of hash
|
547
|
+
# key - Key in each hash which will be key in the map
|
548
|
+
# first_value - Determines which value to save if there are duplicate keys. By default the last instance of the key
|
549
|
+
# will be saved. Set to true to save the first key/value encountered.
|
550
|
+
#
|
551
|
+
# Returns map mapping key to hash
|
552
|
+
|
553
|
+
array
|
554
|
+
.group_by { |obj| obj[key] }
|
555
|
+
.transform_values { |group| first_value ? group.first : group.last }
|
556
|
+
end
|
557
|
+
end
|
558
|
+
end
|