optimizely-sdk 5.0.0 → 5.1.0

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 (65) 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 +589 -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 +183 -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/user_profile_tracker.rb +64 -0
  63. data/lib/optimizely/version.rb +21 -21
  64. data/lib/optimizely.rb +1326 -1262
  65. metadata +8 -5
data/lib/optimizely.rb CHANGED
@@ -1,1262 +1,1326 @@
1
- # frozen_string_literal: true
2
-
3
- #
4
- # Copyright 2016-2023, Optimizely and contributors
5
- #
6
- # Licensed under the Apache License, Version 2.0 (the "License");
7
- # you may not use this file except in compliance with the License.
8
- # You may obtain a copy of the License at
9
- #
10
- # http://www.apache.org/licenses/LICENSE-2.0
11
- #
12
- # Unless required by applicable law or agreed to in writing, software
13
- # distributed under the License is distributed on an "AS IS" BASIS,
14
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
- # See the License for the specific language governing permissions and
16
- # limitations under the License.
17
- #
18
- require_relative 'optimizely/audience'
19
- require_relative 'optimizely/config/datafile_project_config'
20
- require_relative 'optimizely/config_manager/http_project_config_manager'
21
- require_relative 'optimizely/config_manager/static_project_config_manager'
22
- require_relative 'optimizely/decide/optimizely_decide_option'
23
- require_relative 'optimizely/decide/optimizely_decision'
24
- require_relative 'optimizely/decide/optimizely_decision_message'
25
- require_relative 'optimizely/decision_service'
26
- require_relative 'optimizely/error_handler'
27
- require_relative 'optimizely/event_builder'
28
- require_relative 'optimizely/event/batch_event_processor'
29
- require_relative 'optimizely/event/event_factory'
30
- require_relative 'optimizely/event/user_event_factory'
31
- require_relative 'optimizely/event_dispatcher'
32
- require_relative 'optimizely/exceptions'
33
- require_relative 'optimizely/helpers/constants'
34
- require_relative 'optimizely/helpers/group'
35
- require_relative 'optimizely/helpers/validator'
36
- require_relative 'optimizely/helpers/variable_type'
37
- require_relative 'optimizely/logger'
38
- require_relative 'optimizely/notification_center'
39
- require_relative 'optimizely/notification_center_registry'
40
- require_relative 'optimizely/optimizely_config'
41
- require_relative 'optimizely/optimizely_user_context'
42
- require_relative 'optimizely/odp/lru_cache'
43
- require_relative 'optimizely/odp/odp_manager'
44
- require_relative 'optimizely/helpers/sdk_settings'
45
-
46
- module Optimizely
47
- class Project
48
- include Optimizely::Decide
49
-
50
- attr_reader :notification_center
51
- # @api no-doc
52
- attr_reader :config_manager, :decision_service, :error_handler, :event_dispatcher,
53
- :event_processor, :logger, :odp_manager, :stopped
54
-
55
- # Constructor for Projects.
56
- #
57
- # @param datafile - JSON string representing the project.
58
- # @param event_dispatcher - Provides a dispatch_event method which if given a URL and params sends a request to it.
59
- # @param logger - Optional component which provides a log method to log messages. By default nothing would be logged.
60
- # @param error_handler - Optional component which provides a handle_error method to handle exceptions.
61
- # By default all exceptions will be suppressed.
62
- # @param user_profile_service - Optional component which provides methods to store and retreive user profiles.
63
- # @param skip_json_validation - Optional boolean param to skip JSON schema validation of the provided datafile.
64
- # @params sdk_key - Optional string uniquely identifying the datafile corresponding to project and environment combination.
65
- # Must provide at least one of datafile or sdk_key.
66
- # @param config_manager - Optional Responds to 'config' method.
67
- # @param notification_center - Optional Instance of NotificationCenter.
68
- # @param event_processor - Optional Responds to process.
69
- # @param default_decide_options: Optional default decision options.
70
- # @param event_processor_options: Optional hash of options to be passed to the default batch event processor.
71
- # @param settings: Optional instance of OptimizelySdkSettings for sdk configuration.
72
-
73
- def initialize(
74
- datafile: nil,
75
- event_dispatcher: nil,
76
- logger: nil,
77
- error_handler: nil,
78
- skip_json_validation: false,
79
- user_profile_service: nil,
80
- sdk_key: nil,
81
- config_manager: nil,
82
- notification_center: nil,
83
- event_processor: nil,
84
- default_decide_options: [],
85
- event_processor_options: {},
86
- settings: nil
87
- )
88
- @logger = logger || NoOpLogger.new
89
- @error_handler = error_handler || NoOpErrorHandler.new
90
- @event_dispatcher = event_dispatcher || EventDispatcher.new(logger: @logger, error_handler: @error_handler)
91
- @user_profile_service = user_profile_service
92
- @default_decide_options = []
93
- @sdk_settings = settings
94
-
95
- if default_decide_options.is_a? Array
96
- @default_decide_options = default_decide_options.clone
97
- else
98
- @logger.log(Logger::DEBUG, 'Provided default decide options is not an array.')
99
- @default_decide_options = []
100
- end
101
-
102
- unless event_processor_options.is_a? Hash
103
- @logger.log(Logger::DEBUG, 'Provided event processor options is not a hash.')
104
- event_processor_options = {}
105
- end
106
-
107
- begin
108
- validate_instantiation_options
109
- rescue InvalidInputError => e
110
- @logger = SimpleLogger.new
111
- @logger.log(Logger::ERROR, e.message)
112
- end
113
-
114
- @notification_center = notification_center.is_a?(Optimizely::NotificationCenter) ? notification_center : NotificationCenter.new(@logger, @error_handler)
115
-
116
- @config_manager = if config_manager.respond_to?(:config) && config_manager.respond_to?(:sdk_key)
117
- config_manager
118
- elsif sdk_key
119
- HTTPProjectConfigManager.new(
120
- sdk_key: sdk_key,
121
- datafile: datafile,
122
- logger: @logger,
123
- error_handler: @error_handler,
124
- skip_json_validation: skip_json_validation,
125
- notification_center: @notification_center
126
- )
127
- else
128
- StaticProjectConfigManager.new(datafile, @logger, @error_handler, skip_json_validation)
129
- end
130
-
131
- setup_odp!(@config_manager.sdk_key)
132
-
133
- @decision_service = DecisionService.new(@logger, @user_profile_service)
134
-
135
- @event_processor = if event_processor.respond_to?(:process)
136
- event_processor
137
- else
138
- BatchEventProcessor.new(
139
- event_dispatcher: @event_dispatcher,
140
- logger: @logger,
141
- notification_center: @notification_center,
142
- batch_size: event_processor_options[:batch_size] || BatchEventProcessor::DEFAULT_BATCH_SIZE,
143
- flush_interval: event_processor_options[:flush_interval] || BatchEventProcessor::DEFAULT_BATCH_INTERVAL
144
- )
145
- end
146
- end
147
-
148
- # Create a context of the user for which decision APIs will be called.
149
- #
150
- # A user context will be created successfully even when the SDK is not fully configured yet.
151
- #
152
- # @param user_id - The user ID to be used for bucketing.
153
- # @param attributes - A Hash representing user attribute names and values.
154
- #
155
- # @return [OptimizelyUserContext] An OptimizelyUserContext associated with this OptimizelyClient.
156
- # @return [nil] If user attributes are not in valid format.
157
-
158
- def create_user_context(user_id, attributes = nil)
159
- # We do not check for is_valid here as a user context can be created successfully
160
- # even when the SDK is not fully configured.
161
-
162
- # validate user_id
163
- return nil unless Optimizely::Helpers::Validator.inputs_valid?(
164
- {
165
- user_id: user_id
166
- }, @logger, Logger::ERROR
167
- )
168
-
169
- # validate attributes
170
- return nil unless user_inputs_valid?(attributes)
171
-
172
- OptimizelyUserContext.new(self, user_id, attributes)
173
- end
174
-
175
- def decide(user_context, key, decide_options = [])
176
- # raising on user context as it is internal and not provided directly by the user.
177
- raise if user_context.class != OptimizelyUserContext
178
-
179
- reasons = []
180
-
181
- # check if SDK is ready
182
- unless is_valid
183
- @logger.log(Logger::ERROR, InvalidProjectConfigError.new('decide').message)
184
- reasons.push(OptimizelyDecisionMessage::SDK_NOT_READY)
185
- return OptimizelyDecision.new(flag_key: key, user_context: user_context, reasons: reasons)
186
- end
187
-
188
- # validate that key is a string
189
- unless key.is_a?(String)
190
- @logger.log(Logger::ERROR, 'Provided key is invalid')
191
- reasons.push(format(OptimizelyDecisionMessage::FLAG_KEY_INVALID, key))
192
- return OptimizelyDecision.new(flag_key: key, user_context: user_context, reasons: reasons)
193
- end
194
-
195
- # validate that key maps to a feature flag
196
- config = project_config
197
- feature_flag = config.get_feature_flag_from_key(key)
198
- unless feature_flag
199
- @logger.log(Logger::ERROR, "No feature flag was found for key '#{key}'.")
200
- reasons.push(format(OptimizelyDecisionMessage::FLAG_KEY_INVALID, key))
201
- return OptimizelyDecision.new(flag_key: key, user_context: user_context, reasons: reasons)
202
- end
203
-
204
- # merge decide_options and default_decide_options
205
- if decide_options.is_a? Array
206
- decide_options += @default_decide_options
207
- else
208
- @logger.log(Logger::DEBUG, 'Provided decide options is not an array. Using default decide options.')
209
- decide_options = @default_decide_options
210
- end
211
-
212
- # Create Optimizely Decision Result.
213
- user_id = user_context.user_id
214
- attributes = user_context.user_attributes
215
- variation_key = nil
216
- feature_enabled = false
217
- rule_key = nil
218
- flag_key = key
219
- all_variables = {}
220
- decision_event_dispatched = false
221
- experiment = nil
222
- decision_source = Optimizely::DecisionService::DECISION_SOURCES['ROLLOUT']
223
- context = Optimizely::OptimizelyUserContext::OptimizelyDecisionContext.new(key, nil)
224
- variation, reasons_received = @decision_service.validated_forced_decision(config, context, user_context)
225
- reasons.push(*reasons_received)
226
-
227
- if variation
228
- decision = Optimizely::DecisionService::Decision.new(nil, variation, Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST'])
229
- else
230
- decision, reasons_received = @decision_service.get_variation_for_feature(config, feature_flag, user_context, decide_options)
231
- reasons.push(*reasons_received)
232
- end
233
-
234
- # Send impression event if Decision came from a feature test and decide options doesn't include disableDecisionEvent
235
- if decision.is_a?(Optimizely::DecisionService::Decision)
236
- experiment = decision.experiment
237
- rule_key = experiment ? experiment['key'] : nil
238
- variation = decision['variation']
239
- variation_key = variation ? variation['key'] : nil
240
- feature_enabled = variation ? variation['featureEnabled'] : false
241
- decision_source = decision.source
242
- end
243
-
244
- if !decide_options.include?(OptimizelyDecideOption::DISABLE_DECISION_EVENT) && (decision_source == Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST'] || config.send_flag_decisions)
245
- send_impression(config, experiment, variation_key || '', flag_key, rule_key || '', feature_enabled, decision_source, user_id, attributes)
246
- decision_event_dispatched = true
247
- end
248
-
249
- # Generate all variables map if decide options doesn't include excludeVariables
250
- unless decide_options.include? OptimizelyDecideOption::EXCLUDE_VARIABLES
251
- feature_flag['variables'].each do |variable|
252
- variable_value = get_feature_variable_for_variation(key, feature_enabled, variation, variable, user_id)
253
- all_variables[variable['key']] = Helpers::VariableType.cast_value_to_type(variable_value, variable['type'], @logger)
254
- end
255
- end
256
-
257
- should_include_reasons = decide_options.include? OptimizelyDecideOption::INCLUDE_REASONS
258
-
259
- # Send notification
260
- @notification_center.send_notifications(
261
- NotificationCenter::NOTIFICATION_TYPES[:DECISION],
262
- Helpers::Constants::DECISION_NOTIFICATION_TYPES['FLAG'],
263
- user_id, (attributes || {}),
264
- flag_key: flag_key,
265
- enabled: feature_enabled,
266
- variables: all_variables,
267
- variation_key: variation_key,
268
- rule_key: rule_key,
269
- reasons: should_include_reasons ? reasons : [],
270
- decision_event_dispatched: decision_event_dispatched
271
- )
272
-
273
- OptimizelyDecision.new(
274
- variation_key: variation_key,
275
- enabled: feature_enabled,
276
- variables: all_variables,
277
- rule_key: rule_key,
278
- flag_key: flag_key,
279
- user_context: user_context,
280
- reasons: should_include_reasons ? reasons : []
281
- )
282
- end
283
-
284
- def decide_all(user_context, decide_options = [])
285
- # raising on user context as it is internal and not provided directly by the user.
286
- raise if user_context.class != OptimizelyUserContext
287
-
288
- # check if SDK is ready
289
- unless is_valid
290
- @logger.log(Logger::ERROR, InvalidProjectConfigError.new('decide_all').message)
291
- return {}
292
- end
293
-
294
- keys = []
295
- project_config.feature_flags.each do |feature_flag|
296
- keys.push(feature_flag['key'])
297
- end
298
- decide_for_keys(user_context, keys, decide_options)
299
- end
300
-
301
- def decide_for_keys(user_context, keys, decide_options = [])
302
- # raising on user context as it is internal and not provided directly by the user.
303
- raise if user_context.class != OptimizelyUserContext
304
-
305
- # check if SDK is ready
306
- unless is_valid
307
- @logger.log(Logger::ERROR, InvalidProjectConfigError.new('decide_for_keys').message)
308
- return {}
309
- end
310
-
311
- enabled_flags_only = (!decide_options.nil? && (decide_options.include? OptimizelyDecideOption::ENABLED_FLAGS_ONLY)) || (@default_decide_options.include? OptimizelyDecideOption::ENABLED_FLAGS_ONLY)
312
-
313
- decisions = {}
314
- keys.each do |key|
315
- decision = decide(user_context, key, decide_options)
316
- decisions[key] = decision unless enabled_flags_only && !decision.enabled
317
- end
318
- decisions
319
- end
320
-
321
- # Gets variation using variation key or id and flag key.
322
- #
323
- # @param flag_key - flag key from which the variation is required.
324
- # @param target_value - variation value either id or key that will be matched.
325
- # @param attribute - string representing variation attribute.
326
- #
327
- # @return [variation]
328
- # @return [nil] if no variation found in flag_variation_map.
329
-
330
- def get_flag_variation(flag_key, target_value, attribute)
331
- project_config.get_variation_from_flag(flag_key, target_value, attribute)
332
- end
333
-
334
- # Buckets visitor and sends impression event to Optimizely.
335
- #
336
- # @param experiment_key - Experiment which needs to be activated.
337
- # @param user_id - String ID for user.
338
- # @param attributes - Hash representing user attributes and values to be recorded.
339
- #
340
- # @return [Variation Key] representing the variation the user will be bucketed in.
341
- # @return [nil] if experiment is not Running, if user is not in experiment, or if datafile is invalid.
342
-
343
- def activate(experiment_key, user_id, attributes = nil)
344
- unless is_valid
345
- @logger.log(Logger::ERROR, InvalidProjectConfigError.new('activate').message)
346
- return nil
347
- end
348
-
349
- return nil unless Optimizely::Helpers::Validator.inputs_valid?(
350
- {
351
- experiment_key: experiment_key,
352
- user_id: user_id
353
- }, @logger, Logger::ERROR
354
- )
355
-
356
- config = project_config
357
-
358
- variation_key = get_variation_with_config(experiment_key, user_id, attributes, config)
359
-
360
- if variation_key.nil?
361
- @logger.log(Logger::INFO, "Not activating user '#{user_id}'.")
362
- return nil
363
- end
364
-
365
- # Create and dispatch impression event
366
- experiment = config.get_experiment_from_key(experiment_key)
367
- send_impression(
368
- config, experiment, variation_key, '', experiment_key, true,
369
- Optimizely::DecisionService::DECISION_SOURCES['EXPERIMENT'], user_id, attributes
370
- )
371
-
372
- variation_key
373
- end
374
-
375
- # Gets variation where visitor will be bucketed.
376
- #
377
- # @param experiment_key - Experiment for which visitor variation needs to be determined.
378
- # @param user_id - String ID for user.
379
- # @param attributes - Hash representing user attributes.
380
- #
381
- # @return [variation key] where visitor will be bucketed.
382
- # @return [nil] if experiment is not Running, if user is not in experiment, or if datafile is invalid.
383
-
384
- def get_variation(experiment_key, user_id, attributes = nil)
385
- unless is_valid
386
- @logger.log(Logger::ERROR, InvalidProjectConfigError.new('get_variation').message)
387
- return nil
388
- end
389
-
390
- return nil unless Optimizely::Helpers::Validator.inputs_valid?(
391
- {
392
- experiment_key: experiment_key,
393
- user_id: user_id
394
- }, @logger, Logger::ERROR
395
- )
396
-
397
- config = project_config
398
-
399
- get_variation_with_config(experiment_key, user_id, attributes, config)
400
- end
401
-
402
- # Force a user into a variation for a given experiment.
403
- #
404
- # @param experiment_key - String - key identifying the experiment.
405
- # @param user_id - String - The user ID to be used for bucketing.
406
- # @param variation_key - The variation key specifies the variation which the user will
407
- # be forced into. If nil, then clear the existing experiment-to-variation mapping.
408
- #
409
- # @return [Boolean] indicates if the set completed successfully.
410
-
411
- def set_forced_variation(experiment_key, user_id, variation_key)
412
- unless is_valid
413
- @logger.log(Logger::ERROR, InvalidProjectConfigError.new('set_forced_variation').message)
414
- return nil
415
- end
416
-
417
- input_values = {experiment_key: experiment_key, user_id: user_id}
418
- input_values[:variation_key] = variation_key unless variation_key.nil?
419
- return false unless Optimizely::Helpers::Validator.inputs_valid?(input_values, @logger, Logger::ERROR)
420
-
421
- config = project_config
422
-
423
- @decision_service.set_forced_variation(config, experiment_key, user_id, variation_key)
424
- end
425
-
426
- # Gets the forced variation for a given user and experiment.
427
- #
428
- # @param experiment_key - String - Key identifying the experiment.
429
- # @param user_id - String - The user ID to be used for bucketing.
430
- #
431
- # @return [String] The forced variation key.
432
-
433
- def get_forced_variation(experiment_key, user_id)
434
- unless is_valid
435
- @logger.log(Logger::ERROR, InvalidProjectConfigError.new('get_forced_variation').message)
436
- return nil
437
- end
438
-
439
- return nil unless Optimizely::Helpers::Validator.inputs_valid?(
440
- {
441
- experiment_key: experiment_key,
442
- user_id: user_id
443
- }, @logger, Logger::ERROR
444
- )
445
-
446
- config = project_config
447
-
448
- forced_variation_key = nil
449
- forced_variation, = @decision_service.get_forced_variation(config, experiment_key, user_id)
450
- forced_variation_key = forced_variation['key'] if forced_variation
451
-
452
- forced_variation_key
453
- end
454
-
455
- # Send conversion event to Optimizely.
456
- #
457
- # @param event_key - Event key representing the event which needs to be recorded.
458
- # @param user_id - String ID for user.
459
- # @param attributes - Hash representing visitor attributes and values which need to be recorded.
460
- # @param event_tags - Hash representing metadata associated with the event.
461
-
462
- def track(event_key, user_id, attributes = nil, event_tags = nil)
463
- unless is_valid
464
- @logger.log(Logger::ERROR, InvalidProjectConfigError.new('track').message)
465
- return nil
466
- end
467
-
468
- return nil unless Optimizely::Helpers::Validator.inputs_valid?(
469
- {
470
- event_key: event_key,
471
- user_id: user_id
472
- }, @logger, Logger::ERROR
473
- )
474
-
475
- return nil unless user_inputs_valid?(attributes, event_tags)
476
-
477
- config = project_config
478
-
479
- event = config.get_event_from_key(event_key)
480
- unless event
481
- @logger.log(Logger::INFO, "Not tracking user '#{user_id}' for event '#{event_key}'.")
482
- return nil
483
- end
484
-
485
- user_event = UserEventFactory.create_conversion_event(config, event, user_id, attributes, event_tags)
486
- @event_processor.process(user_event)
487
- @logger.log(Logger::INFO, "Tracking event '#{event_key}' for user '#{user_id}'.")
488
-
489
- if @notification_center.notification_count(NotificationCenter::NOTIFICATION_TYPES[:TRACK]).positive?
490
- log_event = EventFactory.create_log_event(user_event, @logger)
491
- @notification_center.send_notifications(
492
- NotificationCenter::NOTIFICATION_TYPES[:TRACK],
493
- event_key, user_id, attributes, event_tags, log_event
494
- )
495
- end
496
- nil
497
- end
498
-
499
- # Determine whether a feature is enabled.
500
- # Sends an impression event if the user is bucketed into an experiment using the feature.
501
- #
502
- # @param feature_flag_key - String unique key of the feature.
503
- # @param user_id - String ID of the user.
504
- # @param attributes - Hash representing visitor attributes and values which need to be recorded.
505
- #
506
- # @return [True] if the feature is enabled.
507
- # @return [False] if the feature is disabled.
508
- # @return [False] if the feature is not found.
509
-
510
- def is_feature_enabled(feature_flag_key, user_id, attributes = nil)
511
- unless is_valid
512
- @logger.log(Logger::ERROR, InvalidProjectConfigError.new('is_feature_enabled').message)
513
- return false
514
- end
515
-
516
- return false unless Optimizely::Helpers::Validator.inputs_valid?(
517
- {
518
- feature_flag_key: feature_flag_key,
519
- user_id: user_id
520
- }, @logger, Logger::ERROR
521
- )
522
-
523
- return false unless user_inputs_valid?(attributes)
524
-
525
- config = project_config
526
-
527
- feature_flag = config.get_feature_flag_from_key(feature_flag_key)
528
- unless feature_flag
529
- @logger.log(Logger::ERROR, "No feature flag was found for key '#{feature_flag_key}'.")
530
- return false
531
- end
532
-
533
- user_context = OptimizelyUserContext.new(self, user_id, attributes, identify: false)
534
- decision, = @decision_service.get_variation_for_feature(config, feature_flag, user_context)
535
-
536
- feature_enabled = false
537
- source_string = Optimizely::DecisionService::DECISION_SOURCES['ROLLOUT']
538
- if decision.is_a?(Optimizely::DecisionService::Decision)
539
- variation = decision['variation']
540
- feature_enabled = variation['featureEnabled']
541
- if decision.source == Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST']
542
- source_string = Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST']
543
- source_info = {
544
- experiment_key: decision.experiment['key'],
545
- variation_key: variation['key']
546
- }
547
- # Send event if Decision came from a feature test.
548
- send_impression(
549
- config, decision.experiment, variation['key'], feature_flag_key, decision.experiment['key'], feature_enabled, source_string, user_id, attributes
550
- )
551
- elsif decision.source == Optimizely::DecisionService::DECISION_SOURCES['ROLLOUT'] && config.send_flag_decisions
552
- send_impression(
553
- config, decision.experiment, variation['key'], feature_flag_key, decision.experiment['key'], feature_enabled, source_string, user_id, attributes
554
- )
555
- end
556
- end
557
-
558
- if decision.nil? && config.send_flag_decisions
559
- send_impression(
560
- config, nil, '', feature_flag_key, '', feature_enabled, source_string, user_id, attributes
561
- )
562
- end
563
-
564
- @notification_center.send_notifications(
565
- NotificationCenter::NOTIFICATION_TYPES[:DECISION],
566
- Helpers::Constants::DECISION_NOTIFICATION_TYPES['FEATURE'],
567
- user_id, (attributes || {}),
568
- feature_key: feature_flag_key,
569
- feature_enabled: feature_enabled,
570
- source: source_string,
571
- source_info: source_info || {}
572
- )
573
-
574
- if feature_enabled == true
575
- @logger.log(Logger::INFO,
576
- "Feature '#{feature_flag_key}' is enabled for user '#{user_id}'.")
577
- return true
578
- end
579
-
580
- @logger.log(Logger::INFO,
581
- "Feature '#{feature_flag_key}' is not enabled for user '#{user_id}'.")
582
- false
583
- end
584
-
585
- # Gets keys of all feature flags which are enabled for the user.
586
- #
587
- # @param user_id - ID for user.
588
- # @param attributes - Dict representing user attributes.
589
- # @return [feature flag keys] A List of feature flag keys that are enabled for the user.
590
-
591
- def get_enabled_features(user_id, attributes = nil)
592
- enabled_features = []
593
- unless is_valid
594
- @logger.log(Logger::ERROR, InvalidProjectConfigError.new('get_enabled_features').message)
595
- return enabled_features
596
- end
597
-
598
- return enabled_features unless Optimizely::Helpers::Validator.inputs_valid?(
599
- {
600
- user_id: user_id
601
- }, @logger, Logger::ERROR
602
- )
603
-
604
- return enabled_features unless user_inputs_valid?(attributes)
605
-
606
- config = project_config
607
-
608
- config.feature_flags.each do |feature|
609
- enabled_features.push(feature['key']) if is_feature_enabled(
610
- feature['key'],
611
- user_id,
612
- attributes
613
- ) == true
614
- end
615
- enabled_features
616
- end
617
-
618
- # Get the value of the specified variable in the feature flag.
619
- #
620
- # @param feature_flag_key - String key of feature flag the variable belongs to
621
- # @param variable_key - String key of variable for which we are getting the value
622
- # @param user_id - String user ID
623
- # @param attributes - Hash representing visitor attributes and values which need to be recorded.
624
- #
625
- # @return [*] the type-casted variable value.
626
- # @return [nil] if the feature flag or variable are not found.
627
-
628
- def get_feature_variable(feature_flag_key, variable_key, user_id, attributes = nil)
629
- unless is_valid
630
- @logger.log(Logger::ERROR, InvalidProjectConfigError.new('get_feature_variable').message)
631
- return nil
632
- end
633
- get_feature_variable_for_type(
634
- feature_flag_key,
635
- variable_key,
636
- nil,
637
- user_id,
638
- attributes
639
- )
640
- end
641
-
642
- # Get the String value of the specified variable in the feature flag.
643
- #
644
- # @param feature_flag_key - String key of feature flag the variable belongs to
645
- # @param variable_key - String key of variable for which we are getting the string value
646
- # @param user_id - String user ID
647
- # @param attributes - Hash representing visitor attributes and values which need to be recorded.
648
- #
649
- # @return [String] the string variable value.
650
- # @return [nil] if the feature flag or variable are not found.
651
-
652
- def get_feature_variable_string(feature_flag_key, variable_key, user_id, attributes = nil)
653
- unless is_valid
654
- @logger.log(Logger::ERROR, InvalidProjectConfigError.new('get_feature_variable_string').message)
655
- return nil
656
- end
657
- get_feature_variable_for_type(
658
- feature_flag_key,
659
- variable_key,
660
- Optimizely::Helpers::Constants::VARIABLE_TYPES['STRING'],
661
- user_id,
662
- attributes
663
- )
664
- end
665
-
666
- # Get the Json value of the specified variable in the feature flag in a Dict.
667
- #
668
- # @param feature_flag_key - String key of feature flag the variable belongs to
669
- # @param variable_key - String key of variable for which we are getting the string value
670
- # @param user_id - String user ID
671
- # @param attributes - Hash representing visitor attributes and values which need to be recorded.
672
- #
673
- # @return [Dict] the Dict containing variable value.
674
- # @return [nil] if the feature flag or variable are not found.
675
-
676
- def get_feature_variable_json(feature_flag_key, variable_key, user_id, attributes = nil)
677
- unless is_valid
678
- @logger.log(Logger::ERROR, InvalidProjectConfigError.new('get_feature_variable_json').message)
679
- return nil
680
- end
681
- get_feature_variable_for_type(
682
- feature_flag_key,
683
- variable_key,
684
- Optimizely::Helpers::Constants::VARIABLE_TYPES['JSON'],
685
- user_id,
686
- attributes
687
- )
688
- end
689
-
690
- # Get the Boolean value of the specified variable in the feature flag.
691
- #
692
- # @param feature_flag_key - String key of feature flag the variable belongs to
693
- # @param variable_key - String key of variable for which we are getting the string value
694
- # @param user_id - String user ID
695
- # @param attributes - Hash representing visitor attributes and values which need to be recorded.
696
- #
697
- # @return [Boolean] the boolean variable value.
698
- # @return [nil] if the feature flag or variable are not found.
699
-
700
- def get_feature_variable_boolean(feature_flag_key, variable_key, user_id, attributes = nil)
701
- unless is_valid
702
- @logger.log(Logger::ERROR, InvalidProjectConfigError.new('get_feature_variable_boolean').message)
703
- return nil
704
- end
705
-
706
- get_feature_variable_for_type(
707
- feature_flag_key,
708
- variable_key,
709
- Optimizely::Helpers::Constants::VARIABLE_TYPES['BOOLEAN'],
710
- user_id,
711
- attributes
712
- )
713
- end
714
-
715
- # Get the Double value of the specified variable in the feature flag.
716
- #
717
- # @param feature_flag_key - String key of feature flag the variable belongs to
718
- # @param variable_key - String key of variable for which we are getting the string value
719
- # @param user_id - String user ID
720
- # @param attributes - Hash representing visitor attributes and values which need to be recorded.
721
- #
722
- # @return [Boolean] the double variable value.
723
- # @return [nil] if the feature flag or variable are not found.
724
-
725
- def get_feature_variable_double(feature_flag_key, variable_key, user_id, attributes = nil)
726
- unless is_valid
727
- @logger.log(Logger::ERROR, InvalidProjectConfigError.new('get_feature_variable_double').message)
728
- return nil
729
- end
730
-
731
- get_feature_variable_for_type(
732
- feature_flag_key,
733
- variable_key,
734
- Optimizely::Helpers::Constants::VARIABLE_TYPES['DOUBLE'],
735
- user_id,
736
- attributes
737
- )
738
- end
739
-
740
- # Get values of all the variables in the feature flag and returns them in a Dict
741
- #
742
- # @param feature_flag_key - String key of feature flag
743
- # @param user_id - String user ID
744
- # @param attributes - Hash representing visitor attributes and values which need to be recorded.
745
- #
746
- # @return [Dict] the Dict containing all the varible values
747
- # @return [nil] if the feature flag is not found.
748
-
749
- def get_all_feature_variables(feature_flag_key, user_id, attributes = nil)
750
- unless is_valid
751
- @logger.log(Logger::ERROR, InvalidProjectConfigError.new('get_all_feature_variables').message)
752
- return nil
753
- end
754
-
755
- return nil unless Optimizely::Helpers::Validator.inputs_valid?(
756
- {
757
- feature_flag_key: feature_flag_key,
758
- user_id: user_id
759
- },
760
- @logger, Logger::ERROR
761
- )
762
-
763
- return nil unless user_inputs_valid?(attributes)
764
-
765
- config = project_config
766
-
767
- feature_flag = config.get_feature_flag_from_key(feature_flag_key)
768
- unless feature_flag
769
- @logger.log(Logger::INFO, "No feature flag was found for key '#{feature_flag_key}'.")
770
- return nil
771
- end
772
-
773
- user_context = OptimizelyUserContext.new(self, user_id, attributes, identify: false)
774
- decision, = @decision_service.get_variation_for_feature(config, feature_flag, user_context)
775
- variation = decision ? decision['variation'] : nil
776
- feature_enabled = variation ? variation['featureEnabled'] : false
777
- all_variables = {}
778
-
779
- feature_flag['variables'].each do |variable|
780
- variable_value = get_feature_variable_for_variation(feature_flag_key, feature_enabled, variation, variable, user_id)
781
- all_variables[variable['key']] = Helpers::VariableType.cast_value_to_type(variable_value, variable['type'], @logger)
782
- end
783
-
784
- source_string = Optimizely::DecisionService::DECISION_SOURCES['ROLLOUT']
785
- if decision && decision['source'] == Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST']
786
- source_info = {
787
- experiment_key: decision.experiment['key'],
788
- variation_key: variation['key']
789
- }
790
- source_string = Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST']
791
- end
792
-
793
- @notification_center.send_notifications(
794
- NotificationCenter::NOTIFICATION_TYPES[:DECISION],
795
- Helpers::Constants::DECISION_NOTIFICATION_TYPES['ALL_FEATURE_VARIABLES'], user_id, (attributes || {}),
796
- feature_key: feature_flag_key,
797
- feature_enabled: feature_enabled,
798
- source: source_string,
799
- variable_values: all_variables,
800
- source_info: source_info || {}
801
- )
802
-
803
- all_variables
804
- end
805
-
806
- # Get the Integer value of the specified variable in the feature flag.
807
- #
808
- # @param feature_flag_key - String key of feature flag the variable belongs to
809
- # @param variable_key - String key of variable for which we are getting the string value
810
- # @param user_id - String user ID
811
- # @param attributes - Hash representing visitor attributes and values which need to be recorded.
812
- #
813
- # @return [Integer] variable value.
814
- # @return [nil] if the feature flag or variable are not found.
815
-
816
- def get_feature_variable_integer(feature_flag_key, variable_key, user_id, attributes = nil)
817
- unless is_valid
818
- @logger.log(Logger::ERROR, InvalidProjectConfigError.new('get_feature_variable_integer').message)
819
- return nil
820
- end
821
-
822
- get_feature_variable_for_type(
823
- feature_flag_key,
824
- variable_key,
825
- Optimizely::Helpers::Constants::VARIABLE_TYPES['INTEGER'],
826
- user_id,
827
- attributes
828
- )
829
- end
830
-
831
- def is_valid
832
- config = project_config
833
- config.is_a?(Optimizely::ProjectConfig)
834
- end
835
-
836
- def close
837
- return if @stopped
838
-
839
- @stopped = true
840
- @config_manager.stop! if @config_manager.respond_to?(:stop!)
841
- @event_processor.stop! if @event_processor.respond_to?(:stop!)
842
- @odp_manager.stop!
843
- end
844
-
845
- def get_optimizely_config
846
- # Get OptimizelyConfig object containing experiments and features data
847
- # Returns Object
848
- #
849
- # OptimizelyConfig Object Schema
850
- # {
851
- # 'experimentsMap' => {
852
- # 'my-fist-experiment' => {
853
- # 'id' => '111111',
854
- # 'key' => 'my-fist-experiment'
855
- # 'variationsMap' => {
856
- # 'variation_1' => {
857
- # 'id' => '121212',
858
- # 'key' => 'variation_1',
859
- # 'variablesMap' => {
860
- # 'age' => {
861
- # 'id' => '222222',
862
- # 'key' => 'age',
863
- # 'type' => 'integer',
864
- # 'value' => '0',
865
- # }
866
- # }
867
- # }
868
- # }
869
- # }
870
- # },
871
- # 'featuresMap' => {
872
- # 'awesome-feature' => {
873
- # 'id' => '333333',
874
- # 'key' => 'awesome-feature',
875
- # 'experimentsMap' => Object,
876
- # 'variablesMap' => Object,
877
- # }
878
- # },
879
- # 'revision' => '13',
880
- # }
881
- #
882
- unless is_valid
883
- @logger.log(Logger::ERROR, InvalidProjectConfigError.new('get_optimizely_config').message)
884
- return nil
885
- end
886
-
887
- # config_manager might not contain optimizely_config if its supplied by the consumer
888
- # Generating a new OptimizelyConfig object in this case as a fallback
889
- if @config_manager.respond_to?(:optimizely_config)
890
- @config_manager.optimizely_config
891
- else
892
- OptimizelyConfig.new(project_config, @logger).config
893
- end
894
- end
895
-
896
- # Send an event to the ODP server.
897
- #
898
- # @param action - the event action name. Cannot be nil or empty string.
899
- # @param identifiers - a hash for identifiers. The caller must provide at least one key-value pair.
900
- # @param type - the event type (default = "fullstack").
901
- # @param data - a hash for associated data. The default event data will be added to this data before sending to the ODP server.
902
-
903
- def send_odp_event(action:, identifiers:, type: Helpers::Constants::ODP_MANAGER_CONFIG[:EVENT_TYPE], data: {})
904
- unless identifiers.is_a?(Hash) && !identifiers.empty?
905
- @logger.log(Logger::ERROR, 'ODP events must have at least one key-value pair in identifiers.')
906
- return
907
- end
908
-
909
- unless is_valid
910
- @logger.log(Logger::ERROR, InvalidProjectConfigError.new('send_odp_event').message)
911
- return
912
- end
913
-
914
- if action.nil? || action.empty?
915
- @logger.log(Logger::ERROR, Helpers::Constants::ODP_LOGS[:ODP_INVALID_ACTION])
916
- return
917
- end
918
-
919
- type = Helpers::Constants::ODP_MANAGER_CONFIG[:EVENT_TYPE] if type.nil? || type.empty?
920
-
921
- @odp_manager.send_event(type: type, action: action, identifiers: identifiers, data: data)
922
- end
923
-
924
- def identify_user(user_id:)
925
- unless is_valid
926
- @logger.log(Logger::ERROR, InvalidProjectConfigError.new('identify_user').message)
927
- return
928
- end
929
-
930
- @odp_manager.identify_user(user_id: user_id)
931
- end
932
-
933
- def fetch_qualified_segments(user_id:, options: [])
934
- unless is_valid
935
- @logger.log(Logger::ERROR, InvalidProjectConfigError.new('fetch_qualified_segments').message)
936
- return
937
- end
938
-
939
- @odp_manager.fetch_qualified_segments(user_id: user_id, options: options)
940
- end
941
-
942
- private
943
-
944
- def get_variation_with_config(experiment_key, user_id, attributes, config)
945
- # Gets variation where visitor will be bucketed.
946
- #
947
- # experiment_key - Experiment for which visitor variation needs to be determined.
948
- # user_id - String ID for user.
949
- # attributes - Hash representing user attributes.
950
- # config - Instance of DatfileProjectConfig
951
- #
952
- # Returns [variation key] where visitor will be bucketed.
953
- # Returns [nil] if experiment is not Running, if user is not in experiment, or if datafile is invalid.
954
- experiment = config.get_experiment_from_key(experiment_key)
955
- return nil if experiment.nil?
956
-
957
- experiment_id = experiment['id']
958
-
959
- return nil unless user_inputs_valid?(attributes)
960
-
961
- user_context = OptimizelyUserContext.new(self, user_id, attributes, identify: false)
962
- variation_id, = @decision_service.get_variation(config, experiment_id, user_context)
963
- variation = config.get_variation_from_id(experiment_key, variation_id) unless variation_id.nil?
964
- variation_key = variation['key'] if variation
965
- decision_notification_type = if config.feature_experiment?(experiment_id)
966
- Helpers::Constants::DECISION_NOTIFICATION_TYPES['FEATURE_TEST']
967
- else
968
- Helpers::Constants::DECISION_NOTIFICATION_TYPES['AB_TEST']
969
- end
970
- @notification_center.send_notifications(
971
- NotificationCenter::NOTIFICATION_TYPES[:DECISION],
972
- decision_notification_type, user_id, (attributes || {}),
973
- experiment_key: experiment_key,
974
- variation_key: variation_key
975
- )
976
-
977
- variation_key
978
- end
979
-
980
- def get_feature_variable_for_type(feature_flag_key, variable_key, variable_type, user_id, attributes = nil)
981
- # Get the variable value for the given feature variable and cast it to the specified type
982
- # The default value is returned if the feature flag is not enabled for the user.
983
- #
984
- # feature_flag_key - String key of feature flag the variable belongs to
985
- # variable_key - String key of variable for which we are getting the string value
986
- # variable_type - String requested type for feature variable
987
- # user_id - String user ID
988
- # attributes - Hash representing visitor attributes and values which need to be recorded.
989
- #
990
- # Returns the type-casted variable value.
991
- # Returns nil if the feature flag or variable or user ID is empty
992
- # in case of variable type mismatch
993
-
994
- return nil unless Optimizely::Helpers::Validator.inputs_valid?(
995
- {
996
- feature_flag_key: feature_flag_key,
997
- variable_key: variable_key,
998
- user_id: user_id,
999
- variable_type: variable_type
1000
- },
1001
- @logger, Logger::ERROR
1002
- )
1003
-
1004
- return nil unless user_inputs_valid?(attributes)
1005
-
1006
- config = project_config
1007
-
1008
- feature_flag = config.get_feature_flag_from_key(feature_flag_key)
1009
- unless feature_flag
1010
- @logger.log(Logger::INFO, "No feature flag was found for key '#{feature_flag_key}'.")
1011
- return nil
1012
- end
1013
-
1014
- variable = config.get_feature_variable(feature_flag, variable_key)
1015
-
1016
- # Error message logged in DatafileProjectConfig- get_feature_flag_from_key
1017
- return nil if variable.nil?
1018
-
1019
- # If variable_type is nil, set it equal to variable['type']
1020
- variable_type ||= variable['type']
1021
- # Returns nil if type differs
1022
- if variable['type'] != variable_type
1023
- @logger.log(Logger::WARN,
1024
- "Requested variable as type '#{variable_type}' but variable '#{variable_key}' is of type '#{variable['type']}'.")
1025
- return nil
1026
- end
1027
-
1028
- user_context = OptimizelyUserContext.new(self, user_id, attributes, identify: false)
1029
- decision, = @decision_service.get_variation_for_feature(config, feature_flag, user_context)
1030
- variation = decision ? decision['variation'] : nil
1031
- feature_enabled = variation ? variation['featureEnabled'] : false
1032
-
1033
- variable_value = get_feature_variable_for_variation(feature_flag_key, feature_enabled, variation, variable, user_id)
1034
- variable_value = Helpers::VariableType.cast_value_to_type(variable_value, variable_type, @logger)
1035
-
1036
- source_string = Optimizely::DecisionService::DECISION_SOURCES['ROLLOUT']
1037
- if decision && decision['source'] == Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST']
1038
- source_info = {
1039
- experiment_key: decision.experiment['key'],
1040
- variation_key: variation['key']
1041
- }
1042
- source_string = Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST']
1043
- end
1044
-
1045
- @notification_center.send_notifications(
1046
- NotificationCenter::NOTIFICATION_TYPES[:DECISION],
1047
- Helpers::Constants::DECISION_NOTIFICATION_TYPES['FEATURE_VARIABLE'], user_id, (attributes || {}),
1048
- feature_key: feature_flag_key,
1049
- feature_enabled: feature_enabled,
1050
- source: source_string,
1051
- variable_key: variable_key,
1052
- variable_type: variable_type,
1053
- variable_value: variable_value,
1054
- source_info: source_info || {}
1055
- )
1056
-
1057
- variable_value
1058
- end
1059
-
1060
- def get_feature_variable_for_variation(feature_flag_key, feature_enabled, variation, variable, user_id)
1061
- # Helper method to get the non type-casted value for a variable attached to a
1062
- # feature flag. Returns appropriate variable value depending on whether there
1063
- # was a matching variation, feature was enabled or not or varible was part of the
1064
- # available variation or not. Also logs the appropriate message explaining how it
1065
- # evaluated the value of the variable.
1066
- #
1067
- # feature_flag_key - String key of feature flag the variable belongs to
1068
- # feature_enabled - Boolean indicating if feature is enabled or not
1069
- # variation - varition returned by decision service
1070
- # user_id - String user ID
1071
- #
1072
- # Returns string value of the variable.
1073
-
1074
- config = project_config
1075
- variable_value = variable['defaultValue']
1076
- if variation
1077
- if feature_enabled == true
1078
- variation_variable_usages = config.variation_id_to_variable_usage_map[variation['id']]
1079
- variable_id = variable['id']
1080
- if variation_variable_usages&.key?(variable_id)
1081
- variable_value = variation_variable_usages[variable_id]['value']
1082
- @logger.log(Logger::INFO,
1083
- "Got variable value '#{variable_value}' for variable '#{variable['key']}' of feature flag '#{feature_flag_key}'.")
1084
- else
1085
- @logger.log(Logger::DEBUG,
1086
- "Variable value is not defined. Returning the default variable value '#{variable_value}' for variable '#{variable['key']}'.")
1087
-
1088
- end
1089
- else
1090
- @logger.log(Logger::DEBUG,
1091
- "Feature '#{feature_flag_key}' is not enabled for user '#{user_id}'. Returning the default variable value '#{variable_value}'.")
1092
- end
1093
- else
1094
- @logger.log(Logger::INFO,
1095
- "User '#{user_id}' was not bucketed into experiment or rollout for feature flag '#{feature_flag_key}'. Returning the default variable value '#{variable_value}'.")
1096
- end
1097
- variable_value
1098
- end
1099
-
1100
- def user_inputs_valid?(attributes = nil, event_tags = nil)
1101
- # Helper method to validate user inputs.
1102
- #
1103
- # attributes - Dict representing user attributes.
1104
- # event_tags - Dict representing metadata associated with an event.
1105
- #
1106
- # Returns boolean True if inputs are valid. False otherwise.
1107
-
1108
- return false if !attributes.nil? && !attributes_valid?(attributes)
1109
-
1110
- return false if !event_tags.nil? && !event_tags_valid?(event_tags)
1111
-
1112
- true
1113
- end
1114
-
1115
- def attributes_valid?(attributes)
1116
- unless Helpers::Validator.attributes_valid?(attributes)
1117
- @logger.log(Logger::ERROR, 'Provided attributes are in an invalid format.')
1118
- @error_handler.handle_error(InvalidAttributeFormatError)
1119
- return false
1120
- end
1121
- true
1122
- end
1123
-
1124
- def event_tags_valid?(event_tags)
1125
- unless Helpers::Validator.event_tags_valid?(event_tags)
1126
- @logger.log(Logger::ERROR, 'Provided event tags are in an invalid format.')
1127
- @error_handler.handle_error(InvalidEventTagFormatError)
1128
- return false
1129
- end
1130
- true
1131
- end
1132
-
1133
- def validate_instantiation_options
1134
- raise InvalidInputError, 'logger' unless Helpers::Validator.logger_valid?(@logger)
1135
-
1136
- unless Helpers::Validator.error_handler_valid?(@error_handler)
1137
- @error_handler = NoOpErrorHandler.new
1138
- raise InvalidInputError, 'error_handler'
1139
- end
1140
-
1141
- return if Helpers::Validator.event_dispatcher_valid?(@event_dispatcher)
1142
-
1143
- @event_dispatcher = EventDispatcher.new(logger: @logger, error_handler: @error_handler)
1144
- raise InvalidInputError, 'event_dispatcher'
1145
- end
1146
-
1147
- def send_impression(config, experiment, variation_key, flag_key, rule_key, enabled, rule_type, user_id, attributes = nil)
1148
- if experiment.nil?
1149
- experiment = {
1150
- 'id' => '',
1151
- 'key' => '',
1152
- 'layerId' => '',
1153
- 'status' => '',
1154
- 'variations' => [],
1155
- 'trafficAllocation' => [],
1156
- 'audienceIds' => [],
1157
- 'audienceConditions' => [],
1158
- 'forcedVariations' => {}
1159
- }
1160
- end
1161
-
1162
- experiment_id = experiment['id']
1163
- experiment_key = experiment['key']
1164
-
1165
- variation_id = config.get_variation_id_from_key_by_experiment_id(experiment_id, variation_key) unless experiment_id.empty?
1166
-
1167
- unless variation_id
1168
- variation = !flag_key.empty? ? get_flag_variation(flag_key, variation_key, 'key') : nil
1169
- variation_id = variation ? variation['id'] : ''
1170
- end
1171
-
1172
- metadata = {
1173
- flag_key: flag_key,
1174
- rule_key: rule_key,
1175
- rule_type: rule_type,
1176
- variation_key: variation_key,
1177
- enabled: enabled
1178
- }
1179
-
1180
- user_event = UserEventFactory.create_impression_event(config, experiment, variation_id, metadata, user_id, attributes)
1181
- @event_processor.process(user_event)
1182
- return unless @notification_center.notification_count(NotificationCenter::NOTIFICATION_TYPES[:ACTIVATE]).positive?
1183
-
1184
- @logger.log(Logger::INFO, "Activating user '#{user_id}' in experiment '#{experiment_key}'.")
1185
-
1186
- experiment = nil if experiment_id == ''
1187
- variation = nil
1188
- variation = config.get_variation_from_id_by_experiment_id(experiment_id, variation_id) unless experiment.nil?
1189
- log_event = EventFactory.create_log_event(user_event, @logger)
1190
- @notification_center.send_notifications(
1191
- NotificationCenter::NOTIFICATION_TYPES[:ACTIVATE],
1192
- experiment, user_id, attributes, variation, log_event
1193
- )
1194
- end
1195
-
1196
- def project_config
1197
- @config_manager.config
1198
- end
1199
-
1200
- def update_odp_config_on_datafile_update
1201
- # if datafile isn't ready, expects to be called again by the internal notification_center
1202
- return if @config_manager.respond_to?(:ready?) && !@config_manager.ready?
1203
-
1204
- config = @config_manager&.config
1205
- return unless config
1206
-
1207
- @odp_manager.update_odp_config(config.public_key_for_odp, config.host_for_odp, config.all_segments)
1208
- end
1209
-
1210
- def setup_odp!(sdk_key)
1211
- unless @sdk_settings.is_a? Optimizely::Helpers::OptimizelySdkSettings
1212
- @logger.log(Logger::DEBUG, 'Provided sdk_settings is not an OptimizelySdkSettings instance.') unless @sdk_settings.nil?
1213
- @sdk_settings = Optimizely::Helpers::OptimizelySdkSettings.new
1214
- end
1215
-
1216
- if !@sdk_settings.odp_segment_manager.nil? && !Helpers::Validator.segment_manager_valid?(@sdk_settings.odp_segment_manager)
1217
- @logger.log(Logger::ERROR, 'Invalid ODP segment manager, reverting to default.')
1218
- @sdk_settings.odp_segment_manager = nil
1219
- end
1220
-
1221
- if !@sdk_settings.odp_event_manager.nil? && !Helpers::Validator.event_manager_valid?(@sdk_settings.odp_event_manager)
1222
- @logger.log(Logger::ERROR, 'Invalid ODP event manager, reverting to default.')
1223
- @sdk_settings.odp_event_manager = nil
1224
- end
1225
-
1226
- if !@sdk_settings.odp_segments_cache.nil? && !Helpers::Validator.segments_cache_valid?(@sdk_settings.odp_segments_cache)
1227
- @logger.log(Logger::ERROR, 'Invalid ODP segments cache, reverting to default.')
1228
- @sdk_settings.odp_segments_cache = nil
1229
- end
1230
-
1231
- # no need to instantiate a cache if a custom cache or segment manager is provided.
1232
- if !@sdk_settings.odp_disabled && @sdk_settings.odp_segment_manager.nil?
1233
- @sdk_settings.odp_segments_cache ||= LRUCache.new(
1234
- @sdk_settings.segments_cache_size,
1235
- @sdk_settings.segments_cache_timeout_in_secs
1236
- )
1237
- end
1238
-
1239
- @odp_manager = OdpManager.new(
1240
- disable: @sdk_settings.odp_disabled,
1241
- segment_manager: @sdk_settings.odp_segment_manager,
1242
- event_manager: @sdk_settings.odp_event_manager,
1243
- segments_cache: @sdk_settings.odp_segments_cache,
1244
- fetch_segments_timeout: @sdk_settings.fetch_segments_timeout,
1245
- odp_event_timeout: @sdk_settings.odp_event_timeout,
1246
- odp_flush_interval: @sdk_settings.odp_flush_interval,
1247
- logger: @logger
1248
- )
1249
-
1250
- return if @sdk_settings.odp_disabled
1251
-
1252
- Optimizely::NotificationCenterRegistry
1253
- .get_notification_center(sdk_key, @logger)
1254
- &.add_notification_listener(
1255
- NotificationCenter::NOTIFICATION_TYPES[:OPTIMIZELY_CONFIG_UPDATE],
1256
- method(:update_odp_config_on_datafile_update)
1257
- )
1258
-
1259
- update_odp_config_on_datafile_update
1260
- end
1261
- end
1262
- end
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright 2016-2023, Optimizely and contributors
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+ require_relative 'optimizely/audience'
19
+ require_relative 'optimizely/config/datafile_project_config'
20
+ require_relative 'optimizely/config_manager/http_project_config_manager'
21
+ require_relative 'optimizely/config_manager/static_project_config_manager'
22
+ require_relative 'optimizely/decide/optimizely_decide_option'
23
+ require_relative 'optimizely/decide/optimizely_decision'
24
+ require_relative 'optimizely/decide/optimizely_decision_message'
25
+ require_relative 'optimizely/decision_service'
26
+ require_relative 'optimizely/error_handler'
27
+ require_relative 'optimizely/event_builder'
28
+ require_relative 'optimizely/event/batch_event_processor'
29
+ require_relative 'optimizely/event/event_factory'
30
+ require_relative 'optimizely/event/user_event_factory'
31
+ require_relative 'optimizely/event_dispatcher'
32
+ require_relative 'optimizely/exceptions'
33
+ require_relative 'optimizely/helpers/constants'
34
+ require_relative 'optimizely/helpers/group'
35
+ require_relative 'optimizely/helpers/validator'
36
+ require_relative 'optimizely/helpers/variable_type'
37
+ require_relative 'optimizely/logger'
38
+ require_relative 'optimizely/notification_center'
39
+ require_relative 'optimizely/notification_center_registry'
40
+ require_relative 'optimizely/optimizely_config'
41
+ require_relative 'optimizely/optimizely_user_context'
42
+ require_relative 'optimizely/odp/lru_cache'
43
+ require_relative 'optimizely/odp/odp_manager'
44
+ require_relative 'optimizely/helpers/sdk_settings'
45
+ require_relative 'optimizely/user_profile_tracker'
46
+
47
+ module Optimizely
48
+ class Project
49
+ include Optimizely::Decide
50
+
51
+ attr_reader :notification_center
52
+ # @api no-doc
53
+ attr_reader :config_manager, :decision_service, :error_handler, :event_dispatcher,
54
+ :event_processor, :logger, :odp_manager, :stopped
55
+
56
+ # Constructor for Projects.
57
+ #
58
+ # @param datafile - JSON string representing the project.
59
+ # @param event_dispatcher - Provides a dispatch_event method which if given a URL and params sends a request to it.
60
+ # @param logger - Optional component which provides a log method to log messages. By default nothing would be logged.
61
+ # @param error_handler - Optional component which provides a handle_error method to handle exceptions.
62
+ # By default all exceptions will be suppressed.
63
+ # @param user_profile_service - Optional component which provides methods to store and retreive user profiles.
64
+ # @param skip_json_validation - Optional boolean param to skip JSON schema validation of the provided datafile.
65
+ # @params sdk_key - Optional string uniquely identifying the datafile corresponding to project and environment combination.
66
+ # Must provide at least one of datafile or sdk_key.
67
+ # @param config_manager - Optional Responds to 'config' method.
68
+ # @param notification_center - Optional Instance of NotificationCenter.
69
+ # @param event_processor - Optional Responds to process.
70
+ # @param default_decide_options: Optional default decision options.
71
+ # @param event_processor_options: Optional hash of options to be passed to the default batch event processor.
72
+ # @param settings: Optional instance of OptimizelySdkSettings for sdk configuration.
73
+
74
+ def initialize(
75
+ datafile: nil,
76
+ event_dispatcher: nil,
77
+ logger: nil,
78
+ error_handler: nil,
79
+ skip_json_validation: false,
80
+ user_profile_service: nil,
81
+ sdk_key: nil,
82
+ config_manager: nil,
83
+ notification_center: nil,
84
+ event_processor: nil,
85
+ default_decide_options: [],
86
+ event_processor_options: {},
87
+ settings: nil
88
+ )
89
+ @logger = logger || NoOpLogger.new
90
+ @error_handler = error_handler || NoOpErrorHandler.new
91
+ @event_dispatcher = event_dispatcher || EventDispatcher.new(logger: @logger, error_handler: @error_handler)
92
+ @user_profile_service = user_profile_service
93
+ @default_decide_options = []
94
+ @sdk_settings = settings
95
+
96
+ if default_decide_options.is_a? Array
97
+ @default_decide_options = default_decide_options.clone
98
+ else
99
+ @logger.log(Logger::DEBUG, 'Provided default decide options is not an array.')
100
+ @default_decide_options = []
101
+ end
102
+
103
+ unless event_processor_options.is_a? Hash
104
+ @logger.log(Logger::DEBUG, 'Provided event processor options is not a hash.')
105
+ event_processor_options = {}
106
+ end
107
+
108
+ begin
109
+ validate_instantiation_options
110
+ rescue InvalidInputError => e
111
+ @logger = SimpleLogger.new
112
+ @logger.log(Logger::ERROR, e.message)
113
+ end
114
+
115
+ @notification_center = notification_center.is_a?(Optimizely::NotificationCenter) ? notification_center : NotificationCenter.new(@logger, @error_handler)
116
+
117
+ @config_manager = if config_manager.respond_to?(:config) && config_manager.respond_to?(:sdk_key)
118
+ config_manager
119
+ elsif sdk_key
120
+ HTTPProjectConfigManager.new(
121
+ sdk_key: sdk_key,
122
+ datafile: datafile,
123
+ logger: @logger,
124
+ error_handler: @error_handler,
125
+ skip_json_validation: skip_json_validation,
126
+ notification_center: @notification_center
127
+ )
128
+ else
129
+ StaticProjectConfigManager.new(datafile, @logger, @error_handler, skip_json_validation)
130
+ end
131
+
132
+ setup_odp!(@config_manager.sdk_key)
133
+
134
+ @decision_service = DecisionService.new(@logger, @user_profile_service)
135
+
136
+ @event_processor = if event_processor.respond_to?(:process)
137
+ event_processor
138
+ else
139
+ BatchEventProcessor.new(
140
+ event_dispatcher: @event_dispatcher,
141
+ logger: @logger,
142
+ notification_center: @notification_center,
143
+ batch_size: event_processor_options[:batch_size] || BatchEventProcessor::DEFAULT_BATCH_SIZE,
144
+ flush_interval: event_processor_options[:flush_interval] || BatchEventProcessor::DEFAULT_BATCH_INTERVAL
145
+ )
146
+ end
147
+ end
148
+
149
+ # Create a context of the user for which decision APIs will be called.
150
+ #
151
+ # A user context will be created successfully even when the SDK is not fully configured yet.
152
+ #
153
+ # @param user_id - The user ID to be used for bucketing.
154
+ # @param attributes - A Hash representing user attribute names and values.
155
+ #
156
+ # @return [OptimizelyUserContext] An OptimizelyUserContext associated with this OptimizelyClient.
157
+ # @return [nil] If user attributes are not in valid format.
158
+
159
+ def create_user_context(user_id, attributes = nil)
160
+ # We do not check for is_valid here as a user context can be created successfully
161
+ # even when the SDK is not fully configured.
162
+
163
+ # validate user_id
164
+ return nil unless Optimizely::Helpers::Validator.inputs_valid?(
165
+ {
166
+ user_id: user_id
167
+ }, @logger, Logger::ERROR
168
+ )
169
+
170
+ # validate attributes
171
+ return nil unless user_inputs_valid?(attributes)
172
+
173
+ OptimizelyUserContext.new(self, user_id, attributes)
174
+ end
175
+
176
+ def create_optimizely_decision(user_context, flag_key, decision, reasons, decide_options, config)
177
+ # Create Optimizely Decision Result.
178
+ user_id = user_context.user_id
179
+ attributes = user_context.user_attributes
180
+ variation_key = nil
181
+ feature_enabled = false
182
+ rule_key = nil
183
+ all_variables = {}
184
+ decision_event_dispatched = false
185
+ feature_flag = config.get_feature_flag_from_key(flag_key)
186
+ experiment = nil
187
+ decision_source = Optimizely::DecisionService::DECISION_SOURCES['ROLLOUT']
188
+ # Send impression event if Decision came from a feature test and decide options doesn't include disableDecisionEvent
189
+ if decision.is_a?(Optimizely::DecisionService::Decision)
190
+ experiment = decision.experiment
191
+ rule_key = experiment ? experiment['key'] : nil
192
+ variation = decision['variation']
193
+ variation_key = variation ? variation['key'] : nil
194
+ feature_enabled = variation ? variation['featureEnabled'] : false
195
+ decision_source = decision.source
196
+ end
197
+
198
+ if !decide_options.include?(OptimizelyDecideOption::DISABLE_DECISION_EVENT) && (decision_source == Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST'] || config.send_flag_decisions)
199
+ send_impression(config, experiment, variation_key || '', flag_key, rule_key || '', feature_enabled, decision_source, user_id, attributes)
200
+ decision_event_dispatched = true
201
+ end
202
+
203
+ # Generate all variables map if decide options doesn't include excludeVariables
204
+ unless decide_options.include? OptimizelyDecideOption::EXCLUDE_VARIABLES
205
+ feature_flag['variables'].each do |variable|
206
+ variable_value = get_feature_variable_for_variation(flag_key, feature_enabled, variation, variable, user_id)
207
+ all_variables[variable['key']] = Helpers::VariableType.cast_value_to_type(variable_value, variable['type'], @logger)
208
+ end
209
+ end
210
+
211
+ should_include_reasons = decide_options.include? OptimizelyDecideOption::INCLUDE_REASONS
212
+
213
+ # Send notification
214
+ @notification_center.send_notifications(
215
+ NotificationCenter::NOTIFICATION_TYPES[:DECISION],
216
+ Helpers::Constants::DECISION_NOTIFICATION_TYPES['FLAG'],
217
+ user_id, (attributes || {}),
218
+ flag_key: flag_key,
219
+ enabled: feature_enabled,
220
+ variables: all_variables,
221
+ variation_key: variation_key,
222
+ rule_key: rule_key,
223
+ reasons: should_include_reasons ? reasons : [],
224
+ decision_event_dispatched: decision_event_dispatched
225
+ )
226
+
227
+ OptimizelyDecision.new(
228
+ variation_key: variation_key,
229
+ enabled: feature_enabled,
230
+ variables: all_variables,
231
+ rule_key: rule_key,
232
+ flag_key: flag_key,
233
+ user_context: user_context,
234
+ reasons: should_include_reasons ? reasons : []
235
+ )
236
+ end
237
+
238
+ def decide(user_context, key, decide_options = [])
239
+ # raising on user context as it is internal and not provided directly by the user.
240
+ raise if user_context.class != OptimizelyUserContext
241
+
242
+ reasons = []
243
+
244
+ # check if SDK is ready
245
+ unless is_valid
246
+ @logger.log(Logger::ERROR, InvalidProjectConfigError.new('decide').message)
247
+ reasons.push(OptimizelyDecisionMessage::SDK_NOT_READY)
248
+ return OptimizelyDecision.new(flag_key: key, user_context: user_context, reasons: reasons)
249
+ end
250
+
251
+ # validate that key is a string
252
+ unless key.is_a?(String)
253
+ @logger.log(Logger::ERROR, 'Provided key is invalid')
254
+ reasons.push(format(OptimizelyDecisionMessage::FLAG_KEY_INVALID, key))
255
+ return OptimizelyDecision.new(flag_key: key, user_context: user_context, reasons: reasons)
256
+ end
257
+
258
+ # validate that key maps to a feature flag
259
+ config = project_config
260
+ feature_flag = config.get_feature_flag_from_key(key)
261
+ unless feature_flag
262
+ @logger.log(Logger::ERROR, "No feature flag was found for key '#{key}'.")
263
+ reasons.push(format(OptimizelyDecisionMessage::FLAG_KEY_INVALID, key))
264
+ return OptimizelyDecision.new(flag_key: key, user_context: user_context, reasons: reasons)
265
+ end
266
+
267
+ # merge decide_options and default_decide_options
268
+ if decide_options.is_a? Array
269
+ decide_options += @default_decide_options
270
+ else
271
+ @logger.log(Logger::DEBUG, 'Provided decide options is not an array. Using default decide options.')
272
+ decide_options = @default_decide_options
273
+ end
274
+
275
+ decide_options.delete(OptimizelyDecideOption::ENABLED_FLAGS_ONLY) if decide_options.include?(OptimizelyDecideOption::ENABLED_FLAGS_ONLY)
276
+ decide_for_keys(user_context, [key], decide_options, true)[key]
277
+ end
278
+
279
+ def decide_all(user_context, decide_options = [])
280
+ # raising on user context as it is internal and not provided directly by the user.
281
+ raise if user_context.class != OptimizelyUserContext
282
+
283
+ # check if SDK is ready
284
+ unless is_valid
285
+ @logger.log(Logger::ERROR, InvalidProjectConfigError.new('decide_all').message)
286
+ return {}
287
+ end
288
+
289
+ keys = []
290
+ project_config.feature_flags.each do |feature_flag|
291
+ keys.push(feature_flag['key'])
292
+ end
293
+ decide_for_keys(user_context, keys, decide_options)
294
+ end
295
+
296
+ def decide_for_keys(user_context, keys, decide_options = [], ignore_default_options = false) # rubocop:disable Style/OptionalBooleanParameter
297
+ # raising on user context as it is internal and not provided directly by the user.
298
+ raise if user_context.class != OptimizelyUserContext
299
+
300
+ # check if SDK is ready
301
+ unless is_valid
302
+ @logger.log(Logger::ERROR, InvalidProjectConfigError.new('decide_for_keys').message)
303
+ return {}
304
+ end
305
+
306
+ # merge decide_options and default_decide_options
307
+ unless ignore_default_options
308
+ if decide_options.is_a?(Array)
309
+ decide_options += @default_decide_options
310
+ else
311
+ @logger.log(Logger::DEBUG, 'Provided decide options is not an array. Using default decide options.')
312
+ decide_options = @default_decide_options
313
+ end
314
+ end
315
+
316
+ # enabled_flags_only = (!decide_options.nil? && (decide_options.include? OptimizelyDecideOption::ENABLED_FLAGS_ONLY)) || (@default_decide_options.include? OptimizelyDecideOption::ENABLED_FLAGS_ONLY)
317
+
318
+ decisions = {}
319
+ valid_keys = []
320
+ decision_reasons_dict = {}
321
+ config = project_config
322
+ return decisions unless config
323
+
324
+ flags_without_forced_decision = []
325
+ flag_decisions = {}
326
+
327
+ keys.each do |key|
328
+ # Retrieve the feature flag from the project's feature flag key map
329
+ feature_flag = config.feature_flag_key_map[key]
330
+
331
+ # If the feature flag is nil, create a default OptimizelyDecision and move to the next key
332
+ if feature_flag.nil?
333
+ decisions[key] = OptimizelyDecision.new(nil, false, nil, nil, key, user_context, [])
334
+ next
335
+ end
336
+ valid_keys.push(key)
337
+ decision_reasons = []
338
+ decision_reasons_dict[key] = decision_reasons
339
+
340
+ config = project_config
341
+ context = Optimizely::OptimizelyUserContext::OptimizelyDecisionContext.new(key, nil)
342
+ variation, reasons_received = @decision_service.validated_forced_decision(config, context, user_context)
343
+ decision_reasons_dict[key].push(*reasons_received)
344
+ if variation
345
+ decision = Optimizely::DecisionService::Decision.new(nil, variation, Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST'])
346
+ flag_decisions[key] = decision
347
+ else
348
+ flags_without_forced_decision.push(feature_flag)
349
+ end
350
+ end
351
+ decision_list = @decision_service.get_variations_for_feature_list(config, flags_without_forced_decision, user_context, decide_options)
352
+
353
+ flags_without_forced_decision.each_with_index do |flag, i|
354
+ decision = decision_list[i][0]
355
+ reasons = decision_list[i][1]
356
+ flag_key = flag['key']
357
+ flag_decisions[flag_key] = decision
358
+ decision_reasons_dict[flag_key] ||= []
359
+ decision_reasons_dict[flag_key].push(*reasons)
360
+ end
361
+ valid_keys.each do |key|
362
+ flag_decision = flag_decisions[key]
363
+ decision_reasons = decision_reasons_dict[key]
364
+ optimizely_decision = create_optimizely_decision(
365
+ user_context,
366
+ key,
367
+ flag_decision,
368
+ decision_reasons,
369
+ decide_options,
370
+ config
371
+ )
372
+
373
+ enabled_flags_only_missing = !decide_options.include?(OptimizelyDecideOption::ENABLED_FLAGS_ONLY)
374
+ is_enabled = optimizely_decision.enabled
375
+
376
+ decisions[key] = optimizely_decision if enabled_flags_only_missing || is_enabled
377
+ end
378
+
379
+ decisions
380
+ end
381
+
382
+ # Gets variation using variation key or id and flag key.
383
+ #
384
+ # @param flag_key - flag key from which the variation is required.
385
+ # @param target_value - variation value either id or key that will be matched.
386
+ # @param attribute - string representing variation attribute.
387
+ #
388
+ # @return [variation]
389
+ # @return [nil] if no variation found in flag_variation_map.
390
+
391
+ def get_flag_variation(flag_key, target_value, attribute)
392
+ project_config.get_variation_from_flag(flag_key, target_value, attribute)
393
+ end
394
+
395
+ # Buckets visitor and sends impression event to Optimizely.
396
+ #
397
+ # @param experiment_key - Experiment which needs to be activated.
398
+ # @param user_id - String ID for user.
399
+ # @param attributes - Hash representing user attributes and values to be recorded.
400
+ #
401
+ # @return [Variation Key] representing the variation the user will be bucketed in.
402
+ # @return [nil] if experiment is not Running, if user is not in experiment, or if datafile is invalid.
403
+
404
+ def activate(experiment_key, user_id, attributes = nil)
405
+ unless is_valid
406
+ @logger.log(Logger::ERROR, InvalidProjectConfigError.new('activate').message)
407
+ return nil
408
+ end
409
+
410
+ return nil unless Optimizely::Helpers::Validator.inputs_valid?(
411
+ {
412
+ experiment_key: experiment_key,
413
+ user_id: user_id
414
+ }, @logger, Logger::ERROR
415
+ )
416
+
417
+ config = project_config
418
+
419
+ variation_key = get_variation_with_config(experiment_key, user_id, attributes, config)
420
+
421
+ if variation_key.nil?
422
+ @logger.log(Logger::INFO, "Not activating user '#{user_id}'.")
423
+ return nil
424
+ end
425
+
426
+ # Create and dispatch impression event
427
+ experiment = config.get_experiment_from_key(experiment_key)
428
+ send_impression(
429
+ config, experiment, variation_key, '', experiment_key, true,
430
+ Optimizely::DecisionService::DECISION_SOURCES['EXPERIMENT'], user_id, attributes
431
+ )
432
+
433
+ variation_key
434
+ end
435
+
436
+ # Gets variation where visitor will be bucketed.
437
+ #
438
+ # @param experiment_key - Experiment for which visitor variation needs to be determined.
439
+ # @param user_id - String ID for user.
440
+ # @param attributes - Hash representing user attributes.
441
+ #
442
+ # @return [variation key] where visitor will be bucketed.
443
+ # @return [nil] if experiment is not Running, if user is not in experiment, or if datafile is invalid.
444
+
445
+ def get_variation(experiment_key, user_id, attributes = nil)
446
+ unless is_valid
447
+ @logger.log(Logger::ERROR, InvalidProjectConfigError.new('get_variation').message)
448
+ return nil
449
+ end
450
+
451
+ return nil unless Optimizely::Helpers::Validator.inputs_valid?(
452
+ {
453
+ experiment_key: experiment_key,
454
+ user_id: user_id
455
+ }, @logger, Logger::ERROR
456
+ )
457
+
458
+ config = project_config
459
+
460
+ get_variation_with_config(experiment_key, user_id, attributes, config)
461
+ end
462
+
463
+ # Force a user into a variation for a given experiment.
464
+ #
465
+ # @param experiment_key - String - key identifying the experiment.
466
+ # @param user_id - String - The user ID to be used for bucketing.
467
+ # @param variation_key - The variation key specifies the variation which the user will
468
+ # be forced into. If nil, then clear the existing experiment-to-variation mapping.
469
+ #
470
+ # @return [Boolean] indicates if the set completed successfully.
471
+
472
+ def set_forced_variation(experiment_key, user_id, variation_key)
473
+ unless is_valid
474
+ @logger.log(Logger::ERROR, InvalidProjectConfigError.new('set_forced_variation').message)
475
+ return nil
476
+ end
477
+
478
+ input_values = {experiment_key: experiment_key, user_id: user_id}
479
+ input_values[:variation_key] = variation_key unless variation_key.nil?
480
+ return false unless Optimizely::Helpers::Validator.inputs_valid?(input_values, @logger, Logger::ERROR)
481
+
482
+ config = project_config
483
+
484
+ @decision_service.set_forced_variation(config, experiment_key, user_id, variation_key)
485
+ end
486
+
487
+ # Gets the forced variation for a given user and experiment.
488
+ #
489
+ # @param experiment_key - String - Key identifying the experiment.
490
+ # @param user_id - String - The user ID to be used for bucketing.
491
+ #
492
+ # @return [String] The forced variation key.
493
+
494
+ def get_forced_variation(experiment_key, user_id)
495
+ unless is_valid
496
+ @logger.log(Logger::ERROR, InvalidProjectConfigError.new('get_forced_variation').message)
497
+ return nil
498
+ end
499
+
500
+ return nil unless Optimizely::Helpers::Validator.inputs_valid?(
501
+ {
502
+ experiment_key: experiment_key,
503
+ user_id: user_id
504
+ }, @logger, Logger::ERROR
505
+ )
506
+
507
+ config = project_config
508
+
509
+ forced_variation_key = nil
510
+ forced_variation, = @decision_service.get_forced_variation(config, experiment_key, user_id)
511
+ forced_variation_key = forced_variation['key'] if forced_variation
512
+
513
+ forced_variation_key
514
+ end
515
+
516
+ # Send conversion event to Optimizely.
517
+ #
518
+ # @param event_key - Event key representing the event which needs to be recorded.
519
+ # @param user_id - String ID for user.
520
+ # @param attributes - Hash representing visitor attributes and values which need to be recorded.
521
+ # @param event_tags - Hash representing metadata associated with the event.
522
+
523
+ def track(event_key, user_id, attributes = nil, event_tags = nil)
524
+ unless is_valid
525
+ @logger.log(Logger::ERROR, InvalidProjectConfigError.new('track').message)
526
+ return nil
527
+ end
528
+
529
+ return nil unless Optimizely::Helpers::Validator.inputs_valid?(
530
+ {
531
+ event_key: event_key,
532
+ user_id: user_id
533
+ }, @logger, Logger::ERROR
534
+ )
535
+
536
+ return nil unless user_inputs_valid?(attributes, event_tags)
537
+
538
+ config = project_config
539
+
540
+ event = config.get_event_from_key(event_key)
541
+ unless event
542
+ @logger.log(Logger::INFO, "Not tracking user '#{user_id}' for event '#{event_key}'.")
543
+ return nil
544
+ end
545
+
546
+ user_event = UserEventFactory.create_conversion_event(config, event, user_id, attributes, event_tags)
547
+ @event_processor.process(user_event)
548
+ @logger.log(Logger::INFO, "Tracking event '#{event_key}' for user '#{user_id}'.")
549
+
550
+ if @notification_center.notification_count(NotificationCenter::NOTIFICATION_TYPES[:TRACK]).positive?
551
+ log_event = EventFactory.create_log_event(user_event, @logger)
552
+ @notification_center.send_notifications(
553
+ NotificationCenter::NOTIFICATION_TYPES[:TRACK],
554
+ event_key, user_id, attributes, event_tags, log_event
555
+ )
556
+ end
557
+ nil
558
+ end
559
+
560
+ # Determine whether a feature is enabled.
561
+ # Sends an impression event if the user is bucketed into an experiment using the feature.
562
+ #
563
+ # @param feature_flag_key - String unique key of the feature.
564
+ # @param user_id - String ID of the user.
565
+ # @param attributes - Hash representing visitor attributes and values which need to be recorded.
566
+ #
567
+ # @return [True] if the feature is enabled.
568
+ # @return [False] if the feature is disabled.
569
+ # @return [False] if the feature is not found.
570
+
571
+ def is_feature_enabled(feature_flag_key, user_id, attributes = nil)
572
+ unless is_valid
573
+ @logger.log(Logger::ERROR, InvalidProjectConfigError.new('is_feature_enabled').message)
574
+ return false
575
+ end
576
+
577
+ return false unless Optimizely::Helpers::Validator.inputs_valid?(
578
+ {
579
+ feature_flag_key: feature_flag_key,
580
+ user_id: user_id
581
+ }, @logger, Logger::ERROR
582
+ )
583
+
584
+ return false unless user_inputs_valid?(attributes)
585
+
586
+ config = project_config
587
+
588
+ feature_flag = config.get_feature_flag_from_key(feature_flag_key)
589
+ unless feature_flag
590
+ @logger.log(Logger::ERROR, "No feature flag was found for key '#{feature_flag_key}'.")
591
+ return false
592
+ end
593
+
594
+ user_context = OptimizelyUserContext.new(self, user_id, attributes, identify: false)
595
+ decision, = @decision_service.get_variation_for_feature(config, feature_flag, user_context)
596
+
597
+ feature_enabled = false
598
+ source_string = Optimizely::DecisionService::DECISION_SOURCES['ROLLOUT']
599
+ if decision.is_a?(Optimizely::DecisionService::Decision)
600
+ variation = decision['variation']
601
+ feature_enabled = variation['featureEnabled']
602
+ if decision.source == Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST']
603
+ source_string = Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST']
604
+ source_info = {
605
+ experiment_key: decision.experiment['key'],
606
+ variation_key: variation['key']
607
+ }
608
+ # Send event if Decision came from a feature test.
609
+ send_impression(
610
+ config, decision.experiment, variation['key'], feature_flag_key, decision.experiment['key'], feature_enabled, source_string, user_id, attributes
611
+ )
612
+ elsif decision.source == Optimizely::DecisionService::DECISION_SOURCES['ROLLOUT'] && config.send_flag_decisions
613
+ send_impression(
614
+ config, decision.experiment, variation['key'], feature_flag_key, decision.experiment['key'], feature_enabled, source_string, user_id, attributes
615
+ )
616
+ end
617
+ end
618
+
619
+ if decision.nil? && config.send_flag_decisions
620
+ send_impression(
621
+ config, nil, '', feature_flag_key, '', feature_enabled, source_string, user_id, attributes
622
+ )
623
+ end
624
+
625
+ @notification_center.send_notifications(
626
+ NotificationCenter::NOTIFICATION_TYPES[:DECISION],
627
+ Helpers::Constants::DECISION_NOTIFICATION_TYPES['FEATURE'],
628
+ user_id, (attributes || {}),
629
+ feature_key: feature_flag_key,
630
+ feature_enabled: feature_enabled,
631
+ source: source_string,
632
+ source_info: source_info || {}
633
+ )
634
+
635
+ if feature_enabled == true
636
+ @logger.log(Logger::INFO,
637
+ "Feature '#{feature_flag_key}' is enabled for user '#{user_id}'.")
638
+ return true
639
+ end
640
+
641
+ @logger.log(Logger::INFO,
642
+ "Feature '#{feature_flag_key}' is not enabled for user '#{user_id}'.")
643
+ false
644
+ end
645
+
646
+ # Gets keys of all feature flags which are enabled for the user.
647
+ #
648
+ # @param user_id - ID for user.
649
+ # @param attributes - Dict representing user attributes.
650
+ # @return [feature flag keys] A List of feature flag keys that are enabled for the user.
651
+
652
+ def get_enabled_features(user_id, attributes = nil)
653
+ enabled_features = []
654
+ unless is_valid
655
+ @logger.log(Logger::ERROR, InvalidProjectConfigError.new('get_enabled_features').message)
656
+ return enabled_features
657
+ end
658
+
659
+ return enabled_features unless Optimizely::Helpers::Validator.inputs_valid?(
660
+ {
661
+ user_id: user_id
662
+ }, @logger, Logger::ERROR
663
+ )
664
+
665
+ return enabled_features unless user_inputs_valid?(attributes)
666
+
667
+ config = project_config
668
+
669
+ config.feature_flags.each do |feature|
670
+ enabled_features.push(feature['key']) if is_feature_enabled(
671
+ feature['key'],
672
+ user_id,
673
+ attributes
674
+ ) == true
675
+ end
676
+ enabled_features
677
+ end
678
+
679
+ # Get the value of the specified variable in the feature flag.
680
+ #
681
+ # @param feature_flag_key - String key of feature flag the variable belongs to
682
+ # @param variable_key - String key of variable for which we are getting the value
683
+ # @param user_id - String user ID
684
+ # @param attributes - Hash representing visitor attributes and values which need to be recorded.
685
+ #
686
+ # @return [*] the type-casted variable value.
687
+ # @return [nil] if the feature flag or variable are not found.
688
+
689
+ def get_feature_variable(feature_flag_key, variable_key, user_id, attributes = nil)
690
+ unless is_valid
691
+ @logger.log(Logger::ERROR, InvalidProjectConfigError.new('get_feature_variable').message)
692
+ return nil
693
+ end
694
+ get_feature_variable_for_type(
695
+ feature_flag_key,
696
+ variable_key,
697
+ nil,
698
+ user_id,
699
+ attributes
700
+ )
701
+ end
702
+
703
+ # Get the String value of the specified variable in the feature flag.
704
+ #
705
+ # @param feature_flag_key - String key of feature flag the variable belongs to
706
+ # @param variable_key - String key of variable for which we are getting the string value
707
+ # @param user_id - String user ID
708
+ # @param attributes - Hash representing visitor attributes and values which need to be recorded.
709
+ #
710
+ # @return [String] the string variable value.
711
+ # @return [nil] if the feature flag or variable are not found.
712
+
713
+ def get_feature_variable_string(feature_flag_key, variable_key, user_id, attributes = nil)
714
+ unless is_valid
715
+ @logger.log(Logger::ERROR, InvalidProjectConfigError.new('get_feature_variable_string').message)
716
+ return nil
717
+ end
718
+ get_feature_variable_for_type(
719
+ feature_flag_key,
720
+ variable_key,
721
+ Optimizely::Helpers::Constants::VARIABLE_TYPES['STRING'],
722
+ user_id,
723
+ attributes
724
+ )
725
+ end
726
+
727
+ # Get the Json value of the specified variable in the feature flag in a Dict.
728
+ #
729
+ # @param feature_flag_key - String key of feature flag the variable belongs to
730
+ # @param variable_key - String key of variable for which we are getting the string value
731
+ # @param user_id - String user ID
732
+ # @param attributes - Hash representing visitor attributes and values which need to be recorded.
733
+ #
734
+ # @return [Dict] the Dict containing variable value.
735
+ # @return [nil] if the feature flag or variable are not found.
736
+
737
+ def get_feature_variable_json(feature_flag_key, variable_key, user_id, attributes = nil)
738
+ unless is_valid
739
+ @logger.log(Logger::ERROR, InvalidProjectConfigError.new('get_feature_variable_json').message)
740
+ return nil
741
+ end
742
+ get_feature_variable_for_type(
743
+ feature_flag_key,
744
+ variable_key,
745
+ Optimizely::Helpers::Constants::VARIABLE_TYPES['JSON'],
746
+ user_id,
747
+ attributes
748
+ )
749
+ end
750
+
751
+ # Get the Boolean value of the specified variable in the feature flag.
752
+ #
753
+ # @param feature_flag_key - String key of feature flag the variable belongs to
754
+ # @param variable_key - String key of variable for which we are getting the string value
755
+ # @param user_id - String user ID
756
+ # @param attributes - Hash representing visitor attributes and values which need to be recorded.
757
+ #
758
+ # @return [Boolean] the boolean variable value.
759
+ # @return [nil] if the feature flag or variable are not found.
760
+
761
+ def get_feature_variable_boolean(feature_flag_key, variable_key, user_id, attributes = nil)
762
+ unless is_valid
763
+ @logger.log(Logger::ERROR, InvalidProjectConfigError.new('get_feature_variable_boolean').message)
764
+ return nil
765
+ end
766
+
767
+ get_feature_variable_for_type(
768
+ feature_flag_key,
769
+ variable_key,
770
+ Optimizely::Helpers::Constants::VARIABLE_TYPES['BOOLEAN'],
771
+ user_id,
772
+ attributes
773
+ )
774
+ end
775
+
776
+ # Get the Double value of the specified variable in the feature flag.
777
+ #
778
+ # @param feature_flag_key - String key of feature flag the variable belongs to
779
+ # @param variable_key - String key of variable for which we are getting the string value
780
+ # @param user_id - String user ID
781
+ # @param attributes - Hash representing visitor attributes and values which need to be recorded.
782
+ #
783
+ # @return [Boolean] the double variable value.
784
+ # @return [nil] if the feature flag or variable are not found.
785
+
786
+ def get_feature_variable_double(feature_flag_key, variable_key, user_id, attributes = nil)
787
+ unless is_valid
788
+ @logger.log(Logger::ERROR, InvalidProjectConfigError.new('get_feature_variable_double').message)
789
+ return nil
790
+ end
791
+
792
+ get_feature_variable_for_type(
793
+ feature_flag_key,
794
+ variable_key,
795
+ Optimizely::Helpers::Constants::VARIABLE_TYPES['DOUBLE'],
796
+ user_id,
797
+ attributes
798
+ )
799
+ end
800
+
801
+ # Get values of all the variables in the feature flag and returns them in a Dict
802
+ #
803
+ # @param feature_flag_key - String key of feature flag
804
+ # @param user_id - String user ID
805
+ # @param attributes - Hash representing visitor attributes and values which need to be recorded.
806
+ #
807
+ # @return [Dict] the Dict containing all the varible values
808
+ # @return [nil] if the feature flag is not found.
809
+
810
+ def get_all_feature_variables(feature_flag_key, user_id, attributes = nil)
811
+ unless is_valid
812
+ @logger.log(Logger::ERROR, InvalidProjectConfigError.new('get_all_feature_variables').message)
813
+ return nil
814
+ end
815
+
816
+ return nil unless Optimizely::Helpers::Validator.inputs_valid?(
817
+ {
818
+ feature_flag_key: feature_flag_key,
819
+ user_id: user_id
820
+ },
821
+ @logger, Logger::ERROR
822
+ )
823
+
824
+ return nil unless user_inputs_valid?(attributes)
825
+
826
+ config = project_config
827
+
828
+ feature_flag = config.get_feature_flag_from_key(feature_flag_key)
829
+ unless feature_flag
830
+ @logger.log(Logger::INFO, "No feature flag was found for key '#{feature_flag_key}'.")
831
+ return nil
832
+ end
833
+
834
+ user_context = OptimizelyUserContext.new(self, user_id, attributes, identify: false)
835
+ decision, = @decision_service.get_variation_for_feature(config, feature_flag, user_context)
836
+ variation = decision ? decision['variation'] : nil
837
+ feature_enabled = variation ? variation['featureEnabled'] : false
838
+ all_variables = {}
839
+
840
+ feature_flag['variables'].each do |variable|
841
+ variable_value = get_feature_variable_for_variation(feature_flag_key, feature_enabled, variation, variable, user_id)
842
+ all_variables[variable['key']] = Helpers::VariableType.cast_value_to_type(variable_value, variable['type'], @logger)
843
+ end
844
+
845
+ source_string = Optimizely::DecisionService::DECISION_SOURCES['ROLLOUT']
846
+ if decision && decision['source'] == Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST']
847
+ source_info = {
848
+ experiment_key: decision.experiment['key'],
849
+ variation_key: variation['key']
850
+ }
851
+ source_string = Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST']
852
+ end
853
+
854
+ @notification_center.send_notifications(
855
+ NotificationCenter::NOTIFICATION_TYPES[:DECISION],
856
+ Helpers::Constants::DECISION_NOTIFICATION_TYPES['ALL_FEATURE_VARIABLES'], user_id, (attributes || {}),
857
+ feature_key: feature_flag_key,
858
+ feature_enabled: feature_enabled,
859
+ source: source_string,
860
+ variable_values: all_variables,
861
+ source_info: source_info || {}
862
+ )
863
+
864
+ all_variables
865
+ end
866
+
867
+ # Get the Integer value of the specified variable in the feature flag.
868
+ #
869
+ # @param feature_flag_key - String key of feature flag the variable belongs to
870
+ # @param variable_key - String key of variable for which we are getting the string value
871
+ # @param user_id - String user ID
872
+ # @param attributes - Hash representing visitor attributes and values which need to be recorded.
873
+ #
874
+ # @return [Integer] variable value.
875
+ # @return [nil] if the feature flag or variable are not found.
876
+
877
+ def get_feature_variable_integer(feature_flag_key, variable_key, user_id, attributes = nil)
878
+ unless is_valid
879
+ @logger.log(Logger::ERROR, InvalidProjectConfigError.new('get_feature_variable_integer').message)
880
+ return nil
881
+ end
882
+
883
+ get_feature_variable_for_type(
884
+ feature_flag_key,
885
+ variable_key,
886
+ Optimizely::Helpers::Constants::VARIABLE_TYPES['INTEGER'],
887
+ user_id,
888
+ attributes
889
+ )
890
+ end
891
+
892
+ def is_valid
893
+ config = project_config
894
+ config.is_a?(Optimizely::ProjectConfig)
895
+ end
896
+
897
+ def close
898
+ return if @stopped
899
+
900
+ @stopped = true
901
+ @config_manager.stop! if @config_manager.respond_to?(:stop!)
902
+ @event_processor.stop! if @event_processor.respond_to?(:stop!)
903
+ @odp_manager.stop!
904
+ end
905
+
906
+ def get_optimizely_config
907
+ # Get OptimizelyConfig object containing experiments and features data
908
+ # Returns Object
909
+ #
910
+ # OptimizelyConfig Object Schema
911
+ # {
912
+ # 'experimentsMap' => {
913
+ # 'my-fist-experiment' => {
914
+ # 'id' => '111111',
915
+ # 'key' => 'my-fist-experiment'
916
+ # 'variationsMap' => {
917
+ # 'variation_1' => {
918
+ # 'id' => '121212',
919
+ # 'key' => 'variation_1',
920
+ # 'variablesMap' => {
921
+ # 'age' => {
922
+ # 'id' => '222222',
923
+ # 'key' => 'age',
924
+ # 'type' => 'integer',
925
+ # 'value' => '0',
926
+ # }
927
+ # }
928
+ # }
929
+ # }
930
+ # }
931
+ # },
932
+ # 'featuresMap' => {
933
+ # 'awesome-feature' => {
934
+ # 'id' => '333333',
935
+ # 'key' => 'awesome-feature',
936
+ # 'experimentsMap' => Object,
937
+ # 'variablesMap' => Object,
938
+ # }
939
+ # },
940
+ # 'revision' => '13',
941
+ # }
942
+ #
943
+ unless is_valid
944
+ @logger.log(Logger::ERROR, InvalidProjectConfigError.new('get_optimizely_config').message)
945
+ return nil
946
+ end
947
+
948
+ # config_manager might not contain optimizely_config if its supplied by the consumer
949
+ # Generating a new OptimizelyConfig object in this case as a fallback
950
+ if @config_manager.respond_to?(:optimizely_config)
951
+ @config_manager.optimizely_config
952
+ else
953
+ OptimizelyConfig.new(project_config, @logger).config
954
+ end
955
+ end
956
+
957
+ # Send an event to the ODP server.
958
+ #
959
+ # @param action - the event action name. Cannot be nil or empty string.
960
+ # @param identifiers - a hash for identifiers. The caller must provide at least one key-value pair.
961
+ # @param type - the event type (default = "fullstack").
962
+ # @param data - a hash for associated data. The default event data will be added to this data before sending to the ODP server.
963
+
964
+ def send_odp_event(action:, identifiers:, type: Helpers::Constants::ODP_MANAGER_CONFIG[:EVENT_TYPE], data: {})
965
+ unless identifiers.is_a?(Hash) && !identifiers.empty?
966
+ @logger.log(Logger::ERROR, 'ODP events must have at least one key-value pair in identifiers.')
967
+ return
968
+ end
969
+
970
+ unless is_valid
971
+ @logger.log(Logger::ERROR, InvalidProjectConfigError.new('send_odp_event').message)
972
+ return
973
+ end
974
+
975
+ if action.nil? || action.empty?
976
+ @logger.log(Logger::ERROR, Helpers::Constants::ODP_LOGS[:ODP_INVALID_ACTION])
977
+ return
978
+ end
979
+
980
+ type = Helpers::Constants::ODP_MANAGER_CONFIG[:EVENT_TYPE] if type.nil? || type.empty?
981
+
982
+ @odp_manager.send_event(type: type, action: action, identifiers: identifiers, data: data)
983
+ end
984
+
985
+ def identify_user(user_id:)
986
+ unless is_valid
987
+ @logger.log(Logger::ERROR, InvalidProjectConfigError.new('identify_user').message)
988
+ return
989
+ end
990
+
991
+ @odp_manager.identify_user(user_id: user_id)
992
+ end
993
+
994
+ def fetch_qualified_segments(user_id:, options: [])
995
+ unless is_valid
996
+ @logger.log(Logger::ERROR, InvalidProjectConfigError.new('fetch_qualified_segments').message)
997
+ return
998
+ end
999
+
1000
+ @odp_manager.fetch_qualified_segments(user_id: user_id, options: options)
1001
+ end
1002
+
1003
+ private
1004
+
1005
+ def get_variation_with_config(experiment_key, user_id, attributes, config)
1006
+ # Gets variation where visitor will be bucketed.
1007
+ #
1008
+ # experiment_key - Experiment for which visitor variation needs to be determined.
1009
+ # user_id - String ID for user.
1010
+ # attributes - Hash representing user attributes.
1011
+ # config - Instance of DatfileProjectConfig
1012
+ #
1013
+ # Returns [variation key] where visitor will be bucketed.
1014
+ # Returns [nil] if experiment is not Running, if user is not in experiment, or if datafile is invalid.
1015
+ experiment = config.get_experiment_from_key(experiment_key)
1016
+ return nil if experiment.nil?
1017
+
1018
+ experiment_id = experiment['id']
1019
+
1020
+ return nil unless user_inputs_valid?(attributes)
1021
+
1022
+ user_context = OptimizelyUserContext.new(self, user_id, attributes, identify: false)
1023
+ user_profile_tracker = UserProfileTracker.new(user_id, @user_profile_service, @logger)
1024
+ user_profile_tracker.load_user_profile
1025
+ variation_id, = @decision_service.get_variation(config, experiment_id, user_context, user_profile_tracker)
1026
+ user_profile_tracker.save_user_profile
1027
+ variation = config.get_variation_from_id(experiment_key, variation_id) unless variation_id.nil?
1028
+ variation_key = variation['key'] if variation
1029
+ decision_notification_type = if config.feature_experiment?(experiment_id)
1030
+ Helpers::Constants::DECISION_NOTIFICATION_TYPES['FEATURE_TEST']
1031
+ else
1032
+ Helpers::Constants::DECISION_NOTIFICATION_TYPES['AB_TEST']
1033
+ end
1034
+ @notification_center.send_notifications(
1035
+ NotificationCenter::NOTIFICATION_TYPES[:DECISION],
1036
+ decision_notification_type, user_id, (attributes || {}),
1037
+ experiment_key: experiment_key,
1038
+ variation_key: variation_key
1039
+ )
1040
+
1041
+ variation_key
1042
+ end
1043
+
1044
+ def get_feature_variable_for_type(feature_flag_key, variable_key, variable_type, user_id, attributes = nil)
1045
+ # Get the variable value for the given feature variable and cast it to the specified type
1046
+ # The default value is returned if the feature flag is not enabled for the user.
1047
+ #
1048
+ # feature_flag_key - String key of feature flag the variable belongs to
1049
+ # variable_key - String key of variable for which we are getting the string value
1050
+ # variable_type - String requested type for feature variable
1051
+ # user_id - String user ID
1052
+ # attributes - Hash representing visitor attributes and values which need to be recorded.
1053
+ #
1054
+ # Returns the type-casted variable value.
1055
+ # Returns nil if the feature flag or variable or user ID is empty
1056
+ # in case of variable type mismatch
1057
+
1058
+ return nil unless Optimizely::Helpers::Validator.inputs_valid?(
1059
+ {
1060
+ feature_flag_key: feature_flag_key,
1061
+ variable_key: variable_key,
1062
+ user_id: user_id,
1063
+ variable_type: variable_type
1064
+ },
1065
+ @logger, Logger::ERROR
1066
+ )
1067
+
1068
+ return nil unless user_inputs_valid?(attributes)
1069
+
1070
+ config = project_config
1071
+
1072
+ feature_flag = config.get_feature_flag_from_key(feature_flag_key)
1073
+ unless feature_flag
1074
+ @logger.log(Logger::INFO, "No feature flag was found for key '#{feature_flag_key}'.")
1075
+ return nil
1076
+ end
1077
+
1078
+ variable = config.get_feature_variable(feature_flag, variable_key)
1079
+
1080
+ # Error message logged in DatafileProjectConfig- get_feature_flag_from_key
1081
+ return nil if variable.nil?
1082
+
1083
+ # If variable_type is nil, set it equal to variable['type']
1084
+ variable_type ||= variable['type']
1085
+ # Returns nil if type differs
1086
+ if variable['type'] != variable_type
1087
+ @logger.log(Logger::WARN,
1088
+ "Requested variable as type '#{variable_type}' but variable '#{variable_key}' is of type '#{variable['type']}'.")
1089
+ return nil
1090
+ end
1091
+
1092
+ user_context = OptimizelyUserContext.new(self, user_id, attributes, identify: false)
1093
+ decision, = @decision_service.get_variation_for_feature(config, feature_flag, user_context)
1094
+ variation = decision ? decision['variation'] : nil
1095
+ feature_enabled = variation ? variation['featureEnabled'] : false
1096
+
1097
+ variable_value = get_feature_variable_for_variation(feature_flag_key, feature_enabled, variation, variable, user_id)
1098
+ variable_value = Helpers::VariableType.cast_value_to_type(variable_value, variable_type, @logger)
1099
+
1100
+ source_string = Optimizely::DecisionService::DECISION_SOURCES['ROLLOUT']
1101
+ if decision && decision['source'] == Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST']
1102
+ source_info = {
1103
+ experiment_key: decision.experiment['key'],
1104
+ variation_key: variation['key']
1105
+ }
1106
+ source_string = Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST']
1107
+ end
1108
+
1109
+ @notification_center.send_notifications(
1110
+ NotificationCenter::NOTIFICATION_TYPES[:DECISION],
1111
+ Helpers::Constants::DECISION_NOTIFICATION_TYPES['FEATURE_VARIABLE'], user_id, (attributes || {}),
1112
+ feature_key: feature_flag_key,
1113
+ feature_enabled: feature_enabled,
1114
+ source: source_string,
1115
+ variable_key: variable_key,
1116
+ variable_type: variable_type,
1117
+ variable_value: variable_value,
1118
+ source_info: source_info || {}
1119
+ )
1120
+
1121
+ variable_value
1122
+ end
1123
+
1124
+ def get_feature_variable_for_variation(feature_flag_key, feature_enabled, variation, variable, user_id)
1125
+ # Helper method to get the non type-casted value for a variable attached to a
1126
+ # feature flag. Returns appropriate variable value depending on whether there
1127
+ # was a matching variation, feature was enabled or not or varible was part of the
1128
+ # available variation or not. Also logs the appropriate message explaining how it
1129
+ # evaluated the value of the variable.
1130
+ #
1131
+ # feature_flag_key - String key of feature flag the variable belongs to
1132
+ # feature_enabled - Boolean indicating if feature is enabled or not
1133
+ # variation - varition returned by decision service
1134
+ # user_id - String user ID
1135
+ #
1136
+ # Returns string value of the variable.
1137
+
1138
+ config = project_config
1139
+ variable_value = variable['defaultValue']
1140
+ if variation
1141
+ if feature_enabled == true
1142
+ variation_variable_usages = config.variation_id_to_variable_usage_map[variation['id']]
1143
+ variable_id = variable['id']
1144
+ if variation_variable_usages&.key?(variable_id)
1145
+ variable_value = variation_variable_usages[variable_id]['value']
1146
+ @logger.log(Logger::INFO,
1147
+ "Got variable value '#{variable_value}' for variable '#{variable['key']}' of feature flag '#{feature_flag_key}'.")
1148
+ else
1149
+ @logger.log(Logger::DEBUG,
1150
+ "Variable value is not defined. Returning the default variable value '#{variable_value}' for variable '#{variable['key']}'.")
1151
+
1152
+ end
1153
+ else
1154
+ @logger.log(Logger::DEBUG,
1155
+ "Feature '#{feature_flag_key}' is not enabled for user '#{user_id}'. Returning the default variable value '#{variable_value}'.")
1156
+ end
1157
+ else
1158
+ @logger.log(Logger::INFO,
1159
+ "User '#{user_id}' was not bucketed into experiment or rollout for feature flag '#{feature_flag_key}'. Returning the default variable value '#{variable_value}'.")
1160
+ end
1161
+ variable_value
1162
+ end
1163
+
1164
+ def user_inputs_valid?(attributes = nil, event_tags = nil)
1165
+ # Helper method to validate user inputs.
1166
+ #
1167
+ # attributes - Dict representing user attributes.
1168
+ # event_tags - Dict representing metadata associated with an event.
1169
+ #
1170
+ # Returns boolean True if inputs are valid. False otherwise.
1171
+
1172
+ return false if !attributes.nil? && !attributes_valid?(attributes)
1173
+
1174
+ return false if !event_tags.nil? && !event_tags_valid?(event_tags)
1175
+
1176
+ true
1177
+ end
1178
+
1179
+ def attributes_valid?(attributes)
1180
+ unless Helpers::Validator.attributes_valid?(attributes)
1181
+ @logger.log(Logger::ERROR, 'Provided attributes are in an invalid format.')
1182
+ @error_handler.handle_error(InvalidAttributeFormatError)
1183
+ return false
1184
+ end
1185
+ true
1186
+ end
1187
+
1188
+ def event_tags_valid?(event_tags)
1189
+ unless Helpers::Validator.event_tags_valid?(event_tags)
1190
+ @logger.log(Logger::ERROR, 'Provided event tags are in an invalid format.')
1191
+ @error_handler.handle_error(InvalidEventTagFormatError)
1192
+ return false
1193
+ end
1194
+ true
1195
+ end
1196
+
1197
+ def validate_instantiation_options
1198
+ raise InvalidInputError, 'logger' unless Helpers::Validator.logger_valid?(@logger)
1199
+
1200
+ unless Helpers::Validator.error_handler_valid?(@error_handler)
1201
+ @error_handler = NoOpErrorHandler.new
1202
+ raise InvalidInputError, 'error_handler'
1203
+ end
1204
+
1205
+ return if Helpers::Validator.event_dispatcher_valid?(@event_dispatcher)
1206
+
1207
+ @event_dispatcher = EventDispatcher.new(logger: @logger, error_handler: @error_handler)
1208
+ raise InvalidInputError, 'event_dispatcher'
1209
+ end
1210
+
1211
+ def send_impression(config, experiment, variation_key, flag_key, rule_key, enabled, rule_type, user_id, attributes = nil)
1212
+ if experiment.nil?
1213
+ experiment = {
1214
+ 'id' => '',
1215
+ 'key' => '',
1216
+ 'layerId' => '',
1217
+ 'status' => '',
1218
+ 'variations' => [],
1219
+ 'trafficAllocation' => [],
1220
+ 'audienceIds' => [],
1221
+ 'audienceConditions' => [],
1222
+ 'forcedVariations' => {}
1223
+ }
1224
+ end
1225
+
1226
+ experiment_id = experiment['id']
1227
+ experiment_key = experiment['key']
1228
+
1229
+ variation_id = config.get_variation_id_from_key_by_experiment_id(experiment_id, variation_key) unless experiment_id.empty?
1230
+
1231
+ unless variation_id
1232
+ variation = !flag_key.empty? ? get_flag_variation(flag_key, variation_key, 'key') : nil
1233
+ variation_id = variation ? variation['id'] : ''
1234
+ end
1235
+
1236
+ metadata = {
1237
+ flag_key: flag_key,
1238
+ rule_key: rule_key,
1239
+ rule_type: rule_type,
1240
+ variation_key: variation_key,
1241
+ enabled: enabled
1242
+ }
1243
+
1244
+ user_event = UserEventFactory.create_impression_event(config, experiment, variation_id, metadata, user_id, attributes)
1245
+ @event_processor.process(user_event)
1246
+ return unless @notification_center.notification_count(NotificationCenter::NOTIFICATION_TYPES[:ACTIVATE]).positive?
1247
+
1248
+ @logger.log(Logger::INFO, "Activating user '#{user_id}' in experiment '#{experiment_key}'.")
1249
+
1250
+ experiment = nil if experiment_id == ''
1251
+ variation = nil
1252
+ variation = config.get_variation_from_id_by_experiment_id(experiment_id, variation_id) unless experiment.nil?
1253
+ log_event = EventFactory.create_log_event(user_event, @logger)
1254
+ @notification_center.send_notifications(
1255
+ NotificationCenter::NOTIFICATION_TYPES[:ACTIVATE],
1256
+ experiment, user_id, attributes, variation, log_event
1257
+ )
1258
+ end
1259
+
1260
+ def project_config
1261
+ @config_manager.config
1262
+ end
1263
+
1264
+ def update_odp_config_on_datafile_update
1265
+ # if datafile isn't ready, expects to be called again by the internal notification_center
1266
+ return if @config_manager.respond_to?(:ready?) && !@config_manager.ready?
1267
+
1268
+ config = @config_manager&.config
1269
+ return unless config
1270
+
1271
+ @odp_manager.update_odp_config(config.public_key_for_odp, config.host_for_odp, config.all_segments)
1272
+ end
1273
+
1274
+ def setup_odp!(sdk_key)
1275
+ unless @sdk_settings.is_a? Optimizely::Helpers::OptimizelySdkSettings
1276
+ @logger.log(Logger::DEBUG, 'Provided sdk_settings is not an OptimizelySdkSettings instance.') unless @sdk_settings.nil?
1277
+ @sdk_settings = Optimizely::Helpers::OptimizelySdkSettings.new
1278
+ end
1279
+
1280
+ if !@sdk_settings.odp_segment_manager.nil? && !Helpers::Validator.segment_manager_valid?(@sdk_settings.odp_segment_manager)
1281
+ @logger.log(Logger::ERROR, 'Invalid ODP segment manager, reverting to default.')
1282
+ @sdk_settings.odp_segment_manager = nil
1283
+ end
1284
+
1285
+ if !@sdk_settings.odp_event_manager.nil? && !Helpers::Validator.event_manager_valid?(@sdk_settings.odp_event_manager)
1286
+ @logger.log(Logger::ERROR, 'Invalid ODP event manager, reverting to default.')
1287
+ @sdk_settings.odp_event_manager = nil
1288
+ end
1289
+
1290
+ if !@sdk_settings.odp_segments_cache.nil? && !Helpers::Validator.segments_cache_valid?(@sdk_settings.odp_segments_cache)
1291
+ @logger.log(Logger::ERROR, 'Invalid ODP segments cache, reverting to default.')
1292
+ @sdk_settings.odp_segments_cache = nil
1293
+ end
1294
+
1295
+ # no need to instantiate a cache if a custom cache or segment manager is provided.
1296
+ if !@sdk_settings.odp_disabled && @sdk_settings.odp_segment_manager.nil?
1297
+ @sdk_settings.odp_segments_cache ||= LRUCache.new(
1298
+ @sdk_settings.segments_cache_size,
1299
+ @sdk_settings.segments_cache_timeout_in_secs
1300
+ )
1301
+ end
1302
+
1303
+ @odp_manager = OdpManager.new(
1304
+ disable: @sdk_settings.odp_disabled,
1305
+ segment_manager: @sdk_settings.odp_segment_manager,
1306
+ event_manager: @sdk_settings.odp_event_manager,
1307
+ segments_cache: @sdk_settings.odp_segments_cache,
1308
+ fetch_segments_timeout: @sdk_settings.fetch_segments_timeout,
1309
+ odp_event_timeout: @sdk_settings.odp_event_timeout,
1310
+ odp_flush_interval: @sdk_settings.odp_flush_interval,
1311
+ logger: @logger
1312
+ )
1313
+
1314
+ return if @sdk_settings.odp_disabled
1315
+
1316
+ Optimizely::NotificationCenterRegistry
1317
+ .get_notification_center(sdk_key, @logger)
1318
+ &.add_notification_listener(
1319
+ NotificationCenter::NOTIFICATION_TYPES[:OPTIMIZELY_CONFIG_UPDATE],
1320
+ method(:update_odp_config_on_datafile_update)
1321
+ )
1322
+
1323
+ update_odp_config_on_datafile_update
1324
+ end
1325
+ end
1326
+ end