diff --git a/core/lib/utils/app_logger.dart b/core/lib/utils/app_logger.dart index fe9dd9b93..07b1c18d7 100644 --- a/core/lib/utils/app_logger.dart +++ b/core/lib/utils/app_logger.dart @@ -1,5 +1,3 @@ -import 'dart:async'; - import 'package:core/utils/build_utils.dart'; import 'package:core/utils/platform_info.dart'; import 'package:core/utils/sentry/sentry_manager.dart'; @@ -82,17 +80,13 @@ void _internalLog( if (shouldSentry) { if (level == Level.trace) { - unawaited( - SentryManager.instance.captureMessage(rawMessage, extras: extras), - ); + SentryManager.instance.captureMessage(rawMessage, extras: extras); } else { - unawaited( - SentryManager.instance.captureException( - exception ?? rawMessage, - stackTrace: stackTrace, - message: rawMessage, - extras: extras, - ), + SentryManager.instance.captureException( + exception ?? rawMessage, + stackTrace: stackTrace, + message: rawMessage, + extras: extras, ); } } diff --git a/core/lib/utils/sentry/sentry_config.dart b/core/lib/utils/sentry/sentry_config.dart index 4bf6f0854..810e413e0 100644 --- a/core/lib/utils/sentry/sentry_config.dart +++ b/core/lib/utils/sentry/sentry_config.dart @@ -1,6 +1,5 @@ import 'package:core/utils/application_manager.dart'; import 'package:core/utils/build_utils.dart'; -import 'package:core/utils/config/env_loader.dart'; import 'package:core/utils/app_logger.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; @@ -15,7 +14,7 @@ class SentryConfig { // Current app release version final String release; - // // Performance monitoring: Set tracesSampleRate to 1.0 to capture 100% of transactions for tracing + // Performance monitoring: Set tracesSampleRate to 1.0 to capture 100% of transactions for tracing final double tracesSampleRate; // Optional profiling @@ -38,23 +37,34 @@ class SentryConfig { // This must match the `--dist` parameter used when uploading source maps to Sentry. final String? dist; + // Release Health: The sampling rate for sessions (0.0 to 1.0). Defines the percentage of sessions to send. + final double sessionSampleRate; + + // Error tracking: The sampling rate for errors (0.0 to 1.0). If set to 0.1, only 10% of errors are sent. + final double onErrorSampleRate; + + // Performance: Tracks UI rendering performance (slow and frozen frames). + final bool enableFramesTracking; + SentryConfig({ required this.dsn, required this.environment, required this.release, this.tracesSampleRate = 1.0, this.profilesSampleRate = 1.0, + this.sessionSampleRate = 1.0, + this.onErrorSampleRate = 1.0, this.enableLogs = true, + this.enableFramesTracking = true, this.isDebug = BuildUtils.isDebugMode, this.attachScreenshot = false, this.isAvailable = false, this.dist, }); - /// Load configuration from an env file. + /// Loads configuration from loaded environment variables. static Future load() async { - await EnvLoader.loadConfigFromEnv(); - + // Note: Ensure EnvLoader.loadEnvFile() is called in main.dart before this. final sentryAvailable = dotenv.get('SENTRY_ENABLED', fallback: 'false'); final isAvailable = sentryAvailable == 'true'; diff --git a/core/lib/utils/sentry/sentry_initializer.dart b/core/lib/utils/sentry/sentry_initializer.dart index 189fd371c..d633b265a 100644 --- a/core/lib/utils/sentry/sentry_initializer.dart +++ b/core/lib/utils/sentry/sentry_initializer.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:core/utils/app_logger.dart'; import 'package:core/utils/sentry/sentry_config.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; @@ -20,34 +21,58 @@ class SentryInitializer { 'token', ]; - static Future init(FutureOr Function() appRunner) async { - final config = await SentryConfig.load(); + /// Initializes Sentry. + /// Returns [true] if Sentry started successfully and handled the app execution. + /// Returns [false] if Sentry is disabled or failed to start. + static Future init({ + FutureOr Function()? appRunner, + SentryConfig? sentryConfig, + }) async { + try { + final config = sentryConfig ?? await SentryConfig.load(); - if (config == null) return false; + // Return false if config is missing or Sentry is disabled via Env. + if (config == null || !config.isAvailable) { + return false; + } - await SentryFlutter.init( - (options) { - options.dsn = config.dsn; - options.environment = config.environment; - options.release = config.release; - options.dist = config.dist; - options.tracesSampleRate = config.tracesSampleRate; - options.profilesSampleRate = config.profilesSampleRate; - options.enableLogs = config.enableLogs; - options.debug = config.isDebug; - options.attachScreenshot = config.attachScreenshot; - options.maxRequestBodySize = MaxRequestBodySize.small; + await SentryFlutter.init( + (options) => _setUpSentryOptions(options, config), + // Sentry will execute this runner if initialization succeeds. + appRunner: appRunner, + ); - // Automatically enable breadcrumbs that are appropriate for the current platform - options.enableBreadcrumbTrackingForCurrentPlatform(); + return true; + } catch (e) { + // In case of any error during init, return false so main() can fallback. + logWarning('[SentryInitializer] Init failed: $e'); + return false; + } + } - // Assign the callback to process events before sending them to Sentry - options.beforeSend = _beforeSendHandler; - }, - appRunner: appRunner, - ); + static FutureOr _setUpSentryOptions( + SentryFlutterOptions options, + SentryConfig config, + ) async { + options.dsn = config.dsn; + options.environment = config.environment; + options.release = config.release; + options.dist = config.dist; + options.tracesSampleRate = config.tracesSampleRate; + options.profilesSampleRate = config.profilesSampleRate; + options.replay.sessionSampleRate = config.sessionSampleRate; + options.replay.onErrorSampleRate = config.onErrorSampleRate; + options.enableLogs = config.enableLogs; + options.enableFramesTracking = config.enableFramesTracking; + options.debug = config.isDebug; + options.attachScreenshot = config.attachScreenshot; + options.maxRequestBodySize = MaxRequestBodySize.small; - return config.isAvailable; + // 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; } /// Handler executed before sending an event to Sentry diff --git a/core/lib/utils/sentry/sentry_manager.dart b/core/lib/utils/sentry/sentry_manager.dart index 6375779dd..d6bb4e897 100644 --- a/core/lib/utils/sentry/sentry_manager.dart +++ b/core/lib/utils/sentry/sentry_manager.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:core/utils/app_logger.dart'; +import 'package:core/utils/sentry/sentry_config.dart'; import 'package:core/utils/sentry/sentry_initializer.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; @@ -14,22 +15,25 @@ class SentryManager { bool get isSentryAvailable => _isSentryAvailable; - /// Initialize Sentry. App still runs if init fails. + /// Initialize Sentry. Future initialize({ required FutureOr Function() appRunner, required FutureOr Function() fallBackRunner, }) async { + if (_isSentryAvailable) { + log('[SentryManager] Already initialized.'); + await appRunner(); + return; + } + try { - if (_isSentryAvailable) { - log('[SentryManager] Already initialized, skipping'); - await appRunner(); - return; - } - _isSentryAvailable = await SentryInitializer.init(appRunner); - log('[SentryManager] Sentry initialized: $_isSentryAvailable'); + _isSentryAvailable = await SentryInitializer.init(appRunner: appRunner); if (!_isSentryAvailable) { + logWarning('[SentryManager] Sentry failed to init, running fallback.'); await fallBackRunner(); + } else { + log('[SentryManager] Sentry active.'); } } catch (e) { logWarning('[SentryManager] Init failed. Exception $e'); @@ -37,69 +41,116 @@ class SentryManager { } } - /// Capture an exception. Metadata is attached as breadcrumbs. - Future captureException( + Future initializeWithSentryConfig(SentryConfig sentryConfig) async { + if (_isSentryAvailable) return; + + try { + _isSentryAvailable = await SentryInitializer.init( + sentryConfig: sentryConfig, + ); + if (_isSentryAvailable) { + log('[SentryManager] Sentry active.'); + } + } catch (e) { + logWarning('[SentryManager] Init exception: $e'); + } + } + + /// Capture an exception. + void 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, - ), - ); - }, + // Use unawaited to prevent linter warnings about unawaited futures. + // We do not want the UI to pause while Sentry writes the crash report. + unawaited( + _captureExceptionInternal(exception, stackTrace, message, extras), ); } - /// Capture a text message. Metadata also goes into breadcrumbs. - Future captureMessage( + Future _captureExceptionInternal( + dynamic exception, + StackTrace? stackTrace, + String? message, + Map? extras, + ) async { + try { + final hasExtras = extras != null && extras.isNotEmpty; + final hasMessage = message != null && message.isNotEmpty; + + if (hasExtras || hasMessage) { + await Sentry.captureException( + exception, + stackTrace: stackTrace, + withScope: (scope) { + if (hasExtras) scope.setContexts('extras', extras); + if (hasMessage) scope.setTag('message', message); + }, + ); + } else { + await Sentry.captureException( + exception, + stackTrace: stackTrace, + ); + } + } catch (e) { + logWarning('[SentryManager] Capture exception failed: $e'); + } + } + + /// Capture a text message. + void 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: level, - ), - ); - }, - ); + unawaited(_captureMessageInternal(message, level, extras)); } - Future setUser(SentryUser user) async { + Future _captureMessageInternal( + String message, + SentryLevel level, + Map? extras, + ) async { + try { + if (extras != null && extras.isNotEmpty) { + await Sentry.captureMessage( + message, + level: level, + withScope: (scope) => scope.setContexts('extras', extras), + ); + } else { + await Sentry.captureMessage(message, level: level); + } + } catch (e) { + logWarning('[SentryManager] Capture message failed: $e'); + } + } + + /// Sets the user context. + void setUser(SentryUser user) { if (!_isSentryAvailable) return; try { - await Sentry.configureScope((scope) => scope.setUser(user)); + Sentry.configureScope((scope) => scope.setUser(user)); log('[SentryManager] User set: ${user.email}'); } catch (e) { logWarning('[SentryManager] Set user failed. Exception: $e'); } } - Future clearUser() async { + /// Clears the user context. + void clearUser() { if (!_isSentryAvailable) return; try { - await Sentry.configureScope((scope) => scope.setUser(null)); + Sentry.configureScope((scope) => scope.setUser(null)); log('[SentryManager] User cleared'); } catch (e) { logWarning('[SentryManager] Clear user failed. Exception: $e');