为什么要做前端监控
- 更快的发现问题和解决问题
- 做产品的决策依据
- 为业务扩展提供了更多可能性
- 提升前端工程师的技术深度和广度
前端监控目标
- js错误:js执行错误、promise异常
- 资源错误:js、css资源加载异常
- 接口错误:ajax、fetch请求接口异常
- 白屏:页面空白
前端监控流程
- 前端埋点
- 数据上报
- 加工汇总
- 可视化展示
- 监控报警
常见的埋点方式
1.代码埋点
嵌入代码的形式
- 优点:精确(任意时刻,数据量全面)
- 缺点:代码工作量大
2.可视化埋点
- 通过可视化交互手段,代替代码埋点
- 将业务代码和埋点代码分离,提供一个可视化的页面,输入业务代码,通过这个系统,可以在业务代码中自定义的增加埋点事件等,最后输出的代码耦合了业务代码和埋点代码
- 用系统来代替手工插入埋点代码
3.无痕埋点
前端的人一个事件被绑定一个表示,所有的事件都被记录下来 通过定期上传记录文件,配合文件解析,解析出想要的数据,生成可视化报告供专业人员分析 无痕埋点的优势点是采集全量数据,不会出现漏埋和误埋等现象 缺点是给数据传输和服务器增加压力,无法灵活定制数据结构
编写采集代码
新建 myMonitor 文件在终点站中打开 npm init -y 初始化项目
项目目录结构
myMonitor
|-- dist //编译打包后
|-- node_modules //依赖
|-- src
|-- mointor //主要逻辑代码
|-- lib
|-- utils
|-- index.js
|-- index.html
|-- index.js
|-- package.json //依赖的配置文件
|-- webpack.config.js //项目配置文件
webpack 配置
在myMonitor目录下新建 webpack.config.js
const path = require("path");
// const webpack打包项目的 HtmlWebpackPlugin 生产出html文件,user-agent
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry:'./src/index.js',//入口文件
context: process.cwd(),//上下文目录
mode:'development',//开发模式
output:{
path:path.resolve(__dirname,'dist'),//输出目录
filename:'monitor.js',//文件名
},
devServer:{
contentBase:path.resolve(__dirname,"dist"),//devServer静态文件根目录
before(router){
router.get('/success',function(req,res){
res.json({id:1})//200
})
router.post('/error',function(req,res){
res.sendStatus(500)//500
})
}
},
plugins:[
new HtmlWebpackPlugin({//自动打包出HTML 文件
template:'./src/index.html',
inject:'head'
})
]
}
1.接入日志系统
各公司一般有自己的日志系统,接收数据上报。
也可以使用:阿里云日志服务 www.aliyun.com/product/sls…
以下案例使用阿里云日志服务进行日志上报 配置阿里云上报接口
在 myMonitor/src/utils/新建 tracker.js
let host = "cn-beijing.log.aliyuncs.com"
let project = "xxxxxx";
let logStore = "xxxxx";
let userAgent = require('user-agent')
function getExtraData(){
return {
title:document.title,//页面名称
url:location.href,//请求地址
timestamp:Date.now(),//发送时间
userAgent:userAgent.parse(navigator.userAgent).name,//浏览器名称
}
}
class SendTracker(){
constructor(){
this.url = `http://${project}.${host}/logstores/${logStore}/track`,//上报的路径
this.xhr = new XMLHttpRequest;
}
send(data={}){
let extraDate = getExtraData();
let log = {...extraDate,...data};
//对象储存值不能是数字
for(let key in log){
if(typeof log[key] === "number"){
log[key] = `${log[key]}`;
}
}
let body = JSON.stringify({
__logs__:[log]
});
this.xhr.open('POST',this.url,true);
this.xhr.setRequestHeader('Content-type','application/json');//请求体类型
this.xhr.setRequestHeader('x-log-apiversion','0.6.0');//版本号
this.xhr.setRequestHeader('x-log-bodyrawsize',body.length);//请求大小
this.xhr.onload = function(){
// console.log(this.xhr.response);
}
this.xhr.onerror = function(error){
// console.log(error);
}
this.xhr.send(body)
}
}
export default new SendTracker();
2.监控错误
错误分类
js 错误(js 执行错误,promise 异常)
错误结构数据分析
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
...
"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"//选择器
监控 js 错误逻辑实现
实现逻辑:通过window.addEventListener监听error错误进行错误日志上报
在 myMonitor/src/utils/新建 getLastEvent.js 用于获取报错事件类型
let lastEvent;
['click','touchstart','mousedown','keydown','mouseover'].forEach(eventType => {
document.addEventListener(eventType,(event)=>{
event.path = event.path || (event.composedPath && event.composedPath())//浏览器兼容问题
lastEvent = event
},{
capture:true,//捕获阶段
passive:true//默认不阻止默认事件
})
})
export default function(){
return lastEvent;
}
在 myMonitor/src/utils/新建 getSelector.js
//将 [input#errorBtn, div.content, div#container, body, html, document, Window] 转换为lastEvent html body div#container div.content input#errorBtn
function getSelectors(path){
return path.reverse().filter(element=>{
return element !== document && element !== window;
}).map(element => {
let selector = ''
if(element.id){
return `${element.nodeName.toLowerCase()}#${element.id}`;
}else if(element.className && typeof element.className === "string"){
return `${element.nodeName.toLowerCase()}.${element.className}`
}else{
selector = element.nodeName.toLowerCase()
}
// console.log('selector',selector);
return selector;
}).join(' ');
}
export default function (pathOrTarget){
if(Array.isArray(pathOrTarget)){
return getSelectors(pathOrTarget) //可能是一个数组
}else{
let path = [];
while(pathOrTarget){
path.push(pathOrTarget);
pathOrTarget = pathOrTarget.parentNode;
}
return getSelectors(path)//也有可以能是一个对象
}
}
在 myMonitor/src/lib/新建 jsError.js
import gatLastEvent from "../utils/getLastEvent"
import getSelector from "../utils/getSelector";
import tracker from "../utils/tracker";
//全局监听错误信息,如报错就上报日志
window.addEventListener('error',function(){
let lastEvent = getLastEvent();//获取最后一个交互事件
tracker.send({
kind:'stability',//监控指标大类
type:"error",//小类型这是一个错误
errorType:"jsError",// 错误类型(js执行错误)
message:evnet.message,//错误详情
filename:evnet.filename,//访问文件名
position:`${evnet.lineno}:${evnet.colno}`, //行列信息
stack: evnet.error ? getLines(evnet.error.stack) : '',//堆栈信息
selector: lastEvent ? getSelector(lastEvent.path) : '',//选择器
})
},true)//true代表在捕获阶段调用,false代表在冒泡阶段捕获,使用true或false都可以
//getLines()
//将TypeError: Cannot set properties of undefined (setting 'error')
// at errorClick (index.html:27:34)
// at HTMLInputElement.onclick (index.html:15:87)
//转换为"errorClick (http://127.0.0.1:5555/monitor/dist/index.html:27:34)^HTMLInputElement.onclick (http://127.0.0.1:5555/monitor/dist/index.html:15:87)"
function getLines(stack){
return stack.split('\n').slice(1).map(item => item.replace(/^\s+at\s+/g,"")).join('^');
}
资源加载错误 判断
import getLastEvent from "../utils/getLastEvent";
import getSelector from "../utils/getSelector";
import tracker from "../utils/tracker";
export function injectJsError(){
//全局监听错误信息,如报错就上报日志
window.addEventListener('error',function(event){
let lastEvent = getLastEvent();//获取最后一个交互事件
/* ++++ start ++++*/
//判断是否资源加载错误
if(event.targat && (evnet.target.src || event.target.href)){
tracker.send({
kind:'stability',//监控指标大类
type:"error",//小类型这是一个错误
errorType:"resourceError",//js或css资源加载错误
filename:evnet.target.src || evnet.target.href,//那个文件报错了
tarName:evnet.target.tarName, //SCRIPT
selector: getSelector(evnet.target),//代表最后一个操作的元素
})
}else{
//js 错误日志上报
tracker.send({
kind:'stability',//监控指标大类
type:"error",//小类型这是一个错误
errorType:"jsError",//js执行错误
message:evnet.message,//报错信息
filename:evnet.filename,//那个文件报错了
position:`${evnet.lineno}:${evnet.colno}`,
stack: evnet.error ? getLines(evnet.error.stack) : '',
selector: lastEvent ? getSelector(lastEvent.path) : '',//代表最后一个操作的元素
})
}
/* ++++ end ++++*/
},true)
function getLines(stack){
return stack.split('\n').slice(1).map(item => item.replace(/^\s+at\s+/g,"")).join('^');
}
}
Promise 通过监听 unhandledrejection
错误进行日志上报
import getLastEvent from "../utils/getLastEvent";
import getSelector from "../utils/getSelector";
import tracker from "../utils/tracker";
export function injectJsError(){
//全局监听错误信息,如报错就上报日志
window.addEventListener('error',function(event){
let lastEvent = getLastEvent();//获取最后一个交互事件
//判断是否资源加载错误
if(event.targat && (evnet.target.src || event.target.href)){
tracker.send({
kind:'stability',//监控指标大类
type:"error",//小类型这是一个错误
errorType:"resourceError",//js或css资源加载错误
filename:evnet.target.src || evnet.target.href,//那个文件报错了
tarName:evnet.target.tarName, //SCRIPT
selector: getSelector(evnet.target),//代表最后一个操作的元素
})
}else{
//js 错误日志上报
tracker.send({
kind:'stability',//监控指标大类
type:"error",//小类型这是一个错误
errorType:"jsError",//js执行错误
message:evnet.message,//报错信息
filename:evnet.filename,//那个文件报错了
position:`${evnet.lineno}:${evnet.colno}`,
stack: evnet.error ? getLines(evnet.error.stack) : '',
selector: lastEvent ? getSelector(lastEvent.path) : '',//代表最后一个操作的元素
})
}
},true)
function getLines(stack){
return stack.split('\n').slice(1).map(item => item.replace(/^\s+at\s+/g,"")).join('^');
}
/* ++++ start ++++*/
//Promose 异常捕获
window.addEventListener('unhandledrejection',function(event){
let lastEvent = getLastEvent();//最后一个交互事件
let massage,filename,line =0,column = 0, stack="",reason = event.reason;
if(typeof reason === "string"){
message = reason;
}else if(typeof reason === "object"){
if(reason.stack){
let matchResult = reason.stack.match(/at\s+(.+):(\d+):(\d+)/);
filename = matchResult[1];
line = matchResult[2]
column = matchResult[3];
}
stack = getLones(reason.stack)
}
tracker.send({
kind:'stability',//监控指标大类
type:"error",//小类型这是一个错误
errorType:"promiseError",//js执行错误
message,//报错信息
filename,//那个文件报错了
position:`${line}:${column}`,
stack,
selector: lastEvent ? getSelector(lastEvent.path) : '',//代表最后一个操作的元素
})
},true);
/* ++++ end ++++*/
function getLines(stack){
return stack.split('\n').slice(1).map(item => item.replace(/^\s+at\s+/g,"")).join('^');
}
}
测试
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>前端监控SDK</title>
<link rel="stylesheet" href="./index.css">
</head>
<body>
<div id="container"> </div>
<script src="cxxx"></script>
<script>
function errorClick(e){
window.someVar.error = 'error'
}
</script>
<script src="./index.js"></script>
</body>
</html>
网络请求错误
数据接口设计
{
"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": "" //参数
}
ajax axios fetch 实现网络请求错误日志上报
- 重写xhr的open、send方法
- 监听load、error、abort事件
在 myMonitor/src/lib/新建 xxx.js
原生XMLHttpRequest
实现
这段代码是一个用于监控XHR请求的工具函数,其主要作用是使得我们能够记录下所有发送XHR请求的信息,包括请求的url,请求的方法(GET、POST等),请求的参数等,并且将这些信息通过一个tracker模块发送到后台服务器上,以便进行可靠性分析和出错定位。
具体来说,这个injectXHR函数是一个用于注入XHR的方法,它将会对XMLHttpRequest的open和send方法进行重写。
在重写open方法时,我们会先判断当前请求的url是否和日志收集服务以及websocket通信相关的请求,如果请求的url不属于这些范畴,我们就会将请求的相关信息存储在logData对象中,以备后续使用。然后,我们会调用原生open方法,但不会将返回值直接返回,因为我们需要在调用send方法时记录下XHR的状态。
在重写send方法时,我们会判断是否存在logData对象,如果存在,则表明这个XHR请求是我们所需要进行监控的,我们会在send方法中为XHR添加各个状态的回调函数,并在回调函数中记录下XHR的状态信息,包括请求持续时间,响应体等。然后,我们将这些数据通过tracker模块发送到后台服务器上,以便进行可靠性分析和出错定位。
// 引入tracker模块
import tracker from '../utils/tracker'
// 定义injectXHR函数对XMLHttpRequest的open和send进行重写
export function injectXHR(){
console.log('错误监控开始了')
// 存储原生XMLHttpRequest对象
const XHMLHttpRequest = window.XMLHttpRequest;
// 重写open方法
const oldOpen = XHMLHttpRequest.prototype.open;
XHMLHttpRequest.prototype.open = function(method, url, async){
// 过滤掉和日志收集服务以及websocket通信相关的请求
if(!url.match(/logstores/) && !url.match(/sockjs/)){
// 存储请求数据
this.logData = {method, url, async}
}
// 调用原生open方法
return oldOpen.apply(this, arguments)
}
// 重写send方法
const oldSend = XHMLHttpRequest.prototype.send;
XHMLHttpRequest.prototype.send = function(body){
if(this.logData){
// 记录发送前的时间
const startTime = Date.now();
// 定义回调函数记录XHR的状态
const handler = (type) => (event) => {
// 计算持续时间
const duration = Date.now() - startTime;
// 获取XHR的状态码和状态信息
const status = this.status; // 200 or 500
const statusText = this.statusText; // ok or Server Error
// 将收集到的数据发送到后台
tracker.send({
kind: 'stability', // 稳定性
type: 'xhr', // xhr请求
eventType: type, // 请求的事件类型(load/error/abort)
pathname: this.logData.url, // 请求的路径
status: `${status}-${statusText}`, // 状态码和状态信息
duration, // 请求持续时间
response: this.response ? JSON.stringify(this.response) : '', // 响应体
params: body || "" // 发送的参数
});
}
// 添加监听回调
this.addEventListener('load', handler('load'), true)
this.addEventListener('error', handler('error'), true)
this.addEventListener('abort', handler('abort'), true);
}
// 调用原生send方法
return oldSend.apply(this, arguments)
}
}
axios
听过拦截实现错误日志上报
在这里,我们使用axios的拦截器,对请求和响应进行拦截,并处理相关日志数据,最后进行上报。其中:
- 在请求拦截器中,我们过滤掉和日志收集服务以及websocket通信相关的请求,并将请求数据存储在config.logData中。
- 在响应拦截器中,我们根据config.logData判断当前请求是否需要上报,计算持续时间,并把收集到的数据发送到后台。
- 对于error,我们在响应拦截器的rejected中捕获,同时用Promise.reject(error)继续向下传递,实现错误的统一处理。
至此,完成了用axios实现请求日志收集服务的功能。
import axios from 'axios';
import tracker from '../utils/tracker';
export function axiosXHRLogs(){
// 拦截请求之前的处理
axios.interceptors.request.use(config => {
console.log('axios','请求拦截开始')
// 过滤掉和日志收集服务以及websocket通信相关的请求
if (!config.url.match(/logstores/) && !config.url.match(/sockjs/)) {
// 存储请求数据
config.logData = {
method: config.method,
url: config.url,
async: config.async
};
}
return config;
});
// 拦截响应之后的处理
axios.interceptors.response.use(response => {
if(response.config.logData){
// 记录发送前的时间
const startTime = Date.now();
// 获取XHR的状态码和状态信息
const status = response.status; // 200 or 500
const statusText = response.statusText; // ok or Server Error
// 计算持续时间
const duration = Date.now() - startTime;
// 将收集到的数据发送到后台
data = {
kind: 'stability', // 稳定性
type: 'xhr', // xhr请求
eventType: 'success', // 请求的事件类型(success/error/abort)
pathname: response.config.logData.url, // 请求的路径
status: `${status}-${statusText}`, // 状态码和状态信息
duration, // 请求持续时间
response: response.data ? JSON.stringify(response.data) : '', // 响应体
params: response.config.data || "" // 发送的参数
}
console.log("response",data)
tracker.send(data);
}
return response;
}, error => {
if(error.config.logData){
// 记录发送前的时间
const startTime = Date.now();
// 获取XHR的状态码和状态信息
const status = error.response ? error.response.status : '';
const statusText = error.response ? error.response.statusText : '';
// 计算持续时间
const duration = Date.now() - startTime;
// 将收集到的数据发送到后台
data = {
kind: 'stability', // 稳定性
type: 'xhr', // xhr请求
eventType: 'error', // 请求的事件类型(success/error/abort)
pathname: error.config.logData.url, // 请求的路径
status: `${status}-${statusText}`, // 状态码和状态信息
duration, // 请求持续时间
response: error.response ? JSON.stringify(error.response.data) : '', // 响应体
params: error.config.data || "" // 发送的参数
}
console.log('error',data)
tracker.send(data);
}
return Promise.reject(error);
});
}
监听所有fetch 请求进行拦截
与XHR的拦截不同,fetch的拦截需要对所有fetch请求进行拦截并进行判断,如果请求地址匹配,则不进行日志收集,直接返回原有的fetch,否则对请求和响应进行处理并进行日志收集。其中:
- 在请求过程中,我们通过传入的options参数判断请求是否需要被拦截并进行日志收集,并把请求数据以及初始的fetch函数参数传递进入then方法以处理响应。对于不需要被处理的请求,我们直接返回原有的fetch函数。
- 在响应过程中,我们计算持续时间,并把收集到的数据发送到后台。对于发生错误的请求,我们记录错误响应体,并用throw将错误继续向下传递。
至此,完成了用fetch实现请求日志收集服务的功能。
import tracker from '../utils/tracker';
/**
* 重写fetch函数
*/
function fetchInterceptor(fetchFunc) {
return function (url, options) {
// 过滤掉和日志收集服务以及websocket通信相关的请求
if (!url.match(/logstores/) && !url.match(/sockjs/)) {
return fetchFunc.apply(this, arguments).then(res => {
// 记录发送前的时间
const startTime = Date.now();
// 获取XHR的状态码和状态信息
const status = res.status; // 200 or 500
const statusText = res.statusText; // ok or Server Error
// 计算持续时间
const duration = Date.now() - startTime;
// 发送请求日志
tracker.send({
kind: 'stability', // 稳定性
type: 'fetch', // fetch请求
eventType: 'success', // 请求的事件类型(success/error/abort)
pathname: url, // 请求的路径
status: `${status}-${statusText}`, // 状态码和状态信息
duration, // 请求持续时间
params: options.body ? options.body : '', // 发送的参数
response: res.text() // 响应体(转成text()后再处理)
});
return res;
}).catch(error => {
// 记录发送前的时间
const startTime = Date.now();
// 获取XHR的状态码和状态信息
const status = error.response ? error.response.status : '';
const statusText = error.response ? error.response.statusText : '';
// 计算持续时间
const duration = Date.now() - startTime;
// 发送请求日志
tracker.send({
kind: 'stability', // 稳定性
type: 'fetch', // fetch请求
eventType: 'error', // 请求的事件类型(success/error/abort)
pathname: url, // 请求的路径
status: `${status}-${statusText}`, // 状态码和状态信息
duration, // 请求持续时间
params: options.body ? options.body : '', // 发送的参数
response: error.response ? error.response.text() : '' // 错误响应体(转成text()后再处理)
});
throw error;
});
} else {
// 如果匹配成功,直接返回原有的fetch,不进行日志收集
return fetchFunc.apply(this, arguments);
}
}
}
// 重新定义fetch
window.fetch = fetchInterceptor(window.fetch);
白屏
白屏就是页面中什么都没有
数据设计
{
"title": "前端监控系统",
"url": "http://localhost:8080/",
"timestamp": "1590822618759",
"userAgent": "chrome",
"kind": "stability", //大类
"type": "blank", //小类
"emptyPoints": "0", //空白点
"screen": "2049x1152", //分辨率
"viewPoint": "2048x994", //视口
"selector": "HTML BODY #container" //选择器
}
实现
- elementsFromPoint方法可以获取到当前视口内指定坐标处,由里到外排列的所有元素
- 根据 elementsFromPoint api,获取屏幕水平中线和竖直中线所在的元素
这段代码的功能是检测页面是否为空白,如果页面为空白则将相关信息发送给tracker,用于监控网站稳定性。具体实现逻辑如下:
- 引入tracker和onload模块。
- 定义blankScreen函数,该函数用于检测页面是否为空白。
- 在函数中定义一个wrapperElement数组,保存需要检测的节点。
- 定义emptyPoints变量,用于记录空白点数量。
- 定义getSelector函数,用于获取元素选择器。
- 定义isWrapper函数,用于检测元素节点是否在wrapperElement中。
- 在onload函数中,遍历屏幕10行10列的地方,并检查每个点处的元素是否wrapper,如果是则将空白点数量加1。
- 如果空白点数量大于等于18,则认为页面是空白的,将页面信息以及空白点数量发送给tracker。
- 最后,blankScreen函数的作用就是检测页面是否为空白,如果是则将相关信息发送给tracker,用于监控网站稳定性。
// 引入tracker和onload模块
import tracker from "../utils/tracker";
import {onload} from '../utils/onload'
// 定义blankScreen函数
export function blankScreen(){
// 定义wrapperElement数组,保存需要检测的节点
let wrapperElement = ['html', 'body', '#container', '.content'];
let emptyPoints = 0; // 定义空白点数量的变量
// 定义获取元素选择器的函数
function getSelector(element){
if(element.id){
return '#' + element.id;
}else if(element.className){ // 如果有类名,则将类名拼接成选择器
return '.' + element.className.split(' ').filter(item => !!item).join('.');
}else{ // 否则,返回元素节点名如div, p等
return element.nodeName.toLowerCase();
}
}
// 定义是否wrapper的函数,用来检测元素节点是否在wrapperElement中
function isWrapper(element){
let selector = getSelector(element);
if(wrapperElement.indexOf(selector) != -1){ // 判断是否在wrapperElement中
emptyPoints++; // 是wrapperElement则将空白点数量加1
}
}
// 当页面加载完成后进行检测
onload(function(){
// 遍历屏幕10行10列的地方,并检查每个点处的元素是否wrapper
for(let i = 1; i <= 9; i++){
let xElements = document.elementsFromPoint( window.innerWidth * i / 10, window.innerHeight / 2);
let yElements = document.elementsFromPoint(window.innerWidth / 2, window.innerHeight * i / 10);
console.log('sss',xElements[0])
isWrapper(xElements[0])
isWrapper(yElements[0])
}
// 如果空白点数量大于等于18,则认为页面是空白的
console.log('emptyPoints',emptyPoints)
if(emptyPoints >= 18){
// 将页面信息以及空白点数量发送给tracker
let centerElements = document.elementsFromPoint( window.innerWidth / 2, window.innerHeight /2);
const data = {
kind:"stability",
type:"blank",
emptyPoints,
screen:window.screen.width + "X" + window.screen.height,
viewPoint:window.innerHeight + "X" + window.innerHeight,
selector:getSelector(centerElements[0])
}
tracker.send(data)
};
})
}
加载时间
阶段含义
字段 | 含义 |
---|---|
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回调函数执行完成的时间 |
阶段计算
字段 | 描述 | 计算方式 | 意义 |
---|---|---|---|
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 |
数据结构
{
"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"
}
加载时间代码实现
import tracker from "../utils/tracker";
// 在页面加载完毕3秒后执行性能数据收集
function sendPerformanceData() {
setTimeout(() => {
// 获取Navigation Timing信息
const navigationTiming = performance.getEntriesByType("navigation")[0].toJSON();
// 对Navigation Timing进行解构
const {
fetchStart,
connectStart,
connectEnd,
requestStart,
responseStart,
responseEnd,
domLoading,
domInteractive,
domContentLoadedEventStart,
domContentLoadedEventEnd,
loadEventStart
} = navigationTiming;
// 获取首次绘制指标:first-paint
const { startTime: firstPaint } = performance.getEntriesByName('first-paint')[0] || {};
// 获取首次有意义绘制指标:first-contentful-paint
const { startTime: firstContentfulPaint } = performance.getEntriesByName("first-contentful-paint")[0] || {};
// 获取页面最大内容绘制指标:largest-contentful-paint
const { startTime: largestContentFulPaint } = performance.getEntriesByName("largest-contentful-paint")[0] || {};
// 对获取的各个指标的值进行处理
const connectTime = connectEnd - connectStart;
const ttfdTime = responseStart - requestStart;
const responseTime = responseEnd - requestStart;
const parseDOMTime = loadEventStart - domLoading;
const domContentLoadedTime = domContentLoadedEventEnd - domContentLoadedEventStart;
const timeToInteractive = domInteractive - fetchStart;
const loadTime = loadEventStart - fetchStart;
// 判断tracker是否可用,若可用则发送数据
if (typeof tracker !== "undefined") {
// 发送用户体验指标数据
tracker.send({
kind: "experience", // 用户体验指标
type: "timer", // 统计每个阶段时间
connectTime, // 链接时间
ttfdTime, // 首字节到达时间
responseTime, // 响应读取时间
parseDOMTime, // DOM 解析时间
domContentLoadedTime, // DOM内容加载完成时间
timeToInteractive, // 首次可交互时间
loadTime // 完整的加载时间
});
// 发送用户体验指标:首次绘制指标、首次有意义绘制指标和最大内容绘制指标
tracker.send({
kind: "experience", // 用户体验指标
type: "paint", // 统计每个阶段的时间
firstPaint, // 首次绘制时间
firstContentfulPaint, // 首次有意义绘制时间
largestContentFulPaint // 最大内容绘制时间
});
}
}, 3000);
};