JavaScript UTM Capture: The Complete Cookie-Based Tracking Script for WordPress

Every WordPress form plugin — Contact Form 7, Gravity Forms, WPForms, Ninja Forms — has one thing in common: none of them capture UTM parameters automatically. If you want to know which ad, campaign, or channel brought each lead to your site, you need a way to grab those values from the URL and keep them available until the visitor submits a form.

That’s where JavaScript comes in. A small script can read UTM parameters from the URL, store them in browser cookies, and make them available on any page of your site — even after the visitor navigates away from the landing page. This guide gives you the complete, production-ready code and explains every line so you understand what it does and how to customise it.

We’ll cover three parsing methods, the cookie storage pattern, session vs persistent cookies, first-touch vs last-touch attribution logic, and how to add the script to your WordPress site. If you’d rather skip the code entirely, we’ll also show you the plugin approach at the end.

The Problem: UTM Parameters Disappear on Navigation

Before diving into code, let’s be clear about the problem this solves. When a visitor clicks your ad and lands on your site, the URL looks something like this:

https://yoursite.com/services/?utm_source=google&utm_medium=cpc&utm_campaign=spring-sale&gclid=CjwKCAjw...

Those query string parameters — utm_source, utm_medium, utm_campaign, and gclid — contain the attribution data you need. But the moment the visitor clicks to another page on your site, those parameters vanish from the URL. If your contact form is on /contact/, the URL has no query string parameters by the time the visitor gets there.

The solution: capture the parameters on the landing page and store them somewhere that persists across page navigation. Cookies are the most reliable option for this.

How to Parse UTM Parameters from the URL

There are three ways to extract query string parameters from a URL in JavaScript. Here’s each method, when to use it, and a working code example.

Method 1: URLSearchParams (Recommended)

The URLSearchParams API is the modern, standards-based way to work with query strings. It’s supported in all current browsers (Chrome, Firefox, Safari 10+, Edge).

// Create a URLSearchParams object from the current URL
var params = new URLSearchParams(window.location.search);

// Read individual parameters
var source   = params.get('utm_source');   // "google" or null
var medium   = params.get('utm_medium');   // "cpc" or null
var campaign = params.get('utm_campaign'); // "spring-sale" or null
var gclid    = params.get('gclid');        // "CjwKCAjw..." or null

// Check if a parameter exists
if (params.has('utm_source')) {
  console.log('UTM Source:', params.get('utm_source'));
}

window.location.search returns the query string portion of the URL, including the leading ? (e.g., ?utm_source=google&utm_medium=cpc). URLSearchParams parses this string and gives you .get(), .has(), .set(), and .delete() methods to work with individual parameters.

When to use: Always, unless you need to support Internet Explorer (which doesn’t support URLSearchParams).

Method 2: Regex Matching (Legacy Fallback)

If you need to support Internet Explorer or very old browsers, you can extract parameters using a regular expression:

function getParam(name) {
  var regex = new RegExp('[?&]' + name + '=([^&#]*)');
  var match = regex.exec(window.location.search);
  return match ? decodeURIComponent(match[1]) : '';
}

var source = getParam('utm_source');   // "google" or ""
var gclid  = getParam('gclid');        // "CjwKCAjw..." or ""

