split 3.3.2 → 3.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.eslintrc +1 -1
- data/.github/ISSUE_TEMPLATE/bug_report.md +24 -0
- data/.rspec +1 -0
- data/.rubocop.yml +6 -1155
- data/.rubocop_todo.yml +679 -0
- data/.travis.yml +8 -14
- data/Appraisals +1 -1
- data/CHANGELOG.md +22 -0
- data/CODE_OF_CONDUCT.md +3 -3
- data/Gemfile +1 -0
- data/README.md +15 -13
- data/Rakefile +1 -0
- data/gemfiles/6.0.gemfile +1 -1
- data/lib/split/algorithms/block_randomization.rb +1 -0
- data/lib/split/alternative.rb +3 -3
- data/lib/split/combined_experiments_helper.rb +1 -1
- data/lib/split/configuration.rb +5 -2
- data/lib/split/dashboard.rb +4 -1
- data/lib/split/dashboard/pagination_helpers.rb +2 -3
- data/lib/split/dashboard/views/layout.erb +1 -1
- data/lib/split/engine.rb +6 -4
- data/lib/split/experiment.rb +29 -18
- data/lib/split/goals_collection.rb +1 -0
- data/lib/split/helper.rb +2 -1
- data/lib/split/persistence/dual_adapter.rb +54 -12
- data/lib/split/redis_interface.rb +1 -0
- data/lib/split/trial.rb +1 -1
- data/lib/split/user.rb +5 -1
- data/lib/split/version.rb +2 -2
- data/spec/dashboard/pagination_helpers_spec.rb +3 -1
- data/spec/dashboard_helpers_spec.rb +2 -2
- data/spec/dashboard_spec.rb +37 -16
- data/spec/encapsulated_helper_spec.rb +1 -1
- data/spec/experiment_spec.rb +44 -5
- data/spec/helper_spec.rb +118 -80
- data/spec/persistence/dual_adapter_spec.rb +160 -68
- data/spec/user_spec.rb +11 -0
- data/split.gemspec +1 -2
- metadata +6 -4
data/.travis.yml
CHANGED
@@ -3,11 +3,11 @@ rvm:
|
|
3
3
|
- 1.9.3
|
4
4
|
- 2.0
|
5
5
|
- 2.1.10
|
6
|
-
- 2.2.0
|
7
6
|
- 2.2.2
|
8
|
-
- 2.
|
9
|
-
- 2.
|
10
|
-
- 2.
|
7
|
+
- 2.3.8
|
8
|
+
- 2.4.9
|
9
|
+
- 2.5.7
|
10
|
+
- 2.6.5
|
11
11
|
|
12
12
|
gemfile:
|
13
13
|
- gemfiles/4.2.gemfile
|
@@ -16,7 +16,6 @@ gemfile:
|
|
16
16
|
- gemfiles/5.2.gemfile
|
17
17
|
- gemfiles/6.0.gemfile
|
18
18
|
|
19
|
-
|
20
19
|
matrix:
|
21
20
|
exclude:
|
22
21
|
- rvm: 1.9.3
|
@@ -43,20 +42,15 @@ matrix:
|
|
43
42
|
gemfile: gemfiles/5.2.gemfile
|
44
43
|
- rvm: 2.1.10
|
45
44
|
gemfile: gemfiles/6.0.gemfile
|
46
|
-
- rvm: 2.2.0
|
47
|
-
gemfile: gemfiles/5.0.gemfile
|
48
|
-
- rvm: 2.2.0
|
49
|
-
gemfile: gemfiles/5.1.gemfile
|
50
|
-
- rvm: 2.2.0
|
51
|
-
gemfile: gemfiles/5.2.gemfile
|
52
|
-
- rvm: 2.2.0
|
53
|
-
gemfile: gemfiles/6.0.gemfile
|
54
45
|
- rvm: 2.2.2
|
55
46
|
gemfile: gemfiles/6.0.gemfile
|
56
|
-
- rvm: 2.
|
47
|
+
- rvm: 2.3.8
|
48
|
+
gemfile: gemfiles/6.0.gemfile
|
49
|
+
- rvm: 2.4.9
|
57
50
|
gemfile: gemfiles/6.0.gemfile
|
58
51
|
|
59
52
|
before_install:
|
53
|
+
- gem uninstall -v '>= 2' -i $(rvm gemdir)@global -ax bundler || true
|
60
54
|
- gem install bundler --version=1.17.3
|
61
55
|
|
62
56
|
script:
|
data/Appraisals
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,25 @@
|
|
1
|
+
## 3.4.0 (November 9th, 2019)
|
2
|
+
|
3
|
+
Features:
|
4
|
+
- Improve DualAdapter (@santib, #588), adds a new configuration for the DualAdapter, making it possible to keep consistency for logged_out/logged_in users. It's a opt-in flag. No Behavior was changed on this release.
|
5
|
+
- Make dashboard pagination default "per" param configurable (@alopatin, #597)
|
6
|
+
|
7
|
+
Bugfixes:
|
8
|
+
- Fix `force_alternative` for experiments with incremented version (@giraffate, #568)
|
9
|
+
- Persist alternative weights (@giraffate, #570)
|
10
|
+
- Combined experiment performance improvements (@gnanou, #575)
|
11
|
+
- Handle correctly case when ab_finished is called before ab_test for a user (@gnanou, #577)
|
12
|
+
- When loading active_experiments, it should not look into user's 'finished' keys (@andrehjr, #582)
|
13
|
+
|
14
|
+
Misc:
|
15
|
+
- Remove `rubyforge_project` from gemspec (@giraffate, #583)
|
16
|
+
- Fix URLs to replace http with https (@giraffate , #584)
|
17
|
+
- Lazily include split helpers in ActionController::Base (@hasghari, #586)
|
18
|
+
- Fix unused variable warnings (@andrehjr, #592)
|
19
|
+
- Fix ruby warnings (@andrehjr, #593)
|
20
|
+
- Update rubocop.yml config (@andrehjr, #594)
|
21
|
+
- Add frozen_string_literal to all files that were missing it (@andrehjr, #595)
|
22
|
+
|
1
23
|
## 3.3.2 (April 12th, 2019)
|
2
24
|
|
3
25
|
Features:
|
data/CODE_OF_CONDUCT.md
CHANGED
@@ -68,7 +68,7 @@ members of the project's leadership.
|
|
68
68
|
## Attribution
|
69
69
|
|
70
70
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
71
|
-
available at [
|
71
|
+
available at [https://contributor-covenant.org/version/1/4][version]
|
72
72
|
|
73
|
-
[homepage]:
|
74
|
-
[version]:
|
73
|
+
[homepage]: https://contributor-covenant.org
|
74
|
+
[version]: https://contributor-covenant.org/version/1/4/
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,13 +1,13 @@
|
|
1
|
-
# [Split](
|
1
|
+
# [Split](https://libraries.io/rubygems/split)
|
2
2
|
|
3
3
|
[![Gem Version](https://badge.fury.io/rb/split.svg)](http://badge.fury.io/rb/split)
|
4
|
-
[![Build Status](https://secure.travis-ci.org/splitrb/split.svg?branch=master)](
|
4
|
+
[![Build Status](https://secure.travis-ci.org/splitrb/split.svg?branch=master)](https://travis-ci.org/splitrb/split)
|
5
5
|
[![Code Climate](https://codeclimate.com/github/splitrb/split/badges/gpa.svg)](https://codeclimate.com/github/splitrb/split)
|
6
6
|
[![Test Coverage](https://codeclimate.com/github/splitrb/split/badges/coverage.svg)](https://codeclimate.com/github/splitrb/split/coverage)
|
7
7
|
[![standard-readme compliant](https://img.shields.io/badge/readme%20style-standard-brightgreen.svg?style=flat-square)](https://github.com/RichardLitt/standard-readme)
|
8
8
|
[![Open Source Helpers](https://www.codetriage.com/splitrb/split/badges/users.svg)](https://www.codetriage.com/splitrb/split)
|
9
9
|
|
10
|
-
> 📈 The Rack Based A/B testing framework
|
10
|
+
> 📈 The Rack Based A/B testing framework https://libraries.io/rubygems/split
|
11
11
|
|
12
12
|
Split is a rack based A/B testing framework designed to work with Rails, Sinatra or any other rack based app.
|
13
13
|
|
@@ -110,9 +110,9 @@ Split has two options for you to use to determine which alternative is the best.
|
|
110
110
|
|
111
111
|
The first option (default on the dashboard) uses a z test (n>30) for the difference between your control and alternative conversion rates to calculate statistical significance. This test will tell you whether an alternative is better or worse than your control, but it will not distinguish between which alternative is the best in an experiment with multiple alternatives. Split will only tell you if your experiment is 90%, 95%, or 99% significant, and this test only works if you have more than 30 participants and 5 conversions for each branch.
|
112
112
|
|
113
|
-
As per this [blog post](
|
113
|
+
As per this [blog post](https://www.evanmiller.org/how-not-to-run-an-ab-test.html) on the pitfalls of A/B testing, it is highly recommended that you determine your requisite sample size for each branch before running the experiment. Otherwise, you'll have an increased rate of false positives (experiments which show a significant effect where really there is none).
|
114
114
|
|
115
|
-
[Here](
|
115
|
+
[Here](https://www.evanmiller.org/ab-testing/sample-size.html) is a sample size calculator for your convenience.
|
116
116
|
|
117
117
|
The second option uses simulations from a beta distribution to determine the probability that the given alternative is the winner compared to all other alternatives. You can view these probabilities by clicking on the drop-down menu labeled "Confidence." This option should be used when the experiment has more than just 1 control and 1 alternative. It can also be used for a simple, 2-alternative A/B test.
|
118
118
|
|
@@ -360,7 +360,7 @@ end
|
|
360
360
|
|
361
361
|
If you are running `ab_test` from a view, you must define your event
|
362
362
|
hook callback as a
|
363
|
-
[helper_method](
|
363
|
+
[helper_method](https://apidock.com/rails/AbstractController/Helpers/ClassMethods/helper_method)
|
364
364
|
in the controller:
|
365
365
|
|
366
366
|
``` ruby
|
@@ -446,7 +446,7 @@ match "/split" => Split::Dashboard, anchor: false, via: [:get, :post, :delete],
|
|
446
446
|
end
|
447
447
|
```
|
448
448
|
|
449
|
-
More information on this [here](
|
449
|
+
More information on this [here](https://steve.dynedge.co.uk/2011/12/09/controlling-access-to-routes-and-rack-apps-in-rails-3-with-devise-and-warden/)
|
450
450
|
|
451
451
|
### Screenshot
|
452
452
|
|
@@ -556,7 +556,7 @@ and:
|
|
556
556
|
ab_finished(:my_first_experiment)
|
557
557
|
```
|
558
558
|
|
559
|
-
You can also add meta data for each experiment, very useful when you need more than an alternative name to change behaviour:
|
559
|
+
You can also add meta data for each experiment, which is very useful when you need more than an alternative name to change behaviour:
|
560
560
|
|
561
561
|
```ruby
|
562
562
|
Split.configure do |config|
|
@@ -601,6 +601,8 @@ or in views:
|
|
601
601
|
<% end %>
|
602
602
|
```
|
603
603
|
|
604
|
+
The keys used in meta data should be Strings
|
605
|
+
|
604
606
|
#### Metrics
|
605
607
|
|
606
608
|
You might wish to track generic metrics, such as conversions, and use
|
@@ -824,8 +826,8 @@ end
|
|
824
826
|
|
825
827
|
## Extensions
|
826
828
|
|
827
|
-
- [Split::Export](
|
828
|
-
- [Split::Analytics](
|
829
|
+
- [Split::Export](https://github.com/splitrb/split-export) - Easily export A/B test data out of Split.
|
830
|
+
- [Split::Analytics](https://github.com/splitrb/split-analytics) - Push test data to Google Analytics.
|
829
831
|
- [Split::Mongoid](https://github.com/MongoHQ/split-mongoid) - Store experiment data in mongoid (still uses redis).
|
830
832
|
- [Split::Cacheable](https://github.com/harrystech/split_cacheable) - Automatically create cache buckets per test.
|
831
833
|
- [Split::Counters](https://github.com/bernardkroes/split-counters) - Add counters per experiment and alternative.
|
@@ -837,7 +839,7 @@ Ryan bates has produced an excellent 10 minute screencast about split on the Rai
|
|
837
839
|
|
838
840
|
## Blogposts
|
839
841
|
|
840
|
-
* [Recipe: A/B testing with KISSMetrics and the split gem](
|
842
|
+
* [Recipe: A/B testing with KISSMetrics and the split gem](https://robots.thoughtbot.com/post/9595887299/recipe-a-b-testing-with-kissmetrics-and-the-split-gem)
|
841
843
|
* [Rails A/B testing with Split on Heroku](http://blog.nathanhumbert.com/2012/02/rails-ab-testing-with-split-on-heroku.html)
|
842
844
|
|
843
845
|
## Backers
|
@@ -917,9 +919,9 @@ Please do! Over 70 different people have contributed to the project, you can see
|
|
917
919
|
|
918
920
|
### Development
|
919
921
|
|
920
|
-
The source code is hosted at [GitHub](
|
922
|
+
The source code is hosted at [GitHub](https://github.com/splitrb/split).
|
921
923
|
|
922
|
-
Report issues and feature requests on [GitHub Issues](
|
924
|
+
Report issues and feature requests on [GitHub Issues](https://github.com/splitrb/split/issues).
|
923
925
|
|
924
926
|
You can find a discussion form on [Google Groups](https://groups.google.com/d/forum/split-ruby).
|
925
927
|
|
data/Rakefile
CHANGED
data/gemfiles/6.0.gemfile
CHANGED
data/lib/split/alternative.rb
CHANGED
@@ -15,7 +15,7 @@ module Split
|
|
15
15
|
@name = name
|
16
16
|
@weight = 1
|
17
17
|
end
|
18
|
-
p_winner = 0.0
|
18
|
+
@p_winner = 0.0
|
19
19
|
end
|
20
20
|
|
21
21
|
def to_s
|
@@ -75,7 +75,7 @@ module Split
|
|
75
75
|
return field
|
76
76
|
end
|
77
77
|
|
78
|
-
def set_completed_count
|
78
|
+
def set_completed_count(count, goal = nil)
|
79
79
|
field = set_field(goal)
|
80
80
|
Split.redis.hset(key, field, count.to_i)
|
81
81
|
end
|
@@ -122,7 +122,7 @@ module Split
|
|
122
122
|
# can't calculate zscore for P(x) > 1
|
123
123
|
return 'N/A' if p_a > 1 || p_c > 1
|
124
124
|
|
125
|
-
|
125
|
+
Split::Zscore.calculate(p_a, n_a, p_c, n_c)
|
126
126
|
end
|
127
127
|
|
128
128
|
def extra_info
|
@@ -31,7 +31,7 @@ module Split
|
|
31
31
|
raise(Split::InvalidExperimentsFormatError, 'Invalid descriptor class (String or Symbol required)') unless metric_descriptor.class == String || metric_descriptor.class == Symbol
|
32
32
|
raise(Split::InvalidExperimentsFormatError, 'Enable configuration') unless Split.configuration.enabled
|
33
33
|
raise(Split::InvalidExperimentsFormatError, 'Enable `allow_multiple_experiments`') unless Split.configuration.allow_multiple_experiments
|
34
|
-
|
34
|
+
Split::configuration.experiments[metric_descriptor.to_sym]
|
35
35
|
end
|
36
36
|
end
|
37
37
|
end
|
data/lib/split/configuration.rb
CHANGED
@@ -1,8 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
module Split
|
3
3
|
class Configuration
|
4
|
-
attr_accessor :bots
|
5
|
-
attr_accessor :robot_regex
|
6
4
|
attr_accessor :ignore_ip_addresses
|
7
5
|
attr_accessor :ignore_filter
|
8
6
|
attr_accessor :db_failover
|
@@ -27,9 +25,13 @@ module Split
|
|
27
25
|
attr_accessor :beta_probability_simulations
|
28
26
|
attr_accessor :winning_alternative_recalculation_interval
|
29
27
|
attr_accessor :redis
|
28
|
+
attr_accessor :dashboard_pagination_default_per_page
|
30
29
|
|
31
30
|
attr_reader :experiments
|
32
31
|
|
32
|
+
attr_writer :bots
|
33
|
+
attr_writer :robot_regex
|
34
|
+
|
33
35
|
def bots
|
34
36
|
@bots ||= {
|
35
37
|
# Indexers
|
@@ -225,6 +227,7 @@ module Split
|
|
225
227
|
@beta_probability_simulations = 10000
|
226
228
|
@winning_alternative_recalculation_interval = 60 * 60 * 24 # 1 day
|
227
229
|
@redis = ENV.fetch(ENV.fetch('REDIS_PROVIDER', 'REDIS_URL'), 'redis://localhost:6379')
|
230
|
+
@dashboard_pagination_default_per_page = 10
|
228
231
|
end
|
229
232
|
|
230
233
|
def redis_url=(value)
|
data/lib/split/dashboard.rb
CHANGED
@@ -33,7 +33,10 @@ module Split
|
|
33
33
|
end
|
34
34
|
|
35
35
|
post '/force_alternative' do
|
36
|
-
Split::
|
36
|
+
experiment = Split::ExperimentCatalog.find(params[:experiment])
|
37
|
+
alternative = Split::Alternative.new(params[:alternative], experiment.name)
|
38
|
+
alternative.increment_participation
|
39
|
+
Split::User.new(self)[experiment.key] = alternative.name
|
37
40
|
redirect url('/')
|
38
41
|
end
|
39
42
|
|
@@ -3,10 +3,9 @@ require 'split/dashboard/paginator'
|
|
3
3
|
|
4
4
|
module Split
|
5
5
|
module DashboardPaginationHelpers
|
6
|
-
DEFAULT_PER = 10
|
7
|
-
|
8
6
|
def pagination_per
|
9
|
-
|
7
|
+
default_per_page = Split.configuration.dashboard_pagination_default_per_page
|
8
|
+
@pagination_per ||= (params[:per] || default_per_page).to_i
|
10
9
|
end
|
11
10
|
|
12
11
|
def page_number
|
data/lib/split/engine.rb
CHANGED
@@ -3,10 +3,12 @@ module Split
|
|
3
3
|
class Engine < ::Rails::Engine
|
4
4
|
initializer "split" do |app|
|
5
5
|
if Split.configuration.include_rails_helper
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
6
|
+
ActiveSupport.on_load(:action_controller) do
|
7
|
+
include Split::Helper
|
8
|
+
helper Split::Helper
|
9
|
+
include Split::CombinedExperimentsHelper
|
10
|
+
helper Split::CombinedExperimentsHelper
|
11
|
+
end
|
10
12
|
end
|
11
13
|
end
|
12
14
|
end
|
data/lib/split/experiment.rb
CHANGED
@@ -2,13 +2,13 @@
|
|
2
2
|
module Split
|
3
3
|
class Experiment
|
4
4
|
attr_accessor :name
|
5
|
-
attr_writer :algorithm
|
6
|
-
attr_accessor :resettable
|
7
5
|
attr_accessor :goals
|
8
|
-
attr_accessor :alternatives
|
9
6
|
attr_accessor :alternative_probabilities
|
10
7
|
attr_accessor :metadata
|
11
8
|
|
9
|
+
attr_reader :alternatives
|
10
|
+
attr_reader :resettable
|
11
|
+
|
12
12
|
DEFAULT_OPTIONS = {
|
13
13
|
:resettable => true
|
14
14
|
}
|
@@ -25,7 +25,7 @@ module Split
|
|
25
25
|
alternatives: load_alternatives_from_configuration,
|
26
26
|
goals: Split::GoalsCollection.new(@name).load_from_configuration,
|
27
27
|
metadata: load_metadata_from_configuration,
|
28
|
-
resettable: exp_config
|
28
|
+
resettable: exp_config.fetch(:resettable, true),
|
29
29
|
algorithm: exp_config[:algorithm]
|
30
30
|
}
|
31
31
|
else
|
@@ -62,7 +62,7 @@ module Split
|
|
62
62
|
alts = load_alternatives_from_configuration
|
63
63
|
options[:goals] = Split::GoalsCollection.new(@name).load_from_configuration
|
64
64
|
options[:metadata] = load_metadata_from_configuration
|
65
|
-
options[:resettable] = exp_config
|
65
|
+
options[:resettable] = exp_config.fetch(:resettable, true)
|
66
66
|
options[:algorithm] = exp_config[:algorithm]
|
67
67
|
end
|
68
68
|
end
|
@@ -81,12 +81,12 @@ module Split
|
|
81
81
|
|
82
82
|
if new_record?
|
83
83
|
start unless Split.configuration.start_manually
|
84
|
+
persist_experiment_configuration
|
84
85
|
elsif experiment_configuration_has_changed?
|
85
86
|
reset unless Split.configuration.reset_manually
|
87
|
+
persist_experiment_configuration
|
86
88
|
end
|
87
89
|
|
88
|
-
persist_experiment_configuration if new_record? || experiment_configuration_has_changed?
|
89
|
-
|
90
90
|
redis.hset(experiment_config_key, :resettable, resettable)
|
91
91
|
redis.hset(experiment_config_key, :algorithm, algorithm.to_s)
|
92
92
|
self
|
@@ -144,11 +144,13 @@ module Split
|
|
144
144
|
end
|
145
145
|
|
146
146
|
def has_winner?
|
147
|
-
|
147
|
+
return @has_winner if defined? @has_winner
|
148
|
+
@has_winner = !winner.nil?
|
148
149
|
end
|
149
150
|
|
150
151
|
def winner=(winner_name)
|
151
152
|
redis.hset(:experiment_winner, name, winner_name.to_s)
|
153
|
+
@has_winner = true
|
152
154
|
end
|
153
155
|
|
154
156
|
def participant_count
|
@@ -161,6 +163,7 @@ module Split
|
|
161
163
|
|
162
164
|
def reset_winner
|
163
165
|
redis.hdel(:experiment_winner, name)
|
166
|
+
@has_winner = false
|
164
167
|
end
|
165
168
|
|
166
169
|
def start
|
@@ -420,14 +423,22 @@ module Split
|
|
420
423
|
end
|
421
424
|
|
422
425
|
def load_alternatives_from_redis
|
423
|
-
case redis.type(@name)
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
426
|
+
alternatives = case redis.type(@name)
|
427
|
+
when 'set' # convert legacy sets to lists
|
428
|
+
alts = redis.smembers(@name)
|
429
|
+
redis.del(@name)
|
430
|
+
alts.reverse.each {|a| redis.lpush(@name, a) }
|
431
|
+
redis.lrange(@name, 0, -1)
|
432
|
+
else
|
433
|
+
redis.lrange(@name, 0, -1)
|
434
|
+
end
|
435
|
+
alternatives.map do |alt|
|
436
|
+
alt = begin
|
437
|
+
JSON.parse(alt)
|
438
|
+
rescue
|
439
|
+
alt
|
440
|
+
end
|
441
|
+
Split::Alternative.new(alt, @name)
|
431
442
|
end
|
432
443
|
end
|
433
444
|
|
@@ -443,7 +454,7 @@ module Split
|
|
443
454
|
|
444
455
|
def persist_experiment_configuration
|
445
456
|
redis_interface.add_to_set(:experiments, name)
|
446
|
-
redis_interface.persist_list(name, @alternatives.map
|
457
|
+
redis_interface.persist_list(name, @alternatives.map{|alt| {alt.name => alt.weight}.to_json})
|
447
458
|
goals_collection.save
|
448
459
|
redis.set(metadata_key, @metadata.to_json) unless @metadata.nil?
|
449
460
|
end
|
@@ -459,7 +470,7 @@ module Split
|
|
459
470
|
existing_alternatives = load_alternatives_from_redis
|
460
471
|
existing_goals = Split::GoalsCollection.new(@name).load_from_redis
|
461
472
|
existing_metadata = load_metadata_from_redis
|
462
|
-
existing_alternatives != @alternatives.map(&:
|
473
|
+
existing_alternatives.map(&:to_s) != @alternatives.map(&:to_s) ||
|
463
474
|
existing_goals != @goals ||
|
464
475
|
existing_metadata != @metadata
|
465
476
|
end
|