|
| 1 | +$:.unshift File.dirname(__FILE__) # for use/testing when no gem is installed |
| 2 | + |
| 3 | +# external |
| 4 | +require 'net/http' |
| 5 | +require 'net/https' |
| 6 | +require 'uri' |
| 7 | +require 'logger' |
| 8 | +require 'rubygems' |
| 9 | +require 'hpricot' |
| 10 | + |
| 11 | +# internal |
| 12 | +require 'gattica/core_extensions' |
| 13 | +require 'gattica/convertible' |
| 14 | +require 'gattica/exceptions' |
| 15 | +require 'gattica/user' |
| 16 | +require 'gattica/auth' |
| 17 | +require 'gattica/account' |
| 18 | +require 'gattica/data_set' |
| 19 | +require 'gattica/data_point' |
| 20 | + |
| 21 | +# Gattica is a Ruby library for talking to the Google Analytics API. |
| 22 | +# |
| 23 | +# = Introduction |
| 24 | +# There are generally three steps to getting info from the GA API: |
| 25 | +# |
| 26 | +# 1. Authenticate |
| 27 | +# 2. Get a profile id |
| 28 | +# 3. Get the data you really want |
| 29 | +# |
| 30 | +# = Usage |
| 31 | +# This library does all three. A typical transaction will look like this: |
| 32 | +# |
| 33 | +# gs = Gattica.new('[email protected]','password',123456) |
| 34 | +# results = gs.get({ :start_date => '2008-01-01', |
| 35 | +# :end_date => '2008-02-01', |
| 36 | +# :dimensions => 'browser', |
| 37 | +# :metrics => 'pageviews', |
| 38 | +# :sort => 'pageviews'}) |
| 39 | +# |
| 40 | +# So we instantiate a copy of Gattica and pass it a Google Account email address and password. |
| 41 | +# The third parameter is the profile_id that we want to access data for. (If you don't know what |
| 42 | +# your profile_id is [and you probably don't since GA doesn't tell you except through this API] |
| 43 | +# then check out Gattica::Engine#accounts). |
| 44 | +# |
| 45 | +# Then we call +get+ with the parameters we want to shape our data with. In this case we want |
| 46 | +# total page views, broken down by browser, from Jan 1 2008 to Feb 1 2008, sorted by page views. |
| 47 | +# |
| 48 | +# If you don't know the profile_id you want to get data for, call +accounts+ |
| 49 | +# |
| 50 | +# gs = Gattica.new('[email protected]','password') |
| 51 | +# accounts = gs.accounts |
| 52 | +# |
| 53 | +# This returns all of the accounts and profiles that the user has access to. Note that if you |
| 54 | +# use this method to get profiles, you need to manually set the profile before you can call +get+ |
| 55 | +# |
| 56 | +# gs.profile_id = 123456 |
| 57 | +# results = gs.get({ :start_date => '2008-01-01', |
| 58 | +# :end_date => '2008-02-01', |
| 59 | +# :dimensions => 'browser', |
| 60 | +# :metrics => 'pageviews', |
| 61 | +# :sort => 'pageviews'}) |
| 62 | + |
| 63 | + |
| 64 | +module Gattica |
| 65 | + |
| 66 | + VERSION = '0.1.1' |
| 67 | + LOGGER = Logger.new(STDOUT) |
| 68 | + |
| 69 | + def self.new(*args) |
| 70 | + Engine.new(*args) |
| 71 | + end |
| 72 | + |
| 73 | + # The real meat of Gattica, deals with talking to GA, returning and parsing results. |
| 74 | + |
| 75 | + class Engine |
| 76 | + |
| 77 | + SERVER = 'www.google.com' |
| 78 | + PORT = 443 |
| 79 | + SECURE = true |
| 80 | + DEFAULT_ARGS = { :start_date => nil, :end_date => nil, :dimensions => [], :metrics => [], :filters => [], :sort => [] } |
| 81 | + |
| 82 | + attr_reader :user, :token |
| 83 | + attr_accessor :profile_id |
| 84 | + |
| 85 | + # Create a user, and get them authorized. |
| 86 | + # If you're making a web app you're going to want to save the token that's returned by this |
| 87 | + # method so that you can use it later (without having to re-authenticate the user each time) |
| 88 | + # |
| 89 | + # ga = Gattica.new('[email protected]','password',123456) |
| 90 | + # ga.token => 'DW9N00wenl23R0...' (really long string) |
| 91 | + |
| 92 | + def initialize(email,password,profile_id=0,debug=false) |
| 93 | + LOGGER.datetime_format = '' if LOGGER.respond_to? 'datetime_format' |
| 94 | + |
| 95 | + @profile_id = profile_id |
| 96 | + @user_accounts = nil |
| 97 | + |
| 98 | + # save an http connection for everyone to use |
| 99 | + @http = Net::HTTP.new(SERVER, PORT) |
| 100 | + @http.use_ssl = SECURE |
| 101 | + @http.set_debug_output $stdout if debug |
| 102 | + |
| 103 | + # create a user and authenticate them |
| 104 | + @user = User.new(email, password) |
| 105 | + @auth = Auth.new(@http, user, { :source => 'active-gattica-0.1' }, { 'User-Agent' => 'ruby 1.8.6 (2008-03-03 patchlevel 114) [universal-darwin9.0] Net::HTTP' }) |
| 106 | + @token = @auth.tokens[:auth] |
| 107 | + @headers = { 'Authorization' => "GoogleLogin auth=#{@token}" } |
| 108 | + |
| 109 | + # 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 |
| 110 | + end |
| 111 | + |
| 112 | + |
| 113 | + # Returns the list of accounts the user has access to. A user may have multiple accounts on Google Analytics |
| 114 | + # and each account may have multiple profiles. You need the profile_id in order to get info from GA. If you |
| 115 | + # don't know the profile_id then use this method to get a list of all them. Then set the profile_id of your |
| 116 | + # instance and you can make regular calls from then on. |
| 117 | + # |
| 118 | + # ga = Gattica.new('[email protected]','password') |
| 119 | + # ga.get_accounts |
| 120 | + # # you parse through the accounts to find the profile_id you need |
| 121 | + # ga.profile_id = 12345678 |
| 122 | + # # now you can perform a regular search, see Gattica::Engine#get |
| 123 | + # |
| 124 | + # If you pass in a profile id when you instantiate Gattica::Search then you won't need to |
| 125 | + # get the accounts and find a profile_id - you apparently already know it! |
| 126 | + # |
| 127 | + # See Gattica::Engine#get to see how to get some data. |
| 128 | + |
| 129 | + def accounts |
| 130 | + # if we haven't retrieved the user's accounts yet, get them now and save them |
| 131 | + if @accts.nil? |
| 132 | + response, data = @http.get('/analytics/feeds/accounts/default', @headers) |
| 133 | + xml = Hpricot(data) |
| 134 | + @user_accounts = xml.search(:entry).collect { |entry| Account.new(entry) } |
| 135 | + end |
| 136 | + return @user_accounts |
| 137 | + end |
| 138 | + |
| 139 | + |
| 140 | + # This is the method that performs the actual request to get data. |
| 141 | + # |
| 142 | + # == Usage |
| 143 | + # |
| 144 | + # gs = Gattica.new('[email protected]','password',123456) |
| 145 | + # gs.get({ :start_date => '2008-01-01', |
| 146 | + # :end_date => '2008-02-01', |
| 147 | + # :dimensions => 'browser', |
| 148 | + # :metrics => 'pageviews', |
| 149 | + # :sort => 'pageviews'}) |
| 150 | + # |
| 151 | + # == Input |
| 152 | + # |
| 153 | + # When calling +get+ you'll pass in a hash of options. For a description of what these mean to |
| 154 | + # Google Analytics, see http://code.google.com/apis/analytics/docs |
| 155 | + # |
| 156 | + # Required values are: |
| 157 | + # |
| 158 | + # * +start_date+ => Beginning of the date range to search within |
| 159 | + # * +end_date+ => End of the date range to search within |
| 160 | + # |
| 161 | + # Optional values are: |
| 162 | + # |
| 163 | + # * +dimensions+ => an array of GA dimensions (without the ga: prefix) |
| 164 | + # * +metrics+ => an array of GA metrics (without the ga: prefix) |
| 165 | + # * +filter+ => an array of GA dimensions/metrics you want to filter by (without the ga: prefix) |
| 166 | + # * +sort+ => an array of GA dimensions/metrics you want to sort by (without the ga: prefix) |
| 167 | + # |
| 168 | + # == Exceptions |
| 169 | + # |
| 170 | + # If a user doesn't have access to the +profile_id+ you specified, you'll receive an error. |
| 171 | + # Likewise, if you attempt to access a dimension or metric that doesn't exist, you'll get an |
| 172 | + # error back from Google Analytics telling you so. |
| 173 | + |
| 174 | + def get(args={}) |
| 175 | + args = validate_and_clean(DEFAULT_ARGS.merge(args)) |
| 176 | + query_string = build_query_string(args,@profile_id) |
| 177 | + LOGGER.debug(query_string) |
| 178 | + response, data = @http.get("/analytics/feeds/data?#{query_string}", @headers) |
| 179 | + begin |
| 180 | + response.value |
| 181 | + rescue Net::HTTPServerException => e |
| 182 | + raise GatticaError::AnalyticsError, data.to_s + " (status code: #{e.message})" |
| 183 | + end |
| 184 | + return DataSet.new(Hpricot.XML(data)) |
| 185 | + end |
| 186 | + |
| 187 | + |
| 188 | + private |
| 189 | + # Creates a valid query string for GA |
| 190 | + def build_query_string(args,profile) |
| 191 | + output = "ids=ga:#{profile}&start-date=#{args[:start_date]}&end-date=#{args[:end_date]}" |
| 192 | + unless args[:dimensions].empty? |
| 193 | + output += '&dimensions=' + args[:dimensions].collect do |dimension| |
| 194 | + "ga:#{dimension}" |
| 195 | + end.join(',') |
| 196 | + end |
| 197 | + unless args[:metrics].empty? |
| 198 | + output += '&metrics=' + args[:metrics].collect do |metric| |
| 199 | + "ga:#{metric}" |
| 200 | + end.join(',') |
| 201 | + end |
| 202 | + unless args[:sort].empty? |
| 203 | + output += '&sort=' + args[:sort].collect do |sort| |
| 204 | + sort[0..0] == '-' ? "-ga:#{sort[1..-1]}" : "ga:#{sort}" # if the first character is a dash, move it before the ga: |
| 205 | + end.join(',') |
| 206 | + end |
| 207 | + unless args[:filters].empty? # filters are a little more complicated because they can have all kinds of modifiers |
| 208 | + |
| 209 | + end |
| 210 | + return output |
| 211 | + end |
| 212 | + |
| 213 | + |
| 214 | + # Validates that the args passed to +get+ are valid |
| 215 | + def validate_and_clean(args) |
| 216 | + |
| 217 | + raise GatticaError::MissingStartDate, ':start_date is required' if args[:start_date].nil? || args[:start_date].empty? |
| 218 | + raise GatticaError::MissingEndDate, ':end_date is required' if args[:end_date].nil? || args[:end_date].empty? |
| 219 | + raise GatticaError::TooManyDimensions, 'You can only have a maximum of 7 dimensions' if args[:dimensions] && (args[:dimensions].is_a?(Array) && args[:dimensions].length > 7) |
| 220 | + raise GatticaError::TooManyMetrics, 'You can only have a maximum of 10 metrics' if args[:metrics] && (args[:metrics].is_a?(Array) && args[:metrics].length > 10) |
| 221 | + |
| 222 | + # make sure that the user is only trying to sort fields that they've previously included with dimensions and metrics |
| 223 | + if args[:sort] |
| 224 | + possible = args[:dimensions] + args[:metrics] |
| 225 | + missing = args[:sort].find_all do |arg| |
| 226 | + !possible.include? arg.gsub(/^-/,'') # remove possible minuses from any sort params |
| 227 | + end |
| 228 | + raise GatticaError::InvalidSort, "You are trying to sort by fields that are not in the available dimensions or metrics: #{missing.join(', ')}" unless missing.empty? |
| 229 | + end |
| 230 | + |
| 231 | + return args |
| 232 | + end |
| 233 | + |
| 234 | + |
| 235 | + end |
| 236 | +end |
0 commit comments