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.
Files changed (59) hide show
  1. data/README.md +262 -0
  2. data/Rakefile +56 -0
  3. data/lib/garb.rb +69 -0
  4. data/lib/garb/account.rb +21 -0
  5. data/lib/garb/account_feed_request.rb +25 -0
  6. data/lib/garb/authentication_request.rb +53 -0
  7. data/lib/garb/data_request.rb +42 -0
  8. data/lib/garb/destination.rb +20 -0
  9. data/lib/garb/filter_parameters.rb +41 -0
  10. data/lib/garb/goal.rb +20 -0
  11. data/lib/garb/management/account.rb +32 -0
  12. data/lib/garb/management/feed.rb +26 -0
  13. data/lib/garb/management/goal.rb +20 -0
  14. data/lib/garb/management/profile.rb +39 -0
  15. data/lib/garb/management/web_property.rb +30 -0
  16. data/lib/garb/model.rb +89 -0
  17. data/lib/garb/profile.rb +33 -0
  18. data/lib/garb/profile_reports.rb +16 -0
  19. data/lib/garb/report.rb +28 -0
  20. data/lib/garb/report_parameter.rb +25 -0
  21. data/lib/garb/report_response.rb +34 -0
  22. data/lib/garb/reports.rb +5 -0
  23. data/lib/garb/reports/bounces.rb +5 -0
  24. data/lib/garb/reports/exits.rb +5 -0
  25. data/lib/garb/reports/pageviews.rb +5 -0
  26. data/lib/garb/reports/unique_pageviews.rb +5 -0
  27. data/lib/garb/reports/visits.rb +5 -0
  28. data/lib/garb/resource.rb +115 -0
  29. data/lib/garb/session.rb +35 -0
  30. data/lib/garb/step.rb +13 -0
  31. data/lib/garb/version.rb +13 -0
  32. data/lib/support.rb +40 -0
  33. data/test/fixtures/cacert.pem +67 -0
  34. data/test/fixtures/profile_feed.xml +72 -0
  35. data/test/fixtures/report_feed.xml +46 -0
  36. data/test/test_helper.rb +37 -0
  37. data/test/unit/garb/account_feed_request_test.rb +9 -0
  38. data/test/unit/garb/account_test.rb +53 -0
  39. data/test/unit/garb/authentication_request_test.rb +121 -0
  40. data/test/unit/garb/data_request_test.rb +120 -0
  41. data/test/unit/garb/destination_test.rb +28 -0
  42. data/test/unit/garb/filter_parameters_test.rb +59 -0
  43. data/test/unit/garb/goal_test.rb +24 -0
  44. data/test/unit/garb/management/account_test.rb +54 -0
  45. data/test/unit/garb/management/profile_test.rb +59 -0
  46. data/test/unit/garb/management/web_property_test.rb +58 -0
  47. data/test/unit/garb/model_test.rb +134 -0
  48. data/test/unit/garb/oauth_session_test.rb +11 -0
  49. data/test/unit/garb/profile_reports_test.rb +29 -0
  50. data/test/unit/garb/profile_test.rb +77 -0
  51. data/test/unit/garb/report_parameter_test.rb +43 -0
  52. data/test/unit/garb/report_response_test.rb +37 -0
  53. data/test/unit/garb/report_test.rb +99 -0
  54. data/test/unit/garb/resource_test.rb +49 -0
  55. data/test/unit/garb/session_test.rb +91 -0
  56. data/test/unit/garb/step_test.rb +15 -0
  57. data/test/unit/garb_test.rb +26 -0
  58. data/test/unit/symbol_operator_test.rb +37 -0
  59. 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
@@ -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
@@ -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