vanity 2.2.10 → 3.1.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 (58) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/test.yml +55 -0
  3. data/Appraisals +24 -20
  4. data/CHANGELOG +19 -1
  5. data/Gemfile +2 -1
  6. data/Gemfile.lock +23 -15
  7. data/README.md +14 -16
  8. data/gemfiles/rails42.gemfile +6 -6
  9. data/gemfiles/rails42.gemfile.lock +95 -86
  10. data/gemfiles/rails42_protected_attributes.gemfile +6 -6
  11. data/gemfiles/rails42_protected_attributes.gemfile.lock +90 -81
  12. data/gemfiles/{rails5.gemfile → rails51.gemfile} +6 -6
  13. data/gemfiles/rails51.gemfile.lock +285 -0
  14. data/gemfiles/{rails41.gemfile → rails52.gemfile} +6 -6
  15. data/gemfiles/rails52.gemfile.lock +295 -0
  16. data/gemfiles/{rails32.gemfile → rails60.gemfile} +6 -9
  17. data/gemfiles/rails60.gemfile.lock +293 -0
  18. data/gemfiles/rails61.gemfile +33 -0
  19. data/gemfiles/rails61.gemfile.lock +293 -0
  20. data/lib/generators/templates/{add_participants_unique_index_migration.rb → add_participants_unique_index_migration.rb.erb} +1 -1
  21. data/lib/generators/templates/{add_unique_indexes_migration.rb → add_unique_indexes_migration.rb.erb} +1 -1
  22. data/lib/generators/templates/{vanity_migration.rb → vanity_migration.rb.erb} +1 -1
  23. data/lib/generators/vanity/migration_generator.rb +34 -0
  24. data/lib/vanity/adapters/active_record_adapter.rb +1 -1
  25. data/lib/vanity/adapters/redis_adapter.rb +20 -20
  26. data/lib/vanity/commands/report.rb +4 -3
  27. data/lib/vanity/configuration.rb +4 -0
  28. data/lib/vanity/experiment/ab_test.rb +16 -11
  29. data/lib/vanity/frameworks/rails.rb +20 -9
  30. data/lib/vanity/locales/vanity.ru.yml +50 -0
  31. data/lib/vanity/templates/_experiment.erb +3 -3
  32. data/lib/vanity/templates/_experiments.erb +1 -1
  33. data/lib/vanity/templates/_metrics.erb +1 -1
  34. data/lib/vanity/templates/_report.erb +2 -2
  35. data/lib/vanity/version.rb +1 -1
  36. data/test/adapters/redis_adapter_test.rb +8 -12
  37. data/test/adapters/shared_tests.rb +7 -6
  38. data/test/commands/report_test.rb +13 -1
  39. data/test/configuration_test.rb +15 -2
  40. data/test/dummy/app/mailers/vanity_mailer.rb +3 -1
  41. data/test/dummy/config/initializers/secret_token.rb +5 -2
  42. data/test/dummy/config/routes.rb +17 -3
  43. data/test/experiment/ab_test.rb +43 -3
  44. data/test/frameworks/rails/action_controller_test.rb +12 -6
  45. data/test/frameworks/rails/action_mailer_test.rb +0 -1
  46. data/test/metric/active_record_test.rb +8 -2
  47. data/test/playground_test.rb +0 -1
  48. data/test/test_helper.rb +57 -10
  49. data/test/web/rails/dashboard_test.rb +19 -10
  50. data/vanity.gemspec +1 -1
  51. metadata +20 -21
  52. data/.travis.yml +0 -33
  53. data/gemfiles/rails32.gemfile.lock +0 -242
  54. data/gemfiles/rails41.gemfile.lock +0 -230
  55. data/gemfiles/rails5.gemfile.lock +0 -256
  56. data/lib/generators/vanity/add_participants_unique_index_generator.rb +0 -15
  57. data/lib/generators/vanity/add_unique_indexes_generator.rb +0 -15
  58. data/lib/generators/vanity_generator.rb +0 -15
@@ -18,7 +18,7 @@ module Vanity
18
18
  end
19
19
 
20
20
  def valid_redis_namespace_version?
21
- Gem.loaded_specs['redis'].version >= Gem::Version.create('1.1.0')
21
+ Gem.loaded_specs['redis-namespace'].version >= Gem::Version.create('1.1.0')
22
22
  end
23
23
  end
24
24
 
@@ -41,11 +41,7 @@ module Vanity
41
41
 
