split 1.0.0 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +3 -22
- data/Appraisals +5 -9
- data/{CHANGELOG.mdown → CHANGELOG.md} +17 -0
- data/Gemfile +1 -1
- data/LICENSE +2 -2
- data/{README.mdown → README.md} +3 -14
- data/gemfiles/4.0.gemfile +1 -1
- data/gemfiles/{3.1.gemfile → 4.1.gemfile} +1 -1
- data/lib/split/alternative.rb +1 -1
- data/lib/split/dashboard.rb +6 -6
- data/lib/split/experiment.rb +2 -19
- data/lib/split/experiment_catalog.rb +20 -11
- data/lib/split/helper.rb +36 -99
- data/lib/split/metric.rb +2 -2
- data/lib/split/persistence/redis_adapter.rb +4 -2
- data/lib/split/trial.rb +84 -22
- data/lib/split/version.rb +1 -1
- data/spec/algorithms/weighted_sample_spec.rb +3 -3
- data/spec/algorithms/whiplash_spec.rb +2 -2
- data/spec/alternative_spec.rb +1 -1
- data/spec/dashboard_spec.rb +3 -3
- data/spec/encapsulated_helper_spec.rb +2 -6
- data/spec/experiment_catalog_spec.rb +37 -0
- data/spec/experiment_spec.rb +26 -26
- data/spec/helper_spec.rb +55 -20
- data/spec/metric_spec.rb +5 -5
- data/spec/persistence/redis_adapter_spec.rb +9 -0
- data/spec/spec_helper.rb +1 -1
- data/spec/trial_spec.rb +126 -29
- data/split.gemspec +3 -3
- metadata +28 -30
- data/changes.rtf +0 -14
- data/gemfiles/3.0.gemfile +0 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ea348c09640ba8869242588929d2d337856fc834
|
4
|
+
data.tar.gz: 0bb555acfabcaf683051058097da01b803c56a50
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f411a5636ee2fcc0c296f2e3e1e24515af7524be73eec1fe8bd86998564eb61fcc60419d52dc02588a92ac85ecb28c6525e9a007cc5607efe1942a56c1fa3262
|
7
|
+
data.tar.gz: 6ff0d6a3be6295653be81d34e0c2479c2d38bd8e1ac6a2fb0aa20e5796329add32cf465b884d5ede25ba9121343ae888de62e78f9aff5e468604f635c2efabb6
|
data/.travis.yml
CHANGED
@@ -1,32 +1,13 @@
|
|
1
1
|
language: ruby
|
2
|
-
bundler_args: ''
|
3
2
|
rvm:
|
4
|
-
- 1.
|
5
|
-
- 1.9.3
|
6
|
-
- 2.0.0
|
7
|
-
- ruby-head
|
8
|
-
- jruby-19mode
|
9
|
-
- jruby-head
|
3
|
+
- 2.1.5
|
10
4
|
|
11
|
-
- rbx-19mode
|
12
|
-
matrix:
|
13
|
-
allow_failures:
|
14
|
-
- rvm: ruby-head
|
15
|
-
- rvm: rbx-19mode
|
16
|
-
- rvm: jruby-head
|
17
|
-
exclude:
|
18
|
-
- rvm: 1.9.2
|
19
|
-
gemfile: gemfiles/4.0.gemfile
|
20
|
-
- rvm: jruby-18mode
|
21
|
-
gemfile: gemfiles/4.0.gemfile
|
22
|
-
- rvm: rbx-18mode
|
23
|
-
gemfile: gemfiles/4.0.gemfile
|
24
5
|
gemfile:
|
25
|
-
- gemfiles/3.0.gemfile
|
26
|
-
- gemfiles/3.1.gemfile
|
27
6
|
- gemfiles/3.2.gemfile
|
28
7
|
- gemfiles/4.0.gemfile
|
8
|
+
- gemfiles/4.1.gemfile
|
29
9
|
|
30
10
|
services:
|
31
11
|
- redis-server
|
32
12
|
cache: bundler
|
13
|
+
sudo: false
|
data/Appraisals
CHANGED
@@ -1,15 +1,11 @@
|
|
1
|
-
appraise "3.0" do
|
2
|
-
gem "rails", "~> 3.0.20"
|
3
|
-
end
|
4
|
-
|
5
|
-
appraise "3.1" do
|
6
|
-
gem "rails", "~> 3.1.12"
|
7
|
-
end
|
8
|
-
|
9
1
|
appraise "3.2" do
|
10
2
|
gem "rails", "~> 3.2.13"
|
11
3
|
end
|
12
4
|
|
13
5
|
appraise "4.0" do
|
14
|
-
gem "rails", "4.0.
|
6
|
+
gem "rails", "~> 4.0.12"
|
7
|
+
end
|
8
|
+
|
9
|
+
appraise "4.1" do
|
10
|
+
gem "rails", "~> 4.1.8"
|
15
11
|
end
|
@@ -1,3 +1,20 @@
|
|
1
|
+
## 1.1.0 (January 9th, 2015)
|
2
|
+
|
3
|
+
Features:
|
4
|
+
|
5
|
+
- Decouple trial from Split::Helper (@joshdover, #286)
|
6
|
+
- Helper method for Active Experiments (@blahblahblah-, #273)
|
7
|
+
|
8
|
+
Misc:
|
9
|
+
|
10
|
+
- Use the new travis container based infrastructure for tests (@andrew, #280)
|
11
|
+
|
12
|
+
## 1.0.0 (October 12th, 2014)
|
13
|
+
|
14
|
+
Changes:
|
15
|
+
|
16
|
+
- Remove support for Ruby 1.8.7 and Rails 2.3 (@qpowell, #271)
|
17
|
+
|
1
18
|
## 0.8.0 (September 25th, 2014)
|
2
19
|
|
3
20
|
Features:
|
data/Gemfile
CHANGED
data/LICENSE
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
The MIT License (MIT)
|
2
2
|
|
3
|
-
Copyright (c)
|
3
|
+
Copyright (c) 2015 Andrew Nesbitt
|
4
4
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining
|
6
6
|
a copy of this software and associated documentation files (the
|
@@ -19,4 +19,4 @@ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
19
19
|
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
20
|
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
21
|
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
-
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/{README.mdown → README.md}
RENAMED
@@ -220,7 +220,7 @@ Using Redis will allow ab_users to persist across sessions or machines.
|
|
220
220
|
|
221
221
|
```ruby
|
222
222
|
Split.configure do |config|
|
223
|
-
config.persistence = Split::Persistence::RedisAdapter.with_config(:lookup_by => proc { |context| context.current_user_id }
|
223
|
+
config.persistence = Split::Persistence::RedisAdapter.with_config(:lookup_by => proc { |context| context.current_user_id })
|
224
224
|
# Equivalent
|
225
225
|
# config.persistence = Split::Persistence::RedisAdapter.with_config(:lookup_by => :current_user_id }
|
226
226
|
end
|
@@ -638,18 +638,7 @@ Ryan bates has produced an excellent 10 minute screencast about split on the Rai
|
|
638
638
|
|
639
639
|
## Contributors
|
640
640
|
|
641
|
-
|
642
|
-
|
643
|
-
* Lloyd Pick
|
644
|
-
* Jeffery Chupp
|
645
|
-
* Andrew Appleton
|
646
|
-
* Phil Nash
|
647
|
-
* Dave Goodchild
|
648
|
-
* Ian Young
|
649
|
-
* Nathan Woodhull
|
650
|
-
* Ville Lautanala
|
651
|
-
* Liu Jin
|
652
|
-
* Peter Schröder
|
641
|
+
Over 70 different people have contributed to the project, you can see them all here: https://github.com/andrew/split/graphs/contributors
|
653
642
|
|
654
643
|
## Development
|
655
644
|
|
@@ -672,4 +661,4 @@ Tests can be ran with `rake spec`
|
|
672
661
|
|
673
662
|
## Copyright
|
674
663
|
|
675
|
-
Copyright (c)
|
664
|
+
Copyright (c) 2015 Andrew Nesbitt. See [LICENSE](https://github.com/andrew/split/blob/master/LICENSE) for details.
|
data/gemfiles/4.0.gemfile
CHANGED
data/lib/split/alternative.rb
CHANGED
data/lib/split/dashboard.rb
CHANGED
@@ -16,7 +16,7 @@ module Split
|
|
16
16
|
|
17
17
|
get '/' do
|
18
18
|
# Display experiments without a winner at the top of the dashboard
|
19
|
-
@experiments = Split::
|
19
|
+
@experiments = Split::ExperimentCatalog.all_active_first
|
20
20
|
|
21
21
|
@metrics = Split::Metric.all
|
22
22
|
|
@@ -30,32 +30,32 @@ module Split
|
|
30
30
|
end
|
31
31
|
|
32
32
|
post '/:experiment' do
|
33
|
-
@experiment = Split::
|
33
|
+
@experiment = Split::ExperimentCatalog.find(params[:experiment])
|
34
34
|
@alternative = Split::Alternative.new(params[:alternative], params[:experiment])
|
35
35
|
@experiment.winner = @alternative.name
|
36
36
|
redirect url('/')
|
37
37
|
end
|
38
38
|
|
39
39
|
post '/start/:experiment' do
|
40
|
-
@experiment = Split::
|
40
|
+
@experiment = Split::ExperimentCatalog.find(params[:experiment])
|
41
41
|
@experiment.start
|
42
42
|
redirect url('/')
|
43
43
|
end
|
44
44
|
|
45
45
|
post '/reset/:experiment' do
|
46
|
-
@experiment = Split::
|
46
|
+
@experiment = Split::ExperimentCatalog.find(params[:experiment])
|
47
47
|
@experiment.reset
|
48
48
|
redirect url('/')
|
49
49
|
end
|
50
50
|
|
51
51
|
post '/reopen/:experiment' do
|
52
|
-
@experiment = Split::
|
52
|
+
@experiment = Split::ExperimentCatalog.find(params[:experiment])
|
53
53
|
@experiment.reset_winner
|
54
54
|
redirect url('/')
|
55
55
|
end
|
56
56
|
|
57
57
|
delete '/:experiment' do
|
58
|
-
@experiment = Split::
|
58
|
+
@experiment = Split::ExperimentCatalog.find(params[:experiment])
|
59
59
|
@experiment.delete
|
60
60
|
redirect url('/')
|
61
61
|
end
|
data/lib/split/experiment.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
|
1
|
+
module Split
|
2
2
|
class Experiment
|
3
3
|
attr_accessor :name
|
4
4
|
attr_writer :algorithm
|
@@ -8,7 +8,7 @@
|
|
8
8
|
attr_accessor :alternative_probabilities
|
9
9
|
|
10
10
|
DEFAULT_OPTIONS = {
|
11
|
-
:resettable => true
|
11
|
+
:resettable => true
|
12
12
|
}
|
13
13
|
|
14
14
|
def initialize(name, options = {})
|
@@ -71,23 +71,6 @@
|
|
71
71
|
alts
|
72
72
|
end
|
73
73
|
|
74
|
-
def self.all
|
75
|
-
ExperimentCatalog.all
|
76
|
-
end
|
77
|
-
|
78
|
-
# Return experiments without a winner (considered "active") first
|
79
|
-
def self.all_active_first
|
80
|
-
ExperimentCatalog.all_active_first
|
81
|
-
end
|
82
|
-
|
83
|
-
def self.find(name)
|
84
|
-
ExperimentCatalog.find(name)
|
85
|
-
end
|
86
|
-
|
87
|
-
def self.find_or_create(label, *alternatives)
|
88
|
-
ExperimentCatalog.find_or_create(label, *alternatives)
|
89
|
-
end
|
90
|
-
|
91
74
|
def save
|
92
75
|
validate!
|
93
76
|
|
@@ -21,23 +21,32 @@ module Split
|
|
21
21
|
obj
|
22
22
|
end
|
23
23
|
|
24
|
-
def self.
|
25
|
-
|
26
|
-
|
24
|
+
def self.find_or_initialize(metric_descriptor, control = nil, *alternatives)
|
25
|
+
# Check if array is passed to ab_test
|
26
|
+
# e.g. ab_test('name', ['Alt 1', 'Alt 2', 'Alt 3'])
|
27
|
+
if control.is_a? Array and alternatives.length.zero?
|
28
|
+
control, alternatives = control.first, control[1..-1]
|
29
|
+
end
|
30
|
+
|
31
|
+
experiment_name_with_version, goals = normalize_experiment(metric_descriptor)
|
32
|
+
experiment_name = experiment_name_with_version.to_s.split(':')[0]
|
33
|
+
Split::Experiment.new(experiment_name,
|
34
|
+
:alternatives => [control].compact + alternatives, :goals => goals)
|
35
|
+
end
|
27
36
|
|
28
|
-
|
29
|
-
|
30
|
-
|
37
|
+
def self.find_or_create(metric_descriptor, control = nil, *alternatives)
|
38
|
+
experiment = find_or_initialize(metric_descriptor, control, *alternatives)
|
39
|
+
experiment.save
|
31
40
|
end
|
32
41
|
|
33
42
|
private
|
34
43
|
|
35
|
-
def self.normalize_experiment(
|
36
|
-
if Hash ===
|
37
|
-
experiment_name =
|
38
|
-
goals =
|
44
|
+
def self.normalize_experiment(metric_descriptor)
|
45
|
+
if Hash === metric_descriptor
|
46
|
+
experiment_name = metric_descriptor.keys.first
|
47
|
+
goals = Array(metric_descriptor.values.first)
|
39
48
|
else
|
40
|
-
experiment_name =
|
49
|
+
experiment_name = metric_descriptor
|
41
50
|
goals = []
|
42
51
|
end
|
43
52
|
return experiment_name, goals
|
data/lib/split/helper.rb
CHANGED
@@ -1,51 +1,42 @@
|
|
1
1
|
module Split
|
2
2
|
module Helper
|
3
3
|
|
4
|
-
def ab_test(metric_descriptor, control=nil, *alternatives)
|
5
|
-
|
6
|
-
# Check if array is passed to ab_test
|
7
|
-
# e.g. ab_test('name', ['Alt 1', 'Alt 2', 'Alt 3'])
|
8
|
-
if control.is_a? Array and alternatives.length.zero?
|
9
|
-
control, alternatives = control.first, control[1..-1]
|
10
|
-
end
|
11
|
-
|
4
|
+
def ab_test(metric_descriptor, control = nil, *alternatives)
|
12
5
|
begin
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
experiment_name,
|
17
|
-
:alternatives => [control].compact + alternatives,
|
18
|
-
:goals => goals)
|
19
|
-
control ||= experiment.control && experiment.control.name
|
20
|
-
|
21
|
-
ret = if Split.configuration.enabled
|
6
|
+
experiment = ExperimentCatalog.find_or_initialize(metric_descriptor, control, *alternatives)
|
7
|
+
|
8
|
+
alternative = if Split.configuration.enabled
|
22
9
|
experiment.save
|
23
|
-
|
10
|
+
trial = Trial.new(:user => ab_user, :experiment => experiment,
|
11
|
+
:override => override_alternative(experiment.name), :exclude => exclude_visitor?,
|
12
|
+
:disabled => split_generically_disabled?)
|
13
|
+
alt = trial.choose!(self)
|
14
|
+
alt ? alt.name : nil
|
24
15
|
else
|
25
|
-
control_variable(control)
|
16
|
+
control_variable(experiment.control)
|
26
17
|
end
|
27
18
|
rescue Errno::ECONNREFUSED, Redis::CannotConnectError => e
|
28
19
|
raise(e) unless Split.configuration.db_failover
|
29
20
|
Split.configuration.db_failover_on_db_error.call(e)
|
30
21
|
|
31
22
|
if Split.configuration.db_failover_allow_parameter_override
|
32
|
-
|
33
|
-
|
23
|
+
alternative = override_alternative(experiment.name) if override_present?(experiment.name)
|
24
|
+
alternative = control_variable(experiment.control) if split_generically_disabled?
|
34
25
|
end
|
35
26
|
ensure
|
36
|
-
|
27
|
+
alternative ||= control_variable(experiment.control)
|
37
28
|
end
|
38
29
|
|
39
30
|
if block_given?
|
40
31
|
if defined?(capture) # a block in a rails view
|
41
|
-
block = Proc.new { yield(
|
42
|
-
concat(capture(
|
32
|
+
block = Proc.new { yield(alternative) }
|
33
|
+
concat(capture(alternative, &block))
|
43
34
|
false
|
44
35
|
else
|
45
|
-
yield(
|
36
|
+
yield(alternative)
|
46
37
|
end
|
47
38
|
else
|
48
|
-
|
39
|
+
alternative
|
49
40
|
end
|
50
41
|
end
|
51
42
|
|
@@ -60,8 +51,9 @@ module Split
|
|
60
51
|
return true
|
61
52
|
else
|
62
53
|
alternative_name = ab_user[experiment.key]
|
63
|
-
trial = Trial.new(:
|
64
|
-
|
54
|
+
trial = Trial.new(:user => ab_user, :experiment => experiment,
|
55
|
+
:alternative => alternative_name)
|
56
|
+
trial.complete!(options[:goals], self)
|
65
57
|
|
66
58
|
if should_reset
|
67
59
|
reset!(experiment)
|
@@ -74,7 +66,7 @@ module Split
|
|
74
66
|
|
75
67
|
def finished(metric_descriptor, options = {:reset => true})
|
76
68
|
return if exclude_visitor? || Split.configuration.disabled?
|
77
|
-
metric_descriptor, goals =
|
69
|
+
metric_descriptor, goals = normalize_metric(metric_descriptor)
|
78
70
|
experiments = Metric.possible_experiments(metric_descriptor)
|
79
71
|
|
80
72
|
if experiments.any?
|
@@ -110,30 +102,7 @@ module Split
|
|
110
102
|
end
|
111
103
|
|
112
104
|
def exclude_visitor?
|
113
|
-
instance_eval(&Split.configuration.ignore_filter)
|
114
|
-
end
|
115
|
-
|
116
|
-
def not_allowed_to_test?(experiment_key)
|
117
|
-
!Split.configuration.allow_multiple_experiments && doing_other_tests?(experiment_key)
|
118
|
-
end
|
119
|
-
|
120
|
-
def doing_other_tests?(experiment_key)
|
121
|
-
keys_without_experiment(ab_user.keys, experiment_key).length > 0
|
122
|
-
end
|
123
|
-
|
124
|
-
def clean_old_versions(experiment)
|
125
|
-
old_versions(experiment).each do |old_key|
|
126
|
-
ab_user.delete old_key
|
127
|
-
end
|
128
|
-
end
|
129
|
-
|
130
|
-
def old_versions(experiment)
|
131
|
-
if experiment.version > 0
|
132
|
-
keys = ab_user.keys.select { |k| k.match(Regexp.new(experiment.name)) }
|
133
|
-
keys_without_experiment(keys, experiment.key)
|
134
|
-
else
|
135
|
-
[]
|
136
|
-
end
|
105
|
+
instance_eval(&Split.configuration.ignore_filter) || is_ignored_ip_address? || is_robot?
|
137
106
|
end
|
138
107
|
|
139
108
|
def is_robot?
|
@@ -149,9 +118,21 @@ module Split
|
|
149
118
|
false
|
150
119
|
end
|
151
120
|
|
121
|
+
def active_experiments
|
122
|
+
experiment_pairs = {}
|
123
|
+
ab_user.keys.each do |key|
|
124
|
+
Metric.possible_experiments(key).each do |experiment|
|
125
|
+
if !experiment.has_winner?
|
126
|
+
experiment_pairs[key] = ab_user[key]
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
return experiment_pairs
|
131
|
+
end
|
132
|
+
|
152
133
|
protected
|
153
134
|
|
154
|
-
def
|
135
|
+
def normalize_metric(metric_descriptor)
|
155
136
|
if Hash === metric_descriptor
|
156
137
|
experiment_name = metric_descriptor.keys.first
|
157
138
|
goals = Array(metric_descriptor.values.first)
|
@@ -163,51 +144,7 @@ module Split
|
|
163
144
|
end
|
164
145
|
|
165
146
|
def control_variable(control)
|
166
|
-
Hash === control ? control.keys.first : control
|
167
|
-
end
|
168
|
-
|
169
|
-
def start_trial(trial)
|
170
|
-
experiment = trial.experiment
|
171
|
-
if override_present?(experiment.name) and experiment[override_alternative(experiment.name)]
|
172
|
-
ret = override_alternative(experiment.name)
|
173
|
-
ab_user[experiment.key] = ret if Split.configuration.store_override
|
174
|
-
elsif split_generically_disabled?
|
175
|
-
ret = experiment.control.name
|
176
|
-
ab_user[experiment.key] = ret if Split.configuration.store_override
|
177
|
-
elsif experiment.has_winner?
|
178
|
-
ret = experiment.winner.name
|
179
|
-
else
|
180
|
-
clean_old_versions(experiment)
|
181
|
-
if exclude_visitor? || not_allowed_to_test?(experiment.key) || not_started?(experiment)
|
182
|
-
ret = experiment.control.name
|
183
|
-
else
|
184
|
-
if ab_user[experiment.key]
|
185
|
-
ret = ab_user[experiment.key]
|
186
|
-
else
|
187
|
-
trial.choose!
|
188
|
-
call_trial_choose_hook(trial)
|
189
|
-
ret = begin_experiment(experiment, trial.alternative.name)
|
190
|
-
end
|
191
|
-
end
|
192
|
-
end
|
193
|
-
|
194
|
-
ret
|
195
|
-
end
|
196
|
-
|
197
|
-
def not_started?(experiment)
|
198
|
-
experiment.start_time.nil?
|
199
|
-
end
|
200
|
-
|
201
|
-
def call_trial_choose_hook(trial)
|
202
|
-
send(Split.configuration.on_trial_choose, trial) if Split.configuration.on_trial_choose
|
203
|
-
end
|
204
|
-
|
205
|
-
def call_trial_complete_hook(trial)
|
206
|
-
send(Split.configuration.on_trial_complete, trial) if Split.configuration.on_trial_complete
|
207
|
-
end
|
208
|
-
|
209
|
-
def keys_without_experiment(keys, experiment_key)
|
210
|
-
keys.reject { |k| k.match(Regexp.new("^#{experiment_key}(:finished)?$")) }
|
147
|
+
Hash === control ? control.keys.first.to_s : control.to_s
|
211
148
|
end
|
212
149
|
end
|
213
150
|
end
|