从0到1构建前端监控系统:错误捕获、性能采集、用户体验全链路追踪实战指南SDK实现

发布于:2025-05-10 ⋅ 阅读:(13) ⋅ 点赞:(0)

目录

前言

        在当今快速发展的Web开发领域中,无论是面试还是与同行交流时,“高大上”的技术术语层出不穷。还记得我第一次了解到前端埋点监控是在一家知名大厂的面试过程中。当面试官问及如何实现前端埋点监控时,我感到一阵茫然——脑海中一片模糊,各种概念交织却难以理清头绪。不出所料,那次面试的第三轮我没有通过。

        这次经历成为我深入学习前端埋点监控的起点。回到家中,我开始查阅大量资料、观看教学视频,逐步摸索出了一套学习路径。本文便是基于这段时间的学习和实践总结而成,旨在为那些刚开始接触前端埋点监控的朋友提供一个详尽的指南。

        文中涵盖了从PV统计、页面加载性能指标(如FMPLCP、FID、FP、FCP)到Promise异常监控、错误监控以及资源加载监控等多方面的内容。为了确保每个细节都能被清晰理解,并且避免将来遗忘关键知识点,我在编写时尽可能地详尽描述了每一步骤和每一个概念。

        无论你是刚踏入前端领域的新人,还是希望进一步提升技能的开发者,相信这篇文章都能为你提供有价值的参考。让我们一起揭开前端埋点监控的神秘面纱,掌握这一提升网站性能和用户体验的重要工具。

        觉得内容有用?别忘了点个赞+收藏+关注,三连走一波!你的支持是我持续输出的动力 💪

gitee仓库地址:地址

为什么要做前端监控

珠峰架构公开课地址【下文根据公开课边学边做边实现,加上自己的个人想法】:珠峰公开课地址

更快发现问题和解决问题。
做产品的决策依据。
提升前端工程师的技术深度和广度。
为业务扩展提供更多可能性。

前端监控目标

稳定性

错误名称 备注
JS错误 JS执行错误或者promise异常
资源异常 scriptlink资源加载异常
接口错误 ajaxfetch接口请求异常
白屏 页面白屏

用户体验

错误名称 备注
加载时间 各个阶段的加载时间
TTFB首字节时间 是指浏览器发起第一个请求到数据返回第一个字节所消耗的时间,这个时间包含了网络请求时间,后端处理时间。
FP首次绘制 首次绘制包括了任何用户自定义的背景绘制,它是将第一个像素点绘制到屏幕的时刻。
FCP首次内容绘制 首次内容绘制是浏览器将第一个DOM渲染到屏幕的时间,可以是任何文本图像和SVG等的时间。
FMP首次有意义绘制 首次有意义绘制是页面可用性的亮度标准。
FID首次输入延迟 用户首次和页面交互到页面响应交互的时间
卡顿 超过50ms的长任务

业务

错误名称 备注
PV page view 即页面浏览量或点击量
UV 指访问某个站点不同IP地址的人数
页面停留时间 用户在每一个页面停留的时间

前端监控流程

  • 前端埋点
  • 数据上报
  • 分析和计算将采集到的数据进行加工汇总
  • 可视化展示将数据按各个维度进行展示
  • 监控报警,发现问题后按一定条件触发报警
    从0到1构建前端监控系统:错误捕获、性能采集、用户体验全链路追踪实战指南SDK实现
    上图中,前端主要关注埋点和数据采集两个阶段即可。

常见埋点方案

代码埋点

代码埋点就是以嵌入代码的形式进行埋点,比如需要监控用户的点击事件,会选择在用户点击时插入一段代码,保存这个监听行为或者直接将监听行为以某一种数据格式直接传递给服务端。

优点: 可以在任意时刻,精确的发送或保存所需要的数据信息。
缺点: 工作量比较大

可视化埋点

通过可视化交互的手段,代替代码埋点,将业务代码和埋点代码分离,提供一个可视化交互的页面,输入业务代码,通过这个可视化系统可以在业务代码中自定义的增加埋点时间等等,最后输出的代码耦合了业务代码和埋点代码,可视化埋点其实是用系统来代替手工插入埋点代码。

无痕埋点

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

缺点: 数据传输和服务器压力增加,无法灵活定制数据结构。

创建项目

第一步、创建monitor文件,cmd进入文件进行npm init -y 项目初始化

从0到1构建前端监控系统:错误捕获、性能采集、用户体验全链路追踪实战指南SDK实现

第二步、创建src/index.jssrc/index.html文件

从0到1构建前端监控系统:错误捕获、性能采集、用户体验全链路追踪实战指南SDK实现

第三步、创建webpack.config.js文件

从0到1构建前端监控系统:错误捕获、性能采集、用户体验全链路追踪实战指南SDK实现

const path = require('path')
// webpack打包项目的,HtmlWebpackPlugin生成产出HTML文件,user-agent 把浏览器的userAgent变成一个对象 
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
  entry: "./src/index.js", // 入口文件 
  context: process.cwd(), // 上下文目录 
  mode: "development", // 开发模式 
  output: {
    path: path.resolve(__dirname, "dist"), // 输出目录 
    filename: "monitor.js" // 文件名 
  },
  devServer: {
    static: {
      directory: path.resolve(__dirname, "dist"), // devServer静态文件目录,替换contentBase 
    },
  }, plugins: [new HtmlWebpackPlugin({ // 自动打包出HTML文件 
    template: "./src/index.html",
    inject: "head"
  })]
}

第四步、安装所需模块

npm install webpack webpack-cli html-webpack-plugin user-agent -D

从0到1构建前端监控系统:错误捕获、性能采集、用户体验全链路追踪实战指南SDK实现

npm i webpack-dev-server -D

从0到1构建前端监控系统:错误捕获、性能采集、用户体验全链路追踪实战指南SDK实现

第五步、配置package.json

{
  "name": "monitor",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "build": "webpack",
    "dev": "webpack-dev-server"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "html-webpack-plugin": "^5.6.3",
    "user-agent": "^1.0.4",
    "webpack": "^5.99.7",
    "webpack-cli": "^6.0.1",
    "webpack-dev-server": "^5.2.1"
  }
}

执行npm run build,执行后会出现两个文件
从0到1构建前端监控系统:错误捕获、性能采集、用户体验全链路追踪实战指南SDK实现
启动服务器: npm run dev
从0到1构建前端监控系统:错误捕获、性能采集、用户体验全链路追踪实战指南SDK实现
从0到1构建前端监控系统:错误捕获、性能采集、用户体验全链路追踪实战指南SDK实现

js报错,资源加载报错,promise报错采集上报脚本

监控错误和错误分类

  • JS错误:js报错。
  • promise异常 资源异常:监听error

错误异常上报数据结构设计

从0到1构建前端监控系统:错误捕获、性能采集、用户体验全链路追踪实战指南SDK实现

