vanity 3.1.0 → 4.0.0

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.
Files changed (90) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/linting.yml +28 -0
  3. data/.github/workflows/test.yml +3 -6
  4. data/.rubocop.yml +114 -0
  5. data/.rubocop_todo.yml +67 -0
  6. data/Appraisals +9 -31
  7. data/CHANGELOG +5 -0
  8. data/Gemfile +7 -3
  9. data/Gemfile.lock +31 -3
  10. data/README.md +4 -9
  11. data/Rakefile +25 -24
  12. data/bin/vanity +1 -1
  13. data/doc/configuring.textile +1 -0
  14. data/gemfiles/rails52.gemfile +6 -3
  15. data/gemfiles/rails52.gemfile.lock +34 -9
  16. data/gemfiles/rails60.gemfile +6 -3
  17. data/gemfiles/rails60.gemfile.lock +34 -9
  18. data/gemfiles/rails61.gemfile +6 -3
  19. data/gemfiles/rails61.gemfile.lock +34 -9
  20. data/lib/generators/vanity/migration_generator.rb +5 -7
  21. data/lib/vanity/adapters/abstract_adapter.rb +43 -45
  22. data/lib/vanity/adapters/active_record_adapter.rb +30 -30
  23. data/lib/vanity/adapters/mock_adapter.rb +14 -18
  24. data/lib/vanity/adapters/mongodb_adapter.rb +73 -69
  25. data/lib/vanity/adapters/redis_adapter.rb +19 -27
  26. data/lib/vanity/adapters.rb +1 -1
  27. data/lib/vanity/autoconnect.rb +6 -7
  28. data/lib/vanity/commands/list.rb +7 -7
  29. data/lib/vanity/commands/report.rb +18 -22
  30. data/lib/vanity/configuration.rb +19 -19
  31. data/lib/vanity/connection.rb +12 -14
  32. data/lib/vanity/experiment/ab_test.rb +82 -70
  33. data/lib/vanity/experiment/alternative.rb +3 -5
  34. data/lib/vanity/experiment/base.rb +24 -19
  35. data/lib/vanity/experiment/bayesian_bandit_score.rb +7 -13
  36. data/lib/vanity/experiment/definition.rb +6 -6
  37. data/lib/vanity/frameworks/rails.rb +39 -39
  38. data/lib/vanity/frameworks.rb +2 -2
  39. data/lib/vanity/helpers.rb +1 -1
  40. data/lib/vanity/metric/active_record.rb +21 -19
  41. data/lib/vanity/metric/base.rb +22 -23
  42. data/lib/vanity/metric/google_analytics.rb +6 -9
  43. data/lib/vanity/metric/remote.rb +3 -5
  44. data/lib/vanity/playground.rb +3 -6
  45. data/lib/vanity/vanity.rb +8 -12
  46. data/lib/vanity/version.rb +1 -1
  47. data/test/adapters/active_record_adapter_test.rb +1 -5
  48. data/test/adapters/mock_adapter_test.rb +0 -2
  49. data/test/adapters/mongodb_adapter_test.rb +1 -5
  50. data/test/adapters/redis_adapter_test.rb +2 -3
  51. data/test/adapters/shared_tests.rb +9 -12
  52. data/test/autoconnect_test.rb +3 -3
  53. data/test/cli_test.rb +0 -1
  54. data/test/configuration_test.rb +18 -34
  55. data/test/connection_test.rb +3 -3
  56. data/test/dummy/Rakefile +1 -1
  57. data/test/dummy/app/controllers/use_vanity_controller.rb +12 -8
  58. data/test/dummy/app/mailers/vanity_mailer.rb +3 -3
  59. data/test/dummy/config/application.rb +1 -1
  60. data/test/dummy/config/boot.rb +3 -3
  61. data/test/dummy/config/environment.rb +1 -1
  62. data/test/dummy/config/environments/development.rb +0 -1
  63. data/test/dummy/config/environments/test.rb +1 -1
  64. data/test/dummy/config/initializers/session_store.rb +1 -1
  65. data/test/dummy/config.ru +1 -1
  66. data/test/dummy/script/rails +2 -2
  67. data/test/experiment/ab_test.rb +148 -154
  68. data/test/experiment/base_test.rb +48 -32
  69. data/test/frameworks/rails/action_controller_test.rb +25 -25
  70. data/test/frameworks/rails/action_mailer_test.rb +2 -2
  71. data/test/frameworks/rails/action_view_test.rb +5 -6
  72. data/test/frameworks/rails/rails_test.rb +147 -181
  73. data/test/helper_test.rb +2 -2
  74. data/test/metric/active_record_test.rb +174 -212
  75. data/test/metric/base_test.rb +21 -46
  76. data/test/metric/google_analytics_test.rb +17 -25
  77. data/test/metric/remote_test.rb +7 -10
  78. data/test/playground_test.rb +7 -14
  79. data/test/templates_test.rb +16 -20
  80. data/test/test_helper.rb +28 -29
  81. data/test/vanity_test.rb +4 -10
  82. data/test/web/rails/dashboard_test.rb +21 -21
  83. data/vanity.gemspec +8 -7
  84. metadata +28 -30
  85. data/gemfiles/rails42.gemfile +0 -33
  86. data/gemfiles/rails42.gemfile.lock +0 -265
  87. data/gemfiles/rails42_protected_attributes.gemfile +0 -34
  88. data/gemfiles/rails42_protected_attributes.gemfile.lock +0 -264
  89. data/gemfiles/rails51.gemfile +0 -33
  90. data/gemfiles/rails51.gemfile.lock +0 -285
