├── .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 | --------------------------------------------------------------------------------