improved error reporting

This commit is contained in:
Thibault Deckers 2024-11-23 23:24:41 +01:00
parent ec7e4ac2f2
commit ffbf0bd8f2
15 changed files with 25 additions and 26 deletions

View file

@ -32,7 +32,7 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry {
final appliedModifier = await _applyDateModifierToEntry(userModifier); final appliedModifier = await _applyDateModifierToEntry(userModifier);
if (appliedModifier == null) { if (appliedModifier == null) {
if (isValid && userModifier.action != DateEditAction.copyField) { if (isValid && userModifier.action != DateEditAction.copyField) {
await reportService.recordError('failed to get date for modifier=$userModifier, entry=$this', null); await reportService.recordError('failed to get date for modifier=$userModifier, entry=$this');
} }
return {}; return {};
} }
@ -65,7 +65,7 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry {
final shiftedDate = date.add(Duration(seconds: appliedModifier.shiftSeconds!)); final shiftedDate = date.add(Duration(seconds: appliedModifier.shiftSeconds!));
editCreateDateXmp(descriptions, shiftedDate); editCreateDateXmp(descriptions, shiftedDate);
} else { } else {
reportService.recordError('failed to parse XMP date=$xmpDate', null); reportService.recordError('failed to parse XMP date=$xmpDate');
} }
} }
case DateEditAction.remove: case DateEditAction.remove:

View file

@ -1,12 +1,12 @@
import 'dart:async'; import 'dart:async';
import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/metadata/catalog.dart';
import 'package:aves/model/media/video/channel_layouts.dart'; import 'package:aves/model/media/video/channel_layouts.dart';
import 'package:aves/model/media/video/codecs.dart'; import 'package:aves/model/media/video/codecs.dart';
import 'package:aves/model/media/video/profiles/aac.dart'; import 'package:aves/model/media/video/profiles/aac.dart';
import 'package:aves/model/media/video/profiles/h264.dart'; import 'package:aves/model/media/video/profiles/h264.dart';
import 'package:aves/model/media/video/profiles/hevc.dart'; import 'package:aves/model/media/video/profiles/hevc.dart';
import 'package:aves/model/metadata/catalog.dart';
import 'package:aves/ref/languages.dart'; import 'package:aves/ref/languages.dart';
import 'package:aves/ref/locales.dart'; import 'package:aves/ref/locales.dart';
import 'package:aves/ref/mime_types.dart'; import 'package:aves/ref/mime_types.dart';
@ -99,7 +99,7 @@ class VideoMetadataFormatter {
if (isDefined(dateString)) { if (isDefined(dateString)) {
dateMillis = parseVideoDate(dateString); dateMillis = parseVideoDate(dateString);
if (dateMillis == null && !isAmbiguousDate(dateString)) { if (dateMillis == null && !isAmbiguousDate(dateString)) {
await reportService.recordError('getCatalogMetadata failed to parse date=$dateString for mimeType=${entry.mimeType} entry=$entry', null); await reportService.recordError('getCatalogMetadata failed to parse date=$dateString for mimeType=${entry.mimeType} entry=$entry');
} }
} }

View file

@ -62,7 +62,7 @@ class MediaStoreSource extends CollectionSource {
final currentTimeZoneOffset = DateTime.now().timeZoneOffset.inMilliseconds; final currentTimeZoneOffset = DateTime.now().timeZoneOffset.inMilliseconds;
final catalogTimeZoneOffset = settings.catalogTimeZoneOffsetMillis; final catalogTimeZoneOffset = settings.catalogTimeZoneOffsetMillis;
if (currentTimeZoneOffset != catalogTimeZoneOffset) { if (currentTimeZoneOffset != catalogTimeZoneOffset) {
unawaited(reportService.recordError('Time zone offset change: $currentTimeZoneOffset -> $catalogTimeZoneOffset. Clear catalog metadata to get correct date/times.', null)); unawaited(reportService.recordError('Time zone offset change: $currentTimeZoneOffset -> $catalogTimeZoneOffset. Clear catalog metadata to get correct date/times.'));
await localMediaDb.clearDates(); await localMediaDb.clearDates();
await localMediaDb.clearCatalogMetadata(); await localMediaDb.clearCatalogMetadata();
settings.catalogTimeZoneOffsetMillis = currentTimeZoneOffset; settings.catalogTimeZoneOffsetMillis = currentTimeZoneOffset;
@ -212,7 +212,7 @@ class MediaStoreSource extends CollectionSource {
// TODO TLAD find duplication cause // TODO TLAD find duplication cause
final duplicates = await localMediaDb.searchLiveDuplicates(EntryOrigins.mediaStoreContent, newEntries); final duplicates = await localMediaDb.searchLiveDuplicates(EntryOrigins.mediaStoreContent, newEntries);
if (duplicates.isNotEmpty) { if (duplicates.isNotEmpty) {
unawaited(reportService.recordError(Exception('Loading entries yielded duplicates=${duplicates.join(', ')}'), StackTrace.current)); unawaited(reportService.recordError(Exception('Loading entries yielded duplicates=${duplicates.join(', ')}')));
// post-error cleanup // post-error cleanup
await localMediaDb.removeIds(duplicates.map((v) => v.id).toSet()); await localMediaDb.removeIds(duplicates.map((v) => v.id).toSet());
for (final duplicate in duplicates) { for (final duplicate in duplicates) {
@ -325,7 +325,7 @@ class MediaStoreSource extends CollectionSource {
// TODO TLAD find duplication cause // TODO TLAD find duplication cause
final duplicates = await localMediaDb.searchLiveDuplicates(EntryOrigins.mediaStoreContent, newEntries); final duplicates = await localMediaDb.searchLiveDuplicates(EntryOrigins.mediaStoreContent, newEntries);
if (duplicates.isNotEmpty) { if (duplicates.isNotEmpty) {
unawaited(reportService.recordError(Exception('Refreshing entries yielded duplicates=${duplicates.join(', ')}'), StackTrace.current)); unawaited(reportService.recordError(Exception('Refreshing entries yielded duplicates=${duplicates.join(', ')}')));
// post-error cleanup // post-error cleanup
await localMediaDb.removeIds(duplicates.map((v) => v.id).toSet()); await localMediaDb.removeIds(duplicates.map((v) => v.id).toSet());
for (final duplicate in duplicates) { for (final duplicate in duplicates) {

View file

@ -76,7 +76,7 @@ mixin TrashMixin on SourceBase {
sourceEntry.trashDetails = _buildTrashDetails(id); sourceEntry.trashDetails = _buildTrashDetails(id);
newEntries.add(sourceEntry); newEntries.add(sourceEntry);
} else { } else {
await reportService.recordError('Failed to recover untracked bin item at uri=$uri', null); await reportService.recordError('Failed to recover untracked bin item at uri=$uri');
} }
} }
}); });

View file

@ -175,7 +175,7 @@ class Vaults extends ChangeNotifier {
sourceEntry.origin = EntryOrigins.vault; sourceEntry.origin = EntryOrigins.vault;
newEntries.add(sourceEntry); newEntries.add(sourceEntry);
} else { } else {
await reportService.recordError('Failed to recover untracked vault item at uri=$uri', null); await reportService.recordError('Failed to recover untracked vault item at uri=$uri');
} }
}); });
} }