42
42
  def disconnect!
43
43
  if redis
44
- begin
45
- redis.client.disconnect
46
- rescue Exception => e
47
- Vanity.logger.warn("Error while disconnecting from redis: #{e.message}")
48
- end
44
+ redis.disconnect!
49
45
  end
50
46
  @redis = nil
51
47
  end
@@ -72,7 +68,7 @@ module Vanity
72
68
  # -- Metrics --
73
69
 
74
70
  def get_metric_last_update_at(metric)
75
- last_update_at = @metrics["#{metric}:last_update_at"]
71
+ last_update_at = @metrics.get("#{metric}:last_update_at")
76
72
  last_update_at && Time.at(last_update_at.to_i)
77
73
  end
78
74
 
@@ -81,7 +77,7 @@ module Vanity
81
77
  values.each_with_index do |v,i|
82
78
  @metrics.incrby "#{metric}:#{timestamp.to_date}:value:#{i}", v
83
79
  end
84
- @metrics["#{metric}:last_update_at"] = Time.now.to_i
80
+ @metrics.set("#{metric}:last_update_at", Time.now.to_i)
85
81
  end
86
82
  end
87
83
 
@@ -98,7 +94,7 @@ module Vanity
98
94
  # -- Experiments --
99
95
 
100
96
  def experiment_persisted?(experiment)
101
- !!@experiments["#{experiment}:created_at"]
97
+ !!@experiments.get("#{experiment}:created_at")
102
98
  end
103
99
 
104
100
  def set_experiment_created_at(experiment, time)
@@ -108,7 +104,7 @@ module Vanity
108
104
  end
109
105
 
110
106
  def get_experiment_created_at(experiment)
111
- created_at = @experiments["#{experiment}:created_at"]
107
+ created_at = @experiments.get("#{experiment}:created_at")
112
108
  created_at && Time.at(created_at.to_i)
113
109
  end
114
110
 
@@ -117,7 +113,7 @@ module Vanity
117
113
  end
118
114
 
119
115
  def get_experiment_completed_at(experiment)
120
- completed_at = @experiments["#{experiment}:completed_at"]
116
+ completed_at = @experiments.get("#{experiment}:completed_at")
121
117
  completed_at && Time.at(completed_at.to_i)
122
118
  end
123
119
 
@@ -134,7 +130,7 @@ module Vanity
134
130
  end
135
131
 
136
132
  def is_experiment_enabled?(experiment)
137
- value = @experiments["#{experiment}:enabled"]
133
+ value = @experiments.get("#{experiment}:enabled")
138
134
  if Vanity.configuration.experiments_start_enabled
139
135
  value != 'false'
140
136
  else
@@ -145,20 +141,20 @@ module Vanity
145
141
  def ab_counts(experiment, alternative)
146
142
  {
147
143
  :participants => @experiments.scard("#{experiment}:alts:#{alternative}:participants").to_i,
148
- :converted => @experiments.scard("#{experiment}:alts:#{alternative}:converted").to_i,
149
- :conversions => @experiments["#{experiment}:alts:#{alternative}:conversions"].to_i
144
+ :converted => @experiments.scard("#{experiment}:alts:#{alternative}:converted").to_i,
145
+ :conversions => @experiments.get("#{experiment}:alts:#{alternative}:conversions").to_i
150
146
  }
151
147
  end
152
148
 
153
149
  def ab_show(experiment, identity, alternative)
154
150
  call_redis_with_failover do
155
- @experiments["#{experiment}:participant:#{identity}:show"] = alternative
151
+ @experiments.set("#{experiment}:participant:#{identity}:show", alternative)
156
152
  end
157
153
  end
158
154
 
159
155
  def ab_showing(experiment, identity)
160
156
  call_redis_with_failover do
161
- alternative = @experiments["#{experiment}:participant:#{identity}:show"]
157
+ alternative = @experiments.get("#{experiment}:participant:#{identity}:show")
162
158
  alternative && alternative.to_i
163
159
  end
164
160
  end
@@ -212,7 +208,7 @@ module Vanity
212
208
  end
213
209
 
214
210
  def ab_get_outcome(experiment)
215
- alternative = @experiments["#{experiment}:outcome"]
211
+ alternative = @experiments.get("#{experiment}:outcome")
216
212
  alternative && alternative.to_i
217
213
  end
218
214
 
@@ -221,9 +217,13 @@ module Vanity
221
217
  end
222
218
 
