improve error logging

This commit is contained in:
otsmr 2026-06-20 13:09:53 +02:00
parent 4c42d55afd
commit e8829ed2c9
20 changed files with 142 additions and 49 deletions

View file

@ -36,7 +36,7 @@ abstract class VideoCompressionChannel {
}); });
return outputPath; return outputPath;
} on PlatformException catch (e) { } on PlatformException catch (e) {
Log.error('Failed to compress video: $e'); Log.warn('Failed to compress video: $e');
return null; return null;
} finally { } finally {
_currentProgressCallback = null; _currentProgressCallback = null;

View file

@ -30,11 +30,15 @@ class ReactionsDao extends DatabaseAccessor<TwonlyDB> with _$ReactionsDaoMixin {
.getMessageById(messageId) .getMessageById(messageId)
.getSingleOrNull(); .getSingleOrNull();
if (msg == null) { if (msg == null) {
Log.error('updateReaction: Message $messageId not found!'); Log.warn('updateReaction: Message $messageId not found!');
return; return;
} }
if (msg.groupId != groupId) { if (msg.groupId != groupId) {
Log.error('updateReaction: Message groupId ${msg.groupId} != $groupId'); Log.warn('updateReaction: Message groupId ${msg.groupId} != $groupId');
Log.error(
'updateReaction: Message groupId mismatch',
onlyIfSentryEnabled: true,
);
return; return;
} }

View file

@ -201,8 +201,8 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
); );
final updatedReceipt = await getReceiptById(newReceiptId); final updatedReceipt = await getReceiptById(newReceiptId);
if (updatedReceipt == null) { if (updatedReceipt == null) {
Log.error( Log.warn(
'Tried to change the receipt ID, but could not get the updated receipt...', '[$oldReceiptId] Tried to change the receipt ID to $newReceiptId, but could not get the updated receipt...',
); );
} }
return updatedReceipt; return updatedReceipt;
@ -253,6 +253,9 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
Future<void> gotReceipt(String receiptId) async { Future<void> gotReceipt(String receiptId) async {
await into( await into(
receivedReceipts, receivedReceipts,
).insert(ReceivedReceiptsCompanion(receiptId: Value(receiptId))); ).insert(
ReceivedReceiptsCompanion(receiptId: Value(receiptId)),
mode: InsertMode.insertOrIgnore,
);
} }
} }

View file

