From 91cb5bef1b71a9de01fd876fc2cf403ccc9e3344 Mon Sep 17 00:00:00 2001 From: Ahmed alaa Date: Wed, 3 Jul 2024 18:16:57 +0300 Subject: [PATCH 1/9] feat: add android feature flag --- .circleci/config.yml | 9 +- CHANGELOG.md | 87 +- android/build.gradle | 7 +- android/src/main/AndroidManifest.xml | 7 + .../com/instabug/flutter/modules/ApmApi.java | 80 +- .../instabug/flutter/modules/InstabugApi.java | 108 +- .../flutter/modules/SessionReplayApi.java | 4 - .../java/com/instabug/flutter/ApmApiTest.java | 135 +- .../com/instabug/flutter/InstabugApiTest.java | 90 +- .../flutter/SessionReplayApiTest.java | 16 +- .../instabug/flutter/util/GlobalMocks.java | 21 +- .../instabug/flutter/util/MockReflected.java | 10 +- e2e/BugReportingTests.cs | 2 +- e2e/SurveysTests.cs | 2 +- example/analysis_options.yaml | 28 + example/android/app/build.gradle | 4 - .../flutter/example/BugReportingUITest.java | 176 --- .../example/FeatureRequestsUITest.java | 32 - .../flutter/example/InstabugUITest.java | 46 - .../flutter/example/SurveysUITest.java | 30 - .../util/HasBackgroundColorMatcher.java | 33 - .../example/util/InstabugAssertions.java | 19 - .../example/util/InstabugViewMatchers.java | 16 - .../example/util/IsToTheLeftMatcher.java | 18 - .../flutter/example/util/Keyboard.java | 28 - .../example/InstabugSample/MainActivity.kt | 3 +- example/ios/InstabugTests/ApmApiTests.m | 99 ++ example/ios/InstabugTests/InstabugApiTests.m | 201 ++- example/ios/InstabugTests/Util/Apm+Test.h | 11 + .../Util/IBGNetworkLogger+Test.h | 7 +- .../ios/InstabugUITests/BugReportingUITests.m | 96 -- .../InstabugUITests/FeatureRequestsUITests.m | 25 - example/ios/InstabugUITests/Info.plist | 22 - example/ios/InstabugUITests/InstabugUITests.m | 36 - example/ios/InstabugUITests/SurveysUITests.m | 25 - .../Util/InstabugUITestsUtils.h | 9 - .../Util/InstabugUITestsUtils.m | 28 - .../Util/XCUIElement+Instabug.h | 12 - .../Util/XCUIElement+Instabug.m | 50 - example/ios/Podfile | 12 +- example/ios/Podfile.lock | 14 +- example/ios/Runner.xcodeproj/project.pbxproj | 248 ---- example/ios/Runner/AppDelegate.swift | 20 +- example/lib/main.dart | 12 +- example/lib/src/app_routes.dart | 4 + .../lib/src/components/network_content.dart | 18 +- example/lib/src/screens/apm_page.dart | 19 + example/lib/src/screens/complex_page.dart | 49 +- example/lib/src/screens/my_home_page.dart | 91 +- ...reen_capture_premature_extension_page.dart | 30 + .../lib/src/screens/screen_loading_page.dart | 186 +++ example/pubspec.lock | 66 +- example/pubspec.yaml | 10 +- ios/Classes/Modules/ApmApi.m | 47 + ios/Classes/Modules/InstabugApi.m | 136 +- ios/Classes/Util/IBGAPM+PrivateAPIs.h | 25 + ios/Classes/Util/IBGNetworkLogger+CP.h | 22 + .../Util/NativeUtils/IBGTimeIntervalUnits.h | 24 + ios/instabug_flutter.podspec | 4 +- lib/instabug_flutter.dart | 6 + lib/src/models/feature_flag.dart | 9 + lib/src/models/generated_w3c_header.dart | 24 + lib/src/models/instabug_route.dart | 11 + lib/src/models/network_data.dart | 60 +- lib/src/models/trace_partial_id.dart | 20 + lib/src/models/w3c_feature_flags.dart | 11 + lib/src/models/w3c_header.dart | 34 + lib/src/modules/apm.dart | 87 ++ lib/src/modules/instabug.dart | 63 +- lib/src/modules/network_logger.dart | 61 +- lib/src/utils/feature_flags_manager.dart | 92 ++ lib/src/utils/instabug_logger.dart | 93 ++ lib/src/utils/instabug_montonic_clock.dart | 22 + .../utils/instabug_navigator_observer.dart | 39 +- lib/src/utils/iterable_ext.dart | 8 + lib/src/utils/repro_steps_constants.dart | 3 + .../utils/screen_loading/flags_config.dart | 23 + .../instabug_capture_screen_loading.dart | 56 + .../utils/screen_loading/route_matcher.dart | 92 ++ .../screen_loading_manager.dart | 449 +++++++ .../screen_loading/screen_loading_trace.dart | 50 + lib/src/utils/screen_loading/ui_trace.dart | 47 + lib/src/utils/screen_name_masker.dart | 44 + lib/src/utils/w3c_header_utils.dart | 66 + pigeons/apm.api.dart | 18 + pigeons/instabug.api.dart | 18 + pubspec.yaml | 7 +- test/apm_test.dart | 83 ++ test/feature_flags_manager_test.dart | 84 ++ test/instabug_test.dart | 84 +- test/network_logger_test.dart | 20 +- test/route_matcher_test.dart | 150 +++ .../instabug_navigator_observer_test.dart | 140 ++ .../screen_loading_manager_test.dart | 1162 +++++++++++++++++ test/utils/screen_name_masker_test.dart | 77 ++ test/w3_header_utils_test.dart | 68 + 96 files changed, 4892 insertions(+), 1158 deletions(-) create mode 100644 example/analysis_options.yaml delete mode 100644 example/android/app/src/androidTest/java/com/instabug/flutter/example/BugReportingUITest.java delete mode 100644 example/android/app/src/androidTest/java/com/instabug/flutter/example/FeatureRequestsUITest.java delete mode 100644 example/android/app/src/androidTest/java/com/instabug/flutter/example/InstabugUITest.java delete mode 100644 example/android/app/src/androidTest/java/com/instabug/flutter/example/SurveysUITest.java delete mode 100644 example/android/app/src/androidTest/java/com/instabug/flutter/example/util/HasBackgroundColorMatcher.java delete mode 100644 example/android/app/src/androidTest/java/com/instabug/flutter/example/util/InstabugAssertions.java delete mode 100644 example/android/app/src/androidTest/java/com/instabug/flutter/example/util/InstabugViewMatchers.java delete mode 100644 example/android/app/src/androidTest/java/com/instabug/flutter/example/util/IsToTheLeftMatcher.java delete mode 100644 example/android/app/src/androidTest/java/com/instabug/flutter/example/util/Keyboard.java create mode 100644 example/ios/InstabugTests/Util/Apm+Test.h delete mode 100644 example/ios/InstabugUITests/BugReportingUITests.m delete mode 100644 example/ios/InstabugUITests/FeatureRequestsUITests.m delete mode 100644 example/ios/InstabugUITests/Info.plist delete mode 100644 example/ios/InstabugUITests/InstabugUITests.m delete mode 100644 example/ios/InstabugUITests/SurveysUITests.m delete mode 100644 example/ios/InstabugUITests/Util/InstabugUITestsUtils.h delete mode 100644 example/ios/InstabugUITests/Util/InstabugUITestsUtils.m delete mode 100644 example/ios/InstabugUITests/Util/XCUIElement+Instabug.h delete mode 100644 example/ios/InstabugUITests/Util/XCUIElement+Instabug.m create mode 100644 example/lib/src/screens/screen_capture_premature_extension_page.dart create mode 100644 example/lib/src/screens/screen_loading_page.dart create mode 100644 ios/Classes/Util/IBGAPM+PrivateAPIs.h create mode 100644 ios/Classes/Util/NativeUtils/IBGTimeIntervalUnits.h create mode 100644 lib/src/models/feature_flag.dart create mode 100644 lib/src/models/generated_w3c_header.dart create mode 100644 lib/src/models/instabug_route.dart create mode 100644 lib/src/models/trace_partial_id.dart create mode 100644 lib/src/models/w3c_feature_flags.dart create mode 100644 lib/src/models/w3c_header.dart create mode 100644 lib/src/utils/feature_flags_manager.dart create mode 100644 lib/src/utils/instabug_logger.dart create mode 100644 lib/src/utils/instabug_montonic_clock.dart create mode 100644 lib/src/utils/iterable_ext.dart create mode 100644 lib/src/utils/repro_steps_constants.dart create mode 100644 lib/src/utils/screen_loading/flags_config.dart create mode 100644 lib/src/utils/screen_loading/instabug_capture_screen_loading.dart create mode 100644 lib/src/utils/screen_loading/route_matcher.dart create mode 100644 lib/src/utils/screen_loading/screen_loading_manager.dart create mode 100644 lib/src/utils/screen_loading/screen_loading_trace.dart create mode 100644 lib/src/utils/screen_loading/ui_trace.dart create mode 100644 lib/src/utils/screen_name_masker.dart create mode 100644 lib/src/utils/w3c_header_utils.dart create mode 100644 test/feature_flags_manager_test.dart create mode 100644 test/route_matcher_test.dart create mode 100644 test/utils/instabug_navigator_observer_test.dart create mode 100644 test/utils/screen_loading/screen_loading_manager_test.dart create mode 100644 test/utils/screen_name_masker_test.dart create mode 100644 test/w3_header_utils_test.dart diff --git a/.circleci/config.yml b/.circleci/config.yml index 3624090f7..67e56d8fa 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -309,8 +309,11 @@ workflows: - test_flutter-stable - test_flutter-2.10.5 - test_android - - e2e_android_captain - - test_ios + # TODO: Fix the flaky tests and add the following jobs to the dependencies for the release + # The following jobs have been removed while releasing v13.2.0 to avoid delaying the release + # as they were just flaky and the tests pass locally without issues. + # - e2e_android_captain + # - test_ios - e2e_ios_captain - verify_pub filters: @@ -321,4 +324,4 @@ workflows: - hold_release filters: branches: - only: master + only: master \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 68d4a153d..b8121ffca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,60 @@ # Changelog -## [13.1.1](https://github.com/Instabug/Instabug-Flutter/compare/v13.0.0...dev) (Jun,11 2024) +## [Unreleased](https://github.com/Instabug/Instabug-Flutter/compare/v13.4.0...dev) ### Added + +- Add support for tracing network requests from Instabug to services like Datadog and New Relic ([#481](https://github.com/Instabug/Instabug-Flutter/pull/481)). + +## [13.4.0](https://github.com/Instabug/Instabug-Flutter/compare/v13.3.0...v13.4.0) (September 29, 2024) + +### Added + +- Add support for masking screen names captured by Instabug through the `Instabug.setScreenNameMaskingCallback` API ([#500](https://github.com/Instabug/Instabug-Flutter/pull/500)). + +### Changed + +- Bump Instabug Android SDK to v13.4.1 ([#509](https://github.com/Instabug/Instabug-Flutter/pull/509)). See release notes for [13.4.0](https://github.com/Instabug/Instabug-Android/releases/tag/v13.4.0) and [13.4.1](https://github.com/Instabug/Instabug-Android/releases/tag/v13.4.1). +- Bump Instabug iOS SDK to v13.4.2 ([#515](https://github.com/Instabug/Instabug-Flutter/pull/515)). See release notes for [13.4.0](https://github.com/Instabug/Instabug-iOS/releases/tag/13.4.0), [13.4.1](https://github.com/Instabug/Instabug-iOS/releases/tag/13.4.1) and [13.4.2](https://github.com/Instabug/Instabug-iOS/releases/tag/13.4.2). + +### Fixed + +- Fixed an issue with empty screen names captured in `InstabugNavigatorObserver` and fallback to `N/A` when the screen name is empty ([#505](https://github.com/Instabug/Instabug-Flutter/pull/505)), closes [#504](https://github.com/Instabug/Instabug-Flutter/issues/504). + +## [13.3.0](https://github.com/Instabug/Instabug-Flutter/compare/v13.2.0...v13.3.0) (August 5, 2024) + +### Added + +- Add support for variants in Feature Flags through the APIs `Instabug.addFeatureFlags`, `Instabug.removeFeatureFlags` and `Instabug.clearAllFeatureFlags` ([#471](https://github.com/Instabug/Instabug-Flutter/pull/471)). + +### Changed + +- Bump Instabug Android SDK to v13.3.0 ([#492](https://github.com/Instabug/Instabug-Flutter/pull/492)). [See release notes](https://github.com/Instabug/Instabug-Android/releases/tag/v13.3.0). +- Bump Instabug iOS SDK to v13.3.0 ([#493](https://github.com/Instabug/Instabug-Flutter/pull/493)). [See release notes](https://github.com/Instabug/Instabug-iOS/releases/tag/13.3.0). + +### Deprecated + +- Deprecate Experiments APIs `Instabug.addExperiments`, `Instabug.removeExperiments` and `Instabug.clearAllExperiments` in favor of the new Feature Flags APIs ([#471](https://github.com/Instabug/Instabug-Flutter/pull/471)). + +### Fixed + +- Fix APM network logging on Android ([#491](https://github.com/Instabug/Instabug-Flutter/pull/491)). + +## [13.2.0](https://github.com/Instabug/Instabug-Flutter/compare/v13.1.1...v13.2.0) + +### Added + +- Add support for capturing screen loading time in APM ([#477](https://github.com/Instabug/Instabug-Flutter/pull/477)). + +### Changed + +- Bump Instabug Android SDK to v13.2.0 ([#482](https://github.com/Instabug/Instabug-Flutter/pull/482)). [See release notes](https://github.com/Instabug/Instabug-Android/releases/tag/v13.2.0). +- Bump Instabug iOS SDK to v13.2.0 ([#483](https://github.com/Instabug/Instabug-Flutter/pull/483)). [See release notes](https://github.com/Instabug/Instabug-iOS/releases/tag/13.2.0). + +## [13.1.1](https://github.com/Instabug/Instabug-Flutter/compare/v13.0.0...v13.1.1) (June 11, 2024) + +### Added + - Add support for passing a grouping fingerprint, error level, and user attributes to the `CrashReporting.reportHandledCrash` non-fatals API ([#461](https://github.com/Instabug/Instabug-Flutter/pull/461)). ### Changed @@ -10,7 +62,6 @@ - Bump Instabug iOS SDK to v13.1.0 ([#1227](https://github.com/Instabug/Instabug-Flutter/pull/1227)). [See release notes](https://github.com/Instabug/Instabug-iOS/releases/tag/13.1.0). - Bump Instabug Android SDK to v13.1.1 ([#474](https://github.com/Instabug/Instabug-Flutter/pull/474)). [See release notes](https://github.com/Instabug/Instabug-Android/releases/tag/v13.1.1). - ## [13.0.0](https://github.com/Instabug/Instabug-Flutter/compare/v12.7.0...dev) (April 29, 2024) ### Added @@ -148,18 +199,18 @@ Below is a list of all the affected APIs: - - `APM.startExecutionTrace` - - `BugReporting.setOnInvokeCallback` - - `BugReporting.setOnDismissCallback` - - `Instabug.getTags` - - `Instabug.getUserAttributeForKey` - - `Instabug.getUserAttributes` - - `Replies.getUnreadRepliesCount` - - `Replies.hasChats` - - `Replies.setOnNewReplyReceivedCallback` - - `Surveys.hasRespondToSurvey` - - `Surveys.setOnShowCallback` - - `Surveys.setOnDismissCallback` + - `APM.startExecutionTrace` + - `BugReporting.setOnInvokeCallback` + - `BugReporting.setOnDismissCallback` + - `Instabug.getTags` + - `Instabug.getUserAttributeForKey` + - `Instabug.getUserAttributes` + - `Replies.getUnreadRepliesCount` + - `Replies.hasChats` + - `Replies.setOnNewReplyReceivedCallback` + - `Surveys.hasRespondToSurvey` + - `Surveys.setOnShowCallback` + - `Surveys.setOnDismissCallback` ## [11.12.0](https://github.com/Instabug/Instabug-Flutter/compare/v11.10.1...v11.12.0) (May 30, 2023) @@ -236,10 +287,10 @@ - Deprecates Instabug.enableAndroid and Instabug.disableAndroid APIs in favour of the new API Instabug.setEnabled, which works on both platforms - Deprecates callbacks in favor of return values in the following APIs: - 1. Replies.getUnreadRepliesCount - 2. Replies.hasChats - 3. Surveys.hasRespondedToSurvey - 4. Surveys.getAvailableSurveys + 1. Replies.getUnreadRepliesCount + 2. Replies.hasChats + 3. Surveys.hasRespondedToSurvey + 4. Surveys.getAvailableSurveys ## 11.3.0 (2022-09-30) diff --git a/android/build.gradle b/android/build.gradle index 4d59dc8b5..d6c5c6c0b 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,5 +1,5 @@ group 'com.instabug.flutter' -version '13.1.1' +version '13.4.0' buildscript { repositories { @@ -41,10 +41,11 @@ android { } dependencies { - api 'com.instabug.library:instabug:13.1.1' - + api 'com.instabug.library:instabug:13.4.1' testImplementation 'junit:junit:4.13.2' testImplementation "org.mockito:mockito-inline:3.12.1" + testImplementation "io.mockk:mockk:1.13.13" + } // add upload_symbols task diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index 393eba80f..8c678b4e2 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -1,3 +1,10 @@ + + + + + diff --git a/android/src/main/java/com/instabug/flutter/modules/ApmApi.java b/android/src/main/java/com/instabug/flutter/modules/ApmApi.java index c0862acac..b28545745 100644 --- a/android/src/main/java/com/instabug/flutter/modules/ApmApi.java +++ b/android/src/main/java/com/instabug/flutter/modules/ApmApi.java @@ -1,15 +1,24 @@ package com.instabug.flutter.modules; import android.util.Log; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; + import com.instabug.apm.APM; +import com.instabug.apm.InternalAPM; +import com.instabug.apm.configuration.cp.APMFeature; +import com.instabug.apm.configuration.cp.FeatureAvailabilityCallback; import com.instabug.apm.model.ExecutionTrace; import com.instabug.apm.networking.APMNetworkLogger; import com.instabug.flutter.generated.ApmPigeon; import com.instabug.flutter.util.Reflection; import com.instabug.flutter.util.ThreadManager; +import com.instabug.apm.networkinterception.cp.APMCPNetworkLog; + import io.flutter.plugin.common.BinaryMessenger; + +import org.jetbrains.annotations.NotNull; import org.json.JSONObject; import java.lang.reflect.Method; @@ -202,9 +211,9 @@ public void networkLogAndroid(@NonNull Map data) { serverErrorMessage = (String) data.get("serverErrorMessage"); } - Method method = Reflection.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); + Method method = Reflection.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) { - method.invoke(apmNetworkLogger, requestStartTime, requestDuration, requestHeaders, requestBody, requestBodySize, requestMethod, requestUrl, requestContentType, responseHeaders, responseBody, responseBodySize, statusCode, responseContentType, errorMessage, gqlQueryName, serverErrorMessage); + method.invoke(apmNetworkLogger, requestStartTime, requestDuration, requestHeaders, requestBody, requestBodySize, requestMethod, requestUrl, requestContentType, responseHeaders, responseBody, responseBodySize, statusCode, responseContentType, errorMessage, gqlQueryName, serverErrorMessage, null); } else { Log.e(TAG, "APMNetworkLogger.log was not found by reflection"); } @@ -213,4 +222,71 @@ public void networkLogAndroid(@NonNull Map data) { e.printStackTrace(); } } + + + @Override + public void startCpUiTrace(@NonNull String screenName, @NonNull Long microTimeStamp, @NonNull Long traceId) { + try { + InternalAPM._startUiTraceCP(screenName, microTimeStamp, traceId); + } catch (Exception e) { + e.printStackTrace(); + } + } + + @Override + public void reportScreenLoadingCP(@NonNull Long startTimeStampMicro, @NonNull Long durationMicro, @NonNull Long uiTraceId) { + try { + InternalAPM._reportScreenLoadingCP(startTimeStampMicro, durationMicro, uiTraceId); + } catch (Exception e) { + e.printStackTrace(); + } + } + + @Override + public void endScreenLoadingCP(@NonNull Long timeStampMicro, @NonNull Long uiTraceId) { + try { + InternalAPM._endScreenLoadingCP(timeStampMicro, uiTraceId); + } catch (Exception e) { + e.printStackTrace(); + } + } + + @Override + public void isEndScreenLoadingEnabled(@NonNull ApmPigeon.Result result) { + isScreenLoadingEnabled(result); + } + + @Override + public void isEnabled(@NonNull ApmPigeon.Result result) { + try { + // TODO: replace true with an actual implementation of APM.isEnabled once implemented + // in the Android SDK. + result.success(true); + } catch (Exception e) { + e.printStackTrace(); + } + } + + @Override + public void isScreenLoadingEnabled(@NonNull ApmPigeon.Result result) { + try { + InternalAPM._isFeatureEnabledCP(APMFeature.SCREEN_LOADING, "InstabugCaptureScreenLoading", new FeatureAvailabilityCallback() { + @Override + public void invoke(boolean isFeatureAvailable) { + result.success(isFeatureAvailable); + } + }); + } catch (Exception e) { + e.printStackTrace(); + } + } + + @Override + public void setScreenLoadingEnabled(@NonNull Boolean isEnabled) { + try { + APM.setScreenLoadingEnabled(isEnabled); + } catch (Exception e) { + e.printStackTrace(); + } + } } diff --git a/android/src/main/java/com/instabug/flutter/modules/InstabugApi.java b/android/src/main/java/com/instabug/flutter/modules/InstabugApi.java index 5cfc178e8..d3b34c5f1 100644 --- a/android/src/main/java/com/instabug/flutter/modules/InstabugApi.java +++ b/android/src/main/java/com/instabug/flutter/modules/InstabugApi.java @@ -11,10 +11,15 @@ import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; + import com.instabug.flutter.util.ArgsRegistry; import com.instabug.flutter.generated.InstabugPigeon; import com.instabug.flutter.util.Reflection; import com.instabug.flutter.util.ThreadManager; +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.Feature; import com.instabug.library.Instabug; import com.instabug.library.InstabugColorTheme; @@ -22,41 +27,49 @@ import com.instabug.library.IssueType; import com.instabug.library.Platform; import com.instabug.library.ReproConfigurations; +import com.instabug.library.featuresflags.model.IBGFeatureFlag; import com.instabug.library.internal.module.InstabugLocale; import com.instabug.library.invocation.InstabugInvocationEvent; import com.instabug.library.model.NetworkLog; import com.instabug.library.ui.onboarding.WelcomeMessage; +import io.flutter.FlutterInjector; +import io.flutter.embedding.engine.loader.FlutterLoader; +import io.flutter.plugin.common.BinaryMessenger; + +import org.jetbrains.annotations.NotNull; import org.json.JSONObject; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Method; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.concurrent.Callable; -import io.flutter.FlutterInjector; -import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.embedding.engine.loader.FlutterLoader; - public class InstabugApi implements InstabugPigeon.InstabugHostApi { private final String TAG = InstabugApi.class.getName(); private final Context context; private final Callable screenshotProvider; private final InstabugCustomTextPlaceHolder placeHolder = new InstabugCustomTextPlaceHolder(); + private final InstabugPigeon.FeatureFlagsFlutterApi featureFlagsFlutterApi; + public static void init(BinaryMessenger messenger, Context context, Callable screenshotProvider) { - final InstabugApi api = new InstabugApi(context, screenshotProvider); + final InstabugPigeon.FeatureFlagsFlutterApi flutterApi = new InstabugPigeon.FeatureFlagsFlutterApi(messenger); + + final InstabugApi api = new InstabugApi(context, screenshotProvider, flutterApi); InstabugPigeon.InstabugHostApi.setup(messenger, api); } - public InstabugApi(Context context, Callable screenshotProvider) { + public InstabugApi(Context context, Callable screenshotProvider, InstabugPigeon.FeatureFlagsFlutterApi featureFlagsFlutterApi) { this.context = context; this.screenshotProvider = screenshotProvider; + this.featureFlagsFlutterApi = featureFlagsFlutterApi; } @VisibleForTesting @@ -85,6 +98,16 @@ public void setEnabled(@NonNull Boolean isEnabled) { } } + @NotNull + @Override + public Boolean isEnabled() { + return Instabug.isEnabled(); + } + + @NotNull + @Override + public Boolean isBuilt() { return Instabug.isBuilt(); } + @Override public void init(@NonNull String token, @NonNull List invocationEvents, @NonNull String debugLogsLevel) { setCurrentPlatform(); @@ -224,6 +247,37 @@ public void clearAllExperiments() { Instabug.clearAllExperiments(); } + @Override + public void addFeatureFlags(@NonNull Map featureFlags) { + try { + List features = new ArrayList<>(); + for (Map.Entry entry : featureFlags.entrySet()) { + features.add(new IBGFeatureFlag(entry.getKey(), entry.getValue().isEmpty() ? null : entry.getValue())); + } + Instabug.addFeatureFlags(features); + } catch (Exception e) { + e.printStackTrace(); + } + } + + @Override + public void removeFeatureFlags(@NonNull List featureFlags) { + try { + Instabug.removeFeatureFlag(featureFlags); + } catch (Exception e) { + e.printStackTrace(); + } + } + + @Override + public void removeAllFeatureFlags() { + try { + Instabug.removeAllFeatureFlags(); + } catch (Exception e) { + e.printStackTrace(); + } + } + @Override public void setUserAttribute(@NonNull String value, @NonNull String key) { Instabug.setUserAttribute(key, value); @@ -397,6 +451,48 @@ public void networkLog(@NonNull Map data) { } } + @Override + public void registerFeatureFlagChangeListener() { + + try { + InternalCore.INSTANCE._setFeaturesStateListener(new FeaturesStateListener() { + @Override + public void invoke(@NonNull CoreFeaturesState featuresState) { + ThreadManager.runOnMainThread(new Runnable() { + @Override + public void run() { + featureFlagsFlutterApi.onW3CFeatureFlagChange(featuresState.isW3CExternalTraceIdEnabled(), + featuresState.isAttachingGeneratedHeaderEnabled(), + featuresState.isAttachingCapturedHeaderEnabled(), + new InstabugPigeon.FeatureFlagsFlutterApi.Reply() { + @Override + public void reply(Void reply) { + + } + }); + } + }); + } + + }); + } catch (Exception e) { + e.printStackTrace(); + } + + } + + @NonNull + @Override + public Map isW3CFeatureFlagsEnabled() { + Map params = new HashMap(); + params.put("isW3cExternalTraceIDEnabled", InternalCore.INSTANCE._isFeatureEnabled(CoreFeature.W3C_EXTERNAL_TRACE_ID)); + params.put("isW3cExternalGeneratedHeaderEnabled", InternalCore.INSTANCE._isFeatureEnabled(CoreFeature.W3C_ATTACHING_GENERATED_HEADER)); + params.put("isW3cCaughtHeaderEnabled", InternalCore.INSTANCE._isFeatureEnabled(CoreFeature.W3C_ATTACHING_CAPTURED_HEADER)); + + + return params; + } + @Override public void willRedirectToStore() { Instabug.willRedirectToStore(); diff --git a/android/src/main/java/com/instabug/flutter/modules/SessionReplayApi.java b/android/src/main/java/com/instabug/flutter/modules/SessionReplayApi.java index 2170f2c98..29ff95596 100644 --- a/android/src/main/java/com/instabug/flutter/modules/SessionReplayApi.java +++ b/android/src/main/java/com/instabug/flutter/modules/SessionReplayApi.java @@ -1,12 +1,8 @@ package com.instabug.flutter.modules; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - import com.instabug.flutter.generated.SessionReplayPigeon; -import com.instabug.library.OnSessionReplayLinkReady; import com.instabug.library.sessionreplay.SessionReplay; - import io.flutter.plugin.common.BinaryMessenger; public class SessionReplayApi implements SessionReplayPigeon.SessionReplayHostApi { diff --git a/android/src/test/java/com/instabug/flutter/ApmApiTest.java b/android/src/test/java/com/instabug/flutter/ApmApiTest.java index aefb3b62d..935521466 100644 --- a/android/src/test/java/com/instabug/flutter/ApmApiTest.java +++ b/android/src/test/java/com/instabug/flutter/ApmApiTest.java @@ -1,18 +1,9 @@ package com.instabug.flutter; -import static com.instabug.flutter.util.GlobalMocks.reflected; -import static com.instabug.flutter.util.MockResult.makeResult; -import static org.junit.Assert.assertEquals; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.mockConstruction; -import static org.mockito.Mockito.mockStatic; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - import com.instabug.apm.APM; +import com.instabug.apm.InternalAPM; +import com.instabug.apm.configuration.cp.APMFeature; +import com.instabug.apm.configuration.cp.FeatureAvailabilityCallback; import com.instabug.apm.model.ExecutionTrace; import com.instabug.apm.networking.APMNetworkLogger; import com.instabug.flutter.generated.ApmPigeon; @@ -20,6 +11,8 @@ import com.instabug.flutter.util.GlobalMocks; import com.instabug.flutter.util.MockReflected; +import io.flutter.plugin.common.BinaryMessenger; + import org.json.JSONObject; import org.junit.After; import org.junit.Assert; @@ -31,23 +24,32 @@ import java.util.HashMap; import java.util.Map; -import io.flutter.plugin.common.BinaryMessenger; +import static com.instabug.flutter.util.GlobalMocks.reflected; +import static com.instabug.flutter.util.MockResult.makeResult; +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; public class ApmApiTest { + + private final BinaryMessenger mMessenger = mock(BinaryMessenger.class); private final ApmApi api = new ApmApi(); private MockedStatic mAPM; + private MockedStatic mInternalApmStatic; private MockedStatic mHostApi; @Before public void setUp() throws NoSuchMethodException { mAPM = mockStatic(APM.class); + mInternalApmStatic = mockStatic(InternalAPM.class); mHostApi = mockStatic(ApmPigeon.ApmHostApi.class); GlobalMocks.setUp(); } @After public void cleanUp() { + mInternalApmStatic.close(); mAPM.close(); mHostApi.close(); GlobalMocks.close(); @@ -260,10 +262,115 @@ public void testNetworkLogAndroid() { responseContentType, errorDomain, null, - serverErrorMessage + serverErrorMessage, + null )); mAPMNetworkLogger.close(); mJSONObject.close(); } + + @Test + public void testStartUiTraceCP() { + String screenName = "screen-name"; + long microTimeStamp = System.currentTimeMillis() / 1000; + long traceId = System.currentTimeMillis(); + + + api.startCpUiTrace(screenName, microTimeStamp, traceId); + + mInternalApmStatic.verify(() -> InternalAPM._startUiTraceCP(screenName, microTimeStamp, traceId)); + mInternalApmStatic.verifyNoMoreInteractions(); + } + + @Test + public void testReportScreenLoadingCP() { + long startTimeStampMicro = System.currentTimeMillis() / 1000; + long durationMicro = System.currentTimeMillis() / 1000; + long uiTraceId = System.currentTimeMillis(); + + api.reportScreenLoadingCP(startTimeStampMicro, durationMicro, uiTraceId); + + mInternalApmStatic.verify(() -> InternalAPM._reportScreenLoadingCP(startTimeStampMicro, durationMicro, uiTraceId)); + mInternalApmStatic.verifyNoMoreInteractions(); + } + + @Test + public void testEndScreenLoading() { + long timeStampMicro = System.currentTimeMillis() / 1000; + long uiTraceId = System.currentTimeMillis(); + + api.endScreenLoadingCP(timeStampMicro, uiTraceId); + + mInternalApmStatic.verify(() -> InternalAPM._endScreenLoadingCP(timeStampMicro, uiTraceId)); + mInternalApmStatic.verifyNoMoreInteractions(); + } + + @Test + public void testIsEnabled() { + boolean expected = true; + ApmPigeon.Result result = spy(makeResult((actual) -> assertEquals(expected, actual))); + mInternalApmStatic.when(() -> InternalAPM._isFeatureEnabledCP(eq(APMFeature.SCREEN_LOADING), any(), any(FeatureAvailabilityCallback.class))).thenAnswer(invocation -> { + FeatureAvailabilityCallback callback = invocation.getArgument(1); + callback.invoke(expected); + return null; + }); + + api.isEnabled(result); + + verify(result).success(expected); + } + + @Test + public void testIsScreenLoadingEnabled() { + boolean expected = true; + ApmPigeon.Result result = spy(makeResult((actual) -> assertEquals(expected, actual))); + + mInternalApmStatic.when(() -> InternalAPM._isFeatureEnabledCP(any(), any(), any())).thenAnswer( + invocation -> { + FeatureAvailabilityCallback callback = (FeatureAvailabilityCallback) invocation.getArguments()[2]; + callback.invoke(expected); + return null; + }); + + + api.isScreenLoadingEnabled(result); + + mInternalApmStatic.verify(() -> InternalAPM._isFeatureEnabledCP(any(), any(), any())); + mInternalApmStatic.verifyNoMoreInteractions(); + + verify(result).success(expected); + } + + @Test + public void testIsEndScreenLoadingEnabled() { + boolean expected = true; + ApmPigeon.Result result = spy(makeResult((actual) -> assertEquals(expected, actual))); + + mInternalApmStatic.when(() -> InternalAPM._isFeatureEnabledCP(any(), any(), any())).thenAnswer( + invocation -> { + FeatureAvailabilityCallback callback = (FeatureAvailabilityCallback) invocation.getArguments()[2]; + callback.invoke(expected); + return null; + }); + + + api.isEndScreenLoadingEnabled(result); + + mInternalApmStatic.verify(() -> InternalAPM._isFeatureEnabledCP(any(), any(), any())); + mInternalApmStatic.verifyNoMoreInteractions(); + + verify(result).success(expected); + + } + + + @Test + public void testSetScreenLoadingMonitoringEnabled() { + boolean isEnabled = false; + + api.setScreenLoadingEnabled(isEnabled); + + mAPM.verify(() -> APM.setScreenLoadingEnabled(isEnabled)); + } } diff --git a/android/src/test/java/com/instabug/flutter/InstabugApiTest.java b/android/src/test/java/com/instabug/flutter/InstabugApiTest.java index b542259b6..f88e58446 100644 --- a/android/src/test/java/com/instabug/flutter/InstabugApiTest.java +++ b/android/src/test/java/com/instabug/flutter/InstabugApiTest.java @@ -17,10 +17,16 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static io.mockk.MockKKt.every; +import static io.mockk.MockKKt.mockkObject; + +import io.mockk.*; + import android.app.Application; import android.graphics.Bitmap; import android.net.Uri; +import com.instabug.apm.InternalAPM; import com.instabug.bug.BugReporting; import com.instabug.flutter.generated.InstabugPigeon; import com.instabug.flutter.modules.InstabugApi; @@ -35,9 +41,15 @@ import com.instabug.library.Platform; import com.instabug.library.ReproConfigurations; import com.instabug.library.ReproMode; +import com.instabug.library.featuresflags.model.IBGFeatureFlag; +import com.instabug.library.internal.crossplatform.CoreFeature; +import com.instabug.library.internal.crossplatform.FeaturesStateListener; +import com.instabug.library.internal.crossplatform.InternalCore; import com.instabug.library.invocation.InstabugInvocationEvent; import com.instabug.library.model.NetworkLog; import com.instabug.library.ui.onboarding.WelcomeMessage; +import com.instabug.survey.Surveys; +import com.instabug.survey.callbacks.OnShowCallback; import org.json.JSONObject; import org.junit.After; @@ -48,15 +60,23 @@ import java.io.File; import java.io.IOException; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Random; import java.util.concurrent.Callable; import io.flutter.plugin.common.BinaryMessenger; +import kotlin.jvm.functions.Function1; + +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.mockito.verification.VerificationMode; public class InstabugApiTest { private final Callable screenshotProvider = () -> mock(Bitmap.class); @@ -66,11 +86,15 @@ public class InstabugApiTest { private MockedStatic mBugReporting; private MockedConstruction mCustomTextPlaceHolder; private MockedStatic mHostApi; - + private InternalCore internalCore; @Before public void setUp() throws NoSuchMethodException { mCustomTextPlaceHolder = mockConstruction(InstabugCustomTextPlaceHolder.class); - api = spy(new InstabugApi(mContext, screenshotProvider)); + internalCore=spy(InternalCore.INSTANCE); + + BinaryMessenger mMessenger = mock(BinaryMessenger.class); + final InstabugPigeon.FeatureFlagsFlutterApi flutterApi = new InstabugPigeon.FeatureFlagsFlutterApi(mMessenger); + api = spy(new InstabugApi(mContext, screenshotProvider, flutterApi)); mInstabug = mockStatic(Instabug.class); mBugReporting = mockStatic(BugReporting.class); mHostApi = mockStatic(InstabugPigeon.InstabugHostApi.class); @@ -84,6 +108,7 @@ public void cleanUp() { mBugReporting.close(); mHostApi.close(); GlobalMocks.close(); + } @Test @@ -155,6 +180,20 @@ public void testSetEnabledGivenFalse() { mInstabug.verify(Instabug::disable); } + @Test + public void testIsEnabled() { + api.isEnabled(); + + mInstabug.verify(Instabug::isEnabled); + } + + @Test + public void testIsBuilt() { + api.isBuilt(); + + mInstabug.verify(Instabug::isBuilt); + } + @Test public void testShow() { api.show(); @@ -330,6 +369,32 @@ public void testClearAllExperiments() { mInstabug.verify(Instabug::clearAllExperiments); } + @Test + public void testAddFeatureFlags() { + Map featureFlags = new HashMap<>(); + featureFlags.put("key1", "variant1"); + api.addFeatureFlags(featureFlags); + List flags = new ArrayList(); + flags.add(new IBGFeatureFlag("key1", "variant1")); + mInstabug.verify(() -> Instabug.addFeatureFlags(flags)); + } + + @Test + public void testRemoveFeatureFlags() { + List featureFlags = Arrays.asList("premium", "star"); + + api.removeFeatureFlags(featureFlags); + + mInstabug.verify(() -> Instabug.removeFeatureFlag(featureFlags)); + } + + @Test + public void testClearAllFeatureFlags() { + api.removeAllFeatureFlags(); + + mInstabug.verify(Instabug::removeAllFeatureFlags); + } + @Test public void testSetUserAttribute() { String key = "is_premium"; @@ -555,4 +620,25 @@ public void testWillRedirectToStore() { api.willRedirectToStore(); mInstabug.verify(Instabug::willRedirectToStore); } + + + @Test + public void isW3CFeatureFlagsEnabled() { + mockkObject(new InternalCore[]{InternalCore.INSTANCE},false); + Random random=new Random(); + Boolean isW3cExternalGeneratedHeaderEnabled = random.nextBoolean(); + Boolean isW3cExternalTraceIDEnabled = random.nextBoolean(); + Boolean isW3cCaughtHeaderEnabled = random.nextBoolean(); + + every((Function1) mockKMatcherScope -> InternalCore.INSTANCE._isFeatureEnabled(CoreFeature.W3C_ATTACHING_GENERATED_HEADER)).returns(isW3cExternalGeneratedHeaderEnabled); + every((Function1) mockKMatcherScope -> InternalCore.INSTANCE._isFeatureEnabled(CoreFeature.W3C_EXTERNAL_TRACE_ID)).returns(isW3cExternalTraceIDEnabled); + every((Function1) mockKMatcherScope -> InternalCore.INSTANCE._isFeatureEnabled(CoreFeature.W3C_ATTACHING_CAPTURED_HEADER)).returns(isW3cCaughtHeaderEnabled); + + + Map flags = api.isW3CFeatureFlagsEnabled(); + assertEquals(isW3cExternalGeneratedHeaderEnabled, flags.get("isW3cExternalGeneratedHeaderEnabled")); + assertEquals(isW3cExternalTraceIDEnabled, flags.get("isW3cExternalTraceIDEnabled")); + assertEquals(isW3cCaughtHeaderEnabled, flags.get("isW3cCaughtHeaderEnabled")); + + } } diff --git a/android/src/test/java/com/instabug/flutter/SessionReplayApiTest.java b/android/src/test/java/com/instabug/flutter/SessionReplayApiTest.java index 223871844..20ad7ff3b 100644 --- a/android/src/test/java/com/instabug/flutter/SessionReplayApiTest.java +++ b/android/src/test/java/com/instabug/flutter/SessionReplayApiTest.java @@ -1,24 +1,22 @@ package com.instabug.flutter; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.mockStatic; -import static org.mockito.Mockito.timeout; -import static org.mockito.Mockito.verify; - import com.instabug.flutter.generated.SessionReplayPigeon; import com.instabug.flutter.modules.SessionReplayApi; import com.instabug.flutter.util.GlobalMocks; import com.instabug.library.OnSessionReplayLinkReady; import com.instabug.library.sessionreplay.SessionReplay; - +import io.flutter.plugin.common.BinaryMessenger; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.mockito.MockedStatic; -import io.flutter.plugin.common.BinaryMessenger; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; public class SessionReplayApiTest { diff --git a/android/src/test/java/com/instabug/flutter/util/GlobalMocks.java b/android/src/test/java/com/instabug/flutter/util/GlobalMocks.java index c80436d36..0795acf77 100644 --- a/android/src/test/java/com/instabug/flutter/util/GlobalMocks.java +++ b/android/src/test/java/com/instabug/flutter/util/GlobalMocks.java @@ -8,6 +8,7 @@ import android.net.Uri; import android.util.Log; +import com.instabug.apm.networkinterception.cp.APMCPNetworkLog; import com.instabug.crash.models.IBGNonFatalException; import org.json.JSONObject; @@ -18,6 +19,10 @@ import java.lang.reflect.Method; import java.util.Map; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; + public class GlobalMocks { public static MockedStatic threadManager; public static MockedStatic log; @@ -65,10 +70,10 @@ public static void setUp() throws NoSuchMethodException { .when(() -> Reflection.getMethod(Class.forName("com.instabug.library.Instabug"), "setCurrentPlatform", int.class)) .thenReturn(mSetCurrentPlatform); - Method mAPMNetworkLog = MockReflected.class.getDeclaredMethod("apmNetworkLog", 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); + Method mAPMNetworkLog = MockReflected.class.getDeclaredMethod("apmNetworkLog", 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); mAPMNetworkLog.setAccessible(true); reflection - .when(() -> Reflection.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)) + .when(() -> Reflection.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)) .thenReturn(mAPMNetworkLog); Method mCrashReportException = MockReflected.class.getDeclaredMethod("crashReportException", JSONObject.class, boolean.class); @@ -89,6 +94,18 @@ public static void setUp() throws NoSuchMethodException { uri = mockStatic(Uri.class); uri.when(() -> Uri.fromFile(any())).thenReturn(mock(Uri.class)); + + Method mStartUiTraceCP = MockReflected.class.getDeclaredMethod("startUiTraceCP", String.class, Long.class, Long.class); + mStartUiTraceCP.setAccessible(true); + reflection.when(() -> Reflection.getMethod(Class.forName("com.instabug.apm.APM"), "startUiTraceCP", String.class, Long.class, Long.class)).thenReturn(mStartUiTraceCP); + + Method mReportScreenLoadingCP = MockReflected.class.getDeclaredMethod("reportScreenLoadingCP", Long.class, Long.class, Long.class); + mReportScreenLoadingCP.setAccessible(true); + reflection.when(() -> Reflection.getMethod(Class.forName("com.instabug.apm.APM"), "reportScreenLoadingCP", Long.class, Long.class, Long.class)).thenReturn(mReportScreenLoadingCP); + + Method mEndScreenLoadingCP = MockReflected.class.getDeclaredMethod("endScreenLoadingCP", Long.class, Long.class); + mEndScreenLoadingCP.setAccessible(true); + reflection.when(() -> Reflection.getMethod(Class.forName("com.instabug.apm.APM"), "endScreenLoadingCP", Long.class, Long.class)).thenReturn(mEndScreenLoadingCP); } public static void close() { diff --git a/android/src/test/java/com/instabug/flutter/util/MockReflected.java b/android/src/test/java/com/instabug/flutter/util/MockReflected.java index 040b0853d..42c85664b 100644 --- a/android/src/test/java/com/instabug/flutter/util/MockReflected.java +++ b/android/src/test/java/com/instabug/flutter/util/MockReflected.java @@ -1,9 +1,9 @@ package com.instabug.flutter.util; import android.graphics.Bitmap; - import androidx.annotation.Nullable; +import com.instabug.apm.networkinterception.cp.APMCPNetworkLog; import com.instabug.crash.models.IBGNonFatalException; import org.json.JSONObject; @@ -34,7 +34,7 @@ public static void setCurrentPlatform(int platform) {} /** * APMNetworkLogger.log */ - public static void apmNetworkLog(long requestStartTime, long requestDuration, String requestHeaders, String requestBody, long requestBodySize, String requestMethod, String requestUrl, String responseHeaders, String responseBody, String responseBodySize, long statusCode, int responseContentType, String errorMessage, String var18, @Nullable String gqlQueryName, @Nullable String serverErrorMessage) {} + public static void apmNetworkLog(long requestStartTime, long requestDuration, String requestHeaders, String requestBody, long requestBodySize, String requestMethod, String requestUrl, String responseHeaders, String responseBody, String responseBodySize, long statusCode, int responseContentType, String errorMessage, String var18, @Nullable String gqlQueryName, @Nullable String serverErrorMessage, @Nullable APMCPNetworkLog.W3CExternalTraceAttributes w3CExternalTraceAttributes) {} /** * CrashReporting.reportException @@ -42,4 +42,10 @@ public static void apmNetworkLog(long requestStartTime, long requestDuration, St public static void crashReportException(JSONObject exception, boolean isHandled) {} public static void crashReportException(JSONObject exception, boolean isHandled, Map userAttributes, JSONObject fingerPrint, IBGNonFatalException.Level level) {} + + public static void startUiTraceCP(String screenName, Long microTimeStamp, Long traceId) {} + + public static void reportScreenLoadingCP(Long startTimeStampMicro, Long durationMicro, Long uiTraceId) {} + + public static void endScreenLoadingCP(Long timeStampMicro, Long uiTraceId) {} } diff --git a/e2e/BugReportingTests.cs b/e2e/BugReportingTests.cs index f73608e2e..f938cfcbd 100644 --- a/e2e/BugReportingTests.cs +++ b/e2e/BugReportingTests.cs @@ -133,7 +133,7 @@ public void MultipleScreenshotsInReproSteps() Assert.Equal(2, reproSteps.Count); } - [Fact] + [Fact(Skip = "The test is flaky on iOS so we're skipping it to unblock the v13.2.0 release")] public void ChangeReportTypes() { ScrollUp(); diff --git a/e2e/SurveysTests.cs b/e2e/SurveysTests.cs index 2875cf44d..1ed0eba48 100644 --- a/e2e/SurveysTests.cs +++ b/e2e/SurveysTests.cs @@ -10,7 +10,7 @@ public class SurveysTests : CaptainTest [Fact] public void ShowManualSurvey() { - ScrollDown(); + ScrollDownLittle(); captain.FindByText("Show Manual Survey").Tap(); captain.WaitForAssertion(() => diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml new file mode 100644 index 000000000..0d2902135 --- /dev/null +++ b/example/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 04f99d12a..13ed775e2 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -70,10 +70,6 @@ flutter { } dependencies { - androidTestImplementation 'androidx.test.ext:junit:1.1.1' - androidTestImplementation 'androidx.test:rules:1.1.0' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0' - androidTestImplementation 'com.android.support.test.uiautomator:uiautomator:2.1.3' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'com.android.support:multidex:1.0.3' implementation 'org.mockito:mockito-core:1.10.19' diff --git a/example/android/app/src/androidTest/java/com/instabug/flutter/example/BugReportingUITest.java b/example/android/app/src/androidTest/java/com/instabug/flutter/example/BugReportingUITest.java deleted file mode 100644 index aed7d7014..000000000 --- a/example/android/app/src/androidTest/java/com/instabug/flutter/example/BugReportingUITest.java +++ /dev/null @@ -1,176 +0,0 @@ -package com.instabug.flutter.example; - -import androidx.test.espresso.action.ViewActions; -import androidx.test.espresso.flutter.action.FlutterActions; -import androidx.test.espresso.flutter.assertion.FlutterAssertions; -import androidx.test.espresso.flutter.matcher.FlutterMatchers; -import androidx.test.espresso.matcher.ViewMatchers; -import androidx.test.rule.ActivityTestRule; -import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.uiautomator.UiDevice; -import androidx.test.uiautomator.UiObject; -import androidx.test.uiautomator.UiObjectNotFoundException; -import androidx.test.uiautomator.UiSelector; - -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; - -import static androidx.test.espresso.Espresso.onView; -import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist; -import static androidx.test.espresso.assertion.ViewAssertions.matches; -import static androidx.test.espresso.flutter.EspressoFlutter.onFlutterWidget; -import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; - -import static com.instabug.flutter.example.util.InstabugAssertions.assertViewWillBeVisible; -import static com.instabug.flutter.example.util.InstabugViewMatchers.isToTheLeft; - -import static org.junit.Assert.assertTrue; - -import android.app.Instrumentation; -import android.graphics.Point; - -import com.instabug.flutter.example.util.Keyboard; - -@RunWith(AndroidJUnit4.class) -public class BugReportingUITest { - Instrumentation instrumentation = getInstrumentation(); - UiDevice device = UiDevice.getInstance(instrumentation); - - @Rule - public ActivityTestRule mActivityRule = - new ActivityTestRule<>(MainActivity.class); - - @Before - public void setUp() { - onFlutterWidget(FlutterMatchers.withText("Restart Instabug")) - .perform(FlutterActions.click()); - } - - private void assertOptionsPromptIsVisible() { - assertViewWillBeVisible("instabug_main_prompt_container", 2000); - } - - @Test - public void floatingButtonInvocationEvent() { - onFlutterWidget(FlutterMatchers.withText("Floating Button")).perform(FlutterActions.click()); - onView(ViewMatchers.withResourceName("instabug_floating_button")).perform(ViewActions.click()); - - assertOptionsPromptIsVisible(); - } - - @Test - public void twoFingersSwipeLeftInvocationEvent() throws InterruptedException { - onFlutterWidget(FlutterMatchers.withText("Two Fingers Swipe Left")) - .perform(FlutterActions.click()); - - // Two-fingers swipe left - UiObject text = device.findObject(new UiSelector().textContains("Hello")); - int width = device.getDisplayWidth(); - text.performTwoPointerGesture( - new Point(width - 50, 100), - new Point(width - 50, 130), - new Point(50, 100), - new Point(50, 130), - // Small steps number for fast swiping - 20 - ); - - Thread.sleep(1000); - - assertOptionsPromptIsVisible(); - } - - @Test - public void noneInvocationEvent() { - onFlutterWidget(FlutterMatchers.withText("None")).perform(FlutterActions.click()); - - onView(ViewMatchers.withResourceName("instabug_floating_button")).check(doesNotExist()); - } - - @Test - public void manualInvocation() { - onFlutterWidget(FlutterMatchers.withText("Invoke")).perform(FlutterActions.click()); - - assertOptionsPromptIsVisible(); - } - - @Test - public void multipleScreenshotsInReproSteps() throws InterruptedException, UiObjectNotFoundException { - String screen = "My Screen"; - - onFlutterWidget(FlutterMatchers.withText("Enter screen name")) - .perform(FlutterActions.scrollTo(), FlutterActions.typeText(screen)); - Thread.sleep(1000); - Keyboard.closeKeyboard(); - onFlutterWidget(FlutterMatchers.withText("Report Screen Change")) - .perform(FlutterActions.scrollTo(), FlutterActions.click(), FlutterActions.click()); - onFlutterWidget(FlutterMatchers.withText("Send Bug Report")) - .perform(FlutterActions.scrollTo(), FlutterActions.click()); - UiObject reproStepsDisclaimer = assertViewWillBeVisible("instabug_text_view_repro_steps_disclaimer", 2000); - reproStepsDisclaimer.click(); - - String screenshotsListId = "instabug_vus_list"; - assertViewWillBeVisible(screenshotsListId, 5000); - onView(ViewMatchers.withResourceName(screenshotsListId)) - .check(matches(ViewMatchers.hasChildCount(2))); - } - - @Test - public void onDismissCallbackIsCalled() throws InterruptedException { - onFlutterWidget(FlutterMatchers.withText("Set On Dismiss Callback")) - .perform(FlutterActions.scrollTo(), FlutterActions.click()); - - onFlutterWidget(FlutterMatchers.withText("Invoke")) - .perform(FlutterActions.scrollTo(), FlutterActions.click()); - Thread.sleep(1000); - device.pressBack(); - - Thread.sleep(1000); - - onFlutterWidget(FlutterMatchers.withText("onDismiss callback called with DismissType.cancel and ReportType.other")) - .check(FlutterAssertions.matches(FlutterMatchers.isExisting())); - } - - @Test - public void changeReportTypes() throws UiObjectNotFoundException { - onFlutterWidget(FlutterMatchers.withText("Bug")) - .perform(FlutterActions.scrollTo(), FlutterActions.click()); - onFlutterWidget(FlutterMatchers.withText("Invoke")) - .perform(FlutterActions.scrollTo(), FlutterActions.click()); - - // Shows bug reporting screen - assertViewWillBeVisible("instabug_edit_text_message", 2000); - - // Close bug reporting screen - device.pressBack(); - device.findObject(new UiSelector().text("DISCARD")).click(); - - // Enable feedback reports - onFlutterWidget(FlutterMatchers.withText("Feedback")) - .perform(FlutterActions.scrollTo(), FlutterActions.click()); - onFlutterWidget(FlutterMatchers.withText("Invoke")) - .perform(FlutterActions.scrollTo(), FlutterActions.click()); - - // Shows both bug reporting and feature requests in prompt options - assertOptionsPromptIsVisible(); - onView(ViewMatchers.withText("Report a bug")) - .check(matches(ViewMatchers.isDisplayed())); - onView(ViewMatchers.withText("Suggest an improvement")) - .check(matches(ViewMatchers.isDisplayed())); - onView(ViewMatchers.withText("Ask a question")) - .check(doesNotExist()); - } - - @Test - public void changeFloatingButtonEdge() { - onFlutterWidget(FlutterMatchers.withText("Floating Button")) - .perform(FlutterActions.scrollTo(), FlutterActions.click()); - onFlutterWidget(FlutterMatchers.withText("Move Floating Button to Left")) - .perform(FlutterActions.scrollTo(), FlutterActions.click()); - onView(ViewMatchers.withResourceName("instabug_floating_button")) - .check(matches(isToTheLeft())); - } -} - diff --git a/example/android/app/src/androidTest/java/com/instabug/flutter/example/FeatureRequestsUITest.java b/example/android/app/src/androidTest/java/com/instabug/flutter/example/FeatureRequestsUITest.java deleted file mode 100644 index 1c1eb25bc..000000000 --- a/example/android/app/src/androidTest/java/com/instabug/flutter/example/FeatureRequestsUITest.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.instabug.flutter.example; - -import static androidx.test.espresso.Espresso.onView; -import static androidx.test.espresso.assertion.ViewAssertions.matches; -import static androidx.test.espresso.flutter.EspressoFlutter.onFlutterWidget; - -import androidx.test.espresso.flutter.action.FlutterActions; -import androidx.test.espresso.flutter.matcher.FlutterMatchers; -import androidx.test.espresso.matcher.ViewMatchers; -import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.rule.ActivityTestRule; - -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; - - -@RunWith(AndroidJUnit4.class) -public class FeatureRequestsUITest { - @Rule - public ActivityTestRule mActivityRule = - new ActivityTestRule<>(MainActivity.class); - - @Test - public void showFeatureRequestsScreen() { - onFlutterWidget(FlutterMatchers.withText("Show Feature Requests")) - .perform(FlutterActions.scrollTo(), FlutterActions.click()); - - onView(ViewMatchers.withText("Feature Requests")) - .check(matches(ViewMatchers.isDisplayed())); - } -} diff --git a/example/android/app/src/androidTest/java/com/instabug/flutter/example/InstabugUITest.java b/example/android/app/src/androidTest/java/com/instabug/flutter/example/InstabugUITest.java deleted file mode 100644 index a2d0d4a4f..000000000 --- a/example/android/app/src/androidTest/java/com/instabug/flutter/example/InstabugUITest.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.instabug.flutter.example; - -import static androidx.test.espresso.Espresso.onView; -import static androidx.test.espresso.assertion.ViewAssertions.matches; -import static androidx.test.espresso.flutter.EspressoFlutter.onFlutterWidget; - -import static com.instabug.flutter.example.util.InstabugViewMatchers.hasBackgroundColor; - -import android.graphics.Color; - -import androidx.test.espresso.flutter.action.FlutterActions; -import androidx.test.espresso.flutter.matcher.FlutterMatchers; -import androidx.test.espresso.matcher.ViewMatchers; -import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.rule.ActivityTestRule; - -import com.instabug.flutter.example.util.Keyboard; - -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; - - -@RunWith(AndroidJUnit4.class) -public class InstabugUITest { - @Rule - public ActivityTestRule mActivityRule = - new ActivityTestRule<>(MainActivity.class); - - @Test - public void changePrimaryColor() { - String color = "#FF0000"; - Color expected = Color.valueOf(0xFFFF0000); - - onFlutterWidget(FlutterMatchers.withText("Enter primary color")) - .perform(FlutterActions.typeText(color)); - Keyboard.closeKeyboard(); - onFlutterWidget(FlutterMatchers.withText("Change Primary Color")) - .perform(FlutterActions.scrollTo(), FlutterActions.click()); - onFlutterWidget(FlutterMatchers.withText("Floating Button")) - .perform(FlutterActions.scrollTo(), FlutterActions.click()); - - onView(ViewMatchers.withResourceName("instabug_floating_button")) - .check(matches(hasBackgroundColor(expected))); - } -} diff --git a/example/android/app/src/androidTest/java/com/instabug/flutter/example/SurveysUITest.java b/example/android/app/src/androidTest/java/com/instabug/flutter/example/SurveysUITest.java deleted file mode 100644 index b5f47386c..000000000 --- a/example/android/app/src/androidTest/java/com/instabug/flutter/example/SurveysUITest.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.instabug.flutter.example; - -import static androidx.test.espresso.flutter.EspressoFlutter.onFlutterWidget; - -import static com.instabug.flutter.example.util.InstabugAssertions.assertViewWillBeVisible; - -import androidx.test.espresso.flutter.action.FlutterActions; -import androidx.test.espresso.flutter.matcher.FlutterMatchers; -import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.rule.ActivityTestRule; - -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; - - -@RunWith(AndroidJUnit4.class) -public class SurveysUITest { - @Rule - public ActivityTestRule mActivityRule = - new ActivityTestRule<>(MainActivity.class); - - @Test - public void showManualSurvey() { - onFlutterWidget(FlutterMatchers.withText("Show Manual Survey")) - .perform(FlutterActions.scrollTo(), FlutterActions.click()); - - assertViewWillBeVisible("instabug_survey_dialog_container", 2000); - } -} diff --git a/example/android/app/src/androidTest/java/com/instabug/flutter/example/util/HasBackgroundColorMatcher.java b/example/android/app/src/androidTest/java/com/instabug/flutter/example/util/HasBackgroundColorMatcher.java deleted file mode 100644 index bcf419670..000000000 --- a/example/android/app/src/androidTest/java/com/instabug/flutter/example/util/HasBackgroundColorMatcher.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.instabug.flutter.example.util; - -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Color; -import android.view.View; - -import org.hamcrest.Description; -import org.hamcrest.TypeSafeMatcher; - -public final class HasBackgroundColorMatcher extends TypeSafeMatcher { - - private final Color color; - - public HasBackgroundColorMatcher(Color color) { - this.color = color; - } - - @Override - protected boolean matchesSafely(View view) { - int width = view.getWidth(); - int height = view.getHeight(); - Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(bitmap); - view.getBackground().draw(canvas); - return bitmap.getColor(width / 2, height / 2).equals(color); - } - - @Override - public void describeTo(Description description) { - description.appendText("has background with color: " + color); - } -} \ No newline at end of file diff --git a/example/android/app/src/androidTest/java/com/instabug/flutter/example/util/InstabugAssertions.java b/example/android/app/src/androidTest/java/com/instabug/flutter/example/util/InstabugAssertions.java deleted file mode 100644 index 31c62c344..000000000 --- a/example/android/app/src/androidTest/java/com/instabug/flutter/example/util/InstabugAssertions.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.instabug.flutter.example.util; - -import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; -import static org.junit.Assert.assertTrue; - -import androidx.test.uiautomator.UiDevice; -import androidx.test.uiautomator.UiObject; -import androidx.test.uiautomator.UiSelector; - -public class InstabugAssertions { - private static final UiDevice device = UiDevice.getInstance(getInstrumentation()); - - public static UiObject assertViewWillBeVisible(String resourceId, long timeout) { - UiObject view = device.findObject(new UiSelector().resourceIdMatches(".*:id/" + resourceId)); - boolean isDisplayed = view.waitForExists(timeout); - assertTrue("View with ID " + resourceId + "didn't show up", isDisplayed); - return view; - } -} diff --git a/example/android/app/src/androidTest/java/com/instabug/flutter/example/util/InstabugViewMatchers.java b/example/android/app/src/androidTest/java/com/instabug/flutter/example/util/InstabugViewMatchers.java deleted file mode 100644 index 67ec0c570..000000000 --- a/example/android/app/src/androidTest/java/com/instabug/flutter/example/util/InstabugViewMatchers.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.instabug.flutter.example.util; - -import android.graphics.Color; -import android.view.View; - -import org.hamcrest.Matcher; - -public class InstabugViewMatchers { - public static Matcher hasBackgroundColor(Color color) { - return new HasBackgroundColorMatcher(color); - } - - public static Matcher isToTheLeft() { - return new IsToTheLeftMatcher(); - } -} diff --git a/example/android/app/src/androidTest/java/com/instabug/flutter/example/util/IsToTheLeftMatcher.java b/example/android/app/src/androidTest/java/com/instabug/flutter/example/util/IsToTheLeftMatcher.java deleted file mode 100644 index d49edb2f4..000000000 --- a/example/android/app/src/androidTest/java/com/instabug/flutter/example/util/IsToTheLeftMatcher.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.instabug.flutter.example.util; - -import android.view.View; - -import org.hamcrest.Description; -import org.hamcrest.TypeSafeMatcher; - -public final class IsToTheLeftMatcher extends TypeSafeMatcher { - @Override - protected boolean matchesSafely(View view) { - return view.getRight() > view.getLeft(); - } - - @Override - public void describeTo(Description description) { - description.appendText("is to the left"); - } -} \ No newline at end of file diff --git a/example/android/app/src/androidTest/java/com/instabug/flutter/example/util/Keyboard.java b/example/android/app/src/androidTest/java/com/instabug/flutter/example/util/Keyboard.java deleted file mode 100644 index a279fb834..000000000 --- a/example/android/app/src/androidTest/java/com/instabug/flutter/example/util/Keyboard.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.instabug.flutter.example.util; - -import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; - -import android.app.Instrumentation; -import android.view.accessibility.AccessibilityWindowInfo; - -import androidx.test.uiautomator.UiDevice; - -public class Keyboard { - private static final Instrumentation instrumentation = getInstrumentation(); - private static final UiDevice device = UiDevice.getInstance(instrumentation); - - public static void closeKeyboard() { - if (isKeyboardOpened()) { - device.pressBack(); - } - } - - private static boolean isKeyboardOpened() { - for (AccessibilityWindowInfo window : instrumentation.getUiAutomation().getWindows()) { - if (window.getType() == AccessibilityWindowInfo.TYPE_INPUT_METHOD) { - return true; - } - } - return false; - } -} diff --git a/example/android/app/src/main/kotlin/com/example/InstabugSample/MainActivity.kt b/example/android/app/src/main/kotlin/com/example/InstabugSample/MainActivity.kt index 39cfd572c..b6d6f7352 100644 --- a/example/android/app/src/main/kotlin/com/example/InstabugSample/MainActivity.kt +++ b/example/android/app/src/main/kotlin/com/example/InstabugSample/MainActivity.kt @@ -6,7 +6,8 @@ import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.FlutterEngine import io.flutter.plugin.common.MethodChannel -class MainActivity: FlutterActivity() { +class +MainActivity : FlutterActivity() { override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) MethodChannel(flutterEngine.dartExecutor.binaryMessenger, METHOD_CHANNEL_NAME).setMethodCallHandler(InstabugExampleMethodCallHandler()) diff --git a/example/ios/InstabugTests/ApmApiTests.m b/example/ios/InstabugTests/ApmApiTests.m index 09e8bad46..073937c04 100644 --- a/example/ios/InstabugTests/ApmApiTests.m +++ b/example/ios/InstabugTests/ApmApiTests.m @@ -3,6 +3,7 @@ #import "ApmApi.h" #import "Instabug/IBGAPM.h" #import "Instabug/Instabug.h" +#import "IBGAPM+PrivateAPIs.h" @interface ApmApiTests : XCTestCase @@ -38,6 +39,65 @@ - (void)testSetEnabled { OCMVerify([self.mAPM setEnabled:YES]); } +- (void)testIsEnabled { + XCTestExpectation *expectation = [self expectationWithDescription:@"Call completion handler"]; + + BOOL isEnabled = YES; + OCMStub([self.mAPM enabled]).andReturn(isEnabled); + [self.api isEnabledWithCompletion:^(NSNumber *isEnabledNumber, FlutterError *error) { + [expectation fulfill]; + + XCTAssertEqualObjects(isEnabledNumber, @(isEnabled)); + XCTAssertNil(error); + }]; + + [self waitForExpectations:@[expectation] timeout:5.0]; +} + +- (void)testSetScreenLoadingEnabled { + + NSNumber *isEnabled = @1; + FlutterError *error; + + [self.api setScreenLoadingEnabledIsEnabled:isEnabled error:&error]; + + OCMVerify([self.mAPM setScreenLoadingEnabled:YES]); +} + +- (void)testIsScreenLoadingEnabled { + XCTestExpectation *expectation = [self expectationWithDescription:@"Call completion handler"]; + + BOOL isScreenLoadingMonitoringEnabled = YES; + OCMStub([self.mAPM screenLoadingEnabled]).andReturn(isScreenLoadingMonitoringEnabled); + + [self.api isScreenLoadingEnabledWithCompletion:^(NSNumber *isEnabledNumber, FlutterError *error) { + [expectation fulfill]; + + XCTAssertEqualObjects(isEnabledNumber, @(isScreenLoadingMonitoringEnabled)); + + XCTAssertNil(error); + }]; + + [self waitForExpectations:@[expectation] timeout:5.0]; +} + +- (void)testIsEndScreenLoadingEnabled { + XCTestExpectation *expectation = [self expectationWithDescription:@"Call completion handler"]; + + BOOL isEndScreenLoadingEnabled = YES; + OCMStub([self.mAPM endScreenLoadingEnabled]).andReturn(isEndScreenLoadingEnabled); + + [self.api isEndScreenLoadingEnabledWithCompletion:^(NSNumber *isEnabledNumber, FlutterError *error) { + [expectation fulfill]; + + XCTAssertEqualObjects(isEnabledNumber, @(isEndScreenLoadingEnabled)); + + XCTAssertNil(error); + }]; + + [self waitForExpectations:@[expectation] timeout:5.0]; +} + - (void)testSetColdAppLaunchEnabled { NSNumber *isEnabled = @1; FlutterError *error; @@ -168,4 +228,43 @@ - (void)testEndAppLaunch { OCMVerify([self.mAPM endAppLaunch]); } +- (void)testStartCpUiTrace { + NSString *screenName = @"testScreen"; + NSNumber *microTimeStamp = @(123456789); + NSNumber *traceId = @(987654321); + + NSTimeInterval microTimeStampMUS = [microTimeStamp doubleValue]; + FlutterError *error; + + [self.api startCpUiTraceScreenName:screenName microTimeStamp:microTimeStamp traceId:traceId error:&error]; + + OCMVerify([self.mAPM startUITraceCPWithName:screenName startTimestampMUS:microTimeStampMUS]); +} + +- (void)testReportScreenLoading { + NSNumber *startTimeStampMicro = @(123456789); + NSNumber *durationMicro = @(987654321); + NSNumber *uiTraceId = @(135792468); + FlutterError *error; + + NSTimeInterval startTimeStampMicroMUS = [startTimeStampMicro doubleValue]; + NSTimeInterval durationMUS = [durationMicro doubleValue]; + + [self.api reportScreenLoadingCPStartTimeStampMicro:startTimeStampMicro durationMicro:durationMicro uiTraceId:uiTraceId error:&error]; + + OCMVerify([self.mAPM reportScreenLoadingCPWithStartTimestampMUS:startTimeStampMicroMUS durationMUS:durationMUS]); +} + +- (void)testEndScreenLoading { + NSNumber *timeStampMicro = @(123456789); + NSNumber *uiTraceId = @(987654321); + FlutterError *error; + + NSTimeInterval endScreenLoadingCPWithEndTimestampMUS = [timeStampMicro doubleValue]; + [self.api endScreenLoadingCPTimeStampMicro:timeStampMicro uiTraceId:uiTraceId error:&error]; + + OCMVerify([self.mAPM endScreenLoadingCPWithEndTimestampMUS:endScreenLoadingCPWithEndTimestampMUS]); +} + + @end diff --git a/example/ios/InstabugTests/InstabugApiTests.m b/example/ios/InstabugTests/InstabugApiTests.m index 9f9684120..6c9544d71 100644 --- a/example/ios/InstabugTests/InstabugApiTests.m +++ b/example/ios/InstabugTests/InstabugApiTests.m @@ -4,8 +4,10 @@ #import "InstabugApi.h" #import "Instabug/Instabug.h" #import "Util/Instabug+Test.h" -#import "Util/IBGNetworkLogger+Test.h" +#import "IBGNetworkLogger+CP.h" #import "Flutter/Flutter.h" +#import "instabug_flutter/IBGAPM+PrivateAPIs.h" +#import "instabug_flutter/IBGNetworkLogger+CP.h" @interface InstabugTests : XCTestCase @@ -224,6 +226,46 @@ - (void)testClearAllExperiments { OCMVerify([self.mInstabug clearAllExperiments]); } +- (void)testAddFeatureFlags { + NSDictionary *featureFlagsMap = @{ @"key13" : @"value1", @"key2" : @"value2"}; + FlutterError *error; + + [self.api addFeatureFlagsFeatureFlagsMap:featureFlagsMap error:&error]; + OCMVerify([self.mInstabug addFeatureFlags: [OCMArg checkWithBlock:^(id value) { + NSArray *featureFlags = value; + NSString* firstFeatureFlagName = [featureFlags objectAtIndex:0 ].name; + NSString* firstFeatureFlagKey = [[featureFlagsMap allKeys] objectAtIndex:0] ; + if([ firstFeatureFlagKey isEqualToString: firstFeatureFlagName]){ + return YES; + } + return NO; + }]]); +} + +- (void)testRemoveFeatureFlags { + NSArray *featureFlags = @[@"exp1"]; + FlutterError *error; + + [self.api removeFeatureFlagsFeatureFlags:featureFlags error:&error]; + OCMVerify([self.mInstabug removeFeatureFlags: [OCMArg checkWithBlock:^(id value) { + NSArray *featureFlagsObJ = value; + NSString* firstFeatureFlagName = [featureFlagsObJ objectAtIndex:0 ].name; + NSString* firstFeatureFlagKey = [featureFlags firstObject] ; + if([ firstFeatureFlagKey isEqualToString: firstFeatureFlagName]){ + return YES; + } + return NO; + }]]);} + +- (void)testRemoveAllFeatureFlags { + FlutterError *error; + + [self.api removeAllFeatureFlagsWithError:&error]; + OCMVerify([self.mInstabug removeAllFeatureFlags]); +} + + + - (void)testSetUserAttribute { NSString *key = @"is_premium"; NSString *value = @"true"; @@ -395,7 +437,11 @@ - (void)testNetworkLog { duration:duration.integerValue gqlQueryName:nil serverErrorMessage:nil - ]); + isW3cCaughted:nil + partialID:nil + timestamp:nil + generatedW3CTraceparent:nil + caughtedW3CTraceparent:nil]); } - (void)testWillRedirectToAppStore { @@ -405,4 +451,155 @@ - (void)testWillRedirectToAppStore { OCMVerify([self.mInstabug willRedirectToAppStore]); } +- (void)testNetworkLogWithW3Caught { + NSString *url = @"https://example.com"; + NSString *requestBody = @"hi"; + NSNumber *requestBodySize = @17; + NSString *responseBody = @"{\"hello\":\"world\"}"; + NSNumber *responseBodySize = @153; + NSString *method = @"POST"; + NSNumber *responseCode = @201; + NSString *responseContentType = @"application/json"; + NSNumber *duration = @23000; + NSNumber *startTime = @1670156107523; + NSString *w3CCaughtHeader = @"1234"; + NSDictionary *requestHeaders = @{ @"Accepts": @"application/json",@"traceparent":w3CCaughtHeader}; + NSDictionary *responseHeaders = @{ @"Content-Type": @"text/plain" }; + NSDictionary *data = @{ + @"url": url, + @"requestBody": requestBody, + @"requestBodySize": requestBodySize, + @"responseBody": responseBody, + @"responseBodySize": responseBodySize, + @"method": method, + @"responseCode": responseCode, + @"requestHeaders": requestHeaders, + @"responseHeaders": responseHeaders, + @"responseContentType": responseContentType, + @"duration": duration, + @"startTime": startTime, + @"isW3cHeaderFound":@1, + @"w3CCaughtHeader":w3CCaughtHeader + }; + + FlutterError* error; + + [self.api networkLogData:data error:&error]; + + OCMVerify([self.mNetworkLogger addNetworkLogWithUrl:url + method:method + requestBody:requestBody + requestBodySize:requestBodySize.integerValue + responseBody:responseBody + responseBodySize:responseBodySize.integerValue + responseCode:(int32_t) responseCode.integerValue + requestHeaders:requestHeaders + responseHeaders:responseHeaders + contentType:responseContentType + errorDomain:nil + errorCode:0 + startTime:startTime.integerValue * 1000 + duration:duration.integerValue + gqlQueryName:nil + serverErrorMessage:nil + isW3cCaughted:@1 + partialID:nil + timestamp:nil + generatedW3CTraceparent:nil + caughtedW3CTraceparent:@"1234" + ]); +} + +- (void)testNetworkLogWithW3GeneratedHeader { + NSString *url = @"https://example.com"; + NSString *requestBody = @"hi"; + NSNumber *requestBodySize = @17; + NSString *responseBody = @"{\"hello\":\"world\"}"; + NSNumber *responseBodySize = @153; + NSString *method = @"POST"; + NSNumber *responseCode = @201; + NSString *responseContentType = @"application/json"; + NSNumber *duration = @23000; + NSNumber *startTime = @1670156107523; + NSDictionary *requestHeaders = @{ @"Accepts": @"application/json" }; + NSDictionary *responseHeaders = @{ @"Content-Type": @"text/plain" }; + NSNumber *partialID = @12; + + NSNumber *timestamp = @34; + + NSString *generatedW3CTraceparent = @"12-34"; + + NSString *caughtedW3CTraceparent = nil; + NSDictionary *data = @{ + @"url": url, + @"requestBody": requestBody, + @"requestBodySize": requestBodySize, + @"responseBody": responseBody, + @"responseBodySize": responseBodySize, + @"method": method, + @"responseCode": responseCode, + @"requestHeaders": requestHeaders, + @"responseHeaders": responseHeaders, + @"responseContentType": responseContentType, + @"duration": duration, + @"startTime": startTime, + @"isW3cHeaderFound": @0, + @"partialId": partialID, + @"networkStartTimeInSeconds": timestamp, + @"w3CGeneratedHeader": generatedW3CTraceparent, + + }; + NSNumber *isW3cCaughted = @0; + + FlutterError* error; + + [self.api networkLogData:data error:&error]; + + OCMVerify([self.mNetworkLogger addNetworkLogWithUrl:url + method:method + requestBody:requestBody + requestBodySize:requestBodySize.integerValue + responseBody:responseBody + responseBodySize:responseBodySize.integerValue + responseCode:(int32_t) responseCode.integerValue + requestHeaders:requestHeaders + responseHeaders:responseHeaders + contentType:responseContentType + errorDomain:nil + errorCode:0 + startTime:startTime.integerValue * 1000 + duration:duration.integerValue + gqlQueryName:nil + serverErrorMessage:nil + isW3cCaughted:isW3cCaughted + partialID:partialID + timestamp:timestamp + generatedW3CTraceparent:generatedW3CTraceparent + caughtedW3CTraceparent:caughtedW3CTraceparent + + + + ]); +} + +- (void)testisW3CFeatureFlagsEnabled { + FlutterError *error; + + id mock = OCMClassMock([IBGNetworkLogger class]); + NSNumber *isW3cExternalTraceIDEnabled = @(YES); + + OCMStub([mock w3ExternalTraceIDEnabled]).andReturn([isW3cExternalTraceIDEnabled boolValue]); + OCMStub([mock w3ExternalGeneratedHeaderEnabled]).andReturn([isW3cExternalTraceIDEnabled boolValue]); + OCMStub([mock w3CaughtHeaderEnabled]).andReturn([isW3cExternalTraceIDEnabled boolValue]); + + + + NSDictionary * result= [self.api isW3CFeatureFlagsEnabledWithError:&error]; + + XCTAssertEqual(result[@"isW3cExternalTraceIDEnabled"],isW3cExternalTraceIDEnabled); + XCTAssertEqual(result[@"isW3cExternalGeneratedHeaderEnabled"],isW3cExternalTraceIDEnabled); + XCTAssertEqual(result[@"isW3cCaughtHeaderEnabled"],isW3cExternalTraceIDEnabled); + +} + @end diff --git a/example/ios/InstabugTests/Util/Apm+Test.h b/example/ios/InstabugTests/Util/Apm+Test.h new file mode 100644 index 000000000..c3bddc4bf --- /dev/null +++ b/example/ios/InstabugTests/Util/Apm+Test.h @@ -0,0 +1,11 @@ +// This header file defines Instabug methods that are called using selectors for test verification. + +#import +#import + +@interface IBGAPM (Test) ++ (void)startUITraceCPWithName:(NSString *)name startTimestampMUS:(NSTimeInterval)startTimestampMUS; ++ (void)reportScreenLoadingCPWithStartTimestampMUS:(NSTimeInterval)startTimestampMUS + durationMUS:(NSTimeInterval)durationMUS; ++ (void)endScreenLoadingCPWithEndTimestampMUS:(NSTimeInterval)endTimestampMUS; +@end diff --git a/example/ios/InstabugTests/Util/IBGNetworkLogger+Test.h b/example/ios/InstabugTests/Util/IBGNetworkLogger+Test.h index 55475a81f..09f10eb8d 100644 --- a/example/ios/InstabugTests/Util/IBGNetworkLogger+Test.h +++ b/example/ios/InstabugTests/Util/IBGNetworkLogger+Test.h @@ -18,5 +18,10 @@ startTime:(int64_t)startTime duration:(int64_t) duration gqlQueryName:(NSString *_Nullable)gqlQueryName - serverErrorMessage:(NSString *_Nullable)gqlServerError; + serverErrorMessage:(NSString *_Nullable)gqlServerError + isW3cCaughted:(NSNumber *_Nullable)isW3cCaughted + partialID:(NSNumber *_Nullable)partialId + timestamp:(NSNumber *_Nullable)timestamp + generatedW3CTraceparent:(NSString *_Nullable)generatedW3CTraceparent + caughtedW3CTraceparent:(NSString *_Nullable)caughtedW3CTraceparent; @end diff --git a/example/ios/InstabugUITests/BugReportingUITests.m b/example/ios/InstabugUITests/BugReportingUITests.m deleted file mode 100644 index 2698fc031..000000000 --- a/example/ios/InstabugUITests/BugReportingUITests.m +++ /dev/null @@ -1,96 +0,0 @@ -#import -#import "Util/XCUIElement+Instabug.h" - -@interface BugReportingUITests : XCTestCase - -@property (nonatomic, strong) XCUIApplication *app; - -@end - -@implementation BugReportingUITests - -- (void)setUp { - self.continueAfterFailure = NO; - self.app = [[XCUIApplication alloc] init]; - [self.app launch]; -} - -- (void)testMultipleScreenshotsInReproSteps { - NSString *screen = @"My Screen"; - - XCUIElement *screenField = self.app.textFields[@"Enter screen name"]; - [screenField tap]; - [screenField typeText:screen]; - [screenField closeKeyboard]; - [self.app.buttons[@"Report Screen Change"] tapWithNumberOfTaps:2 numberOfTouches:1]; - - [self.app.buttons[@"Send Bug Report"] tap]; - [self.app.staticTexts[@"IBGBugVCReproStepsDisclaimerAccessibilityIdentifier"] tap]; - - NSPredicate *screensPredicate = [NSPredicate predicateWithFormat:@"label == %@", screen]; - XCUIElementQuery *screensQuery = [self.app.staticTexts matchingPredicate:screensPredicate]; - - XCTAssertEqual(2, screensQuery.count); -} - -- (void)testFloatingButtonInvocationEvent { - // Grabbing the "Floating Button" invocation event button - // inside the "Change Invocation Events" section as it - // conflicts with Instabug's floating button. - XCUIElement *invocationEvents = [[self.app.otherElements containingPredicate:[NSPredicate predicateWithFormat:@"label == 'Change Invocation Event'"]] element]; - [invocationEvents.buttons[@"Floating Button"] forceTap]; - [self.app.buttons[@"IBGFloatingButtonAccessibilityIdentifier"] tap]; - - [self assertOptionsPromptIsVisible]; -} - -- (void)testNoneInvocationEvent { - [self.app.buttons[@"None"] tap]; - - [self.app.buttons[@"IBGFloatingButtonAccessibilityIdentifier"] assertDoesNotExist]; -} - -- (void)testManualInvocation { - [self.app.buttons[@"Invoke"] tap]; - - [self assertOptionsPromptIsVisible]; -} - -- (void)testOnDismissCallbackIsCalled { - [self.app.buttons[@"Set On Dismiss Callback"] scrollDownAndTap]; - [self.app.buttons[@"Invoke"] scrollUpAndTap]; - [self.app.buttons[@"Cancel"] tap]; - - [self.app.staticTexts[@"onDismiss callback called with DismissType.cancel and ReportType.other"] assertExistsWithTimeout:2]; -} - -- (void)testChangeReportTypes { - [self.app.buttons[@"Bug"] scrollDownAndTap]; - [self.app.buttons[@"Feedback"] scrollDownAndTap]; - [self.app.buttons[@"Invoke"] scrollUpAndTap]; - - [self assertOptionsPromptIsVisible]; - [self.app.staticTexts[@"Report a bug"] assertExistsWithTimeout:2000]; - [self.app.staticTexts[@"Suggest an improvement"] assertExistsWithTimeout:2000]; - [self.app.staticTexts[@"Ask a question"] assertDoesNotExist]; -} - -- (void)testChangeFloatingButtonEdge { - // Grabbing the "Floating Button" invocation event button - // inside the "Change Invocation Events" section as it - // conflicts with Instabug's floating button. - XCUIElement *invocationEvents = [[self.app.otherElements containingPredicate:[NSPredicate predicateWithFormat:@"label == 'Change Invocation Event'"]] element]; - [invocationEvents.buttons[@"Floating Button"] forceTap]; - [self.app.buttons[@"Move Floating Button to Left"] scrollDownAndTap]; - - XCUIElement *floatingButton = self.app.buttons[@"IBGFloatingButtonAccessibilityIdentifier"]; - CGFloat floatingButtonLeft = floatingButton.frame.origin.x; - CGFloat screenMiddle = self.app.frame.size.width / 2.0f; - XCTAssertLessThan(floatingButtonLeft, screenMiddle, @"Floating button isn't to the left of the screen"); -} - -- (void)assertOptionsPromptIsVisible { - [self.app.cells[@"IBGReportBugPromptOptionAccessibilityIdentifier"] assertExistsWithTimeout:2]; -} - -@end diff --git a/example/ios/InstabugUITests/FeatureRequestsUITests.m b/example/ios/InstabugUITests/FeatureRequestsUITests.m deleted file mode 100644 index 95ac389dc..000000000 --- a/example/ios/InstabugUITests/FeatureRequestsUITests.m +++ /dev/null @@ -1,25 +0,0 @@ -#import -#import "Util/XCUIElement+Instabug.h" -#import "Util/InstabugUITestsUtils.h" - -@interface FeatureRequestsUITests : XCTestCase - -@property (nonatomic, strong) XCUIApplication *app; - -@end - -@implementation FeatureRequestsUITests - -- (void)setUp { - self.continueAfterFailure = NO; - self.app = [[XCUIApplication alloc] init]; - [self.app launch]; -} - -- (void)testShowFeatureRequestsScreen { - [self.app.buttons[@"Show Feature Requests"] scrollDownAndTap]; - - [self.app.staticTexts[@"Feature Requests"] assertExistsWithTimeout:2]; -} - -@end diff --git a/example/ios/InstabugUITests/Info.plist b/example/ios/InstabugUITests/Info.plist deleted file mode 100644 index 64d65ca49..000000000 --- a/example/ios/InstabugUITests/Info.plist +++ /dev/null @@ -1,22 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - $(PRODUCT_BUNDLE_PACKAGE_TYPE) - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - - diff --git a/example/ios/InstabugUITests/InstabugUITests.m b/example/ios/InstabugUITests/InstabugUITests.m deleted file mode 100644 index f0590be98..000000000 --- a/example/ios/InstabugUITests/InstabugUITests.m +++ /dev/null @@ -1,36 +0,0 @@ -#import -#import "Util/XCUIElement+Instabug.h" -#import "Util/InstabugUITestsUtils.h" - -@interface InstabugUITests : XCTestCase - -@property (nonatomic, strong) XCUIApplication *app; - -@end - -@implementation InstabugUITests - -- (void)setUp { - self.continueAfterFailure = NO; - self.app = [[XCUIApplication alloc] init]; - [self.app launch]; -} - -- (void)testChangePrimaryColor { - NSString *color = @"#FF0000"; - UIColor *expected = [UIColor redColor]; - XCUIElement *colorField = self.app.textFields[@"Enter primary color"]; - [colorField tap]; - [colorField typeText:color]; - [colorField closeKeyboard]; - [self.app.buttons[@"Change Primary Color"] tap]; - - XCUIElement *floatingButton = self.app.buttons[@"IBGFloatingButtonAccessibilityIdentifier"]; - UIImage *image = [[floatingButton screenshot] image]; - int x = image.size.width / 2; - int y = 5; - UIColor *actual = [InstabugUITestsUtils getPixelColorWithImage:image x:x y:y]; - XCTAssertTrue([actual isEqual:expected]); -} - -@end diff --git a/example/ios/InstabugUITests/SurveysUITests.m b/example/ios/InstabugUITests/SurveysUITests.m deleted file mode 100644 index a020064c9..000000000 --- a/example/ios/InstabugUITests/SurveysUITests.m +++ /dev/null @@ -1,25 +0,0 @@ -#import -#import "Util/XCUIElement+Instabug.h" -#import "Util/InstabugUITestsUtils.h" - -@interface SurveysUITests : XCTestCase - -@property (nonatomic, strong) XCUIApplication *app; - -@end - -@implementation SurveysUITests - -- (void)setUp { - self.continueAfterFailure = NO; - self.app = [[XCUIApplication alloc] init]; - [self.app launch]; -} - -- (void)testShowManualSurvey { - [self.app.buttons[@"Show Manual Survey"] scrollDownAndTap]; - - [self.app.otherElements[@"SurveyNavigationVC"] assertExistsWithTimeout:2]; -} - -@end diff --git a/example/ios/InstabugUITests/Util/InstabugUITestsUtils.h b/example/ios/InstabugUITests/Util/InstabugUITestsUtils.h deleted file mode 100644 index 6bbb6b372..000000000 --- a/example/ios/InstabugUITests/Util/InstabugUITestsUtils.h +++ /dev/null @@ -1,9 +0,0 @@ -#import -#import - -@interface InstabugUITestsUtils : NSObject - -+ (UIColor *)getPixelColorWithImage:(UIImage *)image x:(NSInteger)x y:(NSInteger)y; - -@end - diff --git a/example/ios/InstabugUITests/Util/InstabugUITestsUtils.m b/example/ios/InstabugUITests/Util/InstabugUITestsUtils.m deleted file mode 100644 index 108b4f2dc..000000000 --- a/example/ios/InstabugUITests/Util/InstabugUITestsUtils.m +++ /dev/null @@ -1,28 +0,0 @@ -#import "InstabugUITestsUtils.h" - -@implementation InstabugUITestsUtils - -+ (UIColor *)getPixelColorWithImage:(UIImage *)image x:(NSInteger)x y:(NSInteger)y { - CGImageRef cgImage = image.CGImage; - CGFloat width = image.size.width; - CGFloat height = image.size.height; - CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); - int bytesPerPixel = 4; - NSUInteger bitsPerComponent = 8; - UInt8 pixelData[4] = {0, 0, 0, 0}; - CGContextRef context = CGBitmapContextCreate(pixelData, 1, 1, bitsPerComponent, bytesPerPixel, colorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big); - CGColorSpaceRelease(colorSpace); - CGContextSetBlendMode(context, kCGBlendModeCopy); - - CGContextTranslateCTM(context, -x, y - height); - CGContextDrawImage(context, CGRectMake(0.0f, 0.0f, width, height), cgImage); - CGContextRelease(context); - - CGFloat red = pixelData[0] / 255.0f; - CGFloat green = pixelData[1] / 255.0f; - CGFloat blue = pixelData[2] / 255.0f; - CGFloat alpha = pixelData[3] / 255.0f; - return [UIColor colorWithRed:red green:green blue:blue alpha:alpha]; -} - -@end diff --git a/example/ios/InstabugUITests/Util/XCUIElement+Instabug.h b/example/ios/InstabugUITests/Util/XCUIElement+Instabug.h deleted file mode 100644 index 3f45c93be..000000000 --- a/example/ios/InstabugUITests/Util/XCUIElement+Instabug.h +++ /dev/null @@ -1,12 +0,0 @@ -#import - -@interface XCUIElement (Instabug) - -- (void)scrollDownAndTap; -- (void)scrollUpAndTap; -- (void)closeKeyboard; -- (void)forceTap; -- (void)assertExistsWithTimeout:(NSTimeInterval)timeout; -- (void)assertDoesNotExist; - -@end diff --git a/example/ios/InstabugUITests/Util/XCUIElement+Instabug.m b/example/ios/InstabugUITests/Util/XCUIElement+Instabug.m deleted file mode 100644 index c7880ec36..000000000 --- a/example/ios/InstabugUITests/Util/XCUIElement+Instabug.m +++ /dev/null @@ -1,50 +0,0 @@ -#import "XCUIElement+Instabug.h" - -@implementation XCUIElement (Instabug) - -- (void)scrollDownAndTap { - [self scrollDownAndTapWithBlock:^(XCUIApplication *app) { - [app swipeUp]; - }]; -} - -- (void)scrollUpAndTap { - [self scrollDownAndTapWithBlock:^(XCUIApplication *app) { - [app swipeDown]; - }]; -} - -- (void)scrollDownAndTapWithBlock:(void (^)(XCUIApplication *app))block { - XCUIApplication *app = [[XCUIApplication alloc] init]; - - int count = 0; - while (!self.isHittable && count < 10) { - block(app); - count++; - } - - sleep(1); - [self tap]; -} - -- (void)closeKeyboard { - [self typeText:@"\n"]; -} - -/// Taps on the button's coordinates without checking if it's visible. -/// This is useful as XCUITest fails to scroll to Flutter widgets even though they might be visible -/// on the screen. -- (void)forceTap { - XCUICoordinate *coordinate = [self coordinateWithNormalizedOffset:CGVectorMake(0.0f, 0.0f)]; - [coordinate tap]; -} - -- (void)assertExistsWithTimeout:(NSTimeInterval)timeout { - XCTAssertTrue([self waitForExistenceWithTimeout:timeout], "Element described by: %@ doesn't exist, expected to exist", self.description); -} - -- (void)assertDoesNotExist { - XCTAssertFalse([self waitForExistenceWithTimeout:2], "Element described by: %@ exists, expected to not exist", self.description); -} - -@end diff --git a/example/ios/Podfile b/example/ios/Podfile index 287784283..cdffbc5db 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -platform :ios, '13.0' +platform :ios, '13.4' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' @@ -35,14 +35,7 @@ end target 'InstabugTests' do pod 'OCMock', '3.6' - - use_frameworks! - use_modular_headers! - - flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) -end -target 'InstabugUITests' do use_frameworks! use_modular_headers! @@ -52,5 +45,8 @@ end post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) + target.build_configurations.each do |config| + config.build_settings["IPHONEOS_DEPLOYMENT_TARGET"] = "11.0" + end end end diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index e5ea812c4..b0ee46890 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1,9 +1,9 @@ PODS: - Flutter (1.0.0) - - Instabug (13.1.0) - - instabug_flutter (13.1.1): + - Instabug (13.4.2) + - instabug_flutter (13.4.0): - Flutter - - Instabug (= 13.1.0) + - Instabug (= 13.4.2) - OCMock (3.6) DEPENDENCIES: @@ -24,10 +24,10 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - Instabug: 3d55eff7ea55adf22df404908a2b954b8b585c29 - instabug_flutter: b9e34b14992d267f676f925de884da7eeae5e0ce + Instabug: 7a71890217b97b1e32dbca96661845396b66da2f + instabug_flutter: a2df87e3d4d9e410785e0b1ffef4bc64d1f4b787 OCMock: 5ea90566be239f179ba766fd9fbae5885040b992 -PODFILE CHECKSUM: a8bb1cf031fc5abb4046443c6fa65330087bf638 +PODFILE CHECKSUM: 8f7552fd115ace1988c3db54a69e4a123c448f84 -COCOAPODS: 1.13.0 +COCOAPODS: 1.14.3 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index ebd99c58a..858ba01e5 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -11,23 +11,16 @@ 2001D1442B8F501000885261 /* InstabugExampleMethodCallHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 2001D1432B8F501000885261 /* InstabugExampleMethodCallHandler.m */; }; 206286ED2ABD0A1F00925509 /* SessionReplayApiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 206286EC2ABD0A1F00925509 /* SessionReplayApiTests.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 3EA1F5233E85A5C4F9EF3957 /* Pods_InstabugUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F5446C0D3B2623D9BCC7CCE3 /* Pods_InstabugUITests.framework */; }; 65C88E6E8EAE049E32FF2F52 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 853739F5879F6E4272829F47 /* Pods_Runner.framework */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 9D381ECFBB01BD0E978EBDF2 /* Pods_InstabugTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 71679BEC094CFF3474195C2E /* Pods_InstabugTests.framework */; }; - C09001B425D9A3C5006F3DAE /* BugReportingUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = C09001B325D9A3C5006F3DAE /* BugReportingUITests.m */; }; CC080E112937B7DB0041170A /* InstabugApiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC080E102937B7DB0041170A /* InstabugApiTests.m */; }; CC198C61293E1A21007077C8 /* SurveysApiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC198C60293E1A21007077C8 /* SurveysApiTests.m */; }; CC359DB92937720C0067A924 /* ApmApiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC359DB82937720C0067A924 /* ApmApiTests.m */; }; CC3D69E7293F47FC000DCE54 /* ArgsRegistryTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC3D69E6293F47FC000DCE54 /* ArgsRegistryTests.m */; }; - CC6018BE2948C371003A845D /* InstabugUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC6018BD2948C371003A845D /* InstabugUITests.m */; }; - CC77911C294B6E9B00296485 /* FeatureRequestsUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC77911B294B6E9B00296485 /* FeatureRequestsUITests.m */; }; - CC77911E294B707900296485 /* SurveysUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC77911D294B707900296485 /* SurveysUITests.m */; }; - CC7B6B4D2949D43D00C6274F /* XCUIElement+Instabug.m in Sources */ = {isa = PBXBuildFile; fileRef = CC7B6B4C2949D43D00C6274F /* XCUIElement+Instabug.m */; }; - CC7B6B512949F9B700C6274F /* InstabugUITestsUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = CC7B6B502949F9B700C6274F /* InstabugUITestsUtils.m */; }; CC9925D2293DEB0B001FD3EE /* CrashReportingApiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC9925D1293DEB0B001FD3EE /* CrashReportingApiTests.m */; }; CC9925D5293DF534001FD3EE /* FeatureRequestsApiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC9925D4293DF534001FD3EE /* FeatureRequestsApiTests.m */; }; CC9925D7293DFB03001FD3EE /* InstabugLogApiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC9925D6293DFB03001FD3EE /* InstabugLogApiTests.m */; }; @@ -43,13 +36,6 @@ remoteGlobalIDString = 97C146ED1CF9000F007C117D; remoteInfo = Runner; }; - C09001B625D9A3C5006F3DAE /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 97C146E61CF9000F007C117D /* Project object */; - proxyType = 1; - remoteGlobalIDString = 97C146ED1CF9000F007C117D; - remoteInfo = Runner; - }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -71,7 +57,6 @@ 2001D1432B8F501000885261 /* InstabugExampleMethodCallHandler.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = InstabugExampleMethodCallHandler.m; sourceTree = ""; }; 2001D1452B8F504C00885261 /* InstabugExampleMethodCallHandler.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = InstabugExampleMethodCallHandler.h; sourceTree = ""; }; 206286EC2ABD0A1F00925509 /* SessionReplayApiTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SessionReplayApiTests.m; sourceTree = ""; }; - 20CE6BF92BC6DCA400105F88 /* Apm+Test.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Apm+Test.h"; sourceTree = ""; }; 243EF14638ECA64074771B11 /* Pods-InstabugTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-InstabugTests.release.xcconfig"; path = "Target Support Files/Pods-InstabugTests/Pods-InstabugTests.release.xcconfig"; sourceTree = ""; }; 354EA318B622513FE3FD25E4 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; @@ -96,23 +81,12 @@ BF9025BBD0A6FD7B193E903A /* Pods-InstabugTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-InstabugTests.debug.xcconfig"; path = "Target Support Files/Pods-InstabugTests/Pods-InstabugTests.debug.xcconfig"; sourceTree = ""; }; C090017925D9A030006F3DAE /* InstabugTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = InstabugTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; C090017D25D9A031006F3DAE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - C09001B125D9A3C5006F3DAE /* InstabugUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = InstabugUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - C09001B325D9A3C5006F3DAE /* BugReportingUITests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BugReportingUITests.m; sourceTree = ""; }; - C09001B525D9A3C5006F3DAE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; CC080E102937B7DB0041170A /* InstabugApiTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = InstabugApiTests.m; sourceTree = ""; }; CC198C60293E1A21007077C8 /* SurveysApiTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SurveysApiTests.m; sourceTree = ""; }; CC198C62293E2392007077C8 /* IBGSurvey+Test.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "IBGSurvey+Test.h"; sourceTree = ""; }; CC359DB82937720C0067A924 /* ApmApiTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ApmApiTests.m; sourceTree = ""; }; CC3D69E6293F47FC000DCE54 /* ArgsRegistryTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ArgsRegistryTests.m; sourceTree = ""; }; - CC6018BD2948C371003A845D /* InstabugUITests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = InstabugUITests.m; sourceTree = ""; }; - CC77911B294B6E9B00296485 /* FeatureRequestsUITests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FeatureRequestsUITests.m; sourceTree = ""; }; - CC77911D294B707900296485 /* SurveysUITests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SurveysUITests.m; sourceTree = ""; }; CC78720E293CA8EE008CB2A5 /* Instabug+Test.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Instabug+Test.h"; sourceTree = ""; }; - CC787211293CAB28008CB2A5 /* IBGNetworkLogger+Test.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "IBGNetworkLogger+Test.h"; sourceTree = ""; }; - CC7B6B4C2949D43D00C6274F /* XCUIElement+Instabug.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "XCUIElement+Instabug.m"; sourceTree = ""; }; - CC7B6B4E2949D52800C6274F /* XCUIElement+Instabug.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "XCUIElement+Instabug.h"; sourceTree = ""; }; - CC7B6B4F2949F95600C6274F /* InstabugUITestsUtils.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = InstabugUITestsUtils.h; sourceTree = ""; }; - CC7B6B502949F9B700C6274F /* InstabugUITestsUtils.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = InstabugUITestsUtils.m; sourceTree = ""; }; CC9925D1293DEB0B001FD3EE /* CrashReportingApiTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CrashReportingApiTests.m; sourceTree = ""; }; CC9925D4293DF534001FD3EE /* FeatureRequestsApiTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FeatureRequestsApiTests.m; sourceTree = ""; }; CC9925D6293DFB03001FD3EE /* InstabugLogApiTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = InstabugLogApiTests.m; sourceTree = ""; }; @@ -139,14 +113,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - C09001AE25D9A3C5006F3DAE /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 3EA1F5233E85A5C4F9EF3957 /* Pods_InstabugUITests.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -193,7 +159,6 @@ 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, C090017A25D9A031006F3DAE /* InstabugTests */, - C09001B225D9A3C5006F3DAE /* InstabugUITests */, 97C146EF1CF9000F007C117D /* Products */, 263CD21F80996ACC7964A383 /* Pods */, 54C1C903B090526284242B67 /* Frameworks */, @@ -205,7 +170,6 @@ children = ( 97C146EE1CF9000F007C117D /* Runner.app */, C090017925D9A030006F3DAE /* InstabugTests.xctest */, - C09001B125D9A3C5006F3DAE /* InstabugUITests.xctest */, ); name = Products; sourceTree = ""; @@ -246,41 +210,16 @@ path = InstabugTests; sourceTree = ""; }; - C09001B225D9A3C5006F3DAE /* InstabugUITests */ = { - isa = PBXGroup; - children = ( - CC77911D294B707900296485 /* SurveysUITests.m */, - CC7B6B442949D0E700C6274F /* Util */, - C09001B325D9A3C5006F3DAE /* BugReportingUITests.m */, - C09001B525D9A3C5006F3DAE /* Info.plist */, - CC6018BD2948C371003A845D /* InstabugUITests.m */, - CC77911B294B6E9B00296485 /* FeatureRequestsUITests.m */, - ); - path = InstabugUITests; - sourceTree = ""; - }; CC78720A2938D1C5008CB2A5 /* Util */ = { isa = PBXGroup; children = ( BE26C80C2BD55575009FECCF /* IBGCrashReporting+CP.h */, CC78720E293CA8EE008CB2A5 /* Instabug+Test.h */, - CC787211293CAB28008CB2A5 /* IBGNetworkLogger+Test.h */, CC198C62293E2392007077C8 /* IBGSurvey+Test.h */, ); path = Util; sourceTree = ""; }; - CC7B6B442949D0E700C6274F /* Util */ = { - isa = PBXGroup; - children = ( - CC7B6B4E2949D52800C6274F /* XCUIElement+Instabug.h */, - CC7B6B4C2949D43D00C6274F /* XCUIElement+Instabug.m */, - CC7B6B4F2949F95600C6274F /* InstabugUITestsUtils.h */, - CC7B6B502949F9B700C6274F /* InstabugUITestsUtils.m */, - ); - path = Util; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -326,26 +265,6 @@ productReference = C090017925D9A030006F3DAE /* InstabugTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; - C09001B025D9A3C5006F3DAE /* InstabugUITests */ = { - isa = PBXNativeTarget; - buildConfigurationList = C09001BB25D9A3C5006F3DAE /* Build configuration list for PBXNativeTarget "InstabugUITests" */; - buildPhases = ( - CADA44F7640ED1F5D8ED0572 /* [CP] Check Pods Manifest.lock */, - C09001AD25D9A3C5006F3DAE /* Sources */, - C09001AE25D9A3C5006F3DAE /* Frameworks */, - C09001AF25D9A3C5006F3DAE /* Resources */, - 24075D24972065C24C6CD84D /* [CP] Embed Pods Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - C09001B725D9A3C5006F3DAE /* PBXTargetDependency */, - ); - name = InstabugUITests; - productName = InstabugUITests; - productReference = C09001B125D9A3C5006F3DAE /* InstabugUITests.xctest */; - productType = "com.apple.product-type.bundle.ui-testing"; - }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -364,10 +283,6 @@ LastSwiftMigration = 1340; TestTargetID = 97C146ED1CF9000F007C117D; }; - C09001B025D9A3C5006F3DAE = { - CreatedOnToolsVersion = 12.3; - TestTargetID = 97C146ED1CF9000F007C117D; - }; }; }; buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; @@ -385,7 +300,6 @@ targets = ( 97C146ED1CF9000F007C117D /* Runner */, C090017825D9A030006F3DAE /* InstabugTests */, - C09001B025D9A3C5006F3DAE /* InstabugUITests */, ); }; /* End PBXProject section */ @@ -409,33 +323,9 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - C09001AF25D9A3C5006F3DAE /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 24075D24972065C24C6CD84D /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-InstabugUITests/Pods-InstabugUITests-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-InstabugUITests/Pods-InstabugUITests-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-InstabugUITests/Pods-InstabugUITests-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; 27D8E26CCAF4064D58654530 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -523,28 +413,6 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - CADA44F7640ED1F5D8ED0572 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-InstabugUITests-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; F3392DEBC1FBE1A1719AA7BD /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -597,19 +465,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - C09001AD25D9A3C5006F3DAE /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - CC7B6B512949F9B700C6274F /* InstabugUITestsUtils.m in Sources */, - CC6018BE2948C371003A845D /* InstabugUITests.m in Sources */, - CC77911C294B6E9B00296485 /* FeatureRequestsUITests.m in Sources */, - CC77911E294B707900296485 /* SurveysUITests.m in Sources */, - C09001B425D9A3C5006F3DAE /* BugReportingUITests.m in Sources */, - CC7B6B4D2949D43D00C6274F /* XCUIElement+Instabug.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -618,11 +473,6 @@ target = 97C146ED1CF9000F007C117D /* Runner */; targetProxy = C090017E25D9A031006F3DAE /* PBXContainerItemProxy */; }; - C09001B725D9A3C5006F3DAE /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 97C146ED1CF9000F007C117D /* Runner */; - targetProxy = C09001B625D9A3C5006F3DAE /* PBXContainerItemProxy */; - }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ @@ -994,94 +844,6 @@ }; name = Profile; }; - C09001B825D9A3C5006F3DAE /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = B03C8370EEFE061BDDDA1DA1 /* Pods-InstabugUITests.debug.xcconfig */; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = 56S6Q9SA8U; - GCC_C_LANGUAGE_STANDARD = gnu11; - INFOPLIST_FILE = InstabugUITests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 14.3; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = com.instabug.InstabugUITests; - PRODUCT_NAME = "$(TARGET_NAME)"; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_TARGET_NAME = Runner; - }; - name = Debug; - }; - C09001B925D9A3C5006F3DAE /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 671EF4FEC6834993651EBC4D /* Pods-InstabugUITests.release.xcconfig */; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = 56S6Q9SA8U; - GCC_C_LANGUAGE_STANDARD = gnu11; - INFOPLIST_FILE = InstabugUITests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 14.3; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = com.instabug.InstabugUITests; - PRODUCT_NAME = "$(TARGET_NAME)"; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_TARGET_NAME = Runner; - }; - name = Release; - }; - C09001BA25D9A3C5006F3DAE /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = A964F0D42132F93F7E4DEB73 /* Pods-InstabugUITests.profile.xcconfig */; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = 56S6Q9SA8U; - GCC_C_LANGUAGE_STANDARD = gnu11; - INFOPLIST_FILE = InstabugUITests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 14.3; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = com.instabug.InstabugUITests; - PRODUCT_NAME = "$(TARGET_NAME)"; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_TARGET_NAME = Runner; - }; - name = Profile; - }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -1115,16 +877,6 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - C09001BB25D9A3C5006F3DAE /* Build configuration list for PBXNativeTarget "InstabugUITests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - C09001B825D9A3C5006F3DAE /* Debug */, - C09001B925D9A3C5006F3DAE /* Release */, - C09001BA25D9A3C5006F3DAE /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; /* End XCConfigurationList section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; diff --git a/example/ios/Runner/AppDelegate.swift b/example/ios/Runner/AppDelegate.swift index 70693e4a8..11f416a26 100644 --- a/example/ios/Runner/AppDelegate.swift +++ b/example/ios/Runner/AppDelegate.swift @@ -3,11 +3,17 @@ import Flutter @UIApplicationMain @objc class AppDelegate: FlutterAppDelegate { - override func application( - _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? - ) -> Bool { - GeneratedPluginRegistrant.register(with: self) - return super.application(application, didFinishLaunchingWithOptions: launchOptions) - } + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + let controller = window?.rootViewController as! FlutterViewController + let channel = FlutterMethodChannel(name: kInstabugChannelName, binaryMessenger: controller.binaryMessenger) + let methodCallHandler = InstabugExampleMethodCallHandler() + channel.setMethodCallHandler { methodCall, result in + methodCallHandler.handle(methodCall, result: result) + } + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } } diff --git a/example/lib/main.dart b/example/lib/main.dart index 2889f2ebc..cda7ff3ec 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -2,10 +2,10 @@ import 'dart:async'; import 'dart:developer'; import 'dart:io'; import 'dart:convert'; -import 'package:http/http.dart' as http; import 'package:flutter/material.dart'; import 'package:instabug_flutter/instabug_flutter.dart'; +import 'package:instabug_http_client/instabug_http_client.dart'; import 'package:instabug_flutter_example/src/app_routes.dart'; import 'package:instabug_flutter_example/src/widget/nested_view.dart'; @@ -13,6 +13,7 @@ import 'src/native/instabug_flutter_example_method_channel.dart'; import 'src/widget/instabug_button.dart'; import 'src/widget/instabug_clipboard_input.dart'; import 'src/widget/instabug_text_field.dart'; +import 'package:instabug_flutter/src/utils/screen_loading/screen_loading_manager.dart'; import 'src/widget/section_title.dart'; @@ -22,6 +23,10 @@ part 'src/screens/complex_page.dart'; part 'src/screens/apm_page.dart'; +part 'src/screens/screen_capture_premature_extension_page.dart'; + +part 'src/screens/screen_loading_page.dart'; + part 'src/screens/my_home_page.dart'; part 'src/components/fatal_crashes_content.dart'; @@ -48,8 +53,7 @@ void main() { ); FlutterError.onError = (FlutterErrorDetails details) { - Zone.current.handleUncaughtError( - details.exception, details.stack ?? StackTrace.current); + Zone.current.handleUncaughtError(details.exception, details.stack!); }; runApp(const MyApp()); @@ -68,7 +72,7 @@ class MyApp extends StatelessWidget { navigatorObservers: [ InstabugNavigatorObserver(), ], - routes: appRoutes, + routes: APM.wrapRoutes(appRoutes, exclude: [CrashesPage.screenName]), theme: ThemeData( primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, diff --git a/example/lib/src/app_routes.dart b/example/lib/src/app_routes.dart index b53fdc5d3..9175d5405 100644 --- a/example/lib/src/app_routes.dart +++ b/example/lib/src/app_routes.dart @@ -11,4 +11,8 @@ final appRoutes = { CrashesPage.screenName: (BuildContext context) => const CrashesPage(), ComplexPage.screenName: (BuildContext context) => const ComplexPage(), ApmPage.screenName: (BuildContext context) => const ApmPage(), + ScreenLoadingPage.screenName: (BuildContext context) => + const ScreenLoadingPage(), + ScreenCapturePrematureExtensionPage.screenName: (BuildContext context) => + const ScreenCapturePrematureExtensionPage(), }; diff --git a/example/lib/src/components/network_content.dart b/example/lib/src/components/network_content.dart index c98fc186c..77ac744b2 100644 --- a/example/lib/src/components/network_content.dart +++ b/example/lib/src/components/network_content.dart @@ -10,6 +10,8 @@ class NetworkContent extends StatefulWidget { } class _NetworkContentState extends State { + final http = InstabugHttpClient(); + final endpointUrlController = TextEditingController(); @override @@ -24,14 +26,24 @@ class _NetworkContentState extends State { text: 'Send Request To Url', onPressed: () => _sendRequestToUrl(endpointUrlController.text), ), + Text("W3C Header Section"), + InstabugButton( + text: 'Send Request With Custom traceparent header', + onPressed: () => _sendRequestToUrl(endpointUrlController.text, + headers: {"traceparent": "Custom traceparent header"}), + ), + InstabugButton( + text: 'Send Request Without Custom traceparent header', + onPressed: () => _sendRequestToUrl(endpointUrlController.text), + ), ], ); } - void _sendRequestToUrl(String text) async { + void _sendRequestToUrl(String text, {Map? headers}) async { try { - var url = text.trim().isEmpty ? widget.defaultRequestUrl : text; - final response = await http.get(Uri.parse(url)); + String url = text.trim().isEmpty ? widget.defaultRequestUrl : text; + final response = await http.get(Uri.parse(url), headers: headers); // Handle the response here if (response.statusCode == 200) { diff --git a/example/lib/src/screens/apm_page.dart b/example/lib/src/screens/apm_page.dart index 6a00ff4ed..8580e203f 100644 --- a/example/lib/src/screens/apm_page.dart +++ b/example/lib/src/screens/apm_page.dart @@ -10,6 +10,18 @@ class ApmPage extends StatefulWidget { } class _ApmPageState extends State { + void _navigateToScreenLoading() { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const ScreenLoadingPage(), + settings: const RouteSettings( + name: ScreenLoadingPage.screenName, + ), + ), + ); + } + @override Widget build(BuildContext context) { return Page( @@ -25,6 +37,13 @@ class _ApmPageState extends State { SizedBox.fromSize( size: const Size.fromHeight(12), ), + InstabugButton( + text: 'Screen Loading', + onPressed: _navigateToScreenLoading, + ), + SizedBox.fromSize( + size: const Size.fromHeight(12), + ), ], ); } diff --git a/example/lib/src/screens/complex_page.dart b/example/lib/src/screens/complex_page.dart index 3373b25c4..65fdd8a57 100644 --- a/example/lib/src/screens/complex_page.dart +++ b/example/lib/src/screens/complex_page.dart @@ -43,6 +43,26 @@ class _ComplexPageState extends State { }); } + void _resetDidStartScreenLoading() { + ScreenLoadingManager.I.resetDidStartScreenLoading(); + } + + void _resetDidReportScreenLoading() { + ScreenLoadingManager.I.resetDidReportScreenLoading(); + } + + void _resetDidExtendScreenLoading() { + ScreenLoadingManager.I.resetDidExtendScreenLoading(); + } + + void _enableScreenLoading() { + APM.setScreenLoadingEnabled(true); + } + + void _disableScreenLoading() { + APM.setScreenLoadingEnabled(false); + } + @override Widget build(BuildContext context) { final textTheme = Theme.of(context).textTheme; @@ -70,6 +90,26 @@ class _ComplexPageState extends State { 12.0, ), ), + InstabugButton( + onPressed: _enableScreenLoading, + text: 'Enable Screen loading', + ), + InstabugButton( + onPressed: _disableScreenLoading, + text: 'Disable Screen Loading', + ), + InstabugButton( + onPressed: _resetDidStartScreenLoading, + text: 'Reset Did Start Screen Loading', + ), + InstabugButton( + onPressed: _resetDidReportScreenLoading, + text: 'Reset Did Report Screen Loading', + ), + InstabugButton( + onPressed: _resetDidExtendScreenLoading, + text: 'Reset Did Extend Screen Loading', + ), SingleChildScrollView( scrollDirection: Axis.horizontal, child: NestedView( @@ -82,9 +122,12 @@ class _ComplexPageState extends State { if (widget.isMonitored) { return KeyedSubtree( key: _reloadKey, - child: Page( - title: 'Monitored Complex', - children: content, + child: InstabugCaptureScreenLoading( + screenName: ComplexPage.screenName, + child: Page( + title: 'Monitored Complex', + children: content, + ), ), ); } else { diff --git a/example/lib/src/screens/my_home_page.dart b/example/lib/src/screens/my_home_page.dart index 0bc609fca..404d79cdd 100644 --- a/example/lib/src/screens/my_home_page.dart +++ b/example/lib/src/screens/my_home_page.dart @@ -19,6 +19,15 @@ class _MyHomePageState extends State { final primaryColorController = TextEditingController(); final screenNameController = TextEditingController(); + final featureFlagsController = TextEditingController(); + + @override + void dispose() { + featureFlagsController.dispose(); + screenNameController.dispose(); + primaryColorController.dispose(); + super.dispose(); + } void restartInstabug() { Instabug.setEnabled(false); @@ -106,8 +115,8 @@ class _MyHomePageState extends State { } void changePrimaryColor() { - var text = 'FF' + primaryColorController.text.replaceAll('#', ''); - var color = Color(int.parse(text, radix: 16)); + String text = 'FF' + primaryColorController.text.replaceAll('#', ''); + Color color = Color(int.parse(text, radix: 16)); Instabug.setPrimaryColor(color); } @@ -133,7 +142,10 @@ class _MyHomePageState extends State { Navigator.push( context, MaterialPageRoute( - builder: (context) => ApmPage(), + builder: (context) => const InstabugCaptureScreenLoading( + screenName: ApmPage.screenName, + child: ApmPage(), + ), settings: const RouteSettings(name: ApmPage.screenName), ), ); @@ -215,28 +227,6 @@ class _MyHomePageState extends State { ), ], ), - const SectionTitle('Change Report Types'), - ButtonBar( - mainAxisSize: MainAxisSize.min, - alignment: MainAxisAlignment.start, - children: [ - ElevatedButton( - onPressed: () => toggleReportType(ReportType.bug), - style: buttonStyle, - child: const Text('Bug'), - ), - ElevatedButton( - onPressed: () => toggleReportType(ReportType.feedback), - style: buttonStyle, - child: const Text('Feedback'), - ), - ElevatedButton( - onPressed: () => toggleReportType(ReportType.question), - style: buttonStyle, - child: const Text('Question'), - ), - ], - ), InstabugButton( onPressed: show, text: 'Invoke', @@ -262,6 +252,28 @@ class _MyHomePageState extends State { onPressed: showManualSurvey, text: 'Show Manual Survey', ), + const SectionTitle('Change Report Types'), + ButtonBar( + mainAxisSize: MainAxisSize.min, + alignment: MainAxisAlignment.start, + children: [ + ElevatedButton( + onPressed: () => toggleReportType(ReportType.bug), + style: buttonStyle, + child: const Text('Bug'), + ), + ElevatedButton( + onPressed: () => toggleReportType(ReportType.feedback), + style: buttonStyle, + child: const Text('Feedback'), + ), + ElevatedButton( + onPressed: () => toggleReportType(ReportType.question), + style: buttonStyle, + child: const Text('Question'), + ), + ], + ), InstabugButton( onPressed: changeFloatingButtonEdge, text: 'Move Floating Button to Left', @@ -322,7 +334,36 @@ class _MyHomePageState extends State { ), ], ), + SectionTitle('FeatureFlags'), + InstabugTextField( + controller: featureFlagsController, + label: 'Feature Flag name', + ), + InstabugButton( + onPressed: () => setFeatureFlag(), + text: 'SetFeatureFlag', + ), + InstabugButton( + onPressed: () => removeFeatureFlag(), + text: 'RemoveFeatureFlag', + ), + InstabugButton( + onPressed: () => removeAllFeatureFlags(), + text: 'RemoveAllFeatureFlags', + ), ], ); } + + setFeatureFlag() { + Instabug.addFeatureFlags([FeatureFlag(name: featureFlagsController.text)]); + } + + removeFeatureFlag() { + Instabug.removeFeatureFlags([featureFlagsController.text]); + } + + removeAllFeatureFlags() { + Instabug.clearAllFeatureFlags(); + } } diff --git a/example/lib/src/screens/screen_capture_premature_extension_page.dart b/example/lib/src/screens/screen_capture_premature_extension_page.dart new file mode 100644 index 000000000..befbec341 --- /dev/null +++ b/example/lib/src/screens/screen_capture_premature_extension_page.dart @@ -0,0 +1,30 @@ +part of '../../main.dart'; + +class ScreenCapturePrematureExtensionPage extends StatefulWidget { + static const screenName = 'screenCapturePrematureExtension'; + + const ScreenCapturePrematureExtensionPage({Key? key}) : super(key: key); + + @override + State createState() => + _ScreenCapturePrematureExtensionPageState(); +} + +class _ScreenCapturePrematureExtensionPageState + extends State { + void _extendScreenLoading() { + APM.endScreenLoading(); + } + + @override + Widget build(BuildContext context) { + _extendScreenLoading(); + return const Page( + title: 'Screen Capture Premature Extension', + children: [ + Text( + 'This page calls endScreenLoading before it fully renders allowing us to test the scenario of premature extension of screen loading'), + ], + ); + } +} diff --git a/example/lib/src/screens/screen_loading_page.dart b/example/lib/src/screens/screen_loading_page.dart new file mode 100644 index 000000000..a2b49e681 --- /dev/null +++ b/example/lib/src/screens/screen_loading_page.dart @@ -0,0 +1,186 @@ +part of '../../main.dart'; + +class ScreenLoadingPage extends StatefulWidget { + static const screenName = 'screenLoading'; + + const ScreenLoadingPage({Key? key}) : super(key: key); + + @override + State createState() => _ScreenLoadingPageState(); +} + +class _ScreenLoadingPageState extends State { + final durationController = TextEditingController(); + GlobalKey _reloadKey = GlobalKey(); + final List _capturedWidgets = []; + + void _render() { + setState(() { + // Key can be changed to force reload and re-render + _reloadKey = GlobalKey(); + }); + } + + void _addCapturedWidget() { + setState(() { + debugPrint('adding captured widget'); + _capturedWidgets.add(0); + }); + } + + ///This is the production implementation as [APM.endScreenLoading()] is the method which users use from [APM] class + void _extendScreenLoading() async { + APM.endScreenLoading(); + } + + ///This is a testing implementation as [APM.endScreenLoadingCP()] is marked as @internal method, + ///Therefor we check if SCL is enabled before proceeding + ///This check is internally done inside the production method [APM.endScreenLoading()] + void _extendScreenLoadingTestingEnvironment() async { + final isScreenLoadingEnabled = await APM.isScreenLoadingEnabled(); + if (isScreenLoadingEnabled) { + final currentUiTrace = ScreenLoadingManager.I.currentUiTrace; + final currentScreenLoadingTrace = + ScreenLoadingManager.I.currentScreenLoadingTrace; + final extendedEndTime = + (currentScreenLoadingTrace?.endTimeInMicroseconds ?? 0) + + (int.tryParse(durationController.text.toString()) ?? 0); + APM.endScreenLoadingCP( + extendedEndTime, + currentUiTrace?.traceId ?? 0, + ); + } else { + debugPrint( + 'Screen loading monitoring is disabled, skipping ending screen loading monitoring with APM.endScreenLoading().\n' + 'Please refer to the documentation for how to enable screen loading monitoring in your app: ' + 'https://docs.instabug.com/docs/flutter-apm-screen-loading#disablingenabling-screen-loading-tracking' + "If Screen Loading is enabled but you're still seeing this message, please reach out to support.", + ); + } + } + + void _navigateToComplexPage() { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const ComplexPage.monitored(), + settings: const RouteSettings( + name: ComplexPage.screenName, + ), + ), + ); + } + + void _navigateToMonitoredScreenCapturePrematureExtensionPage() { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const InstabugCaptureScreenLoading( + screenName: ScreenCapturePrematureExtensionPage.screenName, + child: ScreenCapturePrematureExtensionPage(), + ), + settings: const RouteSettings( + name: ScreenCapturePrematureExtensionPage.screenName, + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Page( + title: 'Screen Loading', + floatingActionButton: Container( + height: 40, + child: FloatingActionButton( + tooltip: 'Add', + onPressed: _addCapturedWidget, + child: const Icon(Icons.add, color: Colors.white, size: 28), + ), + ), + children: [ + SectionTitle('6x InstabugCaptureScreen'), + KeyedSubtree( + key: _reloadKey, + child: InstabugCaptureScreenLoading( + screenName: ScreenLoadingPage.screenName, + child: InstabugCaptureScreenLoading( + screenName: ScreenLoadingPage.screenName, + child: InstabugCaptureScreenLoading( + screenName: 'different screen name', + child: InstabugCaptureScreenLoading( + screenName: ScreenLoadingPage.screenName, + child: InstabugCaptureScreenLoading( + screenName: ScreenLoadingPage.screenName, + child: InstabugCaptureScreenLoading( + screenName: ScreenLoadingPage.screenName, + child: Container( + margin: const EdgeInsets.only(top: 12), + child: InstabugButton( + text: 'Reload', + onPressed: _render, // Call _render function here + ), + ), + ), + ), + ), + ), + ), + ), + ), + InstabugTextField( + label: 'Duration', + controller: durationController, + keyboardType: TextInputType.number, + ), + Container( + margin: const EdgeInsets.only(top: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + InstabugButton( + text: 'Extend Screen Loading (Testing)', + onPressed: _extendScreenLoadingTestingEnvironment, + ), + InstabugButton( + text: 'Extend Screen Loading (Production)', + onPressed: _extendScreenLoading, + ), + ], + )), + InstabugButton( + text: 'Monitored Complex Page', + onPressed: _navigateToComplexPage, + ), + InstabugButton( + text: 'Screen Capture Premature Extension Page', + onPressed: _navigateToMonitoredScreenCapturePrematureExtensionPage, + ), + SectionTitle('Dynamic Screen Loading list'), + SizedBox( + height: 100, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20.0), + child: GridView.builder( + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 5, childAspectRatio: 5), + reverse: false, + shrinkWrap: true, + itemCount: _capturedWidgets.length, + itemBuilder: (context, index) { + return InstabugCaptureScreenLoading( + screenName: ScreenLoadingPage.screenName, + child: Text(index.toString()), + ); + }, + ), + ), + ), + SizedBox.fromSize( + size: const Size.fromHeight(12), + ), + ], + ); + } +} diff --git a/example/pubspec.lock b/example/pubspec.lock index eafc8636e..febccd736 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -75,6 +75,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: b543301ad291598523947dc534aaddc5aaad597b709d2426d3a0e0d44c5cb493 + url: "https://pub.dev" + source: hosted + version: "1.0.4" flutter_test: dependency: "direct dev" description: flutter @@ -107,31 +115,47 @@ packages: path: ".." relative: true source: path - version: "13.1.1" + version: "13.4.0" + instabug_http_client: + dependency: "direct main" + description: + name: instabug_http_client + sha256: "7d52803c0dd639f6dddbe07333418eb251ae02f3f9f4d30402517533ca692784" + url: "https://pub.dev" + source: hosted + version: "2.4.0" leak_tracker: dependency: transitive description: name: leak_tracker - sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" url: "https://pub.dev" source: hosted - version: "10.0.0" + version: "10.0.5" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.5" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + lints: + dependency: transitive + description: + name: lints + sha256: a2c3d198cb5ea2e179926622d433331d8b58374ab8f29cdda6e863bd62fd369c url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "1.0.1" matcher: dependency: transitive description: @@ -144,18 +168,18 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.15.0" path: dependency: transitive description: @@ -168,10 +192,10 @@ packages: dependency: transitive description: name: platform - sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" url: "https://pub.dev" source: hosted - version: "3.1.4" + version: "3.1.5" process: dependency: transitive description: @@ -237,18 +261,18 @@ packages: dependency: transitive description: name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.7.2" typed_data: dependency: transitive description: name: typed_data - sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.4.0" vector_math: dependency: transitive description: @@ -261,10 +285,10 @@ packages: dependency: transitive description: name: vm_service - sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" url: "https://pub.dev" source: hosted - version: "13.0.0" + version: "14.2.5" webdriver: dependency: transitive description: @@ -274,5 +298,5 @@ packages: source: hosted version: "3.0.3" sdks: - dart: ">=3.2.0-0 <4.0.0" - flutter: ">=2.10.0" + dart: ">=3.5.0 <4.0.0" + flutter: ">=3.18.0-18.0.pre.54" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index f88ed2aff..7f3e9e622 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -18,14 +18,15 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev version: 1.0.0+1 environment: - sdk: ">=2.12.0 <3.0.0" + sdk: ">=2.12.0 <4.0.0" dependencies: flutter: sdk: flutter + http: ^0.13.0 instabug_flutter: path: ../ - http: ^0.13.3 + instabug_http_client: ^2.4.0 dev_dependencies: espresso: 0.2.0+5 @@ -33,6 +34,11 @@ dev_dependencies: sdk: flutter flutter_test: sdk: flutter + flutter_lints: 1.0.4 + +dependency_overrides: + instabug_flutter: + path: ../ # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/ios/Classes/Modules/ApmApi.m b/ios/Classes/Modules/ApmApi.m index da7fa84be..83efce69d 100644 --- a/ios/Classes/Modules/ApmApi.m +++ b/ios/Classes/Modules/ApmApi.m @@ -1,6 +1,8 @@ #import "Instabug.h" #import "ApmApi.h" #import "ArgsRegistry.h" +#import "IBGAPM+PrivateAPIs.h" +#import "IBGTimeIntervalUnits.h" void InitApmApi(id messenger) { ApmApi *api = [[ApmApi alloc] init]; @@ -21,6 +23,25 @@ - (void)setEnabledIsEnabled:(NSNumber *)isEnabled error:(FlutterError *_Nullable IBGAPM.enabled = [isEnabled boolValue]; } +- (void)isEnabledWithCompletion:(nonnull void (^)(NSNumber * _Nullable, FlutterError * _Nullable))completion { + BOOL isEnabled = IBGAPM.enabled; + + NSNumber *isEnabledNumber = @(isEnabled); + + completion(isEnabledNumber, nil); +} + +- (void)setScreenLoadingEnabledIsEnabled:(nonnull NSNumber *)isEnabled error:(FlutterError * _Nullable __autoreleasing * _Nonnull)error { + [IBGAPM setScreenLoadingEnabled:[isEnabled boolValue]]; +} + + +- (void)isScreenLoadingEnabledWithCompletion:(nonnull void (^)(NSNumber * _Nullable, FlutterError * _Nullable))completion { + BOOL isScreenLoadingEnabled = IBGAPM.screenLoadingEnabled; + NSNumber *isEnabledNumber = @(isScreenLoadingEnabled); + completion(isEnabledNumber, nil); +} + - (void)setColdAppLaunchEnabledIsEnabled:(NSNumber *)isEnabled error:(FlutterError *_Nullable *_Nonnull)error { IBGAPM.coldAppLaunchEnabled = [isEnabled boolValue]; } @@ -84,4 +105,30 @@ - (void)networkLogAndroidData:(NSDictionary *)data error:(Flutte // Android Only } + +- (void)startCpUiTraceScreenName:(nonnull NSString *)screenName microTimeStamp:(nonnull NSNumber *)microTimeStamp traceId:(nonnull NSNumber *)traceId error:(FlutterError * _Nullable __autoreleasing * _Nonnull)error { + NSTimeInterval startTimeStampMUS = [microTimeStamp doubleValue]; + [IBGAPM startUITraceCPWithName:screenName startTimestampMUS:startTimeStampMUS]; +} + + + +- (void)reportScreenLoadingCPStartTimeStampMicro:(nonnull NSNumber *)startTimeStampMicro durationMicro:(nonnull NSNumber *)durationMicro uiTraceId:(nonnull NSNumber *)uiTraceId error:(FlutterError * _Nullable __autoreleasing * _Nonnull)error { + NSTimeInterval startTimeStampMicroMUS = [startTimeStampMicro doubleValue]; + NSTimeInterval durationMUS = [durationMicro doubleValue]; + [IBGAPM reportScreenLoadingCPWithStartTimestampMUS:startTimeStampMicroMUS durationMUS:durationMUS]; +} + +- (void)endScreenLoadingCPTimeStampMicro:(nonnull NSNumber *)timeStampMicro uiTraceId:(nonnull NSNumber *)uiTraceId error:(FlutterError * _Nullable __autoreleasing * _Nonnull)error { + NSTimeInterval endScreenLoadingCPWithEndTimestampMUS = [timeStampMicro doubleValue]; + [IBGAPM endScreenLoadingCPWithEndTimestampMUS:endScreenLoadingCPWithEndTimestampMUS]; +} + +- (void)isEndScreenLoadingEnabledWithCompletion:(nonnull void (^)(NSNumber * _Nullable, FlutterError * _Nullable))completion { + BOOL isEndScreenLoadingEnabled = IBGAPM.endScreenLoadingEnabled; + NSNumber *isEnabledNumber = @(isEndScreenLoadingEnabled); + completion(isEnabledNumber, nil); +} + + @end diff --git a/ios/Classes/Modules/InstabugApi.m b/ios/Classes/Modules/InstabugApi.m index 493d1fc19..8cdd336d1 100644 --- a/ios/Classes/Modules/InstabugApi.m +++ b/ios/Classes/Modules/InstabugApi.m @@ -5,6 +5,7 @@ #import "IBGNetworkLogger+CP.h" #import "InstabugApi.h" #import "ArgsRegistry.h" +#import "../Util/IBGAPM+PrivateAPIs.h" #define UIColorFromRGB(rgbValue) [UIColor colorWithRed:((float)((rgbValue & 0xFF0000) >> 16)) / 255.0 green:((float)((rgbValue & 0xFF00) >> 8)) / 255.0 blue:((float)(rgbValue & 0xFF)) / 255.0 alpha:((float)((rgbValue & 0xFF000000) >> 24)) / 255.0]; @@ -19,6 +20,15 @@ - (void)setEnabledIsEnabled:(NSNumber *)isEnabled error:(FlutterError *_Nullable Instabug.enabled = [isEnabled boolValue]; } +- (nullable NSNumber *)isBuiltWithError:(FlutterError * _Nullable __autoreleasing * _Nonnull)error { + return @(YES); +} + + +- (nullable NSNumber *)isEnabledWithError:(FlutterError * _Nullable __autoreleasing * _Nonnull)error { + return @(Instabug.enabled); +} + - (void)initToken:(NSString *)token invocationEvents:(NSArray *)invocationEvents debugLogsLevel:(NSString *)debugLogsLevel error:(FlutterError *_Nullable *_Nonnull)error { SEL setPrivateApiSEL = NSSelectorFromString(@"setCurrentPlatform:"); if ([[Instabug class] respondsToSelector:setPrivateApiSEL]) { @@ -254,10 +264,10 @@ - (void)networkLogData:(NSDictionary *)data error:(FlutterError NSString *method = data[@"method"]; NSString *requestBody = data[@"requestBody"]; NSString *responseBody = data[@"responseBody"]; - int32_t responseCode = [data[@"responseCode"] integerValue]; + int32_t responseCode = (int32_t) [data[@"responseCode"] integerValue]; int64_t requestBodySize = [data[@"requestBodySize"] integerValue]; int64_t responseBodySize = [data[@"responseBodySize"] integerValue]; - int32_t errorCode = [data[@"errorCode"] integerValue]; + int32_t errorCode = (int32_t) [data[@"errorCode"] integerValue]; NSString *errorDomain = data[@"errorDomain"]; NSDictionary *requestHeaders = data[@"requestHeaders"]; if ([requestHeaders count] == 0) { @@ -270,43 +280,117 @@ - (void)networkLogData:(NSDictionary *)data error:(FlutterError NSString *gqlQueryName = nil; NSString *serverErrorMessage = nil; + NSNumber *isW3cHeaderFound = nil; + NSNumber *partialId = nil; + NSNumber *networkStartTimeInSeconds = nil; + NSString *w3CGeneratedHeader = nil; + NSString *w3CCaughtHeader = nil; + if (data[@"gqlQueryName"] != [NSNull null]) { gqlQueryName = data[@"gqlQueryName"]; } if (data[@"serverErrorMessage"] != [NSNull null]) { serverErrorMessage = data[@"serverErrorMessage"]; } + if (data[@"partialId"] != [NSNull null]) { + partialId = data[@"partialId"]; + } - SEL networkLogSEL = NSSelectorFromString(@"addNetworkLogWithUrl:method:requestBody:requestBodySize:responseBody:responseBodySize:responseCode:requestHeaders:responseHeaders:contentType:errorDomain:errorCode:startTime:duration:gqlQueryName:serverErrorMessage:"); - - if ([[IBGNetworkLogger class] respondsToSelector:networkLogSEL]) { - NSInvocation *inv = [NSInvocation invocationWithMethodSignature:[[IBGNetworkLogger class] methodSignatureForSelector:networkLogSEL]]; - [inv setSelector:networkLogSEL]; - [inv setTarget:[IBGNetworkLogger class]]; - - [inv setArgument:&(url) atIndex:2]; - [inv setArgument:&(method) atIndex:3]; - [inv setArgument:&(requestBody) atIndex:4]; - [inv setArgument:&(requestBodySize) atIndex:5]; - [inv setArgument:&(responseBody) atIndex:6]; - [inv setArgument:&(responseBodySize) atIndex:7]; - [inv setArgument:&(responseCode) atIndex:8]; - [inv setArgument:&(requestHeaders) atIndex:9]; - [inv setArgument:&(responseHeaders) atIndex:10]; - [inv setArgument:&(contentType) atIndex:11]; - [inv setArgument:&(errorDomain) atIndex:12]; - [inv setArgument:&(errorCode) atIndex:13]; - [inv setArgument:&(startTime) atIndex:14]; - [inv setArgument:&(duration) atIndex:15]; - [inv setArgument:&(gqlQueryName) atIndex:16]; - [inv setArgument:&(serverErrorMessage) atIndex:17]; + if (data[@"isW3cHeaderFound"] != [NSNull null]) { + isW3cHeaderFound = data[@"isW3cHeaderFound"]; + } - [inv invoke]; + if (data[@"networkStartTimeInSeconds"] != [NSNull null]) { + networkStartTimeInSeconds = data[@"networkStartTimeInSeconds"]; + } + + if (data[@"w3CGeneratedHeader"] != [NSNull null]) { + w3CGeneratedHeader = data[@"w3CGeneratedHeader"]; + } + + if (data[@"w3CCaughtHeader"] != [NSNull null]) { + w3CCaughtHeader = data[@"w3CCaughtHeader"]; } + + + + [IBGNetworkLogger addNetworkLogWithUrl:url + method:method + requestBody:requestBody + requestBodySize:requestBodySize + responseBody:responseBody + responseBodySize:responseBodySize + responseCode:responseCode + requestHeaders:requestHeaders + responseHeaders:responseHeaders + contentType:contentType + errorDomain:errorDomain + errorCode:errorCode + startTime:startTime + duration:duration + gqlQueryName:gqlQueryName + serverErrorMessage:serverErrorMessage + isW3cCaughted:isW3cHeaderFound + partialID:partialId + timestamp:networkStartTimeInSeconds + generatedW3CTraceparent:w3CGeneratedHeader + caughtedW3CTraceparent:w3CCaughtHeader]; } - (void)willRedirectToStoreWithError:(FlutterError * _Nullable __autoreleasing *)error { [Instabug willRedirectToAppStore]; } +- (void)addFeatureFlagsFeatureFlagsMap:(nonnull NSDictionary *)featureFlagsMap error:(FlutterError * _Nullable __autoreleasing * _Nonnull)error { + NSMutableArray *featureFlags = [NSMutableArray array]; + for(id key in featureFlagsMap){ + NSString* variant =((NSString * )[featureFlagsMap objectForKey:key]); + if ([variant length]==0) { + [featureFlags addObject:[[IBGFeatureFlag alloc] initWithName:key]]; + } + else{ + [featureFlags addObject:[[IBGFeatureFlag alloc] initWithName:key variant:variant]]; + + } + } + [Instabug addFeatureFlags:featureFlags]; +} + + +- (void)removeAllFeatureFlagsWithError:(FlutterError * _Nullable __autoreleasing * _Nonnull)error { + [Instabug removeAllFeatureFlags]; + +} + + +- (void)removeFeatureFlagsFeatureFlags:(nonnull NSArray *)featureFlags error:(FlutterError * _Nullable __autoreleasing * _Nonnull)error { + + NSMutableArray *features = [NSMutableArray array]; + for(id item in featureFlags){ + [features addObject:[[IBGFeatureFlag alloc] initWithName:item]]; + } + @try { + [Instabug removeFeatureFlags:features]; + } @catch (NSException *exception) { + NSLog(@"%@", exception); + + } +} +- (void)registerFeatureFlagChangeListenerWithError:(FlutterError * _Nullable __autoreleasing * _Nonnull)error { + // Android only. We still need this method to exist to match the Pigeon-generated protocol. + +} + + +- (nullable NSDictionary *)isW3CFeatureFlagsEnabledWithError:(FlutterError * _Nullable __autoreleasing * _Nonnull)error { + NSDictionary *result= @{ + @"isW3cExternalTraceIDEnabled":[NSNumber numberWithBool:IBGNetworkLogger.w3ExternalTraceIDEnabled] , + @"isW3cExternalGeneratedHeaderEnabled":[NSNumber numberWithBool:IBGNetworkLogger.w3ExternalGeneratedHeaderEnabled] , + @"isW3cCaughtHeaderEnabled":[NSNumber numberWithBool:IBGNetworkLogger.w3CaughtHeaderEnabled] , + + }; + return result; +} + + @end diff --git a/ios/Classes/Util/IBGAPM+PrivateAPIs.h b/ios/Classes/Util/IBGAPM+PrivateAPIs.h new file mode 100644 index 000000000..e61bda308 --- /dev/null +++ b/ios/Classes/Util/IBGAPM+PrivateAPIs.h @@ -0,0 +1,25 @@ +// +// IBGAPM+PrivateAPIs.h +// Instabug +// +// Created by Yousef Hamza on 9/7/20. +// Copyright © 2020 Moataz. All rights reserved. +// + +#import +#import "IBGTimeIntervalUnits.h" + +@interface IBGAPM (PrivateAPIs) + + +/// `endScreenLoadingEnabled` will be only true if APM, screenLoadingFeature.enabled and autoUITracesUserPreference are true +@property (class, atomic, assign) BOOL endScreenLoadingEnabled; + ++ (void)startUITraceCPWithName:(NSString *)name startTimestampMUS:(IBGMicroSecondsTimeInterval)startTimestampMUS; + ++ (void)reportScreenLoadingCPWithStartTimestampMUS:(IBGMicroSecondsTimeInterval)startTimestampMUS + durationMUS:(IBGMicroSecondsTimeInterval)durationMUS; + ++ (void)endScreenLoadingCPWithEndTimestampMUS:(IBGMicroSecondsTimeInterval)endTimestampMUS; + +@end diff --git a/ios/Classes/Util/IBGNetworkLogger+CP.h b/ios/Classes/Util/IBGNetworkLogger+CP.h index ae5d32d66..764524fb2 100644 --- a/ios/Classes/Util/IBGNetworkLogger+CP.h +++ b/ios/Classes/Util/IBGNetworkLogger+CP.h @@ -6,6 +6,28 @@ NS_ASSUME_NONNULL_BEGIN + (void)disableAutomaticCapturingOfNetworkLogs; ++ (void)addNetworkLogWithUrl:(NSString *_Nonnull)url + method:(NSString *_Nonnull)method + requestBody:(NSString *_Nonnull)request + requestBodySize:(int64_t)requestBodySize + responseBody:(NSString *_Nonnull)response + responseBodySize:(int64_t)responseBodySize + responseCode:(int32_t)code + requestHeaders:(NSDictionary *_Nonnull)requestHeaders + responseHeaders:(NSDictionary *_Nonnull)responseHeaders + contentType:(NSString *_Nonnull)contentType + errorDomain:(NSString *_Nullable)errorDomain + errorCode:(int32_t)errorCode + startTime:(int64_t)startTime + duration:(int64_t) duration + gqlQueryName:(NSString * _Nullable)gqlQueryName + serverErrorMessage:(NSString * _Nullable)serverErrorMessage + isW3cCaughted:(NSNumber * _Nullable)isW3cCaughted + partialID:(NSNumber * _Nullable)partialID + timestamp:(NSNumber * _Nullable)timestamp + generatedW3CTraceparent:(NSString * _Nullable)generatedW3CTraceparent + caughtedW3CTraceparent:(NSString * _Nullable)caughtedW3CTraceparent; + @end NS_ASSUME_NONNULL_END diff --git a/ios/Classes/Util/NativeUtils/IBGTimeIntervalUnits.h b/ios/Classes/Util/NativeUtils/IBGTimeIntervalUnits.h new file mode 100644 index 000000000..9a9072a6d --- /dev/null +++ b/ios/Classes/Util/NativeUtils/IBGTimeIntervalUnits.h @@ -0,0 +1,24 @@ +// +// IBGTimeIntervalUnits.h +// InstabugUtilities +// +// Created by Yousef Hamza on 6/4/20. +// Copyright © 2020 Moataz. All rights reserved. +// + +#import + +typedef double IBGMicroSecondsTimeInterval NS_SWIFT_NAME(MicroSecondsTimeInterval); +typedef double IBGMilliSecondsTimeInterval NS_SWIFT_NAME(MilliSecondsTimeInterval); +typedef double IBGMinutesTimeInterval NS_SWIFT_NAME(MinutesTimeInterval); + +/// Convert from milli timestamp to micro timestamp +/// - Parameter timeInterval: micro timestamp +IBGMicroSecondsTimeInterval ibg_microSecondsIntervalFromTimeEpoch(NSTimeInterval timeInterval); +IBGMicroSecondsTimeInterval ibg_microSecondsIntervalFromTimeInterval(NSTimeInterval timeInterval); +IBGMilliSecondsTimeInterval ibg_milliSecondsIntervalFromTimeInterval(NSTimeInterval timeInterval); +IBGMinutesTimeInterval ibg_minutesIntervalFromTimeInterval(NSTimeInterval timeInterval); + +NSTimeInterval ibg_timeIntervalFromMicroSecondsInterval(IBGMicroSecondsTimeInterval timeInterval); +NSTimeInterval ibg_timeIntervalFromMilliSecondsInterval(IBGMilliSecondsTimeInterval timeInterval); +NSTimeInterval ibg_timeIntervalFromMinutesInterval(IBGMinutesTimeInterval timeInterval); diff --git a/ios/instabug_flutter.podspec b/ios/instabug_flutter.podspec index 274c3f6bd..1d362767f 100644 --- a/ios/instabug_flutter.podspec +++ b/ios/instabug_flutter.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'instabug_flutter' - s.version = '13.1.1' + s.version = '13.4.0' s.summary = 'Flutter plugin for integrating the Instabug SDK.' s.author = 'Instabug' s.homepage = 'https://www.instabug.com/platforms/flutter' @@ -17,6 +17,6 @@ Pod::Spec.new do |s| s.pod_target_xcconfig = { 'OTHER_LDFLAGS' => '-framework "Flutter" -framework "Instabug"'} s.dependency 'Flutter' - s.dependency 'Instabug', '13.1.0' + s.dependency 'Instabug', '13.4.2' end diff --git a/lib/instabug_flutter.dart b/lib/instabug_flutter.dart index c54cb53ac..e38545897 100644 --- a/lib/instabug_flutter.dart +++ b/lib/instabug_flutter.dart @@ -1,8 +1,11 @@ // Models export 'src/models/crash_data.dart'; export 'src/models/exception_data.dart'; +export 'src/models/feature_flag.dart'; export 'src/models/network_data.dart'; export 'src/models/trace.dart'; +export 'src/models/w3c_header.dart'; + // Modules export 'src/modules/apm.dart'; export 'src/modules/bug_reporting.dart'; @@ -16,3 +19,6 @@ export 'src/modules/session_replay.dart'; export 'src/modules/surveys.dart'; // Utils export 'src/utils/instabug_navigator_observer.dart'; +export 'src/utils/screen_loading/instabug_capture_screen_loading.dart'; +export 'src/utils/screen_loading/route_matcher.dart'; +export 'src/utils/screen_name_masker.dart' show ScreenNameMaskingCallback; diff --git a/lib/src/models/feature_flag.dart b/lib/src/models/feature_flag.dart new file mode 100644 index 000000000..2cd94fd67 --- /dev/null +++ b/lib/src/models/feature_flag.dart @@ -0,0 +1,9 @@ +class FeatureFlag { + /// the name of feature flag + String name; + + /// The variant of the feature flag. + String? variant; + + FeatureFlag({required this.name, this.variant}); +} diff --git a/lib/src/models/generated_w3c_header.dart b/lib/src/models/generated_w3c_header.dart new file mode 100644 index 000000000..dc1e4c51f --- /dev/null +++ b/lib/src/models/generated_w3c_header.dart @@ -0,0 +1,24 @@ +class GeneratedW3CHeader { + num timestampInSeconds; + int partialId; + String w3cHeader; + + GeneratedW3CHeader({ + required this.timestampInSeconds, + required this.partialId, + required this.w3cHeader, + }); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is GeneratedW3CHeader && + runtimeType == other.runtimeType && + timestampInSeconds == other.timestampInSeconds && + partialId == other.partialId && + w3cHeader == other.w3cHeader; + + @override + int get hashCode => + timestampInSeconds.hashCode ^ partialId.hashCode ^ w3cHeader.hashCode; +} diff --git a/lib/src/models/instabug_route.dart b/lib/src/models/instabug_route.dart new file mode 100644 index 000000000..8c1539426 --- /dev/null +++ b/lib/src/models/instabug_route.dart @@ -0,0 +1,11 @@ +import 'package:flutter/material.dart'; + +class InstabugRoute { + final Route route; + final String name; + + const InstabugRoute({ + required this.route, + required this.name, + }); +} diff --git a/lib/src/models/network_data.dart b/lib/src/models/network_data.dart index 03a26abd2..6589bb109 100644 --- a/lib/src/models/network_data.dart +++ b/lib/src/models/network_data.dart @@ -1,5 +1,7 @@ +import 'package:instabug_flutter/src/models/w3c_header.dart'; + class NetworkData { - const NetworkData({ + NetworkData({ required this.url, required this.method, this.requestBody = '', @@ -16,7 +18,10 @@ class NetworkData { required this.startTime, this.errorCode = 0, this.errorDomain = '', - }); + W3CHeader? w3cHeader, + }) { + _w3cHeader = w3cHeader; + } final String url; final String method; @@ -34,6 +39,50 @@ class NetworkData { final DateTime startTime; final int errorCode; final String errorDomain; + W3CHeader? _w3cHeader; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is NetworkData && + runtimeType == other.runtimeType && + url == other.url && + method == other.method && + requestBody == other.requestBody && + responseBody == other.responseBody && + requestBodySize == other.requestBodySize && + responseBodySize == other.responseBodySize && + status == other.status && + requestHeaders == other.requestHeaders && + responseHeaders == other.responseHeaders && + duration == other.duration && + requestContentType == other.requestContentType && + responseContentType == other.responseContentType && + endTime == other.endTime && + startTime == other.startTime && + errorCode == other.errorCode && + errorDomain == other.errorDomain && + _w3cHeader == other._w3cHeader; + + @override + int get hashCode => + url.hashCode ^ + method.hashCode ^ + requestBody.hashCode ^ + responseBody.hashCode ^ + requestBodySize.hashCode ^ + responseBodySize.hashCode ^ + status.hashCode ^ + requestHeaders.hashCode ^ + responseHeaders.hashCode ^ + duration.hashCode ^ + requestContentType.hashCode ^ + responseContentType.hashCode ^ + endTime.hashCode ^ + startTime.hashCode ^ + errorCode.hashCode ^ + errorDomain.hashCode ^ + _w3cHeader.hashCode; NetworkData copyWith({ String? url, @@ -52,6 +101,7 @@ class NetworkData { DateTime? startTime, int? errorCode, String? errorDomain, + W3CHeader? w3cHeader, }) { return NetworkData( url: url ?? this.url, @@ -70,6 +120,7 @@ class NetworkData { startTime: startTime ?? this.startTime, errorCode: errorCode ?? this.errorCode, errorDomain: errorDomain ?? this.errorDomain, + w3cHeader: w3cHeader ?? _w3cHeader, ); } @@ -92,6 +143,11 @@ class NetworkData { 'responseBodySize': responseBodySize, 'errorDomain': errorDomain, 'errorCode': errorCode, + "isW3cHeaderFound": _w3cHeader?.isW3cHeaderFound, + "partialId": _w3cHeader?.partialId, + "networkStartTimeInSeconds": _w3cHeader?.networkStartTimeInSeconds, + "w3CGeneratedHeader": _w3cHeader?.w3CGeneratedHeader, + "w3CCaughtHeader": _w3cHeader?.w3CCaughtHeader, }; } } diff --git a/lib/src/models/trace_partial_id.dart b/lib/src/models/trace_partial_id.dart new file mode 100644 index 000000000..ac5e59b87 --- /dev/null +++ b/lib/src/models/trace_partial_id.dart @@ -0,0 +1,20 @@ +class TracePartialId { + int numberPartialId; + String hexPartialId; + + TracePartialId({ + required this.numberPartialId, + required this.hexPartialId, + }); + + @override + bool operator ==(Object other) => + identical(this, other) || + (other is TracePartialId && + runtimeType == other.runtimeType && + numberPartialId == other.numberPartialId && + hexPartialId == other.hexPartialId); + + @override + int get hashCode => numberPartialId.hashCode ^ hexPartialId.hashCode; +} diff --git a/lib/src/models/w3c_feature_flags.dart b/lib/src/models/w3c_feature_flags.dart new file mode 100644 index 000000000..4a298e57b --- /dev/null +++ b/lib/src/models/w3c_feature_flags.dart @@ -0,0 +1,11 @@ +class W3cFeatureFlags { + bool isW3cExternalTraceIDEnabled; + bool isW3cExternalGeneratedHeaderEnabled; + bool isW3cCaughtHeaderEnabled; + + W3cFeatureFlags({ + required this.isW3cExternalTraceIDEnabled, + required this.isW3cExternalGeneratedHeaderEnabled, + required this.isW3cCaughtHeaderEnabled, + }); +} diff --git a/lib/src/models/w3c_header.dart b/lib/src/models/w3c_header.dart new file mode 100644 index 000000000..dfc7b67a2 --- /dev/null +++ b/lib/src/models/w3c_header.dart @@ -0,0 +1,34 @@ +class W3CHeader { + final bool? isW3cHeaderFound; + final num? partialId; + final num? networkStartTimeInSeconds; + final String? w3CGeneratedHeader; + final String? w3CCaughtHeader; + + W3CHeader({ + this.isW3cHeaderFound, + this.partialId, + this.networkStartTimeInSeconds, + this.w3CGeneratedHeader, + this.w3CCaughtHeader, + }); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is W3CHeader && + runtimeType == other.runtimeType && + isW3cHeaderFound == other.isW3cHeaderFound && + partialId == other.partialId && + networkStartTimeInSeconds == other.networkStartTimeInSeconds && + w3CGeneratedHeader == other.w3CGeneratedHeader && + w3CCaughtHeader == other.w3CCaughtHeader; + + @override + int get hashCode => + isW3cHeaderFound.hashCode ^ + partialId.hashCode ^ + networkStartTimeInSeconds.hashCode ^ + w3CGeneratedHeader.hashCode ^ + w3CCaughtHeader.hashCode; +} diff --git a/lib/src/modules/apm.dart b/lib/src/modules/apm.dart index 58b27c082..70f7bf6b0 100644 --- a/lib/src/modules/apm.dart +++ b/lib/src/modules/apm.dart @@ -2,15 +2,19 @@ import 'dart:async'; +import 'package:flutter/widgets.dart' show WidgetBuilder; import 'package:instabug_flutter/src/generated/apm.api.g.dart'; import 'package:instabug_flutter/src/models/network_data.dart'; import 'package:instabug_flutter/src/models/trace.dart'; import 'package:instabug_flutter/src/utils/ibg_build_info.dart'; import 'package:instabug_flutter/src/utils/ibg_date_time.dart'; +import 'package:instabug_flutter/src/utils/instabug_logger.dart'; +import 'package:instabug_flutter/src/utils/screen_loading/screen_loading_manager.dart'; import 'package:meta/meta.dart'; class APM { static var _host = ApmHostApi(); + static String tag = 'Instabug - APM'; /// @nodoc @visibleForTesting @@ -25,6 +29,24 @@ class APM { return _host.setEnabled(isEnabled); } + /// @nodoc + @internal + static Future isEnabled() async { + return _host.isEnabled(); + } + + /// Enables or disables the screenLoading Monitoring feature. + /// [boolean] isEnabled + static Future setScreenLoadingEnabled(bool isEnabled) { + return _host.setScreenLoadingEnabled(isEnabled); + } + + /// @nodoc + @internal + static Future isScreenLoadingEnabled() async { + return _host.isScreenLoadingEnabled(); + } + /// Enables or disables cold app launch tracking. /// [boolean] isEnabled static Future setColdAppLaunchEnabled(bool isEnabled) async { @@ -146,4 +168,69 @@ class APM { return _host.networkLogAndroid(data.toJson()); } } + + /// @nodoc + @internal + static Future startCpUiTrace( + String screenName, + int startTimeInMicroseconds, + int traceId, + ) { + InstabugLogger.I.d( + 'Starting Ui trace — traceId: $traceId, screenName: $screenName, microTimeStamp: $startTimeInMicroseconds', + tag: APM.tag, + ); + return _host.startCpUiTrace(screenName, startTimeInMicroseconds, traceId); + } + + /// @nodoc + @internal + static Future reportScreenLoadingCP( + int startTimeInMicroseconds, + int durationInMicroseconds, + int uiTraceId, + ) { + InstabugLogger.I.d( + 'Reporting screen loading trace — traceId: $uiTraceId, startTimeInMicroseconds: $startTimeInMicroseconds, durationInMicroseconds: $durationInMicroseconds', + tag: APM.tag, + ); + return _host.reportScreenLoadingCP( + startTimeInMicroseconds, + durationInMicroseconds, + uiTraceId, + ); + } + + /// @nodoc + @internal + static Future endScreenLoadingCP( + int endTimeInMicroseconds, + int uiTraceId, + ) { + InstabugLogger.I.d( + 'Extending screen loading trace — traceId: $uiTraceId, endTimeInMicroseconds: $endTimeInMicroseconds', + tag: APM.tag, + ); + return _host.endScreenLoadingCP(endTimeInMicroseconds, uiTraceId); + } + + /// Extends the currently active screen loading trace + static Future endScreenLoading() { + return ScreenLoadingManager.I.endScreenLoading(); + } + + /// @nodoc + @internal + static Future isEndScreenLoadingEnabled() async { + return _host.isEndScreenLoadingEnabled(); + } + + /// Wraps the given routes with [InstabugCaptureScreenLoading] widgets. + /// This allows Instabug to automatically capture screen loading times. + static Map wrapRoutes( + Map routes, { + List exclude = const [], + }) { + return ScreenLoadingManager.wrapRoutes(routes, exclude: exclude); + } } diff --git a/lib/src/modules/instabug.dart b/lib/src/modules/instabug.dart index ac6c49698..766067df6 100644 --- a/lib/src/modules/instabug.dart +++ b/lib/src/modules/instabug.dart @@ -1,19 +1,26 @@ // ignore_for_file: avoid_classes_with_only_static_members import 'dart:async'; + // to maintain supported versions prior to Flutter 3.3 // ignore: unnecessary_import import 'dart:typed_data'; + // to maintain supported versions prior to Flutter 3.3 // ignore: unnecessary_import import 'dart:ui'; import 'package:flutter/material.dart'; +// to maintain supported versions prior to Flutter 3.3 +// ignore: unused_import import 'package:flutter/services.dart'; import 'package:instabug_flutter/instabug_flutter.dart'; import 'package:instabug_flutter/src/generated/instabug.api.g.dart'; import 'package:instabug_flutter/src/utils/enum_converter.dart'; +import 'package:instabug_flutter/src/utils/feature_flags_manager.dart'; import 'package:instabug_flutter/src/utils/ibg_build_info.dart'; +import 'package:instabug_flutter/src/utils/instabug_logger.dart'; +import 'package:instabug_flutter/src/utils/screen_name_masker.dart'; import 'package:meta/meta.dart'; enum InvocationEvent { @@ -131,6 +138,8 @@ enum ReproStepsMode { enabled, disabled, enabledWithNoScreenshots } class Instabug { static var _host = InstabugHostApi(); + static const tag = 'Instabug'; + /// @nodoc @visibleForTesting // ignore: use_setters_to_change_properties @@ -146,6 +155,18 @@ class Instabug { Surveys.$setup(); } + /// @nodoc + @internal + static Future isEnabled() async { + return _host.isEnabled(); + } + + /// @nodoc + @internal + static Future isBuilt() async { + return _host.isBuilt(); + } + /// Enables or disables Instabug functionality. /// [boolean] isEnabled static Future setEnabled(bool isEnabled) async { @@ -164,11 +185,21 @@ class Instabug { LogLevel debugLogsLevel = LogLevel.error, }) async { $setup(); - return _host.init( + InstabugLogger.I.logLevel = debugLogsLevel; + await _host.init( token, invocationEvents.mapToString(), debugLogsLevel.toString(), ); + return FeatureFlagsManager().registerW3CFlagsListener(); + } + + /// Sets a [callback] to be called wehenever a screen name is captured to mask + /// sensitive information in the screen name. + static void setScreenNameMaskingCallback( + ScreenNameMaskingCallback? callback, + ) { + ScreenNameMasker.I.setMaskingCallback(callback); } /// Shows the welcome message in a specific mode. @@ -228,20 +259,50 @@ class Instabug { } /// Adds experiments to the next report. + @Deprecated( + 'Please migrate to the new feature flags APIs: Instabug.addFeatureFlags.', + ) static Future addExperiments(List experiments) async { return _host.addExperiments(experiments); } /// Removes certain experiments from the next report. + @Deprecated( + 'Please migrate to the new feature flags APIs: Instabug.removeFeatureFlags.', + ) static Future removeExperiments(List experiments) async { return _host.removeExperiments(experiments); } /// Clears all experiments from the next report. + + @Deprecated( + 'Please migrate to the new feature flags APIs: Instabug.clearAllFeatureFlags.', + ) static Future clearAllExperiments() async { return _host.clearAllExperiments(); } + /// Adds feature flags to the next report. + static Future addFeatureFlags(List featureFlags) async { + final map = {}; + for (final value in featureFlags) { + map[value.name] = value.variant ?? ''; + } + + return _host.addFeatureFlags(map); + } + + /// Removes certain feature flags from the next report. + static Future removeFeatureFlags(List featureFlags) async { + return _host.removeFeatureFlags(featureFlags); + } + + /// Clears all feature flags from the next report. + static Future clearAllFeatureFlags() async { + return _host.removeAllFeatureFlags(); + } + /// Add custom user attribute [value] with a [key] that is going to be sent with each feedback, bug or crash. static Future setUserAttribute(String value, String key) async { return _host.setUserAttribute(value, key); diff --git a/lib/src/modules/network_logger.dart b/lib/src/modules/network_logger.dart index 83a88538c..14e524f87 100644 --- a/lib/src/modules/network_logger.dart +++ b/lib/src/modules/network_logger.dart @@ -1,12 +1,15 @@ // ignore_for_file: avoid_classes_with_only_static_members import 'dart:async'; - -import 'package:flutter/foundation.dart'; import 'package:instabug_flutter/src/generated/instabug.api.g.dart'; import 'package:instabug_flutter/src/models/network_data.dart'; +import 'package:instabug_flutter/src/models/w3c_header.dart'; import 'package:instabug_flutter/src/modules/apm.dart'; +import 'package:instabug_flutter/src/utils/feature_flags_manager.dart'; +import 'package:instabug_flutter/src/utils/iterable_ext.dart'; import 'package:instabug_flutter/src/utils/network_manager.dart'; +import 'package:instabug_flutter/src/utils/w3c_header_utils.dart'; +import 'package:meta/meta.dart'; class NetworkLogger { static var _host = InstabugHostApi(); @@ -17,6 +20,8 @@ class NetworkLogger { // ignore: use_setters_to_change_properties static void $setHostApi(InstabugHostApi host) { _host = host; + // ignore: invalid_use_of_visible_for_testing_member + FeatureFlagsManager().$setHostApi(host); } /// @nodoc @@ -66,13 +71,59 @@ class NetworkLogger { } Future networkLog(NetworkData data) async { - final omit = await _manager.omitLog(data); + final w3Header = await getW3CHeader( + data.requestHeaders, + data.startTime.millisecondsSinceEpoch, + ); + if (w3Header?.isW3cHeaderFound == false && + w3Header?.w3CGeneratedHeader != null) { + data.requestHeaders['traceparent'] = w3Header?.w3CGeneratedHeader; + } + networkLogInternal(data); + } + @internal + Future networkLogInternal(NetworkData data) async { + final omit = await _manager.omitLog(data); if (omit) return; - final obfuscated = await _manager.obfuscateLog(data); - await _host.networkLog(obfuscated.toJson()); await APM.networkLogAndroid(obfuscated); } + + @internal + Future getW3CHeader( + Map header, + int startTime, + ) async { + final w3cFlags = await FeatureFlagsManager().getW3CFeatureFlagsHeader(); + + if (w3cFlags.isW3cExternalTraceIDEnabled == false) { + return null; + } + + final w3cHeaderFound = header.entries + .firstWhereOrNull( + (element) => element.key.toLowerCase() == 'traceparent', + ) + ?.value as String?; + final isW3cHeaderFound = w3cHeaderFound != null; + + if (isW3cHeaderFound && w3cFlags.isW3cCaughtHeaderEnabled) { + return W3CHeader(isW3cHeaderFound: true, w3CCaughtHeader: w3cHeaderFound); + } else if (w3cFlags.isW3cExternalGeneratedHeaderEnabled && + !isW3cHeaderFound) { + final w3cHeaderData = W3CHeaderUtils().generateW3CHeader( + startTime, + ); + + return W3CHeader( + isW3cHeaderFound: false, + partialId: w3cHeaderData.partialId, + networkStartTimeInSeconds: w3cHeaderData.timestampInSeconds, + w3CGeneratedHeader: w3cHeaderData.w3cHeader, + ); + } + return null; + } } diff --git a/lib/src/utils/feature_flags_manager.dart b/lib/src/utils/feature_flags_manager.dart new file mode 100644 index 000000000..b81dc9777 --- /dev/null +++ b/lib/src/utils/feature_flags_manager.dart @@ -0,0 +1,92 @@ +import 'package:instabug_flutter/src/generated/instabug.api.g.dart'; +import 'package:instabug_flutter/src/models/w3c_feature_flags.dart'; +import 'package:instabug_flutter/src/utils/ibg_build_info.dart'; +import 'package:meta/meta.dart'; + +typedef OnW3CFeatureFlagChange = void Function( + bool isW3cExternalTraceIDEnabled, + bool isW3cExternalGeneratedHeaderEnabled, + bool isW3cCaughtHeaderEnabled, +); + +class FeatureFlagsManager implements FeatureFlagsFlutterApi { + // Access the singleton instance + factory FeatureFlagsManager() { + return _instance; + } + // Private constructor to prevent instantiation from outside the class + FeatureFlagsManager._(); + + // Singleton instance + static final FeatureFlagsManager _instance = FeatureFlagsManager._(); + + // Host API instance + static InstabugHostApi _host = InstabugHostApi(); + + /// @nodoc + @visibleForTesting + // Setter for the host API + // ignore: use_setters_to_change_properties + void $setHostApi(InstabugHostApi host) { + _host = host; + } + + @visibleForTesting + // Setter for the FeatureFlagsManager + void setFeatureFlagsManager(FeatureFlagsManager featureFlagsManager) { + // This can be used for testing, but should be avoided in production + // since it breaks the singleton pattern + } + + // Internal state flags + bool _isAndroidW3CExternalTraceID = false; + bool _isAndroidW3CExternalGeneratedHeader = false; + bool _isAndroidW3CCaughtHeader = false; + + Future getW3CFeatureFlagsHeader() async { + if (IBGBuildInfo.instance.isAndroid) { + return Future.value( + W3cFeatureFlags( + isW3cCaughtHeaderEnabled: _isAndroidW3CCaughtHeader, + isW3cExternalGeneratedHeaderEnabled: + _isAndroidW3CExternalGeneratedHeader, + isW3cExternalTraceIDEnabled: _isAndroidW3CExternalTraceID, + ), + ); + } + final flags = await _host.isW3CFeatureFlagsEnabled(); + return W3cFeatureFlags( + isW3cCaughtHeaderEnabled: flags['isW3cCaughtHeaderEnabled'] ?? false, + isW3cExternalGeneratedHeaderEnabled: + flags['isW3cExternalGeneratedHeaderEnabled'] ?? false, + isW3cExternalTraceIDEnabled: + flags['isW3cExternalTraceIDEnabled'] ?? false, + ); + } + + Future registerW3CFlagsListener() async { + FeatureFlagsFlutterApi.setup(this); // Use 'this' instead of _instance + + final featureFlags = await _host.isW3CFeatureFlagsEnabled(); + _isAndroidW3CCaughtHeader = + featureFlags['isW3cCaughtHeaderEnabled'] ?? false; + _isAndroidW3CExternalTraceID = + featureFlags['isW3cExternalTraceIDEnabled'] ?? false; + _isAndroidW3CExternalGeneratedHeader = + featureFlags['isW3cExternalGeneratedHeaderEnabled'] ?? false; + + return _host.registerFeatureFlagChangeListener(); + } + + @override + @internal + void onW3CFeatureFlagChange( + bool isW3cExternalTraceIDEnabled, + bool isW3cExternalGeneratedHeaderEnabled, + bool isW3cCaughtHeaderEnabled, + ) { + _isAndroidW3CCaughtHeader = isW3cCaughtHeaderEnabled; + _isAndroidW3CExternalTraceID = isW3cExternalTraceIDEnabled; + _isAndroidW3CExternalGeneratedHeader = isW3cExternalGeneratedHeaderEnabled; + } +} diff --git a/lib/src/utils/instabug_logger.dart b/lib/src/utils/instabug_logger.dart new file mode 100644 index 000000000..033dbf3a6 --- /dev/null +++ b/lib/src/utils/instabug_logger.dart @@ -0,0 +1,93 @@ +import 'dart:developer' as developer; + +import 'package:flutter/foundation.dart'; +import 'package:instabug_flutter/instabug_flutter.dart'; +import 'package:instabug_flutter/src/utils/ibg_date_time.dart'; + +abstract class Logger { + void log( + String message, { + required LogLevel level, + required String tag, + }); +} + +class InstabugLogger implements Logger { + InstabugLogger._(); + + static InstabugLogger _instance = InstabugLogger._(); + + static InstabugLogger get instance => _instance; + + /// Shorthand for [instance] + static InstabugLogger get I => instance; + + @visibleForTesting + // ignore: use_setters_to_change_properties + static void setInstance(InstabugLogger instance) { + _instance = instance; + } + + LogLevel _logLevel = LogLevel.error; + + // ignore: avoid_setters_without_getters + set logLevel(LogLevel level) { + _logLevel = level; + } + + @override + void log( + String message, { + required LogLevel level, + String tag = '', + }) { + if (level.getValue() >= _logLevel.getValue()) { + developer.log( + message, + name: tag, + time: IBGDateTime.I.now(), + level: level.getValue(), + ); + } + } + + void e( + String message, { + String tag = '', + }) { + log(message, tag: tag, level: LogLevel.error); + } + + void d( + String message, { + String tag = '', + }) { + log(message, tag: tag, level: LogLevel.debug); + } + + void v( + String message, { + String tag = '', + }) { + log(message, tag: tag, level: LogLevel.verbose); + } +} + +extension LogLevelExtension on LogLevel { + /// Returns the severity level to be used in the `developer.log` function. + /// + /// The severity level is a value between 0 and 2000. + /// The values used here are based on the `package:logging` `Level` class. + int getValue() { + switch (this) { + case LogLevel.none: + return 2000; + case LogLevel.error: + return 1000; + case LogLevel.debug: + return 500; + case LogLevel.verbose: + return 0; + } + } +} diff --git a/lib/src/utils/instabug_montonic_clock.dart b/lib/src/utils/instabug_montonic_clock.dart new file mode 100644 index 000000000..9474a8224 --- /dev/null +++ b/lib/src/utils/instabug_montonic_clock.dart @@ -0,0 +1,22 @@ +import 'dart:developer'; + +import 'package:meta/meta.dart'; + +/// Mockable, monotonic, high-resolution clock. +class InstabugMonotonicClock { + InstabugMonotonicClock._(); + + static InstabugMonotonicClock _instance = InstabugMonotonicClock._(); + static InstabugMonotonicClock get instance => _instance; + + /// Shorthand for [instance] + static InstabugMonotonicClock get I => instance; + + @visibleForTesting + // ignore: use_setters_to_change_properties + static void setInstance(InstabugMonotonicClock instance) { + _instance = instance; + } + + int get now => Timeline.now; +} diff --git a/lib/src/utils/instabug_navigator_observer.dart b/lib/src/utils/instabug_navigator_observer.dart index 7f3cf45e7..d9d6b02db 100644 --- a/lib/src/utils/instabug_navigator_observer.dart +++ b/lib/src/utils/instabug_navigator_observer.dart @@ -1,30 +1,49 @@ import 'package:flutter/material.dart'; +import 'package:instabug_flutter/instabug_flutter.dart'; +import 'package:instabug_flutter/src/models/instabug_route.dart'; import 'package:instabug_flutter/src/modules/instabug.dart'; +import 'package:instabug_flutter/src/utils/instabug_logger.dart'; +import 'package:instabug_flutter/src/utils/repro_steps_constants.dart'; +import 'package:instabug_flutter/src/utils/screen_loading/screen_loading_manager.dart'; +import 'package:instabug_flutter/src/utils/screen_name_masker.dart'; class InstabugNavigatorObserver extends NavigatorObserver { - final List _steps = []; + final List _steps = []; void screenChanged(Route newRoute) { try { + final rawScreenName = newRoute.settings.name.toString().trim(); + final screenName = rawScreenName.isEmpty + ? ReproStepsConstants.emptyScreenFallback + : rawScreenName; + final maskedScreenName = ScreenNameMasker.I.mask(screenName); + + final route = InstabugRoute( + route: newRoute, + name: maskedScreenName, + ); + + // Starts a the new UI trace which is exclusive to screen loading + ScreenLoadingManager.I.startUiTrace(maskedScreenName, screenName); // If there is a step that hasn't been pushed yet if (_steps.isNotEmpty) { // Report the last step and remove it from the list - Instabug.reportScreenChange( - _steps[_steps.length - 1].settings.name.toString(), - ); - _steps.remove(_steps[_steps.length - 1]); + Instabug.reportScreenChange(_steps.last.name); + _steps.removeLast(); } + // Add the new step to the list - _steps.add(newRoute); + _steps.add(route); Future.delayed(const Duration(milliseconds: 1000), () { // If this route is in the array, report it and remove it from the list - if (_steps.contains(newRoute)) { - Instabug.reportScreenChange(newRoute.settings.name.toString()); - _steps.remove(newRoute); + if (_steps.contains(route)) { + Instabug.reportScreenChange(route.name); + _steps.remove(route); } }); } catch (e) { - debugPrint('[INSTABUG] - Reporting screen failed'); + InstabugLogger.I.e('Reporting screen change failed:', tag: Instabug.tag); + InstabugLogger.I.e(e.toString(), tag: Instabug.tag); } } diff --git a/lib/src/utils/iterable_ext.dart b/lib/src/utils/iterable_ext.dart new file mode 100644 index 000000000..e5c0099f5 --- /dev/null +++ b/lib/src/utils/iterable_ext.dart @@ -0,0 +1,8 @@ +extension IterableExtenstions on Iterable { + T? firstWhereOrNull(bool Function(T element) where) { + for (final element in this) { + if (where(element)) return element; + } + return null; + } +} diff --git a/lib/src/utils/repro_steps_constants.dart b/lib/src/utils/repro_steps_constants.dart new file mode 100644 index 000000000..c26dc205c --- /dev/null +++ b/lib/src/utils/repro_steps_constants.dart @@ -0,0 +1,3 @@ +class ReproStepsConstants { + static const emptyScreenFallback = 'N/A'; +} diff --git a/lib/src/utils/screen_loading/flags_config.dart b/lib/src/utils/screen_loading/flags_config.dart new file mode 100644 index 000000000..f18eb1ccb --- /dev/null +++ b/lib/src/utils/screen_loading/flags_config.dart @@ -0,0 +1,23 @@ +import 'package:instabug_flutter/instabug_flutter.dart'; + +enum FlagsConfig { + apm, + uiTrace, + screenLoading, + endScreenLoading, +} + +extension FeatureExtensions on FlagsConfig { + Future isEnabled() async { + switch (this) { + case FlagsConfig.apm: + return APM.isEnabled(); + case FlagsConfig.screenLoading: + return APM.isScreenLoadingEnabled(); + case FlagsConfig.endScreenLoading: + return APM.isEndScreenLoadingEnabled(); + default: + return false; + } + } +} diff --git a/lib/src/utils/screen_loading/instabug_capture_screen_loading.dart b/lib/src/utils/screen_loading/instabug_capture_screen_loading.dart new file mode 100644 index 000000000..3d400ae3d --- /dev/null +++ b/lib/src/utils/screen_loading/instabug_capture_screen_loading.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:instabug_flutter/src/utils/ibg_date_time.dart'; +import 'package:instabug_flutter/src/utils/instabug_montonic_clock.dart'; +import 'package:instabug_flutter/src/utils/screen_loading/screen_loading_manager.dart'; +import 'package:instabug_flutter/src/utils/screen_loading/screen_loading_trace.dart'; + +class InstabugCaptureScreenLoading extends StatefulWidget { + static const tag = "InstabugCaptureScreenLoading"; + + const InstabugCaptureScreenLoading({ + Key? key, + required this.screenName, + required this.child, + }) : super(key: key); + final Widget child; + final String screenName; + + @override + State createState() => + _InstabugCaptureScreenLoadingState(); +} + +class _InstabugCaptureScreenLoadingState + extends State { + ScreenLoadingTrace? trace; + final startTimeInMicroseconds = IBGDateTime.I.now().microsecondsSinceEpoch; + final startMonotonicTimeInMicroseconds = InstabugMonotonicClock.I.now; + final stopwatch = Stopwatch()..start(); + + @override + void initState() { + super.initState(); + trace = ScreenLoadingTrace( + ScreenLoadingManager.I.sanitizeScreenName(widget.screenName), + startTimeInMicroseconds: startTimeInMicroseconds, + startMonotonicTimeInMicroseconds: startMonotonicTimeInMicroseconds, + ); + + ScreenLoadingManager.I.startScreenLoadingTrace(trace!); + + // to maintain supported versions prior to Flutter 3.0.0 + // ignore: invalid_null_aware_operator + WidgetsBinding.instance?.addPostFrameCallback((_) { + stopwatch.stop(); + final duration = stopwatch.elapsedMicroseconds; + trace?.duration = duration; + trace?.endTimeInMicroseconds = startTimeInMicroseconds + duration; + ScreenLoadingManager.I.reportScreenLoading(trace); + }); + } + + @override + Widget build(BuildContext context) { + return widget.child; + } +} diff --git a/lib/src/utils/screen_loading/route_matcher.dart b/lib/src/utils/screen_loading/route_matcher.dart new file mode 100644 index 000000000..3388e20e7 --- /dev/null +++ b/lib/src/utils/screen_loading/route_matcher.dart @@ -0,0 +1,92 @@ +import 'package:meta/meta.dart'; + +class RouteMatcher { + RouteMatcher._(); + + static RouteMatcher _instance = RouteMatcher._(); + + static RouteMatcher get instance => _instance; + + /// Shorthand for [instance] + static RouteMatcher get I => instance; + + @visibleForTesting + // ignore: use_setters_to_change_properties + static void setInstance(RouteMatcher instance) { + _instance = instance; + } + + /// Checks whether the given [routePath] definition matches the given [actualPath]. + /// + /// The [routePath] definition can contain parameters in the form of `:param`, + /// or `**` for a wildcard parameter. + /// + /// Returns `true` if the [actualPath] matches the [routePath], otherwise `false`. + /// + /// Example: + /// ```dart + /// RouteMatcher.I.match('/users', '/users'); // true + /// RouteMatcher.I.match('/user/:id', '/user/123'); // true + /// RouteMatcher.I.match('/user/**', '/user/123/profile'); // false + /// ``` + bool match({ + required String? routePath, + required String? actualPath, + }) { + // null paths are considered equal. + if (routePath == null || actualPath == null) { + return routePath == actualPath; + } + + final routePathSegments = _segmentPath(routePath); + final actualPathSegments = _segmentPath(actualPath); + + final hasWildcard = routePathSegments.contains('**'); + + if (routePathSegments.length != actualPathSegments.length && !hasWildcard) { + return false; + } + + for (var i = 0; i < routePathSegments.length; i++) { + final routeSegment = routePathSegments[i]; + + final isWildcard = routeSegment == '**'; + final isParameter = routeSegment.startsWith(':'); + + final noMoreActualSegments = i >= actualPathSegments.length; + + if (noMoreActualSegments) { + // Only wilcard segments match empty segments + return isWildcard; + } + + final pathSegment = actualPathSegments[i]; + + // If the route segment is a parameter, then segments automatically match. + if (isParameter) { + continue; + } + + // A wildcard matches any path, the assumption is that wildcard paths only + // appear at the end of the route so we return a match if we reach this point. + if (isWildcard) { + return true; + } + + if (routeSegment != pathSegment) { + return false; + } + } + + return true; + } + + List _segmentPath(String path) { + final pathWithoutQuery = path.split('?').first; + + return pathWithoutQuery + .split('/') + .where((segment) => segment.isNotEmpty) + .toList(); + } +} diff --git a/lib/src/utils/screen_loading/screen_loading_manager.dart b/lib/src/utils/screen_loading/screen_loading_manager.dart new file mode 100644 index 000000000..816ffebef --- /dev/null +++ b/lib/src/utils/screen_loading/screen_loading_manager.dart @@ -0,0 +1,449 @@ +import 'package:flutter/widgets.dart' show WidgetBuilder, BuildContext; +import 'package:instabug_flutter/instabug_flutter.dart'; +import 'package:instabug_flutter/src/utils/ibg_build_info.dart'; +import 'package:instabug_flutter/src/utils/ibg_date_time.dart'; +import 'package:instabug_flutter/src/utils/instabug_logger.dart'; +import 'package:instabug_flutter/src/utils/instabug_montonic_clock.dart'; +import 'package:instabug_flutter/src/utils/screen_loading/flags_config.dart'; +import 'package:instabug_flutter/src/utils/screen_loading/screen_loading_trace.dart'; +import 'package:instabug_flutter/src/utils/screen_loading/ui_trace.dart'; +import 'package:meta/meta.dart'; + +/// @nodoc +@internal +class ScreenLoadingManager { + ScreenLoadingManager._(); + + /// @nodoc + @internal + @visibleForTesting + ScreenLoadingManager.init(); + + static ScreenLoadingManager _instance = ScreenLoadingManager._(); + + static ScreenLoadingManager get instance => _instance; + + /// Shorthand for [instance] + static ScreenLoadingManager get I => instance; + static const tag = "ScreenLoadingManager"; + UiTrace? currentUiTrace; + ScreenLoadingTrace? currentScreenLoadingTrace; + + /// @nodoc + @internal + final List prematurelyEndedTraces = []; + + @visibleForTesting + // ignore: use_setters_to_change_properties + static void setInstance(ScreenLoadingManager instance) { + _instance = instance; + } + + /// @nodoc + @internal + void resetDidStartScreenLoading() { + // Allows starting a new screen loading capture trace in the same ui trace (without navigating out and in to the same screen) + currentUiTrace?.didStartScreenLoading = false; + InstabugLogger.I.d( + 'Resetting didStartScreenLoading — setting didStartScreenLoading: ${currentUiTrace?.didStartScreenLoading}', + tag: APM.tag, + ); + } + + /// @nodoc + void _logExceptionErrorAndStackTrace(Object error, StackTrace stackTrace) { + InstabugLogger.I.e( + '[Error]:$error \n' + '[StackTrace]: $stackTrace', + tag: APM.tag, + ); + } + + /// @nodoc + Future _checkInstabugSDKBuilt(String apiName) async { + // Check if Instabug SDK is Built + final isInstabugSDKBuilt = await Instabug.isBuilt(); + if (!isInstabugSDKBuilt) { + InstabugLogger.I.e( + 'Instabug API {$apiName} was called before the SDK is built. To build it, first by following the instructions at this link:\n' + 'https://docs.instabug.com/reference#showing-and-manipulating-the-invocation', + tag: APM.tag, + ); + } + return isInstabugSDKBuilt; + } + + /// @nodoc + @internal + void resetDidReportScreenLoading() { + // Allows reporting a new screen loading capture trace in the same ui trace even if one was reported before by resetting the flag which is used for checking. + currentUiTrace?.didReportScreenLoading = false; + InstabugLogger.I.d( + 'Resetting didExtendScreenLoading — setting didExtendScreenLoading: ${currentUiTrace?.didExtendScreenLoading}', + tag: APM.tag, + ); + } + + /// @nodoc + @internal + void resetDidExtendScreenLoading() { + // Allows reporting a new screen loading capture trace in the same ui trace even if one was reported before by resetting the flag which is used for checking. + currentUiTrace?.didExtendScreenLoading = false; + InstabugLogger.I.d( + 'Resetting didReportScreenLoading — setting didReportScreenLoading: ${currentUiTrace?.didReportScreenLoading}', + tag: APM.tag, + ); + } + + /// The function `sanitizeScreenName` removes leading and trailing slashes from a screen name in Dart. + /// + /// Args: + /// screenName (String): The `sanitizeScreenName` function is designed to remove a specific character + /// ('/') from the beginning and end of a given `screenName` string. If the `screenName` is equal to + /// '/', it will return 'ROOT_PAGE'. Otherwise, it will remove the character from the beginning and end + /// if + /// + /// Returns: + /// The `sanitizeScreenName` function returns the sanitized screen name after removing any leading or + /// trailing '/' characters. If the input `screenName` is equal to '/', it returns 'ROOT_PAGE'. + + @internal + String sanitizeScreenName(String screenName) { + const characterToBeRemoved = '/'; + var sanitizedScreenName = screenName; + + if (screenName == characterToBeRemoved) { + return 'ROOT_PAGE'; + } + if (screenName.startsWith(characterToBeRemoved)) { + sanitizedScreenName = sanitizedScreenName.substring(1); + } + if (screenName.endsWith(characterToBeRemoved)) { + sanitizedScreenName = + sanitizedScreenName.substring(0, sanitizedScreenName.length - 1); + } + return sanitizedScreenName; + } + + /// Starts a new UI trace with [screenName] as the public screen name and + /// [matchingScreenName] as the screen name used for matching the UI trace + /// with a Screen Loading trace. + @internal + Future startUiTrace( + String screenName, [ + String? matchingScreenName, + ]) async { + matchingScreenName ??= screenName; + + try { + resetDidStartScreenLoading(); + + final isSDKBuilt = + await _checkInstabugSDKBuilt("APM.InstabugCaptureScreenLoading"); + if (!isSDKBuilt) return; + + // TODO: On Android, FlagsConfig.apm.isEnabled isn't implemented correctly + // so we skip the isApmEnabled check on Android and only check on iOS. + // This is a temporary fix until we implement the isEnabled check correctly. + // We need to fix this in the future. + final isApmEnabled = await FlagsConfig.apm.isEnabled(); + if (!isApmEnabled && IBGBuildInfo.I.isIOS) { + InstabugLogger.I.e( + 'APM is disabled, skipping starting the UI trace for screen: $screenName.\n' + 'Please refer to the documentation for how to enable APM on your app: ' + 'https://docs.instabug.com/docs/react-native-apm-disabling-enabling', + tag: APM.tag, + ); + return; + } + + final sanitizedScreenName = sanitizeScreenName(screenName); + final sanitizedMatchingScreenName = + sanitizeScreenName(matchingScreenName); + + final microTimeStamp = IBGDateTime.I.now().microsecondsSinceEpoch; + final uiTraceId = IBGDateTime.I.now().millisecondsSinceEpoch; + + APM.startCpUiTrace(sanitizedScreenName, microTimeStamp, uiTraceId); + + currentUiTrace = UiTrace( + screenName: sanitizedScreenName, + matchingScreenName: sanitizedMatchingScreenName, + traceId: uiTraceId, + ); + } catch (error, stackTrace) { + _logExceptionErrorAndStackTrace(error, stackTrace); + } + } + + /// @nodoc + @internal + Future startScreenLoadingTrace(ScreenLoadingTrace trace) async { + try { + final isSDKBuilt = + await _checkInstabugSDKBuilt("APM.InstabugCaptureScreenLoading"); + if (!isSDKBuilt) return; + + final isScreenLoadingEnabled = + await FlagsConfig.screenLoading.isEnabled(); + if (!isScreenLoadingEnabled) { + if (IBGBuildInfo.I.isIOS) { + InstabugLogger.I.e( + 'Screen loading monitoring is disabled, skipping starting screen loading monitoring for screen: ${trace.screenName}.\n' + 'Please refer to the documentation for how to enable screen loading monitoring on your app: ' + 'https://docs.instabug.com/docs/flutter-apm-screen-loading#disablingenabling-screen-loading-tracking ' + "If Screen Loading is enabled but you're still seeing this message, please reach out to support.", + tag: APM.tag, + ); + } + return; + } + + final isSameScreen = currentUiTrace?.matches(trace.screenName) == true; + + final didStartLoading = currentUiTrace?.didStartScreenLoading == true; + + if (isSameScreen && !didStartLoading) { + InstabugLogger.I.d( + 'starting screen loading trace — screenName: ${trace.screenName}, startTimeInMicroseconds: ${trace.startTimeInMicroseconds}', + tag: APM.tag, + ); + currentUiTrace?.didStartScreenLoading = true; + currentScreenLoadingTrace = trace; + return; + } + InstabugLogger.I.d( + 'failed to start screen loading trace — screenName: ${trace.screenName}, startTimeInMicroseconds: ${trace.startTimeInMicroseconds}', + tag: APM.tag, + ); + InstabugLogger.I.d( + 'didStartScreenLoading: $didStartLoading, isSameScreen: $isSameScreen', + tag: APM.tag, + ); + } catch (error, stackTrace) { + _logExceptionErrorAndStackTrace(error, stackTrace); + } + } + + /// @nodoc + @internal + Future reportScreenLoading(ScreenLoadingTrace? trace) async { + try { + final isSDKBuilt = + await _checkInstabugSDKBuilt("APM.InstabugCaptureScreenLoading"); + if (!isSDKBuilt) return; + + int? duration; + final isScreenLoadingEnabled = + await FlagsConfig.screenLoading.isEnabled(); + if (!isScreenLoadingEnabled) { + if (IBGBuildInfo.I.isIOS) { + InstabugLogger.I.e( + 'Screen loading monitoring is disabled, skipping reporting screen loading time for screen: ${trace?.screenName}.\n' + 'Please refer to the documentation for how to enable screen loading monitoring on your app: ' + 'https://docs.instabug.com/docs/flutter-apm-screen-loading#disablingenabling-screen-loading-tracking ' + "If Screen Loading is enabled but you're still seeing this message, please reach out to support.", + tag: APM.tag, + ); + } + return; + } + + final isSameScreen = currentScreenLoadingTrace == trace; + + final isReported = currentUiTrace?.didReportScreenLoading == + true; // Changed to isReported + final isValidTrace = trace != null; + + // Only report the first screen loading trace with the same name as the active UiTrace + if (isSameScreen && !isReported && isValidTrace) { + currentUiTrace?.didReportScreenLoading = true; + + APM.reportScreenLoadingCP( + trace?.startTimeInMicroseconds ?? 0, + duration ?? trace?.duration ?? 0, + currentUiTrace?.traceId ?? 0, + ); + return; + } else { + InstabugLogger.I.d( + 'Failed to report screen loading trace — screenName: ${trace?.screenName}, ' + 'startTimeInMicroseconds: ${trace?.startTimeInMicroseconds}, ' + 'duration: $duration, ' + 'trace.duration: ${trace?.duration ?? 0}', + tag: APM.tag, + ); + InstabugLogger.I.d( + 'didReportScreenLoading: $isReported, ' + 'isSameName: $isSameScreen', + tag: APM.tag, + ); + _reportScreenLoadingDroppedError(trace); + } + return; + } catch (error, stackTrace) { + _logExceptionErrorAndStackTrace(error, stackTrace); + } + } + + void _reportScreenLoadingDroppedError(ScreenLoadingTrace? trace) { + InstabugLogger.I.e( + "Screen Loading trace dropped as the trace isn't from the current screen, or another trace was reported before the current one. — $trace", + tag: APM.tag, + ); + } + + /// Extends the already ended screen loading adding a stage to it + Future endScreenLoading() async { + try { + final isSDKBuilt = await _checkInstabugSDKBuilt("endScreenLoading"); + if (!isSDKBuilt) return; + + final isScreenLoadingEnabled = + await FlagsConfig.screenLoading.isEnabled(); + + if (!isScreenLoadingEnabled) { + if (IBGBuildInfo.I.isIOS) { + InstabugLogger.I.e( + 'Screen loading monitoring is disabled, skipping ending screen loading monitoring with APM.endScreenLoading().\n' + 'Please refer to the documentation for how to enable screen loading monitoring in your app: ' + 'https://docs.instabug.com/docs/flutter-apm-screen-loading#disablingenabling-screen-loading-tracking ' + "If Screen Loading is enabled but you're still seeing this message, please reach out to support.", + tag: APM.tag, + ); + } + return; + } + + final isEndScreenLoadingEnabled = + await FlagsConfig.endScreenLoading.isEnabled(); + + if (!isEndScreenLoadingEnabled) { + if (IBGBuildInfo.I.isIOS) { + InstabugLogger.I.e( + 'End Screen loading API is disabled.\n' + 'Please refer to the documentation for how to enable screen loading monitoring in your app: ' + 'https://docs.instabug.com/docs/flutter-apm-screen-loading#disablingenabling-screen-loading-tracking ' + "If Screen Loading is enabled but you're still seeing this message, please reach out to support.", + tag: APM.tag, + ); + } + return; + } + + final didExtendScreenLoading = + currentUiTrace?.didExtendScreenLoading == true; + if (didExtendScreenLoading) { + InstabugLogger.I.e( + 'endScreenLoading has already been called for the current screen visit. Multiple calls to this API are not allowed during a single screen visit, only the first call will be considered.', + tag: APM.tag, + ); + return; + } + + // Handles no active screen loading trace - cannot end + final didStartScreenLoading = + currentScreenLoadingTrace?.startTimeInMicroseconds != null; + if (!didStartScreenLoading) { + InstabugLogger.I.e( + "endScreenLoading wasn’t called as there is no active screen Loading trace.", + tag: APM.tag, + ); + return; + } + + final extendedMonotonicEndTimeInMicroseconds = + InstabugMonotonicClock.I.now; + + var duration = extendedMonotonicEndTimeInMicroseconds - + currentScreenLoadingTrace!.startMonotonicTimeInMicroseconds; + + var extendedEndTimeInMicroseconds = + currentScreenLoadingTrace!.startTimeInMicroseconds + duration; + + // cannot extend as the trace has not ended yet. + // we report the extension timestamp as 0 and can be override later on. + final didEndScreenLoadingPrematurely = + currentScreenLoadingTrace?.endTimeInMicroseconds == null; + if (didEndScreenLoadingPrematurely) { + extendedEndTimeInMicroseconds = 0; + duration = 0; + + InstabugLogger.I.e( + "endScreenLoading was called too early in the Screen Loading cycle. Please make sure to call the API after the screen is done loading.", + tag: APM.tag, + ); + } + InstabugLogger.I.d( + 'endTimeInMicroseconds: ${currentScreenLoadingTrace?.endTimeInMicroseconds}, ' + 'didEndScreenLoadingPrematurely: $didEndScreenLoadingPrematurely, extendedEndTimeInMicroseconds: $extendedEndTimeInMicroseconds.', + tag: APM.tag, + ); + InstabugLogger.I.d( + 'Ending screen loading capture — duration: $extendedEndTimeInMicroseconds', + tag: APM.tag, + ); + + // Ends screen loading trace + APM.endScreenLoadingCP( + extendedEndTimeInMicroseconds, + currentUiTrace?.traceId ?? 0, + ); + currentUiTrace?.didExtendScreenLoading = true; + + return; + } catch (error, stackTrace) { + _logExceptionErrorAndStackTrace(error, stackTrace); + } + } + + /// Wraps the given routes with [InstabugCaptureScreenLoading] widgets. + /// + /// This allows Instabug to automatically capture screen loading times. + /// + /// Example usage: + /// + /// Map routes = { + /// '/home': (context) => const HomePage(), + /// '/settings': (context) => const SettingsPage(), + /// }; + /// + /// Map wrappedRoutes = + /// ScreenLoadingAutomaticManager.wrapRoutes( routes) + static Map wrapRoutes( + Map routes, { + List exclude = const [], + }) { + final excludedRoutes = {}; + for (final route in exclude) { + excludedRoutes[route] = true; + } + + final wrappedRoutes = {}; + for (final entry in routes.entries) { + if (!excludedRoutes.containsKey(entry.key)) { + wrappedRoutes[entry.key] = + (BuildContext context) => InstabugCaptureScreenLoading( + screenName: entry.key, + child: entry.value(context), + ); + } else { + wrappedRoutes[entry.key] = entry.value; + } + } + + return wrappedRoutes; + } +} + +@internal +class DropScreenLoadingError extends Error { + final ScreenLoadingTrace trace; + + DropScreenLoadingError(this.trace); + + @override + String toString() { + return 'DropScreenLoadingError: $trace'; + } +} diff --git a/lib/src/utils/screen_loading/screen_loading_trace.dart b/lib/src/utils/screen_loading/screen_loading_trace.dart new file mode 100644 index 000000000..42b78ad71 --- /dev/null +++ b/lib/src/utils/screen_loading/screen_loading_trace.dart @@ -0,0 +1,50 @@ +class ScreenLoadingTrace { + ScreenLoadingTrace( + this.screenName, { + required this.startTimeInMicroseconds, + required this.startMonotonicTimeInMicroseconds, + this.endTimeInMicroseconds, + this.duration, + }); + + final String screenName; + int startTimeInMicroseconds; + + /// Start time in microseconds from a monotonic clock like [InstabugMontonicClock.now]. + /// This should be preferred when measuring time durations and [startTimeInMicroseconds] + /// should only be used when reporting the timestamps in Unix epoch. + int startMonotonicTimeInMicroseconds; + + // TODO: Only startTimeInMicroseconds should be a Unix epoch timestamp, all + // other timestamps should be sampled from a monotonic clock like [InstabugMontonicClock.now] + // for higher precision and to avoid issues with system clock changes. + + // TODO: endTimeInMicroseconds depend on one another, so we can turn one of + // them into a getter instead of storing both. + int? endTimeInMicroseconds; + int? duration; + + ScreenLoadingTrace copyWith({ + String? screenName, + int? startTimeInMicroseconds, + int? startMonotonicTimeInMicroseconds, + int? endTimeInMicroseconds, + int? duration, + }) { + return ScreenLoadingTrace( + screenName ?? this.screenName, + startTimeInMicroseconds: + startTimeInMicroseconds ?? this.startTimeInMicroseconds, + startMonotonicTimeInMicroseconds: startMonotonicTimeInMicroseconds ?? + this.startMonotonicTimeInMicroseconds, + endTimeInMicroseconds: + endTimeInMicroseconds ?? this.endTimeInMicroseconds, + duration: duration ?? this.duration, + ); + } + + @override + String toString() { + return 'ScreenLoadingTrace{screenName: $screenName, startTimeInMicroseconds: $startTimeInMicroseconds, startMonotonicTimeInMicroseconds: $startMonotonicTimeInMicroseconds, endTimeInMicroseconds: $endTimeInMicroseconds, duration: $duration}'; + } +} diff --git a/lib/src/utils/screen_loading/ui_trace.dart b/lib/src/utils/screen_loading/ui_trace.dart new file mode 100644 index 000000000..17ef41046 --- /dev/null +++ b/lib/src/utils/screen_loading/ui_trace.dart @@ -0,0 +1,47 @@ +import 'package:instabug_flutter/src/utils/screen_loading/route_matcher.dart'; + +class UiTrace { + final String screenName; + + /// The screen name used while matching the UI trace with a Screen Loading + /// trace. + /// + /// For example, this is set to the original screen name before masking when + /// screen names masking is enabled. + final String _matchingScreenName; + + final int traceId; + bool didStartScreenLoading = false; + bool didReportScreenLoading = false; + bool didExtendScreenLoading = false; + + UiTrace({ + required this.screenName, + required this.traceId, + String? matchingScreenName, + }) : _matchingScreenName = matchingScreenName ?? screenName; + + UiTrace copyWith({ + String? screenName, + String? matchingScreenName, + int? traceId, + }) { + return UiTrace( + screenName: screenName ?? this.screenName, + matchingScreenName: matchingScreenName ?? _matchingScreenName, + traceId: traceId ?? this.traceId, + ); + } + + bool matches(String routePath) { + return RouteMatcher.I.match( + routePath: routePath, + actualPath: _matchingScreenName, + ); + } + + @override + String toString() { + return 'UiTrace{screenName: $screenName, traceId: $traceId, isFirstScreenLoadingReported: $didReportScreenLoading, isFirstScreenLoading: $didStartScreenLoading}'; + } +} diff --git a/lib/src/utils/screen_name_masker.dart b/lib/src/utils/screen_name_masker.dart new file mode 100644 index 000000000..1c2c4a972 --- /dev/null +++ b/lib/src/utils/screen_name_masker.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:instabug_flutter/src/utils/repro_steps_constants.dart'; + +typedef ScreenNameMaskingCallback = String Function(String screen); + +/// Mockable [ScreenNameMasker] responsible for masking screen names +/// before they are sent to the native SDKs. +class ScreenNameMasker { + ScreenNameMasker._(); + + static ScreenNameMasker _instance = ScreenNameMasker._(); + + static ScreenNameMasker get instance => _instance; + + /// Shorthand for [instance] + static ScreenNameMasker get I => instance; + + ScreenNameMaskingCallback? _screenNameMaskingCallback; + + @visibleForTesting + // ignore: use_setters_to_change_properties + static void setInstance(ScreenNameMasker instance) { + _instance = instance; + } + + // ignore: use_setters_to_change_properties + void setMaskingCallback(ScreenNameMaskingCallback? callback) { + _screenNameMaskingCallback = callback; + } + + String mask(String screen) { + if (_screenNameMaskingCallback == null) { + return screen; + } + + final maskedScreen = _screenNameMaskingCallback!(screen).trim(); + + if (maskedScreen.isEmpty) { + return ReproStepsConstants.emptyScreenFallback; + } + + return maskedScreen; + } +} diff --git a/lib/src/utils/w3c_header_utils.dart b/lib/src/utils/w3c_header_utils.dart new file mode 100644 index 000000000..26c3a8896 --- /dev/null +++ b/lib/src/utils/w3c_header_utils.dart @@ -0,0 +1,66 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:instabug_flutter/src/models/generated_w3c_header.dart'; +import 'package:instabug_flutter/src/models/trace_partial_id.dart'; + +class W3CHeaderUtils { + // Access the singleton instance + factory W3CHeaderUtils() { + return _instance; + } + // Private constructor to prevent instantiation + W3CHeaderUtils._(); + + // Singleton instance + static final W3CHeaderUtils _instance = W3CHeaderUtils._(); + + // Random instance + static Random _random = Random(); + + @visibleForTesting + // Setter for the Random instance + // ignore: use_setters_to_change_properties + void $setRandom(Random random) { + _random = random; + } + + /// Generate random 32-bit unsigned integer Hexadecimal (8 chars) lower case letters + /// Should not return all zeros + TracePartialId generateTracePartialId() { + int randomNumber; + String hexString; + + do { + randomNumber = _random.nextInt(0xffffffff); + hexString = randomNumber.toRadixString(16).padLeft(8, '0'); + } while (hexString == '00000000'); + + return TracePartialId( + numberPartialId: randomNumber, + hexPartialId: hexString.toLowerCase(), + ); + } + + /// Generate W3C header in the format of {version}-{trace-id}-{parent-id}-{trace-flag} + /// @param networkStartTime + /// @returns W3C header + GeneratedW3CHeader generateW3CHeader(int networkStartTime) { + final partialIdData = generateTracePartialId(); + final hexStringPartialId = partialIdData.hexPartialId; + final numberPartialId = partialIdData.numberPartialId; + + final timestampInSeconds = (networkStartTime / 1000).floor(); + final hexaDigitsTimestamp = + timestampInSeconds.toRadixString(16).toLowerCase(); + final traceId = + '$hexaDigitsTimestamp$hexStringPartialId$hexaDigitsTimestamp$hexStringPartialId'; + final parentId = '4942472d$hexStringPartialId'; + + return GeneratedW3CHeader( + timestampInSeconds: timestampInSeconds, + partialId: numberPartialId, + w3cHeader: '00-$traceId-$parentId-01', + ); + } +} diff --git a/pigeons/apm.api.dart b/pigeons/apm.api.dart index dfb23366c..84fe9eb8e 100644 --- a/pigeons/apm.api.dart +++ b/pigeons/apm.api.dart @@ -3,6 +3,11 @@ import 'package:pigeon/pigeon.dart'; @HostApi() abstract class ApmHostApi { void setEnabled(bool isEnabled); + @async + bool isEnabled(); + void setScreenLoadingEnabled(bool isEnabled); + @async + bool isScreenLoadingEnabled(); void setColdAppLaunchEnabled(bool isEnabled); void setAutoUITraceEnabled(bool isEnabled); @@ -22,4 +27,17 @@ abstract class ApmHostApi { void endUITrace(); void endAppLaunch(); void networkLogAndroid(Map data); + + void startCpUiTrace(String screenName, int microTimeStamp, int traceId); + + void reportScreenLoadingCP( + int startTimeStampMicro, + int durationMicro, + int uiTraceId, + ); + + void endScreenLoadingCP(int timeStampMicro, int uiTraceId); + + @async + bool isEndScreenLoadingEnabled(); } diff --git a/pigeons/instabug.api.dart b/pigeons/instabug.api.dart index 7113f164c..c0187acb9 100644 --- a/pigeons/instabug.api.dart +++ b/pigeons/instabug.api.dart @@ -1,8 +1,19 @@ import 'package:pigeon/pigeon.dart'; +@FlutterApi() +abstract class FeatureFlagsFlutterApi { + void onW3CFeatureFlagChange( + bool isW3cExternalTraceIDEnabled, + bool isW3cExternalGeneratedHeaderEnabled, + bool isW3cCaughtHeaderEnabled, + ); +} + @HostApi() abstract class InstabugHostApi { void setEnabled(bool isEnabled); + bool isEnabled(); + bool isBuilt(); void init(String token, List invocationEvents, String debugLogsLevel); void show(); @@ -29,6 +40,9 @@ abstract class InstabugHostApi { void addExperiments(List experiments); void removeExperiments(List experiments); void clearAllExperiments(); + void addFeatureFlags(Map featureFlagsMap); + void removeFeatureFlags(List featureFlags); + void removeAllFeatureFlags(); void setUserAttribute(String value, String key); void removeUserAttribute(String key); @@ -55,5 +69,9 @@ abstract class InstabugHostApi { void networkLog(Map data); + void registerFeatureFlagChangeListener(); + + Map isW3CFeatureFlagsEnabled(); + void willRedirectToStore(); } diff --git a/pubspec.yaml b/pubspec.yaml index 7c99e92f6..ba6dd0be2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: instabug_flutter -version: 13.1.1 +version: 13.4.0 description: >- Instabug empowers mobile teams to monitor, prioritize, and debug performance and stability issues throughout the app development lifecycle. @@ -15,11 +15,12 @@ dependencies: dev_dependencies: build_runner: ^2.0.3 + fake_async: '>=1.2.0 <1.4.0' flutter_test: sdk: flutter lint: ^1.0.0 # mockito v5.2.0 is needed for running Flutter 2 tests on CI - mockito: '>=5.2.0 <=5.4.2' + mockito: '>=5.2.0 <5.5.0' pana: ^0.21.0 # pigeon v3.0.0 is needed for running Flutter 2 tests on CI pigeon: '>=3.0.0 <=10.1.5' @@ -34,5 +35,5 @@ flutter: pluginClass: InstabugFlutterPlugin environment: - sdk: ">=2.12.0 <3.0.0" + sdk: ">=2.14.0 <4.0.0" flutter: ">=1.17.0" diff --git a/test/apm_test.dart b/test/apm_test.dart index 8551c0c82..c801926f3 100644 --- a/test/apm_test.dart +++ b/test/apm_test.dart @@ -38,6 +38,34 @@ void main() { ).called(1); }); + test('[isEnabled] should call host method', () async { + when(mHost.isEnabled()).thenAnswer((_) async => true); + await APM.isEnabled(); + + verify( + mHost.isEnabled(), + ).called(1); + }); + + test('[setScreenLoadingMonitoringEnabled] should call host method', () async { + const enabled = true; + + await APM.setScreenLoadingEnabled(enabled); + + verify( + mHost.setScreenLoadingEnabled(enabled), + ).called(1); + }); + + test('[isScreenLoadingMonitoringEnabled] should call host method', () async { + when(mHost.isScreenLoadingEnabled()).thenAnswer((_) async => true); + await APM.isScreenLoadingEnabled(); + + verify( + mHost.isScreenLoadingEnabled(), + ).called(1); + }); + test('[setColdAppLaunchEnabled] should call host method', () async { const enabled = true; @@ -175,4 +203,59 @@ void main() { mHost.networkLogAndroid(data.toJson()), ).called(1); }); + + test('[startCpUiTrace] should call host method', () async { + const screenName = 'screen-name'; + final microTimeStamp = DateTime.now().microsecondsSinceEpoch; + final traceId = DateTime.now().millisecondsSinceEpoch; + + await APM.startCpUiTrace(screenName, microTimeStamp, traceId); + + verify( + mHost.startCpUiTrace(screenName, microTimeStamp, traceId), + ).called(1); + verifyNoMoreInteractions(mHost); + }); + + test('[reportScreenLoading] should call host method', () async { + final startTimeStampMicro = DateTime.now().microsecondsSinceEpoch; + final durationMicro = DateTime.now().microsecondsSinceEpoch; + final uiTraceId = DateTime.now().millisecondsSinceEpoch; + + await APM.reportScreenLoadingCP( + startTimeStampMicro, + durationMicro, + uiTraceId, + ); + + verify( + mHost.reportScreenLoadingCP( + startTimeStampMicro, + durationMicro, + uiTraceId, + ), + ).called(1); + verifyNoMoreInteractions(mHost); + }); + + test('[endScreenLoading] should call host method', () async { + final timeStampMicro = DateTime.now().microsecondsSinceEpoch; + final uiTraceId = DateTime.now().millisecondsSinceEpoch; + + await APM.endScreenLoadingCP(timeStampMicro, uiTraceId); + + verify( + mHost.endScreenLoadingCP(timeStampMicro, uiTraceId), + ).called(1); + verifyNoMoreInteractions(mHost); + }); + + test('[isSEndScreenLoadingEnabled] should call host method', () async { + when(mHost.isEndScreenLoadingEnabled()).thenAnswer((_) async => true); + await APM.isEndScreenLoadingEnabled(); + + verify( + mHost.isEndScreenLoadingEnabled(), + ).called(1); + }); } diff --git a/test/feature_flags_manager_test.dart b/test/feature_flags_manager_test.dart new file mode 100644 index 000000000..1a78f666c --- /dev/null +++ b/test/feature_flags_manager_test.dart @@ -0,0 +1,84 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:instabug_flutter/src/generated/instabug.api.g.dart'; +import 'package:instabug_flutter/src/utils/feature_flags_manager.dart'; +import 'package:instabug_flutter/src/utils/ibg_build_info.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'feature_flags_manager_test.mocks.dart'; + +@GenerateMocks([InstabugHostApi, IBGBuildInfo]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + WidgetsFlutterBinding.ensureInitialized(); + + final mInstabugHost = MockInstabugHostApi(); + final mBuildInfo = MockIBGBuildInfo(); + + setUpAll(() { + FeatureFlagsManager().$setHostApi(mInstabugHost); + IBGBuildInfo.setInstance(mBuildInfo); + }); + + tearDown(() { + reset(mInstabugHost); + }); + + test('[getW3CFeatureFlagsHeader] should call host method on IOS', () async { + when(mBuildInfo.isAndroid).thenReturn(false); + when(mInstabugHost.isW3CFeatureFlagsEnabled()).thenAnswer( + (_) => Future.value({ + "isW3cExternalTraceIDEnabled": true, + "isW3cExternalGeneratedHeaderEnabled": true, + "isW3cCaughtHeaderEnabled": true, + }), + ); + final isW3CExternalTraceID = + await FeatureFlagsManager().getW3CFeatureFlagsHeader(); + expect(isW3CExternalTraceID.isW3cExternalTraceIDEnabled, true); + expect(isW3CExternalTraceID.isW3cExternalGeneratedHeaderEnabled, true); + expect(isW3CExternalTraceID.isW3cCaughtHeaderEnabled, true); + + verify( + mInstabugHost.isW3CFeatureFlagsEnabled(), + ).called(1); + }); + + test('[isW3CExternalTraceID] should call host method on Android', () async { + when(mBuildInfo.isAndroid).thenReturn(true); + when(mInstabugHost.isW3CFeatureFlagsEnabled()).thenAnswer( + (_) => Future.value({ + "isW3cExternalTraceIDEnabled": true, + "isW3cExternalGeneratedHeaderEnabled": true, + "isW3cCaughtHeaderEnabled": true, + }), + ); + await FeatureFlagsManager().registerW3CFlagsListener(); + + final isW3CExternalTraceID = + await FeatureFlagsManager().getW3CFeatureFlagsHeader(); + expect(isW3CExternalTraceID.isW3cExternalTraceIDEnabled, true); + expect(isW3CExternalTraceID.isW3cExternalGeneratedHeaderEnabled, true); + expect(isW3CExternalTraceID.isW3cCaughtHeaderEnabled, true); + verify( + mInstabugHost.isW3CFeatureFlagsEnabled(), + ).called(1); + }); + + test('[registerW3CFlagsListener] should call host method', () async { + when(mInstabugHost.isW3CFeatureFlagsEnabled()).thenAnswer( + (_) => Future.value({ + "isW3cExternalTraceIDEnabled": true, + "isW3cExternalGeneratedHeaderEnabled": true, + "isW3cCaughtHeaderEnabled": true, + }), + ); + + await FeatureFlagsManager().registerW3CFlagsListener(); + + verify( + mInstabugHost.registerFeatureFlagChangeListener(), + ).called(1); + }); +} diff --git a/test/instabug_test.dart b/test/instabug_test.dart index 78a370963..e2fd7d298 100644 --- a/test/instabug_test.dart +++ b/test/instabug_test.dart @@ -5,7 +5,9 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:instabug_flutter/instabug_flutter.dart'; import 'package:instabug_flutter/src/generated/instabug.api.g.dart'; import 'package:instabug_flutter/src/utils/enum_converter.dart'; +import 'package:instabug_flutter/src/utils/feature_flags_manager.dart'; import 'package:instabug_flutter/src/utils/ibg_build_info.dart'; +import 'package:instabug_flutter/src/utils/screen_name_masker.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; @@ -14,6 +16,7 @@ import 'instabug_test.mocks.dart'; @GenerateMocks([ InstabugHostApi, IBGBuildInfo, + ScreenNameMasker, ]) void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -21,10 +24,13 @@ void main() { final mHost = MockInstabugHostApi(); final mBuildInfo = MockIBGBuildInfo(); + final mScreenNameMasker = MockScreenNameMasker(); setUpAll(() { Instabug.$setHostApi(mHost); + FeatureFlagsManager().$setHostApi(mHost); IBGBuildInfo.setInstance(mBuildInfo); + ScreenNameMasker.setInstance(mScreenNameMasker); }); test('[setEnabled] should call host method', () async { @@ -37,10 +43,41 @@ void main() { ).called(1); }); + test('[isEnabled] should call host method', () async { + const expected = true; + when(mHost.isEnabled()).thenAnswer((_) async => expected); + + final actual = await Instabug.isEnabled(); + + verify( + mHost.isEnabled(), + ).called(1); + expect(actual, expected); + }); + + test('[isBuilt] should call host method', () async { + const expected = true; + when(mHost.isBuilt()).thenAnswer((_) async => expected); + + final actual = await Instabug.isBuilt(); + + verify( + mHost.isBuilt(), + ).called(1); + + expect(actual, expected); + }); + test('[start] should call host method', () async { const token = "068ba9a8c3615035e163dc5f829c73be"; const events = [InvocationEvent.shake, InvocationEvent.screenshot]; - + when(mHost.isW3CFeatureFlagsEnabled()).thenAnswer( + (_) => Future.value({ + "isW3cExternalTraceIDEnabled": true, + "isW3cExternalGeneratedHeaderEnabled": true, + "isW3cCaughtHeaderEnabled": true, + }), + ); await Instabug.init( token: token, invocationEvents: events, @@ -51,6 +88,16 @@ void main() { ).called(1); }); + test( + '[setScreenNameMaskingCallback] should set masking callback on screen name masker', + () async { + String callback(String screen) => 'REDACTED/$screen'; + + Instabug.setScreenNameMaskingCallback(callback); + + verify(mScreenNameMasker.setMaskingCallback(callback)).called(1); + }); + test('[show] should call host method', () async { await Instabug.show(); @@ -214,6 +261,7 @@ void main() { test('[addExperiments] should call host method', () async { const experiments = ["exp-1", "exp-2"]; + // ignore: deprecated_member_use_from_same_package await Instabug.addExperiments(experiments); verify( @@ -224,6 +272,7 @@ void main() { test('[removeExperiments] should call host method', () async { const experiments = ["exp-1", "exp-2"]; + // ignore: deprecated_member_use_from_same_package await Instabug.removeExperiments(experiments); verify( @@ -232,6 +281,7 @@ void main() { }); test('[clearAllExperiments] should call host method', () async { + // ignore: deprecated_member_use_from_same_package await Instabug.clearAllExperiments(); verify( @@ -239,6 +289,38 @@ void main() { ).called(1); }); + test('[addFeatureFlags] should call host method', () async { + await Instabug.addFeatureFlags([ + FeatureFlag(name: 'name1', variant: 'variant1'), + FeatureFlag(name: 'name2', variant: 'variant2'), + ]); + + verify( + mHost.addFeatureFlags({ + "name1": "variant1", + "name2": "variant2", + }), + ).called(1); + }); + + test('[removeFeatureFlags] should call host method', () async { + const featureFlags = ["exp-1", "exp-2"]; + + await Instabug.removeFeatureFlags(featureFlags); + + verify( + mHost.removeFeatureFlags(featureFlags), + ).called(1); + }); + + test('[clearAllFeatureFlags] should call host method', () async { + await Instabug.clearAllFeatureFlags(); + + verify( + mHost.removeAllFeatureFlags(), + ).called(1); + }); + test('[setUserAttribute] should call host method', () async { const key = "attr-key"; const attribute = "User Attribute"; diff --git a/test/network_logger_test.dart b/test/network_logger_test.dart index e5a04e236..b7790ac1e 100644 --- a/test/network_logger_test.dart +++ b/test/network_logger_test.dart @@ -5,8 +5,10 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:instabug_flutter/instabug_flutter.dart'; import 'package:instabug_flutter/src/generated/apm.api.g.dart'; import 'package:instabug_flutter/src/generated/instabug.api.g.dart'; +import 'package:instabug_flutter/src/utils/feature_flags_manager.dart'; import 'package:instabug_flutter/src/utils/ibg_build_info.dart'; import 'package:instabug_flutter/src/utils/network_manager.dart'; +import 'package:instabug_flutter/src/utils/w3c_header_utils.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; @@ -17,6 +19,8 @@ import 'network_logger_test.mocks.dart'; InstabugHostApi, IBGBuildInfo, NetworkManager, + W3CHeaderUtils, + FeatureFlagsManager, ]) void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -36,6 +40,7 @@ void main() { setUpAll(() { APM.$setHostApi(mApmHost); + FeatureFlagsManager().$setHostApi(mInstabugHost); NetworkLogger.$setHostApi(mInstabugHost); NetworkLogger.$setManager(mManager); IBGBuildInfo.setInstance(mBuildInfo); @@ -46,6 +51,13 @@ void main() { reset(mInstabugHost); reset(mBuildInfo); reset(mManager); + when(mInstabugHost.isW3CFeatureFlagsEnabled()).thenAnswer( + (_) => Future.value({ + "isW3cExternalTraceIDEnabled": true, + "isW3cExternalGeneratedHeaderEnabled": true, + "isW3cCaughtHeaderEnabled": true, + }), + ); }); test('[networkLog] should call 1 host method on iOS', () async { @@ -53,7 +65,7 @@ void main() { when(mManager.obfuscateLog(data)).thenReturn(data); when(mManager.omitLog(data)).thenReturn(false); - await logger.networkLog(data); + await logger.networkLogInternal(data); verify( mInstabugHost.networkLog(data.toJson()), @@ -69,7 +81,7 @@ void main() { when(mManager.obfuscateLog(data)).thenReturn(data); when(mManager.omitLog(data)).thenReturn(false); - await logger.networkLog(data); + await logger.networkLogInternal(data); verify( mInstabugHost.networkLog(data.toJson()), @@ -87,7 +99,7 @@ void main() { when(mManager.obfuscateLog(data)).thenReturn(obfuscated); when(mManager.omitLog(data)).thenReturn(false); - await logger.networkLog(data); + await logger.networkLogInternal(data); verify( mManager.obfuscateLog(data), @@ -109,7 +121,7 @@ void main() { when(mManager.obfuscateLog(data)).thenReturn(data); when(mManager.omitLog(data)).thenReturn(omit); - await logger.networkLog(data); + await logger.networkLogInternal(data); verify( mManager.omitLog(data), diff --git a/test/route_matcher_test.dart b/test/route_matcher_test.dart new file mode 100644 index 000000000..977c61d88 --- /dev/null +++ b/test/route_matcher_test.dart @@ -0,0 +1,150 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:instabug_flutter/src/utils/screen_loading/route_matcher.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + WidgetsFlutterBinding.ensureInitialized(); + + test('[match] should return true when static paths match', () { + const routePath = '/user/profile'; + const actualPath = '/user/profile'; + + final isMatch = RouteMatcher.I.match( + routePath: routePath, + actualPath: actualPath, + ); + + expect(isMatch, isTrue); + }); + + test( + '[match] should return true when static paths match ignoring query parameters', + () { + const routePath = '/user/profile?name=John&premium=true'; + const actualPath = '/user/profile'; + + final isMatch = RouteMatcher.I.match( + routePath: routePath, + actualPath: actualPath, + ); + + expect(isMatch, isTrue); + }); + + test('[match] should return false when static paths do not match', () { + const routePath = '/user/profile'; + const actualPath = '/user/settings'; + + final isMatch = RouteMatcher.I.match( + routePath: routePath, + actualPath: actualPath, + ); + + expect(isMatch, isFalse); + }); + + test('[match] should return true when parameterized paths match', () { + const routePath = '/user/:id/profile'; + const actualPath = '/user/123/profile'; + + final isMatch = RouteMatcher.I.match( + routePath: routePath, + actualPath: actualPath, + ); + + expect(isMatch, isTrue); + }); + + test('[match] should return false when parameterized paths do not match', () { + const routePath = '/user/:id/profile'; + const actualPath = '/user/profile'; + + final isMatch = RouteMatcher.I.match( + routePath: routePath, + actualPath: actualPath, + ); + + expect(isMatch, isFalse); + }); + + test('[match] should return true when paths match with wildcard', () { + const routePath = '/user/**'; + const actualPath = '/user/123/profile'; + + final isMatch = RouteMatcher.I.match( + routePath: routePath, + actualPath: actualPath, + ); + + expect(isMatch, isTrue); + }); + + test('[match] should return false when paths do not match with wildcard', () { + const routePath = '/profile/**'; + const actualPath = '/user/123/profile'; + + final isMatch = RouteMatcher.I.match( + routePath: routePath, + actualPath: actualPath, + ); + + expect(isMatch, isFalse); + }); + + test( + '[match] should return true when paths match with wildcard and parameters', + () { + const routePath = '/user/:id/friends/:friend/**'; + const actualPath = '/user/123/friends/456/profile/about'; + + final isMatch = RouteMatcher.I.match( + routePath: routePath, + actualPath: actualPath, + ); + + expect(isMatch, isTrue); + }); + + test( + '[match] should return false when paths do not match with wildcard and parameters', + () { + const routePath = '/user/:id/friends/:friend/profile/**'; + const actualPath = '/user/123/friends/123/about'; + + final isMatch = RouteMatcher.I.match( + routePath: routePath, + actualPath: actualPath, + ); + + expect(isMatch, isFalse); + }); + + test( + '[match] should return true when paths match ignoring leading and trailing slashes', + () { + const routePath = 'user/:id/friends/:friend/profile/'; + const actualPath = '/user/123/friends/123/profile'; + + final isMatch = RouteMatcher.I.match( + routePath: routePath, + actualPath: actualPath, + ); + + expect(isMatch, isTrue); + }); + + test( + '[match] should return false when paths do not match ignoring leading and trailing slashes', + () { + const routePath = 'user/:id/friends/:friend/profile/'; + const actualPath = '/user/123/friends/123/about'; + + final isMatch = RouteMatcher.I.match( + routePath: routePath, + actualPath: actualPath, + ); + + expect(isMatch, isFalse); + }); +} diff --git a/test/utils/instabug_navigator_observer_test.dart b/test/utils/instabug_navigator_observer_test.dart new file mode 100644 index 000000000..ebf541137 --- /dev/null +++ b/test/utils/instabug_navigator_observer_test.dart @@ -0,0 +1,140 @@ +import 'package:fake_async/fake_async.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:instabug_flutter/instabug_flutter.dart'; +import 'package:instabug_flutter/src/generated/instabug.api.g.dart'; +import 'package:instabug_flutter/src/utils/screen_loading/screen_loading_manager.dart'; +import 'package:instabug_flutter/src/utils/screen_name_masker.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'instabug_navigator_observer_test.mocks.dart'; + +@GenerateMocks([ + InstabugHostApi, + ScreenLoadingManager, +]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + WidgetsFlutterBinding.ensureInitialized(); + + final mHost = MockInstabugHostApi(); + final mScreenLoadingManager = MockScreenLoadingManager(); + + late InstabugNavigatorObserver observer; + const screen = '/screen'; + const previousScreen = '/previousScreen'; + late Route route; + late Route previousRoute; + + setUpAll(() { + Instabug.$setHostApi(mHost); + ScreenLoadingManager.setInstance(mScreenLoadingManager); + }); + + setUp(() { + observer = InstabugNavigatorObserver(); + route = createRoute(screen); + previousRoute = createRoute(previousScreen); + + ScreenNameMasker.I.setMaskingCallback(null); + }); + + test('should report screen change when a route is pushed', () { + fakeAsync((async) { + observer.didPush(route, previousRoute); + + async.elapse(const Duration(milliseconds: 1000)); + + verify( + mScreenLoadingManager.startUiTrace(screen, screen), + ).called(1); + + verify( + mHost.reportScreenChange(screen), + ).called(1); + }); + }); + + test( + 'should report screen change when a route is popped and previous is known', + () { + fakeAsync((async) { + observer.didPop(route, previousRoute); + + async.elapse(const Duration(milliseconds: 1000)); + + verify( + mScreenLoadingManager.startUiTrace(previousScreen, previousScreen), + ).called(1); + + verify( + mHost.reportScreenChange(previousScreen), + ).called(1); + }); + }); + + test( + 'should not report screen change when a route is popped and previous is not known', + () { + fakeAsync((async) { + observer.didPop(route, null); + + async.elapse(const Duration(milliseconds: 1000)); + + verifyNever( + mScreenLoadingManager.startUiTrace(any, any), + ); + + verifyNever( + mHost.reportScreenChange(any), + ); + }); + }); + + test('should fallback to "N/A" when the screen name is empty', () { + fakeAsync((async) { + final route = createRoute(''); + const fallback = 'N/A'; + + observer.didPush(route, previousRoute); + + async.elapse(const Duration(milliseconds: 1000)); + + verify( + mScreenLoadingManager.startUiTrace(fallback, fallback), + ).called(1); + + verify( + mHost.reportScreenChange(fallback), + ).called(1); + }); + }); + + test('should mask screen name when masking callback is set', () { + const maskedScreen = 'maskedScreen'; + + ScreenNameMasker.I.setMaskingCallback((_) => maskedScreen); + + fakeAsync((async) { + observer.didPush(route, previousRoute); + + async.elapse(const Duration(milliseconds: 1000)); + + verify( + mScreenLoadingManager.startUiTrace(maskedScreen, screen), + ).called(1); + + verify( + mHost.reportScreenChange(maskedScreen), + ).called(1); + }); + }); +} + +Route createRoute(String? name) { + return MaterialPageRoute( + builder: (_) => Container(), + settings: RouteSettings(name: name), + ); +} diff --git a/test/utils/screen_loading/screen_loading_manager_test.dart b/test/utils/screen_loading/screen_loading_manager_test.dart new file mode 100644 index 000000000..c008b8bcc --- /dev/null +++ b/test/utils/screen_loading/screen_loading_manager_test.dart @@ -0,0 +1,1162 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:instabug_flutter/instabug_flutter.dart'; +import 'package:instabug_flutter/src/generated/apm.api.g.dart'; +import 'package:instabug_flutter/src/generated/instabug.api.g.dart'; +import 'package:instabug_flutter/src/utils/ibg_build_info.dart'; +import 'package:instabug_flutter/src/utils/ibg_date_time.dart'; +import 'package:instabug_flutter/src/utils/instabug_logger.dart'; +import 'package:instabug_flutter/src/utils/instabug_montonic_clock.dart'; +import 'package:instabug_flutter/src/utils/screen_loading/flags_config.dart'; +import 'package:instabug_flutter/src/utils/screen_loading/screen_loading_manager.dart'; +import 'package:instabug_flutter/src/utils/screen_loading/screen_loading_trace.dart'; +import 'package:instabug_flutter/src/utils/screen_loading/ui_trace.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'screen_loading_manager_test.mocks.dart'; + +class ScreenLoadingManagerNoResets extends ScreenLoadingManager { + ScreenLoadingManagerNoResets.init() : super.init(); + + @override + void resetDidExtendScreenLoading() {} + + @override + void resetDidReportScreenLoading() {} + + @override + void resetDidStartScreenLoading() {} +} + +@GenerateMocks([ + ApmHostApi, + InstabugHostApi, + InstabugLogger, + IBGDateTime, + InstabugMonotonicClock, + IBGBuildInfo, + RouteMatcher, + BuildContext, + Widget, +]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + WidgetsFlutterBinding.ensureInitialized(); + + late ScreenLoadingManager mScreenLoadingManager; + late MockApmHostApi mApmHost; + late MockInstabugHostApi mInstabugHost; + late MockInstabugLogger mInstabugLogger; + late IBGDateTime mDateTime; + late IBGBuildInfo mIBGBuildInfo; + late MockRouteMatcher mRouteMatcher; + late InstabugMonotonicClock mInstabugMonotonicClock; + late MockWidget mockScreen; + late MockBuildContext mockBuildContext; + const screenName = 'screen1'; + + setUp(() { + mScreenLoadingManager = ScreenLoadingManager.init(); + mApmHost = MockApmHostApi(); + mInstabugHost = MockInstabugHostApi(); + mInstabugLogger = MockInstabugLogger(); + mDateTime = MockIBGDateTime(); + mIBGBuildInfo = MockIBGBuildInfo(); + mRouteMatcher = MockRouteMatcher(); + mInstabugMonotonicClock = MockInstabugMonotonicClock(); + when(mInstabugHost.isBuilt()).thenAnswer((_) async => true); + + ScreenLoadingManager.setInstance(mScreenLoadingManager); + APM.$setHostApi(mApmHost); + Instabug.$setHostApi(mInstabugHost); + InstabugLogger.setInstance(mInstabugLogger); + IBGDateTime.setInstance(mDateTime); + IBGBuildInfo.setInstance(mIBGBuildInfo); + RouteMatcher.setInstance(mRouteMatcher); + InstabugMonotonicClock.setInstance(mInstabugMonotonicClock); + }); + + group('reset methods tests', () { + test( + '[resetDidStartScreenLoading] should set _currentUITrace?.didStartScreenLoading to false', + () async { + const expected = false; + final uiTrace = UiTrace(screenName: 'screen1', traceId: 1); + uiTrace.didStartScreenLoading = true; + mScreenLoadingManager.currentUiTrace = uiTrace; + + ScreenLoadingManager.I.resetDidStartScreenLoading(); + + final actual = + ScreenLoadingManager.I.currentUiTrace?.didStartScreenLoading; + + expect(actual, expected); + verify( + mInstabugLogger.d( + argThat(contains('Resetting didStartScreenLoading')), + tag: APM.tag, + ), + ).called(1); + }); + + test( + '[resetDidReportScreenLoading] should set _currentUITrace?.didReportScreenLoading to false', + () async { + const expected = false; + final uiTrace = UiTrace(screenName: 'screen1', traceId: 1); + uiTrace.didReportScreenLoading = true; + mScreenLoadingManager.currentUiTrace = uiTrace; + + ScreenLoadingManager.I.resetDidReportScreenLoading(); + + final actual = + ScreenLoadingManager.I.currentUiTrace?.didReportScreenLoading; + + expect(actual, expected); + verify( + mInstabugLogger.d( + argThat(contains('Resetting didExtendScreenLoading')), + tag: APM.tag, + ), + ).called(1); + }); + + test( + '[resetDidExtendScreenLoading] should set _currentUITrace?.didExtendScreenLoading to false', + () async { + const expected = false; + final uiTrace = UiTrace(screenName: 'screen1', traceId: 1); + mScreenLoadingManager.currentUiTrace = uiTrace; + + ScreenLoadingManager.I.resetDidExtendScreenLoading(); + + final actualUiTrace = ScreenLoadingManager.I.currentUiTrace; + + expect( + actualUiTrace?.didStartScreenLoading, + false, + ); + expect( + actualUiTrace?.didReportScreenLoading, + false, + ); + expect( + actualUiTrace?.didExtendScreenLoading, + false, + ); + expect(actualUiTrace?.didExtendScreenLoading, expected); + verify( + mInstabugLogger.d( + argThat(contains('Resetting didReportScreenLoading')), + tag: APM.tag, + ), + ).called(1); + }); + }); + + group('startUiTrace tests', () { + late UiTrace uiTrace; + late DateTime time; + + setUp(() { + time = DateTime.now(); + uiTrace = + UiTrace(screenName: screenName, traceId: time.millisecondsSinceEpoch); + ScreenLoadingManager.setInstance(ScreenLoadingManagerNoResets.init()); + mScreenLoadingManager.currentUiTrace = uiTrace; + when(mDateTime.now()).thenReturn(time); + }); + + test('[startUiTrace] with SDK not build should Log error', () async { + mScreenLoadingManager.currentUiTrace = uiTrace; + when(mInstabugHost.isBuilt()).thenAnswer((_) async => false); + + await ScreenLoadingManager.I.startUiTrace(screenName); + + final actualUiTrace = ScreenLoadingManager.I.currentUiTrace; + expect(actualUiTrace, null); + + verify( + mInstabugLogger.e( + 'Instabug API {APM.InstabugCaptureScreenLoading} was called before the SDK is built. To build it, first by following the instructions at this link:\n' + 'https://docs.instabug.com/reference#showing-and-manipulating-the-invocation', + tag: APM.tag, + ), + ).called(1); + verifyNever(mApmHost.startCpUiTrace(any, any, any)); + }); + + test('[startUiTrace] with APM disabled on iOS Platform should Log error', + () async { + mScreenLoadingManager.currentUiTrace = uiTrace; + when(FlagsConfig.apm.isEnabled()).thenAnswer((_) async => false); + when(IBGBuildInfo.I.isIOS).thenReturn(true); + + await ScreenLoadingManager.I.startUiTrace(screenName); + + final actualUiTrace = ScreenLoadingManager.I.currentUiTrace; + expect(actualUiTrace, null); + + verify( + mInstabugLogger.e( + 'APM is disabled, skipping starting the UI trace for screen: $screenName.\n' + 'Please refer to the documentation for how to enable APM on your app: https://docs.instabug.com/docs/react-native-apm-disabling-enabling', + tag: APM.tag, + ), + ).called(1); + verifyNever(mApmHost.startCpUiTrace(any, any, any)); + }); + + test( + '[startUiTrace] with APM enabled on android Platform should call `APM.startCpUiTrace and set UiTrace', + () async { + when(FlagsConfig.apm.isEnabled()).thenAnswer((_) async => true); + when(IBGBuildInfo.I.isIOS).thenReturn(false); + + await ScreenLoadingManager.I.startUiTrace(screenName); + + final actualUiTrace = ScreenLoadingManager.I.currentUiTrace; + + expect( + actualUiTrace?.didStartScreenLoading, + false, + ); + expect(actualUiTrace?.screenName, screenName); + expect(actualUiTrace?.traceId, time.millisecondsSinceEpoch); + verify( + mApmHost.startCpUiTrace( + screenName, + time.microsecondsSinceEpoch, + time.millisecondsSinceEpoch, + ), + ).called(1); + }); + + test( + '[startUiTrace] with APM enabled should create a UI trace with the matching screen name', + () async { + when(FlagsConfig.apm.isEnabled()).thenAnswer((_) async => true); + when(IBGBuildInfo.I.isIOS).thenReturn(false); + when( + RouteMatcher.I.match( + routePath: anyNamed('routePath'), + actualPath: anyNamed('actualPath'), + ), + ).thenReturn(false); + + const matchingScreenName = 'matching_screen_name'; + + await ScreenLoadingManager.I.startUiTrace(screenName, matchingScreenName); + + final actualUiTrace = ScreenLoadingManager.I.currentUiTrace!; + + actualUiTrace.matches(screenName); + + verify( + mRouteMatcher.match( + routePath: screenName, + actualPath: matchingScreenName, + ), + ).called(1); + + verifyNever( + mRouteMatcher.match( + routePath: anyNamed('routePath'), + actualPath: screenName, + ), + ); + + expect(actualUiTrace.screenName, screenName); + verify( + mApmHost.startCpUiTrace( + screenName, + any, + any, + ), + ).called(1); + }); + }); + + group('startScreenLoadingTrace tests', () { + late DateTime time; + late UiTrace uiTrace; + late int traceId; + late ScreenLoadingTrace screenLoadingTrace; + setUp(() { + mScreenLoadingManager = ScreenLoadingManagerNoResets.init(); + time = DateTime.now(); + traceId = time.millisecondsSinceEpoch; + uiTrace = UiTrace(screenName: screenName, traceId: traceId); + mScreenLoadingManager.currentUiTrace = uiTrace; + when(mDateTime.now()).thenReturn(time); + + screenLoadingTrace = ScreenLoadingTrace( + screenName, + startTimeInMicroseconds: time.microsecondsSinceEpoch, + startMonotonicTimeInMicroseconds: time.microsecondsSinceEpoch, + ); + ScreenLoadingManager.setInstance(mScreenLoadingManager); + }); + + test('[startScreenLoadingTrace] with SDK not build should Log error', + () async { + when(mInstabugHost.isBuilt()).thenAnswer((_) async => false); + + await ScreenLoadingManager.I.startScreenLoadingTrace(screenLoadingTrace); + + final actualUiTrace = ScreenLoadingManager.I.currentUiTrace; + final actualScreenLoadingTrace = + ScreenLoadingManager.I.currentScreenLoadingTrace; + + expect( + actualUiTrace?.didStartScreenLoading, + false, + ); + expect( + actualScreenLoadingTrace, + null, + ); + verify( + mInstabugLogger.e( + 'Instabug API {APM.InstabugCaptureScreenLoading} was called before the SDK is built. To build it, first by following the instructions at this link:\n' + 'https://docs.instabug.com/reference#showing-and-manipulating-the-invocation', + tag: APM.tag, + ), + ).called(1); + }); + + test( + '[startScreenLoadingTrace] with screen loading disabled on iOS Platform should log error', + () async { + when(FlagsConfig.screenLoading.isEnabled()) + .thenAnswer((_) async => false); + when(IBGBuildInfo.I.isIOS).thenReturn(true); + + await ScreenLoadingManager.I.startScreenLoadingTrace(screenLoadingTrace); + + final actualUiTrace = ScreenLoadingManager.I.currentUiTrace; + final actualScreenLoadingTrace = + ScreenLoadingManager.I.currentScreenLoadingTrace; + + expect( + actualUiTrace?.didStartScreenLoading, + false, + ); + expect( + actualScreenLoadingTrace, + null, + ); + verify( + mInstabugLogger.e( + 'Screen loading monitoring is disabled, skipping starting screen loading monitoring for screen: $screenName.\n' + 'Please refer to the documentation for how to enable screen loading monitoring on your app: https://docs.instabug.com/docs/flutter-apm-screen-loading#disablingenabling-screen-loading-tracking ' + "If Screen Loading is enabled but you're still seeing this message, please reach out to support.", + tag: APM.tag, + ), + ).called(1); + }); + + test( + '[startScreenLoadingTrace] with screen loading enabled on Android should do nothing', + () async { + ScreenLoadingManager.setInstance(mScreenLoadingManager); + when(FlagsConfig.screenLoading.isEnabled()) + .thenAnswer((_) async => false); + when(IBGBuildInfo.I.isIOS).thenReturn(false); + + await ScreenLoadingManager.I.startScreenLoadingTrace(screenLoadingTrace); + + final actualUiTrace = ScreenLoadingManager.I.currentUiTrace; + final actualScreenLoadingTrace = + ScreenLoadingManager.I.currentScreenLoadingTrace; + + expect( + actualUiTrace?.didStartScreenLoading, + false, + ); + expect( + actualScreenLoadingTrace, + null, + ); + verify(mApmHost.isScreenLoadingEnabled()).called(1); + }); + + test( + '[startScreenLoadingTrace] with screen loading enabled with different screen should log error', + () async { + const isSameScreen = false; + when(FlagsConfig.screenLoading.isEnabled()).thenAnswer((_) async => true); + when(IBGBuildInfo.I.isIOS).thenReturn(false); + when( + RouteMatcher.I.match( + routePath: screenName, + actualPath: screenName, + ), + ).thenReturn(isSameScreen); + + await ScreenLoadingManager.I.startScreenLoadingTrace(screenLoadingTrace); + + final actualUiTrace = ScreenLoadingManager.I.currentUiTrace; + final actualScreenLoadingTrace = + ScreenLoadingManager.I.currentScreenLoadingTrace; + + expect( + actualUiTrace?.didStartScreenLoading, + false, + ); + expect( + actualScreenLoadingTrace, + null, + ); + verify( + mInstabugLogger.d( + argThat(contains('failed to start screen loading trace')), + tag: APM.tag, + ), + ).called(1); + }); + + test( + '[startScreenLoadingTrace] with screen loading enabled should start a new UI Trace', + () async { + const isSameScreen = true; + mScreenLoadingManager = ScreenLoadingManagerNoResets.init(); + when(FlagsConfig.screenLoading.isEnabled()).thenAnswer((_) async => true); + when(IBGBuildInfo.I.isIOS).thenReturn(false); + when(mDateTime.now()).thenReturn(time); + when( + RouteMatcher.I.match( + routePath: screenName, + actualPath: screenName, + ), + ).thenReturn(isSameScreen); + + await ScreenLoadingManager.I.startScreenLoadingTrace(screenLoadingTrace); + + final actualUiTrace = ScreenLoadingManager.I.currentUiTrace; + + expect( + actualUiTrace?.screenName, + screenName, + ); + expect( + actualUiTrace?.traceId, + traceId, + ); + expect( + actualUiTrace?.didStartScreenLoading, + true, + ); + verify( + mInstabugLogger.d( + argThat(contains('starting screen loading trace')), + tag: APM.tag, + ), + ).called(1); + }); + + test( + '[startScreenLoadingTrace] should start screen loading trace when screen loading trace matches UI trace matching screen name', + () async { + const isSameScreen = true; + const matchingScreenName = 'matching_screen_name'; + mScreenLoadingManager = ScreenLoadingManagerNoResets.init(); + when(FlagsConfig.screenLoading.isEnabled()).thenAnswer((_) async => true); + when(IBGBuildInfo.I.isIOS).thenReturn(false); + when(mDateTime.now()).thenReturn(time); + + // Match on matching screen name + when( + RouteMatcher.I.match( + routePath: screenName, + actualPath: matchingScreenName, + ), + ).thenReturn(isSameScreen); + + when( + RouteMatcher.I.match( + routePath: screenName, + actualPath: screenName, + ), + ).thenReturn(!isSameScreen); + + ScreenLoadingManager.I.currentUiTrace = uiTrace.copyWith( + matchingScreenName: matchingScreenName, + ); + + await ScreenLoadingManager.I.startScreenLoadingTrace(screenLoadingTrace); + + final actualUiTrace = ScreenLoadingManager.I.currentUiTrace!; + + expect( + ScreenLoadingManager.I.currentScreenLoadingTrace, + equals(screenLoadingTrace), + ); + expect( + actualUiTrace.didStartScreenLoading, + isTrue, + ); + }); + + test( + '[startScreenLoadingTrace] should not start screen loading trace when screen loading trace does not matches UI trace matching screen name', + () async { + const isSameScreen = false; + const matchingScreenName = 'matching_screen_name'; + mScreenLoadingManager = ScreenLoadingManagerNoResets.init(); + when(FlagsConfig.screenLoading.isEnabled()).thenAnswer((_) async => true); + when(IBGBuildInfo.I.isIOS).thenReturn(false); + when(mDateTime.now()).thenReturn(time); + + // Don't match on matching screen name + when( + RouteMatcher.I.match( + routePath: screenName, + actualPath: matchingScreenName, + ), + ).thenReturn(isSameScreen); + + when( + RouteMatcher.I.match( + routePath: screenName, + actualPath: screenName, + ), + ).thenReturn(!isSameScreen); + + ScreenLoadingManager.I.currentUiTrace = uiTrace.copyWith( + matchingScreenName: matchingScreenName, + ); + + await ScreenLoadingManager.I.startScreenLoadingTrace(screenLoadingTrace); + + final actualUiTrace = ScreenLoadingManager.I.currentUiTrace!; + + expect( + ScreenLoadingManager.I.currentScreenLoadingTrace, + isNull, + ); + expect( + actualUiTrace.didStartScreenLoading, + isFalse, + ); + }); + }); + + group('reportScreenLoading tests', () { + late DateTime time; + late UiTrace uiTrace; + late int traceId; + late ScreenLoadingTrace screenLoadingTrace; + int? duration; + + setUp(() { + time = DateTime.now(); + traceId = time.millisecondsSinceEpoch; + uiTrace = UiTrace(screenName: screenName, traceId: traceId); + mScreenLoadingManager.currentUiTrace = uiTrace; + when(mDateTime.now()).thenReturn(time); + screenLoadingTrace = ScreenLoadingTrace( + screenName, + startTimeInMicroseconds: time.microsecondsSinceEpoch, + startMonotonicTimeInMicroseconds: time.microsecondsSinceEpoch, + ); + mScreenLoadingManager.currentUiTrace?.didStartScreenLoading = true; + mScreenLoadingManager.currentScreenLoadingTrace = screenLoadingTrace; + mScreenLoadingManager.currentUiTrace = uiTrace; + }); + + test('[reportScreenLoading] with SDK not build should Log error', () async { + when(mInstabugHost.isBuilt()).thenAnswer((_) async => false); + + await ScreenLoadingManager.I.reportScreenLoading(screenLoadingTrace); + + final actualUiTrace = ScreenLoadingManager.I.currentUiTrace; + final actualScreenLoadingTrace = + ScreenLoadingManager.I.currentScreenLoadingTrace; + + expect( + actualUiTrace?.didReportScreenLoading, + false, + ); + expect( + actualScreenLoadingTrace?.startTimeInMicroseconds, + time.microsecondsSinceEpoch, + ); + expect( + actualScreenLoadingTrace?.endTimeInMicroseconds, + null, + ); + verify( + mInstabugLogger.e( + 'Instabug API {APM.InstabugCaptureScreenLoading} was called before the SDK is built. To build it, first by following the instructions at this link:\n' + 'https://docs.instabug.com/reference#showing-and-manipulating-the-invocation', + tag: APM.tag, + ), + ).called(1); + verifyNever(mApmHost.reportScreenLoadingCP(any, any, any)); + }); + + test( + '[reportScreenLoading] with screen loading disabled on iOS Platform should log error', + () async { + when(FlagsConfig.screenLoading.isEnabled()) + .thenAnswer((_) async => false); + when(IBGBuildInfo.I.isIOS).thenReturn(true); + + await ScreenLoadingManager.I.reportScreenLoading(screenLoadingTrace); + + final actualUiTrace = ScreenLoadingManager.I.currentUiTrace; + final actualScreenLoadingTrace = + ScreenLoadingManager.I.currentScreenLoadingTrace; + + expect( + actualUiTrace?.didReportScreenLoading, + false, + ); + expect( + actualScreenLoadingTrace?.startTimeInMicroseconds, + time.microsecondsSinceEpoch, + ); + expect( + actualScreenLoadingTrace?.endTimeInMicroseconds, + null, + ); + verify( + mInstabugLogger.e( + 'Screen loading monitoring is disabled, skipping reporting screen loading time for screen: $screenName.\n' + 'Please refer to the documentation for how to enable screen loading monitoring on your app: https://docs.instabug.com/docs/flutter-apm-screen-loading#disablingenabling-screen-loading-tracking ' + "If Screen Loading is enabled but you're still seeing this message, please reach out to support.", + tag: APM.tag, + ), + ).called(1); + verifyNever(mApmHost.reportScreenLoadingCP(any, any, any)); + }); + + test( + '[reportScreenLoading] with screen loading enabled on Android Platform should do nothing', + () async { + mScreenLoadingManager = ScreenLoadingManagerNoResets.init(); + when(FlagsConfig.screenLoading.isEnabled()) + .thenAnswer((_) async => false); + when(IBGBuildInfo.I.isIOS).thenReturn(false); + + await ScreenLoadingManager.I.reportScreenLoading( + screenLoadingTrace, + ); + + final actualUiTrace = ScreenLoadingManager.I.currentUiTrace; + final actualScreenLoadingTrace = + ScreenLoadingManager.I.currentScreenLoadingTrace; + + expect( + actualUiTrace?.didReportScreenLoading, + false, + ); + expect( + actualScreenLoadingTrace?.startTimeInMicroseconds, + time.microsecondsSinceEpoch, + ); + expect( + actualScreenLoadingTrace?.endTimeInMicroseconds, + null, + ); + verifyNever(mApmHost.reportScreenLoadingCP(any, any, any)); + }); + + test( + '[reportScreenLoading] with screen loading enabled with different screen should log error', + () async { + const isSameScreen = false; + when(FlagsConfig.screenLoading.isEnabled()).thenAnswer((_) async => true); + when(IBGBuildInfo.I.isIOS).thenReturn(false); + when( + RouteMatcher.I.match( + routePath: screenName, + actualPath: screenName, + ), + ).thenReturn(isSameScreen); + + final differentTrace = ScreenLoadingTrace( + 'different screenName', + startTimeInMicroseconds: 2500, + startMonotonicTimeInMicroseconds: 2500, + ); + + await ScreenLoadingManager.I.reportScreenLoading( + differentTrace, + ); + + final actualUiTrace = ScreenLoadingManager.I.currentUiTrace; + final actualScreenLoadingTrace = + ScreenLoadingManager.I.currentScreenLoadingTrace; + + expect( + actualUiTrace?.didReportScreenLoading, + false, + ); + expect( + actualScreenLoadingTrace?.startTimeInMicroseconds, + time.microsecondsSinceEpoch, + ); + expect( + actualScreenLoadingTrace?.startTimeInMicroseconds, + time.microsecondsSinceEpoch, + ); + verifyNever(mApmHost.reportScreenLoadingCP(any, any, any)); + verify( + mInstabugLogger.e( + "Screen Loading trace dropped as the trace isn't from the current screen, or another trace was reported before the current one. — $differentTrace", + tag: APM.tag, + ), + ); + }); + + test( + '[reportScreenLoading] with screen loading enabled and a previously reported screen loading trace should log error', + () async { + mScreenLoadingManager.currentUiTrace?.didReportScreenLoading = true; + const isSameScreen = true; + when(FlagsConfig.screenLoading.isEnabled()).thenAnswer((_) async => true); + when(IBGBuildInfo.I.isIOS).thenReturn(false); + when( + RouteMatcher.I.match( + routePath: screenName, + actualPath: screenName, + ), + ).thenReturn(isSameScreen); + + await ScreenLoadingManager.I.reportScreenLoading( + screenLoadingTrace, + ); + + final actualScreenLoadingTrace = + ScreenLoadingManager.I.currentScreenLoadingTrace; + + expect( + actualScreenLoadingTrace?.startTimeInMicroseconds, + time.microsecondsSinceEpoch, + ); + + verifyNever(mApmHost.reportScreenLoadingCP(any, any, any)); + verify( + mInstabugLogger.e( + "Screen Loading trace dropped as the trace isn't from the current screen, or another trace was reported before the current one. — $screenLoadingTrace", + tag: APM.tag, + ), + ); + }); + + test( + '[reportScreenLoading] with screen loading enabled and an invalid screenLoadingTrace should log error', + () async { + const isSameScreen = true; + when(FlagsConfig.screenLoading.isEnabled()).thenAnswer((_) async => true); + when(IBGBuildInfo.I.isIOS).thenReturn(false); + when( + mRouteMatcher.match( + routePath: anyNamed('routePath'), + actualPath: anyNamed('actualPath'), + ), + ).thenReturn(isSameScreen); + const ScreenLoadingTrace? expectedScreenLoadingTrace = null; + + await ScreenLoadingManager.I.reportScreenLoading( + expectedScreenLoadingTrace, + ); + + final actualUiTrace = ScreenLoadingManager.I.currentUiTrace; + final actualScreenLoadingTrace = + ScreenLoadingManager.I.currentScreenLoadingTrace; + + verifyNever(mApmHost.reportScreenLoadingCP(any, any, any)); + expect( + actualUiTrace?.didReportScreenLoading, + false, + ); + expect( + actualScreenLoadingTrace?.endTimeInMicroseconds, + null, + ); + verify( + mInstabugLogger.e( + "Screen Loading trace dropped as the trace isn't from the current screen, or another trace was reported before the current one. — $expectedScreenLoadingTrace", + tag: APM.tag, + ), + ); + }); + + test( + '[reportScreenLoading] with screen loading enabled and a valid trace should report it', + () async { + duration = 1000; + final endTime = time.add(Duration(microseconds: duration ?? 0)); + const isSameScreen = true; + when(FlagsConfig.screenLoading.isEnabled()).thenAnswer((_) async => true); + when(IBGBuildInfo.I.isIOS).thenReturn(false); + when( + RouteMatcher.I.match( + routePath: screenName, + actualPath: screenName, + ), + ).thenReturn(isSameScreen); + screenLoadingTrace.endTimeInMicroseconds = endTime.microsecondsSinceEpoch; + screenLoadingTrace.duration = duration; + + await ScreenLoadingManager.I.reportScreenLoading( + screenLoadingTrace, + ); + + final actualUiTrace = ScreenLoadingManager.I.currentUiTrace; + final actualScreenLoadingTrace = + ScreenLoadingManager.I.currentScreenLoadingTrace; + + expect( + actualUiTrace?.didReportScreenLoading, + true, + ); + expect( + actualScreenLoadingTrace?.endTimeInMicroseconds, + screenLoadingTrace.endTimeInMicroseconds, + ); + expect( + actualScreenLoadingTrace?.duration, + screenLoadingTrace.duration, + ); + verify( + mApmHost.reportScreenLoadingCP( + time.microsecondsSinceEpoch, + duration, + time.millisecondsSinceEpoch, + ), + ).called(1); + verify( + mInstabugLogger.d( + argThat(contains('Reporting screen loading trace')), + tag: APM.tag, + ), + ); + }); + }); + + group('endScreenLoading tests', () { + late DateTime time; + late UiTrace uiTrace; + late int traceId; + late ScreenLoadingTrace screenLoadingTrace; + late DateTime endTime; + int? duration; + late int extendedMonotonic; + + setUp(() { + time = DateTime.now(); + traceId = time.millisecondsSinceEpoch; + uiTrace = UiTrace(screenName: screenName, traceId: traceId); + duration = 1000; + extendedMonotonic = 500; + endTime = time.add(Duration(microseconds: duration ?? 0)); + mScreenLoadingManager.currentUiTrace = uiTrace; + when(mDateTime.now()).thenReturn(time); + when(mInstabugMonotonicClock.now).thenReturn(extendedMonotonic); + screenLoadingTrace = ScreenLoadingTrace( + screenName, + startTimeInMicroseconds: time.microsecondsSinceEpoch, + startMonotonicTimeInMicroseconds: time.microsecondsSinceEpoch, + ); + screenLoadingTrace.endTimeInMicroseconds = endTime.microsecondsSinceEpoch; + screenLoadingTrace.duration = duration; + mScreenLoadingManager.currentUiTrace?.didStartScreenLoading = true; + mScreenLoadingManager.currentUiTrace?.didReportScreenLoading = true; + mScreenLoadingManager.currentUiTrace = uiTrace; + mScreenLoadingManager.currentScreenLoadingTrace = screenLoadingTrace; + }); + + test('[endScreenLoading] with SDK not build should Log error', () async { + when(mInstabugHost.isBuilt()).thenAnswer((_) async => false); + + await ScreenLoadingManager.I.endScreenLoading(); + + final actualUiTrace = ScreenLoadingManager.I.currentUiTrace; + + expect( + actualUiTrace?.didExtendScreenLoading, + false, + ); + verify( + mInstabugLogger.e( + 'Instabug API {endScreenLoading} was called before the SDK is built. To build it, first by following the instructions at this link:\n' + 'https://docs.instabug.com/reference#showing-and-manipulating-the-invocation', + tag: APM.tag, + ), + ).called(1); + verifyNever(mApmHost.endScreenLoadingCP(any, any)); + }); + + test( + '[endScreenLoading] with screen loading disabled on iOS Platform should log error', + () async { + when(FlagsConfig.screenLoading.isEnabled()) + .thenAnswer((_) async => false); + when(IBGBuildInfo.I.isIOS).thenReturn(true); + + await ScreenLoadingManager.I.endScreenLoading(); + + final actualUiTrace = ScreenLoadingManager.I.currentUiTrace; + + expect( + actualUiTrace?.didExtendScreenLoading, + false, + ); + verify( + mInstabugLogger.e( + 'Screen loading monitoring is disabled, skipping ending screen loading monitoring with APM.endScreenLoading().\n' + 'Please refer to the documentation for how to enable screen loading monitoring in your app: https://docs.instabug.com/docs/flutter-apm-screen-loading#disablingenabling-screen-loading-tracking ' + "If Screen Loading is enabled but you're still seeing this message, please reach out to support.", + tag: APM.tag, + ), + ).called(1); + verifyNever(mApmHost.endScreenLoadingCP(any, any)); + }); + + test( + '[endScreenLoading] with screen loading enabled on Android Platform should do nothing', + () async { + when(FlagsConfig.screenLoading.isEnabled()) + .thenAnswer((_) async => false); + when(IBGBuildInfo.I.isIOS).thenReturn(false); + + await ScreenLoadingManager.I.endScreenLoading(); + + final actualUiTrace = ScreenLoadingManager.I.currentUiTrace; + + expect( + actualUiTrace?.didExtendScreenLoading, + false, + ); + verifyNever(mApmHost.endScreenLoadingCP(any, any)); + }); + + test('[endScreenLoading] with a previously extended trace should log error', + () async { + uiTrace.didExtendScreenLoading = true; + when(FlagsConfig.screenLoading.isEnabled()).thenAnswer((_) async => true); + when(FlagsConfig.endScreenLoading.isEnabled()) + .thenAnswer((_) async => true); + when(IBGBuildInfo.I.isIOS).thenReturn(false); + + await ScreenLoadingManager.I.endScreenLoading(); + + verify( + mInstabugLogger.e( + 'endScreenLoading has already been called for the current screen visit. Multiple calls to this API are not allowed during a single screen visit, only the first call will be considered.', + tag: APM.tag, + ), + ); + verifyNever(mApmHost.endScreenLoadingCP(any, any)); + }); + + test('[endScreenLoading] with no active screen loading should log error', + () async { + uiTrace.didStartScreenLoading = false; + mScreenLoadingManager.currentScreenLoadingTrace = null; + when(FlagsConfig.screenLoading.isEnabled()).thenAnswer((_) async => true); + when(FlagsConfig.endScreenLoading.isEnabled()) + .thenAnswer((_) async => true); + when(IBGBuildInfo.I.isIOS).thenReturn(false); + + await ScreenLoadingManager.I.endScreenLoading(); + + final actualUiTrace = ScreenLoadingManager.I.currentUiTrace; + expect( + actualUiTrace?.didExtendScreenLoading, + false, + ); + verify( + mInstabugLogger.e( + 'endScreenLoading wasn’t called as there is no active screen Loading trace.', + tag: APM.tag, + ), + ); + verifyNever(mApmHost.endScreenLoadingCP(any, any)); + }); + + test( + '[endScreenLoading] with prematurely ended screen loading should log error and End screen loading', + () async { + screenLoadingTrace = ScreenLoadingTrace( + screenName, + startTimeInMicroseconds: time.microsecondsSinceEpoch, + startMonotonicTimeInMicroseconds: time.microsecondsSinceEpoch, + ); + const prematureDuration = 0; + mScreenLoadingManager.currentScreenLoadingTrace = screenLoadingTrace; + when(FlagsConfig.screenLoading.isEnabled()).thenAnswer((_) async => true); + when(FlagsConfig.endScreenLoading.isEnabled()) + .thenAnswer((_) async => true); + when(IBGBuildInfo.I.isIOS).thenReturn(false); + + await ScreenLoadingManager.I.endScreenLoading(); + + final actualUiTrace = ScreenLoadingManager.I.currentUiTrace; + + expect( + actualUiTrace?.didExtendScreenLoading, + true, + ); + verify( + mInstabugLogger.e( + 'endScreenLoading was called too early in the Screen Loading cycle. Please make sure to call the API after the screen is done loading.', + tag: APM.tag, + ), + ); + verify(mApmHost.endScreenLoadingCP(prematureDuration, uiTrace.traceId)) + .called(1); + }); + + test('[endScreenLoading] should End screen loading', () async { + when(FlagsConfig.screenLoading.isEnabled()).thenAnswer((_) async => true); + when(FlagsConfig.endScreenLoading.isEnabled()) + .thenAnswer((_) async => true); + when(IBGBuildInfo.I.isIOS).thenReturn(false); + when(mDateTime.now()).thenReturn(time); + const startMonotonicTime = 250; + mScreenLoadingManager.currentScreenLoadingTrace + ?.startMonotonicTimeInMicroseconds = startMonotonicTime; + + await ScreenLoadingManager.I.endScreenLoading(); + + final actualUiTrace = ScreenLoadingManager.I.currentUiTrace; + + final extendedDuration = extendedMonotonic - startMonotonicTime; + final extendedEndTimeInMicroseconds = + time.microsecondsSinceEpoch + extendedDuration; + + expect( + actualUiTrace?.didStartScreenLoading, + true, + ); + expect( + actualUiTrace?.didReportScreenLoading, + true, + ); + expect( + actualUiTrace?.didExtendScreenLoading, + true, + ); + verify(mApmHost.isScreenLoadingEnabled()).called(1); + verify( + mApmHost.endScreenLoadingCP( + extendedEndTimeInMicroseconds, + uiTrace.traceId, + ), + ).called(1); + }); + }); + + group('sanitize screen name tests', () { + test('screen name equals to [/] should be replaced bu [ROOT_PAGE]', () { + const screenName = '/'; + final sanitizedScreenName = + ScreenLoadingManager.I.sanitizeScreenName(screenName); + expect(sanitizedScreenName, "ROOT_PAGE"); + }); + + test('screen name prefixed with [/] should omit [/] char', () { + const screenName = '/Home'; + final sanitizedScreenName = + ScreenLoadingManager.I.sanitizeScreenName(screenName); + expect(sanitizedScreenName, "Home"); + }); + + test('screen name suffixed with [/] should omit [/] char', () { + const screenName = '/Home'; + final sanitizedScreenName = + ScreenLoadingManager.I.sanitizeScreenName(screenName); + expect(sanitizedScreenName, "Home"); + }); + + test('screen name without [/] on edges should return the same ', () { + const screenName = 'Home'; + final sanitizedScreenName = + ScreenLoadingManager.I.sanitizeScreenName(screenName); + expect(sanitizedScreenName, "Home"); + }); + test( + 'screen name prefixed with [//] and suffixed with [/] should omit first and last[/] char', + () { + const screenName = '//Home/'; + final sanitizedScreenName = + ScreenLoadingManager.I.sanitizeScreenName(screenName); + expect(sanitizedScreenName, "/Home"); + }); + }); + + group('wrapRoutes', () { + setUp(() { + mockBuildContext = MockBuildContext(); + mockScreen = MockWidget(); + }); + test('wraps routes with InstabugCaptureScreenLoading widgets', () { + // Create a map of routes + final routes = { + '/home': (context) => mockScreen, + '/settings': (context) => mockScreen, + }; + + // Wrap the routes + final wrappedRoutes = ScreenLoadingManager.wrapRoutes(routes); + + // Verify that the routes are wrapped correctly + expect(wrappedRoutes, isA>()); + expect(wrappedRoutes.length, equals(routes.length)); + for (final route in wrappedRoutes.entries) { + expect( + route.value(mockBuildContext), + isA(), + ); + } + }); + + test('does not wrap excluded routes', () { + // Create a map of routes + final routes = { + '/home': (context) => mockScreen, + '/settings': (context) => mockScreen, + }; + + // Exclude the '/home' route + final wrappedRoutes = + ScreenLoadingManager.wrapRoutes(routes, exclude: ['/home']); + + // Verify that the '/home' route is not wrapped + expect(wrappedRoutes['/home'], equals(routes['/home'])); + + // Verify that the '/settings' route is wrapped + expect( + wrappedRoutes['/settings']?.call(mockBuildContext), + isA(), + ); + }); + + test('handles empty routes map', () { + // Create an empty map of routes + final routes = {}; + + // Wrap the routes + final wrappedRoutes = ScreenLoadingManager.wrapRoutes(routes); + + // Verify that the returned map is empty + expect(wrappedRoutes, isEmpty); + }); + + test('handles null routes map', () { + // Create a null map of routes + Map? routes; + + // Wrap the routes + final wrappedRoutes = ScreenLoadingManager.wrapRoutes(routes ?? {}); + + // Verify that the returned map is empty + expect(wrappedRoutes, isEmpty); + }); + }); +} diff --git a/test/utils/screen_name_masker_test.dart b/test/utils/screen_name_masker_test.dart new file mode 100644 index 000000000..bfdd81c98 --- /dev/null +++ b/test/utils/screen_name_masker_test.dart @@ -0,0 +1,77 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:instabug_flutter/src/utils/screen_name_masker.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + WidgetsFlutterBinding.ensureInitialized(); + + setUp(() { + ScreenNameMasker.I.setMaskingCallback(null); + }); + + test('[mask] should return same screen name when no masking callback is set', + () { + const screen = 'home'; + + final result = ScreenNameMasker.I.mask(screen); + + expect(result, equals(screen)); + }); + + test( + '[mask] should mask screen name when [setMaskingCallback] has set a callback', + () { + const screen = '/documents/314159265'; + const masked = '/documents/REDACTED'; + + ScreenNameMasker.I.setMaskingCallback((screen) { + if (screen.startsWith('/documents/')) { + return masked; + } + + return screen; + }); + + final result = ScreenNameMasker.I.mask(screen); + + expect(result, equals(masked)); + }); + + test('[mask] should fallback to "N/A" when callback returns an empty string', + () { + const fallback = 'N/A'; + const screen = '/documents/314159265'; + const masked = ''; + + ScreenNameMasker.I.setMaskingCallback((screen) { + if (screen.startsWith('/documents/')) { + return masked; + } + + return screen; + }); + + final result = ScreenNameMasker.I.mask(screen); + + expect(result, equals(fallback)); + }); + + test('[mask] should trim masked screen name', () { + const screen = '/documents/314159265'; + const masked = ' /documents/REDACTED '; + const expected = '/documents/REDACTED'; + + ScreenNameMasker.I.setMaskingCallback((screen) { + if (screen.startsWith('/documents/')) { + return masked; + } + + return screen; + }); + + final result = ScreenNameMasker.I.mask(screen); + + expect(result, equals(expected)); + }); +} diff --git a/test/w3_header_utils_test.dart b/test/w3_header_utils_test.dart new file mode 100644 index 000000000..1c10c45cf --- /dev/null +++ b/test/w3_header_utils_test.dart @@ -0,0 +1,68 @@ +import 'dart:math'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:instabug_flutter/src/models/generated_w3c_header.dart'; +import 'package:instabug_flutter/src/utils/w3c_header_utils.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'w3_header_utils_test.mocks.dart'; + +@GenerateMocks([Random]) +void main() { + final mRandom = MockRandom(); + + setUpAll(() { + W3CHeaderUtils().$setRandom(mRandom); + }); + setUp(() { + when(mRandom.nextInt(any)).thenReturn(217222); + }); + + tearDown(() { + reset(mRandom); + }); + + test('generateTracePartialId should generate a non-zero hex string', () { + var callCount = 0; + + when(mRandom.nextInt(any)).thenAnswer((_) => [0, 217222][callCount++]); + + final hexString = W3CHeaderUtils().generateTracePartialId().hexPartialId; + + expect(hexString, isNot('00000000')); + }); + + test('generateTracePartialId should return 8 chars long generated hex string', + () { + final hexString = W3CHeaderUtils().generateTracePartialId().hexPartialId; + expect(hexString.length, 8); + }); + + test( + 'generateW3CHeader should return {version}-{trace-id}-{parent-id}-{trace-flag} format header', + () { + const date = 1716210104248; + const partialId = 217222; + final hexString0 = partialId.toRadixString(16).padLeft(8, '0'); + + final expectedHeader = GeneratedW3CHeader( + timestampInSeconds: (date / 1000).floor(), + partialId: partialId, + w3cHeader: + '00-664b49b8${hexString0}664b49b8$hexString0-4942472d$hexString0-01', + ); + final generatedHeader = W3CHeaderUtils().generateW3CHeader(date); + expect(generatedHeader, expectedHeader); + }); + + test('generateW3CHeader should correctly floor the timestamp', () { + const date = 1716222912145; + final expectedHeader = GeneratedW3CHeader( + timestampInSeconds: (date / 1000).floor(), + partialId: 217222, + w3cHeader: "00-664b7bc000035086664b7bc000035086-4942472d00035086-01", + ); + final generatedHeader = W3CHeaderUtils().generateW3CHeader(date); + expect(generatedHeader, expectedHeader); + }); +} From 2efcb3fbaa51ba7cb568445eb213681fad84e675 Mon Sep 17 00:00:00 2001 From: Ahmed alaa Date: Mon, 4 Nov 2024 17:25:36 +0200 Subject: [PATCH 2/9] fix: test cases --- .../com/instabug/flutter/modules/ApmApi.java | 48 ++++++++++++++++++- .../java/com/instabug/flutter/ApmApiTest.java | 16 +++++++ ios/Classes/Util/IBGNetworkLogger+CP.h | 45 ++++++++++++++++- 3 files changed, 106 insertions(+), 3 deletions(-) diff --git a/android/src/main/java/com/instabug/flutter/modules/ApmApi.java b/android/src/main/java/com/instabug/flutter/modules/ApmApi.java index b28545745..43190631e 100644 --- a/android/src/main/java/com/instabug/flutter/modules/ApmApi.java +++ b/android/src/main/java/com/instabug/flutter/modules/ApmApi.java @@ -11,6 +11,7 @@ import com.instabug.apm.configuration.cp.FeatureAvailabilityCallback; import com.instabug.apm.model.ExecutionTrace; import com.instabug.apm.networking.APMNetworkLogger; +import com.instabug.apm.networkinterception.cp.APMCPNetworkLog; import com.instabug.flutter.generated.ApmPigeon; import com.instabug.flutter.util.Reflection; import com.instabug.flutter.util.ThreadManager; @@ -18,7 +19,6 @@ import io.flutter.plugin.common.BinaryMessenger; -import org.jetbrains.annotations.NotNull; import org.json.JSONObject; import java.lang.reflect.Method; @@ -210,10 +210,54 @@ public void networkLogAndroid(@NonNull Map data) { if (data.containsKey("serverErrorMessage")) { serverErrorMessage = (String) data.get("serverErrorMessage"); } + Boolean isW3cHeaderFound = null; + Number partialId = null; + Number networkStartTimeInSeconds = null; + String w3CGeneratedHeader = null; + String w3CCaughtHeader = null; + + if (data.containsKey("isW3cHeaderFound")) { + isW3cHeaderFound = (Boolean) data.get("isW3cHeaderFound"); + } + + if (data.containsKey("partialId")) { + + + partialId = ((Number) data.get("partialId")); + + } + if (data.containsKey("networkStartTimeInSeconds")) { + networkStartTimeInSeconds = ((Number) data.get("networkStartTimeInSeconds")); + } + + if (data.containsKey("w3CGeneratedHeader")) { + + w3CGeneratedHeader = (String) data.get("w3CGeneratedHeader"); + + } + + + if (data.containsKey("w3CCaughtHeader")) { + w3CCaughtHeader = (String) data.get("w3CCaughtHeader"); + + } + + + APMCPNetworkLog.W3CExternalTraceAttributes w3cExternalTraceAttributes = + null; + if (isW3cHeaderFound != null) { + w3cExternalTraceAttributes = new APMCPNetworkLog.W3CExternalTraceAttributes( + isW3cHeaderFound, partialId == null ? null : partialId.longValue(), + networkStartTimeInSeconds == null ? null : networkStartTimeInSeconds.longValue(), + w3CGeneratedHeader, w3CCaughtHeader + + ); + } + Method method = Reflection.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) { - method.invoke(apmNetworkLogger, requestStartTime, requestDuration, requestHeaders, requestBody, requestBodySize, requestMethod, requestUrl, requestContentType, responseHeaders, responseBody, responseBodySize, statusCode, responseContentType, errorMessage, gqlQueryName, serverErrorMessage, null); + method.invoke(apmNetworkLogger, requestStartTime, requestDuration, requestHeaders, requestBody, requestBodySize, requestMethod, requestUrl, requestContentType, responseHeaders, responseBody, responseBodySize, statusCode, responseContentType, errorMessage, gqlQueryName, serverErrorMessage, w3cExternalTraceAttributes); } else { Log.e(TAG, "APMNetworkLogger.log was not found by reflection"); } diff --git a/android/src/test/java/com/instabug/flutter/ApmApiTest.java b/android/src/test/java/com/instabug/flutter/ApmApiTest.java index 935521466..7d3f147ba 100644 --- a/android/src/test/java/com/instabug/flutter/ApmApiTest.java +++ b/android/src/test/java/com/instabug/flutter/ApmApiTest.java @@ -1,11 +1,27 @@ package com.instabug.flutter; +import static com.instabug.flutter.util.GlobalMocks.reflected; +import static com.instabug.flutter.util.MockResult.makeResult; +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockConstruction; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + import com.instabug.apm.APM; import com.instabug.apm.InternalAPM; import com.instabug.apm.configuration.cp.APMFeature; +import com.instabug.apm.InternalAPM; +import com.instabug.apm.configuration.cp.APMFeature; import com.instabug.apm.configuration.cp.FeatureAvailabilityCallback; import com.instabug.apm.model.ExecutionTrace; import com.instabug.apm.networking.APMNetworkLogger; +import com.instabug.apm.networkinterception.cp.APMCPNetworkLog; import com.instabug.flutter.generated.ApmPigeon; import com.instabug.flutter.modules.ApmApi; import com.instabug.flutter.util.GlobalMocks; diff --git a/ios/Classes/Util/IBGNetworkLogger+CP.h b/ios/Classes/Util/IBGNetworkLogger+CP.h index 764524fb2..2b4277329 100644 --- a/ios/Classes/Util/IBGNetworkLogger+CP.h +++ b/ios/Classes/Util/IBGNetworkLogger+CP.h @@ -2,7 +2,12 @@ NS_ASSUME_NONNULL_BEGIN -@interface IBGNetworkLogger (CP) + +@interface IBGNetworkLogger (PrivateAPIs) + +@property (class, atomic, assign) BOOL w3ExternalTraceIDEnabled; +@property (class, atomic, assign) BOOL w3ExternalGeneratedHeaderEnabled; +@property (class, atomic, assign) BOOL w3CaughtHeaderEnabled; + (void)disableAutomaticCapturingOfNetworkLogs; @@ -28,6 +33,44 @@ NS_ASSUME_NONNULL_BEGIN generatedW3CTraceparent:(NSString * _Nullable)generatedW3CTraceparent caughtedW3CTraceparent:(NSString * _Nullable)caughtedW3CTraceparent; ++ (void)addNetworkLogWithUrl:(NSString *_Nonnull)url + method:(NSString *_Nonnull)method + requestBody:(NSString *_Nonnull)request + requestBodySize:(int64_t)requestBodySize + responseBody:(NSString *_Nonnull)response + responseBodySize:(int64_t)responseBodySize + responseCode:(int32_t)code + requestHeaders:(NSDictionary *_Nonnull)requestHeaders + responseHeaders:(NSDictionary *_Nonnull)responseHeaders + contentType:(NSString *_Nonnull)contentType + errorDomain:(NSString *_Nullable)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)addNetworkLogWithUrl:(NSString *_Nonnull)url + method:(NSString *_Nonnull)method + requestBody:(NSString *_Nonnull)request + requestBodySize:(int64_t)requestBodySize + responseBody:(NSString *_Nonnull)response + responseBodySize:(int64_t)responseBodySize + responseCode:(int32_t)code + requestHeaders:(NSDictionary *_Nonnull)requestHeaders + responseHeaders:(NSDictionary *_Nonnull)responseHeaders + contentType:(NSString *_Nonnull)contentType + errorDomain:(NSString *_Nullable)errorDomain + errorCode:(int32_t)errorCode + startTime:(int64_t)startTime + duration:(int64_t) duration + gqlQueryName:(NSString * _Nullable)gqlQueryName; + @end NS_ASSUME_NONNULL_END From 9a62e2a46bd11313afd81ed1e2dd1106593e8b10 Mon Sep 17 00:00:00 2001 From: Ahmed alaa Date: Mon, 4 Nov 2024 17:38:18 +0200 Subject: [PATCH 3/9] fix: test cases --- .../src/main/java/com/instabug/flutter/modules/ApmApi.java | 2 -- android/src/test/java/com/instabug/flutter/ApmApiTest.java | 6 ------ 2 files changed, 8 deletions(-) diff --git a/android/src/main/java/com/instabug/flutter/modules/ApmApi.java b/android/src/main/java/com/instabug/flutter/modules/ApmApi.java index 2297ae6f0..338129a08 100644 --- a/android/src/main/java/com/instabug/flutter/modules/ApmApi.java +++ b/android/src/main/java/com/instabug/flutter/modules/ApmApi.java @@ -15,11 +15,9 @@ import com.instabug.flutter.generated.ApmPigeon; import com.instabug.flutter.util.Reflection; import com.instabug.flutter.util.ThreadManager; -import com.instabug.apm.networkinterception.cp.APMCPNetworkLog; import io.flutter.plugin.common.BinaryMessenger; -import org.jetbrains.annotations.NotNull; import org.json.JSONObject; import java.lang.reflect.Method; diff --git a/android/src/test/java/com/instabug/flutter/ApmApiTest.java b/android/src/test/java/com/instabug/flutter/ApmApiTest.java index 7e654473a..725d3bd98 100644 --- a/android/src/test/java/com/instabug/flutter/ApmApiTest.java +++ b/android/src/test/java/com/instabug/flutter/ApmApiTest.java @@ -17,14 +17,8 @@ import com.instabug.apm.InternalAPM; import com.instabug.apm.configuration.cp.APMFeature; import com.instabug.apm.configuration.cp.FeatureAvailabilityCallback; -import com.instabug.apm.InternalAPM; -import com.instabug.apm.configuration.cp.APMFeature; -import com.instabug.apm.InternalAPM; -import com.instabug.apm.configuration.cp.APMFeature; -import com.instabug.apm.configuration.cp.FeatureAvailabilityCallback; import com.instabug.apm.model.ExecutionTrace; import com.instabug.apm.networking.APMNetworkLogger; -import com.instabug.apm.networkinterception.cp.APMCPNetworkLog; import com.instabug.flutter.generated.ApmPigeon; import com.instabug.flutter.modules.ApmApi; import com.instabug.flutter.util.GlobalMocks; From 79440e2e9bcc6b9626fc1d893106dee3b345ac70 Mon Sep 17 00:00:00 2001 From: Ahmed alaa Date: Mon, 4 Nov 2024 17:46:05 +0200 Subject: [PATCH 4/9] fix: test cases --- lib/src/modules/instabug.dart | 8 -------- 1 file changed, 8 deletions(-) diff --git a/lib/src/modules/instabug.dart b/lib/src/modules/instabug.dart index 183320c58..766067df6 100644 --- a/lib/src/modules/instabug.dart +++ b/lib/src/modules/instabug.dart @@ -202,14 +202,6 @@ class Instabug { ScreenNameMasker.I.setMaskingCallback(callback); } - /// Sets a [callback] to be called wehenever a screen name is captured to mask - /// sensitive information in the screen name. - static void setScreenNameMaskingCallback( - ScreenNameMaskingCallback? callback, - ) { - ScreenNameMasker.I.setMaskingCallback(callback); - } - /// Shows the welcome message in a specific mode. /// [welcomeMessageMode] is an enum to set the welcome message mode to live, or beta. static Future showWelcomeMessageWithMode( From faf96d3bef8d18f12fc81d791b1cab0e6d1cc182 Mon Sep 17 00:00:00 2001 From: Ahmed alaa Date: Mon, 4 Nov 2024 18:26:00 +0200 Subject: [PATCH 5/9] fix: test cases --- .../com/instabug/flutter/modules/ApmApi.java | 22 +++++++++------- .../Util/NativeUtils/IBGAPM+PrivateAPIs.h | 26 ------------------- 2 files changed, 13 insertions(+), 35 deletions(-) delete mode 100644 ios/Classes/Util/NativeUtils/IBGAPM+PrivateAPIs.h diff --git a/android/src/main/java/com/instabug/flutter/modules/ApmApi.java b/android/src/main/java/com/instabug/flutter/modules/ApmApi.java index 338129a08..1ed5f825d 100644 --- a/android/src/main/java/com/instabug/flutter/modules/ApmApi.java +++ b/android/src/main/java/com/instabug/flutter/modules/ApmApi.java @@ -233,6 +233,8 @@ public void networkLogAndroid(@NonNull Map data) { w3CGeneratedHeader = (String) data.get("w3CGeneratedHeader"); + + } if (data.containsKey("w3CCaughtHeader")) { w3CCaughtHeader = (String) data.get("w3CCaughtHeader"); @@ -250,17 +252,19 @@ public void networkLogAndroid(@NonNull Map data) { ); } - Method method = Reflection.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) { - method.invoke(apmNetworkLogger, requestStartTime, requestDuration, requestHeaders, requestBody, requestBodySize, requestMethod, requestUrl, requestContentType, responseHeaders, responseBody, responseBodySize, statusCode, responseContentType, errorMessage, gqlQueryName, serverErrorMessage, w3cExternalTraceAttributes); - } else { - Log.e(TAG, "APMNetworkLogger.log was not found by reflection"); - } + Method method = Reflection.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) { + method.invoke(apmNetworkLogger, requestStartTime, requestDuration, requestHeaders, requestBody, requestBodySize, requestMethod, requestUrl, requestContentType, responseHeaders, responseBody, responseBodySize, statusCode, responseContentType, errorMessage, gqlQueryName, serverErrorMessage, w3cExternalTraceAttributes); + } else { + Log.e(TAG, "APMNetworkLogger.log was not found by reflection"); + } - } catch (Exception e) { - e.printStackTrace(); + } catch(Exception e){ + e.printStackTrace(); + } } - } + + @Override diff --git a/ios/Classes/Util/NativeUtils/IBGAPM+PrivateAPIs.h b/ios/Classes/Util/NativeUtils/IBGAPM+PrivateAPIs.h deleted file mode 100644 index 2c2158479..000000000 --- a/ios/Classes/Util/NativeUtils/IBGAPM+PrivateAPIs.h +++ /dev/null @@ -1,26 +0,0 @@ -// -// IBGAPM+PrivateAPIs.h -// Instabug -// -// Created by Yousef Hamza on 9/7/20. -// Copyright © 2020 Moataz. All rights reserved. -// - -#import -#import "IBGTimeIntervalUnits.h" - -@interface IBGAPM (PrivateAPIs) - -@property (class, atomic, assign) BOOL networkEnabled; - -/// `endScreenLoadingEnabled` will be only true if APM, screenLoadingFeature.enabled and autoUITracesUserPreference are true -@property (class, atomic, assign) BOOL endScreenLoadingEnabled; - -+ (void)startUITraceCPWithName:(NSString *)name startTimestampMUS:(IBGMicroSecondsTimeInterval)startTimestampMUS; - -+ (void)reportScreenLoadingCPWithStartTimestampMUS:(IBGMicroSecondsTimeInterval)startTimestampMUS - durationMUS:(IBGMicroSecondsTimeInterval)durationMUS; - -+ (void)endScreenLoadingCPWithEndTimestampMUS:(IBGMicroSecondsTimeInterval)endTimestampMUS; - -@end From 692c6885a470b9ca176167a7e9d166ffefbef6ed Mon Sep 17 00:00:00 2001 From: Ahmed alaa Date: Sun, 10 Nov 2024 13:18:08 +0200 Subject: [PATCH 6/9] fix: add more test cases --- lib/src/modules/network_logger.dart | 12 ------- test/network_logger_test.dart | 52 ++++++++++++++++++++++++++++- 2 files changed, 51 insertions(+), 13 deletions(-) diff --git a/lib/src/modules/network_logger.dart b/lib/src/modules/network_logger.dart index 14e524f87..920410837 100644 --- a/lib/src/modules/network_logger.dart +++ b/lib/src/modules/network_logger.dart @@ -70,18 +70,6 @@ class NetworkLogger { _manager.setOmitLogCallback(callback); } - Future networkLog(NetworkData data) async { - final w3Header = await getW3CHeader( - data.requestHeaders, - data.startTime.millisecondsSinceEpoch, - ); - if (w3Header?.isW3cHeaderFound == false && - w3Header?.w3CGeneratedHeader != null) { - data.requestHeaders['traceparent'] = w3Header?.w3CGeneratedHeader; - } - networkLogInternal(data); - } - @internal Future networkLogInternal(NetworkData data) async { final omit = await _manager.omitLog(data); diff --git a/test/network_logger_test.dart b/test/network_logger_test.dart index b7790ac1e..29ace41ca 100644 --- a/test/network_logger_test.dart +++ b/test/network_logger_test.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:math'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -21,6 +22,7 @@ import 'network_logger_test.mocks.dart'; NetworkManager, W3CHeaderUtils, FeatureFlagsManager, + Random, ]) void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -30,7 +32,7 @@ void main() { final mInstabugHost = MockInstabugHostApi(); final mBuildInfo = MockIBGBuildInfo(); final mManager = MockNetworkManager(); - + final mRandom = MockRandom(); final logger = NetworkLogger(); final data = NetworkData( url: "https://httpbin.org/get", @@ -155,4 +157,52 @@ void main() { mManager.setOmitLogCallback(callback), ).called(1); }); + + test( + '[getW3CHeader] should return null when isW3cExternalTraceIDEnabled disabled', + () async { + when(mBuildInfo.isAndroid).thenReturn(true); + + when(mInstabugHost.isW3CFeatureFlagsEnabled()).thenAnswer( + (_) => Future.value({ + "isW3cExternalTraceIDEnabled": false, + "isW3cExternalGeneratedHeaderEnabled": false, + "isW3cCaughtHeaderEnabled": false, + }), + ); + final time = DateTime.now().millisecondsSinceEpoch; + final w3cHeader = await logger.getW3CHeader({}, time); + expect(w3cHeader, null); + }); + + test( + '[getW3CHeader] should return transparent header when isW3cCaughtHeaderEnabled enabled', + () async { + when(mBuildInfo.isAndroid).thenReturn(false); + + final time = DateTime.now().millisecondsSinceEpoch; + final w3cHeader = + await logger.getW3CHeader({"traceparent": "Header test"}, time); + expect(w3cHeader!.isW3cHeaderFound, true); + expect(w3cHeader.w3CCaughtHeader, "Header test"); + }); + + test( + '[getW3CHeader] should return generated header when isW3cExternalGeneratedHeaderEnabled and no traceparent header', + () async { + W3CHeaderUtils().$setRandom(mRandom); + when(mBuildInfo.isAndroid).thenReturn(false); + + when(mRandom.nextInt(any)).thenReturn(217222); + + final time = DateTime.now().millisecondsSinceEpoch; + final w3cHeader = await logger.getW3CHeader({}, time); + final generatedW3CHeader = W3CHeaderUtils().generateW3CHeader(time); + + expect(w3cHeader!.isW3cHeaderFound, false); + expect(w3cHeader.w3CGeneratedHeader, generatedW3CHeader.w3cHeader); + expect(w3cHeader.partialId, generatedW3CHeader.partialId); + expect(w3cHeader.networkStartTimeInSeconds, + generatedW3CHeader.timestampInSeconds); + }); } From 4bdbc0f655d55bb335bc7de38c98623fb9cc2f0c Mon Sep 17 00:00:00 2001 From: Ahmed alaa Date: Sun, 10 Nov 2024 13:22:25 +0200 Subject: [PATCH 7/9] fix: add more test cases --- test/network_logger_test.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/network_logger_test.dart b/test/network_logger_test.dart index 29ace41ca..0add827d4 100644 --- a/test/network_logger_test.dart +++ b/test/network_logger_test.dart @@ -202,7 +202,9 @@ void main() { expect(w3cHeader!.isW3cHeaderFound, false); expect(w3cHeader.w3CGeneratedHeader, generatedW3CHeader.w3cHeader); expect(w3cHeader.partialId, generatedW3CHeader.partialId); - expect(w3cHeader.networkStartTimeInSeconds, - generatedW3CHeader.timestampInSeconds); + expect( + w3cHeader.networkStartTimeInSeconds, + generatedW3CHeader.timestampInSeconds, + ); }); } From 13a02e8196d17132f80d899ca6b0c2e38451a2bb Mon Sep 17 00:00:00 2001 From: Ahmed alaa Date: Sun, 10 Nov 2024 13:57:55 +0200 Subject: [PATCH 8/9] fix: add more test cases --- lib/src/modules/network_logger.dart | 12 ++++++++++++ test/network_logger_test.dart | 22 ++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/lib/src/modules/network_logger.dart b/lib/src/modules/network_logger.dart index 920410837..14e524f87 100644 --- a/lib/src/modules/network_logger.dart +++ b/lib/src/modules/network_logger.dart @@ -70,6 +70,18 @@ class NetworkLogger { _manager.setOmitLogCallback(callback); } + Future networkLog(NetworkData data) async { + final w3Header = await getW3CHeader( + data.requestHeaders, + data.startTime.millisecondsSinceEpoch, + ); + if (w3Header?.isW3cHeaderFound == false && + w3Header?.w3CGeneratedHeader != null) { + data.requestHeaders['traceparent'] = w3Header?.w3CGeneratedHeader; + } + networkLogInternal(data); + } + @internal Future networkLogInternal(NetworkData data) async { final omit = await _manager.omitLog(data); diff --git a/test/network_logger_test.dart b/test/network_logger_test.dart index 0add827d4..0dc099603 100644 --- a/test/network_logger_test.dart +++ b/test/network_logger_test.dart @@ -207,4 +207,26 @@ void main() { generatedW3CHeader.timestampInSeconds, ); }); + + test( + '[networkLog] should add transparent header when isW3cCaughtHeaderEnabled disabled to every request', + () async { + final networkData = data.copyWith(requestHeaders: {}); + when(mBuildInfo.isAndroid).thenReturn(false); + when(mManager.obfuscateLog(networkData)).thenReturn(networkData); + when(mManager.omitLog(networkData)).thenReturn(false); + await logger.networkLog(networkData); + expect(networkData.requestHeaders.containsKey('traceparent'), isTrue); + }); + + test( + '[networkLog] should not add transparent header when there is traceparent', + () async { + final networkData = data.copyWith(requestHeaders: {'traceparent': 'test'}); + when(mBuildInfo.isAndroid).thenReturn(false); + when(mManager.obfuscateLog(networkData)).thenReturn(networkData); + when(mManager.omitLog(networkData)).thenReturn(false); + await logger.networkLog(networkData); + expect(networkData.requestHeaders['traceparent'], 'test'); + }); } From e974278c64a52a50140fd0654341aef7d896f966 Mon Sep 17 00:00:00 2001 From: Ahmed alaa Date: Sun, 10 Nov 2024 14:15:41 +0200 Subject: [PATCH 9/9] fix: add more test cases --- test/network_logger_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/network_logger_test.dart b/test/network_logger_test.dart index 0dc099603..77f5de51d 100644 --- a/test/network_logger_test.dart +++ b/test/network_logger_test.dart @@ -211,7 +211,7 @@ void main() { test( '[networkLog] should add transparent header when isW3cCaughtHeaderEnabled disabled to every request', () async { - final networkData = data.copyWith(requestHeaders: {}); + final networkData = data.copyWith(requestHeaders: {}); when(mBuildInfo.isAndroid).thenReturn(false); when(mManager.obfuscateLog(networkData)).thenReturn(networkData); when(mManager.omitLog(networkData)).thenReturn(false);