@@ -10,7 +10,7 @@ module Vanity
10
10
  # Convert z-score to probability.
11
11
  def probability(score)
12
12
  score = score.abs
13
- probability = AbTest::Z_TO_PROBABILITY.find { |z,p| score >= z }
13
+ probability = AbTest::Z_TO_PROBABILITY.find { |z, _p| score >= z }
14
14
  probability ? probability.last : 0
15
15
  end
16
16
 
@@ -48,6 +48,7 @@ module Vanity
48
48
  class << self
49
49
  define_method :default do |*args|
50
50
  raise ArgumentError, "default has already been set to #{@default.inspect}" unless args.empty?
51
+
51
52
  alternative(@default)
52
53
  end
53
54
  end
@@ -69,9 +70,10 @@ module Vanity
69
70
  # has had #save invoked previous to any enabled= calls.
70
71
  def enabled=(bool)
71
72
  return unless @playground.collecting? && active?
73
+
72
74
  if created_at.nil?
73
75
  Vanity.logger.warn(
74
- 'DB has no created_at for this experiment! This most likely means' +
76
+ 'DB has no created_at for this experiment! This most likely means' \
75
77
  'you didn\'t call #save before calling enabled=, which you should.'
76
78
  )
77
79
  else
@@ -146,10 +148,8 @@ module Vanity
146
148
  # metrics :signup
147
149
  # score_method :bayes_bandit_score
148
150
  # end
149
- def score_method(method=nil)
150
- if method
151
- @score_method = method
152
- end
151
+ def score_method(method = nil)
152
+ @score_method = method if method
153
153
  @score_method
154
154
  end
155
155
 
@@ -172,7 +172,7 @@ module Vanity
172
172
  # alternative for experiment without revealing what values are available
173
173
  # (e.g. choosing alternative from HTTP query parameter).
174
174
  def fingerprint(alternative)
175
- Digest::MD5.hexdigest("#{id}:#{alternative.id}")[-10,10]
175
+ Digest::MD5.hexdigest("#{id}:#{alternative.id}")[-10, 10]
176
176
  end
177
177
 
178
178
  # Chooses a value for this experiment. You probably want to use the
@@ -185,10 +185,10 @@ module Vanity
185
185
  #
186
186
  # @example
187
187
  # color = experiment(:which_blue).choose
188
- def choose(request=nil)
188
+ def choose(request = nil)
189
189
  if @playground.collecting?
190
190
  if active?
191
- if enabled?
191
+ if enabled? # rubocop:todo Style/GuardClause
192
192
  return assignment_for_identity(request)
193
193
  else
194
194
  # Show the default if experiment is disabled.
@@ -229,7 +229,7 @@ module Vanity
229
229
  # teardown do
230
230
  # experiment(:green_button).chooses(nil)
231
231
  # end
232
- def chooses(value, request=nil)
232
+ def chooses(value, request = nil)
233
233
  if @playground.collecting?
234
234
  if value.nil?
235
235
  connection.ab_not_showing @id, identity
@@ -238,8 +238,9 @@ module Vanity
238
238
  save_assignment(identity, index, request) unless filter_visitor?(request, identity)
239
239
 
240
240
  raise ArgumentError, "No alternative #{value.inspect} for #{name}" unless index
241
+
241
242
  if (connection.ab_showing(@id, identity) && connection.ab_showing(@id, identity) != index) ||
