Skip to content

Commit

Permalink
Merge a12a15c into 25f1ca4
Browse files Browse the repository at this point in the history
  • Loading branch information
stefanosiano authored Jul 4, 2024
2 parents 25f1ca4 + a12a15c commit 715ec3c
Show file tree
Hide file tree
Showing 11 changed files with 355 additions and 98 deletions.
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))

## 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();
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 @@ -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

0 comments on commit 715ec3c

Please sign in to comment.