moses-vanity 1.7.1
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.
- data/.autotest +22 -0
- data/.gitignore +7 -0
- data/.rvmrc +3 -0
- data/.travis.yml +13 -0
- data/CHANGELOG +374 -0
- data/Gemfile +28 -0
- data/MIT-LICENSE +21 -0
- data/README.rdoc +108 -0
- data/Rakefile +189 -0
- data/bin/vanity +16 -0
- data/doc/_config.yml +2 -0
- data/doc/_layouts/_header.html +34 -0
- data/doc/_layouts/page.html +47 -0
- data/doc/_metrics.textile +12 -0
- data/doc/ab_testing.textile +210 -0
- data/doc/configuring.textile +45 -0
- data/doc/contributing.textile +93 -0
- data/doc/credits.textile +23 -0
- data/doc/css/page.css +83 -0
- data/doc/css/print.css +43 -0
- data/doc/css/syntax.css +7 -0
- data/doc/email.textile +129 -0
- data/doc/experimental.textile +31 -0
- data/doc/faq.textile +8 -0
- data/doc/identity.textile +43 -0
- data/doc/images/ab_in_dashboard.png +0 -0
- data/doc/images/clear_winner.png +0 -0
- data/doc/images/price_options.png +0 -0
- data/doc/images/sidebar_test.png +0 -0
- data/doc/images/signup_metric.png +0 -0
- data/doc/images/vanity.png +0 -0
- data/doc/index.textile +91 -0
- data/doc/metrics.textile +231 -0
- data/doc/rails.textile +89 -0
- data/doc/site.js +27 -0
- data/generators/templates/vanity_migration.rb +53 -0
- data/generators/vanity_generator.rb +8 -0
- data/lib/generators/templates/vanity_migration.rb +53 -0
- data/lib/generators/vanity_generator.rb +15 -0
- data/lib/vanity.rb +36 -0
- data/lib/vanity/adapters/abstract_adapter.rb +140 -0
- data/lib/vanity/adapters/active_record_adapter.rb +248 -0
- data/lib/vanity/adapters/mock_adapter.rb +157 -0
- data/lib/vanity/adapters/mongodb_adapter.rb +178 -0
- data/lib/vanity/adapters/redis_adapter.rb +160 -0
- data/lib/vanity/backport.rb +26 -0
- data/lib/vanity/commands/list.rb +21 -0
- data/lib/vanity/commands/report.rb +64 -0
- data/lib/vanity/commands/upgrade.rb +34 -0
- data/lib/vanity/experiment/ab_test.rb +507 -0
- data/lib/vanity/experiment/base.rb +214 -0
- data/lib/vanity/frameworks.rb +16 -0
- data/lib/vanity/frameworks/rails.rb +318 -0
- data/lib/vanity/helpers.rb +66 -0
- data/lib/vanity/images/x.gif +0 -0
- data/lib/vanity/metric/active_record.rb +85 -0
- data/lib/vanity/metric/base.rb +244 -0
- data/lib/vanity/metric/google_analytics.rb +83 -0
- data/lib/vanity/metric/remote.rb +53 -0
- data/lib/vanity/playground.rb +396 -0
- data/lib/vanity/templates/_ab_test.erb +28 -0
- data/lib/vanity/templates/_experiment.erb +5 -0
- data/lib/vanity/templates/_experiments.erb +7 -0
- data/lib/vanity/templates/_metric.erb +14 -0
- data/lib/vanity/templates/_metrics.erb +13 -0
- data/lib/vanity/templates/_report.erb +27 -0
- data/lib/vanity/templates/_vanity.js.erb +20 -0
- data/lib/vanity/templates/flot.min.js +1 -0
- data/lib/vanity/templates/jquery.min.js +19 -0
- data/lib/vanity/templates/vanity.css +26 -0
- data/lib/vanity/templates/vanity.js +82 -0
- data/lib/vanity/version.rb +11 -0
- data/test/adapters/redis_adapter_test.rb +17 -0
- data/test/experiment/ab_test.rb +771 -0
- data/test/experiment/base_test.rb +150 -0
- data/test/experiments/age_and_zipcode.rb +19 -0
- data/test/experiments/metrics/cheers.rb +3 -0
- data/test/experiments/metrics/signups.rb +2 -0
- data/test/experiments/metrics/yawns.rb +3 -0
- data/test/experiments/null_abc.rb +5 -0
- data/test/metric/active_record_test.rb +277 -0
- data/test/metric/base_test.rb +293 -0
- data/test/metric/google_analytics_test.rb +104 -0
- data/test/metric/remote_test.rb +109 -0
- data/test/myapp/app/controllers/application_controller.rb +2 -0
- data/test/myapp/app/controllers/main_controller.rb +7 -0
- data/test/myapp/config/boot.rb +110 -0
- data/test/myapp/config/environment.rb +10 -0
- data/test/myapp/config/environments/production.rb +0 -0
- data/test/myapp/config/routes.rb +3 -0
- data/test/passenger_test.rb +43 -0
- data/test/playground_test.rb +26 -0
- data/test/rails_dashboard_test.rb +37 -0
- data/test/rails_helper_test.rb +36 -0
- data/test/rails_test.rb +389 -0
- data/test/test_helper.rb +145 -0
- data/vanity.gemspec +26 -0
- metadata +202 -0
@@ -0,0 +1,66 @@
|
|
1
|
+
module Vanity
|
2
|
+
# Helper methods available on Object.
|
3
|
+
#
|
4
|
+
# @example From ERB template
|
5
|
+
# <%= ab_test(:greeting) %> <%= current_user.name %>
|
6
|
+
# @example From Rails controller
|
7
|
+
# class AccountController < ApplicationController
|
8
|
+
# def create
|
9
|
+
# track! :signup
|
10
|
+
# Acccount.create! params[:account]
|
11
|
+
# end
|
12
|
+
# end
|
13
|
+
# @example From ActiveRecord
|
14
|
+
# class Posts < ActiveRecord::Base
|
15
|
+
# after_create do |post|
|
16
|
+
# track! :images if post.type == :image
|
17
|
+
# end
|
18
|
+
# end
|
19
|
+
module Helpers
|
20
|
+
|
21
|
+
# This method returns one of the alternative values in the named A/B test.
|
22
|
+
#
|
23
|
+
# @example A/B two alternatives for a page
|
24
|
+
# def index
|
25
|
+
# if ab_test(:new_page) # true/false test
|
26
|
+
# render action: "new_page"
|
27
|
+
# else
|
28
|
+
# render action: "index"
|
29
|
+
# end
|
30
|
+
# end
|
31
|
+
# @example Similar, alternative value is page name
|
32
|
+
# def index
|
33
|
+
# render action: ab_test(:new_page)
|
34
|
+
# end
|
35
|
+
# @since 1.2.0
|
36
|
+
def ab_test(name, &block)
|
37
|
+
if Vanity.playground.using_js?
|
38
|
+
@_vanity_experiments ||= {}
|
39
|
+
@_vanity_experiments[name] ||= Vanity.playground.experiment(name).choose
|
40
|
+
value = @_vanity_experiments[name].value
|
41
|
+
else
|
42
|
+
value = Vanity.playground.experiment(name).choose.value
|
43
|
+
end
|
44
|
+
|
45
|
+
if block
|
46
|
+
content = capture(value, &block)
|
47
|
+
block_called_from_erb?(block) ? concat(content) : content
|
48
|
+
else
|
49
|
+
value
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Tracks an action associated with a metric.
|
54
|
+
#
|
55
|
+
# @example
|
56
|
+
# track! :invitation
|
57
|
+
# @since 1.2.0
|
58
|
+
def track!(name, count = 1)
|
59
|
+
Vanity.playground.track! name, count
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
Object.class_eval do
|
65
|
+
include Vanity::Helpers
|
66
|
+
end
|
Binary file
|
@@ -0,0 +1,85 @@
|
|
1
|
+
module Vanity
|
2
|
+
class Metric
|
3
|
+
|
4
|
+
AGGREGATES = [:average, :minimum, :maximum, :sum]
|
5
|
+
|
6
|
+
# Use an ActiveRecord model to get metric data from database table. Also
|
7
|
+
# forwards +after_create+ callbacks to hooks (updating experiments).
|
8
|
+
#
|
9
|
+
# Supported options:
|
10
|
+
# :conditions -- Only select records that match this condition
|
11
|
+
# :average -- Metric value is average of this column
|
12
|
+
# :minimum -- Metric value is minimum of this column
|
13
|
+
# :maximum -- Metric value is maximum of this column
|
14
|
+
# :sum -- Metric value is sum of this column
|
15
|
+
# :timestamp -- Use this column to filter/group records (defaults to
|
16
|
+
# +created_at+)
|
17
|
+
#
|
18
|
+
# @example Track sign ups using User model
|
19
|
+
# metric "Signups" do
|
20
|
+
# model Account
|
21
|
+
# end
|
22
|
+
# @example Track satisfaction using Survey model
|
23
|
+
# metric "Satisfaction" do
|
24
|
+
# model Survey, :average=>:rating
|
25
|
+
# end
|
26
|
+
# @example Track only high ratings
|
27
|
+
# metric "High ratings" do
|
28
|
+
# model Rating, :conditions=>["stars >= 4"]
|
29
|
+
# end
|
30
|
+
# @example Track only high ratings (using scope)
|
31
|
+
# metric "High ratings" do
|
32
|
+
# model Rating.high
|
33
|
+
# end
|
34
|
+
#
|
35
|
+
# @since 1.2.0
|
36
|
+
# @see Vanity::Metric::ActiveRecord
|
37
|
+
def model(class_or_scope, options = nil)
|
38
|
+
options = options || {}
|
39
|
+
conditions = options.delete(:conditions)
|
40
|
+
@ar_scoped = conditions ? class_or_scope.scoped(:conditions=>conditions) : class_or_scope
|
41
|
+
@ar_aggregate = AGGREGATES.find { |key| options.has_key?(key) }
|
42
|
+
@ar_column = options.delete(@ar_aggregate)
|
43
|
+
fail "Cannot use multiple aggregates in a single metric" if AGGREGATES.find { |key| options.has_key?(key) }
|
44
|
+
@ar_timestamp = options.delete(:timestamp) || :created_at
|
45
|
+
@ar_timestamp, @ar_timestamp_table = @ar_timestamp.to_s.split('.').reverse
|
46
|
+
@ar_timestamp_table ||= @ar_scoped.table_name
|
47
|
+
fail "Unrecognized options: #{options.keys * ", "}" unless options.empty?
|
48
|
+
@ar_scoped.after_create self
|
49
|
+
extend ActiveRecord
|
50
|
+
end
|
51
|
+
|
52
|
+
# Calling model method on a metric extends it with these modules, redefining
|
53
|
+
# the values and track! methods.
|
54
|
+
#
|
55
|
+
# @since 1.3.0
|
56
|
+
module ActiveRecord
|
57
|
+
|
58
|
+
# This values method queries the database.
|
59
|
+
def values(sdate, edate)
|
60
|
+
query = { :conditions=> { @ar_timestamp_table => { @ar_timestamp => (sdate.to_time...(edate + 1).to_time) } },
|
61
|
+
:group=>"date(#{@ar_scoped.quoted_table_name}.#{@ar_scoped.connection.quote_column_name @ar_timestamp})" }
|
62
|
+
grouped = @ar_column ? @ar_scoped.send(@ar_aggregate, @ar_column, query) : @ar_scoped.count(query)
|
63
|
+
(sdate..edate).inject([]) { |ordered, date| ordered << (grouped[date.to_s] || 0) }
|
64
|
+
end
|
65
|
+
|
66
|
+
# This track! method stores nothing, but calls the hooks.
|
67
|
+
def track!(args = nil)
|
68
|
+
return unless @playground.collecting?
|
69
|
+
call_hooks *track_args(args)
|
70
|
+
end
|
71
|
+
|
72
|
+
def last_update_at
|
73
|
+
record = @ar_scoped.find(:first, :order=>"#@ar_timestamp DESC", :limit=>1, :select=>@ar_timestamp)
|
74
|
+
record && record.send(@ar_timestamp)
|
75
|
+
end
|
76
|
+
|
77
|
+
# AR model after_create callback notifies all the hooks.
|
78
|
+
def after_create(record)
|
79
|
+
return unless @playground.collecting?
|
80
|
+
count = @ar_column ? (record.send(@ar_column) || 0) : 1
|
81
|
+
call_hooks record.send(@ar_timestamp), nil, [count] if count > 0 && @ar_scoped.exists?(record)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,244 @@
|
|
1
|
+
module Vanity
|
2
|
+
|
3
|
+
# A metric is an object that implements two methods: +name+ and +values+. It
|
4
|
+
# can also respond to addition methods (+track!+, +bounds+, etc), these are
|
5
|
+
# optional.
|
6
|
+
#
|
7
|
+
# This class implements a basic metric that tracks data and stores it in the
|
8
|
+
# database. You can use this as the basis for your metric, or as reference
|
9
|
+
# for the methods your metric must and can implement.
|
10
|
+
#
|
11
|
+
# @since 1.1.0
|
12
|
+
class Metric
|
13
|
+
|
14
|
+
# These methods are available when defining a metric in a file loaded
|
15
|
+
# from the +experiments/metrics+ directory.
|
16
|
+
#
|
17
|
+
# For example:
|
18
|
+
# $ cat experiments/metrics/yawn_sec
|
19
|
+
# metric "Yawns/sec" do
|
20
|
+
# description "Most boring metric ever"
|
21
|
+
# end
|
22
|
+
module Definition
|
23
|
+
|
24
|
+
attr_reader :playground
|
25
|
+
|
26
|
+
# Defines a new metric, using the class Vanity::Metric.
|
27
|
+
def metric(name, &block)
|
28
|
+
fail "Metric #{@metric_id} already defined in playground" if playground.metrics[@metric_id]
|
29
|
+
metric = Metric.new(playground, name.to_s, @metric_id)
|
30
|
+
metric.instance_eval &block
|
31
|
+
playground.metrics[@metric_id] = metric
|
32
|
+
end
|
33
|
+
|
34
|
+
def new_binding(playground, id)
|
35
|
+
@playground, @metric_id = playground, id
|
36
|
+
binding
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
# Startup metrics for pirates. AARRR stands for:
|
42
|
+
# * Acquisition
|
43
|
+
# * Activation
|
44
|
+
# * Retention
|
45
|
+
# * Referral
|
46
|
+
# * Revenue
|
47
|
+
# Read more: http://500hats.typepad.com/500blogs/2007/09/startup-metrics.html
|
48
|
+
|
49
|
+
class << self
|
50
|
+
|
51
|
+
# Helper method to return description for a metric.
|
52
|
+
#
|
53
|
+
# A metric object may have a +description+ method that returns a detailed
|
54
|
+
# description. It may also have no description, or no +description+
|
55
|
+
# method, in which case return +nil+.
|
56
|
+
#
|
57
|
+
# @example
|
58
|
+
# puts Vanity::Metric.description(metric)
|
59
|
+
def description(metric)
|
60
|
+
metric.description if metric.respond_to?(:description)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Helper method to return bounds for a metric.
|
64
|
+
#
|
65
|
+
# A metric object may have a +bounds+ method that returns lower and upper
|
66
|
+
# bounds. It may also have no bounds, or no +bounds+ # method, in which
|
67
|
+
# case we return +[nil, nil]+.
|
68
|
+
#
|
69
|
+
# @example
|
70
|
+
# upper = Vanity::Metric.bounds(metric).last
|
71
|
+
def bounds(metric)
|
72
|
+
metric.respond_to?(:bounds) && metric.bounds || [nil, nil]
|
73
|
+
end
|
74
|
+
|
75
|
+
# Returns data set for a given date range. The data set is an array of
|
76
|
+
# date, value pairs.
|
77
|
+
#
|
78
|
+
# First argument is the metric. Second argument is the start date, or
|
79
|
+
# number of days to go back in history, defaults to 90 days. Third
|
80
|
+
# argument is end date, defaults to today.
|
81
|
+
#
|
82
|
+
# @example These are all equivalent:
|
83
|
+
# Vanity::Metric.data(my_metric)
|
84
|
+
# Vanity::Metric.data(my_metric, 90)
|
85
|
+
# Vanity::Metric.data(my_metric, Date.today - 89)
|
86
|
+
# Vanity::Metric.data(my_metric, Date.today - 89, Date.today)
|
87
|
+
def data(metric, *args)
|
88
|
+
first = args.shift || 90
|
89
|
+
to = args.shift || Date.today
|
90
|
+
from = first.respond_to?(:to_date) ? first.to_date : to - (first - 1)
|
91
|
+
(from..to).zip(metric.values(from, to))
|
92
|
+
end
|
93
|
+
|
94
|
+
# Playground uses this to load metric definitions.
|
95
|
+
def load(playground, stack, file)
|
96
|
+
fail "Circular dependency detected: #{stack.join('=>')}=>#{file}" if stack.include?(file)
|
97
|
+
source = File.read(file)
|
98
|
+
stack.push file
|
99
|
+
id = File.basename(file, ".rb").downcase.gsub(/\W/, "_").to_sym
|
100
|
+
context = Object.new
|
101
|
+
context.instance_eval do
|
102
|
+
extend Definition
|
103
|
+
metric = eval(source, context.new_binding(playground, id), file)
|
104
|
+
fail NameError.new("Expected #{file} to define metric #{id}", id) unless playground.metrics[id]
|
105
|
+
metric
|
106
|
+
end
|
107
|
+
rescue
|
108
|
+
error = NameError.exception($!.message, id)
|
109
|
+
error.set_backtrace $!.backtrace
|
110
|
+
raise error
|
111
|
+
ensure
|
112
|
+
stack.pop
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
116
|
+
|
117
|
+
|
118
|
+
# Takes playground (need this to access Redis), friendly name and optional
|
119
|
+
# id (can infer from name).
|
120
|
+
def initialize(playground, name, id = nil)
|
121
|
+
@playground, @name = playground, name.to_s
|
122
|
+
@id = (id || name.to_s.downcase.gsub(/\W+/, '_')).to_sym
|
123
|
+
@hooks = []
|
124
|
+
end
|
125
|
+
|
126
|
+
|
127
|
+
# -- Tracking --
|
128
|
+
|
129
|
+
# Called to track an action associated with this metric. Most common is not
|
130
|
+
# passing an argument, and it tracks a count of 1. You can pass a different
|
131
|
+
# value as the argument, or array of value (for multi-series metrics), or
|
132
|
+
# hash with the optional keys timestamp, identity and values.
|
133
|
+
#
|
134
|
+
# Example:
|
135
|
+
# hits.track!
|
136
|
+
# foo_and_bar.track! [5,11]
|
137
|
+
def track!(args = nil)
|
138
|
+
return unless @playground.collecting?
|
139
|
+
timestamp, identity, values = track_args(args)
|
140
|
+
connection.metric_track @id, timestamp, identity, values
|
141
|
+
@playground.logger.info "vanity: #{@id} with value #{values.join(", ")}"
|
142
|
+
call_hooks timestamp, identity, values
|
143
|
+
end
|
144
|
+
|
145
|
+
# Parses arguments to track! method and return array with timestamp,
|
146
|
+
# identity and array of values.
|
147
|
+
def track_args(args)
|
148
|
+
case args
|
149
|
+
when Hash
|
150
|
+
timestamp, identity, values = args.values_at(:timestamp, :identity, :values)
|
151
|
+
when Array
|
152
|
+
values = args
|
153
|
+
when Numeric
|
154
|
+
values = [args]
|
155
|
+
end
|
156
|
+
identity = Vanity.context.vanity_identity rescue nil
|
157
|
+
[timestamp || Time.now, identity, values || [1]]
|
158
|
+
end
|
159
|
+
protected :track_args
|
160
|
+
|
161
|
+
# Metric definitions use this to introduce tracking hook. The hook is
|
162
|
+
# called with metric identifier, timestamp, count and possibly additional
|
163
|
+
# arguments.
|
164
|
+
#
|
165
|
+
# For example:
|
166
|
+
# hook do |metric_id, timestamp, count|
|
167
|
+
# syslog.info metric_id
|
168
|
+
# end
|
169
|
+
def hook(&block)
|
170
|
+
@hooks << block
|
171
|
+
end
|
172
|
+
|
173
|
+
# This method returns the acceptable bounds of a metric as an array with
|
174
|
+
# two values: low and high. Use nil for unbounded.
|
175
|
+
#
|
176
|
+
# Alerts are created when metric values exceed their bounds. For example,
|
177
|
+
# a metric of user registration can use historical data to calculate
|
178
|
+
# expected range of new registration for the next day. If actual metric
|
179
|
+
# falls below the expected range, it could indicate registration process is
|
180
|
+
# broken. Going above higher bound could trigger opening a Champagne
|
181
|
+
# bottle.
|
182
|
+
#
|
183
|
+
# The default implementation returns +nil+.
|
184
|
+
def bounds
|
185
|
+
end
|
186
|
+
|
187
|
+
|
188
|
+
# -- Reporting --
|
189
|
+
|
190
|
+
# Human readable metric name. All metrics must implement this method.
|
191
|
+
attr_reader :name
|
192
|
+
alias :to_s :name
|
193
|
+
|
194
|
+
# Human readable description. Use two newlines to break paragraphs.
|
195
|
+
attr_accessor :description
|
196
|
+
|
197
|
+
# Sets or returns description. For example
|
198
|
+
# metric "Yawns/sec" do
|
199
|
+
# description "Most boring metric ever"
|
200
|
+
# end
|
201
|
+
#
|
202
|
+
# puts "Just defined: " + metric(:boring).description
|
203
|
+
def description(text = nil)
|
204
|
+
@description = text if text
|
205
|
+
@description
|
206
|
+
end
|
207
|
+
|
208
|
+
# Given two arguments, a start date and an end date (inclusive), returns an
|
209
|
+
# array of measurements. All metrics must implement this method.
|
210
|
+
def values(from, to)
|
211
|
+
values = connection.metric_values(@id, from, to)
|
212
|
+
values.map { |row| row.first.to_i }
|
213
|
+
end
|
214
|
+
|
215
|
+
# Returns date/time of the last update to this metric.
|
216
|
+
#
|
217
|
+
# @since 1.4.0
|
218
|
+
def last_update_at
|
219
|
+
connection.get_metric_last_update_at(@id)
|
220
|
+
end
|
221
|
+
|
222
|
+
|
223
|
+
# -- Storage --
|
224
|
+
|
225
|
+
def destroy!
|
226
|
+
connection.destroy_metric @id
|
227
|
+
end
|
228
|
+
|
229
|
+
def connection
|
230
|
+
@playground.connection
|
231
|
+
end
|
232
|
+
|
233
|
+
def key(*args)
|
234
|
+
"metrics:#{@id}:#{args.join(':')}"
|
235
|
+
end
|
236
|
+
|
237
|
+
def call_hooks(timestamp, identity, values)
|
238
|
+
@hooks.each do |hook|
|
239
|
+
hook.call @id, timestamp, values.first || 1
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
end
|
244
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
module Vanity
|
2
|
+
class Metric
|
3
|
+
|
4
|
+
# Use Google Analytics metric. Note: you must +require "garb"+ before
|
5
|
+
# vanity.
|
6
|
+
#
|
7
|
+
# @example Page views
|
8
|
+
# metric "Page views" do
|
9
|
+
# google_analytics "UA-1828623-6"
|
10
|
+
# end
|
11
|
+
# @example Visits
|
12
|
+
# metric "Visits" do
|
13
|
+
# google_analytics "UA-1828623-6", :visits
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# @since 1.3.0
|
17
|
+
# @see Vanity::Metric::GoogleAnalytics
|
18
|
+
def google_analytics(web_property_id, *args)
|
19
|
+
require "garb"
|
20
|
+
options = Hash === args.last ? args.pop : {}
|
21
|
+
metric = args.shift || :pageviews
|
22
|
+
@ga_resource = Vanity::Metric::GoogleAnalytics::Resource.new(web_property_id, metric)
|
23
|
+
@ga_mapper = options[:mapper] ||= lambda { |entry| entry.send(@ga_resource.metrics.elements.first).to_i }
|
24
|
+
extend GoogleAnalytics
|
25
|
+
rescue LoadError
|
26
|
+
fail LoadError, "Google Analytics metrics require Garb, please gem install garb first"
|
27
|
+
end
|
28
|
+
|
29
|
+
# Calling google_analytics method on a metric extends it with these modules,
|
30
|
+
# redefining the values and hook methods.
|
31
|
+
#
|
32
|
+
# @since 1.3.0
|
33
|
+
module GoogleAnalytics
|
34
|
+
|
35
|
+
# Returns values from GA using parameters specified by prior call to
|
36
|
+
# google_analytics.
|
37
|
+
def values(from, to)
|
38
|
+
data = @ga_resource.results(from, to).inject({}) do |hash,entry|
|
39
|
+
hash.merge(entry.date=>@ga_mapper.call(entry))
|
40
|
+
end
|
41
|
+
(from..to).map { |day| data[day.strftime('%Y%m%d')] || 0 }
|
42
|
+
end
|
43
|
+
|
44
|
+
# Hooks not supported for GA metrics.
|
45
|
+
def hook
|
46
|
+
fail "Cannot use hooks with Google Analytics methods"
|
47
|
+
end
|
48
|
+
|
49
|
+
# Garb report.
|
50
|
+
def report
|
51
|
+
@ga_resource
|
52
|
+
end
|
53
|
+
|
54
|
+
# Unkown (for now).
|
55
|
+
def last_update_at
|
56
|
+
end
|
57
|
+
|
58
|
+
def track!(args = nil)
|
59
|
+
end
|
60
|
+
|
61
|
+
class Resource
|
62
|
+
# GA profile used for this report. Populated after calling results.
|
63
|
+
attr_reader :profile
|
64
|
+
|
65
|
+
def initialize(web_property_id, metric)
|
66
|
+
self.class.send :include, Garb::Resource
|
67
|
+
@web_property_id = web_property_id
|
68
|
+
metrics metric
|
69
|
+
dimensions :date
|
70
|
+
sort :date
|
71
|
+
end
|
72
|
+
|
73
|
+
def results(start_date, end_date)
|
74
|
+
@profile = Garb::Profile.all.find { |p| p.web_property_id == @web_property_id }
|
75
|
+
@start_date = start_date
|
76
|
+
@end_date = end_date
|
77
|
+
Garb::ReportResponse.new(send_request_for_body).results
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|