optimizely-sdk 5.0.0 → 5.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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