Tracking Complex Interactions In Iframes Embedded Server‑side – Google Analytics & Iframes, Pt 4

November 24, 2015

Welcome to part four 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. In Part Three, we reviewed how to transfer a users Client ID when tracking interactions in dynamically appended iframes. Missed any of this? Check the links at the bottom of this post to catch up. Go on, we'll wait.

Ok, today, we’ll be tackling tracking user interactions cross-domain when the iframe is added server-side.

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

In order for you to be able to track user interactions within your iframe and maintain a consistent user and session within Google Analytics, you must be sending an identical Client ID with every hit you generate. The Client ID, in case you’ve forgotten, is a value stored within the _ga cookie that uniquely identifies a user (or browser, really) to Google Analytics; here’s yours:

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

Without a consistent Client ID, your Analytics data will be in bad shape; anytime a user interacts with your iframe, they’ll generate a brand new user and session in Google Analytics, and the data from their iframe interactions will be irreconcilable with the data from their other interactions on your site.

Getting Started

If your iframe is embedded in the HTML when the page is sent to the client, you’re going to have to be a little trickier. No matter what, resist the urge to re-load the iframe with a new parameter appended, as in our above example; this will wreck all kinds of havoc on your analytics, and it adds unnecessary additional load to your servers and/or application.

Instead, you’ll have to turn to our good friend, the postMessage API!

Using the postMessage API

For a strong introduction to the postMessage API, I’ll refer you to Part One, where we use the postMessage API to trigger Google Analytics Events when simple user interactions occurred within our iframes.

// 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('.');

    }

  }

}
// On our parent

// Using event listener polyfill
addEvent(window, 'message', function(message) {

  var dataLayer = window.dataLayer = window.dataLayer || [];  // Safely instantiate dataLayer locally

  if (message.data === 'formSubmit' && message.origin === 'IFRAME_PROTOCOL_HOSTNAME_AND_PORT') {
    dataLayer.push({
      'event': 'formSubmit'
    });
  }
 
});

In that case, we were sending a message OUT from our child iframe to our parent window; in this example, we’ll be doing the opposite to transfer in our Client ID.

Getting Our Client ID

Just like in Part 3, we’ll need to get our Client ID first, in order to send it our child iframe. Here are a few different ways to do that, as we discussed in detail in Part 3:

// 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"

Generally, it is more reliable to extract the Client ID from the user’s _ga cookie than using the .get('clientId') method.

Once we have our Client ID handy, we’re ready to post it to our child iframe.

Sending a Message to the Child Iframe

In order to send a message to our child iframe, we’ll need to make sure the iframe is ready for our message. To do this, we’ll need to bind to the load event of our iframe. The simplest way to do this is to set the onload attribute inline – it’s not pretty, but it’s effective.

function transmitClientId(evt) {

  var clientId = extractClientId();
  var targetIframe = this;

  if(clientId) {
  
    targetIframe.postMessage('clientId:' + clientId, 'IFRAME_PROTOCOL_HOSTNAME_AND_PORT');
        break;

  }

}

// ... further down the page

<iframe src='https://other-domain.com/some-path' onload='transmitClientId()'></iframe>

Next, in our iframe, we’ll need to add the following listener:

addEvent(window, 'message', function(message) {

  if (message.data.indexOf('clientId:') === 0 && message.origin === 'PARENT_FRAME_PROTOCOL_HOSTNAME_AND_PORT') {
    
    // Handle our Client ID

  }
 
});

Once we’ve caught our Client ID, what we do next will depend on our implementation. If you’re using Google Tag Manager, push that value to the dataLayer:

addEvent(window, 'message', function(message) {

  var dataLayer = window.dataLayer || (window.dataLayer = []);

  if (message.data.indexOf('clientId:') === 0 && message.origin === 'PARENT_FRAME_PROTOCOL_HOSTNAME_AND_PORT') {
    
    dataLayer.push({
      'event': 'clientIdDelivered',
      'clientId': message.data.split('clientId:')[1]
    });

  }
 
});

Then, in the Google Tag Manager interface, create a Data Layer Variable with the value clientID.

Then set the clientId Field to Set on the first hit you send to Google Analytics. Fire that hit on the Custom Event clientIdDelivered.

If you’re using hardcoded Universal Analytics, fire your first hit within the logic that catches your Client ID, like this:

(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');

addEvent(window, 'message', function(message) {

  if (message.data.indexOf('clientId:') === 0 && message.origin === 'PARENT_FRAME_PROTOCOL_HOSTNAME_AND_PORT') {
    ga('create', 'UA-999999-1', 'auto', { 
      'clientId':message.data.split('clientId:')[1] 
    });
    //      ^^^^^^^^^^^^^^^^^^^^^^^^^
    //       Manually sets Client ID
    ga('send', 'pageview');
       
  }
 
});

Last-ditch Options

If you simply can’t implement a client-side solution, the very last thing that you can do is have your server transpose the _ga cookie from a subsequent request to a Set-Cookie header on the response for the contents of the iframe. That should result in the user having the _ga cookie set on both domains, as desired.

Have we missed any edge cases? Share your iframe conundrums in the comments below.