错误名称 | 备注 |
---|---|
JS错误 | JS执行错误或者promise异常 |
资源异常 | script、link等资源加载异常 |
接口错误 | ajax或fetch请求接口异常 |
白屏 | 页面空白 |
错误名称 | 备注 |
---|---|
加载时间 | 各个阶段的加载时间 |
TTFB(time to first byte)(首字节时间) | 是指浏览器发起第一个请求到数据返回第一个字节所消耗的时间,这个时间包含了网络请求时间、后端处理时间 |
FP(First Paint)(首次绘制) | 首次绘制包括了任何用户自定义的背景绘制,它是将第一个像素点绘制到屏幕的时刻 |
FCP(First Content Paint)(首次内容绘制) | 首次内容绘制是浏览器将第一个DOM渲染到屏幕的时间,可以是任何文本、图像、SVG等的时间 |
FMP(First Meaningful paint)(首次有意义绘制) | 首次有意义绘制是页面可用性的量度标准 |
FID(First Input Delay)(首次输入延迟) | 用户首次和页面交互到页面响应交互的时间 |
卡顿 | 超过50ms的长任务 |
错误名称 | 备注 |
---|---|
PV | page view 即页面浏览量或点击量 |
UV | 指访问某个站点的不同IP地址的人数 |
页面的停留时间 | 用户在每一个页面的停留时间 |
{
"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"//选择器
}
{
"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"//选择器
}
{
"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"//选择器
}
* | SELECT kind,count(*) as number GROUP BY kind
webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development',
context: process.cwd(),
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'monitor.js'
},
devServer: {
contentBase: path.resolve(__dirname, 'dist')
},
module: {},
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html',
inject: 'head'
})
]
}
src\index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>monitor</title>
</head>
<body>
<div id="container">
<div class="content">
<input type="button" value="点击抛出错误" onclick="btnClick()" />
<input type="button" value="点击抛出promise错误" onclick="btnPromiseClick()" />
</div>
</div>
<script>
function btnClick() {
window.someVariable.error = 'someVariable';
}
function btnPromiseClick() {
new Promise(function (resolve, reject) {
console.log(someVar.some);
});
}
</script>
<script src="error.js"></script>
</body>
</html>
src\index.js
import './monitor'
src\monitor\index.js
import { injectJsError } from './lib/jsError';
injectJsError();
src\monitor\lib\jsError.js
import tracker from '../util/tracker';
import getLastEvent from '../util/getLastEvent';
import getSelector from '../util/getSelector';
import formatTime from '../util/formatTime';
export function injectJsError() {
//一般JS运行时错误使用window.onerror捕获处理
window.addEventListener('error', function (event) {
let lastEvent = getLastEvent();
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都可以
//当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都可以
}
function getLines(stack) {
if (!stack) {
return '';
}
return stack.split('\n').slice(1).map(item => item.replace(/^\s+at\s+/g, '')).join('^');
}
src\monitor\util\formatTime.js
export default (time) => {
return `${time}`.split(".")[0]
}
src\monitor\util\getLastEvent.js
let 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
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(' ');
}
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);
}
}
src\monitor\util\tracker.js
let host = 'cn-beijing.log.aliyuncs.com';
let project = 'zhufengmonitor';
let logstore = 'zhufengmonitor-store';
var 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 = {}, callback) {
let extraData = getExtraData();
let logs = { ...extraData, ...data };
for (let key in logs) {
if (typeof logs[key] === 'number') {
logs[key] = "" + logs[key];
}
}
console.log(logs);
console.log(JSON.stringify(logs, null, 2));
let body = JSON.stringify({
__logs__: [logs]
});
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();
{
"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"
}
src\index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>monitor</title>
</head>
<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=zhufeng");
}
</script>
</body>
</html>
src\monitor\index.js
import { injectJsError } from './lib/jsError';
+import { injectXHR } from './lib/xhr';
injectJsError();
+injectXHR();
webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development',
context: process.cwd(),
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'monitor.js'
},
devServer: {
contentBase: path.resolve(__dirname, 'dist'),
+ before(router) {
+ router.get('/success', function (req, res) {
+ res.json({ id: 1 });
+ });
+ router.post('/error', function (req, res) {
+ res.sendStatus(500);
+ });
+ }
},
module: {},
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html',
inject: 'head'
})
],
}
src\monitor\lib\xhr.js
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);
};
}
{
"title": "前端监控系统",
"url": "http://localhost:8080/",
"timestamp": "1590822618759",
"userAgent": "chrome",
"kind": "stability", //大类
"type": "blank", //小类
"emptyPoints": "0", //空白点
"screen": "2049x1152", //分辨率
"viewPoint": "2048x994", //视口
"selector": "HTML BODY #container" //选择器
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>monitor</title>
</head>
<body>
<div id="container">
<div class="content" style="width:600px;word-wrap:break-word;">
</div>
</div>
<script>
+ let content = document.getElementsByClassName('content')[0];
+ content.innerHTML = '@'.repeat(10000);
</script>
</body>
</html>
src\monitor\index.js
import { injectJsError } from './lib/jsError';
import { injectXHR } from './lib/xhr';
+import { blankScreen } from './lib/blankScreen';
injectJsError();
injectXHR();
+blankScreen();
src\monitor\util\onload.js
export default function (callback) {
if (document.readyState === 'complete') {
callback();
} else {
window.addEventListener('load', callback);
}
};
src\monitor\lib\blankScreen.js
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 去除工具条与滚动条的窗口高度
字段 | 含义 |
---|---|
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"
}
src\index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>monitor</title>
</head>
<body>
<div id="container">
<div class="content" style="width:600px;word-wrap:break-word;">
</div>
</div>
<script>
let content = document.getElementsByClassName('content')[0];
//content.innerHTML = '@'.repeat(10000);
document.addEventListener('DOMContentLoaded', function () {
+ let start = Date.now();
+ while ((Date.now() - start) < 1000) {}
+ });
</script>
</body>
</html>
src\monitor\index.js
import { injectJsError } from './lib/jsError';
import { injectXHR } from './lib/xhr';
import { blankScreen } from './lib/blankScreen';
+import { timing } from './lib/timing';
injectJsError();
injectXHR();
blankScreen();
+timing();
src\monitor\lib\timing.js
import onload from '../util/onload';
import tracker from '../util/tracker';
import formatTime from '../util/formatTime';
import getLastEvent from '../util/getLastEvent';
import getSelector from '../util/getSelector';
export function timing() {
onload(function () {
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);
});
}
字段 | 描述 | 备注 | 计算方式 |
---|---|---|---|
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(首次输入延迟) | 用户首次和页面交互(单击链接,点击按钮等)到页面响应交互的时间 |
{
"title": "前端监控系统",
"url": "http://localhost:8080/",
"timestamp": "1590828364186",
"userAgent": "chrome",
"kind": "experience",
"type": "paint",
"firstPaint": "102",
"firstContentPaint": "2130",
"firstMeaningfulPaint": "2130",
"largestContentfulPaint": "2130"
}
{
"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"
}
src\index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>monitor</title>
</head>
<body style="background-color: green;">
<div id="container">
<div class="content" style="width:600px;height:600px;word-wrap:break-word;">
<input />
</div>
</div>
<script>
let content = document.getElementsByClassName('content')[0];
//content.innerHTML = '@'.repeat(10000);
+ setTimeout(() => {
+ let h1 = document.createElement('h1');
+ h1.innerHTML = '我是最有重要的内容';
+ h1.setAttribute('elementtiming', 'meaningful');
+ content.appendChild(h1);
+ }, 2000);
</script>
</body>
</html>
src\monitor\lib\timing.js
import onload from '../util/onload';
import tracker from '../util/tracker';
import formatTime from '../util/formatTime';
import getLastEvent from '../util/getLastEvent';
import getSelector from '../util/getSelector';
export function timing() {
+ let FMP, LCP;
+ new PerformanceObserver((entryList, observer) => {
+ let perfEntries = entryList.getEntries();
+ FMP = perfEntries[0];
+ observer.disconnect();
+ }).observe({ entryTypes: ['element'] });
+ 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];
+ if (firstInput) {
+ let inputDelay = firstInput.processingStart - firstInput.startTime;//处理延迟
+ let duration = firstInput.duration;//处理耗时
+ if (firstInput > 0 || duration > 0) {
+ tracker.send({
+ kind: 'experience',
+ type: 'firstInputDelay',
+ inputDelay: inputDelay ? formatTime(inputDelay) : 0,
+ duration: duration ? formatTime(duration) : 0,
+ startTime: firstInput.startTime,
+ selector: lastEvent ? getSelector(lastEvent.path || lastEvent.target) : ''
+ });
+ }
+ }
+ observer.disconnect();
+ }).observe({ type: 'first-input', buffered: true });
onload(function () {
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 ? formatTime(FP.startTime) : 0,
+ firstContentPaint: FCP ? formatTime(FCP.startTime) : 0,
+ firstMeaningfulPaint: FMP ? formatTime(FMP.startTime) : 0,
+ largestContentfulPaint: LCP ? formatTime(LCP.renderTime || LCP.loadTime) : 0
+ });
}, 3000);
});
}
{
"title": "前端监控系统",
"url": "http://localhost:8080/",
"timestamp": "1590828656781",
"userAgent": "chrome",
"kind": "experience",
"type": "longTask",
"eventType": "mouseover",
"startTime": "9331",
"duration": "200",
"selector": "HTML BODY #container .content"
}
src\index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>monitor</title>
</head>
<body style="background-color: green;">
<div id="container">
<div class="content" style="width:600px;height:600px;word-wrap:break-word;">
+ <button id="longTaskBtn">执行longTask</button>
</div>
</div>
<script>
let content = document.getElementsByClassName('content')[0];
+ let longTaskBtn = document.getElementById('longTaskBtn');
+ longTaskBtn.addEventListener('click', longTask);
+ function longTask() {
+ let start = Date.now();
+ console.log('longTask开始 start', start);
+ while (Date.now() < (200 + start)) { }
+ console.log('longTask结束 end', (Date.now() - start));
+ }
</script>
</body>
</html>
src\monitor\index.js
import { injectJsError } from './lib/jsError';
import { injectXHR } from './lib/xhr';
import { blankScreen } from './lib/blankScreen';
import { timing } from './lib/timing';
+import { longTask } from './lib/longTask';
injectJsError();
injectXHR();
blankScreen();
timing();
+longTask();
src\monitor\lib\longTask.js
import tracker from '../util/tracker';
import formatTime from '../util/formatTime';
import getLastEvent from '../util/getLastEvent';
import getSelector from '../util/getSelector';
export function longTask() {
new PerformanceObserver((list) => {
list.getEntries().forEach(entry => {
if (entry.duration > 100) {
let lastEvent = getLastEvent();
requestIdleCallback(() => {
tracker.send({
kind: 'experience',
type: 'longTask',
eventType: lastEvent.type,
startTime: formatTime(entry.startTime),// 开始时间
duration: formatTime(entry.duration),// 持续时间
selector: lastEvent ? getSelector(lastEvent.path || lastEvent.target) : ''
});
});
}
});
}).observe({ entryTypes: ["longtask"] });
}
{
"title": "前端监控系统",
"url": "http://localhost:8080/",
"timestamp": "1590829304423",
"userAgent": "chrome",
"kind": "business",
"type": "pv",
"effectiveType": "4g",
"rtt": "50",
"screen": "2049x1152"
}
src\index.html
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>前端监控SDK</title>
</head>
src\monitor\index.js
import { injectJsError } from './lib/jsError';
import { injectXHR } from './lib/xhr';
import { blankScreen } from './lib/blankScreen';
import { timing } from './lib/timing';
import { longTask } from './lib/longTask';
+import { pv } from './lib/pv';
injectJsError();
injectXHR();
blankScreen();
timing();
longTask();
+pv();
src\monitor\lib\pv.js
import tracker from '../util/tracker';
export function pv() {
var connection = navigator.connection;
tracker.send({
kind: 'business',
type: 'pv',
effectiveType: connection.effectiveType, //网络环境
rtt: connection.rtt,//往返时间
screen: `${window.screen.width}x${window.screen.height}`//设备分辨率
});
let startTime = Date.now();
window.addEventListener('unload', () => {
let stayTime = Date.now() - startTime;
tracker.send({
kind: 'business',
type: 'stayTime',
stayTime
});
}, false);
}
* | SELECT type, COUNT(*) as number GROUP BY type LIMIT 10
* | SELECT userAgent, COUNT(*) as number GROUP BY userAgent LIMIT 10
* | SELECT screen, COUNT(*) as number GROUP BY screen LIMIT 10
DOMContentLoaded
事件调用前执行,不会阻塞页面渲染DOMContentLoaded
事件调用前执行