前言
一套完整的前端监控系统的主要部分:
- 数据上报方式
- 数据上送时机
- 性能数据采集
- 错误数据采集
- 用户行为采集
- 定制化指标
- 监控sdk
监控的目的:
一、数据上报方式
本文的方案是,优先navigator.sendBeacon,降级使用1x1像素gif图片,根据实际情况需要采用xhr/fetch。
1、图片打点
图片打点上报的优势:
1)支持跨域,一般而言,上报域名都不是当前域名,上报的接口请求会构成跨域
2)体积小且不需要插入dom中(相比之下,script、link要挂载到页面上才可以请求)
3)不需要等待服务器返回数据
图片打点缺点是:
1)url受浏览器长度限制
2)只能发送GET请求,无法获取相应结果
通过创建一个Image对象,将要上报的数据作为URL参数拼接到一个1x1像素的透明图片URL中,发送一个GET请求来触发上报。
const data = {
event: 'click', element: 'button' };
const url = ` https://example.com/track?data= ${
encodeURIComponent(JSON.stringify(data))}`;
const img = new Image();
img.src = url;
2、fetch请求上报
这类方法用于业务数据确实,模块未展示,需要紧急上报的情况
优点:可以灵活地设置请求头属性,post请求可以发送大体量数据,满足特定场景的埋点需求。
缺点:数据量大的请求占用带宽资源多,增加服务器压力。页面销毁时的监控埋点大概率上报失败。
3、sendBeacon
navigator.sendBeacon是一个用于发送少量数据到服务器的浏览器API。它有以下几个优点
-
异步和非阻塞
navigator.sendBeacon 是异步的,它不会阻塞浏览器的其他操作。这对于性能监控来说非常重要,因为都不希望监控的过程影响到页面的性能。
-
在页面卸载时仍然可以发送数据
当用户离开页面(例如关闭页面或者导航到其他页面)时,navigator.sendBeacon仍然可以发送数据。这对于捕获和上报页面卸载前的最后一些性能数据来说非常有用。
-
低优先级
navigator.sendBeacon 发送的请求是低优先级的,它不会影响到页面的其他网络请求。
-
简单易用
navigator.sendBeacon 的API非常简单,只需要提供上报的URL和数据,就可以发送请求。
与此同时,navigator.sendBeacon 也有一些限制。例如,它只能发送POST请求,不能发送GET请求。而且,它发送的请求没有返回值,不能接收服务器的响应。
最后,一些旧的浏览器可能不支持 navigator.sendBeacon。因此,在使用 navigator.sendBeacon 时,需要根据实际情况进行兼容性处理。
navigator.sendBeacon()
可以在用户离开页面(包括关闭浏览器窗口、标签页,或者在移动设备上切换应用等情况)时进行数据上报。
navigator.sendBeacon()
的设计目的是在页面卸载时异步发送数据,尽可能确保数据能够被发送出去而不影响页面的卸载流程。然而,当应用被强制终止时,操作系统可能会立即终止应用的所有进程,不给应用任何机会执行数据上报操作。
以下是一个使用navigator.sendBeacon()
的示例代码:
function onPageUnload() {
const data = {
/* 要上报的数据 */ };
const url = '/data-collection-endpoint';
navigator.sendBeacon(url, JSON.stringify(data));
}
window.addEventListener('unload', onPageUnload);
由于兼容问题,一些旧的浏览器可能不支持 navigator.sendBeacon。因此,在使用 navigator.sendBeacon 时,需要根据实际情况进行兼容性处理:
import {
isSupportSendBeacon} from './util'
// 如果浏览器不支持 sendBeacon,就使用图片打点
const sendBeacon = (function(){
if(isSupportSendBeacon()){
return window.navigator.sendBeacon.bind(window.navigator)
}
const reportImageBeacon = function(url, data){
reportImage(url, data)
}
return reportImageBeacon
})()
export function reportImage(url, data) {
const img = new Image();
img.src = url + '?reportData=' + encodeURIComponent(JSON.stringify(data));
}
三、数据上报时机
1、上报时机有三种:
- 采用 requestIdleCallback/setTimeout 延时上报。
- 在 beforeunload 回调函数里上报。
- 缓存上报数据,达到一定数量后再上报。
将三种方式结合一起上报:
先缓存上报数据,缓存到一定数量后,利用 requestIdleCallback/setTimeout
延时上报。在页面离开时统一将未上报的数据进行上报。
2、还有一种情况:
用户杀掉app进程时
将数据暂存在localStorage
中,在用户下次进入应用时检查并上报是一种可行的方法,业内也有这样做的情况。
以下是一个示例代码实现:
// 上报数据的函数
function reportData(data) {
// 发送数据到服务器的逻辑,这里只是示例
fetch('/reporting-endpoint', {
method: 'POST',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json',
},
})
.then(response => response.json())
.then(result => {
console.log('数据上报成功:', result);
// 上报成功后可以从 localStorage 中删除数据
localStorage.removeItem('pendingData');
})
.catch(error => {
console.error('数据上报失败:', error);
// 上报失败可以考虑再次尝试或者在下次进入时继续尝试
});
}
// 在合适的时机尝试上报 localStorage 中的数据
function checkAndReportPendingData() {
const pendingData = localStorage.getItem('pendingData');
if (pendingData) {
reportData(JSON.parse(pendingData));
}
}
// 在某个事件触发时(比如页面加载)调用检查函数
window.addEventListener('load', checkAndReportPendingData);
// 在需要上报数据时,先将数据存入 localStorage
function saveDataForLaterReporting(data) {
localStorage.setItem('pendingData', JSON.stringify(data));
}
这种方法的优点是可以在一定程度上提高数据上报的成功率,尤其是对于一些关键数据,即使在用户意外退出应用的情况下也有机会在下次进入时上报。
然而,这种方法也有一些局限性:
- 如果用户长时间不使用应用或者清除了浏览器缓存(包括
localStorage
数据),那么数据可能无法上报。 - 如果数据量较大,存储在
localStorage
中可能会占用较多的存储空间。 - 需要考虑数据的安全性和隐私性,确保存储在
localStorage
中的数据不会被恶意获取。
四、性能数据收集上报
以Spa页面来说,页面的加载过程大致是这样的:
包括dns查询、建立tcp连接、发送http请求、返回html文档、html文档解析等阶段
最初,可以通过 window.performance.timing 来获取加载过程模型中各个阶段的耗时数据,后来 window.performance.timing 被废弃
,通过 PerformanceObserver
来获取。旧的 api,返回的是一个 UNIX 类型的绝对时间,和用户的系统时间相关,分析的时候需要再次计算。而新的 api,返回的是一个相对时间,可以直接用来分析。
根据最初的规划,性能监控需要收集的数据指标需要有FP、FCP、LCP、DOMContentLoaded、onload、资源加载时间、接口请求时间。
收集FP、FCP、LCP、资源加载时间具体是利用浏览器Performance API。关于Performance API:
// window.performance.timing 各字段说明
{
navigationStart, // 同一个浏览器上下文中,上一个文档结束时的时间戳。如果没有上一个文档,这个值会和 fetchStart 相同。
unloadEventStart, // 上一个文档 unload 事件触发时的时间戳。如果没有上一个文档,为 0。
unloadEventEnd, // 上一个文档 unload 事件结束时的时间戳。如果没有上一个文档,为 0。
redirectStart, // 表示第一个 http 重定向开始时的时间戳。如果没有重定向或者有一个非同源的重定向,为 0。
redirectEnd, // 表示最后一个 http 重定向结束时的时间戳。如果没有重定向或者有一个非同源的重定向,为 0。
fetchStart, // 表示浏览器准备好使用 http 请求来获取文档的时间戳。这个时间点会在检查任何缓存之前。
domainLookupStart, // 域名查询开始的时间戳。如果使用了持久连接或者本地有缓存,这个值会和 fetchStart 相同。
domainLookupEnd, // 域名查询结束的时间戳。如果使用了持久连接或者本地有缓存,这个值会和 fetchStart 相同。
connectStart, // http 请求向服务器发送连接请求时的时间戳。如果使用了持久连接,这个值会和 fetchStart 相同。
connectEnd, // 浏览器和服务器之前建立连接的时间戳,所有握手和认证过程全部结束。如果使用了持久连接,这个值会和 fetchStart 相同。
secureConnectionStart, // 浏览器与服务器开始安全链接的握手时的时间戳。如果当前网页不要求安全连接,返回 0。
requestStart, // 浏览器向服务器发起 http 请求(或者读取本地缓存)时的时间戳,即获取 html 文档。
responseStart, // 浏览器从服务器接收到第一个字节时的时间戳。
responseEnd, // 浏览器从服务器接受到最后一个字节时的时间戳。
domLoading, // dom 结构开始解析的时间戳,document.readyState 的值为 loading。
domInteractive, // dom 结构解析结束,开始加载内嵌资源的时间戳,document.readyState 的状态为 interactive。
domContentLoadedEventStart, // DOMContentLoaded 事件触发时的时间戳,所有需要执行的脚本执行完毕。
domContentLoadedEventEnd, // DOMContentLoaded 事件结束时的时间戳
domComplete, // dom 文档完成解析的时间戳, document.readyState 的值为 complete。
loadEventStart, // load 事件触发的时间。
loadEventEnd // load 时间结束时的时间。
}
1、收集上报FP;
FP(First Paint)首次绘制,即浏览器开始绘制页面的时间点。这包括了任何用户自定义的绘制,它是渲染任何文本、图像、SVG等的开始时间
import {
getPageURL, isSupportPerformanceObserver } from '../utils/util'
import {
lazyReportCache } from '../utils/report'
export default function observePaint() {
if (