Flutter 性能监控 Flutter异常监控 - 叁 | 从bugsnag源码学习如何追溯异常产生路径 唯你 2024-12-13 2025-03-17 前言 没错,继Flutter异常监控 | 框架Catcher原理分析 之后,带着那颗骚动的好奇心我又捣鼓着想找其他Flutter异常监控框架读读,看能不能找到一些好玩的东西,于是在官方介绍 第三方库里发现了这货Bugsnag ,大致扫了下源码发现flutter侧主流程很简单没啥东西可看滴,因为这货强烈依赖对端能力,Flutter异常捕获之后就无脑抛给对端SDK自己啥都不干 ,抛开Bugsnag这种处理异常的方式不论,源码里却也有一些之我见的亮度值得借鉴和学习,比如本文主要介绍Bugsnag如何追溯异常路径的设计思想和实现,对异常捕获的认识有不少帮助。
Bugsnag 功能简介 在介绍可追溯异常路径设计之前,有必要先科普下Bugsnag是什么? 让大佬们有一个大局观,毕竟后面介绍内容只是其中一个小的点。
Bugsnag跟Catcher一样也是Flutter异常监控框架,Bugsnag-flutter只是壳,主要作用有:
规范多平台(安卓,ios)异常调用和上报的接口。
拿到flutter异常相关数据传递给对端。
主要支持功能:
dart侧异常支持手动和自动上报。
支持上报数据序列化,有网环境下会继续上报。
支持记录用户导航步骤,自定义关键节点操作,网络异常自动上报。
这个框架的侧重点跟Catcher完全不同,它不支持异常的UI客户端自定义显示,也不支持对异常的定制化处理。说白了就是你想看异常就只能登陆到Bugsnag后台 看到,后台有套餐包括试用版和收费版(你懂滴)。
基本使用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 void main() async => bugsnag.start( runApp: () => runApp(const ExampleApp()), apiKey: 'add_your_api_key_here' , projectPackages: const BugsnagProjectPackages.only({'bugsnag_example' }), ); 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(), }, ); } }
1 2 3 4 5 6 void _leaveBreadcrumb() async => bugsnag.leaveBreadcrumb('This is a custom breadcrumb' , metadata: {'from' : 'a' , 'to' : 'z' });
1 2 3 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的包装类生成
我们最好不要直接使用onError参数中的error和stack字段,因为为方便问定位一般原始Error会经过各种转换增加附加信息更容易还原异常现场,比如设备id等,对比Catcher中这个经过包装的对象叫Report
操作包装类
上面最终生成的包装类对象会经过一些操作,操作主要三个方面:显示、存储、上报。拿Catcher来举例子,它包含了UI显示和上报两个。一般在项目中可能显示不那么重要,最重要的是存储和上报。
Bugsnag主要流程源码简析 主要领略下”异常捕获通用套路” 大法有多香:
找监控点
这个流程中少了addErrorListener,说明bugsnag对isolate异常是监控不到滴。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 Future<void > start({ FutureOr<void > Function ()? runApp, }) async { _runWithErrorDetection( detectDartErrors, () => WidgetsFlutterBinding.ensureInitialized(), ); await ChannelClient._channel.invokeMethod('start' , <String , dynamic >{ }); 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(); } } void _notifyUnhandled(dynamic error, StackTrace? stackTrace) { _notifyInternal(error, true , null , stackTrace, null ); }
1 2 3 4 5 6 7 8 9 10 ChannelClient(bool autoDetectErrors) { if (autoDetectErrors) { FlutterError.onError = _onFlutterError; } } void _onFlutterError(FlutterErrorDetails details) { _notifyInternal(details.exception, true , details, details.stack, null ); }
找包装类生成
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 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); } 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 ; }
操作包装类
本来以为此处要大干一场,结果灰溜溜给了对端。。。,什么都不想说,内心平静毫无波澜~~~
1 2 Future<void > _deliverEvent(BugsnagEvent event) => _channel.invokeMethod('deliverEvent' , event);
主要源码流程看完了,下面来看下Bugsnag我觉得比较好玩的需求和实现。
什么是可追溯异常路径 这个是我自己想的一个词,该需求目的是能完整记录用户操作的整个行为路径,这样达到清晰指导用户操作过程,对问题的定位很有帮助。可以理解成一个小型的埋点系统,只是该埋点系统只是针对异常来做的。
如下:异常产生流程,state被成功加载后用户先进入了主页,然后从主页进入了native-crashes页之后异常就产生了。 对开发者和测试人员来说很容易复现通过如上路径来复现问题。
异常路径后台显示效果
如何实现 前置知识 Bugsnag中将可追溯的路径命名为Breadcrumb,刚开始我不理解,这个单词英文意思:面包屑,跟路径八竿子都扯不上关系,直到查维基百科才发现为什么这么命名,通过一片一片的面包屑才能找到回家的路。。。,老外们还真够有情怀的!
Breadcrumb的命名的含义, 有没有发觉这个名字起得好形象!
页面路径 (英语:breadcrumb或breadcrumb trail/navigation),又称面包屑导航 ,是在用户界面中的一种导航辅助。它是用户一个在程序或文件中确定和转移他们位置的一种方法。面包屑 这个词来自糖果屋 这个童话故事;故事中,汉赛尔与葛丽特企图依靠洒下的面包屑 找到回家的路。
当然最终这些丢下的面包屑(leaveBreadcrumb)路径数据也是通过调用到对端SDK来实现:
1 2 3 4 5 6 7 8 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 页,也就是上面效果中呈现的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 class ExampleApp extends StatelessWidget { const ExampleApp({Key? key}) : super (key: key); @override Widget build(BuildContext context) { return MaterialApp( navigatorObservers: [BugsnagNavigatorObserver()], ); } } ----[BugsnagNavigatorObserver]-----> 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,从而完成了对网络自埋点路径的上报。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 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 { 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