split 2.2.0 → 3.1.1

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.
data/README.md CHANGED
@@ -1,97 +1,28 @@
1
1
  # [Split](http://libraries.io/rubygems/split)
2
2
 
3
- Split is a rack based ab testing framework designed to work with Rails, Sinatra or any other rack based app.
4
-
5
- Split is heavily inspired by the Abingo and Vanity rails ab testing plugins and Resque in its use of Redis.
6
-
7
- Split is designed to be hacker friendly, allowing for maximum customisation and extensibility.
8
-
9
3
  [![Gem Version](https://badge.fury.io/rb/split.svg)](http://badge.fury.io/rb/split)
10
4
  [![Build Status](https://secure.travis-ci.org/splitrb/split.svg?branch=master)](http://travis-ci.org/splitrb/split)
11
5
  [![Code Climate](https://codeclimate.com/github/splitrb/split/badges/gpa.svg)](https://codeclimate.com/github/splitrb/split)
12
6
  [![Test Coverage](https://codeclimate.com/github/splitrb/split/badges/coverage.svg)](https://codeclimate.com/github/splitrb/split/coverage)
7
+ [![standard-readme compliant](https://img.shields.io/badge/readme%20style-standard-brightgreen.svg?style=flat-square)](https://github.com/RichardLitt/standard-readme)
13
8
 
9
+ > 📈 The Rack Based A/B testing framework http://libraries.io/rubygems/split
14
10
 
15
- # Backers
16
-
17
- Support us with a monthly donation and help us continue our activities. [[Become a backer](https://opencollective.com/split#backer)]
11
+ Split is a rack based A/B testing framework designed to work with Rails, Sinatra or any other rack based app.
18
12
 
19
- <a href="https://opencollective.com/split/backer/0/website" target="_blank"><img src="https://opencollective.com/split/backer/0/avatar.svg"></a>
20
- <a href="https://opencollective.com/split/backer/1/website" target="_blank"><img src="https://opencollective.com/split/backer/1/avatar.svg"></a>
21
- <a href="https://opencollective.com/split/backer/2/website" target="_blank"><img src="https://opencollective.com/split/backer/2/avatar.svg"></a>
22
- <a href="https://opencollective.com/split/backer/3/website" target="_blank"><img src="https://opencollective.com/split/backer/3/avatar.svg"></a>
23
- <a href="https://opencollective.com/split/backer/4/website" target="_blank"><img src="https://opencollective.com/split/backer/4/avatar.svg"></a>
24
- <a href="https://opencollective.com/split/backer/5/website" target="_blank"><img src="https://opencollective.com/split/backer/5/avatar.svg"></a>
25
- <a href="https://opencollective.com/split/backer/6/website" target="_blank"><img src="https://opencollective.com/split/backer/6/avatar.svg"></a>
26
- <a href="https://opencollective.com/split/backer/7/website" target="_blank"><img src="https://opencollective.com/split/backer/7/avatar.svg"></a>
27
- <a href="https://opencollective.com/split/backer/8/website" target="_blank"><img src="https://opencollective.com/split/backer/8/avatar.svg"></a>
28
- <a href="https://opencollective.com/split/backer/9/website" target="_blank"><img src="https://opencollective.com/split/backer/9/avatar.svg"></a>
29
- <a href="https://opencollective.com/split/backer/10/website" target="_blank"><img src="https://opencollective.com/split/backer/10/avatar.svg"></a>
30
- <a href="https://opencollective.com/split/backer/11/website" target="_blank"><img src="https://opencollective.com/split/backer/11/avatar.svg"></a>
31
- <a href="https://opencollective.com/split/backer/12/website" target="_blank"><img src="https://opencollective.com/split/backer/12/avatar.svg"></a>
32
- <a href="https://opencollective.com/split/backer/13/website" target="_blank"><img src="https://opencollective.com/split/backer/13/avatar.svg"></a>
33
- <a href="https://opencollective.com/split/backer/14/website" target="_blank"><img src="https://opencollective.com/split/backer/14/avatar.svg"></a>
34
- <a href="https://opencollective.com/split/backer/15/website" target="_blank"><img src="https://opencollective.com/split/backer/15/avatar.svg"></a>
35
- <a href="https://opencollective.com/split/backer/16/website" target="_blank"><img src="https://opencollective.com/split/backer/16/avatar.svg"></a>
36
- <a href="https://opencollective.com/split/backer/17/website" target="_blank"><img src="https://opencollective.com/split/backer/17/avatar.svg"></a>
37
- <a href="https://opencollective.com/split/backer/18/website" target="_blank"><img src="https://opencollective.com/split/backer/18/avatar.svg"></a>
38
- <a href="https://opencollective.com/split/backer/19/website" target="_blank"><img src="https://opencollective.com/split/backer/19/avatar.svg"></a>
39
- <a href="https://opencollective.com/split/backer/20/website" target="_blank"><img src="https://opencollective.com/split/backer/20/avatar.svg"></a>
40
- <a href="https://opencollective.com/split/backer/21/website" target="_blank"><img src="https://opencollective.com/split/backer/21/avatar.svg"></a>
41
- <a href="https://opencollective.com/split/backer/22/website" target="_blank"><img src="https://opencollective.com/split/backer/22/avatar.svg"></a>
42
- <a href="https://opencollective.com/split/backer/23/website" target="_blank"><img src="https://opencollective.com/split/backer/23/avatar.svg"></a>
43
- <a href="https://opencollective.com/split/backer/24/website" target="_blank"><img src="https://opencollective.com/split/backer/24/avatar.svg"></a>
44
- <a href="https://opencollective.com/split/backer/25/website" target="_blank"><img src="https://opencollective.com/split/backer/25/avatar.svg"></a>
45
- <a href="https://opencollective.com/split/backer/26/website" target="_blank"><img src="https://opencollective.com/split/backer/26/avatar.svg"></a>
46
- <a href="https://opencollective.com/split/backer/27/website" target="_blank"><img src="https://opencollective.com/split/backer/27/avatar.svg"></a>
47
- <a href="https://opencollective.com/split/backer/28/website" target="_blank"><img src="https://opencollective.com/split/backer/28/avatar.svg"></a>
48
- <a href="https://opencollective.com/split/backer/29/website" target="_blank"><img src="https://opencollective.com/split/backer/29/avatar.svg"></a>
49
-
50
-
51
- # Sponsors
52
-
53
- Become a sponsor and get your logo on our README on Github with a link to your site. [[Become a sponsor](https://opencollective.com/split#sponsor)]
54
-
55
- <a href="https://opencollective.com/split/sponsor/0/website" target="_blank"><img src="https://opencollective.com/split/sponsor/0/avatar.svg"></a>
56
- <a href="https://opencollective.com/split/sponsor/1/website" target="_blank"><img src="https://opencollective.com/split/sponsor/1/avatar.svg"></a>
57
- <a href="https://opencollective.com/split/sponsor/2/website" target="_blank"><img src="https://opencollective.com/split/sponsor/2/avatar.svg"></a>
58
- <a href="https://opencollective.com/split/sponsor/3/website" target="_blank"><img src="https://opencollective.com/split/sponsor/3/avatar.svg"></a>
59
- <a href="https://opencollective.com/split/sponsor/4/website" target="_blank"><img src="https://opencollective.com/split/sponsor/4/avatar.svg"></a>
60
- <a href="https://opencollective.com/split/sponsor/5/website" target="_blank"><img src="https://opencollective.com/split/sponsor/5/avatar.svg"></a>
61
- <a href="https://opencollective.com/split/sponsor/6/website" target="_blank"><img src="https://opencollective.com/split/sponsor/6/avatar.svg"></a>
62
- <a href="https://opencollective.com/split/sponsor/7/website" target="_blank"><img src="https://opencollective.com/split/sponsor/7/avatar.svg"></a>
63
- <a href="https://opencollective.com/split/sponsor/8/website" target="_blank"><img src="https://opencollective.com/split/sponsor/8/avatar.svg"></a>
64
- <a href="https://opencollective.com/split/sponsor/9/website" target="_blank"><img src="https://opencollective.com/split/sponsor/9/avatar.svg"></a>
65
- <a href="https://opencollective.com/split/sponsor/10/website" target="_blank"><img src="https://opencollective.com/split/sponsor/10/avatar.svg"></a>
66
- <a href="https://opencollective.com/split/sponsor/11/website" target="_blank"><img src="https://opencollective.com/split/sponsor/11/avatar.svg"></a>
67
- <a href="https://opencollective.com/split/sponsor/12/website" target="_blank"><img src="https://opencollective.com/split/sponsor/12/avatar.svg"></a>
68
- <a href="https://opencollective.com/split/sponsor/13/website" target="_blank"><img src="https://opencollective.com/split/sponsor/13/avatar.svg"></a>
69
- <a href="https://opencollective.com/split/sponsor/14/website" target="_blank"><img src="https://opencollective.com/split/sponsor/14/avatar.svg"></a>
70
- <a href="https://opencollective.com/split/sponsor/15/website" target="_blank"><img src="https://opencollective.com/split/sponsor/15/avatar.svg"></a>
71
- <a href="https://opencollective.com/split/sponsor/16/website" target="_blank"><img src="https://opencollective.com/split/sponsor/16/avatar.svg"></a>
72
- <a href="https://opencollective.com/split/sponsor/17/website" target="_blank"><img src="https://opencollective.com/split/sponsor/17/avatar.svg"></a>
73
- <a href="https://opencollective.com/split/sponsor/18/website" target="_blank"><img src="https://opencollective.com/split/sponsor/18/avatar.svg"></a>
74
- <a href="https://opencollective.com/split/sponsor/19/website" target="_blank"><img src="https://opencollective.com/split/sponsor/19/avatar.svg"></a>
75
- <a href="https://opencollective.com/split/sponsor/20/website" target="_blank"><img src="https://opencollective.com/split/sponsor/20/avatar.svg"></a>
76
- <a href="https://opencollective.com/split/sponsor/21/website" target="_blank"><img src="https://opencollective.com/split/sponsor/21/avatar.svg"></a>
77
- <a href="https://opencollective.com/split/sponsor/22/website" target="_blank"><img src="https://opencollective.com/split/sponsor/22/avatar.svg"></a>
78
- <a href="https://opencollective.com/split/sponsor/23/website" target="_blank"><img src="https://opencollective.com/split/sponsor/23/avatar.svg"></a>
79
- <a href="https://opencollective.com/split/sponsor/24/website" target="_blank"><img src="https://opencollective.com/split/sponsor/24/avatar.svg"></a>
80
- <a href="https://opencollective.com/split/sponsor/25/website" target="_blank"><img src="https://opencollective.com/split/sponsor/25/avatar.svg"></a>
81
- <a href="https://opencollective.com/split/sponsor/26/website" target="_blank"><img src="https://opencollective.com/split/sponsor/26/avatar.svg"></a>
82
- <a href="https://opencollective.com/split/sponsor/27/website" target="_blank"><img src="https://opencollective.com/split/sponsor/27/avatar.svg"></a>
83
- <a href="https://opencollective.com/split/sponsor/28/website" target="_blank"><img src="https://opencollective.com/split/sponsor/28/avatar.svg"></a>
84
- <a href="https://opencollective.com/split/sponsor/29/website" target="_blank"><img src="https://opencollective.com/split/sponsor/29/avatar.svg"></a>
13
+ Split is heavily inspired by the [Abingo](https://github.com/ryanb/abingo) and [Vanity](https://github.com/assaf/vanity) Rails A/B testing plugins and [Resque](https://github.com/resque/resque) in its use of Redis.
85
14
 
15
+ Split is designed to be hacker friendly, allowing for maximum customisation and extensibility.
86
16
 
17
+ ## Install
87
18
 
88
- ## Requirements
19
+ ### Requirements
89
20
 
90
- Split currently requires Ruby 1.9.2 or higher. If your project requires compatibility with Ruby 1.8.x and Rails 2.3, please use v0.8.0.
21
+ Split currently requires Ruby 1.9.3 or higher. If your project requires compatibility with Ruby 1.8.x and Rails 2.3, please use v0.8.0.
91
22
 
92
- Split uses redis as a datastore.
23
+ Split uses Redis as a datastore.
93
24
 
94
- Split only supports redis 2.0 or greater.
25
+ Split only supports Redis 2.0 or greater.
95
26
 
96
27
  If you're on OS X, Homebrew is the simplest way to install Redis:
97
28
 
@@ -100,21 +31,21 @@ brew install redis
100
31
  redis-server /usr/local/etc/redis.conf
101
32
  ```
102
33
 
103
- You now have a Redis daemon running on 6379.
34
+ You now have a Redis daemon running on port `6379`.
104
35
 
105
- ## Setup
36
+ ### Setup
106
37
 
107
38
  ```bash
108
39
  gem install split
109
40
  ```
110
41
 
111
- ### Rails
42
+ #### Rails
112
43
 
113
- Adding `gem 'split'` to your Gemfile will autoload it when rails starts up, as long as you've configured redis it will 'just work'.
44
+ Adding `gem 'split'` to your Gemfile will autoload it when rails starts up, as long as you've configured Redis it will 'just work'.
114
45
 
115
- ### Sinatra
46
+ #### Sinatra
116
47
 
117
- To configure sinatra with Split you need to enable sessions and mix in the helper methods. Add the following lines at the top of your sinatra app:
48
+ To configure Sinatra with Split you need to enable sessions and mix in the helper methods. Add the following lines at the top of your Sinatra app:
118
49
 
119
50
  ```ruby
120
51
  require 'split'
@@ -130,7 +61,7 @@ end
130
61
 
131
62
  ## Usage
132
63
 
133
- To begin your ab test use the `ab_test` method, naming your experiment with the first argument and then the different alternatives which you wish to test on as the other arguments.
64
+ To begin your A/B test use the `ab_test` method, naming your experiment with the first argument and then the different alternatives which you wish to test on as the other arguments.
134
65
 
135
66
  `ab_test` returns one of the alternatives, if a user has already seen that test they will get the same alternative as before, which you can use to split your code on.
136
67
 
@@ -184,7 +115,6 @@ As per this [blog post](http://www.evanmiller.org/how-not-to-run-an-ab-test.html
184
115
 
185
116
  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.
186
117
 
187
-
188
118
  ## Extras
189
119
 
190
120
  ### Weighted alternatives
@@ -220,9 +150,39 @@ In the event you want to disable all tests without having to know the individual
220
150
 
221
151
  It is not required to send `SPLIT_DISABLE=false` to activate Split.
222
152
 
153
+ To aid testing with RSpec, write `split_helper.rb` and call `use_ab_test(alternatives_by_experiment)` in your specs as instructed below:
154
+
155
+ ```ruby
156
+ # Recommended path for this file is 'spec/support/split_helper.rb', and you will need to ensure it
157
+ # is `require`-d by rails_helper.rb or spec_helper.rb
158
+ module SplitHelper
159
+
160
+ # Usage:
161
+ #
162
+ # Force a specific experiment alternative to always be returned:
163
+ # use_ab_test(signup_form: "single_page")
164
+ #
165
+ # Force alternatives for multiple experiments:
166
+ # use_ab_test(signup_form: "single_page", pricing: "show_enterprise_prices")
167
+ #
168
+ def use_ab_test(alternatives_by_experiment)
169
+ allow_any_instance_of(Split::Helper).to receive(:ab_test) do |_receiver, experiment|
170
+ alternative =
171
+ alternatives_by_experiment.fetch(experiment) { |key| raise "Unknown experiment '#{key}'" }
172
+ end
173
+ end
174
+ end
175
+
176
+ RSpec.configure do |config|
177
+ # Make the `use_ab_test` method available to all specs:
178
+ config.include SplitHelper
179
+ end
180
+ ```
181
+
182
+
223
183
  ### Starting experiments manually
224
184
 
225
- By default new AB tests will be active right after deployment. In case you would like to start new test a while after
185
+ By default new A/B tests will be active right after deployment. In case you would like to start new test a while after
226
186
  the deploy, you can do it by setting the `start_manually` configuration option to `true`.
227
187
 
228
188
  After choosing this option tests won't be started right after deploy, but after pressing the `Start` button in Split admin dashboard. If a test is deleted from the Split dashboard, then it can only be started after pressing the `Start` button whenever being re-initialized.
@@ -439,9 +399,9 @@ You may want to password protect that page, you can do so with `Rack::Auth::Basi
439
399
  Split::Dashboard.use Rack::Auth::Basic do |username, password|
440
400
  # Protect against timing attacks:
441
401
  # - Use & (do not use &&) so that it doesn't short circuit.
442
- # - Use `variable_size_secure_compare` to stop length information leaking
443
- ActiveSupport::SecurityUtils.variable_size_secure_compare(username, ENV["SPLIT_USERNAME"]) &
444
- ActiveSupport::SecurityUtils.variable_size_secure_compare(password, ENV["SPLIT_PASSWORD"])
402
+ # - Use digests to stop length information leaking
403
+ ActiveSupport::SecurityUtils.secure_compare(::Digest::SHA256.hexdigest(username), ::Digest::SHA256.hexdigest(ENV["SPLIT_USERNAME"])) &
404
+ ActiveSupport::SecurityUtils.secure_compare(::Digest::SHA256.hexdigest(password), ::Digest::SHA256.hexdigest(ENV["SPLIT_PASSWORD"]))
445
405
  end
446
406
 
447
407
  # Apps without activesupport
@@ -475,7 +435,7 @@ You can override the default configuration options of Split like so:
475
435
 
476
436
  ```ruby
477
437
  Split.configure do |config|
478
- config.db_failover = true # handle redis errors gracefully
438
+ config.db_failover = true # handle Redis errors gracefully
479
439
  config.db_failover_on_db_error = -> (error) { Rails.logger.error(error.message) }
480
440
  config.allow_multiple_experiments = true
481
441
  config.enabled = true
@@ -693,6 +653,35 @@ Once you finish one of the goals, the test is considered to be completed, and fi
693
653
 
694
654
  **Bad Example**: Test both how button color affects signup *and* how it affects login, at the same time. THIS WILL NOT WORK.
695
655
 
656
+ #### Combined Experiments
657
+ If you want to test how how button color affects signup *and* how it affects login, at the same time. Use combined tests
658
+ Configure like so
659
+ ```ruby
660
+ Split.configuration.experiments = {
661
+ :button_color_experiment => {
662
+ :alternatives => ["blue", "green"],
663
+ :combined_experiments => ["button_color_on_signup", "button_color_on_login"]
664
+ }
665
+ }
666
+ ```
667
+
668
+ Starting the combined test starts all combined experiments
669
+ ```ruby
670
+ ab_combined_test(:button_color_experiment)
671
+ ```
672
+ Finish each combined test as normal
673
+
674
+ ```ruby
675
+ ab_finished(:button_color_on_login)
676
+ ab_finished(:button_color_on_signup)
677
+ ```
678
+
679
+ **Additional Configuration**:
680
+ * Be sure to enable `allow_multiple_experiments`
681
+ * In Sinatra include the CombinedExperimentsHelper
682
+ ```
683
+ helpers Split::CombinedExperimentsHelper
684
+ ```
696
685
  ### DB failover solution
697
686
 
698
687
  Due to the fact that Redis has no automatic failover mechanism, it's
@@ -793,6 +782,12 @@ It is possible to specify static weights to favor certain alternatives.
793
782
  This algorithm will automatically weight the alternatives based on their relative performance,
794
783
  choosing the better-performing ones more often as trials are completed.
795
784
 
785
+ `Split::Algorithms::BlockRandomization` is an algorithm that ensures equal
786
+ participation across all alternatives. This algorithm will choose the alternative
787
+ with the fewest participants. In the event of multiple minimum participant alternatives
788
+ (i.e. starting a new "Block") the algorithm will choose a random alternative from
789
+ those minimum participant alternatives.
790
+
796
791
  Users may also write their own algorithms. The default algorithm may be specified globally in the configuration file, or on a per experiment basis using the experiments hash of the configuration file.
797
792
 
798
793
  To change the algorithm globally for all experiments, use the following in your initializer:
@@ -805,12 +800,12 @@ end
805
800
 
806
801
  ## Extensions
807
802
 
808
- - [Split::Export](http://github.com/splitrb/split-export) - easily export ab test data out of Split
809
- - [Split::Analytics](http://github.com/splitrb/split-analytics) - push test data to google analytics
810
- - [Split::Mongoid](https://github.com/MongoHQ/split-mongoid) - store experiment data in mongoid (still uses redis)
811
- - [Split::Cacheable](https://github.com/harrystech/split_cacheable) - automatically create cache buckets per test
812
- - [Split::Counters](https://github.com/bernardkroes/split-counters) - add counters per experiment and alternative
813
- - [Split::Cli](https://github.com/craigmcnamara/split-cli) - a CLI to trigger Split A/B tests
803
+ - [Split::Export](http://github.com/splitrb/split-export) - Easily export A/B test data out of Split.
804
+ - [Split::Analytics](http://github.com/splitrb/split-analytics) - Push test data to Google Analytics.
805
+ - [Split::Mongoid](https://github.com/MongoHQ/split-mongoid) - Store experiment data in mongoid (still uses redis).
806
+ - [Split::Cacheable](https://github.com/harrystech/split_cacheable) - Automatically create cache buckets per test.
807
+ - [Split::Counters](https://github.com/bernardkroes/split-counters) - Add counters per experiment and alternative.
808
+ - [Split::Cli](https://github.com/craigmcnamara/split-cli) - A CLI to trigger Split A/B tests.
814
809
 
815
810
  ## Screencast
816
811
 
@@ -818,15 +813,93 @@ Ryan bates has produced an excellent 10 minute screencast about split on the Rai
818
813
 
819
814
  ## Blogposts
820
815
 
821
- * [A/B Testing with Split in Ruby on Rails](http://grinnick.com/posts/a-b-testing-with-split-in-ruby-on-rails)
822
816
  * [Recipe: A/B testing with KISSMetrics and the split gem](http://robots.thoughtbot.com/post/9595887299/recipe-a-b-testing-with-kissmetrics-and-the-split-gem)
823
817
  * [Rails A/B testing with Split on Heroku](http://blog.nathanhumbert.com/2012/02/rails-ab-testing-with-split-on-heroku.html)
824
818
 
825
- ## Contributors
819
+ ## Backers
826
820
 
827
- Over 70 different people have contributed to the project, you can see them all here: https://github.com/splitrb/split/graphs/contributors
821
+ Support us with a monthly donation and help us continue our activities. [[Become a backer](https://opencollective.com/split#backer)]
828
822
 
829
- ## Development
823
+ <a href="https://opencollective.com/split/backer/0/website" target="_blank"><img src="https://opencollective.com/split/backer/0/avatar.svg"></a>
824
+ <a href="https://opencollective.com/split/backer/1/website" target="_blank"><img src="https://opencollective.com/split/backer/1/avatar.svg"></a>
825
+ <a href="https://opencollective.com/split/backer/2/website" target="_blank"><img src="https://opencollective.com/split/backer/2/avatar.svg"></a>
826
+ <a href="https://opencollective.com/split/backer/3/website" target="_blank"><img src="https://opencollective.com/split/backer/3/avatar.svg"></a>
827
+ <a href="https://opencollective.com/split/backer/4/website" target="_blank"><img src="https://opencollective.com/split/backer/4/avatar.svg"></a>
828
+ <a href="https://opencollective.com/split/backer/5/website" target="_blank"><img src="https://opencollective.com/split/backer/5/avatar.svg"></a>
829
+ <a href="https://opencollective.com/split/backer/6/website" target="_blank"><img src="https://opencollective.com/split/backer/6/avatar.svg"></a>
830
+ <a href="https://opencollective.com/split/backer/7/website" target="_blank"><img src="https://opencollective.com/split/backer/7/avatar.svg"></a>
831
+ <a href="https://opencollective.com/split/backer/8/website" target="_blank"><img src="https://opencollective.com/split/backer/8/avatar.svg"></a>
832
+ <a href="https://opencollective.com/split/backer/9/website" target="_blank"><img src="https://opencollective.com/split/backer/9/avatar.svg"></a>
833
+ <a href="https://opencollective.com/split/backer/10/website" target="_blank"><img src="https://opencollective.com/split/backer/10/avatar.svg"></a>
834
+ <a href="https://opencollective.com/split/backer/11/website" target="_blank"><img src="https://opencollective.com/split/backer/11/avatar.svg"></a>
835
+ <a href="https://opencollective.com/split/backer/12/website" target="_blank"><img src="https://opencollective.com/split/backer/12/avatar.svg"></a>
836
+ <a href="https://opencollective.com/split/backer/13/website" target="_blank"><img src="https://opencollective.com/split/backer/13/avatar.svg"></a>
837
+ <a href="https://opencollective.com/split/backer/14/website" target="_blank"><img src="https://opencollective.com/split/backer/14/avatar.svg"></a>
838
+ <a href="https://opencollective.com/split/backer/15/website" target="_blank"><img src="https://opencollective.com/split/backer/15/avatar.svg"></a>
839
+ <a href="https://opencollective.com/split/backer/16/website" target="_blank"><img src="https://opencollective.com/split/backer/16/avatar.svg"></a>
840
+ <a href="https://opencollective.com/split/backer/17/website" target="_blank"><img src="https://opencollective.com/split/backer/17/avatar.svg"></a>
841
+ <a href="https://opencollective.com/split/backer/18/website" target="_blank"><img src="https://opencollective.com/split/backer/18/avatar.svg"></a>
842
+ <a href="https://opencollective.com/split/backer/19/website" target="_blank"><img src="https://opencollective.com/split/backer/19/avatar.svg"></a>
843
+ <a href="https://opencollective.com/split/backer/20/website" target="_blank"><img src="https://opencollective.com/split/backer/20/avatar.svg"></a>
844
+ <a href="https://opencollective.com/split/backer/21/website" target="_blank"><img src="https://opencollective.com/split/backer/21/avatar.svg"></a>
845
+ <a href="https://opencollective.com/split/backer/22/website" target="_blank"><img src="https://opencollective.com/split/backer/22/avatar.svg"></a>
846
+ <a href="https://opencollective.com/split/backer/23/website" target="_blank"><img src="https://opencollective.com/split/backer/23/avatar.svg"></a>
847
+ <a href="https://opencollective.com/split/backer/24/website" target="_blank"><img src="https://opencollective.com/split/backer/24/avatar.svg"></a>
848
+ <a href="https://opencollective.com/split/backer/25/website" target="_blank"><img src="https://opencollective.com/split/backer/25/avatar.svg"></a>
849
+ <a href="https://opencollective.com/split/backer/26/website" target="_blank"><img src="https://opencollective.com/split/backer/26/avatar.svg"></a>
850
+ <a href="https://opencollective.com/split/backer/27/website" target="_blank"><img src="https://opencollective.com/split/backer/27/avatar.svg"></a>
851
+ <a href="https://opencollective.com/split/backer/28/website" target="_blank"><img src="https://opencollective.com/split/backer/28/avatar.svg"></a>
852
+ <a href="https://opencollective.com/split/backer/29/website" target="_blank"><img src="https://opencollective.com/split/backer/29/avatar.svg"></a>
853
+
854
+
855
+ ## Sponsors
856
+
857
+ Become a sponsor and get your logo on our README on Github with a link to your site. [[Become a sponsor](https://opencollective.com/split#sponsor)]
858
+
859
+ <a href="https://opencollective.com/split/sponsor/0/website" target="_blank"><img src="https://opencollective.com/split/sponsor/0/avatar.svg"></a>
860
+ <a href="https://opencollective.com/split/sponsor/1/website" target="_blank"><img src="https://opencollective.com/split/sponsor/1/avatar.svg"></a>
861
+ <a href="https://opencollective.com/split/sponsor/2/website" target="_blank"><img src="https://opencollective.com/split/sponsor/2/avatar.svg"></a>
862
+ <a href="https://opencollective.com/split/sponsor/3/website" target="_blank"><img src="https://opencollective.com/split/sponsor/3/avatar.svg"></a>
863
+ <a href="https://opencollective.com/split/sponsor/4/website" target="_blank"><img src="https://opencollective.com/split/sponsor/4/avatar.svg"></a>
864
+ <a href="https://opencollective.com/split/sponsor/5/website" target="_blank"><img src="https://opencollective.com/split/sponsor/5/avatar.svg"></a>
865
+ <a href="https://opencollective.com/split/sponsor/6/website" target="_blank"><img src="https://opencollective.com/split/sponsor/6/avatar.svg"></a>
866
+ <a href="https://opencollective.com/split/sponsor/7/website" target="_blank"><img src="https://opencollective.com/split/sponsor/7/avatar.svg"></a>
867
+ <a href="https://opencollective.com/split/sponsor/8/website" target="_blank"><img src="https://opencollective.com/split/sponsor/8/avatar.svg"></a>
868
+ <a href="https://opencollective.com/split/sponsor/9/website" target="_blank"><img src="https://opencollective.com/split/sponsor/9/avatar.svg"></a>
869
+ <a href="https://opencollective.com/split/sponsor/10/website" target="_blank"><img src="https://opencollective.com/split/sponsor/10/avatar.svg"></a>
870
+ <a href="https://opencollective.com/split/sponsor/11/website" target="_blank"><img src="https://opencollective.com/split/sponsor/11/avatar.svg"></a>
871
+ <a href="https://opencollective.com/split/sponsor/12/website" target="_blank"><img src="https://opencollective.com/split/sponsor/12/avatar.svg"></a>
872
+ <a href="https://opencollective.com/split/sponsor/13/website" target="_blank"><img src="https://opencollective.com/split/sponsor/13/avatar.svg"></a>
873
+ <a href="https://opencollective.com/split/sponsor/14/website" target="_blank"><img src="https://opencollective.com/split/sponsor/14/avatar.svg"></a>
874
+ <a href="https://opencollective.com/split/sponsor/15/website" target="_blank"><img src="https://opencollective.com/split/sponsor/15/avatar.svg"></a>
875
+ <a href="https://opencollective.com/split/sponsor/16/website" target="_blank"><img src="https://opencollective.com/split/sponsor/16/avatar.svg"></a>
876
+ <a href="https://opencollective.com/split/sponsor/17/website" target="_blank"><img src="https://opencollective.com/split/sponsor/17/avatar.svg"></a>
877
+ <a href="https://opencollective.com/split/sponsor/18/website" target="_blank"><img src="https://opencollective.com/split/sponsor/18/avatar.svg"></a>
878
+ <a href="https://opencollective.com/split/sponsor/19/website" target="_blank"><img src="https://opencollective.com/split/sponsor/19/avatar.svg"></a>
879
+ <a href="https://opencollective.com/split/sponsor/20/website" target="_blank"><img src="https://opencollective.com/split/sponsor/20/avatar.svg"></a>
880
+ <a href="https://opencollective.com/split/sponsor/21/website" target="_blank"><img src="https://opencollective.com/split/sponsor/21/avatar.svg"></a>
881
+ <a href="https://opencollective.com/split/sponsor/22/website" target="_blank"><img src="https://opencollective.com/split/sponsor/22/avatar.svg"></a>
882
+ <a href="https://opencollective.com/split/sponsor/23/website" target="_blank"><img src="https://opencollective.com/split/sponsor/23/avatar.svg"></a>
883
+ <a href="https://opencollective.com/split/sponsor/24/website" target="_blank"><img src="https://opencollective.com/split/sponsor/24/avatar.svg"></a>
884
+ <a href="https://opencollective.com/split/sponsor/25/website" target="_blank"><img src="https://opencollective.com/split/sponsor/25/avatar.svg"></a>
885
+ <a href="https://opencollective.com/split/sponsor/26/website" target="_blank"><img src="https://opencollective.com/split/sponsor/26/avatar.svg"></a>
886
+ <a href="https://opencollective.com/split/sponsor/27/website" target="_blank"><img src="https://opencollective.com/split/sponsor/27/avatar.svg"></a>
887
+ <a href="https://opencollective.com/split/sponsor/28/website" target="_blank"><img src="https://opencollective.com/split/sponsor/28/avatar.svg"></a>
888
+ <a href="https://opencollective.com/split/sponsor/29/website" target="_blank"><img src="https://opencollective.com/split/sponsor/29/avatar.svg"></a>
889
+
890
+ ## Contribute
891
+
892
+ Please do! Over 70 different people have contributed to the project, you can see them all here: https://github.com/splitrb/split/graphs/contributors.
893
+
894
+ ### Development
895
+
896
+ The source code is hosted at [GitHub](http://github.com/splitrb/split).
897
+
898
+ Report issues and feature requests on [GitHub Issues](http://github.com/splitrb/split/issues).
899
+
900
+ You can find a discussion form on [Google Groups](https://groups.google.com/d/forum/split-ruby).
901
+
902
+ ### Tests
830
903
 
831
904
  Run the tests like this:
832
905
 
@@ -836,23 +909,21 @@ Run the tests like this:
836
909
  bundle
837
910
  rake spec
838
911
 
839
- Source hosted at [GitHub](http://github.com/splitrb/split).
840
-
841
- Report Issues/Feature requests on [GitHub Issues](http://github.com/splitrb/split/issues).
842
-
843
- Discussion at [Google Groups](https://groups.google.com/d/forum/split-ruby).
844
-
845
- ### Note on Patches/Pull Requests
912
+ ### A Note on Patches and Pull Requests
846
913
 
847
914
  * Fork the project.
848
915
  * Make your feature addition or bug fix.
849
916
  * Add tests for it. This is important so I don't break it in a
850
917
  future version unintentionally.
851
918
  * Add documentation if necessary.
852
- * Commit, do not mess with rakefile, version, or history.
853
- (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
919
+ * Commit. Do not mess with the rakefile, version, or history.
920
+ (If you want to have your own version, that is fine. But bump the version in a commit by itself, which I can ignore when I pull.)
854
921
  * Send a pull request. Bonus points for topic branches.
855
922
 
923
+ ### Code of Conduct
924
+
925
+ Please note that this project is released with a [Contributor Code of Conduct](CODE_OF_CONDUCT.md). By participating in this project you agree to abide by its terms.
926
+
856
927
  ## Copyright
857
928
 
858
- Copyright (c) 2016 Andrew Nesbitt. See [LICENSE](https://github.com/splitrb/split/blob/master/LICENSE) for details.
929
+ [MIT License](LICENSE) © 2017 [Andrew Nesbitt](https://github.com/andrew).
data/gemfiles/4.2.gemfile CHANGED
@@ -6,4 +6,4 @@ gem "appraisal"
6
6
  gem "codeclimate-test-reporter"
7
7
  gem "rails", "~> 4.2"
8
8
 
9
- gemspec :path => "../"
9
+ gemspec path: "../"
data/gemfiles/5.0.gemfile CHANGED
@@ -5,6 +5,6 @@ source "https://rubygems.org"
5
5
  gem "appraisal"
6
6
  gem "codeclimate-test-reporter"
7
7
  gem "rails", "~> 5.0"
8
- gem "sinatra", :github => "sinatra/sinatra"
8
+ gem "sinatra", git: "https://github.com/sinatra/sinatra"
9
9
 
10
- gemspec :path => "../"
10
+ gemspec path: "../"
@@ -4,6 +4,7 @@ source "https://rubygems.org"
4
4
 
5
5
  gem "appraisal"
6
6
  gem "codeclimate-test-reporter"
7
- gem "rails", "~> 4.1"
7
+ gem "rails", "~> 5.1"
8
+ gem "sinatra", git: "https://github.com/sinatra/sinatra"
8
9
 
9
- gemspec :path => "../"
10
+ gemspec path: "../"
@@ -0,0 +1,22 @@
1
+ # Selects alternative with minimum count of participants
2
+ # If all counts are even (i.e. all are minimum), samples from all possible alternatives
3
+
4
+ module Split
5
+ module Algorithms
6
+ module BlockRandomization
7
+ class << self
8
+ def choose_alternative(experiment)
9
+ minimum_participant_alternatives(experiment.alternatives).sample
10
+ end
11
+
12
+ private
13
+
14
+ def minimum_participant_alternatives(alternatives)
15
+ alternatives_by_count = alternatives.group_by(&:participant_count)
16
+ min_group = alternatives_by_count.min_by { |k, v| k }
17
+ min_group.last
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -1,15 +1,10 @@
1
1
  # frozen_string_literal: true
2
- require 'split/zscore'
3
-
4
- # TODO - take out require and implement using file paths?
5
-
6
2
  module Split
7
3
  class Alternative
8
4
  attr_accessor :name
9
5
  attr_accessor :experiment_name
10
6
  attr_accessor :weight
11
-
12
- include Zscore
7
+ attr_accessor :recorded_info
13
8
 
14
9
  def initialize(name, experiment_name)
15
10
  @experiment_name = experiment_name
@@ -127,10 +122,37 @@ module Split
127
122
  z_score = Split::Zscore.calculate(p_a, n_a, p_c, n_c)
128
123
  end
129
124
 
125
+ def extra_info
126
+ data = Split.redis.hget(key, 'recorded_info')
127
+ if data && data.length > 1
128
+ begin
129
+ JSON.parse(data)
130
+ rescue
131
+ {}
132
+ end
133
+ else
134
+ {}
135
+ end
136
+ end
137
+
138
+ def record_extra_info(k, value = 1)
139
+ @recorded_info = self.extra_info || {}
140
+
141
+ if value.kind_of?(Numeric)
142
+ @recorded_info[k] ||= 0
143
+ @recorded_info[k] += value
144
+ else
145
+ @recorded_info[k] = value
146
+ end
147
+
148
+ Split.redis.hset key, 'recorded_info', (@recorded_info || {}).to_json
149
+ end
150
+
130
151
  def save
131
152
  Split.redis.hsetnx key, 'participant_count', 0
132
153
  Split.redis.hsetnx key, 'completed_count', 0
133
154
  Split.redis.hsetnx key, 'p_winner', p_winner
155
+ Split.redis.hsetnx key, 'recorded_info', (@recorded_info || {}).to_json
134
156
  end
135
157
 
136
158
  def validate!
@@ -140,7 +162,7 @@ module Split
140
162
  end
141
163
 
142
164
  def reset
143
- Split.redis.hmset key, 'participant_count', 0, 'completed_count', 0
165
+ Split.redis.hmset key, 'participant_count', 0, 'completed_count', 0, 'recorded_info', nil
144
166
  unless goals.empty?
145
167
  goals.each do |g|
146
168
  field = "completed_count:#{g}"
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+ module Split
3
+ module CombinedExperimentsHelper
4
+ def ab_combined_test(metric_descriptor, control = nil, *alternatives)
5
+ return nil unless experiment = find_combined_experiment(metric_descriptor)
6
+ raise(Split::InvalidExperimentsFormatError, 'Unable to find experiment #{metric_descriptor} in configuration') if experiment[:combined_experiments].nil?
7
+
8
+ alternative = nil
9
+ experiment[:combined_experiments].each do |combined_experiment|
10
+ if alternative.nil?
11
+ if control
12
+ alternative = ab_test(combined_experiment, control, alternatives)
13
+ else
14
+ normalized_alternatives = Split::Configuration.new.normalize_alternatives(experiment[:alternatives])
15
+ alternative = ab_test(combined_experiment, normalized_alternatives[0], *normalized_alternatives[1])
16
+ end
17
+ else
18
+ ab_test(combined_experiment, [{alternative => 1}])
19
+ end
20
+ end
21
+ end
22
+
23
+ def find_combined_experiment(metric_descriptor)
24
+ raise(Split::InvalidExperimentsFormatError, 'Invalid descriptor class (String or Symbol required)') unless metric_descriptor.class == String || metric_descriptor.class == Symbol
25
+ raise(Split::InvalidExperimentsFormatError, 'Enable configuration') unless Split.configuration.enabled
26
+ raise(Split::InvalidExperimentsFormatError, 'Enable `allow_multiple_experiments`') unless Split.configuration.allow_multiple_experiments
27
+ experiment = Split::configuration.experiments[metric_descriptor.to_sym]
28
+ end
29
+ end
30
+ end
@@ -48,7 +48,9 @@ module Split
48
48
  'spider' => 'generic web spider',
49
49
  'UnwindFetchor' => 'Gnip crawler',
50
50
  'WordPress' => 'WordPress spider',
51
+ 'YandexAccessibilityBot' => 'Yandex accessibility spider',
51
52
  'YandexBot' => 'Yandex spider',
53
+ 'YandexMobileBot' => 'Yandex mobile spider',
52
54
  'ZIBB' => 'ZIBB spider',
53
55
 
54
56
  # HTTP libraries
@@ -73,10 +75,13 @@ module Split
73
75
  'facebookexternalhit' => 'facebook bot',
74
76
  'Feedfetcher-Google' => 'Google Feedfetcher',
75
77
  'https://developers.google.com/+/web/snippet' => 'Google+ Snippet Fetcher',
78
+ 'LinkedInBot' => 'LinkedIn bot',
76
79
  'LongURL' => 'URL expander service',
77
80
  'NING' => 'NING - Yet Another Twitter Swarmer',
81
+ 'Pinterest' => 'Pinterest Bot',
78
82
  'redditbot' => 'Reddit Bot',
79
83
  'ShortLinkTranslate' => 'Link shortener',
84
+ 'Slackbot' => 'Slackbot link expander',
80
85
  'TweetmemeBot' => 'TweetMeMe Crawler',
81
86
  'Twitterbot' => 'Twitter URL expander',
82
87
  'UnwindFetch' => 'Gnip URL expander',
@@ -18,7 +18,11 @@ module Split
18
18
  end
19
19
 
20
20
  def round(number, precision = 2)
21
- BigDecimal.new(number.to_s).round(precision).to_f
21
+ begin
22
+ BigDecimal.new(number.to_s)
23
+ rescue ArgumentError
24
+ BigDecimal.new(0)
25
+ end.round(precision).to_f
22
26
  end
23
27
 
24
28
  def confidence_level(z_score)