The sheer audacity of 3rd-party form trackers (conversion pixels, enrichment services, attribution libraries) is amazing. “Don’t worry about telling IT,” they’ll say. “It has zero side effects, just drop it onto every landing page.”

 

Now if their code is correctly detecting and using the Marketo Forms JS API, that’d be reassuring. But in reality, most use a Marketo-ignorant — and for that matter, Marketo-competitor-ignorant — approach. They look for <form> elements in the page and at least try, to varying degrees, to listen for native DOM events (focus, change, submit) without disrupting the final outcome.

 

This approach can easily fail. Take a certain social media conversion tracker that was causing every Marketo form to submit twice with the same data:

This naturally messes up all kinds of Smart Campaigns, and even creates duplicates[1] for net new leads.

 

How did they screw up? Here’s a trimmed-down equivalent of their code:

htmlForm.addEventListener("submit", function(e) {
   e.stopImmediatePropagation();
   e.preventDefault();

   const htmlFormButton = htmlForm.querySelector("button[type='submit']");
   htmlFormButton.disabled = true;

   socialTrackFormValues()
   .then( () => {
      htmlFormButton.disabled = false;
      htmlFormButton.click();
   });
}, { once: true });

 

What they think it’s doing:

  1. intercepting the end user’s click on the submit button
  2. pausing the form from submitting for the moment
  3. relaying form values to their service
  4. resuming the submission by synthetically clicking the button (i.e. backwards-compatible requestSubmit()), this time without intercepting it

 

What it’s actually doing:

  1. listening for the end user’s click on the submit button, without pausing the submission
  2. submitting the form a 2nd time by synthetically clicking the button

 

Why the discrepancy?

The direct reason this wasn’t working is the social tracking code was injected via a tag manager. Because of fundamental <script> ordering rules, it runs after the Marketo form is initially rendered, and after Marketo attaches its own native and custom event listeners. If you run stopImmediatePropagation(), that stops later event listeners from seeing the event. You can’t stop earlier event listeners, they’re already done!

 

Also, preventDefault() is irrelevant for a Marketo form. Marketo forms are built with standard <form> elements, a Very Good Thing both semantically and POLA-tically. But the data is sent using XMLHttpRequest (Ajax), not a primitive GET/POST to the action URL. Marketo is already preventing Default.

 

So their code does some of what it promises to do: it does relay the Marketo form data to the external service, and nothing is duplicated there, at least. But it wreaks havoc on the Marketo side.

 

Is there a better, Marketo Forms-aware way?

It depends.

 

If you’re just cross-posting form data to an outside service and don’t care about the response, you can hook Forms 2.0 onSubmit[2] and sendBeacon[3] the data. No need to worry about pausing the form submit because the beacon will finish in the background.

 

But if you’re using the response from an outside service to change/add fields on the form, there’s no “It just works” solution for all Marketo forms. If you’re writing such a tracker, you must (a) be transparent about what the code attempts automatically and (b) offer a way to disable anything automatic and publish a JS API, so IT can merge your expectations with other form behaviors.

 

Notes

[1] When 2 form posts for the same brand new email address arrive near-simultaneously, Marketo can’t see Lead 1 already exists before creating Lead 2. This won’t happen if the email address is already in your instance.

 

[2] Though do you know you’re the final onSubmit listener and thus can trust the form data is complete and submittable? See, it’s always tricky!

 

[3] Or Fetch with keepalive.