titanous-garb 0.8.5
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/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
|