前端監控平臺系列:JS SDK(已開源)

本文做者:cjinhuo,未經受權禁止轉載。javascript

<h1 style="padding: 0px; font-weight: bold; color: black; font-size: 24px; text-align: center; line-height: 60px; margin-top: 10px; margin-bottom: 10px;">
<span style="color: #2db7f5; border-bottom: 2px solid #2db7f5;" class="content">背景</span>
</h1>html

傳統方式下一個前端項目發到正式環境後,全部報錯信息只能經過用戶使用時截圖、口頭描述發送到開發者,而後開發者來根據用戶所描述的場景去模擬這個錯誤的產生,這效率確定超級低,因此不少開源或收費的前端監控平臺就應運而生,好比:前端

等等一些優秀的監控平臺vue

<h2 style="margin-top: 25px; margin-bottom: 15px; padding: 0px; font-weight: bold; color: black; font-size: 20px;" data-id="heading-7"><span style="display: none;" class="prefix"></span><span style="color: #2db7f5; display: inline-block; padding-left: 10px;" class="content">國內經常使用的監控平臺</span><span style="display: none;" class="suffix"></span></h2>java

sentry :從監控錯誤、錯誤統計圖表、多重標籤過濾和標籤統計到觸發告警,這一整套都很完善,團隊項目須要充錢,並且數據量越大錢越貴react

fundebug:除了監控錯誤,還能夠錄屏,也就是記錄錯誤發生的前幾秒用戶的全部操做,壓縮後的體積只有幾十 KB,但操做略微繁瑣git

webfunny:也是含有監控錯誤的功能,能夠支持千萬級別日PV量,額外的亮點是能夠遠程調試、性能分析,也能夠docker私有化部署(免費),業務代碼加密過github

<h2 style="margin-top: 25px; margin-bottom: 15px; padding: 0px; font-weight: bold; color: black; font-size: 20px;" data-id="heading-7"><span style="display: none;" class="prefix"></span><span style="color: #2db7f5; display: inline-block; padding-left: 10px;" class="content">爲何不選擇上面三個監控平臺或者其餘監控平臺,爲何要本身搞?</span><span style="display: none;" class="suffix"></span></h2>web

  1. 首先sentryfundebug須要投入大量金錢來做爲支持,而webfunny雖是能夠用docker私有化部署,但因爲其代碼沒有開源,二次開發受限
  2. 本身開發能夠將公司全部的SDK統一成一個,包括但不限於:埋點平臺SDK、性能監控SDK

<h1 style="padding: 0px; font-weight: bold; color: black; font-size: 24px; text-align: center; line-height: 60px; margin-top: 10px; margin-bottom: 10px;">
<span style="color: #2db7f5; border-bottom: 2px solid #2db7f5;" class="content">監控平臺的組成</span>
</h1>ajax

<h2 style="margin-top: 25px; margin-bottom: 15px; padding: 0px; font-weight: bold; color: black; font-size: 20px;" data-id="heading-7"><span style="display: none;" class="prefix"></span><span style="color: #2db7f5; display: inline-block;" class="content">總體流程</span><span style="display: none;" class="suffix"></span></h2>

<div style="padding: 0px; font-weight: bold; color: black; font-size: 14px; text-align: center; line-height: 30px; margin-bottom: 10px;">
<span style="color: #2db7f5;" class="content">總體流程</span>
</div>

從上圖能夠看出來,若是須要自研監控平臺須要作三個部分:

  1. APP監控SDK:收集錯誤信息並上報
  2. server端:接收錯誤信息,處理數據並作持久化,然後根據告警規則通知對應的開發人員
  3. 可視化平臺:從數據存儲引擎拿出相關錯誤信息進行渲染,用於快速定位問題

