1/** 2 * HomeRunner Toolkit â AI Booking Channel landing capture. 3 * 4 * Runs on every front-end page (including page-cached ones, which is why capture 5 * is client-side: FlyWP nginx caches ignoring Set-Cookie, so a server set on a 6 * cached landing is unreliable). Vanilla, no dependencies. 7 * 8 * Three first-touch paths, all writing a 30-day cookie only when none exists: 9 * 1. hr_src=<accepted tag> in the URL -> cookie "<tag>|<epoch-ms>", then 10 * clean the URL. An unknown tag value is ignored as a tag and path 2 runs. 11 * 2. no cookie + an AI-referrer document.referrer (exact host or dot-suffix) 12 * -> cookie "ref:<base-domain>|<epoch-ms>" and a one-shot sendBeacon. 13 * 3. window.hrtkAicCapture(<script source>) -> cookie "<source>|<epoch-ms>". 14 * Defined at script evaluation, so a first-party script can call it 15 * before DOMContentLoaded. 16 * 17 * On checkout, it copies the cookie into the hidden hrtk_aic_src field and moves 18 * it into the serialized checkout form so the server can read it same-request. 19 */ 20(function () { 21 'use strict'; 22 23 var cfg = window.hrtkAic || {}; 24 var COOKIE = cfg.cookie || 'hrtk_aic_src'; 25 var PARAM = cfg.param || 'hr_src'; 26 var VALUE = cfg.value || 'aic'; 27 // A page cached before `values` existed still localizes only `value`; the 28 // server accepts every tag below, so the script does too. 29 var TAGS = Array.isArray(cfg.values) && cfg.values.length ? cfg.values : [VALUE, 'mcp', 'nmcp']; 30 var SCRIPT_SOURCES = Array.isArray(cfg.scriptSources) ? cfg.scriptSources : ['wmcp']; 31 var WINDOW_DAYS = parseInt(cfg.windowDays, 10) || 30; 32 var REFERRERS = Array.isArray(cfg.referrers) ? cfg.referrers : []; 33 34 function readCookie(name) { 35 var parts = document.cookie ? document.cookie.split('; ') : []; 36 for (var i = 0; i < parts.length; i++) { 37 var eq = parts[i].indexOf('='); 38 if (eq > -1 && parts[i].slice(0, eq) === name) { 39 return decodeURIComponent(parts[i].slice(eq + 1)); 40 } 41 } 42 return ''; 43 } 44 45 function writeCookie(name, value) { 46 var maxAge = WINDOW_DAYS * 24 * 60 * 60; 47 var secure = 'https:' === location.protocol ? '; Secure' : ''; 48 // Not HttpOnly by necessity (the client writes it) and by threat model 49 // (no secret; server validates its fixed grammar). 50 document.cookie = name + '=' + encodeURIComponent(value) + 51 '; Max-Age=' + maxAge + '; Path=/; SameSite=Lax' + secure; 52 } 53 54 /** Canonical base domain for a referrer host, or '' (exact / dot-suffix only). */ 55 function matchReferrer(host) { 56 host = String(host || '').toLowerCase(); 57 if (!host) { 58 return ''; 59 } 60 for (var i = 0; i < REFERRERS.length; i++) { 61 var d = REFERRERS[i]; 62 if (host === d || host.slice(-(d.length + 1)) === '.' + d) { 63 return d; 64 } 65 } 66 return ''; 67 } 68 69 function referrerHost() { 70 if (!document.referrer) { 71 return ''; 72 } 73 try { 74 return new URL(document.referrer).hostname; 75 } catch (e) { 76 return ''; 77 } 78 } 79 80 /** Strip hr_src from the URL without a history entry (replaceState, not push). */ 81 function cleanUrl() { 82 try { 83 var url = new URL(window.location.href); 84 if (url.searchParams.has(PARAM)) { 85 url.searchParams.delete(PARAM); 86 var qs = url.searchParams.toString(); 87 window.history.replaceState(null, '', url.pathname + (qs ? '?' + qs : '') + url.hash); 88 } 89 } catch (e) { /* no-op */ } 90 } 91 92 function capture() { 93 var existing = readCookie(COOKIE); 94 var params; 95 try { 96 params = new URL(window.location.href).searchParams; 97 } catch (e) { 98 params = null; 99 } 100 var tag = params ? params.get(PARAM) : null; 101 var tagged = null !== tag && TAGS.indexOf(tag) > -1; 102 103 // Path 1: tagged click. First-touch â only when no cookie yet. 104 if (tagged) { 105 if (!existing) { 106 writeCookie(COOKIE, tag + '|' + Date.now()); 107 } 108 cleanUrl(); 109 return; 110 } 111 112 // Path 2: AI referrer, only when there is no cookie at all. 113 if (existing) { 114 return; 115 } 116 var base = matchReferrer(referrerHost()); 117 if (!base) { 118 return; 119 } 120 writeCookie(COOKIE, 'ref:' + base + '|' + Date.now()); 121 122 // One-shot beacon so the visit is counted even though nothing was tagged. 123 if (cfg.ajaxUrl && navigator.sendBeacon) { 124 try { 125 var fd = new FormData(); 126 fd.append('action', 'hrtk_aic_beacon'); 127 fd.append('domain', base); 128 navigator.sendBeacon(cfg.ajaxUrl, fd); 129 } catch (e) { /* no-op */ } 130 } 131 } 132
133 /** 134 * Path 3: first-touch write for a first-party script. Does nothing when a 135 * source is already set, or when the source is not one a script may write 136 * (URL tags and referrers have their own paths above). 137 * 138 * @param {string} source A script source, e.g. "wmcp". 139 * @return {boolean} True when the cookie was written. 140 */ 141 function captureSource(source) { 142 if (SCRIPT_SOURCES.indexOf(source) === -1) { 143 return false; 144 } 145 if (readCookie(COOKIE)) { 146 return false; 147 } 148 writeCookie(COOKIE, source + '|' + Date.now()); 149 return true; 150 } 151 152 // Exposed at script evaluation, not at DOMContentLoaded. 153 window.hrtkAicCapture = captureSource; 154 155 /** 156 * Copy the cookie into the hidden checkout field and relocate it into the 157 * serialized checkout form so the pre-create filter can read it. Mirrors the 158 * checkout-survey relocation (retry on ajaxComplete-style re-renders). 159 */ 160 function placeCheckoutField() { 161 var source = document.getElementById('hrtk-aic-src-source'); 162 if (!source) { 163 return false; 164 } 165 var input = source.querySelector('input[name="' + COOKIE + '"]'); 166 if (!input) { 167 return false; 168 } 169 var value = readCookie(COOKIE); 170 if (!value) { 171 return true; // Nothing to attribute; leave the empty field inert. 172 } 173 input.value = value; 174 175 var form = document.getElementById('homelocal-checkout-form'); 176 if (form && input.parentNode !== form) { 177 form.appendChild(input); 178 } 179 return !!form; 180 } 181 182 function watchCheckout() { 183 if (!document.getElementById('hrtk-aic-src-source')) { 184 return; 185 } 186 if (placeCheckoutField()) { 187 return; 188 } 189 // The checkout form can render on a later pass; retry briefly, then stop. 190 var tries = 0; 191 var timer = setInterval(function () { 192 tries++; 193 if (placeCheckoutField() || tries > 40) { 194 clearInterval(timer); 195 } 196 }, 500); 197 } 198 199 function run() { 200 capture(); 201 watchCheckout(); 202 } 203 204 if ('loading' === document.readyState) { 205 document.addEventListener('DOMContentLoaded', run); 206 } else { 207 run(); 208 } 209})();
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.