1/* tts-web-context.js â first-party web attribution capture (Phase 2). 2 * 3 * Runs on any *.techtoschool.com page (storefront theme, forms.techtoschool.com 4 * form pages, mkl-hosted embeds). Collects, without any external calls: 5 * client_id â GA4 client id parsed from the first-party `_ga` cookie 6 * (domain .techtoschool.com, so it crosses storefront â forms). 7 * first â FIRST-touch snapshot, stamped once into the `_tts_ft` cookie 8 * (.techtoschool.com, ~400 days): source/medium/campaign/gclid/ 9 * referrer/landing/ts of the visitor's first-ever arrival. 10 * last â the current (converting) context: utm params + gclid from this 11 * page's URL (or, inside an iframe, the parent page's URL via 12 * document.referrer), external referrer, landing page. 13 * 14 * Usage: window.ttsWebContext() â object|null. Include the file and pass the 15 * result as `web_context` with any form/quote/checkout POST. Self-executing: 16 * stamps the first-touch cookie on load so mere browsing (no conversion) 17 * still anchors first-touch for a later visit. No PII is read or stored. 18 * Server side sanitizes via core/lib/attribution/live.js captureWebContext â 19 * this file is capture, not truth. 20 */ 21(function () { 22 if (window.ttsWebContext) return; 23 24 var COOKIE_DOMAIN = '.techtoschool.com'; 25 var FT_COOKIE = '_tts_ft'; 26 var FT_MAX_AGE = 60 * 60 * 24 * 400; // ~400 days 27 28 function getCookie(name) { 29 try { 30 var m = document.cookie.match(new RegExp('(?:^|; )' + name + '=([^;]*)')); 31 return m ? decodeURIComponent(m[1]) : null; 32 } catch (_) { return null; } 33 } 34 35 function setCookie(name, value) { 36 try { 37 var v = encodeURIComponent(value); 38 var base = name + '=' + v + '; path=/; max-age=' + FT_MAX_AGE + '; SameSite=Lax'; 39 // Only stamp the shared domain when we're actually on it (localhost/dev-tunnel safe). 40 if (location.hostname.indexOf('techtoschool.com') !== -1) base += '; domain=' + COOKIE_DOMAIN; 41 if (location.protocol === 'https:') base += '; Secure'; 42 document.cookie = base; 43 } catch (_) { /* cookie-blocked browsers: capture still works per-page */ } 44 } 45 46 // GA4 client id from the _ga cookie: "GA1.1.123456789.1712345678" â "123456789.1712345678" 47 function gaClientId() { 48 var raw = getCookie('_ga'); 49 if (!raw) return null; 50 var parts = raw.split('.'); 51 if (parts.length < 4) return null; 52 return parts[parts.length - 2] + '.' + parts[parts.length - 1]; 53 } 54 55 function parseParams(url) { 56 var out = {}; 57 try { 58 var q = new URL(url).searchParams; 59 ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'gclid'].forEach(function (k) { 60 var v = q.get(k); 61 if (v) out[k.replace('utm_', '')] = v; 62 }); 63 if (out.gclid == null && q.get('gclid')) out.gclid = q.get('gclid'); 64 } catch (_) {} 65 return out; 66 } 67 68 function isInternal(url) { 69 try { return new URL(url).hostname.indexOf('techtoschool.com') !== -1; } catch (_) { return false; } 70 } 71 72 // The "current arrival" context, seen from wherever this script runs. 73 // Inside a TTS iframe, document.referrer is the EMBEDDING storefront page 74 // (with its query string) â that page's utm params + URL are the real context. 75 function currentVisit() { 76 var inIframe = false; 77 try { inIframe = window.parent && window.parent !== window; } catch (_) { inIframe = true; } 78 var parentUrl = document.referrer || ''; 79 var useParent = inIframe && parentUrl && isInternal(parentUrl); 80 81 var params = parseParams(location.href); 82 var landing = location.href.split('#')[0]; 83 var referrer = document.referrer || ''; 84 if (useParent) { 85 var pp = parseParams(parentUrl); 86 // Parent params win when present (the iframe URL rarely carries utm). 87 Object.keys(pp).forEach(function (k) { if (!params[k]) params[k] = pp[k]; }); 88 landing = parentUrl.split('#')[0]; 89 referrer = ''; // parent's own referrer is unknowable from here 90 } else if (isInternal(referrer)) { 91 referrer = ''; // internal navigation is not a source 92 } 93 94 var v = {}; 95 if (params.source) v.source = params.source; 96 if (params.medium) v.medium = params.medium; 97 if (params.campaign) v.campaign = params.campaign; 98 if (params.term) v.term = params.term; 99 if (params.content) v.content = params.content; 100 if (params.gclid) v.gclid = params.gclid; 101 if (referrer) v.referrer = referrer.slice(0, 500); 102 if (landing) v.landing = landing.slice(0, 500); 103 v.ts = Date.now(); 104 return v; 105 } 106 107 function readFirstTouch() { 108 var raw = getCookie(FT_COOKIE); 109 if (!raw) return null; 110 try { 111 var o = JSON.parse(raw); 112 return (o && typeof o === 'object') ? o : null; 113 } catch (_) { return null; } 114 } 115 116 // Stamp first-touch ONCE. A meaningful first touch = has a source signal 117 // (utm/gclid/external referrer); a bare direct visit still stamps landing+ts 118 // so "first seen" is anchored, but a later visit that DOES carry a source may 119 // upgrade a source-less stamp within 24h (the ad-click-after-direct case). 120 function stampFirstTouch(visit) { 121 var existing = readFirstTouch(); 122 var hasSource = !!(visit.source || visit.gclid || visit.referrer); 123 if (existing) { 124 var existingHasSource = !!(existing.source || existing.gclid || existing.referrer); 125 var fresh = existing.ts && (Date.now() - existing.ts) < 86400000; 126 if (!existingHasSource && hasSource && fresh) {
127 setCookie(FT_COOKIE, JSON.stringify(visit)); 128 return visit; 129 } 130 return existing; 131 } 132 setCookie(FT_COOKIE, JSON.stringify(visit)); 133 return visit; 134 } 135 136 var _visit = currentVisit(); 137 var _first = stampFirstTouch(_visit); 138 139 window.ttsWebContext = function () { 140 try { 141 var out = {}; 142 var cid = gaClientId(); 143 if (cid) out.client_id = cid; 144 var first = readFirstTouch() || _first; 145 if (first) out.first = first; 146 out.last = _visit; 147 return out; 148 } catch (_) { return null; } 149 }; 150})();
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.