如果感觉文章对你有帮忙,点赞、珍藏、关注、评论,一键四连反对,你的反对就是我创作最大的能源。

❤️ 本文原创听蝉 公众号:码里特地有禅 欢送关注原创技术文章第一工夫推送 ❤️

前言

没错,继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会执行到_notifyInternalvoid _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
  1. 主动增加,其中包含两个场景:导航栏跳转和 网络申请

如上两个场景的的实现原理波及到对利用性能的监控性能,重点剖析其中原理。

导航栏主动埋点实现原理

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 NavigatorObserverBugsnagNavigatorObserver({    //...  }) : _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