前言
收集哪些資料
性能
錯誤
輔助資訊
小結
用戶端SDK(探針)相關原理和API
Web
微信小程式
編寫測試用例
單元測試
流程測試
提供Web環境的方式
Mock Web API的方式
結語
随着前端的發展和被重視,慢慢的行業内對于前端監控系統的重視程度也在增加。這裡不對為什麼需要監控再做解釋。那我們先直接說說需求。
對于中小型公司來說,可以直接使用三方的監控,比如自己搭建一套免費的<code>sentry</code>就可以捕獲異常和上報事件,或者使用阿裡雲的<code>ARMS</code>,功能比較全面也并不會太貴。類似的開源系統或者付費系統還很多,都能滿足我們一定的需求。
假如這個公司逐漸成長,已經成為一個中大型的公司,使用者量、業務服務、公司整體架構全部都在更新,這樣三方的監控系統可能就慢慢的出現一些不能滿足需求的問題。比如企業内部各種系統之間的關系太獨立和分散,不能使用内部的統一登陸、不能互相跳轉,想要增加一些字段收集并不能很快得到支援等等。這些問題都會導緻效率上不能滿足企業發展要求。一個内部可控并且能高速響應企業需求的前端監控系統就顯得很有必要。
我們在内部的前端監控系統上已經投入了一定的精力和時間,今天分享一下前端監控<code>SDK</code>部分的内容,主要三個方面:
用戶端SDK(探針)及原理
前端監控系統最核心的首要是收集用戶端的相關資料,我們現在支援的用戶端探針有:<code>web</code>、微信小程式、<code>andriod</code>和<code>ios</code>。它們主要收集如圖以下資訊:

