IT评测·应用市场-qidao123.com

标题: 前端监控 [打印本页]

作者: 数据人与超自然意识    时间: 2025-3-9 16:34
标题: 前端监控
本文代码:本文代码
  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 开通日记服务

4.2 监控错误
4.2.1 错误分类

4.2.2 数据结构计划
  1. {
  2.   "title": "前端监控系统",//页面标题
  3.   "url": "http://localhost:8080/",//页面URL
  4.   "timestamp": "1590815288710",//访问时间戳
  5.   "userAgent": "Chrome",//用户浏览器类型
  6.   "kind": "stability",//大类
  7.   "type": "error",//小类
  8.   "errorType": "jsError",//错误类型
  9.   "message": "Uncaught TypeError: Cannot set property 'error' of undefined",//类型详情
  10.   "filename": "http://localhost:8080/",//访问的文件名
  11.   "position": "0:0",//行列信息
  12.   "stack": "btnClick (http://localhost:8080/:20:39)^HTMLInputElement.onclick (http://localhost:8080/:14:72)",//堆栈信息
  13.   "selector": "HTML BODY #container .content INPUT"//选择器
  14. }
复制代码
  1. {
  2.   "title": "前端监控系统",//页面标题
  3.   "url": "http://localhost:8080/",//页面URL
  4.   "timestamp": "1590815290600",//访问时间戳
  5.   "userAgent": "Chrome",//用户浏览器类型
  6.   "kind": "stability",//大类
  7.   "type": "error",//小类
  8.   "errorType": "promiseError",//错误类型
  9.   "message": "someVar is not defined",//类型详情
  10.   "filename": "http://localhost:8080/",//访问的文件名
  11.   "position": "24:29",//行列信息
  12.   "stack": "http://localhost:8080/:24:29^new Promise (<anonymous>)^btnPromiseClick (http://localhost:8080/:23:13)^HTMLInputElement.onclick (http://localhost:8080/:15:86)",//堆栈信息
  13.   "selector": "HTML BODY #container .content INPUT"//选择器
  14. }
复制代码
  1. {
  2.   "title": "前端监控系统",//页面标题
  3.   "url": "http://localhost:8080/",//页面URL
  4.   "timestamp": "1590816168643",//访问时间戳
  5.   "userAgent": "Chrome",//用户浏览器类型
  6.   "kind": "stability",//大类
  7.   "type": "error",//小类
  8.   "errorType": "resourceError",//错误类型
  9.   "filename": "http://localhost:8080/error.js",//访问的文件名
  10.   "tagName": "SCRIPT",//标签名
  11.   "timeStamp": "76",//时间
  12.   "selector": "HTML BODY SCRIPT"//选择器
  13. }
复制代码
4.2.3 报表

  1. * | SELECT kind,count(*) as number GROUP BY kind
复制代码
4.2.4 实现
  1. const path = require('path');
  2. const HtmlWebpackPlugin = require('html-webpack-plugin');
  3. module.exports = {
  4.     mode: 'development',
  5.     context: process.cwd(),
  6.     entry: './src/index.js',
  7.     output: {
  8.         path: path.resolve(__dirname, 'dist'),
  9.         filename: 'monitor.js'
  10.     },
  11.     devServer: {
  12.         contentBase: path.resolve(__dirname, 'dist')
  13.     },
  14.     module: {},
  15.     plugins: [
  16.         new HtmlWebpackPlugin({
  17.             template: './src/index.html',
  18.             inject: 'head'
  19.         })
  20.     ]
  21. }
复制代码
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4.     <meta charset="UTF-8">
  5.     <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6.     <title>monitor</title>
  7. </head>
  8. <body>
  9.     <div id="container">
  10.         <div class="content">
  11.             <input type="button" value="点击抛出错误" onclick="btnClick()" />
  12.             <input type="button" value="点击抛出promise错误" onclick="btnPromiseClick()" />
  13.         </div>
  14.     </div>
  15.     <script>
  16.         function btnClick() {
  17.             window.someVariable.error = 'someVariable';
  18.         }
  19.         function btnPromiseClick() {
  20.             new Promise(function (resolve, reject) {
  21.                 console.log(someVar.some);
  22.             });
  23.         }
  24.     </script>
  25.     <script src="error.js"></script>
  26. </body>
  27. </html>
复制代码
  1. import './monitor'
