optimizely-sdk 3.9.0 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +202 -202
  3. data/lib/optimizely/audience.rb +127 -97
  4. data/lib/optimizely/bucketer.rb +156 -156
  5. data/lib/optimizely/condition_tree_evaluator.rb +123 -123
  6. data/lib/optimizely/config/datafile_project_config.rb +539 -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