Using GTM to Pass Your UTMs Into Your CRM — A Complete Recipe With Plain JavaScript
Getting UTM parameters from a campaign link on the open web into a CRM like Salesforce or HubSpot is one of the most recurring questions in web analytics. The UTM only exists on the landing page URL — but the form that captures the lead is often several pages, and several minutes, later in the visit. This recipe walks through every hop the UTM has to make to survive that journey.
Credit where it's due: the seed for what follows is "Transfer UTM Parameters From One Page To Another with GTM" by Julius Fedorovicius of Analytics Mania. Read that article first for the deeper background — this post adapts and extends that pattern across the several places a UTM needs to travel before it lands as a field value in your CRM.
The Core Problem: A UTM Only Exists on One URL
A UTM parameter is appended to a single landing URL by whoever built the campaign link. The browser does nothing to carry that value forward automatically. The moment the visitor clicks an internal link and moves to a second page, the UTM is gone from the address bar — yet your lead form, three or four pages into the journey, still needs it. The fix is to capture the UTM once, on arrival, and persist it somewhere durable enough to survive the rest of the session.
Step 1: Persist the Incoming UTM
There are two reasonable places to park the UTM values the moment they arrive: sessionStorage, or a first-party session cookie. Either works; the cookie option has the advantage of being readable across subdomains, which matters if your CRM form lives on a different hostname than your marketing pages.
Option A — Write UTMs to sessionStorage
<script type="text/javascript">
// Parse the URL
function getParameterByName(name) {
name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"),
results = regex.exec(location.search.toLowerCase());
return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
}
var queryArray = ['utm_medium', 'utm_source', 'utm_campaign', 'utm_content', 'utm_term'];
queryArray.forEach(function(item) {
var queryValue = '';
try {
queryValue = getParameterByName(item);
} catch (err) {
queryValue = '';
}
if (typeof(queryValue) == 'undefined' || queryValue == null) { }
if (queryValue.search(/[0-9A-Za-z]+/) >= 0) {
if (!(item in sessionStorage)) {
sessionStorage.setItem(item, queryValue);
}
}
});
</script>
Option B — Write UTMs to a First-Party Session Cookie
<script type="text/javascript">
/* Get Domain */
function getDomainName() {
var hostName = window.location.hostname.toLowerCase();
return hostName.substring(hostName.lastIndexOf(".", hostName.lastIndexOf(".") - 1) + 1);
}
/* Set Cookie */
function setCookie(cookieName, cookieValue) {
var theDomain = getDomainName();
document.cookie = cookieName + "=" + cookieValue + "; path=/; SameSite=None; Secure; domain=." + theDomain + ";";
}
// Parse the URL
function getParameterByName(name) {
name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"),
results = regex.exec(location.search.toLowerCase());
return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
}
var queryArray = ['utm_medium', 'utm_source', 'utm_campaign', 'utm_content', 'utm_term'];
queryArray.forEach(function(item) {
var queryValue = '';
try {
queryValue = getParameterByName(item);
} catch (err) {
queryValue = '';
}
if (typeof(queryValue) == 'undefined' || queryValue == null) { }
if (queryValue.search(/[0-9A-Za-z]+/) >= 0) {
setCookie(item, queryValue);
}
});
</script>
The trigger for either script is a pageview where the page URL contains a question mark — meaning a UTM (or any query string) is present to capture.
Step 2: Carry the UTM Forward to Wherever the Form Lives
For the rest of this recipe, the examples assume you're reading the UTM back out of the first-party session cookie, although the same logic applies if you used sessionStorage instead. Where the UTM needs to go next depends entirely on where the form actually lives.
If the Form Is on a Separate Site — Decorate the Outbound Link
When the form is hosted on a different domain, you need to append the stored UTM values onto the outbound link URL itself before the visitor clicks it:
<script>
// Based on Inherit UTMs by Julius Fedorovicius
(function() {
var domainsToDecorate = [
'www2.mydomain.com', //add or remove domains (without https or trailing slash)
'www.youtube.com',
'zyxw9876.com'
],
queryParams = [
'utm_medium', //add or remove query parameters you want to transfer
'utm_source',
'utm_campaign',
'utm_content',
'utm_term'
]
// Get Cookie
function getCookie(cookieName) {
var mycookieArray = document.cookie.split("; ");
for (var i = 0; i < mycookieArray.length; i++) {
var mycookie = mycookieArray[i].split("=");
if (mycookie[0] === cookieName) {
return decodeURIComponent(mycookie[1]);
}
}
}
// do not edit anything below this line
var links = document.querySelectorAll('a');
// check if links contain domain from the domainsToDecorate array and then decorates
for (var linkIndex = 0; linkIndex < links.length; linkIndex++) {
for (var domainIndex = 0; domainIndex < domainsToDecorate.length; domainIndex++) {
if (links[linkIndex].href.indexOf(domainsToDecorate[domainIndex]) > -1 && links[linkIndex].href.indexOf("#") === -1) {
links[linkIndex].href = decorateUrl(links[linkIndex].href);
}
}
}
// decorates the URL with query params
function decorateUrl(urlToDecorate) {
urlToDecorate = (urlToDecorate.indexOf('?') === -1) ? urlToDecorate + '?' : urlToDecorate + '&';
var collectedQueryParams = [];
for (var queryIndex = 0; queryIndex < queryParams.length; queryIndex++) {
if (getQueryParam(queryParams[queryIndex])) {
collectedQueryParams.push(queryParams[queryIndex] + '=' + getQueryParam(queryParams[queryIndex]))
}
}
return urlToDecorate + collectedQueryParams.join('&');
}
// function retrieves the values from cookie
function getQueryParam(name) {
var itemValue = '';
try {
itemValue = getCookie(name);
} catch (err) {
itemValue = '';
}
if (typeof(itemValue) == 'undefined' || itemValue == null) { }
if (itemValue.search(/[0-9A-Za-z]+/) >= 0) {
return itemValue;
}
}
})();
</script>
If the Form Is in an Iframe — Decorate the Iframe src
An embedded iframe is treated by the browser as an entirely separate document, so its src attribute needs the same decoration treatment as an outbound link:
<script>
// Based on Inherit UTMs by Julius Fedorovicius
(function() {
var domainsToDecorate = [
'www2.mydomain.com', //add or remove domains (without https or trailing slash)
'www.youtube.com',
'zyxw9876.com'
],
queryParams = [
'utm_medium', //add or remove query parameters you want to transfer
'utm_source',
'utm_campaign',
'utm_content',
'utm_term'
]
// Parse the URL
function getParameterByName(name) {
name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"),
results = regex.exec(location.search.toLowerCase());
return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
}
// do not edit anything below this line
var links = document.querySelectorAll('iframe');
// check if links contain domain from the domainsToDecorate array and then decorates
for (var linkIndex = 0; linkIndex < links.length; linkIndex++) {
for (var domainIndex = 0; domainIndex < domainsToDecorate.length; domainIndex++) {
if (links[linkIndex].src.indexOf(domainsToDecorate[domainIndex]) > -1 && links[linkIndex].src.indexOf("#") === -1) {
links[linkIndex].src = decorateUrl(links[linkIndex].src);
}
}
}
// decorates the URL with query params
function decorateUrl(urlToDecorate) {
urlToDecorate = (urlToDecorate.indexOf('?') === -1) ? urlToDecorate + '?' : urlToDecorate + '&';
var collectedQueryParams = [];
for (var queryIndex = 0; queryIndex < queryParams.length; queryIndex++) {
if (getQueryParam(queryParams[queryIndex])) {
collectedQueryParams.push(queryParams[queryIndex] + '=' + getQueryParam(queryParams[queryIndex]))
}
}
return urlToDecorate + collectedQueryParams.join('&');
}
// function retrieves the values from cookie
function getQueryParam(name) {
var itemValue = '';
try {
itemValue = getParameterByName(name);
} catch (err) {
itemValue = '';
}
if (typeof(itemValue) == 'undefined' || itemValue == null) { }
if (itemValue.search(/[0-9A-Za-z]+/) >= 0) {
return itemValue;
}
}
})();
</script>
If the Form Is in the Body of the Page — Populate Hidden Fields Directly
When the form lives directly in the page's DOM, there is no URL to decorate — the cleanest path is to write the stored UTM values straight into hidden form fields. The field selectors below are illustrative; align them with your own form markup.
<script type="text/javascript">
// Get Cookie
function getCookie(cookieName) {
var mycookieArray = document.cookie.split("; ");
for (var i = 0; i < mycookieArray.length; i++) {
var mycookie = mycookieArray[i].split("=");
if (mycookie[0] === cookieName) {
return decodeURIComponent(mycookie[1]);
}
}
}
var queryArray = ['utm_medium', 'utm_source', 'utm_campaign', 'utm_content', 'utm_term'];
queryArray.forEach(function(item) {
var itemValue = '';
try {
itemValue = getCookie(item);
} catch (err) {
itemValue = '';
}
if (typeof(itemValue) == 'undefined' || itemValue == null) { }
if (itemValue.search(/[0-9A-Za-z]+/) >= 0) {
var cssSelector = 'input[data-sc-field-name="' + item + '"]';
document.querySelector(cssSelector).value = itemValue;
}
});
</script>
The trigger for these field-population scripts is the page on which the form (or the link/iframe pointing to the form) actually appears.
If the Form Is an Off-Site Link or Iframe — Read the UTM Straight From the URL
If the form itself sits behind an off-site link or an iframe, that destination page must run its own script to pull the UTMs back out of its own URL (the same values decorated onto it in the steps above) and write them into its hidden fields:
<script type="text/javascript">
// Parse the URL
function getParameterByName(name) {
name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"),
results = regex.exec(location.search.toLowerCase());
return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
}
var queryArray = ['utm_medium', 'utm_source', 'utm_campaign', 'utm_content', 'utm_term'];
queryArray.forEach(function(item) {
var itemValue = '';
try {
itemValue = getParameterByName(item);
} catch (err) {
itemValue = '';
}
if (typeof(itemValue) == 'undefined' || itemValue == null) { }
if (itemValue.search(/[0-9A-Za-z]+/) >= 0) {
var cssSelector = 'input[data-sc-field-name="' + item + '"]';
document.querySelector(cssSelector).value = itemValue;
}
});
</script>
Going Further: Send the Anonymous Client ID and GA4 Stream Too
UTMs alone tell your CRM about the campaign — they don't tell it which anonymous browser session generated the lead, which matters if you ever want to join CRM records back to BigQuery or GA4 event data. Once you already have a GTM 1st-party cookie variable exposing the _ga client ID (see "Anonymous Browser Client Id and Session Id" for how to source that value) and a variable for the active GA4 Measurement Stream, the same hidden-field pattern used for the UTMs applies here as well — this is the missing piece in the original post, where only a screenshot illustrated the technique:
<script type="text/javascript">
document.querySelector('input[data-sc-field-name="clientId"]').value = {{Anonymous Browser Client ID Cookie}};
document.querySelector('input[data-sc-field-name="measurementStream"]').value = {{GA4 Measurement Stream ID}};
</script>
As with the UTM hidden-field example, this illustration assumes the form is in the document body. If the form is instead an off-site link or an iframe, the client ID and stream values must be passed as URL query parameters using the same decoration techniques shown above, rather than written directly into hidden fields.
Summary of the Full Path a UTM Travels
| Step | What Happens |
|---|---|
| 1. Landing | UTM arrives on the page URL; captured to sessionStorage or a first-party session cookie. |
| 2. Navigation | Outbound links and/or iframe src attributes pointing at the CRM are decorated with the stored UTM values. |
| 3. Arrival at the form's domain | If off-site or iframe, the destination page reads the UTM back off its own URL. |
| 4. Field population | UTM values (and optionally client ID / measurement stream) are written into hidden form fields immediately before submission. |
| 5. CRM record created | The CRM now has full campaign attribution attached to the resulting lead or contact. |
And that is all it takes. You have just greatly enhanced your campaign attribution all the way from a link on a social media post to the resulting individual record in your CRM.
This post is a Blogger rewrite of an earlier piece originally published on Weebly. The hidden-field client ID / measurement stream snippet above extends that original post's illustration, following the same pattern used in the companion post "Sending the Source and Landing Page to a CRM."
Comments
Post a Comment