复制代码
  1. import { injectJsError } from './lib/jsError';
  2. injectJsError();
复制代码
  1. import tracker from '../util/tracker';
  2. import getLastEvent from '../util/getLastEvent';
  3. import getSelector from '../util/getSelector';
  4. import formatTime from '../util/formatTime';
  5. export function injectJsError() {
  6.     //一般JS运行时错误使用window.onerror捕获处理
  7.     window.addEventListener('error', function (event) {
  8.         let lastEvent = getLastEvent();
  9.         if (event.target && (event.target.src || event.target.href)) {
  10.             tracker.send({//资源加载错误
  11.                 kind: 'stability',//稳定性指标
  12.                 type: 'error',//resource
  13.                 errorType: 'resourceError',
  14.                 filename: event.target.src || event.target.href,//加载失败的资源
  15.                 tagName: event.target.tagName,//标签名
  16.                 timeStamp: formatTime(event.timeStamp),//时间
  17.                 selector: getSelector(event.path || event.target),//选择器
  18.             })
  19.         } else {
  20.             tracker.send({
  21.                 kind: 'stability',//稳定性指标
  22.                 type: 'error',//error
  23.                 errorType: 'jsError',//jsError
  24.                 message: event.message,//报错信息
  25.                 filename: event.filename,//报错链接
  26.                 position: (event.lineNo || 0) + ":" + (event.columnNo || 0),//行列号
  27.                 stack: getLines(event.error.stack),//错误堆栈
  28.                 selector: lastEvent ? getSelector(lastEvent.path || lastEvent.target) : ''//CSS选择器
  29.             })
  30.         }
  31.     }, true);// true代表在捕获阶段调用,false代表在冒泡阶段捕获,使用true或false都可以
  32.     //当Promise 被 reject 且没有 reject 处理器的时候,会触发 unhandledrejection 事件
  33.     window.addEventListener('unhandledrejection', function (event) {
  34.         let lastEvent = getLastEvent();
  35.         let message = '';
  36.         let line = 0;
  37.         let column = 0;
  38.         let file = '';
  39.         let stack = '';
  40.         if (typeof event.reason === 'string') {
  41.             message = event.reason;
  42.         } else if (typeof event.reason === 'object') {
  43.             message = event.reason.message;
  44.         }
  45.         let reason = event.reason;
  46.         if (typeof reason === 'object') {
  47.             if (reason.stack) {
  48.                 var matchResult = reason.stack.match(/at\s+(.+):(\d+):(\d+)/);
  49.                 if (matchResult) {
  50.                     file = matchResult[1];
  51.                     line = matchResult[2];
  52.                     column = matchResult[3];
  53.                 }
  54.                 stack = getLines(reason.stack);
  55.             }
  56.         }
  57.         tracker.send({//未捕获的promise错误
  58.             kind: 'stability',//稳定性指标
  59.             type: 'error',//jsError
  60.             errorType: 'promiseError',//unhandledrejection
  61.             message: message,//标签名
  62.             filename: file,
  63.             position: line + ':' + column,//行列
  64.             stack,
  65.             selector: lastEvent ? getSelector(lastEvent.path || lastEvent.target) : ''
  66.         })
  67.     }, true);// true代表在捕获阶段调用,false代表在冒泡阶段捕获,使用true或false都可以
  68. }
  69. function getLines(stack) {
  70.     if (!stack) {
  71.         return '';
  72.     }
  73.     return stack.split('\n').slice(1).map(item => item.replace(/^\s+at\s+/g, '')).join('^');
  74. }
复制代码
  1. export default (time) => {
  2.     return `${time}`.split(".")[0]
  3. }
复制代码
  1. let lastEvent;
  2. ['click','pointerdown', 'touchstart', 'mousedown', 'keydown', 'mouseover'].forEach(event => {
  3.     document.addEventListener(event, (event) => {
  4.         lastEvent = event;
  5.     }, {
  6.         capture: true,//capture 控制监听器是在捕获阶段执行还是在冒泡阶段执行
  7.         passive: true //passive 的意思是顺从的,表示它不会对事件的默认行为说 no
  8.     });
  9. });
  10. export default function () {
  11.     return lastEvent;
  12. };
