我開源了一款輕量級(jí)前端監(jiān)控sdk

前言
本文主要介紹的就是我的開源項(xiàng)目,前端監(jiān)控 sdk:heimdallr-sdk。篇幅有限,因此本篇文章僅僅是對(duì) sdk 主要模塊的簡(jiǎn)單介紹,后續(xù)考慮出個(gè)系列,歡迎關(guān)注;同時(shí),項(xiàng)目也已開源,歡迎 star ?
背景
無(wú)論是初創(chuàng)的小公司,還是互聯(lián)網(wǎng)大廠。只要是能賺錢的業(yè)務(wù),那它的前端項(xiàng)目必定是需要監(jiān)控的。
什么?你們的項(xiàng)目直接果奔?不需要任何監(jiān)控? 那沒事了。

但是,有木有一種可能,你們一路果奔的前端項(xiàng)目,在不久的將來(lái),出現(xiàn)了本不該出現(xiàn)的 bug,關(guān)鍵這個(gè) bug 還不是你們內(nèi)部自己發(fā)現(xiàn)的,而是客戶發(fā)現(xiàn)的;然后,你們的線上 bug 數(shù)就喜加一了。在你吭哧吭哧地排查問題的時(shí)候,你還得反復(fù)問客戶 “您是怎么操作的?我可以遠(yuǎn)程看一下嗎?(微笑)”,有的客戶愿意告訴你,有的客戶只會(huì)禮貌問候你或者問候一線
不出意外的話,你今天、明天、大后天,甚至下周、下下周,都會(huì)陷入深深的自我懷疑中
“為什么,明明沒問題啊,怎么回事”
這個(gè)時(shí)候你就需要一款強(qiáng)大的前端監(jiān)控了
前端監(jiān)控已經(jīng)不是個(gè)新鮮玩意兒了,市面上也已經(jīng)有成熟的監(jiān)控系統(tǒng)了,強(qiáng)大如 Sentry,但是,它們有一個(gè)共同點(diǎn),那就是
貴
當(dāng)然貴是相對(duì)小項(xiàng)目而言,對(duì)于大項(xiàng)目更關(guān)注的是安全性以及更多的定制化
同時(shí)作為使用方,一旦監(jiān)控系統(tǒng)出了問題,就會(huì)顯得比較被動(dòng)
因此,不如自己動(dòng)手?jǐn)]一個(gè)前端監(jiān)控
成品
直接上菜 heimdallr-sdk
一款輕量級(jí)、插件化的前端監(jiān)控 sdk
能夠不侵入業(yè)務(wù)代碼并及時(shí)上報(bào)系統(tǒng)狀態(tài)(報(bào)錯(cuò)、使用情況等)。
為了防止功能過(guò)剩,避免引入過(guò)多的包,使得整個(gè)項(xiàng)目過(guò)于臃腫;除了基座是必須引入的,其余 sdk 的功能都將以插件的方式按需引入。
sdk 已經(jīng)能夠覆蓋大部分場(chǎng)景,不說(shuō)全棧吧,至少能覆蓋常見的前端場(chǎng)景了,如:瀏覽器、小程序。
采用了插件化架構(gòu),所以容易擴(kuò)展,允許引入自定義開發(fā)的插件,擴(kuò)展 sdk 的能力
sdk 的引入不需要復(fù)雜的配置,初始化時(shí)提供一下應(yīng)用名稱、監(jiān)控服務(wù)接口地址即可,其余配置按需調(diào)整,配置項(xiàng)少。
提供了 監(jiān)控管理后臺(tái) 與 監(jiān)控服務(wù),可以使用 cli 工具完成快速部署,支持二開
完全開源,不收費(fèi)
架構(gòu)
為了實(shí)現(xiàn)功能的按需引入與可擴(kuò)展性,整體采用插件化架構(gòu)

如上圖,不同端繼承自 Core,每個(gè)端各自有多種功能的插件,根據(jù)需要引入即可
為了能統(tǒng)一工作流,降低項(xiàng)目基建成本,提高團(tuán)隊(duì)協(xié)作性;項(xiàng)目采用目前主流的 monorepo
方式進(jìn)行代碼管理,即把多個(gè) packages 放在同一倉(cāng)庫(kù)中,插件也將作為獨(dú)立的子包放在 packages 下,統(tǒng)一編譯、調(diào)試、發(fā)布

