diff --git a/.gitignore b/.gitignore index 071964ca9..ef70b75a0 100644 --- a/.gitignore +++ b/.gitignore @@ -78,6 +78,9 @@ android/gradlew.bat **/ios/ServiceDefinitions.json **/ios/Runner/GeneratedPluginRegistrant.* +# Project specific +/PRDs/ + # Exceptions to above rules. !**/ios/**/default.mode1v3 !**/ios/**/default.mode2v3 diff --git a/CHANGELOG.md b/CHANGELOG.md index 039ad6ae1..693d27c06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ### Added +- Add support for respecting backend network body size limits. ([#593](https://github.com/Instabug/Instabug-Flutter/pull/593)) + - Add support for xCode 16. ([#574](https://github.com/Instabug/Instabug-Flutter/pull/574)) - Add support for BugReporting user consents. ([#573](https://github.com/Instabug/Instabug-Flutter/pull/573)) diff --git a/android/src/main/java/com/instabug/flutter/modules/InstabugApi.java b/android/src/main/java/com/instabug/flutter/modules/InstabugApi.java index edfde055a..b77aa316f 100644 --- a/android/src/main/java/com/instabug/flutter/modules/InstabugApi.java +++ b/android/src/main/java/com/instabug/flutter/modules/InstabugApi.java @@ -472,6 +472,11 @@ public void reply(Void reply) { } }); + + featureFlagsFlutterApi.onNetworkLogBodyMaxSizeChange( + (long) featuresState.getNetworkLogCharLimit(), + reply -> {} + ); } }); } @@ -509,4 +514,21 @@ public void setNetworkLogBodyEnabled(@NonNull Boolean isEnabled) { e.printStackTrace(); } } + + @Override + public void getNetworkBodyMaxSize(@NonNull InstabugPigeon.Result result) { + ThreadManager.runOnMainThread( + new Runnable() { + @Override + public void run() { + try { + double networkCharLimit = InternalCore.INSTANCE.get_networkLogCharLimit(); + result.success(networkCharLimit); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + ); + } } diff --git a/android/src/test/java/com/instabug/flutter/InstabugApiTest.java b/android/src/test/java/com/instabug/flutter/InstabugApiTest.java index 97b9cdf7b..cd925a495 100644 --- a/android/src/test/java/com/instabug/flutter/InstabugApiTest.java +++ b/android/src/test/java/com/instabug/flutter/InstabugApiTest.java @@ -658,4 +658,15 @@ public void testSetNetworkLogBodyDisabled() { mInstabug.verify(() -> Instabug.setNetworkLogBodyEnabled(false)); } + + @Test + public void testGetNetworkBodyMaxSize() { + double expected = 10240; + InstabugPigeon.Result result = makeResult((actual) -> assertEquals((Double) expected, actual)); + + mockkObject(new InternalCore[]{InternalCore.INSTANCE}, false); + every(mockKMatcherScope -> InternalCore.INSTANCE.get_networkLogCharLimit()).returns((int) expected); + + api.getNetworkBodyMaxSize(result); + } } diff --git a/example/.gitignore b/example/.gitignore index 9d532b18a..5fae00ed2 100644 --- a/example/.gitignore +++ b/example/.gitignore @@ -39,3 +39,6 @@ app.*.symbols # Obfuscation related app.*.map.json + +# Android related +/android/app/.cxx/ \ No newline at end of file diff --git a/example/ios/InstabugTests/InstabugApiTests.m b/example/ios/InstabugTests/InstabugApiTests.m index 9f2c04373..08b86d011 100644 --- a/example/ios/InstabugTests/InstabugApiTests.m +++ b/example/ios/InstabugTests/InstabugApiTests.m @@ -611,4 +611,20 @@ - (void)testisW3CFeatureFlagsEnabled { } +- (void)testGetNetworkBodyMaxSize { + double expectedValue = 10240.0; + XCTestExpectation *expectation = [self expectationWithDescription:@"Call completion handler"]; + + OCMStub([self.mNetworkLogger getNetworkBodyMaxSize]).andReturn(expectedValue); + + [self.api getNetworkBodyMaxSizeWithCompletion:^(NSNumber *actual, FlutterError *error) { + [expectation fulfill]; + XCTAssertEqual(actual.doubleValue, expectedValue); + XCTAssertNil(error); + }]; + + OCMVerify([self.mNetworkLogger getNetworkBodyMaxSize]); + [self waitForExpectations:@[expectation] timeout:5.0]; +} + @end diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 619d1436c..7bbc162c3 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1,7 +1,7 @@ PODS: - Flutter (1.0.0) - Instabug (15.1.1) - - instabug_flutter (15.0.1): + - instabug_flutter (15.1.1): - Flutter - Instabug (= 15.1.1) - OCMock (3.6) @@ -25,7 +25,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 Instabug: 3e7af445c14d7823fcdecba223f09b5f7c0c6ce1 - instabug_flutter: e4e366434313bab3a2db123c1501ca6247dc950b + instabug_flutter: 30aec1138f5d9c99d175e4f4ad49189ed844b72a OCMock: 5ea90566be239f179ba766fd9fbae5885040b992 PODFILE CHECKSUM: 4d0aaaf6a444f68024f992999ff2c2ee26baa6ec diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 0b15932d1..b02ebfdd2 100644 --- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -68,6 +68,7 @@ ignoresPersistentStateOnLaunch = "NO" debugDocumentVersioning = "YES" debugServiceExtension = "internal" + enableGPUValidationMode = "1" allowLocationSimulation = "YES"> diff --git a/ios/Classes/Modules/InstabugApi.m b/ios/Classes/Modules/InstabugApi.m index 3bdc465f0..b30503742 100644 --- a/ios/Classes/Modules/InstabugApi.m +++ b/ios/Classes/Modules/InstabugApi.m @@ -396,4 +396,9 @@ - (void)setNetworkLogBodyEnabledIsEnabled:(NSNumber *)isEnabled IBGNetworkLogger.logBodyEnabled = [isEnabled boolValue]; } +- (void)getNetworkBodyMaxSizeWithCompletion:(nonnull void (^)(NSNumber * _Nullable, FlutterError * _Nullable))completion { + completion(@(IBGNetworkLogger.getNetworkBodyMaxSize), nil); +} + + @end diff --git a/ios/Classes/Util/IBGNetworkLogger+CP.h b/ios/Classes/Util/IBGNetworkLogger+CP.h index 54cb9d7ef..3670f5f8b 100644 --- a/ios/Classes/Util/IBGNetworkLogger+CP.h +++ b/ios/Classes/Util/IBGNetworkLogger+CP.h @@ -49,6 +49,8 @@ NS_ASSUME_NONNULL_BEGIN duration:(int64_t) duration gqlQueryName:(NSString * _Nullable)gqlQueryName; ++ (double)getNetworkBodyMaxSize; + @end NS_ASSUME_NONNULL_END diff --git a/ios/instabug_flutter.podspec b/ios/instabug_flutter.podspec index 2d898f9c6..d4a553d78 100644 --- a/ios/instabug_flutter.podspec +++ b/ios/instabug_flutter.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'instabug_flutter' - s.version = '15.0.1' + s.version = '15.1.1' s.summary = 'Flutter plugin for integrating the Instabug SDK.' s.author = 'Instabug' s.homepage = 'https://www.instabug.com/platforms/flutter' diff --git a/lib/src/modules/instabug.dart b/lib/src/modules/instabug.dart index 6bba8ed1f..1b11e3904 100644 --- a/lib/src/modules/instabug.dart +++ b/lib/src/modules/instabug.dart @@ -191,7 +191,7 @@ class Instabug { invocationEvents.mapToString(), debugLogsLevel.toString(), ); - return FeatureFlagsManager().registerW3CFlagsListener(); + return FeatureFlagsManager().registerFeatureFlagsListener(); } /// Sets a [callback] to be called wehenever a screen name is captured to mask diff --git a/lib/src/modules/network_logger.dart b/lib/src/modules/network_logger.dart index b7acc74c2..9155ed9a2 100644 --- a/lib/src/modules/network_logger.dart +++ b/lib/src/modules/network_logger.dart @@ -6,6 +6,8 @@ import 'package:instabug_flutter/src/models/network_data.dart'; import 'package:instabug_flutter/src/models/w3c_header.dart'; import 'package:instabug_flutter/src/modules/apm.dart'; import 'package:instabug_flutter/src/utils/feature_flags_manager.dart'; +import 'package:instabug_flutter/src/utils/instabug_constants.dart'; +import 'package:instabug_flutter/src/utils/instabug_logger.dart'; import 'package:instabug_flutter/src/utils/iterable_ext.dart'; import 'package:instabug_flutter/src/utils/network_manager.dart'; import 'package:instabug_flutter/src/utils/w3c_header_utils.dart'; @@ -86,7 +88,36 @@ class NetworkLogger { Future networkLogInternal(NetworkData data) async { final omit = await _manager.omitLog(data); if (omit) return; - final obfuscated = await _manager.obfuscateLog(data); + + // Check size limits early to avoid processing large bodies + final requestExceeds = await _manager.didRequestBodyExceedSizeLimit(data); + final responseExceeds = await _manager.didResponseBodyExceedSizeLimit(data); + + var processedData = data; + if (requestExceeds || responseExceeds) { + // Replace bodies with warning messages + processedData = data.copyWith( + requestBody: requestExceeds + ? InstabugConstants.getRequestBodyReplacementMessage( + data.requestBodySize, + ) + : data.requestBody, + responseBody: responseExceeds + ? InstabugConstants.getResponseBodyReplacementMessage( + data.responseBodySize, + ) + : data.responseBody, + ); + + // Log the truncation event. + final isBothExceeds = requestExceeds && responseExceeds; + InstabugLogger.I.e( + "Truncated network ${isBothExceeds ? 'request and response' : requestExceeds ? 'request' : 'response'} body", + tag: InstabugConstants.networkLoggerTag, + ); + } + + final obfuscated = await _manager.obfuscateLog(processedData); await _host.networkLog(obfuscated.toJson()); await APM.networkLogAndroid(obfuscated); } diff --git a/lib/src/utils/feature_flags_manager.dart b/lib/src/utils/feature_flags_manager.dart index b81dc9777..11f03a3ac 100644 --- a/lib/src/utils/feature_flags_manager.dart +++ b/lib/src/utils/feature_flags_manager.dart @@ -9,6 +9,8 @@ typedef OnW3CFeatureFlagChange = void Function( bool isW3cCaughtHeaderEnabled, ); +typedef OnNetworkBodyMaxSizeChangeCallback = void Function(); + class FeatureFlagsManager implements FeatureFlagsFlutterApi { // Access the singleton instance factory FeatureFlagsManager() { @@ -23,6 +25,10 @@ class FeatureFlagsManager implements FeatureFlagsFlutterApi { // Host API instance static InstabugHostApi _host = InstabugHostApi(); + // Callback for network body max size changes + static OnNetworkBodyMaxSizeChangeCallback? + _onNetworkBodyMaxSizeChangeCallback; + /// @nodoc @visibleForTesting // Setter for the host API @@ -38,10 +44,21 @@ class FeatureFlagsManager implements FeatureFlagsFlutterApi { // since it breaks the singleton pattern } + /// Sets the callback for network body max size changes + // ignore: avoid_setters_without_getters + set onNetworkBodyMaxSizeChangeCallback( + OnNetworkBodyMaxSizeChangeCallback callback, + ) { + _onNetworkBodyMaxSizeChangeCallback = callback; + } + // Internal state flags bool _isAndroidW3CExternalTraceID = false; bool _isAndroidW3CExternalGeneratedHeader = false; bool _isAndroidW3CCaughtHeader = false; + int _networkBodyMaxSize = 0; + + int get networkBodyMaxSize => _networkBodyMaxSize; Future getW3CFeatureFlagsHeader() async { if (IBGBuildInfo.instance.isAndroid) { @@ -64,9 +81,10 @@ class FeatureFlagsManager implements FeatureFlagsFlutterApi { ); } - Future registerW3CFlagsListener() async { + Future registerFeatureFlagsListener() async { FeatureFlagsFlutterApi.setup(this); // Use 'this' instead of _instance + // W3C Feature Flags final featureFlags = await _host.isW3CFeatureFlagsEnabled(); _isAndroidW3CCaughtHeader = featureFlags['isW3cCaughtHeaderEnabled'] ?? false; @@ -75,6 +93,10 @@ class FeatureFlagsManager implements FeatureFlagsFlutterApi { _isAndroidW3CExternalGeneratedHeader = featureFlags['isW3cExternalGeneratedHeaderEnabled'] ?? false; + // Network Body Max Size + final networkBodyMaxSize = await _host.getNetworkBodyMaxSize(); + _networkBodyMaxSize = networkBodyMaxSize?.toInt() ?? 0; + return _host.registerFeatureFlagChangeListener(); } @@ -89,4 +111,10 @@ class FeatureFlagsManager implements FeatureFlagsFlutterApi { _isAndroidW3CExternalTraceID = isW3cExternalTraceIDEnabled; _isAndroidW3CExternalGeneratedHeader = isW3cExternalGeneratedHeaderEnabled; } + + @override + void onNetworkLogBodyMaxSizeChange(int networkBodyMaxSize) { + _networkBodyMaxSize = networkBodyMaxSize; + _onNetworkBodyMaxSizeChangeCallback?.call(); + } } diff --git a/lib/src/utils/instabug_constants.dart b/lib/src/utils/instabug_constants.dart new file mode 100644 index 000000000..fb6dc8abb --- /dev/null +++ b/lib/src/utils/instabug_constants.dart @@ -0,0 +1,31 @@ +/// Constants used throughout the Instabug Flutter SDK +class InstabugConstants { + InstabugConstants._(); + + // Network logging constants + static const String networkLoggerTag = 'NetworkLogger'; + static const String networkManagerTag = 'NetworkManager'; + + // Network body replacement messages + static const String requestBodyReplacedPrefix = '[REQUEST_BODY_REPLACED]'; + static const String responseBodyReplacedPrefix = '[RESPONSE_BODY_REPLACED]'; + static const String exceedsLimitSuffix = 'exceeds limit'; + + /// Generates a request body replacement message + static String getRequestBodyReplacementMessage(int size) { + return '$requestBodyReplacedPrefix - Size: $size $exceedsLimitSuffix'; + } + + /// Generates a response body replacement message + static String getResponseBodyReplacementMessage(int size) { + return '$responseBodyReplacedPrefix - Size: $size $exceedsLimitSuffix'; + } + + /// Generates a network body size limit exceeded log message + static String getNetworkBodyLimitExceededMessage({ + required String type, + required int bodySize, + }) { + return 'Network body size limit exceeded for $type - Size: $bodySize'; + } +} diff --git a/lib/src/utils/network_manager.dart b/lib/src/utils/network_manager.dart index 7aaf8f563..6025af364 100644 --- a/lib/src/utils/network_manager.dart +++ b/lib/src/utils/network_manager.dart @@ -1,6 +1,10 @@ import 'dart:async'; import 'package:instabug_flutter/instabug_flutter.dart'; +import 'package:instabug_flutter/src/generated/instabug.api.g.dart'; +import 'package:instabug_flutter/src/utils/feature_flags_manager.dart'; +import 'package:instabug_flutter/src/utils/instabug_constants.dart'; +import 'package:instabug_flutter/src/utils/instabug_logger.dart'; typedef ObfuscateLogCallback = FutureOr Function(NetworkData data); typedef OmitLogCallback = FutureOr Function(NetworkData data); @@ -10,6 +14,16 @@ typedef OmitLogCallback = FutureOr Function(NetworkData data); class NetworkManager { ObfuscateLogCallback? _obfuscateLogCallback; OmitLogCallback? _omitLogCallback; + int? _cachedNetworkBodyMaxSize; + final int _defaultNetworkBodyMaxSize = 10240; // in bytes + final _host = InstabugHostApi(); + + NetworkManager() { + // Register for network body max size changes + FeatureFlagsManager().onNetworkBodyMaxSizeChangeCallback = () { + clearNetworkBodyMaxSizeCache(); + }; + } // ignore: use_setters_to_change_properties void setObfuscateLogCallback(ObfuscateLogCallback callback) { @@ -36,4 +50,100 @@ class NetworkManager { return _omitLogCallback!(data); } + + /// Checks if network request body exceeds backend size limits + /// + /// Returns true if request body size exceeds the limit + Future didRequestBodyExceedSizeLimit(NetworkData data) async { + try { + final limit = await _getNetworkBodyMaxSize(); + if (limit == null) { + return false; // If we can't get the limit, don't block logging + } + + final requestExceeds = data.requestBodySize > limit; + if (requestExceeds) { + InstabugLogger.I.d( + InstabugConstants.getNetworkBodyLimitExceededMessage( + type: 'request', + bodySize: data.requestBodySize, + ), + tag: InstabugConstants.networkManagerTag, + ); + } + + return requestExceeds; + } catch (error) { + InstabugLogger.I.e( + 'Error checking network request body size limit: $error', + tag: InstabugConstants.networkManagerTag, + ); + return false; // Don't block logging on error + } + } + + /// Checks if network response body exceeds backend size limits + /// + /// Returns true if response body size exceeds the limit + Future didResponseBodyExceedSizeLimit(NetworkData data) async { + try { + final limit = await _getNetworkBodyMaxSize(); + if (limit == null) { + return false; // If we can't get the limit, don't block logging + } + + final responseExceeds = data.responseBodySize > limit; + if (responseExceeds) { + InstabugLogger.I.d( + InstabugConstants.getNetworkBodyLimitExceededMessage( + type: 'response', + bodySize: data.responseBodySize, + ), + tag: InstabugConstants.networkManagerTag, + ); + } + + return responseExceeds; + } catch (error) { + InstabugLogger.I.e( + 'Error checking network response body size limit: $error', + tag: InstabugConstants.networkManagerTag, + ); + return false; // Don't block logging on error + } + } + + /// Gets the network body max size from native SDK, with caching + Future _getNetworkBodyMaxSize() async { + if (_cachedNetworkBodyMaxSize != null) { + return _cachedNetworkBodyMaxSize; + } + + final ffmNetworkBodyLimit = FeatureFlagsManager().networkBodyMaxSize; + + if (ffmNetworkBodyLimit > 0) { + _cachedNetworkBodyMaxSize = ffmNetworkBodyLimit; + return ffmNetworkBodyLimit; + } + + try { + final limit = await _host.getNetworkBodyMaxSize(); + _cachedNetworkBodyMaxSize = limit?.toInt(); + return limit?.toInt(); + } catch (error) { + InstabugLogger.I.e( + 'Failed to get network body max size from native API: $error' + '\n' + 'Setting it to the default value of $_defaultNetworkBodyMaxSize bytes = ${_defaultNetworkBodyMaxSize / 1024} KB', + tag: InstabugConstants.networkManagerTag, + ); + _cachedNetworkBodyMaxSize = _defaultNetworkBodyMaxSize; + return _defaultNetworkBodyMaxSize; + } + } + + /// Clears the cached network body max size + void clearNetworkBodyMaxSizeCache() { + _cachedNetworkBodyMaxSize = null; + } } diff --git a/pigeons/instabug.api.dart b/pigeons/instabug.api.dart index 275306987..505eebf41 100644 --- a/pigeons/instabug.api.dart +++ b/pigeons/instabug.api.dart @@ -7,6 +7,8 @@ abstract class FeatureFlagsFlutterApi { bool isW3cExternalGeneratedHeaderEnabled, bool isW3cCaughtHeaderEnabled, ); + + void onNetworkLogBodyMaxSizeChange(int networkBodyMaxSize); } @HostApi() @@ -76,4 +78,7 @@ abstract class InstabugHostApi { void willRedirectToStore(); void setNetworkLogBodyEnabled(bool isEnabled); + + @async + double? getNetworkBodyMaxSize(); } diff --git a/test/feature_flags_manager_test.dart b/test/feature_flags_manager_test.dart index 1a78f666c..e2e9fdb4d 100644 --- a/test/feature_flags_manager_test.dart +++ b/test/feature_flags_manager_test.dart @@ -54,7 +54,10 @@ void main() { "isW3cCaughtHeaderEnabled": true, }), ); - await FeatureFlagsManager().registerW3CFlagsListener(); + when(mInstabugHost.getNetworkBodyMaxSize()).thenAnswer( + (_) => Future.value(10240), + ); + await FeatureFlagsManager().registerFeatureFlagsListener(); final isW3CExternalTraceID = await FeatureFlagsManager().getW3CFeatureFlagsHeader(); @@ -74,8 +77,11 @@ void main() { "isW3cCaughtHeaderEnabled": true, }), ); + when(mInstabugHost.getNetworkBodyMaxSize()).thenAnswer( + (_) => Future.value(10240), + ); - await FeatureFlagsManager().registerW3CFlagsListener(); + await FeatureFlagsManager().registerFeatureFlagsListener(); verify( mInstabugHost.registerFeatureFlagChangeListener(), diff --git a/test/instabug_test.dart b/test/instabug_test.dart index e2fd7d298..1c3ad4106 100644 --- a/test/instabug_test.dart +++ b/test/instabug_test.dart @@ -78,6 +78,9 @@ void main() { "isW3cCaughtHeaderEnabled": true, }), ); + when(mHost.getNetworkBodyMaxSize()).thenAnswer( + (_) => Future.value(10240), + ); await Instabug.init( token: token, invocationEvents: events, diff --git a/test/network_logger_test.dart b/test/network_logger_test.dart index cbdc2db93..df9951f5e 100644 --- a/test/network_logger_test.dart +++ b/test/network_logger_test.dart @@ -60,6 +60,10 @@ void main() { "isW3cCaughtHeaderEnabled": true, }), ); + when(mManager.didRequestBodyExceedSizeLimit(any)) + .thenAnswer((_) async => false); + when(mManager.didResponseBodyExceedSizeLimit(any)) + .thenAnswer((_) async => false); }); test('[networkLog] should call 1 host method on iOS', () async { @@ -239,4 +243,170 @@ void main() { mInstabugHost.setNetworkLogBodyEnabled(enabled), ).called(1); }); + + group('[networkLogInternal] body size limit tests', () { + test('should replace request body when it exceeds size limit', () async { + final largeRequestData = data.copyWith( + requestBodySize: 15000, // 15KB > 10KB default + responseBodySize: 5000, // 5KB < 10KB default + ); + + when(mBuildInfo.isAndroid).thenReturn(true); + when(mManager.obfuscateLog(any)).thenAnswer((invocation) async { + final inputData = invocation.positionalArguments[0] as NetworkData; + return inputData; + }); + when(mManager.omitLog(largeRequestData)).thenReturn(false); + when(mManager.didRequestBodyExceedSizeLimit(largeRequestData)) + .thenAnswer((_) async => true); + when(mManager.didResponseBodyExceedSizeLimit(largeRequestData)) + .thenAnswer((_) async => false); + + await logger.networkLogInternal(largeRequestData); + + // Verify that obfuscateLog was called with modified data + verify( + mManager.obfuscateLog( + argThat( + predicate( + (processedData) => + processedData.requestBody == + '[REQUEST_BODY_REPLACED] - Size: 15000 exceeds limit' && + processedData.responseBody == largeRequestData.responseBody, + ), + ), + ), + ).called(1); + + // Verify that networkLog was called + verify(mInstabugHost.networkLog(any)).called(1); + verify(mApmHost.networkLogAndroid(any)).called(1); + }); + + test('should replace response body when it exceeds size limit', () async { + final largeResponseData = data.copyWith( + requestBodySize: 5000, // 5KB < 10KB default + responseBodySize: 15000, // 15KB > 10KB default + ); + + when(mBuildInfo.isAndroid).thenReturn(true); + when(mManager.obfuscateLog(any)).thenAnswer((invocation) async { + final inputData = invocation.positionalArguments[0] as NetworkData; + return inputData; + }); + when(mManager.omitLog(largeResponseData)).thenReturn(false); + when(mManager.didRequestBodyExceedSizeLimit(largeResponseData)) + .thenAnswer((_) async => false); + when(mManager.didResponseBodyExceedSizeLimit(largeResponseData)) + .thenAnswer((_) async => true); + + await logger.networkLogInternal(largeResponseData); + + // Verify that obfuscateLog was called with modified data + verify( + mManager.obfuscateLog( + argThat( + predicate( + (processedData) => + processedData.requestBody == largeResponseData.requestBody && + processedData.responseBody == + '[RESPONSE_BODY_REPLACED] - Size: 15000 exceeds limit', + ), + ), + ), + ).called(1); + + // Verify that networkLog was called + verify(mInstabugHost.networkLog(any)).called(1); + verify(mApmHost.networkLogAndroid(any)).called(1); + }); + + test('should replace both bodies when both exceed size limit', () async { + final largeBothData = data.copyWith( + requestBodySize: 15000, // 15KB > 10KB default + responseBodySize: 15000, // 15KB > 10KB default + ); + + when(mBuildInfo.isAndroid).thenReturn(true); + when(mManager.obfuscateLog(any)).thenAnswer((invocation) async { + final inputData = invocation.positionalArguments[0] as NetworkData; + return inputData; + }); + when(mManager.omitLog(largeBothData)).thenReturn(false); + when(mManager.didRequestBodyExceedSizeLimit(largeBothData)) + .thenAnswer((_) async => true); + when(mManager.didResponseBodyExceedSizeLimit(largeBothData)) + .thenAnswer((_) async => true); + + await logger.networkLogInternal(largeBothData); + + // Verify that obfuscateLog was called with modified data + verify( + mManager.obfuscateLog( + argThat( + predicate( + (processedData) => + processedData.requestBody == + '[REQUEST_BODY_REPLACED] - Size: 15000 exceeds limit' && + processedData.responseBody == + '[RESPONSE_BODY_REPLACED] - Size: 15000 exceeds limit', + ), + ), + ), + ).called(1); + + // Verify that networkLog was called + verify(mInstabugHost.networkLog(any)).called(1); + verify(mApmHost.networkLogAndroid(any)).called(1); + }); + + test('should not replace bodies when both are within size limit', () async { + final smallData = data.copyWith( + requestBodySize: 5000, // 5KB < 10KB default + responseBodySize: 5000, // 5KB < 10KB default + ); + + when(mBuildInfo.isAndroid).thenReturn(true); + when(mManager.obfuscateLog(any)).thenAnswer((invocation) async { + final inputData = invocation.positionalArguments[0] as NetworkData; + return inputData; + }); + when(mManager.omitLog(smallData)).thenReturn(false); + when(mManager.didRequestBodyExceedSizeLimit(smallData)) + .thenAnswer((_) async => false); + when(mManager.didResponseBodyExceedSizeLimit(smallData)) + .thenAnswer((_) async => false); + + await logger.networkLogInternal(smallData); + + // Verify that obfuscateLog was called with original data + verify(mManager.obfuscateLog(smallData)).called(1); + + // Verify that networkLog was called + verify(mInstabugHost.networkLog(any)).called(1); + verify(mApmHost.networkLogAndroid(any)).called(1); + }); + + test('should not log when data should be omitted', () async { + final largeData = data.copyWith( + requestBodySize: 15000, // 15KB > 10KB default + ); + + when(mBuildInfo.isAndroid).thenReturn(true); + when(mManager.omitLog(largeData)).thenReturn(true); + + await logger.networkLogInternal(largeData); + + // Verify that omitLog was called + verify(mManager.omitLog(largeData)).called(1); + + // Verify that size limit checks were not called + verifyNever(mManager.didRequestBodyExceedSizeLimit(any)); + verifyNever(mManager.didResponseBodyExceedSizeLimit(any)); + + // Verify that networkLog was not called + verifyNever(mInstabugHost.networkLog(any)); + verifyNever(mApmHost.networkLogAndroid(any)); + }); + }); } diff --git a/test/network_manager_test.dart b/test/network_manager_test.dart index 31776c7ea..daad165dc 100644 --- a/test/network_manager_test.dart +++ b/test/network_manager_test.dart @@ -13,6 +13,8 @@ void main() { url: "https://httpbin.org/get", method: "GET", startTime: DateTime.now(), + requestBodySize: 1000, + responseBodySize: 2000, ); late NetworkManager manager; @@ -76,4 +78,78 @@ void main() { expect(result, equals(omit)); }); + + group('[didRequestBodyExceedSizeLimit]', () { + test('should return false when request body size is within default limit', + () async { + final smallData = + data.copyWith(requestBodySize: 5000); // 5KB < 10KB default + + final result = await manager.didRequestBodyExceedSizeLimit(smallData); + + expect(result, isFalse); + }); + + test('should return true when request body size exceeds default limit', + () async { + final largeData = + data.copyWith(requestBodySize: 15000); // 15KB > 10KB default + + final result = await manager.didRequestBodyExceedSizeLimit(largeData); + + expect(result, isTrue); + }); + + test('should return false when request body size equals default limit', + () async { + final exactData = data.copyWith(requestBodySize: 10240); // Exactly 10KB + + final result = await manager.didRequestBodyExceedSizeLimit(exactData); + + expect(result, isFalse); + }); + + test('should handle errors gracefully and return false', () async { + final result = await manager.didRequestBodyExceedSizeLimit(data); + + expect(result, isA()); + }); + }); + + group('[didResponseBodyExceedSizeLimit]', () { + test('should return false when response body size is within default limit', + () async { + final smallData = + data.copyWith(responseBodySize: 5000); // 5KB < 10KB default + + final result = await manager.didResponseBodyExceedSizeLimit(smallData); + + expect(result, isFalse); + }); + + test('should return true when response body size exceeds default limit', + () async { + final largeData = + data.copyWith(responseBodySize: 15000); // 15KB > 10KB default + + final result = await manager.didResponseBodyExceedSizeLimit(largeData); + + expect(result, isTrue); + }); + + test('should return false when response body size equals default limit', + () async { + final exactData = data.copyWith(responseBodySize: 10240); // Exactly 10KB + + final result = await manager.didResponseBodyExceedSizeLimit(exactData); + + expect(result, isFalse); + }); + + test('should handle errors gracefully and return false', () async { + final result = await manager.didResponseBodyExceedSizeLimit(data); + + expect(result, isA()); + }); + }); }