前端监控系统
监控什么
背景
项目某核心页面加载较慢、交互卡顿、实际使用中错误过多
性能监测
针对页面加载较慢,选择监测 FCP,FP
针对交互卡顿,监测用户点击按钮后页面的响应时间
FCP(First Contentful Paint):FCP是指页面上首次渲染任何文本、图像、非空白的canvas或SVG的时间点。它表示了用户首次看到页面有实际内容的时间,即页面开始呈现有意义的内容的时间点。 FP(First Paint):FP是指页面上首次渲染任何内容的时间点,包括背景颜色、图片、文本等。它表示了页面开始呈现任何可视化内容的时间,但不一定是有意义的内容。 简而言之,FCP 关注的是页面上首次呈现有意义内容的时间点,而 FP 关注的是页面上首次呈现任何可视化内容的时间点。FCP 更关注用户感知的页面加载时间,因为它表示用户可以开始阅读或与页面进行交互的时间点。而 FP 则更关注页面开始渲染的时间点,无论内容是否有意义
错误监控
主要是 js 报错
如何监控
性能监测
使用 PerformanceObserver
监控 FCP 与 FP
// src/monitor/performHandler.js
import { isSupportPerformanceObserver } from './utils'
import { reportData } from './report'
export default function () {
if (!isSupportPerformanceObserver()) return
const entryHandler = (list) => {
for (const entry of list.getEntries()) {
// 当收集到 first-contentful-paint 时就可以断开观察了
if (entry.name === 'first-contentful-paint') {
observer.disconnect()
}
const json = entry.toJSON()
delete json.duration
const data = {
...json,
subType: entry.name,
type: 'performance',
pageURL: window.location.href,
}
reportData(data)
}
}
const observer = new PerformanceObserver(entryHandler)
// type 为 paint 包含两种性能指标:first-contentful-paint 和 first-paint
// buffered 属性表示是否观察缓存数据,也就是说观察回调添加的时机比事件触发时机晚也没关系
observer.observe({ type: 'paint', buffered: true })
}
// src/main.jsx
import performHandler from './monitor/performHandler.js'
performHandler()
检测用户操作响应时间,主要是计算从用户点击开始到页面重新渲染的时间。为了减少代码侵入性,将监控操作响应的时间封装为 useClickPerform
hook
// src/monitor/useClickPerform.js
import { useState, useEffect } from 'react'
import { reportData } from './report'
export const useClickPerform = () => {
const [startTime, setStartTime] = useState(null)
const [renderTime, setRenderTime] = useState(0)
// startTime 更新后,useEffect 调用时说明页面已经重新渲染结束了
// 在这里计算 renderTime 即可
useEffect(() => {
if (startTime) {
// 提供高精度时间测量,精确到毫秒的小数点后几位,适合性能监控
const time = performance.now() - startTime
setRenderTime(time)
}
// 卸载时重置 startTime
return () => {
setStartTime(null)
}
}, [startTime])
// renderTime 更新后上报数据
useEffect(() => {
if (renderTime > 0) {
reportData({
subType: 'click res',
type: 'performance',
pageURL: window.location.href,
renderTime
})
}
}, [renderTime])
return {
setStartTime,
renderTime
}
}
// App.jsx
import { Button } from 'antd'
import { useClickPerform } from './monitor/useClickPerform';
function App() {
const { setStartTime, renderTime } = useClickPerform()
const handleClick = () => {
setStartTime(performance.now())
// 业务逻辑...
}
return (
<>
<Button onClick={handleClick}>耗时操作 {renderTime}ms</Button>
</>
);
};
export default Appx
错误监控
React 提供的 ErrorBoundary
只能用 class 组件,因为内部依赖 componentDidCatch 生命周期方法,而且不能捕获回调与异步错误
因此统一改为 onerror
与 unhandledrejection
来捕获全局 js 错误
// src/monitor/errorHandler.js
import { reportData } from "./report"
export default function () {
// 监听 promise 错误
window.addEventListener('unhandledrejection', (e) => {
reportData({
reason: e.reason?.stack,
subType: 'promise',
type: 'error',
startTime: e.timeStamp,
pageURL: window.location.href,
})
})
// 监听 js 错误
window.onerror = (msg, url, line, column, error) => {
reportData({
msg,
line,
column,
error: error.stack,
subType: 'js',
pageURL: url,
type: 'error',
startTime: performance.now(),
})
}
}
// src/main.jsx
import errorHandler from './monitor/errorHandler.js'
errorHandler()
附上监听文件加载错误的代码(本项目用不到)
// 捕获资源加载失败错误 js css img...
window.addEventListener('error', e => {
const target = e.target
if (!target) return
if (target.src || target.href) {
const url = target.src || target.href
reportData({
url,
type: 'error',
subType: 'resource',
startTime: e.timeStamp,
html: target.outerHTML,
resourceType: target.tagName,
paths: e.path.map(item => item.tagName).filter(Boolean),
pageURL: window.location.href,
})
}
}, true)
如何收集
本项目不涉及到错误缓存,下文仅提供思路
- 全局变量
// cache.js
import { deepCopy } from './util'
const cache = []
export function getCache() {
return deepCopy(cache)
}
export function addCache(data) {
cache.push(data)
}
export function clearCache() {
cache.length = 0
}
// 缓存并延时上报
let timer = null
export function lazyReportCache(data, timeout = 3000) {
addCache(data)
clearTimeout(timer)
timer = setTimeout(() => {
const data = getCache()
if (data.length) {
report(data)
clearCache()
}
}, timeout)
}
- 浏览器缓存(SS/LS)也可以
如何上报
图片打点上报,利用 img
元素的 src
属性发起 GET 请求,将数据拼接到 url 中
(new Image).src = url + '?reportData=' + encodeURIComponent(JSON.stringify(data));
无需考虑跨域问题,不需要等待服务器返回数据。但缺点就是 url 长度受浏览器的限制
所以使用 navigation.sendBeacon
,用于发送少量(统计)数据到服务器,具有异步非阻塞、离开页面、低优先级等优点
// src/monitor/report.js
const sendBeacon = (function () {
if (isSupportSendBeacon()) {
return window.navigator.sendBeacon.bind(window.navigator)
}
const reportImageBeacon = function (data) {
reportImage(data)
}
return reportImageBeacon
})()
function reportImage(data) {
const img = new Image();
img.src = url + '?reportData=' + encodeURIComponent(JSON.stringify(data));
}
何时上报
本项目并没有集成缓存的方案,因此采用立即上报的方式,利用 requestIdleCallback
或 setTimeout 在浏览器空闲时上报数据即可
// src/monitor/report.js
export function reportData(data, isImmediate = false) {
const reportData = JSON.stringify({
id: getId(),
data,
})
if (isImmediate) {
sendBeacon(url, reportData)
return
}
if (window.requestIdleCallback) {
window.requestIdleCallback(() => {
sendBeacon(url, reportData)
}, { timeout: 3000 })
} else {
setTimeout(() => {
sendBeacon(url, reportData)
})
}
}
如果使用了缓存,可以当缓存到达一定空间时上报,也可以在 visibilitychange === hidden
时统一上报(考虑到移动端,不建议使用 beforeunload
事件)
附 Utils
import { v4 as uuidv4 } from 'uuid';
export function isSupportPerformanceObserver() {
return !!window.PerformanceObserver
}
export function isSupportSendBeacon() {
return !!window.navigator?.sendBeacon
}
export function getId() {
return uuidv4()
}
附单例模式管理数据
export class Cache {
static instance = null;
static getInstance() {
if (ErrorHandler.instance == null) {
ErrorHandler.instance = new ErrorHandler();
}
return ErrorHandler.instance;
}
}
export default Cache.getInstance()