一文摸清前端監控自研實踐(三)錯誤監控

語言: CN / TW / HK

前言

上篇文章我們分享了關於 使用者行為監控 的內容,本文我們接著來看 錯誤異常監控 的方面

系列文章傳送門

一文摸清前端監控實踐要點(一)效能監控

一文摸清前端監控實踐要點(二)行為監控

一文摸清前端監控實踐要點(三)錯誤監控

一文摸清前端監控實踐要點(四)上報策略、其餘優化(近期上傳)

應用的穩定情況

眾所周知,無論進行釋出前的單元測試整合測試人工測試進行再多輪,都會難免漏掉一些邊緣的測試場景,甚至還有一些奇奇怪怪的玄學故障出現;而出現報錯後,輕則某些資料頁面無法訪問重則導致客戶資料出錯

這時,一個完善的錯誤監控體系就派上很大的用場,它可以幫助我們做以下的事情:

  • 應用報錯時,及時知曉線上應用出現了錯誤,及時安排修復止損;
  • 應用報錯後,根據上報的使用者行為追蹤記錄資料,迅速進行bug復現;
  • 應用報錯後,通過上報的錯誤行列以及錯誤資訊,找到報錯原始碼並快速修正;
  • 資料採集後,進行分析提供巨集觀的 錯誤數、錯誤率、影響使用者數等關鍵指標;

整體封裝

```ts // 錯誤型別 export enum mechanismType { JS = 'js', RS = 'resource', UJ = 'unhandledrejection', HP = 'http', CS = 'cors', VUE = 'vue', }

// 格式化後的 異常資料結構體 export interface exceptionMetrics { mechanism: Object; value?: string; type: string; stackTrace?: Object; pageInformation?: Object; behaviorTracking?: Array; errorUid: string; meta?: any; }

// 初始化用參 export interface ErrorVitalsInitOptions { Vue: any; }

// 判斷是 JS異常、靜態資源異常、還是跨域異常 export const getErrorKey = (event: ErrorEvent | Event) => { const isJsError = event instanceof ErrorEvent; if (!isJsError) return mechanismType.RS; return event.message === 'Script error.' ? mechanismType.CS : mechanismType.JS; };

// 初始化的類 export default class ErrorVitals { private engineInstance: LibraryStarter;

// 已上報的錯誤 uid private submitErrorUids: Array;

constructor(engineInstance: LibraryStarter, options: ErrorVitalsInitOptions) { const { Vue } = options; this.engineInstance = engineInstance; this.submitErrorUids = []; // 初始化 js錯誤 this.initJsError(); // 初始化 靜態資源載入錯誤 this.initResourceError(); // 初始化 Promise異常 this.initPromiseError(); // 初始化 HTTP請求異常 this.initHttpError(); // 初始化 跨域異常 this.initCorsError(); // 初始化 Vue異常 this.initVueError(Vue); }

// 封裝錯誤的上報入口,上報前,判斷錯誤是否已經發生過 errorSendHandler = (data: exceptionMetrics) => { // 統一加上 使用者行為追蹤 和 頁面基本資訊 const submitParams = { ...data, behaviorTracking: this.engineInstance.userInstance.behaviorTracking.get(), pageInformation: this.engineInstance.userInstance.metrics.get('page-information'), } as exceptionMetrics; // 判斷同一個錯誤在本次頁面訪問中是否已經發生過; const hasSubmitStatus = this.submitErrorUids.includes(submitParams.errorUid); // 檢查一下錯誤在本次頁面訪問中,是否已經產生過 if (hasSubmitStatus) return; this.submitErrorUids.push(submitParams.errorUid); // 一般來說,有報錯就立刻上報; console.log('準備上報-hasSubmitStatus-submitParams', hasSubmitStatus, submitParams); };

// 初始化 JS異常 的資料獲取和上報 initJsError = (): void => { //... 詳情程式碼在下 };

// 初始化 靜態資源異常 的資料獲取和上報 initResourceError = (): void => { //... 詳情程式碼在下 };

// 初始化 Promise異常 的資料獲取和上報 initPromiseError = (): void => { //... 詳情程式碼在下 };

// 初始化 HTTP請求異常 的資料獲取和上報 initHttpError = (): void => { //... 詳情程式碼在下 };

// 初始化 跨域異常 的資料獲取和上報 initCorsError = (): void => { //... 詳情程式碼在下 };

// 初始化 Vue異常 的資料獲取和上報 initVueError = (app: Vue): void => { //... 詳情程式碼在下 }; } ```