复制代码
  1. const getSelector = function (path) {
  2.     return path.reverse().filter(function (element) {
  3.         return element !== window && element !== document;
  4.     }).map(function (element) {
  5.         var selector;
  6.         if (element.id) {
  7.             selector = `#${element.id}`;
  8.         } else if (element.className && typeof element.className === 'string') {
  9.             selector = '.' + element.className.split(' ').filter(function (item) { return !!item }).join('.');
  10.         } else {
  11.             selector = element.nodeName;
  12.         }
  13.         return selector;
  14.     }).join(' ');
  15. }
  16. export default function (pathsOrTarget) {
  17.     if (Array.isArray(pathsOrTarget)) {
  18.         return getSelector(pathsOrTarget);
  19.     } else {
  20.         var paths = [];
  21.         var element = pathsOrTarget;
  22.         while (element) {
  23.             paths.push(element);
  24.             element = element.parentNode;
  25.         }
  26.         return getSelector(paths);
  27.     }
  28. }
复制代码
  1. let host = 'cn-beijing.log.aliyuncs.com';
  2. let project = 'zhufengmonitor';
  3. let logstore = 'zhufengmonitor-store';
  4. var userAgent = require('user-agent')
  5. function getExtraData() {
  6.     return {
  7.         title: document.title,
  8.         url: location.href,
  9.         timestamp: Date.now(),
  10.         userAgent: userAgent.parse(navigator.userAgent).name
  11.     };
  12. }
  13. class SendTracker {
  14.     constructor() {
  15.         this.url = `http://${project}.${host}/logstores/${logstore}/track`;
  16.         this.xhr = new XMLHttpRequest();
  17.     }
  18.     send(data = {}, callback) {
  19.         let extraData = getExtraData();
  20.         let logs = { ...extraData, ...data };
  21.         for (let key in logs) {
  22.             if (typeof logs[key] === 'number') {
  23.                 logs[key] = "" + logs[key];
  24.             }
  25.         }
  26.         console.log(logs);
  27.         console.log(JSON.stringify(logs, null, 2));
  28.         let body = JSON.stringify({
  29.             __logs__: [logs]
  30.         });
  31.         this.xhr.open("POST", this.url, true);
  32.         this.xhr.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
  33.         this.xhr.setRequestHeader('x-log-apiversion', '0.6.0');
  34.         this.xhr.setRequestHeader('x-log-bodyrawsize', body.length);
  35.         this.xhr.onload = function () {
  36.             if ((this.status >= 200 && this.status <= 300) || this.status == 304) {
  37.                 callback && callback();
  38.             }
  39.         }
  40.         this.xhr.onerror = function (error) {
  41.             console.log('error', error);
  42.         }
  43.         this.xhr.send(body);
  44.     }
  45. }
  46. 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 实现
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4.     <meta charset="UTF-8">
  5.     <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6.     <title>monitor</title>
  7. </head>
  8. <body>
  9.     <div id="container">
  10.         <div class="content">
  11. +            <input type="button" value="发起ajax成功请求" onclick="sendAjaxSuccess()" />
  12. +            <input type="button" value="发起ajax失败请求" onclick="sendAjaxError()" />
  13.         </div>
  14.     </div>
  15.     <script>
  16. +        function sendAjaxSuccess() {
  17. +            let xhr = new XMLHttpRequest;
  18. +            xhr.open('GET', '/success', true);
  19. +            xhr.responseType = 'json';
  20. +            xhr.onload = function () {
  21. +                console.log(xhr.response);
  22. +            }
  23. +            xhr.send();
  24. +        }
  25. +        function sendAjaxError() {
  26. +            let xhr = new XMLHttpRequest;
  27. +            xhr.open('POST', '/error', true);
  28. +            xhr.responseType = 'json';
  29. +            xhr.onload = function () {
  30. +                console.log(xhr.response);
  31. +            }
  32. +            xhr.onerror = function (error) {
  33. +                console.log(error);
  34. +            }
  35. +            xhr.send("name=zhufeng");
  36.         }
  37.     </script>
  38. </body>
  39. </html>
复制代码
  1. import { injectJsError } from './lib/jsError';
  2. +import { injectXHR } from './lib/xhr';
  3. injectJsError();
  4. +injectXHR();