整體架構(gòu)如下圖所示

大致分為:應(yīng)用接入層
、數(shù)據(jù)存儲(chǔ)層
、數(shù)據(jù)服務(wù)層
、監(jiān)控平臺(tái)層
應(yīng)用接入層即 sdk 的核心部分,負(fù)責(zé)收集應(yīng)用信息并上報(bào)
數(shù)據(jù)存儲(chǔ)使用的是 Mysql,為了方便操作數(shù)據(jù)庫(kù),額外引入了個(gè) ORM 庫(kù)
數(shù)據(jù)服務(wù)層、監(jiān)控平臺(tái)層后文細(xì)說(shuō)
數(shù)據(jù)流
這里我實(shí)現(xiàn)了兩種模式的服務(wù)
單服務(wù)
該模式下日志的上報(bào)、寫入,與監(jiān)控后臺(tái)日志的讀取在同一 node 服務(wù)中,如下圖
node 服務(wù)既負(fù)責(zé)接收日志,也負(fù)責(zé)讀寫數(shù)據(jù)庫(kù)

多服務(wù)
該模式拆分了“消費(fèi)服務(wù)”與“生產(chǎn)服務(wù)”,同時(shí)使用了 RabbitMQ 達(dá)到削峰填谷的效果,如下圖所示

