Flutter異常監控 - 叄 | 從bugsnag源碼學習如何追溯異常產生路徑

来源:https://www.cnblogs.com/xuge2it/archive/2023/01/06/17029577.html
-Advertisement-
Play Games

如果覺得文章對你有幫助,點贊、收藏、關註、評論,一鍵四連支持,你的支持就是我創作最大的動力。 ❤️ 本文原創聽蟬 公眾號:碼里特別有禪 歡迎關註原創技術文章第一時間推送 ❤️ 前言 沒錯,繼Flutter 異常監控 | 框架 Catcher 原理分析 之後,帶著那顆騷動的好奇心我又搗鼓著想找其他 F ...


如果覺得文章對你有幫助,點贊、收藏、關註、評論,一鍵四連支持,你的支持就是我創作最大的動力。

❤️ 本文原創聽蟬 公眾號:碼里特別有禪 歡迎關註原創技術文章第一時間推送  ❤️

前言

沒錯,繼Flutter 異常監控 | 框架 Catcher 原理分析 之後,帶著那顆騷動的好奇心我又搗鼓著想找其他 Flutter 異常監控框架讀讀,看能不能找到一些好玩的東西,於是在官方介紹第三方庫里發現了這貨Bugsnag,大致掃了下源碼發現 flutter 側主流程很簡單沒啥東西可看滴,因為這貨強烈依賴對端能力,Flutter 異常捕獲之後就無腦拋給對端 SDK 自己啥都不幹 ,拋開 Bugsnag 這種處理異常的方式不論,源碼里卻也有一些之我見的亮度值得借鑒和學習,比如本文主要介紹 Bugsnag 如何追溯異常路徑的設計思想和實現,對異常捕獲的認識有不少幫助。

Bugsnag

功能簡介

在介紹可追溯異常路徑設計之前,有必要先科普下 Bugsnag 是什麼? 讓大佬們有一個大局觀,畢竟後面介紹內容只是其中一個小的點。

Bugsnag 跟 Catcher 一樣也是 Flutter 異常監控框架,Bugsnag-flutter 只是殼,主要作用有:

  1. 規範多平臺(安卓,ios)異常調用和上報的介面。
  2. 拿到 flutter 異常相關數據傳遞給對端。

主要支持功能:

  1. dart 側異常支持手動和自動上報。
  2. 支持上報數據序列化,有網環境下會繼續上報。
  3. 支持記錄用戶導航步驟,自定義關鍵節點操作,網路異常自動上報。

這個框架的側重點跟 Catcher 完全不同,它不支持異常的 UI 客戶端自定義顯示,也不支持對異常的定製化處理。說白了就是你想看異常就只能登陸到Bugsnag 後臺看到,後臺有套餐包括試用版和收費版(你懂滴)。

基本使用

void main() async => bugsnag.start(
      runApp: () => runApp(const ExampleApp()),
      // 需要到bugsang後臺註冊賬號申請一個api_key
      apiKey: 'add_your_api_key_here',
      projectPackages: const BugsnagProjectPackages.only({'bugsnag_example'}),
      // onError callbacks can be used to modify or reject certain events
      //...
    );

class ExampleApp extends StatelessWidget {
  const ExampleApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      navigatorObservers: [BugsnagNavigatorObserver()],
      initialRoute: '/',
      routes: {
        '/': (context) => const ExampleHomeScreen(),
        '/native-crashes': (context) => const NativeCrashesScreen(),
      },
    );
  }
}
// Use leaveBreadcrumb() to log potentially useful events in order to
  // understand what happened in your app before each error.
  void _leaveBreadcrumb() async =>
      bugsnag.leaveBreadcrumb('This is a custom breadcrumb',
          // Additional data can be attached to breadcrumbs as metadata
          metadata: {'from': 'a', 'to': 'z'});
import 'package:bugsnag_breadcrumbs_http/bugsnag_breadcrumbs_http.dart' as http;
void _networkFailure() async =>
      http.post(Uri.parse('https://example.com/invalid'));

後臺效果展示

