vanity 3.1.0 → 4.0.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 +4 -4
- data/.github/workflows/linting.yml +28 -0
- data/.github/workflows/test.yml +3 -6
- data/.rubocop.yml +114 -0
- data/.rubocop_todo.yml +67 -0
- data/Appraisals +9 -31
- data/CHANGELOG +5 -0
- data/Gemfile +7 -3
- data/Gemfile.lock +31 -3
- data/README.md +4 -9
- data/Rakefile +25 -24
- data/bin/vanity +1 -1
- data/doc/configuring.textile +1 -0
- data/gemfiles/rails52.gemfile +6 -3
- data/gemfiles/rails52.gemfile.lock +34 -9
- data/gemfiles/rails60.gemfile +6 -3
- data/gemfiles/rails60.gemfile.lock +34 -9
- data/gemfiles/rails61.gemfile +6 -3
- data/gemfiles/rails61.gemfile.lock +34 -9
- data/lib/generators/vanity/migration_generator.rb +5 -7
- data/lib/vanity/adapters/abstract_adapter.rb +43 -45
- data/lib/vanity/adapters/active_record_adapter.rb +30 -30
- data/lib/vanity/adapters/mock_adapter.rb +14 -18
- data/lib/vanity/adapters/mongodb_adapter.rb +73 -69
- data/lib/vanity/adapters/redis_adapter.rb +19 -27
- data/lib/vanity/adapters.rb +1 -1
- data/lib/vanity/autoconnect.rb +6 -7
- data/lib/vanity/commands/list.rb +7 -7
- data/lib/vanity/commands/report.rb +18 -22
- data/lib/vanity/configuration.rb +19 -19
- data/lib/vanity/connection.rb +12 -14
- data/lib/vanity/experiment/ab_test.rb +82 -70
- data/lib/vanity/experiment/alternative.rb +3 -5
- data/lib/vanity/experiment/base.rb +24 -19
- data/lib/vanity/experiment/bayesian_bandit_score.rb +7 -13
- data/lib/vanity/experiment/definition.rb +6 -6
- data/lib/vanity/frameworks/rails.rb +39 -39
- data/lib/vanity/frameworks.rb +2 -2
- data/lib/vanity/helpers.rb +1 -1
- data/lib/vanity/metric/active_record.rb +21 -19
- data/lib/vanity/metric/base.rb +22 -23
- data/lib/vanity/metric/google_analytics.rb +6 -9
- data/lib/vanity/metric/remote.rb +3 -5
- data/lib/vanity/playground.rb +3 -6
- data/lib/vanity/vanity.rb +8 -12
- data/lib/vanity/version.rb +1 -1
- data/test/adapters/active_record_adapter_test.rb +1 -5
- data/test/adapters/mock_adapter_test.rb +0 -2
- data/test/adapters/mongodb_adapter_test.rb +1 -5
- data/test/adapters/redis_adapter_test.rb +2 -3
- data/test/adapters/shared_tests.rb +9 -12
- data/test/autoconnect_test.rb +3 -3
- data/test/cli_test.rb +0 -1
- data/test/configuration_test.rb +18 -34
- data/test/connection_test.rb +3 -3
- data/test/dummy/Rakefile +1 -1
- data/test/dummy/app/controllers/use_vanity_controller.rb +12 -8
- data/test/dummy/app/mailers/vanity_mailer.rb +3 -3
- data/test/dummy/config/application.rb +1 -1
- data/test/dummy/config/boot.rb +3 -3
- data/test/dummy/config/environment.rb +1 -1
- data/test/dummy/config/environments/development.rb +0 -1
- data/test/dummy/config/environments/test.rb +1 -1
- data/test/dummy/config/initializers/session_store.rb +1 -1
- data/test/dummy/config.ru +1 -1
- data/test/dummy/script/rails +2 -2
- data/test/experiment/ab_test.rb +148 -154
- data/test/experiment/base_test.rb +48 -32
- data/test/frameworks/rails/action_controller_test.rb +25 -25
- data/test/frameworks/rails/action_mailer_test.rb +2 -2
- data/test/frameworks/rails/action_view_test.rb +5 -6
- data/test/frameworks/rails/rails_test.rb +147 -181
- data/test/helper_test.rb +2 -2
- data/test/metric/active_record_test.rb +174 -212
- data/test/metric/base_test.rb +21 -46
- data/test/metric/google_analytics_test.rb +17 -25
- data/test/metric/remote_test.rb +7 -10
- data/test/playground_test.rb +7 -14
- data/test/templates_test.rb +16 -20
- data/test/test_helper.rb +28 -29
- data/test/vanity_test.rb +4 -10
- data/test/web/rails/dashboard_test.rb +21 -21
- data/vanity.gemspec +8 -7
- metadata +28 -30
- data/gemfiles/rails42.gemfile +0 -33
- data/gemfiles/rails42.gemfile.lock +0 -265
- data/gemfiles/rails42_protected_attributes.gemfile +0 -34
- data/gemfiles/rails42_protected_attributes.gemfile.lock +0 -264
- data/gemfiles/rails51.gemfile +0 -33
- data/gemfiles/rails51.gemfile.lock +0 -285
@@ -8,14 +8,14 @@ module Vanity
|
|
8
8
|
# metrics :signup
|
9
9
|
# end
|
10
10
|
module Definition
|
11
|
-
|
12
11
|
attr_reader :playground
|
13
12
|
|
14
13
|
# Defines a new experiment, given the experiment's name, type and
|
15
14
|
# definition block.
|
16
15
|
def define(name, type, options = nil, &block)
|
17
|
-
|
18
|
-
|
16
|
+
raise "Experiment #{@experiment_id} already defined in playground" if playground.experiments[@experiment_id]
|
17
|
+
|
18
|
+
klass = Experiment.const_get(type.to_s.gsub(/\/(.?)/) { "::#{Regexp.last_match(1).upcase}" }.gsub(/(?:^|_)(.)/) { Regexp.last_match(1).upcase })
|
19
19
|
experiment = klass.new(playground, @experiment_id, name, options)
|
20
20
|
experiment.instance_eval(&block)
|
21
21
|
experiment.save
|
@@ -23,10 +23,10 @@ module Vanity
|
|
23
23
|
end
|
24
24
|
|
25
25
|
def new_binding(playground, id)
|
26
|
-
@playground
|
26
|
+
@playground = playground
|
27
|
+
@experiment_id = id
|
27
28
|
binding
|
28
29
|
end
|
29
|
-
|
30
30
|
end
|
31
31
|
end
|
32
|
-
end
|
32
|
+
end
|
@@ -69,7 +69,7 @@ module Vanity
|
|
69
69
|
@vanity_identity
|
70
70
|
elsif cookies[Vanity.configuration.cookie_name]
|
71
71
|
@vanity_identity = cookies[Vanity.configuration.cookie_name]
|
72
|
-
elsif identity = vanity_identity_from_method(vanity_identity_method)
|
72
|
+
elsif identity = vanity_identity_from_method(vanity_identity_method) # rubocop:todo Lint/AssignmentInCondition
|
73
73
|
@vanity_identity = identity
|
74
74
|
elsif response # everyday use
|
75
75
|
@vanity_identity = cookies[Vanity.configuration.cookie_name] || SecureRandom.hex(16)
|
@@ -96,7 +96,7 @@ module Vanity
|
|
96
96
|
path: Vanity.configuration.cookie_path,
|
97
97
|
domain: Vanity.configuration.cookie_domain,
|
98
98
|
secure: Vanity.configuration.cookie_secure,
|
99
|
-
httponly: Vanity.configuration.cookie_httponly
|
99
|
+
httponly: Vanity.configuration.cookie_httponly,
|
100
100
|
}
|
101
101
|
result[:domain] ||= ::Rails.application.config.session_options[:domain]
|
102
102
|
result
|
@@ -118,13 +118,13 @@ module Vanity
|
|
118
118
|
if symbol && (@object = symbol)
|
119
119
|
class << self
|
120
120
|
define_method :vanity_identity do
|
121
|
-
@vanity_identity = (
|
121
|
+
@vanity_identity = (@object.is_a?(String) ? @object : @object.id)
|
122
122
|
end
|
123
123
|
end
|
124
124
|
else
|
125
125
|
class << self
|
126
126
|
define_method :vanity_identity do
|
127
|
-
@vanity_identity
|
127
|
+
@vanity_identity ||= SecureRandom.hex(16)
|
128
128
|
end
|
129
129
|
end
|
130
130
|
end
|
@@ -137,7 +137,8 @@ module Vanity
|
|
137
137
|
module Filters
|
138
138
|
# Around filter that sets Vanity.context to controller.
|
139
139
|
def vanity_context_filter
|
140
|
-
previous
|
140
|
+
previous = Vanity.context
|
141
|
+
Vanity.context = self
|
141
142
|
yield
|
142
143
|
ensure
|
143
144
|
Vanity.context = previous
|
@@ -158,9 +159,9 @@ module Vanity
|
|
158
159
|
# http://example.com/.
|
159
160
|
def vanity_query_parameter_filter
|
160
161
|
query_params = request.query_parameters
|
161
|
-
if request.get? && query_params[:_vanity]
|
162
|
+
if request.get? && query_params[:_vanity] # rubocop:todo Style/GuardClause
|
162
163
|
hashes = Array(query_params.delete(:_vanity))
|
163
|
-
Vanity.playground.experiments.each do |
|
164
|
+
Vanity.playground.experiments.each do |_id, experiment|
|
164
165
|
if experiment.respond_to?(:alternatives)
|
165
166
|
experiment.alternatives.each do |alt|
|
166
167
|
if hashes.delete(experiment.fingerprint(alt))
|
@@ -185,15 +186,12 @@ module Vanity
|
|
185
186
|
# Filter to track metrics. Pass _track param along to call track! on that
|
186
187
|
# alternative.
|
187
188
|
def vanity_track_filter
|
188
|
-
if request.get? && params[:_track]
|
189
|
-
Vanity.track! params[:_track]
|
190
|
-
end
|
189
|
+
Vanity.track! params[:_track] if request.get? && params[:_track]
|
191
190
|
end
|
192
191
|
|
193
192
|
protected :vanity_context_filter, :vanity_query_parameter_filter, :vanity_reload_filter
|
194
193
|
end
|
195
194
|
|
196
|
-
|
197
195
|
# Introduces ab_test helper (controllers and views). Similar to the generic
|
198
196
|
# ab_test method, with the ability to capture content (applicable to views,
|
199
197
|
# see examples).
|
@@ -221,13 +219,13 @@ module Vanity
|
|
221
219
|
# <%= count %> features to choose from!
|
222
220
|
# <% end %>
|
223
221
|
def ab_test(name, &block)
|
224
|
-
current_request = respond_to?(:request) ?
|
222
|
+
current_request = respond_to?(:request) ? request : nil
|
225
223
|
value = Vanity.ab_test(name, current_request)
|
226
224
|
|
227
225
|
if block
|
228
226
|
content = capture(value, &block)
|
229
227
|
if defined?(block_called_from_erb?) && block_called_from_erb?(block)
|
230
|
-
|
228
|
+
concat(content)
|
231
229
|
else
|
232
230
|
content
|
233
231
|
end
|
@@ -238,20 +236,21 @@ module Vanity
|
|
238
236
|
|
239
237
|
# Generate url with the identity of the current user and the metric to track on click
|
240
238
|
def vanity_track_url_for(identity, metric, options = {})
|
241
|
-
options = options.merge(:
|
239
|
+
options = options.merge(_identity: identity, _track: metric)
|
242
240
|
url_for(options)
|
243
241
|
end
|
244
242
|
|
245
243
|
# Generate url with the fingerprint for the current Vanity experiment
|
246
244
|
def vanity_tracking_image(identity, metric, options = {})
|
247
|
-
options = options.merge(:
|
248
|
-
image_tag(url_for(options), :
|
245
|
+
options = options.merge(controller: :vanity, action: :image, _identity: identity, _track: metric)
|
246
|
+
image_tag(url_for(options), width: "1px", height: "1px", alt: "")
|
249
247
|
end
|
250
248
|
|
251
249
|
def vanity_js
|
252
250
|
return if Vanity.context.vanity_active_experiments.nil? || Vanity.context.vanity_active_experiments.empty?
|
251
|
+
|
253
252
|
javascript_tag do
|
254
|
-
render :
|
253
|
+
render file: Vanity.template("_vanity.js.erb"), formats: [:js]
|
255
254
|
end
|
256
255
|
end
|
257
256
|
|
@@ -267,7 +266,7 @@ module Vanity
|
|
267
266
|
end
|
268
267
|
end
|
269
268
|
|
270
|
-
def vanity_simple_format(text, html_options={})
|
269
|
+
def vanity_simple_format(text, html_options = {})
|
271
270
|
vanity_html_safe(simple_format(text, html_options))
|
272
271
|
end
|
273
272
|
|
@@ -299,7 +298,6 @@ module Vanity
|
|
299
298
|
end
|
300
299
|
end
|
301
300
|
|
302
|
-
|
303
301
|
# When configuring use_js to true, you must set up a route to
|
304
302
|
# add_participant_route.
|
305
303
|
#
|
@@ -323,17 +321,21 @@ module Vanity
|
|
323
321
|
h = {}
|
324
322
|
params[:v].split(',').each do |pair|
|
325
323
|
exp_id, answer = pair.split('=')
|
326
|
-
exp =
|
324
|
+
exp = begin
|
325
|
+
Vanity.playground.experiment(exp_id.to_s.to_sym)
|
326
|
+
rescue StandardError
|
327
|
+
nil
|
328
|
+
end
|
327
329
|
answer = answer.to_i
|
328
330
|
|
329
331
|
if !exp || !exp.alternatives[answer]
|
330
332
|
head 404
|
331
|
-
return
|
333
|
+
return # rubocop:todo Lint/NonLocalExitFromIterator
|
332
334
|
end
|
333
335
|
h[exp] = exp.alternatives[answer].value
|
334
336
|
end
|
335
337
|
|
336
|
-
h.each{ |e,a| e.chooses(a, request) }
|
338
|
+
h.each { |e, a| e.chooses(a, request) }
|
337
339
|
head 200
|
338
340
|
end
|
339
341
|
end
|
@@ -354,16 +356,16 @@ module Vanity
|
|
354
356
|
|
355
357
|
def index
|
356
358
|
set_vanity_view_path
|
357
|
-
render :
|
358
|
-
:
|
359
|
-
:
|
360
|
-
:
|
359
|
+
render template: "_report", content_type: Mime[:html], locals: {
|
360
|
+
experiments: Vanity.playground.experiments,
|
361
|
+
experiments_persisted: Vanity.playground.experiments_persisted?,
|
362
|
+
metrics: Vanity.playground.metrics,
|
361
363
|
}
|
362
364
|
end
|
363
365
|
|
364
366
|
def participant
|
365
367
|
set_vanity_view_path
|
366
|
-
render :
|
368
|
+
render template: "_participant", locals: { participant_id: params[:id], participant_info: Vanity.playground.participant_info(params[:id]) }, content_type: Mime[:html]
|
367
369
|
end
|
368
370
|
|
369
371
|
def complete
|
@@ -372,12 +374,12 @@ module Vanity
|
|
372
374
|
alt = exp.alternatives[params[:a].to_i]
|
373
375
|
confirmed = params[:confirmed]
|
374
376
|
# make the user confirm before completing an experiment
|
375
|
-
if confirmed && confirmed.to_i==alt.id && exp.active?
|
377
|
+
if confirmed && confirmed.to_i == alt.id && exp.active?
|
376
378
|
exp.complete!(alt.id)
|
377
|
-
render :
|
379
|
+
render template: "_experiment", locals: { experiment: exp }
|
378
380
|
else
|
379
381
|
@to_confirm = alt.id
|
380
|
-
render :
|
382
|
+
render template: "_experiment", locals: { experiment: exp }
|
381
383
|
end
|
382
384
|
end
|
383
385
|
|
@@ -385,21 +387,21 @@ module Vanity
|
|
385
387
|
set_vanity_view_path
|
386
388
|
exp = Vanity.playground.experiment(params[:e].to_sym)
|
387
389
|
exp.enabled = false
|
388
|
-
render :
|
390
|
+
render template: "_experiment", locals: { experiment: exp }
|
389
391
|
end
|
390
392
|
|
391
393
|
def enable
|
392
394
|
set_vanity_view_path
|
393
395
|
exp = Vanity.playground.experiment(params[:e].to_sym)
|
394
396
|
exp.enabled = true
|
395
|
-
render :
|
397
|
+
render template: "_experiment", locals: { experiment: exp }
|
396
398
|
end
|
397
399
|
|
398
400
|
def chooses
|
399
401
|
set_vanity_view_path
|
400
402
|
exp = Vanity.playground.experiment(params[:e].to_sym)
|
401
403
|
exp.chooses(exp.alternatives[params[:a].to_i].value)
|
402
|
-
render :
|
404
|
+
render template: "_experiment", locals: { experiment: exp }
|
403
405
|
end
|
404
406
|
|
405
407
|
def reset
|
@@ -407,7 +409,7 @@ module Vanity
|
|
407
409
|
exp = Vanity.playground.experiment(params[:e].to_sym)
|
408
410
|
exp.reset
|
409
411
|
flash[:notice] = I18n.t 'vanity.experiment_has_been_reset', name: exp.name
|
410
|
-
render :
|
412
|
+
render template: "_experiment", locals: { experiment: exp }
|
411
413
|
end
|
412
414
|
|
413
415
|
include AddParticipant
|
@@ -416,13 +418,12 @@ module Vanity
|
|
416
418
|
module TrackingImage
|
417
419
|
def image
|
418
420
|
# send image
|
419
|
-
send_file(File.expand_path("../images/x.gif", File.dirname(__FILE__)), :
|
421
|
+
send_file(File.expand_path("../images/x.gif", File.dirname(__FILE__)), type: 'image/gif', stream: false, disposition: 'inline')
|
420
422
|
end
|
421
423
|
end
|
422
424
|
end
|
423
425
|
end
|
424
426
|
|
425
|
-
|
426
427
|
# Enhance ActionController with use_vanity, filters and helper methods.
|
427
428
|
ActiveSupport.on_load(:action_controller) do
|
428
429
|
# Include in controller, add view helper methods.
|
@@ -434,7 +435,6 @@ ActiveSupport.on_load(:action_controller) do
|
|
434
435
|
end
|
435
436
|
end
|
436
437
|
|
437
|
-
|
438
438
|
# Include in mailer, add view helper methods.
|
439
439
|
ActiveSupport.on_load(:action_mailer) do
|
440
440
|
include Vanity::Rails::UseVanityMailer
|
@@ -448,8 +448,8 @@ if defined?(PhusionPassenger)
|
|
448
448
|
if forked
|
449
449
|
begin
|
450
450
|
Vanity.playground.reconnect! if Vanity.playground.collecting?
|
451
|
-
rescue Exception=>
|
452
|
-
Rails.logger.error "Error reconnecting: #{
|
451
|
+
rescue Exception => e # rubocop:todo Lint/RescueException
|
452
|
+
Rails.logger.error "Error reconnecting: #{e}"
|
453
453
|
end
|
454
454
|
end
|
455
455
|
end
|
data/lib/vanity/frameworks.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
|
-
# TODO turn this into a real rails engine jobbie
|
1
|
+
# TODO: turn this into a real rails engine jobbie
|
2
2
|
# Automatically configure Vanity.
|
3
3
|
if defined?(Rails)
|
4
4
|
class Plugin < Rails::Railtie # :nodoc:
|
5
|
-
initializer "vanity.require" do |
|
5
|
+
initializer "vanity.require" do |_app|
|
6
6
|
require 'vanity/frameworks/rails'
|
7
7
|
|
8
8
|
Vanity::Rails.load!
|
data/lib/vanity/helpers.rb
CHANGED
@@ -32,7 +32,7 @@ module Vanity
|
|
32
32
|
# render action: Vanity.ab_test(:new_page)
|
33
33
|
# end
|
34
34
|
# @since 1.2.0
|
35
|
-
def ab_test(name, request=nil, &block)
|
35
|
+
def ab_test(name, request = nil, &block)
|
36
36
|
request ||= Vanity.context.respond_to?(:request) ? Vanity.context.request : nil
|
37
37
|
|
38
38
|
alternative = Vanity.playground.experiment(name).choose(request)
|
@@ -1,6 +1,5 @@
|
|
1
1
|
module Vanity
|
2
2
|
class Metric
|
3
|
-
|
4
3
|
AGGREGATES = [:average, :minimum, :maximum, :sum]
|
5
4
|
|
6
5
|
# Use an ActiveRecord model to get metric data from database table. Also
|
@@ -35,15 +34,15 @@ module Vanity
|
|
35
34
|
# @since 1.2.0
|
36
35
|
# @see Vanity::Metric::ActiveRecord
|
37
36
|
def model(class_or_scope, options = nil)
|
38
|
-
ActiveSupport.on_load(:active_record, :
|
37
|
+
ActiveSupport.on_load(:active_record, yield: true) do
|
39
38
|
class_or_scope = class_or_scope.constantize if class_or_scope.is_a?(String)
|
40
|
-
options
|
39
|
+
options ||= {}
|
41
40
|
conditions = options.delete(:conditions)
|
42
41
|
|
43
42
|
@ar_scoped = conditions ? class_or_scope.where(conditions) : class_or_scope
|
44
43
|
@ar_aggregate = AGGREGATES.find { |key| options.has_key?(key) }
|
45
44
|
@ar_column = options.delete(@ar_aggregate)
|
46
|
-
|
45
|
+
raise "Cannot use multiple aggregates in a single metric" if AGGREGATES.find { |key| options.has_key?(key) }
|
47
46
|
|
48
47
|
@ar_timestamp = options.delete(:timestamp) || :created_at
|
49
48
|
@ar_timestamp, @ar_timestamp_table = @ar_timestamp.to_s.split('.').reverse
|
@@ -51,7 +50,7 @@ module Vanity
|
|
51
50
|
|
52
51
|
@ar_identity_block = options.delete(:identity)
|
53
52
|
|
54
|
-
|
53
|
+
raise "Unrecognized options: #{options.keys * ', '}" unless options.empty?
|
55
54
|
|
56
55
|
@ar_scoped.after_create(self)
|
57
56
|
extend ActiveRecord
|
@@ -63,31 +62,31 @@ module Vanity
|
|
63
62
|
#
|
64
63
|
# @since 1.3.0
|
65
64
|
module ActiveRecord
|
66
|
-
|
67
65
|
# This values method queries the database.
|
68
66
|
def values(sdate, edate)
|
69
67
|
time = Time.now.in_time_zone
|
70
68
|
difference = time.to_date - Date.today
|
71
|
-
sdate
|
72
|
-
edate
|
69
|
+
sdate += difference
|
70
|
+
edate += difference
|
73
71
|
|
74
72
|
grouped = @ar_scoped
|
75
|
-
|
76
|
-
|
73
|
+
.where(@ar_timestamp_table => { @ar_timestamp => (sdate.to_time...(edate + 1).to_time) })
|
74
|
+
.group("date(#{@ar_scoped.quoted_table_name}.#{@ar_scoped.connection.quote_column_name(@ar_timestamp)})")
|
77
75
|
|
78
|
-
if @ar_column
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
76
|
+
grouped = if @ar_column
|
77
|
+
grouped.send(@ar_aggregate, @ar_column)
|
78
|
+
else
|
79
|
+
grouped.count
|
80
|
+
end
|
83
81
|
|
84
|
-
grouped =
|
82
|
+
grouped = grouped.map { |k, v| [k.to_date, v] }.to_h
|
85
83
|
(sdate..edate).inject([]) { |ordered, date| ordered << (grouped[date] || 0) }
|
86
84
|
end
|
87
85
|
|
88
86
|
# This track! method stores nothing, but calls the hooks.
|
89
87
|
def track!(args = nil)
|
90
88
|
return unless @playground.collecting?
|
89
|
+
|
91
90
|
call_hooks(*track_args(args))
|
92
91
|
end
|
93
92
|
|
@@ -100,12 +99,15 @@ module Vanity
|
|
100
99
|
# AR model after_create callback notifies all the hooks.
|
101
100
|
def after_create(record)
|
102
101
|
return unless @playground.collecting?
|
102
|
+
|
103
103
|
count = @ar_column ? (record.send(@ar_column) || 0) : 1
|
104
104
|
|
105
|
-
identity =
|
106
|
-
|
107
|
-
|
105
|
+
identity = begin
|
106
|
+
Vanity.context.vanity_identity
|
107
|
+
rescue StandardError
|
108
|
+
nil
|
108
109
|
end
|
110
|
+
identity ||= (@ar_identity_block.call(record) if @ar_identity_block)
|
109
111
|
|
110
112
|
call_hooks record.send(@ar_timestamp), identity, [count] if count > 0 && @ar_scoped.exists?(record.id)
|
111
113
|
end
|
data/lib/vanity/metric/base.rb
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
module Vanity
|
2
|
-
|
3
2
|
# A metric is an object that implements two methods: +name+ and +values+. It
|
4
3
|
# can also respond to addition methods (+track!+, +bounds+, etc), these are
|
5
4
|
# optional.
|
@@ -10,7 +9,6 @@ module Vanity
|
|
10
9
|
#
|
11
10
|
# @since 1.1.0
|
12
11
|
class Metric
|
13
|
-
|
14
12
|
# These methods are available when defining a metric in a file loaded
|
15
13
|
# from the +experiments/metrics+ directory.
|
16
14
|
#
|
@@ -20,22 +18,22 @@ module Vanity
|
|
20
18
|
# description "Most boring metric ever"
|
21
19
|
# end
|
22
20
|
module Definition
|
23
|
-
|
24
21
|
attr_reader :playground
|
25
22
|
|
26
23
|
# Defines a new metric, using the class Vanity::Metric.
|
27
24
|
def metric(name, &block)
|
28
|
-
|
25
|
+
raise "Metric #{@metric_id} already defined in playground" if playground.metrics[@metric_id]
|
26
|
+
|
29
27
|
metric = Metric.new(playground, name.to_s, @metric_id)
|
30
28
|
metric.instance_eval(&block)
|
31
29
|
playground.metrics[@metric_id] = metric
|
32
30
|
end
|
33
31
|
|
34
32
|
def new_binding(playground, id)
|
35
|
-
@playground
|
33
|
+
@playground = playground
|
34
|
+
@metric_id = id
|
36
35
|
binding
|
37
36
|
end
|
38
|
-
|
39
37
|
end
|
40
38
|
|
41
39
|
# Startup metrics for pirates. AARRR stands for:
|
@@ -46,7 +44,6 @@ module Vanity
|
|
46
44
|
# * Revenue
|
47
45
|
# Read more: http://500hats.typepad.com/500blogs/2007/09/startup-metrics.html
|
48
46
|
class << self
|
49
|
-
|
50
47
|
# Helper method to return description for a metric.
|
51
48
|
#
|
52
49
|
# A metric object may have a +description+ method that returns a detailed
|
@@ -68,7 +65,7 @@ module Vanity
|
|
68
65
|
# @example
|
69
66
|
# upper = Vanity::Metric.bounds(metric).last
|
70
67
|
def bounds(metric)
|
71
|
-
metric.respond_to?(:bounds) && metric.bounds || [nil, nil]
|
68
|
+
(metric.respond_to?(:bounds) && metric.bounds) || [nil, nil]
|
72
69
|
end
|
73
70
|
|
74
71
|
# Returns data set for a given date range. The data set is an array of
|
@@ -92,37 +89,37 @@ module Vanity
|
|
92
89
|
|
93
90
|
# Playground uses this to load metric definitions.
|
94
91
|
def load(playground, stack, file)
|
95
|
-
|
92
|
+
raise "Circular dependency detected: #{stack.join('=>')}=>#{file}" if stack.include?(file)
|
93
|
+
|
96
94
|
source = File.read(file)
|
97
95
|
stack.push file
|
98
96
|
id = File.basename(file, ".rb").downcase.gsub(/\W/, "_").to_sym
|
99
97
|
context = Object.new
|
100
98
|
context.instance_eval do
|
101
99
|
extend Definition
|
102
|
-
metric = eval(source, context.new_binding(playground, id), file)
|
103
|
-
|
100
|
+
metric = eval(source, context.new_binding(playground, id), file) # rubocop:todo Security/Eval
|
101
|
+
raise NameError.new("Expected #{file} to define metric #{id}", id) unless playground.metrics[id]
|
102
|
+
|
104
103
|
metric
|
105
104
|
end
|
106
|
-
rescue
|
105
|
+
rescue StandardError
|
107
106
|
error = NameError.exception($!.message, id)
|
108
107
|
error.set_backtrace $!.backtrace
|
109
108
|
raise error
|
110
109
|
ensure
|
111
110
|
stack.pop
|
112
111
|
end
|
113
|
-
|
114
112
|
end
|
115
113
|
|
116
|
-
|
117
114
|
# Takes playground (need this to access Redis), friendly name and optional
|
118
115
|
# id (can infer from name).
|
119
116
|
def initialize(playground, name, id = nil)
|
120
|
-
@playground
|
117
|
+
@playground = playground
|
118
|
+
@name = name.to_s
|
121
119
|
@id = (id || name.to_s.downcase.gsub(/\W+/, '_')).to_sym
|
122
120
|
@hooks = []
|
123
121
|
end
|
124
122
|
|
125
|
-
|
126
123
|
# -- Tracking --
|
127
124
|
|
128
125
|
# Called to track an action associated with this metric. Most common is not
|
@@ -135,9 +132,10 @@ module Vanity
|
|
135
132
|
# foo_and_bar.track! [5,11]
|
136
133
|
def track!(args = nil)
|
137
134
|
return unless @playground.collecting?
|
135
|
+
|
138
136
|
timestamp, identity, values = track_args(args)
|
139
137
|
connection.metric_track @id, timestamp, identity, values
|
140
|
-
@playground.logger.info "vanity: #{@id} with value #{values.join(
|
138
|
+
@playground.logger.info "vanity: #{@id} with value #{values.join(', ')}"
|
141
139
|
call_hooks timestamp, identity, values
|
142
140
|
end
|
143
141
|
|
@@ -152,7 +150,11 @@ module Vanity
|
|
152
150
|
when Numeric
|
153
151
|
values = [args]
|
154
152
|
end
|
155
|
-
identity ||=
|
153
|
+
identity ||= begin
|
154
|
+
Vanity.context.vanity_identity
|
155
|
+
rescue StandardError
|
156
|
+
nil
|
157
|
+
end
|
156
158
|
[timestamp || Time.now, identity, values || [1]]
|
157
159
|
end
|
158
160
|
protected :track_args
|
@@ -183,12 +185,11 @@ module Vanity
|
|
183
185
|
def bounds
|
184
186
|
end
|
185
187
|
|
186
|
-
|
187
188
|
# -- Reporting --
|
188
189
|
|
189
190
|
# Human readable metric name. All metrics must implement this method.
|
190
191
|
attr_reader :name
|
191
|
-
alias
|
192
|
+
alias to_s name
|
192
193
|
|
193
194
|
# Human readable description. Use two newlines to break paragraphs.
|
194
195
|
attr_writer :description
|
@@ -218,7 +219,6 @@ module Vanity
|
|
218
219
|
connection.get_metric_last_update_at(@id)
|
219
220
|
end
|
220
221
|
|
221
|
-
|
222
222
|
# -- Storage --
|
223
223
|
|
224
224
|
def destroy!
|
@@ -235,9 +235,8 @@ module Vanity
|
|
235
235
|
|
236
236
|
def call_hooks(timestamp, identity, values)
|
237
237
|
@hooks.each do |hook|
|
238
|
-
hook.call @id, timestamp, values.first || 1, :identity
|
238
|
+
hook.call @id, timestamp, values.first || 1, identity: identity
|
239
239
|
end
|
240
240
|
end
|
241
|
-
|
242
241
|
end
|
243
242
|
end
|
@@ -1,6 +1,5 @@
|
|
1
1
|
module Vanity
|
2
2
|
class Metric
|
3
|
-
|
4
3
|
# Use Google Analytics metric. Note: you must +require "garb"+ before
|
5
4
|
# vanity.
|
6
5
|
#
|
@@ -17,13 +16,13 @@ module Vanity
|
|
17
16
|
# @see Vanity::Metric::GoogleAnalytics
|
18
17
|
def google_analytics(web_property_id, *args)
|
19
18
|
require "garb"
|
20
|
-
options =
|
19
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
21
20
|
metric = args.shift || :pageviews
|
22
21
|
@ga_resource = Vanity::Metric::GoogleAnalytics::Resource.new(web_property_id, metric)
|
23
|
-
@ga_mapper = options[:mapper] ||=
|
22
|
+
@ga_mapper = options[:mapper] ||= ->(entry) { entry.send(@ga_resource.metrics.elements.first).to_i }
|
24
23
|
extend GoogleAnalytics
|
25
24
|
rescue LoadError
|
26
|
-
|
25
|
+
raise LoadError, "Google Analytics metrics require Garb, please gem install garb first"
|
27
26
|
end
|
28
27
|
|
29
28
|
# Calling google_analytics method on a metric extends it with these modules,
|
@@ -31,19 +30,18 @@ module Vanity
|
|
31
30
|
#
|
32
31
|
# @since 1.3.0
|
33
32
|
module GoogleAnalytics
|
34
|
-
|
35
33
|
# Returns values from GA using parameters specified by prior call to
|
36
34
|
# google_analytics.
|
37
35
|
def values(from, to)
|
38
|
-
data = @ga_resource.results(from, to).inject({}) do |hash,entry|
|
39
|
-
hash.merge(entry.date
|
36
|
+
data = @ga_resource.results(from, to).inject({}) do |hash, entry|
|
37
|
+
hash.merge(entry.date => @ga_mapper.call(entry))
|
40
38
|
end
|
41
39
|
(from..to).map { |day| data[day.strftime('%Y%m%d')] || 0 }
|
42
40
|
end
|
43
41
|
|
44
42
|
# Hooks not supported for GA metrics.
|
45
43
|
def hook
|
46
|
-
|
44
|
+
raise "Cannot use hooks with Google Analytics methods"
|
47
45
|
end
|
48
46
|
|
49
47
|
# Garb report.
|
@@ -77,7 +75,6 @@ module Vanity
|
|
77
75
|
Garb::ReportResponse.new(send_request_for_body).results
|
78
76
|
end
|
79
77
|
end
|
80
|
-
|
81
78
|
end
|
82
79
|
end
|
83
80
|
end
|
data/lib/vanity/metric/remote.rb
CHANGED
@@ -3,7 +3,6 @@ require "cgi"
|
|
3
3
|
|
4
4
|
module Vanity
|
5
5
|
class Metric
|
6
|
-
|
7
6
|
# Specifies the base URL to use for a remote metric. For example:
|
8
7
|
# metric :sandbox do
|
9
8
|
# remote "http://api.vanitydash.com/metrics/sandbox"
|
@@ -26,9 +25,9 @@ module Vanity
|
|
26
25
|
# - Set values by their index using +values[0]+, +values[1]+, etc or
|
27
26
|
# - Set values by series name using +values[foo]+, +values[bar]+, etc.
|
28
27
|
module Remote
|
29
|
-
|
30
28
|
def track!(args = nil)
|
31
29
|
return unless @playground.collecting?
|
30
|
+
|
32
31
|
timestamp, identity, values = track_args(args)
|
33
32
|
params = ["metric=#{CGI.escape @id.to_s}", "timestamp=#{CGI.escape timestamp.httpdate}"]
|
34
33
|
params << "identity=#{CGI.escape identity.to_s}" if identity
|
@@ -36,9 +35,9 @@ module Vanity
|
|
36
35
|
params << @remote_url.query if @remote_url.query
|
37
36
|
@mutex.synchronize do
|
38
37
|
@http ||= Net::HTTP.start(@remote_url.host, @remote_url.port)
|
39
|
-
@http.request Net::HTTP::Post.new(@remote_url.path, "Content-Type"=>"application/x-www-form-urlencoded"), params.join("&")
|
38
|
+
@http.request Net::HTTP::Post.new(@remote_url.path, "Content-Type" => "application/x-www-form-urlencoded"), params.join("&")
|
40
39
|
end
|
41
|
-
rescue Timeout::Error, StandardError
|
40
|
+
rescue Timeout::Error, StandardError # rubocop:todo Lint/ShadowedException
|
42
41
|
@playground.logger.error "Error sending data for metric #{name}: #{$!}"
|
43
42
|
@http = nil
|
44
43
|
ensure
|
@@ -47,7 +46,6 @@ module Vanity
|
|
47
46
|
|
48
47
|
# "Don't worry, be crappy. Revolutionary means you ship and then test."
|
49
48
|
# -- Guy Kawazaki
|
50
|
-
|
51
49
|
end
|
52
50
|
end
|
53
51
|
end
|