242
- alternative_for(identity) != index
243
+ alternative_for(identity) != index
243
244
  connection.ab_show(@id, identity, index)
244
245
  end
245
246
  end
@@ -261,12 +262,11 @@ module Vanity
261
262
  end
262
263
  end
263
264
 
264
-
265
265
  # -- Reporting --
266
266
 
267
267
  def calculate_score
268
268
  if respond_to?(score_method)
269
- self.send(score_method)
269
+ send(score_method)
270
270
  else
271
271
  score
272
272
  end
@@ -298,22 +298,24 @@ module Vanity
298
298
  alts.each do |alt|
299
299
  p = alt.measure
300
300
  n = alt.participants
301
- alt.z_score = (p - pc) / ((p * (1-p)/n) + (pc * (1-pc)/nc)).abs ** 0.5
301
+ alt.z_score = (p - pc) / (((p * (1 - p) / n) + (pc * (1 - pc) / nc)).abs**0.5)
302
302
  alt.probability = AbTest.probability(alt.z_score)
303
303
  end
304
304
  # difference is measured from least performant
305
- if least = sorted.find { |alt| alt.measure > 0 }
305
+ if least = sorted.find { |alt| alt.measure > 0 } # rubocop:todo Lint/AssignmentInCondition
306
306
  alts.each do |alt|
307
- if alt.measure > least.measure
308
- alt.difference = (alt.measure - least.measure) / least.measure * 100
309
- end
307
+ alt.difference = (alt.measure - least.measure) / least.measure * 100 if alt.measure > least.measure
310
308
  end
311
309
  end
312
310
  # best alternative is one with highest conversion rate (best shot).
313
311
  # choice alternative can only pick best if we have high probability (>90%).
314
312
  best = sorted.last if sorted.last.measure > 0.0
315
- choice = outcome ? alts[outcome.id] : (best && best.probability >= probability ? best : nil)
316
- Struct.new(:alts, :best, :base, :least, :choice, :method).new(alts, best, base, least, choice, :score)
313
+ choice = if outcome
314
+ alts[outcome.id]
315
+ else
316
+ (best && best.probability >= probability ? best : nil)
317
+ end
318
+ Struct.new(:alts, :best, :base, :least, :choice, :method).new(alts, best, base, least, choice, :score) # rubocop:todo Lint/StructNewOverride
317
319
  end
318
320
 
319
321
  # Scores alternatives based on the current tracking data, using Bayesian
@@ -336,13 +338,13 @@ module Vanity
336
338
  #
337
339
  # The choice alternative is set only if its probability is higher or
338
340
  # equal to the specified probability (default is 90%).
339
- def bayes_bandit_score(probability = 90)
341
+ def bayes_bandit_score(_probability = 90)
340
342
  begin
341
343
  require "backports/1.9.1/kernel/define_singleton_method" if RUBY_VERSION < "1.9"
342
344
  require "integration"
343
345
  require "rubystats"
344
346
  rescue LoadError
345
- fail("to use bayes_bandit_score, install integration and rubystats gems")
347
+ raise("to use bayes_bandit_score, install integration and rubystats gems")
346
348
  end
347
349
 
348
350
  begin
@@ -358,12 +360,12 @@ module Vanity
358
360
  # array of claims.
359
361
  def conclusion(score = score())
360
362
  claims = []
361
- participants = score.alts.inject(0) { |t,alt| t + alt.participants }
363
+ participants = score.alts.inject(0) { |t, alt| t + alt.participants }
362
364
  claims << if participants.zero?
363
- I18n.t('vanity.no_participants')
364
- else
365
- I18n.t('vanity.experiment_participants', :count=>participants)
366
- end
365
+ I18n.t('vanity.no_participants')
366
+ else
367
+ I18n.t('vanity.experiment_participants', count: participants)
368
+ end
367
369
  # only interested in sorted alternatives with conversion
368
370
  sorted = score.alts.select { |alt| alt.measure > 0.0 }.sort_by(&:measure).reverse
369
371
  if sorted.size > 1
@@ -371,46 +373,45 @@ module Vanity
371
373
  # then alternatives with no conversion.
372
374
  sorted |= score.alts
373
375
  # we want a result that's clearly better than 2nd best.
374
- best, second = sorted[0], sorted[1]
376
+ best = sorted[0]
377
+ second = sorted[1]
375
378
  if best.measure > second.measure
376
379
  diff = ((best.measure - second.measure) / second.measure * 100).round
