jeremyf-gattica 0.3.4

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt ADDED
@@ -0,0 +1,35 @@
1
+ == 0.3.4
2
+ * Removed Hash#to_query as this directly conflicts with Rails Hash#to_query method.
3
+ * Removed Hash#stringify_keys as it was not used in the code base.
4
+
5
+ == 0.3.2
6
+ * er1c updated to use standard Ruby CSV library
7
+
8
+ == 0.3.0
9
+ * Support for filters (filters are all AND'ed together, no OR yet)
10
+
11
+ == 0.2.1
12
+ * More robust error checking on HTTP calls
13
+ * Added to_xml to get raw XML output from Google
14
+
15
+ == 0.2.0 / 2009-04-27
16
+ * Changed initialization format: pass a hash of options rather than individual email, password and profile_id
17
+ * Can initialize with a valid token and use that instead of requiring email/password each time
18
+ * Can initialize with your own logger object instead of having to use the default (useful if you're using with Rails, initialize with RAILS_DEFAULT_LOGGER)
19
+ * Show error if token is invalid or expired (Google returns a 401 on any HTTP call)
20
+ * Started tests
21
+
22
+ == 0.1.4 / 2009-04-22
23
+ * Another attempt at getting the gem to build on github
24
+
25
+ == 0.1.3 / 2009-04-22
26
+ * Getting gem to build on github
27
+
28
+ == 0.1.2 / 2009-04-22
29
+ * Updated readme and examples, better documentation throughout
30
+
31
+ == 0.1.1 / 2009-04-22
32
+ * When outputting as CSV, surround each piece of data with double quotes (appears pretty common for various properties (like Browser name) to contain commas
33
+
34
+ == 0.1.0 / 2009-03-26
35
+ * Basic functionality working good. Can't use filters yet.
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ (The MIT License)
2
+
3
+ Copyright (c) 2009 Rob Cameron
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ 'Software'), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,192 @@
1
+ Gattica is a Ruby library for talking to the Google Analytics API.
2
+
3
+ = Installation
4
+ Install the gattica gem using github as the source:
5
+
6
+ gem install cannikin-gattica -s http://gems.github.com
7
+
8
+ When you want to require, you just use 'gattica' as the gem name:
9
+
10
+ require 'rubygems'
11
+ require 'gattica'
12
+
13
+ = Introduction
14
+ It's a good idea to familiarize yourself with the Google API docs: http://code.google.com/apis/analytics/docs/gdata/gdataDeveloperGuide.html
15
+
16
+ In particular there are some very specific combinations of Metrics and Dimensions that
17
+ you are restricted to and those are explained in this document: http://code.google.com/apis/analytics/docs/gdata/gdataReferenceDimensionsMetrics.html
18
+
19
+ See examples/example.rb for some code that should work automatically if you replace the email/password with your own
20
+
21
+ There are generally three steps to getting info from the GA API:
22
+
23
+ 1. Authenticate
24
+ 2. Get a profile id
25
+ 3. Get the data you really want
26
+
27
+ = Usage
28
+ This library does all three. A typical transaction will look like this:
29
+
30
+ gs = Gattica.new({:email => 'johndoe@google.com', :password => 'password', profile_id => 123456})
31
+ results = gs.get({ :start_date => '2008-01-01',
32
+ :end_date => '2008-02-01',
33
+ :dimensions => 'browser',
34
+ :metrics => 'pageviews',
35
+ :sort => '-pageviews'})
36
+
37
+ So we instantiate a copy of Gattica and pass it a Google Account email address and password.
38
+ The third parameter is the profile_id that we want to access data for.
39
+
40
+ Then we call +get+ with the parameters we want to shape our data with. In this case we want
41
+ total page views, broken down by browser, from Jan 1 2008 to Feb 1 2008, sorted by descending
42
+ page views. If you wanted to sort pageviews ascending, just leave off the minus.
43
+
44
+ If you don't know the profile_id you want to get data for, call +accounts+
45
+
46
+ gs = Gattica.new({:email => 'johndoe@google.com', :password => 'password'})
47
+ accounts = gs.accounts
48
+
49
+ This returns all of the accounts and profiles that the user has access to. Note that if you
50
+ use this method to get profiles, you need to manually set the profile before you can call +get+
51
+
52
+ gs.profile_id = 123456
53
+ results = gs.get({ :start_date => '2008-01-01',
54
+ :end_date => '2008-02-01',
55
+ :dimensions => 'browser',
56
+ :metrics => 'pageviews',
57
+ :sort => '-pageviews'})
58
+
59
+ When you put in the names for the dimensions and metrics you want, refer to this doc for the
60
+ available names: http://code.google.com/apis/analytics/docs/gdata/gdataReferenceDimensionsMetrics.html
61
+
62
+ Note that you do *not* use the 'ga:' prefix when you tell Gattica which ones you want. Gattica
63
+ adds that for you automatically.
64
+
65
+ If you want to search on more than one dimension or metric, pass them in as an array (you can
66
+ also pass in single values as arrays too, if you wish):
67
+
68
+ results = gs.get({ :start_date => '2008-01-01',
69
+ :end_date => '2008-02-01',
70
+ :dimensions => ['browser','browserVersion'],
71
+ :metrics => ['pageviews','visits'],
72
+ :sort => ['-pageviews']})
73
+
74
+ == Filters
75
+ Filters can be pretty complex as far as GA is concerned. You can filter on either dimensions or metrics
76
+ or both. And then your filters can be ANDed together or they can ORed together. There are also rules,
77
+ which are not immediately apparent, about what you can filter and how.
78
+
79
+ By default filters passed to a +get+ are ANDed together. This means that all filters need to match for
80
+ the result to be returned.
81
+
82
+ results = gs.get({:start_date => '2008-01-01',
83
+ :end_date => '2008-02-01',
84
+ :dimensions => ['browser','browserVersion'],
85
+ :metrics => ['pageviews','visits'],
86
+ :sort => ['-pageviews'],
87
+ :filter => ['browser == Firefox','pageviews >= 10000']})
88
+
89
+ This says "return only results where the 'browser' dimension contains the word 'Firefox' and the
90
+ 'pageviews' metric is greater than or equal to 10,000.
91
+
92
+ Filters can contain spaces around the operators, or not. These two lines are equivalent (I think
93
+ the spaces make the filter more readable):
94
+
95
+ :filter => ['browser == Firefox','pageviews >= 10000']
96
+
97
+ :filter => ['browser==Firefox','pageviews>=10000']
98
+
99
+ Once again, do _not_ include the +ga:+ prefix before the dimension/metric you're filtering against.
100
+ Gattica will add this automatically.
101
+
102
+ You will probably find that as you try different filters, GA will report that some of them aren't
103
+ valid. I haven't found any documentation that says which filter combinations are valid in what
104
+ circumstances, so I suppose it's just trial and error at this point.
105
+
106
+ For more on filtering syntax, see the Analytics API docs: http://code.google.com/apis/analytics/docs/gdata/gdataReference.html#filtering
107
+
108
+ = Output
109
+ When Gattica was originally created it was intended to take the data returned and put it into
110
+ Excel for someone else to crunch through the numbers. Thus, Gattica has great built-in support
111
+ for CSV output. Once you have your data simply:
112
+
113
+ results.to_csv
114
+
115
+ A couple example rows of what that looks like:
116
+
117
+ "id","updated","title","browser","pageviews"
118
+ "http://www.google.com/analytics/feeds/data?ids=ga:12345&ga:browser=Internet%20Explorer&start-date=2009-01-01&end-date=2009-01-31","2009-01-30T16:00:00-08:00","ga:browser=Internet Explorer","Internet Explorer","53303"
119
+ "http://www.google.com/analytics/feeds/data?ids=ga:12345&ga:browser=Firefox&start-date=2009-01-01&end-date=2009-01-31","2009-01-30T16:00:00-08:00","ga:browser=Firefox","Firefox","20323"
120
+
121
+ Data is comma-separated and double-quote delimited. In most cases, people don't care
122
+ about the id, updated, or title attributes of this data. They just want the dimensions and
123
+ metrics. In that case, pass the symbol +:short+ to +to_csv+ and receive get back only the
124
+ the good stuff:
125
+
126
+ results.to_csv(:short)
127
+
128
+ Which returns:
129
+
130
+ "browser","pageviews"
131
+ "Internet Explorer","53303"
132
+ "Firefox","20323"
133
+
134
+ You can also just output the results as a string and you'll get the standard inspect syntax:
135
+
136
+ results.to_s
137
+
138
+ Gives you:
139
+
140
+ { "end_date"=>#<Date: 4909725/2,0,2299161>,
141
+ "start_date"=>#<Date: 4909665/2,0,2299161>,
142
+ "points"=>[
143
+ { "title"=>"ga:browser=Internet Explorer",
144
+ "dimensions"=>[{:browser=>"Internet Explorer"}],
145
+ "id"=>"http://www.google.com/analytics/feeds/data?ids=ga:12345&amp;ga:browser=Internet%20Explorer&amp;start-date=2009-01-01&amp;end-date=2009-01-31",
146
+ "metrics"=>[{:pageviews=>53303}],
147
+ "updated"=>#<DateTime: 212100120000001/86400000,-1/3,2299161>}]}
148
+
149
+ == Notes on Authentication
150
+ === Authentication Token
151
+ Google recommends not re-authenticating each time you do a request against the API. To accomplish
152
+ this you should save the authorization token you receive from Google and use that for future
153
+ requests:
154
+
155
+ ga.token => 'DSasdf94...' (some huge long string)
156
+
157
+ You can now initialize Gattica with this token for future requests:
158
+
159
+ ga = Gattica.new({:token => 'DSasdf94...'})
160
+
161
+ (You enter the full token, of course). I'm not sure how long a token from the Google's ClientLogin
162
+ system remains active, but if/when I do I'll add that to the docs here.
163
+
164
+ === Headers
165
+ Google expects a special header in all HTTP requests called 'Authorization'. This contains your
166
+ token:
167
+
168
+ Authorization = GoogleLogin auth=DSasdf94...
169
+
170
+ This header is generated automatically. If you have your own headers you'd like to add, you can
171
+ pass them in when you initialize:
172
+
173
+ ga = Gattica.new({:token => 'DSasdf94...', :headers => {'My-Special-Header':'my_custom_value'}})
174
+
175
+ And they'll be sent with every request you make.
176
+
177
+ = Limitations
178
+ The GA API limits each call to 1000 results per "page." If you want more, you need to tell
179
+ the API what number to begin at and it will return the next 1000. Gattica does not currently
180
+ support this, but it's in the plan for the very next version.
181
+
182
+ Currently all filters you supply are ANDed together before being sent to GA. Support for ORing
183
+ is coming soon.
184
+
185
+ = The Future
186
+ A couple of things I have planned:
187
+
188
+ 1. Tests!
189
+ 2. The option to use a custom delimiter for output
190
+ 3. Automatically handle paging (the API only returns 1000 results at a time). Gattica will request
191
+ one result set, see how many pages there are, then do several calls until all pages are retrieved
192
+ or it hits the limit of the number of results you want and return all that data as one big block.
data/Rakefile ADDED
@@ -0,0 +1,23 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+
5
+ begin
6
+ require 'jeweler'
7
+ Jeweler::Tasks.new do |gemspec|
8
+ gemspec.name = "gattica"
9
+ gemspec.summary = "Gattica is a Ruby library for extracting data from the Google Analytics API."
10
+ gemspec.email = "cannikinn@gmail.com"
11
+ gemspec.homepage = "http://github.com/cannikin/gattica"
12
+ gemspec.description = "Gattica is a Ruby library for extracting data from the Google Analytics API."
13
+ gemspec.authors = ["Rob Cameron"]
14
+ end
15
+ rescue LoadError
16
+ puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
17
+ end
18
+
19
+ Rake::TestTask.new do |t|
20
+ t.libs << 'lib'
21
+ t.pattern = 'test/**/test_*.rb'
22
+ t.verbose = false
23
+ end
data/VERSION.yml ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ :major: 0
3
+ :minor: 3
4
+ :patch: 3
@@ -0,0 +1,42 @@
1
+ require '../lib/gattica'
2
+
3
+ # authenticate with the API via email/password
4
+ ga = Gattica.new({:email => 'username@gmail.com', :password => 'password'})
5
+
6
+ # or, initialize via a pre-existing token. This initialization does not authenticate immediately,
7
+ # but will throw an error on subsequent calls (like ga.accounts) if the token is invalid
8
+ # ga = Gattica.new({:token => 'DQAAAJYAAACN-JMelka5I0Fs-T6lF53eUSfUooeHgcKc1iEdc0wkDS3w8GaXY7LjuUB_4vmzDB94HpScrULiweW_xQsU8yyUgdInDIX7ZnHm8_o0knf6FWSR90IoAZGsphpqteOjZ3O0NlNt603GgG7ylvGWRSeHl1ybD38nysMsKJR-dj0LYgIyPMvtgXLrqr_20oTTEExYbrDSg5_q84PkoLHUcODZ' })
9
+
10
+ # get the list of accounts you have access to with that username and password
11
+ accounts = ga.accounts
12
+
13
+ # for this example we just use the first account's profile_id, but you'll probably want to look
14
+ # at this list and choose the profile_id of the account you want (the web_property_id is the
15
+ # property you're most used to seeing in GA, looks like UA123456-1)
16
+ ga.profile_id = accounts.first.profile_id
17
+
18
+ # If you're using Gattica with a web app you'll want to save the authorization token
19
+ # and use that on subsequent requests (Google recommends not re-authenticating each time)
20
+ # ga.token
21
+
22
+ # now get the number of page views by browser for Janurary 2009
23
+ data = ga.get({ :start_date => '2009-01-01',
24
+ :end_date => '2009-01-31',
25
+ :dimensions => ['browser'],
26
+ :metrics => ['pageviews'],
27
+ :sort => ['-pageviews'] })
28
+
29
+ # output the data as CSV
30
+ puts data.to_csv
31
+
32
+ # a little more complex example with filtering. Show all pageviews by Firefox browsers
33
+ # (any version) where the number of page views is greater than 100
34
+ data = ga.get({ :start_date => '2009-01-01',
35
+ :end_date => '2009-01-31',
36
+ :dimensions => ['browser','browserVersion'],
37
+ :metrics => ['pageviews'],
38
+ :sort => ['-pageviews'],
39
+ :filters => ['browser == Firefox', 'pageviews > 100'] })
40
+
41
+ # write the data out as CSV in "short" format (doesn't include id, updated or title parameters)
42
+ puts data.to_csv(:short)
@@ -0,0 +1,26 @@
1
+ require 'rubygems'
2
+ require 'hpricot'
3
+
4
+ module Gattica
5
+
6
+ # Represents an account that an authenticated user has access to
7
+
8
+ class Account
9
+
10
+ include Convertible
11
+
12
+ attr_reader :id, :updated, :title, :table_id, :account_id, :account_name, :profile_id, :web_property_id
13
+
14
+ def initialize(xml)
15
+ @id = xml.at(:id).inner_html
16
+ @updated = DateTime.parse(xml.at(:updated).inner_html)
17
+ @title = xml.at(:title).inner_html
18
+ @table_id = xml.at('dxp:tableid').inner_html
19
+ @account_id = xml.at("dxp:property[@name='ga:accountId']").attributes['value'].to_i
20
+ @account_name = xml.at("dxp:property[@name='ga:accountName']").attributes['value']
21
+ @profile_id = xml.at("dxp:property[@name='ga:profileId']").attributes['value'].to_i
22
+ @web_property_id = xml.at("dxp:property[@name='ga:webPropertyId']").attributes['value']
23
+ end
24
+
25
+ end
26
+ end
@@ -0,0 +1,56 @@
1
+ require 'net/http'
2
+ require 'net/https'
3
+
4
+ module Gattica
5
+
6
+ # Authenticates a user against the Google Client Login system
7
+
8
+ class Auth
9
+
10
+ include Convertible
11
+
12
+ SCRIPT_NAME = '/accounts/ClientLogin'
13
+ HEADERS = { 'Content-Type' => 'application/x-www-form-urlencoded', 'User-Agent' => 'Ruby Net::HTTP' } # Google asks that you be nice and provide a user-agent string
14
+ OPTIONS = { :source => 'gattica-'+VERSION, :service => 'analytics' } # Google asks that you provide the name of your app as a 'source' parameter in your POST
15
+
16
+ attr_reader :tokens
17
+
18
+ # Try to authenticate the user
19
+ def initialize(http, user)
20
+ require 'cgi' unless defined?(CGI) && defined?(CGI::escape)
21
+ query_params = OPTIONS.merge(user.to_h).collect do |key, value|
22
+ "#{CGI.escape(key.to_s)}=#{CGI.escape(value.to_s)}"
23
+ end.sort * '&'
24
+
25
+
26
+ response, data = http.post(SCRIPT_NAME, query_params, HEADERS)
27
+ if response.code != '200'
28
+ case response.code
29
+ when '403'
30
+ raise GatticaError::CouldNotAuthenticate, 'Your email and/or password is not recognized by the Google ClientLogin system (status code: 403)'
31
+ else
32
+ raise GatticaError::UnknownAnalyticsError, response.body + " (status code: #{response.code})"
33
+ end
34
+ end
35
+ @tokens = parse_tokens(data)
36
+ end
37
+
38
+
39
+ private
40
+
41
+ # Parse the authentication tokens out of the response and makes them available as a hash
42
+ #
43
+ # tokens[:auth] => Google requires this for every request (added to HTTP headers on GET requests)
44
+ # tokens[:sid] => Not used
45
+ # tokens[:lsid] => Not used
46
+
47
+ def parse_tokens(data)
48
+ tokens = {}
49
+ data.split("\n").each do |t|
50
+ tokens.merge!({ t.split('=').first.downcase.to_sym => t.split('=').last })
51
+ end
52
+ return tokens
53
+ end
54
+
55
+ end
56
+ end
@@ -0,0 +1,39 @@
1
+ module Gattica
2
+
3
+ # Common output methods that are sharable
4
+
5
+ module Convertible
6
+
7
+ # output as hash
8
+ def to_h
9
+ output = {}
10
+ instance_variables.each do |var|
11
+ output.merge!({ var[1..-1] => instance_variable_get(var) }) unless var == '@xml' # exclude the whole XML dump
12
+ end
13
+ output
14
+ end
15
+
16
+ # output nice inspect syntax
17
+ def to_s
18
+ to_h.inspect
19
+ end
20
+
21
+ alias inspect to_s
22
+
23
+ def to_query
24
+ to_h.to_query
25
+ end
26
+
27
+ # Return the raw XML (if the object has a @xml instance variable, otherwise convert the object itself to xml)
28
+ def to_xml
29
+ if @xml
30
+ @xml
31
+ else
32
+ self.to_xml
33
+ end
34
+ end
35
+
36
+ alias to_yml to_yaml
37
+
38
+ end
39
+ end
@@ -0,0 +1,10 @@
1
+ class Hash
2
+
3
+ def key
4
+ self.keys.first if self.length == 1
5
+ end
6
+
7
+ def value
8
+ self.values.first if self.length == 1
9
+ end
10
+ end
@@ -0,0 +1,60 @@
1
+ require 'csv'
2
+
3
+ module Gattica
4
+
5
+ # Represents a single "row" of data containing any number of dimensions, metrics
6
+
7
+ class DataPoint
8
+
9
+ include Convertible
10
+
11
+ attr_reader :id, :updated, :title, :dimensions, :metrics, :xml
12
+
13
+ # Parses the XML <entry> element
14
+ def initialize(xml)
15
+ @xml = xml.to_s
16
+ @id = xml.at('id').inner_html
17
+ @updated = DateTime.parse(xml.at('updated').inner_html)
18
+ @title = xml.at('title').inner_html
19
+ @dimensions = xml.search('dxp:dimension').collect do |dimension|
20
+ { dimension.attributes['name'].split(':').last.to_sym => dimension.attributes['value'].split(':').last }
21
+ end
22
+ @metrics = xml.search('dxp:metric').collect do |metric|
23
+ { metric.attributes['name'].split(':').last.to_sym => metric.attributes['value'].split(':').last.to_i }
24
+ end
25
+ end
26
+
27
+
28
+ # Outputs in Comma Seperated Values format
29
+ def to_csv(format = :long)
30
+ output = ''
31
+ columns = []
32
+
33
+ # only output
34
+ case format
35
+ when :long
36
+ [@id, @updated, @title].each { |c| columns << c }
37
+ end
38
+
39
+ # output all dimensions
40
+ @dimensions.map {|d| d.value}.each { |c| columns << c }
41
+
42
+ # output all metrics
43
+ @metrics.map {|m| m.value}.each { |c| columns << c }
44
+
45
+ output = CSV.generate_line(columns)
46
+ return output
47
+ end
48
+
49
+
50
+ def to_yaml
51
+ { 'id' => @id,
52
+ 'updated' => @updated,
53
+ 'title' => @title,
54
+ 'dimensions' => @dimensions,
55
+ 'metrics' => @metrics }.to_yaml
56
+ end
57
+
58
+ end
59
+
60
+ end
@@ -0,0 +1,63 @@
1
+ module Gattica
2
+
3
+ # Encapsulates the data returned by the GA API
4
+
5
+ class DataSet
6
+
7
+ include Convertible
8
+
9
+ attr_reader :total_results, :start_index, :items_per_page, :start_date, :end_date, :points, :xml
10
+
11
+ def initialize(xml)
12
+ @xml = xml.to_s
13
+ @total_results = xml.at('openSearch:totalResults').inner_html.to_i
14
+ @start_index = xml.at('openSearch:startIndex').inner_html.to_i
15
+ @items_per_page = xml.at('openSearch:itemsPerPage').inner_html.to_i
16
+ @start_date = Date.parse(xml.at('dxp:startDate').inner_html)
17
+ @end_date = Date.parse(xml.at('dxp:endDate').inner_html)
18
+ @points = xml.search(:entry).collect { |entry| DataPoint.new(entry) }
19
+ end
20
+
21
+
22
+ # output important data to CSV, ignoring all the specific data about this dataset
23
+ # (total_results, start_date) and just output the data from the points
24
+
25
+ def to_csv(format = :long)
26
+ # build the headers
27
+ output = ''
28
+ columns = []
29
+
30
+ # only show the nitty gritty details of id, updated_at and title if requested
31
+ case format
32
+ when :long
33
+ ["id", "updated", "title"].each { |c| columns << c }
34
+ end
35
+
36
+ unless @points.empty? # if there was at least one result
37
+ @points.first.dimensions.map {|d| d.key}.each { |c| columns << c }
38
+ @points.first.metrics.map {|m| m.key}.each { |c| columns << c }
39
+ end
40
+
41
+ output = CSV.generate_line(columns) + "\n"
42
+
43
+ # get the data from each point
44
+ @points.each do |point|
45
+ output += point.to_csv(format) + "\n"
46
+ end
47
+
48
+ return output
49
+ end
50
+
51
+
52
+ def to_yaml
53
+ { 'total_results' => @total_results,
54
+ 'start_index' => @start_index,
55
+ 'items_per_page' => @items_per_page,
56
+ 'start_date' => @start_date,
57
+ 'end_date' => @end_date,
58
+ 'points' => @points}.to_yaml
59
+ end
60
+
61
+ end
62
+
63
+ end
@@ -0,0 +1,21 @@
1
+ module GatticaError
2
+ # user errors
3
+ class InvalidEmail < StandardError; end;
4
+ class InvalidPassword < StandardError; end;
5
+ # authentication errors
6
+ class CouldNotAuthenticate < StandardError; end;
7
+ class NoLoginOrToken < StandardError; end;
8
+ class InvalidToken < StandardError; end;
9
+ # profile errors
10
+ class InvalidProfileId < StandardError; end;
11
+ # search errors
12
+ class TooManyDimensions < StandardError; end;
13
+ class TooManyMetrics < StandardError; end;
14
+ class InvalidSort < StandardError; end;
15
+ class InvalidFilter < StandardError; end;
16
+ class MissingStartDate < StandardError; end;
17
+ class MissingEndDate < StandardError; end;
18
+ # errors from Analytics
19
+ class AnalyticsError < StandardError; end;
20
+ class UnknownAnalyticsError < StandardError; end;
21
+ end
@@ -0,0 +1,31 @@
1
+ module Gattica
2
+
3
+ # Represents a user to be authenticated by GA
4
+
5
+ class User
6
+
7
+ include Convertible
8
+
9
+ attr_accessor :email, :password
10
+
11
+ def initialize(email,password)
12
+ @email = email
13
+ @password = password
14
+ validate
15
+ end
16
+
17
+ # User gets a special +to_h+ because Google expects +Email+ and +Passwd+ instead of our nicer internal names
18
+ def to_h
19
+ { :Email => @email,
20
+ :Passwd => @password }
21
+ end
22
+
23
+ private
24
+ # Determine whether or not this is a valid user
25
+ def validate
26
+ raise GatticaError::InvalidEmail, "The email address '#{@email}' is not valid" if not @email.match(/^(?:[_a-z0-9-]+)(\.[_a-z0-9-]+)*@([a-z0-9-]+)(\.[a-zA-Z0-9\-\.]+)*(\.[a-z]{2,4})$/i)
27
+ raise GatticaError::InvalidPassword, "The password cannot be blank" if @password.empty? || @password.nil?
28
+ end
29
+
30
+ end
31
+ end
data/lib/gattica.rb ADDED
@@ -0,0 +1,276 @@
1
+ $:.unshift File.dirname(__FILE__) # for use/testing when no gem is installed
2
+
3
+ # external
4
+ require 'net/http'
5
+ require 'net/https'
6
+ require 'uri'
7
+ require 'cgi'
8
+ require 'logger'
9
+ require 'rubygems'
10
+ require 'hpricot'
11
+ require 'yaml'
12
+
13
+ # internal
14
+ require 'gattica/core_extensions'
15
+ require 'gattica/convertible'
16
+ require 'gattica/exceptions'
17
+ require 'gattica/user'
18
+ require 'gattica/auth'
19
+ require 'gattica/account'
20
+ require 'gattica/data_set'
21
+ require 'gattica/data_point'
22
+
23
+ # Gattica is a Ruby library for talking to the Google Analytics API.
24
+ #
25
+ # Please see the README for usage docs.
26
+
27
+ module Gattica
28
+
29
+ VERSION = '0.3.1'
30
+
31
+ # Creates a new instance of Gattica::Engine and gets us going. Please see the README for usage docs.
32
+ #
33
+ # ga = Gattica.new({:email => 'anonymous@anon.com', :password => 'password, :profile_id => 123456 })
34
+
35
+ def self.new(*args)
36
+ Engine.new(*args)
37
+ end
38
+
39
+ # The real meat of Gattica, deals with talking to GA, returning and parsing results. You actually get
40
+ # an instance of this when you go Gattica.new()
41
+
42
+ class Engine
43
+
44
+ SERVER = 'www.google.com'
45
+ PORT = 443
46
+ SECURE = true
47
+ DEFAULT_ARGS = { :start_date => nil, :end_date => nil, :dimensions => [], :metrics => [], :filters => [], :sort => [] }
48
+ DEFAULT_OPTIONS = { :email => nil, :password => nil, :token => nil, :profile_id => nil, :debug => false, :headers => {}, :logger => Logger.new(STDOUT) }
49
+ FILTER_METRIC_OPERATORS = %w{ == != > < >= <= }
50
+ FILTER_DIMENSION_OPERATORS = %w{ == != =~ !~ =@ ~@ }
51
+
52
+ attr_reader :user
53
+ attr_accessor :profile_id, :token
54
+
55
+ # Create a user, and get them authorized.
56
+ # If you're making a web app you're going to want to save the token that's retrieved by Gattica
57
+ # so that you can use it later (Google recommends not re-authenticating the user for each and every request)
58
+ #
59
+ # ga = Gattica.new({:email => 'johndoe@google.com', :password => 'password', :profile_id => 123456})
60
+ # ga.token => 'DW9N00wenl23R0...' (really long string)
61
+ #
62
+ # Or if you already have the token (because you authenticated previously and now want to reuse that session):
63
+ #
64
+ # ga = Gattica.new({:token => '23ohda09hw...', :profile_id => 123456})
65
+
66
+ def initialize(options={})
67
+ @options = DEFAULT_OPTIONS.merge(options)
68
+ @logger = @options[:logger]
69
+
70
+ @profile_id = @options[:profile_id] # if you don't include the profile_id now, you'll have to set it manually later via Gattica::Engine#profile_id=
71
+ @user_accounts = nil # filled in later if the user ever calls Gattica::Engine#accounts
72
+ @headers = {}.merge(@options[:headers]) # headers used for any HTTP requests (Google requires a special 'Authorization' header which is set any time @token is set)
73
+
74
+ # save an http connection for everyone to use
75
+ @http = Net::HTTP.new(SERVER, PORT)
76
+ @http.use_ssl = SECURE
77
+ @http.set_debug_output $stdout if @options[:debug]
78
+
79
+ # authenticate
80
+ if @options[:email] && @options[:password] # email and password: authenticate, get a token from Google's ClientLogin, save it for later
81
+ @user = User.new(@options[:email], @options[:password])
82
+ @auth = Auth.new(@http, user)
83
+ self.token = @auth.tokens[:auth]
84
+ elsif @options[:token] # use an existing token
85
+ self.token = @options[:token]
86
+ else # no login or token, you can't do anything
87
+ raise GatticaError::NoLoginOrToken, 'You must provide an email and password, or authentication token'
88
+ end
89
+
90
+ # TODO: check that the user has access to the specified profile and show an error here rather than wait for Google to respond with a message
91
+ end
92
+
93
+
94
+ # Returns the list of accounts the user has access to. A user may have multiple accounts on Google Analytics
95
+ # and each account may have multiple profiles. You need the profile_id in order to get info from GA. If you
96
+ # don't know the profile_id then use this method to get a list of all them. Then set the profile_id of your
97
+ # instance and you can make regular calls from then on.
98
+ #
99
+ # ga = Gattica.new({:email => 'johndoe@google.com', :password => 'password'})
100
+ # ga.get_accounts
101
+ # # you parse through the accounts to find the profile_id you need
102
+ # ga.profile_id = 12345678
103
+ # # now you can perform a regular search, see Gattica::Engine#get
104
+ #
105
+ # If you pass in a profile id when you instantiate Gattica::Search then you won't need to
106
+ # get the accounts and find a profile_id - you apparently already know it!
107
+ #
108
+ # See Gattica::Engine#get to see how to get some data.
109
+
110
+ def accounts
111
+ # if we haven't retrieved the user's accounts yet, get them now and save them
112
+ if @user_accounts.nil?
113
+ data = do_http_get('/analytics/feeds/accounts/default')
114
+ xml = Hpricot(data)
115
+ @user_accounts = xml.search(:entry).collect { |entry| Account.new(entry) }
116
+ end
117
+ return @user_accounts
118
+ end
119
+
120
+
121
+ # This is the method that performs the actual request to get data.
122
+ #
123
+ # == Usage
124
+ #
125
+ # gs = Gattica.new({:email => 'johndoe@google.com', :password => 'password', :profile_id => 123456})
126
+ # gs.get({ :start_date => '2008-01-01',
127
+ # :end_date => '2008-02-01',
128
+ # :dimensions => 'browser',
129
+ # :metrics => 'pageviews',
130
+ # :sort => 'pageviews',
131
+ # :filters => ['browser == Firefox']})
132
+ #
133
+ # == Input
134
+ #
135
+ # When calling +get+ you'll pass in a hash of options. For a description of what these mean to
136
+ # Google Analytics, see http://code.google.com/apis/analytics/docs
137
+ #
138
+ # Required values are:
139
+ #
140
+ # * +start_date+ => Beginning of the date range to search within
141
+ # * +end_date+ => End of the date range to search within
142
+ #
143
+ # Optional values are:
144
+ #
145
+ # * +dimensions+ => an array of GA dimensions (without the ga: prefix)
146
+ # * +metrics+ => an array of GA metrics (without the ga: prefix)
147
+ # * +filter+ => an array of GA dimensions/metrics you want to filter by (without the ga: prefix)
148
+ # * +sort+ => an array of GA dimensions/metrics you want to sort by (without the ga: prefix)
149
+ #
150
+ # == Exceptions
151
+ #
152
+ # If a user doesn't have access to the +profile_id+ you specified, you'll receive an error.
153
+ # Likewise, if you attempt to access a dimension or metric that doesn't exist, you'll get an
154
+ # error back from Google Analytics telling you so.
155
+
156
+ def get(args={})
157
+ args = validate_and_clean(DEFAULT_ARGS.merge(args))
158
+ query_string = build_query_string(args,@profile_id)
159
+ @logger.debug(query_string) if @debug
160
+ data = do_http_get("/analytics/feeds/data?#{query_string}")
161
+ return DataSet.new(Hpricot.XML(data))
162
+ end
163
+
164
+
165
+ # Since google wants the token to appear in any HTTP call's header, we have to set that header
166
+ # again any time @token is changed so we override the default writer (note that you need to set
167
+ # @token with self.token= instead of @token=)
168
+
169
+ def token=(token)
170
+ @token = token
171
+ set_http_headers
172
+ end
173
+
174
+
175
+ private
176
+
177
+
178
+ # Does the work of making HTTP calls and then going through a suite of tests on the response to make
179
+ # sure it's valid and not an error
180
+
181
+ def do_http_get(query_string)
182
+ response, data = @http.get(query_string, @headers)
183
+
184
+ # error checking
185
+ if response.code != '200'
186
+ case response.code
187
+ when '400'
188
+ raise GatticaError::AnalyticsError, response.body + " (status code: #{response.code})"
189
+ when '401'
190
+ raise GatticaError::InvalidToken, "Your authorization token is invalid or has expired (status code: #{response.code})"
191
+ else # some other unknown error
192
+ raise GatticaError::UnknownAnalyticsError, response.body + " (status code: #{response.code})"
193
+ end
194
+ end
195
+
196
+ return data
197
+ end
198
+
199
+
200
+ # Sets up the HTTP headers that Google expects (this is called any time @token is set either by Gattica
201
+ # or manually by the user since the header must include the token)
202
+ def set_http_headers
203
+ @headers['Authorization'] = "GoogleLogin auth=#{@token}"
204
+ end
205
+
206
+
207
+ # Creates a valid query string for GA
208
+ def build_query_string(args,profile)
209
+ output = "ids=ga:#{profile}&start-date=#{args[:start_date]}&end-date=#{args[:end_date]}"
210
+ unless args[:dimensions].empty?
211
+ output += '&dimensions=' + args[:dimensions].collect do |dimension|
212
+ "ga:#{dimension}"
213
+ end.join(',')
214
+ end
215
+ unless args[:metrics].empty?
216
+ output += '&metrics=' + args[:metrics].collect do |metric|
217
+ "ga:#{metric}"
218
+ end.join(',')
219
+ end
220
+ unless args[:sort].empty?
221
+ output += '&sort=' + args[:sort].collect do |sort|
222
+ sort[0..0] == '-' ? "-ga:#{sort[1..-1]}" : "ga:#{sort}" # if the first character is a dash, move it before the ga:
223
+ end.join(',')
224
+ end
225
+
226
+ # TODO: update so that in regular expression filters (=~ and !~), any initial special characters in the regular expression aren't also picked up as part of the operator (doesn't cause a problem, but just feels dirty)
227
+ unless args[:filters].empty? # filters are a little more complicated because they can have all kinds of modifiers
228
+ output += '&filters=' + args[:filters].collect do |filter|
229
+ match, name, operator, expression = *filter.match(/^(\w*)(\W*)(.*)$/) # splat the resulting Match object to pull out the parts automatically
230
+ unless name.empty? || operator.empty? || expression.empty? # make sure they all contain something
231
+ "ga:#{name}#{CGI::escape(operator.gsub(/ /,''))}#{CGI::escape(expression)}" # remove any whitespace from the operator before output
232
+ else
233
+ raise GatticaError::InvalidFilter, "The filter '#{filter}' is invalid. Filters should look like 'browser == Firefox' or 'browser==Firefox'"
234
+ end
235
+ end.join(';')
236
+ end
237
+ return output
238
+ end
239
+
240
+
241
+ # Validates that the args passed to +get+ are valid
242
+ def validate_and_clean(args)
243
+
244
+ raise GatticaError::MissingStartDate, ':start_date is required' if args[:start_date].nil? || args[:start_date].empty?
245
+ raise GatticaError::MissingEndDate, ':end_date is required' if args[:end_date].nil? || args[:end_date].empty?
246
+ raise GatticaError::TooManyDimensions, 'You can only have a maximum of 7 dimensions' if args[:dimensions] && (args[:dimensions].is_a?(Array) && args[:dimensions].length > 7)
247
+ raise GatticaError::TooManyMetrics, 'You can only have a maximum of 10 metrics' if args[:metrics] && (args[:metrics].is_a?(Array) && args[:metrics].length > 10)
248
+
249
+ possible = args[:dimensions] + args[:metrics]
250
+
251
+ # make sure that the user is only trying to sort fields that they've previously included with dimensions and metrics
252
+ if args[:sort]
253
+ missing = args[:sort].find_all do |arg|
254
+ !possible.include? arg.gsub(/^-/,'') # remove possible minuses from any sort params
255
+ end
256
+ unless missing.empty?
257
+ raise GatticaError::InvalidSort, "You are trying to sort by fields that are not in the available dimensions or metrics: #{missing.join(', ')}"
258
+ end
259
+ end
260
+
261
+ # make sure that the user is only trying to filter fields that are in dimensions or metrics
262
+ if args[:filters]
263
+ missing = args[:filters].find_all do |arg|
264
+ !possible.include? arg.match(/^\w*/).to_s # get the name of the filter and compare
265
+ end
266
+ unless missing.empty?
267
+ raise GatticaError::InvalidSort, "You are trying to filter by fields that are not in the available dimensions or metrics: #{missing.join(', ')}"
268
+ end
269
+ end
270
+
271
+ return args
272
+ end
273
+
274
+
275
+ end
276
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,15 @@
1
+ require File.join(File.dirname(__FILE__), *%w[.. lib gattica])
2
+
3
+ require 'rubygems'
4
+ require 'test/unit'
5
+ require 'mocha'
6
+
7
+ # include Gattica
8
+
9
+ def fixture(name)
10
+ File.read(File.join(File.dirname(__FILE__), 'fixtures', name))
11
+ end
12
+
13
+ def absolute_project_path
14
+ File.expand_path(File.join(File.dirname(__FILE__), '..'))
15
+ end
data/test/suite.rb ADDED
@@ -0,0 +1,6 @@
1
+ require 'test/unit'
2
+
3
+ tests = Dir["#{File.dirname(__FILE__)}/test_*.rb"]
4
+ tests.each do |file|
5
+ require file
6
+ end
data/test/test_auth.rb ADDED
@@ -0,0 +1,12 @@
1
+ require File.dirname(__FILE__) + '/helper'
2
+
3
+ class TestAuth < Test::Unit::TestCase
4
+ def setup
5
+
6
+ end
7
+
8
+ def test_truth
9
+ assert true
10
+ end
11
+
12
+ end
@@ -0,0 +1,14 @@
1
+ require File.dirname(__FILE__) + '/helper'
2
+
3
+ class TestEngine < Test::Unit::TestCase
4
+ def setup
5
+
6
+ end
7
+
8
+ def test_initialization
9
+ # assert Gattica.new({:email => 'anonymous@anon.com', :password => 'none'}) # you can initialize with a potentially invalid email and password
10
+ assert Gattica.new({:token => 'test'}) # you can initialize with a potentially invalid token
11
+ assert_raise GatticaError::NoLoginOrToken do Gattica.new() end # but, you must initialize with one or the other
12
+ end
13
+
14
+ end
data/test/test_user.rb ADDED
@@ -0,0 +1,24 @@
1
+ require File.dirname(__FILE__) + '/helper'
2
+
3
+ class TestUser < Test::Unit::TestCase
4
+ def setup
5
+
6
+ end
7
+
8
+ def test_can_create_user
9
+ assert Gattica::User.new('anonymous@anon.com','none')
10
+ end
11
+
12
+ def test_invalid_email
13
+ assert_raise GatticaError::InvalidEmail do Gattica::User.new('','') end
14
+ assert_raise ArgumentError do Gattica::User.new('') end
15
+ assert_raise GatticaError::InvalidEmail do Gattica::User.new('anonymous','none') end
16
+ assert_raise GatticaError::InvalidEmail do Gattica::User.new('anonymous@asdfcom','none') end
17
+ end
18
+
19
+ def test_invalid_password
20
+ assert_raise GatticaError::InvalidPassword do Gattica::User.new('anonymous@anon.com','') end
21
+ assert_raise ArgumentError do Gattica::User.new('anonymous@anon.com') end
22
+ end
23
+
24
+ end
metadata ADDED
@@ -0,0 +1,78 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jeremyf-gattica
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.3.4
5
+ platform: ruby
6
+ authors:
7
+ - Rob Cameron
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-05-18 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: Gattica is a Ruby library for extracting data from the Google Analytics API.
17
+ email: cannikinn@gmail.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files:
23
+ - LICENSE
24
+ - README.rdoc
25
+ files:
26
+ - History.txt
27
+ - LICENSE
28
+ - README.rdoc
29
+ - Rakefile
30
+ - VERSION.yml
31
+ - examples/example.rb
32
+ - lib/gattica.rb
33
+ - lib/gattica/account.rb
34
+ - lib/gattica/auth.rb
35
+ - lib/gattica/convertible.rb
36
+ - lib/gattica/core_extensions.rb
37
+ - lib/gattica/data_point.rb
38
+ - lib/gattica/data_set.rb
39
+ - lib/gattica/exceptions.rb
40
+ - lib/gattica/user.rb
41
+ - test/helper.rb
42
+ - test/suite.rb
43
+ - test/test_auth.rb
44
+ - test/test_engine.rb
45
+ - test/test_user.rb
46
+ has_rdoc: true
47
+ homepage: http://github.com/cannikin/gattica
48
+ post_install_message:
49
+ rdoc_options:
50
+ - --charset=UTF-8
51
+ require_paths:
52
+ - lib
53
+ required_ruby_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: "0"
58
+ version:
59
+ required_rubygems_version: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: "0"
64
+ version:
65
+ requirements: []
66
+
67
+ rubyforge_project:
68
+ rubygems_version: 1.2.0
69
+ signing_key:
70
+ specification_version: 2
71
+ summary: Gattica is a Ruby library for extracting data from the Google Analytics API.
72
+ test_files:
73
+ - test/helper.rb
74
+ - test/suite.rb
75
+ - test/test_auth.rb
76
+ - test/test_engine.rb
77
+ - test/test_user.rb
78
+ - examples/example.rb