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/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,44 +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
|
|
250
|
+
end
|
|
216
251
|
|
|
252
|
+
describe "winner=" do
|
|
217
253
|
it "should allow you to specify a winner" do
|
|
218
254
|
experiment.save
|
|
219
|
-
experiment.winner =
|
|
220
|
-
expect(experiment.winner.name).to eq(
|
|
255
|
+
experiment.winner = "red"
|
|
256
|
+
expect(experiment.winner.name).to eq("red")
|
|
257
|
+
end
|
|
258
|
+
|
|
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
|
|
265
|
+
before { expect(experiment).to_not have_winner }
|
|
266
|
+
|
|
267
|
+
it "should keep has_winner state consistent" do
|
|
268
|
+
experiment.winner = "red"
|
|
269
|
+
expect(experiment).to have_winner
|
|
270
|
+
end
|
|
221
271
|
end
|
|
222
272
|
end
|
|
223
273
|
|
|
224
|
-
describe
|
|
225
|
-
|
|
226
|
-
before { experiment.winner = 'red' }
|
|
274
|
+
describe "reset_winner" do
|
|
275
|
+
before { experiment.winner = "green" }
|
|
227
276
|
|
|
228
|
-
|
|
277
|
+
it "should reset the winner" do
|
|
278
|
+
experiment.reset_winner
|
|
279
|
+
expect(experiment.winner).to be_nil
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
context "when has_winner state is memoized" do
|
|
283
|
+
before { expect(experiment).to have_winner }
|
|
284
|
+
|
|
285
|
+
it "should keep has_winner state consistent" do
|
|
286
|
+
experiment.reset_winner
|
|
287
|
+
expect(experiment).to_not have_winner
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
describe "has_winner?" do
|
|
293
|
+
context "with winner" do
|
|
294
|
+
before { experiment.winner = "red" }
|
|
295
|
+
|
|
296
|
+
it "returns true" do
|
|
229
297
|
expect(experiment).to have_winner
|
|
230
298
|
end
|
|
231
299
|
end
|
|
232
300
|
|
|
233
|
-
context
|
|
234
|
-
it
|
|
301
|
+
context "without winner" do
|
|
302
|
+
it "returns false" do
|
|
235
303
|
expect(experiment).to_not have_winner
|
|
236
304
|
end
|
|
237
305
|
end
|
|
306
|
+
|
|
307
|
+
it "memoizes has_winner state" do
|
|
308
|
+
expect(experiment).to receive(:winner).once
|
|
309
|
+
expect(experiment).to_not have_winner
|
|
310
|
+
expect(experiment).to_not have_winner
|
|
311
|
+
end
|
|
238
312
|
end
|
|
239
313
|
|
|
240
|
-
describe
|
|
314
|
+
describe "reset" do
|
|
241
315
|
let(:reset_manually) { false }
|
|
242
316
|
|
|
243
317
|
before do
|
|
@@ -247,10 +321,10 @@ describe Split::Experiment do
|
|
|
247
321
|
green.increment_participation
|
|
248
322
|
end
|
|
249
323
|
|
|
250
|
-
it
|
|
251
|
-
experiment.winner =
|
|
324
|
+
it "should reset all alternatives" do
|
|
325
|
+
experiment.winner = "green"
|
|
252
326
|
|
|
253
|
-
expect(experiment.next_alternative.name).to eq(
|
|
327
|
+
expect(experiment.next_alternative.name).to eq("green")
|
|
254
328
|
green.increment_participation
|
|
255
329
|
|
|
256
330
|
experiment.reset
|
|
@@ -259,10 +333,10 @@ describe Split::Experiment do
|
|
|
259
333
|
expect(green.completed_count).to eq(0)
|
|
260
334
|
end
|
|
261
335
|
|
|
262
|
-
it
|
|
263
|
-
experiment.winner =
|
|
336
|
+
it "should reset the winner" do
|
|
337
|
+
experiment.winner = "green"
|
|
264
338
|
|
|
265
|
-
expect(experiment.next_alternative.name).to eq(
|
|
339
|
+
expect(experiment.next_alternative.name).to eq("green")
|
|
266
340
|
green.increment_participation
|
|
267
341
|
|
|
268
342
|
experiment.reset
|
|
@@ -287,64 +361,80 @@ describe Split::Experiment do
|
|
|
287
361
|
end
|
|
288
362
|
end
|
|
289
363
|
|
|
290
|
-
describe
|
|
291
|
-
let(:experiment) { Split::ExperimentCatalog.find_or_create(
|
|
364
|
+
describe "algorithm" do
|
|
365
|
+
let(:experiment) { Split::ExperimentCatalog.find_or_create("link_color", "blue", "red", "green") }
|
|
292
366
|
|
|
293
|
-
it
|
|
367
|
+
it "should use the default algorithm if none is specified" do
|
|
294
368
|
expect(experiment.algorithm).to eq(Split.configuration.algorithm)
|
|
295
369
|
end
|
|
296
370
|
|
|
297
|
-
it
|
|
371
|
+
it "should use the user specified algorithm for this experiment if specified" do
|
|
298
372
|
experiment.algorithm = Split::Algorithms::Whiplash
|
|
299
373
|
expect(experiment.algorithm).to eq(Split::Algorithms::Whiplash)
|
|
300
374
|
end
|
|
301
375
|
end
|
|
302
376
|
|
|
303
|
-
describe
|
|
304
|
-
context
|
|
305
|
-
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") }
|
|
306
380
|
|
|
307
|
-
context
|
|
381
|
+
context "with winner" do
|
|
308
382
|
it "should always return the winner" do
|
|
309
|
-
green = Split::Alternative.new(
|
|
310
|
-
experiment.winner =
|
|
383
|
+
green = Split::Alternative.new("green", "link_color")
|
|
384
|
+
experiment.winner = "green"
|
|
311
385
|
|
|
312
|
-
expect(experiment.next_alternative.name).to eq(
|
|
386
|
+
expect(experiment.next_alternative.name).to eq("green")
|
|
313
387
|
green.increment_participation
|
|
314
388
|
|
|
315
|
-
expect(experiment.next_alternative.name).to eq(
|
|
389
|
+
expect(experiment.next_alternative.name).to eq("green")
|
|
316
390
|
end
|
|
317
391
|
end
|
|
318
392
|
|
|
319
|
-
context
|
|
393
|
+
context "without winner" do
|
|
320
394
|
it "should use the specified algorithm" do
|
|
321
395
|
experiment.algorithm = Split::Algorithms::Whiplash
|
|
322
|
-
expect(experiment.algorithm).to receive(:choose_alternative).and_return(Split::Alternative.new(
|
|
323
|
-
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")
|
|
324
398
|
end
|
|
325
399
|
end
|
|
326
400
|
end
|
|
327
401
|
|
|
328
|
-
context
|
|
329
|
-
let(:experiment) { Split::ExperimentCatalog.find_or_create(
|
|
402
|
+
context "with single alternative" do
|
|
403
|
+
let(:experiment) { Split::ExperimentCatalog.find_or_create("link_color", "blue") }
|
|
330
404
|
|
|
331
405
|
it "should always return the only alternative" do
|
|
332
|
-
expect(experiment.next_alternative.name).to eq(
|
|
333
|
-
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")
|
|
334
408
|
end
|
|
335
409
|
end
|
|
336
410
|
end
|
|
337
411
|
|
|
338
|
-
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
|
|
339
429
|
def same_but_different_alternative
|
|
340
|
-
Split::ExperimentCatalog.find_or_create(
|
|
430
|
+
Split::ExperimentCatalog.find_or_create("link_color", "blue", "yellow", "orange")
|
|
341
431
|
end
|
|
342
432
|
|
|
343
433
|
it "should reset an experiment if it is loaded with different alternatives" do
|
|
344
434
|
experiment.save
|
|
345
435
|
blue.participant_count = 5
|
|
346
436
|
same_experiment = same_but_different_alternative
|
|
347
|
-
expect(same_experiment.alternatives.map(&:name)).to eq([
|
|
437
|
+
expect(same_experiment.alternatives.map(&:name)).to eq(["blue", "yellow", "orange"])
|
|
348
438
|
expect(blue.participant_count).to eq(0)
|
|
349
439
|
end
|
|
350
440
|
|
|
@@ -357,7 +447,22 @@ describe Split::Experiment do
|
|
|
357
447
|
expect(same_experiment_again.version).to eq(1)
|
|
358
448
|
end
|
|
359
449
|
|
|
360
|
-
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
|
|
361
466
|
let(:reset_manually) { false }
|
|
362
467
|
|
|
363
468
|
before do
|
|
@@ -370,15 +475,15 @@ describe Split::Experiment do
|
|
|
370
475
|
experiment.save
|
|
371
476
|
end
|
|
372
477
|
|
|
373
|
-
it
|
|
478
|
+
it "resets all alternatives" do
|
|
374
479
|
expect(green.participant_count).to eq(0)
|
|
375
480
|
expect(green.completed_count).to eq(0)
|
|
376
481
|
end
|
|
377
482
|
|
|
378
|
-
context
|
|
483
|
+
context "when reset_manually is set" do
|
|
379
484
|
let(:reset_manually) { true }
|
|
380
485
|
|
|
381
|
-
it
|
|
486
|
+
it "does not reset alternatives" do
|
|
382
487
|
expect(green.participant_count).to eq(2)
|
|
383
488
|
expect(green.completed_count).to eq(0)
|
|
384
489
|
end
|
|
@@ -386,16 +491,16 @@ describe Split::Experiment do
|
|
|
386
491
|
end
|
|
387
492
|
end
|
|
388
493
|
|
|
389
|
-
describe
|
|
494
|
+
describe "alternatives passed as non-strings" do
|
|
390
495
|
it "should throw an exception if an alternative is passed that is not a string" do
|
|
391
|
-
expect
|
|
392
|
-
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)
|
|
393
498
|
end
|
|
394
499
|
end
|
|
395
500
|
|
|
396
|
-
describe
|
|
501
|
+
describe "specifying weights" do
|
|
397
502
|
let(:experiment_with_weight) {
|
|
398
|
-
Split::ExperimentCatalog.find_or_create(
|
|
503
|
+
Split::ExperimentCatalog.find_or_create("link_color", { "blue" => 1 }, { "red" => 2 })
|
|
399
504
|
}
|
|
400
505
|
|
|
401
506
|
it "should work for a new experiment" do
|
|
@@ -414,9 +519,7 @@ describe Split::Experiment do
|
|
|
414
519
|
}
|
|
415
520
|
|
|
416
521
|
context "saving experiment" do
|
|
417
|
-
|
|
418
|
-
Split::ExperimentCatalog.find_or_create({'link_color' => ["purchase", "refund"]}, 'blue', 'red', 'green')
|
|
419
|
-
end
|
|
522
|
+
let(:same_but_different_goals) { Split::ExperimentCatalog.find_or_create({ "link_color" => ["purchase", "refund"] }, "blue", "red", "green") }
|
|
420
523
|
|
|
421
524
|
before { experiment.save }
|
|
422
525
|
|
|
@@ -425,10 +528,9 @@ describe Split::Experiment do
|
|
|
425
528
|
end
|
|
426
529
|
|
|
427
530
|
it "should reset an experiment if it is loaded with different goals" do
|
|
428
|
-
|
|
531
|
+
same_but_different_goals
|
|
429
532
|
expect(Split::ExperimentCatalog.find("link_color").goals).to eq(["purchase", "refund"])
|
|
430
533
|
end
|
|
431
|
-
|
|
432
534
|
end
|
|
433
535
|
|
|
434
536
|
it "should have goals" do
|
|
@@ -437,9 +539,9 @@ describe Split::Experiment do
|
|
|
437
539
|
|
|
438
540
|
context "find or create experiment" do
|
|
439
541
|
it "should have correct goals" do
|
|
440
|
-
experiment = Split::ExperimentCatalog.find_or_create({
|
|
542
|
+
experiment = Split::ExperimentCatalog.find_or_create({ "link_color3" => ["purchase", "refund"] }, "blue", "red", "green")
|
|
441
543
|
expect(experiment.goals).to eq(["purchase", "refund"])
|
|
442
|
-
experiment = Split::ExperimentCatalog.find_or_create(
|
|
544
|
+
experiment = Split::ExperimentCatalog.find_or_create("link_color3", "blue", "red", "green")
|
|
443
545
|
expect(experiment.goals).to eq([])
|
|
444
546
|
end
|
|
445
547
|
end
|
|
@@ -447,19 +549,19 @@ describe Split::Experiment do
|
|
|
447
549
|
|
|
448
550
|
describe "beta probability calculation" do
|
|
449
551
|
it "should return a hash with the probability of each alternative being the best" do
|
|
450
|
-
experiment = Split::ExperimentCatalog.find_or_create(
|
|
552
|
+
experiment = Split::ExperimentCatalog.find_or_create("mathematicians", "bernoulli", "poisson", "lagrange")
|
|
451
553
|
experiment.calc_winning_alternatives
|
|
452
554
|
expect(experiment.alternative_probabilities).not_to be_nil
|
|
453
555
|
end
|
|
454
556
|
|
|
455
557
|
it "should return between 46% and 54% probability for an experiment with 2 alternatives and no data" do
|
|
456
|
-
experiment = Split::ExperimentCatalog.find_or_create(
|
|
558
|
+
experiment = Split::ExperimentCatalog.find_or_create("scientists", "einstein", "bohr")
|
|
457
559
|
experiment.calc_winning_alternatives
|
|
458
560
|
expect(experiment.alternatives[0].p_winner).to be_within(0.04).of(0.50)
|
|
459
561
|
end
|
|
460
562
|
|
|
461
|
-
it "should calculate the probability of being the winning alternative separately for each goal", :
|
|
462
|
-
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")
|
|
463
565
|
goal1 = experiment.goals[0]
|
|
464
566
|
goal2 = experiment.goals[1]
|
|
465
567
|
experiment.alternatives.each do |alternative|
|
|
@@ -474,8 +576,19 @@ describe Split::Experiment do
|
|
|
474
576
|
expect(p_goal1).not_to be_within(0.04).of(p_goal2)
|
|
475
577
|
end
|
|
476
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
|
+
|
|
477
590
|
it "should return nil and not re-calculate probabilities if they have already been calculated today" do
|
|
478
|
-
experiment = Split::ExperimentCatalog.find_or_create({
|
|
591
|
+
experiment = Split::ExperimentCatalog.find_or_create({ "link_color3" => ["purchase", "refund"] }, "blue", "red", "green")
|
|
479
592
|
expect(experiment.calc_winning_alternatives).not_to be nil
|
|
480
593
|
expect(experiment.calc_winning_alternatives).to be nil
|
|
481
594
|
end
|