Tracking Complex Interactions In Dynamically Added Cross-Domain Iframes – Google Analytics & Iframes, Pt 3

November 18, 2015
By Dan Wilkerson

Welcome to part three of our series on tracking user behavior within iframes. In Part One, we discussed how to track simple user interactions within cross-domain iframes by using the postMessage API. In Part Two, we discussed tracking complex interactions when the iframe in question is on the same domain as the parent frame. If you missed those articles, scroll to the bottom of this post and we have it all lined up for you! Today, we’ll be tackling tracking user interactions cross-domain when the iframe is added dynamically using JavaScript.

STOP HERE AND READ THIS

For this to work, you need to be able to add unadulterated code on the iframe and the page the iframe is inserted on. If you’ve got a third-party service you’d like to track, and you can’t insert code snippets on their pages, this will not work and you’re out of luck. If you can’t add code to the iframe, you can’t measure interactions with it, period. Do not pass GO, do not collect $200. Sorry.

The Challenge

Just like in Part 2, we’re still facing one important challenge; the Client ID for all of our GA hits, a value stored in the user’s _ga cookie, must be identical in order to properly track complex interactions within iframes. The default Client ID consists of a random, unsigned 32-bit integer and a timestamp rounded to the nearest second; here’s yours:

Just kidding, you don’t have JavaScript enabled (or you’ve blocked us from setting the _ga cookie)!

No matter what your approach, you must ensure the Client ID is the same for the hits being sent from your parent frame and the hits being sent from your iframe. If you send hits with two different client IDs, you’ll wind up with two different sessions, and worse, two entirely different, irreconcilable users within Google Analytics.

Getting Started

Many applications dynamically append iframes during the course of user interaction, like a displaying a form when the user clicks ‘Apply Now’. The code for this might look something like the below.

var iframe = document.createElement('iframe');
iframe.src = 'https://lunametrics.thirdpartycart.com/';
document.getElementById('iframe-holder').appendChild(iframe);

If your iframe is being dynamically appended to the page in this or a similar manner, your easiest option is to transmit your user’s Client ID using a dynamically appended query parameter. To do this, you’ll need to adjust the code that creates and appends your iframe. You have two options for transmitting your Client ID: you can use the native linkerParam that Google Analytics uses for cross-domain Client ID Transmission OR you can create your own arbitrary parameter and retrieve and transmit the Client ID manually.

Another Quick Caveat

For this method of tracking to work, you’ll need to be able to edit the code that appends the iframe to your site. If that code is off-limits to you, there’s still hope; look for Part Four in our series, where we’ll discuss tracking embedded iframes using postMessage.

Using linkerParam

linkerParam is a native mechanism for cross-domain transmission of Google Analytics Client IDs. It is attractive to use because it is Google-supported and has robust safeguards to prevent accidental Client ID pollution. To use linkerParam, you’ll need to retrieve it from the Google Analytics tracker running on your site. Here’s a code snippet you can use to retrieve the linkerParam value:

function getLinkerParam(uaNumber) {

  var _ga = window[window.GoogleAnalyticsObject];
  var trackers = _ga.getAll();
  var i;

  for (i = 0; i < trackers.length; i++) {
    
    var _tracker = trackers[i];
    if (!uaNumber || _tracker.get('trackingId') === uaNumber) {

      return _tracker.get('linkerParam');

    }

  }

}

getLinkerParam();  // > "_ga=1.241566660.710478958.1442863401"
// If you're using multiple cookies, specify your UA number
getLinkerParam('UA-999999-1');  // > "_ga=1.234902657.1273269728.1445974319"

Note: the above will fail if invoked before Google Analytics has loaded.

You then dynamically append the linkerParam value to your iframes src attribute:

var iframe = document.createElement('iframe');
var linkerParam = getLinkerParam();
iframe.src = 'https://lunametrics.thirdpartycart.com/' + '?' + linkerParam;
document.getElementById('iframe-holder').appendChild(iframe);

Next, you’ll need to enable allowLinker on your tracking code in your iframe. If you’re using hard-coded Universal Analytics code, adjust your snippet like below:

// Code within iframe
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) })(window,document,'script','//www.google-analytics.com/analytics.js','ga');

ga('create', 'UA-999999-1', 'auto', { 'allowLinker': true });
//                                ^^^^^^^^^^^^^^^^^^^^^^^^^
//                                   Enables allowLinker
// Optionally send a pageview
ga('send', 'pageview');