Untitled.png
Flutter 異常顯示頁

Untitled 1.png
bugsnag 後臺 Breadcrumbs 頁顯示內容:可以看到路徑中包含了當前頁面信息,請求信息和關鍵步驟,異常生成的路徑和時間點

異常捕獲框架閱讀通用套路

在異常上報主流程之前,必要的通用套路不能忘,按照這個思路來追源碼事半功倍,如下:

  1. Flutter 異常監控點

三把斧:FlutterError.onError ,addErrorListener,runZonedGuarded 詳見:不得不知道的 Flutter 異常捕獲知識點:Zone 中 Zone 異常捕獲小節。

  1. 針對 Error 的包裝類生成

我們最好不要直接使用 onError 參數中的 error 和 stack 欄位,因為為方便問定位一般原始 Error 會經過各種轉換增加附加信息更容易還原異常現場,比如設備 id 等,對比 Catcher 中這個經過包裝的對象叫Report

  1. 操作包裝類

上面最終生成的包裝類對象會經過一些操作,操作主要三個方面:顯示、存儲、上報。拿 Catcher 來舉例子,它包含了 UI 顯示和上報兩個。一般在項目中可能顯示不那麼重要,最重要的是存儲和上報。

Bugsnag 主要流程源碼簡析

主要領略下”異常捕獲通用套路” 大法有多香:

找監控點

這個流程中少了 addErrorListener,說明 bugsnag 對 isolate 異常是監控不到滴。

Future<void> start({
    FutureOr<void> Function()? runApp,
    //... Tag1 一堆額外參數
  }) async {
    //...
    //開始就想著用對端SDK,這裡當然少不了初始化通道
    _runWithErrorDetection(
      detectDartErrors,
      () => WidgetsFlutterBinding.ensureInitialized(),
    );

    //...

    await ChannelClient._channel.invokeMethod('start', <String, dynamic>{
      //... Tag2:這裡將Tag1處的額外參數傳給了對端SDK

    });

   //Tag3:dart error的處理類,其中全部都是通過channel來橋接的
    final client = ChannelClient(detectDartErrors);
    client._onErrorCallbacks.addAll(onError);
    this.client = client;


    _runWithErrorDetection(detectDartErrors, () => runApp?.call());
  }

  void _runWithErrorDetection(
    bool errorDetectionEnabled,
    FutureOr<void> Function() block,
  ) async {
    if (errorDetectionEnabled) {
      //多麼熟悉的味道,
      await runZonedGuarded(() async {
        await block();
      }, _reportZonedError);
    } else {
      await block();
    }
  }

//最終_reportZonedError會執行到_notifyInternal
void _notifyUnhandled(dynamic error, StackTrace? stackTrace) {
    _notifyInternal(error, true, null, stackTrace, null);
  }
ChannelClient(bool autoDetectErrors) {
    if (autoDetectErrors) {
      FlutterError.onError = _onFlutterError;
    }
  }

void _onFlutterError(FlutterErrorDetails details) {
    _notifyInternal(details.exception, true, details, details.stack, null);
    //...
  }

找包裝類生成

Future<void> _notifyInternal(
    dynamic error,
    bool unhandled,
    FlutterErrorDetails? details,
    StackTrace? stackTrace,
    BugsnagOnErrorCallback? callback,
  ) async {
    final errorPayload =
        BugsnagErrorFactory.instance.createError(error, stackTrace);
    final event = await _createEvent(
      errorPayload,
      details: details,
      unhandled: unhandled,
      deliver: _onErrorCallbacks.isEmpty && callback == null,
    );

   //...

    await _deliverEvent(event);
  }

//我說什麼來著:連最基本的Event構造,都是在對端。
Future<BugsnagEvent?> _createEvent(
    BugsnagError error, {
    FlutterErrorDetails? details,
    required bool unhandled,
    required bool deliver,
  }) async {
    final buildID = error.stacktrace.first.codeIdentifier;
    //...
    };
    //調用了對端通道方法來實現。
    final eventJson = await _channel.invokeMethod(
      'createEvent',
      {
        'error': error,
        'flutterMetadata': metadata,
        'unhandled': unhandled,
        'deliver': deliver
      },
    );

    if (eventJson != null) {
      return BugsnagEvent.fromJson(eventJson);
    }

    return null;
  }