js报错上报数据结构
{
	errorType: "jsError"
	filename: "http://localhost:8080/"
	kind: "stability"
	message: "Uncaught TypeError: Cannot set properties of undefined (setting 'error')"
	position: "25:30"
	selector: "html body div#container div.content input"
	stack: "TypeError: Cannot set properties of undefined (setting 'error')^errorClick (http://localhost:8080/:25:30)^HTMLInputElement.onclick (http://localhost:8080/:14:72)"
	timestamp: "1746587404145"
	title: "前端监控SDK"
	type: "error"
	url: "http://localhost:8080/"
}
promise报错上报数据结构
	errorType: "PromiseError"  // 错误类型
	filename: "http://localhost:8080/" // 访问的文件名
	kind: "stability" // 大类
	message: "Cannot set properties of undefined (setting 'error')"
	position: "30:32" // 报错行列
	selector: "html body div#container div.content input"
	stack: "TypeError: Cannot set properties of undefined (setting 'error')^http://localhost:8080/:30:32^new Promise (<anonymous>)^promiseErrirClick (http://localhost:8080/:29:9)^HTMLInputElement.onclick (http://localhost:8080/:19:11)" 
	timestamp: "1746587310847"
	title: "前端监控SDK" // 页面标题
	type: "error"  // 小类
	url: "http://localhost:8080/" // url
	userAgent:"chrome 135.0.0.0"  // 用户浏览器信息
资源加载报错上报数据结构
{
	errorType: "resourceError", // 错误类型
	filename: "http://localhost:8080/monitor.css", // 访问的文件名
	kind: "stability", // 大类
	selector: "html head link" "selector", // 选择器
	tagName: "LINK",  // 标签名
	timestamp: "1746587169153",  // 时间戳
	title: "前端监控SDK",  // 页面标题
	type: "error", // 小类
	url: "http://localhost:8080/", // 页面URL
	userAgent:"chrome 135.0.0.0" // 用户浏览器信息
}

js异常监听上报

第一步、编写index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>前端监控SDK</title>
    <script src="monitor.js"></script>
  </head>
  <body>
    <div id="container">
      <div class="content">
        <input type="button" value="点击报错" onclick="errorClick()" />
        <input
          type="button"
          value="点击抛出Promise错误"
          onclick="promiseErrirClick()"
        />
      </div>
    </div>
    <script>
      function errorClick() {
        window.someVar.error = "error";
      }
    </script>
  </body>
</html>

第二步、创建/src/monitor/index.js文件

从0到1构建前端监控系统:错误捕获、性能采集、用户体验全链路追踪实战指南SDK实现

第三步,入口文件中引入src/index.js

从0到1构建前端监控系统:错误捕获、性能采集、用户体验全链路追踪实战指南SDK实现

第四步、创建/src/monitor/lib/jsError.js

创建/src/monitor/lib/jsError.js并导出export function injectJsError() { }
从0到1构建前端监控系统:错误捕获、性能采集、用户体验全链路追踪实战指南SDK实现

第五步、/src/monitor/index.js导入jsError.js
import { injectJsError } from './lib/jsError'
injectJsError()
第六步、查看是否打印成功

从0到1构建前端监控系统:错误捕获、性能采集、用户体验全链路追踪实战指南SDK实现

第七步、新建/src/monitor/utils/getLastEvent.js工具文件
let lastEvent;
['click', 'touchstart', 'mousedown', 'keydown', 'mouseover'].forEach(eventType => {
  document.addEventListener(eventType, function (e) {
    lastEvent = e
  }, {
    capture: true, // 捕获阶段监听
    passive: true, // 默认不阻止默认事件
  })
})
第八步、新建/src/monitor/utils/getSelector.js 获取报错元素路径文件
function getSelectors(path) {
  // 反转输入的元素和路径数组,并过滤掉document和window对象
  return path.reverse().filter(element => {
    return element !== document && element !== window
  }).map(element => {
    let selector = ""; // 初始化选择器字符串
    if (element.id) { // 如果元素有id属性,则使用id创建更具体的选择器
      return `${element.nodeName.toLowerCase()}#${element.id}`
    } else if (element.className && typeof element.className === "string") {
      // 如果元素没有id属性,但是有className属性,并且是个字符串,则尝试使用class来创建选择器
      return `${element.nodeName.toLowerCase()}.${element.className}`
    } else {
      // 如果既没有id也没有class则进返回元素的标签名作为选择器
      return selector = `${element.nodeName.toLowerCase()}`
    }
  }).join(" ") // 将所有选择器用空格拼接
}
export default function (path) {
  if (Array.isArray(path)) {
    return getSelectors(path)
  }
}
第八步、创建上报文件/src/monitor/utils/getSelector.js

监听错误后,调用send(data)方法,调用上报接口,将日志上报到阿里云日志中,当然如下的aliyun链接已经失效,可以换成自己的服务接口即可。

let host = 'en-beijing-log.aliyuncs.com';
let project = 'zhufengmonitor';
let logStore = 'zhufengmonitor-store';
let userAgent = require('user-agent');
function getExtraData() {
  return {
    title: document.title,
    url: location.url,
    timestamp: Date.now(),
    userAgent: userAgent.parse(navigator.userAgent)
    // 用户ID
  }
}
class SendTracker {
  constructor() {
    // 上报路径 : 项目名.主机名/logstores/存储的名字/track
    this.url = `${project}.${host}/logstores/${logStore}/track` // 上报路径
    this.xhr = new XMLHttpRequest();
  }
  send(data = {}) {
    let extraData = getExtraData();
    let log = { ...extraData, ...data }
    // 对象的值不能是数字
    for (let key in log) {
      if (typeof log[key] === 'number') {
        log[key] = `${log[key]}`;
      }
    }
    console.log("send log:", log)
    let body = JSON.stringify(log);
    this.xhr.open('POST', this.url, true);
    this.xhr.setRequestHeader('Content-Type', 'application/json') // 请求体类型
    this.xhr.setRequestHeader('x-log-apiversion', '0.6.0') // 版本号
    this.xhr.setRequestHeader('x-log-bodyrawsize', body.length) // 请求体的大小
    this.xhr.onload = function () {
      console.log(this.xhr.response);
    }
    this.xhr.onerror = function (error) {
      console.log(error);
    }
    this.xhr.send(body);
  }
}
export default new SendTracker();
第八步、编写jsError.js文件

需要注意,要按照阿里云日志接口格式进行传输

