Home
UTM Transfer Script
🔗

UTM Transfer Script

Attribution Continuity

Preserves UTM parameters across all internal navigation by automatically appending campaign data to links, ensuring attribution continuity throughout the user journey and preventing attribution loss.

AttributionIntermediateAttribution & Tracking

Key Features

Automatic link parameter appending
Cross-domain attribution support
Form submission tracking
Dynamic content link handling
Attribution preservation
Custom parameter mapping

Script Overview

📖

What It Does

This script automatically appends UTM parameters to all links on your website, ensuring campaign attribution is preserved as users navigate through your site and click on external links. Essential for maintaining attribution across multiple touchpoints.

🎯

Use Case

Crucial for multi-page funnels and external link tracking without losing campaign data. Perfect for websites with affiliate links, partner integrations, or complex user journeys spanning multiple domains.

Key Benefits

  • Maintains campaign attribution across navigation
  • Preserves UTM data for external link clicks
  • Enables cross-domain attribution tracking
  • Improves conversion path analysis
  • Enhances multi-touch attribution modeling
  • Reduces attribution data loss

JavaScript Code

<script>
(function() {
  'use strict';
  
  // UTM Transfer Script
  // Version: 1.0
  // Description: Transfers UTM parameters to all links for attribution continuity
  // Last Updated: 2025-09-09
  
  // Configuration
  var config = {
    // UTM parameters to transfer
    utmParams: ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content'],
    
    // Additional parameters to transfer
    customParams: ['gclid', 'fbclid', 'msclkid', 'ref'],
    
    // Link selectors to process
    linkSelectors: 'a[href]',
    
    // Domains to treat as internal (will get UTM params)
    internalDomains: [], // Leave empty to auto-detect or add specific domains
    
    // External domains to include (normally external links are processed)
    includeExternalDomains: [], // Specific external domains to include
    
    // Exclude links with these attributes/classes
    excludeSelectors: [
      '[data-no-utm]',
      '.no-utm',
      '[href^="mailto:"]',
      '[href^="tel:"]',
      '[href^="#"]',
      '[href^="javascript:"]'
    ].join(','),
    
    // Storage key for UTM data
    storageKey: 'utm_data',
    
    // Debug mode
    debug: false
  };

  // Utility functions
  function debugLog(message, data) {
    if (config.debug) {
      console.log('[UTM Transfer] ' + message, data || '');
    }
  }

  function getCurrentDomain() {
    return window.location.hostname.toLowerCase();
  }

  function isInternalDomain(hostname) {
    var currentDomain = getCurrentDomain();
    
    // If no internal domains specified, treat same domain as internal
    if (config.internalDomains.length === 0) {
      return hostname === currentDomain;
    }
    
    // Check against specified internal domains
    return config.internalDomains.some(function(domain) {
      return hostname.includes(domain.toLowerCase());
    });
  }

  function shouldIncludeExternalDomain(hostname) {
    if (config.includeExternalDomains.length === 0) {
      return true; // Include all external domains by default
    }
    
    return config.includeExternalDomains.some(function(domain) {
      return hostname.includes(domain.toLowerCase());
    });
  }

  function getStoredUTMParams() {
    try {
      var stored = localStorage.getItem(config.storageKey);
      if (stored) {
        var data = JSON.parse(stored);
        var params = {};
        
        // Extract UTM parameters
        config.utmParams.forEach(function(param) {
          if (data[param]) {
            params[param] = data[param];
          }
        });
        
        // Extract custom parameters
        config.customParams.forEach(function(param) {
          if (data[param]) {
            params[param] = data[param];
          }
        });
        
        return params;
      }
    } catch (e) {
      debugLog('Error reading stored UTM params:', e);
    }
    return {};
  }

  function getCurrentUTMParams() {
    var params = {};
    var urlParams = new URLSearchParams(window.location.search);
    
    // Get UTM parameters from current URL
    config.utmParams.forEach(function(param) {
      var value = urlParams.get(param);
      if (value) {
        params[param] = value;
      }
    });
    
    // Get custom parameters from current URL
    config.customParams.forEach(function(param) {
      var value = urlParams.get(param);
      if (value) {
        params[param] = value;
      }
    });
    
    return params;
  }

  function combineParams(currentParams, storedParams) {
    // Current URL params take precedence over stored params
    return Object.assign({}, storedParams, currentParams);
  }

  function appendParamsToUrl(url, params) {
    try {
      var urlObj = new URL(url, window.location.origin);
      var searchParams = urlObj.searchParams;
      
      // Add parameters that don't already exist
      Object.keys(params).forEach(function(key) {
        if (!searchParams.has(key) && params[key]) {
          searchParams.set(key, params[key]);
        }
      });
      
      return urlObj.toString();
    } catch (e) {
      debugLog('Error appending params to URL:', e);
      return url;
    }
  }

  function shouldProcessLink(link) {
    var href = link.getAttribute('href');
    
    // Skip if no href
    if (!href) return false;
    
    // Skip excluded selectors
    if (config.excludeSelectors && link.matches(config.excludeSelectors)) {
      return false;
    }
    
    // Skip if already processed
    if (link.hasAttribute('data-utm-processed')) {
      return false;
    }
    
    try {
      var linkUrl = new URL(href, window.location.origin);
      var linkHostname = linkUrl.hostname.toLowerCase();
      var currentDomain = getCurrentDomain();
      
      // Process internal links
      if (isInternalDomain(linkHostname)) {
        return true;
      }
      
      // Process external links if they should be included
      if (linkHostname !== currentDomain && shouldIncludeExternalDomain(linkHostname)) {
        return true;
      }
      
    } catch (e) {
      debugLog('Error processing link URL:', e);
    }
    
    return false;
  }

  function processLink(link, params) {
    if (Object.keys(params).length === 0) {
      return; // No parameters to add
    }
    
    var originalHref = link.getAttribute('href');
    var newHref = appendParamsToUrl(originalHref, params);
    
    if (newHref !== originalHref) {
      link.setAttribute('href', newHref);
      link.setAttribute('data-utm-processed', 'true');
      debugLog('Processed link:', {
        original: originalHref,
        updated: newHref,
        params: params
      });
    }
  }

  function processAllLinks() {
    debugLog('Processing all links on page');
    
    // Get current and stored UTM parameters
    var currentParams = getCurrentUTMParams();
    var storedParams = getStoredUTMParams();
    var allParams = combineParams(currentParams, storedParams);
    
    debugLog('UTM parameters to transfer:', allParams);
    
    if (Object.keys(allParams).length === 0) {
      debugLog('No UTM parameters found to transfer');
      return;
    }
    
    // Process all links
    var links = document.querySelectorAll(config.linkSelectors);
    var processedCount = 0;
    
    Array.prototype.forEach.call(links, function(link) {
      if (shouldProcessLink(link)) {
        processLink(link, allParams);
        processedCount++;
      }
    });
    
    debugLog('Processed ' + processedCount + ' links with UTM parameters');
  }

  function observeNewLinks() {
    // Create mutation observer to handle dynamically added links
    var observer = new MutationObserver(function(mutations) {
      var shouldProcess = false;
      
      mutations.forEach(function(mutation) {
        if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
          // Check if any added nodes contain links
          Array.prototype.forEach.call(mutation.addedNodes, function(node) {
            if (node.nodeType === 1) { // Element node
              if (node.tagName === 'A' || node.querySelector && node.querySelector('a')) {
                shouldProcess = true;
              }
            }
          });
        }
      });
      
      if (shouldProcess) {
        debugLog('New links detected, processing...');
        setTimeout(processAllLinks, 100); // Small delay to ensure DOM is ready
      }
    });
    
    observer.observe(document.body, {
      childList: true,
      subtree: true
    });
    
    debugLog('Link observer initialized');
  }

  // Main initialization
  function initUTMTransfer() {
    debugLog('Initializing UTM Transfer Script');
    
    // Process existing links
    processAllLinks();
    
    // Observe for new links
    observeNewLinks();
    
    // Re-process links when hash changes (for SPAs)
    window.addEventListener('hashchange', function() {
      setTimeout(processAllLinks, 100);
    });
  }

  // Initialize when DOM is ready
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', initUTMTransfer);
  } else {
    initUTMTransfer();
  }

  // Expose utility functions
  window.utmTransfer = {
    processLinks: function() {
      processAllLinks();
    },
    getParams: function() {
      return combineParams(getCurrentUTMParams(), getStoredUTMParams());
    },
    refreshProcessing: function() {
      // Remove processed flags and reprocess
      var processedLinks = document.querySelectorAll('[data-utm-processed]');
      Array.prototype.forEach.call(processedLinks, function(link) {
        link.removeAttribute('data-utm-processed');
      });
      processAllLinks();
    }
  };

})();
</script>

💡 Pro Tip: Copy the entire code block above and paste it directly into a GTM Custom HTML tag. The script is self-contained and ready to use immediately.