split 0.1.1 → 0.2.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/CHANGELOG.mdown +31 -0
- data/LICENSE +20 -0
- data/README.mdown +12 -5
- data/lib/split/alternative.rb +19 -7
- data/lib/split/dashboard/public/dashboard.js +7 -0
- data/lib/split/dashboard/public/style.css +17 -1
- data/lib/split/dashboard/views/index.erb +56 -48
- data/lib/split/dashboard/views/layout.erb +1 -1
- data/lib/split/dashboard.rb +6 -0
- data/lib/split/experiment.rb +34 -7
- data/lib/split/helper.rb +15 -0
- data/lib/split/version.rb +1 -1
- data/spec/alternative_spec.rb +95 -0
- data/spec/experiment_spec.rb +41 -3
- data/spec/helper_spec.rb +52 -2
- data/spec/spec_helper.rb +11 -0
- metadata +11 -6
data/CHANGELOG.mdown
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
## 0.2.1 (May 29, 2011)
|
2
|
+
|
3
|
+
Bugfixes:
|
4
|
+
|
5
|
+
- Convert legacy sets to lists to avoid exceptions during upgrades from 0.1.x
|
6
|
+
|
7
|
+
## 0.2.0 (May 29, 2011)
|
8
|
+
|
9
|
+
Features:
|
10
|
+
|
11
|
+
- Override an alternative via a url parameter
|
12
|
+
- Experiments can now be reset from the dashboard
|
13
|
+
- The first alternative is now considered the control
|
14
|
+
- General dashboard usability improvements
|
15
|
+
- Robots are ignored and given the control alternative
|
16
|
+
|
17
|
+
Bugfixes:
|
18
|
+
|
19
|
+
- Alternatives are now store in a list rather than a set to ensure consistent ordering
|
20
|
+
- Fixed diving by zero errors
|
21
|
+
|
22
|
+
## 0.1.1 (May 18, 2011)
|
23
|
+
|
24
|
+
Bugfixes:
|
25
|
+
|
26
|
+
- More Robust conversion rate display on dashboard
|
27
|
+
- Ensure `Split::Version` is available everywhere, fixed dashboard
|
28
|
+
|
29
|
+
## 0.1.0 (May 17, 2011)
|
30
|
+
|
31
|
+
Initial Release
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2011 Andrew Nesbitt
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.mdown
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
# Split
|
2
|
-
## Rack based split testing framework
|
3
2
|
|
4
3
|
Split is a rack based ab testing framework designed to work with Rails, Sinatra or any other rack based app.
|
5
4
|
|
@@ -84,6 +83,17 @@ Example: Conversion tracking (in a view)
|
|
84
83
|
|
85
84
|
Thanks for signing up, dude! <% finished("signup_page_redesign") >
|
86
85
|
|
86
|
+
### Overriding alternatives
|
87
|
+
|
88
|
+
For development and testing, you may wish to force your app to always return an alternative.
|
89
|
+
You can do this by passing it as a parameter in the url.
|
90
|
+
|
91
|
+
If you have an experiment called `button_color` with alternatives called `red` and `blue` used on your homepage, a url such as:
|
92
|
+
|
93
|
+
http://myawesomesite.com?button_color=red
|
94
|
+
|
95
|
+
will always have red buttons. This won't be stored in your session or count towards to results.
|
96
|
+
|
87
97
|
## Web Interface
|
88
98
|
|
89
99
|
Split comes with a Sinatra-based front end to get an overview of how your experiments are doing.
|
@@ -171,7 +181,4 @@ Special thanks to the following people for submitting patches:
|
|
171
181
|
|
172
182
|
## Copyright
|
173
183
|
|
174
|
-
Copyright (c) 2011 Andrew Nesbitt. See LICENSE for details.
|
175
|
-
|
176
|
-
|
177
|
-
n.b don't pass the same alternative twice!
|
184
|
+
Copyright (c) 2011 Andrew Nesbitt. See LICENSE for details.
|
data/lib/split/alternative.rb
CHANGED
@@ -26,27 +26,33 @@ module Split
|
|
26
26
|
return 0 if participant_count.zero?
|
27
27
|
(completed_count.to_f/participant_count.to_f)
|
28
28
|
end
|
29
|
-
|
29
|
+
|
30
|
+
def experiment
|
31
|
+
Split::Experiment.find(experiment_name)
|
32
|
+
end
|
33
|
+
|
30
34
|
def z_score
|
31
35
|
# CTR_E = the CTR within the experiment split
|
32
36
|
# CTR_C = the CTR within the control split
|
33
37
|
# E = the number of impressions within the experiment split
|
34
38
|
# C = the number of impressions within the control split
|
35
|
-
|
36
|
-
|
37
|
-
|
39
|
+
|
40
|
+
control = experiment.control
|
41
|
+
|
38
42
|
alternative = self
|
39
|
-
|
43
|
+
|
40
44
|
return 'N/A' if control.name == alternative.name
|
41
|
-
|
45
|
+
|
42
46
|
ctr_e = alternative.conversion_rate
|
43
47
|
ctr_c = control.conversion_rate
|
44
48
|
|
45
49
|
e = alternative.participant_count
|
46
50
|
c = control.participant_count
|
47
51
|
|
52
|
+
return 0 if ctr_c.zero?
|
53
|
+
|
48
54
|
standard_deviation = ((ctr_e / ctr_c**3) * ((e*ctr_e)+(c*ctr_c)-(ctr_c*ctr_e)*(c+e))/(c*e)) ** 0.5
|
49
|
-
|
55
|
+
|
50
56
|
z_score = ((ctr_e / ctr_c) - 1) / standard_deviation
|
51
57
|
end
|
52
58
|
|
@@ -59,6 +65,12 @@ module Split
|
|
59
65
|
end
|
60
66
|
end
|
61
67
|
|
68
|
+
def reset
|
69
|
+
@participant_count = 0
|
70
|
+
@completed_count = 0
|
71
|
+
save
|
72
|
+
end
|
73
|
+
|
62
74
|
def self.find(name, experiment_name)
|
63
75
|
counters = Split.redis.hgetall "#{experiment_name}:#{name}"
|
64
76
|
self.new(name, experiment_name, counters)
|
@@ -23,12 +23,28 @@ body { padding:0; margin:0; }
|
|
23
23
|
#main h1.wi { margin-bottom:5px;}
|
24
24
|
#main p.sub { font-size:95%; color:#999;}
|
25
25
|
|
26
|
-
#main table.queues { width:
|
26
|
+
#main table.queues { width:60%;}
|
27
27
|
|
28
28
|
#main table .totals td{ background:#eee; font-weight:bold; }
|
29
29
|
|
30
30
|
#footer { padding:10px 5%; background:#efefef; color:#999; font-size:85%; line-height:1.5; border-top:5px solid #ccc; padding-top:10px;}
|
31
31
|
#footer p a { color:#999;}
|
32
32
|
|
33
|
+
h2{
|
34
|
+
float:left;
|
35
|
+
}
|
36
|
+
|
37
|
+
.reset{
|
38
|
+
font-size:10px;
|
39
|
+
line-height:38px;
|
40
|
+
}
|
41
|
+
|
42
|
+
.reset input{
|
43
|
+
margin-left:10px;
|
44
|
+
}
|
45
|
+
|
46
|
+
.queues{
|
47
|
+
clear:both;
|
48
|
+
}
|
33
49
|
|
34
50
|
|
@@ -1,56 +1,64 @@
|
|
1
1
|
<h1>Split Dashboard</h1>
|
2
|
-
|
2
|
+
<% if @experiments.any? %>
|
3
|
+
<p class="intro">The list below contains all the registered experiments along with the number of test participants, completed and conversion rate currently in the system.</p>
|
3
4
|
|
4
|
-
<% @experiments.each do |experiment| %>
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
<th>Non-finished</th>
|
11
|
-
<th>Completed</th>
|
12
|
-
<th>Conversion Rate</th>
|
13
|
-
<th>Z-Score</th>
|
14
|
-
<th>Winner</th>
|
15
|
-
</tr>
|
16
|
-
|
17
|
-
<% total_participants = total_completed = 0 %>
|
18
|
-
<% experiment.alternatives.each do |alternative| %>
|
5
|
+
<% @experiments.each do |experiment| %>
|
6
|
+
<h2><%= experiment.name %></h2>
|
7
|
+
<form action="<%= url "/reset/#{experiment.name}" %>" method='post' class='reset' onclick="return confirmReset()">
|
8
|
+
<input type="submit" value="Reset Data">
|
9
|
+
</form>
|
10
|
+
<table class="queues">
|
19
11
|
<tr>
|
20
|
-
<
|
21
|
-
<
|
22
|
-
<
|
23
|
-
<
|
24
|
-
<
|
25
|
-
<
|
26
|
-
<
|
27
|
-
|
28
|
-
|
29
|
-
|
12
|
+
<th>Alternative Name</th>
|
13
|
+
<th>Participants</th>
|
14
|
+
<th>Non-finished</th>
|
15
|
+
<th>Completed</th>
|
16
|
+
<th>Conversion Rate</th>
|
17
|
+
<th>Z-Score</th>
|
18
|
+
<th>Winner</th>
|
19
|
+
</tr>
|
20
|
+
|
21
|
+
<% total_participants = total_completed = 0 %>
|
22
|
+
<% experiment.alternatives.each do |alternative| %>
|
23
|
+
<tr>
|
24
|
+
<td><%= alternative.name %></td>
|
25
|
+
<td><%= alternative.participant_count %></td>
|
26
|
+
<td><%= alternative.participant_count - alternative.completed_count %></td>
|
27
|
+
<td><%= alternative.completed_count %></td>
|
28
|
+
<td><%= number_to_percentage(alternative.conversion_rate) %>%</td>
|
29
|
+
<td><%= alternative.z_score %></td>
|
30
|
+
<td>
|
31
|
+
<% if experiment.winner %>
|
32
|
+
<% if experiment.winner.name == alternative.name %>
|
33
|
+
Winner
|
34
|
+
<% else %>
|
35
|
+
Loser
|
36
|
+
<% end %>
|
30
37
|
<% else %>
|
31
|
-
|
38
|
+
<form action="<%= url experiment.name %>" method='post'>
|
39
|
+
<input type='hidden' name='alternative' value='<%= alternative.name %>'>
|
40
|
+
<input type="submit" value="Use this">
|
41
|
+
</form>
|
32
42
|
<% end %>
|
33
|
-
|
34
|
-
|
35
|
-
<input type='hidden' name='alternative' value='<%= alternative.name %>'>
|
36
|
-
<input type="submit" value="Use this">
|
37
|
-
</form>
|
38
|
-
<% end %>
|
39
|
-
</td>
|
40
|
-
</tr>
|
43
|
+
</td>
|
44
|
+
</tr>
|
41
45
|
|
42
|
-
|
43
|
-
|
44
|
-
|
46
|
+
<% total_participants += alternative.participant_count %>
|
47
|
+
<% total_completed += alternative.completed_count %>
|
48
|
+
<% end %>
|
45
49
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
50
|
+
<tr class="totals">
|
51
|
+
<td>Totals</td>
|
52
|
+
<td><%= total_participants %></td>
|
53
|
+
<td><%= total_participants - total_completed %></td>
|
54
|
+
<td><%= total_completed %></td>
|
55
|
+
<td>N/A</td>
|
56
|
+
<td>N/A</td>
|
57
|
+
<td>N/A</td>
|
58
|
+
</tr>
|
59
|
+
</table>
|
60
|
+
<% end %>
|
61
|
+
<% else %>
|
62
|
+
<p class="intro">No experiments have started yet, you need to define them in your code and introduce them to your users.</p>
|
63
|
+
<p class="intro">Check out the <a href='https://github.com/andrew/split#readme'>Readme</a> for more help getting started.</p>
|
56
64
|
<% end %>
|
@@ -4,7 +4,7 @@
|
|
4
4
|
<meta content='text/html; charset=utf-8' http-equiv='Content-Type'>
|
5
5
|
<link href="<%= url 'reset.css' %>" media="screen" rel="stylesheet" type="text/css">
|
6
6
|
<link href="<%= url 'style.css' %>" media="screen" rel="stylesheet" type="text/css">
|
7
|
-
|
7
|
+
<script type="text/javascript" src='<%= url 'dashboard.js' %>'></script>
|
8
8
|
<title>Split</title>
|
9
9
|
|
10
10
|
</head>
|
data/lib/split/dashboard.rb
CHANGED
data/lib/split/experiment.rb
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
module Split
|
2
2
|
class Experiment
|
3
3
|
attr_accessor :name
|
4
|
-
attr_accessor :
|
4
|
+
attr_accessor :alternative_names
|
5
5
|
attr_accessor :winner
|
6
6
|
|
7
|
-
def initialize(name, *
|
7
|
+
def initialize(name, *alternative_names)
|
8
8
|
@name = name.to_s
|
9
|
-
@
|
9
|
+
@alternative_names = alternative_names
|
10
10
|
end
|
11
11
|
|
12
12
|
def winner
|
@@ -17,21 +17,48 @@ module Split
|
|
17
17
|
end
|
18
18
|
end
|
19
19
|
|
20
|
+
def control
|
21
|
+
alternatives.first
|
22
|
+
end
|
23
|
+
|
24
|
+
def reset_winner
|
25
|
+
Split.redis.hdel(:experiment_winner, name)
|
26
|
+
end
|
27
|
+
|
20
28
|
def winner=(winner_name)
|
21
29
|
Split.redis.hset(:experiment_winner, name, winner_name.to_s)
|
22
30
|
end
|
23
31
|
|
24
32
|
def alternatives
|
25
|
-
@
|
33
|
+
@alternative_names.map {|a| Split::Alternative.find_or_create(a, name)}
|
26
34
|
end
|
27
35
|
|
28
36
|
def next_alternative
|
29
37
|
winner || alternatives.sort_by{|a| a.participant_count + rand}.first
|
30
38
|
end
|
31
39
|
|
40
|
+
def reset
|
41
|
+
alternatives.each do |alternative|
|
42
|
+
alternative.reset
|
43
|
+
end
|
44
|
+
reset_winner
|
45
|
+
end
|
46
|
+
|
32
47
|
def save
|
33
48
|
Split.redis.sadd(:experiments, name)
|
34
|
-
@
|
49
|
+
@alternative_names.reverse.each {|a| Split.redis.lpush(name, a) }
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.load_alternatives_for(name)
|
53
|
+
case Split.redis.type(name)
|
54
|
+
when 'set' # convert legacy sets to lists
|
55
|
+
alts = Split.redis.smembers(name)
|
56
|
+
Split.redis.del(name)
|
57
|
+
alts.reverse.each {|a| Split.redis.lpush(name, a) }
|
58
|
+
Split.redis.lrange(name, 0, -1)
|
59
|
+
else
|
60
|
+
Split.redis.lrange(name, 0, -1)
|
61
|
+
end
|
35
62
|
end
|
36
63
|
|
37
64
|
def self.all
|
@@ -40,7 +67,7 @@ module Split
|
|
40
67
|
|
41
68
|
def self.find(name)
|
42
69
|
if Split.redis.exists(name)
|
43
|
-
self.new(name, *
|
70
|
+
self.new(name, *load_alternatives_for(name))
|
44
71
|
else
|
45
72
|
raise 'Experiment not found'
|
46
73
|
end
|
@@ -48,7 +75,7 @@ module Split
|
|
48
75
|
|
49
76
|
def self.find_or_create(name, *alternatives)
|
50
77
|
if Split.redis.exists(name)
|
51
|
-
return self.new(name, *
|
78
|
+
return self.new(name, *load_alternatives_for(name))
|
52
79
|
else
|
53
80
|
experiment = self.new(name, *alternatives)
|
54
81
|
experiment.save
|
data/lib/split/helper.rb
CHANGED
@@ -4,6 +4,12 @@ module Split
|
|
4
4
|
experiment = Split::Experiment.find_or_create(experiment_name, *alternatives)
|
5
5
|
return experiment.winner.name if experiment.winner
|
6
6
|
|
7
|
+
if forced_alternative = override(experiment_name, alternatives)
|
8
|
+
return forced_alternative
|
9
|
+
end
|
10
|
+
|
11
|
+
ab_user[experiment_name] = experiment.control.name if is_robot?
|
12
|
+
|
7
13
|
if ab_user[experiment_name]
|
8
14
|
return ab_user[experiment_name]
|
9
15
|
else
|
@@ -15,14 +21,23 @@ module Split
|
|
15
21
|
end
|
16
22
|
|
17
23
|
def finished(experiment_name)
|
24
|
+
return if is_robot?
|
18
25
|
alternative_name = ab_user[experiment_name]
|
19
26
|
alternative = Split::Alternative.find(alternative_name, experiment_name)
|
20
27
|
alternative.increment_completion
|
21
28
|
session[:split].delete(experiment_name)
|
22
29
|
end
|
23
30
|
|
31
|
+
def override(experiment_name, alternatives)
|
32
|
+
return params[experiment_name] if defined?(params) && alternatives.include?(params[experiment_name])
|
33
|
+
end
|
34
|
+
|
24
35
|
def ab_user
|
25
36
|
session[:split] ||= {}
|
26
37
|
end
|
38
|
+
|
39
|
+
def is_robot?
|
40
|
+
request.user_agent =~ /\b(Baidu|Gigabot|Googlebot|libwww-perl|lwp-trivial|msnbot|SiteUptime|Slurp|WordPress|ZIBB|ZyBorg)\b/i
|
41
|
+
end
|
27
42
|
end
|
28
43
|
end
|
data/lib/split/version.rb
CHANGED
@@ -0,0 +1,95 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'split/alternative'
|
3
|
+
|
4
|
+
describe Split::Alternative do
|
5
|
+
before(:each) { Split.redis.flushall }
|
6
|
+
|
7
|
+
it "should have a name" do
|
8
|
+
experiment = Split::Experiment.new('basket_text', 'Basket', "Cart")
|
9
|
+
alternative = Split::Alternative.new('Basket', 'basket_text')
|
10
|
+
alternative.name.should eql('Basket')
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should have a default participation count of 0" do
|
14
|
+
alternative = Split::Alternative.new('Basket', 'basket_text')
|
15
|
+
alternative.participant_count.should eql(0)
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should have a default completed count of 0" do
|
19
|
+
alternative = Split::Alternative.new('Basket', 'basket_text')
|
20
|
+
alternative.completed_count.should eql(0)
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should belong to an experiment" do
|
24
|
+
experiment = Split::Experiment.new('basket_text', 'Basket', "Cart")
|
25
|
+
experiment.save
|
26
|
+
alternative = Split::Alternative.find('Basket', 'basket_text')
|
27
|
+
alternative.experiment.name.should eql(experiment.name)
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should save to redis" do
|
31
|
+
alternative = Split::Alternative.new('Basket', 'basket_text')
|
32
|
+
alternative.save
|
33
|
+
Split.redis.exists('basket_text:Basket').should be true
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should increment participation count" do
|
37
|
+
experiment = Split::Experiment.new('basket_text', 'Basket', "Cart")
|
38
|
+
experiment.save
|
39
|
+
alternative = Split::Alternative.find('Basket', 'basket_text')
|
40
|
+
old_participant_count = alternative.participant_count
|
41
|
+
alternative.increment_participation
|
42
|
+
alternative.participant_count.should eql(old_participant_count+1)
|
43
|
+
end
|
44
|
+
|
45
|
+
it "should increment completed count" do
|
46
|
+
experiment = Split::Experiment.new('basket_text', 'Basket', "Cart")
|
47
|
+
experiment.save
|
48
|
+
alternative = Split::Alternative.find('Basket', 'basket_text')
|
49
|
+
old_completed_count = alternative.participant_count
|
50
|
+
alternative.increment_completion
|
51
|
+
alternative.completed_count.should eql(old_completed_count+1)
|
52
|
+
end
|
53
|
+
|
54
|
+
it "can be reset" do
|
55
|
+
alternative = Split::Alternative.new('Basket', 'basket_text', {'participant_count' => 10, 'completed_count' =>4})
|
56
|
+
alternative.save
|
57
|
+
alternative.reset
|
58
|
+
alternative.participant_count.should eql(0)
|
59
|
+
alternative.completed_count.should eql(0)
|
60
|
+
end
|
61
|
+
|
62
|
+
describe 'conversion rate' do
|
63
|
+
it "should be 0 if there are no conversions" do
|
64
|
+
alternative = Split::Alternative.new('Basket', 'basket_text')
|
65
|
+
alternative.completed_count.should eql(0)
|
66
|
+
alternative.conversion_rate.should eql(0)
|
67
|
+
end
|
68
|
+
|
69
|
+
it "does something" do
|
70
|
+
alternative = Split::Alternative.new('Basket', 'basket_text', {'participant_count' => 10, 'completed_count' =>4})
|
71
|
+
alternative.conversion_rate.should eql(0.4)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
it "should return an existing alternative" do
|
76
|
+
alternative = Split::Alternative.create('Basket', 'basket_text')
|
77
|
+
Split::Alternative.find('Basket', 'basket_text').name.should eql('Basket')
|
78
|
+
end
|
79
|
+
|
80
|
+
describe 'z score' do
|
81
|
+
it 'should be zero when the control has no conversions' do
|
82
|
+
experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red')
|
83
|
+
|
84
|
+
alternative = Split::Alternative.find('red', 'link_color')
|
85
|
+
alternative.z_score.should eql(0)
|
86
|
+
end
|
87
|
+
|
88
|
+
it "should be N/A for the control" do
|
89
|
+
experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red')
|
90
|
+
|
91
|
+
control = experiment.control
|
92
|
+
control.z_score.should eql('N/A')
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
data/spec/experiment_spec.rb
CHANGED
@@ -26,6 +26,14 @@ describe Split::Experiment do
|
|
26
26
|
Split::Experiment.find('basket_text').name.should eql('basket_text')
|
27
27
|
end
|
28
28
|
|
29
|
+
describe 'control' do
|
30
|
+
it 'should be the first alternative' do
|
31
|
+
experiment = Split::Experiment.new('basket_text', 'Basket', "Cart")
|
32
|
+
experiment.save
|
33
|
+
experiment.control.name.should eql('Basket')
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
29
37
|
describe 'winner' do
|
30
38
|
it "should have no winner initially" do
|
31
39
|
experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red')
|
@@ -41,6 +49,36 @@ describe Split::Experiment do
|
|
41
49
|
end
|
42
50
|
end
|
43
51
|
|
52
|
+
describe 'reset' do
|
53
|
+
it 'should reset all alternatives' do
|
54
|
+
experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red', 'green')
|
55
|
+
green = Split::Alternative.find('green', 'link_color')
|
56
|
+
experiment.winner = 'green'
|
57
|
+
|
58
|
+
experiment.next_alternative.name.should eql('green')
|
59
|
+
green.increment_participation
|
60
|
+
|
61
|
+
experiment.reset
|
62
|
+
|
63
|
+
reset_green = Split::Alternative.find('green', 'link_color')
|
64
|
+
reset_green.participant_count.should eql(0)
|
65
|
+
reset_green.completed_count.should eql(0)
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'should reset the winner' do
|
69
|
+
experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red', 'green')
|
70
|
+
green = Split::Alternative.find('green', 'link_color')
|
71
|
+
experiment.winner = 'green'
|
72
|
+
|
73
|
+
experiment.next_alternative.name.should eql('green')
|
74
|
+
green.increment_participation
|
75
|
+
|
76
|
+
experiment.reset
|
77
|
+
|
78
|
+
experiment.winner.should be_nil
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
44
82
|
describe 'next_alternative' do
|
45
83
|
it "should return a random alternative from those with the least participants" do
|
46
84
|
experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red', 'green')
|
@@ -48,7 +86,7 @@ describe Split::Experiment do
|
|
48
86
|
Split::Alternative.find('blue', 'link_color').increment_participation
|
49
87
|
Split::Alternative.find('red', 'link_color').increment_participation
|
50
88
|
|
51
|
-
experiment.next_alternative.name.should
|
89
|
+
experiment.next_alternative.name.should eql('green')
|
52
90
|
end
|
53
91
|
|
54
92
|
it "should always return the winner if one exists" do
|
@@ -56,11 +94,11 @@ describe Split::Experiment do
|
|
56
94
|
green = Split::Alternative.find('green', 'link_color')
|
57
95
|
experiment.winner = 'green'
|
58
96
|
|
59
|
-
experiment.next_alternative.name.should
|
97
|
+
experiment.next_alternative.name.should eql('green')
|
60
98
|
green.increment_participation
|
61
99
|
|
62
100
|
experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red', 'green')
|
63
|
-
experiment.next_alternative.name.should
|
101
|
+
experiment.next_alternative.name.should eql('green')
|
64
102
|
end
|
65
103
|
end
|
66
104
|
end
|
data/spec/helper_spec.rb
CHANGED
@@ -6,6 +6,7 @@ describe Split::Helper do
|
|
6
6
|
before(:each) do
|
7
7
|
Split.redis.flushall
|
8
8
|
@session = {}
|
9
|
+
params = nil
|
9
10
|
end
|
10
11
|
|
11
12
|
describe "ab_test" do
|
@@ -41,6 +42,13 @@ describe Split::Helper do
|
|
41
42
|
|
42
43
|
ab_test('link_color', 'blue', 'red').should == 'orange'
|
43
44
|
end
|
45
|
+
|
46
|
+
it "should allow the alternative to be force by passing it in the params" do
|
47
|
+
experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red')
|
48
|
+
@params = {'link_color' => 'blue'}
|
49
|
+
alternative = ab_test('link_color', 'blue', 'red')
|
50
|
+
alternative.should eql('blue')
|
51
|
+
end
|
44
52
|
end
|
45
53
|
|
46
54
|
describe 'finished' do
|
@@ -63,7 +71,7 @@ describe Split::Helper do
|
|
63
71
|
|
64
72
|
previous_completion_count = Split::Alternative.find(alternative_name, 'link_color').completed_count
|
65
73
|
|
66
|
-
session[:split].should
|
74
|
+
session[:split].should eql("link_color" => alternative_name)
|
67
75
|
finished('link_color')
|
68
76
|
session[:split].should == {}
|
69
77
|
end
|
@@ -83,4 +91,46 @@ describe Split::Helper do
|
|
83
91
|
new_convertion_rate.should eql(1.0)
|
84
92
|
end
|
85
93
|
end
|
86
|
-
|
94
|
+
|
95
|
+
describe 'when user is a robot' do
|
96
|
+
before(:each) do
|
97
|
+
@request = OpenStruct.new(:user_agent => 'Googlebot/2.1 (+http://www.google.com/bot.html)')
|
98
|
+
end
|
99
|
+
|
100
|
+
describe 'ab_test' do
|
101
|
+
it 'should return the control' do
|
102
|
+
experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red')
|
103
|
+
alternative = ab_test('link_color', 'blue', 'red')
|
104
|
+
alternative.should eql experiment.control.name
|
105
|
+
end
|
106
|
+
|
107
|
+
it "should not increment the participation count" do
|
108
|
+
experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red')
|
109
|
+
|
110
|
+
previous_red_count = Split::Alternative.find('red', 'link_color').participant_count
|
111
|
+
previous_blue_count = Split::Alternative.find('blue', 'link_color').participant_count
|
112
|
+
|
113
|
+
ab_test('link_color', 'blue', 'red')
|
114
|
+
|
115
|
+
new_red_count = Split::Alternative.find('red', 'link_color').participant_count
|
116
|
+
new_blue_count = Split::Alternative.find('blue', 'link_color').participant_count
|
117
|
+
|
118
|
+
(new_red_count + new_blue_count).should eql(previous_red_count + previous_blue_count)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
describe 'finished' do
|
122
|
+
it "should not increment the completed count" do
|
123
|
+
experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red')
|
124
|
+
alternative_name = ab_test('link_color', 'blue', 'red')
|
125
|
+
|
126
|
+
previous_completion_count = Split::Alternative.find(alternative_name, 'link_color').completed_count
|
127
|
+
|
128
|
+
finished('link_color')
|
129
|
+
|
130
|
+
new_completion_count = Split::Alternative.find(alternative_name, 'link_color').completed_count
|
131
|
+
|
132
|
+
new_completion_count.should eql(previous_completion_count)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,7 +1,18 @@
|
|
1
1
|
require 'rubygems'
|
2
2
|
require 'bundler/setup'
|
3
3
|
require 'split'
|
4
|
+
require 'ostruct'
|
4
5
|
|
5
6
|
def session
|
6
7
|
@session ||= {}
|
7
8
|
end
|
9
|
+
|
10
|
+
def params
|
11
|
+
@params ||= {}
|
12
|
+
end
|
13
|
+
|
14
|
+
def request(ua = 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_6; de-de) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27')
|
15
|
+
r = OpenStruct.new
|
16
|
+
r.user_agent = ua
|
17
|
+
@request ||= r
|
18
|
+
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: split
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
5
|
-
prerelease:
|
4
|
+
hash: 21
|
5
|
+
prerelease: false
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
+
- 2
|
8
9
|
- 1
|
9
|
-
|
10
|
-
version: 0.1.1
|
10
|
+
version: 0.2.1
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Andrew Nesbitt
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2011-05-
|
18
|
+
date: 2011-05-29 00:00:00 +01:00
|
19
19
|
default_executable:
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|
@@ -91,12 +91,15 @@ extra_rdoc_files: []
|
|
91
91
|
|
92
92
|
files:
|
93
93
|
- .gitignore
|
94
|
+
- CHANGELOG.mdown
|
94
95
|
- Gemfile
|
96
|
+
- LICENSE
|
95
97
|
- README.mdown
|
96
98
|
- Rakefile
|
97
99
|
- lib/split.rb
|
98
100
|
- lib/split/alternative.rb
|
99
101
|
- lib/split/dashboard.rb
|
102
|
+
- lib/split/dashboard/public/dashboard.js
|
100
103
|
- lib/split/dashboard/public/reset.css
|
101
104
|
- lib/split/dashboard/public/style.css
|
102
105
|
- lib/split/dashboard/views/index.erb
|
@@ -104,6 +107,7 @@ files:
|
|
104
107
|
- lib/split/experiment.rb
|
105
108
|
- lib/split/helper.rb
|
106
109
|
- lib/split/version.rb
|
110
|
+
- spec/alternative_spec.rb
|
107
111
|
- spec/experiment_spec.rb
|
108
112
|
- spec/helper_spec.rb
|
109
113
|
- spec/spec_helper.rb
|
@@ -138,11 +142,12 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
138
142
|
requirements: []
|
139
143
|
|
140
144
|
rubyforge_project: split
|
141
|
-
rubygems_version: 1.
|
145
|
+
rubygems_version: 1.3.7
|
142
146
|
signing_key:
|
143
147
|
specification_version: 3
|
144
148
|
summary: Rack based split testing framework
|
145
149
|
test_files:
|
150
|
+
- spec/alternative_spec.rb
|
146
151
|
- spec/experiment_spec.rb
|
147
152
|
- spec/helper_spec.rb
|
148
153
|
- spec/spec_helper.rb
|