let host = 'cn-beijing.log.aliyuncs.com';
let project = 'zhufengmonitor';
let logStore = 'zhufengmonitor-store';
let userAgent = require('user-agent');
function getExtraData() {
  return {
    title: document.title,
    url: location.url,
    timestamp: Date.now(),
    userAgent: userAgent.parse(navigator.userAgent)
    // 用户ID
  }
}
class SendTracker {
  constructor() {
    // 上报路径 : 项目名.主机名/logstores/存储的名字/track
    this.url = `https://${project}.${host}/logstores/${logStore}/track` // 上报路径
    this.xhr = new XMLHttpRequest();
  }
  send(data = {}) {
    let extraData = getExtraData();
    let log = { ...extraData, ...data }
    // 对象的值不能是数字
    for (let key in log) {
      if (typeof log[key] === 'number') {
        log[key] = `${log[key]}`;
      }
    }
    let body = JSON.stringify({
      __logs__: [log]
    })
    this.xhr.open('POST', this.url, true);
    this.xhr.setRequestHeader('Content-Type', 'application/json') // 请求体类型
    this.xhr.setRequestHeader('x-log-apiversion', '0.6.0') // 版本号
    this.xhr.setRequestHeader('x-log-bodyrawsize', body.length) // 请求体的大小
    this.xhr.onload = function () {
      // console.log(this.xhr.response);
    }
    this.xhr.onerror = function (error) {
      // console.log(error);
    }
    this.xhr.send(body);
  }
}
export default new SendTracker();
第九步、上报效果查看

链接地址已经失效,如果实现换成自己的服务接口即可。
从0到1构建前端监控系统:错误捕获、性能采集、用户体验全链路追踪实战指南SDK实现

从0到1构建前端监控系统:错误捕获、性能采集、用户体验全链路追踪实战指南SDK实现
从0到1构建前端监控系统:错误捕获、性能采集、用户体验全链路追踪实战指南SDK实现
传输数据格式
从0到1构建前端监控系统:错误捕获、性能采集、用户体验全链路追踪实战指南SDK实现

Promise异常监听上报

第一步、promise监听异常上报方法实现

src/monitor/lib/jsError.js文件新增promise抛出异常监听代码:

import getLastEvent from '../utils/getLastEvent.js'
import getSelector from '../utils/getSelector.js'
import tracker from '../utils/tracker.js'
export function injectJsError() {
  // 监听全局未捕获的错误
  window.addEventListener('error', function (event) {
    let lastEvent = getLastEvent() // 获取最后一个交互事件
    // 获取冒泡向上的dom路径
    var path = lastEvent.path || (function (evt) {
      var path = [];
      var el = evt.target;
      while (el) {
        path.push(el);
        el = el.parentElement;
      }
      return path;
    })(lastEvent);
    let log = {
      kind: 'stability', // 监控指标的大类
      type: 'error', // 小类型错误
      errorType: 'jsError', // JS执行错误
      filename: event.filename, // 错误所在的文件
      position: event.lineno + ':' + event.colno, // 错误所在的行和列的位置
      message: event.message, // 错误信息
      stack: getLines(event.error.stack), // 错误堆栈
      selector: lastEvent ? getSelector(path) : "" // 代表最后一个操作的元素
    }
    tracker.send(log)  // 上报数据
  })
// 监听全局promise错误异常
  window.addEventListener('unhandledrejection', function (event) {
    console.log("promise error:", event)
    let lastEvent = getLastEvent(); // 最后一个事件对象
    var path = lastEvent.path || (function (evt) {
      var path = [];
      var el = evt.target;
      while (el) {
        path.push(el);
        el = el.parentElement;
      }
      return path;
    })(lastEvent);
    let message;
    let filename;
    let line = 0;
    let column = 0;
    let stack = '';
    let reason = event.reason;
    if (typeof reason === 'string') {
      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];
      }
      stack = getLines(reason.stack)
    }
    message = reason.message;
    tracker.send({
      kind: 'stability', // 监控指标的大类
      type: 'error', // 小类型错误
      errorType: 'PromiseError', // promise执行错误
      filename, // 错误所在的文件
      position: line + ':' + column, // 错误所在的行和列的位置
      message, // 错误信息
      stack, // 错误堆栈
      selector: lastEvent ? getSelector(path) : "" // 代表最后一个操作的元素
    })  // 上报数据
  }, true)
  // 拼接at报错方法一“^”拼接
  function getLines(stack) {
    return stack.split('\n').map(item => item.replace(/^\s+at\s+/g, '')).join('^');
  }
}
第二步、上报效果查看

接口换为公司上报接口即可:
从0到1构建前端监控系统:错误捕获、性能采集、用户体验全链路追踪实战指南SDK实现
从0到1构建前端监控系统:错误捕获、性能采集、用户体验全链路追踪实战指南SDK实现
从0到1构建前端监控系统:错误捕获、性能采集、用户体验全链路追踪实战指南SDK实现

资源加载异常监听上报

第一步、改造/src/monitor/jsError.js文件

如果 (JavaScript 文件CSS 文件)如果加载失败都会触发window.addEventListener('error', (event) => {})的监听事件

import getLastEvent from '../utils/getLastEvent.js'
import getSelector from '../utils/getSelector.js'
import tracker from '../utils/tracker.js'
export function injectJsError() {
  // 监听全局未捕获的错误 
  window.addEventListener('error', (event) => {
    let lastEvent = getLastEvent() // 获取最后一个交互事件
    // 这是一个脚本加载错误
    if (event.target && (event.target.src || event.target.href)) {
      let log = {
        kind: 'stability', // 监控指标的大类
        type: 'error', // 小类型错误
        errorType: 'resourceError', // JS或者CS资源加载错误
        filename: event.target.src || event.target.href, // 哪个文件报错了
        tagName: event.target.tagName, // 错误所在标签
        // stack: getLines(event.error.stack), // 错误堆栈
        selector: getSelector(event.target) // 代表最后一个操作的元素
      }
      tracker.send(log)  // 上报数据
    } else {
      // 获取冒泡向上的dom路径
      var path = lastEvent.path || (function (evt) {
        var path = [];
        var el = evt.target;
        while (el) {
          path.push(el);
          el = el.parentElement;
        }
        return path;
      })(lastEvent);
      let log = {
        kind: 'stability', // 监控指标的大类
        type: 'error', // 小类型错误
        errorType: 'jsError', // JS执行错误
        filename: event.filename, // 错误所在的文件
        position: event.lineno + ':' + event.colno, // 错误所在的行和列的位置
        message: event.message, // 错误信息
        stack: getLines(event.error.stack), // 错误堆栈
        selector: lastEvent ? getSelector(path) : "" // 代表最后一个操作的元素
      }
      tracker.send(log)  // 上报数据
    }

  }, true)

  window.addEventListener('unhandledrejection', function (event) {
    console.log("异常错误");
    let lastEvent = getLastEvent(); // 最后一个事件对象
    var path = lastEvent.path || (function (evt) {
      var path = [];
      var el = evt.target;
      while (el) {
        path.push(el);
        el = el.parentElement;
      }
      return path;
    })(lastEvent);
    let message;
    let filename;
    let line = 0;
    let column = 0;
    let stack = '';
    let reason = event.reason;
    if (typeof reason === 'string') {
      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];
      }
      stack = getLines(reason.stack)
    }
    message = reason.message;
    tracker.send({
      kind: 'stability', // 监控指标的大类
      type: 'error', // 小类型错误
      errorType: 'PromiseError', // JS执行错误
      filename, // 错误所在的文件
      position: line + ':' + column, // 错误所在的行和列的位置
      message, // 错误信息
      stack, // 错误堆栈
      selector: lastEvent ? getSelector(path) : "" // 代表最后一个操作的元素
    })  // 上报数据
  }, true)
  // 拼接at报错方法一“^”拼接
  function getLines(stack) {
    return stack.split('\n').map(item => item.replace(/^\s+at\s+/g, '')).join('^');
  }
}
第二步、修改/src/monitor/utils/getSelector.js方法
function getSelectors(path) {
  // 反转输入的元素和路径数组,并过滤掉document和window对象
  return path.reverse().filter(element => {
    return element !== document && element !== window
  }).map(element => {
    let selector = ""; // 初始化选择器字符串
    if (element.id) { // 如果元素有id属性,则使用id创建更具体的选择器
      return `${element.nodeName.toLowerCase()}#${element.id}`
    } else if (element.className && typeof element.className === "string") {
      // 如果元素没有id属性,但是有className属性,并且是个字符串,则尝试使用class来创建选择器
      return `${element.nodeName.toLowerCase()}.${element.className}`
    } else {
      // 如果既没有id也没有class则进返回元素的标签名作为选择器
      return selector = `${element.nodeName.toLowerCase()}`
    }
  }).join(" ") // 将所有选择器用空格拼接
}
export default function (pathOrTarget) {
  if (Array.isArray(pathOrTarget)) { // 可能是一个数组,也可能是一个对象 
    return getSelectors(pathOrTarget)
  } else {
    let path = []
    while (pathOrTarget) {
      path.push(pathOrTarget);
      pathOrTarget = pathOrTarget.parentNode;
    }
    return getSelectors(path)
  }
}
第三步、上报效果查看

