diff --git a/public/script.js b/public/script.js index 68c70b3..21632f6 100644 --- a/public/script.js +++ b/public/script.js @@ -189,40 +189,17 @@ return cachedSessionId; } - // * Normalize path: strip trailing slash and all query params except UTM/attribution. - // * Allowlist approach — only UTM params pass through because the backend extracts - // * them for attribution before cleaning the stored path. Everything else (cache-busters, - // * ad click IDs, filter params, etc.) is stripped to prevent path fragmentation. - var KEEP_PARAMS = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'source', 'ref']; + // * Normalize path: strip trailing slash, return pathname only. + // * UTM extraction and query handling moved server-side. function cleanPath() { var pathname = window.location.pathname; // * Strip trailing slash (but keep root /) if (pathname.length > 1 && pathname.charAt(pathname.length - 1) === '/') { pathname = pathname.slice(0, -1); } - // * Only keep allowlisted params, strip everything else - var search = window.location.search; - if (search) { - try { - var params = new URLSearchParams(search); - var kept = new URLSearchParams(); - for (var i = 0; i < KEEP_PARAMS.length; i++) { - if (params.has(KEEP_PARAMS[i])) { - kept.set(KEEP_PARAMS[i], params.get(KEEP_PARAMS[i])); - } - } - var remaining = kept.toString(); - if (remaining) pathname += '?' + remaining; - } catch (e) { - // * URLSearchParams not supported — send path without query - } - } return pathname; } - // * SPA referrer: only attribute external referrer to the landing page - var firstPageviewSent = false; - // * Refresh dedup: skip pageview if the same path was tracked within 5 seconds // * Prevents inflated pageview counts from F5/refresh while allowing genuine revisits var REFRESH_DEDUP_WINDOW = 5000; @@ -263,22 +240,7 @@ currentEventId = null; pageStartTime = 0; - // * Only send external referrer on the first pageview (landing page). - // * SPA navigations keep document.referrer stale, so clear it after first hit - // * to avoid inflating traffic source attribution. - var referrer = ''; - if (!firstPageviewSent) { - var rawReferrer = document.referrer || ''; - if (rawReferrer) { - try { - var refHost = new URL(rawReferrer).hostname.replace(/^www\./, ''); - var siteHost = domain.replace(/^www\./, ''); - if (refHost !== siteHost) referrer = rawReferrer; - } catch (e) { - referrer = rawReferrer; - } - } - } + const screenSize = { width: window.innerWidth || window.screen.width, height: window.innerHeight || window.screen.height, @@ -286,8 +248,9 @@ const payload = { domain: domain, - path: path, - referrer: referrer, + url: location.href, + title: document.title, + referrer: document.referrer || '', screen: screenSize, session_id: getSessionId(), }; @@ -303,7 +266,6 @@ }).then(res => res.json()) .then(data => { recordPageview(path); - firstPageviewSent = true; if (data && data.id) { currentEventId = data.id; pageStartTime = Date.now(); @@ -331,8 +293,6 @@ if (url !== lastUrl) { lastUrl = url; trackPageview(); - // * Check for 404 after SPA navigation (deferred so title updates first) - setTimeout(check404, 100); // * Reset scroll depth tracking for the new page if (trackScroll) scrollFired = {}; } @@ -349,58 +309,23 @@ if (url === lastUrl) return; lastUrl = url; trackPageview(); - setTimeout(check404, 100); if (trackScroll) scrollFired = {}; }); - // * Custom events / goals: validate event name (letters, numbers, underscores only; max 64 chars) - var EVENT_NAME_MAX = 64; - var EVENT_NAME_REGEX = /^[a-zA-Z0-9_]+$/; - + // * Custom events / goals function trackCustomEvent(eventName, props) { if (typeof eventName !== 'string' || !eventName.trim()) return; - var name = eventName.trim().toLowerCase(); - if (name.length > EVENT_NAME_MAX || !EVENT_NAME_REGEX.test(name)) { - if (typeof console !== 'undefined' && console.warn) { - console.warn('Pulse: event name must contain only letters, numbers, and underscores (max ' + EVENT_NAME_MAX + ' chars).'); - } - return; - } - var path = cleanPath(); - // * Custom events use same referrer logic: only on first pageview, empty after - var referrer = ''; - if (!firstPageviewSent) { - var rawRef = document.referrer || ''; - if (rawRef) { - try { - var rh = new URL(rawRef).hostname.replace(/^www\./, ''); - var sh = domain.replace(/^www\./, ''); - if (rh !== sh) referrer = rawRef; - } catch (e) { referrer = rawRef; } - } - } - var screenSize = { width: window.innerWidth || 0, height: window.innerHeight || 0 }; var payload = { domain: domain, - path: path, - referrer: referrer, - screen: screenSize, + url: location.href, + title: document.title, + referrer: document.referrer || '', + screen: { width: window.innerWidth || 0, height: window.innerHeight || 0 }, session_id: getSessionId(), - name: name, + name: eventName.trim().toLowerCase(), }; - // * Attach custom properties if provided (max 30 props, key max 200 chars, value max 2000 chars) if (props && typeof props === 'object' && !Array.isArray(props)) { - var sanitized = {}; - var count = 0; - for (var key in props) { - if (!props.hasOwnProperty(key)) continue; - if (count >= 30) break; - var k = String(key).substring(0, 200); - var v = String(props[key]).substring(0, 2000); - sanitized[k] = v; - count++; - } - if (count > 0) payload.props = sanitized; + payload.props = props; } fetch(apiUrl + '/api/v1/events', { method: 'POST', @@ -415,26 +340,6 @@ window.pulse.track = trackCustomEvent; window.pulse.cleanPath = cleanPath; - // * Auto-track 404 error pages (on by default) - // * Detects pages where document.title contains "404" or "not found" - // * Opt-out: add data-no-404 to the script tag - var track404 = !script.hasAttribute('data-no-404'); - var sent404ForUrl = ''; - - function check404() { - if (!track404) return; - // * Only fire once per URL - var currentUrl = location.href; - if (sent404ForUrl === currentUrl) return; - if (/404|not found/i.test(document.title)) { - sent404ForUrl = currentUrl; - trackCustomEvent('404'); - } - } - - // * Check on initial load (deferred so SPAs can set title) - setTimeout(check404, 0); - // * Auto-track scroll depth at 25%, 50%, 75%, and 100% (on by default) // * Each threshold fires once per pageview; resets on SPA navigation // * Opt-out: add data-no-scroll to the script tag