本文详细解析前端监控SDK的核心实现,涵盖错误监控、用户行为埋点及PV/UV统计三大模块。深入探讨同步、异步、Promise及资源加载错误的捕获机制,对比手动与自动埋点的优劣,并分析history与hash路由下的数据采集方案。最后总结xhr、img标签及sendBeacon等数据上报技术的适用场景与兼容性处理,为构建稳定高效的前端监控系统提供完整技术指南。
前端监控SDK的核心功能主要围绕监控内容展开。
- 首先是错误监控,旨在捕捉浏览器兼容性问题、代码Bug以及后端接口异常等影响用户体验的因素。
- 其次是行为日志分析,以电商App为例,通过追踪用户停留时间较长的页面及高频点击按钮,分析用户行为以制定个性化引导策略,促进购买转化。
- 最后是PV/UV统计,用于量化用户访问规模,包括统计页面访问次数及每日独立访客数量。
基于上述三大核心功能,监控SDK的整体设计流程如下:

数据采集阶段,负责收集前端监控所需的关键数据,涵盖PV/UV指标、用户行为轨迹以及各类报错信息。
日志上报阶段,将采集到的原始数据发送至服务端进行存储与处理。
日志查询阶段,通过后台管理界面查询已采集的数据,支持系统层面的深度分析与可视化展示。
为实现上述功能,SDK需进行功能模块拆分。

