optimizely-sdk 5.0.0 → 5.0.1

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