前端监控

1.准备工作

【为什么要做前端监控】

初始化项目

1
npm init -y

新建 webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const path = require('path');
//HtmlWebpackPlugin自动产出HTML文件 user-agent 把浏览器的UserAgent变成一个对象
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静态文件根目录
},
plugins: [
new HtmlWebpackPlugin({//自动打包出HTML文件的
template: './src/index.html',
inject: 'head'
})
]
}

新建 src/index.html

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>前端监控SDK</title>
</head>
<body>

</body>
</html>

新建 src/index.js

安装包

1
npm install webpack webpack-cli webpack-dev-server html-webpack-plugin user-agent -D

修改package.json

1
2
3
4
"scripts": {
"build": "webpack",
"dev": "webpack-dev-server"
}

2.编写监控采集脚本

2.1 开通日志服务

2.1.1 步骤

1、【创建Project】 : 名称(front-end-monitor-test)、所属地域(杭州) 确定;
2、【创建logStore】:名称(font-end-monitor-store)、开启WebTracking 确定;
3、【创建成功,立即接入数据】:搜索WebTracking SDK、下一步、开启包含中文、下一步
4、【完成数据接入配置】:查询日志->立即尝试
5、【有数据之后设置索引】:https://sls.console.aliyun.com/lognext/profile 找到下面的Project下的项目,进入,查询分析属性-设置-开启日志聚类-自动生成索引-确定-确定

2.2 监控错误

2.2.1 错误分类

  • JS错误
    • JS错误
    • Promise异常
  • 资源异常
    -监听error

2.2.2 数据结构设计

1.jsError

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"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 (<anonymous>)^btnPromiseClick (http://localhost:8080/:23:13)^HTMLInputElement.onclick (http://localhost:8080/:15:86)",//堆栈信息
"selector": "HTML BODY #container .content INPUT"//选择器
}

3.resourceError

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"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"//选择器
}

2.2.3 实现

2.2.3.1 收集log信息

src/index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<body>
<div id="container">
<div class="content">
<input id="errorBtn" type="button" value="点击抛出错误" onclick="errorClick()" />
<input id="promiseBtn" type="button" value="点击抛出Promise错误" onclick="promiseErrorClick()" />
</div>
</div>
<script>
// JS错误
function errorClick() {
window.someVar.error = 'error';
}
// Promise错误
function promiseErrorClick() {
new Promise(function (resolve, reject) {
//window.someVar.error = 'error'; // 或者
reject('error --');
}).then(result=>{
console.log("result:", result);
});
}
</script>
<!-- 加载资源错误 -->
<script src="./someError.js"></script>
</body>

src/index.js

1
import './monitor/index';

新建 src/monitor/index.js

1
2
import {injectJsError} from './lib/jsError';
injectJsError();

