optimizely-sdk 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: '05059bf31a28b9a72a3f4ecb95808bf46671438b'
4
- data.tar.gz: 481be969d392df0eb93bf8157f13d7625912d613
3
+ metadata.gz: 3fae777d6ce539eb2e0809dbf38d4784130c8ade
4
+ data.tar.gz: f65bee6796830490f4ee8f1a86da20ed7b4f01fb
5
5
  SHA512:
6
- metadata.gz: 5f5099fc8956cdeec7add609b9c13ec2cb1f7e0524889c626001afb9c24ec456f642bcf2307da4114c0747b1714faa70861820d3e0ed9fcb901f164f5400034b
7
- data.tar.gz: f327bb0d528621dbcddea84d7a0495e1ceec473a616d5ce3afb5a26386794a45452cf1ce83deca7f0d2f0bab0ed2af6dfa3e08a6ec5313558169c25c78a6772a
6
+ metadata.gz: b8e6f2732d692b1844d3f0c990010aba9858c93b82244803962810d33c78b945f925936874097272705a1a7e724f9d3abad1c4c66ba358573ba32c8bd10d8515
7
+ data.tar.gz: a8804feff56093f399f87425869a733e9b91b74f502b075a1d790364f47da5e524c36ecde23a626d2b8c977c344e5bd2fb5c93e742ceb5a2cfcf4a50710b9444
data/lib/optimizely.rb CHANGED
@@ -19,9 +19,12 @@ require_relative 'optimizely/error_handler'
19
19
  require_relative 'optimizely/event_builder'
20
20
  require_relative 'optimizely/event_dispatcher'
21
21
  require_relative 'optimizely/exceptions'
22
+ require_relative 'optimizely/helpers/constants'
22
23
  require_relative 'optimizely/helpers/group'
23
24
  require_relative 'optimizely/helpers/validator'
25
+ require_relative 'optimizely/helpers/variable_type'
24
26
  require_relative 'optimizely/logger'
27
+ require_relative 'optimizely/notification_center'
25
28
  require_relative 'optimizely/project_config'
26
29
 
27
30
  module Optimizely
@@ -36,6 +39,7 @@ module Optimizely
36
39
  attr_reader :event_builder
37
40
  attr_reader :event_dispatcher
38
41
  attr_reader :logger
42
+ attr_reader :notification_center
39
43
 
40
44
  def initialize(datafile, event_dispatcher = nil, logger = nil, error_handler = nil, skip_json_validation = false, user_profile_service = nil)
41
45
  # Constructor for Projects.
@@ -81,6 +85,7 @@ module Optimizely
81
85
 
82
86
  @decision_service = DecisionService.new(@config, @user_profile_service)
83
87
  @event_builder = EventBuilder.new(@config)
88
+ @notification_center = NotificationCenter.new(@logger, @error_handler)
84
89
  end
85
90
 
86
91
  def activate(experiment_key, user_id, attributes = nil)
@@ -107,17 +112,8 @@ module Optimizely
107
112
  end
108
113
 
109
114
  # Create and dispatch impression event
110
- variation_id = @config.get_variation_id_from_key(experiment_key, variation_key)
111
115
  experiment = @config.get_experiment_from_key(experiment_key)
112
- impression_event = @event_builder.create_impression_event(experiment, variation_id, user_id, attributes)
113
- @logger.log(Logger::INFO,
114
- 'Dispatching impression event to URL %s with params %s.' % [impression_event.url,
115
- impression_event.params])
116
- begin
117
- @event_dispatcher.dispatch_event(impression_event)
118
- rescue => e
119
- @logger.log(Logger::ERROR, "Unable to dispatch impression event. Error: #{e}")
120
- end
116
+ send_impression(experiment, variation_key, user_id, attributes)
121
117
 
122
118
  variation_key
123
119
  end
@@ -151,7 +147,10 @@ module Optimizely
151
147
  variation_id = @decision_service.get_variation(experiment_key, user_id, attributes)
152
148
 
153
149
  unless variation_id.nil?
154
- return @config.get_variation_key_from_id(experiment_key, variation_id)
150
+ variation = @config.get_variation_from_id(experiment_key, variation_id)
151
+ if variation
152
+ return variation['key']
153
+ end
155
154
  end
156
155
  nil
157
156
  end
@@ -235,10 +234,204 @@ module Optimizely
235
234
  rescue => e
236
235
  @logger.log(Logger::ERROR, "Unable to dispatch conversion event. Error: #{e}")
237
236
  end
