前端性能监控
利用 performance API let timing = performance.getEntriesByType('navigation')[0];
或者 let timing = performance.timing
。封装一个函数,在页面加载完毕后执行,做了一些各个阶段性能指标的计算,然后通过接口发送到服务器,用于统计判断。
在 Console 中输入 performance.getEntriesByType('navigation')[0]
:
1 | connectEnd: 9.225000045262277 |
输入 performance.timing
可以得到:
1 | connectEnd: 1608797959509 |
计算一些关键的性能指标
1 | window.addEventListener('load', (event) => { |
以上代码只测量了”首次可交互时间”,其它常用的性能指标还有:
1 | DNS 解析耗时: domainLookupEnd - domainLookupStart |
利用 let timing = performance.getEntriesByType('navigation')[0];
或者 let timing = performance.timing
计算结果是基本一致的。
前端错误监控
前端错误的分类
即时运行错误(代码错误)
资源加载错误
即时运行错误捕获方式
全局捕获
对于代码运行错误,通常的办法是使用 window.onerror 或者 addEventListener 拦截报错:
1 | window.onerror = function(errorMessage, scriptURI, lineNo, columnNo, error) { |
1 | window.addEventListener('error', function() { |
通过window.onerror事件,可以得到具体的异常信息、异常文件的URL、异常的行号与列号及异常的堆栈信息,再捕获异常后,统一上报至我们的日志服务器。
该方法能拦截到大部分的详细报错信息,但是也有例外:
- 对于跨域的代码运行错误会显示 Script error. 对于这种情况我们需要给 script 标签添加 crossorigin=”anonymous” 属性,或后端配置 Access-Control-Allow-Origin
- 对于某些浏览器可能不会显示调用栈信息,这种情况可以通过 arguments.callee.caller 来做栈递归
try catch
使用 try… catch 虽然能够较好地进行异常捕获,不至于使得页面由于一处错误挂掉,但 try … catch 捕获方式显得过于臃肿,大多代码使用 try … catch 包裹,影响代码可读性。
不过,对于异步代码来说,可以使用 catch 的方式捕获错误。比如 Promise 可以直接使用 catch 函数,async await 可以使用 try catch。
资源加载错误的捕获方式
object.onerror
img 标签、script 标签等节点都可以添加 onerror 事件,用来捕获资源加载的错误。
performance.getEntries
performance.getEntries 可以获取所有已加载资源的加载时长,通过这种方式,可以间接的拿到没有加载的资源错误。
举例:
浏览器打开一个网站,在Console控制台下,输入:
1 | performance.getEntries().forEach(function(item){console.log(item.name)}) |
上面这个api,返回的是数组,既然是数组,就可以用forEach遍历。打印出来的资源就是已经成功加载的资源。
再入document.getElementsByTagName(‘img’),就会显示出所有需要加载的的img集合。
于是,document.getElementsByTagName('img')
获取的资源数组减去通过 performance.getEntries()
获取的资源数组,剩下的就是没有成功加载的,这种方式可以间接捕获到资源加载错误。
错误上报的两种方式
ajax
采用 Ajax 通信的方式上报(此方式虽然可以上报错误,但是如果是简单的监控我们并不采用这种方式)
利用Image对象上报
利用 Image 对象上报(推荐,网站的监控体系都是采用的这种方式)
1 | //通过Image对象进行错误上报 |
这种方式,不需要借助第三方的库,一行代码即可搞定。
遇到的问题
通常在生产环境下的代码是经过 webpack 打包后压缩混淆的代码,所以我们可能会遇到所有的报错的代码行数都在第一行了,这是因为在生产环境下,我们的代码被压缩成了一行。
解决办法是开启 webpack 的 source-map,我们利用 webpack 打包后的生成的一份 .map 的脚本文件就可以让浏览器对错误位置进行追踪了。
但是问题又来了:
- 直接在生产环境使用 source-map 文件,会暴露源码,怎么办?
- source-map 在部分浏览器不兼容,怎么办?
针对第一个问题,我们可能可以把完整的 source-map 包换成 cheap-module-source-map,这样只会暴露错误所在的文件和代码行号,就不会暴露源码了。但是这样还是无法解决不兼容的问题。
针对第二个问题,可以使用引入 npm 库来支持 source-map,可以参考 mozilla/source-map。这个 npm 库既可以运行在客户端也可以运行在服务端,不过更为推荐的是在服务端使用 Node.js 对接收到的日志信息时使用 source-map 解析,以避免源代码的泄露造成风险,如下代码所示:
1 | const express = require('express'); |
针对第二个问题找到的其它解决方案:
需要服务器配置 .js.map 后缀的文件不可访问。 如果这样的话,服务器解析的时候不能直接去下载静态资源 .map 文件,而是需要去找到服务器本地对应的 map 文件,这样要单独配置路径和写逻辑很麻烦,而且文件夹结构有变动的话也不灵活。 所以我们的方案是做token权限校验,map 文件必须加正确的 token 参数,服务器才会返回资源(xxx.js.map?token=xxxx),否则 nginx 会屏蔽没有 token 或者 token 错误的请求。
这种方式就不需要单独在通过服务端接口解析了
Vue 和 React 捕获异常
Vue
正常我们捕获不到 Vue 组件的异常,查阅资料得知,在 Vue 中,异常可能被 Vue 自身给 try ... catch
了,不会传到 window.onerror
事件触发,那么我们如何把 Vue 组件中的异常作统一捕获呢?
使用 Vue.config.errorHandler 这样的 Vue 全局配置,可以在 Vue 指定组件的渲染和观察期间未捕获错误的处理函数。这个处理函数被调用时,可获取错误信息和 Vue 实例。
1 | Vue.config.errorHandler = function (err, vm, info) { |
React
在 React 中,可以使用 ErrorBoundary
组件包括业务组件的方式进行异常捕获,配合 React 16.0+ 新出的 componentDidCatch API
,可以实现统一的异常捕获和日志上报。
1 | class ErrorBoundary extends React.Component { |
使用方式如下:
1 | <ErrorBoundary> |
实际使用后的优化
我们发现不同的浏览器报错的变量可能不一样,同一个报错在chrome浏览器和firefox上 columnNo 参数一点偏差。 用两种报错解析了一下,如下图,报错的代码都是18行,是没问题的,Firefox报错是下图第一个:console 18 0 true,chrome是testBase 18 0 true,行数没问题,偏差不影响我们最终查错,我的18行源代码是:console.log(testBase)。 testBase是故意没有申明,testBase是undefined,出问题的应该是testBase这个变量,过从报错情况上看,确实是谷歌浏览器更精准一点。 虽然不在意IE,不过IE11报错列数和firefox一致。
页面触发事件报错,用户一直触发按钮,这时就会不停上报错误信息。解决:存储上一个报错信息和时间,进行比对,同一个报错,短时间内避免一直重复发送。
引入监控的项目,由于业务原因可能需要上传一些业务信息方便分析,所以预留一个配置字段,上传错误的时候请求会带上业务相关信息。
页面埋点
页面埋点应该是大家最常写的监控了,一般起码会监控以下几个数据:
- PV / UV
- 停留时长
- 流量来源
- 用户交互
对于这几类统计,一般的实现思路大致可以分为两种,分别为手写埋点和无埋点的方式。
相信第一种方式也是大家最常用的方式,可以自主选择需要监控的数据然后在相应的地方写入代码。这种方式的灵活性很大,但是唯一的缺点就是工作量较大,每个需要监控的地方都得插入代码。
另一种无埋点的方式基本不需要开发者手写埋点了,而是统计所有的事件并且定时上报。这种方式虽然没有前一种方式繁琐了,但是因为统计的是所有事件,所以还需要后期过滤出需要的数据。
开源的解决方案
一些开源的解决方案,比如腾讯的 badjs,淘宝的 JSTracker,阿里巴巴的 FdSafe,支付宝的 saijs,国外的 sentry 和对应的前端 sdk ravenjs,包括对应的 TraceKit。
目前主流的前端监控系统,包括一些收费服务,国内比如 fundebug,产品功能和 sentry 还真像,只不过他只关注前端,sentry 关注所有的 Error 收集场景,他们解决的问题其实都是本文要说到的一些通用问题,但是针对到具体项目适合不适合,就要看业务方自己选择了。
其中个人觉得 sentry 是比较好的选择,有以下几点:
- 开源
- 对各种前端框架的友好支持 (Vue、React、Angular)
- 支持 SourceMap
Sentry 官方提供的免费服务有次数限制,达到一定限制后继续使用就需要收费了,但是我们可以利用 Sentry 的开源库在自己的服务器上搭建服务,官方已经提供了完善的操作文档。
有兴趣的童鞋可以参考 Sentry 搭建错误监控系统:
超详细!搭建一个前端错误监控系统
参考链接: