Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
tree: cd45b3caee
Fetching contributors…

Cannot retrieve contributors at this time

307 lines (267 sloc) 13.148 kB
module Gattica
class Engine
attr_reader :user
attr_accessor :profile_id, :token, :user_accounts
# Initialize Gattica using username/password or token.
#
# == Options:
# To change the defaults see link:settings.rb
# +:debug+:: Send debug info to the logger (default is false)
# +:email+:: Your email/login for Google Analytics
# +:headers+:: Add additional HTTP headers (default is {} )
# +:logger+:: Logger to use (default is STDOUT)
# +:password+:: Your password for Google Analytics
# +:profile_id+:: Use this Google Analytics profile_id (default is nil)
# +:timeout+:: Set Net:HTTP timeout in seconds (default is 300)
# +:token+:: Use an authentication token you received before
# +:api_key+:: The Google API Key for your project
# +:verify_ssl+:: Verify SSL connection (default is true)
def initialize(options={})
@options = Settings::DEFAULT_OPTIONS.merge(options)
handle_init_options(@options)
create_http_connection('www.google.com')
check_init_auth_requirements()
end
# Returns the list of accounts the user has access to. A user may have
# multiple accounts on Google Analytics and each account may have multiple
# profiles. You need the profile_id in order to get info from GA. If you
# don't know the profile_id then use this method to get a list of all them.
# Then set the profile_id of your instance and you can make regular calls
# from then on.
#
# ga = Gattica.new({:email => 'johndoe@google.com', :password => 'password'})
# ga.accounts
# # you parse through the accounts to find the profile_id you need
# ga.profile_id = 12345678
# # now you can perform a regular search, see Gattica::Engine#get
#
# If you pass in a profile id when you instantiate Gattica::Search then you won't need to
# get the accounts and find a profile_id - you apparently already know it!
#
# See Gattica::Engine#get to see how to get some data.
def accounts
if @user_accounts.nil?
create_http_connection('www.googleapis.com')
# get profiles
response = do_http_get("/analytics/v2.4/management/accounts/~all/webproperties/~all/profiles?max-results=10000")
xml = Hpricot(response)
@user_accounts = xml.search(:entry).collect { |profile_xml|
Account.new(profile_xml)
}
# Fill in the goals
response = do_http_get("/analytics/v2.4/management/accounts/~all/webproperties/~all/profiles/~all/goals?max-results=10000")
xml = Hpricot(response)
@user_accounts.each do |ua|
xml.search(:entry).each { |e| ua.set_goals(e) }
end
# Fill in the account name
response = do_http_get("/analytics/v2.4/management/accounts?max-results=10000")
xml = Hpricot(response)
@user_accounts.each do |ua|
xml.search(:entry).each { |e| ua.set_account_name(e) }
end
end
@user_accounts
end
# Returns the list of segments available to the authenticated user.
#
# == Usage
# ga = Gattica.new({:email => 'johndoe@google.com', :password => 'password'})
# ga.segments # Look up segment id
# my_gaid = 'gaid::-5' # Non-paid Search Traffic
# ga.profile_id = 12345678 # Set our profile ID
#
# gs.get({ :start_date => '2008-01-01',
# :end_date => '2008-02-01',
# :dimensions => 'month',
# :metrics => 'views',
# :segment => my_gaid })
def segments
if @user_segments.nil?
create_http_connection('www.googleapis.com')
response = do_http_get("/analytics/v2.4/management/segments?max-results=10000")
xml = Hpricot(response)
@user_segments = xml.search("dxp:segment").collect { |s|
Segment.new(s)
}
end
return @user_segments
end
# This is the method that performs the actual request to get data.
#
# == Usage
#
# gs = Gattica.new({:email => 'johndoe@google.com', :password => 'password', :profile_id => 123456})
# gs.get({ :start_date => '2008-01-01',
# :end_date => '2008-02-01',
# :dimensions => 'browser',
# :metrics => 'pageviews',
# :sort => 'pageviews',
# :filters => ['browser == Firefox']})
#
# == Input
#
# When calling +get+ you'll pass in a hash of options. For a description of what these mean to
# Google Analytics, see http://code.google.com/apis/analytics/docs
#
# Required values are:
#
# * +start_date+ => Beginning of the date range to search within
# * +end_date+ => End of the date range to search within
#
# Optional values are:
#
# * +dimensions+ => an array of GA dimensions (without the ga: prefix)
# * +metrics+ => an array of GA metrics (without the ga: prefix)
# * +filter+ => an array of GA dimensions/metrics you want to filter by (without the ga: prefix)
# * +sort+ => an array of GA dimensions/metrics you want to sort by (without the ga: prefix)
#
# == Exceptions
#
# If a user doesn't have access to the +profile_id+ you specified, you'll receive an error.
# Likewise, if you attempt to access a dimension or metric that doesn't exist, you'll get an
# error back from Google Analytics telling you so.
def get(args={})
args = validate_and_clean(Settings::DEFAULT_ARGS.merge(args))
query_string = build_query_string(args,@profile_id)
@logger.debug(query_string) if @debug
create_http_connection('www.googleapis.com')
data = do_http_get("/analytics/v2.4/data?#{query_string}")
return DataSet.new(Hpricot.XML(data))
end
# Since google wants the token to appear in any HTTP call's header, we have to set that header
# again any time @token is changed so we override the default writer (note that you need to set
# @token with self.token= instead of @token=)
def token=(token)
@token = token
set_http_headers
end
######################################################################
private
# Add the Google API key to the query string, if one is specified in the options.
def add_api_key(query_string)
query_string += "&key=#{@options[:api_key]}" if @options[:api_key]
query_string
end
# Does the work of making HTTP calls and then going through a suite of tests on the response to make
# sure it's valid and not an error
def do_http_get(query_string)
response = @http.get(add_api_key(query_string), @headers)
# error checking
if response.code != '200'
case response.code
when '400'
raise GatticaError::AnalyticsError, response.body + " (status code: #{response.code})"
when '401'
raise GatticaError::InvalidToken, "Your authorization token is invalid or has expired (status code: #{response.code})"
else # some other unknown error
raise GatticaError::UnknownAnalyticsError, response.body + " (status code: #{response.code})"
end
end
return response.body
end
# Sets up the HTTP headers that Google expects (this is called any time @token is set either by Gattica
# or manually by the user since the header must include the token)
def set_http_headers
@headers['Authorization'] = "GoogleLogin auth=#{@token}"
@headers['GData-Version']= '2'
end
# Creates a valid query string for GA
def build_query_string(args,profile)
output = "ids=ga:#{profile}&start-date=#{args[:start_date]}&end-date=#{args[:end_date]}"
if (start_index = args[:start_index].to_i) > 0
output += "&start-index=#{start_index}"
end
unless args[:dimensions].empty?
output += '&dimensions=' + args[:dimensions].collect do |dimension|
"ga:#{dimension}"
end.join(',')
end
unless args[:metrics].empty?
output += '&metrics=' + args[:metrics].collect do |metric|
"ga:#{metric}"
end.join(',')
end
unless args[:sort].empty?
output += '&sort=' + args[:sort].collect do |sort|
sort[0..0] == '-' ? "-ga:#{sort[1..-1]}" : "ga:#{sort}" # if the first character is a dash, move it before the ga:
end.join(',')
end
unless args[:segment].nil?
output += "&segment=#{args[:segment]}"
end
unless args[:max_results].nil?
output += "&max-results=#{args[:max_results]}"
end
# 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)
unless args[:filters].empty? # filters are a little more complicated because they can have all kinds of modifiers
output += '&filters=' + args[:filters].collect do |filter|
match, name, operator, expression = *filter.match(/^(\w*)\s*([=!<>~@]*)\s*(.*)$/) # splat the resulting Match object to pull out the parts automatically
unless name.empty? || operator.empty? || expression.empty? # make sure they all contain something
"ga:#{name}#{CGI::escape(operator.gsub(/ /,''))}#{CGI::escape(expression)}" # remove any whitespace from the operator before output
else
raise GatticaError::InvalidFilter, "The filter '#{filter}' is invalid. Filters should look like 'browser == Firefox' or 'browser==Firefox'"
end
end.join(';')
end
return output
end
# Validates that the args passed to +get+ are valid
def validate_and_clean(args)
raise GatticaError::MissingStartDate, ':start_date is required' if args[:start_date].nil? || args[:start_date].empty?
raise GatticaError::MissingEndDate, ':end_date is required' if args[:end_date].nil? || args[:end_date].empty?
raise GatticaError::TooManyDimensions, 'You can only have a maximum of 7 dimensions' if args[:dimensions] && (args[:dimensions].is_a?(Array) && args[:dimensions].length > 7)
raise GatticaError::TooManyMetrics, 'You can only have a maximum of 10 metrics' if args[:metrics] && (args[:metrics].is_a?(Array) && args[:metrics].length > 10)
possible = args[:dimensions] + args[:metrics]
# make sure that the user is only trying to sort fields that they've previously included with dimensions and metrics
if args[:sort]
missing = args[:sort].find_all do |arg|
!possible.include? arg.gsub(/^-/,'') # remove possible minuses from any sort params
end
unless missing.empty?
raise GatticaError::InvalidSort, "You are trying to sort by fields that are not in the available dimensions or metrics: #{missing.join(', ')}"
end
end
# make sure that the user is only trying to filter fields that are in dimensions or metrics
if args[:filters]
missing = args[:filters].find_all do |arg|
!possible.include? arg.match(/^\w*/).to_s # get the name of the filter and compare
end
unless missing.empty?
raise GatticaError::InvalidSort, "You are trying to filter by fields that are not in the available dimensions or metrics: #{missing.join(', ')}"
end
end
return args
end
def create_http_connection(server)
port = Settings::USE_SSL ? Settings::SSL_PORT : Settings::NON_SSL_PORT
@http = Net::HTTP.new(server, port)
@http.use_ssl = Settings::USE_SSL
@http.verify_mode = @options[:verify_ssl] ? Settings::VERIFY_SSL_MODE : Settings::NO_VERIFY_SSL_MODE
@http.set_debug_output $stdout if @options[:debug]
@http.read_timeout = @options[:timeout] if @options[:timeout]
end
# Sets instance variables from options given during initialization and
def handle_init_options(options)
@logger = options[:logger]
@profile_id = options[:profile_id]
@user_accounts = nil # filled in later if the user ever calls Gattica::Engine#accounts
@user_segments = nil
@headers = { }.merge(options[:headers]) # headers used for any HTTP requests (Google requires a special 'Authorization' header which is set any time @token is set)
@default_account_feed = nil
end
# If the authorization is a email and password then create User objects
# or if it's a previous token, use that. Else, raise exception.
def check_init_auth_requirements
if @options[:token].to_s.length > 200
self.token = @options[:token]
elsif @options[:email] && @options[:password]
@user = User.new(@options[:email], @options[:password])
@auth = Auth.new(@http, user)
self.token = @auth.tokens[:auth]
else
raise GatticaError::NoLoginOrToken, 'An email and password or an authentication token is required to initialize Gattica.'
end
end
end
end
Jump to Line
Something went wrong with that request. Please try again.