Commit 423c2556605a8f4835b003e1bc1f88412ef7c43c

Authored by 常守达
Committed by 权海
1 parent 62536c66

feat(ui): 退登添加日志诊断

1 import 'dart:async'; 1 import 'dart:async';
2 2
  3 +import 'package:doublefeel_flutter/core/logging/app_diagnostic_log_store.dart';
3 import 'package:flutter/widgets.dart'; 4 import 'package:flutter/widgets.dart';
4 import 'package:get/get.dart'; 5 import 'package:get/get.dart';
5 6
@@ -51,4 +52,47 @@ abstract final class AppBootstrap { @@ -51,4 +52,47 @@ abstract final class AppBootstrap {
51 unawaited(AppConfigService.to.fetch()); 52 unawaited(AppConfigService.to.fetch());
52 } 53 }
53 } 54 }
  55 +
  56 + static Future<void> initForBackground() async {
  57 + WidgetsFlutterBinding.ensureInitialized();
  58 + AppLogger.init();
  59 +
  60 + await UserAgentProvider.init();
  61 +
  62 + final local = await LocalStorage.open();
  63 +
  64 + final environmentConfig = AppEnvironmentConfig(local);
  65 + await environmentConfig.init();
  66 + // 1. 【核心】只读取已保存的地区与环境,不要在后台调用 _detectRegion() 重新探测并写入 SharedPreferences,防止误切海外服
  67 + // environmentConfig.environment.value = await local.readEnvironment();
  68 + // environmentConfig.region.value =
  69 + // await local.readRegion() ?? AppRegion.china;
  70 +
  71 + final userPreferencesStorage =
  72 + UserPreferencesStorage(local.sharedPreferences);
  73 + await userPreferencesStorage.init();
  74 +
  75 + final userAccountStorage = UserAccountStorage(local.sharedPreferences);
  76 +
  77 + InitialBinding(
  78 + environmentConfig: environmentConfig,
  79 + userPreferencesStorage: userPreferencesStorage,
  80 + userAccountStorage: userAccountStorage,
  81 + localStorage: local,
  82 + ).dependencies();
  83 +
  84 + NativeHealthDataUpdateBridge.initialize();
  85 + // 2. 后台无 UI,不需要初始化全局 Loading 弹窗
  86 + LoadingService.init();
  87 + // 3. 后台计算不需要重复初始化融云/分析等前台 SDK
  88 + if (local.termsAgreed) {
  89 + await Get.find<UserStateService>().initializeSdks();
  90 + unawaited(AppConfigService.to.fetch());
  91 + }
  92 + AppDiagnosticLogStore.instance.log(
  93 + category: 'initForBackground',
  94 + action: 'success',
  95 + details: {},
  96 + );
  97 + }
54 } 98 }
@@ -8,6 +8,7 @@ import 'package:doublefeel_flutter/core/services/user_state_service.dart'; @@ -8,6 +8,7 @@ import 'package:doublefeel_flutter/core/services/user_state_service.dart';
8 import 'package:flutter/material.dart'; 8 import 'package:flutter/material.dart';
9 import 'package:get/get.dart'; 9 import 'package:get/get.dart';
10 10
  11 +import '../../../../core/logging/app_diagnostic_log_store.dart';
