如果覺得文章對你有幫助,點贊、收藏、關注、評論,一鍵四連支援,你的支援就是我創作最大的動力。
❤️ 本文原創聽蟬 公眾號:碼裡特別有禪 歡迎關注原創技術文章第一時間推播 ❤️
沒錯,繼Flutter 異常監控 | 框架 Catcher 原理分析 之後,帶著那顆騷動的好奇心我又搗鼓著想找其他 Flutter 異常監控框架讀讀,看能不能找到一些好玩的東西,於是在官方介紹第三方庫裡發現了這貨Bugsnag,大致掃了下原始碼發現 flutter 側主流程很簡單沒啥東西可看滴,因為這貨強烈依賴對端能力,Flutter 異常捕獲之後就無腦拋給對端 SDK 自己啥都不幹 ,拋開 Bugsnag 這種處理異常的方式不論,原始碼裡卻也有一些之我見的亮度值得借鑑和學習,比如本文主要介紹 Bugsnag 如何追溯異常路徑的設計思想和實現,對異常捕獲的認識有不少幫助。
在介紹可追溯異常路徑設計之前,有必要先科普下 Bugsnag 是什麼? 讓大佬們有一個大局觀,畢竟後面介紹內容只是其中一個小的點。
Bugsnag 跟 Catcher 一樣也是 Flutter 異常監控框架,Bugsnag-flutter 只是殼,主要作用有:
主要支援功能:
這個框架的側重點跟 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 頁顯示內容:可以看到路徑中包含了當前頁面資訊,請求資訊和關鍵步驟,異常生成的路徑和時間點
在異常上報主流程之前,必要的通用套路不能忘,按照這個思路來追原始碼事半功倍,如下:
三把斧:FlutterError.onError ,addErrorListener,runZonedGuarded 詳見:不得不知道的 Flutter 異常捕獲知識點:Zone 中 Zone 異常捕獲小節。
我們最好不要直接使用 onError 引數中的 error 和 stack 欄位,因為為方便問定位一般原始 Error 會經過各種轉換增加附加資訊更容易還原異常現場,比如裝置 id 等,對比 Catcher 中這個經過包裝的物件叫Report
上面最終生成的包裝類物件會經過一些操作,操作主要三個方面:顯示、儲存、上報。拿 Catcher 來舉例子,它包含了 UI 顯示和上報兩個。一般在專案中可能顯示不那麼重要,最重要的是儲存和上報。
主要領略下」異常捕獲通用套路」 大法有多香:
找監控點
這個流程中少了 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 減少這種不必要的麻煩。
如下程式碼中
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
如下
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
如果覺得文章對你有幫助,點贊、收藏、關注、評論,一鍵四連支援,你的支援就是我創作最大的動力。
❤️ 本文原創聽蟬 公眾號:碼裡特別有禪 歡迎關注原創技術文章第一時間推播 ❤️