From 3f5cfef2fa3a4d7eae1c884e404cea0346cc3184 Mon Sep 17 00:00:00 2001 From: dab246 Date: Thu, 15 May 2025 15:25:41 +0700 Subject: [PATCH] TF-3719 Add LogTracking to trace error `notFound` email in `Email/get` Signed-off-by: dab246 --- core/lib/core.dart | 1 + core/lib/utils/logger/log_tracking.dart | 150 ++++++++++++++++++ .../extensions/list_email_id_extension.dart | 2 + .../thread/data/network/thread_api.dart | 28 +++- lib/main/utils/app_config.dart | 4 + 5 files changed, 183 insertions(+), 2 deletions(-) create mode 100644 core/lib/utils/logger/log_tracking.dart diff --git a/core/lib/core.dart b/core/lib/core.dart index 79cdfdeb4..4fecad801 100644 --- a/core/lib/core.dart +++ b/core/lib/core.dart @@ -54,6 +54,7 @@ export 'utils/mail/domain.dart'; export 'utils/mail/mail_address.dart'; export 'utils/application_manager.dart'; export 'utils/preview_eml_file_utils.dart'; +export 'utils/logger/log_tracking.dart'; // Views export 'presentation/views/text/slogan_builder.dart'; diff --git a/core/lib/utils/logger/log_tracking.dart b/core/lib/utils/logger/log_tracking.dart new file mode 100644 index 000000000..5b5f0ed7d --- /dev/null +++ b/core/lib/utils/logger/log_tracking.dart @@ -0,0 +1,150 @@ +import 'dart:async'; +import 'dart:collection'; +import 'dart:io'; + +import 'package:core/domain/exceptions/download_file_exception.dart'; +import 'package:core/utils/platform_info.dart'; +import 'package:intl/intl.dart'; +import 'package:path_provider/path_provider.dart'; + +class LogTracking { + static const String logFolder = 'TraceLogs'; + static const String logFileNameDatePattern = 'yyyy-MM-dd'; + static const String logMessageDatePattern = 'yyyy-MM-dd, HH:mm:ss'; + + LogTracking._(); + + factory LogTracking() => _instance ??= LogTracking._(); + + static LogTracking? _instance; + + final Queue _messagesQueue = Queue(); + bool _isScheduled = false; + + Future addLog({required String message}) async { + try { + _messagesQueue.add(message); + + if (!_isScheduled) { + _isScheduled = true; + await _executeTraceLog(); + } + } catch (_) { + _messagesQueue.remove(message); + _isScheduled = false; + } + } + + Future _executeTraceLog() async { + while (true) { + try { + if (_messagesQueue.isEmpty) { + _isScheduled = false; + return; + } + + final message = _messagesQueue.removeFirst(); + await saveLog(message: message); + } catch (_) {} + } + } + + Future saveLog({required String message}) async { + try { + final currentDate = DateTime.timestamp(); + + final logFileName = generateLogFileName(currentDate: currentDate); + + final messageSanitized = sanitizeMessage( + message: message, + currentDate: currentDate, + ); + + await saveToFile( + nameFile: logFileName, + folderPath: logFolder, + content: messageSanitized, + ); + } catch (_) {} + } + + String sanitizeMessage({ + required String message, + required DateTime currentDate, + }) { + final dateFormat = getDateFormatAsString( + pattern: logMessageDatePattern, + currentDate: currentDate, + ); + + return '($dateFormat): $message \n'; + } + + String getDateFormatAsString({ + required String pattern, + required DateTime currentDate, + }) { + final dateFormat = DateFormat(pattern); + return dateFormat.format(currentDate); + } + + String generateLogFileName({required DateTime currentDate}) { + final dateFormat = getDateFormatAsString( + pattern: logFileNameDatePattern, + currentDate: currentDate, + ); + + return '${dateFormat}_log'; + } + + Future _getInternalStorageDirPath({ + String? nameFile, + String? folderPath, + String? extensionFile, + }) async { + if (PlatformInfo.isMobile) { + String fileDirectory = + (await getApplicationDocumentsDirectory()).absolute.path; + + if (folderPath != null) { + fileDirectory = '$fileDirectory/$folderPath'; + } + + Directory directory = Directory(fileDirectory); + + if (!await directory.exists()) { + await directory.create(recursive: true); + } + + if (nameFile != null) { + fileDirectory = '$fileDirectory/$nameFile'; + } + + if (extensionFile != null) { + fileDirectory = '$fileDirectory.$extensionFile'; + } + + return fileDirectory; + } else { + throw DeviceNotSupportedException(); + } + } + + Future saveToFile({ + required String nameFile, + required String content, + String? folderPath, + String? extensionFile, + FileMode fileMode = FileMode.append, + }) async { + final internalStorageDirPath = await _getInternalStorageDirPath( + nameFile: nameFile, + folderPath: folderPath, + extensionFile: extensionFile, + ); + + final file = File(internalStorageDirPath); + + return await file.writeAsString(content, mode: fileMode); + } +} diff --git a/lib/features/thread/data/extensions/list_email_id_extension.dart b/lib/features/thread/data/extensions/list_email_id_extension.dart index f16482426..f08623270 100644 --- a/lib/features/thread/data/extensions/list_email_id_extension.dart +++ b/lib/features/thread/data/extensions/list_email_id_extension.dart @@ -9,4 +9,6 @@ import 'package:tmail_ui_user/features/caching/utils/cache_utils.dart'; extension ListEmailIdExtension on List { List toCacheKeyList(AccountId accountId, UserName userName) => map((id) => TupleKey(id.asString, accountId.asString, userName.value).encodeKey).toList(); + + List get asListString => map((id) => id.asString).toList(); } \ No newline at end of file diff --git a/lib/features/thread/data/network/thread_api.dart b/lib/features/thread/data/network/thread_api.dart index 4b1e5a95b..bdb781bcc 100644 --- a/lib/features/thread/data/network/thread_api.dart +++ b/lib/features/thread/data/network/thread_api.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:core/utils/app_logger.dart'; +import 'package:core/utils/logger/log_tracking.dart'; import 'package:jmap_dart_client/http/http_client.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/filter/filter.dart'; @@ -25,10 +26,12 @@ import 'package:tmail_ui_user/features/thread/data/extensions/list_email_extensi import 'package:jmap_dart_client/jmap/mail/email/search_snippet/search_snippet.dart'; import 'package:jmap_dart_client/jmap/mail/email/search_snippet/search_snippet_get_method.dart'; import 'package:jmap_dart_client/jmap/mail/email/search_snippet/search_snippet_get_response.dart'; +import 'package:tmail_ui_user/features/thread/data/extensions/list_email_id_extension.dart'; import 'package:tmail_ui_user/features/thread/data/model/email_change_response.dart'; import 'package:tmail_ui_user/features/thread/domain/model/email_response.dart'; import 'package:tmail_ui_user/features/thread/domain/model/search_emails_response.dart'; import 'package:tmail_ui_user/main/error/capability_validator.dart'; +import 'package:tmail_ui_user/main/utils/app_config.dart'; class ThreadAPI { @@ -101,6 +104,11 @@ class ThreadAPI { ?.toEmailIds() .toList(); log('ThreadAPI::getAllEmail:notFoundEmailIds = $notFoundEmailIds'); + if (notFoundEmailIds?.isNotEmpty == true && AppConfig.isApiLoggingEnabled) { + await LogTracking().addLog( + message: 'ThreadAPI::getAllEmail:notFoundEmailIds = ${notFoundEmailIds!.asListString.toString()}', + ); + } return EmailsResponse( emailList: emailList, notFoundEmailIds: notFoundEmailIds, @@ -299,7 +307,15 @@ class ThreadAPI { newStateEmail = emailResponseUpdated?.state; final notFoundIdsUpdated = emailResponseUpdated?.notFound?.toEmailIds().toList() ?? []; log('ThreadAPI::getChanges:notFoundIdsUpdated = $notFoundIdsUpdated'); - destroyedEmailIds.addAll(notFoundIdsUpdated); + if (notFoundIdsUpdated.isNotEmpty) { + destroyedEmailIds.addAll(notFoundIdsUpdated); + + if (AppConfig.isApiLoggingEnabled) { + await LogTracking().addLog( + message: 'ThreadAPI::getChanges:notFoundIdsUpdated = ${notFoundIdsUpdated.asListString.toString()}', + ); + } + } } if (getEmailCreatedInvocation != null) { @@ -311,7 +327,15 @@ class ThreadAPI { newStateEmail = emailResponseCreated?.state; final notFoundIdsCreated = emailResponseCreated?.notFound?.toEmailIds().toList() ?? []; log('ThreadAPI::getChanges:notFoundIdsCreated = $notFoundIdsCreated'); - destroyedEmailIds.addAll(notFoundIdsCreated); + if (notFoundIdsCreated.isNotEmpty) { + destroyedEmailIds.addAll(notFoundIdsCreated); + + if (AppConfig.isApiLoggingEnabled) { + await LogTracking().addLog( + message: 'ThreadAPI::getChanges:notFoundIdsCreated = ${notFoundIdsCreated.asListString.toString()}', + ); + } + } } log('ThreadAPI::getChanges:newStateChanges = $newStateChanges | newStateEmail = $newStateEmail | hasMoreChanges = $hasMoreChanges'); log('ThreadAPI::getChanges:updatedEmailSize = ${updatedEmail?.length} | createdEmailSize = ${createdEmail?.length}'); diff --git a/lib/main/utils/app_config.dart b/lib/main/utils/app_config.dart index 1e0a7b07b..2efb63681 100644 --- a/lib/main/utils/app_config.dart +++ b/lib/main/utils/app_config.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:core/utils/platform_info.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; @@ -13,6 +14,9 @@ class AppConfig { static const int warningAttachmentFileSizeInMegabytes = 10; static const int defaultLimitAutocomplete = 8; + // For testing + static bool isApiLoggingEnabled = PlatformInfo.isAndroid; + static const String envFileName = 'env.file'; static const String appDashboardConfigurationPath = "configurations/app_dashboard.json"; static const String appFCMConfigurationPath = "configurations/env.fcm";