首頁 > 軟體

Qiankun Sentry 監控異常上報無法自動區分專案解決

2022-11-21 14:01:59

前言

最近專案組決定將前端異常監控由 Fundebug 切換為 Sentry。整個切換過程可以說非常簡單,部署一個後臺服務,然後將 Sentry SDK 整合到前端應用中就完事兒了。在之後的使用過程中,小編遇到了一個問題。由於我們的專案採用的是基於 qiankun 的微前端架構,在應用使用過程中,常常會出現發生異常應用和上報應用不匹配的情況。

為了解決這個問題,小編先去 qiankunissue 下翻了翻,看有沒有好的解決方案。雖然也有不少人遇到了同樣的問題 - 求教一下 主子應用的sentry應該如何實踐 #1088,但是社群裡並沒有一個好的解決方案。於是乎小編決定自己去閱讀 Sentry 原始碼和官方檔案,期望能找到一種合理並通用的解決方案。

經過一番梳理,小編如願找到了解決方案,並且效果還不錯。接下來小編就帶著大家一起了解一下整個解決方案的具體情況。

使用 Sentry 上報異常

在正式介紹解決方案之前,小編先帶大家簡單回顧一下一個前端應用是如何接入 Sentry 的。

第一步,在 Sentry 監控平臺構建一個專案

專案建立好以後,會自動生成一個 dsn,這個 dsn 會在前端專案接入 Sentry 時作為必填項傳入。

第二步,前端應用接入 Sentry

前端應用接入 Sentry 也非常簡單,只要使用 Sentry 提供的 init api,傳入必傳的 dsn 就可以了。

import React from "react";
import ReactDOM from "react-dom";
import * as Sentry from "@sentry/react";
import { Integrations } from "@sentry/tracing";
import App from "./App";
Sentry.init({
  dsn: "https://90eb5fc98bf447a3bdc38713cc253933@sentry.byai.com/66",
  integrations: [new Integrations.BrowserTracing()],
  tracesSampleRate: 1.0,
});
ReactDOM.render(<App />, document.getElementById("root"));

經過這兩步,前端應用的異常監控接入就完成了。當應用在使用時,如果發生異常,Sentry 會自動捕獲異常,然後上報到監控平臺。上報完成以後,我們就可以在專案的 issues 中檢視異常並著手修復。

單個的 Spa 應用接入 Sentry 時按照上面的步驟無腦操作就可以了,但如果應用是基於 qiankun 的微前端架構,那就需要解決異常上報不匹配的問題了。

小編手上的專案就是採用了基於 qiankun 的微前端架構,一個頁面會至少同時存在兩個應用,有時甚至會有 3 到 4 個應用。在應用使用過程中,常常會出現異常上報不匹配的問題。

如上圖所示,主應用、cc sdk 應用中的異常都會上報到 aicc 專案中,這給例外處理帶來很大的困擾。

出現這個問題的原因也非常好理解。

Sentry 在執行 init 方法時會通過覆寫 window.onerrorwindow.unhandledrejection 的方式初始化異常捕獲邏輯。之後不管是哪個應用發生異常,都最終會觸發 onerrorunhandledrejectioncallback 而被 Sentry 感知,然後上報到 dsn 指定的專案中。而且 Sentryinit 程式碼不管是放在主應用中,還是放在子應用裡面,都沒有質的改變,所有被捕獲的異常還是會一股腦的上報到某個專案中,無法自動區分。

瞭解了異常上報無法自動區分的問題,接下來小編就給大家講一下自己是如何解決這個問題的。

解決方案

想要解決這個問題,我們必須要先找到問題的切入點,而異常上報時的介面呼叫就是這個切入點。

Sentry 捕獲到應用產生的異常時,會呼叫一個介面來上報異常,如下:

對比這個介面的 url 和上報應用的 dsn,我們可以發現異常上報介面的 url 其實是由上報應用的 dsn 轉化來的,轉化過程如下:

// https://62187b367e474822bb9cb733c8a89814@sentry.byai.com/56
dsn - https://{param1}@{param2}/{param3}
                |
                |
                v
url - https://{param2}/api/{param3}/store/?sentry_key={param1}&sentry_version=7

