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

  1. Data Sparsity: Accurate attribution is hard if your data is scattered like a jigsaw puzzle.

  2. Platform Limitations: Relying on a single platform like Google Analytics for attribution is like cooking with one hand tied behind your back.

  3. 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)

  1. Create a new Variable in GTM for lastTrafficSource from Local Storage.
  2. 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?

More articles

Analytical Tools: Our Experience with Hotjar, Microsoft Clarity, and PostHog

A deep dive into three popular analytical tools and how to choose the right one for your needs

Read more

Data Engineering Primer - Part 1

Vain Attempt to Make Data Engineering Sexy After Reading One-Third of a Data Engineering Book

Read more

Tell us about your project

  • No strings attached. Let's discuss your project and see if we are a good fit for each other.
  • Gain clarity. I will help you understand what you need and how to achieve it, step by step.
  • Stop feeling uncertain. Get a clear, actionable plan to achieve your goals.