split 1.0.0 → 1.1.0
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 +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
|