1. 背景
事情是这样的,在某一次的例会上我把自己想做一个前端错误监控的想法提了一下,接下来就有了现在的前端应用监控,市面上有很多,但是为什么还要自己做?大多数的没办法私有化部署,能私有化部署的改动量比较大。与其这样不如自己写一套。
2. 架构设计
前端应用监控系统主要分为五部分:日志采集、日志存储、统计与分析、报告与告警、数据可视化平台。
3. 日志采集
(1)错误日志采集
错误日志采集基本上大家都是采集JS错误、Promise错误、Resource加载错误和框架内部错误。具体方案不在多说,基本网上都能查到。这里要说一下,promise会采集到{} ,这个主要是框架内部产生,并没有导致业务系统逻辑错误,所以采用内部过滤此类错误。
(2)接口请求采集
接口请求采集主要是采用AOP方式对ajax与fetch进行拦截。为了实现钱后端日志串联,我们会在每一笔业务请求的header中添加自定义头。后端日志组件会获取到请求头的信息添加到后端日志信息中。
在计算请求耗时时,一般的方案都是在请求发送时定义一个时间戳作为请求发送时间,在请求完成的事件(loadend或者readyState === 4)中设置一个时间戳作为响应时间。经过测试这样计算的耗时明显比浏览器上显示的耗时要大很多,这样得到的监控数据没有什么意义了。所以我们利用performance这个接口计算耗时,让发送时间+耗时得到响应时间,这样虽然请求时间与响应时间偏右,但是耗时是准确的。在使用performance测试同一个接口同时发送多次是会产生死锁现象,所以我们采用第一种方案作为补偿机制。
(3)流量指标采集
我们先看一下Google的标准:
第一步是看在一天24小时里有多少用户到访过以及这些用户总共访问了多少次。多少用户访问过是可以用UV(Unique View)来计算的,通常是用Cookie等监测方式来计算的,这些用户访问的总次数是可以用PV(Page View)来计算的。
通常页面加载一次被计为1个PV,但这种简单的计算办法是不准确的,因为这个页面可能是自动刷新的,也可能是进来后立马去别页面啦等,所以不准确的数据是不太具有参考数据。GA(Google Analytics)将PV和会话(Session)绑定在了一起,而会话是指用户在对网站的一次完整访问过程,一次会话的判断标准是指关闭了网站(即关闭了访问该网站的浏览器),距离上次访问(会话)距离超出了30分钟或者超过当日的24:00。因此,按GA计算逻辑,在一次会话里无论用户对某个页面怎么反复加载都算作一次PV,这种计算方法相对准确些。
第二步是看在一天24小时里所有用户到访过网站的人次,当用户完成所有浏览并最终关掉该网站的所有页面时便完成了一次访问,同一用户1天内可能有多次访问行为,按访问次数计算,计为VV(Visit View)。
第三步是看在一天24小时里访问过网站的独立IP(Internet Protocol)总数,比如同一个办公室有一个公网出口(即一个公网IP),那么一天里所有员工的访问数都被计为1次,即IP数。
总之,通过上述三步的用户访问监测和计算,可以分析出一个网站的受用户感兴趣程度,依次是看在一天24小时内有多少用户访问过网站(即UV),这些用户总共访问了多少次网站页面数(即PV),这些用户总共访问网站多少次(即VV)以及这些到访用户的独立IP数(即IP)。进而在此基础上,加上时间维度(按日、周、月、年等)和页面主题维度等,就可以更多维度地分析网站受欢迎程度,从而可以给网站经营者提供一些内容优化和网站推广方向建议。
我们采用的方案:
PV:group by sessionId+page VV:group by sessionId UV:group by uid(cookie) IP:group by ip
这里主要说一下cookie的设置,我们会为每一个使用监控的应用系统人生成一个userID,不管你是登录还是没有登录,这个userID会一直存在,除非用户手动删除cookie,同样我们考虑到跨系统访问的情况,在设置cookie的时候domain设置成顶级domain,path设置为/。测试过网上很多方法发现不尽人意。所以自己写了一个方法来获取顶级域名。
// 获取顶级domain
export const getCookieDomain = (): string => {
let host: string = location.hostname;
let str: string = "";
const ip = /^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5]).(\d{1,2}|1\d\d|2[0-4]\d|25[0-5]).(\d{1,2}|1\d\d|2[0-4]\d|25[0-5]).(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$/;
if (ip.test(host) === true || host === 'localhost') return `${host}`;
const regex = /([^]*).*/;
const match = host.match(regex);
if (typeof match !== "undefined" && null !== match) str = match[1];
if (typeof host !== "undefined" && null !== host) {
const strAry = host.split(".");
if (strAry.length > 1) {
if (strAry.length === 2) str = `${host}`;
if (str.length > 2) {
str = strAry[strAry.length - 2] + "." + strAry[strAry.length - 1];
}
}
str = str === "com.cn" ? strAry[strAry.length - 3] + "." + str : str;
}
return '.' + str;
}
关于cookie的知识去看 MDN HTTP cookies
4. 日志上报
关于日志上报方法,一般的方案是采用new image或者sendBeacon,测试下来发现如果报文过长就会报错。使用图片这种方式是get 请求长度会有限制,sendBeacon的报文大小现在在65Kb左右。所以我们还是比较保守的采用ajax post方式来进行上报。对于错误日志我们会实时上报,接口数据会缓存在本地,当到达N条或者时间间隔到了以后会上报。同样考虑到跨系统日志共享,采用iframe+postmessage+localstorage来实现。同样我们不可能每条接口数据都采集,所以会设置采样率来做采样处理。关闭网页时会兜底使用sendBeacon上报小与65k一下的数据。