操作包裝類

本來以為此處要大幹一場,結果灰溜溜給了對端。。。,什麼都不想說,內心平靜毫無波瀾~~~

Future<void> _deliverEvent(BugsnagEvent event) =>
      _channel.invokeMethod('deliverEvent', event);

主要源碼流程看完了,下麵來看下 Bugsnag 我覺得比較好玩的需求和實現。

什麼是可追溯異常路徑

這個是我自己想的一個詞,該需求目的是能完整記錄用戶操作的整個行為路徑,這樣達到清晰指導用戶操作過程,對問題的定位很有幫助。可以理解成一個小型的埋點系統,只是該埋點系統只是針對異常來做的。

如下:異常產生流程,state 被成功載入後用戶先進入了主頁,然後從主頁進入了 native-crashes 頁之後異常就產生了。 對開發者和測試人員來說很容易復現通過如上路徑來複現問題。

Untitled 2.png異常路徑後臺顯示效果

如何實現

前置知識

Bugsnag 中將可追溯的路徑命名為 Breadcrumb,剛開始我不理解,這個單詞英文意思:麵包屑,跟路徑八竿子都扯不上關係,直到查維基百科才發現為什麼這麼命名,通過一片一片的麵包屑才能找到回家的路。。。,老外們還真夠有情懷的!

Breadcrumb 的命名的含義, 有沒有發覺這個名字起得好形象!

頁面路徑(英語:breadcrumb 或 breadcrumb trail/navigation),又稱麵包屑導航,是在用戶界面中的一種導航輔助。它是用戶一個在程式或文件中確定和轉移他們位置的一種方法。麵包屑這個詞來自糖果屋 這個童話故事;故事中,漢賽爾與葛麗特企圖依靠灑下的麵包屑找到回家的路。

當然最終這些丟下的麵包屑(leaveBreadcrumb)路徑數據也是通過調用到對端 SDK 來實現:

Future<void> leaveBreadcrumb(
    String message, {
    Map<String, Object>? metadata,
    BugsnagBreadcrumbType type = BugsnagBreadcrumbType.manual,
  }) async {
    final crumb = BugsnagBreadcrumb(message, type: type, metadata: metadata);
    await _channel.invokeMethod('leaveBreadcrumb', crumb);
  }

這裡主要關註下自動添加麵包屑的場景。

如何添加路徑

兩種方式:

  1. 手動添加,通過調用 bugsnag.leaveBreadcrumb

  2. 自動添加,其中包括兩個場景:導航欄跳轉和 網路請求

如上兩個場景的的實現原理涉及到對應用性能的監控功能,重點分析其中原理。

導航欄自動埋點實現原理

MaterialApp: navigatorObservers 來實現對頁面跳轉的監聽,Bugsnag 中是通過自定義 BugsnagNavigatorObserver,併在其回調函數中監聽導航行為手動調用 leaveBreadcrumb 方法上報導航信息給後臺從而達到監聽頁面的效果。

註意事項:
navigatorObservers 是創建導航器的觀察者列表,將要觀察頁面跳轉對象放在該列表中,頁面中發生導航行為時候,就可以監聽到。

如果一個應用中有多個 MaterialApp 的情況,需要保證每個 MaterialApp:navigatorObservers 中都有 BugsnagNavigatorObserver 才行,不然某些 MaterialApp 中也監控不到。最好是一個應用統一一份 MaterialApp 減少這種不必要的麻煩。

