Google Analytics and Google Apps Script

Google Apps Script Patterns: Google Analytics in Google Add-ons and Apps Script projects

For a lot of developers when you mention Google Analytics they probably think tracking website pageviews. Google Analytics is actually far more versatile than that and when used with the Google Analytics Measurement Protocol anything that can connect with the internet can potentially send tracking data and as a result be analysed. In this post I’ll show you some patterns you can use for using Google Analytics client side in HTMLService and server side with any Google Apps Script code you are running backend. As part of this I include GATrack, a Google Analytics tracking helper class for Google Apps Script. GATrack is based on the knowledge I’ve gained over the years from GDE Google Analytics Experts and this post would not have been possible without the invaluable input from one of these experts Simo Ahava who’s blog, simoahava.com, is a continual source of useful information and well worth following.

Update: For additional guidance on using Google Analytics specifically in Google Apps Script Add-ons my fellow GDE Romain Vialard has written his approach in Google Apps Script: Tracking add-on usage with Google Analytics

Google Analytics and HTMLService

As restrictions on the code executed in HTMLService have long been relaxed it’s possible to include the Google Analytics tracking code in all uses of HTMLService including sidebars, dialogs and web apps. To get the tracking code recording useful data you’ll find you’ll need to set a page title and url as the way HTMLService interfaces are embedded prevents the tracking code from doing its usual automatic detection. This can be done using the .createTemplateFromFile() method. For example to track when one of your Google Sheets sidebars is opened you can using the following code (Important: you need to replace YOUR_PAGE_TITLE and YOUR_TRACKING_ID with your own details for this script to work):

Code.gs

function showSidebar() {
  var html = HtmlService.createTemplateFromFile('Page');
  html.ga = { page_title: 'YOUR_PAGE_TITLE',
                      page_url: SpreadsheetApp.getActive().getUrl()};
  SpreadsheetApp.getUi() // Or DocumentApp or FormApp.
      .showSidebar(html.evaluate());
}

Page.html

<script>
  var GA = '<?= ga ?>';
</script>

<script async src="https://www.googletagmanager.com/gtag/js?id=YOUR_TRACKING_ID"></script> 
<script>
  window.dataLayer = window.dataLayer || [];

  function gtag() {
    dataLayer.push(arguments);
  }
  
  gtag('js', new Date());

  gtag('config', 'YOUR_TRACKING_ID', {
    'page_title': GA.page_title,
    'page_location': GA.page_url
  });
</script>

Using User ID for longitudinal insight

There’s actually a couple of ways you can improve this. As in many instances Google Apps Script runs as the authenticated user you can use the user_id property in Google Analytics.

A User ID is a unique, persistent, and non-personally identifiable ID string representing a user. It enables the analysis of groups of sessions across devices. To learn why you should implement the User ID, see Benefits of using the User ID feature.

When including a user_id it is important to remember that Google Analytics does not permit personally identifiable information (PII) so if you are using Session.getEffectiveUser().getEmail() this must be obscured/hashed before setting as a user_id (more information on best practices to avoid sending Personally Identifiable Information (PII) in Google Analytics).

Alternatively, you might want to use Session.getTemporaryActiveUserKey(), which is a unique random id for the active user. If using .getTemporaryActiveUserKey() remember that “the temporary key rotates every 30 days and is unique to the script”.

When using a User ID you can also include a Client ID. Including both is very good practice, but you should be aware that given the way HTMLService is implemented cookies are not persistent. Romain Vialard has provided guidance on how you can use `localStorage` to get around this. Using only a User ID will mean the hit you send will only be collected in a User view.

Using doNotTrack

Google Analytics can also be configured to respect a users preference to opt-out of tracking by reading the Navigator.doNotTrack setting (a tip I picked up from Simo Ahava). Building on the previous example you could use:

Code.gs

function showSidebar() {
  var html = HtmlService.createTemplateFromFile('Page');
  html.ga = {page_title: 'YOUR_PAGE_TITLE',
             page_url: SpreadsheetApp.getActive().getUrl(),
             user_id: getHashedEmail_()};
  SpreadsheetApp.getUi() // Or DocumentApp or FormApp.
      .showSidebar(html.evaluate());
}
 
function getHashedEmail_(){
  // based on https://stackoverflow.com/a/27933459
  var hashedEmail = Utilities.computeHmacSha256Signature(Session.getEffectiveUser().getEmail(),
                                                       "my key - use a stronger one").reduce(function(str,chr){
    chr = (chr < 0 ? chr + 256 : chr).toString(16);
    return str + (chr.length==1?'0':'') + chr;
  },'');
  return hashedEmail; 
}

Page.html

<script>
  var GA = '<?= ga ?>';
</script>

