split 0.4.6 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. data/.travis.yml +11 -3
  2. data/CHANGELOG.mdown +22 -1
  3. data/CONTRIBUTING.md +10 -0
  4. data/LICENSE +1 -1
  5. data/README.mdown +235 -60
  6. data/lib/split.rb +8 -9
  7. data/lib/split/algorithms.rb +3 -0
  8. data/lib/split/algorithms/weighted_sample.rb +17 -0
  9. data/lib/split/algorithms/whiplash.rb +35 -0
  10. data/lib/split/alternative.rb +12 -4
  11. data/lib/split/configuration.rb +91 -1
  12. data/lib/split/dashboard/helpers.rb +3 -3
  13. data/lib/split/dashboard/views/_experiment.erb +1 -1
  14. data/lib/split/exceptions.rb +4 -0
  15. data/lib/split/experiment.rb +112 -24
  16. data/lib/split/extensions.rb +3 -0
  17. data/lib/split/extensions/array.rb +4 -0
  18. data/lib/split/extensions/string.rb +15 -0
  19. data/lib/split/helper.rb +87 -55
  20. data/lib/split/metric.rb +68 -0
  21. data/lib/split/persistence.rb +28 -0
  22. data/lib/split/persistence/cookie_adapter.rb +44 -0
  23. data/lib/split/persistence/session_adapter.rb +28 -0
  24. data/lib/split/trial.rb +43 -0
  25. data/lib/split/version.rb +3 -3
  26. data/spec/algorithms/weighted_sample_spec.rb +18 -0
  27. data/spec/algorithms/whiplash_spec.rb +23 -0
  28. data/spec/alternative_spec.rb +81 -9
  29. data/spec/configuration_spec.rb +61 -9
  30. data/spec/dashboard_helpers_spec.rb +2 -5
  31. data/spec/dashboard_spec.rb +0 -2
  32. data/spec/experiment_spec.rb +144 -74
  33. data/spec/helper_spec.rb +234 -29
  34. data/spec/metric_spec.rb +30 -0
  35. data/spec/persistence/cookie_adapter_spec.rb +31 -0
  36. data/spec/persistence/session_adapter_spec.rb +31 -0
  37. data/spec/persistence_spec.rb +33 -0
  38. data/spec/spec_helper.rb +12 -0
  39. data/spec/support/cookies_mock.rb +19 -0
  40. data/spec/trial_spec.rb +59 -0
  41. data/split.gemspec +7 -3
  42. metadata +58 -29
  43. data/Guardfile +0 -5
@@ -3,7 +3,15 @@ rvm:
3
3
  - 1.8.7
4
4
  - 1.9.2
5
5
  - 1.9.3
6
- - jruby-18mode # JRuby in 1.8 mode
7
- - jruby-19mode # JRuby in 1.9 mode
6
+ - 2.0.0
7
+ - ruby-head
8
+ - jruby-18mode
9
+ - jruby-19mode
10
+ - jruby-head
8
11
  - rbx-18mode
