gattica 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.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
|