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 +31 -0
 - data/LICENSE +20 -0
 - data/README.mdown +12 -5
 - data/lib/split/alternative.rb +19 -7
 - data/lib/split/dashboard/public/dashboard.js +7 -0
 - data/lib/split/dashboard/public/style.css +17 -1
 - data/lib/split/dashboard/views/index.erb +56 -48
 - data/lib/split/dashboard/views/layout.erb +1 -1
 - data/lib/split/dashboard.rb +6 -0
 - data/lib/split/experiment.rb +34 -7
 - data/lib/split/helper.rb +15 -0
 - data/lib/split/version.rb +1 -1
 - data/spec/alternative_spec.rb +95 -0
 - data/spec/experiment_spec.rb +41 -3
 - data/spec/helper_spec.rb +52 -2
 - data/spec/spec_helper.rb +11 -0
 - metadata +11 -6
 
    
        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.
         
     | 
    
        data/lib/split/alternative.rb
    CHANGED
    
    | 
         @@ -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 
     | 
    
         
            -
                   
     | 
| 
       37 
     | 
    
         
            -
             
     | 
| 
      
 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)
         
     | 
| 
         @@ -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: 
     | 
| 
      
 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 
     | 
    
         
            -
             
     | 
| 
      
 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 
     | 
    
         
            -
             
     | 
| 
       6 
     | 
    
         
            -
             
     | 
| 
       7 
     | 
    
         
            -
             
     | 
| 
       8 
     | 
    
         
            -
             
     | 
| 
       9 
     | 
    
         
            -
             
     | 
| 
       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 
     | 
    
         
            -
                    < 
     | 
| 
       21 
     | 
    
         
            -
                    < 
     | 
| 
       22 
     | 
    
         
            -
                    < 
     | 
| 
       23 
     | 
    
         
            -
                    < 
     | 
| 
       24 
     | 
    
         
            -
                    < 
     | 
| 
       25 
     | 
    
         
            -
                    < 
     | 
| 
       26 
     | 
    
         
            -
                    < 
     | 
| 
       27 
     | 
    
         
            -
             
     | 
| 
       28 
     | 
    
         
            -
             
     | 
| 
       29 
     | 
    
         
            -
             
     | 
| 
      
 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 
     | 
    
         
            -
                           
     | 
| 
      
 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 
     | 
    
         
            -
                       
     | 
| 
       34 
     | 
    
         
            -
             
     | 
| 
       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 
     | 
    
         
            -
             
     | 
| 
       43 
     | 
    
         
            -
             
     | 
| 
       44 
     | 
    
         
            -
             
     | 
| 
      
 46 
     | 
    
         
            +
                    <% total_participants += alternative.participant_count %>
         
     | 
| 
      
 47 
     | 
    
         
            +
                    <% total_completed += alternative.completed_count %>
         
     | 
| 
      
 48 
     | 
    
         
            +
                  <% end %>
         
     | 
| 
       45 
49 
     | 
    
         | 
| 
       46 
     | 
    
         
            -
             
     | 
| 
       47 
     | 
    
         
            -
             
     | 
| 
       48 
     | 
    
         
            -
             
     | 
| 
       49 
     | 
    
         
            -
             
     | 
| 
       50 
     | 
    
         
            -
             
     | 
| 
       51 
     | 
    
         
            -
             
     | 
| 
       52 
     | 
    
         
            -
             
     | 
| 
       53 
     | 
    
         
            -
             
     | 
| 
       54 
     | 
    
         
            -
             
     | 
| 
       55 
     | 
    
         
            -
             
     | 
| 
      
 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>
         
     | 
    
        data/lib/split/dashboard.rb
    CHANGED
    
    
    
        data/lib/split/experiment.rb
    CHANGED
    
    | 
         @@ -1,12 +1,12 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            module Split
         
     | 
| 
       2 
2 
     | 
    
         
             
              class Experiment
         
     | 
| 
       3 
3 
     | 
    
         
             
                attr_accessor :name
         
     | 
| 
       4 
     | 
    
         
            -
                attr_accessor : 
     | 
| 
      
 4 
     | 
    
         
            +
                attr_accessor :alternative_names
         
     | 
| 
       5 
5 
     | 
    
         
             
                attr_accessor :winner
         
     | 
| 
       6 
6 
     | 
    
         | 
| 
       7 
     | 
    
         
            -
                def initialize(name, * 
     | 
| 
      
 7 
     | 
    
         
            +
                def initialize(name, *alternative_names)
         
     | 
| 
       8 
8 
     | 
    
         
             
                  @name = name.to_s
         
     | 
| 
       9 
     | 
    
         
            -
                  @ 
     | 
| 
      
 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 
     | 
    
         
            -
                  @ 
     | 
| 
      
 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 
     | 
    
         
            -
                  @ 
     | 
| 
      
 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, * 
     | 
| 
      
 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, * 
     | 
| 
      
 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
    
    
| 
         @@ -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
         
     | 
    
        data/spec/experiment_spec.rb
    CHANGED
    
    | 
         @@ -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  
     | 
| 
      
 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  
     | 
| 
      
 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  
     | 
| 
      
 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  
     | 
| 
      
 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 
     | 
    
         
            -
             
     | 
| 
      
 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:  
     | 
| 
       5 
     | 
    
         
            -
              prerelease: 
         
     | 
| 
      
 4 
     | 
    
         
            +
              hash: 21
         
     | 
| 
      
 5 
     | 
    
         
            +
              prerelease: false
         
     | 
| 
       6 
6 
     | 
    
         
             
              segments: 
         
     | 
| 
       7 
7 
     | 
    
         
             
              - 0
         
     | 
| 
      
 8 
     | 
    
         
            +
              - 2
         
     | 
| 
       8 
9 
     | 
    
         
             
              - 1
         
     | 
| 
       9 
     | 
    
         
            -
               
     | 
| 
       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 
     | 
    
         
            +
            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. 
     | 
| 
      
 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
         
     |