237
+ @notification_center.send_notifications(
238
+ NotificationCenter::NOTIFICATION_TYPES[:TRACK],
239
+ event_key, user_id, attributes, event_tags, conversion_event
240
+ )
241
+ end
242
+
243
+ def is_feature_enabled(feature_flag_key, user_id, attributes = nil)
244
+ # Determine whether a feature is enabled.
245
+ # Sends an impression event if the user is bucketed into an experiment using the feature.
246
+ #
247
+ # feature_flag_key - String unique key of the feature.
248
+ # userId - String ID of the user.
249
+ # attributes - Hash representing visitor attributes and values which need to be recorded.
250
+ #
251
+ # Returns True if the feature is enabled.
252
+ # False if the feature is disabled.
253
+ # False if the feature is not found.
254
+
255
+ unless @is_valid
256
+ logger = SimpleLogger.new
257
+ logger.log(Logger::ERROR, InvalidDatafileError.new('is_feature_enabled').message)
258
+ return nil
259
+ end
260
+
261
+ feature_flag = @config.get_feature_flag_from_key(feature_flag_key)
262
+ unless feature_flag
263
+ @logger.log(Logger::ERROR, "No feature flag was found for key '#{feature_flag_key}'.")
264
+ return false
265
+ end
266
+
267
+ decision = @decision_service.get_variation_for_feature(feature_flag, user_id, attributes)
268
+ unless decision.nil?
269
+ variation = decision['variation']
270
+ experiment = decision['experiment']
271
+ unless experiment.nil?
272
+ send_impression(experiment, variation['key'], user_id, attributes)
273
+ else
274
+ @logger.log(Logger::DEBUG,
275
+ "The user '#{user_id}' is not being experimented on in feature '#{feature_flag_key}'.")
276
+ end
277
+
278
+ @logger.log(Logger::INFO, "Feature '#{feature_flag_key}' is enabled for user '#{user_id}'.")
279
+ return true
280
+ end
281
+
282
+ @logger.log(Logger::INFO,
283
+ "Feature '#{feature_flag_key}' is not enabled for user '#{user_id}'.")
284
+ false
285
+ end
286
+
287
+ def get_feature_variable_string(feature_flag_key, variable_key, user_id, attributes = nil)
288
+ # Get the String value of the specified variable in the feature flag.
289
+ #
290
+ # feature_flag_key - String key of feature flag the variable belongs to
291
+ # variable_key - String key of variable for which we are getting the string value
292
+ # user_id - String user ID
293
+ # attributes - Hash representing visitor attributes and values which need to be recorded.
294
+ #
295
+ # Returns the string variable value.
296
+ # Returns nil if the feature flag or variable are not found.
297
+
298
+ variable_value = get_feature_variable_for_type(
299
+ feature_flag_key,
300
+ variable_key,
301
+ Optimizely::Helpers::Constants::VARIABLE_TYPES["STRING"],
302
+ user_id,
303
+ attributes
304
+ )
305
+
306
+ return variable_value
307
+ end
308
+
309
+ def get_feature_variable_boolean(feature_flag_key, variable_key, user_id, attributes = nil)
310
+ # Get the Boolean value of the specified variable in the feature flag.
311
+ #
312
+ # feature_flag_key - String key of feature flag the variable belongs to
313
+ # variable_key - String key of variable for which we are getting the string value
314
+ # user_id - String user ID
315
+ # attributes - Hash representing visitor attributes and values which need to be recorded.
316
+ #
317
+ # Returns the boolean variable value.
318
+ # Returns nil if the feature flag or variable are not found.
319
+
320
+ variable_value = get_feature_variable_for_type(
321
+ feature_flag_key,
322
+ variable_key,
323
+ Optimizely::Helpers::Constants::VARIABLE_TYPES["BOOLEAN"],
324
+ user_id,
325
+ attributes
326
+ )
327
+
328
+ return variable_value
329
+ end
330
+
331
+ def get_feature_variable_double(feature_flag_key, variable_key, user_id, attributes = nil)
332
+ # Get the Double value of the specified variable in the feature flag.
333
+ #
334
+ # feature_flag_key - String key of feature flag the variable belongs to
335
+ # variable_key - String key of variable for which we are getting the string value
336
+ # user_id - String user ID
337
+ # attributes - Hash representing visitor attributes and values which need to be recorded.
338
+ #
339
+ # Returns the double variable value.
340
+ # Returns nil if the feature flag or variable are not found.
341
+
342
+ variable_value = get_feature_variable_for_type(
343
+ feature_flag_key,
344
+ variable_key,
345
+ Optimizely::Helpers::Constants::VARIABLE_TYPES["DOUBLE"],
346
+ user_id,
347
+ attributes
348
+ )
349
+
350
+ return variable_value
351
+ end
352
+
353
+ def get_feature_variable_integer(feature_flag_key, variable_key, user_id, attributes = nil)
354
+ # Get the Integer value of the specified variable in the feature flag.
355
+ #
356
+ # feature_flag_key - String key of feature flag the variable belongs to
357
+ # variable_key - String key of variable for which we are getting the string value
358
+ # user_id - String user ID
359
+ # attributes - Hash representing visitor attributes and values which need to be recorded.
360
+ #
361
+ # Returns the integer variable value.
362
+ # Returns nil if the feature flag or variable are not found.
363
+
364
+ variable_value = get_feature_variable_for_type(
365
+ feature_flag_key,
366
+ variable_key,
367
+ Optimizely::Helpers::Constants::VARIABLE_TYPES["INTEGER"],
368
+ user_id,
369
+ attributes
370
+ )
371
+
372
+ return variable_value
238
373
  end
