I'm somewhat OCD about checking my Google Analytics reports. I'm mainly concerned about this site, but I've got other sites I monitor as well. In the past I've used tools like Happy Metrix for this purpose, but their pricing now is a bit much for me. (To be clear, it is a kick ass product, but since my sites aren't commercial I can't justify the expense.) I knew that Google had an API for analytics so I thought I'd take a stab at writing my own dashboard.

I began at the core documentation site: Google Analytics - Google Developers. The API covers the full range of what you can do at the site itself - so both management of properties as well as basic stat fetching. For my purposes I focused on the Core Reporting API. I figured their API would be easy, but I was concerned about the authentication aspect. I assumed that would be rather complex. Turns out they actually have an incredibly simple tutorial that walks you through the authentication process and doing your first call. I was able to use their code as a starting point and focus more on the reporting calls. I can't tell you how helpful it was to have docs walk you through the entire process. Kudos to Google for spending the time to give us a complete example.

Once I had authentication working right and was able to get a bit of data returning, the next part was to play with their Data Explorer. As you can imagine, this lets you use a simple form to test hitting your properties and fetching data.

To work with Google Analytics, you have to:

  • Authenticate of course
  • Get the accounts for a user. Most people have one account but you could have multiple. I've got one core account and 2 sub accounts.
  • Get the profiles. The profiles are the actual sites you track.
  • Use the profile ID to make your request. So for example, given a profile of X, I can ask Google to report on the number of page views in a time period.

And that's basically it - you just repeat those calls to get the metrics you want. As I said, the API itself (see the reference) is pretty direct and has handy features like being able to date filter using strings: "today", "3daysAgo", etc.

So - my demo was rather simple. First, I fetch the accounts and cache them.


function beginReport() {

	$("#reports").html("<p><i>Gathering data...</i></p>");

	console.log('begin report');
	//first, we know the GA account yet?
	var gAccounts = window.localStorage.getItem('googleAccounts');
	if(gAccounts) { 
		console.log('loaded from cache');
		googleAccounts = JSON.parse(gAccounts);
		accountId = googleAccounts[0].id;
		getProperties();
	} else {
		gapi.client.analytics.management.accounts.list().execute(handleAccounts);
	}

}

function handleAccounts(results) {
	if (!results.code) {
		if (results && results.items && results.items.length) {
	
			console.log('handleAccounts',results.items);
			// Get the first Google Analytics account
			//var firstAccountId = results.items[0].id;
	
			//store
			window.localStorage.setItem('googleAccounts', JSON.stringify(results.items));

			accountId = results.items[0].id;
			getProperties();
	
		} else {
			console.log('No accounts found for this user.');
		}
	} else {
		console.log('There was an error querying accounts: ' + results.message);
	}
}

