optimizely-sdk 5.0.0 → 5.0.1

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