In Google Tag Manager, you’ll need to add the Fields to Set allowLinker, and set the value to true.

Once you’ve configured these items, run a test while using the Google Analytics Debug Chrome Extension. In the logging output, you should see the following line:

Loaded linker parameter: 1.241566660.710478958.1442863401

Using Your Own Parameter

If you cannot use the linkerParam functionality, you can also create your own parameter and deliver the client ID that way. In order to this, you’ll need to extract the Client ID from the parent frame. There are two ways to do this: asking GA directly, or extracting it from the _ga cookie.

// Asking GA nicely
function getClientId(uaNumber) {

  var _ga = window[window.GoogleAnalyticsObject];
  var trackers = _ga.getAll();
  var i;

  for (i = 0; i < trackers.length; i++) {
    
    var _tracker = trackers[i];
    if (!uaNumber || _tracker.get('trackingId') === uaNumber) {

      return _tracker.get('clientId');

    }

  }

}

getClientId();  // > "1083080632.1444403721"
// If you're using multiple cookies, specify your UA number
getClientId('UA-999999-1');  // > "1273269728.1445974319"

// Extracting it from your _ga cookie
function extractClientId(cookieName) {

  cookieName = cookieName || '_ga';
  var regex = new RegExp(cookieName + '=[^;]*')
  var gaCookie = document.cookie.match(regex);

  if(gaCookie) {

    return gaCookie[0].match(/\d+?\.\d+$/)[0];

  }

}

extractClientId();  // > "1083080632.1444403721"
extractClientId('_customCookieName');  // > "1273269728.1445974319"

Then, you’ll want to append the Client ID to the parameter of your choice, which you’ll append in turn to your iframe’s src attribute. You’ll need to take one more step, though; add an additional value to your parameter to prevent a user from sharing that value with another third party. Google Analytics does this with linkerParam – the first of the three dot-separated numbers is a hash of the clients User Agent and a timestamp, which it then checks against when the client loads the other domain. The linkerParam will be ignored if the value was generated more than 2 minutes ago or the UA doesn’t line up. You might feel comfortable just using a timestamp, for this particular use case.

// In your parent frame
var iframe = document.createElement('iframe');
var customParam = 'transfer_client_id=' + extractClientId() + '.' + +newDate();

iframe.src = 'https://lunametrics.thirdpartycart.com/' + '?' + customParam;
document.getElementById('iframe-holder').appendChild(iframe);

You’ll have to add logic to check that value in your iframe, too.

var clientIdParam = document.location.search.match(/transfer_client_id=[^&]*/);
if (clientIdParam) {

  var timestampStr = clientIdParam[0].split('.').pop();
  var timestampNum = Number(timestampStr);
  
  if (+new Date() - +new Date(timestampNum) < 1000 * 60 * 2) {

    // Handle our valid Client ID    

  }

}

Once you've verified your Client ID, you'll need to instruct Google Analytics to set the Client ID within the iframe to the Client ID that you've transmitted. If you're using hard-coded Universal Analytics, adjust your code accordingly:

// Code within iframe
var clientIdParam = document.location.search.match(/transfer_client_id=[^&]*/);
var clientId;

if (clientIdParam) {

  var parts = clientIdParam[0].split('.');
  var timestampStr = parts.pop();
  var timestampNum = Number(timestampStr);
  
  if (+new Date() - +new Date(timestampNum) < 1000 * 60 * 2) {

    clientId = parts.join('.');

  }

}

(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) })(window,document,'script','//www.google-analytics.com/analytics.js','ga');

ga('create', 'UA-999999-1', 'auto', { 'clientId': clientId });
//                                ^^^^^^^^^^^^^^^^^^^^^^^^^
//                                 Manually sets Client ID
// Optionally send a pageview
ga('send', 'pageview');

If you're using Google Tag Manager, you'll want to set your parameter-extracting code into a Custom JS Variable, and then set the Field to Set clientId to its value.

// Custom JS Variable in iframe Container
// Name: Transferred Client ID
function() {
 
  // Configure a URL Parameter, select Search, and set the value to 'transfer_client_id'
  var clientIdParam = {{Page Query - transfer_client_id}};

  if (clientIdParam) {

    var parts = clientIdParam[0].split('.');
    var timestampStr = parts.pop();
    var timestampNum = Number(timestampStr);
  
    if (+new Date() - +new Date(timestampNum) < 1000 * 60 * 2) {

      return parts.join('.');

    }

  }

}

Part three of our Google & Iframes series.