fix: improve visit duration reliability with pagehide fallback and dedup guard

This commit is contained in:
Usman Baig
2026-03-15 11:58:33 +01:00
parent 7e30d04df3
commit 1e147c955b
2 changed files with 15 additions and 14 deletions

View File

@@ -8,7 +8,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
### Improved
- **Deeper journey exploration.** The depth slider on the Journeys page now goes up to 10 steps (previously capped at 5), so you can follow longer visitor paths through your site and see exactly where people go after many clicks.
- **Redesigned Journeys page.** The depth slider now matches the rest of the UI and goes up to 10 steps. Controls are integrated into the chart card for a cleaner layout, and Top Paths are shown as visual breadcrumb cards instead of a cramped table.
- **More reliable visit duration tracking.** Visit duration was silently dropping to 0s for visitors who only viewed one page — especially on mobile or when closing a tab quickly. The tracking script now captures time-on-page more reliably across all browsers, and sessions where duration couldn't be measured are excluded from the average instead of counting as 0s.
- **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.
- **Fresher CDN data.** BunnyCDN statistics now refresh every 3 hours instead of once a day, so your CDN tab shows much more current bandwidth, request, and cache data.
- **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:

View File

@@ -37,21 +37,20 @@
// * Time-on-page tracking: records when the current pageview started
var pageStartTime = 0;
var metricsSent = false;
function sendMetrics() {
if (!currentEventId) return;
if (!currentEventId || metricsSent) return;
// * Calculate time-on-page in seconds
var durationSec = pageStartTime > 0 ? Math.round((Date.now() - pageStartTime) / 1000) : 0;
var payload = { event_id: currentEventId };
// * Always include duration if we have a valid measurement
if (durationSec > 0) payload.duration = durationSec;
// * Skip if nothing to send (no duration)
if (!payload.duration) return;
if (durationSec <= 0) return;
var data = JSON.stringify(payload);
metricsSent = true;
var data = JSON.stringify({ event_id: currentEventId, duration: durationSec });
if (navigator.sendBeacon) {
navigator.sendBeacon(apiUrl + '/api/v1/metrics', new Blob([data], {type: 'application/json'}));
@@ -66,12 +65,12 @@
}
// * Send metrics when user leaves or hides the page
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
// * Delay slightly so duration measurement captures final moment
setTimeout(sendMetrics, 150);
}
// * visibilitychange is the primary signal, pagehide is the fallback
// * for browsers/scenarios where visibilitychange doesn't fire (tab close, mobile app kill)
document.addEventListener('visibilitychange', function() {
if (document.visibilityState === 'hidden') sendMetrics();
});
window.addEventListener('pagehide', sendMetrics);
// * Memory cache for session ID (fallback if storage is unavailable)
let cachedSessionId = null;
@@ -312,6 +311,7 @@
if (data && data.id) {
currentEventId = data.id;
pageStartTime = Date.now();
metricsSent = false;
}
}).catch(() => {
// * Silently fail - don't interrupt user experience