<script async="" src="https://www.googletagmanager.com/gtag/js?id=YOUR_TRACKING_ID"></script>
<script>
  // Respect do not track for all browsers. Except for IE 10 and 11 where we ignore it
  // https://hjnilsson.com/2015/11/30/setting-up-dnt/
  var dnt_isIe10or11 = (navigator.appVersion.indexOf("MSIE 10") !== -1) || (navigator.userAgent.indexOf("Trident") !== -1 && navigator.userAgent.indexOf("rv:11") !== -1);
  var DNT = (navigator.doNotTrack || navigator.msDoNotTrack || window.doNotTrack);
  DNT = !DNT || DNT == "unspecified" || DNT == "0" ? false : true;
  if (!DNT || dnt_isIe10or11) {
    // do track
  } else {
    // don't track
    window['ga-disable-YOUR_TRACKING_ID'] = true;
  }

  window.dataLayer = window.dataLayer || [];

  function gtag() {
    dataLayer.push(arguments);
  }
  gtag('js', new Date());

  gtag('config', 'YOUR_TRACKING_ID', {
    'page_title': GA.page_title,
    'page_location': GA.page_url,
    'user_id': GA.user_id
  });
</script>

Considering other hit types

In the examples above we are just tracking the dialog opening but Google Analytics provides a wealth of other options and the Google Analytics gtag.js documentation has more information. For example, if your HTMLService dialog had a Twitter sign-in button you could include the following with in an event handler for your button:

$(function() {
  $('#signin').on('click', function() {
    // do something
    // send event to Google Analytics
    gtag('event', 'login', {
      method: 'Twitter'
    });
  });
});

Google Analytics in Google Apps Script server side

So far we’ve looked at using Google Analytics in client HTMLService instances. Google Analytics also provides methods for sending data from server side code you execute via the Google Analytics Measurement Protocol. This can be used to give some insight to server side execution. For example, if integrating with third party APIs it might be useful to combine data returned with the active user. It can also be useful for instances when there is no HTMLService available e.g. Gmail Add-ons.

The Measurement Protocol easily integrates into your existing data collection using the same tracking id (TID), and if you wish, User ID (UID) and Client ID (CID). This makes it easy to build up a bigger picture of user interaction and not just within websites. For example, if you wanted to record a Twitter login server side rather than the client side you could use the following:

var options = {
   'method' : 'POST',
   'payload' : 'v=1&tid=YOUR_TRACKING_ID&uid=A_USER_ID&t=event&ec=engagement&ea=sign_up&el=Twitter&ev=1'
};
UrlFetchApp.fetch('https://www.google-analytics.com/collect', options);

Breaking down the payload:

v=1                   // Version.
&tid=YOUR_TRACKING_ID // Your UA-XXXXX-Y Tracking ID 
&uid=A_USER_ID        // user_id (either a uid or anonymous cid (client id) is required)
&t=event              // Event hit type
&ec=engagement        // Event Category. Required.
&ea=sign_up           // Event Action. Required.
&el=Twitter           // Event label.
&ev=1                 // Event value.

The example above is just the tip of the iceberg in terms of hit types and it’s worth spending some time with the Google Analytics Measurement Protocol documentation to see what is possible.

Batch requests

One important considerations before proliferating your script with Google Analytics hits is there is a UrlFetchApp quota hit each time you call it. There are two features of the Google Analytics Measurement Protocol that can help in this respect. First you can batch multiple hits in a single request. As noted in the documentation batch requests have the following limitations to keep in mind:

  • A maximum of 20 hits can be specified per request.
  • The total size of all hit payloads cannot be greater than 16K bytes.
  • No single hit payload can be greater than 8K bytes.

Queue time

The next feature which works well with batch is Queue Time:

Used to collect offline / latent hits. The value represents the time delta (in milliseconds) between when the hit being reported occurred and the time the hit was sent.

This means if we are collecting Google Analytics hits while your script executes to send as a batch providing a queue time means if timing is important you can include an offset in your analytic hit. There are some limitations on queue time but as Google Apps Script has an execution runtime limit in this use case it shouldn’t be an issue.

GATrack.gs – a Google Analytics helper for Google Apps Script

So how do we pull all of this together into a reusable pattern. In this GitHub repo a simple Google Sheets Add-on which integrates Google Analytics in HTMLService and also server-side. All the code is also available in this script project to make it easier for you to copy and test.

In the example we are interacting with a third party API to pull back a random list of names. The example hopefully highlights some basic Google Analytics tracking techniques. The code includes a GATrack.gs helper which you can drop into your existing and new script projects. GATrack uses batch and queue to reduce UrlFetchApp quota usage and is deliberately abstract to let you send any type of Google Analytics hit.

Setting up GATrack

To use GATrack in your own script project once GATrack.gs is copied you need to initialize it with your Google Analytics tracking ID, and optional User ID by calling:

GATrack.init(TID, optUID);

