diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 684f3b95..9e4b38ac 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -37,11 +37,18 @@ PODS: - fluttertoast (0.0.2): - Flutter - Toast + - package_info_plus (0.4.5): + - Flutter - path_provider_ios (0.0.1): - Flutter - SDWebImage (5.14.2): - SDWebImage/Core (= 5.14.2) - SDWebImage/Core (5.14.2) + - Sentry/HybridSDK (7.31.5) + - sentry_flutter (0.0.1): + - Flutter + - FlutterMacOS + - Sentry/HybridSDK (= 7.31.5) - SwiftyGif (5.4.3) - Toast (4.0.0) @@ -49,13 +56,16 @@ DEPENDENCIES: - file_picker (from `.symlinks/plugins/file_picker/ios`) - Flutter (from `Flutter`) - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) + - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`) + - sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`) SPEC REPOS: trunk: - DKImagePickerController - DKPhotoGallery - SDWebImage + - Sentry - SwiftyGif - Toast @@ -66,8 +76,12 @@ EXTERNAL SOURCES: :path: Flutter fluttertoast: :path: ".symlinks/plugins/fluttertoast/ios" + package_info_plus: + :path: ".symlinks/plugins/package_info_plus/ios" path_provider_ios: :path: ".symlinks/plugins/path_provider_ios/ios" + sentry_flutter: + :path: ".symlinks/plugins/sentry_flutter/ios" SPEC CHECKSUMS: DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac @@ -77,6 +91,8 @@ SPEC CHECKSUMS: fluttertoast: eb263d302cc92e04176c053d2385237e9f43fad0 path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02 SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84 + Sentry: 4c9babff9034785067c896fd580b1f7de44da020 + sentry_flutter: b10ae7a5ddcbc7f04648eeb2672b5747230172f1 SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780 Toast: 91b396c56ee72a5790816f40d3a94dd357abc196 diff --git a/lib/app_config.dart b/lib/app_config.dart index 6c1a8cff..c27d3561 100644 --- a/lib/app_config.dart +++ b/lib/app_config.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; import './routes/middleware.dart'; import './vaahextendflutter/app_theme.dart'; @@ -19,6 +20,9 @@ class AppConfig extends StatelessWidget { theme: ThemeData( primarySwatch: AppTheme.colors['primary'], ), + navigatorObservers: [ + SentryNavigatorObserver(), + ], onGenerateRoute: routeMiddleware, builder: (BuildContext context, Widget? child) { return DebugWidget( diff --git a/lib/main.dart b/lib/main.dart index 9108eba9..fc29ccae 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,9 +4,8 @@ import 'package:get/get.dart'; import './app_config.dart'; import './vaahextendflutter/base/base_controller.dart'; -void main() async { +Future main() async { WidgetsFlutterBinding.ensureInitialized(); BaseController baseController = Get.put(BaseController()); - await baseController.init(); - runApp(const AppConfig()); + await baseController.init(const AppConfig()); // Pass main app as argument in init method } diff --git a/lib/vaahextendflutter/base/base_controller.dart b/lib/vaahextendflutter/base/base_controller.dart index 47933be7..0947a271 100644 --- a/lib/vaahextendflutter/base/base_controller.dart +++ b/lib/vaahextendflutter/base/base_controller.dart @@ -1,5 +1,9 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; import '../../controllers/root_assets_controller.dart'; import '../app_theme.dart'; @@ -7,11 +11,42 @@ import '../env.dart'; import '../services/api.dart'; class BaseController extends GetxController { - Future init() async { + Future init(Widget app) async { await GetStorage.init(); EnvironmentConfig.setEnvConfig(); AppTheme.init(); Api.init(); Get.put(RootAssetsController()); + + final EnvironmentConfig config = EnvironmentConfig.getEnvConfig(); + + if (null != config.sentryConfig && config.sentryConfig!.dsn.isNotEmpty) { + await SentryFlutter.init( + (options) => options + ..dsn = config.sentryConfig!.dsn + ..autoAppStart = config.sentryConfig!.autoAppStart + ..tracesSampleRate = config.sentryConfig!.tracesSampleRate + ..enableAutoPerformanceTracking = config.sentryConfig!.enableAutoPerformanceTracking + ..enableUserInteractionTracing = config.sentryConfig!.enableUserInteractionTracing + ..environment = config.envType, + ); + Widget child = app; + if (config.sentryConfig!.enableUserInteractionTracing) { + child = SentryUserInteractionWidget( + child: child, + ); + } + if (config.sentryConfig!.enableAssetsInstrumentation) { + child = DefaultAssetBundle( + bundle: SentryAssetBundle( + enableStructuredDataTracing: true, + ), + child: child, + ); + } + runApp(child); + } else { + runApp(app); + } } } diff --git a/lib/vaahextendflutter/env.dart b/lib/vaahextendflutter/env.dart index 963014c3..2229d62e 100644 --- a/lib/vaahextendflutter/env.dart +++ b/lib/vaahextendflutter/env.dart @@ -4,49 +4,49 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import './app_theme.dart'; -import './helpers/console.dart'; +import './services/logging_library/logging_library.dart'; // After changing any const you will need to restart the app (Hot-reload won't work). // Version and build const String version = '1.0.0'; // version format 1.0.0 (major.minor.patch) -const String build = '2022100901'; // build no format 'YYYYMMDDNUMBER' +const String build = '2022030201'; // build no format 'YYYYMMDDNUMBER' final EnvironmentConfig defaultConfig = EnvironmentConfig( - appTitle: 'WebReinvent Team', - appTitleShort: 'Team', + appTitle: 'VaahFlutter', + appTitleShort: 'VaahFlutter', envType: 'default', version: version, build: build, backendUrl: '', apiUrl: '', - timeoutLimit: 60 * 1000, // 60 seconds + timeoutLimit: 20 * 1000, // 20 seconds firebaseId: '', - enableConsoleLogs: true, enableLocalLogs: true, - enableApiLogs: true, + enableCloudLogs: true, + enableApiLogInterceptor: true, showDebugPanel: true, debugPanelColor: AppTheme.colors['black']!.withOpacity(0.7), ); // To add new configuration add new key, value pair in envConfigs -Map envConfigs = { +Map _envConfigs = { // Do not remove default config 'default': defaultConfig.copyWith( envType: 'default', ), 'develop': defaultConfig.copyWith( envType: 'develop', - enableLocalLogs: false, + enableCloudLogs: false, ), 'stage': defaultConfig.copyWith( envType: 'stage', - enableLocalLogs: false, + enableCloudLogs: true, ), 'production': defaultConfig.copyWith( envType: 'production', - enableConsoleLogs: false, enableLocalLogs: false, + enableApiLogInterceptor: false, showDebugPanel: false, ), }; @@ -59,16 +59,16 @@ class EnvController extends GetxController { try { _config = getSpecificConfig(environment); update(); - } catch (e) { - Console.danger(e.toString()); + } catch (error, stackTrace) { + Log.exception(error, stackTrace: stackTrace); exit(0); } } EnvironmentConfig getSpecificConfig(String key) { - bool configExists = envConfigs.containsKey(key); + bool configExists = _envConfigs.containsKey(key); if (configExists) { - return envConfigs[key]!; + return _envConfigs[key]!; } throw Exception('Environment configuration not found for key: $key'); } @@ -84,9 +84,10 @@ class EnvironmentConfig { final String apiUrl; final String firebaseId; final int timeoutLimit; - final bool enableConsoleLogs; final bool enableLocalLogs; - final bool enableApiLogs; + final bool enableCloudLogs; + final SentryConfig? sentryConfig; + final bool enableApiLogInterceptor; final bool showDebugPanel; final Color debugPanelColor; @@ -100,9 +101,10 @@ class EnvironmentConfig { required this.apiUrl, required this.firebaseId, required this.timeoutLimit, - required this.enableConsoleLogs, required this.enableLocalLogs, - required this.enableApiLogs, + required this.enableCloudLogs, + this.sentryConfig, + required this.enableApiLogInterceptor, required this.showDebugPanel, required this.debugPanelColor, }); @@ -119,8 +121,14 @@ class EnvironmentConfig { static void setEnvConfig() { String environment = const String.fromEnvironment('environment', defaultValue: 'default'); final EnvController envController = Get.put(EnvController(environment)); - Console.info('Env Type: ${envController.config.envType}'); - Console.info('Version: ${envController.config.version}+${envController.config.build}'); + Log.info( + 'Env Type: ${envController.config.envType}', + disableCloudLogging: true, + ); + Log.info( + 'Version: ${envController.config.version}+${envController.config.build}', + disableCloudLogging: true, + ); } EnvironmentConfig copyWith({ @@ -133,9 +141,10 @@ class EnvironmentConfig { String? apiUrl, String? firebaseId, int? timeoutLimit, - bool? enableConsoleLogs, bool? enableLocalLogs, - bool? enableApiLogs, + bool? enableCloudLogs, + SentryConfig? sentryConfig, + bool? enableApiLogInterceptor, bool? showDebugPanel, Color? debugPanelColor, }) { @@ -149,11 +158,50 @@ class EnvironmentConfig { apiUrl: apiUrl ?? this.apiUrl, firebaseId: firebaseId ?? this.firebaseId, timeoutLimit: timeoutLimit ?? this.timeoutLimit, - enableConsoleLogs: enableConsoleLogs ?? this.enableConsoleLogs, enableLocalLogs: enableLocalLogs ?? this.enableLocalLogs, - enableApiLogs: enableApiLogs ?? this.enableApiLogs, + enableCloudLogs: enableCloudLogs ?? this.enableCloudLogs, + sentryConfig: sentryConfig ?? this.sentryConfig, + enableApiLogInterceptor: enableApiLogInterceptor ?? this.enableApiLogInterceptor, showDebugPanel: showDebugPanel ?? this.showDebugPanel, debugPanelColor: debugPanelColor ?? this.debugPanelColor, ); } } + +class SentryConfig { + final String dsn; + final bool autoAppStart; // To record cold and warm start up time + final double tracesSampleRate; + final bool enableAutoPerformanceTracking; + final bool enableUserInteractionTracing; + final bool enableAssetsInstrumentation; + + const SentryConfig({ + required this.dsn, + this.autoAppStart = true, + this.tracesSampleRate = 0.6, + this.enableAutoPerformanceTracking = true, + this.enableUserInteractionTracing = true, + this.enableAssetsInstrumentation = true, + }); + + SentryConfig copyWith({ + String? dsn, + bool? autoAppStart, + double? tracesSampleRate, + bool? enableAutoPerformanceTracking, + bool? enableUserInteractionTracing, + bool? enableAssetsInstrumentation, + }) { + return SentryConfig( + dsn: dsn ?? this.dsn, + autoAppStart: autoAppStart ?? this.autoAppStart, + tracesSampleRate: tracesSampleRate ?? this.tracesSampleRate, + enableAutoPerformanceTracking: + enableAutoPerformanceTracking ?? this.enableAutoPerformanceTracking, + enableUserInteractionTracing: + enableUserInteractionTracing ?? this.enableUserInteractionTracing, + enableAssetsInstrumentation: enableAssetsInstrumentation ?? this.enableAssetsInstrumentation, + ); + } +} diff --git a/lib/vaahextendflutter/services/api.dart b/lib/vaahextendflutter/services/api.dart index f20c015a..2cbbd5cb 100644 --- a/lib/vaahextendflutter/services/api.dart +++ b/lib/vaahextendflutter/services/api.dart @@ -7,10 +7,10 @@ import 'package:flutter/material.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:get/get.dart' as getx; +import './logging_library/logging_library.dart'; import '../app_theme.dart'; import '../env.dart'; import '../helpers/alerts.dart'; -import '../helpers/console.dart'; import '../helpers/constants.dart'; // alertType : 'dialog', 'toast', @@ -31,53 +31,6 @@ class Api { return Options(headers: header, contentType: contentType); } - static dynamic _parseKeys({ - required dynamic data, - required Function changeKeys, - }) { - if (data is List) { - dynamic parsedData = []; - for (var e in data) { - parsedData.add(_parseKeys(data: e, changeKeys: changeKeys)); - } - return parsedData; - } else if (data is Map) { - Map parsedData = {}; - data.forEach( - (key, value) { - dynamic parsedvalue = _parseKeys(data: value, changeKeys: changeKeys); - parsedData.addAll({ - changeKeys(key): parsedvalue, - }); - }, - ); - return parsedData; - } - return data; - } - - static String _lowerCamelCaseToSnakeCase(String data) { - List parts = data.split(RegExp(r"(?=(?!^)[A-Z])")); - String result = parts.join('_'); - return result.toLowerCase(); - } - - static String _snakeCaseToLowerCamelCase(String data) { - List sentence = data.split('_'); - sentence.removeWhere((element) => element.isEmpty); - String result = ''; - for (var e in sentence) { - result += e[0].toUpperCase() + e.substring(1); - } - if (result.isEmpty) { - return data; - } - if (result[0].isAlphabetOnly) { - result = result[0].toLowerCase() + result.substring(1); - } - return result; - } - // return type of ajax is ApiResponseType? so if there is error // then null will be returned otherwise ApiResponseType object static Future ajax({ @@ -139,11 +92,6 @@ class Api { 'response': response }; } catch (error) { - // On completed, use for hide loading - if (onCompleted != null) { - await onCompleted(); - } - // On inline error if (onError != null) { await onError(error); @@ -195,11 +143,6 @@ class Api { } }; } - - if (callback != null) { - await callback(null, null); - } - rethrow; } finally { // Call finally function if (onFinally != null) { @@ -211,7 +154,8 @@ class Api { static void init() { // get env controller to get variable apiUrl _config = EnvironmentConfig.getEnvConfig(); - if (_config.enableApiLogs) { + _apiBaseUrl = _config.apiUrl; + if (_config.enableApiLogInterceptor) { _dio.interceptors.add( LogInterceptor( responseBody: true, @@ -342,18 +286,26 @@ class Api { final Map formatedResponse = response.data as Map; dynamic responseData = formatedResponse['data']; if (responseData == null) { - Console.warning('response doesn\'t contain data key.'); + Log.warning( + 'response doesn\'t contain data key.', + data: formatedResponse, + disableCloudLogging: true, + ); } List? responseMessages; if (formatedResponse['messages'] == null) { - Console.warning('response doesn\'t contain messages key.'); + Log.warning( + 'response doesn\'t contain messages key.', + data: formatedResponse, + disableCloudLogging: true, + ); } else { responseMessages = (formatedResponse['messages'] as List).map((e) => e.toString()).toList(); } String? responseHint = formatedResponse['hint'] as String?; if (responseHint == null) { - Console.warning('response doesn\'t contain hint key.'); + Log.warning('response doesn\'t contain hint key.', disableCloudLogging: true); } if (showAlert) { if (alertType == 'dialog') { @@ -396,7 +348,7 @@ class Api { bool showAlert, String alertType, ) async { - Console.danger(error.toString()); + Log.exception(error, stackTrace: error.stackTrace); if (showAlert) { if (alertType == 'dialog') { if (Alerts.showErrorDialog != null) { @@ -426,157 +378,193 @@ class Api { } static Future _handleResponseError( - Object error, + DioError error, bool showAlert, String alertType, ) async { - if (error is DioError && error.type == DioErrorType.badResponse) { - final Response? response = error.response; - try { - // By pass dio header error code to get response content - // Try to return response - if (response == null) { - throw DioError( - requestOptions: error.requestOptions, - response: error.response, - type: error.type, - error: response?.statusMessage, - ); - } - final Response res = Response( - data: response.data, - headers: response.headers, - requestOptions: response.requestOptions, - isRedirect: response.isRedirect, - statusCode: response.statusCode, - statusMessage: response.statusMessage, - redirects: response.redirects, - extra: response.extra, - ); + final Response? response = error.response; + try { + // By pass dio header error code to get response content + // Try to return response + if (response == null) { throw DioError( requestOptions: error.requestOptions, - response: res, + response: error.response, type: error.type, - error: res.statusMessage, + error: response?.statusMessage, ); - } catch (e) { - if (error.type == DioErrorType.badResponse) { - String errorCode = 'unknown'; - List errors = error.message == null ? [] : [error.message!]; - String? debug; - if (error.response?.statusCode == 401) { - errorCode = 'unauthorized'; + } + final Response res = Response( + data: response.data, + headers: response.headers, + requestOptions: response.requestOptions, + isRedirect: response.isRedirect, + statusCode: response.statusCode, + statusMessage: response.statusMessage, + redirects: response.redirects, + extra: response.extra, + ); + throw DioError( + requestOptions: error.requestOptions, + response: res, + type: error.type, + error: res.statusMessage, + ); + } catch (catchErr, stackTrace) { + List errors = error.message == null ? [] : [error.message!]; + String? debug; + + if (error.response?.data != null) { + try { + Log.exception(catchErr, data: error.response, stackTrace: stackTrace); + + final Map response = error.response?.data as Map; + if (response['errors'] != null) { + errors = (response['errors'] as List).map((e) => e.toString()).toList(); } - if (error.response?.data != null) { - try { - Console.danger('${error.response}'); - final Map response = error.response?.data as Map; - Console.danger('$response'); - if (response['errors'] != null) { - errors = (response['errors'] as List).map((e) => e.toString()).toList(); - } - if (errors.isEmpty) { - Console.warning('response doesn\'t contain errors key.'); - } - debug = response['debug'] as String?; - if (debug == null) { - Console.warning('response doesn\'t contain debug key.'); - } - } catch (e) { - throw Exception( - 'Unable to parse error response.', - ); - } + if (errors.isEmpty) { + Log.warning('response doesn\'t contain errors key.', disableCloudLogging: true); } - if (errorCode == 'unauthorized') { - Console.danger('Error type: unauthorized'); + debug = response['debug'] as String?; + if (debug == null) { + Log.warning('response doesn\'t contain debug key.', disableCloudLogging: true); } - if (showAlert) { - if (alertType == 'dialog') { - if (Alerts.showErrorDialog != null) { - await Alerts.showErrorDialog!( - title: 'Error', - messages: errors.isEmpty ? null : errors, - hint: debug, - ); - return; - } - Console.danger(errors.toString()); - _showDialog( - title: 'Error', - content: errors.isEmpty ? null : errors, - hint: debug, - ); - } else { - if (Alerts.showErrorToast != null) { - await Alerts.showErrorToast!( - content: errors.isEmpty ? 'Error' : 'ERR: ${errors.join('\n')}', - ); - return; - } - _showToast( - content: errors.isEmpty ? 'Error' : 'ERR: ${errors.join('\n')}', - color: AppTheme.colors['success']!, - ); - } + } catch (e) { + throw Exception( + 'Unable to parse error response', + ); + } + } + + if (showAlert) { + if (alertType == 'dialog') { + if (Alerts.showErrorDialog != null) { + await Alerts.showErrorDialog!( + title: 'Error', + messages: errors.isEmpty ? null : errors, + hint: debug, + ); + return; } - return; + _showDialog( + title: 'Error', + content: errors.isEmpty ? null : errors, + hint: debug, + ); + } else { + if (Alerts.showErrorToast != null) { + await Alerts.showErrorToast!( + content: errors.isEmpty ? 'Error' : 'ERR: ${errors.join('\n')}', + ); + return; + } + _showToast( + content: errors.isEmpty ? 'Error' : 'ERR: ${errors.join('\n')}', + color: AppTheme.colors['success']!, + ); } - Console.danger(error.toString()); - rethrow; } + + return; } } +} - static void _showToast({ - required String content, - Color color = Colors.white, - }) { - Fluttertoast.showToast( - msg: content, - toastLength: Toast.LENGTH_SHORT, - gravity: ToastGravity.BOTTOM, - backgroundColor: color.withOpacity(0.5), - textColor: color == AppTheme.colors['white'] - ? AppTheme.colors['black'] - : AppTheme.colors['whiteColor'], - fontSize: 16.0, +dynamic _parseKeys({ + required dynamic data, + required Function changeKeys, +}) { + if (data is List) { + dynamic parsedData = []; + for (var e in data) { + parsedData.add(_parseKeys(data: e, changeKeys: changeKeys)); + } + return parsedData; + } else if (data is Map) { + Map parsedData = {}; + data.forEach( + (key, value) { + dynamic parsedvalue = _parseKeys(data: value, changeKeys: changeKeys); + parsedData.addAll({ + changeKeys(key): parsedvalue, + }); + }, ); + return parsedData; } + return data; +} - static _showDialog({ - required String title, - List? content, - String? hint, - List? actions, - }) { - return getx.Get.dialog( - CupertinoAlertDialog( - title: Text(title), - content: SingleChildScrollView( - physics: const BouncingScrollPhysics(), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (content != null && content.isNotEmpty) Text(content.join('\n')), - if (content != null && content.isNotEmpty) verticalMargin12, - if (hint != null && hint.trim().isNotEmpty) Text(hint), - ], - ), +String _lowerCamelCaseToSnakeCase(String data) { + List parts = data.split(RegExp(r"(?=(?!^)[A-Z])")); + String result = parts.join('_'); + return result.toLowerCase(); +} + +String _snakeCaseToLowerCamelCase(String data) { + List sentence = data.split('_'); + sentence.removeWhere((element) => element.isEmpty); + String result = ''; + for (var e in sentence) { + result += e[0].toUpperCase() + e.substring(1); + } + if (result.isEmpty) { + return data; + } + if (result[0].isAlphabetOnly) { + result = result[0].toLowerCase() + result.substring(1); + } + return result; +} + +void _showToast({ + required String content, + Color color = Colors.white, +}) { + Fluttertoast.showToast( + msg: content, + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + backgroundColor: color.withOpacity(0.5), + textColor: color == AppTheme.colors['white'] + ? AppTheme.colors['black'] + : AppTheme.colors['whiteColor'], + fontSize: 16.0, + ); +} + +Future _showDialog({ + required String title, + List? content, + String? hint, + List? actions, +}) { + return getx.Get.dialog( + CupertinoAlertDialog( + title: Text(title), + content: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (content != null && content.isNotEmpty) Text(content.join('\n')), + if (content != null && content.isNotEmpty) verticalMargin12, + if (hint != null && hint.trim().isNotEmpty) Text(hint), + ], ), - actions: [ - if (actions == null || actions.isNotEmpty) - CupertinoButton( - child: const Text('Ok'), - onPressed: () { - getx.Get.back(); - }, - ) - else - ...actions, - ], ), - barrierDismissible: false, - ); - } + actions: [ + if (actions == null || actions.isNotEmpty) + CupertinoButton( + child: const Text('Ok'), + onPressed: () { + getx.Get.back(); + }, + ) + else + ...actions, + ], + ), + barrierDismissible: false, + ); } diff --git a/lib/vaahextendflutter/services/logging_library/_cloud/firebase_logging_service.dart b/lib/vaahextendflutter/services/logging_library/_cloud/firebase_logging_service.dart new file mode 100644 index 00000000..6e9401b2 --- /dev/null +++ b/lib/vaahextendflutter/services/logging_library/_cloud/firebase_logging_service.dart @@ -0,0 +1,24 @@ +import './logging_service.dart'; +import '../models/log.dart'; + +abstract class FirebaseLoggingService implements LoggingService { + static logEvent({ + required String message, + EventType? type, + Object? data, + }) => + throw UnimplementedError(); + + static logException( + dynamic throwable, { + dynamic stackTrace, + dynamic hint, + }) => + throw UnimplementedError(); + + static logTransaction({ + required Function execute, + required TransactionDetails details, + }) async => + UnimplementedError(); +} diff --git a/lib/vaahextendflutter/services/logging_library/_cloud/logging_service.dart b/lib/vaahextendflutter/services/logging_library/_cloud/logging_service.dart new file mode 100644 index 00000000..8386145a --- /dev/null +++ b/lib/vaahextendflutter/services/logging_library/_cloud/logging_service.dart @@ -0,0 +1,23 @@ +import '../models/log.dart'; + +abstract class LoggingService { + static logEvent({ + required String message, + required EventType type, + Object? data, + }) => + UnimplementedError(); + + static logException( + dynamic throwable, { + dynamic stackTrace, + dynamic hint, + }) => + UnimplementedError(); + + static logTransaction({ + required Function execute, + required TransactionDetails details, + }) async => + UnimplementedError(); +} diff --git a/lib/vaahextendflutter/services/logging_library/_cloud/sentry_logging_service.dart b/lib/vaahextendflutter/services/logging_library/_cloud/sentry_logging_service.dart new file mode 100644 index 00000000..5d94f4bf --- /dev/null +++ b/lib/vaahextendflutter/services/logging_library/_cloud/sentry_logging_service.dart @@ -0,0 +1,35 @@ +import 'package:sentry_flutter/sentry_flutter.dart'; + +import './logging_service.dart'; +import '../models/log.dart'; + +abstract class SentryLoggingService implements LoggingService { + static logEvent({ + required String message, + SentryLevel? level, + Object? data, + }) { + final SentryEvent event = SentryEvent(message: SentryMessage(message), level: level); + Sentry.captureEvent( + event, + hint: data, + ); + } + + static logException( + dynamic throwable, { + dynamic stackTrace, + dynamic hint, + }) { + Sentry.captureException(throwable, stackTrace: stackTrace, hint: hint); + } + + static logTransaction({ + required Function execute, + required TransactionDetails details, + }) async { + final ISentrySpan transaction = Sentry.startTransaction(details.name, details.operation); + await execute(); + await transaction.finish(); + } +} diff --git a/lib/vaahextendflutter/helpers/console.dart b/lib/vaahextendflutter/services/logging_library/_local/console_service.dart similarity index 63% rename from lib/vaahextendflutter/helpers/console.dart rename to lib/vaahextendflutter/services/logging_library/_local/console_service.dart index 1b968bfb..748dd83b 100644 --- a/lib/vaahextendflutter/helpers/console.dart +++ b/lib/vaahextendflutter/services/logging_library/_local/console_service.dart @@ -3,10 +3,10 @@ import 'dart:convert'; import 'package:colorize/colorize.dart'; import 'package:flutter/material.dart'; -import '../env.dart'; +import '../models/log.dart'; class Console { - static void printChunks(Colorize text) { + static void _printChunks(Colorize text) { final RegExp pattern = RegExp('.{1,800}'); // 800 is the size of each chunk pattern.allMatches(text.toString()).forEach( (RegExpMatch match) => debugPrint( @@ -15,9 +15,8 @@ class Console { ); } - static void printLog(Colorize text) { - if (!EnvironmentConfig.getEnvConfig().enableConsoleLogs) return; - Console.printChunks(text); + static void _printLog(Colorize text) { + _printChunks(text); } static String _parseData(Object? data) { @@ -32,60 +31,76 @@ class Console { static void log(String text, [Object? data]) { Colorize txt = Colorize(text); - Console.printLog(txt); + _printLog(txt); if (data != null) { Colorize dataColor = Colorize(_parseData(data)); dataColor.white(); - Console.printLog(dataColor); + _printLog(dataColor); } } static void info(String text, [Object? data]) { Colorize txt = Colorize(text); txt.blue(); - Console.printLog(txt); + _printLog(txt); if (data != null) { Colorize dataColor = Colorize(_parseData(data)); dataColor.blue(); - Console.printLog(dataColor); + _printLog(dataColor); } } static void success(String text, [Object? data]) { Colorize txt = Colorize(text); txt.green(); - Console.printLog(txt); + _printLog(txt); if (data != null) { Colorize dataColor = Colorize(_parseData(data)); dataColor.green(); - Console.printLog(dataColor); + _printLog(dataColor); } } static void warning(String text, [Object? data]) { Colorize txt = Colorize(text); txt.yellow(); - Console.printLog(txt); + _printLog(txt); if (data != null) { Colorize dataColor = Colorize(_parseData(data)); dataColor.yellow(); - Console.printLog(dataColor); + _printLog(dataColor); } } static void danger(String text, [Object? data]) { Colorize txt = Colorize(text); txt.red(); - Console.printLog(txt); + _printLog(txt); if (data != null) { Colorize dataColor = Colorize(_parseData(data)); dataColor.red(); - Console.printLog(dataColor); + _printLog(dataColor); } } + + static logTransaction({ + required Function execute, + required TransactionDetails details, + }) async { + final DateTime start = DateTime.now(); + await execute(); + final DateTime end = DateTime.now(); + final diff = end.difference(start); + success('------------- execution details -------------'); + info('Transaction Name: ${details.name} | Operation: ${details.operation}'); + if (null != details.description && details.description!.isNotEmpty) { + info('Description: ${details.description}'); + } + info('Execution time in milliseconds: ${diff.inMilliseconds}'); + } } diff --git a/lib/vaahextendflutter/helpers/local_log.dart b/lib/vaahextendflutter/services/logging_library/_local/local_device_service.dart similarity index 100% rename from lib/vaahextendflutter/helpers/local_log.dart rename to lib/vaahextendflutter/services/logging_library/_local/local_device_service.dart diff --git a/lib/vaahextendflutter/services/logging_library/logging_library.dart b/lib/vaahextendflutter/services/logging_library/logging_library.dart new file mode 100644 index 00000000..fb20f2e3 --- /dev/null +++ b/lib/vaahextendflutter/services/logging_library/logging_library.dart @@ -0,0 +1,167 @@ +import './_cloud/firebase_logging_service.dart'; +import './_cloud/sentry_logging_service.dart'; +import './_local/console_service.dart'; +import './models/log.dart'; +import '../../env.dart'; + +class Log { + static final EnvironmentConfig _config = EnvironmentConfig.getEnvConfig(); + + static final List _services = [ + SentryLoggingService, + FirebaseLoggingService, + ]; + + static void log( + dynamic text, { + Object? data, + bool disableLocalLogging = false, + bool disableCloudLogging = false, + }) { + if (_config.enableLocalLogs && !disableLocalLogging) { + Console.log(text.toString(), data); + } + if (_config.enableCloudLogs && !disableCloudLogging) { + _logEvent(text.toString(), data: data, type: EventType.log); + } + } + + static void info( + dynamic text, { + Object? data, + bool disableLocalLogging = false, + bool disableCloudLogging = false, + }) { + if (_config.enableLocalLogs && !disableLocalLogging) { + Console.info(text.toString(), data); + } + if (_config.enableCloudLogs && !disableCloudLogging) { + _logEvent(text.toString(), data: data, type: EventType.info); + } + } + + static void success( + dynamic text, { + Object? data, + bool disableLocalLogging = false, + bool disableCloudLogging = false, + }) { + if (_config.enableLocalLogs && !disableLocalLogging) { + Console.success(text.toString(), data); + } + if (_config.enableCloudLogs && !disableCloudLogging) { + _logEvent(text.toString(), data: data, type: EventType.success); + } + } + + static void warning( + dynamic text, { + Object? data, + bool disableLocalLogging = false, + bool disableCloudLogging = false, + }) { + if (_config.enableLocalLogs && !disableLocalLogging) { + Console.warning(text.toString(), data); + } + if (_config.enableCloudLogs && !disableCloudLogging) { + _logEvent(text.toString(), data: data, type: EventType.warning); + } + } + + static void exception( + dynamic throwable, { + Object? data, + dynamic stackTrace, + dynamic hint, + bool disableLocalLogging = false, + bool disableCloudLogging = false, + }) { + if (_config.enableLocalLogs && !disableLocalLogging) { + Console.danger(throwable.toString(), data); + } + if (_config.enableCloudLogs && !disableCloudLogging) { + final hintWithData = { + 'hint': hint, + 'data': data, + }; + for (final service in _services) { + switch (service) { + case SentryLoggingService: + SentryLoggingService.logException( + throwable, + stackTrace: stackTrace, + hint: hintWithData, + ); + return; + case FirebaseLoggingService: + FirebaseLoggingService.logException( + throwable, + stackTrace: stackTrace, + hint: hintWithData, + ); + return; + default: + return; + } + } + } + } + + static logTransaction({ + required Function execute, + required TransactionDetails details, + bool disableLocalLogging = false, + bool disableCloudLogging = false, + }) async { + if (_config.enableLocalLogs && !disableLocalLogging) { + Console.logTransaction(execute: execute, details: details); + } + if (_config.enableCloudLogs && !disableCloudLogging) { + for (final service in _services) { + switch (service) { + case SentryLoggingService: + SentryLoggingService.logTransaction( + execute: execute, + details: details, + ); + return; + case FirebaseLoggingService: + FirebaseLoggingService.logTransaction( + execute: execute, + details: details, + ); + return; + default: + return; + } + } + } + } + + static void _logEvent( + String text, { + Object? data, + EventType? type, + }) { + for (final service in _services) { + switch (service) { + case SentryLoggingService: + SentryLoggingService.logEvent( + message: text, + level: type?.toSentryLevel, + data: data, + ); + return; + case FirebaseLoggingService: + FirebaseLoggingService.logEvent( + message: text, + type: type, + data: data, + ); + return; + default: + return; + } + } + } +} diff --git a/lib/vaahextendflutter/services/logging_library/models/log.dart b/lib/vaahextendflutter/services/logging_library/models/log.dart new file mode 100644 index 00000000..d188dee5 --- /dev/null +++ b/lib/vaahextendflutter/services/logging_library/models/log.dart @@ -0,0 +1,39 @@ +import 'package:sentry_flutter/sentry_flutter.dart'; + +enum EventType { + log, + info, + success, + warning, +} + +extension EventTypeExtension on EventType { + String get toStr => toString().split('.')[1]; + + SentryLevel? get toSentryLevel { + switch (this) { + case EventType.log: + return SentryLevel.debug; + case EventType.info: + return SentryLevel.info; + case EventType.success: + return SentryLevel.info; + case EventType.warning: + return SentryLevel.warning; + default: + return null; + } + } +} + +class TransactionDetails { + final String name; + final String operation; + final String? description; + + const TransactionDetails({ + required this.name, + required this.operation, + this.description, + }); +} diff --git a/lib/vaahextendflutter/widgets/atoms/input_date_time.dart b/lib/vaahextendflutter/widgets/atoms/input_date_time.dart index e5a7c1b1..ec70d6a2 100644 --- a/lib/vaahextendflutter/widgets/atoms/input_date_time.dart +++ b/lib/vaahextendflutter/widgets/atoms/input_date_time.dart @@ -2,10 +2,10 @@ import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import '../../app_theme.dart'; -import '../../helpers/console.dart'; import '../../helpers/constants.dart'; import '../../helpers/date_time.dart'; import '../../helpers/enums.dart'; +import '../../services/logging_library/logging_library.dart'; enum PickerType { dateOnly, timeOnly, dateAndTime } @@ -183,7 +183,7 @@ class _InputDateTimeState extends State { } break; default: - Console.danger('Error in date time input'); + Log.exception('Error in date time input', disableCloudLogging: true); } } diff --git a/lib/vaahextendflutter/widgets/debug.dart b/lib/vaahextendflutter/widgets/debug.dart index caa641ea..5831dd21 100644 --- a/lib/vaahextendflutter/widgets/debug.dart +++ b/lib/vaahextendflutter/widgets/debug.dart @@ -158,46 +158,35 @@ class DebugWidgetState extends State with SingleTickerProviderState right: defaultPadding, ), children: [ - SelectableText( - _environmentConfig.envType, - style: TextStyles.regular3, + ..._showDetails( + [ + 'App Title: ${_environmentConfig.appTitle}', + 'App Title Short: ${_environmentConfig.appTitleShort}', + _environmentConfig.envType, + 'Version: ${_environmentConfig.version}', + 'Build: ${_environmentConfig.build}', + ], ), - verticalMargin4, - SelectableText( - 'Version: ${_environmentConfig.version}', - style: TextStyles.regular3, + verticalMargin24, + ..._showDetails( + [ + 'Backend URL: ${_environmentConfig.backendUrl}', + 'API URL: ${_environmentConfig.apiUrl}', + 'Request and Response Timeout: ${(_environmentConfig.timeoutLimit) / 1000} Seconds', + 'Firebase Id: ${_environmentConfig.firebaseId}', + 'Local Logs Enabled (Console + Device Specific): ${_environmentConfig.enableLocalLogs}', + 'Cloud Logs Enabled: ${_environmentConfig.enableCloudLogs}', + if (null != _environmentConfig.sentryConfig) ...[ + 'Sentry DSN: ${_environmentConfig.sentryConfig!.dsn}', + 'Sentry Auto App Start (Record Cold And Warm Start Time): ${_environmentConfig.sentryConfig!.autoAppStart}', + 'Sentry Traces Sample Rate: ${_environmentConfig.sentryConfig!.tracesSampleRate}', + 'Sentry User Interaction Tracing: ${_environmentConfig.sentryConfig!.enableUserInteractionTracing}', + 'Sentry Auto Performance Tracking: ${_environmentConfig.sentryConfig!.enableAutoPerformanceTracking}', + 'Sentry Assets Instrumentation: ${_environmentConfig.sentryConfig!.enableAssetsInstrumentation}', + ], + 'API Logs Interceptor: ${_environmentConfig.enableApiLogInterceptor}', + ], ), - verticalMargin4, - SelectableText( - 'Build: ${_environmentConfig.build}', - style: TextStyles.regular3, - ), - verticalMargin16, - SelectableText( - 'Backend URL: ${_environmentConfig.backendUrl}', - style: TextStyles.regular3, - ), - verticalMargin8, - SelectableText( - 'API URL: ${_environmentConfig.apiUrl}', - style: TextStyles.regular3, - ), - verticalMargin8, - SelectableText( - 'Firebase Id: ${_environmentConfig.firebaseId}', - style: TextStyles.regular3, - ), - verticalMargin8, - SelectableText( - 'Console Logs Enabled: ${_environmentConfig.enableConsoleLogs}', - style: TextStyles.regular3, - ), - verticalMargin8, - SelectableText( - 'Local Logs Enabled: ${_environmentConfig.enableLocalLogs}', - style: TextStyles.regular3, - ), - verticalMargin8, ], ); }, @@ -215,6 +204,24 @@ class DebugWidgetState extends State with SingleTickerProviderState ) : widget.child; } + + List _showDetails(List details) { + return details + .map( + (detail) => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SelectableText( + detail, + style: TextStyles.regular3, + ), + verticalMargin8, + ], + ), + ) + .toList(growable: false); + } } @immutable diff --git a/lib/views/pages/ui/components/inputs/complex.dart b/lib/views/pages/ui/components/inputs/complex.dart index 3723bf63..227f7f46 100644 --- a/lib/views/pages/ui/components/inputs/complex.dart +++ b/lib/views/pages/ui/components/inputs/complex.dart @@ -1,9 +1,9 @@ import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; -import '../../../../../vaahextendflutter/helpers/console.dart'; import '../../../../../vaahextendflutter/helpers/constants.dart'; import '../../../../../vaahextendflutter/helpers/styles.dart'; +import '../../../../../vaahextendflutter/services/logging_library/logging_library.dart'; import '../../../../../vaahextendflutter/widgets/atoms/input_auto_complete.dart'; import '../../../../../vaahextendflutter/widgets/atoms/input_date_time.dart'; import '../../../../../vaahextendflutter/widgets/atoms/input_file_picker.dart'; @@ -25,7 +25,7 @@ class InputDateTimePreview extends StatelessWidget { label: 'Choose Date', pickerType: PickerType.dateOnly, callback: (data) { - Console.danger(data.toString()); + Log.info(data, disableCloudLogging: true); }, ), verticalMargin16, @@ -33,7 +33,7 @@ class InputDateTimePreview extends StatelessWidget { label: 'Choose Time', pickerType: PickerType.timeOnly, callback: (data) { - Console.danger(data.toString()); + Log.info(data, disableCloudLogging: true); }, ), verticalMargin16, @@ -41,7 +41,7 @@ class InputDateTimePreview extends StatelessWidget { label: 'Choose Date And Time', pickerType: PickerType.dateAndTime, callback: (data) { - Console.danger(data.toString()); + Log.info(data, disableCloudLogging: true); }, ), ], @@ -117,7 +117,7 @@ class InputSliderPreview extends StatelessWidget { Text('basic slider', style: normal), InputSlider( initialValue: 0.8, - onChanged: (_) => Console.danger(_.toString()), + onChanged: (value) => Log.info(value, disableCloudLogging: true), ), Text('with input slider', style: normal), InputSlider( @@ -126,7 +126,7 @@ class InputSliderPreview extends StatelessWidget { initialValue: 50, step: 2, forceInputBox: true, - onChanged: (_) => Console.danger(_.toString()), + onChanged: (value) => Log.info(value, disableCloudLogging: true), ), Text('step', style: normal), InputSlider( @@ -134,7 +134,7 @@ class InputSliderPreview extends StatelessWidget { min: 0, max: 100, step: 20, - onChanged: (_) => Console.danger(_.toString()), + onChanged: (value) => Log.info(value, disableCloudLogging: true), ), Text('decimal step', style: normal), InputSlider( @@ -142,14 +142,14 @@ class InputSliderPreview extends StatelessWidget { min: 0, max: 10, step: 0.5, - onChanged: (_) => Console.danger(_.toString()), + onChanged: (value) => Log.info(value, disableCloudLogging: true), ), Text('vertical slider', style: normal), Padding( padding: verticalPadding24, child: InputSlider( initialValue: 0, - onChanged: (_) => Console.danger(_.toString()), + onChanged: (value) => Log.info(value, disableCloudLogging: true), forceVertical: true, ), ), @@ -159,7 +159,7 @@ class InputSliderPreview extends StatelessWidget { max: 10, initialValues: const RangeValues(2, 6), step: 0.1, - onChanged: (_) => Console.danger(_.toString()), + onChanged: (value) => Log.info(value, disableCloudLogging: true), precision: 1, ), ], @@ -276,7 +276,7 @@ class InputFilePickerPreview extends StatelessWidget { callback: (List? files) { if (files == null) return; for (final element in files) { - Console.danger(element.name); + Log.info(element.name, disableCloudLogging: true); } }, ), diff --git a/pubspec.lock b/pubspec.lock index 848ec6f1..f31766c8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -49,6 +49,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.0" + crypto: + dependency: transitive + description: + name: crypto + sha256: aa274aa7774f8964e4f4f38cc994db7b6158dd36e9187aaceaddc994b35c6c67 + url: "https://pub.dev" + source: hosted + version: "3.0.2" cupertino_icons: dependency: "direct main" description: @@ -168,6 +176,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.3" + http: + dependency: transitive + description: + name: http + sha256: "6aa2946395183537c8b880962d935877325d6a09a2867c3970c05c0fed6ac482" + url: "https://pub.dev" + source: hosted + version: "0.13.5" http_parser: dependency: transitive description: @@ -224,6 +240,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.8.0" + package_info_plus: + dependency: transitive + description: + name: package_info_plus + sha256: "8df5ab0a481d7dc20c0e63809e90a588e496d276ba53358afc4c4443d0a00697" + url: "https://pub.dev" + source: hosted + version: "3.0.3" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: "9bc8ba46813a4cc42c66ab781470711781940780fd8beddd0c3da62506d3a6c6" + url: "https://pub.dev" + source: hosted + version: "2.0.1" path: dependency: transitive description: @@ -312,6 +344,22 @@ packages: url: "https://pub.dev" source: hosted version: "4.2.4" + sentry: + dependency: transitive + description: + name: sentry + sha256: "81c1f32496ff04476d6ddfe5894215b1034d185301d2e3dffd272853392c5ea7" + url: "https://pub.dev" + source: hosted + version: "6.20.1" + sentry_flutter: + dependency: "direct main" + description: + name: sentry_flutter + sha256: "5ca2c8d86c220f7ad3109bedceb2c51b0e90bac5218e732be98ea2cba8006461" + url: "https://pub.dev" + source: hosted + version: "6.20.1" sky_engine: dependency: transitive description: flutter @@ -373,6 +421,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + uuid: + dependency: transitive + description: + name: uuid + sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313" + url: "https://pub.dev" + source: hosted + version: "3.0.7" vector_math: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index ce825a42..ea998b2c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -22,6 +22,7 @@ dependencies: intl: ^0.18.0 file_picker: ^5.2.5 font_awesome_flutter: ^10.4.0 + sentry_flutter: ^6.20.1 dev_dependencies: flutter_test: