split 0.1.1 → 0.2.1

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