本文代码:本文代码
- 为什么要做前端监控
- 前端监控目标
2.1 稳定性(stability)
2.2 用户体验(experience)
2.3 业务(business)
- 前端监控流程
3.1 常见的埋点方案
3.1.1 代码埋点
3.1.2 可视化埋点
3.1.3 无痕埋点
- 编写监控收罗脚本
4.1 开通日记服务
4.2 监控错误
4.2.1 错误分类
4.2.2 数据结构计划
1. jsError
.2. promiseError
3. resourceError
4.2.3 报表
4.2.4 实现
1. webpack.config.js
2. index.html
3. src\index.js
4. monitor\index.js
5. jsError.js
6. formatTime.js
7. getLastEvent.js
8. getSelector.js
9. tracker.js
4.3.接口异常收罗脚本
4.3.1 数据计划
4.3.2 实现
1. src\index.html
2. monitor\index.js
3. webpack.config.js
4. xhr.js
4.4 白屏
4.4.1 数据计划
4.4.2 实现
1. src\index.html
2. monitor\index.js
3. onload.js
4. blankScreen.js
4.5 加载时间
4.5.1 阶段含义
4.5.2 阶段盘算
4.5.3 数据结构
4.5.4 实现
1. src\index.html
2. monitor\index.js
3. timing.js
4.6 性能指标
4.6.1 数据结构计划
1. paint
2. firstInputDelay
4.6.2 实现
1. src\index.html
2. timing.js
4.7 卡顿
4.7.1 数据计划
4.7.2 实现
1. src\index.html
2. monitor\index.js
3. longTask.js
4.8 pv
4.8.1 数据结构
4.8.2 实现
1. src\index.html
2. src\monitor\index.js
3. pv.js
- 查询报表
5.1 监控项分布
5.2 浏览器分布
5.3 页面分辨率分布
- 参考
6.1 第三方
6.1.1 商业产品
6.1.2 开源产品
6.2 defer 和 async
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)
错误名称备注PVpage 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 数据结构计划
- {
- "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"//选择器
- }
复制代码 4.2.3 报表
- * | SELECT kind,count(*) as number GROUP BY kind
复制代码 4.2.4 实现
- 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'
- })
- ]
- }
复制代码
- index.html
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
src\index.js
- monitor\index.js
src\monitor\index.js
- import { injectJsError } from './lib/jsError';
- injectJsError();
复制代码
- jsError.js
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('^');
- }
复制代码
- formatTime.js
src\monitor\util\formatTime.js
- export default (time) => {
- return `${time}`.split(".")[0]
- }
复制代码
- getLastEvent.js
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;
- };
复制代码
- getSelector.js
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);
- }
- }
复制代码
- tracker.js
src\monitor\util\tracker.js
PutWebtracking
- 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();
复制代码 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 实现
- <!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>
复制代码- import { injectJsError } from './lib/jsError';
- +import { injectXHR } from './lib/xhr';
- injectJsError();
- +injectXHR();
复制代码- 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'
- })
- ],
- }
复制代码- 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 实现
- screen 返回当前window的screen对象,返回当前渲染窗口中和屏幕有关的属性
- innerWidth 只读的 Window 属性 innerWidth 返回以像素为单元的窗口的内部宽度
- innerHeight 窗口的内部高度(结构视口)的高度
- layout_viewport
- elementsFromPoint方法可以获取到当前视口内指定坐标处,由里到外分列的所有元素
- <!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();
复制代码
- onload.js
src\monitor\util\onload.js
- export default function (callback) {
- if (document.readyState === 'complete') {
- callback();
- } else {
- window.addEventListener('load', callback);
- }
- };
复制代码
- blankScreen.js
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 去除工具条与滚动条的窗口高度
复制代码 4.5 加载时间
- PerformanceTiming
- DOMContentLoaded
- FMP
4.5.1 阶段含义
字段含义navigationStart初始化页面,在同一个浏览器上下文中前一个页面unload的时间戳,假如没有前一个页面的unload,则与fetchStart值相等redirectStart第一个HTTP重定向发生的时间,有跳转且是同域的重定向,否则为0redirectEnd最后一个重定向完成时的时间,否则为0fetchStart浏览器准备好使用http请求获取文档的时间,这发生在检查缓存之前domainLookupStartDNS域名开始查询的时间,假如有本地的缓存或keep-alive则时间为0domainLookupEndDNS域名结束查询的时间connectStartTCP开始创建连接的时间,假如是长期连接,则与fetchStart值相等secureConnectionStarthttps 连接开始的时间,假如不是安全连接则为0connectEndTCP完成握手的时间,假如是长期连接则与fetchStart值相等requestStartHTTP请求读取真实文档开始的时间,包括从本地缓存读取requestEndHTTP请求读取真实文档结束的时间,包括从本地缓存读取responseStart返回浏览器从服务器收到(或从本地缓存读取)第一个字节时的Unix毫秒时间戳responseEnd返回浏览器从服务器收到(或从本地缓存读取,或从本地资源读取)最后一个字节时的Unix毫秒时间戳unloadEventStart前一个页面的unload的时间戳 假如没有则为0unloadEventEnd与unloadEventStart相对应,返回的是unload函数执行完成的时间戳domLoading返回当前网页DOM结构开始解析时的时间戳,此时document.readyState酿成loading,并将抛出readyStateChange事件domInteractive返回当前网页DOM结构结束解析、开始加载内嵌资源时时间戳,document.readyState 酿成interactive,并将抛出readyStateChange事件(注意只是DOM树解析完成,这时候并没有开始加载网页内的资源)domContentLoadedEventStart网页domContentLoaded事件发生的时间domContentLoadedEventEnd网页domContentLoaded事件脚本执行完毕的时间,domReady的时间domCompleteDOM树解析完成,且资源也准备停当的时间,document.readyState酿成complete.并将抛出readystatechange事件loadEventStartload 事件发送给文档,也即load回调函数开始执行的时间loadEventEndload回调函数执行完成的时间 4.5.2 阶段盘算
字段描述盘算方式意义unload前一个页面卸载耗时unloadEventEnd – unloadEventStart-redirect重定向耗时redirectEnd – redirectStart重定向的时间appCache缓存耗时domainLookupStart – fetchStart读取缓存的时间dnsDNS 解析耗时domainLookupEnd – domainLookupStart可观察域名解析服务是否正常tcpTCP 连接耗时connectEnd – connectStart创建连接的耗时sslSSL 安全连接耗时connectEnd – secureConnectionStart反映数据安全连接创建耗时ttfbTime to First Byte(TTFB)网络请求耗时responseStart – requestStartTTFB是发出页面请求到吸取到应答数据第一个字节所花费的毫秒数response相应数据传输耗时responseEnd – responseStart观察网络是否正常domDOM解析耗时domInteractive – responseEnd观察DOM结构是否公道,是否有JS壅闭页面解析dclDOMContentLoaded 事件耗时domContentLoadedEventEnd – domContentLoadedEventStart当 HTML 文档被完全加载息争析完成之后,DOMContentLoaded 事件被触发,无需等候样式表、图像和子框架的完成加载resources资源加载耗时domComplete – domContentLoadedEventEnd可观察文档流是否过大domReadyDOM阶段渲染耗时domContentLoadedEventEnd – fetchStartDOM树和页面资源加载完成时间,会触发domContentLoaded事件初次渲染耗时初次渲染耗时responseEnd-fetchStart加载文档到看到第一帧非空图像的时间,也叫白屏时间初次可交互时间初次可交互时间domInteractive-fetchStartDOM树解析完成时间,此时document.readyState为interactive首包时间耗时首包时间responseStart-domainLookupStartDNS解析到相应返回给浏览器第一个字节的时间页面完全加载时间页面完全加载时间loadEventStart - fetchStart-onLoadonLoad事件耗时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 实现
- src\index.html
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>
复制代码
- monitor\index.js
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();
复制代码
- timing.js
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);
- });
- }
复制代码 4.6 性能指标
- PerformanceObserver.observe方法用于观察传入的参数中指定的性能条目类型的集合。当记载一个指定类型的性能条目时,性能监测对象的回调函数将会被调用
- entryType
- paint-timing
- event-timing
- LCP
- FMP
- time-to-interactive
字段描述备注FPFirst Paint(初次绘制)包括了任何用户自定义的配景绘制,它是首先将像素绘制到屏幕的时刻FCPFirst Content Paint(初次内容绘制)是浏览器将第一个 DOM 渲染到屏幕的时间,大概是文本、图像、SVG等,这其实就是白屏时间FMPFirst Meaningful Paint(初次故意义绘制)页面故意义的内容渲染的时间LCP(Largest Contentful Paint)(最大内容渲染)代表在viewport中最大的页面元素加载的时间DCL(DomContentLoaded)(DOM加载完成)当 HTML 文档被完全加载息争析完成之后,DOMContentLoaded 事件被触发,无需等候样式表、图像和子框架的完成加载L(onLoad)当依赖的资源全部加载完毕之后才会触发TTI(Time to Interactive) 可交互时间用于标记应用已进行视觉渲染并能可靠相应用户输入的时间点FIDFirst 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 实现
- src\index.html
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);
- });
- }
复制代码 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 实现
- src\index.html
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>
复制代码
- monitor\index.js
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();
复制代码
- longTask.js
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"] });
- }
复制代码 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 实现
- src\index.html
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
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();
复制代码
- pv.js
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);
- }
复制代码 5.查询报表
- 图表说明
- 查询日记数据
- 聚类分析
- 数据可视化
- 趋势 柱状图、拆线图、曲线图
- 比例 饼图、环状图、面积图
- 仪表盘
5.1 监控项分布
- * | SELECT type, COUNT(*) as number GROUP BY type LIMIT 10
复制代码 5.2 浏览器分布
- * | SELECT userAgent, COUNT(*) as number GROUP BY userAgent LIMIT 10
复制代码 5.3 页面分辨率分布
- * | SELECT screen, COUNT(*) as number GROUP BY screen LIMIT 10
复制代码 6.参考
6.1 第三方
6.1.1 商业产品
6.1.2 开源产品
- fundebug
- BetterJS
- Sentry
- @sentry/browser
6.2 defer 和 async
- defer异步下载JavaScript文件,会在HTML解析完成之后DOMContentLoaded事件调用前执行,不会壅闭页面渲染
- 假如script标签设置了该属性,则浏览器会异步的下载该文件并且不会影响到后续DOM的渲染
- 假如有多个设置了defer的script标签存在,则会按照次序执行所有的script
- defer脚本会在文档渲染完毕后,DOMContentLoaded事件调用前执行
- async异步下载JavaScript文件,下载完成之后会立即执行,有大概会壅闭页面渲染
- async的设置,会使得script脚本异步的加载并在答应的情况下执行
- async的执行,并不会按着script在页面中的次序来执行,而是谁先加载完谁执行
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |