vanity 1.8.1 → 1.8.2
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/.travis.yml +47 -5
- data/Appraisals +3 -1
- data/CHANGELOG +7 -1
- data/Gemfile +21 -10
- data/Gemfile.lock +1 -1
- data/README.rdoc +9 -6
- data/gemfiles/rails3.gemfile +9 -8
- data/gemfiles/rails3.gemfile.lock +8 -2
- data/gemfiles/rails31.gemfile +9 -8
- data/gemfiles/rails31.gemfile.lock +8 -2
- data/gemfiles/rails32.gemfile +9 -8
- data/gemfiles/rails32.gemfile.lock +8 -2
- data/lib/vanity.rb +1 -0
- data/lib/vanity/adapters/abstract_adapter.rb +5 -0
- data/lib/vanity/adapters/active_record_adapter.rb +12 -5
- data/lib/vanity/adapters/mongodb_adapter.rb +6 -0
- data/lib/vanity/adapters/redis_adapter.rb +8 -0
- data/lib/vanity/autoconnect.rb +61 -0
- data/lib/vanity/experiment/ab_test.rb +29 -23
- data/lib/vanity/experiment/base.rb +14 -0
- data/lib/vanity/frameworks/rails.rb +11 -4
- data/lib/vanity/playground.rb +24 -15
- data/lib/vanity/version.rb +1 -1
- data/test/autoconnect_test.rb +25 -0
- data/test/dummy/config/boot.rb +4 -6
- data/test/dummy/config/environments/development.rb +1 -1
- data/test/experiment/ab_test.rb +40 -4
- data/test/experiment/base_test.rb +3 -1
- data/test/metric/base_test.rb +10 -10
- data/test/passenger_test.rb +7 -1
- data/test/playground_test.rb +11 -0
- data/test/rails_test.rb +149 -68
- data/test/test_helper.rb +1 -0
- metadata +58 -43
- checksums.yaml +0 -7
@@ -130,6 +130,14 @@ module Vanity
|
|
130
130
|
@experiments.sadd "#{experiment}:alts:#{alternative}:participants", identity
|
131
131
|
end
|
132
132
|
|
133
|
+
def ab_seen(experiment, identity, alternative)
|
134
|
+
if @experiments.sismember "#{experiment}:alts:#{alternative.id}:participants", identity
|
135
|
+
return alternative
|
136
|
+
else
|
137
|
+
return nil
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
133
141
|
def ab_add_conversion(experiment, alternative, identity, count = 1, implicit = false)
|
134
142
|
if implicit
|
135
143
|
@experiments.sadd "#{experiment}:alts:#{alternative}:participants", identity
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Vanity
|
2
|
+
# A singleton responsible for determining if the playground should connect
|
3
|
+
# to the datastore.
|
4
|
+
module Autoconnect
|
5
|
+
|
6
|
+
BLACKLISTED_RAILS_RAKE_TASKS = [
|
7
|
+
'about',
|
8
|
+
'assets:clean',
|
9
|
+
'assets:clobber',
|
10
|
+
'assets:environment',
|
11
|
+
'assets:precompile',
|
12
|
+
'assets:precompile:all',
|
13
|
+
'db:create',
|
14
|
+
'db:drop',
|
15
|
+
'db:fixtures:load',
|
16
|
+
'db:migrate',
|
17
|
+
'db:migrate:status',
|
18
|
+
'db:rollback',
|
19
|
+
'db:schema:cache:clear',
|
20
|
+
'db:schema:cache:dump',
|
21
|
+
'db:schema:dump',
|
22
|
+
'db:schema:load',
|
23
|
+
'db:seed',
|
24
|
+
'db:setup',
|
25
|
+
'db:structure:dump',
|
26
|
+
'db:version',
|
27
|
+
'doc:app',
|
28
|
+
'log:clear',
|
29
|
+
'middleware',
|
30
|
+
'notes',
|
31
|
+
'notes:custom',
|
32
|
+
'rails:template',
|
33
|
+
'rails:update',
|
34
|
+
'routes',
|
35
|
+
'secret',
|
36
|
+
'stats',
|
37
|
+
'time:zones:all',
|
38
|
+
'tmp:clear',
|
39
|
+
'tmp:create'
|
40
|
+
]
|
41
|
+
ENVIRONMENT_VANITY_DISABLED_FLAG = "VANITY_DISABLED"
|
42
|
+
|
43
|
+
def self.playground_should_autoconnect?
|
44
|
+
!environment_disabled? && !in_blacklisted_rake_task?
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.environment_disabled?
|
48
|
+
!!ENV[ENVIRONMENT_VANITY_DISABLED_FLAG]
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.in_blacklisted_rake_task?
|
52
|
+
!(current_rake_tasks & BLACKLISTED_RAILS_RAKE_TASKS).empty?
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.current_rake_tasks
|
56
|
+
::Rake.application.top_level_tasks
|
57
|
+
rescue => e
|
58
|
+
[]
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -15,7 +15,7 @@ module Vanity
|
|
15
15
|
|
16
16
|
# Alternative id, only unique for this experiment.
|
17
17
|
attr_reader :id
|
18
|
-
|
18
|
+
|
19
19
|
# Alternative name (option A, option B, etc).
|
20
20
|
attr_reader :name
|
21
21
|
|
@@ -48,7 +48,7 @@ module Vanity
|
|
48
48
|
|
49
49
|
# Probability derived from z-score. Populated by AbTest#score.
|
50
50
|
attr_accessor :probability
|
51
|
-
|
51
|
+
|
52
52
|
# Difference from least performing alternative. Populated by AbTest#score.
|
53
53
|
attr_accessor :difference
|
54
54
|
|
@@ -64,7 +64,7 @@ module Vanity
|
|
64
64
|
end
|
65
65
|
|
66
66
|
def <=>(other)
|
67
|
-
measure <=> other.measure
|
67
|
+
measure <=> other.measure
|
68
68
|
end
|
69
69
|
|
70
70
|
def ==(other)
|
@@ -101,7 +101,7 @@ module Vanity
|
|
101
101
|
end
|
102
102
|
|
103
103
|
def friendly_name
|
104
|
-
"A/B Test"
|
104
|
+
"A/B Test"
|
105
105
|
end
|
106
106
|
|
107
107
|
end
|
@@ -112,8 +112,8 @@ module Vanity
|
|
112
112
|
|
113
113
|
|
114
114
|
# -- Metric --
|
115
|
-
|
116
|
-
# Tells A/B test which metric we're measuring, or returns metric in use.
|
115
|
+
|
116
|
+
# Tells A/B test which metric we're measuring, or returns metric in use.
|
117
117
|
#
|
118
118
|
# @example Define A/B test against coolness metric
|
119
119
|
# ab_test "Background color" do
|
@@ -127,7 +127,6 @@ module Vanity
|
|
127
127
|
@metrics
|
128
128
|
end
|
129
129
|
|
130
|
-
|
131
130
|
# -- Alternatives --
|
132
131
|
|
133
132
|
# Call this method once to set alternative values for this experiment
|
@@ -139,7 +138,7 @@ module Vanity
|
|
139
138
|
# metrics :coolness
|
140
139
|
# alternatives "red", "blue", "orange"
|
141
140
|
# end
|
142
|
-
#
|
141
|
+
#
|
143
142
|
# @example Find out which alternatives this test uses
|
144
143
|
# alts = experiment(:background_color).alternatives
|
145
144
|
# puts "#{alts.count} alternatives, with the colors: #{alts.map(&:value).join(", ")}"
|
@@ -175,7 +174,7 @@ module Vanity
|
|
175
174
|
#
|
176
175
|
# @example
|
177
176
|
# ab_test "More bacon" do
|
178
|
-
# metrics :yummyness
|
177
|
+
# metrics :yummyness
|
179
178
|
# false_true
|
180
179
|
# end
|
181
180
|
#
|
@@ -198,13 +197,20 @@ module Vanity
|
|
198
197
|
if @playground.collecting?
|
199
198
|
if active?
|
200
199
|
identity = identity()
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
200
|
+
index = connection.ab_showing(@id, identity)
|
201
|
+
unless index
|
202
|
+
index = alternative_for(identity)
|
203
|
+
if !@playground.using_js?
|
204
|
+
# if we have an on_assignment block, call it on new assignments
|
205
|
+
if @on_assignment_block
|
206
|
+
assignment = alternatives[index.to_i]
|
207
|
+
if !connection.ab_seen @id, identity, assignment
|
208
|
+
@on_assignment_block.call(Vanity.context, identity, assignment, self)
|
209
|
+
end
|
210
|
+
end
|
211
|
+
connection.ab_add_participant @id, index, identity
|
212
|
+
check_completion!
|
213
|
+
end
|
208
214
|
end
|
209
215
|
else
|
210
216
|
index = connection.ab_get_outcome(@id) || alternative_for(identity)
|
@@ -225,9 +231,9 @@ module Vanity
|
|
225
231
|
Digest::MD5.hexdigest("#{id}:#{alternative.id}")[-10,10]
|
226
232
|
end
|
227
233
|
|
228
|
-
|
234
|
+
|
229
235
|
# -- Testing --
|
230
|
-
|
236
|
+
|
231
237
|
# Forces this experiment to use a particular alternative. You'll want to
|
232
238
|
# use this from your test cases to test for the different alternatives.
|
233
239
|
#
|
@@ -256,8 +262,8 @@ module Vanity
|
|
256
262
|
check_completion!
|
257
263
|
end
|
258
264
|
raise ArgumentError, "No alternative #{value.inspect} for #{name}" unless index
|
259
|
-
if (connection.ab_showing(@id, identity) && connection.ab_showing(@id, identity) != index) ||
|
260
|
-
|
265
|
+
if (connection.ab_showing(@id, identity) && connection.ab_showing(@id, identity) != index) ||
|
266
|
+
alternative_for(identity) != index
|
261
267
|
connection.ab_show @id, identity, index
|
262
268
|
end
|
263
269
|
end
|
@@ -294,7 +300,7 @@ module Vanity
|
|
294
300
|
# [:z_score] Z-score (relative to the base alternative).
|
295
301
|
# [:probability] Probability (z-score mapped to 0, 90, 95, 99 or 99.9%).
|
296
302
|
# [:difference] Difference from the least performant altenative.
|
297
|
-
#
|
303
|
+
#
|
298
304
|
# The choice alternative is set only if its probability is higher or
|
299
305
|
# equal to the specified probability (default is 90%).
|
300
306
|
def score(probability = 90)
|
@@ -405,7 +411,7 @@ module Vanity
|
|
405
411
|
begin
|
406
412
|
result = @outcome_is.call
|
407
413
|
outcome = result.id if Alternative === result && result.experiment == self
|
408
|
-
rescue
|
414
|
+
rescue
|
409
415
|
warn "Error in AbTest#complete!: #{$!}"
|
410
416
|
end
|
411
417
|
else
|
@@ -416,7 +422,7 @@ module Vanity
|
|
416
422
|
connection.ab_set_outcome @id, outcome || 0
|
417
423
|
end
|
418
424
|
|
419
|
-
|
425
|
+
|
420
426
|
# -- Store/validate --
|
421
427
|
|
422
428
|
def destroy
|
@@ -69,6 +69,7 @@ module Vanity
|
|
69
69
|
@id, @name = id.to_sym, name
|
70
70
|
@options = options || {}
|
71
71
|
@identify_block = method(:default_identify)
|
72
|
+
@on_assignment_block = nil
|
72
73
|
end
|
73
74
|
|
74
75
|
# Human readable experiment name (first argument you pass when creating a
|
@@ -113,6 +114,19 @@ module Vanity
|
|
113
114
|
@identify_block = block
|
114
115
|
end
|
115
116
|
|
117
|
+
# Defines any additional actions to take when a new assignment is made for the current experiment
|
118
|
+
#
|
119
|
+
# For example, if you want to use the rails default logger to log whenever an assignment is made:
|
120
|
+
# ab_test "Project widget" do
|
121
|
+
# alternatives :small, :medium, :large
|
122
|
+
# on_assignment do |controller, identity, assignment|
|
123
|
+
# controller.logger.info "made a split test assignment for #{experiment.name}: #{identity} => #{assignment}"
|
124
|
+
# end
|
125
|
+
# end
|
126
|
+
def on_assignment(&block)
|
127
|
+
fail "Missing block" unless block
|
128
|
+
@on_assignment_block = block
|
129
|
+
end
|
116
130
|
|
117
131
|
# -- Reporting --
|
118
132
|
|
@@ -182,9 +182,7 @@ module Vanity
|
|
182
182
|
# <%= count %> features to choose from!
|
183
183
|
# <% end %>
|
184
184
|
def ab_test(name, &block)
|
185
|
-
|
186
|
-
@_vanity_experiments[name] ||= Vanity.playground.experiment(name.to_sym).choose
|
187
|
-
value = @_vanity_experiments[name].value
|
185
|
+
value = setup_experiment(name)
|
188
186
|
|
189
187
|
if block
|
190
188
|
content = capture(value, &block)
|
@@ -258,8 +256,17 @@ module Vanity
|
|
258
256
|
experiments[name] = alternative.clone
|
259
257
|
end
|
260
258
|
|
261
|
-
|
259
|
+
experiments
|
260
|
+
end
|
261
|
+
|
262
|
+
protected
|
263
|
+
|
264
|
+
def setup_experiment(name)
|
265
|
+
@_vanity_experiments ||= {}
|
266
|
+
@_vanity_experiments[name] ||= Vanity.playground.experiment(name.to_sym).choose
|
267
|
+
@_vanity_experiments[name].value
|
262
268
|
end
|
269
|
+
|
263
270
|
end
|
264
271
|
|
265
272
|
|
data/lib/vanity/playground.rb
CHANGED
@@ -21,6 +21,7 @@ module Vanity
|
|
21
21
|
# - namespace -- Namespace to use
|
22
22
|
# - load_path -- Path to load experiments/metrics from
|
23
23
|
# - logger -- Logger to use
|
24
|
+
# - redis -- A Redis object that will be used for the connection
|
24
25
|
def initialize(*args)
|
25
26
|
options = Hash === args.last ? args.pop : {}
|
26
27
|
# In the case of Rails, use the Rails logger and collect only for
|
@@ -39,18 +40,6 @@ module Vanity
|
|
39
40
|
end
|
40
41
|
|
41
42
|
@options = defaults.merge(config).merge(options)
|
42
|
-
if @options[:host] == 'redis' && @options.values_at(:host, :port, :db).any?
|
43
|
-
warn "Deprecated: please specify Redis connection as URL (\"redis://host:port/db\")"
|
44
|
-
establish_connection :adapter=>"redis", :host=>@options[:host], :port=>@options[:port], :database=>@options[:db] || @options[:database]
|
45
|
-
elsif @options[:redis]
|
46
|
-
@adapter = RedisAdapter.new(:redis=>@options[:redis])
|
47
|
-
else
|
48
|
-
connection_spec = args.shift || @options[:connection]
|
49
|
-
if connection_spec
|
50
|
-
connection_spec = "redis://" + connection_spec unless connection_spec[/^\w+:/]
|
51
|
-
establish_connection connection_spec
|
52
|
-
end
|
53
|
-
end
|
54
43
|
|
55
44
|
warn "Deprecated: namespace option no longer supported directly" if @options[:namespace]
|
56
45
|
@load_path = @options[:load_path] || DEFAULTS[:load_path]
|
@@ -58,12 +47,15 @@ module Vanity
|
|
58
47
|
@logger = Logger.new(STDOUT)
|
59
48
|
@logger.level = Logger::ERROR
|
60
49
|
end
|
50
|
+
|
51
|
+
autoconnect(@options, args) if Vanity::Autoconnect.playground_should_autoconnect?
|
52
|
+
|
61
53
|
@loading = []
|
62
54
|
@use_js = false
|
63
55
|
self.add_participant_path = DEFAULT_ADD_PARTICIPANT_PATH
|
64
56
|
@collecting = !!@options[:collecting]
|
65
57
|
end
|
66
|
-
|
58
|
+
|
67
59
|
# Deprecated. Use redis.server instead.
|
68
60
|
attr_accessor :host, :port, :db, :password, :namespace
|
69
61
|
|
@@ -219,7 +211,7 @@ module Vanity
|
|
219
211
|
|
220
212
|
|
221
213
|
# -- Connection management --
|
222
|
-
|
214
|
+
|
223
215
|
# This is the preferred way to programmatically create a new connection (or
|
224
216
|
# switch to a new connection). If no connection was established, the
|
225
217
|
# playground will create a new one by calling this method with no arguments.
|
@@ -242,7 +234,7 @@ module Vanity
|
|
242
234
|
# Vanity.playground.establish_connection :adapter=>:redis,
|
243
235
|
# :host=>"redis.local"
|
244
236
|
#
|
245
|
-
# @since 1.4.0
|
237
|
+
# @since 1.4.0
|
246
238
|
def establish_connection(spec = nil)
|
247
239
|
@spec = spec
|
248
240
|
disconnect! if @adapter
|
@@ -343,6 +335,23 @@ module Vanity
|
|
343
335
|
connection
|
344
336
|
end
|
345
337
|
|
338
|
+
protected
|
339
|
+
|
340
|
+
def autoconnect(options, arguments)
|
341
|
+
if options[:host] == 'redis' && options.values_at(:host, :port, :db).any?
|
342
|
+
warn "Deprecated: please specify Redis connection as URL (\"redis://host:port/db\")"
|
343
|
+
establish_connection :adapter=>"redis", :host=>options[:host], :port=>options[:port], :database=>options[:db] || options[:database]
|
344
|
+
elsif options[:redis]
|
345
|
+
@adapter = RedisAdapter.new(:redis=>options[:redis])
|
346
|
+
else
|
347
|
+
connection_spec = arguments.shift || options[:connection]
|
348
|
+
if connection_spec
|
349
|
+
connection_spec = "redis://" + connection_spec unless connection_spec[/^\w+:/]
|
350
|
+
establish_connection connection_spec
|
351
|
+
end
|
352
|
+
end
|
353
|
+
end
|
354
|
+
|
346
355
|
end
|
347
356
|
|
348
357
|
# In the case of Rails, use the Rails logger and collect only for
|
data/lib/vanity/version.rb
CHANGED
@@ -0,0 +1,25 @@
|
|
1
|
+
require "test/test_helper"
|
2
|
+
|
3
|
+
context ".playground_should_autoconnect?" do
|
4
|
+
|
5
|
+
setup do
|
6
|
+
end
|
7
|
+
|
8
|
+
test "returns true by default" do
|
9
|
+
autoconnect = Vanity::Autoconnect.playground_should_autoconnect?
|
10
|
+
assert autoconnect == true
|
11
|
+
end
|
12
|
+
|
13
|
+
test "returns false if environment flag is set" do
|
14
|
+
ENV['VANITY_DISABLED'] = '1'
|
15
|
+
autoconnect = Vanity::Autoconnect.playground_should_autoconnect?
|
16
|
+
assert autoconnect == false
|
17
|
+
ENV['VANITY_DISABLED'] = nil
|
18
|
+
end
|
19
|
+
|
20
|
+
test "returns false if in assets:precompile rake task" do
|
21
|
+
Rake.expects(:application).returns(stub(:top_level_tasks => ['assets:precompile']))
|
22
|
+
autoconnect = Vanity::Autoconnect.playground_should_autoconnect?
|
23
|
+
assert autoconnect == false
|
24
|
+
end
|
25
|
+
end
|
data/test/dummy/config/boot.rb
CHANGED
@@ -1,10 +1,8 @@
|
|
1
1
|
require 'rubygems'
|
2
|
-
gemfile = File.expand_path('../../../../Gemfile', __FILE__)
|
3
2
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
end
|
3
|
+
# Set up gems listed in the Gemfile.
|
4
|
+
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
|
5
|
+
|
6
|
+
require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE'])
|
9
7
|
|
10
8
|
$:.unshift File.expand_path('../../../../lib', __FILE__)
|
@@ -11,7 +11,7 @@ Dummy::Application.configure do
|
|
11
11
|
|
12
12
|
# Show full error reports and disable caching
|
13
13
|
config.consider_all_requests_local = true
|
14
|
-
config.action_view.debug_rjs = true
|
14
|
+
config.action_view.debug_rjs = true if ActionView::Base.respond_to?(:debug_rjs=)
|
15
15
|
config.action_controller.perform_caching = false
|
16
16
|
|
17
17
|
# Don't care if the mailer can't send
|
data/test/experiment/ab_test.rb
CHANGED
@@ -35,6 +35,15 @@ class AbTestTest < ActionController::TestCase
|
|
35
35
|
metric "Coolness"
|
36
36
|
end
|
37
37
|
|
38
|
+
def teardown
|
39
|
+
super
|
40
|
+
if RUBY_VERSION == '1.8.7'
|
41
|
+
GC.enable
|
42
|
+
GC.start
|
43
|
+
GC.disable
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
38
47
|
# -- Experiment definition --
|
39
48
|
|
40
49
|
def test_requires_at_least_two_alternatives_per_experiment
|
@@ -53,7 +62,7 @@ class AbTestTest < ActionController::TestCase
|
|
53
62
|
metrics :coolness
|
54
63
|
end
|
55
64
|
end
|
56
|
-
|
65
|
+
|
57
66
|
def test_returning_alternative_by_value
|
58
67
|
new_ab_test :abcd do
|
59
68
|
alternatives :a, :b, :c, :d
|
@@ -137,6 +146,33 @@ class AbTestTest < ActionController::TestCase
|
|
137
146
|
assert_equal 0, alts.map(&:participants).sum
|
138
147
|
end
|
139
148
|
|
149
|
+
# -- on_assignment --
|
150
|
+
|
151
|
+
def test_calls_on_assignment_on_new_assignment
|
152
|
+
on_assignment_called_times = 0
|
153
|
+
new_ab_test :foobar do
|
154
|
+
alternatives "foo", "bar"
|
155
|
+
identify { "6e98ec" }
|
156
|
+
metrics :coolness
|
157
|
+
on_assignment { on_assignment_called_times = on_assignment_called_times+1 }
|
158
|
+
end
|
159
|
+
2.times { experiment(:foobar).choose }
|
160
|
+
assert_equal 1, on_assignment_called_times
|
161
|
+
end
|
162
|
+
|
163
|
+
def test_returns_the_same_alternative_consistently_when_on_assignment_is_set
|
164
|
+
new_ab_test :foobar do
|
165
|
+
alternatives "foo", "bar"
|
166
|
+
identify { "6e98ec" }
|
167
|
+
on_assignment {}
|
168
|
+
metrics :coolness
|
169
|
+
end
|
170
|
+
assert value = experiment(:foobar).choose.value
|
171
|
+
assert_match /foo|bar/, value
|
172
|
+
1000.times do
|
173
|
+
assert_equal value, experiment(:foobar).choose.value
|
174
|
+
end
|
175
|
+
end
|
140
176
|
|
141
177
|
# -- Running experiment --
|
142
178
|
|
@@ -288,7 +324,7 @@ class AbTestTest < ActionController::TestCase
|
|
288
324
|
|
289
325
|
|
290
326
|
# -- Testing with tests --
|
291
|
-
|
327
|
+
|
292
328
|
def test_with_given_choice
|
293
329
|
new_ab_test :simple do
|
294
330
|
alternatives :a, :b, :c
|
@@ -329,7 +365,7 @@ class AbTestTest < ActionController::TestCase
|
|
329
365
|
|
330
366
|
|
331
367
|
# -- Scoring --
|
332
|
-
|
368
|
+
|
333
369
|
def test_scoring
|
334
370
|
new_ab_test :abcd do
|
335
371
|
alternatives :a, :b, :c, :d
|
@@ -601,7 +637,7 @@ This experiment did not run long enough to find a clear winner.
|
|
601
637
|
|
602
638
|
|
603
639
|
# -- Outcome --
|
604
|
-
|
640
|
+
|
605
641
|
def test_completion_outcome
|
606
642
|
new_ab_test :quick do
|
607
643
|
outcome_is { alternatives[1] }
|