Flutter異常監控

2023-01-06 12:01:08

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

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

前言

沒錯,繼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'));

後臺效果展示


Flutter 異常顯示頁


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 頁之後異常就產生了。 對開發者和測試人員來說很容易復現通過如上路徑來複現問題。

異常路徑後臺顯示效果

如何實現

前置知識

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,從而完成了對網路自埋點路徑的上報。

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

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

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