我們再來看看這個上報介面攜帶的引數:

在介面引數中,exceptions.values[0].stacktrace.frames 是異常的追蹤棧資訊。通過棧資訊中的 filename 欄位,我們可以知道發生異常的 js 檔案的 url。通常情況下,微前端中各個子應用的 jsurl 字首是不相同的(各個子應用靜態檔案的位置是分離的),那麼根據發生異常的 jsurl 就可以判斷該異常屬於哪個應用。

有了上面兩個資訊,異常上報自動區分的解決方案就清晰明瞭了:

  • 第一步攔截異常上報介面,拿到異常詳情,根據追蹤棧中的 filename 判斷異常屬於哪個應用;
  • 第二步,根據匹配應用的 dsn 重新構建 url
  • 第三步,使用新的 url 上報異常;

在這個方案中,最關鍵的是攔截異常上報介面。為了能實現這一步,小編進行了各種嘗試。

失敗的方案一

由於 Sentry 異常上報是通過 window.fetch(url, options) 來實現的,所以我們可以通過覆寫 window.fetch 的方式去攔截異常上報。

程式碼實現如下:

const originFetch = window.fetch;
window.fetch = (url, options) => {
    // 根據 options 中的異常資訊,返回新的 url 和 options
    const [newUrl, newOptions] = sentryFilter(url, options);
    // 使用原生的 fetch
    return originFetch(newUrl, newOptions);
}

該方案看起來很靠譜,然而在實際使用的時候並未發揮作用,原因是 Sentry 內部只會使用原生的 fetch。如果發現 fetch 方法被覆寫,那麼 Sentry 會通過自己的方式重新去獲取原生的 fetch

小編擷取了 Sentry 的部分原始碼給大家看一下:

// FetchTransport 是一個建構函式
// Sentry 在執行 init 方法時會構建一個 FetchTransport 範例,然後通過這個 FetchTransport 範例呼叫 window.fetch 方法去做異常上報
function FetchTransport(options, fetchImpl) {
    if (fetchImpl === void 0) { fetchImpl = getNativeFetchImplementation(); }
    var _this = _super.call(this, options) || this;
    _this._fetch = fetchImpl;
    return _this;
}
// 使用原生的 window.fetch 實現 FetchTransport
function getNativeFetchImplementation() {
    if (cachedFetchImpl) {
        return cachedFetchImpl;
    }
    // 根據 isNativeFetch 來判斷 window.fetch 是否被覆寫
    if (isNativeFetch(global$7.fetch)) {
        return (cachedFetchImpl = global$7.fetch.bind(global$7));
    }
    var document = global$7.document;
    var fetchImpl = global$7.fetch;
    // 如果被覆寫,藉助 iframe 獲取原生的 window.fetch
    if (document && typeof document.createElement === 'function') {
        try {
            var sandbox = document.createElement('iframe');
            sandbox.hidden = true;
            document.head.appendChild(sandbox);
            var contentWindow = sandbox.contentWindow;
            if (contentWindow && contentWindow.fetch) {
                fetchImpl = contentWindow.fetch;
            }
            document.head.removeChild(sandbox);
        }
        catch (e) {
            logger.warn('Could not create sandbox iframe for pure fetch check, bailing to window.fetch: ', e);
        }
    }
    return (cachedFetchImpl = fetchImpl.bind(global$7));
}
// 判斷 window.fetch 是否已經被覆寫
function isNativeFetch(func) {
    return func && /^function fetch()s+{s+[native code]s+}$/.test(func.toString());
}

由於 Sentry 內部有一套邏輯來保證 fetch 必須為原生方法,所以覆寫 window.fetch 的方案失敗, pass

不通用的方案二

既然覆寫 window.fetch 的方案行不通,那我們就重新想辦法。

觀察上面的 FetchTransport 的入參。如果沒有指定 fetchImplSentry 會通過 getNativeFetchImplementation 來實現一個 fetchImpl。那我們主動給 FetchTransport 傳遞覆寫以後的 fetch 方法,不就可以做到攔截 fetch 呼叫了嗎?

這個方案看起來也很靠譜,趕緊試一下,


IT145.com E-mail:sddin#qq.com