Tracking Form Submissions In Iframes ‑ Google Analytics & Iframes, Pt 1
Be it for basic form submission, third-party content, or even behind-the-scenes logging, iframes often play important roles in online user behavior. They provide a simple fix to many of the common snafus of the web – asynchronousity, portability, and cross-domain communication. They’re also the equivalent of a web analytics bear trap.
Here we see the Web Analyst, drunk on the power of Google Tag Manager, encounter a cross-domain form.
In this series, I’ll address some common issues when Google Analytics and iframes meet, as well as solutions to those problems. In Part 1, I’ll be addressing the most common issue – tracking simple, one-off things like events or form submissions within iframes.
Part 2 will cover tracking more complex user behavior in iframes. Be warned: this is a very technical issue, so there will be some technical jargon. If the phrase ‘post a message to the parent frame’ frightens you, you might want to just go ahead and check out another post on our blog today.
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.
Using the postMessage
API
The postMessage
API is a browser API that allows developers to communicate between iframes and the HTML that contains them. Using postMessage
, we can have our child iframe emit a message, which we can ‘listen’ for and use to notify GTM that an important interaction has occurred. This is great for tracking things like simple form submissions within iframes, which we’ll use for our example. We’ll need to take the following steps:
1.) Post a message from our child iframe
2.) Listen for the message in our parent frame
3.) When we catch the message, push an event into the GTM Data Layer
Let’s get started!
Posting a message
Emitting a message from an iframe is relatively simple with the postMessage
API.postMessage
is a method of our parent
object. It requires two arguments: the message we’d like to post, which should be a string, and the targetOrigin
for the message, which is the protocol, hostname, and port of the parent frame we’re trying to send the message to. If the values in the targetOrigin
don’t match our parent frame, the message will fail, so make sure these are correct; if I were trying to post from an iframe on www.example.com to the Bounteous site when a form was submitted, I’d use this code on www.example.com:
try {
parent.postMessage('formSubmit', 'http://www.bounteous.com');
} catch(e) {
// Something went wrong...
window.console && window.console.log(e);
}
Inside of our iframe, we’ll want to put that code in a block that executes once our user action has taken place. If we’re trying to track a form submit, this can just be placed on the ‘Thank You’ page.
That’s it! Pretty simple, right? You can make things more complicated, of course, by doing fancy things like serializing data into JSON and de-serializing it in the parent frame. Experiment!
This only gets us 1/3rd of the way, though; we still need to listen for our message in our parent frame.
Listening for the message
Once we’ve started sending our message, we need to teach our parent frame to ‘listen’ for the message. Think of this like placing a phone call; our postMessage
call is like dialing the number, and now someone needs to pick the phone up in order for us to talk to them. When we post our message, it generates a JavaScript Event
in our parent frame. We can listen for it by attaching an Event
Listener to the window
object of the parent frame. There are some hoops we have to jump through in order to get this to work on older browsers; feel free to copy the below code for your own use:
function addEvent(el, evt, fn) {
if (el.addEventListener) {
el.addEventListener(evt, fn);
} else if (el.attachEvent) {
el.attachEvent('on' + evt, function(evt) {
fn.call(el, evt);
});
} else if (typeof el['on' + evt] === 'undefined' || el['on' + evt] === null) {
el['on' + evt] = function(evt) {
fn.call(el, evt);
};
}
}
Now we can listen for our message using the following code:
addEvent(window, 'message', function(message) {
// Do something with the message
});
Acting on our message
Once we’ve got the code in place to emit and catch our message, it’s time to add some logic to handle the message whenever it shows up. In our example, we want to notify Google Tag Manager that a form has been submitted. How do we notify Google Tag Manager something has occurred? That’s right! We use dataLayer.push()
. Great work, class.
addEvent(window, 'message', function(message) {
var dataLayer = window.dataLayer = window.dataLayer || [];
if (message.data === 'formSubmit' && message.origin === 'IFRAME_PROTOCOL_HOSTNAME_AND_PORT') {
dataLayer.push({
'event': 'formSubmit'
});
}
});
Let’s step through the code above! First, we’re registering a listener for the message
Event on our window. That’s how we catch the message once our iframe emits it.
addEvent(window, 'message', function(message) {
...
});
Next, we’re safely instantiating our dataLayer
variable. This is the tool we use to communicate with Google Tag Manager. Because our code could be moved around, we always instantiate the dataLayer
variable with this special syntax. Read literally, it means ‘Define the phrase “dataLayer” to either be a reference to the dataLayer
that already exists, or, if that doesn’t exist yet, define the global dataLayer
to an empty array, then define the phrase “dataLayer” to refer to that newly created array.’ If that’s Greek to you, just nod and smile (and trust a stranger on the Internet, at least for today). If you’re not using GTM, you’ll just use the Universal Analytics syntax for an event here.
var dataLayer = window.dataLayer || (window.dataLayer = []);
Once our dataLayer is available, we’re ready to handle our message. The string that we sent with our parent.postMessage
call is stored in the data
property of the message
that we catch. We’ll access that property, and then we’ll check to make sure this is the right message for this piece of code; after all, we could be sending a bunch of messages from our child iframe, and not all of them signify a successful form submission. If the message is formSubmit
, we push an event to the Data Layer, letting Google Tag Manager know our form was successfully submitted!
We’re also checking that the message is being emitted from where we expect it by checking the message.origin
property. Just like with our targetOrigin, this must match exactly the protocol, hostname, and port of our child frame. In our example
if(message.data && message.data === 'formSubmit' && message.origin === 'http://www.example.com') {
dataLayer.push({
'event': 'formSubmit'
});
}
Let’s put that all together now:
Ta-da! We’ve done it. Now in Google Tag Manager, all we need to do is fire our Google Analytics event using a Custom Event Trigger with the value formSubmit
.
Troubleshooting
Once we’ve added our code snippets to our child frame and a parent frame, we can test that everything is working by using Debug Mode in Google Tag Manager. In the Preview pane, we should see the formSubmit
message show up:
If there’s no message, check the Developers Console:
- If you see a message like “Failed to execute ‘postMessage’ on ‘DOMWindow’: The target origin provided (‘THE_WRONG_HOSTNAME’) does not match the recipient window’s origin (‘YOUR_PARENT_HOSTNAME’)”, that means the code on your iframe has the incorrect
targetOrigin
. Check for typos! - If you see nothing, check that the code on your parent frame is checking for the right hostname of your child frame and the right message; again, this is fertile ground for typos.
- If you’re still stumped, try adding
console.log()
statements in between all of the steps of your code. It could be that the iframe is not triggering the message at all, or that the parent frame isn’t catching the message in time. Logging to the rescue!