
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.
How Shopify's Google & YouTube channel silently tags free listing URLs with sag_organic, inflating GA4 organic revenue and starving Smart Bidding, and the exact Merchant Center Next fix (Attribute rules, not the old Feed Rules) that stops it at the source.

A real-world case study showing how attribution data (UTM, gclid, fbclid) was captured, persisted, and sent to Shopify orders for accurate marketing attribution. This guide walks through the architecture, implementation, and tracking strategy used in a live TinyBot client project.
Learn how to implement Meta Conversions API (CAPI) using your own server in a Next.js project. This guide breaks down the server-side tracking process step-by-step with real code and structure , no third-party tools needed. Perfect for developers and tracking experts who want full control.