通用的 Flutter 异常上报模块,为线上运营提供上报与监控安全保障,其次借助 Sentry 平台实现异常告警和管理功能。
线上安全运营
在上线每个客户端版本时,我们都要有一个 crash 标准,由于客户端发布后是无法回滚的,因此在发布时需要使用数据来辅助判断,这里就需要使用 crash 率来辅助判断是否需要进一步灰度更多用户。要做到这点,就需要将客户端异常的报错问题,上报到平台。Android 和 iOS 都有类似的功能模块,在 Flutter 官网也介绍了相应的理论方法,大家可以前往官网进行了解。写这篇专栏的目的是希望将该功能作为通用模块,并且实践该过程。
接下来我们就看看,如何利用官网的理论知识,将该功能封装为一个通用的异常上报模块。
通用上报模块
根据官网的文档,我们将该过程分为三个步骤:
1、修改 main.dart 中的 main 函数;
2、异常捕获;
3、捕获内容上报。
接下来我们就分别看看三个过程中的实现原理。
第一步:修改 main.dart 中 main 函数
在我们之前项目代码逻辑的 main 函数中,都是使用下面的代码方式来启动运行 Flutter 项目
/// App 核心入口文件
void main() {
runApp(MyApp());
}
由于需要 catch 异常,因此我们将 runApp 执行放到异常捕获模块中去执行,修改如下的方式。
import 'package:two_you_friend/util/tools/app_sentry.dart';
/// App 核心入口文件
void main() {
AppSentry.runWithCatchError(MyApp());
}
这里引入了通用异常捕获模块,后续你在项目中,也可以直接使用该模块实现上报,接下来我们来看下 AppSentry 的实现异常捕获逻辑。
第二步:异常捕获
该功能的实现会应用到 FlutterError 和 runZonedGuarded 两个知识点。
在 Flutter 中可以通过 FlutterError 来捕获到运行期间的错误,包括构建期间、布局期间和绘制期间。
runZonedGuarded 则是使用 Zone.fork 创建一片新的区域去运行代码逻辑,也就是 runApp,当遇到错误时会执行其回调函数 onError,其次如果在项目使用了 Zone.current.handleUncaughtError 也会将错误抛出执行 onError 逻辑,具体我们看下代码,如下:
/// catch 组件异常
///
/// 开发模式下,本地打印,上线时则调用 sentry 平台
static void runWithCatchError(Widget appMain) {
// 捕获并上报 Flutter 异常
FlutterError.onError = (FlutterErrorDetails details) async {
if (!inProduction) {
FlutterError.dumpErrorToConsole(details);
} else {
Zone.current.handleUncaughtError(details.exception, details.stack);
}
};
runZonedGuarded<Future<Null>>(() async {
runApp(appMain);
}, (error, stackTrace) async {
await _reportError(error, stackTrace);
});
}
FlutterError.onError 来捕获异常,这里会判断是否在正式环境,如果是则在本地打印错误日志,如果不是则去执行 runZonedGuarded onError 逻辑。在 runZonedGuarded 代码中执行 runApp,遇到异常时则调用 _reportError 实现错误上报,接下来我们就看下 _reportError 的实现部分。
第三步:异常上报
上面已经获取到了客户端的具体异常信息,接下来我们利用 Sentry 平台将异常信息进行上报。应用 Sentry 平台前,需要去申请接入,具体接入方法在本课时的“Sentry 平台”中介绍,成功接入 Sentry 平台后,会提供一个 dsn。这里我们需要将其配置在该文件中,如下代码。因为 Sentry 是一个第三方库,因此需要在 pubspec.yaml 增加库依赖,并更新本地库。
/// 创建 SentryClient 用于将异常日志上报给 sentry 平台
final SentryClient _sentry = SentryClient(
dsn: 'https://[email protected]/5362342',
);
/// 判断当前环境类型
const bool inProduction = bool.fromEnvironment("dart.vm.product");
_sentry 就是利用 Sentry 第三方库创建的上报句柄,dsn 则是我们测试的上报标识,请大家测试时自己前往申请,不然无法看到自己的异常上报内容。上述代码中 inProduction 是用来获取是否为正式环境的逻辑。接下来我们看下上报逻辑,代码如下:
/// 上报异常的函数
static Future<void> _reportError(dynamic error, dynamic stackTrace) async {
if (!inProduction) { // 判断是否为正式环境
print(stackTrace);
}
// sentry 上报
final SentryResponse response = await _sentry.captureException(
exception: error,
stackTrace: stackTrace,
);
if (response.isSuccessful) {
print('Success! Event ID: ${response.eventId}');
} else {
print('Failed to report to Sentry.io: ${response.error}');
}
}
首先还是判断是否为正式环境,如果不是则直接打印,也可以应用下面的代码打印本地日志。如果是正式环境,则使用 _sentry.captureException 来上报异常,最后打印是否执行成功,后面一段是可以去掉的,主要是为了在真机调试时候查看。
FlutterError.dumpErrorToConsole(FlutterErrorDetails(exception: error));
以上就完成了通用上报工具的代码,接下来我们来教大家如何注册 Sentry 并创建项目获得 dsn。
Sentry 平台
我们来看下具体的创建步骤:
1.创建注册账户, 前往官网,具体请按照你自己的信息填写,注册完成后,直接登录;
2.创建项目,这个过程如果没有分组会让填写分组,按要求填写即可;
3.上报测试,里面会有各种 SDK 的例子,唯独是没有 Flutter 的,你可以不关注,直接进入项目中或者进入个人页面,可以看到如图 1 的界面。
图 1 Sentry 平台
4.按照图 2 的方法可以看到我们需要的 dsn,如果是新注册的,可以点击图 2 中的 Setup Sentry 快速找到;
图 2 dsn 查询指引
5.替换项目代码中的 dsn。
经过以上 5 个步骤就可以完成项目的接入,接下来我们就验证下上报是否正常。
上报测试
为了验证该功能,我们需要特意去修改正常的代码改为异常。这里我们将左侧菜单“图片流”中的代码做如下修改:
Widget build(BuildContext context) {
if (contentList == null) { // 判断是否为空
return Loading();
}
List<StructContentDetail> tmpList = [];
return ListView.separated(
scrollDirection: Axis.vertical,
shrinkWrap: true,
itemCount: contentList.length + 1, // 增加异常
......
上面只是 pages/home_page/img_flow.dart 部分代码,其中代码第 10 行就是增加了异常,由于数组长度超出了限制,这样会导致在 ListView.separated 抛出数组边界异常。请注意该异常捕获逻辑只会在 release 模式下生效,因此需要正式 build app,利用上一课时的知识,我们将 App build 为一个 apk 文件,然后安装到 Android 手机。
成功安装后,打开 App 选择左侧菜单的“图片流”,这时候你会看到界面异常,大概过 1-2 分钟后,我们前往 Sentry 平台,选择我们创建项目,就会看到如下图 3 的上报内容。
图 3 Sentry 异常例子
点击具体的上报内容,就可以看到详细的报错信息,这里面已经将报错的文件和具体函数都说明了,并打印了调用堆栈。你可以在提示信息中查看到具体的报错文件 img_flow.dart 以及具体的报错内容 RangeError ,通过文件以及报错详情,就可以非常容易的定位到具体问题了。
以上就是异常上报的工具模块,通过这种方式,我们就可以快速地收集问题并解决问题。由于本课时将 main.dart 进行了优化,因此这里也顺带介绍下重构的部分。
AppProvider
为了减轻 main.dart 中的代码,使其更精简,我们将 Provider 部分的逻辑转移到一个新的工具模块中,这个工具在 util/tools/app_provider 中。而 main.dart 中从原来调用函数内部的 _getProviders 方法,修改为调用 AppProvider 中的 getProviders 方法,main.dart 代码如下:
/// App 核心入口文件
void main() {
AppSentry.runWithCatchError(MyApp());
}
/// MyApp 核心入口界面
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return FutureBuilder<Widget>(
future: AppProvider.getProviders(
context,
MaterialApp(
title: 'Two You', // APP 名字
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue, // App 主题
),
routes: Router().registerRouter(),
home: Entrance()),
),
builder: (BuildContext context, AsyncSnapshot<Widget> snapshot) {
if (snapshot.error != null) {
return Container(
child: CommonError(),
);
}
return Container(
child: snapshot.data,
);
});
}
}