vanity 2.2.9 → 3.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/test.yml +55 -0
  3. data/Appraisals +24 -20
  4. data/CHANGELOG +20 -1
  5. data/Gemfile +2 -1
  6. data/Gemfile.lock +24 -14
  7. data/README.md +14 -16
  8. data/gemfiles/rails42.gemfile +6 -6
  9. data/gemfiles/rails42.gemfile.lock +94 -85
  10. data/gemfiles/rails42_protected_attributes.gemfile +6 -6
  11. data/gemfiles/rails42_protected_attributes.gemfile.lock +96 -74
  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.erb +24 -0
  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} +2 -2
  23. data/lib/generators/vanity/migration_generator.rb +34 -0
  24. data/lib/vanity/adapters/active_record_adapter.rb +15 -5
  25. data/lib/vanity/adapters/redis_adapter.rb +20 -20
  26. data/lib/vanity/commands/report.rb +4 -3
  27. data/lib/vanity/experiment/ab_test.rb +3 -2
  28. data/lib/vanity/frameworks/rails.rb +20 -9
  29. data/lib/vanity/locales/vanity.ru.yml +50 -0
  30. data/lib/vanity/templates/_experiment.erb +3 -3
  31. data/lib/vanity/templates/_experiments.erb +1 -1
  32. data/lib/vanity/templates/_metrics.erb +1 -1
  33. data/lib/vanity/templates/_report.erb +2 -2
  34. data/lib/vanity/templates/vanity.css +5 -5
  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 +3 -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/test_helper.rb +57 -10
  48. data/test/web/rails/dashboard_test.rb +19 -10
  49. data/vanity.gemspec +1 -1
  50. metadata +20 -19
  51. data/.travis.yml +0 -33
  52. data/gemfiles/rails32.gemfile.lock +0 -242
  53. data/gemfiles/rails41.gemfile.lock +0 -230
  54. data/gemfiles/rails5.gemfile.lock +0 -256
  55. data/lib/generators/vanity/add_unique_indexes_generator.rb +0 -15
  56. data/lib/generators/vanity_generator.rb +0 -15
@@ -102,12 +102,22 @@ module Vanity
102
102
  # passed then this will be passed to create if creating, or will be
103
103
  # used to update the found participant.
104
104
  def self.retrieve(experiment, identity, create = true, update_with = nil)
105
- if record = VanityParticipant.where(:experiment_id=>experiment.to_s, :identity=>identity.to_s).first
106
- record.update_attributes(update_with) if update_with
107
- elsif create
108
- record = VanityParticipant.create({ :experiment_id=>experiment.to_s, :identity=>identity.to_s }.merge(update_with || {}))
105
+ retried = false
106
+ begin
107
+ if record = VanityParticipant.where(:experiment_id => experiment.to_s, :identity => identity.to_s).first
108
+ record.update(update_with) if update_with
109
+ elsif create
110
+ record = VanityParticipant.create({ :experiment_id => experiment.to_s, :identity => identity.to_s }.merge(update_with || {}))
111
+ end
112
+ record
113
+ rescue ActiveRecord::RecordNotUnique => e
114
+ if retried
115
+ raise e
116
+ else
117
+ retried = true
118
+ retry
119
+ end
109
120
  end
110
- record
111
121
  end
112
122
  end
113
123
 
@@ -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
@@ -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
@@ -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">
@@ -13,15 +13,15 @@
13
13
  .vanity .experiment .status_disabled { background: #FEE }
14
14
  .vanity .experiment .enabled-links { float: right; font-weight: normal }
15
15
 
16
- .vanity .ab_test table { border-collapse: collapse; table-layout: fixed; width: 100%; border-bottom: 1px solid #ccc; margin: 1em 0 0 0 }
17
- .vanity .ab_test td { padding: .5em; border-top: 1px solid #ccc; width: 2em; overflow: hidden }
16
+ .vanity .ab_test table { border-collapse: collapse; width: 100%; border-bottom: 1px solid #ccc; margin: 1em 0 0 0 }
17
+ .vanity .ab_test td { padding: .5em; border-top: 1px solid #ccc; overflow: hidden }
18
18
  .vanity .ab_test .choice td { font-weight: bold; background: #f0f0f8 }
19
19
  .vanity .ab_test caption { caption-side: bottom; padding: .5em; background: transparent; text-align: left }
20
20
  .vanity .ab_test caption .disabled_info { padding-top: .5em }
21
- .vanity .ab_test td.option { width: 5em; white-space: nowrap; overflow: hidden }
21
+ .vanity .ab_test td.option { white-space: nowrap; overflow: hidden }
22
22
  .vanity .ab_test td.option .default { font-size: 75% }
23
- .vanity .ab_test td.value { width: 8em; white-space: nowrap; overflow: hidden }
24
- .vanity .ab_test td.action { width: 6em; overflow: hidden; text-align: right }
23
+ .vanity .ab_test td.value { white-space: nowrap; overflow: hidden }
24
+ .vanity .ab_test td.action { overflow: hidden; text-align: right }
25
25
  .vanity .ab_test button.reset { float: right }
26
26
 
27
27
  .vanity .metrics { list-style: none; margin: 0; padding: 0; border-bottom: 1px solid #ddd }
@@ -1,5 +1,5 @@
1
1
  module Vanity
2
- VERSION = "2.2.9"
2
+ VERSION = "3.0.2"
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
@@ -529,12 +529,12 @@ class AbTestTest < ActionController::TestCase
529
529
  identify { "6e98ec" }
530
530
  metrics :coolness
531
531
  end
532
- assert_equal nil, experiment(:foobar).playground.connection.ab_assigned(experiment(:foobar).id, "6e98ec")
532
+ assert_nil experiment(:foobar).playground.connection.ab_assigned(experiment(:foobar).id, "6e98ec")
533
533
  assert id = experiment(:foobar).choose.id
534
534
  assert_equal id, experiment(:foobar).playground.connection.ab_assigned(experiment(:foobar).id, "6e98ec")
535
535
  end
536
536
 
537
- def test_ab_assigned
537
+ def test_ab_assigned_object
538
538
  identity = { :a => :b }
539
539
  new_ab_test :foobar do
540
540
  alternatives "foo", "bar"
@@ -542,7 +542,7 @@ class AbTestTest < ActionController::TestCase
542
542
  identify { identity }
543
543
  metrics :coolness
544
544
  end
545
- assert_equal nil, experiment(:foobar).playground.connection.ab_assigned(experiment(:foobar).id, identity)
545
+ assert_nil experiment(:foobar).playground.connection.ab_assigned(experiment(:foobar).id, identity)
546
546
  assert id = experiment(:foobar).choose.id
547
547
  assert_equal id, experiment(:foobar).playground.connection.ab_assigned(experiment(:foobar).id, identity)
548
548
  end