split 0.1.1 → 0.2.1

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 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