新建 src/monitor/lib/jsError.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
import getLastEvent from '../util/getLastEvent';
import getSelector from '../util/getSelector';
import tracker from '../util/tracker';
export function injectJsError(){
// 监听全局未捕获的错误
window.addEventListener('error', function(event){ // 错误实践对象
console.log("error: ",event);
let lastEvent = getLastEvent(); // 监听到错误之后,获取到最后一个交互事件
// console.log("lastEvent:", lastEvent);
// 这是一个脚本加载错误
if (event.target && (event.target.src || event.target.href)) {
tracker.send({//资源加载错误
kind: 'stability',//稳定性指标
type: 'error',//resource
errorType: 'resourceError', // js或css资源加载错误
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',//监控指标的大类:stability'稳定性指标
type: 'error', // 小类型,这是一个错误
errorType: 'jsError', //JS执行错误
url: '', // 访问哪个路径报错了s
message:event.message, // 报错信息
filename: event.filename, // 哪个文件报错了
position: `${event.lineno}:${event.colno}`,//行列号
stack: getLines(event.error.stack), // 错误堆栈
selector: lastEvent ? getSelector(lastEvent.path) : '', // 选择器,代表最后一个操作的元素
});
}
}, true); // true代表在捕获阶段调用,false代表在冒泡阶段捕获,使用true或false都可以 (这里是捕获阶段去出发,冒泡阶段有可能阻止冒泡就捕获不到了)
//当Promise 被 reject 且没有 reject 处理器的时候,会触发 unhandledrejection 事件
window.addEventListener('unhandledrejection', function (event) {
console.log("unhandledrejection:",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都可以

function getLines(stack){
if (!stack) {
return '';
}
return stack.split('\n').slice(1).map(item => item.replace(/^\s+at\s+/g, '')).join('^');
}
}

新建 src/monitor/util/getLastEvent.js

1
2
3
4
5
6
7
8
9
10
11
12
13
let lastEvent;
// 每次在用户进行下面这些事件交互时,都会把最新的event赋值给lastEvent
['click','pointerdown', 'touchstart', 'mousedown', 'keydown', 'mouseover'].forEach(event => {
document.addEventListener(event, (event) => {
lastEvent = event;
}, {
capture: true,//capture 控制监听器是在捕获阶段执行还是在冒泡阶段执行
passive: true //passive 的意思是顺从的,表示它不会对事件的默认行为说 no
});
});
export default function () {
return lastEvent;
};

新建 src/monitor/util/getSelector.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const getSelector = function (path) {
return path.reverse().filter(function (element) {
return element !== window && element !== document;
}).map(function (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;
}
return selector;
}).join(' ');
}
// pathsOrTarget [input#errorBtn, div.content, div#container, body, html, document, Window]
export default function (pathsOrTarget){
if (Array.isArray(pathsOrTarget)) { // 有可能是数组
return getSelector(pathsOrTarget);
} else { // 也有可能是对象
var paths = [];
var element = pathsOrTarget;
while (element) {
paths.push(element);
element = element.parentNode;
}
return getSelector(paths);
}
}

2.2.3.2 上报

新建 src/util/tracker.js

PutWebTracking

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
const project = 'front-end-monitor-test';
const endpoint = 'cn-hangzhou.log.aliyuncs.com';
const logstoreName = 'font-end-monitor-store';
var userAgent = require('user-agent');
function getExtraData() {
return {
title: document.title,
url: location.href,
timestamp: Date.now(),
userAgent: userAgent.parse(navigator.userAgent).name
// 用户ID 等等等 获取额外数据
};
}
class SendTracker {
constructor() {
this.url = `http://${project}.${endpoint}/logstores/${logstoreName}/track`; // 上报的路径 文档里有参数说明
this.xhr = new XMLHttpRequest();
}
send(data = {}, callback) {
let extraData = getExtraData();
let logs = { ...extraData, ...data };
// 阿里云传值的要求 对象的值不能是数字
for (let key in logs) {
if (typeof logs[key] === 'number') {
logs[key] = "" + logs[key];
}
}
console.log("log:",logs);
let body = JSON.stringify({
__logs__: [logs]
});
console.log("body:",body);
this.xhr.open("POST", this.url, true);
this.xhr.setRequestHeader("Content-Type", "application/json;charset=UTF-8"); // 请求体类型
this.xhr.setRequestHeader('x-log-apiversion', '0.6.0'); // 版本号
this.xhr.setRequestHeader('x-log-bodyrawsize', body.length); // 请求体大小
this.xhr.onload = function () {
if ((this.status >= 200 && this.status <= 300) || this.status == 304) {
callback && callback();
}
}
this.xhr.onerror = function (error) {
console.log('error', error);
}
this.xhr.send(body);
}
}
export default new SendTracker();

2.3 接口异常采集脚本

2.3.1 数据结构设计

成功的请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"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": "" //参数
}

失败的请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"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=jiafei"
}

2.3.2 实现

src/index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<body>
<div id="container">
<div class="content">
<input type="button" value="发起ajax成功请求" onclick="sendAjaxSuccess()" />
<input type="button" value="发起ajax失败请求" onclick="sendAjaxError()" />
</div>
</div>

