About a year and a half ago I created a demo (Proof of Concept – Dashboard for Google Analytics) of a Google Analytics Dashboard. This demo was entirely client-side based and made use of the API libraries Google created. For the most part, the process was fairly simple. After I figured out how to authenticate the user and query the data, I spent more time making it look pretty than actually working with the API, which is a good thing. More recently I discovered the new Analytics Embed API. The cool thing about the Embed API is that it greatly simplifies the authentication/authorization aspect of getting analytics data and even provides built in charting capabilities. I blogged an example (Quick example of the Google Analytics Embed API) and I thought it might be fun to revisit my dashboard concept using this simpler API.
Before I show the code behind this demo, let me share a screen shot so you can see it in action. I switched to a new charting library for this demo (ChartJS) mainly because Google used it themselves in one of their demos. I didn't spend much time making this pretty - I was going for 'quick impact' in terms of visuals. Obviously though it could be better.

Each chart represents one of my properties. Each line represents page views per days, with the darker blue being the most recent page seven days and the lighter gray being the seven days before that.
The code is made up of several parts. The authentication, as I said, is handled almost entirely by the embed API.
gapi.analytics.ready(function() {
var CLIENT_ID = '818125206534-g1r0datdtu9serq2pf9cp5vkuih3h8pv.apps.googleusercontent.com';
gapi.analytics.auth.authorize({
container: 'auth-button',
clientid: CLIENT_ID,
userInfoLabel:""
});
gapi.analytics.auth.on('success', function(response) {
//hide the auth-button
document.querySelector("#auth-button").style.display='none';
console.log("get profiles");
getProfiles(function(profs) {
window.profiles = profs;
processProfiles();
});
});
Chart.defaults.global.animationSteps = 60;
Chart.defaults.global.animationEasing = 'easeInOutQuart';
Chart.defaults.global.responsive = true;
Chart.defaults.global.maintainAspectRatio = false;
});
In order to get profiles for my account, I make use of a management API. getProfiles
handles fetching, and caching, this result. (I use caching as I had planned, still plan on, adding a few additional filtering options to the report.)
function getProfiles(cb) {
//do we have a cached version?
if(sessionStorage["gaProfiles"]) {
console.log("profiles fetched from cache");
cb(JSON.parse(sessionStorage["gaProfiles"]));
return;
}
gapi.client.analytics.management.accounts.list().then(function(res) {
var accountId = res.result.items[0].id;
var profiles = [];
gapi.client.analytics.management.webproperties.list({'accountId': accountId}).then(function(res) {
res.result.items.forEach(function(item) {
if(item.defaultProfileId) profiles.push({id:"ga:"+item.defaultProfileId,name:item.name});
});
sessionStorage["gaProfiles"] = JSON.stringify(profiles);
cb(profiles);
});
});
}
Note that I do not make use of promises in this block and that's a mistake. I make use of it a bit later in another function so I need (well, I want) to be consistent. Now for the fun part. For all of my properties, I need to fetch data for each site. I was able to rip code from one of Google's demos but I quickly ran into rate limit issues. To get around this, I single thread the calls and add a slight delay.
//Credit: https://ga-dev-tools.appspot.com/embed-api/third-party-visualizations/
function query(params) {
return new Promise(function(resolve, reject) {
var data = new gapi.analytics.report.Data({query: params});
data.once('success', function(response) { resolve(response); })
.once('error', function(response) { reject(response); })
.execute();
});
}
function makeCanvas(id) {
var container = document.getElementById(id);
var canvas = document.createElement('canvas');
var ctx = canvas.getContext('2d');
container.innerHTML = '';
canvas.width = container.offsetWidth;
canvas.height = container.offsetHeight;
container.appendChild(canvas);
return ctx;
}
function processProfiles() {
console.log("working on profile "+profiles[curProfile].name);
var now = moment();
var id = profiles[curProfile].id;
var thisWeek = query({
'ids': id,
'dimensions': 'ga:date,ga:nthDay',
'metrics': 'ga:pageviews',
'start-date': moment(now).subtract(8, 'day').format('YYYY-MM-DD'),
'end-date': moment(now).subtract(1,'day').format('YYYY-MM-DD')
});
var lastWeek = query({
'ids': id,
'dimensions': 'ga:date,ga:nthDay',
'metrics': 'ga:pageviews',
'start-date': moment(now).subtract(15, 'day').subtract(1, 'week')
.format('YYYY-MM-DD'),
'end-date': moment(now).subtract(8, 'day').subtract(1, 'week')
.format('YYYY-MM-DD')
});
Promise.all([thisWeek, lastWeek]).then(function(results) {
console.log("Promise.all");console.dir(results);
var data1 = results[0].rows.map(function(row) { return +row[2]; });
var data2 = results[1].rows.map(function(row) { return +row[2]; });
var labels = results[1].rows.map(function(row) { return +row[0]; });
var totalThisWeek = results[0].totalsForAllResults["ga:pageviews"];
var totalLastWeek = results[1].totalsForAllResults["ga:pageviews"];
var percChange = (((totalThisWeek - totalLastWeek) / totalLastWeek) * 100).toFixed(2);
console.log(totalLastWeek, totalThisWeek, percChange);
labels = labels.map(function(label) {
return moment(label, 'YYYYMMDD').format('ddd');
});
var data = {
labels : labels,
datasets : [
{
label: 'Last Week',
fillColor : 'rgba(220,220,220,0.5)',
strokeColor : 'rgba(220,220,220,1)',
pointColor : 'rgba(220,220,220,1)',
pointStrokeColor : '#fff',
data : data2
},
{
label: 'This Week',
fillColor : 'rgba(151,187,205,0.5)',
strokeColor : 'rgba(151,187,205,1)',
pointColor : 'rgba(151,187,205,1)',
pointStrokeColor : '#fff',
data : data1
}
]
};
var titleStr = profiles[curProfile].name + " ";
if(totalLastWeek > 0 && totalThisWeek > 0) {
if(percChange < 0) {
titleStr += "<span class='down'>(Down "+Math.abs(percChange) + "%)</span>";
} else {
titleStr += "<span class='up'>(Up "+percChange + "%)</span>";
}
}
$("body").append("<div class='reportContainer'><div class='chartTitleContainer'>"+titleStr+"</div><div class='chartContainer' id='chart-"+curProfile+"-container'></div></div>");
new Chart(makeCanvas('chart-'+curProfile+'-container')).Line(data);
if(curProfile+1 < profiles.length) {
curProfile++;
//settimeout to try to avoid GA rate limits
setTimeout(processProfiles,200);
}
});
}
And that's basically it. As I said, I'd like to add a few options to this. Specifically, the ability to compare the current and past 30 days as well as this year versus the past. I'd also like the ability to dismiss/hide some sites I don't care about. I could use LocalStorage to remember those properties and hide them automatically.
Want to see the complete code and test it yourself? Check out the online demo here: https://static.raymondcamden.com/ga_embed/test10.html
Archived Comments
Hi Ray, great post, i tried the api embed last week on a cms. When i try view your example, im getting the same thing that i got during my test, a blank screen, any idea why this would be happening? I clicked the button to authenticate
What do you see in your console?
This is the error that comes up in the console:
TypeError: profiles[curProfile] is undefined
coming from this line:
109. console.log("working on profile "+profiles[curProfile].name);
Before that line, console.dir(prorfiles).
im testing this on your demo page btw
Oh - so download the code and modify it. Or use your devtools to pause there. Or heck, just type console.dir(profiles) in the console, it is a global var.
Excellent post! I have been trying to get the demo https://ga-dev-tools.appspo... working for weeks, but something goes wrong whenever I try.
With your online demo it works for me. Now I just need to alter the code so that I get the last 30 days instead. Lets see how that goes :)
Look at the portion running query() - should be trivial. If you have trouble, just ask.
Thanks. Its always more fun to solve the problem yourself, so I will give it a few days :)
Thank you for the tutorial, but when I open the demo you've linked to log in, I get just a blank page.
This is the error in the console: Uncaught TypeError: Cannot read property 'name' of undefined
What line # is throwing the error?
Your demo link doesnt seem to work. Could you look into it?
I am actually confused from where to begin. I have the google analytic data when I open it on browser. And I wish to use embed API to display it inside my app.
I fixed it.
Nice post Ray! I've not been able to get any GA embed API samples to authenticate when not running on localhost. I get the dreaded redirect_uri_mismatch error. It works fine on my local machine. I've added "http://www.mysite.com" and "http://www.mysite.com/oauth..." to the Authorized redirect URIs list for my project's OAuth2 credentials. Is there any else that needs to be done? Like create a file or folder called oauth2callback? Do javascript web apps like this need to worry about the token at all? My impression is that the embed API is supposed to take care of all of this for you. What am I missing?
Honestly I don't know. Outside of the link for making a client id (https://developers.google.c..., I don't know what else to suggest. I think they have a support forum you could try as well?
It sounds like Google's library didn't load - do you see any other errors in your code? Does the Network panel show anything? Can you show me where you are testing it?
WOW, Thanks for quick reply, I'm testing it on http://localhost:8080 as mentioned in developers tutorial. and thats the only report in chrome console report.
I didn't make it clear, but you need to get your own ClientID - see the instructions here:
https://developers.google.c...
Replace my ID with yours and if you set up the client id then it should work.
Sure, I did it, I past my client id instead of yours. do u think that there is a problem with authentication? fact is I can not run any of the sample codes available for this Embed API, all of them return blank page with strange error codes which there is no data available in the net about them.
So to be clear, even their demos fail on their site? If so - yeah - you got me then. Best I can suggest is trying the support forums.
Well, Raymond, the result of copy pasting the code from this page " https://developers.google.c..." with my client id, returned a blank page with console error :
1921062015-idpiframe.js:24 GET https://accounts.google.com... 403 ()
G @ 1921062015-idpiframe.js:24
Ea @ 1921062015-idpiframe.js:25
h.wb @ 1921062015-idpiframe.js:55
(anonymous) @ 1921062015-idpiframe.js:59
La.Eb @ 1921062015-idpiframe.js:40
(anonymous) @ 1921062015-idpiframe.js:50
No - try the demos on their site. It uses your auth, so if it works there, then it means something else is wrong.
with this ID:818125206534-g1r0datdtu9...
it works but with mine, no!
It may be how you made the ID then. I'd go to that link I shared earlier and ensure you carefully follow the directions on building the client ID. Google makes you set up rules and such about how and where IDs can be used, and if you don't do precisely right, it won't work.
Thanks for your fast replies. I do think the same. but I did everything as mentioned in their walk-through. Thanks again.
Sorry - can't think of much else but seeing how you made the client id. Good luck. You can also try testing on a non-localhost URL.
Hi Ray, sorry to trouble you again.
I have requirement to display Cohort report using Embed API V4, i tried as query but no luck. Please let me know sample to generate cohort report on web app using Embed API.
Sorry - I haven't played with this in a while. Best I can suggest is their docs and support forum.
Hi Ray, i tried your example in my cms site, the result is blank page with error like this:
" Uncaught ReferenceError: curProfile is not defined "
See the full source code here -
view-source:https://static.raymondcamde...
hello sir, i want to add google analytics for my project but my project is in node js with express framework,we were trying to convert same sample from html to jade its not working but in HTML is working fine so do you have any another solution for that...!
Do I have a solution for Jade? Sorry no. I'd look for how to tell Jade to bypass it's usual rules so you can paste stuff in as is.
hello Sir i am working with this but it return me error, how can i solve it?
Uncaught ReferenceError: curProfile is not defined
at processProfiles (index.php:642)
at index.php:586
at getProfiles (index.php:603)
at _.du.<anonymous> (index.php:584)
at _.fp._.k.q0 (cb=gapi.loaded_0:517)
at _.du.Vh (cb=gapi.loaded_0:751)
at YP._.k.pY (cb=gapi.loaded_0:759)
at YP.<anonymous> (cb=gapi.loaded_0:758)
at cb=gapi.loaded_0:377
at h.r2 (cb=gapi.loaded_0:85)
It looks like you rewrote it in PHP. I can't help you with that.