feat(crash): add app version and device info to crash reports

Issue reports now include:
- App version (from package_info_plus)
- OS name and version (non-personal, from dart:io Platform)
- Error and stack trace wrapped in triple-backtick code blocks
  so Codeberg renders them as preformatted text

Closes #59

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Thomas SharedInbox
2026-05-14 20:52:40 +02:00
co-authored by Claude Sonnet 4.6
parent 6f030213a7
commit 26a9a5e6f3
3 changed files with 37 additions and 11 deletions
+21 -7
View File
@@ -1,5 +1,8 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:url_launcher/url_launcher.dart';
class CrashScreen extends StatelessWidget {
@@ -12,6 +15,20 @@ class CrashScreen extends StatelessWidget {
final Object exception;
final StackTrace? stackTrace;
Future<String> _buildReport() async {
String version = 'unknown';
try {
final info = await PackageInfo.fromPlatform();
version = '${info.version}+${info.buildNumber}';
} catch (_) {}
final platform =
'${Platform.operatingSystem} ${Platform.operatingSystemVersion}';
return 'App Version: $version\n'
'Platform: $platform\n\n'
'Error:\n```\n$exception\n```\n\n'
'Stack Trace:\n```\n$stackTrace\n```';
}
@override
Widget build(BuildContext context) {
return MaterialApp(
@@ -74,7 +91,7 @@ class CrashScreen extends StatelessWidget {
const SizedBox(height: 24),
FilledButton.icon(
onPressed: () async {
final data = 'Error: $exception\n\nStack Trace:\n$stackTrace';
final data = await _buildReport();
await Clipboard.setData(ClipboardData(text: data));
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
@@ -91,12 +108,11 @@ class CrashScreen extends StatelessWidget {
const SizedBox(height: 16),
OutlinedButton.icon(
onPressed: () async {
final report = await _buildReport();
final title = Uri.encodeComponent(
'Crash: ${exception.toString().split('\n').first}',
);
final body = Uri.encodeComponent(
'Error: $exception\n\nStack Trace:\n$stackTrace',
);
final body = Uri.encodeComponent(report);
final url = Uri.parse(
'https://codeberg.org/guettli/sharedinbox/issues/new?title=$title&body=$body',
);
@@ -115,9 +131,7 @@ class CrashScreen extends StatelessWidget {
}
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
duration: const Duration(seconds: 5),
content: Text('Error: $e'),
+3
View File
@@ -49,6 +49,9 @@ dependencies:
flutter_local_notifications: ^18.0.1
workmanager: ^0.9.0
# App version metadata for crash reports
package_info_plus: ^8.0.0
dev_dependencies:
flutter_test:
sdk: flutter
+13 -4
View File
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
import 'package:sharedinbox/ui/screens/crash_screen.dart';
import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart';
@@ -23,6 +24,16 @@ class MockUrlLauncher extends Mock
}
void main() {
setUpAll(() {
PackageInfo.setMockInitialValues(
appName: 'SharedInbox',
packageName: 'org.sharedinbox',
version: '1.0.0',
buildNumber: '42',
buildSignature: '',
);
});
testWidgets('CrashScreen shows error details and has a report button', (
tester,
) async {
@@ -56,9 +67,7 @@ void main() {
mock.launchedUrl,
contains('title=Crash%3A%20TestException%3A%20something%20broke'),
);
expect(
mock.launchedUrl,
contains('body=Error%3A%20TestException%3A%20something%20broke'),
);
expect(mock.launchedUrl, contains('App%20Version%3A%201.0.0%2B42'));
expect(mock.launchedUrl, contains('TestException%3A%20something%20broke'));
});
}