377
- better = I18n.t('vanity.better_alternative_than', :probability=>diff.to_i, :alternative=> second.name) if diff > 0
378
- claims << I18n.t('vanity.best_alternative_measure', :best_alternative=>best.name, :measure=>'%.1f' % (best.measure * 100), :better_than=>better)
379
- if score.method == :bayes_bandit_score
380
- if best.probability >= 90
381
- claims << I18n.t('vanity.best_alternative_probability', :probability=>score.best.probability.to_i)
382
- else
383
- claims << I18n.t('vanity.low_result_confidence')
384
- end
385
- else
386
- if best.probability >= 90
387
- claims << I18n.t('vanity.best_alternative_is_significant', :probability=>score.best.probability.to_i)
388
- else
389
- claims << I18n.t('vanity.result_isnt_significant')
390
- end
391
- end
380
+ better = I18n.t('vanity.better_alternative_than', probability: diff.to_i, alternative: second.name) if diff > 0
381
+ claims << I18n.t('vanity.best_alternative_measure', best_alternative: best.name, measure: format('%.1f', (best.measure * 100)), better_than: better)
382
+ claims << if score.method == :bayes_bandit_score
383
+ if best.probability >= 90
384
+ I18n.t('vanity.best_alternative_probability', probability: score.best.probability.to_i)
385
+ else
386
+ I18n.t('vanity.low_result_confidence')
387
+ end
388
+ elsif best.probability >= 90
389
+ I18n.t('vanity.best_alternative_is_significant', probability: score.best.probability.to_i)
390
+ else
391
+ I18n.t('vanity.result_isnt_significant')
392
+ end
392
393
  sorted.delete best
393
394
  end
394
395
  sorted.each do |alt|
395
- if alt.measure > 0.0
396
- claims << I18n.t('vanity.converted_percentage', :alternative=>alt.name.sub(/^\w/, &:upcase), :percentage=>'%.1f' % (alt.measure * 100))
397
- else
398
- claims << I18n.t('vanity.didnt_convert', :alternative=>alt.name.sub(/^\w/, &:upcase))
399
- end
396
+ claims << if alt.measure > 0.0
397
+ I18n.t('vanity.converted_percentage', alternative: alt.name.sub(/^\w/, &:upcase), percentage: format('%.1f', (alt.measure * 100)))
398
+ else
399
+ I18n.t('vanity.didnt_convert', alternative: alt.name.sub(/^\w/, &:upcase))
400
+ end
400
401
  end
401
402
  else
402
403
  claims << I18n.t('vanity.no_clear_winner')
403
404
  end
404
- claims << I18n.t('vanity.selected_as_best', :alternative=>score.choice.name.sub(/^\w/, &:upcase)) if score.choice
405
+ claims << I18n.t('vanity.selected_as_best', alternative: score.choice.name.sub(/^\w/, &:upcase)) if score.choice
405
406
  claims
406
407
  end
407
408
 
408
409
  # -- Unequal probability assignments --
409
410
 
410
- def set_alternative_probabilities(alternative_probabilities)
411
+ def set_alternative_probabilities(alternative_probabilities) # rubocop:todo Naming/AccessorMethodName
411
412
  # create @use_probabilities as a function to go from [0,1] to outcome
412
413
  cumulative_probability = 0.0
413
- new_probabilities = alternative_probabilities.map {|am| [am, (cumulative_probability += am.probability)/100.0]}
414
+ new_probabilities = alternative_probabilities.map { |am| [am, (cumulative_probability += am.probability) / 100.0] }
414
415
  @use_probabilities = new_probabilities
415
416
  end
416
417
 
@@ -425,7 +426,7 @@ module Vanity
425
426
  # end
426
427
  #
427
428
  # puts "The experiment will automatically rebalance after every " + experiment(:simple).description + " users are assigned."
428
- def rebalance_frequency(rf = nil)
429
+ def rebalance_frequency(rf = nil) # rubocop:todo Naming/MethodParameterName
429
430
  if rf
430
431
  @assignments_since_rebalancing = 0
431
432
  @rebalance_frequency = rf
@@ -437,10 +438,9 @@ module Vanity
437
438
  # Force experiment to rebalance.
438
439
  def rebalance!
439
440
  return unless @playground.collecting?
441
+
440
442
  score_results = bayes_bandit_score
441
- if score_results.method == :bayes_bandit_score
442
- set_alternative_probabilities score_results.alts
443
- end
443
+ set_alternative_probabilities score_results.alts if score_results.method == :bayes_bandit_score
444
444
  end
