5 min read

Tracking Form Submission Events the Right Way

The best place to track form submission events is on your “thank you” page.

Why? Because if someone has landed on your thank you page, it means they’ve successfully completed the form. They got past any client- or server-side validation. No oddball errors occurred. The event really happened.

However, if you want to include all that rich form field data in the event you’re sending to tools like KISSmetrics, Mixpanel, etc., event tracking from the thank you page probably won’t work.

Let me explain.

Landing page tools

If you use HubSpot or Unbounce or MailChimp or ANY third-party tool for your forms, your form submission flow looks something like this:

HubSpot Form Flow

The problem is that the custom redirect URL that these apps let you enter doesn’t receive any of the original form submission data. They could let you pass it in the URL’s query string args, but they don’t (and probably for good reason).

So what are your options?

Option #1 is to track the event on your thank you page anyway, and just leave out the form properties. This is fine if you only care about what happened and you’re not concerned with who did it.

analytics.track('Downloaded a resource', {
  resourceName: 'Daily Activity Sheet'
});

Of course you’ll have the who did it information in HubSpot or whatever application processed the form, but if you want to distribute that information to other tools, you’re kinda screwed.

What I really want is:

analytics.track('Downloaded a resource', {
  resourceName: 'Daily Activity Sheet',
  firstName: 'Nymeria',
  lastName: 'Sand',
  email: 'nymeria@sandsnakes.org',
  company: 'Martell',
  location: 'Dorne'
});

Option #2 is to track the event on the form page at the time of submission. This is the option I use.

It’s trickier than it sounds, though. Think about it. You’re filling out a form and you click Submit, what happens? The page changes.

So, your event tracking has to happen in the milliseconds after the button was clicked and before the page transitions.

To account for this, many analytics vendors (Segment, KISSmetrics, Mixpanel) have written JavaScript helpers that are designed to intercept form submission events, pause to do tracking stuff, and then and only then let the form submit proceed.

Awesome, right?

Eh, not so fast.

stephen-tulloch-injures-himself-celebrating

Helping the helpers

What I’ve found is that most of these JavaScript helper functions assume that if the button was clicked, the form will definitely be submitted. This isn’t true!

HubSpot landing pages, for example, use client-side form validation by default. If you forget to fill out a required field and you click the submit button, HubSpot will prevent the submission and display an error message, but the analytics code will kick in anyway.

If the user bails out and never completes the form, your analytics will still show 1 form event instead of 0. If the user tries again and completes the form, your analytics will show 2 form events instead of 1.

This can be a very bad thing. Trust me. I over-reported trial signups for MONTHS before realizing this.

As of today, here’s the behavior of the 3 tools I mentioned above when an invalid form is submitted:

  • Segment’s trackForm helper will track the event as if it happened and submit the incomplete form.
  • KISSmetrics’ trackSubmit helper will track the event but not submit the form.
  • Mixpanel’s track_forms helper will track the event but not submit the form.

Note: I filed an issue on Github about Segment’s helper because submitting a prevented form could be much more problematic than over-reporting events.

Ugh, now what?

All is not lost, people!

jQuery provides a neat function, event.isDefaultPrevented(), that tells you whether a form submission has been prevented by someone else. All we have to do is check that value before firing the analytics events.

So we can do something like this:

// Grab a reference to our form
var form = $('.hs-form');

// Setup a handler to run when the form is submitted
form.on('submit', function(e) {

  // If some client-side validation kicked in and wants to prevent
  // the form from submitting, bail out now without calling track or identify
  if ( e.isDefaultPrevented() ) {
    return
  }

  // If we got here, it's okay to fire our events and submit the form      

  // Stop the form from submitting...for now
  e.preventDefault();

  // Identify this visitor using their email address as a distinct ID
  // and as a new property
  var email = form.find('[name=email]').val();
  if (email) {
    analytics.alias(email);
    analytics.identify(email, {
      email: email
    });
  }

  // Track the event and include values from the form to our event props
  analytics.track(eventName, formValuesToProps(form));

  // Submit the form now that all our analytics stuff is done
  $(e.target).unbind('submit').trigger('submit');
});

The one BIG caveat is that your handler (i.e., line 5) has to be attached higher in the DOM than the page’s form validation handler because if your code runs before the form validation does, it’ll always run. The trick is to get your code to run last.

Hopefully this helps people avoid the same issues I was having and, despite not being a perfect solution, perhaps some of the default helpers will try to incorporate event.isDefaultPrevented() to mitigate against false reporting. The tough thing is that there are a million ways to implement form validation, so it’s a cat-and-mouse game for the devs at KISS and Segment.

A note on HubSpot landing pages

For HubSpot landing pages, the isDefaultPrevented solution worked well…for a time. Then the forms team at HubSpot decided to call preventDefault always, even if the form was completely valid, presumably because they’re submitting the form via AJAX.

Thankfully when I reached out to the HubSpot forms team they had a good solution for me. They fire a custom event called hsvalidatedsubmit that I can listen for instead of listening to the default form submit event:

$(document).ready(function checkFormHS() {
  // Since HubSpot loads forms asynchronously after the page loads
  // we need to poll for it
  if( $('.hs-form').length > 0 ) {
    // Grab a reference to our form
    var form = $('.hs-form');

    // Setup a handler to run when the form passes validation
    form.on('hsvalidatedsubmit', function(e) {

      // Identify this visitor using their email address as a distinct ID
      // and as a new property
      var email = form.find('[name=email]').val();
      if (email) {
        analytics.alias(email);
        analytics.identify(email, {
          email: email
        });
      }

      // Track the event and include values from the form to our event props
      analytics.track(eventName, formValuesToProps(form));   
    })

  } else {
    setTimeout(function() { checkFormHS(); }, 200 );
  };
});

Also notice how I had to wrap everything in a function that polls to see if the form is actually in the DOM yet. All HubSpot forms are loaded via JavaScript, which makes things a PITA. I understand why they do it, but I wish there were a direct embed option.