Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog

## Unreleased

### Fixes

- Check app start spans time and ignore background app starts ([#3550](https://github.com/getsentry/sentry-java/pull/3550))
- This should eliminate long-lasting App Start transactions

## 7.12.0

### Features
Expand Down
5 changes: 4 additions & 1 deletion sentry-android-core/api/sentry-android-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -427,7 +427,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 : io/sentry/android/core/performance/ActivityLifecycleCallbacksAdapter {
public fun <init> ()V
public fun addActivityLifecycleTimeSpans (Lio/sentry/android/core/performance/ActivityLifecycleTimeSpan;)V
public fun clear ()V
Expand All @@ -443,10 +443,13 @@ 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 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 registerApplicationForegroundCheck (Landroid/app/Application;)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 @@ -21,6 +21,7 @@
import io.sentry.NoOpTransaction;
import io.sentry.SentryDate;
import io.sentry.SentryLevel;
import io.sentry.SentryNanotimeDate;
import io.sentry.SentryOptions;
import io.sentry.SpanStatus;
import io.sentry.TracesSamplingDecision;
Expand All @@ -37,6 +38,7 @@
import java.io.Closeable;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.Date;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.Future;
Expand Down Expand Up @@ -75,7 +77,7 @@ public final class ActivityLifecycleIntegration
private @Nullable ISpan appStartSpan;
private final @NotNull WeakHashMap<Activity, ISpan> ttidSpanMap = new WeakHashMap<>();
private final @NotNull WeakHashMap<Activity, ISpan> ttfdSpanMap = new WeakHashMap<>();
private @NotNull SentryDate lastPausedTime = AndroidDateUtils.getCurrentSentryDateTime();
private @NotNull SentryDate lastPausedTime = new SentryNanotimeDate(new Date(0), 0);
private final @NotNull Handler mainHandler = new Handler(Looper.getMainLooper());
private @Nullable Future<?> ttfdAutoCloseFuture = null;

Expand Down Expand Up @@ -627,6 +629,14 @@ WeakHashMap<Activity, ISpan> getTtfdSpanMap() {
}

private void setColdStart(final @Nullable Bundle savedInstanceState) {
// The very first activity start timestamp cannot be set to the class instantiation time, as it
// may happen before an activity is started (service, broadcast receiver, etc). So we set it
// here.
if (hub != null && lastPausedTime.nanoTimestamp() == 0) {
lastPausedTime = hub.getOptions().getDateProvider().now();
} else if (lastPausedTime.nanoTimestamp() == 0) {
lastPausedTime = AndroidDateUtils.getCurrentSentryDateTime();
}
if (!firstActivityCreated) {
// if Activity has savedInstanceState then its a warm start
// https://developer.android.com/topic/performance/vitals/launch-time#warm
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.sentry.android.core;

import android.annotation.SuppressLint;
import android.app.Application;
import android.content.Context;
import android.os.Process;
import android.os.SystemClock;
Expand Down Expand Up @@ -141,6 +142,10 @@ public static synchronized void init(
appStartTimeSpan.setStartedAt(Process.getStartUptimeMillis());
}
}
if (context.getApplicationContext() instanceof Application) {
appStartMetrics.registerApplicationForegroundCheck(
(Application) context.getApplicationContext());
}
final @NotNull TimeSpan sdkInitTimeSpan = appStartMetrics.getSdkInitTimeSpan();
if (sdkInitTimeSpan.hasNotStarted()) {
sdkInitTimeSpan.setStartedAt(sdkInitMillis);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ private void onAppLaunched(

final @NotNull TimeSpan appStartTimespan = appStartMetrics.getAppStartTimeSpan();
appStartTimespan.setStartedAt(Process.getStartUptimeMillis());
appStartMetrics.registerApplicationForegroundCheck(app);

final AtomicBoolean firstDrawDone = new AtomicBoolean(false);

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 extends ActivityLifecycleCallbacksAdapter {

public enum AppStartType {
UNKNOWN,
Expand All @@ -45,6 +54,9 @@ 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;
private boolean isCallbackRegistered = false;

public static @NotNull AppStartMetrics getInstance() {

Expand All @@ -65,6 +77,7 @@ public AppStartMetrics() {
applicationOnCreate = new TimeSpan();
contentProviderOnCreates = new HashMap<>();
activityLifecycles = new ArrayList<>();
appLaunchedInForeground = ContextUtils.isForegroundImportance();
}

/**
Expand Down Expand Up @@ -102,6 +115,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 +155,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 +184,10 @@ public void clear() {
}
appStartProfiler = null;
appStartSamplingDecision = null;
appLaunchTooLong = false;
appLaunchedInForeground = false;
onCreateTime = null;
isCallbackRegistered = false;
}

public @Nullable ITransactionProfiler getAppStartProfiler() {
Expand Down Expand Up @@ -195,7 +225,55 @@ 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.registerApplicationForegroundCheck(application);
}
}

/**
* Register a callback to check if an activity was started after the application was created
*
* @param application The application object to register the callback to
*/
public void registerApplicationForegroundCheck(final @NotNull Application application) {
if (isCallbackRegistered) {
return;
}
isCallbackRegistered = true;
appLaunchedInForeground = appLaunchedInForeground || ContextUtils.isForegroundImportance();
application.registerActivityLifecycleCallbacks(instance);
new Handler(Looper.getMainLooper())
.post(
() -> {
// if no activity has ever been created, app was launched in background
if (onCreateTime == null) {
appLaunchedInForeground = false;
}
application.unregisterActivityLifecycleCallbacks(instance);
// we stop the app start profiler, as it's useless and likely to timeout
if (appStartProfiler != null && appStartProfiler.isRunning()) {
appStartProfiler.close();
appStartProfiler = null;
}
});
}

@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;
}
}

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 @@ -709,15 +711,19 @@ class ActivityLifecycleIntegrationTest {
sut.register(fixture.hub, fixture.options)

val date = SentryNanotimeDate(Date(1), 0)
val date2 = SentryNanotimeDate(Date(2), 2)
setAppStartTime(date)

val activity = mock<Activity>()
// The activity onCreate date will be ignored
fixture.options.dateProvider = SentryDateProvider { date2 }
sut.onActivityCreated(activity, fixture.bundle)

verify(fixture.hub).startTransaction(
any(),
check<TransactionOptions> {
assertEquals(date.nanoTimestamp(), it.startTimestamp!!.nanoTimestamp())
assertNotEquals(date2.nanoTimestamp(), it.startTimestamp!!.nanoTimestamp())
assertFalse(it.isAppStartTransaction)
}
)
Expand Down Expand Up @@ -756,6 +762,30 @@ class ActivityLifecycleIntegrationTest {
)
}

@Test
fun `When firstActivityCreated is true and no app start time is set, default to onActivityCreated time`() {
val sut = fixture.getSut()
fixture.options.tracesSampleRate = 1.0
sut.register(fixture.hub, fixture.options)

// usually set by SentryPerformanceProvider
val date = SentryNanotimeDate(Date(1), 0)
val date2 = SentryNanotimeDate(Date(2), 2)

val activity = mock<Activity>()
// Activity onCreate date will be used
fixture.options.dateProvider = SentryDateProvider { date2 }
sut.onActivityCreated(activity, fixture.bundle)

verify(fixture.hub).startTransaction(
any(),
check<TransactionOptions> {
assertEquals(date2.nanoTimestamp(), it.startTimestamp!!.nanoTimestamp())
assertNotEquals(date.nanoTimestamp(), it.startTimestamp!!.nanoTimestamp())
}
)
}

@Test
fun `Create and finish app start span immediately in case SDK init is deferred`() {
val sut = fixture.getSut(importance = RunningAppProcessInfo.IMPORTANCE_FOREGROUND)
Expand Down Expand Up @@ -940,6 +970,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 +1482,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