<h1 style="padding: 0px; font-weight: bold; color: black; font-size: 24px; text-align: center; line-height: 60px; margin-top: 10px; margin-bottom: 10px;">
<span style="font-size: 24px; color: #2db7f5; border-bottom: 2px solid #2db7f5;" class="content">監控SDK</span>
</h1>
<h2 style="margin-top: 30px; margin-bottom: 15px; padding: 0px; font-weight: bold; color: black; font-size: 20px;" data-id="heading-7"><span style="display: none;" class="prefix"></span><span style="font-size: 20px; color: #2db7f5; display: inline-block;" class="content">總體代碼架構</span><span style="display: none;" class="suffix"></span></h2>

flow

<div style="padding: 0px; font-weight: bold; color: black; font-size: 14px; text-align: center; line-height: 30px; margin-bottom: 10px;">
<span style="color: #2db7f5;" class="content">代碼架構</span>
</div>

總體代碼架構使用發佈-訂閱設計模式以便後續迭代功能,處理邏輯基本都在HandleEvents文件中,這樣設計的好處是若是想穿插hook或者迭代功能能夠在處理事件回調多添加一個函數

handlerEvent

<div style="padding: 0px; font-weight: bold; color: black; font-size: 14px; text-align: center; line-height: 30px; margin-bottom: 10px;">
<span style="color: #2db7f5;" class="content">HandleEvents</span>
</div>

<h2 style="margin-top: 30px; margin-bottom: 15px; padding: 0px; font-weight: bold; color: black; font-size: 20px;" data-id="heading-7"><span style="display: none;" class="prefix"></span><span style="font-size: 20px; color: #2db7f5; display: inline-block; padding-left: 10px;" class="content">web錯誤信息收集</span><span style="display: none;" class="suffix"></span></h2>

通常狀況下都是經過重寫js原生事件而後拿到錯誤信息,好比ajax請求,經過重寫xhrfetch事件來截取接口信息,因此咱們須要優先編寫一個易於重寫事件的函數來複用。

replaceOld

<div style="padding: 0px; font-weight: bold; color: black; font-size: 14px; text-align: center; line-height: 30px; margin-bottom: 10px;">
<span style="color: #2db7f5;" class="content">replaceOld</span>
</div>

<h3 style="margin-top: 20px; margin-bottom: 15px; padding: 0px; font-weight: bold; color: black; font-size: 20px;" data-id="heading-7"><span style="display: none;" class="prefix"></span><span style="font-size: 16px; color: #2db7f5; display: inline-block; padding-left: 10px; border-left: 4px solid #2db7f5;" class="content">接口錯誤</span><span style="display: none;" class="suffix"></span></h3>

全部的請求第三方庫都是基於xhrfetch二次封裝的,因此只須要重寫這兩個事件就能夠拿到全部的接口請求的信息,經過判斷status的值來判斷當前接口是不是正常的。舉個例子,重寫xhr的代碼操做:

xhrReplace

<div style="padding: 0px; font-weight: bold; color: black; font-size: 14px; text-align: center; line-height: 30px; margin-bottom: 10px;">
<span style="color: #2db7f5;" class="content">Xhr重寫</span>
</div>

上面除了拿去接口的信息以外還作一個操做:若是是SDK發送的接口,就不用收集該接口的信息。若是須要發佈事件就調用triggerHandlers(EVENTTYPES.XHR, this.mito_xhr),相似的,fetch也是用這種方式來重寫。

關於接口跨域、超時的問題:這兩種狀況發生的時候,接口返回的響應體和響應頭裏面都是空的,status等於0,因此很難區分二者,可是正常狀況下,通常項目中都的請求都是複雜請求,因此在正式請求會先進行option進行預請求,若是是跨域的話基本幾十毫秒就會返回來,因此以此做爲臨界值來判斷跨域與超時的問題(若是是接口不存在也會被判斷成接口跨域)。

<h3 style="margin-top: 20px; margin-bottom: 15px; padding: 0px; font-weight: bold; color: black; font-size: 20px;" data-id="heading-7"><span style="display: none;" class="prefix"></span><span style="font-size: 16px; color: #2db7f5; display: inline-block; padding-left: 10px; border-left: 4px solid #2db7f5;" class="content">js代碼錯誤&&資源錯誤</span><span style="display: none;" class="suffix"></span></h3>

