chrisle-gattica 0.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/Gemfile +6 -0
- data/Gemfile.lock +21 -0
- data/LICENSE +22 -0
- data/README.md +506 -0
- data/Rakefile +24 -0
- data/VERSION.yml +5 -0
- data/chrisle-gattica.gemspec +68 -0
- data/lib/gattica.rb +35 -0
- data/lib/gattica/account.rb +54 -0
- data/lib/gattica/auth.rb +47 -0
- data/lib/gattica/convertible.rb +39 -0
- data/lib/gattica/data_point.rb +73 -0
- data/lib/gattica/data_set.rb +57 -0
- data/lib/gattica/engine.rb +295 -0
- data/lib/gattica/exceptions.rb +21 -0
- data/lib/gattica/goals.rb +33 -0
- data/lib/gattica/hash_extensions.rb +20 -0
- data/lib/gattica/profiles.rb +33 -0
- data/lib/gattica/segment.rb +17 -0
- data/lib/gattica/settings.rb +34 -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 +109 -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 = "chrisle-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
@@ -0,0 +1,68 @@
|
|
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 = "chrisle-gattica"
|
8
|
+
s.version = "0.6.3"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Christopher Le, et all"]
|
12
|
+
s.date = "2012-06-10"
|
13
|
+
s.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"
|
14
|
+
s.email = "chrisl@seerinteractive.com"
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"LICENSE",
|
17
|
+
"README.md"
|
18
|
+
]
|
19
|
+
s.files = [
|
20
|
+
"Gemfile",
|
21
|
+
"Gemfile.lock",
|
22
|
+
"LICENSE",
|
23
|
+
"README.md",
|
24
|
+
"Rakefile",
|
25
|
+
"VERSION.yml",
|
26
|
+
"chrisle-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/goals.rb",
|
36
|
+
"lib/gattica/hash_extensions.rb",
|
37
|
+
"lib/gattica/profiles.rb",
|
38
|
+
"lib/gattica/segment.rb",
|
39
|
+
"lib/gattica/settings.rb",
|
40
|
+
"lib/gattica/user.rb",
|
41
|
+
"test/helper.rb",
|
42
|
+
"test/settings.rb",
|
43
|
+
"test/suite.rb",
|
44
|
+
"test/test_engine.rb",
|
45
|
+
"test/test_results.rb",
|
46
|
+
"test/test_user.rb"
|
47
|
+
]
|
48
|
+
s.homepage = "http://github.com/chrisle/gattica"
|
49
|
+
s.require_paths = ["lib"]
|
50
|
+
s.rubygems_version = "1.8.24"
|
51
|
+
s.summary = "Gattica is a easy to use Ruby Gem for getting data from the Google Analytics API."
|
52
|
+
|
53
|
+
if s.respond_to? :specification_version then
|
54
|
+
s.specification_version = 3
|
55
|
+
|
56
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
57
|
+
s.add_runtime_dependency(%q<test-unit>, [">= 0"])
|
58
|
+
s.add_runtime_dependency(%q<hpricot>, [">= 0"])
|
59
|
+
else
|
60
|
+
s.add_dependency(%q<test-unit>, [">= 0"])
|
61
|
+
s.add_dependency(%q<hpricot>, [">= 0"])
|
62
|
+
end
|
63
|
+
else
|
64
|
+
s.add_dependency(%q<test-unit>, [">= 0"])
|
65
|
+
s.add_dependency(%q<hpricot>, [">= 0"])
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
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.6.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,54 @@
|
|
1
|
+
module Gattica
|
2
|
+
class Account
|
3
|
+
include Convertible
|
4
|
+
|
5
|
+
attr_reader :id, :updated, :title, :table_id, :account_id, :account_name,
|
6
|
+
:profile_id, :web_property_id, :goals
|
7
|
+
|
8
|
+
def initialize(xml)
|
9
|
+
@id = xml.at("link[@rel='self']").attributes['href']
|
10
|
+
@updated = DateTime.parse(xml.at(:updated).inner_html)
|
11
|
+
@account_id = find_account_id(xml)
|
12
|
+
|
13
|
+
@title = xpath_value(xml, "dxp:property[@name='ga:profileName']")
|
14
|
+
@table_id = xpath_value(xml, "dxp:property[@name='dxp:tableId']")
|
15
|
+
@profile_id = find_profile_id(xml)
|
16
|
+
@web_property_id = xpath_value(xml, "dxp:property[@name='ga:webPropertyId']")
|
17
|
+
@goals = []
|
18
|
+
end
|
19
|
+
|
20
|
+
def xpath_value(xml, xpath)
|
21
|
+
xml.at(xpath).attributes['value']
|
22
|
+
end
|
23
|
+
|
24
|
+
def find_account_id(xml)
|
25
|
+
xml.at("dxp:property[@name='ga:accountId']").attributes['value'].to_i
|
26
|
+
end
|
27
|
+
|
28
|
+
def find_account_name(xml)
|
29
|
+
xml.at("dxp:property[@name='ga:accountName']").attributes['value']
|
30
|
+
end
|
31
|
+
|
32
|
+
def find_profile_id(xml)
|
33
|
+
xml.at("dxp:property[@name='ga:profileId']").attributes['value'].to_i
|
34
|
+
end
|
35
|
+
|
36
|
+
def set_account_name(account_feed_entry)
|
37
|
+
if @account_id == find_account_id(account_feed_entry)
|
38
|
+
@account_name = find_account_name(account_feed_entry)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def set_goals(goals_feed_entry)
|
43
|
+
if @profile_id == find_profile_id(goals_feed_entry)
|
44
|
+
goal = goals_feed_entry.search('ga:goal').first
|
45
|
+
@goals.push({
|
46
|
+
:active => goal.attributes['active'],
|
47
|
+
:name => goal.attributes['name'],
|
48
|
+
:number => goal.attributes['number'].to_i,
|
49
|
+
:value => goal.attributes['value'].to_f
|
50
|
+
})
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
data/lib/gattica/auth.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
module Gattica
|
2
|
+
class Auth
|
3
|
+
include Convertible
|
4
|
+
|
5
|
+
SCRIPT_NAME = '/accounts/ClientLogin'
|
6
|
+
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
|
7
|
+
OPTIONS = { :source => 'gattica', :service => 'analytics' } # Google asks that you provide the name of your app as a 'source' parameter in your POST
|
8
|
+
|
9
|
+
attr_reader :tokens
|
10
|
+
|
11
|
+
# Try to authenticate the user
|
12
|
+
def initialize(http, user)
|
13
|
+
options = OPTIONS.merge(user.to_h)
|
14
|
+
options.extend HashExtensions
|
15
|
+
|
16
|
+
response = http.post(SCRIPT_NAME, options.to_query, HEADERS)
|
17
|
+
data = response.body ||= ''
|
18
|
+
if response.code != '200'
|
19
|
+
case response.code
|
20
|
+
when '403'
|
21
|
+
raise GatticaError::CouldNotAuthenticate, 'Your email and/or password is not recognized by the Google ClientLogin system (status code: 403)'
|
22
|
+
else
|
23
|
+
raise GatticaError::UnknownAnalyticsError, response.body + " (status code: #{response.code})"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
@tokens = parse_tokens(data)
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
# Parse the authentication tokens out of the response and makes them available as a hash
|
33
|
+
#
|
34
|
+
# tokens[:auth] => Google requires this for every request (added to HTTP headers on GET requests)
|
35
|
+
# tokens[:sid] => Not used
|
36
|
+
# tokens[:lsid] => Not used
|
37
|
+
|
38
|
+
def parse_tokens(data)
|
39
|
+
tokens = {}
|
40
|
+
data.split("\n").each do |t|
|
41
|
+
tokens.merge!({ t.split('=').first.downcase.to_sym => t.split('=').last })
|
42
|
+
end
|
43
|
+
return tokens
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
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,73 @@
|
|
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_f }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
# Outputs in Comma Seperated Values format
|
29
|
+
def to_csv(format = :short)
|
30
|
+
output = ''
|
31
|
+
columns = []
|
32
|
+
|
33
|
+
# only output
|
34
|
+
case format
|
35
|
+
when :long
|
36
|
+
|
37
|
+
[@id, @updated, @title].each { |c| columns << c }
|
38
|
+
end
|
39
|
+
|
40
|
+
# output all dimensions
|
41
|
+
|
42
|
+
@dimensions.map {|d| d.values.first}.each { |c| columns << c }
|
43
|
+
# output all metrics
|
44
|
+
@metrics.map {|m| m.values.first}.each { |c| columns << c }
|
45
|
+
|
46
|
+
output = CSV.generate_line(columns)
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
def to_yaml
|
52
|
+
{ 'id' => @id,
|
53
|
+
'updated' => @updated,
|
54
|
+
'title' => @title,
|
55
|
+
'dimensions' => @dimensions,
|
56
|
+
'metrics' => @metrics }.to_yaml
|
57
|
+
end
|
58
|
+
|
59
|
+
def to_hash
|
60
|
+
|
61
|
+
res_hash = {}
|
62
|
+
|
63
|
+
@dimensions.each{|d| res_hash.merge!(d) }
|
64
|
+
# output all metrics
|
65
|
+
@metrics.each{|m| res_hash.merge!(m) }
|
66
|
+
res_hash
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
@@ -0,0 +1,57 @@
|
|
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=:short)
|
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.keys.first}.each { |c| columns << c }
|
33
|
+
@points.first.metrics.map {|m| m.keys.first}.each { |c| columns << c }
|
34
|
+
end
|
35
|
+
output = CSV.generate_line(columns)
|
36
|
+
@points.each do |point|
|
37
|
+
output += point.to_csv(format)
|
38
|
+
end
|
39
|
+
output
|
40
|
+
end
|
41
|
+
|
42
|
+
def to_yaml
|
43
|
+
{ 'total_results' => @total_results,
|
44
|
+
'start_index' => @start_index,
|
45
|
+
'items_per_page' => @items_per_page,
|
46
|
+
'start_date' => @start_date,
|
47
|
+
'end_date' => @end_date,
|
48
|
+
'points' => @points }.to_yaml
|
49
|
+
end
|
50
|
+
|
51
|
+
def to_hash
|
52
|
+
@points.map(&:to_hash)
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
@@ -0,0 +1,295 @@
|
|
1
|
+
module Gattica
|
2
|
+
class Engine
|
3
|
+
|
4
|
+
attr_reader :user
|
5
|
+
attr_accessor :profile_id, :token, :user_accounts
|
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('www.google.com')
|
23
|
+
check_init_auth_requirements()
|
24
|
+
end
|
25
|
+
|
26
|
+
# Returns the list of accounts the user has access to. A user may have
|
27
|
+
# multiple accounts on Google Analytics and each account may have multiple
|
28
|
+
# profiles. You need the profile_id in order to get info from GA. If you
|
29
|
+
# don't know the profile_id then use this method to get a list of all them.
|
30
|
+
# Then set the profile_id of your instance and you can make regular calls
|
31
|
+
# from then on.
|
32
|
+
#
|
33
|
+
# ga = Gattica.new({:email => 'johndoe@google.com', :password => 'password'})
|
34
|
+
# ga.accounts
|
35
|
+
# # you parse through the accounts to find the profile_id you need
|
36
|
+
# ga.profile_id = 12345678
|
37
|
+
# # now you can perform a regular search, see Gattica::Engine#get
|
38
|
+
#
|
39
|
+
# If you pass in a profile id when you instantiate Gattica::Search then you won't need to
|
40
|
+
# get the accounts and find a profile_id - you apparently already know it!
|
41
|
+
#
|
42
|
+
# See Gattica::Engine#get to see how to get some data.
|
43
|
+
|
44
|
+
def accounts
|
45
|
+
if @user_accounts.nil?
|
46
|
+
create_http_connection('www.googleapis.com')
|
47
|
+
|
48
|
+
# get profiles
|
49
|
+
response = do_http_get("/analytics/v2.4/management/accounts/~all/webproperties/~all/profiles?max-results=10000")
|
50
|
+
xml = Hpricot(response)
|
51
|
+
@user_accounts = xml.search(:entry).collect { |profile_xml|
|
52
|
+
Account.new(profile_xml)
|
53
|
+
}
|
54
|
+
|
55
|
+
# Fill in the goals
|
56
|
+
response = do_http_get("/analytics/v2.4/management/accounts/~all/webproperties/~all/profiles/~all/goals?max-results=10000")
|
57
|
+
xml = Hpricot(response)
|
58
|
+
@user_accounts.each do |ua|
|
59
|
+
xml.search(:entry).each { |e| ua.set_goals(e) }
|
60
|
+
end
|
61
|
+
|
62
|
+
# Fill in the account name
|
63
|
+
response = do_http_get("/analytics/v2.4/management/accounts?max-results=10000")
|
64
|
+
xml = Hpricot(response)
|
65
|
+
@user_accounts.each do |ua|
|
66
|
+
xml.search(:entry).each { |e| ua.set_account_name(e) }
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
@user_accounts
|
71
|
+
end
|
72
|
+
|
73
|
+
# Returns the list of segments available to the authenticated user.
|
74
|
+
#
|
75
|
+
# == Usage
|
76
|
+
# ga = Gattica.new({:email => 'johndoe@google.com', :password => 'password'})
|
77
|
+
# ga.segments # Look up segment id
|
78
|
+
# my_gaid = 'gaid::-5' # Non-paid Search Traffic
|
79
|
+
# ga.profile_id = 12345678 # Set our profile ID
|
80
|
+
#
|
81
|
+
# gs.get({ :start_date => '2008-01-01',
|
82
|
+
# :end_date => '2008-02-01',
|
83
|
+
# :dimensions => 'month',
|
84
|
+
# :metrics => 'views',
|
85
|
+
# :segment => my_gaid })
|
86
|
+
|
87
|
+
def segments
|
88
|
+
if @user_segments.nil?
|
89
|
+
response = do_http_get("/analytics/v2.4/management/segments?max-results=10000")
|
90
|
+
xml = Hpricot(response)
|
91
|
+
@user_segments = xml.search("dxp:segment").collect { |s|
|
92
|
+
Segment.new(s)
|
93
|
+
}
|
94
|
+
end
|
95
|
+
return @user_segments
|
96
|
+
end
|
97
|
+
|
98
|
+
# This is the method that performs the actual request to get data.
|
99
|
+
#
|
100
|
+
# == Usage
|
101
|
+
#
|
102
|
+
# gs = Gattica.new({:email => 'johndoe@google.com', :password => 'password', :profile_id => 123456})
|
103
|
+
# gs.get({ :start_date => '2008-01-01',
|
104
|
+
# :end_date => '2008-02-01',
|
105
|
+
# :dimensions => 'browser',
|
106
|
+
# :metrics => 'pageviews',
|
107
|
+
# :sort => 'pageviews',
|
108
|
+
# :filters => ['browser == Firefox']})
|
109
|
+
#
|
110
|
+
# == Input
|
111
|
+
#
|
112
|
+
# When calling +get+ you'll pass in a hash of options. For a description of what these mean to
|
113
|
+
# Google Analytics, see http://code.google.com/apis/analytics/docs
|
114
|
+
#
|
115
|
+
# Required values are:
|
116
|
+
#
|
117
|
+
# * +start_date+ => Beginning of the date range to search within
|
118
|
+
# * +end_date+ => End of the date range to search within
|
119
|
+
#
|
120
|
+
# Optional values are:
|
121
|
+
#
|
122
|
+
# * +dimensions+ => an array of GA dimensions (without the ga: prefix)
|
123
|
+
# * +metrics+ => an array of GA metrics (without the ga: prefix)
|
124
|
+
# * +filter+ => an array of GA dimensions/metrics you want to filter by (without the ga: prefix)
|
125
|
+
# * +sort+ => an array of GA dimensions/metrics you want to sort by (without the ga: prefix)
|
126
|
+
#
|
127
|
+
# == Exceptions
|
128
|
+
#
|
129
|
+
# If a user doesn't have access to the +profile_id+ you specified, you'll receive an error.
|
130
|
+
# Likewise, if you attempt to access a dimension or metric that doesn't exist, you'll get an
|
131
|
+
# error back from Google Analytics telling you so.
|
132
|
+
|
133
|
+
def get(args={})
|
134
|
+
args = validate_and_clean(Settings::DEFAULT_ARGS.merge(args))
|
135
|
+
query_string = build_query_string(args,@profile_id)
|
136
|
+
@logger.debug(query_string) if @debug
|
137
|
+
create_http_connection('www.googleapis.com')
|
138
|
+
data = do_http_get("/analytics/v2.4/data?#{query_string}")
|
139
|
+
return DataSet.new(Hpricot.XML(data))
|
140
|
+
end
|
141
|
+
|
142
|
+
|
143
|
+
# Since google wants the token to appear in any HTTP call's header, we have to set that header
|
144
|
+
# again any time @token is changed so we override the default writer (note that you need to set
|
145
|
+
# @token with self.token= instead of @token=)
|
146
|
+
|
147
|
+
def token=(token)
|
148
|
+
@token = token
|
149
|
+
set_http_headers
|
150
|
+
end
|
151
|
+
|
152
|
+
######################################################################
|
153
|
+
private
|
154
|
+
|
155
|
+
# Does the work of making HTTP calls and then going through a suite of tests on the response to make
|
156
|
+
# sure it's valid and not an error
|
157
|
+
|
158
|
+
def do_http_get(query_string)
|
159
|
+
response = @http.get(query_string, @headers)
|
160
|
+
|
161
|
+
# error checking
|
162
|
+
if response.code != '200'
|
163
|
+
case response.code
|
164
|
+
when '400'
|
165
|
+
raise GatticaError::AnalyticsError, response.body + " (status code: #{response.code})"
|
166
|
+
when '401'
|
167
|
+
raise GatticaError::InvalidToken, "Your authorization token is invalid or has expired (status code: #{response.code})"
|
168
|
+
else # some other unknown error
|
169
|
+
raise GatticaError::UnknownAnalyticsError, response.body + " (status code: #{response.code})"
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
return response.body
|
174
|
+
end
|
175
|
+
|
176
|
+
|
177
|
+
# Sets up the HTTP headers that Google expects (this is called any time @token is set either by Gattica
|
178
|
+
# or manually by the user since the header must include the token)
|
179
|
+
def set_http_headers
|
180
|
+
@headers['Authorization'] = "GoogleLogin auth=#{@token}"
|
181
|
+
@headers['GData-Version']= '2'
|
182
|
+
end
|
183
|
+
|
184
|
+
|
185
|
+
# Creates a valid query string for GA
|
186
|
+
def build_query_string(args,profile)
|
187
|
+
output = "ids=ga:#{profile}&start-date=#{args[:start_date]}&end-date=#{args[:end_date]}"
|
188
|
+
if (start_index = args[:start_index].to_i) > 0
|
189
|
+
output += "&start-index=#{start_index}"
|
190
|
+
end
|
191
|
+
unless args[:dimensions].empty?
|
192
|
+
output += '&dimensions=' + args[:dimensions].collect do |dimension|
|
193
|
+
"ga:#{dimension}"
|
194
|
+
end.join(',')
|
195
|
+
end
|
196
|
+
unless args[:metrics].empty?
|
197
|
+
output += '&metrics=' + args[:metrics].collect do |metric|
|
198
|
+
"ga:#{metric}"
|
199
|
+
end.join(',')
|
200
|
+
end
|
201
|
+
unless args[:sort].empty?
|
202
|
+
output += '&sort=' + args[:sort].collect do |sort|
|
203
|
+
sort[0..0] == '-' ? "-ga:#{sort[1..-1]}" : "ga:#{sort}" # if the first character is a dash, move it before the ga:
|
204
|
+
end.join(',')
|
205
|
+
end
|
206
|
+
unless args[:segment].nil?
|
207
|
+
output += "&segment=#{args[:segment]}"
|
208
|
+
end
|
209
|
+
unless args[:max_results].nil?
|
210
|
+
output += "&max-results=#{args[:max_results]}"
|
211
|
+
end
|
212
|
+
|
213
|
+
# 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)
|
214
|
+
unless args[:filters].empty? # filters are a little more complicated because they can have all kinds of modifiers
|
215
|
+
output += '&filters=' + args[:filters].collect do |filter|
|
216
|
+
match, name, operator, expression = *filter.match(/^(\w*)\s*([=!<>~@]*)\s*(.*)$/) # splat the resulting Match object to pull out the parts automatically
|
217
|
+
unless name.empty? || operator.empty? || expression.empty? # make sure they all contain something
|
218
|
+
"ga:#{name}#{CGI::escape(operator.gsub(/ /,''))}#{CGI::escape(expression)}" # remove any whitespace from the operator before output
|
219
|
+
else
|
220
|
+
raise GatticaError::InvalidFilter, "The filter '#{filter}' is invalid. Filters should look like 'browser == Firefox' or 'browser==Firefox'"
|
221
|
+
end
|
222
|
+
end.join(';')
|
223
|
+
end
|
224
|
+
return output
|
225
|
+
end
|
226
|
+
|
227
|
+
|
228
|
+
# Validates that the args passed to +get+ are valid
|
229
|
+
def validate_and_clean(args)
|
230
|
+
|
231
|
+
raise GatticaError::MissingStartDate, ':start_date is required' if args[:start_date].nil? || args[:start_date].empty?
|
232
|
+
raise GatticaError::MissingEndDate, ':end_date is required' if args[:end_date].nil? || args[:end_date].empty?
|
233
|
+
raise GatticaError::TooManyDimensions, 'You can only have a maximum of 7 dimensions' if args[:dimensions] && (args[:dimensions].is_a?(Array) && args[:dimensions].length > 7)
|
234
|
+
raise GatticaError::TooManyMetrics, 'You can only have a maximum of 10 metrics' if args[:metrics] && (args[:metrics].is_a?(Array) && args[:metrics].length > 10)
|
235
|
+
|
236
|
+
possible = args[:dimensions] + args[:metrics]
|
237
|
+
|
238
|
+
# make sure that the user is only trying to sort fields that they've previously included with dimensions and metrics
|
239
|
+
if args[:sort]
|
240
|
+
missing = args[:sort].find_all do |arg|
|
241
|
+
!possible.include? arg.gsub(/^-/,'') # remove possible minuses from any sort params
|
242
|
+
end
|
243
|
+
unless missing.empty?
|
244
|
+
raise GatticaError::InvalidSort, "You are trying to sort by fields that are not in the available dimensions or metrics: #{missing.join(', ')}"
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
# make sure that the user is only trying to filter fields that are in dimensions or metrics
|
249
|
+
if args[:filters]
|
250
|
+
missing = args[:filters].find_all do |arg|
|
251
|
+
!possible.include? arg.match(/^\w*/).to_s # get the name of the filter and compare
|
252
|
+
end
|
253
|
+
unless missing.empty?
|
254
|
+
raise GatticaError::InvalidSort, "You are trying to filter by fields that are not in the available dimensions or metrics: #{missing.join(', ')}"
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
return args
|
259
|
+
end
|
260
|
+
|
261
|
+
def create_http_connection(server)
|
262
|
+
port = Settings::USE_SSL ? Settings::SSL_PORT : Settings::NON_SSL_PORT
|
263
|
+
@http = Net::HTTP.new(server, port)
|
264
|
+
@http.use_ssl = Settings::USE_SSL
|
265
|
+
@http.set_debug_output $stdout if @options[:debug]
|
266
|
+
@http.read_timeout = @options[:timeout] if @options[:timeout]
|
267
|
+
end
|
268
|
+
|
269
|
+
# Sets instance variables from options given during initialization and
|
270
|
+
def handle_init_options(options)
|
271
|
+
@logger = options[:logger]
|
272
|
+
@profile_id = options[:profile_id]
|
273
|
+
@user_accounts = nil # filled in later if the user ever calls Gattica::Engine#accounts
|
274
|
+
@user_segments = nil
|
275
|
+
@headers = { }.merge(options[:headers]) # headers used for any HTTP requests (Google requires a special 'Authorization' header which is set any time @token is set)
|
276
|
+
@default_account_feed = nil
|
277
|
+
|
278
|
+
end
|
279
|
+
|
280
|
+
# If the authorization is a email and password then create User objects
|
281
|
+
# or if it's a previous token, use that. Else, raise exception.
|
282
|
+
def check_init_auth_requirements
|
283
|
+
if @options[:token].to_s.length > 200
|
284
|
+
self.token = @options[:token]
|
285
|
+
elsif @options[:email] && @options[:password]
|
286
|
+
@user = User.new(@options[:email], @options[:password])
|
287
|
+
@auth = Auth.new(@http, user)
|
288
|
+
self.token = @auth.tokens[:auth]
|
289
|
+
else
|
290
|
+
raise GatticaError::NoLoginOrToken, 'An email and password or an authentication token is required to initialize Gattica.'
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
end
|
295
|
+
end
|