前端性能监控-从0开始

前端性能监控

1.背景

  • 用户量日渐庞大,用户对体验的优化迫在眉睫
  • 性能提升信任度,糟糕的性能会严重体验用户体验,大量的研究表明性能和商业收益成正比
  • 用户反馈已晚矣,用户发现问题到反馈的流程较长,相当于亡羊补牢,那么,前端监控,呼之欲出。
  • 前端监控未雨绸缪,提前告警,精准定位,协助排查,白屏感知,接口异常预警

2.为什么要做前端监控

  • 更快的发现问题和解决问题
  • 做产品的决策依据
  • 为业务扩展提供更多的可能性
  • 根据指标优化产品

3.监控三座大山

1.稳定性(报错相关)

  • js错误:js执行错误,promise异常
  • 资源异常:script,link,img,css等资源的加载异常
  • 接口错误:ajax,fetch请求接口异常
  • 白屏: 页面空白

2.用户体验(性能相关)

  • 加载时间 各个阶段的加载时间
  • TTFB(time to first byte 首字节时间) 是指浏览器发起第一个请求到数据返回第一个字节所消耗时间
  • FP(First Paint 首次绘制时间) 首次渲染的时间,是第一个像素点绘制到屏幕的时间
  • FCP(First Contentful Paint 首次内容绘制时间) 首次有内容绘制渲染的时间,指浏览器将第一个dom渲染到屏幕的时间
  • FMP(First Meaningful paint 首次有意义绘制) 首次有意义绘制时间
  • FID(First Input Delay 首次输入延迟) 用户首次和页面交互到页面响应交互的时间

3.业务埋点(业务相关)

  • PV page view 即页面浏览量和点击量
  • UV 指访某个站点的不同ip地址的人数
  • 页面的停留时间 用户在每一个页面的停留时间
  • 自定义埋点,用户点击某个按钮或者事件的业务统计口径
稳定性
性能相关
业务相关

4.监控流程设计

  • 前端埋点 --> 数据采集 --> 数据建模和存储 --> 数据传输(实时/批量) --> 数据统计(分析) --> 数据可视化 --> 报告和报警(短信)
前端埋点
数据采集
数据存储
统计分析
可视化
  • 代码埋点基本类型
    • 代码埋点:通过在代码里插入指定代码来存储保存信息,缺点是,需要单个去埋点,优点是灵活
    • 可视化埋点:通过可视化交互的手段代替代码埋点,将业务代码和埋点代码分离,提供一个可视化交互的页面
    • 无痕化埋点:绑定所有事件,通过定期上传等手段

5.数据结构设计和工具类编写

1.数据结构设计

{
     kind: 'xxx', //大类
     type: 'xxx', //小类
     errorType: 'xxx', //js执行错误,如:jsError
     url: '', //访问路径报
     message: event.message,  //报错的信息
     filename: event.filename, //文件报错
     position: `${event.lineno}:${event.colno}`,  //报错的行和列
     stack: getStack(event.error.stack), //报错堆栈,能知道报错前调用了谁
     selector: selector //最后一个操作的元素,报错之前操作的最近的一个元素是谁
}

2.工具-存储最后操作的dom

  • 我们需要改写下click事件和key和keydown事件,将上一次触发的元素存储起来
  • 冒泡时间不可控,且容易被阻止冒泡,选用捕获事件,在捕获阶段存储事件
  • 利用闭包特性,将值存储起来
const getLastEvent = (function () {
        let lastEvent // 默认声明
        ['click', 'keydown'].forEach(eventType => {
        	// 将点击事件和键盘事件,捕获阶段进行操作
            document.addEventListener(eventType, (e) => {
                lastEvent = event
            }, {
                passive: true,
                capture: true
            })
        })
        // 利用闭包,将lastEvent存储起来。方便拿到下一次
        function getLastEvent() {
            return lastEvent
        }
        return getLastEvent
    })()

3.工具-提取堆栈信息

  • 去除换行符
  • 截取1到末尾字符
  • map映射,将每一项映射
  • 分割成字符串
function getStack(stack) {
        return stack
       			?.split('\n')
	       			?.slice(1)
		       			?.map(item => item.replace(/^\s+at\s+/g, ''))
			       			?.join("^")
 }

4.工具-dom中提取css

function getSelector(path) {
        if (Array.isArray(path)) {
            return path.reverse().filter(ele => {
                return ele !== document && ele !== window
            }).map(ele => {
                let selector = ''
                if (ele.id) {
                    return `${ele.nodeName.toLowerCase()}#${ele.id}`
                } else if (ele.className && typeof ele.className === 'string') {
                    return `${ele.nodeName.toLowerCase()}.${ele.className}`
                } else {
                    selector = ele.nodeName.toLowerCase()
                }
                return selector
            }).join(' ')
        }
    }

5.埋点或自动上报异常函数封装

  • 通过axios 进行相关的上报
import axios from 'axios'
	function report(params){
		console.log(params,'params')
		const instance = axios.create({
		  baseURL: 'https://some-domain.com/api/',
		  timeout: 1000,
		  headers: {'X-Custom-Header': 'foobar'},
		  body:{params}
		});	
		try{
			instance()
		}catch{
		}
	}

6.前端异常上报

