split 1.4.2 → 1.4.3
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/.travis.yml +1 -1
- data/CHANGELOG.md +14 -0
- data/README.md +8 -2
- data/lib/split/configuration.rb +5 -0
- data/lib/split/trial.rb +12 -6
- data/lib/split/version.rb +1 -1
- data/spec/trial_spec.rb +140 -79
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 86dd69d253b8dd5ddb9296729f89a04d98a482d4
|
4
|
+
data.tar.gz: 561287a2dfc015d8ec5b1751c78ef7852b626edb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c34f1b8d4893ada8c64a27deed0536aaa0f0922531abe9180d6c3eb96742dea570b8dedeef341961184b9cd2bc9ef3d0ee439cd18f945d371069006abee3a530
|
7
|
+
data.tar.gz: 7b63e41dc1d105ce2e1e00fd1488d7d149db7b44f5df6b4de6a835ed17532d8511593829babedab310b5258cf7861840a80e0d9bc50c333f04ce91acf997712d
|
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,17 @@
|
|
1
|
+
## 1.4.3 (April 28th, 2016)
|
2
|
+
|
3
|
+
Features:
|
4
|
+
|
5
|
+
- add on_trial callback whenever a trial is started (@mtyeh411, #375)
|
6
|
+
|
7
|
+
Bugfixes:
|
8
|
+
|
9
|
+
- Allow algorithm configuration at experiment level (@007sumit, #376)
|
10
|
+
|
11
|
+
Misc:
|
12
|
+
|
13
|
+
- only choose override if it exists as valid alternative (@spheric, #377)
|
14
|
+
|
1
15
|
## 1.4.2 (April 25th, 2016)
|
2
16
|
|
3
17
|
Misc:
|
data/README.md
CHANGED
@@ -242,7 +242,8 @@ For example:
|
|
242
242
|
|
243
243
|
``` ruby
|
244
244
|
Split.configure do |config|
|
245
|
-
config.
|
245
|
+
config.on_trial = :log_trial # run on every trial
|
246
|
+
config.on_trial_choose = :log_trial_choose # run on trials with new users only
|
246
247
|
config.on_trial_complete = :log_trial_complete
|
247
248
|
end
|
248
249
|
```
|
@@ -251,11 +252,16 @@ Set these attributes to a method name available in the same context as the
|
|
251
252
|
`ab_test` method. These methods should accept one argument, a `Trial` instance.
|
252
253
|
|
253
254
|
``` ruby
|
254
|
-
def
|
255
|
+
def log_trial(trial)
|
255
256
|
logger.info "experiment=%s alternative=%s user=%s" %
|
256
257
|
[ trial.experiment.name, trial.alternative, current_user.id ]
|
257
258
|
end
|
258
259
|
|
260
|
+
def log_trial_choose(trial)
|
261
|
+
logger.info "[new user] experiment=%s alternative=%s user=%s" %
|
262
|
+
[ trial.experiment.name, trial.alternative, current_user.id ]
|
263
|
+
end
|
264
|
+
|
259
265
|
def log_trial_complete(trial)
|
260
266
|
logger.info "experiment=%s alternative=%s user=%s complete=true" %
|
261
267
|
[ trial.experiment.name, trial.alternative, current_user.id ]
|
data/lib/split/configuration.rb
CHANGED
@@ -15,6 +15,7 @@ module Split
|
|
15
15
|
attr_accessor :algorithm
|
16
16
|
attr_accessor :store_override
|
17
17
|
attr_accessor :start_manually
|
18
|
+
attr_accessor :on_trial
|
18
19
|
attr_accessor :on_trial_choose
|
19
20
|
attr_accessor :on_trial_complete
|
20
21
|
attr_accessor :on_experiment_reset
|
@@ -150,6 +151,10 @@ module Split
|
|
150
151
|
experiment_config[experiment_name.to_sym][:metadata] = metadata
|
151
152
|
end
|
152
153
|
|
154
|
+
if algorithm = value_for(settings, :algorithm)
|
155
|
+
experiment_config[experiment_name.to_sym][:algorithm] = algorithm
|
156
|
+
end
|
157
|
+
|
153
158
|
if (resettable = value_for(settings, :resettable)) != nil
|
154
159
|
experiment_config[experiment_name.to_sym][:resettable] = resettable
|
155
160
|
end
|
data/lib/split/trial.rb
CHANGED
@@ -41,8 +41,7 @@ module Split
|
|
41
41
|
Array(goals).each {|g| alternative.increment_completion(g) }
|
42
42
|
end
|
43
43
|
|
44
|
-
context
|
45
|
-
if Split.configuration.on_trial_complete && context
|
44
|
+
run_callback context, Split.configuration.on_trial_complete
|
46
45
|
end
|
47
46
|
end
|
48
47
|
|
@@ -54,7 +53,7 @@ module Split
|
|
54
53
|
# Only run the process once
|
55
54
|
return alternative if @alternative_choosen
|
56
55
|
|
57
|
-
if
|
56
|
+
if override_is_alternative?
|
58
57
|
self.alternative = @options[:override]
|
59
58
|
elsif @options[:disabled] || Split.configuration.disabled?
|
60
59
|
self.alternative = @experiment.control
|
@@ -73,19 +72,26 @@ module Split
|
|
73
72
|
# Increment the number of participants since we are actually choosing a new alternative
|
74
73
|
self.alternative.increment_participation
|
75
74
|
|
76
|
-
|
77
|
-
context.send(Split.configuration.on_trial_choose, self) \
|
78
|
-
if Split.configuration.on_trial_choose && context
|
75
|
+
run_callback context, Split.configuration.on_trial_choose
|
79
76
|
end
|
80
77
|
end
|
81
78
|
|
82
79
|
@user[@experiment.key] = alternative.name if should_store_alternative?
|
83
80
|
@alternative_choosen = true
|
81
|
+
run_callback context, Split.configuration.on_trial unless @options[:disabled] || Split.configuration.disabled?
|
84
82
|
alternative
|
85
83
|
end
|
86
84
|
|
87
85
|
private
|
88
86
|
|
87
|
+
def run_callback(context, callback_name)
|
88
|
+
context.send(callback_name, self) if callback_name && context.respond_to?(callback_name, true)
|
89
|
+
end
|
90
|
+
|
91
|
+
def override_is_alternative?
|
92
|
+
@experiment.alternatives.map(&:name).include?(@options[:override])
|
93
|
+
end
|
94
|
+
|
89
95
|
def should_store_alternative?
|
90
96
|
if @options[:override] || @options[:disabled]
|
91
97
|
Split.configuration.store_override
|
data/lib/split/version.rb
CHANGED
data/spec/trial_spec.rb
CHANGED
@@ -4,8 +4,9 @@ require 'split/trial'
|
|
4
4
|
|
5
5
|
describe Split::Trial do
|
6
6
|
let(:user) { mock_user }
|
7
|
+
let(:alternatives) { ['basket', 'cart'] }
|
7
8
|
let(:experiment) do
|
8
|
-
Split::Experiment.new('basket_text', :alternatives =>
|
9
|
+
Split::Experiment.new('basket_text', :alternatives => alternatives).save
|
9
10
|
end
|
10
11
|
|
11
12
|
it "should be initializeable" do
|
@@ -19,7 +20,7 @@ describe Split::Trial do
|
|
19
20
|
describe "alternative" do
|
20
21
|
it "should use the alternative if specified" do
|
21
22
|
alternative = double('alternative', :kind_of? => Split::Alternative)
|
22
|
-
trial = Split::Trial.new(:experiment =>
|
23
|
+
trial = Split::Trial.new(:experiment => double('experiment'),
|
23
24
|
:alternative => alternative, :user => user)
|
24
25
|
expect(trial).not_to receive(:choose)
|
25
26
|
expect(trial.alternative).to eq(alternative)
|
@@ -35,7 +36,6 @@ describe Split::Trial do
|
|
35
36
|
end
|
36
37
|
|
37
38
|
describe "metadata" do
|
38
|
-
let(:alternatives) { ['basket', 'cart'] }
|
39
39
|
let(:metadata) { Hash[alternatives.map { |k| [k, "Metadata for #{k}"] }] }
|
40
40
|
let(:experiment) do
|
41
41
|
Split::Experiment.new('basket_text', :alternatives => alternatives, :metadata => metadata).save
|
@@ -51,50 +51,102 @@ describe Split::Trial do
|
|
51
51
|
trial = Split::Trial.new(:experiment => experiment, :user => user)
|
52
52
|
trial.choose!
|
53
53
|
expect(trial.metadata).to eq(metadata[trial.alternative.name])
|
54
|
-
expect(trial.metadata).to match
|
54
|
+
expect(trial.metadata).to match(/#{trial.alternative.name}/)
|
55
55
|
end
|
56
56
|
end
|
57
57
|
|
58
58
|
describe "#choose!" do
|
59
|
+
let(:context) { double(on_trial_callback: 'test callback') }
|
60
|
+
let(:trial) do
|
61
|
+
Split::Trial.new(:user => user, :experiment => experiment)
|
62
|
+
end
|
63
|
+
|
64
|
+
shared_examples_for 'a trial with callbacks' do
|
65
|
+
it 'does not run if on_trial callback is not respondable' do
|
66
|
+
Split.configuration.on_trial = :foo
|
67
|
+
allow(context).to receive(:respond_to?).with(:foo, true).and_return false
|
68
|
+
expect(context).to_not receive(:foo)
|
69
|
+
trial.choose! context
|
70
|
+
end
|
71
|
+
it 'runs on_trial callback' do
|
72
|
+
Split.configuration.on_trial = :on_trial_callback
|
73
|
+
expect(context).to receive(:on_trial_callback)
|
74
|
+
trial.choose! context
|
75
|
+
end
|
76
|
+
it 'does not run nil on_trial callback' do
|
77
|
+
Split.configuration.on_trial = nil
|
78
|
+
expect(context).not_to receive(:on_trial_callback)
|
79
|
+
trial.choose! context
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
59
83
|
def expect_alternative(trial, alternative_name)
|
60
84
|
3.times do
|
61
|
-
trial.choose!
|
62
|
-
expect(trial.alternative.name)
|
85
|
+
trial.choose! context
|
86
|
+
expect(alternative_name).to include(trial.alternative.name)
|
63
87
|
end
|
64
88
|
end
|
65
89
|
|
66
90
|
context "when override is present" do
|
91
|
+
let(:override) { 'cart' }
|
92
|
+
let(:trial) do
|
93
|
+
Split::Trial.new(:user => user, :experiment => experiment, :override => override)
|
94
|
+
end
|
95
|
+
|
96
|
+
it_behaves_like 'a trial with callbacks'
|
97
|
+
|
67
98
|
it "picks the override" do
|
68
|
-
trial = Split::Trial.new(:user => user, :experiment => experiment, :override => 'cart')
|
69
99
|
expect(experiment).to_not receive(:next_alternative)
|
100
|
+
expect_alternative(trial, override)
|
101
|
+
end
|
70
102
|
|
71
|
-
|
103
|
+
context "when alternative doesn't exist" do
|
104
|
+
let(:override) { nil }
|
105
|
+
it 'falls back on next_alternative' do
|
106
|
+
expect(experiment).to receive(:next_alternative).and_call_original
|
107
|
+
expect_alternative(trial, alternatives)
|
108
|
+
end
|
72
109
|
end
|
73
110
|
end
|
74
111
|
|
75
112
|
context "when disabled option is true" do
|
76
|
-
|
77
|
-
|
113
|
+
let(:trial) do
|
114
|
+
Split::Trial.new(:user => user, :experiment => experiment, :disabled => true)
|
115
|
+
end
|
116
|
+
|
117
|
+
it "picks the control", :aggregate_failures do
|
118
|
+
Split.configuration.on_trial = :on_trial_callback
|
78
119
|
expect(experiment).to_not receive(:next_alternative)
|
79
120
|
|
121
|
+
expect(context).not_to receive(:on_trial_callback)
|
122
|
+
|
80
123
|
expect_alternative(trial, 'basket')
|
124
|
+
Split.configuration.on_trial = nil
|
81
125
|
end
|
82
126
|
end
|
83
127
|
|
84
128
|
context "when Split is globally disabled" do
|
85
|
-
it "picks the control" do
|
129
|
+
it "picks the control and does not run on_trial callbacks", :aggregate_failures do
|
86
130
|
Split.configuration.enabled = false
|
87
|
-
|
88
|
-
expect(experiment).to_not receive(:next_alternative)
|
131
|
+
Split.configuration.on_trial = :on_trial_callback
|
89
132
|
|
133
|
+
expect(experiment).to_not receive(:next_alternative)
|
134
|
+
expect(context).not_to receive(:on_trial_callback)
|
90
135
|
expect_alternative(trial, 'basket')
|
136
|
+
|
91
137
|
Split.configuration.enabled = true
|
138
|
+
Split.configuration.on_trial = nil
|
92
139
|
end
|
93
140
|
end
|
94
141
|
|
95
142
|
context "when experiment has winner" do
|
143
|
+
let(:trial) do
|
144
|
+
Split::Trial.new(:user => user, :experiment => experiment)
|
145
|
+
end
|
146
|
+
|
147
|
+
it_behaves_like 'a trial with callbacks'
|
148
|
+
|
96
149
|
it "picks the winner" do
|
97
|
-
trial = Split::Trial.new(:user => user, :experiment => experiment)
|
98
150
|
experiment.winner = 'cart'
|
99
151
|
expect(experiment).to_not receive(:next_alternative)
|
100
152
|
|
@@ -103,18 +155,23 @@ describe Split::Trial do
|
|
103
155
|
end
|
104
156
|
|
105
157
|
context "when exclude is true" do
|
158
|
+
let(:trial) do
|
159
|
+
Split::Trial.new(:user => user, :experiment => experiment, :exclude => true)
|
160
|
+
end
|
161
|
+
|
162
|
+
it_behaves_like 'a trial with callbacks'
|
163
|
+
|
106
164
|
it "picks the control" do
|
107
|
-
trial = Split::Trial.new(:user => user, :experiment => experiment, :exclude => true)
|
108
165
|
expect(experiment).to_not receive(:next_alternative)
|
109
|
-
|
110
166
|
expect_alternative(trial, 'basket')
|
111
167
|
end
|
112
168
|
end
|
113
169
|
|
114
170
|
context "when user is already participating" do
|
171
|
+
it_behaves_like 'a trial with callbacks'
|
172
|
+
|
115
173
|
it "picks the same alternative" do
|
116
174
|
user[experiment.key] = 'basket'
|
117
|
-
trial = Split::Trial.new(:user => user, :experiment => experiment)
|
118
175
|
expect(experiment).to_not receive(:next_alternative)
|
119
176
|
|
120
177
|
expect_alternative(trial, 'basket')
|
@@ -122,95 +179,99 @@ describe Split::Trial do
|
|
122
179
|
end
|
123
180
|
|
124
181
|
context "when user is a new participant" do
|
125
|
-
it "picks a new alternative" do
|
126
|
-
|
182
|
+
it "picks a new alternative and runs on_trial_choose callback", :aggregate_failures do
|
183
|
+
Split.configuration.on_trial_choose = :on_trial_choose_callback
|
184
|
+
|
127
185
|
expect(experiment).to receive(:next_alternative).and_call_original
|
186
|
+
expect(context).to receive(:on_trial_choose_callback)
|
187
|
+
|
188
|
+
trial.choose! context
|
128
189
|
|
129
|
-
trial.choose!
|
130
190
|
expect(trial.alternative.name).to_not be_empty
|
191
|
+
Split.configuration.on_trial_choose = nil
|
131
192
|
end
|
132
193
|
end
|
194
|
+
end
|
133
195
|
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
196
|
+
describe "#complete!" do
|
197
|
+
let(:trial) { Split::Trial.new(:user => user, :experiment => experiment) }
|
198
|
+
context 'when there are no goals' do
|
199
|
+
it 'should complete the trial' do
|
200
|
+
trial.choose!
|
201
|
+
old_completed_count = trial.alternative.completed_count
|
202
|
+
trial.complete!
|
203
|
+
expect(trial.alternative.completed_count).to be(old_completed_count+1)
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
context 'when there are many goals' do
|
208
|
+
let(:goals) { ['first', 'second'] }
|
209
|
+
let(:trial) { Split::Trial.new(:user => user, :experiment => experiment, :goals => goals) }
|
210
|
+
shared_examples_for "goal completion" do
|
211
|
+
it 'should not complete the trial' do
|
138
212
|
trial.choose!
|
139
213
|
old_completed_count = trial.alternative.completed_count
|
140
|
-
trial.complete!
|
141
|
-
expect(trial.alternative.completed_count).
|
214
|
+
trial.complete!(goal)
|
215
|
+
expect(trial.alternative.completed_count).to_not be(old_completed_count+1)
|
142
216
|
end
|
143
217
|
end
|
144
218
|
|
145
|
-
|
146
|
-
let(:
|
147
|
-
|
148
|
-
|
149
|
-
it 'should not complete the trial' do
|
150
|
-
trial.choose!
|
151
|
-
old_completed_count = trial.alternative.completed_count
|
152
|
-
trial.complete!(goal)
|
153
|
-
expect(trial.alternative.completed_count).to_not be(old_completed_count+1)
|
154
|
-
end
|
155
|
-
end
|
156
|
-
|
157
|
-
describe 'Array of Goals' do
|
158
|
-
let(:goal) { [goals.first] }
|
159
|
-
it_behaves_like 'goal completion'
|
160
|
-
end
|
161
|
-
|
162
|
-
describe 'String of Goal' do
|
163
|
-
let(:goal) { goals.first }
|
164
|
-
it_behaves_like 'goal completion'
|
165
|
-
end
|
219
|
+
describe 'Array of Goals' do
|
220
|
+
let(:goal) { [goals.first] }
|
221
|
+
it_behaves_like 'goal completion'
|
222
|
+
end
|
166
223
|
|
224
|
+
describe 'String of Goal' do
|
225
|
+
let(:goal) { goals.first }
|
226
|
+
it_behaves_like 'goal completion'
|
167
227
|
end
|
228
|
+
|
168
229
|
end
|
230
|
+
end
|
169
231
|
|
170
|
-
|
171
|
-
|
232
|
+
describe "alternative recording" do
|
233
|
+
before(:each) { Split.configuration.store_override = false }
|
172
234
|
|
173
|
-
|
174
|
-
|
175
|
-
|
235
|
+
context "when override is present" do
|
236
|
+
it "stores when store_override is true" do
|
237
|
+
trial = Split::Trial.new(:user => user, :experiment => experiment, :override => 'basket')
|
176
238
|
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
239
|
+
Split.configuration.store_override = true
|
240
|
+
expect(user).to receive("[]=")
|
241
|
+
trial.choose!
|
242
|
+
end
|
181
243
|
|
182
|
-
|
183
|
-
|
244
|
+
it "does not store when store_override is false" do
|
245
|
+
trial = Split::Trial.new(:user => user, :experiment => experiment, :override => 'basket')
|
184
246
|
|
185
|
-
|
186
|
-
|
187
|
-
end
|
247
|
+
expect(user).to_not receive("[]=")
|
248
|
+
trial.choose!
|
188
249
|
end
|
250
|
+
end
|
189
251
|
|
190
|
-
|
191
|
-
|
192
|
-
|
252
|
+
context "when disabled is present" do
|
253
|
+
it "stores when store_override is true" do
|
254
|
+
trial = Split::Trial.new(:user => user, :experiment => experiment, :disabled => true)
|
193
255
|
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
256
|
+
Split.configuration.store_override = true
|
257
|
+
expect(user).to receive("[]=")
|
258
|
+
trial.choose!
|
259
|
+
end
|
198
260
|
|
199
|
-
|
200
|
-
|
261
|
+
it "does not store when store_override is false" do
|
262
|
+
trial = Split::Trial.new(:user => user, :experiment => experiment, :disabled => true)
|
201
263
|
|
202
|
-
|
203
|
-
|
204
|
-
end
|
264
|
+
expect(user).to_not receive("[]=")
|
265
|
+
trial.choose!
|
205
266
|
end
|
267
|
+
end
|
206
268
|
|
207
|
-
|
208
|
-
|
209
|
-
|
269
|
+
context "when exclude is present" do
|
270
|
+
it "does not store" do
|
271
|
+
trial = Split::Trial.new(:user => user, :experiment => experiment, :exclude => true)
|
210
272
|
|
211
|
-
|
212
|
-
|
213
|
-
end
|
273
|
+
expect(user).to_not receive("[]=")
|
274
|
+
trial.choose!
|
214
275
|
end
|
215
276
|
end
|
216
277
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: split
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.4.
|
4
|
+
version: 1.4.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew Nesbitt
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-04-
|
11
|
+
date: 2016-04-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: redis
|