1

Flutter异常监控 - 叁 | 从bugsnag源码学习如何追溯异常产生路径 - 码里特别有禅

 1 year ago
source link: https://www.cnblogs.com/xuge2it/p/17029577.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

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

没错,继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'));

后台效果展示

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

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 页之后异常就产生了。 对开发者和测试人员来说很容易复现通过如上路径来复现问题。

Untitled 2.png

异常路径后台显示效果

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

  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 页,也就是上面效果中呈现的。
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

  1. 当点击发送网络请求时,会调用 Bugsnag 自己的 http 库。
  2. Bugsnag http 库中自己实现了 Client 类,该类复写 send 方法(该方法在发生网络行为时都会被触发),并在其中做了网络监听的额外埋点操作_requestFinished,其中包括对网络结果反馈和网络请求时间的统计。
  3. 例子中最终 post 会执行 client.send,从而完成了对网络自埋点路径的上报。
Untitled 3.png
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

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

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK