optimizely-sdk 3.10.1 → 4.0.0

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