@ -684,11 +684,11 @@ class ApiService {
final req = createClientToServerFromHandshake(handshake); final req = createClientToServerFromHandshake(handshake);
final result = await sendRequestSync(req, authenticated: false); final result = await sendRequestSync(req, authenticated: false);
if (result.isError) { if (result.isError) {
Log.error('could not request proof of work params', result); Log.error('could not request proof of work params', error: result);
if (result.error == ErrorCode.RegistrationDisabled) { if (result.error == ErrorCode.RegistrationDisabled) {
return (null, true); return (null, true);
} }
Log.error('could not request proof of work params', result); Log.error('could not request proof of work params', error: result);
return (null, false); return (null, false);
} }
return (result.value.proofOfWork as Response_ProofOfWork, false); return (result.value.proofOfWork as Response_ProofOfWork, false);

View file

@ -17,7 +17,7 @@ Future<void> handleErrorMessage(
String receiptId, { String receiptId, {
String? groupId, String? groupId,
}) async { }) async {
Log.error('[$receiptId] Got error from $fromUserId: $error'); Log.warn('[$receiptId] Got error from $fromUserId: $error');
switch (error.type) { switch (error.type) {
case EncryptedContent_ErrorMessages_Type case EncryptedContent_ErrorMessages_Type
@ -38,14 +38,14 @@ Future<void> handleErrorMessage(
break; // The other user initiated a new signal session, so ignore the error in this case, as the new session works... break; // The other user initiated a new signal session, so ignore the error in this case, as the new session works...
case EncryptedContent_ErrorMessages_Type.GROUP_NOT_FOUND_OR_NOT_A_MEMBER: case EncryptedContent_ErrorMessages_Type.GROUP_NOT_FOUND_OR_NOT_A_MEMBER:
if (groupId == null) { if (groupId == null) {
Log.error( Log.warn(
'[$receiptId] GROUP_NOT_FOUND_OR_NOT_A_MEMBER error received, but groupId is null.', '[$receiptId] GROUP_NOT_FOUND_OR_NOT_A_MEMBER error received, but groupId is null.',
); );
return; return;
} }
final group = await twonlyDB.groupsDao.getGroup(groupId); final group = await twonlyDB.groupsDao.getGroup(groupId);
if (group == null) { if (group == null) {
Log.error( Log.warn(
'[$receiptId] GROUP_NOT_FOUND_OR_NOT_A_MEMBER error received, but group $groupId is not found in database.', '[$receiptId] GROUP_NOT_FOUND_OR_NOT_A_MEMBER error received, but group $groupId is not found in database.',
); );
return; return;

View file

@ -143,8 +143,12 @@ Future<void> handleDownloadStatusUpdate(TaskStatusUpdate update) async {
failed = false; failed = false;
} else { } else {
failed = true; failed = true;
Log.warn(
'[$mediaId] Got invalid response status code: ${update.responseStatusCode}',
);
Log.error( Log.error(
'Got invalid response status code: ${update.responseStatusCode}', 'Got invalid response status code: ${update.responseStatusCode}',
onlyIfSentryEnabled: true,
); );
} }
} else { } else {
@ -227,7 +231,7 @@ Future<void> startDownloadMedia(MediaFile media, bool force) async {
try { try {
await downloadFileFast(media, apiUrl, mediaService.encryptedPath); await downloadFileFast(media, apiUrl, mediaService.encryptedPath);
} catch (e) { } catch (e) {
Log.error('Fast download failed: $e'); Log.warn('Fast download failed: $e');
await FileDownloader().enqueue(task); await FileDownloader().enqueue(task);
} }
} catch (e) { } catch (e) {
@ -251,9 +255,13 @@ Future<void> downloadFileFast(
return; return;
} else { } else {
if (response.statusCode == 404 || response.statusCode == 403) { if (response.statusCode == 404 || response.statusCode == 403) {
Log.error( Log.warn(
'Got ${response.statusCode} from server for media ID ${media.mediaId}. Requesting upload again', 'Got ${response.statusCode} from server for media ID ${media.mediaId}. Requesting upload again',
); );
Log.error(
'Got ${response.statusCode} from server for media ID.',
onlyIfSentryEnabled: true,
);
// Message was deleted from the server. Requesting it again from the sender to upload it again... // Message was deleted from the server. Requesting it again from the sender to upload it again...
await requestMediaReupload(media.mediaId); await requestMediaReupload(media.mediaId);
return; return;
@ -293,7 +301,7 @@ Future<void> handleEncryptedFile(String mediaId) async {
action: () async { action: () async {
final mediaService = await MediaFileService.fromMediaId(mediaId); final mediaService = await MediaFileService.fromMediaId(mediaId);
if (mediaService == null) { if (mediaService == null) {
Log.error('Media file not found in database.'); Log.warn('[$mediaId] Media file not found in database.');
return; return;
} }

View file

@ -80,9 +80,13 @@ Future<void> handleUploadStatusUpdate(TaskStatusUpdate update) async {
await markUploadAsSuccessful(media); await markUploadAsSuccessful(media);
return; return;
} }
Log.error( Log.warn(
'Got HTTP error ${update.responseStatusCode} for $mediaId', 'Got HTTP error ${update.responseStatusCode} for $mediaId',
); );
Log.error(
'Got HTTP error ${update.responseStatusCode} for media.',
onlyIfSentryEnabled: true,
);
} }
if (update.status == TaskStatus.notFound) { if (update.status == TaskStatus.notFound) {
@ -118,9 +122,13 @@ Future<void> handleUploadStatusUpdate(TaskStatusUpdate update) async {
if (update.status == TaskStatus.failed || if (update.status == TaskStatus.failed ||
update.status == TaskStatus.canceled) { update.status == TaskStatus.canceled) {
Log.error( Log.warn(
'Background upload failed for $mediaId with status ${update.status} and ${update.responseStatusCode}. ', 'Background upload failed for $mediaId with status ${update.status} and ${update.responseStatusCode}. ',
); );
Log.error(
'Background upload failed with status ${update.status} and ${update.responseStatusCode}.',
onlyIfSentryEnabled: true,
);
final mediaService = MediaFileService(media); final mediaService = MediaFileService(media);
// in case the media file is already uploaded to not reqtry // in case the media file is already uploaded to not reqtry

View file

@ -252,7 +252,11 @@ Future<void> _handleClient2ClientMessage(
await twonlyDB.receiptsDao.gotReceipt(receiptId); await twonlyDB.receiptsDao.gotReceipt(receiptId);
Log.info('[$receiptId] Finished processing'); Log.info('[$receiptId] Finished processing');
} catch (e) { } catch (e) {
Log.error('[$receiptId] Error marking message as received: $e'); Log.warn('[$receiptId] Error marking message as received: $e');
Log.error(
'Error marking message as received: $e',
onlyIfSentryEnabled: true,
);
} }
} }
@ -426,7 +430,14 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
} }
if (!content.hasGroupId()) { if (!content.hasGroupId()) {
Log.error('[$receiptId] Messages should have a groupId $fromUserId.'); final type = _getEncryptedContentType(content);
Log.warn(
'[$receiptId] Messages should have a groupId $fromUserId. Type: $type',
);
Log.error(
'Messages should have a groupId. Type: $type',
onlyIfSentryEnabled: true,
);
return (null, null); return (null, null);
} }
@ -453,7 +464,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
); );
if (contact == null || !contact.accepted || contact.deletedByUser) { if (contact == null || !contact.accepted || contact.deletedByUser) {
await handleNewContactRequest(fromUserId); await handleNewContactRequest(fromUserId);
Log.error( Log.warn(
'[$receiptId] User tries to send message to direct chat while the user does not exist!', '[$receiptId] User tries to send message to direct chat while the user does not exist!',
); );
return ( return (
@ -478,7 +489,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
); );
} else { } else {
if (content.hasGroupJoin()) { if (content.hasGroupJoin()) {
Log.error( Log.warn(
'[$receiptId] Got group join message, but group does not exist yet, retry later. As probably the GroupCreate was not yet received.', '[$receiptId] Got group join message, but group does not exist yet, retry later. As probably the GroupCreate was not yet received.',
); );
// In case the group join was received before the GroupCreate the sender should send it later again. // In case the group join was received before the GroupCreate the sender should send it later again.
@ -489,7 +500,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
); );
} }
Log.error( Log.warn(
'[$receiptId] User $fromUserId tried to access group ${content.groupId}. Sending GROUP_NOT_FOUND_OR_NOT_A_MEMBER error.', '[$receiptId] User $fromUserId tried to access group ${content.groupId}. Sending GROUP_NOT_FOUND_OR_NOT_A_MEMBER error.',
); );
return ( return (
@ -598,3 +609,26 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
return (null, null); return (null, null);
} }
String _getEncryptedContentType(EncryptedContent content) {
if (content.hasMessageUpdate()) return 'messageUpdate';
if (content.hasMedia()) return 'media';
if (content.hasMediaUpdate()) return 'mediaUpdate';
if (content.hasContactUpdate()) return 'contactUpdate';
if (content.hasContactRequest()) return 'contactRequest';
if (content.hasFlameSync()) return 'flameSync';
if (content.hasPushKeys()) return 'pushKeys';
if (content.hasReaction()) return 'reaction';
if (content.hasTextMessage()) return 'textMessage';
if (content.hasGroupCreate()) return 'groupCreate';
if (content.hasGroupJoin()) return 'groupJoin';
if (content.hasGroupUpdate()) return 'groupUpdate';
if (content.hasResendGroupPublicKey()) return 'resendGroupPublicKey';
if (content.hasErrorMessages()) return 'errorMessages';
if (content.hasAdditionalDataMessage()) return 'additionalDataMessage';
if (content.hasTypingIndicator()) return 'typingIndicator';
if (content.hasUserDiscoveryRequest()) return 'userDiscoveryRequest';
if (content.hasUserDiscoveryUpdate()) return 'userDiscoveryUpdate';
if (content.hasKeyVerificationProof()) return 'keyVerificationProof';
return 'unknown';
}

View file

@ -105,8 +105,9 @@ class BackupService {
))) { ))) {
final backupId = await RustBackupIdentity.getBackupId(); final backupId = await RustBackupIdentity.getBackupId();
if (backupId == null) { if (backupId == null) {
Log.error('No backup password was set by the user.'); Log.warn('No backup password was set by the user.');
backup.identityState = LastBackupUploadState.failed; backup.identityState = LastBackupUploadState.failed;
await UserService.update((u) => u.isBackupEnabled = false);
} else { } else {
Log.info('Performing a identity backup.'); Log.info('Performing a identity backup.');
final encryptedBackup = final encryptedBackup =
@ -162,7 +163,7 @@ class BackupService {
(backupDownloadToken, backupArchive) = (backupDownloadToken, backupArchive) =
await RustBackupArchive.createBackupArchive(); await RustBackupArchive.createBackupArchive();
} catch (e) { } catch (e) {
Log.error(e); Log.warn('Creating archive backup failed: $e');
return; return;
} }
Log.info( Log.info(
@ -337,7 +338,7 @@ class BackupService {
}, },
); );
} catch (e) { } catch (e) {
Log.error('Error fetching backup: $e'); Log.warn('Error fetching backup: $e');
return (null, RecoveryError.noInternet); return (null, RecoveryError.noInternet);
} }

View file

@ -1,4 +1,6 @@
import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:twonly/locator.dart'; import 'package:twonly/locator.dart';
@ -128,6 +130,12 @@ Future<void> handleUserStudyUpload() async {
}); });
} }
} catch (e) { } catch (e) {
Log.error(e); if (e is http.ClientException ||
e is SocketException ||
e is TimeoutException) {
Log.warn('Error uploading user study data: $e');
} else {
Log.error(e);
}
} }
} }

