split 0.2.2 → 0.2.3
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.mdown +12 -0
- data/Guardfile +5 -0
- data/README.mdown +7 -0
- data/Rakefile +2 -3
- data/lib/split.rb +0 -1
- data/lib/split/dashboard.rb +34 -0
- data/lib/split/dashboard/public/dashboard.js +8 -0
- data/lib/split/dashboard/public/style.css +180 -47
- data/lib/split/dashboard/views/_experiment.erb +82 -0
- data/lib/split/dashboard/views/index.erb +1 -67
- data/lib/split/experiment.rb +30 -1
- data/lib/split/helper.rb +35 -15
- data/lib/split/version.rb +1 -1
- data/spec/dashboard_spec.rb +7 -0
- data/spec/experiment_spec.rb +25 -0
- data/spec/helper_spec.rb +65 -0
- data/split.gemspec +8 -7
- metadata +41 -9
data/CHANGELOG.mdown
CHANGED
@@ -1,3 +1,15 @@
|
|
1
|
+
## 0.2.3 (June 26, 2011)
|
2
|
+
|
3
|
+
Features:
|
4
|
+
|
5
|
+
- Experiments can now be deleted from the dashboard
|
6
|
+
- ab_test helper now accepts a block
|
7
|
+
- Improved dashboard
|
8
|
+
|
9
|
+
Bugfixes:
|
10
|
+
|
11
|
+
- After resetting an experiment, existing users of that experiment will also be reset
|
12
|
+
|
1
13
|
## 0.2.2 (June 11, 2011)
|
2
14
|
|
3
15
|
Features:
|
data/Guardfile
ADDED
data/README.mdown
CHANGED
@@ -4,6 +4,8 @@ Split is a rack based ab testing framework designed to work with Rails, Sinatra
|
|
4
4
|
|
5
5
|
Split is heavily inspired by the Abingo and Vanity rails ab testing plugins and Resque in its use of Redis.
|
6
6
|
|
7
|
+
Split is designed to be hacker friendly, allowing for maximum customisation and extensibility.
|
8
|
+
|
7
9
|
## Requirements
|
8
10
|
|
9
11
|
Split uses redis as a datastore.
|
@@ -181,6 +183,11 @@ Simply use the `Split.redis.namespace` accessor:
|
|
181
183
|
We recommend sticking this in your initializer somewhere after Redis
|
182
184
|
is configured.
|
183
185
|
|
186
|
+
## Extensions
|
187
|
+
|
188
|
+
- [Split::Export](http://github.com/andrew/split-export) - easily export ab test data out of Split
|
189
|
+
- [Split::Analytics](http://github.com/andrew/split-analytics) - push test data to google analytics
|
190
|
+
|
184
191
|
## Contributors
|
185
192
|
|
186
193
|
Special thanks to the following people for submitting patches:
|
data/Rakefile
CHANGED
data/lib/split.rb
CHANGED
data/lib/split/dashboard.rb
CHANGED
@@ -9,6 +9,7 @@ module Split
|
|
9
9
|
set :views, "#{dir}/dashboard/views"
|
10
10
|
set :public, "#{dir}/dashboard/public"
|
11
11
|
set :static, true
|
12
|
+
set :method_override, true
|
12
13
|
|
13
14
|
helpers do
|
14
15
|
def url(*path_parts)
|
@@ -26,6 +27,33 @@ module Split
|
|
26
27
|
def round(number, precision = 2)
|
27
28
|
BigDecimal.new(number.to_s).round(precision).to_f
|
28
29
|
end
|
30
|
+
|
31
|
+
def confidence_level(z_score)
|
32
|
+
z = z_score.to_f
|
33
|
+
if z > 0.0
|
34
|
+
if z < 1.96
|
35
|
+
'no confidence'
|
36
|
+
elsif z < 2.57
|
37
|
+
'95% confidence'
|
38
|
+
elsif z < 3.29
|
39
|
+
'99% confidence'
|
40
|
+
else
|
41
|
+
'99.9% confidence'
|
42
|
+
end
|
43
|
+
elsif z < 0.0
|
44
|
+
if z > -1.96
|
45
|
+
'no confidence'
|
46
|
+
elsif z > -2.57
|
47
|
+
'95% confidence'
|
48
|
+
elsif z > -3.29
|
49
|
+
'99% confidence'
|
50
|
+
else
|
51
|
+
'99.9% confidence'
|
52
|
+
end
|
53
|
+
else
|
54
|
+
"No Change"
|
55
|
+
end
|
56
|
+
end
|
29
57
|
end
|
30
58
|
|
31
59
|
get '/' do
|
@@ -45,5 +73,11 @@ module Split
|
|
45
73
|
@experiment.reset
|
46
74
|
redirect url('/')
|
47
75
|
end
|
76
|
+
|
77
|
+
delete '/:experiment' do
|
78
|
+
@experiment = Split::Experiment.find(params[:experiment])
|
79
|
+
@experiment.delete
|
80
|
+
redirect url('/')
|
81
|
+
end
|
48
82
|
end
|
49
83
|
end
|
@@ -6,6 +6,14 @@ function confirmReset() {
|
|
6
6
|
return false;
|
7
7
|
}
|
8
8
|
|
9
|
+
function confirmDelete() {
|
10
|
+
var agree=confirm("Are you sure you want to delete this experiment and all it's data?");
|
11
|
+
if (agree)
|
12
|
+
return true;
|
13
|
+
else
|
14
|
+
return false;
|
15
|
+
}
|
16
|
+
|
9
17
|
function confirmWinner() {
|
10
18
|
var agree=confirm("This will now be returned for all users. Are you sure?");
|
11
19
|
if (agree)
|
@@ -1,58 +1,191 @@
|
|
1
|
-
html {
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
#
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
#
|
32
|
-
|
33
|
-
|
1
|
+
html {
|
2
|
+
background: #efefef;
|
3
|
+
font-family: Arial, Verdana, sans-serif;
|
4
|
+
font-size: 13px;
|
5
|
+
}
|
6
|
+
|
7
|
+
body {
|
8
|
+
padding: 0;
|
9
|
+
margin: 0;
|
10
|
+
}
|
11
|
+
|
12
|
+
.header {
|
13
|
+
background: #000;
|
14
|
+
padding: 8px 5% 0 5%;
|
15
|
+
border-bottom: 1px solid #444;
|
16
|
+
border-bottom: 5px solid #0080FF;
|
17
|
+
}
|
18
|
+
|
19
|
+
.header h1 {
|
20
|
+
color: #333;
|
21
|
+
font-size: 90%;
|
22
|
+
font-weight: bold;
|
23
|
+
margin-bottom: 6px;
|
24
|
+
}
|
25
|
+
|
26
|
+
.header ul li {
|
27
|
+
display: inline;
|
28
|
+
}
|
29
|
+
|
30
|
+
.header ul li a {
|
31
|
+
color: #fff;
|
32
|
+
text-decoration: none;
|
33
|
+
margin-right: 10px;
|
34
|
+
display: inline-block;
|
35
|
+
padding: 8px;
|
36
|
+
-webkit-border-top-right-radius: 6px;
|
37
|
+
-webkit-border-top-left-radius: 6px;
|
38
|
+
-moz-border-radius-topleft: 6px;
|
39
|
+
-moz-border-radius-topright: 6px;
|
40
|
+
}
|
41
|
+
|
42
|
+
.header ul li a:hover {
|
43
|
+
background: #333;
|
44
|
+
}
|
45
|
+
|
46
|
+
.header ul li.current a {
|
47
|
+
background: #0080FF;
|
48
|
+
font-weight: bold;
|
49
|
+
color: #fff;
|
50
|
+
}
|
51
|
+
|
52
|
+
#main {
|
53
|
+
padding: 10px 5%;
|
54
|
+
background: #fff;
|
55
|
+
overflow: hidden;
|
56
|
+
}
|
57
|
+
|
58
|
+
#main .logo {
|
59
|
+
float: right;
|
60
|
+
margin: 10px;
|
61
|
+
}
|
62
|
+
|
63
|
+
#main span.hl {
|
64
|
+
background: #efefef;
|
65
|
+
padding: 2px;
|
66
|
+
}
|
67
|
+
|
68
|
+
#main h1 {
|
69
|
+
margin: 10px 0;
|
70
|
+
font-size: 190%;
|
71
|
+
font-weight: bold;
|
72
|
+
color: #0080FF;
|
73
|
+
}
|
74
|
+
|
75
|
+
#main table {
|
76
|
+
width: 100%;
|
77
|
+
margin: 10px 0;
|
78
|
+
}
|
79
|
+
|
80
|
+
#main table tr td, #main table tr th {
|
81
|
+
border: 1px solid #ccc;
|
82
|
+
padding: 6px;
|
83
|
+
}
|
84
|
+
|
85
|
+
#main table tr th {
|
86
|
+
background: #efefef;
|
87
|
+
color: #888;
|
88
|
+
font-size: 80%;
|
89
|
+
font-weight: bold;
|
90
|
+
}
|
91
|
+
|
92
|
+
#main table tr td.no-data {
|
93
|
+
text-align: center;
|
94
|
+
padding: 40px 0;
|
95
|
+
color: #999;
|
96
|
+
font-style: italic;
|
97
|
+
font-size: 130%;
|
98
|
+
}
|
99
|
+
|
100
|
+
#main a {
|
101
|
+
color: #111;
|
102
|
+
}
|
103
|
+
|
104
|
+
#main p {
|
105
|
+
margin: 5px 0;
|
106
|
+
}
|
107
|
+
|
108
|
+
#main p.intro {
|
109
|
+
margin-bottom: 15px;
|
110
|
+
font-size: 85%;
|
111
|
+
color: #999;
|
112
|
+
margin-top: 0;
|
113
|
+
line-height: 1.3;
|
114
|
+
}
|
115
|
+
|
116
|
+
#main h1.wi {
|
117
|
+
margin-bottom: 5px;
|
118
|
+
}
|
119
|
+
|
120
|
+
#main p.sub {
|
121
|
+
font-size: 95%;
|
122
|
+
color: #999;
|
123
|
+
}
|
124
|
+
|
125
|
+
.experiment {
|
126
|
+
width: 60%;
|
127
|
+
border-top:1px solid #eee;
|
128
|
+
margin:15px 0;
|
129
|
+
}
|
130
|
+
|
131
|
+
.experiment h2 {
|
132
|
+
margin: 10px 0;
|
133
|
+
font-size: 130%;
|
134
|
+
font-weight:bold;
|
34
135
|
float:left;
|
35
136
|
}
|
36
137
|
|
37
|
-
.
|
38
|
-
|
39
|
-
font-size:
|
40
|
-
|
138
|
+
.experiment h2 .version{
|
139
|
+
font-style:italic;
|
140
|
+
font-size:0.8em;
|
141
|
+
color:#bbb;
|
142
|
+
font-weight:normal;
|
143
|
+
}
|
144
|
+
|
145
|
+
.experiment table em{
|
146
|
+
font-style:italic;
|
147
|
+
font-size:0.9em;
|
148
|
+
color:#bbb;
|
41
149
|
}
|
42
150
|
|
43
|
-
.
|
44
|
-
|
151
|
+
.experiment table .totals td {
|
152
|
+
background: #eee;
|
153
|
+
font-weight: bold;
|
45
154
|
}
|
46
155
|
|
47
|
-
|
48
|
-
|
156
|
+
#footer {
|
157
|
+
padding: 10px 5%;
|
158
|
+
background: #efefef;
|
159
|
+
color: #999;
|
160
|
+
font-size: 85%;
|
161
|
+
line-height: 1.5;
|
162
|
+
border-top: 5px solid #ccc;
|
163
|
+
padding-top: 10px;
|
49
164
|
}
|
50
165
|
|
51
|
-
|
52
|
-
color
|
53
|
-
font-size:10px;
|
166
|
+
#footer p a {
|
167
|
+
color: #999;
|
54
168
|
}
|
55
169
|
|
56
|
-
.
|
57
|
-
|
58
|
-
}
|
170
|
+
.inline-controls {
|
171
|
+
float:right;
|
172
|
+
}
|
173
|
+
|
174
|
+
.inline-controls form {
|
175
|
+
display: inline-block;
|
176
|
+
font-size: 10px;
|
177
|
+
line-height: 38px;
|
178
|
+
}
|
179
|
+
|
180
|
+
.inline-controls input {
|
181
|
+
margin-left: 10px;
|
182
|
+
}
|
183
|
+
|
184
|
+
.worse, .better {
|
185
|
+
color: #F00;
|
186
|
+
font-size: 10px;
|
187
|
+
}
|
188
|
+
|
189
|
+
.better {
|
190
|
+
color: #00D500;
|
191
|
+
}
|
@@ -0,0 +1,82 @@
|
|
1
|
+
<div class="experiment">
|
2
|
+
<h2>Experiment: <%= experiment.name %> <% if experiment.version > 1 %><span class='version'>v<%= experiment.version %></span><% end %></h2>
|
3
|
+
<div class='inline-controls'>
|
4
|
+
<form action="<%= url "/reset/#{experiment.name}" %>" method='post' onclick="return confirmReset()">
|
5
|
+
<input type="submit" value="Reset Data">
|
6
|
+
</form>
|
7
|
+
<form action="<%= url "/#{experiment.name}" %>" method='post' onclick="return confirmDelete()">
|
8
|
+
<input type="hidden" name="_method" value="delete"/>
|
9
|
+
<input type="submit" value="Delete">
|
10
|
+
</form>
|
11
|
+
</div>
|
12
|
+
<table>
|
13
|
+
<tr>
|
14
|
+
<th>Alternative Name</th>
|
15
|
+
<th>Participants</th>
|
16
|
+
<th>Non-finished</th>
|
17
|
+
<th>Completed</th>
|
18
|
+
<th>Conversion Rate</th>
|
19
|
+
<th>Confidence</th>
|
20
|
+
<th>Finish</th>
|
21
|
+
</tr>
|
22
|
+
|
23
|
+
<% total_participants = total_completed = 0 %>
|
24
|
+
<% experiment.alternatives.each do |alternative| %>
|
25
|
+
<tr>
|
26
|
+
<td>
|
27
|
+
<%= alternative.name %>
|
28
|
+
<% if alternative.control? %>
|
29
|
+
<em>control</em>
|
30
|
+
<% end %>
|
31
|
+
</td>
|
32
|
+
<td><%= alternative.participant_count %></td>
|
33
|
+
<td><%= alternative.participant_count - alternative.completed_count %></td>
|
34
|
+
<td><%= alternative.completed_count %></td>
|
35
|
+
<td>
|
36
|
+
<%= number_to_percentage(alternative.conversion_rate) %>%
|
37
|
+
<% if experiment.control.conversion_rate > 0 && !alternative.control? %>
|
38
|
+
<% if alternative.conversion_rate > experiment.control.conversion_rate %>
|
39
|
+
<span class='better'>
|
40
|
+
+<%= number_to_percentage((alternative.conversion_rate/experiment.control.conversion_rate)-1) %>%
|
41
|
+
</span>
|
42
|
+
<% elsif alternative.conversion_rate < experiment.control.conversion_rate %>
|
43
|
+
<span class='worse'>
|
44
|
+
<%= number_to_percentage((alternative.conversion_rate/experiment.control.conversion_rate)-1) %>%
|
45
|
+
</span>
|
46
|
+
<% end %>
|
47
|
+
<% end %>
|
48
|
+
</td>
|
49
|
+
<td>
|
50
|
+
<span title='z-score: <%= round(alternative.z_score, 3) %>'><%= confidence_level(alternative.z_score) %></span>
|
51
|
+
</td>
|
52
|
+
<td>
|
53
|
+
<% if experiment.winner %>
|
54
|
+
<% if experiment.winner.name == alternative.name %>
|
55
|
+
Winner
|
56
|
+
<% else %>
|
57
|
+
Loser
|
58
|
+
<% end %>
|
59
|
+
<% else %>
|
60
|
+
<form action="<%= url experiment.name %>" method='post' onclick="return confirmWinner()">
|
61
|
+
<input type='hidden' name='alternative' value='<%= alternative.name %>'>
|
62
|
+
<input type="submit" value="Use this">
|
63
|
+
</form>
|
64
|
+
<% end %>
|
65
|
+
</td>
|
66
|
+
</tr>
|
67
|
+
|
68
|
+
<% total_participants += alternative.participant_count %>
|
69
|
+
<% total_completed += alternative.completed_count %>
|
70
|
+
<% end %>
|
71
|
+
|
72
|
+
<tr class="totals">
|
73
|
+
<td>Totals</td>
|
74
|
+
<td><%= total_participants %></td>
|
75
|
+
<td><%= total_participants - total_completed %></td>
|
76
|
+
<td><%= total_completed %></td>
|
77
|
+
<td>N/A</td>
|
78
|
+
<td>N/A</td>
|
79
|
+
<td>N/A</td>
|
80
|
+
</tr>
|
81
|
+
</table>
|
82
|
+
</div>
|
@@ -3,73 +3,7 @@
|
|
3
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>
|
4
4
|
|
5
5
|
<% @experiments.each do |experiment| %>
|
6
|
-
|
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">
|
11
|
-
<tr>
|
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>
|
29
|
-
<%= number_to_percentage(alternative.conversion_rate) %>%
|
30
|
-
<% if experiment.control.conversion_rate > 0 && !alternative.control? %>
|
31
|
-
<% if alternative.conversion_rate > experiment.control.conversion_rate %>
|
32
|
-
<span class='better'>
|
33
|
-
+<%= number_to_percentage((alternative.conversion_rate/experiment.control.conversion_rate)-1) %>%
|
34
|
-
</span>
|
35
|
-
<% elsif alternative.conversion_rate < experiment.control.conversion_rate %>
|
36
|
-
<span class='worse'>
|
37
|
-
<%= number_to_percentage((alternative.conversion_rate/experiment.control.conversion_rate)-1) %>%
|
38
|
-
</span>
|
39
|
-
<% end %>
|
40
|
-
<% end %>
|
41
|
-
</td>
|
42
|
-
<td><%= round(alternative.z_score, 3) %></td>
|
43
|
-
<td>
|
44
|
-
<% if experiment.winner %>
|
45
|
-
<% if experiment.winner.name == alternative.name %>
|
46
|
-
Winner
|
47
|
-
<% else %>
|
48
|
-
Loser
|
49
|
-
<% end %>
|
50
|
-
<% else %>
|
51
|
-
<form action="<%= url experiment.name %>" method='post' onclick="return confirmWinner()">
|
52
|
-
<input type='hidden' name='alternative' value='<%= alternative.name %>'>
|
53
|
-
<input type="submit" value="Use this">
|
54
|
-
</form>
|
55
|
-
<% end %>
|
56
|
-
</td>
|
57
|
-
</tr>
|
58
|
-
|
59
|
-
<% total_participants += alternative.participant_count %>
|
60
|
-
<% total_completed += alternative.completed_count %>
|
61
|
-
<% end %>
|
62
|
-
|
63
|
-
<tr class="totals">
|
64
|
-
<td>Totals</td>
|
65
|
-
<td><%= total_participants %></td>
|
66
|
-
<td><%= total_participants - total_completed %></td>
|
67
|
-
<td><%= total_completed %></td>
|
68
|
-
<td>N/A</td>
|
69
|
-
<td>N/A</td>
|
70
|
-
<td>N/A</td>
|
71
|
-
</tr>
|
72
|
-
</table>
|
6
|
+
<%= erb :_experiment, :locals => {:experiment => experiment} %>
|
73
7
|
<% end %>
|
74
8
|
<% else %>
|
75
9
|
<p class="intro">No experiments have started yet, you need to define them in your code and introduce them to your users.</p>
|
data/lib/split/experiment.rb
CHANGED
@@ -3,10 +3,12 @@ module Split
|
|
3
3
|
attr_accessor :name
|
4
4
|
attr_accessor :alternative_names
|
5
5
|
attr_accessor :winner
|
6
|
+
attr_accessor :version
|
6
7
|
|
7
8
|
def initialize(name, *alternative_names)
|
8
9
|
@name = name.to_s
|
9
10
|
@alternative_names = alternative_names
|
11
|
+
@version = (Split.redis.get("#{name.to_s}:version").to_i || 0)
|
10
12
|
end
|
11
13
|
|
12
14
|
def winner
|
@@ -37,9 +39,35 @@ module Split
|
|
37
39
|
winner || alternatives.sort_by{|a| a.participant_count + rand}.first
|
38
40
|
end
|
39
41
|
|
42
|
+
def version
|
43
|
+
@version ||= 0
|
44
|
+
end
|
45
|
+
|
46
|
+
def increment_version
|
47
|
+
@version += 1
|
48
|
+
Split.redis.set("#{name}:version", @version)
|
49
|
+
end
|
50
|
+
|
51
|
+
def key
|
52
|
+
if version.to_i > 0
|
53
|
+
"#{name}:#{version}"
|
54
|
+
else
|
55
|
+
name
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
40
59
|
def reset
|
41
60
|
alternatives.each(&:reset)
|
42
61
|
reset_winner
|
62
|
+
increment_version
|
63
|
+
end
|
64
|
+
|
65
|
+
def delete
|
66
|
+
alternatives.each(&:delete)
|
67
|
+
reset_winner
|
68
|
+
Split.redis.srem(:experiments, name)
|
69
|
+
Split.redis.del(name)
|
70
|
+
increment_version
|
43
71
|
end
|
44
72
|
|
45
73
|
def new_record?
|
@@ -77,7 +105,8 @@ module Split
|
|
77
105
|
end
|
78
106
|
end
|
79
107
|
|
80
|
-
def self.find_or_create(
|
108
|
+
def self.find_or_create(key, *alternatives)
|
109
|
+
name = key.split(':')[0]
|
81
110
|
if Split.redis.exists(name)
|
82
111
|
if load_alternatives_for(name) == alternatives
|
83
112
|
experiment = self.new(name, *load_alternatives_for(name))
|
data/lib/split/helper.rb
CHANGED
@@ -2,36 +2,56 @@ module Split
|
|
2
2
|
module Helper
|
3
3
|
def ab_test(experiment_name, *alternatives)
|
4
4
|
experiment = Split::Experiment.find_or_create(experiment_name, *alternatives)
|
5
|
-
|
5
|
+
if experiment.winner
|
6
|
+
ret = experiment.winner.name
|
7
|
+
else
|
8
|
+
if forced_alternative = override(experiment.key, alternatives)
|
9
|
+
ret = forced_alternative
|
10
|
+
else
|
11
|
+
begin_experiment(experiment, experiment.control.name) if exclude_visitor?
|
6
12
|
|
7
|
-
|
8
|
-
|
13
|
+
if ab_user[experiment.key]
|
14
|
+
ret = ab_user[experiment.key]
|
15
|
+
else
|
16
|
+
alternative = experiment.next_alternative
|
17
|
+
alternative.increment_participation
|
18
|
+
begin_experiment(experiment, alternative.name)
|
19
|
+
ret = alternative.name
|
20
|
+
end
|
21
|
+
end
|
9
22
|
end
|
10
23
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
24
|
+
if block_given?
|
25
|
+
if defined?(capture) # a block in a rails view
|
26
|
+
block = Proc.new { yield(ret) }
|
27
|
+
concat(capture(ret, &block))
|
28
|
+
false
|
29
|
+
else
|
30
|
+
yield(ret)
|
31
|
+
end
|
15
32
|
else
|
16
|
-
|
17
|
-
alternative.increment_participation
|
18
|
-
ab_user[experiment_name] = alternative.name
|
19
|
-
return alternative.name
|
33
|
+
ret
|
20
34
|
end
|
21
35
|
end
|
22
36
|
|
23
37
|
def finished(experiment_name)
|
24
38
|
return if exclude_visitor?
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
39
|
+
experiment = Split::Experiment.find(experiment_name)
|
40
|
+
if alternative_name = ab_user[experiment.key]
|
41
|
+
alternative = Split::Alternative.find(alternative_name, experiment_name)
|
42
|
+
alternative.increment_completion
|
43
|
+
session[:split].delete(experiment_name)
|
44
|
+
end
|
29
45
|
end
|
30
46
|
|
31
47
|
def override(experiment_name, alternatives)
|
32
48
|
return params[experiment_name] if defined?(params) && alternatives.include?(params[experiment_name])
|
33
49
|
end
|
34
50
|
|
51
|
+
def begin_experiment(experiment, alternative_name)
|
52
|
+
ab_user[experiment.key] = alternative_name
|
53
|
+
end
|
54
|
+
|
35
55
|
def ab_user
|
36
56
|
session[:split] ||= {}
|
37
57
|
end
|
data/lib/split/version.rb
CHANGED
data/spec/dashboard_spec.rb
CHANGED
@@ -36,6 +36,13 @@ describe Split::Dashboard do
|
|
36
36
|
new_red_count.should eql(0)
|
37
37
|
end
|
38
38
|
|
39
|
+
it "should delete an experiment" do
|
40
|
+
experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red')
|
41
|
+
delete '/link_color'
|
42
|
+
last_response.should be_redirect
|
43
|
+
lambda { Split::Experiment.find('link_color') }.should raise_error
|
44
|
+
end
|
45
|
+
|
39
46
|
it "should mark an alternative as the winner" do
|
40
47
|
experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red')
|
41
48
|
experiment.winner.should be_nil
|
data/spec/experiment_spec.rb
CHANGED
@@ -28,6 +28,24 @@ describe Split::Experiment do
|
|
28
28
|
Split.redis.lrange('basket_text', 0, -1).should eql(['Basket', "Cart"])
|
29
29
|
end
|
30
30
|
|
31
|
+
describe 'deleting' do
|
32
|
+
it 'should delete itself' do
|
33
|
+
experiment = Split::Experiment.new('basket_text', 'Basket', "Cart")
|
34
|
+
experiment.save
|
35
|
+
|
36
|
+
experiment.delete
|
37
|
+
Split.redis.exists('basket_text').should be false
|
38
|
+
lambda { Split::Experiment.find('link_color') }.should raise_error
|
39
|
+
end
|
40
|
+
|
41
|
+
it "should increment the version" do
|
42
|
+
experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red', 'green')
|
43
|
+
experiment.version.should eql(0)
|
44
|
+
experiment.delete
|
45
|
+
experiment.version.should eql(1)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
31
49
|
describe 'new record?' do
|
32
50
|
it "should know if it hasn't been saved yet" do
|
33
51
|
experiment = Split::Experiment.new('basket_text', 'Basket', "Cart")
|
@@ -98,6 +116,13 @@ describe Split::Experiment do
|
|
98
116
|
|
99
117
|
experiment.winner.should be_nil
|
100
118
|
end
|
119
|
+
|
120
|
+
it "should increment the version" do
|
121
|
+
experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red', 'green')
|
122
|
+
experiment.version.should eql(0)
|
123
|
+
experiment.reset
|
124
|
+
experiment.version.should eql(1)
|
125
|
+
end
|
101
126
|
end
|
102
127
|
|
103
128
|
describe 'next_alternative' do
|
data/spec/helper_spec.rb
CHANGED
@@ -51,6 +51,12 @@ describe Split::Helper do
|
|
51
51
|
alternative = ab_test('link_color', 'blue', 'red')
|
52
52
|
alternative.should eql('blue')
|
53
53
|
end
|
54
|
+
|
55
|
+
it "should allow passing a block" do
|
56
|
+
alt = ab_test('link_color', 'blue', 'red')
|
57
|
+
ret = ab_test('link_color', 'blue', 'red') { |alternative| "shared/#{alternative}" }
|
58
|
+
ret.should eql("shared/#{alt}")
|
59
|
+
end
|
54
60
|
end
|
55
61
|
|
56
62
|
describe 'finished' do
|
@@ -179,4 +185,63 @@ describe Split::Helper do
|
|
179
185
|
end
|
180
186
|
end
|
181
187
|
end
|
188
|
+
|
189
|
+
describe 'versioned experiments' do
|
190
|
+
it "should use version zero if no version is present" do
|
191
|
+
experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red')
|
192
|
+
alternative_name = ab_test('link_color', 'blue', 'red')
|
193
|
+
experiment.version.should eql(0)
|
194
|
+
session[:split].should eql({'link_color' => alternative_name})
|
195
|
+
end
|
196
|
+
|
197
|
+
it "should save the version of the experiment to the session" do
|
198
|
+
experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red')
|
199
|
+
experiment.reset
|
200
|
+
experiment.version.should eql(1)
|
201
|
+
alternative_name = ab_test('link_color', 'blue', 'red')
|
202
|
+
session[:split].should eql({'link_color:1' => alternative_name})
|
203
|
+
end
|
204
|
+
|
205
|
+
it "should load the experiment even if the version is not 0" do
|
206
|
+
experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red')
|
207
|
+
experiment.reset
|
208
|
+
experiment.version.should eql(1)
|
209
|
+
alternative_name = ab_test('link_color', 'blue', 'red')
|
210
|
+
session[:split].should eql({'link_color:1' => alternative_name})
|
211
|
+
return_alternative_name = ab_test('link_color', 'blue', 'red')
|
212
|
+
return_alternative_name.should eql(alternative_name)
|
213
|
+
end
|
214
|
+
|
215
|
+
it "should reset the session of a user on an older version of the experiment" do
|
216
|
+
experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red')
|
217
|
+
alternative_name = ab_test('link_color', 'blue', 'red')
|
218
|
+
session[:split].should eql({'link_color' => alternative_name})
|
219
|
+
alternative = Split::Alternative.find(alternative_name, 'link_color')
|
220
|
+
alternative.participant_count.should eql(1)
|
221
|
+
|
222
|
+
experiment.reset
|
223
|
+
experiment.version.should eql(1)
|
224
|
+
alternative = Split::Alternative.find(alternative_name, 'link_color')
|
225
|
+
alternative.participant_count.should eql(0)
|
226
|
+
|
227
|
+
new_alternative_name = ab_test('link_color', 'blue', 'red')
|
228
|
+
session[:split]['link_color:1'].should eql(new_alternative_name)
|
229
|
+
new_alternative = Split::Alternative.find(new_alternative_name, 'link_color')
|
230
|
+
new_alternative.participant_count.should eql(1)
|
231
|
+
end
|
232
|
+
|
233
|
+
it "should only count completion of users on the current version" do
|
234
|
+
experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red')
|
235
|
+
alternative_name = ab_test('link_color', 'blue', 'red')
|
236
|
+
session[:split].should eql({'link_color' => alternative_name})
|
237
|
+
alternative = Split::Alternative.find(alternative_name, 'link_color')
|
238
|
+
|
239
|
+
experiment.reset
|
240
|
+
experiment.version.should eql(1)
|
241
|
+
|
242
|
+
finished('link_color')
|
243
|
+
alternative = Split::Alternative.find(alternative_name, 'link_color')
|
244
|
+
alternative.completed_count.should eql(0)
|
245
|
+
end
|
246
|
+
end
|
182
247
|
end
|
data/split.gemspec
CHANGED
@@ -8,7 +8,7 @@ Gem::Specification.new do |s|
|
|
8
8
|
s.platform = Gem::Platform::RUBY
|
9
9
|
s.authors = ["Andrew Nesbitt"]
|
10
10
|
s.email = ["andrewnez@gmail.com"]
|
11
|
-
s.homepage = ""
|
11
|
+
s.homepage = "https://github.com/andrew/split"
|
12
12
|
s.summary = %q{Rack based split testing framework}
|
13
13
|
|
14
14
|
s.rubyforge_project = "split"
|
@@ -18,11 +18,12 @@ Gem::Specification.new do |s|
|
|
18
18
|
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
19
|
s.require_paths = ["lib"]
|
20
20
|
|
21
|
-
s.add_dependency
|
22
|
-
s.add_dependency
|
23
|
-
s.add_dependency
|
21
|
+
s.add_dependency 'redis', '~> 2.1'
|
22
|
+
s.add_dependency 'redis-namespace', '~> 1.0.3'
|
23
|
+
s.add_dependency 'sinatra', '~> 1.2.6'
|
24
24
|
|
25
|
-
|
26
|
-
s.add_development_dependency
|
27
|
-
s.add_development_dependency
|
25
|
+
s.add_development_dependency 'bundler', '~> 1.0'
|
26
|
+
s.add_development_dependency 'rspec', '~> 2.6'
|
27
|
+
s.add_development_dependency 'rack-test', '~> 0.6'
|
28
|
+
s.add_development_dependency 'guard-rspec', '~> 0.4'
|
28
29
|
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:
|
4
|
+
hash: 17
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 2
|
9
|
-
-
|
10
|
-
version: 0.2.
|
9
|
+
- 3
|
10
|
+
version: 0.2.3
|
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-06-
|
18
|
+
date: 2011-06-26 00:00:00 +01:00
|
19
19
|
default_executable:
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|
@@ -66,9 +66,24 @@ dependencies:
|
|
66
66
|
type: :runtime
|
67
67
|
version_requirements: *id003
|
68
68
|
- !ruby/object:Gem::Dependency
|
69
|
-
name:
|
69
|
+
name: bundler
|
70
70
|
prerelease: false
|
71
71
|
requirement: &id004 !ruby/object:Gem::Requirement
|
72
|
+
none: false
|
73
|
+
requirements:
|
74
|
+
- - ~>
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
hash: 15
|
77
|
+
segments:
|
78
|
+
- 1
|
79
|
+
- 0
|
80
|
+
version: "1.0"
|
81
|
+
type: :development
|
82
|
+
version_requirements: *id004
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rspec
|
85
|
+
prerelease: false
|
86
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
72
87
|
none: false
|
73
88
|
requirements:
|
74
89
|
- - ~>
|
@@ -79,11 +94,11 @@ dependencies:
|
|
79
94
|
- 6
|
80
95
|
version: "2.6"
|
81
96
|
type: :development
|
82
|
-
version_requirements: *
|
97
|
+
version_requirements: *id005
|
83
98
|
- !ruby/object:Gem::Dependency
|
84
99
|
name: rack-test
|
85
100
|
prerelease: false
|
86
|
-
requirement: &
|
101
|
+
requirement: &id006 !ruby/object:Gem::Requirement
|
87
102
|
none: false
|
88
103
|
requirements:
|
89
104
|
- - ~>
|
@@ -94,7 +109,22 @@ dependencies:
|
|
94
109
|
- 6
|
95
110
|
version: "0.6"
|
96
111
|
type: :development
|
97
|
-
version_requirements: *
|
112
|
+
version_requirements: *id006
|
113
|
+
- !ruby/object:Gem::Dependency
|
114
|
+
name: guard-rspec
|
115
|
+
prerelease: false
|
116
|
+
requirement: &id007 !ruby/object:Gem::Requirement
|
117
|
+
none: false
|
118
|
+
requirements:
|
119
|
+
- - ~>
|
120
|
+
- !ruby/object:Gem::Version
|
121
|
+
hash: 3
|
122
|
+
segments:
|
123
|
+
- 0
|
124
|
+
- 4
|
125
|
+
version: "0.4"
|
126
|
+
type: :development
|
127
|
+
version_requirements: *id007
|
98
128
|
description:
|
99
129
|
email:
|
100
130
|
- andrewnez@gmail.com
|
@@ -108,6 +138,7 @@ files:
|
|
108
138
|
- .gitignore
|
109
139
|
- CHANGELOG.mdown
|
110
140
|
- Gemfile
|
141
|
+
- Guardfile
|
111
142
|
- LICENSE
|
112
143
|
- README.mdown
|
113
144
|
- Rakefile
|
@@ -118,6 +149,7 @@ files:
|
|
118
149
|
- lib/split/dashboard/public/dashboard.js
|
119
150
|
- lib/split/dashboard/public/reset.css
|
120
151
|
- lib/split/dashboard/public/style.css
|
152
|
+
- lib/split/dashboard/views/_experiment.erb
|
121
153
|
- lib/split/dashboard/views/index.erb
|
122
154
|
- lib/split/dashboard/views/layout.erb
|
123
155
|
- lib/split/experiment.rb
|
@@ -131,7 +163,7 @@ files:
|
|
131
163
|
- spec/spec_helper.rb
|
132
164
|
- split.gemspec
|
133
165
|
has_rdoc: true
|
134
|
-
homepage:
|
166
|
+
homepage: https://github.com/andrew/split
|
135
167
|
licenses: []
|
136
168
|
|
137
169
|
post_install_message:
|