整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

如何精确统计页面停留时长

.背景

页面停留时间(Time on Page)简称 Tp,是网站分析中很常见的一个指标,用于反映用户在某些页面上停留时间的长短,传统的Tp统计方法会存在一定的统计盲区,比如无法监控单页应用,没有考虑用户切换Tab、最小化窗口等操作场景。 基于上述背景,重新调研和实现了精确统计页面停留时长的方案,需要 兼容单页应用和多页应用,并且不耦合或入侵业务代码。

2.分析

我们可以把一个页面生命周期抽象为三个动作: 「进入」、「活跃状态切换」、「离开」

如下图,计算页面停留时长既如何监控这三个动作,然后在对应触发的事件中记录时间戳,比如要统计活跃停留时长就把 active 区间相加即可,要统计总时长既 tn -t0 。

2.1 如何监听页面的进入和离开?

对于常规页面的 首次加载、页面关闭、刷新 等操作都可以通过 window.onload 和 window.onbeforeunload 事件来监听页面进入和离开,浏览器前进后退可以通过 pageshow 和 pagehide 处理。

对于单页应用内部的跳转可以转化为两个问题:1.监听路由变化,2.判断变化的URL是否为不同页面 。

2.1.1 监听路由变化

目前主流的单页应用大部分都是基于 browserHistory (history api) 或者 hashHistory 来做路由处理,我们可以通过监听路由变化来判断页面是否有可能切换。注意是有可能切换,因为URL发生变化不代表页面一定切换,具体的路由配置是由业务决定的(既URL和页面的匹配规则)。

browserHistory

路由的变化本质都会调用 History.pushState() 或 History.replaceState() ,能监听到这两个事件就能知道。通过 popstate 事件能解决一半问题,因为 popstate 只会在浏览器前进后退的时候触发,当调用 history.pushState() or history.replaceState() 的时候并不会触发。

The popstate event is fired when the active history entry changes. If the history entry being activated was created by a call to history.pushState() or was affected by a call to history.replaceState(), the popstate event's state property contains a copy of the history entry's state object.

Note that just calling history.pushState() or history.replaceState() won't trigger a popstate event. The popstate event will be triggered by doing a browser action such as a click on the back or forward button (or calling。history.back() or history.forward() in JavaScript).

这里需要通过猴子补丁(Monkeypatch)解决,运行时重写 history.pushState 和 history.replaceState 方法:

let _wr = function (type) {
 let orig = window.history[type]
 return function () {
 let rv = orig.apply(this, arguments)
 let e = new Event(type.toLowerCase())
 e.arguments = arguments
 window.dispatchEvent(e)
 return rv
 }
}
window.history.pushState = _wr('pushState')
window.history.replaceState = _wr('replaceState')
window.addEventListener('pushstate', function (event) {})
window.addEventListener('replacestate', function (event) {})

hashHistory

hashHistory 的实现是基于 hash 的变化,hash 的变化可以通过 hashchange 来监听

2.1.2 判断URL是否为不同页面

问题本质是怎么定义一个页面,这里我们无法自动获取,因为不同业务场景定义不同,需要业务方在初始化的时候配置 rules 参数,默认不传入 rules 的情况取 location.pathname 为 key,key 不相同则判断为不同的页面,配置的语法:

