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;
} on PlatformException catch (e) {
Log.error('Failed to compress video: $e');
Log.warn('Failed to compress video: $e');
return null;
} finally {
_currentProgressCallback = null;

View file

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

View file

@ -53,7 +53,7 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
))
.go();
}
Future<void> deleteReceiptsByMessageId(String messageId) async {
await (delete(receipts)..where(
(t) => t.messageId.equals(messageId),
@ -201,8 +201,8 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
);
final updatedReceipt = await getReceiptById(newReceiptId);
if (updatedReceipt == null) {
Log.error(
'Tried to change the receipt ID, but could not get the updated receipt...',
Log.warn(
'[$oldReceiptId] Tried to change the receipt ID to $newReceiptId, but could not get the updated receipt...',
);
}
return updatedReceipt;
@ -253,6 +253,9 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
Future<void> gotReceipt(String receiptId) async {
await into(
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 result = await sendRequestSync(req, authenticated: false);
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) {
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 (result.value.proofOfWork as Response_ProofOfWork, false);

View file

@ -17,7 +17,7 @@ Future<void> handleErrorMessage(
String receiptId, {
String? groupId,
}) async {
Log.error('[$receiptId] Got error from $fromUserId: $error');
Log.warn('[$receiptId] Got error from $fromUserId: $error');
switch (error.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...
case EncryptedContent_ErrorMessages_Type.GROUP_NOT_FOUND_OR_NOT_A_MEMBER:
if (groupId == null) {
Log.error(
Log.warn(
'[$receiptId] GROUP_NOT_FOUND_OR_NOT_A_MEMBER error received, but groupId is null.',
);
return;
}
final group = await twonlyDB.groupsDao.getGroup(groupId);
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.',
);
return;

View file

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

View file

@ -80,9 +80,13 @@ Future<void> handleUploadStatusUpdate(TaskStatusUpdate update) async {
await markUploadAsSuccessful(media);
return;
}
Log.error(
Log.warn(
'Got HTTP error ${update.responseStatusCode} for $mediaId',
);
Log.error(
'Got HTTP error ${update.responseStatusCode} for media.',
onlyIfSentryEnabled: true,
);
}
if (update.status == TaskStatus.notFound) {
@ -118,9 +122,13 @@ Future<void> handleUploadStatusUpdate(TaskStatusUpdate update) async {
if (update.status == TaskStatus.failed ||
update.status == TaskStatus.canceled) {
Log.error(
Log.warn(
'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);
// 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);
Log.info('[$receiptId] Finished processing');
} 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()) {
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);
}
@ -453,7 +464,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
);
if (contact == null || !contact.accepted || contact.deletedByUser) {
await handleNewContactRequest(fromUserId);
Log.error(
Log.warn(
'[$receiptId] User tries to send message to direct chat while the user does not exist!',
);
return (
@ -478,7 +489,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
);
} else {
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.',
);
// 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.',
);
return (
@ -598,3 +609,26 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
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();
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;
await UserService.update((u) => u.isBackupEnabled = false);
} else {
Log.info('Performing a identity backup.');
final encryptedBackup =
@ -162,7 +163,7 @@ class BackupService {
(backupDownloadToken, backupArchive) =
await RustBackupArchive.createBackupArchive();
} catch (e) {
Log.error(e);
Log.warn('Creating archive backup failed: $e');
return;
}
Log.info(
@ -337,7 +338,7 @@ class BackupService {
},
);
} catch (e) {
Log.error('Error fetching backup: $e');
Log.warn('Error fetching backup: $e');
return (null, RecoveryError.noInternet);
}

View file

@ -1,4 +1,6 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:http/http.dart' as http;
import 'package:twonly/locator.dart';
@ -128,6 +130,12 @@ Future<void> handleUserStudyUpload() async {
});
}
} 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(
Object? messageInput, [
Object? messageInput, {
Object? error,
StackTrace? stackTrace,
]) {
bool onlyIfSentryEnabled = false,
}) {
if (!AppState.allowErrorTrackingViaSentry && onlyIfSentryEnabled) {
return;
}
final message = filterLogMessage('$messageInput');
if (AppState.allowErrorTrackingViaSentry) {
try {

View file

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

View file

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

View file

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

View file

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

View file

@ -50,7 +50,13 @@ Future<List<Sticker>> getStickerIndex() async {
return res;
}
} 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;
}
}

View file

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

View file

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

View file

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

View file

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