DEV Community

Cover image for Stop guessing form conversions: fire the GA4 event where the submission is accepted
Vitalii Kiiko
Vitalii Kiiko

Posted on Originally published at craftformswp.com

Stop guessing form conversions: fire the GA4 event where the submission is accepted

Every "track form submissions in GA4" tutorial ends up in one of three places. All three infer a conversion from something the browser did, rather than from the thing you care about: the server accepted the entry.

  1. GA4 enhanced measurement ("form interactions"). It listens for the browser's native submit. Forms that post with fetch() and show an inline "Thanks!" are invisible to it. It also fires whether or not the backend rejected the entry.
  2. Thank-you page views. These are solid if every form redirects to its own URL. Most don't.
  3. GTM's Form Submission trigger. It's bound to the page's markup. Rename a button, swap the theme or wrap the form in a new block, and it quietly stops matching.

So you get conversions that count rejected entries, and forms that never show up at all.

Move the event to where the truth is

Only one piece of code knows a submission really went through: the code that got the success response. Fire the event there, and only there.

The payload I settled on for CraftForms (a WordPress form plugin) is deliberately small:

window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
  event: 'craftforms_submit',   // configurable per form, e.g. generate_lead
  form_id: 1372,
  form_name: 'Wedding enquiry',
  submission_id: 5120,          // matches the stored entry
  value: 149,                   // only when the form has a calculated price > 0
  currency: 'EUR'               // site currency
});
Enter fullscreen mode Exit fullscreen mode

A few rules turned out to matter more than the payload:

  • Push before you navigate. If the success handler redirects to a payment or thank-you page, push first, then change location. Otherwise the event dies with the page.
  • Failures get their own event. A server-side rejection (a slot just booked, an option out of stock, a validation rule) pushes craftforms_submit_error instead. It never pollutes the conversion count, and it shows you which forms frustrate people.
  • No personal data by default. GA4's terms forbid it. Field values are an explicit opt-in, and even then they go to the dataLayer only (for a CRM tag in GTM, say). They are never sent through gtag().

Wiring it: with or without GTM

No GTM, gtag.js on the page (Site Kit and most GA4 plugins add it): the plugin also calls gtag('event', name, params) directly when window.gtag exists. You then mark the event as a key event in GA4 and you're done.

With GTM: a Custom Event trigger on craftforms_submit, Data Layer Variables for form_name, form_id, value and currency, and a GA4 Event tag. The same trigger can fire your Google Ads, Meta or LinkedIn conversion tags from one signal.

One trap if a site runs both: the direct gtag() call plus a GTM tag forwarding the same dataLayer event counts every conversion twice. The fix is a per-form "also send directly via gtag()" switch that you untick when GTM owns forwarding. Explicit beats clever here. Auto-detecting "is there also a GTM tag for this?" from inside the page isn't something you can do reliably.

Why this is worth the effort

Once the event comes from the accept path:

  • conversions per form are real counts, split by form_name after you register it as a custom dimension
  • revenue per form is real too, because value comes from the server-calculated price
  • submission_id lets you trace any GA4 line back to the exact entry

The full guide covers both setups step by step, the custom dimension for per-form reports, real examples (salon booking, configurators, order forms) and the FAQ:

👉 Track WordPress Form Submissions in GA4, With or Without GTM

Disclosure: I build CraftForms, the plugin these examples come from. The pattern (fire on server acceptance, push before redirect, a separate error event, no PII) works with any form stack.

Top comments (2)

Collapse
 
williamsj04 profile image
Jessica Williams •

Pushing before the redirect is the rule that saves the most lost conversions. One more safeguard is to store the submission_id once it has been sent and skip the push if it is already recorded, because a refresh or the back button on a thank-you page can otherwise count the same entry twice.

Collapse
 
omyvnss profile image
Om Yaduvanshi •

the failure-event split is the part most tutorials skip. one caveat on push-before-redirect: on slow connections the push still races a synchronous redirect, since gtag flushes async. letting the success callback navigate after the beacon goes out, or just deferring by a tick, saves a few conversions you would otherwise never notice missing.