Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

config: integrate bugsee in flutter template and update build pipeline #48

Merged
merged 9 commits into from
Nov 19, 2024
4 changes: 3 additions & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@
"program": "src/app/lib/main.dart",
"toolArgs": [
"--dart-define",
"ENV=Staging"
"ENV=Staging",
"--dart-define",
"BUGSEE_TOKEN=<token>"
]
},
{
Expand Down
1 change: 1 addition & 0 deletions build/azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'))
Expand Down
3 changes: 3 additions & 0 deletions build/stage-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@
type: string
- name: googleServicesJsonFile
type: string
- name: bugseeVariableGroup
type: string
default: ''

jobs:
- job: OnWindows_ReleaseNotes
Expand Down
2 changes: 1 addition & 1 deletion build/steps-build-android.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion build/steps-build-ios.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion src/app/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,4 @@ app.*.symbols
app.*.map.json

# Localization related
lib/l10n/gen_l10n/*
lib/l10n/gen_l10n/*
12 changes: 12 additions & 0 deletions src/app/lib/access/bugsee/bugsee_configuration_data.dart
Original file line number Diff line number Diff line change
@@ -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,
});
}
62 changes: 62 additions & 0 deletions src/app/lib/access/bugsee/bugsee_repository.dart
Original file line number Diff line number Diff line change
@@ -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<BugseeConfigurationData> 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<BugseeConfigurationData> 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',
);
}
}
}
5 changes: 4 additions & 1 deletion src/app/lib/access/persistence_exception.dart
Original file line number Diff line number Diff line change
@@ -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});
}
210 changes: 210 additions & 0 deletions src/app/lib/business/bugsee/bugsee_manager.dart
Original file line number Diff line number Diff line change
@@ -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<void> initialize({
String? bugseeToken,
});

/// Manually log a provided exception with a stack trace
/// (medium severity exception in Bugsee dashboard)
Future<void> logException({
required Exception exception,
StackTrace? stackTrace,
});

/// Manually log an unhandled exception with a stack trace
/// (critical severity exception in Bugsee dashboard)
Future<void> logUnhandledException({
required Exception exception,
StackTrace? stackTrace,
});

/// Manually update the current BugseeEnabled flag in shared prefs and in current manager singleton.
Future<void> setIsBugseeEnabled(bool isBugseeEnabled);

/// Manually update the current enableVideoCapture flag in shared prefs and in current manager singleton.
Future<void> setIsVideoCaptureEnabled(bool isBugseeEnabled);

/// Manually shows the built-in capture log report screen of Bugsee.
Future<void> 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<void> 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<void> logException({
required Exception exception,
StackTrace? stackTrace,
}) async {
if (isBugseeEnabled) {
await Bugsee.logException(exception, stackTrace);
}
}

@override
Future<void> logUnhandledException({
required Exception exception,
StackTrace? stackTrace,
}) async {
if (isBugseeEnabled) {
await Bugsee.logUnhandledException(exception);
}
}

@override
Future<void> setIsBugseeEnabled(bool value) async {
if (isConfigurationValid) {
isBugseeEnabled = value;
await bugseeRepository.setIsBugseeEnabled(isBugseeEnabled);

isRestartRequired = _isBugSeeInitialized && isBugseeEnabled;
isVideoCaptureEnabled = isBugseeEnabled;

if (!isRestartRequired) {
await Bugsee.stop();
}
}
}

@override
Future<void> setIsVideoCaptureEnabled(bool value) async {
if (isBugseeEnabled) {
isVideoCaptureEnabled = value;
await bugseeRepository.setIsVideoCaptureEnabled(isVideoCaptureEnabled);
if (!isVideoCaptureEnabled) {
await Bugsee.pause();
} else {
await Bugsee.resume();
}
}
}

@override
Future<void> showCaptureLogReport() async {
if (isBugseeEnabled) {
await Bugsee.showReportDialog();
}
}
}
Loading