Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
App start now takes AppStartMetrics.appLaunchedInForeground variable …
…to add spans to the transaction

App starts longer than 1 minute are dropped (same as Firebase)
  • Loading branch information
stefanosiano committed Jul 4, 2024
commit fa155442cf974e9d28b5cd0c1e68fcb8845e783b
3 changes: 2 additions & 1 deletion sentry-android-core/api/sentry-android-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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<ActivityManager.RunningAppProcessInfo> 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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ public static synchronized void init(
@NotNull Sentry.OptionsConfiguration<SentryAndroidOptions> configuration) {

try {
final BuildInfoProvider buildInfoProvider = new BuildInfoProvider(logger);
Sentry.init(
OptionsContainer.create(SentryAndroidOptions.class),
options -> {
Expand All @@ -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);
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,19 @@
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;
import java.util.Collections;
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;
Expand Down Expand Up @@ -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
*
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<TransactionOptions>()
val contextCaptor = argumentCaptor<TransactionContext>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<BuildInfoProvider>()

@BeforeTest
fun `set up`() {
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -211,6 +213,6 @@ class ContextUtilsTest {
}
)
)
assertFalse(ContextUtils.isForegroundImportance())
assertFalse(ContextUtils.isForegroundImportance(context, buildInfoProvider))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,7 @@ class SentryAndroidTest {
val context = ContextUtilsTestHelper.createMockContext()

Mockito.mockStatic(ContextUtils::class.java).use { mockedContextUtils ->
mockedContextUtils.`when`<Any> { ContextUtils.isForegroundImportance() }
mockedContextUtils.`when`<Any> { ContextUtils.isForegroundImportance(any(), any()) }
.thenReturn(inForeground)
SentryAndroid.init(context) { options ->
options.release = "prod"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -28,6 +30,7 @@ class AppStartMetricsTest {
fun setup() {
AppStartMetrics.getInstance().clear()
SentryShadowProcess.setStartUptimeMillis(42)
AppStartMetrics.getInstance().isAppLaunchedInForeground = true
}

@Test
Expand Down Expand Up @@ -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())
}
}