9
- - rbx-19mode
12
+ - rbx-19mode
13
+ - ree
14
+ matrix:
15
+ allow_failures:
16
+ - rvm: ruby-head
17
+ - rvm: 2.0.0
@@ -1,3 +1,24 @@
1
+ ## 0.5.0 (January 28, 2012)
2
+
3
+ Features:
4
+
5
+ - Persistence Adapters: Cookies and Session (@patbenatar, #98)
6
+ - Configure experiments from a hash (@iangreenleaf, #97)
7
+ - Pluggable sampling algorithms (@woodhull, #105)
8
+
9
+ Bugfixes:
10
+
11
+ - Fixed negative number of non-finished rates (@philnash, #83)
12
+ - Fixed behaviour of finished(:reset => false) (@philnash, #88)
13
+ - Only take into consideration positive z-scores (@thomasmaas, #96)
14
+
15
+ ## 0.4.7 (November 1, 2012)
16
+
17
+ Bugfixes:
18
+
19
+ - Amended ab_test method to raise ArgumentError if passed integers or symbols as
20
+ alternatives (@buddhamagnet, #81)
21
+
1
22
  ## 0.4.6 (October 28, 2012)
2
23
 
3
24
  Features:
@@ -176,4 +197,4 @@ Bugfixes:
176
197
 
177
198
  ## 0.1.0 (May 17, 2011)
178
199
 
179
- Initial Release
200
+ Initial Release
@@ -0,0 +1,10 @@
1
+ ## Contributing
2
+ * Fork the project.
3
+ * Make your feature addition or bug fix.
4
+ * Add tests for it. This is important so I don't break it in a
5
+ future version unintentionally.
6
+ * Add documentation if necessary.
7
+ * Commit, do not mess with rakefile, version, or history.
8
+ (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)
9
+ * Send a pull request. Bonus points for topic branches.
10
+ * Discussion at the [Google Group](https://groups.google.com/d/forum/split-ruby)
data/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2012 Andrew Nesbitt
1
+ Copyright (c) 2013 Andrew Nesbitt
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
@@ -6,7 +6,10 @@ Split is heavily inspired by the Abingo and Vanity rails ab testing plugins and
6
6
 
7
7
  Split is designed to be hacker friendly, allowing for maximum customisation and extensibility.
8
8
 
9
- [![Build Status](https://secure.travis-ci.org/andrew/split.png?branch=master)](http://travis-ci.org/andrew/split) [![Dependency Status](https://gemnasium.com/andrew/split.png)](https://gemnasium.com/andrew/split)
9
+ [![Gem Version](https://badge.fury.io/rb/split.png)](http://badge.fury.io/rb/split)
10
+ [![Build Status](https://secure.travis-ci.org/andrew/split.png?branch=master)](http://travis-ci.org/andrew/split)
11
+ [![Dependency Status](https://gemnasium.com/andrew/split.png)](https://gemnasium.com/andrew/split)
12
+ [![Code Climate](https://codeclimate.com/badge.png)](https://codeclimate.com/github/andrew/split)
10
13
 
11
14
  ## Requirements
12
15
 
@@ -16,8 +19,10 @@ Split only supports redis 2.0 or greater.
16
19
 
17
20
  If you're on OS X, Homebrew is the simplest way to install Redis:
18
21
 
19
- $ brew install redis
20
- $ redis-server /usr/local/etc/redis.conf
22
+ ```bash
23
+ $ brew install redis
24
+ $ redis-server /usr/local/etc/redis.conf
25
+ ```
21
26
 
22
27
  You now have a Redis daemon running on 6379.
23
28
 
@@ -25,19 +30,27 @@ You now have a Redis daemon running on 6379.
25
30
 
26
31
  If you are using bundler add split to your Gemfile:
27
32
 
28
- gem 'split'
33
+ ``` ruby
34
+ gem 'split'
35
+ ```
29
36
 
30
37
  Then run:
31
38
 
32
- bundle install
39
+ ```bash
40
+ $ bundle install
41
+ ```
33
42
 
34
43
  Otherwise install the gem:
35
44
 
36
- gem install split
45
+ ```bash
46
+ $ gem install split
47
+ ```
37
48
 
38
49
  and require it in your project:
39
50
 
40
- require 'split'
51
+ ```ruby
52
+ require 'split'
53
+ ```
41
54
 
42
55
  ### SystemTimer
43
56
 
@@ -45,7 +58,9 @@ If you are using Redis on Ruby 1.8.x then you will likely want to also use the S
45
58
 
46
59
  Put the following in your gemfile as well:
47
60
 
48
- gem 'SystemTimer'
61
+ ```
62
+ gem 'SystemTimer'
63
+ ```
49
64
 
50
65
  ### Rails
51
66
 
@@ -55,13 +70,15 @@ Split is autoloaded when rails starts up, as long as you've configured redis it
55
70
 
56
71
  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:
57
72
 
58
- class MySinatraApp < Sinatra::Base
59
- enable :sessions
60
- helpers Split::Helper
73
+ ```ruby
74
+ class MySinatraApp < Sinatra::Base
75
+ enable :sessions
76
+ helpers Split::Helper
61
77
 
62
- get '/' do
63
- ...
64
- end
78
+ get '/' do
79
+ ...
80
+ end
81
+ ```
65
82
 
66
83
  ## Usage
67
84
 
@@ -75,27 +92,35 @@ It can be used to render different templates, show different text or any other c
75
92
 
76
93
  Example: View
77
94
 
78
- <% ab_test("login_button", "/images/button1.jpg", "/images/button2.jpg") do |button_file| %>
79
- <%= img_tag(button_file, :alt => "Login!") %>
80
- <% end %>
95
+ ```ruby
96
+ <% ab_test("login_button", "/images/button1.jpg", "/images/button2.jpg") do |button_file| %>
97
+ <%= img_tag(button_file, :alt => "Login!") %>
98
+ <% end %>
99
+ ```
81
100
 
82
101
  Example: Controller
83
102
 
84
- def register_new_user
85
- # See what level of free points maximizes users' decision to buy replacement points.
86
- @starter_points = ab_test("new_user_free_points", '100', '200', '300')
87
- end
103
+ ```ruby
104
+ def register_new_user
105
+ # See what level of free points maximizes users' decision to buy replacement points.
106
+ @starter_points = ab_test("new_user_free_points", '100', '200', '300')
107
+ end
108
+ ```
88
109
 
89
110
  Example: Conversion tracking (in a controller!)
90
111
 
91
- def buy_new_points
92
- # some business logic
93
- finished("new_user_free_points")
94
- end
112
+ ```ruby
113
+ def buy_new_points
114
+ # some business logic
115
+ finished("new_user_free_points")
116
+ end
117
+ ```
95
118
 
96
119
  Example: Conversion tracking (in a view)
97
120
 
98
- Thanks for signing up, dude! <% finished("signup_page_redesign") >
121
+ ```ruby
122
+ Thanks for signing up, dude! <% finished("signup_page_redesign") >
123
+ ```
99
124
 
100
125
  You can find more examples, tutorials and guides on the [wiki](https://github.com/andrew/split/wiki).
101
126
 
@@ -107,11 +132,13 @@ Perhaps you only want to show an alternative to 10% of your visitors because it
107
132
 
108
133
  To do this you can pass a weight with each alternative in the following ways:
109
134
 
110
- ab_test('homepage design', {'Old' => 20}, {'New' => 2})
135
+ ```ruby
136
+ ab_test('homepage design', {'Old' => 20}, {'New' => 2})
111
137
 
112
- ab_test('homepage design', 'Old', {'New' => 0.1})
138
+ ab_test('homepage design', 'Old', {'New' => 0.1})
113
139
 
114
- ab_test('homepage design', {'Old' => 10}, 'New')
140
+ ab_test('homepage design', {'Old' => 10}, 'New')
141
+ ```
115
142
 
116
143
  Note: If using ruby 1.8.x and weighted alternatives you should always pass the control alternative through as the second argument with any other alternatives as a third argument because the order of the hash is not preserved in ruby 1.8, ruby 1.9.1+ users are not affected by this bug.
117
144
 
@@ -134,7 +161,9 @@ When a user completes a test their session is reset so that they may start the t
134
161
 
135
162
  To stop this behaviour you can pass the following option to the `finished` method:
136
163
 
137
- finished('experiment_name', :reset => false)
164
+ ```ruby
165
+ finished('experiment_name', :reset => false)
166
+ ```
138
167
 
139
168
  The user will then always see the alternative they started with.
140
169
 
@@ -144,9 +173,40 @@ By default Split will avoid users participating in multiple experiments at once.
144
173
 
145
174
  To stop this behaviour and allow users to participate in multiple experiments at once enable the `allow_multiple_experiments` config option like so:
146
175
 
147
- Split.configure do |config|
148
- config.allow_multiple_experiments = true
149
- end
176
+ ```ruby
177
+ Split.configure do |config|
178
+ config.allow_multiple_experiments = true
179
+ end
180
+ ```
181
+
182
+ ### Experiment Persistence
183
+
184
+ Split comes with two built-in persistence adapters for storing users and the alternatives they've been given for each experiment.
185
+
186
+ By default Split will store the tests for each user in the session.
187
+
188
+ You can optionally configure Split to use a cookie or any custom adapter of your choosing.
189
+
190
+ #### Cookies
191
+
192
+ ```ruby
193
+ Split.configure do |config|
194
+ config.persistence = :cookie
195
+ end
196
+ ```
197
+
198
+ __Note:__ Using cookies depends on `ActionDispatch::Cookies` or any identical API
199
+
200
+ #### Custom Adapter
201
+
202
+ Your custom adapter needs to implement the same API as existing adapters.
203
+ See `Split::Persistance::CookieAdapter` or `Split::Persistence::SessionAdapter` for a starting point.
204
+
205
+ ```ruby
206
+ Split.configure do |config|
207
+ config.persistence = YourCustomAdapterClass
208
+ end
209
+ ```
150
210
 
151
211
  ## Web Interface
152
212
 
@@ -154,39 +214,112 @@ Split comes with a Sinatra-based front end to get an overview of how your experi
154
214
 
155
215
  If you are running Rails 2: You can mount this inside your app using Rack::URLMap in your `config.ru`
156
216
 
157
- require 'split/dashboard'
217
+ ```ruby
218
+ require 'split/dashboard'
158
219
 
159
- run Rack::URLMap.new \
160
- "/" => Your::App.new,
161
- "/split" => Split::Dashboard.new
220
+ run Rack::URLMap.new \
221
+ "/" => Your::App.new,
222
+ "/split" => Split::Dashboard.new
223
+ ```
162
224
 
163
225
  However, if you are using Rails 3: You can mount this inside your app routes by first adding this to the Gemfile:
164
226
 
165
- gem 'split', :require => 'split/dashboard'
227
+ ```ruby
228
+ gem 'split', :require => 'split/dashboard'
229
+ ```
166
230
 
167
231
  Then adding this to config/routes.rb
168
232
 
169
- mount Split::Dashboard, :at => 'split'
233
+ ```ruby
234
+ mount Split::Dashboard, :at => 'split'
235
+ ```
170
236
 
171
237
  You may want to password protect that page, you can do so with `Rack::Auth::Basic`
172
238
 
173
- Split::Dashboard.use Rack::Auth::Basic do |username, password|
174
- username == 'admin' && password == 'p4s5w0rd'
175
- end
239
+ ```ruby
240
+ Split::Dashboard.use Rack::Auth::Basic do |username, password|
241
+ username == 'admin' && password == 'p4s5w0rd'
242
+ end
243
+ ```
176
244
 
177
245
  ## Configuration
178
246
 
179
247
  You can override the default configuration options of Split like so:
180
248
 
249
+ ```ruby
250
+ Split.configure do |config|
251
+ config.robot_regex = /my_custom_robot_regex/
252
+ config.ignore_ip_addresses << '81.19.48.130'
253
+ config.db_failover = true # handle redis errors gracefully
254
+ config.db_failover_on_db_error = proc{|error| Rails.logger.error(error.message) }
255
+ config.allow_multiple_experiments = true
256
+ config.enabled = true
257
+ config.persistence = Split::Persistence::SessionAdapter
258
+ end
259
+ ```
260
+
261
+ ### Experiment configuration
262
+
263
+ Instead of providing the experiment options inline, you can store them
264
+ in a hash or a configuration file:
265
+
266
+ Split.configure do |config|
267
+ config.experiments = YAML.load_file "config/experiments.yml"
268
+ end
269
+
270
+ This hash can control your experiment's variants, weights, algorithm and if the
271
+ experiment resets once finished:
272
+
181
273
  Split.configure do |config|
182
- config.robot_regex = /my_custom_robot_regex/
183
- config.ignore_ip_addresses << '81.19.48.130'
184
- config.db_failover = true # handle redis errors gracefully
185
- config.db_failover_on_db_error = proc{|error| Rails.logger.error(error.message) }
186
- config.allow_multiple_experiments = true
187
- config.enabled = true
274
+ config.experiments = {
275
+ :my_first_experiment => {
276
+ :variants => ["a", "b"],
277
+ :resettable => false,
278
+ },
279
+ :my_second_experiment => {
280
+ :algorithm => 'Split::Algorithms::Whiplash',
281
+ :variants => [
282
+ { :name => "a", :percent => 67 },
283
+ { :name => "b", :percent => 33 },
284
+ ]
285
+ }
286
+ }
188
287
  end
189
288
 
289
+ This simplifies the calls from your code:
290
+
291
+ ab_test(:my_first_experiment)
292
+
293
+ and:
294
+
295
+ finished(:my_first_experiment)
296
+
297
+ #### Metrics
298
+
299
+ You might wish to track generic metrics, such as conversions, and use
300
+ those to complete multiple different experiments without adding more to
301
+ your code. You can use the configuration hash to do this, thanks to
302
+ the `:metric` option.
303
+
304
+ Split.configure do |config|
305
+ config.experiments = {
306
+ :my_first_experiment => {
307
+ :variants => ["a", "b"],
308
+ :metric => :conversion,
309
+ }
310
+ }
311
+ end
312
+
313
+ Your code may then track a completion using the metric instead of
314
+ the experiment name:
315
+
316
+ finished(:conversion)
317
+
318
+ You can also create a new metric by instantiating and saving a new Metric object.
319
+
320
+ Split::Metric.new(:conversion)
321
+ Split::Metric.save
322
+
190
323
  ### DB failover solution
191
324
 
192
325
  Due to the fact that Redis has no autom. failover mechanism, it's
@@ -216,19 +349,23 @@ appropriately.
216
349
 
217
350
  Here's our `config/split.yml`:
218
351
 
219
- development: localhost:6379
220
- test: localhost:6379
221
- staging: redis1.example.com:6379
222
- fi: localhost:6379
223
- production: redis1.example.com:6379
352
+ ```yml
353
+ development: localhost:6379
354
+ test: localhost:6379
355
+ staging: redis1.example.com:6379
356
+ fi: localhost:6379
357
+ production: redis1.example.com:6379
358
+ ```
224
359
 
225
360
  And our initializer:
226
361
 
227
- rails_root = ENV['RAILS_ROOT'] || File.dirname(__FILE__) + '/../..'
228
- rails_env = ENV['RAILS_ENV'] || 'development'
362
+ ```ruby
363
+ rails_root = ENV['RAILS_ROOT'] || File.dirname(__FILE__) + '/../..'
364
+ rails_env = ENV['RAILS_ENV'] || 'development'
229
365
 
230
- split_config = YAML.load_file(rails_root + '/config/split.yml')
231
- Split.redis = split_config[rails_env]
366
+ split_config = YAML.load_file(rails_root + '/config/split.yml')
367
+ Split.redis = split_config[rails_env]
368
+ ```
232
369
 
233
370
  ## Namespaces
234
371
 
@@ -242,11 +379,45 @@ in your Redis server.
242
379
 
243
380
  Simply use the `Split.redis.namespace` accessor:
244
381
 
245
- Split.redis.namespace = "split:blog"
382
+ ```ruby
383
+ Split.redis.namespace = "split:blog"
384
+ ```
246
385
 
247
386
  We recommend sticking this in your initializer somewhere after Redis
248
387
  is configured.
249
388
 
389
+ ## Outside of a Web Session
390
+
391
+ Split provides the Helper module to facilitate running experiments inside web sessions.
392
+
393
+ Alternatively, you can access the underlying Metric, Trial, Experiment and Alternative objects to
394
+ conduct experiments that are not tied to a web session.
395
+
396
+ ```ruby
397
+ # create a new experiment
398
+ experiment = Split::Experiment.find_or_create('color', 'red', 'blue')
399
+ # create a new trial
400
+ trial = Trial.new(:experiment => experiment)
401
+ # run trial
402
+ trial.choose!
403
+ # get the result, returns either red or blue
404
+ trial.alternative.name
405
+
406
+ # if the goal has been achieved, increment the successful completions for this alternative.
407
+ if goal_acheived?
408
+ trial.complete!
409
+ end
410
+
411
+ ```
412
+
413
+ ## Algorithms
414
+
415
+ By default, Split ships with an algorithm that randomly selects from possible alternatives for a traditional a/b test.
416
+
417
+ An implementation of a bandit algorithm is also provided.
418
+
419
+ 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.
420
+
250
421
  ## Extensions
251
422
 
252
423
  - [Split::Export](http://github.com/andrew/split-export) - easily export ab test data out of Split
@@ -263,11 +434,14 @@ Special thanks to the following people for submitting patches:
263
434
  * Lloyd Pick
264
435
  * Jeffery Chupp
265
436
  * Andrew Appleton
437
+ * Phil Nash
438
+ * Dave Goodchild
266
439
 
267
440
  ## Development
268
441
 
269
442
  Source hosted at [GitHub](http://github.com/andrew/split).
270
443
  Report Issues/Feature requests on [GitHub Issues](http://github.com/andrew/split/issues).
444
+ Discussion at [Google Groups](https://groups.google.com/d/forum/split-ruby)
271
445
 
272
446
  Tests can be ran with `rake spec`
273
447
 
@@ -277,10 +451,11 @@ Tests can be ran with `rake spec`
277
451
  * Make your feature addition or bug fix.
278
452
  * Add tests for it. This is important so I don't break it in a
279
453
  future version unintentionally.
454
+ * Add documentation if necessary.
280
455
  * Commit, do not mess with rakefile, version, or history.
281
456
  (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)
282
- * Send me a pull request. Bonus points for topic branches.
457
+ * Send a pull request. Bonus points for topic branches.
283
458
 
284
459
  ## Copyright
285
460
 
286
- Copyright (c) 2012 Andrew Nesbitt. See [LICENSE](https://github.com/andrew/split/blob/master/LICENSE) for details.
461
+ Copyright (c) 2013 Andrew Nesbitt. See [LICENSE](https://github.com/andrew/split/blob/master/LICENSE) for details.