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.
@@ -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
- index = connection.ab_showing(@id, identity)
202
- unless index
203
- index = alternative_for(identity)
204
- if !@playground.using_js?
205
- connection.ab_add_participant @id, index, identity
206
- check_completion!
207
- end
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
- alternative_for(identity) != index
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
- @_vanity_experiments ||= {}
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
- return experiments
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
 
@@ -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
@@ -1,5 +1,5 @@
1
1
  module Vanity
2
- VERSION = "1.8.1"
2
+ VERSION = "1.8.2"
3
3
 
4
4
  module Version
5
5
  version = VERSION.to_s.split(".").map { |i| i.to_i }
@@ -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
@@ -1,10 +1,8 @@
1
1
  require 'rubygems'
2
- gemfile = File.expand_path('../../../../Gemfile', __FILE__)
3
2
 
4
- if File.exist?(gemfile)
5
- ENV['BUNDLE_GEMFILE'] = gemfile
6
- require 'bundler'
7
- Bundler.setup
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
@@ -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] }