445
445
 
446
446
  # -- Completion --
@@ -461,12 +461,14 @@ module Vanity
461
461
  def outcome_is(&block)
462
462
  raise ArgumentError, "Missing block" unless block
463
463
  raise "outcome_is already called on this experiment" if defined?(@outcome_is)
464
+
464
465
  @outcome_is = block
465
466
  end
466
467
 
467
468
  # Alternative chosen when this experiment completed.
468
469
  def outcome
469
470
  return unless @playground.collecting?
471
+
470
472
  outcome = connection.ab_get_outcome(@id)
471
473
  outcome && alternatives[outcome]
472
474
  end
@@ -474,6 +476,7 @@ module Vanity
474
476
  def complete!(outcome = nil)
475
477
  # This statement is equivalent to: return unless collecting?
476
478
  return unless @playground.collecting? && active?
479
+
477
480
  self.enabled = false
478
481
  super
479
482
 
@@ -481,8 +484,8 @@ module Vanity
481
484
  if defined?(@outcome_is)
482
485
  begin
483
486
  result = @outcome_is.call
484
- outcome = result.id if Alternative === result && result.experiment == self
485
- rescue => e
487
+ outcome = result.id if result.is_a?(Alternative) && result.experiment == self
488
+ rescue StandardError => e
486
489
  Vanity.logger.warn("Error in AbTest#complete!: #{e}")
487
490
  end
488
491
  else
@@ -494,7 +497,6 @@ module Vanity
494
497
  connection.ab_set_outcome(@id, outcome || 0)
495
498
  end
496
499
 
497
-
498
500
  # -- Store/validate --
499
501
 
500
502
  def destroy
@@ -505,6 +507,7 @@ module Vanity
505
507
  # clears all collected data for the experiment
506
508
  def reset
507
509
  return unless @playground.collecting?
510
+
508
511
  connection.destroy_experiment(@id)
509
512
  connection.set_experiment_created_at(@id, Time.now)
510
513
  @outcome = @completed_at = nil
@@ -523,7 +526,8 @@ module Vanity
523
526
  end
524
527
  @saved = true
525
528
  true_false unless defined?(@alternatives)
526
- fail "Experiment #{name} needs at least two alternatives" unless @alternatives.size >= 2
529
+ raise "Experiment #{name} needs at least two alternatives" unless @alternatives.size >= 2
530
+
527
531
  if !@is_default_set
528
532
  default(@alternatives.first)
529
533
  Vanity.logger.warn("No default alternative specified; choosing #{@default} as default.")
@@ -540,17 +544,23 @@ module Vanity
540
544
  @metrics = [default_metric]
541
545
  end
542
546
  @metrics.each do |metric|
543
- metric.hook(&method(:track!))
547
+ metric.hook(&method(:track!)) # rubocop:todo Performance/MethodObjectAsBlock
544
548
  end
545
549
  end
546
550
 
547
551
  # Called via a hook by the associated metric.
548
- def track!(metric_id, timestamp, count, *args)
552
+ def track!(_metric_id, _timestamp, count, *args)
549
553
  return unless active? && enabled?
554
+
550
555
  identity = args.last[:identity] if args.last.is_a?(Hash)
551
- identity ||= identity() rescue nil
552
- if identity
556
+ identity ||= begin
557
+ identity()
558
+ rescue StandardError
559
+ nil
560
+ end
561
+ if identity # rubocop:todo Style/GuardClause
553
562
  return if connection.ab_showing(@id, identity)
563
+
554
564
  index = alternative_for(identity)
555
565
  connection.ab_add_conversion(@id, index, identity, count)
556
566
  check_completion!
@@ -561,7 +571,7 @@ module Vanity
561
571
  # launched too late.
562
572
  # -- Reid Hoffman, founder of LinkedIn
563
573
 
564
- protected
574
+ protected
565
575
 
566
576
  # Used for testing.
567
577
  def fake(values)
@@ -570,11 +580,13 @@ module Vanity
570
580
  participants.times do |identity|
571
581
  index = @alternatives.index(value)
572
582
  raise ArgumentError, "No alternative #{value.inspect} for #{name}" unless index
583
+
573
584
  connection.ab_add_participant @id, index, "#{index}:#{identity}"
574
585
  end
575
586
  conversions.times do |identity|
576
587
  index = @alternatives.index(value)
577
588
  raise ArgumentError, "No alternative #{value.inspect} for #{name}" unless index
589
+
578
590
  connection.ab_add_conversion @id, index, "#{index}:#{identity}"
