optimizely-sdk 4.0.1 → 5.0.0.pre.beta

Sign up to get free protection for your applications and to get access to all the features.
@@ -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