View file

@ -41,10 +41,14 @@ class Log {
} }
static void error( static void error(
Object? messageInput, [ Object? messageInput, {
Object? error, Object? error,
StackTrace? stackTrace, StackTrace? stackTrace,
]) { bool onlyIfSentryEnabled = false,
}) {
if (!AppState.allowErrorTrackingViaSentry && onlyIfSentryEnabled) {
return;
}
final message = filterLogMessage('$messageInput'); final message = filterLogMessage('$messageInput');
if (AppState.allowErrorTrackingViaSentry) { if (AppState.allowErrorTrackingViaSentry) {
try { try {

View file

@ -28,7 +28,7 @@ class ScreenshotImageHelper {
try { try {
return imageBytes = await imageBytesFuture; return imageBytes = await imageBytesFuture;
} catch (e) { } catch (e) {
Log.error('Could not resolve imageBytesFuture: $e'); Log.warn('Could not resolve imageBytesFuture: $e');
return null; return null;
} }
} }
@ -36,14 +36,14 @@ class ScreenshotImageHelper {
try { try {
return imageBytes = await file!.readAsBytes(); return imageBytes = await file!.readAsBytes();
} catch (e) { } catch (e) {
Log.error('Could not read bytes from file: $e'); Log.warn('Could not read bytes from file: $e');
return null; return null;
} }
} }
if (image == null) return null; if (image == null) return null;
final img = await image!.toByteData(format: io.ImageByteFormat.png); final img = await image!.toByteData(format: io.ImageByteFormat.png);
if (img == null) { if (img == null) {
Log.error('Got no image'); Log.warn('Got no image');
return null; return null;
} }
return imageBytes = img.buffer.asUint8List(); return imageBytes = img.buffer.asUint8List();
@ -94,7 +94,7 @@ class ScreenshotController {
}); });
return completer.future; return completer.future;
} }
Log.error(e); Log.warn('Could not capture screenshot: $e');
} }
return null; return null;
} }