239
374
 
240
375
  private
241
376
 
377
+ def get_feature_variable_for_type(feature_flag_key, variable_key, variable_type, user_id, attributes = nil)
378
+ # Get the variable value for the given feature variable and cast it to the specified type
379
+ # The default value is returned if the feature flag is not enabled for the user.
380
+ #
381
+ # feature_flag_key - String key of feature flag the variable belongs to
382
+ # variable_key - String key of variable for which we are getting the string value
383
+ # variable_type - String requested type for feature variable
384
+ # user_id - String user ID
385
+ # attributes - Hash representing visitor attributes and values which need to be recorded.
386
+ #
387
+ # Returns the type-casted variable value.
388
+ # Returns nil if the feature flag or variable are not found.
389
+
390
+ feature_flag = @config.get_feature_flag_from_key(feature_flag_key)
391
+ unless feature_flag
392
+ @logger.log(Logger::INFO, "No feature flag was found for key '#{feature_flag_key}'.")
393
+ return nil
394
+ end
395
+
396
+ variable_value = nil
397
+ variable = @config.get_feature_variable(feature_flag, variable_key)
398
+ unless variable.nil?
399
+ variable_value = variable['defaultValue']
400
+
401
+ decision = @decision_service.get_variation_for_feature(feature_flag, user_id, attributes)
402
+ unless decision
403
+ @logger.log(Logger::INFO,
404
+ "User '#{user_id}' was not bucketed into any variation for feature flag '#{feature_flag_key}'. Returning the default variable value '#{variable_value}'.")
405
+ else
406
+ variation = decision['variation']
407
+ variation_variable_usages = @config.variation_id_to_variable_usage_map[variation['id']]
408
+ variable_id = variable['id']
409
+ unless variation_variable_usages and variation_variable_usages.key?(variable_id)
410
+ variation_key = variation['key']
411
+ @logger.log(Logger::DEBUG,
412
+ "Variable '#{variable_key}' is not used in variation '#{variation_key}'. Returning the default variable value '#{variable_value}'."
413
+ )
414
+ else
415
+ variable_value = variation_variable_usages[variable_id]['value']
416
+ @logger.log(Logger::INFO,
417
+ "Got variable value '#{variable_value}' for variable '#{variable_key}' of feature flag '#{feature_flag_key}'.")
418
+ end
419
+ end
420
+ end
421
+
422
+ unless variable_value.nil?
423
+ actual_variable_type = variable['type']
424
+ unless variable_type == actual_variable_type
425
+ @logger.log(Logger::WARN,
426
+ "Requested variable type '#{variable_type}' but variable '#{variable_key}' is of type '#{actual_variable_type}'.")
427
+ end
428
+
429
+ variable_value = Helpers::VariableType.cast_value_to_type(variable_value, variable_type, @logger)
430
+ end
431
+
432
+ return variable_value
433
+ end
434
+
242
435
  def get_valid_experiments_for_event(event_key, user_id, attributes)
243
436
  # Get the experiments that we should be tracking for the given event.
244
437
  #
@@ -313,5 +506,24 @@ module Optimizely
313
506
  raise InvalidInputError.new('error_handler') unless Helpers::Validator.error_handler_valid?(@error_handler)
314
507
  raise InvalidInputError.new('event_dispatcher') unless Helpers::Validator.event_dispatcher_valid?(@event_dispatcher)
315
508
  end
509
+
510
+ def send_impression(experiment, variation_key, user_id, attributes = nil)
511
+ experiment_key = experiment['key']
512
+ variation_id = @config.get_variation_id_from_key(experiment_key, variation_key)
513
+ impression_event = @event_builder.create_impression_event(experiment, variation_id, user_id, attributes)
514
+ @logger.log(Logger::INFO,
515
+ 'Dispatching impression event to URL %s with params %s.' % [impression_event.url,
516
+ impression_event.params])
517
+ begin
518
+ @event_dispatcher.dispatch_event(impression_event)
519
+ rescue => e
520
+ @logger.log(Logger::ERROR, "Unable to dispatch impression event. Error: #{e}")
521
+ end
522
+ variation = @config.get_variation_from_id(experiment_key, variation_id)
523
+ @notification_center.send_notifications(
524
+ NotificationCenter::NOTIFICATION_TYPES[:ACTIVATE],
525
+ experiment,user_id, attributes, variation, impression_event
526
+ )
527
+ end
316
528
  end
