前端监控系统

前端监控

前端监控一般来说需要实现三个方面的监控:
  • 异常监控
  • 性能监控
  • 用户行为监控
前端异常监控和性能监控的重要性不言而喻,做好前端异常监控和性能监控有助于帮助我们追踪异常,突破性能瓶颈。
对于第三点用户行为监控,这并非是窥探用户隐私,而是在用户允许允许的范围内收集用户的匿名行为数据来帮助产品提升用户体验。例如通过分析用户在某界面停留时间和使用人数等信息,来判断是否需要对该页面做进一步的体验优化。
从功能上来说,前端监控系统需要实现以下功能:
  • 捕获异常
  • 捕获性能问题
  • 用户匿名数据收集
  • 监控看板
  • 异常监控报警
  • 任务执行
前端监控的具体方法是埋点+上报。
 

捕获异常

try…catch 和 window.onerror

通过try…catch和window.onerror可以捕获js执行错误,try…catch还可以捕获async函数执行错误。
window.addEventListener("error", ({filename,colno ,lineno ,error,message,path}) => { // console.log(error); console.table({filename ,colno ,lineno ,error:error.stack , message, path}); }); s // 报错 /* (索引) 值 filename 'http://127.0.0.1:5500/err.html' colno 5 lineno 53 error 'ReferenceError: s is not defined\n at http://127.0.0.1:5500/err.html:53:5' message 'Uncaught ReferenceError: s is not defined' */
async function fetchData(){ try{ const res = await getAPI(); }catch{ // 捕获异常 } }

Promise.catch 和 unhandledrejection

Promise.catch用于捕获Promise的异常。但是如果Promise异常没有被Promise.catch捕获,异常是不会传递到window.onerror事件中的而是传递到window.unhandledrejection事件中。
Promise.reject('error').catch(err=>{ console.error(err); })
// 如果Promise异常没有被Promise.catch捕获,异常会传递到window.unhandledrejection事件中 window.addEventListener('unhandledrejection',(err)=>{ console.error(err); }) Promise.reject('error')

捕获接口异常

如果使用axiosjs框架进行前后端交互,那么通过axios的响应拦截器可以很方便地捕获后端接口异常。
如果是原生xhr,可以通过设置xhr.onerror事件回调函数来监听接口异常。
如果是fetch API,则需要进行封装,利用promise.catch进行捕获。

资源加载异常

以img为例,可以直接在img.onerror事件中捕获资源加载异常,但是这种异常不会向上冒泡到window,但是可以在捕获阶段捕获资源异常。
const img = document.querySelector("img"); img.onerror = function (e) { console.log(e); };
直接在img元素上捕获error事件,但是如果img过多,设置多个事件处理程序会导致性能问题。
window.addEventListener('error',function(e){ console.log(e); },{ capture: true })
在window中捕获error,但是必须是在捕获阶段才能捕获到资源加载异常。
可以通过e.target.tagName获取资源类型。

Vue异常捕获

  • errorHandler
  • errorCaptured

app.config.errorHandler

用于为应用内传递的未捕获的错误指定一个全局处理函数。错误处理器接收三个参数:错误对象、触发该错误的组件实例和一个指出错误来源类型信息的字符串。

errorCaptured生命周期

在捕获了后代组件传递的错误时调用。这个钩子带有三个实参:错误对象、触发该错误的组件实例,以及一个说明错误来源类型的信息字符串。这个钩子可以通过返回 false来阻止错误继续传递。

捕获性能问题

通常情况下,前端性能问题会在开发阶段就通过lightHouse等工具进行检测,但是由于生产环境具有设备差异,场景多变等特性,因此在生产时捕获性能问题也非常有必要。

衡量性能的指标

下面是一些重要的前端性能指标:
  • LCP 最大内容绘制
  • FID 首次输入延迟
  • CLS 累积布局偏移
  • FCP 首次内容绘制
  • TTI 可交互时间
  • TBT 总阻塞时间

Chrome API

谷歌浏览器提供专门的API来计算First Paint(首次绘制)时间,计算方式是:
(window.chrome.loadTimes().firstPaintTime - window.chrome.loadTimes().startLoadTime) * 1000
但是很遗憾,这并非浏览器标准,也就是说其他浏览器可能没有实现相同的API。实际开发中不建议使用。

PerformanceTiming

window.performance.timing对象中挂载了许多关键事件的时间缀,可以通过这些时间点之间的计算来获取加载过程的时间。
DNS查询耗时 = domainLookupEnd - domainLookupStart TCP链接耗时 = connectEnd - connectStart request请求耗时 = responseEnd - responseStart 解析dom树耗时 = domComplete - domInteractive 白屏时间 = domContentLoadedEventEnd- fetchStart domready可操作时间 = domContentLoadedEventEnd - fetchStart onload总下载时间 = loadEventEnd - fetchStart
这套API目前已经被废弃,取而代之的是PerformanceEntry API。

PerformanceEntry

新的 W3C 草案及 WICG 提案定义了一系列 PerformanceEntry API,不仅取代了原 PerformanceTiming 的能力,并增加了更多维度的信息,并且每个指标事件都可以使用PerformanceObserver进行监控。
旧的 PerformanceTiming API 返回的是一个 UNIX 类型的绝对时间,和用户的系统时间相关,分析的时候需要再次计算。而新的 PerformanceEntry API,返回的是一个相对时间,可以直接用来分析。
performanceEntry常用的三种方法
  • performance.getEntries() 获取所有时间缀信息
  • performance.getEntriesByName(name,type); 获取指定名称和类型的时间缀信息
  • performance.getEntriesByType(type); 获取指定类型的时间缀信息
除此之外也可以使用PerformanceObserver,这会启动一个Observer,每当监听指标变化时就会执行回调。
const observer = new PerformanceObserver(function (list) { const perfEntries = list.getEntries().forEach((entry) => { console.log(entry); }); }); observer.observe ( {entryTypes: [ ...类型 ] } )
PerformanceResource API(资源加载时间)
通过window.performance.getEntriesByType('resource') 获取页面中的资源信息,可以通过initiatorType 属性判断资源类型,例如img、css、xhr和navigator等,然后通过duration属性获取加载时间。
setTimeout(() => { window.performance.getEntriesByType("resource").forEach((item) => { const { name, duration, initiatorType } = item; console.log({ name, // 资源url duration, // 加载时间 initiatorType, // 资源类型 }); }); }, 1000);
同时也可以performanceObserver
const observer = new PerformanceObserver(function (list) { const perfEntries = list.getEntries().forEach((entry) => { console.log(entry); }); }); observer.observe ( {entryTypes: [ "resource" ] } )
PerformancePaintTiming(FP)
window.performance.timing和window.chrome这两种方法要不就是只能粗略计算要不就是不具有普遍性,一种更好的方法是使用PerformanceObserver,这种方法不仅兼容性好,还可以获取三种类型的信息:
  • FP: First Paint,即首次绘制
  • FCP: First Contentful Paint,首次内容绘制 (FCP) 指标测量页面从开始加载到页面内容的任何部分在屏幕上完成渲染的时间。对于该指标,"内容"指的是文本、图像(包括背景图像)、<svg>元素或非白色的<canvas>元素。
  • FMP: First Meaningful Paint,即首次有意义的绘制,测量页面的主要内容对用户可见的情况。
使用方法类似于InterSectionObserver,注意observe需要传入一个包含entryTypes属性的对象。
var a = new PerformanceObserver((entries=>{entries.getEntries()?.forEach((item)=>{ console.log(`${item.name}:${item.startTime + item.duration}`); })})) a.observe({ entryTypes: ['paint'] }) // first-paint:1118.7000000476837 // first-contentful-paint:1118.7000000476837
PerformanceElementtming API(LCP)
前面的First Paint方法只能获取FP、FCP和FMP,无法获取到LCP,但是往往最大内容绘制(LCP)更能表示首屏加载情况,这时候我们可以用Elementtming API来获取LCP。
首先在需要测量的元素(通常是首屏最大块元素)上加上elementtiming属性,值是测量标签名,然后使用performanceObserver进行监测。
该 API 支持的元素有:
  1. <img>元素
  1. <image> 内部的<svg>
  1. <video>
  1. 具有background-image的元素
  1. 文本节点组,如 <p>.
示例
const observer = new PerformanceObserver((list) => { let entries = list.getEntries().forEach((entry) => { console.log(entry); }); }); observer.observe({ entryTypes: ["element"] });
PerformanceEventtiming(FID)
PerformanceEventTiming 记录了某个事件的相关信息,具体字段如下:
  • name:保存的是 event.type 字段,表示一个事件的类型,例如 touchstart、touchmove
  • entryType:表示 entry 的类型,这里是一个常量 "event"
  • startTime:保存 event.timeStamp 字段,记录了事件生成时的时间戳
  • processingStart:记录内核开始处理事件的时间点
  • processingEnd:记录内核处理完事件的时间点
  • duration:在处理完事件时,通过 Math.ceil((performance.now() - newEntry.startTime)/8) * 8 计算,这里的处理主要是为了减少时间精度,增加安全性
  • cancelable:保存 event.cancelable 字段,表示事件是否可以被取消
  • Performance 接口只有一个 eventCounts 属性,它记录了所有已经分发过的 Event,处理时间是否大于 50ms
使用示例
const observer = new PerformanceObserver(function (list) { const perfEntries = list.getEntries().forEach((entry) => { console.log(entry); // 全程 const inputDuration = entry.duration; // 输入延迟(处理事件之前) const inputDelay = entry.processingStart - entry.startTime; // 同步事件处理时间(在开始和结束分发之间)。 const inputSyncProcessingTime = entry.processingEnd - entry.processingStart; }); }); // // 注册事件观察者。 observer.observe({entryTypes:['event']});
获取首次输入延迟(FID)
observer.observe({ type: 'first-input', buffered: true, });
User Timing API(任务执行时间)
User Timing API 是基于时间的通用度量 API,你可以使用它任意标记时间点,然后测量这些标记之间持续的时间。
可以使用该API测量某些行为的执行时间。
function wait() { return new Promise((resolve) => { setTimeout(() => { resolve() }, 1000) }) } async function fn() { performance.mark('myTask:start'); await wait() // 记录任务运行后的时间。 performance.mark('myTask:end'); // 测量任务开始和结束之间的增量 console.log(performance.measure('myTask', 'myTask:start', 'myTask:end')); }
和其他performanceEntry API一样,这个事件也可以使用PerformanceObserver。
const observer = new PerformanceObserver((list)=>{ list.getEntries().forEach(item=>{ console.log(item); }) }) observer.observe({ entryTypes: ['measure'] }) setTimeout(()=>{ let a = 0 performance.mark('hello:start') for(let i=0; i<100000000;i++){a++} performance.mark('hello:end') performance.measure('hello', 'hello:start', 'hello:end') },0)
PerformanceLongtask API(事件循环)
页面卡顿情况与事件循环息息相关,如果在一个事件循环周期中有大量或长时间的任务占用主线程,那么就会导致浏览器无法及时的重新渲染,如果每次渲染时间间隔大于60ms,就容易出现卡顿的情况,因此可以通过PerformanceLongtask API来监控事件循环的执行时长。
const observer = new PerformanceObserver((list)=>{ list.getEntries().forEach(item=>{ console.log(item); }) }) observer.observe({ entryTypes: ['longtask'] }) // 假设有长时间的任务 setTimeout(()=>{ let a = 0 for(let i=0; i<100000000;i++){a++} },0)

用户匿名数据收集

页面打开次数

页面中添加一张display:none 或者宽高为0的图片,图片src为统计页面打开次数的接口。每次打开就发送信息给后端。

路由跳转

现代路由框架(例如react-router和vue-router)通常有三种模式:
  • hash
  • history
  • memeory
在history模式下使用HTML5 historyAPI的pushStatereplaceState来改变路由的,可以使用 window.onpopstate 监听 HTML5 History API 的状态变化的示例代码如下:
javascript复制代码 window.onpopstate = function(event) { console.log("State changed: " + event.state); };
hash模式下可以使用 window.onhashchange 监听 URL 中 hash 值变化的示例代码如下:
javascript复制代码 window.onhashchange = function() { console.log("Hash changed: " + location.hash); };
momery模式是框架自身维护了一套历史记录栈,可以利用框架自身的API实现监听,例如在vue-router中设置全局前置守卫。

用户点击

监听document的click事件,并上报。

页面停留时间

要收集页面停留时间,可以在页面加载时记录当前时间戳,并在用户离开页面时再记录一次时间戳,计算两个时间戳之间的差值即为页面停留时间。注意这种方法只能粗略地判断页面停留时长,不考虑用户是否将浏览器窗口最小化或切换到其他标签页等情况。示例代码如下:
let startTime = Date.now(); window.addEventListener("unload", function() { let endTime = Date.now(); let duration = endTime - startTime; console.log("Page stay time: " + duration + "ms"); });

浏览器信息收集

window.navigator.userAgent可以获取浏览器型号

页面来源

通过document.referrer 可以获取当前页面来源网站。
也可以通过html文档http请求的referrer获取。

数据上报

数据上报有两种方式实现:
方案
优点
缺点
img
1.可以跨域 2. 兼容性好
1. 部分浏览器会丢点,延迟页面卸载 2. 只支持GET方法,url长度有限制
xhr或fetch
1. 兼容性好 2. 可定制性强
1. 需要解决跨域问题。 2. fetch丢点,同步xhr不丢点,但是会延迟页面卸载
navigator.sendBeacon
1. 不丢点 2. 不延迟页面卸载
1. 兼容性问题
丢点:在浏览器点击跳转时,跳转前的点击上报请求都会进行一个三次握手,如果此时,网络较慢、服务器运行缓慢或者上报请求还在处理阶段,这时,如果页面被卸载了,浏览器都会自动对当前的请求进行abort。这样,这个http的请求就没有建立,导致上报没有真正发出。

前端容灾

前端容灾是指当后端由于各种原因挂了之后,前端仍然能够将页面正常显示出来。
主要有两种方法
  • CDN
  • LocalStorage
CDN会将静态资源保存到缓存服务器中,即使源服务器挂了,但是缓存服务器仍然能够在有效期内提供静态资源到客户端,
LocalStorage能够实现客户端本地存储,并且最大能存储10MB左右的内容,在一些重要的页面中,在正常的时候可以将一些数据保存到LocalStorage中,以便当出现接口异常时能紧急恢复页面。(例如:课表小程序的课表,一般会将数据存一份到storage中)
 

埋点

前端埋点有三类:
  • 代码埋点(手动埋点)
  • 可视化埋点
  • 全埋点

代码埋点

代码埋点是指在代码中手动埋点,比如在try…catch中捕获异常并上报。
优点:控制精准,可定制性强
缺点:工作量大,更新成本高

可视化埋点

可视化埋点顾名思义是使用可视化根据去进行埋点。
优点: 操作简便,工作量小。
缺点:无法专门定制

无痕埋点

监听异常事件,将所有异常都发送到后端。
优点:实现方便简单
缺点:无法定制,服务器压力大
 

参考