A quick primer on the nature of the problem
One of the most common problems I run into with Google Analytics (GA) occurs when a website uses a 3rd-party shopping cart, booking system or other transactional functionality. These systems often run on another domain, which means a user goes from xyz.com to xyz.shoppingcart.com to complete a transaction, for example. By default, GA counts this user as two sessions, assuming you have your GA tag placed on xyz.shoppingcart.com. For several reasons, this is not typically what you want. The two most important reasons are:
- User sessions are double-counted, giving you an inflated view of traffic to your website.
- The source of the user on xyz.shoppingcart.com is shown as xyz.com, or direct if you have added xyz.com to your referral exclusion list. This gives the false appearance that traffic from organic search, social media, advertising, etc. doesn’t produce conversions.
With the release of Universal Analytics in 2012, Google introduced the Linker plugin, to solve this problem. It was possible to fix before that, but much more tedious. The Linker plugin automagically transfers the GA user ID from one domain to the other, allowing GA to track visits to both domains as a single session. This works beautifully, most of the time.
Cross-domain tracking in an iFrame
One case where the Linker plugin does not work is when a 3rd-party system is delivered in an iFrame on the website domain. The Linker does not have time to do its automagical goodness on the iFrame link, because the request for the iFrame happens before the Linker has loaded.
Google offers a method to overcome this, but in my experience, it does not work. The method they suggest entails adding a function to xyz.com that sends a JavaScript message event with the user’s clientId to xyz.shoppingcart.com, and also adding a JavaScript event listener to xyz.shoppingcart.com to grab the clientId, then create the GA tracker on xyz.shoppingcart.com. The problem I ran into stems from the fact that JavaScript events are asynchronous. The script that sends the message event doesn’t know if anybody is listening. There is no built-in mechanism to ensure that the message is delivered. So, for the script to work, the event listener has to be running before the event arrives. In the case of the Google iFrame code, this means that the browser has to execute the event listener in the iFrame before it executes the function on xyz.com that sends the event. Seems to me that this would rarely happen. I can say for sure that it does not in cases that I’ve tried.
One simple trick
Luckily, a few small modifications to Google’s sample code fix this problem. If you’ve been following along, it has probably already occurred to you: you need to add a delay to the function that sends the message event, to give the iFrame time to load. Here is a summary of the changes I made, with detailed code samples below:
- I wrapped the function that sends the message event in a setTimeout function.
- I increased the delay in the fallback createTracker function call in the iFrame code. I did this to increase the time window between when the message is sent and when the event listener gets overridden by the fallback.
Those are the important changes. Below are my code snippets with comments.
To be placed on the page that contains the iFrame:
<script> | |
/* | |
* adapted from https://developers.google.com/analytics/devguides/collection/analyticsjs/cross-domain#iframes | |
* I wrapped the whole thing in a setTimeout, so I could observe the delayed execution time | |
* of the GA function in the JavaScript console, using the GA debugger Chrome plugin. | |
* alternatively, you could just wrap the frameWindow.postMessage in a setTimeout | |
*/ | |
setTimeout(function(){ | |
ga(function(tracker) { | |
var clientId = tracker.get('clientId'); | |
var frameWindow = document.getElementById('destination-frame').contentWindow; | |
// change https://xyz.shoppingcart.com to match your iFrame domain | |
frameWindow.postMessage(clientId, 'https://xyz.shoppingcart.com'); | |
}); | |
}, 2000); | |
// I decided on a 2 second timeout by trial and error. In my case, this seemed to be | |
// long enough to allow the iFrame to load > 95% of the time. Increasing the delay too | |
// much risks having the user move on before being tracked at all | |
</script> |
To be placed on the page that loads in the iFrame – this is a modified version of Google’s code that includes a fallback createTracker call:
<script> | |
/* | |
* adapted from https://developers.google.com/analytics/devguides/collection/analyticsjs/cross-domain#iframes | |
* I include the GA object reference in my script, so that one version of | |
* the script works whether served in an iFrame or on a standalone page. | |
* I usually have to rely on the 3rd-party vendor to implement the code, so | |
* I like to keep implementation as simple as possible. | |
*/ | |
(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','https://www.google-analytics.com/analytics.js','ga'); | |
var trackerCreated = false; | |
function createTracker(opt_clientId) { | |
if (!trackerCreated) { | |
var fields = {}; | |
if (opt_clientId) { | |
fields.clientId = opt_clientId; | |
} | |
ga('create', 'UA-XXXXXXXX-Y', 'auto', fields); | |
// Google’s sample doesn’t do anything with the tracker | |
// once created, but presumably you will want to send either a | |
// GA pageview or event, like I have here. | |
ga('send', 'pageview'); | |
trackerCreated = true; | |
} | |
} | |
window.addEventListener('message', function(event) { | |
if (event.origin != 'http://www.xyz.com') return; | |
createTracker(event.data); | |
}); | |
// I increased the timeout to 5 seconds to give the message more | |
// time to arrive. | |
setTimeout(createTracker, 5000); | |
</script> |
Once you are done, you can use the GA debugger Chrome add-in to see if the client ID is passing from the parent window to the child frame. When ‘create’ is called on the GA object, you should see the clientId included as a parameter. When you do, go ahead and treat yourself to a victory dance. I did.
If my solution doesn’t work in your situation, I recommend that you read through Luna Metrics’ comprehensive posts on cross-domain tracking and iFrames.