经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 移动开发 » Flutter » 查看文章
Flutter异常监控 - 叁 | 从bugsnag源码学习如何追溯异常产生路径
来源:cnblogs  作者:码里特别有禅  时间:2023/1/6 10:07:24  对本文有异议

如果觉得文章对你有帮助,点赞、收藏、关注、评论,一键四连支持,你的支持就是我创作最大的动力。

?? 本文原创听蝉 公众号:码里特别有禅 欢迎关注原创技术文章第一时间推送  ??

前言

没错,继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 后台看到,后台有套餐包括试用版和收费版(你懂滴)。

基本使用

  1. void main() async => bugsnag.start(
  2. runApp: () => runApp(const ExampleApp()),
  3. // 需要到bugsang后台注册账号申请一个api_key
  4. apiKey: 'add_your_api_key_here',
  5. projectPackages: const BugsnagProjectPackages.only({'bugsnag_example'}),
  6. // onError callbacks can be used to modify or reject certain events
  7. //...
  8. );
  9. class ExampleApp extends StatelessWidget {
  10. const ExampleApp({Key? key}) : super(key: key);
  11. @override
  12. Widget build(BuildContext context) {
  13. return MaterialApp(
  14. navigatorObservers: [BugsnagNavigatorObserver()],
  15. initialRoute: '/',
  16. routes: {
  17. '/': (context) => const ExampleHomeScreen(),
  18. '/native-crashes': (context) => const NativeCrashesScreen(),
  19. },
  20. );
  21. }
  22. }
  1. // Use leaveBreadcrumb() to log potentially useful events in order to
  2. // understand what happened in your app before each error.
  3. void _leaveBreadcrumb() async =>
  4. bugsnag.leaveBreadcrumb('This is a custom breadcrumb',
  5. // Additional data can be attached to breadcrumbs as metadata
  6. metadata: {'from': 'a', 'to': 'z'});
  1. import 'package:bugsnag_breadcrumbs_http/bugsnag_breadcrumbs_http.dart' as http;
  2. void _networkFailure() async =>
  3. http.post(Uri.parse('https://example.com/invalid'));

后台效果展示

Untitled.png
Flutter 异常显示页

Untitled 1.png
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 异常是监控不到滴。

  1. Future<void> start({
  2. FutureOr<void> Function()? runApp,
  3. //... Tag1 一堆额外参数
  4. }) async {
  5. //...
  6. //开始就想着用对端SDK,这里当然少不了初始化通道
  7. _runWithErrorDetection(
  8. detectDartErrors,
  9. () => WidgetsFlutterBinding.ensureInitialized(),
  10. );
  11. //...
  12. await ChannelClient._channel.invokeMethod('start', <String, dynamic>{
  13. //... Tag2:这里将Tag1处的额外参数传给了对端SDK
  14. });
  15. //Tag3:dart error的处理类,其中全部都是通过channel来桥接的
  16. final client = ChannelClient(detectDartErrors);
  17. client._onErrorCallbacks.addAll(onError);
  18. this.client = client;
  19. _runWithErrorDetection(detectDartErrors, () => runApp?.call());
  20. }
  21. void _runWithErrorDetection(
  22. bool errorDetectionEnabled,
  23. FutureOr<void> Function() block,
  24. ) async {
  25. if (errorDetectionEnabled) {
  26. //多么熟悉的味道,
  27. await runZonedGuarded(() async {
  28. await block();
  29. }, _reportZonedError);
  30. } else {
  31. await block();
  32. }
  33. }
  34. //最终_reportZonedError会执行到_notifyInternal
  35. void _notifyUnhandled(dynamic error, StackTrace? stackTrace) {
  36. _notifyInternal(error, true, null, stackTrace, null);
  37. }
  1. ChannelClient(bool autoDetectErrors) {
  2. if (autoDetectErrors) {
  3. FlutterError.onError = _onFlutterError;
  4. }
  5. }
  6. void _onFlutterError(FlutterErrorDetails details) {
  7. _notifyInternal(details.exception, true, details, details.stack, null);
  8. //...
  9. }

