optimizely-sdk 1.4.0 → 1.5.0

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