diff --git a/.vscode/launch.json b/.vscode/launch.json index 37ad430..dc9aba8 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -28,7 +28,9 @@ "program": "src/app/lib/main.dart", "toolArgs": [ "--dart-define", - "ENV=Staging" + "ENV=Staging", + "--dart-define", + "BUGSEE_TOKEN=" ] }, { diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index a4ce924..c3d7727 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -86,6 +86,7 @@ stages: firebaseJsonFile: $(InternalFirebaseJson) firebaseOptionsDartFile: $(InternalFirebaseOptionsDart) googleServicesJsonFile: $(InternalGoogleServicesJson) + bugseeVariableGroup: 'FlutterApplicationTemplate.Bugsee.Tokens' - stage: AppCenter_TestFlight_Staging condition: and(succeeded(), eq(variables['IsPullRequestBuild'], 'false')) diff --git a/build/stage-build.yml b/build/stage-build.yml index 6ebe055..0e0b6b1 100644 --- a/build/stage-build.yml +++ b/build/stage-build.yml @@ -56,6 +56,9 @@ type: string - name: googleServicesJsonFile type: string +- name: bugseeVariableGroup + type: string + default: '' jobs: - job: OnWindows_ReleaseNotes diff --git a/build/steps-build-android.yml b/build/steps-build-android.yml index 30600cc..cb1e4d8 100644 --- a/build/steps-build-android.yml +++ b/build/steps-build-android.yml @@ -89,7 +89,7 @@ steps: profileMode: false projectDirectory: '${{ parameters.pathToSrc }}/app' verboseMode: true - dartDefine: ENV=$(applicationEnvironment) + dartDefineMulti: ENV=$(applicationEnvironment) BUGSEE_TOKEN=$(AndroidBugseeToken) - template: templates/flutter-diagnostics.yml parameters: diff --git a/build/steps-build-ios.yml b/build/steps-build-ios.yml index d19eed7..2c11ba7 100644 --- a/build/steps-build-ios.yml +++ b/build/steps-build-ios.yml @@ -95,7 +95,7 @@ steps: projectDirectory: '${{ parameters.pathToSrc }}/app' verboseMode: true exportOptionsPlist: '$(exportOptions.secureFilePath)' - dartDefine: ENV=$(applicationEnvironment) + dartDefineMulti: ENV=$(applicationEnvironment) BUGSEE_TOKEN=$(iOSBugseeToken) - template: templates/flutter-diagnostics.yml parameters: diff --git a/src/app/.gitignore b/src/app/.gitignore index bb0c89f..9f864c4 100644 --- a/src/app/.gitignore +++ b/src/app/.gitignore @@ -33,4 +33,4 @@ app.*.symbols app.*.map.json # Localization related -lib/l10n/gen_l10n/* +lib/l10n/gen_l10n/* \ No newline at end of file diff --git a/src/app/lib/access/bugsee/bugsee_configuration_data.dart b/src/app/lib/access/bugsee/bugsee_configuration_data.dart new file mode 100644 index 0000000..1255c78 --- /dev/null +++ b/src/app/lib/access/bugsee/bugsee_configuration_data.dart @@ -0,0 +1,12 @@ +final class BugseeConfigurationData { + /// 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. + final bool? isVideoCaptureEnabled; + + const BugseeConfigurationData({ + required this.isBugseeEnabled, + required this.isVideoCaptureEnabled, + }); +} diff --git a/src/app/lib/access/bugsee/bugsee_repository.dart b/src/app/lib/access/bugsee/bugsee_repository.dart new file mode 100644 index 0000000..3f270eb --- /dev/null +++ b/src/app/lib/access/bugsee/bugsee_repository.dart @@ -0,0 +1,62 @@ +import 'package:app/access/bugsee/bugsee_configuration_data.dart'; +import 'package:app/access/persistence_exception.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +abstract interface class BugseeRepository { + factory BugseeRepository() = _BugseeRepository; + + /// Load the current bugsee configuration stored in shared prefs. + Future getBugseeConfiguration(); + + /// Update the current Bugsee enabled flag in shared prefs. + Future setIsBugseeEnabled(bool isBugseeEnabled); + + /// Update the current video captured or not flag in shared prefs. + Future setIsVideoCaptureEnabled(bool isVideoCaptureEnabled); +} + +final class _BugseeRepository implements BugseeRepository { + final String _bugseeEnabledKey = 'bugseeEnabledKey'; + final String _videoCaptureKey = 'videoCaptureKey'; + + @override + Future getBugseeConfiguration() async { + final sharedPrefInstance = await SharedPreferences.getInstance(); + return BugseeConfigurationData( + isBugseeEnabled: sharedPrefInstance.getBool(_bugseeEnabledKey), + isVideoCaptureEnabled: sharedPrefInstance.getBool(_videoCaptureKey), + ); + } + + @override + Future setIsBugseeEnabled(bool isBugseeEnabled) async { + final sharedPrefInstance = await SharedPreferences.getInstance(); + + bool isSaved = await sharedPrefInstance.setBool( + _bugseeEnabledKey, + isBugseeEnabled, + ); + + if (!isSaved) { + throw PersistenceException( + message: 'Error while setting $_bugseeEnabledKey $isBugseeEnabled', + ); + } + } + + @override + Future setIsVideoCaptureEnabled(bool isVideoCaptureEnabled) async { + final sharedPrefInstance = await SharedPreferences.getInstance(); + + bool isSaved = await sharedPrefInstance.setBool( + _videoCaptureKey, + isVideoCaptureEnabled, + ); + + if (!isSaved) { + throw PersistenceException( + message: 'Error while setting $_videoCaptureKey $isVideoCaptureEnabled', + ); + } + } +} diff --git a/src/app/lib/access/persistence_exception.dart b/src/app/lib/access/persistence_exception.dart index 669ec4f..f1d7670 100644 --- a/src/app/lib/access/persistence_exception.dart +++ b/src/app/lib/access/persistence_exception.dart @@ -1,5 +1,8 @@ /// Exception thrown when something couldn't be persisted in the shared preference. /// It was created due to https://github.com/flutter/flutter/issues/146070. final class PersistenceException implements Exception { - const PersistenceException(); + /// A descriptive message detailing the persistence exception + final String? message; + + const PersistenceException({this.message}); } diff --git a/src/app/lib/business/bugsee/bugsee_manager.dart b/src/app/lib/business/bugsee/bugsee_manager.dart new file mode 100644 index 0000000..9493fda --- /dev/null +++ b/src/app/lib/business/bugsee/bugsee_manager.dart @@ -0,0 +1,210 @@ +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:bugsee_flutter/bugsee_flutter.dart'; +import 'package:flutter/foundation.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}$'; + +/// 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; + + /// 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; + + /// Initialize bugsee with given token + /// bugsee is not available in debug mode + /// * [bugseeToken]: nullable bugsee token, if null bugsee won't be initialized make sure you provide + /// [BUGSEE_TOKEN] in the env using `--dart-define` or `launch.json` on vscode + Future initialize({ + String? bugseeToken, + }); + + /// Manually log a provided exception with a stack trace + /// (medium severity exception in Bugsee dashboard) + 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, + }); + + /// Manually update the current BugseeEnabled flag in shared prefs and in current manager singleton. + Future setIsBugseeEnabled(bool isBugseeEnabled); + + /// Manually update the current enableVideoCapture flag in shared prefs and in current manager singleton. + Future setIsVideoCaptureEnabled(bool isBugseeEnabled); + + /// Manually shows the built-in capture log report screen of Bugsee. + Future showCaptureLogReport(); +} + +final class _BugseeManager implements BugseeManager { + final Logger logger; + final BugseeRepository bugseeRepository; + + _BugseeManager({ + required this.logger, + required this.bugseeRepository, + }); + + @override + bool isRestartRequired = false; + + @override + bool isBugseeEnabled = false; + + @override + late bool isVideoCaptureEnabled = false; + + @override + bool isConfigurationValid = true; + + late bool _isBugSeeInitialized; + BugseeLaunchOptions? launchOptions; + + @override + Future initialize({ + String? bugseeToken, + }) async { + BugseeConfigurationData bugseeConfigurationData = + await bugseeRepository.getBugseeConfiguration(); + + launchOptions = _initializeLaunchOptions(); + _isBugSeeInitialized = false; + + if (kDebugMode) { + isConfigurationValid = false; + logger.i("BUGSEE: deactivated in debug mode"); + return; + } + + if (bugseeToken == null || + !RegExp(bugseeTokenFormat).hasMatch(bugseeToken)) { + isConfigurationValid = false; + logger.i( + "BUGSEE: token is null or invalid, bugsee won't be initialized", + ); + return; + } + + if (bugseeConfigurationData.isBugseeEnabled ?? true) { + await _launchBugseeLogger(bugseeToken); + } + + isBugseeEnabled = _isBugSeeInitialized; + isVideoCaptureEnabled = _isBugSeeInitialized && + (bugseeConfigurationData.isVideoCaptureEnabled ?? true); + } + + Future _launchBugseeLogger(String bugseeToken) async { + HttpOverrides.global = Bugsee.defaultHttpOverrides; + await Bugsee.launch( + bugseeToken, + appRunCallback: (isBugseeLaunched) { + if (!isBugseeLaunched) { + logger.e( + "BUGSEE: not initialized, verify bugsee token configuration", + ); + } + _isBugSeeInitialized = isBugseeLaunched; + }, + launchOptions: launchOptions, + ); + } + + BugseeLaunchOptions? _initializeLaunchOptions() { + if (Platform.isAndroid) { + return AndroidLaunchOptions(); + } else if (Platform.isIOS) { + return IOSLaunchOptions(); + } + return null; + } + + @override + Future logException({ + required Exception exception, + StackTrace? stackTrace, + }) async { + if (isBugseeEnabled) { + await Bugsee.logException(exception, stackTrace); + } + } + + @override + Future logUnhandledException({ + required Exception exception, + StackTrace? stackTrace, + }) async { + if (isBugseeEnabled) { + await Bugsee.logUnhandledException(exception); + } + } + + @override + Future setIsBugseeEnabled(bool value) async { + if (isConfigurationValid) { + isBugseeEnabled = value; + await bugseeRepository.setIsBugseeEnabled(isBugseeEnabled); + + isRestartRequired = _isBugSeeInitialized && isBugseeEnabled; + isVideoCaptureEnabled = isBugseeEnabled; + + if (!isRestartRequired) { + await Bugsee.stop(); + } + } + } + + @override + Future setIsVideoCaptureEnabled(bool value) async { + if (isBugseeEnabled) { + isVideoCaptureEnabled = value; + await bugseeRepository.setIsVideoCaptureEnabled(isVideoCaptureEnabled); + if (!isVideoCaptureEnabled) { + await Bugsee.pause(); + } else { + await Bugsee.resume(); + } + } + } + + @override + Future showCaptureLogReport() async { + if (isBugseeEnabled) { + await Bugsee.showReportDialog(); + } + } +} diff --git a/src/app/lib/main.dart b/src/app/lib/main.dart index 72ad47a..d3d805f 100644 --- a/src/app/lib/main.dart +++ b/src/app/lib/main.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:alice/alice.dart'; +import 'package:app/access/bugsee/bugsee_repository.dart'; import 'package:app/access/dad_jokes/dad_jokes_mocked_repository.dart'; import 'package:app/access/dad_jokes/dad_jokes_repository.dart'; import 'package:app/access/dad_jokes/favorite_dad_jokes_mocked_repository.dart'; @@ -17,6 +18,7 @@ import 'package:app/access/logger/logger_repository.dart'; import 'package:app/access/mocking/mocking_repository.dart'; import 'package:app/app.dart'; import 'package:app/app_router.dart'; +import 'package:app/business/bugsee/bugsee_manager.dart'; import 'package:app/business/dad_jokes/dad_jokes_service.dart'; import 'package:app/business/diagnostics/diagnostics_service.dart'; import 'package:app/business/environment/environment.dart'; @@ -35,7 +37,6 @@ late Logger _logger; Future main() async { await initializeComponents(); - runApp(const App()); } @@ -43,6 +44,7 @@ Future initializeComponents({bool? isMocked}) async { WidgetsFlutterBinding.ensureInitialized(); await _registerAndLoadEnvironment(); await _registerAndLoadLoggers(); + await _registerBugseeManager(); _logger.d("Initialized environment and logger."); @@ -117,6 +119,19 @@ Future _registerAndLoadLoggers() async { GetIt.I.registerSingleton(_logger); } +Future _registerBugseeManager() async { + GetIt.I.registerSingleton(BugseeRepository()); + GetIt.I.registerSingleton( + BugseeManager( + logger: GetIt.I.get(), + bugseeRepository: GetIt.I.get(), + ), + ); + GetIt.I.get().initialize( + bugseeToken: const String.fromEnvironment('BUGSEE_TOKEN'), + ); +} + /// Registers the HTTP client. void _registerHttpClient() { final dio = Dio(); diff --git a/src/app/lib/presentation/diagnostic/bugsee_configuration_widget.dart b/src/app/lib/presentation/diagnostic/bugsee_configuration_widget.dart new file mode 100644 index 0000000..3952cbf --- /dev/null +++ b/src/app/lib/presentation/diagnostic/bugsee_configuration_widget.dart @@ -0,0 +1,110 @@ +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'; + +class BugseeConfigurationWidget extends StatefulWidget { + const BugseeConfigurationWidget({super.key}); + + @override + State createState() => + _BugseeConfigurationWidgetState(); +} + +class _BugseeConfigurationWidgetState extends State { + final BugseeManager bugseeManager = GetIt.I.get(); + + late bool isConfigEnabled; + late bool isCaptureVideoEnabled; + late bool requireRestart; + + @override + void initState() { + super.initState(); + isConfigEnabled = bugseeManager.isBugseeEnabled; + isCaptureVideoEnabled = bugseeManager.isVideoCaptureEnabled; + requireRestart = bugseeManager.isRestartRequired; + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Column( + children: [ + if (!bugseeManager.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( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.bold, + decoration: TextDecoration.none, + ), + ), + ), + if (requireRestart) + Container( + color: const Color.fromARGB(170, 255, 0, 0), + child: const Text( + "In order to reactivate Bugsee logger restart the app.", + style: TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.bold, + decoration: TextDecoration.none, + ), + ), + ), + DiagnosticSwitch( + label: 'Bugsee enabled', + value: isConfigEnabled, + onChanged: (value) async { + await bugseeManager.setIsBugseeEnabled(value); + setState(() { + isConfigEnabled = bugseeManager.isBugseeEnabled; + isCaptureVideoEnabled = bugseeManager.isVideoCaptureEnabled; + requireRestart = bugseeManager.isRestartRequired; + }); + }, + ), + DiagnosticSwitch( + label: 'Video capture enabled', + value: isCaptureVideoEnabled, + onChanged: (value) async { + await bugseeManager.setIsVideoCaptureEnabled(value); + setState(() { + isCaptureVideoEnabled = bugseeManager.isVideoCaptureEnabled; + }); + }, + ), + ], + ), + DiagnosticButton( + label: 'Log an exception', + onPressed: () { + bugseeManager.logException(exception: Exception()); + }, + ), + DiagnosticButton( + label: 'Log an unhandled exception', + onPressed: () { + bugseeManager.logUnhandledException(exception: Exception()); + }, + ), + DiagnosticButton( + label: 'Show report dialog', + onPressed: () { + bugseeManager.showCaptureLogReport(); + }, + ), + ], + ); + } +} diff --git a/src/app/lib/presentation/diagnostic/expanded_diagnostic_page.dart b/src/app/lib/presentation/diagnostic/expanded_diagnostic_page.dart index 9b11275..049ec1c 100644 --- a/src/app/lib/presentation/diagnostic/expanded_diagnostic_page.dart +++ b/src/app/lib/presentation/diagnostic/expanded_diagnostic_page.dart @@ -1,3 +1,4 @@ +import 'package:app/presentation/diagnostic/bugsee_configuration_widget.dart'; import 'package:app/presentation/diagnostic/device_info_widget.dart'; import 'package:app/presentation/diagnostic/environment_diagnostic_widget.dart'; import 'package:app/presentation/diagnostic/logger_diagnostic_widget.dart'; @@ -23,13 +24,15 @@ final class _ExpandedDiagnosticPageState extends State EnvironmentDiagnosticWidget(), LoggerDiagnosticWidget(), const MockingDiagnosticWidget(), + const BugseeConfigurationWidget(), ]; int _selectedIndex = 0; @override void initState() { - _tabController = TabController(length: expandedDiagnosticWidgets.length, vsync: this); + _tabController = + TabController(length: expandedDiagnosticWidgets.length, vsync: this); super.initState(); } @@ -75,6 +78,9 @@ final class _ExpandedDiagnosticPageState extends State Tab( text: "Mocking", ), + Tab( + text: "Bugsee", + ), ], controller: _tabController, ), diff --git a/src/app/pubspec.lock b/src/app/pubspec.lock index 74e2607..188b15b 100644 --- a/src/app/pubspec.lock +++ b/src/app/pubspec.lock @@ -57,6 +57,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + bugsee_flutter: + dependency: "direct main" + description: + name: bugsee_flutter + sha256: "570e37a678178d772a7fa2fc52cfe9e5bd3beadc225e89091d688a1d3c97e691" + url: "https://pub.dev" + source: hosted + version: "8.4.0" build: dependency: transitive description: diff --git a/src/app/pubspec.yaml b/src/app/pubspec.yaml index 85f99b1..5b600d3 100644 --- a/src/app/pubspec.yaml +++ b/src/app/pubspec.yaml @@ -31,6 +31,7 @@ dependencies: flutter_localizations: sdk: flutter intl: any + bugsee_flutter: ^8.4.0 # This is required with alice installed unless this PR is merged https://github.com/jhomlala/alice/pull/171 dependency_overrides: diff --git a/src/cli/CHANGELOG.md b/src/cli/CHANGELOG.md index 07c8c62..a5ec128 100644 --- a/src/cli/CHANGELOG.md +++ b/src/cli/CHANGELOG.md @@ -5,6 +5,10 @@ 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.21.0 +- Add bugsee sdk in Fluttter template +- Update build stage in `steps-build-android.yml` and `steps-build-ios` providing bugsee token + ## 0.20.4 - Updates to documentation