DEV Community

Gorilla Vibe
Gorilla Vibe

Posted on

Capturing UTM parameters into CRM hidden form fields

A visitor clicks a tagged ad, browses three pages, then fills in your contact form. By the time the lead lands in your CRM, the campaign that brought them has been forgotten. The usual fix is a set of hidden form fields that carry the UTM values along with the submission.

This article shows a small, plain JavaScript snippet that reads UTM parameters from the URL, keeps them for the visit, and writes them into hidden inputs.

Step 1: add hidden fields to the form

Add one hidden input per parameter, with the name matching the parameter exactly:

<form action="/contact" method="post">
  <input type="hidden" name="utm_source">
  <input type="hidden" name="utm_medium">
  <input type="hidden" name="utm_campaign">
  <input type="hidden" name="utm_term">
  <input type="hidden" name="utm_content">
  <!-- visible fields here -->
</form>
Enter fullscreen mode Exit fullscreen mode

If you use a form builder or an embedded CRM form, most allow hidden fields and let you set a default value. The same snippet will work as long as the field names match.

Step 2: the snippet

const KEYS = ["utm_source", "utm_medium", "utm_campaign", "utm_term", "utm_content"];
const STORE = "utm_params";

function readStored() {
  try {
    return JSON.parse(sessionStorage.getItem(STORE)) || {};
  } catch {
    return {};
  }
}

function capture() {
  const params = new URLSearchParams(window.location.search);
  const found = {};
  for (const key of KEYS) {
    const value = params.get(key);
    if (value) found[key] = value.slice(0, 200);
  }
  if (Object.keys(found).length) {
    try {
      sessionStorage.setItem(STORE, JSON.stringify(found));
    } catch {
      /* storage blocked: the values still fill this page */
    }
    return found;
  }
  return readStored();
}

function fill(values) {
  for (const form of document.querySelectorAll("form")) {
    for (const key of KEYS) {
      const field = form.querySelector(`input[type="hidden"][name="${key}"]`);
      if (field && values[key]) field.value = values[key];
    }
  }
}

document.addEventListener("DOMContentLoaded", () => fill(capture()));
Enter fullscreen mode Exit fullscreen mode

Here is what each part does:

  • capture() reads the query string. If any UTM values are present, it saves them in sessionStorage and returns them. If none are present, which is the case on the second page of a visit, it returns whatever was saved earlier.
  • value.slice(0, 200) stops an absurdly long or malformed value from filling a field.
  • The try and catch blocks matter. Storage can be unavailable in private windows or when the browser blocks it, and the snippet should still work on the landing page.
  • fill() only writes a value when the matching hidden input exists, so it is safe to load on every page.

Step 3: map the fields in the CRM

Create a custom field in your CRM for each parameter, then map each hidden input to its field in the form or integration settings. Use text fields rather than dropdowns. A dropdown will reject any value you did not predict, and you will lose leads' source data silently.

Things to check before relying on it

  1. Forms that load late. Embedded forms that appear after the page loads will not exist when DOMContentLoaded fires. Call fill(capture()) again from the form's own ready callback, or retry on a short timer.
  2. Consent. Storing values in the browser may fall under your cookie and consent rules, depending on where your visitors are and how you describe the storage. Check with whoever owns privacy for your business. The snippet works without storage on the landing page, because it fills the fields straight from the URL.
  3. Case and spelling. utm_source=LinkedIn and utm_source=linkedin become two values in your reports. Agree a lowercase rule for whoever builds your links.
  4. Test with a real submission. Open a tagged URL, move to another page, submit the form, and look at the lead record in the CRM. Check all five fields, not only the first.
  5. First touch or last touch. This version overwrites stored values when a new tagged link arrives, so it records the latest campaign. If you want the first campaign instead, only save when STORE is empty.

When ad platforms, tracking and the CRM need to agree and nobody has time to wire them together, campaign tracking and paid media work is something Gorilla Vibe takes on, with no lock-in contracts.

Written with AI assistance for the Gorilla Vibe marketing team.

Top comments (0)