从0到1构建前端监控系统:错误捕获、性能采集、用户体验全链路追踪实战指南SDK实现
从0到1构建前端监控系统:错误捕获、性能采集、用户体验全链路追踪实战指南SDK实现
从0到1构建前端监控系统:错误捕获、性能采集、用户体验全链路追踪实战指南SDK实现
上面换成公司服务上报接口即可

接口异常采集上报脚本

接口异常上报数据结构设计

从0到1构建前端监控系统:错误捕获、性能采集、用户体验全链路追踪实战指南SDK实现

接口监听上报

第一步、模拟接口请求

编写/src/index.html 文件

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>前端监控SDK</title>
    <script src="monitor.js"></script>
  </head>
  <body>
    <div id="container">
      <div class="content">
        <input
          id="successBtn"
          type="button"
          value="ajax成功请求"
          onclick="sendSuccess()"
        />
        <input
          id="errorBtn"
          type="button"
          value="ajax失败请求"
          onclick="sendError()"
        />
      </div>
    </div>
    <script>
      //
      function sendSuccess() {
        let xhr = new XMLHttpRequest();
        xhr.open(
          "POST",
          "http://192.168.60.38:32753/visiondevice/checkParamConfig/getDetails",
          true
        );
        xhr.responseType = "json";
        xhr.onload = function () {
          console.log(xhr.response);
        };
        xhr.send();
      }
      // 发送错误报错
      function sendError() {
        let xhr = new XMLHttpRequest();
        xhr.open(
          "POST",
          "http://192.168.60.38:32753/visiondevice/systemConfig/getDetail",
          true
        );
        xhr.responseType = "json";
        xhr.onload = function () {
          console.log(xhr.response);
        };
        xhr.onerror = function (error) {
          console.log("error", error);
        };
        xhr.send();
      }
    </script>
  </body>
</html>
第二步、创建xhr.js监听接口上报文件

创建/src/monitor/lib/xhr.js文件,编写增强 XMLHttpRequest 对象的功能,以监控和记录所有的 AJAX 请求及其状态。通过重写 XMLHttpRequest.prototype.openXMLHttpRequest.prototype.send 方法,可以在请求发出前、请求完成时以及请求失败或被取消时收集相关数据,并使用 tracker.send 方法将这些数据上报给某个监控系统。

import tracker from '../utils/tracker.js';
export default function injectXHR() {
  // 保存原始的 open 和 send 方法
  const originalXhrOpen = window.XMLHttpRequest.prototype.open;
  window.XMLHttpRequest.prototype.open = function (method, url, async) {
    console.log("url", url)
    // 上报请求不需要返回
    if (!url.match(/logstores/) && !url.match(/sockjs/)) {
      console.log("logstores")
      this.logData = {
        method,
        url,
        async,
        startTime: Date.now()
      };
    }
    return originalXhrOpen.apply(this, arguments);
  };
  const originalXhrSend = window.XMLHttpRequest.prototype.send;
  window.XMLHttpRequest.prototype.send = function (body) {
    if (this.logData) {
      const startTime = Date.now();
      const handler = (type) => (event) => {
        const duration = Date.now() - startTime;
        const status = this.status; // 状态码
        const statusText = this.response.error; // OK Server Error
        tracker.send({
          kind: 'stability',
          type: 'xhr',
          eventType: event.type,
          pathname: this.logData.url, // 请求路径
          status: `${status}-${statusText}`, // 状态码
          duration, // 持续时间
          response: JSON.stringify(this.response),
          requestData: body || '',
          params: this.logData.params, // 响应体
          timestamp: Date.now()
        });
      };

      this.addEventListener('load', handler('load'), false);
      this.addEventListener('error', handler('error'), false);
      this.addEventListener('abort', handler('abort'), false);
    }

    return originalXhrSend.apply(this, arguments);
  };
}
第三步、引入并使用上报监听

src/monitor/index.js引入,并使用

import { injectJsError } from './lib/jsError'
import injectXHR from './lib/xhr'
injectJsError()
injectXHR()
第四步、查看接口监听上报效果

接口请求成功:(忽略上报接口)
从0到1构建前端监控系统:错误捕获、性能采集、用户体验全链路追踪实战指南SDK实现
接口请求失败:(忽略上报接口)
从0到1构建前端监控系统:错误捕获、性能采集、用户体验全链路追踪实战指南SDK实现
请求成功上报效果:
从0到1构建前端监控系统:错误捕获、性能采集、用户体验全链路追踪实战指南SDK实现

请求失败上报效果:
从0到1构建前端监控系统:错误捕获、性能采集、用户体验全链路追踪实战指南SDK实现

白屏采集上报脚本

白屏采集上报数据结构

从0到1构建前端监控系统:错误捕获、性能采集、用户体验全链路追踪实战指南SDK实现

思路

从0到1构建前端监控系统:错误捕获、性能采集、用户体验全链路追踪实战指南SDK实现
在页面中垂直交叉选取多个采样点,使用elementsFromPointAPI获取采样点下的HTML元素,判断采样点元素是否与容器元素相同,遍历采样点,设置采样点与容器元素相同的个数从而判断是否出现摆屏,这种方法精确度比较高,技术栈无关,通用性能好,但是开发成本比较高。

