diff --git a/android/build.gradle b/android/build.gradle index 0f1d8b852..3f5817b8c 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,5 +1,5 @@ group 'com.instabug.flutter' -version '15.0.2' +version '14.3.0' buildscript { repositories { @@ -17,11 +17,16 @@ rootProject.allprojects { google() mavenCentral() maven { - url 'https://oss.sonatype.org/content/repositories/snapshots' + url "https://mvn.instabug.com/nexus/repository/instabug-internal/" + credentials { + username "instabug" + password System.getenv("INSTABUG_REPOSITORY_PASSWORD") + } } } } + apply plugin: 'com.android.library' android { @@ -47,11 +52,10 @@ android { } dependencies { - api 'com.instabug.library:instabug:15.0.2' + api 'com.instabug.library:instabug:15.0.2.7020723-SNAPSHOT' 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/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index ca8c529fc..5ca64aa4c 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,3 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.8-bin.zip diff --git a/android/src/main/java/com/instabug/flutter/InstabugFlutterPlugin.java b/android/src/main/java/com/instabug/flutter/InstabugFlutterPlugin.java index bb3b043fa..3a51a4051 100644 --- a/android/src/main/java/com/instabug/flutter/InstabugFlutterPlugin.java +++ b/android/src/main/java/com/instabug/flutter/InstabugFlutterPlugin.java @@ -4,7 +4,9 @@ import android.app.Activity; import android.content.Context; import android.graphics.Bitmap; +import android.os.Build; import android.util.Log; +import android.view.Display; import android.view.View; import androidx.annotation.NonNull; @@ -73,7 +75,14 @@ public Bitmap call() { } }; - ApmApi.init(messenger); + Callable refreshRateProvider = new Callable() { + @Override + public Float call(){ + return getRefreshRate(); + } + }; + + ApmApi.init(messenger, refreshRateProvider); BugReportingApi.init(messenger); CrashReportingApi.init(messenger); FeatureRequestsApi.init(messenger); @@ -99,4 +108,20 @@ private static Bitmap takeScreenshot(FlutterRenderer renderer) { return null; } } + + private static float getRefreshRate() { + float refreshRate = 60f; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + final Display display = activity.getDisplay(); + if (display != null) { + refreshRate = display.getRefreshRate(); + } + } else { + refreshRate = activity.getWindowManager().getDefaultDisplay().getRefreshRate(); + } + + return refreshRate; + } + } 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 607c569a4..61d0fca68 100644 --- a/android/src/main/java/com/instabug/flutter/modules/ApmApi.java +++ b/android/src/main/java/com/instabug/flutter/modules/ApmApi.java @@ -12,35 +12,46 @@ import com.instabug.apm.model.ExecutionTrace; import com.instabug.apm.networking.APMNetworkLogger; import com.instabug.apm.networkinterception.cp.APMCPNetworkLog; +import com.instabug.apm.screenrendering.models.cp.IBGFrameData; +import com.instabug.apm.screenrendering.models.cp.IBGScreenRenderingData; import com.instabug.flutter.generated.ApmPigeon; import com.instabug.flutter.util.Reflection; import com.instabug.flutter.util.ThreadManager; -import io.flutter.plugin.common.BinaryMessenger; - import org.json.JSONObject; import java.lang.reflect.Method; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.concurrent.Callable; + +import io.flutter.plugin.common.BinaryMessenger; public class ApmApi implements ApmPigeon.ApmHostApi { private final String TAG = ApmApi.class.getName(); private final HashMap traces = new HashMap<>(); + private final Callable refreshRate; + + public ApmApi(Callable refreshRate) { + this.refreshRate = refreshRate; + } - public static void init(BinaryMessenger messenger) { - final ApmApi api = new ApmApi(); + public static void init(BinaryMessenger messenger, Callable refreshRateProvider) { + + final ApmApi api = new ApmApi(refreshRateProvider); ApmPigeon.ApmHostApi.setup(messenger, api); } - /** - * The function sets the enabled status of APM. - * - * @param isEnabled The `setEnabled` method in the code snippet is used to enable or disable a - * feature, and it takes a `Boolean` parameter named `isEnabled`. When this method is called with - * `true`, it enables the feature, and when called with `false`, it disables the feature. The method - * internally calls - */ + /** + * The function sets the enabled status of APM. + * + * @param isEnabled The `setEnabled` method in the code snippet is used to enable or disable a + * feature, and it takes a `Boolean` parameter named `isEnabled`. When this method is called with + * `true`, it enables the feature, and when called with `false`, it disables the feature. The method + * internally calls + */ @Override public void setEnabled(@NonNull Boolean isEnabled) { try { @@ -51,12 +62,12 @@ public void setEnabled(@NonNull Boolean isEnabled) { } /** - * Sets the cold app launch enabled status using the APM library. - * - * @param isEnabled The `isEnabled` parameter is a Boolean value that indicates whether cold app launch - * is enabled or not. When `isEnabled` is set to `true`, cold app launch is enabled, and when it is set - * to `false`, cold app launch is disabled. - */ + * Sets the cold app launch enabled status using the APM library. + * + * @param isEnabled The `isEnabled` parameter is a Boolean value that indicates whether cold app launch + * is enabled or not. When `isEnabled` is set to `true`, cold app launch is enabled, and when it is set + * to `false`, cold app launch is disabled. + */ @Override public void setColdAppLaunchEnabled(@NonNull Boolean isEnabled) { try { @@ -66,14 +77,14 @@ public void setColdAppLaunchEnabled(@NonNull Boolean isEnabled) { } } - /** - * The function sets the auto UI trace enabled status in an APM system, handling any exceptions that - * may occur. - * - * @param isEnabled The `isEnabled` parameter is a Boolean value that indicates whether the Auto UI - * trace feature should be enabled or disabled. When `isEnabled` is set to `true`, the Auto UI trace - * feature is enabled, and when it is set to `false`, the feature is disabled. - */ + /** + * The function sets the auto UI trace enabled status in an APM system, handling any exceptions that + * may occur. + * + * @param isEnabled The `isEnabled` parameter is a Boolean value that indicates whether the Auto UI + * trace feature should be enabled or disabled. When `isEnabled` is set to `true`, the Auto UI trace + * feature is enabled, and when it is set to `false`, the feature is disabled. + */ @Override public void setAutoUITraceEnabled(@NonNull Boolean isEnabled) { try { @@ -83,59 +94,56 @@ public void setAutoUITraceEnabled(@NonNull Boolean isEnabled) { } } - /** - * Starts an execution trace and handles the result - * using callbacks. - * - * @param id The `id` parameter is a non-null String that represents the identifier of the execution - * trace. - * @param name The `name` parameter in the `startExecutionTrace` method represents the name of the - * execution trace that will be started. It is used as a reference to identify the trace during - * execution monitoring. - * @param result The `result` parameter in the `startExecutionTrace` method is an instance of - * `ApmPigeon.Result`. This parameter is used to provide the result of the execution trace - * operation back to the caller. The `success` method of the `result` object is called with the - * - * @deprecated see {@link #startFlow} - */ + /** + * Starts an execution trace and handles the result + * using callbacks. + * + * @param id The `id` parameter is a non-null String that represents the identifier of the execution + * trace. + * @param name The `name` parameter in the `startExecutionTrace` method represents the name of the + * execution trace that will be started. It is used as a reference to identify the trace during + * execution monitoring. + * @param result The `result` parameter in the `startExecutionTrace` method is an instance of + * `ApmPigeon.Result`. This parameter is used to provide the result of the execution trace + * operation back to the caller. The `success` method of the `result` object is called with the + * @deprecated see {@link #startFlow} + */ @Override public void startExecutionTrace(@NonNull String id, @NonNull String name, ApmPigeon.Result result) { - ThreadManager.runOnBackground( - new Runnable() { - @Override - public void run() { - try { - ExecutionTrace trace = APM.startExecutionTrace(name); - if (trace != null) { - traces.put(id, trace); - - ThreadManager.runOnMainThread(new Runnable() { - @Override - public void run() { - result.success(id); - } - }); - } else { - ThreadManager.runOnMainThread(new Runnable() { - @Override - public void run() { - result.success(null); - } - }); + ThreadManager.runOnBackground(new Runnable() { + @Override + public void run() { + try { + ExecutionTrace trace = APM.startExecutionTrace(name); + if (trace != null) { + traces.put(id, trace); + + ThreadManager.runOnMainThread(new Runnable() { + @Override + public void run() { + result.success(id); } - } catch (Exception e) { - e.printStackTrace(); - - ThreadManager.runOnMainThread(new Runnable() { - @Override - public void run() { - result.success(null); - } - }); - } + }); + } else { + ThreadManager.runOnMainThread(new Runnable() { + @Override + public void run() { + result.success(null); + } + }); } + } catch (Exception e) { + e.printStackTrace(); + + ThreadManager.runOnMainThread(new Runnable() { + @Override + public void run() { + result.success(null); + } + }); } - ); + } + }); } /** @@ -158,7 +166,7 @@ public void startFlow(@NonNull String name) { } } - /** + /** * Sets custom attributes for AppFlow with a given name. *
* Setting an attribute value to null will remove its corresponding key if it already exists. @@ -187,7 +195,7 @@ public void setFlowAttribute(@NonNull String name, @NonNull String key, @Nullabl } } - /** + /** * Ends AppFlow with a given name. * * @param name AppFlow name to be ended. It can not be empty string or null @@ -201,13 +209,12 @@ public void endFlow(@NonNull String name) { } } - /** + /** * Adds a new attribute to trace * * @param id String id of the trace. * @param key attribute key * @param value attribute value. Null to remove attribute - * * @deprecated see {@link #setFlowAttribute} */ @Override @@ -223,7 +230,6 @@ public void setExecutionTraceAttribute(@NonNull String id, @NonNull String key, * Ends a trace * * @param id string id of the trace. - * * @deprecated see {@link #endFlow} */ @Override @@ -276,7 +282,7 @@ public void endAppLaunch() { /** * logs network-related information - * + * * @param data Map of network data object. */ @Override @@ -342,43 +348,40 @@ public void networkLogAndroid(@NonNull Map data) { } - if (data.containsKey("w3CCaughtHeader")) { - w3CCaughtHeader = (String) data.get("w3CCaughtHeader"); - - } + 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 - ); - } + 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, w3cExternalTraceAttributes); - } else { - Log.e(TAG, "APMNetworkLogger.log was not found by reflection"); - } + ); + } - } catch(Exception e){ - e.printStackTrace(); + 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(); } + } - /** - * This method is responsible for initiating a custom performance UI trace - * in the APM module. It takes three parameters: - * @param screenName: A string representing the name of the screen or UI element being traced. - * @param microTimeStamp: A number representing the timestamp in microseconds when the trace is started. - * @param traceId: A number representing the unique identifier for the trace. - */ + /** + * This method is responsible for initiating a custom performance UI trace + * in the APM module. It takes three parameters: + * + * @param screenName: A string representing the name of the screen or UI element being traced. + * @param microTimeStamp: A number representing the timestamp in microseconds when the trace is started. + * @param traceId: A number representing the unique identifier for the trace. + */ @Override public void startCpUiTrace(@NonNull String screenName, @NonNull Long microTimeStamp, @NonNull Long traceId) { try { @@ -389,16 +392,17 @@ public void startCpUiTrace(@NonNull String screenName, @NonNull Long microTimeSt } - /** - * This method is responsible for reporting the screen - * loading data from Dart side to Android side. It takes three parameters: - * @param startTimeStampMicro: A number representing the start timestamp in microseconds of the screen - * loading custom performance data. - * @param durationMicro: A number representing the duration in microseconds of the screen loading custom - * performance data. - * @param uiTraceId: A number representing the unique identifier for the UI trace associated with the - * screen loading. - */ + /** + * This method is responsible for reporting the screen + * loading data from Dart side to Android side. It takes three parameters: + * + * @param startTimeStampMicro: A number representing the start timestamp in microseconds of the screen + * loading custom performance data. + * @param durationMicro: A number representing the duration in microseconds of the screen loading custom + * performance data. + * @param uiTraceId: A number representing the unique identifier for the UI trace associated with the + * screen loading. + */ @Override public void reportScreenLoadingCP(@NonNull Long startTimeStampMicro, @NonNull Long durationMicro, @NonNull Long uiTraceId) { try { @@ -410,13 +414,14 @@ public void reportScreenLoadingCP(@NonNull Long startTimeStampMicro, @NonNull Lo /** - * This method is responsible for extend the end time if the screen loading custom - * trace. It takes two parameters: - * @param timeStampMicro: A number representing the timestamp in microseconds when the screen loading - * custom trace is ending. - * @param uiTraceId: A number representing the unique identifier for the UI trace associated with the - * screen loading. - */ + * This method is responsible for extend the end time if the screen loading custom + * trace. It takes two parameters: + * + * @param timeStampMicro: A number representing the timestamp in microseconds when the screen loading + * custom trace is ending. + * @param uiTraceId: A number representing the unique identifier for the UI trace associated with the + * screen loading. + */ @Override public void endScreenLoadingCP(@NonNull Long timeStampMicro, @NonNull Long uiTraceId) { try { @@ -428,14 +433,13 @@ public void endScreenLoadingCP(@NonNull Long timeStampMicro, @NonNull Long uiTra /** - * This method is used to check whether the end screen loading feature is enabled or not. + * This method is used to check whether the end screen loading feature is enabled or not. */ @Override public void isEndScreenLoadingEnabled(@NonNull ApmPigeon.Result result) { isScreenLoadingEnabled(result); } - @Override public void isEnabled(@NonNull ApmPigeon.Result result) { try { @@ -447,9 +451,9 @@ public void isEnabled(@NonNull ApmPigeon.Result result) { } } - /** - * checks whether the screen loading feature is enabled. - * */ + /** + * checks whether the screen loading feature is enabled. + */ @Override public void isScreenLoadingEnabled(@NonNull ApmPigeon.Result result) { try { @@ -475,4 +479,92 @@ public void setScreenLoadingEnabled(@NonNull Boolean isEnabled) { e.printStackTrace(); } } + + @Override + public void setScreenRenderEnabled(@NonNull Boolean isEnabled) { + try { + APM.setScreenRenderingEnabled(isEnabled); + } catch (Exception e) { + e.printStackTrace(); + } + } + + @Override + public void isScreenRenderEnabled(@NonNull ApmPigeon.Result result) { + try { + InternalAPM._isFeatureEnabledCP(APMFeature.SCREEN_RENDERING, "InstabugCaptureScreenRender", new FeatureAvailabilityCallback() { + @Override + public void invoke(boolean isEnabled) { + result.success(isEnabled); + } + }); + } catch (Exception e) { + e.printStackTrace(); + } + } + + @Override + public void deviceRefreshRate(@NonNull ApmPigeon.Result result) { + try { + result.success(refreshRate.call().doubleValue()); + } catch (Exception e) { + e.printStackTrace(); + } + } + + @Override + public void endScreenRenderForAutoUiTrace(@NonNull Map data) { + try { + final long traceId = ((Number) data.get("traceId")).longValue(); + final long slowFramesTotalDuration = ((Number) data.get("slowFramesTotalDuration")).longValue(); + final long frozenFramesTotalDuration = ((Number) data.get("frozenFramesTotalDuration")).longValue(); + final long endTime = ((Number) data.get("endTime")).longValue(); + + // Don't cast directly to ArrayList> because the inner lists may actually be ArrayList + // Instead, cast to List> and convert each value to long explicitly + List> rawFrames = (List>) data.get("frameData"); + ArrayList frames = new ArrayList<>(); + if (rawFrames != null) { + for (List frameValues : rawFrames) { + // Defensive: check size and nulls + if (frameValues != null && frameValues.size() >= 2) { + long frameStart = frameValues.get(0).longValue(); + long frameDuration = frameValues.get(1).longValue(); + frames.add(new IBGFrameData(frameStart, frameDuration)); + } + } + } + IBGScreenRenderingData screenRenderingData = new IBGScreenRenderingData(traceId, slowFramesTotalDuration, frozenFramesTotalDuration, frames); + InternalAPM._endAutoUiTraceWithScreenRendering(screenRenderingData, endTime); + } catch (Exception e) { + e.printStackTrace(); + } + } + + @Override + public void endScreenRenderForCustomUiTrace(@NonNull Map data) { + try { + final long traceId = ((Number) data.get("traceId")).longValue(); + final long slowFramesTotalDuration = ((Number) data.get("slowFramesTotalDuration")).longValue(); + final long frozenFramesTotalDuration = ((Number) data.get("frozenFramesTotalDuration")).longValue(); + + List> rawFrames = (List>) data.get("frameData"); + ArrayList frames = new ArrayList<>(); + if (rawFrames != null) { + for (List frameValues : rawFrames) { + // Defensive: check size and nulls + if (frameValues != null && frameValues.size() >= 2) { + long frameStart = frameValues.get(0).longValue(); + long frameDuration = frameValues.get(1).longValue(); + frames.add(new IBGFrameData(frameStart, frameDuration)); + } + } + } + IBGScreenRenderingData screenRenderingData = new IBGScreenRenderingData(traceId, slowFramesTotalDuration, frozenFramesTotalDuration, frames); + InternalAPM._endCustomUiTraceWithScreenRenderingCP(screenRenderingData); + } catch (Exception e) { + e.printStackTrace(); + } + } + } diff --git a/android/src/test/java/com/instabug/flutter/ApmApiTest.java b/android/src/test/java/com/instabug/flutter/ApmApiTest.java index 725d3bd98..ab5ec93ab 100644 --- a/android/src/test/java/com/instabug/flutter/ApmApiTest.java +++ b/android/src/test/java/com/instabug/flutter/ApmApiTest.java @@ -8,10 +8,9 @@ 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.spy; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; import com.instabug.apm.APM; import com.instabug.apm.InternalAPM; @@ -24,8 +23,6 @@ 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; @@ -36,18 +33,21 @@ import java.util.HashMap; import java.util.Map; +import java.util.concurrent.Callable; 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.*; +import io.flutter.plugin.common.BinaryMessenger; public class ApmApiTest { private final BinaryMessenger mMessenger = mock(BinaryMessenger.class); - private final ApmApi api = new ApmApi(); + private final Callable refreshRateProvider = () -> mock(Float.class); + private final ApmApi api = new ApmApi(refreshRateProvider); private MockedStatic mAPM; private MockedStatic mInternalApmStatic; private MockedStatic mHostApi; @@ -83,7 +83,7 @@ private ExecutionTrace mockTrace(String id) { public void testInit() { BinaryMessenger messenger = mock(BinaryMessenger.class); - ApmApi.init(messenger); + ApmApi.init(messenger, refreshRateProvider); mHostApi.verify(() -> ApmPigeon.ApmHostApi.setup(eq(messenger), any(ApmApi.class))); } @@ -386,4 +386,152 @@ public void testSetScreenLoadingMonitoringEnabled() { mAPM.verify(() -> APM.setScreenLoadingEnabled(isEnabled)); } + + @Test + public void testIsScreenRenderEnabled() { + + 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.isScreenRenderEnabled(result); + + mInternalApmStatic.verify(() -> InternalAPM._isFeatureEnabledCP(any(), any(), any())); + mInternalApmStatic.verifyNoMoreInteractions(); + + verify(result).success(expected); + } + + @Test + public void testSetScreenRenderEnabled() { + boolean isEnabled = false; + + api.setScreenRenderEnabled(isEnabled); + + mAPM.verify(() -> APM.setScreenRenderingEnabled(isEnabled)); + } + + @Test + public void testDeviceRefreshRate() throws Exception { + float expectedRefreshRate = 60.0f; + Double expectedResult = 60.0; + ApmPigeon.Result result = spy(makeResult((actual) -> assertEquals(expectedResult, actual))); + + // Mock the refresh rate provider to return the expected value + Callable mockRefreshRateProvider = () -> expectedRefreshRate; + ApmApi testApi = new ApmApi(mockRefreshRateProvider); + + testApi.deviceRefreshRate(result); + + verify(result).success(expectedResult); + } + + @Test + public void testDeviceRefreshRateWithException() throws Exception { + ApmPigeon.Result result = spy(makeResult((actual) -> {})); + + // Mock the refresh rate provider to throw an exception + Callable mockRefreshRateProvider = () -> { + throw new RuntimeException("Test exception"); + }; + ApmApi testApi = new ApmApi(mockRefreshRateProvider); + + testApi.deviceRefreshRate(result); + + // Verify that the method doesn't crash when an exception occurs + // The exception is caught and printed, but the result is not called + verify(result, never()).success(any()); + } + + @Test + public void testEndScreenRenderForAutoUiTrace() { + Map data = new HashMap<>(); + data.put("traceId", 123L); + data.put("slowFramesTotalDuration", 1000L); + data.put("frozenFramesTotalDuration", 2000L); + data.put("endTime", 1234567890L); + data.put("frameData", null); + + api.endScreenRenderForAutoUiTrace(data); + + mInternalApmStatic.verify(() -> InternalAPM._endAutoUiTraceWithScreenRendering(any(), eq(1234567890L))); + mInternalApmStatic.verifyNoMoreInteractions(); + } + + @Test + public void testEndScreenRenderForAutoUiTraceWithFrameData() { + Map data = new HashMap<>(); + data.put("traceId", 123L); + data.put("slowFramesTotalDuration", 1000L); + data.put("frozenFramesTotalDuration", 2000L); + data.put("endTime", 1234567890L); + + // Create frame data with ArrayList> + java.util.ArrayList> frameData = new java.util.ArrayList<>(); + java.util.ArrayList frame1 = new java.util.ArrayList<>(); + frame1.add(100L); + frame1.add(200L); + frameData.add(frame1); + + java.util.ArrayList frame2 = new java.util.ArrayList<>(); + frame2.add(300L); + frame2.add(400L); + frameData.add(frame2); + + data.put("frameData", frameData); + + api.endScreenRenderForAutoUiTrace(data); + + mInternalApmStatic.verify(() -> InternalAPM._endAutoUiTraceWithScreenRendering(any(), eq(1234567890L))); + mInternalApmStatic.verifyNoMoreInteractions(); + } + + @Test + public void testEndScreenRenderForCustomUiTrace() { + Map data = new HashMap<>(); + data.put("traceId", 123L); + data.put("slowFramesTotalDuration", 1000L); + data.put("frozenFramesTotalDuration", 2000L); + data.put("endTime", 1234567890L); + data.put("frameData", null); + + api.endScreenRenderForCustomUiTrace(data); + + mInternalApmStatic.verify(() -> InternalAPM._endCustomUiTraceWithScreenRenderingCP(any())); + mInternalApmStatic.verifyNoMoreInteractions(); + } + + @Test + public void testEndScreenRenderForCustomUiTraceWithFrameData() { + Map data = new HashMap<>(); + data.put("traceId", 123L); + data.put("slowFramesTotalDuration", 1000L); + data.put("frozenFramesTotalDuration", 2000L); + + // Create frame data with ArrayList> + java.util.ArrayList> frameData = new java.util.ArrayList<>(); + java.util.ArrayList frame1 = new java.util.ArrayList<>(); + frame1.add(100L); + frame1.add(200L); + frameData.add(frame1); + + java.util.ArrayList frame2 = new java.util.ArrayList<>(); + frame2.add(300L); + frame2.add(400L); + frameData.add(frame2); + + data.put("frameData", frameData); + + api.endScreenRenderForCustomUiTrace(data); + + mInternalApmStatic.verify(() -> InternalAPM._endCustomUiTraceWithScreenRenderingCP(any())); + mInternalApmStatic.verifyNoMoreInteractions(); + } } diff --git a/example/ios/InstabugTests/ApmApiTests.m b/example/ios/InstabugTests/ApmApiTests.m index bdb710ac7..ae634487d 100644 --- a/example/ios/InstabugTests/ApmApiTests.m +++ b/example/ios/InstabugTests/ApmApiTests.m @@ -266,5 +266,325 @@ - (void)testEndScreenLoading { OCMVerify([self.mAPM endScreenLoadingCPWithEndTimestampMUS:endScreenLoadingCPWithEndTimestampMUS]); } +- (void)testIsScreenRenderEnabled { + XCTestExpectation *expectation = [self expectationWithDescription:@"Call completion handler"]; + + BOOL isScreenRenderEnabled = YES; + OCMStub([self.mAPM isScreenRenderingOperational]).andReturn(isScreenRenderEnabled); + + [self.api isScreenRenderEnabledWithCompletion:^(NSNumber *isEnabledNumber, FlutterError *error) { + [expectation fulfill]; + + XCTAssertEqualObjects(isEnabledNumber, @(isScreenRenderEnabled)); + XCTAssertNil(error); + }]; + + [self waitForExpectations:@[expectation] timeout:5.0]; +} + +- (void)testIsScreenRenderEnabledWhenDisabled { + XCTestExpectation *expectation = [self expectationWithDescription:@"Call completion handler"]; + + BOOL isScreenRenderEnabled = NO; + OCMStub([self.mAPM isScreenRenderingOperational]).andReturn(isScreenRenderEnabled); + + [self.api isScreenRenderEnabledWithCompletion:^(NSNumber *isEnabledNumber, FlutterError *error) { + [expectation fulfill]; + + XCTAssertEqualObjects(isEnabledNumber, @(isScreenRenderEnabled)); + XCTAssertNil(error); + }]; + + [self waitForExpectations:@[expectation] timeout:5.0]; +} + +- (void)testSetScreenRenderEnabled { + NSNumber *isEnabled = @1; + FlutterError *error; + + [self.api setScreenRenderEnabledIsEnabled:isEnabled error:&error]; + + OCMVerify([self.mAPM setScreenRenderingEnabled:YES]); +} + +- (void)testSetScreenRenderDisabled { + NSNumber *isEnabled = @0; + FlutterError *error; + + [self.api setScreenRenderEnabledIsEnabled:isEnabled error:&error]; + + OCMVerify([self.mAPM setScreenRenderingEnabled:NO]); +} + +- (void)testDeviceRefreshRate { + XCTestExpectation *expectation = [self expectationWithDescription:@"Call completion handler"]; + + // Mock UIScreen for iOS 10.3+ + id mockScreen = OCMClassMock([UIScreen class]); + OCMStub([mockScreen mainScreen]).andReturn(mockScreen); + OCMStub([mockScreen maximumFramesPerSecond]).andReturn(120.0); + + [self.api deviceRefreshRateWithCompletion:^(NSNumber *refreshRate, FlutterError *error) { + [expectation fulfill]; + + XCTAssertEqualObjects(refreshRate, @(120.0)); + XCTAssertNil(error); + }]; + + [self waitForExpectations:@[expectation] timeout:5.0]; + + [mockScreen stopMocking]; +} + +- (void)testDeviceRefreshRateFallback { + XCTestExpectation *expectation = [self expectationWithDescription:@"Call completion handler"]; + + // Note: Testing the fallback behavior for iOS < 10.3 is challenging in unit tests + // since we can't easily mock the iOS version check. In a real scenario, this would + // return 60.0 for older iOS versions. For now, we'll test the normal case. + + // Mock UIScreen to return 60.0 (typical fallback value) + id mockScreen = OCMClassMock([UIScreen class]); + OCMStub([mockScreen mainScreen]).andReturn(mockScreen); + OCMStub([mockScreen maximumFramesPerSecond]).andReturn(60.0); + + [self.api deviceRefreshRateWithCompletion:^(NSNumber *refreshRate, FlutterError *error) { + [expectation fulfill]; + + XCTAssertEqualObjects(refreshRate, @(60.0)); + XCTAssertNil(error); + }]; + + [self waitForExpectations:@[expectation] timeout:5.0]; + + [mockScreen stopMocking]; +} + +- (void)testEndScreenRenderForAutoUiTrace { + FlutterError *error; + + // Create mock frame data + NSDictionary *frameData = @{ + @"frameData": @[ + @[@(1000.0), @(16.67)], // Frame 1: start time 1000.0µs, duration 16.67µs + @[@(1016.67), @(33.33)], // Frame 2: start time 1016.67µs, duration 33.33µs + @[@(1050.0), @(50.0)] // Frame 3: start time 1050.0µs, duration 50.0µs + ] + }; + + [self.api endScreenRenderForAutoUiTraceData:frameData error:&error]; + + // Verify that endAutoUITraceCPWithFrames was called + OCMVerify([self.mAPM endAutoUITraceCPWithFrames:[OCMArg checkWithBlock:^BOOL(NSArray *frames) { + // Verify that we have the correct number of frames + XCTAssertEqual(frames.count, 3); + + // Verify the first frame + IBGFrameInfo *firstFrame = frames[0]; + XCTAssertEqual(firstFrame.startTimestampInMicroseconds, 1000.0); + XCTAssertEqual(firstFrame.durationInMicroseconds, 16.67); + + // Verify the second frame + IBGFrameInfo *secondFrame = frames[1]; + XCTAssertEqual(secondFrame.startTimestampInMicroseconds, 1016.67); + XCTAssertEqual(secondFrame.durationInMicroseconds, 33.33); + + // Verify the third frame + IBGFrameInfo *thirdFrame = frames[2]; + XCTAssertEqual(thirdFrame.startTimestampInMicroseconds, 1050.0); + XCTAssertEqual(thirdFrame.durationInMicroseconds, 50.0); + + return YES; + }]]); +} + +- (void)testEndScreenRenderForCustomUiTrace { + FlutterError *error; + + // Create mock frame data + NSDictionary *frameData = @{ + @"frameData": @[ + @[@(2000.0), @(20.0)], // Frame 1: start time 2000.0µs, duration 20.0µs + @[@(2020.0), @(25.0)] // Frame 2: start time 2020.0µs, duration 25.0µs + ] + }; + + [self.api endScreenRenderForCustomUiTraceData:frameData error:&error]; + + // Verify that endCustomUITraceCPWithFrames was called + OCMVerify([self.mAPM endCustomUITraceCPWithFrames:[OCMArg checkWithBlock:^BOOL(NSArray *frames) { + // Verify that we have the correct number of frames + XCTAssertEqual(frames.count, 2); + + // Verify the first frame + IBGFrameInfo *firstFrame = frames[0]; + XCTAssertEqual(firstFrame.startTimestampInMicroseconds, 2000.0); + XCTAssertEqual(firstFrame.durationInMicroseconds, 20.0); + + // Verify the second frame + IBGFrameInfo *secondFrame = frames[1]; + XCTAssertEqual(secondFrame.startTimestampInMicroseconds, 2020.0); + XCTAssertEqual(secondFrame.durationInMicroseconds, 25.0); + + return YES; + }]]); +} + +- (void)testEndScreenRenderForAutoUiTraceWithEmptyFrameData { + FlutterError *error; + + // Create empty frame data + NSDictionary *frameData = @{ + @"frameData": @[] + }; + + [self.api endScreenRenderForAutoUiTraceData:frameData error:&error]; + + // Verify that endAutoUITraceCPWithFrames was called with empty array + OCMVerify([self.mAPM endAutoUITraceCPWithFrames:[OCMArg checkWithBlock:^BOOL(NSArray *frames) { + XCTAssertEqual(frames.count, 0); + return YES; + }]]); +} + +- (void)testEndScreenRenderForCustomUiTraceWithEmptyFrameData { + FlutterError *error; + + // Create empty frame data + NSDictionary *frameData = @{ + @"frameData": @[] + }; + + [self.api endScreenRenderForCustomUiTraceData:frameData error:&error]; + + // Verify that endCustomUITraceCPWithFrames was called with empty array + OCMVerify([self.mAPM endCustomUITraceCPWithFrames:[OCMArg checkWithBlock:^BOOL(NSArray *frames) { + XCTAssertEqual(frames.count, 0); + return YES; + }]]); +} + +- (void)testEndScreenRenderForAutoUiTraceWithMalformedFrameData { + FlutterError *error; + + // Create malformed frame data (missing values or extra values) + NSDictionary *frameData = @{ + @"frameData": @[ + @[@(1000.0)], // Frame with only one value (should be ignored) + @[@(1016.67), @(33.33)], // Valid frame + @[@(1050.0), @(50.0), @(100.0)] // Frame with extra values (should be ignored) + ] + }; + + [self.api endScreenRenderForAutoUiTraceData:frameData error:&error]; + + // Verify that endAutoUITraceCPWithFrames was called with only valid frames + OCMVerify([self.mAPM endAutoUITraceCPWithFrames:[OCMArg checkWithBlock:^BOOL(NSArray *frames) { + // Should only have 1 valid frame (first and third frames are ignored due to wrong count) + XCTAssertEqual(frames.count, 1); + + // Verify the valid frame + IBGFrameInfo *frame = frames[0]; + XCTAssertEqual(frame.startTimestampInMicroseconds, 1016.67); + XCTAssertEqual(frame.durationInMicroseconds, 33.33); + + return YES; + }]]); +} + +- (void)testEndScreenRenderForCustomUiTraceWithMalformedFrameData { + FlutterError *error; + + // Create malformed frame data (missing values) + NSDictionary *frameData = @{ + @"frameData": @[ + @[@(2000.0)], // Frame with only one value (should be ignored) + @[@(2020.0), @(25.0)] // Valid frame + ] + }; + + [self.api endScreenRenderForCustomUiTraceData:frameData error:&error]; + + // Verify that endCustomUITraceCPWithFrames was called with only valid frames + OCMVerify([self.mAPM endCustomUITraceCPWithFrames:[OCMArg checkWithBlock:^BOOL(NSArray *frames) { + // Should only have 1 valid frame + XCTAssertEqual(frames.count, 1); + + // Verify the valid frame + IBGFrameInfo *frame = frames[0]; + XCTAssertEqual(frame.startTimestampInMicroseconds, 2020.0); + XCTAssertEqual(frame.durationInMicroseconds, 25.0); + + return YES; + }]]); +} + +- (void)testEndScreenRenderForAutoUiTraceWithNilFrameData { + FlutterError *error; + + // Create frame data with nil frameData + NSDictionary *frameData = @{ + @"frameData": [NSNull null] + }; + + [self.api endScreenRenderForAutoUiTraceData:frameData error:&error]; + + // Verify that endAutoUITraceCPWithFrames was called with empty array + OCMVerify([self.mAPM endAutoUITraceCPWithFrames:[OCMArg checkWithBlock:^BOOL(NSArray *frames) { + XCTAssertEqual(frames.count, 0); + return YES; + }]]); +} + +- (void)testEndScreenRenderForCustomUiTraceWithNilFrameData { + FlutterError *error; + + // Create frame data with nil frameData + NSDictionary *frameData = @{ + @"frameData": [NSNull null] + }; + + [self.api endScreenRenderForCustomUiTraceData:frameData error:&error]; + + // Verify that endCustomUITraceCPWithFrames was called with empty array + OCMVerify([self.mAPM endCustomUITraceCPWithFrames:[OCMArg checkWithBlock:^BOOL(NSArray *frames) { + XCTAssertEqual(frames.count, 0); + return YES; + }]]); +} + +- (void)testEndScreenRenderForAutoUiTraceWithMissingFrameDataKey { + FlutterError *error; + + // Create frame data without frameData key + NSDictionary *frameData = @{ + @"otherKey": @"someValue" + }; + + [self.api endScreenRenderForAutoUiTraceData:frameData error:&error]; + + // Verify that endAutoUITraceCPWithFrames was called with empty array + OCMVerify([self.mAPM endAutoUITraceCPWithFrames:[OCMArg checkWithBlock:^BOOL(NSArray *frames) { + XCTAssertEqual(frames.count, 0); + return YES; + }]]); +} + +- (void)testEndScreenRenderForCustomUiTraceWithMissingFrameDataKey { + FlutterError *error; + + // Create frame data without frameData key + NSDictionary *frameData = @{ + @"otherKey": @"someValue" + }; + + [self.api endScreenRenderForCustomUiTraceData:frameData error:&error]; + + // Verify that endCustomUITraceCPWithFrames was called with empty array + OCMVerify([self.mAPM endCustomUITraceCPWithFrames:[OCMArg checkWithBlock:^BOOL(NSArray *frames) { + XCTAssertEqual(frames.count, 0); + return YES; + }]]); +} @end diff --git a/example/ios/Podfile b/example/ios/Podfile index 00756a1dd..9b49aa25b 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -30,7 +30,7 @@ target 'Runner' do use_frameworks! use_modular_headers! - + pod 'Instabug', :podspec => 'https://ios-releases.instabug.com/custom/faeture-screen_rendering-release/15.1.23/Instabug.podspec' flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) end diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 505c29865..43ffb07ea 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1,33 +1,35 @@ PODS: - Flutter (1.0.0) - - Instabug (15.1.1) - - instabug_flutter (15.0.2): + - Instabug (15.1.23) + - instabug_flutter (14.3.0): - Flutter - - Instabug (= 15.1.1) + - Instabug (= 15.1.23) - OCMock (3.6) DEPENDENCIES: - Flutter (from `Flutter`) + - Instabug (from `https://ios-releases.instabug.com/custom/faeture-screen_rendering-release/15.1.23/Instabug.podspec`) - instabug_flutter (from `.symlinks/plugins/instabug_flutter/ios`) - OCMock (= 3.6) SPEC REPOS: trunk: - - Instabug - OCMock EXTERNAL SOURCES: Flutter: :path: Flutter + Instabug: + :podspec: https://ios-releases.instabug.com/custom/faeture-screen_rendering-release/15.1.23/Instabug.podspec instabug_flutter: :path: ".symlinks/plugins/instabug_flutter/ios" SPEC CHECKSUMS: Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - Instabug: 3e7af445c14d7823fcdecba223f09b5f7c0c6ce1 - instabug_flutter: c5a8cb73d6c50dd193fc267b0b283087dc05c37a + Instabug: b4659339dc6f67693cf9bd1224abc66831b8722f + instabug_flutter: eeb2e13eefca00e94de1f9156df4889f5481506a OCMock: 5ea90566be239f179ba766fd9fbae5885040b992 -PODFILE CHECKSUM: 4d0aaaf6a444f68024f992999ff2c2ee26baa6ec +PODFILE CHECKSUM: 6d8ca5577997736d9cc2249886c9f6d10238385d -COCOAPODS: 1.16.2 +COCOAPODS: 1.15.2 diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 0b15932d1..3264af370 100644 --- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -26,6 +26,7 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit" shouldUseLaunchSchemeArgsEnv = "YES"> diff --git a/example/lib/main.dart b/example/lib/main.dart index 91b0a67e7..ada18a241 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,46 +1,42 @@ import 'dart:async'; +import 'dart:convert'; import 'dart:developer'; import 'dart:io'; -import 'dart:convert'; +import 'dart:math' show Random; import 'package:flutter/material.dart'; import 'package:instabug_flutter/instabug_flutter.dart'; -import 'package:instabug_flutter_example/src/components/apm_switch.dart'; -import 'package:instabug_http_client/instabug_http_client.dart'; +import 'package:instabug_flutter/src/utils/screen_loading/screen_loading_manager.dart'; import 'package:instabug_flutter_example/src/app_routes.dart'; +import 'package:instabug_flutter_example/src/utils/show_messages.dart'; import 'package:instabug_flutter_example/src/widget/nested_view.dart'; +import 'package:instabug_http_client/instabug_http_client.dart'; 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'; -part 'src/screens/crashes_page.dart'; - -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/animated_box.dart'; +part 'src/components/apm_switch.dart'; part 'src/components/fatal_crashes_content.dart'; - -part 'src/components/non_fatal_crashes_content.dart'; - +part 'src/components/flows_content.dart'; part 'src/components/network_content.dart'; - +part 'src/components/non_fatal_crashes_content.dart'; part 'src/components/page.dart'; - +part 'src/components/screen_render.dart'; +part 'src/components/screen_render_switch.dart'; part 'src/components/traces_content.dart'; - -part 'src/components/flows_content.dart'; +part 'src/components/ui_traces_content.dart'; +part 'src/screens/apm_page.dart'; +part 'src/screens/complex_page.dart'; +part 'src/screens/crashes_page.dart'; +part 'src/screens/my_home_page.dart'; +part 'src/screens/screen_capture_premature_extension_page.dart'; +part 'src/screens/screen_loading_page.dart'; +part 'src/screens/screen_render_page.dart'; void main() { runZonedGuarded( @@ -49,9 +45,12 @@ void main() { Instabug.init( token: 'ed6f659591566da19b67857e1b9d40ab', + // token: '4d75635ae06e5afb4360c04cfcf1987c', invocationEvents: [InvocationEvent.floatingButton], debugLogsLevel: LogLevel.verbose, - ); + ).then((_) { + // APM.setScreenRenderEnabled(false); + }); FlutterError.onError = (FlutterErrorDetails details) { Zone.current.handleUncaughtError(details.exception, details.stack!); diff --git a/example/lib/src/components/animated_box.dart b/example/lib/src/components/animated_box.dart new file mode 100644 index 000000000..fc0a8e362 --- /dev/null +++ b/example/lib/src/components/animated_box.dart @@ -0,0 +1,80 @@ +part of '../../main.dart'; + +class AnimatedBox extends StatefulWidget { + const AnimatedBox({Key? key}) : super(key: key); + + @override + _AnimatedBoxState createState() => _AnimatedBoxState(); +} + +class _AnimatedBoxState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _animation; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: const Duration(minutes: 1, seconds: 40), + vsync: this, + ); + _animation = Tween(begin: 0, end: 100).animate(_controller) + ..addListener(() { + setState(() { + // The state that has changed here is the animation value + }); + }); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + void _startAnimation() { + _controller.forward(); + } + + void _stopAnimation() { + _controller.stop(); + } + + void _resetAnimation() { + _controller.reset(); + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + RotationTransition( + turns: _animation, + child: const FlutterLogo(size: 100), + ), + const SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () => _startAnimation(), + child: const Text('Start'), + ), + const SizedBox(width: 20), + ElevatedButton( + onPressed: () => _stopAnimation(), + child: const Text('Stop'), + ), + const SizedBox(width: 20), + ElevatedButton( + onPressed: () => _resetAnimation(), + child: const Text('reset'), + ), + ], + ), + ], + ); + } +} diff --git a/example/lib/src/components/apm_switch.dart b/example/lib/src/components/apm_switch.dart index df8dd6123..fca7d4bb6 100644 --- a/example/lib/src/components/apm_switch.dart +++ b/example/lib/src/components/apm_switch.dart @@ -1,6 +1,4 @@ -import 'package:flutter/material.dart'; -import 'package:instabug_flutter/instabug_flutter.dart'; -import 'package:instabug_flutter_example/src/utils/show_messages.dart'; +part of '../../main.dart'; class APMSwitch extends StatefulWidget { const APMSwitch({Key? key}) : super(key: key); @@ -14,15 +12,19 @@ class _APMSwitchState extends State { @override Widget build(BuildContext context) { - return Column( - children: [ - SwitchListTile.adaptive( - title: const Text('APM Enabled'), - value: isEnabled, - onChanged: (value) => onAPMChanged(context, value), - ), - ], - ); + return FutureBuilder( + future: APM.isEnabled(), + builder: (context, snapshot) { + if (snapshot.hasData) { + isEnabled = snapshot.data ?? false; + return SwitchListTile.adaptive( + title: const Text('APM Enabled'), + value: isEnabled, + onChanged: (value) => onAPMChanged(context, value), + ); + } + return const SizedBox.shrink(); + }); } void onAPMChanged(BuildContext context, bool value) { diff --git a/example/lib/src/components/screen_render.dart b/example/lib/src/components/screen_render.dart new file mode 100644 index 000000000..2ab70282f --- /dev/null +++ b/example/lib/src/components/screen_render.dart @@ -0,0 +1,31 @@ +part of '../../main.dart'; + +class ScreenRender extends StatelessWidget { + const ScreenRender({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + const SectionTitle('Screen Render'), + InstabugButton( + text: 'Screen Render', + onPressed: () => _navigateToScreenRender(context), + ), + ], + ); + } + + _navigateToScreenRender(BuildContext context) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const InstabugCaptureScreenLoading( + screenName: ScreenRenderPage.screenName, + child: ScreenRenderPage(), + ), + settings: const RouteSettings(name: ScreenRenderPage.screenName), + ), + ); + } +} diff --git a/example/lib/src/components/screen_render_switch.dart b/example/lib/src/components/screen_render_switch.dart new file mode 100644 index 000000000..d5c6d9e18 --- /dev/null +++ b/example/lib/src/components/screen_render_switch.dart @@ -0,0 +1,35 @@ +part of '../../main.dart'; + +class ScreenRenderSwitch extends StatefulWidget { + const ScreenRenderSwitch({Key? key}) : super(key: key); + + @override + State createState() => _ScreenRenderSwitchState(); +} + +class _ScreenRenderSwitchState extends State { + bool isEnabled = false; + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: APM.isScreenRenderEnabled(), + builder: (context, snapshot) { + if (snapshot.hasData) { + isEnabled = snapshot.data ?? false; + return SwitchListTile.adaptive( + title: const Text('Screen Render Enabled'), + value: isEnabled, + onChanged: (value) => onScreenRenderChanged(context, value), + ); + } + return const SizedBox.shrink(); + }); + } + + void onScreenRenderChanged(BuildContext context, bool value) { + APM.setScreenRenderEnabled(value); + showSnackBar(context, "Screen Render is ${value ? "enabled" : "disabled"}"); + setState(() => isEnabled = value); + } +} diff --git a/example/lib/src/components/ui_traces_content.dart b/example/lib/src/components/ui_traces_content.dart new file mode 100644 index 000000000..b05144408 --- /dev/null +++ b/example/lib/src/components/ui_traces_content.dart @@ -0,0 +1,74 @@ +part of '../../main.dart'; + +class UITracesContent extends StatefulWidget { + const UITracesContent({Key? key}) : super(key: key); + + @override + State createState() => _UITracesContentState(); +} + +class _UITracesContentState extends State { + final traceNameController = TextEditingController(); + + @override + Widget build(BuildContext context) { + final textTheme = Theme.of(context).textTheme; + return Column( + children: [ + InstabugTextField( + label: 'UI Trace name', + labelStyle: textTheme.labelMedium, + controller: traceNameController, + ), + SizedBox.fromSize( + size: const Size.fromHeight(10.0), + ), + Row( + children: [ + Flexible( + flex: 5, + child: InstabugButton.smallFontSize( + text: 'Start UI Trace', + onPressed: () => _startTrace(traceNameController.text), + margin: const EdgeInsetsDirectional.only( + start: 20.0, + end: 10.0, + ), + ), + ), + Flexible( + flex: 5, + child: InstabugButton.smallFontSize( + text: 'End UI Trace', + onPressed: () => _endTrace(), + margin: const EdgeInsetsDirectional.only( + start: 10.0, + end: 20.0, + ), + ), + ), + ], + ), + ], + ); + } + + void _startTrace( + String traceName, { + int delayInMilliseconds = 0, + }) { + if (traceName.trim().isNotEmpty) { + log('_startTrace — traceName: $traceName, delay in Milliseconds: $delayInMilliseconds'); + log('traceName: $traceName'); + Future.delayed(Duration(milliseconds: delayInMilliseconds), + () => APM.startUITrace(traceName)); + } else { + log('startUITrace - Please enter a trace name'); + } + } + + void _endTrace() { + log('endUITrace - '); + APM.endUITrace(); + } +} diff --git a/example/lib/src/screens/apm_page.dart b/example/lib/src/screens/apm_page.dart index 798e906fa..b5de7a228 100644 --- a/example/lib/src/screens/apm_page.dart +++ b/example/lib/src/screens/apm_page.dart @@ -40,6 +40,8 @@ class _ApmPageState extends State { const TracesContent(), const SectionTitle('Flows'), const FlowsContent(), + const SectionTitle('Custom UI Traces'), + const UITracesContent(), const SectionTitle('Screen Loading'), SizedBox.fromSize( size: const Size.fromHeight(12), @@ -51,6 +53,7 @@ class _ApmPageState extends State { SizedBox.fromSize( size: const Size.fromHeight(12), ), + const ScreenRender(), ], ); } diff --git a/example/lib/src/screens/screen_render_page.dart b/example/lib/src/screens/screen_render_page.dart new file mode 100644 index 000000000..6c5ee315c --- /dev/null +++ b/example/lib/src/screens/screen_render_page.dart @@ -0,0 +1,60 @@ +part of '../../main.dart'; + +class ScreenRenderPage extends StatefulWidget { + const ScreenRenderPage({Key? key}) : super(key: key); + static const String screenName = "/screenRenderPageRoute"; + + @override + State createState() => _ScreenRenderPageState(); +} + +class _ScreenRenderPageState extends State { + @override + Widget build(BuildContext context) { + return Page(title: 'Screen Render', children: [ + const ScreenRenderSwitch(), + SizedBox.fromSize(size: const Size.fromHeight(16.0)), + const AnimatedBox(), + SizedBox.fromSize( + size: const Size.fromHeight(50), + ), + SizedBox.fromSize(size: const Size.fromHeight(16.0)), + InstabugButton( + text: 'Trigger Slow Frame', + onPressed: () => _simulateHeavyComputation(200), + ), + InstabugButton( + text: 'Trigger Frozen Frame', + onPressed: () => _simulateHeavyComputation(1000), + ), + InstabugButton( + text: 'Monitored Complex Page', + onPressed: () => _navigateToComplexPage(context), + ), + ]); + } + + void _navigateToComplexPage(BuildContext context) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const ComplexPage.monitored(), + settings: const RouteSettings( + name: ComplexPage.screenName, + ), + ), + ); + } + + // Simulates a computationally expensive task + _simulateHeavyComputation(int delayInMilliseconds) { + setState(() { + final startTime = DateTime.now(); + final pauseTime = delayInMilliseconds; + // Block the UI thread for ~delayInMilliseconds + while (DateTime.now().difference(startTime).inMilliseconds <= pauseTime) { + // Busy waiting + } + }); + } +} diff --git a/example/pubspec.lock b/example/pubspec.lock index 99e8f9d56..dbdfc1d49 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -5,58 +5,58 @@ packages: dependency: transitive description: name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.11.0" + version: "2.13.0" boolean_selector: dependency: transitive description: name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" characters: dependency: transitive description: name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" clock: dependency: transitive description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" collection: dependency: transitive description: name: collection - sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" url: "https://pub.dev" source: hosted - version: "1.19.0" + version: "1.19.1" fake_async: dependency: transitive description: name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.3" file: dependency: transitive description: name: file - sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "7.0.1" flutter: dependency: "direct main" description: flutter @@ -97,10 +97,10 @@ packages: dependency: transitive description: name: http_parser - sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.1.2" instabug_flutter: dependency: "direct main" description: @@ -112,26 +112,26 @@ packages: dependency: "direct main" description: name: instabug_http_client - sha256: "7d52803c0dd639f6dddbe07333418eb251ae02f3f9f4d30402517533ca692784" + sha256: "97a6ab88491eff87e42437564b528d6986a65eb3f3262f73373009f949cb4560" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.5.1" leak_tracker: dependency: transitive description: name: leak_tracker - sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" url: "https://pub.dev" source: hosted - version: "10.0.7" + version: "10.0.9" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 url: "https://pub.dev" source: hosted - version: "3.0.8" + version: "3.0.9" leak_tracker_testing: dependency: transitive description: @@ -152,10 +152,10 @@ packages: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.17" material_color_utilities: dependency: transitive description: @@ -168,34 +168,34 @@ packages: dependency: transitive description: name: meta - sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.16.0" path: dependency: transitive description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" platform: dependency: transitive description: name: platform - sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" url: "https://pub.dev" source: hosted - version: "3.1.5" + version: "3.1.6" process: dependency: transitive description: name: process - sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32" + sha256: "107d8be718f120bbba9dcd1e95e3bd325b1b4a4f07db64154635ba03f2567a0d" url: "https://pub.dev" source: hosted - version: "5.0.2" + version: "5.0.3" sky_engine: dependency: transitive description: flutter @@ -205,34 +205,34 @@ packages: dependency: transitive description: name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.10.1" stack_trace: dependency: transitive description: name: stack_trace - sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.12.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" string_scanner: dependency: transitive description: name: string_scanner - sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.1" sync_http: dependency: transitive description: @@ -245,18 +245,18 @@ packages: dependency: transitive description: name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" test_api: dependency: transitive description: name: test_api - sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd url: "https://pub.dev" source: hosted - version: "0.7.3" + version: "0.7.4" typed_data: dependency: transitive description: @@ -277,18 +277,18 @@ packages: dependency: transitive description: name: vm_service - sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 url: "https://pub.dev" source: hosted - version: "14.3.0" + version: "15.0.0" webdriver: dependency: transitive description: name: webdriver - sha256: "3d773670966f02a646319410766d3b5e1037efb7f07cc68f844d5e06cd4d61c8" + sha256: "2f3a14ca026957870cfd9c635b83507e0e51d8091568e90129fbf805aba7cade" url: "https://pub.dev" source: hosted - version: "3.0.4" + version: "3.1.0" sdks: - dart: ">=3.5.0 <4.0.0" + dart: ">=3.7.0-0 <4.0.0" flutter: ">=3.18.0-18.0.pre.54" diff --git a/ios/Classes/Modules/ApmApi.m b/ios/Classes/Modules/ApmApi.m index c6295ce67..4787a9b22 100644 --- a/ios/Classes/Modules/ApmApi.m +++ b/ios/Classes/Modules/ApmApi.m @@ -76,7 +76,7 @@ - (void)setAutoUITraceEnabledIsEnabled:(NSNumber *)isEnabled error:(FlutterError // Deprecated - see [startFlowName, setFlowAttributeName & endFlowName]. - (void)startExecutionTraceId:(NSString *)id name:(NSString *)name completion:(void(^)(NSString *_Nullable, FlutterError *_Nullable))completion { IBGExecutionTrace *trace = [IBGAPM startExecutionTraceWithName:name]; - + if (trace != nil) { [traces setObject:trace forKey:id]; return completion(id, nil); @@ -91,7 +91,7 @@ - (void)startExecutionTraceId:(NSString *)id name:(NSString *)name completion:(v // Deprecated - see [startFlowName, setFlowAttributeName & endFlowName]. - (void)setExecutionTraceAttributeId:(NSString *)id key:(NSString *)key value:(NSString *)value error:(FlutterError *_Nullable *_Nonnull)error { IBGExecutionTrace *trace = [traces objectForKey:id]; - + if (trace != nil) { [trace setAttributeWithKey:key value:value]; } @@ -103,7 +103,7 @@ - (void)setExecutionTraceAttributeId:(NSString *)id key:(NSString *)key value:(N // Deprecated - see [startFlowName, setFlowAttributeName & endFlowName]. - (void)endExecutionTraceId:(NSString *)id error:(FlutterError *_Nullable *_Nonnull)error { IBGExecutionTrace *trace = [traces objectForKey:id]; - + if (trace != nil) { [trace end]; } @@ -130,13 +130,13 @@ - (void)endFlowName:(nonnull NSString *)name error:(FlutterError * _Nullable __a [IBGAPM endFlowWithName:name]; } -// This method is responsible for starting a UI trace with the given `name`. +// This method is responsible for starting a UI trace with the given `name`. // Which initiates the tracking of user interface interactions for monitoring the performance of the application. - (void)startUITraceName:(NSString *)name error:(FlutterError *_Nullable *_Nonnull)error { [IBGAPM startUITraceWithName:name]; } -// The method is responsible for ending the currently active UI trace. +// The method is responsible for ending the currently active UI trace. // Which signifies the completion of tracking user interface interactions. - (void)endUITraceWithError:(FlutterError *_Nullable *_Nonnull)error { [IBGAPM endUITrace]; @@ -197,5 +197,63 @@ - (void)isEndScreenLoadingEnabledWithCompletion:(nonnull void (^)(NSNumber * _Nu completion(isEnabledNumber, nil); } +- (void)isScreenRenderEnabledWithCompletion:(void (^)(NSNumber * _Nullable, FlutterError * _Nullable))completion{ + BOOL isScreenRenderEnabled = IBGAPM.isScreenRenderingOperational; + NSNumber *isEnabledNumber = @(isScreenRenderEnabled); + completion(isEnabledNumber, nil); +} + +- (void)setScreenRenderEnabledIsEnabled:(nonnull NSNumber *)isEnabled error:(FlutterError * _Nullable __autoreleasing * _Nonnull)error { + [IBGAPM setScreenRenderingEnabled:[isEnabled boolValue]]; + +} + +- (void)deviceRefreshRateWithCompletion:(void (^)(NSNumber * _Nullable, FlutterError * _Nullable))completion{ + if (@available(iOS 10.3, *)) { + double refreshRate = [UIScreen mainScreen].maximumFramesPerSecond; + completion(@(refreshRate) ,nil); + } else { + // Fallback for very old iOS versions. + completion(@(60.0) , nil); + } +} + +- (void)endScreenRenderForAutoUiTraceData:(nonnull NSDictionary *)data error:(FlutterError * _Nullable __autoreleasing * _Nonnull)error { + NSArray *> *rawFrames = data[@"frameData"]; + NSMutableArray *frameInfos = [[NSMutableArray alloc] init]; + + if (rawFrames && [rawFrames isKindOfClass:[NSArray class]]) { + for (NSArray *frameValues in rawFrames) { + if ([frameValues count] == 2) { + IBGFrameInfo *frameInfo = [[IBGFrameInfo alloc] init]; + frameInfo.startTimestampInMicroseconds = [frameValues[0] doubleValue]; + frameInfo.durationInMicroseconds = [frameValues[1] doubleValue]; + [frameInfos addObject:frameInfo]; + } + } + } + [IBGAPM endAutoUITraceCPWithFrames:frameInfos]; +} + + +- (void)endScreenRenderForCustomUiTraceData:(nonnull NSDictionary *)data error:(FlutterError * _Nullable __autoreleasing * _Nonnull)error { + NSArray *> *rawFrames = data[@"frameData"]; + NSMutableArray *frameInfos = [[NSMutableArray alloc] init]; + + if (rawFrames && [rawFrames isKindOfClass:[NSArray class]]) { + for (NSArray *frameValues in rawFrames) { + if ([frameValues count] == 2) { + IBGFrameInfo *frameInfo = [[IBGFrameInfo alloc] init]; + frameInfo.startTimestampInMicroseconds = [frameValues[0] doubleValue]; + frameInfo.durationInMicroseconds = [frameValues[1] doubleValue]; + [frameInfos addObject:frameInfo]; + } + } + } + + [IBGAPM endCustomUITraceCPWithFrames:frameInfos]; +} + + @end diff --git a/ios/Classes/Util/IBGAPM+PrivateAPIs.h b/ios/Classes/Util/IBGAPM+PrivateAPIs.h index 22207d45b..c562e629e 100644 --- a/ios/Classes/Util/IBGAPM+PrivateAPIs.h +++ b/ios/Classes/Util/IBGAPM+PrivateAPIs.h @@ -8,6 +8,7 @@ #import #import "IBGTimeIntervalUnits.h" +#import @interface IBGAPM (PrivateAPIs) @@ -15,6 +16,8 @@ /// `endScreenLoadingEnabled` will be only true if APM, screenLoadingFeature.enabled and autoUITracesUserPreference are true @property (class, atomic, assign) BOOL endScreenLoadingEnabled; ++ (void)setScreenRenderingEnabled:(BOOL)enabled; + + (void)startUITraceCPWithName:(NSString *)name startTimestampMUS:(IBGMicroSecondsTimeInterval)startTimestampMUS; + (void)reportScreenLoadingCPWithStartTimestampMUS:(IBGMicroSecondsTimeInterval)startTimestampMUS @@ -22,4 +25,10 @@ + (void)endScreenLoadingCPWithEndTimestampMUS:(IBGMicroSecondsTimeInterval)endTimestampMUS; ++ (BOOL)isScreenRenderingOperational; + ++ (void)endAutoUITraceCPWithFrames:(nullable NSArray *)frames; + ++ (void)endCustomUITraceCPWithFrames:(nullable NSArray *)frames; + @end diff --git a/ios/instabug_flutter.podspec b/ios/instabug_flutter.podspec index 785f1c07b..b4625d3ad 100644 --- a/ios/instabug_flutter.podspec +++ b/ios/instabug_flutter.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'instabug_flutter' - s.version = '15.0.2' + s.version = '14.3.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 "InstabugSDK"'} s.dependency 'Flutter' - s.dependency 'Instabug', '15.1.1' + s.dependency 'Instabug', '15.1.23' end diff --git a/lib/instabug_flutter.dart b/lib/instabug_flutter.dart index e38545897..d2df80429 100644 --- a/lib/instabug_flutter.dart +++ b/lib/instabug_flutter.dart @@ -5,7 +5,6 @@ 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'; @@ -20,5 +19,5 @@ 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; +export 'src/utils/ui_trace/route_matcher.dart'; diff --git a/lib/src/models/instabug_frame_data.dart b/lib/src/models/instabug_frame_data.dart new file mode 100644 index 000000000..d4d31c709 --- /dev/null +++ b/lib/src/models/instabug_frame_data.dart @@ -0,0 +1,20 @@ +class InstabugFrameData { + int startTimeTimestamp; + int duration; + + InstabugFrameData(this.startTimeTimestamp, this.duration); + + @override + String toString() => "start time: $startTimeTimestamp, duration: $duration"; + + @override + // ignore: hash_and_equals + bool operator ==(covariant InstabugFrameData other) { + if (identical(this, other)) return true; + return startTimeTimestamp == other.startTimeTimestamp && + duration == other.duration; + } + + /// Serializes the object to a List for efficient channel transfer. + List toList() => [startTimeTimestamp, duration]; +} diff --git a/lib/src/models/instabug_screen_render_data.dart b/lib/src/models/instabug_screen_render_data.dart new file mode 100644 index 000000000..ad9e5fe8a --- /dev/null +++ b/lib/src/models/instabug_screen_render_data.dart @@ -0,0 +1,65 @@ +import 'package:flutter/foundation.dart'; +import 'package:instabug_flutter/src/models/instabug_frame_data.dart'; +import 'package:instabug_flutter/src/utils/ibg_date_time.dart'; + +class InstabugScreenRenderData { + int traceId; + int slowFramesTotalDurationMicro; + int frozenFramesTotalDurationMicro; + int? endTimeMicro; + List frameData; + + InstabugScreenRenderData({ + this.slowFramesTotalDurationMicro = 0, + this.frozenFramesTotalDurationMicro = 0, + this.endTimeMicro, + required this.frameData, + this.traceId = -1, + }); + + bool get isEmpty => frameData.isEmpty; + + bool get isNotEmpty => frameData.isNotEmpty; + + bool get isActive => traceId != -1; + + void clear() { + traceId = -1; + frozenFramesTotalDurationMicro = 0; + slowFramesTotalDurationMicro = 0; + frameData.clear(); + } + + void saveEndTime() => + endTimeMicro = IBGDateTime.I.now().microsecondsSinceEpoch; + + @override + String toString() => '\nTraceId: $traceId\n' + 'SlowFramesTotalDuration: $slowFramesTotalDurationMicro\n' + 'FrozenFramesTotalDuration: $frozenFramesTotalDurationMicro\n' + 'EndTime: $endTimeMicro\n' + 'FrameData: [${frameData.map((element) => '$element')}]'; + + @override + // ignore: hash_and_equals + bool operator ==(covariant InstabugScreenRenderData other) { + if (identical(this, other)) return true; + return traceId == other.traceId && + slowFramesTotalDurationMicro == other.slowFramesTotalDurationMicro && + frozenFramesTotalDurationMicro == + other.frozenFramesTotalDurationMicro && + listEquals(frameData, other.frameData); + } + + /// Serializes the object to a Map for efficient channel transfer. + Map toMap() { + return { + 'traceId': traceId, + 'slowFramesTotalDuration': slowFramesTotalDurationMicro, + 'frozenFramesTotalDuration': frozenFramesTotalDurationMicro, + 'endTime': endTimeMicro, + // Convert List to List> + 'frameData': frameData.map((frame) => frame.toList()).toList(), + }; + } +} diff --git a/lib/src/modules/apm.dart b/lib/src/modules/apm.dart index a9f6e0a7c..05b2298c8 100644 --- a/lib/src/modules/apm.dart +++ b/lib/src/modules/apm.dart @@ -2,14 +2,17 @@ import 'dart:async'; -import 'package:flutter/widgets.dart' show WidgetBuilder; +import 'package:flutter/widgets.dart' show WidgetBuilder, WidgetsBinding; import 'package:instabug_flutter/src/generated/apm.api.g.dart'; +import 'package:instabug_flutter/src/models/instabug_screen_render_data.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:instabug_flutter/src/utils/screen_rendering/instabug_screen_render_manager.dart'; +import 'package:instabug_flutter/src/utils/ui_trace/flags_config.dart'; import 'package:meta/meta.dart'; class APM { @@ -189,7 +192,18 @@ class APM { /// Returns: /// The method is returning a `Future`. static Future startUITrace(String name) async { - return _host.startUITrace(name); + return _host.startUITrace(name).then( + (_) async { + // Start screen render collector for custom ui trace if enabled. + if (await FlagsConfig.screenRendering.isEnabled()) { + InstabugScreenRenderManager.I.endScreenRenderCollector(); + + // final uiTraceId = IBGDateTime.I.now().millisecondsSinceEpoch; + InstabugScreenRenderManager.I + .startScreenRenderCollectorForTraceId(0, UiTraceType.custom); + } + }, + ); } /// The [endUITrace] function ends a UI trace. @@ -197,6 +211,11 @@ class APM { /// Returns: /// The method is returning a `Future`. static Future endUITrace() async { + // End screen render collector for custom ui trace if enabled. + if (InstabugScreenRenderManager.I.screenRenderEnabled) { + return InstabugScreenRenderManager.I.endScreenRenderCollector(); + } + return _host.endUITrace(); } @@ -346,4 +365,74 @@ class APM { }) { return ScreenLoadingManager.wrapRoutes(routes, exclude: exclude); } + + /// Returns a Future indicating whether the screen + /// render is enabled. + /// + /// Returns: + /// A Future is being returned. + @internal + static Future isScreenRenderEnabled() async { + return _host.isScreenRenderEnabled(); + } + + /// Retrieve the device refresh rate from native side . + /// + /// Returns: + /// A Future that represent the refresh rate. + @internal + static Future getDeviceRefreshRate() { + return _host.deviceRefreshRate(); + } + + /// Sets the screen Render state based on the provided boolean value. + /// + /// Args: + /// isEnabled (bool): The [isEnabled] parameter is a boolean value that determines whether screen + /// Render is enabled or disabled. If [isEnabled] is `true`, screen render will be enabled; if + /// [isEnabled] is `false`, screen render will be disabled. + /// + /// Returns: + /// A Future is being returned. + static Future setScreenRenderEnabled(bool isEnabled) async { + return _host.setScreenRenderEnabled(isEnabled).then((_) async { + if (isEnabled) { + await InstabugScreenRenderManager.I.init(WidgetsBinding.instance); + } else { + InstabugScreenRenderManager.I.dispose(); + } + }); + } + + /// Ends screen rendering for + /// automatic UI tracing using data provided in `InstabugScreenRenderData` object. + /// + /// Args: + /// data (InstabugScreenRenderData): The `data` parameter in the `endScreenRenderForAutoUiTrace` + /// function is of type `InstabugScreenRenderData`. It contains information related to screen + /// rendering. + /// + /// Returns: + /// A `Future` is being returned. + static Future endScreenRenderForAutoUiTrace( + InstabugScreenRenderData data, + ) { + return _host.endScreenRenderForAutoUiTrace(data.toMap()); + } + + /// Ends the screen render for a custom + /// UI trace using data provided in `InstabugScreenRenderData`. + /// + /// Args: + /// data (InstabugScreenRenderData): The `data` parameter in the `endScreenRenderForCustomUiTrace` + /// function is of type `InstabugScreenRenderData`, which contains information related to the + /// rendering of a screen in the Instabug custom UI. + /// + /// Returns: + /// A `Future` is being returned. + static Future endScreenRenderForCustomUiTrace( + InstabugScreenRenderData data, + ) { + return _host.endScreenRenderForCustomUiTrace(data.toMap()); + } } diff --git a/lib/src/modules/instabug.dart b/lib/src/modules/instabug.dart index 6bba8ed1f..68658985c 100644 --- a/lib/src/modules/instabug.dart +++ b/lib/src/modules/instabug.dart @@ -1,4 +1,5 @@ // ignore_for_file: avoid_classes_with_only_static_members +// ignore_for_file: deprecated_member_use import 'dart:async'; @@ -11,6 +12,7 @@ import 'dart:typed_data'; 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'; @@ -21,6 +23,7 @@ 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:instabug_flutter/src/utils/screen_rendering/instabug_widget_binding_observer.dart'; import 'package:meta/meta.dart'; enum InvocationEvent { @@ -186,11 +189,13 @@ class Instabug { }) async { $setup(); InstabugLogger.I.logLevel = debugLogsLevel; + checkForWidgetBinding(); await _host.init( token, invocationEvents.mapToString(), debugLogsLevel.toString(), ); + return FeatureFlagsManager().registerW3CFlagsListener(); } diff --git a/lib/src/utils/instabug_navigator_observer.dart b/lib/src/utils/instabug_navigator_observer.dart index d9d6b02db..b20b56b89 100644 --- a/lib/src/utils/instabug_navigator_observer.dart +++ b/lib/src/utils/instabug_navigator_observer.dart @@ -1,3 +1,6 @@ +import 'dart:async'; +import 'dart:developer'; + import 'package:flutter/material.dart'; import 'package:instabug_flutter/instabug_flutter.dart'; import 'package:instabug_flutter/src/models/instabug_route.dart'; @@ -6,6 +9,8 @@ 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'; +import 'package:instabug_flutter/src/utils/screen_rendering/instabug_screen_render_manager.dart'; +import 'package:instabug_flutter/src/utils/ui_trace/flags_config.dart'; class InstabugNavigatorObserver extends NavigatorObserver { final List _steps = []; @@ -23,8 +28,11 @@ class InstabugNavigatorObserver extends NavigatorObserver { name: maskedScreenName, ); - // Starts a the new UI trace which is exclusive to screen loading - ScreenLoadingManager.I.startUiTrace(maskedScreenName, screenName); + InstabugScreenRenderManager.I.endScreenRenderCollector(); + ScreenLoadingManager.I + .startUiTrace(maskedScreenName, screenName) + .then(_startScreenRenderCollector); + // If there is a step that hasn't been pushed yet if (_steps.isNotEmpty) { // Report the last step and remove it from the list @@ -58,4 +66,26 @@ class InstabugNavigatorObserver extends NavigatorObserver { void didPush(Route route, Route? previousRoute) { screenChanged(route); } + + FutureOr _startScreenRenderCollector(int? uiTraceId) async { + final isScreenRenderEnabled = await FlagsConfig.screenRendering.isEnabled(); + log("isScreenRenderEnabled $isScreenRenderEnabled", name: "Andrew"); + await _checkForScreenRenderInitialization(isScreenRenderEnabled); + if (uiTraceId != null && isScreenRenderEnabled) { + InstabugScreenRenderManager.I + .startScreenRenderCollectorForTraceId(uiTraceId); + } + } + + Future _checkForScreenRenderInitialization(bool isScreenRender) async { + if (isScreenRender) { + if (!InstabugScreenRenderManager.I.screenRenderEnabled) { + await InstabugScreenRenderManager.I.init(WidgetsBinding.instance); + } + } else { + if (InstabugScreenRenderManager.I.screenRenderEnabled) { + InstabugScreenRenderManager.I.dispose(); + } + } + } } diff --git a/lib/src/utils/screen_loading/screen_loading_manager.dart b/lib/src/utils/screen_loading/screen_loading_manager.dart index b01b77627..fce8884b4 100644 --- a/lib/src/utils/screen_loading/screen_loading_manager.dart +++ b/lib/src/utils/screen_loading/screen_loading_manager.dart @@ -4,9 +4,9 @@ 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:instabug_flutter/src/utils/ui_trace/flags_config.dart'; +import 'package:instabug_flutter/src/utils/ui_trace/ui_trace.dart'; import 'package:meta/meta.dart'; /// Manages screen loading traces and UI traces for performance monitoring. @@ -139,7 +139,7 @@ class ScreenLoadingManager { /// [matchingScreenName] as the screen name used for matching the UI trace /// with a Screen Loading trace. @internal - Future startUiTrace( + Future startUiTrace( String screenName, [ String? matchingScreenName, ]) async { @@ -148,10 +148,6 @@ class ScreenLoadingManager { 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. @@ -164,7 +160,7 @@ class ScreenLoadingManager { 'https://docs.instabug.com/docs/react-native-apm-disabling-enabling', tag: APM.tag, ); - return; + return null; } final sanitizedScreenName = sanitizeScreenName(screenName); @@ -181,8 +177,10 @@ class ScreenLoadingManager { matchingScreenName: sanitizedMatchingScreenName, traceId: uiTraceId, ); + return uiTraceId; } catch (error, stackTrace) { _logExceptionErrorAndStackTrace(error, stackTrace); + return null; } } diff --git a/lib/src/utils/screen_rendering/instabug_screen_render_manager.dart b/lib/src/utils/screen_rendering/instabug_screen_render_manager.dart new file mode 100644 index 000000000..4876d0b15 --- /dev/null +++ b/lib/src/utils/screen_rendering/instabug_screen_render_manager.dart @@ -0,0 +1,430 @@ +import 'dart:async'; +import 'dart:developer' show log; +import 'dart:ui' show TimingsCallback, FrameTiming, FramePhase; + +import 'package:flutter/widgets.dart'; +import 'package:instabug_flutter/src/models/instabug_frame_data.dart'; +import 'package:instabug_flutter/src/models/instabug_screen_render_data.dart'; +import 'package:instabug_flutter/src/modules/apm.dart'; +import 'package:instabug_flutter/src/utils/instabug_logger.dart'; +import 'package:instabug_flutter/src/utils/screen_rendering/instabug_widget_binding_observer.dart'; +import 'package:meta/meta.dart'; + +@internal +enum UiTraceType { + auto, + custom, +} + +@internal +class InstabugScreenRenderManager { + WidgetsBinding? _widgetsBinding; + late int _buildTimeMs; + late int _rasterTimeMs; + late int _totalTimeMs; + late TimingsCallback _timingsCallback; + late InstabugScreenRenderData _screenRenderForAutoUiTrace; + late InstabugScreenRenderData _screenRenderForCustomUiTrace; + int _slowFramesTotalDurationMicro = 0; + int _frozenFramesTotalDurationMicro = 0; + int? _epochOffset; + bool _isTimingsListenerAttached = false; + bool screenRenderEnabled = false; + bool _isWidgetBindingObserverAdded = false; + + final List _delayedFrames = []; + + /// 1 / DeviceRefreshRate * 1000 + double _deviceRefreshRate = 60; + + /// Default refresh rate for 60 FPS displays in milliseconds (16.67ms) + double _slowFrameThresholdMs = 16.67; + + /// Default frozen frame threshold in milliseconds (700ms) + final _frozenFrameThresholdMs = 700; + + // final _microsecondsPerMillisecond = 1000; + + InstabugScreenRenderManager._(); + + static InstabugScreenRenderManager _instance = + InstabugScreenRenderManager._(); + + /// Returns the singleton instance of [InstabugScreenRenderManager]. + static InstabugScreenRenderManager get instance => _instance; + + /// Shorthand for [instance] + static InstabugScreenRenderManager get I => instance; + + /// Logging tag for debugging purposes. + static const tag = "ScreenRenderManager"; + + /// setup function for [InstabugScreenRenderManager] + @internal + Future init(WidgetsBinding? widgetBinding) async { + try { + // passing WidgetsBinding? (nullable) for flutter versions prior than 3.x + if (!screenRenderEnabled && widgetBinding != null) { + _widgetsBinding = widgetBinding; + _addWidgetBindingObserver(); + await _initStaticValues(); + _initFrameTimings(); + screenRenderEnabled = true; + } + } catch (error, stackTrace) { + _logExceptionErrorAndStackTrace(error, stackTrace); + } + } + + /// Analyze frame data to detect slow or frozen frames efficiently. + @visibleForTesting + void analyzeFrameTiming(FrameTiming frameTiming) { + _buildTimeMs = frameTiming.buildDuration.inMilliseconds; + _rasterTimeMs = frameTiming.rasterDuration.inMilliseconds; + _totalTimeMs = frameTiming.totalSpan.inMilliseconds; + + if (_isTotalTimeLarge) { + final micros = frameTiming.totalSpan.inMicroseconds; + _frozenFramesTotalDurationMicro += micros; + _onDelayedFrameDetected( + _getMicrosecondsSinceEpoch( + frameTiming.timestampInMicroseconds(FramePhase.vsyncStart), + ), + micros, + ); + return; + } + + if (_isUiSlow) { + final micros = frameTiming.buildDuration.inMicroseconds; + _slowFramesTotalDurationMicro += micros; + _onDelayedFrameDetected( + _getMicrosecondsSinceEpoch( + frameTiming.timestampInMicroseconds(FramePhase.buildStart), + ), + micros, + ); + return; + } + + if (_isRasterSlow) { + final micros = frameTiming.rasterDuration.inMicroseconds; + _slowFramesTotalDurationMicro += micros; + _onDelayedFrameDetected( + _getMicrosecondsSinceEpoch( + frameTiming.timestampInMicroseconds(FramePhase.rasterStart), + ), + micros, + ); + } + } + + /// Start collecting screen render data for the running [UITrace]. + /// It ends the running collector when starting a new one of the same type [UiTraceType]. + @internal + void startScreenRenderCollectorForTraceId( + int traceId, [ + UiTraceType type = UiTraceType.auto, + ]) { + try { + // Return if frameTimingListener not attached + if (!screenRenderEnabled || !_isTimingsListenerAttached) { + return; + } + + if (type == UiTraceType.custom) { + _screenRenderForCustomUiTrace.traceId = traceId; + } + + if (type == UiTraceType.auto) { + _screenRenderForAutoUiTrace.traceId = traceId; + } + } catch (error, stackTrace) { + _logExceptionErrorAndStackTrace(error, stackTrace); + } + } + + @internal + void endScreenRenderCollector([ + UiTraceType type = UiTraceType.auto, + ]) { + try { + // Return if frameTimingListener not attached + if (!screenRenderEnabled || !_isTimingsListenerAttached) { + return; + } + + //Save the memory cached data to be sent to native side + if (_delayedFrames.isNotEmpty) { + _saveCollectedData(); + _resetCachedFrameData(); + } + + //Sync the captured screen render data of the Custom UI trace if the collector was active + if (type == UiTraceType.custom && + _screenRenderForCustomUiTrace.isActive) { + _reportScreenRenderForCustomUiTrace(_screenRenderForCustomUiTrace); + _screenRenderForCustomUiTrace.clear(); + } + + //Sync the captured screen render data of the Auto UI trace if the collector was active + if (type == UiTraceType.auto && _screenRenderForAutoUiTrace.isActive) { + _reportScreenRenderForAutoUiTrace(_screenRenderForAutoUiTrace); + _screenRenderForAutoUiTrace.clear(); + } + } catch (error, stackTrace) { + _logExceptionErrorAndStackTrace(error, stackTrace); + } + } + + /// Stop screen render collector and sync the captured data. + @internal + void stopScreenRenderCollector() { + try { + if (_delayedFrames.isNotEmpty) { + _saveCollectedData(); + } + + // Sync Screen Render data for custom ui trace if exists + if (_screenRenderForCustomUiTrace.isActive) { + _reportScreenRenderForCustomUiTrace(_screenRenderForCustomUiTrace); + } + + // Sync Screen Render data for auto ui trace if exists + if (_screenRenderForAutoUiTrace.isActive) { + _reportScreenRenderForAutoUiTrace(_screenRenderForAutoUiTrace); + } + } catch (error, stackTrace) { + _logExceptionErrorAndStackTrace(error, stackTrace); + } + } + + /// Dispose InstabugScreenRenderManager by removing timings callback and cashed data. + void dispose() { + _resetCachedFrameData(); + _removeFrameTimings(); + _removeWidgetBindingObserver(); + _widgetsBinding = null; + screenRenderEnabled = false; + } + + /// --------------------------- private methods --------------------- + + bool get _isUiSlow => + _buildTimeMs > _slowFrameThresholdMs && + _buildTimeMs < _frozenFrameThresholdMs; + + bool get _isRasterSlow => + _rasterTimeMs > _slowFrameThresholdMs && + _rasterTimeMs < _frozenFrameThresholdMs; + + bool get _isTotalTimeLarge => _totalTimeMs >= _frozenFrameThresholdMs; + + /// Calculate the target time for the frame to be drawn in milliseconds based on the device refresh rate. + double _targetMsPerFrame(double displayRefreshRate) => + 1 / displayRefreshRate * 1000; + + /// Get device refresh rate from native side. + Future get _getDeviceRefreshRateFromNative => + APM.getDeviceRefreshRate(); + + /// add new [WidgetsBindingObserver] to track app lifecycle. + void _addWidgetBindingObserver() { + if (_widgetsBinding == null) { + return; + } + if (!_isWidgetBindingObserverAdded) { + _widgetsBinding!.addObserver(InstabugWidgetsBindingObserver.instance); + _isWidgetBindingObserverAdded = true; + } + } + + /// remove [WidgetsBindingObserver] from [WidgetsBinding] + void _removeWidgetBindingObserver() { + if (_widgetsBinding == null) { + return; + } + if (_isWidgetBindingObserverAdded) { + _widgetsBinding!.removeObserver(InstabugWidgetsBindingObserver.instance); + _isWidgetBindingObserverAdded = false; + } + } + + /// Initialize the static variables + Future _initStaticValues() async { + _timingsCallback = (timings) { + // Establish the offset on the first available timing. + _epochOffset ??= _getEpochOffset(timings.first); + + for (final frameTiming in timings) { + analyzeFrameTiming(frameTiming); + } + }; + _deviceRefreshRate = await _getDeviceRefreshRateFromNative; + _slowFrameThresholdMs = _targetMsPerFrame(_deviceRefreshRate); + _screenRenderForAutoUiTrace = InstabugScreenRenderData(frameData: []); + _screenRenderForCustomUiTrace = InstabugScreenRenderData(frameData: []); + } + + int _getEpochOffset(FrameTiming firstPatchedFrameTiming) { + return DateTime.now().microsecondsSinceEpoch - + firstPatchedFrameTiming.timestampInMicroseconds(FramePhase.vsyncStart); + } + + /// Add a frame observer by calling [WidgetsBinding.instance.addTimingsCallback] + void _initFrameTimings() { + if (_isTimingsListenerAttached) { + return; // A timings callback is already attached + } + _widgetsBinding?.addTimingsCallback(_timingsCallback); + _isTimingsListenerAttached = true; + } + + /// Remove the running frame observer by calling [_widgetsBinding.removeTimingsCallback] + void _removeFrameTimings() { + if (!_isTimingsListenerAttached) return; // No timings callback attached. + _widgetsBinding?.removeTimingsCallback(_timingsCallback); + _isTimingsListenerAttached = false; + } + + /// Reset the memory cashed data + void _resetCachedFrameData() { + _slowFramesTotalDurationMicro = 0; + _frozenFramesTotalDurationMicro = 0; + _delayedFrames.clear(); + } + + /// Save Slow/Frozen Frames data + void _onDelayedFrameDetected(int startTime, int durationInMicroseconds) { + log( + "${durationInMicroseconds >= 700000 ? "🚨Frozen" : "⚠️Slow"} Frame Detected (startTime: $startTime, duration: $durationInMicroseconds µs)", + name: tag, + ); + _delayedFrames.add( + InstabugFrameData( + startTime, + durationInMicroseconds, + ), + ); + } + + /// Ends custom ui trace with the screen render data that has been collected for it. + /// params: + /// [InstabugScreenRenderData] screenRenderData. + Future _reportScreenRenderForCustomUiTrace( + InstabugScreenRenderData screenRenderData, + ) async { + try { + log( + "reportScreenRenderForCustomUiTrace $screenRenderData", + name: tag, + ); + await APM.endScreenRenderForCustomUiTrace(screenRenderData); + return true; + } catch (error, stackTrace) { + _logExceptionErrorAndStackTrace(error, stackTrace); + return false; + } + } + + /// Ends auto ui trace with the screen render data that has been collected for it. + /// params: + /// [InstabugScreenRenderData] screenRenderData. + Future _reportScreenRenderForAutoUiTrace( + InstabugScreenRenderData screenRenderData, + ) async { + try { + // Save the end time for the running ui trace, it's only needed in Android SDK. + screenRenderData.saveEndTime(); + log( + "reportScreenRenderForAutoUiTrace $screenRenderData", + name: tag, + ); + await APM.endScreenRenderForAutoUiTrace(screenRenderData); + + return true; + } catch (error, stackTrace) { + _logExceptionErrorAndStackTrace(error, stackTrace); + return false; + } + } + + /// Add the memory cashed data to the objects that will be synced asynchronously to the native side. + void _saveCollectedData() { + if (_screenRenderForAutoUiTrace.isActive) { + _updateAutoUiData(); + } + if (_screenRenderForCustomUiTrace.isActive) { + _updateCustomUiData(); + } + } + + /// Updates the custom UI trace screen render data with the currently collected + /// frame information and durations. + /// + /// This method accumulates the total duration of slow and frozen frames (in microseconds) + /// for the custom UI trace, and appends the list of delayed frames collected so far + /// to the trace's frame data. This prepares the custom UI trace data to be reported + /// or synced with the native side. + void _updateCustomUiData() { + _screenRenderForCustomUiTrace.slowFramesTotalDurationMicro += + _slowFramesTotalDurationMicro; + _screenRenderForCustomUiTrace.frozenFramesTotalDurationMicro += + _frozenFramesTotalDurationMicro; + _screenRenderForCustomUiTrace.frameData.addAll(_delayedFrames); + } + + /// Updates the auto UI trace screen render data with the currently collected + /// frame information and durations. + /// + /// This method accumulates the total duration of slow and frozen frames (in microseconds) + /// for the auto UI trace, and appends the list of delayed frames collected so far + /// to the trace's frame data. This prepares the auto UI trace data to be reported + /// or synced with the native side. + void _updateAutoUiData() { + _screenRenderForAutoUiTrace.slowFramesTotalDurationMicro += + _slowFramesTotalDurationMicro; + _screenRenderForAutoUiTrace.frozenFramesTotalDurationMicro += + _frozenFramesTotalDurationMicro; + _screenRenderForAutoUiTrace.frameData.addAll(_delayedFrames); + } + + /// @nodoc + void _logExceptionErrorAndStackTrace(Object error, StackTrace stackTrace) { + InstabugLogger.I.e( + '[Error]:$error \n' + '[StackTrace]: $stackTrace', + tag: tag, + ); + } + + /// @nodoc + int _getMicrosecondsSinceEpoch(int timeInMicroseconds) => + timeInMicroseconds + (_epochOffset ?? 0); + + /// --------------------------- testing helper methods --------------------- + + @visibleForTesting + InstabugScreenRenderManager.init(); + + @visibleForTesting + // ignore: use_setters_to_change_properties + static void setInstance(InstabugScreenRenderManager instance) { + _instance = instance; + } + + @visibleForTesting + InstabugScreenRenderData get screenRenderForAutoUiTrace => + _screenRenderForAutoUiTrace; + + @visibleForTesting + InstabugScreenRenderData get screenRenderForCustomUiTrace => + _screenRenderForCustomUiTrace; + + @visibleForTesting + void setFrameData(InstabugScreenRenderData data) { + _delayedFrames.addAll(data.frameData); + _frozenFramesTotalDurationMicro = data.frozenFramesTotalDurationMicro; + _slowFramesTotalDurationMicro = data.slowFramesTotalDurationMicro; + } +} diff --git a/lib/src/utils/screen_rendering/instabug_widget_binding_observer.dart b/lib/src/utils/screen_rendering/instabug_widget_binding_observer.dart new file mode 100644 index 000000000..5a27d15f0 --- /dev/null +++ b/lib/src/utils/screen_rendering/instabug_widget_binding_observer.dart @@ -0,0 +1,87 @@ +import 'package:flutter/widgets.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:instabug_flutter/src/utils/screen_rendering/instabug_screen_render_manager.dart'; +import 'package:meta/meta.dart'; + +class InstabugWidgetsBindingObserver extends WidgetsBindingObserver { + InstabugWidgetsBindingObserver._(); + + static final InstabugWidgetsBindingObserver _instance = + InstabugWidgetsBindingObserver._(); + + /// Returns the singleton instance of [InstabugWidgetsBindingObserver]. + static InstabugWidgetsBindingObserver get instance => _instance; + + /// Shorthand for [instance] + static InstabugWidgetsBindingObserver get I => instance; + + /// Logging tag for debugging purposes. + static const tag = "InstabugWidgetsBindingObserver"; + + static void dispose() { + // Always call dispose to ensure proper cleanup with tracking flags + // The dispose method is safe to call multiple times due to state tracking + InstabugScreenRenderManager.I.dispose(); + } + + void _handleResumedState() { + final lastUiTrace = ScreenLoadingManager.I.currentUiTrace; + if (lastUiTrace == null) { + return; + } + final maskedScreenName = ScreenNameMasker.I.mask(lastUiTrace.screenName); + ScreenLoadingManager.I + .startUiTrace(maskedScreenName, lastUiTrace.screenName) + .then((uiTraceId) { + if (uiTraceId != null && + InstabugScreenRenderManager.I.screenRenderEnabled) { + InstabugScreenRenderManager.I + .startScreenRenderCollectorForTraceId(uiTraceId); + } + }); + } + + void _handlePausedState() { + if (InstabugScreenRenderManager.I.screenRenderEnabled) { + InstabugScreenRenderManager.I.stopScreenRenderCollector(); + } + } + + void _handleDetachedState() { + if (InstabugScreenRenderManager.I.screenRenderEnabled) { + InstabugScreenRenderManager.I.stopScreenRenderCollector(); + } + dispose(); + } + + void _handleDefaultState() { + // Added for lint warnings + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + switch (state) { + case AppLifecycleState.resumed: + _handleResumedState(); + break; + case AppLifecycleState.paused: + _handlePausedState(); + break; + case AppLifecycleState.detached: + _handleDetachedState(); + break; + default: + _handleDefaultState(); + } + } +} + +@internal +void checkForWidgetBinding() { + try { + WidgetsBinding.instance; + } catch (_) { + WidgetsFlutterBinding.ensureInitialized(); + } +} diff --git a/lib/src/utils/screen_loading/flags_config.dart b/lib/src/utils/ui_trace/flags_config.dart similarity index 83% rename from lib/src/utils/screen_loading/flags_config.dart rename to lib/src/utils/ui_trace/flags_config.dart index f18eb1ccb..92a899b15 100644 --- a/lib/src/utils/screen_loading/flags_config.dart +++ b/lib/src/utils/ui_trace/flags_config.dart @@ -5,6 +5,7 @@ enum FlagsConfig { uiTrace, screenLoading, endScreenLoading, + screenRendering, } extension FeatureExtensions on FlagsConfig { @@ -16,6 +17,8 @@ extension FeatureExtensions on FlagsConfig { return APM.isScreenLoadingEnabled(); case FlagsConfig.endScreenLoading: return APM.isEndScreenLoadingEnabled(); + case FlagsConfig.screenRendering: + return APM.isScreenRenderEnabled(); default: return false; } diff --git a/lib/src/utils/screen_loading/route_matcher.dart b/lib/src/utils/ui_trace/route_matcher.dart similarity index 100% rename from lib/src/utils/screen_loading/route_matcher.dart rename to lib/src/utils/ui_trace/route_matcher.dart diff --git a/lib/src/utils/screen_loading/ui_trace.dart b/lib/src/utils/ui_trace/ui_trace.dart similarity index 94% rename from lib/src/utils/screen_loading/ui_trace.dart rename to lib/src/utils/ui_trace/ui_trace.dart index 17ef41046..34c88cbc0 100644 --- a/lib/src/utils/screen_loading/ui_trace.dart +++ b/lib/src/utils/ui_trace/ui_trace.dart @@ -1,4 +1,4 @@ -import 'package:instabug_flutter/src/utils/screen_loading/route_matcher.dart'; +import 'package:instabug_flutter/src/utils/ui_trace/route_matcher.dart'; class UiTrace { final String screenName; diff --git a/pigeons/apm.api.dart b/pigeons/apm.api.dart index 84fe9eb8e..3711d53fc 100644 --- a/pigeons/apm.api.dart +++ b/pigeons/apm.api.dart @@ -3,29 +3,42 @@ 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); @async String? startExecutionTrace(String id, String name); void startFlow(String name); + void setFlowAttribute(String name, String key, String? value); + void endFlow(String name); + void setExecutionTraceAttribute( String id, String key, String value, ); + void endExecutionTrace(String id); + void startUITrace(String name); + void endUITrace(); + void endAppLaunch(); + void networkLogAndroid(Map data); void startCpUiTrace(String screenName, int microTimeStamp, int traceId); @@ -40,4 +53,16 @@ abstract class ApmHostApi { @async bool isEndScreenLoadingEnabled(); + + @async + bool isScreenRenderEnabled(); + + @async + double deviceRefreshRate(); + + void setScreenRenderEnabled(bool isEnabled); + + void endScreenRenderForAutoUiTrace(Map data); + + void endScreenRenderForCustomUiTrace(Map data); } diff --git a/scripts/pigeon.sh b/scripts/pigeon.sh old mode 100644 new mode 100755 diff --git a/test/apm_test.dart b/test/apm_test.dart index c801926f3..76aaf28f4 100644 --- a/test/apm_test.dart +++ b/test/apm_test.dart @@ -4,6 +4,7 @@ import 'package:instabug_flutter/instabug_flutter.dart'; import 'package:instabug_flutter/src/generated/apm.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/screen_rendering/instabug_screen_render_manager.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; @@ -13,6 +14,7 @@ import 'apm_test.mocks.dart'; ApmHostApi, IBGDateTime, IBGBuildInfo, + InstabugScreenRenderManager, ]) void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -21,6 +23,7 @@ void main() { final mHost = MockApmHostApi(); final mDateTime = MockIBGDateTime(); final mBuildInfo = MockIBGBuildInfo(); + final mScreenRenderManager = MockInstabugScreenRenderManager(); setUpAll(() { APM.$setHostApi(mHost); @@ -165,6 +168,9 @@ void main() { test('[startUITrace] should call host method', () async { const name = 'UI-trace'; + //disable the feature flag for screen render feature in order to skip its checking. + when(mHost.isScreenRenderEnabled()).thenAnswer((_) async => false); + await APM.startUITrace(name); verify( @@ -214,7 +220,6 @@ void main() { verify( mHost.startCpUiTrace(screenName, microTimeStamp, traceId), ).called(1); - verifyNoMoreInteractions(mHost); }); test('[reportScreenLoading] should call host method', () async { @@ -235,7 +240,6 @@ void main() { uiTraceId, ), ).called(1); - verifyNoMoreInteractions(mHost); }); test('[endScreenLoading] should call host method', () async { @@ -247,7 +251,6 @@ void main() { verify( mHost.endScreenLoadingCP(timeStampMicro, uiTraceId), ).called(1); - verifyNoMoreInteractions(mHost); }); test('[isSEndScreenLoadingEnabled] should call host method', () async { @@ -258,4 +261,115 @@ void main() { mHost.isEndScreenLoadingEnabled(), ).called(1); }); + + group("ScreenRender", () { + setUp(() { + InstabugScreenRenderManager.setInstance(mScreenRenderManager); + }); + tearDown(() { + reset(mScreenRenderManager); + }); + test("[isScreenRenderEnabled] should call host method", () async { + when(mHost.isScreenRenderEnabled()).thenAnswer((_) async => true); + await APM.isScreenRenderEnabled(); + verify(mHost.isScreenRenderEnabled()); + }); + + test("[getDeviceRefreshRate] should call host method", () async { + when(mHost.deviceRefreshRate()).thenAnswer((_) async => 60.0); + await APM.getDeviceRefreshRate(); + verify(mHost.deviceRefreshRate()).called(1); + }); + + test("[setScreenRenderEnabled] should call host method", () async { + const isEnabled = false; + await APM.setScreenRenderEnabled(isEnabled); + verify(mHost.setScreenRenderEnabled(isEnabled)).called(1); + }); + + test( + "[setScreenRenderEnabled] should call [init()] screen render collector, is the feature is enabled", + () async { + const isEnabled = true; + await APM.setScreenRenderEnabled(isEnabled); + verify(mScreenRenderManager.init(any)).called(1); + verifyNoMoreInteractions(mScreenRenderManager); + }); + + test( + "[setScreenRenderEnabled] should call [remove()] screen render collector, is the feature is enabled", + () async { + const isEnabled = false; + await APM.setScreenRenderEnabled(isEnabled); + verify(mScreenRenderManager.dispose()).called(1); + verifyNoMoreInteractions(mScreenRenderManager); + }); + + test( + "[startUITrace] should start screen render collector with right params, if screen render feature is enabled", + () async { + when(mHost.isScreenRenderEnabled()).thenAnswer((_) async => true); + + const traceName = "traceNameTest"; + await APM.startUITrace(traceName); + + verify(mHost.startUITrace(traceName)).called(1); + verify(mHost.isScreenRenderEnabled()).called(1); + verify( + mScreenRenderManager.startScreenRenderCollectorForTraceId( + 0, + UiTraceType.custom, + ), + ).called(1); + }); + + test( + "[startUITrace] should not start screen render collector, if screen render feature is disabled", + () async { + when(mHost.isScreenRenderEnabled()).thenAnswer((_) async => false); + + const traceName = "traceNameTest"; + await APM.startUITrace(traceName); + + verify(mHost.startUITrace(traceName)).called(1); + verify(mHost.isScreenRenderEnabled()).called(1); + verifyNever( + mScreenRenderManager.startScreenRenderCollectorForTraceId( + any, + any, + ), + ); + }); + + test( + "[endUITrace] should stop screen render collector with, if screen render feature is enabled", + () async { + when(mHost.isScreenRenderEnabled()).thenAnswer((_) async => true); + when(mScreenRenderManager.screenRenderEnabled).thenReturn(true); + await APM.endUITrace(); + + verify( + mScreenRenderManager.endScreenRenderCollector(), + ).called(1); + verifyNever(mHost.endUITrace()); + }); + + test( + "[endUITrace] should acts as normal and do nothing related to screen render, if screen render feature is disabled", + () async { + when(mHost.isScreenRenderEnabled()).thenAnswer((_) async => false); + when(mScreenRenderManager.screenRenderEnabled).thenReturn(false); + const traceName = "traceNameTest"; + await APM.startUITrace(traceName); + await APM.endUITrace(); + + verify(mHost.startUITrace(traceName)).called(1); + verify( + mHost.endUITrace(), + ).called(1); + verifyNever( + mScreenRenderManager.endScreenRenderCollector(), + ); + }); + }); } diff --git a/test/instabug_test.dart b/test/instabug_test.dart index e2fd7d298..06f3a3ef2 100644 --- a/test/instabug_test.dart +++ b/test/instabug_test.dart @@ -1,8 +1,10 @@ +// ignore_for_file: deprecated_member_use import 'dart:typed_data'; 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/enum_converter.dart'; import 'package:instabug_flutter/src/utils/feature_flags_manager.dart'; @@ -17,6 +19,7 @@ import 'instabug_test.mocks.dart'; InstabugHostApi, IBGBuildInfo, ScreenNameMasker, + ApmHostApi, ]) void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -25,12 +28,14 @@ void main() { final mHost = MockInstabugHostApi(); final mBuildInfo = MockIBGBuildInfo(); final mScreenNameMasker = MockScreenNameMasker(); + final mApmHost = MockApmHostApi(); setUpAll(() { Instabug.$setHostApi(mHost); FeatureFlagsManager().$setHostApi(mHost); IBGBuildInfo.setInstance(mBuildInfo); ScreenNameMasker.setInstance(mScreenNameMasker); + APM.$setHostApi(mApmHost); }); test('[setEnabled] should call host method', () async { @@ -78,6 +83,10 @@ void main() { "isW3cCaughtHeaderEnabled": true, }), ); + + //disable the feature flag for screen render feature in order to skip its checking. + when(mApmHost.isScreenRenderEnabled()).thenAnswer((_) async => false); + await Instabug.init( token: token, invocationEvents: events, diff --git a/test/route_matcher_test.dart b/test/route_matcher_test.dart index 977c61d88..5d8b234c4 100644 --- a/test/route_matcher_test.dart +++ b/test/route_matcher_test.dart @@ -1,6 +1,6 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:instabug_flutter/src/utils/screen_loading/route_matcher.dart'; +import 'package:instabug_flutter/src/utils/ui_trace/route_matcher.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); diff --git a/test/utils/instabug_navigator_observer_test.dart b/test/utils/instabug_navigator_observer_test.dart index ebf541137..76ae1840e 100644 --- a/test/utils/instabug_navigator_observer_test.dart +++ b/test/utils/instabug_navigator_observer_test.dart @@ -5,6 +5,7 @@ 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:instabug_flutter/src/utils/screen_rendering/instabug_screen_render_manager.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; @@ -13,13 +14,14 @@ import 'instabug_navigator_observer_test.mocks.dart'; @GenerateMocks([ InstabugHostApi, ScreenLoadingManager, + InstabugScreenRenderManager, ]) void main() { TestWidgetsFlutterBinding.ensureInitialized(); - WidgetsFlutterBinding.ensureInitialized(); final mHost = MockInstabugHostApi(); final mScreenLoadingManager = MockScreenLoadingManager(); + final mScreenRenderManager = MockInstabugScreenRenderManager(); late InstabugNavigatorObserver observer; const screen = '/screen'; @@ -38,10 +40,11 @@ void main() { previousRoute = createRoute(previousScreen); ScreenNameMasker.I.setMaskingCallback(null); + when(mScreenRenderManager.screenRenderEnabled).thenReturn(false); }); test('should report screen change when a route is pushed', () { - fakeAsync((async) { + fakeAsync((async) async { observer.didPush(route, previousRoute); async.elapse(const Duration(milliseconds: 1000)); @@ -60,6 +63,9 @@ void main() { 'should report screen change when a route is popped and previous is known', () { fakeAsync((async) { + when(mScreenLoadingManager.startUiTrace(previousScreen, previousScreen)) + .thenAnswer((realInvocation) async => null); + observer.didPop(route, previousRoute); async.elapse(const Duration(milliseconds: 1000)); @@ -97,6 +103,9 @@ void main() { final route = createRoute(''); const fallback = 'N/A'; + when(mScreenLoadingManager.startUiTrace(fallback, fallback)) + .thenAnswer((realInvocation) async => null); + observer.didPush(route, previousRoute); async.elapse(const Duration(milliseconds: 1000)); @@ -114,6 +123,9 @@ void main() { test('should mask screen name when masking callback is set', () { const maskedScreen = 'maskedScreen'; + when(mScreenLoadingManager.startUiTrace(maskedScreen, screen)) + .thenAnswer((realInvocation) async => null); + ScreenNameMasker.I.setMaskingCallback((_) => maskedScreen); fakeAsync((async) { @@ -130,6 +142,62 @@ void main() { ).called(1); }); }); + + test('should start new screen render collector when a route is pushed', () { + fakeAsync((async) async { + const traceID = 123; + + when(mScreenLoadingManager.startUiTrace(screen, screen)) + .thenAnswer((_) async => traceID); + when(mScreenRenderManager.screenRenderEnabled).thenReturn(true); + + observer.didPush(route, previousRoute); + + async.elapse(const Duration(milliseconds: 1000)); + + verify( + mScreenRenderManager.startScreenRenderCollectorForTraceId(traceID), + ).called(1); + }); + }); + + test( + 'should not start new screen render collector when a route is pushed and [traceID] is null', + () { + fakeAsync((async) async { + when(mScreenLoadingManager.startUiTrace(screen, screen)) + .thenAnswer((_) async => null); + + when(mScreenRenderManager.screenRenderEnabled).thenReturn(true); + + observer.didPush(route, previousRoute); + + async.elapse(const Duration(milliseconds: 1000)); + + verifyNever( + mScreenRenderManager.startScreenRenderCollectorForTraceId(any), + ); + }); + }); + + test( + 'should not start new screen render collector when a route is pushed and [mScreenRenderManager.screenRenderEnabled] is false', + () { + fakeAsync((async) async { + when(mScreenLoadingManager.startUiTrace(screen, screen)) + .thenAnswer((_) async => 123); + + when(mScreenRenderManager.screenRenderEnabled).thenReturn(false); + + observer.didPush(route, previousRoute); + + async.elapse(const Duration(milliseconds: 1000)); + + verifyNever( + mScreenRenderManager.startScreenRenderCollectorForTraceId(any), + ); + }); + }); } Route createRoute(String? name) { diff --git a/test/utils/screen_loading/screen_loading_manager_test.dart b/test/utils/screen_loading/screen_loading_manager_test.dart index c008b8bcc..6b5c954f3 100644 --- a/test/utils/screen_loading/screen_loading_manager_test.dart +++ b/test/utils/screen_loading/screen_loading_manager_test.dart @@ -7,12 +7,13 @@ 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:instabug_flutter/src/utils/ui_trace/flags_config.dart'; +import 'package:instabug_flutter/src/utils/ui_trace/ui_trace.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; + import 'screen_loading_manager_test.mocks.dart'; class ScreenLoadingManagerNoResets extends ScreenLoadingManager { @@ -167,25 +168,6 @@ void main() { 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; diff --git a/test/utils/screen_loading/ui_trace_test.dart b/test/utils/screen_loading/ui_trace_test.dart index 11ed57c66..68a0f06cf 100644 --- a/test/utils/screen_loading/ui_trace_test.dart +++ b/test/utils/screen_loading/ui_trace_test.dart @@ -1,6 +1,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:instabug_flutter/instabug_flutter.dart'; -import 'package:instabug_flutter/src/utils/screen_loading/ui_trace.dart'; +import 'package:instabug_flutter/src/utils/ui_trace/ui_trace.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; diff --git a/test/utils/screen_render/instabug_screen_render_manager_test.dart b/test/utils/screen_render/instabug_screen_render_manager_test.dart new file mode 100644 index 000000000..adbe7d74d --- /dev/null +++ b/test/utils/screen_render/instabug_screen_render_manager_test.dart @@ -0,0 +1,449 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:instabug_flutter/instabug_flutter.dart'; +import 'package:instabug_flutter/src/models/instabug_frame_data.dart'; +import 'package:instabug_flutter/src/models/instabug_screen_render_data.dart'; +import 'package:instabug_flutter/src/utils/screen_rendering/instabug_screen_render_manager.dart'; +import 'package:mockito/mockito.dart'; + +import 'instabug_screen_render_manager_test_manual_mocks.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + late InstabugScreenRenderManager manager; + late MockApmHostApi mApmHost; + late MockWidgetsBinding mWidgetBinding; + + setUp(() async { + mApmHost = MockApmHostApi(); + mWidgetBinding = MockWidgetsBinding(); + manager = InstabugScreenRenderManager.init(); // test-only constructor + APM.$setHostApi(mApmHost); + when(mApmHost.deviceRefreshRate()).thenAnswer((_) async => 60); + manager.init(mWidgetBinding); + }); + + group('InstabugScreenRenderManager.init()', () { + test('should initialize timings callback and add observer', () async { + expect(manager, isA()); + + verify(mWidgetBinding.addObserver(any)).called(1); + + verify(mWidgetBinding.addTimingsCallback(any)).called(1); + }); + + test('calling init more that one time should do nothing', () async { + manager.init(mWidgetBinding); + manager.init( + mWidgetBinding, + ); // second call should be ignored + + verify(mWidgetBinding.addObserver(any)).called(1); + + verify(mWidgetBinding.addTimingsCallback(any)).called(1); + }); + }); + + group('startScreenRenderCollectorForTraceId()', () { + test('should not attach timing listener if it is attached', () async { + manager.startScreenRenderCollectorForTraceId(1); + manager.startScreenRenderCollectorForTraceId(2); + manager.startScreenRenderCollectorForTraceId(3); + + verify(mWidgetBinding.addTimingsCallback(any)).called( + 1, + ); // the one form initForTesting() + }); + + test('should attach timing listener if it is not attached', () async { + manager.stopScreenRenderCollector(); // this should detach listener safely + + manager.startScreenRenderCollectorForTraceId(1); + + verify(mWidgetBinding.addTimingsCallback(any)).called( + 1, + ); + }); + + test('should update the data for same trace type', () { + const firstTraceId = 123; + const secondTraceId = 456; + + expect(manager.screenRenderForAutoUiTrace.isActive, false); + + manager.startScreenRenderCollectorForTraceId( + firstTraceId, + ); + expect(manager.screenRenderForAutoUiTrace.isActive, true); + expect(manager.screenRenderForAutoUiTrace.traceId, firstTraceId); + + manager.startScreenRenderCollectorForTraceId( + secondTraceId, + ); + expect(manager.screenRenderForAutoUiTrace.isActive, true); + expect(manager.screenRenderForAutoUiTrace.traceId, secondTraceId); + }); + + test('should not update the data for same trace type', () { + const firstTraceId = 123; + const secondTraceId = 456; + + expect(manager.screenRenderForAutoUiTrace.isActive, false); + expect(manager.screenRenderForCustomUiTrace.isActive, false); + + manager.startScreenRenderCollectorForTraceId( + firstTraceId, + ); + expect(manager.screenRenderForAutoUiTrace.isActive, true); + expect(manager.screenRenderForAutoUiTrace.traceId, firstTraceId); + + manager.startScreenRenderCollectorForTraceId( + secondTraceId, + UiTraceType.custom, + ); + expect(manager.screenRenderForAutoUiTrace.traceId, firstTraceId); + expect(manager.screenRenderForCustomUiTrace.traceId, secondTraceId); + }); + }); + + group('stopScreenRenderCollector()', () { + test('should not save data if no UI trace is started', () { + final frameTestData = InstabugScreenRenderData( + traceId: 123, + frameData: [ + InstabugFrameData(10000, 200), + InstabugFrameData(20000, 1000), + ], + frozenFramesTotalDurationMicro: 1000, + slowFramesTotalDurationMicro: 200, + ); + + manager.setFrameData(frameTestData); + + manager.stopScreenRenderCollector(); + + expect(manager.screenRenderForAutoUiTrace.isActive, false); + expect(manager.screenRenderForAutoUiTrace == frameTestData, false); + + expect(manager.screenRenderForCustomUiTrace.isActive, false); + expect(manager.screenRenderForCustomUiTrace == frameTestData, false); + }); + + test( + 'should save and data to screenRenderForAutoUiTrace when for autoUITrace', + () { + final frameTestData = InstabugScreenRenderData( + traceId: 123, + frameData: [ + InstabugFrameData(10000, 400), + InstabugFrameData(10000, 600), + InstabugFrameData(20000, 1000), + ], + frozenFramesTotalDurationMicro: 1000, + slowFramesTotalDurationMicro: 1000, + endTimeMicro: 30000, + ); + + manager.startScreenRenderCollectorForTraceId( + frameTestData.traceId, + ); + + manager.setFrameData(frameTestData); + + manager.stopScreenRenderCollector(); + + expect(manager.screenRenderForAutoUiTrace.isActive, true); + + expect(manager.screenRenderForCustomUiTrace.isActive, false); + + expect(manager.screenRenderForAutoUiTrace == frameTestData, true); + + verify( + mApmHost.endScreenRenderForAutoUiTrace(any), + ); // the content has been verified in the above assertion. + }); + + test( + 'should save and data to screenRenderForCustomUiTrace when for customUITrace', + () { + final frameTestData = InstabugScreenRenderData( + traceId: 123, + frameData: [ + InstabugFrameData(10000, 400), + InstabugFrameData(10000, 600), + InstabugFrameData(20000, 1000), + ], + frozenFramesTotalDurationMicro: 1000, + slowFramesTotalDurationMicro: 1000, + endTimeMicro: 30000, + ); + + manager.startScreenRenderCollectorForTraceId( + frameTestData.traceId, + UiTraceType.custom, + ); + + manager.setFrameData(frameTestData); + + manager.stopScreenRenderCollector(); + + expect(manager.screenRenderForCustomUiTrace.isActive, true); + + expect(manager.screenRenderForAutoUiTrace.isActive, false); + + expect(manager.screenRenderForCustomUiTrace == frameTestData, true); + + verify( + mApmHost.endScreenRenderForCustomUiTrace(any), + ); // the content has been verified in the above assertion. + }); + + test('should not remove timing callback listener', () { + final frameTestData = InstabugScreenRenderData( + traceId: 123, + frameData: [ + InstabugFrameData(10000, 200), + InstabugFrameData(20000, 1000), + ], + frozenFramesTotalDurationMicro: 1000, + slowFramesTotalDurationMicro: 200, + ); + + manager.setFrameData(frameTestData); + manager.stopScreenRenderCollector(); + + verifyNever(mWidgetBinding.removeTimingsCallback(any)); + }); + + test('should report data to native side with the correct type', () async { + final frameTestData = InstabugScreenRenderData( + traceId: 123, + frameData: [ + InstabugFrameData(10000, 200), + InstabugFrameData(20000, 1000), + ], + frozenFramesTotalDurationMicro: 1000, + slowFramesTotalDurationMicro: 200, + ); + + manager.startScreenRenderCollectorForTraceId(0, UiTraceType.custom); + manager.setFrameData(frameTestData); + manager.stopScreenRenderCollector(); + verify(mApmHost.endScreenRenderForCustomUiTrace(any)).called(1); + verifyNever(mApmHost.endScreenRenderForAutoUiTrace(any)); + }); + }); + + group('endScreenRenderCollectorForCustomUiTrace()', () { + setUp(() { + manager.screenRenderForAutoUiTrace.clear(); + manager.screenRenderForCustomUiTrace.clear(); + }); + + test('should not save data if no custom UI trace is started', () { + final frameTestData = InstabugScreenRenderData( + traceId: 123, + frameData: [ + InstabugFrameData(10000, 200), + InstabugFrameData(20000, 1000), + ], + frozenFramesTotalDurationMicro: 1000, + slowFramesTotalDurationMicro: 200, + ); + + manager.setFrameData(frameTestData); + + manager.endScreenRenderCollector(); + + expect(manager.screenRenderForCustomUiTrace.isActive, false); + expect(manager.screenRenderForCustomUiTrace == frameTestData, false); + }); + + test( + 'should save data to screenRenderForCustomUiTrace if custom UI trace is started', + () { + final frameTestData = InstabugScreenRenderData( + traceId: 123, + frameData: [ + InstabugFrameData(10000, 200), + InstabugFrameData(20000, 1000), + ], + frozenFramesTotalDurationMicro: 1000, + slowFramesTotalDurationMicro: 200, + ); + + manager.startScreenRenderCollectorForTraceId( + frameTestData.traceId, + UiTraceType.custom, + ); + + manager.setFrameData(frameTestData); + + manager.endScreenRenderCollector(); + }); + + test('should not remove timing callback listener', () { + manager.endScreenRenderCollector(); + + verifyNever(mWidgetBinding.removeTimingsCallback(any)); + }); + + test('should report data to native side', () async { + final frameTestData = InstabugScreenRenderData( + traceId: 123, + frameData: [ + InstabugFrameData(10000, 200), + InstabugFrameData(20000, 1000), + ], + frozenFramesTotalDurationMicro: 1000, + slowFramesTotalDurationMicro: 200, + ); + + manager.startScreenRenderCollectorForTraceId(0, UiTraceType.custom); + manager.setFrameData(frameTestData); + manager.endScreenRenderCollector(UiTraceType.custom); + verify(mApmHost.endScreenRenderForCustomUiTrace(any)).called(1); + }); + }); + + group('analyzeFrameTiming()', () { + late MockFrameTiming mockFrameTiming; + + setUp(() { + mockFrameTiming = MockFrameTiming(); + when(mockFrameTiming.buildDuration) + .thenReturn(const Duration(milliseconds: 1)); + when(mockFrameTiming.rasterDuration) + .thenReturn(const Duration(milliseconds: 1)); + when(mockFrameTiming.totalSpan) + .thenReturn(const Duration(milliseconds: 2)); + when(mockFrameTiming.timestampInMicroseconds(any)).thenReturn(1000); + }); + + test('should detect slow frame on ui thread and record duration', () { + const buildDuration = 20; + when(mockFrameTiming.buildDuration) + .thenReturn(const Duration(milliseconds: buildDuration)); + + manager.startScreenRenderCollectorForTraceId(1); // start new collector + manager.analyzeFrameTiming(mockFrameTiming); // mock frame timing + manager.stopScreenRenderCollector(); // should save data + + expect(manager.screenRenderForAutoUiTrace.frameData.length, 1); + expect( + manager.screenRenderForAutoUiTrace.slowFramesTotalDurationMicro, + buildDuration * 1000, + ); // * 1000 to convert from milliseconds to microseconds + expect( + manager.screenRenderForAutoUiTrace.frozenFramesTotalDurationMicro, + 0, + ); + }); + + test('should detect slow frame on raster thread and record duration', () { + const rasterDuration = 20; + when(mockFrameTiming.rasterDuration) + .thenReturn(const Duration(milliseconds: rasterDuration)); + + manager.startScreenRenderCollectorForTraceId(1); // start new collector + manager.analyzeFrameTiming(mockFrameTiming); // mock frame timing + manager.stopScreenRenderCollector(); // should save data + + expect(manager.screenRenderForAutoUiTrace.frameData.length, 1); + expect( + manager.screenRenderForAutoUiTrace.slowFramesTotalDurationMicro, + rasterDuration * 1000, + ); // * 1000 to convert from milliseconds to microseconds + expect( + manager.screenRenderForAutoUiTrace.frozenFramesTotalDurationMicro, + 0, + ); + }); + + test( + 'should detect frozen frame when durations are greater than or equal 700 ms', + () { + const totalTime = 700; + when(mockFrameTiming.totalSpan) + .thenReturn(const Duration(milliseconds: totalTime)); + manager.startScreenRenderCollectorForTraceId(1); // start new collector + manager.analyzeFrameTiming(mockFrameTiming); // mock frame timing + manager.stopScreenRenderCollector(); // should save data + + expect(manager.screenRenderForAutoUiTrace.frameData.length, 1); + expect( + manager.screenRenderForAutoUiTrace.frozenFramesTotalDurationMicro, + totalTime * 1000, + ); // * 1000 to convert from milliseconds to microseconds + expect( + manager.screenRenderForAutoUiTrace.slowFramesTotalDurationMicro, + 0, + ); + }); + + test('should detect no slow or frozen frame under thresholds', () { + when(mockFrameTiming.buildDuration) + .thenReturn(const Duration(milliseconds: 5)); + when(mockFrameTiming.rasterDuration) + .thenReturn(const Duration(milliseconds: 5)); + when(mockFrameTiming.totalSpan) + .thenReturn(const Duration(milliseconds: 10)); + manager.analyzeFrameTiming(mockFrameTiming); + expect(manager.screenRenderForAutoUiTrace.frameData.isEmpty, true); + expect( + manager.screenRenderForAutoUiTrace.frozenFramesTotalDurationMicro, + 0, + ); // * 1000 to convert from milliseconds to microseconds + expect( + manager.screenRenderForAutoUiTrace.slowFramesTotalDurationMicro, + 0, + ); + }); + }); + + group('InstabugScreenRenderManager.endScreenRenderCollector', () { + test('should save and reset cached data if delayed frames exist', () { + final frameTestData = InstabugScreenRenderData( + traceId: 123, + frameData: [ + InstabugFrameData(10000, 200), + InstabugFrameData(20000, 1000), + ], + frozenFramesTotalDurationMicro: 1000, + slowFramesTotalDurationMicro: 200, + ); + manager.startScreenRenderCollectorForTraceId(1); + manager.setFrameData(frameTestData); + manager.endScreenRenderCollector(); + verify(mApmHost.endScreenRenderForAutoUiTrace(any)).called(1); + expect(manager.screenRenderForAutoUiTrace.isEmpty, true); + expect(manager.screenRenderForAutoUiTrace.isActive, false); + }); + + test('should report and clear custom trace if type is custom and active', + () { + final frameTestData = InstabugScreenRenderData( + traceId: 123, + frameData: [ + InstabugFrameData(10000, 200), + InstabugFrameData(20000, 1000), + ], + frozenFramesTotalDurationMicro: 1000, + slowFramesTotalDurationMicro: 200, + ); + manager.startScreenRenderCollectorForTraceId(1, UiTraceType.custom); + manager.setFrameData(frameTestData); + manager.endScreenRenderCollector(UiTraceType.custom); + verify(mApmHost.endScreenRenderForCustomUiTrace(any)).called(1); + expect(manager.screenRenderForCustomUiTrace.isEmpty, true); + expect(manager.screenRenderForCustomUiTrace.isActive, false); + }); + + test('should return early if not enabled or timings not attached', () { + manager.screenRenderEnabled = false; + manager.endScreenRenderCollector(); + verifyNever(mApmHost.endScreenRenderForAutoUiTrace(any)); + verifyNever(mApmHost.endScreenRenderForCustomUiTrace(any)); + }); + }); +} diff --git a/test/utils/screen_render/instabug_screen_render_manager_test_manual_mocks.dart b/test/utils/screen_render/instabug_screen_render_manager_test_manual_mocks.dart new file mode 100644 index 000000000..64d147846 --- /dev/null +++ b/test/utils/screen_render/instabug_screen_render_manager_test_manual_mocks.dart @@ -0,0 +1,759 @@ +// Mocks generated by Mockito 5.2.0 from annotations +// in instabug_flutter/example/ios/.symlinks/plugins/instabug_flutter/test/utils/screen_render/instabug_screen_render_manager_test.dart. +// Do not manually edit this file. + +import 'dart:async' as _i9; +import 'dart:developer' as _i13; +import 'dart:ui' as _i4; + +import 'package:flutter/foundation.dart' as _i3; +import 'package:flutter/gestures.dart' as _i6; +import 'package:flutter/rendering.dart' as _i7; +import 'package:flutter/scheduler.dart' as _i11; +import 'package:flutter/services.dart' as _i5; +import 'package:flutter/src/widgets/binding.dart' as _i10; +import 'package:flutter/src/widgets/focus_manager.dart' as _i2; +import 'package:flutter/src/widgets/framework.dart' as _i12; +import 'package:instabug_flutter/src/generated/apm.api.g.dart' as _i8; +import 'package:mockito/mockito.dart' as _i1; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types + +class _FakeFocusManager_0 extends _i1.Fake implements _i2.FocusManager { + @override + String toString({_i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeSingletonFlutterWindow_1 extends _i1.Fake + implements _i4.SingletonFlutterWindow {} + +class _FakePlatformDispatcher_2 extends _i1.Fake + implements _i4.PlatformDispatcher {} + +class _FakeHardwareKeyboard_3 extends _i1.Fake implements + _i5.HardwareKeyboard {} + +class _FakeKeyEventManager_4 extends _i1.Fake implements _i5.KeyEventManager {} + +class _FakeBinaryMessenger_5 extends _i1.Fake implements _i5.BinaryMessenger {} + +class _FakeChannelBuffers_6 extends _i1.Fake implements _i4.ChannelBuffers {} + +class _FakeRestorationManager_7 extends _i1.Fake + implements _i5.RestorationManager {} + +class _FakeDuration_8 extends _i1.Fake implements Duration {} + +class _FakePointerRouter_9 extends _i1.Fake implements _i6.PointerRouter {} + +class _FakeGestureArenaManager_10 extends _i1.Fake + implements _i6.GestureArenaManager {} + +class _FakePointerSignalResolver_11 extends _i1.Fake + implements _i6.PointerSignalResolver {} + +class _FakeMouseTracker_12 extends _i1.Fake implements _i7.MouseTracker {} + +class _FakePipelineOwner_13 extends _i1.Fake implements _i7.PipelineOwner {} + +class _FakeRenderView_14 extends _i1.Fake implements _i7.RenderView { + @override + String toString({_i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeAccessibilityFeatures_15 extends _i1.Fake + implements _i4.AccessibilityFeatures {} + +class _FakeViewConfiguration_16 extends _i1.Fake + implements _i7.ViewConfiguration {} + +class _FakeSemanticsUpdateBuilder_17 extends _i1.Fake + implements _i4.SemanticsUpdateBuilder {} + +/// A class which mocks [ApmHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockApmHostApi extends _i1.Mock implements _i8.ApmHostApi { + MockApmHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + _i9.Future setEnabled(bool? arg_isEnabled) => + (super.noSuchMethod(Invocation.method(#setEnabled, [arg_isEnabled]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i9.Future); + @override + _i9.Future isEnabled() => + (super.noSuchMethod(Invocation.method(#isEnabled, []), + returnValue: Future.value(false)) as _i9.Future); + @override + _i9.Future setScreenLoadingEnabled(bool? arg_isEnabled) => + (super.noSuchMethod( + Invocation.method(#setScreenLoadingEnabled, [arg_isEnabled]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i9.Future); + @override + _i9.Future isScreenLoadingEnabled() => + (super.noSuchMethod(Invocation.method(#isScreenLoadingEnabled, []), + returnValue: Future.value(false)) as _i9.Future); + @override + _i9.Future setColdAppLaunchEnabled(bool? arg_isEnabled) => + (super.noSuchMethod( + Invocation.method(#setColdAppLaunchEnabled, [arg_isEnabled]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i9.Future); + @override + _i9.Future setAutoUITraceEnabled(bool? arg_isEnabled) => (super + .noSuchMethod(Invocation.method(#setAutoUITraceEnabled, [arg_isEnabled]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i9.Future); + @override + _i9.Future startExecutionTrace(String? arg_id, String? arg_name) => + (super.noSuchMethod( + Invocation.method(#startExecutionTrace, [arg_id, arg_name]), + returnValue: Future.value()) as _i9.Future); + @override + _i9.Future startFlow(String? arg_name) => + (super.noSuchMethod(Invocation.method(#startFlow, [arg_name]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i9.Future); + @override + _i9.Future setFlowAttribute( + String? arg_name, String? arg_key, String? arg_value) => + (super.noSuchMethod( + Invocation.method(#setFlowAttribute, [arg_name, arg_key, arg_value]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i9.Future); + @override + _i9.Future endFlow(String? arg_name) => + (super.noSuchMethod(Invocation.method(#endFlow, [arg_name]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i9.Future); + @override + _i9.Future setExecutionTraceAttribute( + String? arg_id, String? arg_key, String? arg_value) => + (super.noSuchMethod( + Invocation.method( + #setExecutionTraceAttribute, [arg_id, arg_key, arg_value]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i9.Future); + @override + _i9.Future endExecutionTrace(String? arg_id) => + (super.noSuchMethod(Invocation.method(#endExecutionTrace, [arg_id]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i9.Future); + @override + _i9.Future startUITrace(String? arg_name) => + (super.noSuchMethod(Invocation.method(#startUITrace, [arg_name]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i9.Future); + @override + _i9.Future endUITrace() => + (super.noSuchMethod(Invocation.method(#endUITrace, []), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i9.Future); + @override + _i9.Future endAppLaunch() => + (super.noSuchMethod(Invocation.method(#endAppLaunch, []), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i9.Future); + @override + _i9.Future networkLogAndroid(Map? arg_data) => + (super.noSuchMethod(Invocation.method(#networkLogAndroid, [arg_data]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i9.Future); + @override + _i9.Future startCpUiTrace( + String? arg_screenName, int? arg_microTimeStamp, int? arg_traceId) => + (super.noSuchMethod( + Invocation.method(#startCpUiTrace, + [arg_screenName, arg_microTimeStamp, arg_traceId]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i9.Future); + @override + _i9.Future reportScreenLoadingCP(int? arg_startTimeStampMicro, + int? arg_durationMicro, int? arg_uiTraceId) => + (super.noSuchMethod( + Invocation.method(#reportScreenLoadingCP, + [arg_startTimeStampMicro, arg_durationMicro, arg_uiTraceId]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i9.Future); + @override + _i9.Future endScreenLoadingCP( + int? arg_timeStampMicro, int? arg_uiTraceId) => + (super.noSuchMethod( + Invocation.method( + #endScreenLoadingCP, [arg_timeStampMicro, arg_uiTraceId]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i9.Future); + @override + _i9.Future isEndScreenLoadingEnabled() => + (super.noSuchMethod(Invocation.method(#isEndScreenLoadingEnabled, []), + returnValue: Future.value(false)) as _i9.Future); + @override + _i9.Future isScreenRenderEnabled() => + (super.noSuchMethod(Invocation.method(#isScreenRenderEnabled, []), + returnValue: Future.value(false)) as _i9.Future); + @override + _i9.Future deviceRefreshRate() => + (super.noSuchMethod(Invocation.method(#deviceRefreshRate, []), + returnValue: Future.value(0.0)) as _i9.Future); + @override + _i9.Future setScreenRenderEnabled(bool? arg_isEnabled) => (super + .noSuchMethod(Invocation.method(#setScreenRenderEnabled, [arg_isEnabled]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i9.Future); + @override + _i9.Future endScreenRenderForAutoUiTrace( + Map? arg_data) => + (super.noSuchMethod( + Invocation.method(#endScreenRenderForAutoUiTrace, [arg_data]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i9.Future); + @override + _i9.Future endScreenRenderForCustomUiTrace( + Map? arg_data) => + (super.noSuchMethod( + Invocation.method(#endScreenRenderForCustomUiTrace, [arg_data]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i9.Future); +} + +/// A class which mocks [WidgetsBinding]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockWidgetsBinding extends _i1.Mock implements _i10.WidgetsBinding { + MockWidgetsBinding() { + _i1.throwOnMissingStub(this); + } + + @override + bool get debugBuildingDirtyElements => + (super.noSuchMethod(Invocation.getter(#debugBuildingDirtyElements), + returnValue: false) as bool); + @override + set debugBuildingDirtyElements(bool? _debugBuildingDirtyElements) => + super.noSuchMethod( + Invocation.setter( + #debugBuildingDirtyElements, _debugBuildingDirtyElements), + returnValueForMissingStub: null); + @override + _i2.FocusManager get focusManager => + (super.noSuchMethod(Invocation.getter(#focusManager), + returnValue: _FakeFocusManager_0()) as _i2.FocusManager); + @override + bool get firstFrameRasterized => + (super.noSuchMethod(Invocation.getter(#firstFrameRasterized), + returnValue: false) as bool); + @override + _i9.Future get waitUntilFirstFrameRasterized => + (super.noSuchMethod(Invocation.getter(#waitUntilFirstFrameRasterized), + returnValue: Future.value()) as _i9.Future); + @override + bool get debugDidSendFirstFrameEvent => + (super.noSuchMethod(Invocation.getter(#debugDidSendFirstFrameEvent), + returnValue: false) as bool); + @override + bool get framesEnabled => + (super.noSuchMethod(Invocation.getter(#framesEnabled), returnValue: false) + as bool); + @override + bool get isRootWidgetAttached => + (super.noSuchMethod(Invocation.getter(#isRootWidgetAttached), + returnValue: false) as bool); + @override + _i4.SingletonFlutterWindow get window => + (super.noSuchMethod(Invocation.getter(#window), + returnValue: _FakeSingletonFlutterWindow_1()) + as _i4.SingletonFlutterWindow); + @override + _i4.PlatformDispatcher get platformDispatcher => + (super.noSuchMethod(Invocation.getter(#platformDispatcher), + returnValue: _FakePlatformDispatcher_2()) as _i4.PlatformDispatcher); + @override + bool get locked => + (super.noSuchMethod(Invocation.getter(#locked), returnValue: false) + as bool); + @override + _i5.HardwareKeyboard get keyboard => + (super.noSuchMethod(Invocation.getter(#keyboard), + returnValue: _FakeHardwareKeyboard_3()) as _i5.HardwareKeyboard); + @override + _i5.KeyEventManager get keyEventManager => + (super.noSuchMethod(Invocation.getter(#keyEventManager), + returnValue: _FakeKeyEventManager_4()) as _i5.KeyEventManager); + @override + _i5.BinaryMessenger get defaultBinaryMessenger => + (super.noSuchMethod(Invocation.getter(#defaultBinaryMessenger), + returnValue: _FakeBinaryMessenger_5()) as _i5.BinaryMessenger); + @override + _i4.ChannelBuffers get channelBuffers => + (super.noSuchMethod(Invocation.getter(#channelBuffers), + returnValue: _FakeChannelBuffers_6()) as _i4.ChannelBuffers); + @override + _i5.RestorationManager get restorationManager => + (super.noSuchMethod(Invocation.getter(#restorationManager), + returnValue: _FakeRestorationManager_7()) as _i5.RestorationManager); + @override + _i11.SchedulingStrategy get schedulingStrategy => + (super.noSuchMethod(Invocation.getter(#schedulingStrategy), + returnValue: ({int? priority, _i11.SchedulerBinding? scheduler}) => + false) as _i11.SchedulingStrategy); + @override + set schedulingStrategy(_i11.SchedulingStrategy? _schedulingStrategy) => super + .noSuchMethod(Invocation.setter(#schedulingStrategy, _schedulingStrategy), + returnValueForMissingStub: null); + @override + int get transientCallbackCount => + (super.noSuchMethod(Invocation.getter(#transientCallbackCount), + returnValue: 0) as int); + @override + _i9.Future get endOfFrame => + (super.noSuchMethod(Invocation.getter(#endOfFrame), + returnValue: Future.value()) as _i9.Future); + @override + bool get hasScheduledFrame => + (super.noSuchMethod(Invocation.getter(#hasScheduledFrame), + returnValue: false) as bool); + @override + _i11.SchedulerPhase get schedulerPhase => + (super.noSuchMethod(Invocation.getter(#schedulerPhase), + returnValue: _i11.SchedulerPhase.idle) as _i11.SchedulerPhase); + @override + Duration get currentFrameTimeStamp => + (super.noSuchMethod(Invocation.getter(#currentFrameTimeStamp), + returnValue: _FakeDuration_8()) as Duration); + @override + Duration get currentSystemFrameTimeStamp => + (super.noSuchMethod(Invocation.getter(#currentSystemFrameTimeStamp), + returnValue: _FakeDuration_8()) as Duration); + @override + _i6.PointerRouter get pointerRouter => + (super.noSuchMethod(Invocation.getter(#pointerRouter), + returnValue: _FakePointerRouter_9()) as _i6.PointerRouter); + @override + _i6.GestureArenaManager get gestureArena => (super.noSuchMethod( + Invocation.getter(#gestureArena), + returnValue: _FakeGestureArenaManager_10()) as _i6.GestureArenaManager); + @override + _i6.PointerSignalResolver get pointerSignalResolver => + (super.noSuchMethod(Invocation.getter(#pointerSignalResolver), + returnValue: _FakePointerSignalResolver_11()) + as _i6.PointerSignalResolver); + @override + bool get resamplingEnabled => + (super.noSuchMethod(Invocation.getter(#resamplingEnabled), + returnValue: false) as bool); + @override + set resamplingEnabled(bool? _resamplingEnabled) => super.noSuchMethod( + Invocation.setter(#resamplingEnabled, _resamplingEnabled), + returnValueForMissingStub: null); + @override + Duration get samplingOffset => + (super.noSuchMethod(Invocation.getter(#samplingOffset), + returnValue: _FakeDuration_8()) as Duration); + @override + set samplingOffset(Duration? _samplingOffset) => + super.noSuchMethod(Invocation.setter(#samplingOffset, _samplingOffset), + returnValueForMissingStub: null); + @override + _i7.MouseTracker get mouseTracker => + (super.noSuchMethod(Invocation.getter(#mouseTracker), + returnValue: _FakeMouseTracker_12()) as _i7.MouseTracker); + @override + _i7.PipelineOwner get pipelineOwner => + (super.noSuchMethod(Invocation.getter(#pipelineOwner), + returnValue: _FakePipelineOwner_13()) as _i7.PipelineOwner); + @override + _i7.RenderView get renderView => + (super.noSuchMethod(Invocation.getter(#renderView), + returnValue: _FakeRenderView_14()) as _i7.RenderView); + @override + set renderView(_i7.RenderView? value) => + super.noSuchMethod(Invocation.setter(#renderView, value), + returnValueForMissingStub: null); + @override + bool get sendFramesToEngine => + (super.noSuchMethod(Invocation.getter(#sendFramesToEngine), + returnValue: false) as bool); + @override + _i4.AccessibilityFeatures get accessibilityFeatures => + (super.noSuchMethod(Invocation.getter(#accessibilityFeatures), + returnValue: _FakeAccessibilityFeatures_15()) + as _i4.AccessibilityFeatures); + @override + bool get disableAnimations => + (super.noSuchMethod(Invocation.getter(#disableAnimations), + returnValue: false) as bool); + @override + void initInstances() => + super.noSuchMethod(Invocation.method(#initInstances, []), + returnValueForMissingStub: null); + @override + void initServiceExtensions() => + super.noSuchMethod(Invocation.method(#initServiceExtensions, []), + returnValueForMissingStub: null); + @override + void addObserver(_i10.WidgetsBindingObserver? observer) => + super.noSuchMethod(Invocation.method(#addObserver, [observer]), + returnValueForMissingStub: null); + @override + bool removeObserver(_i10.WidgetsBindingObserver? observer) => + (super.noSuchMethod(Invocation.method(#removeObserver, [observer]), + returnValue: false) as bool); + @override + void handleMetricsChanged() => + super.noSuchMethod(Invocation.method(#handleMetricsChanged, []), + returnValueForMissingStub: null); + @override + void handleTextScaleFactorChanged() => + super.noSuchMethod(Invocation.method(#handleTextScaleFactorChanged, []), + returnValueForMissingStub: null); + @override + void handlePlatformBrightnessChanged() => super.noSuchMethod( + Invocation.method(#handlePlatformBrightnessChanged, []), + returnValueForMissingStub: null); + @override + void handleAccessibilityFeaturesChanged() => super.noSuchMethod( + Invocation.method(#handleAccessibilityFeaturesChanged, []), + returnValueForMissingStub: null); + @override + void handleLocaleChanged() => + super.noSuchMethod(Invocation.method(#handleLocaleChanged, []), + returnValueForMissingStub: null); + @override + void dispatchLocalesChanged(List<_i4.Locale>? locales) => + super.noSuchMethod(Invocation.method(#dispatchLocalesChanged, [locales]), + returnValueForMissingStub: null); + @override + void dispatchAccessibilityFeaturesChanged() => super.noSuchMethod( + Invocation.method(#dispatchAccessibilityFeaturesChanged, []), + returnValueForMissingStub: null); + @override + _i9.Future handlePopRoute() => + (super.noSuchMethod(Invocation.method(#handlePopRoute, []), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i9.Future); + @override + _i9.Future handlePushRoute(String? route) => + (super.noSuchMethod(Invocation.method(#handlePushRoute, [route]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i9.Future); + @override + void handleAppLifecycleStateChanged(_i4.AppLifecycleState? state) => super + .noSuchMethod(Invocation.method(#handleAppLifecycleStateChanged, [state]), + returnValueForMissingStub: null); + @override + void handleMemoryPressure() => + super.noSuchMethod(Invocation.method(#handleMemoryPressure, []), + returnValueForMissingStub: null); + @override + void drawFrame() => super.noSuchMethod(Invocation.method(#drawFrame, []), + returnValueForMissingStub: null); + @override + void scheduleAttachRootWidget(_i12.Widget? rootWidget) => super.noSuchMethod( + Invocation.method(#scheduleAttachRootWidget, [rootWidget]), + returnValueForMissingStub: null); + @override + void attachRootWidget(_i12.Widget? rootWidget) => + super.noSuchMethod(Invocation.method(#attachRootWidget, [rootWidget]), + returnValueForMissingStub: null); + @override + _i9.Future performReassemble() => + (super.noSuchMethod(Invocation.method(#performReassemble, []), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i9.Future); + @override + _i4.Locale? computePlatformResolvedLocale( + List<_i4.Locale>? supportedLocales) => + (super.noSuchMethod(Invocation.method( + #computePlatformResolvedLocale, [supportedLocales])) as _i4.Locale?); + @override + _i9.Future lockEvents(_i9.Future Function()? callback) => + (super.noSuchMethod(Invocation.method(#lockEvents, [callback]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i9.Future); + @override + void unlocked() => super.noSuchMethod(Invocation.method(#unlocked, []), + returnValueForMissingStub: null); + @override + _i9.Future reassembleApplication() => + (super.noSuchMethod(Invocation.method(#reassembleApplication, []), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i9.Future); + @override + void registerSignalServiceExtension( + {String? name, _i3.AsyncCallback? callback}) => + super.noSuchMethod( + Invocation.method(#registerSignalServiceExtension, [], + {#name: name, #callback: callback}), + returnValueForMissingStub: null); + @override + void registerBoolServiceExtension( + {String? name, + _i3.AsyncValueGetter? getter, + _i3.AsyncValueSetter? setter}) => + super.noSuchMethod( + Invocation.method(#registerBoolServiceExtension, [], + {#name: name, #getter: getter, #setter: setter}), + returnValueForMissingStub: null); + @override + void registerNumericServiceExtension( + {String? name, + _i3.AsyncValueGetter? getter, + _i3.AsyncValueSetter? setter}) => + super.noSuchMethod( + Invocation.method(#registerNumericServiceExtension, [], + {#name: name, #getter: getter, #setter: setter}), + returnValueForMissingStub: null); + @override + void postEvent(String? eventKind, Map? eventData) => + super.noSuchMethod(Invocation.method(#postEvent, [eventKind, eventData]), + returnValueForMissingStub: null); + @override + void registerStringServiceExtension( + {String? name, + _i3.AsyncValueGetter? getter, + _i3.AsyncValueSetter? setter}) => + super.noSuchMethod( + Invocation.method(#registerStringServiceExtension, [], + {#name: name, #getter: getter, #setter: setter}), + returnValueForMissingStub: null); + @override + void registerServiceExtension( + {String? name, _i3.ServiceExtensionCallback? callback}) => + super.noSuchMethod( + Invocation.method(#registerServiceExtension, [], + {#name: name, #callback: callback}), + returnValueForMissingStub: null); + @override + _i5.BinaryMessenger createBinaryMessenger() => + (super.noSuchMethod(Invocation.method(#createBinaryMessenger, []), + returnValue: _FakeBinaryMessenger_5()) as _i5.BinaryMessenger); + @override + _i9.Future handleSystemMessage(Object? systemMessage) => (super + .noSuchMethod(Invocation.method(#handleSystemMessage, [systemMessage]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i9.Future); + @override + void initLicenses() => + super.noSuchMethod(Invocation.method(#initLicenses, []), + returnValueForMissingStub: null); + @override + void evict(String? asset) => + super.noSuchMethod(Invocation.method(#evict, [asset]), + returnValueForMissingStub: null); + @override + void readInitialLifecycleStateFromNativeWindow() => super.noSuchMethod( + Invocation.method(#readInitialLifecycleStateFromNativeWindow, []), + returnValueForMissingStub: null); + @override + _i5.RestorationManager createRestorationManager() => + (super.noSuchMethod(Invocation.method(#createRestorationManager, []), + returnValue: _FakeRestorationManager_7()) as _i5.RestorationManager); + @override + void setSystemUiChangeCallback(_i5.SystemUiChangeCallback? callback) => super + .noSuchMethod(Invocation.method(#setSystemUiChangeCallback, [callback]), + returnValueForMissingStub: null); + @override + void addTimingsCallback(_i4.TimingsCallback? callback) => + super.noSuchMethod(Invocation.method(#addTimingsCallback, [callback]), + returnValueForMissingStub: null); + @override + void removeTimingsCallback(_i4.TimingsCallback? callback) => + super.noSuchMethod(Invocation.method(#removeTimingsCallback, [callback]), + returnValueForMissingStub: null); + @override + _i9.Future scheduleTask( + _i11.TaskCallback? task, _i11.Priority? priority, + {String? debugLabel, _i13.Flow? flow}) => + (super.noSuchMethod( + Invocation.method(#scheduleTask, [task, priority], + {#debugLabel: debugLabel, #flow: flow}), + returnValue: Future.value(null)) as _i9.Future); + @override + bool handleEventLoopCallback() => + (super.noSuchMethod(Invocation.method(#handleEventLoopCallback, []), + returnValue: false) as bool); + @override + int scheduleFrameCallback(_i11.FrameCallback? callback, + {bool? rescheduling = false}) => + (super.noSuchMethod( + Invocation.method(#scheduleFrameCallback, [callback], + {#rescheduling: rescheduling}), + returnValue: 0) as int); + @override + void cancelFrameCallbackWithId(int? id) => + super.noSuchMethod(Invocation.method(#cancelFrameCallbackWithId, [id]), + returnValueForMissingStub: null); + @override + bool debugAssertNoTransientCallbacks(String? reason) => (super.noSuchMethod( + Invocation.method(#debugAssertNoTransientCallbacks, [reason]), + returnValue: false) as bool); + @override + void addPersistentFrameCallback(_i11.FrameCallback? callback) => super + .noSuchMethod(Invocation.method(#addPersistentFrameCallback, [callback]), + returnValueForMissingStub: null); + @override + void addPostFrameCallback(_i11.FrameCallback? callback) => + super.noSuchMethod(Invocation.method(#addPostFrameCallback, [callback]), + returnValueForMissingStub: null); + @override + void ensureFrameCallbacksRegistered() => + super.noSuchMethod(Invocation.method(#ensureFrameCallbacksRegistered, []), + returnValueForMissingStub: null); + @override + void ensureVisualUpdate() => + super.noSuchMethod(Invocation.method(#ensureVisualUpdate, []), + returnValueForMissingStub: null); + @override + void scheduleFrame() => + super.noSuchMethod(Invocation.method(#scheduleFrame, []), + returnValueForMissingStub: null); + @override + void scheduleForcedFrame() => + super.noSuchMethod(Invocation.method(#scheduleForcedFrame, []), + returnValueForMissingStub: null); + @override + void scheduleWarmUpFrame() => + super.noSuchMethod(Invocation.method(#scheduleWarmUpFrame, []), + returnValueForMissingStub: null); + @override + void resetEpoch() => super.noSuchMethod(Invocation.method(#resetEpoch, []), + returnValueForMissingStub: null); + @override + void handleBeginFrame(Duration? rawTimeStamp) => + super.noSuchMethod(Invocation.method(#handleBeginFrame, [rawTimeStamp]), + returnValueForMissingStub: null); + @override + void handleDrawFrame() => + super.noSuchMethod(Invocation.method(#handleDrawFrame, []), + returnValueForMissingStub: null); + @override + void cancelPointer(int? pointer) => + super.noSuchMethod(Invocation.method(#cancelPointer, [pointer]), + returnValueForMissingStub: null); + @override + void handlePointerEvent(_i6.PointerEvent? event) => + super.noSuchMethod(Invocation.method(#handlePointerEvent, [event]), + returnValueForMissingStub: null); + @override + void hitTest(_i6.HitTestResult? result, _i4.Offset? position) => + super.noSuchMethod(Invocation.method(#hitTest, [result, position]), + returnValueForMissingStub: null); + @override + void dispatchEvent( + _i6.PointerEvent? event, _i6.HitTestResult? hitTestResult) => + super.noSuchMethod( + Invocation.method(#dispatchEvent, [event, hitTestResult]), + returnValueForMissingStub: null); + @override + void handleEvent(_i6.PointerEvent? event, _i6.HitTestEntry? entry) => + super.noSuchMethod(Invocation.method(#handleEvent, [event, entry]), + returnValueForMissingStub: null); + @override + void resetGestureBinding() => + super.noSuchMethod(Invocation.method(#resetGestureBinding, []), + returnValueForMissingStub: null); + @override + void initRenderView() => + super.noSuchMethod(Invocation.method(#initRenderView, []), + returnValueForMissingStub: null); + @override + _i7.ViewConfiguration createViewConfiguration() => + (super.noSuchMethod(Invocation.method(#createViewConfiguration, []), + returnValue: _FakeViewConfiguration_16()) as _i7.ViewConfiguration); + @override + void initMouseTracker([_i7.MouseTracker? tracker]) => + super.noSuchMethod(Invocation.method(#initMouseTracker, [tracker]), + returnValueForMissingStub: null); + @override + void setSemanticsEnabled(bool? enabled) => + super.noSuchMethod(Invocation.method(#setSemanticsEnabled, [enabled]), + returnValueForMissingStub: null); + @override + void deferFirstFrame() => + super.noSuchMethod(Invocation.method(#deferFirstFrame, []), + returnValueForMissingStub: null); + @override + void allowFirstFrame() => + super.noSuchMethod(Invocation.method(#allowFirstFrame, []), + returnValueForMissingStub: null); + @override + void resetFirstFrameSent() => + super.noSuchMethod(Invocation.method(#resetFirstFrameSent, []), + returnValueForMissingStub: null); + @override + _i4.SemanticsUpdateBuilder createSemanticsUpdateBuilder() => + (super.noSuchMethod(Invocation.method(#createSemanticsUpdateBuilder, []), + returnValue: _FakeSemanticsUpdateBuilder_17()) + as _i4.SemanticsUpdateBuilder); +} + +/// A class which mocks [FrameTiming]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockFrameTiming extends _i1.Mock implements _i4.FrameTiming { + MockFrameTiming() { + _i1.throwOnMissingStub(this); + } + + @override + Duration get buildDuration => + (super.noSuchMethod(Invocation.getter(#buildDuration), + returnValue: _FakeDuration_8()) as Duration); + @override + Duration get rasterDuration => + (super.noSuchMethod(Invocation.getter(#rasterDuration), + returnValue: _FakeDuration_8()) as Duration); + @override + Duration get vsyncOverhead => + (super.noSuchMethod(Invocation.getter(#vsyncOverhead), + returnValue: _FakeDuration_8()) as Duration); + @override + Duration get totalSpan => (super.noSuchMethod(Invocation.getter(#totalSpan), + returnValue: _FakeDuration_8()) as Duration); + @override + int get layerCacheCount => + (super.noSuchMethod(Invocation.getter(#layerCacheCount), returnValue: 0) + as int); + @override + int get layerCacheBytes => + (super.noSuchMethod(Invocation.getter(#layerCacheBytes), returnValue: 0) + as int); + @override + double get layerCacheMegabytes => + (super.noSuchMethod(Invocation.getter(#layerCacheMegabytes), + returnValue: 0.0) as double); + @override + int get pictureCacheCount => + (super.noSuchMethod(Invocation.getter(#pictureCacheCount), returnValue: 0) + as int); + @override + int get pictureCacheBytes => + (super.noSuchMethod(Invocation.getter(#pictureCacheBytes), returnValue: 0) + as int); + @override + double get pictureCacheMegabytes => + (super.noSuchMethod(Invocation.getter(#pictureCacheMegabytes), + returnValue: 0.0) as double); + @override + int get frameNumber => + (super.noSuchMethod(Invocation.getter(#frameNumber), returnValue: 0) + as int); + @override + int timestampInMicroseconds(_i4.FramePhase? phase) => + (super.noSuchMethod(Invocation.method(#timestampInMicroseconds, [phase]), + returnValue: 0) as int); +} diff --git a/test/utils/screen_render/instabug_widget_binding_observer_test.dart b/test/utils/screen_render/instabug_widget_binding_observer_test.dart new file mode 100644 index 000000000..b97d312a0 --- /dev/null +++ b/test/utils/screen_render/instabug_widget_binding_observer_test.dart @@ -0,0 +1,109 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.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:instabug_flutter/src/utils/screen_rendering/instabug_screen_render_manager.dart'; +import 'package:instabug_flutter/src/utils/screen_rendering/instabug_widget_binding_observer.dart'; +import 'package:instabug_flutter/src/utils/ui_trace/ui_trace.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'instabug_widget_binding_observer_test.mocks.dart'; + +@GenerateMocks([ + InstabugScreenRenderManager, + ScreenLoadingManager, + ScreenNameMasker, + UiTrace, +]) +void main() { + late MockInstabugScreenRenderManager mockRenderManager; + late MockScreenLoadingManager mockLoadingManager; + late MockScreenNameMasker mockNameMasker; + late MockUiTrace mockUiTrace; + + TestWidgetsFlutterBinding.ensureInitialized(); + + setUp(() { + mockRenderManager = MockInstabugScreenRenderManager(); + mockLoadingManager = MockScreenLoadingManager(); + mockNameMasker = MockScreenNameMasker(); + mockUiTrace = MockUiTrace(); + + // Inject singleton mocks + InstabugScreenRenderManager.setInstance(mockRenderManager); + ScreenLoadingManager.setInstance(mockLoadingManager); + ScreenNameMasker.setInstance(mockNameMasker); + }); + + group('InstabugWidgetsBindingObserver', () { + test('returns the singleton instance', () { + final instance = InstabugWidgetsBindingObserver.instance; + final shorthand = InstabugWidgetsBindingObserver.I; + expect(instance, isA()); + expect(shorthand, same(instance)); + }); + + test('handles AppLifecycleState.resumed and starts UiTrace', () async { + when(mockLoadingManager.currentUiTrace).thenReturn(mockUiTrace); + when(mockUiTrace.screenName).thenReturn("HomeScreen"); + when(mockNameMasker.mask("HomeScreen")).thenReturn("MaskedHome"); + when(mockLoadingManager.startUiTrace("MaskedHome", "HomeScreen")) + .thenAnswer((_) async => 123); + when(mockRenderManager.screenRenderEnabled).thenReturn(true); + + InstabugWidgetsBindingObserver.I + .didChangeAppLifecycleState(AppLifecycleState.resumed); + + // wait for async call to complete + await untilCalled( + mockRenderManager.startScreenRenderCollectorForTraceId(123), + ); + + verify(mockRenderManager.startScreenRenderCollectorForTraceId(123)) + .called(1); + }); + + test('handles AppLifecycleState.paused and stops render collector', () { + when(mockRenderManager.screenRenderEnabled).thenReturn(true); + + InstabugWidgetsBindingObserver.I + .didChangeAppLifecycleState(AppLifecycleState.paused); + + verify(mockRenderManager.stopScreenRenderCollector()).called(1); + }); + + test('handles AppLifecycleState.detached and stops render collector', () { + when(mockRenderManager.screenRenderEnabled).thenReturn(true); + + InstabugWidgetsBindingObserver.I + .didChangeAppLifecycleState(AppLifecycleState.detached); + + verify(mockRenderManager.stopScreenRenderCollector()).called(1); + }); + + test('handles AppLifecycleState.inactive with no action', () { + // Just ensure it doesn't crash + expect( + () { + InstabugWidgetsBindingObserver.I + .didChangeAppLifecycleState(AppLifecycleState.inactive); + }, + returnsNormally, + ); + }); + + test('_handleResumedState does nothing if no currentUiTrace', () { + when(mockLoadingManager.currentUiTrace).thenReturn(null); + + InstabugWidgetsBindingObserver.I + .didChangeAppLifecycleState(AppLifecycleState.resumed); + + verifyNever(mockRenderManager.startScreenRenderCollectorForTraceId(any)); + }); + + test('checkForWidgetBinding ensures initialization', () { + expect(() => checkForWidgetBinding(), returnsNormally); + }); + }); +}