基于JavaScript计算用户网站停留时长的问题求助
页面停留时长计算与会话丢失问题修复
问题背景
需要计算用户从页面加载到卸载的停留时长,当前通过对比navigationStart和beforeunload触发时间实现,但存在计算不准确、会话数据丢失的问题,原代码如下:
window.addEventListener("beforeunload", function (event) { // Ensure the current interaction is saved newrelic.interaction().save(); var navStartTime = window.performance.timing.navigationStart; var unloadTime = Date.now(); var timeOnPage = unloadTime - navStartTime; var durationTimeoutReached = false; // Limit the page session to 30 minutes (1800000 milliseconds). 30-minute timeout is based on analytics standards. if (timeOnPage > 1800000) { durationTimeoutReached = true; timeOnPage = 1800000; } // Custom value passed to New Relic using API newrelic.setCustomAttribute('timeOnPage', timeOnPage); newrelic.setCustomAttribute('durationTimeoutReached', durationTimeoutReached); }); function getCookie(cookieName) { var name = cookieName + "="; var decodedCookie = decodeURIComponent(document.cookie); var ca = decodedCookie.split(';'); for (var i = 0; i < ca.length; i++) { var c = ca[i]; while (c.charAt(0) == ' ') { c = c.substring(1); } if (c.indexOf(name) == 0) { return c.substring(name.length, c.length); } } return ""; } function generateId() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }) } function setCookie(cookieName, cookieValue, maxAge) { document.cookie = cookieName + "=" + cookieValue + "; max-age=" + maxAge * 60 + "; path=/"; } function setNrAttributes(name, value) { newrelic.setCustomAttribute(name, value); newrelic.interaction().setAttribute(name, value); } function recordActivity() { activityRecorded = true; } function keepSessionCurrent() { // Check for existing session ID sessionId = getCookie("nr-user-session"); if (sessionId != '') { // If a cookie with session ID already exists, set custom attributes using it // and then reset the cookie expiration timer setNrAttributes('userSessionID', sessionId); setCookie('nr-user-session', sessionId, timeoutMinutes); } else { // If no cookie with session ID exists, generate a new ID and then create // the cookie to store it sessionId = generateId(); setNrAttributes('userSessionID', sessionId); setCookie('nr-user-session', sessionId, timeoutMinutes); } } if (typeof newrelic !== 'undefined') { if (navigator.cookieEnabled) { var timeoutMinutes = 30; var activityRecorded = false; var sessionId = ''; // When page first loads, check session cookie and update if necessary keepSessionCurrent(); // Set up event listeners to track user activity var events = ['click', 'mousedown', 'mousemove', 'keypress', 'scroll', 'touchstart']; events.forEach(function (name) { document.addEventListener(name, recordActivity, true); }); // Each minute, check if activity has occurred and reset cookie expiration if so var updateCycle = setInterval(function () { if (activityRecorded == true) { keepSessionCurrent(); activityRecorded = false; } }, 60000); } else { setNrAttributes('userSessionID', 'Unable to set cookie'); } }
问题根源
- beforeunload事件不可靠:页面崩溃、浏览器强制关闭、移动端APP被系统回收、网络中断后关闭标签等场景下,该事件不会触发,导致时长数据完全丢失。
- navigationStart的局限性:对于长时间停留的页面,浏览器可能重置performance.timing数据,或者页面在后台被冻结,导致时间计算偏差。
- 会话更新机制滞后:仅通过每分钟定时器更新会话,若用户在定时器周期内有活动但立即关闭页面,会话状态可能未及时更新;且后台时定时器可能被浏览器节流,导致cookie过期。
- 单一存储依赖:会话ID仅存在cookie中,若cookie被清除或不可用,会导致会话丢失。
修复方案与优化代码
核心优化点
- 用页面可见性API跟踪页面状态,后台时暂停计时,避免无效时长统计。
- 实现定时上报+卸载上报的双重机制,减少数据丢失风险。
- 用
Date.now()记录页面加载起始时间,替代navigationStart提升准确性。 - 结合
localStorage存储会话ID,增强鲁棒性;用户活动时立即更新会话,而非依赖定时器。
优化后的代码
if (typeof newrelic !== 'undefined') { // 基础配置 const MAX_SESSION_DURATION = 1800000; // 30分钟上限 const SESSION_TIMEOUT_MINUTES = 30; const REPORT_INTERVAL = 30000; // 每30秒上报一次当前时长 // 状态变量 let pageStartTime = Date.now(); let accumulatedTime = 0; let isPageVisible = document.visibilityState === 'visible'; let lastReportTime = pageStartTime; let sessionId = ''; let activityDetected = false; // ------------------------------ // 会话ID管理 // ------------------------------ function getSessionId() { // 先从cookie取,再从localStorage取 let id = getCookie("nr-user-session") || localStorage.getItem("nr-user-session"); if (!id) { id = generateId(); setCookie("nr-user-session", id, SESSION_TIMEOUT_MINUTES); localStorage.setItem("nr-user-session", id); } return id; } function updateSession() { sessionId = getSessionId(); setNrAttributes('userSessionID', sessionId); // 更新cookie和localStorage过期时间 setCookie("nr-user-session", sessionId, SESSION_TIMEOUT_MINUTES); localStorage.setItem("nr-user-session", sessionId); } // ------------------------------ // 时长计算与上报 // ------------------------------ function calculateCurrentDuration() { if (!isPageVisible) return accumulatedTime; const currentTime = Date.now(); const elapsed = currentTime - lastReportTime; accumulatedTime += elapsed; lastReportTime = currentTime; // 超过30分钟则截断 return Math.min(accumulatedTime, MAX_SESSION_DURATION); } function reportDuration(isFinal = false) { const duration = calculateCurrentDuration(); const timeoutReached = duration === MAX_SESSION_DURATION; newrelic.setCustomAttribute('timeOnPage', duration); newrelic.setCustomAttribute('durationTimeoutReached', timeoutReached); if (isFinal) { newrelic.interaction().save(); } } // ------------------------------ // 事件监听 // ------------------------------ // 页面可见性变化 document.addEventListener('visibilitychange', () => { const wasVisible = isPageVisible; isPageVisible = document.visibilityState === 'visible'; if (wasVisible && !isPageVisible) { // 页面切后台,计算当前时长并暂停 calculateCurrentDuration(); } else if (!wasVisible && isPageVisible) { // 页面切前台,重置上报时间 lastReportTime = Date.now(); // 更新会话 updateSession(); } }); // 用户活动监听 const activityEvents = ['click', 'mousedown', 'keypress', 'scroll', 'touchstart']; activityEvents.forEach(eventName => { document.addEventListener(eventName, () => { activityDetected = true; updateSession(); // 立即更新会话 }, true); }); // 定时上报 setInterval(() => { reportDuration(); // 检查活动,更新会话(兜底) if (activityDetected) { updateSession(); activityDetected = false; } }, REPORT_INTERVAL); // 卸载时最终上报 window.addEventListener("beforeunload", () => { reportDuration(true); }); // 初始化 updateSession(); pageStartTime = Date.now(); lastReportTime = pageStartTime; // ------------------------------ // 工具函数(复用原逻辑,优化细节) // ------------------------------ function getCookie(cookieName) { const name = `${cookieName}=`; const decodedCookie = decodeURIComponent(document.cookie); for (const c of decodedCookie.split(';')) { const trimmed = c.trim(); if (trimmed.startsWith(name)) { return trimmed.substring(name.length); } } return ""; } function generateId() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { const r = Math.random() * 16 | 0; const v = c === 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); } function setCookie(cookieName, cookieValue, maxAgeMinutes) { document.cookie = `${cookieName}=${cookieValue}; max-age=${maxAgeMinutes * 60}; path=/; SameSite=Lax`; } function setNrAttributes(name, value) { newrelic.setCustomAttribute(name, value); newrelic.interaction().setAttribute(name, value); } }
优化说明
- 数据可靠性提升:定时上报机制保证即使beforeunload未触发,已产生的时长数据也能被记录;页面可见性API避免后台无效时长被统计。
- 会话稳定性增强:同时使用cookie和localStorage存储会话ID,用户活动时立即更新会话,避免定时器节流导致的会话过期。
- 计算准确性优化:用实时时间戳计算时长,替代performance.timing的静态值,适配长时间停留和页面冻结场景。
内容的提问来源于stack exchange,提问作者Salman
相关产品推荐
相关产品推荐