579
591
  end
580
592
  end
@@ -605,7 +617,7 @@ module Vanity
605
617
  return existing_assignment if existing_assignment
606
618
 
607
619
  if @use_probabilities
608
- random_outcome = rand()
620
+ random_outcome = rand
609
621
  @use_probabilities.each do |alternative, max_prob|
610
622
  return alternative.id if random_outcome < max_prob
611
623
  end
@@ -651,7 +663,7 @@ module Vanity
651
663
 
652
664
  def rebalance_if_necessary!
653
665
  # if we are rebalancing probabilities, keep track of how long it has been since we last rebalanced
654
- if defined?(@rebalance_frequency) && @rebalance_frequency
666
+ if defined?(@rebalance_frequency) && @rebalance_frequency # rubocop:todo Style/GuardClause
655
667
  @assignments_since_rebalancing += 1
656
668
  if @assignments_since_rebalancing >= @rebalance_frequency
657
669
  @assignments_since_rebalancing = 0
@@ -660,13 +672,13 @@ module Vanity
660
672
  end
661
673
  end
662
674
 
663
- def has_alternative_weights?(args)
675
+ def has_alternative_weights?(args) # rubocop:todo Naming/PredicateName
664
676
  (!defined?(@alternatives) || @alternatives.nil?) && args.size == 1 && args[0].is_a?(Hash)
665
677
  end
666
678
 
667
679
  def build_alternatives_with_weights(args)
668
680
  @alternatives = args[0]
669
- sum_of_probability = @alternatives.values.reduce(0) { |a,b| a+b }
681
+ sum_of_probability = @alternatives.values.reduce(0) { |a, b| a + b }
670
682
  cumulative_probability = 0.0
671
683
  @use_probabilities = []
672
684
  result = []
@@ -691,9 +703,9 @@ module Vanity
691
703
  avg = 50.0
692
704
  # Returns array of [z-score, percentage]
693
705
  norm_dist = []
694
- (0.0..3.1).step(0.01) { |x| norm_dist << [x, avg += 1 / Math.sqrt(2 * Math::PI) * Math::E ** (-x ** 2 / 2)] }
706
+ (0.0..3.1).step(0.01) { |x| norm_dist << [x, avg += 1 / Math.sqrt(2 * Math::PI) * (Math::E**(-x**2 / 2))] }
695
707
  # We're really only interested in 90%, 95%, 99% and 99.9%.
696
- Z_TO_PROBABILITY = [90, 95, 99, 99.9].map { |pct| [norm_dist.find { |x,a| a >= pct }.first, pct] }.reverse
708
+ Z_TO_PROBABILITY = [90, 95, 99, 99.9].map { |pct| [norm_dist.find { |_x, a| a >= pct }.first, pct] }.reverse
697
709
  end
698
710
  end
699
711
 
@@ -1,13 +1,11 @@
1
1
  module Vanity
2
2
  module Experiment
3
-
4
3
  # One of several alternatives in an A/B test (see AbTest#alternatives).
5
4
  class Alternative
6
-
7
- def initialize(experiment, id, value) #, participants, converted, conversions)
5
+ def initialize(experiment, id, value) # , participants, converted, conversions)
8
6
  @experiment = experiment
9
7
  @id = id
10
- @name = I18n.t('vanity.option_number', :char=>(@id + 65).chr.upcase)
8
+ @name = I18n.t('vanity.option_number', char: (@id + 65).chr.upcase)
11
9
  @value = value
12
10
  end
13
11
 
@@ -56,7 +54,7 @@ module Vanity
56
54
 
57
55
  # Conversion rate calculated as converted/participants
58
56
  def conversion_rate
59
- @conversion_rate ||= (participants > 0 ? converted.to_f/participants.to_f : 0.0)
57
+ @conversion_rate ||= (participants > 0 ? converted.to_f / participants : 0.0)
60
58
  end
61
59
 
62
60
  # The measure we use to order (sort) alternatives and decide which one
@@ -4,40 +4,41 @@ module Vanity
4
4
  module Experiment
5
5
  # Base class that all experiment types are derived from.
6
6
  class Base
7
-
8
7
  class << self
9
8
  # Returns the type of this class as a symbol (e.g. AbTest becomes
10
9
  # ab_test).
11
10
  def type
