Class: Vanity::Metric

Inherits:
Object
  • Object
show all
Defined in:
lib/vanity/metric/base.rb,
lib/vanity/metric/remote.rb,
lib/vanity/metric/active_record.rb,
lib/vanity/metric/google_analytics.rb

Overview

A metric is an object that implements two methods: name and values. It can also respond to addition methods (track!, bounds, etc), these are optional.

This class implements a basic metric that tracks data and stores it in the database. You can use this as the basis for your metric, or as reference for the methods your metric must and can implement.

Since:

  • 1.1.0

Defined Under Namespace

Modules: ActiveRecord, Definition, GoogleAnalytics, Remote

Constant Summary collapse

AGGREGATES =

Since:

  • 1.1.0

[:average, :minimum, :maximum, :sum]

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(playground, name, id = nil) ⇒ Metric

Takes playground (need this to access Redis), friendly name and optional id (can infer from name).

Since:

  • 1.1.0


116
117
118
119
120
121
# File 'lib/vanity/metric/base.rb', line 116

def initialize(playground, name, id = nil)
  @playground = playground
  @name = name.to_s
  @id = (id || name.to_s.downcase.gsub(/\W+/, '_')).to_sym
  @hooks = []
end

Instance Attribute Details

#description(text = nil) ⇒ Object

Sets or returns description. For example

metric "Yawns/sec" do
  description "Most boring metric ever"
end

puts "Just defined: " + metric(:boring).description

Since:

  • 1.1.0


203
204
205
206
# File 'lib/vanity/metric/base.rb', line 203

def description(text = nil)
  @description = text if text
  @description if defined?(@description)
end

#nameObject (readonly) Also known as: to_s

Human readable metric name. All metrics must implement this method.

Since:

  • 1.1.0


191
192
193
# File 'lib/vanity/metric/base.rb', line 191

def name
  @name
end

Class Method Details

.bounds(metric) ⇒ Object

Helper method to return bounds for a metric.

A metric object may have a bounds method that returns lower and upper bounds. It may also have no bounds, or no bounds # method, in which case we return [nil, nil].

Examples:

upper = Vanity::Metric.bounds(metric).last

Since:

  • 1.1.0


67
68
69
# File 'lib/vanity/metric/base.rb', line 67

def bounds(metric)
  (metric.respond_to?(:bounds) && metric.bounds) || [nil, nil]
end

.data(metric, *args) ⇒ Object

Returns data set for a given date range. The data set is an array of date, value pairs.

First argument is the metric. Second argument is the start date, or number of days to go back in history, defaults to 90 days. Third argument is end date, defaults to today.

Examples:

These are all equivalent:

Vanity::Metric.data(my_metric)
Vanity::Metric.data(my_metric, 90)
Vanity::Metric.data(my_metric, Date.today - 89)
Vanity::Metric.data(my_metric, Date.today - 89, Date.today)

Since:

  • 1.1.0


83
84
85
86
87
88
# File 'lib/vanity/metric/base.rb', line 83

def data(metric, *args)
  first = args.shift || 90
  to = args.shift || Date.today
  from = first.respond_to?(:to_date) ? first.to_date : to - (first - 1)
  (from..to).zip(metric.values(from, to))
end

.description(metric) ⇒ Object

Helper method to return description for a metric.

A metric object may have a description method that returns a detailed description. It may also have no description, or no description method, in which case return nil.

Examples:

puts Vanity::Metric.description(metric)

Since:

  • 1.1.0


55
56
57
# File 'lib/vanity/metric/base.rb', line 55

def description(metric)
  metric.description if metric.respond_to?(:description)
end

.load(playground, stack, file) ⇒ Object

Playground uses this to load metric definitions.

Since:

  • 1.1.0


91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/vanity/metric/base.rb', line 91

def load(playground, stack, file)
  raise "Circular dependency detected: #{stack.join('=>')}=>#{file}" if stack.include?(file)

  source = File.read(file)
  stack.push file
  id = File.basename(file, ".rb").downcase.gsub(/\W/, "_").to_sym
  context = Object.new
  context.instance_eval do
    extend Definition
    metric = eval(source, context.new_binding(playground, id), file) # rubocop:todo Security/Eval
    raise NameError.new("Expected #{file} to define metric #{id}", id) unless playground.metrics[id]

    metric
  end
rescue StandardError
  error = NameError.exception($!.message, id)
  error.set_backtrace $!.backtrace
  raise error
ensure
  stack.pop
end

Instance Method Details

#boundsObject

This method returns the acceptable bounds of a metric as an array with two values: low and high. Use nil for unbounded.

Alerts are created when metric values exceed their bounds. For example, a metric of user registration can use historical data to calculate expected range of new registration for the next day. If actual metric falls below the expected range, it could indicate registration process is broken. Going above higher bound could trigger opening a Champagne bottle.

The default implementation returns nil.

Since:

  • 1.1.0


185
186
# File 'lib/vanity/metric/base.rb', line 185

def bounds
end

#call_hooks(timestamp, identity, values) ⇒ Object

Since:

  • 1.1.0


236
237
238
239
240
# File 'lib/vanity/metric/base.rb', line 236

def call_hooks(timestamp, identity, values)
  @hooks.each do |hook|
    hook.call @id, timestamp, values.first || 1, identity: identity
  end
end

#connectionObject

Since:

  • 1.1.0


228
229
230
# File 'lib/vanity/metric/base.rb', line 228

def connection
  @playground.connection
end

#destroy!Object

– Storage –