317
529
  end
@@ -20,7 +20,7 @@ module Optimizely
20
20
  class Bucketer
21
21
  # Optimizely bucketing algorithm that evenly distributes visitors.
22
22
 
23
- BUCKETING_ID_TEMPLATE = '%{user_id}%{entity_id}'
23
+ BUCKETING_ID_TEMPLATE = '%{bucketing_id}%{entity_id}'
24
24
  HASH_SEED = 1
25
25
  MAX_HASH_VALUE = 2**32
26
26
  MAX_TRAFFIC_VALUE = 10_000
@@ -35,13 +35,15 @@ module Optimizely
35
35
  @config = config
36
36
  end
37
37
 
38
- def bucket(experiment, user_id)
38
+ def bucket(experiment, bucketing_id, user_id)
39
39
  # Determines ID of variation to be shown for a given experiment key and user ID.
40
40
  #
41
41
  # experiment - Experiment for which visitor is to be bucketed.
42
+ # bucketing_id - String A customer-assigned value used to generate the bucketing key
42
43
  # user_id - String ID for user.
43
44
  #
44
45
  # Returns variation in which visitor with ID user_id has been placed. Nil if no variation.
46
+ return nil if experiment.nil?
45
47
 
46
48
  # check if experiment is in a group; if so, check if user is bucketed into specified experiment
47
49
  experiment_id = experiment['id']
@@ -51,7 +53,7 @@ module Optimizely
51
53
  group = @config.group_key_map.fetch(group_id)
52
54
  if Helpers::Group.random_policy?(group)
53
55
  traffic_allocations = group.fetch('trafficAllocation')
54
- bucketed_experiment_id = find_bucket(user_id, group_id, traffic_allocations)
56
+ bucketed_experiment_id = find_bucket(bucketing_id, user_id, group_id, traffic_allocations)
55
57
  # return if the user is not bucketed into any experiment
56
58
  unless bucketed_experiment_id
57
59
  @config.logger.log(Logger::INFO, "User '#{user_id}' is in no experiment.")
@@ -76,7 +78,7 @@ module Optimizely
76
78
  end
77
79
 
78
80
  traffic_allocations = experiment['trafficAllocation']
79
- variation_id = find_bucket(user_id, experiment_id, traffic_allocations)
81
+ variation_id = find_bucket(bucketing_id, user_id, experiment_id, traffic_allocations)
80
82
  if variation_id && variation_id != ''
81
83
  variation = @config.get_variation_from_id(experiment_key, variation_id)
82
84
  variation_key = variation ? variation['key'] : nil
@@ -96,18 +98,18 @@ module Optimizely
96
98
  nil
97
99
  end
98
100
 
99
- def find_bucket(user_id, parent_id, traffic_allocations)
101
+ def find_bucket(bucketing_id, user_id, parent_id, traffic_allocations)
100
102
  # Helper function to find the matching entity ID for a given bucketing value in a list of traffic allocations.
101
103
  #
104
+ # bucketing_id - String A customer-assigned value user to generate bucketing key
102
105
  # user_id - String ID for user
103
106
  # parent_id - String entity ID to use for bucketing ID
104
107
  # traffic_allocations - Array of traffic allocations
105
108
  #
106
109
  # Returns entity ID corresponding to the provided bucket value or nil if no match is found.
107
-
108
- bucketing_id = sprintf(BUCKETING_ID_TEMPLATE, user_id: user_id, entity_id: parent_id)
109
- bucket_value = generate_bucket_value(bucketing_id)
110
- @config.logger.log(Logger::DEBUG, "Assigned bucket #{bucket_value} to user '#{user_id}'.")
110
+ bucketing_key = sprintf(BUCKETING_ID_TEMPLATE, bucketing_id: bucketing_id, entity_id: parent_id)
111
+ bucket_value = generate_bucket_value(bucketing_key)
112
+ @config.logger.log(Logger::DEBUG, "Assigned bucket #{bucket_value} to user '#{user_id}' with bucketing ID: '#{bucketing_id}'.")
111
113
 
112
114
  traffic_allocations.each do |traffic_allocation|
113
115
  current_end_of_range = traffic_allocation['endOfRange']
@@ -122,25 +124,25 @@ module Optimizely
122
124
 
123
125
  private
124
126
 
