garb-no-activesupport 0.7.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. data/README.md +250 -0
  2. data/Rakefile +55 -0
  3. data/lib/garb/account.rb +29 -0
  4. data/lib/garb/authentication_request.rb +53 -0
  5. data/lib/garb/data_request.rb +42 -0
  6. data/lib/garb/filter_parameters.rb +41 -0
  7. data/lib/garb/profile.rb +57 -0
  8. data/lib/garb/profile_reports.rb +15 -0
  9. data/lib/garb/report.rb +26 -0
  10. data/lib/garb/report_parameter.rb +25 -0
  11. data/lib/garb/report_response.rb +62 -0
  12. data/lib/garb/reports/bounces.rb +5 -0
  13. data/lib/garb/reports/exits.rb +5 -0
  14. data/lib/garb/reports/pageviews.rb +5 -0
  15. data/lib/garb/reports/unique_pageviews.rb +5 -0
  16. data/lib/garb/reports/visits.rb +5 -0
  17. data/lib/garb/reports.rb +5 -0
  18. data/lib/garb/resource.rb +92 -0
  19. data/lib/garb/session.rb +25 -0
  20. data/lib/garb/version.rb +13 -0
  21. data/lib/garb.rb +38 -0
  22. data/lib/string_ext.rb +20 -0
  23. data/lib/support.rb +39 -0
  24. data/test/fixtures/cacert.pem +67 -0
  25. data/test/fixtures/profile_feed.xml +40 -0
  26. data/test/fixtures/report_feed.xml +46 -0
  27. data/test/test_helper.rb +18 -0
  28. data/test/unit/garb/account_test.rb +53 -0
  29. data/test/unit/garb/authentication_request_test.rb +121 -0
  30. data/test/unit/garb/data_request_test.rb +106 -0
  31. data/test/unit/garb/filter_parameters_test.rb +59 -0
  32. data/test/unit/garb/oauth_session_test.rb +11 -0
  33. data/test/unit/garb/profile_reports_test.rb +29 -0
  34. data/test/unit/garb/profile_test.rb +87 -0
  35. data/test/unit/garb/report_parameter_test.rb +43 -0
  36. data/test/unit/garb/report_response_test.rb +29 -0
  37. data/test/unit/garb/report_test.rb +91 -0
  38. data/test/unit/garb/resource_test.rb +38 -0
  39. data/test/unit/garb/session_test.rb +84 -0
  40. data/test/unit/garb_test.rb +14 -0
  41. data/test/unit/symbol_operator_test.rb +37 -0
  42. metadata +133 -0