<script>
function sendAjaxSuccess() {
let xhr = new XMLHttpRequest;
xhr.open('GET', '/success', true);
xhr.responseType = 'json';
xhr.onload = function () {
console.log(xhr.response);
}
xhr.send();
}
function sendAjaxError() {
let xhr = new XMLHttpRequest;
xhr.open('POST', '/error', true);
xhr.responseType = 'json';
xhr.onload = function () {
console.log(xhr.response);
}
xhr.onerror = function (error) {
console.log(error);
}
xhr.send("name=test");
}
</script>
</body>

webpack.config.js 添加before方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const path = require('path');
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'),
// before是用来配置路由的 内部是起了express服务 webpack-dev-server内置express,所以不用再安装express
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({
template: './src/index.html',
inject: 'head'
})
]
}

新建 src/monitor/lib/xhr.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import tracker from '../util/tracker';
export function injectXHR() {
let XMLHttpRequest = window.XMLHttpRequest;
let oldOpen = XMLHttpRequest.prototype.open;
// 重写open方法 增强
XMLHttpRequest.prototype.open = function (method, url, async) {
if (!url.match(/logstores/) && !url.match(/sockjs/)) { // 把tracker上报的请求排除
this.logData = {method, url, async}; // 这个this指XMLHttpRequest的实例,给这个实例上加一个logData的属性
}
return oldOpen.apply(this, arguments);
}

let oldSend = XMLHttpRequest.prototype.send;
// 重写send方法 增强
XMLHttpRequest.prototype.send = function (body) { // body是请求体
if (this.logData) {
let start = Date.now(); // 在发送前记录下开始的时间
// XMLHttpRequest readyState 0 1 2 3 4 当状态变为4的时候就是load了
// 之后 status 2xx 304 就是成功 其他就是失败
let handler = (type) => (event) => { // 这里的type 就是 load、error、abort
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); // 终止请求 abort
}
oldSend.apply(this, arguments);
};
}

/src/monitor/index.js

1
2
3
4
import {injectJsError} from './lib/jsError';
import {injectXHR} from './lib/xhr';
injectJsError();
injectXHR();

2.4 白屏

2.4.1 数据设计

1
2
3
4
5
6
7
8
9
10
11
12
{
"title": "前端监控系统",
"url": "http://localhost:8080/",
"timestamp": "1590822618759",
"userAgent": "chrome",
"kind": "stability", //大类
"type": "blank", //小类
"emptyPoints": "0", //空白点
"screen": "2049x1152", //分辨率
"viewPoint": "2048x994", //视口
"selector": "HTML BODY #container" //选择器
}

2.4.2 实现

  • elementsFromPoint方法可以获取到当前视口内指定坐标处,由里到外排列的所有元素

src/index.html

1
2
3
4
5
6
7
<body>
<div id="container">
<div class="content">

</div>
</div>
</body>

新建 src/monitor/util/onload.js

1
2
3
4
5
6
7
export default function (callback) {
if (document.readyState === 'complete') {
callback();
} else {
window.addEventListener('load', callback);
}
};

