最新文章
【前端技术】前端实现监控 SDK 的全面解析(二)
三、功能拆分
(一)初始化
初始化阶段需要获取用户传递过来的相关参数,接着调用初始化函数。在这个初始化函数里,能够注入一些监听事件,以此来达成数据统计的功能。以下是具体的代码实现示例及解析:
javascript
// 初始化配置
function init(options) {
// ------- 加载配置 ----------
loadConfig(options);
}
// 加载配置
export function loadConfig(options) {
const {
appId, // 系统id
userId, // 用户id
reportUrl, // 后端url
autoTracker, // 自动埋点
delay, // 延迟和合并上报的功能
hashPage, // 是否hash录有
errorReport // 是否开启错误监控
} = options;
// --------- appId ----------------
if (appId) {
window['_monitor_app_id_'] = appId;
}
// --------- userId ----------------
if (userId) {
window['_monitor_user_id_'] = userId;
}
// --------- 服务端地址 ----------------
if (reportUrl) {
window['_monitor_report_url_'] = reportUrl;
}
// -------- 合并上报的间隔 ------------
if (delay) {
window['_monitor_delay_'] = delay;
}
// --------- 是否开启错误监控 ------------
if (errorReport) {
errorTrackerReport();
}
// --------- 是否开启无痕埋点 ----------
if (autoTracker) {
autoTrackerReport();
}
// ----------- 路由监听 --------------
if (hashPage) {
hashPageTrackerReport(); // hash路由上报
} else {
historyPageTrackerReport(); // history路由上报
}
}
(二)错误监控
前端错误类型多样,不同类型的错误需要采用不同的捕获方式来实现全面监控。
1. 语法错误
这类错误通常在开发阶段就能被发现,例如拼写错误、符号使用错误等情况。语法错误没办法通过 try{}catch{} 进行捕获,因为正常在开发流程中就会被排查出来,基本不会发布到线上环境。
2. 同步错误
在 JavaScript 同步执行过程中产生的错误属于同步错误,像变量未定义这类情况,此类错误是可以被 try-catch 语句捕获到的。
3. 异步错误
在诸如 setTimeout 等函数执行中出现的错误就是异步错误,它无法被 try-catch 捕获,但可以利用 Window.onerror 来进行捕获处理,相较而言这种方式要比 try-catch 方便许多。
4. Promise 错误
在 Promise 中,如果使用 catch 语句是可以捕获到异步错误的,然而要是没写 catch 的话,在 Window.onerror 里是没办法捕获到这类错误的。对此,可以在全局添加 unhandledrejection 监听来捕获那些没被捕获到的 Promise 错误。
5. 资源加载错误
指的是一些资源文件获取失败的情况,一般通过 Window.addEventListener 来进行捕获操作。
综合来看,SDK 监控错误主要围绕以上这些类型来实现,try-catch 用于在可预见的情形下监控特定错误,Window.onerror 主要负责捕获那些预料之外的错误(比如异步错误),但对于 Promise 错误和网络错误无法单纯依靠它们捕获,所以需要借助 Window.unhandledrejection 监听捕获 Promise 错误,通过 error 监听捕获资源加载错误,以此达成对各类型错误的全面覆盖。
(三)用户埋点统计
埋点是用于监控用户在应用上的各种动作表现的一种手段。
1. 手动埋点
需要手动在代码中添加相关的埋点代码,比如当用户点击某个按钮或者提交一个表单时,就在按钮点击事件以及提交事件中添加对应的埋点代码。其优点在于可控性较强,能够根据需求自定义上报具体的数据内容;但缺点也很明显,就是对业务代码的侵入性较大,如果需要在很多地方进行埋点操作,那就得逐个去添加代码了。
2. 自动埋点
自动埋点很好地解决了手动埋点的缺点,实现了无需侵入业务代码就能在应用里添加埋点监控的功能。不过它也存在不足,只能上报基本的行为交互信息,没办法上报自定义的数据,而且只要页面中有点击操作,就会向服务器上报,这可能导致上报次数过多,给服务器带来较大压力。同时需要注意,如果在 click 事件中阻止了冒泡行为,自动埋点是无法捕获到的,这种情况下就需要进行手动埋点上报,以确保上报的全面覆盖。
(四)PV 统计
PV 即页面浏览量,表示页面被访问的次数。对于非 SPA 页面,只需通过监听 onload 事件就能统计页面的 PV 了。但在 SPA 页面中,路由的切换主要依靠前端来实现,而且单页面切换又分为 hash 路由和 history 路由,这两种路由的实现原理不一样,所以要分别针对它们实现不同的数据采集方式。
1. history 路由
history 路由是依赖全局对象 history 来实现的,它包含诸如 history.back()(返回上一页,对应浏览器回退操作)、history.forward()(前进一页,即浏览器前进操作)、history.go()(跳转历史中某一页)、history.pushState()(添加新记录)、history.replaceState()(修改当前记录)等方法。其中 pushState 和 replaceState 这两个方法不能被 popstate 监听到,因此需要对这两个方法进行重写,并添加自定义事件监听来实现数据采集,以下是具体的代码实现示例及解析:
javascript
import { lazyReport } from './report';
// history路由监听
export function historyPageTrackerReport() {
let beforeTime = Date.now(); // 进入页面的时间
let beforePage = ''; // 上一个页面
// 获取在某个页面的停留时间
function getStayTime() {
let curTime = Date.now();
let stayTime = curTime - beforeTime;
beforeTime = curTime;
return stayTime;
}
// 重写pushState和replaceState方法
const createHistoryEvent = function (name) {
// 拿到原来的处理方法
const origin = window.history[name];
return function(event) {
let res = origin.apply(this, arguments);
let e = new Event(name);
e.arguments = arguments;
window.dispatchEvent(e);
return res;
};
};
// history.pushState
window.addEventListener('pushState', function () {
listener()
});
// history.replaceState
window.addEventListener('replaceState', function () {
listener()
});
window.history.pushState = createHistoryEvent('pushState');
window.history.replaceState = createHistoryEvent('replaceState');
function listener() {
const stayTime = getStayTime(); // 停留时间
const currentPage = window.location.href; // 页面路径
lazyReport('visit', {
stayTime,
page: beforePage,
})
beforePage = currentPage;
}
// 页面load监听
window.addEventListener('load', function () {
// beforePage = location.href;
listener()
});
// unload监听
window.addEventListener('unload', function () {
listener()
});
// history.go()、history.back()、history.forward() 监听
window.addEventListener('popstate', function () {
listener()
});
}
2. hash 路由
在 hash 路由中,url 里的 hash 值发生变化时会触发 hashChange 的监听,所以只需在全局添加一个监听函数,然后在这个函数里实现数据采集上报就可以了。不过在 React 和 Vue 等框架中,hash 路由的跳转有时候是通过 pushState 实现的,所以还需要加上对 pushState 的监听,以下是具体代码示例及解析:
javascript
// hash路由监听
export function hashPageTrackerReport() {
let beforeTime = Date.now(); // 进入页面的时间
let beforePage = ''; // 上一个页面
function getStayTime() {
let curTime = Date.now();
let stayTime = curTime - beforeTime; //当前时间 - 进入时间
beforeTime = curTime;
return stayTime;
}
function listener() {
const stayTime = getStayTime();
const currentPage = window.location.href;
lazyReport('visit', {
stayTime,
page: beforePage,
})
beforePage = currentPage;
}
// hash路由监听
window.addEventListener('hashchange', function () {
listener()
});
// 页面load监听
window.addEventListener('load', function () {
listener()
});
const createHistoryEvent = function (name) {
const origin = window.history[name];
return function(event) {
//自定义事件
let res = origin.apply(this, arguments);
let e = new Event(name);
e.arguments = arguments;
window.dispatchEvent(e);
return res;
};
};
window.history.pushState = createHistoryEvent('pushState');
// history.pushState
window.addEventListener('pushState', function () {
listener()
});
}
(五)UV 统计
UV 统计相对来说较为简单,只需在 SDK 初始化时上报一条消息即可完成相关数据的收集。
四、数据上报方式
(一)xhr 接口请求
采用接口请求的方式来上报数据,其原理和其他业务请求类似,只不过传递的数据是埋点相关的数据。但这种方式存在一些问题,一方面,通常公司里处理埋点的服务器和处理业务逻辑的服务器并非同一台,所以往往需要手动去解决跨域问题;另一方面,如果在上报过程中出现页面刷新或者重新打开页面的情况,很可能会造成埋点数据的缺失,因此传统的 xhr 接口请求方式在适应埋点需求方面存在一定局限性。
(二)img 标签
利用 img 标签上报数据,是将埋点数据伪装成图片 url 的请求形式,这样做的好处是能够避免跨域问题。然而,浏览器对 url 的长度是有限制的,所以这种方式不太适合大数据量的上报,而且同样存在刷新或重新打开页面时数据丢失的问题。
(三)sendBeacon
这种上报方式不会出现跨域问题,也不存在刷新或重新打开页面导致的数据丢失情况,但它有兼容性方面的问题。在日常开发中,通常会采用 sendBeacon 上报和 img 标签上报相结合的方式,以此来兼顾各种情况,确保数据上报的有效性和稳定性。以下是具体的上报函数代码示例:
javascript
// 上报
export function report(data) {
const url = window['_monitor_report_url_'];
// ------- fetch方式上报 -------
// 跨域问题
// fetch(url, {
// method: 'POST',
// body: JSON.stringify(data),
// headers: {
// 'Content-Type': 'application/json',
// },
// }).then(res => {
// console.log(res);
// }).catch(err => {
// console.error(err);
// })
// ------- navigator/img方式上报 -------
// 不会有跨域问题
if (navigator.sendBeacon) { // 支持sendBeacon的浏览器
navigator.sendBeacon(url, JSON.stringify(data));
} else { // 不支持sendBeacon的浏览器
let oImage = new Image();
oImage.src = `${url}?logs=${data}`;
}
clearCache();
}
通过上述对前端实现监控 SDK 的介绍,涵盖了能拆分到数据上报环节,旨在帮助开发人员更好地构建和运用监控系统,以保障前端应用的稳定运行以及对用户行为等方面的有效洞察。