new Tracer({
 rules: [
 { path: '/index' },
 { path: '/detail/:id' },
 { path: '/user', query: {tab: 'profile'} }
 ]
)

对于页面进入和离开相关事件整理

2.2 如何监听页面活跃状态切换?

可以通过 Page Visibility API 以及在 window 上声明 onblur/onfocus 事件来处理。

2.2.1 Page Visibility API

一个网页的可见状态可以通过 Page Visibility API 获取,比如当用户 切换浏览器Tab、最小化窗口、电脑睡眠 的时候,系统API会派发一个当前页面可见状态变化的 visibilitychange 事件,然后在事件绑定函数中通过 document.hidden 或者 document.visibilityState 读取当前状态。

document.addEventListener('visibilitychange', function (event) {
 console.log(document.hidden, document.visibilityState)
})

2.2.2 onblur/onfocus

2.3 什么时机上报数据?

2.3.1 页面离开时上报

对于页面刷新或者关闭窗口触发的操作可能会造成数据丢失

2.3.2 下次打开页面时上报

会丢失历史访问记录中的最后一个页面数据

目前采用的方案2,对于单页内部跳转是即时上报,对于单页/多页应用触发 window.onbeforeunload 事件的时候会把当前页面数据暂存在 localStorage 中,当用户下次进入页面的时候会把暂存数据上报。有个细节问题,如果用户下次打开页面是在第二天,对于统计当天的活跃时长会有一定的误差,所以在数据上报的同时会把该条数据的页面进入时间/离开时间带上。

3.设计

3.1 UML类关系图

Tracer

核心类,用来实例化一个监控,对原生事件和自定义事件的封装,监听 enter activechange exit 事件来操作当前 Page 实例。

P.S. 取名来自暴雪旗下游戏守望先锋英雄猎空(Tracer),直译为:追踪者。

Page

页面的抽象类,用来实例化一个页面,封装了 enter exit active inactive 等操作,内部通过 state 属性来维护当前页面状态。

3.2 事件派发关系图

4.兼容性

Desktop

Mobile

5.思考

对于页面停留时长的定义可能在不同场景会有差异,比如内部业务系统或者OA系统,产品可能更关心用户在页面的活跃时长;而对于资讯类型的产品,页面可见时长会更有价值。单一的数据对业务分析是有限的,所以在具体的代码实过程中我们会把停留时长分三个指标,这样能更好的帮助产品/运营分析。

active 页面活跃时长

visible 页面可见时长 //仅支持Desktop

duration 页面总停留时长

6.TODO

移动端的兼容性目前还没完全覆盖;

对于页面的配置目前还不够灵活,考虑支持 react-router / vue-router 的配置;

byted-cg-tracer 待封装;开发中

7.参考

https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onhashchange

https://developer.mozilla.org/en-US/docs/Web/Events/popstate

https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API

https://stackoverflow.com/questions/4570093/how-to-get-notified-about-changes-of-the-history-via-history-pushstate

.什么是时间戳?

时间戳是字符或编码信息的序列,用于标识何时发生特定事件,通常给出日期和时间,有时精确到几分之一秒。 该术语源自办公室中使用的橡皮图章,用于在纸质文档上用墨水在当前日期和时间上加盖戳记,以记录接收文档的时间。 此类时间戳的常见用例是在纸质信件上的邮戳或考勤打卡纸上的“入”和“出”时间。在现代,该术语的使用已扩展为指附加到数字数据的数字日期和时间信息。 例如,计算机文件包含时间戳,该时间戳指示文件的最后修改时间,数码相机将时间戳添加到所拍摄的照片中,记录拍摄日期和时间。

对于电子商务应用,如电子合同签署、电子文档签名等,都需要一个能证明合同签署时间和文档签名时间的可信证明,但是由于用户桌面电脑或手机或者服务器时间是可以随意修改的,如果签署合同和文档时用这些不可信的时间,则无法保证合同的签署时间可信。因此合同签署和文档签名需要一个权威第三方来提供可信赖的且不可抵赖的时间戳服务。

2.密信时间戳服务工作原理

密信(MeSign)App提供的电子文档数字签名服务免费为用户配套提供Adobe全球信任的时间戳服务,密信时间戳服务符合RFC3161国际标准和相应的国家标准。时间戳服务就是将经过时间戳服务器签名的一个可信赖的日期和时间与特定电子数据绑定在一起,为PDF签名应用提供可信的时间证明。 其工作原理示意图如下左图所示,用户对待签名文件生成摘要数据,并把此数据提交给时间戳服务器请求签名,时间戳服务器对摘要数据和一个

来自权威时间源的一个日期/时间记录进行签名,生成时间戳签名数据返回给签名工具,签名工具把此时间戳数据写入到待签名的PDF文件即完成时间戳签名。密信时间戳服务时间源来自国家授时中心可靠计时系统。

用户在使用密信App的电子签名服务数字签名文档和签署电子合同时,密信App自动调用密信时间戳服务并自动把时间戳签名数据同文件签名数据和LTV数据一起按照国际标准写入到待签名的PDF文件中完成PDF文件数字签名。用户无需手动配置时间戳服务网址,无需另外花钱购买时间戳服务,使用密信App完成数字签名的时间戳服务完全免费和完全全自动。

3.使用密信App签名的文档的时间戳是什么样的?

如下左图所示,使用密信(MeSign)App签名的文档使用Adobe阅读器打开后显示的详细时间戳信息,显示时间戳颁发机构为 MeSign Time Stamping Signer。如下中图所示,Adobe阅读器显示的签名有效性小结信息,显示“签名包含嵌入的时间戳”和“安全时间戳时间已验证”,表明此文档采用了Adobe全球信任的时间戳服务。如下右图所示,点击Adobe阅读器左边的签名图标也会显示此签名包含嵌入的时间戳。

为了确保所有已签名PDF文件的时间戳签名全球信任和国密合规,密信技术独创了双算法证书双时间戳签名技术,自动对待签名的同一个PDF文件用RSA和SM2算法计算两份摘要(HASH)数据,并分别用RSA时间戳证书和SM2时间戳证书实现双时间戳签名,再把这两个时间戳签名数据写入到PDF文件中,完成双证书双时间戳签名,确保用户使用Adobe阅读器显示RSA时间戳签名信息和国密阅读器显示国密时间戳签名信息,只有这样,才能真正满足Adobe全球信任和国密合规的双标准要求,才能真正实现已签署合同和文件的全球范围具有法律效力。

使用Adobe阅读器打开上面已下载的已签名样板文件,查看时间戳签名信息和显示的效果如下左图所示。而使用支持国密算法的 密信阅读器(内测版)打开此文件,查看时间戳签名信息和显示的效果如下图右图所示。从这个样板文件的两个时间戳时间对比可以看出,密信App文档签名时先用国密时间戳证书签名后用RSA时间戳签名。

密信可信时间戳免费试用搜索:密信MeSign

试用地址:https://www.mesign.com/zh-cn/tsa/index.html

例:倒计时

案例分析:

1.这个倒计时是不断变化的,因此需要定时器来自动变化(setInterval)

2.三个黑色盒子里面分别存放时分秒

3.三个黑色盒子利用innerHTML放入计算的小时分钟秒数

4.第一次执行也是间隔毫秒数,因此刚刷新页面会有空白

5.最好采取封装函数的方式,这样可以先调用一次这个函数,防止刚开始刷新页面有空白问题。

倒计时的算法:

1.核心算法:输入的时间减去现在的时间就是剩余的时间,即倒计时,但是不能拿着时分秒相减,比如05分减去25分,

结果会是负数的

2.用时间戳来做,用户输入时间总的毫秒数减去现在时间的总的毫秒数,得到的就是剩余时间的毫秒数.

3.把剩余时间总的毫秒数转换为天、时、分、秒、(时间戳转换为时分秒)*/

//转换公式如下:

/*d = parseInt(总秒数/60/60/24); //计算天数

h = parseInt(总秒数/60/60%24); //计算小时

m = parseInt(总秒数/60%24); //计算分钟

s = parseInt(总秒数%60); //计算当前秒数*/

function conus(time){

var dqtime = +new Date(time);

var zqtime = +new Date();

var times = (dqtime - zqtime) / 1000;

var t = parseInt(times / 60 / 60 / 24);//天

t = t < 10 ? '0' + t:t;

var s = parseInt(times / 60 / 60 % 24);//时

s = s < 10 ? '0' + s:s;

var f = parseInt(times / 60 % 60);//分

f = f < 10 ? '0' + f:f;

var m = parseInt(times % 60);//秒

m = m < 10 ? '0' + m:m;

return t + '天' + s + '时' + f + '分' + m + '秒';

}

console.log(conus('2022-6-7 12:00:00'));

源码如下