🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
[TOC] # 收集用户信息 ## navigator API * 当前的网络状态 * 运营商 * 地理位置 * 访问时间 * 客户端的版本(如果是通过客户端访问) * 系统版本 * 浏览器信息 * 设备分辨率 * 页面的来源 * 用户账号信息 * 页面访问流程各阶段耗时 * js代码报错信息 * 用户的浏览器语言编码 * 浏览器语言设置 * flash的版本 * 网页的标题 * 网页的来源 * cookie的数据 <br> # 各阶段访问耗时记录 ## performance timing [performance API](https://developer.mozilla.org/en-US/docs/Web/API/Performance) * DNS 查询耗时 :domainLookupEnd - domainLookupStart * TCP 链接耗时 :connectEnd - connectStart * request 请求耗时 :responseEnd - responseStart * 解析 dom 树耗时 : domComplete - domInteractive * 白屏时间 :responseStart - navigationStart * domready 时间 :domContentLoadedEventEnd - navigationStart * onload 时间 :loadEventEnd – navigationStart 具体参见[W3C Resource timing](https://www.w3.org/TR/resource-timing/),[Performance.getEntries()](https://developer.mozilla.org/zh-CN/docs/Web/API/Performance/getEntries) ## performance getEntries 通过performance.getEntries(),可以得到一个数组,其中的每个元素分别代表着一个资源,元素对象包括的属性跟上面的performance timing有点接近,还有不同的属性包括name代表资源的地址,请求花费的时间duration。 <br> ## 其他方法 记录访问开始的时间可有以下的方法: * 服务器将访问的时间渲染到页面上 * SPA的话,记录前一个页面卸载的时间 记录访问过程的时间 * 在head标签解析后,渲染body标签前加入script标签进行打点,一般将这个时间视为白屏时间 * 捕获DOMContentLoaded事件来记录dom元素加载完毕的时间 * 在首屏页面的所有图片加载完后进行记录,保存首屏时间 * 捕获load事件记录页面加载完成的时间 <br> <br> # 错误采集 ## window.onerror window.onerror可以捕捉运行时错误,可以拿到出错的信息,堆栈,出错的文件、行号、列号 ~~~ window.onerror = function(msg, url, row, col, error){ report({ msg, // 错误信息 url, // 发生错误对应的脚本链接 row, // 行号 col // 列号 }) } ~~~ > 注: onerror兼容性有问题,ie10 & safari6 才完整支持以上全部参数。 > 要注意以下几点: * 要把 `window.onerror` 这个代码块分离出去,并且比其他脚本先执行(注意这个前提!)即可捕捉到语法错误。 * 由于网络请求异常事件不会冒泡,需要在**捕获**阶段进行处理 * 不能捕获 **promise** 的错误信息 * 跨域资源需要专门处理,需要在script标签加上 **crossorigin** 属性,服务器设置 `Access-Control-Allow-Origin` * window.onerror 函数只有在返回 true 的时候,异常才不会向上抛出,否则即使是知道异常的发生控制台还是会显示 Uncaught Error: xxxxx。 <br> ## 跨域资源处理 Script error 是浏览器在同源策略限制下产生的,浏览器处于对安全性上的考虑,当页面引用非同域名外部脚本文件时中抛出异常的话,此时本页面是没有权利知道这个报错信息的,取而代之的是输出 Script error 这样的信息。 首先为页面上的 script 标签添加 crossOrigin 属性 ~~~html <script src="http://localhost:8081/test.js" crossorigin></script> ~~~ 其次,后端在响应头里加上 ~~~ Access-Control-Allow-Origin: * ~~~ <br> ## promise的错误处理 通过 Promise 可以帮助我们解决异步回调地狱的问题,但是一旦 Promise 实例抛出异常而你没有用 catch 去捕获的话,onerror 或 try-catch 也无能为力,无法捕捉到错误。 ~~~js window.addEventListener('error', (msg, url, row, col, error) => { console.log('我感知不到 promise 错误'); console.log( msg, url, row, col, error ); }, true); Promise.reject('promise error'); new Promise((resolve, reject) => { reject('promise error'); }); new Promise((resolve) => { resolve(); }).then(() => { throw 'promise error' }); ~~~ 所以如果你的应用用到很多的 Promise 实例的话,特别是你在一些基于 promise 的异步库比如 axios 等一定要小心,因为你不知道什么时候这些异步请求会抛出异常而你并没有处理它,所以你最好添加一个 Promise 全局异常捕获事件 `unhandledrejection`。 ~~~ window.addEventListener("unhandledrejection", function(e){ // Event新增属性 // @prop {Promise} promise - 状态为rejected的Promise实例 // @prop {String|Object} reason - 异常信息或rejected的内容 // 会阻止异常继续抛出,不让Uncaught(in promise) Error产生 e.preventDefault() }) ~~~ <br> ## try catch 无法捕捉到语法错误,只能捕捉运行时错误; 可以拿到出错的信息,堆栈,出错的文件、行号、列号; 需要借助工具把所有的function块以及文件块加入try,catch,可以在这个阶段打入更多的静态信息。 要注意的是try catch只能捕获同步代码的异常,对回调,setTimeout,promise等无能为力 <br> ## 网络异常错误 由于网络请求异常不会事件冒泡,因此必须在捕获阶段将其捕捉到才行,但是这种方式虽然可以捕捉到网络请求的异常,但是无法判断 HTTP 的状态是 404 还是其他比如 500 等等,所以还需要配合服务端日志才进行排查分析才可以。 ~~~ <img src="./404.png" alt=""> <script> window.addEventListener('error', (msg, url, row, col, error) => { console.log('我知道 404 错误了'); console.log( msg, url, row, col, error ); return true; }, true); </script> ~~~ <br> <br> # 上报错误 * 后端提供接口,前端ajax上传 * 创建一个新的图片,url参数带上错误信息 使用创建图片的好处是简单方便可跨域,可以防止重复请求,但要注意到地址栏可以携带的信息有限。 ~~~ function report(error) { var reportUrl = 'http://xxxx/report'; new Image().src = reportUrl + 'error=' + error; } ~~~ ### 页面加载时间统计 ~~~ window.logInfo = {}; window.logInfo.openTime = performance.timing.navigationStart; // 白屏时间 window.logInfo.whiteScreenTime = +new Date() - window.logInfo.openTime; document.addEventListener('DOMContentLoaded', function (event) { window.logInfo.readyTime = +new Date() - window.logInfo.openTime; }); window.onload = function () { window.logInfo.allloadTime = +new Date() - window.logInfo.openTime; window.logInfo.nowTime = new Date().getTime(); var timname = { whiteScreenTime: '白屏时间', readyTime: '用户可操作时间', allloadTime: '总下载时间', mobile: '使用设备', nowTime: '时间', }; var logStr = ''; for (var i in timname) { console.warn(timname[i] + ':' + window.logInfo[i] + 'ms'); if (i === 'mobile') { logStr += '&' + i + '=' + window.logInfo[i]; } else { logStr += '&' + i + '=' + window.logInfo[i]; } } (new Image()).src = '/action?' + logStr; }; ~~~ <br> ### 统计用户使用设备 ~~~ window.logInfo.mobile = mobileType(); function mobileType() { var u = navigator.userAgent, app = navigator.appVersion; var type = {// 移动终端浏览器版本信息 ios: !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/), //ios终端 iPad: u.indexOf('iPad') > -1, //是否iPad android: u.indexOf('Android') > -1 || u.indexOf('Linux') > -1, //android终端或者uc浏览器 iPhone: u.indexOf('iPhone') > -1 || u.indexOf('Mac') > -1, //是否为iPhone或者QQHD浏览器 trident: u.indexOf('Trident') > -1, //IE内核 presto: u.indexOf('Presto') > -1, //opera内核 webKit: u.indexOf('AppleWebKit') > -1, //苹果、谷歌内核 gecko: u.indexOf('Gecko') > -1 && u.indexOf('KHTML') == -1, //火狐内核 mobile: !!u.match(/AppleWebKit.*Mobile/i) || !!u.match(/MIDP|SymbianOS|NOKIA|SAMSUNG|LG|NEC|TCL|Alcatel|BIRD|DBTEL|Dopod|PHILIPS|HAIER|LENOVO|MOT-|Nokia|SonyEricsson|SIE-|Amoi|ZTE/), //是否为移动终端 webApp: u.indexOf('Safari') == -1 //是否web应该程序,没有头部与底部 }; var lists = Object.keys(type); for(var i = 0; i < lists.length; i++) { if(type[lists[i]]) { return lists[i]; } } } ~~~ <br> ### 错误量的统计 ~~~ var defaults = { msg: '', // 错误的具体信息 url: '', // 错误所在的url line: '', // 错误所在的行 col: '', // 错误所在的列 nowTime: '',// 时间 }; window.onerror = function (msg, url, line, col, error) { col = col || (window.event && window.event.errorCharacter) || 0; defaults.url = url; defaults.line = line; defaults.col = col; defaults.nowTime = new Date().getTime(); if (error && error.stack) { // 如果浏览器有堆栈信息,直接使用 defaults.msg = error.stack.toString(); } else if (arguments.callee) { // 尝试通过callee拿堆栈信息 var ext = []; var fn = arguments.callee.caller; var floor = 3; while (fn && (--floor > 0)) { ext.push(fn.toString()); if (fn === fn.caller) { break; } fn = fn.caller; } ext = ext.join(","); defaults.msg = error.stack.toString(); } var str = '' for (var i in defaults) { // console.log(i,defaults[i]); if (defaults[i] === null || defaults[i] === undefined) { defaults[i] = 'null'; } str += '&' + i + '=' + defaults[i].toString(); } srt = str.replace('&', '').replace('\n', '').replace(/\s/g, ''); (new Image()).src = '/error?' + srt; } ~~~ ### 数据处理、数据展示 # 工具、平台 ## [Fundebug](https://www.fundebug.com/) ## [AlloyLever](https://github.com/AlloyTeam/AlloyLever) <br> # 参考资料 [搭建一个前端监控系统,不再错过BUG - 掘金](https://juejin.im/post/5a372716518825258a5fbc80) [前端错误监控与收集探究 | hpoenixf's blog](http://hpoenixf.com/前端错误监控与收集探究.html) https://juejin.im/post/5b35921af265da598f1563cf https://juejin.im/post/5aaa93345188257bf550cbfd https://juejin.im/post/5b5dcfb46fb9a04f8f37afbb [前端代码异常监控实战](https://zhuanlan.zhihu.com/p/31979395) https://zhuanlan.zhihu.com/p/27305665