top of page
bottom of page
/* UTM Tracker v2.0 - first/latest touch attribution and form autofill */
(function (window, document) {
"use strict";
var userConfig = window.UTM_TRACKER_CONFIG || {};
var config = {
storageKey: userConfig.storageKey || "utm_attribution_v2",
cookieName: userConfig.cookieName || "utm_attribution_v2",
ownedDomains: Array.isArray(userConfig.ownedDomains) ? userConfig.ownedDomains : [],
defaultTouch: userConfig.defaultTouch === "latest" ? "latest" : "first",
cookieDays: Math.min(Number(userConfig.cookieDays) || 400, 400),
linkerMinutes: Math.min(Number(userConfig.linkerMinutes) || 30, 1440),
decorateLinks: userConfig.decorateLinks !== false,
removeLinkerFromUrl: userConfig.removeLinkerFromUrl !== false,
debug: userConfig.debug === true,
canStore: typeof userConfig.canStore === "function" ? userConfig.canStore : function () { return true; }
};
var PARAMS = [
"utm_source", "utm_medium", "utm_campaign", "utm_term", "utm_content",
"utm_id", "utm_source_platform", "utm_creative_format",
"utm_marketing_tactic", "gclid", "gbraid", "wbraid", "fbclid",
"msclkid", "ttclid", "twclid", "li_fat_id", "dclid", "srsltid"
];
var META = ["landing_page", "referrer", "captured_at"];
var LINKER_PARAM = "_utm_attribution";
var MAX_VALUE_LENGTH = 2048;
var memoryData = emptyData();
var observedRoots = [];
var scheduled = false;
function emptyData() { return { first: null, latest: null }; }
function log() {
if (config.debug && window.console) {
window.console.log.apply(window.console, ["[UTM Tracker]"].concat([].slice.call(arguments)));
}
}
function clean(value, max) {
if (typeof value !== "string") return "";
return value.replace(/[\u0000-\u001F\u007F]/g, "").trim().slice(0, max || MAX_VALUE_LENGTH);
}
function parseJSON(value) {
try {
var parsed = JSON.parse(value);
return parsed && typeof parsed === "object" ? parsed : null;
} catch (_) { return null; }
}
function storageAllowed() {
try { return config.canStore() === true; } catch (_) { return false; }
}
function validTouch(value) {
if (!value || typeof value !== "object") return null;
var result = {};
PARAMS.concat(META).forEach(function (key) {
var item = clean(value[key] || "", key === "captured_at" ? 40 : MAX_VALUE_LENGTH);
if (item) result[key] = item;
});
return Object.keys(result).length ? result : null;
}
function validData(value) {
return {
first: validTouch(value && value.first),
latest: validTouch(value && value.latest)
};
}
function readCookie() {
var match = document.cookie.match(new RegExp("(?:^|; )" + config.cookieName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "=([^;]*)"));
if (!match) return null;
try { return parseJSON(decodeURIComponent(match[1])); } catch (_) { return null; }
}
function compactData(data) {
function compactTouch(touch) {
if (!touch) return null;
var result = {};
PARAMS.forEach(function (key) { if (touch[key]) result[key] = clean(touch[key], 250); });
if (touch.captured_at) result.captured_at = touch.captured_at;
return Object.keys(result).length ? result : null;
}
return { first: compactTouch(data.first), latest: compactTouch(data.latest) };
}
function cookieData(data) {
var compact = compactData(data);
var encoded = encodeURIComponent(JSON.stringify(compact));
if (encoded.length <= 3800) return encoded;
/* Stay below the practical 4 KB cookie limit by retaining values in
priority order. localStorage still keeps the complete record. */
var priority = ["utm_source", "utm_medium", "utm_campaign", "utm_id",
"utm_term", "utm_content", "gclid", "gbraid", "wbraid", "fbclid",
"msclkid", "ttclid", "twclid", "li_fat_id", "dclid", "srsltid"];
var reduced = emptyData();
["first", "latest"].forEach(function (touchName) {
if (!compact[touchName]) return;
reduced[touchName] = {};
priority.forEach(function (key) {
if (!compact[touchName][key]) return;
var candidate = JSON.parse(JSON.stringify(reduced));
candidate[touchName][key] = clean(compact[touchName][key], 120);
var candidateEncoded = encodeURIComponent(JSON.stringify(candidate));
if (candidateEncoded.length <= 3800) reduced = candidate;
});
});
return encodeURIComponent(JSON.stringify(reduced));
}
function readData() {
if (!storageAllowed()) return memoryData;
var parsed = null;
try { parsed = parseJSON(window.localStorage.getItem(config.storageKey)); } catch (_) {}
if (!parsed) parsed = readCookie();
memoryData = validData(parsed || memoryData);
return memoryData;
}
function writeData(data) {
memoryData = validData(data);
if (!storageAllowed()) return;
try { window.localStorage.setItem(config.storageKey, JSON.stringify(memoryData)); } catch (error) { log("localStorage unavailable", error); }
try {
var value = cookieData(memoryData);
document.cookie = config.cookieName + "=" + value + "; Path=/; Max-Age=" + Math.floor(config.cookieDays * 86400) + "; SameSite=Lax" + (location.protocol === "https:" ? "; Secure" : "");
} catch (error) { log("cookie unavailable", error); }
}
function allParamValues(search, key) {
var values = search.getAll(key);
for (var i = 0; i < values.length; i += 1) {
var value = clean(values[i]);
if (value) return value;
}
return "";
}
function base64UrlEncode(value) {
var bytes = new TextEncoder().encode(value), binary = "";
bytes.forEach(function (byte) { binary += String.fromCharCode(byte); });
return window.btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}
function base64UrlDecode(value) {
var input = value.replace(/-/g, "+").replace(/_/g, "/");
input += "===".slice((input.length + 3) % 4);
var binary = window.atob(input), bytes = new Uint8Array(binary.length);
for (var i = 0; i < binary.length; i += 1) bytes[i] = binary.charCodeAt(i);
return new TextDecoder().decode(bytes);
}
function readLinker(search) {
var raw = clean(search.get(LINKER_PARAM) || "", 12000);
if (!raw) return null;
try {
var payload = parseJSON(base64UrlDecode(raw));
if (!payload || !Number.isFinite(payload.created)) return null;
if (Math.abs(Date.now() - payload.created) > config.linkerMinutes * 60000) return null;
return validData(payload.data);
} catch (_) { return null; }
}
function removeLinker() {
if (!config.removeLinkerFromUrl || !window.history || !window.history.replaceState) return;
try {
var url = new URL(location.href);
if (!url.searchParams.has(LINKER_PARAM)) return;
url.searchParams.delete(LINKER_PARAM);
history.replaceState(history.state, "", url.href);
} catch (_) {}
}
function capture() {
var search = new URL(location.href).searchParams;
var linked = readLinker(search);
var data = readData();
if (linked) {
if (!data.first && linked.first) data.first = linked.first;
if (linked.latest) data.latest = linked.latest;
removeLinker();
}
var touch = {};
PARAMS.forEach(function (key) {
var value = allParamValues(search, key);
if (value) touch[key] = value;
});
if (Object.keys(touch).length) {
touch.landing_page = clean(location.href);
touch.referrer = clean(document.referrer || "");
touch.captured_at = new Date().toISOString();
if (!data.first) data.first = touch;
data.latest = touch;
}
writeData(data);
return data;
}
function normalizeName(field) {
return clean(field.getAttribute("data-utm-field") || field.name || field.id || "", 200)
.toLowerCase().replace(/\[\]$/, "").replace(/[\s.-]+/g, "_");
}
function fieldInfo(field) {
var name = normalizeName(field), touch = config.defaultTouch;
if (/^(first_touch_|first_|original_)/.test(name)) { touch = "first"; name = name.replace(/^(first_touch_|first_|original_)/, ""); }
else if (/^(latest_touch_|latest_|last_touch_|last_)/.test(name)) { touch = "latest"; name = name.replace(/^(latest_touch_|latest_|last_touch_|last_)/, ""); }
var aliases = { landing_url: "landing_page", utm_landing_page: "landing_page", utm_referrer: "referrer", utm_captured_at: "captured_at" };
name = aliases[name] || name;
return PARAMS.concat(META).indexOf(name) === -1 ? null : { key: name, touch: touch };
}
function setNativeValue(field, value) {
var prototype = field.tagName === "TEXTAREA" ? window.HTMLTextAreaElement.prototype : window.HTMLInputElement.prototype;
var setter = Object.getOwnPropertyDescriptor(prototype, "value");
if (setter && setter.set) setter.set.call(field, value); else field.value = value;
field.setAttribute("value", value);
field.dispatchEvent(new Event("input", { bubbles: true }));
field.dispatchEvent(new Event("change", { bubbles: true }));
}
function fillField(field, data) {
if (!field || field.disabled || /^(button|submit|reset|file|checkbox|radio)$/i.test(field.type || "")) return;
var info = fieldInfo(field);
if (!info) return;
var existing = clean(field.value || "");
if (existing && existing.toLowerCase() !== "no utm") return;
var primary = data[info.touch], fallback = data[info.touch === "first" ? "latest" : "first"];
var value = (primary && primary[info.key]) || (fallback && fallback[info.key]) || "";
if (value) setNativeValue(field, value);
}
function scan(root) {
var data = readData(), fields = [];
if (root.matches && root.matches("input,textarea")) fields.push(root);
if (root.querySelectorAll) fields = fields.concat([].slice.call(root.querySelectorAll("input,textarea")));
fields.forEach(function (field) { fillField(field, data); });
if (root.querySelectorAll) {
[].slice.call(root.querySelectorAll("*")).forEach(function (element) {
if (element.shadowRoot) observeRoot(element.shadowRoot);
});
}
}
function scheduleScan() {
if (scheduled) return;
scheduled = true;
(window.requestAnimationFrame || window.setTimeout)(function () { scheduled = false; scan(document); }, 0);
}
function observeRoot(root) {
if (!root || observedRoots.indexOf(root) !== -1) return;
observedRoots.push(root);
scan(root);
new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
[].slice.call(mutation.addedNodes).forEach(function (node) { if (node.nodeType === 1) scan(node); });
});
}).observe(root, { childList: true, subtree: true });
}
function owned(hostname) {
var host = hostname.toLowerCase().replace(/^www\./, "");
return config.ownedDomains.some(function (domain) {
var candidate = clean(domain, 253).toLowerCase().replace(/^https?:\/\//, "").split("/")[0].replace(/^www\./, "").replace(/:\d+$/, "");
return host === candidate || host.slice(-candidate.length - 1) === "." + candidate;
});
}
function decorate(anchor) {
if (!config.decorateLinks || !anchor || !config.ownedDomains.length) return;
try {
var url = new URL(anchor.href, location.href);
if (!/^https?:$/.test(url.protocol) || !owned(url.hostname) || url.hostname === location.hostname || url.searchParams.has(LINKER_PARAM)) return;
var data = readData();
if (!data.first && !data.latest) return;
var payload = base64UrlEncode(JSON.stringify({ created: Date.now(), data: compactData(data) }));
if (payload.length <= 6000) url.searchParams.set(LINKER_PARAM, payload);
anchor.href = url.href;
} catch (_) {}
}
function handleRouteChange() {
window.setTimeout(function () { capture(); scheduleScan(); }, 0);
}
function clearData() {
memoryData = emptyData();
try { window.localStorage.removeItem(config.storageKey); } catch (_) {}
try {
document.cookie = config.cookieName + "=; Path=/; Max-Age=0; SameSite=Lax" + (location.protocol === "https:" ? "; Secure" : "");
} catch (_) {}
}
function patchHistory(method) {
if (!window.history || !history[method]) return;
var original = history[method];
history[method] = function () {
var result = original.apply(this, arguments);
handleRouteChange();
return result;
};
}
function init() {
capture();
observeRoot(document.documentElement);
document.addEventListener("submit", function (event) { scan(event.target); }, true);
document.addEventListener("focusin", function (event) { fillField(event.target, readData()); }, true);
document.addEventListener("pointerdown", function (event) { decorate(event.target.closest && event.target.closest("a[href]")); }, true);
document.addEventListener("click", function (event) { decorate(event.target.closest && event.target.closest("a[href]")); }, true);
document.addEventListener("contextmenu", function (event) { decorate(event.target.closest && event.target.closest("a[href]")); }, true);
document.addEventListener("keydown", function (event) { if (event.key === "Enter") decorate(event.target.closest && event.target.closest("a[href]")); }, true);
window.addEventListener("popstate", handleRouteChange);
patchHistory("pushState");
patchHistory("replaceState");
}
/* Useful for consent managers and debugging. Call refresh() immediately
after analytics/marketing consent is granted. */
window.UTMTracker = {
refresh: function () { var data = capture(); scheduleScan(); return data; },
get: function () { return validData(readData()); },
clear: clearData
};
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", init, { once: true });
else init();
})(window, document);