页面关键点采样对比技术实现白屏上报

第一步、创建onload.js文件方法

        新建src/monitor/utils/onload.js文件
        如果在页面完全没有加载完毕的时候【元素,样式表,iframe等外部资源】,dom可能还未稳定,就会导致document.elementsFromPoint() 获取到的元素并不是最终用户看到的那些。如果在这种情况下执行白屏检测,可能会错误地判断某些点为空白点,因为这些点对应的元素尚未加载或渲染完成。
        在页面加载过程中,JavaScript 可能会在 DOMContentLoaded 事件触发后立即执行,这时虽然HTML文档已经被完全加载和解析,但是页面上的图片、样式表等外部资源可能还没有加载完毕。因此,在这种状态下直接进行白屏检测,有可能会错过一些重要的视觉变化,导致误判。
从0到1构建前端监控系统:错误捕获、性能采集、用户体验全链路追踪实战指南SDK实现

export default function (callback) {
  if (document.readyState === 'complete') {
    callback()
  } else {
    window.addEventListener('load', () => {
      callback()
    })
  }
}
第二步、关键点采样对比监控白屏上报

创建/src/monitor/lib/xhr.js文件,页面关键点采样对比实现白屏上报功能:

import tracker from '../utils/tracker.js';
import onload from '../utils/onload.js';
// src/monitor/utils/onload.js

export default function blankScreen() {
  let warpperElements = ['html', 'body', '#container', ".content"]
  let enptyPoints = 0
  function getSelector(element) {
    if (element && element.id) {
      return "#" + element.id
    } else if (element && element.className) {
      //  a b c => .a .b .c
      return "." + element.className.split(' ').filter(item => !!item).join('.')
    } else {
      return element && element.nodeName.toLowerCase()
    }
  }
  //  是包裹元素++
  function isWrapper(element) {
    let selector = getSelector(element)
    if (warpperElements.indexOf(selector) != -1) {
      enptyPoints++
    }
  }
  // 当整个页面渲染完成了才去判断是否是白屏
  onload(function () {
    for (let i = 1; i <= 18; i++) {
      let xElement = document.elementsFromPoint(window.innerWidth * i / 10, window.innerHeight / 2)
      let yElement = document.elementsFromPoint(window.innerWidth / 2, window.innerHeight * i / 10)
      isWrapper(xElement[0])
      isWrapper(yElement[0])
    }
    console.log("enptyPoints", enptyPoints)
    if (enptyPoints >= 16) {
      let centerElements = document.elementsFromPoint(window.innerWidth / 2, window.innerHeight / 2)
      tracker.send({
        kind: "stability",
        type: "blank",
        blankCount: enptyPoints, // 空白点
        screen: window.screen.width + "*" + window.screen.height, // 屏幕尺寸
        viewPoint: window.innerWidth + "*" + window.innerHeight, // 视口尺寸
        selector: getSelector(centerElements[0]),
        page: window.location.href, // 页面地址
        message: "页面空白点大于16个"
      })
    }
  });
}
第三步、编写可监听到白屏和非白屏的HTML文件

不触发白屏监听上报html文件,

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>前端监控SDK</title>
    <script src="monitor.js"></script>
  </head>
  <body>
    <div id="container">
      <div class="content" style="width: 100%; word-wrap: break-word"></div>
    </div>
    <script>
      let content = document.getElementsByClassName("content")[0];
      content.innerHTML = "<span>aaa</span>".repeat(10000);
    </script>
  </body>
</html>

触发白屏监听上报html文件:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>前端监控SDK</title>
    <script src="monitor.js"></script>
  </head>
  <body>
    <div id="container">
      <div class="content">
        <input
          id="successBtn"
          type="button"
          value="ajax成功请求"
          onclick="sendSuccess()"
        />
        <input
          id="errorBtn"
          type="button"
          value="ajax失败请求"
          onclick="sendError()"
        />
      </div>
    </div>
    <script>
      //
      function sendSuccess() {
        let xhr = new XMLHttpRequest();
        xhr.open(
          "POST",
          "http://192.168.60.38:32753/visiondevice/systemConfig/getDetail",
          true
        );
        xhr.responseType = "json";
        xhr.onload = function () {
          console.log(xhr.response);
        };
        xhr.send();
      }
      // 发送错误报错
      function sendError() {
        let xhr = new XMLHttpRequest();
        xhr.open(
          "POST",
          "http://192.168.60.38:32753/visiondevice/checkParamConfig/getDetailsAAAA",
          true
        );
        xhr.responseType = "json";
        xhr.onload = function () {
          console.log(xhr.response);
        };
        xhr.onerror = function (error) {
          console.log("error", error);
        };
        xhr.send();
      }
    </script>
  </body>
</html>
第四步、监测白屏上报和非白屏效果

填满<span>标签,就不会出现白屏,没有打印上报日志
从0到1构建前端监控系统:错误捕获、性能采集、用户体验全链路追踪实战指南SDK实现
不填满<span>标签,就会出现白屏空袭,就会打印上报日志并且发送给服务端
从0到1构建前端监控系统:错误捕获、性能采集、用户体验全链路追踪实战指南SDK实现
发送服务端数据
从0到1构建前端监控系统:错误捕获、性能采集、用户体验全链路追踪实战指南SDK实现

加载时间采集上报

浏览器加载一个网页的整个流程

从0到1构建前端监控系统:错误捕获、性能采集、用户体验全链路追踪实战指南SDK实现
上面一张图展示了浏览器加载一个网页的整个流程,从开始时间到页面完全加载完成。

