split 0.5.0 → 0.6.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.
- data/.travis.yml +1 -3
- data/CHANGELOG.mdown +25 -6
- data/Gemfile +1 -1
- data/README.mdown +137 -52
- data/lib/split.rb +1 -1
- data/lib/split/alternative.rb +54 -26
- data/lib/split/configuration.rb +94 -30
- data/lib/split/dashboard/public/style.css +10 -2
- data/lib/split/dashboard/views/_experiment.erb +31 -22
- data/lib/split/dashboard/views/_experiment_with_goal_header.erb +14 -0
- data/lib/split/dashboard/views/index.erb +8 -1
- data/lib/split/experiment.rb +166 -146
- data/lib/split/helper.rb +33 -23
- data/lib/split/metric.rb +13 -0
- data/lib/split/persistence/cookie_adapter.rb +10 -2
- data/lib/split/trial.rb +18 -12
- data/lib/split/version.rb +3 -3
- data/spec/alternative_spec.rb +121 -78
- data/spec/configuration_spec.rb +92 -4
- data/spec/dashboard_spec.rb +27 -11
- data/spec/experiment_spec.rb +110 -67
- data/spec/helper_spec.rb +287 -104
- data/spec/persistence/cookie_adapter_spec.rb +8 -1
- data/spec/spec_helper.rb +1 -0
- data/spec/trial_spec.rb +10 -8
- data/split.gemspec +1 -1
- metadata +19 -18
data/.travis.yml
CHANGED
data/CHANGELOG.mdown
CHANGED
@@ -1,4 +1,28 @@
|
|
1
|
-
## 0.
|
1
|
+
## 0.6.0 (April 4, 2013)
|
2
|
+
|
3
|
+
Features:
|
4
|
+
|
5
|
+
- Support for Ruby 2.0.0 (@phoet, #142)
|
6
|
+
- Multiple Goals (@liujin, #109)
|
7
|
+
- Ignoring IPs using Regular Expressions (@waynemoore, #119)
|
8
|
+
- Added ability to add more bots to the default list (@themgt, #140)
|
9
|
+
- Allow custom configuration of user blocking logic (@phoet , #148)
|
10
|
+
|
11
|
+
Bugfixes:
|
12
|
+
|
13
|
+
- Fixed regression in handling of config files (@iangreenleaf, #115)
|
14
|
+
- Fixed completion rate increases for experiments users aren't participating in (@philnash, #67)
|
15
|
+
- Handle exceptions from invalid JSON in cookies (@iangreenleaf, #126)
|
16
|
+
|
17
|
+
Misc:
|
18
|
+
|
19
|
+
- updated minimum json version requirement
|
20
|
+
- Refactor Yaml Configuration (@rtwomey, #124)
|
21
|
+
- Refactoring of Experiments (@iangreenleaf @tamird, #117 #118)
|
22
|
+
- Added more known Bots, including Pingdom, Bing, YandexBot (@julesie, @zinkkrysty)
|
23
|
+
- Improved Readme (@iangreenleaf @phoet)
|
24
|
+
|
25
|
+
## 0.5.0 (January 28, 2013)
|
2
26
|
|
3
27
|
Features:
|
4
28
|
|
@@ -11,11 +35,6 @@ Bugfixes:
|
|
11
35
|
- Fixed negative number of non-finished rates (@philnash, #83)
|
12
36
|
- Fixed behaviour of finished(:reset => false) (@philnash, #88)
|
13
37
|
- Only take into consideration positive z-scores (@thomasmaas, #96)
|
14
|
-
|
15
|
-
## 0.4.7 (November 1, 2012)
|
16
|
-
|
17
|
-
Bugfixes:
|
18
|
-
|
19
38
|
- Amended ab_test method to raise ArgumentError if passed integers or symbols as
|
20
39
|
alternatives (@buddhamagnet, #81)
|
21
40
|
|
data/Gemfile
CHANGED
data/README.mdown
CHANGED
@@ -82,7 +82,7 @@ end
|
|
82
82
|
|
83
83
|
## Usage
|
84
84
|
|
85
|
-
To begin your ab test use the `ab_test` method, naming your experiment with the first argument and then the different
|
85
|
+
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.
|
86
86
|
|
87
87
|
`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.
|
88
88
|
|
@@ -92,7 +92,7 @@ It can be used to render different templates, show different text or any other c
|
|
92
92
|
|
93
93
|
Example: View
|
94
94
|
|
95
|
-
```
|
95
|
+
```erb
|
96
96
|
<% ab_test("login_button", "/images/button1.jpg", "/images/button2.jpg") do |button_file| %>
|
97
97
|
<%= img_tag(button_file, :alt => "Login!") %>
|
98
98
|
<% end %>
|
@@ -118,8 +118,8 @@ end
|
|
118
118
|
|
119
119
|
Example: Conversion tracking (in a view)
|
120
120
|
|
121
|
-
```
|
122
|
-
Thanks for signing up, dude! <% finished("signup_page_redesign")
|
121
|
+
```erb
|
122
|
+
Thanks for signing up, dude! <% finished("signup_page_redesign") %>
|
123
123
|
```
|
124
124
|
|
125
125
|
You can find more examples, tutorials and guides on the [wiki](https://github.com/andrew/split/wiki).
|
@@ -234,7 +234,7 @@ Then adding this to config/routes.rb
|
|
234
234
|
mount Split::Dashboard, :at => 'split'
|
235
235
|
```
|
236
236
|
|
237
|
-
You may want to password protect that page, you can do so with `Rack::Auth::Basic`
|
237
|
+
You may want to password protect that page, you can do so with `Rack::Auth::Basic` (in your split initializer file)
|
238
238
|
|
239
239
|
```ruby
|
240
240
|
Split::Dashboard.use Rack::Auth::Basic do |username, password|
|
@@ -242,14 +242,16 @@ Split::Dashboard.use Rack::Auth::Basic do |username, password|
|
|
242
242
|
end
|
243
243
|
```
|
244
244
|
|
245
|
+
### Screenshot
|
246
|
+
|
247
|
+

|
248
|
+
|
245
249
|
## Configuration
|
246
250
|
|
247
251
|
You can override the default configuration options of Split like so:
|
248
252
|
|
249
253
|
```ruby
|
250
254
|
Split.configure do |config|
|
251
|
-
config.robot_regex = /my_custom_robot_regex/
|
252
|
-
config.ignore_ip_addresses << '81.19.48.130'
|
253
255
|
config.db_failover = true # handle redis errors gracefully
|
254
256
|
config.db_failover_on_db_error = proc{|error| Rails.logger.error(error.message) }
|
255
257
|
config.allow_multiple_experiments = true
|
@@ -258,41 +260,84 @@ Split.configure do |config|
|
|
258
260
|
end
|
259
261
|
```
|
260
262
|
|
263
|
+
### Filtering
|
264
|
+
|
265
|
+
In most scenarios you don't want to have AB-Testing enabled for web spiders, robots or special groups of users.
|
266
|
+
Split provides functionality to filter this based on a predefined, extensible list of bots, IP-lists or custom exclude logic.
|
267
|
+
|
268
|
+
```ruby
|
269
|
+
Split.configure do |config|
|
270
|
+
# bot config
|
271
|
+
config.robot_regex = /my_custom_robot_regex/ # or
|
272
|
+
config.bots['newbot'] = "Description for bot with 'newbot' user agent, which will be added to config.robot_regex for exclusion"
|
273
|
+
|
274
|
+
# IP config
|
275
|
+
config.ignore_ip_addresses << '81.19.48.130' # or regex: /81\.19\.48\.[0-9]+/
|
276
|
+
|
277
|
+
# or provide your own filter functionality, the default is proc{ |request| is_robot? || is_ignored_ip_address? }
|
278
|
+
config.ignore_filter = proc{ |request| CustomExcludeLogic.excludes?(request) }
|
279
|
+
end
|
280
|
+
```
|
281
|
+
|
261
282
|
### Experiment configuration
|
262
283
|
|
263
284
|
Instead of providing the experiment options inline, you can store them
|
264
|
-
in a hash
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
}
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
285
|
+
in a hash. This hash can control your experiment's alternatives, weights,
|
286
|
+
algorithm and if the experiment resets once finished:
|
287
|
+
|
288
|
+
```ruby
|
289
|
+
Split.configure do |config|
|
290
|
+
config.experiments = {
|
291
|
+
"my_first_experiment" => {
|
292
|
+
:alternatives => ["a", "b"],
|
293
|
+
:resettable => false
|
294
|
+
},
|
295
|
+
"my_second_experiment" => {
|
296
|
+
:algorithm => 'Split::Algorithms::Whiplash',
|
297
|
+
:alternatives => [
|
298
|
+
{ :name => "a", :percent => 67 },
|
299
|
+
{ :name => "b", :percent => 33 }
|
300
|
+
]
|
301
|
+
}
|
302
|
+
}
|
303
|
+
end
|
304
|
+
```
|
305
|
+
|
306
|
+
You can also store your experiments in a YAML file:
|
307
|
+
|
308
|
+
```ruby
|
309
|
+
Split.configure do |config|
|
310
|
+
config.experiments = YAML.load_file "config/experiments.yml"
|
311
|
+
end
|
312
|
+
```
|
313
|
+
|
314
|
+
You can then define the YAML file like:
|
315
|
+
|
316
|
+
```yaml
|
317
|
+
my_first_experiment:
|
318
|
+
alternatives:
|
319
|
+
- a
|
320
|
+
- b
|
321
|
+
my_second_experiment:
|
322
|
+
alternatives:
|
323
|
+
- name: a
|
324
|
+
percent: 67
|
325
|
+
- name: b
|
326
|
+
percent: 33
|
327
|
+
resettable: false
|
328
|
+
```
|
288
329
|
|
289
330
|
This simplifies the calls from your code:
|
290
331
|
|
291
|
-
|
332
|
+
```ruby
|
333
|
+
ab_test("my_first_experiment")
|
334
|
+
```
|
292
335
|
|
293
336
|
and:
|
294
337
|
|
295
|
-
|
338
|
+
```ruby
|
339
|
+
finished("my_first_experiment")
|
340
|
+
```
|
296
341
|
|
297
342
|
#### Metrics
|
298
343
|
|
@@ -301,28 +346,62 @@ those to complete multiple different experiments without adding more to
|
|
301
346
|
your code. You can use the configuration hash to do this, thanks to
|
302
347
|
the `:metric` option.
|
303
348
|
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
349
|
+
```ruby
|
350
|
+
Split.configure do |config|
|
351
|
+
config.experiments = {
|
352
|
+
"my_first_experiment" => {
|
353
|
+
:alternatives => ["a", "b"],
|
354
|
+
:metric => :my_metric,
|
355
|
+
}
|
356
|
+
}
|
357
|
+
end
|
358
|
+
```
|
312
359
|
|
313
360
|
Your code may then track a completion using the metric instead of
|
314
361
|
the experiment name:
|
315
362
|
|
316
|
-
|
317
|
-
|
318
|
-
|
363
|
+
```ruby
|
364
|
+
finished(:my_metric)
|
365
|
+
```
|
366
|
+
|
367
|
+
You can also create a new metric by instantiating and saving a new Metric object.
|
368
|
+
|
369
|
+
```ruby
|
370
|
+
Split::Metric.new(:my_metric)
|
371
|
+
Split::Metric.save
|
372
|
+
```
|
373
|
+
|
374
|
+
#### Goals
|
319
375
|
|
320
|
-
|
321
|
-
|
376
|
+
You might wish to allow an experiment to have multiple, distinguishable goals.
|
377
|
+
The API to define goals for an experiment is this:
|
378
|
+
|
379
|
+
```ruby
|
380
|
+
ab_test({"link_color" => ["purchase", "refund"]}, "red", "blue")
|
381
|
+
```
|
382
|
+
|
383
|
+
or you can you can define them in a configuration file:
|
384
|
+
|
385
|
+
```ruby
|
386
|
+
Split.configure do |config|
|
387
|
+
config.experiments = {
|
388
|
+
"link_color" => {
|
389
|
+
:alternatives => ["red", "blue"],
|
390
|
+
:goals => ["purchase", "refund"]
|
391
|
+
}
|
392
|
+
}
|
393
|
+
end
|
394
|
+
```
|
395
|
+
|
396
|
+
To complete a goal conversion, you do it like:
|
397
|
+
|
398
|
+
```ruby
|
399
|
+
finished("link_color" => "purchase")
|
400
|
+
```
|
322
401
|
|
323
402
|
### DB failover solution
|
324
403
|
|
325
|
-
Due to the fact that Redis has no
|
404
|
+
Due to the fact that Redis has no automatic failover mechanism, it's
|
326
405
|
possible to switch on the `db_failover` config option, so that `ab_test`
|
327
406
|
and `finished` will not crash in case of a db failure. `ab_test` always
|
328
407
|
delivers alternative A (the first one) in that case.
|
@@ -373,7 +452,7 @@ If you're running multiple, separate instances of Split you may want
|
|
373
452
|
to namespace the keyspaces so they do not overlap. This is not unlike
|
374
453
|
the approach taken by many memcached clients.
|
375
454
|
|
376
|
-
This feature is provided by the [redis-namespace]
|
455
|
+
This feature is provided by the [redis-namespace](https://github.com/defunkt/redis-namespace) library, which
|
377
456
|
Split uses by default to separate the keys it manages from other keys
|
378
457
|
in your Redis server.
|
379
458
|
|
@@ -388,7 +467,7 @@ is configured.
|
|
388
467
|
|
389
468
|
## Outside of a Web Session
|
390
469
|
|
391
|
-
Split provides the Helper module to facilitate running experiments inside web sessions.
|
470
|
+
Split provides the Helper module to facilitate running experiments inside web sessions.
|
392
471
|
|
393
472
|
Alternatively, you can access the underlying Metric, Trial, Experiment and Alternative objects to
|
394
473
|
conduct experiments that are not tied to a web session.
|
@@ -412,16 +491,17 @@ end
|
|
412
491
|
|
413
492
|
## Algorithms
|
414
493
|
|
415
|
-
By default, Split ships with an algorithm that randomly selects from possible alternatives for a traditional a/b test.
|
494
|
+
By default, Split ships with an algorithm that randomly selects from possible alternatives for a traditional a/b test.
|
416
495
|
|
417
|
-
An implementation of a bandit algorithm is also provided.
|
496
|
+
An implementation of a bandit algorithm is also provided.
|
418
497
|
|
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.
|
498
|
+
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
499
|
|
421
500
|
## Extensions
|
422
501
|
|
423
502
|
- [Split::Export](http://github.com/andrew/split-export) - easily export ab test data out of Split
|
424
503
|
- [Split::Analytics](http://github.com/andrew/split-analytics) - push test data to google analytics
|
504
|
+
- [Split::Mongoid](https://github.com/MongoHQ/split-mongoid) - store data in mongoid instead of redis
|
425
505
|
|
426
506
|
## Screencast
|
427
507
|
|
@@ -436,6 +516,11 @@ Special thanks to the following people for submitting patches:
|
|
436
516
|
* Andrew Appleton
|
437
517
|
* Phil Nash
|
438
518
|
* Dave Goodchild
|
519
|
+
* Ian Young
|
520
|
+
* Nathan Woodhull
|
521
|
+
* Ville Lautanala
|
522
|
+
* Liu Jin
|
523
|
+
* Peter Schröder
|
439
524
|
|
440
525
|
## Development
|
441
526
|
|
data/lib/split.rb
CHANGED
data/lib/split/alternative.rb
CHANGED
@@ -19,50 +19,71 @@ module Split
|
|
19
19
|
name
|
20
20
|
end
|
21
21
|
|
22
|
+
def goals
|
23
|
+
self.experiment.goals
|
24
|
+
end
|
25
|
+
|
22
26
|
def participant_count
|
23
|
-
|
27
|
+
Split.redis.hget(key, 'participant_count').to_i
|
24
28
|
end
|
25
29
|
|
26
30
|
def participant_count=(count)
|
27
|
-
@participant_count = count
|
28
31
|
Split.redis.hset(key, 'participant_count', count.to_i)
|
29
32
|
end
|
30
33
|
|
31
|
-
def completed_count
|
32
|
-
|
34
|
+
def completed_count(goal = nil)
|
35
|
+
field = set_field(goal)
|
36
|
+
Split.redis.hget(key, field).to_i
|
33
37
|
end
|
34
|
-
|
38
|
+
|
39
|
+
def all_completed_count
|
40
|
+
if goals.empty?
|
41
|
+
completed_count
|
42
|
+
else
|
43
|
+
goals.inject(completed_count) do |sum, g|
|
44
|
+
sum + completed_count(g)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
35
49
|
def unfinished_count
|
36
|
-
participant_count -
|
50
|
+
participant_count - all_completed_count
|
51
|
+
end
|
52
|
+
|
53
|
+
def set_field(goal)
|
54
|
+
field = "completed_count"
|
55
|
+
field += ":" + goal unless goal.nil?
|
56
|
+
return field
|
37
57
|
end
|
38
58
|
|
39
|
-
def
|
40
|
-
|
41
|
-
Split.redis.hset(key,
|
59
|
+
def set_completed_count (count, goal = nil)
|
60
|
+
field = set_field(goal)
|
61
|
+
Split.redis.hset(key, field, count.to_i)
|
42
62
|
end
|
43
63
|
|
44
64
|
def increment_participation
|
45
|
-
|
65
|
+
Split.redis.hincrby key, 'participant_count', 1
|
46
66
|
end
|
47
67
|
|
48
|
-
def increment_completion
|
49
|
-
|
68
|
+
def increment_completion(goal = nil)
|
69
|
+
field = set_field(goal)
|
70
|
+
Split.redis.hincrby(key, field, 1)
|
50
71
|
end
|
51
72
|
|
52
73
|
def control?
|
53
74
|
experiment.control.name == self.name
|
54
75
|
end
|
55
76
|
|
56
|
-
def conversion_rate
|
77
|
+
def conversion_rate(goal = nil)
|
57
78
|
return 0 if participant_count.zero?
|
58
|
-
(completed_count.to_f/participant_count.to_f
|
79
|
+
(completed_count(goal).to_f)/participant_count.to_f
|
59
80
|
end
|
60
81
|
|
61
82
|
def experiment
|
62
83
|
Split::Experiment.find(experiment_name)
|
63
84
|
end
|
64
85
|
|
65
|
-
def z_score
|
86
|
+
def z_score(goal = nil)
|
66
87
|
# CTR_E = the CTR within the experiment split
|
67
88
|
# CTR_C = the CTR within the control split
|
68
89
|
# E = the number of impressions within the experiment split
|
@@ -74,8 +95,9 @@ module Split
|
|
74
95
|
|
75
96
|
return 'N/A' if control.name == alternative.name
|
76
97
|
|
77
|
-
ctr_e = alternative.conversion_rate
|
78
|
-
ctr_c = control.conversion_rate
|
98
|
+
ctr_e = alternative.conversion_rate(goal)
|
99
|
+
ctr_c = control.conversion_rate(goal)
|
100
|
+
|
79
101
|
|
80
102
|
e = alternative.participant_count
|
81
103
|
c = control.participant_count
|
@@ -92,28 +114,34 @@ module Split
|
|
92
114
|
Split.redis.hsetnx key, 'completed_count', 0
|
93
115
|
end
|
94
116
|
|
117
|
+
def validate!
|
118
|
+
unless String === @name || hash_with_correct_values?(@name)
|
119
|
+
raise ArgumentError, 'Alternative must be a string'
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
95
123
|
def reset
|
96
|
-
@participant_count = nil
|
97
|
-
@completed_count = nil
|
98
124
|
Split.redis.hmset key, 'participant_count', 0, 'completed_count', 0
|
125
|
+
unless goals.empty?
|
126
|
+
goals.each do |g|
|
127
|
+
field = "completed_count:#{g}"
|
128
|
+
Split.redis.hset key, field, 0
|
129
|
+
end
|
130
|
+
end
|
99
131
|
end
|
100
132
|
|
101
133
|
def delete
|
102
134
|
Split.redis.del(key)
|
103
135
|
end
|
104
136
|
|
105
|
-
|
106
|
-
String === name || hash_with_correct_values?(name)
|
107
|
-
end
|
137
|
+
private
|
108
138
|
|
109
|
-
def
|
139
|
+
def hash_with_correct_values?(name)
|
110
140
|
Hash === name && String === name.keys.first && Float(name.values.first) rescue false
|
111
141
|
end
|
112
142
|
|
113
|
-
private
|
114
|
-
|
115
143
|
def key
|
116
144
|
"#{experiment_name}:#{name}"
|
117
145
|
end
|
118
146
|
end
|
119
|
-
end
|
147
|
+
end
|