监控网页崩溃的方案思考

本文最后更新于:2020年11月15日 晚上

众所周知,网页崩溃的时候,我们的JS是无法运行的,那我们要如何去做网页崩溃的监控呢?

个人理解崩溃监控的主要有两个点:能够准确监控及时上报。业界对这种监控有一个专业名词:心跳检测。大致思路是每隔一段时间做一个指定的操作来证明网页还“存活”着,当超过一定的时间没有执行该操作就说明网页已经崩溃。基于这种思路,我们还可以利用网页的loadbeforeunload事件来实现。

load事件是在页面加载后触发,beforeunload是正常网页关闭之前触发,而当网页崩溃时的关闭是无法触发beforeunload事件的,所以我们可以基于心跳检测的概念和这两个事件来实现网页崩溃的监控。

方案一:使用sessionStorage

这里使用到两个标识字段,exitFlagupdateTime来分别标识网页状态和操作的更新时间。

在load事件里面设置exitFlag字段值为pending,并加入updateTime字段,值为当前时间,设置定时器,每隔10s执行更新updateTime值。然后在beforeunload事件里面更改exitFlag字段的值为unload,表示网页正常关闭。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
window.addEventListener('load', function() {
sessionStorage.setItem('exitFlag', 'pending');
setInterval(function () {
sessionStorage.setItem('updateTime', new Date().toString());
}, 10000);
});

window.addEventListener('beforeunload', function () {
sessionStorage.setItem('exitFlag', 'unload');
});
if (sessionStorage.getItem('exitFlag') &&
sessionStorage.getItem('exitFlag') !== 'unload') {
/* 崩溃了 */
console.log('crashed on: ' + sessionStorage.getItem('updateTime'));
}

监控逻辑:当用户进入页面的时候,去查看exitFlag,如果exitFlag存在且值为pending,那就说明之前网页已经崩溃,这时可以执行上报操作。

弊端:sessionStorage只跟当前会话页面关联,如果用户在页面崩溃的时候直接关闭网页而不是刷新页面,那么sessionStorage保存的内容也跟着被清理,做不到记录上报的作用;同时因为需要用户下次刷新页面才会上报,所以无法做到崩溃的时候及时上报。

方案二:使用localStorage

既然会话页面关闭sessionStorage会失去作用,那换成localStorage呢?将字段保存在localStorage不会存在关闭页面就清空的问题,但是同源的页面会使用同一个localStorage,有可能造成数据相互覆盖和多次误报的问题,所以还需要加上一个uniqueKey唯一字段来区分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
function generatorKey() {
return +((Math.random().toString().substring(3,8) + Date.now()).toString())
}
const CRASH_TIME = 5000;
const uniqueKey = generatorKey();

window.addEventListener('load', function() {
localStorage.setItem(uniqueKey, JSON.stringify({
timeBeforeCrash: +new Date()
}));
setInterval(function () {
localStorage.setItem(uniqueKey, JSON.stringify({
timeBeforeCrash: +new Date()
}));
}, CRASH_TIME);
});

window.addEventListener('beforeunload', function () {
localStorage.removeItem(uniqueKey);
});

// 遍历 localStorage 中储存的数据
for (let key in localStorage.valueOf()) {
const item = JSON.parse(localStorage.getItem(key));
// 超过 CRASH_TIME 没有更新时间,则认为页面崩溃
if (item && item.timeBeforeCrash && (+new Date() - item.timeBeforeCrash) > CRASH_TIME) {
// 执行上报等操作...
localStorage.removeItem(key)
}
}

监控逻辑:检测关联的每个页面uniqueKeytimeBeforeCrash时间,如果超过设置的阈值未更新,则判断为页面崩溃。

弊端:同sessionStorage因为需要用户下次刷新页面才会上报,所以无法做到崩溃的时候及时上报。

方案三:使用serviceWorker

上面两种方案都是页面崩溃后需要等待再次进入页面才能上报,这就存在无法及时上报的问题,有没有其它的方法可以在页面崩溃的时候上报的工作还能继续运行,这时候就想到了web workershared workerservice woker了。考虑到各自的生命周期这个因素,service worker一般生命周期会比页面更长,关联的页面关闭后它还会继续存在,所以这里可以使用service worker来完成监控上报工作:

主页面代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function sendMessageToSw(msg){
navigator.serviceWorker.controller && navigator.serviceWorker.controller.postMessage(msg);
}
function tryToRegister() {
sendMessageToSw({
type: 'register',
reportData: {
url: location.href,
}
});
}
// 注册service worker
navigator.serviceWorker.register('./sw.js').then(registration => {
tryToRegister();
})

tryToRegister();
// 心跳回包
navigator.serviceWorker.addEventListener('message', function(event){
if (event.data.type === 'checkHealth') {
sendMessageToSw({type: 'keepHealth'});
}
});
// 页面关之前发送退出信息
window.addEventListener("beforeunload", function (event) {
sendMessageToSw({
type: 'unregister',
})
});

service worker代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
// sw 代码
const heartDetection = {};
const CRASH_TIME = 5000;
/**
* @param {Object} client
* @param {Object} msg
* 给对应的client发送消息
*/
function sendMessageToClient(client, msg){
client.postMessage(msg);
}
/**
* @param {String} id
* 根据 id 给主页面发送心跳包并检测是否存活
* 下一个心跳包发送的的时候,上一个还没回来,则认为页面已崩溃
*/
function checkHealth(id) {
if (heartDetection[id]) {
// 状态不健康就上报
if (heartDetection[id].flag !== 'healthy') {
// do something
// reportCrash(heartDetection[id].reportData);
removeCheck(id);
return;
}
// 设置成不健康,下次定时器的时候检查
heartDetection[id].flag = 'unhealthy';
sendMessageToClient(heartDetection[id].client, {type: 'checkHealth'})
}
}
/**
* @param {String} id
* 清理心跳定时器并从map中移除
*/
function removeCheck(id) {
if (heartDetection[id]) {
heartDetection[id].timer && clearInterval(heartDetection[id].timer);
delete heartDetection[id];
}
}
self.addEventListener('message', function(event){
const sourceId = event.source.id;
switch (event.data.type) {
// 页面新来的时候注册
case 'register':
// 根据id拿到对应的页面
self.clients.get(sourceId)
.then(function(client) {
heartDetection[sourceId] = {
client: client,
reportData: event.data.reportData,
timer: setInterval(function() {
checkHealth(sourceId);
}, CRASH_TIME),
flag: 'healthy',
};
client.postMessage({type: 'registerSuccess'})
})
.catch(function(err) {
console.log(err);
})
break;
// 页面关闭的时候删除有关信息
case 'unregister':
removeCheck(sourceId);
break;
case 'keepHealth':
if(heartDetection[sourceId]) {
heartDetection[sourceId].flag = 'healthy';
}
}
});

监控逻辑:主页面通过发送心跳keepHealth来表示当前关联页面的健康状态,service worker定时检测心跳的状态是否正常,如果下一个心跳包发送的的时候,上一个还没回来,则认为页面unhealthy已崩溃。

以上就是关于监控页面崩溃的一些想法和方案,后续如果有遇到更好的解决方案会同步更新。