复制代码
  1. const path = require('path');
  2. const HtmlWebpackPlugin = require('html-webpack-plugin');
  3. module.exports = {
  4.     mode: 'development',
  5.     context: process.cwd(),
  6.     entry: './src/index.js',
  7.     output: {
  8.         path: path.resolve(__dirname, 'dist'),
  9.         filename: 'monitor.js'
  10.     },
  11.     devServer: {
  12.         contentBase: path.resolve(__dirname, 'dist'),
  13. +        before(router) {
  14. +            router.get('/success', function (req, res) {
  15. +                res.json({ id: 1 });
  16. +            });
  17. +            router.post('/error', function (req, res) {
  18. +                res.sendStatus(500);
  19. +            });
  20. +        }
  21.     },
  22.     module: {},
  23.     plugins: [
  24.         new HtmlWebpackPlugin({
  25.             template: './src/index.html',
  26.             inject: 'head'
  27.         })
  28.     ],
  29. }
复制代码
  1. import tracker from '../util/tracker';
  2. export function injectXHR() {
  3.     let XMLHttpRequest = window.XMLHttpRequest;
  4.     let oldOpen = XMLHttpRequest.prototype.open;
  5.     XMLHttpRequest.prototype.open = function (method, url, async, username, password) {
  6.         if (!url.match(/logstores/) && !url.match(/sockjs/)) {
  7.             this.logData = {
  8.                 method, url, async, username, password
  9.             }
  10.         }
  11.         return oldOpen.apply(this, arguments);
  12.     }
  13.     let oldSend = XMLHttpRequest.prototype.send;
  14.     let start;
  15.     XMLHttpRequest.prototype.send = function (body) {
  16.         if (this.logData) {
  17.             start = Date.now();
  18.             let handler = (type) => (event) => {
  19.                 let duration = Date.now() - start;
  20.                 let status = this.status;
  21.                 let statusText = this.statusText;
  22.                 tracker.send({//未捕获的promise错误
  23.                     kind: 'stability',//稳定性指标
  24.                     type: 'xhr',//xhr
  25.                     eventType: type,//load error abort
  26.                     pathname: this.logData.url,//接口的url地址
  27.                     status: status + "-" + statusText,
  28.                     duration: "" + duration,//接口耗时
  29.                     response: this.response ? JSON.stringify(this.response) : "",
  30.                     params: body || ''
  31.                 })
  32.             }
  33.             this.addEventListener('load', handler('load'), false);
  34.             this.addEventListener('error', handler('error'), false);
  35.             this.addEventListener('abort', handler('abort'), false);
  36.         }
  37.         oldSend.apply(this, arguments);
  38.     };
  39. }
复制代码
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 实现

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4.     <meta charset="UTF-8">
  5.     <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6.     <title>monitor</title>
  7. </head>
  8. <body>
  9.     <div id="container">
  10.         <div class="content" style="width:600px;word-wrap:break-word;">
  11.         </div>
  12.     </div>
  13.     <script>
  14. +        let content = document.getElementsByClassName('content')[0];
  15. +        content.innerHTML = '@'.repeat(10000);
  16.     </script>
  17. </body>
  18. </html>
复制代码
src\monitor\index.js
  1. import { injectJsError } from './lib/jsError';
  2. import { injectXHR } from './lib/xhr';
  3. +import { blankScreen } from './lib/blankScreen';
  4. injectJsError();
  5. injectXHR();
  6. +blankScreen();
复制代码
  1. export default function (callback) {
  2.     if (document.readyState === 'complete') {
  3.         callback();
  4.     } else {
  5.         window.addEventListener('load', callback);
  6.     }
  7. };
