split 3.4.1 → 4.0.4
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/FUNDING.yml +1 -0
- data/.github/dependabot.yml +7 -0
- data/.github/workflows/ci.yml +76 -0
- data/.rubocop.yml +177 -4
- data/CHANGELOG.md +87 -0
- data/CONTRIBUTING.md +1 -1
- data/Gemfile +2 -1
- data/README.md +37 -9
- data/Rakefile +5 -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} +2 -4
- data/lib/split/algorithms/block_randomization.rb +6 -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 +22 -22
- data/lib/split/cache.rb +27 -0
- data/lib/split/combined_experiments_helper.rb +5 -4
- data/lib/split/configuration.rb +89 -94
- data/lib/split/dashboard/helpers.rb +7 -7
- data/lib/split/dashboard/pagination_helpers.rb +54 -54
- data/lib/split/dashboard/paginator.rb +1 -0
- data/lib/split/dashboard/public/dashboard.js +10 -0
- data/lib/split/dashboard/public/style.css +10 -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 +19 -4
- data/lib/split/dashboard.rb +42 -21
- data/lib/split/encapsulated_helper.rb +15 -8
- data/lib/split/engine.rb +1 -0
- data/lib/split/exceptions.rb +1 -0
- data/lib/split/experiment.rb +151 -124
- data/lib/split/experiment_catalog.rb +7 -8
- data/lib/split/extensions/string.rb +2 -1
- data/lib/split/goals_collection.rb +9 -10
- data/lib/split/helper.rb +50 -23
- data/lib/split/metric.rb +6 -6
- data/lib/split/persistence/cookie_adapter.rb +46 -44
- data/lib/split/persistence/dual_adapter.rb +7 -8
- 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 +15 -29
- data/lib/split/trial.rb +43 -34
- data/lib/split/user.rb +25 -14
- data/lib/split/version.rb +2 -4
- data/lib/split/zscore.rb +2 -3
- data/lib/split.rb +34 -27
- 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 +69 -67
- data/spec/dashboard/paginator_spec.rb +10 -9
- data/spec/dashboard_helpers_spec.rb +19 -18
- data/spec/dashboard_spec.rb +122 -38
- data/spec/encapsulated_helper_spec.rb +46 -22
- data/spec/experiment_catalog_spec.rb +14 -13
- data/spec/experiment_spec.rb +198 -118
- data/spec/goals_collection_spec.rb +18 -16
- data/spec/helper_spec.rb +454 -385
- data/spec/metric_spec.rb +14 -14
- data/spec/persistence/cookie_adapter_spec.rb +26 -11
- data/spec/persistence/dual_adapter_spec.rb +71 -71
- 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 +35 -24
- data/spec/split_spec.rb +11 -11
- data/spec/support/cookies_mock.rb +1 -2
- data/spec/trial_spec.rb +102 -75
- data/spec/user_spec.rb +60 -29
- data/split.gemspec +22 -21
- metadata +43 -40
- data/.rubocop_todo.yml +0 -679
- data/.travis.yml +0 -60
- data/Appraisals +0 -19
- data/gemfiles/4.2.gemfile +0 -9
data/spec/experiment_spec.rb
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "time"
|
|
4
5
|
|
|
5
6
|
describe Split::Experiment do
|
|
6
|
-
def new_experiment(goals=[])
|
|
7
|
-
Split::Experiment.new(
|
|
7
|
+
def new_experiment(goals = [])
|
|
8
|
+
Split::Experiment.new("link_color", alternatives: ["blue", "red", "green"], goals: goals)
|
|
8
9
|
end
|
|
9
10
|
|
|
10
11
|
def alternative(color)
|
|
11
|
-
Split::Alternative.new(color,
|
|
12
|
+
Split::Alternative.new(color, "link_color")
|
|
12
13
|
end
|
|
13
14
|
|
|
14
15
|
let(:experiment) { new_experiment }
|
|
@@ -17,10 +18,10 @@ describe Split::Experiment do
|
|
|
17
18
|
let(:green) { alternative("green") }
|
|
18
19
|
|
|
19
20
|
context "with an experiment" do
|
|
20
|
-
let(:experiment) { Split::Experiment.new(
|
|
21
|
+
let(:experiment) { Split::Experiment.new("basket_text", alternatives: ["Basket", "Cart"]) }
|
|
21
22
|
|
|
22
23
|
it "should have a name" do
|
|
23
|
-
expect(experiment.name).to eq(
|
|
24
|
+
expect(experiment.name).to eq("basket_text")
|
|
24
25
|
end
|
|
25
26
|
|
|
26
27
|
it "should have alternatives" do
|
|
@@ -28,7 +29,7 @@ describe Split::Experiment do
|
|
|
28
29
|
end
|
|
29
30
|
|
|
30
31
|
it "should have alternatives with correct names" do
|
|
31
|
-
expect(experiment.alternatives.collect{|a| a.name}).to eq([
|
|
32
|
+
expect(experiment.alternatives.collect { |a| a.name }).to eq(["Basket", "Cart"])
|
|
32
33
|
end
|
|
33
34
|
|
|
34
35
|
it "should be resettable by default" do
|
|
@@ -37,7 +38,7 @@ describe Split::Experiment do
|
|
|
37
38
|
|
|
38
39
|
it "should save to redis" do
|
|
39
40
|
experiment.save
|
|
40
|
-
expect(Split.redis.exists(
|
|
41
|
+
expect(Split.redis.exists?("basket_text")).to be true
|
|
41
42
|
end
|
|
42
43
|
|
|
43
44
|
it "should save the start time to redis" do
|
|
@@ -45,14 +46,14 @@ describe Split::Experiment do
|
|
|
45
46
|
expect(Time).to receive(:now).and_return(experiment_start_time)
|
|
46
47
|
experiment.save
|
|
47
48
|
|
|
48
|
-
expect(Split::ExperimentCatalog.find(
|
|
49
|
+
expect(Split::ExperimentCatalog.find("basket_text").start_time).to eq(experiment_start_time)
|
|
49
50
|
end
|
|
50
51
|
|
|
51
52
|
it "should not save the start time to redis when start_manually is enabled" do
|
|
52
53
|
expect(Split.configuration).to receive(:start_manually).and_return(true)
|
|
53
54
|
experiment.save
|
|
54
55
|
|
|
55
|
-
expect(Split::ExperimentCatalog.find(
|
|
56
|
+
expect(Split::ExperimentCatalog.find("basket_text").start_time).to be_nil
|
|
56
57
|
end
|
|
57
58
|
|
|
58
59
|
it "should save the selected algorithm to redis" do
|
|
@@ -60,16 +61,16 @@ describe Split::Experiment do
|
|
|
60
61
|
experiment.algorithm = experiment_algorithm
|
|
61
62
|
experiment.save
|
|
62
63
|
|
|
63
|
-
expect(Split::ExperimentCatalog.find(
|
|
64
|
+
expect(Split::ExperimentCatalog.find("basket_text").algorithm).to eq(experiment_algorithm)
|
|
64
65
|
end
|
|
65
66
|
|
|
66
67
|
it "should handle having a start time stored as a string" do
|
|
67
68
|
experiment_start_time = Time.parse("Sat Mar 03 14:01:03")
|
|
68
69
|
expect(Time).to receive(:now).twice.and_return(experiment_start_time)
|
|
69
70
|
experiment.save
|
|
70
|
-
Split.redis.hset(:experiment_start_times, experiment.name, experiment_start_time)
|
|
71
|
+
Split.redis.hset(:experiment_start_times, experiment.name, experiment_start_time.to_s)
|
|
71
72
|
|
|
72
|
-
expect(Split::ExperimentCatalog.find(
|
|
73
|
+
expect(Split::ExperimentCatalog.find("basket_text").start_time).to eq(experiment_start_time)
|
|
73
74
|
end
|
|
74
75
|
|
|
75
76
|
it "should handle not having a start time" do
|
|
@@ -79,17 +80,17 @@ describe Split::Experiment do
|
|
|
79
80
|
|
|
80
81
|
Split.redis.hdel(:experiment_start_times, experiment.name)
|
|
81
82
|
|
|
82
|
-
expect(Split::ExperimentCatalog.find(
|
|
83
|
+
expect(Split::ExperimentCatalog.find("basket_text").start_time).to be_nil
|
|
83
84
|
end
|
|
84
85
|
|
|
85
86
|
it "should not create duplicates when saving multiple times" do
|
|
86
87
|
experiment.save
|
|
87
88
|
experiment.save
|
|
88
|
-
expect(Split.redis.exists(
|
|
89
|
-
expect(Split.redis.lrange(
|
|
89
|
+
expect(Split.redis.exists?("basket_text")).to be true
|
|
90
|
+
expect(Split.redis.lrange("basket_text", 0, -1)).to eq(['{"Basket":1}', '{"Cart":1}'])
|
|
90
91
|
end
|
|
91
92
|
|
|
92
|
-
describe
|
|
93
|
+
describe "new record?" do
|
|
93
94
|
it "should know if it hasn't been saved yet" do
|
|
94
95
|
expect(experiment.new_record?).to be_truthy
|
|
95
96
|
end
|
|
@@ -100,55 +101,82 @@ describe Split::Experiment do
|
|
|
100
101
|
end
|
|
101
102
|
end
|
|
102
103
|
|
|
103
|
-
describe
|
|
104
|
-
it
|
|
104
|
+
describe "control" do
|
|
105
|
+
it "should be the first alternative" do
|
|
105
106
|
experiment.save
|
|
106
|
-
expect(experiment.control.name).to eq(
|
|
107
|
+
expect(experiment.control.name).to eq("Basket")
|
|
107
108
|
end
|
|
108
109
|
end
|
|
109
110
|
end
|
|
110
111
|
|
|
111
|
-
describe
|
|
112
|
+
describe "initialization" do
|
|
112
113
|
it "should set the algorithm when passed as an option to the initializer" do
|
|
113
|
-
|
|
114
|
-
|
|
114
|
+
experiment = Split::Experiment.new("basket_text", alternatives: ["Basket", "Cart"], algorithm: Split::Algorithms::Whiplash)
|
|
115
|
+
expect(experiment.algorithm).to eq(Split::Algorithms::Whiplash)
|
|
115
116
|
end
|
|
116
117
|
|
|
117
118
|
it "should be possible to make an experiment not resettable" do
|
|
118
|
-
experiment = Split::Experiment.new(
|
|
119
|
+
experiment = Split::Experiment.new("basket_text", alternatives: ["Basket", "Cart"], resettable: false)
|
|
119
120
|
expect(experiment.resettable).to be_falsey
|
|
120
121
|
end
|
|
121
|
-
end
|
|
122
122
|
|
|
123
|
-
|
|
123
|
+
context "from configuration" do
|
|
124
|
+
let(:experiment_name) { :my_experiment }
|
|
125
|
+
let(:experiments) do
|
|
126
|
+
{
|
|
127
|
+
experiment_name => {
|
|
128
|
+
alternatives: ["Control Opt", "Alt one"]
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
before { Split.configuration.experiments = experiments }
|
|
134
|
+
|
|
135
|
+
it "assigns default values to the experiment" do
|
|
136
|
+
expect(Split::Experiment.new(experiment_name).resettable).to eq(true)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
124
140
|
|
|
141
|
+
describe "persistent configuration" do
|
|
125
142
|
it "should persist resettable in redis" do
|
|
126
|
-
experiment = Split::Experiment.new(
|
|
143
|
+
experiment = Split::Experiment.new("basket_text", alternatives: ["Basket", "Cart"], resettable: false)
|
|
127
144
|
experiment.save
|
|
128
145
|
|
|
129
|
-
e = Split::ExperimentCatalog.find(
|
|
146
|
+
e = Split::ExperimentCatalog.find("basket_text")
|
|
130
147
|
expect(e).to eq(experiment)
|
|
131
148
|
expect(e.resettable).to be_falsey
|
|
132
|
-
|
|
133
149
|
end
|
|
134
150
|
|
|
135
|
-
describe
|
|
136
|
-
let(:experiment) { Split::Experiment.new(
|
|
137
|
-
|
|
138
|
-
|
|
151
|
+
describe "#metadata" do
|
|
152
|
+
let(:experiment) { Split::Experiment.new("basket_text", alternatives: ["Basket", "Cart"], algorithm: Split::Algorithms::Whiplash, metadata: meta) }
|
|
153
|
+
let(:meta) { { a: "b" } }
|
|
154
|
+
|
|
155
|
+
before do
|
|
156
|
+
experiment.save
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
it "should delete the key when metadata is removed" do
|
|
160
|
+
experiment.metadata = nil
|
|
161
|
+
experiment.save
|
|
162
|
+
|
|
163
|
+
expect(Split.redis.exists?(experiment.metadata_key)).to be_falsey
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
context "simple hash" do
|
|
167
|
+
let(:meta) { { "basket" => "a", "cart" => "b" } }
|
|
168
|
+
|
|
139
169
|
it "should persist metadata in redis" do
|
|
140
|
-
|
|
141
|
-
e = Split::ExperimentCatalog.find('basket_text')
|
|
170
|
+
e = Split::ExperimentCatalog.find("basket_text")
|
|
142
171
|
expect(e).to eq(experiment)
|
|
143
172
|
expect(e.metadata).to eq(meta)
|
|
144
173
|
end
|
|
145
174
|
end
|
|
146
175
|
|
|
147
|
-
context
|
|
148
|
-
let(:meta) { {
|
|
176
|
+
context "nested hash" do
|
|
177
|
+
let(:meta) { { "basket" => { "one" => "two" }, "cart" => "b" } }
|
|
149
178
|
it "should persist metadata in redis" do
|
|
150
|
-
|
|
151
|
-
e = Split::ExperimentCatalog.find('basket_text')
|
|
179
|
+
e = Split::ExperimentCatalog.find("basket_text")
|
|
152
180
|
expect(e).to eq(experiment)
|
|
153
181
|
expect(e.metadata).to eq(meta)
|
|
154
182
|
end
|
|
@@ -156,32 +184,32 @@ describe Split::Experiment do
|
|
|
156
184
|
end
|
|
157
185
|
|
|
158
186
|
it "should persist algorithm in redis" do
|
|
159
|
-
experiment = Split::Experiment.new(
|
|
187
|
+
experiment = Split::Experiment.new("basket_text", alternatives: ["Basket", "Cart"], algorithm: Split::Algorithms::Whiplash)
|
|
160
188
|
experiment.save
|
|
161
189
|
|
|
162
|
-
e = Split::ExperimentCatalog.find(
|
|
190
|
+
e = Split::ExperimentCatalog.find("basket_text")
|
|
163
191
|
expect(e).to eq(experiment)
|
|
164
192
|
expect(e.algorithm).to eq(Split::Algorithms::Whiplash)
|
|
165
193
|
end
|
|
166
194
|
|
|
167
195
|
it "should persist a new experiment in redis, that does not exist in the configuration file" do
|
|
168
|
-
experiment = Split::Experiment.new(
|
|
196
|
+
experiment = Split::Experiment.new("foobar", alternatives: ["tra", "la"], algorithm: Split::Algorithms::Whiplash)
|
|
169
197
|
experiment.save
|
|
170
198
|
|
|
171
|
-
e = Split::ExperimentCatalog.find(
|
|
199
|
+
e = Split::ExperimentCatalog.find("foobar")
|
|
172
200
|
expect(e).to eq(experiment)
|
|
173
|
-
expect(e.alternatives.collect{|a| a.name}).to eq([
|
|
201
|
+
expect(e.alternatives.collect { |a| a.name }).to eq(["tra", "la"])
|
|
174
202
|
end
|
|
175
203
|
end
|
|
176
204
|
|
|
177
|
-
describe
|
|
178
|
-
it
|
|
179
|
-
experiment = Split::Experiment.new(
|
|
205
|
+
describe "deleting" do
|
|
206
|
+
it "should delete itself" do
|
|
207
|
+
experiment = Split::Experiment.new("basket_text", alternatives: [ "Basket", "Cart"])
|
|
180
208
|
experiment.save
|
|
181
209
|
|
|
182
210
|
experiment.delete
|
|
183
|
-
expect(Split.redis.exists(
|
|
184
|
-
expect(Split::ExperimentCatalog.find(
|
|
211
|
+
expect(Split.redis.exists?("link_color")).to be false
|
|
212
|
+
expect(Split::ExperimentCatalog.find("link_color")).to be_nil
|
|
185
213
|
end
|
|
186
214
|
|
|
187
215
|
it "should increment the version" do
|
|
@@ -200,79 +228,90 @@ describe Split::Experiment do
|
|
|
200
228
|
experiment.delete
|
|
201
229
|
end
|
|
202
230
|
|
|
203
|
-
it
|
|
231
|
+
it "should reset the start time if the experiment should be manually started" do
|
|
204
232
|
Split.configuration.start_manually = true
|
|
205
233
|
experiment.start
|
|
206
234
|
experiment.delete
|
|
207
235
|
expect(experiment.start_time).to be_nil
|
|
208
236
|
end
|
|
209
|
-
end
|
|
210
237
|
|
|
238
|
+
it "should default cohorting back to false" do
|
|
239
|
+
experiment.disable_cohorting
|
|
240
|
+
expect(experiment.cohorting_disabled?).to eq(true)
|
|
241
|
+
experiment.delete
|
|
242
|
+
expect(experiment.cohorting_disabled?).to eq(false)
|
|
243
|
+
end
|
|
244
|
+
end
|
|
211
245
|
|
|
212
|
-
describe
|
|
246
|
+
describe "winner" do
|
|
213
247
|
it "should have no winner initially" do
|
|
214
248
|
expect(experiment.winner).to be_nil
|
|
215
249
|
end
|
|
216
250
|
end
|
|
217
251
|
|
|
218
|
-
describe
|
|
252
|
+
describe "winner=" do
|
|
219
253
|
it "should allow you to specify a winner" do
|
|
220
254
|
experiment.save
|
|
221
|
-
experiment.winner =
|
|
222
|
-
expect(experiment.winner.name).to eq(
|
|
255
|
+
experiment.winner = "red"
|
|
256
|
+
expect(experiment.winner.name).to eq("red")
|
|
223
257
|
end
|
|
224
258
|
|
|
225
|
-
|
|
259
|
+
it "should call the on_experiment_winner_choose hook" do
|
|
260
|
+
expect(Split.configuration.on_experiment_winner_choose).to receive(:call)
|
|
261
|
+
experiment.winner = "green"
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
context "when has_winner state is memoized" do
|
|
226
265
|
before { expect(experiment).to_not have_winner }
|
|
227
266
|
|
|
228
|
-
it
|
|
229
|
-
experiment.winner =
|
|
267
|
+
it "should keep has_winner state consistent" do
|
|
268
|
+
experiment.winner = "red"
|
|
230
269
|
expect(experiment).to have_winner
|
|
231
270
|
end
|
|
232
271
|
end
|
|
233
272
|
end
|
|
234
273
|
|
|
235
|
-
describe
|
|
236
|
-
before { experiment.winner =
|
|
274
|
+
describe "reset_winner" do
|
|
275
|
+
before { experiment.winner = "green" }
|
|
237
276
|
|
|
238
|
-
it
|
|
277
|
+
it "should reset the winner" do
|
|
239
278
|
experiment.reset_winner
|
|
240
279
|
expect(experiment.winner).to be_nil
|
|
241
280
|
end
|
|
242
281
|
|
|
243
|
-
context
|
|
282
|
+
context "when has_winner state is memoized" do
|
|
244
283
|
before { expect(experiment).to have_winner }
|
|
245
284
|
|
|
246
|
-
it
|
|
285
|
+
it "should keep has_winner state consistent" do
|
|
247
286
|
experiment.reset_winner
|
|
248
287
|
expect(experiment).to_not have_winner
|
|
249
288
|
end
|
|
250
289
|
end
|
|
251
290
|
end
|
|
252
291
|
|
|
253
|
-
describe
|
|
254
|
-
context
|
|
255
|
-
before { experiment.winner =
|
|
292
|
+
describe "has_winner?" do
|
|
293
|
+
context "with winner" do
|
|
294
|
+
before { experiment.winner = "red" }
|
|
256
295
|
|
|
257
|
-
it
|
|
296
|
+
it "returns true" do
|
|
258
297
|
expect(experiment).to have_winner
|
|
259
298
|
end
|
|
260
299
|
end
|
|
261
300
|
|
|
262
|
-
context
|
|
263
|
-
it
|
|
301
|
+
context "without winner" do
|
|
302
|
+
it "returns false" do
|
|
264
303
|
expect(experiment).to_not have_winner
|
|
265
304
|
end
|
|
266
305
|
end
|
|
267
306
|
|
|
268
|
-
it
|
|
307
|
+
it "memoizes has_winner state" do
|
|
269
308
|
expect(experiment).to receive(:winner).once
|
|
270
309
|
expect(experiment).to_not have_winner
|
|
271
310
|
expect(experiment).to_not have_winner
|
|
272
311
|
end
|
|
273
312
|
end
|
|
274
313
|
|
|
275
|
-
describe
|
|
314
|
+
describe "reset" do
|
|
276
315
|
let(:reset_manually) { false }
|
|
277
316
|
|
|
278
317
|
before do
|
|
@@ -282,10 +321,10 @@ describe Split::Experiment do
|
|
|
282
321
|
green.increment_participation
|
|
283
322
|
end
|
|
284
323
|
|
|
285
|
-
it
|
|
286
|
-
experiment.winner =
|
|
324
|
+
it "should reset all alternatives" do
|
|
325
|
+
experiment.winner = "green"
|
|
287
326
|
|
|
288
|
-
expect(experiment.next_alternative.name).to eq(
|
|
327
|
+
expect(experiment.next_alternative.name).to eq("green")
|
|
289
328
|
green.increment_participation
|
|
290
329
|
|
|
291
330
|
experiment.reset
|
|
@@ -294,10 +333,10 @@ describe Split::Experiment do
|
|
|
294
333
|
expect(green.completed_count).to eq(0)
|
|
295
334
|
end
|
|
296
335
|
|
|
297
|
-
it
|
|
298
|
-
experiment.winner =
|
|
336
|
+
it "should reset the winner" do
|
|
337
|
+
experiment.winner = "green"
|
|
299
338
|
|
|
300
|
-
expect(experiment.next_alternative.name).to eq(
|
|
339
|
+
expect(experiment.next_alternative.name).to eq("green")
|
|
301
340
|
green.increment_participation
|
|
302
341
|
|
|
303
342
|
experiment.reset
|
|
@@ -322,64 +361,80 @@ describe Split::Experiment do
|
|
|
322
361
|
end
|
|
323
362
|
end
|
|
324
363
|
|
|
325
|
-
describe
|
|
326
|
-
let(:experiment) { Split::ExperimentCatalog.find_or_create(
|
|
364
|
+
describe "algorithm" do
|
|
365
|
+
let(:experiment) { Split::ExperimentCatalog.find_or_create("link_color", "blue", "red", "green") }
|
|
327
366
|
|
|
328
|
-
it
|
|
367
|
+
it "should use the default algorithm if none is specified" do
|
|
329
368
|
expect(experiment.algorithm).to eq(Split.configuration.algorithm)
|
|
330
369
|
end
|
|
331
370
|
|
|
332
|
-
it
|
|
371
|
+
it "should use the user specified algorithm for this experiment if specified" do
|
|
333
372
|
experiment.algorithm = Split::Algorithms::Whiplash
|
|
334
373
|
expect(experiment.algorithm).to eq(Split::Algorithms::Whiplash)
|
|
335
374
|
end
|
|
336
375
|
end
|
|
337
376
|
|
|
338
|
-
describe
|
|
339
|
-
context
|
|
340
|
-
let(:experiment) { Split::ExperimentCatalog.find_or_create(
|
|
377
|
+
describe "#next_alternative" do
|
|
378
|
+
context "with multiple alternatives" do
|
|
379
|
+
let(:experiment) { Split::ExperimentCatalog.find_or_create("link_color", "blue", "red", "green") }
|
|
341
380
|
|
|
342
|
-
context
|
|
381
|
+
context "with winner" do
|
|
343
382
|
it "should always return the winner" do
|
|
344
|
-
green = Split::Alternative.new(
|
|
345
|
-
experiment.winner =
|
|
383
|
+
green = Split::Alternative.new("green", "link_color")
|
|
384
|
+
experiment.winner = "green"
|
|
346
385
|
|
|
347
|
-
expect(experiment.next_alternative.name).to eq(
|
|
386
|
+
expect(experiment.next_alternative.name).to eq("green")
|
|
348
387
|
green.increment_participation
|
|
349
388
|
|
|
350
|
-
expect(experiment.next_alternative.name).to eq(
|
|
389
|
+
expect(experiment.next_alternative.name).to eq("green")
|
|
351
390
|
end
|
|
352
391
|
end
|
|
353
392
|
|
|
354
|
-
context
|
|
393
|
+
context "without winner" do
|
|
355
394
|
it "should use the specified algorithm" do
|
|
356
395
|
experiment.algorithm = Split::Algorithms::Whiplash
|
|
357
|
-
expect(experiment.algorithm).to receive(:choose_alternative).and_return(Split::Alternative.new(
|
|
358
|
-
expect(experiment.next_alternative.name).to eq(
|
|
396
|
+
expect(experiment.algorithm).to receive(:choose_alternative).and_return(Split::Alternative.new("green", "link_color"))
|
|
397
|
+
expect(experiment.next_alternative.name).to eq("green")
|
|
359
398
|
end
|
|
360
399
|
end
|
|
361
400
|
end
|
|
362
401
|
|
|
363
|
-
context
|
|
364
|
-
let(:experiment) { Split::ExperimentCatalog.find_or_create(
|
|
402
|
+
context "with single alternative" do
|
|
403
|
+
let(:experiment) { Split::ExperimentCatalog.find_or_create("link_color", "blue") }
|
|
365
404
|
|
|
366
405
|
it "should always return the only alternative" do
|
|
367
|
-
expect(experiment.next_alternative.name).to eq(
|
|
368
|
-
expect(experiment.next_alternative.name).to eq(
|
|
406
|
+
expect(experiment.next_alternative.name).to eq("blue")
|
|
407
|
+
expect(experiment.next_alternative.name).to eq("blue")
|
|
369
408
|
end
|
|
370
409
|
end
|
|
371
410
|
end
|
|
372
411
|
|
|
373
|
-
describe
|
|
412
|
+
describe "#cohorting_disabled?" do
|
|
413
|
+
it "returns false when nothing has been configured" do
|
|
414
|
+
expect(experiment.cohorting_disabled?).to eq false
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
it "returns true when enable_cohorting is performed" do
|
|
418
|
+
experiment.enable_cohorting
|
|
419
|
+
expect(experiment.cohorting_disabled?).to eq false
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
it "returns false when nothing has been configured" do
|
|
423
|
+
experiment.disable_cohorting
|
|
424
|
+
expect(experiment.cohorting_disabled?).to eq true
|
|
425
|
+
end
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
describe "changing an existing experiment" do
|
|
374
429
|
def same_but_different_alternative
|
|
375
|
-
Split::ExperimentCatalog.find_or_create(
|
|
430
|
+
Split::ExperimentCatalog.find_or_create("link_color", "blue", "yellow", "orange")
|
|
376
431
|
end
|
|
377
432
|
|
|
378
433
|
it "should reset an experiment if it is loaded with different alternatives" do
|
|
379
434
|
experiment.save
|
|
380
435
|
blue.participant_count = 5
|
|
381
436
|
same_experiment = same_but_different_alternative
|
|
382
|
-
expect(same_experiment.alternatives.map(&:name)).to eq([
|
|
437
|
+
expect(same_experiment.alternatives.map(&:name)).to eq(["blue", "yellow", "orange"])
|
|
383
438
|
expect(blue.participant_count).to eq(0)
|
|
384
439
|
end
|
|
385
440
|
|
|
@@ -392,7 +447,22 @@ describe Split::Experiment do
|
|
|
392
447
|
expect(same_experiment_again.version).to eq(1)
|
|
393
448
|
end
|
|
394
449
|
|
|
395
|
-
context
|
|
450
|
+
context "when metadata is changed" do
|
|
451
|
+
it "should increase version" do
|
|
452
|
+
experiment.save
|
|
453
|
+
experiment.metadata = { "foo" => "bar" }
|
|
454
|
+
|
|
455
|
+
expect { experiment.save }.to change { experiment.version }.by(1)
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
it "does not increase version" do
|
|
459
|
+
experiment.metadata = nil
|
|
460
|
+
experiment.save
|
|
461
|
+
expect { experiment.save }.to change { experiment.version }.by(0)
|
|
462
|
+
end
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
context "when experiment configuration is changed" do
|
|
396
466
|
let(:reset_manually) { false }
|
|
397
467
|
|
|
398
468
|
before do
|
|
@@ -405,15 +475,15 @@ describe Split::Experiment do
|
|
|
405
475
|
experiment.save
|
|
406
476
|
end
|
|
407
477
|
|
|
408
|
-
it
|
|
478
|
+
it "resets all alternatives" do
|
|
409
479
|
expect(green.participant_count).to eq(0)
|
|
410
480
|
expect(green.completed_count).to eq(0)
|
|
411
481
|
end
|
|
412
482
|
|
|
413
|
-
context
|
|
483
|
+
context "when reset_manually is set" do
|
|
414
484
|
let(:reset_manually) { true }
|
|
415
485
|
|
|
416
|
-
it
|
|
486
|
+
it "does not reset alternatives" do
|
|
417
487
|
expect(green.participant_count).to eq(2)
|
|
418
488
|
expect(green.completed_count).to eq(0)
|
|
419
489
|
end
|
|
@@ -421,16 +491,16 @@ describe Split::Experiment do
|
|
|
421
491
|
end
|
|
422
492
|
end
|
|
423
493
|
|
|
424
|
-
describe
|
|
494
|
+
describe "alternatives passed as non-strings" do
|
|
425
495
|
it "should throw an exception if an alternative is passed that is not a string" do
|
|
426
|
-
expect
|
|
427
|
-
expect
|
|
496
|
+
expect { Split::ExperimentCatalog.find_or_create("link_color", :blue, :red) }.to raise_error(ArgumentError)
|
|
497
|
+
expect { Split::ExperimentCatalog.find_or_create("link_enabled", true, false) }.to raise_error(ArgumentError)
|
|
428
498
|
end
|
|
429
499
|
end
|
|
430
500
|
|
|
431
|
-
describe
|
|
501
|
+
describe "specifying weights" do
|
|
432
502
|
let(:experiment_with_weight) {
|
|
433
|
-
Split::ExperimentCatalog.find_or_create(
|
|
503
|
+
Split::ExperimentCatalog.find_or_create("link_color", { "blue" => 1 }, { "red" => 2 })
|
|
434
504
|
}
|
|
435
505
|
|
|
436
506
|
it "should work for a new experiment" do
|
|
@@ -449,7 +519,7 @@ describe Split::Experiment do
|
|
|
449
519
|
}
|
|
450
520
|
|
|
451
521
|
context "saving experiment" do
|
|
452
|
-
let(:same_but_different_goals) { Split::ExperimentCatalog.find_or_create({
|
|
522
|
+
let(:same_but_different_goals) { Split::ExperimentCatalog.find_or_create({ "link_color" => ["purchase", "refund"] }, "blue", "red", "green") }
|
|
453
523
|
|
|
454
524
|
before { experiment.save }
|
|
455
525
|
|
|
@@ -461,7 +531,6 @@ describe Split::Experiment do
|
|
|
461
531
|
same_but_different_goals
|
|
462
532
|
expect(Split::ExperimentCatalog.find("link_color").goals).to eq(["purchase", "refund"])
|
|
463
533
|
end
|
|
464
|
-
|
|
465
534
|
end
|
|
466
535
|
|
|
467
536
|
it "should have goals" do
|
|
@@ -470,9 +539,9 @@ describe Split::Experiment do
|
|
|
470
539
|
|
|
471
540
|
context "find or create experiment" do
|
|
472
541
|
it "should have correct goals" do
|
|
473
|
-
experiment = Split::ExperimentCatalog.find_or_create({
|
|
542
|
+
experiment = Split::ExperimentCatalog.find_or_create({ "link_color3" => ["purchase", "refund"] }, "blue", "red", "green")
|
|
474
543
|
expect(experiment.goals).to eq(["purchase", "refund"])
|
|
475
|
-
experiment = Split::ExperimentCatalog.find_or_create(
|
|
544
|
+
experiment = Split::ExperimentCatalog.find_or_create("link_color3", "blue", "red", "green")
|
|
476
545
|
expect(experiment.goals).to eq([])
|
|
477
546
|
end
|
|
478
547
|
end
|
|
@@ -480,19 +549,19 @@ describe Split::Experiment do
|
|
|
480
549
|
|
|
481
550
|
describe "beta probability calculation" do
|
|
482
551
|
it "should return a hash with the probability of each alternative being the best" do
|
|
483
|
-
experiment = Split::ExperimentCatalog.find_or_create(
|
|
552
|
+
experiment = Split::ExperimentCatalog.find_or_create("mathematicians", "bernoulli", "poisson", "lagrange")
|
|
484
553
|
experiment.calc_winning_alternatives
|
|
485
554
|
expect(experiment.alternative_probabilities).not_to be_nil
|
|
486
555
|
end
|
|
487
556
|
|
|
488
557
|
it "should return between 46% and 54% probability for an experiment with 2 alternatives and no data" do
|
|
489
|
-
experiment = Split::ExperimentCatalog.find_or_create(
|
|
558
|
+
experiment = Split::ExperimentCatalog.find_or_create("scientists", "einstein", "bohr")
|
|
490
559
|
experiment.calc_winning_alternatives
|
|
491
560
|
expect(experiment.alternatives[0].p_winner).to be_within(0.04).of(0.50)
|
|
492
561
|
end
|
|
493
562
|
|
|
494
|
-
it "should calculate the probability of being the winning alternative separately for each goal", :
|
|
495
|
-
experiment = Split::ExperimentCatalog.find_or_create({
|
|
563
|
+
it "should calculate the probability of being the winning alternative separately for each goal", skip: true do
|
|
564
|
+
experiment = Split::ExperimentCatalog.find_or_create({ "link_color3" => ["purchase", "refund"] }, "blue", "red", "green")
|
|
496
565
|
goal1 = experiment.goals[0]
|
|
497
566
|
goal2 = experiment.goals[1]
|
|
498
567
|
experiment.alternatives.each do |alternative|
|
|
@@ -507,8 +576,19 @@ describe Split::Experiment do
|
|
|
507
576
|
expect(p_goal1).not_to be_within(0.04).of(p_goal2)
|
|
508
577
|
end
|
|
509
578
|
|
|
579
|
+
it "should not calculate when data is not valid for beta distribution" do
|
|
580
|
+
experiment = Split::ExperimentCatalog.find_or_create("scientists", "einstein", "bohr")
|
|
581
|
+
|
|
582
|
+
experiment.alternatives.each do |alternative|
|
|
583
|
+
alternative.participant_count = 9
|
|
584
|
+
alternative.set_completed_count(10)
|
|
585
|
+
end
|
|
586
|
+
|
|
587
|
+
expect { experiment.calc_winning_alternatives }.to_not raise_error
|
|
588
|
+
end
|
|
589
|
+
|
|
510
590
|
it "should return nil and not re-calculate probabilities if they have already been calculated today" do
|
|
511
|
-
experiment = Split::ExperimentCatalog.find_or_create({
|
|
591
|
+
experiment = Split::ExperimentCatalog.find_or_create({ "link_color3" => ["purchase", "refund"] }, "blue", "red", "green")
|
|
512
592
|
expect(experiment.calc_winning_alternatives).not_to be nil
|
|
513
593
|
expect(experiment.calc_winning_alternatives).to be nil
|
|
514
594
|
end
|