監聽windowerror事件

window.addEventListener('error',function(e){
  // 拿到錯誤信息,發佈事件:triggerHandlers
}, true)
  • 資源錯誤

判斷e.target.localName是否有值,有的話就是資源錯誤,在handleErrors中拿到信息:

handleError

<div style="padding: 0px; font-weight: bold; color: black; font-size: 14px; text-align: center; line-height: 30px; margin-bottom: 10px;">
<span style="color: #2db7f5;" class="content">handleError</span>
</div>

  • 代碼錯誤

上面判斷爲false時,表明是代碼錯誤,在回調中能夠拿到對應的錯誤代碼文件、代碼行數等等信息,而後經過source-map這個npm包+sourceMap文件進行解析,就能夠還原出線上真實代碼錯誤的位置。

<h3 style="margin-top: 20px; margin-bottom: 15px; padding: 0px; font-weight: bold; color: black; font-size: 20px;" data-id="heading-7"><span style="display: none;" class="prefix"></span><span style="font-size: 16px; color: #2db7f5; display: inline-block; padding-left: 10px; border-left: 4px solid #2db7f5;" class="content">監聽unhandledrejection</span><span style="display: none;" class="suffix"></span></h3>

Promisereject 且沒有 reject 處理器的時候,會觸發 unhandledrejection 事件

replaceUnhandlerejecttion

<div style="padding: 0px; font-weight: bold; color: black; font-size: 14px; text-align: center; line-height: 30px; margin-bottom: 10px;">
<span style="color: #2db7f5;" class="content">unhandledrejection監聽</span>
</div>

<h2 style="margin-top: 25px; margin-bottom: 15px; padding: 0px; font-weight: bold; color: black; font-size: 20px;"><span style="display: none;" class="prefix"></span><span style="color: #2db7f5; display: inline-block;" class="content">用戶行爲信息收集</span><span style="display: none;" class="suffix"></span></h2>

單純收集錯誤信息是能夠提升錯誤定位的效率,但若是再配合上用戶行爲的話就錦上添花,定位錯誤的效率再上一層,以下圖所示,能夠清晰的看到用戶作了哪些事:進了哪一個頁面 => 點擊了哪一個按鈕 => 觸發了哪一個接口:

breadcrumb

<div style="padding: 0px; font-weight: bold; color: black; font-size: 14px; text-align: center; line-height: 30px; margin-bottom: 10px;">
<span style="color: #2db7f5;" class="content">用戶行爲前端頁面展現</span>
</div>

<h3 style="margin-top: 20px; margin-bottom: 15px; padding: 0px; font-weight: bold; color: black; font-size: 20px;" data-id="heading-7"><span style="display: none;" class="prefix"></span><span style="font-size: 16px; color: #2db7f5; display: inline-block; padding-left: 10px; border-left: 4px solid #2db7f5;" class="content">dom事件信息</span><span style="display: none;" class="suffix"></span></h3>

dom事件獲取包括不少:clickinputdoubleClick等等,一種直接在window上面監聽click事件(注意第三個參數爲true):

window.addEventListener('click',function(e){
    // 利用節流,以防事件觸發過快
  // 發佈事件 triggerHandlers
}, true)

還有一種是經過重寫window.addEventListener的方式來截取開發者對dom的監聽事件。

<h3 style="margin-top: 20px; margin-bottom: 15px; padding: 0px; font-weight: bold; color: black; font-size: 20px;" data-id="heading-7"><span style="display: none;" class="prefix"></span><span style="font-size: 16px; color: #2db7f5; display: inline-block; padding-left: 10px; border-left: 4px solid #2db7f5;" class="content">路由切換信息</span><span style="display: none;" class="suffix"></span></h3>

在單頁應用中有兩種路由變換:hashchangehistory

  • history

當瀏覽器支持history模式時,會被如下兩個事件所影響:pushStatereplaceState,且這兩個事件不會觸發onpopstate的回調,因此咱們須要監聽這個三個事件:

