From ec75bb3cab8a4f386fda344e44129f230ed41c4e Mon Sep 17 00:00:00 2001 From: dab246 Date: Mon, 15 Dec 2025 12:01:21 +0700 Subject: [PATCH] TF-4136 Add config Sentry --- contact/pubspec.lock | 58 ++++++++- contact/pubspec.yaml | 2 +- core/lib/core.dart | 7 +- core/lib/utils/app_logger.dart | 34 +++--- core/lib/utils/application_manager.dart | 24 +++- core/lib/utils/config/env_loader.dart | 44 +++++++ core/lib/utils/sentry/sentry_config.dart | 74 ++++++++++++ core/lib/utils/sentry/sentry_initializer.dart | 45 +++++++ core/lib/utils/sentry/sentry_manager.dart | 103 ++++++++++++++++ core/pubspec.lock | 56 ++++++++- core/pubspec.yaml | 6 +- core/test/utils/application_manager_test.dart | 113 +++++++++--------- email_recovery/pubspec.lock | 6 +- email_recovery/pubspec.yaml | 2 +- env.file | 5 +- fcm/pubspec.lock | 6 +- fcm/pubspec.yaml | 2 +- forward/pubspec.lock | 6 +- forward/pubspec.yaml | 2 +- integration_test/base/test_base.dart | 4 +- lib/features/base/base_controller.dart | 2 +- .../authorization_interceptors.dart | 4 +- .../login/data/network/oidc_http_client.dart | 6 +- .../mailbox_dashboard_controller.dart | 14 ++- .../manage_account_dashboard_controller.dart | 13 ++ .../upload/data/network/file_uploader.dart | 4 +- .../domain/model/upload_attachment.dart | 2 +- lib/main.dart | 35 +----- lib/main/app_runner.dart | 49 ++++++++ lib/main/bindings/core/core_bindings.dart | 2 - .../bindings/network/network_bindings.dart | 16 ++- .../network/network_isolate_binding.dart | 22 ++-- .../exceptions/remote_exception_thrower.dart | 10 +- lib/main/main_entry.dart | 34 ++++++ lib/main/utils/app_config.dart | 2 - lib/main/utils/app_utils.dart | 22 +--- lib/main/utils/toast_manager.dart | 2 +- model/pubspec.lock | 58 ++++++++- pubspec.lock | 46 ++++++- pubspec.yaml | 6 +- rule_filter/pubspec.lock | 6 +- rule_filter/pubspec.yaml | 2 +- scribe/pubspec.lock | 54 ++++++++- scribe/pubspec.yaml | 2 +- server_settings/pubspec.lock | 6 +- server_settings/pubspec.yaml | 2 +- 46 files changed, 804 insertions(+), 216 deletions(-) create mode 100644 core/lib/utils/config/env_loader.dart create mode 100644 core/lib/utils/sentry/sentry_config.dart create mode 100644 core/lib/utils/sentry/sentry_initializer.dart create mode 100644 core/lib/utils/sentry/sentry_manager.dart create mode 100644 lib/main/app_runner.dart create mode 100644 lib/main/main_entry.dart diff --git a/contact/pubspec.lock b/contact/pubspec.lock index 52faef956..d58109b58 100644 --- a/contact/pubspec.lock +++ b/contact/pubspec.lock @@ -29,10 +29,10 @@ packages: dependency: transitive description: name: args - sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.7.0" async: dependency: transitive description: @@ -268,10 +268,10 @@ packages: dependency: "direct main" description: name: dio - sha256: "9fdbf71baeb250fc9da847f6cb2052196f62c19906a3657adfc18631a667d316" + sha256: e44ce32c01f02c54a101551def8586f15d6189b4f999d4420aad38995ac62e6d url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "5.2.0" equatable: dependency: "direct main" description: @@ -381,6 +381,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.2" + flutter_dotenv: + dependency: transitive + description: + name: flutter_dotenv + sha256: d9283d92059a22e9834bc0a31336658ffba77089fb6f3cc36751f1fc7c6661a3 + url: "https://pub.dev" + source: hosted + version: "5.0.2" flutter_image_compress: dependency: transitive description: @@ -676,10 +684,18 @@ packages: description: path: "." ref: main - resolved-ref: "446efbbccdde981371b4d2a636762184adf6e926" + resolved-ref: "8e2bd9cb1bbd8236be895aa04bf0d0da8e2ff3b2" url: "https://github.com/linagora/jmap-dart-client.git" source: git version: "0.3.6" + jni: + dependency: transitive + description: + name: jni + sha256: d2c361082d554d4593c3012e26f6b188f902acd291330f13d6427641a92b3da1 + url: "https://pub.dev" + source: hosted + version: "0.14.2" js: dependency: transitive description: @@ -799,6 +815,14 @@ packages: relative: true source: path version: "1.0.0+1" + objective_c: + dependency: transitive + description: + name: objective_c + sha256: "64e35e1e2e79da4e83f2ace3bf4e5437cef523f46c7db2eba9a1419c49573790" + url: "https://pub.dev" + source: hosted + version: "8.0.0" package_config: dependency: transitive description: @@ -1024,6 +1048,22 @@ packages: url: "https://github.com/linagora/dart-neats.git" source: git version: "3.0.1" + sentry: + dependency: transitive + description: + name: sentry + sha256: "10a0bc25f5f21468e3beeae44e561825aaa02cdc6829438e73b9b64658ff88d9" + url: "https://pub.dev" + source: hosted + version: "9.8.0" + sentry_flutter: + dependency: transitive + description: + name: sentry_flutter + sha256: aafbf41c63c98a30b17bdbf3313424d5102db62b08735c44bff810f277e786a5 + url: "https://pub.dev" + source: hosted + version: "9.8.0" shelf: dependency: transitive description: @@ -1229,6 +1269,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.0" + uuid: + dependency: transitive + description: + name: uuid + sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8 + url: "https://pub.dev" + source: hosted + version: "4.5.2" vector_graphics: dependency: transitive description: diff --git a/contact/pubspec.yaml b/contact/pubspec.yaml index 8014f649f..96fe00b80 100644 --- a/contact/pubspec.yaml +++ b/contact/pubspec.yaml @@ -25,7 +25,7 @@ dependencies: json_annotation: 4.8.0 - dio: 5.0.0 + dio: 5.2.0 http_mock_adapter: 0.4.2 diff --git a/core/lib/core.dart b/core/lib/core.dart index f79050fd9..24c76429c 100644 --- a/core/lib/core.dart +++ b/core/lib/core.dart @@ -60,6 +60,8 @@ export 'utils/preview_eml_file_utils.dart'; export 'utils/logger/log_tracking.dart'; export 'utils/html/html_utils.dart'; export 'utils/web_link_generator.dart'; +export 'utils/sentry/sentry_manager.dart'; +export 'utils/config/env_loader.dart'; // Views export 'presentation/views/text/slogan_builder.dart'; @@ -138,4 +140,7 @@ export 'data/model/source_type/data_source_type.dart'; export 'data/model/query/query_parameter.dart'; // Action -export 'presentation/action/action_callback_define.dart'; \ No newline at end of file +export 'presentation/action/action_callback_define.dart'; + +// Library +export 'package:package_info_plus/package_info_plus.dart'; \ No newline at end of file diff --git a/core/lib/utils/app_logger.dart b/core/lib/utils/app_logger.dart index 9ccc0055a..439a29e2b 100644 --- a/core/lib/utils/app_logger.dart +++ b/core/lib/utils/app_logger.dart @@ -1,8 +1,7 @@ -import 'dart:async'; -import 'package:universal_html/html.dart' as html; import 'package:core/utils/build_utils.dart'; import 'package:core/utils/platform_info.dart'; -import 'package:flutter/material.dart'; +import 'package:core/utils/sentry/sentry_manager.dart'; +import 'package:universal_html/html.dart' as html; /// ANSI escape colors (Web only) const ansiReset = '\x1B[0m'; @@ -73,6 +72,15 @@ void _internalLog( // ignore: avoid_print print('$appLogName $formattedMessage'); } + + if (_shouldReportToSentry(level)) { + SentryManager.instance.captureException( + exception ?? rawMessage, + stackTrace: stackTrace, + message: rawMessage, + extras: extras, + ); + } } String _buildRawMessage( @@ -114,6 +122,10 @@ void _printWebConsole(Level level, String value) { } } +bool _shouldReportToSentry(Level level) { + return level == Level.error || level == Level.critical; +} + void logError( String? message, { Object? exception, @@ -205,18 +217,4 @@ enum Level { info, debug, trace, -} - -// Take from: https://flutter.dev/docs/testing/errors -void initLogger(VoidCallback runApp) { - runZonedGuarded(() async { - WidgetsFlutterBinding.ensureInitialized(); - FlutterError.onError = (details) { - FlutterError.dumpErrorToConsole(details); - logWarning('AppLogger::initLogger::runZonedGuarded:FlutterError.onError: ${details.stack.toString()}'); - }; - runApp.call(); - }, (error, stack) { - logWarning('AppLogger::initLogger::runZonedGuarded:onError: $error | stack: $stack'); - }); -} +} \ No newline at end of file diff --git a/core/lib/utils/application_manager.dart b/core/lib/utils/application_manager.dart index fa854be93..78a16adb5 100644 --- a/core/lib/utils/application_manager.dart +++ b/core/lib/utils/application_manager.dart @@ -1,15 +1,23 @@ - import 'package:core/utils/app_logger.dart'; import 'package:core/utils/platform_info.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:fk_user_agent/fk_user_agent.dart'; +import 'package:flutter/cupertino.dart'; import 'package:package_info_plus/package_info_plus.dart'; class ApplicationManager { + static final ApplicationManager _instance = ApplicationManager._internal(); - final DeviceInfoPlugin _deviceInfoPlugin; + factory ApplicationManager() => _instance; - ApplicationManager(this._deviceInfoPlugin); + ApplicationManager._internal(); + + // Allow overriding in unit tests + @visibleForTesting + static DeviceInfoPlugin? debugDeviceInfoOverride; + + DeviceInfoPlugin get _deviceInfoPlugin => + debugDeviceInfoOverride ?? DeviceInfoPlugin(); Future getPackageInfo() async { final packageInfo = await PackageInfo.fromPlatform(); @@ -18,9 +26,13 @@ class ApplicationManager { } Future getVersion() async { - final version = (await getPackageInfo()).version; - log('ApplicationManager::getVersion: $version'); - return version; + try { + final version = (await getPackageInfo()).version; + log('ApplicationManager::getVersion: $version'); + return version; + } catch (e) { + return ''; + } } Future getUserAgent() async { diff --git a/core/lib/utils/config/env_loader.dart b/core/lib/utils/config/env_loader.dart new file mode 100644 index 000000000..aab975b31 --- /dev/null +++ b/core/lib/utils/config/env_loader.dart @@ -0,0 +1,44 @@ +import 'package:core/utils/app_logger.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; + +class EnvLoader { + const EnvLoader._(); + + static const String envFileName = 'env.file'; + static const String appFCMConfigurationPath = "configurations/env.fcm"; + + static Future loadEnvFile() async { + await loadConfigFromEnv(); + final mapEnvData = Map.from(dotenv.env); + await loadFcmConfigFileToEnv( + currentMapEnvData: mapEnvData, + onCallBack: () async { + await loadConfigFromEnv(); + }, + ); + } + + static Future loadFcmConfigFileToEnv({ + Map? currentMapEnvData, + VoidCallback? onCallBack, + }) async { + try { + await dotenv.load( + fileName: appFCMConfigurationPath, + mergeWith: currentMapEnvData ?? {}, + ); + } catch (e) { + logWarning('EnvLoader::loadFcmConfigFileToEnv: Exception = $e'); + onCallBack?.call(); + } + } + + static Future loadConfigFromEnv() async { + try { + await dotenv.load(fileName: envFileName); + } catch (e) { + logWarning('EnvLoader::loadConfigFromEnv:Exception = $e'); + } + } +} diff --git a/core/lib/utils/sentry/sentry_config.dart b/core/lib/utils/sentry/sentry_config.dart new file mode 100644 index 000000000..72c1bcf84 --- /dev/null +++ b/core/lib/utils/sentry/sentry_config.dart @@ -0,0 +1,74 @@ +import 'package:core/utils/application_manager.dart'; +import 'package:core/utils/build_utils.dart'; +import 'package:core/utils/config/env_loader.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; + +/// Holds configuration values for initializing Sentry. +class SentryConfig { + // DSN (Data Source Name) endpoint for the Sentry project + final String dsn; + + // Running environment (production/staging/dev) + final String environment; + + // Current app release version + final String release; + + // // Performance monitoring: Set tracesSampleRate to 1.0 to capture 100% of transactions for tracing + final double tracesSampleRate; + + // Optional profiling + final double profilesSampleRate; + + // Enable logs to be sent to Sentry. To use Sentry.logger.fmt + final bool enableLogs; + + // Debug logs during development + final bool isDebug; + + // Automatically attaches a screenshot when capturing an error or exception. + final bool attachScreenshot; + + // Check if Sentry is available + final bool isAvailable; + + SentryConfig({ + required this.dsn, + required this.environment, + required this.release, + this.tracesSampleRate = 1.0, + this.profilesSampleRate = 1.0, + this.enableLogs = true, + this.isDebug = BuildUtils.isDebugMode, + this.attachScreenshot = false, + this.isAvailable = false, + }); + + /// Load configuration from an env file. + static Future load() async { + await EnvLoader.loadConfigFromEnv(); + + final sentryAvailable = dotenv.get('SENTRY_ENABLED', fallback: 'false'); + + final isAvailable = sentryAvailable == 'true'; + final sentryDSN = dotenv.get('SENTRY_DSN', fallback: ''); + final sentryEnvironment = dotenv.get('SENTRY_ENVIRONMENT', fallback: ''); + + if (!isAvailable) { + throw Exception('Sentry is not available'); + } + + if (sentryDSN.trim().isEmpty || sentryEnvironment.trim().isEmpty) { + throw Exception('Sentry configuration is missing'); + } + + final appVersion = await ApplicationManager().getVersion(); + + return SentryConfig( + dsn: sentryDSN, + environment: sentryEnvironment, + release: appVersion, + isAvailable: isAvailable, + ); + } +} diff --git a/core/lib/utils/sentry/sentry_initializer.dart b/core/lib/utils/sentry/sentry_initializer.dart new file mode 100644 index 000000000..601a3c407 --- /dev/null +++ b/core/lib/utils/sentry/sentry_initializer.dart @@ -0,0 +1,45 @@ +import 'dart:async'; + +import 'package:core/utils/sentry/sentry_config.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; + +class SentryInitializer { + static Future init(FutureOr Function() appRunner) async { + final config = await SentryConfig.load(); + + await SentryFlutter.init( + (options) { + options.dsn = config.dsn; + options.environment = config.environment; + options.release = config.release; + options.tracesSampleRate = config.tracesSampleRate; + options.profilesSampleRate = config.profilesSampleRate; + options.enableLogs = config.enableLogs; + options.debug = config.isDebug; + options.attachScreenshot = config.attachScreenshot; + options.maxRequestBodySize = MaxRequestBodySize.small; + + // Automatically enable breadcrumbs that are appropriate for the current platform + options.enableBreadcrumbTrackingForCurrentPlatform(); + + // Assign the callback to process events before sending them to Sentry + options.beforeSend = _beforeSendHandler; + }, + appRunner: appRunner, + ); + + return config.isAvailable; + } + + /// Handler executed before sending an event to Sentry + static Future _beforeSendHandler( + SentryEvent event, + Hint? hint, + ) async { + // Ignore AssertionError events + if (event.throwable is AssertionError) { + return null; + } + return event; + } +} diff --git a/core/lib/utils/sentry/sentry_manager.dart b/core/lib/utils/sentry/sentry_manager.dart new file mode 100644 index 000000000..909dd723a --- /dev/null +++ b/core/lib/utils/sentry/sentry_manager.dart @@ -0,0 +1,103 @@ +import 'dart:async'; + +import 'package:core/utils/app_logger.dart'; +import 'package:core/utils/sentry/sentry_initializer.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; + +/// Controls Sentry initialization and error reporting. +class SentryManager { + SentryManager._(); + + static final SentryManager instance = SentryManager._(); + + bool _isSentryAvailable = false; + + bool get isSentryAvailable => _isSentryAvailable; + + /// Initialize Sentry. App still runs if init fails. + Future initialize({ + required FutureOr Function() appRunner, + required FutureOr Function() fallBackRunner, + }) async { + try { + _isSentryAvailable = await SentryInitializer.init(appRunner); + log('[SentryManager] Sentry initialized: $_isSentryAvailable'); + } catch (e, st) { + logError('[SentryManager] Init failed', exception: e, stackTrace: st); + await fallBackRunner(); + } + } + + /// Capture an exception. Metadata is attached as breadcrumbs. + Future captureException( + dynamic exception, { + StackTrace? stackTrace, + String? message, + Map? extras, + }) async { + if (!_isSentryAvailable) return; + + await Sentry.captureException( + exception, + stackTrace: stackTrace, + withScope: (scope) { + scope.addBreadcrumb( + Breadcrumb( + message: message ?? exception.toString(), + data: extras, + level: SentryLevel.error, + ), + ); + }, + ); + } + + /// Capture a text message. Metadata also goes into breadcrumbs. + Future captureMessage( + String message, { + SentryLevel level = SentryLevel.info, + Map? extras, + }) async { + if (!_isSentryAvailable) return; + + await Sentry.captureMessage( + message, + level: level, + withScope: (scope) { + scope.addBreadcrumb( + Breadcrumb( + message: message, + data: extras, + level: SentryLevel.info, + ), + ); + }, + ); + } + + Future setUser(SentryUser user) async { + if (!_isSentryAvailable) return; + + try { + await Sentry.configureScope((scope) => scope.setUser(user)); + log('[SentryManager] User set: ${user.email}'); + } catch (e, st) { + logError('[SentryManager] Set user failed', exception: e, stackTrace: st); + } + } + + Future clearUser() async { + if (!_isSentryAvailable) return; + + try { + await Sentry.configureScope((scope) => scope.setUser(null)); + log('[SentryManager] User cleared'); + } catch (e, st) { + logError( + '[SentryManager] Clear user failed', + exception: e, + stackTrace: st, + ); + } + } +} diff --git a/core/pubspec.lock b/core/pubspec.lock index ed0f00234..fe2f4a19b 100644 --- a/core/pubspec.lock +++ b/core/pubspec.lock @@ -29,10 +29,10 @@ packages: dependency: transitive description: name: args - sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.7.0" async: dependency: transitive description: @@ -261,10 +261,10 @@ packages: dependency: "direct main" description: name: dio - sha256: "9fdbf71baeb250fc9da847f6cb2052196f62c19906a3657adfc18631a667d316" + sha256: e44ce32c01f02c54a101551def8586f15d6189b4f999d4420aad38995ac62e6d url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "5.2.0" equatable: dependency: "direct main" description: @@ -374,6 +374,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.2" + flutter_dotenv: + dependency: "direct main" + description: + name: flutter_dotenv + sha256: d9283d92059a22e9834bc0a31336658ffba77089fb6f3cc36751f1fc7c6661a3 + url: "https://pub.dev" + source: hosted + version: "5.0.2" flutter_image_compress: dependency: "direct main" description: @@ -656,6 +664,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + jni: + dependency: transitive + description: + name: jni + sha256: d2c361082d554d4593c3012e26f6b188f902acd291330f13d6427641a92b3da1 + url: "https://pub.dev" + source: hosted + version: "0.14.2" js: dependency: transitive description: @@ -760,6 +776,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.4.4" + objective_c: + dependency: transitive + description: + name: objective_c + sha256: "64e35e1e2e79da4e83f2ace3bf4e5437cef523f46c7db2eba9a1419c49573790" + url: "https://pub.dev" + source: hosted + version: "8.0.0" package_config: dependency: transitive description: @@ -977,6 +1001,22 @@ packages: url: "https://github.com/linagora/dart-neats.git" source: git version: "3.0.1" + sentry: + dependency: transitive + description: + name: sentry + sha256: "10a0bc25f5f21468e3beeae44e561825aaa02cdc6829438e73b9b64658ff88d9" + url: "https://pub.dev" + source: hosted + version: "9.8.0" + sentry_flutter: + dependency: "direct main" + description: + name: sentry_flutter + sha256: aafbf41c63c98a30b17bdbf3313424d5102db62b08735c44bff810f277e786a5 + url: "https://pub.dev" + source: hosted + version: "9.8.0" shelf: dependency: transitive description: @@ -1166,6 +1206,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.0" + uuid: + dependency: transitive + description: + name: uuid + sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8 + url: "https://pub.dev" + source: hosted + version: "4.5.2" vector_graphics: dependency: transitive description: diff --git a/core/pubspec.yaml b/core/pubspec.yaml index 4c10217ea..620d7568e 100644 --- a/core/pubspec.yaml +++ b/core/pubspec.yaml @@ -41,7 +41,7 @@ dependencies: flutter_svg: 2.1.0 - dio: 5.0.0 + dio: 5.2.0 dartz: 0.10.1 @@ -101,6 +101,10 @@ dependencies: web: 1.1.1 + flutter_dotenv: 5.0.2 + + sentry_flutter: 9.8.0 + dev_dependencies: flutter_test: sdk: flutter diff --git a/core/test/utils/application_manager_test.dart b/core/test/utils/application_manager_test.dart index 5eddbd545..4c1ee11a1 100644 --- a/core/test/utils/application_manager_test.dart +++ b/core/test/utils/application_manager_test.dart @@ -10,9 +10,7 @@ import 'package:mockito/mockito.dart'; import 'application_manager_test.mocks.dart'; -@GenerateNiceMocks([ - MockSpec() -]) +@GenerateNiceMocks([MockSpec()]) void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -21,55 +19,73 @@ void main() { setUp(() { mockDeviceInfoPlugin = MockDeviceInfoPlugin(); - applicationManager = ApplicationManager(mockDeviceInfoPlugin); + ApplicationManager.debugDeviceInfoOverride = mockDeviceInfoPlugin; + + applicationManager = ApplicationManager(); + }); + + tearDown(() { + ApplicationManager.debugDeviceInfoOverride = null; + PlatformInfo.isTestingForWeb = false; + debugDefaultTargetPlatformOverride = null; + + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler( + const MethodChannel('fk_user_agent'), + null, + ); + + FkUserAgent.release(); }); group('ApplicationManager::getUserAgent test', () { - test('WHEN platform is Web THEN getUserAgent should be return user agent for web', () async { + test( + 'WHEN platform is Web THEN getUserAgent should return user agent for web', + () async { const webUserAgent = 'User-Agent-Twake-Mail-Web'; PlatformInfo.isTestingForWeb = true; - when(mockDeviceInfoPlugin.webBrowserInfo) - .thenAnswer((_) async => WebBrowserInfo( - userAgent: webUserAgent, - appCodeName: '', - appName: '', - appVersion: '', - deviceMemory: null, - language: '', - languages: [], - platform: '', - product: '', - productSub: '', - vendor: '', - vendorSub: '', - maxTouchPoints: null, - hardwareConcurrency: null, - )); + when(mockDeviceInfoPlugin.webBrowserInfo).thenAnswer( + (_) async => WebBrowserInfo( + userAgent: webUserAgent, + appCodeName: '', + appName: '', + appVersion: '', + deviceMemory: null, + language: '', + languages: [], + platform: '', + product: '', + productSub: '', + vendor: '', + vendorSub: '', + maxTouchPoints: null, + hardwareConcurrency: null, + ), + ); final userAgent = await applicationManager.getUserAgent(); expect(userAgent, webUserAgent); - - PlatformInfo.isTestingForWeb = false; }); - test('WHEN platform is Android THEN getUserAgent should be return user agent for Android', () async { + test( + 'WHEN platform is Android THEN getUserAgent should return user agent for Android', + () async { debugDefaultTargetPlatformOverride = TargetPlatform.android; const androidUserAgent = 'User-Agent-Twake-Mail-Android'; - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler( + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler( const MethodChannel('fk_user_agent'), (message) async { if (message.method == 'getProperties') { - return { - 'userAgent': androidUserAgent - }; + return {'userAgent': androidUserAgent}; } return null; - } + }, ); await FkUserAgent.init(); @@ -77,42 +93,35 @@ void main() { final userAgent = await applicationManager.getUserAgent(); expect(userAgent, androidUserAgent); - - debugDefaultTargetPlatformOverride = null; - FkUserAgent.release(); - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler( - const MethodChannel('fk_user_agent'), - null); }); - test('WHEN platform is Web\n' - 'AND mockDeviceInfoPlugin.webBrowserInfo throw exception\n' - 'THEN getUserAgent should be return empty string', - () async { + test( + 'WHEN platform is Web AND mockDeviceInfoPlugin.webBrowserInfo throws exception THEN return empty string', + () async { PlatformInfo.isTestingForWeb = true; - when(mockDeviceInfoPlugin.webBrowserInfo).thenThrow(Exception('Failed to get web browser info')); + when(mockDeviceInfoPlugin.webBrowserInfo) + .thenThrow(Exception('Failed to get web browser info')); final userAgent = await applicationManager.getUserAgent(); expect(userAgent, ''); - - PlatformInfo.isTestingForWeb = false; }); - test('WHEN platform is Android\n' - 'AND FkUserAgent.userAgent return empty string\n' - 'THEN getUserAgent should be return empty string', - () async { + test( + 'WHEN platform is Android AND FkUserAgent.userAgent empty THEN return empty string', + () async { debugDefaultTargetPlatformOverride = TargetPlatform.android; - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler( + + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler( const MethodChannel('fk_user_agent'), (message) async { if (message.method == 'getProperties') { return {}; } return null; - } + }, ); await FkUserAgent.init(); @@ -120,12 +129,6 @@ void main() { final userAgent = await applicationManager.getUserAgent(); expect(userAgent, ''); - - debugDefaultTargetPlatformOverride = null; - FkUserAgent.release(); - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler( - const MethodChannel('fk_user_agent'), - null); }); }); } diff --git a/email_recovery/pubspec.lock b/email_recovery/pubspec.lock index 5337408e8..482e7ec42 100644 --- a/email_recovery/pubspec.lock +++ b/email_recovery/pubspec.lock @@ -181,10 +181,10 @@ packages: dependency: "direct main" description: name: dio - sha256: "9fdbf71baeb250fc9da847f6cb2052196f62c19906a3657adfc18631a667d316" + sha256: e44ce32c01f02c54a101551def8586f15d6189b4f999d4420aad38995ac62e6d url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "5.2.0" equatable: dependency: "direct main" description: @@ -296,7 +296,7 @@ packages: description: path: "." ref: main - resolved-ref: "446efbbccdde981371b4d2a636762184adf6e926" + resolved-ref: "8e2bd9cb1bbd8236be895aa04bf0d0da8e2ff3b2" url: "https://github.com/linagora/jmap-dart-client.git" source: git version: "0.3.6" diff --git a/email_recovery/pubspec.yaml b/email_recovery/pubspec.yaml index 4755dffa3..b969fbcb5 100644 --- a/email_recovery/pubspec.yaml +++ b/email_recovery/pubspec.yaml @@ -22,7 +22,7 @@ dependencies: json_annotation: 4.8.0 - dio: 5.0.0 + dio: 5.2.0 http_mock_adapter: 0.4.2 diff --git a/env.file b/env.file index 9c6b135b6..e31c00b9f 100644 --- a/env.file +++ b/env.file @@ -9,4 +9,7 @@ FORWARD_WARNING_MESSAGE= PLATFORM=other WS_ECHO_PING= COZY_INTEGRATION= -COZY_EXTERNAL_BRIDGE_VERSION= \ No newline at end of file +COZY_EXTERNAL_BRIDGE_VERSION= +SENTRY_ENABLED=false +SENTRY_DSN= +SENTRY_ENVIRONMENT= \ No newline at end of file diff --git a/fcm/pubspec.lock b/fcm/pubspec.lock index 073e3e139..43c357c03 100644 --- a/fcm/pubspec.lock +++ b/fcm/pubspec.lock @@ -181,10 +181,10 @@ packages: dependency: "direct main" description: name: dio - sha256: "9fdbf71baeb250fc9da847f6cb2052196f62c19906a3657adfc18631a667d316" + sha256: e44ce32c01f02c54a101551def8586f15d6189b4f999d4420aad38995ac62e6d url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "5.2.0" equatable: dependency: "direct main" description: @@ -296,7 +296,7 @@ packages: description: path: "." ref: main - resolved-ref: "446efbbccdde981371b4d2a636762184adf6e926" + resolved-ref: "8e2bd9cb1bbd8236be895aa04bf0d0da8e2ff3b2" url: "https://github.com/linagora/jmap-dart-client.git" source: git version: "0.3.6" diff --git a/fcm/pubspec.yaml b/fcm/pubspec.yaml index e90697be6..76bf3f169 100644 --- a/fcm/pubspec.yaml +++ b/fcm/pubspec.yaml @@ -22,7 +22,7 @@ dependencies: json_annotation: 4.8.0 - dio: 5.0.0 + dio: 5.2.0 http_mock_adapter: 0.4.2 diff --git a/forward/pubspec.lock b/forward/pubspec.lock index 073e3e139..43c357c03 100644 --- a/forward/pubspec.lock +++ b/forward/pubspec.lock @@ -181,10 +181,10 @@ packages: dependency: "direct main" description: name: dio - sha256: "9fdbf71baeb250fc9da847f6cb2052196f62c19906a3657adfc18631a667d316" + sha256: e44ce32c01f02c54a101551def8586f15d6189b4f999d4420aad38995ac62e6d url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "5.2.0" equatable: dependency: "direct main" description: @@ -296,7 +296,7 @@ packages: description: path: "." ref: main - resolved-ref: "446efbbccdde981371b4d2a636762184adf6e926" + resolved-ref: "8e2bd9cb1bbd8236be895aa04bf0d0da8e2ff3b2" url: "https://github.com/linagora/jmap-dart-client.git" source: git version: "0.3.6" diff --git a/forward/pubspec.yaml b/forward/pubspec.yaml index 81bfdce8c..086cbd686 100644 --- a/forward/pubspec.yaml +++ b/forward/pubspec.yaml @@ -22,7 +22,7 @@ dependencies: json_annotation: 4.8.0 - dio: 5.0.0 + dio: 5.2.0 http_mock_adapter: 0.4.2 diff --git a/integration_test/base/test_base.dart b/integration_test/base/test_base.dart index 45a63ac21..2f49c0fdc 100644 --- a/integration_test/base/test_base.dart +++ b/integration_test/base/test_base.dart @@ -1,7 +1,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:patrol/patrol.dart'; -import 'package:tmail_ui_user/main.dart' as app; +import 'package:tmail_ui_user/main/main_entry.dart'; import 'base_scenario.dart'; @@ -34,7 +34,7 @@ class TestBase { } Future setupTest() async { - await app.runTmail(); + await runTmail(); final originalOnError = FlutterError.onError!; FlutterError.onError = (FlutterErrorDetails details) { diff --git a/lib/features/base/base_controller.dart b/lib/features/base/base_controller.dart index 843cd1ca9..58ba7acfe 100644 --- a/lib/features/base/base_controller.dart +++ b/lib/features/base/base_controller.dart @@ -81,7 +81,6 @@ abstract class BaseController extends GetxController final ImagePaths imagePaths = Get.find(); final ResponsiveUtils responsiveUtils = Get.find(); final Uuid uuid = Get.find(); - final ApplicationManager applicationManager = Get.find(); final ToastManager toastManager = Get.find(); final TwakeAppManager twakeAppManager = Get.find(); @@ -554,6 +553,7 @@ abstract class BaseController extends GetxController Future clearDataAndGoToLoginPage() async { log('$runtimeType::clearDataAndGoToLoginPage:'); + SentryManager.instance.clearUser(); await clearAllData(); removeAllPageAndGoToLogin(); } diff --git a/lib/features/login/data/network/interceptors/authorization_interceptors.dart b/lib/features/login/data/network/interceptors/authorization_interceptors.dart index 67ce31b1d..28c6ee913 100644 --- a/lib/features/login/data/network/interceptors/authorization_interceptors.dart +++ b/lib/features/login/data/network/interceptors/authorization_interceptors.dart @@ -82,7 +82,7 @@ class AuthorizationInterceptors extends QueuedInterceptorsWrapper { } @override - void onError(DioError err, ErrorInterceptorHandler handler) async { + void onError(DioException err, ErrorInterceptorHandler handler) async { logWarning('AuthorizationInterceptors::onError(): DIO_ERROR = $err'); try { final requestOptions = err.requestOptions; @@ -159,7 +159,7 @@ class AuthorizationInterceptors extends QueuedInterceptorsWrapper { ); if (e is ServerError || e is TemporarilyUnavailable) { return super.onError( - DioError(requestOptions: err.requestOptions, error: e), + DioException(requestOptions: err.requestOptions, error: e), handler, ); } else { diff --git a/lib/features/login/data/network/oidc_http_client.dart b/lib/features/login/data/network/oidc_http_client.dart index bf0064f85..43b3b7370 100644 --- a/lib/features/login/data/network/oidc_http_client.dart +++ b/lib/features/login/data/network/oidc_http_client.dart @@ -5,6 +5,7 @@ import 'dart:io'; import 'package:core/data/model/query/query_parameter.dart'; import 'package:core/data/network/dio_client.dart'; import 'package:core/utils/app_logger.dart'; +import 'package:dio/dio.dart' show DioException; import 'package:get/get_connect/http/src/exceptions/exceptions.dart'; import 'package:model/oidc/oidc_configuration.dart'; import 'package:model/oidc/request/oidc_request.dart'; @@ -14,10 +15,9 @@ import 'package:model/oidc/response/oidc_user_info.dart'; import 'package:tmail_ui_user/features/login/data/extensions/service_path_extension.dart'; import 'package:tmail_ui_user/features/login/data/network/config/oidc_constant.dart'; import 'package:tmail_ui_user/features/login/data/network/endpoint.dart'; -import 'package:tmail_ui_user/features/login/domain/exceptions/login_exception.dart'; import 'package:tmail_ui_user/features/login/data/network/oidc_error.dart'; +import 'package:tmail_ui_user/features/login/domain/exceptions/login_exception.dart'; import 'package:tmail_ui_user/main/utils/app_config.dart'; -import 'package:dio/dio.dart' show DioError; class OIDCHttpClient { @@ -45,7 +45,7 @@ class OIDCHttpClient { } on FormatException catch (exception) { log('checkOIDCIsAvailable(): error while parsing server response (JSON expected): ${exception.message}'); throw InvalidOIDCResponseException(); - } on DioError catch (exception) { + } on DioException catch (exception) { if (exception.error is HandshakeException) { throw exception.error!; } else if (exception.error is SocketException) { diff --git a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart index 82ddb7271..838f50b73 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -29,6 +29,7 @@ import 'package:jmap_dart_client/jmap/quotas/quota.dart'; import 'package:model/model.dart'; import 'package:receive_sharing_intent/receive_sharing_intent.dart'; import 'package:rxdart/transformers.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:server_settings/server_settings/tmail_server_settings_extension.dart'; import 'package:tmail_ui_user/features/base/action/ui_action.dart'; import 'package:tmail_ui_user/features/base/mixin/ai_scribe_mixin.dart'; @@ -389,7 +390,7 @@ class MailboxDashBoardController extends ReloadableController _registerStreamListener(); BackButtonInterceptor.add(onBackButtonInterceptor, name: AppRoutes.dashboard); WidgetsBinding.instance.addPostFrameCallback((_) async { - await applicationManager.initUserAgent(); + await ApplicationManager().initUserAgent(); }); super.onInit(); } @@ -868,6 +869,15 @@ class MailboxDashBoardController extends ReloadableController accountId.value = currentAccountId; synchronizeOwnEmailAddress(session.getOwnEmailAddressOrEmpty()); + SentryManager.instance.setUser( + SentryUser( + id: currentAccountId.asString, + name: session.getUserDisplayName(), + username: session.username.value, + email: session.getOwnEmailAddressOrEmpty(), + ) + ); + _setUpMinInputLengthAutocomplete(); injectAutoCompleteBindings(session, currentAccountId); injectRuleFilterBindings(session, currentAccountId); @@ -3403,7 +3413,7 @@ class MailboxDashBoardController extends ReloadableController _refreshActionEventController.close(); _notificationManager.closeStream(); _fcmService.closeStream(); - applicationManager.releaseUserAgent(); + ApplicationManager().releaseUserAgent(); BackButtonInterceptor.removeByName(AppRoutes.dashboard); _identities = null; outboxMailbox = null; diff --git a/lib/features/manage_account/presentation/manage_account_dashboard_controller.dart b/lib/features/manage_account/presentation/manage_account_dashboard_controller.dart index 763877225..751c97a4d 100644 --- a/lib/features/manage_account/presentation/manage_account_dashboard_controller.dart +++ b/lib/features/manage_account/presentation/manage_account_dashboard_controller.dart @@ -13,6 +13,7 @@ import 'package:jmap_dart_client/jmap/quotas/quota.dart'; import 'package:model/model.dart'; import 'package:rule_filter/rule_filter/capability_rule_filter.dart'; import 'package:scribe/scribe/ai/presentation/utils/ai_scribe_constants.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:server_settings/server_settings/capability_server_settings.dart'; import 'package:tmail_ui_user/features/base/action/ui_action.dart'; import 'package:tmail_ui_user/features/base/mixin/ai_scribe_mixin.dart'; @@ -151,6 +152,18 @@ class ManageAccountDashBoardController extends ReloadableController sessionCurrent = session; accountId.value = session?.accountId; synchronizeOwnEmailAddress(session?.getOwnEmailAddressOrEmpty() ?? ''); + + if (session != null) { + SentryManager.instance.setUser( + SentryUser( + id: session.accountId.asString, + name: session.getUserDisplayName(), + username: session.username.value, + email: session.getOwnEmailAddressOrEmpty(), + ), + ); + } + _setUpMinInputLengthAutocomplete(); _bindingInteractorForMenuItemView(sessionCurrent, accountId.value); _getVacationResponse(); diff --git a/lib/features/upload/data/network/file_uploader.dart b/lib/features/upload/data/network/file_uploader.dart index 25039e796..5492708f3 100644 --- a/lib/features/upload/data/network/file_uploader.dart +++ b/lib/features/upload/data/network/file_uploader.dart @@ -147,8 +147,8 @@ class FileUploader { resultJson: resultJson, fileName: argsUpload.fileInfo.fileName); } - } on DioError catch (exception) { - logWarning('FileUploader::_handleUploadAttachmentAction():DioError: $exception'); + } on DioException catch (exception) { + logWarning('FileUploader::_handleUploadAttachmentAction():DioException: $exception'); throw exception.copyWith( requestOptions: exception.requestOptions.copyWith(data: '')); diff --git a/lib/features/upload/domain/model/upload_attachment.dart b/lib/features/upload/domain/model/upload_attachment.dart index 8fe86f506..800b79227 100644 --- a/lib/features/upload/domain/model/upload_attachment.dart +++ b/lib/features/upload/domain/model/upload_attachment.dart @@ -62,7 +62,7 @@ class UploadAttachment with EquatableMixin { _updateEvent(Right(SuccessAttachmentUploadState(uploadTaskId, attachment, fileInfo))); } catch (error, stackTrace) { logWarning('UploadAttachment::upload():ERROR: $error'); - if (error is DioError && error.type == DioErrorType.cancel) { + if (error is DioException && error.type == DioExceptionType.cancel) { _updateEvent(Left(CancelAttachmentUploadState(uploadTaskId))); } else { try { diff --git a/lib/main.dart b/lib/main.dart index 5cebea408..b5f4ff9dc 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,51 +1,22 @@ import 'package:core/presentation/utils/theme_utils.dart'; -import 'package:core/utils/app_logger.dart'; -import 'package:core/utils/build_utils.dart'; import 'package:core/utils/platform_info.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; -import 'package:tmail_ui_user/features/caching/config/hive_cache_config.dart'; -import 'package:tmail_ui_user/main/bindings/main_bindings.dart'; import 'package:tmail_ui_user/main/deep_links/deep_links_manager.dart'; import 'package:scribe/scribe/ai/localizations/scribe_localizations.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations_delegate.dart'; import 'package:tmail_ui_user/main/localizations/localization_service.dart'; +import 'package:tmail_ui_user/main/main_entry.dart'; +import 'package:tmail_ui_user/main/app_runner.dart'; import 'package:tmail_ui_user/main/pages/app_pages.dart'; import 'package:tmail_ui_user/main/routes/app_routes.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; -import 'package:tmail_ui_user/main/utils/app_utils.dart'; -import 'package:tmail_ui_user/main/utils/asset_preloader.dart'; -import 'package:tmail_ui_user/main/utils/cozy_integration.dart'; -import 'package:url_strategy/url_strategy.dart'; -import 'package:worker_manager/worker_manager.dart'; Future main() async { - initLogger(() async { - WidgetsFlutterBinding.ensureInitialized(); - await runTmail(); - }); -} - -Future runTmail() async { - ThemeUtils.setSystemLightUIStyle(); - - await Future.wait([ - MainBindings().dependencies(), - HiveCacheConfig.instance.setUp(), - Executor().warmUp(log: BuildUtils.isDebugMode), - AppUtils.loadEnvFile(), - if (PlatformInfo.isWeb) - AssetPreloader.preloadHtmlEditorAssets(), - ]); - await CozyIntegration.integrateCozy(); - await HiveCacheConfig.instance.initializeEncryptionKey(); - - setPathUrlStrategy(); - - runApp(const TMailApp()); + await runAppWithMonitoring(runTmail); } class TMailApp extends StatefulWidget { diff --git a/lib/main/app_runner.dart b/lib/main/app_runner.dart new file mode 100644 index 000000000..02b6b88f0 --- /dev/null +++ b/lib/main/app_runner.dart @@ -0,0 +1,49 @@ +import 'dart:async'; + +import 'package:core/utils/app_logger.dart'; +import 'package:core/utils/sentry/sentry_manager.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; +import 'package:tmail_ui_user/main.dart'; +import 'package:tmail_ui_user/main/main_entry.dart'; + +Future runAppWithMonitoring(Future Function() runTmail) async { + await runZonedGuarded(() async { + WidgetsFlutterBinding.ensureInitialized(); + + // Handling Flutter UI and Build Errors + FlutterError.onError = (details) async { + logError( + 'FlutterError: ${details.exception}', + exception: details.exception, + stackTrace: details.stack, + ); + FlutterError.presentError(details); + }; + + // Handling Uncaught and Platform-Specific Errors + PlatformDispatcher.instance.onError = (error, stack) { + logError( + 'PlatformDispatcher: Error: $error', + exception: error, + stackTrace: stack, + ); + return true; + }; + + await SentryManager.instance.initialize( + appRunner: () async { + await runTmailPreload(); + runApp(SentryWidget(child: const TMailApp())); + }, + fallBackRunner: runTmail, + ); + }, (error, stack) async { + logError( + 'Uncaught zone error: $error', + exception: error, + stackTrace: stack, + ); + }); +} diff --git a/lib/main/bindings/core/core_bindings.dart b/lib/main/bindings/core/core_bindings.dart index b0cab9cbe..7523caaf3 100644 --- a/lib/main/bindings/core/core_bindings.dart +++ b/lib/main/bindings/core/core_bindings.dart @@ -3,7 +3,6 @@ import 'package:core/data/utils/device_manager.dart'; import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/presentation/utils/app_toast.dart'; import 'package:core/presentation/utils/responsive_utils.dart'; -import 'package:core/utils/application_manager.dart'; import 'package:core/utils/config/app_config_loader.dart'; import 'package:core/utils/file_utils.dart'; import 'package:core/utils/platform_info.dart'; @@ -73,7 +72,6 @@ class CoreBindings extends Bindings { Get.put(AppConfigLoader()); Get.put(FileUtils()); Get.put(PrintUtils()); - Get.put(ApplicationManager(Get.find())); Get.put(BeforeReconnectManager()); if (PlatformInfo.isIOS) { Get.put(IOSNotificationManager()); diff --git a/lib/main/bindings/network/network_bindings.dart b/lib/main/bindings/network/network_bindings.dart index 6c36a6cfe..615f9bf4b 100644 --- a/lib/main/bindings/network/network_bindings.dart +++ b/lib/main/bindings/network/network_bindings.dart @@ -5,10 +5,10 @@ import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:core/core.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:dio/dio.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter_appauth/flutter_appauth.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/http/http_client.dart'; +import 'package:sentry_dio/sentry_dio.dart'; import 'package:tmail_ui_user/features/contact/data/network/contact_api.dart'; import 'package:tmail_ui_user/features/email/data/network/email_api.dart'; import 'package:tmail_ui_user/features/email/data/network/mdn_api.dart'; @@ -88,18 +88,22 @@ class NetworkBindings extends Bindings { } void _bindingInterceptors() { + final dio = Get.find(); Get.put(DynamicUrlInterceptors()); Get.put(AuthorizationInterceptors( - Get.find(), + dio, Get.find(), Get.find(), Get.find(), Get.find(), )); - Get.find().interceptors.add(Get.find()); - Get.find().interceptors.add(Get.find()); - if (kDebugMode) { - Get.find().interceptors.add(LogInterceptor(requestBody: true)); + dio.interceptors.add(Get.find()); + dio.interceptors.add(Get.find()); + if (BuildUtils.isDebugMode) { + dio.interceptors.add(LogInterceptor(requestBody: true)); + } + if (SentryManager.instance.isSentryAvailable) { + dio.addSentry(); } } diff --git a/lib/main/bindings/network/network_isolate_binding.dart b/lib/main/bindings/network/network_isolate_binding.dart index d80686c43..a443483d8 100644 --- a/lib/main/bindings/network/network_isolate_binding.dart +++ b/lib/main/bindings/network/network_isolate_binding.dart @@ -4,6 +4,7 @@ import 'package:dio/dio.dart'; import 'package:flutter_appauth/flutter_appauth.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/http/http_client.dart'; +import 'package:sentry_dio/sentry_dio.dart'; import 'package:tmail_ui_user/features/email/data/local/html_analyzer.dart'; import 'package:tmail_ui_user/features/email/data/network/email_api.dart'; import 'package:tmail_ui_user/features/login/data/local/account_cache_manager.dart'; @@ -51,20 +52,23 @@ class NetworkIsolateBindings extends Bindings { } void _bindingInterceptors() { - Get.put(AuthorizationInterceptors( - Get.find(tag: BindingTag.isolateTag), + final dio = Get.find(tag: BindingTag.isolateTag); + final authorizationInterceptors = AuthorizationInterceptors( + dio, Get.find(tag: BindingTag.isolateTag), Get.find(tag: BindingTag.isolateTag), Get.find(tag: BindingTag.isolateTag), Get.find(tag: BindingTag.isolateTag), - ), tag: BindingTag.isolateTag); - Get.find(tag: BindingTag.isolateTag).interceptors - .add(Get.find()); - Get.find(tag: BindingTag.isolateTag).interceptors - .add(Get.find(tag: BindingTag.isolateTag)); + ); + Get.put(authorizationInterceptors, tag: BindingTag.isolateTag); + + dio.interceptors.add(Get.find()); + dio.interceptors.add(authorizationInterceptors); if (BuildUtils.isDebugMode) { - Get.find(tag: BindingTag.isolateTag).interceptors - .add(LogInterceptor(requestBody: true)); + dio.interceptors.add(LogInterceptor(requestBody: true)); + } + if (SentryManager.instance.isSentryAvailable) { + dio.addSentry(); } } diff --git a/lib/main/exceptions/remote_exception_thrower.dart b/lib/main/exceptions/remote_exception_thrower.dart index 39aa62d7c..57de91129 100644 --- a/lib/main/exceptions/remote_exception_thrower.dart +++ b/lib/main/exceptions/remote_exception_thrower.dart @@ -32,7 +32,7 @@ class RemoteExceptionThrower extends ExceptionThrower { } void handleDioError(dynamic error) { - if (error is DioError) { + if (error is DioException) { logWarning( 'RemoteExceptionThrower::throwException():type: ${error.type} | response: ${error.response} | error: ${error.error}', ); @@ -74,13 +74,13 @@ class RemoteExceptionThrower extends ExceptionThrower { throw error; } - void _handleDioErrorWithoutResponse(DioError error) { + void _handleDioErrorWithoutResponse(DioException error) { switch (error.type) { - case DioErrorType.connectionTimeout: + case DioExceptionType.connectionTimeout: throw ConnectionTimeout(message: error.message); - case DioErrorType.connectionError: + case DioExceptionType.connectionError: throw ConnectionError(message: error.message); - case DioErrorType.badResponse: + case DioExceptionType.badResponse: throw const BadCredentialsException(); default: final underlyingError = error.error; diff --git a/lib/main/main_entry.dart b/lib/main/main_entry.dart new file mode 100644 index 000000000..f2c31ac4e --- /dev/null +++ b/lib/main/main_entry.dart @@ -0,0 +1,34 @@ +import 'package:core/presentation/utils/theme_utils.dart'; +import 'package:core/utils/build_utils.dart'; +import 'package:core/utils/config/env_loader.dart'; +import 'package:core/utils/platform_info.dart'; +import 'package:flutter/widgets.dart'; +import 'package:tmail_ui_user/features/caching/config/hive_cache_config.dart'; +import 'package:tmail_ui_user/main.dart'; +import 'package:tmail_ui_user/main/bindings/main_bindings.dart'; +import 'package:tmail_ui_user/main/utils/asset_preloader.dart'; +import 'package:tmail_ui_user/main/utils/cozy_integration.dart'; +import 'package:url_strategy/url_strategy.dart'; +import 'package:worker_manager/worker_manager.dart'; + +Future runTmail() async { + await runTmailPreload(); + runApp(const TMailApp()); +} + +Future runTmailPreload() async { + ThemeUtils.setSystemLightUIStyle(); + + await Future.wait([ + MainBindings().dependencies(), + HiveCacheConfig.instance.setUp(), + Executor().warmUp(log: BuildUtils.isDebugMode), + EnvLoader.loadEnvFile(), + if (PlatformInfo.isWeb) AssetPreloader.preloadHtmlEditorAssets(), + ]); + + await CozyIntegration.integrateCozy(); + await HiveCacheConfig.instance.initializeEncryptionKey(); + + setPathUrlStrategy(); +} diff --git a/lib/main/utils/app_config.dart b/lib/main/utils/app_config.dart index f22cd5a35..98d0d6b5f 100644 --- a/lib/main/utils/app_config.dart +++ b/lib/main/utils/app_config.dart @@ -11,9 +11,7 @@ class AppConfig { static const int warningAttachmentFileSizeInMegabytes = 10; static const int defaultLimitAutocomplete = 8; - static const String envFileName = 'env.file'; static const String appDashboardConfigurationPath = "configurations/app_dashboard.json"; - static const String appFCMConfigurationPath = "configurations/env.fcm"; static const String iOSKeychainSharingGroupId = 'KUT463DS29.com.linagora.ios.teammail.shared'; static const String iOSKeychainSharingService = 'com.linagora.ios.teammail.sessions'; static const String saasPlatform = 'saas'; diff --git a/lib/main/utils/app_utils.dart b/lib/main/utils/app_utils.dart index 925dbea61..196f17ac2 100644 --- a/lib/main/utils/app_utils.dart +++ b/lib/main/utils/app_utils.dart @@ -2,36 +2,16 @@ import 'package:core/core.dart'; import 'package:date_format/date_format.dart' as date_format; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:get/get.dart'; import 'package:intl/intl.dart' as intl; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/localizations/language_code_constants.dart'; -import 'package:tmail_ui_user/main/utils/app_config.dart'; -import 'package:url_launcher/url_launcher.dart'; import 'package:tmail_ui_user/main/universal_import/html_stub.dart' as html; +import 'package:url_launcher/url_launcher.dart'; class AppUtils { const AppUtils._(); - static Future loadEnvFile() async { - await dotenv.load(fileName: AppConfig.envFileName); - final mapEnvData = Map.from(dotenv.env); - try { - await loadFcmConfigFileToEnv(currentMapEnvData: mapEnvData); - } catch (e) { - logWarning('AppUtils::loadEnvFile:loadFcmConfigFileToEnv: Exception = $e'); - await dotenv.load(fileName: AppConfig.envFileName); - } - } - - static Future loadFcmConfigFileToEnv({Map? currentMapEnvData}) { - return dotenv.load( - fileName: AppConfig.appFCMConfigurationPath, - mergeWith: currentMapEnvData ?? {} - ); - } - static void launchLink(String url, {bool isNewTab = true}) { log('AppUtils::launchLink: url = $url'); if (PlatformInfo.isWeb && HtmlUtils.isSafariBelow17()) { diff --git a/lib/main/utils/toast_manager.dart b/lib/main/utils/toast_manager.dart index 29869f82f..1e5a68987 100644 --- a/lib/main/utils/toast_manager.dart +++ b/lib/main/utils/toast_manager.dart @@ -158,7 +158,7 @@ class ToastManager { exception, useDefaultMessage: true, ); - } else if (_isEmptySpamFolderFailure(exception)) { + } else if (_isEmptySpamFolderFailure(failure)) { message = message ?? appLocalizations.emptySpamFolderFailed; } else if (_isEmptyTrashFolderFailure(failure)) { message = message ?? appLocalizations.emptyTrashFolderFailed; diff --git a/model/pubspec.lock b/model/pubspec.lock index 76f564fe3..fa3fab792 100644 --- a/model/pubspec.lock +++ b/model/pubspec.lock @@ -29,10 +29,10 @@ packages: dependency: transitive description: name: args - sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.7.0" async: dependency: transitive description: @@ -268,10 +268,10 @@ packages: dependency: transitive description: name: dio - sha256: "9fdbf71baeb250fc9da847f6cb2052196f62c19906a3657adfc18631a667d316" + sha256: e44ce32c01f02c54a101551def8586f15d6189b4f999d4420aad38995ac62e6d url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "5.2.0" equatable: dependency: "direct main" description: @@ -381,6 +381,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.2" + flutter_dotenv: + dependency: transitive + description: + name: flutter_dotenv + sha256: d9283d92059a22e9834bc0a31336658ffba77089fb6f3cc36751f1fc7c6661a3 + url: "https://pub.dev" + source: hosted + version: "5.0.2" flutter_image_compress: dependency: transitive description: @@ -668,10 +676,18 @@ packages: description: path: "." ref: main - resolved-ref: "446efbbccdde981371b4d2a636762184adf6e926" + resolved-ref: "8e2bd9cb1bbd8236be895aa04bf0d0da8e2ff3b2" url: "https://github.com/linagora/jmap-dart-client.git" source: git version: "0.3.6" + jni: + dependency: transitive + description: + name: jni + sha256: d2c361082d554d4593c3012e26f6b188f902acd291330f13d6427641a92b3da1 + url: "https://pub.dev" + source: hosted + version: "0.14.2" js: dependency: transitive description: @@ -776,6 +792,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + objective_c: + dependency: transitive + description: + name: objective_c + sha256: "64e35e1e2e79da4e83f2ace3bf4e5437cef523f46c7db2eba9a1419c49573790" + url: "https://pub.dev" + source: hosted + version: "8.0.0" package_config: dependency: transitive description: @@ -1001,6 +1025,22 @@ packages: url: "https://github.com/linagora/dart-neats.git" source: git version: "3.0.1" + sentry: + dependency: transitive + description: + name: sentry + sha256: "10a0bc25f5f21468e3beeae44e561825aaa02cdc6829438e73b9b64658ff88d9" + url: "https://pub.dev" + source: hosted + version: "9.8.0" + sentry_flutter: + dependency: transitive + description: + name: sentry_flutter + sha256: aafbf41c63c98a30b17bdbf3313424d5102db62b08735c44bff810f277e786a5 + url: "https://pub.dev" + source: hosted + version: "9.8.0" shelf: dependency: transitive description: @@ -1206,6 +1246,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.0" + uuid: + dependency: transitive + description: + name: uuid + sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8 + url: "https://pub.dev" + source: hosted + version: "4.5.2" vector_graphics: dependency: transitive description: diff --git a/pubspec.lock b/pubspec.lock index 638c4881c..639c6efa9 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -451,10 +451,10 @@ packages: dependency: "direct main" description: name: dio - sha256: "9fdbf71baeb250fc9da847f6cb2052196f62c19906a3657adfc18631a667d316" + sha256: e44ce32c01f02c54a101551def8586f15d6189b4f999d4420aad38995ac62e6d url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "5.2.0" dispose_scope: dependency: transitive description: @@ -1363,10 +1363,18 @@ packages: description: path: "." ref: main - resolved-ref: "446efbbccdde981371b4d2a636762184adf6e926" + resolved-ref: "8e2bd9cb1bbd8236be895aa04bf0d0da8e2ff3b2" url: "https://github.com/linagora/jmap-dart-client.git" source: git version: "0.3.6" + jni: + dependency: transitive + description: + name: jni + sha256: d2c361082d554d4593c3012e26f6b188f902acd291330f13d6427641a92b3da1 + url: "https://pub.dev" + source: hosted + version: "0.14.2" js: dependency: transitive description: @@ -1527,6 +1535,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + objective_c: + dependency: transitive + description: + name: objective_c + sha256: "64e35e1e2e79da4e83f2ace3bf4e5437cef523f46c7db2eba9a1419c49573790" + url: "https://pub.dev" + source: hosted + version: "8.0.0" open_file: dependency: "direct main" description: @@ -2008,6 +2024,30 @@ packages: relative: true source: path version: "0.0.1" + sentry: + dependency: transitive + description: + name: sentry + sha256: "10a0bc25f5f21468e3beeae44e561825aaa02cdc6829438e73b9b64658ff88d9" + url: "https://pub.dev" + source: hosted + version: "9.8.0" + sentry_dio: + dependency: "direct main" + description: + name: sentry_dio + sha256: bee438fd790c534da77f0a6c9cd04c54c818184b8a54bbe7d916489c8aad56a0 + url: "https://pub.dev" + source: hosted + version: "9.8.0" + sentry_flutter: + dependency: "direct main" + description: + name: sentry_flutter + sha256: aafbf41c63c98a30b17bdbf3313424d5102db62b08735c44bff810f277e786a5 + url: "https://pub.dev" + source: hosted + version: "9.8.0" server_settings: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 454db8890..1b1ebc270 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -118,7 +118,7 @@ dependencies: flutter_svg: 2.1.0 - dio: 5.0.0 + dio: 5.2.0 equatable: 2.0.5 @@ -265,6 +265,10 @@ dependencies: flutter_custom_tab_bar: 1.2.1 + sentry_flutter: 9.8.0 + + sentry_dio: 9.8.0 + dev_dependencies: flutter_test: sdk: flutter diff --git a/rule_filter/pubspec.lock b/rule_filter/pubspec.lock index 073e3e139..43c357c03 100644 --- a/rule_filter/pubspec.lock +++ b/rule_filter/pubspec.lock @@ -181,10 +181,10 @@ packages: dependency: "direct main" description: name: dio - sha256: "9fdbf71baeb250fc9da847f6cb2052196f62c19906a3657adfc18631a667d316" + sha256: e44ce32c01f02c54a101551def8586f15d6189b4f999d4420aad38995ac62e6d url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "5.2.0" equatable: dependency: "direct main" description: @@ -296,7 +296,7 @@ packages: description: path: "." ref: main - resolved-ref: "446efbbccdde981371b4d2a636762184adf6e926" + resolved-ref: "8e2bd9cb1bbd8236be895aa04bf0d0da8e2ff3b2" url: "https://github.com/linagora/jmap-dart-client.git" source: git version: "0.3.6" diff --git a/rule_filter/pubspec.yaml b/rule_filter/pubspec.yaml index 500e6edc5..21b393372 100644 --- a/rule_filter/pubspec.yaml +++ b/rule_filter/pubspec.yaml @@ -22,7 +22,7 @@ dependencies: json_annotation: 4.8.0 - dio: 5.0.0 + dio: 5.2.0 http_mock_adapter: 0.4.2 diff --git a/scribe/pubspec.lock b/scribe/pubspec.lock index b7640a88d..e70428d0f 100644 --- a/scribe/pubspec.lock +++ b/scribe/pubspec.lock @@ -268,10 +268,10 @@ packages: dependency: "direct main" description: name: dio - sha256: "9fdbf71baeb250fc9da847f6cb2052196f62c19906a3657adfc18631a667d316" + sha256: e44ce32c01f02c54a101551def8586f15d6189b4f999d4420aad38995ac62e6d url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "5.2.0" equatable: dependency: transitive description: @@ -381,6 +381,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + flutter_dotenv: + dependency: transitive + description: + name: flutter_dotenv + sha256: d9283d92059a22e9834bc0a31336658ffba77089fb6f3cc36751f1fc7c6661a3 + url: "https://pub.dev" + source: hosted + version: "5.0.2" flutter_image_compress: dependency: transitive description: @@ -674,10 +682,18 @@ packages: description: path: "." ref: main - resolved-ref: "446efbbccdde981371b4d2a636762184adf6e926" + resolved-ref: "8e2bd9cb1bbd8236be895aa04bf0d0da8e2ff3b2" url: "https://github.com/linagora/jmap-dart-client.git" source: git version: "0.3.6" + jni: + dependency: transitive + description: + name: jni + sha256: d2c361082d554d4593c3012e26f6b188f902acd291330f13d6427641a92b3da1 + url: "https://pub.dev" + source: hosted + version: "0.14.2" js: dependency: transitive description: @@ -774,6 +790,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.6" + objective_c: + dependency: transitive + description: + name: objective_c + sha256: "64e35e1e2e79da4e83f2ace3bf4e5437cef523f46c7db2eba9a1419c49573790" + url: "https://pub.dev" + source: hosted + version: "8.0.0" package_config: dependency: transitive description: @@ -1007,6 +1031,22 @@ packages: url: "https://github.com/linagora/dart-neats.git" source: git version: "3.0.1" + sentry: + dependency: transitive + description: + name: sentry + sha256: "10a0bc25f5f21468e3beeae44e561825aaa02cdc6829438e73b9b64658ff88d9" + url: "https://pub.dev" + source: hosted + version: "9.8.0" + sentry_flutter: + dependency: transitive + description: + name: sentry_flutter + sha256: aafbf41c63c98a30b17bdbf3313424d5102db62b08735c44bff810f277e786a5 + url: "https://pub.dev" + source: hosted + version: "9.8.0" shelf: dependency: transitive description: @@ -1204,6 +1244,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.5" + uuid: + dependency: transitive + description: + name: uuid + sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8 + url: "https://pub.dev" + source: hosted + version: "4.5.2" vector_graphics: dependency: transitive description: diff --git a/scribe/pubspec.yaml b/scribe/pubspec.yaml index 7c21abd24..34e61c054 100644 --- a/scribe/pubspec.yaml +++ b/scribe/pubspec.yaml @@ -26,7 +26,7 @@ dependencies: ### Dependencies from pub.dev ### dartz: 0.10.1 - dio: 5.0.0 + dio: 5.2.0 get: 4.6.6 diff --git a/server_settings/pubspec.lock b/server_settings/pubspec.lock index 1d92bfe45..f18aacd30 100644 --- a/server_settings/pubspec.lock +++ b/server_settings/pubspec.lock @@ -181,10 +181,10 @@ packages: dependency: "direct main" description: name: dio - sha256: "9fdbf71baeb250fc9da847f6cb2052196f62c19906a3657adfc18631a667d316" + sha256: e44ce32c01f02c54a101551def8586f15d6189b4f999d4420aad38995ac62e6d url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "5.2.0" equatable: dependency: "direct main" description: @@ -288,7 +288,7 @@ packages: description: path: "." ref: main - resolved-ref: "446efbbccdde981371b4d2a636762184adf6e926" + resolved-ref: "8e2bd9cb1bbd8236be895aa04bf0d0da8e2ff3b2" url: "https://github.com/linagora/jmap-dart-client.git" source: git version: "0.3.6" diff --git a/server_settings/pubspec.yaml b/server_settings/pubspec.yaml index 18af869f9..ca2fe232b 100644 --- a/server_settings/pubspec.yaml +++ b/server_settings/pubspec.yaml @@ -21,7 +21,7 @@ dependencies: json_annotation: 4.8.0 - dio: 5.0.0 + dio: 5.2.0 http_mock_adapter: 0.4.2