淺談前端異常監(jiān)控平臺(tái)實(shí)現(xiàn)方案

異常捕獲是改善軟件質(zhì)量的跟蹤手段之一,常見(jiàn)的方式是記錄日志,從日志分析異常問(wèn)題進(jìn)而跟進(jìn)。對(duì)于前端項(xiàng)目來(lái)說(shuō),異常可能是后端接口數(shù)據(jù)導(dǎo)致,可能是前端本身業(yè)務(wù)邏輯問(wèn)題導(dǎo)致,不管是什么導(dǎo)致的異常,只要能夠精準(zhǔn)的捕獲到就能夠分析出問(wèn)題所在。可能有小伙說(shuō)有測(cè)試階段,全面的測(cè)試機(jī)制的確能夠降低異常的出現(xiàn),但是測(cè)試大部份情況是在非生產(chǎn)環(huán)境上進(jìn)行的,覆蓋面有限。
日志是收集異常的最佳方式,一個(gè)異常監(jiān)控平臺(tái)就需要包括異常采集、異常存儲(chǔ)、異常統(tǒng)計(jì)與分析、異常報(bào)告、異常告警,而對(duì)于一個(gè)通用平臺(tái)來(lái)說(shuō),就需要項(xiàng)目管理、版本管理、團(tuán)隊(duì)管理、倉(cāng)庫(kù)管理等等。本文主要介紹一下異常采集需要考慮的問(wèn)題,并跟大家分享兩種現(xiàn)成的解決方案。
異常介紹
異常,是每種編程語(yǔ)言都需要考慮的一種結(jié)構(gòu),如何友好的跟蹤異常而不影響生產(chǎn)環(huán)境上的業(yè)務(wù),這就需要從項(xiàng)目開(kāi)發(fā)到上線整個(gè)過(guò)程做一定的規(guī)范。下面就來(lái)談?wù)勄岸说漠惓<疤幚矸绞健?/p>
異常分類(lèi)
先來(lái)說(shuō)說(shuō)JavaScript的錯(cuò)誤類(lèi)型,ECMA-262 定義了 7 種錯(cuò)誤類(lèi)型,說(shuō)明如下:
Error:普通異常,通常與 throw 語(yǔ)句和try/catch 語(yǔ)句一起使用,利用屬性 name 可以聲明或了解異常的類(lèi)型,利用message 屬性可以設(shè)置和讀取異常的詳細(xì)信息。
EvalError:Eval 函數(shù)執(zhí)行異常。
SyntaxError:語(yǔ)法解析不合理,即語(yǔ)法錯(cuò)誤。
RangeError:在數(shù)字超出合法范圍時(shí)拋出,比如數(shù)組下標(biāo)越界就會(huì)報(bào)這種錯(cuò)誤。
ReferenceError:在讀取不存在的變量時(shí)拋出,比如沒(méi)定義變量 a,后面卻使用這個(gè)變量 a,就會(huì)報(bào)這種錯(cuò)。
TypeError:當(dāng)一個(gè)值的類(lèi)型錯(cuò)誤時(shí)拋出該異常,比如傳遞給函數(shù)的參數(shù)與預(yù)期的不符,就會(huì)報(bào)這種錯(cuò)誤。
URIError:以一種錯(cuò)誤的方式使用全局 URI 處理函數(shù)而產(chǎn)生的錯(cuò)誤
異常處理
前端捕獲異常分為全局捕獲和單點(diǎn)捕獲。全局捕獲代碼集中,易于管理;單點(diǎn)捕獲作為補(bǔ)充,對(duì)某些特殊情況進(jìn)行捕獲,但分散,不利于管理,容易遺漏。在項(xiàng)目開(kāi)發(fā)過(guò)程中,定義一個(gè)錯(cuò)誤捕獲模塊,將項(xiàng)目所有的異常(全局異常和單點(diǎn)異常)都交給錯(cuò)誤模塊來(lái)統(tǒng)一處理,這就需要項(xiàng)目約定。
try-catch
try-catch 語(yǔ)句,是 JavaScript 處理異常的一種標(biāo)準(zhǔn)方式。基本語(yǔ)法如下:
try {
} catch (error) {
// 錯(cuò)誤處理
}
try 塊中的代碼發(fā)生了錯(cuò)誤,就會(huì)立即退出代碼執(zhí)行過(guò)程,然后執(zhí)行 catch 塊。catch 塊會(huì)接收到一個(gè)包含錯(cuò)誤信息的對(duì)象。一般是error.message。
finally
finally 在 try-catch 語(yǔ)句中是可選的,如果 finally 子句已經(jīng)使用,則其代碼無(wú)論如何都會(huì)執(zhí)行。無(wú)論 try 或 catch 語(yǔ)句塊中包含什么代碼——甚至 return 語(yǔ)句,都不會(huì)阻止 finally 子句的執(zhí)行。只要代碼中包含 finally 子句,那么無(wú)論 try 還是 catch 語(yǔ)句塊中的 return 語(yǔ)句都將被忽略。因此,在使用 finally 子句之前,一定要非常清楚想讓代碼怎么樣。看下面這個(gè)函數(shù):
const errorHelper = () => {
try {
return devpoint;
} catch (error) {
return "error";
} finally {
return "不管有無(wú)錯(cuò)誤,我都執(zhí)行了!";
}
};
console.log(errorHelper()); // 函數(shù)本身是發(fā)生了異常,但是最終打印的結(jié)果為:不管有無(wú)錯(cuò)誤,我都執(zhí)行了!
上面的函數(shù)代碼實(shí)際上是有異常的,因?yàn)樽兞?devpoint 并沒(méi)有定義,不過(guò)最終執(zhí)行了 finally 子句輸出了 不管有無(wú)錯(cuò)誤,我都執(zhí)行了!。
throw
與 try-catch 語(yǔ)句相配的 throw 操作符,用于隨時(shí)的主動(dòng)拋出自定義錯(cuò)誤。
const errorHelper = () => {
try {
return devpoint;
} catch (error) {
return "error";
} finally {
throw new Error("devpoint變量未定義");
}
};
console.log(errorHelper());
window.onerror
window.onerror,是全局異常捕獲,對(duì)于單點(diǎn)異常捕獲不到的異常就到這里了。
異常采集
觸發(fā)異常有很多原因,為了更好的分析,除了捕獲程序的錯(cuò)誤信息外,還需要采集執(zhí)行程序的外部環(huán)境,對(duì)于前端項(xiàng)目,外部環(huán)境就包括系統(tǒng)(Window、IOS、Android)和系統(tǒng)版本、瀏覽器(Chrome、IE、火狐等)和版本、IP地址、用戶信息、運(yùn)行的頁(yè)面、網(wǎng)絡(luò)環(huán)境、API接口數(shù)據(jù)。針對(duì)這些信息就需要設(shè)計(jì)采集的日志結(jié)構(gòu)。
在采集異常日志的時(shí)候,有個(gè)原則需要注意:采集日志行為不影響用戶體驗(yàn)及應(yīng)用本身的性能。
下面是一個(gè)參考的日志結(jié)構(gòu):
projectId:項(xiàng)目信息eventId:事件ID,日志的唯一標(biāo)志stack:錯(cuò)誤stack信息requestId:開(kāi)發(fā)者定義的異常標(biāo)志level:異常級(jí)別,可以是 error、info、warnbrowser:瀏覽器信息device:設(shè)備信息os:操作系統(tǒng)信息release:應(yīng)用版本信息url:異常觸發(fā)頁(yè)面urluser:用戶信息,可以是iPcreateAt:異常產(chǎn)生時(shí)間network:網(wǎng)絡(luò)信息eventKey:觸發(fā)的鍵dataRes:API響應(yīng)數(shù)據(jù)screenWidth:屏幕寬度screenHeight:屏幕高度message:異常詳細(xì)信息
異常上報(bào)
收集到異常數(shù)據(jù)如何上報(bào)呢?即需要將異常日志收集到云端存儲(chǔ),供項(xiàng)目開(kāi)發(fā)跟進(jìn)分析,一種方式是直接通過(guò)API異步上報(bào),在捕獲信息比較多的情況下,還是會(huì)占用網(wǎng)絡(luò)請(qǐng)求,影響應(yīng)用本身。可以考慮將采集的異常日志存儲(chǔ)在本地,最佳的選擇是IndexedDB,容量大,支持異步操作,可以自定義查詢(xún)。
IndexedDB 是WEB離線存儲(chǔ)的一種方式,因此存儲(chǔ)只是暫時(shí)的,還需要設(shè)計(jì)一個(gè)同步機(jī)制,將本地存儲(chǔ)的日志同步到云端服務(wù)器上。為了更好的同步,就需要設(shè)計(jì)暫存區(qū)、歸檔區(qū),新產(chǎn)生的日志存儲(chǔ)在暫存區(qū),已成功同步的日志存儲(chǔ)在歸檔區(qū)。有了本地存儲(chǔ),同步的過(guò)程批量同步。
后端存儲(chǔ),可以考慮使用leveldb,在性能方面,基本可以碾壓了mongodb和sqlite。
LevelDB是google公司開(kāi)發(fā)出來(lái)的一款超高性能kv存儲(chǔ)引擎,以其驚人的讀性能和更加驚人的寫(xiě)性能在輕量級(jí)nosql數(shù)據(jù)庫(kù)中鶴立雞群,此開(kāi)源項(xiàng)目目前是支持處理十億級(jí)別規(guī)模Key-Value型數(shù)據(jù)持久性存儲(chǔ)的C++ 程序庫(kù)。在優(yōu)秀的表現(xiàn)下對(duì)于內(nèi)存的占用也非常小,大量數(shù)據(jù)都直接存儲(chǔ)在磁盤(pán)上,可以理解為以空間換取時(shí)間。
第三方平臺(tái)
上面簡(jiǎn)單介紹實(shí)現(xiàn)異常監(jiān)控平臺(tái)的幾個(gè)關(guān)鍵點(diǎn),現(xiàn)在就跟大家分享兩個(gè)可以用于前端異常跟蹤的工具Google Analytics 和 Sentry 。
Google Analytics
沒(méi)錯(cuò),Google Analytics一般想到的是用于網(wǎng)站流量統(tǒng)計(jì)分析。可以借助Google Analytics的事件統(tǒng)計(jì)來(lái)跟蹤異常,下面是簡(jiǎn)單的方法:
function postEvents(error) {
var category = error.level || "warn",
action = error.action || "",
label = error.message || "";
ga("send", "event", category, action, label);
}
缺點(diǎn)就是無(wú)法方便的確定觸發(fā)異常的環(huán)境條件,后續(xù)也無(wú)法跟蹤版本等等。
Sentry
sentry 是一個(gè)實(shí)時(shí)事件日志記錄和聚合平臺(tái)。它專(zhuān)門(mén)用于監(jiān)視錯(cuò)誤和提取執(zhí)行適當(dāng)?shù)氖潞蟛僮魉璧乃行畔? 而無(wú)需使用標(biāo)準(zhǔn)用戶反饋循環(huán)的任何麻煩。
這是一個(gè)比較專(zhuān)業(yè)的異常監(jiān)控工具,基本支持所有主流編程語(yǔ)言,這里只是簡(jiǎn)單介紹一個(gè)前端的使用。
首先在頁(yè)面上加入以下腳本:
<script src="https://cdn.ravenjs.com/3.20.1/raven.min.js" crossorigin="anonymous"></script>
<script type="text/javascript">
try {
if ((typeof Raven) != "undefined"){
Raven.config('https://ffd39f4582184540a7214fb82bb3e888@sentry.io/248888',{
release: 'release_0.0.5',
allowSecretKey: true
}).install()
}
} catch (error) {
}
</script>
然后項(xiàng)目中可以寫(xiě)一個(gè)統(tǒng)一的入口:
function ExceptionJs() {
const ravenJs = typeof Raven != "undefined" ? Raven : {};
this.capture = function (error) {
try {
if (!(error instanceof Error)) {
if (typeof error === "object") {
error = JSON.stringify(error);
}
}
if (typeof ravenJs.captureException === "function") {
ravenJs.captureException(error);
}
} catch (e) {
// 這里是確保異常跟蹤腳本出錯(cuò)了不至于影響應(yīng)用程序
}
};
}
在需要的位置加入以下代碼:
const exceptionHelper = new ExceptionJs(),
try {
} catch (error) {
exceptionHelper.capture(error);
}
現(xiàn)在來(lái)看看收集上來(lái)的異常信息:
下面這個(gè)是異常的統(tǒng)計(jì)

異常列表

異常詳情

sentry 工具還提供了異常跟蹤處理的功能,有興趣的小伙伴可以去嘗試體驗(yàn)一下。