vanity 1.2.0 → 1.3.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.
- data/CHANGELOG +34 -0
- data/Gemfile +16 -0
- data/MIT-LICENSE +1 -1
- data/README.rdoc +10 -5
- data/Rakefile +119 -0
- data/bin/vanity +23 -18
- data/lib/vanity.rb +12 -4
- data/lib/vanity/commands.rb +1 -0
- data/lib/vanity/commands/list.rb +21 -0
- data/lib/vanity/experiment/ab_test.rb +8 -1
- data/lib/vanity/experiment/base.rb +40 -30
- data/lib/vanity/frameworks/rails.rb +222 -0
- data/lib/vanity/metric/active_record.rb +77 -0
- data/lib/vanity/{metric.rb → metric/base.rb} +6 -71
- data/lib/vanity/metric/google_analytics.rb +76 -0
- data/lib/vanity/playground.rb +93 -44
- data/lib/vanity/templates/_metric.erb +12 -7
- data/lib/vanity/templates/vanity.css +1 -0
- data/test/ab_test_test.rb +69 -48
- data/test/experiment_test.rb +29 -15
- data/test/metric_test.rb +104 -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/myapp/log/production.log +80 -0
- data/test/passenger_test.rb +34 -0
- data/test/rails_test.rb +129 -1
- data/test/test_helper.rb +12 -4
- data/vanity.gemspec +2 -2
- data/vendor/cache/RedCloth-4.2.2.gem +0 -0
- data/vendor/cache/actionmailer-2.3.5.gem +0 -0
- data/vendor/cache/actionpack-2.3.5.gem +0 -0
- data/vendor/cache/activerecord-2.3.5.gem +0 -0
- data/vendor/cache/activeresource-2.3.5.gem +0 -0
- data/vendor/cache/activesupport-2.3.5.gem +0 -0
- data/vendor/cache/autotest-4.2.7.gem +0 -0
- data/vendor/cache/autotest-fsevent-0.2.1.gem +0 -0
- data/vendor/cache/autotest-growl-0.2.0.gem +0 -0
- data/vendor/cache/bundler-0.9.7.gem +0 -0
- data/vendor/cache/classifier-1.3.1.gem +0 -0
- data/vendor/cache/directory_watcher-1.3.1.gem +0 -0
- data/vendor/cache/fastthread-1.0.7.gem +0 -0
- data/vendor/cache/garb-0.7.0.gem +0 -0
- data/vendor/cache/happymapper-0.3.0.gem +0 -0
- data/vendor/cache/jekyll-0.5.7.gem +0 -0
- data/vendor/cache/libxml-ruby-1.1.3.gem +0 -0
- data/vendor/cache/liquid-2.0.0.gem +0 -0
- data/vendor/cache/maruku-0.6.0.gem +0 -0
- data/vendor/cache/mocha-0.9.8.gem +0 -0
- data/vendor/cache/open4-1.0.1.gem +0 -0
- data/vendor/cache/passenger-2.2.9.gem +0 -0
- data/vendor/cache/rack-1.0.1.gem +0 -0
- data/vendor/cache/rails-2.3.5.gem +0 -0
- data/vendor/cache/rake-0.8.7.gem +0 -0
- data/vendor/cache/rubygems-update-1.3.5.gem +0 -0
- data/vendor/cache/shoulda-2.10.3.gem +0 -0
- data/vendor/cache/sqlite3-ruby-1.2.5.gem +0 -0
- data/vendor/cache/stemmer-1.0.1.gem +0 -0
- data/vendor/cache/syntax-1.0.0.gem +0 -0
- data/vendor/cache/sys-uname-0.8.4.gem +0 -0
- data/vendor/cache/timecop-0.3.4.gem +0 -0
- metadata +60 -11
- data/lib/vanity/rails.rb +0 -22
- data/lib/vanity/rails/dashboard.rb +0 -24
- data/lib/vanity/rails/helpers.rb +0 -101
- data/lib/vanity/rails/testing.rb +0 -11
@@ -0,0 +1,222 @@
|
|
1
|
+
module Vanity
|
2
|
+
module Rails #:nodoc:
|
3
|
+
# The use_vanity method will setup the controller to allow testing and
|
4
|
+
# tracking of the current user.
|
5
|
+
module UseVanity
|
6
|
+
# Defines the vanity_identity method and the set_identity_context filter.
|
7
|
+
#
|
8
|
+
# Call with the name of a method that returns an object whose identity
|
9
|
+
# will be used as the Vanity identity. Confusing? Let's try by example:
|
10
|
+
#
|
11
|
+
# class ApplicationController < ActionController::Base
|
12
|
+
# use_vanity :current_user
|
13
|
+
#
|
14
|
+
# def current_user
|
15
|
+
# User.find(session[:user_id])
|
16
|
+
# end
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# If that method (current_user in this example) returns nil, Vanity will
|
20
|
+
# set the identity for you (using a cookie to remember it across
|
21
|
+
# requests). It also uses this mechanism if you don't provide an
|
22
|
+
# identity object, by calling use_vanity with no arguments.
|
23
|
+
#
|
24
|
+
# Of course you can also use a block:
|
25
|
+
# class ProjectController < ApplicationController
|
26
|
+
# use_vanity { |controller| controller.params[:project_id] }
|
27
|
+
# end
|
28
|
+
def use_vanity(symbol = nil, &block)
|
29
|
+
if block
|
30
|
+
define_method(:vanity_identity) { block.call(self) }
|
31
|
+
else
|
32
|
+
define_method :vanity_identity do
|
33
|
+
return @vanity_identity if @vanity_identity
|
34
|
+
if symbol && object = send(symbol)
|
35
|
+
@vanity_identity = object.id
|
36
|
+
elsif response # everyday use
|
37
|
+
@vanity_identity = cookies["vanity_id"] || ActiveSupport::SecureRandom.hex(16)
|
38
|
+
cookies["vanity_id"] = { :value=>@vanity_identity, :expires=>1.month.from_now }
|
39
|
+
@vanity_identity
|
40
|
+
else # during functional testing
|
41
|
+
@vanity_identity = "test"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
around_filter :vanity_context_filter
|
46
|
+
before_filter :vanity_reload_filter unless ::Rails.configuration.cache_classes
|
47
|
+
before_filter :vanity_query_parameter_filter
|
48
|
+
end
|
49
|
+
protected :use_vanity
|
50
|
+
end
|
51
|
+
|
52
|
+
|
53
|
+
# Vanity needs these filters. They are includes in ActionController and
|
54
|
+
# automatically added when you use #use_vanity in your controller.
|
55
|
+
module Filters
|
56
|
+
# Around filter that sets Vanity.context to controller.
|
57
|
+
def vanity_context_filter
|
58
|
+
previous, Vanity.context = Vanity.context, self
|
59
|
+
yield
|
60
|
+
ensure
|
61
|
+
Vanity.context = previous
|
62
|
+
end
|
63
|
+
|
64
|
+
# This filter allows user to choose alternative in experiment using query
|
65
|
+
# parameter.
|
66
|
+
#
|
67
|
+
# Each alternative has a unique fingerprint (run vanity list command to
|
68
|
+
# see them all). A request with the _vanity query parameter is
|
69
|
+
# intercepted, the alternative is chosen, and the user redirected to the
|
70
|
+
# same request URL sans _vanity parameter. This only works for GET
|
71
|
+
# requests.
|
72
|
+
#
|
73
|
+
# For example, if the user requests the page
|
74
|
+
# http://example.com/?_vanity=2907dac4de, the first alternative of the
|
75
|
+
# :null_abc experiment is chosen and the user redirected to
|
76
|
+
# http://example.com/.
|
77
|
+
def vanity_query_parameter_filter
|
78
|
+
if request.get? && params[:_vanity]
|
79
|
+
hashes = Array(params.delete(:_vanity))
|
80
|
+
Vanity.playground.experiments.each do |id, experiment|
|
81
|
+
if experiment.respond_to?(:alternatives)
|
82
|
+
experiment.alternatives.each do |alt|
|
83
|
+
if hash = hashes.delete(experiment.fingerprint(alt))
|
84
|
+
experiment.chooses alt.value
|
85
|
+
break
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
break if hashes.empty?
|
90
|
+
end
|
91
|
+
redirect_to url_for(params)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# Before filter to reload Vanity experiments/metrics. Enabled when
|
96
|
+
# cache_classes is false (typically, testing environment).
|
97
|
+
def vanity_reload_filter
|
98
|
+
Vanity.playground.reload!
|
99
|
+
end
|
100
|
+
|
101
|
+
protected :vanity_context_filter, :vanity_query_parameter_filter, :vanity_reload_filter
|
102
|
+
end
|
103
|
+
|
104
|
+
|
105
|
+
# Introduces ab_test helper (controllers and views). Similar to the generic
|
106
|
+
# ab_test method, with the ability to capture content (applicable to views,
|
107
|
+
# see examples).
|
108
|
+
module Helpers
|
109
|
+
# This method returns one of the alternative values in the named A/B test.
|
110
|
+
#
|
111
|
+
# @example A/B two alternatives for a page
|
112
|
+
# def index
|
113
|
+
# if ab_test(:new_page) # true/false test
|
114
|
+
# render action: "new_page"
|
115
|
+
# else
|
116
|
+
# render action: "index"
|
117
|
+
# end
|
118
|
+
# end
|
119
|
+
# @example Similar, alternative value is page name
|
120
|
+
# def index
|
121
|
+
# render action: ab_test(:new_page)
|
122
|
+
# end
|
123
|
+
# @example A/B test inside ERB template (condition)
|
124
|
+
# <%= if ab_test(:banner) %>100% less complexity!<% end %>
|
125
|
+
# @example A/B test inside ERB template (value)
|
126
|
+
# <%= ab_test(:greeting) %> <%= current_user.name %>
|
127
|
+
# @example A/B test inside ERB template (capture)
|
128
|
+
# <% ab_test :features do |count| %>
|
129
|
+
# <%= count %> features to choose from!
|
130
|
+
# <% end %>
|
131
|
+
def ab_test(name, &block)
|
132
|
+
value = Vanity.playground.experiment(name).choose
|
133
|
+
if block
|
134
|
+
content = capture(value, &block)
|
135
|
+
block_called_from_erb?(block) ? concat(content) : content
|
136
|
+
else
|
137
|
+
value
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
|
143
|
+
# Step 1: Add a new resource in config/routes.rb:
|
144
|
+
# map.vanity "/vanity/:action/:id", :controller=>:vanity
|
145
|
+
#
|
146
|
+
# Step 2: Create a new experiments controller:
|
147
|
+
# class VanityController < ApplicationController
|
148
|
+
# include Vanity::Rails::Dashboard
|
149
|
+
# end
|
150
|
+
#
|
151
|
+
# Step 3: Open your browser to http://localhost:3000/vanity
|
152
|
+
module Dashboard
|
153
|
+
def index
|
154
|
+
render :template=>Vanity.template("_report"), :content_type=>Mime::HTML, :layout=>true
|
155
|
+
end
|
156
|
+
|
157
|
+
def chooses
|
158
|
+
exp = Vanity.playground.experiment(params[:e])
|
159
|
+
exp.chooses(exp.alternatives[params[:a].to_i].value)
|
160
|
+
render :partial=>Vanity.template("experiment"), :locals=>{ :experiment=>exp }
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
|
167
|
+
# Enhance ActionController with use_vanity, filters and helper methods.
|
168
|
+
if defined?(ActionController)
|
169
|
+
# Include in controller, add view helper methods.
|
170
|
+
ActionController::Base.class_eval do
|
171
|
+
extend Vanity::Rails::UseVanity
|
172
|
+
include Vanity::Rails::Filters
|
173
|
+
helper Vanity::Rails::Helpers
|
174
|
+
end
|
175
|
+
|
176
|
+
module ActionController
|
177
|
+
class TestCase
|
178
|
+
alias :setup_controller_request_and_response_without_vanity :setup_controller_request_and_response
|
179
|
+
# Sets Vanity.context to the current controller, so you can do things like:
|
180
|
+
# experiment(:simple).chooses(:green)
|
181
|
+
def setup_controller_request_and_response
|
182
|
+
setup_controller_request_and_response_without_vanity
|
183
|
+
Vanity.context = @controller
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
|
190
|
+
# Automatically configure Vanity. Uses the
|
191
|
+
if defined?(Rails)
|
192
|
+
Rails.configuration.after_initialize do
|
193
|
+
# Use Rails logger by default.
|
194
|
+
Vanity.playground.logger ||= Rails.logger
|
195
|
+
Vanity.playground.load_path = Rails.root + Vanity.playground.load_path
|
196
|
+
config_file = Rails.root + "config/redis.yml"
|
197
|
+
if !Vanity.playground.connected? && config_file.exist?
|
198
|
+
config = YAML.load_file(config_file)[Rails.env.to_s]
|
199
|
+
Vanity.playground.redis = config if config
|
200
|
+
end
|
201
|
+
|
202
|
+
# Do this at the very end of initialization, allowing test environment to do
|
203
|
+
# Vanity.playground.mock! before any database access takes place.
|
204
|
+
Rails.configuration.after_initialize do
|
205
|
+
Vanity.playground.load!
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
|
211
|
+
# Reconnect whenever we fork under Passenger.
|
212
|
+
if defined?(PhusionPassenger)
|
213
|
+
PhusionPassenger.on_event(:starting_worker_process) do |forked|
|
214
|
+
if forked
|
215
|
+
begin
|
216
|
+
Vanity.playground.reconnect!
|
217
|
+
rescue Exception=>ex
|
218
|
+
Rails.logger.error "Error reconnecting Redis: #{ex.to_s}"
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
@@ -0,0 +1,77 @@
|
|
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
|
+
fail "Unrecognized options: #{options.keys * ", "}" unless options.empty?
|
46
|
+
@ar_scoped.after_create self
|
47
|
+
extend ActiveRecord
|
48
|
+
end
|
49
|
+
|
50
|
+
# Calling model method on a metric extends it with these modules, redefining
|
51
|
+
# the values and track! methods.
|
52
|
+
#
|
53
|
+
# @since 1.3.0
|
54
|
+
module ActiveRecord
|
55
|
+
|
56
|
+
# This values method queries the database.
|
57
|
+
def values(sdate, edate)
|
58
|
+
query = { :conditions=>{ @ar_timestamp=>(sdate.to_time...(edate + 1).to_time) },
|
59
|
+
:group=>"date(#{@ar_scoped.connection.quote_column_name @ar_timestamp})" }
|
60
|
+
grouped = @ar_column ? @ar_scoped.calculate(@ar_aggregate, @ar_column, query) : @ar_scoped.count(query)
|
61
|
+
(sdate..edate).inject([]) { |ordered, date| ordered << (grouped[date.to_s] || 0) }
|
62
|
+
end
|
63
|
+
|
64
|
+
# This track! method stores nothing, but calls the hooks.
|
65
|
+
def track!(*args)
|
66
|
+
count = args.first || 1
|
67
|
+
call_hooks Time.now, count if count > 0
|
68
|
+
end
|
69
|
+
|
70
|
+
# AR model after_create callback notifies all the hooks.
|
71
|
+
def after_create(record)
|
72
|
+
count = @ar_column ? (record.send(@ar_column) || 0) : 1
|
73
|
+
call_hooks record.send(@ar_timestamp), count if count > 0 && @ar_scoped.exists?(record)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -25,15 +25,14 @@ module Vanity
|
|
25
25
|
|
26
26
|
# Defines a new metric, using the class Vanity::Metric.
|
27
27
|
def metric(name, &block)
|
28
|
-
|
29
|
-
|
30
|
-
metric = Metric.new(playground, name.to_s, id)
|
28
|
+
fail "Metric #{@metric_id} already defined in playground" if playground.metrics[@metric_id]
|
29
|
+
metric = Metric.new(playground, name.to_s, @metric_id)
|
31
30
|
metric.instance_eval &block
|
32
|
-
playground.metrics[
|
31
|
+
playground.metrics[@metric_id] = metric
|
33
32
|
end
|
34
33
|
|
35
|
-
def
|
36
|
-
@playground = playground
|
34
|
+
def new_binding(playground, id)
|
35
|
+
@playground, @metric_id = playground, id
|
37
36
|
binding
|
38
37
|
end
|
39
38
|
|
@@ -101,7 +100,7 @@ module Vanity
|
|
101
100
|
context = Object.new
|
102
101
|
context.instance_eval do
|
103
102
|
extend Definition
|
104
|
-
metric = eval(source, context.
|
103
|
+
metric = eval(source, context.new_binding(playground, id), file)
|
105
104
|
fail NameError.new("Expected #{file} to define metric #{id}", id) unless playground.metrics[id]
|
106
105
|
metric
|
107
106
|
end
|
@@ -197,70 +196,6 @@ module Vanity
|
|
197
196
|
end
|
198
197
|
|
199
198
|
|
200
|
-
# -- ActiveRecord support --
|
201
|
-
|
202
|
-
AGGREGATES = [:average, :minimum, :maximum, :sum]
|
203
|
-
|
204
|
-
# Use an ActiveRecord model to get metric data from database table. Also
|
205
|
-
# forwards @after_create@ callbacks to hooks (updating experiments).
|
206
|
-
#
|
207
|
-
# Supported options:
|
208
|
-
# :conditions -- Only select records that match this condition
|
209
|
-
# :average -- Metric value is average of this column
|
210
|
-
# :minimum -- Metric value is minimum of this column
|
211
|
-
# :maximum -- Metric value is maximum of this column
|
212
|
-
# :sum -- Metric value is sum of this column
|
213
|
-
# :timestamp -- Use this column to filter/group records (defaults to
|
214
|
-
# +created_at+)
|
215
|
-
#
|
216
|
-
# @example Track sign ups using User model
|
217
|
-
# metric "Signups" do
|
218
|
-
# model Account
|
219
|
-
# end
|
220
|
-
# @example Track satisfaction using Survey model
|
221
|
-
# metric "Satisfaction" do
|
222
|
-
# model Survey, :average=>:rating
|
223
|
-
# end
|
224
|
-
# @example Track only high ratings
|
225
|
-
# metric "High ratings" do
|
226
|
-
# model Rating, :conditions=>["stars >= 4"]
|
227
|
-
# end
|
228
|
-
# @example Track only high ratings (using scope)
|
229
|
-
# metric "High ratings" do
|
230
|
-
# model Rating.high
|
231
|
-
# end
|
232
|
-
#
|
233
|
-
# @since 1.2.0
|
234
|
-
def model(class_or_scope, options = nil)
|
235
|
-
options = (options || {}).clone
|
236
|
-
conditions = options.delete(:conditions)
|
237
|
-
scoped = conditions ? class_or_scope.scoped(:conditions=>conditions) : class_or_scope
|
238
|
-
aggregate = AGGREGATES.find { |key| options.has_key?(key) }
|
239
|
-
column = options.delete(aggregate)
|
240
|
-
fail "Cannot use multiple aggregates in a single metric" if AGGREGATES.find { |key| options.has_key?(key) }
|
241
|
-
timestamp = options.delete(:timestamp) || :created_at
|
242
|
-
fail "Unrecognized options: #{options.keys * ", "}" unless options.empty?
|
243
|
-
|
244
|
-
# Hook into model's after_create
|
245
|
-
scoped.after_create do |record|
|
246
|
-
count = column ? (record.send(column) || 0) : 1
|
247
|
-
call_hooks record.send(timestamp), count if count > 0 && scoped.exists?(record)
|
248
|
-
end
|
249
|
-
# Redefine values method to perform query
|
250
|
-
eigenclass = class << self ; self ; end
|
251
|
-
eigenclass.send :define_method, :values do |sdate, edate|
|
252
|
-
query = { :conditions=>{ timestamp=>(sdate.to_time...(edate + 1).to_time) }, :group=>"date(#{scoped.connection.quote_column_name timestamp})" }
|
253
|
-
grouped = column ? scoped.calculate(aggregate, column, query) : scoped.count(query)
|
254
|
-
(sdate..edate).inject([]) { |ordered, date| ordered << (grouped[date.to_s] || 0) }
|
255
|
-
end
|
256
|
-
# Redefine track! method to call on hooks
|
257
|
-
eigenclass.send :define_method, :track! do |*args|
|
258
|
-
count = args.first || 1
|
259
|
-
call_hooks Time.now, count if count > 0
|
260
|
-
end
|
261
|
-
end
|
262
|
-
|
263
|
-
|
264
199
|
# -- Storage --
|
265
200
|
|
266
201
|
def destroy!
|
@@ -0,0 +1,76 @@
|
|
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
|
+
class Resource
|
55
|
+
# GA profile used for this report. Populated after calling results.
|
56
|
+
attr_reader :profile
|
57
|
+
|
58
|
+
def initialize(web_property_id, metric)
|
59
|
+
self.class.send :include, Garb::Resource
|
60
|
+
@web_property_id = web_property_id
|
61
|
+
metrics metric
|
62
|
+
dimensions :date
|
63
|
+
sort :date
|
64
|
+
end
|
65
|
+
|
66
|
+
def results(start_date, end_date)
|
67
|
+
@profile = Garb::Profile.all.find { |p| p.web_property_id == @web_property_id }
|
68
|
+
@start_date = start_date
|
69
|
+
@end_date = end_date
|
70
|
+
Garb::ReportResponse.new(send_request_for_body).results
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|