onpopstate

<div style="padding: 0px; font-weight: bold; color: black; font-size: 14px; text-align: center; line-height: 30px; margin-bottom: 10px;">
<span style="color: #2db7f5;" class="content">onpopstate重寫</span>
</div>

  • hashchange

當瀏覽器只支持hashchange時,就須要重寫hashchange:

hashchange

<div style="padding: 0px; font-weight: bold; color: black; font-size: 14px; text-align: center; line-height: 30px; margin-bottom: 10px;">
<span style="color: #2db7f5;" class="content">hashchange重寫</span>
</div>

<h3 style="margin-top: 20px; margin-bottom: 15px; padding: 0px; font-weight: bold; color: black; font-size: 20px;" data-id="heading-7"><span style="display: none;" class="prefix"></span><span style="font-size: 16px; color: #2db7f5; display: inline-block; padding-left: 10px; border-left: 4px solid #2db7f5;" class="content">console信息</span><span style="display: none;" class="suffix"></span></h3>

正常狀況下正式環境是不該該有console的,那爲何要收集console的信息?第一:非正常狀況下,正式環境或預發環境也可能會有console,第二:不少時候也能夠把sdk放入測試環境上面調試。因此最終仍是決定收集console信息,可是在初始化的時候的傳參來告訴sdk是否監聽console的信息收集。

relaceConsole

<div style="padding: 0px; font-weight: bold; color: black; font-size: 14px; text-align: center; line-height: 30px; margin-bottom: 10px;">
<span style="color: #2db7f5;" class="content">console重寫</span>
</div>

<h2 style="margin-top: 25px; margin-bottom: 15px; padding: 0px; font-weight: bold; color: black; font-size: 20px;"><span style="display: none;" class="prefix"></span><span style="color: #2db7f5; display: inline-block;" class="content">框架層錯誤信息收集</span><span style="display: none;" class="suffix"></span></h2>

<h3 style="margin-top: 20px; margin-bottom: 15px; padding: 0px; font-weight: bold; color: black; font-size: 20px;"><span style="display: none;" class="prefix"></span><span style="font-size: 16px; color: #2db7f5; display: inline-block; padding-left: 10px; border-left: 4px solid #2db7f5;" class="content">Vue</span><span style="display: none;" class="suffix"></span></h3>

vue2.6官網提供了兩個報錯函數的回調:Vue.config.errorHandlerVue.config.warnHandler

errorHandle

<div style="padding: 0px; font-weight: bold; color: black; font-size: 14px; text-align: center; line-height: 30px; margin-bottom: 10px;">
<span style="color: #2db7f5;" class="content">vue錯誤信息收集</span>
</div>

<h3 style="margin-top: 20px; margin-bottom: 15px; padding: 0px; font-weight: bold; color: black; font-size: 20px;"><span style="display: none;" class="prefix"></span><span style="font-size: 16px; color: #2db7f5; display: inline-block; padding-left: 10px; border-left: 4px solid #2db7f5;" class="content">React</span><span style="display: none;" class="suffix"></span></h3>

React16.13中提供了componentDidCatch鉤子函數來回調錯誤信息,因此咱們能夠新建一個類ErrorBoundary來繼承React,而後而後聲明componentDidCatch鉤子函數,能夠拿到錯誤信息(目前沒寫react的錯誤收集,看官網文檔簡述,簡易版應該是這樣寫的)。

react-errro

<div style="padding: 0px; font-weight: bold; color: black; font-size: 14px; text-align: center; line-height: 30px; margin-bottom: 10px;">
<span style="color: #2db7f5;" class="content">react錯誤信息收集</span>
</div>

<h2 style="margin-top: 25px; margin-bottom: 15px; padding: 0px; font-weight: bold; color: black; font-size: 20px;"><span style="display: none;" class="prefix"></span><span style="color: #2db7f5; display: inline-block;" class="content">自定義上報錯誤</span><span style="display: none;" class="suffix"></span></h2>

