TF-4136 Apply early exit to prevent string operation in log

This commit is contained in:
dab246
2026-01-02 13:28:31 +07:00
committed by Dat H. Pham
parent 95534e28e0
commit fedb8fed93
23 changed files with 296 additions and 210 deletions
+22 -13
View File
@@ -1,3 +1,5 @@
import 'dart:async';
import 'package:core/utils/build_utils.dart';
import 'package:core/utils/platform_info.dart';
import 'package:core/utils/sentry/sentry_manager.dart';
@@ -59,26 +61,33 @@ void _internalLog(
? PlatformInfo.isWeb
: BuildUtils.isDebugMode;
if (!shouldPrint) {
final shouldSentry = _shouldReportToSentry(level);
if (!shouldPrint && !shouldSentry) {
return;
}
final rawMessage = _buildRawMessage(message, exception, extras, stackTrace);
final formattedMessage = _formatMessage(level, rawMessage);
if (webConsoleEnabled && PlatformInfo.isWeb) {
_printWebConsole(level, formattedMessage);
} else {
// ignore: avoid_print
print('$appLogName $formattedMessage');
if (shouldPrint) {
final formattedMessage = _formatMessage(level, rawMessage);
if (webConsoleEnabled && PlatformInfo.isWeb) {
_printWebConsole(level, formattedMessage);
} else {
// ignore: avoid_print
print('$appLogName $formattedMessage');
}
}
if (_shouldReportToSentry(level)) {
SentryManager.instance.captureException(
exception ?? rawMessage,
stackTrace: stackTrace,
message: rawMessage,
extras: extras,
if (shouldSentry) {
unawaited(
SentryManager.instance.captureException(
exception ?? rawMessage,
stackTrace: stackTrace,
message: rawMessage,
extras: extras,
),
);
}
}
+123 -35
View File
@@ -2,71 +2,159 @@ import 'package:core/utils/app_logger.dart';
import 'package:core/utils/platform_info.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:fk_user_agent/fk_user_agent.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:package_info_plus/package_info_plus.dart';
class ApplicationManager {
ApplicationManager._internal();
static final ApplicationManager _instance = ApplicationManager._internal();
factory ApplicationManager() => _instance;
ApplicationManager._internal();
// Allow overriding in unit tests
@visibleForTesting
static DeviceInfoPlugin? debugDeviceInfoOverride;
DeviceInfoPlugin get _deviceInfoPlugin =>
DeviceInfoPlugin get _deviceInfo =>
debugDeviceInfoOverride ?? DeviceInfoPlugin();
Future<PackageInfo> getPackageInfo() async {
final packageInfo = await PackageInfo.fromPlatform();
log('ApplicationManager::getPackageInto: $packageInfo');
return packageInfo;
PackageInfo? _packageInfoCache;
String? _versionCache;
String? _cachedWebUserAgent;
String? _cachedMobileUserAgent;
bool _isMobileUserAgentInitialized = false;
@visibleForTesting
void clearCache() {
_packageInfoCache = null;
_versionCache = null;
_cachedWebUserAgent = null;
_cachedMobileUserAgent = null;
}
Future<String> getVersion() async {
Future<PackageInfo> getPackageInfo() async {
if (_packageInfoCache != null) {
return _packageInfoCache!;
}
final info = await PackageInfo.fromPlatform();
_packageInfoCache = info;
log('ApplicationManager:getPackageInfo -> cached: $info');
return info;
}
Future<String> getAppVersion() async {
if (_versionCache != null) {
return _versionCache!;
}
try {
final version = (await getPackageInfo()).version;
log('ApplicationManager::getVersion: $version');
_versionCache = version;
log('ApplicationManager:getAppVersion -> cached: $version');
return version;
} catch (e) {
return '';
}
}
Future<String> getUserAgent() async {
try {
String userAgent = '';
if (PlatformInfo.isWeb) {
final webBrowserInfo = await _deviceInfoPlugin.webBrowserInfo;
userAgent = webBrowserInfo.userAgent ?? '';
} else if (PlatformInfo.isMobile) {
userAgent = FkUserAgent.userAgent ?? '';
}
log('ApplicationManager::getUserAgent: $userAgent');
return userAgent;
} catch(e) {
logWarning('ApplicationManager::getUserAgent: Exception: $e');
logWarning('ApplicationManager:getAppVersion failedd, Exception = $e');
return '';
}
}
Future<void> initUserAgent() async {
if (PlatformInfo.isMobile) {
if (!PlatformInfo.isMobile) return;
try {
await FkUserAgent.init();
_isMobileUserAgentInitialized = true;
_cachedMobileUserAgent = null;
log('ApplicationManager:initUserAgent -> initialized');
} catch (e) {
logWarning('ApplicationManager:initUserAgent failed, Exception $e');
}
}
Future<void> releaseUserAgent() async {
if (PlatformInfo.isMobile) {
if (!PlatformInfo.isMobile) return;
try {
FkUserAgent.release();
_isMobileUserAgentInitialized = false;
_cachedMobileUserAgent = null;
log('ApplicationManager:releaseUserAgent -> released & cache cleared');
} catch (e) {
logWarning('ApplicationManager:releaseUserAgent failed, Exception $e');
}
}
Future<String> generateApplicationUserAgent() async {
final userAgent = await getUserAgent();
final version = await getVersion();
return 'Twake-Mail/$version $userAgent';
Future<String> getUserAgent() async {
try {
if (PlatformInfo.isWeb) {
return _getWebUserAgent();
}
if (PlatformInfo.isMobile) {
return _getMobileUserAgent();
}
return '';
} catch (e) {
logWarning('ApplicationManager:getUserAgent failed');
return '';
}
}
}
Future<String> _getWebUserAgent() async {
if (_cachedWebUserAgent != null) {
return _cachedWebUserAgent!;
}
WebBrowserInfo webInfo;
try {
webInfo = await _deviceInfo.webBrowserInfo;
} catch (e) {
logWarning('ApplicationManager:_getWebUserAgent failed, Exception $e');
return '';
}
final userAgent = webInfo.userAgent ?? '';
_cachedWebUserAgent = userAgent;
log('ApplicationManager:_getWebUserAgent -> cached');
return userAgent;
}
Future<String> _getMobileUserAgent() async {
if (!_isMobileUserAgentInitialized) {
logWarning(
'ApplicationManager:_getMobileUserAgent called before initUserAgent',
);
return '';
}
if (_cachedMobileUserAgent != null) {
return _cachedMobileUserAgent!;
}
final userAgent = FkUserAgent.userAgent;
if (userAgent == null || userAgent.isEmpty) {
return '';
}
_cachedMobileUserAgent = userAgent;
log('ApplicationManager:_getMobileUserAgent -> cached');
return userAgent;
}
Future<String> generateApplicationUserAgent() async {
final version = await getAppVersion();
final userAgent = await getUserAgent();
return 'Twake-Mail/$version $userAgent'.trim();
}
}
+1 -1
View File
@@ -62,7 +62,7 @@ class SentryConfig {
throw Exception('Sentry configuration is missing');
}
final appVersion = await ApplicationManager().getVersion();
final appVersion = await ApplicationManager().getAppVersion();
return SentryConfig(
dsn: sentryDSN,
+143 -69
View File
@@ -22,6 +22,7 @@ void main() {
ApplicationManager.debugDeviceInfoOverride = mockDeviceInfoPlugin;
applicationManager = ApplicationManager();
applicationManager.clearCache();
});
tearDown(() {
@@ -38,97 +39,170 @@ void main() {
FkUserAgent.release();
});
group('ApplicationManager::getUserAgent test', () {
group('ApplicationManager::getUserAgent', () {
test(
'WHEN platform is Web THEN getUserAgent should return user agent for web',
() async {
const webUserAgent = 'User-Agent-Twake-Mail-Web';
'WHEN platform is Web THEN return web userAgent AND cache it',
() async {
const webUserAgent = 'User-Agent-Twake-Mail-Web';
PlatformInfo.isTestingForWeb = true;
PlatformInfo.isTestingForWeb = true;
when(mockDeviceInfoPlugin.webBrowserInfo).thenAnswer(
(_) async => WebBrowserInfo(
userAgent: webUserAgent,
appCodeName: '',
appName: '',
appVersion: '',
deviceMemory: null,
language: '',
languages: [],
platform: '',
product: '',
productSub: '',
vendor: '',
vendorSub: '',
maxTouchPoints: null,
hardwareConcurrency: null,
),
);
when(mockDeviceInfoPlugin.webBrowserInfo).thenAnswer(
(_) async => WebBrowserInfo(
userAgent: webUserAgent,
appCodeName: '',
appName: '',
appVersion: '',
deviceMemory: null,
language: '',
languages: const [],
platform: '',
product: '',
productSub: '',
vendor: '',
vendorSub: '',
maxTouchPoints: null,
hardwareConcurrency: null,
),
);
final userAgent = await applicationManager.getUserAgent();
final firstCall = await applicationManager.getUserAgent();
final secondCall = await applicationManager.getUserAgent();
expect(userAgent, webUserAgent);
});
expect(firstCall, webUserAgent);
expect(secondCall, webUserAgent);
// platform channel must be called only once (cached)
verify(mockDeviceInfoPlugin.webBrowserInfo).called(1);
},
);
test(
'WHEN platform is Android THEN getUserAgent should return user agent for Android',
() async {
debugDefaultTargetPlatformOverride = TargetPlatform.android;
'WHEN platform is Android THEN return mobile userAgent AND cache it',
() async {
debugDefaultTargetPlatformOverride = TargetPlatform.android;
const androidUserAgent = 'User-Agent-Twake-Mail-Android';
const androidUserAgent = 'User-Agent-Twake-Mail-Android';
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(
const MethodChannel('fk_user_agent'),
(message) async {
if (message.method == 'getProperties') {
return {'userAgent': androidUserAgent};
}
return null;
},
);
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(
const MethodChannel('fk_user_agent'),
(message) async {
if (message.method == 'getProperties') {
return {'userAgent': androidUserAgent};
}
return null;
},
);
await FkUserAgent.init();
await applicationManager.initUserAgent();
final userAgent = await applicationManager.getUserAgent();
final firstCall = await applicationManager.getUserAgent();
final secondCall = await applicationManager.getUserAgent();
expect(userAgent, androidUserAgent);
});
expect(firstCall, androidUserAgent);
expect(secondCall, androidUserAgent);
},
);
test(
'WHEN platform is Web AND mockDeviceInfoPlugin.webBrowserInfo throws exception THEN return empty string',
() async {
PlatformInfo.isTestingForWeb = true;
'WHEN platform is Web AND webBrowserInfo throws exception THEN return empty string',
() async {
PlatformInfo.isTestingForWeb = true;
when(mockDeviceInfoPlugin.webBrowserInfo)
.thenThrow(Exception('Failed to get web browser info'));
when(mockDeviceInfoPlugin.webBrowserInfo).thenAnswer(
(_) => Future<WebBrowserInfo>.error(
Exception('Failed to get web browser info'),
),
);
final userAgent = await applicationManager.getUserAgent();
final userAgent = await applicationManager.getUserAgent();
expect(userAgent, '');
});
expect(userAgent, '');
},
);
test(
'WHEN platform is Android AND FkUserAgent.userAgent empty THEN return empty string',
() async {
debugDefaultTargetPlatformOverride = TargetPlatform.android;
'WHEN platform is Android AND FkUserAgent returns empty THEN return empty string',
() async {
debugDefaultTargetPlatformOverride = TargetPlatform.android;
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(
const MethodChannel('fk_user_agent'),
(message) async {
if (message.method == 'getProperties') {
return {};
}
return null;
},
);
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(
const MethodChannel('fk_user_agent'),
(message) async {
if (message.method == 'getProperties') {
return {};
}
return null;
},
);
await FkUserAgent.init();
await applicationManager.initUserAgent();
final userAgent = await applicationManager.getUserAgent();
final userAgent = await applicationManager.getUserAgent();
expect(userAgent, '');
});
expect(userAgent, '');
},
);
test(
'WHEN mobile userAgent is released THEN getUserAgent returns empty string',
() async {
debugDefaultTargetPlatformOverride = TargetPlatform.android;
const androidUserAgent = 'User-Agent-Twake-Mail-Android';
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(
const MethodChannel('fk_user_agent'),
(message) async {
if (message.method == 'getProperties') {
return {'userAgent': androidUserAgent};
}
return null;
},
);
await applicationManager.initUserAgent();
final firstCall = await applicationManager.getUserAgent();
await applicationManager.releaseUserAgent();
final secondCall = await applicationManager.getUserAgent();
expect(firstCall, androidUserAgent);
expect(secondCall, '');
},
);
test(
'WHEN mobile userAgent is released AND re-initialized THEN cache rebuilt',
() async {
debugDefaultTargetPlatformOverride = TargetPlatform.android;
const androidUserAgent = 'User-Agent-Twake-Mail-Android';
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(
const MethodChannel('fk_user_agent'),
(message) async {
if (message.method == 'getProperties') {
return {'userAgent': androidUserAgent};
}
return null;
},
);
await applicationManager.initUserAgent();
final firstCall = await applicationManager.getUserAgent();
await applicationManager.releaseUserAgent();
await applicationManager.initUserAgent();
final secondCall = await applicationManager.getUserAgent();
expect(firstCall, androidUserAgent);
expect(secondCall, androidUserAgent);
},
);
});
}