producer 即生產(chǎn)者,負(fù)責(zé)接收客戶端上報(bào)的日志,并推入消息隊(duì)列。
consumer 也即消費(fèi)者,從消息隊(duì)列中讀取消息,拼接日志信息,寫入數(shù)據(jù)庫(kù)中;同時(shí)處理監(jiān)控后臺(tái)發(fā)來(lái)的請(qǐng)求,從數(shù)據(jù)庫(kù)中讀取相應(yīng)信息,處理后返回給監(jiān)控后臺(tái)。
Core
Core 是 SDK 的核心抽象類,完成一些基礎(chǔ)的初始化操作,負(fù)責(zé)提供 SDK 內(nèi)與平臺(tái)無(wú)關(guān)的代碼,同時(shí)規(guī)范各個(gè)客戶端的屬性與方法。
Core 主要做了以下事情
完成 SDK 配置項(xiàng)的初始化與綁定
實(shí)現(xiàn)引用插件的功能
使用發(fā)布訂閱模式完成日志的捕獲與上報(bào)
統(tǒng)一控制臺(tái)的輸出方法
提供面包屑功能,給各個(gè)插件使用(暫不支持手動(dòng)增加面包屑,可以使用 @heimdallr-sdk/customer 上報(bào))
規(guī)范初始化應(yīng)用方法,各客戶端所需的應(yīng)用信息不一致,因此這里只提供抽象方法,需要各個(gè)客戶端自己實(shí)現(xiàn)
規(guī)范數(shù)據(jù)轉(zhuǎn)換方法,與上一條一樣,這里也只提供抽象方法,需要客戶端自行實(shí)現(xiàn)
規(guī)范數(shù)據(jù)上報(bào)方法,因?yàn)椴煌蛻舳酥С值木W(wǎng)絡(luò)請(qǐng)求方式不一致,如:瀏覽器端有多種網(wǎng)絡(luò)請(qǐng)求 API 可用,而 wx 只能使用 wx.request 方法發(fā)起請(qǐng)求,因此這里也只提供抽象方法,得客戶端自己實(shí)現(xiàn)
Client
Client 即客戶端,也就是在不同平臺(tái)使用的 sdk 基座
Browser
Browser 即瀏覽器端的監(jiān)控基座,以瀏覽器為載體的應(yīng)用都可以使用該基座
繼承自 Core 抽象類,實(shí)現(xiàn)了 Core 中的抽象方法:
初始化應(yīng)用
數(shù)據(jù)轉(zhuǎn)換
數(shù)據(jù)上報(bào):支持 sendBeacon、圖片上報(bào)、get 三種上報(bào)方式,默認(rèn)使用 sendBeacon
Browser 基座同時(shí)內(nèi)置了錯(cuò)誤監(jiān)控 sdk,以內(nèi)置插件的方式集成在基座中,可以監(jiān)聽到以下三種類型的錯(cuò)誤:
代碼錯(cuò)誤(支持 sourcemap,需上傳 sourcemap 文件)
資源加載錯(cuò)誤
代碼中未捕獲的錯(cuò)誤
此外還監(jiān)聽了頁(yè)面的加載與卸載,作為一次訪問會(huì)話上報(bào),以頁(yè)面加載作為會(huì)話開始、頁(yè)面卸載視為會(huì)話結(jié)束
Browser 基座支持 CDN 與 NPM 兩種引入方式,這也就意味著絕大多數(shù)技術(shù)棧的前端應(yīng)用都可以使用該基座
CDN 方式引入如下
<script>
????window.__HEIMDALLR_OPTIONS__?=?{
????????dsn:?{
????????????host:?'localhost:8888',
????????????init:?'/project/init',
????????????upload:?'/log/upload'
????????},
????????app:?{
????????????name:?'playgroundAPP',
????????????leader:?'test',
????????????desc:?'test?proj'
????????},
????????userIdentify:?{
????????????name:?'__state__.a.0.user.id',?//?window.__state__?=?{?a:?[{?user:?{?id:'123'?}?}]?}
????????????position:?'global'
????????}
????};
</script>
<script?async?src="/browser-dist/browser.iife.js"></script>
NPM 引入
import?heimdallr?from?"@heimdallr-sdk/browser";
heimdallr({
????dsn:?{
????????host:?'localhost:8888',
????????init:?'/project/init',
????????upload:?'/log/upload'
????},
????app:?{
????????name:?'playgroundAPP',
????????leader:?'test',
????????desc:?'test?proj'
????},
????userIdentify:?{
????????name:?'__state__.a.0.user.id',?//?window.__state__?=?{?a:?[{?user:?{?id:'123'?}?}]?}
????????position:?'global'
????}
});
Node
Node 即 nodejs 服務(wù)端的監(jiān)控基座
同樣繼承自 Core 抽象類,實(shí)現(xiàn)了應(yīng)用初始化、上報(bào)數(shù)據(jù)最后的轉(zhuǎn)換、數(shù)據(jù)上報(bào)三個(gè)方法
這里的上報(bào)方式使用了第三方庫(kù)來(lái)實(shí)現(xiàn),node-fetch
Node 基座同樣默認(rèn)集成了錯(cuò)誤監(jiān)聽的能力,監(jiān)聽了 uncaughtException
的錯(cuò)誤并上報(bào)
Node 服務(wù)端一般不以“會(huì)話”為監(jiān)控維度,更關(guān)注接口與服務(wù)器性能,因此沒有 Browser 中的“會(huì)話”的概念
該基座可以通過(guò) NPM 方式引入,與 Browser 引入方式類似
Wx
Wx 即微信小程序的監(jiān)控基座
老規(guī)矩,繼承自 Core 抽象類,實(shí)現(xiàn)初始化、轉(zhuǎn)換、上報(bào)三個(gè)方法
同樣的,Wx 基座也集成了基礎(chǔ)的錯(cuò)誤監(jiān)控,本質(zhì)上就是重寫了 APP.onError
,捕獲到錯(cuò)誤并上報(bào)
與 Browser 最大的區(qū)別就是如何監(jiān)聽一個(gè)完整的會(huì)話,這里人為規(guī)定以 onShow 為一次會(huì)話的開始,以 onHide 為一次會(huì)話的結(jié)束,同時(shí)提供了兩種方式去監(jiān)聽會(huì)話:
提供
trace
函數(shù),在每個(gè)頁(yè)面的 onShow 與 onHide 方法內(nèi)手動(dòng)添加埋點(diǎn)重寫小程序的 Page 方法,返回
heimdallrPage
方法,在頁(yè)面中直接使用 heimdallrPage 替代 Page 方法
通過(guò) NPM 方式引入,引入方式參考微信官方文檔啦
Plugins
當(dāng)前僅有 Browser 基座與 Wx 基座的插件
篇幅有限,只能羅列一下了,沒法一個(gè)個(gè)單獨(dú)講
For Browser
Browser 基座的所有插件均提供 CDN 與 NPM 兩種引入方式
@heimdallr-sdk/console
監(jiān)聽瀏覽器控制臺(tái)的輸出并上報(bào),debug 為 false 時(shí),控制臺(tái)所有信息都不會(huì)打印
@heimdallr-sdk/customer
自動(dòng)讀取存儲(chǔ)在 cookie、localStorage、sessionStorage、window 上的數(shù)據(jù)并上報(bào),同時(shí)也可以通過(guò)調(diào)用
window.HEIMDALLR_REPORT(type: string, data: any)
手動(dòng)上報(bào)@heimdallr-sdk/dom
監(jiān)聽頁(yè)面的點(diǎn)擊事件并上報(bào)
@heimdallr-sdk/fetch
監(jiān)聽頁(yè)面發(fā)起的 fetch 請(qǐng)求,reportResponds 為 true 時(shí),將連同接口返回值一同上報(bào)
@heimdallr-sdk/xhr
監(jiān)聽頁(yè)面發(fā)起的 XMLHttpRequest 請(qǐng)求,reportResponds 為 true 時(shí),將連同接口返回值一同上報(bào)
@heimdallr-sdk/hash
監(jiān)聽頁(yè)面路由的 hash 變化,記錄來(lái)源與跳轉(zhuǎn)地址并上報(bào)
@heimdallr-sdk/history
監(jiān)聽頁(yè)面路由的變化,包括手動(dòng)點(diǎn)擊瀏覽器按鈕的跳轉(zhuǎn),自動(dòng)記錄來(lái)源與跳轉(zhuǎn)地址并上報(bào)
@heimdallr-sdk/performance
頁(yè)面性能監(jiān)控,可以得到下列性能指標(biāo)
dnsSearch: DNS 解析耗時(shí)
tcpConnect: TCP 連接耗時(shí)
sslConnect: SSL 安全連接耗時(shí)
request: TTFB 網(wǎng)絡(luò)請(qǐng)求耗時(shí)
response: 數(shù)據(jù)傳輸耗時(shí)
parseDomTree: DOM 解析耗時(shí)
resource: 資源加載耗時(shí)
domReady: DOM Ready
httpHead: http 頭部大小
interactive: 首次可交互時(shí)間
complete: 頁(yè)面完全加載
redirect: 重定向次數(shù)
redirectTime: 重定向耗時(shí)
duration
fp: 渲染出第一個(gè)像素點(diǎn),白屏?xí)r間
fcp: 渲染出第一個(gè)內(nèi)容,首屏結(jié)束時(shí)間
fmp: 有意義內(nèi)容渲染時(shí)間
fps: 刷新率
lcp: 最大內(nèi)容渲染時(shí)間,2.5s 內(nèi)
fid: 交互性能,應(yīng)小于 100ms
cls: 視覺穩(wěn)定性,應(yīng)小于 0.1
resource: 頁(yè)面資源加載耗時(shí)
@heimdallr-sdk/record
錄制當(dāng)前會(huì)話所有操作并上報(bào)
@heimdallr-sdk/page_crash
監(jiān)聽頁(yè)面崩潰,需配合
@heimdallr-sdk/page-crash-worker
使用,不走基座的上報(bào)與數(shù)據(jù)轉(zhuǎn)換,在 page-crash-worker 文件中使用 get 方法上報(bào)崩潰數(shù)據(jù)。從命名就能看出來(lái),核心原理就是使用 Worker (狗頭)@heimdallr-sdk/vue
捕獲 vue 拋出的錯(cuò)誤并上報(bào),支持 sourcemap(需上傳 sourcemap 文件)
For Wx
小程序基座的插件較少,但也不太需要那么多,畢竟小程序自己就有一套性能、錯(cuò)誤監(jiān)控;因此,只寫了幾個(gè)常用但小程序沒提供的監(jiān)控插件
@heimdallr-sdk/wx-dom
監(jiān)聽小程序的點(diǎn)擊事件,記錄觸發(fā)的函數(shù)名以及附帶信息并上報(bào)
@heimdallr-sdk/wx-request
監(jiān)聽小程序發(fā)起的請(qǐng)求,包括 request、downloadFile、uploadFile,同樣可通過(guò) reportResponds 配置決定是否上報(bào)接口返回結(jié)果
@heimdallr-sdk/wx-route
捕獲小程序的路由跳轉(zhuǎn),記錄來(lái)源、跳轉(zhuǎn)地址與跳轉(zhuǎn)狀態(tài)(成功與否)并上報(bào)
自定義插件
插件本質(zhì)上就是一個(gè)個(gè) Plugin 類型對(duì)象
基礎(chǔ)的 Plugin 類型如下:
export?interface?BasePluginType?{
??name:?string;
??monitor:?(notify:?(collectedData:?any)?=>?void)?=>?void;
??transform?:?(collectedData:?any)?=>?ReportDataType<any>;
}
name: 當(dāng)前插件名稱(不能寫中文)
monitor: 插件邏輯的具體實(shí)現(xiàn)放在這個(gè)函數(shù)體中
notify 函數(shù)負(fù)責(zé)將數(shù)據(jù)上報(bào),collectedData 還不是最終上報(bào)到服務(wù)器的數(shù)據(jù),會(huì)在基座的 transform 內(nèi)包裝一下再上報(bào)
transform: 可選配置,即接收 notify 中上報(bào)的數(shù)據(jù),在這里轉(zhuǎn)換一下;最終也是會(huì)到基座的 transform 方法內(nèi)做最后的“包裝”
因此,只需要實(shí)現(xiàn)并返回一個(gè)符合 BasePluginType 的對(duì)象,即可接入到 heimdallr-sdk 的基座中作為插件使用
Server
服務(wù)端作為私有子包,不發(fā)布,可通過(guò) @heimdallr-sdk/cli 腳手架快速部署
服務(wù)端使用 express 作為 Node 服務(wù)端框架,ORM 庫(kù)使用 Prisma,數(shù)據(jù)庫(kù)則使用的是 MySQL
正如前面說(shuō)的,這里我提供了兩種服務(wù)端,我把它稱為“單服務(wù)”與“多服務(wù)”
“單服務(wù)”
“單服務(wù)”采用的是傳統(tǒng)的 MVC 架構(gòu),不過(guò)這里默認(rèn)的 View 不調(diào)用 API,而是作為接口文檔,方便查閱;也可以修改 route 指向不同的頁(yè)面
實(shí)現(xiàn)的主要功能如下:
項(xiàng)目的初始化(其實(shí)就是應(yīng)用信息入庫(kù))
會(huì)話的創(chuàng)建與寫入
日志信息的接收與寫入(同時(shí)支持 post 與 get)
應(yīng)用列表
會(huì)話列表
日志列表
接收 sourcemap 文件
解析 sourcemap
“單服務(wù)”既負(fù)責(zé)接收,也負(fù)責(zé)提供接口給監(jiān)控后臺(tái)(Manager)使用,能直接讀寫數(shù)據(jù)庫(kù)

