前端监控:错误捕获与行为日志全解析
行为日志采集
- 页面浏览(Page Views)
- 用户点击(Clicks)
- 键盘输入(Keyboard Inputs)
- 页面停留时间(Time on Page)
- 滚动行为(Scrolling Behavior)
- 表单填写情况(Form Interactions)
错误日志采集
语法错误
- 缺少括号、大括号或方括号 [() {} []](javascript:void(0))
- 缺少分号或逗号
; , - 字符串引号不匹配
- 关键字拼写错误
同步错误
异步错误
异步错误的话我们可以用window.onerror来进行处理,这个方法比try catch要强大很多
promise错误
在promise中使用catch可以捕获到异步的错误,但是如果没有写catch去捕获错误的话
window,onerror也捕获不到的,所以写promise的时候最好要写上catch,或者可以在全局
加上unhandledrejection的监听,用来监听没有被捕获的promise错误。
resource错误
资源加载错误指的是比如一些资源文件获取失败,可能是服务器挂掉了等原因造成的,出现这种
情况就比较严重了,所以需要能够及时的处理,网路错误一般用window.addEventListener来
捕获。
全局错误捕获
import { lazyReport } from './report';/*** 全局错误捕获*/
export function errorTrackerReport() {// -------- js error ---------const originOnError = window.onerror;window.onerror = function (msg, url, row, col, error) {// 处理原有的onerrorif (originOnError) {originOnError.call(window, msg, url, row, col, error);}// 错误上报lazyReport('error', {message: msg,file: url,row,col,error,errorType: 'jsError'});}// ------ promise error --------window.addEventListener('unhandledrejection', (error) => {lazyReport('error', {message: error.reason,error,errorType: 'promiseError'});});// ------- resource error --------window.addEventListener('error', (error) => {let target = error.target;let isElementTarget = target instanceof HTMLScriptElement || target instanceof HTMLLinkElement || target instanceof HTMLImageElement;if (!isElementTarget) {return; // js error不再处理}lazyReport('error', {message: "加载 " + target.tagName + " 资源错误",file: target.src,errorType: 'resourceError'});}, true)
}/*** 手动捕获错误*/
export function errorCaptcher(error, msg) {// 上报错误lazyReport('error', {message: msg,error: error,errorType: 'catchError'});
}
路由监听
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.pushStatewindow.addEventListener('pushState', function () {listener()});// history.replaceStatewindow.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路由监听*/
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.pushStatewindow.addEventListener('pushState', function () {listener()});
}
上报
import { getCache, addCache, clearCache } from './cache';let timer = null;/*** 上报* @param {*} type * @param {*} params */
export function lazyReport(type, params) {const appId = window['_monitor_app_id_'];const userId = window['_monitor_user_id_'];const delay = window['_monitor_delay_'];const logParams = {appId, // 项目的appIduserId, // 用户idtype, // error/action/visit/userdata: params, // 上报的数据currentTime: new Date().getTime(), // 时间戳currentPage: window.location.href, // 当前页面ua: navigator.userAgent, // ua信息};let logParamsString = JSON.stringify(logParams);addCache(logParamsString);const data = getCache();if (delay === 0) { // delay=0相当于不做延迟上报report(data);return;}if (data.length > 10) {report(data);clearTimeout(timer);return;}clearTimeout(timer);timer = setTimeout(() => {report(data)}, delay);
}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();
}