找包装类生成

  1. Future<void> _notifyInternal(
  2. dynamic error,
  3. bool unhandled,
  4. FlutterErrorDetails? details,
  5. StackTrace? stackTrace,
  6. BugsnagOnErrorCallback? callback,
  7. ) async {
  8. final errorPayload =
  9. BugsnagErrorFactory.instance.createError(error, stackTrace);
  10. final event = await _createEvent(
  11. errorPayload,
  12. details: details,
  13. unhandled: unhandled,
  14. deliver: _onErrorCallbacks.isEmpty && callback == null,
  15. );
  16. //...
  17. await _deliverEvent(event);
  18. }
  19. //我说什么来着:连最基本的Event构造,都是在对端。
  20. Future<BugsnagEvent?> _createEvent(
  21. BugsnagError error, {
  22. FlutterErrorDetails? details,
  23. required bool unhandled,
  24. required bool deliver,
  25. }) async {
  26. final buildID = error.stacktrace.first.codeIdentifier;
  27. //...
  28. };
  29. //调用了对端通道方法来实现。
  30. final eventJson = await _channel.invokeMethod(
  31. 'createEvent',
  32. {
  33. 'error': error,
  34. 'flutterMetadata': metadata,
  35. 'unhandled': unhandled,
  36. 'deliver': deliver
  37. },
  38. );
  39. if (eventJson != null) {
  40. return BugsnagEvent.fromJson(eventJson);
  41. }
  42. return null;
  43. }

操作包装类

本来以为此处要大干一场,结果灰溜溜给了对端。。。,什么都不想说,内心平静毫无波澜~~~

  1. Future<void> _deliverEvent(BugsnagEvent event) =>
  2. _channel.invokeMethod('deliverEvent', event);

主要源码流程看完了,下面来看下 Bugsnag 我觉得比较好玩的需求和实现。

什么是可追溯异常路径

这个是我自己想的一个词,该需求目的是能完整记录用户操作的整个行为路径,这样达到清晰指导用户操作过程,对问题的定位很有帮助。可以理解成一个小型的埋点系统,只是该埋点系统只是针对异常来做的。

如下:异常产生流程,state 被成功加载后用户先进入了主页,然后从主页进入了 native-crashes 页之后异常就产生了。 对开发者和测试人员来说很容易复现通过如上路径来复现问题。

Untitled 2.png异常路径后台显示效果

如何实现

前置知识

Bugsnag 中将可追溯的路径命名为 Breadcrumb,刚开始我不理解,这个单词英文意思:面包屑,跟路径八竿子都扯不上关系,直到查维基百科才发现为什么这么命名,通过一片一片的面包屑才能找到回家的路。。。,老外们还真够有情怀的!

Breadcrumb 的命名的含义, 有没有发觉这个名字起得好形象!

页面路径(英语:breadcrumb 或 breadcrumb trail/navigation),又称面包屑导航,是在用户界面中的一种导航辅助。它是用户一个在程序或文件中确定和转移他们位置的一种方法。面包屑这个词来自糖果屋 这个童话故事;故事中,汉赛尔与葛丽特企图依靠洒下的面包屑找到回家的路。

当然最终这些丢下的面包屑(leaveBreadcrumb)路径数据也是通过调用到对端 SDK 来实现:

  1. Future<void> leaveBreadcrumb(
  2. String message, {
  3. Map<String, Object>? metadata,
  4. BugsnagBreadcrumbType type = BugsnagBreadcrumbType.manual,
  5. }) async {
  6. final crumb = BugsnagBreadcrumb(message, type: type, metadata: metadata);
  7. await _channel.invokeMethod('leaveBreadcrumb', crumb);
  8. }

这里主要关注下自动添加面包屑的场景。

如何添加路径

