1.为什要做前端监控
- 更快发现问题和解决问题
- 做产品的决策依据
- 提升前端工程师的技术深度和广度,打造简历亮点
- 为业务扩展提供了更多可能性
2.前端监控目标
2.1 稳定性(stability)
错误名称 | 备注 |
---|---|
JS错误 | JS执行错误或者promise异常 |
资源异常 | script、link等资源加载异常 |
接口错误 | ajax或fetch请求接口异常 |
白屏 | 页面空白 |
2.2 用户体验(experience)
错误名称 | 备注 |
---|---|
加载时间 | 各个阶段的加载时间 |
TTFB(time to first byte)(首字节时间) | 是指浏览器发起第一个请求到数据返回第一个字节所消耗的时间,这个时间包含了网络请求时间、后端处理时间 |
FP(First Paint)(首次绘制) | 首次绘制包括了任何用户自定义的背景绘制,它是将第一个像素点绘制到屏幕的时刻 |
FCP(First Content Paint)(首次内容绘制) | 首次内容绘制是浏览器将第一个DOM渲染到屏幕的时间,可以是任何文本、图像、SVG等的时间 |
FMP(First Meaningful paint)(首次有意义绘制) | 首次有意义绘制是页面可用性的量度标准 |
FID(First Input Delay)(首次输入延迟) | 用户首次和页面交互到页面响应交互的时间 |
卡顿 | 超过50ms的长任务 |
2.3 业务(business)
错误名称 | 备注 |
---|---|
PV | page view 即页面浏览量或点击量 |
UV | 指访问某个站点的不同IP地址的人数 |
页面的停留时间 | 用户在每一个页面的停留时间 |
3.前端监控流程
- 前端埋点
- 数据上报
- 分析和计算 将采集到的数据进行加工汇总
- 可视化展示 将数据按各种维度进行展示
-
监控报警 发现问题后按一定的条件触发报警
3.1 常见的埋点方案
3.1.1 代码埋点
- 代码埋点,就是以嵌入代码的形式进行埋点,比如需要监控用户的点击事件,会选择在用户点击时,插入一段代码,保存这个监听行为或者直接将监听行为以某一种数据格式直接传递给服务器端
- 优点是可以在任意时刻,精确的发送或保存所需要的数据信息
- 缺点是工作量较大
3.1.2 可视化埋点
- 通过可视化交互的手段,代替代码埋点
- 将业务代码和埋点代码分离,提供一个可视化交互的页面,输入为业务代码,通过这个可视化系统,可以在业务代码中自定义的增加埋点事件等等,最后输出的代码耦合了业务代码和埋点代码
- 可视化埋点其实是用系统来代替手工插入埋点代码
3.1.3 无痕埋点
- 前端的任意一个事件都被绑定一个标识,所有的事件都别记录下来
- 通过定期上传记录文件,配合文件解析,解析出来我们想要的数据,并生成可视化报告供专业人员分析
- 无痕埋点的优点是采集全量数据,不会出现漏埋和误埋等现象
- 缺点是给数据传输和服务器增加压力,也无法灵活定制数据结构
4.编写监控采集脚本
4.1 开通日志服务
- 日志服务(Log Service,简称 SLS)是针对日志类数据一站式服务,用户无需开发就能快捷完成数据采集、消费、投递以及查询分析等功能,帮助提升运维、运营效率,建立 DT 时代海量日志处理能力
- 日志服务帮助文档
- Web Tracking
4.2 监控错误
4.2.1 错误分类
- JS错误
- JS错误
- Promise异常
- 资源异常
- 监听error
4.2.2 数据结构设计
- 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"//选择器
}
- promiseError
{
"title": "前端监控系统",//页面标题
"url": "http://localhost:8080/",//页面URL
"timestamp": "1590815290600",//访问时间戳
"userAgent": "Chrome",//用户浏览器类型
"kind": "stability",//大类
"type": "error",//小类
"errorType": "promiseError",//错误类型
"message": "someVar is not defined",//类型详情
"filename": "http://localhost:8080/",//访问的文件名
"position": "24:29",//行列信息
"stack": "http://localhost:8080/:24:29^new Promise ()^btnPromiseClick (http://localhost:8080/:23:13)^HTMLInputElement.onclick (http://localhost:8080/:15:86)",//堆栈信息
"selector": "HTML BODY #container .content INPUT"//选择器
}
- resourceError
{
"title": "前端监控系统",//页面标题
"url": "http://localhost:8080/",//页面URL
"timestamp": "1590816168643",//访问时间戳
"userAgent": "Chrome",//用户浏览器类型
"kind": "stability",//大类
"type": "error",//小类
"errorType": "resourceError",//错误类型
"filename": "http://localhost:8080/error.js",//访问的文件名
"tagName": "SCRIPT",//标签名
"timeStamp": "76",//时间
"selector": "HTML BODY SCRIPT"//选择器
}
4.2.3 实现
实现代码见项目
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=aniu"
}
4.3.2 实现
实现代码见项目
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 实现
- screen 返回当前window的screen对象,返回当前渲染窗口中和屏幕有关的属性
- innerWidth 只读的 Window 属性 innerWidth 返回以像素为单位的窗口的内部宽度
- innerHeight 窗口的内部高度(布局视口)的高度
- layout_viewport
- elementsFromPoint方法可以获取到当前视口内指定坐标处,由里到外排列的所有元素
实现代码见项目
4.5 加载时间
- PerformanceTiming
- DOMContentLoaded
- FMP
4.5.1 阶段含义
字段 | 含义 |
---|---|
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 | 加载文档到看到第一帧非空图像的时间,也叫白屏时间 |
首次可交互时间 | 首次可交互时间 | domInteractive-fetchStart | DOM树解析完成时间,此时document.readyState为interactive |
首包时间耗时 | 首包时间 | responseStart-domainLookupStart | DNS解析到响应返回给浏览器第一个字节的时间 |
页面完全加载时间 | 页面完全加载时间 | loadEventStart - fetchStart | - |
onLoad | onLoad事件耗时 loadEventEnd – loadEventStart | - | - |
4.5.3 数据结构
{
"title": "前端监控系统",
"url": "http://localhost:8080/",
"timestamp": "1590828364183",
"userAgent": "chrome",
"kind": "experience",
"type": "timing",
"connectTime": "0",
"ttfbTime": "1",
"responseTime": "1",
"parseDOMTime": "80",
"domContentLoadedTime": "0",
"timeToInteractive": "88",
"loadTime": "89"
}
4.5.4 实现
实现代码见项目
4.6 性能指标
- PerformanceObserver.observe方法用于观察传入的参数中指定的性能条目类型的集合。当记录一个指定类型的性能条目时,性能监测对象的回调函数将会被调用
- entryType
- paint-timing
- event-timing
- LCP
- FMP
- time-to-interactive
字段 | 描述 | 备注 |
---|---|---|
FP | First Paint(首次绘制) | 包括了任何用户自定义的背景绘制,它是首先将像素绘制到屏幕的时刻 |
FCP | First Content Paint(首次内容绘制) | 是浏览器将第一个 DOM 渲染到屏幕的时间,可能是文本、图像、SVG等,这其实就是白屏时间 |
FMP | First Meaningful Paint(首次有意义绘制) | 页面有意义的内容渲染的时间 |
LCP | (Largest Contentful Paint)(最大内容渲染) | 代表在viewport中最大的页面元素加载的时间 |
DCL | (DomContentLoaded)(DOM加载完成) | 当 HTML 文档被完全加载和解析完成之后,DOMContentLoaded 事件被触发,无需等待样式表、图像和子框架的完成加载 |
L | (onLoad) | 当依赖的资源全部加载完毕之后才会触发 |
TTI | (Time to Interactive) 可交互时间 | 用于标记应用已进行视觉渲染并能可靠响应用户输入的时间点 |
FID | First Input Delay(首次输入延迟) | 用户首次和页面交互(单击链接,点击按钮等)到页面响应交互的时间 |
4.6.1 数据结构设计
- paint
{
"title": "前端监控系统",
"url": "http://localhost:8080/",
"timestamp": "1590828364186",
"userAgent": "chrome",
"kind": "experience",
"type": "paint",
"firstPaint": "102",
"firstContentPaint": "2130",
"firstMeaningfulPaint": "2130",
"largestContentfulPaint": "2130"
}
- firstInputDelay
{
"title": "前端监控系统",
"url": "http://localhost:8080/",
"timestamp": "1590828477284",
"userAgent": "chrome",
"kind": "experience",
"type": "firstInputDelay",
"inputDelay": "3",
"duration": "8",
"startTime": "4812.344999983907",
"selector": "HTML BODY #container .content H1"
}
4.6.2 实现
实现代码见项目
4.7 卡顿
- 响应用户交互的响应时间如果大于100ms,用户就会感觉卡顿
4.7.1 数据设计
{
"title": "前端监控系统",
"url": "http://localhost:8080/",
"timestamp": "1590828656781",
"userAgent": "chrome",
"kind": "experience",
"type": "longTask",
"eventType": "mouseover",
"startTime": "9331",
"duration": "200",
"selector": "HTML BODY #container .content"
}
4.7.2 实现
实现代码见项目
4.8 pv
- netinfo
- RTT(Round Trip Time)一个连接的往返时间,即数据发送时刻到接收到确认的时刻的差值
- navigator.sendBeacon() 方法可用于通过HTTP将少量数据异步传输到Web服务器。
4.8.1 数据结构
{
"title": "前端监控系统",
"url": "http://localhost:8080/",
"timestamp": "1590829304423",
"userAgent": "chrome",
"kind": "business",
"type": "pv",
"effectiveType": "4g",
"rtt": "50",
"screen": "2049x1152"
}
4.8.2 实现
实现代码见项目