vigetlabs-garb 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +0 -0
- data/README.md +168 -0
- data/Rakefile +26 -0
- data/garb.gemspec +42 -0
- data/lib/extensions/happymapper.rb +67 -0
- data/lib/extensions/operator.rb +20 -0
- data/lib/extensions/string.rb +13 -0
- data/lib/extensions/symbol.rb +36 -0
- data/lib/garb/authentication_request.rb +46 -0
- data/lib/garb/data_request.rb +28 -0
- data/lib/garb/profile.rb +45 -0
- data/lib/garb/report.rb +211 -0
- data/lib/garb/report_parameter.rb +36 -0
- data/lib/garb/report_response.rb +56 -0
- data/lib/garb/session.rb +19 -0
- data/lib/garb.rb +67 -0
- data/test/authentication_request_test.rb +91 -0
- data/test/data_request_test.rb +52 -0
- data/test/fixtures/profile_feed.xml +33 -0
- data/test/fixtures/report_feed.xml +46 -0
- data/test/garb_test.rb +9 -0
- data/test/operator_test.rb +37 -0
- data/test/profile_test.rb +58 -0
- data/test/report_parameter_test.rb +55 -0
- data/test/report_response_test.rb +24 -0
- data/test/report_test.rb +124 -0
- data/test/session_test.rb +26 -0
- data/test/symbol_test.rb +44 -0
- data/test/test_helper.rb +13 -0
- metadata +99 -0
data/History.txt
ADDED
File without changes
|
data/README.md
ADDED
@@ -0,0 +1,168 @@
|
|
1
|
+
garb
|
2
|
+
====
|
3
|
+
|
4
|
+
by Tony Pitale and Justin Marney
|
5
|
+
|
6
|
+
http://github.com/vigetlabs/garb
|
7
|
+
|
8
|
+
Description
|
9
|
+
-----------
|
10
|
+
|
11
|
+
Provides a Ruby API to the Google Analytics API.
|
12
|
+
|
13
|
+
http://code.google.com/apis/analytics/docs/gdata/gdataDeveloperGuide.html
|
14
|
+
|
15
|
+
Basic Usage
|
16
|
+
===========
|
17
|
+
|
18
|
+
Login
|
19
|
+
-----
|
20
|
+
|
21
|
+
> Garb::Session.login(username, password)
|
22
|
+
|
23
|
+
Profiles
|
24
|
+
--------
|
25
|
+
|
26
|
+
> Garb::Profile.all
|
27
|
+
> profile = Garb::Profile.all.first
|
28
|
+
|
29
|
+
Define a Report Class
|
30
|
+
---------------------
|
31
|
+
|
32
|
+
class ExitsReport < Garb::Report
|
33
|
+
def initialize(profile)
|
34
|
+
super(profile) do |config|
|
35
|
+
config.start_date = Time.now.at_beginning_of_month
|
36
|
+
config.end_date = Time.now.at_end_of_month
|
37
|
+
config.metrics << [:exits, :pageviews, :exit_rate]
|
38
|
+
config.dimensions << :request_uri
|
39
|
+
config.sort << :exits.desc
|
40
|
+
config.max_results = 10
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
Parameters
|
46
|
+
----------
|
47
|
+
|
48
|
+
* start_date: The date of the period you would like this report to start
|
49
|
+
* end_date: The date to end, inclusive
|
50
|
+
* max_results: The maximum number of results to be returned
|
51
|
+
|
52
|
+
Metrics & Dimensions
|
53
|
+
--------------------
|
54
|
+
|
55
|
+
Metrics and Dimensions are very complex because of the ways in which the can and cannot be combined.
|
56
|
+
|
57
|
+
I suggest reading the google documentation to familiarize yourself with this.
|
58
|
+
|
59
|
+
http://code.google.com/apis/analytics/docs/gdata/gdataReferenceDimensionsMetrics.html#bounceRate
|
60
|
+
|
61
|
+
When you've returned, you can pass the appropriate combinations (up to 50 metrics and 2 dimenstions)
|
62
|
+
to garb, as an array, of symbols. Or you can simply push a symbol into the array.
|
63
|
+
|
64
|
+
Sorting
|
65
|
+
-------
|
66
|
+
|
67
|
+
Sorting can be done on any metric or dimension defined in the request, with .desc reversing the sort.
|
68
|
+
|
69
|
+
Building a Report
|
70
|
+
-----------------
|
71
|
+
|
72
|
+
Given the class, session, and profile from above:
|
73
|
+
|
74
|
+
reports = ExitsReport.new(profile).all
|
75
|
+
|
76
|
+
reports will be an array of OpenStructs with methods for the metrics and dimensions returned.
|
77
|
+
|
78
|
+
Build a One-Off Report
|
79
|
+
----------------------
|
80
|
+
|
81
|
+
report = Garb::Report.new(profile)
|
82
|
+
report.metrics << :pageviews
|
83
|
+
report.dimensions << :request_uri
|
84
|
+
|
85
|
+
report.all
|
86
|
+
|
87
|
+
Filtering
|
88
|
+
---------
|
89
|
+
|
90
|
+
Google Analytics supports a significant number of filtering options.
|
91
|
+
|
92
|
+
http://code.google.com/apis/analytics/docs/gdata/gdataReference.html#filtering
|
93
|
+
|
94
|
+
We handle filtering as an array of hashes that you can push into,
|
95
|
+
which will be joined together (AND'd)
|
96
|
+
|
97
|
+
Here is what we can do currently:
|
98
|
+
(the operator is a method on a symbol metric or dimension)
|
99
|
+
|
100
|
+
Operators on metrics:
|
101
|
+
|
102
|
+
:eql => '==',
|
103
|
+
:not_eql => '!=',
|
104
|
+
:gt => '>',
|
105
|
+
:gte => '>=',
|
106
|
+
:lt => '<',
|
107
|
+
:lte => '<='
|
108
|
+
|
109
|
+
Operators on dimensions:
|
110
|
+
|
111
|
+
:matches => '==',
|
112
|
+
:does_not_match => '!=',
|
113
|
+
:contains => '=~',
|
114
|
+
:does_not_contain => '!~',
|
115
|
+
:substring => '=@',
|
116
|
+
:not_substring => '!@'
|
117
|
+
|
118
|
+
Given the previous example one-off report, we can add a line for filter:
|
119
|
+
|
120
|
+
report.filters << {:request_uri.eql => '/extend/effectively-using-git-with-subversion/'}
|
121
|
+
|
122
|
+
TODOS
|
123
|
+
-----
|
124
|
+
|
125
|
+
* Sessions are currently global, which isn't awesome
|
126
|
+
* Single user login is the only supported method currently.
|
127
|
+
Intend to add hooks for using OAuth
|
128
|
+
* Intend to make defined report classes before more like AR
|
129
|
+
* Support start-index
|
130
|
+
* Read opensearch header in results
|
131
|
+
* OR joining filter parameters
|
132
|
+
|
133
|
+
Requirements
|
134
|
+
------------
|
135
|
+
|
136
|
+
libxml
|
137
|
+
happymapper
|
138
|
+
|
139
|
+
Install
|
140
|
+
-------
|
141
|
+
|
142
|
+
sudo gem install vigetlabs-garb -s http://gems.github.com
|
143
|
+
|
144
|
+
License
|
145
|
+
-------
|
146
|
+
|
147
|
+
(The MIT License)
|
148
|
+
|
149
|
+
Copyright (c) 2008 Viget Labs
|
150
|
+
|
151
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
152
|
+
a copy of this software and associated documentation files (the
|
153
|
+
'Software'), to deal in the Software without restriction, including
|
154
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
155
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
156
|
+
permit persons to whom the Software is furnished to do so, subject to
|
157
|
+
the following conditions:
|
158
|
+
|
159
|
+
The above copyright notice and this permission notice shall be
|
160
|
+
included in all copies or substantial portions of the Software.
|
161
|
+
|
162
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
163
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
164
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
165
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
166
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
167
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
168
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# Look in the tasks/setup.rb file for the various options that can be
|
2
|
+
# configured in this Rakefile. The .rake files in the tasks directory
|
3
|
+
# are where the options are used.
|
4
|
+
|
5
|
+
begin
|
6
|
+
require 'bones'
|
7
|
+
Bones.setup
|
8
|
+
rescue LoadError
|
9
|
+
load 'tasks/setup.rb'
|
10
|
+
end
|
11
|
+
|
12
|
+
ensure_in_path 'lib'
|
13
|
+
require 'garb'
|
14
|
+
|
15
|
+
task :default => 'test'
|
16
|
+
|
17
|
+
PROJ.name = 'garb'
|
18
|
+
PROJ.authors = ['Tony Pitale','Justin Marney']
|
19
|
+
PROJ.email = 'tony.pitale@viget.com'
|
20
|
+
PROJ.url = 'http://github.com/vigetlabs/garb'
|
21
|
+
PROJ.version = Garb::VERSION
|
22
|
+
PROJ.rubyforge.name = 'garb'
|
23
|
+
PROJ.test.files = FileList['test/**/*_test.rb']
|
24
|
+
PROJ.spec.opts << '--color'
|
25
|
+
|
26
|
+
# EOF
|
data/garb.gemspec
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = "garb"
|
3
|
+
s.version = "0.1.2"
|
4
|
+
s.date = "2009-04-22"
|
5
|
+
s.summary = "Google Analytics API Ruby Wrapper"
|
6
|
+
s.email = "tony.pitale@viget.com"
|
7
|
+
s.homepage = "http://github.com/vigetlabs/garb"
|
8
|
+
s.description = "A ruby gem to aid in the use of the Google Analytics API"
|
9
|
+
s.has_rdoc = false
|
10
|
+
s.authors = ["Tony Pitale"]
|
11
|
+
s.files = ["History.txt",
|
12
|
+
"README.md",
|
13
|
+
"Rakefile",
|
14
|
+
"garb.gemspec",
|
15
|
+
"lib/garb.rb",
|
16
|
+
"lib/garb/authentication_request.rb",
|
17
|
+
"lib/garb/data_request.rb",
|
18
|
+
"lib/garb/profile.rb",
|
19
|
+
"lib/garb/report.rb",
|
20
|
+
"lib/garb/report_parameter.rb",
|
21
|
+
"lib/garb/report_response.rb",
|
22
|
+
"lib/garb/session.rb",
|
23
|
+
"lib/extensions/symbol.rb",
|
24
|
+
"lib/extensions/string.rb",
|
25
|
+
"lib/extensions/operator.rb",
|
26
|
+
"lib/extensions/happymapper.rb"]
|
27
|
+
s.test_files = ['test/authentication_request_test.rb',
|
28
|
+
'test/data_request_test.rb',
|
29
|
+
'test/garb_test.rb',
|
30
|
+
'test/operator_test.rb',
|
31
|
+
'test/profile_test.rb',
|
32
|
+
'test/report_parameter_test.rb',
|
33
|
+
'test/report_response_test.rb',
|
34
|
+
'test/report_test.rb',
|
35
|
+
'test/session_test.rb',
|
36
|
+
'test/symbol_test.rb',
|
37
|
+
'test/test_helper.rb',
|
38
|
+
'test/fixtures/profile_feed.xml',
|
39
|
+
'test/fixtures/report_feed.xml']
|
40
|
+
s.add_dependency("jnunemaker-happymapper", [">= 0.2.2"])
|
41
|
+
s.add_dependency("libxml-ruby", [">= 0.9.8"])
|
42
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'libxml'
|
2
|
+
|
3
|
+
module HappyMapper
|
4
|
+
|
5
|
+
module ClassMethods
|
6
|
+
include LibXML
|
7
|
+
|
8
|
+
def parse(xml, options = {})
|
9
|
+
# locally scoped copy of namespace for this parse run
|
10
|
+
namespace = @namespace
|
11
|
+
|
12
|
+
if xml.is_a?(XML::Node)
|
13
|
+
node = xml
|
14
|
+
else
|
15
|
+
if xml.is_a?(XML::Document)
|
16
|
+
node = xml.root
|
17
|
+
else
|
18
|
+
node = XML::Parser.string(xml).parse.root
|
19
|
+
end
|
20
|
+
|
21
|
+
root = node.name == tag_name
|
22
|
+
end
|
23
|
+
|
24
|
+
# This is the entry point into the parsing pipeline, so the default
|
25
|
+
# namespace prefix registered here will propagate down
|
26
|
+
namespaces = node.namespaces
|
27
|
+
if namespaces && namespaces.default
|
28
|
+
already_assigned = namespaces.definitions.detect do |defn|
|
29
|
+
namespaces.default && namespaces.default.href == defn.href && defn.prefix
|
30
|
+
end
|
31
|
+
namespaces.default_prefix = DEFAULT_NS unless already_assigned
|
32
|
+
namespace ||= DEFAULT_NS
|
33
|
+
end
|
34
|
+
|
35
|
+
xpath = root ? '/' : './/'
|
36
|
+
xpath += "#{namespace}:" if namespace
|
37
|
+
xpath += tag_name
|
38
|
+
# puts "parse: #{xpath}"
|
39
|
+
|
40
|
+
nodes = node.find(xpath)
|
41
|
+
collection = nodes.collect do |n|
|
42
|
+
obj = new
|
43
|
+
|
44
|
+
attributes.each do |attr|
|
45
|
+
obj.send("#{attr.method_name}=",
|
46
|
+
attr.from_xml_node(n, namespace))
|
47
|
+
end
|
48
|
+
|
49
|
+
elements.each do |elem|
|
50
|
+
obj.send("#{elem.method_name}=",
|
51
|
+
elem.from_xml_node(n, namespace))
|
52
|
+
end
|
53
|
+
|
54
|
+
obj
|
55
|
+
end
|
56
|
+
|
57
|
+
# per http://libxml.rubyforge.org/rdoc/classes/LibXML/XML/Document.html#M000354
|
58
|
+
nodes = nil
|
59
|
+
|
60
|
+
if options[:single] || root
|
61
|
+
collection.first
|
62
|
+
else
|
63
|
+
collection
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# Concept from dm-core
|
2
|
+
class Operator
|
3
|
+
attr_reader :target, :operator, :prefix
|
4
|
+
|
5
|
+
def initialize(target, operator, prefix=false)
|
6
|
+
@target = target.to_ga
|
7
|
+
@operator = operator
|
8
|
+
@prefix = prefix
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_ga
|
12
|
+
@prefix ? "#{operator}#{target}" : "#{target}#{operator}"
|
13
|
+
end
|
14
|
+
|
15
|
+
def ==(rhs)
|
16
|
+
target == rhs.target &&
|
17
|
+
operator == rhs.operator &&
|
18
|
+
prefix == rhs.prefix
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
class Symbol
|
2
|
+
# OPERATORS
|
3
|
+
|
4
|
+
def self.operator(operators)
|
5
|
+
operators.each do |method, operator|
|
6
|
+
class_eval <<-CODE
|
7
|
+
def #{method}
|
8
|
+
Operator.new(self, '#{operator}')
|
9
|
+
end
|
10
|
+
CODE
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# Sorting
|
15
|
+
def desc
|
16
|
+
Operator.new(self, '-', true)
|
17
|
+
end
|
18
|
+
|
19
|
+
operator :eql => '==',
|
20
|
+
:not_eql => '!=',
|
21
|
+
:gt => '>',
|
22
|
+
:gte => '>=',
|
23
|
+
:lt => '<',
|
24
|
+
:lte => '<=',
|
25
|
+
:matches => '==',
|
26
|
+
:does_not_match => '!=',
|
27
|
+
:contains => '=~',
|
28
|
+
:does_not_contain => '!~',
|
29
|
+
:substring => '=@',
|
30
|
+
:not_substring => '!@'
|
31
|
+
|
32
|
+
# Metric filters
|
33
|
+
def to_ga
|
34
|
+
"ga:#{self.to_s.lower_camelized}"
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Garb
|
2
|
+
class AuthenticationRequest
|
3
|
+
class AuthError < StandardError;end
|
4
|
+
|
5
|
+
URL = 'https://www.google.com/accounts/ClientLogin'
|
6
|
+
|
7
|
+
def initialize(email, password)
|
8
|
+
@email = email
|
9
|
+
@password = password
|
10
|
+
end
|
11
|
+
|
12
|
+
def parameters
|
13
|
+
{
|
14
|
+
'Email' => @email,
|
15
|
+
'Passwd' => @password,
|
16
|
+
'accountType' => 'HOSTED_OR_GOOGLE',
|
17
|
+
'service' => 'analytics',
|
18
|
+
'source' => 'vigetLabs-garb-001'
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
def uri
|
23
|
+
URI.parse(URL)
|
24
|
+
end
|
25
|
+
|
26
|
+
def send_request
|
27
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
28
|
+
http.use_ssl = true
|
29
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
30
|
+
http.request(build_request) do |response|
|
31
|
+
raise AuthError unless response.is_a?(Net::HTTPOK)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def build_request
|
36
|
+
post = Net::HTTP::Post.new(uri.path)
|
37
|
+
post.set_form_data(parameters)
|
38
|
+
post
|
39
|
+
end
|
40
|
+
|
41
|
+
def auth_token
|
42
|
+
send_request.body.match(/^Auth=(.*)$/)[1]
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Garb
|
2
|
+
class DataRequest
|
3
|
+
|
4
|
+
def initialize(base_url, parameters={})
|
5
|
+
@base_url = base_url
|
6
|
+
@parameters = parameters
|
7
|
+
end
|
8
|
+
|
9
|
+
def query_string
|
10
|
+
parameter_list = @parameters.map {|k,v| "#{k}=#{v}" }
|
11
|
+
parameter_list.empty? ? '' : "?#{parameter_list.join('&')}"
|
12
|
+
end
|
13
|
+
|
14
|
+
def uri
|
15
|
+
URI.parse(@base_url)
|
16
|
+
end
|
17
|
+
|
18
|
+
def send_request
|
19
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
20
|
+
http.use_ssl = true
|
21
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
22
|
+
response = http.get("#{uri.path}#{query_string}", 'Authorization' => "GoogleLogin auth=#{Session.auth_token}")
|
23
|
+
raise response.body.inspect unless response.is_a?(Net::HTTPOK)
|
24
|
+
response
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
data/lib/garb/profile.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
module Garb
|
2
|
+
class Profile
|
3
|
+
|
4
|
+
attr_reader :table_id, :title, :account_name
|
5
|
+
|
6
|
+
class Property
|
7
|
+
include HappyMapper
|
8
|
+
|
9
|
+
tag 'property'
|
10
|
+
namespace 'dxp'
|
11
|
+
|
12
|
+
attribute :name, String
|
13
|
+
attribute :value, String
|
14
|
+
end
|
15
|
+
|
16
|
+
class Entry
|
17
|
+
include HappyMapper
|
18
|
+
|
19
|
+
tag 'entry'
|
20
|
+
|
21
|
+
element :id, Integer
|
22
|
+
element :title, String
|
23
|
+
element :tableId, String, :namespace => 'dxp'
|
24
|
+
|
25
|
+
# has_one :table_id, TableId
|
26
|
+
has_many :properties, Property
|
27
|
+
end
|
28
|
+
|
29
|
+
def initialize(entry)
|
30
|
+
@title = entry.title
|
31
|
+
@table_id = entry.tableId
|
32
|
+
@account_name = entry.properties.detect{|p| p.name == 'ga:accountName'}.value
|
33
|
+
end
|
34
|
+
|
35
|
+
def id
|
36
|
+
@table_id.sub(/^ga:/, '')
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.all
|
40
|
+
url = "https://www.google.com/analytics/feeds/accounts/#{Session.email}"
|
41
|
+
response = DataRequest.new(url).send_request
|
42
|
+
Entry.parse(response.body).map {|e| Garb::Profile.new(e)}
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|