复制代码
  1. import tracker from '../util/tracker';
  2. import onload from '../util/onload';
  3. function getSelector(element) {
  4.     var selector;
  5.     if (element.id) {
  6.         selector = `#${element.id}`;
  7.     } else if (element.className && typeof element.className === 'string') {
  8.         selector = '.' + element.className.split(' ').filter(function (item) { return !!item }).join('.');
  9.     } else {
  10.         selector = element.nodeName.toLowerCase();
  11.     }
  12.     return selector;
  13. }
  14. export function blankScreen() {
  15.     const wrapperSelectors = ['body', 'html', '#container', '.content'];
  16.     let emptyPoints = 0;
  17.     function isWrapper(element) {
  18.         let selector = getSelector(element);
  19.         if (wrapperSelectors.indexOf(selector) >= 0) {
  20.             emptyPoints++;
  21.         }
  22.     }
  23.     onload(function () {
  24.         let xElements, yElements;
  25.         debugger
  26.         for (let i = 1; i <= 9; i++) {
  27.             xElements = document.elementsFromPoint(window.innerWidth * i / 10, window.innerHeight / 2)
  28.             yElements = document.elementsFromPoint(window.innerWidth / 2, window.innerHeight * i / 10)
  29.             isWrapper(xElements[0]);
  30.             isWrapper(yElements[0]);
  31.         }
  32.         if (emptyPoints >= 0) {
  33.             let centerElements = document.elementsFromPoint(window.innerWidth / 2, window.innerHeight / 2)
  34.             tracker.send({
  35.                 kind: 'stability',
  36.                 type: 'blank',
  37.                 emptyPoints: "" + emptyPoints,
  38.                 screen: window.screen.width + "x" + window.screen.height,
  39.                 viewPoint: window.innerWidth + 'x' + window.innerHeight,
  40.                 selector: getSelector(centerElements[0]),
  41.             })
  42.         }
  43.     });
  44. }
  45. //screen.width  屏幕的宽度   screen.height 屏幕的高度
  46. //window.innerWidth 去除工具条与滚动条的窗口宽度 window.innerHeight 去除工具条与滚动条的窗口高度
复制代码
4.5 加载时间

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 实现
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4.     <meta charset="UTF-8">
  5.     <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6.     <title>monitor</title>
  7. </head>
  8. <body>
  9.     <div id="container">
  10.         <div class="content" style="width:600px;word-wrap:break-word;">
  11.         </div>
  12.     </div>
  13.     <script>
  14.         let content = document.getElementsByClassName('content')[0];
  15.         //content.innerHTML = '@'.repeat(10000);
  16.         document.addEventListener('DOMContentLoaded', function () {
  17. +            let start = Date.now();
  18. +            while ((Date.now() - start) < 1000) {}
  19. +        });
  20.     </script>
  21. </body>
  22. </html>
复制代码
  1. import { injectJsError } from './lib/jsError';
  2. import { injectXHR } from './lib/xhr';
  3. import { blankScreen } from './lib/blankScreen';
  4. +import { timing } from './lib/timing';
  5. injectJsError();
  6. injectXHR();
  7. blankScreen();
  8. +timing();
复制代码
  1. import onload from '../util/onload';
  2. import tracker from '../util/tracker';
  3. import formatTime from '../util/formatTime';
  4. import getLastEvent from '../util/getLastEvent';
  5. import getSelector from '../util/getSelector';
  6. export function timing() {
  7.     onload(function () {
  8.         setTimeout(() => {
  9.             const {
  10.                 fetchStart,
  11.                 connectStart,
  12.                 connectEnd,
  13.                 requestStart,
  14.                 responseStart,
  15.                 responseEnd,
  16.                 domLoading,
  17.                 domInteractive,
  18.                 domContentLoadedEventStart,
  19.                 domContentLoadedEventEnd,
  20.                 loadEventStart } = performance.timing;
  21.             tracker.send({
  22.                 kind: 'experience',
  23.                 type: 'timing',
  24.                 connectTime: connectEnd - connectStart,//TCP连接耗时
  25.                 ttfbTime: responseStart - requestStart,//ttfb
  26.                 responseTime: responseEnd - responseStart,//Response响应耗时
  27.                 parseDOMTime: loadEventStart - domLoading,//DOM解析渲染耗时
  28.                 domContentLoadedTime: domContentLoadedEventEnd - domContentLoadedEventStart,//DOMContentLoaded事件回调耗时
  29.                 timeToInteractive: domInteractive - fetchStart,//首次可交互时间
  30.                 loadTime: loadEventStart - fetchStart//完整的加载时间
  31.             });
  32.         }, 3000);
  33.     });
  34. }
复制代码
4.6 性能指标

字段描述备注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 数据结构计划
4.6.2 实现
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4.     <meta charset="UTF-8">
  5.     <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6.     <title>monitor</title>
  7. </head>
  8. <body style="background-color: green;">
  9.     <div id="container">
  10.         <div class="content" style="width:600px;height:600px;word-wrap:break-word;">
  11.             <input />
  12.         </div>
  13.     </div>
  14.     <script>
  15.         let content = document.getElementsByClassName('content')[0];
  16.         //content.innerHTML = '@'.repeat(10000);
  17. +        setTimeout(() => {
  18. +            let h1 = document.createElement('h1');
  19. +            h1.innerHTML = '我是最有重要的内容';
  20. +            h1.setAttribute('elementtiming', 'meaningful');
  21. +            content.appendChild(h1);
  22. +        }, 2000);
  23.     </script>
  24. </body>
  25. </html>