如下代碼中

  1. Bugsnag 框架自定義了 BugsnagNavigatorObserver 對象, 該對象必須繼承 NavigatorObserver 並實現其中回調函數方可放入到 MaterialApp:navigatorObservers 中,不是隨便什麼對象都可以放到列表中的。
  2. 這樣 Bugsnag 就具有了對整個接入應用導航的監控能力,頁面進入或者頁面退出行為都可以被監控到。
  3. 然後在步驟 2 回調中手動調用_leaveBreadcrumb 來實現對導航路徑的監聽。
  4. _leaveBreadcrumb 將數據傳送給對端 SDK,SDK 傳輸數據給 bugsnag 後臺 Breadcrumb 頁,也就是上面效果中呈現的。
class ExampleApp extends StatelessWidget {
  const ExampleApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      navigatorObservers: [BugsnagNavigatorObserver()],
      //...
    );
  }
}

----[BugsnagNavigatorObserver]----->
// BugsnagNavigatorObserver extends NavigatorObserver
BugsnagNavigatorObserver({
    //...
  }) : _navigatorName = (navigatorName != null) ? navigatorName : 'navigator';

  @override
  void didReplace({Route<dynamic>? newRoute, Route<dynamic>? oldRoute}) {
    _leaveBreadcrumb('Route replaced on', {
      if (oldRoute != null) 'oldRoute': _routeMetadata(oldRoute),
      if (newRoute != null) 'newRoute': _routeMetadata(newRoute),
    });
    //...
  }

  //....其他回調函數

  void _leaveBreadcrumb(String function, Map<String, Object> metadata) {
    if (leaveBreadcrumbs) {
      bugsnag.leaveBreadcrumb(
        _operationDescription(function),
        type: BugsnagBreadcrumbType.navigation,
        metadata: metadata,
      );
    }
  }

網路請求自動埋點實現原理

通過自定義 http.BaseClient 實現對預設 http.Client 中 send 方法代理來實現,對請求發送和失敗進行統一化監聽,並記錄了請求時長埋點上報。

推薦個網路監聽通用方案:
可以看下 didi 的 Flutter 方案: 覆寫 HttpOverride 即可,DoKit/dokit_http.dart at master · didi/DoKit

如下

  1. 當點擊發送網路請求時,會調用 Bugsnag 自己的 http 庫。
  2. Bugsnag http 庫中自己實現了 Client 類,該類覆寫 send 方法(該方法在發生網路行為時都會被觸發),併在其中做了網路監聽的額外埋點操作_requestFinished,其中包括對網路結果反饋和網路請求時間的統計。
  3. 例子中最終 post 會執行 client.send,從而完成了對網路自埋點路徑的上報。

Untitled 3.png

import 'package:bugsnag_breadcrumbs_http/bugsnag_breadcrumbs_http.dart' as http;
void _networkFailure() async =>
      http.post(Uri.parse('https://example.com/invalid'));

----[bugsnag_breadcrumbs_http.dart]---->
Future<http.Response> post(Uri url,
        {Map<String, String>? headers, Object? body, Encoding? encoding}) =>
    _withClient((client) =>
        client.post(url, headers: headers, body: body, encoding: encoding));

Future<T> _withClient<T>(Future<T> Function(Client) fn) async {
  var client = Client();
  try {
    return await fn(client);
  } finally {
    client.close();
  }
}

---->[client.dart]---->
class Client extends http.BaseClient {
  /// The wrapped client.
  final http.Client _inner;

  Client() : _inner = http.Client();

  Client.withClient(http.Client client) : _inner = client;

  @override
  Future<http.StreamedResponse> send(http.BaseRequest request) async {
    final stopwatch = Stopwatch()..start();
    try {
      final response = await _inner.send(request);
      //攔截點:這裡監聽發送成功
      await _requestFinished(request, stopwatch, response);
      return response;
    } catch (e) {
      //攔截點:這裡監聽發送失敗
      await _requestFinished(request, stopwatch);
      rethrow;
    }
  }

  Future<void> _requestFinished(
    http.BaseRequest request,
    Stopwatch stopwatch, [
    http.StreamedResponse? response,
  ]) =>
      _leaveBreadcrumb(Breadcrumb.build(_inner, request, stopwatch, response));
}

總結

