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
  • 一:背景 1. 講故事 年前遇到了好幾例托管堆被損壞的案例,有些運氣好一些,從被破壞的托管堆記憶體現場能觀測出大概是什麼問題,但更多的情況下是無法做出準確判斷的,原因就在於生成的dump是第二現場,借用之前文章的一張圖,大家可以理解一下。 為了幫助更多受此問題困擾的朋友,這篇來整理一下如何 快狠準 的 ...
  • 前言 .NET6 開始,.NET Croe API 項目取消了 Startup.cs 文件,在 Program.cs 文件的 Main 函數中完成服務的註冊和中間件管道的管理。但當我們項目引入更多包的時候,Program.cs 文件也會看起來很臃腫。 而且,我們不只會有一個後端項目,為了方便快速創建 ...
  • 目錄 背景 get 與 post 的區別 所有介面都用 post 請求? 背景 最近在逛知乎的時候發現一個有趣的問題:公司規定所有介面都用 post 請求,這是為什麼? 看到這個問題的時候其實我也挺有感觸的,因為我也曾經這樣問過我自己。在上上一家公司的時候接到一個項目是從零開始搭建一個微服務,當時就 ...
  • *以下內容為本人的學習筆記,如需要轉載,請聲明原文鏈接 微信公眾號「englyf」https://mp.weixin.qq.com/s/2GFLTstDC7w6u3fTJxflNA 本文大概 1685 個字,閱讀需花 6 分鐘內容不多, 但也花了一些精力如要交流, 歡迎關註我然後評論區留言 謝謝你的 ...
  • 在新版本的pandas中,上述代碼會引起警告,建議改成SQLAlchemy connectable(engine/connection),後續代碼將引入這種升級的連接方式。 ...
  • 幾乎所有的高級編程語言都有自己的垃圾回收機制,開發者不需要關註記憶體的申請與釋放,Python 也不例外。Python 官方團隊的文章 https://devguide.python.org/internals/garbage-collector 詳細介紹了 Python 中的垃圾回收演算法,本文是這篇 ...
  • 如果您想查找高於或低於平均值的數字,可以不必計算該平均值,就能查看更高或更低的值。通過Java應用程式,可以自動突出顯示這些數字。除了快速突出顯示高於或低於平均值的值外,您還可以查看高於或低於的值的個數。現在讓我們看看如何在 Java應用程式中實現此操作。 引入jar包 導入方法1: 手動引入。將  ...
  • 第一種方式:使用{} firstDict = {"name": "wang yuan wai ", "age" : 25} 說明:{}為創建一個空的字典對象 第二種方式:使用fromkeys()方法 second_dict = dict.fromkeys(("name", "age")) #valu ...
  • 在golang中可以使用a := b這種方式將b賦值給a,只有當b能進行深拷貝時a與b才不會互相影響,否則就需要進行更為複雜的深拷貝。 下麵就是Go賦值操作的一個說明: Go語言中所有賦值操作都是值傳遞,如果結構中不含指針,則直接賦值就是深度拷貝;如果結構中含有指針(包括自定義指針,以及切片,map ...
  • 本文結合京東監控埋點場景,對解決樣板代碼的技術選型方案進行分析,給出最終解決方案後,結合理論和實踐進一步展開。通過關註文中的技術分析過程和技術場景,讀者可收穫一種樣板代碼思想過程和解決思路,並對Java編譯器底層有初步瞭解。 ...