Since:

  • 1.1.0


224
225
226
# File 'lib/vanity/metric/base.rb', line 224

def destroy!
  connection.destroy_metric @id
end

#google_analytics(web_property_id, *args) ⇒ Object

Use Google Analytics metric. Note: you must require “garb” before vanity.

Examples:

Page views

metric "Page views" do
  google_analytics "UA-1828623-6"
end

Visits

metric "Visits" do
  google_analytics "UA-1828623-6", :visits
end

See Also:

Since:

  • 1.3.0


17
18
19
20
21
22
23
24
25
26
# File 'lib/vanity/metric/google_analytics.rb', line 17

def google_analytics(web_property_id, *args)
  require "garb"
  options = args.last.is_a?(Hash) ? args.pop : {}
  metric = args.shift || :pageviews
  @ga_resource = Vanity::Metric::GoogleAnalytics::Resource.new(web_property_id, metric)
  @ga_mapper = options[:mapper] ||= ->(entry) { entry.send(@ga_resource.metrics.elements.first).to_i }
  extend GoogleAnalytics
rescue LoadError
  raise LoadError, "Google Analytics metrics require Garb, please gem install garb first"
end

#hook(&block) ⇒ Object

Metric definitions use this to introduce tracking hooks. The hook is called with metric identifier, timestamp, count and possibly additional arguments.

For example:

hook do |metric_id, timestamp, count|
  syslog.info metric_id
end

Since:

  • 1.1.0


170
171
172
# File 'lib/vanity/metric/base.rb', line 170

def hook(&block)
  @hooks << block
end

#key(*args) ⇒ Object

Since:

  • 1.1.0


232
233
234
# File 'lib/vanity/metric/base.rb', line 232

def key(*args)
  "metrics:#{@id}:#{args.join(':')}"
end

#last_update_atObject

Returns date/time of the last update to this metric.

Since:

  • 1.4.0


218
219
220
# File 'lib/vanity/metric/base.rb', line 218

def last_update_at
  connection.get_metric_last_update_at(@id)
end

#model(class_or_scope, options = nil) ⇒ Object

Use an ActiveRecord model to get metric data from database table. Also forwards after_create callbacks to hooks (updating experiments).

Supported options: :conditions – Only select records that match this condition :average – Metric value is average of this column :minimum – Metric value is minimum of this column :maximum – Metric value is maximum of this column :sum – Metric value is sum of this column :timestamp – Use this column to filter/group records (defaults to created_at)

Examples:

Track sign ups using User model

metric "Signups" do
  model Account
end

Track satisfaction using Survey model

metric "Satisfaction" do
  model Survey, :average=>:rating
end

Track only high ratings

metric "High ratings" do
  model Rating, :conditions=>["stars >= 4"]
end

Track only high ratings (using scope)

metric "High ratings" do
  model Rating.high
end

See Also:

Since:

  • 1.2.0


36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# File 'lib/vanity/metric/active_record.rb', line 36

def model(class_or_scope, options = nil)
  ActiveSupport.on_load(:active_record, yield: true) do
    class_or_scope = class_or_scope.constantize if class_or_scope.is_a?(String)
    options ||= {}
    conditions = options.delete(:conditions)

    @ar_scoped = conditions ? class_or_scope.where(conditions) : class_or_scope
    @ar_aggregate = AGGREGATES.find { |key| options.has_key?(key) }
    @ar_column = options.delete(@ar_aggregate)
    raise "Cannot use multiple aggregates in a single metric" if AGGREGATES.find { |key| options.has_key?(key) }

    @ar_timestamp = options.delete(:timestamp) || :created_at
    @ar_timestamp, @ar_timestamp_table = @ar_timestamp.to_s.split('.').reverse
    @ar_timestamp_table ||= @ar_scoped.table_name

    @ar_identity_block = options.delete(:identity)

    raise "Unrecognized options: #{options.keys * ', '}" unless options.empty?

    @ar_scoped.after_create(self)
    extend ActiveRecord
  end
end

#remote(url = nil) ⇒ Object

Specifies the base URL to use for a remote metric. For example:

metric :sandbox do
  remote "http://api.vanitydash.com/metrics/sandbox"
end

Since:

  • 1.1.0


10
11
12
13
14
15
# File 'lib/vanity/metric/remote.rb', line 10

def remote(url = nil)
  @remote_url = URI.parse(url) if url
  @mutex ||= Mutex.new
  extend Remote
  @remote_url
end

#track!(args = nil) ⇒ Object

Called to track an action associated with this metric. Most common is not passing an argument, and it tracks a count of 1. You can pass a different value as the argument, or array of value (for multi-series metrics), or hash with the optional keys timestamp, identity and values.

Example:

hits.track!
foo_and_bar.track! [5,11]

Since:

  • 1.1.0


133
134
135
136
137
138
139
140
# File 'lib/vanity/metric/base.rb', line 133

def track!(args = nil)
  return unless @playground.collecting?

  timestamp, identity, values = track_args(args)
  connection.metric_track @id, timestamp, identity, values
  @playground.logger.info "vanity: #{@id} with value #{values.join(', ')}"
  call_hooks timestamp, identity, values
end

#values(from, to) ⇒ Object

Given two arguments, a start date and an end date (inclusive), returns an array of measurements. All metrics must implement this method.

Since:

  • 1.1.0


210
211
212
213
# File 'lib/vanity/metric/base.rb', line 210

def values(from, to)
  values = connection.metric_values(@id, from, to)
  values.map { |row| row.first.to_i }
end