optimizely-sdk 3.9.0 → 4.0.0

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