Using the Google Analytics Embed API to Build a Dashboard

This post is more than 2 years old.

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.

shot1

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

Raymond Camden's Picture

About Raymond Camden

Raymond is a developer advocate for HERE Technologies. He focuses on JavaScript, serverless 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 John Charalambides posted on 7/8/2015 at 10:00 AM

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

Comment 2 (In reply to #1) by Raymond Camden posted on 7/8/2015 at 10:57 AM

What do you see in your console?

Comment 3 (In reply to #2) by John Charalambides posted on 7/8/2015 at 11:00 AM

This is the error that comes up in the console:
TypeError: profiles[curProfile] is undefined

Comment 4 (In reply to #2) by John Charalambides posted on 7/8/2015 at 11:01 AM

coming from this line:
109. console.log("working on profile "+profiles[curProfile].name);

Comment 5 (In reply to #3) by Raymond Camden posted on 7/8/2015 at 11:05 AM

Before that line, console.dir(prorfiles).

Comment 6 (In reply to #5) by John Charalambides posted on 7/8/2015 at 11:08 AM

im testing this on your demo page btw

Comment 7 (In reply to #6) by Raymond Camden posted on 7/8/2015 at 11:56 AM

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.

Comment 8 by Rene Pedersen posted on 7/8/2015 at 12:22 PM

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 :)

Comment 9 (In reply to #8) by Raymond Camden posted on 7/8/2015 at 12:30 PM

Look at the portion running query() - should be trivial. If you have trouble, just ask.

Comment 10 (In reply to #9) by Rene Pedersen posted on 7/8/2015 at 12:55 PM

Thanks. Its always more fun to solve the problem yourself, so I will give it a few days :)

Comment 11 by JSnoob posted on 8/31/2015 at 12:32 AM

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

Comment 12 (In reply to #11) by Raymond Camden posted on 8/31/2015 at 12:45 PM

What line # is throwing the error?

Comment 13 by Akash Pal posted on 5/16/2016 at 6:01 AM

Your demo link doesnt seem to work. Could you look into it?

Comment 14 by Akash Pal posted on 5/16/2016 at 6:13 AM

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.

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

I fixed it.

Comment 16 by Brian Miller posted on 6/30/2016 at 1:40 AM

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?

Comment 17 (In reply to #16) by Raymond Camden posted on 6/30/2016 at 9:32 AM

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?

Comment 18 (In reply to #0) by Raymond Camden posted on 1/14/2017 at 4:52 PM

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?

Comment 19 (In reply to #18) by Farhood KarimiSaber posted on 1/14/2017 at 5:02 PM

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.

Comment 20 (In reply to #19) by Raymond Camden posted on 1/14/2017 at 5:07 PM

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.

Comment 21 (In reply to #20) by Farhood KarimiSaber posted on 1/14/2017 at 5:10 PM

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.

Comment 22 (In reply to #21) by Raymond Camden posted on 1/14/2017 at 5:11 PM

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.

Comment 23 (In reply to #22) by Farhood KarimiSaber posted on 1/14/2017 at 5:14 PM

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

Comment 24 (In reply to #23) by Raymond Camden posted on 1/14/2017 at 5:17 PM

No - try the demos on their site. It uses your auth, so if it works there, then it means something else is wrong.

Comment 25 (In reply to #24) by Farhood KarimiSaber posted on 1/14/2017 at 5:25 PM

with this ID:818125206534-g1r0datdtu9...

it works but with mine, no!

Comment 26 (In reply to #25) by Raymond Camden posted on 1/14/2017 at 5:27 PM

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.

Comment 27 (In reply to #26) by Farhood KarimiSaber posted on 1/14/2017 at 5:33 PM

Thanks for your fast replies. I do think the same. but I did everything as mentioned in their walk-through. Thanks again.

Comment 28 (In reply to #27) by Raymond Camden posted on 1/14/2017 at 5:36 PM

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.

Comment 29 by Rashmi posted on 6/7/2017 at 4:59 PM

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.

Comment 30 (In reply to #29) by Raymond Camden posted on 6/7/2017 at 6:34 PM

Sorry - I haven't played with this in a while. Best I can suggest is their docs and support forum.

Comment 31 by Aidia Network posted on 10/13/2017 at 4:29 AM

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 "

Comment 32 (In reply to #31) by Raymond Camden posted on 10/13/2017 at 12:18 PM

See the full source code here -

view-source:https://static.raymondcamde...

Comment 33 by Hosting Duty posted on 12/28/2017 at 3:34 PM

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...!

Comment 34 (In reply to #33) by Raymond Camden posted on 12/29/2017 at 2:36 AM

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.

Comment 35 by arshad jilani posted on 5/31/2018 at 6:34 AM

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)

Comment 36 (In reply to #35) by Raymond Camden posted on 6/3/2018 at 6:57 PM

It looks like you rewrote it in PHP. I can't help you with that.