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