vanity 0.2.0 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +7 -1
- data/README.rdoc +1 -1
- data/lib/vanity/experiment/ab_test.rb +154 -35
- data/lib/vanity/experiment/base.rb +80 -21
- data/test/ab_test_test.rb +167 -7
- data/test/experiment_test.rb +4 -1
- data/vanity.gemspec +1 -1
- metadata +3 -4
- data/test/ab_test_template.erb +0 -3
data/CHANGELOG
CHANGED
@@ -1,4 +1,10 @@
|
|
1
|
-
0.
|
1
|
+
0.2.1 (2009-11-11)
|
2
|
+
* Added: z-score and confidence level for A/B test alternatives.
|
3
|
+
* Added: test auto-completion and auto-outcome (complete_it, outcome_is).
|
4
|
+
* Changed: default alternatives are now false/true, so if can't decide
|
5
|
+
outcome, fall back on false.
|
6
|
+
|
7
|
+
0.2.0 (2009-11-10)
|
2
8
|
* Added: experiment method on object, used to define and access experiments.
|
3
9
|
* Added: playground configuration (Vanity.playground.namespace = , etc).
|
4
10
|
* Added: use_vanity now accepts block instead of symbol.
|
data/README.rdoc
CHANGED
@@ -31,6 +31,15 @@ module Vanity
|
|
31
31
|
redis.get(key("conversions")).to_i
|
32
32
|
end
|
33
33
|
|
34
|
+
# Conversion rate calculated as converted/participants.
|
35
|
+
def conversion_rate
|
36
|
+
converted.to_f / participants.to_f
|
37
|
+
end
|
38
|
+
|
39
|
+
def <=>(other)
|
40
|
+
conversion_rate <=> other.conversion_rate
|
41
|
+
end
|
42
|
+
|
34
43
|
def participating!(identity)
|
35
44
|
redis.sadd key("participants"), identity
|
36
45
|
end
|
@@ -42,6 +51,33 @@ module Vanity
|
|
42
51
|
end
|
43
52
|
end
|
44
53
|
|
54
|
+
# Z-score this alternativet related to the base alternative. This
|
55
|
+
# alternative is better than base if it receives a positive z-score,
|
56
|
+
# worse if z-score is negative. Call #confident if you need confidence
|
57
|
+
# level (percentage).
|
58
|
+
def z_score
|
59
|
+
return 0 if base == self
|
60
|
+
pc = base.conversion_rate
|
61
|
+
nc = base.participants
|
62
|
+
p = conversion_rate
|
63
|
+
n = participants
|
64
|
+
(p - pc) / Math.sqrt((p * (1-p)/n) + (pc * (1-pc)/nc))
|
65
|
+
end
|
66
|
+
|
67
|
+
# How confident are we in this alternative being an improvement over the
|
68
|
+
# base alternative. Returns 0, 90, 95, 99 or 99.9 (percentage).
|
69
|
+
def confidence
|
70
|
+
score = z_score
|
71
|
+
confidence = AbTest::Z_TO_CONFIDENCE.find { |z,p| score >= z }
|
72
|
+
confidence ? confidence.last : 0
|
73
|
+
end
|
74
|
+
|
75
|
+
def destroy #:nodoc:
|
76
|
+
redis.del key("participants")
|
77
|
+
redis.del key("converted")
|
78
|
+
redis.del key("conversions")
|
79
|
+
end
|
80
|
+
|
45
81
|
protected
|
46
82
|
|
47
83
|
def key(name)
|
@@ -52,39 +88,20 @@ module Vanity
|
|
52
88
|
@experiment.redis
|
53
89
|
end
|
54
90
|
|
91
|
+
def base
|
92
|
+
@base ||= @experiment.alternatives.first
|
93
|
+
end
|
94
|
+
|
55
95
|
end
|
56
96
|
|
97
|
+
|
57
98
|
# The meat.
|
58
99
|
class AbTest < Base
|
59
100
|
def initialize(*args) #:nodoc:
|
60
101
|
super
|
61
102
|
end
|
62
103
|
|
63
|
-
#
|
64
|
-
#
|
65
|
-
# This method returns different values for different identity (see
|
66
|
-
# #identify), and consistenly the same value for the same
|
67
|
-
# expriment/identity pair.
|
68
|
-
#
|
69
|
-
# For example:
|
70
|
-
# color = experiment(:which_blue).choose
|
71
|
-
def choose
|
72
|
-
identity = identify
|
73
|
-
alt = alternative_for(identity)
|
74
|
-
alt.participating! identity
|
75
|
-
alt.value
|
76
|
-
end
|
77
|
-
|
78
|
-
# Records a conversion.
|
79
|
-
#
|
80
|
-
# For example:
|
81
|
-
# experiment(:which_blue).conversion!
|
82
|
-
def conversion!
|
83
|
-
identity = identify
|
84
|
-
alt = alternative_for(identity)
|
85
|
-
alt.conversion! identity
|
86
|
-
alt.id
|
87
|
-
end
|
104
|
+
# -- Alternatives --
|
88
105
|
|
89
106
|
# Call this method once to specify values for the A/B test. At least two
|
90
107
|
# values are required.
|
@@ -99,7 +116,7 @@ module Vanity
|
|
99
116
|
# alts = experiment(:background_color).alternatives
|
100
117
|
# puts "#{alts.count} alternatives, with the colors: #{alts.map(&:value).join(", ")}"
|
101
118
|
def alternatives(*args)
|
102
|
-
args = [
|
119
|
+
args = [false, true] if args.empty?
|
103
120
|
@alternatives = []
|
104
121
|
args.each_with_index do |arg, i|
|
105
122
|
@alternatives << Alternative.new(self, i, arg)
|
@@ -108,18 +125,50 @@ module Vanity
|
|
108
125
|
alternatives
|
109
126
|
end
|
110
127
|
|
111
|
-
# Sets this test to two alternatives:
|
112
|
-
def
|
113
|
-
alternatives
|
128
|
+
# Sets this test to two alternatives: false and true.
|
129
|
+
def false_true
|
130
|
+
alternatives false, true
|
114
131
|
end
|
132
|
+
alias true_false false_true
|
115
133
|
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
134
|
+
# Chooses a value for this experiment.
|
135
|
+
#
|
136
|
+
# This method returns different values for different identity (see
|
137
|
+
# #identify), and consistenly the same value for the same
|
138
|
+
# expriment/identity pair.
|
139
|
+
#
|
140
|
+
# For example:
|
141
|
+
# color = experiment(:which_blue).choose
|
142
|
+
def choose
|
143
|
+
if active?
|
144
|
+
identity = identify
|
145
|
+
alt = alternative_for(identity)
|
146
|
+
alt.participating! identity
|
147
|
+
check_completion!
|
148
|
+
alt.value
|
149
|
+
elsif alternative = outcome
|
150
|
+
alternative.value
|
151
|
+
else
|
152
|
+
alternatives.first.value
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
# Records a conversion.
|
157
|
+
#
|
158
|
+
# For example:
|
159
|
+
# experiment(:which_blue).conversion!
|
160
|
+
def conversion!
|
161
|
+
if active?
|
162
|
+
identity = identify
|
163
|
+
alt = alternative_for(identity)
|
164
|
+
alt.conversion! identity
|
165
|
+
check_completion!
|
166
|
+
end
|
121
167
|
end
|
122
168
|
|
169
|
+
|
170
|
+
# -- Testing --
|
171
|
+
|
123
172
|
# Forces this experiment to use a particular alternative. Useful for
|
124
173
|
# tests, e.g.
|
125
174
|
#
|
@@ -142,15 +191,78 @@ module Vanity
|
|
142
191
|
Vanity.context.session[:vanity][id] = alternative.id
|
143
192
|
end
|
144
193
|
|
194
|
+
|
195
|
+
# -- Reporting --
|
196
|
+
|
197
|
+
def report
|
198
|
+
alts = alternatives.map { |alt|
|
199
|
+
"<dt>Option #{(65 + alt.id).chr}</dt><dd><code>#{CGI.escape_html alt.value.inspect}</code> viewed #{alt.participants} times, converted #{alt.conversions}, rate #{alt.conversion_rate}, z_score #{alt.z_score}, confidence #{alt.confidence}<dd>"
|
200
|
+
}
|
201
|
+
%{<dl class="data">#{alts.join}</dl>}
|
202
|
+
end
|
203
|
+
|
145
204
|
def humanize
|
146
205
|
"A/B Test"
|
147
206
|
end
|
148
207
|
|
208
|
+
|
209
|
+
# -- Completion --
|
210
|
+
|
211
|
+
# Defines how the experiment can choose the optimal outcome on completion.
|
212
|
+
#
|
213
|
+
# The default implementation looks for the best (highest conversion rate)
|
214
|
+
# alternative. If it's certain (95% or more) that this alternative is
|
215
|
+
# better than the first alternative, it switches to that one. If it has
|
216
|
+
# no such certainty, it starts using the first alternative exclusively.
|
217
|
+
#
|
218
|
+
# The default implementation reads like this:
|
219
|
+
# outcome_is do
|
220
|
+
# highest = alternatives.sort.last
|
221
|
+
# highest.confidence >= 95 ? highest ? alternatives.first
|
222
|
+
# end
|
223
|
+
def outcome_is(&block)
|
224
|
+
raise ArgumentError, "Missing block" unless block
|
225
|
+
raise "outcome_is already called on this experiment" if @outcome_is
|
226
|
+
@outcome_is = block
|
227
|
+
end
|
228
|
+
|
229
|
+
# Alternative chosen when this experiment was completed.
|
230
|
+
def outcome
|
231
|
+
outcome = redis.get(key("outcome"))
|
232
|
+
outcome && alternatives[outcome.to_i]
|
233
|
+
end
|
234
|
+
|
235
|
+
def complete! #:nodoc:
|
236
|
+
super
|
237
|
+
if @outcome_is
|
238
|
+
begin
|
239
|
+
outcome = alternatives.find_index(@outcome_is.call)
|
240
|
+
rescue
|
241
|
+
# TODO: logging
|
242
|
+
end
|
243
|
+
end
|
244
|
+
unless outcome
|
245
|
+
highest = alternatives.sort.last rescue nil
|
246
|
+
outcome = highest && highest.confidence >= 95 ? highest.id : 0
|
247
|
+
end
|
248
|
+
# TODO: logging
|
249
|
+
redis.setnx key("outcome"), outcome
|
250
|
+
end
|
251
|
+
|
252
|
+
|
253
|
+
# -- Store/validate --
|
254
|
+
|
149
255
|
def save #:nodoc:
|
150
256
|
fail "Experiment #{name} needs at least two alternatives" unless alternatives.count >= 2
|
151
257
|
super
|
152
258
|
end
|
153
259
|
|
260
|
+
def destroy #:nodoc:
|
261
|
+
redis.del key(:outcome)
|
262
|
+
alternatives.each(&:destroy)
|
263
|
+
super
|
264
|
+
end
|
265
|
+
|
154
266
|
private
|
155
267
|
|
156
268
|
# Chooses an alternative for the identity and returns its index. This
|
@@ -164,7 +276,14 @@ module Vanity
|
|
164
276
|
alternatives[index]
|
165
277
|
end
|
166
278
|
|
279
|
+
begin
|
280
|
+
a = 0
|
281
|
+
# Returns array of [z-score, percentage]
|
282
|
+
norm_dist = (-5.0..3.1).step(0.01).map { |x| [x, a += 1 / Math.sqrt(2 * Math::PI) * Math::E ** (-x ** 2 / 2)] }
|
283
|
+
# We're really only interested in 90%, 95%, 99% and 99.9%.
|
284
|
+
Z_TO_CONFIDENCE = [90, 95, 99, 99.9].map { |pct| [norm_dist.find { |x,a| a >= pct }.first, pct] }.reverse
|
285
|
+
end
|
286
|
+
|
167
287
|
end
|
168
288
|
end
|
169
289
|
end
|
170
|
-
|
@@ -18,8 +18,8 @@ module Vanity
|
|
18
18
|
@playground = playground
|
19
19
|
@id, @name = id.to_sym, name
|
20
20
|
@namespace = "#{@playground.namespace}:#{@id}"
|
21
|
-
|
22
|
-
@created_at = Time.at(
|
21
|
+
redis.setnx key(:created_at), Time.now.to_i
|
22
|
+
@created_at = Time.at(redis.get(key(:created_at)).to_i)
|
23
23
|
@identify_block = ->(context){ context.vanity_identity }
|
24
24
|
end
|
25
25
|
|
@@ -31,26 +31,10 @@ module Vanity
|
|
31
31
|
|
32
32
|
# Experiment creation timestamp.
|
33
33
|
attr_reader :created_at
|
34
|
-
|
35
|
-
# Sets or returns description. For example
|
36
|
-
# experiment :simple do
|
37
|
-
# description "Simple experiment"
|
38
|
-
# end
|
39
|
-
#
|
40
|
-
# puts "Just defined: " + experiment(:simple).description
|
41
|
-
def description(text = nil)
|
42
|
-
@description = text if text
|
43
|
-
@description
|
44
|
-
end
|
45
|
-
|
46
|
-
def report
|
47
|
-
fail "Implement me"
|
48
|
-
end
|
49
|
-
|
50
|
-
# Called to save the experiment definition.
|
51
|
-
def save #:nodoc:
|
52
|
-
end
|
53
34
|
|
35
|
+
# Experiment completion timestamp.
|
36
|
+
attr_reader :completed_at
|
37
|
+
|
54
38
|
# Call this method with no argument or block to return an identity. Call
|
55
39
|
# this method with a block to define how to obtain an identity for the
|
56
40
|
# current experiment.
|
@@ -80,6 +64,70 @@ module Vanity
|
|
80
64
|
end
|
81
65
|
end
|
82
66
|
|
67
|
+
|
68
|
+
# -- Reporting --
|
69
|
+
|
70
|
+
# Sets or returns description. For example
|
71
|
+
# experiment :simple do
|
72
|
+
# description "Simple experiment"
|
73
|
+
# end
|
74
|
+
#
|
75
|
+
# puts "Just defined: " + experiment(:simple).description
|
76
|
+
def description(text = nil)
|
77
|
+
@description = text if text
|
78
|
+
@description
|
79
|
+
end
|
80
|
+
|
81
|
+
def report
|
82
|
+
fail "Implement me"
|
83
|
+
end
|
84
|
+
|
85
|
+
|
86
|
+
# -- Experiment completion --
|
87
|
+
|
88
|
+
# Define experiment completion condition. For example:
|
89
|
+
# complete_if do
|
90
|
+
# alternatives.all? { |alt| alt.participants >= 100 } &&
|
91
|
+
# alternatives.any? { |alt| alt.confidence >= 0.95 }
|
92
|
+
# end
|
93
|
+
def complete_if(&block)
|
94
|
+
raise ArgumentError, "Missing block" unless block
|
95
|
+
raise "complete_if already called on this experiment" if @complete_block
|
96
|
+
@complete_block = block
|
97
|
+
end
|
98
|
+
|
99
|
+
# Derived classes call this after state changes that may lead to
|
100
|
+
# experiment completing.
|
101
|
+
def check_completion!
|
102
|
+
if @complete_block
|
103
|
+
begin
|
104
|
+
complete! if @complete_block.call
|
105
|
+
rescue
|
106
|
+
# TODO: logging
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
protected :check_completion!
|
111
|
+
|
112
|
+
# Force experiment to complete.
|
113
|
+
def complete!
|
114
|
+
redis.setnx key(:completed_at), Time.now.to_i
|
115
|
+
# TODO: logging
|
116
|
+
end
|
117
|
+
|
118
|
+
# Time stamp when experiment was completed.
|
119
|
+
def completed_at
|
120
|
+
Time.at(redis.get(key(:completed_at)).to_i)
|
121
|
+
end
|
122
|
+
|
123
|
+
# Returns true if experiment active, false if completed.
|
124
|
+
def active?
|
125
|
+
redis.get(key(:completed_at)).nil?
|
126
|
+
end
|
127
|
+
|
128
|
+
|
129
|
+
# -- Store/validate --
|
130
|
+
|
83
131
|
# Returns key for this experiment, or with an argument, return a key
|
84
132
|
# using the experiment as the namespace. Examples:
|
85
133
|
# key => "vanity:experiments:green_button"
|
@@ -92,6 +140,17 @@ module Vanity
|
|
92
140
|
def redis #:nodoc:
|
93
141
|
@playground.redis
|
94
142
|
end
|
143
|
+
|
144
|
+
# Called to save the experiment definition.
|
145
|
+
def save #:nodoc:
|
146
|
+
end
|
147
|
+
|
148
|
+
# Get rid of all experiment data.
|
149
|
+
def destroy
|
150
|
+
redis.del key(:created_at)
|
151
|
+
redis.del key(:completed_at)
|
152
|
+
end
|
153
|
+
|
95
154
|
end
|
96
155
|
end
|
97
156
|
end
|
data/test/ab_test_test.rb
CHANGED
@@ -13,7 +13,7 @@ class AbTestController < ActionController::Base
|
|
13
13
|
end
|
14
14
|
|
15
15
|
def test_capture
|
16
|
-
render
|
16
|
+
render inline: "<% ab_test :simple_ab do |value| %><%= value %><% end %>"
|
17
17
|
end
|
18
18
|
|
19
19
|
def goal
|
@@ -29,7 +29,8 @@ class AbTestTest < ActionController::TestCase
|
|
29
29
|
experiment(:simple_ab) { }
|
30
30
|
end
|
31
31
|
|
32
|
-
|
32
|
+
|
33
|
+
# -- Experiment definition --
|
33
34
|
|
34
35
|
def uses_ab_test_when_type_is_ab_test
|
35
36
|
experiment(:ab, type: :ab_test) { }
|
@@ -52,7 +53,8 @@ class AbTestTest < ActionController::TestCase
|
|
52
53
|
end
|
53
54
|
end
|
54
55
|
|
55
|
-
|
56
|
+
|
57
|
+
# -- Running experiment --
|
56
58
|
|
57
59
|
def returns_the_same_alternative_consistently
|
58
60
|
experiment :foobar do
|
@@ -122,7 +124,7 @@ class AbTestTest < ActionController::TestCase
|
|
122
124
|
end
|
123
125
|
|
124
126
|
|
125
|
-
# A/B helper methods
|
127
|
+
# -- A/B helper methods --
|
126
128
|
|
127
129
|
def test_fail_if_no_experiment
|
128
130
|
new_playground
|
@@ -167,7 +169,7 @@ class AbTestTest < ActionController::TestCase
|
|
167
169
|
end
|
168
170
|
|
169
171
|
|
170
|
-
# Testing with tests
|
172
|
+
# -- Testing with tests --
|
171
173
|
|
172
174
|
def test_with_given_choice
|
173
175
|
100.times do
|
@@ -177,8 +179,8 @@ class AbTestTest < ActionController::TestCase
|
|
177
179
|
post :goal
|
178
180
|
end
|
179
181
|
alts = experiment(:simple_ab).alternatives
|
180
|
-
assert_equal [100
|
181
|
-
assert_equal [100
|
182
|
+
assert_equal [0,100], alts.map { |alt| alt.participants }
|
183
|
+
assert_equal [0,100], alts.map { |alt| alt.conversions }
|
182
184
|
end
|
183
185
|
|
184
186
|
def test_which_chooses_non_existent_alternative
|
@@ -187,4 +189,162 @@ class AbTestTest < ActionController::TestCase
|
|
187
189
|
end
|
188
190
|
end
|
189
191
|
|
192
|
+
|
193
|
+
# -- Z-score --
|
194
|
+
|
195
|
+
def test_z_score
|
196
|
+
experiment :abcd do
|
197
|
+
alternatives :a, :b, :c, :d
|
198
|
+
end
|
199
|
+
alts = experiment(:abcd).alternatives
|
200
|
+
# participating, conversions, rate, z-score
|
201
|
+
# Control: 182 35 19.23% N/A
|
202
|
+
182.times { |i| alts[0].participating!(i) }
|
203
|
+
35.times { |i| alts[0].conversion!(i) }
|
204
|
+
# Treatment A: 180 45 25.00% 1.33
|
205
|
+
180.times { |i| alts[1].participating!(i + 200) }
|
206
|
+
45.times { |i| alts[1].conversion!(i + 200) }
|
207
|
+
# Treatment B: 189 28 14.81% -1.13
|
208
|
+
189.times { |i| alts[2].participating!(i + 400) }
|
209
|
+
28.times { |i| alts[2].conversion!(i + 400) }
|
210
|
+
# Treatment C: 188 61 32.45% 2.94
|
211
|
+
188.times { |i| alts[3].participating!(i + 600) }
|
212
|
+
61.times { |i| alts[3].conversion!(i + 600) }
|
213
|
+
|
214
|
+
z_scores = alts.map { |alt| sprintf("%4.2f", alt.z_score) }
|
215
|
+
assert_equal %w{0.00 1.33 -1.13 2.94}, z_scores
|
216
|
+
|
217
|
+
confidences = alts.map { |alt| alt.confidence }
|
218
|
+
assert_equal [0, 90, 0, 99], confidences
|
219
|
+
end
|
220
|
+
|
221
|
+
|
222
|
+
# -- Completion --
|
223
|
+
|
224
|
+
def test_completion_if
|
225
|
+
experiment :simple do
|
226
|
+
identify { rand }
|
227
|
+
complete_if { true }
|
228
|
+
end
|
229
|
+
experiment(:simple).choose
|
230
|
+
refute experiment(:simple).active?
|
231
|
+
end
|
232
|
+
|
233
|
+
def test_completion_if_fails
|
234
|
+
experiment :simple do
|
235
|
+
identify { rand }
|
236
|
+
complete_if { fail }
|
237
|
+
end
|
238
|
+
experiment(:simple).choose
|
239
|
+
assert experiment(:simple).active?
|
240
|
+
end
|
241
|
+
|
242
|
+
def test_completion
|
243
|
+
ids = Array.new(100) { |i| i.to_s }.shuffle
|
244
|
+
experiment :simple do
|
245
|
+
identify { ids.pop }
|
246
|
+
complete_if { alternatives.map(&:participants).sum >= 100 }
|
247
|
+
end
|
248
|
+
99.times do |i|
|
249
|
+
experiment(:simple).choose
|
250
|
+
assert experiment(:simple).active?
|
251
|
+
end
|
252
|
+
|
253
|
+
experiment(:simple).choose
|
254
|
+
refute experiment(:simple).active?
|
255
|
+
end
|
256
|
+
|
257
|
+
def test_ab_methods_after_completion
|
258
|
+
ids = Array.new(200) { |i| i.to_s }.shuffle
|
259
|
+
test = self
|
260
|
+
experiment :simple do
|
261
|
+
identify { test.identity ||= ids.pop }
|
262
|
+
complete_if { alternatives.map(&:participants).sum >= 100 }
|
263
|
+
outcome_is { alternatives[1] }
|
264
|
+
end
|
265
|
+
# Run experiment to completion (100 participants)
|
266
|
+
results = Set.new
|
267
|
+
100.times do
|
268
|
+
test.identity = nil
|
269
|
+
results << experiment(:simple).choose
|
270
|
+
experiment(:simple).conversion!
|
271
|
+
end
|
272
|
+
assert results.include?(true) && results.include?(false)
|
273
|
+
refute experiment(:simple).active?
|
274
|
+
|
275
|
+
# Test that we always get the same choice (true)
|
276
|
+
100.times do
|
277
|
+
test.identity = nil
|
278
|
+
assert_equal true, experiment(:simple).choose
|
279
|
+
experiment(:simple).conversion!
|
280
|
+
end
|
281
|
+
# We don't get to count the 100 participant's conversion, but that's ok.
|
282
|
+
assert_equal 99, experiment(:simple).alternatives.map(&:converted).sum
|
283
|
+
assert_equal 99, experiment(:simple).alternatives.map(&:conversions).sum
|
284
|
+
end
|
285
|
+
|
286
|
+
|
287
|
+
# -- Outcome --
|
288
|
+
|
289
|
+
def test_completion_outcome
|
290
|
+
experiment :quick do
|
291
|
+
outcome_is { alternatives[1] }
|
292
|
+
end
|
293
|
+
experiment(:quick).complete!
|
294
|
+
assert_equal experiment(:quick).alternatives[1], experiment(:quick).outcome
|
295
|
+
end
|
296
|
+
|
297
|
+
def test_outcome_is_returns_nil
|
298
|
+
experiment :quick do
|
299
|
+
outcome_is { nil }
|
300
|
+
end
|
301
|
+
experiment(:quick).complete!
|
302
|
+
assert_equal experiment(:quick).alternatives.first, experiment(:quick).outcome
|
303
|
+
end
|
304
|
+
|
305
|
+
def test_outcome_is_returns_something_else
|
306
|
+
experiment :quick do
|
307
|
+
outcome_is { "error" }
|
308
|
+
end
|
309
|
+
experiment(:quick).complete!
|
310
|
+
assert_equal experiment(:quick).alternatives.first, experiment(:quick).outcome
|
311
|
+
end
|
312
|
+
|
313
|
+
def test_outcome_is_fails
|
314
|
+
experiment :quick do
|
315
|
+
outcome_is { fail }
|
316
|
+
end
|
317
|
+
experiment(:quick).complete!
|
318
|
+
assert_equal experiment(:quick).alternatives.first, experiment(:quick).outcome
|
319
|
+
end
|
320
|
+
|
321
|
+
def test_outcome_choosing_best_alternative
|
322
|
+
experiment :quick do
|
323
|
+
end
|
324
|
+
2.times do |i|
|
325
|
+
experiment(:quick).alternatives[0].participating!(i)
|
326
|
+
end
|
327
|
+
10.times do |i|
|
328
|
+
experiment(:quick).alternatives[1].participating!(i)
|
329
|
+
experiment(:quick).alternatives[1].conversion!(i)
|
330
|
+
end
|
331
|
+
experiment(:quick).complete!
|
332
|
+
assert_equal experiment(:quick).alternatives[1], experiment(:quick).outcome
|
333
|
+
end
|
334
|
+
|
335
|
+
def test_outcome_choosing_first_alternative
|
336
|
+
experiment :quick do
|
337
|
+
end
|
338
|
+
8.times do |i|
|
339
|
+
experiment(:quick).alternatives[0].participating!(i)
|
340
|
+
experiment(:quick).alternatives[0].conversion!(i)
|
341
|
+
end
|
342
|
+
7.times do |i|
|
343
|
+
experiment(:quick).alternatives[1].participating!(i)
|
344
|
+
experiment(:quick).alternatives[1].conversion!(i)
|
345
|
+
end
|
346
|
+
experiment(:quick).complete!
|
347
|
+
assert_equal experiment(:quick).alternatives[0], experiment(:quick).outcome
|
348
|
+
end
|
349
|
+
|
190
350
|
end
|
data/test/experiment_test.rb
CHANGED
@@ -27,11 +27,13 @@ class ExperimentTest < MiniTest::Spec
|
|
27
27
|
end
|
28
28
|
|
29
29
|
it "keeps creation timestamp across definitions" do
|
30
|
-
early = Time.now - 1.day
|
30
|
+
early, late = Time.now - 1.day, Time.now
|
31
31
|
Time.expects(:now).once.returns(early)
|
32
32
|
experiment(:simple) { }
|
33
33
|
assert_equal early.to_i, experiment(:simple).created_at.to_i
|
34
|
+
|
34
35
|
new_playground
|
36
|
+
Time.expects(:now).once.returns(late)
|
35
37
|
experiment(:simple) { }
|
36
38
|
assert_equal early.to_i, experiment(:simple).created_at.to_i
|
37
39
|
end
|
@@ -42,4 +44,5 @@ class ExperimentTest < MiniTest::Spec
|
|
42
44
|
end
|
43
45
|
assert_equal "Simple experiment", experiment(:simple).description
|
44
46
|
end
|
47
|
+
|
45
48
|
end
|
data/vanity.gemspec
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: vanity
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Assaf Arkin
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2009-11-
|
12
|
+
date: 2009-11-11 00:00:00 -08:00
|
13
13
|
default_executable:
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
@@ -40,7 +40,6 @@ files:
|
|
40
40
|
- lib/vanity/rails.rb
|
41
41
|
- lib/vanity/report.erb
|
42
42
|
- lib/vanity.rb
|
43
|
-
- test/ab_test_template.erb
|
44
43
|
- test/ab_test_test.rb
|
45
44
|
- test/experiment_test.rb
|
46
45
|
- test/playground_test.rb
|
@@ -56,7 +55,7 @@ licenses: []
|
|
56
55
|
post_install_message:
|
57
56
|
rdoc_options:
|
58
57
|
- --title
|
59
|
-
- Vanity 0.2.
|
58
|
+
- Vanity 0.2.1
|
60
59
|
- --main
|
61
60
|
- README.rdoc
|
62
61
|
- --webcvs
|
data/test/ab_test_template.erb
DELETED