PageSourceSearch

https://mountaincabinsutah.com/wp-content/plugins/homerunner-conve…/assets/js/hrck-tracker.js?ver=1.159.17

js mountaincabinsutah.com collected 2026-10-10 02:01:25 UTC 11,268 bytes, 319 lines download raw bytes

1/**
2 * HRCK Visitor Tracker
3 *
4 * Lightweight (~3KB) vanilla JS that fires on every frontend page load.
5 * Persists a 1-year track_id cookie, captures UTM/gclid/fbclid params,
6 * detects page context, and sends a visit ping to the REST endpoint.
7 *
8 * No jQuery dependency. No external requests.
9 */
10(function () {
11	'use strict';
12
13	var COOKIE_NAME  = 'hrck_track_id';
14	var SESSION_KEY  = 'hrck_track_session'; // sessionStorage new-session flag
15	var API_URL      = (window.hrckTracker && window.hrckTracker.apiUrl)
16		? window.hrckTracker.apiUrl
17		: '/wp-json/hrck/v1/track/visit';
18	// URL cleanup is opt-in (#407): stripping utm_*/gclid/fbclid via
19	// replaceState races GA4/Ads/Meta tags that read location.href after
20	// DOMContentLoaded, killing their campaign attribution fleet-wide.
21	var CLEAN_URL    = !!(window.hrckTracker && window.hrckTracker.cleanUrl);
22
23	// -------------------------------------------------------------------------
24	// Cookie helpers
25	// -------------------------------------------------------------------------
26
27	function getCookie(name) {
28		var match = document.cookie.match(new RegExp('(?:^|;\\s*)' + name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '=([^;]*)'));
29		return match ? decodeURIComponent(match[1]) : null;
30	}
31
32	function setCookie(name, value, days) {
33		var expires = new Date(Date.now() + days * 864e5).toUTCString();
34		// SameSite=Lax keeps cookies on same-site navigations while blocking
35		// cross-site requests (no Secure requirement for non-HTTPS dev sites).
36		document.cookie = name + '=' + encodeURIComponent(value)
37			+ '; expires=' + expires
38			+ '; path=/'
39			+ '; SameSite=Lax';
40	}
41
42	// -------------------------------------------------------------------------
43	// UUID generation
44	// -------------------------------------------------------------------------
45
46	function generateUUID() {
47		if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
48			return crypto.randomUUID();
49		}
50		// Fallback for Safari < 15.4 and older browsers
51		return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
52			var r = Math.random() * 16 | 0;
53			var v = c === 'x' ? r : (r & 0x3 | 0x8);
54			return v.toString(16);
55		});
56	}
57
58	// -------------------------------------------------------------------------
59	// Track ID management
60	// -------------------------------------------------------------------------
61
62	function getTrackId() {
63		var existing = getCookie(COOKIE_NAME);
64		// Validate UUID v4 format before trusting stored value
65		if (existing && /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(existing)) {
66			return existing;
67		}
68		var id = generateUUID();
69		setCookie(COOKIE_NAME, id, 365);
70		return id;
71	}
72
73	// -------------------------------------------------------------------------
74	// Session detection
75	// -------------------------------------------------------------------------
76
77	function isNewSession() {
78		try {
79			if (sessionStorage.getItem(SESSION_KEY)) {
80				return false;
81			}
82			sessionStorage.setItem(SESSION_KEY, '1');
83			return true;
84		} catch (e) {
85			// sessionStorage blocked (private mode edge cases)
86			return false;
87		}
88	}
89
90	// -------------------------------------------------------------------------
91	// UTM / click-ID capture
92	// -------------------------------------------------------------------------
93
94	// promo_id rides Promo Bar deep links (DeepLinkBuilder stamps it on every CTA).
95	// Adding it here threads it through getUtmParams/hasTrackedParams/cleanUrl and the
96	// payload-merge loop, so a promo click is captured into the visit row (M7). The key
97	// must stay 'promo_id' — the exact get_param() key TrackingController::track_visit reads.
98	// hrck_plan rides Trip Planner matched-property links (a signed plan-click
99	// credential, ckit#422 §18d). TrackingController verifies it and stamps
100	// trip_plan_id on the visit; the address bar is cleaned like the utm keys.
101	var TRACKED_PARAMS = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'gclid', 'fbclid', 'promo_id', 'tb_src', 'hrck_plan'];
102
103	function getUtmParams() {
104		var params = new URLSearchParams(window.location.search);
105		var utm = {};
106		TRACKED_PARAMS.forEach(function (key) {
107			var val = params.get(key);
108			if (val) {
109				utm[key] = val;
110			}
111		});
112		return utm;
113	}
114
115	function hasTrackedParams() {
116		var search = window.location.search;
117		for (var i = 0; i < TRACKED_PARAMS.length; i++) {
118			if (search.indexOf(TRACKED_PARAMS[i]) > -1) {
119				return true;
120			}
121		}
122		return false;
123	}
124
125	function cleanUrl(utmKeys) {
126		if (!CLEAN_URL || !utmKeys.length || !window.history || !window.history.replaceState) {
127			return;
128		}
129		var url = new URL(window.location.href);
130		var removed = false;
131		utmKeys.forEach(function (key) {
132			if (url.searchParams.has(key)) {
133				url.searchParams.delete(key);
134				removed = true;
135			}
136		});
137		if (removed) {
138			window.history.replaceState({}, '', url.toString());
139		}
140	}
141
142	// -------------------------------------------------------------------------
143	// Page context detection
144	// -------------------------------------------------------------------------
145
146	function bodyHasClass(cls) {
147		return !!(document.body && document.body.classList && document.body.classList.contains(cls));
148	}
149
150	// Detection markers verified against real fleet markup (ckit#143 — the old
151	// property/search selectors matched nothing, so ~99% of visits logged as
152	// 'other'):
153	//  - property: WP core emits body.single-property for the 'property' CPT
154	//    on every site; .homelocal-single-property kept as a fallback.
155	//  - search:   .hfse-properties is the Explorer results grid — the same
156	//    marker cart-abandonment.js isBrowseContext() relies on.
157	//  - landing:  WP core body.home (the 'landing' value is already in the
158	//    server's ALLOWED_PAGE_TYPES — the detector just never sent it).
159	//    Checked BEFORE search so a homepage embedding a properties grid
160	//    classifies as 'landing', not 'search'.
161	function getPageType() {
162		if (bodyHasClass('single-property') || document.querySelector('.homelocal-single-property')) {
163			return 'property';
164		}
165		if (document.querySelector('.hrck-wishlist-page')) {
166			return 'wishlist';
167		}
168		if (document.querySelector('.homelocal-checkout, .homerunner-checkout')) {
169			return 'checkout';
170		}
171		if (bodyHasClass('home')) {
172			return 'landing';
173		}
174		if (document.querySelector('.hfse-properties, .homelocal-search, .homelocal-explorer')) {
175			return 'search';
176		}
177		return 'other';
178	}
179
180	function getPropertyId() {
181		var el = document.querySelector('[data-property-id]');
182		if (el) {
183			var id = parseInt(el.getAttribute('data-property-id'), 10);
184			return id > 0 ? id : null;
185		}
186		var match = document.body.className.match(/\bproperty-(\d+)\b/);
187		return match ? parseInt(match[1], 10) : null;
188	}
189
190	function getDeviceType() {
191		var w = window.innerWidth;
192		if (w < 768) { return 'mobile'; }
193		if (w < 960) { return 'tablet'; }
194		return 'desktop';
195	}
196
197	// -------------------------------------------------------------------------
198	// Session cookie helpers
199	// -------------------------------------------------------------------------
200
201	function getSessionId() {
202		return getCookie('hrck_session_id') || getCookie('hrck_session') || null;
203	}
204
205	// -------------------------------------------------------------------------
206	// Trip Planner click credential (ckit#422 §18d)
207	// -------------------------------------------------------------------------
208
209	// The credential is (1) kept in a first-party cookie so booking attribution
210	// can resolve the plan even when this beacon is lost or the guest has no
211	// visit rows yet, and (2) stripped from the address bar UNCONDITIONALLY —
212	// independent of the opt-in CLEAN_URL — so it never lingers in history,
213	// referrers, or shared URLs. Set client-side on purpose: FlyWP replays
214	// cached Set-Cookie headers, so a server-set cookie could hand one guest's
215	// credential to the next visitor of a cached property page.
216	var PLAN_COOKIE_MAX_AGE = 30 * 24 * 60 * 60;
217
218	function captureTripPlanClick(credential) {
219		try {
220			document.cookie = 'hrck_plan=' + encodeURIComponent(credential)
221				+ '; Max-Age=' + PLAN_COOKIE_MAX_AGE + '; Path=/; SameSite=Lax'
222				+ (window.location.protocol === 'https:' ? '; Secure' : '');
223		} catch (e) { /* cookie blocked — the visit row is the fallback */ }
224		if (!window.history || !window.history.replaceState || typeof URL !== 'function') {
225			return;
226		}
227		try {
228			var url = new URL(window.location.href);
229			if (url.searchParams.has('hrck_plan')) {
230				url.searchParams.delete('hrck_plan');
231				window.history.replaceState({}, '', url.toString());
232			}
233		} catch (e) { /* malformed URL — nothing to strip */ }
234	}
235
236	// -------------------------------------------------------------------------
237	// Main tracking function
238	// -------------------------------------------------------------------------
239
240	function track() {
241		var trackId    = getTrackId();
242		var newSession = isNewSession();
243		var utmParams  = {};
244
245		// Capture UTM/click-ID params when they are present in the URL
246		// or when this is a brand-new session (first pageview — include referrer).
247		if (newSession || hasTrackedParams()) {
248			utmParams = getUtmParams();
249			if (utmParams.hrck_plan) {
250				captureTripPlanClick(utmParams.hrck_plan);
251			}
252			if (Object.keys(utmParams).length > 0) {
253				cleanUrl(Object.keys(utmParams));
254			}
255		}
256
257		var data = {
258			track_id:    trackId,
259			page_url:    window.location.pathname + window.location.search,
260			page_type:   getPageType(),
261			device_type: getDeviceType()
262		};
263
264		var propertyId = getPropertyId();
265		if (propertyId) {
266			data.property_id = propertyId;
267		}
268
269		// Only include referrer on new sessions to avoid polluting every row
270		if (newSession && document.referrer) {
271			data.referrer = document.referrer;
272		}
273
274		// Merge UTM / click-ID params into payload
275		Object.keys(utmParams).forEach(function (key) {
276			data[key] = utmParams[key];
277		});
278
279		// Attach session_id if known
280		var sessionId = getSessionId();
281		if (sessionId) {
282			data.session_id = sessionId;
283		}
284
285		// -----------------------------------------------------------------------
286		// Send — prefer sendBeacon for reliability on page unload, fall back to
287		// fetch with keepalive so it survives navigations.
288		// -----------------------------------------------------------------------
289		var payload = JSON.stringify(data);
290
291		if (typeof navigator.sendBeacon === 'function') {
292			navigator.sendBeacon(API_URL, new Blob([payload], {type: 'application/json'}));
293		} else if (typeof fetch === 'function') {
294			fetch(API_URL, {
295				method:    'POST',
296				headers:   {'Content-Type': 'application/json'},
297				body:      payload,
298				keepalive: true
299			}).catch(function () {
300				// Intentionally silent — tracking must never break the page.
301			});
302		}
303	}
304
305	// -------------------------------------------------------------------------
306	// Boot — skip wp-admin; wait for DOM if not yet ready
307	// -------------------------------------------------------------------------
308
309	if (document.body && document.body.classList.contains('wp-admin')) {
310		return;
311	}
312
313	if (document.readyState === 'loading') {
314		document.addEventListener('DOMContentLoaded', track);
315	} else {
316		track();
317	}
318
319})();

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.