feat: add automatic 404 detection, scroll depth tracking, and scroll depth dashboard card

- 404 detection: checks document.title for "404" or "not found", fires custom event, SPA-aware
- Scroll depth: passive scroll listener fires events at 25/50/75/100% thresholds
- ScrollDepth dashboard card: progress bar visualization showing % of visitors reaching each threshold
- Scroll events filtered out of GoalStats to avoid duplication
- Both features on by default, opt-out via data-no-404 / data-no-scroll
This commit is contained in:
Usman Baig
2026-03-06 20:00:22 +01:00
parent 53a0341925
commit 8b1d196812
4 changed files with 151 additions and 3 deletions

View File

@@ -299,6 +299,10 @@
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 = {};
}
}
new MutationObserver(onUrlChange).observe(document, { subtree: true, childList: true });
@@ -308,7 +312,11 @@
history.replaceState = function() { _replace.apply(this, arguments); onUrlChange(); };
// * Track popstate (browser back/forward)
window.addEventListener('popstate', trackPageview);
window.addEventListener('popstate', function() {
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;
@@ -346,6 +354,62 @@
window.pulse = window.pulse || {};
window.pulse.track = trackCustomEvent;
// * 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
var trackScroll = !script.hasAttribute('data-no-scroll');
if (trackScroll) {
var scrollThresholds = [25, 50, 75, 100];
var scrollFired = {};
var scrollTicking = false;
function checkScroll() {
var docHeight = document.documentElement.scrollHeight;
var viewHeight = window.innerHeight;
// * Page fits in viewport — nothing to scroll
if (docHeight <= viewHeight) return;
var scrollTop = window.scrollY;
var scrollPercent = Math.round((scrollTop + viewHeight) / docHeight * 100);
for (var i = 0; i < scrollThresholds.length; i++) {
var t = scrollThresholds[i];
if (!scrollFired[t] && scrollPercent >= t) {
scrollFired[t] = true;
trackCustomEvent('scroll_' + t);
}
}
scrollTicking = false;
}
window.addEventListener('scroll', function() {
if (!scrollTicking) {
scrollTicking = true;
requestAnimationFrame(checkScroll);
}
}, { passive: true });
}
// * Auto-track outbound link clicks and file downloads (on by default)
// * Opt-out: add data-no-outbound or data-no-downloads to the script tag
var trackOutbound = !script.hasAttribute('data-no-outbound');