From fa155442cf974e9d28b5cd0c1e68fcb8845e783b Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Thu, 4 Jul 2024 16:50:26 +0200 Subject: [PATCH 1/4] App start now takes AppStartMetrics.appLaunchedInForeground variable to add spans to the transaction App starts longer than 1 minute are dropped (same as Firebase) --- .../api/sentry-android-core.api | 3 +- .../core/ActivityLifecycleIntegration.java | 3 +- .../io/sentry/android/core/ContextUtils.java | 76 ++++++++++++++++--- .../io/sentry/android/core/SentryAndroid.java | 5 +- .../core/performance/AppStartMetrics.java | 32 +++++++- .../core/ActivityLifecycleIntegrationTest.kt | 1 + .../sentry/android/core/ContextUtilsTest.kt | 6 +- .../PerformanceAndroidEventProcessorTest.kt | 1 + .../sentry/android/core/SentryAndroidTest.kt | 2 +- .../core/performance/AppStartMetricsTest.kt | 68 +++++++++++++++++ 10 files changed, 177 insertions(+), 20 deletions(-) diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api index adcc6ea87d..ed624a3559 100644 --- a/sentry-android-core/api/sentry-android-core.api +++ b/sentry-android-core/api/sentry-android-core.api @@ -158,7 +158,7 @@ public final class io/sentry/android/core/BuildInfoProvider { } public final class io/sentry/android/core/ContextUtils { - public static fun isForegroundImportance ()Z + public static fun isForegroundImportance (Landroid/content/Context;Lio/sentry/android/core/BuildInfoProvider;)Z } public class io/sentry/android/core/CurrentActivityHolder { @@ -445,6 +445,7 @@ public class io/sentry/android/core/performance/AppStartMetrics { public static fun onApplicationPostCreate (Landroid/app/Application;)V public static fun onContentProviderCreate (Landroid/content/ContentProvider;)V public static fun onContentProviderPostCreate (Landroid/content/ContentProvider;)V + public fun setAppLaunchedInForeground (Z)V public fun setAppStartProfiler (Lio/sentry/ITransactionProfiler;)V public fun setAppStartSamplingDecision (Lio/sentry/TracesSamplingDecision;)V public fun setAppStartType (Lio/sentry/android/core/performance/AppStartMetrics$AppStartType;)V diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java index 1121a6bfe7..7e804038fa 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java @@ -167,7 +167,8 @@ private void startTracing(final @NotNull Activity activity) { // we only track app start for processes that will show an Activity (full launch). // Here we check the process importance which will tell us that. - final boolean foregroundImportance = ContextUtils.isForegroundImportance(); + final boolean foregroundImportance = + ContextUtils.isForegroundImportance(activity, buildInfoProvider); if (foregroundImportance && appStartTimeSpan.hasStarted()) { appStartTime = appStartTimeSpan.getStartTimestamp(); coldStart = diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ContextUtils.java b/sentry-android-core/src/main/java/io/sentry/android/core/ContextUtils.java index 2e76de4d12..ec77f5bdcf 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ContextUtils.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ContextUtils.java @@ -1,6 +1,5 @@ package io.sentry.android.core; -import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND; import static android.content.Context.ACTIVITY_SERVICE; import static android.content.Context.RECEIVER_EXPORTED; import static android.content.pm.PackageInfo.REQUESTED_PERMISSION_GRANTED; @@ -15,6 +14,7 @@ import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.os.Build; +import android.os.PowerManager; import android.provider.Settings; import android.util.DisplayMetrics; import io.sentry.ILogger; @@ -26,6 +26,7 @@ import java.io.FileReader; import java.io.IOException; import java.util.HashMap; +import java.util.List; import java.util.Map; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -161,22 +162,77 @@ static String getVersionName(final @NotNull PackageInfo packageInfo) { return Integer.toString(packageInfo.versionCode); } + /* + * https://github.com/firebase/firebase-android-sdk/blob/58540de24c9b1eb7780c9f642c2cf17478e65734/firebase-perf/src/main/java/com/google/firebase/perf/metrics/AppStartTrace.java#L497 + * + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ /** * Check if the Started process has IMPORTANCE_FOREGROUND importance which means that the process * will start an Activity. * * @return true if IMPORTANCE_FOREGROUND and false otherwise */ - @ApiStatus.Internal - public static boolean isForegroundImportance() { - try { - final ActivityManager.RunningAppProcessInfo appProcessInfo = - new ActivityManager.RunningAppProcessInfo(); - ActivityManager.getMyMemoryState(appProcessInfo); - return appProcessInfo.importance == IMPORTANCE_FOREGROUND; - } catch (Throwable ignored) { - // should never happen + @SuppressLint("NewApi") + @SuppressWarnings("deprecation") + public static boolean isForegroundImportance( + final @NotNull Context appContext, final @NotNull BuildInfoProvider buildInfoProvider) { + + // Do not call ProcessStats.getActivityManger, caching will break tests that indirectly depend + // on ProcessStats. + ActivityManager activityManager = + (ActivityManager) appContext.getSystemService(Context.ACTIVITY_SERVICE); + if (activityManager == null) { + return true; + } + List appProcesses = + activityManager.getRunningAppProcesses(); + if (appProcesses != null) { + String appProcessName = appContext.getPackageName(); + String allowedAppProcessNamePrefix = appProcessName + ":"; + for (ActivityManager.RunningAppProcessInfo appProcess : appProcesses) { + if (appProcess.importance != ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) { + continue; + } + if (appProcess.processName.equals(appProcessName) + || appProcess.processName.startsWith(allowedAppProcessNamePrefix)) { + boolean isAppInForeground = true; + + // For the case when the app is in foreground and the device transitions to sleep mode, + // the importance of the process is set to IMPORTANCE_TOP_SLEEPING. However, this + // importance level was introduced in M. Pre M, the process importance is not changed to + // IMPORTANCE_TOP_SLEEPING when the display turns off. So we need to rely also on the + // state of the display to decide if any app process is really visible. + if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.M) { + PowerManager powerManager = + (PowerManager) appContext.getSystemService(Context.POWER_SERVICE); + if (powerManager != null) { + isAppInForeground = + buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.KITKAT_WATCH + ? powerManager.isInteractive() + : powerManager.isScreenOn(); + } + } + + if (isAppInForeground) { + return true; + } + } + } } + return false; } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java index 46590826ef..1fee9efae0 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java @@ -87,6 +87,7 @@ public static synchronized void init( @NotNull Sentry.OptionsConfiguration configuration) { try { + final BuildInfoProvider buildInfoProvider = new BuildInfoProvider(logger); Sentry.init( OptionsContainer.create(SentryAndroidOptions.class), options -> { @@ -103,7 +104,6 @@ public static synchronized void init( (isTimberUpstreamAvailable && classLoader.isClassAvailable(SENTRY_TIMBER_INTEGRATION_CLASS_NAME, options)); - final BuildInfoProvider buildInfoProvider = new BuildInfoProvider(logger); final LoadClass loadClass = new LoadClass(); final ActivityFramesTracker activityFramesTracker = new ActivityFramesTracker(loadClass, options); @@ -148,7 +148,8 @@ public static synchronized void init( true); final @NotNull IHub hub = Sentry.getCurrentHub(); - if (hub.getOptions().isEnableAutoSessionTracking() && ContextUtils.isForegroundImportance()) { + if (hub.getOptions().isEnableAutoSessionTracking() + && ContextUtils.isForegroundImportance(context, buildInfoProvider)) { // The LifecycleWatcher of AppLifecycleIntegration may already started a session // so only start a session if it's not already started // This e.g. happens on React Native, or e.g. on deferred SDK init diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java b/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java index 5c29e95b63..fc5a5f81e8 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java @@ -4,8 +4,11 @@ import android.content.ContentProvider; import android.os.SystemClock; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import io.sentry.ITransactionProfiler; import io.sentry.TracesSamplingDecision; +import io.sentry.android.core.AndroidLogger; +import io.sentry.android.core.BuildInfoProvider; import io.sentry.android.core.ContextUtils; import io.sentry.android.core.SentryAndroidOptions; import java.util.ArrayList; @@ -13,6 +16,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.TimeUnit; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.TestOnly; @@ -102,6 +106,11 @@ public boolean isAppLaunchedInForeground() { return appLaunchedInForeground; } + @VisibleForTesting + public void setAppLaunchedInForeground(boolean appLaunchedInForeground) { + this.appLaunchedInForeground = appLaunchedInForeground; + } + /** * Provides all collected content provider onCreate time spans * @@ -137,12 +146,27 @@ public long getClassLoadedUptimeMs() { // Only started when sdk version is >= N final @NotNull TimeSpan appStartSpan = getAppStartTimeSpan(); if (appStartSpan.hasStarted()) { - return appStartSpan; + return validateAppStartSpan(appStartSpan); } } // fallback: use sdk init time span, as it will always have a start time set - return getSdkInitTimeSpan(); + return validateAppStartSpan(getSdkInitTimeSpan()); + } + + private @NotNull TimeSpan validateAppStartSpan(final @NotNull TimeSpan appStartSpan) { + long spanStartMillis = appStartSpan.getStartTimestampMs(); + long spanEndMillis = + appStartSpan.hasStopped() + ? appStartSpan.getProjectedStopTimestampMs() + : SystemClock.uptimeMillis(); + long durationMillis = spanEndMillis - spanStartMillis; + // If the app was launched more than 1 minute ago or it was launched in the background we return + // an empty span, as the app start will be wrong + if (durationMillis > TimeUnit.MINUTES.toMillis(1) || !isAppLaunchedInForeground()) { + return new TimeSpan(); + } + return appStartSpan; } @TestOnly @@ -195,7 +219,9 @@ public static void onApplicationCreate(final @NotNull Application application) { final @NotNull AppStartMetrics instance = getInstance(); if (instance.applicationOnCreate.hasNotStarted()) { instance.applicationOnCreate.setStartedAt(now); - instance.appLaunchedInForeground = ContextUtils.isForegroundImportance(); + instance.appLaunchedInForeground = + ContextUtils.isForegroundImportance( + application, new BuildInfoProvider(new AndroidLogger())); } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt index f936b6251c..897c4628c5 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt @@ -94,6 +94,7 @@ class ActivityLifecycleIntegrationTest { whenever(hub.options).thenReturn(options) + AppStartMetrics.getInstance().isAppLaunchedInForeground = true // We let the ActivityLifecycleIntegration create the proper transaction here val optionCaptor = argumentCaptor() val contextCaptor = argumentCaptor() diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ContextUtilsTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ContextUtilsTest.kt index b758fae1f8..8c7e295c0e 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ContextUtilsTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ContextUtilsTest.kt @@ -38,6 +38,7 @@ class ContextUtilsTest { private lateinit var shadowActivityManager: ShadowActivityManager private lateinit var context: Context private lateinit var logger: ILogger + private val buildInfoProvider = mock() @BeforeTest fun `set up`() { @@ -46,6 +47,7 @@ class ContextUtilsTest { ShadowBuild.reset() val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager? shadowActivityManager = Shadow.extract(activityManager) + whenever(buildInfoProvider.sdkInfoVersion).thenReturn(Build.VERSION_CODES.TIRAMISU) } @Test @@ -197,7 +199,7 @@ class ContextUtilsTest { @Test fun `returns true when app started with foreground importance`() { - assertTrue(ContextUtils.isForegroundImportance()) + assertTrue(ContextUtils.isForegroundImportance(context, buildInfoProvider)) } @Test @@ -211,6 +213,6 @@ class ContextUtilsTest { } ) ) - assertFalse(ContextUtils.isForegroundImportance()) + assertFalse(ContextUtils.isForegroundImportance(context, buildInfoProvider)) } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/PerformanceAndroidEventProcessorTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/PerformanceAndroidEventProcessorTest.kt index 4283326677..ef99613f9c 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/PerformanceAndroidEventProcessorTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/PerformanceAndroidEventProcessorTest.kt @@ -46,6 +46,7 @@ class PerformanceAndroidEventProcessorTest { tracesSampleRate: Double? = 1.0, enablePerformanceV2: Boolean = false ): PerformanceAndroidEventProcessor { + AppStartMetrics.getInstance().isAppLaunchedInForeground = true options.tracesSampleRate = tracesSampleRate options.isEnablePerformanceV2 = enablePerformanceV2 whenever(hub.options).thenReturn(options) diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt index 990c3f4b13..966a29a24e 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt @@ -339,7 +339,7 @@ class SentryAndroidTest { val context = ContextUtilsTestHelper.createMockContext() Mockito.mockStatic(ContextUtils::class.java).use { mockedContextUtils -> - mockedContextUtils.`when` { ContextUtils.isForegroundImportance() } + mockedContextUtils.`when` { ContextUtils.isForegroundImportance(any(), any()) } .thenReturn(inForeground) SentryAndroid.init(context) { options -> options.release = "prod" diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/performance/AppStartMetricsTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/performance/AppStartMetricsTest.kt index 0885024b00..64a51bf457 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/performance/AppStartMetricsTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/performance/AppStartMetricsTest.kt @@ -10,8 +10,10 @@ import org.junit.Before import org.junit.runner.RunWith import org.mockito.kotlin.mock import org.robolectric.annotation.Config +import java.util.concurrent.TimeUnit import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFalse import kotlin.test.assertNotEquals import kotlin.test.assertNull import kotlin.test.assertSame @@ -28,6 +30,7 @@ class AppStartMetricsTest { fun setup() { AppStartMetrics.getInstance().clear() SentryShadowProcess.setStartUptimeMillis(42) + AppStartMetrics.getInstance().isAppLaunchedInForeground = true } @Test @@ -106,4 +109,69 @@ class AppStartMetricsTest { fun `class load time is set`() { assertNotEquals(0, AppStartMetrics.getInstance().classLoadedUptimeMs) } + + @Test + fun `if app is launched in background, appStartTimeSpanWithFallback returns an empty span`() { + AppStartMetrics.getInstance().isAppLaunchedInForeground = false + val appStartTimeSpan = AppStartMetrics.getInstance().appStartTimeSpan + appStartTimeSpan.start() + assertTrue(appStartTimeSpan.hasStarted()) + + val options = SentryAndroidOptions().apply { + isEnablePerformanceV2 = false + } + + val timeSpan = AppStartMetrics.getInstance().getAppStartTimeSpanWithFallback(options) + assertFalse(timeSpan.hasStarted()) + } + + @Test + fun `if app is launched in background with perfV2, appStartTimeSpanWithFallback returns an empty span`() { + AppStartMetrics.getInstance().isAppLaunchedInForeground = false + val appStartTimeSpan = AppStartMetrics.getInstance().appStartTimeSpan + appStartTimeSpan.start() + assertTrue(appStartTimeSpan.hasStarted()) + + val options = SentryAndroidOptions().apply { + isEnablePerformanceV2 = true + } + + val timeSpan = AppStartMetrics.getInstance().getAppStartTimeSpanWithFallback(options) + assertFalse(timeSpan.hasStarted()) + } + + @Test + fun `if app start span is at most 1 minute, appStartTimeSpanWithFallback returns the app start span`() { + val appStartTimeSpan = AppStartMetrics.getInstance().appStartTimeSpan + appStartTimeSpan.start() + appStartTimeSpan.stop() + appStartTimeSpan.setStartedAt(1) + appStartTimeSpan.setStoppedAt(TimeUnit.MINUTES.toMillis(1) + 1) + assertTrue(appStartTimeSpan.hasStarted()) + + val options = SentryAndroidOptions().apply { + isEnablePerformanceV2 = true + } + + val timeSpan = AppStartMetrics.getInstance().getAppStartTimeSpanWithFallback(options) + assertTrue(timeSpan.hasStarted()) + assertSame(appStartTimeSpan, timeSpan) + } + + @Test + fun `if app start span is longer than 1 minute, appStartTimeSpanWithFallback returns an empty span`() { + val appStartTimeSpan = AppStartMetrics.getInstance().appStartTimeSpan + appStartTimeSpan.start() + appStartTimeSpan.stop() + appStartTimeSpan.setStartedAt(1) + appStartTimeSpan.setStoppedAt(TimeUnit.MINUTES.toMillis(1) + 2) + assertTrue(appStartTimeSpan.hasStarted()) + + val options = SentryAndroidOptions().apply { + isEnablePerformanceV2 = true + } + + val timeSpan = AppStartMetrics.getInstance().getAppStartTimeSpanWithFallback(options) + assertFalse(timeSpan.hasStarted()) + } } From a12a15cb7a67490552906193513eaeaf915e7016 Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Thu, 4 Jul 2024 17:44:48 +0200 Subject: [PATCH 2/4] updated changelog added tests --- CHANGELOG.md | 6 + .../core/ActivityLifecycleIntegrationTest.kt | 48 ++++- .../PerformanceAndroidEventProcessorTest.kt | 202 +++++++++++------- 3 files changed, 178 insertions(+), 78 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f67618933f..3d51617a9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Fixes + +- Check app start spans time and foreground state ([#3550](https://github.com/getsentry/sentry-java/pull/3550)) + ## 7.11.0 ### Features diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt index 897c4628c5..5d1888f99e 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt @@ -54,6 +54,7 @@ import org.robolectric.shadow.api.Shadow import org.robolectric.shadows.ShadowActivityManager import java.util.Date import java.util.concurrent.Future +import java.util.concurrent.TimeUnit import kotlin.test.AfterTest import kotlin.test.BeforeTest import kotlin.test.Test @@ -941,6 +942,46 @@ class ActivityLifecycleIntegrationTest { assertEquals(span.startDate.nanoTimestamp(), date.nanoTimestamp()) } + @Test + fun `When firstActivityCreated is true and app started more than 1 minute ago, app start spans are dropped`() { + val sut = fixture.getSut() + fixture.options.tracesSampleRate = 1.0 + sut.register(fixture.hub, fixture.options) + + val date = SentryNanotimeDate(Date(1), 0) + val duration = TimeUnit.MINUTES.toMillis(1) + 2 + val durationNanos = TimeUnit.MILLISECONDS.toNanos(duration) + val stopDate = SentryNanotimeDate(Date(duration), durationNanos) + setAppStartTime(date, stopDate) + + val activity = mock() + sut.onActivityCreated(activity, null) + + val appStartSpan = fixture.transaction.children.firstOrNull { + it.description == "Cold Start" + } + assertNull(appStartSpan) + } + + @Test + fun `When firstActivityCreated is true and app started in background, app start spans are dropped`() { + val sut = fixture.getSut() + AppStartMetrics.getInstance().isAppLaunchedInForeground = false + fixture.options.tracesSampleRate = 1.0 + sut.register(fixture.hub, fixture.options) + + val date = SentryNanotimeDate(Date(1), 0) + setAppStartTime(date) + + val activity = mock() + sut.onActivityCreated(activity, null) + + val appStartSpan = fixture.transaction.children.firstOrNull { + it.description == "Cold Start" + } + assertNull(appStartSpan) + } + @Test fun `When firstActivityCreated is false, start transaction but not with given appStartTime`() { val sut = fixture.getSut() @@ -1413,18 +1454,19 @@ class ActivityLifecycleIntegrationTest { shadowOf(Looper.getMainLooper()).idle() } - private fun setAppStartTime(date: SentryDate = SentryNanotimeDate(Date(1), 0)) { + private fun setAppStartTime(date: SentryDate = SentryNanotimeDate(Date(1), 0), stopDate: SentryDate = SentryNanotimeDate(Date(0), 0)) { // set by SentryPerformanceProvider so forcing it here val sdkAppStartTimeSpan = AppStartMetrics.getInstance().sdkInitTimeSpan val appStartTimeSpan = AppStartMetrics.getInstance().appStartTimeSpan val millis = DateUtils.nanosToMillis(date.nanoTimestamp().toDouble()).toLong() + val stopMillis = DateUtils.nanosToMillis(stopDate.nanoTimestamp().toDouble()).toLong() sdkAppStartTimeSpan.setStartedAt(millis) sdkAppStartTimeSpan.setStartUnixTimeMs(millis) - sdkAppStartTimeSpan.setStoppedAt(0) + sdkAppStartTimeSpan.setStoppedAt(stopMillis) appStartTimeSpan.setStartedAt(millis) appStartTimeSpan.setStartUnixTimeMs(millis) - appStartTimeSpan.setStoppedAt(0) + appStartTimeSpan.setStoppedAt(stopMillis) } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/PerformanceAndroidEventProcessorTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/PerformanceAndroidEventProcessorTest.kt index ef99613f9c..38bb044f02 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/PerformanceAndroidEventProcessorTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/PerformanceAndroidEventProcessorTest.kt @@ -18,12 +18,14 @@ import io.sentry.android.core.performance.ActivityLifecycleTimeSpan import io.sentry.android.core.performance.AppStartMetrics import io.sentry.android.core.performance.AppStartMetrics.AppStartType import io.sentry.protocol.MeasurementValue +import io.sentry.protocol.SentryId import io.sentry.protocol.SentrySpan import io.sentry.protocol.SentryTransaction import org.junit.runner.RunWith import org.mockito.kotlin.any import org.mockito.kotlin.mock import org.mockito.kotlin.whenever +import java.util.concurrent.TimeUnit import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals @@ -57,6 +59,22 @@ class PerformanceAndroidEventProcessorTest { private val fixture = Fixture() + private fun createAppStartSpan(traceId: SentryId) = SentrySpan( + 0.0, + 1.0, + traceId, + SpanId(), + null, + APP_START_COLD, + "App Start", + SpanStatus.OK, + null, + emptyMap(), + emptyMap(), + null, + null + ) + @BeforeTest fun `reset instance`() { AppStartMetrics.getInstance().clear() @@ -234,21 +252,7 @@ class PerformanceAndroidEventProcessorTest { var tr = SentryTransaction(tracer) // and it contains an app.start.cold span - val appStartSpan = SentrySpan( - 0.0, - 1.0, - tr.contexts.trace!!.traceId, - SpanId(), - null, - APP_START_COLD, - "App Start", - SpanStatus.OK, - null, - emptyMap(), - emptyMap(), - null, - null - ) + val appStartSpan = createAppStartSpan(tr.contexts.trace!!.traceId) tr.spans.add(appStartSpan) // then the app start metrics should be attached @@ -286,6 +290,110 @@ class PerformanceAndroidEventProcessorTest { ) } + @Test + fun `when app launched from background, app start spans are dropped`() { + // given some app start metrics + val appStartMetrics = AppStartMetrics.getInstance() + appStartMetrics.appStartType = AppStartType.COLD + appStartMetrics.appStartTimeSpan.setStartedAt(123) + appStartMetrics.appStartTimeSpan.setStoppedAt(456) + + val contentProvider = mock() + AppStartMetrics.onContentProviderCreate(contentProvider) + AppStartMetrics.onContentProviderPostCreate(contentProvider) + + appStartMetrics.applicationOnCreateTimeSpan.apply { + setStartedAt(10) + setStoppedAt(42) + } + + val activityTimeSpan = ActivityLifecycleTimeSpan() + activityTimeSpan.onCreate.description = "MainActivity.onCreate" + activityTimeSpan.onStart.description = "MainActivity.onStart" + + activityTimeSpan.onCreate.setStartedAt(200) + activityTimeSpan.onStart.setStartedAt(220) + activityTimeSpan.onStart.setStoppedAt(240) + activityTimeSpan.onCreate.setStoppedAt(260) + appStartMetrics.addActivityLifecycleTimeSpans(activityTimeSpan) + + // when an activity transaction is created + val sut = fixture.getSut(enablePerformanceV2 = true) + val context = TransactionContext("Activity", UI_LOAD_OP) + val tracer = SentryTracer(context, fixture.hub) + var tr = SentryTransaction(tracer) + + // and it contains an app.start.cold span + val appStartSpan = createAppStartSpan(tr.contexts.trace!!.traceId) + tr.spans.add(appStartSpan) + + // but app is launched in background + AppStartMetrics.getInstance().isAppLaunchedInForeground = false + + // then the app start metrics are not attached + tr = sut.process(tr, Hint()) + + assertFalse( + tr.spans.any { + "process.load" == it.op || + "contentprovider.load" == it.op || + "application.load" == it.op || + "activity.load" == it.op + } + ) + } + + @Test + fun `when app start takes more than 1 minute, app start spans are dropped`() { + // given some app start metrics + val appStartMetrics = AppStartMetrics.getInstance() + appStartMetrics.appStartType = AppStartType.COLD + appStartMetrics.appStartTimeSpan.setStartedAt(123) + // and app start takes more than 1 minute + appStartMetrics.appStartTimeSpan.setStoppedAt(TimeUnit.MINUTES.toMillis(1) + 124) + + val contentProvider = mock() + AppStartMetrics.onContentProviderCreate(contentProvider) + AppStartMetrics.onContentProviderPostCreate(contentProvider) + + appStartMetrics.applicationOnCreateTimeSpan.apply { + setStartedAt(10) + setStoppedAt(42) + } + + val activityTimeSpan = ActivityLifecycleTimeSpan() + activityTimeSpan.onCreate.description = "MainActivity.onCreate" + activityTimeSpan.onStart.description = "MainActivity.onStart" + + activityTimeSpan.onCreate.setStartedAt(200) + activityTimeSpan.onStart.setStartedAt(220) + activityTimeSpan.onStart.setStoppedAt(240) + activityTimeSpan.onCreate.setStoppedAt(260) + appStartMetrics.addActivityLifecycleTimeSpans(activityTimeSpan) + + // when an activity transaction is created + val sut = fixture.getSut(enablePerformanceV2 = true) + val context = TransactionContext("Activity", UI_LOAD_OP) + val tracer = SentryTracer(context, fixture.hub) + var tr = SentryTransaction(tracer) + + // and it contains an app.start.cold span + val appStartSpan = createAppStartSpan(tr.contexts.trace!!.traceId) + tr.spans.add(appStartSpan) + + // then the app start metrics are not attached + tr = sut.process(tr, Hint()) + + assertFalse( + tr.spans.any { + "process.load" == it.op || + "contentprovider.load" == it.op || + "application.load" == it.op || + "activity.load" == it.op + } + ) + } + @Test fun `does not add app start metrics to app start txn when it is not a cold start`() { // given some WARM app start metrics @@ -331,21 +439,7 @@ class PerformanceAndroidEventProcessorTest { val context = TransactionContext("Activity", UI_LOAD_OP) val tracer = SentryTracer(context, fixture.hub) var tr = SentryTransaction(tracer) - val appStartSpan = SentrySpan( - 0.0, - 1.0, - tr.contexts.trace!!.traceId, - SpanId(), - null, - APP_START_COLD, - "App Start", - SpanStatus.OK, - null, - emptyMap(), - emptyMap(), - null, - null - ) + val appStartSpan = createAppStartSpan(tr.contexts.trace!!.traceId) tr.spans.add(appStartSpan) // then the app start metrics should not be attached @@ -382,21 +476,7 @@ class PerformanceAndroidEventProcessorTest { val context = TransactionContext("Activity", UI_LOAD_OP) val tracer = SentryTracer(context, fixture.hub) var tr = SentryTransaction(tracer) - val appStartSpan = SentrySpan( - 0.0, - 1.0, - tr.contexts.trace!!.traceId, - SpanId(), - null, - APP_START_COLD, - "App Start", - SpanStatus.OK, - null, - emptyMap(), - emptyMap(), - null, - null - ) + val appStartSpan = createAppStartSpan(tr.contexts.trace!!.traceId) tr.spans.add(appStartSpan) // when the processor attaches the app start spans @@ -429,21 +509,7 @@ class PerformanceAndroidEventProcessorTest { val context = TransactionContext("Activity", UI_LOAD_OP) val tracer = SentryTracer(context, fixture.hub) var tr = SentryTransaction(tracer) - val appStartSpan = SentrySpan( - 0.0, - 1.0, - tr.contexts.trace!!.traceId, - SpanId(), - null, - APP_START_COLD, - "App Start", - SpanStatus.OK, - null, - emptyMap(), - emptyMap(), - null, - null - ) + val appStartSpan = createAppStartSpan(tr.contexts.trace!!.traceId) tr.spans.add(appStartSpan) // when the processor attaches the app start spans @@ -494,21 +560,7 @@ class PerformanceAndroidEventProcessorTest { val tracer = SentryTracer(context, fixture.hub) var tr = SentryTransaction(tracer) - val appStartSpan = SentrySpan( - 0.0, - 1.0, - tr.contexts.trace!!.traceId, - SpanId(), - null, - APP_START_COLD, - "App Start", - SpanStatus.OK, - null, - emptyMap(), - emptyMap(), - null, - null - ) + val appStartSpan = createAppStartSpan(tr.contexts.trace!!.traceId) tr.spans.add(appStartSpan) // when the processor attaches the app start spans From bf5e505ed984c70d9162ead4958e3ed386348ca3 Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Fri, 5 Jul 2024 19:29:11 +0200 Subject: [PATCH 3/4] added Activity lifecycle registration to check start launch time and foreground status updated tests --- .../api/sentry-android-core.api | 12 ++- .../core/ActivityLifecycleIntegration.java | 3 +- .../io/sentry/android/core/ContextUtils.java | 76 +++--------------- .../io/sentry/android/core/SentryAndroid.java | 5 +- .../core/performance/AppStartMetrics.java | 80 +++++++++++++++---- .../core/ActivityLifecycleIntegrationTest.kt | 7 +- .../sentry/android/core/ContextUtilsTest.kt | 6 +- .../PerformanceAndroidEventProcessorTest.kt | 4 +- .../sentry/android/core/SentryAndroidTest.kt | 2 +- .../core/performance/AppStartMetricsTest.kt | 29 ++++++- 10 files changed, 126 insertions(+), 98 deletions(-) diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api index ed624a3559..b329bb2232 100644 --- a/sentry-android-core/api/sentry-android-core.api +++ b/sentry-android-core/api/sentry-android-core.api @@ -158,7 +158,7 @@ public final class io/sentry/android/core/BuildInfoProvider { } public final class io/sentry/android/core/ContextUtils { - public static fun isForegroundImportance (Landroid/content/Context;Lio/sentry/android/core/BuildInfoProvider;)Z + public static fun isForegroundImportance ()Z } public class io/sentry/android/core/CurrentActivityHolder { @@ -425,7 +425,7 @@ public class io/sentry/android/core/performance/ActivityLifecycleTimeSpan : java public final fun getOnStart ()Lio/sentry/android/core/performance/TimeSpan; } -public class io/sentry/android/core/performance/AppStartMetrics { +public class io/sentry/android/core/performance/AppStartMetrics : android/app/Application$ActivityLifecycleCallbacks { public fun ()V public fun addActivityLifecycleTimeSpans (Lio/sentry/android/core/performance/ActivityLifecycleTimeSpan;)V public fun clear ()V @@ -441,10 +441,18 @@ public class io/sentry/android/core/performance/AppStartMetrics { public static fun getInstance ()Lio/sentry/android/core/performance/AppStartMetrics; public fun getSdkInitTimeSpan ()Lio/sentry/android/core/performance/TimeSpan; public fun isAppLaunchedInForeground ()Z + public fun onActivityCreated (Landroid/app/Activity;Landroid/os/Bundle;)V + public fun onActivityDestroyed (Landroid/app/Activity;)V + public fun onActivityPaused (Landroid/app/Activity;)V + public fun onActivityResumed (Landroid/app/Activity;)V + public fun onActivitySaveInstanceState (Landroid/app/Activity;Landroid/os/Bundle;)V + public fun onActivityStarted (Landroid/app/Activity;)V + public fun onActivityStopped (Landroid/app/Activity;)V public static fun onApplicationCreate (Landroid/app/Application;)V public static fun onApplicationPostCreate (Landroid/app/Application;)V public static fun onContentProviderCreate (Landroid/content/ContentProvider;)V public static fun onContentProviderPostCreate (Landroid/content/ContentProvider;)V + public fun setAppLaunchTooLong (Z)V public fun setAppLaunchedInForeground (Z)V public fun setAppStartProfiler (Lio/sentry/ITransactionProfiler;)V public fun setAppStartSamplingDecision (Lio/sentry/TracesSamplingDecision;)V diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java index 7e804038fa..1121a6bfe7 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java @@ -167,8 +167,7 @@ private void startTracing(final @NotNull Activity activity) { // we only track app start for processes that will show an Activity (full launch). // Here we check the process importance which will tell us that. - final boolean foregroundImportance = - ContextUtils.isForegroundImportance(activity, buildInfoProvider); + final boolean foregroundImportance = ContextUtils.isForegroundImportance(); if (foregroundImportance && appStartTimeSpan.hasStarted()) { appStartTime = appStartTimeSpan.getStartTimestamp(); coldStart = diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ContextUtils.java b/sentry-android-core/src/main/java/io/sentry/android/core/ContextUtils.java index ec77f5bdcf..2e76de4d12 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ContextUtils.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ContextUtils.java @@ -1,5 +1,6 @@ package io.sentry.android.core; +import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND; import static android.content.Context.ACTIVITY_SERVICE; import static android.content.Context.RECEIVER_EXPORTED; import static android.content.pm.PackageInfo.REQUESTED_PERMISSION_GRANTED; @@ -14,7 +15,6 @@ import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.os.Build; -import android.os.PowerManager; import android.provider.Settings; import android.util.DisplayMetrics; import io.sentry.ILogger; @@ -26,7 +26,6 @@ import java.io.FileReader; import java.io.IOException; import java.util.HashMap; -import java.util.List; import java.util.Map; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -162,77 +161,22 @@ static String getVersionName(final @NotNull PackageInfo packageInfo) { return Integer.toString(packageInfo.versionCode); } - /* - * https://github.com/firebase/firebase-android-sdk/blob/58540de24c9b1eb7780c9f642c2cf17478e65734/firebase-perf/src/main/java/com/google/firebase/perf/metrics/AppStartTrace.java#L497 - * - * Copyright 2022 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ /** * Check if the Started process has IMPORTANCE_FOREGROUND importance which means that the process * will start an Activity. * * @return true if IMPORTANCE_FOREGROUND and false otherwise */ - @SuppressLint("NewApi") - @SuppressWarnings("deprecation") - public static boolean isForegroundImportance( - final @NotNull Context appContext, final @NotNull BuildInfoProvider buildInfoProvider) { - - // Do not call ProcessStats.getActivityManger, caching will break tests that indirectly depend - // on ProcessStats. - ActivityManager activityManager = - (ActivityManager) appContext.getSystemService(Context.ACTIVITY_SERVICE); - if (activityManager == null) { - return true; - } - List appProcesses = - activityManager.getRunningAppProcesses(); - if (appProcesses != null) { - String appProcessName = appContext.getPackageName(); - String allowedAppProcessNamePrefix = appProcessName + ":"; - for (ActivityManager.RunningAppProcessInfo appProcess : appProcesses) { - if (appProcess.importance != ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) { - continue; - } - if (appProcess.processName.equals(appProcessName) - || appProcess.processName.startsWith(allowedAppProcessNamePrefix)) { - boolean isAppInForeground = true; - - // For the case when the app is in foreground and the device transitions to sleep mode, - // the importance of the process is set to IMPORTANCE_TOP_SLEEPING. However, this - // importance level was introduced in M. Pre M, the process importance is not changed to - // IMPORTANCE_TOP_SLEEPING when the display turns off. So we need to rely also on the - // state of the display to decide if any app process is really visible. - if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.M) { - PowerManager powerManager = - (PowerManager) appContext.getSystemService(Context.POWER_SERVICE); - if (powerManager != null) { - isAppInForeground = - buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.KITKAT_WATCH - ? powerManager.isInteractive() - : powerManager.isScreenOn(); - } - } - - if (isAppInForeground) { - return true; - } - } - } + @ApiStatus.Internal + public static boolean isForegroundImportance() { + try { + final ActivityManager.RunningAppProcessInfo appProcessInfo = + new ActivityManager.RunningAppProcessInfo(); + ActivityManager.getMyMemoryState(appProcessInfo); + return appProcessInfo.importance == IMPORTANCE_FOREGROUND; + } catch (Throwable ignored) { + // should never happen } - return false; } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java index 1fee9efae0..46590826ef 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java @@ -87,7 +87,6 @@ public static synchronized void init( @NotNull Sentry.OptionsConfiguration configuration) { try { - final BuildInfoProvider buildInfoProvider = new BuildInfoProvider(logger); Sentry.init( OptionsContainer.create(SentryAndroidOptions.class), options -> { @@ -104,6 +103,7 @@ public static synchronized void init( (isTimberUpstreamAvailable && classLoader.isClassAvailable(SENTRY_TIMBER_INTEGRATION_CLASS_NAME, options)); + final BuildInfoProvider buildInfoProvider = new BuildInfoProvider(logger); final LoadClass loadClass = new LoadClass(); final ActivityFramesTracker activityFramesTracker = new ActivityFramesTracker(loadClass, options); @@ -148,8 +148,7 @@ public static synchronized void init( true); final @NotNull IHub hub = Sentry.getCurrentHub(); - if (hub.getOptions().isEnableAutoSessionTracking() - && ContextUtils.isForegroundImportance(context, buildInfoProvider)) { + if (hub.getOptions().isEnableAutoSessionTracking() && ContextUtils.isForegroundImportance()) { // The LifecycleWatcher of AppLifecycleIntegration may already started a session // so only start a session if it's not already started // This e.g. happens on React Native, or e.g. on deferred SDK init diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java b/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java index fc5a5f81e8..87e769d6bb 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java @@ -1,14 +1,19 @@ package io.sentry.android.core.performance; +import android.app.Activity; import android.app.Application; import android.content.ContentProvider; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; import android.os.SystemClock; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import io.sentry.ITransactionProfiler; +import io.sentry.SentryDate; +import io.sentry.SentryNanotimeDate; import io.sentry.TracesSamplingDecision; -import io.sentry.android.core.AndroidLogger; -import io.sentry.android.core.BuildInfoProvider; import io.sentry.android.core.ContextUtils; import io.sentry.android.core.SentryAndroidOptions; import java.util.ArrayList; @@ -27,7 +32,7 @@ * transformed into SDK specific txn/span data structures. */ @ApiStatus.Internal -public class AppStartMetrics { +public class AppStartMetrics implements Application.ActivityLifecycleCallbacks { public enum AppStartType { UNKNOWN, @@ -49,6 +54,8 @@ public enum AppStartType { private final @NotNull List activityLifecycles; private @Nullable ITransactionProfiler appStartProfiler = null; private @Nullable TracesSamplingDecision appStartSamplingDecision = null; + private @Nullable SentryDate onCreateTime = null; + private boolean appLaunchTooLong = false; public static @NotNull AppStartMetrics getInstance() { @@ -107,7 +114,7 @@ public boolean isAppLaunchedInForeground() { } @VisibleForTesting - public void setAppLaunchedInForeground(boolean appLaunchedInForeground) { + public void setAppLaunchedInForeground(final boolean appLaunchedInForeground) { this.appLaunchedInForeground = appLaunchedInForeground; } @@ -155,15 +162,8 @@ public long getClassLoadedUptimeMs() { } private @NotNull TimeSpan validateAppStartSpan(final @NotNull TimeSpan appStartSpan) { - long spanStartMillis = appStartSpan.getStartTimestampMs(); - long spanEndMillis = - appStartSpan.hasStopped() - ? appStartSpan.getProjectedStopTimestampMs() - : SystemClock.uptimeMillis(); - long durationMillis = spanEndMillis - spanStartMillis; - // If the app was launched more than 1 minute ago or it was launched in the background we return - // an empty span, as the app start will be wrong - if (durationMillis > TimeUnit.MINUTES.toMillis(1) || !isAppLaunchedInForeground()) { + // If the app launch took too long or it was launched in the background we return an empty span + if (appLaunchTooLong || !appLaunchedInForeground) { return new TimeSpan(); } return appStartSpan; @@ -182,6 +182,9 @@ public void clear() { } appStartProfiler = null; appStartSamplingDecision = null; + appLaunchTooLong = false; + appLaunchedInForeground = false; + onCreateTime = null; } public @Nullable ITransactionProfiler getAppStartProfiler() { @@ -219,12 +222,57 @@ public static void onApplicationCreate(final @NotNull Application application) { final @NotNull AppStartMetrics instance = getInstance(); if (instance.applicationOnCreate.hasNotStarted()) { instance.applicationOnCreate.setStartedAt(now); - instance.appLaunchedInForeground = - ContextUtils.isForegroundImportance( - application, new BuildInfoProvider(new AndroidLogger())); + application.registerActivityLifecycleCallbacks(instance); + instance.appLaunchedInForeground = ContextUtils.isForegroundImportance(); + new Handler(Looper.getMainLooper()) + .post( + () -> { + // if no activity has ever been created, app was launched in background + if (instance.onCreateTime == null) { + instance.appLaunchedInForeground = false; + } + }); } } + @Override + public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) { + // An activity already called onCreate() + if (!appLaunchedInForeground || onCreateTime != null) { + return; + } + onCreateTime = new SentryNanotimeDate(); + + final long spanStartMillis = appStartSpan.getStartTimestampMs(); + final long spanEndMillis = + appStartSpan.hasStopped() + ? appStartSpan.getProjectedStopTimestampMs() + : System.currentTimeMillis(); + final long durationMillis = spanEndMillis - spanStartMillis; + // If the app was launched more than 1 minute ago, it's likely wrong + if (durationMillis > TimeUnit.MINUTES.toMillis(1)) { + appLaunchTooLong = true; + } + } + + @Override + public void onActivityStarted(@NonNull Activity activity) {} + + @Override + public void onActivityResumed(@NonNull Activity activity) {} + + @Override + public void onActivityPaused(@NonNull Activity activity) {} + + @Override + public void onActivityStopped(@NonNull Activity activity) {} + + @Override + public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) {} + + @Override + public void onActivityDestroyed(@NonNull Activity activity) {} + /** * Called by instrumentation * diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt index 5d1888f99e..ad97965788 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt @@ -1454,12 +1454,12 @@ class ActivityLifecycleIntegrationTest { shadowOf(Looper.getMainLooper()).idle() } - private fun setAppStartTime(date: SentryDate = SentryNanotimeDate(Date(1), 0), stopDate: SentryDate = SentryNanotimeDate(Date(0), 0)) { + private fun setAppStartTime(date: SentryDate = SentryNanotimeDate(Date(1), 0), stopDate: SentryDate? = null) { // set by SentryPerformanceProvider so forcing it here val sdkAppStartTimeSpan = AppStartMetrics.getInstance().sdkInitTimeSpan val appStartTimeSpan = AppStartMetrics.getInstance().appStartTimeSpan val millis = DateUtils.nanosToMillis(date.nanoTimestamp().toDouble()).toLong() - val stopMillis = DateUtils.nanosToMillis(stopDate.nanoTimestamp().toDouble()).toLong() + val stopMillis = DateUtils.nanosToMillis(stopDate?.nanoTimestamp()?.toDouble() ?: 0.0).toLong() sdkAppStartTimeSpan.setStartedAt(millis) sdkAppStartTimeSpan.setStartUnixTimeMs(millis) @@ -1468,5 +1468,8 @@ class ActivityLifecycleIntegrationTest { appStartTimeSpan.setStartedAt(millis) appStartTimeSpan.setStartUnixTimeMs(millis) appStartTimeSpan.setStoppedAt(stopMillis) + if (stopDate != null) { + AppStartMetrics.getInstance().onActivityCreated(mock(), mock()) + } } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ContextUtilsTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ContextUtilsTest.kt index 8c7e295c0e..b758fae1f8 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ContextUtilsTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ContextUtilsTest.kt @@ -38,7 +38,6 @@ class ContextUtilsTest { private lateinit var shadowActivityManager: ShadowActivityManager private lateinit var context: Context private lateinit var logger: ILogger - private val buildInfoProvider = mock() @BeforeTest fun `set up`() { @@ -47,7 +46,6 @@ class ContextUtilsTest { ShadowBuild.reset() val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager? shadowActivityManager = Shadow.extract(activityManager) - whenever(buildInfoProvider.sdkInfoVersion).thenReturn(Build.VERSION_CODES.TIRAMISU) } @Test @@ -199,7 +197,7 @@ class ContextUtilsTest { @Test fun `returns true when app started with foreground importance`() { - assertTrue(ContextUtils.isForegroundImportance(context, buildInfoProvider)) + assertTrue(ContextUtils.isForegroundImportance()) } @Test @@ -213,6 +211,6 @@ class ContextUtilsTest { } ) ) - assertFalse(ContextUtils.isForegroundImportance(context, buildInfoProvider)) + assertFalse(ContextUtils.isForegroundImportance()) } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/PerformanceAndroidEventProcessorTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/PerformanceAndroidEventProcessorTest.kt index 38bb044f02..23ab5a3bc8 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/PerformanceAndroidEventProcessorTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/PerformanceAndroidEventProcessorTest.kt @@ -73,7 +73,9 @@ class PerformanceAndroidEventProcessorTest { emptyMap(), null, null - ) + ).also { + AppStartMetrics.getInstance().onActivityCreated(mock(), mock()) + } @BeforeTest fun `reset instance`() { diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt index 966a29a24e..990c3f4b13 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt @@ -339,7 +339,7 @@ class SentryAndroidTest { val context = ContextUtilsTestHelper.createMockContext() Mockito.mockStatic(ContextUtils::class.java).use { mockedContextUtils -> - mockedContextUtils.`when` { ContextUtils.isForegroundImportance(any(), any()) } + mockedContextUtils.`when` { ContextUtils.isForegroundImportance() } .thenReturn(inForeground) SentryAndroid.init(context) { options -> options.release = "prod" diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/performance/AppStartMetricsTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/performance/AppStartMetricsTest.kt index 64a51bf457..c9dd07e863 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/performance/AppStartMetricsTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/performance/AppStartMetricsTest.kt @@ -3,12 +3,14 @@ package io.sentry.android.core.performance import android.app.Application import android.content.ContentProvider import android.os.Build +import android.os.Looper import androidx.test.ext.junit.runners.AndroidJUnit4 import io.sentry.android.core.SentryAndroidOptions import io.sentry.android.core.SentryShadowProcess import org.junit.Before import org.junit.runner.RunWith import org.mockito.kotlin.mock +import org.robolectric.Shadows import org.robolectric.annotation.Config import java.util.concurrent.TimeUnit import kotlin.test.Test @@ -31,6 +33,7 @@ class AppStartMetricsTest { AppStartMetrics.getInstance().clear() SentryShadowProcess.setStartUptimeMillis(42) AppStartMetrics.getInstance().isAppLaunchedInForeground = true + AppStartMetrics.onApplicationCreate(mock()) } @Test @@ -116,6 +119,8 @@ class AppStartMetricsTest { val appStartTimeSpan = AppStartMetrics.getInstance().appStartTimeSpan appStartTimeSpan.start() assertTrue(appStartTimeSpan.hasStarted()) + AppStartMetrics.getInstance().onActivityCreated(mock(), mock()) + Shadows.shadowOf(Looper.getMainLooper()).idle() val options = SentryAndroidOptions().apply { isEnablePerformanceV2 = false @@ -127,10 +132,11 @@ class AppStartMetricsTest { @Test fun `if app is launched in background with perfV2, appStartTimeSpanWithFallback returns an empty span`() { - AppStartMetrics.getInstance().isAppLaunchedInForeground = false val appStartTimeSpan = AppStartMetrics.getInstance().appStartTimeSpan appStartTimeSpan.start() assertTrue(appStartTimeSpan.hasStarted()) + AppStartMetrics.getInstance().isAppLaunchedInForeground = false + AppStartMetrics.getInstance().onActivityCreated(mock(), mock()) val options = SentryAndroidOptions().apply { isEnablePerformanceV2 = true @@ -148,6 +154,7 @@ class AppStartMetricsTest { appStartTimeSpan.setStartedAt(1) appStartTimeSpan.setStoppedAt(TimeUnit.MINUTES.toMillis(1) + 1) assertTrue(appStartTimeSpan.hasStarted()) + AppStartMetrics.getInstance().onActivityCreated(mock(), mock()) val options = SentryAndroidOptions().apply { isEnablePerformanceV2 = true @@ -158,6 +165,25 @@ class AppStartMetricsTest { assertSame(appStartTimeSpan, timeSpan) } + @Test + fun `if activity is never started, returns an empty span`() { + val appStartTimeSpan = AppStartMetrics.getInstance().appStartTimeSpan + appStartTimeSpan.start() + appStartTimeSpan.stop() + appStartTimeSpan.setStartedAt(1) + appStartTimeSpan.setStoppedAt(TimeUnit.MINUTES.toMillis(1) + 1) + assertTrue(appStartTimeSpan.hasStarted()) + // Job on main thread checks if activity was launched + Shadows.shadowOf(Looper.getMainLooper()).idle() + + val options = SentryAndroidOptions().apply { + isEnablePerformanceV2 = true + } + + val timeSpan = AppStartMetrics.getInstance().getAppStartTimeSpanWithFallback(options) + assertFalse(timeSpan.hasStarted()) + } + @Test fun `if app start span is longer than 1 minute, appStartTimeSpanWithFallback returns an empty span`() { val appStartTimeSpan = AppStartMetrics.getInstance().appStartTimeSpan @@ -166,6 +192,7 @@ class AppStartMetricsTest { appStartTimeSpan.setStartedAt(1) appStartTimeSpan.setStoppedAt(TimeUnit.MINUTES.toMillis(1) + 2) assertTrue(appStartTimeSpan.hasStarted()) + AppStartMetrics.getInstance().onActivityCreated(mock(), mock()) val options = SentryAndroidOptions().apply { isEnablePerformanceV2 = true From c2b6daa703e250f9fbeeba8f481f2b2dbb1fdde1 Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Mon, 8 Jul 2024 13:31:45 +0200 Subject: [PATCH 4/4] updated api file --- sentry-android-core/api/sentry-android-core.api | 1 - 1 file changed, 1 deletion(-) diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api index b329bb2232..933a11d579 100644 --- a/sentry-android-core/api/sentry-android-core.api +++ b/sentry-android-core/api/sentry-android-core.api @@ -452,7 +452,6 @@ public class io/sentry/android/core/performance/AppStartMetrics : android/app/Ap public static fun onApplicationPostCreate (Landroid/app/Application;)V public static fun onContentProviderCreate (Landroid/content/ContentProvider;)V public static fun onContentProviderPostCreate (Landroid/content/ContentProvider;)V - public fun setAppLaunchTooLong (Z)V public fun setAppLaunchedInForeground (Z)V public fun setAppStartProfiler (Lio/sentry/ITransactionProfiler;)V public fun setAppStartSamplingDecision (Lio/sentry/TracesSamplingDecision;)V