223
219
  def destroy_experiment(experiment)
224
- @experiments.del "#{experiment}:outcome", "#{experiment}:created_at", "#{experiment}:completed_at"
225
- alternatives = @experiments.keys("#{experiment}:alts:*")
226
- @experiments.del(*alternatives) unless alternatives.empty?
220
+ cursor = nil
221
+
222
+ while cursor != "0" do
223
+ cursor, keys = @experiments.scan(cursor || "0", match: "#{experiment}:*")
224
+
225
+ @experiments.del(*keys) unless keys.empty?
226
+ end
227
227
  end
228
228
 
229
229
  protected
@@ -11,7 +11,7 @@ module Vanity
11
11
  def render(path_or_options, locals = {})
12
12
  if path_or_options.respond_to?(:keys)
13
13
  render_erb(
14
- path_or_options[:file] || path_or_options[:partial],
14
+ path_or_options[:template] || path_or_options[:partial],
15
15
  path_or_options[:locals]
16
16
  )
17
17
  else
@@ -54,9 +54,10 @@ module Vanity
54
54
  struct = Struct.new(*keys)
55
55
  struct.send :include, Render
56
56
  locals = struct.new(*locals.values_at(*keys))
57
+ path = "#{Vanity.template(path)}.erb" unless path =~ /\/.*\.erb\z/
57
58
  dir, base = File.split(path)
58
59
  path = File.join(dir, partialize(base))
59
- erb = ERB.new(File.read("#{path}.erb"), nil, '<>')
60
+ erb = ERB.new(File.read(path), nil, '<>')
60
61
  erb.filename = path
61
62
  erb.result(locals.instance_eval { binding })
62
63
  end
@@ -78,7 +79,7 @@ module Vanity
78
79
  # Generate an HTML report. Outputs to the named file, or stdout with no
79
80
  # arguments.
80
81
  def report(output = nil)