125
- def generate_bucket_value(bucketing_id)
127
+ def generate_bucket_value(bucketing_key)
126
128
  # Helper function to generate bucket value in half-closed interval [0, MAX_TRAFFIC_VALUE).
127
129
  #
128
- # bucketing_id - String ID for bucketing.
130
+ # bucketing_key - String - Value used to generate bucket value
129
131
  #
130
- # Returns bucket value corresponding to the provided bucketing ID.
132
+ # Returns bucket value corresponding to the provided bucketing key.
131
133
 
132
- ratio = (generate_unsigned_hash_code_32_bit(bucketing_id)).to_f / MAX_HASH_VALUE
134
+ ratio = (generate_unsigned_hash_code_32_bit(bucketing_key)).to_f / MAX_HASH_VALUE
133
135
  (ratio * MAX_TRAFFIC_VALUE).to_i
134
136
  end
135
137
 
136
- def generate_unsigned_hash_code_32_bit(bucketing_id)
138
+ def generate_unsigned_hash_code_32_bit(bucketing_key)
137
139
  # Helper function to retreive hash code
138
140
  #
139
- # bucketing_id - String ID for bucketing.
141
+ # bucketing_key - String - Value used for the key of the murmur hash
140
142
  #
141
143
  # Returns hash code which is a 32 bit unsigned integer.
142
144
 
143
- MurmurHash3::V32.str_hash(bucketing_id, @bucket_seed) & UNSIGNED_MAX_32_BIT_VALUE
145
+ MurmurHash3::V32.str_hash(bucketing_key, @bucket_seed) & UNSIGNED_MAX_32_BIT_VALUE
144
146
  end
145
147
  end
146
- end
148
+ end
@@ -16,6 +16,9 @@
16
16
  require_relative './bucketer'
17
17
 
18
18
  module Optimizely
19
+
20
+ RESERVED_ATTRIBUTE_KEY_BUCKETING_ID = "\$opt_bucketing_id".freeze
21
+
19
22
  class DecisionService
20
23
  # Optimizely's decision service that determines into which variation of an experiment a user will be allocated.
21
24
  #
@@ -28,16 +31,16 @@ module Optimizely
28
31
  # 4. Check user profile service for past bucketing decisions (sticky bucketing)
29
32
  # 5. Check audience targeting
30
33
  # 6. Use Murmurhash3 to bucket the user
31
-
34
+
32
35
  attr_reader :bucketer
33
36
  attr_reader :config
34
-
37
+
35
38
  def initialize(config, user_profile_service = nil)
36
39
  @config = config
37
40
  @user_profile_service = user_profile_service
38
41
  @bucketer = Bucketer.new(@config)
39
42
  end
40
-
43
+
41
44
  def get_variation(experiment_key, user_id, attributes = nil)
42
45
  # Determines variation into which user will be bucketed.
43
46
  #
@@ -46,58 +49,250 @@ module Optimizely
46
49
  # attributes - Hash representing user attributes
47
50
  #
48
51
  # Returns variation ID where visitor will be bucketed (nil if experiment is inactive or user does not meet audience conditions)
49
-
52
+
53
+ # By default, the bucketing ID should be the user ID
54
+ bucketing_id = user_id;
55
+
56
+ # If the bucketing ID key is defined in attributes, then use that in place of the userID
57
+ if attributes and attributes[RESERVED_ATTRIBUTE_KEY_BUCKETING_ID].is_a? String
58
+ unless attributes[RESERVED_ATTRIBUTE_KEY_BUCKETING_ID].empty?
59
+ bucketing_id = attributes[RESERVED_ATTRIBUTE_KEY_BUCKETING_ID]
60
+ @config.logger.log(Logger::DEBUG, "Setting the bucketing ID '#{bucketing_id}'")
61
+ end
62
+ end
63
+
50
64
  # Check to make sure experiment is active
51
65
  experiment = @config.get_experiment_from_key(experiment_key)
52
- if experiment.nil?
53
- return nil
54
- end
55
-
66
+ return nil if experiment.nil?
67
+
56
68
  experiment_id = experiment['id']
57
69
  unless @config.experiment_running?(experiment)
58
70
  @config.logger.log(Logger::INFO, "Experiment '#{experiment_key}' is not running.")
59
71
  return nil
60
72
  end
61
-
73
+
62
74
  # Check if a forced variation is set for the user
63
75
  forced_variation = @config.get_forced_variation(experiment_key, user_id)
64
76
  return forced_variation['id'] if forced_variation
65
-
77
+
66
78
  # Check if user is in a white-listed variation
67
79
  whitelisted_variation_id = get_whitelisted_variation_id(experiment_key, user_id)
68
80
  return whitelisted_variation_id if whitelisted_variation_id
69
-
81
+
70
82
  # Check for saved bucketing decisions
