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.
@@ -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:
@@ -0,0 +1,5 @@
1
+ guard 'rspec', :version => 2 do
2
+ watch(%r{^spec/.+_spec\.rb$})
3
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
4
+ watch('spec/spec_helper.rb') { "spec" }
5
+ end
@@ -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
@@ -1,6 +1,5 @@
1
- require 'bundler'
2
- Bundler::GemHelper.install_tasks
3
-
1
+ #!/usr/bin/env rake
2
+ require 'bundler/gem_tasks'
4
3
  require 'rspec/core/rake_task'
5
4
 
6
5
  RSpec::Core::RakeTask.new('spec')
@@ -1,4 +1,3 @@
1
- require 'rubygems'
2
1
  require 'split/experiment'
3
2
  require 'split/alternative'
4
3
  require 'split/helper'
@@ -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 { background:#efefef; font-family:Arial, Verdana, sans-serif; font-size:13px; }
2
- body { padding:0; margin:0; }
3
-
4
- .header { background:#000; padding:8px 5% 0 5%; border-bottom:1px solid #444;border-bottom:5px solid #0080FF;}
5
- .header h1 { color:#333; font-size:90%; font-weight:bold; margin-bottom:6px;}
6
- .header ul li { display:inline;}
7
- .header ul li a { color:#fff; text-decoration:none; margin-right:10px; display:inline-block; padding:8px; -webkit-border-top-right-radius:6px; -webkit-border-top-left-radius:6px; -moz-border-radius-topleft:6px; -moz-border-radius-topright:6px; }
8
- .header ul li a:hover { background:#333;}
9
- .header ul li.current a { background:#0080FF; font-weight:bold; color:#fff;}
10
-
11
- #main { padding:10px 5%; background:#fff; overflow:hidden; }
12
- #main .logo { float:right; margin:10px;}
13
- #main span.hl { background:#efefef; padding:2px;}
14
- #main h1 { margin:10px 0; font-size:190%; font-weight:bold; color:#0080FF;}
15
- #main h2 { margin:10px 0; font-size:130%;}
16
- #main table { width:100%; margin:10px 0;}
17
- #main table tr td, #main table tr th { border:1px solid #ccc; padding:6px;}
18
- #main table tr th { background:#efefef; color:#888; font-size:80%; font-weight:bold;}
19
- #main table tr td.no-data { text-align:center; padding:40px 0; color:#999; font-style:italic; font-size:130%;}
20
- #main a { color:#111;}
21
- #main p { margin:5px 0;}
22
- #main p.intro { margin-bottom:15px; font-size:85%; color:#999; margin-top:0; line-height:1.3;}
23
- #main h1.wi { margin-bottom:5px;}
24
- #main p.sub { font-size:95%; color:#999;}
25
-
26
- #main table.queues { width:60%;}
27
-
28
- #main table .totals td{ background:#eee; font-weight:bold; }
29
-
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
- #footer p a { color:#999;}
32
-
33
- h2{
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
- .reset{
38
- display:inline-block;
39
- font-size:10px;
40
- line-height:38px;
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
- .reset input{
44
- margin-left:10px;
151
+ .experiment table .totals td {
152
+ background: #eee;
153
+ font-weight: bold;
45
154
  }
46
155
 
47
- .queues{
48
- clear:both;
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
- .worse, .better{
52
- color:#F00;
53
- font-size:10px;
166
+ #footer p a {
167
+ color: #999;
54
168
  }
55
169
 
56
- .better{
57
- color:#00D500;
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
- <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">
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>
@@ -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(name, *alternatives)
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))
@@ -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
- return experiment.winner.name if experiment.winner
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
- if forced_alternative = override(experiment_name, alternatives)
8
- return forced_alternative
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
- ab_user[experiment_name] = experiment.control.name if exclude_visitor?
12
-
13
- if ab_user[experiment_name]
14
- return ab_user[experiment_name]
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
- alternative = experiment.next_alternative
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
- alternative_name = ab_user[experiment_name]
26
- alternative = Split::Alternative.find(alternative_name, experiment_name)
27
- alternative.increment_completion
28
- session[:split].delete(experiment_name)
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
@@ -1,3 +1,3 @@
1
1
  module Split
2
- VERSION = "0.2.2"
2
+ VERSION = "0.2.3"
3
3
  end
@@ -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
@@ -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
@@ -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
@@ -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(%q<redis>, ["~> 2.1"])
22
- s.add_dependency(%q<redis-namespace>, ["~> 1.0.3"])
23
- s.add_dependency(%q<sinatra>, ["~> 1.2.6"])
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
- # Development Dependencies
26
- s.add_development_dependency(%q<rspec>, ["~> 2.6"])
27
- s.add_development_dependency(%q<rack-test>, ["~> 0.6"])
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: 19
4
+ hash: 17
5
5
  prerelease: false
6
6
  segments:
7
7
  - 0
8
8
  - 2
9
- - 2
10
- version: 0.2.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-12 00:00:00 +01:00
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: rspec
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: *id004
97
+ version_requirements: *id005
83
98
  - !ruby/object:Gem::Dependency
84
99
  name: rack-test
85
100
  prerelease: false
86
- requirement: &id005 !ruby/object:Gem::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: *id005
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: