Proof of Concept - Dashboard for Google Analytics

This post is more than 2 years old.

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.

Raymond Camden's Picture

About Raymond Camden

Raymond is a senior developer evangelist for Adobe. He focuses on document services, JavaScript, and enterprise cat demos. If you like this article, please consider visiting my Amazon Wishlist or donating via PayPal to show your support. You can even buy me a coffee!

Lafayette, LA https://www.raymondcamden.com

Archived Comments

Comment 1 by Misty posted on 1/25/2014 at 7:43 PM

Hi Ray, Very Nice, but can't you use ColdFusion for this to do

Comment 2 by Raymond Camden posted on 1/25/2014 at 8:05 PM

Yes, you can. Todd Sharp built code for it. We use it at RIAForge.

Comment 3 by Akash Pal posted on 5/18/2016 at 1:54 PM

Does this still work? Do I require a server.

Comment 4 (In reply to #3) by Raymond Camden posted on 5/18/2016 at 2:07 PM

Does what? The demo? It does for me. If you want to use the code yourself on your own site, then obviously you need a host for your site.

Comment 5 (In reply to #4) by Akash Pal posted on 5/18/2016 at 5:26 PM

using chartjs is perfectly fine however i want a way to obatin the data directly from google analytic by making an api call.

So does the Core Reporting API have this feature? I have been trying to make seem but insn't working.

Comment 6 (In reply to #5) by Raymond Camden posted on 5/18/2016 at 7:02 PM

I believe we are having this discussion on another post too.

Comment 7 (In reply to #6) by Akash Pal posted on 5/19/2016 at 5:59 AM

Yes , so now am getting the data using core reporting api. However I require to authorize it the first time. As I want any use to view the data. I wish to hardcode the the authorization details. Using google OAuth 2.0 is possible however I cant get it. Could you look into it?

Comment 8 (In reply to #7) by Raymond Camden posted on 5/19/2016 at 9:20 AM

I believe you want the service accounts section of the docs: https://developers.google.c...

Comment 9 (In reply to #8) by Akash Pal posted on 5/19/2016 at 11:18 AM

Yes thats exactly what I want. As my code is in javascript ; a service account is not possible at the client side.

So I am looking at Using OAuth 2.0 for Server to Server Applications
https://developers.google.c...

Now the problem is that for making a HTTP/REST call a JWT token needs to be generated. Google has just given the steps and no code for this.

So I found a way to do this which is mentioned in
http://stackoverflow.com/qu...

However when I make a request for the access token request am getting the error as

{
"error": "invalid_grant",
"error_description": "Invalid JWT Signature."
}

I kindly require some feedback in this regard.Could you help me out?

Comment 10 (In reply to #9) by Akash Pal posted on 5/20/2016 at 6:38 AM

Finally I have been able to get the access token using JWT token. And even I have implemented the google analytic core reporting API within cordova and its working seamlessly.

Comment 11 (In reply to #10) by Raymond Camden posted on 5/20/2016 at 10:15 AM

Glad to here.

Comment 12 by ๐•ญ๐–Š๐–•๐–•๐–Šโนโฐโฐโฐ posted on 5/18/2017 at 12:50 PM

Hello, is there a way to automatically authenticate an account (a dedicated one)? I want to make a public dashboard.

Comment 13 (In reply to #10) by ๐•ญ๐–Š๐–•๐–•๐–Šโนโฐโฐโฐ posted on 5/18/2017 at 12:52 PM

Hi could you publish an example on http://jsbin.com or something like that? I'm trying to allow any user to see the stats without authenticating.

Comment 14 (In reply to #12) by Raymond Camden posted on 5/18/2017 at 3:36 PM

You would use their REST API in a server-side app instead of the client-side code I used here.

Comment 15 (In reply to #14) by Geetika Guleria posted on 2/6/2018 at 7:08 AM

could you point to a tutorial if possible? I wish to do the same

Comment 16 (In reply to #15) by Raymond Camden posted on 2/6/2018 at 1:56 PM

I don't know of one - I'd check Google's docs for their APIs. They typically have demos.