目錄
- 前言
- 為什么要做前端監控
- 前端監控目標
- 穩定性
- 用戶體驗
- 業務
- 前端監控流程
- 常見埋點方案
- 代碼埋點
- 可視化埋點
- 無痕埋點
- 創建項目
- 第一步、創建`monitor`文件,`cmd`進入文件進行`npm init -y` 項目初始化
- 第二步、創建`src/index.js`和`src/index.html`文件
- 第三步、創建`webpack.config.js`文件
- 第四步、安裝所需模塊
- 第五步、配置package.json
- js報錯,資源加載報錯,promise報錯采集上報腳本
- 監控錯誤和錯誤分類
- 錯誤異常上報數據結構設計
- js報錯上報數據結構
- promise報錯上報數據結構
- 資源加載報錯上報數據結構
- js異常監聽上報
- 第一步、編寫index.html
- 第二步、創建`/src/monitor/index.js`文件
- 第三步,入口文件中引入`src/index.js`
- 第四步、創建`/src/monitor/lib/jsError.js`
- 第五步、`/src/monitor/index.js`導入`jsError.js`
- 第六步、查看是否打印成功
- 第七步、新建`/src/monitor/utils/getLastEvent.js`工具文件
- 第八步、新建`/src/monitor/utils/getSelector.js` 獲取報錯元素路徑文件
- 第八步、創建上報文件`/src/monitor/utils/getSelector.js`
- 第八步、編寫`jsError.js`文件
- 第九步、上報效果查看
- Promise異常監聽上報
- 第一步、promise監聽異常上報方法實現
- 第二步、上報效果查看
- 資源加載異常監聽上報
- 第一步、改造`/src/monitor/jsError.js文件`
- 第二步、修改`/src/monitor/utils/getSelector.js`方法
- 第三步、上報效果查看
- 接口異常采集上報腳本
- 接口異常上報數據結構設計
- 接口監聽上報
- 第一步、模擬接口請求
- 第二步、創建`xhr.js`監聽接口上報文件
- 第三步、引入并使用上報監聽
- 第四步、查看接口監聽上報效果
- 白屏采集上報腳本
- 白屏采集上報數據結構
- 思路
- 頁面關鍵點采樣對比技術實現白屏上報
- 第一步、創建onload.js文件方法
- 第二步、關鍵點采樣對比監控白屏上報
- 第三步、編寫可監聽到白屏和非白屏的HTML文件
- 第四步、監測白屏上報和非白屏效果
- 加載時間采集上報
- 瀏覽器加載一個網頁的整個流程
- 流程中字段描述
- 其他輔助階段字段描述
- 需要上報字段描述
- 瀏覽器加載和渲染網頁的整個過程
- 瀏覽器加載和渲染時間計算上報實現
- 第一步、創建`/src/monitor/lib/timing.js`文件
- 第二步、模擬DOM加載完成延遲inde.html文件
- 第三步、瀏覽器加載和渲染時間上報效果
- 性能指標采集上報
- 性能指標描述
- 各個性能指標獲取
- 觸發用例index.html
- FMP 首次有意義繪制
- LCP 最大內容渲染
- FID 首次輸入延遲
- FP 首次繪制
- FCP 首次內容繪制
- 性能指標上報整體代碼
- 查看各項性能指標采集上報效果
- 卡頓監聽上報
- 監聽方案
- 數據結構
- 借助Long Tasks API實現卡頓上報
- 第一步、創建longTask.js文件
- 第二步、循環模仿卡頓效果
- 第三步、查看卡頓上報效果
- PV、UV、頁面停留時間采集上報
- 實現pv和頁面停留時間監聽
- 第一步、創建pv.js
- 第二步、查看上報結果
- 完結~
前言
????????在當今快速發展的Web
開發領域中,無論是面試還是與同行交流時,“高大上”的技術術語層出不窮。還記得我第一次了解到前端埋點監控是在一家知名大廠的面試過程中。當面試官問及如何實現前端埋點監控時,我感到一陣茫然——腦海中一片模糊,各種概念交織卻難以理清頭緒。不出所料,那次面試的第三輪我沒有通過。
????????這次經歷成為我深入學習前端埋點監控的起點。回到家中,我開始查閱大量資料、觀看教學視頻,逐步摸索出了一套學習路徑。本文便是基于這段時間的學習和實踐總結而成,旨在為那些剛開始接觸前端埋點監控的朋友提供一個詳盡的指南。
????????文中涵蓋了從PV
統計、頁面加載性能指標(如FMP
、LCP
、FID、FP、FCP)到Promise異常監控、錯誤監控以及資源加載監控等多方面的內容。為了確保每個細節都能被清晰理解,并且避免將來遺忘關鍵知識點,我在編寫時盡可能地詳盡描述了每一步驟和每一個概念。
????????無論你是剛踏入前端領域的新人,還是希望進一步提升技能的開發者,相信這篇文章都能為你提供有價值的參考。讓我們一起揭開前端埋點監控的神秘面紗,掌握這一提升網站性能和用戶體驗的重要工具。
????????覺得內容有用?別忘了點個贊+收藏+關注,三連走一波!你的支持是我持續輸出的動力 💪
gitee倉庫地址:地址
為什么要做前端監控
珠峰架構公開課地址【下文根據公開課邊學邊做邊實現,加上自己的個人想法】:珠峰公開課地址
更快發現問題和解決問題。
做產品的決策依據。
提升前端工程師的技術深度和廣度。
為業務擴展提供更多可能性。
前端監控目標
穩定性
錯誤名稱 | 備注 |
---|---|
JS 錯誤 | JS 執行錯誤或者promise異常 |
資源異常 | script ,link 資源加載異常 |
接口錯誤 | ajax 或fetch 接口請求異常 |
白屏 | 頁面白屏 |
用戶體驗
錯誤名稱 | 備注 |
---|---|
加載時間 | 各個階段的加載時間 |
TTFB 首字節時間 | 是指瀏覽器發起第一個請求到數據返回第一個字節所消耗的時間,這個時間包含了網絡請求時間,后端處理時間。 |
FP 首次繪制 | 首次繪制包括了任何用戶自定義的背景繪制,它是將第一個像素點繪制到屏幕的時刻。 |
FCP 首次內容繪制 | 首次內容繪制是瀏覽器將第一個DOM 渲染到屏幕的時間,可以是任何文本圖像和SVG 等的時間。 |
FMP 首次有意義繪制 | 首次有意義繪制是頁面可用性的亮度標準。 |
FID 首次輸入延遲 | 用戶首次和頁面交互到頁面響應交互的時間 |
卡頓 | 超過50ms 的長任務 |
業務
錯誤名稱 | 備注 |
---|---|
PV | page view 即頁面瀏覽量或點擊量 |
UV | 指訪問某個站點不同IP 地址的人數 |
頁面停留時間 | 用戶在每一個頁面停留的時間 |
前端監控流程
- 前端埋點
- 數據上報
- 分析和計算將采集到的數據進行加工匯總
- 可視化展示將數據按各個維度進行展示
- 監控報警,發現問題后按一定條件觸發報警
上圖中,前端主要關注埋點和數據采集兩個階段即可。
常見埋點方案
代碼埋點
代碼埋點就是以嵌入代碼的形式進行埋點,比如需要監控用戶的點擊事件,會選擇在用戶點擊時插入一段代碼,保存這個監聽行為或者直接將監聽行為以某一種數據格式直接傳遞給服務端。
優點: 可以在任意時刻,精確的發送或保存所需要的數據信息。
缺點: 工作量比較大
可視化埋點
通過可視化交互的手段,代替代碼埋點,將業務代碼和埋點代碼分離,提供一個可視化交互的頁面,輸入業務代碼,通過這個可視化系統可以在業務代碼中自定義的增加埋點時間等等,最后輸出的代碼耦合了業務代碼和埋點代碼,可視化埋點其實是用系統來代替手工插入埋點代碼。
無痕埋點
前端的任意一個事件都被綁定一個標識,所有的事件都被記錄下來,通過定期上傳記錄文件,配合文件解析,解析出來我們想要的數據,并生成可視化報告供專業人員分析.
缺點: 數據傳輸和服務器壓力增加,無法靈活定制數據結構。
創建項目
第一步、創建monitor
文件,cmd
進入文件進行npm init -y
項目初始化
第二步、創建src/index.js
和src/index.html
文件
第三步、創建webpack.config.js
文件
const path = require('path')
// webpack打包項目的,HtmlWebpackPlugin生成產出HTML文件,user-agent 把瀏覽器的userAgent變成一個對象
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {entry: "./src/index.js", // 入口文件 context: process.cwd(), // 上下文目錄 mode: "development", // 開發模式 output: {path: path.resolve(__dirname, "dist"), // 輸出目錄 filename: "monitor.js" // 文件名 },devServer: {static: {directory: path.resolve(__dirname, "dist"), // devServer靜態文件目錄,替換contentBase },}, plugins: [new HtmlWebpackPlugin({ // 自動打包出HTML文件 template: "./src/index.html",inject: "head"})]
}
第四步、安裝所需模塊
npm install webpack webpack-cli html-webpack-plugin user-agent -D
npm i webpack-dev-server -D
第五步、配置package.json
{"name": "monitor","version": "1.0.0","description": "","main": "index.js","scripts": {"build": "webpack","dev": "webpack-dev-server"},"keywords": [],"author": "","license": "ISC","devDependencies": {"html-webpack-plugin": "^5.6.3","user-agent": "^1.0.4","webpack": "^5.99.7","webpack-cli": "^6.0.1","webpack-dev-server": "^5.2.1"}
}
執行npm run build
,執行后會出現兩個文件
啟動服務器: npm run dev
js報錯,資源加載報錯,promise報錯采集上報腳本
監控錯誤和錯誤分類
JS
錯誤:js
報錯。promise
異常 資源異常:監聽error
。
錯誤異常上報數據結構設計
js報錯上報數據結構
{errorType: "jsError"filename: "http://localhost:8080/"kind: "stability"message: "Uncaught TypeError: Cannot set properties of undefined (setting 'error')"position: "25:30"selector: "html body div#container div.content input"stack: "TypeError: Cannot set properties of undefined (setting 'error')^errorClick (http://localhost:8080/:25:30)^HTMLInputElement.onclick (http://localhost:8080/:14:72)"timestamp: "1746587404145"title: "前端監控SDK"type: "error"url: "http://localhost:8080/"
}
promise報錯上報數據結構
errorType: "PromiseError" // 錯誤類型filename: "http://localhost:8080/" // 訪問的文件名kind: "stability" // 大類message: "Cannot set properties of undefined (setting 'error')"position: "30:32" // 報錯行列selector: "html body div#container div.content input"stack: "TypeError: Cannot set properties of undefined (setting 'error')^http://localhost:8080/:30:32^new Promise (<anonymous>)^promiseErrirClick (http://localhost:8080/:29:9)^HTMLInputElement.onclick (http://localhost:8080/:19:11)" timestamp: "1746587310847"title: "前端監控SDK" // 頁面標題type: "error" // 小類url: "http://localhost:8080/" // urluserAgent:"chrome 135.0.0.0" // 用戶瀏覽器信息
資源加載報錯上報數據結構
{errorType: "resourceError", // 錯誤類型filename: "http://localhost:8080/monitor.css", // 訪問的文件名kind: "stability", // 大類selector: "html head link" "selector", // 選擇器tagName: "LINK", // 標簽名timestamp: "1746587169153", // 時間戳title: "前端監控SDK", // 頁面標題type: "error", // 小類url: "http://localhost:8080/", // 頁面URLuserAgent:"chrome 135.0.0.0" // 用戶瀏覽器信息
}
js異常監聽上報
第一步、編寫index.html
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>前端監控SDK</title><script src="monitor.js"></script></head><body><div id="container"><div class="content"><input type="button" value="點擊報錯" onclick="errorClick()" /><inputtype="button"value="點擊拋出Promise錯誤"onclick="promiseErrirClick()"/></div></div><script>function errorClick() {window.someVar.error = "error";}</script></body>
</html>
第二步、創建/src/monitor/index.js
文件
第三步,入口文件中引入src/index.js
第四步、創建/src/monitor/lib/jsError.js
創建/src/monitor/lib/jsError.js
并導出export function injectJsError() { }
第五步、/src/monitor/index.js
導入jsError.js
import { injectJsError } from './lib/jsError'
injectJsError()
第六步、查看是否打印成功
第七步、新建/src/monitor/utils/getLastEvent.js
工具文件
let lastEvent;
['click', 'touchstart', 'mousedown', 'keydown', 'mouseover'].forEach(eventType => {document.addEventListener(eventType, function (e) {lastEvent = e}, {capture: true, // 捕獲階段監聽passive: true, // 默認不阻止默認事件})
})
第八步、新建/src/monitor/utils/getSelector.js
獲取報錯元素路徑文件
function getSelectors(path) {// 反轉輸入的元素和路徑數組,并過濾掉document和window對象return path.reverse().filter(element => {return element !== document && element !== window}).map(element => {let selector = ""; // 初始化選擇器字符串if (element.id) { // 如果元素有id屬性,則使用id創建更具體的選擇器return `${element.nodeName.toLowerCase()}#${element.id}`} else if (element.className && typeof element.className === "string") {// 如果元素沒有id屬性,但是有className屬性,并且是個字符串,則嘗試使用class來創建選擇器return `${element.nodeName.toLowerCase()}.${element.className}`} else {// 如果既沒有id也沒有class則進返回元素的標簽名作為選擇器return selector = `${element.nodeName.toLowerCase()}`}}).join(" ") // 將所有選擇器用空格拼接
}
export default function (path) {if (Array.isArray(path)) {return getSelectors(path)}
}
第八步、創建上報文件/src/monitor/utils/getSelector.js
監聽錯誤后,調用send(data)方法,調用上報接口,將日志上報到阿里云日志中,當然如下的aliyun鏈接已經失效,可以換成自己的服務接口即可。
let host = 'en-beijing-log.aliyuncs.com';
let project = 'zhufengmonitor';
let logStore = 'zhufengmonitor-store';
let userAgent = require('user-agent');
function getExtraData() {return {title: document.title,url: location.url,timestamp: Date.now(),userAgent: userAgent.parse(navigator.userAgent)// 用戶ID}
}
class SendTracker {constructor() {// 上報路徑 : 項目名.主機名/logstores/存儲的名字/trackthis.url = `${project}.${host}/logstores/${logStore}/track` // 上報路徑this.xhr = new XMLHttpRequest();}send(data = {}) {let extraData = getExtraData();let log = { ...extraData, ...data }// 對象的值不能是數字for (let key in log) {if (typeof log[key] === 'number') {log[key] = `${log[key]}`;}}console.log("send log:", log)let body = JSON.stringify(log);this.xhr.open('POST', this.url, true);this.xhr.setRequestHeader('Content-Type', 'application/json') // 請求體類型this.xhr.setRequestHeader('x-log-apiversion', '0.6.0') // 版本號this.xhr.setRequestHeader('x-log-bodyrawsize', body.length) // 請求體的大小this.xhr.onload = function () {console.log(this.xhr.response);}this.xhr.onerror = function (error) {console.log(error);}this.xhr.send(body);}
}
export default new SendTracker();
第八步、編寫jsError.js
文件
需要注意,要按照阿里云日志接口格式進行傳輸
let host = 'cn-beijing.log.aliyuncs.com';
let project = 'zhufengmonitor';
let logStore = 'zhufengmonitor-store';
let userAgent = require('user-agent');
function getExtraData() {return {title: document.title,url: location.url,timestamp: Date.now(),userAgent: userAgent.parse(navigator.userAgent)// 用戶ID}
}
class SendTracker {constructor() {// 上報路徑 : 項目名.主機名/logstores/存儲的名字/trackthis.url = `https://${project}.${host}/logstores/${logStore}/track` // 上報路徑this.xhr = new XMLHttpRequest();}send(data = {}) {let extraData = getExtraData();let log = { ...extraData, ...data }// 對象的值不能是數字for (let key in log) {if (typeof log[key] === 'number') {log[key] = `${log[key]}`;}}let body = JSON.stringify({__logs__: [log]})this.xhr.open('POST', this.url, true);this.xhr.setRequestHeader('Content-Type', 'application/json') // 請求體類型this.xhr.setRequestHeader('x-log-apiversion', '0.6.0') // 版本號this.xhr.setRequestHeader('x-log-bodyrawsize', body.length) // 請求體的大小this.xhr.onload = function () {// console.log(this.xhr.response);}this.xhr.onerror = function (error) {// console.log(error);}this.xhr.send(body);}
}
export default new SendTracker();
第九步、上報效果查看
鏈接地址已經失效,如果實現換成自己的服務接口即可。
傳輸數據格式
Promise異常監聽上報
第一步、promise監聽異常上報方法實現
src/monitor/lib/jsError.js
文件新增promise
拋出異常監聽代碼:
import getLastEvent from '../utils/getLastEvent.js'
import getSelector from '../utils/getSelector.js'
import tracker from '../utils/tracker.js'
export function injectJsError() {// 監聽全局未捕獲的錯誤window.addEventListener('error', function (event) {let lastEvent = getLastEvent() // 獲取最后一個交互事件// 獲取冒泡向上的dom路徑var path = lastEvent.path || (function (evt) {var path = [];var el = evt.target;while (el) {path.push(el);el = el.parentElement;}return path;})(lastEvent);let log = {kind: 'stability', // 監控指標的大類type: 'error', // 小類型錯誤errorType: 'jsError', // JS執行錯誤filename: event.filename, // 錯誤所在的文件position: event.lineno + ':' + event.colno, // 錯誤所在的行和列的位置message: event.message, // 錯誤信息stack: getLines(event.error.stack), // 錯誤堆棧selector: lastEvent ? getSelector(path) : "" // 代表最后一個操作的元素}tracker.send(log) // 上報數據})
// 監聽全局promise錯誤異常window.addEventListener('unhandledrejection', function (event) {console.log("promise error:", event)let lastEvent = getLastEvent(); // 最后一個事件對象var path = lastEvent.path || (function (evt) {var path = [];var el = evt.target;while (el) {path.push(el);el = el.parentElement;}return path;})(lastEvent);let message;let filename;let line = 0;let column = 0;let stack = '';let reason = event.reason;if (typeof reason === 'string') {message = reason;} else if (typeof reason === 'object') { // 說明是一個錯誤對象 if (reason.stack) {let matchResult = reason.stack.match(/at\s+(.+):(\d+):(\d+)/);filename = matchResult[1];line = matchResult[2];column = matchResult[3];}stack = getLines(reason.stack)}message = reason.message;tracker.send({kind: 'stability', // 監控指標的大類type: 'error', // 小類型錯誤errorType: 'PromiseError', // promise執行錯誤filename, // 錯誤所在的文件position: line + ':' + column, // 錯誤所在的行和列的位置message, // 錯誤信息stack, // 錯誤堆棧selector: lastEvent ? getSelector(path) : "" // 代表最后一個操作的元素}) // 上報數據}, true)// 拼接at報錯方法一“^”拼接function getLines(stack) {return stack.split('\n').map(item => item.replace(/^\s+at\s+/g, '')).join('^');}
}
第二步、上報效果查看
接口換為公司上報接口即可:
資源加載異常監聽上報
第一步、改造/src/monitor/jsError.js文件
如果 (JavaScript 文件
、CSS 文件
)如果加載失敗都會觸發window.addEventListener('error', (event) => {})
的監聽事件
import getLastEvent from '../utils/getLastEvent.js'
import getSelector from '../utils/getSelector.js'
import tracker from '../utils/tracker.js'
export function injectJsError() {// 監聽全局未捕獲的錯誤 window.addEventListener('error', (event) => {let lastEvent = getLastEvent() // 獲取最后一個交互事件// 這是一個腳本加載錯誤if (event.target && (event.target.src || event.target.href)) {let log = {kind: 'stability', // 監控指標的大類type: 'error', // 小類型錯誤errorType: 'resourceError', // JS或者CS資源加載錯誤filename: event.target.src || event.target.href, // 哪個文件報錯了tagName: event.target.tagName, // 錯誤所在標簽// stack: getLines(event.error.stack), // 錯誤堆棧selector: getSelector(event.target) // 代表最后一個操作的元素}tracker.send(log) // 上報數據} else {// 獲取冒泡向上的dom路徑var path = lastEvent.path || (function (evt) {var path = [];var el = evt.target;while (el) {path.push(el);el = el.parentElement;}return path;})(lastEvent);let log = {kind: 'stability', // 監控指標的大類type: 'error', // 小類型錯誤errorType: 'jsError', // JS執行錯誤filename: event.filename, // 錯誤所在的文件position: event.lineno + ':' + event.colno, // 錯誤所在的行和列的位置message: event.message, // 錯誤信息stack: getLines(event.error.stack), // 錯誤堆棧selector: lastEvent ? getSelector(path) : "" // 代表最后一個操作的元素}tracker.send(log) // 上報數據}}, true)window.addEventListener('unhandledrejection', function (event) {console.log("異常錯誤");let lastEvent = getLastEvent(); // 最后一個事件對象var path = lastEvent.path || (function (evt) {var path = [];var el = evt.target;while (el) {path.push(el);el = el.parentElement;}return path;})(lastEvent);let message;let filename;let line = 0;let column = 0;let stack = '';let reason = event.reason;if (typeof reason === 'string') {message = reason;} else if (typeof reason === 'object') { // 說明是一個錯誤對象 if (reason.stack) {let matchResult = reason.stack.match(/at\s+(.+):(\d+):(\d+)/);filename = matchResult[1];line = matchResult[2];column = matchResult[3];}stack = getLines(reason.stack)}message = reason.message;tracker.send({kind: 'stability', // 監控指標的大類type: 'error', // 小類型錯誤errorType: 'PromiseError', // JS執行錯誤filename, // 錯誤所在的文件position: line + ':' + column, // 錯誤所在的行和列的位置message, // 錯誤信息stack, // 錯誤堆棧selector: lastEvent ? getSelector(path) : "" // 代表最后一個操作的元素}) // 上報數據}, true)// 拼接at報錯方法一“^”拼接function getLines(stack) {return stack.split('\n').map(item => item.replace(/^\s+at\s+/g, '')).join('^');}
}
第二步、修改/src/monitor/utils/getSelector.js
方法
function getSelectors(path) {// 反轉輸入的元素和路徑數組,并過濾掉document和window對象return path.reverse().filter(element => {return element !== document && element !== window}).map(element => {let selector = ""; // 初始化選擇器字符串if (element.id) { // 如果元素有id屬性,則使用id創建更具體的選擇器return `${element.nodeName.toLowerCase()}#${element.id}`} else if (element.className && typeof element.className === "string") {// 如果元素沒有id屬性,但是有className屬性,并且是個字符串,則嘗試使用class來創建選擇器return `${element.nodeName.toLowerCase()}.${element.className}`} else {// 如果既沒有id也沒有class則進返回元素的標簽名作為選擇器return selector = `${element.nodeName.toLowerCase()}`}}).join(" ") // 將所有選擇器用空格拼接
}
export default function (pathOrTarget) {if (Array.isArray(pathOrTarget)) { // 可能是一個數組,也可能是一個對象 return getSelectors(pathOrTarget)} else {let path = []while (pathOrTarget) {path.push(pathOrTarget);pathOrTarget = pathOrTarget.parentNode;}return getSelectors(path)}
}
第三步、上報效果查看
上面換成公司服務上報接口即可
接口異常采集上報腳本
接口異常上報數據結構設計
接口監聽上報
第一步、模擬接口請求
編寫/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>前端監控SDK</title><script src="monitor.js"></script></head><body><div id="container"><div class="content"><inputid="successBtn"type="button"value="ajax成功請求"onclick="sendSuccess()"/><inputid="errorBtn"type="button"value="ajax失敗請求"onclick="sendError()"/></div></div><script>//function sendSuccess() {let xhr = new XMLHttpRequest();xhr.open("POST","http://192.168.60.38:32753/visiondevice/checkParamConfig/getDetails",true);xhr.responseType = "json";xhr.onload = function () {console.log(xhr.response);};xhr.send();}// 發送錯誤報錯function sendError() {let xhr = new XMLHttpRequest();xhr.open("POST","http://192.168.60.38:32753/visiondevice/systemConfig/getDetail",true);xhr.responseType = "json";xhr.onload = function () {console.log(xhr.response);};xhr.onerror = function (error) {console.log("error", error);};xhr.send();}</script></body>
</html>
第二步、創建xhr.js
監聽接口上報文件
創建/src/monitor/lib/xhr.js
文件,編寫增強 XMLHttpRequest
對象的功能,以監控和記錄所有的 AJAX
請求及其狀態。通過重寫 XMLHttpRequest.prototype.open
和 XMLHttpRequest.prototype.send
方法,可以在請求發出前、請求完成時以及請求失敗或被取消時收集相關數據,并使用 tracker.send
方法將這些數據上報給某個監控系統。
import tracker from '../utils/tracker.js';
export default function injectXHR() {// 保存原始的 open 和 send 方法const originalXhrOpen = window.XMLHttpRequest.prototype.open;window.XMLHttpRequest.prototype.open = function (method, url, async) {console.log("url", url)// 上報請求不需要返回if (!url.match(/logstores/) && !url.match(/sockjs/)) {console.log("logstores")this.logData = {method,url,async,startTime: Date.now()};}return originalXhrOpen.apply(this, arguments);};const originalXhrSend = window.XMLHttpRequest.prototype.send;window.XMLHttpRequest.prototype.send = function (body) {if (this.logData) {const startTime = Date.now();const handler = (type) => (event) => {const duration = Date.now() - startTime;const status = this.status; // 狀態碼const statusText = this.response.error; // OK Server Errortracker.send({kind: 'stability',type: 'xhr',eventType: event.type,pathname: this.logData.url, // 請求路徑status: `${status}-${statusText}`, // 狀態碼duration, // 持續時間response: JSON.stringify(this.response),requestData: body || '',params: this.logData.params, // 響應體timestamp: Date.now()});};this.addEventListener('load', handler('load'), false);this.addEventListener('error', handler('error'), false);this.addEventListener('abort', handler('abort'), false);}return originalXhrSend.apply(this, arguments);};
}
第三步、引入并使用上報監聽
在src/monitor/index.js
引入,并使用
import { injectJsError } from './lib/jsError'
import injectXHR from './lib/xhr'
injectJsError()
injectXHR()
第四步、查看接口監聽上報效果
接口請求成功:(忽略上報接口)
接口請求失敗:(忽略上報接口)
請求成功上報效果:
請求失敗上報效果:
白屏采集上報腳本
白屏采集上報數據結構
思路
在頁面中垂直交叉選取多個采樣點,使用elementsFromPoint
API獲取采樣點下的HTML
元素,判斷采樣點元素是否與容器元素相同,遍歷采樣點,設置采樣點與容器元素相同的個數從而判斷是否出現擺屏,這種方法精確度比較高,技術棧無關,通用性能好,但是開發成本比較高。
頁面關鍵點采樣對比技術實現白屏上報
第一步、創建onload.js文件方法
????????新建src/monitor/utils/onload.js
文件
????????如果在頁面完全沒有加載完畢的時候【元素,樣式表,iframe
等外部資源】,dom
可能還未穩定,就會導致document.elementsFromPoint()
獲取到的元素并不是最終用戶看到的那些。如果在這種情況下執行白屏檢測,可能會錯誤地判斷某些點為空白點,因為這些點對應的元素尚未加載或渲染完成。
????????在頁面加載過程中,JavaScript 可能會在 DOMContentLoaded 事件觸發后立即執行,這時雖然HTML文檔已經被完全加載和解析,但是頁面上的圖片、樣式表等外部資源可能還沒有加載完畢。因此,在這種狀態下直接進行白屏檢測,有可能會錯過一些重要的視覺變化,導致誤判。
export default function (callback) {if (document.readyState === 'complete') {callback()} else {window.addEventListener('load', () => {callback()})}
}
第二步、關鍵點采樣對比監控白屏上報
創建/src/monitor/lib/xhr.js
文件,頁面關鍵點采樣對比實現白屏上報功能:
import tracker from '../utils/tracker.js';
import onload from '../utils/onload.js';
// src/monitor/utils/onload.jsexport default function blankScreen() {let warpperElements = ['html', 'body', '#container', ".content"]let enptyPoints = 0function getSelector(element) {if (element && element.id) {return "#" + element.id} else if (element && element.className) {// a b c => .a .b .creturn "." + element.className.split(' ').filter(item => !!item).join('.')} else {return element && element.nodeName.toLowerCase()}}// 是包裹元素++function isWrapper(element) {let selector = getSelector(element)if (warpperElements.indexOf(selector) != -1) {enptyPoints++}}// 當整個頁面渲染完成了才去判斷是否是白屏onload(function () {for (let i = 1; i <= 18; i++) {let xElement = document.elementsFromPoint(window.innerWidth * i / 10, window.innerHeight / 2)let yElement = document.elementsFromPoint(window.innerWidth / 2, window.innerHeight * i / 10)isWrapper(xElement[0])isWrapper(yElement[0])}console.log("enptyPoints", enptyPoints)if (enptyPoints >= 16) {let centerElements = document.elementsFromPoint(window.innerWidth / 2, window.innerHeight / 2)tracker.send({kind: "stability",type: "blank",blankCount: enptyPoints, // 空白點screen: window.screen.width + "*" + window.screen.height, // 屏幕尺寸viewPoint: window.innerWidth + "*" + window.innerHeight, // 視口尺寸selector: getSelector(centerElements[0]),page: window.location.href, // 頁面地址message: "頁面空白點大于16個"})}});
}
第三步、編寫可監聽到白屏和非白屏的HTML文件
不觸發白屏監聽上報html
文件,
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>前端監控SDK</title><script src="monitor.js"></script></head><body><div id="container"><div class="content" style="width: 100%; word-wrap: break-word"></div></div><script>let content = document.getElementsByClassName("content")[0];content.innerHTML = "<span>aaa</span>".repeat(10000);</script></body>
</html>
觸發白屏監聽上報html
文件:
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>前端監控SDK</title><script src="monitor.js"></script></head><body><div id="container"><div class="content"><inputid="successBtn"type="button"value="ajax成功請求"onclick="sendSuccess()"/><inputid="errorBtn"type="button"value="ajax失敗請求"onclick="sendError()"/></div></div><script>//function sendSuccess() {let xhr = new XMLHttpRequest();xhr.open("POST","http://192.168.60.38:32753/visiondevice/systemConfig/getDetail",true);xhr.responseType = "json";xhr.onload = function () {console.log(xhr.response);};xhr.send();}// 發送錯誤報錯function sendError() {let xhr = new XMLHttpRequest();xhr.open("POST","http://192.168.60.38:32753/visiondevice/checkParamConfig/getDetailsAAAA",true);xhr.responseType = "json";xhr.onload = function () {console.log(xhr.response);};xhr.onerror = function (error) {console.log("error", error);};xhr.send();}</script></body>
</html>
第四步、監測白屏上報和非白屏效果
填滿<span>
標簽,就不會出現白屏,沒有打印上報日志
不填滿<span>
標簽,就會出現白屏空襲,就會打印上報日志并且發送給服務端
發送服務端數據
加載時間采集上報
瀏覽器加載一個網頁的整個流程
上面一張圖展示了瀏覽器加載一個網頁的整個流程,從開始時間到頁面完全加載完成。
流程中字段描述
字段 | 描述 |
---|---|
startTime(開始時間) | 這是整個加載過程的起點 |
Prompt for unload(準備卸載舊頁面) | 如果當前頁面是從另一個頁面導航過來的,瀏覽器會觸發卸載事件,準備卸載舊頁面。 |
redirectStart(開始重定向) | 如果存在重定向,瀏覽器會在此時開始重定向過程。 |
redirectEnd(結束重定向) | 重定向過程結束。 |
fetchStart(開始獲取文檔) | 瀏覽器開始獲取文檔的時間點。 |
domainLookupStart(開始域名解析) | 瀏覽器開始進行DNS查詢以解析域名。 |
domainLookupEnd(結束域名解析) | DNS查詢完成。 |
connectStart(開始鏈接) | 瀏覽器開始建立與服務器的TCP連接。 |
secureConnectionStart(開始安全連接) | 如果使用HTTPS,瀏覽器開始TLS握手過程。 |
connectEnd(結束連接) | TCP連接建立完成。 |
requestStart(開始請求) | 瀏覽器開始發送HTTP請求到服務器。 |
Time to First Byte(TTFB,首次字節時間) | 從發送請求到接收到第一個字節響應的時間。 |
responseStart(響應開始) | 服務器開始發送響應數據。 |
responseEnd(響應結束) | 服務器完成響應數據的發送。 |
unloadEventStart(卸載事件開始) | 在前一個頁面的卸載過程中,卸載事件開始。 |
unloadEventEnd(卸載事件結束) | 卸載事件結束。 |
domLoading(開始解析DOM) | 瀏覽器開始解析HTML文檔并構建DOM樹。 |
domInteractive(DOM結構基本解析完畢) | DOM樹的基本結構已經構建完成,可以進行交互操作。 |
domContentLoadedEventStart(DOMContentLoaded事件開始) | DOM完全加載且解析完成,但不包括樣式表、圖片等外部資源。 |
domContentLoadedEventEnd(DOMContentLoaded事件結束) | DOMContentLoaded事件處理程序執行完畢。 |
domComplete(DOM和資源解析都完成) | 所有資源(如圖片、腳本等)都已加載完成。 |
loadEventStart(開始load回調函數) | 瀏覽器開始執行load事件的回調函數。 |
onLoad(加載事件) | 頁面完全加載完成,觸發load事件。 |
loadEventEnd(結束load回調函數) | load事件的回調函數執行完畢。 |
其他輔助階段字段描述
字段 | 描述 |
---|---|
appCache(緩存存儲) | 應用緩存的處理過程 |
DNS(域名緩存) | DNS緩存的處理過程 |
TCP(網絡連接) | TCP鏈接的處理過程 |
Request(請求) | 請求的處理過程 |
Response(相應) | 響應的處理過程 |
Procsssing(處理) | 對響應數據的處理過程 |
Load(加載) | 頁面加載的最終階段 |
需要上報字段描述
字段 | 描述 | 計算方式 | 意義 |
---|---|---|---|
unload | 前一個頁面卸載耗時 | unloadEventEnd - unloadEventStart | - |
redirect | 重定向耗時 | redirectEnd - redirectStart | 重定向耗時 |
appCache | 緩存耗時 | domainLookupStart - fetchStart | 讀取緩存的時間 |
dns | dns解析耗時 | domainLookupEnd - domainLookupStart | 可觀察域名解析服務是否正常 |
tcp | tcp鏈接耗時 | connectEnd - cibbectStart | 建立連接耗時 |
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 階段渲染耗時 | responseEnd - fetchStart | DOM樹和頁面資源加載完成時間,會觸發domContentLoaded事件 |
首次渲染耗時 | 首次渲染耗時 | responseEnd - fetchStart | 加載文檔到看到第一幀非空圖像的時間,也叫做白屏時間 |
首次可交互事件 | 首次可交互時間 | domInteractive - fetchStart | DOM樹解析完成時間,此時document.readyStart為interactive |
首包時間耗時 | 首包耗時 | responseStart - domainLookupStart | DNS解析到響應返回給瀏覽器第一個字節的時間 |
頁面完全加載時間 | 頁面完全加載時間 | loadEventSart - fetchStart | - |
onLoad | onLoad事件耗時 | LoadEventEnd - loadEventStart | - |
瀏覽器加載和渲染網頁的整個過程
這張圖展示了瀏覽器加載和渲染網頁的整個過程,從請求HTML文件開始,到最終頁面渲染完成。
大致上圖流程為是一個階段,下面是對上圖中十一個階段的解讀:
- 第一階段,開始請求
HTML
文件,瀏覽器接收到用戶輸入的URL
或者點擊鏈接等操作后,開始向服務器發送HTTP
請求,請求獲取HTML
文件。- 第二階段,響應
HTML
文件,服務器接收到請求后,返回HTML
文件給瀏覽器。- 第三階段,開始加載,瀏覽器接收到
HTML
文件后,開始加載過程。- 第四階段,構建
DOM
文檔對象模型,瀏覽器使用HTML
解析器解析HTML
文件構建DOM
樹,DOM
樹是HTML
結構的表示形式,用于描述頁面的層次結構,在構建DOM
樹的過程中,如果遇到<link>
標簽(CSS文件引用)
或<script>
標簽(js文件引入)
瀏覽器會進行預解析,并發起對CSS
文件和JS
文件的請求。- 第五階段,請求
CSS
文件和JS
文件,瀏覽器在預解析過程中發現需要的CSS
和JS
文件后,分別向服務器發送請求,獲取這些資源文件。- 第六階段,返回
CSS
數據和JavaScript
,服務器接收到請求后,返回相應的CSS
文件和JS
文件給瀏覽器。- 第七階段,構建
CSSOM
也就是CSS
對象模型,瀏覽器使用CSS解析器
解析返回的CSS
數據,構建CSSOM
樹,CSSOM
樹包含了所有樣式信息,用于描述頁面元素的央視。- 第八階段,執行
JavaScript
,瀏覽器使用V8
解析器解析并執行返回的JavaScript
代碼,javaScript可以修改DOM
和CSSOM
,因此在這個階段可能會繼續構建DOM
樹。- 第九階段,繼續構建
DOM
,如果JS
代碼中包含了對DOM
的修改操作,瀏覽器會繼續構建和更新DOM
樹。
10.第十階段,構建布局樹,當DOM
樹和CSSOM
樹都構建完成后,瀏覽器會將他們合并成一個布局樹也成為渲染樹,布局樹包含了所需要的渲染的節點及其央視信息- 第十一階段,渲染階段,最后瀏覽器會根據布局樹進行頁面渲染,將頁面內容展示給用戶。
瀏覽器加載和渲染時間計算上報實現
????????在討論瀏覽器加載網頁的過程時候,Performance(性能)
通常指的是web性能,Performance
也是只瀏覽器提供的一個內置對象,window.performance
通過這個API
,開發者可以獲取到詳細的頁面加載時間和資源加載時間等信息,從而進行性能優化,windong.performance
提供了多個屬性和方法來幫助分析網頁性能。
屬性 | 描述 |
---|---|
navigation | 包含了相關的導航信息,比如頁面是如何被加載的,以及設計重定向的次數。 |
timing | 提供了從開始導航到當前頁面完全加載過程中各個關鍵時間的時間點,如重定向時間,DNS 查詢時間,TCP 鏈接時間,請求發送時間,響應接收時間等。 |
打印Performance
如下
第一步、創建/src/monitor/lib/timing.js
文件
import tracker from '../utils/tracker.js';
import onload from '../utils/onload.js';
export default function timing() {onload(function () {setTimeout(() => {const {fetchStart,connectStart,connectEnd,requestStart,responseStart,responseEnd,domLoading,domInteractive,domContentLoadedEventStart,domContentLoadedEventEnd,loadEventStart,} = performance.timing;tracker.send({kind: 'exeprience', // 用戶體驗指標type: 'timing', // 統計每個階段的時間connectTime: connectEnd - connectStart, // 連接時間ttfbTime: responseStart - requestStart, // 首字節到達時間responseTime: responseEnd - responseStart, // 響應時間parseDOMTime: loadEventStart - domLoading, // dom解析時間domContentLoadedTime: domContentLoadedEventEnd - domContentLoadedEventStart, // dom加載完成時間timeToInteractive: domInteractive - fetchStart, // 首次可交互時間loadTime: loadEventStart - fetchStart, // 完整的加載時間});}, 3000)});
}
第二步、模擬DOM加載完成延遲inde.html文件
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>前端監控SDK</title><script src="monitor.js"></script></head><body><div id="container"><div class="content" style="width: 100%; word-wrap: break-word"></div></div><script>// Dom解析完成后,即使依賴的資源沒有加載完成,也會觸發這個事件 document.addEventListener("DOMContentLoaded", function () {let Start = Date.now();while (date.now() - Start < 5000) {}});</script></body>
</html>
第三步、瀏覽器加載和渲染時間上報效果
性能指標采集上報
性能指標描述
字段 | 全稱 | 描述 |
---|---|---|
FP | First Paint(首次繪制) | 包括了任何用戶自定義的背景繪制,它是首先將像素會知道屏幕的時刻 |
FCP | First Content Paint(首次內容繪制) | 是瀏覽器將第一個DOM元素渲染到屏幕的時間,可能是文本、圖像、SVG等,這其實就是白屏的時間 |
FMP | First Meaningfun Paint(首次有意義繪制) | 頁面有意義的內容渲染時間 |
LCP | Largest ContentFul Paint (最大內容渲染) | 代表在viewport中最大的頁面元素加載的時間 |
DCL | DomContentLoaded (DOM加載完成) | 當HTML文檔被完全加載和解析完成之后,DOMContentLoaded時間被觸發,無需等待樣式表,圖像和子框架的完成加載 |
L | onLoad | 當以來的資源全部加載完畢之后才會觸發。 |
TTI | Time to Interactive (可交互時間) | 用于標記應用以進行視覺渲染并能可靠相應用戶輸入的時間點。 |
FID | First Input Delay(首次輸入延遲) | 用戶首次和頁面交互(單機鏈接,點擊按鈕等)到頁面響應交互的時間 |
FMP有意義繪制是根據自己給定判斷,如: h1.setAttribute("elementtiming", "meaningful");
各個性能指標獲取
觸發用例index.html
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>前端監控SDK</title><script src="monitor.js"></script></head><body><div id="container"><p style="color: red">hello</p><div class="content" style="width: 100%; word-wrap: break-word"><button id="CLICKBTN" onclick="clickMe()">Click Me</button></div></div><script>function clickMe() {let start = Date.now();while (Date.now() - start < 1000) {}}setTimeout(() => {let content = document.getElementsByClassName("content")[0];let h1 = document.createElement("h1");h1.innerHTML = "我是這個頁面中最有意義的內容";h1.setAttribute("elementtiming", "meaningful");content.appendChild(h1);}, 2000);</script></body>
</html>
FMP 首次有意義繪制
// 增加一個性能條目的觀察者new PerformanceObserver((entryList, observer) => {let perfEntriens = entryList.getEntries();FMP = perfEntriens[0];observer.disconnect(); // 不需要觀察}).observe({ entryTypes: ['element'] }) // 觀察頁面中有意義的元素
LCP 最大內容渲染
new PerformanceObserver((entryList, observer) => {let perfEntriens = entryList.getEntries();LCP = perfEntriens[0];observer.disconnect(); // 不需要觀察}).observe({ entryTypes: ['largest-contentful-paint'] }) // 觀察頁面中最大的元素
FID 首次輸入延遲
new PerformanceObserver((entryList, observer) => {let lastEvent = getLastEvent();let firstInput = entryList.getEntries()[0]; // 渠道第一個條目console.log("FID", firstInput);if (firstInput) {// 開始處理的時間 - 開始點擊的時間,的差值就是處理的延遲let inputDelay = firstInput.processingStart - firstInput.startTime;let duration = firstInput.duration; // 處理時長if (inputDelay > 0 || duration) {tracker.send({kind: 'exeprience', // 用戶體驗指標type: 'firstInputDeay', // 首次輸入延遲inputDelay, // 輸入延遲duration, // 處理的時間startTime: firstInput.startTime,selector: lastEvent ? getSelector(lastEvent.path || lastEvent.target) : ''});}}observer.disconnect(); // 不需要觀察}).observe({ type: 'first-input', buffered: true }) // 用戶第一次交互,點擊頁面
首次點擊效果:
FP 首次繪制
let FP = performance.getEntriesByName('first-paint')[0]
FCP 首次內容繪制
let FCP = performance.getEntriesByName('first-contentful-paint')[0]
性能指標上報整體代碼
import tracker from '../utils/tracker.js';
import onload from '../utils/onload.js';
import getLastEvent from '../utils/getLastEvent.js';
import getSelector from '../utils/getSelector.js';
export default function timing() {let FMP, LCP;// 增加一個性能條目的觀察者new PerformanceObserver((entryList, observer) => {let perfEntriens = entryList.getEntries();FMP = perfEntriens[0];observer.disconnect(); // 不需要觀察}).observe({ entryTypes: ['element'] }) // 觀察頁面中有意義的元素new PerformanceObserver((entryList, observer) => {let perfEntriens = entryList.getEntries();LCP = perfEntriens[0];observer.disconnect(); // 不需要觀察}).observe({ entryTypes: ['largest-contentful-paint'] }) // 觀察頁面中最大的元素new PerformanceObserver((entryList, observer) => {let lastEvent = getLastEvent();let firstInput = entryList.getEntries()[0]; // 渠道第一個條目console.log("FID", firstInput);if (firstInput) {// 開始處理的時間 - 開始點擊的時間,的差值就是處理的延遲let inputDelay = firstInput.processingStart - firstInput.startTime;let duration = firstInput.duration; // 處理時長if (inputDelay > 0 || duration) {tracker.send({kind: 'exeprience', // 用戶體驗指標type: 'firstInputDeay', // 首次輸入延遲inputDelay, // 輸入延遲duration, // 處理的時間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;// console.log("performance timing:", performance)tracker.send({kind: 'exeprience', // 用戶體驗指標type: 'timing', // 統計每個階段的時間connectTime: connectEnd - connectStart, // 連接時間ttfbTime: responseStart - requestStart, // 首字節到達時間responseTime: responseEnd - responseStart, // 響應時間parseDOMTime: loadEventStart - domLoading, // dom解析時間domContentLoadedTime: domContentLoadedEventEnd - domContentLoadedEventStart, // dom加載完成時間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: 'exeprience', // 用戶體驗指標type: 'paint', // 統計每個階段的時間firstPant: FP.startTime, // 首次繪制firstContentFulPant: FCP.startTime,firstMeaningfulPant: FMP.startTime,largestContentFulPaint: LCP.startTime, // 最大內容繪制});}, 3000)});
}
查看各項性能指標采集上報效果
卡頓監聽上報
監聽方案
當任務阻塞主線程達到 100 ms
或更長時間時,將引發諸多問題,例如,可交互時間延遲、嚴重不穩定的交互行為 (輕擊、單擊、滾動、滾輪等) 延遲、嚴重不穩定的事件回調延遲、紊亂的動畫和滾動等。我們將任何主 UI
線程連續不間斷繁忙 1000
毫秒及以上的時間區間定義為長任務(Long task)
。目前,瀏覽器中存在一個 Long Tasks API
,借助該 API
與PerformanceObserver
的結合使用,我們能夠精準地定位長任務,從而有效實現對卡頓現象的檢測,當然還有心跳檢測,fps幀率檢測卡頓等方法,先從最簡單的實現,借助new PerformanceObserver
的回調方法參數entry.duration
判斷是否大于100ms
,如果大于則可以認為卡頓。
數據結構
{"title":"前端監控","url":"192.168.60.32:8080/", // 卡頓頁面"timestamp":"158654654845", // 時間戳"userAgent":"", // 瀏覽器信息"kind":"experience","type":"longTask","eventType":"mouseover","startTime":"9331","duration":"150","selector":"HTML BODY#container .content",
}
借助Long Tasks API實現卡頓上報
第一步、創建longTask.js文件
import tracker from '../utils/tracker.js';
import getLastEvent from '../utils/getLastEvent.js';
import getSelector from '../utils/getSelector.js';
export default function longTask() {new PerformanceObserver((entryList, observer) => {entryList.getEntries().forEach(entry => {if (entry.duration > 100) {let lastEvent = getLastEvent();var path = lastEvent.path || (function (evt) {var path = [];var el = evt.target;while (el) {path.push(el);el = el.parentElement;}return path;})(lastEvent);console.log("longTask:", getSelector(path))tracker.send({kind: 'experience',type: 'longTask',eventType: lastEvent.type,startTime: entry.startTime, // 開始時間duration: entry.duration, // 持續時間selector: lastEvent ? getSelector(path) : "",})}})}).observe({ entryTypes: ['longtask'] })
}
第二步、循環模仿卡頓效果
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Document</title></head><body><div id="container"><div class="content" style="width: 100%; word-wrap: break-word"><button id="button">點擊卡頓</button></div></div><script>document.getElementById("button").addEventListener("click", function () {// 模擬頁面卡頓for (let i = 0; i < 1000000000; i++) {// do nothing}});</script></body>
</html>
第三步、查看卡頓上報效果
PV、UV、頁面停留時間采集上報
PV(page view) 是頁面瀏覽量。
UV(Unique visitor)用戶訪問量。
PV 只要訪問一次頁面就算一次。
UV同一天內多次訪問只算一次。
對于前端來說,只要每次進入頁面上報一次 PV 就行,UV 的統計放在服務端來做,主要是分析上報的數據來統計得出 UV。
實現pv和頁面停留時間監聽
第一步、創建pv.js
import tracker from '../utils/tracker.js';
// utils.js 或者直接放在你的文件中
function getPageURL() {return window.location.href;
}function getUUID() {return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {var r = Math.random() * 16 | 0,v = c == 'x' ? r : (r & 0x3 | 0x8);return v.toString(16);});
}export default function pv() {tracker.send({kind: "business",type: "pv",startTime: performance.now(),pageURL: getPageURL(),referrer: document.referrer,uuid: getUUID(),});let startTime = Date.now();window.addEventListener("beforeunload",() => {let stayTime = Date.now() - startTime;tracker.send({kind: "business",type: "stayTime",stayTime,pageURL: getPageURL(),uuid: getUUID(),});},false);
}
第二步、查看上報結果
完結~
覺得內容有用?別忘了點個贊+收藏+關注,三連走一波!你的支持是我持續輸出的動力 💪