比较全的前端监控体系搭建篇(长文预警),多线程高并发

34 阅读14分钟

"selector": "HTML BODY #container .content INPUT" // 选择器

}

2. promiseError

{

...

"errorType": "promiseError",//错误类型 

"message": "someVar is not defined",//类型详情

"stack": "http://localhost:8080/:24:29^new Promise ()^btnPromiseClick (http://localhost:8080/:23:13)^HTMLInputElement.onclick (http://localhost:8080/:15:86)",//堆栈信息 

"selector": "HTML BODY #container .content INPUT"//选择器

}

3. resourceError

...

"errorType": "resourceError",//错误类型

"filename": "http://localhost:8080/error.js",//访问的文件名

"tagName": "SCRIPT",//标签名

"timeStamp": "76",//时间

4.2.3 实现

1. 资源加载错误 + js执行错误

//一般JS运行时错误使用window.onerror捕获处理

window.addEventListener(

"error",

function (event) {

let lastEvent = getLastEvent();

// 有 e.target.src(href) 的认定为资源加载错误

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都可以

2. promise异常

//当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都可以

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 实现

使用webpack devServer模拟请求

  • 重写xhr的open、send方法

  • 监听load、error、abort事件

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 实现

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

  • 根据 elementsFromPoint api,获取屏幕水平中线和竖直中线所在的元素

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[2]

  • DOMContentLoaded[3]

  • FMP[4]

4.5.1 阶段含义

faa89c469c9d97b4ff40e35e2701952c.png

image.png

| 字段 | 含义 |

| --- | --- |

| 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回调函数执行完成的时间 |

4.5.2 阶段计算

| 字段 | 描述 | 计算方式 | 意义 |

| --- | --- | --- | --- |

| 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 |

|

92e2cdb43324e057df2926c4ffae2738.png

image.png

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 实现

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[5]方法用于观察传入的参数中指定的性能条目类型的集合。当记录一个指定类型的性能条目时,性能监测对象的回调函数将会被调用

  • entryType[6]

  • paint-timing[7]

  • event-timing[8]

  • LCP[9]

  • FMP[10]

  • time-to-interactive[11]

| 字段 | 描述 | 备注 | 计算方式 |

| --- | --- | --- | --- |

| 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(首次输入延迟) | 用户首次和页面交互(单击链接,点击按钮等)到页面响应交互的时间 |

|

7ebdae1b3b92db1a57c3485f20241e28.png

image.png

e375c203cefca306a97f951080b05be2.png

image.png

4.6.1 数据结构设计

1. paint

{

"title": "前端监控系统",

"url": "http://localhost:8080/",

"timestamp": "1590828364186",

"userAgent": "chrome",

"kind": "experience",

"type": "paint",

"firstPaint": "102",

"firstContentPaint": "2130",

"firstMeaningfulPaint": "2130",

"largestContentfulPaint": "2130"

}

2. firstInputDelay

{

"title": "前端监控系统",

"url": "http://localhost:8080/",

"timestamp": "1590828477284",

"userAgent": "chrome",

"kind": "experience",

"type": "firstInputDelay",

"inputDelay": "3",

"duration": "8",

"startTime": "4812.344999983907",

"selector": "HTML BODY #container .content H1"

}

4.6.2 实现

关键时间节点通过window.performance.timing获取

f0f584f58fa587b5ca17e73139e3cdf9.png

image.png

import tracker from "../utils/tracker";

import onload from "../utils/onload";

import getLastEvent from "../utils/getLastEvent";

import getSelector from "../utils/getSelector";

export function timing() {

let FMP, LCP;

// 增加一个性能条目的观察者

new PerformanceObserver((entryList, observer) => {

const 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((entryList, observer) => {

const lastEvent = getLastEvent();

const firstInput = entryList.getEntries()[0];

if (firstInput) {

// 开始处理的时间 - 开始点击的时间,差值就是处理的延迟

let inputDelay = firstInput.processingStart - firstInput.startTime;

let duration = firstInput.duration; // 处理的耗时

if (inputDelay > 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,

} = window.performance.timing;

// 发送时间指标

tracker.send({

kind: "experience", // 用户体验指标

type: "timing", // 统计每个阶段的时间

connectTime: connectEnd - connectStart, // TCP连接耗时

ttfbTime: responseStart - requestStart, // 首字节到达时间

responseTime: responseEnd - responseStart, // response响应耗时

parseDOMTime: loadEventStart - domLoading, // DOM解析渲染的时间

domContentLoadedTime:

domContentLoadedEventEnd - domContentLoadedEventStart, // DOMContentLoaded事件回调耗时

timeToInteractive: domInteractive - fetchStart, // 首次可交互时间

loadTime: loadEventStart - fetchStart, // 完整的加载时间

});

// 发送性能指标

let FP = performance.getEntriesByName("first-paint")[0];

let 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 数据设计 longTask

{

"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 实现

  • new PerformanceObserver

  • entry.duration > 100 判断大于100ms,即可认定为长任务

  • 使用 requestIdleCallback上报数据

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、UV、用户停留时间

4.8.1 数据设计 business

{

"title": "前端监控系统",

"url": "http://localhost:8080/",

"timestamp": "1590829304423",

"userAgent": "chrome",

"kind": "business",

"type": "pv",

"effectiveType": "4g",

"rtt": "50",

"screen": "2049x1152"

}

4.8.2 PV、UV、用户停留时间

PV(page view) 是页面浏览量,UV(Unique visitor)用户访问量。PV 只要访问一次页面就算一次,UV 同一天内多次访问只算一次。

对于前端来说,只要每次进入页面上报一次 PV 就行,UV 的统计放在服务端来做,主要是分析上报的数据来统计得出 UV。

import tracker from "../util/tracker";

export function pv() {

tracker.send({

kind: "business",

type: "pv",

startTime: performance.now(), 下面是我在学习HTML和CSS的时候整理的一些笔记,有兴趣的可以看下:

HTML、CSS部分截图

进阶阶段

进阶阶段,开始攻 JS,对于刚接触 JS 的初学者,确实比学习 HTML 和 CSS 有难度,但是只要肯下功夫,这部分对于你来说,也不是什么大问题。

JS 内容涉及到的知识点较多,看到网上有很多人建议你从头到尾抱着那本《JavaScript高级程序设计》学,我是不建议的,毕竟刚接触 JS 谁能看得下去,当时我也不能,也没那样做。

我这部分的学习技巧是,增加次数,减少单次看的内容。就是说,第一遍学习 JS 走马观花的看,看个大概,去找视频以及网站学习,不建议直接看书。因为看书看不下去的时候很打击你学下去的信心。

然后通过一些网站的小例子,开始动手敲代码,一定要去实践、实践、实践,这一遍是为了更好的去熟悉 JS 的语法。别只顾着来回的看知识点,眼高手低可不是个好习惯,我在这吃过亏,你懂的。

1、JavaScript 和 ES6

在这个过程你会发现,有很多 JS 知识点你并不能更好的理解为什么这么设计,以及这样设计的好处是什么,这就逼着让你去学习这单个知识点的来龙去脉,去哪学?第一,书籍,我知道你不喜欢看,我最近通过刷大厂面试题整理了一份前端核心知识笔记,比较书籍更精简,一句废话都没有,这份笔记也让我通过跳槽从8k涨成20k。

JavaScript部分截图

2、前端框架

前端框架太多了,真的学不动了,别慌,其实对于前端的三大马车,Angular、React、Vue 只要把其中一种框架学明白,底层原理实现,其他两个学起来不会很吃力,这也取决于你以后就职的公司要求你会哪一个框架了,当然,会的越多越好,但是往往每个人的时间是有限的,对于自学的学生,或者即将面试找工作的人,当然要选择一门框架深挖原理。

以 Vue 为例,我整理了如下的面试题。

Vue部分截图

开源分享:docs.qq.com/doc/DSmRnRG…