Flutter异常监控 - 叁 | 从bugsnag源码学习如何追溯异常产生路径
如果觉得文章对你有帮助,点赞、收藏、关注、评论,一键四连支持,你的支持就是我创作最大的动力。
️ 本文原创听蝉 公众号:码里特别有禅 欢迎关注原创技术文章第一时间推送 ️
前言
没错,继Flutter 异常监控 | 框架 Catcher 原理分析 之后,带着那颗骚动的好奇心我又捣鼓着想找其他 Flutter 异常监控框架读读,看能不能找到一些好玩的东西,于是在官方介绍第三方库里发现了这货Bugsnag,大致扫了下源码发现 flutter 侧主流程很简单没啥东西可看滴,因为这货强烈依赖对端能力,Flutter 异常捕获之后就无脑抛给对端 SDK 自己啥都不干 ,抛开 Bugsnag 这种处理异常的方式不论,源码里却也有一些之我见的亮度值得借鉴和学习,比如本文主要介绍 Bugsnag 如何追溯异常路径的设计思想和实现,对异常捕获的认识有不少帮助。
Bugsnag
功能简介
在介绍可追溯异常路径设计之前,有必要先科普下 Bugsnag 是什么? 让大佬们有一个大局观,毕竟后面介绍内容只是其中一个小的点。
Bugsnag 跟 Catcher 一样也是 Flutter 异常监控框架,Bugsnag-flutter 只是壳,主要作用有:
- 规范多平台(安卓,ios)异常调用和上报的接口。
- 拿到 flutter 异常相关数据传递给对端。
主要支持功能:
- dart 侧异常支持手动和自动上报。
- 支持上报数据序列化,有网环境下会继续上报。
- 支持记录用户导航步骤,自定义关键节点操作,网络异常自动上报。
这个框架的侧重点跟 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 页显示内容:可以看到路径中包含了当前页面信息,请求信息和关键步骤,异常生成的路径和时间点
异常捕获框架阅读通用套路
在异常上报主流程之前,必要的通用套路不能忘,按照这个思路来追源码事半功倍,如下:
- Flutter 异常监控点
三把斧:FlutterError.onError ,addErrorListener,runZonedGuarded 详见:不得不知道的 Flutter 异常捕获知识点:Zone 中 Zone 异常捕获小节。
- 针对 Error 的包装类生成
我们最好不要直接使用 onError 参数中的 error 和 stack 字段,因为为方便问定位一般原始 Error 会经过各种转换增加附加信息更容易还原异常现场,比如设备 id 等,对比 Catcher 中这个经过包装的对象叫Report
- 操作包装类
上面最终生成的包装类对象会经过一些操作,操作主要三个方面:显示、存储、上报。拿 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);
}
这里主要关注下自动添加面包屑的场景。
如何添加路径
两种方式:
手动添加,通过调用 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 页,也就是上面效果中呈现的。
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
如下
- 当点击发送网络请求时,会调用 Bugsnag 自己的 http 库。
- Bugsnag http 库中自己实现了 Client 类,该类复写 send 方法(该方法在发生网络行为时都会被触发),并在其中做了网络监听的额外埋点操作_requestFinished,其中包括对网络结果反馈和网络请求时间的统计。
- 例子中最终 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源码学习如何追溯异常产生路径的更多相关文章
- Asp.NetCore源码学习[2-1]:配置[Configuration]
Asp.NetCore源码学习[2-1]:配置[Configuration] 在Asp. NetCore中,配置系统支持不同的配置源(文件.环境变量等),虽然有多种的配置源,但是最终提供给系统使用的只 ...
- Spring5.0源码学习系列之Spring AOP简述
前言介绍 附录:Spring源码学习专栏 在前面章节的学习中,我们对Spring框架的IOC实现源码有了一定的了解,接着本文继续学习Springframework一个核心的技术点AOP技术. 在学习S ...
- JUC源码学习笔记5——线程池,FutureTask,Executor框架源码解析
JUC源码学习笔记5--线程池,FutureTask,Executor框架源码解析 源码基于JDK8 参考了美团技术博客 https://tech.meituan.com/2020/04/02/jav ...
- Java集合专题总结(1):HashMap 和 HashTable 源码学习和面试总结
2017年的秋招彻底结束了,感觉Java上面的最常见的集合相关的问题就是hash--系列和一些常用并发集合和队列,堆等结合算法一起考察,不完全统计,本人经历:先后百度.唯品会.58同城.新浪微博.趣分 ...
- Java并发包源码学习之AQS框架(四)AbstractQueuedSynchronizer源码分析
经过前面几篇文章的铺垫,今天我们终于要看看AQS的庐山真面目了,建议第一次看AbstractQueuedSynchronizer 类源码的朋友可以先看下我前面几篇文章: <Java并发包源码学习 ...
- Java并发包源码学习之AQS框架(三)LockSupport和interrupt
接着上一篇文章今天我们来介绍下LockSupport和Java中线程的中断(interrupt). 其实除了LockSupport,Java之初就有Object对象的wait和notify方法可以实现 ...
- java Integer 源码学习
转载自http://www.hollischuang.com/archives/1058 Integer 类在对象中包装了一个基本类型 int 的值.Integer 类型的对象包含一个 int 类型的 ...
- Dubbo源码学习--服务是如何引用的
ReferenceBean 跟服务引用一样,Dubbo的reference配置会被转成ReferenceBean类,ReferenceBean实现了InitializingBean接口,直接看afte ...
- Dubbo源码学习--注册中心分析
相关文章: Dubbo源码学习--服务是如何发布的 Dubbo源码学习--服务是如何引用的 注册中心 关于注册中心,Dubbo提供了多个实现方式,有比较成熟的使用zookeeper 和 redis 的 ...
- 【Java】Objects 源码学习
2017-02-10 by 安静的下雪天 http://www.cnblogs.com/quiet-snowy-day/p/6387321.html 本篇概要 Objects 与 Object ...
随机推荐
- 【pytest官方文档】解读- 插件开发之hooks 函数(钩子)
上一节讲到如何安装和使用第三方插件,用法很简单.接下来解读下如何自己开发pytest插件. 但是,由于一个插件包含一个或多个钩子函数开发而来,所以在具体开发插件之前还需要先学习hooks函数. 一.什 ...
- [leetcode]95.不同的二叉搜索树
Posted by 微博@Yangsc_o 原创文章,版权声明:自由转载-非商用-非衍生-保持署名 | Creative Commons BY-NC-ND 3.0 95. 不同的二叉搜索树 II 给你 ...
- 解决办法:ImportError:'module'object has no attribute 'check specifier'
在安装envsubst命令不存在的报错, 安装centos本地源, 再安装gettext) 在指定版本的时候发现还是报错根据后面提示指定44.0.0问题解决 pip install --upgrade ...
- .NET性能系列文章二:Newtonsoft.Json vs. System.Text.Json
微软终于追上了? 图片来自 Glenn Carstens-Peters Unsplash 欢迎来到.NET性能系列的另一章.这个系列的特点是对.NET世界中许多不同的主题进行研究.基准和比较.正如标题 ...
- 19_Vue如何监测到对象类型数据发生改变的?
数据更新 关于监视 我们之前讲过,我们在data当中配置的属性,最终会挂载在vue实例身上,而data这个配置项,最终也会在vue身上成为一个新的属性 == _data 当我们在页面DOM当中,去使用 ...
- 又拍云之 Keepalived 高可用部署
在聊 Keepalived 之前,我们需要先简单了解一下 VRRP.VRRP(Virtual Router Redundancy Protocol)即虚拟路由冗余协议,是专门为了解决静态路由的高可用而 ...
- 2022春每日一题:Day 21
题目:[SCOI2007]降雨量 这题比较坑,分几种情况,但是可以总起来说,分开写,两个月份都没出现,maybe,否则如果两个月份都大于[l+1,r-1]的最大值,如果两个月份差值=r-l输出,tru ...
- 深入理解Golang 闭包,直通面试
大家好 今天为大家讲解的面试专题是: 闭包. 定义 闭包在计算机科学中的定义是:在函数内部引用了函数内部变量的函数. 看完定义后,我陷入了沉思...确实,如果之前没有接触过闭包或者对闭包不理解的话,这 ...
- # Android网络请求(4) 网络请求框架Volley
Android网络请求(4) 网络请求框架Volley Volley是Google在2013年5月15日到17日在旧金山Moscone中心举办网络开发者年会中推出的Android异步网络加载框架和图片 ...
- 推荐一款 .NET 编写的 嵌入式平台的开源仿真器--Renode
Renode 是一个开发框架,通过让你模拟物理硬件系统来加速物联网和嵌入式系统开发. Renode 可以模拟 Cortex-M.RISC-V 等微控制器,不仅可以模拟 CPU指令,还可以模拟外设,甚至 ...