生成錯誤 uid

首先,什麼叫為每個錯誤生成 uid,這裡生成的 uid 有什麼用呢?答案其實很簡單:

  • 一次使用者訪問(頁籤未關閉),上報過一次錯誤後,後續產生重複錯誤不再上報
  • 多個使用者產生的同一個錯誤,在服務端可以歸類,分析影響使用者數、錯誤數等指標
  • 需要注意的是,對於同一個原因產生的同一個錯誤,生成的 uid 是相同的

ts // 對每一個錯誤詳情,生成一串編碼 export const getErrorUid = (input: string) => { return window.btoa(unescape(encodeURIComponent(input))); };

錯誤堆疊

在做錯誤監控之前,我們先來了解一下什麼是錯誤堆疊;我們寫程式碼經常報錯的時候能夠看到,下圖這樣子類似的錯誤,一個錯誤加上很多條很多條的呼叫資訊組成的錯誤;這就是丟擲的 Error物件 裡的 Stack錯誤堆疊,裡面包含了很多資訊:包括呼叫鏈檔名呼叫地址行列資訊等等;而在下文的錯誤捕獲中,我們也需要去對 Stack錯誤堆疊 進行解析;

248666eeb9c549a7af8a57a6a5022077.png

當然,解析這一長串的東西還是比較痛苦的,我這邊就給出我的解析方法以供參考

```js // 正則表示式,用以解析堆疊split後得到的字串 const FULL_MATCH = /^\sat (?:(.?) ?()?((?:file|https?|blob|chrome-extension|address|native|eval|webpack||[-a-z]+:|.bundle|\/).?)(?::(\d+))?(?::(\d+))?)?\s*$/i;

// 限制只追溯10個 const STACKTRACE_LIMIT = 10;

// 解析每一行 export function parseStackLine(line: string) { const lineMatch = line.match(FULL_MATCH); if (!lineMatch) return {}; const filename = lineMatch[2]; const functionName = lineMatch[1] || ''; const lineno = parseInt(lineMatch[3], 10) || undefined; const colno = parseInt(lineMatch[4], 10) || undefined; return { filename, functionName, lineno, colno, }; }

// 解析錯誤堆疊 export function parseStackFrames(error: Error) { const { stack } = error; // 無 stack 時直接返回 if (!stack) return []; const frames = []; for (const line of stack.split('\n').slice(1)) { const frame = parseStackLine(line); if (frame) { frames.push(frame); } } return frames.slice(0, STACKTRACE_LIMIT); } ```

呼叫 parseStackFrames() 方法將 error物件 傳入後,我們可以看到解析的效果還是可以的:

image.png

JS執行異常

什麼叫 JS執行異常 呢?其實很簡單,當 JavaScript執行時產生的錯誤 就屬於 JS執行異常

比如,我們未定義一個方法就直接呼叫它,它會報錯:Uncaught ReferenceError: xxx is not defined,這就屬於 JS執行異常

js noEmit(); // 沒有定義,直接呼叫 // 會報錯:Uncaught ReferenceError: noEmit is not defined

那麼,既然發生了錯誤,我們就需要去捕獲它;而捕獲JS執行異常有兩種方法:

方法一

我們可以使用 window.onerror 來捕獲全域性的 JS執行異常,window.onerror 是一個全域性變數,預設值為null。當有js執行時錯誤觸發時,window會觸發error事件,並執行 window.onerror(),藉助這個特性,我們對 window.onerror 進行重寫就可以捕獲到程式碼中的異常;

