Tracking Form Submissions In Iframes ‑ Google Analytics & Iframes, Pt 1

October 21, 2015
tracking form submissions

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!