HomeDrupalRescuing Your UTMs: A Cookie-Based Approach to Reliable Attribution for multi pages...

Rescuing Your UTMs: A Cookie-Based Approach to Reliable Attribution for multi pages and sub domains

Why Your UTMs Keep Disappearing

If you care about marketing attribution, UTM parameters are the lifeblood of your reporting. But they are also incredibly fragile. They live in the URL, vanish on navigation, and are often lost when users move across subdomains or interact with dynamically rendered forms or other third-party embeds.

The result is broken attribution: campaigns that look underperforming, sources that show up as “direct,” and a CRM full of leads with no clear origin.

To fix that, we use a UTM cookie script that captures UTM values once, persists them at the root Webocreation domain, and injects them into any compatible form—no matter when the form appears in the DOM.

What the UTM Cookie Script Does

At a high level, the script does three things:

  • Reads UTM query parameters from the page URL.
  • Stores them as cookies shared across the Webocreation domain.
  • Populates matching form fields (utm_campaignutm_contentutm_mediumutm_sourceutm_term) even if the form is initialized asynchronously.

This pattern lets you preserve UTMs across subdomains, such as www.webocreation.com and hello.webocreation.com, ensuring the original campaign data survives the user’s entire session, not just the first pageview.

Where It Runs Today

The script is designed to be reusable and is currently embedded in multiple Webocreation pages, including:

    Both copies share the same underlying logic so that UTM values are captured consistently and can be applied to any forms present on those pages.

    How the Script Behaves on Page Load

    On every page load, the script follows a clear decision tree:

    • If the URL contains UTM query parameters:
      • Each UTM value is sanitized.
      • The sanitized value is saved in a cookie on .webocreation.com.
      • Form fields with the corresponding names are populated immediately.
    • If the URL does not contain UTM parameters:
      • The script looks for existing UTM cookies.
      • It applies those stored values to any matching form fields on the page.

    To support dynamic or delayed form rendering (for embedded forms), the script also retries field population for a short period after load, so late-arriving fields still receive the correct UTM values.

    Cookie Strategy and Lifetime

    The cookie model is simple but intentional:

    • Cookie names: utm_campaignutm_contentutm_mediumutm_sourceutm_term
    • Domain: computed from the current hostname (e.g., www.webocreation.com becomes .webocreation.com)
    • Path: /
    • Lifetime: 7 days
    • SameSite: Lax

    By deriving the cookie domain at runtime, the script can run on any Webocreation subdomain without hardcoding domain values, which makes it easier to reuse in different environments and deployment targets.

    How the Cookie Domain Is Computed

    Getting the cookie domain right is what enables cross-subdomain attribution without dirty hacks. The function getCookieDomain() computes the domain dynamically based on window.location.hostname:

    • For single-label hostnames (e.g., localhost), it returns the hostname itself.
    • For simple, two-part domains like webocreation.com, it returns .webocreation.com.
    • For common three-part hostnames like www.webocreation.com, it returns .webocreation.com (second-level plus top-level domain).
    • For two-letter TLDs with likely three-part public suffixes (e.g., www.example.co.uk), it returns .example.co.uk.

    This logic lets the script behave correctly across local development, production domains, and more complex country-code TLDs without manual tuning.

    Security-First: Sanitizing UTM Values

    UTM parameters come from the URL, which means they are fully user-controlled input. Treating them as trusted strings is a recipe for XSS or polluted data. The sanitizeValue(value) function cleans every UTM value before it’s stored or applied by:

    • Decoding URL-encoded strings.
    • Stripping HTML tags.
    • Removing script-like tokens and patterns.
    • Dropping quotes and semicolons.
    • Trimming any leading or trailing whitespace.

    This protects downstream systems from injection and keeps your analytics data usable and clean.

    Core Functions and Their Responsibilities

    The script is structured around a set of focused helper functions:

    • sanitizeValue(value)
      Cleans raw UTM values to remove HTML, scripts, quotes, and other unsafe characters before storage or form population.
    • getQueryParams()
      Parses window.location.search and returns a normalized object that includes only the allowed UTM parameters, ignoring anything else in the query string.
    • setCookie(name, value, days)
      Writes a cookie using the domain computed from window.location.hostname, with the appropriate path and expiration.
    • getCookieDomain()
      Derives the correct cookie domain based on the current hostname, handling simple domains, subdomains, and multi-part public suffixes.
    • getCookie(name)
      Reads and returns a cookie value from document.cookie so stored UTMs can be reused on subsequent pages.
    • setFieldValue(name, value)
      Finds all form fields with a given name attribute and sets field.value when the element is an inputtextarea, or select.

    Applying and Reapplying UTM Values

    The script includes several orchestration functions that ensure UTM values are applied reliably:

    • applyStoredUtmValues()
      Reads each UTM cookie and applies its value to any form fields with matching names.
    • applyUtmValues()
      If the current URL has UTM parameters, it sanitizes, stores, and applies them immediately; otherwise, it falls back to applying existing cookie values.
    • areUtmFieldsPopulated()
      Checks whether all target UTM fields currently contain a value, which helps avoid unnecessary reapplication.
    • reapplyUtmValuesUntilReady()
      Re-applies stored cookie values every 300ms for up to 10 seconds so that fields created or modified after initial page load still receive UTM values.
    • observeFormFields()
      Uses a MutationObserver to watch for DOM changes and reapply UTM values when new form fields are injected by third-party scripts or SPA frameworks.
    • initUtmFields()
      Kicks off the entire UTM population flow once the DOM is ready or the DOMContentLoaded event fires.

    Code to capture UTM values and set cookies, and persists to different pages

    <script>
        (function () {
          var utmFields = ['utm_campaign', 'utm_content', 'utm_medium', 'utm_source', 'utm_term'];
          var cookieDomain = getCookieDomain();
    
          function getCookieDomain() {
            var hostname = window.location.hostname;
            var parts = hostname.split('.');
            if (parts.length < 2) {
              return hostname;
            }
            if (parts.length === 2) {
              return '.' + hostname;
            }
            var tld = parts[parts.length - 1];
            var sld = parts[parts.length - 2];
            if (tld.length === 2 && sld.length <= 3 && parts.length > 2) {
              return '.' + parts.slice(parts.length - 3).join('.');
            }
            return '.' + parts.slice(parts.length - 2).join('.');
          }
    
          function sanitizeValue(value) {
            if (value == null) {
              return '';
            }
            var decoded = String(value);
            try {
              decoded = decodeURIComponent(decoded.replace(/\+/g, ' '));
            } catch (err) {
              decoded = decoded;
            }
            decoded = decoded.replace(/<[^>]*>/g, '');
            decoded = decoded.replace(/javascript:/gi, '');
            decoded = decoded.replace(/\b(onerror|onload|onclick|onmouseover|onmouseleave|onmouseenter|onfocus|onblur|style)\b/gi, '');
            decoded = decoded.replace(/["'`;]/g, '');
            return decoded.trim();
          }
    
          function getQueryParams() {
            var params = {};
            var query = window.location.search;
            if (!query) {
              return params;
            }
            var pairs = query.substring(1).split('&');
            for (var i = 0; i < pairs.length; i++) {
              var part = pairs[i];
              if (!part) {
                continue;
              }
              var pair = part.split('=');
              var name = pair[0] ? pair[0].toLowerCase() : '';
              if (utmFields.indexOf(name) === -1) {
                continue;
              }
              var value = pair.slice(1).join('=');
              params[name] = sanitizeValue(value);
            }
            return params;
          }
    
          function setCookie(name, value, days) {
            if (!name || value == null || value === '') {
              return;
            }
            var maxAge = (days || 7) * 24 * 60 * 60;
            document.cookie = name + '=' + encodeURIComponent(value) + ';max-age=' + maxAge + ';path=/;domain=' + cookieDomain + ';SameSite=Lax';
          }
    
          function getCookie(name) {
            if (!name) {
              return '';
            }
            var cookies = document.cookie ? document.cookie.split(';') : [];
            var prefix = name + '=';
            for (var i = 0; i < cookies.length; i++) {
              var cookie = cookies[i].trim();
              if (cookie.indexOf(prefix) === 0) {
                return decodeURIComponent(cookie.substring(prefix.length));
              }
            }
            return '';
          }
    
          function setFieldValue(name, value) {
            if (!name || value == null || value === '') {
              return false;
            }
            var fields = document.getElementsByName(name);
            var set = false;
            for (var i = 0; i < fields.length; i++) {
              var field = fields[i];
              if (!field || !field.tagName) {
                continue;
              }
              var tag = field.tagName.toLowerCase();
              if (tag === 'input' || tag === 'textarea' || tag === 'select') {
                field.value = value;
                set = true;
              }
            }
            return set;
          }
    
          function applyStoredUtmValues() {
            var applied = false;
            for (var j = 0; j < utmFields.length; j++) {
              var cookieName = utmFields[j];
              var storedValue = sanitizeValue(getCookie(cookieName));
              if (storedValue) {
                applied = setFieldValue(cookieName, storedValue) || applied;
              }
            }
            return applied;
          }
    
          function applyUtmValues() {
            var queryParams = getQueryParams();
            var hasQuery = Object.keys(queryParams).length > 0;
            if (hasQuery) {
              for (var i = 0; i < utmFields.length; i++) {
                var key = utmFields[i];
                var value = queryParams[key];
                if (value) {
                  setCookie(key, value, 7);
                  setFieldValue(key, value);
                }
              }
              return;
            }
    
            applyStoredUtmValues();
          }
    
          function areUtmFieldsPopulated() {
            var selectors = utmFields.map(function(name) {
              return 'input[name="' + name + '"],textarea[name="' + name + '"],select[name="' + name + '"]';
            }).join(',');
            var fields = document.querySelectorAll(selectors);
            if (!fields.length) {
              return false;
            }
            for (var i = 0; i < fields.length; i++) {
              if (!fields[i].value) {
                return false;
              }
            }
            return true;
          }
    
          function reapplyUtmValuesUntilReady() {
            var deadline = Date.now() + 10000;
            var intervalId = setInterval(function() {
              applyStoredUtmValues();
              if (areUtmFieldsPopulated() || Date.now() > deadline) {
                clearInterval(intervalId);
              }
            }, 300);
          }
    
          function observeFormFields() {
            if (!window.MutationObserver) {
              return;
            }
            var deadline = Date.now() + 5000;
            var observer = new MutationObserver(function() {
              if (applyStoredUtmValues() || Date.now() > deadline) {
                observer.disconnect();
              }
            });
            observer.observe(document.documentElement || document.body, {
              childList: true,
              subtree: true
            });
          }
    
          function initUtmFields() {
            applyUtmValues();
            observeFormFields();
            reapplyUtmValuesUntilReady();
          }
    
          if (document.readyState === 'complete' || document.readyState === 'interactive') {
            initUtmFields();
          } else {
            document.addEventListener('DOMContentLoaded', initUtmFields, false);
          }
        })();
      </script>

    Why It Works Well with Dynamic and Embedded Forms

    Many real-world forms are not present at initial page load. They may be:

    • Embedded via a third-party marketing platform.
    • Injected by a tag manager.
    • Rendered by a single-page application framework.

    The combination of a short-interval retry loop and a MutationObserver means this script doesn’t rely on timing luck. If a form appears in the DOM any time within the initial 10-second window—or triggers attribute changes that remove values—the script can re-apply the UTM data and keep the fields in sync.

    Practical Notes and Gotchas

    If a page is setting UTM cookies but a specific form is not receiving values, check the following:

    • Confirm that the UTM cookies exist for .webocreation.com using your browser’s developer tools.
    • Ensure the form contains hidden inputs (or visible fields) with names that exactly match the UTM keys: utm_campaignutm_sourceutm_mediumutm_contentutm_term.
    • Verify that the UTM cookie script is included before or alongside the form initialization logic, so it can observe DOM changes and start retries in time.
    • If a third-party embed overwrites the UTM fields after they are set, remember that the retry loop will continue reapplying values for up to 10 seconds—but beyond that, the external script may “win” the race.

    These checks typically resolve most “UTM not showing up” issues without needing to modify the script itself.


    When to Use This Pattern

    A cookie-based UTM persistence script is particularly useful when:

    • You operate multiple subdomains within the same brand or product experience.
    • You rely on embedded or dynamically rendered forms for lead capture.
    • You want a front-end solution that doesn’t require backend or CRM integration work.
    • You need a reusable pattern that can travel with your site templates and marketing pages.

    By capturing UTMs once and making them reliably available to every compatible form, you significantly improve campaign attribution accuracy with relatively little implementation effort.

    Rupak Nepali
    Author of four Opencart book. The recent are Opencart 4 developer book and Opencart 4 user manual
    RELATED ARTICLES

    LEAVE A REPLY

    Please enter your comment!
    Please enter your name here