View file

@ -6,10 +6,8 @@ import 'package:aves/model/entry/extensions/props.dart';
import 'package:aves/model/metadata/date_modifier.dart'; import 'package:aves/model/metadata/date_modifier.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:aves_model/aves_model.dart'; import 'package:aves_model/aves_model.dart';
import 'package:aves_report/aves_report.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:stack_trace/stack_trace.dart';
abstract class MetadataEditService { abstract class MetadataEditService {
Future<Map<String, dynamic>> rotate(AvesEntry entry, {required bool clockwise}); Future<Map<String, dynamic>> rotate(AvesEntry entry, {required bool clockwise});
@ -135,24 +133,22 @@ class PlatformMetadataEditService implements MetadataEditService {
} }
} }
StackTrace? _currentStack() => ReportService.buildReportStack(Trace.current(), level: 1);
// distinct exceptions to convince Crashlytics to split reports into distinct issues // distinct exceptions to convince Crashlytics to split reports into distinct issues
// The distinct debug statement is there to make the body unique, so that the methods are not merged at compile time. // The distinct debug statement is there to make the body unique, so that the methods are not merged at compile time.
Future<void> mp4LargeMoov(CustomPlatformException e) { Future<void> mp4LargeMoov(CustomPlatformException e) {
debugPrint('mp4LargeMoov $e'); debugPrint('mp4LargeMoov $e');
return reportService.recordError(e, _currentStack()); return reportService.recordError(e);
} }
Future<void> mp4LargeOther(CustomPlatformException e) { Future<void> mp4LargeOther(CustomPlatformException e) {
debugPrint('mp4LargeOther $e'); debugPrint('mp4LargeOther $e');
return reportService.recordError(e, _currentStack()); return reportService.recordError(e);
} }
Future<void> fileNotFound(CustomPlatformException e) { Future<void> fileNotFound(CustomPlatformException e) {
debugPrint('fileNotFound $e'); debugPrint('fileNotFound $e');
return reportService.recordError(e, _currentStack()); return reportService.recordError(e);
} }
} }

View file

