Skip to content

Commit

Permalink
Merge 082edcb into 25f1ca4
Browse files Browse the repository at this point in the history
  • Loading branch information
stefanosiano authored Jul 12, 2024
2 parents 25f1ca4 + 082edcb commit bcf684e
Show file tree
Hide file tree
Showing 10 changed files with 418 additions and 84 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
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 @@ -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 : 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 @@ -441,10 +441,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
@@ -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 @@ -135,6 +136,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,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 All @@ -65,6 +76,7 @@ public AppStartMetrics() {
applicationOnCreate = new TimeSpan();
contentProviderOnCreates = new HashMap<>();
activityLifecycles = new ArrayList<>();
appLaunchedInForeground = ContextUtils.isForegroundImportance();
}

/**
Expand Down Expand Up @@ -102,6 +114,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 +154,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 +183,9 @@ public void clear() {
}
appStartProfiler = null;
appStartSamplingDecision = null;
appLaunchTooLong = false;
appLaunchedInForeground = false;
onCreateTime = null;
}

public @Nullable ITransactionProfiler getAppStartProfiler() {
Expand Down Expand Up @@ -195,7 +223,47 @@ 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 =
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) {
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);
});
}

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

0 comments on commit bcf684e

Please sign in to comment.