document.ready_document.ready_document.ready

目录

一、背景

二、监控类型

1.性能监控

2.异常告警监控

3.日常巡检监控

三、监控知识梳理

1.前端监控的必要性

2.前端监控目标

2.1保证稳定性(错误监控)

2.2提升用户体验(性能监控)

2.3针对业务进行统计

3.前端监控的流程

3.1前端埋点方案

3.2监控脚本

四、结语

背景

得物的服务端监控是比较全面和有效的,除了上报原始日志数据,还通过数据分析制定线上告警机制,调用链路分析,而针对前端项目这一块,还是不够全面的。对前端线上问题感应不及时,靠人肉发现,没有告警机制等问题,所以就有个前端监控这个项目。前端监控也确实很有必要,我们需要对线上的页面有个全面的把控,而至于怎么做监控,做数据上报,以及数据分析,如何针对监控数据分析出有用的核心链路的告警等也能有个全面的认识。本文主要是介绍得物针对监控做了哪些事情以及对前端底层监控手段做个总结。

监控类型

前端监控的范围很广,如监控性能,监控异常,监控告警等一系列的维度来确保我们的页面和功能是正常的,在出现问题时研发可以及时做出响应,及时追踪,定位问题。

性能监控

目前前端平台在性能监控和异常监控是分开的,性能监控有专门 SDK,去做上报和分析,联合得物 App 端上一起做了性能优化和数据分析,这块是独立的。主要包括性能的上报,以及性能的优化手段。下图是前端性能监控方面的数据展示效果。

总体来说性能监控是做的比较大的,而且监控对提高页面的秒开是有实际优化指导意义的,离线包、预请求等都是优化手段。

异常告警监控

针对异常监控也有单独的平台和 SDK 去承接和上报,下图来展示收集一些异常信息,对异常信息进行分类,再通过设置告警机制来通知开发人员及时发现问题:

document.ready_document.ready_document.ready

这些错误类的信息不一定是我们都需要关注的,有些疑难杂症,但是有不影响页面展示和功能的报错,也是可以忽略的,要知道不是所有的错误都能被解决的,这个时候我们可以只关注那些影响我们页面核心功能的部分,针对这部分做一个告警配置,例如:

document.ready_document.ready_document.ready

针对告警信息,再进一步对错误进行分析,找到能解决的问题,达到对页面稳定性的把控。

日常巡检监控

还有一种比较特殊的场景,针对运营活动做的营销会场,在各个配置的坑位去做巡检,提前发现会场是否正常,有没有白屏、API 异常等提前发现,然后联系相应的人去人工处理。这种监控模式对于要投放出去的页面做提前检查是很有效果的,事实证明也确实如此,提前避免了很多线上问题,很厉害。

document.ready_document.ready_document.ready

监控知识梳理

上面几个大类型的监控都是前端同学在监控方面做的努力和成果,涉及到的知识点也很多,同样对于业务的把控和理解也很深入,很值得学习并了解,后面的内容就是针对监控做个梳理总结,了解下大致的实现方式。

前端监控的必要性

用户在访问页面的时候大致会经历下面的阶段:

这些阶段都有报错的可能,而前端要做的就是监控后面这阶段:资源加载和页面交互。

做前端监控也有很多其他好处, 例如:

前端监控是很有必要的,通过监控,我们能在线上应用异常时,第一时间收到反馈,并及时止损。对业务的发展是有正向作用的。

前端监控目标

保证稳定性(错误监控)

错误监控包括 JavaScript 代码错误、Promsie 错误、接口(XHR,Fetch)错误、资源加载错误(Script,Link等)等,这些错误大多会导致页面功能异常甚至白屏。

提升用户体验(性能监控)

性能监控包括页面的加载时间、接口响应时间等,侧面反应了用户体验的好坏。

针对业务进行统计

前端监控的流程

前端埋点方案

代码埋点

代码埋点,就是项目中引入埋点 SDK,手动在业务代码中标记,触发埋点事件进行上报。比如页面中的某一个模块的点击事件,会在点击事件的监听中加入触发埋点的代码this.$track(‘事件名’, { 需要上传的业务数据 }),将数据上报到服务器端。

这个方案也是我们实际项目中现有的方案。

可视化埋点

通过可视化交互的手段,代替代码埋点,可以新建、编辑、修改埋点。在组件和页面的维度进行埋点的设计。

将业务代码和埋点代码分离,提供一个可视化交互的页面,输入为业务代码,通过这个可视化系统,可以在业务代码中自定义的增加埋点事件,最后输出的代码耦合了业务代码和埋点代码。

这个方案是可以解决第一种代码埋点的痛点,也是我们目前正准备做的方案。

无痕埋点

前端的任意一个事件都被绑定一个标识,所有的事件都被记录下来,通过定期上传记录文件,配合文件解析,解析出来我们想要的数据,并生成可视化报告。

无痕埋点的优点是采集全量数据,不会出现漏埋和误埋等现象。缺点是给数据传输和服务器增加压力,也无法灵活定制数据结构。针对业务数据的准确性不高。

监控脚本

日志存储

前端的埋点上报需要存储起来,这个可以使用阿里云的日志服务,不需要投入开发就可以采集。

新建一个项目比如:xxx-monitor

新建一个存储日志,根据阿里云的要求发起请求,携带需要上报的数据:

${project}.${host}/logstores/${logStore}/track

document.ready_document.ready_document.ready

代码中调用 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()

这里展示的是自定义要上报的数据字段:

document.ready_document.ready_document.ready

监控错误

前端需要监控的错误有两类:

脚本实现

新建一个 fronend-monitor 项目,这个项目就相当于我们的工程项目,监控的核心实现可以写到项目里面,也可以抽成 SDK 的形式 Import 引入进来,这里先写到项目中。

webpack.config.js 用来打包项目,做接口数据 Mock,测试 XHR 请求监控接口错误等。

const path = require('path')const HtmlWebpackPlugin = xxx
module.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      monitor              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') }

上报未捕获的 Javascript 错误

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+ats+(.+):(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。

统计pv (页面的停留时间)

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);}

总结

前端监控是一个成熟业务线的标配,目前最多的场景是监控 JS 错误,接口请求和性能优化,然后根据日志信息进行分析分类的可视化展示,在发生异常的时候通知到相应的业务开发,监控的性能指标给页面的体验优化提供数据对比和优化的方向。

参考文章:

往期回顾

*文/Zyqy

关注得物技术,每周一、三、五更新技术干货

要是觉得文章对你有帮助的话,欢迎评论转发点赞~

限 时 特 惠: 本站每日持续更新海量各大内部创业教程,一年会员只需98元,全站资源免费下载 点击查看详情
站 长 微 信: lzxmw777

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注