View file

@ -289,12 +289,18 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
mc.cameraController == null) { mc.cameraController == null) {
return; return;
} }
await mc.cameraController?.setZoomLevel( try {
newScale.clamp( if (mc.cameraController!.value.isInitialized) {
mc.selectedCameraDetails.minAvailableZoom, await mc.cameraController!.setZoomLevel(
mc.selectedCameraDetails.maxAvailableZoom, newScale.clamp(
), mc.selectedCameraDetails.minAvailableZoom,
); mc.selectedCameraDetails.maxAvailableZoom,
),
);
}
} on CameraException catch (e) {
Log.warn('Failed to set zoom level: $e');
}
setState(() { setState(() {
mc.selectedCameraDetails.scaleFactor = newScale; mc.selectedCameraDetails.scaleFactor = newScale;
}); });
@ -491,9 +497,16 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
(_basePanY - (details.localPosition.dy as double)) / 30) (_basePanY - (details.localPosition.dy as double)) / 30)
.clamp(1, mc.selectedCameraDetails.maxAvailableZoom); .clamp(1, mc.selectedCameraDetails.maxAvailableZoom);
}); });
await mc.cameraController!.setZoomLevel( try {
mc.selectedCameraDetails.scaleFactor, if (mc.cameraController != null &&
); mc.cameraController!.value.isInitialized) {
await mc.cameraController!.setZoomLevel(
mc.selectedCameraDetails.scaleFactor,
);
}
} on CameraException catch (e) {
Log.warn('Failed to set zoom level: $e');
}
if (!userService.currentUser.hasZoomed) { if (!userService.currentUser.hasZoomed) {
await UserService.update((u) => u.hasZoomed = true); await UserService.update((u) => u.hasZoomed = true);
} }