收集頁面加載、靜态資源、<code>ajax</code>接口等性能資訊,名額有加載時間、<code>http</code>協定版本、響應體大小等,這是為業務整體品質提升提供資料支撐,解決慢查詢問題等。
收集<code>js</code>報錯、靜态資源加載錯誤、<code>ajax</code>接口加載錯誤,這些正常錯誤收集都很好了解。下面主要說明一下"業務接口錯誤(bussiness)":
用戶端發送<code>ajax</code>請求後端業務接口,接口都會傳回<code>json</code>資料結構,而其中一般都會有<code>errorcode</code>和<code>message</code>兩個字段,<code>errorcode</code>為業務接口内部定義的狀态碼。正常的業務響應内部都會約定比如<code>errorcode==0</code>等,那如果不為<code>0</code>可能是一些異常問題或者可預見的異常問題,這種錯誤資料就是需要收集的。
由于不同團隊或者接口可能約定都不一樣,是以我們隻會提供一個預設方法,預設方法會在<code>ajax</code>請求響應後調用,業務方自己根據約定和響應的<code>json</code>資料,在預設的方法中編寫判斷邏輯控制是否上報。像是下面這樣:
除了上面兩類硬名額資料,我們還需要很多其它的資訊,比如:使用者的通路軌迹、使用者點選行為、使用者ID、裝置版本、裝置型号、UV/UA辨別、<code>traceId</code>等等。很多時候我們要解決的問題并不是那麼簡單直接就能排查出來,甚至我們需要前端監控和其它系統在某些情況下能夠關聯上,是以這些軟名額資訊同樣很重要。
在這裡專門解釋一下<code>traceId</code>:
現在的後端服務都會使用<code>APM</code>(應用性能管理)系統,<code>APM</code>工具會在一次完整請求調用之初生成唯一的<code>id</code>,通常叫做<code>traceId</code>,它會記錄整個請求過程服務端的鍊路細節。如果前端能夠擷取到它,就能通過它去後端<code>APM</code>系統中查詢某次請求的日志資訊。隻要後端做好相關的配置,後端接口在響應用戶端<code>http</code>請求時,可以把<code>traceId</code>傳回給用戶端,SDK便可以去收集<code>ajax</code>請求的<code>traceId</code>,這樣前後端監控就能夠關聯上了。
收集以上的資訊并開發一套管理台,能夠達到監控前端性能和異常錯誤的目的。想象一個場景,當我們收到監控系統的告警或者相關同僚的問題回報時,我們能打開管理台,首先檢視到實時的錯誤,如果發現是<code>js</code>的代碼導緻的問題,我們能很快找到前端代碼錯誤的地方。如果不是前端的錯誤,我們通過收集的業務接口錯誤發現是後端接口的問題,我們也能及時的通知後端同僚,在什麼時間哪個接口報出<code>errorcode</code>為xx的錯誤,并且我們還能通過<code>traceId</code>直接查到這次<code>ajax</code>請求的後端鍊路監控資料。如果實在不是明顯就能排查到的問題,我們還能通過收集到的使用者軌迹、裝置資訊和網絡請求等資料,多方面的分析還原使用者當時的場景,來輔助我們排查代碼中的難以複現的<code>bug</code>或者相容問題。
在以上這個場景中,我們能夠提高前端排查問題的能力,甚至能輔助後端同學。在大部分時候,出現<code>bug</code>,很可能第一時間首先是找到前端做回報,前端是排查問題的先頭部隊。當我們有這樣的前端監控系統之後,不至于每次遇到問題手足無措,解決問題的時間也會快許多。
【具體字段一覽】
确定好了要收集哪些資訊,接下來就需要去實作用戶端<code>SDK</code>,它能夠在業務項目中自動收集資料上報給服務端。
所謂探針,是因為我們的<code>SDK</code>要依托于監控的前端項目的運作環境,在其運作環境的底層<code>API</code>中加入探針函數來收集資訊,下面分享<code>WEB</code>和微信小程式<code>SDK</code>實作的主要原理和使用的<code>API</code>。
下圖是<code>SDK</code>主要使用的<code>Web API</code>,通過這幾個<code>API</code>我們就能分别擷取到:頁面性能資訊、資源性能資訊、<code>ajax</code>資訊、錯誤資訊。
通過<code>performance.timing</code>可以拿到頁面首次加載的性能資料,<code>dns</code>、<code>tcp</code>、白屏時間等,而在最新的标準中<code>performance.timing</code>已經被廢棄,是以我們也改造為使用<code>performance.getEntriesByType('navigation')</code>。這裡的白屏時間可能和實際真正的使用者感官的白屏時間是有差異的,僅供參考。
通過<code>new PerformanceObserver</code>監聽器,我們可以監聽所有資源(<code>css</code>,<code>script</code>,<code>img</code>,<code>ajax</code>等)加載的性能資料:加載時間,響應大小,<code>http</code>協定版本(<code>http1.1</code>/<code>http2</code>)等。而後我們需要通過一個數組去管理資源性能資料,在完成資料上報後,清空數組。
由于浏覽器并沒有提供一個統一的<code>API</code>使我們能夠收集到<code>ajax</code>請求和響應資料,并且不管我們是用<code>axois</code>還是使用其他的<code>http</code>請求庫,他們都是基于<code>fetch</code>和<code>xmlHttpRequest</code>實作的。是以隻能通過重寫<code>fetch</code>和<code>xmlHttpRequest</code>,并在對應的函數和邏輯中插入自定義代碼,來達到收集的目的。相關的文章很多,這裡就不再細說了。
最後這幾個API都是收集js相關錯誤資訊的。需要注意兩個問題:
一是<code>onerror</code>會擷取不到跨域的<code>script</code>錯誤,解決方案也很簡單:為跨域的<code>script</code>标簽設定<code>crossorigin</code>屬性,并且需要靜态伺服器為目前資源設定<code>CORS</code>響應頭。
二是代碼壓縮後的報錯資訊需要通過<code>sourceMap</code>檔案解析出源代碼對應的行列和錯誤資訊,<code>sourceMap</code>本身是一種資料結構,存儲了源代碼和壓縮代碼的關系資料,通過解析庫能夠很輕松轉換它們。但如何自動化管理和操作<code>sourceMap</code>檔案才是前端監控系統核心需要解決的問題。這裡就需要結合企業内部的靜态資源釋出系統和前端監控系統,來解決低效率的手動打包上傳問題。
微信小程式底層使用<code>js</code>實作,有着它自己的一套生命周期,也提供了全局的<code>API</code>。通過重寫它的部分全局函數和相關<code>API</code>我們能擷取到:網絡請求、錯誤資訊、裝置和版本資訊等。由于微信小程式的加載流程是由微信<code>APP</code>控制的,<code>js</code>等資源也被微信内部托管,是以和<code>web</code>不同,我們沒有辦法擷取到<code>web</code>中<code>performance</code>能擷取到的頁面和資源加載資訊(後來發現小程式已經在v2.11.0 (2020-04-24)版本中,新增 API 提供performance性能對象名額,以後可以使用了)。下圖是<code>SDK</code>主要使用的<code>API</code>
通過重寫全局的<code>App</code>函數,綁定<code>onError</code>方法監聽錯誤,重寫它的<code>onShow</code>方法執行小程式啟動時<code>SDK</code>需要的邏輯。通過重寫<code>Component</code>的<code>onShow</code>方法,可以在頁面元件切換時執行我們的路徑收集和執行上報等邏輯。
這裡也是因為和 <code>fetch/xmlHttpRequest</code> 一樣,并沒有一個全局的<code>API</code>能讓我們捕獲到請求資訊,是以隻能通過重寫<code>wx.request</code>來達到監聽收集的功能。
當我們已經實作了<code>SDK</code>之後或者說在實作的過程中,就需要編寫測試代碼了,下面說說編寫測試用例。
<code>SDK</code>屬于一個需要長期維護和更新的獨立庫,它被使用在很多業務項目中,要求更加穩定,當出現問題的時候,它的更新成本很高。需要經曆:更新代碼->釋出新版本->業務方更新依賴版本,等流程,而如果在這個流程中,假如<code>SDK</code>又改出其它問題,那将會再啟上述循環,業務同僚肯定會被麻煩死。随着接入監控的系統增多,在疊代過程中改動任何的代碼已經讓人開始發慌,因為存在很多流程性的關聯邏輯,害怕改出問題。在一次代碼的重構和優化過程中,決心完善單元測試和流程測試。
單元測試主要是對一些有明顯輸入輸出的通用方法,比如<code>SDK</code>的<code>utils</code>中的常用方法,<code>SDK</code>的參數配置方法等。而對于監控<code>SDK</code>來說,更多的測試代碼主要集中在流程測試,對于單元測試這裡就不具體說明了。
監控<code>SDK</code>在業務項目中初始化之後,主要是通過加入探針監聽業務項目的運作狀态而收集資訊并進行上傳的,它在大部分情況下并不是業務方調用什麼就執行什麼。比如我們頁面初次加載,<code>SDK</code>在合适的時機會執行首次加載相關資訊的收集并上傳,那我們需要通過測試代碼來模拟這個流程,保障上報的資料是預期的。
我們的<code>SDK</code>運作在浏覽器環境中,在<code>node</code>環境下是不支援<code>Web</code>相關<code>API</code>的。是以我們需要讓我們的測試代碼在浏覽器中運作,或者提供相關<code>API</code>的支援。下面我們将會介紹兩種不同的方式,來支援我們的測試代碼正常運作。
假如我們使用<code>mocha</code>或者<code>jest</code>作為測試架構,可以通過<code>mocha</code>自帶的<code>mocha.run</code>方法在<code>html</code>中編寫和執行我們的測試代碼,并在浏覽器中打開運作;<code>jest-lite</code>也可以支援讓<code>jest</code>運作在浏覽器中。
但有時候我們不想讓它打開浏覽器,希望在終端中就能完成測試代碼運作,可以使用無頭浏覽器,在<code>node</code>中加載浏覽器環境,比如<code>phontomjs</code>或者<code>puppeteer</code>。他們提供了相關的工具,比如<code>mocha-phantomjs</code>就能直接在終端中運作<code>html</code>執行測試流程。
基于寫好的<code>html</code>測試檔案,再使用<code>mocha-phantomjs</code>和<code>phantomjs</code>,以下是<code>package.json</code>的指令配置。
<code>phontomjs</code>已經被廢棄了,不被推薦使用。推薦<code>puppeteer</code>,相關的功能和類似工具都有支援。
舉例說明:
以前有在<code>WebSocket</code>的代碼庫中使用過這種方式。因為依賴Web Api: <code>WebSocket</code>。需要通過<code>new WebSocket()</code>,來完成測試流程,而<code>node</code>環境下沒有此<code>API</code>。于是使用<code>mocha</code>在<code>html</code>中寫測試用例,如果希望全程使用終端跑測試,還可以配合使用<code>mocha-phantomjs</code>讓測試的<code>html</code>檔案可以在終端中執行而不用打開本地的網頁運作。
當然其實完全可以直接在浏覽器中打開<code>html</code>檢視測試運作結果,而且<code>phantomjs</code>相關的依賴包非常大、安裝也比較慢。但當時我們使用了持續繼承服務travis,當我們的代碼更新到遠端倉庫以後,<code>travis</code>将會啟動多個獨立容器并在終端中執行我們的測試檔案,如果不使用<code>mocha-phantomjs</code>在終端中跑測試沒有辦法在<code>travis</code>中成功通過。
在這次完善監控<code>SDK</code>測試的過程中,嘗試了另一種方式,全程使用<code>Mock</code>的方式。
上面的<code>Web</code>環境運作方式需要提供浏覽器或者無頭浏覽器。但實際我們需要測試的代碼并不是<code>Web API</code>,我們隻是使用了它們。我們假定它們是穩定的,我們隻需要在乎它的輸入輸出,如果它們内部出<code>bug</code>了,我們也是不能控制的,那是浏覽器開發商的事情。是以我要做的事情僅僅是在<code>node</code>環境中模拟相關的<code>Web API</code>。
拿前面說到的<code>WebSocket</code>舉例,因為<code>node</code>中不支援<code>WebSocket</code>,我們沒有辦法<code>new WebSocket</code>。那假如有完全模拟<code>WebSocket</code>的三方<code>node</code>庫,我們就可以在<code>node</code>代碼中,直接讓執行環境支援<code>WebSocket</code>: <code>const WebSocket = require('WebSocket')</code>。這樣我們就不需要在浏覽器或者無頭浏覽器環境下運作了。
下面就具體拿我們的監控<code>SDK</code>中的<code>fetch</code>舉例,是如何模拟流程測試的,總的來說要支援下面3個内容,
啟動一個httpserver服務提供接口服務
引入三方庫,讓node支援fetch
node中手動模拟部分performance API
首先說明一下<code>SDK</code>中<code>fetch</code>的正常流程,當我們的<code>SDK</code>在業務項目中初始化了之後,<code>SDK</code>會重寫<code>fetch</code>,于是業務項目中真正使用<code>fetch</code>做業務接口請求的時候,<code>SDK</code>就能通過之前重寫的邏輯擷取到<code>http</code>請求和響應資訊,同時也會通過<code>performance</code>擷取到<code>fetch</code>請求的性能資訊,并進行上報。我們要寫的測試代碼,就是驗證這個流程能夠順利完成。
因為是驗證<code>fetch</code>完整流程,我們需要啟動一個<code>httpserver</code>服務,提供接口來接收和響應這次<code>fetch</code>請求。
<code>node</code>環境中支援<code>fetch</code>的話,我們可以直接使用三方庫node-fetch,在執行環境的頂部,我們就可以提前定義<code>fetch</code>。
而<code>performance</code>就比較特殊一點,沒有一個三方的庫能夠支援。對于<code>fetch</code>流程來說,我們如果要模拟<code>performance</code>,隻需要模拟我們使用的<code>PerformanceObserver</code>,甚至一些入參和傳回我們也可以隻模拟我們需要的。下面的代碼是<code>PerformanceObserver</code>的使用例子。在<code>SDK</code>中,我們主要也是使用這一段代碼。
在浏覽器内部<code>performance</code>底層會自動去監聽資源請求,我們隻是通過它提供<code>PerformanceObserver</code>去收集它的資料。本質上來說,主動收集的行為探針在<code>performance</code>内部實作。
下面我們模拟<code>PerformanceObserver</code>一部分功能,來支援我們需要的測試流程。定義<code>window.PerformanceObserver</code>為構造函數,把傳入方法參數<code>fn</code>加入到數組中。<code>mockPerformanceEntriesAdd</code> 是我們需要手動調用的方法,當我們發起一次<code>fetch</code>,我們就手動調用一下此方法,把<code>mock</code>資料傳入給注冊的監聽函數,這樣就能使<code>PerformanceObserver</code>的執行個體接收到我們的<code>mock</code>資料,以此來模拟浏覽器中<code>performance</code>内部的行為。
通俗點舉例來說,十号公司要給打勞工銀行卡發工資的,打勞工的工資銀行卡第二天就會被扣房貸。打勞工最關心的保障正常扣房貸否則影響征信。本來打勞工隻需要關注銀行是否成功完成扣款,但是打勞工最近丢工作了公司不會打款到工資卡,是以隻能拿積蓄卡給自己的扣貸銀行卡轉錢,讓後續銀行可以扣錢還房貸。公司就是浏覽器<code>performance</code>底層,打勞工給自己轉錢就是<code>mockPerformanceEntriesAdd</code>,把公司發工資到銀行卡替換為自己轉錢進去,從被動接收變為主動執行。細品,你細品~
<code>mockPerformanceEntriesAdd</code>就是模拟浏覽器的主動行為,入參是性能資訊,我們可以直接寫死(下方<code>mockData</code>)。
看看測試代碼
當<code>mockPerformanceEntriesAdd</code>執行的時候,<code>SDK</code>内部的<code>PerformanceObserver</code>便能收集到mock的性能資訊了。( 這裡注意,我們還需要啟動一個<code>httpserver</code>的服務,服務提供<code>http://localhost:xx/api/getData</code>接口 )
當上面的測試代碼運作的時候,<code>SDK</code>能夠擷取位址為<code>http://localhost:xx/api/getData</code>的<code>fetch</code>的請求、響應和性能資訊,并且<code>SDK</code>也會發送一次<code>fetch</code>請求把收集的資料上報給後端服務。我們可以再次重寫<code>window.fetch</code>,來攔截<code>SDK</code>的上報請求,就可以擷取到請求内容,用請求内容來做預期測試判斷
合并後的測試代碼
如上圖所示,我們主要是以這樣的模式進行<code>SDK</code>的流程測試和代碼編寫。有了測試代碼後,能夠在很大程度上保障代碼維護疊代過程中的穩定性可控性,也能省去很多後期測試成本。
以上分享是我們在做監控<code>SDK</code>時比較核心的這三個方面,還有很多其它的細節和實作,比如:如何節流、上報時機、資料合并、初始化配置等。開發疊代過程中,要避免用戶端<code>SDK</code>或者後端服務因為疊代造成的相容性問題。還比較重要的是要考慮後期資料庫查詢和存儲方面的需求,收集、存儲和查詢才能完整的構成這套前端監控系統。
- End -
有沒有人打賞?沒有的話,那我晚點再來問問。
關注大詩人公衆号,第一時間擷取最新文章。
如果你有購買鋼琴的打算,可以從這裡了解到在售資訊,價格實惠品質保障。
---轉發請标明,并添加原文連結---