betapond-gattica 0.5.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.rvmrc +1 -0
- data/Gemfile +3 -0
- data/LICENSE +22 -0
- data/README.md +475 -0
- data/Rakefile +24 -0
- data/VERSION.yml +5 -0
- data/gattica.gemspec +72 -0
- data/lib/gattica.rb +35 -0
- data/lib/gattica/account.rb +34 -0
- data/lib/gattica/auth.rb +53 -0
- data/lib/gattica/convertible.rb +39 -0
- data/lib/gattica/data_point.rb +60 -0
- data/lib/gattica/data_set.rb +52 -0
- data/lib/gattica/engine.rb +283 -0
- data/lib/gattica/exceptions.rb +21 -0
- data/lib/gattica/hash_extensions.rb +20 -0
- data/lib/gattica/segment.rb +17 -0
- data/lib/gattica/settings.rb +51 -0
- data/lib/gattica/user.rb +31 -0
- data/test/helper.rb +14 -0
- data/test/settings.rb +28 -0
- data/test/suite.rb +6 -0
- data/test/test_engine.rb +48 -0
- data/test/test_results.rb +23 -0
- data/test/test_user.rb +24 -0
- metadata +124 -0
data/Rakefile
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/testtask'
|
3
|
+
require 'rake/task'
|
4
|
+
|
5
|
+
begin
|
6
|
+
require 'jeweler'
|
7
|
+
Jeweler::Tasks.new do |gemspec|
|
8
|
+
gemspec.name = "gattica"
|
9
|
+
gemspec.summary = "Gattica is a easy to use Ruby Gem for getting data from the Google Analytics API."
|
10
|
+
gemspec.email = "chrisl@seerinteractive.com"
|
11
|
+
gemspec.homepage = "http://github.com/chrisle/gattica"
|
12
|
+
gemspec.description = "Gattica is a easy to use Ruby Gem for getting data from the Google Analytics API. It supports metrics, dimensions, sort, filters, goals, and segments. It can handle accounts with 1000+ profiles, and can return data in CSV, Hash, or JSON"
|
13
|
+
gemspec.authors = ["Christopher Le, et all"]
|
14
|
+
gemspec.add_dependency 'hpricot'
|
15
|
+
end
|
16
|
+
rescue LoadError
|
17
|
+
puts "Jeweler not available. Install it with: sudo gem install jeweler"
|
18
|
+
end
|
19
|
+
|
20
|
+
Rake::TestTask.new do |t|
|
21
|
+
t.libs << 'lib'
|
22
|
+
t.pattern = 'test/**/test_*.rb'
|
23
|
+
t.verbose = false
|
24
|
+
end
|
data/VERSION.yml
ADDED
data/gattica.gemspec
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{betapond-gattica}
|
8
|
+
s.version = "0.5.1"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = [%q{Christopher Le, et all}]
|
12
|
+
s.date = %q{2011-06-06}
|
13
|
+
s.description = %q{Gattica is a easy to use Ruby Gem for getting data from the Google Analytics API. It supports metrics, dimensions, sort, filters, goals, and segments. It can handle accounts with 1000+ profiles, and can return data in CSV, Hash, or JSON}
|
14
|
+
s.email = %q{chrisl@seerinteractive.com}
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"LICENSE",
|
17
|
+
"README.md"
|
18
|
+
]
|
19
|
+
s.files = [
|
20
|
+
".rvmrc",
|
21
|
+
"Gemfile",
|
22
|
+
"LICENSE",
|
23
|
+
"README.md",
|
24
|
+
"Rakefile",
|
25
|
+
"VERSION.yml",
|
26
|
+
"gattica.gemspec",
|
27
|
+
"lib/gattica.rb",
|
28
|
+
"lib/gattica/account.rb",
|
29
|
+
"lib/gattica/auth.rb",
|
30
|
+
"lib/gattica/convertible.rb",
|
31
|
+
"lib/gattica/data_point.rb",
|
32
|
+
"lib/gattica/data_set.rb",
|
33
|
+
"lib/gattica/engine.rb",
|
34
|
+
"lib/gattica/exceptions.rb",
|
35
|
+
"lib/gattica/hash_extensions.rb",
|
36
|
+
"lib/gattica/segment.rb",
|
37
|
+
"lib/gattica/settings.rb",
|
38
|
+
"lib/gattica/user.rb",
|
39
|
+
"test/helper.rb",
|
40
|
+
"test/settings.rb",
|
41
|
+
"test/suite.rb",
|
42
|
+
"test/test_engine.rb",
|
43
|
+
"test/test_results.rb",
|
44
|
+
"test/test_user.rb"
|
45
|
+
]
|
46
|
+
s.homepage = %q{http://github.com/chrisle/gattica}
|
47
|
+
s.require_paths = [%q{lib}]
|
48
|
+
s.rubygems_version = %q{1.8.4}
|
49
|
+
s.summary = %q{Gattica is a easy to use Ruby Gem for getting data from the Google Analytics API.}
|
50
|
+
|
51
|
+
if s.respond_to? :specification_version then
|
52
|
+
s.specification_version = 3
|
53
|
+
|
54
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
55
|
+
s.add_runtime_dependency(%q<gattica>, [">= 0"])
|
56
|
+
s.add_runtime_dependency(%q<test-unit>, [">= 0"])
|
57
|
+
s.add_development_dependency(%q<rake>, [">= 0"])
|
58
|
+
s.add_runtime_dependency(%q<hpricot>, [">= 0"])
|
59
|
+
else
|
60
|
+
s.add_dependency(%q<gattica>, [">= 0"])
|
61
|
+
s.add_dependency(%q<test-unit>, [">= 0"])
|
62
|
+
s.add_dependency(%q<rake>, [">= 0"])
|
63
|
+
s.add_dependency(%q<hpricot>, [">= 0"])
|
64
|
+
end
|
65
|
+
else
|
66
|
+
s.add_dependency(%q<gattica>, [">= 0"])
|
67
|
+
s.add_dependency(%q<test-unit>, [">= 0"])
|
68
|
+
s.add_dependency(%q<rake>, [">= 0"])
|
69
|
+
s.add_dependency(%q<hpricot>, [">= 0"])
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
data/lib/gattica.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
$:.unshift File.dirname(__FILE__) # for use/testing when no gem is installed
|
2
|
+
|
3
|
+
require 'net/http'
|
4
|
+
require 'net/https'
|
5
|
+
require 'uri'
|
6
|
+
require 'cgi'
|
7
|
+
require 'logger'
|
8
|
+
require 'rubygems'
|
9
|
+
require 'hpricot'
|
10
|
+
require 'yaml'
|
11
|
+
|
12
|
+
require 'gattica/engine'
|
13
|
+
require 'gattica/settings'
|
14
|
+
require 'gattica/hash_extensions'
|
15
|
+
require 'gattica/convertible'
|
16
|
+
require 'gattica/exceptions'
|
17
|
+
require 'gattica/user'
|
18
|
+
require 'gattica/auth'
|
19
|
+
require 'gattica/account'
|
20
|
+
require 'gattica/data_set'
|
21
|
+
require 'gattica/data_point'
|
22
|
+
require 'gattica/segment'
|
23
|
+
|
24
|
+
# Gattica is a Ruby library for talking to the Google Analytics API.
|
25
|
+
# Please see the README for usage docs.
|
26
|
+
module Gattica
|
27
|
+
|
28
|
+
VERSION = '0.5.1'
|
29
|
+
|
30
|
+
# Creates a new instance of Gattica::Engine
|
31
|
+
def self.new(*args)
|
32
|
+
Engine.new(*args)
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'hpricot'
|
3
|
+
|
4
|
+
module Gattica
|
5
|
+
|
6
|
+
# Represents an account that an authenticated user has access to
|
7
|
+
|
8
|
+
class Account
|
9
|
+
|
10
|
+
include Convertible
|
11
|
+
|
12
|
+
attr_reader :id, :updated, :title, :table_id, :account_id, :account_name,
|
13
|
+
:profile_id, :web_property_id, :goals
|
14
|
+
|
15
|
+
def initialize(xml)
|
16
|
+
@id = xml.at(:id).inner_html
|
17
|
+
@updated = DateTime.parse(xml.at(:updated).inner_html)
|
18
|
+
@title = xml.at(:title).inner_html
|
19
|
+
@table_id = xml.at('dxp:tableid').inner_html
|
20
|
+
@account_id = xml.at("dxp:property[@name='ga:accountId']").attributes['value'].to_i
|
21
|
+
@account_name = xml.at("dxp:property[@name='ga:accountName']").attributes['value']
|
22
|
+
@profile_id = xml.at("dxp:property[@name='ga:profileId']").attributes['value'].to_i
|
23
|
+
@web_property_id = xml.at("dxp:property[@name='ga:webPropertyId']").attributes['value']
|
24
|
+
@goals = xml.search('ga:goal').collect do |goal| {
|
25
|
+
:active => goal.attributes['active'],
|
26
|
+
:name => goal.attributes['name'],
|
27
|
+
:number => goal.attributes['number'].to_i,
|
28
|
+
:value => goal.attributes['value'].to_f,
|
29
|
+
}
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
data/lib/gattica/auth.rb
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'net/https'
|
3
|
+
|
4
|
+
module Gattica
|
5
|
+
|
6
|
+
# Authenticates a user against the Google Client Login system
|
7
|
+
|
8
|
+
class Auth
|
9
|
+
|
10
|
+
include Convertible
|
11
|
+
|
12
|
+
SCRIPT_NAME = '/accounts/ClientLogin'
|
13
|
+
HEADERS = { 'Content-Type' => 'application/x-www-form-urlencoded', 'User-Agent' => 'Ruby Net::HTTP' } # Google asks that you be nice and provide a user-agent string
|
14
|
+
OPTIONS = { :source => 'gattica', :service => 'analytics' } # Google asks that you provide the name of your app as a 'source' parameter in your POST
|
15
|
+
|
16
|
+
attr_reader :tokens
|
17
|
+
|
18
|
+
# Try to authenticate the user
|
19
|
+
def initialize(http, user)
|
20
|
+
options = OPTIONS.merge(user.to_h)
|
21
|
+
options.extend HashExtensions
|
22
|
+
|
23
|
+
response, data = http.post(SCRIPT_NAME, options.to_query, HEADERS)
|
24
|
+
if response.code != '200'
|
25
|
+
case response.code
|
26
|
+
when '403'
|
27
|
+
raise GatticaError::CouldNotAuthenticate, 'Your email and/or password is not recognized by the Google ClientLogin system (status code: 403)'
|
28
|
+
else
|
29
|
+
raise GatticaError::UnknownAnalyticsError, response.body + " (status code: #{response.code})"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
@tokens = parse_tokens(data)
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
# Parse the authentication tokens out of the response and makes them available as a hash
|
39
|
+
#
|
40
|
+
# tokens[:auth] => Google requires this for every request (added to HTTP headers on GET requests)
|
41
|
+
# tokens[:sid] => Not used
|
42
|
+
# tokens[:lsid] => Not used
|
43
|
+
|
44
|
+
def parse_tokens(data)
|
45
|
+
tokens = {}
|
46
|
+
data.split("\n").each do |t|
|
47
|
+
tokens.merge!({ t.split('=').first.downcase.to_sym => t.split('=').last })
|
48
|
+
end
|
49
|
+
return tokens
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Gattica
|
2
|
+
|
3
|
+
# Common output methods that are sharable
|
4
|
+
|
5
|
+
module Convertible
|
6
|
+
|
7
|
+
# output as hash
|
8
|
+
def to_h
|
9
|
+
output = {}
|
10
|
+
instance_variables.each do |var|
|
11
|
+
output.merge!({ var[1..-1] => instance_variable_get(var) }) unless var == '@xml' # exclude the whole XML dump
|
12
|
+
end
|
13
|
+
output.tap { |h| h.include? HashExtensions }
|
14
|
+
end
|
15
|
+
|
16
|
+
# output nice inspect syntax
|
17
|
+
def to_s
|
18
|
+
to_h.inspect
|
19
|
+
end
|
20
|
+
|
21
|
+
alias inspect to_s
|
22
|
+
|
23
|
+
def to_query
|
24
|
+
to_h.to_query
|
25
|
+
end
|
26
|
+
|
27
|
+
# Return the raw XML (if the object has a @xml instance variable, otherwise convert the object itself to xml)
|
28
|
+
def to_xml
|
29
|
+
if @xml
|
30
|
+
@xml
|
31
|
+
else
|
32
|
+
self.to_xml
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
alias to_yml to_yaml
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'csv'
|
2
|
+
|
3
|
+
module Gattica
|
4
|
+
|
5
|
+
# Represents a single "row" of data containing any number of dimensions, metrics
|
6
|
+
|
7
|
+
class DataPoint
|
8
|
+
|
9
|
+
include Convertible
|
10
|
+
|
11
|
+
attr_reader :id, :updated, :title, :dimensions, :metrics, :xml
|
12
|
+
|
13
|
+
# Parses the XML <entry> element
|
14
|
+
def initialize(xml)
|
15
|
+
@xml = xml.to_s
|
16
|
+
@id = xml.at('id').inner_html
|
17
|
+
@updated = DateTime.parse(xml.at('updated').inner_html)
|
18
|
+
@title = xml.at('title').inner_html
|
19
|
+
@dimensions = xml.search('dxp:dimension').collect do |dimension|
|
20
|
+
{ dimension.attributes['name'].split(':').last.to_sym => dimension.attributes['value'].split(':').last }
|
21
|
+
end
|
22
|
+
@metrics = xml.search('dxp:metric').collect do |metric|
|
23
|
+
{ metric.attributes['name'].split(':').last.to_sym => metric.attributes['value'].split(':').last.to_i }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
# Outputs in Comma Seperated Values format
|
29
|
+
def to_csv(format = :long)
|
30
|
+
output = ''
|
31
|
+
columns = []
|
32
|
+
|
33
|
+
# only output
|
34
|
+
case format
|
35
|
+
when :long
|
36
|
+
[@id, @updated, @title].each { |c| columns << c }
|
37
|
+
end
|
38
|
+
|
39
|
+
# output all dimensions
|
40
|
+
@dimensions.map {|d| d.value}.each { |c| columns << c }
|
41
|
+
|
42
|
+
# output all metrics
|
43
|
+
@metrics.map {|m| m.value}.each { |c| columns << c }
|
44
|
+
|
45
|
+
output = CSV.generate_line(columns)
|
46
|
+
return output
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
def to_yaml
|
51
|
+
{ 'id' => @id,
|
52
|
+
'updated' => @updated,
|
53
|
+
'title' => @title,
|
54
|
+
'dimensions' => @dimensions,
|
55
|
+
'metrics' => @metrics }.to_yaml
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Gattica
|
2
|
+
|
3
|
+
# Encapsulates the data returned by the GA API
|
4
|
+
class DataSet
|
5
|
+
include Convertible
|
6
|
+
|
7
|
+
attr_reader :total_results, :start_index, :items_per_page, :start_date,
|
8
|
+
:end_date, :points, :xml
|
9
|
+
|
10
|
+
def initialize(xml)
|
11
|
+
@xml = xml.to_s
|
12
|
+
@total_results = xml.at('openSearch:totalResults').inner_html.to_i
|
13
|
+
@start_index = xml.at('openSearch:startIndex').inner_html.to_i
|
14
|
+
@items_per_page = xml.at('openSearch:itemsPerPage').inner_html.to_i
|
15
|
+
@start_date = Date.parse(xml.at('dxp:startDate').inner_html)
|
16
|
+
@end_date = Date.parse(xml.at('dxp:endDate').inner_html)
|
17
|
+
@points = xml.search(:entry).collect { |entry| DataPoint.new(entry) }
|
18
|
+
end
|
19
|
+
|
20
|
+
# Returns a string formatted as a CSV containing just the data points.
|
21
|
+
#
|
22
|
+
# == Parameters:
|
23
|
+
# +format=:long+:: Adds id, updated, title to output columns
|
24
|
+
def to_csv(format=:long)
|
25
|
+
output = ''
|
26
|
+
columns = []
|
27
|
+
case format
|
28
|
+
when :long
|
29
|
+
["id", "updated", "title"].each { |c| columns << c }
|
30
|
+
end
|
31
|
+
unless @points.empty? # if there was at least one result
|
32
|
+
@points.first.dimensions.map {|d| d.key}.each { |c| columns << c }
|
33
|
+
@points.first.metrics.map {|m| m.key}.each { |c| columns << c }
|
34
|
+
end
|
35
|
+
output = CSV.generate_line(columns) + "\n"
|
36
|
+
@points.each do |point|
|
37
|
+
output += point.to_csv(format) + "\n"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def to_yaml
|
42
|
+
{ 'total_results' => @total_results,
|
43
|
+
'start_index' => @start_index,
|
44
|
+
'items_per_page' => @items_per_page,
|
45
|
+
'start_date' => @start_date,
|
46
|
+
'end_date' => @end_date,
|
47
|
+
'points' => @points }.to_yaml
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
@@ -0,0 +1,283 @@
|
|
1
|
+
module Gattica
|
2
|
+
class Engine
|
3
|
+
|
4
|
+
attr_reader :user
|
5
|
+
attr_accessor :profile_id, :token
|
6
|
+
|
7
|
+
# Initialize Gattica using username/password or token.
|
8
|
+
#
|
9
|
+
# == Options:
|
10
|
+
# To change the defaults see link:settings.rb
|
11
|
+
# +:debug+:: Send debug info to the logger (default is false)
|
12
|
+
# +:email+:: Your email/login for Google Analytics
|
13
|
+
# +:headers+:: Add additional HTTP headers (default is {} )
|
14
|
+
# +:logger+:: Logger to use (default is STDOUT)
|
15
|
+
# +:password+:: Your password for Google Analytics
|
16
|
+
# +:profile_id+:: Use this Google Analytics profile_id (default is nil)
|
17
|
+
# +:timeout+:: Set Net:HTTP timeout in seconds (default is 300)
|
18
|
+
# +:token+:: Use an authentication token you received before
|
19
|
+
def initialize(options={})
|
20
|
+
@options = Settings::DEFAULT_OPTIONS.merge(options)
|
21
|
+
handle_init_options(@options)
|
22
|
+
create_http_connection()
|
23
|
+
check_init_auth_requirements()
|
24
|
+
# TODO: check that the user has access to the specified profile and show an error here rather than wait for Google to respond with a message
|
25
|
+
end
|
26
|
+
|
27
|
+
# Returns the list of accounts the user has access to. A user may have
|
28
|
+
# multiple accounts on Google Analytics and each account may have multiple
|
29
|
+
# profiles. You need the profile_id in order to get info from GA. If you
|
30
|
+
# don't know the profile_id then use this method to get a list of all them.
|
31
|
+
# Then set the profile_id of your instance and you can make regular calls
|
32
|
+
# from then on.
|
33
|
+
#
|
34
|
+
# ga = Gattica.new({:email => 'johndoe@google.com', :password => 'password'})
|
35
|
+
# ga.accounts
|
36
|
+
# # you parse through the accounts to find the profile_id you need
|
37
|
+
# ga.profile_id = 12345678
|
38
|
+
# # now you can perform a regular search, see Gattica::Engine#get
|
39
|
+
#
|
40
|
+
# If you pass in a profile id when you instantiate Gattica::Search then you won't need to
|
41
|
+
# get the accounts and find a profile_id - you apparently already know it!
|
42
|
+
#
|
43
|
+
# See Gattica::Engine#get to see how to get some data.
|
44
|
+
|
45
|
+
def accounts
|
46
|
+
# if we haven't retrieved the user's accounts yet, get them now and save them
|
47
|
+
if @user_accounts.nil?
|
48
|
+
data = request_default_account_feed
|
49
|
+
xml = Hpricot(data)
|
50
|
+
@user_accounts = xml.search(:entry).collect { |entry| Account.new(entry) }
|
51
|
+
end
|
52
|
+
return @user_accounts
|
53
|
+
end
|
54
|
+
|
55
|
+
# Returns the list of segments available to the authenticated user.
|
56
|
+
#
|
57
|
+
# == Usage
|
58
|
+
# ga = Gattica.new({:email => 'johndoe@google.com', :password => 'password'})
|
59
|
+
# ga.segments # Look up segment id
|
60
|
+
# my_gaid = 'gaid::-5' # Non-paid Search Traffic
|
61
|
+
# ga.profile_id = 12345678 # Set our profile ID
|
62
|
+
#
|
63
|
+
# gs.get({ :start_date => '2008-01-01',
|
64
|
+
# :end_date => '2008-02-01',
|
65
|
+
# :dimensions => 'month',
|
66
|
+
# :metrics => 'views',
|
67
|
+
# :segment => my_gaid })
|
68
|
+
|
69
|
+
def segments
|
70
|
+
if @user_segments.nil?
|
71
|
+
data = request_default_account_feed
|
72
|
+
xml = Hpricot(data)
|
73
|
+
@user_segments = xml.search("dxp:segment").collect { |s| Segment.new(s) }
|
74
|
+
end
|
75
|
+
return @user_segments
|
76
|
+
end
|
77
|
+
|
78
|
+
# This is the method that performs the actual request to get data.
|
79
|
+
#
|
80
|
+
# == Usage
|
81
|
+
#
|
82
|
+
# gs = Gattica.new({:email => 'johndoe@google.com', :password => 'password', :profile_id => 123456})
|
83
|
+
# gs.get({ :start_date => '2008-01-01',
|
84
|
+
# :end_date => '2008-02-01',
|
85
|
+
# :dimensions => 'browser',
|
86
|
+
# :metrics => 'pageviews',
|
87
|
+
# :sort => 'pageviews',
|
88
|
+
# :filters => ['browser == Firefox']})
|
89
|
+
#
|
90
|
+
# == Input
|
91
|
+
#
|
92
|
+
# When calling +get+ you'll pass in a hash of options. For a description of what these mean to
|
93
|
+
# Google Analytics, see http://code.google.com/apis/analytics/docs
|
94
|
+
#
|
95
|
+
# Required values are:
|
96
|
+
#
|
97
|
+
# * +start_date+ => Beginning of the date range to search within
|
98
|
+
# * +end_date+ => End of the date range to search within
|
99
|
+
#
|
100
|
+
# Optional values are:
|
101
|
+
#
|
102
|
+
# * +dimensions+ => an array of GA dimensions (without the ga: prefix)
|
103
|
+
# * +metrics+ => an array of GA metrics (without the ga: prefix)
|
104
|
+
# * +filter+ => an array of GA dimensions/metrics you want to filter by (without the ga: prefix)
|
105
|
+
# * +sort+ => an array of GA dimensions/metrics you want to sort by (without the ga: prefix)
|
106
|
+
#
|
107
|
+
# == Exceptions
|
108
|
+
#
|
109
|
+
# If a user doesn't have access to the +profile_id+ you specified, you'll receive an error.
|
110
|
+
# Likewise, if you attempt to access a dimension or metric that doesn't exist, you'll get an
|
111
|
+
# error back from Google Analytics telling you so.
|
112
|
+
|
113
|
+
def get(args={})
|
114
|
+
args = validate_and_clean(Settings::DEFAULT_ARGS.merge(args))
|
115
|
+
query_string = build_query_string(args,@profile_id)
|
116
|
+
@logger.debug(query_string) if @debug
|
117
|
+
data = do_http_get("/analytics/feeds/data?#{query_string}")
|
118
|
+
#data = do_http_get("/analytics/feeds/data?ids=ga%3A915568&metrics=ga%3Avisits&segment=gaid%3A%3A-7&start-date=2010-03-29&end-date=2010-03-29&max-results=50")
|
119
|
+
return DataSet.new(Hpricot.XML(data))
|
120
|
+
end
|
121
|
+
|
122
|
+
|
123
|
+
# Since google wants the token to appear in any HTTP call's header, we have to set that header
|
124
|
+
# again any time @token is changed so we override the default writer (note that you need to set
|
125
|
+
# @token with self.token= instead of @token=)
|
126
|
+
|
127
|
+
def token=(token)
|
128
|
+
@token = token
|
129
|
+
set_http_headers
|
130
|
+
end
|
131
|
+
|
132
|
+
######################################################################
|
133
|
+
private
|
134
|
+
|
135
|
+
# Gets the default account feed from Google
|
136
|
+
def request_default_account_feed
|
137
|
+
if @default_account_feed.nil?
|
138
|
+
@default_account_feed = do_http_get('/analytics/feeds/accounts/default')
|
139
|
+
end
|
140
|
+
return @default_account_feed
|
141
|
+
end
|
142
|
+
|
143
|
+
# Does the work of making HTTP calls and then going through a suite of tests on the response to make
|
144
|
+
# sure it's valid and not an error
|
145
|
+
|
146
|
+
def do_http_get(query_string)
|
147
|
+
response, data = @http.get(query_string, @headers)
|
148
|
+
|
149
|
+
# error checking
|
150
|
+
if response.code != '200'
|
151
|
+
case response.code
|
152
|
+
when '400'
|
153
|
+
raise GatticaError::AnalyticsError, response.body + " (status code: #{response.code})"
|
154
|
+
when '401'
|
155
|
+
raise GatticaError::InvalidToken, "Your authorization token is invalid or has expired (status code: #{response.code})"
|
156
|
+
else # some other unknown error
|
157
|
+
raise GatticaError::UnknownAnalyticsError, response.body + " (status code: #{response.code})"
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
return data
|
162
|
+
end
|
163
|
+
|
164
|
+
|
165
|
+
# Sets up the HTTP headers that Google expects (this is called any time @token is set either by Gattica
|
166
|
+
# or manually by the user since the header must include the token)
|
167
|
+
def set_http_headers
|
168
|
+
@headers['Authorization'] = "GoogleLogin auth=#{@token}"
|
169
|
+
@headers['GData-Version']= '2'
|
170
|
+
end
|
171
|
+
|
172
|
+
|
173
|
+
# Creates a valid query string for GA
|
174
|
+
def build_query_string(args,profile)
|
175
|
+
output = "ids=ga:#{profile}&start-date=#{args[:start_date]}&end-date=#{args[:end_date]}"
|
176
|
+
if (start_index = args[:start_index].to_i) > 0
|
177
|
+
output += "&start-index=#{start_index}"
|
178
|
+
end
|
179
|
+
unless args[:dimensions].empty?
|
180
|
+
output += '&dimensions=' + args[:dimensions].collect do |dimension|
|
181
|
+
"ga:#{dimension}"
|
182
|
+
end.join(',')
|
183
|
+
end
|
184
|
+
unless args[:metrics].empty?
|
185
|
+
output += '&metrics=' + args[:metrics].collect do |metric|
|
186
|
+
"ga:#{metric}"
|
187
|
+
end.join(',')
|
188
|
+
end
|
189
|
+
unless args[:sort].empty?
|
190
|
+
output += '&sort=' + args[:sort].collect do |sort|
|
191
|
+
sort[0..0] == '-' ? "-ga:#{sort[1..-1]}" : "ga:#{sort}" # if the first character is a dash, move it before the ga:
|
192
|
+
end.join(',')
|
193
|
+
end
|
194
|
+
unless args[:segment].nil?
|
195
|
+
output += "&segment=#{args[:segment]}"
|
196
|
+
end
|
197
|
+
unless args[:max_results].nil?
|
198
|
+
output += "&max-results=#{args[:max_results]}"
|
199
|
+
end
|
200
|
+
|
201
|
+
# TODO: update so that in regular expression filters (=~ and !~), any initial special characters in the regular expression aren't also picked up as part of the operator (doesn't cause a problem, but just feels dirty)
|
202
|
+
unless args[:filters].empty? # filters are a little more complicated because they can have all kinds of modifiers
|
203
|
+
output += '&filters=' + args[:filters].collect do |filter|
|
204
|
+
match, name, operator, expression = *filter.match(/^(\w*)\s*([=!<>~@]*)\s*(.*)$/) # splat the resulting Match object to pull out the parts automatically
|
205
|
+
unless name.empty? || operator.empty? || expression.empty? # make sure they all contain something
|
206
|
+
"ga:#{name}#{CGI::escape(operator.gsub(/ /,''))}#{CGI::escape(expression)}" # remove any whitespace from the operator before output
|
207
|
+
else
|
208
|
+
raise GatticaError::InvalidFilter, "The filter '#{filter}' is invalid. Filters should look like 'browser == Firefox' or 'browser==Firefox'"
|
209
|
+
end
|
210
|
+
end.join(';')
|
211
|
+
end
|
212
|
+
return output
|
213
|
+
end
|
214
|
+
|
215
|
+
|
216
|
+
# Validates that the args passed to +get+ are valid
|
217
|
+
def validate_and_clean(args)
|
218
|
+
|
219
|
+
raise GatticaError::MissingStartDate, ':start_date is required' if args[:start_date].nil? || args[:start_date].empty?
|
220
|
+
raise GatticaError::MissingEndDate, ':end_date is required' if args[:end_date].nil? || args[:end_date].empty?
|
221
|
+
raise GatticaError::TooManyDimensions, 'You can only have a maximum of 7 dimensions' if args[:dimensions] && (args[:dimensions].is_a?(Array) && args[:dimensions].length > 7)
|
222
|
+
raise GatticaError::TooManyMetrics, 'You can only have a maximum of 10 metrics' if args[:metrics] && (args[:metrics].is_a?(Array) && args[:metrics].length > 10)
|
223
|
+
|
224
|
+
possible = args[:dimensions] + args[:metrics]
|
225
|
+
|
226
|
+
# make sure that the user is only trying to sort fields that they've previously included with dimensions and metrics
|
227
|
+
if args[:sort]
|
228
|
+
missing = args[:sort].find_all do |arg|
|
229
|
+
!possible.include? arg.gsub(/^-/,'') # remove possible minuses from any sort params
|
230
|
+
end
|
231
|
+
unless missing.empty?
|
232
|
+
raise GatticaError::InvalidSort, "You are trying to sort by fields that are not in the available dimensions or metrics: #{missing.join(', ')}"
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
# make sure that the user is only trying to filter fields that are in dimensions or metrics
|
237
|
+
if args[:filters]
|
238
|
+
missing = args[:filters].find_all do |arg|
|
239
|
+
!possible.include? arg.match(/^\w*/).to_s # get the name of the filter and compare
|
240
|
+
end
|
241
|
+
unless missing.empty?
|
242
|
+
raise GatticaError::InvalidSort, "You are trying to filter by fields that are not in the available dimensions or metrics: #{missing.join(', ')}"
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
return args
|
247
|
+
end
|
248
|
+
|
249
|
+
def create_http_connection
|
250
|
+
port = Settings::USE_SSL ? Settings::SSL_PORT : Settings::NON_SSL_PORT
|
251
|
+
@http = Net::HTTP.new(Settings::SERVER, port)
|
252
|
+
@http.use_ssl = Settings::USE_SSL
|
253
|
+
@http.set_debug_output $stdout if @options[:debug]
|
254
|
+
@http.read_timeout = @options[:timeout] if defined? @options[:timeout]
|
255
|
+
end
|
256
|
+
|
257
|
+
# Sets instance variables from options given during initialization and
|
258
|
+
def handle_init_options(options)
|
259
|
+
@logger = options[:logger]
|
260
|
+
@profile_id = options[:profile_id]
|
261
|
+
@user_accounts = nil # filled in later if the user ever calls Gattica::Engine#accounts
|
262
|
+
@user_segments = nil
|
263
|
+
@headers = { }.merge(options[:headers]) # headers used for any HTTP requests (Google requires a special 'Authorization' header which is set any time @token is set)
|
264
|
+
@default_account_feed = nil
|
265
|
+
|
266
|
+
end
|
267
|
+
|
268
|
+
# If the authorization is a email and password then create User objects
|
269
|
+
# or if it's a previous token, use that. Else, raise exception.
|
270
|
+
def check_init_auth_requirements
|
271
|
+
if ((defined? @options[:email]) && (defined? @options[:password]))
|
272
|
+
@user = User.new(@options[:email], @options[:password])
|
273
|
+
@auth = Auth.new(@http, user)
|
274
|
+
self.token = @auth.tokens[:auth]
|
275
|
+
elsif (defined? @options[:token])
|
276
|
+
self.token = @options[:token]
|
277
|
+
else
|
278
|
+
raise GatticaError::NoLoginOrToken, 'An email and password or an authentication token is required to initialize Gattica.'
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
end
|
283
|
+
end
|