optimizely-sdk 4.0.1 → 5.0.0.pre.beta

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.
@@ -1,539 +1,542 @@
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')
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
- if !skip_json_validation && !Helpers::Validator.datafile_valid?(datafile)
188
- default_logger = SimpleLogger.new
189
- default_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
- default_logger = SimpleLogger.new
197
- error_to_handle = e.instance_of?(InvalidDatafileVersionError) ? e : InvalidInputError.new('datafile')
198
- error_msg = error_to_handle.message
199
-
200
- default_logger.log(Logger::ERROR, error_msg)
201
- error_handler.handle_error error_to_handle
202
- return nil
203
- end
204
-
205
- config
206
- end
207
-
208
- def experiment_running?(experiment)
209
- # Determine if experiment corresponding to given key is running
210
- #
211
- # experiment - Experiment
212
- #
213
- # Returns true if experiment is running
214
- RUNNING_EXPERIMENT_STATUS.include?(experiment['status'])
215
- end
216
-
217
- def get_experiment_from_key(experiment_key)
218
- # Retrieves experiment ID for a given key
219
- #
220
- # experiment_key - String key representing the experiment
221
- #
222
- # Returns Experiment or nil if not found
223
-
224
- experiment = @experiment_key_map[experiment_key]
225
- return experiment if experiment
226
-
227
- @logger.log Logger::ERROR, "Experiment key '#{experiment_key}' is not in datafile."
228
- @error_handler.handle_error InvalidExperimentError
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
- @logger.log Logger::ERROR, "Experiment id '#{experiment_id}' is not in datafile."
243
- @error_handler.handle_error InvalidExperimentError
244
- nil
245
- end
246
-
247
- def get_experiment_key(experiment_id)
248
- # Retrieves experiment key for a given ID.
249
- #
250
- # experiment_id - String ID representing the experiment.
251
- #
252
- # Returns String key.
253
-
254
- experiment = @experiment_id_map[experiment_id]
255
- return experiment['key'] unless experiment.nil?
256
-
257
- @logger.log Logger::ERROR, "Experiment id '#{experiment_id}' is not in datafile."
258
- @error_handler.handle_error InvalidExperimentError
259
- nil
260
- end
261
-
262
- def get_event_from_key(event_key)
263
- # Get event for the provided event key.
264
- #
265
- # event_key - Event key for which event is to be determined.
266
- #
267
- # Returns Event corresponding to the provided event key.
268
-
269
- event = @event_key_map[event_key]
270
- return event if event
271
-
272
- @logger.log Logger::ERROR, "Event '#{event_key}' is not in datafile."
273
- @error_handler.handle_error InvalidEventError
274
- nil
275
- end
276
-
277
- def get_audience_from_id(audience_id)
278
- # Get audience for the provided audience ID
279
- #
280
- # audience_id - ID of the audience
281
- #
282
- # Returns the audience
283
-
284
- audience = @audience_id_map[audience_id]
285
- return audience if audience
286
-
287
- @logger.log Logger::ERROR, "Audience '#{audience_id}' is not in datafile."
288
- @error_handler.handle_error InvalidAudienceError
289
- nil
290
- end
291
-
292
- def get_variation_from_flag(flag_key, target_value, attribute)
293
- variations = @flag_variation_map[flag_key]
294
- return variations.select { |variation| variation[attribute] == target_value }.first if variations
295
-
296
- nil
297
- end
298
-
299
- def get_variation_from_id(experiment_key, variation_id)
300
- # Get variation given experiment key and variation ID
301
- #
302
- # experiment_key - Key representing parent experiment of variation
303
- # variation_id - ID of the variation
304
- #
305
- # Returns the variation or nil if not found
306
-
307
- variation_id_map = @variation_id_map[experiment_key]
308
- if variation_id_map
309
- variation = variation_id_map[variation_id]
310
- return variation if variation
311
-
312
- @logger.log Logger::ERROR, "Variation id '#{variation_id}' is not in datafile."
313
- @error_handler.handle_error InvalidVariationError
314
- return nil
315
- end
316
-
317
- @logger.log Logger::ERROR, "Experiment key '#{experiment_key}' is not in datafile."
318
- @error_handler.handle_error InvalidExperimentError
319
- nil
320
- end
321
-
322
- def get_variation_from_id_by_experiment_id(experiment_id, variation_id)
323
- # Get variation given experiment ID and variation ID
324
- #
325
- # experiment_id - ID representing parent experiment of variation
326
- # variation_id - ID of the variation
327
- #
328
- # Returns the variation or nil if not found
329
-
330
- variation_id_map_by_experiment_id = @variation_id_map_by_experiment_id[experiment_id]
331
- if variation_id_map_by_experiment_id
332
- variation = variation_id_map_by_experiment_id[variation_id]
333
- return variation if variation
334
-
335
- @logger.log Logger::ERROR, "Variation id '#{variation_id}' is not in datafile."
336
- @error_handler.handle_error InvalidVariationError
337
- return nil
338
- end
339
-
340
- @logger.log Logger::ERROR, "Experiment id '#{experiment_id}' is not in datafile."
341
- @error_handler.handle_error InvalidExperimentError
342
- nil
343
- end
344
-
345
- def get_variation_id_from_key_by_experiment_id(experiment_id, variation_key)
346
- # Get variation given experiment ID and variation key
347
- #
348
- # experiment_id - ID representing parent experiment of variation
349
- # variation_key - Key of the variation
350
- #
351
- # Returns the variation or nil if not found
352
-
353
- variation_key_map = @variation_key_map_by_experiment_id[experiment_id]
354
- if variation_key_map
355
- variation = variation_key_map[variation_key]
356
- return variation['id'] if variation
357
-
358
- @logger.log Logger::ERROR, "Variation key '#{variation_key}' is not in datafile."
359
- @error_handler.handle_error InvalidVariationError
360
- return nil
361
- end
362
-
363
- @logger.log Logger::ERROR, "Experiment id '#{experiment_id}' is not in datafile."
364
- @error_handler.handle_error InvalidExperimentError
365
- nil
366
- end
367
-
368
- def get_variation_id_from_key(experiment_key, variation_key)
369
- # Get variation ID given experiment key and variation key
370
- #
371
- # experiment_key - Key representing parent experiment of variation
372
- # variation_key - Key of the variation
373
- #
374
- # Returns ID of the variation
375
-
376
- variation_key_map = @variation_key_map[experiment_key]
377
- if variation_key_map
378
- variation = variation_key_map[variation_key]
379
- return variation['id'] if variation
380
-
381
- @logger.log Logger::ERROR, "Variation key '#{variation_key}' is not in datafile."
382
- @error_handler.handle_error InvalidVariationError
383
- return nil
384
- end
385
-
386
- @logger.log Logger::ERROR, "Experiment key '#{experiment_key}' is not in datafile."
387
- @error_handler.handle_error InvalidExperimentError
388
- nil
389
- end
390
-
391
- def get_whitelisted_variations(experiment_id)
392
- # Retrieves whitelisted variations for a given experiment id
393
- #
394
- # experiment_id - String id representing the experiment
395
- #
396
- # Returns whitelisted variations for the experiment or nil
397
-
398
- experiment = @experiment_id_map[experiment_id]
399
- return experiment['forcedVariations'] if experiment
400
-
401
- @logger.log Logger::ERROR, "Experiment ID '#{experiment_id}' is not in datafile."
402
- @error_handler.handle_error InvalidExperimentError
403
- end
404
-
405
- def get_attribute_id(attribute_key)
406
- # Get attribute ID for the provided attribute key.
407
- #
408
- # Args:
409
- # Attribute key for which attribute is to be fetched.
410
- #
411
- # Returns:
412
- # Attribute ID corresponding to the provided attribute key.
413
- attribute = @attribute_key_map[attribute_key]
414
- has_reserved_prefix = attribute_key.to_s.start_with?(RESERVED_ATTRIBUTE_PREFIX)
415
- unless attribute.nil?
416
- if has_reserved_prefix
417
- @logger.log(Logger::WARN, "Attribute '#{attribute_key}' unexpectedly has reserved prefix '#{RESERVED_ATTRIBUTE_PREFIX}'; "\
418
- 'using attribute ID instead of reserved attribute name.')
419
- end
420
- return attribute['id']
421
- end
422
- return attribute_key if has_reserved_prefix
423
-
424
- @logger.log Logger::ERROR, "Attribute key '#{attribute_key}' is not in datafile."
425
- @error_handler.handle_error InvalidAttributeError
426
- nil
427
- end
428
-
429
- def variation_id_exists?(experiment_id, variation_id)
430
- # Determines if a given experiment ID / variation ID pair exists in the datafile
431
- #
432
- # experiment_id - String experiment ID
433
- # variation_id - String variation ID
434
- #
435
- # Returns true if variation is in datafile
436
-
437
- experiment_key = get_experiment_key(experiment_id)
438
- variation_id_map = @variation_id_map[experiment_key]
439
- if variation_id_map
440
- variation = variation_id_map[variation_id]
441
- return true if variation
442
-
443
- @logger.log Logger::ERROR, "Variation ID '#{variation_id}' is not in datafile."
444
- @error_handler.handle_error InvalidVariationError
445
- end
446
-
447
- false
448
- end
449
-
450
- def get_feature_flag_from_key(feature_flag_key)
451
- # Retrieves the feature flag with the given key
452
- #
453
- # feature_flag_key - String feature key
454
- #
455
- # Returns feature flag if found, otherwise nil
456
- feature_flag = @feature_flag_key_map[feature_flag_key]
457
- return feature_flag if feature_flag
458
-
459
- @logger.log Logger::ERROR, "Feature flag key '#{feature_flag_key}' is not in datafile."
460
- nil
461
- end
462
-
463
- def get_feature_variable(feature_flag, variable_key)
464
- # Retrieves the variable with the given key for the given feature
465
- #
466
- # feature_flag - The feature flag for which we are retrieving the variable
467
- # variable_key - String variable key
468
- #
469
- # Returns variable if found, otherwise nil
470
- feature_flag_key = feature_flag['key']
471
- variable = @feature_variable_key_map[feature_flag_key][variable_key]
472
- return variable if variable
473
-
474
- @logger.log Logger::ERROR, "No feature variable was found for key '#{variable_key}' in feature flag "\
475
- "'#{feature_flag_key}'."
476
- nil
477
- end
478
-
479
- def get_rollout_from_id(rollout_id)
480
- # Retrieves the rollout with the given ID
481
- #
482
- # rollout_id - String rollout ID
483
- #
484
- # Returns the rollout if found, otherwise nil
485
- rollout = @rollout_id_map[rollout_id]
486
- return rollout if rollout
487
-
488
- @logger.log Logger::ERROR, "Rollout with ID '#{rollout_id}' is not in the datafile."
489
- nil
490
- end
491
-
492
- def feature_experiment?(experiment_id)
493
- # Determines if given experiment is a feature test.
494
- #
495
- # experiment_id - String experiment ID
496
- #
497
- # Returns true if experiment belongs to any feature,
498
- # false otherwise.
499
- @experiment_feature_map.key?(experiment_id)
500
- end
501
-
502
- def rollout_experiment?(experiment_id)
503
- # Determines if given experiment is a rollout test.
504
- #
505
- # experiment_id - String experiment ID
506
- #
507
- # Returns true if experiment belongs to any rollout,
508
- # false otherwise.
509
- @rollout_experiment_id_map.key?(experiment_id)
510
- end
511
-
512
- private
513
-
514
- def generate_feature_variation_map(feature_flags)
515
- flag_variation_map = {}
516
- feature_flags.each do |flag|
517
- variations = []
518
- get_rules_for_flag(flag).each do |rule|
519
- rule['variations'].each do |rule_variation|
520
- variations.push(rule_variation) if variations.select { |variation| variation['id'] == rule_variation['id'] }.empty?
521
- end
522
- end
523
- flag_variation_map[flag['key']] = variations
524
- end
525
- flag_variation_map
526
- end
527
-
528
- def generate_key_map(array, key)
529
- # Helper method to generate map from key to hash in array of hashes
530
- #
531
- # array - Array consisting of hash
532
- # key - Key in each hash which will be key in the map
533
- #
534
- # Returns map mapping key to hash
535
-
536
- Hash[array.map { |obj| [obj[key], obj] }]
537
- end
538
- end
539
- 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
+ @logger.log Logger::ERROR, "Experiment key '#{experiment_key}' is not in datafile."
227
+ @error_handler.handle_error InvalidExperimentError
228
+ nil
229
+ end
230
+
231
+ def get_experiment_from_id(experiment_id)
232
+ # Retrieves experiment ID for a given key
233
+ #
234
+ # experiment_id - String id representing the experiment
235
+ #
236
+ # Returns Experiment or nil if not found
237
+
238
+ experiment = @experiment_id_map[experiment_id]
239
+ return experiment if experiment
240
+
241
+ @logger.log Logger::ERROR, "Experiment id '#{experiment_id}' is not in datafile."
242
+ @error_handler.handle_error InvalidExperimentError
243
+ nil
244
+ end
245
+
246
+ def get_experiment_key(experiment_id)
247
+ # Retrieves experiment key for a given ID.
248
+ #
249
+ # experiment_id - String ID representing the experiment.
250
+ #
251
+ # Returns String key.
252
+
253
+ experiment = @experiment_id_map[experiment_id]
254
+ return experiment['key'] unless experiment.nil?
255
+
256
+ @logger.log Logger::ERROR, "Experiment id '#{experiment_id}' is not in datafile."
257
+ @error_handler.handle_error InvalidExperimentError
258
+ nil
259
+ end
260
+
261
+ def get_event_from_key(event_key)
262
+ # Get event for the provided event key.
263
+ #
264
+ # event_key - Event key for which event is to be determined.
265
+ #
266
+ # Returns Event corresponding to the provided event key.
267
+
268
+ event = @event_key_map[event_key]
269
+ return event if event
270
+
271
+ @logger.log Logger::ERROR, "Event '#{event_key}' is not in datafile."
272
+ @error_handler.handle_error InvalidEventError
273
+ nil
274
+ end
275
+
276
+ def get_audience_from_id(audience_id)
277
+ # Get audience for the provided audience ID
278
+ #
279
+ # audience_id - ID of the audience
280
+ #
281
+ # Returns the audience
282
+
283
+ audience = @audience_id_map[audience_id]
284
+ return audience if audience
285
+
286
+ @logger.log Logger::ERROR, "Audience '#{audience_id}' is not in datafile."
287
+ @error_handler.handle_error InvalidAudienceError
288
+ nil
289
+ end
290
+
291
+ def get_variation_from_flag(flag_key, target_value, attribute)
292
+ variations = @flag_variation_map[flag_key]
293
+ return variations.select { |variation| variation[attribute] == target_value }.first if variations
294
+
295
+ nil
296
+ end
297
+
298
+ def get_variation_from_id(experiment_key, variation_id)
299
+ # Get variation given experiment key and variation ID
300
+ #
301
+ # experiment_key - Key representing parent experiment of variation
302
+ # variation_id - ID of the variation
303
+ #
304
+ # Returns the variation or nil if not found
305
+
306
+ variation_id_map = @variation_id_map[experiment_key]
307
+ if variation_id_map
308
+ variation = variation_id_map[variation_id]
309
+ return variation if variation
310
+
311
+ @logger.log Logger::ERROR, "Variation id '#{variation_id}' is not in datafile."
312
+ @error_handler.handle_error InvalidVariationError
313
+ return nil
314
+ end
315
+
316
+ @logger.log Logger::ERROR, "Experiment key '#{experiment_key}' is not in datafile."
317
+ @error_handler.handle_error InvalidExperimentError
318
+ nil
319
+ end
320
+
321
+ def get_variation_from_id_by_experiment_id(experiment_id, variation_id)
322
+ # Get variation given experiment ID and variation ID
323
+ #
324
+ # experiment_id - ID representing parent experiment of variation
325
+ # variation_id - ID of the variation
326
+ #
327
+ # Returns the variation or nil if not found
328
+
329
+ variation_id_map_by_experiment_id = @variation_id_map_by_experiment_id[experiment_id]
330
+ if variation_id_map_by_experiment_id
331
+ variation = variation_id_map_by_experiment_id[variation_id]
332
+ return variation if variation
333
+
334
+ @logger.log Logger::ERROR, "Variation id '#{variation_id}' is not in datafile."
335
+ @error_handler.handle_error InvalidVariationError
336
+ return nil
337
+ end
338
+
339
+ @logger.log Logger::ERROR, "Experiment id '#{experiment_id}' is not in datafile."
340
+ @error_handler.handle_error InvalidExperimentError
341
+ nil
342
+ end
343
+
344
+ def get_variation_id_from_key_by_experiment_id(experiment_id, variation_key)
345
+ # Get variation given experiment ID and variation key
346
+ #
347
+ # experiment_id - ID representing parent experiment of variation
348
+ # variation_key - Key of the variation
349
+ #
350
+ # Returns the variation or nil if not found
351
+
352
+ variation_key_map = @variation_key_map_by_experiment_id[experiment_id]
353
+ if variation_key_map
354
+ variation = variation_key_map[variation_key]
355
+ return variation['id'] if variation
356
+
357
+ @logger.log Logger::ERROR, "Variation key '#{variation_key}' is not in datafile."
358
+ @error_handler.handle_error InvalidVariationError
359
+ return nil
360
+ end
361
+
362
+ @logger.log Logger::ERROR, "Experiment id '#{experiment_id}' is not in datafile."
363
+ @error_handler.handle_error InvalidExperimentError
364
+ nil
365
+ end
366
+
367
+ def get_variation_id_from_key(experiment_key, variation_key)
368
+ # Get variation ID given experiment key and variation key
369
+ #
370
+ # experiment_key - Key representing parent experiment of variation
371
+ # variation_key - Key of the variation
372
+ #
373
+ # Returns ID of the variation
374
+
375
+ variation_key_map = @variation_key_map[experiment_key]
376
+ if variation_key_map
377
+ variation = variation_key_map[variation_key]
378
+ return variation['id'] if variation
379
+
380
+ @logger.log Logger::ERROR, "Variation key '#{variation_key}' is not in datafile."
381
+ @error_handler.handle_error InvalidVariationError
382
+ return nil
383
+ end
384
+
385
+ @logger.log Logger::ERROR, "Experiment key '#{experiment_key}' is not in datafile."
386
+ @error_handler.handle_error InvalidExperimentError
387
+ nil
388
+ end
389
+
390
+ def get_whitelisted_variations(experiment_id)
391
+ # Retrieves whitelisted variations for a given experiment id
392
+ #
393
+ # experiment_id - String id representing the experiment
394
+ #
395
+ # Returns whitelisted variations for the experiment or nil
396
+
397
+ experiment = @experiment_id_map[experiment_id]
398
+ return experiment['forcedVariations'] if experiment
399
+
400
+ @logger.log Logger::ERROR, "Experiment ID '#{experiment_id}' is not in datafile."
401
+ @error_handler.handle_error InvalidExperimentError
402
+ end
403
+
404
+ def get_attribute_id(attribute_key)
405
+ # Get attribute ID for the provided attribute key.
406
+ #
407
+ # Args:
408
+ # Attribute key for which attribute is to be fetched.
409
+ #
410
+ # Returns:
411
+ # Attribute ID corresponding to the provided attribute key.
412
+ attribute = @attribute_key_map[attribute_key]
413
+ has_reserved_prefix = attribute_key.to_s.start_with?(RESERVED_ATTRIBUTE_PREFIX)
414
+ unless attribute.nil?
415
+ if has_reserved_prefix
416
+ @logger.log(Logger::WARN, "Attribute '#{attribute_key}' unexpectedly has reserved prefix '#{RESERVED_ATTRIBUTE_PREFIX}'; "\
417
+ 'using attribute ID instead of reserved attribute name.')
418
+ end
419
+ return attribute['id']
420
+ end
421
+ return attribute_key if has_reserved_prefix
422
+
423
+ @logger.log Logger::ERROR, "Attribute key '#{attribute_key}' is not in datafile."
424
+ @error_handler.handle_error InvalidAttributeError
425
+ nil
426
+ end
427
+
428
+ def variation_id_exists?(experiment_id, variation_id)
429
+ # Determines if a given experiment ID / variation ID pair exists in the datafile
430
+ #
431
+ # experiment_id - String experiment ID
432
+ # variation_id - String variation ID
433
+ #
434
+ # Returns true if variation is in datafile
435
+
436
+ experiment_key = get_experiment_key(experiment_id)
437
+ variation_id_map = @variation_id_map[experiment_key]
438
+ if variation_id_map
439
+ variation = variation_id_map[variation_id]
440
+ return true if variation
441
+
442
+ @logger.log Logger::ERROR, "Variation ID '#{variation_id}' is not in datafile."
443
+ @error_handler.handle_error InvalidVariationError
444
+ end
445
+
446
+ false
447
+ end
448
+
449
+ def get_feature_flag_from_key(feature_flag_key)
450
+ # Retrieves the feature flag with the given key
451
+ #
452
+ # feature_flag_key - String feature key
453
+ #
454
+ # Returns feature flag if found, otherwise nil
455
+ feature_flag = @feature_flag_key_map[feature_flag_key]
456
+ return feature_flag if feature_flag
457
+
458
+ @logger.log Logger::ERROR, "Feature flag key '#{feature_flag_key}' is not in datafile."
459
+ nil
460
+ end
461
+
462
+ def get_feature_variable(feature_flag, variable_key)
463
+ # Retrieves the variable with the given key for the given feature
464
+ #
465
+ # feature_flag - The feature flag for which we are retrieving the variable
466
+ # variable_key - String variable key
467
+ #
468
+ # Returns variable if found, otherwise nil
469
+ feature_flag_key = feature_flag['key']
470
+ variable = @feature_variable_key_map[feature_flag_key][variable_key]
471
+ return variable if variable
472
+
473
+ @logger.log Logger::ERROR, "No feature variable was found for key '#{variable_key}' in feature flag "\
474
+ "'#{feature_flag_key}'."
475
+ nil
476
+ end
477
+
478
+ def get_rollout_from_id(rollout_id)
479
+ # Retrieves the rollout with the given ID
480
+ #
481
+ # rollout_id - String rollout ID
482
+ #
483
+ # Returns the rollout if found, otherwise nil
484
+ rollout = @rollout_id_map[rollout_id]
485
+ return rollout if rollout
486
+
487
+ @logger.log Logger::ERROR, "Rollout with ID '#{rollout_id}' is not in the datafile."
488
+ nil
489
+ end
490
+
491
+ def feature_experiment?(experiment_id)
492
+ # Determines if given experiment is a feature test.
493
+ #
494
+ # experiment_id - String experiment ID
495
+ #
496
+ # Returns true if experiment belongs to any feature,
497
+ # false otherwise.
498
+ @experiment_feature_map.key?(experiment_id)
499
+ end
500
+
501
+ def rollout_experiment?(experiment_id)
502
+ # Determines if given experiment is a rollout test.
503
+ #
504
+ # experiment_id - String experiment ID
505
+ #
506
+ # Returns true if experiment belongs to any rollout,
507
+ # false otherwise.
508
+ @rollout_experiment_id_map.key?(experiment_id)
509
+ end
510
+
511
+ private
512
+
513
+ def generate_feature_variation_map(feature_flags)
514
+ flag_variation_map = {}
515
+ feature_flags.each do |flag|
516
+ variations = []
517
+ get_rules_for_flag(flag).each do |rule|
518
+ rule['variations'].each do |rule_variation|
519
+ variations.push(rule_variation) if variations.select { |variation| variation['id'] == rule_variation['id'] }.empty?
520
+ end
521
+ end
522
+ flag_variation_map[flag['key']] = variations
523
+ end
524
+ flag_variation_map
525
+ end
526
+
527
+ def generate_key_map(array, key, first_value: false)
528
+ # Helper method to generate map from key to hash in array of hashes
529
+ #
530
+ # array - Array consisting of hash
531
+ # key - Key in each hash which will be key in the map
532
+ # first_value - Determines which value to save if there are duplicate keys. By default the last instance of the key
533
+ # will be saved. Set to true to save the first key/value encountered.
534
+ #
535
+ # Returns map mapping key to hash
536
+
537
+ array
538
+ .group_by { |obj| obj[key] }
539
+ .transform_values { |group| first_value ? group.first : group.last }
540
+ end
541
+ end
542
+ end