11 import '../../../../core/logging/app_logger.dart'; 12 import '../../../../core/logging/app_logger.dart';
12 import '../../../../core/services/raw_data_service/health_raw_data_core_service.dart'; 13 import '../../../../core/services/raw_data_service/health_raw_data_core_service.dart';
13 import '../../../../core/services/raw_data_service/health_raw_models.dart'; 14 import '../../../../core/services/raw_data_service/health_raw_models.dart';
@@ -32,6 +33,7 @@ class DeveloperOptionsItem { @@ -32,6 +33,7 @@ class DeveloperOptionsItem {
32 } 33 }
33 34
34 enum DeveloperOptionsAction { 35 enum DeveloperOptionsAction {
  36 + shareAppDiagnosticLog,
35 shareHealthRawDataDatabase, 37 shareHealthRawDataDatabase,
36 shareNativeAppleHealthObserverRecord, 38 shareNativeAppleHealthObserverRecord,
37 shareFlutterObserverRecord, 39 shareFlutterObserverRecord,
@@ -75,6 +77,10 @@ class DeveloperOptionsController extends GetxController { @@ -75,6 +77,10 @@ class DeveloperOptionsController extends GetxController {
75 routeName: Routes.TOKEN_LOGIN, 77 routeName: Routes.TOKEN_LOGIN,
76 ), 78 ),
77 DeveloperOptionsItem( 79 DeveloperOptionsItem(
  80 + title: '分享启动及登录态诊断日志',
  81 + action: DeveloperOptionsAction.shareAppDiagnosticLog,
  82 + ),
  83 + DeveloperOptionsItem(
78 title: '分享本地计算数据库文件', 84 title: '分享本地计算数据库文件',
79 action: DeveloperOptionsAction.shareHealthRawDataDatabase, 85 action: DeveloperOptionsAction.shareHealthRawDataDatabase,
80 ), 86 ),
@@ -149,6 +155,9 @@ class DeveloperOptionsController extends GetxController { @@ -149,6 +155,9 @@ class DeveloperOptionsController extends GetxController {
149 } 155 }
150 156
151 switch (item.action) { 157 switch (item.action) {
  158 + case DeveloperOptionsAction.shareAppDiagnosticLog:
  159 + await shareAppDiagnosticLog();
  160 + break;
152 case DeveloperOptionsAction.shareHealthRawDataDatabase: 161 case DeveloperOptionsAction.shareHealthRawDataDatabase:
153 await shareHealthRawDataDatabase(); 162 await shareHealthRawDataDatabase();
154 break; 163 break;
@@ -209,6 +218,16 @@ class DeveloperOptionsController extends GetxController { @@ -209,6 +218,16 @@ class DeveloperOptionsController extends GetxController {
209 } 218 }
210 } 219 }
211 220
  221 + Future<void> shareAppDiagnosticLog() async {
  222 + try {
  223 + final bytes = await AppDiagnosticLogStore.instance.readBytes();
  224 + await _platformHostApi.shareFileData(bytes);
  225 + } catch (error, stackTrace) {
  226 + AppLogger.e('Share AppDiagnosticLog failed', error, stackTrace);
  227 + Get.snackbar('Share failed', error.toString());
  228 + }
  229 + }
  230 +
212 Future<void> shareHealthRawDataDatabase() async { 231 Future<void> shareHealthRawDataDatabase() async {
213 try { 232 try {
214 final bytes = await _healthRawDataCoreService.readDatabaseFileBytes(); 233 final bytes = await _healthRawDataCoreService.readDatabaseFileBytes();
@@ -2,6 +2,7 @@ import 'package:doublefeel_flutter/app/modules/friends/controllers/friend_trend_ @@ -2,6 +2,7 @@ import 'package:doublefeel_flutter/app/modules/friends/controllers/friend_trend_
2 import 'package:doublefeel_flutter/app/modules/friends/controllers/friends_controller.dart'; 2 import 'package:doublefeel_flutter/app/modules/friends/controllers/friends_controller.dart';
3 import 'package:doublefeel_flutter/core/constants/flutter_bridge_method_name.dart'; 3 import 'package:doublefeel_flutter/core/constants/flutter_bridge_method_name.dart';
4 import 'package:doublefeel_flutter/core/constants/intent_keys.dart'; 4 import 'package:doublefeel_flutter/core/constants/intent_keys.dart';
  5 +import 'package:doublefeel_flutter/core/logging/app_diagnostic_log_store.dart';
5 import 'package:doublefeel_flutter/core/network/api/friend_api.dart'; 6 import 'package:doublefeel_flutter/core/network/api/friend_api.dart';
6 import 'package:doublefeel_flutter/core/result/app_result.dart'; 7 import 'package:doublefeel_flutter/core/result/app_result.dart';
7 import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_data_core_service.dart'; 8 import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_data_core_service.dart';
@@ -47,6 +48,13 @@ class HomeController extends GetxController { @@ -47,6 +48,13 @@ class HomeController extends GetxController {
47 SchedulerBinding.instance.addPostFrameCallback((_) async { 48 SchedulerBinding.instance.addPostFrameCallback((_) async {
48 try { 49 try {
49 final route = await _platformHostApi.requestUnhandedUrl(); 50 final route = await _platformHostApi.requestUnhandedUrl();
  51 + AppDiagnosticLogStore.instance.log(
  52 + category: 'requestUnhandedUrl',
  53 + action: 'native_return_url',
  54 + details: {
  55 + 'route': route ?? '',
  56 + },
  57 + );
50 // final route ='doublefeel://flutter/home?tab=today&scroll_to_latest_hrv=true'; 58 // final route ='doublefeel://flutter/home?tab=today&scroll_to_latest_hrv=true';
51 // final route = 'doublefeel://flutter/friend-home?user_id=131'; 59 // final route = 'doublefeel://flutter/friend-home?user_id=131';
52 // final route = 'doublefeel://flutter/home?tab=trend&type=sleep'; 60 // final route = 'doublefeel://flutter/home?tab=trend&type=sleep';
  1 +import 'package:doublefeel_flutter/core/logging/app_diagnostic_log_store.dart';
1 import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_data_core_service.dart'; 2 import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_data_core_service.dart';
2 import 'package:doublefeel_flutter/core/util/app_toast.dart'; 3 import 'package:doublefeel_flutter/core/util/app_toast.dart';
3 import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; 4 import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
@@ -43,6 +44,11 @@ class SplashController extends GetxController { @@ -43,6 +44,11 @@ class SplashController extends GetxController {
43 final splashDelay = 44 final splashDelay =
44 Get.testMode ? Duration.zero : const Duration(milliseconds: 800); 45 Get.testMode ? Duration.zero : const Duration(milliseconds: 800);
45 await Future<void>.delayed(splashDelay); 46 await Future<void>.delayed(splashDelay);
  47 + AppDiagnosticLogStore.instance.log(
  48 + category: 'Splash',
  49 + action: 'navigate_to_login_due_to_empty_token',
  50 + details: {},
  51 + );
46 Get.offAllNamed(AppRoutes.login); 52 Get.offAllNamed(AppRoutes.login);
47 return; 53 return;
48 } 54 }
@@ -55,6 +61,13 @@ class SplashController extends GetxController { @@ -55,6 +61,13 @@ class SplashController extends GetxController {
55 } catch (_) {} 61 } catch (_) {}
56 await Get.offAllNamed(AppRoutes.home); 62 await Get.offAllNamed(AppRoutes.home);
57 } else { 63 } else {
  64 + AppDiagnosticLogStore.instance.log(
  65 + category: 'Splash',
  66 + action: 'navigate_to_login_due_to_session_refresh_failed',
  67 + details: {
  68 + 'tokenLength': token.length,
  69 + },
  70 + );
58 Get.offAllNamed(AppRoutes.login); 71 Get.offAllNamed(AppRoutes.login);
59 } 72 }
60 } 73 }
@@ -77,6 +90,14 @@ class SplashController extends GetxController { @@ -77,6 +90,14 @@ class SplashController extends GetxController {
77 return true; 90 return true;
78 91
79 case AppFailure(:final error): 92 case AppFailure(:final error):
  93 + AppDiagnosticLogStore.instance.log(
  94 + category: 'Splash',
  95 + action: 'refresh_session_failure',
  96 + details: {
  97 + 'errorType': error.runtimeType.toString(),
  98 + 'displayMessage': error.displayMessage,
  99 + },
  100 + );
80 if (error.isAccessTokenInvalid) { 101 if (error.isAccessTokenInvalid) {
81 return false; 102 return false;
82 } 103 }
1 import 'package:doublefeel_flutter/app/utils/platform_compact.dart'; 1 import 'package:doublefeel_flutter/app/utils/platform_compact.dart';
  2 +import 'package:doublefeel_flutter/core/logging/app_diagnostic_log_store.dart';
2 import 'package:doublefeel_flutter/core/network/dio_client.dart'; 3 import 'package:doublefeel_flutter/core/network/dio_client.dart';
3 import 'package:doublefeel_flutter/core/platform/pigeon_api_facade.dart'; 4 import 'package:doublefeel_flutter/core/platform/pigeon_api_facade.dart';
4 import 'package:flutter/foundation.dart'; 5 import 'package:flutter/foundation.dart';
@@ -24,6 +25,20 @@ class AppEnvironmentConfig { @@ -24,6 +25,20 @@ class AppEnvironmentConfig {
24 _isNativeDebug = await _platformHostApi.isDebugEnvoriment(); 25 _isNativeDebug = await _platformHostApi.isDebugEnvoriment();
25 if (isOhos()) { 26 if (isOhos()) {
26 region.value = AppRegion.china; 27 region.value = AppRegion.china;
  28 + await setRegion(AppRegion.china);
  29 + return this;
  30 + }
  31 + AppDiagnosticLogStore.instance.log(
  32 + category: 'Debug',
  33 + action: 'detect_debug_from_native_success',
  34 + details: {
  35 + 'isDebug': _isNativeDebug,
  36 + },
  37 + );
  38 + final stored = await _storage.readRegion();
  39 + if (stored == null) {
  40 + final detected = await _detectRegion();
  41 + await setRegion(detected);
27 } else { 42 } else {
28 final stored = await _storage.readRegion(); 43 final stored = await _storage.readRegion();
29 if (stored == null) { 44 if (stored == null) {
@@ -41,11 +56,27 @@ class AppEnvironmentConfig { @@ -41,11 +56,27 @@ class AppEnvironmentConfig {
41 56
42 Future<AppRegion> _detectRegion() async { 57 Future<AppRegion> _detectRegion() async {
43 try { 58 try {
44 - return await _platformHostApi.isChinaRegion()  
45 - ? AppRegion.china  
46 - : AppRegion.global;  
47 - } catch (_) {  
48 - return _detectRegionFromLocale(); 59 + final isChina = await _platformHostApi.isChinaRegion();
  60 + AppDiagnosticLogStore.instance.log(
  61 + category: 'Region',
  62 + action: 'detect_region_from_native_success',
  63 + details: {
  64 + 'isChina': isChina,
  65 + },
  66 + );
  67 + return isChina ? AppRegion.china : AppRegion.global;
  68 + } catch (error, stackTrace) {
  69 + final fallback = _detectRegionFromLocale();
  70 + AppDiagnosticLogStore.instance.log(
  71 + category: 'Region',
  72 + action: 'detect_region_from_native_failed',
  73 + details: {
  74 + 'error': error.toString(),
  75 + 'stackTrace': stackTrace.toString(),
  76 + 'fallbackResult': fallback.name,
  77 + },
  78 + );
  79 + return fallback;
49 } 80 }
50 } 81 }
51 82
@@ -77,8 +108,17 @@ class AppEnvironmentConfig { @@ -77,8 +108,17 @@ class AppEnvironmentConfig {
77 if (Get.isRegistered<DioClient>()) { 108 if (Get.isRegistered<DioClient>()) {
78 Get.find<DioClient>().refreshBaseUrl(); 109 Get.find<DioClient>().refreshBaseUrl();
79 } 110 }
80 - } catch (e) { 111 + } catch (e, stackTrace) {
81 debugPrint('setRegion failed: $e'); 112 debugPrint('setRegion failed: $e');
  113 + AppDiagnosticLogStore.instance.log(
  114 + category: 'Region',
  115 + action: 'set_region_failed',
  116 + details: {
  117 + 'error': e.toString(),
  118 + 'stackTrace': stackTrace.toString(),
  119 + 'targetRegion': value.name,
  120 + },
  121 + );
82 } 122 }
83 } 123 }
84 124
@@ -2,6 +2,7 @@ import 'package:doublefeel_flutter/app/actions/dialog_action.dart'; @@ -2,6 +2,7 @@ import 'package:doublefeel_flutter/app/actions/dialog_action.dart';
2 import 'package:doublefeel_flutter/app/models/dialog_meta_data.dart'; 2 import 'package:doublefeel_flutter/app/models/dialog_meta_data.dart';
3 import 'package:doublefeel_flutter/app/routes/app_pages.dart'; 3 import 'package:doublefeel_flutter/app/routes/app_pages.dart';
4 import 'package:doublefeel_flutter/app/utils/dialog_utils.dart'; 4 import 'package:doublefeel_flutter/app/utils/dialog_utils.dart';
  5 +import 'package:doublefeel_flutter/core/logging/app_diagnostic_log_store.dart';
5 import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; 6 import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
6 import 'package:get/get.dart'; 7 import 'package:get/get.dart';
7 8
@@ -64,7 +65,13 @@ class AppErrorHandler { @@ -64,7 +65,13 @@ class AppErrorHandler {
64 iconAsset: 'assets/images/common/ic_warning.png', 65 iconAsset: 'assets/images/common/ic_warning.png',
65 )); 66 ));
66 if (confirmResult.action != DialogAction.confirm) return; 67 if (confirmResult.action != DialogAction.confirm) return;
67 - 68 + AppDiagnosticLogStore.instance.log(
  69 + category: 'AccessToken',
  70 + action: 'invalid',
  71 + details: {
  72 + 'error': '401',
  73 + },
  74 + );
68 Get.offAllNamed(AppRoutes.login); 75 Get.offAllNamed(AppRoutes.login);
69 } 76 }
70 77
  1 +import 'dart:convert';
  2 +import 'dart:io';
  3 +import 'dart:typed_data';
  4 +
  5 +import 'package:path_provider/path_provider.dart';
  6 +
  7 +import 'app_logger.dart';
  8 +
  9 +/// Persists structured diagnostic events (region detection, login/user-prefs lifecycle, etc.)
  10 +/// to a local JSON file using path_provider, which can be shared from Developer Options.
  11 +class AppDiagnosticLogStore {
  12 + AppDiagnosticLogStore({Directory? rootDirectory})
  13 + : _rootDirectory = rootDirectory;
  14 +
  15 + static final AppDiagnosticLogStore instance = AppDiagnosticLogStore();
  16 +
  17 + static const _fileName = 'app_diagnostic_events.json';
  18 + static const _maxEventCount = 500;
  19 +
  20 + final Directory? _rootDirectory;
  21 + Future<void> _writeQueue = Future.value();
  22 +
  23 + /// Appends a diagnostic event to the local file.
  24 + Future<void> log({
  25 + required String category,
  26 + required String action,
  27 + Map<String, Object?>? details,
  28 + }) {
  29 + _writeQueue = _writeQueue.then((_) async {
  30 + try {
  31 + final now = DateTime.now();
  32 + final event = <String, Object?>{
  33 + 'category': category,
  34 + 'action': action,
  35 + 'timestamp': now.toIso8601String(),
  36 + 'timestamp_readable': _readableTime(now),
  37 + 'timestamp_unix': now.millisecondsSinceEpoch / 1000,
  38 + if (details != null && details.isNotEmpty)
  39 + 'details': _sanitizeMap(details),
  40 + };
  41 +
  42 + try {
  43 + AppLogger.d('[$category] $action details=$details');
  44 + } catch (_) {}
  45 +
  46 + final file = await _file();
  47 + await file.parent.create(recursive: true);
  48 + final events = await _readEvents(file);
  49 + events.add(event);
  50 +
  51 + final limited = events.length > _maxEventCount
  52 + ? events.sublist(events.length - _maxEventCount)
  53 + : events;
  54 +
  55 + const encoder = JsonEncoder.withIndent(' ');
  56 + await file.writeAsString(encoder.convert(limited), flush: true);
  57 + } catch (error, stackTrace) {
  58 + try {
  59 + AppLogger.e('AppDiagnosticLogStore log failed', error, stackTrace);
  60 + } catch (_) {}
  61 + }
  62 + });
  63 + return _writeQueue;
  64 + }
  65 +
  66 + /// Reads the log file as [Uint8List] bytes for sharing via platform APIs.
  67 + Future<Uint8List> readBytes() async {
  68 + final file = await _file();
  69 + if (!await file.exists()) {
  70 + const emptyContent = '[\n {\n "message": "No diagnostic logs found"\n }\n]';
  71 + return Uint8List.fromList(utf8.encode(emptyContent));
  72 + }
  73 + return await file.readAsBytes();
  74 + }
  75 +
  76 + /// Reads the log file content as a formatted string.
  77 + Future<String> readText() async {
  78 + final file = await _file();
  79 + if (!await file.exists()) {
  80 + return '暂无诊断日志';
  81 + }
  82 + final text = await file.readAsString();
  83 + return text.trim().isEmpty ? '暂无诊断日志' : text;
  84 + }
  85 +
  86 + /// Clears the log file.
  87 + Future<void> clear() async {
  88 + final file = await _file();
  89 + if (await file.exists()) {
  90 + await file.delete();
  91 + }
  92 + }
  93 +
  94 + Future<File> _file() async {
  95 + final root = _rootDirectory ?? await getApplicationDocumentsDirectory();
  96 + return File('${root.path}/$_fileName');
  97 + }
  98 +
  99 + Future<List<Map<String, Object?>>> _readEvents(File file) async {
  100 + if (!await file.exists()) {
  101 + return <Map<String, Object?>>[];
  102 + }
  103 + try {
  104 + final text = await file.readAsString();
  105 + if (text.trim().isEmpty) {
  106 + return <Map<String, Object?>>[];
  107 + }
  108 + final json = jsonDecode(text);
  109 + if (json is List) {
  110 + return json
  111 + .whereType<Map>()
  112 + .map((e) => _sanitizeMap(e.cast<String, Object?>()))
  113 + .toList();
  114 + }
  115 + } catch (_) {}
  116 + return <Map<String, Object?>>[];
  117 + }
  118 +
  119 + static Map<String, Object?> _sanitizeMap(Map<String, Object?> map) {
  120 + return map.map((key, value) => MapEntry(key, _sanitizeValue(value)));
  121 + }
  122 +
  123 + static Object? _sanitizeValue(Object? value) {
  124 + return switch (value) {
  125 + null => null,
  126 + String() => value,
  127 + num() => value,
  128 + bool() => value,
  129 + DateTime() => value.toIso8601String(),
  130 + List() => value.map(_sanitizeValue).toList(),
  131 + Map() => value.map(
  132 + (key, val) => MapEntry(key.toString(), _sanitizeValue(val)),
  133 + ),
  134 + _ => value.toString(),
  135 + };
  136 + }
  137 +
  138 + static String _readableTime(DateTime time) {
  139 + return '${time.year.toString().padLeft(4, '0')}-'
  140 + '${time.month.toString().padLeft(2, '0')}-'
  141 + '${time.day.toString().padLeft(2, '0')} '
  142 + '${time.hour.toString().padLeft(2, '0')}:'
  143 + '${time.minute.toString().padLeft(2, '0')}:'
  144 + '${time.second.toString().padLeft(2, '0')}';
  145 + }
  146 +}
@@ -2,6 +2,8 @@ import 'dart:convert'; @@ -2,6 +2,8 @@ import 'dart:convert';
2 2
3 import 'package:doublefeel_flutter/core/config/app_environment_config.dart'; 3 import 'package:doublefeel_flutter/core/config/app_environment_config.dart';
4 import 'package:doublefeel_flutter/core/platform/pigeon_api_facade.dart'; 4 import 'package:doublefeel_flutter/core/platform/pigeon_api_facade.dart';
  5 +import 'package:doublefeel_flutter/core/logging/app_diagnostic_log_store.dart';
  6 +import 'package:doublefeel_flutter/core/logging/app_logger.dart';
5 import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_data_core_service.dart'; 7 import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_data_core_service.dart';
6 import 'package:get/get.dart'; 8 import 'package:get/get.dart';
7 import 'package:shared_preferences/shared_preferences.dart'; 9 import 'package:shared_preferences/shared_preferences.dart';
@@ -42,7 +44,17 @@ class UserPreferencesStorage { @@ -42,7 +44,17 @@ class UserPreferencesStorage {
42 try { 44 try {
43 final map = jsonDecode(raw) as Map<String, dynamic>; 45 final map = jsonDecode(raw) as Map<String, dynamic>;
44 preferences.value = UserPreferences.fromJson(map); 46 preferences.value = UserPreferences.fromJson(map);
45 - } catch (_) { 47 + } catch (error, stackTrace) {
  48 + AppLogger.e('UserPreferences JSON parse failed', error, stackTrace);
  49 + AppDiagnosticLogStore.instance.log(
  50 + category: 'UserPreferences',
  51 + action: 'load_deserialization_failed',
  52 + details: {
  53 + 'error': error.toString(),
  54 + 'stackTrace': stackTrace.toString(),
  55 + 'rawPreview': raw.length > 200 ? '${raw.substring(0, 200)}...' : raw,
  56 + },
  57 + );
46 preferences.value = UserPreferences.empty; 58 preferences.value = UserPreferences.empty;
47 } 59 }
48 } 60 }
@@ -41,7 +41,7 @@ Future<void> healthBackgroundMain() async { @@ -41,7 +41,7 @@ Future<void> healthBackgroundMain() async {
41 await _healthBackgroundLog(channel, 'entry.start'); 41 await _healthBackgroundLog(channel, 'entry.start');
42 try { 42 try {
43 await _healthBackgroundLog(channel, 'bootstrap.start'); 43 await _healthBackgroundLog(channel, 'bootstrap.start');
44 - await AppBootstrap.init(); 44 + await AppBootstrap.initForBackground();
45 await _healthBackgroundLog(channel, 'bootstrap.success'); 45 await _healthBackgroundLog(channel, 'bootstrap.success');
46 } catch (error, stackTrace) { 46 } catch (error, stackTrace) {
47 await _healthBackgroundLog( 47 await _healthBackgroundLog(