The regex [?&] matches either the ? at the start of the query string or an & separator between parameters. ([^&#]*) captures everything after the = until the next &, #, or end of string. decodeURIComponent() handles URL-encoded characters like %20 for spaces.

When to use: Only if you must support IE11 or earlier. For everything else, use URLSearchParams.

Method 3: Manual Split (For Understanding)

This method splits the query string by & and then by =. It’s the most verbose but also the easiest to understand and debug:

function getAllParams() {
  var result = {};
  var query = window.location.search.substring(1); // Remove the "?"
  if (!query) return result;

  var pairs = query.split('&');
  for (var i = 0; i < pairs.length; i++) {
    var pair = pairs[i].split('=');
    var key = decodeURIComponent(pair[0]);
    var val = decodeURIComponent(pair[1] || '');
    result[key] = val;
  }
  return result;
}

var params = getAllParams();
console.log(params.utm_source);  // "google"
console.log(params.gclid);      // "CjwKCAjw..."

When to use: For learning or debugging. In production, prefer URLSearchParams or the regex method — they handle edge cases (encoded ampersands, missing values, duplicate keys) more reliably.

Comparison

MethodBrowser SupportHandles Edge CasesCode ComplexityBest For
URLSearchParamsAll modern browsersYes (built-in)LowProduction use
RegexAll browsers (including IE)Mostly (manual decoding)MediumLegacy support
Manual splitAll browsersNo (fragile)HighLearning / debugging

Storing UTM Parameters in Cookies

Once you've read the parameters from the URL, you need to store them so they're available on subsequent pages. Cookies are the best option for this because they persist across page navigation and (with the right expiry) across browser sessions.

Setting a Cookie in JavaScript

JavaScript sets cookies using the document.cookie property. Here's the basic syntax:

document.cookie = 'utm_source=google;path=/;max-age=7776000;SameSite=Lax';

Let's break down each part:

AttributeWhat It DoesRecommended Value
utm_source=googleThe cookie name and valueMatch the URL parameter name
path=/Makes the cookie available on all pages of the site, not just the current directoryAlways /
max-age=7776000How long the cookie lives, in seconds. 7776000 = 90 days2592000 (30 days) to 7776000 (90 days)
SameSite=LaxPrevents the cookie from being sent on cross-site requests (security best practice)Always Lax

Reading a Cookie in JavaScript

Reading cookies is trickier than setting them because document.cookie returns all cookies as a single string (e.g., utm_source=google; utm_medium=cpc; _ga=GA1.2.12345). You need a helper function to extract a specific value:

function getCookie(name) {
  var match = document.cookie.match(
    new RegExp('(^| )' + name + '=([^;]+)')
  );
  return match ? decodeURIComponent(match[2]) : '';
}

// Usage
var source = getCookie('utm_source');  // "google"
var gclid  = getCookie('gclid');       // "CjwKCAjw..."

The regex (^| ) matches either the start of the string or a space (cookies are separated by ; — semicolon plus space). ([^;]+) captures everything until the next semicolon. decodeURIComponent() handles any URL-encoded values.

Session vs Persistent Cookies: Which to Use for UTM Tracking

This is one of the most common questions in UTM tracking, and the answer depends on your sales cycle.

Session Cookies

A session cookie has no max-age or expires attribute. It's automatically deleted when the browser closes. Session cookies work for same-session attribution — the visitor clicks your ad, browses a few pages, and submits a form in the same sitting.

The problem: Real visitors often don't convert in a single session. They click your ad, browse your site, leave, and come back hours or days later. A session cookie is gone by then, and so is your attribution data.

Session Storage (Not the Same Thing)

Some guides suggest using sessionStorage instead of cookies. This is worse than session cookies for UTM tracking because session storage is scoped to a single browser tab. If a visitor opens a new tab to your contact page, the session storage from the original tab isn't available. Session storage also clears when the tab closes.

Persistent Cookies (Recommended)

A persistent cookie includes a max-age or expires attribute that tells the browser to keep the cookie for a specified duration. This is what you want for UTM tracking — the attribution data survives browser closes and return visits.

Common expiry durations:

Durationmax-age ValueBest For
7 days604800Short sales cycles (e-commerce impulse buys)
30 days2592000Medium sales cycles (SaaS free trials, service enquiries)
90 days7776000Long sales cycles (B2B, enterprise, high-ticket services)

90 days is the most common choice because it aligns with Google Ads' default attribution window. If a visitor clicks your ad and converts within 90 days, Google Ads attributes the conversion to that click. Your cookie expiry should match so you can correlate the form submission back to the original ad click.

Safari ITP: The 7-Day Limit

Apple's Intelligent Tracking Prevention (ITP) in Safari restricts client-side (JavaScript-set) cookies to 7 days of expiry, regardless of what max-age you set. This means on Safari, your 90-day cookie actually expires after 7 days.

There's no JavaScript workaround for this. If Safari traffic is significant for your audience, consider a server-side solution (like a WordPress plugin that sets cookies via PHP — server-set cookies are not subject to ITP restrictions) or accept the 7-day limitation.

Comparison

Storage MethodSurvives Page NavigationSurvives Browser CloseWorks Across TabsRecommended for UTMs
Session storageYesNoNoNo
Session cookieYesNoYesOnly for same-session tracking
Persistent cookie (30-90 days)YesYesYesYes (recommended)
localStorageYesYesYesPossible, but not readable by the server

First-Touch vs Last-Touch Attribution

When a visitor arrives via a Google Ad on Monday and then returns via a Facebook ad on Wednesday, which campaign should get credit for the eventual form submission? This is the first-touch vs last-touch question, and your cookie logic determines the answer.

First-Touch Attribution

First-touch attribution gives credit to the first campaign that brought the visitor to your site. The cookie logic: only write UTM cookies if they don't already exist. If the visitor returns with new UTM parameters, the original values are preserved.

// First-touch: only set cookies if they don't exist yet
if (!getCookie('utm_source')) {
  var source = params.get('utm_source');
  if (source) {
    document.cookie = 'utm_source=' + encodeURIComponent(source) +
      ';path=/;max-age=7776000;SameSite=Lax';
  }
}

Best for: Understanding which channels drive initial awareness. Useful for top-of-funnel reporting — "How did this lead first discover us?"

Last-Touch Attribution

Last-touch attribution gives credit to the most recent campaign before conversion. The cookie logic: overwrite UTM cookies every time new UTM parameters appear in the URL.

// Last-touch: always overwrite with the latest values
var source = params.get('utm_source');
if (source) {
  document.cookie = 'utm_source=' + encodeURIComponent(source) +
    ';path=/;max-age=7776000;SameSite=Lax';
}

Best for: Understanding which channels drive conversions. Useful for bottom-of-funnel reporting — "What was the final push that made this lead convert?"

Capturing Both

The best approach captures both. Use separate cookie prefixes for first-touch and last-touch data:

var params = new URLSearchParams(window.location.search);
var utmKeys = ['utm_source', 'utm_medium', 'utm_campaign',
               'utm_term', 'utm_content'];

utmKeys.forEach(function(key) {
  var val = params.get(key);
  if (val) {
    // Last-touch: always overwrite
    document.cookie = 'lt_' + key + '=' + encodeURIComponent(val) +
      ';path=/;max-age=7776000;SameSite=Lax';

    // First-touch: only write if no existing cookie
    if (!getCookie('ft_' + key)) {
      document.cookie = 'ft_' + key + '=' + encodeURIComponent(val) +
        ';path=/;max-age=7776000;SameSite=Lax';
    }
  }
});

When the visitor submits a form, you have both sets of data: ft_utm_source=google (the first ad they clicked) and lt_utm_source=facebook (the most recent ad before conversion).

The Complete Production-Ready Script

Here's the full script that captures UTM parameters, click IDs (gclid, fbclid, msclkid, li_fat_id, ttclid), the landing page URL, and the referrer — with last-touch attribution on campaign parameters and first-visit-only capture on referrer and landing page:

<script>
(function() {
  // --- Configuration ---
  var EXPIRY = 7776000; // 90 days in seconds
  var PARAMS = ['utm_source', 'utm_medium', 'utm_campaign',
                'utm_term', 'utm_content', 'gclid', 'fbclid',
                'msclkid', 'li_fat_id', 'ttclid'];

  // --- Helper: read a cookie by name ---
  function getCookie(name) {
    var match = document.cookie.match(
      new RegExp('(^| )' + name + '=([^;]+)')
    );
    return match ? decodeURIComponent(match[2]) : '';
  }

  // --- Helper: set a cookie ---
  function setCookie(name, value) {
    document.cookie = name + '=' + encodeURIComponent(value) +
      ';path=/;max-age=' + EXPIRY + ';SameSite=Lax';
  }

  // --- Step 1: Read URL parameters ---
  var search = new URLSearchParams(window.location.search);
  var hasTrackingParams = false;

  PARAMS.forEach(function(p) {
    if (search.has(p)) hasTrackingParams = true;
  });

  // --- Step 2: Store parameters in cookies (last-touch) ---
  // Only overwrite if the current URL has tracking parameters.
  // This prevents clearing cookies on internal page navigation.
  if (hasTrackingParams) {
    PARAMS.forEach(function(p) {
      setCookie(p, search.get(p) || '');
    });
  }

  // --- Step 3: Capture referrer and landing page (first-visit only) ---
  // Only set these once per visitor. The "landing_page" cookie acts
  // as a flag — if it exists, this isn't the visitor's first page.
  if (!getCookie('landing_page')) {
    setCookie('landing_page', window.location.href);
    setCookie('referrer_url', document.referrer || '(direct)');
  }
})();
</script>

What This Script Does

  1. Defines the parameters to track — all five UTM parameters plus five click IDs from Google, Meta, Microsoft, LinkedIn, and TikTok.
  2. Checks if the current URL has tracking parameters — this prevents overwriting existing cookies when the visitor navigates to internal pages (which have no query string).
  3. Stores parameter values as cookies — using last-touch logic: if the visitor arrives with new tracking parameters, the old values are overwritten.
  4. Captures the landing page and referrer on first visit — uses first-touch logic: only writes these cookies if they don't already exist, preserving the original entry point and external referrer.

What Each Cookie Stores

Cookie NameSourceExample ValueAttribution Model
utm_sourceURL parametergoogleLast-touch
utm_mediumURL parametercpcLast-touch
utm_campaignURL parameterspring-saleLast-touch
utm_termURL parametermarketing agencyLast-touch
utm_contentURL parameterheadline-v2Last-touch
gclidGoogle Ads auto-tagCjwKCAjw7NKx...Last-touch
fbclidMeta auto-tagIwAR3abc123...Last-touch
msclkidMicrosoft auto-tagabc123def456Last-touch
li_fat_idLinkedIn click IDxyz789abc...Last-touch
ttclidTikTok click IDE1.abc123...Last-touch
landing_pagewindow.location.href/services/?utm_source=googleFirst-touch
referrer_urldocument.referrerhttps://www.google.com/First-touch

Adding the Script to Your WordPress Site

There are four ways to add this JavaScript to your WordPress site. Choose the one that fits your comfort level:

Option 1: Code Snippets Plugin (Easiest)

Install a code snippets plugin like WPCode (free) or Code Snippets. Create a new snippet, set the type to "Header & Footer" or "JavaScript," paste the script, and set it to run in the <head> on all pages. This survives theme updates and doesn't require editing theme files.

Option 2: Child Theme header.php

If you're comfortable editing theme files, add the script to your child theme's header.php file, just before the closing </head> tag. Make sure you're editing a child theme — if you edit the parent theme directly, your changes will be lost on the next theme update.

Option 3: wp_head Hook in functions.php

Add the script via your child theme's functions.php using the wp_head action hook:

add_action('wp_head', function() {
  ?>
  <script>
  (function() {
    // ... paste the full script here ...
  })();
  </script>
  <?php
});

This is cleaner than editing header.php and works with any theme.

Option 4: Google Tag Manager

If you're already using Google Tag Manager, create a Custom HTML tag, paste the script (including the <script> tags), and set it to fire on "All Pages" with the trigger "DOM Ready." This keeps the code outside of WordPress entirely and is easy to modify without deploying code changes.

Connecting the Cookies to Your Form Plugin

Once the cookies are set, you need to read them and inject the values into your form's hidden fields before submission. The approach differs by form plugin:

Form PluginHow to Read Cookies Into FieldsDetailed Guide
Contact Form 7JavaScript populates hidden fields on form render; or use the wpcf7_before_send_mail PHP hook to read cookies server-sideCF7 UTM Tracking Guide
Gravity FormsPHP gform_field_value_{param} filters read $_COOKIE values and inject them into hidden fields with dynamic population enabledGravity Forms UTM Tracking Guide
WPFormsJavaScript reads cookies and sets hidden field values via DOM manipulation (matching field labels)WPForms UTM Tracking Guide

Each form plugin has its own method for connecting cookies to hidden fields. See the linked guides above for step-by-step instructions specific to your plugin. The cookie-capture script on this page is the same for all of them — only the form-integration code differs.

Common Mistakes and How to Avoid Them

1. Overwriting Cookies on Every Page Load

If your script writes empty cookie values when no query string parameters are present, you'll wipe out the attribution data every time the visitor navigates to a new page. The production script above avoids this by checking hasTrackingParams before writing — it only overwrites cookies when the current URL actually contains tracking parameters.

2. Forgetting to URL-Encode Cookie Values

Click IDs and some UTM values contain special characters (=, &, +) that will break cookie parsing if not encoded. Always use encodeURIComponent() when setting and decodeURIComponent() when reading.

3. Missing the path=/ Attribute

Without path=/, a cookie set on /services/ won't be available on /contact/. Always set path=/ so the cookie is accessible site-wide.

4. Using jQuery When You Don't Need It

Many older UTM capture scripts use jQuery for DOM manipulation and parameter parsing. jQuery adds ~90KB to your page and is unnecessary for this task. The URLSearchParams API and document.cookie handle everything natively. Unless your site already loads jQuery (WordPress does by default on admin pages but may not on the frontend), use vanilla JavaScript.

5. Not Testing in Incognito

When testing, always use an incognito/private browsing window to start with a clean cookie state. If you test in your regular browser, old cookies from previous tests can give misleading results.

The Automated Alternative: LeadSourcePro

The JavaScript approach works, but it comes with maintenance overhead. You need to keep the script running on every page, ensure it doesn't break on theme or plugin updates, configure hidden fields on every form, and write form-specific integration code for each plugin. If any piece fails, attribution data silently stops flowing and you may not notice for weeks.

LeadSourcePro handles all of this automatically. It's a WordPress plugin that captures UTM parameters, all five click IDs, the referrer URL, and the landing page — then attaches the data to every form submission across nine form plugins (Contact Form 7, Gravity Forms, WPForms, Ninja Forms, Elementor Forms, Formidable Forms, Fluent Forms, WS Form, and HubSpot Forms). No JavaScript to maintain, no hidden fields to configure, no cookie logic to debug.

FeatureCustom JavaScriptLeadSourcePro
UTM parametersYes (with code)Yes (automatic)
Click IDs (gclid, fbclid, msclkid, li_fat_id, ttclid)Yes (with code)Yes (automatic)
Referrer URLYes (with code)Yes (automatic)
Landing page URLYes (with code)Yes (automatic)
Works with page cachingYesYes
Hidden fields requiredYes (per-form setup)No
Survives theme updatesOnly with child theme or pluginYes
Safari ITP handledNo (7-day JS cookie limit)Yes (server-side cookies)
Form plugin integrationCustom code per pluginAutomatic (9 plugins)
MaintenanceOngoingNone

Frequently Asked Questions

Do I need jQuery for UTM parameter capture?

No. The URLSearchParams API and document.cookie handle everything in vanilla JavaScript. jQuery was useful before these APIs existed, but all modern browsers support them natively. Using vanilla JS avoids loading a ~90KB library for a task that takes a few hundred bytes of code.

Should I use localStorage or cookies for UTM tracking?

Cookies are better for UTM tracking because they're accessible from both JavaScript (client-side) and PHP (server-side). This means your form plugin can read cookie values during form processing without additional JavaScript. localStorage is only accessible via JavaScript, which makes server-side form processing harder. Cookies also support automatic expiry, while localStorage persists until manually cleared.

What cookie expiry should I use for UTM tracking?

90 days (max-age=7776000) is the most common choice because it matches Google Ads' default attribution window. If your sales cycle is shorter (e-commerce, quick sign-ups), 30 days works. If your sales cycle is very short (same-day), you could use a session cookie — but persistent cookies are almost always safer because real visitor behaviour is unpredictable.

Will this script work with page caching plugins?

Yes. Unlike server-side UTM capture (PHP reading $_GET parameters), this script runs entirely in the browser (client-side JavaScript). Page caching serves static HTML, but the browser still executes JavaScript on each page load. This means the script works correctly even on fully cached pages — it reads the URL and sets cookies in the visitor's browser, not on the server.

How do I test that the script is working?

Open an incognito window. Navigate to your site with UTM parameters in the URL (e.g., ?utm_source=test&utm_medium=test). Open the browser's Developer Tools (F12), go to the Application tab, and click "Cookies" in the left panel. You should see your UTM cookies listed with the correct values and expiry dates. Then navigate to another page and verify the cookies persist.

Skip the JavaScript — capture UTM data automatically

LeadSourcePro captures UTM parameters, click IDs, referrer URL, and landing page on every form submission — no JavaScript snippets, no hidden fields, no cookie debugging. Works with 9 form plugins out of the box. Install LeadSourcePro and start tracking lead sources in minutes.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *