split 0.2.4 → 0.3.0

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