diff --git a/.circleci/config.yml b/.circleci/config.yml index 3293c65d1..afebbdeaf 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -401,22 +401,33 @@ jobs: prepare_steps: type: steps default: [] + api_endpoint: + type: string + dashboard_token_env: + type: string working_directory: ~/project executor: name: node/default steps: - advanced-checkout/shallow-checkout + - run: git clone git@github.com:Instabug/Escape.git + - run: + working_directory: Escape + command: swift build -c release + - run: + working_directory: Escape/.build/release + command: cp -f Escape /usr/local/bin/escape - steps: << parameters.prepare_steps >> - install_node_modules - run: name: Build the SDK command: yarn build - run: - name: Authorize with NPM - command: echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc - - run: - name: Publish new enterprise version - command: npm publish + name: Publish Custom Package with Escape + command: Escape react-native publish-custompackage + environment: + DASHBOARD_TOKEN: << parameters.dashboard_token_env >> + DASHBOARD_API_ENDPOINT: << parameters.api_endpoint >> publish: macos: @@ -438,8 +449,11 @@ jobs: working_directory: project command: yarn build - run: + name: Publish Official Package with Escape working_directory: project command: Escape react-native publish + environment: + DASHBOARD_API_ENDPOINT: api.instabug.com publish_new_namespace: working_directory: ~/project executor: @@ -562,6 +576,8 @@ workflows: npm_package: '@instabug/react-native-nn' android_package: nn api_endpoint: st001009nn.instabug.com + api_endpoint: st001009nn.instabug.com + dashboard_token_env: ${NN_TOKEN} - hold_release_injazat: requires: *release_dependencies @@ -581,6 +597,8 @@ workflows: npm_package: '@instabug/react-native-injazat' android_package: injazat api_endpoint: st001013mec1.instabug.com + api_endpoint: st001013mec1.instabug.com + dashboard_token_env: ${INJAZAT_TOKEN} # Dream11 tests - hold_test_dream11: @@ -622,3 +640,5 @@ workflows: only: dream11 prepare_steps: - prepare_dream11 + api_endpoint: st001012dream11.instabug.com + dashboard_token_env: ${DREAM11_TOKEN} diff --git a/CHANGELOG.md b/CHANGELOG.md index f53e24293..55cd87389 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,66 @@ # Changelog +## [15.0.2](https://github.com/Instabug/Instabug-React-Native/compare/v15.2.0...dev) + +### Added + +- Add support for ignoreFlagSecure to bypass SDK screenshot security protocols on Android. ([#1394](https://github.com/Instabug/Instabug-React-Native/pull/1394)) + +### Fixed + +- async initialization. ([#1427](https://github.com/Instabug/Instabug-React-Native/pull/1427)) + +## [15.0.1](https://github.com/Instabug/Instabug-React-Native/compare/v14.3.0...v15.0.1) + +### Added + +- Add support enable/disable screenshot auto masking. ([#1389](https://github.com/Instabug/Instabug-React-Native/pull/1389)) + +- Add support for BugReporting user consents. ([#1383](https://github.com/Instabug/Instabug-React-Native/pull/1383)) + +- Add support for xCode 16. ([#1370](https://github.com/Instabug/Instabug-React-Native/pull/1370)) + +- Add support for network spans. ([#1394](https://github.com/Instabug/Instabug-React-Native/pull/1394)) + +- Add respect to backend network body limit. ([#1397](https://github.com/Instabug/Instabug-React-Native/pull/1397)) + +### Changed + +- Bump Instabug iOS SDK to v15.1.1 ([#1402](https://github.com/Instabug/Instabug-React-Native/pull/1402)). [See release notes](https://github.com/Instabug/Instabug-iOS/releases/tag/15.1.1). + +- Bump Instabug Android SDK to v15.0.1 ([#1402](https://github.com/Instabug/Instabug-React-Native/pull/1402)). [See release notes](https://github.com/Instabug/Instabug-Android/releases/tag/v15.0.1). + +### Fixed + +- Not sending the inComplete xhrRequest. ([#1365](https://github.com/Instabug/Instabug-React-Native/pull/1365)) + +- Added more search capabilities to the find-token.sh script. e.g., searching in .env file for react config. [#1366](https://github.com/Instabug/Instabug-React-Native/pull/1366) + +- Updated the CHANGELOG on the dashboard for the enterprise users. [#1404](https://github.com/Instabug/Instabug-React-Native/pull/1404) + +## [14.3.0](https://github.com/Instabug/Instabug-React-Native/compare/v14.1.0...14.3.0) + +### Added + +- Add support for enable/disable capturing network body. ([#1362](https://github.com/Instabug/Instabug-React-Native/pull/1362)) + +### Changed + +- Bump Instabug iOS SDK to v14.3.0 ([#1367](https://github.com/Instabug/Instabug-React-Native/pull/1367)). [See release notes](https://github.com/Instabug/Instabug-iOS/releases/tag/14.3.0). + +- Bump Instabug Android SDK to v14.3.0 ([#1375](https://github.com/Instabug/Instabug-React-Native/pull/1375)). [See release notes](https://github.com/Instabug/Instabug-Android/releases/tag/v14.3.0). + +## [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 +72,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/RNInstabug.podspec b/RNInstabug.podspec index 40c480f4c..af69112cc 100644 --- a/RNInstabug.podspec +++ b/RNInstabug.podspec @@ -12,9 +12,10 @@ Pod::Spec.new do |s| s.homepage = package["homepage"] s.source = { :git => "https://github.com/Instabug/Instabug-React-Native.git", :tag => 'v' + package["version"] } - s.platform = :ios, "9.0" + s.platform = :ios, "13.0" s.source_files = "ios/**/*.{h,m,mm}" s.dependency 'React-Core' use_instabug!(s) + end diff --git a/android/build.gradle b/android/build.gradle index 485acf84a..806266ea1 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 "15.0.1" multiDexEnabled true ndk { abiFilters "armeabi-v7a", "x86" diff --git a/android/native.gradle b/android/native.gradle index ca10ca83b..faa3246cd 100644 --- a/android/native.gradle +++ b/android/native.gradle @@ -1,5 +1,5 @@ project.ext.instabug = [ - version: '14.0.0' + version: '15.0.1' ] dependencies { diff --git a/android/src/main/java/com/instabug/reactlibrary/ArgsRegistry.java b/android/src/main/java/com/instabug/reactlibrary/ArgsRegistry.java index b56db804d..15fa45a1d 100644 --- a/android/src/main/java/com/instabug/reactlibrary/ArgsRegistry.java +++ b/android/src/main/java/com/instabug/reactlibrary/ArgsRegistry.java @@ -17,6 +17,7 @@ import com.instabug.library.invocation.util.InstabugVideoRecordingButtonPosition; import com.instabug.library.sessionreplay.model.SessionMetadata; import com.instabug.library.ui.onboarding.WelcomeMessage; +import com.instabug.library.MaskingType; import java.util.ArrayList; import java.util.HashMap; @@ -60,6 +61,8 @@ static Map getAll() { putAll(locales); putAll(placeholders); putAll(launchType); + putAll(autoMaskingTypes); + putAll(userConsentActionType); }}; } @@ -142,6 +145,12 @@ static Map getAll() { put("reproStepsDisabled", ReproMode.Disable); }}; + static final ArgsMap userConsentActionType = new ArgsMap() {{ + put("dropAutoCapturedMedia", com.instabug.bug.userConsent.ActionType.DROP_AUTO_CAPTURED_MEDIA); + put("dropLogs", com.instabug.bug.userConsent.ActionType.DROP_LOGS); + put("noChat", com.instabug.bug.userConsent.ActionType.NO_CHAT); + }}; + static final ArgsMap sdkLogLevels = new ArgsMap() {{ put("sdkDebugLogsLevelNone", com.instabug.library.LogLevel.NONE); put("sdkDebugLogsLevelError", com.instabug.library.LogLevel.ERROR); @@ -253,5 +262,10 @@ static Map getAll() { put(SessionMetadata.LaunchType.COLD,"cold"); put(SessionMetadata.LaunchType.WARM,"warm" ); }}; - + public static final ArgsMap autoMaskingTypes = new ArgsMap() {{ + put("labels", MaskingType.LABELS); + put("textInputs", MaskingType.TEXT_INPUTS); + put("media", MaskingType.MEDIA); + put("none", MaskingType.MASK_NOTHING); + }}; } diff --git a/android/src/main/java/com/instabug/reactlibrary/Constants.java b/android/src/main/java/com/instabug/reactlibrary/Constants.java index fcab68332..f5b584700 100644 --- a/android/src/main/java/com/instabug/reactlibrary/Constants.java +++ b/android/src/main/java/com/instabug/reactlibrary/Constants.java @@ -9,6 +9,12 @@ 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_FEATURES_UPDATED_CALLBACK = "IBGOnFeatureUpdatedCallback"; + final static String IBG_NETWORK_LOGGER_HANDLER = "IBGNetworkLoggerHandler"; + + final static String IBG_ON_FEATURE_FLAGS_UPDATE_RECEIVED_CALLBACK = "IBGOnNewFeatureFlagsUpdateReceivedCallback"; + final static String IBG_SESSION_REPLAY_ON_SYNC_CALLBACK_INVOCATION = "IBGSessionReplayOnSyncCallback"; } diff --git a/android/src/main/java/com/instabug/reactlibrary/RNInstabug.java b/android/src/main/java/com/instabug/reactlibrary/RNInstabug.java index 9b6348ab4..7c0901936 100644 --- a/android/src/main/java/com/instabug/reactlibrary/RNInstabug.java +++ b/android/src/main/java/com/instabug/reactlibrary/RNInstabug.java @@ -19,7 +19,8 @@ public class RNInstabug { private static RNInstabug instance; - private RNInstabug() {} + private RNInstabug() { + } public static RNInstabug getInstance() { @@ -36,14 +37,13 @@ public static RNInstabug getInstance() { /** * Initializes the SDK on the native side, which is useful for capturing startup issues specific to the native part of the app. * - * @param application The application context. + * @param application The application context. * @param applicationToken The app's identifying token, available on your dashboard. - * @param logLevel The level of detail in logs that you want to print. - *

