PageSourceSearch

https://eastpassexcursions.com/static/js/track.js

js eastpassexcursions.com collected 2026-10-04 03:18:20 UTC 4,899 bytes, 131 lines download raw bytes

1/* East Pass Excursions — first-party analytics beacon.
2 *
3 * Cookie-free, PII-free, and fully self-contained. On load it records a
4 * pageview (with referrer + UTM tags) and auto-wires clicks on booking/CTA,
5 * phone, email, and tour-card links. Anything else can be tracked manually via
6 * window.track(type, opts).
7 *
8 * Transport: navigator.sendBeacon to POST /api/track (falls back to fetch
9 * keepalive). The server responds 204 immediately, so this never blocks.
10 *
11 * Session id: a short random id kept only in memory for this page-session. It
12 * is NOT stored in a cookie or localStorage (those are blocked in some embeds
13 * and we want to stay cookie-free). It groups events within a single tab load.
14 */
15(function () {
16  'use strict';
17  if (window.__epxTrack) return; // guard against double-load
18
19  var ENDPOINT = '/api/track';
20
21  // Cookie-free, in-memory session id (per tab load).
22  function rid() {
23    try {
24      if (window.crypto && window.crypto.getRandomValues) {
25        var a = new Uint8Array(9);
26        window.crypto.getRandomValues(a);
27        return Array.prototype.map.call(a, function (b) { return ('0' + b.toString(16)).slice(-2); }).join('');
28      }
29    } catch (e) {}
30    return (Date.now().toString(36) + Math.random().toString(36).slice(2, 10));
31  }
32  var SID = rid();
33
34  function qp(name) {
35    try {
36      return new URLSearchParams(window.location.search).get(name) || undefined;
37    } catch (e) { return undefined; }
38  }
39
40  function base(extra) {
41    var o = {
42      sid: SID,
43      path: window.location.pathname + (window.location.search || ''),
44      ref: document.referrer || undefined,
45      utm_source: qp('utm_source'),
46      utm_medium: qp('utm_medium'),
47      utm_campaign: qp('utm_campaign'),
48    };
49    if (extra) for (var k in extra) if (extra.hasOwnProperty(k)) o[k] = extra[k];
50    return o;
51  }
52
53  function send(payload) {
54    try {
55      var body = JSON.stringify(payload);
56      if (navigator.sendBeacon) {
57        var blob = new Blob([body], { type: 'application/json' });
58        if (navigator.sendBeacon(ENDPOINT, blob)) return;
59      }
60      // Fallback
61      fetch(ENDPOINT, {
62        method: 'POST',
63        headers: { 'Content-Type': 'application/json' },
64        body: body,
65        keepalive: true,
66        credentials: 'same-origin',
67      }).catch(function () {});
68    } catch (e) { /* swallow */ }
69  }
70
71  // Public API: track('cta_click', { label: 'Book now', boat: 'pontoon' })
72  function track(type, opts) {
73    if (!type) return;
74    send(base(Object.assign({ type: type }, opts || {})));
75  }
76  window.track = track;
77  window.__epxTrack = { sid: SID, track: track };
78
79  // ---- auto pageview ----
80  track('pageview');
81
82  // ---- auto-wire common clicks (delegated, one listener) ----
83  function labelFor(el) {
84    var l = el.getAttribute('data-track-label');
85    if (l) return l;
86    var txt = (el.textContent || '').trim().replace(/\s+/g, ' ');
87    if (txt) return txt.slice(0, 80);
88    var aria = el.getAttribute('aria-label');
89    return aria ? aria.slice(0, 80) : '(unlabeled)';
90  }
91
92  document.addEventListener('click', function (e) {
93    var el = e.target && e.target.closest ? e.target.closest('a,button,[data-track]') : null;
94    if (!el) return;
95
96    // Explicit opt-in: data-track="event_type" [data-track-label] [data-boat]
97    var explicit = el.getAttribute('data-track');
98    if (explicit) {
99      track(explicit, { label: labelFor(el), boat: el.getAttribute('data-boat') || undefined });
100      return;
101    }
102
103    var href = (el.getAttribute('href') || '').trim();
104
105    // Phone / email links
106    if (/^tel:/i.test(href)) { track('phone_click', { label: labelFor(el) }); return; }
107    if (/^mailto:/i.test(href)) { track('email_click', { label: labelFor(el) }); return; }
108
109    // Booking / CTA buttons. Only count genuine booking/CTA intent — the bare
110    // `btn` utility class is NOT enough (it's on every button, incl. waiver
111    // Continue/Back). Require a CTA-specific class or CTA-intent text.
112    var cls = (el.className || '').toString();
113    var txt = (el.textContent || '').toLowerCase().replace(/\s+/g, ' ').trim();
114    var isCta = /\b(cta|book(ing)?|reserve)\b/i.test(cls) ||
115                /\bbook a trip\b|\bbook now\b|\bbook\b|\breserve\b|check availability|request a trip|inquire/.test(txt);
116    // Exclude waiver wizard navigation (Continue/Back/Submit) which happens to
117    // use the .btn class but is not a booking CTA.
118    var isWizardNav = el.hasAttribute('data-next') || el.hasAttribute('data-back') ||
119                      /^(continue|back|next|submit|previous)$/.test(txt);
120    if (isCta && !isWizardNav) {
121      track('cta_click', { label: labelFor(el), boat: el.getAttribute('data-boat') || undefined });
122      return;
123    }
124
125    // Tour cards / tour links
126    if (/\/tours\//.test(href)) {
127      track('tour_click', { label: labelFor(el) });
128      return;
129    }
130  }, true);
131})();

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.