上面收集的是web端的代碼錯誤、接口報錯和框架層面的報錯等等,還有一種是業務錯誤信息:好比點擊支付的時候,可能服務端接口返回200,可是響應體是錯誤信息,就須要手動上報這塊的錯誤信息。既然要手動上報,SDK就須要提供一個全局函數功能開發者調用:

import MITO from 'mitojs'
MITO.log({
  info: '支付失敗,餘額不足',
  tag: 'business'
})

<h2 style="margin-top: 25px; margin-bottom: 15px; padding: 0px; font-weight: bold; color: black; font-size: 20px;"><span style="display: none;" class="prefix"></span><span style="color: #2db7f5; display: inline-block;" class="content">Breadcrumb收集</span><span style="display: none;" class="suffix"></span></h2>

在上面收集完錯誤信息的時候,都在最後追加一行breadcrumb.push(data),這樣就能夠保存用戶的行爲軌跡,默認狀況設置20長度,也能夠在初始化時可配置,可是建議最高不要超過100,由於若是信息過多,內存佔用過大,對頁面不太友好。

<h3 style="margin-top: 20px; margin-bottom: 15px; padding: 0px; font-weight: bold; color: black; font-size: 20px;"><span style="display: none;" class="prefix"></span><span style="font-size: 16px; color: #2db7f5; display: inline-block; padding-left: 10px; border-left: 4px solid #2db7f5;" class="content">類型整合</span><span style="display: none;" class="suffix"></span></h3>

在每一個事件類型的回調的時候都將類型整合:好比用戶點擊、路由跳轉都是屬於用戶行爲,這樣作的緣由是讓開發者更好過濾無用信息和精準定位到須要的信息。

breadcrumb-category

<div style="padding: 0px; font-weight: bold; color: black; font-size: 14px; text-align: center; line-height: 30px; margin-bottom: 10px;">
<span style="color: #2db7f5;" class="content">用戶行爲類型整合</span>
</div>

<h2 style="margin-top: 25px; margin-bottom: 15px; padding: 0px; font-weight: bold; color: black; font-size: 20px;"><span style="display: none;" class="prefix"></span><span style="color: #2db7f5; display: inline-block;" class="content">Error id生成</span><span style="display: none;" class="suffix"></span></h2>

每一個錯誤事件觸發時都會有不少信息,咱們須要儘可能保證每一個不一樣信息的錯誤生成的id不同,這邊採起的措施是先將每一個錯誤的對象key按照必定規則遞歸排序,而後根據每一個對象的值進行hashCode,獲得一串errorId

<h2 style="margin-top: 25px; margin-bottom: 15px; padding: 0px; font-weight: bold; color: black; font-size: 20px;"><span style="display: none;" class="prefix"></span><span style="color: #2db7f5; display: inline-block;" class="content">上報錯誤信息</span><span style="display: none;" class="suffix"></span></h2>

當SDK拿到錯誤的全部信息時須要上報到服務端,有幾種方式上報服務端

<h3 style="margin-top: 20px; margin-bottom: 15px; padding: 0px; font-weight: bold; color: black; font-size: 20px;"><span style="display: none;" class="prefix"></span><span style="font-size: 16px; color: #2db7f5; display: inline-block; padding-left: 10px; border-left: 4px solid #2db7f5;" class="content">經過xhr上報</span><span style="display: none;" class="suffix"></span></h3>

經過xhr上報,若是設置成異步的時候,當用戶跳轉新頁面或者關閉頁面時就會丟失當前這個請求,若是設置成同步,又會讓頁面形成卡頓的現象

sentry目前是經過xhr發送的,不過它在發送前會推到它設置的一個請求緩衝區 _buffer,以此來優化併發請求過多的問題。

<h3 style="margin-top: 20px; margin-bottom: 15px; padding: 0px; font-weight: bold; color: black; font-size: 20px;"><span style="display: none;" class="prefix"></span><span style="font-size: 16px; color: #2db7f5; display: inline-block; padding-left: 10px; border-left: 4px solid #2db7f5;" class="content">Image的形式來發送請求</span><span style="display: none;" class="suffix"></span></h3>