1.监听全局js错误和资源异常

  • 对于全局的js异常和和资源加载等异常,需要指定参数为true,useCapture参数解析
    • useCapture为true时,优先级大于fasle,大于处于他之后声明的true
    • useCaptrue为false,优先级小于true,大于在他之前的false
	window.addEventListener('error', function (e) {
		let lastEvent = getLastEvent() //拿到最后一个交互事件
        let selector = lastEvent && getSelector(lastEvent.path) // 获得元素的层级,html>div>span
        if (event.target && (event.target.src || event.target.href)) {
                //说明这是一个脚本错误
                const params = {
                    kind: 'stability', //大类
                    type: 'error', //小类
                    errorType: 'resourceError', //js或css加载错误
                    filename: event.target.src || event.target.href, 
                    tagName: event.target.tagName,
                    selector: selector
                }
                report(params) //捕获到的错误,需要上报的数据
                return
       }
		const params = {
                kind: 'stability', //大类
                type: 'error', //小类
                errorType: 'jsError', //js执行错误
                url: '', //访问哪个路径报错了
                message: event.message,
                filename: event.filename,
                position: `${event.lineno}:${event.colno}`,
                stack: getStack(event.error.stack), 
                selector: selector 
        }
        //捕获到的错误,需要上报的数据
        report(params)
	},true)

2.监听未捕获的promise异常unhandledrejection

  • 由于promise的特殊性,未被catch的promise的都会触发unhandledrejection的异常事件
  • unhandledrejection为promise专属的
  • 必须忽略axiosError等相关的报错
	window.addEventListener('unhandledrejection', function (e) {
		let lastEvent = getLastEvent()
            let selector = lastEvent && getSelector(lastEvent.path)
            let message = ''
            let filename = ''
            let line = 0
            let column = 0
            let stack = ''
            let reason = event.reason
            if(reason?.name === 'AxiosError'){
                return // 对于axios的promise异常不上报
            }
            // promise抛出的异常结果如果为字符串或者数字,直接吐出去即可
            if (typeof reason === 'string' || typeof reason === 'number') {
                message = reason
            } else if (typeof reason === 'object') {
           		// 对于对象的解析处理
                if (reason.stack) {
                    let matchResult = reason.stack.match(/at\s+(.+):(\d+):(\d+)/)
                    filename = matchResult[1]
                    line = matchResult[2]
                    column = matchResult[3]
                }
                message = reason.message
                stack = getStack(reason.stack)
            }
            const params = {
                kind: 'stability', //大类
                type: 'error', //小类
                errorType: 'promiseError', //promise错误
                message,
                filename, 
                position: `${line}:${column}`,
                stack, 
                selector: selector
            }
            //捕获到的错误,需要上报的数据
        	report(params)
	})

3.监听xhr接口异常

  • axios和jQuery底层也是基于xhr的垫片魔改
  • 我们也基于xhr将监控做在里面
(function () {
            let XMLHttpRequest = window.XMLHttpRequest
            let oldOpen = XMLHttpRequest.prototype.open
            XMLHttpRequest.prototype.open = function (method, url, async) {
                if (!url.match(new RegExp(`${reportUrl}`))) {
                    this.logData = {
                        method,
                        url,
                        async
                    }
                }
                return oldOpen.apply(this, arguments)
            }
            let oldSend = XMLHttpRequest.prototype.send
            XMLHttpRequest.prototype.send = function (body) {
                if (this.logData) {
                    let startTime = Date.now() //请求发送之前记录开始时间

                    //请求响应后的处理函数
                    const handler = (type) => (event) => {
                        //请求持续的时间 
                        let duration = Date.now() - startTime
                        let status = this.status
                        let statusText = this.statusText
                        const params = {
                            kind: 'stability', //大类
                            type: 'xhr', //小类
                            errorType: type, //xhr错误类型
                            pathname: this.logData.url, //请求路径
                            status: status + '-' + statusText, //状态码
                            duration, //持续时间
                            response: this.response ? JSON.stringify(this.response) : '', //响应体
                            params: body || '' //参数
                        }
                        //上报数据
                        //捕获到的错误,需要上报的数据
        				report(params)
                    }
                    this.addEventListener('load', handler('load'), false)
                    this.addEventListener('error', handler('error'), false)
                    this.addEventListener('abort', handler('abort'), false)
                }
                return oldSend.apply(this, arguments)
            }
 })();

4.监听fetch接口异常

  • 包裹改写fetch
  • 针对特定url过滤,防止无限请求
(function () {
      const oldFetch = window.fetch;
      let startTime=0
      window.fetch = function (url, params) {
          //请求响应后的处理函数
          const handler = (type,res) => {
              //请求持续的时间 
              let duration = Date.now() - startTime
              let status = res.status
              let statusText = res.statusText
              const log = {
                  kind: 'stability', //大类
                  type: 'xhr', //小类
                  errorType: type, //xhr错误类型
                  pathname: url, //请求路径
                  status: status + '-' + statusText, //状态码
                  duration, //持续时间
                  response: res.response ? JSON.stringify(res.response) : '', //响应体
                  params: params || '' //参数
              }
              if (url.match(new RegExp(`${reportUrl}`))) {
                  return
              }
              //上报数据
              report(log)
          } 
          startTime = Date.now() //请求发送之前记录开始时间
          return oldFetch(url, params).then(res => {
              if(!res?.ok || status !== 200){
                  handler(res?.type || 'error',res)
              }
              return res
          }).catch(res => handler('error',res))
      }

        })();
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

家雀安知鸿鹄志

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值