为什么需要前端监控
通常,后端会配备日志功能以供查阅,这些日志详尽记录了各类接口调用的情况。然而,尽管后端日志对于追踪系统行为至关重要,前端页面却因其与用户有着"面对面"交互的特性,同样需要我们密切关注。具体来说,我们需要洞察用户在页面上的实际操作路径,监测前端在实际运行中是否遭遇问题,以及识别存在哪些潜在的优化空间,以确保前端体验的顺畅与高效。
操作路径收集
通过事件收集操作脚印
我们能够运用事件监听来捕获用户在页面上的 click
、keydown
等互动行为。然而,在此过程中,我们需意识到并非所有此类事件均具有分析价值。例如,用户对页面空白区域的无效点击,或随意敲击键盘产生的输入,本质上属于干扰数据,即"噪音"。这类"噪音"应当在数据收集阶段就被精准识别并剔除,以确保我们专注于分析真正有意义的用户交互信息。
typescript
document.addEventListener('click', ({ target, type }) => {
const tagName = target.tagName.toLowerCase();
/**
* 认为 'button', 'a', 'input' 标签,以及添加了 `cursor: pointer` 样式的标签是有效点击
*/
if (['button', 'a', 'input'].includes(tagName) || getComputedStyle(target).cursor === 'pointer') {
log('event', {
text: target?.innerText,
eventType: type,
tagName,
// 使用了 dom-selector-generator (npm) 生成元素 selector
selector: selectorGenerator(target),
});
}
})
document.addEventListener('keydown', ({ target, type }) => {
const tagName = target.tagName.toLowerCase();
/**
* 认为 'textarea', 'input' 标签是有效输入
*/
if (['textarea', 'input'].includes(tagName)) {
log('event', {
text: target?.value,
eventType: type,
tagName,
// 使用了 dom-selector-generator (npm) 生成元素 selector
selector: selectorGenerator(target),
});
}
})
通过路由相关监听,收集用户路过哪处地标
要达成我们的目标,可以采取两种策略:
- 利用路由事件监听机制,实时监控每一次路由变更情况;
- 对涉及路由跳转的关键 API 进行"把守",确保对其调用的全程控制。
scss
// callback 为通用页面跳转事件收集回调方法
// 页面进入
addEventListener('hashchange', callback);
addEventListener('popstate', callback);
const originPushState = history?.pushState;
if (!!originPushState) {
history.pushState = function() {
originPushState.apply(this, arguments);
callback();
}
}
const originReplaceState = history?.replaceState;
if (!!originReplaceState) {
history.replaceState = function(state, _, url) {
originReplaceState.apply(this, arguments);
if (history.state === null && state?.idx === 0 && !url) return;
callback();
}
}
// 页面离开
addEventListener('beforeunload', callback);
页面性能 & 错误信息收集
白屏监控
无论是采用 React
、Angular
还是 Vue
框架开发,每个应用皆拥有一个顶层的根元素。即便在运用其他框架或采用原生页面编写方式时,我们同样可以设定这样一个根节点。这样一来,当页面完全加载完毕后,只需查验该根节点下是否存在有效内容,即可准确判断出页面是否正处于空白(白屏)状态。
ini
const timer = setInterval(() => {
if (document.readyState === 'complete') {
const rootEl = document.querySelector('#app');
if (!!rootEl) {
if (!!rootEl.innerHTML) {
clearInterval(timer);
}
}
log('blank', {
target: rootSelector,
});
clearInterval(timer);
}
}, 1000);
脚本报错
我们能利用对 error
和 unhandledrejection
事件的监听,系统性地汇集JavaScript脚本运行时的错误信息。其中,error
事件负责捕捉常规的脚本错误,而 unhandledrejection
事件则专门用于收录那些未被 catch
处理的 Promise
异常,从而实现对两类不同来源报错的全面监测与记录。
arduino
addEventListener('error', ev => {
const {
filename,
error,
} = ev;
log('js-error', {
filename,
message: error?.message,
stack: error?.stack,
});
});
addEventListener('unhandledrejection', ev => {
const { reason } = ev;
log('unhandledrejection-error', {
message: reason,
});
})
前端性能异常收集
我们可借助 PerformanceObserver
这一 api
,通过为其接收到的性能指标设定阈值,来有效地识别并记录前端性能问题。如此一来,任何超出预设标准的性能表现都将被自动纳入监控范围,有助于我们及时发现并着手解决前端性能瓶颈。
详细见:developer.mozilla.org/en-US/docs/...
请求监控
我们能够通过对 fetch
和 XMLHttpRequest
原生接口的全面"把守",精确追踪其发送请求及接收响应的各个环节,以此来搜集网络请求与响应的相关信息。
javascript
function fetchHandle() {
const originFetch = window.fetch;
window.fetch = function (input: RequestInfo | URL, init?: RequestInit | undefined) {
// 收集请求信息
const startTime = new Date().getTime();
return originFetch.apply(window, arguments).then(async response => {
const duration = new Date().getTime() - startTime;
// 收集正常响应信息
return response;
}).catch(err => {
const duration = new Date().getTime() - startTime;
// 收集异常响应信息
throw err;
});
}
}
function XHRHandle() {
if (typeof XMLHttpRequest !== 'function') return;
const originXMLHttpRequest = window.XMLHttpRequest;
const originOpen = originXMLHttpRequest.prototype.open;
const originSend = originXMLHttpRequest.prototype.send;
const originSetRequestHeader = originXMLHttpRequest.prototype.setRequestHeader;
originXMLHttpRequest.prototype.open = function (method, url) {
originOpen.apply(this, arguments);
// 收集请求信息
}
originXMLHttpRequest.prototype.setRequestHeader = function (name, value) {
originSetRequestHeader.apply(this, arguments);
// 收集请求头信息
}
originXMLHttpRequest.prototype.send = function (body) {
originSend.apply(this, arguments as any);
const startTime = new Date().getTime();
// 收集请求信息
const onLoaded = () => {
const duration = new Date().getTime() - startTime;
// 收集正常响应信息
}
xhr.addEventListener('readystatechange', () => {
if (xhr.readyState === 4) {
onLoaded();
}
})
}
}
收集资源加载失败信息
我们可以通过监听 error
事件,并对其抛出的事件对象进行类型检测类型检测,判断是否属于 ErrorEvent
即可判断是否存在资源加载失败的情况
javascript
addEventListener('error', ev => {
if (!(ev instanceof ErrorEvent)) {
const target = (ev as Event).target as (HTMLScriptElement | HTMLLinkElement);
const source = (target as HTMLScriptElement).src || (target as HTMLLinkElement).href;
log('resource-error', {
source,
type: target.tagName,
});
}
}, true);
尝试实现
基于上述方案设计与编码实践,简单开发出了一款小工具,并配套设计了一个简洁的测试页面以验证其功能。以下是该工具的实际测试效果展示:
小工具开源分享
提供了 CDN 和 npm 两种接入手段,用来达到对前端的监控
本文包含的插图和文案均通过通义(通义万象、通义千问)产品生成或加工