split 3.2.0 → 4.0.5
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 +5 -5
- data/.eslintrc +1 -1
- data/.github/FUNDING.yml +1 -0
- data/.github/ISSUE_TEMPLATE/bug_report.md +24 -0
- data/.github/dependabot.yml +7 -0
- data/.github/workflows/ci.yml +63 -0
- data/.rspec +1 -0
- data/.rubocop.yml +67 -1043
- data/CHANGELOG.md +174 -0
- data/CODE_OF_CONDUCT.md +3 -3
- data/CONTRIBUTING.md +1 -1
- data/Gemfile +6 -1
- data/README.md +79 -33
- data/Rakefile +6 -5
- data/lib/split/algorithms/block_randomization.rb +7 -6
- data/lib/split/algorithms/weighted_sample.rb +2 -1
- data/lib/split/algorithms/whiplash.rb +17 -18
- data/lib/split/algorithms.rb +14 -0
- data/lib/split/alternative.rb +25 -25
- data/lib/split/cache.rb +27 -0
- data/lib/split/combined_experiments_helper.rb +6 -5
- data/lib/split/configuration.rb +94 -91
- data/lib/split/dashboard/helpers.rb +9 -9
- data/lib/split/dashboard/pagination_helpers.rb +86 -0
- data/lib/split/dashboard/paginator.rb +17 -0
- data/lib/split/dashboard/public/dashboard.js +10 -0
- data/lib/split/dashboard/public/style.css +19 -2
- data/lib/split/dashboard/views/_controls.erb +13 -0
- data/lib/split/dashboard/views/_experiment.erb +2 -1
- data/lib/split/dashboard/views/index.erb +24 -5
- data/lib/split/dashboard/views/layout.erb +1 -1
- data/lib/split/dashboard.rb +47 -20
- data/lib/split/encapsulated_helper.rb +15 -8
- data/lib/split/engine.rb +7 -4
- data/lib/split/exceptions.rb +1 -0
- data/lib/split/experiment.rb +160 -122
- data/lib/split/experiment_catalog.rb +7 -8
- data/lib/split/extensions/string.rb +2 -1
- data/lib/split/goals_collection.rb +10 -10
- data/lib/split/helper.rb +56 -24
- data/lib/split/metric.rb +6 -6
- data/lib/split/persistence/cookie_adapter.rb +52 -15
- data/lib/split/persistence/dual_adapter.rb +53 -12
- data/lib/split/persistence/redis_adapter.rb +8 -4
- data/lib/split/persistence/session_adapter.rb +1 -2
- data/lib/split/persistence.rb +8 -6
- data/lib/split/redis_interface.rb +16 -31
- data/lib/split/trial.rb +48 -41
- data/lib/split/user.rb +30 -15
- data/lib/split/version.rb +2 -4
- data/lib/split/zscore.rb +2 -3
- data/lib/split.rb +39 -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 +84 -0
- data/spec/combined_experiments_helper_spec.rb +18 -17
- data/spec/configuration_spec.rb +41 -45
- data/spec/dashboard/pagination_helpers_spec.rb +202 -0
- data/spec/dashboard/paginator_spec.rb +38 -0
- data/spec/dashboard_helpers_spec.rb +19 -18
- data/spec/dashboard_spec.rb +153 -48
- data/spec/encapsulated_helper_spec.rb +47 -23
- data/spec/experiment_catalog_spec.rb +14 -13
- data/spec/experiment_spec.rb +224 -111
- data/spec/goals_collection_spec.rb +18 -16
- data/spec/helper_spec.rb +539 -419
- data/spec/metric_spec.rb +14 -14
- data/spec/persistence/cookie_adapter_spec.rb +105 -27
- data/spec/persistence/dual_adapter_spec.rb +158 -66
- data/spec/persistence/redis_adapter_spec.rb +35 -27
- data/spec/persistence/session_adapter_spec.rb +2 -3
- data/spec/persistence_spec.rb +1 -2
- data/spec/redis_interface_spec.rb +25 -82
- data/spec/spec_helper.rb +38 -24
- data/spec/split_spec.rb +18 -18
- data/spec/support/cookies_mock.rb +1 -2
- data/spec/trial_spec.rb +117 -70
- data/spec/user_spec.rb +69 -27
- data/split.gemspec +26 -22
- metadata +85 -37
- data/.travis.yml +0 -41
- data/Appraisals +0 -13
- data/gemfiles/4.2.gemfile +0 -9
- data/gemfiles/5.0.gemfile +0 -10
- data/gemfiles/5.1.gemfile +0 -10
data/lib/split/trial.rb
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
module Split
|
|
3
4
|
class Trial
|
|
5
|
+
attr_accessor :goals
|
|
4
6
|
attr_accessor :experiment
|
|
5
|
-
|
|
7
|
+
attr_writer :metadata
|
|
6
8
|
|
|
7
9
|
def initialize(attrs = {})
|
|
8
10
|
self.experiment = attrs.delete(:experiment)
|
|
9
11
|
self.alternative = attrs.delete(:alternative)
|
|
10
12
|
self.metadata = attrs.delete(:metadata)
|
|
13
|
+
self.goals = attrs.delete(:goals) || []
|
|
11
14
|
|
|
12
15
|
@user = attrs.delete(:user)
|
|
13
16
|
@options = attrs
|
|
14
17
|
|
|
15
|
-
@
|
|
18
|
+
@alternative_chosen = false
|
|
16
19
|
end
|
|
17
20
|
|
|
18
21
|
def metadata
|
|
@@ -21,24 +24,24 @@ module Split
|
|
|
21
24
|
|
|
22
25
|
def alternative
|
|
23
26
|
@alternative ||= if @experiment.has_winner?
|
|
24
|
-
|
|
25
|
-
|
|
27
|
+
@experiment.winner
|
|
28
|
+
end
|
|
26
29
|
end
|
|
27
30
|
|
|
28
31
|
def alternative=(alternative)
|
|
29
32
|
@alternative = if alternative.kind_of?(Split::Alternative)
|
|
30
33
|
alternative
|
|
31
34
|
else
|
|
32
|
-
@experiment.alternatives.find{|a| a.name == alternative }
|
|
35
|
+
@experiment.alternatives.find { |a| a.name == alternative }
|
|
33
36
|
end
|
|
34
37
|
end
|
|
35
38
|
|
|
36
|
-
def complete!(
|
|
39
|
+
def complete!(context = nil)
|
|
37
40
|
if alternative
|
|
38
41
|
if Array(goals).empty?
|
|
39
42
|
alternative.increment_completion
|
|
40
43
|
else
|
|
41
|
-
Array(goals).each {|g| alternative.increment_completion(g) }
|
|
44
|
+
Array(goals).each { |g| alternative.increment_completion(g) }
|
|
42
45
|
end
|
|
43
46
|
|
|
44
47
|
run_callback context, Split.configuration.on_trial_complete
|
|
@@ -51,8 +54,9 @@ module Split
|
|
|
51
54
|
def choose!(context = nil)
|
|
52
55
|
@user.cleanup_old_experiments!
|
|
53
56
|
# Only run the process once
|
|
54
|
-
return alternative if @
|
|
57
|
+
return alternative if @alternative_chosen
|
|
55
58
|
|
|
59
|
+
new_participant = @user[@experiment.key].nil?
|
|
56
60
|
if override_is_alternative?
|
|
57
61
|
self.alternative = @options[:override]
|
|
58
62
|
if should_store_alternative? && !@user[@experiment.key]
|
|
@@ -68,52 +72,55 @@ module Split
|
|
|
68
72
|
if exclude_user?
|
|
69
73
|
self.alternative = @experiment.control
|
|
70
74
|
else
|
|
71
|
-
|
|
72
|
-
if
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
75
|
+
self.alternative = @user[@experiment.key]
|
|
76
|
+
if alternative.nil?
|
|
77
|
+
if @experiment.cohorting_disabled?
|
|
78
|
+
self.alternative = @experiment.control
|
|
79
|
+
else
|
|
80
|
+
self.alternative = @experiment.next_alternative
|
|
81
|
+
|
|
82
|
+
# Increment the number of participants since we are actually choosing a new alternative
|
|
83
|
+
self.alternative.increment_participation
|
|
84
|
+
|
|
85
|
+
run_callback context, Split.configuration.on_trial_choose
|
|
86
|
+
end
|
|
81
87
|
end
|
|
82
88
|
end
|
|
83
89
|
end
|
|
84
90
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
91
|
+
new_participant_and_cohorting_disabled = new_participant && @experiment.cohorting_disabled?
|
|
92
|
+
|
|
93
|
+
@user[@experiment.key] = alternative.name unless @experiment.has_winner? || !should_store_alternative? || new_participant_and_cohorting_disabled
|
|
94
|
+
@alternative_chosen = true
|
|
95
|
+
run_callback context, Split.configuration.on_trial unless @options[:disabled] || Split.configuration.disabled? || new_participant_and_cohorting_disabled
|
|
88
96
|
alternative
|
|
89
97
|
end
|
|
90
98
|
|
|
91
99
|
private
|
|
100
|
+
def run_callback(context, callback_name)
|
|
101
|
+
context.send(callback_name, self) if callback_name && context.respond_to?(callback_name, true)
|
|
102
|
+
end
|
|
92
103
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
def override_is_alternative?
|
|
98
|
-
@experiment.alternatives.map(&:name).include?(@options[:override])
|
|
99
|
-
end
|
|
104
|
+
def override_is_alternative?
|
|
105
|
+
@experiment.alternatives.map(&:name).include?(@options[:override])
|
|
106
|
+
end
|
|
100
107
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
108
|
+
def should_store_alternative?
|
|
109
|
+
if @options[:override] || @options[:disabled]
|
|
110
|
+
Split.configuration.store_override
|
|
111
|
+
else
|
|
112
|
+
!exclude_user?
|
|
113
|
+
end
|
|
106
114
|
end
|
|
107
|
-
end
|
|
108
115
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
116
|
+
def cleanup_old_versions
|
|
117
|
+
if @experiment.version > 0
|
|
118
|
+
@user.cleanup_old_versions!(@experiment)
|
|
119
|
+
end
|
|
112
120
|
end
|
|
113
|
-
end
|
|
114
121
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
122
|
+
def exclude_user?
|
|
123
|
+
@options[:exclude] || @experiment.start_time.nil? || @user.max_experiments_reached?(@experiment.key)
|
|
124
|
+
end
|
|
118
125
|
end
|
|
119
126
|
end
|
data/lib/split/user.rb
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "forwardable"
|
|
2
4
|
|
|
3
5
|
module Split
|
|
4
6
|
class User
|
|
@@ -6,11 +8,13 @@ module Split
|
|
|
6
8
|
def_delegators :@user, :keys, :[], :[]=, :delete
|
|
7
9
|
attr_reader :user
|
|
8
10
|
|
|
9
|
-
def initialize(context, adapter=nil)
|
|
11
|
+
def initialize(context, adapter = nil)
|
|
10
12
|
@user = adapter || Split::Persistence.adapter.new(context)
|
|
13
|
+
@cleaned_up = false
|
|
11
14
|
end
|
|
12
15
|
|
|
13
16
|
def cleanup_old_experiments!
|
|
17
|
+
return if @cleaned_up
|
|
14
18
|
keys_without_finished(user.keys).each do |key|
|
|
15
19
|
experiment = ExperimentCatalog.find key_without_version(key)
|
|
16
20
|
if experiment.nil? || experiment.has_winner? || experiment.start_time.nil?
|
|
@@ -18,12 +22,14 @@ module Split
|
|
|
18
22
|
user.delete Experiment.finished_key(key)
|
|
19
23
|
end
|
|
20
24
|
end
|
|
25
|
+
@cleaned_up = true
|
|
21
26
|
end
|
|
22
27
|
|
|
23
28
|
def max_experiments_reached?(experiment_key)
|
|
24
|
-
if Split.configuration.allow_multiple_experiments ==
|
|
29
|
+
if Split.configuration.allow_multiple_experiments == "control"
|
|
25
30
|
experiments = active_experiments
|
|
26
|
-
|
|
31
|
+
experiment_key_without_version = key_without_version(experiment_key)
|
|
32
|
+
count_control = experiments.count { |k, v| k == experiment_key_without_version || v == "control" }
|
|
27
33
|
experiments.size > count_control
|
|
28
34
|
else
|
|
29
35
|
!Split.configuration.allow_multiple_experiments &&
|
|
@@ -32,13 +38,13 @@ module Split
|
|
|
32
38
|
end
|
|
33
39
|
|
|
34
40
|
def cleanup_old_versions!(experiment)
|
|
35
|
-
keys = user.keys.select { |k| k
|
|
41
|
+
keys = user.keys.select { |k| key_without_version(k) == experiment.name }
|
|
36
42
|
keys_without_experiment(keys, experiment.key).each { |key| user.delete(key) }
|
|
37
43
|
end
|
|
38
44
|
|
|
39
45
|
def active_experiments
|
|
40
46
|
experiment_pairs = {}
|
|
41
|
-
user.keys.each do |key|
|
|
47
|
+
keys_without_finished(user.keys).each do |key|
|
|
42
48
|
Metric.possible_experiments(key_without_version(key)).each do |experiment|
|
|
43
49
|
if !experiment.has_winner?
|
|
44
50
|
experiment_pairs[key_without_version(key)] = user[key]
|
|
@@ -48,18 +54,27 @@ module Split
|
|
|
48
54
|
experiment_pairs
|
|
49
55
|
end
|
|
50
56
|
|
|
51
|
-
|
|
57
|
+
def self.find(user_id, adapter)
|
|
58
|
+
adapter = adapter.is_a?(Symbol) ? Split::Persistence::ADAPTERS[adapter] : adapter
|
|
52
59
|
|
|
53
|
-
|
|
54
|
-
|
|
60
|
+
if adapter.respond_to?(:find)
|
|
61
|
+
User.new(nil, adapter.find(user_id))
|
|
62
|
+
else
|
|
63
|
+
nil
|
|
64
|
+
end
|
|
55
65
|
end
|
|
56
66
|
|
|
57
|
-
|
|
58
|
-
keys
|
|
59
|
-
|
|
67
|
+
private
|
|
68
|
+
def keys_without_experiment(keys, experiment_key)
|
|
69
|
+
keys.reject { |k| k.match(Regexp.new("^#{experiment_key}(:finished)?$")) }
|
|
70
|
+
end
|
|
60
71
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
72
|
+
def keys_without_finished(keys)
|
|
73
|
+
keys.reject { |k| k.include?(":finished") }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def key_without_version(key)
|
|
77
|
+
key.split(/\:\d(?!\:)/)[0]
|
|
78
|
+
end
|
|
64
79
|
end
|
|
65
80
|
end
|
data/lib/split/version.rb
CHANGED
data/lib/split/zscore.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
module Split
|
|
3
4
|
class Zscore
|
|
4
|
-
|
|
5
5
|
include Math
|
|
6
6
|
|
|
7
7
|
def self.calculate(p1, n1, p2, n2)
|
|
@@ -50,8 +50,7 @@ module Split
|
|
|
50
50
|
# Calculate z-score
|
|
51
51
|
z_score = (p_1 - p_2)/(se)
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
z_score
|
|
55
54
|
end
|
|
56
55
|
end
|
|
57
56
|
end
|
data/lib/split.rb
CHANGED
|
@@ -1,27 +1,30 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
-
require 'redis'
|
|
3
2
|
|
|
4
|
-
require
|
|
5
|
-
|
|
6
|
-
require
|
|
7
|
-
require
|
|
8
|
-
require
|
|
9
|
-
require
|
|
10
|
-
require
|
|
11
|
-
require
|
|
12
|
-
require
|
|
13
|
-
require
|
|
14
|
-
require
|
|
15
|
-
require
|
|
16
|
-
require
|
|
17
|
-
require
|
|
18
|
-
require
|
|
19
|
-
require
|
|
20
|
-
require
|
|
21
|
-
require
|
|
22
|
-
require
|
|
23
|
-
require
|
|
24
|
-
require
|
|
3
|
+
require "redis"
|
|
4
|
+
|
|
5
|
+
require "split/algorithms"
|
|
6
|
+
require "split/algorithms/block_randomization"
|
|
7
|
+
require "split/algorithms/weighted_sample"
|
|
8
|
+
require "split/algorithms/whiplash"
|
|
9
|
+
require "split/alternative"
|
|
10
|
+
require "split/cache"
|
|
11
|
+
require "split/configuration"
|
|
12
|
+
require "split/encapsulated_helper"
|
|
13
|
+
require "split/exceptions"
|
|
14
|
+
require "split/experiment"
|
|
15
|
+
require "split/experiment_catalog"
|
|
16
|
+
require "split/extensions/string"
|
|
17
|
+
require "split/goals_collection"
|
|
18
|
+
require "split/helper"
|
|
19
|
+
require "split/combined_experiments_helper"
|
|
20
|
+
require "split/metric"
|
|
21
|
+
require "split/persistence"
|
|
22
|
+
require "split/redis_interface"
|
|
23
|
+
require "split/trial"
|
|
24
|
+
require "split/user"
|
|
25
|
+
require "split/version"
|
|
26
|
+
require "split/zscore"
|
|
27
|
+
require "split/engine" if defined?(::Rails::Engine)
|
|
25
28
|
|
|
26
29
|
module Split
|
|
27
30
|
extend self
|
|
@@ -35,9 +38,9 @@ module Split
|
|
|
35
38
|
# `Redis::DistRedis`, or `Redis::Namespace`.
|
|
36
39
|
def redis=(server)
|
|
37
40
|
@redis = if server.is_a?(String)
|
|
38
|
-
Redis.new(:
|
|
41
|
+
Redis.new(url: server)
|
|
39
42
|
elsif server.is_a?(Hash)
|
|
40
|
-
Redis.new(server
|
|
43
|
+
Redis.new(server)
|
|
41
44
|
elsif server.respond_to?(:smembers)
|
|
42
45
|
server
|
|
43
46
|
else
|
|
@@ -64,6 +67,17 @@ module Split
|
|
|
64
67
|
self.configuration ||= Configuration.new
|
|
65
68
|
yield(configuration)
|
|
66
69
|
end
|
|
70
|
+
|
|
71
|
+
def cache(namespace, key, &block)
|
|
72
|
+
Split::Cache.fetch(namespace, key, &block)
|
|
73
|
+
end
|
|
67
74
|
end
|
|
68
75
|
|
|
69
|
-
|
|
76
|
+
# Check to see if being run in a Rails application. If so, wait until before_initialize to run configuration so Gems that create ENV variables have the chance to initialize first.
|
|
77
|
+
if defined?(::Rails::Railtie)
|
|
78
|
+
class Split::Railtie < Rails::Railtie
|
|
79
|
+
config.before_initialize { Split.configure { } }
|
|
80
|
+
end
|
|
81
|
+
else
|
|
82
|
+
Split.configure { }
|
|
83
|
+
end
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require "spec_helper"
|
|
2
4
|
|
|
3
5
|
describe Split::Algorithms::BlockRandomization do
|
|
4
|
-
|
|
5
|
-
let(:
|
|
6
|
-
let(:
|
|
7
|
-
let(:
|
|
8
|
-
let(:alternative_C) { Split::Alternative.new 'C', 'experiment' }
|
|
6
|
+
let(:experiment) { Split::Experiment.new "experiment" }
|
|
7
|
+
let(:alternative_A) { Split::Alternative.new "A", "experiment" }
|
|
8
|
+
let(:alternative_B) { Split::Alternative.new "B", "experiment" }
|
|
9
|
+
let(:alternative_C) { Split::Alternative.new "C", "experiment" }
|
|
9
10
|
|
|
10
11
|
before :each do
|
|
11
12
|
allow(experiment).to receive(:alternatives) { [alternative_A, alternative_B, alternative_C] }
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
require "spec_helper"
|
|
3
4
|
|
|
4
5
|
describe Split::Algorithms::WeightedSample do
|
|
5
6
|
it "should return an alternative" do
|
|
6
|
-
experiment = Split::ExperimentCatalog.find_or_create(
|
|
7
|
+
experiment = Split::ExperimentCatalog.find_or_create("link_color", { "blue" => 100 }, { "red" => 0 })
|
|
7
8
|
expect(Split::Algorithms::WeightedSample.choose_alternative(experiment).class).to eq(Split::Alternative)
|
|
8
9
|
end
|
|
9
10
|
|
|
10
11
|
it "should always return a heavily weighted option" do
|
|
11
|
-
experiment = Split::ExperimentCatalog.find_or_create(
|
|
12
|
-
expect(Split::Algorithms::WeightedSample.choose_alternative(experiment).name).to eq(
|
|
12
|
+
experiment = Split::ExperimentCatalog.find_or_create("link_color", { "blue" => 100 }, { "red" => 0 })
|
|
13
|
+
expect(Split::Algorithms::WeightedSample.choose_alternative(experiment).name).to eq("blue")
|
|
13
14
|
end
|
|
14
15
|
|
|
15
16
|
it "should return one of the results" do
|
|
16
|
-
experiment = Split::ExperimentCatalog.find_or_create(
|
|
17
|
-
expect([
|
|
17
|
+
experiment = Split::ExperimentCatalog.find_or_create("link_color", { "blue" => 1 }, { "red" => 1 })
|
|
18
|
+
expect(["red", "blue"]).to include Split::Algorithms::WeightedSample.choose_alternative(experiment).name
|
|
18
19
|
end
|
|
19
20
|
end
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
require "spec_helper"
|
|
3
4
|
|
|
4
5
|
describe Split::Algorithms::Whiplash do
|
|
5
|
-
|
|
6
6
|
it "should return an algorithm" do
|
|
7
|
-
experiment = Split::ExperimentCatalog.find_or_create(
|
|
7
|
+
experiment = Split::ExperimentCatalog.find_or_create("link_color", { "blue" => 1 }, { "red" => 1 })
|
|
8
8
|
expect(Split::Algorithms::Whiplash.choose_alternative(experiment).class).to eq(Split::Alternative)
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
it "should return one of the results" do
|
|
12
|
-
experiment = Split::ExperimentCatalog.find_or_create(
|
|
13
|
-
expect([
|
|
12
|
+
experiment = Split::ExperimentCatalog.find_or_create("link_color", { "blue" => 1 }, { "red" => 1 })
|
|
13
|
+
expect(["red", "blue"]).to include Split::Algorithms::Whiplash.choose_alternative(experiment).name
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
it "should guess floats" do
|
|
@@ -20,5 +20,4 @@ describe Split::Algorithms::Whiplash do
|
|
|
20
20
|
expect(Split::Algorithms::Whiplash.send(:arm_guess, 1000, 5).class).to eq(Float)
|
|
21
21
|
expect(Split::Algorithms::Whiplash.send(:arm_guess, 10, -2).class).to eq(Float)
|
|
22
22
|
end
|
|
23
|
-
|
|
24
23
|
end
|
data/spec/alternative_spec.rb
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
-
require 'spec_helper'
|
|
3
|
-
require 'split/alternative'
|
|
4
2
|
|
|
5
|
-
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "split/alternative"
|
|
6
5
|
|
|
6
|
+
describe Split::Alternative do
|
|
7
7
|
let(:alternative) {
|
|
8
|
-
Split::Alternative.new(
|
|
8
|
+
Split::Alternative.new("Basket", "basket_text")
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
let(:alternative2) {
|
|
12
|
-
Split::Alternative.new(
|
|
12
|
+
Split::Alternative.new("Cart", "basket_text")
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
let!(:experiment) {
|
|
16
|
-
Split::ExperimentCatalog.find_or_create({"basket_text" => ["purchase", "refund"]}, "Basket", "Cart")
|
|
16
|
+
Split::ExperimentCatalog.find_or_create({ "basket_text" => ["purchase", "refund"] }, "Basket", "Cart")
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
let(:goal1) { "purchase" }
|
|
@@ -24,48 +24,48 @@ describe Split::Alternative do
|
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
it "should have and only return the name" do
|
|
27
|
-
expect(alternative.name).to eq(
|
|
27
|
+
expect(alternative.name).to eq("Basket")
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
-
describe
|
|
30
|
+
describe "weights" do
|
|
31
31
|
it "should set the weights" do
|
|
32
|
-
experiment = Split::Experiment.new(
|
|
32
|
+
experiment = Split::Experiment.new("basket_text", alternatives: [{ "Basket" => 0.6 }, { "Cart" => 0.4 }])
|
|
33
33
|
first = experiment.alternatives[0]
|
|
34
|
-
expect(first.name).to eq(
|
|
34
|
+
expect(first.name).to eq("Basket")
|
|
35
35
|
expect(first.weight).to eq(0.6)
|
|
36
36
|
|
|
37
37
|
second = experiment.alternatives[1]
|
|
38
|
-
expect(second.name).to eq(
|
|
38
|
+
expect(second.name).to eq("Cart")
|
|
39
39
|
expect(second.weight).to eq(0.4)
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
it "accepts probability on alternatives" do
|
|
43
43
|
Split.configuration.experiments = {
|
|
44
|
-
:
|
|
45
|
-
:
|
|
46
|
-
{ :
|
|
47
|
-
{ :
|
|
48
|
-
{ :
|
|
44
|
+
my_experiment: {
|
|
45
|
+
alternatives: [
|
|
46
|
+
{ name: "control_opt", percent: 67 },
|
|
47
|
+
{ name: "second_opt", percent: 10 },
|
|
48
|
+
{ name: "third_opt", percent: 23 },
|
|
49
49
|
]
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
experiment = Split::Experiment.new(:my_experiment)
|
|
53
53
|
first = experiment.alternatives[0]
|
|
54
|
-
expect(first.name).to eq(
|
|
54
|
+
expect(first.name).to eq("control_opt")
|
|
55
55
|
expect(first.weight).to eq(0.67)
|
|
56
56
|
|
|
57
57
|
second = experiment.alternatives[1]
|
|
58
|
-
expect(second.name).to eq(
|
|
58
|
+
expect(second.name).to eq("second_opt")
|
|
59
59
|
expect(second.weight).to eq(0.1)
|
|
60
60
|
end
|
|
61
61
|
|
|
62
62
|
it "accepts probability on some alternatives" do
|
|
63
63
|
Split.configuration.experiments = {
|
|
64
|
-
:
|
|
65
|
-
:
|
|
66
|
-
{ :
|
|
64
|
+
my_experiment: {
|
|
65
|
+
alternatives: [
|
|
66
|
+
{ name: "control_opt", percent: 34 },
|
|
67
67
|
"second_opt",
|
|
68
|
-
{ :
|
|
68
|
+
{ name: "third_opt", percent: 23 },
|
|
69
69
|
"fourth_opt",
|
|
70
70
|
],
|
|
71
71
|
}
|
|
@@ -87,11 +87,11 @@ describe Split::Alternative do
|
|
|
87
87
|
#
|
|
88
88
|
it "allows name param without probability" do
|
|
89
89
|
Split.configuration.experiments = {
|
|
90
|
-
:
|
|
91
|
-
:
|
|
92
|
-
{ :
|
|
90
|
+
my_experiment: {
|
|
91
|
+
alternatives: [
|
|
92
|
+
{ name: "control_opt" },
|
|
93
93
|
"second_opt",
|
|
94
|
-
{ :
|
|
94
|
+
{ name: "third_opt", percent: 64 },
|
|
95
95
|
],
|
|
96
96
|
}
|
|
97
97
|
}
|
|
@@ -126,7 +126,7 @@ describe Split::Alternative do
|
|
|
126
126
|
|
|
127
127
|
it "should save to redis" do
|
|
128
128
|
alternative.save
|
|
129
|
-
expect(Split.redis.exists(
|
|
129
|
+
expect(Split.redis.exists?("basket_text:Basket")).to be true
|
|
130
130
|
end
|
|
131
131
|
|
|
132
132
|
it "should increment participation count" do
|
|
@@ -166,7 +166,7 @@ describe Split::Alternative do
|
|
|
166
166
|
expect(alternative2.control?).to be_falsey
|
|
167
167
|
end
|
|
168
168
|
|
|
169
|
-
describe
|
|
169
|
+
describe "unfinished_count" do
|
|
170
170
|
it "should be difference between participant and completed counts" do
|
|
171
171
|
alternative.increment_participation
|
|
172
172
|
expect(alternative.unfinished_count).to eq(alternative.participant_count)
|
|
@@ -182,7 +182,7 @@ describe Split::Alternative do
|
|
|
182
182
|
end
|
|
183
183
|
end
|
|
184
184
|
|
|
185
|
-
describe
|
|
185
|
+
describe "conversion rate" do
|
|
186
186
|
it "should be 0 if there are no conversions" do
|
|
187
187
|
expect(alternative.completed_count).to eq(0)
|
|
188
188
|
expect(alternative.conversion_rate).to eq(0)
|
|
@@ -225,8 +225,7 @@ describe Split::Alternative do
|
|
|
225
225
|
end
|
|
226
226
|
end
|
|
227
227
|
|
|
228
|
-
describe
|
|
229
|
-
|
|
228
|
+
describe "z score" do
|
|
230
229
|
it "should return an error string when the control has 0 people" do
|
|
231
230
|
expect(alternative2.z_score).to eq("Needs 30+ participants.")
|
|
232
231
|
expect(alternative2.z_score(goal1)).to eq("Needs 30+ participants.")
|
|
@@ -269,9 +268,9 @@ describe Split::Alternative do
|
|
|
269
268
|
|
|
270
269
|
it "should be N/A for the control" do
|
|
271
270
|
control = experiment.control
|
|
272
|
-
expect(control.z_score).to eq(
|
|
273
|
-
expect(control.z_score(goal1)).to eq(
|
|
274
|
-
expect(control.z_score(goal2)).to eq(
|
|
271
|
+
expect(control.z_score).to eq("N/A")
|
|
272
|
+
expect(control.z_score(goal1)).to eq("N/A")
|
|
273
|
+
expect(control.z_score(goal2)).to eq("N/A")
|
|
275
274
|
end
|
|
276
275
|
|
|
277
276
|
it "should not blow up for Conversion Rates > 1" do
|
|
@@ -289,8 +288,8 @@ describe Split::Alternative do
|
|
|
289
288
|
|
|
290
289
|
describe "extra_info" do
|
|
291
290
|
it "reads saved value of recorded_info in redis" do
|
|
292
|
-
saved_recorded_info = {"key_1" => 1, "key_2" => "2"}
|
|
293
|
-
Split.redis.hset "#{alternative.experiment_name}:#{alternative.name}",
|
|
291
|
+
saved_recorded_info = { "key_1" => 1, "key_2" => "2" }
|
|
292
|
+
Split.redis.hset "#{alternative.experiment_name}:#{alternative.name}", "recorded_info", saved_recorded_info.to_json
|
|
294
293
|
extra_info = alternative.extra_info
|
|
295
294
|
|
|
296
295
|
expect(extra_info).to eql(saved_recorded_info)
|