View file

@ -81,6 +81,7 @@ class PermissionHandlerViewState extends State<PermissionHandlerView>
} }
Future<void> _checkAndTriggerSuccess() async { Future<void> _checkAndTriggerSuccess() async {
if (!mounted) return;
if (_isSuccessTriggered) return; if (_isSuccessTriggered) return;
final route = ModalRoute.of(context); final route = ModalRoute.of(context);
if (route != null && !route.isCurrent) { if (route != null && !route.isCurrent) {

View file

@ -499,7 +499,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
pixelRatio: pixelRatio, pixelRatio: pixelRatio,
); );
if (image == null) { if (image == null) {
Log.error('screenshotController did not return image bytes'); Log.warn('screenshotController did not return image bytes');
return null; return null;
} }
@ -544,7 +544,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
if (image == null) return null; if (image == null) return null;
final bytes = await image.getBytes(); final bytes = await image.getBytes();
if (bytes == null) { if (bytes == null) {
Log.error('imageBytes are empty'); Log.warn('imageBytes are empty');
return null; return null;
} }
if (media.type == MediaType.image || media.type == MediaType.gif) { if (media.type == MediaType.image || media.type == MediaType.gif) {

View file

@ -50,7 +50,13 @@ Future<List<Sticker>> getStickerIndex() async {
return res; return res;
} }
} catch (e) { } catch (e) {
Log.error('$e'); if (e is http.ClientException ||
e is SocketException ||
e is TimeoutException) {
Log.warn('Could not load stickers index: $e');
} else {
Log.error('Could not load stickers index: $e');
}
return res; return res;
} }
} }

View file

@ -20,9 +20,9 @@ class _TypingIndicatorSubtitleCompState
extends State<TypingIndicatorSubtitleComp> { extends State<TypingIndicatorSubtitleComp> {
List<GroupMember> _groupMembers = []; List<GroupMember> _groupMembers = [];
late StreamSubscription<List<(Contact, GroupMember)>> membersSub; StreamSubscription<List<(Contact, GroupMember)>>? membersSub;
late Timer _periodicUpdate; Timer? _periodicUpdate;
@override @override
void initState() { void initState() {
@ -50,8 +50,8 @@ class _TypingIndicatorSubtitleCompState
@override @override
void dispose() { void dispose() {
membersSub.cancel(); membersSub?.cancel();
_periodicUpdate.cancel(); _periodicUpdate?.cancel();
super.dispose(); super.dispose();
} }

View file

@ -99,6 +99,7 @@ class _ChatMessagesViewState extends State<ChatMessagesView>
// } // }
bool _isViewActive() { bool _isViewActive() {
if (!mounted) return false;
return !AppState.isAppInBackground && return !AppState.isAppInBackground &&
(ModalRoute.of(context)?.isCurrent ?? false); (ModalRoute.of(context)?.isCurrent ?? false);
} }

View file

@ -169,7 +169,7 @@ class _RegisterViewState extends State<RegisterView> {
await apiService.authenticate(); await apiService.authenticate();
widget.callbackOnSuccess(); widget.callbackOnSuccess();
} catch (e, stack) { } catch (e, stack) {
Log.error('Error creating new user', e, stack); Log.error('Error creating new user', error: e, stackTrace: stack);
if (mounted) { if (mounted) {
setState(() { setState(() {
_isTryingToRegister = false; _isTryingToRegister = false;

View file

@ -46,6 +46,7 @@ class _SubscriptionViewState extends State<SubscriptionView> {
additionalOwnerName = ownerId.toString(); additionalOwnerName = ownerId.toString();
} }
} }
if (!mounted) return;
setState(() {}); setState(() {});
await apiService.forceIpaCheck(); await apiService.forceIpaCheck();
} }
@ -224,6 +225,7 @@ class _PlanCardState extends State<PlanCard> {
}); });
await context.read<PurchasesProvider>().buy(product); await context.read<PurchasesProvider>().buy(product);
await widget.onPurchase!(); await widget.onPurchase!();
if (!mounted) return;
setState(() { setState(() {
_isLoading = null; _isLoading = null;
}); });