Note that all error handling is done via console. That's sucky, but, it is a proof of concept. Also note that for this POC I work with your first account only. In theory it wouldn't be too difficult to provide a way to select an account. After getting the account I then fetch the profiles. Again, I cache this. (I don't provide a way to clear the cache, so for now you would need to use your dev tools to nuke it.)


function getProperties() {
	console.log('Querying Properties.');

	var props = window.localStorage.getItem('googleProperties');
	if(props) {
		googleProperties = JSON.parse(props);
		getData();
	} else {
		gapi.client.analytics.management.webproperties.list({'accountId': accountId}).execute(handleWebproperties);
	}
}

function handleWebproperties(results) {
	if (!results.code) {
		if (results && results.items && results.items.length) {
	
			//console.log('web props', results.items);
			
			//filter out properties with no profileCount
			var props = [];
			for(var i=0, len=results.items.length; i<len; i++) {
				if(results.items[i].profileCount >= 1) props.push(results.items[i]);
			}
			
			//Now we get our profiles and update our main props object
			//Again, assuming 1
			console.log("getting profiles for our props");
			gapi.client.analytics.management.profiles.list({
				'accountId': accountId,
				'webPropertyId': '~all'
			}).execute(function(res) {
				console.log('ok back getting profiles');
				//in theory same order, but we'll be anal
				for(var i=0, len=res.items.length;i<len; i++) {
					var profile = res.items[i];
					for(var x=0, plen=props.length; x<plen; x++) {
						if(profile.webPropertyId === props[x].id) {
							props[x].profile = profile;
							break;
						}
					}
				}

				googleProperties = props;
	
				//store
				window.localStorage.setItem('googleProperties', JSON.stringify(props));
				
				getData();
			});
			
			
		} else {
			console.log('No web properties found for this user.');
		}
	} else {
		console.log('There was an error querying webproperties: ' + results.message);
	}
}

Note that the API lets me fetch all the properties at once. This helps keep you under the quota limit! The final bit is to actually get the data. For my dashboard I wanted two weeks of page views. I use localStorage again, but this time I'm only caching for an hour.


function getData(counter) {
	if(!counter) counter = 0;
	
	var prop = googleProperties[counter];
	console.log(prop.name,prop.websiteUrl);
	//do we have a cached version of the data that isn't expired?
	var cacheKey = prop.profile.id;
	var cachedDataRaw = window.localStorage.getItem(cacheKey);
	if(cachedDataRaw) {
		var cachedData = JSON.parse(cachedDataRaw);
		if(((new Date().getTime()) - cachedData.time) > (1000 * 60 * 60)) {
			window.localStorage.removeItem(cacheKey);	
		} else {
			console.log('sweet, had a cache for '+cacheKey);
			prop.data = cachedData.data;
		}
	}
	if(!prop.data) {
		console.log('no cache for '+cacheKey);
		gapi.client.analytics.data.ga.get({
		'ids': 'ga:' + prop.profile.id,
		'start-date': '13daysAgo',
		'end-date': 'today',
		'metrics': 'ga:visits,ga:pageviews',
		'dimensions':'ga:date'
		}).execute(function(results) {
			prop.data = results;
			//cache this bitch
			var toCache = {data:prop.data, time:new Date().getTime()};
			window.localStorage.setItem(cacheKey, JSON.stringify(toCache));
			//console.log('got results and counter is currently '+counter);	
			if(counter+1 < googleProperties.length) {
				getData(++counter);
			} else {
				displayReport();
			}
		});
	} else {
		//TODO: Repeated the above shit, bad
		if(counter+1 < googleProperties.length) {
			getData(++counter);
		} else {
			displayReport();
		}		
	}
	
}

Woot! Now I just have to render this crap. I made use of HandlebarsJS for my template and Chart.js because it was free, simple, and pretty.


function displayReport() {

	$("#reports").html("");

	var source   = $("#reportTemplate").html();
	var template = Handlebars.compile(source);
	var i, len, x;
	
	//for charting, figure out labels - I could probably do this once
	var weekdays = ["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"];
	
	var days = [];
	var d = new Date();
	for(i=0;i<7;i++) {
		var dow = d.getDay();
		days.push(weekdays[dow]);
		d.setDate(d.getDate() - 1); 
	}
	days.reverse();
	
	/*
	structure notes
	I believe array cols match dimensions/metrics
	0=date, 1=visits,2=pageviews
	*/
	for(i=0, len=googleProperties.length; i<len; i++) {
		//console.log("doing "+i);
		var property = googleProperties[i];
		//console.dir(property);
		var context = {};
		context.index = i;
		context.title = property.name;
		context.url = property.websiteUrl;
		context.pageViews = 0;
		context.pageViewsLastWeek = 0;
		var lastWeek = [];
		var thisWeek = [];
		//get page views for this week
		for(x=7;x<14;x++) {
			context.pageViews += parseInt(property.data.rows[x][2],10);
			thisWeek.push(parseInt(property.data.rows[x][2],10));
		}
		//get for last week
		for(x=0;x<7;x++) {
			context.pageViewsLastWeek += parseInt(property.data.rows[x][2],10);
			lastWeek.push(parseInt(property.data.rows[x][2],10));
		}
				
		var html = template(context);
		$("#reports").append(html);
		
		//Setup chart
		var ctx = document.getElementById("chart"+context.index).getContext("2d");
		var options = {animation:false};
		var data = {
			labels : days,
			datasets : [
				{
					fillColor : "rgba(220,220,220,0.5)",
					strokeColor : "rgba(220,220,220,1)",
					pointColor : "rgba(220,220,220,1)",
					pointStrokeColor : "#fff",
					data : lastWeek
				},
				{
					fillColor : "rgba(151,187,205,0.5)",
					strokeColor : "rgba(151,187,205,1)",
					pointColor : "rgba(151,187,205,1)",
					pointStrokeColor : "#fff",
					data : thisWeek
				}
			]
		};

		var myNewChart = new Chart(ctx).Line(data,options);
		
	}
}

The end result is pretty cool I think. Here it is on my desktop (I've got 12 properties so this is only half of them):

I also tested this on mobile. On my iPhone, the Chrome browser never closed the popup window for authorization, but it worked. Mobile Safari worked perfectly.

I tested on my Android and there Chrome had no problem with the authorization flow. So - want to see all the code? Try it yourself? Hit the link below, but please remember that a) you need to actually use Google Analytics and b) the error reporting is all done in console.