本文主要對可追溯 Crash 路徑自動埋點原理進行分析,該需求是讀 Bugsnag 是覺得想法上有亮點的地方,就重點拎出來說說,結合自身做 Flutter 異常捕獲過程經驗,壓根沒考慮到這種記錄異常路徑的需求。而且它還做得這麼細針對了導航監聽和網路監聽自動埋點,而這兩塊又恰恰是對定位問題比較關鍵的,試問哪個異常出現了你不關註發生的頁面,哪個線上 App 逃得開網路異常。

另外本文也總結閱讀 Flutter 異常監控框架必看的幾個關鍵步驟,結合 Bugsnag 源碼進行實際講解。其實 Flutter 異常監控框架來回就那麼幾個步驟沒什麼大的變化,主要是看其中有什麼亮度的需求並針對需求做了哪些開閉設計,這些才是令人振奮的東西。

參考鏈接

bugsnag/bugsnag-flutter: Bugsnag crash reporting for Flutter apps
DoKit/Flutter at master · didi/DoKit

如果覺得文章對你有幫助,點贊、收藏、關註、評論,一鍵四連支持,你的支持就是我創作最大的動力。

❤️ 本文原創聽蟬 公眾號:碼里特別有禪 歡迎關註原創技術文章第一時間推送 ❤️


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 本實例使用了工具包SKIT.FlurlHttpClient.Wechat.TenpayV3(github:https://github.com/fudiwei/DotNetCore.SKIT.FlurlHttpClient.Wechat) 示例中的_repositoryWrapper的相關使用是我們 ...
  • cloudflare 賬號註冊 https://www.cloudflare-cn.com/products/tunnel/ 功能變數名稱準備和配置 有兩種方式: 在 cloudflare 自己購買功能變數名稱, 比較貴 在其他平臺的功能變數名稱, 通過配置解析功能變數名稱規則, 這樣可以托管在 cloudflare 以阿裡雲為, ...
  • 背景 https://www.cnblogs.com/liteng0305/p/17018299.html 上次使用樂鑫編譯好的OpenOCD失敗,可能是因為沒有開啟CMSIS-DAP支持,手動開啟編譯試一下 平臺 Ubuntu Linux 5.4.0 官方OpenOCD 直接下載的OpenOCD沒 ...
  • mycode : mycode 思考 突破引導程式方法: 再寫一個程式,並且把這個程式放到存儲介質中; 主引導程式要載入這個新的程式,將控制權轉交給新的程式; 遇到的問題:怎麼在存儲介質中找這個新的程式呢? 那就需要藉助於一個文件系統,有了文件系統,就可以很方便的把寫好的程式放到軟盤裡了,也可以根據 ...
  • 目的 手裡有調試STM32的DAP-LINK,想試試通過JTAG調試ESP32 OpenOCD支持CMSIS-DAP DAP-LINK支持的晶元,我手上這款描述如下,應該JTAG協議的都支持 平臺 windows10 + ESP-IDF ESP-WROOM-32E模組 + 燒錄底座 DAP-LINK ...
  • 目錄 PostgreSQL(01): Ubuntu20.04/22.04 PostgreSQL 安裝配置記錄 PostgreSQL(02): PostgreSQL常用命令 PostgreSQL 常用命令 滿足驗證條件的用戶, 可以用psql命令進入pg的命令行交互模式 用戶管理相關 查看用戶列表 \ ...
  • 摘要:主要介紹華為雲在HBase 2.x內核所做的一些MTTR優化實踐。 本文分享自華為雲社區《華為雲在HBase MTTR上的優化實踐》,作者: 搬磚小能手。 隨著HBase在華為雲的廣泛應用,HBase的數據節點規模也越來越大。最新版本的MRS可支持的單集群HBase數據節點規模可達到1024節 ...
  • 一:背景 1. 講故事 在 SQLSERVER 中有非常多的索引,比如:聚集索引,非聚集索引,唯一索引,複合索引,Include索引,交叉索引,連接索引,奇葩索引等等,當索引多了之後很容易傻傻的分不清,比如:複合索引 和 Include索引,但又在真實場景中用的特別多,本篇我們就從底層數據頁層面釐清 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...