optimizely-sdk 2.0.0.beta → 2.0.0.beta1
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 +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
|