复制代码
src\monitor\lib\timing.js
  1. 在这里插入代码片import onload from '../util/onload';
  2. import tracker from '../util/tracker';
  3. import formatTime from '../util/formatTime';
  4. import getLastEvent from '../util/getLastEvent';
  5. import getSelector from '../util/getSelector';
  6. export function timing() {
  7. +    let FMP, LCP;
  8. +    new PerformanceObserver((entryList, observer) => {
  9. +        let perfEntries = entryList.getEntries();
  10. +        FMP = perfEntries[0];
  11. +        observer.disconnect();
  12. +    }).observe({ entryTypes: ['element'] });
  13. +    new PerformanceObserver((entryList, observer) => {
  14. +        const perfEntries = entryList.getEntries();
  15. +        const lastEntry = perfEntries[perfEntries.length - 1];
  16. +        LCP = lastEntry;
  17. +        observer.disconnect();
  18. +    }).observe({ entryTypes: ['largest-contentful-paint'] });
  19. +    new PerformanceObserver(function (entryList, observer) {
  20. +        let lastEvent = getLastEvent();
  21. +        const firstInput = entryList.getEntries()[0];
  22. +        if (firstInput) {
  23. +            let inputDelay = firstInput.processingStart - firstInput.startTime;//处理延迟
  24. +            let duration = firstInput.duration;//处理耗时
  25. +            if (firstInput > 0 || duration > 0) {
  26. +                tracker.send({
  27. +                    kind: 'experience',
  28. +                    type: 'firstInputDelay',
  29. +                    inputDelay: inputDelay ? formatTime(inputDelay) : 0,
  30. +                    duration: duration ? formatTime(duration) : 0,
  31. +                    startTime: firstInput.startTime,
  32. +                    selector: lastEvent ? getSelector(lastEvent.path || lastEvent.target) : ''
  33. +                });
  34. +            }
  35. +        }
  36. +        observer.disconnect();
  37. +    }).observe({ type: 'first-input', buffered: true });
  38.     onload(function () {
  39.         setTimeout(() => {
  40.             const {
  41.                 fetchStart,
  42.                 connectStart,
  43.                 connectEnd,
  44.                 requestStart,
  45.                 responseStart,
  46.                 responseEnd,
  47.                 domLoading,
  48.                 domInteractive,
  49.                 domContentLoadedEventStart,
  50.                 domContentLoadedEventEnd,
  51.                 loadEventStart } = performance.timing;
  52.             tracker.send({
  53.                 kind: 'experience',
  54.                 type: 'timing',
  55.                 connectTime: connectEnd - connectStart,//TCP连接耗时
  56.                 ttfbTime: responseStart - requestStart,//ttfb
  57.                 responseTime: responseEnd - responseStart,//Response响应耗时
  58.                 parseDOMTime: loadEventStart - domLoading,//DOM解析渲染耗时
  59.                 domContentLoadedTime: domContentLoadedEventEnd - domContentLoadedEventStart,//DOMContentLoaded事件回调耗时
  60.                 timeToInteractive: domInteractive - fetchStart,//首次可交互时间
  61.                 loadTime: loadEventStart - fetchStart//完整的加载时间
  62.             });
  63. +            const FP = performance.getEntriesByName('first-paint')[0];
  64. +            const FCP = performance.getEntriesByName('first-contentful-paint')[0];
  65. +            console.log('FP', FP);
  66. +            console.log('FCP', FCP);
  67. +            console.log('FMP', FMP);
  68. +            console.log('LCP', LCP);
  69. +            tracker.send({
  70. +                kind: 'experience',
  71. +                type: 'paint',
  72. +                firstPaint: FP ? formatTime(FP.startTime) : 0,
  73. +                firstContentPaint: FCP ? formatTime(FCP.startTime) : 0,
  74. +                firstMeaningfulPaint: FMP ? formatTime(FMP.startTime) : 0,
  75. +                largestContentfulPaint: LCP ? formatTime(LCP.renderTime || LCP.loadTime) : 0
  76. +            });
  77.         }, 3000);
  78.     });
  79. }
