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 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 内绝对不能抛出异常 } } }