feat: add dead click detection to tracking script
This commit is contained in:
121
public/script.js
121
public/script.js
@@ -547,6 +547,127 @@
|
||||
}, true); // * Capture phase
|
||||
}
|
||||
|
||||
// * Auto-track dead clicks (clicks on interactive elements that produce no effect)
|
||||
// * Fires dead_click when an interactive element is clicked but no DOM change, navigation,
|
||||
// * or network request occurs within 1 second
|
||||
// * Opt-out: add data-no-dead to the script tag
|
||||
if (!script.getAttribute('data-no-dead')) {
|
||||
var INTERACTIVE_SELECTOR = 'a,button,input,select,textarea,[role="button"],[role="link"],[role="tab"],[role="menuitem"],[onclick],[tabindex]';
|
||||
var DEAD_CLICK_DEBOUNCE = 10000;
|
||||
var DEAD_CLEANUP_INTERVAL = 30000;
|
||||
var deadClickDebounce = {}; // * selector -> lastFiredTimestamp
|
||||
|
||||
// * Cleanup stale dead click debounce entries every 30 seconds
|
||||
setInterval(function() {
|
||||
var now = Date.now();
|
||||
for (var key in deadClickDebounce) {
|
||||
if (!deadClickDebounce.hasOwnProperty(key)) continue;
|
||||
if (now - deadClickDebounce[key] > DEAD_CLEANUP_INTERVAL) {
|
||||
delete deadClickDebounce[key];
|
||||
}
|
||||
}
|
||||
}, DEAD_CLEANUP_INTERVAL);
|
||||
|
||||
// * Polyfill check for Element.matches
|
||||
var matchesFn = (function() {
|
||||
var ep = Element.prototype;
|
||||
return ep.matches || ep.msMatchesSelector || ep.webkitMatchesSelector || null;
|
||||
})();
|
||||
|
||||
// * Find the nearest interactive element by walking up max 3 levels
|
||||
function findInteractiveElement(el) {
|
||||
if (!matchesFn) return null;
|
||||
var depth = 0;
|
||||
var current = el;
|
||||
while (current && depth <= 3) {
|
||||
if (current.nodeType === 1 && matchesFn.call(current, INTERACTIVE_SELECTOR)) {
|
||||
return current;
|
||||
}
|
||||
current = current.parentElement;
|
||||
depth++;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
document.addEventListener('click', function(e) {
|
||||
var target = findInteractiveElement(e.target);
|
||||
if (!target) return;
|
||||
|
||||
var selector = getElementIdentifier(target);
|
||||
if (!selector) return;
|
||||
|
||||
var now = Date.now();
|
||||
|
||||
// * Debounce: max one dead_click per element per 10 seconds
|
||||
if (deadClickDebounce[selector] && now - deadClickDebounce[selector] < DEAD_CLICK_DEBOUNCE) {
|
||||
return;
|
||||
}
|
||||
|
||||
var currentPath = window.location.pathname + window.location.search;
|
||||
var clickX = String(Math.round(e.clientX));
|
||||
var clickY = String(Math.round(e.clientY));
|
||||
var effectDetected = false;
|
||||
var hrefBefore = location.href;
|
||||
var mutationObs = null;
|
||||
var perfObs = null;
|
||||
var cleanupTimer = 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; }
|
||||
}
|
||||
|
||||
function onEffect() {
|
||||
effectDetected = true;
|
||||
cleanup();
|
||||
}
|
||||
|
||||
// * Set up MutationObserver to detect DOM changes on the element and its parent
|
||||
if (typeof MutationObserver !== 'undefined') {
|
||||
try {
|
||||
mutationObs = new MutationObserver(function() {
|
||||
onEffect();
|
||||
});
|
||||
var mutOpts = { childList: true, attributes: true, characterData: true, subtree: true };
|
||||
mutationObs.observe(target, mutOpts);
|
||||
if (target.parentElement) {
|
||||
mutationObs.observe(target.parentElement, mutOpts);
|
||||
}
|
||||
} catch (ex) {
|
||||
mutationObs = null;
|
||||
}
|
||||
}
|
||||
|
||||
// * Set up PerformanceObserver to detect network requests
|
||||
if (typeof PerformanceObserver !== 'undefined') {
|
||||
try {
|
||||
perfObs = new PerformanceObserver(function() {
|
||||
onEffect();
|
||||
});
|
||||
perfObs.observe({ type: 'resource' });
|
||||
} catch (ex) {
|
||||
perfObs = null;
|
||||
}
|
||||
}
|
||||
|
||||
// * After 1 second, check if any effect was detected
|
||||
cleanupTimer = setTimeout(function() {
|
||||
cleanup();
|
||||
// * Also check if navigation occurred
|
||||
if (effectDetected || location.href !== hrefBefore) return;
|
||||
|
||||
deadClickDebounce[selector] = Date.now();
|
||||
trackCustomEvent('dead_click', {
|
||||
selector: selector,
|
||||
page_path: currentPath,
|
||||
x: clickX,
|
||||
y: clickY
|
||||
});
|
||||
}, 1000);
|
||||
}, true); // * Capture phase
|
||||
}
|
||||
|
||||
// * 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');
|
||||
|
||||
Reference in New Issue
Block a user