复制代码
4.7 卡顿

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 实现
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4.     <meta charset="UTF-8">
  5.     <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6.     <title>monitor</title>
  7. </head>
  8. <body style="background-color: green;">
  9.     <div id="container">
  10.         <div class="content" style="width:600px;height:600px;word-wrap:break-word;">
  11. +            <button id="longTaskBtn">执行longTask</button>
  12.         </div>
  13.     </div>
  14.     <script>
  15.         let content = document.getElementsByClassName('content')[0];
  16. +        let longTaskBtn = document.getElementById('longTaskBtn');
  17. +        longTaskBtn.addEventListener('click', longTask);
  18. +        function longTask() {
  19. +            let start = Date.now();
  20. +            console.log('longTask开始 start', start);
  21. +            while (Date.now() < (200 + start)) { }
  22. +            console.log('longTask结束 end', (Date.now() - start));
  23. +        }
  24.     </script>
  25. </body>
  26. </html>
复制代码
  1. import { injectJsError } from './lib/jsError';
  2. import { injectXHR } from './lib/xhr';
  3. import { blankScreen } from './lib/blankScreen';
  4. import { timing } from './lib/timing';
  5. +import { longTask } from './lib/longTask';
  6. injectJsError();
  7. injectXHR();
  8. blankScreen();
  9. timing();
  10. +longTask();
复制代码
  1. import tracker from '../util/tracker';
  2. import formatTime from '../util/formatTime';
  3. import getLastEvent from '../util/getLastEvent';
  4. import getSelector from '../util/getSelector';
  5. export function longTask() {
  6.     new PerformanceObserver((list) => {
  7.         list.getEntries().forEach(entry => {
  8.             if (entry.duration > 100) {
  9.                 let lastEvent = getLastEvent();
  10.                 requestIdleCallback(() => {
  11.                     tracker.send({
  12.                         kind: 'experience',
  13.                         type: 'longTask',
  14.                         eventType: lastEvent.type,
  15.                         startTime: formatTime(entry.startTime),// 开始时间
  16.                         duration: formatTime(entry.duration),// 持续时间
  17.                         selector: lastEvent ? getSelector(lastEvent.path || lastEvent.target) : ''
  18.                     });
  19.                 });
  20.             }
  21.         });
  22.     }).observe({ entryTypes: ["longtask"] });
  23. }
复制代码
4.8 pv

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 实现
  1. <head>
  2.     <meta charset="UTF-8">
  3.     <meta name="viewport" content="width=device-width, initial-scale=1.0">
  4.     <title>前端监控SDK</title>
  5. </head>
复制代码
  1. import { injectJsError } from './lib/jsError';
  2. import { injectXHR } from './lib/xhr';
  3. import { blankScreen } from './lib/blankScreen';
  4. import { timing } from './lib/timing';
  5. import { longTask } from './lib/longTask';
  6. +import { pv } from './lib/pv';
  7. injectJsError();
  8. injectXHR();
  9. blankScreen();
  10. timing();
  11. longTask();
  12. +pv();
复制代码
  1. import tracker from '../util/tracker';
  2. export function pv() {
  3.     var connection = navigator.connection;
  4.     tracker.send({
  5.         kind: 'business',
  6.         type: 'pv',
  7.         effectiveType: connection.effectiveType, //网络环境
  8.         rtt: connection.rtt,//往返时间
  9.         screen: `${window.screen.width}x${window.screen.height}`//设备分辨率
  10.     });
  11.     let startTime = Date.now();
  12.     window.addEventListener('unload', () => {
  13.         let stayTime = Date.now() - startTime;
  14.         tracker.send({
  15.             kind: 'business',
  16.             type: 'stayTime',
  17.             stayTime
  18.         });
  19.     }, false);
  20. }
复制代码
5.查询报表

5.1 监控项分布
  1. * | SELECT type, COUNT(*) as number GROUP BY type LIMIT 10
复制代码
5.2 浏览器分布
  1. * | SELECT userAgent, COUNT(*) as number GROUP BY userAgent LIMIT 10
复制代码
5.3 页面分辨率分布
  1. * | SELECT screen, COUNT(*) as number GROUP BY screen LIMIT 10
复制代码
6.参考
6.1 第三方
6.1.1 商业产品

6.1.2 开源产品

6.2 defer 和 async


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。




欢迎光临 IT评测·应用市场-qidao123.com (https://dis.qidao123.com/) Powered by Discuz! X3.4