vanity 2.2.10 → 3.1.0

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