titanous-garb 0.8.5
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +262 -0
- data/Rakefile +56 -0
- data/lib/garb.rb +69 -0
- data/lib/garb/account.rb +21 -0
- data/lib/garb/account_feed_request.rb +25 -0
- data/lib/garb/authentication_request.rb +53 -0
- data/lib/garb/data_request.rb +42 -0
- data/lib/garb/destination.rb +20 -0
- data/lib/garb/filter_parameters.rb +41 -0
- data/lib/garb/goal.rb +20 -0
- data/lib/garb/management/account.rb +32 -0
- data/lib/garb/management/feed.rb +26 -0
- data/lib/garb/management/goal.rb +20 -0
- data/lib/garb/management/profile.rb +39 -0
- data/lib/garb/management/web_property.rb +30 -0
- data/lib/garb/model.rb +89 -0
- data/lib/garb/profile.rb +33 -0
- data/lib/garb/profile_reports.rb +16 -0
- data/lib/garb/report.rb +28 -0
- data/lib/garb/report_parameter.rb +25 -0
- data/lib/garb/report_response.rb +34 -0
- data/lib/garb/reports.rb +5 -0
- data/lib/garb/reports/bounces.rb +5 -0
- data/lib/garb/reports/exits.rb +5 -0
- data/lib/garb/reports/pageviews.rb +5 -0
- data/lib/garb/reports/unique_pageviews.rb +5 -0
- data/lib/garb/reports/visits.rb +5 -0
- data/lib/garb/resource.rb +115 -0
- data/lib/garb/session.rb +35 -0
- data/lib/garb/step.rb +13 -0
- data/lib/garb/version.rb +13 -0
- data/lib/support.rb +40 -0
- data/test/fixtures/cacert.pem +67 -0
- data/test/fixtures/profile_feed.xml +72 -0
- data/test/fixtures/report_feed.xml +46 -0
- data/test/test_helper.rb +37 -0
- data/test/unit/garb/account_feed_request_test.rb +9 -0
- data/test/unit/garb/account_test.rb +53 -0
- data/test/unit/garb/authentication_request_test.rb +121 -0
- data/test/unit/garb/data_request_test.rb +120 -0
- data/test/unit/garb/destination_test.rb +28 -0
- data/test/unit/garb/filter_parameters_test.rb +59 -0
- data/test/unit/garb/goal_test.rb +24 -0
- data/test/unit/garb/management/account_test.rb +54 -0
- data/test/unit/garb/management/profile_test.rb +59 -0
- data/test/unit/garb/management/web_property_test.rb +58 -0
- data/test/unit/garb/model_test.rb +134 -0
- data/test/unit/garb/oauth_session_test.rb +11 -0
- data/test/unit/garb/profile_reports_test.rb +29 -0
- data/test/unit/garb/profile_test.rb +77 -0
- data/test/unit/garb/report_parameter_test.rb +43 -0
- data/test/unit/garb/report_response_test.rb +37 -0
- data/test/unit/garb/report_test.rb +99 -0
- data/test/unit/garb/resource_test.rb +49 -0
- data/test/unit/garb/session_test.rb +91 -0
- data/test/unit/garb/step_test.rb +15 -0
- data/test/unit/garb_test.rb +26 -0
- data/test/unit/symbol_operator_test.rb +37 -0
- metadata +180 -0
@@ -0,0 +1,42 @@
|
|
1
|
+
module Garb
|
2
|
+
class DataRequest
|
3
|
+
class ClientError < StandardError; end
|
4
|
+
|
5
|
+
def initialize(session, base_url, parameters={})
|
6
|
+
@session = session
|
7
|
+
@base_url = base_url
|
8
|
+
@parameters = parameters
|
9
|
+
end
|
10
|
+
|
11
|
+
def query_string
|
12
|
+
parameter_list = @parameters.map {|k,v| "#{k}=#{v}" }
|
13
|
+
parameter_list.empty? ? '' : "?#{parameter_list.join('&')}"
|
14
|
+
end
|
15
|
+
|
16
|
+
def uri
|
17
|
+
URI.parse(@base_url)
|
18
|
+
end
|
19
|
+
|
20
|
+
def send_request
|
21
|
+
response = if @session.single_user?
|
22
|
+
single_user_request(@session.auth_sub? ? 'AuthSub' : 'GoogleLogin')
|
23
|
+
elsif @session.oauth_user?
|
24
|
+
oauth_user_request
|
25
|
+
end
|
26
|
+
|
27
|
+
raise ClientError, response.body.inspect unless response.kind_of?(Net::HTTPSuccess)
|
28
|
+
response
|
29
|
+
end
|
30
|
+
|
31
|
+
def single_user_request(auth='GoogleLogin')
|
32
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
33
|
+
http.use_ssl = true
|
34
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
35
|
+
http.get("#{uri.path}#{query_string}", { 'Authorization' => "#{auth} #{auth == 'GoogleLogin' ? 'auth' : 'token'}=#{@session.auth_token}", 'GData-Version' => '2' })
|
36
|
+
end
|
37
|
+
|
38
|
+
def oauth_user_request
|
39
|
+
@session.access_token.get("#{uri}#{query_string}", {'GData-Version' => '2'})
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Garb
|
2
|
+
class Destination
|
3
|
+
attr_reader :match_type, :expression, :steps
|
4
|
+
|
5
|
+
def initialize(attributes)
|
6
|
+
return unless attributes.is_a?(Hash)
|
7
|
+
|
8
|
+
@match_type = attributes['matchType']
|
9
|
+
@expression = attributes['expression']
|
10
|
+
@case_sensitive = (attributes['caseSensitive'] == 'true')
|
11
|
+
|
12
|
+
step_attributes = attributes[Garb.to_ga('step')]
|
13
|
+
@steps = Array(step_attributes.is_a?(Hash) ? [step_attributes] : step_attributes).map {|s| Step.new(s)}
|
14
|
+
end
|
15
|
+
|
16
|
+
def case_sensitive?
|
17
|
+
@case_sensitive
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Garb
|
2
|
+
class FilterParameters
|
3
|
+
def self.define_operators(*methods)
|
4
|
+
methods.each do |method|
|
5
|
+
class_eval <<-CODE
|
6
|
+
def #{method}(field, value)
|
7
|
+
@filter_hash.merge!({SymbolOperator.new(field, :#{method}) => value})
|
8
|
+
end
|
9
|
+
CODE
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
define_operators :eql, :not_eql, :gt, :gte, :lt, :lte, :matches,
|
14
|
+
:does_not_match, :contains, :does_not_contain, :substring, :not_substring
|
15
|
+
|
16
|
+
attr_accessor :parameters
|
17
|
+
|
18
|
+
def initialize
|
19
|
+
self.parameters = []
|
20
|
+
end
|
21
|
+
|
22
|
+
def filters(&block)
|
23
|
+
@filter_hash = {}
|
24
|
+
|
25
|
+
instance_eval &block
|
26
|
+
|
27
|
+
self.parameters << @filter_hash
|
28
|
+
end
|
29
|
+
|
30
|
+
def to_params
|
31
|
+
value = self.parameters.map do |param|
|
32
|
+
param.map do |k,v|
|
33
|
+
next unless k.is_a?(SymbolOperator)
|
34
|
+
"#{URI.encode(k.to_google_analytics, /[=<>]/)}#{CGI::escape(v.to_s)}"
|
35
|
+
end.join('%3B') # Hash AND (no duplicate keys), escape char for ';' fixes oauth
|
36
|
+
end.join(',') # Array OR
|
37
|
+
|
38
|
+
value.empty? ? {} : {'filters' => value}
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/lib/garb/goal.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
module Garb
|
2
|
+
class Goal
|
3
|
+
attr_reader :name, :number, :value, :destination
|
4
|
+
|
5
|
+
def initialize(attributes={})
|
6
|
+
return unless attributes.is_a?(Hash)
|
7
|
+
|
8
|
+
@name = attributes['name']
|
9
|
+
@number = attributes['number'].to_i
|
10
|
+
@value = attributes['value'].to_f
|
11
|
+
@active = (attributes['active'] == 'true')
|
12
|
+
|
13
|
+
@destination = Destination.new(attributes[Garb.to_ga('destination')])
|
14
|
+
end
|
15
|
+
|
16
|
+
def active?
|
17
|
+
@active
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Garb
|
2
|
+
module Management
|
3
|
+
class Account
|
4
|
+
attr_reader :session, :path
|
5
|
+
attr_reader :id, :title, :name
|
6
|
+
|
7
|
+
def self.all(session = Session)
|
8
|
+
feed = Feed.new(session, '/accounts') # builds request and parses response
|
9
|
+
|
10
|
+
feed.entries.map {|entry| new(entry, session)}
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(entry, session)
|
14
|
+
@session = session
|
15
|
+
@path = Garb.parse_link(entry, "self").gsub(Feed::BASE_URL, '')
|
16
|
+
@title = entry['title'].gsub('Google Analytics Account ', '')
|
17
|
+
|
18
|
+
properties = Garb.parse_properties(entry)
|
19
|
+
@id = properties["account_id"]
|
20
|
+
@name = properties["account_name"]
|
21
|
+
end
|
22
|
+
|
23
|
+
def web_properties
|
24
|
+
@web_properties ||= WebProperty.for_account(self) # will call path
|
25
|
+
end
|
26
|
+
|
27
|
+
def profiles
|
28
|
+
@profiles ||= Profile.for_account(self)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Garb
|
2
|
+
module Management
|
3
|
+
class Feed
|
4
|
+
BASE_URL = "https://www.google.com/analytics/feeds/datasources/ga"
|
5
|
+
|
6
|
+
attr_reader :request
|
7
|
+
|
8
|
+
def initialize(session, path)
|
9
|
+
@request = DataRequest.new(session, BASE_URL+path)
|
10
|
+
end
|
11
|
+
|
12
|
+
def parsed_response
|
13
|
+
@parsed_response ||= Crack::XML.parse(response.body)
|
14
|
+
end
|
15
|
+
|
16
|
+
def entries
|
17
|
+
# possible to have nil entries, yuck
|
18
|
+
parsed_response ? [parsed_response['feed']['entry']].flatten.reject {|e| e.nil?} : []
|
19
|
+
end
|
20
|
+
|
21
|
+
def response
|
22
|
+
@response ||= request.send_request
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Garb
|
2
|
+
class Goal
|
3
|
+
attr_reader :name, :number, :value, :destination
|
4
|
+
|
5
|
+
def initialize(attributes={})
|
6
|
+
return unless attributes.is_a?(Hash)
|
7
|
+
|
8
|
+
@name = attributes['name']
|
9
|
+
@number = attributes['number'].to_i
|
10
|
+
@value = attributes['value'].to_f
|
11
|
+
@active = (attributes['active'] == 'true')
|
12
|
+
|
13
|
+
@destination = Destination.new(attributes[Garb.to_ga('destination')])
|
14
|
+
end
|
15
|
+
|
16
|
+
def active?
|
17
|
+
@active
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Garb
|
2
|
+
module Management
|
3
|
+
class Profile
|
4
|
+
|
5
|
+
include ProfileReports
|
6
|
+
|
7
|
+
attr_reader :session, :path
|
8
|
+
attr_reader :id, :table_id, :title, :account_id, :web_property_id
|
9
|
+
|
10
|
+
def self.all(session = Session, path = '/accounts/~all/webproperties/~all/profiles')
|
11
|
+
feed = Feed.new(session, path)
|
12
|
+
feed.entries.map {|entry| new(entry, session)}
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.for_account(account)
|
16
|
+
all(account.session, account.path+'/webproperties/~all/profiles')
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.for_web_property(web_property)
|
20
|
+
all(web_property.session, web_property.path+'/profiles')
|
21
|
+
end
|
22
|
+
|
23
|
+
def initialize(entry, session)
|
24
|
+
@session = session
|
25
|
+
@path = Garb.parse_link(entry, "self").gsub(Feed::BASE_URL, '')
|
26
|
+
|
27
|
+
properties = Garb.parse_properties(entry)
|
28
|
+
@id = properties['profile_id']
|
29
|
+
@table_id = properties['table_id']
|
30
|
+
@title = properties['profile_name']
|
31
|
+
@account_id = properties['account_id']
|
32
|
+
@web_property_id = properties['web_property_id']
|
33
|
+
end
|
34
|
+
|
35
|
+
# def goals
|
36
|
+
# end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Garb
|
2
|
+
module Management
|
3
|
+
class WebProperty
|
4
|
+
attr_reader :session, :path
|
5
|
+
attr_reader :id, :account_id
|
6
|
+
|
7
|
+
def self.all(session = Session, path='/accounts/~all/webproperties')
|
8
|
+
feed = Feed.new(session, path)
|
9
|
+
feed.entries.map {|entry| new(entry, session)}
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.for_account(account)
|
13
|
+
all(account.session, account.path+'/webproperties')
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(entry, session)
|
17
|
+
@session = session
|
18
|
+
@path = Garb.parse_link(entry, "self").gsub(Feed::BASE_URL, '')
|
19
|
+
|
20
|
+
properties = Garb.parse_properties(entry)
|
21
|
+
@id = properties["web_property_id"]
|
22
|
+
@account_id = properties["account_id"]
|
23
|
+
end
|
24
|
+
|
25
|
+
def profiles
|
26
|
+
@profiles ||= Profile.for_web_property(self)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/garb/model.rb
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
module Garb
|
2
|
+
module Model
|
3
|
+
MONTH = 2592000
|
4
|
+
URL = "https://www.google.com/analytics/feeds/data"
|
5
|
+
|
6
|
+
def self.extended(base)
|
7
|
+
ProfileReports.add_report_method(base)
|
8
|
+
end
|
9
|
+
|
10
|
+
def metrics(*fields)
|
11
|
+
@metrics ||= ReportParameter.new(:metrics)
|
12
|
+
@metrics << fields
|
13
|
+
end
|
14
|
+
|
15
|
+
def dimensions(*fields)
|
16
|
+
@dimensions ||= ReportParameter.new(:dimensions)
|
17
|
+
@dimensions << fields
|
18
|
+
end
|
19
|
+
|
20
|
+
def set_instance_klass(klass)
|
21
|
+
@instance_klass = klass
|
22
|
+
end
|
23
|
+
|
24
|
+
def instance_klass
|
25
|
+
@instance_klass || OpenStruct
|
26
|
+
end
|
27
|
+
|
28
|
+
def results(profile, options = {})
|
29
|
+
default_params = build_default_params(profile)
|
30
|
+
|
31
|
+
param_set = [
|
32
|
+
default_params,
|
33
|
+
metrics.to_params,
|
34
|
+
dimensions.to_params,
|
35
|
+
parse_filters(options).to_params,
|
36
|
+
parse_segment(options),
|
37
|
+
parse_sort(options).to_params,
|
38
|
+
build_page_params(options)
|
39
|
+
]
|
40
|
+
|
41
|
+
data = send_request_for_data(profile, build_params(param_set))
|
42
|
+
ReportResponse.new(data, instance_klass).results
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
def send_request_for_data(profile, params)
|
47
|
+
request = DataRequest.new(profile.session, URL, params)
|
48
|
+
response = request.send_request
|
49
|
+
response.body
|
50
|
+
end
|
51
|
+
|
52
|
+
def build_params(param_set)
|
53
|
+
param_set.inject({}) {|p,i| p.merge(i)}.reject{|k,v| v.nil?}
|
54
|
+
end
|
55
|
+
|
56
|
+
def parse_filters(options)
|
57
|
+
filters = FilterParameters.new
|
58
|
+
filters.parameters << options[:filters] if options.has_key?(:filters)
|
59
|
+
filters
|
60
|
+
end
|
61
|
+
|
62
|
+
def parse_segment(options)
|
63
|
+
segment_id = "gaid::#{options[:segment_id].to_i}" if options.has_key?(:segment_id)
|
64
|
+
{'segment' => segment_id}
|
65
|
+
end
|
66
|
+
|
67
|
+
def parse_sort(options)
|
68
|
+
sort = ReportParameter.new(:sort)
|
69
|
+
sort << options[:sort] if options.has_key?(:sort)
|
70
|
+
sort
|
71
|
+
end
|
72
|
+
|
73
|
+
def build_default_params(profile)
|
74
|
+
{
|
75
|
+
'ids' => Garb.to_ga(profile.id),
|
76
|
+
'start-date' => format_time(Time.now - Model::MONTH),
|
77
|
+
'end-date' => format_time(Time.now)
|
78
|
+
}
|
79
|
+
end
|
80
|
+
|
81
|
+
def build_page_params(options)
|
82
|
+
{'max-results' => options[:limit], 'start-index' => options[:offset]}
|
83
|
+
end
|
84
|
+
|
85
|
+
def format_time(t)
|
86
|
+
t.strftime('%Y-%m-%d')
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
data/lib/garb/profile.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
module Garb
|
2
|
+
class Profile
|
3
|
+
|
4
|
+
include ProfileReports
|
5
|
+
|
6
|
+
attr_reader :session, :table_id, :title, :account_name, :account_id, :web_property_id, :goals
|
7
|
+
|
8
|
+
def initialize(entry, session)
|
9
|
+
@session = session
|
10
|
+
@title = entry['title']
|
11
|
+
@table_id = entry['dxp:tableId']
|
12
|
+
@goals = (entry[Garb.to_ga('goal')] || []).map {|g| Goal.new(g)}
|
13
|
+
|
14
|
+
Garb.parse_properties(entry).each do |k,v|
|
15
|
+
instance_variable_set :"@#{k}", v
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def id
|
20
|
+
Garb.from_ga(@table_id)
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.all(session = Session)
|
24
|
+
ActiveSupport::Deprecation.warn("Garb::Profile.all is deprecated in favor of Garb::Management::Profile.all")
|
25
|
+
AccountFeedRequest.new(session).entries.map {|entry| new(entry, session)}
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.first(id, session = Session)
|
29
|
+
ActiveSupport::Deprecation.warn("Garb::Profile.first is deprecated in favor of Garb::Management::WebProperty")
|
30
|
+
all(session).detect {|profile| profile.id == id || profile.web_property_id == id }
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Garb
|
2
|
+
module ProfileReports
|
3
|
+
def self.add_report_method(klass)
|
4
|
+
# demodulize leaves potential to redefine
|
5
|
+
# these methods given different namespaces
|
6
|
+
method_name = klass.name.demodulize.underscore
|
7
|
+
return unless method_name.length > 0
|
8
|
+
|
9
|
+
class_eval <<-CODE
|
10
|
+
def #{method_name}(opts = {}, &block)
|
11
|
+
#{klass}.results(self, opts, &block)
|
12
|
+
end
|
13
|
+
CODE
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
data/lib/garb/report.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
module Garb
|
2
|
+
class Report
|
3
|
+
include Resource
|
4
|
+
|
5
|
+
MONTH = 2592000
|
6
|
+
URL = "https://www.google.com/analytics/feeds/data"
|
7
|
+
|
8
|
+
def initialize(profile, opts={})
|
9
|
+
ActiveSupport::Deprecation.warn("The use of Report will be removed in favor of 'extend Garb::Model'")
|
10
|
+
|
11
|
+
@profile = profile
|
12
|
+
|
13
|
+
@start_date = opts.fetch(:start_date, Time.now - MONTH)
|
14
|
+
@end_date = opts.fetch(:end_date, Time.now)
|
15
|
+
@limit = opts.fetch(:limit, nil)
|
16
|
+
@offset = opts.fetch(:offset, nil)
|
17
|
+
|
18
|
+
metrics opts.fetch(:metrics, [])
|
19
|
+
dimensions opts.fetch(:dimensions, [])
|
20
|
+
sort opts.fetch(:sort, [])
|
21
|
+
end
|
22
|
+
|
23
|
+
def results
|
24
|
+
ReportResponse.new(send_request_for_body).results
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|