Added Google Analytics Reader for .NET

Tags: Google, Analytics, API

I found out yesterday that Google recently launched anAnalytics API. That is a very welcome to their APIs. But I was surprised to find that .NET developers were left to create their own HTTP requests and parse the results. I'm not going to go into any conspiracy theories here, but they managed to support Java, so why not .NET?

Well if Google won't support .NET, then I will :-) I added Analytics support to the Reimers.Google project in the public SVN trunk (svn://svn.reimers.dk/Reimers.Google). There will also be some code later in this post if you don't know how to use SVN.

The workhorse is the ReportRequestor class which handles the login and report requests and parsing. To read an Analytics report you have to create an instance of the ReportRequestor and set the username and password.

Before you can generate a report you will need to find the Analytics account you want to request the report for. Get all your available Analytics account by calling the GetAccounts method and selecting the one you want to use, like so:

C#

IEnumerable accounts = requestor.GetAccounts();
AnalyticsAccountInfo account = accounts.First(a => a.Title == "Reimers.dk");

Once you have your account information you can use it to request a report. The ReportRequestor exposes the RequestReport method which returns an IEnumerable. The GenericEntry is basically just a container for the Dimensions and Metrics you requested.

In the project code there is also a GetUserCountByLocation method which shows how you can take the GenericEntry list and generate a strongly typed entry based on your own business logic.

All the response parsing is done using LINQ, so you will need .NET 3.5. But then you should be ready to generate your reports.

The complete source code is below. You can use it for your own personal or commercial projects, but please remember to include credits. If you find any bugs or want assistance in generating you Analytics reports send me a message.

C#

ReportRequestor

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.IO;
using System.Xml.Linq;
using Reimers.Google.Analytics.Reports;
using System.Globalization;

namespace Reimers.Google.Analytics
{
	public class ReportRequestor
	{
		#region Fields

		private static readonly string requestUrlFormat = "https://www.google.com/analytics/feeds/data?ids={0}&dimensions={1}&metrics={2}&start-date={3}&end-date={4}";
		private static readonly string authUrlFormat = "accountType=GOOGLE&Email={0}&Passwd={1}&source=reimers.dk-analyticsreader-0.1&service=analytics";
		private static CultureInfo ci = CultureInfo.GetCultureInfo("en-US");
		private string _token = null;
		private string _username = null;
		private string _password = null;

		#endregion

		#region Constructor

		public ReportRequestor() { }

		public ReportRequestor(string email, string password)
		{
			_username = email;
			_password = password;
		}

		#endregion

		#region Properties

		public string Email
		{
			get { return _username; }

			set
			{
				if (!string.Equals(_username, value))
				{
					_username = value;
					_token = null;
				}
			}
		}

		public string Password
		{
			get { return _password; }
			set
			{
				if (!string.Equals(_password, value))
				{
					_password = value;
					_token = null;
				}
			}
		}

		#endregion

		#region Methods

		private string GetToken(string username, string password)
		{
			if (string.IsNullOrEmpty(_username) || string.IsNullOrEmpty(_password))
			{
				throw new ArgumentNullException("Username, Password", "Username and/or password not set");
			}

			string authBody = string.Format(authUrlFormat, username, password);
			HttpWebRequest req = (HttpWebRequest)HttpWebRequest.Create("https://www.google.com/accounts/ClientLogin");
			req.Method = "POST";
			req.ContentType = "application/x-www-form-urlencoded";
			req.UserAgent = "Reimers.dk req";

			Stream stream = req.GetRequestStream();
			StreamWriter sw = new StreamWriter(stream);
			sw.Write(authBody);
			sw.Close();
			sw.Dispose();

			HttpWebResponse response = (HttpWebResponse)req.GetResponse();
			StreamReader sr = new StreamReader(response.GetResponseStream());
			string token = sr.ReadToEnd();
			string[] tokens = token.Split(new string[] { "\n" }, StringSplitOptions.RemoveEmptyEntries);

			foreach (string item in tokens)
			{
				if (item.StartsWith("Auth="))
				{
					return item.Replace("Auth=", "");
				}
			}

			return string.Empty;
		}