71
83
  user_profile = get_user_profile(user_id)
72
84
  saved_variation_id = get_saved_variation_id(experiment_id, user_profile)
73
85
  if saved_variation_id
74
86
  @config.logger.log(
75
- Logger::INFO,
76
- "Returning previously activated variation ID #{saved_variation_id} of experiment '#{experiment_key}' for user '#{user_id}' from user profile."
87
+ Logger::INFO,
88
+ "Returning previously activated variation ID #{saved_variation_id} of experiment '#{experiment_key}' for user '#{user_id}' from user profile."
77
89
  )
78
90
  return saved_variation_id
79
91
  end
80
-
92
+
81
93
  # Check audience conditions
82
94
  unless Audience.user_in_experiment?(@config, experiment, attributes)
83
95
  @config.logger.log(
84
- Logger::INFO,
85
- "User '#{user_id}' does not meet the conditions to be in experiment '#{experiment_key}'."
96
+ Logger::INFO,
97
+ "User '#{user_id}' does not meet the conditions to be in experiment '#{experiment_key}'."
86
98
  )
87
99
  return nil
88
100
  end
89
-
101
+
90
102
  # Bucket normally
91
- variation = @bucketer.bucket(experiment, user_id)
103
+ variation = @bucketer.bucket(experiment, bucketing_id, user_id)
92
104
  variation_id = variation ? variation['id'] : nil
93
-
105
+
94
106
  # Persist bucketing decision
95
107
  save_user_profile(user_profile, experiment_id, variation_id)
96
108
  variation_id
97
109
  end
