| 错误名称 | 备注 | 
|---|---|
| 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事件调用前执行