diff --git a/CHANGELOG.md b/CHANGELOG.md index 09bfaef..b0f8d70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - **Preloaded pages no longer count as visits.** Modern browsers sometimes preload pages in the background before you actually visit them. These ghost visits no longer inflate your pageview counts — only pages the visitor actually sees are tracked. - **Marketing parameters no longer fragment your pages.** Pages like `/about?utm_source=google` and `/about?utm_campaign=spring` now correctly show as just `/about` in your Top Pages. UTM tags, Facebook click IDs, Google click IDs, and other tracking parameters are stripped from the page path so all visits to the same page are grouped together. - **Trailing slashes no longer split pages.** `/about/` and `/about` now count as the same page instead of appearing as separate entries in your analytics. +- **Traffic sources are no longer over-counted.** When a visitor arrived from Facebook (or any external source) and browsed multiple pages, every page was credited to Facebook instead of just the first. Now only the landing page shows the referrer, giving you accurate traffic source numbers. +- **More ad parameters are cleaned from page paths.** Facebook and Meta ad parameters like `ad_id` and `campaign_id` are now stripped, so pages shared across different ad campaigns all show as one entry in your Top Pages. ## [0.15.0-alpha] - 2026-03-13 diff --git a/public/script.js b/public/script.js index fa81997..37c2fdb 100644 --- a/public/script.js +++ b/public/script.js @@ -226,7 +226,7 @@ } // * Normalize path: strip trailing slash and UTM/marketing query parameters - var UTM_PARAMS = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'utm_id', 'fbclid', 'gclid', 'gad_source', 'msclkid', 'twclid', 'dclid', 'mc_cid', 'mc_eid', 'ref']; + var UTM_PARAMS = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'utm_id', 'fbclid', 'gclid', 'gad_source', 'msclkid', 'twclid', 'dclid', 'mc_cid', 'mc_eid', 'ref', 'ad_id', 'adset_id', 'campaign_id', 'ad_name', 'adset_name', 'campaign_name', 'placement', 'site_source_name']; function cleanPath() { var pathname = window.location.pathname; // * Strip trailing slash (but keep root /) @@ -250,6 +250,9 @@ 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; @@ -295,16 +298,20 @@ lcpObserved = false; clsObserved = false; currentEventId = null; - // * Strip self-referrals: don't send referrer if it matches the current site domain - var rawReferrer = document.referrer || ''; + // * 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 (rawReferrer) { - try { - var refHost = new URL(rawReferrer).hostname.replace(/^www\./, ''); - var siteHost = domain.replace(/^www\./, ''); - if (refHost !== siteHost) referrer = rawReferrer; - } catch (e) { - referrer = rawReferrer; + 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 = { @@ -331,6 +338,7 @@ }).then(res => res.json()) .then(data => { recordPageview(path); + firstPageviewSent = true; if (data && data.id) { currentEventId = data.id; // * For SPA navigations the browser never emits a new largest-contentful-paint @@ -407,14 +415,17 @@ return; } var path = cleanPath(); - var rawRef = document.referrer || ''; + // * Custom events use same referrer logic: only on first pageview, empty after var 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; } + 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 = {