		public IEnumerable GetAccounts()
		{
			if (string.IsNullOrEmpty(_token))
			{
				_token = GetToken(_username, _password);
			}

			HttpWebRequest req = (HttpWebRequest)HttpWebRequest.Create("https://www.google.com/analytics/feeds/accounts/default");
			req.Headers.Add("Authorization: GoogleLogin auth=" + _token);
			HttpWebResponse response = (HttpWebResponse)req.GetResponse();

			Stream responseStream = response.GetResponseStream();
			StreamReader sr = new StreamReader(responseStream);
			string responseXml = sr.ReadToEnd();

			XDocument doc = XDocument.Parse(responseXml);
			XNamespace dxpSpace = doc.Root.GetNamespaceOfPrefix("dxp");
			XNamespace defaultSpace = doc.Root.GetDefaultNamespace();

			var entries = from en in doc.Root.Descendants(defaultSpace + "entry")
						  select new AnalyticsAccountInfo
						  {
							  AccountID = en.Elements(dxpSpace + "property").Where(xe => xe.Attribute("name").Value == "ga:accountId").First().Attribute("value").Value,
							  AccountName = en.Elements(dxpSpace + "property").Where(xe => xe.Attribute("name").Value == "ga:accountName").First().Attribute("value").Value,
							  ID = en.Element(defaultSpace + "id").Value,
							  Title = en.Element(defaultSpace + "title").Value,
							  ProfileID = en.Elements(dxpSpace + "property").Where(xe => xe.Attribute("name").Value == "ga:profileId").First().Attribute("value").Value,
							  WebPropertyID = en.Elements(dxpSpace + "property").Where(xe => xe.Attribute("name").Value == "ga:webPropertyId").First().Attribute("value").Value
						  };

			return entries;
		}

		private XDocument getReport(AnalyticsAccountInfo account, IEnumerable dimensions, IEnumerable metrics, DateTime from, DateTime to)
		{
			if (string.IsNullOrEmpty(_token))
			{
				_token = GetToken(_username, _password);
			}

			StringBuilder dims = new StringBuilder();

			foreach (Dimension item in dimensions)
			{
				dims.Append("ga:" + item.ToString() + ",");
			}

			StringBuilder mets = new StringBuilder();

			foreach (Metric item in metrics)
			{
				mets.Append("ga:" + item.ToString() + ",");
			}

			string requestUrl = string.Format(requestUrlFormat, "ga:" + account.ProfileID, dims.ToString().Trim(",".ToCharArray()), mets.ToString().Trim(",".ToCharArray()), from.ToString("yyyy-MM-dd"), to.ToString("yyyy-MM-dd"));

			HttpWebRequest req = (HttpWebRequest)HttpWebRequest.Create(requestUrl);
			req.Headers.Add("Authorization: GoogleLogin auth=" + _token);
			HttpWebResponse response = (HttpWebResponse)req.GetResponse();

			Stream responseStream = response.GetResponseStream();
			string responseXml = new StreamReader(responseStream, Encoding.UTF8, true).ReadToEnd();
			XDocument doc = XDocument.Parse(responseXml);

			return doc;
		}

		public IEnumerable RequestReport(AnalyticsAccountInfo account, IEnumerable dimensions, IEnumerable metrics, DateTime from, DateTime to)
		{
			XDocument doc = getReport(account, dimensions, metrics, from, to);
			XNamespace dxpSpace = doc.Root.GetNamespaceOfPrefix("dxp");
			XNamespace defaultSpace = doc.Root.GetDefaultNamespace();

			var gr = from r in doc.Root.Descendants(defaultSpace + "entry")
					 select new GenericEntry
					 {
						 Dimensions = new List>(
							 from rd in r.Elements(dxpSpace + "dimension")
							 select new KeyValuePair(
																 (Dimension)Enum.Parse(
																 typeof(Dimension),
																 rd.Attribute("name").Value.Replace("ga:", ""),
																 true),
																 rd.Attribute("value").Value)),
						 Metrics = new List>(
							 from rm in r.Elements(dxpSpace + "metric")
							 select new KeyValuePair(
								 (Metric)Enum.Parse(typeof(Metric), rm.Attribute("name").Value.Replace("ga:", ""), true),
								 rm.Attribute("value").Value))
					 };

			return gr;
		}

		public IEnumerable GetUserCountByLocation(AnalyticsAccountInfo account, DateTime from, DateTime to)
		{
			IEnumerable report = RequestReport(account, new Dimension[] { Dimension.city, Dimension.latitude, Dimension.longitude }, new Metric[] { Metric.visits }, from, to);

			var cr = from r in report
					 select new CityReport
										 {
											 City = r.Dimensions.First(d => d.Key == Dimension.city).Value,
											 Latitude = Convert.ToDouble(r.Dimensions.First(d => d.Key == Dimension.latitude).Value, ci),
											 Longitude = Convert.ToDouble(r.Dimensions.First(d => d.Key == Dimension.longitude).Value, ci),
											 Count = Convert.ToInt32(r.Metrics.First(m => m.Key == Metric.visits).Value)
										 };

			return cr;
		}

		#endregion
	}
}

