Diff -- /var/www/vhosts/3dshawn.com/abforge.3dshawn.com/t.js
Diff

/var/www/vhosts/3dshawn.com/abforge.3dshawn.com/t.js

added on local at 2026-07-01 16:01:41

Added
+372
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to b55cf57627c9
Restore this content →
Unified diff
Naive LCS-based line diff. Additions in emerald, deletions in rose. Both sides decompressed live from BLOB_STORE.
1/* =====================================================================
2 ABForge tracker -- embedded on any customer website with:
3 <script async src="https://abforge.3dshawn.com/t.js?k=PUBLIC_KEY"></script>
4
5 What it does:
6 1. Reads/creates abf_uid (long-lived visitor cookie) and abf_sid
7 (session cookie).
8 2. Sends a pageview to /c.cgi on load (sendBeacon when possible).
9 3. Asks /x.cgi for any live A/B / MVT variant assignments for the
10 current URL, then applies the variant's DOM ops client-side.
11 4. Tracks clicks (heatmap), max scroll depth, and form submits.
12 5. Sends a heartbeat every 15s with active time on page + scroll
13 depth so we can compute time-on-page accurately.
14 6. Exposes a tiny global API: window.abforge.event(name, value)
15 and window.abforge.goal(name, value) for the customer to fire
16 custom events from their own code.
17===================================================================== */
18(function () {
19 'use strict';
20
21 if (window.__abforge_loaded) return;
22 window.__abforge_loaded = true;
23
24 /* ---- Resolve the public key from the script's own src ----------- */
25 function findKey() {
26 var s = document.currentScript;
27 if (!s) {
28 var ss = document.getElementsByTagName('script');
29 for (var i = 0; i < ss.length; i++) {
30 if ((ss[i].src || '').indexOf('/t.js') > 0) { s = ss[i]; break; }
31 }
32 }
33 if (!s) return { key: '', host: '' };
34 var src = s.src || '';
35 var m = src.match(/[?&]k=([A-Za-z0-9]+)/);
36 var hostM = src.match(/^(https?:\/\/[^\/]+)/);
37 return { key: m ? m[1] : '', host: hostM ? hostM[1] : '' };
38 }
39 var ctx = findKey();
40 if (!ctx.key) return;
41 var KEY = ctx.key;
42 var HOST = ctx.host; // e.g. https://abforge.3dshawn.com
43
44 var COLLECT = HOST + '/c.cgi';
45 var EXPOSE = HOST + '/x.cgi';
46 var GOAL = HOST + '/g.cgi';
47 var HEAT = HOST + '/h.cgi';
48
49 /* ---- Cookie helpers --------------------------------------------- */
50 function getCookie(name) {
51 var m = document.cookie.match(new RegExp('(?:^|; )' + name + '=([^;]*)'));
52 return m ? decodeURIComponent(m[1]) : '';
53 }
54 function setCookie(name, value, days) {
55 var exp = '';
56 if (days) {
57 var d = new Date();
58 d.setTime(d.getTime() + days * 86400000);
59 exp = '; expires=' + d.toUTCString();
60 }
61 document.cookie = name + '=' + encodeURIComponent(value) + exp + '; path=/; SameSite=Lax';
62 }
63 function uuid() {
64 return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
65 var r = Math.random() * 16 | 0;
66 return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
67 });
68 }
69 var UID_COOKIE = 'abf_uid';
70 var SID_COOKIE = 'abf_sid';
71 var UID = getCookie(UID_COOKIE);
72 if (!/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(UID)) {
73 UID = uuid(); setCookie(UID_COOKIE, UID, 365 * 2);
74 }
75 var SID = getCookie(SID_COOKIE);
76 if (!/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(SID)) {
77 SID = uuid(); setCookie(SID_COOKIE, SID, 1); // 1 day; server enforces 30-min idle
78 }
79
80 /* ---- Fingerprint ------------------------------------------------- */
81 function detect() {
82 var ua = navigator.userAgent || '';
83 var os = '', osv = '';
84 if (/Windows NT 10/i.test(ua)) { os = 'Windows'; osv = '10/11'; }
85 else if (/Windows NT ([\d.]+)/i.test(ua)) { os = 'Windows'; osv = RegExp.$1; }
86 else if (/Mac OS X ([\d_]+)/i.test(ua)) { os = 'macOS'; osv = RegExp.$1.replace(/_/g, '.'); }
87 else if (/iPhone|iPad|iPod/i.test(ua)) { os = 'iOS'; osv = (ua.match(/OS ([\d_]+)/) || [,''])[1].replace(/_/g, '.'); }
88 else if (/Android ([\d.]+)/i.test(ua)) { os = 'Android'; osv = RegExp.$1; }
89 else if (/Linux/i.test(ua)) { os = 'Linux'; }
90 var br = '', brv = '';
91 if (/Edg\/([\d.]+)/.test(ua)) { br = 'Edge'; brv = RegExp.$1; }
92 else if (/OPR\/([\d.]+)/.test(ua)) { br = 'Opera'; brv = RegExp.$1; }
93 else if (/Firefox\/([\d.]+)/.test(ua)){ br = 'Firefox'; brv = RegExp.$1; }
94 else if (/Chrome\/([\d.]+)/.test(ua)) { br = 'Chrome'; brv = RegExp.$1; }
95 else if (/Version\/([\d.]+).*Safari/.test(ua)) { br = 'Safari'; brv = RegExp.$1; }
96 var dev = 'desktop';
97 if (/bot|crawl|spider|google|bing|yahoo|slurp|facebookexternalhit/i.test(ua)) dev = 'bot';
98 else if (/iPad|Tablet/i.test(ua)) dev = 'tablet';
99 else if (/Mobi|iPhone|Android.*Mobile/i.test(ua)) dev = 'mobile';
100 var tz = '';
101 try { tz = Intl.DateTimeFormat().resolvedOptions().timeZone || ''; } catch (e) {}
102 return {
103 os_name: os, os_version: osv,
104 browser_name: br, browser_version: brv,
105 device_type: dev,
106 screen_width: (screen && screen.width) || 0,
107 screen_height: (screen && screen.height) || 0,
108 viewport_width: window.innerWidth || 0,
109 viewport_height: window.innerHeight || 0,
110 color_depth: (screen && screen.colorDepth) || 24,
111 pixel_ratio: window.devicePixelRatio || 1,
112 touch_capable: ('ontouchstart' in window || navigator.maxTouchPoints > 0) ? 1 : 0,
113 language: navigator.language || '',
114 timezone: tz
115 };
116 }
117 var FP = detect();
118
119 /* ---- UTM params ------------------------------------------------- */
120 function readUTM() {
121 var qs = location.search || '';
122 var out = {};
123 qs.replace(/[?&]([^=&]+)=([^&]*)/g, function (_, k, v) {
124 k = decodeURIComponent(k); v = decodeURIComponent(v.replace(/\+/g, ' '));
125 if (/^utm_/.test(k)) out[k] = v;
126 });
127 return out;
128 }
129 var UTM = readUTM();
130
131 /* ---- Lightweight transport (sendBeacon or fetch keepalive) ------ */
132 function send(url, payload) {
133 var body = JSON.stringify(payload);
134 if (navigator.sendBeacon) {
135 try { return navigator.sendBeacon(url, new Blob([body], { type: 'application/json' })); } catch (e) {}
136 }
137 try {
138 fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' },
139 body: body, keepalive: true, credentials: 'omit', mode: 'cors' });
140 } catch (e) {}
141 }
142 function jget(url) {
143 return fetch(url, { method: 'GET', credentials: 'omit', mode: 'cors' })
144 .then(function (r) { return r.json(); })
145 .catch(function () { return null; });
146 }
147
148 /* ---- Selector path for heatmap clicks --------------------------- */
149 function selectorPath(el) {
150 if (!el || el.nodeType !== 1) return '';
151 var parts = [], depth = 0;
152 while (el && el.nodeType === 1 && depth < 8) {
153 var tag = (el.tagName || '').toLowerCase();
154 if (!tag || tag === 'html') break;
155 if (el.id && /^[A-Za-z][\w\-:.]*$/.test(el.id)) {
156 parts.unshift('#' + el.id); break;
157 }
158 var part = tag;
159 var parent = el.parentNode;
160 if (parent && parent.children && parent.children.length > 1) {
161 var n = 1;
162 for (var i = 0; i < parent.children.length; i++) {
163 var sib = parent.children[i];
164 if (sib === el) break;
165 if (sib.tagName === el.tagName) n++;
166 }
167 part += ':nth-of-type(' + n + ')';
168 }
169 parts.unshift(part);
170 if (tag === 'body') break;
171 el = parent; depth++;
172 }
173 return parts.join('>').slice(0, 500);
174 }
175
176 /* ---- Variant exposure ------------------------------------------- */
177 function applyOps(ops) {
178 if (!ops || !ops.length) return;
179 for (var i = 0; i < ops.length; i++) {
180 var op = ops[i];
181 try {
182 var els = document.querySelectorAll(op.selector);
183 for (var j = 0; j < els.length; j++) {
184 var el = els[j];
185 if (op.op === 'text') el.textContent = op.value || '';
186 else if (op.op === 'html') el.innerHTML = op.value || '';
187 else if (op.op === 'attr') el.setAttribute(op.attr, op.value || '');
188 else if (op.op === 'css') el.style.cssText += ';' + (op.value || '');
189 else if (op.op === 'class') el.classList.add(op.value || '');
190 else if (op.op === 'hide') el.style.display = 'none';
191 else if (op.op === 'show') el.style.display = '';
192 }
193 } catch (e) {}
194 }
195 }
196 function applyVariant(t, v) {
197 var ch = {};
198 try { ch = typeof v.changes === 'string' ? JSON.parse(v.changes || '{}') : (v.changes || {}); } catch (e) {}
199 if (ch.redirect_url) { try { location.replace(ch.redirect_url); } catch (e) {} return; }
200 if (ch.custom_css) {
201 var st = document.createElement('style'); st.textContent = ch.custom_css; document.head.appendChild(st);
202 }
203 applyOps(ch.ops);
204 if (ch.custom_js) { try { new Function(ch.custom_js).call(window); } catch (e) {} }
205 // Tag DOM so the customer can verify in DevTools
206 if (document.body) document.body.setAttribute('data-abf-exposure', t + ':' + v.id);
207 }
208
209 var ACTIVE_EXPERIMENTS = []; // [{test_id, variant_id}]
210 function fetchExperiments() {
211 var u = EXPOSE + '?k=' + encodeURIComponent(KEY)
212 + '&u=' + encodeURIComponent(UID)
213 + '&s=' + encodeURIComponent(SID)
214 + '&p=' + encodeURIComponent(location.pathname + location.search);
215 return jget(u).then(function (res) {
216 if (!res || !res.ok || !res.experiments) return;
217 for (var i = 0; i < res.experiments.length; i++) {
218 var ex = res.experiments[i];
219 if (ex.variant) {
220 applyVariant(ex.test_id, ex.variant);
221 ACTIVE_EXPERIMENTS.push({ test_id: ex.test_id, variant_id: ex.variant.id });
222 }
223 }
224 });
225 }
226
227 /* ---- Pageview + lifecycle --------------------------------------- */
228 function basePayload(extras) {
229 var p = {
230 k: KEY, u: UID, s: SID,
231 p: location.pathname + location.search,
232 url: location.href,
233 r: document.referrer || ''
234 };
235 if (extras) for (var k in extras) if (extras.hasOwnProperty(k)) p[k] = extras[k];
236 return p;
237 }
238 function pickExperimentContext() {
239 if (!ACTIVE_EXPERIMENTS.length) return {};
240 var e = ACTIVE_EXPERIMENTS[0]; // primary exposure -- multiple OK at write time
241 return { xt: e.test_id, xv: e.variant_id };
242 }
243
244 function sendPageview() {
245 var payload = basePayload({
246 t: 'pv', title: document.title || '',
247 referrer: document.referrer || ''
248 });
249 for (var k in FP) if (FP.hasOwnProperty(k)) payload[k] = FP[k];
250 for (var u in UTM) if (UTM.hasOwnProperty(u)) payload[u] = UTM[u];
251 var ec = pickExperimentContext();
252 for (var x in ec) if (ec.hasOwnProperty(x)) payload[x] = ec[x];
253 send(COLLECT, payload);
254 }
255
256 /* ---- Heartbeat + scroll depth ----------------------------------- */
257 var ACTIVE_MS = 0, LAST_TICK = Date.now(), IS_ACTIVE = true, MAX_SCROLL = 0;
258 function tick() {
259 var now = Date.now();
260 if (IS_ACTIVE && document.visibilityState === 'visible') {
261 ACTIVE_MS += (now - LAST_TICK);
262 }
263 LAST_TICK = now;
264 }
265 function fireHeartbeat() {
266 tick();
267 var secs = Math.round(ACTIVE_MS / 1000);
268 if (secs <= 0) return;
269 var ec = pickExperimentContext();
270 send(COLLECT, basePayload(Object.assign({ t: 'hb', d: secs, sd: MAX_SCROLL }, ec)));
271 ACTIVE_MS = 0;
272 }
273 document.addEventListener('visibilitychange', function () {
274 IS_ACTIVE = (document.visibilityState === 'visible');
275 LAST_TICK = Date.now();
276 });
277 window.addEventListener('scroll', function () {
278 var h = Math.max(document.body.scrollHeight - window.innerHeight, 1);
279 var pct = Math.min(100, Math.round((window.scrollY / h) * 100));
280 if (pct > MAX_SCROLL) MAX_SCROLL = pct;
281 }, { passive: true });
282 setInterval(fireHeartbeat, 15000);
283 window.addEventListener('pagehide', fireHeartbeat);
284 window.addEventListener('beforeunload', fireHeartbeat);
285
286 /* ---- Click + form_submit + rage clicks -------------------------- */
287 var clickStamps = [];
288 document.addEventListener('click', function (e) {
289 var t = e.target; var sel = selectorPath(t);
290 var ox = 0.5, oy = 0.5, have = 0;
291 try {
292 var r = t.getBoundingClientRect();
293 if (r && r.width > 0 && r.height > 0) {
294 ox = (e.clientX - r.left) / r.width;
295 oy = (e.clientY - r.top) / r.height;
296 if (ox < 0) ox = 0; else if (ox > 1) ox = 1;
297 if (oy < 0) oy = 0; else if (oy > 1) oy = 1;
298 have = 1;
299 }
300 } catch (err) {}
301 var xpct = (e.clientX / Math.max(window.innerWidth, 1)) * 100;
302 var ypct = ((e.clientY + window.scrollY) / Math.max(document.body.scrollHeight, 1)) * 100;
303 var ec = pickExperimentContext();
304 send(COLLECT, basePayload(Object.assign({
305 t: 'click', sel: sel,
306 text: ((t.innerText || t.value || '') + '').slice(0, 80),
307 x: xpct.toFixed(2), y: ypct.toFixed(2)
308 }, ec)));
309 // Heatmap row (anchored to element)
310 send(HEAT, basePayload(Object.assign({
311 t: 'hm', sel: sel,
312 ox: have ? ox.toFixed(4) : null,
313 oy: have ? oy.toFixed(4) : null,
314 x: xpct.toFixed(2),
315 y: ypct.toFixed(2),
316 vw: window.innerWidth || 0
317 }, ec)));
318 // Rage-click detection: 3+ clicks on same element within 1s
319 var now = Date.now();
320 clickStamps = clickStamps.filter(function (c) { return now - c.ts < 1000; });
321 clickStamps.push({ ts: now, sel: sel });
322 var same = 0;
323 for (var i = 0; i < clickStamps.length; i++) { if (clickStamps[i].sel === sel) same++; }
324 if (same >= 3) {
325 send(COLLECT, basePayload(Object.assign({ t: 'rage_click', sel: sel }, ec)));
326 clickStamps = [];
327 }
328 }, true);
329
330 document.addEventListener('submit', function (e) {
331 var f = e.target;
332 var sel = selectorPath(f);
333 var ec = pickExperimentContext();
334 send(COLLECT, basePayload(Object.assign({
335 t: 'form_submit', sel: sel,
336 name: (f.getAttribute && f.getAttribute('name')) || ''
337 }, ec)));
338 }, true);
339
340 /* ---- Public API for the customer site --------------------------- */
341 window.abforge = {
342 event: function (name, value, meta) {
343 var ec = pickExperimentContext();
344 send(COLLECT, basePayload(Object.assign({
345 t: 'custom', name: String(name).slice(0, 120),
346 v: typeof value === 'number' ? value : null,
347 meta: meta ? JSON.stringify(meta).slice(0, 480) : ''
348 }, ec)));
349 },
350 goal: function (name, value) {
351 send(GOAL, basePayload({
352 name: String(name).slice(0, 120),
353 v: typeof value === 'number' ? value : null
354 }));
355 },
356 identify: function (props) {
357 // Best-effort: just attach as a custom event so it ends up on the timeline
358 this.event('identify', null, props);
359 },
360 isExposed: function () { return ACTIVE_EXPERIMENTS.slice(0); }
361 };
362
363 /* ---- Boot ------------------------------------------------------- */
364 function boot() {
365 fetchExperiments().then(sendPageview).catch(sendPageview);
366 }
367 if (document.readyState === 'loading') {
368 document.addEventListener('DOMContentLoaded', boot);
369 } else {
370 boot();
371 }
372})();
Keyboard: j next diff k previous diff g top G bottom r restore c compile-check ? help