data/README.md ADDED
@@ -0,0 +1,250 @@
1
+ Garb
2
+ ====
3
+
4
+ http://github.com/vigetlabs/garb
5
+
6
+ Important Changes
7
+ =================
8
+
9
+ Please read CHANGELOG
10
+
11
+ Description
12
+ -----------
13
+
14
+ Provides a Ruby API to the Google Analytics API.
15
+
16
+ http://code.google.com/apis/analytics/docs/gdata/gdataDeveloperGuide.html
17
+
18
+ Basic Usage
19
+ ===========
20
+
21
+ Single User Login
22
+ -----------------
23
+
24
+ > Garb::Session.login(username, password)
25
+
26
+ OAuth Access Token
27
+ ------------------
28
+
29
+ > Garb::Session.access_token = access_token # assign from oauth gem
30
+
31
+ Accounts
32
+ --------
33
+
34
+ > Garb::Account.all
35
+
36
+ Profiles
37
+ --------
38
+
39
+ > Garb::Account.first.profiles
40
+
41
+ > Garb::Profile.first('UA-XXXX-XX')
42
+
43
+ > Garb::Profile.all
44
+ > profile = Garb::Profile.all.first
45
+
46
+ Define a Report Class
47
+ ---------------------
48
+
49
+ class Exits
50
+ extend Garb::Resource
51
+
52
+ metrics :exits, :pageviews, :exit_rate
53
+ dimensions :page_path
54
+ sort :exits
55
+
56
+ filters do
57
+ eql(:page_path, 'season')
58
+ end
59
+
60
+ # alternative:
61
+ # filters :page_path.eql => 10
62
+ end
63
+
64
+ Get the Results
65
+ ---------------
66
+
67
+ > Exits.results(profile)
68
+
69
+ OR shorthand
70
+
71
+ > profile.exits
72
+
73
+ Other Parameters
74
+ ----------------
75
+
76
+ * start_date: The date of the period you would like this report to start
77
+ * end_date: The date to end, inclusive
78
+ * limit: The maximum number of results to be returned
79
+ * offset: The starting index
80
+
81
+ Metrics & Dimensions
82
+ --------------------
83
+
84
+ Metrics and Dimensions are very complex because of the ways in which the can and cannot be combined.
85
+
86
+ I suggest reading the google documentation to familiarize yourself with this.
87
+
88
+ http://code.google.com/apis/analytics/docs/gdata/gdataReferenceDimensionsMetrics.html#bounceRate
89
+
90
+ When you've returned, you can pass the appropriate combinations (up to 50 metrics and 2 dimenstions)
91
+ to garb, as an array, of symbols. Or you can simply push a symbol into the array.
92
+
93
+ Sorting
94
+ -------
95
+
96
+ Sorting can be done on any metric or dimension defined in the request, with .desc reversing the sort.
97
+
98
+ Building a Report
99
+ -----------------
100
+
101
+ Given the class, session, and profile from above we can do:
102
+
103
+ Exits.results(profile, :limit => 10, :offset => 19)
104
+
105
+ Or, with sorting and filters:
106
+
107
+ Exits.results(profile, :limit => 10, :offset => 19) do
108
+ sort :exits
109
+
110
+ filters do
111
+ contains(:page_path, 'season')
112
+ gt(:exits, 100)
113
+ end
114
+
115
+ # or with a hash
116
+ # filters :page_path.contains => 'season', :exits.gt => 100
117
+ end
118
+
119
+ reports will be an array of OpenStructs with methods for the metrics and dimensions returned.
120
+
121
+ Build a One-Off Report
122
+ ----------------------
123
+
124
+ report = Garb::Report.new(profile)
125
+ report.metrics :pageviews, :exits
126
+ report.dimensions :page_path
127
+ report.sort :exits
128
+
129
+ report.filters do
130
+ contains(:page_path, 'season')
131
+ gte(:exits, 10)
132
+ and
133
+
134
+ # or with a hash
135
+ # report.filters :page_path.contains => 'season', :exits.gt => 100
136
+
137
+ report.results
138
+
139
+ Filtering
140
+ ---------
141
+
142
+ Google Analytics supports a significant number of filtering options.
143
+
144
+ http://code.google.com/apis/analytics/docs/gdata/gdataReference.html#filtering
145
+
146
+ We handle filtering as an array of hashes that you can push into,
147
+ which will be joined together (AND'd)
148
+
149
+ Here is what we can do currently:
150
+ (the operator is a method on a symbol metric or dimension)
151
+
152
+ Operators on metrics:
153
+
154
+ eql => '==',
155
+ not_eql => '!=',
156
+ gt => '>',
157
+ gte => '>=',
158
+ lt => '<',
159
+ lte => '<='
160
+
161
+ Operators on dimensions:
162
+
163
+ matches => '==',
164
+ does_not_match => '!=',
165
+ contains => '=~',
166
+ does_not_contain => '!~',
167
+ substring => '=@',
168
+ not_substring => '!@'
169
+
170
+ Given the previous example one-off report, we can add a line for filter:
171
+
172
+ report.filters do
173
+ eql(:page_path, '/extend/effectively-using-git-with-subversion/')
174
+ end
175
+
176
+ Or, if you're comfortable using symbol operators:
177
+
178
+ report.filters :page_path.eql => '/extend/effectively-using-git-with-subversion/'
179
+
180
+ SSL
181
+ ---
182
+
183
+ Version 0.2.3 includes support for real ssl encryption for SINGLE USER authentication. First do:
184
+
185
+ Garb::Session.login(username, password, :secure => true)
186
+
187
+ Next, be sure to download http://curl.haxx.se/ca/cacert.pem into your application somewhere.
188
+ Then, define a constant CA_CERT_FILE and point to that file.
189
+
190
+ For whatever reason, simply creating a new certificate store and setting the defaults would
191
+ not validate the google ssl certificate as authentic.
192
+
193
+ TODOS
194
+ -----
195
+
196
+ * Read opensearch header in results
197
+ * Investigate new features from GA to see if they're in the API, implement if so
198
+ * clarify AND/OR filtering behavior in code and documentation
199
+
200
+ Requirements
201
+ ------------
202
+
203
+ * happymapper >= 0.3.0
204
+ * active_support >= 2.2.0
205
+
206
+ Requirements for Testing
207
+ ------------------------
208
+
209
+ * jferris-mocha
210
+ * tpitale-shoulda (works with minitest)
211
+
212
+ Install
213
+ -------
214
+
215
+ sudo gem install garb
216
+
217
+ Contributors
218
+ ------------
219
+
220
+ Many Thanks, for all their help, goes to:
221
+
222
+ * Patrick Reagan
223
+ * Justin Marney
224
+ * Nick Plante
225
+
226
+ License
227
+ -------
228
+
229
+ (The MIT License)
230
+
231
+ Copyright (c) 2010 Viget Labs
232
+
233
+ Permission is hereby granted, free of charge, to any person obtaining
234
+ a copy of this software and associated documentation files (the
235
+ 'Software'), to deal in the Software without restriction, including
236
+ without limitation the rights to use, copy, modify, merge, publish,
237
+ distribute, sublicense, and/or sell copies of the Software, and to
238
+ permit persons to whom the Software is furnished to do so, subject to
239
+ the following conditions:
240
+
241
+ The above copyright notice and this permission notice shall be
242
+ included in all copies or substantial portions of the Software.
243
+
244
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
245
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
246
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
247
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
248
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
249
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
250
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,55 @@
1
+ require 'rubygems'
2
+ require 'rake/gempackagetask'
3
+ require 'rake/testtask'
4
+
5
+ require 'lib/garb/version'
6
+
7
+ task :default => :test
8
+
9
+ spec = Gem::Specification.new do |s|
10
+ s.name = 'garb-no-activesupport'
11
+ s.version = Garb::Version.to_s
12
+ s.has_rdoc = false
13
+ s.rubyforge_project = 'viget'
14
+ s.summary = "Google Analytics API Ruby Wrapper"
15
+ s.authors = ['Tony Pitale', 'jonah honeyman']
16
+ s.email = 'gewglestolemypass@gmail.com'
17
+ s.homepage = 'http://github.com/jonuts/garb'
18
+ s.files = %w(README.md Rakefile) + Dir.glob("lib/**/*")
19
+ s.test_files = Dir.glob("test/**/*")
20
+
21
+ s.add_dependency("happymapper", [">= 0.3.0"])
22
+ end
23
+
24
+ Rake::GemPackageTask.new(spec) do |pkg|
25
+ pkg.gem_spec = spec
26
+ end
27
+
28
+ Rake::TestTask.new do |t|
29
+ t.libs << 'test'
30
+ t.test_files = FileList["test/**/*_test.rb"]
31
+ t.verbose = true
32
+ end
33
+
34
+ desc 'Generate the gemspec to serve this Gem from Github'
35
+ task :github do
36
+ file = File.dirname(__FILE__) + "/#{spec.name}.gemspec"
37
+ File.open(file, 'w') {|f| f << spec.to_ruby }
38
+ puts "Created gemspec: #{file}"
39
+ end
40
+
41
+ begin
42
+ require 'rcov/rcovtask'
43
+
44
+ desc "Generate RCov coverage report"
45
+ Rcov::RcovTask.new(:rcov) do |t|
46
+ t.test_files = FileList['test/**/*_test.rb']
47
+ t.rcov_opts << "-x lib/garb.rb -x lib/garb/version.rb"
48
+ end
49
+ rescue LoadError
50
+ nil
51
+ end
52
+
53
+ task :default => 'test'
54
+
55
+ # EOF
@@ -0,0 +1,29 @@
1
+ module Garb
2
+ class Account
3
+ attr_reader :id, :name, :profiles
4
+
5
+ def initialize(profiles)
6
+ @id = profiles.first.account_id
7
+ @name = profiles.first.account_name
8
+ @profiles = profiles
9
+ end
10
+
11
+ def self.all(session = Session)
12
+ # Profile.all.group_to_array{|p| p.account_id}.map{|profiles| new(profiles)}
13
+
14
+ profile_groups = Profile.all(session).inject({}) do |hash, profile|
15
+ key = profile.account_id
16
+
17
+ if hash.has_key?(key)
18
+ hash[key] << profile
19
+ else
20
+ hash[key] = [profile]
21
+ end
22
+
23
+ hash
24
+ end
25
+
26
+ profile_groups.map {|k,v| v}.map {|profiles| new(profiles)}
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,53 @@
1
+ module Garb
2
+ class AuthenticationRequest
3
+ class AuthError < StandardError;end
4
+
5
+ URL = 'https://www.google.com/accounts/ClientLogin'
6
+
7
+ def initialize(email, password, opts={})
8
+ @email = email
9
+ @password = password
10
+ @account_type = opts.fetch(:account_type, 'HOSTED_OR_GOOGLE')
11
+ end
12
+
13
+ def parameters
14
+ {
15
+ 'Email' => @email,
16
+ 'Passwd' => @password,
17
+ 'accountType' => @account_type,
18
+ 'service' => 'analytics',
19
+ 'source' => 'vigetLabs-garb-001'
20
+ }
21
+ end
22
+
23
+ def uri
24
+ URI.parse(URL)
25
+ end
26
+
27
+ def send_request(ssl_mode)
28
+ http = Net::HTTP.new(uri.host, uri.port)
29
+ http.use_ssl = true
30
+ http.verify_mode = ssl_mode
31
+
32
+ if ssl_mode == OpenSSL::SSL::VERIFY_PEER
33
+ http.ca_file = CA_CERT_FILE
34
+ end
35
+
36
+ http.request(build_request) do |response|
37
+ raise AuthError unless response.is_a?(Net::HTTPOK)
38
+ end
39
+ end
40
+
41
+ def build_request
42
+ post = Net::HTTP::Post.new(uri.path)
43
+ post.set_form_data(parameters)
44
+ post
45
+ end
46
+
47
+ def auth_token(opts={})
48
+ ssl_mode = opts[:secure] ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
49
+ send_request(ssl_mode).body.match(/^Auth=(.*)$/)[1]
50
+ end
51
+
52
+ end
53
+ end
@@ -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
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
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' => "GoogleLogin auth=#{@session.auth_token}")
36
+ end
37
+
38
+ def oauth_user_request
39
+ @session.access_token.get("#{uri}#{query_string}")
40
+ end
41
+ end
42
+ 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(';') # Hash AND (no duplicate keys)
36
+ end.join(',') # Array OR
37
+
38
+ value.empty? ? {} : {'filters' => value}
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,57 @@
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
7
+
8
+ class Property
9
+ include HappyMapper
10
+
11
+ tag 'property'
12
+ namespace 'http://schemas.google.com/analytics/2009'
13
+
14
+ attribute :name, String
15
+ attribute :value, String
16
+
17
+ def instance_name
18
+ Garb.from_google_analytics(name)
19
+ end
20
+ end
21
+
22
+ class Entry
23
+ include HappyMapper
24
+
25
+ tag 'entry'
26
+
27
+ element :title, String
28
+ element :tableId, String, :namespace => 'http://schemas.google.com/analytics/2009'
29
+
30
+ has_many :properties, Property
31
+ end
32
+
33
+ def initialize(entry, session)
34
+ @session = session
35
+ @title = entry.title
36
+ @table_id = entry.tableId
37
+
38
+ entry.properties.each do |p|
39
+ instance_variable_set :"@#{p.instance_name}", p.value
40
+ end
41
+ end
42
+
43
+ def id
44
+ Garb.from_google_analytics(@table_id)
45
+ end
46
+
47
+ def self.all(session = Session)
48
+ url = "https://www.google.com/analytics/feeds/accounts/default"
49
+ response = DataRequest.new(session, url).send_request
50
+ Entry.parse(response.body).map {|entry| new(entry, session)}
51
+ end
52
+
53
+ def self.first(id, session = Session)
54
+ all(session).detect {|profile| profile.id == id || profile.web_property_id == id }
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,15 @@
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.to_s.demodulize.underscore
7
+
8
+ class_eval <<-CODE
9
+ def #{method_name}(opts = {}, &block)
10
+ #{klass}.results(self, opts, &block)
11
+ end
12
+ CODE
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,26 @@
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
+ @profile = profile
10
+
11
+ @start_date = opts.fetch(:start_date, Time.now - MONTH)
12
+ @end_date = opts.fetch(:end_date, Time.now)
13
+ @limit = opts.fetch(:limit, nil)
14
+ @offset = opts.fetch(:offset, nil)
15
+
16
+ metrics opts.fetch(:metrics, [])
17
+ dimensions opts.fetch(:dimensions, [])
18
+ sort opts.fetch(:sort, [])
19
+ end
20
+
21
+ def results
22
+ ReportResponse.new(send_request_for_body).results
23
+ end
24
+
25
+ end
26
+ end
@@ -0,0 +1,25 @@
1
+ module Garb
2
+ class ReportParameter
3
+
4
+ attr_reader :elements
5
+
6
+ def initialize(name)
7
+ @name = name
8
+ @elements = []
9
+ end
10
+
11
+ def name
12
+ @name.to_s
13
+ end
14
+
15
+ def <<(element)
16
+ (@elements += [element].flatten).compact!
17
+ self
18
+ end
19
+
20
+ def to_params
21
+ value = self.elements.map{|param| Garb.to_google_analytics(param)}.join(',')
22
+ value.empty? ? {} : {self.name => value}
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,62 @@
1
+ module Garb
2
+ class ReportResponse
3
+ # include Enumerable
4
+
5
+ def initialize(response_body)
6
+ @xml = response_body
7
+ end
8
+
9
+ def parse
10
+ entries = Entry.parse(@xml)
11
+
12
+ @results = entries.collect do |entry|
13
+ hash = {}
14
+
15
+ entry.metrics.each do |m|
16
+ name = m.name.sub(/^ga\:/,'').underscore
17
+ hash.merge!({name => m.value})
18
+ end
19
+
20
+ entry.dimensions.each do |d|
21
+ name = d.name.sub(/^ga\:/,'').underscore
22
+ hash.merge!({name => d.value})
23
+ end
24
+
25
+ OpenStruct.new(hash)
26
+ end
27
+ end
28
+
29
+ def results
30
+ @results || parse
31
+ end
32
+
33
+ class Metric
34
+ include HappyMapper
35
+
36
+ tag 'metric'
37
+ namespace 'http://schemas.google.com/analytics/2009'
38
+
39
+ attribute :name, String
40
+ attribute :value, String
41
+ end
42
+
43
+ class Dimension
44
+ include HappyMapper
45
+
46
+ tag 'dimension'
47
+ namespace 'http://schemas.google.com/analytics/2009'
48
+
49
+ attribute :name, String
50
+ attribute :value, String
51
+ end
52
+
53
+ class Entry
54
+ include HappyMapper
55
+
56
+ tag 'entry'
57
+
58
+ has_many :metrics, Metric
59
+ has_many :dimensions, Dimension
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,5 @@
1
+ class Bounces
2
+ extend Garb::Resource
3
+
4
+ metrics :bounces
5
+ end
@@ -0,0 +1,5 @@
1
+ class Exits
2
+ extend Garb::Resource
3
+
4
+ metrics :exits
5
+ end
@@ -0,0 +1,5 @@
1
+ class Pageviews
2
+ extend Garb::Resource
3
+
4
+ metrics :pageviews
5
+ end
@@ -0,0 +1,5 @@
1
+ class UniquePageviews
2
+ extend Garb::Resource
3
+
4
+ metrics :unique_pageviews
5
+ end
@@ -0,0 +1,5 @@
1
+ class Visits
2
+ extend Garb::Resource
3
+
4
+ metrics :visits
5
+ end
@@ -0,0 +1,5 @@
1
+ require 'garb/reports/exits'
2
+ require 'garb/reports/visits'
3
+ require 'garb/reports/bounces'
4
+ require 'garb/reports/pageviews'
5
+ require 'garb/reports/unique_pageviews'