diff --git a/CHANGELOG.md b/CHANGELOG.md index fccae4fc43..7ec9aad5b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [Unreleased](https://github.com/Instabug/Instabug-React-Native/compare/v13.2.0...dev) + +### Fixed + +- Fix an OOM (out-of-memory) crash while saving network logs on Android ([#1244](https://github.com/Instabug/Instabug-React-Native/pull/1244)). + ## [13.2.0](https://github.com/Instabug/Instabug-React-Native/compare/v13.1.1...v13.2.0) (July 7, 2024) ### Changed diff --git a/android/src/main/java/com/instabug/reactlibrary/RNInstabugAPMModule.java b/android/src/main/java/com/instabug/reactlibrary/RNInstabugAPMModule.java index d7662c6536..7ffaf4de73 100644 --- a/android/src/main/java/com/instabug/reactlibrary/RNInstabugAPMModule.java +++ b/android/src/main/java/com/instabug/reactlibrary/RNInstabugAPMModule.java @@ -5,6 +5,8 @@ import android.util.Log; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; @@ -312,56 +314,58 @@ public void run() { }); } - /** - * Send Apm network log by Reflection - */ @ReactMethod - public void networkLog(String networkData) throws JSONException { - try{ - APMNetworkLogger apmNetworkLogger = new APMNetworkLogger(); - JSONObject jsonObject = new JSONObject(networkData); - final String requestUrl = (String) jsonObject.get("url"); - final String requestBody = (String) jsonObject.get("requestBody"); - final String responseBody = (String) jsonObject.get("responseBody"); - final String requestMethod = (String) jsonObject.get("method"); - //-------------------------------------------- - final String requestContentType = (String) jsonObject.get("requestContentType"); - final String responseContentType = (String) jsonObject.get("contentType"); - //-------------------------------------------- - final long requestBodySize = ((Number) jsonObject.get("requestBodySize")).longValue(); - final long responseBodySize = ((Number) jsonObject.get("responseBodySize")).longValue(); - //-------------------------------------------- - final String errorDomain = (String) jsonObject.get("errorDomain"); - final Integer statusCode = (Integer) jsonObject.get("responseCode"); - final long requestDuration = ((Number) jsonObject.get("duration")).longValue(); - final long requestStartTime = ((Number) jsonObject.get("startTime")).longValue() * 1000; - final String requestHeaders = (String) jsonObject.get("requestHeaders").toString(); - final String responseHeaders = (String) jsonObject.get("responseHeaders").toString(); - final String errorMessage; - if(errorDomain.equals("")) { - errorMessage = null; - } else { - errorMessage = errorDomain; - } - //-------------------------------------------- - String gqlQueryName = null; - if(jsonObject.has("gqlQueryName")){ - gqlQueryName = (String) jsonObject.get("gqlQueryName"); - } - final String serverErrorMessage = (String) jsonObject.get("serverErrorMessage"); + private void networkLogAndroid(final double requestStartTime, + final double requestDuration, + final String requestHeaders, + final String requestBody, + final double requestBodySize, + final String requestMethod, + final String requestUrl, + final String requestContentType, + final String responseHeaders, + final String responseBody, + final double responseBodySize, + final double statusCode, + final String responseContentType, + @Nullable final String errorDomain, + @Nullable final String gqlQueryName, + @Nullable final String serverErrorMessage) { + try { + APMNetworkLogger networkLogger = new APMNetworkLogger(); + + final boolean hasError = errorDomain != null && !errorDomain.isEmpty(); + final String errorMessage = hasError ? errorDomain : null; try { Method method = getMethod(Class.forName("com.instabug.apm.networking.APMNetworkLogger"), "log", long.class, long.class, String.class, String.class, long.class, String.class, String.class, String.class, String.class, String.class, long.class, int.class, String.class, String.class, String.class, String.class); if (method != null) { - method.invoke(apmNetworkLogger, requestStartTime, requestDuration, requestHeaders, requestBody, requestBodySize, requestMethod, requestUrl, requestContentType, responseHeaders, responseBody, responseBodySize, statusCode, responseContentType, errorMessage, gqlQueryName, serverErrorMessage); + method.invoke( + networkLogger, + requestStartTime, + requestDuration, + requestHeaders, + requestBody, + requestBodySize, + requestMethod, + requestUrl, + requestContentType, + responseHeaders, + responseBody, + responseBodySize, + statusCode, + responseContentType, + errorMessage, + gqlQueryName, + serverErrorMessage + ); } else { - Log.e("IB-CP-Bridge", "apmNetworkLogByReflection was not found by reflection"); + Log.e("IB-CP-Bridge", "APMNetworkLogger.log was not found by reflection"); } } catch (Throwable e) { e.printStackTrace(); } - } - catch(Throwable e) { + } catch(Throwable e) { e.printStackTrace(); } } diff --git a/android/src/main/java/com/instabug/reactlibrary/RNInstabugReactnativeModule.java b/android/src/main/java/com/instabug/reactlibrary/RNInstabugReactnativeModule.java index c056f15831..7d14656089 100644 --- a/android/src/main/java/com/instabug/reactlibrary/RNInstabugReactnativeModule.java +++ b/android/src/main/java/com/instabug/reactlibrary/RNInstabugReactnativeModule.java @@ -5,6 +5,7 @@ import android.app.Application; import android.graphics.Bitmap; import android.net.Uri; +import android.util.Log; import android.view.View; import androidx.annotation.UiThread; @@ -34,6 +35,7 @@ import com.instabug.library.model.NetworkLog; import com.instabug.library.model.Report; import com.instabug.library.ui.onboarding.WelcomeMessage; +import com.instabug.library.util.InstabugSDKLogger; import com.instabug.reactlibrary.utils.ArrayUtil; import com.instabug.reactlibrary.utils.EventEmitterModule; import com.instabug.reactlibrary.utils.MainThreadHandler; @@ -60,7 +62,7 @@ */ public class RNInstabugReactnativeModule extends EventEmitterModule { - private static final String TAG = RNInstabugReactnativeModule.class.getSimpleName(); + private static final String TAG = "IBG-RN-Core"; private InstabugCustomTextPlaceHolder placeHolders; private static Report currentReport; @@ -895,27 +897,38 @@ public void run() { }); } - /** - * Extracts HTTP connection properties. Request method, Headers, Date, Url and Response code - * - * @param jsonObject the JSON object containing all HTTP connection properties - * @throws JSONException - */ @ReactMethod - public void networkLog(String jsonObject) throws JSONException { - NetworkLog networkLog = new NetworkLog(); - String date = System.currentTimeMillis()+""; - networkLog.setDate(date); - JSONObject newJSONObject = new JSONObject(jsonObject); - networkLog.setUrl(newJSONObject.getString("url")); - networkLog.setRequest(newJSONObject.getString("requestBody")); - networkLog.setResponse(newJSONObject.getString("responseBody")); - networkLog.setMethod(newJSONObject.getString("method")); - networkLog.setResponseCode(newJSONObject.getInt("responseCode")); - networkLog.setRequestHeaders(newJSONObject.getString("requestHeaders")); - networkLog.setResponseHeaders(newJSONObject.getString("responseHeaders")); - networkLog.setTotalDuration(newJSONObject.getLong("duration")); - networkLog.insert(); + public void networkLogAndroid(final String url, + final String requestBody, + final String responseBody, + final String method, + final double responseCode, + final String requestHeaders, + final String responseHeaders, + final double duration) { + try { + final String date = String.valueOf(System.currentTimeMillis()); + + NetworkLog networkLog = new NetworkLog(); + networkLog.setDate(date); + networkLog.setUrl(url); + networkLog.setMethod(method); + networkLog.setResponseCode((int) responseCode); + networkLog.setTotalDuration((long) duration); + + try { + networkLog.setRequest(requestBody); + networkLog.setResponse(responseBody); + networkLog.setRequestHeaders(requestHeaders); + networkLog.setResponseHeaders(responseHeaders); + } catch (OutOfMemoryError | Exception exception) { + Log.d(TAG, "Error: " + exception.getMessage() + "while trying to set network log contents (request body, response body, request headers, and response headers)."); + } + + networkLog.insert(); + } catch (OutOfMemoryError | Exception exception) { + Log.d(TAG, "Error: " + exception.getMessage() + "while trying to insert a network log"); + } } @UiThread diff --git a/examples/default/ios/InstabugTests/InstabugSampleTests.m b/examples/default/ios/InstabugTests/InstabugSampleTests.m index bf4edbd434..37990b450c 100644 --- a/examples/default/ios/InstabugTests/InstabugSampleTests.m +++ b/examples/default/ios/InstabugTests/InstabugSampleTests.m @@ -13,6 +13,7 @@ #import #import "IBGConstants.h" #import "RNInstabug.h" +#import @protocol InstabugCPTestProtocol /** @@ -313,6 +314,61 @@ - (void)testSetWelcomeMessageMode { OCMVerify([mock setWelcomeMessageMode:welcomeMessageMode]); } +- (void)testNetworkLogIOS { + id mIBGNetworkLogger = OCMClassMock([IBGNetworkLogger class]); + + NSString *url = @"https://api.instabug.com"; + NSString *method = @"GET"; + NSString *requestBody = @"requestBody"; + double requestBodySize = 10; + NSString *responseBody = @"responseBody"; + double responseBodySize = 15; + double responseCode = 200; + NSDictionary *requestHeaders = @{ @"accept": @"application/json" }; + NSDictionary *responseHeaders = @{ @"cache-control": @"no-store" }; + NSString *contentType = @"application/json"; + double errorCode = 0; + NSString *errorDomain = nil; + double startTime = 1719847101199; + double duration = 150; + NSString *gqlQueryName = nil; + NSString *serverErrorMessage = nil; + + [self.instabugBridge networkLogIOS:url + method:method + requestBody:requestBody + requestBodySize:requestBodySize + responseBody:responseBody + responseBodySize:responseBodySize + responseCode:responseCode + requestHeaders:requestHeaders + responseHeaders:responseHeaders + contentType:contentType + errorDomain:errorDomain + errorCode:errorCode + startTime:startTime + duration:duration + gqlQueryName:gqlQueryName + serverErrorMessage:serverErrorMessage]; + + OCMVerify([mIBGNetworkLogger addNetworkLogWithUrl:url + method:method + requestBody:requestBody + requestBodySize:requestBodySize + responseBody:responseBody + responseBodySize:responseBodySize + responseCode:responseCode + requestHeaders:requestHeaders + responseHeaders:responseHeaders + contentType:contentType + errorDomain:errorDomain + errorCode:errorCode + startTime:startTime * 1000 + duration:duration * 1000 + gqlQueryName:gqlQueryName + serverErrorMessage:serverErrorMessage]); +} + - (void)testSetFileAttachment { id mock = OCMClassMock([Instabug class]); NSString *fileLocation = @"test"; diff --git a/ios/RNInstabug/InstabugReactBridge.h b/ios/RNInstabug/InstabugReactBridge.h index 9e9a8db489..5da9b9cc71 100644 --- a/ios/RNInstabug/InstabugReactBridge.h +++ b/ios/RNInstabug/InstabugReactBridge.h @@ -107,6 +107,23 @@ - (void)setNetworkLoggingEnabled:(BOOL)isEnabled; +- (void)networkLogIOS:(NSString * _Nonnull)url + method:(NSString * _Nonnull)method + requestBody:(NSString * _Nonnull)requestBody + requestBodySize:(double)requestBodySize + responseBody:(NSString * _Nonnull)responseBody + responseBodySize:(double)responseBodySize + responseCode:(double)responseCode + requestHeaders:(NSDictionary * _Nonnull)requestHeaders + responseHeaders:(NSDictionary * _Nonnull)responseHeaders + contentType:(NSString * _Nonnull)contentType + errorDomain:(NSString * _Nullable)errorDomain + errorCode:(double)errorCode + startTime:(double)startTime + duration:(double)duration + gqlQueryName:(NSString * _Nullable)gqlQueryName + serverErrorMessage:(NSString * _Nullable)serverErrorMessage; + /* +------------------------------------------------------------------------+ | Experiments | diff --git a/ios/RNInstabug/InstabugReactBridge.m b/ios/RNInstabug/InstabugReactBridge.m index 01dd41aa87..2f2b24f27a 100644 --- a/ios/RNInstabug/InstabugReactBridge.m +++ b/ios/RNInstabug/InstabugReactBridge.m @@ -15,6 +15,7 @@ #import #import #import "RNInstabug.h" +#import "Util/IBGNetworkLogger+CP.h" @interface Instabug (PrivateWillSendAPI) + (void)setWillSendReportHandler_private:(void(^)(IBGReport *report, void(^reportCompletionHandler)(IBGReport *)))willSendReportHandler_private; @@ -283,66 +284,38 @@ - (dispatch_queue_t)methodQueue { } } -RCT_EXPORT_METHOD(networkLog:(NSDictionary *) networkData) { - NSString* url = networkData[@"url"]; - NSString* method = networkData[@"method"]; - NSString* requestBody = networkData[@"requestBody"]; - int64_t requestBodySize = [networkData[@"requestBodySize"] integerValue]; - NSString* responseBody = nil; - if (networkData[@"responseBody"] != [NSNull null]) { - responseBody = networkData[@"responseBody"]; - } - int64_t responseBodySize = [networkData[@"responseBodySize"] integerValue]; - int32_t responseCode = [networkData[@"responseCode"] integerValue]; - NSDictionary* requestHeaders = @{}; - if([networkData[@"requestHeaders"] isKindOfClass:[NSDictionary class]]){ - requestHeaders = networkData[@"requestHeaders"]; - } - NSDictionary* responseHeaders = @{}; - if([networkData[@"responseHeaders"] isKindOfClass:[NSDictionary class]]){ - responseHeaders = networkData[@"responseHeaders"]; - } - NSString* contentType = networkData[@"contentType"]; - NSString* errorDomain = networkData[@"errorDomain"]; - int32_t errorCode = [networkData[@"errorCode"] integerValue]; - int64_t startTime = [networkData[@"startTime"] integerValue] * 1000; - int64_t duration = [networkData[@"duration"] doubleValue] * 1000; - - NSString* gqlQueryName = nil; - NSString* serverErrorMessage = nil; - if (networkData[@"gqlQueryName"] != [NSNull null]) { - gqlQueryName = networkData[@"gqlQueryName"]; - } - if (networkData[@"serverErrorMessage"] != [NSNull null]) { - serverErrorMessage = networkData[@"serverErrorMessage"]; - } - - SEL networkLogSEL = NSSelectorFromString(@"addNetworkLogWithUrl:method:requestBody:requestBodySize:responseBody:responseBodySize:responseCode:requestHeaders:responseHeaders:contentType:errorDomain:errorCode:startTime:duration:gqlQueryName:serverErrorMessage:"); - - if([[IBGNetworkLogger class] respondsToSelector:networkLogSEL]) { - NSInvocation *inv = [NSInvocation invocationWithMethodSignature:[[IBGNetworkLogger class] methodSignatureForSelector:networkLogSEL]]; - [inv setSelector:networkLogSEL]; - [inv setTarget:[IBGNetworkLogger class]]; - - [inv setArgument:&(url) atIndex:2]; - [inv setArgument:&(method) atIndex:3]; - [inv setArgument:&(requestBody) atIndex:4]; - [inv setArgument:&(requestBodySize) atIndex:5]; - [inv setArgument:&(responseBody) atIndex:6]; - [inv setArgument:&(responseBodySize) atIndex:7]; - [inv setArgument:&(responseCode) atIndex:8]; - [inv setArgument:&(requestHeaders) atIndex:9]; - [inv setArgument:&(responseHeaders) atIndex:10]; - [inv setArgument:&(contentType) atIndex:11]; - [inv setArgument:&(errorDomain) atIndex:12]; - [inv setArgument:&(errorCode) atIndex:13]; - [inv setArgument:&(startTime) atIndex:14]; - [inv setArgument:&(duration) atIndex:15]; - [inv setArgument:&(gqlQueryName) atIndex:16]; - [inv setArgument:&(serverErrorMessage) atIndex:17]; - - [inv invoke]; - } +RCT_EXPORT_METHOD(networkLogIOS:(NSString * _Nonnull)url + method:(NSString * _Nonnull)method + requestBody:(NSString * _Nonnull)requestBody + requestBodySize:(double)requestBodySize + responseBody:(NSString * _Nonnull)responseBody + responseBodySize:(double)responseBodySize + responseCode:(double)responseCode + requestHeaders:(NSDictionary * _Nonnull)requestHeaders + responseHeaders:(NSDictionary * _Nonnull)responseHeaders + contentType:(NSString * _Nonnull)contentType + errorDomain:(NSString * _Nullable)errorDomain + errorCode:(double)errorCode + startTime:(double)startTime + duration:(double)duration + gqlQueryName:(NSString * _Nullable)gqlQueryName + serverErrorMessage:(NSString * _Nullable)serverErrorMessage) { + [IBGNetworkLogger addNetworkLogWithUrl:url + method:method + requestBody:requestBody + requestBodySize:requestBodySize + responseBody:responseBody + responseBodySize:responseBodySize + responseCode:responseCode + requestHeaders:requestHeaders + responseHeaders:responseHeaders + contentType:contentType + errorDomain:errorDomain + errorCode:errorCode + startTime:startTime * 1000 + duration:duration * 1000 + gqlQueryName:gqlQueryName + serverErrorMessage:serverErrorMessage]; } RCT_EXPORT_METHOD(addPrivateView: (nonnull NSNumber *)reactTag) { diff --git a/ios/RNInstabug/Util/IBGNetworkLogger+CP.h b/ios/RNInstabug/Util/IBGNetworkLogger+CP.h index ae5d32d669..5ae464785f 100644 --- a/ios/RNInstabug/Util/IBGNetworkLogger+CP.h +++ b/ios/RNInstabug/Util/IBGNetworkLogger+CP.h @@ -5,6 +5,22 @@ NS_ASSUME_NONNULL_BEGIN @interface IBGNetworkLogger (CP) + (void)disableAutomaticCapturingOfNetworkLogs; ++ (void)addNetworkLogWithUrl:(NSString *)url + method:(NSString *)method + requestBody:(NSString *)request + requestBodySize:(int64_t)requestBodySize + responseBody:(NSString *)response + responseBodySize:(int64_t)responseBodySize + responseCode:(int32_t)code + requestHeaders:(NSDictionary *)requestHeaders + responseHeaders:(NSDictionary *)responseHeaders + contentType:(NSString *)contentType + errorDomain:(NSString *)errorDomain + errorCode:(int32_t)errorCode + startTime:(int64_t)startTime + duration:(int64_t) duration + gqlQueryName:(NSString * _Nullable)gqlQueryName + serverErrorMessage:(NSString * _Nullable)serverErrorMessage; @end diff --git a/src/modules/NetworkLogger.ts b/src/modules/NetworkLogger.ts index 8e102e3ab3..67f3a54ccf 100644 --- a/src/modules/NetworkLogger.ts +++ b/src/modules/NetworkLogger.ts @@ -1,11 +1,8 @@ -import { Platform } from 'react-native'; - import type { RequestHandler } from '@apollo/client'; -import { NativeAPM } from '../native/NativeAPM'; -import { NativeInstabug } from '../native/NativeInstabug'; import InstabugConstants from '../utils/InstabugConstants'; import xhr, { NetworkData, ProgressCallback } from '../utils/XhrNetworkInterceptor'; +import { reportNetworkLog, isContentTypeNotAllowed } from '../utils/InstabugUtils'; export type { NetworkData }; @@ -30,12 +27,31 @@ export const setEnabled = (isEnabled: boolean) => { network = await _networkDataObfuscationHandler(network); } - if (Platform.OS === 'android') { - NativeInstabug.networkLog(JSON.stringify(network)); - NativeAPM.networkLog(JSON.stringify(network)); - } else { - NativeInstabug.networkLog(network); + if (network.requestBodySize > InstabugConstants.MAX_NETWORK_BODY_SIZE_IN_BYTES) { + network.requestBody = InstabugConstants.MAX_REQUEST_BODY_SIZE_EXCEEDED_MESSAGE; + console.warn('IBG-RN:', InstabugConstants.MAX_REQUEST_BODY_SIZE_EXCEEDED_MESSAGE); + } + + if (network.responseBodySize > InstabugConstants.MAX_NETWORK_BODY_SIZE_IN_BYTES) { + network.responseBody = InstabugConstants.MAX_RESPONSE_BODY_SIZE_EXCEEDED_MESSAGE; + console.warn('IBG-RN:', InstabugConstants.MAX_RESPONSE_BODY_SIZE_EXCEEDED_MESSAGE); + } + + if (network.requestBody && isContentTypeNotAllowed(network.requestContentType)) { + network.requestBody = `Body is omitted because content type ${network.requestContentType} isn't supported`; + console.warn( + `IBG-RN: The request body for the network request with URL ${network.url} has been omitted because the content type ${network.requestContentType} isn't supported.`, + ); + } + + if (network.responseBody && isContentTypeNotAllowed(network.contentType)) { + network.responseBody = `Body is omitted because content type ${network.contentType} isn't supported`; + console.warn( + `IBG-RN: The response body for the network request with URL ${network.url} has been omitted because the content type ${network.contentType} isn't supported.`, + ); } + + reportNetworkLog(network); } catch (e) { console.error(e); } diff --git a/src/native/NativeAPM.ts b/src/native/NativeAPM.ts index 2c2121587c..b1981cfe37 100644 --- a/src/native/NativeAPM.ts +++ b/src/native/NativeAPM.ts @@ -7,7 +7,24 @@ export interface ApmNativeModule extends NativeModule { setEnabled(isEnabled: boolean): void; // Network APIs // - networkLog(data: string): void; + networkLogAndroid( + requestStartTime: number, + requestDuration: number, + requestHeaders: string, + requestBody: string, + requestBodySize: number, + requestMethod: string, + requestUrl: string, + requestContentType: string, + responseHeaders: string, + responseBody: string | null, + responseBodySize: number, + statusCode: number, + responseContentType: string, + errorDomain: string, + gqlQueryName?: string, + serverErrorMessage?: string, + ): void; // App Launches APIs // setAppLaunchEnabled(isEnabled: boolean): void; diff --git a/src/native/NativeInstabug.ts b/src/native/NativeInstabug.ts index 3ff68de26b..3990da4c8b 100644 --- a/src/native/NativeInstabug.ts +++ b/src/native/NativeInstabug.ts @@ -10,7 +10,6 @@ import type { StringKey, WelcomeMessageMode, } from '../utils/Enums'; -import type { NetworkData } from '../utils/XhrNetworkInterceptor'; import type { NativeConstants } from './NativeConstants'; import { NativeModules } from './NativePackage'; @@ -40,7 +39,36 @@ export interface InstabugNativeModule extends NativeModule { setString(string: string, key: StringKey): void; // Network APIs // - networkLog(network: NetworkData | string): void; + networkLogAndroid( + url: string, + requestBody: string, + responseBody: string | null, + method: string, + responseCode: number, + requestHeaders: string, + responseHeaders: string, + duration: number, + ): void; + + networkLogIOS( + url: string, + method: string, + requestBody: string | null, + requestBodySize: number, + responseBody: string | null, + responseBodySize: number, + responseCode: number, + requestHeaders: Record, + responseHeaders: Record, + contentType: string, + errorDomain: string, + errorCode: number, + startTime: number, + duration: number, + gqlQueryName: string | undefined, + serverErrorMessage: string | undefined, + ): void; + setNetworkLoggingEnabled(isEnabled: boolean): void; // Repro Steps APIs // diff --git a/src/utils/InstabugConstants.ts b/src/utils/InstabugConstants.ts index c7b4988771..59bdb2324f 100644 --- a/src/utils/InstabugConstants.ts +++ b/src/utils/InstabugConstants.ts @@ -1,5 +1,12 @@ -enum InstabugConstants { - GRAPHQL_HEADER = 'ibg-graphql-header', -} +const InstabugConstants = { + GRAPHQL_HEADER: 'ibg-graphql-header', + + // TODO: dyanmically get the max size from the native SDK and update the error message to reflect the dynamic size. + MAX_NETWORK_BODY_SIZE_IN_BYTES: 1024 * 10, // 10 KB + MAX_RESPONSE_BODY_SIZE_EXCEEDED_MESSAGE: + 'The response body has not been logged because it exceeds the maximum size of 10 Kb', + MAX_REQUEST_BODY_SIZE_EXCEEDED_MESSAGE: + 'The request body has not been logged because it exceeds the maximum size of 10 Kb', +}; export default InstabugConstants; diff --git a/src/utils/InstabugUtils.ts b/src/utils/InstabugUtils.ts index 6a8dfb7405..d4238f14f0 100644 --- a/src/utils/InstabugUtils.ts +++ b/src/utils/InstabugUtils.ts @@ -8,7 +8,10 @@ import type { NavigationState as NavigationStateV5, PartialState } from '@react- import type { NavigationState as NavigationStateV4 } from 'react-navigation'; import type { CrashData } from '../native/NativeCrashReporting'; +import type { NetworkData } from './XhrNetworkInterceptor'; import { NativeCrashReporting } from '../native/NativeCrashReporting'; +import { NativeInstabug } from '../native/NativeInstabug'; +import { NativeAPM } from '../native/NativeAPM'; export const parseErrorStack = (error: ExtendedError): StackFrame[] => { return parseErrorStackLib(error); @@ -124,6 +127,75 @@ export async function sendCrashReport( return remoteSenderCallback(jsonObject); } +export function isContentTypeNotAllowed(contentType: string) { + const allowed = [ + 'application/protobuf', + 'application/json', + 'application/xml', + 'text/xml', + 'text/html', + 'text/plain', + ]; + + return allowed.every((type) => !contentType.includes(type)); +} + +export function reportNetworkLog(network: NetworkData) { + if (Platform.OS === 'android') { + const requestHeaders = JSON.stringify(network.requestHeaders); + const responseHeaders = JSON.stringify(network.responseHeaders); + + NativeInstabug.networkLogAndroid( + network.url, + network.requestBody, + network.responseBody, + network.method, + network.responseCode, + requestHeaders, + responseHeaders, + network.duration, + ); + + NativeAPM.networkLogAndroid( + network.startTime, + network.duration, + requestHeaders, + network.requestBody, + network.requestBodySize, + network.method, + network.url, + network.requestContentType, + responseHeaders, + network.responseBody, + network.responseBodySize, + network.responseCode, + network.contentType, + network.errorDomain, + network.gqlQueryName, + network.serverErrorMessage, + ); + } else { + NativeInstabug.networkLogIOS( + network.url, + network.method, + network.requestBody, + network.requestBodySize, + network.responseBody, + network.responseBodySize, + network.responseCode, + network.requestHeaders, + network.responseHeaders, + network.contentType, + network.errorDomain, + network.errorCode, + network.startTime, + network.duration, + network.gqlQueryName, + network.serverErrorMessage, + ); + } +} + export default { parseErrorStack, captureJsErrors, diff --git a/src/utils/XhrNetworkInterceptor.ts b/src/utils/XhrNetworkInterceptor.ts index ad79f2d4fa..701533f15c 100644 --- a/src/utils/XhrNetworkInterceptor.ts +++ b/src/utils/XhrNetworkInterceptor.ts @@ -1,4 +1,5 @@ import InstabugConstants from './InstabugConstants'; +import { stringifyIfNotString } from './InstabugUtils'; export type ProgressCallback = (totalBytesSent: number, totalBytesExpectedToSend: number) => void; export type NetworkDataCallback = (data: NetworkData) => void; @@ -80,7 +81,12 @@ export default { }; XMLHttpRequest.prototype.setRequestHeader = function (header, value) { - network.requestHeaders[header] = typeof value === 'string' ? value : JSON.stringify(value); + // According to the HTTP RFC, headers are case-insensitive, so we convert + // them to lower-case to make accessing headers predictable. + // This avoid issues like failing to get the Content-Type header for a request + // because the header is set as 'Content-Type' instead of 'content-type'. + const key = header.toLowerCase(); + network.requestHeaders[key] = stringifyIfNotString(value); originalXHRSetRequestHeader.apply(this, [header, value]); }; diff --git a/test/mocks/mockAPM.ts b/test/mocks/mockAPM.ts index ca9dd297de..27644c694a 100644 --- a/test/mocks/mockAPM.ts +++ b/test/mocks/mockAPM.ts @@ -16,7 +16,7 @@ const mockAPM: ApmNativeModule = { endUITrace: jest.fn(), endAppLaunch: jest.fn(), ibgSleep: jest.fn(), - networkLog: jest.fn(), + networkLogAndroid: jest.fn(), }; export default mockAPM; diff --git a/test/mocks/mockInstabug.ts b/test/mocks/mockInstabug.ts index bb69eda517..c2dac12b5a 100644 --- a/test/mocks/mockInstabug.ts +++ b/test/mocks/mockInstabug.ts @@ -52,7 +52,8 @@ const mockInstabug: InstabugNativeModule = { addExperiments: jest.fn(), removeExperiments: jest.fn(), clearAllExperiments: jest.fn(), - networkLog: jest.fn(), + networkLogIOS: jest.fn(), + networkLogAndroid: jest.fn(), appendTagToReport: jest.fn(), appendConsoleLogToReport: jest.fn(), setUserAttributeToReport: jest.fn(), diff --git a/test/mocks/mockInstabugUtils.ts b/test/mocks/mockInstabugUtils.ts index 335dd5c016..7d58be394e 100644 --- a/test/mocks/mockInstabugUtils.ts +++ b/test/mocks/mockInstabugUtils.ts @@ -10,5 +10,7 @@ jest.mock('../../src/utils/InstabugUtils', () => { sendCrashReport: jest.fn(), getStackTrace: jest.fn().mockReturnValue('javascriptStackTrace'), getFullRoute: jest.fn().mockImplementation(() => 'ScreenName'), + reportNetworkLog: jest.fn(), + isContentTypeNotAllowed: jest.fn(), }; }); diff --git a/test/modules/NetworkLogger.spec.ts b/test/modules/NetworkLogger.spec.ts index de121c636e..71dd2dd778 100644 --- a/test/modules/NetworkLogger.spec.ts +++ b/test/modules/NetworkLogger.spec.ts @@ -1,34 +1,40 @@ import '../mocks/mockXhrNetworkInterceptor'; - -import { Platform } from 'react-native'; +import '../mocks/mockInstabugUtils'; import waitForExpect from 'wait-for-expect'; import * as NetworkLogger from '../../src/modules/NetworkLogger'; -import { NativeAPM } from '../../src/native/NativeAPM'; -import { NativeInstabug } from '../../src/native/NativeInstabug'; import Interceptor from '../../src/utils/XhrNetworkInterceptor'; +import { isContentTypeNotAllowed, reportNetworkLog } from '../../src/utils/InstabugUtils'; +import InstabugConstants from '../../src/utils/InstabugConstants'; -const clone = (obj: any) => { +const clone = (obj: T): T => { return JSON.parse(JSON.stringify(obj)); }; describe('NetworkLogger Module', () => { - const network = { + const network: NetworkLogger.NetworkData = { url: 'https://api.instabug.com', requestBody: '', - requestHeaders: { 'Content-type': 'application/json' }, + requestHeaders: { 'content-type': 'application/json' }, method: 'GET', responseBody: '', responseCode: 200, - responseHeaders: '', + responseHeaders: { 'content-type': 'application/json' }, contentType: 'application/json', duration: 0, + requestBodySize: 0, + responseBodySize: 0, + errorDomain: '', + errorCode: 0, + startTime: 0, + serverErrorMessage: '', + requestContentType: 'application/json', }; beforeEach(() => { - // @ts-ignore NetworkLogger.setNetworkDataObfuscationHandler(null); + NetworkLogger.setRequestFilterExpression('false'); }); it('should set onProgressCallback with callback', () => { @@ -52,52 +58,18 @@ describe('NetworkLogger Module', () => { expect(Interceptor.disableInterception).toBeCalledTimes(1); }); - it('should send log network when Platform is ios', () => { - Platform.OS = 'ios'; + it('should report the network log', () => { Interceptor.setOnDoneCallback = jest .fn() .mockImplementation((callback) => callback(clone(network))); - NetworkLogger.setEnabled(true); - expect(NativeInstabug.networkLog).toBeCalledTimes(1); - expect(NativeInstabug.networkLog).toBeCalledWith(network); - }); - - it('should send log network when Platform is android', () => { - Platform.OS = 'android'; - Interceptor.setOnDoneCallback = jest - .fn() - .mockImplementation((callback) => callback(clone(network))); NetworkLogger.setEnabled(true); - expect(NativeInstabug.networkLog).toBeCalledWith(JSON.stringify(network)); - expect(NativeAPM.networkLog).toBeCalledWith(JSON.stringify(network)); - }); - - it('should not break if it fails to stringify to JSON on network log if platform is android', () => { - Platform.OS = 'android'; - - // Avoid the console.error to clutter the test log - const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); - - // Make a circular object, this should make JSON.stringify fail - const networkResult = clone(network); - networkResult.responseBody = {}; - networkResult.responseBody.result = { body: networkResult.responseBody }; - - Interceptor.setOnDoneCallback = jest - .fn() - .mockImplementation((callback) => callback(networkResult)); - - expect(() => NetworkLogger.setEnabled(true)).not.toThrow(); - expect(NativeInstabug.networkLog).not.toBeCalled(); - expect(NativeAPM.networkLog).not.toBeCalled(); - - consoleSpy.mockRestore(); + expect(reportNetworkLog).toBeCalledTimes(1); + expect(reportNetworkLog).toBeCalledWith(network); }); - it('should send log network when setNetworkDataObfuscationHandler is set and Platform is ios', async () => { - Platform.OS = 'ios'; + it('should send log network when setNetworkDataObfuscationHandler is set', async () => { const randomString = '28930q938jqhd'; Interceptor.setOnDoneCallback = jest .fn() @@ -111,33 +83,11 @@ describe('NetworkLogger Module', () => { await waitForExpect(() => { const newData = clone(network); newData.requestHeaders.token = randomString; - expect(NativeInstabug.networkLog).toBeCalledWith(newData); + expect(reportNetworkLog).toBeCalledWith(newData); }); }); - it('should send log network when setNetworkDataObfuscationHandler is set and Platform is android', async () => { - Platform.OS = 'android'; - const randomString = '28930q938jqhd'; - Interceptor.setOnDoneCallback = jest - .fn() - .mockImplementation((callback) => callback(clone(network))); - NetworkLogger.setNetworkDataObfuscationHandler((networkData) => { - networkData.requestHeaders.token = randomString; - return Promise.resolve(networkData); - }); - NetworkLogger.setEnabled(true); - - await waitForExpect(() => { - const newData = clone(network); - newData.requestHeaders.token = randomString; - expect(NativeInstabug.networkLog).toBeCalledWith(JSON.stringify(newData)); - expect(NativeAPM.networkLog).toBeCalledWith(JSON.stringify(newData)); - }); - }); - - it('should not break if network data obfuscation fails when platform is android', async () => { - Platform.OS = 'android'; - + it('should not break if network data obfuscation fails', async () => { // Avoid the console.error to clutter the test log const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); @@ -152,8 +102,7 @@ describe('NetworkLogger Module', () => { NetworkLogger.setNetworkDataObfuscationHandler(handler); expect(() => NetworkLogger.setEnabled(true)).not.toThrow(); - expect(NativeInstabug.networkLog).not.toBeCalled(); - expect(NativeAPM.networkLog).not.toBeCalled(); + expect(reportNetworkLog).not.toBeCalled(); consoleSpy.mockRestore(); }); @@ -164,12 +113,11 @@ describe('NetworkLogger Module', () => { .mockImplementation((callback) => callback(clone(network))); NetworkLogger.setRequestFilterExpression( - "network.requestHeaders['Content-type'] === 'application/json'", + "network.requestHeaders['content-type'] === 'application/json'", ); NetworkLogger.setEnabled(true); - expect(NativeInstabug.networkLog).not.toBeCalled(); - expect(NativeAPM.networkLog).not.toBeCalled(); + expect(reportNetworkLog).not.toBeCalled(); }); it('should test that operationSetContext at apollo handler called', async () => { @@ -200,4 +148,132 @@ describe('NetworkLogger Module', () => { consoleSpy.mockRestore(); }); + + it('should omit request body if its content type is not allowed', () => { + const consoleWarn = jest.spyOn(console, 'warn').mockImplementation(); + jest.mocked(isContentTypeNotAllowed).mockReturnValueOnce(true); + + const networkData = { + ...network, + requestBody: 'some request body', + }; + + Interceptor.setOnDoneCallback = jest + .fn() + .mockImplementation((callback) => callback(networkData)); + + NetworkLogger.setEnabled(true); + + expect(reportNetworkLog).toHaveBeenCalledWith({ + ...networkData, + requestBody: expect.stringContaining('omitted'), + }); + + expect(consoleWarn).toBeCalledTimes(1); + + consoleWarn.mockRestore(); + }); + + it('should omit response body if its content type is not allowed', () => { + const consoleWarn = jest.spyOn(console, 'warn').mockImplementation(); + jest.mocked(isContentTypeNotAllowed).mockReturnValueOnce(true); + + const networkData = { + ...network, + responseBody: 'some response body', + }; + + Interceptor.setOnDoneCallback = jest + .fn() + .mockImplementation((callback) => callback(networkData)); + + NetworkLogger.setEnabled(true); + + expect(reportNetworkLog).toHaveBeenCalledWith({ + ...networkData, + responseBody: expect.stringContaining('omitted'), + }); + + expect(consoleWarn).toBeCalledTimes(1); + + consoleWarn.mockRestore(); + }); + + it('should omit request body if its size exceeds the maximum allowed size', () => { + const consoleWarn = jest.spyOn(console, 'warn').mockImplementation(); + + const networkData = { + ...network, + requestBodySize: InstabugConstants.MAX_NETWORK_BODY_SIZE_IN_BYTES + 1, + }; + + Interceptor.setOnDoneCallback = jest + .fn() + .mockImplementation((callback) => callback(networkData)); + + NetworkLogger.setEnabled(true); + + expect(reportNetworkLog).toHaveBeenCalledWith({ + ...networkData, + requestBody: InstabugConstants.MAX_REQUEST_BODY_SIZE_EXCEEDED_MESSAGE, + }); + + expect(consoleWarn).toBeCalledTimes(1); + + consoleWarn.mockRestore(); + }); + + it('should not omit request body if its size does not exceed the maximum allowed size', () => { + const networkData = { + ...network, + requestBodySize: InstabugConstants.MAX_NETWORK_BODY_SIZE_IN_BYTES, + }; + + Interceptor.setOnDoneCallback = jest + .fn() + .mockImplementation((callback) => callback(networkData)); + + NetworkLogger.setEnabled(true); + + expect(reportNetworkLog).toHaveBeenCalledWith(networkData); + }); + + it('should omit response body if its size exceeds the maximum allowed size', () => { + const consoleWarn = jest.spyOn(console, 'warn').mockImplementation(); + + const networkData = { + ...network, + responseBodySize: InstabugConstants.MAX_NETWORK_BODY_SIZE_IN_BYTES + 1, + }; + + Interceptor.setOnDoneCallback = jest + .fn() + .mockImplementation((callback) => callback(networkData)); + + NetworkLogger.setEnabled(true); + + expect(reportNetworkLog).toHaveBeenCalledWith({ + ...networkData, + responseBody: InstabugConstants.MAX_RESPONSE_BODY_SIZE_EXCEEDED_MESSAGE, + }); + + expect(consoleWarn).toBeCalledTimes(1); + + consoleWarn.mockRestore(); + }); + + it('should not omit response body if its size does not exceed the maximum allowed size', () => { + const networkData = { + ...network, + responseBodySize: InstabugConstants.MAX_NETWORK_BODY_SIZE_IN_BYTES, + }; + + Interceptor.setOnDoneCallback = jest + .fn() + .mockImplementation((callback) => callback(networkData)); + + NetworkLogger.setEnabled(true); + + expect(reportNetworkLog).toHaveBeenCalledWith(networkData); + }); }); diff --git a/test/utils/InstabugUtils.spec.ts b/test/utils/InstabugUtils.spec.ts index 9ecb2332a2..becfccc0e9 100644 --- a/test/utils/InstabugUtils.spec.ts +++ b/test/utils/InstabugUtils.spec.ts @@ -5,8 +5,14 @@ import parseErrorStackLib from 'react-native/Libraries/Core/Devtools/parseErrorS import * as Instabug from '../../src/modules/Instabug'; import { NativeCrashReporting } from '../../src/native/NativeCrashReporting'; -import { InvocationEvent, NonFatalErrorLevel } from '../../src'; -import InstabugUtils, { getStackTrace, sendCrashReport } from '../../src/utils/InstabugUtils'; +import { InvocationEvent, NetworkData, NonFatalErrorLevel } from '../../src'; +import InstabugUtils, { + getStackTrace, + reportNetworkLog, + sendCrashReport, +} from '../../src/utils/InstabugUtils'; +import { NativeInstabug } from '../../src/native/NativeInstabug'; +import { NativeAPM } from '../../src/native/NativeAPM'; describe('Test global error handler', () => { beforeEach(() => { @@ -233,3 +239,91 @@ describe('Instabug Utils', () => { ); }); }); + +describe('reportNetworkLog', () => { + const network: NetworkData = { + url: 'https://api.instabug.com', + method: 'GET', + requestBody: 'requestBody', + requestHeaders: { request: 'header' }, + responseBody: 'responseBody', + responseCode: 200, + responseHeaders: { response: 'header' }, + contentType: 'application/json', + startTime: 1, + duration: 2, + requestBodySize: 3, + responseBodySize: 4, + errorCode: 5, + errorDomain: 'errorDomain', + serverErrorMessage: 'serverErrorMessage', + requestContentType: 'requestContentType', + }; + + it('reportNetworkLog should send network logs to native with the correct parameters on Android', () => { + Platform.OS = 'android'; + + const requestHeaders = JSON.stringify(network.requestHeaders); + const responseHeaders = JSON.stringify(network.responseHeaders); + + reportNetworkLog(network); + + expect(NativeInstabug.networkLogAndroid).toHaveBeenCalledTimes(1); + expect(NativeInstabug.networkLogAndroid).toHaveBeenCalledWith( + network.url, + network.requestBody, + network.responseBody, + network.method, + network.responseCode, + requestHeaders, + responseHeaders, + network.duration, + ); + + expect(NativeAPM.networkLogAndroid).toHaveBeenCalledTimes(1); + expect(NativeAPM.networkLogAndroid).toHaveBeenCalledWith( + network.startTime, + network.duration, + requestHeaders, + network.requestBody, + network.requestBodySize, + network.method, + network.url, + network.requestContentType, + responseHeaders, + network.responseBody, + network.responseBodySize, + network.responseCode, + network.contentType, + network.errorDomain, + network.gqlQueryName, + network.serverErrorMessage, + ); + }); + + it('reportNetworkLog should send network logs to native with the correct parameters on iOS', () => { + Platform.OS = 'ios'; + + reportNetworkLog(network); + + expect(NativeInstabug.networkLogIOS).toHaveBeenCalledTimes(1); + expect(NativeInstabug.networkLogIOS).toHaveBeenCalledWith( + network.url, + network.method, + network.requestBody, + network.requestBodySize, + network.responseBody, + network.responseBodySize, + network.responseCode, + network.requestHeaders, + network.responseHeaders, + network.contentType, + network.errorDomain, + network.errorCode, + network.startTime, + network.duration, + network.gqlQueryName, + network.serverErrorMessage, + ); + }); +});