js window.onerror = (msg, url, row, col, error) => { const exception = { // 上報錯誤歸類 mechanism: { type: 'js' }, // 錯誤資訊 value: msg, // 錯誤型別 type: error.name || 'UnKnowun', // 解析後的錯誤堆疊 stackTrace: { frames: parseStackFrames(error), }, meta: { url, // 檔案地址 row, // 行號 col, // 列號 } }; // 獲取了報錯詳情,就可以走上報方法上報錯誤資訊 console.log('JS執行error', exception); return true; // 返回 true,阻止了預設事件執行,也就是原本將要在控制檯列印的錯誤資訊 };

方法二

我們還可以使用 window.addEventListener('error') 來捕獲 JS執行異常;它會比 window.onerror 先觸發

我們簡單封裝一下:

ts // 初始化 JS異常 的資料獲取和上報 initJsError = (): void => { const handler = (event: ErrorEvent) => { // 阻止向上丟擲控制檯報錯 event.preventDefault(); // 如果不是 JS異常 就結束 if (getErrorKey(event) !== mechanismType.JS) return; const exception = { // 上報錯誤歸類 mechanism: { type: mechanismType.JS, }, // 錯誤資訊 value: event.message, // 錯誤型別 type: (event.error && event.error.name) || 'UnKnowun', // 解析後的錯誤堆疊 stackTrace: { frames: parseStackFrames(event.error), }, // 使用者行為追蹤 behaviorTracking 在 errorSendHandler 中統一封裝 // 頁面基本資訊 pageInformation 也在 errorSendHandler 中統一封裝 // 錯誤的標識碼 errorUid: getErrorUid(`${mechanismType.JS}-${event.message}-${event.filename}`), // 附帶資訊 meta: { // file 錯誤所處的檔案地址 file: event.filename, // col 錯誤列號 col: event.colno, // row 錯誤行號 row: event.lineno, }, } as exceptionMetrics; // 一般錯誤異常立刻上報,不用快取在本地 this.errorSendHandler(exception); }; window.addEventListener('error', (event) => handler(event), true); };

兩者的區別和選用

閱讀了上文,我們瞭解到想要監控 JS執行異常 ,我們有兩種方法可以選用,那麼我們應該選用哪一種呢?或者說它們兩者方法之間有什麼區別呢?

  • 它們兩者均可以捕獲到 JS執行異常,但是 方法二除了可以監聽 JS執行異常 之外,還可以同時捕獲到 靜態資源載入異常
  • onerror 可以接受多個引數。而 addEventListener('error') 只有一個儲存所有錯誤資訊的引數

我這邊個人更加建議使用第二種 addEventListener('error') 的方式;原因很簡單:不像方法一可以被 window.onerror 重新覆蓋而且可以同時處理靜態資源錯誤

錯誤型別

細心的同學應該看見了,上文的捕獲中,有一個引數叫做 錯誤型別我們可以通過這個來快速判斷錯誤是基於什麼導致的,那麼 JS執行時的錯誤型別常見的有哪些呢?

| 型別 | 含義 | 說明 | | --- | --- |--- | | SyntaxError | 語法錯誤 | 語法錯誤 | | ReferenceError | 引用錯誤 | 常見於引用了一個不存在的變數: let a = undefinedVariable; | | RangeError | 有效範圍錯誤 | 數值變數或引數超出了其有效範圍。 常見於 1.建立一個負長度陣列 2.Number物件的方法引數超出範圍:let b = new Array(-1) | | TypeError | 型別錯誤 | 常見於變數或引數不屬於有效型別 let foo = 3;foo(); | | URIError | URL處理函式錯誤 | 使用全域性URL處理函式錯誤,比如 decodeURIComponent('%'); |

  • 這裡有一個點需要特別注意,我們主觀感覺上的 SyntaxError 語法錯誤,除了用 eval() 執行的指令碼以外,一般是不可以被捕獲到的,比如我們編寫一個正常的語法錯誤

js const d d = 1; // 控制檯報錯 :Uncaught SyntaxError: Missing initializer in const declaration // 但是上述的捕獲方法無法正常捕獲錯誤;

  • 這明顯上是一個語法上的錯誤,但是我們上述的 兩個錯誤捕獲方法都沒辦法捕獲到錯誤
  • 只有在程式碼中通過 eval() 執行的程式碼指令碼才可以正常捕獲到錯誤資訊;

js eval('ddd fff'); // 控制檯報錯 VM149:1 Uncaught SyntaxError: Unexpected identifier // 上文的錯誤捕獲方法可以正常捕獲到錯誤;

  • 那麼,WHY

其實原因很簡單, const d d = 1; 這種語法錯誤,在編譯解析階段就已經報錯了,而擁有語法錯誤的指令碼不會放入任務佇列進行執行,自然也就不會有錯誤冒泡到我們的捕獲程式碼;而我們使用 eval();在編譯解析階段一切正常,直到執行的時候才進行報錯,自然我們就可以捕獲到這段錯誤;

當然,現在程式碼檢查這麼好用,早在編寫程式碼時這種語法錯誤就被避免掉了,一般我們碰不上語法錯誤的~

靜態資源載入異常

有的時候,我們介面上的 img圖片CDN資源 突然失效了、打不開了,就比如以下面這個為例子,我們往html中放進一個img,把它的路徑設為請求不到的地址:

js <img src="http://localhost:8888/nottrue.jpg"> // 會報錯 GET http://localhost:8888/nottrue.jpg net::ERR_CONNECTION_REFUSED

那我們怎麼去捕獲到這種請求不到資源的、或者說靜態資源失效的報錯呢?很簡單,只需要祭出 window.addEventListener('error') 就可以了

```ts // 靜態資源錯誤的 ErrorTarget export interface ResourceErrorTarget { src?: string; tagName?: string; outerHTML?: string; }

// 初始化 靜態資源異常 的資料獲取和上報 initResourceError = (): void => { const handler = (event: Event) => { event.preventDefault(); // 阻止向上丟擲控制檯報錯 // 如果不是跨域指令碼異常,就結束 if (getErrorKey(event) !== mechanismType.RS) return; const target = event.target as ResourceErrorTarget; const exception = { // 上報錯誤歸類 mechanism: { type: mechanismType.RS, }, // 錯誤資訊 value: '', // 錯誤型別 type: 'ResourceError', // 使用者行為追蹤 behaviorTracking 在 errorSendHandler 中統一封裝 // 頁面基本資訊 pageInformation 也在 errorSendHandler 中統一封裝 // 錯誤的標識碼 errorUid: getErrorUid(${mechanismType.RS}-${target.src}-${target.tagName}), // 附帶資訊 meta: { url: target.src, html: target.outerHTML, type: target.tagName, }, } as exceptionMetrics; // 一般錯誤異常立刻上報,不用快取在本地 this.errorSendHandler(exception); }; window.addEventListener('error', (event) => handler(event), true); }; ```

使用 addEventListener 捕獲資源錯誤時,一定要將 第三個選項設為 true,因為資源錯誤沒有冒泡,所以只能在捕獲階段捕獲。同理,由於 window.onerror 是通過在冒泡階段捕獲錯誤,所以無法捕獲資源錯誤。

Promise異常

什麼叫 Promise異常 呢?其實就是我們使用 Promise 的過程中,當 Promise 被 reject 且沒有被 catch 處理的時候,就會丟擲 Promise異常;同樣的,如果我們在使用 Promise 的過程中,報了JS的錯誤,同樣也被以 Promise異常 的形式丟擲:

下面我舉兩個會產生 Promise異常 的例子

js Promise.resolve().then(() => console.log(c)); // Uncaught (in promise) ReferenceError: c is not defined Promise.reject('reject了但是沒有處理!') // Uncaught (in promise) reject了但是沒有處理!

而當丟擲 Promise異常 時,會觸發 unhandledrejection 事件,所以我們只需要去監聽它就可以進行 Promise 異常 的捕獲了,不過值得注意的一點是:相比與上面所述的直接獲取報錯的行號、列號等資訊Promise異常 我們只能捕獲到一個 報錯原因 而已;

``ts // 初始化 Promise異常 的資料獲取和上報 initPromiseError = (): void => { const handler = (event: PromiseRejectionEvent) => { event.preventDefault(); // 阻止向上丟擲控制檯報錯 const value = event.reason.message || event.reason; const type = event.reason.name || 'UnKnowun'; const exception = { // 上報錯誤歸類 mechanism: { type: mechanismType.UJ, }, // 錯誤資訊 value, // 錯誤型別 type, // 解析後的錯誤堆疊 stackTrace: { frames: parseStackFrames(event.reason), }, // 使用者行為追蹤 behaviorTracking 在 errorSendHandler 中統一封裝 // 頁面基本資訊 pageInformation 也在 errorSendHandler 中統一封裝 // 錯誤的標識碼 errorUid: getErrorUid(${mechanismType.UJ}-${value}-${type}`), // 附帶資訊 meta: {}, } as exceptionMetrics; // 一般錯誤異常立刻上報,不用快取在本地 this.errorSendHandler(exception); };

window.addEventListener('unhandledrejection', (event) => handler(event), true); }; ```

HTTP請求異常

HTTP請求的捕獲,我在前文中已經寫過程式碼,可以回翻: 一文摸清前端監控實踐要點(二)行為監控 HTTP 請求捕獲

所謂 Http請求異常 也就是非同步請求 HTTP 介面時的異常罷了,比如我呼叫了一個登入介面,但是我的傳參不對,登入介面給我返回了 500 錯誤碼,其實這個時候就已經產生了異常了;

是否屬於 Promise異常

看到這裡,其實有的同學可能會疑惑,我們現在的呼叫 HTTP 介面,一般也就是通過 async/await 這種基於Promise的解決非同步的最終方案;那麼,假如說請求了一個介面地址報了500,因為是基於 Promise 呼叫的介面,我們能夠在上文的 Promise異常 捕獲中,獲取到一個錯誤資訊(如下圖);

但是有一個問題別忘記了,Promise異常捕獲沒辦法獲取報錯的行列,我們只知道 Promise 報錯了,報錯的資訊是 介面請求500;但是我們根本不知道是哪個介面報錯了

js 1e3b1763cf19402fbd0988d356fcd590.png

所以說,我們對於 Http請求異常 的捕獲需求就是:全域性統一監控報錯的具體介面請求狀態碼請求耗時以及請求引數等等;

而為了實現上述的監控需求,我們需要了解到:現在非同步請求的底層原理都是呼叫的 XMLHttpRequest 或者 Fetch,我們只需要對這兩個方法都進行 劫持 ,就可以往介面請求的過程中加入我們所需要的一些引數捕獲;

程式碼實現

ts // 初始化 HTTP請求異常 的資料獲取和上報 initHttpError = (): void => { const loadHandler = (metrics: httpMetrics) => { // 如果 status 狀態碼為 401 和 200,說明沒有 HTTP 請求錯誤 if (metrics.status === 401 || metrics.status === 200) return; const value = metrics.response; const exception = { // 上報錯誤歸類 mechanism: { type: mechanismType.HP, }, // 錯誤資訊 value, // 錯誤型別 type: 'HttpError', // 錯誤的標識碼 errorUid: getErrorUid(`${mechanismType.HP}-${value}-${metrics.statusText}`), // 附帶資訊 meta: { metrics, }, } as exceptionMetrics; // 一般錯誤異常立刻上報,不用快取在本地 this.errorSendHandler(exception); }; proxyXmlHttp(null, loadHandler); proxyFetch(null, loadHandler); };

跨域指令碼錯誤

介紹

還有一種錯誤,平常我們較難遇到,那就是 跨域指令碼錯誤 ,什麼叫 跨域指令碼錯誤 呢?比如說我們新建一個texterror.js 檔案到 專案B 的 public 目錄下以供外部訪問;

js // 新建的 texterror.js 檔案 const a = new Array(-1);

可以看到,我們在 texterror.js 檔案中寫了一行會報錯的程式碼,認真看過上文的同學應該知道,它會被捕獲在 JS執行異常中,且錯誤型別為 RangeError ;而我們從 專案A 中引入它;

```js // 專案B的地址,和專案A埠不同;

```

載入後執行,我們自然能在控制檯發現報錯:而我們上文的程式碼捕獲也有錯誤捕獲到:

99e47b449f444af3be512799d4933ddf.png

image.png

但是我們發現,這裡的 msg 資訊是 Script error,也沒有獲取到行號列號檔名等的資訊,這是怎麼回事呢?

其實這是瀏覽器的一個安全機制當跨域載入的指令碼中發生語法錯誤時,瀏覽器出於安全考慮,不會報告錯誤的細節,而只報告簡單的 Script error。瀏覽器只允許同域下的指令碼捕獲具體錯誤資訊,而其他指令碼只知道發生了一個錯誤,但無法獲知錯誤的具體內容(控制檯仍然可以看到,JS指令碼無法捕獲),我們上文通過專案A去載入專案B的檔案,自然產生了跨域;

處理

其實對於三方指令碼的錯誤,我們是否捕獲都可以,不過我們需要一點處理,如果不需要捕獲的話,就不進行上報,如果需要捕獲的話,只上報型別:

我們對上文的 window.addEventListener('error') 再加上對跨域資源的判斷,以和正常的程式碼中錯誤區分開;

ts // 初始化 跨域異常 的資料獲取和上報 initCorsError = (): void => { const handler = (event: ErrorEvent) => { // 阻止向上丟擲控制檯報錯 event.preventDefault(); // 如果不是跨域指令碼異常,就結束 if (getErrorKey(event) !== mechanismType.CS) return; const exception = { // 上報錯誤歸類 mechanism: { type: mechanismType.CS, }, // 錯誤資訊 value: event.message, // 錯誤型別 type: 'CorsError', // 錯誤的標識碼 errorUid: getErrorUid(`${mechanismType.JS}-${event.message}`), // 附帶資訊 meta: {}, } as exceptionMetrics; // 自行上報異常,也可以跨域指令碼的異常都不上報; this.errorSendHandler(exception); }; window.addEventListener('error', (event) => handler(event), true); };

補充

看到了這裡,可能還有的同學想了解:那麼這種跨域的指令碼錯誤我們就沒有辦法進行獲取錯誤詳情嗎?答案還是有的:

我們只需要 開啟跨域資源共享CORS(Cross Origin Resource Sharing),就可以捕獲錯誤了~我們需要分兩步來進行實現:

  1. 新增crossorigin="anonymous"屬性。

```js

`` 2. **新增跨域HTTP響應頭`。**

js Access-Control-Allow-Origin: *

這兩步完成後,允許了跨域,我們就可以在錯誤捕獲指令碼中獲取到具體的錯誤資訊拉!

Vue2、Vue3 錯誤捕獲

  • Vue2 如果在元件渲染時出現執行錯誤,錯誤將會被傳遞至全域性 Vue.config.errorHandler 配置函式;
  • Vue3Vue2,如果在元件渲染時出現執行錯誤,錯誤將會被傳遞至全域性的 app.config.errorHandler 配置函式;

我們可以利用這兩個鉤子函式來進行錯誤捕獲,由於是依賴於 Vue配置函式 的錯誤捕獲,所以我們在初始化時,需要使用者將 Vue例項 傳進來;

獲取報錯元件名

```ts export interface Vue { config: { errorHandler?: any; warnHandler?: any; }; }

export interface ViewModel { _isVue?: boolean; __isVue?: boolean; $root: ViewModel; $parent?: ViewModel; $props: { [key: string]: any }; $options: { name?: string; propsData?: { [key: string]: any }; _componentTag?: string; __file?: string; }; }

// 獲取報錯元件名 const classifyRE = /(?:^|[-])(\w)/g; const classify = (str: string) => str.replace(classifyRE, (c) => c.toUpperCase()).replace(/[-]/g, ''); const ROOT_COMPONENT_NAME = ''; const ANONYMOUS_COMPONENT_NAME = ''; export const formatComponentName = (vm: ViewModel, includeFile: Boolean) => { if (!vm) { return ANONYMOUS_COMPONENT_NAME; } if (vm.$root === vm) { return ROOT_COMPONENT_NAME; } const options = vm.$options; let name = options.name || options._componentTag; const file = options.__file; if (!name && file) { const match = file.match(/([^/\]+).vue$/); if (match) { name = match[1]; } } return ( (name ? <${classify(name)}> : ANONYMOUS_COMPONENT_NAME) + (file && includeFile !== false ? at ${file} : '') ); }; ```

初始化封裝

ts // 只需要在外部把初始化好的 Vue 物件傳入即可~ // 初始化 Vue異常 的資料獲取和上報 initVueError = (app: Vue): void => { app.config.errorHandler = (err: Error, vm: ViewModel, info: string): void => { const componentName = formatComponentName(vm, false); const exception = { // 上報錯誤歸類 mechanism: { type: mechanismType.VUE, }, // 錯誤資訊 value: err.message, // 錯誤型別 type: err.name, // 解析後的錯誤堆疊 stackTrace: { frames: parseStackFrames(err), }, // 錯誤的標識碼 errorUid: getErrorUid(`${mechanismType.JS}-${err.message}-${componentName}-${info}`), // 附帶資訊 meta: { // 報錯的Vue元件名 componentName, // 報錯的Vue階段 hook: info, }, } as exceptionMetrics; // 一般錯誤異常立刻上報,不用快取在本地 this.errorSendHandler(exception); }; };

React 錯誤捕獲

React 一樣也有官方提供的錯誤捕獲,見文件:https://zh-hans.reactjs.org/docs/react-component.html#componentdidcatch

Vue 不同的是,我們需要自己定義一個高階元件暴露給專案使用,我這裡就不具體詳寫了,感興趣的同學可以自己進行補全:

js import * as React from 'react'; class ErrorBoundary extends React.Component { constructor(props) { super(props); } // ... componentDidCatch(error, info) { // "元件堆疊" 例子: // in ComponentThatThrows (created by App) // in ErrorBoundary (created by App) // in div (created by App) // in App } // ... }

專案使用方只需要這樣既可:

```js import React from "react";

; ```

Source Map

我們的專案想要部署上線,就需要將專案原始碼經過混淆壓縮babel編譯轉化等等的操作之後,生成最終的打包產物,再進行線上部署;而這樣混淆後的程式碼,我們基本上無法閱讀,即使在上文的錯誤監控裡,我們獲取了報錯程式碼的行號、列號等關鍵資訊,我們也無法找到具體的原始碼位置所在;這個時候就需要請出我們的 Sourcemap

Sourcemap 本質上是一個資訊檔案,裡面儲存著程式碼轉換前後的對應位置資訊。它記錄了轉換壓縮後的程式碼所對應的轉換前的原始碼位置,是原始碼和生產程式碼的對映。

我們通過種種打包工具打包後,如果開啟了 Sourcemap 功能,就會在打包產物裡發現字尾為 .map 的檔案,通過對它的解析,我們就可以得到專案的原始碼;

  • 我這裡舉例一個通過 nodejs 進行 SourceMap 解析的例子程式碼:

```js // 這裡因為 npm 裝了 babel,所以用的 import,正常 nodejs 下為 require import sourceMap from 'source-map'; //source-map庫 import fs from 'fs' //fs為nodejs讀取檔案的庫 import rp from 'request-promise'

/ * @description: 用來解析 sourcemap 的函式方法 * @param {} sourceMapFile 傳入的 .map 原始檔 * @param {} line 報錯行數 * @param {} column 報錯列數 * @param {} offset 需要擷取幾行的程式碼 * @return {} / export const sourceMapAnalysis = async (sourceMapFile, line, column, offset) => { // 通過 sourceMap 庫轉換為sourceMapConsumer物件 const consumer = await new sourceMap.SourceMapConsumer(sourceMapFile); // 傳入要查詢的行列數,查詢到壓縮前的原始檔及行列數 const sm = consumer.originalPositionFor({ line, // 壓縮後的行數 column, // 壓縮後的列數 }); // 壓縮前的所有原始檔列表 const { sources } = consumer; // 根據查到的source,到原始檔列表中查詢索引位置 const smIndex = sources.indexOf(sm.source); // 到原始碼列表中查到原始碼 const smContent = consumer.sourcesContent[smIndex]; // 將原始碼串按"行結束標記"拆分為陣列形式 const rawLines = smContent.split(/\r?\n/g); let begin = sm.line - offset; const end = sm.line + offset + 1; begin = begin < 0 ? 0 : begin; const context = rawLines.slice(begin, end); // 可以根據自己的需要,在末尾處加上 \n // const context = rawLines.slice(begin, end).join('\n'); // 銷燬 consumer.destroy(); return { // 報錯的具體程式碼 context, // 報錯在檔案的第幾行 originLine: sm.line + 1, // line 是從 0 開始數,所以 +1 // source 報錯的檔案路徑 source: sm.source, } };

// 請求線上的 .map 檔案進行解析 export const loadMapFileByUrl = async (url)=>{ return await rp(url) } const line = 9; const column = 492621; const rawSourceMap = JSON.parse( // 這裡載入在本地的 .map 檔案 fs.readFileSync('./xxxxxxxxxxxxxxx.map','utf-8').toString() // 路徑自擬 ); const inlineSourceMap = JSON.parse(await loadMapFileByUrl('http://xxxxxxxxxxxx.map')) // 路徑自換

// 從url獲取 sourcemap 檔案 // const res = await sourceMapAnalysis(inlineSourceMap,line,column,2) // 從本地獲取 sourcemap 檔案 const res = await sourceMapAnalysis(rawSourceMap,line,column,2)

console.log(res); ```

效果如下:

d35fa52dcdb1439ebb64a112223b0848.png

注意:使用 Sourcemap 的同學注意在打包的時候,將 .map 檔案和部署產物分離,不能部署到線上地址哦! 如果你將 .map 部署上去了,那麼你專案的程式碼也就是直接明文跑在網頁上,誰都可以檢視未混淆的原始碼拉!

參考閱讀

React componentDidCatch

Vue errorHandler

sentry-javascript 原始碼