前端 监控
Posted Jay_帅小伙
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了前端 监控相关的知识,希望对你有一定的参考价值。
概览
- 为什么要做前端监控
- 前端监控目标
- 前端监控流程
- 编写采集脚本
- 日志系统监控
- 错误监控
- 接口异常
- 白屏监控
- 加载时间
- 性能指标
- 卡顿
- pv
- 扩展问题
- 性能监控指标
- 前端怎么做性能监控
- 线上错误监控怎么做
- 导致内存泄漏的方法,怎么监控内存泄漏
- Node 怎么做性能监控
1. 为什么要做前端监控
- 更快的发现问题和解决问题
- 做产品的决策依据
- 为业务扩展提供了更多可能性
- 提升前端工程师的技术深度和广度,打造简历亮点
2. 前端监控目标
2.1 稳定性 stability
- js错误:js执行错误、promise异常
- 资源错误:js、css资源加载异常
- 接口错误:ajax、fetch请求接口异常
- 白屏:页面空白
2.2 用户体验 experience
2.3 业务 business
- pv:页面浏览量和点击量
- uv:访问某个站点的不同ip的人数
- 用户在每一个页面的停留时间
3. 前端监控流程
- 前端埋点
- 数据上报
- 加工汇总
- 可视化展示
- 监控报警
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GXA5Dhh3-1665996889919)(https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/bd91043540624db19d7bb390ee9752e4~tplv-k3u1fbpfcp-watermark.image?)]
3.1 常见的埋点方案
3.1.1 代码埋点
- 嵌入代码的形式
- 优点:精确(任意时刻,数据量全面)
- 缺点:代码工作量点
3.1.2 可视化埋点
- 通过可视化交互的手段,代替代码埋点
- 将业务代码和埋点代码分离,提供一个可视化交互的页面,输入为业务代码,通过这个系统,可以在业务代码中自定义的增加埋点事件等等,最后输出的代码耦合了业务代码和埋点代码
- 用系统来代替手工插入埋点代码
3.1.3 无痕埋点
- 前端的任意一个事件被绑定一个标识,所有的事件都被记录下来
- 通过定期上传记录文件,配合文件解析,解析出来我们想要的数据,并生成可视化报告供专业人员分析
- 无痕埋点的优点是采集全量数据,不会出现漏埋和误埋等现象
- 缺点是给数据传输和服务器增加压力,也无法灵活定制数据结构
4. 编写采集脚本
4.1 接入日志系统
- 各公司一般都有自己的日志系统,接收数据上报,例如:阿里云
4.2 监控错误
4.2.1 错误分类
- js错误(js执行错误,promise异常)
- 资源加载异常:监听error
4.2.2 数据结构分析
1. jsError
"title": "前端监控系统", // 页面标题
"url": "http://localhost:8080/", // 页面URL
"timestamp": "1590815288710", // 访问时间戳
"userAgent": "Chrome", // 用户浏览器类型
"kind": "stability", // 大类
"type": "error", // 小类
"errorType": "jsError", // 错误类型
"message": "Uncaught TypeError: Cannot set property 'error' of undefined", // 类型详情
"filename": "http://localhost:8080/", // 访问的文件名
"position": "0:0", // 行列信息
"stack": "btnClick (http://localhost:8080/:20:39)^htmlInputElement.onclick (http://localhost:8080/:14:72)", // 堆栈信息
"selector": "HTML BODY #container .content INPUT" // 选择器
2. promiseError
...
"errorType": "promiseError",//错误类型
"message": "someVar is not defined",//类型详情
"stack": "http://localhost:8080/:24:29^new Promise (<anonymous>)^btnPromiseClick (http://localhost:8080/:23:13)^HTMLInputElement.onclick (http://localhost:8080/:15:86)",//堆栈信息
"selector": "HTML BODY #container .content INPUT"//选择器
3. resourceError
...
"errorType": "resourceError",//错误类型
"filename": "http://localhost:8080/error.js",//访问的文件名
"tagName": "SCRIPT",//标签名
"timeStamp": "76",//时间
4.2.3 实现
1. 资源加载错误 + js执行错误
//一般JS运行时错误使用window.onerror捕获处理
window.addEventListener(
"error",
function (event)
let lastEvent = getLastEvent();
// 有 e.target.src(href) 的认定为资源加载错误
if (event.target && (event.target.src || event.target.href))
tracker.send(
//资源加载错误
kind: "stability", //稳定性指标
type: "error", //resource
errorType: "resourceError",
filename: event.target.src || event.target.href, //加载失败的资源
tagName: event.target.tagName, //标签名
timeStamp: formatTime(event.timeStamp), //时间
selector: getSelector(event.path || event.target), //选择器
);
else
tracker.send(
kind: "stability", //稳定性指标
type: "error", //error
errorType: "jsError", //jsError
message: event.message, //报错信息
filename: event.filename, //报错链接
position: (event.lineNo || 0) + ":" + (event.columnNo || 0), //行列号
stack: getLines(event.error.stack), //错误堆栈
selector: lastEvent
? getSelector(lastEvent.path || lastEvent.target)
: "", //CSS选择器
);
,
true
); // true代表在捕获阶段调用,false代表在冒泡阶段捕获,使用true或false都可以
2. promise异常
//当Promise 被 reject 且没有 reject 处理器的时候,会触发 unhandledrejection 事件
window.addEventListener(
"unhandledrejection",
function (event)
let lastEvent = getLastEvent();
let message = "";
let line = 0;
let column = 0;
let file = "";
let stack = "";
if (typeof event.reason === "string")
message = event.reason;
else if (typeof event.reason === "object")
message = event.reason.message;
let reason = event.reason;
if (typeof reason === "object")
if (reason.stack)
var matchResult = reason.stack.match(/at\\s+(.+):(\\d+):(\\d+)/);
if (matchResult)
file = matchResult[1];
line = matchResult[2];
column = matchResult[3];
stack = getLines(reason.stack);
tracker.send(
//未捕获的promise错误
kind: "stability", //稳定性指标
type: "error", //jsError
errorType: "promiseError", //unhandledrejection
message: message, //标签名
filename: file,
position: line + ":" + column, //行列
stack,
selector: lastEvent
? getSelector(lastEvent.path || lastEvent.target)
: "",
);
,
true
); // true代表在捕获阶段调用,false代表在冒泡阶段捕获,使用true或false都可以
4.3 接口异常采集脚本
4.3.1 数据设计
"title": "前端监控系统", //标题
"url": "http://localhost:8080/", //url
"timestamp": "1590817024490", //timestamp
"userAgent": "Chrome", //浏览器版本
"kind": "stability", //大类
"type": "xhr", //小类
"eventType": "load", //事件类型
"pathname": "/success", //路径
"status": "200-OK", //状态码
"duration": "7", //持续时间
"response": "\\"id\\":1", //响应内容
"params": "" //参数
"title": "前端监控系统",
"url": "http://localhost:8080/",
"timestamp": "1590817025617",
"userAgent": "Chrome",
"kind": "stability",
"type": "xhr",
"eventType": "load",
"pathname": "/error",
"status": "500-Internal Server Error",
"duration": "7",
"response": "",
"params": "name=zhufeng"
4.3.2 实现
使用webpack devServer模拟请求
- 重写xhr的open、send方法
- 监听load、error、abort事件
import tracker from "../util/tracker";
export function injectXHR()
let XMLHttpRequest = window.XMLHttpRequest;
let oldOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function (
method,
url,
async,
username,
password
)
// 上报的接口不用处理
if (!url.match(/logstores/) && !url.match(/sockjs/))
this.logData =
method,
url,
async,
username,
password,
;
return oldOpen.apply(this, arguments);
;
let oldSend = XMLHttpRequest.prototype.send;
let start;
XMLHttpRequest.prototype.send = function (body)
if (this.logData)
start = Date.now();
let handler = (type) => (event) =>
let duration = Date.now() - start;
let status = this.status;
let statusText = this.statusText;
tracker.send(
//未捕获的promise错误
kind: "stability", //稳定性指标
type: "xhr", //xhr
eventType: type, //load error abort
pathname: this.logData.url, //接口的url地址
status: status + "-" + statusText,
duration: "" + duration, //接口耗时
response: this.response ? JSON.stringify(this.response) : "",
params: body || "",
);
;
this.addEventListener("load", handler("load"), false);
this.addEventListener("error", handler("error"), false);
this.addEventListener("abort", handler("abort"), false);
oldSend.apply(this, arguments);
;
4.4 白屏
- 白屏就是页面上什么都没有
4.4.1 数据设计
"title": "前端监控系统",
"url": "http://localhost:8080/",
"timestamp": "1590822618759",
"userAgent": "chrome",
"kind": "stability", //大类
"type": "blank", //小类
"emptyPoints": "0", //空白点
"screen": "2049x1152", //分辨率
"viewPoint": "2048x994", //视口
"selector": "HTML BODY #container" //选择器
4.4.2 实现
- elementsFromPoint方法可以获取到当前视口内指定坐标处,由里到外排列的所有元素
- 根据 elementsFromPoint api,获取屏幕水平中线和竖直中线所在的元素
import tracker from "../util/tracker";
import onload from "../util/onload";
function getSelector(element)
var selector;
if (element.id)
selector = `#$element.id`;
else if (element.className && typeof element.className === "string")
selector =
"." +
element.className
.split(" ")
.filter(function (item)
return !!item;
)
.join(".");
else
selector = element.nodeName.toLowerCase();
return selector;
export function blankScreen()
const wrapperSelectors = ["body", "html", "#container", ".content"];
let emptyPoints = 0;
function isWrapper(element)
let selector = getSelector(element);
if (wrapperSelectors.indexOf(selector) >= 0)
emptyPoints++;
onload(function ()
let xElements, yElements;
debugger;
for (let i = 1; i <= 9; i++)
xElements = document.elementsFromPoint(
(window.innerWidth * i) / 10,
window.innerHeight / 2
);
yElements = document.elementsFromPoint(
window.innerWidth / 2,
(window.innerHeight * i) / 10
);
isWrapper(xElements[0]);
isWrapper(yElements[0]);
if (emptyPoints >= 0)
let centerElements = document.elementsFromPoint(
window.innerWidth / 2,
window.innerHeight / 2
);
tracker.send(
kind: "stability",
type: "blank",
emptyPoints: "" + emptyPoints,
screen: window.screen.width + "x" + window.screen.height,
viewPoint: window.innerWidth + "x" + window.innerHeight,
selector: getSelector(centerElements[0]),
);
);
//screen.width 屏幕的宽度 screen.height 屏幕的高度
//window.innerWidth 去除工具条与滚动条的窗口宽度 window.innerHeight 去除工具条与滚动条的窗口高度
4.5 加载时间
4.5.1 阶段含义
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5HB2c44P-1665996889921)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/7d30801313694b8bbf1e8977b8d8bd45~tplv-k3u1fbpfcp-watermark.image?)]
字段 | 含义 |
---|---|
navigationStart | 初始化页面,在同一个浏览器上下文中前一个页面unload的时间戳,如果没有前一个页面的unload,则与fetchStart值相等 |
redirectStart | 第一个HTTP重定向发生的时间,有跳转且是同域的重定向,否则为0 |
redirectEnd | 最后一个重定向完成时的时间,否则为0 |
fetchStart | 浏览器准备好使用http请求获取文档的时间,这发生在检查缓存之前 |
domainLookupStart | DNS域名开始查询的时间,如果有本地的缓存或keep-alive则时间为0 |
domainLookupEnd | DNS域名结束查询的时间 |
connectStart | TCP开始建立连接的时间,如果是持久连接,则与fetchStart 值相等 |
secureConnectionStart | https 连接开始的时间,如果不是安全连接则为0 |
connectEnd | TCP完成握手的时间,如果是持久连接则与fetchStart 值相等 |
requestStart | HTTP请求读取真实文档开始的时间,包括从本地缓存读取 |
requestEnd | HTTP请求读取真实文档结束的时间,包括从本地缓存读取 |
responseStart | 返回浏览器从服务器收到(或从本地缓存读取)第一个字节时的Unix毫秒时间戳 |
responseEnd | 返回浏览器从服务器收到(或从本地缓存读取,或从本地资源读取)最后一个字节时的Unix毫秒时间戳 |
unloadEventStart | 前一个页面的unload的时间戳 如果没有则为0 |
unloadEventEnd | 与unloadEventStart 相对应,返回的是unload 函数执行完成的时间戳 |
domLoading | 返回当前网页DOM结构开始解析时的时间戳,此时document.readyState 变成loading,并将抛出readyStateChange 事件 |
domInteractive | 返回当前网页DOM结构结束解析、开始加载内嵌资源时时间戳,document.readyState 变成interactive ,并将抛出readyStateChange 事件(注意只是DOM树解析完成,这时候并没有开始加载网页内的资源) |
domContentLoadedEventStart | 网页domContentLoaded事件发生的时间 |
domContentLoadedEventEnd | 网页domContentLoaded事件脚本执行完毕的时间,domReady的时间 |
domComplete | DOM树解析完成,且资源也准备就绪的时间,document.readyState 变成complete .并将抛出readystatechange 事件 |
loadEventStart | load 事件发送给文档,也即load回调函数开始执行的时间 |
loadEventEnd | load回调函数执行完成的时间 |
4.5.2 阶段计算
字段 | 描述 | 计算方式 | 意义 |
---|---|---|---|
unload | 前一个页面卸载耗时 | unloadEventEnd – unloadEventStart | - |
redirect | 重定向耗时 | redirectEnd – redirectStart | 重定向的时间 |
appCache | 缓存耗时 | domainLookupStart – fetchStart | 读取缓存的时间 |
dns | DNS 解析耗时 | domainLookupEnd – domainLookupStart | 可观察域名解析服务是否正常 |
tcp | TCP 连接耗时 | connectEnd – connectStart | 建立连接的耗时 |
ssl | SSL 安全连接耗时 | connectEnd – secureConnectionStart | 反映数据安全连接建立耗时 |
ttfb | Time to First Byte(TTFB)网络请求耗时 | responseStart – requestStart | TTFB是发出页面请求到接收到应答数据第一个字节所花费的毫秒数 |
response | 响应数据传输耗时 | responseEnd – responseStart | 观察网络是否正常 |
dom | DOM解析耗时 | domInteractive – responseEnd | 观察DOM结构是否合理,是否有JS阻塞页面解析 |
dcl | DOMContentLoaded 事件耗时 | domContentLoadedEventEnd – domContentLoadedEventStart | 当 HTML 文档被完全加载和解析完成之后,DOMContentLoaded 事件被触发,无需等待样式表、图像和子框架的完成加载 |
resources | 资源加载耗时 | domComplete – domContentLoadedEventEnd | 可观察文档流是否过大 |
domReady | DOM阶段渲染耗时 | domContentLoadedEventEnd – fetchStart | DOM树和页面资源加载完成时间,会触发domContentLoaded 事件 |
首次渲染耗时 | 首次渲染耗时 | responseEnd-fetchStart | 加载文档到看到第一帧非空图像 以上是关于前端 监控的主要内容,如果未能解决你的问题,请参考以下文章 |