监控内容
- 错误监控 如:浏览器兼容问题、代码bug、后端接口挂掉等问题
- 行为日志 如常用的电商app,通过分析用户浏览时间较长页面有哪些、常点击按钮有哪些等行为,通过分析用户的行为定制不同策略引导用户进行购买
- PV/UV统计 如:统计用户访问页面次数,每天有多少用户访问系统
围绕以上三点进行设计,主要流程如下:
数据采集:采集前端监控的相关数据,包括PV/UV、用户行为、报错信息。
日志上报:将采集到的数据发送给服务端。
日志查询:在后台页面中查询采集到的数据,进行系统分析。
功能拆分
初始化
获取用户传递的参数,调用初始化函数,在初始化函数中可以注入一些监听事件来实现数据统计的功能。
/**
* 初始化配置
* @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('--- 语法错误 --') }
- 同步错误 指在js同步执行过程中发生的错误,如变量未定义,可被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事件中阻止了冒泡行为,是不会冒泡到click监听里的,所以,对于加了冒泡行为的click事件需要进行手动埋点上报,从而保证上报全覆盖。
{/* 自动埋点 */}
<button
style={{ marginRight: 20 }}
onClick={(e) => {
e.stopPropagation(); // 阻止事件冒泡
tracker('submit', '按钮1被点击了'); //手动上报
}}
>按钮3</button>
- 优点:不用入侵代码就可以实现全局埋点上报。
- 缺点:只能上报基本的行为交互信息,无法上报自定义数据。只要在页面中点击了,就会上报至服务器,导致上报次数会太多,服务器压力大。
PV统计
PV即页面浏览量,表示页面的访问次数
非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统计只需在SDK初始化时上报一条消息即可。
/**
* 初始化配置
* @param {*} options 配置信息
*/
function init(options) {
// ------- 加载配置 ----------
// -------- uv统计 -----------
lazyReport('user', '加载应用');
}
数据上报
- xhr接口请求 采用接口请求的方式,就像其他业务请求一样,知识传递的数据是埋点的数据。通常情况下,公司里处理埋点的服务器和处理业务逻辑的服务器不是同一台,所以需要手动解决跨域问题。另一方面,如果在上报过程中刷新或者重新打开页面,可能会造成埋点数据的缺失,所以传统xhr接口请求方式并不能很好适应埋点的需求。
- img标签 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();
}
版权归原作者 棋丶 所有, 如有侵权,请联系我们删除。