81
- html = render(Vanity.template("report"),
82
+ html = render(Vanity.template("_report.erb"),
82
83
  :experiments=>Vanity.playground.experiments,
83
84
  :experiments_persisted=>Vanity.playground.experiments_persisted?,
84
85
  :metrics=>Vanity.playground.metrics
@@ -49,6 +49,7 @@ module Vanity
49
49
  on_datastore_error: ->(error, klass, method, arguments) {
50
50
  default_on_datastore_error(error, klass, method, arguments)
51
51
  },
52
+ on_assignment: nil,
52
53
  request_filter: ->(request) { default_request_filter(request) },
53
54
  templates_path: File.expand_path(File.join(File.dirname(__FILE__), 'templates')),
54
55
  use_js: false,
@@ -181,6 +182,9 @@ module Vanity
181
182
  # Cookie path. If true, cookie will not be available to JS. By default false.
182
183
  attr_writer :cookie_httponly
183
184
 
185
+ # Default callback on assigment
186
+ attr_writer :on_assignment
187
+
184
188
  # We independently list each attr_accessor to includes docs, otherwise
185
189
  # something like DEFAULTS.each { |key, value| attr_accessor key } would be
186
190
  # shorter.
@@ -601,9 +601,10 @@ module Vanity
601
601
  # identity, and randomly distributed alternatives for each identity (in the
602
602
  # same experiment).
603
603
  def alternative_for(identity)
604
+ existing_assignment = connection.ab_assigned @id, identity
605
+ return existing_assignment if existing_assignment
606
+
604
607
  if @use_probabilities
605
- existing_assignment = connection.ab_assigned @id, identity
606
- return existing_assignment if existing_assignment
607
608
  random_outcome = rand()
608
609
  @use_probabilities.each do |alternative, max_prob|
609
610
  return alternative.id if random_outcome < max_prob
@@ -616,13 +617,15 @@ module Vanity
616
617
  # Saves the assignment of an alternative to a person and performs the
617
618
  # necessary housekeeping. Ignores repeat identities and filters using
618
619
  # Playground#request_filter.
619
- def save_assignment(identity, index, request)
620
+ def save_assignment(identity, index, _request)
620
621
  return if index == connection.ab_showing(@id, identity)
622
+ return if connection.ab_seen @id, identity, index
621
623
 
622
- call_on_assignment_if_available(identity, index)
623
624
  rebalance_if_necessary!
624
625
 
625
626
  connection.ab_add_participant(@id, index, identity)
627
+ call_on_assignment_if_available(identity, index)
628
+
626
629
  check_completion!
627
630
  end
628
631
 
@@ -632,13 +635,18 @@ module Vanity
632
635
  end
633
636
 
634
637
  def call_on_assignment_if_available(identity, index)
638
+ assignment = alternatives[index]
639
+
635
640
  # if we have an on_assignment block, call it on new assignments
636
641
  if defined?(@on_assignment_block) && @on_assignment_block
637
- assignment = alternatives[index]
638
- if !connection.ab_seen @id, identity, index
639
- @on_assignment_block.call(Vanity.context, identity, assignment, self)
640
- end
642
+ @on_assignment_block.call(Vanity.context, identity, assignment, self)
643
+
644
+ return
641
645
  end
646
+
647
+ return unless Vanity.configuration.on_assignment.is_a?(Proc)
648
+
649
+ Vanity.configuration.on_assignment.call(Vanity.context, identity, assignment, self)
642
650
  end
643
651
 
644
652
  def rebalance_if_necessary!
@@ -687,10 +695,8 @@ module Vanity
687
695
  # We're really only interested in 90%, 95%, 99% and 99.9%.
688
696
  Z_TO_PROBABILITY = [90, 95, 99, 99.9].map { |pct| [norm_dist.find { |x,a| a >= pct }.first, pct] }.reverse
689
697
  end
690
-
691
698
  end
692
699
 
693
-
694
700
  module Definition
695
701
  # Define an A/B test with the given name. For example:
696
702
  # ab_test "New Banner" do
@@ -700,6 +706,5 @@ module Vanity
700
706
  define name, :ab_test, &block
701
707
  end
702
708
  end
703
-
704
709
  end
705
710
  end
@@ -251,7 +251,7 @@ module Vanity
251
251
  def vanity_js
252
252
  return if Vanity.context.vanity_active_experiments.nil? || Vanity.context.vanity_active_experiments.empty?
253
253
  javascript_tag do
254
- render :file => Vanity.template("_vanity"), :formats => [:js]
254
+ render :file => Vanity.template("_vanity.js.erb"), :formats => [:js]
255
255
  end
256
256
  end
257
257
 
@@ -348,8 +348,13 @@ module Vanity
348
348
  #
349
349
  # Step 3: Open your browser to http://localhost:3000/vanity
350
350
  module Dashboard
351
+ def set_vanity_view_path
352
+ prepend_view_path Vanity.template('')
353
+ end
354
+
351
355
  def index
352
- render :file=>Vanity.template("_report"),:content_type=>Mime[:html], :locals=>{
356
+ set_vanity_view_path
357
+ render :template=>"_report", :content_type=>Mime[:html], :locals=>{
353
358
  :experiments=>Vanity.playground.experiments,
354
359
  :experiments_persisted=>Vanity.playground.experiments_persisted?,
355
360
  :metrics=>Vanity.playground.metrics
@@ -357,46 +362,52 @@ module Vanity
357
362
  end
358
363
 
359
364
  def participant
360
- render :file=>Vanity.template("_participant"), :locals=>{:participant_id => params[:id], :participant_info => Vanity.playground.participant_info(params[:id])}, :content_type=>Mime[:html]
365
+ set_vanity_view_path
366
+ render :template=>"_participant", :locals=>{:participant_id => params[:id], :participant_info => Vanity.playground.participant_info(params[:id])}, :content_type=>Mime[:html]
361
367
  end
362
368
 
363
369
  def complete
370
+ set_vanity_view_path
364
371
  exp = Vanity.playground.experiment(params[:e].to_sym)
365
372
  alt = exp.alternatives[params[:a].to_i]
366
373
  confirmed = params[:confirmed]
367
374
  # make the user confirm before completing an experiment
368
375
  if confirmed && confirmed.to_i==alt.id && exp.active?
369
376
  exp.complete!(alt.id)
370
- render :file=>Vanity.template("_experiment"), :locals=>{:experiment=>exp}
377
+ render :template=>"_experiment", :locals=>{:experiment=>exp}
371
378
  else
372
379
  @to_confirm = alt.id
373
- render :file=>Vanity.template("_experiment"), :locals=>{:experiment=>exp}
380
+ render :template=>"_experiment", :locals=>{:experiment=>exp}
374
381
  end
375
382
  end
376
383
 
377
384
  def disable
385
+ set_vanity_view_path
378
386
  exp = Vanity.playground.experiment(params[:e].to_sym)
379
387
  exp.enabled = false
380
- render :file=>Vanity.template("_experiment"), :locals=>{:experiment=>exp}
388
+ render :template=>"_experiment", :locals=>{:experiment=>exp}
381
389
  end
382
390
 
383
391
  def enable
392
+ set_vanity_view_path
384
393
  exp = Vanity.playground.experiment(params[:e].to_sym)
385
394
  exp.enabled = true
386
- render :file=>Vanity.template("_experiment"), :locals=>{:experiment=>exp}
395
+ render :template=>"_experiment", :locals=>{:experiment=>exp}
387
396
  end
388
397
 
389
398
  def chooses
399
+ set_vanity_view_path
390
400
  exp = Vanity.playground.experiment(params[:e].to_sym)
391
401
  exp.chooses(exp.alternatives[params[:a].to_i].value)
392
- render :file=>Vanity.template("_experiment"), :locals=>{:experiment=>exp}
402
+ render :template=>"_experiment", :locals=>{:experiment=>exp}
393
403
  end
394
404
 
395
405
  def reset
406
+ set_vanity_view_path
396
407
  exp = Vanity.playground.experiment(params[:e].to_sym)
397
408
  exp.reset
398
409
  flash[:notice] = I18n.t 'vanity.experiment_has_been_reset', name: exp.name
399
- render :file=>Vanity.template("_experiment"), :locals=>{:experiment=>exp}
410
+ render :template=>"_experiment", :locals=>{:experiment=>exp}
400
411
  end
401
412
 
402
413
  include AddParticipant
@@ -0,0 +1,50 @@
1
+ ru:
2
+ vanity:
3
+ act_on_this_experiment:
4
+ enable: 'Включить этот эксперимент'
5
+ disable: 'Выключить этот эксперимент'
6
+ best_alternative: '(%{probability}% это лучшая альтернатива)'
7
+ best_alternative_is_significant: 'С %{probability}% вероятностью этот результат является статистически значимым.'
8
+ best_alternative_probability: 'С %{probability}% вероятностью этот результат является самым лучшим.'
9
+ best_alternative_measure: 'Самый лучший выбор %{best_alternative}: он конвертирован в %{measure}% %{better_than}.'
10
+ better_alternative_than: '(%{probability}% лучше чем %{alternative})'
11
+ choose_experiment: 'Показать мне этот вариант прямо сейчас'
12
+ complete_and_confirm_experiment: 'Подтвердите завершение эксперимента и назначение всех нынешних и будущих участников, %{name}'
13
+ complete_and_confirm_experiment_label: 'Завершить этот эксперимент и назначить всех нынешних и будущих участников %{name}?'
14
+ complete_experiment: 'Завершить эксперимент и назначить всех нынешних и будущих участников %{name}'
15
+ completed_at: 'Завершено %{timestamp}'
16
+ confirm: 'подтвердить'
17
+ converted: '%{count} конвертировано'
18
+ converted_percentage: '%{alternative} конвертировано в %{percentage}%.'
19
+ currently_shown: 'сейчас отображено'
20
+ default: '(Default)'
21
+ didnt_convert: '%{alternative} невозможно конвертировать.'
22
+ disable: 'Отключить'
23
+ disabled: 'Отключено'
24
+ enable: 'Включить'
25
+ enabled: 'Включено'
26
+ experiment_has_been_reset: '%{name} эксперимент был сброшен.'
27
+ experiment_has_been_disabled: 'Этот эксперимент в данное время отключен и всегда будет выбираться по умолчанию %{name}.'
28
+ experiment_participants:
29
+ one: 'В этом эксперименте один участник.'
30
+ other: 'В этом эксперименте %{count} участников.'
31
+ experiments: 'Эксперименты'
32
+ experiments_out_of_sync: "Кэшированные эксперименты Vanity не синхронизированы с экспериментами в файловой системе и / или в хранилище данных. Перезапустите сервер и / или включите сбор."
33
+ generated_by: 'Создан с помощью %{link}'
34
+ low_result_confidence: 'Полученный результат не является надежным, предлагаю продолжить этот эксперимент.'
35
+ make_permanent: 'сделать постоянным'
36
+ metrics: 'Метрики'
37
+ no_clear_winner: 'Этот эксперимент не длился достаточно долго, чтобы найти явного победителя.'
38
+ no_participants: 'В этом эксперименте пока нет участников.'
39
+ not_collecting_data: 'Vanity в данный момент не собирает данные и метрики. Чтобы включить сбор данных, установите %{setting} в %{file}.'
40
+ option_number: 'выбор %{char}'
41
+ participant_taking_part_on: 'Участник с идентификатором %{participant_id} принимает участие в следующих экспериментах:'
42
+ participants:
43
+ one: '1 участник'
44
+ other: '%{count} участников'
45
+ reset: 'Сбросить'
46
+ report: 'Отчет Vanity: %{timestamp}'
47
+ result_isnt_significant: 'Полученный результат не является статистически важным, предлагаю продолжить этот эксперимент.'
48
+ selected_as_best: '%{alternative} выбран в качестве лучшей альтернативы.'
49
+ show_me: 'показать мне'
50
+ started_at: 'Начато %{timestamp}'
@@ -9,13 +9,13 @@
9
9
  <% if experiment.type == 'ab_test' && experiment.active? && experiment.playground.collecting? %>
10
10
  <span class='enabled-links'>
11
11
  <% action = experiment.enabled? ? :disable : :enable %>
12
-
12
+
13
13
  <% if experiment.enabled? %> <%= I18n.t( 'vanity.enabled' ) %> | <% end %>
14
14
  <a title='<%=I18n.t( action, scope: 'vanity.act_on_this_experiment' )%>' href='#'
15
15
  data-id='<%= experiment.id %>' data-url='<%= url_for(:action=>action, :e => experiment.id) %>'>
16
16
  <%= action %></a>
17
17
  <% if !experiment.enabled? %> | <%= I18n.t( 'vanity.disabled' ) %> <% end %>
18
-
18
+
19
19
  </span>
20
20
  <% end %>
21
21
  </h3>
@@ -26,7 +26,7 @@
26
26
  </p>
27
27
  <% end %>
28
28
  <a class="button reset" title="<%= I18n.t('vanity.reset') %>" href="#" data-id="<%= experiment.id %>" data-url="<%= url_for(:action=>:reset, :e=>experiment.id) %>"><%= I18n.t 'vanity.reset' %></a>
29
- <%= render :file => Vanity.template("_" + experiment.type), :locals => {:experiment => experiment} %>
29
+ <%= render :template => "_" + experiment.type, :locals => {:experiment => experiment} %>
30
30
  <p class="meta">
31
31
  <%= I18n.t('vanity.started_at', :timestamp=>I18n.l(experiment.created_at, :format=>'%a, %b %d')) %>
32
32
  <%= ' | '+I18n.t('vanity.completed_at', :timestamp=>I18n.l(experiment.completed_at, :format=>'%a, %b %d')) unless experiment.active? %>
@@ -1,7 +1,7 @@
1
1
  <ul class="experiments">
2
2
  <% experiments.sort_by{|id, experiment| experiment.created_at}.sort_by{ |id, experiment| experiment.name }.reverse.each do |id, experiment| %>
3
3
  <li class="experiment <%= experiment.type %>" id="experiment_<%=vanity_h id.to_s %>">
4
- <%= render :file => Vanity.template("_experiment"), :locals => { :id => id, :experiment => experiment } %>
4
+ <%= render :template=>"_experiment", :locals => { :id => id, :experiment => experiment } %>
5
5
  </li>
6
6
  <% end %>
7
7
  </ul>
@@ -1,7 +1,7 @@
1
1
  <ul class="metrics">
2
2
  <% metrics.sort_by { |id, metric| metric.name }.each do |id, metric| %>
3
3
  <li class="metric" id="metric_<%= id %>">
4
- <%= render :file=>Vanity.template("_metric"), :locals=>{:id=>id, :metric=>metric} %>
4
+ <%= render :template=>"_metric", :locals=>{:id=>id, :metric=>metric} %>
5
5
  </li>
6
6
  <% end %>
7
7
  </ul>
@@ -22,12 +22,12 @@
22
22
  <% if experiments_persisted %>
23
23
  <% if experiments.present? %>
24
24
  <h2><%= I18n.t 'vanity.experiments' %></h2>
25
- <%= render :file=>Vanity.template("_experiments"), :locals=>{:experiments=>experiments} %>
25
+ <%= render :template=>"_experiments", :locals=>{:experiments=>experiments} %>
26
26
  <% end %>
27
27
 
28
28
  <% unless metrics.empty? %>
29
29
  <h2><%= I18n.t 'vanity.metrics' %></h2>
30
- <%= render :file=>Vanity.template("_metrics"), :locals=>{:metrics=>metrics, :experiments=>experiments} %>
30
+ <%= render :template=>"_metrics", :locals=>{:metrics=>metrics, :experiments=>experiments} %>
31
31
  <% end %>
32
32
  <% else %>
33
33
  <div class="alert persistance">
@@ -1,5 +1,5 @@
1
1
  module Vanity
2
- VERSION = "2.2.10"
2
+ VERSION = "3.1.0"
3
3
 
4
4
  module Version
5
5
  version = VERSION.to_s.split(".").map { |i| i.to_i }
@@ -18,17 +18,13 @@ describe Vanity::Adapters::RedisAdapter do
18
18
 
19
19
  include Vanity::Adapters::SharedTests
20
20
 
21
- it "warns on disconnect error" do
21
+ it "disconnects" do
22
22
  if defined?(Redis)
23
- assert_silent do
24
- Redis.any_instance.stubs(:connect!)
25
- mocked_redis = stub("Redis")
26
- mocked_redis.expects(:client).raises(RuntimeError)
27
- redis_adapter = Vanity::Adapters::RedisAdapter.new({})
28
- redis_adapter.stubs(:redis).returns(mocked_redis)
29
- Vanity.logger.expects(:warn).with("Error while disconnecting from redis: RuntimeError")
30
- redis_adapter.disconnect!
31
- end
23
+ mocked_redis = stub("Redis")
24
+ mocked_redis.expects(:disconnect!)
25
+ redis_adapter = Vanity::Adapters::RedisAdapter.new({})
26
+ redis_adapter.stubs(:redis).returns(mocked_redis)
27
+ redis_adapter.disconnect!
32
28
  end
33
29
  end
34
30
 
@@ -75,7 +71,7 @@ describe Vanity::Adapters::RedisAdapter do
75
71
 
76
72
  it "gracefully fails in #ab_show" do
77
73
  redis_adapter, mocked_redis = stub_redis
78
- mocked_redis.stubs(:[]=).raises(RuntimeError)
74
+ mocked_redis.stubs(:set).raises(RuntimeError)
79
75
 
80
76
  assert_silent do
81
77
  redis_adapter.ab_show("price_options", "3ff62e2fb51f0b22646a342a2d357aec", 0)
@@ -84,7 +80,7 @@ describe Vanity::Adapters::RedisAdapter do
84
80
 
85
81
  it "gracefully fails in #ab_showing" do
86
82
  redis_adapter, mocked_redis = stub_redis
87
- mocked_redis.stubs(:[]).raises(RuntimeError)
83
+ mocked_redis.stubs(:get).raises(RuntimeError)
88
84
 
89
85
  assert_silent do
90
86
  redis_adapter.ab_showing("price_options", "3ff62e2fb51f0b22646a342a2d357aec")
@@ -123,6 +123,10 @@ module Vanity::Adapters::SharedTests
123
123
  @subject.destroy_experiment(experiment)
124
124
 
125
125
  refute(@subject.experiment_persisted?(experiment))
126
+ assert_equal(
127
+ { :participants => 0, :converted => 0, :conversions => 0 },
128
+ @subject.ab_counts(experiment, alternative)
129
+ )
126
130
  end
127
131
  end
128
132
 
@@ -272,8 +276,7 @@ module Vanity::Adapters::SharedTests
272
276
  end
273
277
 
274
278
  it 'returns nil if the identity has no assignment' do
275
- assert_equal(
276
- nil,
279
+ assert_nil(
277
280
  @subject.ab_assigned(experiment, identity)
278
281
  )
279
282
  end
@@ -301,8 +304,7 @@ module Vanity::Adapters::SharedTests
301
304
  end
302
305
 
303
306
  it 'returns nil otherwise' do
304
- assert_equal(
305
- nil,
307
+ assert_nil(
306
308
  @subject.ab_get_outcome(experiment)
307
309
  )
308
310
  end
@@ -324,8 +326,7 @@ module Vanity::Adapters::SharedTests
324
326
  @subject.ab_show(experiment, identity, alternative)
325
327
  @subject.ab_not_showing(experiment, identity)
326
328
 
327
- assert_equal(
328
- nil,
329
+ assert_nil(
329
330
  @subject.ab_showing(experiment, identity)
330
331
  )
331
332
  end
@@ -6,6 +6,15 @@ describe Vanity::Commands do
6
6
  metric "Coolness"
7
7
  end
8
8
 
9
+ def with_captured_stdout
10
+ original_stdout = $stdout
11
+ $stdout = StringIO.new
12
+ yield
13
+ $stdout.string
14
+ ensure
15
+ $stdout = original_stdout
16
+ end
17
+
9
18
  describe ".report" do
10
19
  describe "given file" do
11
20
  let(:file) { "tmp/config/redis.yml" }
@@ -20,7 +29,10 @@ describe Vanity::Commands do
20
29
  experiment(:foobar).choose
21
30
 
22
31
  FileUtils.mkpath "tmp/config"
23
- Vanity::Commands.report(file)
32
+
33
+ with_captured_stdout do
34
+ Vanity::Commands.report(file)
35
+ end
24
36
  end
25
37
 
26
38
  after do
@@ -113,6 +113,19 @@ describe Vanity::Configuration do
113
113
  end
114
114
 
115
115
  describe "setup_locales" do
116
- it "adds vanity locales to the gem"
116
+ it "adds vanity locales to the I18n gem" do
117
+ begin
118
+ original_load_path = I18n.load_path
119
+
120
+ config.setup_locales
121
+
122
+ assert_includes(
123
+ I18n.load_path,
124
+ File.expand_path(File.join(__FILE__, '..', '..', 'lib/vanity/locales/vanity.en.yml'))
125
+ )
126
+ ensure
127
+ I18n.load_path = original_load_path
128
+ end
129
+ end
117
130
  end
118
- end
131
+ end
@@ -12,8 +12,10 @@ class VanityMailer < ActionMailer::Base
12
12
  def ab_test_content(user)
13
13
  use_vanity_mailer user
14
14
 
15
+ image_html = view_context.vanity_tracking_image(Vanity.context.vanity_identity, :open, :host => "127.0.0.1:3000")
16
+
15
17
  mail do |format|
16
- format.html { render :text=>view_context.vanity_tracking_image(Vanity.context.vanity_identity, :open, :host => "127.0.0.1:3000") }
18
+ format.html { render :html=>image_html.html_safe }
17
19
  end
18
20
  end
19
21
  end
@@ -4,5 +4,8 @@
4
4
  # If you change this key, all old signed cookies will become invalid!
5
5
  # Make sure the secret is at least 30 characters and all random,
6
6
  # no regular words or you'll be exposed to dictionary attacks.
7
- Dummy::Application.config.secret_token = '33ccbc9a29f3b02e87c08904505b1c9a3a1e97dd01f02e598e65ee9e7b96fff2ca4a6d0dd7c4a8d3682d8c64f84d372661e141264e70697dc576c722c72d80d0'
8
- Dummy::Application.config.secret_key_base = 'secret' if Dummy::Application.config.respond_to?(:secret_key_base=)
7
+ if Dummy::Application.config.respond_to?(:secret_key_base=)
8
+ Dummy::Application.config.secret_key_base = 'secret'
9
+ else
10
+ Dummy::Application.config.secret_token = '33ccbc9a29f3b02e87c08904505b1c9a3a1e97dd01f02e598e65ee9e7b96fff2ca4a6d0dd7c4a8d3682d8c64f84d372661e141264e70697dc576c722c72d80d0'
11
+ end
@@ -52,7 +52,21 @@ Dummy::Application.routes.draw do
52
52
 
53
53
  # See how all your routes lay out with "rake routes"
54
54
 
55
- # This is a legacy wild controller route that's not recommended for RESTful applications.
56
- # Note: This route will make all actions in every controller accessible via GET requests.
57
- match ':controller(/:action(/:id(.:format)))', :via => [:get, :post]
55
+ get '/use_vanity(/:id(.:format))', controller: :use_vanity, action: :index
56
+
57
+ %w(js view_helper_ab_test_js global_ab_test_js model_js).each do |action|
58
+ get "/use_vanity/#{action}(/:id(.:format))", controller: :use_vanity, action: action
59
+ end
60
+
61
+ %w(track test_capture test_render test_view).each do |action|
62
+ get "/ab_test/#{action}(/:id(.:format))", controller: :ab_test, action: action
63
+ end
64
+
65
+ %w(reset disable enable chooses add_participant complete).each do |action|
66
+ post "/vanity(/#{action}(/:id(.:format)))", controller: :vanity, action: action
67
+ end
68
+
69
+ %w(index participant image).each do |action|
70
+ get "/vanity(/#{action}(/:id(.:format)))", controller: :vanity, action: action
71
+ end
58
72
  end