前言
小萌公司的电商项目经过一个多月的加班测试,昨天终于成功上线。然而上线没多久就收到用户反馈:线上产品不稳定,有些页面加载很久不显示,有些会直接报错,公司领导也无法知道系统有多少用户访问,那些商品用户浏览时间比较长,那些功能是用户经常使用的。当前系统无法提供领导决策需要的数据。基于以上问题公司决定让小萌负责对项目进行改进。
前端监控
小萌找到大锤商量,大锤经验丰富告诉小萌,你们的产品缺少一个前端监控系统。前端监控可以帮助开发者及时发现和解决用户在使用前端应用时遇到的问题,提升用户体验,并为产品决策提供数据支持。
市面上有很多成熟的监控平台可以直接部署使用。如:sentry、arms、fundebug、webfunny等。如果公司对监控有定制化需求,就需要自己开发。
小萌咨询了领导,领导表示为了公司后期发展决定让小萌自己开发一套前端监控,领导交代的事情不会也得办呀,谁叫现在开发这么卷。于是网上查资料做分析,实现了第一个版本。我们看下小萌是怎么做的。
需要监控什么
-
错误统计
首先,我们的代码发布到线上总是会发生奇奇怪怪的错误,错误原因也五花八门,可能是浏览器兼容问题,可能是代码里面没做兜底,也可能是后端接口挂掉了等等错误,可能随便一个错误都会影响用户的使用,所以对线上进行错误监控显的尤为重要,能够让我们第一时间去响应报错并解决。
-
行为日志埋点
对于一些常见的电商app,都有一套自己的用户行为分析的系统,分析用户浏览时间比较长的页面有哪些,常点击的按钮有哪些,通过分析用户的这些行为去制定不同的策略引导购物,这些都可以通过前端埋点去实现对用户行为的监控。
-
PV/UV统计
上线那么多的前端页面,肯定特别想知道用户对哪些页面的访问比较多,每天有多少用户访问我们的系统,这就需要做PV(Page View)和UV(Unique Visitor)的统计,pv是页面浏览量,uv为独立访客数。
监控模块设计
完整的监控模块功能包含数据采集、数据上报、数据分析、报警。小萌先完成数据采集、上传、查看功能。
数据采集
报错数据采集
前端页面报错很影响用户体验,即使在测试充分后上线也会因为用户的操作行为不当以及操作的环境不同出现各种错误,所以前端的错误监控非常重要。监控错误类型分为:
-
语法错误,一般在开发阶段就可以发现,比如常见的单词拼写错误,中英文符号错误等。注意:语法错误是无法被try catch捕获的,因为在开发阶段就能发现,所以一般不会发布到线上环境。
try { let name = 'heima; // 少一个单引号 console.log(name); } catch (error) { console.log('----捕获到了语法错误-----'); }
-
同步错误,指的是在js同步执行过程中的错误,比如变量未定义,是可以被try catch给捕获到的。
try { const name = 'heima'; console.log(nam); } catch (error) { console.log('------同步错误-------') }
-
异步错误,指的是在setTimeout等函数中发生的错误,是无法被try catch捕获到的。
try { setTimeout(() => { undefined.map(); }, 0); } catch (error) { console.log('-----异步错误-----') }
异步错误的话我们可以用window.onerror来进行处理,这个方法比try catch要强大很多。
/** * @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('出错了!!!'); console.log(msg); console.log(url); console.log(row); console.log(col); console.log(error); };
-
promise错误,在 promise 中使用 catch 可以捕获到异步的错误,但是如果没有写 catch 去捕获错误的话 window.onerror 也捕获不到的,所以写 promise 的时候最好要写上 catch ,或者可以在全局加上 unhandledrejection 的监听,用来监听没有被捕获的promise错误。
window.addEventListener("unhandledrejection", function(error){ console.log('捕获到异常:', error); }, true);
-
资源加载错误,资源加载错误指的是比如一些资源文件获取失败,可能是服务器挂掉了等原因造成的,出现这种情况就比较严重了,所以需要能够及时的处理,网路错误一般用 window.addEventListener 来捕获。
window.addEventListener('error', (error) => { console.log(error); }, true);
try-catch 用来在可预见情况下监控特定的错误,window.onerror 主要是来捕获预料之外的错误,比如异步错误。但是 window.onerror 也并不是万能的,它可以捕获语法,同步,异步的错误,但是对于promise错误以及网络错误还是无能为力,所以还需要 unhandledrejection 监听来捕获promise错误,最后,再加上 error 监听捕获资源加载的错误就能将各种类型的错误全覆盖了。
埋点数据采集
埋点是监控用户在应用上的一些动作表现,是不是经常感觉有些应用推荐的内容都是自己感兴趣的,这就是埋点这个“内鬼”在搞怪,比如你在淘宝上的某类型的鞋子的页面浏览了几分钟,那么就会有一个“张三在2042-11-11 11:30搜索了某款运动鞋并浏览了十分钟”的记录的上报,后台就可以根据这些上报的数据去分析用户的行为,并且制定之后推送或者产品的迭代优化等,对于产品后续的发展起着重要作用。
埋点又分为手动埋点和无痕埋点。
1. 手动埋点
手动埋点就是手动的在代码里面添加相关的埋点代码,比如用户点击某个按钮,就在这个按钮的点击事件中加入相关的埋点代码,或者提交了一个表单,就在这个提交事件中加入埋点代码。
// 方式1
<button
onClick={() => {
// 业务代码
tracker('click', '用户去支付');
// tracker('visit', '访问新页面');
// tracker('submit', '提交表单');
}}
>手动埋点</button>
// 方式2
<button
data-target="支付按钮"
onClick={() => {
// 业务代码
}}
>手动上报</button>
-
优点:可控性强,可以自定义上报具体的数据。
-
缺点:对业务代码侵入性强,如果有很多地方需要埋点就得一个一个手动的去添加埋点代码。
2. 无痕埋点
无痕埋点是为了解决手动埋点的缺点,实现一种不用侵入业务代码就能在应用中添加埋点监控的埋点方式。
<button onClick={() => {
// 业务代码
}}>自动埋点</button>
// 自动埋点实现
function autoTracker () {
// 添加全局click监听
document.body.addEventListener('click', function (e) {
const clickedDom = e.target;
// 获取data-target属性值
let target = clickedDom?.getAttribute('data-target');
if (target) {
// 如果设置data-target属性就上报对应的值--手动埋点
tracker('click', target);
} else {
// 如果没有设置data-target属性就上报被点击元素的html路径
const path = getPathTo(clickedDom);
tracker('click', path);
}
}, false);
};
-
优点:不用侵入务代码就能实现全局的埋点。
-
缺点:只能上报基本的行为交互信息,无法上报自定义的数据;上报次数多,服务器性能压力大。
PV/UV数据采集
1.PV统计
PV即页面浏览量,用来表示该页面的访问数量
在SPA应用之前只需要监听 onload 事件即可统计页面的PV,在SPA应用中,页面路由的切换完全由前端实现,主流的react和vue框架都有自己的路由管理库,而单页路由又区分为 hash 路由和 history 路由,两种路由的原理又不一样,所以统计起来会有点复杂。我们这里将分别针对两种路由来实现不同的采集数据的方式。
history路由
history路由的实现主要依赖的就是 pushState 和 replaceState 来实现的,但是这两种方法不能被 popstate 监听到,所以需要对这两种方法进行重写来实现数据的采集。
/**
* 重写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;
};
};
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;
}
// history.go()、history.back()、history.forward() 监听
window.addEventListener('popstate', function () {
listener()
});
// history.pushState
window.addEventListener('pushState', function () {
listener()
});
// history.replaceState
window.addEventListener('replaceState', function () {
listener()
});
hash路由
url上hash的改变会出发 hashchange 的监听,所以我们只需要在全局加上一个监听函数,在监听函数中实现采集并上报就可以了。但是在react和vue中,对于hash路由的跳转并不是通过 hashchange 的监听实现的,而是通过 pushState 实现,所以,还需要加上对 pushState 的监听才可以。
export function hashPageTrackerReport() {
let beforeTime = Date.now(); // 进入页面的时间
let beforePage = ''; // 上一个页面
// 上报
function listener() {
const stayTime = getStayTime();
const currentPage = window.location.href;
lazyReport('visit', {
stayTime,
page: beforePage,
})
beforePage = currentPage;
}
// hash路由监听
window.addEventListener('hashchange', function () {
listener()
});
}
2. UV统计
UV统计的是一天内访问该网站的用户数,uv统计比较简单,就只需要在初始化的时候上报一条消息就可以了。
/**
* 初始化配置
* @param {*} options
*/
function init(options) {
... // 加载配置
report('user', '加载应用'); // uv统计
}
数据上报
xhr接口请求
采用接口请求的方式是最简单的,就跟请求其他业务接口一样,只不过上传的是埋点的数据。但是在通常的情况下,一般在公司里面处理埋点的服务器和处理业务逻辑的处理器不是同一台,所以还需要手动解决跨域的问题,另一方面,如果在上报的过程中刷新或者重新打开新页面,可能会造成埋点数据的缺失,所以传统的xhr接口请求的方式并不能很好的适应埋点的需求。
-
img标签
img标签的方式是通过将埋点数据伪装成图片URL的请求方式,这样就避免了跨域的问题,但是因为浏览器对url的长度会有限制,所以通过这种方式上报不适合大数据量上报的场景,而且也会存在刷新或者打开页面的时候上报的数据丢失的情况。
-
sendBeacon
鉴于以上两种方式的缺点,sendBeacon应运而生了,sendBeacon可以说是为埋点量身定做的,这种方式不会有跨域的限制,也不会存在因为刷新页面等情况造成数据丢失的情况,唯一的缺点就是在某些浏览器上存在兼容性的问题,所以在日常的开发场景中,通常采用sendBeacon上报和img标签上报结合的方式
* 上报
* @param {*} type
* @param {*} params
*/
export function report(type, params) {
const appId = window['_monitor_app_id_'];
const userId = window['_monitor_user_id_'];
const url = window['_monitor_report_url_'];
const logParams = {
appId, // 项目的appId
userId,
type, // error/action/visit/user
data: params, // 上报的数据
currentTime: new Date().getTime(), // 时间戳
currentPage: window.location.href, // 当前页面
ua: navigator.userAgent, // ua信息
};
let logParamsString = JSON.stringify(logParams);
if (navigator.sendBeacon) { // 支持sendBeacon的浏览器
navigator.sendBeacon(url, logParamsString);
} else { // 不支持sendBeacon的浏览器
let oImage = new Image();
oImage.src = `${url}?logs=${logParamsString}`;
}
}
合并上报
对于无痕埋点来说,一次点击就进行一次上报对服务器来说压力有点大,所以最好是能进行一个合并上报。
// cache.js
const cache = [];
export function getCache() {
return cache;
}
export function addCache(data) {
cache.push(data);
}
// lazyReport.js
export function lazyReport(type, params) {
// ....
const data = getCache();
if (delay === 0) { // delay=0相当于不做延迟上报
report(data);
return;
}
if (data.length > 10) { // 数据达到10条上报
report(data);
clearTimeout(timer);
return;
}
clearTimeout(timer);
timer = setTimeout(() => { // 合并上报
report(data);
}, delay);
}
运行效果
总结
数据上报到服务器后,对数据进行分析可视化展示,继续添加报警处理功能。本文重点介绍数据采集、
原创文章,作者:guozi,如若转载,请注明出处:https://www.sudun.com/ask/81929.html