AnalyticsAccountInfo

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Reimers.Google.Analytics
{
	public class AnalyticsAccountInfo
	{
		#region Fields

		private string _id = null;
		private string _title = null;
		private string _accountId = null;
		private string _accountName = null;
		private string _profileId = null;
		private string _webProperty = null;

		#endregion

		#region Properties

		public string ID
		{
			get { return _id; }
			set { _id = value; }
		}

		public string Title
		{
			get { return _title; }
			set { _title = value; }
		}

		public string AccountID
		{
			get { return _accountId; }
			set { _accountId = value; }
		}

		public string AccountName
		{
			get { return _accountName; }
			set { _accountName = value; }
		}

		public string ProfileID
		{
			get { return _profileId; }
			set { _profileId = value; }
		}

		public string WebPropertyID
		{
			get { return _webProperty; }
			set { _webProperty = value; }
		}
		#endregion
	}
}

GenericEntry

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Reimers.Google.Analytics
{
	public class GenericEntry
	{
		#region Fields

		List> _dimensions = null;
		List> _metrics = null;

		#endregion

		#region Properties

		public List> Dimensions
		{
			get { return _dimensions; }
			set { _dimensions = value; }
		}

		public List> Metrics
		{
			get { return _metrics; }
			set { _metrics = value; }
		}

		#endregion
	}
}

CityReport

namespace Reimers.Google.Analytics.Reports
{
	public class CityReport
	{
		#region Fields

		private string _city = string.Empty;
		private double _lat = 0;
		private double _lng = 0;
		private int _count = 0;

		#endregion

		#region Properties

		public string City
		{
			get { return _city; }
			set { _city = value; }
		}

		public double Latitude
		{
			get { return _lat; }
			set { _lat = value; }
		}

		public double Longitude
		{
			get { return _lng; }
			set { _lng = value; }
		}

		public int Count
		{
			get { return _count; }
			set { _count = value; }
		}

		#endregion
	}
}

Dimension

namespace Reimers.Google.Analytics
{
	public enum Dimension
	{
		browser = 0,
		browserVersion = 1,
		city = 2,
		connectionSpeed = 3,
		continent = 4,
		countOfVisits = 5,
		country = 6,
		date = 7,
		day = 8,
		daysSinceLastVisit = 9,
		flashVersion = 10,
		hostname = 11,
		hour = 12,
		javaEnabled = 13,
		language = 14,
		latitude = 15,
		longitude = 16,
		month = 17,
		networkDomain = 18,
		networkLocation = 19,
		pageDepth = 20,
		operatingSystem = 21,
		operatingSystemVersion = 22,
		region = 23,
		screenColors = 24,
		screenResolution = 25,
		subContinent = 25,
		userDefinedValue = 26,
		visitorType = 26,
		week = 27,
		year = 28,
		adContent = 29,
		adGroup = 30,
		adSlot = 31,
		adSlotPosition = 32,
		campaign = 33,
		keyword = 34,
		medium = 35,
		referralPath = 36,
		source = 37,
		exitPagePath = 38,
		landingPagePath = 39,
		pagePath = 40,
		pageTitle = 41,
		affiliation = 42,
		daysToTransaction = 43,
		productCategory = 44,
		productName = 45,
		productSku = 46,
		transactionId = 47,
		searchCategory = 48,
		searchDestinationPage = 49,
		searchKeyword = 50,
		searchKeywordRefinement = 51,
		searchStartPage = 52,
		searchUsed = 53
	}
}

Metric

namespace Reimers.Google.Analytics
{
	public enum Metric
	{
		bounces = 0,
		entrances = 1,
		exits = 2,
		newVisits = 3,
		pageviews = 4,
		timeOnPage = 5,
		timeOnSite = 6,
		visitors = 7,
		visits = 8,
		adCost = 9,
		adClicks = 10,
		CPC = 11,
		CPM = 12,
		CTR = 13,
		impressions = 14,
		uniquePageviews = 15,
		itemQuantity = 16,
		itemRevenue = 17,
		transactionRevenue = 18,
		transactions = 19,
		transactionShipping = 20,
		transactionTax = 21,
		uniquePurchases = 22,
		searchDepth = 23,
		searchDuration = 24,
		searchExits = 25,
		searchRefinements = 26,
		searchUniques = 27,
		searchVisits = 28,
		goal1Completions = 29,
		goal2Completions = 30,
		goal3Completions = 31,
		goal4Completions = 32,
		goalCompletionsAll = 33,
		goal1Starts = 34,
		goal2Starts = 35,
		goal3Starts = 36,
		goal4Starts = 37,
		goalStartsAll = 38,
		goal1Value = 39,
		goal2Value = 40,
		goal3Value = 41,
		goal4Value = 42,
		goalValueAll = 43
	}
}

Latest Tweets