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

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

前言

没错,继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会执行到_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);
}

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

如何添加路径

两种方式:

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

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

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

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

Flutter异常监控 - 叁 | 从bugsnag源码学习如何追溯异常产生路径的更多相关文章

  1. Asp.NetCore源码学习[2-1]:配置[Configuration]

    Asp.NetCore源码学习[2-1]:配置[Configuration] 在Asp. NetCore中,配置系统支持不同的配置源(文件.环境变量等),虽然有多种的配置源,但是最终提供给系统使用的只 ...

  2. Spring5.0源码学习系列之Spring AOP简述

    前言介绍 附录:Spring源码学习专栏 在前面章节的学习中,我们对Spring框架的IOC实现源码有了一定的了解,接着本文继续学习Springframework一个核心的技术点AOP技术. 在学习S ...

  3. JUC源码学习笔记5——线程池,FutureTask,Executor框架源码解析

    JUC源码学习笔记5--线程池,FutureTask,Executor框架源码解析 源码基于JDK8 参考了美团技术博客 https://tech.meituan.com/2020/04/02/jav ...

  4. Java集合专题总结(1):HashMap 和 HashTable 源码学习和面试总结

    2017年的秋招彻底结束了,感觉Java上面的最常见的集合相关的问题就是hash--系列和一些常用并发集合和队列,堆等结合算法一起考察,不完全统计,本人经历:先后百度.唯品会.58同城.新浪微博.趣分 ...

  5. Java并发包源码学习之AQS框架(四)AbstractQueuedSynchronizer源码分析

    经过前面几篇文章的铺垫,今天我们终于要看看AQS的庐山真面目了,建议第一次看AbstractQueuedSynchronizer 类源码的朋友可以先看下我前面几篇文章: <Java并发包源码学习 ...

  6. Java并发包源码学习之AQS框架(三)LockSupport和interrupt

    接着上一篇文章今天我们来介绍下LockSupport和Java中线程的中断(interrupt). 其实除了LockSupport,Java之初就有Object对象的wait和notify方法可以实现 ...

  7. java Integer 源码学习

    转载自http://www.hollischuang.com/archives/1058 Integer 类在对象中包装了一个基本类型 int 的值.Integer 类型的对象包含一个 int 类型的 ...

  8. Dubbo源码学习--服务是如何引用的

    ReferenceBean 跟服务引用一样,Dubbo的reference配置会被转成ReferenceBean类,ReferenceBean实现了InitializingBean接口,直接看afte ...

  9. Dubbo源码学习--注册中心分析

    相关文章: Dubbo源码学习--服务是如何发布的 Dubbo源码学习--服务是如何引用的 注册中心 关于注册中心,Dubbo提供了多个实现方式,有比较成熟的使用zookeeper 和 redis 的 ...

  10. 【Java】Objects 源码学习

    2017-02-10 by 安静的下雪天  http://www.cnblogs.com/quiet-snowy-day/p/6387321.html    本篇概要 Objects 与 Object ...

随机推荐

  1. python基础作业2

    目录 编写一个用户认证装饰器 利用有参装饰器编写多种用户登录校验策略 利用递归函数依次打印列表中每一个数据值 获取用户权限并校验用户登录 编写一个用户认证装饰器 """ ...

  2. Pycharm安装使用

    目录 使用pycharm软件 配置调整 下载链接地址:https://www.jetbrains.com/pycharm/download/#section=windows 根据自己的系统需要安装对应 ...

  3. 死锁与Lock锁

    死锁1.死锁的理解:不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁 2.说明: 1)出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞 ...

  4. 一天十道Java面试题----第五天(spring的事务传播机制------>mybatis的优缺点)

    这里是参考B站上的大佬做的面试题笔记.大家也可以去看视频讲解!!! 文章目录 41.spring的事务传播机制 42 .spring事务什么时候会失效 43 .什么的是bean的自动装配.有哪些方式? ...

  5. GitLab CI/CD 自动化部署入门

    前言:因为找了B站内推,测试开发,正好知道内部使用GitLab做自动化测试,所以简单学了一下,有错误的地方请指正. 入门 初始化 cp: 无法获取'/root/node-v12.9.0-linux-x ...

  6. Ubuntu实现电商网站+Mysql主从复制+NFS

    Ubuntu实现电商网站+Mysql主从复制+NFS 1.环境准备 提前准备:Mysql8.0.30安装包.Mysql安装脚本.shopxo2.3.0安装包.DNS脚本 服务器 IP地址 作用 系统版 ...

  7. 所有selenium相关的库

    通过爬虫 获取 官方文档库 如果想获取 相应的库 修改对应配置即可 代码如下 from urllib.parse import urljoin import requests from lxml im ...

  8. CF815D

    模拟赛遇到的题目. 看各位大佬的做法都不是很懂,于是自己一通乱搞搞出来了. 题意 翻译清楚的不能再清楚了 做法 为了方便叙述,我们将每个给出的三元组表示成 \((a_i,b_i,c_i)\),所选的三 ...

  9. Jmeter之聚合报告“造假”

    通过Jmeter,模拟一个"虚假"的聚合报告,可"应付"日常现场项目的性能测试验收.本文档着重介绍jmeter的固定定时器,通过设置随机的延迟时间(如想业务场景 ...

  10. iview table json数据里的num排序问题

    title: 'Num', key: 'num', sortable: true, sortMethod:function(a,b,type){ //可以用Number()或者parseInt(a)转 ...