optimizely-sdk 2.0.0.beta → 2.0.0.beta1
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 +4 -4
- data/lib/optimizely.rb +166 -73
- data/lib/optimizely/audience.rb +6 -3
- data/lib/optimizely/bucketer.rb +21 -16
- data/lib/optimizely/condition.rb +4 -2
- data/lib/optimizely/decision_service.rb +141 -141
- data/lib/optimizely/error_handler.rb +5 -5
- data/lib/optimizely/event_builder.rb +158 -147
- data/lib/optimizely/event_dispatcher.rb +7 -6
- data/lib/optimizely/exceptions.rb +12 -1
- data/lib/optimizely/helpers/constants.rb +64 -63
- data/lib/optimizely/helpers/event_tag_utils.rb +86 -11
- data/lib/optimizely/helpers/group.rb +3 -1
- data/lib/optimizely/helpers/validator.rb +9 -1
- data/lib/optimizely/helpers/variable_type.rb +11 -7
- data/lib/optimizely/logger.rb +5 -5
- data/lib/optimizely/notification_center.rb +150 -0
- data/lib/optimizely/params.rb +3 -1
- data/lib/optimizely/project_config.rb +128 -24
- data/lib/optimizely/user_profile_service.rb +2 -0
- data/lib/optimizely/version.rb +4 -1
- metadata +15 -16
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f57d0b3e13c6c5bc057719cd5fa0689c9b93ade1
|
4
|
+
data.tar.gz: e67d075ed5fc5e42e41e8a5620e4e038ae3f1afd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5fe41284c79ebc016276afcac83a1d87fb6e7a355738c8ff6c9fb38cd4c7d1a0494c44558394170edd380c0dee42c8accc91d67afb300a6d973a33f03762e984
|
7
|
+
data.tar.gz: 85fa4cb24da1cdd082846b80f4939419023c774e9305d048e9564381e68e4acdf78f76a937885998523afdecf47cce1c09dca3eb7b04ea320c852805d69b44cd
|
data/lib/optimizely.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
#
|
2
|
-
# Copyright 2016-
|
4
|
+
# Copyright 2016-2018, Optimizely and contributors
|
3
5
|
#
|
4
6
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
5
7
|
# you may not use this file except in compliance with the License.
|
@@ -24,11 +26,11 @@ require_relative 'optimizely/helpers/group'
|
|
24
26
|
require_relative 'optimizely/helpers/validator'
|
25
27
|
require_relative 'optimizely/helpers/variable_type'
|
26
28
|
require_relative 'optimizely/logger'
|
29
|
+
require_relative 'optimizely/notification_center'
|
27
30
|
require_relative 'optimizely/project_config'
|
28
31
|
|
29
32
|
module Optimizely
|
30
33
|
class Project
|
31
|
-
|
32
34
|
# Boolean representing if the instance represents a usable Optimizely Project
|
33
35
|
attr_reader :is_valid
|
34
36
|
|
@@ -38,6 +40,7 @@ module Optimizely
|
|
38
40
|
attr_reader :event_builder
|
39
41
|
attr_reader :event_dispatcher
|
40
42
|
attr_reader :logger
|
43
|
+
attr_reader :notification_center
|
41
44
|
|
42
45
|
def initialize(datafile, event_dispatcher = nil, logger = nil, error_handler = nil, skip_json_validation = false, user_profile_service = nil)
|
43
46
|
# Constructor for Projects.
|
@@ -82,7 +85,8 @@ module Optimizely
|
|
82
85
|
end
|
83
86
|
|
84
87
|
@decision_service = DecisionService.new(@config, @user_profile_service)
|
85
|
-
@event_builder = EventBuilder.new(@config)
|
88
|
+
@event_builder = EventBuilder.new(@config, logger)
|
89
|
+
@notification_center = NotificationCenter.new(@logger, @error_handler)
|
86
90
|
end
|
87
91
|
|
88
92
|
def activate(experiment_key, user_id, attributes = nil)
|
@@ -131,6 +135,11 @@ module Optimizely
|
|
131
135
|
return nil
|
132
136
|
end
|
133
137
|
|
138
|
+
if user_id.to_s.empty?
|
139
|
+
@logger.log(Logger::ERROR, 'User ID cannot be empty.')
|
140
|
+
return nil
|
141
|
+
end
|
142
|
+
|
134
143
|
unless user_inputs_valid?(attributes)
|
135
144
|
@logger.log(Logger::INFO, "Not activating user '#{user_id}.")
|
136
145
|
return nil
|
@@ -140,13 +149,39 @@ module Optimizely
|
|
140
149
|
|
141
150
|
unless variation_id.nil?
|
142
151
|
variation = @config.get_variation_from_id(experiment_key, variation_id)
|
143
|
-
if variation
|
144
|
-
return variation['key']
|
145
|
-
end
|
152
|
+
return variation['key'] if variation
|
146
153
|
end
|
147
154
|
nil
|
148
155
|
end
|
149
156
|
|
157
|
+
def set_forced_variation(experiment_key, user_id, variation_key)
|
158
|
+
# Force a user into a variation for a given experiment.
|
159
|
+
#
|
160
|
+
# experiment_key - String - key identifying the experiment.
|
161
|
+
# user_id - String - The user ID to be used for bucketing.
|
162
|
+
# variation_key - The variation key specifies the variation which the user will
|
163
|
+
# be forced into. If nil, then clear the existing experiment-to-variation mapping.
|
164
|
+
#
|
165
|
+
# Returns - Boolean - indicates if the set completed successfully.
|
166
|
+
|
167
|
+
@config.set_forced_variation(experiment_key, user_id, variation_key)
|
168
|
+
end
|
169
|
+
|
170
|
+
def get_forced_variation(experiment_key, user_id)
|
171
|
+
# Gets the forced variation for a given user and experiment.
|
172
|
+
#
|
173
|
+
# experiment_key - String - Key identifying the experiment.
|
174
|
+
# user_id - String - The user ID to be used for bucketing.
|
175
|
+
#
|
176
|
+
# Returns String|nil The forced variation key.
|
177
|
+
|
178
|
+
forced_variation_key = nil
|
179
|
+
forced_variation = @config.get_forced_variation(experiment_key, user_id)
|
180
|
+
forced_variation_key = forced_variation['key'] if forced_variation
|
181
|
+
|
182
|
+
forced_variation_key
|
183
|
+
end
|
184
|
+
|
150
185
|
def track(event_key, user_id, attributes = nil, event_tags = nil)
|
151
186
|
# Send conversion event to Optimizely.
|
152
187
|
#
|
@@ -161,11 +196,9 @@ module Optimizely
|
|
161
196
|
return nil
|
162
197
|
end
|
163
198
|
|
164
|
-
if
|
165
|
-
|
166
|
-
|
167
|
-
}
|
168
|
-
@logger.log(Logger::WARN, 'Event value is deprecated in track call. Use event tags to pass in revenue value instead.')
|
199
|
+
if user_id.to_s.empty?
|
200
|
+
@logger.log(Logger::ERROR, 'User ID cannot be empty.')
|
201
|
+
return nil
|
169
202
|
end
|
170
203
|
|
171
204
|
return nil unless user_inputs_valid?(attributes, event_tags)
|
@@ -189,13 +222,18 @@ module Optimizely
|
|
189
222
|
conversion_event = @event_builder.create_conversion_event(event_key, user_id, attributes,
|
190
223
|
event_tags, experiment_variation_map)
|
191
224
|
@logger.log(Logger::INFO,
|
192
|
-
|
193
|
-
conversion_event.params])
|
225
|
+
"Dispatching conversion event to URL #{conversion_event.url} with params #{conversion_event.params}.")
|
194
226
|
begin
|
195
227
|
@event_dispatcher.dispatch_event(conversion_event)
|
196
228
|
rescue => e
|
197
229
|
@logger.log(Logger::ERROR, "Unable to dispatch conversion event. Error: #{e}")
|
198
230
|
end
|
231
|
+
|
232
|
+
@notification_center.send_notifications(
|
233
|
+
NotificationCenter::NOTIFICATION_TYPES[:TRACK],
|
234
|
+
event_key, user_id, attributes, event_tags, conversion_event
|
235
|
+
)
|
236
|
+
nil
|
199
237
|
end
|
200
238
|
|
201
239
|
def is_feature_enabled(feature_flag_key, user_id, attributes = nil)
|
@@ -213,7 +251,17 @@ module Optimizely
|
|
213
251
|
unless @is_valid
|
214
252
|
logger = SimpleLogger.new
|
215
253
|
logger.log(Logger::ERROR, InvalidDatafileError.new('is_feature_enabled').message)
|
216
|
-
return
|
254
|
+
return false
|
255
|
+
end
|
256
|
+
|
257
|
+
unless feature_flag_key
|
258
|
+
@logger.log(Logger::ERROR, 'Feature flag key cannot be empty.')
|
259
|
+
return false
|
260
|
+
end
|
261
|
+
|
262
|
+
if user_id.to_s.empty?
|
263
|
+
@logger.log(Logger::ERROR, 'User ID cannot be empty.')
|
264
|
+
return false
|
217
265
|
end
|
218
266
|
|
219
267
|
feature_flag = @config.get_feature_flag_from_key(feature_flag_key)
|
@@ -223,23 +271,55 @@ module Optimizely
|
|
223
271
|
end
|
224
272
|
|
225
273
|
decision = @decision_service.get_variation_for_feature(feature_flag, user_id, attributes)
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
else
|
232
|
-
@logger.log(Logger::DEBUG,
|
233
|
-
"The user '#{user_id}' is not being experimented on in feature '#{feature_flag_key}'.")
|
234
|
-
end
|
274
|
+
if decision.nil?
|
275
|
+
@logger.log(Logger::INFO,
|
276
|
+
"Feature '#{feature_flag_key}' is not enabled for user '#{user_id}'.")
|
277
|
+
return false
|
278
|
+
end
|
235
279
|
|
236
|
-
|
237
|
-
|
280
|
+
variation = decision['variation']
|
281
|
+
unless variation['featureEnabled']
|
282
|
+
@logger.log(Logger::INFO,
|
283
|
+
"Feature '#{feature_flag_key}' is not enabled for user '#{user_id}'.")
|
284
|
+
return false
|
238
285
|
end
|
239
286
|
|
240
|
-
|
241
|
-
|
242
|
-
|
287
|
+
if decision.source == Optimizely::DecisionService::DECISION_SOURCE_EXPERIMENT
|
288
|
+
# Send event if Decision came from an experiment.
|
289
|
+
send_impression(decision.experiment, variation['key'], user_id, attributes)
|
290
|
+
else
|
291
|
+
@logger.log(Logger::DEBUG,
|
292
|
+
"The user '#{user_id}' is not being experimented on in feature '#{feature_flag_key}'.")
|
293
|
+
end
|
294
|
+
@logger.log(Logger::INFO, "Feature '#{feature_flag_key}' is enabled for user '#{user_id}'.")
|
295
|
+
|
296
|
+
true
|
297
|
+
end
|
298
|
+
|
299
|
+
def get_enabled_features(user_id, attributes = nil)
|
300
|
+
# Gets keys of all feature flags which are enabled for the user.
|
301
|
+
# Args:
|
302
|
+
# user_id: ID for user.
|
303
|
+
# attributes: Dict representing user attributes.
|
304
|
+
# Returns:
|
305
|
+
# A List of feature flag keys that are enabled for the user.
|
306
|
+
#
|
307
|
+
enabled_features = []
|
308
|
+
|
309
|
+
unless @is_valid
|
310
|
+
logger = SimpleLogger.new
|
311
|
+
logger.log(Logger::ERROR, InvalidDatafileError.new('get_enabled_features').message)
|
312
|
+
return enabled_features
|
313
|
+
end
|
314
|
+
|
315
|
+
@config.feature_flags.each do |feature|
|
316
|
+
enabled_features.push(feature['key']) if is_feature_enabled(
|
317
|
+
feature['key'],
|
318
|
+
user_id,
|
319
|
+
attributes
|
320
|
+
) == true
|
321
|
+
end
|
322
|
+
enabled_features.sort
|
243
323
|
end
|
244
324
|
|
245
325
|
def get_feature_variable_string(feature_flag_key, variable_key, user_id, attributes = nil)
|
@@ -256,12 +336,12 @@ module Optimizely
|
|
256
336
|
variable_value = get_feature_variable_for_type(
|
257
337
|
feature_flag_key,
|
258
338
|
variable_key,
|
259
|
-
Optimizely::Helpers::Constants::VARIABLE_TYPES[
|
339
|
+
Optimizely::Helpers::Constants::VARIABLE_TYPES['STRING'],
|
260
340
|
user_id,
|
261
341
|
attributes
|
262
342
|
)
|
263
343
|
|
264
|
-
|
344
|
+
variable_value
|
265
345
|
end
|
266
346
|
|
267
347
|
def get_feature_variable_boolean(feature_flag_key, variable_key, user_id, attributes = nil)
|
@@ -278,12 +358,12 @@ module Optimizely
|
|
278
358
|
variable_value = get_feature_variable_for_type(
|
279
359
|
feature_flag_key,
|
280
360
|
variable_key,
|
281
|
-
Optimizely::Helpers::Constants::VARIABLE_TYPES[
|
361
|
+
Optimizely::Helpers::Constants::VARIABLE_TYPES['BOOLEAN'],
|
282
362
|
user_id,
|
283
363
|
attributes
|
284
364
|
)
|
285
365
|
|
286
|
-
|
366
|
+
variable_value
|
287
367
|
end
|
288
368
|
|
289
369
|
def get_feature_variable_double(feature_flag_key, variable_key, user_id, attributes = nil)
|
@@ -300,12 +380,12 @@ module Optimizely
|
|
300
380
|
variable_value = get_feature_variable_for_type(
|
301
381
|
feature_flag_key,
|
302
382
|
variable_key,
|
303
|
-
Optimizely::Helpers::Constants::VARIABLE_TYPES[
|
383
|
+
Optimizely::Helpers::Constants::VARIABLE_TYPES['DOUBLE'],
|
304
384
|
user_id,
|
305
385
|
attributes
|
306
386
|
)
|
307
387
|
|
308
|
-
|
388
|
+
variable_value
|
309
389
|
end
|
310
390
|
|
311
391
|
def get_feature_variable_integer(feature_flag_key, variable_key, user_id, attributes = nil)
|
@@ -322,12 +402,12 @@ module Optimizely
|
|
322
402
|
variable_value = get_feature_variable_for_type(
|
323
403
|
feature_flag_key,
|
324
404
|
variable_key,
|
325
|
-
Optimizely::Helpers::Constants::VARIABLE_TYPES[
|
405
|
+
Optimizely::Helpers::Constants::VARIABLE_TYPES['INTEGER'],
|
326
406
|
user_id,
|
327
407
|
attributes
|
328
408
|
)
|
329
409
|
|
330
|
-
|
410
|
+
variable_value
|
331
411
|
end
|
332
412
|
|
333
413
|
private
|
@@ -343,7 +423,23 @@ module Optimizely
|
|
343
423
|
# attributes - Hash representing visitor attributes and values which need to be recorded.
|
344
424
|
#
|
345
425
|
# Returns the type-casted variable value.
|
346
|
-
# Returns nil if the feature flag or variable
|
426
|
+
# Returns nil if the feature flag or variable or user ID is empty
|
427
|
+
# in case of variable type mismatch
|
428
|
+
|
429
|
+
unless feature_flag_key
|
430
|
+
@logger.log(Logger::ERROR, 'Feature flag key cannot be empty.')
|
431
|
+
return nil
|
432
|
+
end
|
433
|
+
|
434
|
+
unless variable_key
|
435
|
+
@logger.log(Logger::ERROR, 'Variable key cannot be empty.')
|
436
|
+
return nil
|
437
|
+
end
|
438
|
+
|
439
|
+
if user_id.to_s.empty?
|
440
|
+
@logger.log(Logger::ERROR, 'User ID cannot be empty.')
|
441
|
+
return nil
|
442
|
+
end
|
347
443
|
|
348
444
|
feature_flag = @config.get_feature_flag_from_key(feature_flag_key)
|
349
445
|
unless feature_flag
|
@@ -351,43 +447,40 @@ module Optimizely
|
|
351
447
|
return nil
|
352
448
|
end
|
353
449
|
|
354
|
-
variable_value = nil
|
355
450
|
variable = @config.get_feature_variable(feature_flag, variable_key)
|
356
|
-
unless variable.nil?
|
357
|
-
variable_value = variable['defaultValue']
|
358
451
|
|
452
|
+
# Error message logged in ProjectConfig- get_feature_flag_from_key
|
453
|
+
return nil if variable.nil?
|
454
|
+
|
455
|
+
# Returns nil if type differs
|
456
|
+
if variable['type'] != variable_type
|
457
|
+
@logger.log(Logger::WARN,
|
458
|
+
"Requested variable as type '#{variable_type}' but variable '#{variable_key}' is of type '#{variable['type']}'.")
|
459
|
+
return nil
|
460
|
+
else
|
359
461
|
decision = @decision_service.get_variation_for_feature(feature_flag, user_id, attributes)
|
360
|
-
|
361
|
-
|
362
|
-
"User '#{user_id}' was not bucketed into any variation for feature flag '#{feature_flag_key}'. Returning the default variable value '#{variable_value}'.")
|
363
|
-
else
|
462
|
+
variable_value = variable['defaultValue']
|
463
|
+
if decision
|
364
464
|
variation = decision['variation']
|
365
465
|
variation_variable_usages = @config.variation_id_to_variable_usage_map[variation['id']]
|
366
466
|
variable_id = variable['id']
|
367
|
-
|
368
|
-
variation_key = variation['key']
|
369
|
-
@logger.log(Logger::DEBUG,
|
370
|
-
"Variable '#{variable_key}' is not used in variation '#{variation_key}'. Returning the default variable value '#{variable_value}'."
|
371
|
-
)
|
372
|
-
else
|
467
|
+
if variation_variable_usages&.key?(variable_id)
|
373
468
|
variable_value = variation_variable_usages[variable_id]['value']
|
374
469
|
@logger.log(Logger::INFO,
|
375
|
-
|
470
|
+
"Got variable value '#{variable_value}' for variable '#{variable_key}' of feature flag '#{feature_flag_key}'.")
|
471
|
+
else
|
472
|
+
@logger.log(Logger::DEBUG,
|
473
|
+
"Variable '#{variable_key}' is not used in variation '#{variation['key']}'. Returning the default variable value '#{variable_value}'.")
|
376
474
|
end
|
475
|
+
else
|
476
|
+
@logger.log(Logger::INFO,
|
477
|
+
"User '#{user_id}' was not bucketed into any variation for feature flag '#{feature_flag_key}'. Returning the default variable value '#{variable_value}'.")
|
377
478
|
end
|
378
479
|
end
|
379
480
|
|
380
|
-
|
381
|
-
actual_variable_type = variable['type']
|
382
|
-
unless variable_type == actual_variable_type
|
383
|
-
@logger.log(Logger::WARN,
|
384
|
-
"Requested variable type '#{variable_type}' but variable '#{variable_key}' is of type '#{actual_variable_type}'.")
|
385
|
-
end
|
481
|
+
variable_value = Helpers::VariableType.cast_value_to_type(variable_value, variable_type, @logger)
|
386
482
|
|
387
|
-
|
388
|
-
end
|
389
|
-
|
390
|
-
return variable_value
|
483
|
+
variable_value
|
391
484
|
end
|
392
485
|
|
393
486
|
def get_valid_experiments_for_event(event_key, user_id, attributes)
|
@@ -426,13 +519,9 @@ module Optimizely
|
|
426
519
|
#
|
427
520
|
# Returns boolean True if inputs are valid. False otherwise.
|
428
521
|
|
429
|
-
if !attributes.nil? && !attributes_valid?(attributes)
|
430
|
-
return false
|
431
|
-
end
|
522
|
+
return false if !attributes.nil? && !attributes_valid?(attributes)
|
432
523
|
|
433
|
-
if !event_tags.nil? && !event_tags_valid?(event_tags)
|
434
|
-
return false
|
435
|
-
end
|
524
|
+
return false if !event_tags.nil? && !event_tags_valid?(event_tags)
|
436
525
|
|
437
526
|
true
|
438
527
|
end
|
@@ -457,12 +546,12 @@ module Optimizely
|
|
457
546
|
|
458
547
|
def validate_instantiation_options(datafile, skip_json_validation)
|
459
548
|
unless skip_json_validation
|
460
|
-
raise InvalidInputError
|
549
|
+
raise InvalidInputError, 'datafile' unless Helpers::Validator.datafile_valid?(datafile)
|
461
550
|
end
|
462
551
|
|
463
|
-
raise InvalidInputError
|
464
|
-
raise InvalidInputError
|
465
|
-
raise InvalidInputError
|
552
|
+
raise InvalidInputError, 'logger' unless Helpers::Validator.logger_valid?(@logger)
|
553
|
+
raise InvalidInputError, 'error_handler' unless Helpers::Validator.error_handler_valid?(@error_handler)
|
554
|
+
raise InvalidInputError, 'event_dispatcher' unless Helpers::Validator.event_dispatcher_valid?(@event_dispatcher)
|
466
555
|
end
|
467
556
|
|
468
557
|
def send_impression(experiment, variation_key, user_id, attributes = nil)
|
@@ -470,13 +559,17 @@ module Optimizely
|
|
470
559
|
variation_id = @config.get_variation_id_from_key(experiment_key, variation_key)
|
471
560
|
impression_event = @event_builder.create_impression_event(experiment, variation_id, user_id, attributes)
|
472
561
|
@logger.log(Logger::INFO,
|
473
|
-
|
474
|
-
impression_event.params])
|
562
|
+
"Dispatching impression event to URL #{impression_event.url} with params #{impression_event.params}.")
|
475
563
|
begin
|
476
564
|
@event_dispatcher.dispatch_event(impression_event)
|
477
565
|
rescue => e
|
478
566
|
@logger.log(Logger::ERROR, "Unable to dispatch impression event. Error: #{e}")
|
479
567
|
end
|
568
|
+
variation = @config.get_variation_from_id(experiment_key, variation_id)
|
569
|
+
@notification_center.send_notifications(
|
570
|
+
NotificationCenter::NOTIFICATION_TYPES[:ACTIVATE],
|
571
|
+
experiment, user_id, attributes, variation, impression_event
|
572
|
+
)
|
480
573
|
end
|
481
574
|
end
|
482
575
|
end
|
data/lib/optimizely/audience.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
#
|
2
|
-
# Copyright 2016, Optimizely and contributors
|
4
|
+
# Copyright 2016-2017, Optimizely and contributors
|
3
5
|
#
|
4
6
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
5
7
|
# you may not use this file except in compliance with the License.
|
@@ -41,8 +43,9 @@ module Optimizely
|
|
41
43
|
# Return true if any one of the audience conditions are met
|
42
44
|
@condition_evaluator = ConditionEvaluator.new(attributes)
|
43
45
|
audience_ids.each do |audience_id|
|
44
|
-
|
45
|
-
audience_conditions =
|
46
|
+
audience = config.get_audience_from_id(audience_id)
|
47
|
+
audience_conditions = audience['conditions']
|
48
|
+
audience_conditions = JSON.parse(audience_conditions)
|
46
49
|
return true if @condition_evaluator.evaluate(audience_conditions)
|
47
50
|
end
|
48
51
|
|
data/lib/optimizely/bucketer.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
#
|
2
4
|
# Copyright 2016-2017, Optimizely and contributors
|
3
5
|
#
|
@@ -20,7 +22,7 @@ module Optimizely
|
|
20
22
|
class Bucketer
|
21
23
|
# Optimizely bucketing algorithm that evenly distributes visitors.
|
22
24
|
|
23
|
-
BUCKETING_ID_TEMPLATE = '
|
25
|
+
BUCKETING_ID_TEMPLATE = '%<bucketing_id>s%<entity_id>s'
|
24
26
|
HASH_SEED = 1
|
25
27
|
MAX_HASH_VALUE = 2**32
|
26
28
|
MAX_TRAFFIC_VALUE = 10_000
|
@@ -35,13 +37,15 @@ module Optimizely
|
|
35
37
|
@config = config
|
36
38
|
end
|
37
39
|
|
38
|
-
def bucket(experiment, user_id)
|
40
|
+
def bucket(experiment, bucketing_id, user_id)
|
39
41
|
# Determines ID of variation to be shown for a given experiment key and user ID.
|
40
42
|
#
|
41
43
|
# experiment - Experiment for which visitor is to be bucketed.
|
44
|
+
# bucketing_id - String A customer-assigned value used to generate the bucketing key
|
42
45
|
# user_id - String ID for user.
|
43
46
|
#
|
44
47
|
# Returns variation in which visitor with ID user_id has been placed. Nil if no variation.
|
48
|
+
return nil if experiment.nil?
|
45
49
|
|
46
50
|
# check if experiment is in a group; if so, check if user is bucketed into specified experiment
|
47
51
|
experiment_id = experiment['id']
|
@@ -51,7 +55,7 @@ module Optimizely
|
|
51
55
|
group = @config.group_key_map.fetch(group_id)
|
52
56
|
if Helpers::Group.random_policy?(group)
|
53
57
|
traffic_allocations = group.fetch('trafficAllocation')
|
54
|
-
bucketed_experiment_id = find_bucket(user_id, group_id, traffic_allocations)
|
58
|
+
bucketed_experiment_id = find_bucket(bucketing_id, user_id, group_id, traffic_allocations)
|
55
59
|
# return if the user is not bucketed into any experiment
|
56
60
|
unless bucketed_experiment_id
|
57
61
|
@config.logger.log(Logger::INFO, "User '#{user_id}' is in no experiment.")
|
@@ -76,7 +80,7 @@ module Optimizely
|
|
76
80
|
end
|
77
81
|
|
78
82
|
traffic_allocations = experiment['trafficAllocation']
|
79
|
-
variation_id = find_bucket(user_id, experiment_id, traffic_allocations)
|
83
|
+
variation_id = find_bucket(bucketing_id, user_id, experiment_id, traffic_allocations)
|
80
84
|
if variation_id && variation_id != ''
|
81
85
|
variation = @config.get_variation_from_id(experiment_key, variation_id)
|
82
86
|
variation_key = variation ? variation['key'] : nil
|
@@ -96,18 +100,19 @@ module Optimizely
|
|
96
100
|
nil
|
97
101
|
end
|
98
102
|
|
99
|
-
def find_bucket(user_id, parent_id, traffic_allocations)
|
103
|
+
def find_bucket(bucketing_id, user_id, parent_id, traffic_allocations)
|
100
104
|
# Helper function to find the matching entity ID for a given bucketing value in a list of traffic allocations.
|
101
105
|
#
|
106
|
+
# bucketing_id - String A customer-assigned value user to generate bucketing key
|
102
107
|
# user_id - String ID for user
|
103
108
|
# parent_id - String entity ID to use for bucketing ID
|
104
109
|
# traffic_allocations - Array of traffic allocations
|
105
110
|
#
|
106
111
|
# Returns entity ID corresponding to the provided bucket value or nil if no match is found.
|
107
|
-
|
108
|
-
|
109
|
-
bucket_value
|
110
|
-
|
112
|
+
bucketing_key = format(BUCKETING_ID_TEMPLATE, bucketing_id: bucketing_id, entity_id: parent_id)
|
113
|
+
bucket_value = generate_bucket_value(bucketing_key)
|
114
|
+
@config.logger.log(Logger::DEBUG, "Assigned bucket #{bucket_value} to user '#{user_id}' "\
|
115
|
+
"with bucketing ID: '#{bucketing_id}'.")
|
111
116
|
|
112
117
|
traffic_allocations.each do |traffic_allocation|
|
113
118
|
current_end_of_range = traffic_allocation['endOfRange']
|
@@ -122,25 +127,25 @@ module Optimizely
|
|
122
127
|
|
123
128
|
private
|
124
129
|
|
125
|
-
def generate_bucket_value(
|
130
|
+
def generate_bucket_value(bucketing_key)
|
126
131
|
# Helper function to generate bucket value in half-closed interval [0, MAX_TRAFFIC_VALUE).
|
127
132
|
#
|
128
|
-
#
|
133
|
+
# bucketing_key - String - Value used to generate bucket value
|
129
134
|
#
|
130
|
-
# Returns bucket value corresponding to the provided bucketing
|
135
|
+
# Returns bucket value corresponding to the provided bucketing key.
|
131
136
|
|
132
|
-
ratio =
|
137
|
+
ratio = generate_unsigned_hash_code_32_bit(bucketing_key).to_f / MAX_HASH_VALUE
|
133
138
|
(ratio * MAX_TRAFFIC_VALUE).to_i
|
134
139
|
end
|
135
140
|
|
136
|
-
def generate_unsigned_hash_code_32_bit(
|
141
|
+
def generate_unsigned_hash_code_32_bit(bucketing_key)
|
137
142
|
# Helper function to retreive hash code
|
138
143
|
#
|
139
|
-
#
|
144
|
+
# bucketing_key - String - Value used for the key of the murmur hash
|
140
145
|
#
|
141
146
|
# Returns hash code which is a 32 bit unsigned integer.
|
142
147
|
|
143
|
-
MurmurHash3::V32.str_hash(
|
148
|
+
MurmurHash3::V32.str_hash(bucketing_key, @bucket_seed) & UNSIGNED_MAX_32_BIT_VALUE
|
144
149
|
end
|
145
150
|
end
|
146
151
|
end
|