vanity 3.1.0 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
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