新建 src/monitor/lib/blankScreen.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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() {
// 不需要穷举所有的,像react 的话就只有id=root
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;
for (let i = 1; i <= 9; i++) {
// 根据具体页面布局风格定位点的坐标
xElements = document.elementsFromPoint(window.innerWidth * i / 10, window.innerHeight / 2); // 纵坐标都是高度的一半,横坐标宽度分成10份
yElements = document.elementsFromPoint(window.innerWidth / 2, window.innerHeight * i / 10); // 横坐标是宽度的一半,纵坐标高度分成10份
isWrapper(xElements[0]); // xElements 返回包含多个元素的数组,这里取第0个
isWrapper(yElements[0]);
}
if (emptyPoints >0) { // 一共有18个点 假设大于16个点就是白屏 这里写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 去除工具条与滚动条的窗口高度

src/monitor/index.js

1
2
3
4
5
6
import {injectJsError} from './lib/jsError';
import {injectXHR} from './lib/xhr';
import {blankScreen} from './lib/blankScreen';
injectJsError();
injectXHR();
blankScreen();

2.4.2.1 举例

src/index.html

1
2
3
4
5
6
7
8
9
10
11
<body>
<div id="container">
<div class="content" style="width:200px;word-wrap:break-word;">

</div>
</div>
<script>
let content = document.getElementsByClassName('content')[0];
content.innerHTML = '<span>@</span>'.repeat(1000);
</script>
</body>

可以依次拉伸显示不同宽度尺寸的屏幕,页面上定位到的emptyPoints个数随之变化,来设置最大白屏点数去发送白屏tracker

2.5 加载时间

2.5.1 阶段含义

2.5.2 阶段计算

【注意】计算阶段下面第二张图注意:执行javascript 依赖css解析完毕,构建DOM 依赖JS解析完毕。
css没有解析完毕会阻塞js,这里涉及async、defer

2.5.3 数据结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"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"
}

2.5.4 实现

新建 /src/monitor/lib/timing.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import tracker from '../util/tracker';
import onload from '../util/onload';
export function timing() {
onload(function () {
// 一般延迟3秒后再去拿时间,这样更加准确
setTimeout(() => {
const {
fetchStart,
connectStart,
connectEnd,
requestStart,
responseStart,
responseEnd,
domLoading,
domInteractive,
domContentLoadedEventStart,
domContentLoadedEventEnd,
loadEventStart } = performance.timing;
tracker.send({
kind: 'experience', // 用户体验指标
type: 'timing', // 每个阶段的时间
connectTime: connectEnd - connectStart,//TCP连接耗时
ttfbTime: responseStart - requestStart,//ttfb 首字节到达时间
responseTime: responseEnd - responseStart,//Response响应耗时
parseDOMTime: loadEventStart - domLoading,//DOM解析渲染耗时
domContentLoadedTime: domContentLoadedEventEnd - domContentLoadedEventStart,//DOMContentLoaded事件回调耗时
timeToInteractive: domInteractive - fetchStart,//首次可交互时间
loadTime: loadEventStart - fetchStart//完整的加载时间
});

}, 3000);
});
}

src/monitor/index.js

1
2
3
4
5
6
7
8
import {injectJsError} from './lib/jsError';
import {injectXHR} from './lib/xhr';
import {blankScreen} from './lib/blankScreen';
import {timing} from './lib/timing';
injectJsError();
injectXHR();
blankScreen();
timing();

刷新页面,可以看到向阿里云服务器发送track请求, 可以看到事件回调耗时0毫秒

2.5.4.1 举例

src/index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<body>
<div id="container">
<div class="content" style="width:600px;word-wrap:break-word;">

</div>
</div>
<script>
// 当初始的 HTML 文档被完全加载和解析完成之后,DOMContentLoaded 事件被触发,而无需等待样式表、图像和子框架的完全加载。
// DOM解析完成,即使依赖的资源没有加载完成也会触发这个事件
document.addEventListener('DOMContentLoaded', function(){
let start = Date.now();
while((Date.now() - start) < 1000){}
})
</script>
</body>

刷新页面,可以看到发送track请求 domContentLoadedTime: 1001

2.6 性能指标

2.6.1 字段描述

【注意】字段下面的Performance图片中的Rendering、Painting,它们的区别是:渲染引擎会在内部将页面画好Rendering,画好之后一次性的放到浏览器中Painting,始终有两张图,这个叫双缓冲。多久更新一次:比如设备的屏幕刷新率为 60 次/秒,1000ms/60,大概16.6ms绘制一帧(交换一次图片)。

2.6.2 数据结构设计

1.paint

1
2
3
4
5
6
7
8
9
10
11
12
{
"title": "前端监控系统",
"url": "http://localhost:8080/",
"timestamp": "1590828364186",
"userAgent": "chrome",
"kind": "experience",
"type": "paint",
"firstPaint": "102",
"firstContentPaint": "2130",
"firstMeaningfulPaint": "2130",
"largestContentfulPaint": "2130"
}

2.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”
}

2.6.3 实现

