split 4.0.0.pre2 → 4.0.2
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/.github/workflows/ci.yml +14 -1
- data/.rubocop.yml +2 -5
- data/CHANGELOG.md +26 -2
- data/CONTRIBUTING.md +1 -1
- data/Gemfile +2 -1
- data/README.md +4 -2
- data/Rakefile +4 -5
- data/gemfiles/5.2.gemfile +1 -3
- data/gemfiles/6.0.gemfile +1 -3
- data/gemfiles/{5.0.gemfile → 6.1.gemfile} +2 -4
- data/gemfiles/{5.1.gemfile → 7.0.gemfile} +3 -4
- data/lib/split/algorithms/block_randomization.rb +5 -6
- data/lib/split/algorithms/whiplash.rb +16 -18
- data/lib/split/algorithms.rb +22 -0
- data/lib/split/alternative.rb +21 -22
- data/lib/split/cache.rb +0 -1
- data/lib/split/combined_experiments_helper.rb +4 -4
- data/lib/split/configuration.rb +83 -84
- data/lib/split/dashboard/helpers.rb +6 -7
- data/lib/split/dashboard/pagination_helpers.rb +53 -54
- data/lib/split/dashboard/public/style.css +5 -2
- data/lib/split/dashboard/views/index.erb +19 -4
- data/lib/split/dashboard.rb +29 -23
- data/lib/split/encapsulated_helper.rb +4 -6
- data/lib/split/experiment.rb +84 -88
- data/lib/split/experiment_catalog.rb +6 -5
- data/lib/split/extensions/string.rb +1 -1
- data/lib/split/goals_collection.rb +8 -10
- data/lib/split/helper.rb +19 -19
- data/lib/split/metric.rb +4 -5
- data/lib/split/persistence/cookie_adapter.rb +44 -47
- data/lib/split/persistence/dual_adapter.rb +7 -8
- data/lib/split/persistence/redis_adapter.rb +2 -3
- data/lib/split/persistence/session_adapter.rb +0 -2
- data/lib/split/persistence.rb +4 -4
- data/lib/split/redis_interface.rb +1 -2
- data/lib/split/trial.rb +23 -24
- data/lib/split/user.rb +12 -13
- data/lib/split/version.rb +1 -1
- data/lib/split/zscore.rb +1 -3
- data/lib/split.rb +26 -25
- data/spec/algorithms/block_randomization_spec.rb +6 -5
- data/spec/algorithms/weighted_sample_spec.rb +6 -5
- data/spec/algorithms/whiplash_spec.rb +4 -5
- data/spec/alternative_spec.rb +35 -36
- data/spec/cache_spec.rb +15 -19
- data/spec/combined_experiments_helper_spec.rb +18 -17
- data/spec/configuration_spec.rb +32 -38
- data/spec/dashboard/pagination_helpers_spec.rb +69 -67
- data/spec/dashboard/paginator_spec.rb +10 -9
- data/spec/dashboard_helpers_spec.rb +19 -18
- data/spec/dashboard_spec.rb +67 -35
- data/spec/encapsulated_helper_spec.rb +12 -14
- data/spec/experiment_catalog_spec.rb +14 -13
- data/spec/experiment_spec.rb +121 -123
- data/spec/goals_collection_spec.rb +17 -15
- data/spec/helper_spec.rb +379 -382
- data/spec/metric_spec.rb +14 -14
- data/spec/persistence/cookie_adapter_spec.rb +23 -8
- data/spec/persistence/dual_adapter_spec.rb +71 -71
- data/spec/persistence/redis_adapter_spec.rb +25 -26
- data/spec/persistence/session_adapter_spec.rb +2 -3
- data/spec/persistence_spec.rb +1 -2
- data/spec/redis_interface_spec.rb +16 -14
- data/spec/spec_helper.rb +15 -13
- data/spec/split_spec.rb +11 -11
- data/spec/support/cookies_mock.rb +1 -2
- data/spec/trial_spec.rb +61 -60
- data/spec/user_spec.rb +36 -36
- data/split.gemspec +20 -20
- metadata +9 -10
- data/.rubocop_todo.yml +0 -226
- data/Appraisals +0 -19
data/lib/split/experiment.rb
CHANGED
@@ -1,7 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'rubystats'
|
4
|
-
|
5
3
|
module Split
|
6
4
|
class Experiment
|
7
5
|
attr_accessor :name
|
@@ -13,7 +11,7 @@ module Split
|
|
13
11
|
attr_reader :resettable
|
14
12
|
|
15
13
|
DEFAULT_OPTIONS = {
|
16
|
-
:
|
14
|
+
resettable: true
|
17
15
|
}
|
18
16
|
|
19
17
|
def self.find(name)
|
@@ -52,7 +50,7 @@ module Split
|
|
52
50
|
|
53
51
|
if alts.length == 1
|
54
52
|
if alts[0].is_a? Hash
|
55
|
-
alts = alts[0].map{|k, v| {k => v} }
|
53
|
+
alts = alts[0].map { |k, v| { k => v } }
|
56
54
|
end
|
57
55
|
end
|
58
56
|
|
@@ -87,7 +85,7 @@ module Split
|
|
87
85
|
persist_experiment_configuration
|
88
86
|
end
|
89
87
|
|
90
|
-
redis.hmset(experiment_config_key, :resettable, resettable,
|
88
|
+
redis.hmset(experiment_config_key, :resettable, resettable.to_s,
|
91
89
|
:algorithm, algorithm.to_s)
|
92
90
|
self
|
93
91
|
end
|
@@ -96,7 +94,7 @@ module Split
|
|
96
94
|
if @alternatives.empty? && Split.configuration.experiment_for(@name).nil?
|
97
95
|
raise ExperimentNotFound.new("Experiment #{@name} not found")
|
98
96
|
end
|
99
|
-
@alternatives.each {|a| a.validate! }
|
97
|
+
@alternatives.each { |a| a.validate! }
|
100
98
|
goals_collection.validate!
|
101
99
|
end
|
102
100
|
|
@@ -109,7 +107,7 @@ module Split
|
|
109
107
|
end
|
110
108
|
|
111
109
|
def [](name)
|
112
|
-
alternatives.find{|a| a.name == name}
|
110
|
+
alternatives.find { |a| a.name == name }
|
113
111
|
end
|
114
112
|
|
115
113
|
def algorithm
|
@@ -121,7 +119,7 @@ module Split
|
|
121
119
|
end
|
122
120
|
|
123
121
|
def resettable=(resettable)
|
124
|
-
@resettable = resettable.is_a?(String) ? resettable ==
|
122
|
+
@resettable = resettable.is_a?(String) ? resettable == "true" : resettable
|
125
123
|
end
|
126
124
|
|
127
125
|
def alternatives=(alts)
|
@@ -157,7 +155,7 @@ module Split
|
|
157
155
|
end
|
158
156
|
|
159
157
|
def participant_count
|
160
|
-
alternatives.inject(0){|sum, a| sum + a.participant_count}
|
158
|
+
alternatives.inject(0) { |sum, a| sum + a.participant_count }
|
161
159
|
end
|
162
160
|
|
163
161
|
def control
|
@@ -262,8 +260,8 @@ module Split
|
|
262
260
|
exp_config = redis.hgetall(experiment_config_key)
|
263
261
|
|
264
262
|
options = {
|
265
|
-
resettable: exp_config[
|
266
|
-
algorithm: exp_config[
|
263
|
+
resettable: exp_config["resettable"],
|
264
|
+
algorithm: exp_config["algorithm"],
|
267
265
|
alternatives: load_alternatives_from_redis,
|
268
266
|
goals: Split::GoalsCollection.new(@name).load_from_redis,
|
269
267
|
metadata: load_metadata_from_redis
|
@@ -328,11 +326,11 @@ module Split
|
|
328
326
|
winning_counts.each do |alternative, wins|
|
329
327
|
alternative_probabilities[alternative] = wins / number_of_simulations.to_f
|
330
328
|
end
|
331
|
-
|
329
|
+
alternative_probabilities
|
332
330
|
end
|
333
331
|
|
334
332
|
def count_simulated_wins(winning_alternatives)
|
335
|
-
|
333
|
+
# initialize a hash to keep track of winning alternative in simulations
|
336
334
|
winning_counts = {}
|
337
335
|
alternatives.each do |alternative|
|
338
336
|
winning_counts[alternative] = 0
|
@@ -341,7 +339,7 @@ module Split
|
|
341
339
|
winning_alternatives.each do |alternative|
|
342
340
|
winning_counts[alternative] += 1
|
343
341
|
end
|
344
|
-
|
342
|
+
winning_counts
|
345
343
|
end
|
346
344
|
|
347
345
|
def find_simulated_winner(simulated_cr_hash)
|
@@ -353,7 +351,7 @@ module Split
|
|
353
351
|
end
|
354
352
|
end
|
355
353
|
winner = winning_pair[0]
|
356
|
-
|
354
|
+
winner
|
357
355
|
end
|
358
356
|
|
359
357
|
def calc_simulated_conversion_rates(beta_params)
|
@@ -363,11 +361,11 @@ module Split
|
|
363
361
|
beta_params.each do |alternative, params|
|
364
362
|
alpha = params[0]
|
365
363
|
beta = params[1]
|
366
|
-
simulated_conversion_rate =
|
364
|
+
simulated_conversion_rate = Split::Algorithms.beta_distribution_rng(alpha, beta)
|
367
365
|
simulated_cr_hash[alternative] = simulated_conversion_rate
|
368
366
|
end
|
369
367
|
|
370
|
-
|
368
|
+
simulated_cr_hash
|
371
369
|
end
|
372
370
|
|
373
371
|
def calc_beta_params(goal = nil)
|
@@ -381,7 +379,7 @@ module Split
|
|
381
379
|
|
382
380
|
beta_params[alternative] = params
|
383
381
|
end
|
384
|
-
|
382
|
+
beta_params
|
385
383
|
end
|
386
384
|
|
387
385
|
def calc_time=(time)
|
@@ -394,11 +392,11 @@ module Split
|
|
394
392
|
|
395
393
|
def jstring(goal = nil)
|
396
394
|
js_id = if goal.nil?
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
js_id.gsub(
|
395
|
+
name
|
396
|
+
else
|
397
|
+
name + "-" + goal
|
398
|
+
end
|
399
|
+
js_id.gsub("/", "--")
|
402
400
|
end
|
403
401
|
|
404
402
|
def cohorting_disabled?
|
@@ -410,95 +408,93 @@ module Split
|
|
410
408
|
|
411
409
|
def disable_cohorting
|
412
410
|
@cohorting_disabled = true
|
413
|
-
redis.hset(experiment_config_key, :cohorting, true)
|
411
|
+
redis.hset(experiment_config_key, :cohorting, true.to_s)
|
414
412
|
end
|
415
413
|
|
416
414
|
def enable_cohorting
|
417
415
|
@cohorting_disabled = false
|
418
|
-
redis.hset(experiment_config_key, :cohorting, false)
|
416
|
+
redis.hset(experiment_config_key, :cohorting, false.to_s)
|
419
417
|
end
|
420
418
|
|
421
419
|
protected
|
420
|
+
def experiment_config_key
|
421
|
+
"experiment_configurations/#{@name}"
|
422
|
+
end
|
422
423
|
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
def load_metadata_from_configuration
|
428
|
-
Split.configuration.experiment_for(@name)[:metadata]
|
429
|
-
end
|
424
|
+
def load_metadata_from_configuration
|
425
|
+
Split.configuration.experiment_for(@name)[:metadata]
|
426
|
+
end
|
430
427
|
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
428
|
+
def load_metadata_from_redis
|
429
|
+
meta = redis.get(metadata_key)
|
430
|
+
JSON.parse(meta) unless meta.nil?
|
431
|
+
end
|
435
432
|
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
433
|
+
def load_alternatives_from_configuration
|
434
|
+
alts = Split.configuration.experiment_for(@name)[:alternatives]
|
435
|
+
raise ArgumentError, "Experiment configuration is missing :alternatives array" unless alts
|
436
|
+
if alts.is_a?(Hash)
|
437
|
+
alts.keys
|
438
|
+
else
|
439
|
+
alts.flatten
|
440
|
+
end
|
443
441
|
end
|
444
|
-
end
|
445
442
|
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
443
|
+
def load_alternatives_from_redis
|
444
|
+
alternatives = redis.lrange(@name, 0, -1)
|
445
|
+
alternatives.map do |alt|
|
446
|
+
alt = begin
|
447
|
+
JSON.parse(alt)
|
448
|
+
rescue
|
449
|
+
alt
|
450
|
+
end
|
451
|
+
Split::Alternative.new(alt, @name)
|
452
|
+
end
|
455
453
|
end
|
456
|
-
end
|
457
454
|
|
458
455
|
private
|
456
|
+
def redis
|
457
|
+
Split.redis
|
458
|
+
end
|
459
459
|
|
460
|
-
|
461
|
-
|
462
|
-
|
460
|
+
def redis_interface
|
461
|
+
RedisInterface.new
|
462
|
+
end
|
463
463
|
|
464
|
-
|
465
|
-
|
466
|
-
|
464
|
+
def persist_experiment_configuration
|
465
|
+
redis_interface.add_to_set(:experiments, name)
|
466
|
+
redis_interface.persist_list(name, @alternatives.map { |alt| { alt.name => alt.weight }.to_json })
|
467
|
+
goals_collection.save
|
467
468
|
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
469
|
+
if @metadata
|
470
|
+
redis.set(metadata_key, @metadata.to_json)
|
471
|
+
else
|
472
|
+
delete_metadata
|
473
|
+
end
|
474
|
+
end
|
472
475
|
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
+
def remove_experiment_configuration
|
477
|
+
@alternatives.each(&:delete)
|
478
|
+
goals_collection.delete
|
476
479
|
delete_metadata
|
480
|
+
redis.del(@name)
|
477
481
|
end
|
478
|
-
end
|
479
482
|
|
480
|
-
|
481
|
-
|
482
|
-
goals_collection.delete
|
483
|
-
delete_metadata
|
484
|
-
redis.del(@name)
|
485
|
-
end
|
483
|
+
def experiment_configuration_has_changed?
|
484
|
+
existing_experiment = Experiment.find(@name)
|
486
485
|
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
existing_experiment.goals != @goals ||
|
492
|
-
existing_experiment.metadata != @metadata
|
493
|
-
end
|
486
|
+
existing_experiment.alternatives.map(&:to_s) != @alternatives.map(&:to_s) ||
|
487
|
+
existing_experiment.goals != @goals ||
|
488
|
+
existing_experiment.metadata != @metadata
|
489
|
+
end
|
494
490
|
|
495
|
-
|
496
|
-
|
497
|
-
|
491
|
+
def goals_collection
|
492
|
+
Split::GoalsCollection.new(@name, @goals)
|
493
|
+
end
|
498
494
|
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
495
|
+
def remove_experiment_cohorting
|
496
|
+
@cohorting_disabled = false
|
497
|
+
redis.hdel(experiment_config_key, :cohorting)
|
498
|
+
end
|
503
499
|
end
|
504
500
|
end
|
@@ -1,15 +1,16 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module Split
|
3
4
|
class ExperimentCatalog
|
4
5
|
# Return all experiments
|
5
6
|
def self.all
|
6
7
|
# Call compact to prevent nil experiments from being returned -- seems to happen during gem upgrades
|
7
|
-
Split.redis.smembers(:experiments).map {|e| find(e)}.compact
|
8
|
+
Split.redis.smembers(:experiments).map { |e| find(e) }.compact
|
8
9
|
end
|
9
10
|
|
10
11
|
# Return experiments without a winner (considered "active") first
|
11
12
|
def self.all_active_first
|
12
|
-
all.partition{|e| not e.winner}.map{|es| es.sort_by(&:name)}.flatten
|
13
|
+
all.partition { |e| not e.winner }.map { |es| es.sort_by(&:name) }.flatten
|
13
14
|
end
|
14
15
|
|
15
16
|
def self.find(name)
|
@@ -19,14 +20,14 @@ module Split
|
|
19
20
|
def self.find_or_initialize(metric_descriptor, control = nil, *alternatives)
|
20
21
|
# Check if array is passed to ab_test
|
21
22
|
# e.g. ab_test('name', ['Alt 1', 'Alt 2', 'Alt 3'])
|
22
|
-
if control.is_a?
|
23
|
+
if control.is_a?(Array) && alternatives.length.zero?
|
23
24
|
control, alternatives = control.first, control[1..-1]
|
24
25
|
end
|
25
26
|
|
26
27
|
experiment_name_with_version, goals = normalize_experiment(metric_descriptor)
|
27
|
-
experiment_name = experiment_name_with_version.to_s.split(
|
28
|
+
experiment_name = experiment_name_with_version.to_s.split(":")[0]
|
28
29
|
Split::Experiment.new(experiment_name,
|
29
|
-
:
|
30
|
+
alternatives: [control].compact + alternatives, goals: goals)
|
30
31
|
end
|
31
32
|
|
32
33
|
def self.find_or_create(metric_descriptor, control = nil, *alternatives)
|
@@ -4,7 +4,7 @@ class String
|
|
4
4
|
# Constatntize is often provided by ActiveSupport, but ActiveSupport is not a dependency of Split.
|
5
5
|
unless method_defined?(:constantize)
|
6
6
|
def constantize
|
7
|
-
names = self.split(
|
7
|
+
names = self.split("::")
|
8
8
|
names.shift if names.empty? || names.first.empty?
|
9
9
|
|
10
10
|
constant = Object
|
@@ -2,8 +2,7 @@
|
|
2
2
|
|
3
3
|
module Split
|
4
4
|
class GoalsCollection
|
5
|
-
|
6
|
-
def initialize(experiment_name, goals=nil)
|
5
|
+
def initialize(experiment_name, goals = nil)
|
7
6
|
@experiment_name = experiment_name
|
8
7
|
@goals = goals
|
9
8
|
end
|
@@ -15,10 +14,10 @@ module Split
|
|
15
14
|
def load_from_configuration
|
16
15
|
goals = Split.configuration.experiment_for(@experiment_name)[:goals]
|
17
16
|
|
18
|
-
if goals
|
19
|
-
goals = []
|
20
|
-
else
|
17
|
+
if goals
|
21
18
|
goals.flatten
|
19
|
+
else
|
20
|
+
[]
|
22
21
|
end
|
23
22
|
end
|
24
23
|
|
@@ -29,7 +28,7 @@ module Split
|
|
29
28
|
|
30
29
|
def validate!
|
31
30
|
unless @goals.nil? || @goals.kind_of?(Array)
|
32
|
-
raise ArgumentError,
|
31
|
+
raise ArgumentError, "Goals must be an array"
|
33
32
|
end
|
34
33
|
end
|
35
34
|
|
@@ -38,9 +37,8 @@ module Split
|
|
38
37
|
end
|
39
38
|
|
40
39
|
private
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
end
|
40
|
+
def goals_key
|
41
|
+
"#{@experiment_name}:goals"
|
42
|
+
end
|
45
43
|
end
|
46
44
|
end
|
data/lib/split/helper.rb
CHANGED
@@ -12,9 +12,9 @@ module Split
|
|
12
12
|
alternative = if Split.configuration.enabled && !exclude_visitor?
|
13
13
|
experiment.save
|
14
14
|
raise(Split::InvalidExperimentsFormatError) unless (Split.configuration.experiments || {}).fetch(experiment.name.to_sym, {})[:combined_experiments].nil?
|
15
|
-
trial = Trial.new(:
|
16
|
-
:
|
17
|
-
:
|
15
|
+
trial = Trial.new(user: ab_user, experiment: experiment,
|
16
|
+
override: override_alternative(experiment.name), exclude: exclude_visitor?,
|
17
|
+
disabled: split_generically_disabled?)
|
18
18
|
alt = trial.choose!(self)
|
19
19
|
alt ? alt.name : nil
|
20
20
|
else
|
@@ -44,21 +44,21 @@ module Split
|
|
44
44
|
ab_user.delete(experiment.key)
|
45
45
|
end
|
46
46
|
|
47
|
-
def finish_experiment(experiment, options = {:
|
47
|
+
def finish_experiment(experiment, options = { reset: true })
|
48
48
|
return false if active_experiments[experiment.name].nil?
|
49
49
|
return true if experiment.has_winner?
|
50
50
|
should_reset = experiment.resettable? && options[:reset]
|
51
51
|
if ab_user[experiment.finished_key] && !should_reset
|
52
|
-
|
52
|
+
true
|
53
53
|
else
|
54
54
|
alternative_name = ab_user[experiment.key]
|
55
55
|
trial = Trial.new(
|
56
|
-
:
|
57
|
-
:
|
58
|
-
:
|
59
|
-
:
|
60
|
-
)
|
61
|
-
|
56
|
+
user: ab_user,
|
57
|
+
experiment: experiment,
|
58
|
+
alternative: alternative_name,
|
59
|
+
goals: options[:goals],
|
60
|
+
)
|
61
|
+
|
62
62
|
trial.complete!(self)
|
63
63
|
|
64
64
|
if should_reset
|
@@ -69,7 +69,7 @@ module Split
|
|
69
69
|
end
|
70
70
|
end
|
71
71
|
|
72
|
-
def ab_finished(metric_descriptor, options = {:
|
72
|
+
def ab_finished(metric_descriptor, options = { reset: true })
|
73
73
|
return if exclude_visitor? || Split.configuration.disabled?
|
74
74
|
metric_descriptor, goals = normalize_metric(metric_descriptor)
|
75
75
|
experiments = Metric.possible_experiments(metric_descriptor)
|
@@ -77,7 +77,7 @@ module Split
|
|
77
77
|
if experiments.any?
|
78
78
|
experiments.each do |experiment|
|
79
79
|
next if override_present?(experiment.key)
|
80
|
-
finish_experiment(experiment, options.merge(:
|
80
|
+
finish_experiment(experiment, options.merge(goals: goals))
|
81
81
|
end
|
82
82
|
end
|
83
83
|
rescue => e
|
@@ -95,7 +95,7 @@ module Split
|
|
95
95
|
alternative_name = ab_user[experiment.key]
|
96
96
|
|
97
97
|
if alternative_name
|
98
|
-
alternative = experiment.alternatives.find{|alt| alt.name == alternative_name}
|
98
|
+
alternative = experiment.alternatives.find { |alt| alt.name == alternative_name }
|
99
99
|
alternative.record_extra_info(key, value) if alternative
|
100
100
|
end
|
101
101
|
end
|
@@ -105,7 +105,7 @@ module Split
|
|
105
105
|
Split.configuration.db_failover_on_db_error.call(e)
|
106
106
|
end
|
107
107
|
|
108
|
-
def ab_active_experiments
|
108
|
+
def ab_active_experiments
|
109
109
|
ab_user.active_experiments
|
110
110
|
rescue => e
|
111
111
|
raise unless Split.configuration.db_failover
|
@@ -127,14 +127,14 @@ module Split
|
|
127
127
|
def override_alternative_by_cookies(experiment_name)
|
128
128
|
return unless defined?(request)
|
129
129
|
|
130
|
-
if request.cookies && request.cookies.key?(
|
131
|
-
experiments = JSON.parse(request.cookies[
|
130
|
+
if request.cookies && request.cookies.key?("split_override")
|
131
|
+
experiments = JSON.parse(request.cookies["split_override"]) rescue {}
|
132
132
|
experiments[experiment_name]
|
133
133
|
end
|
134
134
|
end
|
135
135
|
|
136
136
|
def split_generically_disabled?
|
137
|
-
defined?(params) && params[
|
137
|
+
defined?(params) && params["SPLIT_DISABLE"]
|
138
138
|
end
|
139
139
|
|
140
140
|
def ab_user
|
@@ -150,7 +150,7 @@ module Split
|
|
150
150
|
end
|
151
151
|
|
152
152
|
def is_preview?
|
153
|
-
defined?(request) && defined?(request.headers) && request.headers[
|
153
|
+
defined?(request) && defined?(request.headers) && request.headers["x-purpose"] == "preview"
|
154
154
|
end
|
155
155
|
|
156
156
|
def is_ignored_ip_address?
|
data/lib/split/metric.rb
CHANGED
@@ -16,13 +16,13 @@ module Split
|
|
16
16
|
def self.load_from_redis(name)
|
17
17
|
metric = Split.redis.hget(:metrics, name)
|
18
18
|
if metric
|
19
|
-
experiment_names = metric.split(
|
19
|
+
experiment_names = metric.split(",")
|
20
20
|
|
21
21
|
experiments = experiment_names.collect do |experiment_name|
|
22
22
|
Split::ExperimentCatalog.find(experiment_name)
|
23
23
|
end
|
24
24
|
|
25
|
-
Split::Metric.new(:
|
25
|
+
Split::Metric.new(name: name, experiments: experiments)
|
26
26
|
else
|
27
27
|
nil
|
28
28
|
end
|
@@ -31,7 +31,7 @@ module Split
|
|
31
31
|
def self.load_from_configuration(name)
|
32
32
|
metrics = Split.configuration.metrics
|
33
33
|
if metrics && metrics[name]
|
34
|
-
Split::Metric.new(:
|
34
|
+
Split::Metric.new(experiments: metrics[name], name: name)
|
35
35
|
else
|
36
36
|
nil
|
37
37
|
end
|
@@ -77,7 +77,7 @@ module Split
|
|
77
77
|
end
|
78
78
|
|
79
79
|
def save
|
80
|
-
Split.redis.hset(:metrics, name, experiments.map(&:name).join(
|
80
|
+
Split.redis.hset(:metrics, name, experiments.map(&:name).join(","))
|
81
81
|
end
|
82
82
|
|
83
83
|
def complete!
|
@@ -97,6 +97,5 @@ module Split
|
|
97
97
|
return metric_name, goals
|
98
98
|
end
|
99
99
|
private_class_method :normalize_metric
|
100
|
-
|
101
100
|
end
|
102
101
|
end
|
@@ -5,7 +5,6 @@ require "json"
|
|
5
5
|
module Split
|
6
6
|
module Persistence
|
7
7
|
class CookieAdapter
|
8
|
-
|
9
8
|
def initialize(context)
|
10
9
|
@context = context
|
11
10
|
@request, @response = context.request, context.response
|
@@ -30,50 +29,49 @@ module Split
|
|
30
29
|
end
|
31
30
|
|
32
31
|
private
|
32
|
+
def set_cookie(value = {})
|
33
|
+
cookie_key = :split.to_s
|
34
|
+
cookie_value = default_options.merge(value: JSON.generate(value))
|
35
|
+
if action_dispatch?
|
36
|
+
# The "send" is necessary when we call ab_test from the controller
|
37
|
+
# and thus @context is a rails controller, because then "cookies" is
|
38
|
+
# a private method.
|
39
|
+
@context.send(:cookies)[cookie_key] = cookie_value
|
40
|
+
else
|
41
|
+
set_cookie_via_rack(cookie_key, cookie_value)
|
42
|
+
end
|
43
|
+
end
|
33
44
|
|
34
|
-
|
35
|
-
|
36
|
-
cookie_value = default_options.merge(value: JSON.generate(value))
|
37
|
-
if action_dispatch?
|
38
|
-
# The "send" is necessary when we call ab_test from the controller
|
39
|
-
# and thus @context is a rails controller, because then "cookies" is
|
40
|
-
# a private method.
|
41
|
-
@context.send(:cookies)[cookie_key] = cookie_value
|
42
|
-
else
|
43
|
-
set_cookie_via_rack(cookie_key, cookie_value)
|
45
|
+
def default_options
|
46
|
+
{ expires: @expires, path: "/", domain: cookie_domain_config }.compact
|
44
47
|
end
|
45
|
-
end
|
46
48
|
|
47
|
-
|
48
|
-
|
49
|
-
|
49
|
+
def set_cookie_via_rack(key, value)
|
50
|
+
delete_cookie_header!(@response.header, key, value)
|
51
|
+
Rack::Utils.set_cookie_header!(@response.header, key, value)
|
52
|
+
end
|
50
53
|
|
51
|
-
|
52
|
-
delete_cookie_header!(
|
53
|
-
|
54
|
-
|
54
|
+
# Use Rack::Utils#make_delete_cookie_header after Rack 2.0.0
|
55
|
+
def delete_cookie_header!(header, key, value)
|
56
|
+
cookie_header = header["Set-Cookie"]
|
57
|
+
case cookie_header
|
58
|
+
when nil, ""
|
59
|
+
cookies = []
|
60
|
+
when String
|
61
|
+
cookies = cookie_header.split("\n")
|
62
|
+
when Array
|
63
|
+
cookies = cookie_header
|
64
|
+
end
|
55
65
|
|
56
|
-
|
57
|
-
|
58
|
-
cookie_header = header['Set-Cookie']
|
59
|
-
case cookie_header
|
60
|
-
when nil, ''
|
61
|
-
cookies = []
|
62
|
-
when String
|
63
|
-
cookies = cookie_header.split("\n")
|
64
|
-
when Array
|
65
|
-
cookies = cookie_header
|
66
|
+
cookies.reject! { |cookie| cookie =~ /\A#{Rack::Utils.escape(key)}=/ }
|
67
|
+
header["Set-Cookie"] = cookies.join("\n")
|
66
68
|
end
|
67
69
|
|
68
|
-
|
69
|
-
|
70
|
-
end
|
71
|
-
|
72
|
-
def hash
|
73
|
-
@hash ||= begin
|
74
|
-
if cookies = @cookies[:split.to_s]
|
70
|
+
def hash
|
71
|
+
@hash ||= if cookies = @cookies[:split.to_s]
|
75
72
|
begin
|
76
|
-
JSON.parse(cookies)
|
73
|
+
parsed = JSON.parse(cookies)
|
74
|
+
parsed.is_a?(Hash) ? parsed : {}
|
77
75
|
rescue JSON::ParserError
|
78
76
|
{}
|
79
77
|
end
|
@@ -81,19 +79,18 @@ module Split
|
|
81
79
|
{}
|
82
80
|
end
|
83
81
|
end
|
84
|
-
end
|
85
82
|
|
86
|
-
|
87
|
-
|
88
|
-
|
83
|
+
def cookie_length_config
|
84
|
+
Split.configuration.persistence_cookie_length
|
85
|
+
end
|
89
86
|
|
90
|
-
|
91
|
-
|
92
|
-
|
87
|
+
def cookie_domain_config
|
88
|
+
Split.configuration.persistence_cookie_domain
|
89
|
+
end
|
93
90
|
|
94
|
-
|
95
|
-
|
96
|
-
|
91
|
+
def action_dispatch?
|
92
|
+
defined?(Rails) && @response.is_a?(ActionDispatch::Response)
|
93
|
+
end
|
97
94
|
end
|
98
95
|
end
|
99
96
|
end
|