diff --git a/CHANGELOG.md b/CHANGELOG.md index c711b6e..2525a3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ## [Unreleased] +### Improved + +- **More accurate rage click detection.** Rage clicks no longer fire when you triple-click to select text on a page. Previously, selecting a paragraph (a normal 3-click action) was being counted as a rage click, which inflated frustration metrics. Only genuinely frustrated rapid clicking is tracked now. +- **More accurate dead click detection.** Dead clicks were being reported on elements that actually worked — like close buttons on cart drawers, modal dismiss buttons, and page content areas. Three fixes make dead clicks much more reliable: + - Buttons that trigger changes elsewhere on the page (closing a drawer, opening a modal) are no longer flagged as dead. + - Page content areas that aren't actually clickable (like `
` containers) are no longer treated as interactive elements. + - Single-page app navigations are now properly detected, so links that use client-side routing aren't mistakenly reported as broken. + ### Removed - **Performance insights removed.** The Performance tab, Core Web Vitals tracking (LCP, CLS, INP), and the "Enable performance insights" toggle in Settings have been removed. The tracking script no longer collects Web Vitals data. Visit duration tracking continues to work as before. diff --git a/public/script.js b/public/script.js index a2a596b..340590a 100644 --- a/public/script.js +++ b/public/script.js @@ -579,6 +579,15 @@ // * Check if rage click threshold is met if (entry.times.length >= RAGE_CLICK_THRESHOLD) { + // * Skip if user is selecting text (triple-click to select paragraph) + try { + var sel = window.getSelection(); + if (sel && sel.toString().trim().length > 0) { + entry.times = []; + return; + } + } catch (ex) {} + // * Debounce: max one rage_click per element per 5 seconds if (now - entry.lastFired >= RAGE_CLICK_DEBOUNCE) { var clickCount = entry.times.length; @@ -602,7 +611,7 @@ // * or network request occurs within 1 second // * Opt-out: add data-no-dead to the script tag if (!script.hasAttribute('data-no-dead')) { - var INTERACTIVE_SELECTOR = 'a,button,input,select,textarea,[role="button"],[role="link"],[role="tab"],[role="menuitem"],[onclick],[tabindex]'; + var INTERACTIVE_SELECTOR = 'a,button,input,select,textarea,[role="button"],[role="link"],[role="tab"],[role="menuitem"],[onclick],[tabindex]:not([tabindex="-1"])'; var DEAD_CLICK_DEBOUNCE = 10000; var DEAD_CLEANUP_INTERVAL = 30000; var deadClickDebounce = {}; // * selector -> lastFiredTimestamp @@ -665,11 +674,15 @@ var mutationObs = null; var perfObs = null; var cleanupTimer = null; + var popstateHandler = null; + var hashchangeHandler = null; function cleanup() { if (mutationObs) { try { mutationObs.disconnect(); } catch (ex) {} mutationObs = null; } if (perfObs) { try { perfObs.disconnect(); } catch (ex) {} perfObs = null; } if (cleanupTimer) { clearTimeout(cleanupTimer); cleanupTimer = null; } + if (popstateHandler) { window.removeEventListener('popstate', popstateHandler); popstateHandler = null; } + if (hashchangeHandler) { window.removeEventListener('hashchange', hashchangeHandler); hashchangeHandler = null; } } function onEffect() { @@ -677,7 +690,7 @@ cleanup(); } - // * Set up MutationObserver to detect DOM changes on the element and its parent + // * Set up MutationObserver to detect DOM changes on the element, its parent, and body if (typeof MutationObserver !== 'undefined') { try { mutationObs = new MutationObserver(function() { @@ -689,6 +702,8 @@ if (parent && parent.tagName !== 'HTML' && parent.tagName !== 'BODY') { mutationObs.observe(parent, { childList: true }); } + // * Also observe body for top-level DOM changes (modals, drawers, overlays, toasts) + mutationObs.observe(document.body, { childList: true, attributes: true }); } catch (ex) { mutationObs = null; } @@ -706,6 +721,12 @@ } } + // * Listen for SPA navigation events (popstate, hashchange) + popstateHandler = function() { onEffect(); }; + hashchangeHandler = function() { onEffect(); }; + window.addEventListener('popstate', popstateHandler); + window.addEventListener('hashchange', hashchangeHandler); + // * After 1 second, check if any effect was detected cleanupTimer = setTimeout(function() { cleanup();