12
- name.split("::").last.gsub(/([a-z])([A-Z])/) { "#{$1}_#{$2}" }.gsub(/([A-Z])([A-Z][a-z])/) { "#{$1}_#{$2}" }.downcase
11
+ name.split("::").last.gsub(/([a-z])([A-Z])/) { "#{Regexp.last_match(1)}_#{Regexp.last_match(2)}" }.gsub(/([A-Z])([A-Z][a-z])/) { "#{Regexp.last_match(1)}_#{Regexp.last_match(2)}" }.downcase
13
12
  end
14
13
 
15
14
  # Playground uses this to load experiment definitions.
16
15
  def load(playground, stack, file)
17
- fail "Circular dependency detected: #{stack.join('=>')}=>#{file}" if stack.include?(file)
16
+ raise "Circular dependency detected: #{stack.join('=>')}=>#{file}" if stack.include?(file)
17
+
18
18
  source = File.read(file)
19
19
  stack.push file
20
20
  id = File.basename(file, ".rb").downcase.gsub(/\W/, "_").to_sym
21
21
  context = Object.new
22
22
  context.instance_eval do
23
23
  extend Definition
24
- experiment = eval(source, context.new_binding(playground, id), file)
25
- fail NameError.new("Expected #{file} to define experiment #{id}", id) unless playground.experiments[id]
24
+ experiment = eval(source, context.new_binding(playground, id), file) # rubocop:todo Security/Eval
25
+ raise NameError.new("Expected #{file} to define experiment #{id}", id) unless playground.experiments[id]
26
+
26
27
  return experiment
27
28
  end
28
- rescue
29
+ rescue StandardError
29
30
  error = NameError.exception($!.message, id)
30
31
  error.set_backtrace $!.backtrace
31
32
  raise error
32
33
  ensure
33
34
  stack.pop
34
35
  end
35
-
36
36
  end
37
37
 
38
38
  def initialize(playground, id, name, options = nil)
39
39
  @playground = playground
40
- @id, @name = id.to_sym, name
40
+ @id = id.to_sym
41
+ @name = name
41
42
  @options = options || {}
42
43
  @identify_block = method(:default_identify)
43
44
  @on_assignment_block = nil
@@ -46,7 +47,7 @@ module Vanity
46
47
  # Human readable experiment name (first argument you pass when creating a
47
48
  # new experiment).
48
49
  attr_reader :name
49
- alias :to_s :name
50
+ alias to_s name
50
51
 
51
52
  # Unique identifier, derived from name experiment name, e.g. "Green
52
53
  # Button" becomes :green_button.
@@ -78,7 +79,8 @@ module Vanity
78
79
  # end
79
80
  # end
80
81
  def identify(&block)
81
- fail "Missing block" unless block
82
+ raise "Missing block" unless block
83
+
82
84
  @identify_block = block
83
85
  end
84
86
 
@@ -92,7 +94,8 @@ module Vanity
92
94
  # end
93
95
  # end
94
96
  def on_assignment(&block)
95
- fail "Missing block" unless block
97
+ raise "Missing block" unless block
98
+
96
99
  @on_assignment_block = block
97
100
  end
98
101
 
@@ -109,7 +112,6 @@ module Vanity
109
112
  @description if defined?(@description)
110
113
  end
111
114
 
112
-
113
115
  # -- Experiment completion --
114
116
 
115
117
  # Define experiment completion condition. For example:
@@ -119,15 +121,17 @@ module Vanity
119
121
  def complete_if(&block)
120
122
  raise ArgumentError, "Missing block" unless block
121
123
  raise "complete_if already called on this experiment" if defined?(@complete_block)
124
+
122
125
  @complete_block = block
123
126
  end
124
127
 
125
128
  # Force experiment to complete.
126
129
  # @param optional integer id of the alternative that is the decided
127
130
  # outcome of the experiment
128
- def complete!(outcome = nil)
131
+ def complete!(_outcome = nil)
129
132
  @playground.logger.info "vanity: completed experiment #{id}"
130
133
  return unless @playground.collecting?
134
+
131
135
  connection.set_experiment_completed_at @id, Time.now
132
136
  @completed_at = connection.get_experiment_completed_at(@id)
133
137
  end
@@ -153,6 +157,7 @@ module Vanity
153
157
  # Called by Playground to save the experiment definition.
154
158
  def save
155
159
  return unless @playground.collecting?
160
+
156
161
  connection.set_experiment_created_at @id, Time.now
157
162
  end
158
163
 
@@ -165,12 +170,13 @@ module Vanity
165
170
  # end
166
171
  #
167
172
  def reject(&block)
