You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于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');
    }
}

问题根源

  1. beforeunload事件不可靠:页面崩溃、浏览器强制关闭、移动端APP被系统回收、网络中断后关闭标签等场景下,该事件不会触发,导致时长数据完全丢失。
  2. navigationStart的局限性:对于长时间停留的页面,浏览器可能重置performance.timing数据,或者页面在后台被冻结,导致时间计算偏差。
  3. 会话更新机制滞后:仅通过每分钟定时器更新会话,若用户在定时器周期内有活动但立即关闭页面,会话状态可能未及时更新;且后台时定时器可能被浏览器节流,导致cookie过期。
  4. 单一存储依赖:会话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);
    }
}

优化说明

  1. 数据可靠性提升:定时上报机制保证即使beforeunload未触发,已产生的时长数据也能被记录;页面可见性API避免后台无效时长被统计。
  2. 会话稳定性增强:同时使用cookie和localStorage存储会话ID,用户活动时立即更新会话,避免定时器节流导致的会话过期。
  3. 计算准确性优化:用实时时间戳计算时长,替代performance.timing的静态值,适配长时间停留和页面冻结场景。

内容的提问来源于stack exchange,提问作者Salman

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.04 06:55:57