页面不可见时,停留时长永远统计不到:一次 dwell 计时排查
页面不可见时,停留时长永远统计不到:一次 dwell 计时排查
现象
一个「浏览详情页超过 30 秒」的任务,页面打开后停留了 48 秒,中间每 5 秒滚动一次,看起来完全符合要求。刷新任务接口,返回依旧是:
{"task_key":"case-view-daily","today_status":"todo","elapsed_seconds":0,
"need_seconds":30,"max_gap_ms":15000}
elapsed_seconds 为 0 说明计时器一次都没有累加,不是"差几秒"的问题。
先排除接口和服务端
这类任务通常有三个字段值得先读:need_seconds(门槛秒数)、max_gap_ms(心跳最大间隔)、elapsed_seconds(已累计秒数)。如果 elapsed_seconds 恒为 0,而任务状态始终 todo,基本可以判定前端计时器没有上报,而不是服务端判定失败。
检查页面自身发起的请求:
performance.getEntriesByType('resource')
.map(r => r.name)
.filter(n => /growth|task|progress|heartbeat/i.test(n));
结果里有 case-view-daily 和 case-view-daily/sessions,说明会话已经建立、上报通道存在,缺的是计时数据。
关键判据:有焦点 ≠ 可见
JSON.stringify({
vis: document.visibilityState, // "hidden"
focus: document.hasFocus() // true
})
hasFocus() 返回 true 非常容易误导。它只表示页面是当前窗口的活动文档,不代表窗口在屏幕上可见。浏览器最小化、被其他窗口完全遮挡、或者运行在无交互桌面的会话里,visibilityState 都会是 hidden。
而停留时长统计普遍用 visibilitychange 和 requestAnimationFrame / setInterval 组合:
- 页面
hidden时两种计时源都会被浏览器节流甚至暂停,累计值停住; - 心跳间隔超过
max_gap_ms,累计值还会被判定为"离开了"并重置。
所以只要 visibilityState 是 hidden,滚多少次都不算数。
恢复可见性的三步
单纯 Page.bringToFront 常常不够——窗口如果是最小化状态,即使把它提到最前,标签页依然是 hidden。需要三件事一起做:
// 1) 把标签页提到最前
await c.send('Page.bringToFront');
// 2) 还原窗口(最小化/最大化都可能让页面保持 hidden)
const w = await c.send('Browser.getWindowForTarget', { targetId });
await c.send('Browser.setWindowBounds', { windowId: w.windowId, bounds: { windowState: 'normal' } });
// 3) 强制页面生命周期为 active
await c.send('Page.setWebLifecycleState', { state: 'active' });
第三步是关键。Page.setWebLifecycleState({state:'active'}) 会把页面从 frozen/ 后台状态拉回活跃状态,并把可见性重置为 visible;前两步只改窗口层级,改不了页面的 lifecycle 状态。
实测加上这三步后,同一个页面立刻变成:
{"vis":"visible","focus":true}
再停留 40 秒(每 5 秒滚动一次),任务状态从 todo 变为 done,completed_at 出现时间戳。
判据要落在接口字段上
不要用"我停留了 40 秒"当结论,也不要只看界面。停留类任务的可靠判据是这两项同时成立:
document.visibilityState === 'visible'(等待期间持续为 true)- 接口返回的
elapsed_seconds增长,最终today_status === 'done'且completed_at非空
如果第 1 项不满足,第 2 项永远不会变,此时无论重试多少次都是白跑。
附带记一条:elementFromPoint 命中不代表请求会返回
同一次排查里还有个更容易误判的现象:弹窗确认按钮用 elementFromPoint 回验命中、pointer-events: auto、display: block、opacity: 1,点击后 DOM 有反应,但界面不前进。
打开 Network 域后再点一次,只看到一条请求发出去,没有任何 responseReceived:
POST /api/files/create (发出,25 秒内无响应、无 loadingFailed)
请求发出但既不成功也不失败、监听不到 loadingFailed,说明阻塞点在下游服务,跟选择器、坐标、点击方式都无关。这种情况继续换点击方式、换坐标都是无效动作,应当直接判定为服务端问题并停止重试。
- 点赞
- 收藏
- 关注作者
评论(0)