gattica 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/History.txt +42 -0
- data/LICENSE +22 -0
- data/README.rdoc +190 -0
- data/Rakefile +24 -0
- data/VERSION.yml +4 -0
- data/examples/example.rb +42 -0
- data/gattica.gemspec +72 -0
- data/lib/gattica.rb +348 -0
- data/lib/gattica/account.rb +26 -0
- data/lib/gattica/auth.rb +52 -0
- data/lib/gattica/convertible.rb +39 -0
- data/lib/gattica/core_extensions.rb +25 -0
- data/lib/gattica/data_point.rb +60 -0
- data/lib/gattica/data_set.rb +76 -0
- data/lib/gattica/exceptions.rb +23 -0
- data/lib/gattica/user.rb +31 -0
- data/test/helper.rb +15 -0
- data/test/suite.rb +6 -0
- data/test/test_auth.rb +12 -0
- data/test/test_engine.rb +14 -0
- data/test/test_gattica.rb +16 -0
- data/test/test_user.rb +24 -0
- metadata +93 -0
data/.gitignore
ADDED
data/History.txt
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
== 0.4.0
|
2
|
+
* er1c added start_index and max_results
|
3
|
+
* er1c added paging for all results
|
4
|
+
* er1c added get_to_csv to "stream" saving results to a file IO
|
5
|
+
* thieso2 fix DataPoint when GA-Path includec colons
|
6
|
+
* jeremyf Added ability to parse addition query strings
|
7
|
+
* nedski Added support for proxy via env var
|
8
|
+
|
9
|
+
== 0.3.4
|
10
|
+
* rumble updated the regex used to pull apart the filters so it didn't get confused when there a filename, for example, started with a /
|
11
|
+
|
12
|
+
== 0.3.2
|
13
|
+
* er1c updated to use standard Ruby CSV library
|
14
|
+
|
15
|
+
== 0.3.0
|
16
|
+
* Support for filters (filters are all AND'ed together, no OR yet)
|
17
|
+
|
18
|
+
== 0.2.1
|
19
|
+
* More robust error checking on HTTP calls
|
20
|
+
* Added to_xml to get raw XML output from Google
|
21
|
+
|
22
|
+
== 0.2.0 / 2009-04-27
|
23
|
+
* Changed initialization format: pass a hash of options rather than individual email, password and profile_id
|
24
|
+
* Can initialize with a valid token and use that instead of requiring email/password each time
|
25
|
+
* 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)
|
26
|
+
* Show error if token is invalid or expired (Google returns a 401 on any HTTP call)
|
27
|
+
* Started tests
|
28
|
+
|
29
|
+
== 0.1.4 / 2009-04-22
|
30
|
+
* Another attempt at getting the gem to build on github
|
31
|
+
|
32
|
+
== 0.1.3 / 2009-04-22
|
33
|
+
* Getting gem to build on github
|
34
|
+
|
35
|
+
== 0.1.2 / 2009-04-22
|
36
|
+
* Updated readme and examples, better documentation throughout
|
37
|
+
|
38
|
+
== 0.1.1 / 2009-04-22
|
39
|
+
* When outputting as CSV, surround each piece of data with double quotes (appears pretty common for various properties (like Browser name) to contain commas
|
40
|
+
|
41
|
+
== 0.1.0 / 2009-03-26
|
42
|
+
* 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,190 @@
|
|
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
|
+
:filters => ['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
|
+
:filters => ['browser == Firefox','pageviews >= 10000']
|
96
|
+
|
97
|
+
:filters => ['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&ga:browser=Internet%20Explorer&start-date=2009-01-01&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
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,24 @@
|
|
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
|
+
gemspec.add_dependency('hpricot','>=0.6.164')
|
15
|
+
end
|
16
|
+
rescue LoadError
|
17
|
+
puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
|
18
|
+
end
|
19
|
+
|
20
|
+
Rake::TestTask.new do |t|
|
21
|
+
t.libs << 'lib'
|
22
|
+
t.pattern = 'test/**/test_*.rb'
|
23
|
+
t.verbose = false
|
24
|
+
end
|
data/VERSION.yml
ADDED
data/examples/example.rb
ADDED
@@ -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)
|
data/gattica.gemspec
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run `rake gemspec`
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{gattica}
|
8
|
+
s.version = "0.4.0"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Rob Cameron"]
|
12
|
+
s.date = %q{2009-09-04}
|
13
|
+
s.description = %q{Gattica is a Ruby library for extracting data from the Google Analytics API.}
|
14
|
+
s.email = %q{cannikinn@gmail.com}
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"LICENSE",
|
17
|
+
"README.rdoc"
|
18
|
+
]
|
19
|
+
s.files = [
|
20
|
+
".gitignore",
|
21
|
+
"History.txt",
|
22
|
+
"LICENSE",
|
23
|
+
"README.rdoc",
|
24
|
+
"Rakefile",
|
25
|
+
"VERSION.yml",
|
26
|
+
"examples/example.rb",
|
27
|
+
"gattica.gemspec",
|
28
|
+
"lib/gattica.rb",
|
29
|
+
"lib/gattica/account.rb",
|
30
|
+
"lib/gattica/auth.rb",
|
31
|
+
"lib/gattica/convertible.rb",
|
32
|
+
"lib/gattica/core_extensions.rb",
|
33
|
+
"lib/gattica/data_point.rb",
|
34
|
+
"lib/gattica/data_set.rb",
|
35
|
+
"lib/gattica/exceptions.rb",
|
36
|
+
"lib/gattica/user.rb",
|
37
|
+
"test/helper.rb",
|
38
|
+
"test/suite.rb",
|
39
|
+
"test/test_auth.rb",
|
40
|
+
"test/test_engine.rb",
|
41
|
+
"test/test_gattica.rb",
|
42
|
+
"test/test_user.rb"
|
43
|
+
]
|
44
|
+
s.has_rdoc = true
|
45
|
+
s.homepage = %q{http://github.com/cannikin/gattica}
|
46
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
47
|
+
s.require_paths = ["lib"]
|
48
|
+
s.rubygems_version = %q{1.3.1}
|
49
|
+
s.summary = %q{Gattica is a Ruby library for extracting data from the Google Analytics API.}
|
50
|
+
s.test_files = [
|
51
|
+
"test/helper.rb",
|
52
|
+
"test/suite.rb",
|
53
|
+
"test/test_auth.rb",
|
54
|
+
"test/test_engine.rb",
|
55
|
+
"test/test_gattica.rb",
|
56
|
+
"test/test_user.rb",
|
57
|
+
"examples/example.rb"
|
58
|
+
]
|
59
|
+
|
60
|
+
if s.respond_to? :specification_version then
|
61
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
62
|
+
s.specification_version = 2
|
63
|
+
|
64
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
65
|
+
s.add_runtime_dependency(%q<hpricot>, [">= 0.6.164"])
|
66
|
+
else
|
67
|
+
s.add_dependency(%q<hpricot>, [">= 0.6.164"])
|
68
|
+
end
|
69
|
+
else
|
70
|
+
s.add_dependency(%q<hpricot>, [">= 0.6.164"])
|
71
|
+
end
|
72
|
+
end
|
data/lib/gattica.rb
ADDED
@@ -0,0 +1,348 @@
|
|
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.4.0'
|
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 => [], :start_index => 1, :max_results => 10000, :page => false }
|
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 a proxy-aware http connection for everyone to use
|
75
|
+
proxy_host = nil
|
76
|
+
proxy_port = nil
|
77
|
+
proxy_var = SECURE ? 'https_proxy' : 'http_proxy'
|
78
|
+
[proxy_var, proxy_var.upcase].each do |pxy|
|
79
|
+
if ENV[pxy]
|
80
|
+
uri = URI::parse(ENV[pxy])
|
81
|
+
proxy_host = uri.host
|
82
|
+
proxy_port = uri.port
|
83
|
+
end
|
84
|
+
end
|
85
|
+
@http = Net::HTTP::Proxy(proxy_host,proxy_port).new(SERVER, PORT)
|
86
|
+
@http.use_ssl = SECURE
|
87
|
+
@http.set_debug_output $stdout if @options[:debug]
|
88
|
+
|
89
|
+
# authenticate
|
90
|
+
if @options[:email] && @options[:password] # email and password: authenticate, get a token from Google's ClientLogin, save it for later
|
91
|
+
@user = User.new(@options[:email], @options[:password])
|
92
|
+
@auth = Auth.new(@http, user)
|
93
|
+
self.token = @auth.tokens[:auth]
|
94
|
+
elsif @options[:token] # use an existing token
|
95
|
+
self.token = @options[:token]
|
96
|
+
else # no login or token, you can't do anything
|
97
|
+
raise GatticaError::NoLoginOrToken, 'You must provide an email and password, or authentication token'
|
98
|
+
end
|
99
|
+
|
100
|
+
# 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
|
101
|
+
end
|
102
|
+
|
103
|
+
|
104
|
+
# Returns the list of accounts the user has access to. A user may have multiple accounts on Google Analytics
|
105
|
+
# and each account may have multiple profiles. You need the profile_id in order to get info from GA. If you
|
106
|
+
# don't know the profile_id then use this method to get a list of all them. Then set the profile_id of your
|
107
|
+
# instance and you can make regular calls from then on.
|
108
|
+
#
|
109
|
+
# ga = Gattica.new({:email => 'johndoe@google.com', :password => 'password'})
|
110
|
+
# ga.get_accounts
|
111
|
+
# # you parse through the accounts to find the profile_id you need
|
112
|
+
# ga.profile_id = 12345678
|
113
|
+
# # now you can perform a regular search, see Gattica::Engine#get
|
114
|
+
#
|
115
|
+
# If you pass in a profile id when you instantiate Gattica::Search then you won't need to
|
116
|
+
# get the accounts and find a profile_id - you apparently already know it!
|
117
|
+
#
|
118
|
+
# See Gattica::Engine#get to see how to get some data.
|
119
|
+
|
120
|
+
def accounts
|
121
|
+
# if we haven't retrieved the user's accounts yet, get them now and save them
|
122
|
+
if @user_accounts.nil?
|
123
|
+
data = do_http_get('/analytics/feeds/accounts/default')
|
124
|
+
xml = Hpricot(data)
|
125
|
+
@user_accounts = xml.search(:entry).collect { |entry| Account.new(entry) }
|
126
|
+
end
|
127
|
+
return @user_accounts
|
128
|
+
end
|
129
|
+
|
130
|
+
# Performs a Gattica::Engine#get but instead of returning the dataset streams it to the file handle in a CSV format
|
131
|
+
#
|
132
|
+
# == Usage
|
133
|
+
#
|
134
|
+
# gs = Gattica.new({:email => 'johndoe@google.com', :password => 'password', :profile_id => 123456})
|
135
|
+
# fh = File.new("file.csv", "w")
|
136
|
+
# gs.get_to_csv({ :start_date => '2008-01-01',
|
137
|
+
# :end_date => '2008-02-01',
|
138
|
+
# :dimensions => 'browser',
|
139
|
+
# :metrics => 'pageviews',
|
140
|
+
# :sort => 'pageviews',
|
141
|
+
# :filters => ['browser == Firefox']}, fh, :short)
|
142
|
+
#
|
143
|
+
# See Gattica::Engine#get to see details of arguments
|
144
|
+
|
145
|
+
def get_to_csv(args={}, fh = nil, format = :long)
|
146
|
+
raise GatticaError::InvalidFileType, "Invalid file handle" unless !fh.nil?
|
147
|
+
results(args, fh, :csv, format)
|
148
|
+
end
|
149
|
+
|
150
|
+
# This is the method that performs the actual request to get data.
|
151
|
+
#
|
152
|
+
# == Usage
|
153
|
+
#
|
154
|
+
# gs = Gattica.new({:email => 'johndoe@google.com', :password => 'password', :profile_id => 123456})
|
155
|
+
# gs.get({ :start_date => '2008-01-01',
|
156
|
+
# :end_date => '2008-02-01',
|
157
|
+
# :dimensions => 'browser',
|
158
|
+
# :metrics => 'pageviews',
|
159
|
+
# :sort => 'pageviews',
|
160
|
+
# :filters => ['browser == Firefox']})
|
161
|
+
#
|
162
|
+
# == Input
|
163
|
+
#
|
164
|
+
# When calling +get+ you'll pass in a hash of options. For a description of what these mean to
|
165
|
+
# Google Analytics, see http://code.google.com/apis/analytics/docs
|
166
|
+
#
|
167
|
+
# Required values are:
|
168
|
+
#
|
169
|
+
# * +start_date+ => Beginning of the date range to search within
|
170
|
+
# * +end_date+ => End of the date range to search within
|
171
|
+
#
|
172
|
+
# Optional values are:
|
173
|
+
#
|
174
|
+
# * +dimensions+ => an array of GA dimensions (without the ga: prefix)
|
175
|
+
# * +metrics+ => an array of GA metrics (without the ga: prefix)
|
176
|
+
# * +filter+ => an array of GA dimensions/metrics you want to filter by (without the ga: prefix)
|
177
|
+
# * +sort+ => an array of GA dimensions/metrics you want to sort by (without the ga: prefix)
|
178
|
+
# * +page+ => true|false Does the paging to create a single set of all of the data
|
179
|
+
# * +start_index+ => Beginning offset of the query (default 1)
|
180
|
+
# * +max_results+ => How many results to grab (maximum 10,000)
|
181
|
+
#
|
182
|
+
# == Exceptions
|
183
|
+
#
|
184
|
+
# If a user doesn't have access to the +profile_id+ you specified, you'll receive an error.
|
185
|
+
# Likewise, if you attempt to access a dimension or metric that doesn't exist, you'll get an
|
186
|
+
# error back from Google Analytics telling you so.
|
187
|
+
|
188
|
+
def get(args={})
|
189
|
+
return results(args)
|
190
|
+
end
|
191
|
+
|
192
|
+
private
|
193
|
+
|
194
|
+
def results(args={}, fh=nil, type=nil, format=nil)
|
195
|
+
raise GatticaError::InvalidFileType, "Invalid file type" unless type.nil? ||[:csv,:xml].include?(type)
|
196
|
+
args = validate_and_clean(DEFAULT_ARGS.merge(args))
|
197
|
+
|
198
|
+
header = 0
|
199
|
+
results = nil
|
200
|
+
total_results = args[:max_results]
|
201
|
+
while(args[:start_index] < total_results)
|
202
|
+
query_string = build_query_string(args,@profile_id)
|
203
|
+
@logger.debug("Query String: " + query_string) if @debug
|
204
|
+
|
205
|
+
data = do_http_get("/analytics/feeds/data?#{query_string}")
|
206
|
+
result = DataSet.new(Hpricot.XML(data))
|
207
|
+
|
208
|
+
#handle returning results
|
209
|
+
results.points.concat(result.points) if !results.nil? && fh.nil?
|
210
|
+
#handle csv
|
211
|
+
|
212
|
+
if(!fh.nil? && type == :csv && header == 0)
|
213
|
+
fh.write result.to_csv_header(format)
|
214
|
+
header = 1
|
215
|
+
end
|
216
|
+
|
217
|
+
fh.write result.to_csv(:noheader) if !fh.nil? && type == :csv
|
218
|
+
fh.flush if !fh.nil?
|
219
|
+
|
220
|
+
results = result if results.nil?
|
221
|
+
total_results = result.total_results
|
222
|
+
args[:start_index] += args[:max_results]
|
223
|
+
break if !args[:page] # only continue while if we are suppose to page
|
224
|
+
end
|
225
|
+
return results if fh.nil?
|
226
|
+
end
|
227
|
+
|
228
|
+
# Since google wants the token to appear in any HTTP call's header, we have to set that header
|
229
|
+
# again any time @token is changed so we override the default writer (note that you need to set
|
230
|
+
# @token with self.token= instead of @token=)
|
231
|
+
|
232
|
+
def token=(token)
|
233
|
+
@token = token
|
234
|
+
set_http_headers
|
235
|
+
end
|
236
|
+
|
237
|
+
|
238
|
+
# Does the work of making HTTP calls and then going through a suite of tests on the response to make
|
239
|
+
# sure it's valid and not an error
|
240
|
+
|
241
|
+
def do_http_get(query_string)
|
242
|
+
response, data = @http.get(query_string, @headers)
|
243
|
+
|
244
|
+
# error checking
|
245
|
+
if response.code != '200'
|
246
|
+
case response.code
|
247
|
+
when '400'
|
248
|
+
raise GatticaError::AnalyticsError, response.body + " (status code: #{response.code})"
|
249
|
+
when '401'
|
250
|
+
raise GatticaError::InvalidToken, "Your authorization token is invalid or has expired (status code: #{response.code})"
|
251
|
+
else # some other unknown error
|
252
|
+
raise GatticaError::UnknownAnalyticsError, response.body + " (status code: #{response.code})"
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
return data
|
257
|
+
end
|
258
|
+
|
259
|
+
private
|
260
|
+
|
261
|
+
# Sets up the HTTP headers that Google expects (this is called any time @token is set either by Gattica
|
262
|
+
# or manually by the user since the header must include the token)
|
263
|
+
def set_http_headers
|
264
|
+
@headers['Authorization'] = "GoogleLogin auth=#{@token}"
|
265
|
+
end
|
266
|
+
|
267
|
+
|
268
|
+
# Creates a valid query string for GA
|
269
|
+
def build_query_string(args,profile)
|
270
|
+
query_params = args.clone
|
271
|
+
ga_start_date = query_params.delete(:start_date)
|
272
|
+
ga_end_date = query_params.delete(:end_date)
|
273
|
+
ga_dimensions = query_params.delete(:dimensions)
|
274
|
+
ga_metrics = query_params.delete(:metrics)
|
275
|
+
ga_sort = query_params.delete(:sort)
|
276
|
+
ga_filters = query_params.delete(:filters)
|
277
|
+
|
278
|
+
output = "ids=ga:#{profile}&start-date=#{ga_start_date}&end-date=#{ga_end_date}"
|
279
|
+
unless ga_dimensions.nil? || ga_dimensions.empty?
|
280
|
+
output += '&dimensions=' + ga_dimensions.collect do |dimension|
|
281
|
+
"ga:#{dimension}"
|
282
|
+
end.join(',')
|
283
|
+
end
|
284
|
+
unless ga_metrics.nil? || ga_metrics.empty?
|
285
|
+
output += '&metrics=' + ga_metrics.collect do |metric|
|
286
|
+
"ga:#{metric}"
|
287
|
+
end.join(',')
|
288
|
+
end
|
289
|
+
unless ga_sort.nil? || ga_sort.empty?
|
290
|
+
output += '&sort=' + Array(ga_sort).collect do |sort|
|
291
|
+
sort[0..0] == '-' ? "-ga:#{sort[1..-1]}" : "ga:#{sort}" # if the first character is a dash, move it before the ga:
|
292
|
+
end.join(',')
|
293
|
+
end
|
294
|
+
|
295
|
+
# 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)
|
296
|
+
unless args[:filters].empty? # filters are a little more complicated because they can have all kinds of modifiers
|
297
|
+
output += '&filters=' + args[:filters].collect do |filter|
|
298
|
+
match, name, operator, expression = *filter.match(/^(\w*)\s*([=!<>~@]*)\s*(.*)$/) # splat the resulting Match object to pull out the parts automatically
|
299
|
+
unless name.empty? || operator.empty? || expression.empty? # make sure they all contain something
|
300
|
+
"ga:#{name}#{CGI::escape(operator.gsub(/ /,''))}#{CGI::escape(expression)}" # remove any whitespace from the operator before output
|
301
|
+
else
|
302
|
+
raise GatticaError::InvalidFilter, "The filter '#{filter}' is invalid. Filters should look like 'browser == Firefox' or 'browser==Firefox'"
|
303
|
+
end
|
304
|
+
end.join(';')
|
305
|
+
end
|
306
|
+
|
307
|
+
query_params.inject(output) {|m,(key,value)| m << "&#{key}=#{value}"}
|
308
|
+
|
309
|
+
return output
|
310
|
+
end
|
311
|
+
|
312
|
+
|
313
|
+
# Validates that the args passed to +get+ are valid
|
314
|
+
def validate_and_clean(args)
|
315
|
+
|
316
|
+
raise GatticaError::MissingStartDate, ':start_date is required' if args[:start_date].nil? || args[:start_date].empty?
|
317
|
+
raise GatticaError::MissingEndDate, ':end_date is required' if args[:end_date].nil? || args[:end_date].empty?
|
318
|
+
raise GatticaError::TooManyDimensions, 'You can only have a maximum of 7 dimensions' if args[:dimensions] && (args[:dimensions].is_a?(Array) && args[:dimensions].length > 7)
|
319
|
+
raise GatticaError::TooManyMetrics, 'You can only have a maximum of 10 metrics' if args[:metrics] && (args[:metrics].is_a?(Array) && args[:metrics].length > 10)
|
320
|
+
|
321
|
+
possible = args[:dimensions] + args[:metrics]
|
322
|
+
|
323
|
+
# make sure that the user is only trying to sort fields that they've previously included with dimensions and metrics
|
324
|
+
if args[:sort]
|
325
|
+
missing = args[:sort].find_all do |arg|
|
326
|
+
!possible.include? arg.gsub(/^-/,'') # remove possible minuses from any sort params
|
327
|
+
end
|
328
|
+
unless missing.empty?
|
329
|
+
raise GatticaError::InvalidSort, "You are trying to sort by fields that are not in the available dimensions or metrics: #{missing.join(', ')}"
|
330
|
+
end
|
331
|
+
end
|
332
|
+
|
333
|
+
# make sure that the user is only trying to filter fields that are in dimensions or metrics
|
334
|
+
if args[:filters]
|
335
|
+
missing = args[:filters].find_all do |arg|
|
336
|
+
!possible.include? arg.match(/^\w*/).to_s # get the name of the filter and compare
|
337
|
+
end
|
338
|
+
unless missing.empty?
|
339
|
+
raise GatticaError::InvalidSort, "You are trying to filter by fields that are not in the available dimensions or metrics: #{missing.join(', ')}"
|
340
|
+
end
|
341
|
+
end
|
342
|
+
|
343
|
+
return args
|
344
|
+
end
|
345
|
+
|
346
|
+
|
347
|
+
end
|
348
|
+
end
|
@@ -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
|
data/lib/gattica/auth.rb
ADDED
@@ -0,0 +1,52 @@
|
|
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
|
+
options = OPTIONS.merge(user.to_h)
|
21
|
+
|
22
|
+
response, data = http.post(SCRIPT_NAME, options.to_query, HEADERS)
|
23
|
+
if response.code != '200'
|
24
|
+
case response.code
|
25
|
+
when '403'
|
26
|
+
raise GatticaError::CouldNotAuthenticate, 'Your email and/or password is not recognized by the Google ClientLogin system (status code: 403)'
|
27
|
+
else
|
28
|
+
raise GatticaError::UnknownAnalyticsError, response.body + " (status code: #{response.code})"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
@tokens = parse_tokens(data)
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
# Parse the authentication tokens out of the response and makes them available as a hash
|
38
|
+
#
|
39
|
+
# tokens[:auth] => Google requires this for every request (added to HTTP headers on GET requests)
|
40
|
+
# tokens[:sid] => Not used
|
41
|
+
# tokens[:lsid] => Not used
|
42
|
+
|
43
|
+
def parse_tokens(data)
|
44
|
+
tokens = {}
|
45
|
+
data.split("\n").each do |t|
|
46
|
+
tokens.merge!({ t.split('=').first.downcase.to_sym => t.split('=').last })
|
47
|
+
end
|
48
|
+
return tokens
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
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,25 @@
|
|
1
|
+
class Hash
|
2
|
+
|
3
|
+
def to_query
|
4
|
+
require 'cgi' unless defined?(CGI) && defined?(CGI::escape)
|
5
|
+
self.collect do |key, value|
|
6
|
+
"#{CGI.escape(key.to_s)}=#{CGI.escape(value.to_s)}"
|
7
|
+
end.sort * '&'
|
8
|
+
end
|
9
|
+
|
10
|
+
def key
|
11
|
+
self.keys.first if self.length == 1
|
12
|
+
end
|
13
|
+
|
14
|
+
def value
|
15
|
+
self.values.first if self.length == 1
|
16
|
+
end
|
17
|
+
|
18
|
+
def stringify_keys
|
19
|
+
inject({}) do |options, (key, value)|
|
20
|
+
options[key.to_s] = value
|
21
|
+
options
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
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(':', 1).last }
|
21
|
+
end
|
22
|
+
@metrics = xml.search('dxp:metric').collect do |metric|
|
23
|
+
{ metric.attributes['name'].split(':').last.to_sym => metric.attributes['value'].split(':', 1).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
|
+
|
32
|
+
columns = []
|
33
|
+
# only output
|
34
|
+
case format
|
35
|
+
when :long
|
36
|
+
columns.concat([@id, @updated, @title])
|
37
|
+
end
|
38
|
+
|
39
|
+
# output all dimensions
|
40
|
+
columns.concat(@dimensions.map {|d| d.value})
|
41
|
+
|
42
|
+
# output all metrics
|
43
|
+
columns.concat(@metrics.map {|m| m.value})
|
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,76 @@
|
|
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
|
+
def to_csv_header(format = :long)
|
22
|
+
# build the headers
|
23
|
+
output = ''
|
24
|
+
columns = []
|
25
|
+
|
26
|
+
# only show the nitty gritty details of id, updated_at and title if requested
|
27
|
+
case format #it would be nice if case statements in ruby worked differently
|
28
|
+
when :long
|
29
|
+
columns.concat(["id", "updated", "title"])
|
30
|
+
unless @points.empty? # if there was at least one result
|
31
|
+
columns.concat(@points.first.dimensions.map {|d| d.key})
|
32
|
+
columns.concat(@points.first.metrics.map {|m| m.key})
|
33
|
+
end
|
34
|
+
when :short
|
35
|
+
unless @points.empty? # if there was at least one result
|
36
|
+
columns.concat(@points.first.dimensions.map {|d| d.key})
|
37
|
+
columns.concat(@points.first.metrics.map {|m| m.key})
|
38
|
+
end
|
39
|
+
when :noheader
|
40
|
+
end
|
41
|
+
|
42
|
+
output = CSV.generate_line(columns) + "\n" if (columns.size > 0)
|
43
|
+
|
44
|
+
return output
|
45
|
+
end
|
46
|
+
|
47
|
+
# output important data to CSV, ignoring all the specific data about this dataset
|
48
|
+
# (total_results, start_date) and just output the data from the points
|
49
|
+
|
50
|
+
def to_csv(format = :long)
|
51
|
+
output = ''
|
52
|
+
|
53
|
+
# build the headers
|
54
|
+
output = to_csv_header(format)
|
55
|
+
|
56
|
+
# get the data from each point
|
57
|
+
@points.each do |point|
|
58
|
+
output += point.to_csv(format) + "\n"
|
59
|
+
end
|
60
|
+
|
61
|
+
return output
|
62
|
+
end
|
63
|
+
|
64
|
+
|
65
|
+
def to_yaml
|
66
|
+
{ 'total_results' => @total_results,
|
67
|
+
'start_index' => @start_index,
|
68
|
+
'items_per_page' => @items_per_page,
|
69
|
+
'start_date' => @start_date,
|
70
|
+
'end_date' => @end_date,
|
71
|
+
'points' => @points}.to_yaml
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module GatticaError
|
2
|
+
# usage errors
|
3
|
+
class InvalidFileType < StandardError; end;
|
4
|
+
# user errors
|
5
|
+
class InvalidEmail < StandardError; end;
|
6
|
+
class InvalidPassword < StandardError; end;
|
7
|
+
# authentication errors
|
8
|
+
class CouldNotAuthenticate < StandardError; end;
|
9
|
+
class NoLoginOrToken < StandardError; end;
|
10
|
+
class InvalidToken < StandardError; end;
|
11
|
+
# profile errors
|
12
|
+
class InvalidProfileId < StandardError; end;
|
13
|
+
# search errors
|
14
|
+
class TooManyDimensions < StandardError; end;
|
15
|
+
class TooManyMetrics < StandardError; end;
|
16
|
+
class InvalidSort < StandardError; end;
|
17
|
+
class InvalidFilter < StandardError; end;
|
18
|
+
class MissingStartDate < StandardError; end;
|
19
|
+
class MissingEndDate < StandardError; end;
|
20
|
+
# errors from Analytics
|
21
|
+
class AnalyticsError < StandardError; end;
|
22
|
+
class UnknownAnalyticsError < StandardError; end;
|
23
|
+
end
|
data/lib/gattica/user.rb
ADDED
@@ -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/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
data/test/test_auth.rb
ADDED
data/test/test_engine.rb
ADDED
@@ -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
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/helper'
|
2
|
+
|
3
|
+
class TestUser < Test::Unit::TestCase
|
4
|
+
def test_build_query_string
|
5
|
+
@gattica = Gattica.new(:token => 'ga-token', :profile_id => 'ga-profile_id')
|
6
|
+
expected = "ids=ga:ga-profile_id&start-date=2008-01-02&end-date=2008-01-03&dimensions=ga:pageTitle,ga:pagePath&metrics=ga:pageviews&sort=-ga:pageviews&max-results=3"
|
7
|
+
result = @gattica.send(:build_query_string, {
|
8
|
+
:start_date => Date.civil(2008,1,2),
|
9
|
+
:end_date => Date.civil(2008,1,3),
|
10
|
+
:dimensions => ['pageTitle','pagePath'],
|
11
|
+
:metrics => ['pageviews'],
|
12
|
+
:sort => '-pageviews',
|
13
|
+
'max-results' => '3'}, 'ga-profile_id')
|
14
|
+
assert_equal expected, result
|
15
|
+
end
|
16
|
+
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,93 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: gattica
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.4.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Rob Cameron
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-09-04 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: hpricot
|
17
|
+
type: :runtime
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 0.6.164
|
24
|
+
version:
|
25
|
+
description: Gattica is a Ruby library for extracting data from the Google Analytics API.
|
26
|
+
email: cannikinn@gmail.com
|
27
|
+
executables: []
|
28
|
+
|
29
|
+
extensions: []
|
30
|
+
|
31
|
+
extra_rdoc_files:
|
32
|
+
- LICENSE
|
33
|
+
- README.rdoc
|
34
|
+
files:
|
35
|
+
- .gitignore
|
36
|
+
- History.txt
|
37
|
+
- LICENSE
|
38
|
+
- README.rdoc
|
39
|
+
- Rakefile
|
40
|
+
- VERSION.yml
|
41
|
+
- examples/example.rb
|
42
|
+
- gattica.gemspec
|
43
|
+
- lib/gattica.rb
|
44
|
+
- lib/gattica/account.rb
|
45
|
+
- lib/gattica/auth.rb
|
46
|
+
- lib/gattica/convertible.rb
|
47
|
+
- lib/gattica/core_extensions.rb
|
48
|
+
- lib/gattica/data_point.rb
|
49
|
+
- lib/gattica/data_set.rb
|
50
|
+
- lib/gattica/exceptions.rb
|
51
|
+
- lib/gattica/user.rb
|
52
|
+
- test/helper.rb
|
53
|
+
- test/suite.rb
|
54
|
+
- test/test_auth.rb
|
55
|
+
- test/test_engine.rb
|
56
|
+
- test/test_gattica.rb
|
57
|
+
- test/test_user.rb
|
58
|
+
has_rdoc: true
|
59
|
+
homepage: http://github.com/cannikin/gattica
|
60
|
+
licenses: []
|
61
|
+
|
62
|
+
post_install_message:
|
63
|
+
rdoc_options:
|
64
|
+
- --charset=UTF-8
|
65
|
+
require_paths:
|
66
|
+
- lib
|
67
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
68
|
+
requirements:
|
69
|
+
- - ">="
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
version: "0"
|
72
|
+
version:
|
73
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
74
|
+
requirements:
|
75
|
+
- - ">="
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: "0"
|
78
|
+
version:
|
79
|
+
requirements: []
|
80
|
+
|
81
|
+
rubyforge_project:
|
82
|
+
rubygems_version: 1.3.5
|
83
|
+
signing_key:
|
84
|
+
specification_version: 2
|
85
|
+
summary: Gattica is a Ruby library for extracting data from the Google Analytics API.
|
86
|
+
test_files:
|
87
|
+
- test/helper.rb
|
88
|
+
- test/suite.rb
|
89
|
+
- test/test_auth.rb
|
90
|
+
- test/test_engine.rb
|
91
|
+
- test/test_gattica.rb
|
92
|
+
- test/test_user.rb
|
93
|
+
- examples/example.rb
|