Important: if no User ID is specified you need to include a Client ID (CID) as part of the hitsObject detailed below. If the hit doesn’t have either CID or UID, it won’t get processed.

As GATrack uses a number of services such as Property Service and UrlFetchApp if you are using this in an Add-on and initializing in onOpen() you need to handle this based on authorization mode, for example:

function onOpen(e) {
  var menu = SpreadsheetApp.getUi().createAddonMenu();
  if (e && e.authMode !== ScriptApp.AuthMode.NONE) {
    // As using Properties service and UrlFetchApp can only run when not in AuthMode.NONE 
    GATrack.init('UA-48225260-5', Session.getTemporaryActiveUserKey());
    menu.addItem('Show Sidebar', 'showSidebar');
  } else {
    // Add a normal menu item (works in all authorization modes).
  }
  menu.addToUi();
}

Building Google Analytics hits in your server side code

To build hits for your Google Analytics account you use the .addToGA() method:

GATrack.addToGA(hitsObject);

The hitsObject is deliberately abstract to give you complete flexibility in the hit you send. For example, to send an event hit type you could use:

GATrack.addToGA({t: 'event', ec: 'GATrackDemo', ea: 'Name Gen.', el: 'Name Length', ev: 1});

Or a timing hit with:

GATrack.addToGA({t: 'timing', utc: 'GATrackDemo', utv: 'runtime', utt:time, utl:'Name Gen.' });

You can keep adding Google Analytics hits throughout your script execution. If you add more than 20 hits GATrack will automatically send the queued data to Google Analytics allowing you to keep adding hits. All hits you add are also automatically timestamped so that there is no latency in the data.

Flushing Google Analytics hits at the end of script execution

To flush any remaining queued hits you need to include the following line at the end of your script execution:

GATrack.flushGAQueue();

This is very important to do otherwise to you do as queued data is not persistent and is only stored for the duration of the script execution. If you are also using try/catch and still want to send data when there is an exception remember to include .flushGAQueue(). Google Analytics also has an exceptions hit type and the following code is used in the example:

function getLotsOfThings(){
  // ...
  try {
    // ...
    GATrack.addToGA(...);
    GATrack.flushGAQueue();
  } catch(e) {
    GATrack.addToGA({t: 'exception', exd: 'Line '+e.lineNumber+' '+e.message});
    GATrack.flushGAQueue();
  }
}

GATrack and doNotTrack

GATrack also includes a .setDoNotTrack() method which allows your users to opt out of Google Analytics tracking:

GATrack.setDoNotTrack(true);

The setting is stored in Property Service as a User Property so it has persistence outwith a single script execution. GATrack doesn’t implement any UI so it is up to you to decide how to record user preferences/consent. GATrack does however expose .setDoNotTrack() so that it can be used within a HTMLService interface with:

google.script.run.expose('GATrack','setDoNotTrack', true );

This can be combined with Navigator.doNotTrack setting mentioned earlier as the example below demonstrates:

<script async src="https://www.googletagmanager.com/gtag/js?id=YOUR_TRACKING_ID"></script>
<script>
  // Respect do not track for all browsers. Except for IE 10 and 11 where we ignore it
  // https://hjnilsson.com/2015/11/30/setting-up-dnt/
  var dnt_isIe10or11 = (navigator.appVersion.indexOf("MSIE 10") !== -1) || (navigator.userAgent.indexOf("Trident") !== -1 && navigator.userAgent.indexOf("rv:11") !== -1);
  var DNT = (navigator.doNotTrack || navigator.msDoNotTrack || window.doNotTrack);
  DNT = !DNT || DNT == "unspecified" || DNT == "0" ? false : true;
  if (!DNT || dnt_isIe10or11) {
    // do track
    google.script.run.expose('GATrack', 'setDoNotTrack', '');
  } else {
    // don't track
    window['ga-disable-YOUR_TRACKING_ID_HERE'] = true;
    google.script.run.expose('GATrack', 'setDoNotTrack', true);
  }

  window.dataLayer = window.dataLayer || [];

  function gtag() {
    dataLayer.push(arguments);
  }
  gtag('js', new Date());

  gtag('config', 'YOUR_TRACKING_ID_HERE', {
    'page_title': GA.page_title,
    'page_location': GA.page_url,
    'user_id': GA.user_id
  });
</script>

Summary

Hopefully this post has provided some useful tips on how Google Analytics tracking can be integrated in Google Apps Script. If you aren’t familiar with Google Analytics you are probably finding it’s a huge topic and integrating Google Analytics and actually gaining actionable insight will require to learn about the possibilities and limitations of Google Analytics. In particular when implementing Google Analytics in your script projects it is worth keeping in mind quotas and protocol policies.

If you have questions, comments or code suggestions feel free to comment here or in the GATrack GitHub repo. Once again thank you to Simo Ahava for his input on this post … follow him, you will learn so much.

chevron_left
chevron_right
css.php