专栏 马蜂窝技术 文章详情
马蜂窝技术 95 发布于 马蜂窝技术 关注专栏
2 天前发布
JavaScript 543 次阅读 . 读完需要 28 分钟
19
随着业务的快速发展, 我们对生产环境下的问题感知能力越来越关注. 作为距离用户最近的一层, 前端的表现是否可靠, 稳定, 好用, 很大程度上决定着用户对整个产品的体验和感受. 因此, 对于前端的监控不容忽视.
搭建一套前端监控平台需要考虑的方面很多, 比如数据采集, 埋点模式, 数据处理和分析, 报警以及监控平台在具体业务中的应用等等. 在这所有环节中, 准确, 完整, 全面的数据采集是一切的前提, 也为后续的用户精细化运营提供基础.
前端技术的日新月异给数据采集也带来了变化和挑战, 传统的手工打点模式已经不能满足需求. 如何在新的技术背景下让前端数据采集工作更加完善, 高效, 是本文讨论的重点.
前端监控数据采集
在采集数据之前, 首先要考虑采集什么样的数据. 我们重点关注两类数据, 一类是与用户体验相关的, 如首屏时间, 文件加载时间, 页面性能等; 另外是帮助我们及时感知产品上线后是否出现异常的, 比如资源错误, API 响应时间等. 具体来说, 我们对前端的数据采集具体主要分为:
路由切换 (href,hashchange,pushState)
JsError
性能 (performance)
资源错误
API
日志上报
路由切换
vue,React,Angular 等前端技术的快速发展使单页面应用盛行. 我们都知道, 传统的页面应用是用一些超链接来实现页面切换和跳转的, 而单页面应用是使用各自的路由系统来管理前端的每一个页面切换, 例如 vue-router,react-router 等, 跳转时仅刷新局部资源 ,JS,CSS 等公共资源只需要加载一次, 这就使传统网页进入离开的方式只有第一次打开能被记录. 单页应用后续所有路由切换的方式有两种, 一种是 Hash, 一种是 html5 推出的 History API.
1. href
href 为页面初始化的第一次进入, 这里只需要单纯上报「进入页面」事件即可.
2. hashchange
Hash 路由一个明显的标志是带有「 # 」.Hash 的优势是兼容性更好, 但问题在于 URL 中一直存在「 # 」并不美观. 我们主要通过监听 URL 中的 hashchange 来捕获具体的 hash 值进行检测.
- Windows.addEventListener('hashchange', function() {
- // 上报[进入页面] 事件
- }, true)
需要注意的是, 在新版 vue-router 中如果浏览器支持 history, 即使 mode 选择 hash 也会优先选择 history 模式, 虽然表现形式暂时还是 # 号, 但实际上是模拟的, 所以千万不要认为自己在 mode 选择了 hash 就一定会是 hash.
3. History API
History 利用了 HTML5 History Interface 中新增的 pushState() 和 replaceState() 方法进行路由切换, 是目前主流的无刷新切换路由方式. 与 hashchange 只能改变 # 后面的代码片段相比, History API (pushState,replaceState) 给了前端完全的自由.
PopState 是浏览器返回事件的回调, 但是更新路由的 pushState,replaceState 并没有回调事件, 因此, 还需要分别在 history.pushState() 和 history.replaceState() 方法里处理 URL 的变化. 在这里, 我们运用到了一种类似 Java 的 AOP 编程思想, 对 pushState 和 replaceState 进行改造.
AOP (Aspect-oriented programming)即面向切面编程, 提倡针对同一类问题进行统一处理. AOP 的核心思想是让某个模块能够重用, 它采用横向抽取机制, 将功能代码从业务逻辑代码中分离出来, 扩展功能而不修改源代码, 相比封装来说隔离得更加彻底.
下面介绍我们的具体改造方式:
- // 第一阶段: 我们对原生方法进行包装, 调用前执行 dispatchEvent 了一个同样的事件
- function aop (type) {
- var source = Windows.history[type];
- return function () {
- var event = new Event(type);
- event.arguments = arguments;
- Windows.dispatchEvent(event);
- var rewrite = source.apply(this, arguments);
- return rewrite;
- };
- }
- // 第二阶段: 将 pushState 和 replaceState 进行基于 AOP 思想的代码注入
- Windows.history.pushState = aop('pushState');
- Windows.history.replaceState = aop('replaceState'); // 更改路由, 不会留下历史记录
- // 第三阶段: 捕获 pushState 和 replaceState
- Windows.addEventListener('pushState', function() {
- // 上报[进入页面] 事件
- }, true)
- Windows.addEventListener('replaceState', function() {
- // 上报[进入页面] 事件
- }, true)
Windows.history.pushState 实际调用关系如图:
至此, 我们对 pushState,replaceState 改造完毕, 实现了有效地捕获路由切换. 可以看到, 我们在不侵入业务代码的情况下, 对 Windows.history.pushState 进行了扩展, 在调用的同时会主动 dispatchEvent 一个 pushState.
但在这里我们也能看到一个弊端, 就是如果 AOP 代理函数发生 JS 错误, 将会阻断后续的调用关系, 使实际的 Windows.history.pushState 无法被调用. 所以在使用此方式的时候, 要对 AOP 代理函数的内容做好完善的 try catch, 来防止业务上出现异常.
*__Tips: 想自动捕获页面停留时间只需要在下一个进入页面事件触发时, 通过上一个页面的打点时间和当前时间做差值即可, 这时候可以上报一个[离开页面] 事件.
JsError
前端项目中, 由于 JavaScript 本身是一个弱类型语言, 加上浏览器环境的复杂性, 网络问题等, 很容易发生错误. 因此做好网页错误监控, 不断优化代码, 提高代码健壮性是一项很重要的工作.
JsError 的捕获可以帮助我们分析和监控线上问题, 它与我们在 Chrome 浏览器的调试工具 Console 中看到的内容一致.
1. Windows.onerror
我们使用 Windows.onerror 捕获一般情况下 JS 错误的异常信息. 捕获 JS 错误的方式有两种, Windows.onerror 和 Windows.addEventListener('error'). 一般情况下, 捕获 JS 异常不推荐使用 addEventListener('error'), 主要是因为它没有堆栈信息, 而且还需要对捕获到的信息做区分, 因为它会将所有异常信息捕获到, 包括资源加载错误等.
- Windows.onerror = function (msg, url, lineno, colno, stack) {
- // 上报 [JS 错误] 事件
- }
- 2. Uncaught (in promise)
当 Promise 内发生 JS 错误或者 reject 信息未被业务处理的情况时, 会抛出一个 unhandledrejection, 并且这个错误不会被 Windows.onerror 以及 Windows.addEventListener('error') 捕获, 这里需要用专门的 Windows.addEventListener('unhandledrejection') 进行捕获处理:
- Windows.addEventListener('unhandledrejection', function (e) {
- var reg_url = /\(([^)]*)\)/;
- var fileMsg = e.reason.stack.split('\n')[1].match(reg_url)[1];
- var fileArr = fileMsg.split(':');
- var lineno = fileArr[fileArr.length - 2];
- var colno = fileArr[fileArr.length - 1];
- var url = fileMsg.slice(0, -lno.length - cno.length - 2);}, true);
- var msg = e.reason.message;
- // 上报 [JS 错误] 事件
- }
我们注意到 unhandledrejection 因为继承自 PromiseRejectionEvent,PromiseRejectionEvent 又继承自 Event, 所以 msg,url,lineno,colno,stack 以字符串形式放到了 e.reason.stack 中, 我们需要解析出来上述参数来和 onerror 参数对齐, 为后续监控平台的指标统一化打下基础.
3. 常见问题
"Script error."
如果出现捕获的 msg 全部为 "Script error." , 问题在于你的 JS 地址和当前网页不在同一个域下. 因为我们要经常在线上的版本做静态资源 CDN 化, 会导致常访问的页面跟脚本文件来自不同的域名. 这时如果没有进行额外的配置, 浏览器出于安全方面的设计就容易出现 "Script error.". 我们可以利用目前流行的 webpack 打包工具来处理此类问题.
- // webpack config 配置
- // 处理 HTML 注入 JS 添加跨域标识
- plugins: [
- new HtmlWebpackPlugin({
- filename: 'html/index.html',
- template: HTML_PATH,
- attributes: {
- crossorigin: 'anonymous'
- }
- }),
- new HtmlWebpackPluginCrossorigin({
- inject: true
- })
- ]
- // 处理按需加载的 JS 添加跨域标识
- output: {
- crossOriginLoading: true
- }
- SourceMap
大部分场景下, 生产环境中的代码都是经过压缩合并的, 这使得我们捕获到的错误很难映射到具体的源码, 为我们解决问题带来很大困扰, 这里简要提出 2 个解决方案的思路.
生产环境我们需要添加 sourceMap 配置, 这会导致安全隐患, 因为这样外网就可以通过 sourceMap 进行源码映射. 为了降低风险, 我们可以通过如下方式:
将 sourceMap 生成的 .map 文件设置公司内网访问, 降低源码安全风险
在发布代码到 CDN 的时候, 将 .map 文件存储到公司内网下
这时我们已经拥有了 .map 文件, 后续要做的就是通过捕获到的 lineno,colno,url 调用 mozilla/source-map 库进行源码映射, 即可拿到真实的源码错误信息.
性能
- Windows.addEventListener('error', function (e) {
- var target = e.target || e.srcElement;
- if (target instanceof HTMLScriptElement) {
- // 上报 [资源错误] 事件
- }
- }, true)
- 1. XmlHttpRequest
- var xhr = Windows.XMLHttpRequest;
- var _open = xhr.prototype.open;
- var _send = xhr.prototype.send;
- var attr = {};
- var openReplacement = function (method, url) {
- // 可以存储 method,url, 时间打点等信息
- attr.duration = new Date().getTime();
- _open.apply(this, arguments);
- }
- var sendReplacement = function () {
- methods.addEvent(this, 'readystatechange', function (attr) {
- // 可以存储 response 的 status, 计算客户端实际响应时间
- attr.status = this.status;
- attr.duration = new Date().getTime() - attr.duration;
- // 上报[API] 事件
- }.bind(this, , JSON.parse(JSON.stringify(attr))));
- _send.apply(this, arguments);
- }
- xmlhttp.prototype.open = openReplacement;
- xmlhttp.prototype.send = sendReplacement;
- 2. Fetch
- var _fetch = Windows.fetch;
- Windows.fetch = function () {
- var attr = {
- method: arguments[1].method,
- url: arguments[0],
- duration: new Date().getTime()
- };
- return _fetch.apply(this, arguments).then(res => {
- attr.status = res.status;
- attr.duration = new Date().getTime() - attr.duration;
- // 上报[API] 事件
- return res;
- });
- }
- Windows.navigator.sendBeacon('上报事件的 api', '数据参数')
- 2. img.src
- var img = new Image();
- img.src = API + '?' + '数据参数'
来源: https://segmentfault.com/a/1190000018918875