如果覺得文章對你有幫助,點贊、收藏、關注、評論,一鍵四連支持,你的支持就是我創作最大的動力,
?? 本文原創聽蟬 公眾號:碼里特別有禪 歡迎關注原創技術文章第一時間推送 ??
前言
沒錯,繼Flutter 例外監控 | 框架 Catcher 原理分析 之后,帶著那顆騷動的好奇心我又搗鼓著想找其他 Flutter 例外監控框架讀讀,看能不能找到一些好玩的東西,于是在官方介紹第三方庫里發現了這貨Bugsnag,大致掃了下原始碼發現 flutter 側主流程很簡單沒啥東西可看滴,因為這貨強烈依賴對端能力,Flutter 例外捕獲之后就無腦拋給對端 SDK 自己啥都不干 ,拋開 Bugsnag 這種處理例外的方式不論,原始碼里卻也有一些之我見的亮度值得借鑒和學習,比如本文主要介紹 Bugsnag 如何追溯例外路徑的設計思想和實作,對例外捕獲的認識有不少幫助,
Bugsnag
功能簡介
在介紹可追溯例外路徑設計之前,有必要先科普下 Bugsnag 是什么? 讓大佬們有一個大局觀,畢竟后面介紹內容只是其中一個小的點,
Bugsnag 跟 Catcher 一樣也是 Flutter 例外監控框架,Bugsnag-flutter 只是殼,主要作用有:
- 規范多平臺(安卓,ios)例外呼叫和上報的介面,
- 拿到 flutter 例外相關資料傳遞給對端,
主要支持功能:
- dart 側例外支持手動和自動上報,
- 支持上報資料序列化,有網環境下會繼續上報,
- 支持記錄用戶導航步驟,自定義關鍵節點操作,網路例外自動上報,
這個框架的側重點跟 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'}),
// one rror 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 頁顯示內容:可以看到路徑中包含了當前頁面資訊,請求資訊和關鍵步驟,例外生成的路徑和時間點
例外捕獲框架閱讀通用套路
在例外上報主流程之前,必要的通用套路不能忘,按照這個思路來追原始碼事半功倍,如下:
- Flutter 例外監控點
三把斧:FlutterError.onError ,addErrorListener,runZonedGuarded 詳見:不得不知道的 Flutter 例外捕獲知識點:Zone 中 Zone 例外捕獲小節,
- 針對 Error 的包裝類生成
我們最好不要直接使用 one rror 引數中的 error 和 stack 欄位,因為為方便問定位一般原始 Error 會經過各種轉換增加附加資訊更容易還原例外現場,比如設備 id 等,對比 Catcher 中這個經過包裝的物件叫Report
- 操作包裝類
上面最終生成的包裝類物件會經過一些操作,操作主要三個方面:顯示、存盤、上報,拿 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);
}
這里主要關注下自動添加面包屑的場景,
如何添加路徑
兩種方式:
-
手動添加,通過呼叫 bugsnag.leaveBreadcrumb
-
自動添加,其中包括兩個場景:導航欄跳轉和 網路請求
如上兩個場景的的實作原理涉及到對應用性能的監控功能,重點分析其中原理,
導航欄自動埋點實作原理
MaterialApp: navigatorObservers 來實作對頁面跳轉的監聽,Bugsnag 中是通過自定義 BugsnagNavigatorObserver,并在其回呼函式中監聽導航行為手動呼叫 leaveBreadcrumb 方法上報導航資訊給后臺從而達到監聽頁面的效果,
注意事項:
navigatorObservers 是創建導航器的觀察者串列,將要觀察頁面跳轉物件放在該串列中,頁面中發生導航行為時候,就可以監聽到,如果一個應用中有多個 MaterialApp 的情況,需要保證每個 MaterialApp:navigatorObservers 中都有 BugsnagNavigatorObserver 才行,不然某些 MaterialApp 中也監控不到,最好是一個應用統一一份 MaterialApp 減少這種不必要的麻煩,
如下代碼中
- Bugsnag 框架自定義了 BugsnagNavigatorObserver 物件, 該物件必須繼承 NavigatorObserver 并實作其中回呼函式方可放入到 MaterialApp:navigatorObservers 中,不是隨便什么物件都可以放到串列中的,
- 這樣 Bugsnag 就具有了對整個接入應用導航的監控能力,頁面進入或者頁面退出行為都可以被監控到,
- 然后在步驟 2 回呼中手動呼叫_leaveBreadcrumb 來實作對導航路徑的監聽,
- _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
如下
- 當點擊發送網路請求時,會呼叫 Bugsnag 自己的 http 庫,
- Bugsnag http 庫中自己實作了 Client 類,該類復寫 send 方法(該方法在發生網路行為時都會被觸發),并在其中做了網路監聽的額外埋點操作_requestFinished,其中包括對網路結果反饋和網路請求時間的統計,
- 例子中最終 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
如果覺得文章對你有幫助,點贊、收藏、關注、評論,一鍵四連支持,你的支持就是我創作最大的動力,
?? 本文原創聽蟬 公眾號:碼里特別有禪 歡迎關注原創技術文章第一時間推送 ??
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/541413.html
標籤:其他
下一篇:安卓+n2n實作異地組建局域網
