Unmasking Marketing Attribution - A Techie's Guide to Getting It Right
by Eugene Venger, Data Engineer
The Need for Marketing Attribution
The digital universe is inundated with touchpoints. From that casual tweet to those pesky display ads, your consumers meet you in a myriad of places. Knowing which touchpoint seals the deal helps us pump more energy (and budget!) into what actually works.
The Challenge
-
Data Sparsity: Accurate attribution is hard if your data is scattered like a jigsaw puzzle.
-
Platform Limitations: Relying on a single platform like Google Analytics for attribution is like cooking with one hand tied behind your back.
-
Customer Journey Complexity: A user might flirt with several channels before going steady with your service. How do you keep track?
What, How, and Why
What: We're not here to play guess-the-channel with our marketing campaigns. Instead, we're capturing the last traffic source of a visitor for rock-solid marketing attribution, personalization, and an in-depth understanding of channel performance.
How: Google Tag Manager for the nitty-gritty capture work to store all required data in the cookie, JavaScript and hidden fields for holding onto the source and medium, and ActiveCampaign for tailored marketing. Finally Looker (former Google Data Studio) for data viz.
Why: Because knowledge is power, and we're turning that power into more conversions, better customer relationships, and a marketing ROI that gives us a competitive advantage.
Now, let's cut to the chase and talk implementation.
Set Up GTM Tag to Read the Last Traffic Source of Visitor
Step 1: Creating a Custom JavaScript Tag
Navigate to Google Tag Manager and open a new Tag. Choose the "Custom HTML Tag" as your Tag type. In the HTML tab, paste the custom script.
Script solution inspired by measureschool
Click to expand code snippet
<script>
(function (document) {
var referrer = document.referrer;
var gaReferral = {
'utmcsr': '(direct)',
'utmcmd': '(none)',
};
var thisHostname = document.location.hostname;
var thisDomain = getDomain_(thisHostname);
var referringDomain = getDomain_(document.referrer);
var sessionCookie = getCookie_('__utmzzses');
var cookieExpiration = new Date(+new Date() + 1000 * 60 * 60 * 24 * 30 * 24);
var qs = document.location.search.replace('?', '');
var hash = document.location.hash.replace('#', '');
var gaParams = parseGoogleParams(qs + '#' + hash);
var referringInfo = parseGaReferrer(referrer);
var storedVals = getCookie_('__utmz') || getCookie_('__utmzz');
var newCookieVals = [];
var keyMap = {
'utm_source': 'utmcsr',
'utm_medium': 'utmcmd',
};
var keyFilter = ['utmcsr', 'utmcmd'];
var keyName,
values,
_val,
_key,
raw,
key,
len,
i;
if (sessionCookie && referringDomain === thisDomain) {
gaParams = null;
referringInfo = null;
}
if (gaParams && (gaParams.utm_source || gaParams.gclid || gaParams.dclid)) {
for (key in gaParams) {
if (typeof gaParams[key] !== 'undefined') {
keyName = keyMap[key];
gaReferral[keyName] = gaParams[key];
}
}
if (gaParams.gclid || gaParams.dclid) {
gaReferral.utmcsr = 'google';
gaReferral.utmcmd = gaReferral.utmgclid ? 'cpc' : 'cpm';
}
} else if (referringInfo) {
gaReferral.utmcsr = referringInfo.source;
gaReferral.utmcmd = referringInfo.medium;
if (referringInfo.term) gaReferral.utmctr = referringInfo.term;
} else if (storedVals) {
values = {};
raw = storedVals.split('|');
len = raw.length;
for (i = 0; i < len; i++) {
_val = raw[i].split('=');
_key = _val[0].split('.').pop();
values[_key] = _val[1];
}
gaReferral = values;
}
for (key in gaReferral) {
if (typeof gaReferral[key] !== 'undefined' && keyFilter.indexOf(key) > -1) {
newCookieVals.push(gaReferral[key]);
}
}
if (referringDomain !== thisDomain && !isPageReloaded()) {
writeCookie_('lastTrafficSource', newCookieVals.join('/'), cookieExpiration, '/', thisDomain);
}
writeCookie_('__utmzzses', 1, null, '/', thisDomain);
function isPageReloaded() {
if (performance.navigation.type == performance.navigation.TYPE_RELOAD) {
return true
}
else {
return false
}
}
function parseGoogleParams(str) {
var campaignParams = ['source', 'medium', 'campaign', 'term', 'content'];
var regex = new RegExp('(utm_(' + campaignParams.join('|') + ')|(d|g)clid)=.*?([^&#]*|$)', 'gi');
var gaParams = str.match(regex);
var paramsObj,
vals,
len,
i;
if (gaParams) {
paramsObj = {};
len = gaParams.length;
for (i = 0; i < len; i++) {
vals = gaParams[i].split('=');
if (vals) {
paramsObj[vals[0]] = vals[1];
}
}
}
return paramsObj;
}
function parseGaReferrer(referrer) {
if (!referrer) return;
var searchEngines = {
'daum.net': {
'p': 'q',
'n': 'daum'
},
'eniro.se': {
'p': 'search_word',
'n': 'eniro '
},
'naver.com': {
'p': 'query',
'n': 'naver '
},
'yahoo.com': {
'p': 'p',
'n': 'yahoo'
},
'msn.com': {
'p': 'q',
'n': 'msn'
},
'bing.com': {
'p': 'q',
'n': 'live'
},
'aol.com': {
'p': 'q',
'n': 'aol'
},
'lycos.com': {
'p': 'q',
'n': 'lycos'
},
'ask.com': {
'p': 'q',
'n': 'ask'
},
'altavista.com': {
'p': 'q',
'n': 'altavista'
},
'search.netscape.com': {
'p': 'query',
'n': 'netscape'
},
'cnn.com': {
'p': 'query',
'n': 'cnn'
},
'about.com': {
'p': 'terms',
'n': 'about'
},
'mamma.com': {
'p': 'query',
'n': 'mama'
},
'alltheweb.com': {
'p': 'q',
'n': 'alltheweb'
},
'voila.fr': {
'p': 'rdata',
'n': 'voila'
},
'search.virgilio.it': {
'p': 'qs',
'n': 'virgilio'
},
'baidu.com': {
'p': 'wd',
'n': 'baidu'
},
'alice.com': {
'p': 'qs',
'n': 'alice'
},
'yandex.com': {
'p': 'text',
'n': 'yandex'
},
'najdi.org.mk': {
'p': 'q',
'n': 'najdi'
},
'seznam.cz': {
'p': 'q',
'n': 'seznam'
},
'search.com': {
'p': 'q',
'n': 'search'
},
'wp.pl': {
'p': 'szukaj ',
'n': 'wirtulana polska'
},
'online.onetcenter.org': {
'p': 'qt',
'n': 'o*net'
},
'szukacz.pl': {
'p': 'q',
'n': 'szukacz'
},
'yam.com': {
'p': 'k',
'n': 'yam'
},
'pchome.com': {
'p': 'q',
'n': 'pchome'
},
'kvasir.no': {
'p': 'q',
'n': 'kvasir'
},
'sesam.no': {
'p': 'q',
'n': 'sesam'
},
'ozu.es': {
'p': 'q',
'n': 'ozu '
},
'terra.com': {
'p': 'query',
'n': 'terra'
},
'mynet.com': {
'p': 'q',
'n': 'mynet'
},
'ekolay.net': {
'p': 'q',
'n': 'ekolay'
},
'google': {
'p': 'q',
'n': 'google'
},
'duckduckgo.com': {
'p': 'q',
'n': 'duckduckgo'
}
};
var a = document.createElement('a');
var values = {};
var searchEngine,
termRegex,
term;
a.href = referrer;
// Shim for the billion google search engines
if (a.hostname.indexOf('google') > -1) {
referringDomain = 'google';
}
if (searchEngines[referringDomain]) {
searchEngine = searchEngines[referringDomain];
termRegex = new RegExp(searchEngine.p + '=.*?([^&#]*|$)', 'gi');
term = a.search.match(termRegex);
values.source = searchEngine.n;
values.medium = 'organic';
values.term = (term ? term[0].split('=')[1] : '') || '(not provided)';
} else if (referringDomain !== thisDomain) {
values.source = a.hostname;
values.medium = 'referral';
}
return values;
}
function writeCookie_(name, value, expiration, path, domain) {
var str = name + '=' + value + ';';
if (expiration) str += 'Expires=' + expiration.toGMTString() + ';';
if (path) str += 'Path=' + path + ';';
if (domain) str += 'Domain=' + domain + ';';
document.cookie = str;
}
function getCookie_(name) {
var cookies = '; ' + document.cookie
var cvals = cookies.split('; ' + name + '=');
if (cvals.length > 1) return cvals.pop().split(';')[0];
}
function getDomain_(url) {
if (!url) return;
var a = document.createElement('a');
a.href = url;
try {
return a.hostname.match(/[^.]*\.[^.]{2,3}(?:\.[^.]{2,3})?$/)[0];
} catch (squelch) {
console.log('erorr is', squelch)
}
}
})(document);
</script>
Step 2: Add an All Pages Trigger
This Tag should fire every time a new page loads. Add an "All Pages" trigger to make sure this happens.
Step 3: Name and Save
Slap a fitting name and hit Save.
Step 4: Test and Verify
Switch to Preview mode and refresh your site. Navigate to Developer Tools → Applications → Cookies. Look for the lastTrafficSource cookie to ensure everything's working.
🚨 Note: If the lastTrafficSource cookie exists, GTM won't overwrite it. That's good; we're after the last touchpoint.
Step 5: Create a User-Defined Variable in GTM
Your GTM preview won't show this data by default. Navigate to GTM and create a new variable. Select "1st Party Cookie" and name it lastTrafficSource.
Step 6: How to Capture Last Traffic Source in Form Submissions
Capturing the last traffic source with form submissions can help you gain valuable insights into where your engaged users are coming from. Below are some methods for different tech stacks.
Using Vanilla JavaScript
<input type="hidden" id="lastTrafficSource" name="lastTrafficSource">
Then use JavaScript to populate it when the form loads:
document.addEventListener('DOMContentLoaded', () => {
const lastTrafficSource = localStorage.getItem('lastTrafficSource');
document.getElementById('lastTrafficSource').value = lastTrafficSource;
});
Using React
You can use React's useState and useEffect to achieve the same:
import { useState, useEffect } from 'react';
const MyForm = () => {
const [lastTrafficSource, setLastTrafficSource] = useState('');
useEffect(() => {
setLastTrafficSource(localStorage.getItem('lastTrafficSource'));
}, []);
return (
<form>
<input type="hidden" value={lastTrafficSource} name="lastTrafficSource" />
{/* rest of your form */}
</form>
);
};
Using Google Tag Manager (GTM)
- Create a new Variable in GTM for lastTrafficSource from Local Storage.
- Use this variable to populate the hidden form field through GTM tags.
With these methods, you'll not only track where your users come from but also understand how effective your marketing channels are in driving high-intent users.
What do you think? Can this section add a missing puzzle piece to the analytics game?