Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Check app start spans time and foreground state #3550

Merged
merged 10 commits into from
Jul 17, 2024
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## Unreleased

### Fixes

- Check app start spans time and foreground state ([#3550](https://github.com/getsentry/sentry-java/pull/3550))
stefanosiano marked this conversation as resolved.
Show resolved Hide resolved

## 7.11.0

### Features
Expand Down
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();
stefanosiano marked this conversation as resolved.
Show resolved Hide resolved
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) {
stefanosiano marked this conversation as resolved.
Show resolved Hide resolved
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 =
stefanosiano marked this conversation as resolved.
Show resolved Hide resolved
appStartSpan.hasStopped()
? appStartSpan.getProjectedStopTimestampMs()
: SystemClock.uptimeMillis();
stefanosiano marked this conversation as resolved.
Show resolved Hide resolved
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 =
stefanosiano marked this conversation as resolved.
Show resolved Hide resolved
ContextUtils.isForegroundImportance(
application, new BuildInfoProvider(new AndroidLogger()));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -94,6 +95,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 Expand Up @@ -940,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<Activity>()
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<Activity>()
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()
Expand Down Expand Up @@ -1412,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)
}
}
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))
}
}
Loading
Loading