98
-
110
+
111
+ def get_variation_for_feature(feature_flag, user_id, attributes = nil)
112
+ # Get the variation the user is bucketed into for the given FeatureFlag.
113
+ #
114
+ # feature_flag - The feature flag the user wants to access
115
+ # user_id - String ID for the user
116
+ # attributes - Hash representing user attributes
117
+ #
118
+ # Returns hash with the experiment and variation where visitor will be bucketed (nil if the user is not bucketed into any of the experiments on the feature)
119
+
120
+ # check if the feature is being experiment on and whether the user is bucketed into the experiment
121
+ decision = get_variation_for_feature_experiment(feature_flag, user_id, attributes)
122
+ return decision unless decision.nil?
123
+
124
+ feature_flag_key = feature_flag['key']
125
+ variation = get_variation_for_feature_rollout(feature_flag, user_id, attributes)
126
+ if variation
127
+ @config.logger.log(
128
+ Logger::INFO,
129
+ "User '#{user_id}' is in the rollout for feature flag '#{feature_flag_key}'."
130
+ )
131
+ # return decision with nil experiment so we don't track impressions for it
132
+ return {
133
+ 'experiment' => nil,
134
+ 'variation' => variation
135
+ }
136
+ else
137
+ @config.logger.log(
138
+ Logger::INFO,
139
+ "User '#{user_id}' is not in the rollout for feature flag '#{feature_flag_key}'."
140
+ )
141
+ end
142
+
143
+ return nil
144
+ end
145
+
146
+ def get_variation_for_feature_experiment(feature_flag, user_id, attributes = nil)
147
+ # Gets the variation the user is bucketed into for the feature flag's experiment.
148
+ #
149
+ # feature_flag - The feature flag the user wants to access
150
+ # user_id - String ID for the user
151
+ # attributes - Hash representing user attributes
152
+ #
153
+ # Returns a hash with the experiment and variation where visitor will be bucketed
154
+ # or nil if the user is not bucketed into any of the experiments on the feature
155
+
156
+ feature_flag_key = feature_flag['key']
157
+ unless feature_flag['experimentIds'].empty?
158
+ # check if experiment is part of mutex group
159
+ experiment_id = feature_flag['experimentIds'][0]
160
+ experiment = @config.experiment_id_map[experiment_id]
161
+ unless experiment
162
+ @config.logger.log(
163
+ Logger::DEBUG,
164
+ "Feature flag experiment with ID '#{experiment_id}' is not in the datafile."
165
+ )
166
+ return nil
167
+ end
168
+
169
+ group_id = experiment['groupId']
170
+ # if experiment is part of mutex group we first determine which experiment (if any) in the group the user is part of
171
+ if group_id and @config.group_key_map.has_key?(group_id)
172
+ group = @config.group_key_map[group_id]
173
+ bucketed_experiment_id = @bucketer.find_bucket(user_id, group_id, group['trafficAllocation'])
174
+ if bucketed_experiment_id.nil?
175
+ @config.logger.log(
176
+ Logger::INFO,
177
+ "The user '#{user_id}' is not bucketed into any of the experiments on the feature '#{feature_flag_key}'."
178
+ )
179
+ return nil
180
+ end
181
+ else
182
+ bucketed_experiment_id = experiment_id
183
+ end
184
+
185
+ if feature_flag['experimentIds'].include?(bucketed_experiment_id)
186
+ experiment = @config.experiment_id_map[bucketed_experiment_id]
187
+ experiment_key = experiment['key']
188
+ variation_id = get_variation(experiment_key, user_id, attributes)
189
+ unless variation_id.nil?
190
+ variation = @config.variation_id_map[experiment_key][variation_id]
191
+ @config.logger.log(
192
+ Logger::INFO,
193
+ "The user '#{user_id}' is bucketed into experiment '#{experiment_key}' of feature '#{feature_flag_key}'."
194
+ )
195
+ return {
196
+ 'variation' => variation,
197
+ 'experiment' => experiment
198
+ }
199
+ else
200
+ @config.logger.log(
201
+ Logger::INFO,
202
+ "The user '#{user_id}' is not bucketed into any of the experiments on the feature '#{feature_flag_key}'."
203
+ )
204
+ end
205
+ end
206
+ else
207
+ @config.logger.log(
208
+ Logger::DEBUG,
209
+ "The feature flag '#{feature_flag_key}' is not used in any experiments."
210
+ )
211
+ end
212
+
213
+ return nil
214
+ end
215
+
216
+ def get_variation_for_feature_rollout(feature_flag, user_id, attributes = nil)
217
+ # Determine which variation the user is in for a given rollout.
218
+ # Returns the variation of the first experiment the user qualifies for.
219
+ #
220
+ # feature_flag - The feature flag the user wants to access
221
+ # user_id - String ID for the user
222
+ # attributes - Hash representing user attributes
223
+ #
224
+ # Returns the variation the user is bucketed into or nil if not bucketed into any of the targeting rules
225
+
226
+ rollout_id = feature_flag['rolloutId']
227
+ if rollout_id.nil? or rollout_id.empty?
228
+ feature_flag_key = feature_flag['key']
229
+ @config.logger.log(
230
+ Logger::DEBUG,
231
+ "Feature flag '#{feature_flag_key}' is not part of a rollout."
232
+ )
233
+ return nil
234
+ end
235
+
236
+ rollout = @config.get_rollout_from_id(rollout_id)
237
+ unless rollout.nil? or rollout['experiments'].empty?
238
+ rollout_experiments = rollout['experiments']
239
+ number_of_rules = rollout_experiments.length - 1
240
+
241
+ # Go through each experiment in order and try to get the variation for the user
242
+ for index in (0...number_of_rules)
243
+ experiment = rollout_experiments[index]
244
+ experiment_key = experiment['key']
245
+
246
+ # Check that user meets audience conditions for targeting rule
247
+ unless Audience.user_in_experiment?(@config, experiment, attributes)
248
+ @config.logger.log(
249
+ Logger::DEBUG,
250
+ "User '#{user_id}' does not meet the conditions to be in experiment '#{experiment_key}'."
251
+ )
252
+ # move onto the next targeting rule
253
+ next
254
+ end
255
+
256
+ @config.logger.log(
257
+ Logger::DEBUG,
258
+ "User '#{user_id}' meets conditions for targeting rule '#{index + 1}'."
259
+ )
260
+ variation = @bucketer.bucket(experiment, user_id)
261
+ unless variation.nil?
262
+ variation_key = variation['key']
263
+ return variation
264
+ end
265
+
266
+ # User failed traffic allocation, jump to Everyone Else rule
267
+ @config.logger.log(
268
+ Logger::DEBUG,
269
+ "User '#{user_id}' is not in the traffic group for the targeting rule. Checking 'Eveyrone Else' rule now."
270
+ )
271
+ break
272
+ end
273
+
274
+ # Evalute the "Everyone Else" rule, which is the last rule.
275
+ everyone_else_experiment = rollout_experiments[number_of_rules]
276
+ variation = @bucketer.bucket(everyone_else_experiment, user_id)
277
+ unless variation.nil?
278
+ @config.logger.log(
279
+ Logger::DEBUG,
280
+ "User '#{user_id}' meets conditions for targeting rule 'Everyone Else'."
281
+ )
282
+ return variation
283
+ end
284
+
285
+ @config.logger.log(
286
+ Logger::DEBUG,
287
+ "User '#{user_id}' does not meet conditions for targeting rule 'Everyone Else'."
288
+ )
289
+ end
290
+
291
+ return nil
292
+ end
293
+
99
294
  private
100
-
295
+
101
296
  def get_whitelisted_variation_id(experiment_key, user_id)
102
297
  # Determine if a user is whitelisted into a variation for the given experiment and return the ID of that variation
103
298
  #
@@ -105,32 +300,32 @@ module Optimizely
105
300
  # user_id - ID for the user
