diff --git a/doc/Bugsee.md b/doc/Bugsee.md new file mode 100644 index 00000000..345de76c --- /dev/null +++ b/doc/Bugsee.md @@ -0,0 +1,206 @@ +# Bugsee Integration Documentation + +This document provides a comprehensive guide to integrating and using **Bugsee** in your mobile application. Bugsee is a powerful tool for monitoring and debugging your app by capturing and reporting unhandled exceptions, providing insights into app crashes, user interactions, and more. + +## **Overview** + +**Bugsee** helps developers quickly identify and troubleshoot crashes, bugs, and performance issues in mobile applications. By integrating Bugsee, developers can capture detailed logs, screen recordings, and contextual data (such as user attributes) to understand and fix issues faster. + +--- + +## **Features** + +This implementation of Bugsee leverages the following features to provide robust exception tracking and reporting: + +### 1. **Manual Invocation** + - Developers can trigger Bugsee for testing purposes or to verify the integration. You can also use different tokens for testing in different environments. + - Documentation: [Bugsee SDK Docs](https://docs.bugsee.com/) + +### 2. **Custom Data Reporting** + - Add additional user-specific data (like email addresses) or custom attributes to exception reports for better context. + - **Email:** Helps identify the specific user experiencing issues. + - **Attributes:** Attach custom key-value pairs for further context. + - **Traces:** Track specific values or conditions before an exception occurs. + - **Events:** Log events leading to exceptions and attach structured JSON data for detailed insights. + - Documentation: [Bugsee Custom Data](https://docs.bugsee.com/) + +### 3. **Exception Logging** + - Bugsee automatically captures unhandled exceptions in your Dart and Flutter code. + - **Dart Exceptions:** Captures logic and data errors. + - **Flutter Exceptions:** Captures rendering and layout errors. + - You can also manually log exceptions with additional context, such as traces and events. + - Documentation: [Bugsee Exception Logging](https://bugsee.com/) + +### 4. **Video Capture** + - Bugsee automatically captures screen recordings of user interactions that lead to exceptions. This helps developers visually understand what the user was doing when the issue occurred. + - You can disable video capture by setting the `videoEnabled` flag. + - Documentation: [Bugsee Flutter Installation](https://docs.bugsee.com/sdk/flutter/installation/) + +### 5. **Log Reporting and Filtering** + - Bugsee integrates with your app’s logging system. By default, logs are filtered to remove sensitive information to protect user privacy. + - You can customize log collection behavior using configuration options. + - Documentation: [Bugsee Log Reporting](https://docs.bugsee.com/sdk/flutter/installation/) + +### 6. **Data Obfuscation** + - Sensitive user data (like passwords or personal information) in captured videos is automatically obscured by default to prevent leaks. + - Documentation: [Bugsee Data Obscuration](https://docs.bugsee.com/sdk/flutter/installation/) + +--- + +## **Default Configurations** + +Bugsee’s behavior can be controlled via environment settings, particularly for data obscuration, log collection, and file attachment. The default configurations are defined in the `.env.staging` file as follows: + +```env +BUGSEE_IS_DATA_OBSCURE=true # Enables data obscuration for captured videos +BUGSEE_DISABLE_LOG_COLLECTION=true # Disables log collection by default +BUGSEE_FILTER_LOG_COLLECTION=false # Allows all logs unless manually filtered +BUGSEE_ATTACH_LOG_FILE=true # Attaches log files with Bugsee reports +``` + +Ensure that these values are properly set for different environments (e.g., staging, production). + +--- + +## **Implementation Details** + +The Bugsee integration consists of several key components for handling configuration, exception tracking, and reporting. + +### 1. **[Bugsee Manager](../src/app/lib/business/bugsee/bugsee_manager.dart)** + - Responsible for initializing Bugsee, capturing logs, and configuring Bugsee features (like video capture, data obfuscation, and log filtering). + +### 2. **[Bugsee Config State](../src/app/lib/business/bugsee/bugsee_config_state.dart)** + - Maintains the current state of Bugsee’s features (enabled/disabled) within the app. + +### 3. **[Bugsee Repository](../src/app/lib/access/bugsee/bugsee_repository.dart)** + - Handles the saving and retrieving of Bugsee configurations from shared preferences. + +### 4. **[Bugsee Saved Configuration](../src/app/lib/access/bugsee/bugsee_configuration_data.dart)** + - Stores and manages the saved configurations used to initialize Bugsee upon app launch. + +--- + +## **Exception Handling and Reporting** + +### Intercepting Exceptions +By default, Bugsee intercepts all unhandled Dart and Flutter exceptions globally: + +1. **Dart Exceptions**: + - These are data or logic errors that happen within your Dart code. + +2. **Flutter Exceptions**: + - These occur during layout or rendering issues. + +Both types of exceptions are captured and reported to Bugsee’s dashboard. + +The exceptions are intercepted using `runZonedGuarded` and `FlutterError.onError`: + +```dart +// main.dart + +runZonedGuarded( + () async { + FlutterError.onError = + GetIt.I.get().inteceptRenderExceptions; + await initializeComponents(); + await registerBugseeManager(); + runApp(const App()); + }, + GetIt.I.get().inteceptExceptions, + ); + +// bugsee_manager.dart + +@override + Future inteceptExceptions( + Object error, + StackTrace stackTrace, + ) async { + String? message = switch (error.runtimeType) { + const (PersistenceException) => (error as PersistenceException).message, + _ => null, + }; + await logException( + exception: Exception(error), + stackTrace: stackTrace, + traces: { + 'message': message, + }, + ); + } + + @override + Future inteceptRenderExceptions(FlutterErrorDetails error) async { + await logException( + exception: Exception(error.exception), + stackTrace: error.stack, + ); + } +``` + +### **Manually Reporting Issues** + +You can manually trigger Bugsee to capture logs and display a report dialog using the `showCaptureLogReport` method: + +```dart +final bugseeManager = Get.I.get(); +bugseeManager.showCaptureLogReport(); +``` + +This is useful for debugging specific scenarios or reporting custom issues. + +### **Manually Logging Exceptions** + +To manually log an exception (with or without additional traces), use the `logException` method: + +```dart +final bugseeManager = Get.I.get(); +bugseeManager.logException(exception: Exception("Custom error")); +``` + +You can add additional context **traces**: + +```dart +bugseeManager.logException( + exception: Exception("Custom error"), + traces: ["Trace 1", "Trace 2"], // Add relevant traces +); +``` + +### **Adding User Attributes** + +To provide more context about the user experiencing the issue, you can add custom attributes such as an email address: + +- **Add Email Attribute**: + +```dart +final bugseeManager = Get.I.get(); +bugseeManager.addEmailAttribute("johndoe@nventive.com"); +``` + +- **Clear Email Attribute**: + +```dart +bugseeManager.clearEmailAttribute(); +``` + +- **Add Custom Attributes**: + +You can also add custom key-value pairs as additional attributes to enrich the exception reports: + +```dart +bugseeManager.addAttributes({ + "userType": "premium", + "accountStatus": "active" +}); +``` + +## **Additional Resources** + +- [Bugsee SDK Documentation](https://docs.bugsee.com/) +- [Bugsee Flutter Installation Guide](https://docs.bugsee.com/sdk/flutter/installation/) +- [bugsee_flutter package](https://pub.dev/packages/bugsee_flutter) +- [Handling Flutter errors](https://docs.flutter.dev/testing/errors) +- [runZoneGuarded Error handling](https://api.flutter.dev/flutter/dart-async/runZonedGuarded.html) + +--- diff --git a/src/app/.env.dev b/src/app/.env.dev index 22d74e66..5f1cc2c9 100644 --- a/src/app/.env.dev +++ b/src/app/.env.dev @@ -4,5 +4,4 @@ MINIMUM_LEVEL='debug' DAD_JOKES_BASE_URL='https://www.reddit.com/r/dadjokes' APP_STORE_URL_IOS=https://apps.apple.com/us/app/uno-calculator/id1464736591 APP_STORE_URL_Android=https://play.google.com/store/apps/details?id=uno.platform.calculator -REMOTE_CONFIG_FETCH_INTERVAL_MINUTES=1 -DIAGNOSTIC_ENABLED=true \ No newline at end of file +REMOTE_CONFIG_FETCH_INTERVAL_MINUTES=1 \ No newline at end of file diff --git a/src/app/.env.prod b/src/app/.env.prod index e2c01228..1aef1e2b 100644 --- a/src/app/.env.prod +++ b/src/app/.env.prod @@ -4,5 +4,4 @@ MINIMUM_LEVEL='warning' DAD_JOKES_BASE_URL='https://www.reddit.com/r/dadjokes' APP_STORE_URL_IOS=https://apps.apple.com/us/app/uno-calculator/id1464736591 APP_STORE_URL_Android=https://play.google.com/store/apps/details?id=uno.platform.calculator -REMOTE_CONFIG_FETCH_INTERVAL_MINUTES=720 -DIAGNOSTIC_ENABLED=false \ No newline at end of file +REMOTE_CONFIG_FETCH_INTERVAL_MINUTES=720 \ No newline at end of file diff --git a/src/app/.env.staging b/src/app/.env.staging index 2412f27b..88998534 100644 --- a/src/app/.env.staging +++ b/src/app/.env.staging @@ -5,4 +5,8 @@ DAD_JOKES_BASE_URL='https://www.reddit.com/r/dadjokes' APP_STORE_URL_IOS=https://apps.apple.com/us/app/uno-calculator/id1464736591 APP_STORE_URL_Android=https://play.google.com/store/apps/details?id=uno.platform.calculator REMOTE_CONFIG_FETCH_INTERVAL_MINUTES=1 -DIAGNOSTIC_ENABLED=true \ No newline at end of file +DIAGNOSTIC_ENABLED=true +BUGSEE_IS_DATA_OBSCURE=true +BUGSEE_DISABLE_LOG_COLLECTION=true +BUGSEE_FILTER_LOG_COLLECTION=false +BUGSEE_ATTACH_LOG_FILE=true \ No newline at end of file diff --git a/src/app/integration_test/bugsee_test.dart b/src/app/integration_test/bugsee_test.dart new file mode 100644 index 00000000..bb2c4c2f --- /dev/null +++ b/src/app/integration_test/bugsee_test.dart @@ -0,0 +1,9 @@ +import 'package:flutter_test/flutter_test.dart'; + +/// Test all Bugsee setup features +Future bugseeSetupTest() async { + testWidgets( + 'Test Bugsee configuration', + (tester) async {}, + ); +} diff --git a/src/app/integration_test/integration_test.dart b/src/app/integration_test/integration_test.dart index 5e555c07..18187bc0 100644 --- a/src/app/integration_test/integration_test.dart +++ b/src/app/integration_test/integration_test.dart @@ -4,6 +4,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:get_it/get_it.dart'; import 'package:integration_test/integration_test.dart'; +import 'bugsee_test.dart'; import 'dad_jokes_page_test.dart'; import 'forced_update_test.dart'; import 'kill_switch_test.dart'; @@ -12,6 +13,11 @@ import 'kill_switch_test.dart'; Future main() async { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); await initializeComponents(isMocked: true); + await registerBugseeManager( + isMock: true, + //A mock hexadecimal-based Bugsee token + bugseeToken: '01234567-0123-0123-0123-0123456789AB', + ); tearDownAll( () async => await GetIt.I.get().setMocking(false), @@ -20,4 +26,5 @@ Future main() async { await dadJokeTest(); await killSwitchTest(); await forcedUpdateTest(); + await bugseeSetupTest(); } diff --git a/src/app/lib/access/bugsee/bugsee_configuration_data.dart b/src/app/lib/access/bugsee/bugsee_configuration_data.dart index 1255c78b..8e1d0f15 100644 --- a/src/app/lib/access/bugsee/bugsee_configuration_data.dart +++ b/src/app/lib/access/bugsee/bugsee_configuration_data.dart @@ -1,12 +1,59 @@ -final class BugseeConfigurationData { +import 'package:equatable/equatable.dart'; + +final class BugseeConfigurationData extends Equatable { /// Gets whether the Bugsee SDK is enabled or not. if [Null] it fallbacks to a new installed app so it will be enabled. final bool? isBugseeEnabled; - /// Indicate whether the video capturing feature in Bugsee is enabled or not. + /// Indicates whether the video capturing feature in Bugsee is enabled or not. final bool? isVideoCaptureEnabled; + /// Indicates whether Bugsee obscures application data in videos and images or not. + final bool? isDataObscured; + + /// Indicates whether logs are collected or not. + final bool? isLogCollectionEnabled; + + /// Indicates whether logs are filtred during reports or not. + final bool? isLogsFilterEnabled; + + /// Indicates whether attaching file in the Bugsee report is enabled or not + final bool? attachLogFileEnabled; + const BugseeConfigurationData({ - required this.isBugseeEnabled, - required this.isVideoCaptureEnabled, + this.isBugseeEnabled, + this.isVideoCaptureEnabled, + this.isDataObscured, + this.isLogCollectionEnabled, + this.isLogsFilterEnabled, + this.attachLogFileEnabled, }); + + BugseeConfigurationData copyWith({ + bool? isBugseeEnabled, + bool? isVideoCaptureEnabled, + bool? isDataObscured, + bool? isLogCollectionEnabled, + bool? isLogsFilterEnabled, + bool? attachLogFileEnabled, + }) => + BugseeConfigurationData( + isBugseeEnabled: isBugseeEnabled ?? this.isBugseeEnabled, + isVideoCaptureEnabled: + isVideoCaptureEnabled ?? this.isVideoCaptureEnabled, + isDataObscured: isDataObscured ?? this.isDataObscured, + isLogCollectionEnabled: + isLogCollectionEnabled ?? this.isLogCollectionEnabled, + isLogsFilterEnabled: isLogsFilterEnabled ?? this.isLogsFilterEnabled, + attachLogFileEnabled: attachLogFileEnabled ?? this.attachLogFileEnabled, + ); + + @override + List get props => [ + isBugseeEnabled, + isVideoCaptureEnabled, + isDataObscured, + isLogCollectionEnabled, + isLogsFilterEnabled, + attachLogFileEnabled, + ]; } diff --git a/src/app/lib/access/bugsee/bugsee_repository.dart b/src/app/lib/access/bugsee/bugsee_repository.dart index 3f270eb3..c78c8dcc 100644 --- a/src/app/lib/access/bugsee/bugsee_repository.dart +++ b/src/app/lib/access/bugsee/bugsee_repository.dart @@ -13,18 +13,40 @@ abstract interface class BugseeRepository { /// Update the current video captured or not flag in shared prefs. Future setIsVideoCaptureEnabled(bool isVideoCaptureEnabled); + + /// Update whether data is obscured in shared prefs. + Future setIsDataObscure(bool isDataObscure); + + /// Update the logCollection flag in shared prefs. + Future setIsLogCollectionEnabled(bool isLogCollectionEnabled); + + /// Update the logFilter flag in shared prefs. + Future setIsLogFilterEnabled(bool isLogFilterEnabled); + + /// Update the attachFile boolean flag in shared prefs. + Future setAttachLogFileEnabled(bool attachLogFile); } final class _BugseeRepository implements BugseeRepository { final String _bugseeEnabledKey = 'bugseeEnabledKey'; - final String _videoCaptureKey = 'videoCaptureKey'; + final String _bugseeVideoCaptureKey = 'bugseeVideoCaptureKey'; + final String _bugseeDataObscureKey = 'bugseeDataObscureKey'; + final String _bugseeDisableLogCollectionKey = 'bugseeDisableLogCollectionKey'; + final String _bugseeDisableLogFilterKey = 'bugseeDisableLogFilterKey'; + final String _bugseeAttachLogFileKey = 'bugseeAttachLogFileKey'; @override Future getBugseeConfiguration() async { final sharedPrefInstance = await SharedPreferences.getInstance(); return BugseeConfigurationData( isBugseeEnabled: sharedPrefInstance.getBool(_bugseeEnabledKey), - isVideoCaptureEnabled: sharedPrefInstance.getBool(_videoCaptureKey), + isVideoCaptureEnabled: sharedPrefInstance.getBool(_bugseeVideoCaptureKey), + isDataObscured: sharedPrefInstance.getBool(_bugseeDataObscureKey), + isLogCollectionEnabled: + sharedPrefInstance.getBool(_bugseeDisableLogCollectionKey), + isLogsFilterEnabled: + sharedPrefInstance.getBool(_bugseeDisableLogFilterKey), + attachLogFileEnabled: sharedPrefInstance.getBool(_bugseeAttachLogFileKey), ); } @@ -49,13 +71,80 @@ final class _BugseeRepository implements BugseeRepository { final sharedPrefInstance = await SharedPreferences.getInstance(); bool isSaved = await sharedPrefInstance.setBool( - _videoCaptureKey, + _bugseeVideoCaptureKey, isVideoCaptureEnabled, ); if (!isSaved) { throw PersistenceException( - message: 'Error while setting $_videoCaptureKey $isVideoCaptureEnabled', + message: + 'Error while setting $_bugseeVideoCaptureKey $isVideoCaptureEnabled', + ); + } + } + + @override + Future setIsDataObscure(bool isDataObscured) async { + final sharedPrefInstance = await SharedPreferences.getInstance(); + + bool isSaved = await sharedPrefInstance.setBool( + _bugseeDataObscureKey, + isDataObscured, + ); + + if (!isSaved) { + throw PersistenceException( + message: 'Error while setting $_bugseeDataObscureKey $isDataObscured', + ); + } + } + + @override + Future setIsLogCollectionEnabled(bool isLogCollected) async { + final sharedPrefInstance = await SharedPreferences.getInstance(); + + bool isSaved = await sharedPrefInstance.setBool( + _bugseeDisableLogCollectionKey, + isLogCollected, + ); + + if (!isSaved) { + throw PersistenceException( + message: + 'Error while setting $_bugseeDisableLogCollectionKey $isLogCollected', + ); + } + } + + @override + Future setIsLogFilterEnabled(bool isLogFilterEnabled) async { + final sharedPrefInstance = await SharedPreferences.getInstance(); + + bool isSaved = await sharedPrefInstance.setBool( + _bugseeDisableLogFilterKey, + isLogFilterEnabled, + ); + + if (!isSaved) { + throw PersistenceException( + message: + 'Error while setting $_bugseeDisableLogFilterKey $isLogFilterEnabled', + ); + } + } + + @override + Future setAttachLogFileEnabled(bool attachLogFile) async { + final sharedPrefInstance = await SharedPreferences.getInstance(); + + bool isSaved = await sharedPrefInstance.setBool( + _bugseeAttachLogFileKey, + attachLogFile, + ); + + if (!isSaved) { + throw PersistenceException( + message: 'Error while setting $_bugseeAttachLogFileKey $attachLogFile', ); } } diff --git a/src/app/lib/business/bugsee/bugsee_config_state.dart b/src/app/lib/business/bugsee/bugsee_config_state.dart new file mode 100644 index 00000000..8276b309 --- /dev/null +++ b/src/app/lib/business/bugsee/bugsee_config_state.dart @@ -0,0 +1,107 @@ +import 'package:equatable/equatable.dart'; + +enum ConfigErrorEnum { + invalidReleaseMode(error: 'Bugsee is disabled in debug mode'), + invalidToken(error: 'Invalid token, cannot start Bugsee reporting'), + invalidPlatform(error: 'Bugsee cannot be configured on this platform'); + + final String error; + const ConfigErrorEnum({ + required this.error, + }); +} + +final class BugseeConfigState extends Equatable { + /// Indicate if the app require a restart to reactivate the bugsee configurations + /// + /// `true` only if `isConfigurationValid == true` and bugsee is turned on + final bool isRestartRequired; + + /// Indicate if bugsee is enabled or not + /// by default bugsee is enabled if `isConfigurationValid == true`. + final bool isBugseeEnabled; + + /// Indicate whether video capturing is enabled or not. + /// enabled by default if `isBugseeEnabled == true`. + /// + /// cannot be true if `isBugseeEnabled == false`. + final bool isVideoCaptureEnabled; + + /// Indicate if bugsee configuration is valid + /// config is valid if app in release mode and the provided token is valid + /// following the [bugseeTokenFormat] regex. + final bool isConfigurationValid; + + /// Indicate whether data is obscured in report videos + /// + /// cannot be true if `isBugseeEnabled == false`. + final bool isDataObscured; + + /// Indicate whether log will be collected during Bugsee reporting or not + /// by default logs are collected but filterd. + /// + /// This value is initialized from [dotenv.env] and shared prefs storage. + final bool isLogCollectionEnabled; + + /// Indicate whether log will be filterd or not + /// by default all logs are filted using [bugseeFilterRegex] defined in [BugseeManager] + /// + /// This value is initialized from [dotenv.env] map and shared prefs storage. + final bool isLogFilterEnabled; + + /// Indicate whether Bugsee will attach the log file when reporting crashes/exceptions + /// or not + /// + /// The initial value is taken from [dotenv.env] and shared prefs. + /// By default it's enabled. + final bool attachLogFile; + + /// Indicate the configuration error type (debug, invalid token or invalid platform) + final ConfigErrorEnum? configErrorEnum; + + const BugseeConfigState({ + this.isRestartRequired = false, + this.isBugseeEnabled = false, + this.isVideoCaptureEnabled = false, + this.isConfigurationValid = false, + this.isDataObscured = false, + this.isLogCollectionEnabled = false, + this.isLogFilterEnabled = false, + this.attachLogFile = false, + this.configErrorEnum, + }); + + BugseeConfigState copyWith({ + bool? isRestartRequired, + bool? isBugseeEnabled, + bool? isVideoCaptureEnabled, + bool? isConfigurationValid, + bool? isDataObscured, + bool? isLogCollectionEnabled, + bool? isLogFilterEnabled, + bool? attachLogFile, + ConfigErrorEnum? configErrorEnum, + }) => + BugseeConfigState( + isRestartRequired: isRestartRequired ?? this.isRestartRequired, + isBugseeEnabled: isBugseeEnabled ?? this.isBugseeEnabled, + isConfigurationValid: isConfigurationValid ?? this.isConfigurationValid, + isDataObscured: isDataObscured ?? this.isDataObscured, + isLogFilterEnabled: isLogFilterEnabled ?? this.isLogFilterEnabled, + attachLogFile: attachLogFile ?? this.attachLogFile, + isLogCollectionEnabled: + isLogCollectionEnabled ?? this.isLogCollectionEnabled, + isVideoCaptureEnabled: + isVideoCaptureEnabled ?? this.isVideoCaptureEnabled, + configErrorEnum: configErrorEnum ?? this.configErrorEnum, + ); + + @override + List get props => [ + isRestartRequired, + isBugseeEnabled, + isVideoCaptureEnabled, + isConfigurationValid, + isDataObscured, + ]; +} diff --git a/src/app/lib/business/bugsee/bugsee_manager.dart b/src/app/lib/business/bugsee/bugsee_manager.dart index 9493fdad..592fc5e4 100644 --- a/src/app/lib/business/bugsee/bugsee_manager.dart +++ b/src/app/lib/business/bugsee/bugsee_manager.dart @@ -1,42 +1,26 @@ import 'dart:async'; import 'dart:io'; - import 'package:app/access/bugsee/bugsee_configuration_data.dart'; import 'package:app/access/bugsee/bugsee_repository.dart'; +import 'package:app/access/persistence_exception.dart'; +import 'package:app/business/bugsee/bugsee_config_state.dart'; +import 'package:app/business/logger/logger_manager.dart'; import 'package:bugsee_flutter/bugsee_flutter.dart'; import 'package:flutter/foundation.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:logger/logger.dart'; import 'package:logger/web.dart'; const String bugseeTokenFormat = r'^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'; +const String bugseeFilterRegex = r'.'; /// Service related to initializing Bugsee service abstract interface class BugseeManager { - factory BugseeManager({ - required Logger logger, - required BugseeRepository bugseeRepository, - }) = _BugseeManager; - - /// Indicate if the app require a restart to reactivate the bugsee configurations - /// - /// `true` only if `isConfigurationValid == true` and bugsee is turned on - bool get isRestartRequired; - - /// Indicate if bugsee is enabled or not - /// by default bugsee is enabled if `isConfigurationValid == true`. - bool get isBugseeEnabled; - - /// Indicate whether video capturing is enabled or not. - /// enabled by default if `isBugseeEnabled == true`. - /// - /// cannot be true if `isBugseeEnabled == false`. - bool get isVideoCaptureEnabled; + factory BugseeManager() = _BugseeManager; - /// Indicate if bugsee configuration is valid - /// config is valid if app in release mode and the provided token is valid - /// following the [bugseeTokenFormat] regex. - bool get isConfigurationValid; + /// Current BugseeManager state + BugseeConfigState get bugseeConfigState; /// Initialize bugsee with given token /// bugsee is not available in debug mode @@ -44,91 +28,183 @@ abstract interface class BugseeManager { /// [BUGSEE_TOKEN] in the env using `--dart-define` or `launch.json` on vscode Future initialize({ String? bugseeToken, + bool isMock, + required Logger logger, + required LoggerManager loggerManager, + required BugseeRepository bugseeRepository, }); /// Manually log a provided exception with a stack trace /// (medium severity exception in Bugsee dashboard) + /// + ///* exception: the exception instance that will be reported + ///* stackTrace: the strack trace of the exception, by default it's null + ///* traces: the traces that led to the exception. + ///* events: the events where the exception has been caught. Future logException({ required Exception exception, StackTrace? stackTrace, - }); - - /// Manually log an unhandled exception with a stack trace - /// (critical severity exception in Bugsee dashboard) - Future logUnhandledException({ - required Exception exception, - StackTrace? stackTrace, + Map traces, }); /// Manually update the current BugseeEnabled flag in shared prefs and in current manager singleton. - Future setIsBugseeEnabled(bool isBugseeEnabled); + Future setIsBugseeEnabled(bool value); + + /// Manually update isDataObscured flag in shared prefs and in current state [bugseeConfigState]. + Future setIsDataObscured(bool value); /// Manually update the current enableVideoCapture flag in shared prefs and in current manager singleton. - Future setIsVideoCaptureEnabled(bool isBugseeEnabled); + Future setIsVideoCaptureEnabled(bool value); /// Manually shows the built-in capture log report screen of Bugsee. Future showCaptureLogReport(); -} -final class _BugseeManager implements BugseeManager { - final Logger logger; - final BugseeRepository bugseeRepository; + /// Manually update whether logs will be collected or not flag in shared prefs. + Future setIsLogsCollectionEnabled(bool value); - _BugseeManager({ - required this.logger, - required this.bugseeRepository, - }); + /// Manually update isLogFilterEnabled flag in shared prefs. + Future setIsLogFilterEnabeld(bool value); - @override - bool isRestartRequired = false; + /// Manually update whether Bugsee attach the log file with the reported + /// exception or not. + /// + /// By default the log file is attached + Future setAttachLogFileEnabled(bool value); - @override - bool isBugseeEnabled = false; + /// Intecept all unhandled exception thrown by the dart framework + Future inteceptExceptions(Object error, StackTrace stackTrace); - @override - late bool isVideoCaptureEnabled = false; + /// Intercept all unhandled rending exception thrown by the Flutter framework + Future inteceptRenderingExceptions(FlutterErrorDetails error); + + /// Manually add a map of attributes + /// - the map entry key is the attribute name + /// - the map entry value is the attribute value (string, int, boolean) + /// + /// Attributes will be attached to all reported exception unless app is uninstalled + /// or attributes are removed manually + Future addAttributes(Map attributes); + + /// Manually add an email attached to all reported. + /// + /// The email will be attached to all reported exception unless app is uninstalled + /// or the email is removed manually. + Future addEmailAttribute(String email); + + /// Manually remove the email attribute attached using [addEmailAttribute] + Future clearEmailAttribute(); + + /// Manually remove an attribute by the given key attached using [addAttributes] + Future clearAttribute(String attribute); + + /// Manually log Bugsee events that will be attached to the reported issues + void logEvents(Map> events); +} + +final class _BugseeManager implements BugseeManager { + late Logger logger; + late LoggerManager loggerManager; + late BugseeRepository bugseeRepository; + + _BugseeManager(); + + BugseeConfigState _currentState = const BugseeConfigState(); @override - bool isConfigurationValid = true; + BugseeConfigState get bugseeConfigState => _currentState; late bool _isBugSeeInitialized; + late BugseeConfigurationData configurationData; + BugseeLaunchOptions? launchOptions; @override Future initialize({ String? bugseeToken, + bool isMock = false, + required Logger logger, + required LoggerManager loggerManager, + required BugseeRepository bugseeRepository, }) async { - BugseeConfigurationData bugseeConfigurationData = - await bugseeRepository.getBugseeConfiguration(); + this.logger = logger; + this.loggerManager = loggerManager; + this.bugseeRepository = bugseeRepository; + + if (!Platform.isIOS && !Platform.isAndroid) { + _currentState = _currentState.copyWith( + isConfigurationValid: false, + configErrorEnum: ConfigErrorEnum.invalidPlatform, + ); + logger.i("BUGSEE: ${_currentState.configErrorEnum?.error}"); + return; + } + + configurationData = await bugseeRepository.getBugseeConfiguration(); + configurationData = configurationData.copyWith( + isLogCollectionEnabled: configurationData.isLogCollectionEnabled ?? + bool.parse( + dotenv.env['BUGSEE_DISABLE_LOG_COLLECTION'] ?? 'true', + ), + isLogsFilterEnabled: configurationData.isLogsFilterEnabled ?? + bool.parse( + dotenv.env['BUGSEE_FILTER_LOG_COLLECTION'] ?? 'true', + ), + isDataObscured: configurationData.isDataObscured ?? + bool.parse(dotenv.env['BUGSEE_IS_DATA_OBSCURE'] ?? 'true'), + attachLogFileEnabled: configurationData.attachLogFileEnabled ?? + bool.parse(dotenv.env['BUGSEE_ATTACH_LOG_FILE'] ?? 'true'), + ); launchOptions = _initializeLaunchOptions(); _isBugSeeInitialized = false; + if (isMock) { + _initializeBugsee(bugseeToken ?? ''); + return; + } + if (kDebugMode) { - isConfigurationValid = false; - logger.i("BUGSEE: deactivated in debug mode"); + _currentState = _currentState.copyWith( + isConfigurationValid: false, + configErrorEnum: ConfigErrorEnum.invalidReleaseMode, + ); + logger.i("BUGSEE: ${_currentState.configErrorEnum?.error}"); return; } if (bugseeToken == null || !RegExp(bugseeTokenFormat).hasMatch(bugseeToken)) { - isConfigurationValid = false; + _currentState = _currentState.copyWith( + isConfigurationValid: false, + configErrorEnum: ConfigErrorEnum.invalidToken, + ); logger.i( - "BUGSEE: token is null or invalid, bugsee won't be initialized", + "BUGSEE: ${_currentState.configErrorEnum?.error}", ); return; } + _initializeBugsee(bugseeToken); + } - if (bugseeConfigurationData.isBugseeEnabled ?? true) { - await _launchBugseeLogger(bugseeToken); + void _initializeBugsee(String bugseeToken) async { + if (configurationData.isBugseeEnabled ?? true) { + _isBugSeeInitialized = await _launchBugseeLogger(bugseeToken); } - isBugseeEnabled = _isBugSeeInitialized; - isVideoCaptureEnabled = _isBugSeeInitialized && - (bugseeConfigurationData.isVideoCaptureEnabled ?? true); + _currentState = _currentState.copyWith( + isConfigurationValid: _isBugSeeInitialized, + isBugseeEnabled: _isBugSeeInitialized, + isVideoCaptureEnabled: _isBugSeeInitialized && + (configurationData.isVideoCaptureEnabled ?? true), + isDataObscured: configurationData.isDataObscured, + isLogFilterEnabled: configurationData.isLogsFilterEnabled, + isLogCollectionEnabled: configurationData.isLogCollectionEnabled, + attachLogFile: configurationData.attachLogFileEnabled, + ); } - Future _launchBugseeLogger(String bugseeToken) async { + Future _launchBugseeLogger(String bugseeToken) async { + bool isInitialized = false; HttpOverrides.global = Bugsee.defaultHttpOverrides; await Bugsee.launch( bugseeToken, @@ -138,51 +214,75 @@ final class _BugseeManager implements BugseeManager { "BUGSEE: not initialized, verify bugsee token configuration", ); } - _isBugSeeInitialized = isBugseeLaunched; + isInitialized = isBugseeLaunched; }, launchOptions: launchOptions, ); + if (configurationData.isLogsFilterEnabled ?? false) { + Bugsee.setLogFilter(_filterBugseeLogs); + } + if (configurationData.attachLogFileEnabled ?? false) { + Bugsee.setAttachmentsCallback(_attachLogFile); + } + return isInitialized; } BugseeLaunchOptions? _initializeLaunchOptions() { if (Platform.isAndroid) { - return AndroidLaunchOptions(); + return AndroidLaunchOptions() + ..captureLogs = (configurationData.isLogCollectionEnabled ?? true); } else if (Platform.isIOS) { - return IOSLaunchOptions(); + return IOSLaunchOptions() + ..captureLogs = (configurationData.isLogCollectionEnabled ?? true); } return null; } - @override - Future logException({ - required Exception exception, - StackTrace? stackTrace, - }) async { - if (isBugseeEnabled) { - await Bugsee.logException(exception, stackTrace); + Future _filterBugseeLogs(BugseeLogEvent logEvent) async { + logEvent.text = logEvent.text.replaceAll(RegExp(bugseeFilterRegex), ''); + return logEvent; + } + + Future> _attachLogFile(BugseeReport report) async { + var attachments = []; + if (loggerManager.logFile != null) { + attachments.add( + BugseeAttachment( + "logFile", + loggerManager.logFile!.path, + loggerManager.logFile!.readAsBytesSync(), + ), + ); } + return attachments; } @override - Future logUnhandledException({ + Future logException({ required Exception exception, StackTrace? stackTrace, + Map traces = const {}, }) async { - if (isBugseeEnabled) { - await Bugsee.logUnhandledException(exception); + if (_currentState.isBugseeEnabled) { + for (var trace in traces.entries) { + await Bugsee.trace(trace.key, trace.value); + } + await Bugsee.logException(exception, stackTrace); } } @override Future setIsBugseeEnabled(bool value) async { - if (isConfigurationValid) { - isBugseeEnabled = value; - await bugseeRepository.setIsBugseeEnabled(isBugseeEnabled); - - isRestartRequired = _isBugSeeInitialized && isBugseeEnabled; - isVideoCaptureEnabled = isBugseeEnabled; + if (_currentState.isConfigurationValid) { + await bugseeRepository.setIsBugseeEnabled(value); + _currentState = _currentState.copyWith( + isBugseeEnabled: value, + isRestartRequired: value, + isVideoCaptureEnabled: value, + isDataObscured: value, + ); - if (!isRestartRequired) { + if (!_currentState.isRestartRequired) { await Bugsee.stop(); } } @@ -190,10 +290,14 @@ final class _BugseeManager implements BugseeManager { @override Future setIsVideoCaptureEnabled(bool value) async { - if (isBugseeEnabled) { - isVideoCaptureEnabled = value; - await bugseeRepository.setIsVideoCaptureEnabled(isVideoCaptureEnabled); - if (!isVideoCaptureEnabled) { + if (_currentState.isBugseeEnabled) { + _currentState = _currentState.copyWith( + isVideoCaptureEnabled: value, + ); + await bugseeRepository.setIsVideoCaptureEnabled( + _currentState.isVideoCaptureEnabled, + ); + if (!_currentState.isVideoCaptureEnabled) { await Bugsee.pause(); } else { await Bugsee.resume(); @@ -203,8 +307,112 @@ final class _BugseeManager implements BugseeManager { @override Future showCaptureLogReport() async { - if (isBugseeEnabled) { + if (_currentState.isBugseeEnabled) { await Bugsee.showReportDialog(); } } + + @override + Future setIsDataObscured(bool value) async { + if (_currentState.isBugseeEnabled) { + await bugseeRepository.setIsDataObscure(value); + _currentState = _currentState.copyWith( + isRestartRequired: value != configurationData.isDataObscured, + isDataObscured: value, + ); + } + } + + @override + Future setIsLogsCollectionEnabled(bool value) async { + if (_currentState.isBugseeEnabled) { + await bugseeRepository.setIsLogCollectionEnabled(value); + _currentState = _currentState.copyWith( + isRestartRequired: value != configurationData.isLogCollectionEnabled, + isLogCollectionEnabled: value, + ); + if (!value) { + _currentState = _currentState.copyWith( + isLogFilterEnabled: false, + ); + } + } + } + + @override + Future setIsLogFilterEnabeld(bool value) async { + if (_currentState.isBugseeEnabled && _currentState.isLogCollectionEnabled) { + await bugseeRepository.setIsLogFilterEnabled(value); + _currentState = _currentState.copyWith( + isRestartRequired: value != configurationData.isLogsFilterEnabled, + isLogFilterEnabled: value, + ); + } + } + + @override + Future setAttachLogFileEnabled(bool value) async { + if (_currentState.isBugseeEnabled) { + await bugseeRepository.setAttachLogFileEnabled(value); + _currentState = _currentState.copyWith( + isRestartRequired: value != configurationData.attachLogFileEnabled, + attachLogFile: value, + ); + } + } + + @override + Future inteceptExceptions( + Object error, + StackTrace stackTrace, + ) async { + String? message = switch (error.runtimeType) { + const (PersistenceException) => (error as PersistenceException).message, + _ => null, + }; + await logException( + exception: Exception(error), + stackTrace: stackTrace, + traces: { + 'message': message, + }, + ); + } + + @override + Future addAttributes(Map attributes) async { + for (var attribute in attributes.entries) { + await Bugsee.setAttribute(attribute.key, attribute.value); + } + } + + @override + Future clearAttribute(String attribute) async { + await Bugsee.clearAttribute(attribute); + } + + @override + Future addEmailAttribute(String email) async { + await Bugsee.setEmail(email); + } + + @override + Future clearEmailAttribute() async { + await Bugsee.clearEmail(); + } + + @override + Future inteceptRenderingExceptions(FlutterErrorDetails error) async { + await logException( + exception: Exception(error.exception), + stackTrace: error.stack, + ); + } + + @override + void logEvents(Map> events) async { + for (var event in events.entries) { + Bugsee.event(event.key, event.value); + } + } } diff --git a/src/app/lib/business/logger/logger_manager.dart b/src/app/lib/business/logger/logger_manager.dart index 47f0a8d7..9b39ff22 100644 --- a/src/app/lib/business/logger/logger_manager.dart +++ b/src/app/lib/business/logger/logger_manager.dart @@ -18,6 +18,8 @@ abstract interface class LoggerManager { required Alice alice, }) = _LoggerManager; + File? get logFile; + /// Gets whether console logging is enabled. bool get isConsoleLoggingEnabled; @@ -54,6 +56,9 @@ final class _LoggerManager implements LoggerManager { File? _logFile; + @override + File? logFile; + final String _fileName = "ApplicationTemplate.log"; late bool _initialIsConsoleLoggingEnabled; @@ -97,6 +102,7 @@ final class _LoggerManager implements LoggerManager { final Directory appDocumentsDir = await getApplicationDocumentsDirectory(); _logFile = File('${appDocumentsDir.path}/$_fileName'); + logFile = _logFile; loggerOutputs.add(CustomFileOutput(file: _logFile!)); } isFileLoggingEnabled = _initialIsFileLoggingEnabled; @@ -126,6 +132,7 @@ final class _LoggerManager implements LoggerManager { _logger.t("Log file exists. It's time to delete it."); await _logFile!.delete(); + logFile = null; _logger.i("The log file was deleted successfully."); return true; diff --git a/src/app/lib/main.dart b/src/app/lib/main.dart index d3d805f6..8b55bcfe 100644 --- a/src/app/lib/main.dart +++ b/src/app/lib/main.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'package:alice/alice.dart'; @@ -36,15 +37,23 @@ import 'package:logger/logger.dart'; late Logger _logger; Future main() async { - await initializeComponents(); - runApp(const App()); + _initializeBugseeManager(); + runZonedGuarded( + () async { + FlutterError.onError = + GetIt.I.get().inteceptRenderingExceptions; + await initializeComponents(); + await registerBugseeManager(); + runApp(const App()); + }, + GetIt.I.get().inteceptExceptions, + ); } Future initializeComponents({bool? isMocked}) async { WidgetsFlutterBinding.ensureInitialized(); await _registerAndLoadEnvironment(); await _registerAndLoadLoggers(); - await _registerBugseeManager(); _logger.d("Initialized environment and logger."); @@ -119,16 +128,24 @@ Future _registerAndLoadLoggers() async { GetIt.I.registerSingleton(_logger); } -Future _registerBugseeManager() async { +void _initializeBugseeManager() { GetIt.I.registerSingleton(BugseeRepository()); GetIt.I.registerSingleton( - BugseeManager( - logger: GetIt.I.get(), - bugseeRepository: GetIt.I.get(), - ), + BugseeManager(), ); +} + +Future registerBugseeManager({bool? isMock, String? bugseeToken}) async { + if (!GetIt.I.isRegistered()) { + _initializeBugseeManager(); + } GetIt.I.get().initialize( - bugseeToken: const String.fromEnvironment('BUGSEE_TOKEN'), + bugseeToken: + bugseeToken ?? const String.fromEnvironment('BUGSEE_TOKEN'), + logger: GetIt.I.get(), + loggerManager: GetIt.I.get(), + bugseeRepository: GetIt.I.get(), + isMock: isMock ?? false, ); } diff --git a/src/app/lib/presentation/dad_jokes/dad_joke_list_item.dart b/src/app/lib/presentation/dad_jokes/dad_joke_list_item.dart index 3c6bcd77..78811cb8 100644 --- a/src/app/lib/presentation/dad_jokes/dad_joke_list_item.dart +++ b/src/app/lib/presentation/dad_jokes/dad_joke_list_item.dart @@ -1,5 +1,7 @@ +import 'package:app/business/bugsee/bugsee_manager.dart'; import 'package:app/business/dad_jokes/dad_joke.dart'; import 'package:app/business/dad_jokes/dad_jokes_service.dart'; +import 'package:bugsee_flutter/bugsee_flutter.dart'; import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; @@ -7,6 +9,7 @@ import 'package:get_it/get_it.dart'; final class DadJokeListItem extends StatelessWidget { /// The dad jokes service used to add or remove favorite. final _dadJokesService = GetIt.I(); + final _bugseeManager = GetIt.I(); /// The dad joke. final DadJoke dadJoke; @@ -17,8 +20,14 @@ final class DadJokeListItem extends StatelessWidget { Widget build(BuildContext context) { return Card( child: ListTile( - title: Text(dadJoke.title), - subtitle: Text(dadJoke.text), + title: BugseeSecureView( + enabled: _bugseeManager.bugseeConfigState.isDataObscured, + child: Text(dadJoke.title), + ), + subtitle: BugseeSecureView( + enabled: _bugseeManager.bugseeConfigState.isDataObscured, + child: Text(dadJoke.text), + ), trailing: dadJoke.isFavorite ? const Icon( Icons.favorite, diff --git a/src/app/lib/presentation/diagnostic/bugsee_configuration_widget.dart b/src/app/lib/presentation/diagnostic/bugsee_configuration_widget.dart index 3952cbf5..8c95f58a 100644 --- a/src/app/lib/presentation/diagnostic/bugsee_configuration_widget.dart +++ b/src/app/lib/presentation/diagnostic/bugsee_configuration_widget.dart @@ -1,7 +1,9 @@ +import 'dart:math'; + +import 'package:app/business/bugsee/bugsee_config_state.dart'; import 'package:app/business/bugsee/bugsee_manager.dart'; import 'package:app/presentation/diagnostic/diagnostic_button.dart'; import 'package:app/presentation/diagnostic/diagnostic_switch.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; @@ -16,16 +18,12 @@ class BugseeConfigurationWidget extends StatefulWidget { class _BugseeConfigurationWidgetState extends State { final BugseeManager bugseeManager = GetIt.I.get(); - late bool isConfigEnabled; - late bool isCaptureVideoEnabled; - late bool requireRestart; + late BugseeConfigState state; @override void initState() { super.initState(); - isConfigEnabled = bugseeManager.isBugseeEnabled; - isCaptureVideoEnabled = bugseeManager.isVideoCaptureEnabled; - requireRestart = bugseeManager.isRestartRequired; + state = bugseeManager.bugseeConfigState; } @override @@ -34,14 +32,12 @@ class _BugseeConfigurationWidgetState extends State { children: [ Column( children: [ - if (!bugseeManager.isConfigurationValid) + if (!state.isConfigurationValid) Container( color: const Color.fromARGB(170, 255, 0, 0), - child: const Text( - kDebugMode - ? "Bugsee is disabled in debug mode." - : "Invalid Bugsee token, capturing exceptions could not start", - style: TextStyle( + child: Text( + state.configErrorEnum?.error ?? '', + style: const TextStyle( color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold, @@ -49,11 +45,11 @@ class _BugseeConfigurationWidgetState extends State { ), ), ), - if (requireRestart) + if (state.isRestartRequired) Container( color: const Color.fromARGB(170, 255, 0, 0), child: const Text( - "In order to reactivate Bugsee logger restart the app.", + "Bugsee configuration changed. Please restart the application to apply the changes.", style: TextStyle( color: Colors.white, fontSize: 20, @@ -64,23 +60,61 @@ class _BugseeConfigurationWidgetState extends State { ), DiagnosticSwitch( label: 'Bugsee enabled', - value: isConfigEnabled, + value: state.isBugseeEnabled, onChanged: (value) async { await bugseeManager.setIsBugseeEnabled(value); setState(() { - isConfigEnabled = bugseeManager.isBugseeEnabled; - isCaptureVideoEnabled = bugseeManager.isVideoCaptureEnabled; - requireRestart = bugseeManager.isRestartRequired; + state = bugseeManager.bugseeConfigState; }); }, ), DiagnosticSwitch( label: 'Video capture enabled', - value: isCaptureVideoEnabled, + value: state.isVideoCaptureEnabled, onChanged: (value) async { await bugseeManager.setIsVideoCaptureEnabled(value); setState(() { - isCaptureVideoEnabled = bugseeManager.isVideoCaptureEnabled; + state = bugseeManager.bugseeConfigState; + }); + }, + ), + DiagnosticSwitch( + label: 'Obscure data', + value: state.isDataObscured, + onChanged: (value) async { + await bugseeManager.setIsDataObscured(value); + setState(() { + state = bugseeManager.bugseeConfigState; + }); + }, + ), + DiagnosticSwitch( + label: 'Log collection enabled', + value: state.isLogCollectionEnabled, + onChanged: (value) async { + await bugseeManager.setIsLogsCollectionEnabled(value); + setState(() { + state = bugseeManager.bugseeConfigState; + }); + }, + ), + DiagnosticSwitch( + label: 'Filter log enabled', + value: state.isLogFilterEnabled, + onChanged: (value) async { + await bugseeManager.setIsLogFilterEnabeld(value); + setState(() { + state = bugseeManager.bugseeConfigState; + }); + }, + ), + DiagnosticSwitch( + label: 'Log file attached', + value: state.attachLogFile, + onChanged: (value) async { + bugseeManager.setAttachLogFileEnabled(value); + setState(() { + state = bugseeManager.bugseeConfigState; }); }, ), @@ -93,9 +127,42 @@ class _BugseeConfigurationWidgetState extends State { }, ), DiagnosticButton( - label: 'Log an unhandled exception', + label: 'Add events to the exception', + onPressed: () { + bugseeManager.logEvents( + { + 'data': { + 'date': DateTime.now().millisecondsSinceEpoch, + 'id': Random().nextInt(20), + }, + }, + ); + }, + ), + DiagnosticButton( + label: 'Add email attributes', + onPressed: () { + bugseeManager.addEmailAttribute('john.doe@nventive.com'); + }, + ), + DiagnosticButton( + label: 'Add name attribute', + onPressed: () { + bugseeManager.addAttributes({ + 'name': 'John Doe', + }); + }, + ), + DiagnosticButton( + label: 'Clear email attribute', + onPressed: () { + bugseeManager.clearEmailAttribute(); + }, + ), + DiagnosticButton( + label: 'Clear name attribute', onPressed: () { - bugseeManager.logUnhandledException(exception: Exception()); + bugseeManager.clearAttribute('name'); }, ), DiagnosticButton( diff --git a/src/cli/CHANGELOG.md b/src/cli/CHANGELOG.md index a5ec128b..01b8b9c3 100644 --- a/src/cli/CHANGELOG.md +++ b/src/cli/CHANGELOG.md @@ -5,6 +5,18 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) Prefix your items with `(Template)` if the change is about the template and not the resulting application. +## 0.22.0 +- Add bugsee global inteceptor for dart exceptions to template app +- Add bugsee global inteceptor for flutter layout exceptions to template app +- Add bugsee custom attributes logging +- Add bugsee custom events logging +- Add bugsee custom traces logging +- Implement log file attach to reported bugsee exceptions +- Implement obsucre data feature from reported videos +- Implement Bugsee enable log and log filter features +- Update diagnostic overlay to test Bugsee advanced features + + ## 0.21.0 - Add bugsee sdk in Fluttter template - Update build stage in `steps-build-android.yml` and `steps-build-ios` providing bugsee token