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 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,13 @@
1
+ class String
2
+ def underscored
3
+ self.gsub(/([A-Z])/, '_\1').downcase
4
+ end
5
+
6
+ def lower_camelized
7
+ self.gsub(/(_)(.)/) { $2.upcase }
8
+ end
9
+
10
+ def to_ga
11
+ "ga:#{self}"
12
+ end
13
+ 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
@@ -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