PageSourceSearch

https://page.agency/wp-content/mu-plugins/pa-tracking/assets/pa-tracking.js?ver=1783380760

js page.agency collected 2026-10-07 15:01:50 UTC 11,157 bytes, 322 lines download raw bytes

1/**
2 * PA Tracking — client-side attribution & journey cookie layer.
3 *
4 * Captures UTM params, referrer, landing page, visit count, and channel
5 * classification on every page load. Stores values in _pa_* cookies so
6 * server-side code (PHP) can read them and populate form hidden fields.
7 *
8 * Reusable: edit CONFIG below to adapt to any site.
9 */
10(function () {
11	'use strict';
12
13	/* ── Configuration ──────────────────────────────────────────────── */
14	var CFG = {
15		prefix: '_pa_',
16		firstTouchDays: 90,
17		lastTouchDays: 30,
18		visitsDays: 365,
19		lastPageDays: 1,
20		sessionMinutes: 30,
21		cookiePath: '/',
22		cookieSameSite: 'Lax',
23
24		/* Referrer domains classified per channel (lowercase, no www.) */
25		socialDomains: [
26			'facebook.com', 'instagram.com', 'linkedin.com',
27			'twitter.com', 'x.com', 'tiktok.com', 'pinterest.com',
28			'youtube.com', 'reddit.com', 'threads.net', 'snapchat.com',
29			'whatsapp.com', 'telegram.org', 'tumblr.com'
30		],
31		nativeDomains: [
32			'taboola.com', 'outbrain.com', 'revcontent.com',
33			'sharethrough.com', 'mgid.com', 'triplelift.com',
34			'nativo.com', 'zemanta.com', 'content.ad'
35		],
36		searchDomains: [
37			'google.com', 'google.co.uk', 'google.com.au', 'google.ca',
38			'google.co.in', 'google.de', 'google.fr', 'google.es',
39			'bing.com', 'yahoo.com', 'duckduckgo.com', 'baidu.com',
40			'yandex.com', 'ecosia.org', 'ask.com', 'startpage.com'
41		],
42
43		/* utm_medium values that map to each channel */
44		ppcMediums: ['cpc', 'ppc', 'paid', 'paid_search', 'paidsearch', 'paid-search', 'sem'],
45		socialMediums: ['social', 'social-media', 'social_media', 'socialmedia'],
46		nativeMediums: ['native', 'content', 'content-ad', 'content_ad', 'native-ad', 'native_ad', 'sponsored', 'sponsored-content'],
47		displayMediums: ['display', 'banner', 'programmatic', 'cpm', 'retargeting', 'remarketing', 'prospecting']
48	};
49
50	/* ── Cookie helpers ──────────────────────────────────────────────── */
51	function getCookie(name) {
52		var match = document.cookie.match(
53			new RegExp('(?:^|;\\s*)' + CFG.prefix + name + '=([^;]*)')
54		);
55		if (!match) return '';
56		try { return decodeURIComponent(match[1]); } catch (e) { return match[1]; }
57	}
58
59	function setCookie(name, value, days) {
60		if (value === null || value === undefined || value === '') return;
61		var expires = '';
62		if (days) {
63			var d = new Date();
64			d.setTime(d.getTime() + days * 24 * 60 * 60 * 1000);
65			expires = '; expires=' + d.toUTCString();
66		}
67		var secure = location.protocol === 'https:' ? '; Secure' : '';
68		document.cookie =
69			CFG.prefix + name + '=' + encodeURIComponent(value) +
70			expires +
71			'; path=' + CFG.cookiePath +
72			'; SameSite=' + CFG.cookieSameSite +
73			secure;
74	}
75
76	function hasCookie(name) {
77		return getCookie(name) !== '';
78	}
79
80	/* ── URL helpers ─────────────────────────────────────────────────── */
81	function getParam(key) {
82		try {
83			return new URLSearchParams(window.location.search).get(key) || '';
84		} catch (e) {
85			/* IE fallback */
86			var match = window.location.search.match(
87				new RegExp('[?&]' + key + '=([^&]*)')
88			);
89			return match ? decodeURIComponent(match[1].replace(/\+/g, ' ')) : '';
90		}
91	}
92
93	function getUTMs() {
94		return {
95			source: getParam('utm_source'),
96			medium: getParam('utm_medium'),
97			campaign: getParam('utm_campaign'),
98			content: getParam('utm_content'),
99			term: getParam('utm_term')
100		};
101	}
102
103	function getClickIDs() {
104		return {
105			gclid: getParam('gclid'),
106			fbclid: getParam('fbclid'),
107			msclkid: getParam('msclkid'),
108			ttclid: getParam('ttclid'),
109			li_fat: getParam('li_fat_id')
110		};
111	}
112
113	/* ── Referrer helpers ────────────────────────────────────────────── */
114	function getReferrerDomain() {
115		try {
116			var ref = document.referrer;
117			if (!ref) return '';
118			var host = new URL(ref).hostname.replace(/^www\./, '').toLowerCase();
119			/* Ignore internal referrers */
120			var own = window.location.hostname.replace(/^www\./, '').toLowerCase();
121			return host === own ? '' : host;
122		} catch (e) {
123			return '';
124		}
125	}
126
127	function domainInList(domain, list) {
128		if (!domain) return false;
129		for (var i = 0; i < list.length; i++) {
130			if (domain === list[i] || domain.indexOf('.' + list[i]) !== -1) {
131				return true;
132			}
133		}
134		return false;
135	}
136
137	function mediumInList(medium, list) {
138		if (!medium) return false;
139		var m = medium.toLowerCase();
140		for (var i = 0; i < list.length; i++) {
141			if (m === list[i]) return true;
142		}
143		return false;
144	}
145
146	/* ── Channel detection ───────────────────────────────────────────── */
147	/**
148	 * Returns one of: ppc | social | native | programmatic | organic | referral | direct
149	 *
150	 * Priority order ensures a paid-social click (fbclid + social referrer)
151	 * lands under 'social' (paid variant), not programmatic.
152	 */
153	function detectChannel(utms, clickIDs, referrerDomain) {
154		var medium = (utms.medium || '').toLowerCase();
155
156		/* 1. PPC */
157		if (
158			mediumInList(medium, CFG.ppcMediums) ||
159			clickIDs.gclid ||
160			clickIDs.msclkid
161		) {
162			return 'ppc';
163		}
164
165		/* 2. Social (organic or paid social — both route here) */
166		if (
167			mediumInList(medium, CFG.socialMediums) ||
168			clickIDs.ttclid ||
169			clickIDs.li_fat ||
170			domainInList(referrerDomain, CFG.socialDomains)
171		) {
172			return 'social';
173		}
174
175		/* Meta Ads (fbclid) can be social or display — default to social */
176		if (clickIDs.fbclid) {
177			return 'social';
178		}
179
180		/* 3. Native */
181		if (
182			mediumInList(medium, CFG.nativeMediums) ||
183			domainInList(referrerDomain, CFG.nativeDomains)
184		) {
185			return 'native';
186		}
187
188		/* 4. Programmatic / Display */
189		if (mediumInList(medium, CFG.displayMediums)) {
190			return 'programmatic';
191		}
192
193		/* 5. Organic search */
194		if (
195			!utms.source && !utms.medium &&
196			domainInList(referrerDomain, CFG.searchDomains)
197		) {
198			return 'organic';
199		}
200
201		/* 6. Referral (any other external referrer) */
202		if (referrerDomain) {
203			return 'referral';
204		}
205
206		/* 7. Direct */
207		return 'direct';
208	}
209
210	/* ── Session detection ───────────────────────────────────────────── */
211	function isNewSession() {
212		return !hasCookie('session');
213	}
214
215	function refreshSession() {
216		/* Session cookie stored without the prefix so it's a plain short-lived cookie */
217		var days = CFG.sessionMinutes / (60 * 24);
218		setCookie('session', '1', days);
219	}
220
221	/* ── Main ────────────────────────────────────────────────────────── */
222	function init() {
223		var utms = getUTMs();
224		var clickIDs = getClickIDs();
225		var referrer = getReferrerDomain();
226		var channel = detectChannel(utms, clickIDs, referrer);
227		var currentURL = window.location.href;
228		var newSession = isNewSession();
229
230		/* Always refresh the session heartbeat */
231		refreshSession();
232
233		/* ── First-touch (written once, never overwritten) ─────────── */
234		if (!hasCookie('ft_landing_page')) {
235			setCookie('ft_landing_page', currentURL, CFG.firstTouchDays);
236		}
237		if (!hasCookie('ft_first_seen')) {
238			setCookie('ft_first_seen', new Date().toISOString().split('T')[0], CFG.firstTouchDays);
239		}
240		if (!hasCookie('ft_referrer') && referrer) {
241			setCookie('ft_referrer', referrer, CFG.firstTouchDays);
242		}
243		if (!hasCookie('ft_channel')) {
244			setCookie('ft_channel', channel, CFG.firstTouchDays);
245		}
246		if (!hasCookie('ft_utm_source') && utms.source) {
247			setCookie('ft_utm_source', utms.source, CFG.firstTouchDays);
248		}
249		if (!hasCookie('ft_utm_medium') && utms.medium) {
250			setCookie('ft_utm_medium', utms.medium, CFG.firstTouchDays);
251		}
252		if (!hasCookie('ft_utm_campaign') && utms.campaign) {
253			setCookie('ft_utm_campaign', utms.campaign, CFG.firstTouchDays);
254		}
255		if (!hasCookie('ft_utm_content') && utms.content) {
256			setCookie('ft_utm_content', utms.content, CFG.firstTouchDays);
257		}
258		if (!hasCookie('ft_utm_term') && utms.term) {
259			setCookie('ft_utm_term', utms.term, CFG.firstTouchDays);
260		}
261		if (!hasCookie('ft_gclid') && clickIDs.gclid) {
262			setCookie('ft_gclid', clickIDs.gclid, CFG.firstTouchDays);
263		}
264		if (!hasCookie('ft_fbclid') && clickIDs.fbclid) {
265			setCookie('ft_fbclid', clickIDs.fbclid, CFG.firstTouchDays);
266		}
267
268		/* ── Per-session updates (only on new session start) ────────── */
269		if (newSession) {
270			/* Last-touch referrer and channel update every new session */
271			if (referrer) {
272				setCookie('lt_referrer', referrer, CFG.lastTouchDays);
273			}
274			setCookie('lt_channel', channel, CFG.lastTouchDays);
275
276			/* Last-touch UTMs update only when this session has UTM params */
277			if (utms.source) setCookie('lt_utm_source', utms.source, CFG.lastTouchDays);
278			if (utms.medium) setCookie('lt_utm_medium', utms.medium, CFG.lastTouchDays);
279			if (utms.campaign) setCookie('lt_utm_campaign', utms.campaign, CFG.lastTouchDays);
280
281			/* Increment visit count */
282			var visits = parseInt(getCookie('visits') || '0', 10);
283			setCookie('visits', String(visits + 1), CFG.visitsDays);
284		}
285
286		/* ── Last page (updated every page load) ─────────────────────
287		 * PHP reads this cookie server-side when the form renders, before
288		 * this JS runs on the form page — so the value is still the
289		 * previous page at the moment of capture. See tracking-fields.md.
290		 */
291		setCookie('last_page', currentURL, CFG.lastPageDays);
292
293		/* ── Mautic session mirror ────────────────────────────────────
294		 * Copy Mautic's own tracking cookies into _pa_* cookies so PHP
295		 * can read them server-side.
296		 *
297		 * mtc_id IS the Mautic contact ID for this visitor. When present,
298		 * the Mautic API submission will PATCH that specific contact
299		 * (merging all tracked page history) rather than creating a new one.
300		 *
301		 * Refreshed on every page load so the value stays current if
302		 * Mautic identifies the visitor mid-session.
303		 */
304		var mauticMirror = ['mtc_id', 'mautic_device_id'];
305		for (var i = 0; i < mauticMirror.length; i++) {
306			var mName = mauticMirror[i];
307			var mMatch = document.cookie.match(
308				new RegExp('(?:^|;\\s*)' + mName + '=([^;]*)')
309			);
310			if (mMatch) {
311				try {
312					setCookie(mName, decodeURIComponent(mMatch[1]), 1);
313				} catch (e) {
314					setCookie(mName, mMatch[1], 1);
315				}
316			}
317		}
318	}
319
320	init();
321
322})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.