流程中字段描述
字段 描述
startTime(开始时间) 这是整个加载过程的起点
Prompt for unload(准备卸载旧页面) 如果当前页面是从另一个页面导航过来的,浏览器会触发卸载事件,准备卸载旧页面。
redirectStart(开始重定向) 如果存在重定向,浏览器会在此时开始重定向过程。
redirectEnd(结束重定向) 重定向过程结束。
fetchStart(开始获取文档) 浏览器开始获取文档的时间点。
domainLookupStart(开始域名解析) 浏览器开始进行DNS查询以解析域名。
domainLookupEnd(结束域名解析) DNS查询完成。
connectStart(开始链接) 浏览器开始建立与服务器的TCP连接。
secureConnectionStart(开始安全连接) 如果使用HTTPS,浏览器开始TLS握手过程。
connectEnd(结束连接) TCP连接建立完成。
requestStart(开始请求) 浏览器开始发送HTTP请求到服务器。
Time to First Byte(TTFB,首次字节时间) 从发送请求到接收到第一个字节响应的时间。
responseStart(响应开始) 服务器开始发送响应数据。
responseEnd(响应结束) 服务器完成响应数据的发送。
unloadEventStart(卸载事件开始) 在前一个页面的卸载过程中,卸载事件开始。
unloadEventEnd(卸载事件结束) 卸载事件结束。
domLoading(开始解析DOM) 浏览器开始解析HTML文档并构建DOM树。
domInteractive(DOM结构基本解析完毕) DOM树的基本结构已经构建完成,可以进行交互操作。
domContentLoadedEventStart(DOMContentLoaded事件开始) DOM完全加载且解析完成,但不包括样式表、图片等外部资源。
domContentLoadedEventEnd(DOMContentLoaded事件结束) DOMContentLoaded事件处理程序执行完毕。
domComplete(DOM和资源解析都完成) 所有资源(如图片、脚本等)都已加载完成。
loadEventStart(开始load回调函数) 浏览器开始执行load事件的回调函数。
onLoad(加载事件) 页面完全加载完成,触发load事件。
loadEventEnd(结束load回调函数) load事件的回调函数执行完毕。
其他辅助阶段字段描述
字段 描述
appCache(缓存存储) 应用缓存的处理过程
DNS(域名缓存) DNS缓存的处理过程
TCP(网络连接) TCP链接的处理过程
Request(请求) 请求的处理过程
Response(相应) 响应的处理过程
Procsssing(处理) 对响应数据的处理过程
Load(加载) 页面加载的最终阶段
需要上报字段描述
字段 描述 计算方式 意义
unload 前一个页面卸载耗时 unloadEventEnd - unloadEventStart -
redirect 重定向耗时 redirectEnd - redirectStart 重定向耗时
appCache 缓存耗时 domainLookupStart - fetchStart 读取缓存的时间
dns dns解析耗时 domainLookupEnd - domainLookupStart 可观察域名解析服务是否正常
tcp tcp链接耗时 connectEnd - cibbectStart 建立连接耗时
ssl SSL安全连接耗时 connectEnd - secureConnectionStart 反应数据安全连接建立耗时
ttfb Time to First Byte(TTFB)网络请求耗时 responseStart - requestStart TTFB是发出页面请求到接收到应答数据第一个字节所花费的毫秒数
response 相应数据传输耗时 responseEnd - responseStart 观察网络是否正常
dom DOM解析耗时 domInteractive - responseEnd 观察DOM结构是否合理,是否有JS阻塞页面解析
dcl DOMContentLoaded时间耗时 domContentLoadedEventEnd - domContentLoadedEventStart 当HTML文档被完全加载和解析完成之后,DOMContentLoaded事件等待样式表,图像和子框架完成加载
resources 资源加载耗时 domComplete - domContentLoadedEventEnd 可观文档流量是否过大
domReady DOM 阶段渲染耗时 responseEnd - fetchStart DOM树和页面资源加载完成时间,会触发domContentLoaded事件
首次渲染耗时 首次渲染耗时 responseEnd - fetchStart 加载文档到看到第一帧非空图像的时间,也叫做白屏时间
首次可交互事件 首次可交互时间 domInteractive - fetchStart DOM树解析完成时间,此时document.readyStart为interactive
首包时间耗时 首包耗时 responseStart - domainLookupStart DNS解析到响应返回给浏览器第一个字节的时间
页面完全加载时间 页面完全加载时间 loadEventSart - fetchStart -
onLoad onLoad事件耗时 LoadEventEnd - loadEventStart -

浏览器加载和渲染网页的整个过程

从0到1构建前端监控系统:错误捕获、性能采集、用户体验全链路追踪实战指南SDK实现
这张图展示了浏览器加载和渲染网页的整个过程,从请求HTML文件开始,到最终页面渲染完成。
大致上图流程为是一个阶段,下面是对上图中十一个阶段的解读:

  1. 第一阶段,开始请求HTML文件,浏览器接收到用户输入的URL或者点击链接等操作后,开始向服务器发送HTTP请求,请求获取HTML文件。
  2. 第二阶段,响应HTML文件,服务器接收到请求后,返回HTML文件给浏览器。
  3. 第三阶段,开始加载,浏览器接收到HTML文件后,开始加载过程。
  4. 第四阶段,构建DOM文档对象模型,浏览器使用HTML解析器解析HTML文件构建DOM树,DOM树是HTML结构的表示形式,用于描述页面的层次结构,在构建DOM树的过程中,如果遇到<link>标签(CSS文件引用)<script>标签(js文件引入)浏览器会进行预解析,并发起对CSS文件和JS文件的请求。
  5. 第五阶段,请求CSS文件和JS文件,浏览器在预解析过程中发现需要的CSSJS文件后,分别向服务器发送请求,获取这些资源文件。
  6. 第六阶段,返回CSS数据和JavaScript,服务器接收到请求后,返回相应的CSS文件和JS文件给浏览器。
  7. 第七阶段,构建CSSOM也就是CSS对象模型,浏览器使用CSS解析器解析返回的CSS数据,构建CSSOM树,CSSOM树包含了所有样式信息,用于描述页面元素的央视。
  8. 第八阶段,执行JavaScript,浏览器使用V8解析器解析并执行返回的JavaScript代码,javaScript可以修改DOMCSSOM,因此在这个阶段可能会继续构建DOM树。
  9. 第九阶段,继续构建DOM,如果JS代码中包含了对DOM的修改操作,浏览器会继续构建和更新DOM树。
    10.第十阶段,构建布局树,当DOM树和CSSOM树都构建完成后,浏览器会将他们合并成一个布局树也成为渲染树,布局树包含了所需要的渲染的节点及其央视信息
  10. 第十一阶段,渲染阶段,最后浏览器会根据布局树进行页面渲染,将页面内容展示给用户。

浏览器加载和渲染时间计算上报实现

        在讨论浏览器加载网页的过程时候,Performance(性能)通常指的是web性能,Performance也是只浏览器提供的一个内置对象,window.performance通过这个API,开发者可以获取到详细的页面加载时间和资源加载时间等信息,从而进行性能优化,windong.performance提供了多个属性和方法来帮助分析网页性能。

属性 描述
navigation 包含了相关的导航信息,比如页面是如何被加载的,以及设计重定向的次数。
timing 提供了从开始导航到当前页面完全加载过程中各个关键时间的时间点,如重定向时间,DNS查询时间,TCP链接时间,请求发送时间,响应接收时间等。

打印Performance如下
从0到1构建前端监控系统:错误捕获、性能采集、用户体验全链路追踪实战指南SDK实现

第一步、创建/src/monitor/lib/timing.js文件

从0到1构建前端监控系统:错误捕获、性能采集、用户体验全链路追踪实战指南SDK实现

