split 0.2.2 → 0.2.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: