Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Allow accesing data from GA Data Feed.

  • Loading branch information...
commit 0fa941681795695c4920f1837ce56c6ef78f7d12 1 parent 3748297
@knaveofdiamonds authored
View
39 .gitignore
@@ -1,3 +1,5 @@
+Gemfile.lock
+
# rcov generated
coverage
coverage.data
@@ -5,45 +7,8 @@ coverage.data
# rdoc generated
rdoc
-# yard generated
-doc
-.yardoc
-
# bundler
.bundle
# jeweler generated
pkg
-
-# Have editor/IDE/OS specific files you need to ignore? Consider using a global gitignore:
-#
-# * Create a file at ~/.gitignore
-# * Include files you want ignored
-# * Run: git config --global core.excludesfile ~/.gitignore
-#
-# After doing this, these files will be ignored in all your git projects,
-# saving you from having to 'pollute' every project you touch with them
-#
-# Not sure what to needs to be ignored for particular editors/OSes? Here's some ideas to get you started. (Remember, remove the leading # of the line)
-#
-# For MacOS:
-#
-#.DS_Store
-
-# For TextMate
-#*.tmproj
-#tmtags
-
-# For emacs:
-#*~
-#\#*
-#.\#*
-
-# For vim:
-#*.swp
-
-# For redcar:
-#.redcar
-
-# For rubinius:
-#*.rbc
View
12 Gemfile
@@ -1,14 +1,14 @@
source "http://rubygems.org"
-# Add dependencies required to use your gem here.
-# Example:
-# gem "activesupport", ">= 2.3.5"
+
+gem "ox", "< 2"
+gem "faraday"
+gem "addressable"
# Add dependencies to develop your gem here.
# Include everything needed to run rake, tests, features, etc.
group :development do
- gem "rspec", "~> 2.8.0"
+ gem "rspec", "~> 2"
gem "rdoc", "~> 3.12"
- gem "bundler", "~> 1.0.0"
- gem "jeweler", "~> 1.8.4"
+ gem "jeweler", "~> 1.8"
gem "rcov", ">= 0"
end
View
47 README.rdoc
@@ -1,6 +1,49 @@
-= google_analytics_feeds
+= Google Analytics Feeds
-Description goes here.
+Allows access to google analytics feeds from ruby.
+
+Does not support any of the OAuth-related authentication methods.
+
+== Installation
+
+ gem install google_analytics_feeds
+
+== Usage
+
+ require 'google_analytics_feeds'
+
+ # Define your report. Reports are built up like an ActiveRecord or
+ # Sequel::Dataset query.
+ report = GoogleAnalyticsFeeds::DataFeed.new.
+ profile(123456).
+ dimensions(:visitor_type).
+ metrics(:visits)
+
+ # Create a session and login. You can optionally pass a
+ # Faraday::Connection to +new+ to use a different adapter, deal
+ # with proxies etc.
+ session = GoogleAnalyticsFeeds::Session.new
+ session.login("username", "password")
+
+ # Fetch a report. Rows are handled with a
+ # GoogleAnalyticsFeed::RowHandler - this may just be an anonymous
+ # block as demonstrated here.
+ session.fetch_report(report) do
+ def row(r)
+ puts r.inspect # => outputs the row as a hash.
+ end
+ end
+
+== TODO
+
+* Tests, documentation
+* Filters
+* Access to the management data feed.
+* Auto-follow of paginated result sets.
+
+== Versioning
+
+Patch-level releases will not change the API. Minor releases may make breaking changes to the API until a 1.0 release.
== Contributing to google_analytics_feeds
View
1  VERSION
@@ -0,0 +1 @@
+0.0.1
View
244 lib/google_analytics_feeds.rb
@@ -0,0 +1,244 @@
+require 'ox'
+require 'addressable/uri'
+require 'stringio'
+require 'faraday'
+
+module GoogleAnalyticsFeeds
+ class AuthenticationError < StandardError ; end
+ class HttpError < StandardError ; end
+
+ class Session
+ CLIENT_LOGIN_URI = "https://www.google.com/accounts/ClientLogin"
+
+ def initialize(connection=Faraday.default_connection)
+ @connection = connection
+ end
+
+ def login(username, password)
+ return @token if @token
+ response = @connection.post(CLIENT_LOGIN_URI,
+ 'Email' => username,
+ 'Passwd' => password,
+ 'accountType' => 'HOSTED_OR_GOOGLE',
+ 'service' => 'analytics',
+ 'source' => 'ruby-google-analytics-feeds')
+
+ if response.success?
+ @token = response.body.match(/^Auth=(.*)$/)[1]
+ else
+ raise AuthenticationError
+ end
+ end
+
+ def fetch_report(report, handler=nil, &block)
+ handler = block if handler.nil?
+ response = report.retrieve(@token, @connection)
+
+ if response.success?
+ DataFeedParser.new(handler).parse_rows(StringIO.new(response.body))
+ else
+ raise HttpError.new
+ end
+ end
+ end
+
+ # A SAX-style row handler.
+ #
+ # Extend this class and override the methods you care about to
+ # handle data feed row data.
+ class RowHandler
+ def start_rows
+ end
+
+ def row(row)
+ end
+
+ def end_rows
+ end
+ end
+
+ module Naming
+ # Returns a ruby-friendly symbol from a google analytics name.
+ #
+ # For example:
+ #
+ # name_to_symbol("ga:visitorType") # => :visitor_type
+ def name_to_symbol(name)
+ name.sub(/^ga\:/,'').gsub(/(.)([A-Z])/,'\1_\2').downcase.to_sym
+ end
+
+ # Returns a google analytics name from a ruby symbol.
+ #
+ # For example:
+ #
+ # symbol_to_name(:visitor_type) # => "ga:visitorType"
+ def symbol_to_name(sym)
+ parts = sym.to_s.split("_").map(&:capitalize)
+ parts[0].downcase!
+ "ga:" + parts.join('')
+ end
+ end
+
+ # Parses rows from the GA feed via SAX. Clients shouldn't have to
+ # use this - use a RowHandler instead.
+ class RowParser < ::Ox::Sax
+ include Naming
+
+ def initialize(handler)
+ @handler = handler
+ end
+
+ def start_element(element)
+ case element
+
+ when :entry
+ @row = {}
+ when :"dxp:dimension", :"dxp:metric"
+ @property = {}
+ end
+ end
+
+ def attr(name, value)
+ if @property
+ @property[name] = value
+ end
+ end
+
+ def end_element(element)
+ case element
+ when :entry
+ handle_complete_row
+ @row = nil
+ when :"dxp:dimension", :"dxp:metric"
+ handle_complete_property
+ @property = nil
+ end
+ end
+
+ private
+
+ def handle_complete_row
+ @handler.row(@row)
+ end
+
+ def handle_complete_property
+ if @row
+ value = @property[:value]
+ if @property[:type] == "integer"
+ value = Integer(value)
+ end
+ name = name_to_symbol(@property[:name])
+ @row[name] = value
+ end
+ end
+ end
+
+ class DataFeed
+ include Naming
+
+ BASE_URI = "https://www.googleapis.com/analytics/v2.4/data"
+
+ def initialize
+ @params = {}
+ end
+
+ def profile(id)
+ clone_and_set {|params|
+ params['ids'] = symbol_to_name(id)
+ }
+ end
+
+ def metrics(*vals)
+ clone_and_set {|params|
+ params['metrics'] = vals.map {|v| symbol_to_name(v) }.join(',')
+ }
+ end
+
+ def dimensions(*vals)
+ clone_and_set {|params|
+ params['dimensions'] = vals.map {|v| symbol_to_name(v) }.join(',')
+ }
+ end
+
+ def dates(start_date, end_date)
+ clone_and_set {|params|
+ params['start-date'] = start_date.strftime("%Y-%m-%d")
+ params['end-date'] = end_date.strftime("%Y-%m-%d")
+ }
+ end
+
+ def start_index(i)
+ clone_and_set {|params|
+ params['start-index'] = i.to_s
+ }
+ end
+
+ def max_results(i)
+ clone_and_set {|params|
+ params['max-results'] = i.to_s
+ }
+ end
+
+ # Sorts the result set by a column.
+ #
+ # Direction can be :asc or :desc.
+ def sort(column, direction)
+ clone_and_set {|params|
+ c = symbol_to_name(column)
+ params['sort'] = (direction == :desc ? "-#{c}" : c)
+ }
+ end
+
+ def uri
+ uri = Addressable::URI.parse(BASE_URI)
+ uri.query_values = @params
+ uri.to_s
+ end
+
+ alias :to_s :uri
+
+ def retrieve(session_token, connection)
+ connection.get(uri) do |request|
+ request.headers['Authorization'] =
+ "GoogleLogin auth=#{session_token}"
+ end
+ end
+
+ def clone
+ obj = super
+ obj.instance_variable_set(:@params, @params.clone)
+ obj
+ end
+
+ protected
+
+ attr_reader :params
+
+ private
+
+ def clone_and_set
+ obj = clone
+ yield obj.params
+ obj
+ end
+ end
+
+ class DataFeedParser
+ def initialize(handler)
+ if handler.kind_of?(Proc)
+ @handler = Class.new(RowHandler, &handler).new
+ elsif handler.kind_of?(Class)
+ @handler = handler.new
+ else
+ @handler = handler
+ end
+ end
+
+ # Parse rows from an IO object.
+ def parse_rows(io)
+ @handler.start_rows
+ Ox.sax_parse(RowParser.new(@handler), io)
+ @handler.end_rows
+ end
+ end
+end
View
15 lib/sample.rb
@@ -0,0 +1,15 @@
+require File.dirname(__FILE__) + "/google_analytics_feeds"
+
+class LoggingRowHandler < GoogleAnalyticsFeeds::RowHandler
+ def row(r)
+ puts r.inspect
+ end
+
+ def end_rows
+ puts "Done!"
+ end
+end
+
+File.open("/home/roland/gaout2.xml") do |fh|
+ GoogleAnalyticsFeeds.parse_data_rows(fh, LoggingRowHandler)
+end
View
10 spec/google_analytics_feeds_spec.rb
@@ -1,7 +1,11 @@
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
-describe "GoogleAnalyticsFeeds" do
- it "fails" do
- fail "hey buddy, you should probably rename this file and start specing for real"
+describe "Parsing property names" do
+ it "snake cases, symbolizes and removes ga namespace" do
+ GoogleAnalyticsFeeds::DataFeed.new.name_to_symbol("ga:visitorType").should == :visitor_type
+ end
+
+ it "converts a ruby symbol to a google analytics name" do
+ GoogleAnalyticsFeeds::DataFeed.new.symbol_to_name(:visitor_type).should == "ga:visitorType"
end
end
View
47 spec/request_spec.rb
@@ -0,0 +1,47 @@
+require 'spec_helper'
+require 'addressable/uri'
+
+describe GoogleAnalyticsFeeds::DataFeed do
+ it "builds a URI" do
+ request = described_class.new.
+ profile(123).
+ metrics(:foo, :bar).
+ dimensions(:baz).
+ dates(Date.new(2010, 3, 14), Date.new(2010, 3, 14)).
+ max_results(50).
+ start_index(10)
+
+ uri = Addressable::URI.parse(request.uri)
+ uri.scheme.should == "https"
+ uri.host.should == "www.googleapis.com"
+ uri.path.should == "/analytics/v2.4/data"
+ uri.query_values.should == {
+ "ids" => "ga:123",
+ "metrics" => "ga:foo,ga:bar",
+ "dimensions" => "ga:baz",
+ "start-date" => "2010-03-14",
+ "end-date" => "2010-03-14",
+ "max-results" => "50",
+ "start-index" => "10",
+ }
+ end
+
+ it "doesn't modify the original request" do
+ request = described_class.new.profile(123)
+ request.metrics(:foo) # result thrown away
+
+ uri = Addressable::URI.parse(request.uri)
+ uri.query_values.should == {"ids" => "ga:123"}
+ end
+
+ it "adds the appropriate header and makes the request" do
+ connection = mock(:connection)
+ headers = {}
+ request = mock(:request, :headers => headers).as_null_object
+
+ connection.should_receive(:get).and_yield(request)
+ described_class.new.retrieve("123", connection)
+
+ headers["Authorization"].should == "GoogleLogin auth=123"
+ end
+end
Please sign in to comment.
Something went wrong with that request. Please try again.