页面不可见时,停留时长永远统计不到:一次 dwell 计时排查

举报
yd_228692151 发表于 2026/11/06 06:53:55 2026/11/06
【摘要】 页面不可见时,停留时长永远统计不到:一次 dwell 计时排查 现象一个「浏览详情页超过 30 秒」的任务,页面打开后停留了 48 秒,中间每 5 秒滚动一次,看起来完全符合要求。刷新任务接口,返回依旧是:{"task_key":"case-view-daily","today_status":"todo","elapsed_seconds":0, "need_seconds":30,"m...

页面不可见时,停留时长永远统计不到:一次 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 秒"当结论,也不要只看界面。停留类任务的可靠判据是这两项同时成立:

  1. document.visibilityState === 'visible'(等待期间持续为 true)
  2. 接口返回的 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,说明阻塞点在下游服务,跟选择器、坐标、点击方式都无关。这种情况继续换点击方式、换坐标都是无效动作,应当直接判定为服务端问题并停止重试。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。