From 37abd5abb6da6f686c9738f2112e30aefe037c6e Mon Sep 17 00:00:00 2001 From: Thomas SharedInbox Date: Sat, 23 May 2026 16:05:05 +0200 Subject: [PATCH] fix: add git hash to crash screen and extend DB path retries (#179) Two issues from #179: - crash_screen.dart now reads GIT_HASH compile-time constant and includes 'Git Commit: ' in both the on-screen UI and the copied report, so crash reports always show the exact build that crashed. - _resolveDatabasePath() retry delays extended from [100, 300, 600] ms (total ~1 s, 4 attempts) to [200, 500, 1000, 2000, 4000] ms (total ~7.7 s, 6 attempts) to handle slow/non-standard Android devices where the path_provider Pigeon channel takes several seconds to become ready. Co-Authored-By: Claude Sonnet 4.6 --- lib/data/db/database.dart | 6 +++-- lib/ui/screens/crash_screen.dart | 12 +++++++++ test/widget/crash_screen_test.dart | 43 ++++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 2 deletions(-) diff --git a/lib/data/db/database.dart b/lib/data/db/database.dart index e9abf31..5dfbd05 100644 --- a/lib/data/db/database.dart +++ b/lib/data/db/database.dart @@ -596,8 +596,10 @@ Future initDatabasePath() async { Future _resolveDatabasePath() async { if (_dbPath != null) return _dbPath!; // initDatabasePath() failed (channel not ready before runApp). Retry now - // that the engine is fully initialised, with brief back-off. - const delays = [100, 300, 600]; + // that the engine is fully initialised, with exponential back-off. + // Longer delays handle slow/non-standard Android devices where the Pigeon + // channel takes several seconds to become available. + const delays = [200, 500, 1000, 2000, 4000]; for (final ms in delays) { try { final dir = await getApplicationSupportDirectory(); diff --git a/lib/ui/screens/crash_screen.dart b/lib/ui/screens/crash_screen.dart index 0badbae..4756a91 100644 --- a/lib/ui/screens/crash_screen.dart +++ b/lib/ui/screens/crash_screen.dart @@ -15,15 +15,19 @@ class CrashScreen extends StatelessWidget { final Object exception; final StackTrace? stackTrace; + static const _gitHash = String.fromEnvironment('GIT_HASH'); + Future _buildReport() async { String version = 'unknown'; try { final info = await PackageInfo.fromPlatform(); version = '${info.version}+${info.buildNumber}'; } catch (_) {} + final gitLine = _gitHash.isNotEmpty ? 'Git Commit: $_gitHash\n' : ''; final platform = '${Platform.operatingSystem} ${Platform.operatingSystemVersion}'; return 'App Version: $version\n' + '$gitLine' 'Platform: $platform\n\n' 'Error:\n```\n$exception\n```\n\n' 'Stack Trace:\n```\n$stackTrace\n```'; @@ -50,6 +54,14 @@ class CrashScreen extends StatelessWidget { style: Theme.of(ctx).textTheme.titleMedium, textAlign: TextAlign.center, ), + if (_gitHash.isNotEmpty) ...[ + const SizedBox(height: 8), + const Text( + 'Git Commit: $_gitHash', + style: TextStyle(fontSize: 12, color: Colors.grey), + textAlign: TextAlign.center, + ), + ], const SizedBox(height: 24), const Text( 'Error Details:', diff --git a/test/widget/crash_screen_test.dart b/test/widget/crash_screen_test.dart index c897fe5..c9a2e56 100644 --- a/test/widget/crash_screen_test.dart +++ b/test/widget/crash_screen_test.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; import 'package:package_info_plus/package_info_plus.dart'; @@ -76,6 +77,48 @@ void main() { expect(mock.launchedUrl, isNot(contains('Stack%20Trace'))); }); + testWidgets( + 'Copy to Clipboard includes App Version and excludes Git Commit when GIT_HASH is empty', + ( + tester, + ) async { + tester.view.physicalSize = const Size(800, 1200); + tester.view.devicePixelRatio = 1.0; + addTearDown(() => tester.view.resetPhysicalSize()); + + UrlLauncherPlatform.instance = MockUrlLauncher(); + + String? capturedClipboard; + tester.binding.defaultBinaryMessenger.setMockMethodCallHandler( + SystemChannels.platform, + (MethodCall call) async { + if (call.method == 'Clipboard.setData') { + capturedClipboard = (call.arguments as Map)['text'] as String; + } + return null; + }, + ); + + const exception = 'TestException: clipboard test'; + final stackTrace = StackTrace.current; + + await tester.pumpWidget( + MaterialApp( + home: CrashScreen(exception: exception, stackTrace: stackTrace), + ), + ); + + await tester.tap(find.text('Copy to Clipboard')); + await tester.pumpAndSettle(); + + expect(capturedClipboard, isNotNull); + expect(capturedClipboard, contains('App Version:')); + expect(capturedClipboard, contains('Platform:')); + expect(capturedClipboard, contains('TestException: clipboard test')); + // GIT_HASH is empty in tests (no --dart-define), so the line is omitted. + expect(capturedClipboard, isNot(contains('Git Commit:'))); + }); + testWidgets( 'CrashScreen used as root widget — buttons work without ScaffoldMessenger crash', (tester) async {