特色:

  1. 沒有跨域問題、
  2. 發 GET 請求以後不須要獲取和處理數據、
  3. 服務器也不須要發送數據、
  4. 不會攜帶當前域名 cookie、不會阻塞頁面加載,影響用戶的體驗,只需 new Image 對象、
  5. 相比於 BMP/PNG 體積最小,能夠節約 41% / 35% 的網絡資源小

<h3 style="margin-top: 20px; margin-bottom: 15px; padding: 0px; font-weight: bold; color: black; font-size: 20px;"><span style="display: none;" class="prefix"></span><span style="font-size: 16px; color: #2db7f5; display: inline-block; padding-left: 10px; border-left: 4px solid #2db7f5;" class="content">Navigator.sendBeacon</span><span style="display: none;" class="suffix"></span></h3>

MDN:可用於經過HTTP將少許數據異步傳輸到Web服務器,統計和診斷代碼一般要在 unload 或者 beforeunload 事件處理器中發起一個同步 XMLHttpRequest 來發送數據。同步的 XMLHttpRequest 迫使用戶代理延遲卸載文檔,並使得下一個導航出現的更晚。下一個頁面對於這種較差的載入表現無能爲力

特色:

  1. 發出的是異步請求,而且是POST請求
  2. 發出的請求,是放到的瀏覽器任務隊列執行的,脫離了當前頁面,因此不會阻塞當前頁面的卸載和後面頁面的加載過程,用戶體驗較好
  3. 只能判斷出是否放入瀏覽器任務隊列,不能判斷是否發送成功
  4. Beacon API不提供相應的回調,所以後端返回最好省略response body
  5. 兼容性不是很友好

<h2 style="margin-top: 25px; margin-bottom: 15px; padding: 0px; font-weight: bold; color: black; font-size: 20px;"><span style="display: none;" class="prefix"></span><span style="color: #2db7f5; display: inline-block;" class="content">用戶惟一標識</span><span style="display: none;" class="suffix"></span></h2>

爲了方便統計用戶量,在每次上報的時候會帶一個惟一標識符trackerId,生成這個trackerId的途徑有兩種:

  1. 若是你是用ajax上報的話,發現cookie中沒有帶trackerId這個字段,服務端生成並setCookie設置到用戶端的cookie
  2. 直接用SDK生成,在每次上報以前都判斷localstorage是否存在trackerId,有則隨着錯誤信息一塊兒發送,沒有的話生成一個並設置到localstorage

<h1 style="padding: 0px; font-weight: bold; color: black; font-size: 24px; text-align: center; line-height: 60px; margin-top: 10px; margin-bottom: 10px;">
<span style="color: #2db7f5; border-bottom: 2px solid #2db7f5;" class="content">總結</span>
</h1>

<h2 style="margin-top: 25px; margin-bottom: 15px; padding: 0px; font-weight: bold; color: black; font-size: 20px;"><span style="display: none;" class="prefix"></span><span style="color: #2db7f5; display: inline-block;" class="content">SDK小結</span><span style="display: none;" class="suffix"></span></h2>

訂閱事件 => 重寫原生事件 => 觸發原生事件(發佈事件) => 拿到錯誤信息 => 提取有用的錯誤信息 => 上報服務端

<h2 style="margin-top: 25px; margin-bottom: 15px; padding: 0px; font-weight: bold; color: black; font-size: 20px;"><span style="display: none;" class="prefix"></span><span style="color: #2db7f5; display: inline-block;" class="content">關於開源</span><span style="display: none;" class="suffix"></span></h2>

SDK開源:mitojs,下一篇會講服務端的表結構設計思路、怎樣在千萬條數據中多重標籤毫秒級查詢錯誤事件以及更好的告警機制通知開發人員

感興趣的小夥伴能夠點個關注,後續好文不斷!!!

相關文章
相關標籤/搜索