refactor: remove CrashReporter and related logging components

- Removed CrashReporter interface and its implementation, SentryCrashReporter.
- Deleted log buffering functionality and associated LogBuffer class.
- Updated LoggerAppLogger to eliminate dependencies on removed components.
- Simplified logging structure to focus on AppLogger and sensitive data scrubbing.
- Adjusted AppEnv to remove Sentry-related configurations.
- Refactored tests to align with the new logging architecture.
This commit is contained in:
Guangfei.Zhao
2026-09-03 11:39:04 +08:00
parent d786fc31ff
commit 782fba80bc
39 changed files with 192 additions and 1152 deletions
+1 -2
View File
@@ -1,7 +1,6 @@
{
"_comment": "dev 环境。构建时必须带 --dart-define-from-file=env/dev.json,否则 AppEnv.fromDartDefine 会直接抛错。所有地址都是占位符,TODO(ops) 待运维确认真实域名。",
"_comment": "dev 环境。未指定 flavor 的启动(包括 DevEco 默认 HAP 构建)使用此配置。所有地址都是占位符,TODO(ops) 待运维确认真实域名。",
"API_BASE_URL": "https://api-dev.example.com",
"ENABLE_LOG": true,
"SENTRY_DSN": "",
"H5_ALLOWED_HOSTS": "h5-dev.example.com"
}
+1 -2
View File
@@ -1,7 +1,6 @@
{
"_comment": "prod 环境。ENABLE_LOG 必须为 false(13:生产不写控制台、不落日志文件)。TODO(ops) 待运维确认真实域名与 Sentry DSN。",
"_comment": "prod 环境。ENABLE_LOG 必须为 false,生产不写控制台日志。TODO(ops) 待运维确认真实域名。",
"API_BASE_URL": "https://api.example.com",
"ENABLE_LOG": false,
"SENTRY_DSN": "",
"H5_ALLOWED_HOSTS": "h5.example.com"
}
-1
View File
@@ -2,6 +2,5 @@
"_comment": "uat 环境。TODO(ops) 待运维确认真实域名与 Sentry DSN。",
"API_BASE_URL": "https://api-uat.example.com",
"ENABLE_LOG": true,
"SENTRY_DSN": "",
"H5_ALLOWED_HOSTS": "h5-uat.example.com"
}
+11 -2
View File
@@ -7,7 +7,7 @@
//
// 跑法:
// flutter test integration_test/app_test.dart \
// --flavor dev -t lib/main_dev.dart --dart-define-from-file=env/dev.json
// --flavor dev
// ---------------------------------------------------------------------------
import 'package:app/bootstrap.dart';
@@ -20,7 +20,16 @@ void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('冷启动后停在登录页', (WidgetTester tester) async {
await bootstrap(AppEnv.fromDartDefine(flavor: 'dev'));
await bootstrap(
AppEnv.fromConfig(
flavor: 'dev',
values: <String, Object?>{
'API_BASE_URL': 'https://api-dev.example.com',
'ENABLE_LOG': true,
'H5_ALLOWED_HOSTS': 'h5-dev.example.com',
},
),
);
await tester.pumpAndSettle();
// 没有 token → SessionUnauthenticated → redirect 到 /login(04)。
+3 -4
View File
@@ -62,7 +62,7 @@ Xcode 自己维护的二进制风格文本,手写会在下一次 Xcode 打开
6. 验证:
```bash
cd app
flutter build ios --flavor dev -t lib/main_dev.dart --dart-define-from-file=env/dev.json
flutter build ios --flavor dev
```
三个 flavor 各跑一次,确认桌面上能同时装下三个图标、名字不同。
@@ -71,6 +71,5 @@ Xcode 自己维护的二进制风格文本,手写会在下一次 Xcode 打开
- **CocoaPods 和 flavor 无关**:`Podfile` 不需要改,pod 是按 target 装的,
不是按 configuration。但新增 configuration 后要跑一次 `pod install`,否则
会报 `Unable to find a configuration named 'Debug-dev'`。
- **`--flavor` 和 `-t` 必须同时给**。只给 `--flavor dev` 会用默认的
`lib/main.dart`——这个文件在本仓库里**不存在**(入口是 `main_dev.dart`),
报错信息是找不到文件,和 flavor 看不出关系。
- 项目固定使用 `lib/main.dart`。`--flavor` 除了选择 Scheme,还会让启动代码
选择同名的内置环境配置;不要再额外传 `-t` 或 `--dart-define-from-file`。
+8 -50
View File
@@ -1,6 +1,6 @@
/// 三个入口共用的启动编排。
/// 唯一入口共用的启动编排。
///
/// 来源:08(多环境)、13(Sentry / 日志)、12(全局错误)、03(Riverpod 装配)。
/// 来源:08(多环境)、12(全局错误)、03(Riverpod 装配)。
library;
import 'package:app/src/app_widget.dart';
@@ -20,12 +20,11 @@ import 'package:feature_auth/feature_auth.dart';
import 'package:feature_home/feature_home.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
/// TODO(app): 换成 package_info_plus 读真实版本,现在写死会在灰度期骗人。
const String _appVersion = '1.0.0+1';
/// 全部三个 main_*.dart 都只调这一个函数。
/// 唯一 `main.dart` 调用的启动函数。
///
/// ---------------------------------------------------------------------------
/// 这个函数是**整个仓库唯一一处知道所有包的地方**。各个 core_* 只声明自己需要
@@ -40,14 +39,7 @@ Future<void> bootstrap(AppEnv env) async {
// 装一次全局单例,供拿不到 Ref 的 TokenRefresher 用;重复调用会抛错。
AppEnv.install(env);
final LogBuffer buffer = LogBuffer();
final AppLogger logger = LoggerAppLogger(env: env, buffer: buffer);
// dsn 为空(dev 默认)时连 SDK 都不初始化:开发期的噪音不该混进线上数据。
final bool sentryEnabled = env.sentryDsn.isNotEmpty;
final CrashReporter reporter = sentryEnabled
? const SentryCrashReporter()
: const NoopCrashReporter();
final AppLogger logger = LoggerAppLogger(env: env);
final Prefs prefs = Prefs();
final ClientInfo clientInfo = ClientInfo(
@@ -62,13 +54,11 @@ Future<void> bootstrap(AppEnv env) async {
Widget buildApp() {
return ProviderScope(
observers: <ProviderObserver>[ErrorObserver(logger: logger, reporter: reporter)],
observers: <ProviderObserver>[ErrorObserver(logger: logger)],
overrides: [
// --- 基础设施:值已经在上面造好了,直接注入 -----------------------
appEnvProvider.overrideWithValue(env), // core_foundation
appLoggerProvider.overrideWithValue(logger), // core_logging
logBufferProvider.overrideWithValue(buffer), // core_logging(和 beforeSend 同一实例)
crashReporterProvider.overrideWithValue(reporter), // core_logging
prefsProvider.overrideWithValue(prefs), // core_storage
clientInfoProvider.overrideWithValue(clientInfo), // core_network 端口
// --- 端口:接口在 core_*,实现在能依赖 core_network 的这一层 --------
@@ -87,10 +77,7 @@ Future<void> bootstrap(AppEnv env) async {
),
sessionObserversProvider.overrideWith(
(Ref ref) => <SessionObserver>[
AppSessionObserver(
analytics: ref.watch(analyticsProvider),
reporter: ref.watch(crashReporterProvider),
),
AppSessionObserver(analytics: ref.watch(analyticsProvider)),
],
),
@@ -98,16 +85,8 @@ Future<void> bootstrap(AppEnv env) async {
appRoutesProvider.overrideWith(
(Ref ref) => <RouteBase>[...buildAuthRoutes(), ...buildHomeRoutes()],
),
navigatorObserversProvider.overrideWith(
(Ref ref) => <NavigatorObserver>[
CrashBreadcrumbObserver(ref.watch(crashReporterProvider)),
],
),
routeReporterProvider.overrideWith(
(Ref ref) => AppRouteReporter(
logger: ref.watch(appLoggerProvider),
reporter: ref.watch(crashReporterProvider),
),
(Ref ref) => AppRouteReporter(logger: ref.watch(appLoggerProvider)),
),
// --- 日志出口:core_network 只知道"往这里写字符串" -------------------
@@ -123,26 +102,5 @@ Future<void> bootstrap(AppEnv env) async {
);
}
if (!sentryEnabled) {
runApp(buildApp());
return;
}
await SentryFlutter.init(
(SentryFlutterOptions options) {
options.dsn = env.sentryDsn;
options.environment = env.flavor.name;
options.release = 'conti-retail-app@$_appVersion';
options.tracesSampleRate = env.isProd ? 0.1 : 1.0;
// 合规红线:不自动带用户 IP / 请求头 / cookie。
options.sendDefaultPii = false;
options.beforeBreadcrumb = scrubBreadcrumb;
options.beforeSend = buildScrubEvent(buffer);
options.debug = false;
},
// 用 appRunner 而不是自己写 FlutterError.onError:SentryFlutter 已经在
// 里面装好了 Flutter / PlatformDispatcher / Zone 三层钩子,再手写一遍
// 会**每个异常上报两次**(13)。
appRunner: () => runApp(buildApp()),
);
runApp(buildApp());
}
+15
View File
@@ -0,0 +1,15 @@
/// 应用唯一 Dart 入口。
///
/// Android、iOS 与 Web 用 `--flavor dev|uat|prod` 选择环境;未提供 flavor
/// 的平台(例如 DevEco 的默认 HAP 构建)统一使用 dev。
library;
import 'package:app/bootstrap.dart';
import 'package:app/src/bundled_app_env.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show appFlavor;
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await bootstrap(await loadBundledAppEnv(flavor: appFlavor));
}
-14
View File
@@ -1,14 +0,0 @@
/// dev 环境入口。
///
/// 跑法(08):
/// ```
/// flutter run --flavor dev -t lib/main_dev.dart --dart-define-from-file=env/dev.json
/// ```
/// flavor 名写死在这里而不是从 dart-define 读——"用 dev 的入口配了别的环境的
/// json"这种事故必须在代码里看得见。
library;
import 'package:app/bootstrap.dart';
import 'package:core_foundation/core_foundation.dart';
Future<void> main() => bootstrap(AppEnv.fromDartDefine(flavor: 'dev'));
-14
View File
@@ -1,14 +0,0 @@
/// prod 环境入口。
///
/// 跑法(08):
/// ```
/// flutter run --flavor prod -t lib/main_prod.dart --dart-define-from-file=env/prod.json
/// ```
/// flavor 名写死在这里而不是从 dart-define 读——"用 prod 的入口配了别的环境的
/// json"这种事故必须在代码里看得见。
library;
import 'package:app/bootstrap.dart';
import 'package:core_foundation/core_foundation.dart';
Future<void> main() => bootstrap(AppEnv.fromDartDefine(flavor: 'prod'));
-14
View File
@@ -1,14 +0,0 @@
/// uat 环境入口。
///
/// 跑法(08):
/// ```
/// flutter run --flavor uat -t lib/main_uat.dart --dart-define-from-file=env/uat.json
/// ```
/// flavor 名写死在这里而不是从 dart-define 读——"用 uat 的入口配了别的环境的
/// json"这种事故必须在代码里看得见。
library;
import 'package:app/bootstrap.dart';
import 'package:core_foundation/core_foundation.dart';
Future<void> main() => bootstrap(AppEnv.fromDartDefine(flavor: 'uat'));
+29
View File
@@ -0,0 +1,29 @@
/// 随 App 打包的环境配置加载器。
library;
import 'dart:convert';
import 'package:core_foundation/core_foundation.dart';
import 'package:flutter/foundation.dart' show FlutterError;
import 'package:flutter/services.dart';
/// 根据 Flutter 的 flavor 加载对应环境配置;未指定时默认开发环境。
Future<AppEnv> loadBundledAppEnv({String? flavor}) async {
final String selectedFlavor = flavor ?? AppFlavor.dev.name;
final String assetPath = 'env/$selectedFlavor.json';
try {
final String jsonObj = await rootBundle.loadString(assetPath);
final decoded = jsonDecode(jsonObj);
if (decoded is! Map<String, dynamic>) {
throw const FormatException('根节点必须是 JSON 对象');
}
return AppEnv.fromConfig(flavor: selectedFlavor, values: decoded.cast<String, Object?>());
} on StateError {
rethrow;
} on FormatException catch (error) {
throw StateError('环境配置 $assetPath 格式错误:${error.message}');
} on FlutterError catch (error) {
throw StateError('无法加载环境配置 $assetPath:${error.message}');
}
}
+2 -6
View File
@@ -10,7 +10,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
///
/// ---------------------------------------------------------------------------
/// 它是**兜底**,不是主路径:UI 该显示的错误由 `AsyncValueView` 负责,这里只
/// 负责"这个异常有没有人处理过"之外的另一件事——落日志和上报。
/// 负责"这个异常有没有人处理过"之外的另一件事——落日志。
///
/// 三类要**主动排除**,否则线上告警会被噪音淹没:
/// - [BusinessException]:后端明确告诉我们"这个操作不允许",是预期内的流程
@@ -20,14 +20,11 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
/// ---------------------------------------------------------------------------
final class ErrorObserver extends ProviderObserver {
/// 构造。
ErrorObserver({required this.logger, required this.reporter});
ErrorObserver({required this.logger});
/// 日志出口。
final AppLogger logger;
/// 崩溃上报出口。
final CrashReporter reporter;
@override
void providerDidFail(ProviderObserverContext context, Object error, StackTrace stackTrace) {
final String name = context.provider.name ?? context.provider.runtimeType.toString();
@@ -42,6 +39,5 @@ final class ErrorObserver extends ProviderObserver {
}
logger.e('provider 失败 $name', error: error, stackTrace: stackTrace);
reporter.report(error, stackTrace, extra: <String, String>{'provider': name});
}
}
+5 -21
View File
@@ -1,4 +1,4 @@
/// 会话事件的旁路接线:埋点身份、崩溃上报的用户上下文、路由错误上报。
/// 会话事件的旁路接线:埋点身份与路由错误日志。
///
/// 这些都是 core_auth 声明的端口(`session_ports.dart` / `core_router/ports.dart`)
/// 的实现——core_auth 不能依赖 core_analytics / core_logging,所以实现落在这里。
@@ -9,26 +9,20 @@ import 'package:core_auth/core_auth.dart';
import 'package:core_logging/core_logging.dart';
import 'package:core_router/core_router.dart';
/// 把会话变化广播给埋点和崩溃上报。
/// 把会话变化广播给埋点。
class AppSessionObserver implements SessionObserver {
/// 构造。
const AppSessionObserver({required this.analytics, required this.reporter});
const AppSessionObserver({required this.analytics});
/// 埋点。
final Analytics analytics;
/// 崩溃上报。
final CrashReporter reporter;
@override
void onUserIdentified(UserContext user) {
analytics.identify(user.userId);
analytics.registerSuperProperties(<String, Object?>{
AnalyticsSuperProperty.roleCode: user.roleCode,
});
// 只传 userId,不传手机号——Sentry 侧 sendDefaultPii = false 的前提就是
// 我们自己也不往里塞 PII。
reporter.setUser(user.userId);
}
@override
@@ -37,7 +31,6 @@ class AppSessionObserver implements SessionObserver {
analytics.registerSuperProperties(<String, Object?>{
AnalyticsSuperProperty.storeId: store.storeId,
});
reporter.setTag('storeId', '${store.storeId}');
}
@override
@@ -46,7 +39,6 @@ class AppSessionObserver implements SessionObserver {
analytics.track(AnalyticsEvent.logout, <String, Object?>{AnalyticsParam.reason: reason.name});
// 门店设备是共用的:不 reset,下一个人的数据会串到上一个人身上。
analytics.reset();
reporter.clearUser();
}
@override
@@ -57,26 +49,18 @@ class AppSessionObserver implements SessionObserver {
}
}
/// 路由未命中时上报。
/// 路由未命中时记录日志。
class AppRouteReporter implements RouteReporter {
/// 构造。
const AppRouteReporter({required this.logger, required this.reporter});
const AppRouteReporter({required this.logger});
/// 日志。
final AppLogger logger;
/// 崩溃上报。
final CrashReporter reporter;
@override
void onRouteNotFound(String location) {
// 记路径不记 query——H5 相关路径的 query 里带票据(13 §脱敏)。
final String path = Uri.tryParse(location)?.path ?? location;
logger.w('路由未命中: $path');
reporter.report(
StateError('route not found'),
StackTrace.current,
extra: <String, String>{AnalyticsParam.path: path},
);
}
}
+4 -11
View File
@@ -26,7 +26,6 @@ dependencies:
flutter_riverpod: 3.2.1
intl: any
native_scan: ^0.1.0
sentry_flutter: ^9.26.0
dev_dependencies:
flutter_lints: ^6.0.0
@@ -34,19 +33,13 @@ dev_dependencies:
sdk: flutter
integration_test:
sdk: flutter
sentry_dart_plugin: ^3.4.0
# 只为测试拿 InMemorySharedPreferencesAsync,不在 lib/ 里出现。
shared_preferences_platform_interface: ^2.4.2
flutter:
uses-material-design: true
generate: true
# release 构建后由 CI 调用 `dart run sentry_dart_plugin` 上传符号表。
# 见 08 §release 构建 和 13 §崩溃上报。
sentry:
upload_debug_symbols: true
upload_source_maps: false
project: conti-retail-app
org: continental
# auth_token 只从 CI 变量 SENTRY_AUTH_TOKEN 读,绝不写进仓库
assets:
- env/dev.json
- env/uat.json
- env/prod.json