前端监控 SDK 开发 | 青训营笔记
这是我参与「第五届青训营」伴学笔记创作活动的第 12 天
0x1 前端监控
-
什么是前端监控
前端监控就是尽可能的采集从输入 URL 到页面展示这一过程以及后续用户交互中产出的性能指标与发生的异常事件并上报到平台完成消费
-
为什么需要前端监控
前端监控通过对页面数据的采集和上报,来帮助开发者快速地对质量差的页面进行分析与归因
- 加载慢:页面某个关键资源渲染太慢
- 交互卡:页面同步计算任务多,阻塞渲染
- 资源加载失败:客户端网络状态差,或上游服务节点异常
- 页面白屏:页面脚本执行失败,或关键资源加载失败,或请求失败
-
监控了什么
- 性能指标
- 异常事件
- 用户行为
0x2 常用性能指标
-
Web 性能标准
2010年始,由 W3C 成立的 Web 性能工作组开始调研和制定
-
传统的性能指标
主要依赖 Navigation Timing 或 Navigation Timing 2,通过记录一个文档从发起请求到加载完毕的各阶段的性能耗时,以加载速度来衡量性能
-
以用户为中心的性能指标
专注于用户视角下的浏览体验
用户体验 指标 发生了吗 FP(First Paint), FCP(First Contentful Paint) 内容有用吗 FMP(First Meaningful Paint), SI(Speed Index), LCP(Largest Contentful Paint) 内容可用吗 TTI(Time to Interactive), TBT(Total Blocking Time) 令人愉悦吗 FID(First Input Delay), CLS(Cumulative Layout Shift) - FP:首次渲染的时间点
- FCP:首次有内容渲染的时间点
- FMP:首次绘制有意义内容的时间点
- SI:衡量页面可视区域加载速度,帮助检测页面的加载体验差异
- LCP:最大的内容在可视区域内变得可见的时间点
- TTI:测量页面从开始加载到主要子资源完成渲染,并能够快速可靠地响应用户输入所需的时间
- TBT:量化主线程在空闲之前的繁忙程度,有助于理解在加载期间,页面无法响应用户输入的时间有多久
- FID:测量从用户第一次与页面交互直至浏览器对交互做出响应并实际能够开始处理事件时,处理程序所经过的时间
- CLS:量化了在页面加载期间,视口中元素的移动程度
0x3 前端常见异常
-
静态资源错误
静态资源:加载页面所需的 HTML、CSS、JS 等文件,以及其他各类多媒体文件
在拉取和加载静态资源过程中发生了预期之外的错误,如网络延迟等,导致静态资源无法正常渲染到页面上
-
请求异常
请求异常 = 请求响应状态码 >= 400
对于通过异步请求拉取的静态资源错误也可选择归类到请求异常
-
JS 错误
在页面运行时发生的 JS 错误会严重影响页面的正常渲染和交互
-
白屏异常
通常可以通过判断 DOM 树的结构来粗略的判断白屏是否发生
白屏异常原因:
- 发生 JS 错误导致关键资源渲染失败
- 请求异常或静态资源加载失败
- 长时间的 JS 线程繁忙阻塞渲染任务
0x4 监控前端性能与异常实践
-
性能指标监控
利用 Performance 和 PerformanceObserver 可以监控到一些标准的渲染性数据
-
通过 PerformanceObserver 监听
/* * 列举出性能指标对应的 entry type * FP/FCP --> paint * LCP --> largest-contentful-paint * FIP --> first-input */ const entryTypes = ['paint', 'largest-contentful-paint', 'first-input'] const p = new PerformanceObserver(list => { for(const entry of list.getEntries()) { console.log(entry) } }) p.observe({entryTypes})
-
通过 Performance 对象读取性能指标
window.performance.getEntriesByType('paint') window.performance.getEntriesByType('first-input')
-
封装成一个 monitor
function createrPerfMonitor(report:({ name: string, data: any }) => void) { const name = "performance" const entryTypes = ['paint', 'largest-contentful-paint', 'first-input'] function start() { const p = new PerformanceObserver(list => { for(const entry of list.getEntries()) { // console.log(entry) report({ name, data: entry }) } }) p.observe({entryTypes}) } return { name, start } }
-
-
JS 错误监控
利用window.addEventListener的 error 和 unhandledrejection 可以监控到全局的 JS 错误
-
监听 JS 执行报错
window.addEventListener("error", (e) => { if(e.error) { console.log(e.error) } })
-
监听 Promise rejection
window.addEventListener("unhandledrejection", (e) => { console.log(e) })
-
封装成一个 monitor
function createJsErrorMonitor(report: ({ name: string, data: any }) => void) { const name = "js-error" function start() { window.addEventListener("error", (e) => { if(e.error) { report({ name, data: { type: e.type, message: e.message } }) } }) window.addEventListener("unhandledrejection", (e) => { report({ name, data: { type: e.type, reason: e.reason } }) }) } return { name, start } }
-
-
静态资源错误监控
利用window.addEventListener的 error 事件可以监控到静态资源错误
-
监控静态资源错误(需要在捕获阶段才能监听到)
window.addEventListener('error', (e) => { const target = e.target || e.srcElement if (!target) { return } if (target instanceof HTMLElement) { let url if (target.tagName.toLowerCase() === 'link') { url = target.getAttribute('href') } else { url = target.getAttribute('src') } console.log('Warning', url) } }, true)
-
封装成一个 monitor
function createResourceErrorMonitor(report: ({ name: string, data: any }) => void) { const name = "resource-error" function start() { window.addEventListener('error', e => { const target = e.target || e.srcElement if (!target) { return } if (target instanceof HTMLElement) { let url if (target.tagName.toLowerCase() === 'link') { url = target.getAttribute('href') } else { url = target.getAttribute('src') } report({ name: name, data: { url } }) } }, true) } return { name, start } }
-
-
请求异常监控
通过 hook xhr 和 fetch 对象来监听请求时发生的错误
-
简易的 hook 函数
function hookMethod( obj: any, key: string, hookFunc: Function ) { return (...params: any[]) => { obj[key] = hookFunction(obj[key], ...params) } }
-
hook xhr 对象的 open 方法拿到请求地址和方法
hookMethod(XMLHttpRequest.protype, 'open', (origin: Function) => function(this, method: string, url: string) { this.payload = { method, url } origin.apply(this, [method, url]) })() const xhr = new XMLHttpRequest() xhr.open("post", "example.com")
-
hook xhr 对象的 send 方法监听到错误的请求
hookMethod(XMLHttpRequest.protype, 'open', (origin: Function) => function(this, ...params: any[]) { this.addEventListener("readystatechange", function() { if(this.readyState === 4 && this.status >= 400) { this.payload.status = this.status console.log(this.payload) } }) origin.apply(this, ...params) })() const xhr = new XMLHttpRequest() xhr.send()
-
0x5 封装一个通用的 SDK
-
前端监控流程概述
SDK 主要完成前两步,后两步需要后端服务和平台的支持
-
数据上报
封装一个用于给监控器上报已收集数据的上报函数,如:
report: ({ name: string, data: any }) => void
为实现
report()
的具体上报功能,可使用专为前端监控打造的请求函数Navigator.sendBeacon() -
按需加载监控能力
function createSDK(url: string) { const monitors: Array<{ name: string, start: Function }> = [] const sdk = { url, report, loadMonitor, monitors, start } function report({ name: string, data: any }) { navigator.sendBeacon(url, JSON.stringify({ name: string, data: any })) } function loadMonitor({ name: string, start: Function }) { monitors.push({ name: string, start: Function }) return sdk } function start() { monitors.forEach(m => m.start()) } return sdk } const sdk = createSDK("example.com") const jsMonitor = createJsErrorMonitor(createPerfMonitor(sdk.report)) sdk.start()
0x6 强化 SDK
-
实现更多性能指标计算算法,如 FMP、CLS、TTI 等
-
除了请求异常外,还需关注慢请求等
对于慢请求,可参阅 Performance Resource Timing 来获取请求各阶段的耗时,找到慢请求
-
上述例子中的 hook 函数缺失了关键的 unhook 能力(即还原被 hook 的函数),应当使用更安全和稳定的 Hook 函数
-
添加用户行为监控