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/lib/garb/report.rb
ADDED
@@ -0,0 +1,211 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
|
3
|
+
module Garb
|
4
|
+
class Report
|
5
|
+
MONTH = 2592000
|
6
|
+
URL = "https://www.google.com/analytics/feeds/data"
|
7
|
+
|
8
|
+
METRICS = {
|
9
|
+
:visitor => [
|
10
|
+
:avgPageviews,
|
11
|
+
:avgSessionTime,
|
12
|
+
:bounces,
|
13
|
+
:bounceRate,
|
14
|
+
:entrances,
|
15
|
+
:exits,
|
16
|
+
:exitRate,
|
17
|
+
:newVisitors,
|
18
|
+
:pageDuration,
|
19
|
+
:pageviews,
|
20
|
+
:visitDuration,
|
21
|
+
:visitors,
|
22
|
+
:visits
|
23
|
+
],
|
24
|
+
:campaign => [
|
25
|
+
:cost,
|
26
|
+
:clicks,
|
27
|
+
:clickThroughRate,
|
28
|
+
:costPerConversion,
|
29
|
+
:costPerGoalConversion,
|
30
|
+
:costPerMilleImpressions,
|
31
|
+
:costPerTransaction,
|
32
|
+
:impressions
|
33
|
+
],
|
34
|
+
:content => [
|
35
|
+
:uniquePageviews
|
36
|
+
],
|
37
|
+
:ecommerce => [
|
38
|
+
:productPurchases,
|
39
|
+
:productRevenue,
|
40
|
+
:products,
|
41
|
+
:revenue,
|
42
|
+
:revenuePerClick,
|
43
|
+
:revenuePerTransaction,
|
44
|
+
:revenuePerVisit,
|
45
|
+
:shipping,
|
46
|
+
:tax,
|
47
|
+
:transactions
|
48
|
+
],
|
49
|
+
:internal_search => [
|
50
|
+
:searchDepth,
|
51
|
+
:searchDuration,
|
52
|
+
:searchExits,
|
53
|
+
:searchTransitions,
|
54
|
+
:uniqueInternalSearches,
|
55
|
+
:visitsWithSearches
|
56
|
+
],
|
57
|
+
:goals => [
|
58
|
+
:goalCompletions1,
|
59
|
+
:goalCompletions2,
|
60
|
+
:goalCompletions3,
|
61
|
+
:goalCompletions4,
|
62
|
+
:goalCompletionsAll,
|
63
|
+
:goalConversionRate,
|
64
|
+
:goalStarts1,
|
65
|
+
:goalStarts2,
|
66
|
+
:goalStarts3,
|
67
|
+
:goalStarts4,
|
68
|
+
:goalStartsAll,
|
69
|
+
:goalValue1,
|
70
|
+
:goalValue2,
|
71
|
+
:goalValue3,
|
72
|
+
:goalValue4,
|
73
|
+
:goalValueAll,
|
74
|
+
:goalValuePerVisit
|
75
|
+
]
|
76
|
+
}
|
77
|
+
|
78
|
+
DIMENSIONS = {
|
79
|
+
:visitor => [
|
80
|
+
:browser,
|
81
|
+
:browserVersion,
|
82
|
+
:city,
|
83
|
+
:connectionSpeed,
|
84
|
+
:continent,
|
85
|
+
:country,
|
86
|
+
:daysSinceLastVisit,
|
87
|
+
:domain,
|
88
|
+
:flashVersion,
|
89
|
+
:hostname,
|
90
|
+
:hour,
|
91
|
+
:javaEnabled,
|
92
|
+
:languqage,
|
93
|
+
:medium,
|
94
|
+
:organization,
|
95
|
+
:pageDepth,
|
96
|
+
:platform,
|
97
|
+
:platformVersion,
|
98
|
+
:referralPath,
|
99
|
+
:region,
|
100
|
+
:screenColors,
|
101
|
+
:screenResolution,
|
102
|
+
:subContinentRegion,
|
103
|
+
:userDefinedValue,
|
104
|
+
:visitNumber,
|
105
|
+
:visitorType
|
106
|
+
],
|
107
|
+
:campaign => [
|
108
|
+
:adGroup,
|
109
|
+
:adSlot,
|
110
|
+
:adSlotPosition,
|
111
|
+
:campaign,
|
112
|
+
:content,
|
113
|
+
:keyword,
|
114
|
+
:source,
|
115
|
+
:sourceMedium
|
116
|
+
],
|
117
|
+
:content => [
|
118
|
+
:pageTitle,
|
119
|
+
:requestUri,
|
120
|
+
:requestUri1,
|
121
|
+
:requestUriLast
|
122
|
+
],
|
123
|
+
:ecommerce => [
|
124
|
+
:affiliation,
|
125
|
+
:daysToTransaction,
|
126
|
+
:productCode,
|
127
|
+
:productName,
|
128
|
+
:productVariation,
|
129
|
+
:transactionId,
|
130
|
+
:visitsToTransaction
|
131
|
+
],
|
132
|
+
:internal_search => [
|
133
|
+
:hasInternalSearch,
|
134
|
+
:internalSearchKeyword,
|
135
|
+
:internalSearchNext,
|
136
|
+
:internalSearchType
|
137
|
+
]
|
138
|
+
}
|
139
|
+
|
140
|
+
attr_accessor :metrics, :dimensions, :sort, :filters,
|
141
|
+
:start_date, :max_results, :end_date
|
142
|
+
|
143
|
+
attr_reader :profile
|
144
|
+
|
145
|
+
# def self.element_id(property_name)
|
146
|
+
# property_name.is_a?(Operator) ? property_name.to_s : property_name.to_ga.lower_camelized
|
147
|
+
# end
|
148
|
+
#
|
149
|
+
# def self.property_value(entry, property_name)
|
150
|
+
# (entry/property_name).first.inner_text
|
151
|
+
# end
|
152
|
+
#
|
153
|
+
# def self.property_values(entry, property_names)
|
154
|
+
# hash = property_names.inject({}) do |hash, property_name|
|
155
|
+
# hash.merge({property_name => property_value(entry, property_name.to_s.lower_camelized)})
|
156
|
+
# end
|
157
|
+
# OpenStruct.new hash
|
158
|
+
# end
|
159
|
+
#
|
160
|
+
def self.format_time(t)
|
161
|
+
t.strftime('%Y-%m-%d')
|
162
|
+
end
|
163
|
+
|
164
|
+
def initialize(profile, opts={})
|
165
|
+
@profile = profile
|
166
|
+
|
167
|
+
@sort = ReportParameter.new(:sort) << opts.fetch(:sort, [])
|
168
|
+
@filters = ReportParameter.new(:filters) << opts.fetch(:filters, [])
|
169
|
+
@metrics = ReportParameter.new(:metrics) << opts.fetch(:metrics, [])
|
170
|
+
@dimensions = ReportParameter.new(:dimensions) << opts.fetch(:dimensions, [])
|
171
|
+
|
172
|
+
@start_date = opts.fetch(:start_date, Time.now - MONTH)
|
173
|
+
@end_date = opts.fetch(:end_date, Time.now)
|
174
|
+
|
175
|
+
yield self if block_given?
|
176
|
+
end
|
177
|
+
|
178
|
+
def page_params
|
179
|
+
max_results.nil? ? {} : {'max-results' => max_results}
|
180
|
+
end
|
181
|
+
|
182
|
+
def default_params
|
183
|
+
{'ids' => profile.table_id,
|
184
|
+
'start-date' => self.class.format_time(start_date),
|
185
|
+
'end-date' => self.class.format_time(end_date)}
|
186
|
+
end
|
187
|
+
|
188
|
+
def params
|
189
|
+
[
|
190
|
+
metrics.to_params,
|
191
|
+
dimensions.to_params,
|
192
|
+
sort.to_params,
|
193
|
+
filters.to_params,
|
194
|
+
page_params
|
195
|
+
].inject(default_params) do |p, i|
|
196
|
+
p.merge(i)
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
def send_request_for_body
|
201
|
+
request = DataRequest.new(URL, params)
|
202
|
+
response = request.send_request
|
203
|
+
response.body
|
204
|
+
end
|
205
|
+
|
206
|
+
def all
|
207
|
+
@entries = ReportResponse.new(send_request_for_body).parse
|
208
|
+
end
|
209
|
+
|
210
|
+
end
|
211
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Garb
|
2
|
+
class ReportParameter
|
3
|
+
|
4
|
+
attr_reader :elements
|
5
|
+
|
6
|
+
def initialize(name)
|
7
|
+
@name = name
|
8
|
+
@elements = []
|
9
|
+
end
|
10
|
+
|
11
|
+
def name
|
12
|
+
@name.to_s
|
13
|
+
end
|
14
|
+
|
15
|
+
def <<(element)
|
16
|
+
@elements += [element].flatten
|
17
|
+
self
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_params
|
21
|
+
params = self.elements.map do |elem|
|
22
|
+
case elem
|
23
|
+
when Hash
|
24
|
+
elem.collect do |k,v|
|
25
|
+
next unless k.is_a?(Operator)
|
26
|
+
"#{k.target}#{CGI::escape(k.operator.to_s)}#{CGI::escape(v.to_s)}"
|
27
|
+
end.join(';')
|
28
|
+
else
|
29
|
+
elem.to_ga
|
30
|
+
end
|
31
|
+
end.join(',')
|
32
|
+
|
33
|
+
params.empty? ? {} : {self.name => params}
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Garb
|
2
|
+
class ReportResponse
|
3
|
+
def initialize(response_body)
|
4
|
+
@xml = response_body
|
5
|
+
end
|
6
|
+
|
7
|
+
def parse
|
8
|
+
entries = Entry.parse(@xml)
|
9
|
+
|
10
|
+
entries.collect do |entry|
|
11
|
+
hash = {}
|
12
|
+
|
13
|
+
entry.metrics.each do |m|
|
14
|
+
name = m.name.sub(/^ga\:/,'').underscored
|
15
|
+
hash.merge!({name => m.value})
|
16
|
+
end
|
17
|
+
|
18
|
+
entry.dimensions.each do |d|
|
19
|
+
name = d.name.sub(/^ga\:/,'').underscored
|
20
|
+
hash.merge!({name => d.value})
|
21
|
+
end
|
22
|
+
|
23
|
+
OpenStruct.new(hash)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class Metric
|
28
|
+
include HappyMapper
|
29
|
+
|
30
|
+
tag 'metric'
|
31
|
+
namespace 'dxp'
|
32
|
+
|
33
|
+
attribute :name, String
|
34
|
+
attribute :value, String
|
35
|
+
end
|
36
|
+
|
37
|
+
class Dimension
|
38
|
+
include HappyMapper
|
39
|
+
|
40
|
+
tag 'dimension'
|
41
|
+
namespace 'dxp'
|
42
|
+
|
43
|
+
attribute :name, String
|
44
|
+
attribute :value, String
|
45
|
+
end
|
46
|
+
|
47
|
+
class Entry
|
48
|
+
include HappyMapper
|
49
|
+
|
50
|
+
tag 'entry'
|
51
|
+
|
52
|
+
has_many :metrics, Metric
|
53
|
+
has_many :dimensions, Dimension
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
data/lib/garb/session.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
module Garb
|
2
|
+
class Session
|
3
|
+
|
4
|
+
def self.login(email, password)
|
5
|
+
@email = email
|
6
|
+
auth_request = AuthenticationRequest.new(email, password)
|
7
|
+
@auth_token = auth_request.auth_token
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.auth_token
|
11
|
+
@auth_token
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.email
|
15
|
+
@email
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
data/lib/garb.rb
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
$:.unshift File.expand_path(File.dirname(__FILE__))
|
2
|
+
|
3
|
+
require 'net/http'
|
4
|
+
require 'net/https'
|
5
|
+
require 'rubygems'
|
6
|
+
require 'cgi'
|
7
|
+
require 'happymapper'
|
8
|
+
|
9
|
+
require 'garb/authentication_request'
|
10
|
+
require 'garb/data_request'
|
11
|
+
require 'garb/session'
|
12
|
+
require 'garb/profile'
|
13
|
+
require 'garb/report'
|
14
|
+
require 'garb/report_parameter'
|
15
|
+
require 'garb/report_response'
|
16
|
+
|
17
|
+
require 'extensions/string'
|
18
|
+
require 'extensions/operator'
|
19
|
+
require 'extensions/symbol'
|
20
|
+
require 'extensions/happymapper'
|
21
|
+
|
22
|
+
module Garb
|
23
|
+
# :stopdoc:
|
24
|
+
GA = "http://schemas.google.com/analytics/2008"
|
25
|
+
|
26
|
+
VERSION = '0.1.2'
|
27
|
+
LIBPATH = ::File.expand_path(::File.dirname(__FILE__)) + ::File::SEPARATOR
|
28
|
+
PATH = ::File.dirname(LIBPATH) + ::File::SEPARATOR
|
29
|
+
# :startdoc:
|
30
|
+
|
31
|
+
# Returns the version string for the library.
|
32
|
+
#
|
33
|
+
def self.version
|
34
|
+
VERSION
|
35
|
+
end
|
36
|
+
|
37
|
+
# Returns the library path for the module. If any arguments are given,
|
38
|
+
# they will be joined to the end of the libray path using
|
39
|
+
# <tt>File.join</tt>.
|
40
|
+
#
|
41
|
+
def self.libpath( *args )
|
42
|
+
args.empty? ? LIBPATH : ::File.join(LIBPATH, args.flatten)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Returns the lpath for the module. If any arguments are given,
|
46
|
+
# they will be joined to the end of the path using
|
47
|
+
# <tt>File.join</tt>.
|
48
|
+
#
|
49
|
+
def self.path( *args )
|
50
|
+
args.empty? ? PATH : ::File.join(PATH, args.flatten)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Utility method used to rquire all files ending in .rb that lie in the
|
54
|
+
# directory below this file that has the same name as the filename passed
|
55
|
+
# in. Optionally, a specific _directory_ name can be passed in such that
|
56
|
+
# the _filename_ does not have to be equivalent to the directory.
|
57
|
+
#
|
58
|
+
def self.require_all_libs_relative_to( fname, dir = nil )
|
59
|
+
dir ||= ::File.basename(fname, '.*')
|
60
|
+
search_me = ::File.expand_path(
|
61
|
+
::File.join(::File.dirname(fname), dir, '*', '*.rb'))
|
62
|
+
|
63
|
+
Dir.glob(search_me).sort.each {|rb| require rb}
|
64
|
+
end
|
65
|
+
end # module Garb
|
66
|
+
|
67
|
+
# EOF
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/test_helper'
|
2
|
+
|
3
|
+
module Garb
|
4
|
+
class AuthenticationRequestTest < Test::Unit::TestCase
|
5
|
+
|
6
|
+
context "An instance of the AuthenticationRequest class" do
|
7
|
+
|
8
|
+
setup { @request = AuthenticationRequest.new('email', 'password') }
|
9
|
+
|
10
|
+
should "have a collection of parameters that include the email and password" do
|
11
|
+
expected =
|
12
|
+
{
|
13
|
+
'Email' => 'user@example.com',
|
14
|
+
'Passwd' => 'fuzzybunnies',
|
15
|
+
'accountType' => 'HOSTED_OR_GOOGLE',
|
16
|
+
'service' => 'analytics',
|
17
|
+
'source' => 'vigetLabs-garb-001'
|
18
|
+
}
|
19
|
+
|
20
|
+
request = AuthenticationRequest.new('user@example.com', 'fuzzybunnies')
|
21
|
+
assert_equal expected, request.parameters
|
22
|
+
end
|
23
|
+
|
24
|
+
should "have a URI" do
|
25
|
+
assert_equal URI.parse('https://www.google.com/accounts/ClientLogin'), @request.uri
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
should "be able to send a request to the GAAPI service" do
|
30
|
+
@request.expects(:build_request).returns('post')
|
31
|
+
response = mock {|m| m.expects(:is_a?).with(Net::HTTPOK).returns(true) }
|
32
|
+
|
33
|
+
http = mock do |m|
|
34
|
+
m.expects(:use_ssl=).with(true)
|
35
|
+
m.expects(:verify_mode=).with(OpenSSL::SSL::VERIFY_NONE)
|
36
|
+
m.expects(:request).with('post').yields(response)
|
37
|
+
end
|
38
|
+
|
39
|
+
Net::HTTP.expects(:new).with('www.google.com', 443).returns(http)
|
40
|
+
|
41
|
+
@request.send_request
|
42
|
+
end
|
43
|
+
|
44
|
+
should "be able to build a request for the GAAPI service" do
|
45
|
+
params = "param"
|
46
|
+
@request.expects(:parameters).with().returns(params)
|
47
|
+
|
48
|
+
post = mock
|
49
|
+
post.expects(:set_form_data).with(params)
|
50
|
+
|
51
|
+
Net::HTTP::Post.expects(:new).with('/accounts/ClientLogin').returns(post)
|
52
|
+
|
53
|
+
@request.build_request
|
54
|
+
end
|
55
|
+
|
56
|
+
should "be able to retrieve an auth_token from the body" do
|
57
|
+
response_data =
|
58
|
+
"SID=mysid\n" +
|
59
|
+
"LSID=mylsid\n" +
|
60
|
+
"Auth=auth_token\n"
|
61
|
+
|
62
|
+
@request.expects(:send_request).with().returns(stub(:body => response_data))
|
63
|
+
|
64
|
+
assert_equal 'auth_token', @request.auth_token
|
65
|
+
end
|
66
|
+
|
67
|
+
should "raise an exception when requesting an auth_token when the authorization fails" do
|
68
|
+
@request.stubs(:build_request)
|
69
|
+
response = mock do |m|
|
70
|
+
m.expects(:is_a?).with(Net::HTTPOK).returns(false)
|
71
|
+
end
|
72
|
+
|
73
|
+
http = stub do |s|
|
74
|
+
s.stubs(:use_ssl=)
|
75
|
+
s.stubs(:verify_mode=)
|
76
|
+
s.stubs(:request).yields(response)
|
77
|
+
end
|
78
|
+
|
79
|
+
Net::HTTP.stubs(:new).with('www.google.com', 443).returns(http)
|
80
|
+
|
81
|
+
assert_raise(Garb::AuthenticationRequest::AuthError) do
|
82
|
+
@request.send_request
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
|
88
|
+
|
89
|
+
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'test_helper')
|
2
|
+
|
3
|
+
module Garb
|
4
|
+
class DataRequestTest < Test::Unit::TestCase
|
5
|
+
|
6
|
+
context "An instance of the DataRequest class" do
|
7
|
+
|
8
|
+
should "be able to build the query string from parameters" do
|
9
|
+
parameters = {'ids' => '12345', 'metrics' => 'country'}
|
10
|
+
data_request = DataRequest.new("", parameters)
|
11
|
+
|
12
|
+
query_string = data_request.query_string
|
13
|
+
|
14
|
+
assert_match(/^\?/, query_string)
|
15
|
+
|
16
|
+
query_string.sub!(/^\?/, '')
|
17
|
+
|
18
|
+
assert_equal ["ids=12345", "metrics=country"], query_string.split('&').sort
|
19
|
+
end
|
20
|
+
|
21
|
+
should "return an empty query string if parameters are empty" do
|
22
|
+
data_request = DataRequest.new("")
|
23
|
+
assert_equal "", data_request.query_string
|
24
|
+
end
|
25
|
+
|
26
|
+
should "be able to build a uri" do
|
27
|
+
url = 'http://example.com'
|
28
|
+
expected = URI.parse('http://example.com')
|
29
|
+
|
30
|
+
assert_equal expected, DataRequest.new(url).uri
|
31
|
+
end
|
32
|
+
|
33
|
+
should "be able to make a request to the GAAPI" do
|
34
|
+
Session.expects(:auth_token).with().returns('toke')
|
35
|
+
response = mock
|
36
|
+
response.expects(:is_a?).with(Net::HTTPOK).returns(true)
|
37
|
+
|
38
|
+
http = mock do |m|
|
39
|
+
m.expects(:use_ssl=).with(true)
|
40
|
+
m.expects(:verify_mode=).with(OpenSSL::SSL::VERIFY_NONE)
|
41
|
+
m.expects(:get).with('/data?key=value', 'Authorization' => 'GoogleLogin auth=toke').returns(response)
|
42
|
+
end
|
43
|
+
|
44
|
+
Net::HTTP.expects(:new).with('example.com', 443).returns(http)
|
45
|
+
|
46
|
+
data_request = DataRequest.new('https://example.com/data', 'key' => 'value')
|
47
|
+
assert_equal response, data_request.send_request
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
<?xml version='1.0' encoding='UTF-8'?>
|
2
|
+
<feed xmlns='http://www.w3.org/2005/Atom' xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/' xmlns:dxp='http://schemas.google.com/analytics/2009'>
|
3
|
+
<id>http://www.google.com/analytics/feeds/accounts/email@example.com</id>
|
4
|
+
<updated>2009-03-27T08:14:28.000-07:00</updated>
|
5
|
+
<title type='text'>Profile list for email@example.com</title>
|
6
|
+
<link rel='self' type='application/atom+xml' href='http://www.google.com/analytics/feeds/accounts/email@example.com'/>
|
7
|
+
<author><name>Google Analytics</name></author>
|
8
|
+
<generator version='1.0'>Google Analytics</generator>
|
9
|
+
<openSearch:totalResults>2</openSearch:totalResults>
|
10
|
+
<openSearch:startIndex>1</openSearch:startIndex>
|
11
|
+
<openSearch:itemsPerPage>2</openSearch:itemsPerPage>
|
12
|
+
<entry>
|
13
|
+
<id>http://www.google.com/analytics/feeds/accounts/ga:12345</id>
|
14
|
+
<updated>2008-07-21T14:05:57.000-07:00</updated>
|
15
|
+
<title type='text'>Historical</title>
|
16
|
+
<dxp:tableId>ga:12345</dxp:tableId>
|
17
|
+
<dxp:property name='ga:accountId' value='1111'/>
|
18
|
+
<dxp:property name='ga:accountName' value='Blog Beta'/>
|
19
|
+
<dxp:property name='ga:profileId' value='1212'/>
|
20
|
+
<dxp:property name='ga:webPropertyId' value='UA-1111-1'/>
|
21
|
+
</entry>
|
22
|
+
<entry>
|
23
|
+
<id>http://www.google.com/analytics/feeds/accounts/ga:12346</id>
|
24
|
+
<updated>2008-11-24T11:51:07.000-08:00</updated>
|
25
|
+
<title type='text'>Presently</title>
|
26
|
+
<dxp:tableId>ga:12346</dxp:tableId>
|
27
|
+
<dxp:property name='ga:accountId' value='1111'/>
|
28
|
+
<dxp:property name='ga:accountName' value='Blog Beta'/>
|
29
|
+
<dxp:property name='ga:profileId' value='1213'/>
|
30
|
+
<dxp:property name='ga:webPropertyId' value='UA-1111-2'/>
|
31
|
+
</entry>
|
32
|
+
</feed>
|
33
|
+
|
@@ -0,0 +1,46 @@
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
2
|
+
<feed xmlns='http://www.w3.org/2005/Atom'
|
3
|
+
xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/'
|
4
|
+
xmlns:dxp='http://schemas.google.com/dataexport/2009'
|
5
|
+
xmlns:ga='http://schemas.google.com/analytics/2008'>
|
6
|
+
<id>http://www.google.com/analytics/feeds/data?ids=ga:983247&dimensions=ga:country,ga:city&metrics=ga:pageViews&start-date=2008-01-01&end-date=2008-01-02</id>
|
7
|
+
<updated>2008-01-02T15:59:59.999-08:00 </updated>
|
8
|
+
<title type="text">Google Analytics Data for Profile 983247</title>
|
9
|
+
<link href="http://www.google.com/analytics/feeds/data" rel="http://schemas.google.com/g/2005#feed" type="application/atom+xml"/>
|
10
|
+
<link href="http://www.google.com/analytics/feeds/data?end-date=2008-01-02&start-date=2008-01-01&metrics=ga%3ApageViews&ids=ga%3A983247&dimensions=ga%3Acountry%2Cga%3Acity" rel="self" type="application/atom+xml"/>
|
11
|
+
<link href="http://www.google.com/analytics/feeds/data?start-index=1001&max-results=1000&end-date=2008-01-02&start-date=2008-01-01&metrics=ga%3ApageViews&ids=ga%3A983247&dimensions=ga%3Acountry%2Cga%3Acity" rel="next" type="application/atom+xml"/>
|
12
|
+
<author>
|
13
|
+
<name>Google Analytics</name>
|
14
|
+
</author>
|
15
|
+
<openSearch:startIndex>3</openSearch:startIndex>
|
16
|
+
<openSearch:itemsPerPage>4</openSearch:itemsPerPage>
|
17
|
+
<ga:webPropertyID>UA-983247-67</ga:webPropertyID>
|
18
|
+
<ga:start-date>2008-01-01</ga:start-date>
|
19
|
+
<ga:end-date>2008-01-02</ga:end-date>
|
20
|
+
|
21
|
+
<entry>
|
22
|
+
<id> http://www.google.com/analytics/feeds/data?ids=ga:1174&ga:country=%28not%20set%29&ga:city=%28not%20set%29&start-date=2008-01-01&end-date=2008-01-02 </id>
|
23
|
+
<updated> 2008-01-01T16:00:00.001-08:00 </updated>
|
24
|
+
<title type="text"> ga:country=(not set) | ga:city=(not set) </title>
|
25
|
+
<link href="http://www.google.com/analytics/feeds/data" rel="self" type="application/atom+xml"/>
|
26
|
+
<dxp:dimension name="ga:country" value="(not set)" />
|
27
|
+
<dxp:dimension name="ga:city" value="(not set)" />
|
28
|
+
<dxp:metric name="ga:pageviews" value="33" />
|
29
|
+
</entry>
|
30
|
+
<entry>
|
31
|
+
<id> http://www.google.com/analytics/feeds/data?ids=ga:1174&ga:country=Afghanistan&ga:city=Kabul&start-date=2008-01-01&end-date=2008-01-02 </id>
|
32
|
+
<updated> 2008-01-01T16:00:00.001-08:00 </updated>
|
33
|
+
<title type="text"> ga:country=Afghanistan | ga:city=Kabul </title>
|
34
|
+
<dxp:dimension name="ga:country" value="Afghanistan" />
|
35
|
+
<dxp:dimension name="ga:city" value="Kabul" />
|
36
|
+
<dxp:metric name="ga:pageviews" value="2" />
|
37
|
+
</entry>
|
38
|
+
<entry>
|
39
|
+
<id> http://www.google.com/analytics/feeds/data?ids=ga:1174&ga:country=Albania&ga:city=Tirana&start-date=2008-01-01&end-date=2008-01-02 </id>
|
40
|
+
<updated> 2008-01-01T16:00:00.001-08:00 </updated>
|
41
|
+
<title type="text"> ga:country=Albania | ga:city=Tirana </title>
|
42
|
+
<dxp:dimension name="ga:country" value="Albania" />
|
43
|
+
<dxp:dimension name="ga:city" value="Tirana" />
|
44
|
+
<dxp:metric name="ga:pageviews" value="1" />
|
45
|
+
</entry>
|
46
|
+
</feed>
|
data/test/garb_test.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'test_helper')
|
2
|
+
|
3
|
+
class OperatorTest < Test::Unit::TestCase
|
4
|
+
context "An instance of an Operator" do
|
5
|
+
should "lower camelize the target" do
|
6
|
+
assert_equal "ga:uniqueVisits=", Operator.new(:unique_visits, "=").to_ga
|
7
|
+
end
|
8
|
+
|
9
|
+
should "return target and operator together" do
|
10
|
+
assert_equal "ga:metric=", Operator.new(:metric, "=").to_ga
|
11
|
+
end
|
12
|
+
|
13
|
+
should "prefix the operator to the target" do
|
14
|
+
assert_equal "-ga:metric", Operator.new(:metric, "-", true).to_ga
|
15
|
+
end
|
16
|
+
|
17
|
+
should "know if it is equal to another operator" do
|
18
|
+
op1 = Operator.new(:hello, "==")
|
19
|
+
op2 = Operator.new(:hello, "==")
|
20
|
+
assert_equal op1, op2
|
21
|
+
end
|
22
|
+
|
23
|
+
should "not be equal to another operator if target, operator, or prefix is different" do
|
24
|
+
op1 = Operator.new(:hello, "==")
|
25
|
+
op2 = Operator.new(:hello, "==", true)
|
26
|
+
assert_not_equal op1, op2
|
27
|
+
|
28
|
+
op1 = Operator.new(:hello1, "==")
|
29
|
+
op2 = Operator.new(:hello2, "==")
|
30
|
+
assert_not_equal op1, op2
|
31
|
+
|
32
|
+
op1 = Operator.new(:hello, "!=")
|
33
|
+
op2 = Operator.new(:hello, "==")
|
34
|
+
assert_not_equal op1, op2
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|