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

          使用typescript開發(fā)前端錯(cuò)誤及性能監(jiān)控SDK

          共 10626字,需瀏覽 22分鐘

           ·

          2021-05-19 13:39

          122add9412ab5eca0dfdf2bb7bc4d98a.webp

          前端的錯(cuò)誤監(jiān)控、性能數(shù)據(jù)往往對業(yè)務(wù)的穩(wěn)定性有很重要的影響,即使我們在開發(fā)階段十分小心,也難免上線后出現(xiàn)異常,并且線上異常我們往往后知后覺。而頁面的性能數(shù)據(jù)則關(guān)系到用戶體驗(yàn),因此這部分?jǐn)?shù)據(jù)也在我們采集范圍內(nèi)。

          現(xiàn)在第三方完整解決方案國外有sentry,國內(nèi)有fundebug、frontjs,他們提供前端接入的SDK和數(shù)據(jù)服務(wù),然后有一定的免費(fèi)額度,超出就需要使用付費(fèi)方案。前端的SDK用戶監(jiān)控用戶端異常和性能,后端服務(wù)用戶可以創(chuàng)建應(yīng)用,每個(gè)應(yīng)用分配一個(gè)APPKEY,然后SDK完成自動(dòng)上報(bào)。

          本文不考慮數(shù)據(jù)服務(wù),只對前端監(jiān)控進(jìn)行分析,講下web如何進(jìn)行監(jiān)控和采集這些數(shù)據(jù),并且通過TS集成這些功能做出一套前端監(jiān)控SDK。

          既然需要采集數(shù)據(jù),我們要明確下可能需要哪些數(shù)據(jù),目前來看有如下一些數(shù)據(jù):

          • 頁面錯(cuò)誤數(shù)據(jù)
          • 頁面資源加載情況
          • 頁面性能數(shù)據(jù)
          • 接口數(shù)據(jù)
          • 手機(jī)、瀏覽器數(shù)據(jù)
          • 頁面訪問數(shù)據(jù)
          • 用戶行為數(shù)據(jù)
          • ...

          下面分析一下這些數(shù)據(jù)如何獲取:

          頁面錯(cuò)誤數(shù)據(jù)
          • window.onerror AOP捕獲異常能力無論是異步還是非異步錯(cuò)誤,onerror 都能捕獲到運(yùn)行時(shí)錯(cuò)誤。
          • window.onerror不能捕獲頁面資源的加載錯(cuò)誤,但資源加載錯(cuò)誤能被window.addEventListener在捕獲階段捕獲。由于addEventListener也能夠捕獲js錯(cuò)誤,因此需要過濾避免重復(fù)觸發(fā)事件鉤子
          • window.onerror無法捕獲Promise任務(wù)中未被處理的異常,通過unhandledrejection可以捕獲

          頁面資源加載異常

          window.addEventListener(
          ??"error",
          ??function?(event)?{
          ????const?target:?any?=?event.target?||?event.srcElement;
          ????const?isElementTarget?=
          ??????target?instanceof?HTMLScriptElement?||
          ??????target?instanceof?HTMLLinkElement?||
          ??????target?instanceof?HTMLImageElement;
          ????if?(!isElementTarget)?return?false;

          ????const?url?=?target.src?||?target.href;
          ????onResourceError?.call(this,?url);
          ??},
          ??true
          );

          頁面邏輯和未catch的promise異常

          ?const?oldOnError?=?window.onerror;
          ?const?oldUnHandleRejection?=?window.onunhandledrejection;

          ?window.onerror?=?function?(...args)?{
          ???if?(oldOnError)?{
          ?????oldOnError(...args);
          ???}

          ???const?[msg,?url,?line,?column,?error]?=?args;
          ???onError?.call(this,?{
          ?????msg,
          ?????url,
          ?????line,
          ?????column,
          ?????error
          ???});
          ?};

          ?window.onunhandledrejection?=?function?(e:?PromiseRejectionEvent)?{
          ???if?(oldUnHandleRejection)?{
          ?????oldUnHandleRejection.call(window,?e);
          ???}

          ???onUnHandleRejection?&&?onUnHandleRejection(e);
          ?};

          在Vue中,我們應(yīng)該通過Vue.config.errorHandler = function(err, vm, info) {};進(jìn)行異常捕獲,這樣可以獲取到更多的上下文信息。

          對于React,React 16 提供了一個(gè)內(nèi)置函數(shù) componentDidCatch,使用它可以非常簡單的獲取到 react 下的錯(cuò)誤信息

          componentDidCatch(error,?info)?{
          ????console.log(error,?info);
          }
          頁面性能數(shù)據(jù)

          通常我們會關(guān)注以下性能指標(biāo):

          • 白屏?xí)r間:從瀏覽器輸入地址并回車后到頁面開始有內(nèi)容的時(shí)間;
          • 首屏?xí)r間:從瀏覽器輸入地址并回車后到首屏內(nèi)容渲染完畢的時(shí)間;
          • 用戶可操作時(shí)間節(jié)點(diǎn):domready觸發(fā)節(jié)點(diǎn),點(diǎn)擊事件有反應(yīng);
          • 總下載時(shí)間:window.onload的觸發(fā)節(jié)點(diǎn)。

          白屏?xí)r間

          白屏?xí)r間節(jié)點(diǎn)指的是從用戶進(jìn)入網(wǎng)站(輸入url、刷新、跳轉(zhuǎn)等方式)的時(shí)刻開始計(jì)算,一直到頁面有內(nèi)容展示出來的時(shí)間節(jié)點(diǎn)。這個(gè)過程包括dns查詢、建立tcp連接、發(fā)送首個(gè)http請求(如果使用https還要介入TLS的驗(yàn)證時(shí)間)、返回html文檔、html文檔head解析完畢。

          首屏?xí)r間

          首屏?xí)r間的統(tǒng)計(jì)比較復(fù)雜,因?yàn)樯婕皥D片等多種元素及異步渲染等方式。觀察加載視圖可發(fā)現(xiàn),影響首屏的主要因素的「圖片的加載」。通過統(tǒng)計(jì)首屏內(nèi)圖片的加載時(shí)間便可以獲取首屏渲染完成的時(shí)間。

          • 頁面存在 iframe 的情況下也需要判斷加載時(shí)間
          • gif 圖片在 IE 上可能重復(fù)觸發(fā) load 事件需排除
          • 異步渲染的情況下應(yīng)在異步獲取數(shù)據(jù)插入之后再計(jì)算首屏
          • css 重要背景圖片可以通過 JS 請求圖片 url 來統(tǒng)計(jì)(瀏覽器不會重復(fù)加載)
          • 沒有圖片則以統(tǒng)計(jì) JS 執(zhí)行時(shí)間為首屏,即認(rèn)為文字出現(xiàn)時(shí)間

          用戶可操作時(shí)間

          DOM解析完畢時(shí)間,可統(tǒng)計(jì)DomReady時(shí)間,因?yàn)橥ǔ谶@個(gè)時(shí)間點(diǎn)綁定事件

          對于web端獲取性能數(shù)據(jù)方法很簡單,只需要使用瀏覽器自帶的Performance接口

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

          ?

          Performance 接口可以獲取到當(dāng)前頁面中與性能相關(guān)的信息,它是 High Resolution Time API 的一部分,同時(shí)也融合了 Performance Timeline API、Navigation Timing API、 User Timing API 和 Resource Timing API。

          ?
          218250ab76795f6a715000dd053c267a.webpperformance

          從圖中可以看到很多指標(biāo)都是成對出現(xiàn),這里我們直接求差值,就可以求出對應(yīng)頁面加載過程中關(guān)鍵節(jié)點(diǎn)的耗時(shí),這里我們介紹幾個(gè)比較常用的,比如:

          const?timingInfo?=?window.performance.timing;

          //?DNS解析,DNS查詢耗時(shí)
          timingInfo.domainLookupEnd?-?timingInfo.domainLookupStart;

          //?TCP連接耗時(shí)
          timingInfo.connectEnd?-?timingInfo.connectStart;

          //?獲得首字節(jié)耗費(fèi)時(shí)間,也叫TTFB
          timingInfo.responseStart?-?timingInfo.navigationStart;

          //?*:?domReady時(shí)間(與DomContentLoad事件對應(yīng))
          timingInfo.domContentLoadedEventStart?-?timingInfo.navigationStart;

          //?DOM資源下載
          timingInfo.responseEnd?-?timingInfo.responseStart;

          //?準(zhǔn)備新頁面時(shí)間耗時(shí)
          timingInfo.fetchStart?-?timingInfo.navigationStart;

          //?重定向耗時(shí)
          timingInfo.redirectEnd?-?timingInfo.redirectStart;

          //?Appcache?耗時(shí)
          timingInfo.domainLookupStart?-?timingInfo.fetchStart;

          //?unload?前文檔耗時(shí)
          timingInfo.unloadEventEnd?-?timingInfo.unloadEventStart;

          //?request請求耗時(shí)
          timingInfo.responseEnd?-?timingInfo.requestStart;

          //?請求完畢至DOM加載
          timingInfo.domInteractive?-?timingInfo.responseEnd;

          //?解釋dom樹耗時(shí)
          timingInfo.domComplete?-?timingInfo.domInteractive;

          //?*:從開始至load總耗時(shí)
          timingInfo.loadEventEnd?-?timingInfo.navigationStart;

          //?*:?白屏?xí)r間
          timingInfo.responseStart?-?timingInfo.fetchStart;

          //?*:?首屏?xí)r間
          timingInfo.domComplete?-?timingInfo.fetchStart;
          接口數(shù)據(jù)

          接口數(shù)據(jù)主要包括接口耗時(shí)、接口請求異常,耗時(shí)可以通過對XmlHttpRequest 和 fetch請求的攔截過程中進(jìn)行時(shí)間統(tǒng)計(jì),異常通過xhr的readyState和status屬性判斷。

          XmlHttpRequest 攔截:修改XMLHttpRequest的原型,在發(fā)送請求時(shí)開啟事件監(jiān)聽,注入SDK鉤子 XMLHttpRequest.readyState的五種就緒狀態(tài):

          • 0:請求未初始化(還沒有調(diào)用 open())。
          • 1:請求已經(jīng)建立,但是還沒有發(fā)送(還沒有調(diào)用 send())。
          • 2:請求已發(fā)送,正在處理中(通常現(xiàn)在可以從響應(yīng)中獲取內(nèi)容頭)。
          • 3:請求在處理中;通常響應(yīng)中已有部分?jǐn)?shù)據(jù)可用了,但是服務(wù)器還沒有完成響應(yīng)的生成。
          • 4:響應(yīng)已完成;您可以獲取并使用服務(wù)器的響應(yīng)了。
          XMLHttpRequest.prototype.open?=?function?(method:?string,?url:?string)?{
          ??//?...省略
          ??return?open.call(this,?method,?url,?true);
          };
          XMLHttpRequest.prototype.send?=?function?(...rest:?any[])?{
          ??//?...省略
          ??const?body?=?rest[0];

          ??this.addEventListener("readystatechange",?function?()?{
          ????if?(this.readyState?===?4)?{
          ??????if?(this.status?>=?200?&&?this.status?<?300)?{
          ????????//?...省略
          ??????}?else?{
          ????????//?...省略
          ??????}
          ????}
          ??});
          ??return?send.call(this,?body);
          };

          Fetch 攔截:Object.defineProperty

          Object.defineProperty(window,?"fetch",?{
          ??configurable:?true,
          ??enumerable:?true,
          ??get()?{
          ????return?(url:?string,?options:?any?=?{})?=>?{
          ??????return?originFetch(url,?options)
          ????????.then((res)?=>?{
          ?????????//?...
          ????????})
          ????};
          ??}
          });
          手機(jī)、瀏覽器數(shù)據(jù)

          通過navigatorAPI獲取在進(jìn)行解析,使用第三方包mobile-detect幫助我們獲取解析

          頁面訪問數(shù)據(jù)

          全局?jǐn)?shù)據(jù)增加url、頁面標(biāo)題、用戶標(biāo)識,SDK可以自動(dòng)為網(wǎng)頁session分配一個(gè)隨機(jī)用戶label作為標(biāo)識,以此標(biāo)識單個(gè)用戶

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

          主要包含用戶點(diǎn)擊頁面元素、控制臺信息、用戶鼠標(biāo)移動(dòng)軌跡。

          • 用戶點(diǎn)擊元素:window事件代理
          • 控制臺信息:重寫console
          • 用戶鼠標(biāo)移動(dòng)軌跡:第三方庫rrweb

          下面是針對這些數(shù)據(jù)進(jìn)行統(tǒng)一的監(jiān)控SDK設(shè)計(jì)

          SDK開發(fā)

          為更好的解耦模塊,我決定使用基于事件訂閱的方式,整個(gè)SDK分成幾個(gè)核心的模塊,由于使用ts開發(fā)并且代碼會保持良好的命名規(guī)范和語義化,只有在關(guān)鍵的地方才會有注釋,完整的代碼實(shí)現(xiàn)見文末Github倉庫。

          • class: ?WebMonitor:核心監(jiān)控類
          • class:AjaxInterceptor:攔截ajax請求
          • class:ErrorObserver:監(jiān)控全局錯(cuò)誤
          • class:FetchInterceptor:攔截fetch請求
          • class:Reporter:上報(bào)
          • class:Performance:監(jiān)控性能數(shù)據(jù)
          • class:RrwebObserver:接入rrweb獲取用戶行為軌跡
          • class:SpaHandler:針對SPA應(yīng)用做處理
          • util: DeviceUtil:設(shè)備信息獲取輔助函數(shù)
          • event: 事件中心

          SDK提供的事件

          對外暴露事件,_開頭為框架內(nèi)部事件

          export?enum?TrackerEvents?{
          ??//?對外暴露事件
          ??performanceInfoReady?=?"performanceInfoReady",??//?頁面性能數(shù)據(jù)獲取完畢
          ??reqStart?=?"reqStart",??//?接口請求開始
          ??reqEnd?=?"reqEnd",???//?接口請求完成
          ??reqError?=?"reqError",??//?請求錯(cuò)誤
          ??jsError?=?"jsError",??//?頁面邏輯異常
          ??vuejsError?=?"vuejsError",??//?vue錯(cuò)誤監(jiān)控事件
          ??unHandleRejection?=?"unHandleRejection",??//?未處理promise異常
          ??resourceError?=?"resourceError",??//?資源加載錯(cuò)誤
          ??batchErrors?=?"batchErrors",??//?錯(cuò)誤合并上報(bào)事件,用戶合并上報(bào)請求節(jié)省請求數(shù)量
          ??mouseTrack?=?"mouseTrack",??//??用戶鼠標(biāo)行為追蹤
          }

          使用方式

          import?{?WebMonitor?}?from?"femonitor-web";
          const?monitor?=?Monitor.init();
          /*?Listen?single?event?*/
          monitor.on([event],?(emitData)?=>?{});
          /*?Or?Listen?all?event?*/
          monitor.on("event",?(eventName,?emitData)?=>?{})
          核心模塊解析

          WebMonitor、errorObserver、ajaxInterceptor、fetchInterceptor、performance

          WebMonitor

          集成了框架的其他類,對傳入配置和默認(rèn)配置進(jìn)行deepmerge,根據(jù)配置進(jìn)行初始化

          this.initOptions(options);

          this.getDeviceInfo();
          this.getNetworkType();
          this.getUserAgent();

          this.initGlobalData();?//?設(shè)置一些全局的數(shù)據(jù),在所有事件中g(shù)lobalData中都會帶上
          this.initInstances();
          this.initEventListeners();

          API

          支持鏈?zhǔn)讲僮?/p>

          • on:監(jiān)聽事件
          • off:移除事件
          • useVueErrorListener:使用Vue錯(cuò)誤監(jiān)控,獲取更詳細(xì)的組件數(shù)據(jù)
          • changeOptions:修改配置
          • configData:設(shè)置全局?jǐn)?shù)據(jù)

          errorObserver

          監(jiān)聽window.onerror和window.onunhandledrejection,并且對err.message進(jìn)行解析,獲取想要emit的錯(cuò)誤數(shù)據(jù)。

          window.onerror?=?function?(...args)?{
          ??//?調(diào)用原始方法
          ??if?(oldOnError)?{
          ????oldOnError(...args);
          ??}

          ??const?[msg,?url,?line,?column,?error]?=?args;

          ??const?stackTrace?=?error???ErrorStackParser.parse(error)?:?[];
          ??const?msgText?=?typeof?msg?===?"string"???msg?:?msg.type;
          ??const?errorObj:?IError?=?{};

          ??myEmitter.customEmit(TrackerEvents.jsError,?errorObj);
          };

          window.onunhandledrejection?=?function?(error:?PromiseRejectionEvent)?{
          ??if?(oldUnHandleRejection)?{
          ????oldUnHandleRejection.call(window,?error);
          ??}

          ??const?errorObj:?IUnHandleRejectionError?=?{};
          ??myEmitter.customEmit(TrackerEvents.unHandleRejection,?errorObj);
          };

          window.addEventListener(
          ??"error",
          ??function?(event)?{
          ????const?target:?any?=?event.target?||?event.srcElement;
          ????const?isElementTarget?=
          ??????target?instanceof?HTMLScriptElement?||
          ??????target?instanceof?HTMLLinkElement?||
          ??????target?instanceof?HTMLImageElement;
          ????if?(!isElementTarget)?return?false;

          ????const?url?=?target.src?||?target.href;

          ????const?errorObj:?BaseError?=?{};
          ????myEmitter.customEmit(TrackerEvents.resourceError,?errorObj);
          ??},
          ??true
          );

          ajaxInterceptor

          攔截ajax請求,并觸發(fā)自定義的事件。對XMLHttpRequest的open和send方法進(jìn)行重寫

          XMLHttpRequest.prototype.open?=?function?(method:?string,?url:?string)?{
          ??const?reqStartRes:?IAjaxReqStartRes?=?{
          ??};

          ??myEmitter.customEmit(TrackerEvents.reqStart,?reqStartRes);
          ??return?open.call(this,?method,?url,?true);
          };

          XMLHttpRequest.prototype.send?=?function?(...rest:?any[])?{
          ??const?body?=?rest[0];
          ??const?requestData:?string?=?body;
          ??const?startTime?=?Date.now();

          ??this.addEventListener("readystatechange",?function?()?{
          ????if?(this.readyState?===?4)?{
          ??????if?(this.status?>=?200?&&?this.status?<?300)?{
          ????????const?reqEndRes:?IReqEndRes?=?{};

          ????????myEmitter.customEmit(TrackerEvents.reqEnd,?reqEndRes);
          ??????}?else?{
          ????????const?reqErrorObj:?IHttpReqErrorRes?=?{};
          ????????
          ????????myEmitter.customEmit(TrackerEvents.reqError,?reqErrorObj);
          ??????}
          ????}
          ??});
          ??return?send.call(this,?body);
          };

          fetchInterceptor

          對fetch進(jìn)行攔截,并且觸發(fā)自定義的事件。

          Object.defineProperty(window,?"fetch",?{
          ??configurable:?true,
          ??enumerable:?true,
          ??get()?{
          ????return?(url:?string,?options:?any?=?{})?=>?{
          ??????const?reqStartRes:?IFetchReqStartRes?=?{};
          ??????myEmitter.customEmit(TrackerEvents.reqStart,?reqStartRes);

          ??????return?originFetch(url,?options)
          ????????.then((res)?=>?{
          ??????????const?status?=?res.status;
          ??????????const?reqEndRes:?IReqEndRes?=?{};

          ??????????const?reqErrorRes:?IHttpReqErrorRes?=?{};

          ??????????if?(status?>=?200?&&?status?<?300)?{
          ????????????myEmitter.customEmit(TrackerEvents.reqEnd,?reqEndRes);
          ??????????}?else?{
          ????????????if?(this._url?!==?self._options.reportUrl)?{
          ??????????????myEmitter.customEmit(TrackerEvents.reqError,?reqErrorRes);
          ????????????}
          ??????????}

          ??????????return?Promise.resolve(res);
          ????????})
          ????????.catch((e:?Error)?=>?{
          ??????????const?reqErrorRes:?IHttpReqErrorRes?=?{};
          ??????????myEmitter.customEmit(TrackerEvents.reqError,?reqErrorRes);
          ????????});
          ????};
          ??}
          });

          performance

          通過Performance獲取頁面性能,在性能數(shù)據(jù)完備后emit事件

          const?{
          ??domainLookupEnd,
          ??domainLookupStart,
          ??connectEnd,
          ??connectStart,
          ??responseEnd,
          ??requestStart,
          ??domComplete,
          ??domInteractive,
          ??domContentLoadedEventEnd,
          ??loadEventEnd,
          ??navigationStart,
          ??responseStart,
          ??fetchStart
          }?=?this.timingInfo;

          const?dnsLkTime?=?domainLookupEnd?-?domainLookupStart;
          const?tcpConTime?=?connectEnd?-?connectStart;
          const?reqTime?=?responseEnd?-?requestStart;
          const?domParseTime?=?domComplete?-?domInteractive;
          const?domReadyTime?=?domContentLoadedEventEnd?-?fetchStart;
          const?loadTime?=?loadEventEnd?-?navigationStart;
          const?fpTime?=?responseStart?-?fetchStart;
          const?fcpTime?=?domComplete?-?fetchStart;

          const?performanceInfo:?IPerformanceInfo<number>?=?{
          ??dnsLkTime,
          ??tcpConTime,
          ??reqTime,
          ??domParseTime,
          ??domReadyTime,
          ??loadTime,
          ??fpTime,
          ??fcpTime
          };

          myEmitter.emit(TrackerEvents.performanceInfoReady,?performanceInfo);

          完整SDK實(shí)現(xiàn)見下方Github倉庫地址,歡迎star、fork、issue,點(diǎn)擊左下方“閱讀原文”即可跳轉(zhuǎn)。

          web前端監(jiān)控SDK:https://github.com/alex1504/femonitor-web

          如果本文對你有幫助,歡迎點(diǎn)贊、收藏及轉(zhuǎn)發(fā),也歡迎在下方評論區(qū)一起交流,你的支持是我前進(jìn)的動(dòng)力。

          瀏覽 49
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <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>
                  亚洲三级天天影院 | 青草2017男人天堂 | 多国五级毛片 | 亚洲一区欧美国产日韩 云播 | 亚洲中文字幕在线看 |