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.
- 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
|