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.