import tracker from '../utils/tracker.js';
import onload from '../utils/onload.js';
export default function timing() {
  onload(function () {
    setTimeout(() => {
      const {
        fetchStart,
        connectStart,
        connectEnd,
        requestStart,
        responseStart,
        responseEnd,
        domLoading,
        domInteractive,
        domContentLoadedEventStart,
        domContentLoadedEventEnd,
        loadEventStart,
      } = performance.timing;

      tracker.send({
        kind: 'exeprience', // 用户体验指标
        type: 'timing', // 统计每个阶段的时间
        connectTime: connectEnd - connectStart, // 连接时间
        ttfbTime: responseStart - requestStart, // 首字节到达时间
        responseTime: responseEnd - responseStart, // 响应时间
        parseDOMTime: loadEventStart - domLoading, // dom解析时间
        domContentLoadedTime: domContentLoadedEventEnd - domContentLoadedEventStart, // dom加载完成时间
        timeToInteractive: domInteractive - fetchStart, // 首次可交互时间
        loadTime: loadEventStart - fetchStart, // 完整的加载时间
      });
    }, 3000)
  });
}
第二步、模拟DOM加载完成延迟inde.html文件
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>前端监控SDK</title>
    <script src="monitor.js"></script>
  </head>
  <body>
    <div id="container">
      <div class="content" style="width: 100%; word-wrap: break-word"></div>
    </div>
    <script>
      // Dom解析完成后,即使依赖的资源没有加载完成,也会触发这个事件 
      document.addEventListener("DOMContentLoaded", function () {
        let Start = Date.now();
        while (date.now() - Start < 5000) {}
      });
    </script>
  </body>
</html>
第三步、浏览器加载和渲染时间上报效果

从0到1构建前端监控系统:错误捕获、性能采集、用户体验全链路追踪实战指南SDK实现

性能指标采集上报

性能指标描述

字段 全称 描述
FP First Paint(首次绘制) 包括了任何用户自定义的背景绘制,它是首先将像素会知道屏幕的时刻
FCP First Content Paint(首次内容绘制) 是浏览器将第一个DOM元素渲染到屏幕的时间,可能是文本、图像、SVG等,这其实就是白屏的时间
FMP First Meaningfun Paint(首次有意义绘制) 页面有意义的内容渲染时间
LCP Largest ContentFul Paint (最大内容渲染) 代表在viewport中最大的页面元素加载的时间
DCL DomContentLoaded (DOM加载完成) 当HTML文档被完全加载和解析完成之后,DOMContentLoaded时间被触发,无需等待样式表,图像和子框架的完成加载
L onLoad 当以来的资源全部加载完毕之后才会触发。
TTI Time to Interactive (可交互时间) 用于标记应用以进行视觉渲染并能可靠相应用户输入的时间点。
FID First Input Delay(首次输入延迟) 用户首次和页面交互(单机链接,点击按钮等)到页面响应交互的时间

从0到1构建前端监控系统:错误捕获、性能采集、用户体验全链路追踪实战指南SDK实现

FMP有意义绘制是根据自己给定判断,如: h1.setAttribute("elementtiming", "meaningful");
从0到1构建前端监控系统:错误捕获、性能采集、用户体验全链路追踪实战指南SDK实现

各个性能指标获取

触发用例index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>前端监控SDK</title>
    <script src="monitor.js"></script>
  </head>
  <body>
    <div id="container">
      <p style="color: red">hello</p>
      <div class="content" style="width: 100%; word-wrap: break-word">
        <button id="CLICKBTN" onclick="clickMe()">Click Me</button>
      </div>
    </div>
    <script>
      function clickMe() {
        let start = Date.now();
        while (Date.now() - start < 1000) {}
      }
      setTimeout(() => {
        let content = document.getElementsByClassName("content")[0];
        let h1 = document.createElement("h1");
        h1.innerHTML = "我是这个页面中最有意义的内容";
        h1.setAttribute("elementtiming", "meaningful");
        content.appendChild(h1);
      }, 2000);
    </script>
  </body>
</html>
FMP 首次有意义绘制
// 增加一个性能条目的观察者
  new PerformanceObserver((entryList, observer) => {
    let perfEntriens = entryList.getEntries();
    FMP = perfEntriens[0];
    observer.disconnect(); // 不需要观察
  }).observe({ entryTypes: ['element'] }) // 观察页面中有意义的元素
LCP 最大内容渲染
  new PerformanceObserver((entryList, observer) => {
    let perfEntriens = entryList.getEntries();
    LCP = perfEntriens[0];
    observer.disconnect(); // 不需要观察
  }).observe({ entryTypes: ['largest-contentful-paint'] }) // 观察页面中最大的元素
FID 首次输入延迟
  new PerformanceObserver((entryList, observer) => {
    let lastEvent = getLastEvent();
    let firstInput = entryList.getEntries()[0]; // 渠道第一个条目
    console.log("FID", firstInput);
    if (firstInput) {
      // 开始处理的时间 - 开始点击的时间,的差值就是处理的延迟
      let inputDelay = firstInput.processingStart - firstInput.startTime;
      let duration = firstInput.duration; // 处理时长
      if (inputDelay > 0 || duration) {
        tracker.send({
          kind: 'exeprience', // 用户体验指标
          type: 'firstInputDeay', // 首次输入延迟
          inputDelay, // 输入延迟
          duration,  // 处理的时间
          startTime: firstInput.startTime,
          selector: lastEvent ? getSelector(lastEvent.path || lastEvent.target) : ''
        });
      }
    }
    observer.disconnect(); // 不需要观察
  }).observe({ type: 'first-input', buffered: true }) // 用户第一次交互,点击页面

首次点击效果:
从0到1构建前端监控系统:错误捕获、性能采集、用户体验全链路追踪实战指南SDK实现

FP 首次绘制
 let FP = performance.getEntriesByName('first-paint')[0]
FCP 首次内容绘制
 let FCP = performance.getEntriesByName('first-contentful-paint')[0]
