diff --git a/lib/main.dart b/lib/main.dart index 50aeb9b..aef3add 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,13 +1,3 @@ -import 'dart:async'; - -import 'package:sizzle_starter/src/core/utils/logger.dart'; import 'package:sizzle_starter/src/feature/initialization/logic/app_runner.dart'; -void main() { - final logger = DeveloperLogger(); - - runZonedGuarded( - () => AppRunner(logger).initializeAndRun(), - logger.logZoneError, - ); -} +void main() => AppRunner.startup(); diff --git a/lib/src/core/constant/config.dart b/lib/src/core/constant/application_config.dart similarity index 62% rename from lib/src/core/constant/config.dart rename to lib/src/core/constant/application_config.dart index a1968fe..662f9ec 100644 --- a/lib/src/core/constant/config.dart +++ b/lib/src/core/constant/application_config.dart @@ -1,44 +1,44 @@ import 'package:sizzle_starter/src/feature/initialization/model/environment.dart'; /// Application configuration -class Config { - /// Creates a new [Config] instance. - const Config(); +class ApplicationConfig { + /// Creates a new [ApplicationConfig] instance. + const ApplicationConfig(); /// The current environment. Environment get environment { - var env = const String.fromEnvironment('ENVIRONMENT'); + var env = const String.fromEnvironment('ENVIRONMENT').trim(); if (env.isNotEmpty) { return Environment.from(env); } - env = const String.fromEnvironment('FLUTTER_APP_FLAVOR'); + env = const String.fromEnvironment('FLUTTER_APP_FLAVOR').trim(); return Environment.from(env); } /// The Sentry DSN. - String get sentryDsn => const String.fromEnvironment('SENTRY_DSN'); + String get sentryDsn => const String.fromEnvironment('SENTRY_DSN').trim(); /// Whether Sentry is enabled. bool get enableSentry => sentryDsn.isNotEmpty; } /// {@template testing_dependencies_container} -/// A special version of [Config] that is used in tests. +/// A special version of [ApplicationConfig] that is used in tests. /// -/// In order to use [Config] in tests, it is needed to +/// In order to use [ApplicationConfig] in tests, it is needed to /// extend this class and provide the dependencies that are needed for the test. /// {@endtemplate} -base class TestConfig implements Config { +base class TestConfig implements ApplicationConfig { /// {@macro testing_dependencies_container} const TestConfig(); @override Object noSuchMethod(Invocation invocation) { throw UnimplementedError( - 'The test tries to access ${invocation.memberName} config option, but ' + 'The test tries to access ${invocation.memberName} (${invocation.runtimeType}) config option, but ' 'it was not provided. Please provide the option in the test. ' 'You can do it by extending this class and providing the option.', ); diff --git a/lib/src/core/rest_client/src/http/rest_client_http.dart b/lib/src/core/rest_client/src/http/rest_client_http.dart index fb5522f..7c1d6cd 100644 --- a/lib/src/core/rest_client/src/http/rest_client_http.dart +++ b/lib/src/core/rest_client/src/http/rest_client_http.dart @@ -5,7 +5,7 @@ import 'package:http/http.dart' as http; import 'package:sizzle_starter/src/core/rest_client/rest_client.dart'; import 'package:sizzle_starter/src/core/rest_client/src/http/check_exception_io.dart' if (dart.library.js_interop) 'package:sizzle_starter/src/core/rest_client/src/http/check_exception_browser.dart'; -import 'package:sizzle_starter/src/core/utils/logger.dart'; +import 'package:sizzle_starter/src/core/utils/logger/logger.dart'; // coverage:ignore-start /// Creates an [http.Client] based on the current platform. diff --git a/lib/src/core/utils/analytics/firebase_analytics_reporter.dart b/lib/src/core/utils/analytics/firebase_analytics_reporter.dart index 1128e07..751a089 100644 --- a/lib/src/core/utils/analytics/firebase_analytics_reporter.dart +++ b/lib/src/core/utils/analytics/firebase_analytics_reporter.dart @@ -1,6 +1,6 @@ import 'package:firebase_analytics/firebase_analytics.dart'; import 'package:sizzle_starter/src/core/utils/analytics/analytics_reporter.dart'; -import 'package:sizzle_starter/src/core/utils/logger.dart'; +import 'package:sizzle_starter/src/core/utils/logger/logger.dart'; /// {@template firebase_analytics_reporter} /// An implementation of [AnalyticsReporter] that reports events to Firebase diff --git a/lib/src/core/utils/app_bloc_observer.dart b/lib/src/core/utils/app_bloc_observer.dart index cde8a74..e7c79f4 100644 --- a/lib/src/core/utils/app_bloc_observer.dart +++ b/lib/src/core/utils/app_bloc_observer.dart @@ -1,6 +1,6 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:sizzle_starter/src/core/utils/extensions/string_extension.dart'; -import 'package:sizzle_starter/src/core/utils/logger.dart'; +import 'package:sizzle_starter/src/core/utils/logger/logger.dart'; /// [BlocObserver] which logs all bloc state changes, errors and events. class AppBlocObserver extends BlocObserver { diff --git a/lib/src/core/utils/error_reporter/error_reporter.dart b/lib/src/core/utils/error_reporter/error_reporter.dart new file mode 100644 index 0000000..00927f7 --- /dev/null +++ b/lib/src/core/utils/error_reporter/error_reporter.dart @@ -0,0 +1,78 @@ +import 'dart:async'; + +import 'package:sizzle_starter/src/core/utils/logger/logger.dart'; + +/// {@template error_reporter} +/// An interface for reporting errors. +/// +/// Implementations should report errors to a service like Sentry/Crashlytics. +/// {@endtemplate} +abstract interface class ErrorReporter { + /// Returns `true` if the error reporting service is initialized + /// and ready to report errors. + /// + /// If this returns `false`, the error reporting service should not be used. + bool get isInitialized; + + /// Initializes the error reporting service. + Future initialize(); + + /// Closes the error reporting service. + Future close(); + + /// Capture an exception to be reported. + /// + /// The [throwable] is the exception that was thrown. + /// The [stackTrace] is the stack trace associated with the exception. + Future captureException({ + required Object throwable, + StackTrace? stackTrace, + }); +} + +/// {@template error_reporter_log_observer} +/// An observer that reports logs to the error reporter if it is active. +/// {@endtemplate} +final class ErrorReporterLogObserver extends LogObserver { + /// {@macro error_reporter_log_observer} + const ErrorReporterLogObserver(this._errorReporter); + + /// Error reporter used to report errors. + final ErrorReporter _errorReporter; + + @override + void onLog(LogMessage logMessage) { + // If the error reporter is not initialized, do nothing + if (!_errorReporter.isInitialized) return; + + // If the log level is error or higher, report the error + if (logMessage.level.index >= LogLevel.error.index) { + _errorReporter.captureException( + throwable: logMessage.error ?? ReportedMessageException(logMessage.message), + stackTrace: logMessage.stackTrace ?? StackTrace.current, + ); + } + } +} + +/// An exception used for error logs without an exception, only a message. +class ReportedMessageException implements Exception { + /// Constructs an instance of [ReportedMessageException]. + const ReportedMessageException(this.message); + + /// The message that was reported. + final String message; + + @override + String toString() => message; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is ReportedMessageException && other.message == message; + } + + @override + int get hashCode => message.hashCode; +} diff --git a/lib/src/core/utils/error_reporter/sentry_error_reporter.dart b/lib/src/core/utils/error_reporter/sentry_error_reporter.dart new file mode 100644 index 0000000..944ffbe --- /dev/null +++ b/lib/src/core/utils/error_reporter/sentry_error_reporter.dart @@ -0,0 +1,49 @@ +import 'package:flutter/foundation.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; +import 'package:sizzle_starter/src/core/utils/error_reporter/error_reporter.dart'; + +/// {@template sentry_error_reporter} +/// An implementation of [ErrorReporter] that reports errors to Sentry. +/// {@endtemplate} +class SentryErrorReporter implements ErrorReporter { + /// {@macro sentry_error_reporter} + const SentryErrorReporter({ + required this.sentryDsn, + required this.environment, + }); + + /// The Sentry DSN. + final String sentryDsn; + + /// The Sentry environment. + final String environment; + + @override + bool get isInitialized => Sentry.isEnabled; + + @override + Future initialize() async { + await SentryFlutter.init( + (options) => options + ..dsn = sentryDsn + ..tracesSampleRate = 0.10 + ..debug = kDebugMode + ..environment = environment + ..anrEnabled = true + ..sendDefaultPii = true, + ); + } + + @override + Future close() async { + await Sentry.close(); + } + + @override + Future captureException({ + required Object throwable, + StackTrace? stackTrace, + }) async { + await Sentry.captureException(throwable, stackTrace: stackTrace); + } +} diff --git a/lib/src/core/utils/error_tracking_manager/error_tracking_manager.dart b/lib/src/core/utils/error_tracking_manager/error_tracking_manager.dart deleted file mode 100644 index eb226ed..0000000 --- a/lib/src/core/utils/error_tracking_manager/error_tracking_manager.dart +++ /dev/null @@ -1,63 +0,0 @@ -import 'dart:async'; - -import 'package:meta/meta.dart'; -import 'package:sizzle_starter/src/core/utils/logger.dart'; - -/// {@template error_tracking_manager} -/// A class which is responsible for enabling error tracking. -/// {@endtemplate} -abstract interface class ErrorTrackingManager { - /// Handles the log message. - /// - /// This method is called when a log message is received. - Future report(LogMessage log); - - /// Enables error tracking. - /// - /// This method should be called when the user has opted in to error tracking. - Future enableReporting(); - - /// Disables error tracking. - /// - /// This method should be called when the user has opted out of error tracking - Future disableReporting(); -} - -/// {@template error_tracking_manager_base} -/// A class that is responsible for managing Sentry error tracking. -/// {@endtemplate} -abstract base class ErrorTrackingManagerBase implements ErrorTrackingManager { - /// {@macro error_tracking_manager_base} - ErrorTrackingManagerBase(this._logger); - - final Logger _logger; - StreamSubscription? _subscription; - - /// Catch only warnings and errors - Stream get _reportLogs => _logger.logs.where(_warnOrUp); - - static bool _warnOrUp(LogMessage log) => log.level.index >= LogLevel.warn.index; - - @mustCallSuper - @mustBeOverridden - @override - Future disableReporting() async { - await _subscription?.cancel(); - _subscription = null; - } - - @mustCallSuper - @mustBeOverridden - @override - Future enableReporting() async { - _subscription ??= _reportLogs.listen((log) async { - if (shouldReport(log.error)) { - await report(log); - } - }); - } - - /// Returns `true` if the error should be reported. - @pragma('vm:prefer-inline') - bool shouldReport(Object? error) => true; -} diff --git a/lib/src/core/utils/error_tracking_manager/sentry_tracking_manager.dart b/lib/src/core/utils/error_tracking_manager/sentry_tracking_manager.dart deleted file mode 100644 index a84a9da..0000000 --- a/lib/src/core/utils/error_tracking_manager/sentry_tracking_manager.dart +++ /dev/null @@ -1,73 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; -import 'package:sizzle_starter/src/core/utils/error_tracking_manager/error_tracking_manager.dart'; -import 'package:sizzle_starter/src/core/utils/logger.dart'; - -/// {@template sentry_tracking_manager} -/// A class that is responsible for managing Sentry error tracking. -/// {@endtemplate} -final class SentryTrackingManager extends ErrorTrackingManagerBase { - /// {@macro sentry_tracking_manager} - SentryTrackingManager( - super._logger, { - required this.sentryDsn, - required this.environment, - }); - - /// The Sentry DSN. - final String sentryDsn; - - /// The Sentry environment. - final String environment; - - @override - Future report(LogMessage log) async { - final error = log.error; - final stackTrace = log.stackTrace; - final hint = log.context != null ? Hint.withMap(log.context!) : null; - - if (error == null && stackTrace == null) { - await Sentry.captureMessage( - log.message, - level: _logLevel(log.level), - hint: hint, - ); - return; - } - - await Sentry.captureException( - error ?? log.message, - stackTrace: stackTrace, - hint: hint, - ); - } - - @override - Future enableReporting() async { - await SentryFlutter.init((options) { - options.dsn = sentryDsn; - - // Set the sample rate to 10% of events. - options.tracesSampleRate = 0.10; - options.debug = kDebugMode; - options.environment = environment; - options.anrEnabled = true; - options.sendDefaultPii = true; - }); - await super.enableReporting(); - } - - @override - Future disableReporting() async { - await Sentry.close(); - await super.disableReporting(); - } - - SentryLevel _logLevel(LogLevel level) => switch (level) { - LogLevel.trace || LogLevel.debug => SentryLevel.debug, - LogLevel.info => SentryLevel.info, - LogLevel.warn => SentryLevel.warning, - LogLevel.error => SentryLevel.error, - LogLevel.fatal => SentryLevel.fatal, - }; -} diff --git a/lib/src/core/utils/logger.dart b/lib/src/core/utils/logger/logger.dart similarity index 59% rename from lib/src/core/utils/logger.dart rename to lib/src/core/utils/logger/logger.dart index 3378b4d..d9c89be 100644 --- a/lib/src/core/utils/logger.dart +++ b/lib/src/core/utils/logger/logger.dart @@ -1,114 +1,21 @@ -import 'dart:async'; -import 'dart:developer' as developer; - import 'package:clock/clock.dart'; import 'package:flutter/foundation.dart'; -import 'package:stack_trace/stack_trace.dart'; - -/// Configuration options for logging behavior. -/// -/// Allows customization of how log messages are formatted and displayed. -class LoggingOptions { - /// Constructs an instance of [LoggingOptions]. - /// - /// - [logInRelease]: Whether logging is enabled in release builds. - /// Defaults to `false`. - /// - [level]: The minimum log level that will be displayed. - /// Defaults to [LogLevel.info]. - const LoggingOptions({ - this.logInRelease = false, - this.level = LogLevel.info, - }); - - /// Whether logging is enabled in release builds. - final bool logInRelease; - - /// The minimum log level that will be displayed. - final LogLevel level; -} - -/// Internal class used by [DefaultLogger] to wrap the log messages. -class LogWrapper { - /// Constructs an instance of [LogWrapper]. - LogWrapper({ - required this.message, - required this.printStackTrace, - required this.printError, - }); - - /// The log message to be wrapped. - LogMessage message; - - /// Whether to print the stack trace. - bool printStackTrace; - - /// Whether to print the error. - bool printError; -} -/// {@template printing_logger} -/// A logger that uses [developer.log] to print log messages. +/// {@template app_logger} +/// Logger class, that manages the logging of messages /// {@endtemplate} -base class DeveloperLogger extends DefaultLogger { - /// Constructs an instance of [DeveloperLogger]. - DeveloperLogger([this.options = const LoggingOptions()]) { - _subscription = _logWrapStream.listen((wrappedLog) { - _printLogMessage(wrappedLog, options); - }); - } +base class AppLogger extends Logger { + /// Constructs an instance of [AppLogger]. + AppLogger({List observers = const []}) : _observers = List.unmodifiable(observers); - /// The options used by this logger. - final LoggingOptions options; + final List _observers; - /// The subscription to the log stream. - StreamSubscription? _subscription; + /// Whether the logger has been destroyed. + bool get destroyed => _destroyed; + var _destroyed = false; @override void destroy() { - super.destroy(); - _subscription?.cancel(); - } - - void _printLogMessage(LogWrapper wrappedLog, LoggingOptions options) { - if (wrappedLog.message.level.compareTo(options.level) < 0) return; - - final log = wrappedLog.message; - - final logLevelsLength = LogLevel.values.length; - final severityPerLevel = 2000 ~/ logLevelsLength; - final severity = log.level.index * severityPerLevel; - - developer.log( - log.message, - error: wrappedLog.printError ? log.error : null, - // We have levels from 0 to 5, but developer.log has from 0 to 2000, - // so we need to multiply by 400 to get a value between 0 and 2000. - level: severity, - name: log.level.toShortName(), - stackTrace: wrappedLog.printStackTrace && log.stackTrace != null - ? Trace.from(log.stackTrace!).terse - : null, - time: log.timestamp, - ); - } -} - -/// The default logger implementation, used by the application. -base class DefaultLogger extends Logger { - /// Constructs an instance of [DefaultLogger]. - DefaultLogger(); - - final _controller = StreamController(); - late final _logWrapStream = _controller.stream.asBroadcastStream(); - late final _logs = _logWrapStream.map((wrapper) => wrapper.message); - bool _destroyed = false; - - @override - Stream get logs => _logs; - - @override - void destroy() { - _controller.close(); _destroyed = true; } @@ -122,37 +29,27 @@ base class DefaultLogger extends Logger { bool printStackTrace = true, bool printError = true, }) { - assert(!_destroyed, 'Logger has been destroyed. It cannot be used anymore.'); - if (_destroyed) return; - - final logMessage = LogWrapper( - message: LogMessage( - message: message, - level: level, - timestamp: clock.now(), - error: error, - stackTrace: stackTrace, - context: context, - ), - printStackTrace: printStackTrace, - printError: printError, + if (_destroyed || _observers.isEmpty) return; + + final logMessage = LogMessage( + message: message, + level: level, + error: error, + stackTrace: stackTrace, + timestamp: clock.now(), ); - _controller.add(logMessage); + for (final observer in _observers) { + observer.onLog(logMessage); + } } } -/// {@macro logger} -/// -/// A logger that does nothing, used for testing purposes. +/// A logger that does nothing. final class NoOpLogger extends Logger { /// Constructs an instance of [NoOpLogger]. const NoOpLogger(); - @override - // ignore: no-empty-block - void destroy() {} - @override void log( String message, { @@ -163,10 +60,15 @@ final class NoOpLogger extends Logger { bool printStackTrace = true, bool printError = true, // ignore: no-empty-block - }) {} + }) { + // no-op + } @override - Stream get logs => const Stream.empty(); + // ignore: no-empty-block + void destroy() { + // no-op + } } /// {@template logger} @@ -176,9 +78,6 @@ abstract base class Logger { /// Constructs an instance of [Logger]. const Logger(); - /// Stream of log messages - Stream get logs; - /// Destroys the logger and releases all resources /// /// After calling this method, the logger should not be used anymore. @@ -204,14 +103,16 @@ abstract base class Logger { ); /// Logs a flutter error with [LogLevel.error]. - void logFlutterError(FlutterErrorDetails details) => log( - details.toString(), - level: LogLevel.error, - error: details.exception, - stackTrace: details.stack, - printStackTrace: false, - printError: false, - ); + void logFlutterError(FlutterErrorDetails details) { + log( + details.summary.toString(), + level: LogLevel.error, + error: details.exception, + stackTrace: details.stack, + printStackTrace: false, + printError: false, + ); + } /// Logs a platform dispatcher error with [LogLevel.error]. bool logPlatformDispatcherError(Object error, StackTrace stackTrace) { @@ -225,9 +126,6 @@ abstract base class Logger { return true; } - /// Creates a logger that uses this instance with a new prefix. - Logger withPrefix(String prefix) => PrefixedLogger(this, prefix); - /// Logs a message with [LogLevel.trace]. void trace( String message, { @@ -343,43 +241,50 @@ abstract base class Logger { ); } -/// A logger that prefixes all log messages with a given string. -base class PrefixedLogger extends Logger { - /// Constructs an instance of [PrefixedLogger]. - const PrefixedLogger(this._logger, this._prefix); +/// Log level, that describes the severity of the log message +/// +/// Index of the log level is used to determine the severity of the log message. +enum LogLevel implements Comparable { + /// A log level describing events showing step by step execution of your code + /// that can be ignored during the standard operation, + /// but may be useful during extended debugging sessions. + trace._(), - final Logger _logger; - final String _prefix; + /// A log level used for events considered to be useful during software + /// debugging when more granular information is needed. + debug._(), - @override - Stream get logs => _logger.logs; + /// An event happened, the event is purely informative + /// and can be ignored during normal operations. + info._(), - @override - void destroy() => _logger.destroy(); + /// Unexpected behavior happened inside the application, but it is continuing + /// its work and the key business features are operating as expected. + warn._(), - @override - Logger withPrefix(String prefix) => PrefixedLogger(_logger, '$_prefix $prefix'); + /// One or more functionalities are not working, + /// preventing some functionalities from working correctly. + /// For example, a network request failed, a file is missing, etc. + error._(), + + /// One or more key business functionalities are not working + /// and the whole system doesn’t fulfill the business functionalities. + fatal._(); + + const LogLevel._(); @override - void log( - String message, { - required LogLevel level, - Object? error, - StackTrace? stackTrace, - Map? context, - bool printStackTrace = true, - bool printError = true, - }) { - _logger.log( - '$_prefix $message', - level: level, - error: error, - stackTrace: stackTrace, - context: context, - printStackTrace: printStackTrace, - printError: printError, - ); - } + int compareTo(LogLevel other) => index - other.index; + + /// Return short name of the log level. + String toShortName() => switch (this) { + LogLevel.trace => 'TRC', + LogLevel.debug => 'DBG', + LogLevel.info => 'INF', + LogLevel.warn => 'WRN', + LogLevel.error => 'ERR', + LogLevel.fatal => 'FTL', + }; } /// Represents a single log message with various details @@ -394,15 +299,12 @@ class LogMessage { /// - [error]: Optional. Any error object associated with the log message. /// - [stackTrace]: Optional. The stack trace associated with the log message, /// typically provided when logging errors. - /// - [context]: Optional. Additional contextual information provided - /// as a map, which can be useful for debugging. const LogMessage({ required this.message, required this.level, required this.timestamp, this.error, this.stackTrace, - this.context, }); /// The main content of the log message. @@ -425,53 +327,16 @@ class LogMessage { /// This provides detailed information about the call stack leading /// up to the log message, which is particularly useful when logging errors. final StackTrace? stackTrace; - - /// Additional contextual information provided as a map. - final Map? context; } -/// Log level, that describes the severity of the log message -/// -/// Index of the log level is used to determine the severity of the log message. -enum LogLevel implements Comparable { - /// A log level describing events showing step by step execution of your code - /// that can be ignored during the standard operation, - /// but may be useful during extended debugging sessions. - trace._(), - - /// A log level used for events considered to be useful during software - /// debugging when more granular information is needed. - debug._(), - - /// An event happened, the event is purely informative - /// and can be ignored during normal operations. - info._(), - - /// Unexpected behavior happened inside the application, but it is continuing - /// its work and the key business features are operating as expected. - warn._(), - - /// One or more functionalities are not working, - /// preventing some functionalities from working correctly. - /// For example, a network request failed, a file is missing, etc. - error._(), - - /// One or more key business functionalities are not working - /// and the whole system doesn’t fulfill the business functionalities. - fatal._(); - - const LogLevel._(); - - @override - int compareTo(LogLevel other) => index - other.index; +/// {@template log_observer} +/// Observer class, that is notified when a new log message is created +/// {@endtemplate} +abstract base class LogObserver { + /// Constructs an instance of [LogObserver]. + const LogObserver(); - /// Return short name of the log level. - String toShortName() => switch (this) { - LogLevel.trace => 'TRC', - LogLevel.debug => 'DBG', - LogLevel.info => 'INF', - LogLevel.warn => 'WRN', - LogLevel.error => 'ERR', - LogLevel.fatal => 'FTL', - }; + /// Called when a new log message is created. + // ignore: no-empty-block + void onLog(LogMessage logMessage) {} } diff --git a/lib/src/core/utils/logger/printing_log_observer.dart b/lib/src/core/utils/logger/printing_log_observer.dart new file mode 100644 index 0000000..a006a24 --- /dev/null +++ b/lib/src/core/utils/logger/printing_log_observer.dart @@ -0,0 +1,32 @@ +import 'dart:developer' as dev; + +import 'package:sizzle_starter/src/core/utils/logger/logger.dart'; + +/// {@template printing_log_observer} +/// [LogObserver] that prints logs using `dart:developer`. +/// {@endtemplate} +final class PrintingLogObserver extends LogObserver { + /// {@macro printing_log_observer} + const PrintingLogObserver({required this.logLevel}); + + /// The log level to observe. + final LogLevel logLevel; + + @override + void onLog(LogMessage logMessage) { + if (logMessage.level.index >= logLevel.index) { + final logLevelsLength = LogLevel.values.length; + final severityPerLevel = 2000 ~/ logLevelsLength; + final level = logMessage.level.index * severityPerLevel; + + dev.log( + logMessage.message, + time: logMessage.timestamp, + error: logMessage.error, + stackTrace: logMessage.stackTrace, + level: level, + name: logMessage.level.toShortName(), + ); + } + } +} diff --git a/lib/src/feature/home/widget/home_screen.dart b/lib/src/feature/home/widget/home_screen.dart index 0ca2b40..e56b4e8 100644 --- a/lib/src/feature/home/widget/home_screen.dart +++ b/lib/src/feature/home/widget/home_screen.dart @@ -13,12 +13,12 @@ class HomeScreen extends StatefulWidget { } class _HomeScreenState extends State { - late final _homeLogger = DependenciesScope.of(context).logger.withPrefix('[HOME]'); + late final _logger = DependenciesScope.of(context).logger; @override void initState() { super.initState(); - _homeLogger.info('Welcome To Sizzle Starter!'); + _logger.info('Welcome To Sizzle Starter!'); } @override diff --git a/lib/src/feature/initialization/logic/app_runner.dart b/lib/src/feature/initialization/logic/app_runner.dart index ed24844..01bfb0f 100644 --- a/lib/src/feature/initialization/logic/app_runner.dart +++ b/lib/src/feature/initialization/logic/app_runner.dart @@ -1,62 +1,75 @@ import 'dart:async'; -import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:sizzle_starter/src/core/constant/config.dart'; +import 'package:sizzle_starter/src/core/constant/application_config.dart'; import 'package:sizzle_starter/src/core/utils/app_bloc_observer.dart'; import 'package:sizzle_starter/src/core/utils/bloc_transformer.dart'; -import 'package:sizzle_starter/src/core/utils/logger.dart'; +import 'package:sizzle_starter/src/core/utils/error_reporter/error_reporter.dart'; +import 'package:sizzle_starter/src/core/utils/logger/logger.dart'; +import 'package:sizzle_starter/src/core/utils/logger/printing_log_observer.dart'; import 'package:sizzle_starter/src/feature/initialization/logic/composition_root.dart'; -import 'package:sizzle_starter/src/feature/initialization/widget/app.dart'; import 'package:sizzle_starter/src/feature/initialization/widget/initialization_failed_app.dart'; +import 'package:sizzle_starter/src/feature/initialization/widget/root_context.dart'; /// {@template app_runner} -/// A class which is responsible for initialization and running the app. +/// A class that is responsible for running the application. /// {@endtemplate} -final class AppRunner { +sealed class AppRunner { /// {@macro app_runner} - const AppRunner(this.logger); - - /// The logger instance - final Logger logger; - - /// Start the initialization and in case of success run application - Future initializeAndRun() async { - final binding = WidgetsFlutterBinding.ensureInitialized(); - - // Preserve splash screen - binding.deferFirstFrame(); - - // Override logging - FlutterError.onError = logger.logFlutterError; - WidgetsBinding.instance.platformDispatcher.onError = logger.logPlatformDispatcherError; - - // Setup bloc observer and transformer - Bloc.observer = AppBlocObserver(logger); - Bloc.transformer = SequentialBlocTransformer().transform; - const config = Config(); - - Future initializeAndRun() async { - try { - final result = await CompositionRoot(config, logger).compose(); - // Attach this widget to the root of the tree. - runApp(App(result: result)); - } catch (e, stackTrace) { - logger.error('Initialization failed', error: e, stackTrace: stackTrace); - runApp( - InitializationFailedApp( - error: e, - stackTrace: stackTrace, - onRetryInitialization: initializeAndRun, - ), - ); - } finally { - // Allow rendering - binding.allowFirstFrame(); - } - } - - // Run the app - await initializeAndRun(); + const AppRunner._(); + + /// Initializes dependencies and launches the application within a guarded execution zone. + static Future startup() async { + const config = ApplicationConfig(); + final errorReporter = await const ErrorReporterFactory(config).create(); + + final logger = AppLoggerFactory( + observers: [ + ErrorReporterLogObserver(errorReporter), + if (!kReleaseMode) const PrintingLogObserver(logLevel: LogLevel.trace), + ], + ).create(); + + await runZonedGuarded( + () async { + // Ensure Flutter is initialized + WidgetsFlutterBinding.ensureInitialized(); + + // Configure global error interception + FlutterError.onError = logger.logFlutterError; + WidgetsBinding.instance.platformDispatcher.onError = logger.logPlatformDispatcherError; + + // Setup bloc observer and transformer + Bloc.observer = AppBlocObserver(logger); + Bloc.transformer = SequentialBlocTransformer().transform; + + Future launchApplication() async { + try { + final compositionResult = await CompositionRoot( + config: config, + logger: logger, + errorReporter: errorReporter, + ).compose(); + + runApp(RootContext(compositionResult: compositionResult)); + } on Object catch (e, stackTrace) { + logger.error('Initialization failed', error: e, stackTrace: stackTrace); + runApp( + InitializationFailedApp( + error: e, + stackTrace: stackTrace, + onRetryInitialization: launchApplication, + ), + ); + } + } + + // Launch the application + await launchApplication(); + }, + logger.logZoneError, + ); } } diff --git a/lib/src/feature/initialization/logic/composition_root.dart b/lib/src/feature/initialization/logic/composition_root.dart index 682f2d6..897cbcd 100644 --- a/lib/src/feature/initialization/logic/composition_root.dart +++ b/lib/src/feature/initialization/logic/composition_root.dart @@ -1,47 +1,53 @@ import 'package:clock/clock.dart'; -import 'package:flutter/foundation.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:sizzle_starter/src/core/constant/config.dart'; -import 'package:sizzle_starter/src/core/utils/error_tracking_manager/error_tracking_manager.dart'; -import 'package:sizzle_starter/src/core/utils/error_tracking_manager/sentry_tracking_manager.dart'; -import 'package:sizzle_starter/src/core/utils/logger.dart'; +import 'package:sizzle_starter/src/core/constant/application_config.dart'; +import 'package:sizzle_starter/src/core/utils/error_reporter/error_reporter.dart'; +import 'package:sizzle_starter/src/core/utils/error_reporter/sentry_error_reporter.dart'; +import 'package:sizzle_starter/src/core/utils/logger/logger.dart'; import 'package:sizzle_starter/src/feature/initialization/model/dependencies_container.dart'; import 'package:sizzle_starter/src/feature/settings/bloc/app_settings_bloc.dart'; import 'package:sizzle_starter/src/feature/settings/data/app_settings_datasource.dart'; import 'package:sizzle_starter/src/feature/settings/data/app_settings_repository.dart'; /// {@template composition_root} -/// A place where all dependencies are initialized. +/// A place where top-level dependencies are initialized. /// {@endtemplate} /// /// {@template composition_process} /// Composition of dependencies is a process of creating and configuring /// instances of classes that are required for the application to work. -/// -/// It is a good practice to keep all dependencies in one place to make it -/// easier to manage them and to ensure that they are initialized only once. /// {@endtemplate} final class CompositionRoot { /// {@macro composition_root} - const CompositionRoot(this.config, this.logger); + const CompositionRoot({ + required this.config, + required this.logger, + required this.errorReporter, + }); /// Application configuration - final Config config; + final ApplicationConfig config; /// Logger used to log information during composition process. final Logger logger; + /// Error tracking manager used to track errors in the application. + final ErrorReporter errorReporter; + /// Composes dependencies and returns result of composition. Future compose() async { final stopwatch = clock.stopwatch()..start(); logger.info('Initializing dependencies...'); // initialize dependencies - final dependencies = await DependenciesFactory(config, logger).create(); - logger.info('Dependencies initialized'); - + final dependencies = await DependenciesFactory( + config: config, + logger: logger, + errorReporter: errorReporter, + ).create(); stopwatch.stop(); + logger.info('Dependencies initialized successfully in ${stopwatch.elapsedMilliseconds} ms.'); final result = CompositionResult( dependencies: dependencies, millisecondsSpent: stopwatch.elapsedMilliseconds, @@ -76,10 +82,16 @@ final class CompositionResult { ')'; } +/// Value with time. +typedef ValueWithTime = ({T value, Duration timeSpent}); + /// {@template factory} /// Factory that creates an instance of [T]. /// {@endtemplate} abstract class Factory { + /// {@macro factory} + const Factory(); + /// Creates an instance of [T]. T create(); } @@ -88,6 +100,9 @@ abstract class Factory { /// Factory that creates an instance of [T] asynchronously. /// {@endtemplate} abstract class AsyncFactory { + /// {@macro async_factory} + const AsyncFactory(); + /// Creates an instance of [T]. Future create(); } @@ -97,67 +112,87 @@ abstract class AsyncFactory { /// {@endtemplate} class DependenciesFactory extends AsyncFactory { /// {@macro dependencies_factory} - DependenciesFactory(this.config, this.logger); + const DependenciesFactory({ + required this.config, + required this.logger, + required this.errorReporter, + }); /// Application configuration - final Config config; + final ApplicationConfig config; /// Logger used to log information during composition process. final Logger logger; + /// Error tracking manager used to track errors in the application. + final ErrorReporter errorReporter; + @override Future create() async { final sharedPreferences = SharedPreferencesAsync(); final packageInfo = await PackageInfo.fromPlatform(); - final errorTrackingManager = await ErrorTrackingManagerFactory(config, logger).create(); - final settingsBloc = await SettingsBlocFactory(sharedPreferences).create(); + final settingsBloc = await AppSettingsBlocFactory(sharedPreferences).create(); return DependenciesContainer( logger: logger, config: config, - appSettingsBloc: settingsBloc, - errorTrackingManager: errorTrackingManager, + errorReporter: errorReporter, packageInfo: packageInfo, + appSettingsBloc: settingsBloc, ); } } -/// {@template error_tracking_manager_factory} -/// Factory that creates an instance of [ErrorTrackingManager]. +/// {@template app_logger_factory} +/// Factory that creates an instance of [AppLogger]. /// {@endtemplate} -class ErrorTrackingManagerFactory extends AsyncFactory { - /// {@macro error_tracking_manager_factory} - ErrorTrackingManagerFactory(this.config, this.logger); +class AppLoggerFactory extends Factory { + /// {@macro app_logger_factory} + const AppLoggerFactory({this.observers = const []}); - /// Application configuration - final Config config; + /// List of observers that will be notified when a log message is received. + final List observers; - /// Logger used to log information during composition process. - final Logger logger; + @override + AppLogger create() => AppLogger(observers: observers); +} + +/// {@template error_reporter_factory} +/// Factory that creates an instance of [ErrorReporter]. +/// {@endtemplate} +class ErrorReporterFactory extends AsyncFactory { + /// {@macro error_reporter_factory} + const ErrorReporterFactory(this.config); + + /// Application configuration + final ApplicationConfig config; @override - Future create() async { - final errorTrackingManager = SentryTrackingManager( - logger, + Future create() async { + final errorReporter = SentryErrorReporter( sentryDsn: config.sentryDsn, environment: config.environment.value, ); - if (config.enableSentry && kReleaseMode) { - await errorTrackingManager.enableReporting(); + if (config.sentryDsn.isNotEmpty) { + await errorReporter.initialize(); } - return errorTrackingManager; + return errorReporter; } } -/// {@template settings_bloc_factory} +/// {@template app_settings_bloc_factory} /// Factory that creates an instance of [AppSettingsBloc]. +/// +/// The [AppSettingsBloc] should be initialized during the application startup +/// in order to load the app settings from the local storage, so user can see +/// their selected theme,locale, etc. /// {@endtemplate} -class SettingsBlocFactory extends AsyncFactory { - /// {@macro settings_bloc_factory} - SettingsBlocFactory(this.sharedPreferences); +class AppSettingsBlocFactory extends AsyncFactory { + /// {@macro app_settings_bloc_factory} + const AppSettingsBlocFactory(this.sharedPreferences); /// Shared preferences instance final SharedPreferencesAsync sharedPreferences; diff --git a/lib/src/feature/initialization/model/dependencies_container.dart b/lib/src/feature/initialization/model/dependencies_container.dart index 5a70d73..d403da7 100644 --- a/lib/src/feature/initialization/model/dependencies_container.dart +++ b/lib/src/feature/initialization/model/dependencies_container.dart @@ -1,15 +1,11 @@ import 'package:package_info_plus/package_info_plus.dart'; -import 'package:sizzle_starter/src/core/constant/config.dart'; -import 'package:sizzle_starter/src/core/utils/error_tracking_manager/error_tracking_manager.dart'; -import 'package:sizzle_starter/src/core/utils/logger.dart'; -import 'package:sizzle_starter/src/feature/initialization/logic/composition_root.dart'; +import 'package:sizzle_starter/src/core/constant/application_config.dart'; +import 'package:sizzle_starter/src/core/utils/error_reporter/error_reporter.dart'; +import 'package:sizzle_starter/src/core/utils/logger/logger.dart'; import 'package:sizzle_starter/src/feature/settings/bloc/app_settings_bloc.dart'; /// {@template dependencies_container} -/// Composed dependencies from the [CompositionRoot]. -/// -/// This class contains all the dependencies that are required for the application -/// to work. +/// Container used to reuse dependencies across the application. /// /// {@macro composition_process} /// {@endtemplate} @@ -19,21 +15,21 @@ class DependenciesContainer { required this.logger, required this.config, required this.appSettingsBloc, - required this.errorTrackingManager, + required this.errorReporter, required this.packageInfo, }); /// [Logger] instance, used to log messages. final Logger logger; - /// [Config] instance, contains configuration of the application. - final Config config; + /// [ApplicationConfig] instance, contains configuration of the application. + final ApplicationConfig config; /// [AppSettingsBloc] instance, used to manage theme and locale. final AppSettingsBloc appSettingsBloc; - /// [ErrorTrackingManager] instance, used to report errors. - final ErrorTrackingManager errorTrackingManager; + /// [ErrorReporter] instance, used to report errors. + final ErrorReporter errorReporter; /// [PackageInfo] instance, contains information about the application. final PackageInfo packageInfo; diff --git a/lib/src/feature/initialization/widget/app.dart b/lib/src/feature/initialization/widget/root_context.dart similarity index 75% rename from lib/src/feature/initialization/widget/app.dart rename to lib/src/feature/initialization/widget/root_context.dart index 90ea549..85f7e89 100644 --- a/lib/src/feature/initialization/widget/app.dart +++ b/lib/src/feature/initialization/widget/root_context.dart @@ -7,25 +7,25 @@ import 'package:sizzle_starter/src/feature/initialization/widget/material_contex import 'package:sizzle_starter/src/feature/settings/widget/settings_scope.dart'; /// {@template app} -/// [App] is an entry point to the application. +/// [RootContext] is an entry point to the application. /// /// If a scope doesn't depend on any inherited widget returned by /// [MaterialApp] or [WidgetsApp], like [Directionality] or [Theme], /// and it should be available in the whole application, it can be /// placed here. /// {@endtemplate} -class App extends StatelessWidget { +class RootContext extends StatelessWidget { /// {@macro app} - const App({required this.result, super.key}); + const RootContext({required this.compositionResult, super.key}); - /// The result from the [CompositionRoot]. - final CompositionResult result; + /// The result from the [CompositionRoot], required to launch the application. + final CompositionResult compositionResult; @override Widget build(BuildContext context) => DefaultAssetBundle( bundle: SentryAssetBundle(), child: DependenciesScope( - dependencies: result.dependencies, + dependencies: compositionResult.dependencies, child: const SettingsScope( child: WindowSizeScope(child: MaterialContext()), ), diff --git a/test/src/core/utils/firebase_analytics_reporter_test.dart b/test/src/core/utils/firebase_analytics_reporter_test.dart index 256da75..cc81eab 100644 --- a/test/src/core/utils/firebase_analytics_reporter_test.dart +++ b/test/src/core/utils/firebase_analytics_reporter_test.dart @@ -4,7 +4,7 @@ import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'package:sizzle_starter/src/core/utils/analytics/analytics_reporter.dart'; import 'package:sizzle_starter/src/core/utils/analytics/firebase_analytics_reporter.dart'; -import 'package:sizzle_starter/src/core/utils/logger.dart'; +import 'package:sizzle_starter/src/core/utils/logger/logger.dart'; @GenerateNiceMocks([MockSpec()]) import 'firebase_analytics_reporter_test.mocks.dart'; diff --git a/test/src/feature/home/widget/home_screen_test.dart b/test/src/feature/home/widget/home_screen_test.dart index ce39a8a..7c9e4ff 100644 --- a/test/src/feature/home/widget/home_screen_test.dart +++ b/test/src/feature/home/widget/home_screen_test.dart @@ -1,5 +1,5 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:sizzle_starter/src/core/utils/logger.dart'; +import 'package:sizzle_starter/src/core/utils/logger/logger.dart'; import 'package:sizzle_starter/src/core/utils/test/test_widget_controller.dart'; import 'package:sizzle_starter/src/feature/home/widget/home_screen.dart'; import 'package:sizzle_starter/src/feature/initialization/model/dependencies_container.dart';