diff --git a/CHANGELOG.md b/CHANGELOG.md index f53e242939..e1f28a933f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## [14.1.0](https://github.com/Instabug/Instabug-React-Native/compare/v14.0.0...v14.1.0) (January 2, 2025) + +### Added + +- Add support for tracing network requests from Instabug to services like Datadog and New Relic ([#1288](https://github.com/Instabug/Instabug-React-Native/pull/1288)) + +### Changed + +- Bump Instabug iOS SDK to v14.1.0 ([#1335](https://github.com/Instabug/Instabug-React-Native/pull/1335)). [See release notes](https://github.com/Instabug/Instabug-iOS/releases/tag/14.1.0). +- Bump Instabug Android SDK to v14.1.0 ([#1335](https://github.com/Instabug/Instabug-React-Native/pull/1335)). [See release notes](https://github.com/Instabug/Instabug-Android/releases/tag/v14.1.0). + ## [14.0.0](https://github.com/Instabug/Instabug-React-Native/compare/v13.4.0...14.0.0) (November 19, 2024) ### Added @@ -11,6 +22,10 @@ - Bump Instabug iOS SDK to v14.0.0 ([#1312](https://github.com/Instabug/Instabug-React-Native/pull/1312)). [See release notes](https://github.com/Instabug/Instabug-iOS/releases/tag/14.0.0). - Bump Instabug Android SDK to v14.0.0 ([#1312](https://github.com/Instabug/Instabug-React-Native/pull/1312)). [See release notes](https://github.com/Instabug/Instabug-Android/releases/tag/v14.0.0). +### Added + +- Exclude DEV server from network logs ([#1307](https://github.com/Instabug/Instabug-React-Native/pull/1307)). + ### Fixed - Replace thrown errors with logs ([#1220](https://github.com/Instabug/Instabug-React-Native/pull/1220)) diff --git a/android/build.gradle b/android/build.gradle index 485acf84aa..8a312ccb37 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -57,7 +57,7 @@ android { minSdkVersion getExtOrDefault('minSdkVersion').toInteger() targetSdkVersion getExtOrDefault('targetSdkVersion').toInteger() versionCode 1 - versionName "13.4.0" + versionName "14.1.0" multiDexEnabled true ndk { abiFilters "armeabi-v7a", "x86" diff --git a/android/native.gradle b/android/native.gradle index ca10ca83b8..652733c4f8 100644 --- a/android/native.gradle +++ b/android/native.gradle @@ -1,5 +1,5 @@ project.ext.instabug = [ - version: '14.0.0' + version: '14.1.0' ] dependencies { diff --git a/android/src/main/java/com/instabug/reactlibrary/Constants.java b/android/src/main/java/com/instabug/reactlibrary/Constants.java index fcab683326..f6986200d3 100644 --- a/android/src/main/java/com/instabug/reactlibrary/Constants.java +++ b/android/src/main/java/com/instabug/reactlibrary/Constants.java @@ -9,6 +9,9 @@ final class Constants { final static String IBG_ON_NEW_MESSAGE_HANDLER = "IBGonNewMessageHandler"; final static String IBG_ON_NEW_REPLY_RECEIVED_CALLBACK = "IBGOnNewReplyReceivedCallback"; + + final static String IBG_ON_NEW_W3C_FLAGS_UPDATE_RECEIVED_CALLBACK = "IBGOnNewW3CFlagsUpdateReceivedCallback"; + final static String IBG_SESSION_REPLAY_ON_SYNC_CALLBACK_INVOCATION = "IBGSessionReplayOnSyncCallback"; } diff --git a/android/src/main/java/com/instabug/reactlibrary/RNInstabugAPMModule.java b/android/src/main/java/com/instabug/reactlibrary/RNInstabugAPMModule.java index e2ff10b991..8b3c3206eb 100644 --- a/android/src/main/java/com/instabug/reactlibrary/RNInstabugAPMModule.java +++ b/android/src/main/java/com/instabug/reactlibrary/RNInstabugAPMModule.java @@ -9,17 +9,16 @@ import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReactApplicationContext; -import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.ReadableMap; import com.instabug.apm.APM; import com.instabug.apm.model.ExecutionTrace; import com.instabug.apm.networking.APMNetworkLogger; import com.instabug.apm.networkinterception.cp.APMCPNetworkLog; +import com.instabug.reactlibrary.utils.EventEmitterModule; +import com.instabug.apm.networkinterception.cp.APMCPNetworkLog; import com.instabug.reactlibrary.utils.MainThreadHandler; -import org.json.JSONException; -import org.json.JSONObject; -import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.HashMap; @@ -28,7 +27,7 @@ import static com.instabug.reactlibrary.utils.InstabugUtil.getMethod; -public class RNInstabugAPMModule extends ReactContextBaseJavaModule { +public class RNInstabugAPMModule extends EventEmitterModule { public RNInstabugAPMModule(ReactApplicationContext reactApplicationContext) { super(reactApplicationContext); @@ -330,14 +329,41 @@ private void networkLogAndroid(final double requestStartTime, final double statusCode, final String responseContentType, @Nullable final String errorDomain, + @Nullable final ReadableMap w3cAttributes, @Nullable final String gqlQueryName, - @Nullable final String serverErrorMessage) { + @Nullable final String serverErrorMessage + ) { try { APMNetworkLogger networkLogger = new APMNetworkLogger(); final boolean hasError = errorDomain != null && !errorDomain.isEmpty(); final String errorMessage = hasError ? errorDomain : null; + Boolean isW3cHeaderFound=false; + Long partialId=null; + Long networkStartTimeInSeconds=null; + + + try { + if (!w3cAttributes.isNull("isW3cHeaderFound")) { + isW3cHeaderFound = w3cAttributes.getBoolean("isW3cHeaderFound"); + } + if (!w3cAttributes.isNull("partialId")) { + partialId =(long) w3cAttributes.getDouble("partialId"); + networkStartTimeInSeconds = (long) w3cAttributes.getDouble("networkStartTimeInSeconds"); + } + + } catch (Exception e) { + e.printStackTrace(); + } + APMCPNetworkLog.W3CExternalTraceAttributes w3cExternalTraceAttributes = + new APMCPNetworkLog.W3CExternalTraceAttributes( + isW3cHeaderFound, + partialId, + networkStartTimeInSeconds, + w3cAttributes.getString("w3cGeneratedHeader"), + w3cAttributes.getString("w3cCaughtHeader") + ); 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, APMCPNetworkLog.W3CExternalTraceAttributes.class); if (method != null) { @@ -359,7 +385,7 @@ private void networkLogAndroid(final double requestStartTime, errorMessage, gqlQueryName, serverErrorMessage, - null + w3cExternalTraceAttributes ); } else { Log.e("IB-CP-Bridge", "APMNetworkLogger.log was not found by reflection"); diff --git a/android/src/main/java/com/instabug/reactlibrary/RNInstabugReactnativeModule.java b/android/src/main/java/com/instabug/reactlibrary/RNInstabugReactnativeModule.java index f936eaa12e..9c901cb7a5 100644 --- a/android/src/main/java/com/instabug/reactlibrary/RNInstabugReactnativeModule.java +++ b/android/src/main/java/com/instabug/reactlibrary/RNInstabugReactnativeModule.java @@ -8,6 +8,7 @@ import android.util.Log; import android.view.View; +import androidx.annotation.NonNull; import androidx.annotation.UiThread; import com.facebook.react.bridge.Arguments; @@ -22,6 +23,8 @@ import com.facebook.react.bridge.WritableNativeArray; import com.facebook.react.bridge.WritableNativeMap; import com.facebook.react.uimanager.UIManagerModule; +import com.instabug.apm.InternalAPM; +import com.instabug.apm.configuration.cp.APMFeature; import com.instabug.library.Feature; import com.instabug.library.Instabug; import com.instabug.library.InstabugColorTheme; @@ -30,6 +33,11 @@ import com.instabug.library.LogLevel; import com.instabug.library.ReproConfigurations; import com.instabug.library.core.InstabugCore; +import com.instabug.library.internal.crossplatform.CoreFeature; +import com.instabug.library.internal.crossplatform.CoreFeaturesState; +import com.instabug.library.internal.crossplatform.FeaturesStateListener; +import com.instabug.library.internal.crossplatform.InternalCore; +import com.instabug.library.featuresflags.model.IBGFeatureFlag; import com.instabug.library.featuresflags.model.IBGFeatureFlag; import com.instabug.library.internal.module.InstabugLocale; import com.instabug.library.invocation.InstabugInvocationEvent; @@ -1148,6 +1156,105 @@ public void run() { } }); } + /** + * Register a listener for W3C flags value change + */ + @ReactMethod + public void registerW3CFlagsChangeListener(){ + + MainThreadHandler.runOnMainThread(new Runnable() { + @Override + public void run() { + try { + InternalCore.INSTANCE._setFeaturesStateListener(new FeaturesStateListener() { + @Override + public void invoke(@NonNull CoreFeaturesState featuresState) { + WritableMap params = Arguments.createMap(); + params.putBoolean("isW3ExternalTraceIDEnabled", featuresState.isW3CExternalTraceIdEnabled()); + params.putBoolean("isW3ExternalGeneratedHeaderEnabled", featuresState.isAttachingGeneratedHeaderEnabled()); + params.putBoolean("isW3CaughtHeaderEnabled", featuresState.isAttachingCapturedHeaderEnabled()); + + sendEvent(Constants.IBG_ON_NEW_W3C_FLAGS_UPDATE_RECEIVED_CALLBACK, params); + } + }); + } + catch (Exception e) { + e.printStackTrace(); + } + + } + + }); + } + + + /** + * Get first time Value of W3ExternalTraceID flag + */ + @ReactMethod + public void isW3ExternalTraceIDEnabled(Promise promise){ + + MainThreadHandler.runOnMainThread(new Runnable() { + @Override + public void run() { + try { + promise.resolve(InternalCore.INSTANCE._isFeatureEnabled(CoreFeature.W3C_EXTERNAL_TRACE_ID)); + } + catch (Exception e) { + e.printStackTrace(); + promise.resolve(false); + } + + } + + }); + } + + + /** + * Get first time Value of W3ExternalGeneratedHeader flag + */ + @ReactMethod + public void isW3ExternalGeneratedHeaderEnabled(Promise promise){ + + MainThreadHandler.runOnMainThread(new Runnable() { + @Override + public void run() { + try { + promise.resolve(InternalCore.INSTANCE._isFeatureEnabled(CoreFeature.W3C_ATTACHING_GENERATED_HEADER)); + } + catch (Exception e) { + e.printStackTrace(); + promise.resolve(false); + } + + } + + }); + } + + /** + * Get first time Value of W3CaughtHeader flag + */ + @ReactMethod + public void isW3CaughtHeaderEnabled(Promise promise){ + + MainThreadHandler.runOnMainThread(new Runnable() { + @Override + public void run() { + try { + promise.resolve(InternalCore.INSTANCE._isFeatureEnabled(CoreFeature.W3C_ATTACHING_CAPTURED_HEADER)); + } + catch (Exception e) { + e.printStackTrace(); + promise.resolve(false); + } + + } + + }); + } + /** * Map between the exported JS constant and the arg key in {@link ArgsRegistry}. diff --git a/android/src/test/java/com/instabug/reactlibrary/RNInstabugAPMModuleTest.java b/android/src/test/java/com/instabug/reactlibrary/RNInstabugAPMModuleTest.java index 5045f929e6..85ca1384d1 100644 --- a/android/src/test/java/com/instabug/reactlibrary/RNInstabugAPMModuleTest.java +++ b/android/src/test/java/com/instabug/reactlibrary/RNInstabugAPMModuleTest.java @@ -204,4 +204,6 @@ public void testSetFlowAttribute() { verify(APM.class, times(1)); APM.endUITrace(); } + + } diff --git a/android/src/test/java/com/instabug/reactlibrary/RNInstabugReactnativeModuleTest.java b/android/src/test/java/com/instabug/reactlibrary/RNInstabugReactnativeModuleTest.java index b9bf2308cc..e8aad5b0c5 100644 --- a/android/src/test/java/com/instabug/reactlibrary/RNInstabugReactnativeModuleTest.java +++ b/android/src/test/java/com/instabug/reactlibrary/RNInstabugReactnativeModuleTest.java @@ -18,6 +18,9 @@ import com.instabug.library.IssueType; import com.instabug.library.ReproConfigurations; import com.instabug.library.ReproMode; +import com.instabug.library.internal.crossplatform.CoreFeature; +import com.instabug.library.internal.crossplatform.InternalCore; +import com.instabug.library.featuresflags.model.IBGFeatureFlag; import com.instabug.library.featuresflags.model.IBGFeatureFlag; import com.instabug.library.internal.module.InstabugLocale; import com.instabug.library.ui.onboarding.WelcomeMessage; @@ -635,4 +638,28 @@ public void testWillRedirectToStore() { // then mockInstabug.verify(() -> Instabug.willRedirectToStore()); } + @Test + public void testW3CExternalTraceIDFlag(){ + Promise promise = mock(Promise.class); + InternalCore internalAPM = mock(InternalCore.class); + rnModule.isW3ExternalTraceIDEnabled(promise); + boolean expected=internalAPM._isFeatureEnabled(CoreFeature.W3C_EXTERNAL_TRACE_ID); + verify(promise).resolve(expected); + } + @Test + public void testW3CExternalGeneratedHeaderFlag(){ + Promise promise = mock(Promise.class); + InternalCore internalAPM = mock(InternalCore.class); + rnModule.isW3ExternalGeneratedHeaderEnabled(promise); + boolean expected=internalAPM._isFeatureEnabled(CoreFeature.W3C_ATTACHING_GENERATED_HEADER); + verify(promise).resolve(expected); + } + @Test + public void testW3CCaughtHeaderFlag(){ + Promise promise = mock(Promise.class); + InternalCore internalAPM = mock(InternalCore.class); + rnModule.isW3CaughtHeaderEnabled(promise); + boolean expected=internalAPM._isFeatureEnabled(CoreFeature.W3C_ATTACHING_CAPTURED_HEADER); + verify(promise).resolve(expected); + } } diff --git a/examples/default/ios/InstabugTests/InstabugAPMTests.m b/examples/default/ios/InstabugTests/InstabugAPMTests.m index dd96841dea..5945b2b791 100644 --- a/examples/default/ios/InstabugTests/InstabugAPMTests.m +++ b/examples/default/ios/InstabugTests/InstabugAPMTests.m @@ -13,6 +13,7 @@ #import #import "Instabug/Instabug.h" #import "IBGConstants.h" +#import "RNInstabug/IBGAPM+PrivateAPIs.h" @interface InstabugAPMTests : XCTestCase @property (nonatomic, retain) InstabugAPMBridge *instabugBridge; @@ -176,4 +177,6 @@ - (void) testEndUITrace { OCMVerify([mock endUITrace]); } + + @end diff --git a/examples/default/ios/InstabugTests/InstabugCrashReportingTests.m b/examples/default/ios/InstabugTests/InstabugCrashReportingTests.m index d42e47022a..d8ae7a0e54 100644 --- a/examples/default/ios/InstabugTests/InstabugCrashReportingTests.m +++ b/examples/default/ios/InstabugTests/InstabugCrashReportingTests.m @@ -19,11 +19,13 @@ - (void)setUp { } - (void)testSetEnabled { + + [self.bridge setEnabled:NO]; + XCTAssertFalse(IBGCrashReporting.enabled); + [self.bridge setEnabled:YES]; XCTAssertTrue(IBGCrashReporting.enabled); - [self.bridge setEnabled:NO]; - XCTAssertFalse(IBGCrashReporting.enabled); } - (void)testSendJSCrash { diff --git a/examples/default/ios/InstabugTests/InstabugSampleTests.m b/examples/default/ios/InstabugTests/InstabugSampleTests.m index 51bbe182c2..8744ce4eb8 100644 --- a/examples/default/ios/InstabugTests/InstabugSampleTests.m +++ b/examples/default/ios/InstabugTests/InstabugSampleTests.m @@ -315,7 +315,7 @@ - (void)testSetWelcomeMessageMode { - (void)testNetworkLogIOS { id mIBGNetworkLogger = OCMClassMock([IBGNetworkLogger class]); - + NSString *url = @"https://api.instabug.com"; NSString *method = @"GET"; NSString *requestBody = @"requestBody"; @@ -332,7 +332,12 @@ - (void)testNetworkLogIOS { double duration = 150; NSString *gqlQueryName = nil; NSString *serverErrorMessage = nil; - + NSDictionary* w3cExternalTraceAttributes = nil; + NSNumber *isW3cCaughted = nil; + NSNumber *partialID = nil; + NSNumber *timestamp= nil; + NSString *generatedW3CTraceparent= nil; + NSString *caughtedW3CTraceparent= nil; [self.instabugBridge networkLogIOS:url method:method requestBody:requestBody @@ -348,8 +353,11 @@ - (void)testNetworkLogIOS { startTime:startTime duration:duration gqlQueryName:gqlQueryName - serverErrorMessage:serverErrorMessage]; - + serverErrorMessage:serverErrorMessage + w3cExternalTraceAttributes:w3cExternalTraceAttributes + + ]; + OCMVerify([mIBGNetworkLogger addNetworkLogWithUrl:url method:method requestBody:requestBody @@ -366,11 +374,12 @@ - (void)testNetworkLogIOS { duration:duration * 1000 gqlQueryName:gqlQueryName serverErrorMessage:serverErrorMessage - isW3cCaughted:nil - partialID:nil - timestamp:nil - generatedW3CTraceparent:nil - caughtedW3CTraceparent:nil]); + isW3cCaughted:isW3cCaughted + partialID:partialID + timestamp:timestamp + generatedW3CTraceparent:generatedW3CTraceparent + caughtedW3CTraceparent:caughtedW3CTraceparent + ]); } - (void)testSetFileAttachment { @@ -541,4 +550,63 @@ - (void)testRemoveAllFeatureFlags { OCMVerify([mock removeAllFeatureFlags]); } + +- (void) testIsW3ExternalTraceIDEnabled { + id mock = OCMClassMock([IBGNetworkLogger class]); + NSNumber *expectedValue = @(YES); + + OCMStub([mock w3ExternalTraceIDEnabled]).andReturn([expectedValue boolValue]); + + XCTestExpectation *expectation = [self expectationWithDescription:@"Call completion handler"]; + RCTPromiseResolveBlock resolve = ^(NSNumber *result) { + XCTAssertEqualObjects(result, expectedValue); + [expectation fulfill]; + }; + + [self.instabugBridge isW3ExternalTraceIDEnabled:resolve :nil]; + + [self waitForExpectationsWithTimeout:1.0 handler:nil]; + + OCMVerify([mock w3ExternalTraceIDEnabled]); +} + +- (void) testIsW3ExternalGeneratedHeaderEnabled { + id mock = OCMClassMock([IBGNetworkLogger class]); + NSNumber *expectedValue = @(YES); + + OCMStub([mock w3ExternalGeneratedHeaderEnabled]).andReturn([expectedValue boolValue]); + + XCTestExpectation *expectation = [self expectationWithDescription:@"Call completion handler"]; + RCTPromiseResolveBlock resolve = ^(NSNumber *result) { + XCTAssertEqualObjects(result, expectedValue); + [expectation fulfill]; + }; + + [self.instabugBridge isW3ExternalGeneratedHeaderEnabled:resolve :nil]; + + [self waitForExpectationsWithTimeout:1.0 handler:nil]; + + OCMVerify([mock w3ExternalGeneratedHeaderEnabled]); +} + +- (void) testIsW3CaughtHeaderEnabled { + id mock = OCMClassMock([IBGNetworkLogger class]); + NSNumber *expectedValue = @(YES); + + OCMStub([mock w3CaughtHeaderEnabled]).andReturn([expectedValue boolValue]); + + XCTestExpectation *expectation = [self expectationWithDescription:@"Call completion handler"]; + RCTPromiseResolveBlock resolve = ^(NSNumber *result) { + XCTAssertEqualObjects(result, expectedValue); + [expectation fulfill]; + }; + + [self.instabugBridge isW3CaughtHeaderEnabled:resolve :nil]; + + [self waitForExpectationsWithTimeout:1.0 handler:nil]; + + OCMVerify([mock w3CaughtHeaderEnabled]); +} + + @end diff --git a/examples/default/ios/InstabugTests/RNInstabugTests.m b/examples/default/ios/InstabugTests/RNInstabugTests.m index cde248ad30..930da52ca5 100644 --- a/examples/default/ios/InstabugTests/RNInstabugTests.m +++ b/examples/default/ios/InstabugTests/RNInstabugTests.m @@ -69,7 +69,7 @@ - (void)testInitWithLogsLevel { - (void) testSetCodePushVersion { NSString *codePushVersion = @"1.0.0(1)"; [RNInstabug setCodePushVersion:codePushVersion]; - + OCMVerify([self.mInstabug setCodePushVersion:codePushVersion]); } diff --git a/examples/default/ios/Podfile.lock b/examples/default/ios/Podfile.lock index 81ba100a5a..6572db6072 100644 --- a/examples/default/ios/Podfile.lock +++ b/examples/default/ios/Podfile.lock @@ -31,7 +31,7 @@ PODS: - hermes-engine (0.75.4): - hermes-engine/Pre-built (= 0.75.4) - hermes-engine/Pre-built (0.75.4) - - Instabug (14.0.0) + - Instabug (14.1.0) - instabug-reactnative-ndk (0.1.0): - DoubleConversion - glog @@ -1319,6 +1319,27 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga + - react-native-webview (13.12.3): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga - React-nativeconfig (0.75.4) - React-NativeModulesApple (0.75.4): - glog @@ -1602,8 +1623,8 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - RNInstabug (14.0.0): - - Instabug (= 14.0.0) + - RNInstabug (14.1.0): + - Instabug (= 14.1.0) - React-Core - RNReanimated (3.16.1): - DoubleConversion @@ -1786,6 +1807,7 @@ DEPENDENCIES: - react-native-maps (from `../node_modules/react-native-maps`) - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`) - "react-native-slider (from `../node_modules/@react-native-community/slider`)" + - react-native-webview (from `../node_modules/react-native-webview`) - React-nativeconfig (from `../node_modules/react-native/ReactCommon`) - React-NativeModulesApple (from `../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`) - React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`) @@ -1915,6 +1937,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-safe-area-context" react-native-slider: :path: "../node_modules/@react-native-community/slider" + react-native-webview: + :path: "../node_modules/react-native-webview" React-nativeconfig: :path: "../node_modules/react-native/ReactCommon" React-NativeModulesApple: @@ -1993,7 +2017,7 @@ SPEC CHECKSUMS: Google-Maps-iOS-Utils: f77eab4c4326d7e6a277f8e23a0232402731913a GoogleMaps: 032f676450ba0779bd8ce16840690915f84e57ac hermes-engine: ea92f60f37dba025e293cbe4b4a548fd26b610a0 - Instabug: a0beffc01658773e2fac549845782f8937707dc4 + Instabug: 8cbca8974168c815658133e2813f5ac3a36f8e20 instabug-reactnative-ndk: d765ac289d56e8896398d02760d9abf2562fc641 OCMock: 589f2c84dacb1f5aaf6e4cec1f292551fe748e74 RCT-Folly: 4464f4d875961fce86008d45f4ecf6cef6de0740 @@ -2031,6 +2055,7 @@ SPEC CHECKSUMS: react-native-maps: 72a8a903f8a1b53e2c777ba79102078ab502e0bf react-native-safe-area-context: 142fade490cbebbe428640b8cbdb09daf17e8191 react-native-slider: 4a0f3386a38fc3d2d955efc515aef7096f7d1ee4 + react-native-webview: 926d2665cf3196e39c4449a72d136d0a53b9df8a React-nativeconfig: 8c83d992b9cc7d75b5abe262069eaeea4349f794 React-NativeModulesApple: 9f7920224a3b0c7d04d77990067ded14cee3c614 React-perflogger: 59e1a3182dca2cee7b9f1f7aab204018d46d1914 @@ -2059,7 +2084,7 @@ SPEC CHECKSUMS: ReactCommon: 6a952e50c2a4b694731d7682aaa6c79bc156e4ad RNCClipboard: 2821ac938ef46f736a8de0c8814845dde2dcbdfb RNGestureHandler: 511250b190a284388f9dd0d2e56c1df76f14cfb8 - RNInstabug: eaa8cde2bcd3c8e757c6dd5c0d33a20814f9658a + RNInstabug: 96e629f47c0af2e9455fbcf800d12049f980d873 RNReanimated: f42a5044d121d68e91680caacb0293f4274228eb RNScreens: c7ceced6a8384cb9be5e7a5e88e9e714401fd958 RNSVG: 8b1a777d54096b8c2a0fd38fc9d5a454332bbb4d diff --git a/examples/default/package.json b/examples/default/package.json index 617cdc17b8..26959bb0b9 100644 --- a/examples/default/package.json +++ b/examples/default/package.json @@ -32,6 +32,7 @@ "react-native-screens": "^3.35.0", "react-native-svg": "^15.8.0", "react-native-vector-icons": "^10.2.0", + "react-native-webview": "^13.12.3", "react-query": "^3.39.3" }, "devDependencies": { diff --git a/examples/default/src/navigation/HomeStack.tsx b/examples/default/src/navigation/HomeStack.tsx index 716ea05d9f..090aa65873 100644 --- a/examples/default/src/navigation/HomeStack.tsx +++ b/examples/default/src/navigation/HomeStack.tsx @@ -28,6 +28,9 @@ import { FlowsScreen } from '../screens/apm/FlowsScreen'; import { SessionReplayScreen } from '../screens/SessionReplayScreen'; import { LegacyModeScreen } from '../screens/LegacyModeScreen'; import { HttpScreen } from '../screens/apm/HttpScreen'; +import { WebViewsScreen } from '../screens/apm/webViews/WebViewsScreen'; +import { FullWebViewsScreen } from '../screens/apm/webViews/FullWebViewsScreen'; +import { PartialWebViewsScreen } from '../screens/apm/webViews/PartialWebViewsScreen'; export type HomeStackParamList = { Home: undefined; @@ -55,6 +58,9 @@ export type HomeStackParamList = { NetworkTraces: undefined; ExecutionTraces: undefined; AppFlows: undefined; + WebViews: undefined; + FullWebViews: undefined; + PartialWebViews: undefined; }; const HomeStack = createNativeStackNavigator(); @@ -142,6 +148,21 @@ export const HomeStackNavigator: React.FC = () => { options={{ title: 'LegacyMode' }} /> + + + ); }; diff --git a/examples/default/src/screens/BugReportingScreen.tsx b/examples/default/src/screens/BugReportingScreen.tsx index 95a8505699..a8afc14a8d 100644 --- a/examples/default/src/screens/BugReportingScreen.tsx +++ b/examples/default/src/screens/BugReportingScreen.tsx @@ -1,11 +1,20 @@ import React from 'react'; -import Instabug, { BugReporting, InvocationOption, ReportType } from 'instabug-reactnative'; +import Instabug, { + BugReporting, + InvocationOption, + ReportType, + ExtendedBugReportMode, + WelcomeMessageMode, +} from 'instabug-reactnative'; import { ListTile } from '../components/ListTile'; import { Screen } from '../components/Screen'; +import { useToast } from 'native-base'; +import { Section } from '../components/Section'; export const BugReportingScreen: React.FC = () => { + const toast = useToast(); return ( Instabug.show()} /> @@ -15,6 +24,59 @@ export const BugReportingScreen: React.FC = () => { onPress={() => BugReporting.show(ReportType.feedback, [InvocationOption.emailFieldHidden])} /> BugReporting.show(ReportType.question, [])} /> + + BugReporting.setExtendedBugReportMode(ExtendedBugReportMode.enabledWithRequiredFields) + } + /> + + BugReporting.setExtendedBugReportMode(ExtendedBugReportMode.enabledWithOptionalFields) + } + /> + Instabug.setSessionProfilerEnabled(true)} + /> + Instabug.showWelcomeMessage(WelcomeMessageMode.beta)} + /> + Instabug.showWelcomeMessage(WelcomeMessageMode.live)} + /> + +
+ + BugReporting.onInvokeHandler(function () { + Instabug.appendTags(['Invocation Handler tag1']); + }) + } + /> + + Instabug.onReportSubmitHandler(() => { + toast.show({ + description: 'Submission succeeded', + }); + }) + } + /> + + BugReporting.onSDKDismissedHandler(function () { + Instabug.setPrimaryColor('#FF0000'); + }) + } + /> +
); }; diff --git a/examples/default/src/screens/apm/APMScreen.tsx b/examples/default/src/screens/apm/APMScreen.tsx index 0b04e61919..d63ee65f4f 100644 --- a/examples/default/src/screens/apm/APMScreen.tsx +++ b/examples/default/src/screens/apm/APMScreen.tsx @@ -26,6 +26,7 @@ export const APMScreen: React.FC navigation.navigate('NetworkTraces')} /> navigation.navigate('ExecutionTraces')} /> navigation.navigate('AppFlows')} /> + navigation.navigate('WebViews')} /> ); }; diff --git a/examples/default/src/screens/apm/NetworkScreen.tsx b/examples/default/src/screens/apm/NetworkScreen.tsx index 8aa20f49f0..f9f057f612 100644 --- a/examples/default/src/screens/apm/NetworkScreen.tsx +++ b/examples/default/src/screens/apm/NetworkScreen.tsx @@ -94,6 +94,14 @@ export const NetworkScreen: React.FC< }; const { data, isError, isSuccess, isLoading, refetch } = useQuery('helloQuery', fetchGraphQlData); + const simulateNetworkRequest = () => { + axios.get('https://httpbin.org/anything', { + headers: { traceparent: 'Caught Header Example' }, + }); + }; + const simulateNetworkRequestWithoutHeader = () => { + axios.get('https://httpbin.org/anything'); + }; return ( @@ -111,7 +119,14 @@ export const NetworkScreen: React.FC< onPress={sendRequestToUrlUsingAxios} title="Send Request To Url Using Axios" /> - + simulateNetworkRequest()} + /> + simulateNetworkRequestWithoutHeader()} + /> refetch} title="Reload GraphQL" /> {isLoading && Loading...} diff --git a/examples/default/src/screens/apm/webViews/FullWebViewsScreen.tsx b/examples/default/src/screens/apm/webViews/FullWebViewsScreen.tsx new file mode 100644 index 0000000000..fbee5c028f --- /dev/null +++ b/examples/default/src/screens/apm/webViews/FullWebViewsScreen.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { Screen } from '../../../components/Screen'; +import { WebView } from 'react-native-webview'; + +export const FullWebViewsScreen: React.FC = () => { + return ( + + + + ); +}; diff --git a/examples/default/src/screens/apm/webViews/PartialWebViewsScreen.tsx b/examples/default/src/screens/apm/webViews/PartialWebViewsScreen.tsx new file mode 100644 index 0000000000..6cb4a62639 --- /dev/null +++ b/examples/default/src/screens/apm/webViews/PartialWebViewsScreen.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { Screen } from '../../../components/Screen'; +import { WebView } from 'react-native-webview'; +import { StyleSheet } from 'react-native'; + +export const PartialWebViewsScreen: React.FC = () => { + return ( + + + + + ); +}; +const styles = StyleSheet.create({ + webView: { + marginBottom: 20, + }, +}); diff --git a/examples/default/src/screens/apm/webViews/WebViewsScreen.tsx b/examples/default/src/screens/apm/webViews/WebViewsScreen.tsx new file mode 100644 index 0000000000..0c3309dfee --- /dev/null +++ b/examples/default/src/screens/apm/webViews/WebViewsScreen.tsx @@ -0,0 +1,16 @@ +import type { NativeStackScreenProps } from '@react-navigation/native-stack'; +import type { HomeStackParamList } from '../../../navigation/HomeStack'; +import React from 'react'; +import { Screen } from '../../../components/Screen'; +import { ListTile } from '../../../components/ListTile'; + +export const WebViewsScreen: React.FC> = ({ + navigation, +}) => { + return ( + + navigation.navigate('FullWebViews')} /> + navigation.navigate('PartialWebViews')} /> + + ); +}; diff --git a/examples/default/yarn.lock b/examples/default/yarn.lock index 348eda2fa4..3e1f047c3f 100644 --- a/examples/default/yarn.lock +++ b/examples/default/yarn.lock @@ -4427,7 +4427,7 @@ intl-messageformat@^10.1.0: "@formatjs/icu-messageformat-parser" "2.9.1" tslib "2" -invariant@^2.2.4: +invariant@2.2.4, invariant@^2.2.4: version "2.2.4" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== @@ -6329,6 +6329,14 @@ react-native-vector-icons@^10.2.0: prop-types "^15.7.2" yargs "^16.1.1" +react-native-webview@^13.12.3: + version "13.12.3" + resolved "https://registry.yarnpkg.com/react-native-webview/-/react-native-webview-13.12.3.tgz#3aa9d2fc982ba2681e56d3e96e22b63a0d929270" + integrity sha512-Y1I5YyDYyE7NC96RHLhd2nxh7ymLYOYLTefgx5ixxw2OToQK0ow3OJ+o77QcI1Tuevj5PCxwqC/14ceS/7yPJQ== + dependencies: + escape-string-regexp "^4.0.0" + invariant "2.2.4" + react-native@0.75.4: version "0.75.4" resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.75.4.tgz#40fc337b9c005521b5b7e039481bc4d444b009a9" diff --git a/ios/RNInstabug/InstabugAPMBridge.m b/ios/RNInstabug/InstabugAPMBridge.m index 0324be4a8b..daea8b4c1a 100644 --- a/ios/RNInstabug/InstabugAPMBridge.m +++ b/ios/RNInstabug/InstabugAPMBridge.m @@ -8,6 +8,7 @@ #import #import #import +#import "Util/IBGAPM+PrivateAPIs.h" @implementation InstabugAPMBridge @@ -110,6 +111,9 @@ - (id) init } + + + @synthesize description; @synthesize hash; diff --git a/ios/RNInstabug/InstabugReactBridge.h b/ios/RNInstabug/InstabugReactBridge.h index a3cfc21c13..bca04ddfd0 100644 --- a/ios/RNInstabug/InstabugReactBridge.h +++ b/ios/RNInstabug/InstabugReactBridge.h @@ -105,7 +105,9 @@ */ - (void)setNetworkLoggingEnabled:(BOOL)isEnabled; - +- (void)isW3ExternalTraceIDEnabled:(RCTPromiseResolveBlock)resolve :(RCTPromiseRejectBlock)reject; +- (void)isW3ExternalGeneratedHeaderEnabled:(RCTPromiseResolveBlock)resolve :(RCTPromiseRejectBlock)reject; +- (void)isW3CaughtHeaderEnabled:(RCTPromiseResolveBlock)resolve :(RCTPromiseRejectBlock)reject; - (void)networkLogIOS:(NSString * _Nonnull)url method:(NSString * _Nonnull)method requestBody:(NSString * _Nonnull)requestBody @@ -121,7 +123,8 @@ startTime:(double)startTime duration:(double)duration gqlQueryName:(NSString * _Nullable)gqlQueryName - serverErrorMessage:(NSString * _Nullable)serverErrorMessage; + serverErrorMessage:(NSString * _Nullable)serverErrorMessage +w3cExternalTraceAttributes:(NSDictionary * _Nullable)w3cExternalTraceAttributes; /* +------------------------------------------------------------------------+ diff --git a/ios/RNInstabug/InstabugReactBridge.m b/ios/RNInstabug/InstabugReactBridge.m index 534b849081..e7ca15600e 100644 --- a/ios/RNInstabug/InstabugReactBridge.m +++ b/ios/RNInstabug/InstabugReactBridge.m @@ -298,7 +298,14 @@ - (dispatch_queue_t)methodQueue { startTime:(double)startTime duration:(double)duration gqlQueryName:(NSString * _Nullable)gqlQueryName - serverErrorMessage:(NSString * _Nullable)serverErrorMessage) { + serverErrorMessage:(NSString * _Nullable)serverErrorMessage + w3cExternalTraceAttributes:(NSDictionary * _Nullable)w3cExternalTraceAttributes){ + NSNumber *isW3cCaught = (w3cExternalTraceAttributes[@"isW3cHeaderFound"] != [NSNull null]) ? w3cExternalTraceAttributes[@"isW3cHeaderFound"] : nil; + NSNumber * partialID = (w3cExternalTraceAttributes[@"partialId"] != [NSNull null]) ? w3cExternalTraceAttributes[@"partialId"] : nil; + NSNumber * timestamp = (w3cExternalTraceAttributes[@"networkStartTimeInSeconds"] != [NSNull null]) ? w3cExternalTraceAttributes[@"networkStartTimeInSeconds"] : nil; + NSString * generatedW3CTraceparent = (w3cExternalTraceAttributes[@"w3cGeneratedHeader"] != [NSNull null]) ? w3cExternalTraceAttributes[@"w3cGeneratedHeader"] : nil; + NSString * caughtW3CTraceparent = (w3cExternalTraceAttributes[@"w3cCaughtHeader"] != [NSNull null]) ? w3cExternalTraceAttributes[@"w3cCaughtHeader"] : nil; + [IBGNetworkLogger addNetworkLogWithUrl:url method:method requestBody:requestBody @@ -315,11 +322,12 @@ - (dispatch_queue_t)methodQueue { duration:duration * 1000 gqlQueryName:gqlQueryName serverErrorMessage:serverErrorMessage - isW3cCaughted:nil - partialID:nil - timestamp:nil - generatedW3CTraceparent:nil - caughtedW3CTraceparent:nil]; + isW3cCaughted:isW3cCaught + partialID:partialID + timestamp:timestamp + generatedW3CTraceparent:generatedW3CTraceparent + caughtedW3CTraceparent:caughtW3CTraceparent + ]; } RCT_EXPORT_METHOD(addPrivateView: (nonnull NSNumber *)reactTag) { @@ -369,7 +377,7 @@ - (dispatch_queue_t)methodQueue { [featureFlags addObject:[[IBGFeatureFlag alloc] initWithName:key variant:variant]]; } } - + [Instabug addFeatureFlags:featureFlags]; } @@ -378,7 +386,7 @@ - (dispatch_queue_t)methodQueue { for(id item in featureFlags){ [features addObject:[[IBGFeatureFlag alloc] initWithName:item]]; } - + @try { [Instabug removeFeatureFlags:features]; } @@ -395,6 +403,17 @@ - (dispatch_queue_t)methodQueue { [Instabug willRedirectToAppStore]; } +RCT_EXPORT_METHOD(isW3ExternalTraceIDEnabled:(RCTPromiseResolveBlock)resolve :(RCTPromiseRejectBlock)reject) { + resolve(@(IBGNetworkLogger.w3ExternalTraceIDEnabled)); +} +RCT_EXPORT_METHOD(isW3ExternalGeneratedHeaderEnabled:(RCTPromiseResolveBlock)resolve :(RCTPromiseRejectBlock)reject) { + resolve(@(IBGNetworkLogger.w3ExternalGeneratedHeaderEnabled)); +} +RCT_EXPORT_METHOD(isW3CaughtHeaderEnabled:(RCTPromiseResolveBlock)resolve :(RCTPromiseRejectBlock)reject) { + resolve(@(IBGNetworkLogger.w3CaughtHeaderEnabled)); +} + + - (NSDictionary *)constantsToExport { return ArgsRegistry.getAll; } diff --git a/ios/RNInstabug/Util/IBGAPM+PrivateAPIs.h b/ios/RNInstabug/Util/IBGAPM+PrivateAPIs.h new file mode 100644 index 0000000000..a451a0ad50 --- /dev/null +++ b/ios/RNInstabug/Util/IBGAPM+PrivateAPIs.h @@ -0,0 +1,15 @@ +// +// IBGAPM+PrivateAPIs.h +// Pods +// +// Created by Instabug on 02/06/2024. +// + +//#import "IBGAPM.h" + +@interface IBGAPM (PrivateAPIs) + +@property (class, atomic, assign) BOOL networkEnabled; + + +@end diff --git a/ios/RNInstabug/Util/IBGNetworkLogger+CP.h b/ios/RNInstabug/Util/IBGNetworkLogger+CP.h index 436553620e..805591d0ce 100644 --- a/ios/RNInstabug/Util/IBGNetworkLogger+CP.h +++ b/ios/RNInstabug/Util/IBGNetworkLogger+CP.h @@ -4,6 +4,11 @@ NS_ASSUME_NONNULL_BEGIN @interface IBGNetworkLogger (CP) +@property (class, atomic, assign) BOOL w3ExternalTraceIDEnabled; +@property (class, atomic, assign) BOOL w3ExternalGeneratedHeaderEnabled; +@property (class, atomic, assign) BOOL w3CaughtHeaderEnabled; + + + (void)disableAutomaticCapturingOfNetworkLogs; + (void)addNetworkLogWithUrl:(NSString *_Nonnull)url method:(NSString *_Nonnull)method @@ -27,6 +32,28 @@ NS_ASSUME_NONNULL_BEGIN generatedW3CTraceparent:(NSString * _Nullable)generatedW3CTraceparent caughtedW3CTraceparent:(NSString * _Nullable)caughtedW3CTraceparent; ++ (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 + isW3cCaughted:(NSNumber * _Nullable)isW3cCaughted + partialID:(NSNumber * _Nullable)partialID + timestamp:(NSNumber * _Nullable)timestamp + generatedW3CTraceparent:(NSString * _Nullable)generatedW3CTraceparent + caughtedW3CTraceparent:(NSString * _Nullable)caughtedW3CTraceparent; + @end NS_ASSUME_NONNULL_END diff --git a/ios/native.rb b/ios/native.rb index 6970521416..4fc710cbe5 100644 --- a/ios/native.rb +++ b/ios/native.rb @@ -1,4 +1,4 @@ -$instabug = { :version => '14.0.0' } +$instabug = { :version => '14.1.0' } def use_instabug! (spec = nil) version = $instabug[:version] diff --git a/package.json b/package.json index 75005c28bc..e065cb993d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "instabug-reactnative", "description": "React Native plugin for integrating the Instabug SDK", - "version": "14.0.0", + "version": "14.1.0", "author": "Instabug (https://instabug.com)", "repository": "github:Instabug/Instabug-React-Native", "homepage": "https://www.instabug.com/platforms/react-native", diff --git a/src/models/W3cExternalTraceAttributes.ts b/src/models/W3cExternalTraceAttributes.ts new file mode 100644 index 0000000000..f4e7ab6a45 --- /dev/null +++ b/src/models/W3cExternalTraceAttributes.ts @@ -0,0 +1,22 @@ +export type W3cExternalTraceAttributes = { + /** + * A key that determines if the traceparent header was found + */ + isW3cHeaderFound: boolean | null; + /** + * A unique identifier for the trace generated by the SDK in case of no cought header found + */ + partialId: number | null; + /** + * The start time of the network request + */ + networkStartTimeInSeconds: number | null; + /** + * The traceparent header generated by the SDK + */ + w3cGeneratedHeader: string | null; + /** + * The traceparent header received by the server + */ + w3cCaughtHeader: string | null; +}; diff --git a/src/modules/CrashReporting.ts b/src/modules/CrashReporting.ts index c374c7d772..858e035a32 100644 --- a/src/modules/CrashReporting.ts +++ b/src/modules/CrashReporting.ts @@ -5,6 +5,7 @@ import InstabugUtils from '../utils/InstabugUtils'; import { Platform } from 'react-native'; import type { NonFatalOptions } from '../models/NonFatalOptions'; import { NonFatalErrorLevel } from '../utils/Enums'; +import { Logger } from '../utils/logger'; /** * Enables and disables everything related to crash reporting including intercepting @@ -35,7 +36,7 @@ export const reportError = (error: ExtendedError, nonFatalOptions: NonFatalOptio ), ); } else { - console.warn( + Logger.warn( `IBG-RN: The error ${error} has been omitted because only error type is supported.`, ); return; diff --git a/src/modules/Instabug.ts b/src/modules/Instabug.ts index 1d528fba94..91f6c5c127 100644 --- a/src/modules/Instabug.ts +++ b/src/modules/Instabug.ts @@ -10,7 +10,8 @@ import type { NavigationAction, NavigationState as NavigationStateV4 } from 'rea import type { InstabugConfig } from '../models/InstabugConfig'; import Report from '../models/Report'; -import { NativeEvents, NativeInstabug, emitter } from '../native/NativeInstabug'; +import { emitter, NativeEvents, NativeInstabug } from '../native/NativeInstabug'; +import { registerW3CFlagsListener } from '../utils/FeatureFlags'; import { ColorTheme, Locale, @@ -26,6 +27,8 @@ import { captureUnhandledRejections } from '../utils/UnhandledRejectionTracking' import type { ReproConfig } from '../models/ReproConfig'; import type { FeatureFlag } from '../models/FeatureFlag'; import InstabugConstants from '../utils/InstabugConstants'; +import { InstabugRNConfig } from '../utils/config'; +import { Logger } from '../utils/logger'; let _currentScreen: string | null = null; let _lastScreen: string | null = null; @@ -68,6 +71,10 @@ export const init = (config: InstabugConfig) => { InstabugUtils.captureJsErrors(); captureUnhandledRejections(); + if (Platform.OS === 'android') { + registerW3CFlagsListener(); + } + // Default networkInterceptionMode to JavaScript if (config.networkInterceptionMode == null) { config.networkInterceptionMode = NetworkInterceptionMode.javascript; @@ -88,6 +95,8 @@ export const init = (config: InstabugConfig) => { _isFirstScreen = true; _currentScreen = firstScreen; + InstabugRNConfig.debugLogsLevel = config.debugLogsLevel ?? LogLevel.error; + reportCurrentViewForAndroid(firstScreen); setTimeout(() => { if (_currentScreen === firstScreen) { @@ -382,9 +391,10 @@ export const setReproStepsConfig = (config: ReproConfig) => { */ export const setUserAttribute = (key: string, value: string) => { if (!key || typeof key !== 'string' || typeof value !== 'string') { - console.error(InstabugConstants.SET_USER_ATTRIBUTES_ERROR_TYPE_MESSAGE); + Logger.error(InstabugConstants.SET_USER_ATTRIBUTES_ERROR_TYPE_MESSAGE); return; } + NativeInstabug.setUserAttribute(key, value); }; @@ -406,7 +416,7 @@ export const getUserAttribute = async (key: string): Promise => { */ export const removeUserAttribute = (key: string) => { if (!key || typeof key !== 'string') { - console.error(InstabugConstants.REMOVE_USER_ATTRIBUTES_ERROR_TYPE_MESSAGE); + Logger.error(InstabugConstants.REMOVE_USER_ATTRIBUTES_ERROR_TYPE_MESSAGE); return; } @@ -633,6 +643,13 @@ export const willRedirectToStore = () => { NativeInstabug.willRedirectToStore(); }; +/** + * This API has be called when changing the default Metro server port (8081) to exclude the DEV URL from network logging. + */ +export const setMetroDevServerPort = (port: number) => { + InstabugRNConfig.metroDevServerPort = port.toString(); +}; + export const componentDidAppearListener = (event: ComponentDidAppearEvent) => { if (_isFirstScreen) { _lastScreen = event.componentName; @@ -644,3 +661,20 @@ export const componentDidAppearListener = (event: ComponentDidAppearEvent) => { _lastScreen = event.componentName; } }; + +/** + * Sets listener to W3ExternalTraceID flag changes + * @param handler A callback that gets the update value of the flag + */ +export const _registerW3CFlagsChangeListener = ( + handler: (payload: { + isW3ExternalTraceIDEnabled: boolean; + isW3ExternalGeneratedHeaderEnabled: boolean; + isW3CaughtHeaderEnabled: boolean; + }) => void, +) => { + emitter.addListener(NativeEvents.ON_W3C_FLAGS_CHANGE, (payload) => { + handler(payload); + }); + NativeInstabug.registerW3CFlagsChangeListener(); +}; diff --git a/src/modules/NetworkLogger.ts b/src/modules/NetworkLogger.ts index 67f3a54ccf..1e40b9fa9b 100644 --- a/src/modules/NetworkLogger.ts +++ b/src/modules/NetworkLogger.ts @@ -2,7 +2,9 @@ import type { RequestHandler } from '@apollo/client'; import InstabugConstants from '../utils/InstabugConstants'; import xhr, { NetworkData, ProgressCallback } from '../utils/XhrNetworkInterceptor'; -import { reportNetworkLog, isContentTypeNotAllowed } from '../utils/InstabugUtils'; +import { isContentTypeNotAllowed, reportNetworkLog } from '../utils/InstabugUtils'; +import { InstabugRNConfig } from '../utils/config'; +import { Logger } from '../utils/logger'; export type { NetworkData }; @@ -10,6 +12,11 @@ export type NetworkDataObfuscationHandler = (data: NetworkData) => Promise { network = await _networkDataObfuscationHandler(network); } + if (__DEV__) { + const urlPort = getPortFromUrl(network.url); + if (urlPort === InstabugRNConfig.metroDevServerPort) { + return; + } + } 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); + Logger.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); + Logger.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( + Logger.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( + Logger.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); + Logger.error(e); } } }); @@ -96,7 +109,7 @@ export const apolloLinkRequestHandler: RequestHandler = (operation, forward) => return { headers: newHeaders }; }); } catch (e) { - console.error(e); + Logger.error(e); } return forward(operation); diff --git a/src/native/NativeAPM.ts b/src/native/NativeAPM.ts index b1981cfe37..9fa30b702c 100644 --- a/src/native/NativeAPM.ts +++ b/src/native/NativeAPM.ts @@ -1,5 +1,7 @@ import type { NativeModule } from 'react-native'; +import { NativeEventEmitter } from 'react-native'; +import type { W3cExternalTraceAttributes } from '../models/W3cExternalTraceAttributes'; import { NativeModules } from './NativePackage'; export interface ApmNativeModule extends NativeModule { @@ -22,6 +24,7 @@ export interface ApmNativeModule extends NativeModule { statusCode: number, responseContentType: string, errorDomain: string, + w3cExternalTraceAttributes: W3cExternalTraceAttributes, gqlQueryName?: string, serverErrorMessage?: string, ): void; @@ -48,3 +51,5 @@ export interface ApmNativeModule extends NativeModule { } export const NativeAPM = NativeModules.IBGAPM; + +export const emitter = new NativeEventEmitter(NativeAPM); diff --git a/src/native/NativeInstabug.ts b/src/native/NativeInstabug.ts index 3b72f5951f..5f0628ef71 100644 --- a/src/native/NativeInstabug.ts +++ b/src/native/NativeInstabug.ts @@ -11,6 +11,7 @@ import type { WelcomeMessageMode, } from '../utils/Enums'; import type { NativeConstants } from './NativeConstants'; +import type { W3cExternalTraceAttributes } from '../models/W3cExternalTraceAttributes'; import { NativeModules } from './NativePackage'; export interface InstabugNativeModule extends NativeModule { @@ -67,6 +68,7 @@ export interface InstabugNativeModule extends NativeModule { duration: number, gqlQueryName: string | undefined, serverErrorMessage: string | undefined, + W3cExternalTraceAttributes: W3cExternalTraceAttributes, ): void; setNetworkLoggingEnabled(isEnabled: boolean): void; @@ -140,12 +142,23 @@ export interface InstabugNativeModule extends NativeModule { addFileAttachmentWithURLToReport(url: string, filename?: string): void; addFileAttachmentWithDataToReport(data: string, filename?: string): void; willRedirectToStore(): void; + + // W3C Feature Flags + isW3ExternalTraceIDEnabled(): Promise; + + isW3ExternalGeneratedHeaderEnabled(): Promise; + + isW3CaughtHeaderEnabled(): Promise; + + // W3C Feature Flags Listener for Android + registerW3CFlagsChangeListener(): void; } export const NativeInstabug = NativeModules.Instabug; export enum NativeEvents { PRESENDING_HANDLER = 'IBGpreSendingHandler', + ON_W3C_FLAGS_CHANGE = 'IBGOnNewW3CFlagsUpdateReceivedCallback', } export const emitter = new NativeEventEmitter(NativeInstabug); diff --git a/src/utils/FeatureFlags.ts b/src/utils/FeatureFlags.ts new file mode 100644 index 0000000000..479ab7ba47 --- /dev/null +++ b/src/utils/FeatureFlags.ts @@ -0,0 +1,28 @@ +import { NativeInstabug } from '../native/NativeInstabug'; +import { _registerW3CFlagsChangeListener } from '../modules/Instabug'; + +export const FeatureFlags = { + isW3ExternalTraceID: () => NativeInstabug.isW3ExternalTraceIDEnabled(), + isW3ExternalGeneratedHeader: () => NativeInstabug.isW3ExternalGeneratedHeaderEnabled(), + isW3CaughtHeader: () => NativeInstabug.isW3CaughtHeaderEnabled(), +}; + +export const registerW3CFlagsListener = () => { + _registerW3CFlagsChangeListener( + (res: { + isW3ExternalTraceIDEnabled: boolean; + isW3ExternalGeneratedHeaderEnabled: boolean; + isW3CaughtHeaderEnabled: boolean; + }) => { + FeatureFlags.isW3ExternalTraceID = async () => { + return res.isW3ExternalTraceIDEnabled; + }; + FeatureFlags.isW3ExternalGeneratedHeader = async () => { + return res.isW3ExternalGeneratedHeaderEnabled; + }; + FeatureFlags.isW3CaughtHeader = async () => { + return res.isW3CaughtHeaderEnabled; + }; + }, + ); +}; diff --git a/src/utils/InstabugConstants.ts b/src/utils/InstabugConstants.ts index 6d117d9871..aedc840701 100644 --- a/src/utils/InstabugConstants.ts +++ b/src/utils/InstabugConstants.ts @@ -11,6 +11,7 @@ const InstabugConstants = { 'IBG-RN: Expected key and value passed to setUserAttribute to be of type string', REMOVE_USER_ATTRIBUTES_ERROR_TYPE_MESSAGE: 'IBG-RN: Expected key and value passed to removeUserAttribute to be of type string', + DEFAULT_METRO_PORT: '8081', }; export default InstabugConstants; diff --git a/src/utils/InstabugUtils.ts b/src/utils/InstabugUtils.ts index d4238f14f0..df19f0d42a 100644 --- a/src/utils/InstabugUtils.ts +++ b/src/utils/InstabugUtils.ts @@ -126,6 +126,44 @@ export async function sendCrashReport( return remoteSenderCallback(jsonObject); } +/** + * Generate random 32 bit unsigned integer Hexadecimal (8 chars) lower case letters + * Should not return all zeros + */ +export const generateTracePartialId = () => { + let randomNumber: number; + let hexString: string; + + do { + randomNumber = Math.floor(Math.random() * 0xffffffff); + hexString = randomNumber.toString(16).padStart(8, '0'); + } while (hexString === '00000000'); + + return { numberPartilId: randomNumber, hexStringPartialId: hexString.toLowerCase() }; +}; +/** + * Generate W3C header in the format of {version}-{trace-id}-{parent-id}-{trace-flag} + * @param networkStartTime + * @returns w3c header + */ +export const generateW3CHeader = (networkStartTime: number) => { + const { hexStringPartialId, numberPartilId } = generateTracePartialId(); + + const TRACESTATE = '4942472d'; + const VERSION = '00'; + const TRACE_FLAG = '01'; + + const timestampInSeconds = Math.floor(networkStartTime.valueOf() / 1000); + const hexaDigitsTimestamp = timestampInSeconds.toString(16).toLowerCase(); + const traceId = `${hexaDigitsTimestamp}${hexStringPartialId}${hexaDigitsTimestamp}${hexStringPartialId}`; + const parentId = `${TRACESTATE}${hexStringPartialId}`; + + return { + timestampInSeconds, + partialId: numberPartilId, + w3cHeader: `${VERSION}-${traceId}-${parentId}-${TRACE_FLAG}`, + }; +}; export function isContentTypeNotAllowed(contentType: string) { const allowed = [ @@ -171,6 +209,13 @@ export function reportNetworkLog(network: NetworkData) { network.responseCode, network.contentType, network.errorDomain, + { + isW3cHeaderFound: network.isW3cHeaderFound, + partialId: network.partialId, + networkStartTimeInSeconds: network.networkStartTimeInSeconds, + w3cGeneratedHeader: network.w3cGeneratedHeader, + w3cCaughtHeader: network.w3cCaughtHeader, + }, network.gqlQueryName, network.serverErrorMessage, ); @@ -192,6 +237,13 @@ export function reportNetworkLog(network: NetworkData) { network.duration, network.gqlQueryName, network.serverErrorMessage, + { + isW3cHeaderFound: network.isW3cHeaderFound, + partialId: network.partialId, + networkStartTimeInSeconds: network.networkStartTimeInSeconds, + w3cGeneratedHeader: network.w3cGeneratedHeader, + w3cCaughtHeader: network.w3cCaughtHeader, + }, ); } } @@ -204,4 +256,6 @@ export default { getStackTrace, stringifyIfNotString, sendCrashReport, + generateTracePartialId, + generateW3CHeader, }; diff --git a/src/utils/UnhandledRejectionTracking.ts b/src/utils/UnhandledRejectionTracking.ts index d8049a9b09..9cbe0dc91a 100644 --- a/src/utils/UnhandledRejectionTracking.ts +++ b/src/utils/UnhandledRejectionTracking.ts @@ -2,6 +2,7 @@ import tracking, { RejectionTrackingOptions } from 'promise/setimmediate/rejecti import { sendCrashReport } from './InstabugUtils'; import { NativeCrashReporting } from '../native/NativeCrashReporting'; import { NonFatalErrorLevel } from './Enums'; +import { Logger } from './logger'; export interface HermesInternalType { enablePromiseRejectionTracker?: (options?: RejectionTrackingOptions) => void; @@ -113,5 +114,5 @@ function _originalOnUnhandled(id: number, rejection: unknown = {}) { `Possible Unhandled Promise Rejection (id: ${id}):\n` + `${message ?? ''}\n` + (stack == null ? '' : stack); - console.warn(warning); + Logger.warn(warning); } diff --git a/src/utils/XhrNetworkInterceptor.ts b/src/utils/XhrNetworkInterceptor.ts index 98c5ef9cc5..4443940362 100644 --- a/src/utils/XhrNetworkInterceptor.ts +++ b/src/utils/XhrNetworkInterceptor.ts @@ -1,5 +1,7 @@ import InstabugConstants from './InstabugConstants'; -import { stringifyIfNotString } from './InstabugUtils'; +import { stringifyIfNotString, generateW3CHeader } from './InstabugUtils'; + +import { FeatureFlags } from '../utils/FeatureFlags'; export type ProgressCallback = (totalBytesSent: number, totalBytesExpectedToSend: number) => void; export type NetworkDataCallback = (data: NetworkData) => void; @@ -22,6 +24,11 @@ export interface NetworkData { gqlQueryName?: string; serverErrorMessage: string; requestContentType: string; + isW3cHeaderFound: boolean | null; + partialId: number | null; + networkStartTimeInSeconds: number | null; + w3cGeneratedHeader: string | null; + w3cCaughtHeader: string | null; } const XMLHttpRequest = global.XMLHttpRequest; @@ -53,8 +60,85 @@ const _reset = () => { gqlQueryName: '', serverErrorMessage: '', requestContentType: '', + isW3cHeaderFound: null, + partialId: null, + networkStartTimeInSeconds: null, + w3cGeneratedHeader: null, + w3cCaughtHeader: null, }; }; +const getTraceparentHeader = async (networkData: NetworkData) => { + const [ + isW3cExternalTraceIDEnabled, + isW3cExternalGeneratedHeaderEnabled, + isW3cCaughtHeaderEnabled, + ] = await Promise.all([ + FeatureFlags.isW3ExternalTraceID(), + FeatureFlags.isW3ExternalGeneratedHeader(), + FeatureFlags.isW3CaughtHeader(), + ]); + + return injectHeaders(networkData, { + isW3cExternalTraceIDEnabled, + isW3cExternalGeneratedHeaderEnabled, + isW3cCaughtHeaderEnabled, + }); +}; + +export const injectHeaders = async ( + networkData: NetworkData, + featureFlags: { + isW3cExternalTraceIDEnabled: boolean; + isW3cExternalGeneratedHeaderEnabled: boolean; + isW3cCaughtHeaderEnabled: boolean; + }, +) => { + const { + isW3cExternalTraceIDEnabled, + isW3cExternalGeneratedHeaderEnabled, + isW3cCaughtHeaderEnabled, + } = featureFlags; + + if (!isW3cExternalTraceIDEnabled) { + return; + } + + const isHeaderFound = networkData.requestHeaders.traceparent != null; + + networkData.isW3cHeaderFound = isHeaderFound; + + const injectionMethodology = isHeaderFound + ? identifyCaughtHeader(networkData, isW3cCaughtHeaderEnabled) + : injectGeneratedData(networkData, isW3cExternalGeneratedHeaderEnabled); + return injectionMethodology; +}; + +const identifyCaughtHeader = async ( + networkData: NetworkData, + isW3cCaughtHeaderEnabled: boolean, +) => { + if (isW3cCaughtHeaderEnabled) { + networkData.w3cCaughtHeader = networkData.requestHeaders.traceparent; + return networkData.requestHeaders.traceparent; + } + return; +}; + +const injectGeneratedData = ( + networkData: NetworkData, + isW3cExternalGeneratedHeaderEnabled: boolean, +) => { + const { timestampInSeconds, partialId, w3cHeader } = generateW3CHeader(networkData.startTime); + networkData.partialId = partialId; + networkData.networkStartTimeInSeconds = timestampInSeconds; + + if (isW3cExternalGeneratedHeaderEnabled) { + networkData.w3cGeneratedHeader = w3cHeader; + return w3cHeader; + } + + return; +}; export default { setOnDoneCallback(callback: NetworkDataCallback) { @@ -91,7 +175,7 @@ export default { originalXHRSetRequestHeader.apply(this, [header, value]); }; - XMLHttpRequest.prototype.send = function (data) { + XMLHttpRequest.prototype.send = async function (data) { const cloneNetwork = JSON.parse(JSON.stringify(network)); cloneNetwork.requestBody = data ? data : ''; @@ -226,6 +310,10 @@ export default { } cloneNetwork.startTime = Date.now(); + const traceparent = await getTraceparentHeader(cloneNetwork); + if (traceparent) { + this.setRequestHeader('Traceparent', traceparent); + } originalXHRSend.apply(this, [data]); }; isInterceptorEnabled = true; diff --git a/src/utils/config.ts b/src/utils/config.ts new file mode 100644 index 0000000000..de7073d931 --- /dev/null +++ b/src/utils/config.ts @@ -0,0 +1,7 @@ +import InstabugConstants from './InstabugConstants'; +import { LogLevel } from './Enums'; + +export const InstabugRNConfig = { + metroDevServerPort: InstabugConstants.DEFAULT_METRO_PORT, + debugLogsLevel: LogLevel.error, +}; diff --git a/src/utils/logger.ts b/src/utils/logger.ts new file mode 100644 index 0000000000..e43d740d03 --- /dev/null +++ b/src/utils/logger.ts @@ -0,0 +1,54 @@ +import { InstabugRNConfig } from './config'; +import { LogLevel } from './Enums'; + +export class Logger { + private static shouldLog(level: LogLevel): boolean { + const currentLevel = InstabugRNConfig.debugLogsLevel; + + // Return true if the current log level is equal to or more verbose than the requested level + const logLevelHierarchy: Record = { + [LogLevel.verbose]: 3, + [LogLevel.debug]: 2, + [LogLevel.error]: 1, + [LogLevel.none]: 0, + }; + + return logLevelHierarchy[currentLevel] >= logLevelHierarchy[level]; + } + + // General logging method that takes a logging function as an argument + private static logMessage( + level: LogLevel, + logMethod: (...args: any[]) => void, + message?: any, + ...optionalParams: any[] + ): void { + if (this.shouldLog(level)) { + logMethod(message, ...optionalParams); + } + } + + static error(message?: any, ...optionalParams: any[]) { + this.logMessage(LogLevel.error, console.error, message, ...optionalParams); // Pass console.error for errors + } + + static info(message?: any, ...optionalParams: any[]) { + this.logMessage(LogLevel.verbose, console.info, message, ...optionalParams); // Pass console.info for info + } + + static log(message?: any, ...optionalParams: any[]) { + this.logMessage(LogLevel.verbose, console.log, message, ...optionalParams); // Default log method + } + + static warn(message?: any, ...optionalParams: any[]) { + this.logMessage(LogLevel.debug, console.warn, message, ...optionalParams); // Use console.warn for debug + } + + static trace(message?: any, ...optionalParams: any[]) { + this.logMessage(LogLevel.debug, console.trace, message, ...optionalParams); // Use console.trace for debugging + } + + static debug(message?: any, ...optionalParams: any[]) { + this.logMessage(LogLevel.debug, console.debug, message, ...optionalParams); // Use console.debug for debug logs + } +} diff --git a/test/mocks/mockInstabug.ts b/test/mocks/mockInstabug.ts index 5139afcde3..7b3cf2e695 100644 --- a/test/mocks/mockInstabug.ts +++ b/test/mocks/mockInstabug.ts @@ -69,6 +69,10 @@ const mockInstabug: InstabugNativeModule = { addFileAttachmentWithDataToReport: jest.fn(), setNetworkLoggingEnabled: jest.fn(), willRedirectToStore: jest.fn(), + isW3ExternalTraceIDEnabled: jest.fn(), + isW3ExternalGeneratedHeaderEnabled: jest.fn(), + isW3CaughtHeaderEnabled: jest.fn(), + registerW3CFlagsChangeListener: jest.fn(), }; export default mockInstabug; diff --git a/test/modules/Instabug.spec.ts b/test/modules/Instabug.spec.ts index 46b4b208e1..d1bca25c90 100644 --- a/test/modules/Instabug.spec.ts +++ b/test/modules/Instabug.spec.ts @@ -24,6 +24,7 @@ import { import InstabugUtils from '../../src/utils/InstabugUtils'; import type { FeatureFlag } from '../../src/models/FeatureFlag'; import InstabugConstants from '../../src/utils/InstabugConstants'; +import { Logger } from '../../src/utils/logger'; describe('Instabug Module', () => { beforeEach(() => { @@ -641,7 +642,7 @@ describe('Instabug Module', () => { [{}, 'value'], ['key', []], ])("should fail if key and value aren't strings when calling setUserAttribute", (key, value) => { - const logSpy = jest.spyOn(console, 'error'); + const logSpy = jest.spyOn(Logger, 'error'); // @ts-ignore Instabug.setUserAttribute(key, value); @@ -870,4 +871,20 @@ describe('Instabug Module', () => { Instabug.willRedirectToStore(); expect(NativeInstabug.willRedirectToStore).toBeCalledTimes(1); }); + + it('should register W3C flag listener', async () => { + const callback = jest.fn(); + Instabug._registerW3CFlagsChangeListener(callback); + + expect(NativeInstabug.registerW3CFlagsChangeListener).toBeCalledTimes(1); + }); + + it('should invoke callback on emitting the event IBGOnNewW3CFlagsUpdateReceivedCallback', () => { + const callback = jest.fn(); + Instabug._registerW3CFlagsChangeListener(callback); + emitter.emit(NativeEvents.ON_W3C_FLAGS_CHANGE); + + expect(emitter.listenerCount(NativeEvents.ON_W3C_FLAGS_CHANGE)).toBe(1); + expect(callback).toHaveBeenCalled(); + }); }); diff --git a/test/modules/NetworkLogger.spec.ts b/test/modules/NetworkLogger.spec.ts index 71dd2dd778..dbf35eddb9 100644 --- a/test/modules/NetworkLogger.spec.ts +++ b/test/modules/NetworkLogger.spec.ts @@ -7,6 +7,7 @@ import * as NetworkLogger from '../../src/modules/NetworkLogger'; import Interceptor from '../../src/utils/XhrNetworkInterceptor'; import { isContentTypeNotAllowed, reportNetworkLog } from '../../src/utils/InstabugUtils'; import InstabugConstants from '../../src/utils/InstabugConstants'; +import { Logger } from '../../src/utils/logger'; const clone = (obj: T): T => { return JSON.parse(JSON.stringify(obj)); @@ -30,6 +31,11 @@ describe('NetworkLogger Module', () => { startTime: 0, serverErrorMessage: '', requestContentType: 'application/json', + isW3cHeaderFound: null, + partialId: null, + networkStartTimeInSeconds: null, + w3cGeneratedHeader: null, + w3cCaughtHeader: null, }; beforeEach(() => { @@ -89,7 +95,7 @@ describe('NetworkLogger Module', () => { 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(() => {}); + const consoleSpy = jest.spyOn(Logger, 'error').mockImplementation(() => {}); // Make a circular object, this should make JSON.stringify fail const handler = jest.fn(() => { @@ -133,7 +139,7 @@ describe('NetworkLogger Module', () => { }); it('should not break if apollo handler throws an error', async () => { - const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + const consoleSpy = jest.spyOn(Logger, 'error').mockImplementation(() => {}); const operation = { setContext: jest.fn(() => { @@ -150,7 +156,7 @@ describe('NetworkLogger Module', () => { }); it('should omit request body if its content type is not allowed', () => { - const consoleWarn = jest.spyOn(console, 'warn').mockImplementation(); + const consoleWarn = jest.spyOn(Logger, 'warn').mockImplementation(); jest.mocked(isContentTypeNotAllowed).mockReturnValueOnce(true); const networkData = { @@ -175,7 +181,7 @@ describe('NetworkLogger Module', () => { }); it('should omit response body if its content type is not allowed', () => { - const consoleWarn = jest.spyOn(console, 'warn').mockImplementation(); + const consoleWarn = jest.spyOn(Logger, 'warn').mockImplementation(); jest.mocked(isContentTypeNotAllowed).mockReturnValueOnce(true); const networkData = { @@ -200,7 +206,7 @@ describe('NetworkLogger Module', () => { }); it('should omit request body if its size exceeds the maximum allowed size', () => { - const consoleWarn = jest.spyOn(console, 'warn').mockImplementation(); + const consoleWarn = jest.spyOn(Logger, 'warn').mockImplementation(); const networkData = { ...network, @@ -239,7 +245,7 @@ describe('NetworkLogger Module', () => { }); it('should omit response body if its size exceeds the maximum allowed size', () => { - const consoleWarn = jest.spyOn(console, 'warn').mockImplementation(); + const consoleWarn = jest.spyOn(Logger, 'warn').mockImplementation(); const networkData = { ...network, diff --git a/test/utils/InstabugUtils.spec.ts b/test/utils/InstabugUtils.spec.ts index becfccc0e9..fd389d7f0b 100644 --- a/test/utils/InstabugUtils.spec.ts +++ b/test/utils/InstabugUtils.spec.ts @@ -258,6 +258,11 @@ describe('reportNetworkLog', () => { errorDomain: 'errorDomain', serverErrorMessage: 'serverErrorMessage', requestContentType: 'requestContentType', + isW3cHeaderFound: null, + partialId: null, + networkStartTimeInSeconds: null, + w3cGeneratedHeader: null, + w3cCaughtHeader: null, }; it('reportNetworkLog should send network logs to native with the correct parameters on Android', () => { @@ -296,6 +301,13 @@ describe('reportNetworkLog', () => { network.responseCode, network.contentType, network.errorDomain, + { + isW3cHeaderFound: null, + partialId: null, + networkStartTimeInSeconds: null, + w3cGeneratedHeader: null, + w3cCaughtHeader: null, + }, network.gqlQueryName, network.serverErrorMessage, ); @@ -324,6 +336,13 @@ describe('reportNetworkLog', () => { network.duration, network.gqlQueryName, network.serverErrorMessage, + { + isW3cHeaderFound: null, + partialId: null, + networkStartTimeInSeconds: null, + w3cGeneratedHeader: null, + w3cCaughtHeader: null, + }, ); }); }); diff --git a/test/utils/XhrNetworkInterceptor.spec.ts b/test/utils/XhrNetworkInterceptor.spec.ts index 10a8f1abb8..dfb9e7f43b 100644 --- a/test/utils/XhrNetworkInterceptor.spec.ts +++ b/test/utils/XhrNetworkInterceptor.spec.ts @@ -4,7 +4,7 @@ import nock from 'nock'; import waitForExpect from 'wait-for-expect'; import InstabugConstants from '../../src/utils/InstabugConstants'; -import Interceptor from '../../src/utils/XhrNetworkInterceptor'; +import Interceptor, { injectHeaders } from '../../src/utils/XhrNetworkInterceptor'; const url = 'http://api.instabug.com'; const method = 'GET'; @@ -293,3 +293,203 @@ describe('Network Interceptor', () => { FakeRequest.send(); }); }); + +describe('Network Interceptor W3C Headers', () => { + beforeEach(() => { + nock.cleanAll(); + }); + + it('should attach generated header if all flags are enabled on no header found', (done) => { + const featureFlags = { + isW3cExternalTraceIDEnabled: true, + isW3cExternalGeneratedHeaderEnabled: true, + isW3cCaughtHeaderEnabled: true, + }; + Interceptor.enableInterception(); + Interceptor.setOnDoneCallback((network) => { + injectHeaders(network, featureFlags); + expect(network.isW3cHeaderFound).toBe(false); + expect(network.partialId).not.toBe(null); + expect(network.networkStartTimeInSeconds).toEqual(Math.floor(network.startTime / 1000)); + expect(network.w3cGeneratedHeader).toHaveLength(55); + expect(network.w3cCaughtHeader).toBe(null); + }); + done(); + FakeRequest.mockResponse(request); + FakeRequest.open(method, url); + FakeRequest.send(); + }); + it('should attach generated header if key flag & generated header flags are enabled on no header found', (done) => { + const featureFlags = { + isW3cExternalTraceIDEnabled: true, + isW3cExternalGeneratedHeaderEnabled: true, + isW3cCaughtHeaderEnabled: false, + }; + Interceptor.enableInterception(); + Interceptor.setOnDoneCallback((network) => { + injectHeaders(network, featureFlags); + expect(network.isW3cHeaderFound).toBe(false); + expect(network.partialId).not.toBe(null); + expect(network.networkStartTimeInSeconds).toEqual(Math.floor(network.startTime / 1000)); + expect(network.w3cGeneratedHeader).toHaveLength(55); + expect(network.w3cCaughtHeader).toBe(null); + }); + done(); + FakeRequest.mockResponse(request); + FakeRequest.open(method, url); + FakeRequest.send(); + }); + + it('should not attach headers when key flag is disabled & generated, caught header flags are enabled', (done) => { + const featureFlags = { + isW3cExternalTraceIDEnabled: false, + isW3cExternalGeneratedHeaderEnabled: true, + isW3cCaughtHeaderEnabled: true, + }; + Interceptor.enableInterception(); + Interceptor.setOnDoneCallback((network) => { + injectHeaders(network, featureFlags); + expect(network.isW3cHeaderFound).toBe(null); + expect(network.partialId).toBe(null); + expect(network.networkStartTimeInSeconds).toBe(null); + expect(network.w3cGeneratedHeader).toBe(null); + expect(network.w3cCaughtHeader).toBe(null); + expect(network.requestHeaders).not.toHaveProperty('traceparent'); + + done(); + }); + FakeRequest.mockResponse(request); + FakeRequest.open(method, url); + FakeRequest.send(); + }); + it('should not attach headers when all feature flags are disabled', (done) => { + const featureFlags = { + isW3cExternalTraceIDEnabled: false, + isW3cExternalGeneratedHeaderEnabled: false, + isW3cCaughtHeaderEnabled: false, + }; + Interceptor.enableInterception(); + Interceptor.setOnDoneCallback((network) => { + injectHeaders(network, featureFlags); + expect(network.isW3cHeaderFound).toBe(null); + expect(network.partialId).toBe(null); + expect(network.networkStartTimeInSeconds).toBe(null); + expect(network.w3cGeneratedHeader).toBe(null); + expect(network.w3cCaughtHeader).toBe(null); + expect(network.requestHeaders).not.toHaveProperty('traceparent'); + + done(); + }); + FakeRequest.mockResponse(request); + FakeRequest.open(method, url); + FakeRequest.send(); + }); + it('should not attach headers when key & caught header flags are disabled and generated header flag is enabled', (done) => { + const featureFlags = { + isW3cExternalTraceIDEnabled: false, + isW3cExternalGeneratedHeaderEnabled: true, + isW3cCaughtHeaderEnabled: false, + }; + Interceptor.enableInterception(); + Interceptor.setOnDoneCallback((network) => { + injectHeaders(network, featureFlags); + expect(network.isW3cHeaderFound).toBe(null); + expect(network.partialId).toBe(null); + expect(network.networkStartTimeInSeconds).toBe(null); + expect(network.w3cGeneratedHeader).toBe(null); + expect(network.w3cCaughtHeader).toBe(null); + expect(network.requestHeaders).not.toHaveProperty('traceparent'); + done(); + }); + FakeRequest.mockResponse(request); + FakeRequest.open(method, url); + FakeRequest.send(); + }); + it('should not attach headers when key & generated header flags are disabled and caught header flag is enabled', (done) => { + const featureFlags = { + isW3cExternalTraceIDEnabled: false, + isW3cExternalGeneratedHeaderEnabled: false, + isW3cCaughtHeaderEnabled: true, + }; + Interceptor.enableInterception(); + Interceptor.setOnDoneCallback((network) => { + injectHeaders(network, featureFlags); + expect(network.isW3cHeaderFound).toBe(null); + expect(network.partialId).toBe(null); + expect(network.networkStartTimeInSeconds).toBe(null); + expect(network.w3cGeneratedHeader).toBe(null); + expect(network.w3cCaughtHeader).toBe(null); + expect(network.requestHeaders).not.toHaveProperty('traceparent'); + done(); + }); + FakeRequest.mockResponse(request); + FakeRequest.open(method, url); + FakeRequest.send(); + }); + it('should not attach headers when key flag is enabled & generated, caught header flags are disabled on header found', (done) => { + const featureFlags = { + isW3cExternalTraceIDEnabled: true, + isW3cExternalGeneratedHeaderEnabled: false, + isW3cCaughtHeaderEnabled: false, + }; + Interceptor.enableInterception(); + Interceptor.setOnDoneCallback((network) => { + network.requestHeaders.traceparent = 'caught traceparent header'; + injectHeaders(network, featureFlags); + expect(network.isW3cHeaderFound).toEqual(true); + expect(network.partialId).toBe(null); + expect(network.networkStartTimeInSeconds).toBe(null); + expect(network.w3cGeneratedHeader).toBe(null); + expect(network.w3cCaughtHeader).toBe(null); + done(); + }); + FakeRequest.mockResponse(request); + FakeRequest.open(method, url); + FakeRequest.send(); + }); + + it('should attach caught header if all flags are enabled ', (done) => { + const featureFlags = { + isW3cExternalTraceIDEnabled: true, + isW3cExternalGeneratedHeaderEnabled: true, + isW3cCaughtHeaderEnabled: true, + }; + Interceptor.enableInterception(); + Interceptor.setOnDoneCallback((network) => { + network.requestHeaders.traceparent = 'caught traceparent header'; + injectHeaders(network, featureFlags); + expect(network.isW3cHeaderFound).toBe(true); + expect(network.partialId).toBe(null); + expect(network.networkStartTimeInSeconds).toBe(null); + expect(network.w3cGeneratedHeader).toBe(null); + expect(network.w3cCaughtHeader).toBe('caught traceparent header'); + expect(network.requestHeaders).toHaveProperty('traceparent'); + done(); + }); + FakeRequest.mockResponse(request); + FakeRequest.open(method, url); + FakeRequest.send(); + }); + it('should attach caught header if key & caught header flags are enabled and generated header flag is disabled', (done) => { + const featureFlags = { + isW3cExternalTraceIDEnabled: true, + isW3cExternalGeneratedHeaderEnabled: false, + isW3cCaughtHeaderEnabled: true, + }; + Interceptor.enableInterception(); + Interceptor.setOnDoneCallback((network) => { + network.requestHeaders.traceparent = 'caught traceparent header'; + injectHeaders(network, featureFlags); + expect(network.isW3cHeaderFound).toBe(true); + expect(network.partialId).toBe(null); + expect(network.networkStartTimeInSeconds).toBe(null); + expect(network.w3cGeneratedHeader).toBe(null); + expect(network.w3cCaughtHeader).toBe('caught traceparent header'); + expect(network.requestHeaders).toHaveProperty('traceparent'); + done(); + }); + FakeRequest.mockResponse(request); + FakeRequest.open(method, url); + FakeRequest.send(); + }); +});