性能指标上报整体代码
import tracker from '../utils/tracker.js';
import onload from '../utils/onload.js';
import getLastEvent from '../utils/getLastEvent.js';
import getSelector from '../utils/getSelector.js';
export default function timing() {
  let FMP, LCP;
  // 增加一个性能条目的观察者
  new PerformanceObserver((entryList, observer) => {
    let perfEntriens = entryList.getEntries();
    FMP = perfEntriens[0];
    observer.disconnect(); // 不需要观察
  }).observe({ entryTypes: ['element'] }) // 观察页面中有意义的元素

  new PerformanceObserver((entryList, observer) => {
    let perfEntriens = entryList.getEntries();
    LCP = perfEntriens[0];
    observer.disconnect(); // 不需要观察
  }).observe({ entryTypes: ['largest-contentful-paint'] }) // 观察页面中最大的元素


  new PerformanceObserver((entryList, observer) => {
    let lastEvent = getLastEvent();
    let firstInput = entryList.getEntries()[0]; // 渠道第一个条目
    console.log("FID", firstInput);
    if (firstInput) {
      // 开始处理的时间 - 开始点击的时间,的差值就是处理的延迟
      let inputDelay = firstInput.processingStart - firstInput.startTime;
      let duration = firstInput.duration; // 处理时长
      if (inputDelay > 0 || duration) {
        tracker.send({
          kind: 'exeprience', // 用户体验指标
          type: 'firstInputDeay', // 首次输入延迟
          inputDelay, // 输入延迟
          duration,  // 处理的时间
          startTime: firstInput.startTime,
          selector: lastEvent ? getSelector(lastEvent.path || lastEvent.target) : ''
        });
      }
    }
    observer.disconnect(); // 不需要观察
  }).observe({ type: 'first-input', buffered: true }) // 用户第一次交互,点击页面
  onload(function () {

    setTimeout(() => {
      const {
        fetchStart,
        connectStart,
        connectEnd,
        requestStart,
        responseStart,
        responseEnd,
        domLoading,
        domInteractive,
        domContentLoadedEventStart,
        domContentLoadedEventEnd,
        loadEventStart,
      } = performance.timing;
      // console.log("performance timing:", performance)

      tracker.send({
        kind: 'exeprience', // 用户体验指标
        type: 'timing', // 统计每个阶段的时间
        connectTime: connectEnd - connectStart, // 连接时间
        ttfbTime: responseStart - requestStart, // 首字节到达时间
        responseTime: responseEnd - responseStart, // 响应时间
        parseDOMTime: loadEventStart - domLoading, // dom解析时间
        domContentLoadedTime: domContentLoadedEventEnd - domContentLoadedEventStart, // dom加载完成时间
        timeToInteractive: domInteractive - fetchStart, // 首次可交互时间
        loadTime: loadEventStart - fetchStart, // 完整的加载时间
      });
      let FP = performance.getEntriesByName('first-paint')[0]
      let FCP = performance.getEntriesByName('first-contentful-paint')[0]
      // 开始发送性能指标
      console.log("FP", FP);
      console.log("FCP", FCP);
      console.log("FMP", FMP);
      console.log("LCP", LCP);


      tracker.send({
        kind: 'exeprience', // 用户体验指标
        type: 'paint', // 统计每个阶段的时间
        firstPant: FP.startTime, // 首次绘制
        firstContentFulPant: FCP.startTime,
        firstMeaningfulPant: FMP.startTime,
        largestContentFulPaint: LCP.startTime,  // 最大内容绘制
      });
    }, 3000)
  });
}
查看各项性能指标采集上报效果

从0到1构建前端监控系统:错误捕获、性能采集、用户体验全链路追踪实战指南SDK实现

卡顿监听上报

监听方案

当任务阻塞主线程达到 100 ms或更长时间时,将引发诸多问题,例如,可交互时间延迟、严重不稳定的交互行为 (轻击、单击、滚动、滚轮等) 延迟、严重不稳定的事件回调延迟、紊乱的动画和滚动等。我们将任何主 UI 线程连续不间断繁忙 1000毫秒及以上的时间区间定义为长任务(Long task)。目前,浏览器中存在一个 Long Tasks API,借助该 APIPerformanceObserver 的结合使用,我们能够精准地定位长任务,从而有效实现对卡顿现象的检测,当然还有心跳检测,fps帧率检测卡顿等方法,先从最简单的实现,借助new PerformanceObserver的回调方法参数entry.duration判断是否大于100ms,如果大于则可以认为卡顿。

数据结构

{
	"title":"前端监控",
	"url":"192.168.60.32:8080/", // 卡顿页面
	"timestamp":"158654654845", // 时间戳
	"userAgent":"", // 浏览器信息
	"kind":"experience",
	"type":"longTask",
	"eventType":"mouseover",
	"startTime":"9331",
	"duration":"150",
	"selector":"HTML BODY#container .content",
}

借助Long Tasks API实现卡顿上报

第一步、创建longTask.js文件
import tracker from '../utils/tracker.js';
import getLastEvent from '../utils/getLastEvent.js';
import getSelector from '../utils/getSelector.js';
export default function longTask() {
  new PerformanceObserver((entryList, observer) => {
    entryList.getEntries().forEach(entry => {
      if (entry.duration > 100) {
        let lastEvent = getLastEvent();
        var path = lastEvent.path || (function (evt) {
          var path = [];
          var el = evt.target;
          while (el) {
            path.push(el);
            el = el.parentElement;
          }
          return path;
        })(lastEvent);
        console.log("longTask:", getSelector(path))
        tracker.send({
          kind: 'experience',
          type: 'longTask',
          eventType: lastEvent.type,
          startTime: entry.startTime, // 开始时间
          duration: entry.duration,  // 持续时间
          selector: lastEvent ? getSelector(path) : "",
        })
      }
    })
  }).observe({ entryTypes: ['longtask'] })
}
第二步、循环模仿卡顿效果
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="container">
      <div class="content" style="width: 100%; word-wrap: break-word">
        <button id="button">点击卡顿</button>
      </div>
    </div>
    <script>
      document.getElementById("button").addEventListener("click", function () {
        // 模拟页面卡顿
        for (let i = 0; i < 1000000000; i++) {
          // do nothing
        }
      });
    </script>
  </body>
</html>
第三步、查看卡顿上报效果

从0到1构建前端监控系统:错误捕获、性能采集、用户体验全链路追踪实战指南SDK实现
从0到1构建前端监控系统:错误捕获、性能采集、用户体验全链路追踪实战指南SDK实现

PV、UV、页面停留时间采集上报

PV(page view) 是页面浏览量。
UV(Unique visitor)用户访问量。
PV 只要访问一次页面就算一次。
UV同一天内多次访问只算一次。

对于前端来说,只要每次进入页面上报一次 PV 就行,UV 的统计放在服务端来做,主要是分析上报的数据来统计得出 UV。

实现pv和页面停留时间监听

第一步、创建pv.js
import tracker from '../utils/tracker.js';
// utils.js 或者直接放在你的文件中
function getPageURL() {
  return window.location.href;
}

function getUUID() {
  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
    var r = Math.random() * 16 | 0,
      v = c == 'x' ? r : (r & 0x3 | 0x8);
    return v.toString(16);
  });
}

export default function pv() {
  tracker.send({
    kind: "business",
    type: "pv",
    startTime: performance.now(),
    pageURL: getPageURL(),
    referrer: document.referrer,
    uuid: getUUID(),
  });
  let startTime = Date.now();
  window.addEventListener(
    "beforeunload",
    () => {
      let stayTime = Date.now() - startTime;
      tracker.send({
        kind: "business",
        type: "stayTime",
        stayTime,
        pageURL: getPageURL(),
        uuid: getUUID(),
      });
    },
    false
  );
}
第二步、查看上报结果

从0到1构建前端监控系统:错误捕获、性能采集、用户体验全链路追踪实战指南SDK实现
从0到1构建前端监控系统:错误捕获、性能采集、用户体验全链路追踪实战指南SDK实现

完结~

觉得内容有用?别忘了点个赞+收藏+关注,三连走一波!你的支持是我持续输出的动力 💪


网站公告

今日签到

点亮在社区的每一天
去签到