两种方式:

  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 页,也就是上面效果中呈现的。
  1. class ExampleApp extends StatelessWidget {
  2. const ExampleApp({Key? key}) : super(key: key);
  3. @override
  4. Widget build(BuildContext context) {
  5. return MaterialApp(
  6. navigatorObservers: [BugsnagNavigatorObserver()],
  7. //...
  8. );
  9. }
  10. }
  11. ----[BugsnagNavigatorObserver]----->
  12. // BugsnagNavigatorObserver extends NavigatorObserver
  13. BugsnagNavigatorObserver({
  14. //...
  15. }) : _navigatorName = (navigatorName != null) ? navigatorName : 'navigator';
  16. @override
  17. void didReplace({Route<dynamic>? newRoute, Route<dynamic>? oldRoute}) {
  18. _leaveBreadcrumb('Route replaced on', {
  19. if (oldRoute != null) 'oldRoute': _routeMetadata(oldRoute),
  20. if (newRoute != null) 'newRoute': _routeMetadata(newRoute),
  21. });
  22. //...
  23. }
  24. //....其他回调函数
  25. void _leaveBreadcrumb(String function, Map<String, Object> metadata) {
  26. if (leaveBreadcrumbs) {
  27. bugsnag.leaveBreadcrumb(
  28. _operationDescription(function),
  29. type: BugsnagBreadcrumbType.navigation,
  30. metadata: metadata,
  31. );
  32. }
  33. }

网络请求自动埋点实现原理

通过自定义 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,从而完成了对网络自埋点路径的上报。

Untitled 3.png

  1. import 'package:bugsnag_breadcrumbs_http/bugsnag_breadcrumbs_http.dart' as http;
  2. void _networkFailure() async =>
  3. http.post(Uri.parse('https://example.com/invalid'));
  4. ----[bugsnag_breadcrumbs_http.dart]---->
  5. Future<http.Response> post(Uri url,
  6. {Map<String, String>? headers, Object? body, Encoding? encoding}) =>
  7. _withClient((client) =>
  8. client.post(url, headers: headers, body: body, encoding: encoding));
  9. Future<T> _withClient<T>(Future<T> Function(Client) fn) async {
  10. var client = Client();
  11. try {
  12. return await fn(client);
  13. } finally {
  14. client.close();
  15. }
  16. }
  17. ---->[client.dart]---->
  18. class Client extends http.BaseClient {
  19. /// The wrapped client.
  20. final http.Client _inner;
  21. Client() : _inner = http.Client();
  22. Client.withClient(http.Client client) : _inner = client;
  23. @override
  24. Future<http.StreamedResponse> send(http.BaseRequest request) async {
  25. final stopwatch = Stopwatch()..start();
  26. try {
  27. final response = await _inner.send(request);
  28. //拦截点:这里监听发送成功
  29. await _requestFinished(request, stopwatch, response);
  30. return response;
  31. } catch (e) {
  32. //拦截点:这里监听发送失败
  33. await _requestFinished(request, stopwatch);
  34. rethrow;
  35. }
  36. }
  37. Future<void> _requestFinished(
  38. http.BaseRequest request,
  39. Stopwatch stopwatch, [
  40. http.StreamedResponse? response,
  41. ]) =>
  42. _leaveBreadcrumb(Breadcrumb.build(_inner, request, stopwatch, response));
  43. }

总结

本文主要对可追溯 Crash 路径自动埋点原理进行分析,该需求是读 Bugsnag 是觉得想法上有亮点的地方,就重点拎出来说说,结合自身做 Flutter 异常捕获过程经验,压根没考虑到这种记录异常路径的需求。而且它还做得这么细针对了导航监听和网络监听自动埋点,而这两块又恰恰是对定位问题比较关键的,试问哪个异常出现了你不关注发生的页面,哪个线上 App 逃得开网络异常。

另外本文也总结阅读 Flutter 异常监控框架必看的几个关键步骤,结合 Bugsnag 源码进行实际讲解。其实 Flutter 异常监控框架来回就那么几个步骤没什么大的变化,主要是看其中有什么亮度的需求并针对需求做了哪些开闭设计,这些才是令人振奋的东西。

参考链接

bugsnag/bugsnag-flutter: Bugsnag crash reporting for Flutter apps
DoKit/Flutter at master · didi/DoKit

如果觉得文章对你有帮助,点赞、收藏、关注、评论,一键四连支持,你的支持就是我创作最大的动力。

?? 本文原创听蝉 公众号:码里特别有禅 欢迎关注原创技术文章第一时间推送 ??

原文链接:https://www.cnblogs.com/xuge2it/p/17029577.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号