split 0.2.4 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,16 @@
1
+ ## 0.3.0 (October 9, 2011)
2
+
3
+ Features:
4
+
5
+ - Redesigned dashboard (@mrappleton, #17)
6
+ - Use atomic increments in redis for better concurrency (@lautis, #18)
7
+ - Weighted alternatives
8
+
9
+ Bugfixes:
10
+
11
+ - Fix to allow overriding of experiments that aren't on version 1
12
+
13
+
1
14
  ## 0.2.4 (July 18, 2011)
2
15
 
3
16
  Features:
@@ -80,15 +80,15 @@ Example: View
80
80
  Example: Controller
81
81
 
82
82
  def register_new_user
83
- #See what level of free points maximizes users' decision to buy replacement points.
83
+ # See what level of free points maximizes users' decision to buy replacement points.
84
84
  @starter_points = ab_test("new_user_free_points", 100, 200, 300)
85
85
  end
86
86
 
87
87
  Example: Conversion tracking (in a controller!)
88
88
 
89
89
  def buy_new_points
90
- #some business logic
91
- finished("buy_new_points") #Either a conversion named with :conversion or a test name.
90
+ # some business logic
91
+ finished("buy_new_points")
92
92
  end
93
93
 
94
94
  Example: Conversion tracking (in a view)
@@ -97,6 +97,22 @@ Example: Conversion tracking (in a view)
97
97
 
98
98
  You can find more examples, tutorials and guides on the [wiki](https://github.com/andrew/split/wiki).
99
99
 
100
+ ## Extras
101
+
102
+ ### Weighted alternatives
103
+
104
+ Perhaps you only want to show an alternative to 10% of your visitors because it is very experimental or not yet fully load tested.
105
+
106
+ To do this you can pass a weight with each alternative in the following ways:
107
+
108
+ ab_test('homepage design', 'Old' => 20, 'New' => 2)
109
+
110
+ ab_test('homepage design', 'Old', {'New' => 0.1})
111
+
112
+ ab_test('homepage design', {'Old' => 10}, 'New')
113
+
114
+ This will only show the new alternative to visitors 1 in 10 times, the default weight for an alternative is 1.
115
+
100
116
  ### Overriding alternatives
101
117
 
102
118
  For development and testing, you may wish to force your app to always return an alternative.
@@ -108,11 +124,21 @@ If you have an experiment called `button_color` with alternatives called `red` a
108
124
 
109
125
  will always have red buttons. This won't be stored in your session or count towards to results.
110
126
 
127
+ ### Reset after completion
128
+
129
+ When a user completes a test their session is reset so that they may start the test again in the future.
130
+
131
+ To stop this behaviour you can pass the following option to the `finished` method:
132
+
133
+ finished('experiment_name', :reset => false)
134
+
135
+ The user will then always see the alternative they started with.
136
+
111
137
  ## Web Interface
112
138
 
113
139
  Split comes with a Sinatra-based front end to get an overview of how your experiments are doing.
114
140
 
115
- You can mount this inside your app using Rack::URLMap in your `config.ru`
141
+ If you are running Rails 2: You can mount this inside your app using Rack::URLMap in your `config.ru`
116
142
 
117
143
  require 'split/dashboard'
118
144
 
@@ -120,6 +146,14 @@ You can mount this inside your app using Rack::URLMap in your `config.ru`
120
146
  "/" => Your::App.new,
121
147
  "/split" => Split::Dashboard.new
122
148
 
149
+ However, if you are using Rails 3: You can mount this inside your app routes by first adding this to the Gemfile:
150
+
151
+ gem 'split', :require => 'split/dashboard'
152
+
153
+ Then adding this to config/routes.rb
154
+
155
+ mount Split::Dashboard, :at => 'split'
156
+
123
157
  You may want to password protect that page, you can do so with `Rack::Auth::Basic`
124
158
 
125
159
  Split::Dashboard.use Rack::Auth::Basic do |username, password|
@@ -196,6 +230,7 @@ Special thanks to the following people for submitting patches:
196
230
 
197
231
  * Lloyd Pick
198
232
  * Jeffery Chupp
233
+ * Andrew Appleton
199
234
 
200
235
  ## Development
201
236
 
@@ -4,12 +4,19 @@ module Split
4
4
  attr_accessor :participant_count
5
5
  attr_accessor :completed_count
6
6
  attr_accessor :experiment_name
7
+ attr_accessor :weight
7
8
 
8
9
  def initialize(name, experiment_name, counters = {})
9
10
  @experiment_name = experiment_name
10
- @name = name
11
11
  @participant_count = counters['participant_count'].to_i
12
12
  @completed_count = counters['completed_count'].to_i
13
+ if name.class == Hash
14
+ @name = name.keys.first
15
+ @weight = name.values.first
16
+ else
17
+ @name = name
18
+ @weight = 1
19
+ end
13
20
  end
14
21
 
15
22
  def to_s
@@ -18,12 +25,12 @@ module Split
18
25
 
19
26
  def increment_participation
20
27
  @participant_count +=1
21
- self.save
28
+ Split.redis.hincrby "#{experiment_name}:#{name}", 'participant_count', 1
22
29
  end
23
30
 
24
31
  def increment_completion
25
32
  @completed_count +=1
26
- self.save
33
+ Split.redis.hincrby "#{experiment_name}:#{name}", 'completed_count', 1
27
34
  end
28
35
 
29
36
  def control?
@@ -39,7 +46,7 @@ module Split
39
46
  Split::Experiment.find(experiment_name)
40
47
  end
41
48
 
42
- def z_score
49
+ def z_score
43
50
  # CTR_E = the CTR within the experiment split
44
51
  # CTR_C = the CTR within the control split
45
52
  # E = the number of impressions within the experiment split
@@ -97,5 +104,17 @@ module Split
97
104
  alt.save
98
105
  alt
99
106
  end
107
+
108
+ def self.valid?(name)
109
+ string?(name) or hash_with_correct_values?(name)
110
+ end
111
+
112
+ def self.string?(name)
113
+ name.class == String
114
+ end
115
+
116
+ def self.hash_with_correct_values?(name)
117
+ name.class == Hash && name.keys.first.class == String && Float(name.values.first) rescue false
118
+ end
100
119
  end
101
120
  end
@@ -73,7 +73,7 @@ module Split
73
73
  @experiment.reset
74
74
  redirect url('/')
75
75
  end
76
-
76
+
77
77
  delete '/:experiment' do
78
78
  @experiment = Split::Experiment.find(params[:experiment])
79
79
  @experiment.delete
@@ -7,7 +7,7 @@ function confirmReset() {
7
7
  }
8
8
 
9
9
  function confirmDelete() {
10
- var agree=confirm("Are you sure you want to delete this experiment and all it's data?");
10
+ var agree=confirm("Are you sure you want to delete this experiment and all its data?");
11
11
  if (agree)
12
12
  return true;
13
13
  else
@@ -6,43 +6,43 @@ small, strike, strong, sub, sup, tt, var,
6
6
  dl, dt, dd, ul, li,
7
7
  form, label, legend,
8
8
  table, caption, tbody, tfoot, thead, tr, th, td {
9
- margin: 0;
10
- padding: 0;
11
- border: 0;
12
- outline: 0;
13
- font-weight: inherit;
14
- font-style: normal;
15
- font-size: 100%;
16
- font-family: inherit;
9
+ margin: 0;
10
+ padding: 0;
11
+ border: 0;
12
+ outline: 0;
13
+ font-weight: inherit;
14
+ font-style: normal;
15
+ font-size: 100%;
16
+ font-family: inherit;
17
17
  }
18
18
 
19
19
  :focus {
20
- outline: 0;
20
+ outline: 0;
21
21
  }
22
22
 
23
23
  body {
24
- line-height: 1;
24
+ line-height: 1;
25
25
  }
26
26
 
27
27
  ul {
28
- list-style: none;
28
+ list-style: none;
29
29
  }
30
30
 
31
31
  table {
32
- border-collapse: collapse;
33
- border-spacing: 0;
32
+ border-collapse: collapse;
33
+ border-spacing: 0;
34
34
  }
35
35
 
36
36
  caption, th, td {
37
- text-align: left;
38
- font-weight: normal;
37
+ text-align: left;
38
+ font-weight: normal;
39
39
  }
40
40
 
41
41
  blockquote:before, blockquote:after,
42
42
  q:before, q:after {
43
- content: "";
43
+ content: "";
44
44
  }
45
45
 
46
46
  blockquote, q {
47
- quotes: "" "";
47
+ quotes: "" "";
48
48
  }
@@ -1,26 +1,44 @@
1
1
  html {
2
2
  background: #efefef;
3
- font-family: Arial, Verdana, sans-serif;
3
+ font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
4
4
  font-size: 13px;
5
5
  }
6
6
 
7
7
  body {
8
- padding: 0;
9
- margin: 0;
8
+ padding: 0 10px;
9
+ margin: 10px auto 0;
10
+ max-width:800px;
10
11
  }
11
12
 
12
13
  .header {
13
- background: #000;
14
- padding: 8px 5% 0 5%;
15
- border-bottom: 1px solid #444;
16
- border-bottom: 5px solid #0080FF;
14
+ background: #ededed;
15
+ background: -webkit-gradient(linear, left top, left bottom,
16
+ color-stop(0%,#576a76),
17
+ color-stop(100%,#4d5256));
18
+ background: -moz-linear-gradient (top, #576a76 0%, #414e58 100%);
19
+ background: -webkit-linear-gradient(top, #576a76 0%, #414e58 100%);
20
+ background: -o-linear-gradient (top, #576a76 0%, #414e58 100%);
21
+ background: -ms-linear-gradient (top, #576a76 0%, #414e58 100%);
22
+ background: linear-gradient (top, #576a76 0%, #414e58 100%);
23
+ border-bottom: 1px solid #fff;
24
+ -moz-border-radius-topleft: 5px;
25
+ -webkit-border-top-left-radius: 5px;
26
+ border-top-left-radius: 5px;
27
+ -moz-border-radius-topright: 5px;
28
+ -webkit-border-top-right-radius:5px;
29
+ border-top-right-radius: 5px;
30
+
31
+ overflow:hidden;
32
+ padding: 10px 5%;
33
+ text-shadow:0 1px 0 #000;
17
34
  }
18
35
 
19
36
  .header h1 {
20
- color: #333;
21
- font-size: 90%;
22
- font-weight: bold;
23
- margin-bottom: 6px;
37
+ color: #eee;
38
+ float:left;
39
+ font-size:1.2em;
40
+ font-weight:normal;
41
+ margin:2px 30px 0 0;
24
42
  }
25
43
 
26
44
  .header ul li {
@@ -28,30 +46,43 @@ body {
28
46
  }
29
47
 
30
48
  .header ul li a {
31
- color: #fff;
49
+ color: #eee;
32
50
  text-decoration: none;
33
51
  margin-right: 10px;
34
52
  display: inline-block;
35
- padding: 8px;
36
- -webkit-border-top-right-radius: 6px;
37
- -webkit-border-top-left-radius: 6px;
38
- -moz-border-radius-topleft: 6px;
39
- -moz-border-radius-topright: 6px;
53
+ padding: 4px 8px;
54
+ -moz-border-radius: 10px;
55
+ -webkit-border-radius:10px;
56
+ border-radius: 10px;
57
+
40
58
  }
41
59
 
42
60
  .header ul li a:hover {
43
- background: #333;
61
+ background: rgba(255,255,255,0.1);
62
+ }
63
+
64
+ .header ul li a:active {
65
+ -moz-box-shadow: inset 0 1px 0 rgba(0,0,0,0.2);
66
+ -webkit-box-shadow:inset 0 1px 0 rgba(0,0,0,0.2);
67
+ box-shadow: inset 0 1px 0 rgba(0,0,0,0.2);
44
68
  }
45
69
 
46
70
  .header ul li.current a {
47
- background: #0080FF;
48
- font-weight: bold;
71
+ background: rgba(255,255,255,0.1);
72
+ -moz-box-shadow: inset 0 1px 0 rgba(0,0,0,0.2);
73
+ -webkit-box-shadow:inset 0 1px 0 rgba(0,0,0,0.2);
74
+ box-shadow: inset 0 1px 0 rgba(0,0,0,0.2);
49
75
  color: #fff;
50
76
  }
51
77
 
52
78
  #main {
53
79
  padding: 10px 5%;
54
- background: #fff;
80
+ background: #f9f9f9;
81
+ border:1px solid #ccc;
82
+ border-top:none;
83
+ -moz-box-shadow: 0 3px 10px rgba(0,0,0,0.2);
84
+ -webkit-box-shadow:0 3px 10px rgba(0,0,0,0.2);
85
+ box-shadow: 0 3px 10px rgba(0,0,0,0.2);
55
86
  overflow: hidden;
56
87
  }
57
88
 
@@ -74,11 +105,11 @@ body {
74
105
 
75
106
  #main table {
76
107
  width: 100%;
77
- margin: 10px 0;
108
+ margin:0 0 10px;
78
109
  }
79
110
 
80
111
  #main table tr td, #main table tr th {
81
- border: 1px solid #ccc;
112
+ border-bottom: 1px solid #ccc;
82
113
  padding: 6px;
83
114
  }
84
115
 
@@ -86,7 +117,7 @@ body {
86
117
  background: #efefef;
87
118
  color: #888;
88
119
  font-size: 80%;
89
- font-weight: bold;
120
+ text-transform:uppercase;
90
121
  }
91
122
 
92
123
  #main table tr td.no-data {
@@ -123,16 +154,34 @@ body {
123
154
  }
124
155
 
125
156
  .experiment {
126
- width: 60%;
127
- border-top:1px solid #eee;
128
- margin:15px 0;
157
+ background:#fff;
158
+ border: 1px solid #eee;
159
+ border-bottom:none;
160
+ margin:30px 0;
161
+ }
162
+
163
+ .experiment .experiment-header {
164
+ background: #f4f4f4;
165
+ background: -webkit-gradient(linear, left top, left bottom,
166
+ color-stop(0%,#f4f4f4),
167
+ color-stop(100%,#e0e0e0));
168
+ background: -moz-linear-gradient (top, #f4f4f4 0%, #e0e0e0 100%);
169
+ background: -webkit-linear-gradient(top, #f4f4f4 0%, #e0e0e0 100%);
170
+ background: -o-linear-gradient (top, #f4f4f4 0%, #e0e0e0 100%);
171
+ background: -ms-linear-gradient (top, #f4f4f4 0%, #e0e0e0 100%);
172
+ background: linear-gradient (top, #f4f4f4 0%, #e0e0e0 100%);
173
+ border-top:1px solid #fff;
174
+ overflow:hidden;
175
+ padding:0 10px;
129
176
  }
130
177
 
131
178
  .experiment h2 {
132
- margin: 10px 0;
133
- font-size: 130%;
179
+ color:#888;
180
+ margin: 12px 0 0;
181
+ font-size: 1em;
134
182
  font-weight:bold;
135
183
  float:left;
184
+ text-shadow:0 1px 0 rgba(255,255,255,0.8);
136
185
  }
137
186
 
138
187
  .experiment h2 .version{
@@ -155,11 +204,9 @@ body {
155
204
 
156
205
  #footer {
157
206
  padding: 10px 5%;
158
- background: #efefef;
159
207
  color: #999;
160
208
  font-size: 85%;
161
209
  line-height: 1.5;
162
- border-top: 5px solid #ccc;
163
210
  padding-top: 10px;
164
211
  }
165
212
 
@@ -182,10 +229,67 @@ body {
182
229
  }
183
230
 
184
231
  .worse, .better {
185
- color: #F00;
232
+ color: #773F3F;
186
233
  font-size: 10px;
234
+ font-weight:bold;
187
235
  }
188
236
 
189
237
  .better {
190
- color: #00D500;
238
+ color: #408C48;
191
239
  }
240
+
241
+ a.button, button, input[type="submit"] {
242
+ padding: 4px 10px;
243
+ overflow: hidden;
244
+ background: #d8dae0;
245
+ -moz-box-shadow: 0 1px 0 rgba(0,0,0,0.5);
246
+ -webkit-box-shadow:0 1px 0 rgba(0,0,0,0.5);
247
+ box-shadow: 0 1px 0 rgba(0,0,0,0.5);
248
+ border:none;
249
+ -moz-border-radius: 30px;
250
+ -webkit-border-radius:30px;
251
+ border-radius: 30px;
252
+ color:#2e3035;
253
+ cursor: pointer;
254
+ font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
255
+ text-decoration: none;
256
+ text-shadow:0 1px 0 rgba(255,255,255,0.8);
257
+ -moz-user-select: none;
258
+ -webkit-user-select:none;
259
+ user-select: none;
260
+ white-space: nowrap;
261
+ }
262
+ a.button:hover, button:hover, input[type="submit"]:hover,
263
+ a.button:focus, button:focus, input[type="submit"]:focus{
264
+ background:#bbbfc7;
265
+ }
266
+ a.button:active, button:active, input[type="submit"]:active{
267
+ -moz-box-shadow: inset 0 0 4px #484d57;
268
+ -webkit-box-shadow:inset 0 0 4px #484d57;
269
+ box-shadow: inset 0 0 4px #484d57;
270
+ position:relative;
271
+ top:1px;
272
+ }
273
+
274
+ a.button.red, button.red, input[type="submit"].red,
275
+ a.button.green, button.green, input[type="submit"].green {
276
+ color:#fff;
277
+ text-shadow:0 1px 0 rgba(0,0,0,0.4);
278
+ }
279
+
280
+ a.button.red, button.red, input[type="submit"].red {
281
+ background:#a56d6d;
282
+ }
283
+ a.button.red:hover, button.red:hover, input[type="submit"].red:hover,
284
+ a.button.red:focus, button.red:focus, input[type="submit"].red:focus {
285
+ background:#895C5C;
286
+ }
287
+ a.button.green, button.green, input[type="submit"].green {
288
+ background:#8daa92;
289
+ }
290
+ a.button.green:hover, button.green:hover, input[type="submit"].green:hover,
291
+ a.button.green:focus, button.green:focus, input[type="submit"].green:focus {
292
+ background:#768E7A;
293
+ }
294
+
295
+
@@ -1,13 +1,15 @@
1
1
  <div class="experiment">
2
- <h2>Experiment: <%= experiment.name %> <% if experiment.version > 1 %><span class='version'>v<%= experiment.version %></span><% end %></h2>
3
- <div class='inline-controls'>
4
- <form action="<%= url "/reset/#{experiment.name}" %>" method='post' onclick="return confirmReset()">
5
- <input type="submit" value="Reset Data">
6
- </form>
7
- <form action="<%= url "/#{experiment.name}" %>" method='post' onclick="return confirmDelete()">
8
- <input type="hidden" name="_method" value="delete"/>
9
- <input type="submit" value="Delete">
10
- </form>
2
+ <div class="experiment-header">
3
+ <h2>Experiment: <%= experiment.name %> <% if experiment.version > 1 %><span class='version'>v<%= experiment.version %></span><% end %></h2>
4
+ <div class='inline-controls'>
5
+ <form action="<%= url "/reset/#{experiment.name}" %>" method='post' onclick="return confirmReset()">
6
+ <input type="submit" value="Reset Data">
7
+ </form>
8
+ <form action="<%= url "/#{experiment.name}" %>" method='post' onclick="return confirmDelete()">
9
+ <input type="hidden" name="_method" value="delete"/>
10
+ <input type="submit" value="Delete" class="red">
11
+ </form>
12
+ </div>
11
13
  </div>
12
14
  <table>
13
15
  <tr>
@@ -59,7 +61,7 @@
59
61
  <% else %>
60
62
  <form action="<%= url experiment.name %>" method='post' onclick="return confirmWinner()">
61
63
  <input type='hidden' name='alternative' value='<%= alternative.name %>'>
62
- <input type="submit" value="Use this">
64
+ <input type="submit" value="Use this" class="green">
63
65
  </form>
64
66
  <% end %>
65
67
  </td>
@@ -1,4 +1,3 @@
1
- <h1>Split Dashboard</h1>
2
1
  <% if @experiments.any? %>
3
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>
4
3
 
@@ -9,7 +9,9 @@
9
9
 
10
10
  </head>
11
11
  <body>
12
- <div class="header"></div>
12
+ <div class="header">
13
+ <h1>Split Dashboard</h1>
14
+ </div>
13
15
 
14
16
  <div id="main">
15
17
  <%= yield %>
@@ -7,7 +7,10 @@ module Split
7
7
 
8
8
  def initialize(name, *alternative_names)
9
9
  @name = name.to_s
10
- @alternative_names = alternative_names
10
+ @alternative_names = alternative_names.map do |alternative|
11
+ Split::Alternative.new(alternative, name)
12
+ end.map(&:name)
13
+
11
14
  @version = (Split.redis.get("#{name.to_s}:version").to_i || 0)
12
15
  end
13
16
 
@@ -36,7 +39,19 @@ module Split
36
39
  end
37
40
 
38
41
  def next_alternative
39
- winner || alternatives.sort_by{|a| a.participant_count + rand}.first
42
+ winner || random_alternative
43
+ end
44
+
45
+ def random_alternative
46
+ weights = alternatives.map(&:weight)
47
+
48
+ total = weights.inject(0.0) {|t,w| t+w}
49
+ point = rand * total
50
+
51
+ alternatives.zip(weights).each do |n,w|
52
+ return n if w >= point
53
+ point -= w
54
+ end
40
55
  end
41
56
 
42
57
  def version
@@ -61,7 +76,7 @@ module Split
61
76
  reset_winner
62
77
  increment_version
63
78
  end
64
-
79
+
65
80
  def delete
66
81
  alternatives.each(&:delete)
67
82
  reset_winner
@@ -108,10 +123,18 @@ module Split
108
123
  def self.find_or_create(key, *alternatives)
109
124
  name = key.to_s.split(':')[0]
110
125
 
111
- raise InvalidArgument, 'Alternatives must be strings' if alternatives.map(&:class).uniq != [String]
126
+ if alternatives.length == 1
127
+ if alternatives[0].is_a? Hash
128
+ alternatives = alternatives[0].map{|k,v| {k => v} }
129
+ else
130
+ raise InvalidArgument, 'You must declare at least 2 alternatives'
131
+ end
132
+ end
133
+
134
+ alts = initialize_alternatives(alternatives, name)
112
135
 
113
136
  if Split.redis.exists(name)
114
- if load_alternatives_for(name) == alternatives
137
+ if load_alternatives_for(name) == alts.map(&:name)
115
138
  experiment = self.new(name, *load_alternatives_for(name))
116
139
  else
117
140
  exp = self.new(name, *load_alternatives_for(name))
@@ -125,6 +148,18 @@ module Split
125
148
  experiment.save
126
149
  end
127
150
  return experiment
151
+
152
+ end
153
+
154
+ def self.initialize_alternatives(alternatives, name)
155
+
156
+ if alternatives.reject {|a| Split::Alternative.valid? a}.any?
157
+ raise InvalidArgument, 'Alternatives must be strings'
158
+ end
159
+
160
+ alternatives.map do |alternative|
161
+ Split::Alternative.new(alternative, name)
162
+ end
128
163
  end
129
164
  end
130
165
  end
@@ -5,7 +5,7 @@ module Split
5
5
  if experiment.winner
6
6
  ret = experiment.winner.name
7
7
  else
8
- if forced_alternative = override(experiment.key, alternatives)
8
+ if forced_alternative = override(experiment.name, alternatives)
9
9
  ret = forced_alternative
10
10
  else
11
11
  begin_experiment(experiment, experiment.control.name) if exclude_visitor?
@@ -1,3 +1,3 @@
1
1
  module Split
2
- VERSION = "0.2.4"
2
+ VERSION = "0.3.0"
3
3
  end
@@ -9,6 +9,12 @@ describe Split::Alternative do
9
9
  alternative = Split::Alternative.new('Basket', 'basket_text')
10
10
  alternative.name.should eql('Basket')
11
11
  end
12
+
13
+ it "return only the name" do
14
+ experiment = Split::Experiment.new('basket_text', {'Basket' => 0.6}, {"Cart" => 0.4})
15
+ alternative = Split::Alternative.new('Basket', 'basket_text')
16
+ alternative.name.should eql('Basket')
17
+ end
12
18
 
13
19
  it "should have a default participation count of 0" do
14
20
  alternative = Split::Alternative.new('Basket', 'basket_text')
@@ -40,6 +46,8 @@ describe Split::Alternative do
40
46
  old_participant_count = alternative.participant_count
41
47
  alternative.increment_participation
42
48
  alternative.participant_count.should eql(old_participant_count+1)
49
+
50
+ Split::Alternative.find('Basket', 'basket_text').participant_count.should eql(old_participant_count+1)
43
51
  end
44
52
 
45
53
  it "should increment completed count" do
@@ -49,6 +57,8 @@ describe Split::Alternative do
49
57
  old_completed_count = alternative.participant_count
50
58
  alternative.increment_completion
51
59
  alternative.completed_count.should eql(old_completed_count+1)
60
+
61
+ Split::Alternative.find('Basket', 'basket_text').completed_count.should eql(old_completed_count+1)
52
62
  end
53
63
 
54
64
  it "can be reset" do
@@ -37,7 +37,7 @@ describe Split::Experiment do
37
37
  Split.redis.exists('basket_text').should be false
38
38
  lambda { Split::Experiment.find('link_color') }.should raise_error
39
39
  end
40
-
40
+
41
41
  it "should increment the version" do
42
42
  experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red', 'green')
43
43
  experiment.version.should eql(0)
@@ -126,15 +126,6 @@ describe Split::Experiment do
126
126
  end
127
127
 
128
128
  describe 'next_alternative' do
129
- it "should return a random alternative from those with the least participants" do
130
- experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red', 'green')
131
-
132
- Split::Alternative.find('blue', 'link_color').increment_participation
133
- Split::Alternative.find('red', 'link_color').increment_participation
134
-
135
- experiment.next_alternative.name.should eql('green')
136
- end
137
-
138
129
  it "should always return the winner if one exists" do
139
130
  experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red', 'green')
140
131
  green = Split::Alternative.find('green', 'link_color')
@@ -57,6 +57,17 @@ describe Split::Helper do
57
57
  ret = ab_test('link_color', 'blue', 'red') { |alternative| "shared/#{alternative}" }
58
58
  ret.should eql("shared/#{alt}")
59
59
  end
60
+
61
+ it "should allow the share of visitors see an alternative to be specificed" do
62
+ ab_test('link_color', {'blue' => 0.8}, {'red' => 20})
63
+ ['red', 'blue'].should include(ab_user['link_color'])
64
+ end
65
+
66
+ it "should allow alternative weighting interface as a single hash" do
67
+ ab_test('link_color', 'blue' => 0.01, 'red' => 0.2)
68
+ experiment = Split::Experiment.find('link_color')
69
+ experiment.alternative_names.should eql(['blue', 'red'])
70
+ end
60
71
  end
61
72
 
62
73
  describe 'finished' do
@@ -95,17 +106,17 @@ describe Split::Helper do
95
106
  session[:split].should eql("link_color" => alternative_name)
96
107
  end
97
108
  end
98
-
109
+
99
110
  describe 'conversions' do
100
111
  it 'should return a conversion rate for an alternative' do
101
112
  experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red')
102
113
  alternative_name = ab_test('link_color', 'blue', 'red')
103
-
114
+
104
115
  previous_convertion_rate = Split::Alternative.find(alternative_name, 'link_color').conversion_rate
105
116
  previous_convertion_rate.should eql(0.0)
106
117
 
107
118
  finished('link_color')
108
-
119
+
109
120
  new_convertion_rate = Split::Alternative.find(alternative_name, 'link_color').conversion_rate
110
121
  new_convertion_rate.should eql(1.0)
111
122
  end
metadata CHANGED
@@ -1,140 +1,100 @@
1
- --- !ruby/object:Gem::Specification
1
+ --- !ruby/object:Gem::Specification
2
2
  name: split
3
- version: !ruby/object:Gem::Version
4
- hash: 31
5
- prerelease: false
6
- segments:
7
- - 0
8
- - 2
9
- - 4
10
- version: 0.2.4
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.3.0
5
+ prerelease:
11
6
  platform: ruby
12
- authors:
7
+ authors:
13
8
  - Andrew Nesbitt
14
9
  autorequire:
15
10
  bindir: bin
16
11
  cert_chain: []
17
-
18
- date: 2011-07-18 00:00:00 +01:00
19
- default_executable:
20
- dependencies:
21
- - !ruby/object:Gem::Dependency
12
+ date: 2011-10-09 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
22
15
  name: redis
23
- prerelease: false
24
- requirement: &id001 !ruby/object:Gem::Requirement
16
+ requirement: &70155561576500 !ruby/object:Gem::Requirement
25
17
  none: false
26
- requirements:
18
+ requirements:
27
19
  - - ~>
28
- - !ruby/object:Gem::Version
29
- hash: 1
30
- segments:
31
- - 2
32
- - 1
33
- version: "2.1"
20
+ - !ruby/object:Gem::Version
21
+ version: '2.1'
34
22
  type: :runtime
35
- version_requirements: *id001
36
- - !ruby/object:Gem::Dependency
37
- name: redis-namespace
38
23
  prerelease: false
39
- requirement: &id002 !ruby/object:Gem::Requirement
24
+ version_requirements: *70155561576500
25
+ - !ruby/object:Gem::Dependency
26
+ name: redis-namespace
27
+ requirement: &70155561575180 !ruby/object:Gem::Requirement
40
28
  none: false
41
- requirements:
29
+ requirements:
42
30
  - - ~>
43
- - !ruby/object:Gem::Version
44
- hash: 17
45
- segments:
46
- - 1
47
- - 0
48
- - 3
31
+ - !ruby/object:Gem::Version
49
32
  version: 1.0.3
50
33
  type: :runtime
51
- version_requirements: *id002
52
- - !ruby/object:Gem::Dependency
53
- name: sinatra
54
34
  prerelease: false
55
- requirement: &id003 !ruby/object:Gem::Requirement
35
+ version_requirements: *70155561575180
36
+ - !ruby/object:Gem::Dependency
37
+ name: sinatra
38
+ requirement: &70155561564480 !ruby/object:Gem::Requirement
56
39
  none: false
57
- requirements:
40
+ requirements:
58
41
  - - ~>
59
- - !ruby/object:Gem::Version
60
- hash: 19
61
- segments:
62
- - 1
63
- - 2
64
- - 6
42
+ - !ruby/object:Gem::Version
65
43
  version: 1.2.6
66
44
  type: :runtime
67
- version_requirements: *id003
68
- - !ruby/object:Gem::Dependency
69
- name: bundler
70
45
  prerelease: false
71
- requirement: &id004 !ruby/object:Gem::Requirement
46
+ version_requirements: *70155561564480
47
+ - !ruby/object:Gem::Dependency
48
+ name: bundler
49
+ requirement: &70155561563360 !ruby/object:Gem::Requirement
72
50
  none: false
73
- requirements:
51
+ requirements:
74
52
  - - ~>
75
- - !ruby/object:Gem::Version
76
- hash: 15
77
- segments:
78
- - 1
79
- - 0
80
- version: "1.0"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.0'
81
55
  type: :development
82
- version_requirements: *id004
83
- - !ruby/object:Gem::Dependency
84
- name: rspec
85
56
  prerelease: false
86
- requirement: &id005 !ruby/object:Gem::Requirement
57
+ version_requirements: *70155561563360
58
+ - !ruby/object:Gem::Dependency
59
+ name: rspec
60
+ requirement: &70155561562780 !ruby/object:Gem::Requirement
87
61
  none: false
88
- requirements:
62
+ requirements:
89
63
  - - ~>
90
- - !ruby/object:Gem::Version
91
- hash: 15
92
- segments:
93
- - 2
94
- - 6
95
- version: "2.6"
64
+ - !ruby/object:Gem::Version
65
+ version: '2.6'
96
66
  type: :development
97
- version_requirements: *id005
98
- - !ruby/object:Gem::Dependency
99
- name: rack-test
100
67
  prerelease: false
101
- requirement: &id006 !ruby/object:Gem::Requirement
68
+ version_requirements: *70155561562780
69
+ - !ruby/object:Gem::Dependency
70
+ name: rack-test
71
+ requirement: &70155561562200 !ruby/object:Gem::Requirement
102
72
  none: false
103
- requirements:
73
+ requirements:
104
74
  - - ~>
105
- - !ruby/object:Gem::Version
106
- hash: 7
107
- segments:
108
- - 0
109
- - 6
110
- version: "0.6"
75
+ - !ruby/object:Gem::Version
76
+ version: '0.6'
111
77
  type: :development
112
- version_requirements: *id006
113
- - !ruby/object:Gem::Dependency
114
- name: guard-rspec
115
78
  prerelease: false
116
- requirement: &id007 !ruby/object:Gem::Requirement
79
+ version_requirements: *70155561562200
80
+ - !ruby/object:Gem::Dependency
81
+ name: guard-rspec
82
+ requirement: &70155561561580 !ruby/object:Gem::Requirement
117
83
  none: false
118
- requirements:
84
+ requirements:
119
85
  - - ~>
120
- - !ruby/object:Gem::Version
121
- hash: 3
122
- segments:
123
- - 0
124
- - 4
125
- version: "0.4"
86
+ - !ruby/object:Gem::Version
87
+ version: '0.4'
126
88
  type: :development
127
- version_requirements: *id007
89
+ prerelease: false
90
+ version_requirements: *70155561561580
128
91
  description:
129
- email:
92
+ email:
130
93
  - andrewnez@gmail.com
131
94
  executables: []
132
-
133
95
  extensions: []
134
-
135
96
  extra_rdoc_files: []
136
-
137
- files:
97
+ files:
138
98
  - .gitignore
139
99
  - CHANGELOG.mdown
140
100
  - Gemfile
@@ -162,41 +122,31 @@ files:
162
122
  - spec/helper_spec.rb
163
123
  - spec/spec_helper.rb
164
124
  - split.gemspec
165
- has_rdoc: true
166
125
  homepage: https://github.com/andrew/split
167
126
  licenses: []
168
-
169
127
  post_install_message:
170
128
  rdoc_options: []
171
-
172
- require_paths:
129
+ require_paths:
173
130
  - lib
174
- required_ruby_version: !ruby/object:Gem::Requirement
131
+ required_ruby_version: !ruby/object:Gem::Requirement
175
132
  none: false
176
- requirements:
177
- - - ">="
178
- - !ruby/object:Gem::Version
179
- hash: 3
180
- segments:
181
- - 0
182
- version: "0"
183
- required_rubygems_version: !ruby/object:Gem::Requirement
133
+ requirements:
134
+ - - ! '>='
135
+ - !ruby/object:Gem::Version
136
+ version: '0'
137
+ required_rubygems_version: !ruby/object:Gem::Requirement
184
138
  none: false
185
- requirements:
186
- - - ">="
187
- - !ruby/object:Gem::Version
188
- hash: 3
189
- segments:
190
- - 0
191
- version: "0"
139
+ requirements:
140
+ - - ! '>='
141
+ - !ruby/object:Gem::Version
142
+ version: '0'
192
143
  requirements: []
193
-
194
144
  rubyforge_project: split
195
- rubygems_version: 1.3.7
145
+ rubygems_version: 1.8.6
196
146
  signing_key:
197
147
  specification_version: 3
198
148
  summary: Rack based split testing framework
199
- test_files:
149
+ test_files:
200
150
  - spec/alternative_spec.rb
201
151
  - spec/configuration_spec.rb
202
152
  - spec/dashboard_spec.rb