从0到1构建前端监控系统:错误捕获、性能采集、用户体验全链路追踪实战指南SDK实现
目录
- 前言
- 为什么要做前端监控
- 前端监控目标
- 稳定性
- 用户体验
- 业务
- 前端监控流程
- 常见埋点方案
- 代码埋点
- 可视化埋点
- 无痕埋点
- 创建项目
- 第一步、创建`monitor`文件,`cmd`进入文件进行`npm init -y` 项目初始化
- 第二步、创建`src/index.js`和`src/index.html`文件
- 第三步、创建`webpack.config.js`文件
- 第四步、安装所需模块
- 第五步、配置package.json
- js报错,资源加载报错,promise报错采集上报脚本
- 监控错误和错误分类
- 错误异常上报数据结构设计
- js报错上报数据结构
- promise报错上报数据结构
- 资源加载报错上报数据结构
- js异常监听上报
- 第一步、编写index.html
- 第二步、创建`/src/monitor/index.js`文件
- 第三步,入口文件中引入`src/index.js`
- 第四步、创建`/src/monitor/lib/jsError.js`
- 第五步、`/src/monitor/index.js`导入`jsError.js`
- 第六步、查看是否打印成功
- 第七步、新建`/src/monitor/utils/getLastEvent.js`工具文件
- 第八步、新建`/src/monitor/utils/getSelector.js` 获取报错元素路径文件
- 第八步、创建上报文件`/src/monitor/utils/getSelector.js`
- 第八步、编写`jsError.js`文件
- 第九步、上报效果查看
- Promise异常监听上报
- 第一步、promise监听异常上报方法实现
- 第二步、上报效果查看
- 资源加载异常监听上报
- 第一步、改造`/src/monitor/jsError.js文件`
- 第二步、修改`/src/monitor/utils/getSelector.js`方法
- 第三步、上报效果查看
- 接口异常采集上报脚本
- 接口异常上报数据结构设计
- 接口监听上报
- 第一步、模拟接口请求
- 第二步、创建`xhr.js`监听接口上报文件
- 第三步、引入并使用上报监听
- 第四步、查看接口监听上报效果
- 白屏采集上报脚本
- 白屏采集上报数据结构
- 思路
- 页面关键点采样对比技术实现白屏上报
- 第一步、创建onload.js文件方法
- 第二步、关键点采样对比监控白屏上报
- 第三步、编写可监听到白屏和非白屏的HTML文件
- 第四步、监测白屏上报和非白屏效果
- 加载时间采集上报
- 浏览器加载一个网页的整个流程
- 流程中字段描述
- 其他辅助阶段字段描述
- 需要上报字段描述
- 浏览器加载和渲染网页的整个过程
- 浏览器加载和渲染时间计算上报实现
- 第一步、创建`/src/monitor/lib/timing.js`文件
- 第二步、模拟DOM加载完成延迟inde.html文件
- 第三步、浏览器加载和渲染时间上报效果
- 性能指标采集上报
- 性能指标描述
- 各个性能指标获取
- 触发用例index.html
- FMP 首次有意义绘制
- LCP 最大内容渲染
- FID 首次输入延迟
- FP 首次绘制
- FCP 首次内容绘制
- 性能指标上报整体代码
- 查看各项性能指标采集上报效果
- 卡顿监听上报
- 监听方案
- 数据结构
- 借助Long Tasks API实现卡顿上报
- 第一步、创建longTask.js文件
- 第二步、循环模仿卡顿效果
- 第三步、查看卡顿上报效果
- PV、UV、页面停留时间采集上报
- 实现pv和页面停留时间监听
- 第一步、创建pv.js
- 第二步、查看上报结果
- 完结~
前言
在当今快速发展的Web
开发领域中,无论是面试还是与同行交流时,“高大上”的技术术语层出不穷。还记得我第一次了解到前端埋点监控是在一家知名大厂的面试过程中。当面试官问及如何实现前端埋点监控时,我感到一阵茫然——脑海中一片模糊,各种概念交织却难以理清头绪。不出所料,那次面试的第三轮我没有通过。
这次经历成为我深入学习前端埋点监控的起点。回到家中,我开始查阅大量资料、观看教学视频,逐步摸索出了一套学习路径。本文便是基于这段时间的学习和实践总结而成,旨在为那些刚开始接触前端埋点监控的朋友提供一个详尽的指南。
文中涵盖了从PV
统计、页面加载性能指标(如FMP
、LCP
、FID、FP、FCP)到Promise异常监控、错误监控以及资源加载监控等多方面的内容。为了确保每个细节都能被清晰理解,并且避免将来遗忘关键知识点,我在编写时尽可能地详尽描述了每一步骤和每一个概念。
无论你是刚踏入前端领域的新人,还是希望进一步提升技能的开发者,相信这篇文章都能为你提供有价值的参考。让我们一起揭开前端埋点监控的神秘面纱,掌握这一提升网站性能和用户体验的重要工具。
觉得内容有用?别忘了点个赞+收藏+关注,三连走一波!你的支持是我持续输出的动力 💪
gitee仓库地址:地址
为什么要做前端监控
珠峰架构公开课地址【下文根据公开课边学边做边实现,加上自己的个人想法】:珠峰公开课地址
更快发现问题和解决问题。
做产品的决策依据。
提升前端工程师的技术深度和广度。
为业务扩展提供更多可能性。
前端监控目标
稳定性
错误名称 | 备注 |
---|---|
JS 错误 | JS 执行错误或者promise异常 |
资源异常 | script ,link 资源加载异常 |
接口错误 | ajax 或fetch 接口请求异常 |
白屏 | 页面白屏 |
用户体验
错误名称 | 备注 |
---|---|
加载时间 | 各个阶段的加载时间 |
TTFB 首字节时间 | 是指浏览器发起第一个请求到数据返回第一个字节所消耗的时间,这个时间包含了网络请求时间,后端处理时间。 |
FP 首次绘制 | 首次绘制包括了任何用户自定义的背景绘制,它是将第一个像素点绘制到屏幕的时刻。 |
FCP 首次内容绘制 | 首次内容绘制是浏览器将第一个DOM 渲染到屏幕的时间,可以是任何文本图像和SVG 等的时间。 |
FMP 首次有意义绘制 | 首次有意义绘制是页面可用性的亮度标准。 |
FID 首次输入延迟 | 用户首次和页面交互到页面响应交互的时间 |
卡顿 | 超过50ms 的长任务 |
业务
错误名称 | 备注 |
---|---|
PV | page view 即页面浏览量或点击量 |
UV | 指访问某个站点不同IP 地址的人数 |
页面停留时间 | 用户在每一个页面停留的时间 |
前端监控流程
- 前端埋点
- 数据上报
- 分析和计算将采集到的数据进行加工汇总
- 可视化展示将数据按各个维度进行展示
- 监控报警,发现问题后按一定条件触发报警
上图中,前端主要关注埋点和数据采集两个阶段即可。
常见埋点方案
代码埋点
代码埋点就是以嵌入代码的形式进行埋点,比如需要监控用户的点击事件,会选择在用户点击时插入一段代码,保存这个监听行为或者直接将监听行为以某一种数据格式直接传递给服务端。
优点: 可以在任意时刻,精确的发送或保存所需要的数据信息。
缺点: 工作量比较大
可视化埋点
通过可视化交互的手段,代替代码埋点,将业务代码和埋点代码分离,提供一个可视化交互的页面,输入业务代码,通过这个可视化系统可以在业务代码中自定义的增加埋点时间等等,最后输出的代码耦合了业务代码和埋点代码,可视化埋点其实是用系统来代替手工插入埋点代码。
无痕埋点
前端的任意一个事件都被绑定一个标识,所有的事件都被记录下来,通过定期上传记录文件,配合文件解析,解析出来我们想要的数据,并生成可视化报告供专业人员分析.
缺点: 数据传输和服务器压力增加,无法灵活定制数据结构。
创建项目
第一步、创建monitor
文件,cmd
进入文件进行npm init -y
项目初始化
第二步、创建src/index.js
和src/index.html
文件
第三步、创建webpack.config.js
文件
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
npm i webpack-dev-server -D
第五步、配置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
,执行后会出现两个文件
启动服务器: npm run dev
js报错,资源加载报错,promise报错采集上报脚本
监控错误和错误分类
JS
错误:js
报错。promise
异常 资源异常:监听error
。
错误异常上报数据结构设计
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/" // urluserAgent:"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/", // 页面URLuserAgent:"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()" /><inputtype="button"value="点击抛出Promise错误"onclick="promiseErrirClick()"/></div></div><script>function errorClick() {window.someVar.error = "error";}</script></body>
</html>
第二步、创建/src/monitor/index.js
文件
第三步,入口文件中引入src/index.js
第四步、创建/src/monitor/lib/jsError.js
创建/src/monitor/lib/jsError.js
并导出export function injectJsError() { }
第五步、/src/monitor/index.js
导入jsError.js
import { injectJsError } from './lib/jsError'
injectJsError()
第六步、查看是否打印成功
第七步、新建/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/存储的名字/trackthis.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/存储的名字/trackthis.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();
第九步、上报效果查看
链接地址已经失效,如果实现换成自己的服务接口即可。
传输数据格式
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('^');}
}
第二步、上报效果查看
接口换为公司上报接口即可:
资源加载异常监听上报
第一步、改造/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)}
}
第三步、上报效果查看
上面换成公司服务上报接口即可
接口异常采集上报脚本
接口异常上报数据结构设计
接口监听上报
第一步、模拟接口请求
编写/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"><inputid="successBtn"type="button"value="ajax成功请求"onclick="sendSuccess()"/><inputid="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.open
和 XMLHttpRequest.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 Errortracker.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()
第四步、查看接口监听上报效果
接口请求成功:(忽略上报接口)
接口请求失败:(忽略上报接口)
请求成功上报效果:
请求失败上报效果:
白屏采集上报脚本
白屏采集上报数据结构
思路
在页面中垂直交叉选取多个采样点,使用elementsFromPoint
API获取采样点下的HTML
元素,判断采样点元素是否与容器元素相同,遍历采样点,设置采样点与容器元素相同的个数从而判断是否出现摆屏,这种方法精确度比较高,技术栈无关,通用性能好,但是开发成本比较高。
页面关键点采样对比技术实现白屏上报
第一步、创建onload.js文件方法
新建src/monitor/utils/onload.js
文件
如果在页面完全没有加载完毕的时候【元素,样式表,iframe
等外部资源】,dom
可能还未稳定,就会导致document.elementsFromPoint()
获取到的元素并不是最终用户看到的那些。如果在这种情况下执行白屏检测,可能会错误地判断某些点为空白点,因为这些点对应的元素尚未加载或渲染完成。
在页面加载过程中,JavaScript 可能会在 DOMContentLoaded 事件触发后立即执行,这时虽然HTML文档已经被完全加载和解析,但是页面上的图片、样式表等外部资源可能还没有加载完毕。因此,在这种状态下直接进行白屏检测,有可能会错过一些重要的视觉变化,导致误判。
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.jsexport default function blankScreen() {let warpperElements = ['html', 'body', '#container', ".content"]let enptyPoints = 0function getSelector(element) {if (element && element.id) {return "#" + element.id} else if (element && element.className) {// a b c => .a .b .creturn "." + 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"><inputid="successBtn"type="button"value="ajax成功请求"onclick="sendSuccess()"/><inputid="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>
标签,就不会出现白屏,没有打印上报日志
不填满<span>
标签,就会出现白屏空袭,就会打印上报日志并且发送给服务端
发送服务端数据
加载时间采集上报
浏览器加载一个网页的整个流程
上面一张图展示了浏览器加载一个网页的整个流程,从开始时间到页面完全加载完成。
流程中字段描述
字段 | 描述 |
---|---|
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 | - |
浏览器加载和渲染网页的整个过程
这张图展示了浏览器加载和渲染网页的整个过程,从请求HTML文件开始,到最终页面渲染完成。
大致上图流程为是一个阶段,下面是对上图中十一个阶段的解读:
- 第一阶段,开始请求
HTML
文件,浏览器接收到用户输入的URL
或者点击链接等操作后,开始向服务器发送HTTP
请求,请求获取HTML
文件。- 第二阶段,响应
HTML
文件,服务器接收到请求后,返回HTML
文件给浏览器。- 第三阶段,开始加载,浏览器接收到
HTML
文件后,开始加载过程。- 第四阶段,构建
DOM
文档对象模型,浏览器使用HTML
解析器解析HTML
文件构建DOM
树,DOM
树是HTML
结构的表示形式,用于描述页面的层次结构,在构建DOM
树的过程中,如果遇到<link>
标签(CSS文件引用)
或<script>
标签(js文件引入)
浏览器会进行预解析,并发起对CSS
文件和JS
文件的请求。- 第五阶段,请求
CSS
文件和JS
文件,浏览器在预解析过程中发现需要的CSS
和JS
文件后,分别向服务器发送请求,获取这些资源文件。- 第六阶段,返回
CSS
数据和JavaScript
,服务器接收到请求后,返回相应的CSS
文件和JS
文件给浏览器。- 第七阶段,构建
CSSOM
也就是CSS
对象模型,浏览器使用CSS解析器
解析返回的CSS
数据,构建CSSOM
树,CSSOM
树包含了所有样式信息,用于描述页面元素的央视。- 第八阶段,执行
JavaScript
,浏览器使用V8
解析器解析并执行返回的JavaScript
代码,javaScript可以修改DOM
和CSSOM
,因此在这个阶段可能会继续构建DOM
树。- 第九阶段,继续构建
DOM
,如果JS
代码中包含了对DOM
的修改操作,浏览器会继续构建和更新DOM
树。
10.第十阶段,构建布局树,当DOM
树和CSSOM
树都构建完成后,浏览器会将他们合并成一个布局树也成为渲染树,布局树包含了所需要的渲染的节点及其央视信息- 第十一阶段,渲染阶段,最后浏览器会根据布局树进行页面渲染,将页面内容展示给用户。
浏览器加载和渲染时间计算上报实现
在讨论浏览器加载网页的过程时候,Performance(性能)
通常指的是web性能,Performance
也是只浏览器提供的一个内置对象,window.performance
通过这个API
,开发者可以获取到详细的页面加载时间和资源加载时间等信息,从而进行性能优化,windong.performance
提供了多个属性和方法来帮助分析网页性能。
属性 | 描述 |
---|---|
navigation | 包含了相关的导航信息,比如页面是如何被加载的,以及设计重定向的次数。 |
timing | 提供了从开始导航到当前页面完全加载过程中各个关键时间的时间点,如重定向时间,DNS 查询时间,TCP 链接时间,请求发送时间,响应接收时间等。 |
打印Performance
如下
第一步、创建/src/monitor/lib/timing.js
文件
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>
第三步、浏览器加载和渲染时间上报效果
性能指标采集上报
性能指标描述
字段 | 全称 | 描述 |
---|---|---|
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(首次输入延迟) | 用户首次和页面交互(单机链接,点击按钮等)到页面响应交互的时间 |
FMP有意义绘制是根据自己给定判断,如: h1.setAttribute("elementtiming", "meaningful");
各个性能指标获取
触发用例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 }) // 用户第一次交互,点击页面
首次点击效果:
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)});
}
查看各项性能指标采集上报效果
卡顿监听上报
监听方案
当任务阻塞主线程达到 100 ms
或更长时间时,将引发诸多问题,例如,可交互时间延迟、严重不稳定的交互行为 (轻击、单击、滚动、滚轮等) 延迟、严重不稳定的事件回调延迟、紊乱的动画和滚动等。我们将任何主 UI
线程连续不间断繁忙 1000
毫秒及以上的时间区间定义为长任务(Long task)
。目前,浏览器中存在一个 Long Tasks API
,借助该 API
与PerformanceObserver
的结合使用,我们能够精准地定位长任务,从而有效实现对卡顿现象的检测,当然还有心跳检测,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>
第三步、查看卡顿上报效果
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);
}
第二步、查看上报结果
完结~
觉得内容有用?别忘了点个赞+收藏+关注,三连走一波!你的支持是我持续输出的动力 💪