
Author Name
Emtiaz Hossain
First-touch attribution credits the first channel that brought a user to your site. Last-touch attribution credits the final interaction before conversion. With Google Tag Manager server-side tracking, capture both using localStorage JSON and a custom sGTM custom template.
This GTM attribution schema persists UTM parameters and Google Click IDs (gclid, gbraid, wbraid) across sessions. The first touch vs last touch data flows through GA4 event parameters into server-side GTM variables for Google Ads offline conversions, Google Sheets lead pipelines, and CRM webhooks.
First-touch assigns 100% credit to the first marketing channel. Last-touch assigns 100% credit to the last channel before conversion. For a useful marketing attribution model, you need both perspectives.
Example: User clicks Google Ad (gclid captured as first touch), leaves, returns 3 days later via organic search (utm_source=google as last touch), then converts. First-touch credits Google Ads. Last-touch credits organic search.
Stored in localStorage under attribution_data. The UTM parameter tracking schema sets first_touch once and updates last_touch on every page visit:
{
"schema_version": 1,
"first_touch": {
"gclid": "Cj0K...",
"gbraid": "",
"wbraid": "",
"utm_source": "google",
"utm_medium": "cpc",
"utm_campaign": "kitchen_remodel"
},
"last_touch": {
"gclid": "",
"gbraid": "",
"wbraid": "",
"utm_source": "google",
"utm_medium": "organic",
"utm_campaign": ""
},
"first_touch_timestamp": "2026-07-15 09:23:14",
"last_touch_timestamp": "2026-07-17 14:05:32"
}Tag type: Custom HTML. Name: cHTML - URL Param store in LS. Trigger: Initialization - All Pages. Fires before GA4 loads.
<script>
(function () {
var STORAGE_KEY = "attribution_data";
var storage = {
schema_version: 1,
first_touch: {},
last_touch: {},
first_touch_timestamp: null,
last_touch_timestamp: null
};
if (!window.location.search || window.location.search.length <= 1) {
return;
}
try {
var existing = JSON.parse(localStorage.getItem(STORAGE_KEY));
if (existing) {
storage = existing;
if (!storage.schema_version) {
storage.schema_version = 1;
}
}
} catch (e) {}
var params = {};
var query = window.location.search.substring(1);
var pairs = query.split("&");
for (var i = 0; i < pairs.length; i++) {
if (!pairs[i]) continue;
var pair = pairs[i].split("=");
var key = decodeURIComponent(pair[0]);
var value = pair.length > 1
? decodeURIComponent(pair.slice(1).join("=").replace(/\+/g, " "))
: "";
if (key) params[key] = value;
}
if (Object.keys(params).length === 0) return;
function formatTimestamp(d) {
return d.getFullYear() + "-" +
String(d.getMonth() + 1).padStart(2, "0") + "-" +
String(d.getDate()).padStart(2, "0") + " " +
String(d.getHours()).padStart(2, "0") + ":" +
String(d.getMinutes()).padStart(2, "0") + ":" +
String(d.getSeconds()).padStart(2, "0");
}
if (!storage.first_touch_timestamp) {
storage.first_touch = params;
storage.first_touch_timestamp = formatTimestamp(new Date());
}
storage.last_touch = params;
storage.last_touch_timestamp = formatTimestamp(new Date());
localStorage.setItem(STORAGE_KEY, JSON.stringify(storage));
})();
</script>Create Custom JavaScript variable JS - Attribution Data:
function() {
try {
return JSON.stringify(
JSON.parse(localStorage.getItem("attribution_data") || "{}")
);
} catch(e) {
return "{}";
}
}Reads localStorage and serializes for passing as GA4 custom event parameter.
In each GA4 event tag (generate_lead, phone_click, email_click, map_click):
custom_properties{{JS - Attribution Data}}The GA4 event parameters carry the full attribution JSON to server-side GTM. The sGTM GA4 client receives it and makes it available to server-side GTM variables.
Download the Attribution JSON Parser from GitHub:
https://github.com/EmtiazHossainE2/attribution-json-parserAttribution JSON Parser.tplInputs: jsonInput (serialized JSON) + path (dot-notation like first_touch.gclid)
Create Event Data Variable, keyPath custom_properties, name ED - custom_properties. Feeds the GA4 event parameter into the parser.
For each field: variable > type Attribution JSON Parser. jsonInput = {{ED - custom_properties}}. Path = field path.
First-touch:
FT - gclidFT - gbraidFT - wbraidFT - utm_sourceFT - utm_mediumFT - utm_campaignFT - utm_termFT - utm_contentFT - timestampLast-touch:
LT - gclidLT - gbraidLT - wbraidLT - utm_sourceLT - utm_mediumLT - utm_campaignLT - utm_termLT - utm_contentLT - timestampAlso add schema_version.
Building FT/LT attribution in GTM gives you both top-of-funnel discovery and bottom-of-funnel conversion visibility. Lightweight approach works with any GA4 property or ad platform supporting URL parameters.
Get the template: https://github.com/EmtiazHossainE2/attribution-json-parser
Discussed this on LinkedIn
Found this helpful?
Drop a comment on the LinkedIn post – I read every reply and love connecting with tracking folks.
Book a free 30-minute call. I will review your current tracking setup, show you where conversions are leaking, and tell you exactly what I would fix.
A full GA4/GTM/Google Ads audit on a UK Shopify store spending £18,000+/month found duplicate conversion tags inflating reported purchases 2x, a dropped GCLID at checkout, and misconfigured variables silently degrading Enhanced Conversions. Here's the audit, the fixes, and the migration off a legacy GTM setup before Shopify's deprecation deadline.
How a UK marketing agency's LinkedIn Insight Tag upgrade turned into a full conversion audit, catching duplicate tags and stale triggers across eight lead-gen conversions.
Duplicate form conversions in GA4 are usually a reload firing the same tag twice, not a real second submission. Here's the Custom JavaScript variable that catches it, plus other common causes to rule out.