split 0.6.2 → 0.6.3
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/CHANGELOG.mdown +10 -0
- data/README.mdown +42 -1
- data/lib/split/configuration.rb +2 -0
- data/lib/split/experiment.rb +9 -2
- data/lib/split/helper.rb +11 -0
- data/lib/split/version.rb +1 -1
- data/spec/configuration_spec.rb +2 -2
- data/spec/experiment_spec.rb +10 -1
- data/spec/helper_spec.rb +28 -12
- data/spec/persistence/cookie_adapter_spec.rb +1 -1
- data/spec/persistence/session_adapter_spec.rb +1 -1
- data/spec/persistence_spec.rb +1 -1
- data/spec/spec_helper.rb +4 -0
- data/spec/trial_spec.rb +6 -6
- data/split.gemspec +2 -1
- metadata +21 -4
data/.gitignore
CHANGED
data/CHANGELOG.mdown
CHANGED
data/README.mdown
CHANGED
@@ -9,7 +9,8 @@ Split is designed to be hacker friendly, allowing for maximum customisation and
|
|
9
9
|
[![Gem Version](https://badge.fury.io/rb/split.png)](http://badge.fury.io/rb/split)
|
10
10
|
[![Build Status](https://secure.travis-ci.org/andrew/split.png?branch=master)](http://travis-ci.org/andrew/split)
|
11
11
|
[![Dependency Status](https://gemnasium.com/andrew/split.png)](https://gemnasium.com/andrew/split)
|
12
|
-
[![Code Climate](https://codeclimate.com/
|
12
|
+
[![Code Climate](https://codeclimate.com/github/andrew/split.png)](https://codeclimate.com/github/andrew/split)
|
13
|
+
[![Coverage Status](https://coveralls.io/repos/andrew/split/badge.png)](https://coveralls.io/r/andrew/split)
|
13
14
|
|
14
15
|
## Requirements
|
15
16
|
|
@@ -217,6 +218,35 @@ Split.configure do |config|
|
|
217
218
|
end
|
218
219
|
```
|
219
220
|
|
221
|
+
### Trial Event Hooks
|
222
|
+
|
223
|
+
You can define methods that will be called at the same time as experiment
|
224
|
+
alternative participation and goal completion.
|
225
|
+
|
226
|
+
For example:
|
227
|
+
|
228
|
+
``` ruby
|
229
|
+
Split.configure do |config|
|
230
|
+
config.on_trial_choose = :log_trial_choice
|
231
|
+
config.on_trial_complete = :log_trial_complete
|
232
|
+
end
|
233
|
+
```
|
234
|
+
|
235
|
+
Set these attributes to a method name available in the same context as the
|
236
|
+
`ab_test` method. These methods should accept one argument, a `Trial` instance.
|
237
|
+
|
238
|
+
``` ruby
|
239
|
+
def log_trial_choose(trial)
|
240
|
+
logger.info "experiment=%s alternative=%s user=%s" %
|
241
|
+
[ trial.experiment.name, trial.alternative, current_user.id ]
|
242
|
+
end
|
243
|
+
|
244
|
+
def log_trial_complete(trial)
|
245
|
+
logger.info "experiment=%s alternative=%s user=%s complete=true" %
|
246
|
+
[ trial.experiment.name, trial.alternative, current_user.id ]
|
247
|
+
end
|
248
|
+
```
|
249
|
+
|
220
250
|
## Web Interface
|
221
251
|
|
222
252
|
Split comes with a Sinatra-based front end to get an overview of how your experiments are doing.
|
@@ -251,6 +281,17 @@ Split::Dashboard.use Rack::Auth::Basic do |username, password|
|
|
251
281
|
end
|
252
282
|
```
|
253
283
|
|
284
|
+
You can even use Devise or any other Warden-based authentication method to authorize users. Just replace `mount Split::Dashboard, :at => 'split'` in `config/routes.rb` with the following:
|
285
|
+
```ruby
|
286
|
+
match "/split" => Split::Dashboard, :anchor => false, :constraints => lambda { |request|
|
287
|
+
request.env['warden'].authenticated? # are we authenticated?
|
288
|
+
request.env['warden'].authenticate! # authenticate if not already
|
289
|
+
# or even check any other condition such as request.env['warden'].user.is_admin?
|
290
|
+
}
|
291
|
+
```
|
292
|
+
|
293
|
+
More information on this [here](http://steve.dynedge.co.uk/2011/12/09/controlling-access-to-routes-and-rack-apps-in-rails-3-with-devise-and-warden/)
|
294
|
+
|
254
295
|
### Screenshot
|
255
296
|
|
256
297
|
![split_screenshot](https://f.cloud.github.com/assets/78887/306152/99c64650-9670-11e2-93f8-197f49495d02.png)
|
data/lib/split/configuration.rb
CHANGED
data/lib/split/experiment.rb
CHANGED
@@ -65,7 +65,7 @@ module Split
|
|
65
65
|
|
66
66
|
if new_record?
|
67
67
|
Split.redis.sadd(:experiments, name)
|
68
|
-
Split.redis.hset(:experiment_start_times, @name, Time.now)
|
68
|
+
Split.redis.hset(:experiment_start_times, @name, Time.now.to_i)
|
69
69
|
@alternatives.reverse.each {|a| Split.redis.lpush(name, a.name)}
|
70
70
|
@goals.reverse.each {|a| Split.redis.lpush(goals_key, a)} unless @goals.nil?
|
71
71
|
else
|
@@ -157,7 +157,14 @@ module Split
|
|
157
157
|
|
158
158
|
def start_time
|
159
159
|
t = Split.redis.hget(:experiment_start_times, @name)
|
160
|
-
|
160
|
+
if t
|
161
|
+
# Check if stored time is an integer
|
162
|
+
if t =~ /^[-+]?[0-9]+$/
|
163
|
+
t = Time.at(t.to_i)
|
164
|
+
else
|
165
|
+
t = Time.parse(t)
|
166
|
+
end
|
167
|
+
end
|
161
168
|
end
|
162
169
|
|
163
170
|
def next_alternative
|
data/lib/split/helper.rb
CHANGED
@@ -63,6 +63,8 @@ module Split
|
|
63
63
|
alternative_name = ab_user[experiment.key]
|
64
64
|
trial = Trial.new(:experiment => experiment, :alternative => alternative_name, :goals => options[:goals])
|
65
65
|
trial.complete!
|
66
|
+
call_trial_complete_hook(trial)
|
67
|
+
|
66
68
|
if should_reset
|
67
69
|
reset!(experiment)
|
68
70
|
else
|
@@ -178,6 +180,7 @@ module Split
|
|
178
180
|
ret = ab_user[experiment.key]
|
179
181
|
else
|
180
182
|
trial.choose!
|
183
|
+
call_trial_choose_hook(trial)
|
181
184
|
ret = begin_experiment(experiment, trial.alternative.name)
|
182
185
|
end
|
183
186
|
end
|
@@ -186,6 +189,14 @@ module Split
|
|
186
189
|
ret
|
187
190
|
end
|
188
191
|
|
192
|
+
def call_trial_choose_hook(trial)
|
193
|
+
send(Split.configuration.on_trial_choose, trial) if Split.configuration.on_trial_choose
|
194
|
+
end
|
195
|
+
|
196
|
+
def call_trial_complete_hook(trial)
|
197
|
+
send(Split.configuration.on_trial_complete, trial) if Split.configuration.on_trial_complete
|
198
|
+
end
|
199
|
+
|
189
200
|
def keys_without_experiment(keys, experiment_key)
|
190
201
|
keys.reject { |k| k.match(Regexp.new("^#{experiment_key}(:finished)?$")) }
|
191
202
|
end
|
data/lib/split/version.rb
CHANGED
data/spec/configuration_spec.rb
CHANGED
@@ -151,7 +151,7 @@ describe Split::Configuration do
|
|
151
151
|
let(:input) { '' }
|
152
152
|
|
153
153
|
it "should raise an error" do
|
154
|
-
expect { @config.experiments = yaml }.to raise_error
|
154
|
+
expect { @config.experiments = yaml }.to raise_error
|
155
155
|
end
|
156
156
|
end
|
157
157
|
|
@@ -159,7 +159,7 @@ describe Split::Configuration do
|
|
159
159
|
let(:input) { '---' }
|
160
160
|
|
161
161
|
it "should raise an error" do
|
162
|
-
expect { @config.experiments = yaml }.to raise_error
|
162
|
+
expect { @config.experiments = yaml }.to raise_error
|
163
163
|
end
|
164
164
|
end
|
165
165
|
end
|
data/spec/experiment_spec.rb
CHANGED
@@ -44,7 +44,7 @@ describe Split::Experiment do
|
|
44
44
|
end
|
45
45
|
|
46
46
|
it "should save the start time to redis" do
|
47
|
-
experiment_start_time = Time.
|
47
|
+
experiment_start_time = Time.at(1372167761)
|
48
48
|
Time.stub(:now => experiment_start_time)
|
49
49
|
experiment.save
|
50
50
|
|
@@ -59,6 +59,15 @@ describe Split::Experiment do
|
|
59
59
|
Split::Experiment.find('basket_text').algorithm.should == experiment_algorithm
|
60
60
|
end
|
61
61
|
|
62
|
+
it "should handle having a start time stored as a string" do
|
63
|
+
experiment_start_time = Time.parse("Sat Mar 03 14:01:03")
|
64
|
+
Time.stub(:now => experiment_start_time)
|
65
|
+
experiment.save
|
66
|
+
Split.redis.hset(:experiment_start_times, experiment.name, experiment_start_time)
|
67
|
+
|
68
|
+
Split::Experiment.find('basket_text').start_time.should == experiment_start_time
|
69
|
+
end
|
70
|
+
|
62
71
|
it "should handle not having a start time" do
|
63
72
|
experiment_start_time = Time.parse("Sat Mar 03 14:01:03")
|
64
73
|
Time.stub(:now => experiment_start_time)
|
data/spec/helper_spec.rb
CHANGED
@@ -20,11 +20,11 @@ describe Split::Helper do
|
|
20
20
|
end
|
21
21
|
|
22
22
|
it "should raise the appropriate error when passed integers for alternatives" do
|
23
|
-
lambda { ab_test('xyz', 1, 2, 3) }.should raise_error
|
23
|
+
lambda { ab_test('xyz', 1, 2, 3) }.should raise_error
|
24
24
|
end
|
25
25
|
|
26
26
|
it "should raise the appropriate error when passed symbols for alternatives" do
|
27
|
-
lambda { ab_test('xyz', :a, :b, :c) }.should raise_error
|
27
|
+
lambda { ab_test('xyz', :a, :b, :c) }.should raise_error
|
28
28
|
end
|
29
29
|
|
30
30
|
it "should not raise error when passed an array for goals" do
|
@@ -112,6 +112,14 @@ describe Split::Helper do
|
|
112
112
|
end
|
113
113
|
end
|
114
114
|
|
115
|
+
context "when on_trial_choose is set" do
|
116
|
+
before { Split.configuration.on_trial_choose = :some_method }
|
117
|
+
it "should call the method" do
|
118
|
+
self.should_receive(:some_method)
|
119
|
+
ab_test('link_color', 'blue', 'red')
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
115
123
|
it "should allow passing a block" do
|
116
124
|
alt = ab_test('link_color', 'blue', 'red')
|
117
125
|
ret = ab_test('link_color', 'blue', 'red') { |alternative| "shared/#{alternative}" }
|
@@ -247,6 +255,14 @@ describe Split::Helper do
|
|
247
255
|
doing_other_tests?(@experiment.key).should be false
|
248
256
|
end
|
249
257
|
|
258
|
+
context "when on_trial_complete is set" do
|
259
|
+
before { Split.configuration.on_trial_complete = :some_method }
|
260
|
+
it "should call the method" do
|
261
|
+
self.should_receive(:some_method)
|
262
|
+
finished(@experiment_name)
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
250
266
|
end
|
251
267
|
|
252
268
|
context "finished with config" do
|
@@ -273,7 +289,7 @@ describe Split::Helper do
|
|
273
289
|
alts = Split.configuration.experiments[experiment_name][:alternatives]
|
274
290
|
experiment = Split::Experiment.find_or_create(experiment_name, *alts)
|
275
291
|
alt_name = ab_user[experiment.key] = alts.first
|
276
|
-
alt =
|
292
|
+
alt = double('alternative')
|
277
293
|
alt.stub(:name).and_return(alt_name)
|
278
294
|
Split::Alternative.stub(:new).with(alt_name, experiment_name.to_s).and_return(alt)
|
279
295
|
if should_finish
|
@@ -588,7 +604,7 @@ describe Split::Helper do
|
|
588
604
|
it 'should raise an exception' do
|
589
605
|
lambda {
|
590
606
|
ab_test('link_color', 'blue', 'red')
|
591
|
-
}.should raise_error
|
607
|
+
}.should raise_error
|
592
608
|
end
|
593
609
|
end
|
594
610
|
|
@@ -596,7 +612,7 @@ describe Split::Helper do
|
|
596
612
|
it 'should raise an exception' do
|
597
613
|
lambda {
|
598
614
|
finished('link_color')
|
599
|
-
}.should raise_error
|
615
|
+
}.should raise_error
|
600
616
|
end
|
601
617
|
end
|
602
618
|
|
@@ -618,14 +634,14 @@ describe Split::Helper do
|
|
618
634
|
|
619
635
|
lambda {
|
620
636
|
ab_test('link_color', 'blue', 'red')
|
621
|
-
}.should_not raise_error
|
637
|
+
}.should_not raise_error
|
622
638
|
end
|
623
639
|
|
624
640
|
it "should return control variable" do
|
625
641
|
ab_test('link_color', 'blue', 'red').should eq('blue')
|
626
642
|
lambda {
|
627
643
|
finished('link_color')
|
628
|
-
}.should_not raise_error
|
644
|
+
}.should_not raise_error
|
629
645
|
end
|
630
646
|
|
631
647
|
end
|
@@ -645,7 +661,7 @@ describe Split::Helper do
|
|
645
661
|
it 'should not raise an exception' do
|
646
662
|
lambda {
|
647
663
|
ab_test('link_color', 'blue', 'red')
|
648
|
-
}.should_not raise_error
|
664
|
+
}.should_not raise_error
|
649
665
|
end
|
650
666
|
it 'should call db_failover_on_db_error proc with error as parameter' do
|
651
667
|
Split.configure do |config|
|
@@ -703,7 +719,7 @@ describe Split::Helper do
|
|
703
719
|
it 'should not raise an exception' do
|
704
720
|
lambda {
|
705
721
|
finished('link_color')
|
706
|
-
}.should_not raise_error
|
722
|
+
}.should_not raise_error
|
707
723
|
end
|
708
724
|
it 'should call db_failover_on_db_error proc with error as parameter' do
|
709
725
|
Split.configure do |config|
|
@@ -817,16 +833,16 @@ describe Split::Helper do
|
|
817
833
|
|
818
834
|
it "fails gracefully if config is missing experiment" do
|
819
835
|
Split.configuration.experiments = { :other_experiment => { :foo => "Bar" } }
|
820
|
-
lambda { ab_test :my_experiment }.should raise_error
|
836
|
+
lambda { ab_test :my_experiment }.should raise_error
|
821
837
|
end
|
822
838
|
|
823
839
|
it "fails gracefully if config is missing" do
|
824
|
-
lambda { Split.configuration.experiments = nil }.should raise_error
|
840
|
+
lambda { Split.configuration.experiments = nil }.should raise_error
|
825
841
|
end
|
826
842
|
|
827
843
|
it "fails gracefully if config is missing alternatives" do
|
828
844
|
Split.configuration.experiments[:my_experiment] = { :foo => "Bar" }
|
829
|
-
lambda { ab_test :my_experiment }.should raise_error
|
845
|
+
lambda { ab_test :my_experiment }.should raise_error
|
830
846
|
end
|
831
847
|
end
|
832
848
|
|
@@ -2,7 +2,7 @@ require "spec_helper"
|
|
2
2
|
|
3
3
|
describe Split::Persistence::CookieAdapter do
|
4
4
|
|
5
|
-
let(:context) {
|
5
|
+
let(:context) { double(:cookies => CookiesMock.new) }
|
6
6
|
subject { Split::Persistence::CookieAdapter.new(context) }
|
7
7
|
|
8
8
|
describe "#[] and #[]=" do
|
data/spec/persistence_spec.rb
CHANGED
@@ -18,7 +18,7 @@ describe Split::Persistence do
|
|
18
18
|
|
19
19
|
it "should raise if the adapter cannot be found" do
|
20
20
|
Split.configuration.stub(:persistence).and_return(:something_weird)
|
21
|
-
expect { subject.adapter }.to raise_error
|
21
|
+
expect { subject.adapter }.to raise_error
|
22
22
|
end
|
23
23
|
end
|
24
24
|
context "when the persistence config is a class" do
|
data/spec/spec_helper.rb
CHANGED
data/spec/trial_spec.rb
CHANGED
@@ -3,8 +3,8 @@ require 'split/trial'
|
|
3
3
|
|
4
4
|
describe Split::Trial do
|
5
5
|
it "should be initializeable" do
|
6
|
-
experiment =
|
7
|
-
alternative =
|
6
|
+
experiment = double('experiment')
|
7
|
+
alternative = double('alternative', :kind_of? => Split::Alternative)
|
8
8
|
trial = Split::Trial.new(:experiment => experiment, :alternative => alternative)
|
9
9
|
trial.experiment.should == experiment
|
10
10
|
trial.alternative.should == alternative
|
@@ -13,8 +13,8 @@ describe Split::Trial do
|
|
13
13
|
|
14
14
|
describe "alternative" do
|
15
15
|
it "should use the alternative if specified" do
|
16
|
-
alternative =
|
17
|
-
trial = Split::Trial.new(:experiment => experiment =
|
16
|
+
alternative = double('alternative', :kind_of? => Split::Alternative)
|
17
|
+
trial = Split::Trial.new(:experiment => experiment = double('experiment'), :alternative => alternative)
|
18
18
|
trial.should_not_receive(:choose)
|
19
19
|
trial.alternative.should == alternative
|
20
20
|
end
|
@@ -39,8 +39,8 @@ describe Split::Trial do
|
|
39
39
|
|
40
40
|
|
41
41
|
it "should choose from the available alternatives" do
|
42
|
-
trial = Split::Trial.new(:experiment => experiment =
|
43
|
-
alternative =
|
42
|
+
trial = Split::Trial.new(:experiment => experiment = double('experiment'))
|
43
|
+
alternative = double('alternative', :kind_of? => Split::Alternative)
|
44
44
|
experiment.should_receive(:next_alternative).and_return(alternative)
|
45
45
|
alternative.should_receive(:increment_participation)
|
46
46
|
experiment.stub(:winner).and_return nil
|
data/split.gemspec
CHANGED
@@ -29,6 +29,7 @@ Gem::Specification.new do |s|
|
|
29
29
|
|
30
30
|
s.add_development_dependency 'rake'
|
31
31
|
s.add_development_dependency 'bundler', '~> 1.3'
|
32
|
-
s.add_development_dependency 'rspec', '~> 2.
|
32
|
+
s.add_development_dependency 'rspec', '~> 2.14'
|
33
33
|
s.add_development_dependency 'rack-test', '>= 0.5.7'
|
34
|
+
s.add_development_dependency 'coveralls'
|
34
35
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: split
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.6.
|
4
|
+
version: 0.6.3
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-
|
12
|
+
date: 2013-07-08 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: redis
|
@@ -114,7 +114,7 @@ dependencies:
|
|
114
114
|
requirements:
|
115
115
|
- - ~>
|
116
116
|
- !ruby/object:Gem::Version
|
117
|
-
version: '2.
|
117
|
+
version: '2.14'
|
118
118
|
type: :development
|
119
119
|
prerelease: false
|
120
120
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -122,7 +122,7 @@ dependencies:
|
|
122
122
|
requirements:
|
123
123
|
- - ~>
|
124
124
|
- !ruby/object:Gem::Version
|
125
|
-
version: '2.
|
125
|
+
version: '2.14'
|
126
126
|
- !ruby/object:Gem::Dependency
|
127
127
|
name: rack-test
|
128
128
|
requirement: !ruby/object:Gem::Requirement
|
@@ -139,6 +139,22 @@ dependencies:
|
|
139
139
|
- - ! '>='
|
140
140
|
- !ruby/object:Gem::Version
|
141
141
|
version: 0.5.7
|
142
|
+
- !ruby/object:Gem::Dependency
|
143
|
+
name: coveralls
|
144
|
+
requirement: !ruby/object:Gem::Requirement
|
145
|
+
none: false
|
146
|
+
requirements:
|
147
|
+
- - ! '>='
|
148
|
+
- !ruby/object:Gem::Version
|
149
|
+
version: '0'
|
150
|
+
type: :development
|
151
|
+
prerelease: false
|
152
|
+
version_requirements: !ruby/object:Gem::Requirement
|
153
|
+
none: false
|
154
|
+
requirements:
|
155
|
+
- - ! '>='
|
156
|
+
- !ruby/object:Gem::Version
|
157
|
+
version: '0'
|
142
158
|
description:
|
143
159
|
email:
|
144
160
|
- andrewnez@gmail.com
|
@@ -247,3 +263,4 @@ test_files:
|
|
247
263
|
- spec/spec_helper.rb
|
248
264
|
- spec/support/cookies_mock.rb
|
249
265
|
- spec/trial_spec.rb
|
266
|
+
has_rdoc:
|