初始化模块是SDK运行的基础。
该模块负责接收用户传递的配置参数,并调用初始化函数。在初始化过程中,可注入各类监听事件,从而启动数据统计功能。
/**
* 初始化配置
* @param {*} options 配置信息
*/
function init(options) {
// ------- 加载配置 ----------
loadConfig(options);
}/**
* 加载配置
* @param {*} 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路由上报
}
}
错误监控模块是保障用户体验的关键环节。
前端直接面向用户,页面报错会严重损害用户体验。即便经过充分测试并上线,仍可能因用户操作习惯或环境差异产生各类错误。因此,除了后端报警监控外,前端错误监控同样至关重要。
前端错误主要分为以下几种类型:
- 语法错误类型分析。
语法错误通常在开发阶段即可发现,如拼写错误或符号错误等。由于语法错误无法被try{}catch{}捕获,且这类错误在开发阶段即可修正,因此不会发布至线上环境。try { const name = 'wsjyq; console.log(name); } catch (error) { console.log('--- 语法错误 --') } - 同步错误类型分析。
同步错误指在JavaScript同步执行过程中发生的错误,例如变量未定义等情况。此类错误可被try-catch机制try { const name = 'wsjy'; console.log(nam); } catch (error) { // console.log('--- 同步错误 ---- ') } - 异步错误类型分析。
异步错误指在setTimeout等异步函数中发生的错误,无法被try-catch机制直接捕获。异步错误可通过Window.onerror进行捕获处理,相较于try-catch方式更为便捷。
{/* 异步错误 */} <button style={{ marginRight: 20 }} onClick={() => { // 异步错误无法被trycatch捕获 try { setTimeout(() => { let name = 'wsjyq'; name.map(); }) } catch (error) { console.log('--- 异步错误---- ') } }} >异步错误</button>// ----- 异步错误捕获 -------- /** * @param {String} msg 错误描述 * @param {String} url 报错文件 * @param {Number} row 行号 * @param {Number} col 列号 * @param {Object} error 错误Error对象 */ window.onerror = function (msg, url, row, col, error) { console.log('---- 捕获到js执行错误 ----'); console.log(msg); console.log(url); console.log(row); console.log(col); console.log(error); return true; }; 关于Promise错误,虽然在Promise链中使用catch可以捕获异步错误,但若未编写catch分支,Window.onerror将无法捕获此类错误。因此,建议在全局添加unhandledrejection监听器,以捕获未被处理的Promise错误。
{/* promise错误 */} <button style={{ marginRight: 20 }} onClick={() => { Promise.reject('promise error').catch(err => { console.log('----- promise error -----'); }); Promise.reject('promise error'); }} >promise错误</button>// ------ promise error ----- window.addEventListener('unhandledrejection', (error) => { console.log('---- 捕获到promise error ---') }, true);资源加载错误指各类资源文件获取失败的情况,通常通过Window.addEventListener进行捕获。
{/* resource错误 */} <button style={{ marginRight: 20 }} onClick={() => { setShow(true); }} >resource错误</button> { show && <img src="localhost:8000/images/test.png" /> // 资源不存在 } </div>// ------ resource error ---- window.addEventListener('error', (error) => { console.log('---- 捕获到resource error ---') }, true);
SDK的错误监控功能正是围绕上述几类错误构建。try-catch用于在可预见场景下监控特定错误;Window.onerror主要用于捕获如异步错误等意外情况。然而,Promise错误和网络错误无法被传统方式捕获,因此需借助Window.unhandledrejection监听Promise错误,并通过error监听捕获资源加载错误,从而实现全类型错误的覆盖监控。
用户埋点统计模块用于追踪用户在应用中的行为表现。
埋点技术记录用户在应用上的具体动作,例如在淘宝某商品页停留数分钟,后台将生成该用户搜索并停留的记录。基于这些数据分析用户行为,进而指导推送策略或产品迭代优化,对产品的长远发展具有重要价值。
埋点技术主要分为手动埋点与自动埋点两种模式。
手动埋点模式要求开发者在代码中显式添加相关埋点逻辑。
例如,在用户点击特定按钮或提交表单时,在对应的事件回调中添加埋点代码。
{/* 手动埋点 */}
<button
onClick={() => {
tracker('submit', '提交表单');
tracker('click', '用户点击');
tracker('visit', '访问新页面');
}}
>按钮1</button>
{/* 属性埋点 */}
<button data-target="按钮2被点击了">按钮2</button>- 手动埋点的优点在于可控性强,允许自定义上报的具体数据内容。
- 其缺点是对业务代码入侵性较强,若需在多处进行埋点,需逐一添加代码,维护成本较高。
自动埋点模式旨在解决手动埋点的不足,实现无需侵入业务代码即可进行全局埋点监控。
自动埋点通过技术手段在应用中静默添加监控逻辑,无需修改具体业务代码。
{/* 自动埋点 */}
<button
style={{ marginRight: 20 }}
onClick={(e) => {
//业务代码
}}
>按钮3</button>/**
* 自动上报
*/
export function autoTrackerReport() {
// 自动上报
document.body.addEventListener('click', function (e) {
const clickedDom = e.target;
// 获取标签上的data-target属性的值
let target = clickedDom?.getAttribute('data-target');
// 获取标签上的data-no属性的值
let no = clickedDom?.getAttribute('data-no');
// 避免重复上报
if (no) {
return;
}
if (target) {
lazyReport('action', {
actionType: 'click',
data: target
});
} else {
// 获取被点击元素的dom路径
const path = getPathTo(clickedDom);
lazyReport('action', {
actionType: 'click',
data: path
});
}
}, false);
}需注意,无痕埋点通常通过全局监听click事件的冒泡行为实现。若click事件中阻止了冒泡,事件将无法传递至全局监听器,因此涉及冒泡行为的事件需配合手动埋点上报,以确保数据覆盖的完整性。
{/* 自动埋点 */}
<button
style={{ marginRight: 20 }}
onClick={(e) => {
e.stopPropagation(); // 阻止事件冒泡
tracker('submit', '按钮1被点击了'); //手动上报
}}
>按钮3</button>- 自动埋点的优点在于无需侵入代码即可实现全局埋点上报。
- 其缺点在于仅能上报基础的行为交互信息,无法支持自定义数据上报。此外,页面中任何点击操作均会上报至服务器,可能导致上报频率过高,增加服务器压力。
PV统计模块用于计算页面浏览量。
PV(Page View)即页面浏览量,表示页面的访问次数。对于非SPA页面,只需监听onload事件即可统计PV;而在SPA应用中,路由切换由前端控制,单页面切换分为hash路由和history路由,两者实现原理不同,因此需针对这两种路由分别实现数据采集方案。
针对history路由的实现机制进行分析。
history路由依赖于全局对象history实现。
- history.back():用于返回上一页(模拟浏览器回退操作)。
- history.forward():用于前进一页(模拟浏览器前进操作)。
- history.go():用于跳转至历史列表中的指定页面。
- history.pushState():用于向历史栈中添加新记录。
- history.replaceState():用于修改当前历史记录。
history路由的数据采集主要依赖pushState和replaceState方法,但由于这两个方法无法触发popstate事件,因此需对这两个方法进行重写,并监听自定义事件以实现数据采集。
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方法
* @param {*} name
* @returns
*/
const createHistoryEvent = function (name) {
// 拿到原来的处理方法
const origin = window.history[name];
return function(event) {
// if (name === 'replaceState') {
// const { current } = event;
// const pathName = location.pathname;
// if (current === pathName) {
// let res = origin.apply(this, arguments);
// return res;
// }
// }
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()
});
}
针对hash路由的实现机制进行分析。
URL中hash值的变化会触发hashChange事件,因此只需在全局添加监听函数并在其中实现数据采集上报即可。但在React和Vue等框架中,hash路由的跳转通常通过pushState实现,因此还需额外监听pushState事件以确保数据准确采集。
/**
* 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) {
//自定义事件
// if (name === 'replaceState') {
// const { current } = event;
// const pathName = location.pathname;
// if (current === pathName) {
// let res = origin.apply(this, arguments);
// return res;
// }
// }
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(Unique Visitor)统计一天内访问网站的用户数量。该统计只需在SDK初始化时上报一条消息即可完成。
/**
* 初始化配置
* @param {*} options 配置信息
*/
function init(options) {
// ------- 加载配置 ----------
// -------- uv统计 -----------
lazyReport('user', '加载应用');
}数据上报模块负责将采集到的数据发送至服务端。
- xhr接口请求方式采用标准的接口请求,类似于其他业务请求,仅传递数据为埋点信息。通常情况下,处理埋点的服务器与处理业务逻辑的服务器并非同一台,因此需手动解决跨域问题。此外,若在上报过程中刷新或重新打开页面,可能导致埋点数据丢失,故传统xhr接口请求方式难以完全适应埋点需求。
- img标签方式将埋点数据伪装为图片URL请求,从而避免跨域问题。但浏览器对URL长度有限制,因此不适合大数据量上报,且同样存在刷新或重新打开页面导致数据丢失的问题。
- sendBeacon方式不会出现跨域问题,也不存在刷新或重新打开页面导致数据丢失的问题,但其缺点在于存在兼容性问题。在日常开发中,通常采用sendBeacon上报与img标签上报相结合的策略,以平衡兼容性与数据完整性。
/**
* 上报
* @param {*} type
* @param {*} params
*/
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的实现进行了全面总结。
至此,关于前端实现监控SDK的介绍已结束。如需了解更多前端监控SDK相关内容,请搜索本站历史文章或继续浏览后续相关文章,感谢大家的支持。