optimizely-sdk 3.10.1 → 4.0.0

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