“多服務(wù)”
“多服務(wù)”將服務(wù)端一分為二,分為“消費(fèi)服務(wù)”與“生產(chǎn)服務(wù)”
使用 RabbitMQ 完成對(duì)流量的削峰填谷

Producer
“生產(chǎn)服務(wù)” 也就是圖中的 producer,即生產(chǎn)者,面向監(jiān)控 SDK,從 SDK 接收上報(bào)數(shù)據(jù)
主要功能如下:
接收應(yīng)用信息,并推入應(yīng)用隊(duì)列
接收日志信息(會(huì)話就是兩條一前一后的日志),并推入日志隊(duì)列
接收 sourcemap 文件
Consumer
“消費(fèi)服務(wù)” 也就是上圖的 consumer,也即消費(fèi)者,面向監(jiān)控后臺(tái),提供讀取接口給監(jiān)控后臺(tái)調(diào)用。
主要功能如下:
從應(yīng)用隊(duì)列中提取應(yīng)用消息,寫入數(shù)據(jù)庫(kù)
從日志隊(duì)列中提取日志消息,完成日志消息的“組裝”,再寫入數(shù)據(jù)庫(kù)
解析 sourcemap 文件
提供統(tǒng)計(jì)數(shù)據(jù)接口
提供應(yīng)用/項(xiàng)目列表接口
提供會(huì)話列表接口
提供日志列表接口
Manager
Manager 即監(jiān)控服務(wù)的管理后臺(tái),私有包,不發(fā)布,同樣可以通過(guò) @heimdallr-sdk/cli 腳手架工具快速部署
使用了自己寫的 Vue3 腳手架 vva-cli 快速開發(fā)的,技術(shù)棧是 Vue3 + Typescript + Element-Plus,使用 Vite 打包編譯
有以下四個(gè)模塊:
總覽

