optimizely-sdk 5.0.0 → 5.1.0

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