前端监控的范围很广,如监控性能,监控异常,监控告警等一系列的维度来确保我们的页面和功能是正常的,在出现问题时研发可以及时做出响应,及时追踪,定位问题。
图片
总体来说性能监控是做的比较大的,而且监控对提高页面的秒开是有实际优化指导意义的,离线包、预请求等都是优化手段。
图片
这些错误类的信息不一定是我们都需要关注的,有些疑难杂症,但是有不影响页面展示和功能的报错,也是可以忽略的,要知道不是所有的错误都能被解决的,这个时候我们可以只关注那些影响我们页面核心功能的部分,针对这部分做一个告警配置,例如:
图片
针对告警信息,再进一步对错误进行分析,找到能解决的问题,达到对页面稳定性的把控。
图片
资源加载成功之后页面渲染继续运行。
这些阶段都有报错的可能,而前端要做的就是监控后面这阶段:资源加载和页面交互。
做前端监控也有很多其他好处, 例如:
前端监控是很有必要的,通过监控,我们能在线上应用异常时,第一时间收到反馈,并及时止损。对业务的发展是有正向作用的。
错误监控包括 JavaScript 代码错误、Promsie 错误、接口(XHR,Fetch)错误、资源加载错误(Script,Link等)等,这些错误大多会导致页面功能异常甚至白屏。
性能监控包括页面的加载时间、接口响应时间等,侧面反应了用户体验的好坏。
代码埋点,就是项目中引入埋点 SDK,手动在业务代码中标记,触发埋点事件进行上报。比如页面中的某一个模块的点击事件,会在点击事件的监听中加入触发埋点的代码 this.$track('事件名', { 需要上传的业务数据 }),将数据上报到服务器端。
这个方案也是我们实际项目中现有的方案。
通过可视化交互的手段,代替代码埋点,可以新建、编辑、修改埋点。在组件和页面的维度进行埋点的设计。
将业务代码和埋点代码分离,提供一个可视化交互的页面,输入为业务代码,通过这个可视化系统,可以在业务代码中自定义的增加埋点事件,最后输出的代码耦合了业务代码和埋点代码。
这个方案是可以解决第一种代码埋点的痛点,也是我们目前正准备做的方案。
前端的任意一个事件都被绑定一个标识,所有的事件都被记录下来,通过定期上传记录文件,配合文件解析,解析出来我们想要的数据,并生成可视化报告。
无痕埋点的优点是采集全量数据,不会出现漏埋和误埋等现象。缺点是给数据传输和服务器增加压力,也无法灵活定制数据结构。针对业务数据的准确性不高。
前端的埋点上报需要存储起来,这个可以使用阿里云的日志服务,不需要投入开发就可以采集。
新建一个项目比如:xxx-monitor
新建一个存储日志,根据阿里云的要求发起请求,携带需要上报的数据:
http://${project}.${host}/logstores/${logStore}/track
图片
代码中调用 Track 上报日志:
日志的上报可以封装成公共的调用方式, monitor/utils/里面放所有的工具方法;
tracker.js 的实现就是按照阿里云的上报格式发送请求,并带上处理好的需要上报的业务数据即可,下面的都是固定的,在日志服务建好:
图片
实现一个 Tracker 类导出类的实例即可,这样在监控的核心代码中直接调用 tracker.send(data),核心实现代码如下:
// monitor/utils/get/track.js...class SendTrackLoger { constructor() { this.url = `http://${project}.${host}/logstores/${logStore}/track` this.xhr = new XMLHttpRequest() } send(data = {}, callback) { const logData = {...logData} for(let key in logs) { if (typeof logs[key] === 'number') { logs[key] = `${logs[key]}` // 这是阿里云的要求,字段不能是数字类型 } } let body = JSON.stringify({ __logs__: [logs] }) this.xhr.open('POST', this.url, true) this.xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8') this.xhr.setRequestHeader('x-log-apiversion', '0.6.0') this.xhr.setRequestHeader('x-log-bodyrawsize', body.length) this.xhr.onload = function() { if (this.status >= 200 && this.status <= 300 || this.status === 304) { callback && callback() } } this.xhr.onerror = function(error) { console.log(error) } this.xhr.send(body) }}export default new SendTrackLoger()
这里展示的是自定义要上报的数据字段:
图片
前端需要监控的错误有两类:
新建一个 fronend-monitor 项目,这个项目就相当于我们的工程项目,监控的核心实现可以写到项目里面,也可以抽成 SDK 的形式 Import 引入进来,这里先写到项目中。
webpack.config.js 用来打包项目,做接口数据 Mock,测试 XHR 请求监控接口错误等。
const path = require('path')const HtmlWebpackPlugin = xxxmodule.exports = { mode: 'development', context: process.cwd(), entry:'./src/index.js', output: { path: path.resolve(__dirname, 'dist'), filename: 'monitor.js' }, devServer: { contentBase: path.resolve(__dirname, 'dist'), before(router) { router.get('/success', function(req, res) { res.json({ id: 1 }) }) router.post('/error', function(req, res) { res.sendStatus(500) }) }, }, module: {}, plugins: [ new HtmlWebpackPlugin({ template: './src/index.html', inject: "head" }) ]}
新建一个 src/index.html 在这个里面写一些问题代码,然后测试监控的错误捕获。
// src/index.html<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" cnotallow="width=device-width, initial-scale=1.0"> <title>monitor</title></head><body> <input id="jsErrorBtn" type="button" value="js 代码错误" notallow="btnClick()" /> <input id="promiseErrorBtn" type="button" value="promise 错误" notallow="promiseClick()" /> <input id="successBtn" type="button" value="成功 ajax 请求" notallow="successAjax()" /> <input id="errorBtn" type="button" value="失败 ajax 请求" notallow="errorAjax()" /> <script> function btnClick() { window.goods.type = 2 } function promiseClick() { new Promise((resolve, reject) => { resolve(1) }, () => { console.log(123) }) } function successAjax() { var xhr = new XMLHttpRequest() xhr.open('GET', '/success', true) xhr.responseType = 'json' xhr.onload = function () { console.log(xhr.response) } xhr.send() } function errorAjax() { var xhr = new XMLHttpRequest() xhr.open('POST', '/error', true) xhr.responseType = 'json' xhr.onload = function() { console.log(xhr.response) } xhr.onerror = function(err) { console.log(err) } xhr.send('name=123') }</script></body></html>
Javascript 错误分为 2 种:语法错误,资源家加载错误,这些错误都会被 window.addEventListener('error', function(event) {})捕获,来判断是否是资源加载错误。
window.addEventListener('error', function(event) { // 如果 target 是script link 等资源 if (event.target && (event.target.src || event.target.href)) { const element = getElement(event.target || event.path) tracker.send({ ... title: document.title, url: location.href, timestamp: event.timeStamp, userAgent: navigator.userAgent, type: 'resourceError', ... }) } else { tracker.send({ ... title: document.title, url: location.href, timestamp: event.timeStamp, userAgent: navigator.userAgent, type: 'jsError', ... }) } }, true)
代码中未被捕获的 Promise 错误,要监听 unhandledrejection 事件 window.addEventListener('unhandledrejection', function(event) {})。
// 监听未捕获的 promise 错误 window.addEventListener('unhandledrejection', function(event) { // PromiseRejectionEvent let message = '' let stack = '' const reason = event.reason let filename = '' let lineno = '' let colno = '' if (reason) { message = reason.message stack = reason.stack const match = stack.match(//s+at/s+(.+):(/d+):(/d+).+/) filename = match[1] lineno = match[2] colno = match[3] } tracker.send({ ... title: document.title, url: location.href, timestamp: event.timeStamp, userAgent: navigator.userAgent, type: 'promiseError', ... }) }, true)
接口异常上报主要是拦截请求,拦截 XMLHttpRequest 对象,改写 XHR 的 Open 和 Send 方法,将需要上报的数据发到阿里云存储,监听 Load,Error,Abort 事件,上报数据:
// src/monitor/lib/xhr.jsimport tracker from '../utils/tracker'export default function injectXHR() { // 获取 window 上的 XMLHttpRequest 对象 const XMLHttpRequest = window.XMLHttpRequest // 保存旧的open, send函数 const prevOpen = XMLHttpRequest.prototype.open const prevSend = XMLHttpRequest.prototype.send // 不可使用箭头函数,不然会找不到 this 实例 XMLHttpRequest.prototype.open = function (method, url, async, username, password) { // 重写open,拦截请求 // 不拦截 track 本身以及 socket, 直接放行 if (!url.match(/logstores/) && !url.match(/sockjs/)) { this.logData = { method, url, async, username, password } } return prevOpen.apply(this, arguments) } XMLHttpRequest.prototype.send = function (body) { // 重写 send,拦截有 logData 的请求,获取 body 参数 if (this.logData) { this.logData.body = body let startTime = Date.now() function handler(type) { return function (event) { // event: ProgressEvent let duration = Date.now() - startTime let status = this.status let statusText = this.statusText console.log(event) tracker.send({ type: 'xhr', eventType: type, pathname: this.logData.url, status: `${status} ${statusText}`, duration: `${duration}`, // 接口响应时长 response: this.response ? JSON.stringify(this.response) : '', params: body || '', }) } } this.addEventListener('load', handler('load'), false) this.addEventListener('error', handler('error'), false) this.addEventListener('abort', handler('abort'), false) } return prevSend.apply(this, arguments) }}
白屏就是页面上什么东西也没有,在页面加载完成之后,如果页面上的空白点很多,就说明页面是白屏的,需要上报,这个上报的时机是:document.readyState === 'complete' 表示文档和所有的子资源已完成加载,表示load(window.addEventListener('load')状态事件即将被触发。
document.readyState 有三个值:loading(document正在加载),interactive(可交互,表示正在加载的状态结束,但是图像,样式和框架之类的子资源仍在加载),complete 就是完成,所以监控白屏需要在文档都加载完成的情况下触发。
// src/monitor/utils/onload.jsexport function onload(callback) { if (document.readyState === 'complete') { callback() } else { window.addEventListener('onload', callback) }}
监控白屏的思路主要是:可以将可视区域中心点作为坐标轴的中心,在X、Y轴上各分 10 个点,找出这个 20 个坐标点上最上层的 DOM 元素,如过这些元素是包裹元素,空白点数就加一,包裹元素可以自定义比如 Html Body App Root Container Content 等,空白点数大于 0 就上报白屏日志。
export default function computedBlankScreen() { // 包裹玉元素列表 const wrapperSelectors = ['body', 'html', '#root', '#App'] // 空白节点的个数 let emptyPoints = 0 // 判断20个点处的元素是否是包裹元素 function isWrapper(element) { const selector = getSelector(element) console.log(selector) if (wrapperSelectors.indexOf(selector) >= 0) { // 表示是在包裹元素里面,空白点就要加一 emptyPoints++ } } onload(function() { let xElements, yElements for (let i = 0; i <=9; i++) { xElements = document.elementFromPoint(window.innerWidth * i / 10, window.innerHeight / 2) yElements = document.elementFromPoint(window.innerHeight * i / 10, window.innerWidth / 2) // document.elementFromPoint 返回的是某一个坐标点的由到外的html元素的集合 isWrapper(xElements[0]) isWrapper(yElements[0]) } if (emptyPoints >= 0) { let centerPoint = document.elementFromPoint(window.innerWidth / 2, window.innerHeight / 2) tracker.send() } })}
用户交互的响应时间如果大于某一个时间,用户就会感觉卡顿。可以定一个时间比如 100 毫秒,就代表响应时间长,会卡顿。
PerformanceObserver 构造函数使用给定的观察者 Callback 生成新的PerformanceObserver 对象,当通过 Observe() 方法注册条目类型(需要监控的类型)的性能条目被记录下来时,会调用该观察者回调。
所以可以 new PerformanceObserver 来监控 longTask,监控的资源加载如果超过 100 毫秒就表示卡顿,可以浏览器空闲(requestIdleCallback)的时候上报数据。
....export default function longTask() { new PerformanceObserver(function(list) { list.getEntries().forEach(function(entry) { if (entry.duration > 100) { // 浏览器空闲的时候上报 requestIdleCallback(() => { tracker.send({ type: 'longTask', eventType: lastEvent.type, startTime: formatTime(entry.startTime), duration: formatTime(entry.duration), }); }); } }) }).observe({ entryTypes: ['longtask']})}
PerformanceObserver.observe 方法用于观察传入的参数中指定的性能条目类型的集合。当记录一个指定类型的性能条目时,性能监测对象的回调函数将会被调用。performance.timing 记录了从输入 URL 到页面加载完成的所有的时间,从这些字段中可以提取对对页面性能的监控,通过分析这些指标来优化页面的体验,比如统计 FMP、LCP 等,具体可以查看 MDN。
navigator.connection 对象获取网络连接的信息:effectiveType(网络类型),RTT(估算饿往返时间)等,还能通过监听 window.addEventListener('unload')事件计算用户在页面的停留时间。
import tracker from '../util/tracker';export function pv() { var connection = navigator.connection; tracker.send({ type: 'pv', networkType: connection.effectiveType, // 网络类型 rtt: connection.rtt, // 往返时间 screen: `${window.screen.width}x${window.screen.height}` // 设备分辨率 }); let startTime = Date.now(); window.addEventListener('unload', () => { let stayTime = Date.now() - startTime; // 页面停留时间 tracker.send({ type: 'stayTime', stayTime }); }, false);}
本文链接:http://www.28at.com/showinfo-26-17850-0.html一文搞懂得物前端监控
声明:本网页内容旨在传播知识,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。邮件:2376512515@qq.com
上一篇: 想要微信小程序+Uniapp?XBoot开源项目助你快速开发!
下一篇: 十个Python中的数据类型技巧