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 +4 -4
- data/lib/optimizely.rb +223 -11
- data/lib/optimizely/bucketer.rb +19 -17
- data/lib/optimizely/decision_service.rb +245 -50
- data/lib/optimizely/event_builder.rb +33 -18
- data/lib/optimizely/exceptions.rb +9 -0
- data/lib/optimizely/helpers/constants.rb +7 -0
- data/lib/optimizely/helpers/variable_type.rb +56 -0
- data/lib/optimizely/notification_center.rb +148 -0
- data/lib/optimizely/project_config.rb +91 -37
- data/lib/optimizely/version.rb +2 -2
- metadata +5 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3fae777d6ce539eb2e0809dbf38d4784130c8ade
|
4
|
+
data.tar.gz: f65bee6796830490f4ee8f1a86da20ed7b4f01fb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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
|
-
|
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
|
data/lib/optimizely/bucketer.rb
CHANGED
@@ -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 = '%{
|
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
|
-
|
109
|
-
bucket_value
|
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(
|
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
|
-
#
|
130
|
+
# bucketing_key - String - Value used to generate bucket value
|
129
131
|
#
|
130
|
-
# Returns bucket value corresponding to the provided bucketing
|
132
|
+
# Returns bucket value corresponding to the provided bucketing key.
|
131
133
|
|
132
|
-
ratio = (generate_unsigned_hash_code_32_bit(
|
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(
|
138
|
+
def generate_unsigned_hash_code_32_bit(bucketing_key)
|
137
139
|
# Helper function to retreive hash code
|
138
140
|
#
|
139
|
-
#
|
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(
|
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
|
-
|
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
|
-
|
76
|
-
|
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
|
-
|
85
|
-
|
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
|
-
|
122
|
-
|
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
|
-
|
129
|
-
|
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
|
-
|
150
|
-
|
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
|
-
|
164
|
-
|
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
|
-
|
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
|