src/index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<body>
<div id="container">
<div class="content" style="width:600px;word-wrap:break-word;">
</div>
</div>
<script>
let content = document.getElementsByClassName('content')[0];
setTimeout(() => {
let h1 = document.createElement('h1');
h1.innerHTML = '我是最有重要的内容';
// 加这个elementtiming属性,来表示这个是有意义的内容 FCP
h1.setAttribute('elementtiming', 'meaningful');
content.appendChild(h1);
}, 2000);
</script>
</body>

/src/monitor/lib/timing.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
import tracker from '../util/tracker';
import onload from '../util/onload';
import getSelector from '../util/getSelector';
import getLastEvent from '../util/getLastEvent';
export function timing() {
onload(function () {
let FMP, LCP;
// 增加一个性能条目的观察者
// 捕获页面中出现有意义的元素
new PerformanceObserver((entryList, observer) => {
let perfEntries = entryList.getEntries();
console.log("perfEntries:",perfEntries);
FMP = perfEntries[0];
observer.disconnect(); // 不在观察了
}).observe({ entryTypes: ['element'] }); // 观察页面中有意义的元素

// 捕获页面中的large content paint 代表在viewport中最大的页面元素加载的时间
new PerformanceObserver((entryList, observer) => {
const perfEntries = entryList.getEntries();
const lastEntry = perfEntries[perfEntries.length - 1];
LCP = lastEntry;
observer.disconnect();
}).observe({ entryTypes: ['largest-contentful-paint'] });

// 用户首次和页面交互(单击链接,点击按钮等)到页面响应交互的时间 直接在页面上点击就能触发
new PerformanceObserver(function (entryList, observer) {
let lastEvent = getLastEvent();
const firstInput = entryList.getEntries()[0];
console.log("FID:", firstInput);
if (firstInput) {
// processingStart 开始处理的时间 startTime 开始点击的时间
let inputDelay = firstInput.processingStart - firstInput.startTime;//处理延迟
let duration = firstInput.duration;//处理耗时
if (firstInput > 0 || duration > 0) {
tracker.send({
kind: 'experience', // 用户体验指标
type: 'firstInputDelay', // 首次输入延迟
inputDelay: inputDelay, // 延时的时间
duration: duration, // 处理的时间
startTime: firstInput.startTime,
selector: lastEvent ? getSelector(lastEvent.path || lastEvent.target) : ''
});
}
}
observer.disconnect();
}).observe({ type: 'first-input', buffered: true }); // 用户的第一次交互,可以是点击页面,也可以是第一次输入

setTimeout(() => {
const {
fetchStart,
connectStart,
connectEnd,
requestStart,
responseStart,
responseEnd,
domLoading,
domInteractive,
domContentLoadedEventStart,
domContentLoadedEventEnd,
loadEventStart } = performance.timing;
tracker.send({
kind: 'experience', // 用户体验指标
type: 'timing', // 每个阶段的时间
connectTime: connectEnd - connectStart,//TCP连接耗时
ttfbTime: responseStart - requestStart,//ttfb 首字节到达时间
responseTime: responseEnd - responseStart,//Response响应耗时
parseDOMTime: loadEventStart - domLoading,//DOM解析渲染耗时
domContentLoadedTime: domContentLoadedEventEnd - domContentLoadedEventStart,//DOMContentLoaded事件回调耗时
timeToInteractive: domInteractive - fetchStart,//首次可交互时间
loadTime: loadEventStart - fetchStart//完整的加载时间
});

// 开始发送性能指标
const FP = performance.getEntriesByName('first-paint')[0];
const FCP = performance.getEntriesByName('first-contentful-paint')[0];
console.log('FP', FP);
console.log('FCP', FCP);
console.log('FMP', FMP);
console.log('LCP', LCP);
tracker.send({
kind: 'experience',
type: 'paint',
firstPaint: FP.startTime,
firstContentPaint: FCP.startTime,
firstMeaningfulPaint: FMP.startTime,
largestContentfulPaint: LCP.startTime
});

}, 3000);
});
}

相关文章

http://www.jiafeipeixun.cn/2020/html/101.1.monitor.html