168
- fail "Missing block" unless block
173
+ raise "Missing block" unless block
169
174
  raise "filter already called on this experiment" if @request_filter_block
175
+
170
176
  @request_filter_block = block
171
177
  end
172
178
 
173
- protected
179
+ protected
174
180
 
175
181
  def identity
176
182
  @identify_block.call(Vanity.context)
@@ -179,16 +185,17 @@ module Vanity
179
185
  def default_identify(context)
180
186
  raise "No Vanity.context" unless context
181
187
  raise "Vanity.context does not respond to vanity_identity" unless context.respond_to?(:vanity_identity, true)
188
+
182
189
  context.send(:vanity_identity) or raise "Vanity.context.vanity_identity - no identity"
183
190
  end
184
191
 
185
192
  # Derived classes call this after state changes that may lead to
186
193
  # experiment completing.
187
194
  def check_completion!
188
- if defined?(@complete_block) && @complete_block
195
+ if defined?(@complete_block) && @complete_block # rubocop:todo Style/GuardClause
189
196
  begin
190
197
  complete! if @complete_block.call
191
- rescue => e
198
+ rescue StandardError => e
192
199
  Vanity.logger.warn("Error in Vanity::Experiment::Base: #{e}")
193
200
  end
194
201
  end
@@ -206,11 +213,9 @@ module Vanity
206
213
  def connection
207
214
  @playground.connection
208
215
  end
209
-
210
216
  end
211
217
  end
212
218
 
213
219
  class NoExperimentError < NameError
214
220
  end
215
-
216
221
  end
@@ -7,13 +7,13 @@ module Vanity
7
7
  class BayesianBanditScore < Score
8
8
  DEFAULT_PROBABILITY = 90
9
9
 
10
- def initialize(alternatives, outcome)
10
+ def initialize(alternatives, outcome) # rubocop:todo Lint/MissingSuper
11
11
  @alternatives = alternatives
12
12
  @outcome = outcome
13
13
  @method = :bayes_bandit_score
14
14
  end
15
15
 
16
- def calculate!(probability=DEFAULT_PROBABILITY)
16
+ def calculate!(probability = DEFAULT_PROBABILITY)
17
17
  # sort by conversion rate to find second best
18
18
  @alts = @alternatives.sort_by(&:measure)
19
19
  @base = @alts[-2]
@@ -36,8 +36,6 @@ module Vanity
36
36
  alternatives[@outcome.id]
37
37
  elsif best && best.probability >= probability
38
38
  best
39
- else
40
- nil
41
39
  end
42
40
  end
43
41
 
@@ -54,24 +52,22 @@ module Vanity
54
52
  alternatives.map do |alternative|
55
53
  x = alternative.converted
56
54
  n = alternative.participants
57
- Rubystats::BetaDistribution.new(x+1, n-x+1)
55
+ Rubystats::BetaDistribution.new(x + 1, n - x + 1)
58
56
  end
59
57
  end
60
58
 
61
59
  def probability_alternative_is_best(alternative_being_examined, all_alternatives)
62
- Integration.integrate(0, 1, :tolerance=>1e-4) do |z|
60
+ Integration.integrate(0, 1, tolerance: 1e-4) do |z|
63
61
  pdf_alternative_is_best(z, alternative_being_examined, all_alternatives)
64
62
  end
65
63
  end
66
64
 
67
- def pdf_alternative_is_best(z, alternative_being_examined, all_alternatives)
65
+ def pdf_alternative_is_best(z, alternative_being_examined, all_alternatives) # rubocop:todo Naming/MethodParameterName
68
66
  # get the pdf for this alternative at z
69
67
  pdf = alternative_being_examined.pdf(z)
70
68
  # now multiply by the probability that all the other alternatives are lower
71
69
  all_alternatives.each do |alternative|
72
- if alternative != alternative_being_examined
73
- pdf = pdf * alternative.cdf(z)
74
- end
70
+ pdf *= alternative.cdf(z) if alternative != alternative_being_examined
75
71
  end
76
72
  pdf
77
73
  end
@@ -81,9 +77,7 @@ module Vanity
81
77
  least = alternatives.find { |alternative| alternative.measure > 0 }
82
78
  if least
83
79
  alternatives.each do |alternative|
84
- if alternative.measure > least.measure
85
- alternative.difference = (alternative.measure - least.measure) / least.measure * 100
86
- end
80
+ alternative.difference = (alternative.measure - least.measure) / least.measure * 100 if alternative.measure > least.measure
87
81
  end
88
82
  end
89
83
  least