@ -693,7 +693,7 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
_mediaStoreSource.updateDerivedFilters(); _mediaStoreSource.updateDerivedFilters();
} }
void _onError(String? error) => reportService.recordError(error, null); void _onError(String? error) => reportService.recordError(error);
void _onAppModeChanged() { void _onAppModeChanged() {
final appMode = _appModeNotifier.value; final appMode = _appModeNotifier.value;

View file

@ -61,8 +61,8 @@ mixin SingleEntryEditorMixin on FeedbackMixin, PermissionAwareMixin {
} else { } else {
showFeedback(context, FeedbackType.warn, l10n.genericFailureFeedback); showFeedback(context, FeedbackType.warn, l10n.genericFailureFeedback);
} }
} catch (error, stack) { } catch (e, stack) {
await reportService.recordError(error, stack); await reportService.recordError(e, stack);
} }
source?.resumeMonitoring(); source?.resumeMonitoring();
} }

View file

@ -96,8 +96,8 @@ mixin CastMixin {
); );
debugPrint('cast: play entry=$entry'); debugPrint('cast: play entry=$entry');
unawaited(renderer.play()); unawaited(renderer.play());
} catch (error, stack) { } catch (e, stack) {
await reportService.recordError(error, stack); await reportService.recordError(e, stack);
} }
} }

View file

@ -15,7 +15,7 @@ abstract class ReportService {
Future<void> setCustomKeys(Map<String, Object> map); Future<void> setCustomKeys(Map<String, Object> map);
Future<void> recordError(dynamic exception, StackTrace? stack); Future<void> recordError(dynamic exception, [StackTrace? stack]);
Future<void> recordFlutterError(FlutterErrorDetails flutterErrorDetails); Future<void> recordFlutterError(FlutterErrorDetails flutterErrorDetails);

View file

@ -9,7 +9,7 @@ class PlatformReportService extends ReportService {
Future<void> log(String message) async => debugPrint('Report log with message=$message'); Future<void> log(String message) async => debugPrint('Report log with message=$message');
@override @override
Future<void> recordError(dynamic exception, StackTrace? stack) async => debugPrint('Report error with exception=$exception, stack=$stack'); Future<void> recordError(dynamic exception, [StackTrace? stack]) async => debugPrint('Report error with exception=$exception, stack=$stack');
@override @override
Future<void> recordFlutterError(FlutterErrorDetails flutterErrorDetails) async => debugPrint('Report Flutter error with details=$flutterErrorDetails'); Future<void> recordFlutterError(FlutterErrorDetails flutterErrorDetails) async => debugPrint('Report Flutter error with details=$flutterErrorDetails');

View file

@ -5,6 +5,7 @@ import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:stack_trace/stack_trace.dart';
class PlatformReportService extends ReportService { class PlatformReportService extends ReportService {
FirebaseCrashlytics? get _instance { FirebaseCrashlytics? get _instance {
@ -65,11 +66,12 @@ class PlatformReportService extends ReportService {
} }
@override @override
Future<void> recordError(dynamic exception, StackTrace? stack) async { Future<void> recordError(dynamic exception, [StackTrace? stack]) async {
if (exception is PlatformException && stack != null) { if (exception is PlatformException && stack != null) {
stack = ReportService.buildReportStack(stack, level: 2); stack = ReportService.buildReportStack(stack, level: 2);
} }
if (exception is! UnreportedStateError) { if (exception is! UnreportedStateError) {
stack ??= ReportService.buildReportStack(Trace.current(), level: 1);
return _instance?.recordError(exception, stack); return _instance?.recordError(exception, stack);
} }
} }

View file

@ -213,7 +213,7 @@ packages:
source: hosted source: hosted
version: "1.10.0" version: "1.10.0"
stack_trace: stack_trace:
dependency: transitive dependency: "direct main"
description: description:
name: stack_trace name: stack_trace
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"

View file

@ -14,6 +14,7 @@ dependencies:
# so that the transitive `path` gets upgraded to v1.8.3 # so that the transitive `path` gets upgraded to v1.8.3
firebase_core: ">=2.10.0" firebase_core: ">=2.10.0"
firebase_crashlytics: firebase_crashlytics:
stack_trace:
dev_dependencies: dev_dependencies:
flutter_lints: flutter_lints:

View file

@ -21,7 +21,7 @@ class FakeReportService extends ReportService {
Future<void> setCustomKeys(Map<String, Object> map) => SynchronousFuture(null); Future<void> setCustomKeys(Map<String, Object> map) => SynchronousFuture(null);
@override @override
Future<void> recordError(dynamic exception, StackTrace? stack) => SynchronousFuture(null); Future<void> recordError(dynamic exception, [StackTrace? stack]) => SynchronousFuture(null);
@override @override
Future<void> recordFlutterError(FlutterErrorDetails flutterErrorDetails) => SynchronousFuture(null); Future<void> recordFlutterError(FlutterErrorDetails flutterErrorDetails) => SynchronousFuture(null);