在实际的项目开发,布署上线过程中,经常会遇到如下问题:
- 系统上线后,用户发现访问页面速度 变慢了。项目排查认为是框架问题,让框架进行排查;框架排查认为是网络问题与框架无关。 那到底如何明确定位到底是属于网络问题、框架问题、还是业务逻辑问题呢?
- 系统上线后,发现系统不稳定 ,界面操作有报错现象。但公司目前现场发过来的错误日志,只有后端日志,没有前端报错日志,前端人员需要浏览器控制台报错信息才能进行排查,同时开发人员也不清楚复现步骤
- 随着系统页面越来越多,功能越来越复杂,需要能持续监控、评估、预警页面性能状况、发现瓶颈,指导优化工作的进行。
为了解决上面的问题,需要进行前端监控。前端监控分为采集、存储、分析。下面主要介绍 前端监控之数据采集。
前端页面访问速度
网络耗时数据可以借助Performance API 获取,通过此接口可以获取 重定向耗时、DNS查询耗时、TCP链接耗时、HTTP请求耗时、解析DOM树耗时、DOM Ready时间、onload时间等耗时,示意图如下所示:
前端API请求监控与调用链iTracing 后端打通
原有的公司调用链iTracing是从nginx接收到请求开始记录的 , 加入前端监控链路后,它会将API请求从前端发出到后端调用的链路串联起来,真实还原代码执行的完整现场,能够全面准确确定系统性能瓶颈。
实现原理:前端不管是通过什么样的框架或库,底层都是调用两个最基本的web api XMLHttpRequest、Fetch才能进行数据请求。
通过修改XMLHttpRequest、Fetch的原型(prototype),拦截用户请求。在发送请求时开启事件监听、在请求返回结果时进行拦截。可以获取 API请求列表、返回信息列表、HTTP状态码等信息,通过Response header可以获取最重要的微服务调用链 ITRACING_TRACE_ID: 010010168049_84961_1^1665555540177^14,通过 微服务调用链ITRACING_TRACE_ID 可以显示出后端所有的调用链。
具体实现步骤如下:
1. 在前端拦截XMLHttpRequest请求
// 伪代码
(function (xhr) {
var send = xhr.send;
xhr.send = function (data) {
onLoadStart.apply(this,aguments);
this.addEventListener('loadend', onLoadEnd);
this.addEventListener('error', onError);
return send.apply(this, arguments);
};
})(XMLHttpRequest.prototype);
2. 在 请求开始前的 onLoadStart中 生成一个 前端Trace ID(跟踪本次前端请求),并放入请求头中。
// 伪代码
function onLoadStart() {
let traceid = genTraceId();
this.setRequestHeader("uber-trace-id",traceid);
//插入本地日志待发送缓存队列中
insertLogBuffer({traceid:traceid, beginTime:new Data(), ...})
}
3. 在 请求结束后的 onLoadEnd中,根据 'Respone Headers' 返回的ITRACING_TRACE_ID,以后前端Trace ID,补充本次前端请求日志的其他信息
// 伪代码
function onLoadEnd() {
let uberTraceId = this.getResponseHeader("uber-trace-id");
let iTracingTraceId = this.getResponseHeader("ITRACING_TRACE_ID");
//根据前端的traceid,补充本次前端请求日志的其他信息
updateLogBuffer(uberTraceId, {
ITRACING_TRACE_ID:iTracingTraceId,
costTime: new Data()- beginTime,
url,
httpCode, //请求返回的状态
ip
...})
}
4. 每隔一段时间 取日志缓存中的日志一起发向 日志中心。
应用名 日志产生时间(beginTime) 状态(httpCode) ip 服务名(url) 方法堆栈 时间轴(costTime) 客户端
A 10/12 15:03:12 404 10.45.18.240 /getUserInfo 9ms Chrome/78.0.3904.108
A 10/12 15:04:12 200 10.45.18.240 /check 010010168049_84961_1^1665555540177^14 15ms Chrome/78.0.3904.108
5.根据方法堆栈中的 iTracingTraceId可以获取查看后端调用链占用时间
- 通过上例 我们可以知道 从前端发起请求,至前端接收到请求 共花了15ms,其中后端总共花了 11ms。大约有4ms花了在网络链路
前端错误诊断
前端错误捕获
通常我们能捕获错误异常的手段和范围,如下表 捕获异常手段 同步方法 异步方法 资源加载 Promise async/await try/catch ✓ ✓ window.onerror ✓ ✓ error事件 ✓ ✓ ✓ unhandledrejection ✓ ✓
默认情况下,我们使用window.addEventListener('error', this.onError, true);去监听用户错误脚本;使用unhandledrejection 捕获未被捕获的Promise的异常,自动上报。
用户使用的有些前端框架会捕获js错误,错误信息不会抛至window.onError、error事件,这种情况需用户手动添加调用
- React: // 伪代码 最外层包一个根组件,捕获react异常 componentDidCatch(error) { // Display fallback UI this.setState({ hasError: true }); // You can also log the error to an error reporting service logErrorToMyService(error); }
- Vue: // 伪代码 const errorHandler = (error)=>{ console.error(error); logErrorToMyService(error); } Vue.config.errorHandler = errorHandler; Vue.prototype.$throw = (error)=> errorHandler(error,this);
源码定位
现在开发的大部分前端项目,都会采用前端工程化打包工具,比如gulp、grunt、webpack等等,最终编译出的代码,都会对源码进行混淆压缩,在真实线上项目,捕获js抛出的出错信息往往是压缩后的位置和变量信息,根本看不懂。 如:at f (http://xxxxx/runtime.0ed5fae112953bd474f7.js:1:27956
如何解决这个问题? 可利用Source Map还原代码真正的错误位置,这样使得开发者能够迅速定位出错的源代码位置以及相应的代码块。 采用 source-map-visualization等库,可输入压缩后的行列号,以及sourcemap可以定位出错的源代码位置以及相应的代码块
静态资源监测
资源监测分析主要使用 Resource Timing APIperformance.getEntriesByType("resource") 可以获取到资源加载的各个阶段的时间戳,如重定向、DNS 查询、TCP 连接建立。这些阶段和他们的属性名见下图:
可以使用这些属性值去计算某个阶段的耗时长度,用来帮助诊断性能问题 // 伪代码 var resources = performance.getEntriesByType("resource");
console.log("= Calculate Load Times"); for (var i=0; i < resources.length; i++) { console.log("== Resource[" + i + "] - " + resources[i].name); // Redirect time 重定向耗时 var t = resources[i].redirectEnd - resources[i].redirectStart; console.log("... Redirect time = " + t);
// DNS time DNS 查询耗时
t = resources[i].domainLookupEnd - resources[i].domainLookupStart;
console.log("... DNS lookup time = " + t);
// TCP handshake time TCP 握手耗时
t = resources[i].connectEnd - resources[i].connectStart;
console.log("... TCP time = " + t);
// Secure connection time SSL握手时间(HTTPS协议会有SSL握手)
t = (resources[i].secureConnectionStart > 0) ? (resources[i].connectEnd - resources[i].secureConnectionStart) : "0";
console.log("... Secure connection time = " + t);
// Response time 在浏览器收到服务器响应的第一个字节 至 最后一字节的时间
t = resources[i].responseEnd - resources[i].responseStart;
console.log("... Response time = " + t);
// Fetch until response end 浏览器准备好使用HTTP请求 至 最后一字节的时间
t = (resources[i].fetchStart > 0) ? (resources[i].responseEnd - resources[i].fetchStart) : "0";
console.log("... Fetch until response end time = " + t);
// TTFB 首包时间
t = (resources[i].requestStart > 0) ? (resources[i].responseStart - resources[i].startTime) : "0";
console.log("... TTFB time = " + t);
// Start until reponse end 本次请求总时长
t = (resources[i].startTime > 0) ? (resources[i].responseEnd - resources[i].startTime) : "0";
console.log("... Start until response end time = " + t);
}
除了监控时长外,也可以监控资源的大小 /* 应用程序资源的大小可能会影响应用程序的性能,因此获取有关资源大小的准确数据非常重要(尤其是对于非托管资源)。 PerformanceResourceTiming接口具有三个属性,可用于获取有关资源的大小数据。 transferSize属性返回获取的资源的大小(以八位字节为单位),包括响应头字段和响应有效载荷主体。 encodedBodySize属性返回从有效内容主体的提取(HTTP或缓存)接收的大小(以八位字节为单位),然后再删除任何应用的内容编码。 decodedBodySize从抓取(HTTP或高速缓存)的接收到的返回的大小(以字节)消息主体,之后除去任何施加的内容编码。
*/
function display_size_data(){
var list = performance.getEntriesByType("resource");
// For each "resource", display its *Size property values
console.log("= Display Size Data");
for (var i=0; i < list.length; i++) {
console.log("== Resource[" + i + "] - " + list[i].name);
if ("decodedBodySize" in list[i])
console.log("... decodedBodySize[" + i + "] = " + list[i].decodedBodySize);
else
console.log("... decodedBodySize[" + i + "] = NOT supported");
if ("encodedBodySize" in list[i])
console.log("... encodedBodySize[" + i + "] = " + list[i].encodedBodySize);
else
console.log("... encodedBodySize[" + i + "] = NOT supported");
if ("transferSize" in list[i])
console.log("... transferSize[" + i + "] = " + list[i].transferSize);
else
console.log("... transferSize[" + i + "] = NOT supported");
}
}
用户行为回溯
用户行为回溯: 是把页面上发生的各个事件节点定义为用户行为,包括页面加载、路由跳转、页面单击、API请求、控制台输出等信息,日志中心 按照时间顺序将用户行为串联起来就是用户的行为链路。通过出错时的行为回溯可以帮助开发者复现问题
如:下例所示 时间 事件类型 事件内容 09:09:47 页面浏览 http://10.10.168.49:54712/check.html 09:09:55 foucsout select标签(出版社) select-value:国家人民出版社 09:09:56 点击 BUTTON标签 (查询) 09:09:56 request post http://10.10.168.49:54712/ searchbook FormData: 09:09:57 response 状态:200 ** 09:09:58 发生错误 CustomizeError: 09:09:58 控制台输出 CustomizeError: Warning: Each child in a list should have a unique "key" prop.%s%s See https://fb.me/react-warning-keys for more information.%s
其中涉及到
- 页面访问监控
- 页面元素监控
- API请求拦截
- 控制台拦截
其中 1. 页面访问监控 与 3. API请求拦截 已经在上面介绍了。
下面介绍一下 页面元素控制 及 控制台拦截 技术实现
页面元素监控
实现原理:给整个window对象的unload、click、dblclick、keydown、focus事件设置监听器。当监听到这几类事件时,会执行dealEvent方法进行数据上传。
例:PTO js 全埋点部分实现 //监听页面离开unload的事件 window.addEventListener('unload', function (e) { endDate = new Date(); e.restTime = endDate - startDate; e.listnerEvent = "unload"; bp_config.dealEvent(e); }, true);
//监听click的事件 window.addEventListener('click', function (e) { clearTimeout(clickTimeId); clickTimeId = setTimeout(function() { e.listnerEvent = "click"; bp_config.dealEvent(e); }, 500); }, true);
//监听双击dbclick事件 window.addEventListener('dblclick', function (e) { clearTimeout(clickTimeId); e.listnerEvent = "dblclick"; bp_config.dealEvent(e); }, true);
//监听按键事件 window.addEventListener('keydown', function (e) { e.listnerEvent = "keydown"; if(e.altKey||e.shiftKey||e.ctrlKey||e.metaKey){ e.preventDefault(); if(e.keyCode != 16&&e.keyCode != 17&&e.keyCode != 18&&e.keyCode != 36 ){//组合键被按下 bp_config.dealEvent(e); } } }, true); //监听input的事件 for (var i = 0; i < inputList.length; i++) { inputList[i].addEventListener('focus', function (e) { e.listnerEvent = "focus"; bp_config.dealEvent(e); }, true); }
控制台拦截
通过对 控制台 console 原型进行改写 ,可以获取控制台输出
// 伪代码 重写console.error 其他类型
const oldError = console.error;
const oldLog = console.log;
console.error = function () {
setTimeout(function () {
//进行上报
}, 0);
return oldError.apply(console, arguments);
};
console.log = function () {
setTimeout(function () {
//进行上报
}, 0);
return oldLog.apply(console, arguments);
};
总结
经过上面基础数据的采集,进行汇总统计后,我们可以得到如下能力
- 页面各阶段耗时统计: 重定向耗时、DNS查询耗时、TCP链接耗时、HTTP请求耗时、解析DOM树耗时、DOM Ready时间、onload时间等。
- 错误统计: 精细化分析每一个报错问题,源码定位。通过探针监控和上报线上环境的报错,以及一些自定义异常,可以准确定位到代码的问题所在。同时能够看到每一个报错的变化趋势。
- 性能分析: 分析页面和接口性能,加载耗时,成功率。探针对页面的加载性能进行分析,直观反映在报表之上。也对接口的性能进行了分析,如:耗时、成功率等。
- 全链路性能监控: 结合操作日志和调用链实现全链路性能监控视图, 能够全面准确确定系统性能瓶颈
- 资源监测分析: 监控静态资源加载情况,统计分析静态资源加载失败的情况,分析前端白屏
从而很方便解决如下场景的问题:
- 系统上线后,发现访问页面速度 相对比前老系统 变慢了。 客户要求解决,否则极其影响用户体验。 有了前端监控后我们可以按如下进行排查
● 如果页面加载的首次渲染时间偏高,或DNS查询、TCP连接和SSL建连耗时偏高,表示页面打开缓慢是由客户网络原因造成的。 ● 如果页面 DOM Ready耗时偏高,或页面加载的请求响应和内容传输耗时偏高,表示页面打开缓慢的原因可能是API请求缓慢。 那可以在API请求列表 里查看整体耗时和微服务调用链的调用时序图,查看链路上哪部分内容耗时较长,继而定位问题。
- 系统上线后,发现系统不稳定 ,界面操作有报错现象。 前端监控后我们可以按如下进行排查,复现
● 查看监控 错误列表,针对前端上报的错误进行排查 。 ● 最好的辅助排查方式就是复现,前端监控把页面上发生的各个事件节点定义为用户行为,包括页面加载、路由跳转、页面单击、API请求、控制台输出等信息,按照时间顺序将用户行为串联起来就是用户的行为链路。通过出错时的行为回溯可以帮助开发者复现问题
- 随着页面越来越多,功能越来越复杂,主动发现系统问题
● 通过实时前端监控,及时发现上报错错误、访问慢接口 主动及时优化系统,
后续
上面是前端监控系统前端技术的研究,前端监控系统中前端探针js正在开发,开发中肯定会遇到各种坑,后续文章会记录下各种坑。同时欢迎各位大佬指正