Template
Core 基础设施:Flavor 三包体系、Drift+SQLCipher 加密数据库 7 拦截器 Dio 网络栈、CrashReporter 崩溃日志、Sealed Failure 错误体系 5 色板主题系统、Auth 认证骨架(Clean Architecture)、Dev Panel、Mock Adapter 通过验证:flutter analyze(0 errors)、flutter test(全绿)、staging APK 74.8MB
130 lines
4.3 KiB
Dart
130 lines
4.3 KiB
Dart
import 'dart:io';
|
||
|
||
import 'package:device_info_plus/device_info_plus.dart';
|
||
import 'package:flutter/foundation.dart';
|
||
import 'package:package_info_plus/package_info_plus.dart';
|
||
import 'package:path/path.dart' as p;
|
||
import 'package:path_provider/path_provider.dart';
|
||
import 'package:sunny_mochi/core/crash/pending_crash_report.dart';
|
||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||
|
||
part 'crash_reporter.g.dart';
|
||
|
||
/// Riverpod provider:崩溃报告在 main.dart 通过 overrideWithValue 注入。
|
||
/// 默认值为 null(正常启动时无崩溃)。
|
||
@Riverpod(keepAlive: true)
|
||
PendingCrashReport? pendingCrash(Ref ref) => null;
|
||
|
||
/// 全局崩溃日志服务(纯静态,不依赖 Riverpod / Flutter framework)。
|
||
///
|
||
/// **生命周期**:
|
||
/// 1. `preInit()` — main() 最早期调用,异步缓存路径 + 设备信息
|
||
/// 2. `consumePending()` — 读取并删除上次崩溃文件(一次性消费)
|
||
/// 3. `installHooks()` — 在 SentrySetup.init appRunner 内调用,链式挂载钩子
|
||
///
|
||
/// **崩溃写入**:仅用 `File.writeAsStringSync(flush: true)`,绝对不能 await。
|
||
abstract class CrashReporter {
|
||
static String? _crashFilePath;
|
||
static String _deviceModel = 'unknown';
|
||
static String _osVersion = 'unknown';
|
||
static String _appVersion = 'unknown';
|
||
static String _appBuild = '0';
|
||
|
||
/// 步骤 1:异步预初始化(收集路径 + 设备信息)。
|
||
static Future<void> preInit() async {
|
||
try {
|
||
final dir = await getApplicationDocumentsDirectory();
|
||
_crashFilePath = p.join(dir.path, 'crash_report.json');
|
||
|
||
final pkg = await PackageInfo.fromPlatform();
|
||
_appVersion = pkg.version;
|
||
_appBuild = pkg.buildNumber;
|
||
|
||
if (kIsWeb) return;
|
||
|
||
if (Platform.isAndroid) {
|
||
final info = await DeviceInfoPlugin().androidInfo;
|
||
_deviceModel = '${info.manufacturer} ${info.model}'.trim();
|
||
_osVersion = 'Android ${info.version.release}';
|
||
} else if (Platform.isIOS) {
|
||
final info = await DeviceInfoPlugin().iosInfo;
|
||
_deviceModel = info.utsname.machine;
|
||
_osVersion = '${info.systemName} ${info.systemVersion}';
|
||
} else {
|
||
_deviceModel = Platform.operatingSystem;
|
||
_osVersion = Platform.operatingSystemVersion;
|
||
}
|
||
} catch (_) {
|
||
// preInit 失败不能阻断启动流程
|
||
}
|
||
}
|
||
|
||
/// 步骤 2:一次性消费崩溃文件(读取 + 立即删除)。
|
||
/// 返回 null 表示本次是正常启动。
|
||
static PendingCrashReport? consumePending() {
|
||
final path = _crashFilePath;
|
||
if (path == null) return null;
|
||
|
||
final file = File(path);
|
||
if (!file.existsSync()) return null;
|
||
|
||
try {
|
||
final raw = file.readAsStringSync();
|
||
file.deleteSync();
|
||
return PendingCrashReport.fromRawJson(raw);
|
||
} catch (_) {
|
||
try {
|
||
file.deleteSync();
|
||
} catch (_) {}
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/// 步骤 3:安装全局崩溃钩子(必须在 SentrySetup appRunner 内调用)。
|
||
///
|
||
/// 采用链式挂载:先读出 Sentry 已设置的 handler,我们包一层写回,
|
||
/// 确保 Sentry 依然能接到所有错误。
|
||
static void installHooks() {
|
||
final previousOnError = PlatformDispatcher.instance.onError;
|
||
PlatformDispatcher.instance.onError = (error, stack) {
|
||
_writeCrashSync(kind: 'dart', error: error, stackTrace: stack);
|
||
return previousOnError?.call(error, stack) ?? false;
|
||
};
|
||
}
|
||
|
||
/// 同步写入崩溃文件(crash hook 专用,不能有任何 await)。
|
||
static void _writeCrashSync({
|
||
required String kind,
|
||
required Object error,
|
||
required StackTrace stackTrace,
|
||
}) {
|
||
final path = _crashFilePath;
|
||
if (path == null) return;
|
||
|
||
try {
|
||
final stackStr = stackTrace.toString();
|
||
final truncated = stackStr.length > 4096
|
||
? '${stackStr.substring(0, 4096)}\n...(truncated)'
|
||
: stackStr;
|
||
|
||
final report = PendingCrashReport(
|
||
kind: kind,
|
||
error: error.toString(),
|
||
stackTrace: truncated,
|
||
occurredAt: DateTime.now().toUtc().toIso8601String(),
|
||
deviceModel: _deviceModel,
|
||
osVersion: _osVersion,
|
||
appVersion: _appVersion,
|
||
appBuild: _appBuild,
|
||
);
|
||
|
||
File(path).writeAsStringSync(
|
||
report.toRawJson(),
|
||
flush: true,
|
||
);
|
||
} catch (_) {
|
||
// crash hook 内绝对不能抛出异常
|
||
}
|
||
}
|
||
}
|