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
10 changes: 9 additions & 1 deletion sentry-android-core/api/sentry-android-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -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 <init> ()V
public fun addActivityLifecycleTimeSpans (Lio/sentry/android/core/performance/ActivityLifecycleTimeSpan;)V
public fun clear ()V
Expand All @@ -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 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
@@ -1,10 +1,18 @@
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.ContextUtils;
import io.sentry.android.core.SentryAndroidOptions;
Expand All @@ -13,6 +21,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;
Expand All @@ -23,7 +32,7 @@
* transformed into SDK specific txn/span data structures.
*/
@ApiStatus.Internal
public class AppStartMetrics {
public class AppStartMetrics implements Application.ActivityLifecycleCallbacks {
stefanosiano marked this conversation as resolved.
Show resolved Hide resolved

public enum AppStartType {
UNKNOWN,
Expand All @@ -45,6 +54,8 @@ public enum AppStartType {
private final @NotNull List<ActivityLifecycleTimeSpan> 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() {

Expand Down Expand Up @@ -102,6 +113,11 @@ public boolean isAppLaunchedInForeground() {
return appLaunchedInForeground;
}

@VisibleForTesting
public void setAppLaunchedInForeground(final boolean appLaunchedInForeground) {
this.appLaunchedInForeground = appLaunchedInForeground;
}

/**
* Provides all collected content provider onCreate time spans
*
Expand Down Expand Up @@ -137,12 +153,20 @@ 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) {
// 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;
}

@TestOnly
Expand All @@ -158,6 +182,9 @@ public void clear() {
}
appStartProfiler = null;
appStartSamplingDecision = null;
appLaunchTooLong = false;
appLaunchedInForeground = false;
onCreateTime = null;
}

public @Nullable ITransactionProfiler getAppStartProfiler() {
Expand Down Expand Up @@ -195,10 +222,57 @@ public static void onApplicationCreate(final @NotNull Application application) {
final @NotNull AppStartMetrics instance = getInstance();
if (instance.applicationOnCreate.hasNotStarted()) {
instance.applicationOnCreate.setStartedAt(now);
application.registerActivityLifecycleCallbacks(instance);
stefanosiano marked this conversation as resolved.
Show resolved Hide resolved
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
*
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,22 @@ 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? = 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() ?: 0.0).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)
if (stopDate != null) {
AppStartMetrics.getInstance().onActivityCreated(mock(), mock())
}
}
}
Loading
Loading