vanity 2.2.10 → 3.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.github/workflows/test.yml +55 -0
- data/Appraisals +24 -20
- data/CHANGELOG +19 -1
- data/Gemfile +2 -1
- data/Gemfile.lock +23 -15
- data/README.md +14 -16
- data/gemfiles/rails42.gemfile +6 -6
- data/gemfiles/rails42.gemfile.lock +95 -86
- data/gemfiles/rails42_protected_attributes.gemfile +6 -6
- data/gemfiles/rails42_protected_attributes.gemfile.lock +90 -81
- data/gemfiles/{rails5.gemfile → rails51.gemfile} +6 -6
- data/gemfiles/rails51.gemfile.lock +285 -0
- data/gemfiles/{rails41.gemfile → rails52.gemfile} +6 -6
- data/gemfiles/rails52.gemfile.lock +295 -0
- data/gemfiles/{rails32.gemfile → rails60.gemfile} +6 -9
- data/gemfiles/rails60.gemfile.lock +293 -0
- data/gemfiles/rails61.gemfile +33 -0
- data/gemfiles/rails61.gemfile.lock +293 -0
- data/lib/generators/templates/{add_participants_unique_index_migration.rb → add_participants_unique_index_migration.rb.erb} +1 -1
- data/lib/generators/templates/{add_unique_indexes_migration.rb → add_unique_indexes_migration.rb.erb} +1 -1
- data/lib/generators/templates/{vanity_migration.rb → vanity_migration.rb.erb} +1 -1
- data/lib/generators/vanity/migration_generator.rb +34 -0
- data/lib/vanity/adapters/active_record_adapter.rb +1 -1
- data/lib/vanity/adapters/redis_adapter.rb +20 -20
- data/lib/vanity/commands/report.rb +4 -3
- data/lib/vanity/configuration.rb +4 -0
- data/lib/vanity/experiment/ab_test.rb +16 -11
- data/lib/vanity/frameworks/rails.rb +20 -9
- data/lib/vanity/locales/vanity.ru.yml +50 -0
- data/lib/vanity/templates/_experiment.erb +3 -3
- data/lib/vanity/templates/_experiments.erb +1 -1
- data/lib/vanity/templates/_metrics.erb +1 -1
- data/lib/vanity/templates/_report.erb +2 -2
- data/lib/vanity/version.rb +1 -1
- data/test/adapters/redis_adapter_test.rb +8 -12
- data/test/adapters/shared_tests.rb +7 -6
- data/test/commands/report_test.rb +13 -1
- data/test/configuration_test.rb +15 -2
- data/test/dummy/app/mailers/vanity_mailer.rb +3 -1
- data/test/dummy/config/initializers/secret_token.rb +5 -2
- data/test/dummy/config/routes.rb +17 -3
- data/test/experiment/ab_test.rb +43 -3
- data/test/frameworks/rails/action_controller_test.rb +12 -6
- data/test/frameworks/rails/action_mailer_test.rb +0 -1
- data/test/metric/active_record_test.rb +8 -2
- data/test/playground_test.rb +0 -1
- data/test/test_helper.rb +57 -10
- data/test/web/rails/dashboard_test.rb +19 -10
- data/vanity.gemspec +1 -1
- metadata +20 -21
- data/.travis.yml +0 -33
- data/gemfiles/rails32.gemfile.lock +0 -242
- data/gemfiles/rails41.gemfile.lock +0 -230
- data/gemfiles/rails5.gemfile.lock +0 -256
- data/lib/generators/vanity/add_participants_unique_index_generator.rb +0 -15
- data/lib/generators/vanity/add_unique_indexes_generator.rb +0 -15
- 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
|
-
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
-
|
149
|
-
:conversions => @experiments
|
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
|
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
|
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
|
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
|
-
|
225
|
-
|
226
|
-
|
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[:
|
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(
|
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("
|
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
|
data/lib/vanity/configuration.rb
CHANGED
@@ -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,
|
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
|
638
|
-
|
639
|
-
|
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
|
-
|
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
|
-
|
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 :
|
377
|
+
render :template=>"_experiment", :locals=>{:experiment=>exp}
|
371
378
|
else
|
372
379
|
@to_confirm = alt.id
|
373
|
-
render :
|
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 :
|
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 :
|
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 :
|
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 :
|
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 :
|
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 :
|
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 :
|
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 :
|
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 :
|
30
|
+
<%= render :template=>"_metrics", :locals=>{:metrics=>metrics, :experiments=>experiments} %>
|
31
31
|
<% end %>
|
32
32
|
<% else %>
|
33
33
|
<div class="alert persistance">
|
data/lib/vanity/version.rb
CHANGED
@@ -18,17 +18,13 @@ describe Vanity::Adapters::RedisAdapter do
|
|
18
18
|
|
19
19
|
include Vanity::Adapters::SharedTests
|
20
20
|
|
21
|
-
it "
|
21
|
+
it "disconnects" do
|
22
22
|
if defined?(Redis)
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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(:
|
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(:
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
32
|
+
|
33
|
+
with_captured_stdout do
|
34
|
+
Vanity::Commands.report(file)
|
35
|
+
end
|
24
36
|
end
|
25
37
|
|
26
38
|
after do
|
data/test/configuration_test.rb
CHANGED
@@ -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 :
|
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.
|
8
|
-
Dummy::Application.config.secret_key_base = 'secret'
|
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
|
data/test/dummy/config/routes.rb
CHANGED
@@ -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
|
-
|
56
|
-
|
57
|
-
|
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
|