added on local at 2026-07-01 15:03:05
| 1 | /* ===================================================================== | |
| 2 | ContactForge tracker -- embedded on crm.3dshawn.com marketing pages: | |
| 3 | <script async src="/t.js"></script> | |
| 4 | ||
| 5 | Single-tenant: every event posts to this CRM's own ingest endpoints. | |
| 6 | No public key, no site filter -- the back end stores everything as | |
| 7 | "this site's" traffic. | |
| 8 | ||
| 9 | What it does: | |
| 10 | 1. Reads/creates cf_track (long-lived visitor cookie) and cf_sess | |
| 11 | (session cookie). UUID v4. | |
| 12 | 2. Sends a pageview to /c.cgi on load (sendBeacon when possible). | |
| 13 | 3. Tracks clicks (heatmap), max scroll depth, and form submits. | |
| 14 | 4. Sends a heartbeat every 15s with active time on page + scroll | |
| 15 | depth so we can compute time-on-page accurately. | |
| 16 | 5. Exposes a tiny global API: window.cftrack.event(name, value) | |
| 17 | and window.cftrack.goal(name, value) for fire-and-forget events. | |
| 18 | ===================================================================== */ | |
| 19 | (function () { | |
| 20 | 'use strict'; | |
| 21 | ||
| 22 | if (window.__cftrack_loaded) return; | |
| 23 | window.__cftrack_loaded = true; | |
| 24 | ||
| 25 | /* ---- Endpoints (relative; same-origin) -------------------------- */ | |
| 26 | var COLLECT = '/c.cgi'; | |
| 27 | var HEAT = '/h.cgi'; | |
| 28 | var GOAL = '/g.cgi'; | |
| 29 | ||
| 30 | /* ---- Cookie helpers --------------------------------------------- */ | |
| 31 | function getCookie(name) { | |
| 32 | var m = document.cookie.match(new RegExp('(?:^|; )' + name + '=([^;]*)')); | |
| 33 | return m ? decodeURIComponent(m[1]) : ''; | |
| 34 | } | |
| 35 | function setCookie(name, value, days) { | |
| 36 | var exp = ''; | |
| 37 | if (days) { | |
| 38 | var d = new Date(); | |
| 39 | d.setTime(d.getTime() + days * 86400000); | |
| 40 | exp = '; expires=' + d.toUTCString(); | |
| 41 | } | |
| 42 | document.cookie = name + '=' + encodeURIComponent(value) + exp + '; path=/; SameSite=Lax'; | |
| 43 | } | |
| 44 | function uuid() { | |
| 45 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { | |
| 46 | var r = Math.random() * 16 | 0; | |
| 47 | return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16); | |
| 48 | }); | |
| 49 | } | |
| 50 | var UID_COOKIE = 'cf_track'; | |
| 51 | var SID_COOKIE = 'cf_sess'; | |
| 52 | var UID = getCookie(UID_COOKIE); | |
| 53 | if (!/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(UID)) { | |
| 54 | UID = uuid(); setCookie(UID_COOKIE, UID, 365 * 2); | |
| 55 | } | |
| 56 | var SID = getCookie(SID_COOKIE); | |
| 57 | if (!/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(SID)) { | |
| 58 | SID = uuid(); setCookie(SID_COOKIE, SID, 1); // 1 day; server enforces 30-min idle | |
| 59 | } | |
| 60 | ||
| 61 | /* ---- Fingerprint ------------------------------------------------- */ | |
| 62 | function detect() { | |
| 63 | var ua = navigator.userAgent || ''; | |
| 64 | var os = '', osv = ''; | |
| 65 | if (/Windows NT 10/i.test(ua)) { os = 'Windows'; osv = '10/11'; } | |
| 66 | else if (/Windows NT ([\d.]+)/i.test(ua)) { os = 'Windows'; osv = RegExp.$1; } | |
| 67 | else if (/Mac OS X ([\d_]+)/i.test(ua)) { os = 'macOS'; osv = RegExp.$1.replace(/_/g, '.'); } | |
| 68 | else if (/iPhone|iPad|iPod/i.test(ua)) { os = 'iOS'; osv = (ua.match(/OS ([\d_]+)/) || [,''])[1].replace(/_/g, '.'); } | |
| 69 | else if (/Android ([\d.]+)/i.test(ua)) { os = 'Android'; osv = RegExp.$1; } | |
| 70 | else if (/Linux/i.test(ua)) { os = 'Linux'; } | |
| 71 | var br = '', brv = ''; | |
| 72 | if (/Edg\/([\d.]+)/.test(ua)) { br = 'Edge'; brv = RegExp.$1; } | |
| 73 | else if (/OPR\/([\d.]+)/.test(ua)) { br = 'Opera'; brv = RegExp.$1; } | |
| 74 | else if (/Firefox\/([\d.]+)/.test(ua)){ br = 'Firefox'; brv = RegExp.$1; } | |
| 75 | else if (/Chrome\/([\d.]+)/.test(ua)) { br = 'Chrome'; brv = RegExp.$1; } | |
| 76 | else if (/Version\/([\d.]+).*Safari/.test(ua)) { br = 'Safari'; brv = RegExp.$1; } | |
| 77 | var dev = 'desktop'; | |
| 78 | if (/bot|crawl|spider|google|bing|yahoo|slurp|facebookexternalhit/i.test(ua)) dev = 'bot'; | |
| 79 | else if (/iPad|Tablet/i.test(ua)) dev = 'tablet'; | |
| 80 | else if (/Mobi|iPhone|Android.*Mobile/i.test(ua)) dev = 'mobile'; | |
| 81 | var tz = ''; | |
| 82 | try { tz = Intl.DateTimeFormat().resolvedOptions().timeZone || ''; } catch (e) {} | |
| 83 | return { | |
| 84 | os_name: os, os_version: osv, | |
| 85 | browser_name: br, browser_version: brv, | |
| 86 | device_type: dev, | |
| 87 | screen_w: (screen && screen.width) || 0, | |
| 88 | screen_h: (screen && screen.height) || 0, | |
| 89 | viewport_w: window.innerWidth || 0, | |
| 90 | viewport_h: window.innerHeight || 0, | |
| 91 | language: navigator.language || '', | |
| 92 | timezone: tz | |
| 93 | }; | |
| 94 | } | |
| 95 | var FP = detect(); | |
| 96 | ||
| 97 | /* ---- UTM params ------------------------------------------------- */ | |
| 98 | function readUTM() { | |
| 99 | var qs = location.search || ''; | |
| 100 | var out = {}; | |
| 101 | qs.replace(/[?&]([^=&]+)=([^&]*)/g, function (_, k, v) { | |
| 102 | k = decodeURIComponent(k); v = decodeURIComponent(v.replace(/\+/g, ' ')); | |
| 103 | if (/^utm_/.test(k)) out[k] = v; | |
| 104 | }); | |
| 105 | return out; | |
| 106 | } | |
| 107 | var UTM = readUTM(); | |
| 108 | ||
| 109 | /* ---- Lightweight transport (sendBeacon or fetch keepalive) ------ */ | |
| 110 | function send(url, payload) { | |
| 111 | var body = JSON.stringify(payload); | |
| 112 | if (navigator.sendBeacon) { | |
| 113 | try { return navigator.sendBeacon(url, new Blob([body], { type: 'application/json' })); } catch (e) {} | |
| 114 | } | |
| 115 | try { | |
| 116 | fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, | |
| 117 | body: body, keepalive: true, credentials: 'omit', mode: 'cors' }); | |
| 118 | } catch (e) {} | |
| 119 | } | |
| 120 | ||
| 121 | /* ---- Selector path for heatmap clicks --------------------------- */ | |
| 122 | function selectorPath(el) { | |
| 123 | if (!el || el.nodeType !== 1) return ''; | |
| 124 | var parts = [], depth = 0; | |
| 125 | while (el && el.nodeType === 1 && depth < 8) { | |
| 126 | var tag = (el.tagName || '').toLowerCase(); | |
| 127 | if (!tag || tag === 'html') break; | |
| 128 | if (el.id && /^[A-Za-z][\w\-:.]*$/.test(el.id)) { | |
| 129 | parts.unshift('#' + el.id); break; | |
| 130 | } | |
| 131 | var part = tag; | |
| 132 | var parent = el.parentNode; | |
| 133 | if (parent && parent.children && parent.children.length > 1) { | |
| 134 | var n = 1; | |
| 135 | for (var i = 0; i < parent.children.length; i++) { | |
| 136 | var sib = parent.children[i]; | |
| 137 | if (sib === el) break; | |
| 138 | if (sib.tagName === el.tagName) n++; | |
| 139 | } | |
| 140 | part += ':nth-of-type(' + n + ')'; | |
| 141 | } | |
| 142 | parts.unshift(part); | |
| 143 | if (tag === 'body') break; | |
| 144 | el = parent; depth++; | |
| 145 | } | |
| 146 | return parts.join('>').slice(0, 500); | |
| 147 | } | |
| 148 | ||
| 149 | /* ---- Payload builders ------------------------------------------- */ | |
| 150 | function basePayload(extras) { | |
| 151 | var p = { | |
| 152 | u: UID, s: SID, | |
| 153 | p: location.pathname + location.search, | |
| 154 | url: location.href, | |
| 155 | r: document.referrer || '' | |
| 156 | }; | |
| 157 | if (extras) for (var k in extras) if (extras.hasOwnProperty(k)) p[k] = extras[k]; | |
| 158 | return p; | |
| 159 | } | |
| 160 | ||
| 161 | function sendPageview() { | |
| 162 | var payload = basePayload({ | |
| 163 | t: 'pv', title: document.title || '', | |
| 164 | referrer: document.referrer || '' | |
| 165 | }); | |
| 166 | for (var k in FP) if (FP.hasOwnProperty(k)) payload[k] = FP[k]; | |
| 167 | for (var u in UTM) if (UTM.hasOwnProperty(u)) payload[u] = UTM[u]; | |
| 168 | send(COLLECT, payload); | |
| 169 | } | |
| 170 | ||
| 171 | /* ---- Heartbeat + scroll depth ----------------------------------- */ | |
| 172 | var ACTIVE_MS = 0, LAST_TICK = Date.now(), IS_ACTIVE = true, MAX_SCROLL = 0; | |
| 173 | function tick() { | |
| 174 | var now = Date.now(); | |
| 175 | if (IS_ACTIVE && document.visibilityState === 'visible') { | |
| 176 | ACTIVE_MS += (now - LAST_TICK); | |
| 177 | } | |
| 178 | LAST_TICK = now; | |
| 179 | } | |
| 180 | function fireHeartbeat() { | |
| 181 | tick(); | |
| 182 | var secs = Math.round(ACTIVE_MS / 1000); | |
| 183 | if (secs <= 0) return; | |
| 184 | send(COLLECT, basePayload({ t: 'hb', d: secs, sd: MAX_SCROLL })); | |
| 185 | // Also send a scroll-depth heatmap row so we can build the histogram. | |
| 186 | send(HEAT, basePayload({ | |
| 187 | t: 'scroll', sd: MAX_SCROLL, | |
| 188 | vw: window.innerWidth || 0, | |
| 189 | vh: window.innerHeight || 0, | |
| 190 | ph: document.body ? document.body.scrollHeight : 0 | |
| 191 | })); | |
| 192 | ACTIVE_MS = 0; | |
| 193 | } | |
| 194 | document.addEventListener('visibilitychange', function () { | |
| 195 | IS_ACTIVE = (document.visibilityState === 'visible'); | |
| 196 | LAST_TICK = Date.now(); | |
| 197 | }); | |
| 198 | window.addEventListener('scroll', function () { | |
| 199 | var h = Math.max(document.body.scrollHeight - window.innerHeight, 1); | |
| 200 | var pct = Math.min(100, Math.round((window.scrollY / h) * 100)); | |
| 201 | if (pct > MAX_SCROLL) MAX_SCROLL = pct; | |
| 202 | }, { passive: true }); | |
| 203 | setInterval(fireHeartbeat, 15000); | |
| 204 | window.addEventListener('pagehide', fireHeartbeat); | |
| 205 | window.addEventListener('beforeunload', fireHeartbeat); | |
| 206 | ||
| 207 | /* ---- Click + form_submit + rage clicks -------------------------- */ | |
| 208 | var clickStamps = []; | |
| 209 | document.addEventListener('click', function (e) { | |
| 210 | var t = e.target; var sel = selectorPath(t); | |
| 211 | var xpct = (e.clientX / Math.max(window.innerWidth, 1)) * 100; | |
| 212 | var ypct = ((e.clientY + window.scrollY) / Math.max(document.body.scrollHeight, 1)) * 100; | |
| 213 | send(COLLECT, basePayload({ | |
| 214 | t: 'click', sel: sel, | |
| 215 | text: ((t.innerText || t.value || '') + '').slice(0, 80), | |
| 216 | x: xpct.toFixed(2), y: ypct.toFixed(2) | |
| 217 | })); | |
| 218 | // Heatmap row. | |
| 219 | send(HEAT, basePayload({ | |
| 220 | t: 'hm', sel: sel, | |
| 221 | x: xpct.toFixed(2), | |
| 222 | y: ypct.toFixed(2), | |
| 223 | vw: window.innerWidth || 0, | |
| 224 | vh: window.innerHeight || 0 | |
| 225 | })); | |
| 226 | // Rage-click detection: 3+ clicks on same element within 1s. | |
| 227 | var now = Date.now(); | |
| 228 | clickStamps = clickStamps.filter(function (c) { return now - c.ts < 1000; }); | |
| 229 | clickStamps.push({ ts: now, sel: sel }); | |
| 230 | var same = 0; | |
| 231 | for (var i = 0; i < clickStamps.length; i++) { if (clickStamps[i].sel === sel) same++; } | |
| 232 | if (same >= 3) { | |
| 233 | send(COLLECT, basePayload({ t: 'rage_click', sel: sel })); | |
| 234 | clickStamps = []; | |
| 235 | } | |
| 236 | }, true); | |
| 237 | ||
| 238 | document.addEventListener('submit', function (e) { | |
| 239 | var f = e.target; | |
| 240 | var sel = selectorPath(f); | |
| 241 | send(COLLECT, basePayload({ | |
| 242 | t: 'form_submit', sel: sel, | |
| 243 | name: (f.getAttribute && f.getAttribute('name')) || '' | |
| 244 | })); | |
| 245 | }, true); | |
| 246 | ||
| 247 | /* ---- Public API ------------------------------------------------- */ | |
| 248 | window.cftrack = { | |
| 249 | event: function (name, value, meta) { | |
| 250 | send(COLLECT, basePayload({ | |
| 251 | t: 'custom', name: String(name).slice(0, 120), | |
| 252 | v: typeof value === 'number' ? value : null, | |
| 253 | meta: meta ? JSON.stringify(meta).slice(0, 480) : '' | |
| 254 | })); | |
| 255 | }, | |
| 256 | goal: function (name, value) { | |
| 257 | send(GOAL, basePayload({ | |
| 258 | name: String(name).slice(0, 120), | |
| 259 | v: typeof value === 'number' ? value : null | |
| 260 | })); | |
| 261 | }, | |
| 262 | identify: function (props) { | |
| 263 | this.event('identify', null, props); | |
| 264 | } | |
| 265 | }; | |
| 266 | ||
| 267 | /* ---- Boot ------------------------------------------------------- */ | |
| 268 | function boot() { sendPageview(); } | |
| 269 | if (document.readyState === 'loading') { | |
| 270 | document.addEventListener('DOMContentLoaded', boot); | |
| 271 | } else { | |
| 272 | boot(); | |
| 273 | } | |
| 274 | })(); |