應(yīng)用列表

會(huì)話


詳情不單開頁(yè)面,在列表頁(yè)右側(cè)增加抽屜式彈層展示

(回放功能需引入
@heimdallr-sdk/record
插件)日志


同樣的,日志詳情也不單開頁(yè)面,在列表頁(yè)右側(cè)增加抽屜式彈層展示
Tools
@heimdallr-sdk/cli
heimdallr-sdk 的腳手架工具
主要作用就是為了能夠快速部署“監(jiān)控服務(wù)端”與“監(jiān)控管理后臺(tái)”
全局安裝腳手架
npm?i?@heimdallr-sdk/cli
安裝完成后輸入 heimdallr-create
命令,即可開始選擇相應(yīng)的模板

提供監(jiān)控后臺(tái)管理臺(tái)
和監(jiān)控服務(wù)
以及帶消息隊(duì)列的監(jiān)控服務(wù)
三類模板
依次完成配置(作答)后,在當(dāng)前目錄下將自動(dòng)創(chuàng)建項(xiàng)目文件夾

三個(gè)模板前文已經(jīng)介紹了,這里就不再贅述了
@heimdallr-sdk/webpack-plugin-sourcemap-upload
這個(gè)插件,件如其名(doge),主要功能就是在以 webpack 為構(gòu)建工具的項(xiàng)目中,自動(dòng)完成 sourcemap 文件的上傳
它將在 webpack 構(gòu)建完成后,將產(chǎn)出的 sourcemap 文件自動(dòng)上傳到指定服務(wù)器
用法也很簡(jiǎn)單,指定一下初始化 sdk 時(shí)使用的應(yīng)用名稱,以及文件上傳的接口地址即可
import?UploadSourceMapPlugin?from?"@heimdallr-sdk/webpack-plugin-sourcemap-upload";
const?config?=?{
??plugins:?[
????new?UploadSourceMapPlugin({
??????appname:?"playground",
??????url:?`http://localhost:8001/sourcemap/upload`,
????}),
??],
??//?TODO--
};
@heimdallr-sdk/vite-plugin-sourcemap-upload
這個(gè)插件功能同上,不同點(diǎn)在于:上一個(gè)插件是針對(duì)以 webpack 為構(gòu)建工具的項(xiàng)目,而這個(gè)插件是針對(duì)以 vite 為構(gòu)建工具的項(xiàng)目
同樣是在 vite 構(gòu)建工作完成后,將產(chǎn)出的 sourcemap 文件自動(dòng)上傳到指定服務(wù)器
因?yàn)?vite 底層其實(shí)是使用 rollup 構(gòu)建,因此,該插件監(jiān)聽的是 writeBundle
和 closeBundle
兩個(gè)階段的 hook
用法如下
import?sourceMapUpload?from?"@heimdallr-sdk/vite-plugin-sourcemap-upload";
export?default?defineConfig({
??plugins:?[
????vue(),
????sourceMapUpload({
??????appname:?"playground",
??????url:?`http://localhost:8001/sourcemap/upload`,
????}),
??],
??build:?{
????sourcemap:?true,
??},
??//?TODO--
});
使用時(shí)需要注意的是,@heimdallr-sdk/webpack-plugin-sourcemap-upload
對(duì)外暴露的是一個(gè)類,而 @heimdallr-sdk/vite-plugin-sourcemap-upload
對(duì)外暴露的則是一個(gè)函數(shù)
后記
后續(xù)考慮出個(gè)系列,再詳細(xì)寫一下實(shí)現(xiàn)。剛開源不久,可能還有 bug ??,歡迎多多提 issue