Pick one of the log levels described in {@link LogLevel}. - * default logLevel is {@link LogLevel#ERROR}

- * @param InvocationEvent The events that trigger the SDK's user interface. - * Choose from the available events listed in {@link InstabugInvocationEvent}. - * + * @param logLevel The level of detail in logs that you want to print. + *

Pick one of the log levels described in {@link LogLevel}. + * default logLevel is {@link LogLevel#ERROR}

+ * @param InvocationEvent The events that trigger the SDK's user interface. + * Choose from the available events listed in {@link InstabugInvocationEvent}. * @example

Here's an example usage:

*
      * RNInstabug.getInstance().init(
@@ -59,17 +59,24 @@ public void init(
             @NonNull Application application,
             @NonNull String applicationToken,
             int logLevel,
+            Boolean ignoreSecureFlag,
             @NonNull InstabugInvocationEvent... InvocationEvent
-    ) {
+            ) {
         try {
 
             setBaseUrlForDeprecationLogs();
             setCurrentPlatform();
 
-            new Instabug.Builder(application, applicationToken)
+            Instabug.Builder builder = new Instabug.Builder(application, applicationToken)
                     .setInvocationEvents(InvocationEvent)
-                    .setSdkDebugLogsLevel(logLevel)
-                    .build();
+                    .setSdkDebugLogsLevel(logLevel);
+
+            if (ignoreSecureFlag != null) {
+                builder.ignoreFlagSecure(ignoreSecureFlag);
+            }
+
+            builder.build();
+
 
             // Temporarily disabling APM hot launches
             APM.setHotAppLaunchEnabled(false);
@@ -80,15 +87,13 @@ public void init(
     }
 
 
-
     /**
      * Initializes the SDK on the native side, which is useful for capturing startup issues specific to the native part of the app.
      *
-     * @param application The application context.
+     * @param application      The application context.
      * @param applicationToken The app's identifying token, available on your dashboard.
-     * @param invocationEvent The events that trigger the SDK's user interface.
-     *      Choose from the available events listed in {@link InstabugInvocationEvent}.
-     *
+     * @param invocationEvent  The events that trigger the SDK's user interface.
+     *                         Choose from the available events listed in {@link InstabugInvocationEvent}.
      * @example 

Here's an example usage:

*
      * RNInstabug.getInstance().init(
@@ -104,7 +109,7 @@ public void init(
             @NonNull String applicationToken,
             @NonNull InstabugInvocationEvent... invocationEvent
     ) {
-        init(application, applicationToken, LogLevel.ERROR, invocationEvent);
+        init(application, applicationToken, LogLevel.ERROR,null, invocationEvent);
     }
 
     @VisibleForTesting
@@ -160,6 +165,7 @@ public static class Builder {
          * The events that trigger the SDK's user interface.
          */
         private InstabugInvocationEvent[] invocationEvents;
+        private Boolean ignoreFlagSecure;
 
 
         /**
@@ -210,6 +216,16 @@ public Builder setCodePushVersion(String codePushVersion) {
             return this;
         }
 
+        /**
+         * Sets flag to override SDK screenshot security behavior.
+         *
+         * @param ignoreFlagSecure flag to override SDK screenshot security behavior.
+         */
+        public Builder ignoreFlagSecure(boolean ignoreFlagSecure) {
+            this.ignoreFlagSecure = ignoreFlagSecure;
+            return this;
+        }
+
         /**
          * Sets the invocation triggering events for the SDK's user interface
          *
@@ -237,6 +253,10 @@ public void build() {
                     instabugBuilder.setCodePushVersion(codePushVersion);
                 }
 
+                if (ignoreFlagSecure != null) {
+                    instabugBuilder.ignoreFlagSecure(ignoreFlagSecure);
+                }
+
                 instabugBuilder.build();
 
                 // Temporarily disabling APM hot launches
diff --git a/android/src/main/java/com/instabug/reactlibrary/RNInstabugAPMModule.java b/android/src/main/java/com/instabug/reactlibrary/RNInstabugAPMModule.java
index e2ff10b99..d75b4f75b 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);
@@ -44,7 +43,7 @@ public String getName() {
     }
 
     /**
-     * Sets the printed logs priority. Filter to one of the following levels.
+     * Pauses the current thread for 3 seconds.
      */
     @ReactMethod
     public void ibgSleep() {
@@ -58,6 +57,7 @@ public void run() {
 
     /**
      * Enables or disables APM.
+     *
      * @param isEnabled boolean indicating enabled or disabled.
      */
     @ReactMethod
@@ -76,6 +76,7 @@ public void run() {
 
     /**
      * Enables or disables app launch tracking.
+     *
      * @param isEnabled boolean indicating enabled or disabled.
      */
     @ReactMethod
@@ -93,7 +94,7 @@ public void run() {
     }
 
     /**
-     * Ends app launch
+     * This method is used to signal the end of the app launch process.
      */
     @ReactMethod
     public void endAppLaunch() {
@@ -111,6 +112,7 @@ public void run() {
 
     /**
      * Enables or disables auto UI tracing
+     *
      * @param isEnabled boolean indicating enabled or disabled.
      */
     @ReactMethod
@@ -282,6 +284,7 @@ public void run() {
 
     /**
      * Starts a UI trace
+     *
      * @param name string name of the UI trace.
      */
     @ReactMethod
@@ -299,7 +302,7 @@ public void run() {
     }
 
     /**
-     * Ends the current running UI trace
+     * This method is used to terminate the currently active UI trace.
      */
     @ReactMethod
     public void endUITrace() {
@@ -315,6 +318,73 @@ public void run() {
         });
     }
 
+ /**
+  * The `networkLogAndroid` function logs network-related information using APMNetworkLogger in a React
+  * Native module.
+  *
+  * @param requestStartTime The `requestStartTime` parameter in the `networkLogAndroid` method
+  * represents the timestamp when the network request started. It is of type `double` and is passed as
+  * a parameter to log network-related information.
+  * @param requestDuration The `requestDuration` parameter in the `networkLogAndroid` method represents
+  * the duration of the network request in milliseconds. It indicates the time taken for the request to
+  * complete from the moment it was initiated until the response was received. This parameter helps in
+  * measuring the performance of network requests and identifying any potential
+  * @param requestHeaders requestHeaders is a string parameter that contains the headers of the network
+  * request. It typically includes information such as the content type, authorization token, and any
+  * other headers that were sent with the request.
+  * @param requestBody The `requestBody` parameter in the `networkLogAndroid` method represents the
+  * body of the HTTP request being logged. It contains the data that is sent as part of the request to
+  * the server. This could include form data, JSON payload, XML data, or any other content that is
+  * being transmitted
+  * @param requestBodySize The `requestBodySize` parameter in the `networkLogAndroid` method represents
+  * the size of the request body in bytes. It is a double value that indicates the size of the request
+  * body being sent in the network request. This parameter is used to log information related to the
+  * network request, including details
+  * @param requestMethod The `requestMethod` parameter in the `networkLogAndroid` method represents the
+  * HTTP method used in the network request, such as GET, POST, PUT, DELETE, etc. It indicates the type
+  * of operation that the client is requesting from the server.
+  * @param requestUrl The `requestUrl` parameter in the `networkLogAndroid` method represents the URL
+  * of the network request being logged. It typically contains the address of the server to which the
+  * request is being made, along with any additional path or query parameters required for the request.
+  * This URL is essential for identifying the
+  * @param requestContentType The `requestContentType` parameter in the `networkLogAndroid` method
+  * represents the content type of the request being made. This could be values like
+  * "application/json", "application/xml", "text/plain", etc., indicating the format of the data being
+  * sent in the request body. It helps in specifying
+  * @param responseHeaders The `responseHeaders` parameter in the `networkLogAndroid` method represents
+  * the headers of the response received from a network request. These headers typically include
+  * information such as content type, content length, server information, and any other metadata
+  * related to the response. The `responseHeaders` parameter is expected to
+  * @param responseBody The `responseBody` parameter in the `networkLogAndroid` method represents the
+  * body of the response received from a network request. It contains the data or content sent back by
+  * the server in response to the request made by the client. This could be in various formats such as
+  * JSON, XML, HTML
+  * @param responseBodySize The `responseBodySize` parameter in the `networkLogAndroid` method
+  * represents the size of the response body in bytes. It is a double value that indicates the size of
+  * the response body received from the network request. This parameter is used to log information
+  * related to the network request and response, including
+  * @param statusCode The `statusCode` parameter in the `networkLogAndroid` method represents the HTTP
+  * status code of the network request/response. It indicates the status of the HTTP response, such as
+  * success (200), redirection (3xx), client errors (4xx), or server errors (5xx). This parameter is
+  * @param responseContentType The `responseContentType` parameter in the `networkLogAndroid` method
+  * represents the content type of the response received from the network request. It indicates the
+  * format of the data in the response, such as JSON, XML, HTML, etc. This information is useful for
+  * understanding how to parse and handle the
+  * @param errorDomain The `errorDomain` parameter in the `networkLogAndroid` method is used to specify
+  * the domain of an error, if any occurred during the network request. If there was no error, this
+  * parameter will be `null`.
+  * @param w3cAttributes The `w3cAttributes` parameter in the `networkLogAndroid` method is a
+  * ReadableMap object that contains additional attributes related to W3C external trace. It may
+  * include the following key-value pairs:
+  * @param gqlQueryName The `gqlQueryName` parameter in the `networkLogAndroid` method represents the
+  * name of the GraphQL query being executed. It is a nullable parameter, meaning it can be null if no
+  * GraphQL query name is provided. This parameter is used to log information related to GraphQL
+  * queries in the network logging
+  * @param serverErrorMessage The `serverErrorMessage` parameter in the `networkLogAndroid` method is
+  * used to pass any error message received from the server during network communication. This message
+  * can provide additional details about any errors that occurred on the server side, helping in
+  * debugging and troubleshooting network-related issues.
+  */
     @ReactMethod
     private void networkLogAndroid(final double requestStartTime,
                                    final double requestDuration,
@@ -330,14 +400,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 +456,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/RNInstabugBugReportingModule.java b/android/src/main/java/com/instabug/reactlibrary/RNInstabugBugReportingModule.java
index 1f0b6ddac..0dd9270e0 100644
--- a/android/src/main/java/com/instabug/reactlibrary/RNInstabugBugReportingModule.java
+++ b/android/src/main/java/com/instabug/reactlibrary/RNInstabugBugReportingModule.java
@@ -2,6 +2,7 @@
 
 import android.annotation.SuppressLint;
 import android.annotation.TargetApi;
+import androidx.annotation.Nullable;
 
 import com.facebook.react.bridge.Arguments;
 import com.facebook.react.bridge.Callback;
@@ -21,6 +22,7 @@
 import com.instabug.reactlibrary.utils.ArrayUtil;
 import com.instabug.reactlibrary.utils.EventEmitterModule;
 import com.instabug.reactlibrary.utils.MainThreadHandler;
+import com.instabug.bug.userConsent.ActionType;
 
 import java.util.ArrayList;
 
@@ -415,4 +417,30 @@ public void run() {
             }
         });
     }
+
+    /**
+    * Adds a user consent item to the bug reporting
+    * @param key A unique identifier string for the consent item.
+    * @param description The text shown to the user describing the consent item.
+    * @param mandatory Whether the user must agree to this item before submitting a report.
+    * @param checked Whether the consent checkbox is pre-selected.
+    * @param actionType A string representing the action type to map to SDK behavior.
+    */
+    @ReactMethod
+    public void addUserConsent(String key, String description, boolean mandatory, boolean checked, @Nullable String actionType) {
+        MainThreadHandler.runOnMainThread(new Runnable() {
+            @Override
+            public void run() {
+            try {
+            String mappedActionType = ArgsRegistry.userConsentActionType.get(actionType);
+        BugReporting.addUserConsent(key, description, mandatory, checked, mappedActionType);
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+        }
+    });
+    
+    }
+    
+
 }
diff --git a/android/src/main/java/com/instabug/reactlibrary/RNInstabugNetworkLoggerModule.java b/android/src/main/java/com/instabug/reactlibrary/RNInstabugNetworkLoggerModule.java
new file mode 100644
index 000000000..a47cc7e21
--- /dev/null
+++ b/android/src/main/java/com/instabug/reactlibrary/RNInstabugNetworkLoggerModule.java
@@ -0,0 +1,195 @@
+package com.instabug.reactlibrary;
+
+
+import static com.instabug.apm.configuration.cp.APMFeature.APM_NETWORK_PLUGIN_INSTALLED;
+import static com.instabug.apm.configuration.cp.APMFeature.CP_NATIVE_INTERCEPTION_ENABLED;
+
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import com.facebook.react.bridge.Arguments;
+import com.facebook.react.bridge.Promise;
+import com.facebook.react.bridge.ReactApplicationContext;
+import com.facebook.react.bridge.ReactMethod;
+import com.facebook.react.bridge.ReadableMap;
+import com.facebook.react.bridge.ReadableMapKeySetIterator;
+import com.facebook.react.bridge.WritableMap;
+import com.facebook.react.bridge.WritableNativeMap;
+import com.instabug.apm.InternalAPM;
+import com.instabug.apm.sanitization.OnCompleteCallback;
+import com.instabug.library.logging.listeners.networklogs.NetworkLogSnapshot;
+import com.instabug.reactlibrary.utils.EventEmitterModule;
+import com.instabug.reactlibrary.utils.MainThreadHandler;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+
+public class RNInstabugNetworkLoggerModule extends EventEmitterModule {
+
+    public final ConcurrentHashMap> callbackMap = new ConcurrentHashMap>();
+
+    public RNInstabugNetworkLoggerModule(ReactApplicationContext reactContext) {
+        super(reactContext);
+    }
+
+
+    @NonNull
+    @Override
+    public String getName() {
+        return "IBGNetworkLogger";
+    }
+
+
+    @ReactMethod
+    public void addListener(String event) {
+        super.addListener(event);
+    }
+
+    @ReactMethod
+    public void removeListeners(Integer count) {
+        super.removeListeners(count);
+    }
+
+    private boolean getFlagValue(String key) {
+        return InternalAPM._isFeatureEnabledCP(key, "");
+    }
+
+    private WritableMap convertFromMapToWritableMap(Map map) {
+        WritableMap writableMap = new WritableNativeMap();
+        for (String key : map.keySet()) {
+            Object value = map.get(key);
+            writableMap.putString(key, (String) value);
+        }
+        return writableMap;
+    }
+
+    private Map convertReadableMapToMap(ReadableMap readableMap) {
+        Map map = new HashMap<>();
+        if (readableMap != null) {
+            ReadableMapKeySetIterator iterator = readableMap.keySetIterator();
+            while (iterator.hasNextKey()) {
+                String key = iterator.nextKey();
+                map.put(key, readableMap.getString(key));
+            }
+        }
+        return map;
+    }
+
+    /**
+     * Get first time Value of [cp_native_interception_enabled] flag
+     */
+    @ReactMethod
+    public void isNativeInterceptionEnabled(Promise promise) {
+        MainThreadHandler.runOnMainThread(new Runnable() {
+            @Override
+            public void run() {
+                try {
+                    promise.resolve(getFlagValue(CP_NATIVE_INTERCEPTION_ENABLED));
+                } catch (Exception e) {
+                    e.printStackTrace();
+                    promise.resolve(false); // Will rollback to JS interceptor
+                }
+
+            }
+        });
+    }
+
+    /**
+     * Indicate if user added APM Network plugin or not
+     * [true] means user added the APM plugin
+     * [false] means not
+     */
+    @ReactMethod
+    public void hasAPMNetworkPlugin(Promise promise) {
+        MainThreadHandler.runOnMainThread(new Runnable() {
+            @Override
+            public void run() {
+                try {
+                    promise.resolve(getFlagValue(APM_NETWORK_PLUGIN_INSTALLED));
+                } catch (Exception e) {
+                    e.printStackTrace();
+                    promise.resolve(false);  // Will rollback to JS interceptor
+                }
+
+            }
+        });
+    }
+
+
+    @ReactMethod
+    public void registerNetworkLogsListener() {
+        MainThreadHandler.runOnMainThread(new Runnable() {
+            @Override
+            public void run() {
+                InternalAPM._registerNetworkLogSanitizer((networkLogSnapshot, onCompleteCallback) -> {
+                    final String id = String.valueOf(onCompleteCallback.hashCode());
+                    callbackMap.put(id, onCompleteCallback);
+
+                    WritableMap networkSnapshotParams = Arguments.createMap();
+                    networkSnapshotParams.putString("id", id);
+                    networkSnapshotParams.putString("url", networkLogSnapshot.getUrl());
+                    networkSnapshotParams.putInt("responseCode", networkLogSnapshot.getResponseCode());
+                    networkSnapshotParams.putString("requestBody", networkLogSnapshot.getRequestBody());
+                    networkSnapshotParams.putString("response", networkLogSnapshot.getResponse());
+                    final Map requestHeaders = networkLogSnapshot.getRequestHeaders();
+                    if (requestHeaders != null) {
+                        networkSnapshotParams.putMap("requestHeader", convertFromMapToWritableMap(requestHeaders));
+                    }
+                    final Map responseHeaders = networkLogSnapshot.getResponseHeaders();
+                    if (responseHeaders != null) {
+                        networkSnapshotParams.putMap("responseHeader", convertFromMapToWritableMap(responseHeaders));
+                    }
+
+                    sendEvent(Constants.IBG_NETWORK_LOGGER_HANDLER, networkSnapshotParams);
+                });
+            }
+        });
+    }
+
+    @ReactMethod
+    public void resetNetworkLogsListener() {
+        MainThreadHandler.runOnMainThread(new Runnable() {
+            @Override
+            public void run() {
+                InternalAPM._registerNetworkLogSanitizer(null);
+            }
+        });
+    }
+
+    @ReactMethod
+    public void updateNetworkLogSnapshot(
+            String url,
+            String callbackID,
+            String requestBody,
+            String responseBody,
+            int responseCode,
+            ReadableMap requestHeaders,
+            ReadableMap responseHeaders
+    ) {
+        try {
+            // Convert ReadableMap to a Java Map for easier handling
+            Map requestHeadersMap = convertReadableMapToMap(requestHeaders);
+            Map responseHeadersMap = convertReadableMapToMap(responseHeaders);
+
+            NetworkLogSnapshot modifiedSnapshot = null;
+            if (!url.isEmpty()) {
+                modifiedSnapshot = new NetworkLogSnapshot(url, requestHeadersMap, requestBody, responseHeadersMap, responseBody, responseCode);
+            }
+
+            final OnCompleteCallback callback = callbackMap.get(callbackID);
+            if (callback != null) {
+                callback.onComplete(modifiedSnapshot);
+                callbackMap.remove(callbackID);
+            }
+        } catch (Exception e) {
+            // Reject the promise to indicate an error occurred
+            Log.e("IB-CP-Bridge", "InstabugNetworkLogger.updateNetworkLogSnapshot failed to parse the network snapshot object.");
+        }
+    }
+}
diff --git a/android/src/main/java/com/instabug/reactlibrary/RNInstabugReactnativeModule.java b/android/src/main/java/com/instabug/reactlibrary/RNInstabugReactnativeModule.java
index f936eaa12..21bcf4f44 100644
--- a/android/src/main/java/com/instabug/reactlibrary/RNInstabugReactnativeModule.java
+++ b/android/src/main/java/com/instabug/reactlibrary/RNInstabugReactnativeModule.java
@@ -1,5 +1,7 @@
 package com.instabug.reactlibrary;
 
+import static com.instabug.apm.configuration.cp.APMFeature.APM_NETWORK_PLUGIN_INSTALLED;
+import static com.instabug.apm.configuration.cp.APMFeature.CP_NATIVE_INTERCEPTION_ENABLED;
 import static com.instabug.reactlibrary.utils.InstabugUtil.getMethod;
 
 import android.app.Application;
@@ -8,6 +10,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 +25,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,7 +35,13 @@
 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.internal.crossplatform.InternalCore;
+import com.instabug.library.internal.crossplatform.OnFeaturesUpdatedListener;
 import com.instabug.library.internal.module.InstabugLocale;
 import com.instabug.library.invocation.InstabugInvocationEvent;
 import com.instabug.library.logging.InstabugLog;
@@ -112,7 +123,7 @@ public void setEnabled(final boolean isEnabled) {
             @Override
             public void run() {
                 try {
-                    if(isEnabled)
+                    if (isEnabled)
                         Instabug.enable();
                     else
                         Instabug.disable();
@@ -125,10 +136,11 @@ public void run() {
 
     /**
      * Initializes the SDK.
-     * @param token The token that identifies the app. You can find it on your dashboard.
+     *
+     * @param token                 The token that identifies the app. You can find it on your dashboard.
      * @param invocationEventValues The events that invoke the SDK's UI.
-     * @param logLevel The level of detail in logs that you want to print.
-     * @param codePushVersion The Code Push version to be used for all reports.
+     * @param logLevel              The level of detail in logs that you want to print.
+     * @param codePushVersion       The Code Push version to be used for all reports.
      */
     @ReactMethod
     public void init(
@@ -136,7 +148,8 @@ public void init(
             final ReadableArray invocationEventValues,
             final String logLevel,
             final boolean useNativeNetworkInterception,
-            @Nullable final String codePushVersion
+            @Nullable final String codePushVersion,
+            final ReadableMap map
     ) {
         MainThreadHandler.runOnMainThread(new Runnable() {
             @Override
@@ -154,8 +167,12 @@ public void run() {
                         .setInvocationEvents(invocationEvents)
                         .setLogLevel(parsedLogLevel);
 
-                if(codePushVersion != null) {
-                    if(Instabug.isBuilt()) {
+                if (map!=null&&map.hasKey("ignoreAndroidSecureFlag")) {
+                    builder.ignoreFlagSecure(map.getBoolean("ignoreAndroidSecureFlag"));
+                }
+
+                if (codePushVersion != null) {
+                    if (Instabug.isBuilt()) {
                         Instabug.setCodePushVersion(codePushVersion);
                     } else {
                         builder.setCodePushVersion(codePushVersion);
@@ -321,7 +338,7 @@ public void run() {
      *
      * @param userEmail User's default email
      * @param userName  Username.
-     * @param userId User's ID
+     * @param userId    User's ID
      */
     @ReactMethod
     public void identifyUser(
@@ -741,15 +758,15 @@ public void addFileAttachmentWithDataToReport(String data, String fileName) {
 
     private WritableMap convertFromHashMapToWriteableMap(HashMap hashMap) {
         WritableMap writableMap = new WritableNativeMap();
-        for(int i = 0; i < hashMap.size(); i++) {
+        for (int i = 0; i < hashMap.size(); i++) {
             Object key = hashMap.keySet().toArray()[i];
             Object value = hashMap.get(key);
-            writableMap.putString((String) key,(String) value);
+            writableMap.putString((String) key, (String) value);
         }
         return writableMap;
     }
 
-    private static JSONObject objectToJSONObject(Object object){
+    private static JSONObject objectToJSONObject(Object object) {
         Object json = null;
         JSONObject jsonObject = null;
         try {
@@ -766,13 +783,12 @@ private static JSONObject objectToJSONObject(Object object){
     private WritableArray convertArrayListToWritableArray(List arrayList) {
         WritableArray writableArray = new WritableNativeArray();
 
-        for(int i = 0; i < arrayList.size(); i++) {
+        for (int i = 0; i < arrayList.size(); i++) {
             Object object = arrayList.get(i);
 
-            if(object instanceof String) {
+            if (object instanceof String) {
                 writableArray.pushString((String) object);
-            }
-            else {
+            } else {
                 JSONObject jsonObject = objectToJSONObject(object);
                 writableArray.pushMap((WritableMap) jsonObject);
             }
@@ -828,7 +844,7 @@ public void run() {
      * Shows the welcome message in a specific mode.
      *
      * @param welcomeMessageMode An enum to set the welcome message mode to
-      *                          live, or beta.
+     *                           live, or beta.
      */
     @ReactMethod
     public void showWelcomeMessageWithMode(final String welcomeMessageMode) {
@@ -850,7 +866,7 @@ public void run() {
      * Sets the welcome message mode to live, beta or disabled.
      *
      * @param welcomeMessageMode An enum to set the welcome message mode to
-      *                          live, beta or disabled.
+     *                           live, beta or disabled.
      */
     @ReactMethod
     public void setWelcomeMessageMode(final String welcomeMessageMode) {
@@ -985,7 +1001,6 @@ public void run() {
      * Reports that the screen name been changed (Current View).
      *
      * @param screenName string containing the screen name
-     *
      */
     @ReactMethod
     public void reportCurrentViewChange(final String screenName) {
@@ -1008,7 +1023,6 @@ public void run() {
      * Reports that the screen has been changed (Repro Steps) the screen sent to this method will be the 'current view' on the dashboard
      *
      * @param screenName string containing the screen name
-     *
      */
     @ReactMethod
     public void reportScreenChange(final String screenName) {
@@ -1018,7 +1032,7 @@ public void run() {
                 try {
                     Method method = getMethod(Class.forName("com.instabug.library.Instabug"), "reportScreenChange", Bitmap.class, String.class);
                     if (method != null) {
-                        method.invoke(null , null, screenName);
+                        method.invoke(null, null, screenName);
                     }
                 } catch (Exception e) {
                     e.printStackTrace();
@@ -1112,7 +1126,7 @@ public void removeFeatureFlags(final ReadableArray featureFlags) {
             @Override
             public void run() {
                 try {
-                   ArrayList stringArray = ArrayUtil.parseReadableArrayOfStrings(featureFlags);
+                    ArrayList stringArray = ArrayUtil.parseReadableArrayOfStrings(featureFlags);
                     Instabug.removeFeatureFlag(stringArray);
                 } catch (Exception e) {
                     e.printStackTrace();
@@ -1149,6 +1163,103 @@ public void run() {
         });
     }
 
+    /**
+     * Register a listener for feature flags value change
+     */
+    @ReactMethod
+    public void registerFeatureFlagsChangeListener() {
+
+        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());
+                            params.putInt("networkBodyLimit",featuresState.getNetworkLogCharLimit());
+
+                            sendEvent(Constants.IBG_ON_FEATURE_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}.
      * The constant name and the arg key should match to be able to resolve the
@@ -1167,4 +1278,82 @@ public Map getConstants() {
 
         return constants;
     }
+
+
+    @ReactMethod
+    public void setOnFeaturesUpdatedListener() {
+        InternalCore.INSTANCE._setOnFeaturesUpdatedListener(new OnFeaturesUpdatedListener() {
+            @Override
+            public void invoke() {
+                final boolean cpNativeInterceptionEnabled = InternalAPM._isFeatureEnabledCP(CP_NATIVE_INTERCEPTION_ENABLED, "");
+                final boolean hasAPMPlugin = InternalAPM._isFeatureEnabledCP(APM_NETWORK_PLUGIN_INSTALLED, "");
+
+                WritableMap params = Arguments.createMap();
+                params.putBoolean("cpNativeInterceptionEnabled", cpNativeInterceptionEnabled);
+                params.putBoolean("hasAPMPlugin", hasAPMPlugin);
+                sendEvent(Constants.IBG_ON_FEATURES_UPDATED_CALLBACK, params);
+            }
+        });
+    }
+    /**
+    * Enables or disables capturing network body.
+    * @param isEnabled A boolean to enable/disable capturing network body.
+    */
+   @ReactMethod
+   public void setNetworkLogBodyEnabled(final boolean isEnabled) {
+       MainThreadHandler.runOnMainThread(new Runnable() {
+           @Override
+           public void run() {
+               try {
+                   Instabug.setNetworkLogBodyEnabled(isEnabled);
+               } catch (Exception e) {
+                   e.printStackTrace();
+               }
+           }
+       });
+   }
+
+    /**
+     * Sets the auto mask screenshots types.
+     *
+     * @param autoMaskingTypes The masking type to be applied.
+     */
+    @ReactMethod
+    public void enableAutoMasking(@NonNull ReadableArray autoMaskingTypes) {
+        MainThreadHandler.runOnMainThread(new Runnable() {
+
+            @Override
+            public void run() {
+                int[] autoMassingTypesArray = new int[autoMaskingTypes.size()];
+                for (int i = 0; i < autoMaskingTypes.size(); i++) {
+                    String key = autoMaskingTypes.getString(i);
+
+                    autoMassingTypesArray[i] = ArgsRegistry.autoMaskingTypes.get(key);
+
+                }
+
+                Instabug.setAutoMaskScreenshotsTypes(autoMassingTypesArray);
+            }
+
+        });
+    }
+
+    /**
+     * Get network body size limit
+     */
+    @ReactMethod
+    public void getNetworkBodyMaxSize(Promise promise) {
+
+        MainThreadHandler.runOnMainThread(new Runnable() {
+            @Override
+            public void run() {
+                try {
+                    promise.resolve(InternalCore.INSTANCE.get_networkLogCharLimit());
+                } catch (Exception e) {
+                    e.printStackTrace();
+                    promise.resolve(false);
+                }
+            }
+        });
+    }
 }
diff --git a/android/src/main/java/com/instabug/reactlibrary/RNInstabugReactnativePackage.java b/android/src/main/java/com/instabug/reactlibrary/RNInstabugReactnativePackage.java
index 1d3828725..0cabd1bcf 100644
--- a/android/src/main/java/com/instabug/reactlibrary/RNInstabugReactnativePackage.java
+++ b/android/src/main/java/com/instabug/reactlibrary/RNInstabugReactnativePackage.java
@@ -29,6 +29,7 @@ public List createNativeModules(@NonNull ReactApplicationContext r
         modules.add(new RNInstabugRepliesModule(reactContext));
         modules.add(new RNInstabugAPMModule(reactContext));
         modules.add(new RNInstabugSessionReplayModule(reactContext));
+        modules.add(new RNInstabugNetworkLoggerModule(reactContext));
         return modules;
     }
 
diff --git a/android/src/test/java/com/instabug/reactlibrary/RNInstabugAPMModuleTest.java b/android/src/test/java/com/instabug/reactlibrary/RNInstabugAPMModuleTest.java
index 5045f929e..85ca1384d 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/RNInstabugBugReportingModuleTest.java b/android/src/test/java/com/instabug/reactlibrary/RNInstabugBugReportingModuleTest.java
index 2a96c389b..dc55e81a5 100644
--- a/android/src/test/java/com/instabug/reactlibrary/RNInstabugBugReportingModuleTest.java
+++ b/android/src/test/java/com/instabug/reactlibrary/RNInstabugBugReportingModuleTest.java
@@ -362,4 +362,22 @@ public Object answer(InvocationOnMock invocation) {
         
         BugReporting.setCommentMinimumCharacterCount(count, type1);
     }
+    @Test
+    public void TestAddUserConsent() {
+               final Map args = ArgsRegistry.userConsentActionType;
+               final String[] keysArray = args.keySet().toArray(new String[0]);
+       
+               final String key = "testKey";
+               final String description = "Consent description";
+               final boolean mandatory = true;
+               final boolean checked = true;
+               final String inputAction = keysArray[0];
+       
+               final String expectedMappedAction = args.get(inputAction);
+       
+               bugReportingModule.addUserConsent(key, description, mandatory, checked, inputAction);
+           
+               verify(BugReporting.class, VerificationModeFactory.times(1));
+               BugReporting.addUserConsent(key, description, mandatory, checked, expectedMappedAction);
+    }
 }
diff --git a/android/src/test/java/com/instabug/reactlibrary/RNInstabugNetworkLoggerModuleTest.java b/android/src/test/java/com/instabug/reactlibrary/RNInstabugNetworkLoggerModuleTest.java
new file mode 100644
index 000000000..30cec00ab
--- /dev/null
+++ b/android/src/test/java/com/instabug/reactlibrary/RNInstabugNetworkLoggerModuleTest.java
@@ -0,0 +1,207 @@
+package com.instabug.reactlibrary;
+
+import static com.instabug.apm.configuration.cp.APMFeature.APM_NETWORK_PLUGIN_INSTALLED;
+import static com.instabug.apm.configuration.cp.APMFeature.CP_NATIVE_INTERCEPTION_ENABLED;
+import static org.mockito.Mockito.*;
+import static org.junit.Assert.*;
+
+import android.os.Looper;
+
+import com.facebook.react.bridge.Promise;
+import com.facebook.react.bridge.ReactApplicationContext;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+import org.mockito.stubbing.Answer;
+
+import com.instabug.apm.InternalAPM;
+import com.instabug.reactlibrary.utils.MainThreadHandler;
+
+public class RNInstabugNetworkLoggerModuleTest {
+
+    // Mock Objects
+    private MockedStatic mockLooper;
+    private MockedStatic mockMainThreadHandler;
+    private RNInstabugNetworkLoggerModule rnInstabugNetworkLoggerModule;
+    private Promise mockPromise;
+
+    @Before
+    public void mockMainThreadHandler() {
+        // Mock Object
+        ReactApplicationContext mockReactApplicationContext = mock(ReactApplicationContext.class);
+        mockPromise = mock(Promise.class);
+        rnInstabugNetworkLoggerModule = new RNInstabugNetworkLoggerModule(mockReactApplicationContext);
+
+        // Mock static functions
+        mockLooper = mockStatic(Looper.class);
+        mockMainThreadHandler = mockStatic(MainThreadHandler.class);
+        // Mock Looper class
+        Looper mockMainThreadLooper = mock(Looper.class);
+        when(Looper.getMainLooper()).thenReturn(mockMainThreadLooper);
+
+        // Override runOnMainThread
+        Answer handlerPostAnswer = invocation -> {
+            invocation.getArgument(0, Runnable.class).run();
+            return true;
+        };
+        Mockito.doAnswer(handlerPostAnswer).when(MainThreadHandler.class);
+        MainThreadHandler.runOnMainThread(any(Runnable.class));
+    }
+
+    @After
+    public void tearDown() {
+        // Remove static mocks
+        mockLooper.close();
+        mockMainThreadHandler.close();
+    }
+
+
+    @Test
+    public void testGetName() {
+        // Test the getName method
+        String name = rnInstabugNetworkLoggerModule.getName();
+        assertEquals("IBGNetworkLogger", name);
+    }
+
+    @Test
+    public void testAddListener() {
+        // Test addListener method
+        rnInstabugNetworkLoggerModule.addListener("event_name");
+        // Nothing to assert, but check no exceptions are thrown
+    }
+
+    @Test
+    public void testRemoveListeners() {
+        // Test removeListeners method
+        rnInstabugNetworkLoggerModule.removeListeners(1);
+        // Nothing to assert, but check no exceptions are thrown
+    }
+
+    @Test
+    public void testIsNativeInterceptionEnabled_True() {
+
+        // Mock InternalAPM behavior within the scope of this test
+        try (MockedStatic internalAPMMock = mockStatic(InternalAPM.class)) {
+            internalAPMMock.when(() -> InternalAPM._isFeatureEnabledCP(CP_NATIVE_INTERCEPTION_ENABLED, "")).thenReturn(true);
+
+            // Execute the method
+            rnInstabugNetworkLoggerModule.isNativeInterceptionEnabled(mockPromise);
+
+            // Capture the Promise.resolve() call
+            ArgumentCaptor captor = ArgumentCaptor.forClass(Boolean.class);
+            verify(mockPromise).resolve(captor.capture());
+
+            // Assert that true was passed to resolve
+            internalAPMMock.verify(() -> InternalAPM._isFeatureEnabledCP(CP_NATIVE_INTERCEPTION_ENABLED, ""));
+            assertTrue(captor.getValue());
+        }
+    }
+
+    @Test
+    public void testIsNativeInterceptionEnabled_False() {
+
+        try (MockedStatic internalAPMMock = mockStatic(InternalAPM.class)) {
+            internalAPMMock.when(() -> InternalAPM._isFeatureEnabledCP(CP_NATIVE_INTERCEPTION_ENABLED, "")).thenReturn(false);
+
+            // Execute the method
+            rnInstabugNetworkLoggerModule.isNativeInterceptionEnabled(mockPromise);
+
+            // Capture the Promise.resolve() call
+            ArgumentCaptor captor = ArgumentCaptor.forClass(Boolean.class);
+            verify(mockPromise).resolve(captor.capture());
+
+            // Assert that false was passed to resolve
+            assertFalse(captor.getValue());
+        }
+    }
+
+    @Test
+    public void testIsNativeInterceptionEnabled_Exception() {
+
+        // Simulate an exception in InternalAPM
+        try (MockedStatic internalAPMMock = mockStatic(InternalAPM.class)) {
+            internalAPMMock.when(() -> InternalAPM._isFeatureEnabledCP(anyString(), anyString())).thenThrow(new RuntimeException("Error"));
+
+            // Execute the method
+            rnInstabugNetworkLoggerModule.isNativeInterceptionEnabled(mockPromise);
+
+            // Capture the Promise.resolve() call in case of an exception
+            ArgumentCaptor captor = ArgumentCaptor.forClass(Boolean.class);
+            verify(mockPromise).resolve(captor.capture());
+
+            // Assert that false was passed to resolve when exception occurs
+            assertFalse(captor.getValue());
+        }
+    }
+
+    @Test
+    public void testHasAPMNetworkPlugin_True() {
+
+        try (MockedStatic internalAPMMock = mockStatic(InternalAPM.class)) {
+            internalAPMMock.when(() -> InternalAPM._isFeatureEnabledCP(APM_NETWORK_PLUGIN_INSTALLED, "")).thenReturn(true);
+
+            // Execute the method
+            rnInstabugNetworkLoggerModule.hasAPMNetworkPlugin(mockPromise);
+
+            // Capture the Promise.resolve() call
+            ArgumentCaptor captor = ArgumentCaptor.forClass(Boolean.class);
+            verify(mockPromise).resolve(captor.capture());
+
+            // Assert that true was passed to resolve
+            internalAPMMock.verify(() -> InternalAPM._isFeatureEnabledCP(APM_NETWORK_PLUGIN_INSTALLED, ""));
+            assertTrue(captor.getValue());
+        }
+    }
+
+    @Test
+    public void testHasAPMNetworkPlugin_False() {
+
+        try (MockedStatic internalAPMMock = mockStatic(InternalAPM.class)) {
+            internalAPMMock.when(() -> InternalAPM._isFeatureEnabledCP(APM_NETWORK_PLUGIN_INSTALLED, "")).thenReturn(false);
+
+            // Execute the method
+            rnInstabugNetworkLoggerModule.hasAPMNetworkPlugin(mockPromise);
+
+            // Capture the Promise.resolve() call
+            ArgumentCaptor captor = ArgumentCaptor.forClass(Boolean.class);
+            verify(mockPromise).resolve(captor.capture());
+
+            // Assert that false was passed to resolve
+            assertFalse(captor.getValue());
+        }
+    }
+
+    @Test
+    public void testHasAPMNetworkPlugin_Exception() {
+
+        // Simulate an exception in InternalAPM
+        try (MockedStatic internalAPMMock = mockStatic(InternalAPM.class)) {
+            internalAPMMock.when(() -> InternalAPM._isFeatureEnabledCP(anyString(), anyString())).thenThrow(new RuntimeException("Error"));
+
+            // Execute the method
+            rnInstabugNetworkLoggerModule.hasAPMNetworkPlugin(mockPromise);
+
+            // Capture the Promise.resolve() call in case of an exception
+            ArgumentCaptor captor = ArgumentCaptor.forClass(Boolean.class);
+            verify(mockPromise).resolve(captor.capture());
+
+            // Assert that false was passed to resolve when exception occurs
+            assertFalse(captor.getValue());
+        }
+    }
+
+    @Test
+    public void testRegisterNetworkLogsListenerCalled() {
+        try (MockedStatic internalAPMMock = mockStatic(InternalAPM.class)) {
+            // Run the method
+            rnInstabugNetworkLoggerModule.registerNetworkLogsListener();
+
+            // Verify the sanitizer was registered
+            internalAPMMock.verify(() -> InternalAPM._registerNetworkLogSanitizer(any()));
+        }
+    }
+}
diff --git a/android/src/test/java/com/instabug/reactlibrary/RNInstabugReactnativeModuleTest.java b/android/src/test/java/com/instabug/reactlibrary/RNInstabugReactnativeModuleTest.java
index b9bf2308c..f4f6f9bc1 100644
--- a/android/src/test/java/com/instabug/reactlibrary/RNInstabugReactnativeModuleTest.java
+++ b/android/src/test/java/com/instabug/reactlibrary/RNInstabugReactnativeModuleTest.java
@@ -18,10 +18,14 @@
 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;
 import com.instabug.reactlibrary.utils.MainThreadHandler;
+import com.instabug.library.MaskingType;
 
 import org.junit.After;
 import org.junit.Assert;
@@ -635,4 +639,69 @@ 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);
+    }
+
+
+    @Test
+    public void testSetNetworkLogBodyEnabled() {
+        rnModule.setNetworkLogBodyEnabled(true);
+
+        mockInstabug.verify(() -> Instabug.setNetworkLogBodyEnabled(true));
+    }
+
+    @Test
+    public void testSetNetworkLogBodyDisabled() {
+        rnModule.setNetworkLogBodyEnabled(false);
+
+        mockInstabug.verify(() -> Instabug.setNetworkLogBodyEnabled(false));
+    }
+    
+    @Test
+    public void testEnableAutoMasking(){
+
+            String maskLabel = "labels";
+            String maskTextInputs = "textInputs";
+            String maskMedia = "media";
+            String maskNone = "none";
+
+            rnModule.enableAutoMasking(JavaOnlyArray.of(maskLabel, maskMedia, maskTextInputs,maskNone));
+
+            mockInstabug.verify(() -> Instabug.setAutoMaskScreenshotsTypes(MaskingType.LABELS,MaskingType.MEDIA,MaskingType.TEXT_INPUTS,MaskingType.MASK_NOTHING));
+    }
+
+    @Test
+    public void testGetNetworkBodyMaxSize_resolvesPromiseWithExpectedValue() {
+        Promise promise = mock(Promise.class);
+        InternalCore internalAPM = mock(InternalCore.class);
+        int expected = 10240;
+        when(internalAPM.get_networkLogCharLimit()).thenReturn(expected);
+
+        rnModule.getNetworkBodyMaxSize(promise);
+
+        verify(promise).resolve(expected);
+    }
+
 }
diff --git a/android/src/test/java/com/instabug/reactlibrary/RNInstabugTest.java b/android/src/test/java/com/instabug/reactlibrary/RNInstabugTest.java
index df169df1e..625eab1c9 100644
--- a/android/src/test/java/com/instabug/reactlibrary/RNInstabugTest.java
+++ b/android/src/test/java/com/instabug/reactlibrary/RNInstabugTest.java
@@ -4,6 +4,7 @@
 import static com.instabug.reactlibrary.util.GlobalMocks.reflected;
 import static org.junit.Assert.assertEquals;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.mockConstruction;
@@ -62,18 +63,20 @@ public void testInitWithLogLevel() {
                     // Initializes Instabug with the correct token
                     assertEquals(token, actualToken);
                     when(mock.setSdkDebugLogsLevel(anyInt())).thenReturn(mock);
+                    when(mock.ignoreFlagSecure(anyBoolean())).thenReturn(mock);
                     when(mock.setInvocationEvents(any())).thenReturn(mock);
                 });
 
-        sut.init(mContext, token, logLevel, invocationEvents);
+        sut.init(mContext, token, logLevel, true, invocationEvents);
 
         Instabug.Builder builder = mInstabugBuilder.constructed().get(0);
 
         // Here we check that it has changed to verbose value of the `logLevel` property
         verify(builder).setSdkDebugLogsLevel(LogLevel.VERBOSE);
         verify(builder).setInvocationEvents(invocationEvents);
-        verify(builder).build();
+        verify(builder).ignoreFlagSecure(true);
 
+        verify(builder).build();
 
 
         verify(sut).setBaseUrlForDeprecationLogs();
@@ -95,7 +98,7 @@ public void testInitWithoutLogLevel() {
 
         sut.init(mContext, token, invocationEvents);
 
-        verify(sut).init(mContext, token, defaultLogLevel, invocationEvents);
+        verify(sut).init(mContext, token, defaultLogLevel, null,invocationEvents);
         mInstabugBuilder.close();
     }
 
diff --git a/examples/default/android/app/build.gradle b/examples/default/android/app/build.gradle
index b017ddb2f..b039dd190 100644
--- a/examples/default/android/app/build.gradle
+++ b/examples/default/android/app/build.gradle
@@ -2,7 +2,7 @@ apply plugin: "com.android.application"
 apply plugin: "org.jetbrains.kotlin.android"
 apply plugin: "com.facebook.react"
 apply from: project(':react-native-config').projectDir.getPath() + "/dotenv.gradle"
-
+apply plugin: 'instabug-apm'
 /**
  * This is the configuration block to customize your React Native Android app.
  * By default you don't need to apply any configuration, just uncomment the lines you need.
@@ -124,6 +124,12 @@ android {
     }
 }
 
+instabug {
+    apm {
+        networkEnabled = true
+    }
+}
+
 dependencies {
     // The version of react-native is set by the React Native Gradle Plugin
     implementation("com.facebook.react:react-android")
diff --git a/examples/default/android/build.gradle b/examples/default/android/build.gradle
index 72cc6f93b..6ca76372e 100644
--- a/examples/default/android/build.gradle
+++ b/examples/default/android/build.gradle
@@ -12,11 +12,19 @@ buildscript {
     repositories {
         google()
         mavenCentral()
+        maven {
+            url "https://mvn.instabug.com/nexus/repository/instabug-internal/"
+            credentials {
+                username "instabug"
+                password System.getenv("INSTABUG_REPOSITORY_PASSWORD")
+            }
+        }
     }
     dependencies {
         classpath("com.android.tools.build:gradle:8.1.0")
         classpath("com.facebook.react:react-native-gradle-plugin")
         classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
+        classpath("com.instabug.library:instabug-plugin:15.0.1")
     }
 }
 
@@ -26,6 +34,13 @@ allprojects {
             url("$rootDir/../node_modules/detox/Detox-android")
         }
 
+        maven {
+            url "https://mvn.instabug.com/nexus/repository/instabug-internal/"
+            credentials {
+                username "instabug"
+                password System.getenv("INSTABUG_REPOSITORY_PASSWORD")
+            }
+        }
         maven {
             credentials {
                 username System.getenv("DREAM11_MAVEN_USERNAME")
diff --git a/examples/default/ios/InstabugTests/InstabugAPMTests.m b/examples/default/ios/InstabugTests/InstabugAPMTests.m
index dd96841de..949393adb 100644
--- a/examples/default/ios/InstabugTests/InstabugAPMTests.m
+++ b/examples/default/ios/InstabugTests/InstabugAPMTests.m
@@ -9,10 +9,11 @@
 #import 
 #import "OCMock/OCMock.h"
 #import "InstabugAPMBridge.h"
-#import 
-#import 
-#import "Instabug/Instabug.h"
+#import 
+#import 
+#import "InstabugSDK/InstabugSDK.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/InstabugBugReportingTests.m b/examples/default/ios/InstabugTests/InstabugBugReportingTests.m
index 99c328220..f0b6f97ec 100644
--- a/examples/default/ios/InstabugTests/InstabugBugReportingTests.m
+++ b/examples/default/ios/InstabugTests/InstabugBugReportingTests.m
@@ -9,8 +9,8 @@
 #import 
 #import "OCMock/OCMock.h"
 #import "InstabugBugReportingBridge.h"
-#import 
-#import "Instabug/Instabug.h"
+#import 
+#import "InstabugSDK/InstabugSDK.h"
 #import "IBGConstants.h"
 
 @interface InstabugBugReportingTests : XCTestCase
@@ -39,7 +39,7 @@ - (void) testgivenBoolean$setBugReportingEnabled_whenQuery_thenShouldCallNativeA
 - (void) testgivenInvocationEvent$setInvocationEvents_whenQuery_thenShouldCallNativeApiWithArgs {
   NSArray *invocationEventsArr;
   invocationEventsArr = [NSArray arrayWithObjects:  @(IBGInvocationEventScreenshot), nil];
-  
+
   [self.instabugBridge setInvocationEvents:invocationEventsArr];
   IBGInvocationEvent invocationEvents = 0;
   for (NSNumber *boxedValue in invocationEventsArr) {
@@ -76,7 +76,7 @@ - (void) testgivenHandlerSUBMIT$setOnSDKDismissedHandler_whenQuery_thenShouldCal
   RCTResponseSenderBlock callback = ^(NSArray *response) {};
   [partialMock setOnSDKDismissedHandler:callback];
   XCTAssertNotNil(IBGBugReporting.didDismissHandler);
-  
+
   NSDictionary *result = @{ @"dismissType": @"SUBMIT",
                             @"reportType": @"feedback"};
   OCMStub([partialMock sendEventWithName:@"IBGpostInvocationHandler" body:result]);
@@ -137,14 +137,14 @@ - (void) testgivenArgs$showBugReportingWithReportTypeAndOptions_whenQuery_thenSh
   }
   OCMStub([mock showWithReportType:reportType options:parsedOptions]);
   [self.instabugBridge show:reportType options:options];
-  
+
   XCTestExpectation *expectation = [self expectationWithDescription:@"Test ME PLX"];
-  
+
   [[NSRunLoop mainRunLoop] performBlock:^{
     OCMVerify([mock showWithReportType:reportType options:parsedOptions]);
     [expectation fulfill];
   }];
-  
+
   [self waitForExpectationsWithTimeout:EXPECTATION_TIMEOUT handler:nil];
 }
 
@@ -187,6 +187,27 @@ - (void) testSetCommentMinimumCharacterCount {
   [self.instabugBridge setCommentMinimumCharacterCount:limit reportTypes:reportTypesArr];
   OCMVerify([mock setCommentMinimumCharacterCountForReportTypes:reportTypes withLimit:limit.intValue]);
 }
+- (void)testAddUserConsentWithKey {
+  id mock = OCMClassMock([IBGBugReporting class]);
 
+  NSString *key = @"testKey";
+  NSString *description = @"Consent description";
+  BOOL mandatory = YES;
+  BOOL checked = NO;
+  NSNumber *actionType = @2;
+  IBGActionType mappedActionType = (IBGActionType)[actionType integerValue];
+
+  [self.instabugBridge addUserConsent:key
+                                  description:description
+                                    mandatory:mandatory
+                                      checked:checked
+                                   actionType:actionType];
+
+  OCMVerify([mock addUserConsentWithKey:key
+                                        description:description
+                                          mandatory:mandatory
+                                            checked:checked
+                                         actionType:mappedActionType]);
+}
 @end
 
diff --git a/examples/default/ios/InstabugTests/InstabugCrashReportingTests.m b/examples/default/ios/InstabugTests/InstabugCrashReportingTests.m
index d42e47022..5af452c03 100644
--- a/examples/default/ios/InstabugTests/InstabugCrashReportingTests.m
+++ b/examples/default/ios/InstabugTests/InstabugCrashReportingTests.m
@@ -1,5 +1,5 @@
 #import 
-#import "Instabug/Instabug.h"
+#import "InstabugSDK/InstabugSDK.h"
 #import "InstabugCrashReportingBridge.h"
 #import "OCMock/OCMock.h"
 #import "Util/IBGCrashReporting+CP.h"
@@ -19,11 +19,13 @@ - (void)setUp {
 }
 
 - (void)testSetEnabled {
-  [self.bridge setEnabled:YES];
-  XCTAssertTrue(IBGCrashReporting.enabled);
 
   [self.bridge setEnabled:NO];
   XCTAssertFalse(IBGCrashReporting.enabled);
+
+  [self.bridge setEnabled:YES];
+  XCTAssertTrue(IBGCrashReporting.enabled);
+
 }
 
 - (void)testSendJSCrash {
@@ -35,7 +37,7 @@ - (void)testSendJSCrash {
     [expectation fulfill];
   };
   RCTPromiseRejectBlock reject = ^(NSString *code, NSString *message, NSError *error) {};
-  
+
   [self.bridge sendJSCrash:stackTrace resolver:resolve rejecter:reject];
 
   [self waitForExpectations:@[expectation] timeout:1];
@@ -50,9 +52,9 @@ - (void)testSendNonFatalErrorJsonCrash {
   NSDictionary *userAttributes = @{ @"key" : @"value",  };
   IBGNonFatalLevel ibgNonFatalLevel = IBGNonFatalLevelInfo;
 
-  
+
   [self.bridge sendHandledJSCrash:jsonCrash userAttributes:userAttributes  fingerprint:fingerPrint nonFatalExceptionLevel:ibgNonFatalLevel resolver:resolve rejecter:reject];
-    
+
     OCMVerify([self.mCrashReporting cp_reportNonFatalCrashWithStackTrace:jsonCrash
            level:IBGNonFatalLevelInfo
          groupingString:fingerPrint
diff --git a/examples/default/ios/InstabugTests/InstabugFeatureRequestsTests.m b/examples/default/ios/InstabugTests/InstabugFeatureRequestsTests.m
index 4c300ab4c..dae3bfaae 100644
--- a/examples/default/ios/InstabugTests/InstabugFeatureRequestsTests.m
+++ b/examples/default/ios/InstabugTests/InstabugFeatureRequestsTests.m
@@ -9,8 +9,8 @@
 #import 
 #import "OCMock/OCMock.h"
 #import "InstabugFeatureRequestsBridge.h"
-#import 
-#import "Instabug/Instabug.h"
+#import 
+#import "InstabugSDK/InstabugSDK.h"
 #import "IBGConstants.h"
 
 @interface InstabugFeatureRequestsTests : XCTestCase
@@ -48,12 +48,12 @@ - (void) testgive$show_whenQuery_thenShouldCallNativeApi {
   OCMStub([mock show]);
   [self.instabugBridge show];
   XCTestExpectation *expectation = [self expectationWithDescription:@"Test ME PLX"];
-  
+
   [[NSRunLoop mainRunLoop] performBlock:^{
     OCMVerify([mock show]);
     [expectation fulfill];
   }];
-  
+
   [self waitForExpectationsWithTimeout:EXPECTATION_TIMEOUT handler:nil];
 }
 
diff --git a/examples/default/ios/InstabugTests/InstabugNetworkLoggerTests.m b/examples/default/ios/InstabugTests/InstabugNetworkLoggerTests.m
new file mode 100644
index 000000000..693c2c01c
--- /dev/null
+++ b/examples/default/ios/InstabugTests/InstabugNetworkLoggerTests.m
@@ -0,0 +1,124 @@
+#import 
+#import "InstabugNetworkLoggerBridge.h"
+
+@interface InstabugNetworkLoggerBridgeTests : XCTestCase
+
+@property (nonatomic, strong) InstabugNetworkLoggerBridge *networkLoggerBridge;
+
+@end
+
+@implementation InstabugNetworkLoggerBridgeTests
+
+- (void)setUp {
+    [super setUp];
+    self.networkLoggerBridge = [[InstabugNetworkLoggerBridge alloc] init];
+}
+
+- (void)tearDown {
+    self.networkLoggerBridge = nil;
+    [super tearDown];
+}
+
+- (void)testInitialization {
+    XCTAssertNotNil(self.networkLoggerBridge.requestObfuscationCompletionDictionary);
+    XCTAssertNotNil(self.networkLoggerBridge.responseObfuscationCompletionDictionary);
+    XCTAssertNotNil(self.networkLoggerBridge.requestFilteringCompletionDictionary);
+    XCTAssertNotNil(self.networkLoggerBridge.responseFilteringCompletionDictionary);
+}
+
+- (void)testRequiresMainQueueSetup {
+    XCTAssertFalse([InstabugNetworkLoggerBridge requiresMainQueueSetup]);
+}
+
+- (void)testSupportedEvents {
+    NSArray *events = [self.networkLoggerBridge supportedEvents];
+    NSArray *expectedEvents = @[@"IBGpreInvocationHandler", @"IBGNetworkLoggerHandler"];
+    XCTAssertEqualObjects(events, expectedEvents);
+}
+
+- (void)testMethodQueue {
+    dispatch_queue_t queue = [self.networkLoggerBridge methodQueue];
+    XCTAssertEqual(queue, dispatch_get_main_queue());
+}
+
+- (void)testStartObserving {
+    [self.networkLoggerBridge startObserving];
+    // Since `hasListeners` is private, we will assume it is true based on no errors or behavior issues
+    XCTAssertTrue(YES); // Expect no crashes
+}
+
+- (void)testStopObserving {
+    [self.networkLoggerBridge stopObserving];
+    XCTAssertTrue(YES); // Ensure the method doesn't cause issues
+}
+
+- (void)testIsNativeInterceptionEnabled {
+    XCTestExpectation *expectation = [self expectationWithDescription:@"isNativeInterceptionEnabled"];
+    
+    [self.networkLoggerBridge isNativeInterceptionEnabled:^(id result) {
+        XCTAssertNotNil(result);
+        XCTAssertTrue([result isKindOfClass:[NSNumber class]]);
+        [expectation fulfill];
+    } :^(NSString *code, NSString *message, NSError *error) {
+        XCTFail(@"Promise rejection not expected.");
+    }];
+    
+    [self waitForExpectationsWithTimeout:1.0 handler:nil];
+}
+
+- (void)testRegisterNetworkLogsListenerFiltering {
+    [self.networkLoggerBridge registerNetworkLogsListener:NetworkListenerTypeFiltering];
+    // Expect no crashes and check that filtering handler was set
+    XCTAssertTrue(YES); // Could add additional assertions if more visibility into handler setup is possible
+}
+
+- (void)testRegisterNetworkLogsListenerObfuscation {
+    [self.networkLoggerBridge registerNetworkLogsListener:NetworkListenerTypeObfuscation];
+    XCTAssertTrue(YES); // Expect no crashes, similar reasoning
+}
+
+- (void)testRegisterNetworkLogsListenerBoth {
+    [self.networkLoggerBridge registerNetworkLogsListener:NetworkListenerTypeBoth];
+    XCTAssertTrue(YES); // Same reason, ensuring no crash
+}
+
+- (void)testUpdateNetworkLogSnapshotValidJson {
+    NSString *jsonString = @"{\"url\":\"https://example.com\",\"requestBody\":\"bodyData\",\"requestHeader\":{\"key\":\"value\"},\"id\":\"12345\"}";
+    
+    [self.networkLoggerBridge updateNetworkLogSnapshot:jsonString];
+    
+    // Expect no errors or logs regarding completion issues
+    XCTAssertTrue(YES);
+}
+
+- (void)testUpdateNetworkLogSnapshotInvalidJson {
+    NSString *invalidJsonString = @"invalid json string";
+    
+    // This should fail gracefully and log an error
+    [self.networkLoggerBridge updateNetworkLogSnapshot:invalidJsonString];
+    XCTAssertTrue(YES); // No crash, expect graceful handling
+}
+
+- (void)testSetNetworkLoggingRequestFilterPredicateIOS {
+    NSString *callbackID = @"12345";
+    
+    // Mock a completion handler
+    self.networkLoggerBridge.requestFilteringCompletionDictionary[callbackID] = ^(BOOL shouldSave) {
+        XCTAssertTrue(shouldSave);
+    };
+    
+    [self.networkLoggerBridge setNetworkLoggingRequestFilterPredicateIOS:callbackID :YES];
+    
+    XCTAssertTrue(YES); // Ensure that the handler is invoked correctly
+}
+
+- (void)testSetNetworkLoggingRequestFilterPredicateIOSInvalidCallback {
+    NSString *invalidCallbackID = @"invalidID";
+    
+    // This should fail gracefully and log an error
+    [self.networkLoggerBridge setNetworkLoggingRequestFilterPredicateIOS:invalidCallbackID :YES];
+    
+    XCTAssertTrue(YES); // No crash, expect graceful handling
+}
+
+@end
diff --git a/examples/default/ios/InstabugTests/InstabugRepliesTests.m b/examples/default/ios/InstabugTests/InstabugRepliesTests.m
index a5c31e934..d968cc8eb 100644
--- a/examples/default/ios/InstabugTests/InstabugRepliesTests.m
+++ b/examples/default/ios/InstabugTests/InstabugRepliesTests.m
@@ -9,8 +9,8 @@
 #import 
 #import "OCMock/OCMock.h"
 #import "InstabugRepliesBridge.h"
-#import 
-#import "Instabug/Instabug.h"
+#import 
+#import "InstabugSDK/InstabugSDK.h"
 #import "IBGConstants.h"
 
 @interface InstabugRepliesTests : XCTestCase
@@ -53,12 +53,12 @@ - (void) testgiven$show_whenQuery_thenShouldCallNativeApi {
   OCMStub([mock show]);
   [self.instabugBridge show];
   XCTestExpectation *expectation = [self expectationWithDescription:@"Test ME PLX"];
-  
+
   [[NSRunLoop mainRunLoop] performBlock:^{
     OCMVerify([mock show]);
     [expectation fulfill];
   }];
-  
+
   [self waitForExpectationsWithTimeout:EXPECTATION_TIMEOUT handler:nil];
 }
 
@@ -67,7 +67,7 @@ - (void) testgivenOnNewReplyReceivedHandler$setOnNewReplyReceivedCallback_whenQu
   RCTResponseSenderBlock callback = ^(NSArray *response) {};
   [partialMock setOnNewReplyReceivedHandler:callback];
   XCTAssertNotNil(IBGReplies.didReceiveReplyHandler);
-  
+
   OCMStub([partialMock sendEventWithName:@"IBGOnNewReplyReceivedCallback" body:nil]);
   IBGReplies.didReceiveReplyHandler();
   OCMVerify([partialMock sendEventWithName:@"IBGOnNewReplyReceivedCallback" body:nil]);
@@ -90,7 +90,7 @@ - (void) testgivenBoolean$setInAppNotificationEnabled_whenQuery_thenShouldCallNa
 - (void)testSetPushNotificationsEnabled {
   id mock = OCMClassMock([IBGReplies class]);
   BOOL isPushNotificationEnabled = true;
-  
+
   OCMStub([mock setPushNotificationsEnabled:isPushNotificationEnabled]);
   [self.instabugBridge setPushNotificationsEnabled:isPushNotificationEnabled];
   OCMVerify([mock setPushNotificationsEnabled:isPushNotificationEnabled]);
diff --git a/examples/default/ios/InstabugTests/InstabugSampleTests.m b/examples/default/ios/InstabugTests/InstabugSampleTests.m
index 51bbe182c..34fe9cfe3 100644
--- a/examples/default/ios/InstabugTests/InstabugSampleTests.m
+++ b/examples/default/ios/InstabugTests/InstabugSampleTests.m
@@ -7,9 +7,9 @@
 
 #import 
 #import "OCMock/OCMock.h"
-#import "Instabug/Instabug.h"
+#import "InstabugSDK/InstabugSDK.h"
 #import "InstabugReactBridge.h"
-#import 
+#import 
 #import "IBGConstants.h"
 #import "RNInstabug.h"
 #import 
@@ -75,7 +75,9 @@ - (void)testInit {
 
   OCMStub([mock setCodePushVersion:codePushVersion]);
 
-  [self.instabugBridge init:appToken invocationEvents:invocationEvents debugLogsLevel:sdkDebugLogsLevel useNativeNetworkInterception:useNativeNetworkInterception codePushVersion:codePushVersion];
+  [self.instabugBridge init:appToken invocationEvents:invocationEvents debugLogsLevel:sdkDebugLogsLevel useNativeNetworkInterception:useNativeNetworkInterception codePushVersion:codePushVersion
+    options:nil
+  ];
   OCMVerify([mock setCodePushVersion:codePushVersion]);
 
   OCMVerify([self.mRNInstabug initWithToken:appToken invocationEvents:floatingButtonInvocationEvent debugLogsLevel:sdkDebugLogsLevel useNativeNetworkInterception:useNativeNetworkInterception]);
@@ -315,7 +317,7 @@ - (void)testSetWelcomeMessageMode {
 
 - (void)testNetworkLogIOS {
   id mIBGNetworkLogger = OCMClassMock([IBGNetworkLogger class]);
-  
+
   NSString *url = @"https://api.instabug.com";
   NSString *method = @"GET";
   NSString *requestBody = @"requestBody";
@@ -332,7 +334,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 +355,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 +376,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 +552,107 @@ - (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]);
+}
+
+- (void)testEnableAutoMasking {
+    id mock = OCMClassMock([Instabug class]);
+
+    NSArray *autoMaskingTypes = [NSArray arrayWithObjects:
+         [NSNumber numberWithInteger:IBGAutoMaskScreenshotOptionLabels],
+         [NSNumber numberWithInteger:IBGAutoMaskScreenshotOptionTextInputs],
+         [NSNumber numberWithInteger:IBGAutoMaskScreenshotOptionMedia],
+         [NSNumber numberWithInteger:IBGAutoMaskScreenshotOptionMaskNothing],
+         nil];
+
+     OCMStub([mock setAutoMaskScreenshots:IBGAutoMaskScreenshotOptionLabels | IBGAutoMaskScreenshotOptionTextInputs | IBGAutoMaskScreenshotOptionMedia | IBGAutoMaskScreenshotOptionMaskNothing]);
+
+     [self.instabugBridge enableAutoMasking:autoMaskingTypes];
+
+     OCMVerify([mock setAutoMaskScreenshots:IBGAutoMaskScreenshotOptionLabels | IBGAutoMaskScreenshotOptionTextInputs | IBGAutoMaskScreenshotOptionMedia | IBGAutoMaskScreenshotOptionMaskNothing]);
+}
+
+- (void)testSetNetworkLogBodyEnabled {
+    id mock = OCMClassMock([IBGNetworkLogger class]);
+    BOOL isEnabled = YES;
+
+    OCMStub([mock setLogBodyEnabled:isEnabled]);
+    [self.instabugBridge setNetworkLogBodyEnabled:isEnabled];
+    OCMVerify([mock setLogBodyEnabled:isEnabled]);
+}
+
+- (void)testGetNetworkBodyMaxSize {
+    id mock = OCMClassMock([IBGNetworkLogger class]);
+    double expectedValue = 10240.0;
+
+    OCMStub([mock getNetworkBodyMaxSize]).andReturn(expectedValue);
+
+    XCTestExpectation *expectation = [self expectationWithDescription:@"Call resolve block"];
+    RCTPromiseResolveBlock resolve = ^(NSNumber *result) {
+        XCTAssertEqual(result.doubleValue, expectedValue);
+        [expectation fulfill];
+    };
+
+    [self.instabugBridge getNetworkBodyMaxSize:resolve :nil];
+    [self waitForExpectationsWithTimeout:1.0 handler:nil];
+
+    OCMVerify(ClassMethod([mock getNetworkBodyMaxSize]));
+}
+
+
 @end
diff --git a/examples/default/ios/InstabugTests/InstabugSessionReplayTests.m b/examples/default/ios/InstabugTests/InstabugSessionReplayTests.m
index c3f037e60..74bc62c3f 100644
--- a/examples/default/ios/InstabugTests/InstabugSessionReplayTests.m
+++ b/examples/default/ios/InstabugTests/InstabugSessionReplayTests.m
@@ -1,8 +1,8 @@
 #import 
 #import "OCMock/OCMock.h"
 #import "InstabugSessionReplayBridge.h"
-#import 
-#import "Instabug/Instabug.h"
+#import 
+#import "InstabugSDK/InstabugSDK.h"
 #import "IBGConstants.h"
 
 @interface InstabugSessionReplayTests : XCTestCase
diff --git a/examples/default/ios/InstabugTests/InstabugSurveysTests.m b/examples/default/ios/InstabugTests/InstabugSurveysTests.m
index b0790b8b2..fb4ec7005 100644
--- a/examples/default/ios/InstabugTests/InstabugSurveysTests.m
+++ b/examples/default/ios/InstabugTests/InstabugSurveysTests.m
@@ -9,8 +9,8 @@
 #import 
 #import "OCMock/OCMock.h"
 #import "InstabugSurveysBridge.h"
-#import 
-#import "Instabug/Instabug.h"
+#import 
+#import "InstabugSDK/InstabugSDK.h"
 #import "IBGConstants.h"
 
 @interface InstabugSurveysTests : XCTestCase
@@ -45,7 +45,7 @@ - (void)setUp {
 - (void)testShowingSurvey {
   id mock = OCMClassMock([IBGSurveys class]);
   NSString *token = @"token";
-  
+
   OCMStub([mock showSurveyWithToken:token]);
   [self.instabugBridge showSurvey:token];
   OCMVerify([mock showSurveyWithToken:token]);
@@ -54,7 +54,7 @@ - (void)testShowingSurvey {
 
 - (void) testShowSurveyIfAvailable {
   id mock = OCMClassMock([IBGSurveys class]);
-  
+
   OCMStub([mock showSurveyIfAvailable]);
   [self.instabugBridge showSurveysIfAvailable];
   OCMVerify([mock showSurveyIfAvailable]);
@@ -63,7 +63,7 @@ - (void) testShowSurveyIfAvailable {
 - (void) testAutoShowingSurveysEnabled {
   id mock = OCMClassMock([IBGSurveys class]);
   BOOL isEnabled = YES;
-  
+
   OCMStub([mock setAutoShowingEnabled:isEnabled]);
   [self.instabugBridge setAutoShowingEnabled:isEnabled];
   OCMVerify([mock setAutoShowingEnabled:isEnabled]);
@@ -72,7 +72,7 @@ - (void) testAutoShowingSurveysEnabled {
 - (void) testSetShouldShowSurveysWelcomeScreen {
   id mock = OCMClassMock([IBGSurveys class]);
   BOOL isEnabled = YES;
-  
+
   OCMStub([mock setShouldShowWelcomeScreen:isEnabled]);
   [self.instabugBridge setShouldShowWelcomeScreen:isEnabled];
   OCMVerify([mock setShouldShowWelcomeScreen:isEnabled]);
@@ -80,7 +80,7 @@ - (void) testSetShouldShowSurveysWelcomeScreen {
 
 - (void) testSetSurveysEnabled {
   BOOL isEnabled = YES;
-  
+
   [self.instabugBridge setEnabled:isEnabled];
   XCTAssertTrue(IBGSurveys.enabled);
 }
@@ -95,7 +95,7 @@ - (void) testHasRespondedToSurveyWithToken {
     [expectation fulfill];
   };
   RCTPromiseRejectBlock reject = ^(NSString *code, NSString *message, NSError *error) {};
-  
+
   OCMStub([mock hasRespondedToSurveyWithToken:surveyToken completionHandler:[OCMArg invokeBlock]]);
   [self.instabugBridge hasRespondedToSurvey:surveyToken :resolve :reject];
   OCMVerify([mock hasRespondedToSurveyWithToken:surveyToken completionHandler:[OCMArg isNotNil]]);
@@ -136,7 +136,7 @@ - (void) testSetDidDismissSurveyHandler {
 
 - (void) testSetAppStoreURL {
   NSString *appStoreURL = @"http://test";
-  
+
   [self.instabugBridge setAppStoreURL:appStoreURL];
   XCTAssertEqual(IBGSurveys.appStoreURL, appStoreURL);
 }
diff --git a/examples/default/ios/InstabugTests/RNInstabugTests.m b/examples/default/ios/InstabugTests/RNInstabugTests.m
index cde248ad3..abf355614 100644
--- a/examples/default/ios/InstabugTests/RNInstabugTests.m
+++ b/examples/default/ios/InstabugTests/RNInstabugTests.m
@@ -1,7 +1,7 @@
 #import 
 #import "OCMock/OCMock.h"
-#import "Instabug/Instabug.h"
-#import 
+#import "InstabugSDK/InstabugSDK.h"
+#import 
 #import "RNInstabug.h"
 #import "RNInstabug/Instabug+CP.h"
 #import "RNInstabug/IBGNetworkLogger+CP.h"
@@ -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/InstabugTests/Util/IBGCrashReporting+CP.h b/examples/default/ios/InstabugTests/Util/IBGCrashReporting+CP.h
index 4229dbcea..cf3a1c200 100644
--- a/examples/default/ios/InstabugTests/Util/IBGCrashReporting+CP.h
+++ b/examples/default/ios/InstabugTests/Util/IBGCrashReporting+CP.h
@@ -1,4 +1,4 @@
-#import 
+#import 
 
 
 @interface IBGCrashReporting (CP)
diff --git a/examples/default/ios/Podfile b/examples/default/ios/Podfile
index 3526171cd..e1dda08e3 100644
--- a/examples/default/ios/Podfile
+++ b/examples/default/ios/Podfile
@@ -15,6 +15,7 @@ target 'InstabugExample' do
   config = use_native_modules!
   rn_maps_path = '../node_modules/react-native-maps'
   pod 'react-native-google-maps', :path => rn_maps_path
+
   # Flags change depending on the env values.
   flags = get_default_flags()
 
diff --git a/examples/default/ios/Podfile.lock b/examples/default/ios/Podfile.lock
index 81ba100a5..274dbfc09 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 (15.1.1)
   - instabug-reactnative-ndk (0.1.0):
     - DoubleConversion
     - glog
@@ -1296,6 +1296,8 @@ PODS:
     - React-Core
   - react-native-maps (1.10.3):
     - React-Core
+  - react-native-netinfo (11.4.1):
+    - React-Core
   - react-native-safe-area-context (4.12.0):
     - React-Core
   - react-native-slider (4.5.5):
@@ -1319,6 +1321,27 @@ PODS:
     - ReactCommon/turbomodule/bridging
     - ReactCommon/turbomodule/core
     - Yoga
+  - react-native-webview (13.13.2):
+    - 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 +1625,8 @@ PODS:
     - ReactCommon/turbomodule/bridging
     - ReactCommon/turbomodule/core
     - Yoga
-  - RNInstabug (14.0.0):
-    - Instabug (= 14.0.0)
+  - RNInstabug (15.0.2):
+    - Instabug (= 15.1.1)
     - React-Core
   - RNReanimated (3.16.1):
     - DoubleConversion
@@ -1784,8 +1807,10 @@ DEPENDENCIES:
   - react-native-config (from `../node_modules/react-native-config`)
   - react-native-google-maps (from `../node_modules/react-native-maps`)
   - react-native-maps (from `../node_modules/react-native-maps`)
+  - "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)"
   - 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`)
@@ -1911,10 +1936,14 @@ EXTERNAL SOURCES:
     :path: "../node_modules/react-native-maps"
   react-native-maps:
     :path: "../node_modules/react-native-maps"
+  react-native-netinfo:
+    :path: "../node_modules/@react-native-community/netinfo"
   react-native-safe-area-context:
     :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 +2022,7 @@ SPEC CHECKSUMS:
   Google-Maps-iOS-Utils: f77eab4c4326d7e6a277f8e23a0232402731913a
   GoogleMaps: 032f676450ba0779bd8ce16840690915f84e57ac
   hermes-engine: ea92f60f37dba025e293cbe4b4a548fd26b610a0
-  Instabug: a0beffc01658773e2fac549845782f8937707dc4
+  Instabug: 3e7af445c14d7823fcdecba223f09b5f7c0c6ce1
   instabug-reactnative-ndk: d765ac289d56e8896398d02760d9abf2562fc641
   OCMock: 589f2c84dacb1f5aaf6e4cec1f292551fe748e74
   RCT-Folly: 4464f4d875961fce86008d45f4ecf6cef6de0740
@@ -2029,8 +2058,10 @@ SPEC CHECKSUMS:
   react-native-config: 8f7283449bbb048902f4e764affbbf24504454af
   react-native-google-maps: 1bcc1f9f13f798fcf230db7fe476f3566d0bc0a3
   react-native-maps: 72a8a903f8a1b53e2c777ba79102078ab502e0bf
+  react-native-netinfo: f0a9899081c185db1de5bb2fdc1c88c202a059ac
   react-native-safe-area-context: 142fade490cbebbe428640b8cbdb09daf17e8191
   react-native-slider: 4a0f3386a38fc3d2d955efc515aef7096f7d1ee4
+  react-native-webview: c0b91a4598bd54e9fbc70353aebf1e9bab2e5bb9
   React-nativeconfig: 8c83d992b9cc7d75b5abe262069eaeea4349f794
   React-NativeModulesApple: 9f7920224a3b0c7d04d77990067ded14cee3c614
   React-perflogger: 59e1a3182dca2cee7b9f1f7aab204018d46d1914
@@ -2059,7 +2090,7 @@ SPEC CHECKSUMS:
   ReactCommon: 6a952e50c2a4b694731d7682aaa6c79bc156e4ad
   RNCClipboard: 2821ac938ef46f736a8de0c8814845dde2dcbdfb
   RNGestureHandler: 511250b190a284388f9dd0d2e56c1df76f14cfb8
-  RNInstabug: eaa8cde2bcd3c8e757c6dd5c0d33a20814f9658a
+  RNInstabug: c4d26c830b40c474422012d1a216d8ea37c88151
   RNReanimated: f42a5044d121d68e91680caacb0293f4274228eb
   RNScreens: c7ceced6a8384cb9be5e7a5e88e9e714401fd958
   RNSVG: 8b1a777d54096b8c2a0fd38fc9d5a454332bbb4d
@@ -2067,6 +2098,6 @@ SPEC CHECKSUMS:
   SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d
   Yoga: 055f92ad73f8c8600a93f0e25ac0b2344c3b07e6
 
-PODFILE CHECKSUM: 63bf073bef3872df95ea45e7c9c023a331ebb3c3
+PODFILE CHECKSUM: 837b933596e1616ff02cc206bb17dee4f611fdbc
 
 COCOAPODS: 1.14.0
diff --git a/examples/default/ios/native/CrashReportingExampleModule.m b/examples/default/ios/native/CrashReportingExampleModule.m
index b337da215..7cf031b2d 100644
--- a/examples/default/ios/native/CrashReportingExampleModule.m
+++ b/examples/default/ios/native/CrashReportingExampleModule.m
@@ -1,6 +1,6 @@
 #import "CrashReportingExampleModule.h"
-#import 
-#import 
+#import 
+#import 
 
 @interface CrashReportingExampleModule()
 @property (nonatomic, strong) NSMutableArray *oomBelly;
diff --git a/examples/default/package.json b/examples/default/package.json
index 617cdc17b..7e6bc4c4e 100644
--- a/examples/default/package.json
+++ b/examples/default/package.json
@@ -11,6 +11,7 @@
   },
   "dependencies": {
     "@react-native-clipboard/clipboard": "^1.14.3",
+    "@react-native-community/netinfo": "^11.4.1",
     "@react-native-community/slider": "^4.5.5",
     "@react-navigation/bottom-tabs": "^6.5.7",
     "@react-navigation/native": "^6.1.6",
@@ -32,6 +33,7 @@
     "react-native-screens": "^3.35.0",
     "react-native-svg": "^15.8.0",
     "react-native-vector-icons": "^10.2.0",
+    "react-native-webview": "^13.13.2",
     "react-query": "^3.39.3"
   },
   "devDependencies": {
diff --git a/examples/default/src/App.tsx b/examples/default/src/App.tsx
index abdab1111..ad1c32579 100644
--- a/examples/default/src/App.tsx
+++ b/examples/default/src/App.tsx
@@ -3,15 +3,17 @@ import { StyleSheet } from 'react-native';
 
 import { GestureHandlerRootView } from 'react-native-gesture-handler';
 import { NavigationContainer, useNavigationContainerRef } from '@react-navigation/native';
+import type { SessionMetadata } from 'instabug-reactnative';
 import Instabug, {
   CrashReporting,
   InvocationEvent,
+  LaunchType,
   LogLevel,
+  NetworkInterceptionMode,
+  NetworkLogger,
   ReproStepsMode,
   SessionReplay,
-  LaunchType,
 } from 'instabug-reactnative';
-import type { SessionMetadata } from 'instabug-reactnative';
 import { NativeBaseProvider } from 'native-base';
 
 import { RootTabNavigator } from './navigation/RootTab';
@@ -38,22 +40,34 @@ export const App: React.FC = () => {
 
   const navigationRef = useNavigationContainerRef();
 
-  useEffect(() => {
-    SessionReplay.setSyncCallback((data) => shouldSyncSession(data));
+  const initializeInstabug = () => {
+    try {
+      SessionReplay.setSyncCallback((data) => shouldSyncSession(data));
 
-    Instabug.init({
-      token: 'deb1910a7342814af4e4c9210c786f35',
-      invocationEvents: [InvocationEvent.floatingButton],
-      debugLogsLevel: LogLevel.verbose,
-    });
-    CrashReporting.setNDKCrashesEnabled(true);
+      Instabug.init({
+        token: 'deb1910a7342814af4e4c9210c786f35',
+        invocationEvents: [InvocationEvent.floatingButton],
+        debugLogsLevel: LogLevel.verbose,
+        networkInterceptionMode: NetworkInterceptionMode.javascript,
+      });
 
-    Instabug.setReproStepsConfig({
-      all: ReproStepsMode.enabled,
+      CrashReporting.setNDKCrashesEnabled(true);
+      Instabug.setReproStepsConfig({ all: ReproStepsMode.enabled });
+    } catch (error) {
+      console.error('Instabug initialization failed:', error);
+    }
+  };
+
+  useEffect(() => {
+    initializeInstabug();
+    NetworkLogger.setNetworkDataObfuscationHandler(async (networkData) => {
+      networkData.url = `${networkData.url}/JS/Obfuscated`;
+      return networkData;
     });
-  }, []);
+  });
 
   useEffect(() => {
+    // @ts-ignore
     const unregisterListener = Instabug.setNavigationListener(navigationRef);
 
     return unregisterListener;
@@ -63,7 +77,7 @@ export const App: React.FC = () => {
     
       
         
-          
+          
             
           
         
@@ -76,4 +90,9 @@ const styles = StyleSheet.create({
   root: {
     flex: 1,
   },
+  loading: {
+    flex: 1,
+    justifyContent: 'center',
+    alignItems: 'center',
+  },
 });
diff --git a/examples/default/src/components/CustomGap.tsx b/examples/default/src/components/CustomGap.tsx
new file mode 100644
index 000000000..bf43eb9d4
--- /dev/null
+++ b/examples/default/src/components/CustomGap.tsx
@@ -0,0 +1,33 @@
+import React from 'react';
+import { type DimensionValue, View } from 'react-native';
+
+// Define the component type with static properties
+interface CustomGapComponent extends React.FC<{ height?: DimensionValue; width?: DimensionValue }> {
+  small: JSX.Element;
+  smallV: JSX.Element;
+  smallH: JSX.Element;
+  large: JSX.Element;
+  largeV: JSX.Element;
+  largeH: JSX.Element;
+}
+
+const defaultDimensionValue = 16;
+
+// Define the CustomGap component
+const CustomGap: CustomGapComponent = ({ height, width }) => {
+  return (
+    
+  );
+};
+
+// Assign static properties for predefined gaps
+CustomGap.small = ;
+CustomGap.large = ;
+CustomGap.smallV = ;
+CustomGap.largeV = ;
+CustomGap.smallH = ;
+CustomGap.largeH = ;
+
+export default CustomGap;
diff --git a/examples/default/src/navigation/HomeStack.tsx b/examples/default/src/navigation/HomeStack.tsx
index 716ea05d9..090aa6587 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 95a850569..a8afc14a8 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 0b04e6191..3652a95c5 100644 --- a/examples/default/src/screens/apm/APMScreen.tsx +++ b/examples/default/src/screens/apm/APMScreen.tsx @@ -3,9 +3,10 @@ import type { HomeStackParamList } from '../../navigation/HomeStack'; import React, { useState } from 'react'; import { ListTile } from '../../components/ListTile'; import { Screen } from '../../components/Screen'; -import { Text, Switch } from 'react-native'; +import { StyleSheet, Switch, Text, View } from 'react-native'; import { APM } from 'instabug-reactnative'; import { showNotification } from '../../utils/showNotification'; +import CustomGap from '../../components/CustomGap'; export const APMScreen: React.FC> = ({ navigation, @@ -17,15 +18,26 @@ export const APMScreen: React.FC - Enable APM: - + + Enable APM: + + + {CustomGap.smallV} APM.endAppLaunch()} /> navigation.navigate('NetworkTraces')} /> navigation.navigate('ExecutionTraces')} /> navigation.navigate('AppFlows')} /> + navigation.navigate('WebViews')} /> + navigation.navigate('ComplexViews')} /> ); }; diff --git a/examples/default/src/screens/apm/NetworkScreen.tsx b/examples/default/src/screens/apm/NetworkScreen.tsx index 8aa20f49f..4225e6185 100644 --- a/examples/default/src/screens/apm/NetworkScreen.tsx +++ b/examples/default/src/screens/apm/NetworkScreen.tsx @@ -5,26 +5,31 @@ import { Screen } from '../../components/Screen'; import { ClipboardTextInput } from '../../components/ClipboardTextInput'; import { useQuery } from 'react-query'; import { HStack, VStack } from 'native-base'; -import { gql, request } from 'graphql-request'; +import { gql, GraphQLClient } from 'graphql-request'; import { CustomButton } from '../../components/CustomButton'; import axios from 'axios'; import type { HomeStackParamList } from '../../navigation/HomeStack'; import type { NativeStackScreenProps } from '@react-navigation/native-stack'; -import { ListTile } from '../../components/ListTile'; +import { useNetInfo } from '@react-native-community/netinfo'; export const NetworkScreen: React.FC< NativeStackScreenProps > = ({ navigation }) => { const [endpointUrl, setEndpointUrl] = useState(''); const { width, height } = useWindowDimensions(); - const defaultRequestUrl = 'https://jsonplaceholder.typicode.com/posts/1'; + + const { isConnected } = useNetInfo(); + const defaultRequestBaseUrl = 'https://jsonplaceholder.typicode.com/posts/'; + const shortenLink = 'https://shorturl.at/3Ufj3'; + const defaultRequestUrl = `${defaultRequestBaseUrl}1`; + const imageUrls = [ 'https://fastly.picsum.photos/id/57/200/300.jpg?hmac=l908G1qVr4r7dP947-tak2mY8Vvic_vEYzCXUCKKskY', 'https://fastly.picsum.photos/id/619/200/300.jpg?hmac=WqBGwlGjuY9RCdpzRaG9G-rc9Fi7TGUINX_-klAL2kA', ]; async function sendRequestToUrl() { - let urlToSend = ''; + let urlToSend: string; if (endpointUrl.trim() !== '') { urlToSend = endpointUrl; @@ -52,7 +57,7 @@ export const NetworkScreen: React.FC< } async function sendRequestToUrlUsingAxios() { - let urlToSend = ''; + let urlToSend: string; if (endpointUrl.trim() !== '') { urlToSend = endpointUrl; @@ -77,7 +82,33 @@ export const NetworkScreen: React.FC< } } + async function sendRedirectRequestToUrl() { + try { + console.log('Sending request to: ', shortenLink); + const response = await fetch(shortenLink); + console.log('Received from: ', response.url); + + // Format the JSON response for better logging + const data = await response.json(); + + // Format the JSON response for better logging + const formattedData = JSON.stringify(data, null, 2); + + // Log the formatted response + console.log('Response:', formattedData); + } catch (error) { + // Handle errors appropriately + console.error('Error:', error); + } + } + const fetchGraphQlData = async () => { + const client = new GraphQLClient('https://countries.trevorblades.com/graphql', { + headers: { + 'ibg-graphql-header': 'AndrewQL', // change Query Name here + }, + }); + const document = gql` query { country(code: "EG") { @@ -87,13 +118,51 @@ export const NetworkScreen: React.FC< } `; - return request<{ country: { emoji: string; name: string } }>( - 'https://countries.trevorblades.com/graphql', - document, - ); + return client.request<{ country: { emoji: string; name: string } }>(document); }; 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'); + }; + + function generateUrls(count: number = 10) { + const urls = []; + for (let i = 1; i <= count; i++) { + urls.push(defaultRequestBaseUrl + i); + } + return urls; + } + + async function makeSequentialApiCalls(urls: string[]): Promise { + const results: any[] = []; + + try { + for (let i = 0; i < urls.length; i++) { + await fetch(urls[i]); + results.push(results[i]); + } + return results; + } catch (error) { + console.error('Error making parallel API calls:', error); + throw error; + } + } + async function makeParallelApiCalls(urls: string[]): Promise { + const fetchPromises = urls.map((url) => fetch(url).then((response) => response.json())); + + try { + return await Promise.all(fetchPromises); + } catch (error) { + console.error('Error making parallel API calls:', error); + throw error; + } + } return ( @@ -107,13 +176,41 @@ export const NetworkScreen: React.FC< value={endpointUrl} /> + + makeParallelApiCalls(generateUrls())} + title="Send Parallel Requests" + /> + makeSequentialApiCalls(generateUrls())} + title="Send Sequantail Requests" + /> + + refetch()} title="Reload GraphQL" /> + navigation.navigate('HttpScreen')} + title="Go HTTP Screen" + /> + + simulateNetworkRequest()} + /> + simulateNetworkRequestWithoutHeader()} + /> refetch} title="Reload GraphQL" /> + {isConnected ? 'Network is Connected' : 'Network is not connected'} {isLoading && Loading...} {isSuccess && GraphQL Data: {data.country.emoji}} {isError && Error!} @@ -133,7 +230,6 @@ export const NetworkScreen: React.FC< ))} - navigation.navigate('HttpScreen')} /> ); 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 000000000..fbee5c028 --- /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 000000000..6cb4a6263 --- /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 000000000..0c3309dfe --- /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 348eda2fa..012fe261d 100644 --- a/examples/default/yarn.lock +++ b/examples/default/yarn.lock @@ -2041,6 +2041,11 @@ prompts "^2.4.2" semver "^7.5.2" +"@react-native-community/netinfo@^11.4.1": + version "11.4.1" + resolved "https://registry.yarnpkg.com/@react-native-community/netinfo/-/netinfo-11.4.1.tgz#a3c247aceab35f75dd0aa4bfa85d2be5a4508688" + integrity sha512-B0BYAkghz3Q2V09BF88RA601XursIEA111tnc2JOaN7axJWmNefmfjZqw/KdSxKZp7CZUuPpjBmz/WCR9uaHYg== + "@react-native-community/slider@^4.5.5": version "4.5.5" resolved "https://registry.yarnpkg.com/@react-native-community/slider/-/slider-4.5.5.tgz#d70fc5870477760033769bbd6625d57e7d7678b2" @@ -4427,7 +4432,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 +6334,14 @@ react-native-vector-icons@^10.2.0: prop-types "^15.7.2" yargs "^16.1.1" +react-native-webview@^13.13.2: + version "13.13.2" + resolved "https://registry.yarnpkg.com/react-native-webview/-/react-native-webview-13.13.2.tgz#6d72fd8492f11accbcf3be4d1386fa961e424357" + integrity sha512-zACPDTF0WnaEnKZ9mA/r/UpcOpV2gQM06AAIrOOexnO8UJvXL8Pjso0b/wTqKFxUZZnmjKuwd8gHVUosVOdVrw== + 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/ArgsRegistry.h b/ios/RNInstabug/ArgsRegistry.h index c7720e38f..c760ae36c 100644 --- a/ios/RNInstabug/ArgsRegistry.h +++ b/ios/RNInstabug/ArgsRegistry.h @@ -1,6 +1,6 @@ #import -#import -#import +#import +#import typedef NSDictionary ArgsDictionary; @@ -23,7 +23,9 @@ typedef NSDictionary ArgsDictionary; + (ArgsDictionary *) locales; + (ArgsDictionary *)nonFatalExceptionLevel; + (ArgsDictionary *) launchType; ++ (ArgsDictionary *) userConsentActionTypes; + (NSDictionary *) placeholders; ++ (ArgsDictionary *)autoMaskingTypes; @end diff --git a/ios/RNInstabug/ArgsRegistry.m b/ios/RNInstabug/ArgsRegistry.m index bc14302ac..8fb1e9b77 100644 --- a/ios/RNInstabug/ArgsRegistry.m +++ b/ios/RNInstabug/ArgsRegistry.m @@ -21,7 +21,9 @@ + (NSMutableDictionary *) getAll { [all addEntriesFromDictionary:ArgsRegistry.nonFatalExceptionLevel]; [all addEntriesFromDictionary:ArgsRegistry.placeholders]; [all addEntriesFromDictionary:ArgsRegistry.launchType]; - + [all addEntriesFromDictionary:ArgsRegistry.autoMaskingTypes]; + [all addEntriesFromDictionary:ArgsRegistry.userConsentActionTypes]; + return all; } @@ -110,7 +112,13 @@ + (ArgsDictionary *) actionTypes { @"addCommentToFeature": @(IBGActionAddCommentToFeature), }; } - ++ (ArgsDictionary *) userConsentActionTypes { + return @{ + @"dropAutoCapturedMedia": @(IBGActionTypeDropAutoCapturedMedia), + @"dropLogs": @(IBGActionTypeDropLogs), + @"noChat": @(IBGActionTypeNoChat) + }; +} + (ArgsDictionary *) extendedBugReportStates { return @{ @"enabledWithRequiredFields": @(IBGExtendedBugReportModeEnabledWithRequiredFields), @@ -249,4 +257,12 @@ + (ArgsDictionary *) launchType { }; } ++ (ArgsDictionary *)autoMaskingTypes { + return @{ + @"labels" : @(IBGAutoMaskScreenshotOptionLabels), + @"textInputs" : @(IBGAutoMaskScreenshotOptionTextInputs), + @"media" : @(IBGAutoMaskScreenshotOptionMedia), + @"none" : @(IBGAutoMaskScreenshotOptionMaskNothing) + }; +} @end diff --git a/ios/RNInstabug/InstabugAPMBridge.h b/ios/RNInstabug/InstabugAPMBridge.h index b15788d50..0a0ea397c 100644 --- a/ios/RNInstabug/InstabugAPMBridge.h +++ b/ios/RNInstabug/InstabugAPMBridge.h @@ -2,7 +2,7 @@ #import #import #import -#import +#import @interface InstabugAPMBridge : RCTEventEmitter /* diff --git a/ios/RNInstabug/InstabugAPMBridge.m b/ios/RNInstabug/InstabugAPMBridge.m index 0324be4a8..c28c7f425 100644 --- a/ios/RNInstabug/InstabugAPMBridge.m +++ b/ios/RNInstabug/InstabugAPMBridge.m @@ -1,13 +1,14 @@ #import "InstabugAPMBridge.h" -#import -#import +#import +#import #import #import #import -#import +#import #import +#import "Util/IBGAPM+PrivateAPIs.h" @implementation InstabugAPMBridge @@ -35,33 +36,34 @@ - (id) init return self; } +// Pauses the current thread for 3 seconds. RCT_EXPORT_METHOD(ibgSleep) { [NSThread sleepForTimeInterval:3.0f]; - // for (int i = 1; i <= 1000000; i++) - // { - // double value = sqrt(i); - - // printf("%@", [NSNumber numberWithDouble:value]); - // } - // [NSThread sleepForTimeInterval:3.0f]; } +// Enables or disables APM. RCT_EXPORT_METHOD(setEnabled:(BOOL)isEnabled) { IBGAPM.enabled = isEnabled; } +// Determines either coldAppLaunch is enabled or not. RCT_EXPORT_METHOD(setAppLaunchEnabled:(BOOL)isEnabled) { IBGAPM.coldAppLaunchEnabled = isEnabled; } +// This method is used to signal the end of the app launch process. RCT_EXPORT_METHOD(endAppLaunch) { [IBGAPM endAppLaunch]; } +// Controls whether automatic tracing of UI interactions is enabled or disabled within the SDK. RCT_EXPORT_METHOD(setAutoUITraceEnabled:(BOOL)isEnabled) { IBGAPM.autoUITraceEnabled = isEnabled; } +// Starts new execution trace with the specified `name`. +// +// Deprecated see [startFlow: (NSString *)name] RCT_EXPORT_METHOD(startExecutionTrace:(NSString *)name :(NSString *)id :(RCTPromiseResolveBlock)resolve :(RCTPromiseRejectBlock)reject) { @@ -74,6 +76,9 @@ - (id) init } } +// Sets a user defined attribute for the execution trace. +// +// Deprecated see [setFlowAttribute:(NSString *)name :(NSString *)key :(NSString *_Nullable)value] RCT_EXPORT_METHOD(setExecutionTraceAttribute:(NSString *)id :(NSString *)key :(NSString *)value) { IBGExecutionTrace *trace = [traces objectForKey:id]; if (trace != nil) { @@ -81,6 +86,9 @@ - (id) init } } +// Ends execution trace with the specified `name`. +// +// Deprecated see [endFlow: (NSString *)name] RCT_EXPORT_METHOD(endExecutionTrace:(NSString *)id) { IBGExecutionTrace *trace = [traces objectForKey:id]; if (trace != nil) { @@ -88,28 +96,38 @@ - (id) init } } +// Starts a flow trace with the specified `name`, +// allowing the SDK to capture and analyze the flow of execution within the application. RCT_EXPORT_METHOD(startFlow: (NSString *)name) { [IBGAPM startFlowWithName:name]; } +// Ends a flow with the specified `name`. RCT_EXPORT_METHOD(endFlow: (NSString *)name) { [IBGAPM endFlowWithName:name]; } +// Sets a user defined attribute for the currently active flow. RCT_EXPORT_METHOD(setFlowAttribute:(NSString *)name :(NSString *)key :(NSString *_Nullable)value) { [IBGAPM setAttributeForFlowWithName:name key:key value:value]; } +// Starts a new `UITrace` with the provided `name` parameter, +// allowing the SDK to capture and analyze the UI components within the application. RCT_EXPORT_METHOD(startUITrace:(NSString *)name) { [IBGAPM startUITraceWithName:name]; } +// Terminates the currently active UI trace. RCT_EXPORT_METHOD(endUITrace) { [IBGAPM endUITrace]; } + + + @synthesize description; @synthesize hash; diff --git a/ios/RNInstabug/InstabugBugReportingBridge.h b/ios/RNInstabug/InstabugBugReportingBridge.h index ffb393a0a..343016d36 100644 --- a/ios/RNInstabug/InstabugBugReportingBridge.h +++ b/ios/RNInstabug/InstabugBugReportingBridge.h @@ -9,8 +9,8 @@ #import #import #import -#import -#import +#import +#import @interface InstabugBugReportingBridge : RCTEventEmitter /* @@ -51,4 +51,10 @@ - (void)setCommentMinimumCharacterCount:(NSNumber *)limit reportTypes:(NSArray *)reportTypes; +- (void)addUserConsent:(NSString *)key + description:(NSString *)description + mandatory:(BOOL)mandatory + checked:(BOOL)checked + actionType:(id)actionType; + @end diff --git a/ios/RNInstabug/InstabugBugReportingBridge.m b/ios/RNInstabug/InstabugBugReportingBridge.m index 488a0c989..75e058eb7 100644 --- a/ios/RNInstabug/InstabugBugReportingBridge.m +++ b/ios/RNInstabug/InstabugBugReportingBridge.m @@ -7,7 +7,7 @@ // #import "InstabugBugReportingBridge.h" -#import +#import #import #import #import @@ -59,7 +59,7 @@ + (BOOL)requiresMainQueueSetup RCT_EXPORT_METHOD(setOnSDKDismissedHandler:(RCTResponseSenderBlock)callBack) { if (callBack != nil) { IBGBugReporting.didDismissHandler = ^(IBGDismissType dismissType, IBGReportType reportType) { - + //parse dismiss type enum NSString* dismissTypeString; if (dismissType == IBGDismissTypeCancel) { @@ -69,7 +69,7 @@ + (BOOL)requiresMainQueueSetup } else if (dismissType == IBGDismissTypeAddAttachment) { dismissTypeString = @"ADD_ATTACHMENT"; } - + //parse report type enum NSString* reportTypeString; if (reportType == IBGReportTypeBug) { @@ -90,9 +90,9 @@ + (BOOL)requiresMainQueueSetup RCT_EXPORT_METHOD(setDidSelectPromptOptionHandler:(RCTResponseSenderBlock)callBack) { if (callBack != nil) { - + IBGBugReporting.didSelectPromptOptionHandler = ^(IBGPromptOption promptOption) { - + NSString *promptOptionString; if (promptOption == IBGPromptOptionBug) { promptOptionString = @"bug"; @@ -103,7 +103,7 @@ + (BOOL)requiresMainQueueSetup } else { promptOptionString = @"none"; } - + [self sendEventWithName:@"IBGDidSelectPromptOptionHandler" body:@{ @"promptOption": promptOptionString }]; @@ -123,11 +123,11 @@ + (BOOL)requiresMainQueueSetup RCT_EXPORT_METHOD(setOptions:(NSArray*)invocationOptionsArray) { IBGBugReportingOption invocationOptions = 0; - + for (NSNumber *boxedValue in invocationOptionsArray) { invocationOptions |= [boxedValue intValue]; } - + IBGBugReporting.bugReportingOptions = invocationOptions; } @@ -157,7 +157,7 @@ + (BOOL)requiresMainQueueSetup if(screenRecording) { attachmentTypes |= IBGAttachmentTypeScreenRecording; } - + IBGBugReporting.enabledAttachmentTypes = attachmentTypes; } @@ -219,6 +219,21 @@ - (void) showBugReportingWithReportTypeAndOptionsHelper:(NSArray*)args { [IBGBugReporting setCommentMinimumCharacterCountForReportTypes:parsedReportTypes withLimit:limit.intValue]; } +RCT_EXPORT_METHOD(addUserConsent:(NSString *)key + description:(NSString *)description + mandatory:(BOOL)mandatory + checked:(BOOL)checked + actionType:(id)actionType) { + IBGActionType mappedActionType = (IBGActionType)[actionType integerValue]; + + [IBGBugReporting addUserConsentWithKey:key + description:description + mandatory:mandatory + checked:checked + actionType:mappedActionType]; +} + + @synthesize description; @synthesize hash; diff --git a/ios/RNInstabug/InstabugCrashReportingBridge.h b/ios/RNInstabug/InstabugCrashReportingBridge.h index 0dbb7f59f..7da8c187a 100644 --- a/ios/RNInstabug/InstabugCrashReportingBridge.h +++ b/ios/RNInstabug/InstabugCrashReportingBridge.h @@ -1,10 +1,10 @@ #import #import #import -#import -#import -#import -#import +#import +#import +#import +#import #import "ArgsRegistry.h" @interface InstabugCrashReportingBridge : RCTEventEmitter diff --git a/ios/RNInstabug/InstabugCrashReportingBridge.m b/ios/RNInstabug/InstabugCrashReportingBridge.m index c73a85d5b..9f4cdab55 100644 --- a/ios/RNInstabug/InstabugCrashReportingBridge.m +++ b/ios/RNInstabug/InstabugCrashReportingBridge.m @@ -29,6 +29,7 @@ + (BOOL)requiresMainQueueSetup RCT_EXPORT_METHOD(sendJSCrash:(NSDictionary *)stackTrace resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0ul); dispatch_async(queue, ^{ [IBGCrashReporting cp_reportFatalCrashWithStackTrace:stackTrace]; @@ -40,6 +41,14 @@ + (BOOL)requiresMainQueueSetup userAttributes:(nullable NSDictionary *)userAttributes fingerprint:(nullable NSString *)fingerprint nonFatalExceptionLevel:(IBGNonFatalLevel)nonFatalExceptionLevel resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + + if([fingerprint isKindOfClass:NSNull.class]){ + fingerprint = nil; + } + + if([userAttributes isKindOfClass:NSNull.class]){ + userAttributes = nil; + } dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul); dispatch_async(queue, ^{ [IBGCrashReporting cp_reportNonFatalCrashWithStackTrace:stackTrace level:nonFatalExceptionLevel groupingString:fingerprint userAttributes:userAttributes]; diff --git a/ios/RNInstabug/InstabugFeatureRequestsBridge.h b/ios/RNInstabug/InstabugFeatureRequestsBridge.h index fa4e9dced..0a3623431 100644 --- a/ios/RNInstabug/InstabugFeatureRequestsBridge.h +++ b/ios/RNInstabug/InstabugFeatureRequestsBridge.h @@ -9,7 +9,7 @@ #import #import #import -#import +#import @interface InstabugFeatureRequestsBridge : RCTEventEmitter /* diff --git a/ios/RNInstabug/InstabugFeatureRequestsBridge.m b/ios/RNInstabug/InstabugFeatureRequestsBridge.m index 58816ec58..5658a4fe7 100644 --- a/ios/RNInstabug/InstabugFeatureRequestsBridge.m +++ b/ios/RNInstabug/InstabugFeatureRequestsBridge.m @@ -7,11 +7,11 @@ // #import "InstabugFeatureRequestsBridge.h" -#import +#import #import #import #import -#import +#import #import @implementation InstabugFeatureRequestsBridge @@ -38,11 +38,11 @@ + (BOOL)requiresMainQueueSetup RCT_EXPORT_METHOD(setEmailFieldRequiredForFeatureRequests:(BOOL)isEmailFieldRequired forAction:(NSArray *)actionTypesArray) { IBGAction actionTypes = 0; - + for (NSNumber *boxedValue in actionTypesArray) { actionTypes |= [boxedValue intValue]; } - + [IBGFeatureRequests setEmailFieldRequired:isEmailFieldRequired forAction:actionTypes]; } diff --git a/ios/RNInstabug/InstabugNetworkLoggerBridge.h b/ios/RNInstabug/InstabugNetworkLoggerBridge.h new file mode 100644 index 000000000..532f21d23 --- /dev/null +++ b/ios/RNInstabug/InstabugNetworkLoggerBridge.h @@ -0,0 +1,44 @@ +#import +#import +#import + +typedef void (^ IBGURLRequestAsyncObfuscationCompletedHandler)(NSURLRequest * _Nonnull request); +typedef void (^IBGURLRequestResponseAsyncFilteringCompletedHandler)(BOOL keep); + +typedef NS_ENUM(NSInteger, NetworkListenerType) { + NetworkListenerTypeFiltering, + NetworkListenerTypeObfuscation, + NetworkListenerTypeBoth +}; + +@interface InstabugNetworkLoggerBridge : RCTEventEmitter + +@property NSMutableDictionary * _Nonnull requestObfuscationCompletionDictionary; +@property NSMutableDictionary * _Nonnull responseObfuscationCompletionDictionary; +@property NSMutableDictionary * _Nonnull requestFilteringCompletionDictionary; +@property NSMutableDictionary * _Nonnull responseFilteringCompletionDictionary; + +/* + +------------------------------------------------------------------------+ + | NetworkLogger Module | + +------------------------------------------------------------------------+ + */ + +- (BOOL)isNativeInterceptionEnabled; + +- (void) registerNetworkLogsListener:(NetworkListenerType)listenerType; + +- (void)updateNetworkLogSnapshot:(NSString * _Nonnull)url + callbackID:(NSString * _Nonnull)callbackID + requestBody:(NSString * _Nullable)requestBody + responseBody:(NSString * _Nullable)responseBody + responseCode:(double)responseCode + requestHeaders:(NSDictionary * _Nullable)requestHeaders + responseHeaders:(NSDictionary * _Nullable)responseHeaders; + +- (void) setNetworkLoggingRequestFilterPredicateIOS:(NSString * _Nonnull) callbackID : (BOOL)value; + +- (void)forceStartNetworkLoggingIOS; + +- (void)forceStopNetworkLoggingIOS; +@end diff --git a/ios/RNInstabug/InstabugNetworkLoggerBridge.m b/ios/RNInstabug/InstabugNetworkLoggerBridge.m new file mode 100644 index 000000000..2a2ddeb97 --- /dev/null +++ b/ios/RNInstabug/InstabugNetworkLoggerBridge.m @@ -0,0 +1,206 @@ +// +// InstabugNetworkLoggerBridge.m +// RNInstabug +// +// Created by Andrew Amin on 01/10/2024. +// +#import "InstabugNetworkLoggerBridge.h" +#import "Util/IBGNetworkLogger+CP.h" + +#import +#import + +// Extend RCTConvert to handle NetworkListenerType enum conversion +@implementation RCTConvert (NetworkListenerType) + +// The RCT_ENUM_CONVERTER macro handles the conversion between JS values (Int) and Objective-C enum values +RCT_ENUM_CONVERTER(NetworkListenerType, (@{ + @"filtering": @(NetworkListenerTypeFiltering), + @"obfuscation": @(NetworkListenerTypeObfuscation), + @"both": @(NetworkListenerTypeBoth) +}), NetworkListenerTypeFiltering, integerValue) + +@end + +@implementation InstabugNetworkLoggerBridge + + +- (instancetype)init { + self = [super init]; + if (self) { + _requestObfuscationCompletionDictionary = [[NSMutableDictionary alloc] init]; + _responseObfuscationCompletionDictionary = [[NSMutableDictionary alloc] init]; + _requestFilteringCompletionDictionary = [[NSMutableDictionary alloc] init]; + _responseFilteringCompletionDictionary = [[NSMutableDictionary alloc] init]; + } + return self; +} + +- (dispatch_queue_t)methodQueue { + return dispatch_get_main_queue(); +} + ++ (BOOL)requiresMainQueueSetup +{ + return NO; +} + +- (NSArray *)supportedEvents { + return @[ + @"IBGpreInvocationHandler", + @"IBGNetworkLoggerHandler" + ]; +} +RCT_EXPORT_MODULE(IBGNetworkLogger) + +bool hasListeners = NO; + + + +// Will be called when this module's first listener is added. +-(void)startObserving { + hasListeners = YES; + // Set up any upstream listeners or background tasks as necessary +} + +// Will be called when this module's last listener is removed, or on dealloc. +-(void)stopObserving { + hasListeners = NO; + // Remove upstream listeners, stop unnecessary background tasks +} + +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(isNativeInterceptionEnabled) { + return @(IBGNetworkLogger.isNativeNetworkInterceptionFeatureEnabled); +} + + + +RCT_EXPORT_METHOD(registerNetworkLogsListener: (NetworkListenerType) listenerType) { + switch (listenerType) { + case NetworkListenerTypeFiltering: + [self setupRequestFilteringHandler]; + break; + + case NetworkListenerTypeObfuscation: + [self setupRequestObfuscationHandler]; + break; + + case NetworkListenerTypeBoth: + // The obfuscation handler sends additional data to the JavaScript side. If filtering is applied, the request will be ignored; otherwise, it will be obfuscated and saved in the database. + [self setupRequestObfuscationHandler]; + break; + + default: + NSLog(@"Unknown NetworkListenerType"); + break; + } +} + + +RCT_EXPORT_METHOD(updateNetworkLogSnapshot:(NSString * _Nonnull)url + callbackID:(NSString * _Nonnull)callbackID + requestBody:(NSString * _Nullable)requestBody + responseBody:(NSString * _Nullable)responseBody + responseCode:(double)responseCode + requestHeaders:(NSDictionary * _Nullable)requestHeaders + responseHeaders:(NSDictionary * _Nullable)responseHeaders) +{ + // Validate and construct the URL + NSURL *requestURL = [NSURL URLWithString:url]; + if (!requestURL) { + NSLog(@"Invalid URL: %@", url); + return; + } + + // Initialize the NSMutableURLRequest + NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:requestURL]; + + // Set the HTTP body if provided + if (requestBody && [requestBody isKindOfClass:[NSString class]]) { + request.HTTPBody = [requestBody dataUsingEncoding:NSUTF8StringEncoding]; + } + + // Ensure requestHeaders is a valid dictionary before setting it + if (requestHeaders && [requestHeaders isKindOfClass:[NSDictionary class]]) { + request.allHTTPHeaderFields = requestHeaders; + } else { + NSLog(@"Invalid requestHeaders format, expected NSDictionary."); + } + + // Ensure callbackID is valid and the completion handler exists + IBGURLRequestAsyncObfuscationCompletedHandler completionHandler = self.requestObfuscationCompletionDictionary[callbackID]; + if (callbackID && [callbackID isKindOfClass:[NSString class]] && completionHandler) { + // Call the completion handler with the constructed request + completionHandler(request); + } else { + NSLog(@"CallbackID not found or completion handler is unavailable for CallbackID: %@", callbackID); + } +} + +RCT_EXPORT_METHOD(setNetworkLoggingRequestFilterPredicateIOS: (NSString * _Nonnull) callbackID : (BOOL)value ){ + + if (self.requestFilteringCompletionDictionary[callbackID] != nil) { + // ⬇️ YES == Request will be saved, NO == will be ignored + ((IBGURLRequestResponseAsyncFilteringCompletedHandler)self.requestFilteringCompletionDictionary[callbackID])(value); + } else { + NSLog(@"Not Available Completion"); + } +} + + +#pragma mark - Helper Methods + +// Set up the filtering handler +- (void)setupRequestFilteringHandler { + [IBGNetworkLogger setCPRequestFilteringHandler:^(NSURLRequest * _Nonnull request, void (^ _Nonnull completion)(BOOL)) { + NSString *callbackID = [[[NSUUID alloc] init] UUIDString]; + self.requestFilteringCompletionDictionary[callbackID] = completion; + + NSDictionary *dict = [self createNetworkRequestDictForRequest:request callbackID:callbackID]; + if(hasListeners){ + [self sendEventWithName:@"IBGNetworkLoggerHandler" body:dict]; + } + + }]; +} + +// Set up the obfuscation handler +- (void)setupRequestObfuscationHandler { + [IBGNetworkLogger setCPRequestAsyncObfuscationHandler:^(NSURLRequest * _Nonnull request, void (^ _Nonnull completion)(NSURLRequest * _Nonnull)) { + NSString *callbackID = [[[NSUUID alloc] init] UUIDString]; + self.requestObfuscationCompletionDictionary[callbackID] = completion; + + + NSDictionary *dict = [self createNetworkRequestDictForRequest:request callbackID:callbackID]; + if (hasListeners) { + [self sendEventWithName:@"IBGNetworkLoggerHandler" body:dict]; + } + + }]; +} + +// Helper to create a dictionary from the request and callbackID +- (NSDictionary *)createNetworkRequestDictForRequest:(NSURLRequest *)request callbackID:(NSString *)callbackID { + NSString *urlString = request.URL.absoluteString ?: @""; + NSString *bodyString = [[NSString alloc] initWithData:request.HTTPBody encoding:NSUTF8StringEncoding] ?: @""; + NSDictionary *headerDict = request.allHTTPHeaderFields ?: @{}; + + return @{ + @"id": callbackID, + @"url": urlString, + @"requestBody": bodyString, + @"requestHeader": headerDict + }; +} + +RCT_EXPORT_METHOD(forceStartNetworkLoggingIOS) { + [IBGNetworkLogger forceStartNetworkLogging]; +} + +RCT_EXPORT_METHOD(forceStopNetworkLoggingIOS) { + [IBGNetworkLogger forceStopNetworkLogging]; +} + + + +@end diff --git a/ios/RNInstabug/InstabugReactBridge.h b/ios/RNInstabug/InstabugReactBridge.h index a3cfc21c1..45c075098 100644 --- a/ios/RNInstabug/InstabugReactBridge.h +++ b/ios/RNInstabug/InstabugReactBridge.h @@ -9,11 +9,11 @@ #import #import #import -#import -#import -#import -#import -#import +#import +#import +#import +#import +#import #import "ArgsRegistry.h" @interface InstabugReactBridge : RCTEventEmitter @@ -26,7 +26,8 @@ - (void)setEnabled:(BOOL)isEnabled; -- (void)init:(NSString *)token invocationEvents:(NSArray *)invocationEventsArray debugLogsLevel:(IBGSDKDebugLogsLevel)sdkDebugLogsLevel useNativeNetworkInterception:(BOOL)useNativeNetworkInterception codePushVersion:(NSString *)codePushVersion; +- (void)init:(NSString *)token invocationEvents:(NSArray *)invocationEventsArray debugLogsLevel:(IBGSDKDebugLogsLevel)sdkDebugLogsLevel useNativeNetworkInterception:(BOOL)useNativeNetworkInterception codePushVersion:(NSString *)codePushVersion +options:(nullable NSDictionary *)options; - (void)setCodePushVersion:(NSString *)version; @@ -105,7 +106,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 +124,8 @@ startTime:(double)startTime duration:(double)duration gqlQueryName:(NSString * _Nullable)gqlQueryName - serverErrorMessage:(NSString * _Nullable)serverErrorMessage; + serverErrorMessage:(NSString * _Nullable)serverErrorMessage +w3cExternalTraceAttributes:(NSDictionary * _Nullable)w3cExternalTraceAttributes; /* +------------------------------------------------------------------------+ @@ -135,4 +139,8 @@ - (void)addFeatureFlags:(NSDictionary *)featureFlagsMap; - (void)removeFeatureFlags:(NSArray *)featureFlags; - (void)removeAllFeatureFlags; +- (void)setNetworkLogBodyEnabled:(BOOL)isEnabled; +- (void)enableAutoMasking:(NSArray *)autoMaskingTypes; +- (void)getNetworkBodyMaxSize:(RCTPromiseResolveBlock)resolve :(RCTPromiseRejectBlock)reject; + @end diff --git a/ios/RNInstabug/InstabugReactBridge.m b/ios/RNInstabug/InstabugReactBridge.m index 534b84908..682896515 100644 --- a/ios/RNInstabug/InstabugReactBridge.m +++ b/ios/RNInstabug/InstabugReactBridge.m @@ -5,11 +5,11 @@ // Created by Yousef Hamza on 9/29/16. #import "InstabugReactBridge.h" -#import -#import -#import -#import -#import +#import +#import +#import +#import +#import #import #import #import @@ -23,7 +23,7 @@ + (void)setWillSendReportHandler_private:(void(^)(IBGReport *report, void(^repor @implementation InstabugReactBridge - (NSArray *)supportedEvents { - return @[@"IBGpreSendingHandler"]; + return @[@"IBGpreSendingHandler" , @"IBGNetworkLoggerHandler"]; } RCT_EXPORT_MODULE(Instabug) @@ -41,7 +41,9 @@ - (dispatch_queue_t)methodQueue { invocationEvents:(NSArray *)invocationEventsArray debugLogsLevel:(IBGSDKDebugLogsLevel)sdkDebugLogsLevel useNativeNetworkInterception:(BOOL)useNativeNetworkInterception - codePushVersion:(NSString *)codePushVersion) { + codePushVersion:(NSString *)codePushVersion + options:(nullable NSDictionary *)options + ) { IBGInvocationEvent invocationEvents = 0; for (NSNumber *boxedValue in invocationEventsArray) { @@ -298,7 +300,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 +324,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 +379,7 @@ - (dispatch_queue_t)methodQueue { [featureFlags addObject:[[IBGFeatureFlag alloc] initWithName:key variant:variant]]; } } - + [Instabug addFeatureFlags:featureFlags]; } @@ -378,7 +388,7 @@ - (dispatch_queue_t)methodQueue { for(id item in featureFlags){ [features addObject:[[IBGFeatureFlag alloc] initWithName:item]]; } - + @try { [Instabug removeFeatureFlags:features]; } @@ -395,6 +405,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; } @@ -421,4 +442,24 @@ + (BOOL)iOSVersionIsLessThan:(NSString *)iOSVersion { return [iOSVersion compare:[UIDevice currentDevice].systemVersion options:NSNumericSearch] == NSOrderedDescending; }; +RCT_EXPORT_METHOD(enableAutoMasking:(NSArray *)autoMaskingTypes) { + + IBGAutoMaskScreenshotOption autoMaskingOptions = 0; + + for (NSNumber *event in autoMaskingTypes) { + + autoMaskingOptions |= [event intValue]; + } + + [Instabug setAutoMaskScreenshots: autoMaskingOptions]; +}; + +RCT_EXPORT_METHOD(getNetworkBodyMaxSize:(RCTPromiseResolveBlock)resolve :(RCTPromiseRejectBlock)reject) { + resolve(@(IBGNetworkLogger.getNetworkBodyMaxSize)); +} + +RCT_EXPORT_METHOD(setNetworkLogBodyEnabled:(BOOL)isEnabled) { + IBGNetworkLogger.logBodyEnabled = isEnabled; +} + @end diff --git a/ios/RNInstabug/InstabugRepliesBridge.h b/ios/RNInstabug/InstabugRepliesBridge.h index 2f33e28f8..a6ffa6811 100644 --- a/ios/RNInstabug/InstabugRepliesBridge.h +++ b/ios/RNInstabug/InstabugRepliesBridge.h @@ -10,7 +10,7 @@ #import #import #import -#import +#import @interface InstabugRepliesBridge : RCTEventEmitter /* diff --git a/ios/RNInstabug/InstabugRepliesBridge.m b/ios/RNInstabug/InstabugRepliesBridge.m index 1006f1ece..16a1e0cbe 100644 --- a/ios/RNInstabug/InstabugRepliesBridge.m +++ b/ios/RNInstabug/InstabugRepliesBridge.m @@ -8,11 +8,11 @@ // #import "InstabugRepliesBridge.h" -#import +#import #import #import #import -#import +#import #import @implementation InstabugRepliesBridge @@ -39,7 +39,7 @@ + (BOOL)requiresMainQueueSetup RCT_EXPORT_METHOD(hasChats:(RCTPromiseResolveBlock)resolve :(RCTPromiseRejectBlock)reject) { BOOL hasChats = IBGReplies.hasChats; resolve(@(hasChats)); - + } RCT_EXPORT_METHOD(show) { @@ -54,7 +54,7 @@ + (BOOL)requiresMainQueueSetup } else { IBGReplies.didReceiveReplyHandler = nil; } - + } RCT_EXPORT_METHOD(getUnreadRepliesCount:(RCTPromiseResolveBlock)resolve :(RCTPromiseRejectBlock)reject) { diff --git a/ios/RNInstabug/InstabugSessionReplayBridge.h b/ios/RNInstabug/InstabugSessionReplayBridge.h index 259ea1c14..113342bb2 100644 --- a/ios/RNInstabug/InstabugSessionReplayBridge.h +++ b/ios/RNInstabug/InstabugSessionReplayBridge.h @@ -1,8 +1,8 @@ #import #import #import -#import -#import +#import +#import @interface InstabugSessionReplayBridge : RCTEventEmitter /* diff --git a/ios/RNInstabug/InstabugSessionReplayBridge.m b/ios/RNInstabug/InstabugSessionReplayBridge.m index 2c865ecb5..fd64edf37 100644 --- a/ios/RNInstabug/InstabugSessionReplayBridge.m +++ b/ios/RNInstabug/InstabugSessionReplayBridge.m @@ -1,9 +1,9 @@ #import #import #import -#import +#import #import -#import +#import #import "InstabugSessionReplayBridge.h" @implementation InstabugSessionReplayBridge @@ -50,11 +50,11 @@ + (BOOL)requiresMainQueueSetup - (NSArray *)getNetworkLogsArray: (NSArray*) networkLogs { NSMutableArray *networkLogsArray = [NSMutableArray array]; - + for (IBGSessionMetadataNetworkLogs* log in networkLogs) { NSDictionary *nLog = @{@"url": log.url, @"statusCode": @(log.statusCode), @"duration": @(log.duration)}; [networkLogsArray addObject:nLog]; - } + } return networkLogsArray; } @@ -76,22 +76,22 @@ - (NSDictionary *)getMetadataObjectMap:(IBGSessionMetadata *)metadataObject { RCT_EXPORT_METHOD(setSyncCallback) { [IBGSessionReplay setSyncCallbackWithHandler:^(IBGSessionMetadata * _Nonnull metadataObject, SessionEvaluationCompletion _Nonnull completion) { - + [self sendEventWithName:@"IBGSessionReplayOnSyncCallback" body:[self getMetadataObjectMap:metadataObject]]; - + self.sessionEvaluationCompletion = completion; }]; } RCT_EXPORT_METHOD(evaluateSync:(BOOL)result) { - + if (self.sessionEvaluationCompletion) { - + self.sessionEvaluationCompletion(result); - + self.sessionEvaluationCompletion = nil; - + } } diff --git a/ios/RNInstabug/InstabugSurveysBridge.h b/ios/RNInstabug/InstabugSurveysBridge.h index fe483130e..e1ca5058d 100644 --- a/ios/RNInstabug/InstabugSurveysBridge.h +++ b/ios/RNInstabug/InstabugSurveysBridge.h @@ -9,7 +9,7 @@ #import #import #import -#import +#import @interface InstabugSurveysBridge : RCTEventEmitter /* diff --git a/ios/RNInstabug/InstabugSurveysBridge.m b/ios/RNInstabug/InstabugSurveysBridge.m index 465970764..6beeae393 100644 --- a/ios/RNInstabug/InstabugSurveysBridge.m +++ b/ios/RNInstabug/InstabugSurveysBridge.m @@ -7,11 +7,11 @@ // #import "InstabugSurveysBridge.h" -#import +#import #import #import #import -#import +#import #import @implementation InstabugSurveysBridge diff --git a/ios/RNInstabug/RCTConvert+InstabugEnums.m b/ios/RNInstabug/RCTConvert+InstabugEnums.m index 3e675ca2f..4d4989b60 100644 --- a/ios/RNInstabug/RCTConvert+InstabugEnums.m +++ b/ios/RNInstabug/RCTConvert+InstabugEnums.m @@ -7,7 +7,7 @@ // #import "RCTConvert+InstabugEnums.h" -#import +#import @implementation RCTConvert (InstabugEnums) @@ -109,4 +109,19 @@ @implementation RCTConvert (InstabugEnums) integerValue ); +RCT_ENUM_CONVERTER( + IBGAutoMaskScreenshotOption, + ArgsRegistry.autoMaskingTypes, + IBGAutoMaskScreenshotOptionMaskNothing, + integerValue +); + +RCT_ENUM_CONVERTER( + IBGActionType, + ArgsRegistry.userConsentActionTypes, + IBGActionTypeNoChat, + integerValue +); + @end + diff --git a/ios/RNInstabug/RNInstabug.h b/ios/RNInstabug/RNInstabug.h index 91eaea84d..70612fef7 100644 --- a/ios/RNInstabug/RNInstabug.h +++ b/ios/RNInstabug/RNInstabug.h @@ -1,7 +1,7 @@ #ifndef RNInstabug_h #define RNInstabug_h -#import +#import @interface RNInstabug : NSObject @@ -18,11 +18,11 @@ useNativeNetworkInterception:(BOOL)useNativeNetworkInterception; /** @brief Set codePush version before starting the SDK. - + @discussion Sets Code Push version to be used for all reports. should be called from `-[UIApplicationDelegate application:didFinishLaunchingWithOptions:]` and before `startWithToken`. - + @param codePushVersion the Code Push version to be used for all reports. */ + (void)setCodePushVersion:(NSString *)codePushVersion; diff --git a/ios/RNInstabug/RNInstabug.m b/ios/RNInstabug/RNInstabug.m index 275d0095b..3ea51ae59 100644 --- a/ios/RNInstabug/RNInstabug.m +++ b/ios/RNInstabug/RNInstabug.m @@ -1,4 +1,4 @@ -#import +#import #import #import "RNInstabug.h" #import "Util/IBGNetworkLogger+CP.h" diff --git a/ios/RNInstabug/Util/IBGAPM+PrivateAPIs.h b/ios/RNInstabug/Util/IBGAPM+PrivateAPIs.h new file mode 100644 index 000000000..a451a0ad5 --- /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/IBGCrashReporting+CP.h b/ios/RNInstabug/Util/IBGCrashReporting+CP.h index 4229dbcea..cf3a1c200 100644 --- a/ios/RNInstabug/Util/IBGCrashReporting+CP.h +++ b/ios/RNInstabug/Util/IBGCrashReporting+CP.h @@ -1,4 +1,4 @@ -#import +#import @interface IBGCrashReporting (CP) diff --git a/ios/RNInstabug/Util/IBGNetworkLogger+CP.h b/ios/RNInstabug/Util/IBGNetworkLogger+CP.h index 436553620..b5e923f84 100644 --- a/ios/RNInstabug/Util/IBGNetworkLogger+CP.h +++ b/ios/RNInstabug/Util/IBGNetworkLogger+CP.h @@ -1,9 +1,15 @@ -#import +#import 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; + +@property (class, atomic, assign) BOOL isNativeNetworkInterceptionFeatureEnabled; + + (void)disableAutomaticCapturingOfNetworkLogs; + (void)addNetworkLogWithUrl:(NSString *_Nonnull)url method:(NSString *_Nonnull)method @@ -27,6 +33,36 @@ 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; + ++ (void)forceStartNetworkLogging; ++ (void)forceStopNetworkLogging; + ++ (void)setCPRequestAsyncObfuscationHandler:(void (^)(NSURLRequest * requestToBeObfuscated, void (^ completion)(NSURLRequest * obfuscatedRequest)))asyncObfuscationHandler; ++ (void)setCPRequestFilteringHandler:(void (^)(NSURLRequest * request, void (^completion)(BOOL keep)))requestFilteringHandler; ++ (void)setCPResponseFilteringHandler:(void (^)(NSURLResponse * response, void (^comppletion)(BOOL keep)))responseFilteringHandler; ++ (double)getNetworkBodyMaxSize; + @end NS_ASSUME_NONNULL_END diff --git a/ios/RNInstabug/Util/Instabug+CP.h b/ios/RNInstabug/Util/Instabug+CP.h index 8666413f0..2cc35a4b7 100644 --- a/ios/RNInstabug/Util/Instabug+CP.h +++ b/ios/RNInstabug/Util/Instabug+CP.h @@ -1,5 +1,5 @@ -#import -#import +#import +#import NS_ASSUME_NONNULL_BEGIN diff --git a/ios/native.rb b/ios/native.rb index 697052141..41f497687 100644 --- a/ios/native.rb +++ b/ios/native.rb @@ -1,4 +1,4 @@ -$instabug = { :version => '14.0.0' } +$instabug = { :version => '15.1.1' } def use_instabug! (spec = nil) version = $instabug[:version] diff --git a/package.json b/package.json index 75005c28b..61af51056 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": "15.0.2", "author": "Instabug (https://instabug.com)", "repository": "github:Instabug/Instabug-React-Native", "homepage": "https://www.instabug.com/platforms/react-native", diff --git a/scripts/customize-ios-endpoints.sh b/scripts/customize-ios-endpoints.sh index 2fecd9d72..2b790cd5f 100755 --- a/scripts/customize-ios-endpoints.sh +++ b/scripts/customize-ios-endpoints.sh @@ -13,11 +13,11 @@ if [ ! -f $instabug_plist ]; then exit 1 fi -for dir in examples/default/ios/Pods/Instabug/Instabug.xcframework/ios-*/ +for dir in examples/default/ios/Pods/Instabug/InstabugSDK.xcframework/ios-*/ do echo "Replacing Config.plist in $dir" - config_path=$dir/Instabug.framework/InstabugResources.bundle/Config.plist + config_path=$dir/InstabugSDK.framework/InstabugResources.bundle/Config.plist if [ ! -f $config_path ]; then echo "Config.plist not found in $dir" diff --git a/scripts/find-token.sh b/scripts/find-token.sh index 3b162def9..ded75b85f 100644 --- a/scripts/find-token.sh +++ b/scripts/find-token.sh @@ -1,14 +1,27 @@ #!/bin/sh # Searches for app token within source files. +JSON_APP_TOKEN=$( + grep "app_token" -r -A 1 -m 1 --exclude-dir={node_modules,ios,android} --include=instabug.json ./ | + sed 's/[[:space:]]//g' | + grep -o ":[\"\'][0-9a-zA-Z]*[\"\']" | + cut -d ":" -f 2 | + cut -d "\"" -f 2 | + cut -d "'" -f 2 +) + +if [ ! -z "${JSON_APP_TOKEN}" ]; then + echo $JSON_APP_TOKEN + exit 0 +fi INIT_APP_TOKEN=$( grep "Instabug.init({" -r -A 6 -m 1 --exclude-dir={node_modules,ios,android} --include=\*.{js,ts,jsx,tsx} ./ | - grep "token:[[:space:]]*[\"\'][0-9a-zA-Z]*[\"\']" | + grep "token[[:space:]]*:[[:space:]]*[\"\'][0-9a-zA-Z]*[\"\']" | grep -o "[\"\'][0-9a-zA-Z]*[\"\']" | cut -d "\"" -f 2 | cut -d "'" -f 2 -) +) if [ ! -z "${INIT_APP_TOKEN}" ]; then echo $INIT_APP_TOKEN @@ -20,12 +33,38 @@ START_APP_TOKEN=$( grep -o "[\"\'][0-9a-zA-Z]*[\"\']" | cut -d "\"" -f 2 | cut -d "'" -f 2 -) +) if [ ! -z "${START_APP_TOKEN}" ]; then echo $START_APP_TOKEN exit 0 fi +ENV_APP_TOKEN=$( + grep "INSTABUG_APP_TOKEN" -r -A 1 -m 1 --exclude-dir={node_modules,ios,android} --include=\*.env ./ | + sed 's/[[:space:]]//g' | + grep -o "INSTABUG_APP_TOKEN=.*" | + cut -d "=" -f 2 +) + +if [ ! -z "${ENV_APP_TOKEN}" ]; then + echo $ENV_APP_TOKEN + exit 0 +fi + +CONSTANTS_APP_TOKEN=$( + grep "INSTABUG_APP_TOKEN" -r -A 1 -m 1 --exclude-dir={node_modules,ios,android} --include=\*.{js,ts,jsx,tsx} ./ | + sed 's/[[:space:]]//g' | + grep -o "=[\"\'][0-9a-zA-Z]*[\"\']" | + cut -d "=" -f 2 | + cut -d "\"" -f 2 | + cut -d "'" -f 2 +) + +if [ ! -z "${CONSTANTS_APP_TOKEN}" ]; then + echo $CONSTANTS_APP_TOKEN + exit 0 +fi + echo "Couldn't find Instabug's app token" exit 1 diff --git a/src/models/InstabugConfig.ts b/src/models/InstabugConfig.ts index 614ade892..af1d6e841 100644 --- a/src/models/InstabugConfig.ts +++ b/src/models/InstabugConfig.ts @@ -19,6 +19,11 @@ export interface InstabugConfig { */ codePushVersion?: string; + /** + * An optional flag to override SDK screenshot security behavior. + */ + ignoreAndroidSecureFlag?: boolean; + /** * An optional network interception mode, this determines whether network interception * is done in the JavaScript side or in the native Android and iOS SDK side. diff --git a/src/models/W3cExternalTraceAttributes.ts b/src/models/W3cExternalTraceAttributes.ts new file mode 100644 index 000000000..f4e7ab6a4 --- /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/APM.ts b/src/modules/APM.ts index 664f4473c..92d401389 100644 --- a/src/modules/APM.ts +++ b/src/modules/APM.ts @@ -13,7 +13,8 @@ export const setEnabled = (isEnabled: boolean) => { }; /** - * Enables or disables APM App Launch + * If APM is enabled, Instabug SDK starts collecting data about the app launch time by default. + * This API is used to give user more control over this behavior. * @param isEnabled */ export const setAppLaunchEnabled = (isEnabled: boolean) => { @@ -21,7 +22,9 @@ export const setAppLaunchEnabled = (isEnabled: boolean) => { }; /** - * Ends app launch + * To define when an app launch is complete, + * such as when it's intractable, use the end app launch API. + * You can then view this data with the automatic cold app launch. */ export const endAppLaunch = () => { NativeAPM.endAppLaunch(); @@ -29,7 +32,7 @@ export const endAppLaunch = () => { /** * Enables or disables APM Network Metric - * @param isEnabled + * @param isEnabled - a boolean indicates either iOS monitoring is enabled or disabled. */ export const setNetworkEnabledIOS = (isEnabled: boolean) => { if (Platform.OS === 'ios') { @@ -114,15 +117,17 @@ export const setFlowAttribute = (name: string, key: string, value?: string | nul }; /** - * Starts a custom trace - * @param name + * Initiates a UI trace with the specified name using a native module. + * @param {string} name - The `name` parameter in the `startUITrace` function is a string that + * represents the name of the UI trace that you want to start. This name is used to identify and track + * the specific UI trace within the application. */ export const startUITrace = (name: string) => { NativeAPM.startUITrace(name); }; /** - * Ends a custom trace + * Ends the currently running custom trace. */ export const endUITrace = () => { NativeAPM.endUITrace(); diff --git a/src/modules/BugReporting.ts b/src/modules/BugReporting.ts index 2883cbc5d..486169ecc 100644 --- a/src/modules/BugReporting.ts +++ b/src/modules/BugReporting.ts @@ -10,6 +10,7 @@ import type { InvocationOption, RecordingButtonPosition, ReportType, + userConsentActionType, } from '../utils/Enums'; /** @@ -169,6 +170,23 @@ export const setViewHierarchyEnabled = (isEnabled: boolean) => { NativeBugReporting.setViewHierarchyEnabled(isEnabled); }; +/** + * Adds a user consent item to the bug reporting form. + * @param key A unique identifier string for the consent item. + * @param description The text shown to the user describing the consent item. + * @param mandatory Whether the user must agree to this item before submitting a report. + * @param checked Whether the consent checkbox is pre-selected. + * @param actionType A string representing the action type to map to SDK behavior. + */ +export const addUserConsent = ( + key: string, + description: string, + mandatory: boolean, + checked: boolean, + actionType?: userConsentActionType, +) => { + NativeBugReporting.addUserConsent(key, description, mandatory, checked, actionType); +}; /** * Sets a block of code to be executed when a prompt option is selected. * @param handler - A callback that gets executed when a prompt option is selected. diff --git a/src/modules/CrashReporting.ts b/src/modules/CrashReporting.ts index c374c7d77..858e035a3 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 1d528fba9..fd4f17600 100644 --- a/src/modules/Instabug.ts +++ b/src/modules/Instabug.ts @@ -1,5 +1,10 @@ -import type React from 'react'; -import { Platform, findNodeHandle, processColor } from 'react-native'; +import { + AppState, + type AppStateStatus, + findNodeHandle, + Platform, + processColor, +} from 'react-native'; import type { NavigationContainerRefWithCurrent, @@ -10,8 +15,10 @@ 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 { registerFeatureFlagsListener } from '../utils/FeatureFlags'; import { + AutoMaskingType, ColorTheme, Locale, LogLevel, @@ -20,17 +27,30 @@ import { StringKey, WelcomeMessageMode, } from '../utils/Enums'; -import InstabugUtils, { stringifyIfNotString } from '../utils/InstabugUtils'; +import InstabugUtils, { + checkNetworkRequestHandlers, + resetNativeObfuscationListener, + setApmNetworkFlagsIfChanged, + stringifyIfNotString, +} from '../utils/InstabugUtils'; import * as NetworkLogger from './NetworkLogger'; import { captureUnhandledRejections } from '../utils/UnhandledRejectionTracking'; import type { ReproConfig } from '../models/ReproConfig'; import type { FeatureFlag } from '../models/FeatureFlag'; +import { addAppStateListener } from '../utils/AppStatesHandler'; +import { NativeNetworkLogger } from '../native/NativeNetworkLogger'; 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; let _isFirstScreen = false; const firstScreen = 'Initial Screen'; +let _currentAppState = AppState.currentState; +let isNativeInterceptionFeatureEnabled = false; // Checks the value of "cp_native_interception_enabled" backend flag. +let hasAPMNetworkPlugin = false; // Android only: checks if the APM plugin is installed. +let shouldEnableNativeInterception = false; // For Android: used to disable APM logging inside reportNetworkLog() -> NativeAPM.networkLogAndroid(), For iOS: used to control native interception (true == enabled , false == disabled) /** * Enables or disables Instabug functionality. @@ -65,37 +85,237 @@ function reportCurrentViewForAndroid(screenName: string | null) { * @param config SDK configurations. See {@link InstabugConfig} for more info. */ export const init = (config: InstabugConfig) => { + if (Platform.OS === 'android') { + // Add android feature flags listener for android + registerFeatureFlagsListener(); + addOnFeatureUpdatedListener(config); + } else { + isNativeInterceptionFeatureEnabled = NativeNetworkLogger.isNativeInterceptionEnabled(); + + // Add app state listener to handle background/foreground transitions + addAppStateListener(async (nextAppState) => handleAppStateChange(nextAppState, config)); + + handleNetworkInterceptionMode(config); + + //Set APM networking flags for the first time + setApmNetworkFlagsIfChanged({ + isNativeInterceptionFeatureEnabled: isNativeInterceptionFeatureEnabled, + hasAPMNetworkPlugin: hasAPMNetworkPlugin, + shouldEnableNativeInterception: shouldEnableNativeInterception, + }); + } + + // call Instabug native init method + initializeNativeInstabug(config); + + // Set up error capturing and rejection handling InstabugUtils.captureJsErrors(); captureUnhandledRejections(); - // Default networkInterceptionMode to JavaScript + _isFirstScreen = true; + _currentScreen = firstScreen; + + InstabugRNConfig.debugLogsLevel = config.debugLogsLevel ?? LogLevel.error; + + reportCurrentViewForAndroid(firstScreen); + setTimeout(() => { + if (_currentScreen === firstScreen) { + NativeInstabug.reportScreenChange(firstScreen); + _currentScreen = null; + } + }, 1000); +}; + +/** + * Handles app state changes and updates APM network flags if necessary. + */ +const handleAppStateChange = async (nextAppState: AppStateStatus, config: InstabugConfig) => { + // Checks if the app has come to the foreground + if (['inactive', 'background'].includes(_currentAppState) && nextAppState === 'active') { + const isUpdated = await fetchApmNetworkFlags(); + if (isUpdated) { + refreshAPMNetworkConfigs(config); + } + } + + _currentAppState = nextAppState; +}; + +/** + * Fetches the current APM network flags. + */ +const fetchApmNetworkFlags = async () => { + let isUpdated = false; + const newNativeInterceptionFeatureEnabled = NativeNetworkLogger.isNativeInterceptionEnabled(); + if (isNativeInterceptionFeatureEnabled !== newNativeInterceptionFeatureEnabled) { + isNativeInterceptionFeatureEnabled = newNativeInterceptionFeatureEnabled; + isUpdated = true; + } + if (Platform.OS === 'android') { + const newHasAPMNetworkPlugin = await NativeNetworkLogger.hasAPMNetworkPlugin(); + if (hasAPMNetworkPlugin !== newHasAPMNetworkPlugin) { + hasAPMNetworkPlugin = newHasAPMNetworkPlugin; + isUpdated = true; + } + } + return isUpdated; +}; + +/** + * Handles platform-specific checks and updates the network interception mode. + */ +const handleNetworkInterceptionMode = (config: InstabugConfig) => { + // Default networkInterceptionMode to JavaScript if not set if (config.networkInterceptionMode == null) { config.networkInterceptionMode = NetworkInterceptionMode.javascript; } + if (Platform.OS === 'android') { + handleInterceptionModeForAndroid(config); + config.networkInterceptionMode = NetworkInterceptionMode.javascript; // Need to enable JS interceptor in all scenarios for Bugs & Crashes network logs + } else if (Platform.OS === 'ios') { + handleInterceptionModeForIOS(config); + //enable | disable native obfuscation and filtering synchronously + NetworkLogger.setNativeInterceptionEnabled(shouldEnableNativeInterception); + } + if (config.networkInterceptionMode === NetworkInterceptionMode.javascript) { NetworkLogger.setEnabled(true); } +}; + +/** + * Handles the network interception logic for Android if the user set + * network interception mode with [NetworkInterceptionMode.javascript]. + */ +function handleAndroidJSInterception() { + if (isNativeInterceptionFeatureEnabled && hasAPMNetworkPlugin) { + shouldEnableNativeInterception = true; + Logger.warn( + InstabugConstants.IBG_APM_TAG + InstabugConstants.SWITCHED_TO_NATIVE_INTERCEPTION_MESSAGE, + ); + } +} + +/** + * Handles the network interception logic for Android if the user set + * network interception mode with [NetworkInterceptionMode.native]. + */ +function handleAndroidNativeInterception() { + if (isNativeInterceptionFeatureEnabled) { + shouldEnableNativeInterception = hasAPMNetworkPlugin; + if (!hasAPMNetworkPlugin) { + Logger.error(InstabugConstants.IBG_APM_TAG + InstabugConstants.PLUGIN_NOT_INSTALLED_MESSAGE); + } + } else { + shouldEnableNativeInterception = false; // rollback to use JS interceptor for APM & Core. + Logger.error( + InstabugConstants.IBG_APM_TAG + InstabugConstants.NATIVE_INTERCEPTION_DISABLED_MESSAGE, + ); + } +} +/** + * Control either to enable or disable the native interception for iOS after the init method is called. + */ +function handleIOSNativeInterception(config: InstabugConfig) { + if ( + shouldEnableNativeInterception && + config.networkInterceptionMode === NetworkInterceptionMode.native + ) { + NativeNetworkLogger.forceStartNetworkLoggingIOS(); // Enable native iOS automatic network logging. + } else { + NativeNetworkLogger.forceStopNetworkLoggingIOS(); // Disable native iOS automatic network logging. + } +} + +/** + * Handles the network interception mode logic for Android. + * By deciding which interception mode should be enabled (Native or JavaScript). + */ +const handleInterceptionModeForAndroid = (config: InstabugConfig) => { + const { networkInterceptionMode } = config; + + if (networkInterceptionMode === NetworkInterceptionMode.javascript) { + handleAndroidJSInterception(); + } else { + handleAndroidNativeInterception(); + } +}; + +/** + * Handles the interception mode logic for iOS. + * By deciding which interception mode should be enabled (Native or JavaScript). + */ +const handleInterceptionModeForIOS = (config: InstabugConfig) => { + if (config.networkInterceptionMode === NetworkInterceptionMode.native) { + if (isNativeInterceptionFeatureEnabled) { + shouldEnableNativeInterception = true; + NetworkLogger.setEnabled(false); // insure JS interceptor is disabled + } else { + shouldEnableNativeInterception = false; + NetworkLogger.setEnabled(true); // rollback to JS interceptor + Logger.error( + InstabugConstants.IBG_APM_TAG + InstabugConstants.NATIVE_INTERCEPTION_DISABLED_MESSAGE, + ); + } + } +}; + +/** + * Initializes Instabug with the given configuration. + */ +const initializeNativeInstabug = (config: InstabugConfig) => { NativeInstabug.init( config.token, config.invocationEvents, config.debugLogsLevel ?? LogLevel.error, - config.networkInterceptionMode === NetworkInterceptionMode.native, + shouldEnableNativeInterception && + config.networkInterceptionMode === NetworkInterceptionMode.native, config.codePushVersion, + config.ignoreAndroidSecureFlag != null + ? { + ignoreAndroidSecureFlag: config.ignoreAndroidSecureFlag, + } + : undefined, ); +}; - _isFirstScreen = true; - _currentScreen = firstScreen; +/** + * Refresh the APM network configurations. + */ +function refreshAPMNetworkConfigs(config: InstabugConfig, forceRefreshIOS: boolean = true) { + handleNetworkInterceptionMode(config); + if (Platform.OS === 'ios' && forceRefreshIOS) { + handleIOSNativeInterception(config); + } + setApmNetworkFlagsIfChanged({ + isNativeInterceptionFeatureEnabled, + hasAPMNetworkPlugin, + shouldEnableNativeInterception, + }); + if (shouldEnableNativeInterception) { + checkNetworkRequestHandlers(); + } else { + // remove any attached [NativeNetworkLogger] Listeners if exists, to avoid memory leaks. + resetNativeObfuscationListener(); + } +} - reportCurrentViewForAndroid(firstScreen); - setTimeout(() => { - if (_currentScreen === firstScreen) { - NativeInstabug.reportScreenChange(firstScreen); - _currentScreen = null; - } - }, 1000); -}; +/** + * Add Android Listener for native feature flags changes. + */ +function addOnFeatureUpdatedListener(config: InstabugConfig) { + emitter.addListener(NativeEvents.IBG_ON_FEATURES_UPDATED_CALLBACK, (flags) => { + const { cpNativeInterceptionEnabled, hasAPMPlugin } = flags; + isNativeInterceptionFeatureEnabled = cpNativeInterceptionEnabled; + hasAPMNetworkPlugin = hasAPMPlugin; + shouldEnableNativeInterception = + config.networkInterceptionMode === NetworkInterceptionMode.native; + refreshAPMNetworkConfigs(config); + }); + NativeInstabug.setOnFeaturesUpdatedListener(); +} /** * Sets the Code Push version to be sent with each report. @@ -382,9 +602,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 +627,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 +854,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 +872,29 @@ export const componentDidAppearListener = (event: ComponentDidAppearEvent) => { _lastScreen = event.componentName; } }; + +/** + * Sets listener to feature flag changes + * @param handler A callback that gets the update value of the flag + */ +export const _registerFeatureFlagsChangeListener = ( + handler: (payload: { + isW3ExternalTraceIDEnabled: boolean; + isW3ExternalGeneratedHeaderEnabled: boolean; + isW3CaughtHeaderEnabled: boolean; + networkBodyLimit: number; + }) => void, +) => { + emitter.addListener(NativeEvents.ON_FEATURE_FLAGS_CHANGE, (payload) => { + handler(payload); + }); + NativeInstabug.registerFeatureFlagsChangeListener(); +}; + +/** + * Sets the auto mask screenshots types. + * @param autoMaskingTypes The masking type to be applied. + */ +export const enableAutoMasking = (autoMaskingTypes: AutoMaskingType[]) => { + NativeInstabug.enableAutoMasking(autoMaskingTypes); +}; diff --git a/src/modules/NetworkLogger.ts b/src/modules/NetworkLogger.ts index 67f3a54cc..4bd334f56 100644 --- a/src/modules/NetworkLogger.ts +++ b/src/modules/NetworkLogger.ts @@ -2,13 +2,37 @@ 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 { InstabugRNConfig } from '../utils/config'; +import { Logger } from '../utils/logger'; +import { NativeInstabug } from '../native/NativeInstabug'; +import { + isContentTypeNotAllowed, + registerFilteringAndObfuscationListener, + registerFilteringListener, + registerObfuscationListener, + reportNetworkLog, +} from '../utils/InstabugUtils'; +import { + NativeNetworkLogger, + NativeNetworkLoggerEvent, + NetworkListenerType, + NetworkLoggerEmitter, +} from '../native/NativeNetworkLogger'; +import { Platform } from 'react-native'; export type { NetworkData }; export type NetworkDataObfuscationHandler = (data: NetworkData) => Promise; let _networkDataObfuscationHandler: NetworkDataObfuscationHandler | null | undefined; let _requestFilterExpression = 'false'; +let _isNativeInterceptionEnabled = false; +let _networkListener: NetworkListenerType | null = null; +let hasFilterExpression = false; + +function getPortFromUrl(url: string) { + const portMatch = url.match(/:(\d+)(?=\/|$)/); + return portMatch ? portMatch[1] : null; +} /** * Sets whether network logs should be sent with bug reports. @@ -21,39 +45,61 @@ export const setEnabled = (isEnabled: boolean) => { xhr.setOnDoneCallback(async (network) => { // eslint-disable-next-line no-new-func const predicate = Function('network', 'return ' + _requestFilterExpression); + if (!predicate(network)) { + const MAX_NETWORK_BODY_SIZE_IN_BYTES = await NativeInstabug.getNetworkBodyMaxSize(); try { if (_networkDataObfuscationHandler) { network = await _networkDataObfuscationHandler(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 (__DEV__) { + const urlPort = getPortFromUrl(network.url); + if (urlPort === InstabugRNConfig.metroDevServerPort) { + return; + } + } + if (network.requestBodySize > MAX_NETWORK_BODY_SIZE_IN_BYTES) { + network.requestBody = `${InstabugConstants.MAX_REQUEST_BODY_SIZE_EXCEEDED_MESSAGE}${ + MAX_NETWORK_BODY_SIZE_IN_BYTES / 1024 + } Kb`; + Logger.warn( + 'IBG-RN:', + `${InstabugConstants.MAX_REQUEST_BODY_SIZE_EXCEEDED_MESSAGE}${ + MAX_NETWORK_BODY_SIZE_IN_BYTES / 1024 + } Kb`, + ); } - 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.responseBodySize > MAX_NETWORK_BODY_SIZE_IN_BYTES) { + network.responseBody = `${InstabugConstants.MAX_RESPONSE_BODY_SIZE_EXCEEDED_MESSAGE}${ + MAX_NETWORK_BODY_SIZE_IN_BYTES / 1024 + } Kb`; + Logger.warn( + 'IBG-RN:', + `${InstabugConstants.MAX_RESPONSE_BODY_SIZE_EXCEEDED_MESSAGE}${ + MAX_NETWORK_BODY_SIZE_IN_BYTES / 1024 + } Kb`, + ); } 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); } } }); @@ -62,6 +108,22 @@ export const setEnabled = (isEnabled: boolean) => { } }; +/** + * @internal + * Sets whether enabling or disabling native network interception. + * It is disabled by default. + * @param isEnabled + */ +export const setNativeInterceptionEnabled = (isEnabled: boolean) => { + _isNativeInterceptionEnabled = isEnabled; +}; + +export const getNetworkDataObfuscationHandler = () => _networkDataObfuscationHandler; + +export const getRequestFilterExpression = () => _requestFilterExpression; + +export const hasRequestFilterExpression = () => hasFilterExpression; + /** * Obfuscates any response data. * @param handler @@ -70,6 +132,13 @@ export const setNetworkDataObfuscationHandler = ( handler?: NetworkDataObfuscationHandler | null | undefined, ) => { _networkDataObfuscationHandler = handler; + if (_isNativeInterceptionEnabled && Platform.OS === 'ios') { + if (hasFilterExpression) { + registerFilteringAndObfuscationListener(_requestFilterExpression); + } else { + registerObfuscationListener(); + } + } }; /** @@ -78,6 +147,15 @@ export const setNetworkDataObfuscationHandler = ( */ export const setRequestFilterExpression = (expression: string) => { _requestFilterExpression = expression; + hasFilterExpression = true; + + if (_isNativeInterceptionEnabled && Platform.OS === 'ios') { + if (_networkDataObfuscationHandler) { + registerFilteringAndObfuscationListener(_requestFilterExpression); + } else { + registerFilteringListener(_requestFilterExpression); + } + } }; /** @@ -96,8 +174,97 @@ export const apolloLinkRequestHandler: RequestHandler = (operation, forward) => return { headers: newHeaders }; }); } catch (e) { - console.error(e); + Logger.error(e); } return forward(operation); }; + +/** + * Sets whether network body logs will be captured or not. + * @param isEnabled + */ +export const setNetworkLogBodyEnabled = (isEnabled: boolean) => { + NativeInstabug.setNetworkLogBodyEnabled(isEnabled); +}; + +/** + * @internal + * Exported for internal/testing purposes only. + */ +export const resetNetworkListener = () => { + if (process.env.NODE_ENV === 'test') { + _networkListener = null; + NativeNetworkLogger.resetNetworkLogsListener(); + } else { + Logger.error( + `${InstabugConstants.IBG_APM_TAG}: The \`resetNetworkListener()\` method is intended solely for testing purposes.`, + ); + } +}; + +/** + * @internal + * Exported for internal/testing purposes only. + */ +export const registerNetworkLogsListener = ( + type: NetworkListenerType, + handler?: (networkSnapshot: NetworkData) => void, +) => { + if (Platform.OS === 'ios') { + // remove old listeners + if (NetworkLoggerEmitter.listenerCount(NativeNetworkLoggerEvent.NETWORK_LOGGER_HANDLER) > 0) { + NetworkLoggerEmitter.removeAllListeners(NativeNetworkLoggerEvent.NETWORK_LOGGER_HANDLER); + } + + if (_networkListener == null) { + // set new listener. + _networkListener = type; + } else { + // attach a new listener to the existing one. + _networkListener = NetworkListenerType.both; + } + } + + NetworkLoggerEmitter.addListener( + NativeNetworkLoggerEvent.NETWORK_LOGGER_HANDLER, + (networkSnapshot) => { + // Mapping the data [Native -> React-Native]. + const { id, url, requestHeader, requestBody, responseHeader, response, responseCode } = + networkSnapshot; + + const networkSnapshotObj: NetworkData = { + id: id, + url: url, + requestBody: requestBody, + requestHeaders: requestHeader, + method: '', + responseBody: response, + responseCode: responseCode, + responseHeaders: responseHeader, + contentType: '', + duration: 0, + requestBodySize: 0, + responseBodySize: 0, + errorDomain: '', + errorCode: 0, + startTime: 0, + serverErrorMessage: '', + requestContentType: '', + isW3cHeaderFound: true, + networkStartTimeInSeconds: 0, + partialId: 0, + w3cCaughtHeader: '', + w3cGeneratedHeader: '', + }; + if (handler) { + handler(networkSnapshotObj); + } + }, + ); + if (Platform.OS === 'ios') { + NativeNetworkLogger.registerNetworkLogsListener(_networkListener ?? NetworkListenerType.both); + } else { + NativeNetworkLogger.registerNetworkLogsListener(); + } +}; diff --git a/src/native/NativeAPM.ts b/src/native/NativeAPM.ts index b1981cfe3..9fa30b702 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/NativeBugReporting.ts b/src/native/NativeBugReporting.ts index 802722247..f448a8b53 100644 --- a/src/native/NativeBugReporting.ts +++ b/src/native/NativeBugReporting.ts @@ -8,6 +8,7 @@ import type { InvocationOption, RecordingButtonPosition, ReportType, + userConsentActionType, } from '../utils/Enums'; import { NativeModules } from './NativePackage'; @@ -48,6 +49,14 @@ export interface BugReportingNativeModule extends NativeModule { setOnSDKDismissedHandler( handler: (dismissType: DismissType, reportType: ReportType) => void, ): void; + + addUserConsent( + key: string, + description: string, + mandatory: boolean, + checked: boolean, + actionType?: userConsentActionType, + ): void; } export const NativeBugReporting = NativeModules.IBGBugReporting; diff --git a/src/native/NativeConstants.ts b/src/native/NativeConstants.ts index a4e98e2c8..f95634caf 100644 --- a/src/native/NativeConstants.ts +++ b/src/native/NativeConstants.ts @@ -13,7 +13,9 @@ export type NativeConstants = NativeSdkDebugLogsLevel & NativeLocale & NativeNonFatalErrorLevel & NativeStringKey & - NativeLaunchType; + NativeLaunchType & + NativeAutoMaskingType & + NativeUserConsentActionType; interface NativeSdkDebugLogsLevel { sdkDebugLogsLevelVerbose: any; @@ -21,7 +23,11 @@ interface NativeSdkDebugLogsLevel { sdkDebugLogsLevelError: any; sdkDebugLogsLevelNone: any; } - +interface NativeUserConsentActionType { + dropAutoCapturedMedia: any; + dropLogs: any; + noChat: any; +} interface NativeInvocationEvent { invocationEventNone: any; invocationEventShake: any; @@ -195,3 +201,10 @@ interface NativeLaunchType { warm: any; unknown: any; } + +interface NativeAutoMaskingType { + labels: any; + textInputs: any; + media: any; + none: any; +} diff --git a/src/native/NativeInstabug.ts b/src/native/NativeInstabug.ts index 3b72f5951..c9c078f37 100644 --- a/src/native/NativeInstabug.ts +++ b/src/native/NativeInstabug.ts @@ -2,6 +2,7 @@ import { NativeEventEmitter, NativeModule, ProcessedColorValue } from 'react-nat import type Report from '../models/Report'; import type { + AutoMaskingType, ColorTheme, InvocationEvent, Locale, @@ -11,6 +12,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 { @@ -24,6 +26,9 @@ export interface InstabugNativeModule extends NativeModule { debugLogsLevel: LogLevel, useNativeNetworkInterception: boolean, codePushVersion?: string, + options?: { + ignoreAndroidSecureFlag?: boolean; + }, ): void; show(): void; @@ -67,9 +72,11 @@ export interface InstabugNativeModule extends NativeModule { duration: number, gqlQueryName: string | undefined, serverErrorMessage: string | undefined, + W3cExternalTraceAttributes: W3cExternalTraceAttributes, ): void; setNetworkLoggingEnabled(isEnabled: boolean): void; + setNetworkLogBodyEnabled(isEnabled: boolean): void; // Repro Steps APIs // setReproStepsConfig( @@ -140,12 +147,28 @@ 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; + + // Feature Flags Listener for Android + registerFeatureFlagsChangeListener(): void; + + setOnFeaturesUpdatedListener(handler?: (params: any) => void): void; // android only + enableAutoMasking(autoMaskingTypes: AutoMaskingType[]): void; + getNetworkBodyMaxSize(): Promise; } export const NativeInstabug = NativeModules.Instabug; export enum NativeEvents { PRESENDING_HANDLER = 'IBGpreSendingHandler', + IBG_ON_FEATURES_UPDATED_CALLBACK = 'IBGOnFeatureUpdatedCallback', + ON_FEATURE_FLAGS_CHANGE = 'IBGOnNewFeatureFlagsUpdateReceivedCallback', } export const emitter = new NativeEventEmitter(NativeInstabug); diff --git a/src/native/NativeNetworkLogger.ts b/src/native/NativeNetworkLogger.ts new file mode 100644 index 000000000..c38f11873 --- /dev/null +++ b/src/native/NativeNetworkLogger.ts @@ -0,0 +1,42 @@ +import { NativeModules } from './NativePackage'; +import { NativeEventEmitter, type NativeModule } from 'react-native'; + +export enum NetworkListenerType { + filtering = 'filtering', + obfuscation = 'obfuscation', + both = 'both', +} + +export interface NetworkLoggerNativeModule extends NativeModule { + isNativeInterceptionEnabled(): boolean; + + registerNetworkLogsListener(type?: NetworkListenerType): void; + + updateNetworkLogSnapshot( + url: string, + callbackID: string, + requestBody: string | null, + responseBody: string | null, + responseCode: number, + requestHeaders: Record, + responseHeaders: Record, + ): void; + + hasAPMNetworkPlugin(): Promise; // Android only + + resetNetworkLogsListener(): void; //Android only + + setNetworkLoggingRequestFilterPredicateIOS(id: string, value: boolean): void; // iOS only + + forceStartNetworkLoggingIOS(): void; // iOS only; + + forceStopNetworkLoggingIOS(): void; // iOS only; +} + +export const NativeNetworkLogger = NativeModules.IBGNetworkLogger; + +export enum NativeNetworkLoggerEvent { + NETWORK_LOGGER_HANDLER = 'IBGNetworkLoggerHandler', +} + +export const NetworkLoggerEmitter = new NativeEventEmitter(NativeNetworkLogger); diff --git a/src/native/NativePackage.ts b/src/native/NativePackage.ts index 9c31789bd..51ac6fd0c 100644 --- a/src/native/NativePackage.ts +++ b/src/native/NativePackage.ts @@ -8,6 +8,7 @@ import type { InstabugNativeModule } from './NativeInstabug'; import type { RepliesNativeModule } from './NativeReplies'; import type { SurveysNativeModule } from './NativeSurveys'; import type { SessionReplayNativeModule } from './NativeSessionReplay'; +import type { NetworkLoggerNativeModule } from './NativeNetworkLogger'; export interface InstabugNativePackage { IBGAPM: ApmNativeModule; @@ -18,6 +19,7 @@ export interface InstabugNativePackage { IBGReplies: RepliesNativeModule; IBGSurveys: SurveysNativeModule; IBGSessionReplay: SessionReplayNativeModule; + IBGNetworkLogger: NetworkLoggerNativeModule; } export const NativeModules = ReactNativeModules as InstabugNativePackage; diff --git a/src/utils/AppStatesHandler.ts b/src/utils/AppStatesHandler.ts new file mode 100644 index 000000000..ac71f50b9 --- /dev/null +++ b/src/utils/AppStatesHandler.ts @@ -0,0 +1,19 @@ +import { AppState, type AppStateStatus } from 'react-native'; + +let subscription: any = null; + +// Register the event listener manually +export const addAppStateListener = (handleAppStateChange: (state: AppStateStatus) => void) => { + if (!subscription) { + subscription = AppState.addEventListener('change', handleAppStateChange); + } +}; + +// Unregister the event listener manually +//todo: find where to Unregister appState listener +export const removeAppStateListener = () => { + if (subscription) { + subscription.remove(); + subscription = null; + } +}; diff --git a/src/utils/Enums.ts b/src/utils/Enums.ts index 37ffa33ce..1859ed2be 100644 --- a/src/utils/Enums.ts +++ b/src/utils/Enums.ts @@ -12,6 +12,15 @@ export enum LogLevel { error = constants.sdkDebugLogsLevelError, none = constants.sdkDebugLogsLevelNone, } +/** + * Enum representing the available user consent action types. + * + */ +export enum userConsentActionType { + dropAutoCapturedMedia = constants.dropAutoCapturedMedia, + dropLogs = constants.dropLogs, + noChat = constants.noChat, +} /** * The event used to invoke the feedback form. @@ -241,3 +250,9 @@ export enum LaunchType { */ warm = constants.warm, } +export enum AutoMaskingType { + labels = constants.labels, + textInputs = constants.textInputs, + media = constants.media, + none = constants.none, +} diff --git a/src/utils/FeatureFlags.ts b/src/utils/FeatureFlags.ts new file mode 100644 index 000000000..f9c644e99 --- /dev/null +++ b/src/utils/FeatureFlags.ts @@ -0,0 +1,33 @@ +import { NativeInstabug } from '../native/NativeInstabug'; +import { _registerFeatureFlagsChangeListener } from '../modules/Instabug'; + +export const FeatureFlags = { + isW3ExternalTraceID: () => NativeInstabug.isW3ExternalTraceIDEnabled(), + isW3ExternalGeneratedHeader: () => NativeInstabug.isW3ExternalGeneratedHeaderEnabled(), + isW3CaughtHeader: () => NativeInstabug.isW3CaughtHeaderEnabled(), + networkLogLimit: () => NativeInstabug.getNetworkBodyMaxSize(), +}; + +export const registerFeatureFlagsListener = () => { + _registerFeatureFlagsChangeListener( + (res: { + isW3ExternalTraceIDEnabled: boolean; + isW3ExternalGeneratedHeaderEnabled: boolean; + isW3CaughtHeaderEnabled: boolean; + networkBodyLimit: number; + }) => { + FeatureFlags.isW3ExternalTraceID = async () => { + return res.isW3ExternalTraceIDEnabled; + }; + FeatureFlags.isW3ExternalGeneratedHeader = async () => { + return res.isW3ExternalGeneratedHeaderEnabled; + }; + FeatureFlags.isW3CaughtHeader = async () => { + return res.isW3CaughtHeaderEnabled; + }; + FeatureFlags.networkLogLimit = async () => { + return res.networkBodyLimit; + }; + }, + ); +}; diff --git a/src/utils/InstabugConstants.ts b/src/utils/InstabugConstants.ts index 6d117d987..4d8fca486 100644 --- a/src/utils/InstabugConstants.ts +++ b/src/utils/InstabugConstants.ts @@ -4,13 +4,21 @@ const InstabugConstants = { // 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', + 'The response body has not been logged because it exceeds the maximum size of ', MAX_REQUEST_BODY_SIZE_EXCEEDED_MESSAGE: - 'The request body has not been logged because it exceeds the maximum size of 10 Kb', + 'The request body has not been logged because it exceeds the maximum size of ', SET_USER_ATTRIBUTES_ERROR_TYPE_MESSAGE: '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', + IBG_APM_TAG: 'IBG-APM: ', + SWITCHED_TO_NATIVE_INTERCEPTION_MESSAGE: + 'Android Plugin Detected. Switched to Native Interception.', + PLUGIN_NOT_INSTALLED_MESSAGE: + 'Network Spans will not be captured as Android Plugin is not installed. Disabling Native Interception to minimize data loss.', + NATIVE_INTERCEPTION_DISABLED_MESSAGE: + 'Network Spans capture is disabled by Instabug. Disabling native interception to avoid data loss.', }; export default InstabugConstants; diff --git a/src/utils/InstabugUtils.ts b/src/utils/InstabugUtils.ts index d4238f14f..2a619f31d 100644 --- a/src/utils/InstabugUtils.ts +++ b/src/utils/InstabugUtils.ts @@ -8,10 +8,41 @@ 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 type { NetworkData } from './XhrNetworkInterceptor'; import { NativeInstabug } from '../native/NativeInstabug'; import { NativeAPM } from '../native/NativeAPM'; +import * as NetworkLogger from '../modules/NetworkLogger'; +import { + NativeNetworkLogger, + NativeNetworkLoggerEvent, + NetworkListenerType, + NetworkLoggerEmitter, +} from '../native/NativeNetworkLogger'; + +type ApmNetworkFlags = { + isNativeInterceptionFeatureEnabled: boolean; + hasAPMNetworkPlugin: boolean; + shouldEnableNativeInterception: boolean; +}; + +let apmFlags: ApmNetworkFlags = { + isNativeInterceptionFeatureEnabled: false, + hasAPMNetworkPlugin: false, + shouldEnableNativeInterception: false, +}; + +export function setApmNetworkFlagsIfChanged(flags: ApmNetworkFlags): boolean { + if ( + flags.hasAPMNetworkPlugin === apmFlags.hasAPMNetworkPlugin && + flags.isNativeInterceptionFeatureEnabled === apmFlags.isNativeInterceptionFeatureEnabled && + flags.shouldEnableNativeInterception === apmFlags.shouldEnableNativeInterception + ) { + return false; + } + apmFlags = flags; + return true; +} export const parseErrorStack = (error: ExtendedError): StackFrame[] => { return parseErrorStackLib(error); @@ -43,7 +74,7 @@ export const getActiveRouteName = (navigationState: NavigationStateV4): string | return route.routeName; }; -function getFullRoute(state: NavigationStateV5 | PartialState): string { +export function getFullRoute(state: NavigationStateV5 | PartialState): string { try { if (!state.routes[state.index!].state) { return state.routes[state.index!].name; @@ -126,6 +157,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 = [ @@ -140,7 +209,7 @@ export function isContentTypeNotAllowed(contentType: string) { return allowed.every((type) => !contentType.includes(type)); } -export function reportNetworkLog(network: NetworkData) { +export const reportNetworkLog = (network: NetworkData) => { if (Platform.OS === 'android') { const requestHeaders = JSON.stringify(network.requestHeaders); const responseHeaders = JSON.stringify(network.responseHeaders); @@ -155,25 +224,37 @@ export function reportNetworkLog(network: NetworkData) { 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, - ); + if ( + !apmFlags.isNativeInterceptionFeatureEnabled || + !apmFlags.hasAPMNetworkPlugin || + !apmFlags.shouldEnableNativeInterception + ) { + 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, + { + isW3cHeaderFound: network.isW3cHeaderFound, + partialId: network.partialId, + networkStartTimeInSeconds: network.networkStartTimeInSeconds, + w3cGeneratedHeader: network.w3cGeneratedHeader, + w3cCaughtHeader: network.w3cCaughtHeader, + }, + network.gqlQueryName, + network.serverErrorMessage, + ); + } } else { NativeInstabug.networkLogIOS( network.url, @@ -192,8 +273,134 @@ 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, + }, ); } +}; + +/** + * @internal + * This method is for internal use only. + */ +export function registerObfuscationListener() { + NetworkLogger.registerNetworkLogsListener( + NetworkListenerType.obfuscation, + async (networkSnapshot) => { + const _networkDataObfuscationHandler = NetworkLogger.getNetworkDataObfuscationHandler(); + if (_networkDataObfuscationHandler) { + networkSnapshot = await _networkDataObfuscationHandler(networkSnapshot); + } + updateNetworkLogSnapshot(networkSnapshot); + }, + ); +} + +/** + * @internal + * This method is for internal use only. + */ +export function registerFilteringListener(filterExpression: string) { + NetworkLogger.registerNetworkLogsListener( + NetworkListenerType.filtering, + async (networkSnapshot) => { + // eslint-disable-next-line no-new-func + const predicate = Function('network', 'return ' + filterExpression); + const value = predicate(networkSnapshot); + if (Platform.OS === 'ios') { + // For iOS True == Request will be saved, False == will be ignored + NativeNetworkLogger.setNetworkLoggingRequestFilterPredicateIOS(networkSnapshot.id, !value); + } else { + // For Android Setting the [url] to an empty string will ignore the request; + if (value) { + networkSnapshot.url = ''; + updateNetworkLogSnapshot(networkSnapshot); + } + } + }, + ); +} + +/** + * @internal + * This method is for internal use only. + */ +export function registerFilteringAndObfuscationListener(filterExpression: string) { + NetworkLogger.registerNetworkLogsListener(NetworkListenerType.both, async (networkSnapshot) => { + // eslint-disable-next-line no-new-func + const predicate = Function('network', 'return ' + filterExpression); + const value = predicate(networkSnapshot); + if (Platform.OS === 'ios') { + // For iOS True == Request will be saved, False == will be ignored + NativeNetworkLogger.setNetworkLoggingRequestFilterPredicateIOS(networkSnapshot.id, !value); + } else { + // For Android Setting the [url] to an empty string will ignore the request; + if (value) { + networkSnapshot.url = ''; + updateNetworkLogSnapshot(networkSnapshot); + } + } + if (!value) { + const _networkDataObfuscationHandler = NetworkLogger.getNetworkDataObfuscationHandler(); + if (_networkDataObfuscationHandler) { + networkSnapshot = await _networkDataObfuscationHandler(networkSnapshot); + } + updateNetworkLogSnapshot(networkSnapshot); + } + }); +} + +/** + * @internal + * This method is for internal use only. + */ +export function checkNetworkRequestHandlers() { + const obfuscationHandler = NetworkLogger.getNetworkDataObfuscationHandler(); + const hasFilterExpression = NetworkLogger.hasRequestFilterExpression(); + + if (hasFilterExpression && obfuscationHandler) { + // Register listener that handles both (Filtering & Obfuscation) + registerFilteringAndObfuscationListener(NetworkLogger.getRequestFilterExpression()); + return; + } + if (obfuscationHandler) { + // Register listener that handles only (Obfuscation) + registerObfuscationListener(); + return; + } + + if (hasFilterExpression) { + // Register listener that handles only (Filtering) + registerFilteringListener(NetworkLogger.getRequestFilterExpression()); + return; + } +} +export function resetNativeObfuscationListener() { + if (Platform.OS === 'android') { + NativeNetworkLogger.resetNetworkLogsListener(); + } + NetworkLoggerEmitter.removeAllListeners(NativeNetworkLoggerEvent.NETWORK_LOGGER_HANDLER); +} + +/** + * @internal + * This method is for internal use only. + */ +export function updateNetworkLogSnapshot(networkSnapshot: NetworkData) { + NativeNetworkLogger.updateNetworkLogSnapshot( + networkSnapshot.url, + networkSnapshot.id, + networkSnapshot.requestBody, + networkSnapshot.responseBody, + networkSnapshot.responseCode ?? 200, + networkSnapshot.requestHeaders, + networkSnapshot.responseHeaders, + ); } export default { @@ -204,4 +411,7 @@ export default { getStackTrace, stringifyIfNotString, sendCrashReport, + reportNetworkLog, + generateTracePartialId, + generateW3CHeader, }; diff --git a/src/utils/UnhandledRejectionTracking.ts b/src/utils/UnhandledRejectionTracking.ts index d8049a9b0..9cbe0dc91 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 98c5ef9cc..7485a4853 100644 --- a/src/utils/XhrNetworkInterceptor.ts +++ b/src/utils/XhrNetworkInterceptor.ts @@ -1,10 +1,13 @@ 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; export interface NetworkData { + readonly id: string; url: string; method: string; requestBody: string; @@ -22,6 +25,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; @@ -36,6 +44,7 @@ let network: NetworkData; const _reset = () => { network = { + id: '', url: '', method: '', requestBody: '', @@ -53,8 +62,82 @@ 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 = ( + 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 = (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 +174,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 +309,14 @@ export default { } cloneNetwork.startTime = Date.now(); + const traceparent = await getTraceparentHeader(cloneNetwork); + if (traceparent) { + this.setRequestHeader('Traceparent', traceparent); + } + if (this.readyState === this.UNSENT) { + return; // Prevent sending the request if not opened + } + originalXHRSend.apply(this, [data]); }; isInterceptorEnabled = true; diff --git a/src/utils/config.ts b/src/utils/config.ts new file mode 100644 index 000000000..de7073d93 --- /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 000000000..e43d740d0 --- /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/mockBugReporting.ts b/test/mocks/mockBugReporting.ts index 03d3bd35d..15f1d47de 100644 --- a/test/mocks/mockBugReporting.ts +++ b/test/mocks/mockBugReporting.ts @@ -23,6 +23,7 @@ const mockBugReporting: BugReportingNativeModule = { setVideoRecordingFloatingButtonPosition: jest.fn(), setDisclaimerText: jest.fn(), setCommentMinimumCharacterCount: jest.fn(), + addUserConsent: jest.fn(), }; export default mockBugReporting; diff --git a/test/mocks/mockInstabug.ts b/test/mocks/mockInstabug.ts index 5139afcde..391a00a38 100644 --- a/test/mocks/mockInstabug.ts +++ b/test/mocks/mockInstabug.ts @@ -69,6 +69,14 @@ const mockInstabug: InstabugNativeModule = { addFileAttachmentWithDataToReport: jest.fn(), setNetworkLoggingEnabled: jest.fn(), willRedirectToStore: jest.fn(), + isW3ExternalTraceIDEnabled: jest.fn(), + isW3ExternalGeneratedHeaderEnabled: jest.fn(), + isW3CaughtHeaderEnabled: jest.fn(), + registerFeatureFlagsChangeListener: jest.fn(), + setNetworkLogBodyEnabled: jest.fn(), + setOnFeaturesUpdatedListener: jest.fn(), + enableAutoMasking: jest.fn(), + getNetworkBodyMaxSize: jest.fn().mockResolvedValue(10240), // 10 KB }; export default mockInstabug; diff --git a/test/mocks/mockNativeModules.ts b/test/mocks/mockNativeModules.ts index 5618930e8..6d4c19fac 100644 --- a/test/mocks/mockNativeModules.ts +++ b/test/mocks/mockNativeModules.ts @@ -7,10 +7,12 @@ import mockSessionReplay from './mockSessionReplay'; import mockInstabug from './mockInstabug'; import mockReplies from './mockReplies'; import mockSurveys from './mockSurveys'; +import mockNetworkLogger from './mockNetworkLogger'; jest.mock('react-native', () => { const RN = jest.requireActual('react-native'); const mockNativeModules: InstabugNativePackage = { + IBGNetworkLogger: mockNetworkLogger, IBGAPM: mockAPM, IBGBugReporting: mockBugReporting, IBGCrashReporting: mockCrashReporting, diff --git a/test/mocks/mockNetworkLogger.ts b/test/mocks/mockNetworkLogger.ts index 0eaed26e0..88608016c 100644 --- a/test/mocks/mockNetworkLogger.ts +++ b/test/mocks/mockNetworkLogger.ts @@ -1 +1,16 @@ -jest.mock('../../src/modules/NetworkLogger'); +import type { NetworkLoggerNativeModule } from '../../src/native/NativeNetworkLogger'; + +const mockNetworkLogger: NetworkLoggerNativeModule = { + addListener: jest.fn(), + removeListeners: jest.fn(), + hasAPMNetworkPlugin: jest.fn(), + isNativeInterceptionEnabled: jest.fn(), + forceStartNetworkLoggingIOS: jest.fn(), + forceStopNetworkLoggingIOS: jest.fn(), + registerNetworkLogsListener: jest.fn(), + updateNetworkLogSnapshot: jest.fn(), + setNetworkLoggingRequestFilterPredicateIOS: jest.fn(), + resetNetworkLogsListener: jest.fn(), +}; + +export default mockNetworkLogger; diff --git a/test/modules/Instabug.spec.ts b/test/modules/Instabug.spec.ts index 46b4b208e..eeec8e8e4 100644 --- a/test/modules/Instabug.spec.ts +++ b/test/modules/Instabug.spec.ts @@ -1,18 +1,19 @@ import '../mocks/mockInstabugUtils'; import '../mocks/mockNetworkLogger'; -import { Platform, findNodeHandle, processColor } from 'react-native'; +import { findNodeHandle, Platform, processColor } from 'react-native'; import type { NavigationContainerRefWithCurrent } from '@react-navigation/native'; // Import the hook - import { mocked } from 'jest-mock'; import waitForExpect from 'wait-for-expect'; import Report from '../../src/models/Report'; import * as Instabug from '../../src/modules/Instabug'; import * as NetworkLogger from '../../src/modules/NetworkLogger'; -import { NativeEvents, NativeInstabug, emitter } from '../../src/native/NativeInstabug'; +import { emitter, NativeEvents, NativeInstabug } from '../../src/native/NativeInstabug'; import { + AutoMaskingType, ColorTheme, + type InstabugConfig, InvocationEvent, Locale, LogLevel, @@ -20,11 +21,19 @@ import { ReproStepsMode, StringKey, WelcomeMessageMode, -} from '../../src/utils/Enums'; +} from '../../src'; import InstabugUtils from '../../src/utils/InstabugUtils'; import type { FeatureFlag } from '../../src/models/FeatureFlag'; +import { Logger } from '../../src/utils/logger'; +import { NativeNetworkLogger } from '../../src/native/NativeNetworkLogger'; import InstabugConstants from '../../src/utils/InstabugConstants'; +jest.mock('../../src/modules/NetworkLogger'); + +function fakeTimer(callback: () => void) { + setTimeout(callback, 100); +} + describe('Instabug Module', () => { beforeEach(() => { const events = Object.values(NativeEvents); @@ -60,7 +69,7 @@ describe('Instabug Module', () => { expect(NativeInstabug.reportScreenChange).toBeCalledWith(screenName); }); - it("componentDidAppearListener shouldn't call the native method reportScreenChange if first screen", async () => { + it("componentDidAppearListener shouldn't call the native method reportScreenChange if first screen", () => { Instabug.init({ token: 'some-token', invocationEvents: [InvocationEvent.none], @@ -72,7 +81,7 @@ describe('Instabug Module', () => { componentType: 'Component', }); - await waitForExpect(() => { + waitForExpect(() => { // Only first screen should be reported expect(NativeInstabug.reportScreenChange).toBeCalledTimes(1); expect(NativeInstabug.reportScreenChange).toBeCalledWith('Initial Screen'); @@ -80,6 +89,11 @@ describe('Instabug Module', () => { }); it("componentDidAppearListener shouldn't call the native method reportScreenChange twice if same screen", (done) => { + Instabug.init({ + token: 'some-token', + invocationEvents: [InvocationEvent.none], + }); + Array(5).forEach(() => { Instabug.componentDidAppearListener({ componentId: '1', @@ -98,7 +112,7 @@ describe('Instabug Module', () => { // 2. Second+ calls: // The screen name is the same as _lastScreen (stored in 1st call) // so it doesn't report a screen change - expect(NativeInstabug.reportScreenChange).not.toBeCalled(); + expect(NativeInstabug.reportScreenChange).toBeCalledTimes(1); done(); }, 1500); }); @@ -280,6 +294,7 @@ describe('Instabug Module', () => { invocationEvents: [InvocationEvent.floatingButton, InvocationEvent.shake], debugLogsLevel: LogLevel.debug, codePushVersion: '1.1.0', + ignoreAndroidSecureFlag: true, }; const usesNativeNetworkInterception = false; @@ -293,6 +308,7 @@ describe('Instabug Module', () => { instabugConfig.debugLogsLevel, usesNativeNetworkInterception, instabugConfig.codePushVersion, + { ignoreAndroidSecureFlag: instabugConfig.ignoreAndroidSecureFlag }, ); }); @@ -312,20 +328,40 @@ describe('Instabug Module', () => { debugLogsLevel: LogLevel.debug, networkInterceptionMode: NetworkInterceptionMode.native, codePushVersion: '1.1.0', + ignoreAndroidSecureFlag: true, }; + // Stubbing Network feature flags + jest.spyOn(NativeNetworkLogger, 'isNativeInterceptionEnabled').mockReturnValue(true); + jest.spyOn(NativeNetworkLogger, 'hasAPMNetworkPlugin').mockReturnValue(Promise.resolve(true)); + Instabug.init(instabugConfig); - expect(NetworkLogger.setEnabled).not.toBeCalled(); - expect(NativeInstabug.init).toBeCalledTimes(1); - expect(NativeInstabug.init).toBeCalledWith( - instabugConfig.token, - instabugConfig.invocationEvents, - instabugConfig.debugLogsLevel, - // usesNativeNetworkInterception should be true when using native interception mode - true, - instabugConfig.codePushVersion, - ); + if (Platform.OS === 'android') { + expect(NetworkLogger.setEnabled).not.toBeCalled(); + expect(NativeInstabug.init).toBeCalledTimes(1); + + expect(NativeInstabug.init).toBeCalledWith( + instabugConfig.token, + instabugConfig.invocationEvents, + instabugConfig.debugLogsLevel, + // usesNativeNetworkInterception should be false when using native interception mode with Android + false, + instabugConfig.codePushVersion, + ); + } else { + expect(NativeInstabug.init).toBeCalledTimes(1); + + expect(NativeInstabug.init).toBeCalledWith( + instabugConfig.token, + instabugConfig.invocationEvents, + instabugConfig.debugLogsLevel, + // usesNativeNetworkInterception should be true when using native interception mode with iOS + true, + instabugConfig.codePushVersion, + { ignoreAndroidSecureFlag: instabugConfig.ignoreAndroidSecureFlag }, + ); + } }); it('should report the first screen on SDK initialization', async () => { @@ -641,7 +677,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 +906,202 @@ describe('Instabug Module', () => { Instabug.willRedirectToStore(); expect(NativeInstabug.willRedirectToStore).toBeCalledTimes(1); }); + + it('should register feature flag listener', () => { + const callback = jest.fn(); + Instabug._registerFeatureFlagsChangeListener(callback); + + expect(NativeInstabug.registerFeatureFlagsChangeListener).toBeCalledTimes(1); + }); + + it('should invoke callback on emitting the event IBGOnNewFeatureFlagsUpdateReceivedCallback', () => { + const callback = jest.fn(); + Instabug._registerFeatureFlagsChangeListener(callback); + emitter.emit(NativeEvents.ON_FEATURE_FLAGS_CHANGE); + + expect(emitter.listenerCount(NativeEvents.ON_FEATURE_FLAGS_CHANGE)).toBe(1); + expect(callback).toHaveBeenCalled(); + }); + + it('should call the native method enableAutoMasking', () => { + Instabug.enableAutoMasking([AutoMaskingType.labels]); + + expect(NativeInstabug.enableAutoMasking).toBeCalledTimes(1); + expect(NativeInstabug.enableAutoMasking).toBeCalledWith([AutoMaskingType.labels]); + }); +}); + +describe('Instabug iOS initialization tests', () => { + let config: InstabugConfig; + beforeEach(() => { + Platform.OS = 'ios'; + config = { + token: 'some-token', + invocationEvents: [InvocationEvent.floatingButton, InvocationEvent.shake], + debugLogsLevel: LogLevel.debug, + networkInterceptionMode: NetworkInterceptionMode.native, + codePushVersion: '1.1.0', + }; + // Fast-forward until all timers have been executed + jest.advanceTimersByTime(1000); + }); + + it('should initialize correctly with javascript interception mode', () => { + config.networkInterceptionMode = NetworkInterceptionMode.javascript; + + Instabug.init(config); + + expect(NativeNetworkLogger.isNativeInterceptionEnabled).toHaveBeenCalled(); + expect(NetworkLogger.setEnabled).toHaveBeenCalledWith(true); + expect(NativeInstabug.init).toHaveBeenCalledWith( + config.token, + config.invocationEvents, + config.debugLogsLevel, + false, // Disable native interception + config.codePushVersion, + config.ignoreAndroidSecureFlag, + ); + }); + + it('should initialize correctly with native interception mode when [isNativeInterceptionEnabled] == ture', () => { + jest.spyOn(NativeNetworkLogger, 'isNativeInterceptionEnabled').mockReturnValue(true); + + Instabug.init(config); + + expect(NativeNetworkLogger.isNativeInterceptionEnabled).toHaveBeenCalled(); + expect(NetworkLogger.setEnabled).toHaveBeenCalledWith(false); + expect(NativeInstabug.init).toHaveBeenCalledWith( + config.token, + config.invocationEvents, + config.debugLogsLevel, + true, // Enable native interception + config.codePushVersion, + config.ignoreAndroidSecureFlag, + ); + }); + + it('should disable native interception mode when user sets networkInterceptionMode to native and [isNativeInterceptionEnabled] == false', () => { + jest.spyOn(NativeNetworkLogger, 'isNativeInterceptionEnabled').mockReturnValue(false); + + Instabug.init(config); + + expect(NativeNetworkLogger.isNativeInterceptionEnabled).toHaveBeenCalled(); + expect(NetworkLogger.setEnabled).toHaveBeenCalled(); + expect(NativeInstabug.init).toHaveBeenCalledWith( + config.token, + config.invocationEvents, + config.debugLogsLevel, + false, // Disable native interception + config.codePushVersion, + config.ignoreAndroidSecureFlag, + ); + }); + + it('should display error message when user sets networkInterceptionMode to native and [isNativeInterceptionEnabled] == false', () => { + jest.spyOn(NativeNetworkLogger, 'isNativeInterceptionEnabled').mockReturnValue(false); + const logSpy = jest.spyOn(global.console, 'error'); + + Instabug.init(config); + + expect(logSpy).toBeCalledTimes(1); + expect(logSpy).toBeCalledWith( + InstabugConstants.IBG_APM_TAG + InstabugConstants.NATIVE_INTERCEPTION_DISABLED_MESSAGE, + ); + }); +}); + +describe('Instabug Android initialization tests', () => { + let config: InstabugConfig; + + beforeEach(() => { + Platform.OS = 'android'; + config = { + token: 'some-token', + invocationEvents: [InvocationEvent.floatingButton, InvocationEvent.shake], + debugLogsLevel: LogLevel.debug, + networkInterceptionMode: NetworkInterceptionMode.javascript, + codePushVersion: '1.1.0', + }; + }); + + it('should initialize correctly with native interception enabled', () => { + config.networkInterceptionMode = NetworkInterceptionMode.native; + Instabug.init(config); + fakeTimer(() => { + expect(NativeInstabug.setOnFeaturesUpdatedListener).toHaveBeenCalled(); + expect(NetworkLogger.setEnabled).toHaveBeenCalledWith(true); + expect(NativeInstabug.init).toHaveBeenCalledWith( + config.token, + config.invocationEvents, + config.debugLogsLevel, + false, // always disable native interception to insure sending network logs to core (Bugs & Crashes). + config.codePushVersion, + { ignoreAndroidSecureFlag: config.ignoreAndroidSecureFlag }, + ); + }); + }); + + it('should show warning message when networkInterceptionMode == javascript and user added APM plugin', () => { + jest.spyOn(NativeNetworkLogger, 'isNativeInterceptionEnabled').mockReturnValue(true); + jest.spyOn(NativeNetworkLogger, 'hasAPMNetworkPlugin').mockReturnValue(Promise.resolve(true)); + const logSpy = jest.spyOn(global.console, 'warn'); + + Instabug.init(config); + fakeTimer(() => { + expect(logSpy).toBeCalledTimes(1); + expect(logSpy).toBeCalledWith( + InstabugConstants.IBG_APM_TAG + InstabugConstants.SWITCHED_TO_NATIVE_INTERCEPTION_MESSAGE, + ); + }); + }); + + it('should show error message when networkInterceptionMode == native and user did not add APM plugin', () => { + config.networkInterceptionMode = NetworkInterceptionMode.native; + + jest.spyOn(NativeNetworkLogger, 'isNativeInterceptionEnabled').mockReturnValue(true); + jest.spyOn(NativeNetworkLogger, 'hasAPMNetworkPlugin').mockReturnValue(Promise.resolve(false)); + const logSpy = jest.spyOn(global.console, 'error'); + + Instabug.init(config); + + fakeTimer(() => { + expect(logSpy).toBeCalledTimes(1); + expect(logSpy).toBeCalledWith( + InstabugConstants.IBG_APM_TAG + InstabugConstants.PLUGIN_NOT_INSTALLED_MESSAGE, + ); + }); + }); + + it('should show error message when networkInterceptionMode == native and user did not add APM plugin and the isNativeInterceptionEnabled is disabled', () => { + config.networkInterceptionMode = NetworkInterceptionMode.native; + + jest.spyOn(NativeNetworkLogger, 'isNativeInterceptionEnabled').mockReturnValue(false); + jest.spyOn(NativeNetworkLogger, 'hasAPMNetworkPlugin').mockReturnValue(Promise.resolve(false)); + const logSpy = jest.spyOn(global.console, 'error'); + + Instabug.init(config); + + fakeTimer(() => { + expect(logSpy).toBeCalledTimes(1); + expect(logSpy).toBeCalledWith( + InstabugConstants.IBG_APM_TAG + InstabugConstants.NATIVE_INTERCEPTION_DISABLED_MESSAGE, + ); + }); + }); + + it('should show error message when networkInterceptionMode == native and the isNativeInterceptionEnabled is disabled', () => { + config.networkInterceptionMode = NetworkInterceptionMode.native; + jest.spyOn(NativeNetworkLogger, 'isNativeInterceptionEnabled').mockReturnValue(false); + jest.spyOn(NativeNetworkLogger, 'hasAPMNetworkPlugin').mockReturnValue(Promise.resolve(true)); + const logSpy = jest.spyOn(global.console, 'error'); + + Instabug.init(config); + + fakeTimer(() => { + expect(logSpy).toBeCalledTimes(1); + expect(logSpy).toBeCalledWith( + InstabugConstants.IBG_APM_TAG + InstabugConstants.NATIVE_INTERCEPTION_DISABLED_MESSAGE, + ); + }); + }); }); diff --git a/test/modules/NetworkLogger.spec.ts b/test/modules/NetworkLogger.spec.ts index 71dd2dd77..3c95024a5 100644 --- a/test/modules/NetworkLogger.spec.ts +++ b/test/modules/NetworkLogger.spec.ts @@ -7,13 +7,27 @@ 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 * as Instabug from '../../src/modules/Instabug'; +import { + NativeNetworkLogger, + NativeNetworkLoggerEvent, + NetworkListenerType, + NetworkLoggerEmitter, +} from '../../src/native/NativeNetworkLogger'; +import { InvocationEvent, LogLevel, NetworkInterceptionMode } from '../../src'; +import { Platform } from 'react-native'; +import { Logger } from '../../src/utils/logger'; +import { NativeInstabug } from '../../src/native/NativeInstabug'; const clone = (obj: T): T => { return JSON.parse(JSON.stringify(obj)); }; +jest.mock('../../src/native/NativeNetworkLogger'); + describe('NetworkLogger Module', () => { const network: NetworkLogger.NetworkData = { + id: '', url: 'https://api.instabug.com', requestBody: '', requestHeaders: { 'content-type': 'application/json' }, @@ -30,6 +44,11 @@ describe('NetworkLogger Module', () => { startTime: 0, serverErrorMessage: '', requestContentType: 'application/json', + isW3cHeaderFound: null, + partialId: null, + networkStartTimeInSeconds: null, + w3cGeneratedHeader: null, + w3cCaughtHeader: null, }; beforeEach(() => { @@ -58,22 +77,23 @@ describe('NetworkLogger Module', () => { expect(Interceptor.disableInterception).toBeCalledTimes(1); }); - it('should report the network log', () => { + it('should report the network log', async () => { Interceptor.setOnDoneCallback = jest .fn() - .mockImplementation((callback) => callback(clone(network))); + .mockImplementation(async (callback) => await callback(clone(network))); - NetworkLogger.setEnabled(true); + await NetworkLogger.setEnabled(true); expect(reportNetworkLog).toBeCalledTimes(1); expect(reportNetworkLog).toBeCalledWith(network); + expect(NativeInstabug.getNetworkBodyMaxSize).toBeCalledTimes(1); }); it('should send log network when setNetworkDataObfuscationHandler is set', async () => { const randomString = '28930q938jqhd'; Interceptor.setOnDoneCallback = jest .fn() - .mockImplementation((callback) => callback(clone(network))); + .mockImplementation(async (callback) => await callback(clone(network))); NetworkLogger.setNetworkDataObfuscationHandler((networkData) => { networkData.requestHeaders.token = randomString; return Promise.resolve(networkData); @@ -89,7 +109,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 +153,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(() => { @@ -149,8 +169,8 @@ describe('NetworkLogger Module', () => { consoleSpy.mockRestore(); }); - it('should omit request body if its content type is not allowed', () => { - const consoleWarn = jest.spyOn(console, 'warn').mockImplementation(); + it('should omit request body if its content type is not allowed', async () => { + const consoleWarn = jest.spyOn(Logger, 'warn').mockImplementation(); jest.mocked(isContentTypeNotAllowed).mockReturnValueOnce(true); const networkData = { @@ -160,9 +180,9 @@ describe('NetworkLogger Module', () => { Interceptor.setOnDoneCallback = jest .fn() - .mockImplementation((callback) => callback(networkData)); + .mockImplementation(async (callback) => await callback(networkData)); - NetworkLogger.setEnabled(true); + await NetworkLogger.setEnabled(true); expect(reportNetworkLog).toHaveBeenCalledWith({ ...networkData, @@ -174,8 +194,8 @@ describe('NetworkLogger Module', () => { consoleWarn.mockRestore(); }); - it('should omit response body if its content type is not allowed', () => { - const consoleWarn = jest.spyOn(console, 'warn').mockImplementation(); + it('should omit response body if its content type is not allowed', async () => { + const consoleWarn = jest.spyOn(Logger, 'warn').mockImplementation(); jest.mocked(isContentTypeNotAllowed).mockReturnValueOnce(true); const networkData = { @@ -185,9 +205,9 @@ describe('NetworkLogger Module', () => { Interceptor.setOnDoneCallback = jest .fn() - .mockImplementation((callback) => callback(networkData)); + .mockImplementation(async (callback) => await callback(networkData)); - NetworkLogger.setEnabled(true); + await NetworkLogger.setEnabled(true); expect(reportNetworkLog).toHaveBeenCalledWith({ ...networkData, @@ -199,23 +219,26 @@ describe('NetworkLogger Module', () => { consoleWarn.mockRestore(); }); - it('should omit request body if its size exceeds the maximum allowed size', () => { - const consoleWarn = jest.spyOn(console, 'warn').mockImplementation(); + it('should omit request body if its size exceeds the maximum allowed size', async () => { + const consoleWarn = jest.spyOn(Logger, 'warn').mockImplementation(); + const MAX_NETWORK_BODY_SIZE_IN_BYTES = await NativeInstabug.getNetworkBodyMaxSize(); const networkData = { ...network, - requestBodySize: InstabugConstants.MAX_NETWORK_BODY_SIZE_IN_BYTES + 1, + requestBodySize: MAX_NETWORK_BODY_SIZE_IN_BYTES + 1, }; Interceptor.setOnDoneCallback = jest .fn() - .mockImplementation((callback) => callback(networkData)); + .mockImplementation(async (callback) => await callback(networkData)); - NetworkLogger.setEnabled(true); + await NetworkLogger.setEnabled(true); expect(reportNetworkLog).toHaveBeenCalledWith({ ...networkData, - requestBody: InstabugConstants.MAX_REQUEST_BODY_SIZE_EXCEEDED_MESSAGE, + requestBody: `${InstabugConstants.MAX_REQUEST_BODY_SIZE_EXCEEDED_MESSAGE}${ + MAX_NETWORK_BODY_SIZE_IN_BYTES / 1024 + } Kb`, }); expect(consoleWarn).toBeCalledTimes(1); @@ -223,38 +246,43 @@ describe('NetworkLogger Module', () => { consoleWarn.mockRestore(); }); - it('should not omit request body if its size does not exceed the maximum allowed size', () => { + it('should not omit request body if its size does not exceed the maximum allowed size', async () => { + const MAX_NETWORK_BODY_SIZE_IN_BYTES = await NativeInstabug.getNetworkBodyMaxSize(); + const networkData = { ...network, - requestBodySize: InstabugConstants.MAX_NETWORK_BODY_SIZE_IN_BYTES, + requestBodySize: MAX_NETWORK_BODY_SIZE_IN_BYTES, }; Interceptor.setOnDoneCallback = jest .fn() - .mockImplementation((callback) => callback(networkData)); + .mockImplementation(async (callback) => await callback(networkData)); - NetworkLogger.setEnabled(true); + await 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(); + it('should omit response body if its size exceeds the maximum allowed size', async () => { + const consoleWarn = jest.spyOn(Logger, 'warn').mockImplementation(); + const MAX_NETWORK_BODY_SIZE_IN_BYTES = await NativeInstabug.getNetworkBodyMaxSize(); const networkData = { ...network, - responseBodySize: InstabugConstants.MAX_NETWORK_BODY_SIZE_IN_BYTES + 1, + responseBodySize: MAX_NETWORK_BODY_SIZE_IN_BYTES + 1, }; Interceptor.setOnDoneCallback = jest .fn() - .mockImplementation((callback) => callback(networkData)); + .mockImplementation(async (callback) => await callback(networkData)); - NetworkLogger.setEnabled(true); + await NetworkLogger.setEnabled(true); expect(reportNetworkLog).toHaveBeenCalledWith({ ...networkData, - responseBody: InstabugConstants.MAX_RESPONSE_BODY_SIZE_EXCEEDED_MESSAGE, + responseBody: `${InstabugConstants.MAX_RESPONSE_BODY_SIZE_EXCEEDED_MESSAGE}${ + MAX_NETWORK_BODY_SIZE_IN_BYTES / 1024 + } Kb`, }); expect(consoleWarn).toBeCalledTimes(1); @@ -262,18 +290,141 @@ describe('NetworkLogger Module', () => { consoleWarn.mockRestore(); }); - it('should not omit response body if its size does not exceed the maximum allowed size', () => { + it('should not omit response body if its size does not exceed the maximum allowed size', async () => { + const MAX_NETWORK_BODY_SIZE_IN_BYTES = await NativeInstabug.getNetworkBodyMaxSize(); + const networkData = { ...network, - responseBodySize: InstabugConstants.MAX_NETWORK_BODY_SIZE_IN_BYTES, + responseBodySize: MAX_NETWORK_BODY_SIZE_IN_BYTES, }; Interceptor.setOnDoneCallback = jest .fn() - .mockImplementation((callback) => callback(networkData)); + .mockImplementation(async (callback) => await callback(networkData)); - NetworkLogger.setEnabled(true); + await NetworkLogger.setEnabled(true); expect(reportNetworkLog).toHaveBeenCalledWith(networkData); }); + + it('should call the native method setNetworkLogBodyEnabled', () => { + NetworkLogger.setNetworkLogBodyEnabled(true); + + expect(NativeInstabug.setNetworkLogBodyEnabled).toBeCalledTimes(1); + expect(NativeInstabug.setNetworkLogBodyEnabled).toBeCalledWith(true); + }); + + it('Instabug.init should call NativeNetworkLogger.isNativeInterceptionEnabled and not call NativeNetworkLogger.hasAPMNetworkPlugin with iOS', async () => { + Platform.OS = 'ios'; + const config = { + token: 'some-token', + invocationEvents: [InvocationEvent.floatingButton, InvocationEvent.shake], + debugLogsLevel: LogLevel.debug, + networkInterceptionMode: NetworkInterceptionMode.native, + codePushVersion: '1.1.0', + }; + await Instabug.init(config); + + expect(NativeNetworkLogger.isNativeInterceptionEnabled).toHaveBeenCalled(); + expect(NativeNetworkLogger.hasAPMNetworkPlugin).not.toHaveBeenCalled(); + }); +}); + +describe('_registerNetworkLogsListener', () => { + let handlerMock: jest.Mock; + let type: NetworkListenerType; + + beforeEach(() => { + handlerMock = jest.fn(); + type = NetworkListenerType.both; + jest.resetAllMocks(); // Reset mock implementation and calls + NetworkLogger.resetNetworkListener(); // Clear only calls, keeping implementation intact + }); + + it('should remove old listeners if they exist', () => { + Platform.OS = 'ios'; + + // Simulate that there are existing listeners + jest.spyOn(NetworkLoggerEmitter, 'listenerCount').mockReturnValue(2); + + NetworkLogger.registerNetworkLogsListener(type, handlerMock); + + expect(NetworkLoggerEmitter.removeAllListeners).toHaveBeenCalledWith( + NativeNetworkLoggerEvent.NETWORK_LOGGER_HANDLER, + ); + }); + + it('should set the new listener if _networkListener is null', () => { + Platform.OS = 'ios'; + // No existing listener + jest.spyOn(NetworkLoggerEmitter, 'listenerCount').mockReturnValue(0); + + NetworkLogger.registerNetworkLogsListener(type, handlerMock); + + expect(NetworkLoggerEmitter.addListener).toHaveBeenCalled(); + expect(NativeNetworkLogger.registerNetworkLogsListener).toHaveBeenCalledWith(type); + }); + + it('should attach a new listener to the existing one if _networkListener is set', () => { + Platform.OS = 'ios'; + + type = NetworkListenerType.filtering; + const newType = NetworkListenerType.both; + + // First call to set the listener + NetworkLogger.registerNetworkLogsListener(type, handlerMock); + + // Second call with a different type to trigger setting to `both` + NetworkLogger.registerNetworkLogsListener(newType, handlerMock); + + expect(NetworkLoggerEmitter.addListener).toHaveBeenCalledTimes(2); + expect(NativeNetworkLogger.registerNetworkLogsListener).toHaveBeenCalledWith( + NetworkListenerType.both, + ); + }); + + it('should map networkSnapshot data correctly and call handler', () => { + const mockNetworkSnapshot = { + id: '123', + url: 'http://example.com', + requestHeader: {}, + requestBody: 'test request body', + responseHeader: {}, + response: 'test response', + responseCode: 200, + }; + + (NetworkLoggerEmitter.addListener as jest.Mock).mockImplementation((_, callback) => { + callback(mockNetworkSnapshot); + }); + + NetworkLogger.registerNetworkLogsListener(type, handlerMock); + + const expectedNetworkData: NetworkLogger.NetworkData = { + id: '123', + url: 'http://example.com', + requestBody: 'test request body', + requestHeaders: {}, + method: '', + responseBody: 'test response', + responseCode: 200, + responseHeaders: {}, + contentType: '', + duration: 0, + requestBodySize: 0, + responseBodySize: 0, + errorDomain: '', + errorCode: 0, + startTime: 0, + serverErrorMessage: '', + requestContentType: '', + isW3cHeaderFound: true, + networkStartTimeInSeconds: 0, + partialId: 0, + w3cCaughtHeader: '', + w3cGeneratedHeader: '', + }; + + expect(handlerMock).toHaveBeenCalledWith(expectedNetworkData); + }); }); diff --git a/test/utils/AppStatesHandler.spec.ts b/test/utils/AppStatesHandler.spec.ts new file mode 100644 index 000000000..c784da86a --- /dev/null +++ b/test/utils/AppStatesHandler.spec.ts @@ -0,0 +1,60 @@ +import { AppState } from 'react-native'; +import { addAppStateListener, removeAppStateListener } from '../../src/utils/AppStatesHandler'; + +jest.mock('react-native', () => ({ + AppState: { + addEventListener: jest.fn(), + }, +})); + +describe('AppState Listener', () => { + const mockHandleAppStateChange = jest.fn(); + const mockRemove = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + (AppState.addEventListener as jest.Mock).mockReturnValue({ remove: mockRemove }); + }); + + afterEach(() => { + removeAppStateListener(); // Ensure no leftover subscriptions between tests + }); + + it('should add an AppState listener if none exists', () => { + addAppStateListener(mockHandleAppStateChange); + + expect(AppState.addEventListener).toHaveBeenCalledTimes(1); + expect(AppState.addEventListener).toHaveBeenCalledWith('change', mockHandleAppStateChange); + }); + + it('should not add another listener if one already exists', () => { + addAppStateListener(mockHandleAppStateChange); + addAppStateListener(mockHandleAppStateChange); + + expect(AppState.addEventListener).toHaveBeenCalledTimes(1); // Only called once + }); + + it('should remove the AppState listener if one exists', () => { + addAppStateListener(mockHandleAppStateChange); + removeAppStateListener(); + + expect(mockRemove).toHaveBeenCalledTimes(1); // The remove function is called + }); + + it('should do nothing if removeAppStateListener is called without an existing subscription', () => { + removeAppStateListener(); + + expect(mockRemove).not.toHaveBeenCalled(); // No remove is called + }); + + it('should handle multiple add/remove calls properly', () => { + addAppStateListener(mockHandleAppStateChange); + removeAppStateListener(); + + addAppStateListener(mockHandleAppStateChange); + removeAppStateListener(); + + expect(AppState.addEventListener).toHaveBeenCalledTimes(2); // Listener is added twice + expect(mockRemove).toHaveBeenCalledTimes(2); // Listener is removed twice + }); +}); diff --git a/test/utils/InstabugUtils.spec.ts b/test/utils/InstabugUtils.spec.ts index becfccc0e..49ce13c58 100644 --- a/test/utils/InstabugUtils.spec.ts +++ b/test/utils/InstabugUtils.spec.ts @@ -4,16 +4,21 @@ import { Platform } from 'react-native'; import parseErrorStackLib from 'react-native/Libraries/Core/Devtools/parseErrorStack'; import * as Instabug from '../../src/modules/Instabug'; +import * as NetworkLogger from '../../src/modules/NetworkLogger'; import { NativeCrashReporting } from '../../src/native/NativeCrashReporting'; import { InvocationEvent, NetworkData, NonFatalErrorLevel } from '../../src'; -import InstabugUtils, { - getStackTrace, - reportNetworkLog, - sendCrashReport, -} from '../../src/utils/InstabugUtils'; +import * as InstabugUtils from '../../src/utils/InstabugUtils'; + +import { + NativeNetworkLogger, + NetworkListenerType, + NetworkLoggerEmitter, +} from '../../src/native/NativeNetworkLogger'; import { NativeInstabug } from '../../src/native/NativeInstabug'; import { NativeAPM } from '../../src/native/NativeAPM'; +jest.mock('../../src/modules/NetworkLogger'); + describe('Test global error handler', () => { beforeEach(() => { Instabug.init({ token: '', invocationEvents: [InvocationEvent.none] }); @@ -189,9 +194,9 @@ describe('Instabug Utils', () => { const remoteSenderCallback = NativeCrashReporting.sendHandledJSCrash; Platform.OS = 'android'; const errorMock = new TypeError('Invalid type'); - const jsStackTrace = getStackTrace(errorMock); + const jsStackTrace = InstabugUtils.getStackTrace(errorMock); - sendCrashReport(errorMock, (data) => + InstabugUtils.sendCrashReport(errorMock, (data) => remoteSenderCallback(data, null, null, NonFatalErrorLevel.error), ); @@ -217,9 +222,9 @@ describe('Instabug Utils', () => { const remoteSenderCallback = NativeCrashReporting.sendHandledJSCrash; Platform.OS = 'ios'; const errorMock = new TypeError('Invalid type'); - const jsStackTrace = getStackTrace(errorMock); + const jsStackTrace = InstabugUtils.getStackTrace(errorMock); - sendCrashReport(errorMock, (data) => + InstabugUtils.sendCrashReport(errorMock, (data) => remoteSenderCallback(data, null, null, NonFatalErrorLevel.error), ); const expectedMap = { @@ -242,6 +247,7 @@ describe('Instabug Utils', () => { describe('reportNetworkLog', () => { const network: NetworkData = { + id: 'id', url: 'https://api.instabug.com', method: 'GET', requestBody: 'requestBody', @@ -258,15 +264,22 @@ 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', () => { + it('reportNetworkLog should send network logs to native with the correct parameters on Android', async () => { Platform.OS = 'android'; + jest.spyOn(NativeNetworkLogger, 'isNativeInterceptionEnabled').mockReturnValue(false); + jest.spyOn(NativeNetworkLogger, 'hasAPMNetworkPlugin').mockReturnValue(Promise.resolve(false)); + Instabug.init({ token: '', invocationEvents: [InvocationEvent.none] }); const requestHeaders = JSON.stringify(network.requestHeaders); const responseHeaders = JSON.stringify(network.responseHeaders); - - reportNetworkLog(network); + InstabugUtils.reportNetworkLog(network); expect(NativeInstabug.networkLogAndroid).toHaveBeenCalledTimes(1); expect(NativeInstabug.networkLogAndroid).toHaveBeenCalledWith( @@ -296,6 +309,13 @@ describe('reportNetworkLog', () => { network.responseCode, network.contentType, network.errorDomain, + { + isW3cHeaderFound: null, + partialId: null, + networkStartTimeInSeconds: null, + w3cGeneratedHeader: null, + w3cCaughtHeader: null, + }, network.gqlQueryName, network.serverErrorMessage, ); @@ -304,7 +324,7 @@ describe('reportNetworkLog', () => { it('reportNetworkLog should send network logs to native with the correct parameters on iOS', () => { Platform.OS = 'ios'; - reportNetworkLog(network); + InstabugUtils.reportNetworkLog(network); expect(NativeInstabug.networkLogIOS).toHaveBeenCalledTimes(1); expect(NativeInstabug.networkLogIOS).toHaveBeenCalledWith( @@ -324,6 +344,201 @@ describe('reportNetworkLog', () => { network.duration, network.gqlQueryName, network.serverErrorMessage, + { + isW3cHeaderFound: null, + partialId: null, + networkStartTimeInSeconds: null, + w3cGeneratedHeader: null, + w3cCaughtHeader: null, + }, ); }); }); + +describe('test registerNetworkLogsListener usage', () => { + beforeEach(() => { + jest.clearAllMocks(); // Clear all mocks before each test + }); + + const network: NetworkLogger.NetworkData = { + id: '', + url: 'https://api.instabug.com', + requestBody: '', + requestHeaders: { 'content-type': 'application/json' }, + method: 'GET', + responseBody: '', + responseCode: 200, + responseHeaders: { 'content-type': 'application/json' }, + contentType: 'application/json', + duration: 0, + requestBodySize: 0, + responseBodySize: 0, + errorDomain: '', + errorCode: 0, + startTime: 0, + serverErrorMessage: '', + requestContentType: 'application/json', + isW3cHeaderFound: true, + networkStartTimeInSeconds: 0, + partialId: 0, + w3cCaughtHeader: '', + w3cGeneratedHeader: '', + }; + + it('registerObfuscationListener should call NetworkLogger.registerNetworkLogsListener() with NetworkListenerType = NetworkListenerType.obfuscation', () => { + InstabugUtils.registerObfuscationListener(); + expect(NetworkLogger.registerNetworkLogsListener).toBeCalledTimes(1); + expect(NetworkLogger.registerNetworkLogsListener).toBeCalledWith( + NetworkListenerType.obfuscation, + expect.any(Function), + ); + }); + + it('registerFilteringListener should call NetworkLogger.registerNetworkLogsListener() with NetworkListenerType = NetworkListenerType.filtering', () => { + const testText = 'true'; + InstabugUtils.registerFilteringListener(testText); + + expect(NetworkLogger.registerNetworkLogsListener).toBeCalledTimes(1); + expect(NetworkLogger.registerNetworkLogsListener).toBeCalledWith( + NetworkListenerType.filtering, + expect.any(Function), + ); + }); + + it('registerFilteringAndObfuscationListener should call NetworkLogger.registerNetworkLogsListener() with NetworkListenerType = NetworkListenerType.both', () => { + const testText = 'true'; + InstabugUtils.registerFilteringAndObfuscationListener(testText); + + expect(NetworkLogger.registerNetworkLogsListener).toBeCalledTimes(1); + expect(NetworkLogger.registerNetworkLogsListener).toBeCalledWith( + NetworkListenerType.both, + expect.any(Function), + ); + }); + + it('should call NetworkLoggerEmitter.removeAllListeners when call resetNativeObfuscationListener', () => { + jest.spyOn(NetworkLoggerEmitter, 'removeAllListeners').mockImplementation(); + InstabugUtils.resetNativeObfuscationListener(); + expect(NetworkLoggerEmitter.removeAllListeners).toBeCalledTimes(1); + }); + + it('should call NativeNetworkLogger.resetNetworkLogsListener when call resetNativeObfuscationListener on android platform', () => { + Platform.OS = 'android'; + jest.spyOn(NativeNetworkLogger, 'resetNetworkLogsListener').mockImplementation(); + jest.spyOn(NetworkLoggerEmitter, 'removeAllListeners').mockImplementation(); + InstabugUtils.resetNativeObfuscationListener(); + expect(NativeNetworkLogger.resetNetworkLogsListener).toBeCalledTimes(1); + expect(NetworkLoggerEmitter.removeAllListeners).toBeCalledTimes(1); + }); + + it('should call NativeNetworkLogger.updateNetworkLogSnapshot when call updateNetworkLogSnapshot with correct parameters', () => { + jest.spyOn(NativeNetworkLogger, 'updateNetworkLogSnapshot').mockImplementation(); + + InstabugUtils.updateNetworkLogSnapshot(network); + expect(NativeNetworkLogger.updateNetworkLogSnapshot).toBeCalledTimes(1); + expect(NativeNetworkLogger.updateNetworkLogSnapshot).toHaveBeenCalledWith( + network.url, + network.id, + network.requestBody, + network.responseBody, + network.responseCode ?? 200, + network.requestHeaders, + network.responseHeaders, + ); + }); +}); + +describe('InstabugUtils', () => { + it('setApmNetworkFlagsIfChanged should return true if flags change', () => { + const flags = { + isNativeInterceptionFeatureEnabled: true, + hasAPMNetworkPlugin: true, + shouldEnableNativeInterception: true, + }; + expect(InstabugUtils.setApmNetworkFlagsIfChanged(flags)).toBe(true); + expect(InstabugUtils.setApmNetworkFlagsIfChanged(flags)).toBe(false); + }); + + it('generateTracePartialId should return a non-zero hex string and number', () => { + const { numberPartilId, hexStringPartialId } = InstabugUtils.generateTracePartialId(); + expect(hexStringPartialId).toMatch(/^[0-9a-f]{8}$/); + expect(hexStringPartialId).not.toBe('00000000'); + expect(typeof numberPartilId).toBe('number'); + expect(numberPartilId).not.toBe(0); + }); + + it('generateW3CHeader should return a valid w3c header object', () => { + const now = Date.now(); + const result = InstabugUtils.generateW3CHeader(now); + expect(result).toHaveProperty('timestampInSeconds'); + expect(result).toHaveProperty('partialId'); + expect(result).toHaveProperty('w3cHeader'); + expect(typeof result.w3cHeader).toBe('string'); + expect(result.w3cHeader.split('-').length).toBe(4); + }); + + it('isContentTypeNotAllowed should return false for allowed types and true for not allowed', () => { + expect(InstabugUtils.isContentTypeNotAllowed('application/json')).toBe(false); + expect(InstabugUtils.isContentTypeNotAllowed('text/plain')).toBe(false); + expect(InstabugUtils.isContentTypeNotAllowed('image/png')).toBe(true); + expect(InstabugUtils.isContentTypeNotAllowed('application/pdf')).toBe(true); + }); +}); + +describe('checkNetworkRequestHandlers', () => { + let registerNetworkLogsListenerSpy: jest.SpyInstance; + + beforeEach(() => { + jest.clearAllMocks(); + registerNetworkLogsListenerSpy = jest + .spyOn(NetworkLogger, 'registerNetworkLogsListener') + .mockImplementation(jest.fn()); + }); + + it('should register for both if obfuscation handler and filter expression exist', () => { + jest.spyOn(NetworkLogger, 'getNetworkDataObfuscationHandler').mockReturnValue(jest.fn()); + jest.spyOn(NetworkLogger, 'hasRequestFilterExpression').mockReturnValue(true); + jest.spyOn(NetworkLogger, 'getRequestFilterExpression').mockReturnValue('true'); + + InstabugUtils.checkNetworkRequestHandlers(); + + expect(registerNetworkLogsListenerSpy).toHaveBeenCalledWith( + NetworkListenerType.both, + expect.any(Function), + ); + }); + + it('should register for obfuscation only if only obfuscation handler exists', () => { + jest.spyOn(NetworkLogger, 'getNetworkDataObfuscationHandler').mockReturnValue(jest.fn()); + jest.spyOn(NetworkLogger, 'hasRequestFilterExpression').mockReturnValue(false); + + InstabugUtils.checkNetworkRequestHandlers(); + + expect(registerNetworkLogsListenerSpy).toHaveBeenCalledWith( + NetworkListenerType.obfuscation, + expect.any(Function), + ); + }); + + it('should register for filtering only if only filter expression exists', () => { + jest.spyOn(NetworkLogger, 'getNetworkDataObfuscationHandler').mockReturnValue(undefined); + jest.spyOn(NetworkLogger, 'hasRequestFilterExpression').mockReturnValue(true); + jest.spyOn(NetworkLogger, 'getRequestFilterExpression').mockReturnValue('true'); + + InstabugUtils.checkNetworkRequestHandlers(); + + expect(registerNetworkLogsListenerSpy).toHaveBeenCalledWith( + NetworkListenerType.filtering, + expect.any(Function), + ); + }); + + it('should not register any listener if neither exist', () => { + jest.spyOn(NetworkLogger, 'getNetworkDataObfuscationHandler').mockReturnValue(undefined); + jest.spyOn(NetworkLogger, 'hasRequestFilterExpression').mockReturnValue(false); + + InstabugUtils.checkNetworkRequestHandlers(); + + expect(registerNetworkLogsListenerSpy).not.toHaveBeenCalled(); + }); +}); diff --git a/test/utils/XhrNetworkInterceptor.spec.ts b/test/utils/XhrNetworkInterceptor.spec.ts index 10a8f1abb..dfb9e7f43 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(); + }); +});