PageSourceSearch

https://mountaincabinsutah.com/wp-content/plugins/homerunner-toolkit/assets/js/aic-attribution.js?ver=3.309.18

js mountaincabinsutah.com collected 2026-10-10 02:01:28 UTC 6,323 bytes, 209 lines download raw bytes

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.