split 0.2.2 → 0.2.3
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 +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:
|