├── .gitignore
├── lib
├── garb
│ ├── version.rb
│ ├── session.rb
│ ├── account.rb
│ ├── oauth_session.rb
│ ├── report_parameter.rb
│ ├── data_request.rb
│ ├── report.rb
│ ├── profile.rb
│ ├── report_response.rb
│ ├── authentication_request.rb
│ └── resource.rb
├── extensions
│ ├── array.rb
│ ├── string.rb
│ ├── operator.rb
│ └── symbol.rb
└── garb.rb
├── test
├── unit
│ ├── garb_test.rb
│ ├── oauth_session_test.rb
│ ├── string_test.rb
│ ├── resource_test.rb
│ ├── session_test.rb
│ ├── report_response_test.rb
│ ├── operator_test.rb
│ ├── account_test.rb
│ ├── symbol_test.rb
│ ├── profile_test.rb
│ ├── report_parameter_test.rb
│ ├── data_request_test.rb
│ ├── report_test.rb
│ └── authentication_request_test.rb
├── test_helper.rb
└── fixtures
│ ├── profile_feed.xml
│ ├── report_feed.xml
│ └── cacert.pem
├── TODO.txt
├── Rakefile
├── garb.gemspec
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | /pkg/
2 | demo.rb
3 | .DS_Store
4 | output.xml
5 | coverage
--------------------------------------------------------------------------------
/lib/garb/version.rb:
--------------------------------------------------------------------------------
1 | module Garb
2 | module Version
3 |
4 | MAJOR = 0
5 | MINOR = 2
6 | TINY = 6
7 |
8 | def self.to_s # :nodoc:
9 | [MAJOR, MINOR, TINY].join('.')
10 | end
11 |
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/test/unit/garb_test.rb:
--------------------------------------------------------------------------------
1 | require File.join(File.dirname(__FILE__), '..', '/test_helper')
2 |
3 | class GarbTest < Test::Unit::TestCase
4 | context "A green egg" do
5 | should "be served with ham" do
6 | assert true
7 | end
8 | end
9 | end
--------------------------------------------------------------------------------
/test/unit/oauth_session_test.rb:
--------------------------------------------------------------------------------
1 | require File.join(File.dirname(__FILE__), '..', '/test_helper')
2 |
3 | module Garb
4 | class OAuthSessionTest < Test::Unit::TestCase
5 | context "An instance of OAuthSession" do
6 | should "have tests" do
7 | assert true
8 | end
9 | end
10 | end
11 | end
--------------------------------------------------------------------------------
/lib/extensions/array.rb:
--------------------------------------------------------------------------------
1 | class Array
2 | def group_to_array
3 | h = Hash.new
4 |
5 | each do |element|
6 | key = yield(element)
7 | if h.has_key?(key)
8 | h[key] << element
9 | else
10 | h[key] = [element]
11 | end
12 | end
13 |
14 | h.map{|k,v| v}
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/lib/garb/session.rb:
--------------------------------------------------------------------------------
1 | module Garb
2 | class Session
3 |
4 | # this time, just returns a token that developpers should keep warm
5 | def self.login(email, password, opts={})
6 | auth_request = AuthenticationRequest.new(email, password)
7 | auth_request.auth_token(opts)
8 | end
9 |
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/lib/extensions/string.rb:
--------------------------------------------------------------------------------
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 |
14 | def from_ga
15 | self.gsub(/^ga\:/, '')
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/TODO.txt:
--------------------------------------------------------------------------------
1 | TODO
2 | ====
3 |
4 | * Sessions are currently global, which isn't awesome
5 | * Single user login is the only supported method currently. Intend to add hooks for using OAuth
6 | * Intend to make defined report classes behave more like AR
7 | * Read opensearch header in results
8 | * OR joining filter parameters
9 | * DONE: Support start-index
--------------------------------------------------------------------------------
/test/test_helper.rb:
--------------------------------------------------------------------------------
1 | $:.reject! { |e| e.include? 'TextMate' }
2 |
3 | require 'rubygems'
4 | require 'test/unit'
5 | require 'shoulda'
6 | require 'mocha'
7 |
8 | require File.dirname(__FILE__) + '/../lib/garb'
9 |
10 | class Test::Unit::TestCase
11 |
12 | def read_fixture(filename)
13 | File.read(File.dirname(__FILE__) + "/fixtures/#{filename}")
14 | end
15 |
16 | end
--------------------------------------------------------------------------------
/test/unit/string_test.rb:
--------------------------------------------------------------------------------
1 | require File.join(File.dirname(__FILE__), '..', '/test_helper')
2 |
3 | class StringTest < Test::Unit::TestCase
4 | context "An instance of a String" do
5 | should 'prefix a string with ga: for GA' do
6 | assert_equal 'ga:bob', 'bob'.to_ga
7 | end
8 |
9 | should 'remove ga: prefix' do
10 | assert_equal 'bob', 'ga:bob'.from_ga
11 | end
12 | end
13 | end
--------------------------------------------------------------------------------
/lib/garb/account.rb:
--------------------------------------------------------------------------------
1 | module Garb
2 | class Account
3 | attr_reader :id, :name, :profiles
4 |
5 | def initialize(profiles)
6 | @id = profiles.first.account_id
7 | @name = profiles.first.account_name
8 | @profiles = profiles
9 | end
10 |
11 | def self.all(auth_token)
12 | Profile.all(auth_token).group_to_array{|p| p.account_id}.map{|profiles| new(profiles)}
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/lib/extensions/operator.rb:
--------------------------------------------------------------------------------
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
21 |
--------------------------------------------------------------------------------
/lib/garb/oauth_session.rb:
--------------------------------------------------------------------------------
1 | module Garb
2 | class OAuthSession
3 | attr_accessor :access_token
4 |
5 | OAuthGetRequestToken = "https://www.google.com/accounts/OAuthGetRequestToken"
6 | OAuthAuthorizeToken = "https://www.google.com/accounts/OAuthAuthorizeToken"
7 | OAuthGetAccessToken = "https://www.google.com/accounts/OAuthGetAccessToken"
8 |
9 | def get_request_token
10 |
11 | end
12 |
13 | def authorization_request
14 |
15 | end
16 |
17 | def get_access_token
18 |
19 | end
20 | end
21 | end
--------------------------------------------------------------------------------
/test/unit/resource_test.rb:
--------------------------------------------------------------------------------
1 | require File.join(File.dirname(__FILE__), '..', '/test_helper')
2 |
3 | class TestReport
4 | include Garb::Resource
5 | end
6 |
7 | # Most of the resource testing is done as a part of ReportTest
8 | class ResourceTest < Test::Unit::TestCase
9 |
10 | context "A class with Garb::Resource mixed in" do
11 | should "get results from GA" do
12 | profile = stub
13 | TestReport.expects(:send_request_for_body).returns('xml')
14 | Garb::ReportResponse.expects(:new).with('xml').returns(mock(:results => 'analytics'))
15 |
16 | assert_equal 'analytics', TestReport.results('token', profile)
17 | end
18 | end
19 | end
--------------------------------------------------------------------------------
/lib/garb/report_parameter.rb:
--------------------------------------------------------------------------------
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).compact!
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}#{URI.encode(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
--------------------------------------------------------------------------------
/lib/garb/data_request.rb:
--------------------------------------------------------------------------------
1 | module Garb
2 | class DataRequest
3 |
4 | def initialize(auth_token, base_url, parameters={})
5 | @auth_token = auth_token
6 | @base_url = base_url
7 | @parameters = parameters
8 | end
9 |
10 | def query_string
11 | parameter_list = @parameters.map {|k,v| "#{k}=#{v}" }
12 | parameter_list.empty? ? '' : "?#{parameter_list.join('&')}"
13 | end
14 |
15 | def uri
16 | URI.parse(@base_url)
17 | end
18 |
19 | def send_request
20 | http = Net::HTTP.new(uri.host, uri.port)
21 | http.use_ssl = true
22 | http.verify_mode = OpenSSL::SSL::VERIFY_NONE
23 | response = http.get("#{uri.path}#{query_string}", 'Authorization' => "GoogleLogin auth=#{@auth_token}")
24 | raise response.body.inspect unless response.is_a?(Net::HTTPOK)
25 | response
26 | end
27 |
28 | end
29 | end
--------------------------------------------------------------------------------
/lib/extensions/symbol.rb:
--------------------------------------------------------------------------------
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
--------------------------------------------------------------------------------
/lib/garb/report.rb:
--------------------------------------------------------------------------------
1 | module Garb
2 | class Report
3 | include Resource::ResourceMethods
4 |
5 | MONTH = 2592000
6 | URL = "https://www.google.com/analytics/feeds/data"
7 |
8 | def initialize(auth_token, profile, opts={})
9 | @auth_token = auth_token
10 | @profile = profile
11 |
12 | @start_date = opts.fetch(:start_date, Time.now - MONTH)
13 | @end_date = opts.fetch(:end_date, Time.now)
14 | @limit = opts.fetch(:limit, nil)
15 | @offset = opts.fetch(:offset, nil)
16 |
17 | # clear filters and sort
18 | @filters = ReportParameter.new(:filters)
19 | @sorts = ReportParameter.new(:sort)
20 |
21 | metrics opts.fetch(:metrics, [])
22 | dimensions opts.fetch(:dimensions, [])
23 | filter opts.fetch(:filter, [])
24 | sort opts.fetch(:sort, [])
25 | end
26 |
27 | def results
28 | ReportResponse.new(send_request_for_body).results
29 | end
30 |
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/test/unit/session_test.rb:
--------------------------------------------------------------------------------
1 | require File.join(File.dirname(__FILE__), '..', '/test_helper')
2 |
3 | module Garb
4 | class SessionTest < Test::Unit::TestCase
5 |
6 | context "The Session class" do
7 |
8 | should "be able retrieve an auth_token for a user" do
9 | auth_request = mock { |m| m.expects(:auth_token).with({}).returns('toke') }
10 | AuthenticationRequest.expects(:new).with('email', 'password').returns(auth_request)
11 |
12 | token = Session.login('email', 'password')
13 | assert_equal 'toke', token
14 | end
15 |
16 | should "be able retrieve an auth_token for a user with secure ssl" do
17 | auth_request = mock {|m| m.expects(:auth_token).with({:secure => true}).returns('toke') }
18 | AuthenticationRequest.expects(:new).with('email', 'password').returns(auth_request)
19 |
20 | token = Session.login('email', 'password', :secure => true)
21 | assert_equal 'toke', token
22 | end
23 |
24 | end
25 |
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/test/unit/report_response_test.rb:
--------------------------------------------------------------------------------
1 | require File.join(File.dirname(__FILE__), '..', '/test_helper')
2 |
3 | module Garb
4 | class ReportResponseTest < Test::Unit::TestCase
5 | context "An instance of the ReportResponse class" do
6 | setup do
7 | @xml = File.read(File.join(File.dirname(__FILE__), '..', "/fixtures/report_feed.xml"))
8 | @response = ReportResponse.new(@xml)
9 | end
10 |
11 | should "parse xml response with happymapper" do
12 | h1 = {"city"=>"(not set)", "pageviews"=>"33", "country"=>"(not set)"}
13 | h2 = {"city"=>"Kabul", "pageviews"=>"2", "country"=>"Afghanistan"}
14 | h3 = {"city"=>"Tirana", "pageviews"=>"1", "country"=>"Albania"}
15 |
16 | OpenStruct.expects(:new).with(h1).returns('entry1')
17 | OpenStruct.expects(:new).with(h2).returns('entry2')
18 | OpenStruct.expects(:new).with(h3).returns('entry3')
19 |
20 | assert_equal(['entry1', 'entry2', 'entry3'], @response.parse)
21 | end
22 |
23 | should "have results or parse them" do
24 | @response.expects(:parse)
25 | @response.results
26 | end
27 | end
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/lib/garb/profile.rb:
--------------------------------------------------------------------------------
1 | module Garb
2 | class Profile
3 |
4 | attr_reader :table_id, :title, :account_name, :account_id, :profile_id
5 |
6 | class Property
7 | include HappyMapper
8 |
9 | tag 'property'
10 | namespace 'dxp'
11 |
12 | attribute :name, String
13 | attribute :value, String
14 |
15 | def instance_name
16 | name.from_ga.underscored
17 | end
18 | end
19 |
20 | class Entry
21 | include HappyMapper
22 |
23 | tag 'entry'
24 |
25 | element :title, String
26 | element :tableId, String, :namespace => 'dxp'
27 |
28 | has_many :properties, Property
29 | end
30 |
31 | def initialize(entry)
32 | @title = entry.title
33 | @table_id = entry.tableId
34 |
35 | entry.properties.each do |p|
36 | instance_variable_set :"@#{p.instance_name}", p.value
37 | end
38 | end
39 |
40 | def id
41 | @table_id.from_ga
42 | end
43 |
44 | def self.all(auth_token)
45 | url = "https://www.google.com/analytics/feeds/accounts/default"
46 | response = DataRequest.new(auth_token, url).send_request
47 |
48 | Entry.parse(response.body).map {|entry| new(entry)}
49 | end
50 | end
51 | end
52 |
--------------------------------------------------------------------------------
/test/unit/operator_test.rb:
--------------------------------------------------------------------------------
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
38 |
--------------------------------------------------------------------------------
/lib/garb/report_response.rb:
--------------------------------------------------------------------------------
1 | module Garb
2 | class ReportResponse
3 | # include Enumerable
4 |
5 | def initialize(response_body)
6 | @xml = response_body
7 | end
8 |
9 | def parse
10 | entries = Entry.parse(@xml)
11 |
12 | @results = entries.collect do |entry|
13 | hash = {}
14 |
15 | entry.metrics.each do |m|
16 | name = m.name.sub(/^ga\:/,'').underscored
17 | hash.merge!({name => m.value})
18 | end
19 |
20 | entry.dimensions.each do |d|
21 | name = d.name.sub(/^ga\:/,'').underscored
22 | hash.merge!({name => d.value})
23 | end
24 |
25 | OpenStruct.new(hash)
26 | end
27 | end
28 |
29 | def results
30 | @results || parse
31 | end
32 |
33 | class Metric
34 | include HappyMapper
35 |
36 | tag 'metric'
37 | namespace 'dxp'
38 |
39 | attribute :name, String
40 | attribute :value, String
41 | end
42 |
43 | class Dimension
44 | include HappyMapper
45 |
46 | tag 'dimension'
47 | namespace 'dxp'
48 |
49 | attribute :name, String
50 | attribute :value, String
51 | end
52 |
53 | class Entry
54 | include HappyMapper
55 |
56 | tag 'entry'
57 |
58 | has_many :metrics, Metric
59 | has_many :dimensions, Dimension
60 | end
61 | end
62 | end
--------------------------------------------------------------------------------
/lib/garb/authentication_request.rb:
--------------------------------------------------------------------------------
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(ssl_mode)
27 | http = Net::HTTP.new(uri.host, uri.port)
28 | http.use_ssl = true
29 | http.verify_mode = ssl_mode
30 |
31 | if ssl_mode == OpenSSL::SSL::VERIFY_PEER
32 | http.ca_file = CA_CERT_FILE
33 | end
34 |
35 | http.request(build_request) do |response|
36 | raise AuthError unless response.is_a?(Net::HTTPOK)
37 | end
38 | end
39 |
40 | def build_request
41 | post = Net::HTTP::Post.new(uri.path)
42 | post.set_form_data(parameters)
43 | post
44 | end
45 |
46 | def auth_token(opts={})
47 | ssl_mode = opts[:secure] ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
48 | send_request(ssl_mode).body.match(/^Auth=(.*)$/)[1]
49 | end
50 |
51 | end
52 | end
53 |
--------------------------------------------------------------------------------
/test/unit/account_test.rb:
--------------------------------------------------------------------------------
1 | require File.join(File.dirname(__FILE__), '..', '/test_helper')
2 |
3 | module Garb
4 | class AccountTest < Test::Unit::TestCase
5 | context "The Account class" do
6 | should "have an array of accounts with all profiles" do
7 | p1 = stub(:account_id => '1111', :account_name => 'Blog 1')
8 | p2 = stub(:account_id => '1112', :account_name => 'Blog 2')
9 | Profile.stubs(:all).returns([p1,p2,p1,p2])
10 | Account.expects(:new).with([p1,p1]).returns('account1')
11 | Account.expects(:new).with([p2,p2]).returns('account2')
12 | assert_equal ['account1','account2'], Account.all('token')
13 | end
14 | end
15 |
16 | context "An instance of the Account class" do
17 | context "when creating a new account from an array of profiles" do
18 | setup do
19 | profile = stub(:account_id => '1111', :account_name => 'Blog 1')
20 | @profiles = [profile,profile]
21 | @account = Account.new(@profiles)
22 | end
23 |
24 | should "take the account id from the first profile" do
25 | assert_equal @profiles.first.account_id, @account.id
26 | end
27 |
28 | should "take the account name from the first profile" do
29 | assert_equal @profiles.first.account_name, @account.name
30 | end
31 |
32 | should "store the array of profiles" do
33 | assert_equal @profiles, @account.profiles
34 | end
35 | end
36 | end
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require 'rubygems'
2 | require 'rake/gempackagetask'
3 | require 'rake/testtask'
4 |
5 | require 'lib/garb/version'
6 |
7 | task :default => :test
8 |
9 | spec = Gem::Specification.new do |s|
10 | s.name = 'garb'
11 | s.version = Garb::Version.to_s
12 | s.has_rdoc = false
13 | s.summary = "Google Analytics API Ruby Wrapper"
14 | s.authors = ['Tony Pitale','Justin Marney', 'Patrick Reagan']
15 | s.email = 'tony.pitale@viget.com'
16 | s.homepage = 'http://github.com/vigetlabs/garb'
17 | s.files = %w(README.md Rakefile) + Dir.glob("lib/**/*")
18 | s.test_files = Dir.glob("test/**/*")
19 |
20 | s.add_dependency("happymapper", [">= 0.2.5"])
21 | end
22 |
23 | Rake::GemPackageTask.new(spec) do |pkg|
24 | pkg.gem_spec = spec
25 | end
26 |
27 | Rake::TestTask.new do |t|
28 | t.libs << 'test'
29 | t.test_files = FileList["test/**/*_test.rb"]
30 | t.verbose = true
31 | end
32 |
33 | desc 'Generate the gemspec to serve this Gem from Github'
34 | task :github do
35 | file = File.dirname(__FILE__) + "/#{spec.name}.gemspec"
36 | File.open(file, 'w') {|f| f << spec.to_ruby }
37 | puts "Created gemspec: #{file}"
38 | end
39 |
40 | begin
41 | require 'rcov/rcovtask'
42 |
43 | desc "Generate RCov coverage report"
44 | Rcov::RcovTask.new(:rcov) do |t|
45 | t.test_files = FileList['test/**/*_test.rb']
46 | t.rcov_opts << "-x lib/garb.rb -x lib/garb/version.rb"
47 | end
48 | rescue LoadError
49 | nil
50 | end
51 |
52 | task :default => 'test'
53 |
54 | # EOF
55 |
--------------------------------------------------------------------------------
/test/unit/symbol_test.rb:
--------------------------------------------------------------------------------
1 | require File.join(File.dirname(__FILE__), '..', '/test_helper')
2 |
3 | class SymbolTest < Test::Unit::TestCase
4 |
5 | context "An instance of the Symbol class" do
6 |
7 | should "properly format itself for ga" do
8 | assert_equal "ga:requestUri", :request_uri.to_ga
9 | end
10 |
11 | should "define a :desc operator" do
12 | operator = stub()
13 | symbol = :foo
14 |
15 | Operator.expects(:new).with(:foo, '-', true).returns(operator)
16 | assert_equal operator, :foo.desc
17 | end
18 |
19 | def self.should_define_operator(operators)
20 | operators.each do |method, operator|
21 | should "define an :#{method} operator" do
22 | new_operator = stub()
23 | symbol = :foo
24 |
25 | Operator.expects(:new).with(:foo, operator).returns(new_operator)
26 | assert_equal new_operator, :foo.send(method)
27 | end
28 | end
29 | end
30 |
31 | should_define_operator :eql => '==',
32 | :not_eql => '!=',
33 | :gt => '>',
34 | :gte => '>=',
35 | :lt => '<',
36 | :lte => '<=',
37 | :matches => '==',
38 | :does_not_match => '!=',
39 | :contains => '=~',
40 | :does_not_contain => '!~',
41 | :substring => '=@',
42 | :not_substring => '!@'
43 | end
44 | end
--------------------------------------------------------------------------------
/test/fixtures/profile_feed.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | http://www.google.com/analytics/feeds/accounts/email@example.com
4 | 2009-03-27T08:14:28.000-07:00
5 | Profile list for email@example.com
6 |
7 | Google Analytics
8 | Google Analytics
9 | 2
10 | 1
11 | 2
12 |
13 | http://www.google.com/analytics/feeds/accounts/ga:12345
14 | 2008-07-21T14:05:57.000-07:00
15 | Historical
16 | ga:12345
17 |
18 |
19 |
20 |
21 |
22 |
23 | http://www.google.com/analytics/feeds/accounts/ga:12346
24 | 2008-11-24T11:51:07.000-08:00
25 | Presently
26 | ga:12346
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/test/unit/profile_test.rb:
--------------------------------------------------------------------------------
1 | require File.join(File.dirname(__FILE__), '..', '/test_helper')
2 |
3 | module Garb
4 | class ProfileTest < Test::Unit::TestCase
5 |
6 | context "The Profile class" do
7 |
8 | should "be able to return a list of all profiles" do
9 | url = 'https://www.google.com/analytics/feeds/accounts/default'
10 |
11 | xml = read_fixture('profile_feed.xml')
12 |
13 | data_request = mock
14 | data_request.expects(:send_request).with().returns(stub(:body => xml))
15 |
16 | DataRequest.expects(:new).with('token', url).returns(data_request)
17 |
18 | entries = [stub]
19 |
20 | Profile::Entry.expects(:parse).with(xml).returns(entries)
21 |
22 | profiles = []
23 | entries.each do |entry|
24 | profile = stub
25 | profiles << profile
26 | Garb::Profile.expects(:new).with(entry).returns(profile)
27 | end
28 |
29 | assert_equal profiles, Profile.all('token')
30 | end
31 |
32 | end
33 |
34 | context "An instance of the Profile class" do
35 |
36 | setup do
37 | @entry = (Profile::Entry.parse(read_fixture('profile_feed.xml'))).first
38 | @profile = Profile.new(@entry)
39 | end
40 |
41 | should "have a value for :title" do
42 | assert_equal "Historical", @profile.title
43 | end
44 |
45 | should "have a value for :table_id" do
46 | assert_equal 'ga:12345', @profile.table_id
47 | end
48 |
49 | should "have a value for :id" do
50 | assert_equal '12345', @profile.id
51 | end
52 |
53 | should "have a value for :account_id" do
54 | assert_equal '1111', @profile.account_id
55 | end
56 |
57 | should "have a value for :account_name" do
58 | assert_equal 'Blog Beta', @profile.account_name
59 | end
60 |
61 | end
62 |
63 | end
64 | end
--------------------------------------------------------------------------------
/test/unit/report_parameter_test.rb:
--------------------------------------------------------------------------------
1 | require File.join(File.dirname(__FILE__), '..', '/test_helper')
2 |
3 | module Garb
4 | class ReportParameterTest < Test::Unit::TestCase
5 |
6 | context "An instance of the ReportParameter class" do
7 | setup do
8 | @metrics = ReportParameter.new(:metrics)
9 | end
10 |
11 | should "have a name" do
12 | assert_equal "metrics", @metrics.name
13 | end
14 |
15 | should "have a list of elements" do
16 | assert_equal [], @metrics.elements
17 | end
18 |
19 | should "be able to add new elements" do
20 | assert_equal(@metrics, @metrics << :request_uri)
21 | assert_equal [:request_uri], @metrics.elements
22 | end
23 |
24 | should "merge an array of elements" do
25 | assert_equal(@metrics, @metrics << [:request_uri])
26 | assert_equal [:request_uri], @metrics.elements
27 | end
28 |
29 | context "converting to params" do
30 | should "be able to format the parameters into strings" do
31 | @metrics << :request_uri
32 | assert_equal({'metrics' => 'ga:requestUri'}, @metrics.to_params)
33 | end
34 |
35 | should "join multiple symbol elements" do
36 | @metrics << :request_uri << :city
37 | assert_equal({'metrics' => 'ga:requestUri,ga:city'}, @metrics.to_params)
38 | end
39 |
40 | should "join operator elements" do
41 | @metrics << :city.desc
42 | assert_equal({'metrics' => '-ga:city'}, @metrics.to_params)
43 | end
44 |
45 | should "parameterize hash operators and join elements" do
46 | @metrics << {:city.eql => 'New York'}
47 | params = {'metrics' => 'ga:city%3D%3DNew+York'}
48 |
49 | assert_equal params, @metrics.to_params
50 | end
51 |
52 | should "properly encode operators" do
53 | @metrics << {:request_uri.contains => 'New York'}
54 | params = {'metrics' => 'ga:requestUri%3D~New+York'}
55 |
56 | assert_equal params, @metrics.to_params
57 | end
58 | end
59 | end
60 |
61 | end
62 | end
--------------------------------------------------------------------------------
/test/unit/data_request_test.rb:
--------------------------------------------------------------------------------
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 both token and parameters are empty" do
22 | data_request = DataRequest.new("", "")
23 | assert_equal "", data_request.query_string
24 | end
25 |
26 | should "return an empty query string if parameters are empty" do
27 | data_request = DataRequest.new("token", "")
28 | assert_equal "", data_request.query_string
29 | end
30 |
31 | should "be able to build a uri" do
32 | url = 'http://example.com'
33 | expected = URI.parse('http://example.com')
34 |
35 | assert_equal expected, DataRequest.new("token", url).uri
36 | end
37 |
38 | should "be able to make a request to the GAAPI" do
39 | # Session.expects(:auth_token).with().returns('toke')
40 | response = mock
41 | response.expects(:is_a?).with(Net::HTTPOK).returns(true)
42 |
43 | http = mock do |m|
44 | m.expects(:use_ssl=).with(true)
45 | m.expects(:verify_mode=).with(OpenSSL::SSL::VERIFY_NONE)
46 | m.expects(:get).with('/data?key=value', 'Authorization' => 'GoogleLogin auth=toke').returns(response)
47 | end
48 |
49 | Net::HTTP.expects(:new).with('example.com', 443).returns(http)
50 |
51 | data_request = DataRequest.new('toke', 'https://example.com/data', 'key' => 'value')
52 | assert_equal response, data_request.send_request
53 | end
54 | end
55 |
56 | end
57 | end
--------------------------------------------------------------------------------
/lib/garb.rb:
--------------------------------------------------------------------------------
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 'ostruct'
8 | require 'happymapper'
9 |
10 | require 'garb/version'
11 | require 'garb/authentication_request'
12 | require 'garb/data_request'
13 | require 'garb/session'
14 | require 'garb/profile'
15 | require 'garb/account'
16 | require 'garb/report_parameter'
17 | require 'garb/report_response'
18 | require 'garb/resource'
19 | require 'garb/report'
20 |
21 | require 'extensions/string'
22 | require 'extensions/operator'
23 | require 'extensions/symbol'
24 | require 'extensions/array'
25 |
26 | module Garb
27 | # :stopdoc:
28 | GA = "http://schemas.google.com/analytics/2008"
29 |
30 | VERSION = '0.1.2'
31 | LIBPATH = ::File.expand_path(::File.dirname(__FILE__)) + ::File::SEPARATOR
32 | PATH = ::File.dirname(LIBPATH) + ::File::SEPARATOR
33 | # :startdoc:
34 |
35 | # Returns the version string for the library.
36 | #
37 | def self.version
38 | VERSION
39 | end
40 |
41 | # Returns the library path for the module. If any arguments are given,
42 | # they will be joined to the end of the libray path using
43 | # File.join.
44 | #
45 | def self.libpath( *args )
46 | args.empty? ? LIBPATH : ::File.join(LIBPATH, args.flatten)
47 | end
48 |
49 | # Returns the lpath for the module. If any arguments are given,
50 | # they will be joined to the end of the path using
51 | # File.join.
52 | #
53 | def self.path( *args )
54 | args.empty? ? PATH : ::File.join(PATH, args.flatten)
55 | end
56 |
57 | # Utility method used to rquire all files ending in .rb that lie in the
58 | # directory below this file that has the same name as the filename passed
59 | # in. Optionally, a specific _directory_ name can be passed in such that
60 | # the _filename_ does not have to be equivalent to the directory.
61 | #
62 | def self.require_all_libs_relative_to( fname, dir = nil )
63 | dir ||= ::File.basename(fname, '.*')
64 | search_me = ::File.expand_path(
65 | ::File.join(::File.dirname(fname), dir, '*', '*.rb'))
66 |
67 | Dir.glob(search_me).sort.each {|rb| require rb}
68 | end
69 | end # module Garb
70 |
71 | # EOF
72 |
--------------------------------------------------------------------------------
/lib/garb/resource.rb:
--------------------------------------------------------------------------------
1 | module Garb
2 | module Resource
3 | MONTH = 2592000
4 | URL = "https://www.google.com/analytics/feeds/data"
5 |
6 | def self.included(report)
7 | report.extend(ResourceMethods)
8 | end
9 |
10 | module ResourceMethods
11 |
12 | def metrics(*fields)
13 | @metrics ||= ReportParameter.new(:metrics)
14 | @metrics << fields
15 | end
16 |
17 | def dimensions(*fields)
18 | @dimensions ||= ReportParameter.new(:dimensions)
19 | @dimensions << fields
20 | end
21 |
22 | def filter(*hash)
23 | @filters << hash
24 | end
25 |
26 | def filters
27 | @filters ||= ReportParameter.new(:filters)
28 | end
29 |
30 | def sort(*fields)
31 | @sorts << fields
32 | end
33 |
34 | def sorts
35 | @sorts ||= ReportParameter.new(:sort)
36 | end
37 |
38 | def results(auth_token, profile, opts = {}, &block)
39 | @auth_token = auth_token
40 | @profile = profile
41 |
42 | # clear filters and sort
43 | @filters = ReportParameter.new(:filters)
44 | @sorts = ReportParameter.new(:sort)
45 |
46 | @start_date = opts.fetch(:start_date, Time.now - MONTH)
47 | @end_date = opts.fetch(:end_date, Time.now)
48 | @limit = opts.fetch(:limit, nil)
49 | @offset = opts.fetch(:offset, nil)
50 |
51 | instance_eval(&block) if block_given?
52 |
53 | ReportResponse.new(send_request_for_body).results
54 | end
55 |
56 | def page_params
57 | {'max-results' => @limit, 'start-index' => @offset}.reject{|k,v| v.nil?}
58 | end
59 |
60 | def default_params
61 | {'ids' => @profile.table_id,
62 | 'start-date' => format_time(@start_date),
63 | 'end-date' => format_time(@end_date)}
64 | end
65 |
66 | def params
67 | [
68 | metrics.to_params,
69 | dimensions.to_params,
70 | sorts.to_params,
71 | filters.to_params,
72 | page_params
73 | ].inject(default_params) do |p, i|
74 | p.merge(i)
75 | end
76 | end
77 |
78 | def format_time(t)
79 | t.strftime('%Y-%m-%d')
80 | end
81 |
82 | def send_request_for_body
83 | request = DataRequest.new(@auth_token, URL, params)
84 | response = request.send_request
85 | response.body
86 | end
87 |
88 | end
89 | end
90 | end
91 |
--------------------------------------------------------------------------------
/garb.gemspec:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 |
3 | Gem::Specification.new do |s|
4 | s.name = %q{garb}
5 | s.version = "0.2.6"
6 |
7 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8 | s.authors = ["Tony Pitale", "Justin Marney", "Patrick Reagan"]
9 | s.date = %q{2009-06-30}
10 | s.email = %q{tony.pitale@viget.com}
11 | s.files = ["README.md", "Rakefile", "lib/extensions", "lib/extensions/array.rb", "lib/extensions/operator.rb", "lib/extensions/string.rb", "lib/extensions/symbol.rb", "lib/garb", "lib/garb/account.rb", "lib/garb/authentication_request.rb", "lib/garb/data_request.rb", "lib/garb/oauth_session.rb", "lib/garb/profile.rb", "lib/garb/report.rb", "lib/garb/report_parameter.rb", "lib/garb/report_response.rb", "lib/garb/resource.rb", "lib/garb/session.rb", "lib/garb/version.rb", "lib/garb.rb", "test/fixtures", "test/fixtures/cacert.pem", "test/fixtures/profile_feed.xml", "test/fixtures/report_feed.xml", "test/test_helper.rb", "test/unit", "test/unit/account_test.rb", "test/unit/authentication_request_test.rb", "test/unit/data_request_test.rb", "test/unit/garb_test.rb", "test/unit/oauth_session_test.rb", "test/unit/operator_test.rb", "test/unit/profile_test.rb", "test/unit/report_parameter_test.rb", "test/unit/report_response_test.rb", "test/unit/report_test.rb", "test/unit/resource_test.rb", "test/unit/session_test.rb", "test/unit/string_test.rb", "test/unit/symbol_test.rb"]
12 | s.homepage = %q{http://github.com/vigetlabs/garb}
13 | s.require_paths = ["lib"]
14 | s.rubygems_version = %q{1.3.4}
15 | s.summary = %q{Google Analytics API Ruby Wrapper}
16 | s.test_files = ["test/fixtures", "test/fixtures/cacert.pem", "test/fixtures/profile_feed.xml", "test/fixtures/report_feed.xml", "test/test_helper.rb", "test/unit", "test/unit/account_test.rb", "test/unit/authentication_request_test.rb", "test/unit/data_request_test.rb", "test/unit/garb_test.rb", "test/unit/oauth_session_test.rb", "test/unit/operator_test.rb", "test/unit/profile_test.rb", "test/unit/report_parameter_test.rb", "test/unit/report_response_test.rb", "test/unit/report_test.rb", "test/unit/resource_test.rb", "test/unit/session_test.rb", "test/unit/string_test.rb", "test/unit/symbol_test.rb"]
17 |
18 | if s.respond_to? :specification_version then
19 | current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
20 | s.specification_version = 3
21 |
22 | if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
23 | s.add_runtime_dependency(%q, [">= 0.2.5"])
24 | else
25 | s.add_dependency(%q, [">= 0.2.5"])
26 | end
27 | else
28 | s.add_dependency(%q, [">= 0.2.5"])
29 | end
30 | end
--------------------------------------------------------------------------------
/test/unit/report_test.rb:
--------------------------------------------------------------------------------
1 | require File.join(File.dirname(__FILE__), '..', '/test_helper')
2 |
3 | module Garb
4 | # Also tests Garb::Resource, which is the basis for Garb::Report
5 | class ReportTest < Test::Unit::TestCase
6 | context "An instance of the Report class" do
7 | setup do
8 | @now = Time.now
9 | Time.stubs(:now).returns(@now)
10 | @auth_token = 'token'
11 | @profile = stub(:table_id => 'ga:1234')
12 | @report = Report.new(@auth_token, @profile)
13 | end
14 |
15 | %w(metrics dimensions filters sorts).each do |param|
16 | should "have parameters for #{param}" do
17 | assert @report.send(:"#{param}").is_a?(ReportParameter)
18 | end
19 | end
20 |
21 | should "have default parameters" do
22 | @report.stubs(:format_time).returns('2009-08-01')
23 | params = {'ids' => 'ga:1234', 'start-date' => '2009-08-01', 'end-date' => '2009-08-01'}
24 | assert_equal params, @report.default_params
25 | end
26 |
27 | should "collect params from metrics, dimensions, filters, sort, and defaults" do
28 | @report.stubs(:metrics).returns(stub(:to_params => {'metrics' => 6}))
29 | @report.stubs(:dimensions).returns(stub(:to_params => {'dimensions' => 5}))
30 | @report.stubs(:filters).returns(stub(:to_params => {'filters' => 4}))
31 | @report.stubs(:sorts).returns(stub(:to_params => {'sort' => 3}))
32 | @report.stubs(:page_params).returns({'page_params' => 2})
33 | @report.stubs(:default_params).returns({'default_params' => 1})
34 |
35 | params = {'metrics' => 6, 'dimensions' => 5, 'filters' => 4, 'sort' => 3, 'page_params' => 2, 'default_params' => 1}
36 | assert_equal params, @report.params
37 | end
38 |
39 | should "format time" do
40 | assert_equal @now.strftime('%Y-%m-%d'), @report.format_time(@now)
41 | end
42 |
43 | should "send a data request to GA" do
44 | response = mock {|m| m.expects(:body).returns('response body') }
45 | request = mock {|m| m.expects(:send_request).returns(response) }
46 | @report.expects(:params).returns('params')
47 |
48 | DataRequest.expects(:new).with('token', Garb::Report::URL, 'params').returns(request)
49 | assert_equal 'response body', @report.send_request_for_body
50 | end
51 |
52 | should "fetch and parse results from GA" do
53 | @report.expects(:send_request_for_body).with().returns('xml')
54 | ReportResponse.expects(:new).with('xml').returns(mock(:results => ['entry']))
55 | assert_equal ['entry'], @report.results
56 | end
57 | end
58 |
59 | context "An instance of the Report class with initial options" do
60 | setup do
61 | @profile = stub(:table_id => 'ga:1234')
62 | @report = Report.new(@auth_token, @profile, :limit => 10, :offset => 20)
63 | end
64 |
65 | should "have page paramaters" do
66 | params = {'max-results' => 10, 'start-index' => 20}
67 | assert_equal params, @report.page_params
68 | end
69 | end
70 |
71 | end
72 | end
--------------------------------------------------------------------------------
/test/fixtures/report_feed.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 | 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
7 | 2008-01-02T15:59:59.999-08:00
8 | Google Analytics Data for Profile 983247
9 |
10 |
11 |
12 |
13 | Google Analytics
14 |
15 | 3
16 | 4
17 | UA-983247-67
18 | 2008-01-01
19 | 2008-01-02
20 |
21 |
22 | 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
23 | 2008-01-01T16:00:00.001-08:00
24 | ga:country=(not set) | ga:city=(not set)
25 |
26 |
27 |
28 |
29 |
30 |
31 | 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
32 | 2008-01-01T16:00:00.001-08:00
33 | ga:country=Afghanistan | ga:city=Kabul
34 |
35 |
36 |
37 |
38 |
39 | 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
40 | 2008-01-01T16:00:00.001-08:00
41 | ga:country=Albania | ga:city=Tirana
42 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/test/fixtures/cacert.pem:
--------------------------------------------------------------------------------
1 | ##
2 | ## cacert.pem-foo -- Bundle of CA Root Certificates
3 | ##
4 | ## Converted at: Thu Mar 26 21:23:06 2009 UTC
5 | ##
6 | ## This is a bundle of X.509 certificates of public Certificate Authorities
7 | ## (CA). These were automatically extracted from Mozilla's root certificates
8 | ## file (certdata.txt). This file can be found in the mozilla source tree:
9 | ## '/mozilla/security/nss/lib/ckfw/builtins/certdata.txt'
10 | ##
11 | ## It contains the certificates in PEM format and therefore
12 | ## can be directly used with curl / libcurl / php_curl, or with
13 | ## an Apache+mod_ssl webserver for SSL client authentication.
14 | ## Just configure this file as the SSLCACertificateFile.
15 | ##
16 |
17 | # ***** BEGIN LICENSE BLOCK *****
18 | # Version: MPL 1.1/GPL 2.0/LGPL 2.1
19 | #
20 | # The contents of this file are subject to the Mozilla Public License Version
21 | # 1.1 (the "License"); you may not use this file except in compliance with
22 | # the License. You may obtain a copy of the License at
23 | # http://www.mozilla.org/MPL/
24 | #
25 | # Software distributed under the License is distributed on an "AS IS" basis,
26 | # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
27 | # for the specific language governing rights and limitations under the
28 | # License.
29 | #
30 | # The Original Code is the Netscape security libraries.
31 | #
32 | # The Initial Developer of the Original Code is
33 | # Netscape Communications Corporation.
34 | # Portions created by the Initial Developer are Copyright (C) 1994-2000
35 | # the Initial Developer. All Rights Reserved.
36 | #
37 | # Contributor(s):
38 | #
39 | # Alternatively, the contents of this file may be used under the terms of
40 | # either the GNU General Public License Version 2 or later (the "GPL"), or
41 | # the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
42 | # in which case the provisions of the GPL or the LGPL are applicable instead
43 | # of those above. If you wish to allow use of your version of this file only
44 | # under the terms of either the GPL or the LGPL, and not to allow others to
45 | # use your version of this file under the terms of the MPL, indicate your
46 | # decision by deleting the provisions above and replace them with the notice
47 | # and other provisions required by the GPL or the LGPL. If you do not delete
48 | # the provisions above, a recipient may use your version of this file under
49 | # the terms of any one of the MPL, the GPL or the LGPL.
50 | #
51 | # ***** END LICENSE BLOCK *****
52 | # @(#) $RCSfile: certdata.txt,v $ $Revision: 1.51 $ $Date: 2009/01/15 22:35:15 $
53 |
54 | Verisign/RSA Secure Server CA
55 | =============================
56 | -----BEGIN CERTIFICATE-----
57 | MIICNDCCAaECEAKtZn5ORf5eV288mBle3cAwDQYJKoZIhvcNAQECBQAwXzELMAkGA1UEBhMCVVMx
58 | IDAeBgNVBAoTF1JTQSBEYXRhIFNlY3VyaXR5LCBJbmMuMS4wLAYDVQQLEyVTZWN1cmUgU2VydmVy
59 | IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk0MTEwOTAwMDAwMFoXDTEwMDEwNzIzNTk1OVow
60 | XzELMAkGA1UEBhMCVVMxIDAeBgNVBAoTF1JTQSBEYXRhIFNlY3VyaXR5LCBJbmMuMS4wLAYDVQQL
61 | EyVTZWN1cmUgU2VydmVyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGbMA0GCSqGSIb3DQEBAQUA
62 | A4GJADCBhQJ+AJLOesGugz5aqomDV6wlAXYMra6OLDfO6zV4ZFQD5YRAUcm/jwjiioII0haGN1Xp
63 | sSECrXZogZoFokvJSyVmIlZsiAeP94FZbYQHZXATcXY+m3dM41CJVphIuR2nKRoTLkoRWZweFdVJ
64 | VCxzOmmCsZc5nG1wZ0jl3S3WyB57AgMBAAEwDQYJKoZIhvcNAQECBQADfgBl3X7hsuyw4jrg7HFG
65 | mhkRuNPHoLQDQCYCPgmc4RKz0Vr2N6W3YQO2WxZpO8ZECAyIUwxrl0nHPjXcbLm7qt9cuzovk2C2
66 | qUtN8iD3zV9/ZHuO3ABc1/p3yjkWWW8O6tO1g39NTUJWdrTJXwT4OPjr0l91X817/OWOgHz8UA==
67 | -----END CERTIFICATE-----
68 |
--------------------------------------------------------------------------------
/test/unit/authentication_request_test.rb:
--------------------------------------------------------------------------------
1 | require File.join(File.dirname(__FILE__), '..', '/test_helper')
2 |
3 | CA_CERT_FILE = File.join(File.dirname(__FILE__), '..', '/cacert.pem')
4 |
5 | module Garb
6 | class AuthenticationRequestTest < Test::Unit::TestCase
7 |
8 | context "An instance of the AuthenticationRequest class" do
9 |
10 | setup { @request = AuthenticationRequest.new('email', 'password') }
11 |
12 | should "have a collection of parameters that include the email and password" do
13 | expected =
14 | {
15 | 'Email' => 'user@example.com',
16 | 'Passwd' => 'fuzzybunnies',
17 | 'accountType' => 'HOSTED_OR_GOOGLE',
18 | 'service' => 'analytics',
19 | 'source' => 'vigetLabs-garb-001'
20 | }
21 |
22 | request = AuthenticationRequest.new('user@example.com', 'fuzzybunnies')
23 | assert_equal expected, request.parameters
24 | end
25 |
26 | should "have a URI" do
27 | assert_equal URI.parse('https://www.google.com/accounts/ClientLogin'), @request.uri
28 | end
29 |
30 | should "be able to send a request to the GAAPI service with proper ssl" do
31 | @request.expects(:build_request).returns('post')
32 |
33 | response = mock {|m| m.expects(:is_a?).with(Net::HTTPOK).returns(true) }
34 |
35 | http = mock do |m|
36 | m.expects(:use_ssl=).with(true)
37 | m.expects(:verify_mode=).with(OpenSSL::SSL::VERIFY_PEER)
38 | m.expects(:ca_file=).with(CA_CERT_FILE)
39 | m.expects(:request).with('post').yields(response)
40 | end
41 |
42 | Net::HTTP.expects(:new).with('www.google.com', 443).returns(http)
43 |
44 | @request.send_request(OpenSSL::SSL::VERIFY_PEER)
45 | end
46 |
47 | should "be able to send a request to the GAAPI service with ignoring ssl" do
48 | @request.expects(:build_request).returns('post')
49 |
50 | response = mock {|m| m.expects(:is_a?).with(Net::HTTPOK).returns(true) }
51 |
52 | http = mock do |m|
53 | m.expects(:use_ssl=).with(true)
54 | m.expects(:verify_mode=).with(OpenSSL::SSL::VERIFY_NONE)
55 | m.expects(:request).with('post').yields(response)
56 | end
57 |
58 | Net::HTTP.expects(:new).with('www.google.com', 443).returns(http)
59 |
60 | @request.send_request(OpenSSL::SSL::VERIFY_NONE)
61 | end
62 |
63 | should "be able to build a request for the GAAPI service" do
64 | params = "param"
65 | @request.expects(:parameters).with().returns(params)
66 |
67 | post = mock
68 | post.expects(:set_form_data).with(params)
69 |
70 | Net::HTTP::Post.expects(:new).with('/accounts/ClientLogin').returns(post)
71 |
72 | @request.build_request
73 | end
74 |
75 | should "be able to retrieve an auth_token from the body" do
76 | response_data =
77 | "SID=mysid\n" +
78 | "LSID=mylsid\n" +
79 | "Auth=auth_token\n"
80 |
81 | @request.expects(:send_request).with(OpenSSL::SSL::VERIFY_NONE).returns(stub(:body => response_data))
82 |
83 | assert_equal 'auth_token', @request.auth_token
84 | end
85 |
86 | should "use VERIFY_PEER if auth_token needs to be secure" do
87 | response_data =
88 | "SID=mysid\n" +
89 | "LSID=mylsid\n" +
90 | "Auth=auth_token\n"
91 |
92 | @request.expects(:send_request).with(OpenSSL::SSL::VERIFY_PEER).returns(stub(:body => response_data))
93 |
94 | assert_equal 'auth_token', @request.auth_token(:secure => true)
95 | end
96 |
97 | should "raise an exception when requesting an auth_token when the authorization fails" do
98 | @request.stubs(:build_request)
99 | response = mock do |m|
100 | m.expects(:is_a?).with(Net::HTTPOK).returns(false)
101 | end
102 |
103 | http = stub do |s|
104 | s.stubs(:use_ssl=)
105 | s.stubs(:verify_mode=)
106 | s.stubs(:request).yields(response)
107 | end
108 |
109 | Net::HTTP.stubs(:new).with('www.google.com', 443).returns(http)
110 |
111 | assert_raise(Garb::AuthenticationRequest::AuthError) do
112 | @request.send_request(OpenSSL::SSL::VERIFY_NONE)
113 | end
114 | end
115 |
116 | end
117 |
118 | end
119 | end
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | garb
2 | ====
3 |
4 | by Tony Pitale with much help from Justin Marney, Patrick Reagan and others at Viget Labs
5 |
6 | http://github.com/vigetlabs/garb
7 |
8 | Important Changes
9 | =================
10 |
11 | Version 0.2.4 requires happymapper from rubygems, version 0.2.5. Be sure to update.
12 |
13 | Version 0.2.0 makes major changes (compared to 0.1.0) to the way garb is used to build reports.
14 | There is now both a module that gets included for generating defined classes,
15 | as well as, slight changes to the way that the Report class can be used.
16 |
17 | Description
18 | -----------
19 |
20 | Provides a Ruby API to the Google Analytics API.
21 |
22 | http://code.google.com/apis/analytics/docs/gdata/gdataDeveloperGuide.html
23 |
24 | Basic Usage
25 | ===========
26 |
27 | Login
28 | -----
29 |
30 | > token = Garb::Session.login(username, password)
31 |
32 | Accounts
33 | --------
34 | > Garb::Account.all(token)
35 |
36 | Profiles
37 | --------
38 |
39 | > Garb::Profile.all(token)
40 | > profile = Garb::Profile.all(token).first
41 |
42 | Define a Report Class and Get Results
43 | -------------------------------------
44 |
45 | class Exits
46 | include Garb::Resource
47 |
48 | metrics :exits, :pageviews, :exit_rate
49 | dimensions :request_uri
50 | end
51 |
52 | Parameters
53 | ----------
54 |
55 | * start_date: The date of the period you would like this report to start
56 | * end_date: The date to end, inclusive
57 | * limit: The maximum number of results to be returned
58 | * offset: The starting index
59 |
60 | Metrics & Dimensions
61 | --------------------
62 |
63 | Metrics and Dimensions are very complex because of the ways in which the can and cannot be combined.
64 |
65 | I suggest reading the google documentation to familiarize yourself with this.
66 |
67 | http://code.google.com/apis/analytics/docs/gdata/gdataReferenceDimensionsMetrics.html#bounceRate
68 |
69 | When you've returned, you can pass the appropriate combinations (up to 50 metrics and 2 dimenstions)
70 | to garb, as an array, of symbols. Or you can simply push a symbol into the array.
71 |
72 | Sorting
73 | -------
74 |
75 | Sorting can be done on any metric or dimension defined in the request, with .desc reversing the sort.
76 |
77 | Building a Report
78 | -----------------
79 |
80 | Given the class, session, and profile from above we can do:
81 |
82 | Exits.results(token, profile, :limit => 10, :offset => 19)
83 |
84 | Or, with sorting and filters:
85 |
86 | Exits.results(token, profile, :limit => 10, :offset => 19) do
87 | filter :request_uri.contains => 'season', :exits.gt => 100
88 | sort :exits
89 | end
90 |
91 | reports will be an array of OpenStructs with methods for the metrics and dimensions returned.
92 |
93 | Build a One-Off Report
94 | ----------------------
95 |
96 | report = Garb::Report.new(token, profile)
97 | report.metrics :pageviews
98 | report.dimensions :request_uri
99 |
100 | report.filter :request_uri.contains => 'season', :exits.gte => 10
101 | report.sort :exits
102 |
103 | report.results
104 |
105 | Filtering
106 | ---------
107 |
108 | Google Analytics supports a significant number of filtering options.
109 |
110 | http://code.google.com/apis/analytics/docs/gdata/gdataReference.html#filtering
111 |
112 | We handle filtering as an array of hashes that you can push into,
113 | which will be joined together (AND'd)
114 |
115 | Here is what we can do currently:
116 | (the operator is a method on a symbol metric or dimension)
117 |
118 | Operators on metrics:
119 |
120 | :eql => '==',
121 | :not_eql => '!=',
122 | :gt => '>',
123 | :gte => '>=',
124 | :lt => '<',
125 | :lte => '<='
126 |
127 | Operators on dimensions:
128 |
129 | :matches => '==',
130 | :does_not_match => '!=',
131 | :contains => '=~',
132 | :does_not_contain => '!~',
133 | :substring => '=@',
134 | :not_substring => '!@'
135 |
136 | Given the previous example one-off report, we can add a line for filter:
137 |
138 | report.filters << {:request_uri.eql => '/extend/effectively-using-git-with-subversion/'}
139 |
140 | SSL
141 | ---
142 |
143 | Version 0.2.3 includes support for real ssl encryption for authentication. First do:
144 |
145 | Garb::Session.login(username, password, :secure => true)
146 |
147 | Next, be sure to download http://curl.haxx.se/ca/cacert.pem into your application somewhere.
148 | Then, define a constant CA_CERT_FILE and point to that file.
149 |
150 | For whatever reason, simply creating a new certificate store and setting the defaults would
151 | not validate the google ssl certificate as authentic.
152 |
153 | TODOS
154 | -----
155 |
156 | * Sessions are currently global, which isn't awesome
157 | * Single user login is the only supported method currently.
158 | Intend to add hooks for using OAuth
159 | * Read opensearch header in results
160 | * OR joining filter parameters
161 |
162 | Requirements
163 | ------------
164 |
165 | happymapper >= 0.2.5 (should also install libxml)
166 |
167 | Install
168 | -------
169 |
170 | sudo gem install garb
171 |
172 | OR
173 |
174 | sudo gem install vigetlabs-garb -s http://gems.github.com
175 |
176 | License
177 | -------
178 |
179 | (The MIT License)
180 |
181 | Copyright (c) 2008 Viget Labs
182 |
183 | Permission is hereby granted, free of charge, to any person obtaining
184 | a copy of this software and associated documentation files (the
185 | 'Software'), to deal in the Software without restriction, including
186 | without limitation the rights to use, copy, modify, merge, publish,
187 | distribute, sublicense, and/or sell copies of the Software, and to
188 | permit persons to whom the Software is furnished to do so, subject to
189 | the following conditions:
190 |
191 | The above copyright notice and this permission notice shall be
192 | included in all copies or substantial portions of the Software.
193 |
194 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
195 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
196 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
197 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
198 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
199 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
200 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
201 |
--------------------------------------------------------------------------------