Improve error collection and reporting. Fixes #37

This commit is contained in:
franzap
2024-07-02 13:08:40 -03:00
parent d380aef4df
commit 63b0e35ced
6 changed files with 54 additions and 59 deletions
+20 -19
View File
@@ -1,7 +1,7 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter_data/flutter_data.dart';
import 'package:flutter_phoenix/flutter_phoenix.dart';
@@ -16,8 +16,8 @@ const kDbVersion = 1;
/// Application entry point.
/// - Initializes Riverpod (and Flutter Data local storage)
/// - Handles errors globally
/// - Calls router
/// - Handles application errors
/// - Triggers routing
void main() {
runZonedGuarded(() {
runApp(
@@ -62,23 +62,24 @@ Directory? _dir;
void errorHandler(Object exception, StackTrace? stack) {
debounce((exception: exception, stack: stack), (records) async {
// TODO Implement reporting
final map = records.groupSetsBy((err) => '${err.exception}\n${err.stack}');
print(map.length);
// final text =
// '${DateTime.now().toIso8601String()}\n${err.exception}\n${err.stack}';
// // print('${err.exception}\n${err.stack}');
// final hash = sha256.convert(utf8.encode('${err.exception}\n${err.stack}'));
// print(hash);
// print(text);
// print('-----');
if (records.isEmpty) return;
_dir ??= await getApplicationDocumentsDirectory();
// // final file = File('${directory.path}/data.txt');
// // await file.writeAsString(newContents);
final file = File('${_dir!.path}/errors.json');
// collect system information
final errorMap = await file.exists()
? Map<String, String>.from(jsonDecode(await file.readAsString()))
: <String, String>{};
for (final record in records) {
final full =
'${record.exception}${record.stack?.toString() ?? ''}${DateTime.now().toIso8601String()}';
final key = full.split('\n').take(2).join();
// Only keep longest stack of similar errors, prevents duplicates
if (full.length > (errorMap[key]?.length ?? 0)) {
errorMap[key] = full;
}
}
print(errorMap.length);
await file.writeAsString(jsonEncode(errorMap));
});
}
+2 -1
View File
@@ -17,7 +17,8 @@ class MobileScaffold extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
// TODO Inefficient, should be able to query for this (we need to store app info in database)
// TODO Inefficient, should be able to query for this
// (need to store app status in database, local only)
final appsToUpdate =
ref.apps.watchAll().model.where((a) => a.canUpdate).length;
return Scaffold(
+1 -1
View File
@@ -139,7 +139,7 @@ final dataLibrariesInitializer = FutureProvider<void>((ref) async {
ref
.read(relayMessageNotifierProvider.notifier)
.initialize(['wss://relay.zap.store', 'wss://relay.nostr.band']);
// TODO Is this best place for this listener?
_lifecycleListener = AppLifecycleListener(
onStateChange: (state) async {
if (state == AppLifecycleState.resumed) {
+1 -1
View File
@@ -60,7 +60,7 @@ class SettingsScreen extends HookConsumerWidget {
if (controller.text.trim().isNotEmpty) {
final text =
'${controller.text.trim()} [from ${user.npub} on ${DateFormat('MMMM d, y').format(DateTime.now())}]';
final event = BaseEvent(content: text).sign(kI);
final event = BaseEvent(kind: 1011, content: text).sign(kI);
await ref.apps.nostrAdapter.notifier
.publish(event, relayUrls: ['wss://relay.zap.store']);
controller.clear();
+3 -3
View File
@@ -4,13 +4,13 @@ const _debounceTime = 1200;
Timer? timer;
final acc = [];
void debounce<T>(T input, Function(List<T>) cb) {
void debounce<T>(T input, Future Function(List<T>) cb) {
if (timer?.isActive ?? false) {
timer!.cancel();
}
acc.add(input);
timer = Timer(Duration(milliseconds: _debounceTime), () {
cb(acc.cast<T>());
timer = Timer(Duration(milliseconds: _debounceTime), () async {
await cb(acc.cast<T>());
acc.clear();
});
}
+27 -34
View File
@@ -1,9 +1,15 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_data/flutter_data.dart';
import 'package:flutter_phoenix/flutter_phoenix.dart';
import 'package:gap/gap.dart';
import 'package:http/http.dart' as http;
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:purplebase/purplebase.dart';
import 'package:zapstore/screens/settings_screen.dart';
import 'package:zapstore/utils/system_info.dart';
import 'package:zapstore/utils/theme.dart';
class ErrorContainer extends HookConsumerWidget {
@@ -51,43 +57,12 @@ class ErrorContainer extends HookConsumerWidget {
textAlign: TextAlign.center,
),
Gap(24),
Text(
'First try restart. If it does not fix it, try clear.\nLastly, you can send us an error report.',
style: TextStyle(
fontSize: 15,
color: Colors.grey,
),
textAlign: TextAlign.center,
),
Gap(10),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () {
Phoenix.rebirth(context);
context.go('/');
},
style: ElevatedButton.styleFrom(
backgroundColor: Color.fromARGB(255, 255, 99, 99),
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: const Text(
'Restart',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
),
Gap(10),
ElevatedButton(
onPressed: () {
ref.read(localStorageProvider).destroy().then((_) {
print('destory done');
Phoenix.rebirth(context);
context.go('/');
});
@@ -100,7 +75,7 @@ class ErrorContainer extends HookConsumerWidget {
),
),
child: const Text(
'Clear',
'Clear and reload app',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
@@ -109,7 +84,7 @@ class ErrorContainer extends HookConsumerWidget {
),
Gap(10),
ElevatedButton(
onPressed: () {},
onPressed: () => _sendErrorReport(ref, exception, stack),
style: ElevatedButton.styleFrom(
backgroundColor: Color.fromARGB(255, 255, 22, 22),
foregroundColor: Colors.white,
@@ -118,7 +93,7 @@ class ErrorContainer extends HookConsumerWidget {
),
),
child: const Text(
'Report',
'Report error',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
@@ -150,3 +125,21 @@ Widget materialErrorBuilder(BuildContext context, Widget? widget) {
if (widget != null) return widget;
throw StateError('widget is null');
}
Future<void> _sendErrorReport(
WidgetRef ref, Object exception, StackTrace? stack) async {
final systemInfo = await ref.read(systemInfoProvider.future);
var map = {
'e': exception.toString(),
if (stack != null) 'stack': stack.toString(),
'info': systemInfo.androidInfo.toString()
};
final client = http.Client();
final event = BaseEvent(kind: 1011, content: jsonEncode(map)).sign(kI);
await client.post(Uri.parse('https://relay.zap.store/'),
body: jsonEncode(event.toMap()),
headers: {'Content-Type': 'application/json'});
client.close();
}