<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          從0到1搭建前端監(jiān)控平臺,面試必備的亮點項目總結(jié)

          共 32875字,需瀏覽 66分鐘

           ·

          2022-12-22 11:48

          大廠技術(shù)  高級前端  Node進階

          點擊上方 程序員成長指北,關(guān)注公眾號

          回復1,加入高級Node交流群

          原文鏈接: https://juejin.cn/post/7172072612430872584

          作者:海闊_天空

          前言

          常常會苦惱,平常做的項目很普通,沒啥亮點;面試中也經(jīng)常會被問到:做過哪些亮點項目嗎?

          前端監(jiān)控就是一個很有亮點的項目,各個大廠都有自己的內(nèi)部實現(xiàn),沒有監(jiān)控的項目好比是在裸奔

          文章分成以下六部分來介紹:

          • 自研監(jiān)控平臺解決了哪些痛點,實現(xiàn)了什么亮點功能?

          • 相比sentry等監(jiān)控方案,自研監(jiān)控的優(yōu)勢有哪些?

          • 前端監(jiān)控的設計方案、監(jiān)控的目的

          • 數(shù)據(jù)的采集方式:錯誤信息、性能數(shù)據(jù)、用戶行為、加載資源、個性化指標等

          • 設計開發(fā)一個完整的監(jiān)控SDK

          • 監(jiān)控后臺錯誤還原演示示例

          痛點

          某?天用戶:xx商品無法下單!
          ??天運營:xx廣告在手機端打開不了!

          大家反饋的bug,怎么都復現(xiàn)不出來,尷尬的要死!??

          如何記錄項目的錯誤,并將錯誤還原出來,這是監(jiān)控平臺要解決的痛點之一

          錯誤還原

          web-see[1] 監(jiān)控提供三種錯誤還原方式:定位源碼、播放錄屏、記錄用戶行為

          定位源碼

          項目出錯,要是能定位到源碼就好了,可線上的項目都是打包后的代碼,也不能把 .map 文件放到線上

          監(jiān)控平臺通過 source-map[2] 可以實現(xiàn)該功能

          最終效果:

          errorCode.jpg

          播放錄屏

          多數(shù)場景下,定位到具體的源碼,就可以定位bug,但如果是用戶做了異常操作,或者是在某些復雜操作下才出現(xiàn)的bug,僅僅通過定位源碼,還是不能還原錯誤

          要是能把用戶的操作都錄制下來,然后通過回放來還原錯誤就好了

          監(jiān)控平臺通過 rrweb[3] 可以實現(xiàn)該功能

          最終效果: 

          回放的錄屏中,記錄了用戶的所有操作,紅色的線代表了鼠標的移動軌跡

          前端錄屏確實是件很酷的事情,但是不能走極端,如果把用戶的所有操作都錄制下來,是沒有意義的

          我們更關(guān)注的是,頁面報錯的時候用戶做了哪些操作,所以監(jiān)控平臺只把報錯前10s的視頻保存下來(單次錄屏時長也可以自定義)

          記錄用戶行為

          通過 定位源碼 + 播放錄屏 這套組合,還原錯誤應該夠用了,同時監(jiān)控平臺也提供了 記錄用戶行為 這種方式

          假如用戶做了很多操作,操作的間隔超過了單次錄屏時長,錄制的視頻可能是不完整的,此時可以借助用戶行為來分析用戶的操作,幫助復現(xiàn)bug

          最終效果:

          用戶行為列表記錄了:鼠標點擊、接口調(diào)用、資源加載、頁面路由變化、代碼報錯等信息

          通過 定位源碼、播放錄屏、記錄用戶行為 這三板斧,解決了復現(xiàn)bug的痛點

          自研監(jiān)控的優(yōu)勢

          為什么不直接用sentry私有化部署,而選擇自研前端監(jiān)控?

          這是優(yōu)先要思考的問題,sentry作為前端監(jiān)控的行業(yè)標桿,有很多可以借鑒的地方

          相比sentry,自研監(jiān)控平臺的優(yōu)勢在于:

          1、可以將公司的SDK統(tǒng)一成一個,包括但不限于:監(jiān)控SDK、埋點SDK、錄屏SDK、廣告SDK等

          2、提供了更多的錯誤還原方式,同時錯誤信息可以和埋點信息聯(lián)動,便可拿到更細致的用戶行為棧,更快的排查線上錯誤

          3、監(jiān)控自定義的個性化指標:如 long task、memory頁面內(nèi)存、首屏加載時間等。過多的長任務會造成頁面丟幀、卡頓;過大的內(nèi)存可能會造成低端機器的卡死、崩潰

          4、統(tǒng)計資源緩存率,來判斷項目的緩存策略是否合理,提升緩存率可以減少服務器壓力,也可以提升頁面的打開速度

          5、提供了 采樣對比+ 輪詢修正機制 的白屏檢測方案,用于檢測頁面是否一直處于白屏狀態(tài),讓開發(fā)者知道頁面什么時候白了,具體實現(xiàn)見 前端白屏的檢測方案,解決你的線上之憂[4]

          設計思路

          一個完整的前端監(jiān)控平臺包括三個部分:數(shù)據(jù)采集與上報、數(shù)據(jù)分析和存儲、數(shù)據(jù)展示

          system.png

          監(jiān)控目的

          title.png

          異常分析

          按照 5W1H 法則來分析前端異常,需要知道以下信息

          1. What,發(fā)?了什么錯誤:JS錯誤、異步錯誤、資源加載、接口錯誤等
          2. When,出現(xiàn)的時間段,如時間戳
          3. Who,影響了多少用戶,包括報錯事件數(shù)、IP
          4. Where,出現(xiàn)的頁面是哪些,包括頁面、對應的設備信息
          5. Why,錯誤的原因是為什么,包括錯誤堆棧、?列、SourceMap、異常錄屏
          6. How,如何定位還原問題,如何異常報警,避免類似的錯誤發(fā)生

          錯誤數(shù)據(jù)采集

          錯誤信息是最基礎也是最重要的數(shù)據(jù),錯誤信息主要分為下面幾類:

          • JS 代碼運行錯誤、語法錯誤等
          • 異步錯誤等
          • 靜態(tài)資源加載錯誤
          • 接口請求報錯

          錯誤捕獲方式

          1)try/catch

          只能捕獲代碼常規(guī)的運行錯誤,語法錯誤和異步錯誤不能捕獲到

          示例:

          // 示例1:常規(guī)運行時錯誤,可以捕獲 ?
           try {
             let a = undefined;
             if (a.length) {
               console.log('111');
             }
           } catch (e) {
             console.log('捕獲到異常:', e);
          }

          // 示例2:語法錯誤,不能捕獲 ?  
          try {
            const notdefined,
          catch(e) {
            console.log('捕獲不到異常:''Uncaught SyntaxError');
          }
            
          // 示例3:異步錯誤,不能捕獲 ?
          try {
            setTimeout(() => {
              console.log(notdefined);
            }, 0)
          catch(e) {
            console.log('捕獲不到異常:''Uncaught ReferenceError');
          }

          2) window.onerror

          window.onerror 可以捕獲常規(guī)錯誤、異步錯誤,但不能捕獲資源錯誤

          /**
          @param { string } message 錯誤信息
          @param { string } source 發(fā)生錯誤的腳本URL
          @param { number } lineno 發(fā)生錯誤的行號
          @param { number } colno 發(fā)生錯誤的列號
          @param { object } error Error對象
          */

          window.onerror = function(message, source, lineno, colno, error{
             console.log('捕獲到的錯誤信息是:', message, source, lineno, colno, error )
          }

          示例:

          window.onerror = function(message, source, lineno, colno, error) {
          console.log("捕獲到的錯誤信息是:", message, source, lineno, colno, error);
          };

          // 示例1:常規(guī)運行時錯誤,可以捕獲 ?
          console.log(notdefined);

          // 示例2:語法錯誤,不能捕獲 ?
          const notdefined;

          // 示例3:異步錯誤,可以捕獲 ?
          setTimeout(() => {
          console.log(notdefined);
          }, 0);

          // 示例4:資源錯誤,不能捕獲 ?
          let script = document.createElement("script");
          script.type = "text/javascript";
          script.src = "https://www.test.com/index.js";
          document.body.appendChild(script);

          3) window.addEventListener

          當靜態(tài)資源加載失敗時,會觸發(fā) error 事件, 此時 window.onerror 不能捕獲到

          示例:

          <!DOCTYPE html>
          <html lang="en">
          <head>
            <meta charset="UTF-8">
          </head>
          <script>
            window.addEventListener('error', (error) => {
              console.log('捕獲到異常:', error);
            }, true)
          </script>

          <!-- 圖片、script、css加載錯誤,都能被捕獲 ? -->
          <img src="https://test.cn/×××.png">
          <script src="https://test.cn/×××.js"></script>
          <link href="https://test.cn/×××.css" rel="stylesheet" />

          <script>
            // new Image錯誤,不能捕獲 ?
            // new Image運用的比較少,可以自己單獨處理
            new Image().src = 'https://test.cn/×××.png'
          </script>
          </html>

          4)Promise錯誤

          Promise中拋出的錯誤,無法被 window.onerror、try/catch、 error 事件捕獲到,可通過 unhandledrejection 事件來處理

          示例:

          try {
            new Promise((resolve, reject) => {
              JSON.parse("");
              resolve();
            });
          catch (err) {
            // try/catch 不能捕獲Promise中錯誤 ?
            console.error("in try catch", err);
          }

          // error事件 不能捕獲Promise中錯誤 ?
          window.addEventListener(
            "error",
            error => {
              console.log("捕獲到異常:", error);
            },
            true
          );

          // window.onerror 不能捕獲Promise中錯誤 ?
          window.onerror = function(message, source, lineno, colno, error{
            console.log("捕獲到異常:", { message, source, lineno, colno, error });
          };

          // unhandledrejection 可以捕獲Promise中的錯誤 ?
          window.addEventListener("unhandledrejection"function(e{
            console.log("捕獲到異常", e);
            // preventDefault阻止傳播,不會在控制臺打印
            e.preventDefault();
          });

          Vue 錯誤

          Vue項目中,window.onerror 和 error 事件不能捕獲到常規(guī)的代碼錯誤

          異常代碼:

          export default {
            created() {
              let a = null;
              if(a.length > 1) {
                  // ...
              }
            }
          };

          main.js中添加捕獲代碼:

          window.addEventListener('error', (error) => {
            console.log('error', error);
          });
          window.onerror = function (msg, url, line, col, error{
            console.log('onerror', msg, url, line, col, error);
          };

          控制臺會報錯,但是 window.onerror 和 error 不能捕獲到

          error.jpg

          vue 通過 Vue.config.errorHander 來捕獲異常:

          Vue.config.errorHandler = (err, vm, info) => {
              console.log('進來啦~', err);
          }

          控制臺打印:

          error2.jpg

          errorHandler源碼分析

          src/core/util目錄下,有一個error.js文件

          function globalHandleError (err, vm, info{
            // 獲取全局配置,判斷是否設置處理函數(shù),默認undefined
            // 配置config.errorHandler方法
            if (config.errorHandler) {
              try {
                // 執(zhí)行 errorHandler
                return config.errorHandler.call(null, err, vm, info)
              } catch (e) {
                // 如果開發(fā)者在errorHandler函數(shù)中,手動拋出同樣錯誤信息throw err,判斷err信息是否相等,避免log兩次
                if (e !== err) {
                  logError(e, null'config.errorHandler')
                }
              }
            }
            // 沒有配置,常規(guī)輸出
            logError(err, vm, info)
          }

          function logError (err, vm, info{
            if (process.env.NODE_ENV !== 'production') {
              warn(`Error in ${info}: "${err.toString()}"`, vm)
            }
            /* istanbul ignore else */
            if ((inBrowser || inWeex) && typeof console !== 'undefined') {
              console.error(err)
            } else {
              throw err
            }
          }

          通過源碼明白了,vue 使用 try/catch 來捕獲常規(guī)代碼的報錯,被捕獲的錯誤會通過 console.error 輸出而避免應用崩潰

          可以在 Vue.config.errorHandler 中將捕獲的錯誤上報

          Vue.config.errorHandler = function (err, vm, info) { 
          // handleError方法用來處理錯誤并上報
          handleError(err);
          }

          React 錯誤

          從 react16 開始,官方提供了 ErrorBoundary 錯誤邊界的功能,被該組件包裹的子組件,render 函數(shù)報錯時會觸發(fā)離當前組件最近父組件的ErrorBoundary

          生產(chǎn)環(huán)境,一旦被 ErrorBoundary 捕獲的錯誤,也不會觸發(fā)全局的 window.onerror 和 error 事件

          父組件代碼:

          import React from 'react';
          import Child from './Child.js';

          // window.onerror 不能捕獲render函數(shù)的錯誤 ?
          window.onerror = function (err, msg, c, l{
            console.log('err', err, msg);
          };

          // error 不能render函數(shù)的錯誤 ?
          window.addEventListener( 'error', (error) => {
              console.log('捕獲到異常:', error);
            },true
          );

          class ErrorBoundary extends React.Component {
            constructor(props) {
              super(props);
              this.state = { hasErrorfalse };
            }

            static getDerivedStateFromError(error) {
              // 更新 state 使下一次渲染能夠顯示降級后的 UI
              return { hasErrortrue };
            }
            componentDidCatch(error, errorInfo) {
              // componentDidCatch 可以捕獲render函數(shù)的錯誤 
              console.log(error, errorInfo)
              
              // 同樣可以將錯誤日志上報給服務器
              reportError(error, errorInfo);
            }
            render() {
              if (this.state.hasError) {
                // 自定義降級后的 UI 并渲染
                return <h1>Something went wrong.</h1>;
              }
              return this.props.children;
            }
          }

          function Parent({
            return (
              <div>
                父組件
                <ErrorBoundary>
                  <Child />
                </ErrorBoundary>
              </div>

            );
          }

          export default Parent;

          子組件代碼:

          // 子組件 渲染出錯
          function Child({
            let list = {};
            return (
              <div>
                子組件
                {list.map((item, key) => (
                  <span key={key}>{item}</span>
                ))}
              </div>

            );
          }
          export default Child;

          同vue項目的處理類似,react項目中,可以在 componentDidCatch 中將捕獲的錯誤上報

          componentDidCatch(error, errorInfo) {
          // handleError方法用來處理錯誤并上報
          handleError(err);
          }

          跨域問題

          如果當前頁面中,引入了其他域名的JS資源,如果資源出現(xiàn)錯誤,error 事件只會監(jiān)測到一個 script error 的異常。

          示例:

          window.addEventListener("error", error => { 
            console.log("捕獲到異常:", error);
          }, true );

          // 當前頁面加載其他域的資源,如https://www.test.com/index.js
          <script src="https://www.test.com/index.js"></script>

          // 加載的https://www.test.com/index.js的代碼
          function fn({
            JSON.parse("");
          }
          fn();

          報錯信息:

          scriptError.jpg

          只能捕獲到 script error 的原因:

          是由于瀏覽器基于安全考慮,故意隱藏了其它域JS文件拋出的具體錯誤信息,這樣可以有效避免敏感信息無意中被第三方(不受控制的)腳本捕獲到,因此,瀏覽器只允許同域下的腳本捕獲具體的錯誤信息

          解決方法:

          前端script加crossorigin,后端配置 Access-Control-Allow-Origin

          <script src="https://www.test.com/index.js" crossorigin></script>

          添加 crossorigin 后可以捕獲到完整的報錯信息:

          ScriptError1.jpg

          如果不能修改服務端的請求頭,可以考慮通過使用 try/catch 繞過,將錯誤拋出

          <!doctype html>
          <html>
          <body>
            <script src="https://www.test.com/index.js"></script>
            <script>
            window.addEventListener("error", error => { 
              console.log("捕獲到異常:", error);
            }, true );
            
            try {
              // 調(diào)用https://www.test.com/index.js中定義的fn方法
              fn(); 
            } catch (e) {
              throw e;
            }
            
          </script>
          </body>
          </html>

          接口錯誤

          接口監(jiān)控的實現(xiàn)原理:針對瀏覽器內(nèi)置的 XMLHttpRequest、fetch 對象,利用 AOP 切片編程重寫該方法,實現(xiàn)對請求的接口攔截,從而獲取接口報錯的情況并上報

          1)攔截XMLHttpRequest請求示例:

          function xhrReplace({
            if (!("XMLHttpRequest" in window)) {
              return;
            }
            const originalXhrProto = XMLHttpRequest.prototype;
            // 重寫XMLHttpRequest 原型上的open方法
            replaceAop(originalXhrProto, "open", originalOpen => {
              return function(...args{
                // 獲取請求的信息
                this._xhr = {
                  methodtypeof args[0] === "string" ? args[0].toUpperCase() : args[0],
                  url: args[1],
                  startTimenew Date().getTime(),
                  type"xhr"
                };
                // 執(zhí)行原始的open方法
                originalOpen.apply(this, args);
              };
            });
            // 重寫XMLHttpRequest 原型上的send方法
            replaceAop(originalXhrProto, "send", originalSend => {
              return function(...args{
                // 當請求結(jié)束時觸發(fā),無論請求成功還是失敗都會觸發(fā)
                this.addEventListener("loadend", () => {
                  const { responseType, response, status } = this;
                  const endTime = new Date().getTime();
                  this._xhr.reqData = args[0];
                  this._xhr.status = status;
                  if (["""json""text"].indexOf(responseType) !== -1) {
                    this._xhr.responseText =
                      typeof response === "object" ? JSON.stringify(response) : response;
                  }
                  // 獲取接口的請求時長
                  this._xhr.elapsedTime = endTime - this._xhr.startTime;

                  // 上報xhr接口數(shù)據(jù)
                  reportData(this._xhr);
                });
                // 執(zhí)行原始的send方法
                originalSend.apply(this, args);
              };
            });
          }

          /**
           * 重寫指定的方法
           * @param { object } source 重寫的對象
           * @param { string } name 重寫的屬性
           * @param { function } fn 攔截的函數(shù)
           */

          function replaceAop(source, name, fn{
            if (source === undefinedreturn;
            if (name in source) {
              var original = source[name];
              var wrapped = fn(original);
              if (typeof wrapped === "function") {
                source[name] = wrapped;
              }
            }
          }

          2)攔截fetch請求示例:

          function fetchReplace() {
          if (!("fetch" in window)) {
          return;
          }
          // 重寫fetch方法
          replaceAop(window, "fetch", originalFetch => {
          return function(url, config) {
          const sTime = new Date().getTime();
          const method = (config && config.method) || "GET";
          let handlerData = {
          type: "fetch",
          method,
          reqData: config && config.body,
          url
          };

          return originalFetch.apply(window, [url, config]).then(
          res => {
          // res.clone克隆,防止被標記已消費
          const tempRes = res.clone();
          const eTime = new Date().getTime();
          handlerData = {
          ...handlerData,
          elapsedTime: eTime - sTime,
          status: tempRes.status
          };
          tempRes.text().then(data => {
          handlerData.responseText = data;
          // 上報fetch接口數(shù)據(jù)
          reportData(handlerData);
          });

          // 返回原始的結(jié)果,外部繼續(xù)使用then接收
          return res;
          },
          err => {
          const eTime = new Date().getTime();
          handlerData = {
          ...handlerData,
          elapsedTime: eTime - sTime,
          status: 0
          };
          // 上報fetch接口數(shù)據(jù)
          reportData(handlerData);
          throw err;
          }
          );
          };
          });
          }

          性能數(shù)據(jù)采集

          談到性能數(shù)據(jù)采集,就會提及加載過程模型圖:

          timing.png

          以Spa頁面來說,頁面的加載過程大致是這樣的:

          spa.png

          包括dns查詢、建立tcp連接、發(fā)送http請求、返回html文檔、html文檔解析等階段

          最初,可以通過 window.performance.timing 來獲取加載過程模型中各個階段的耗時數(shù)據(jù)

          // window.performance.timing 各字段說明
          {
          navigationStart, // 同一個瀏覽器上下文中,上一個文檔結(jié)束時的時間戳。如果沒有上一個文檔,這個值會和 fetchStart 相同。
          unloadEventStart, // 上一個文檔 unload 事件觸發(fā)時的時間戳。如果沒有上一個文檔,為 0。
          unloadEventEnd, // 上一個文檔 unload 事件結(jié)束時的時間戳。如果沒有上一個文檔,為 0。
          redirectStart, // 表示第一個 http 重定向開始時的時間戳。如果沒有重定向或者有一個非同源的重定向,為 0。
          redirectEnd, // 表示最后一個 http 重定向結(jié)束時的時間戳。如果沒有重定向或者有一個非同源的重定向,為 0。
          fetchStart, // 表示瀏覽器準備好使用 http 請求來獲取文檔的時間戳。這個時間點會在檢查任何緩存之前。
          domainLookupStart, // 域名查詢開始的時間戳。如果使用了持久連接或者本地有緩存,這個值會和 fetchStart 相同。
          domainLookupEnd, // 域名查詢結(jié)束的時間戳。如果使用了持久連接或者本地有緩存,這個值會和 fetchStart 相同。
          connectStart, // http 請求向服務器發(fā)送連接請求時的時間戳。如果使用了持久連接,這個值會和 fetchStart 相同。
          connectEnd, // 瀏覽器和服務器之前建立連接的時間戳,所有握手和認證過程全部結(jié)束。如果使用了持久連接,這個值會和 fetchStart 相同。
          secureConnectionStart, // 瀏覽器與服務器開始安全鏈接的握手時的時間戳。如果當前網(wǎng)頁不要求安全連接,返回 0。
          requestStart, // 瀏覽器向服務器發(fā)起 http 請求(或者讀取本地緩存)時的時間戳,即獲取 html 文檔。
          responseStart, // 瀏覽器從服務器接收到第一個字節(jié)時的時間戳。
          responseEnd, // 瀏覽器從服務器接受到最后一個字節(jié)時的時間戳。
          domLoading, // dom 結(jié)構(gòu)開始解析的時間戳,document.readyState 的值為 loading。
          domInteractive, // dom 結(jié)構(gòu)解析結(jié)束,開始加載內(nèi)嵌資源的時間戳,document.readyState 的狀態(tài)為 interactive。
          domContentLoadedEventStart, // DOMContentLoaded 事件觸發(fā)時的時間戳,所有需要執(zhí)行的腳本執(zhí)行完畢。
          domContentLoadedEventEnd, // DOMContentLoaded 事件結(jié)束時的時間戳
          domComplete, // dom 文檔完成解析的時間戳, document.readyState 的值為 complete。
          loadEventStart, // load 事件觸發(fā)的時間。
          loadEventEnd // load 時間結(jié)束時的時間。
          }

          后來 window.performance.timing 被廢棄,通過 PerformanceObserver[5] 來獲取。舊的 api,返回的是一個 UNIX 類型的絕對時間,和用戶的系統(tǒng)時間相關(guān),分析的時候需要再次計算。而新的 api,返回的是一個相對時間,可以直接用來分析

          現(xiàn)在 chrome 開發(fā)團隊提供了 web-vitals[6] 庫,方便來計算各性能數(shù)據(jù)(注意:web-vitals 不支持safari瀏覽器)

          關(guān)于 FP、FCP、LCP、CLS、TTFB、FID 等性能指標的含義和計算方式,我在 「歷時8個月」10萬字前端知識體系總結(jié)(工程化篇)??[7] 中有詳細的講解,這里不再贅述

          用戶行為數(shù)據(jù)采集

          用戶行為包括:頁面路由變化、鼠標點擊、資源加載、接口調(diào)用、代碼報錯等行為

          設計思路

          1、通過Breadcrumb類來創(chuàng)建用戶行為的對象,來存儲和管理所有的用戶行為

          2、通過重寫或添加相應的事件,完成用戶行為數(shù)據(jù)的采集

          用戶行為代碼示例:

          // 創(chuàng)建用戶行為類
          class Breadcrumb {
            // maxBreadcrumbs控制上報用戶行為的最大條數(shù)
            maxBreadcrumbs = 20;
            // stack 存儲用戶行為
            stack = [];
            constructor() {}
            // 添加用戶行為棧
            push(data) {
              if (this.stack.length >= this.maxBreadcrumbs) {
                // 超出則刪除第一條
                this.stack.shift();
              }
              this.stack.push(data);
              // 按照時間排序
              this.stack.sort((a, b) => a.time - b.time);
            }
          }

          let breadcrumb = new Breadcrumb();

          // 添加一條頁面跳轉(zhuǎn)的行為,從home頁面跳轉(zhuǎn)到about頁面
          breadcrumb.push({
            type: "Route",
            form: '/home',
            to: '/about'
            url: "http://localhost:3000/index.html",
            time: "1668759320435"
          });

          // 添加一條用戶點擊行為
          breadcrumb.push({
            type: "Click",
            dom: "<button id='btn'>按鈕</button>",
            time: "1668759620485"
          });

          // 添加一條調(diào)用接口行為
          breadcrumb.push({
            type: "Xhr",
            url: "http://10.105.10.12/monitor/open/pushData",
            time: "1668760485550"
          });

          // 上報用戶行為
          reportData({
            uuid: "a6481683-6d2e-4bd8-bba1-64819d8cce8c",
            stack: breadcrumb.getStack()
          });

          頁面跳轉(zhuǎn)

          通過監(jiān)聽路由的變化來判斷頁面跳轉(zhuǎn),路由有history、hash兩種模式,history模式可以監(jiān)聽popstate事件,hash模式通過重寫 pushState和 replaceState事件

          vue項目中不能通過 hashchange 事件來監(jiān)聽路由變化,vue-router 底層調(diào)用的是 history.pushStatehistory.replaceState,不會觸發(fā) hashchange

          vue-router源碼:

          function pushState (url, replace) {
          saveScrollPosition();
          var history = window.history;
          try {
          if (replace) {
          history.replaceState({ key: _key }, '', url);
          } else {
          _key = genKey();
          history.pushState({ key: _key }, '', url);
          }
          } catch (e) {
          window.location[replace ? 'replace' : 'assign'](url);
          }
          }
          ...

          // this.$router.push時觸發(fā)
          function pushHash (path) {
          if (supportsPushState) {
          pushState(getUrl(path));
          } else {
          window.location.hash = path;
          }
          }

          通過重寫 pushState、replaceState 事件來監(jiān)聽路由變化

          // lastHref 前一個頁面的路由
          let lastHref = document.location.href;
          function historyReplace({
            function historyReplaceFn(originalHistoryFn{
              return function(...args{
                const url = args.length > 2 ? args[2] : undefined;
                if (url) {
                  const from = lastHref;
                  const to = String(url);
                  lastHref = to;
                  // 上報路由變化
                  reportData("routeChange", {
                    from,
                    to
                  });
                }
                return originalHistoryFn.apply(this, args);
              };
            }
            // 重寫pushState事件
            replaceAop(window.history, "pushState", historyReplaceFn);
            // 重寫replaceState事件
            replaceAop(window.history, "replaceState", historyReplaceFn);
          }

          function replaceAop(source, name, fn{
            if (source === undefinedreturn;
            if (name in source) {
              var original = source[name];
              var wrapped = fn(original);
              if (typeof wrapped === "function") {
                source[name] = wrapped;
              }
            }
          }

          用戶點擊

          給 document 對象添加click事件,并上報

          function domReplace({
            document.addEventListener("click",({ target }) => {
                const tagName = target.tagName.toLowerCase();
                if (tagName === "body") {
                  return null;
                }
                let classNames = target.classList.value;
                classNames = classNames !== "" ? ` class="${classNames}"` : "";
                const id = target.id ? ` id="${target.id}"` : "";
                const innerText = target.innerText;
                // 獲取包含id、class、innerTextde字符串的標簽
                let dom = `<${tagName}${id}${
                  classNames !== "" ? classNames : ""
                }
          >${innerText}</${tagName}>`
          ;
                // 上報
                reportData({
                  type'Click',
                  dom
                });
              },
              true
            );
          }

          資源加載

          獲取頁面中加載的資源信息,比如它們的 url 是什么、加載了多久、是否來自緩存等,最終生成 資源加載瀑布圖[8]

          waterfall .png

          瀑布圖展現(xiàn)了瀏覽器為渲染網(wǎng)頁而加載的所有的資源,包括加載的順序和每個資源的加載時間

          分析這些資源是如何加載的, 可以幫助我們了解究竟是什么原因拖慢了網(wǎng)頁,從而采取對應的措施來提升網(wǎng)頁速度

          可以通過 performance.getEntriesByType('resource')[9] 獲取頁面加載的資源列表,同時可以結(jié)合 initiatorType 字段來判斷資源類型,對資源進行過濾

          其中 PerformanceResourceTiming[10] 來分析資源加載的詳細數(shù)據(jù)

          // PerformanceResourceTiming 各字段說明
          {
          connectEnd, // 表示瀏覽器完成建立與服務器的連接以檢索資源之后的時間
          connectStart, // 表示瀏覽器開始建立與服務器的連接以檢索資源之前的時間
          decodedBodySize, // 表示在刪除任何應用的內(nèi)容編碼之后,從*消息主體*的請求(HTTP 或緩存)中接收到的大?。ㄒ园宋蛔止?jié)為單位)
          domainLookupEnd, // 表示瀏覽器完成資源的域名查找之后的時間
          domainLookupStart, // 表示在瀏覽器立即開始資源的域名查找之前的時間
          duration, // 返回一個timestamp,即 responseEnd 和 startTime 屬性的差值
          encodedBodySize, // 表示在刪除任何應用的內(nèi)容編碼之前,從*有效內(nèi)容主體*的請求(HTTP 或緩存)中接收到的大?。ㄒ园宋蛔止?jié)為單位)
          entryType, // 返回 "resource"
          fetchStart, // 表示瀏覽器即將開始獲取資源之前的時間
          initiatorType, // 代表啟動性能條目的資源的類型,如 PerformanceResourceTiming.initiatorType 中所指定
          name, // 返回資源 URL
          nextHopProtocol, // 代表用于獲取資源的網(wǎng)絡協(xié)議
          redirectEnd, // 表示收到上一次重定向響應的發(fā)送最后一個字節(jié)時的時間
          redirectStart, // 表示上一次重定向開始的時間
          requestStart, // 表示瀏覽器開始向服務器請求資源之前的時間
          responseEnd, // 表示在瀏覽器接收到資源的最后一個字節(jié)之后或在傳輸連接關(guān)閉之前(以先到者為準)的時間
          responseStart, // 表示瀏覽器從服務器接收到響應的第一個字節(jié)后的時間
          secureConnectionStart, // 表示瀏覽器即將開始握手過程以保護當前連接之前的時間
          serverTiming, // 一個 PerformanceServerTiming 數(shù)組,包含服務器計時指標的PerformanceServerTiming 條目
          startTime, // 表示資源獲取開始的時間。該值等效于 PerformanceEntry.fetchStart
          transferSize, // 代表所獲取資源的大?。ㄒ园宋蛔止?jié)為單位)。該大小包括響應標頭字段以及響應有效內(nèi)容主體
          workerStart // 如果服務 Worker 線程已經(jīng)在運行,則返回在分派 FetchEvent 之前的時間戳,如果尚未運行,則返回在啟動 Service Worker 線程之前的時間戳。如果服務 Worker 未攔截該資源,則該屬性將始終返回 0。
          }

          獲取資源加載時長為 duration 字段,即 responseEnd 與 startTime 的差值

          獲取加載資源列表:

          function getResource({
            if (performance.getEntriesByType) {
              const entries = performance.getEntriesByType('resource');
              // 過濾掉非靜態(tài)資源的 fetch、 xmlhttprequest、beacon
              let list = entries.filter((entry) => {
                return ['fetch''xmlhttprequest''beacon'].indexOf(entry.initiatorType) === -1;
              });

              if (list.length) {
                list = JSON.parse(JSON.stringify(list));
                list.forEach((entry) => {
                  entry.isCache = isCache(entry);
                });
              }
              return list;
            }
          }

          // 判斷資料是否來自緩存
          // transferSize為0,說明是從緩存中直接讀取的(強制緩存)
          // transferSize不為0,但是`encodedBodySize` 字段為 0,說明它走的是協(xié)商緩存(`encodedBodySize 表示請求響應數(shù)據(jù) body 的大小`)
          function isCache(entry{
            return entry.transferSize === 0 || (entry.transferSize !== 0 && entry.encodedBodySize === 0);
          }

          一個真實的頁面中,資源加載大多數(shù)是逐步進行的,有些資源本身就做了延遲加載,有些是需要用戶發(fā)生交互后才會去請求一些資源

          如果我們只關(guān)注首頁資源,可以在 window.onload 事件中去收集

          如果要收集所有的資源,需要通過定時器反復地去收集,并且在一輪收集結(jié)束后,通過調(diào)用 clearResourceTimings[11] 將 performance entries 里的信息清空,避免在下一輪收集時取到重復的資源

          個性化指標

          long task

          執(zhí)行時間超過50ms的任務,被稱為 long task[12] 長任務

          獲取頁面的長任務列表:

          const entryHandler = list => {
          for (const long of list.getEntries()) {
          // 獲取長任務詳情
          console.log(long);
          }
          };

          let observer = new PerformanceObserver(entryHandler);
          observer.observe({ entryTypes: ["longtask"] });

          memory頁面內(nèi)存

          performance.memory 可以顯示此刻內(nèi)存占用情況,它是一個動態(tài)值,其中:

          • jsHeapSizeLimit 該屬性代表的含義是:內(nèi)存大小的限制。

          • totalJSHeapSize 表示總內(nèi)存的大小。

          • usedJSHeapSize 表示可使用的內(nèi)存的大小。

          通常,usedJSHeapSize 不能大于 totalJSHeapSize,如果大于,有可能出現(xiàn)了內(nèi)存泄漏

          // load事件中獲取此時頁面的內(nèi)存大小
          window.addEventListener("load", () => {
            console.log("memory", performance.memory);
          });

          首屏加載時間

          首屏加載時間和首頁加載時間不一樣,首屏指的是屏幕內(nèi)的dom渲染完成的時間

          比如首頁很長需要好幾屏展示,這種情況下屏幕以外的元素不考慮在內(nèi)

          計算首屏加載時間流程

          1)利用MutationObserver監(jiān)聽document對象,每當dom變化時觸發(fā)該事件

          2)判斷監(jiān)聽的dom是否在首屏內(nèi),如果在首屏內(nèi),將該dom放到指定的數(shù)組中,記錄下當前dom變化的時間點

          3)在MutationObserver的callback函數(shù)中,通過防抖函數(shù),監(jiān)聽document.readyState狀態(tài)的變化

          4)當document.readyState === 'complete',停止定時器和 取消對document的監(jiān)聽

          5)遍歷存放dom的數(shù)組,找出最后變化節(jié)點的時間,用該時間點減去performance.timing.navigationStart 得出首屏的加載時間

          監(jiān)控SDK

          監(jiān)控SDK的作用:數(shù)據(jù)采集與上報

          整體架構(gòu)

          sdkProcess.jpg

          整體架構(gòu)使用 發(fā)布-訂閱 設計模式,這樣設計的好處是便于后續(xù)擴展與維護,如果想添加新的hook或事件,在該回調(diào)中添加對應的函數(shù)即可

          SDK 入口

          src/index.js

          對外導出init事件,配置了vue、react項目的不同引入方式

          vue項目在Vue.config.errorHandler中上報錯誤,react項目在ErrorBoundary中上報錯誤

          entry.png

          事件發(fā)布與訂閱

          通過添加監(jiān)聽事件來捕獲錯誤,利用 AOP 切片編程,重寫接口請求、路由監(jiān)聽等功能,從而獲取對應的數(shù)據(jù)

          src/load.js

          replace.png

          用戶行為收集

          core/breadcrumb.js

          創(chuàng)建用戶行為類,stack用來存儲用戶行為,當長度超過限制時,最早的一條數(shù)據(jù)會被覆蓋掉,在上報錯誤時,對應的用戶行為會添加到該錯誤信息中

          bread.png

          數(shù)據(jù)上報方式

          支持圖片打點上報和fetch請求上報兩種方式

          圖片打點上報的優(yōu)勢:
          1)支持跨域,一般而言,上報域名都不是當前域名,上報的接口請求會構(gòu)成跨域
          2)體積小且不需要插入dom中
          3)不需要等待服務器返回數(shù)據(jù)

          圖片打點缺點是:url受瀏覽器長度限制

          core/transportData.js

          send.png

          數(shù)據(jù)上報時機

          優(yōu)先使用 requestIdleCallback,利用瀏覽器空閑時間上報,其次使用微任務上報

          queue.png

          監(jiān)控SDK,參考了 sentry[13]monitor[14]、 mitojs[15]

          項目后臺demo

          主要用來演示錯誤還原功能,方式包括:定位源碼、播放錄屏、記錄用戶行為

          [16]web-see.gif

          后臺demo功能介紹:

          1、使用 express 開啟靜態(tài)服務器,模擬線上環(huán)境,用于實現(xiàn)定位源碼的功能

          2、server.js 中實現(xiàn)了 reportData(錯誤上報)、getmap(獲取 map 文件)、getRecordScreenId(獲取錄屏信息)、 getErrorList(獲取錯誤列表)的接口

          3、用戶可點擊 'js 報錯'、'異步報錯'、'promise 錯誤' 按鈕,上報對應的代碼錯誤,后臺實現(xiàn)錯誤還原功能

          4、點擊 'xhr 請求報錯'、'fetch 請求報錯' 按鈕,上報接口報錯信息

          5、點擊 '加載資源報錯' 按鈕,上報對應的資源報錯信息

          通過這些異步的捕獲,了解監(jiān)控平臺的整體流程

          安裝與使用

          npm官網(wǎng)搜索 web-see[17] https://www.npmjs.com/package/web-see

          install.jpg

          倉庫地址

          監(jiān)控SDK: web-see[18]  https://github.com/xy-sea/web-see

          監(jiān)控后臺: web-see-demo[19]   https://github.com/xy-sea/web-see-demo

          總結(jié)

          目前市面上的前端監(jiān)控方案可謂是百花齊放,但底層原理都是相通的。從基礎的理論知識到實現(xiàn)一個可用的監(jiān)控平臺,收獲還是挺多的

          有興趣的小伙伴可以結(jié)合git倉庫的源碼玩一玩,再結(jié)合本文一起閱讀,幫助加深理解

          后續(xù)

          下一篇會繼續(xù)討論前端監(jiān)控,講解具體如何實現(xiàn):定位源碼、播放錄屏等功能

          感興趣的小伙伴可以點個關(guān)注,后續(xù)好文不斷!

          Node 社群



          我組建了一個氛圍特別好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你對Node.js學習感興趣的話(后續(xù)有計劃也可以),我們可以一起進行Node.js相關(guān)的交流、學習、共建。下方加 考拉 好友回復「Node」即可。


             “分享、點贊、在看” 支持一波??


          參考資料

          [1]

          https://github.com/xy-sea/web-see: https://github.com/xy-sea/web-see

          [2]

          https://github.com/mozilla/source-map: https://github.com/mozilla/source-map

          [3]

          https://github.com/rrweb-io/rrweb: https://github.com/rrweb-io/rrweb

          [4]

          https://juejin.cn/post/7176206226903007292: https://juejin.cn/post/7176206226903007292

          [5]

          https://link.juejin.cn?target=https%3A%2F%2Fdeveloper.mozilla.org%2Fzh-CN%2Fdocs%2FWeb%2FAPI%2FPerformanceObserver: https://developer.mozilla.org/zh-CN/docs/Web/API/PerformanceObserver

          [6]

          https://www.npmjs.com/package/web-vitals: https://www.npmjs.com/package/web-vitals

          [7]

          https://juejin.cn/post/7146976516692410376/#heading-63: https://juejin.cn/post/7146976516692410376/#heading-63

          [8]

          https://blog.csdn.net/csdn_girl/article/details/54911632: https://blog.csdn.net/csdn_girl/article/details/54911632

          [9]

          https://developer.mozilla.org/zh-CN/docs/Web/API/Performance/getEntriesByType: https://developer.mozilla.org/zh-CN/docs/Web/API/Performance/getEntriesByType

          [10]

          https://developer.mozilla.org/zh-CN/docs/Web/API/Performance/getEntriesByType: https://developer.mozilla.org/zh-CN/docs/Web/API/Performance/getEntriesByType

          [11]

          https://link.zhihu.com/?target=https%3A//developer.mozilla.org/zh-CN/docs/Web/API/Performance/clearResourceTimings: https://link.zhihu.com/?target=https%3A//developer.mozilla.org/zh-CN/docs/Web/API/Performance/clearResourceTimings

          [12]

          https://developer.mozilla.org/zh-CN/docs/Web/API/Long_Tasks_API: https://developer.mozilla.org/zh-CN/docs/Web/API/Long_Tasks_API

          [13]

          https://github.com/getsentry/sentry-javascript: https://github.com/getsentry/sentry-javascript

          [14]

          https://github.com/clouDr-f2e/monitor: https://github.com/clouDr-f2e/monitor

          [15]

          https://github.com/mitojs/mitojs: https://github.com/mitojs/mitojs

          [16]

          https://github.com/xy-sea/web-see-demo#%E5%8A%9F%E8%83%BD: https://github.com/xy-sea/web-see-demo#%E5%8A%9F%E8%83%BD

          [17]

          https://www.npmjs.com/package/web-see: https://www.npmjs.com/package/web-see

          [18]

          https://github.com/xy-sea/web-see: https://github.com/xy-sea/web-see

          [19]

          https://github.com/xy-sea/web-see-demo: https://github.com/xy-sea/web-see-demo


          瀏覽 67
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  看免费毛片!!! | 国产v亚洲| 亚洲综合第一页 | 边添小泬边狠狠躁视频 | 一级毛片全部免费播放特黄 |