106
301
  #
107
302
  # Returns variation ID into which user_id is whitelisted (nil if no variation)
108
-
303
+
109
304
  whitelisted_variations = @config.get_whitelisted_variations(experiment_key)
110
-
305
+
111
306
  return nil unless whitelisted_variations
112
-
307
+
113
308
  whitelisted_variation_key = whitelisted_variations[user_id]
114
-
309
+
115
310
  return nil unless whitelisted_variation_key
116
-
311
+
117
312
  whitelisted_variation_id = @config.get_variation_id_from_key(experiment_key, whitelisted_variation_key)
118
-
313
+
119
314
  unless whitelisted_variation_id
120
315
  @config.logger.log(
121
- Logger::INFO,
122
- "User '#{user_id}' is whitelisted into variation '#{whitelisted_variation_key}', which is not in the datafile."
316
+ Logger::INFO,
317
+ "User '#{user_id}' is whitelisted into variation '#{whitelisted_variation_key}', which is not in the datafile."
123
318
  )
124
319
  return nil
125
320
  end
126
-
321
+
127
322
  @config.logger.log(
128
- Logger::INFO,
129
- "User '#{user_id}' is whitelisted into variation '#{whitelisted_variation_key}' of experiment '#{experiment_key}'."
323
+ Logger::INFO,
324
+ "User '#{user_id}' is whitelisted into variation '#{whitelisted_variation_key}' of experiment '#{experiment_key}'."
130
325
  )
131
326
  whitelisted_variation_id
132
327
  end
133
-
328
+
134
329
  def get_saved_variation_id(experiment_id, user_profile)
135
330
  # Retrieve variation ID of stored bucketing decision for a given experiment from a given user profile
136
331
  #
@@ -139,56 +334,56 @@ module Optimizely
139
334
  #
140
335
  # Returns string variation ID (nil if no decision is found)
141
336
  return nil unless user_profile[:experiment_bucket_map]
142
-
337
+
143
338
  decision = user_profile[:experiment_bucket_map][experiment_id]
144
339
  return nil unless decision
145
340
  variation_id = decision[:variation_id]
146
341
  return variation_id if @config.variation_id_exists?(experiment_id, variation_id)
147
-
342
+
148
343
  @config.logger.log(
149
- Logger::INFO,
150
- "User '#{user_profile['user_id']}' was previously bucketed into variation ID '#{variation_id}' for experiment '#{experiment_id}', but no matching variation was found. Re-bucketing user."
344
+ Logger::INFO,
345
+ "User '#{user_profile['user_id']}' was previously bucketed into variation ID '#{variation_id}' for experiment '#{experiment_id}', but no matching variation was found. Re-bucketing user."
151
346
  )
152
347
  nil
153
348
  end
154
-
349
+
155
350
  def get_user_profile(user_id)
156
351
  # Determine if a user is forced into a variation for the given experiment and return the ID of that variation
157
352
  #
158
353
  # user_id - String ID for the user
159
354
  #
160
355
  # Returns Hash stored user profile (or a default one if lookup fails or user profile service not provided)
161
-
356
+
162
357
  user_profile = {
163
- :user_id => user_id,
164
- :experiment_bucket_map => {}
358
+ :user_id => user_id,
359
+ :experiment_bucket_map => {}
165
360
  }
166
-
361
+
167
362
  return user_profile unless @user_profile_service
168
-
363
+
169
364
  begin
170
365
  user_profile = @user_profile_service.lookup(user_id) || user_profile
171
366
  rescue => e
172
367
  @config.logger.log(Logger::ERROR, "Error while looking up user profile for user ID '#{user_id}': #{e}.")
173
368
  end
174
-
369
+
175
370
  user_profile
176
371
  end
177
-
178
-
372
+
373
+
179
374
  def save_user_profile(user_profile, experiment_id, variation_id)
180
375
  # Save a given bucketing decision to a given user profile
181
376
  #
182
377
  # user_profile - Hash user profile
183
378
  # experiment_id - String experiment ID
184
379
  # variation_id - String variation ID
185
-
380
+
186
381
  return unless @user_profile_service
187
-
382
+
188
383
  user_id = user_profile[:user_id]
189
384
  begin
190
385
  user_profile[:experiment_bucket_map][experiment_id] = {
191
- :variation_id => variation_id
386
+ :variation_id => variation_id
192
387
  }
193
388
  @user_profile_service.save(user_profile)
194
389
  @config.logger.log(Logger::INFO, "Saved variation ID #{variation_id} of experiment ID #{experiment_id} for user '#{user_id}'.")
@@ -197,4 +392,4 @@ module Optimizely
197
392
  end
198
393
  end
199
394
  end
200
- end
395
+ end