optimizely-sdk 5.0.0 → 5.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +202 -202
  3. data/lib/optimizely/audience.rb +127 -127
  4. data/lib/optimizely/bucketer.rb +156 -156
  5. data/lib/optimizely/condition_tree_evaluator.rb +123 -123
  6. data/lib/optimizely/config/datafile_project_config.rb +558 -558
  7. data/lib/optimizely/config/proxy_config.rb +34 -34
  8. data/lib/optimizely/config_manager/async_scheduler.rb +95 -95
  9. data/lib/optimizely/config_manager/http_project_config_manager.rb +340 -340
  10. data/lib/optimizely/config_manager/project_config_manager.rb +25 -25
  11. data/lib/optimizely/config_manager/static_project_config_manager.rb +55 -55
  12. data/lib/optimizely/decide/optimizely_decide_option.rb +28 -28
  13. data/lib/optimizely/decide/optimizely_decision.rb +60 -60
  14. data/lib/optimizely/decide/optimizely_decision_message.rb +26 -26
  15. data/lib/optimizely/decision_service.rb +563 -563
  16. data/lib/optimizely/error_handler.rb +39 -39
  17. data/lib/optimizely/event/batch_event_processor.rb +235 -235
  18. data/lib/optimizely/event/entity/conversion_event.rb +44 -44
  19. data/lib/optimizely/event/entity/decision.rb +38 -38
  20. data/lib/optimizely/event/entity/event_batch.rb +86 -86
  21. data/lib/optimizely/event/entity/event_context.rb +50 -50
  22. data/lib/optimizely/event/entity/impression_event.rb +48 -48
  23. data/lib/optimizely/event/entity/snapshot.rb +33 -33
  24. data/lib/optimizely/event/entity/snapshot_event.rb +48 -48
  25. data/lib/optimizely/event/entity/user_event.rb +22 -22
  26. data/lib/optimizely/event/entity/visitor.rb +36 -36
  27. data/lib/optimizely/event/entity/visitor_attribute.rb +38 -38
  28. data/lib/optimizely/event/event_factory.rb +156 -156
  29. data/lib/optimizely/event/event_processor.rb +25 -25
  30. data/lib/optimizely/event/forwarding_event_processor.rb +44 -44
  31. data/lib/optimizely/event/user_event_factory.rb +88 -88
  32. data/lib/optimizely/event_builder.rb +221 -221
  33. data/lib/optimizely/event_dispatcher.rb +69 -69
  34. data/lib/optimizely/exceptions.rb +193 -193
  35. data/lib/optimizely/helpers/constants.rb +459 -459
  36. data/lib/optimizely/helpers/date_time_utils.rb +30 -30
  37. data/lib/optimizely/helpers/event_tag_utils.rb +132 -132
  38. data/lib/optimizely/helpers/group.rb +31 -31
  39. data/lib/optimizely/helpers/http_utils.rb +68 -68
  40. data/lib/optimizely/helpers/sdk_settings.rb +61 -61
  41. data/lib/optimizely/helpers/validator.rb +236 -236
  42. data/lib/optimizely/helpers/variable_type.rb +67 -67
  43. data/lib/optimizely/logger.rb +46 -46
  44. data/lib/optimizely/notification_center.rb +174 -174
  45. data/lib/optimizely/notification_center_registry.rb +71 -71
  46. data/lib/optimizely/odp/lru_cache.rb +114 -114
  47. data/lib/optimizely/odp/odp_config.rb +102 -102
  48. data/lib/optimizely/odp/odp_event.rb +75 -75
  49. data/lib/optimizely/odp/odp_event_api_manager.rb +70 -70
  50. data/lib/optimizely/odp/odp_event_manager.rb +286 -286
  51. data/lib/optimizely/odp/odp_manager.rb +159 -159
  52. data/lib/optimizely/odp/odp_segment_api_manager.rb +122 -122
  53. data/lib/optimizely/odp/odp_segment_manager.rb +97 -97
  54. data/lib/optimizely/optimizely_config.rb +273 -273
  55. data/lib/optimizely/optimizely_factory.rb +184 -184
  56. data/lib/optimizely/optimizely_user_context.rb +238 -238
  57. data/lib/optimizely/params.rb +31 -31
  58. data/lib/optimizely/project_config.rb +99 -99
  59. data/lib/optimizely/semantic_version.rb +166 -166
  60. data/lib/optimizely/user_condition_evaluator.rb +391 -391
  61. data/lib/optimizely/user_profile_service.rb +35 -35
  62. data/lib/optimizely/version.rb +21 -21
  63. data/lib/optimizely.rb +1262 -1262
  64. 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