vanity 0.2.0 → 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 +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