Skip to content

Commit 09db9ad

Browse files
authored
Merge 9b09e73 into 25f1ca4
2 parents 25f1ca4 + 9b09e73 commit 09db9ad

File tree

8 files changed

+359
-84
lines changed

8 files changed

+359
-84
lines changed

CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
### Fixes
6+
7+
- Check app start spans time and foreground state ([#3550](https://github.com/getsentry/sentry-java/pull/3550))
8+
39
## 7.11.0
410

511
### Features

sentry-android-core/api/sentry-android-core.api

+4-1
Original file line numberDiff line numberDiff line change
@@ -425,7 +425,7 @@ public class io/sentry/android/core/performance/ActivityLifecycleTimeSpan : java
425425
public final fun getOnStart ()Lio/sentry/android/core/performance/TimeSpan;
426426
}
427427

428-
public class io/sentry/android/core/performance/AppStartMetrics {
428+
public class io/sentry/android/core/performance/AppStartMetrics : io/sentry/android/core/performance/ActivityLifecycleCallbacksAdapter {
429429
public fun <init> ()V
430430
public fun addActivityLifecycleTimeSpans (Lio/sentry/android/core/performance/ActivityLifecycleTimeSpan;)V
431431
public fun clear ()V
@@ -441,10 +441,13 @@ public class io/sentry/android/core/performance/AppStartMetrics {
441441
public static fun getInstance ()Lio/sentry/android/core/performance/AppStartMetrics;
442442
public fun getSdkInitTimeSpan ()Lio/sentry/android/core/performance/TimeSpan;
443443
public fun isAppLaunchedInForeground ()Z
444+
public fun onActivityCreated (Landroid/app/Activity;Landroid/os/Bundle;)V
444445
public static fun onApplicationCreate (Landroid/app/Application;)V
445446
public static fun onApplicationPostCreate (Landroid/app/Application;)V
446447
public static fun onContentProviderCreate (Landroid/content/ContentProvider;)V
447448
public static fun onContentProviderPostCreate (Landroid/content/ContentProvider;)V
449+
public fun registerApplicationForegroundCheck (Landroid/app/Application;)V
450+
public fun setAppLaunchedInForeground (Z)V
448451
public fun setAppStartProfiler (Lio/sentry/ITransactionProfiler;)V
449452
public fun setAppStartSamplingDecision (Lio/sentry/TracesSamplingDecision;)V
450453
public fun setAppStartType (Lio/sentry/android/core/performance/AppStartMetrics$AppStartType;)V

sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java

+1
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@ private void onAppLaunched(
201201

202202
final @NotNull TimeSpan appStartTimespan = appStartMetrics.getAppStartTimeSpan();
203203
appStartTimespan.setStartedAt(Process.getStartUptimeMillis());
204+
appStartMetrics.registerApplicationForegroundCheck(app);
204205

205206
final AtomicBoolean firstDrawDone = new AtomicBoolean(false);
206207

sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java

+71-4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
11
package io.sentry.android.core.performance;
22

3+
import android.app.Activity;
34
import android.app.Application;
45
import android.content.ContentProvider;
6+
import android.os.Bundle;
7+
import android.os.Handler;
8+
import android.os.Looper;
59
import android.os.SystemClock;
10+
import androidx.annotation.NonNull;
611
import androidx.annotation.Nullable;
12+
import androidx.annotation.VisibleForTesting;
713
import io.sentry.ITransactionProfiler;
14+
import io.sentry.SentryDate;
15+
import io.sentry.SentryNanotimeDate;
816
import io.sentry.TracesSamplingDecision;
917
import io.sentry.android.core.ContextUtils;
1018
import io.sentry.android.core.SentryAndroidOptions;
@@ -13,6 +21,7 @@
1321
import java.util.HashMap;
1422
import java.util.List;
1523
import java.util.Map;
24+
import java.util.concurrent.TimeUnit;
1625
import org.jetbrains.annotations.ApiStatus;
1726
import org.jetbrains.annotations.NotNull;
1827
import org.jetbrains.annotations.TestOnly;
@@ -23,7 +32,7 @@
2332
* transformed into SDK specific txn/span data structures.
2433
*/
2534
@ApiStatus.Internal
26-
public class AppStartMetrics {
35+
public class AppStartMetrics extends ActivityLifecycleCallbacksAdapter {
2736

2837
public enum AppStartType {
2938
UNKNOWN,
@@ -45,6 +54,8 @@ public enum AppStartType {
4554
private final @NotNull List<ActivityLifecycleTimeSpan> activityLifecycles;
4655
private @Nullable ITransactionProfiler appStartProfiler = null;
4756
private @Nullable TracesSamplingDecision appStartSamplingDecision = null;
57+
private @Nullable SentryDate onCreateTime = null;
58+
private boolean appLaunchTooLong = false;
4859

4960
public static @NotNull AppStartMetrics getInstance() {
5061

@@ -65,6 +76,7 @@ public AppStartMetrics() {
6576
applicationOnCreate = new TimeSpan();
6677
contentProviderOnCreates = new HashMap<>();
6778
activityLifecycles = new ArrayList<>();
79+
appLaunchedInForeground = ContextUtils.isForegroundImportance();
6880
}
6981

7082
/**
@@ -102,6 +114,11 @@ public boolean isAppLaunchedInForeground() {
102114
return appLaunchedInForeground;
103115
}
104116

117+
@VisibleForTesting
118+
public void setAppLaunchedInForeground(final boolean appLaunchedInForeground) {
119+
this.appLaunchedInForeground = appLaunchedInForeground;
120+
}
121+
105122
/**
106123
* Provides all collected content provider onCreate time spans
107124
*
@@ -137,12 +154,20 @@ public long getClassLoadedUptimeMs() {
137154
// Only started when sdk version is >= N
138155
final @NotNull TimeSpan appStartSpan = getAppStartTimeSpan();
139156
if (appStartSpan.hasStarted()) {
140-
return appStartSpan;
157+
return validateAppStartSpan(appStartSpan);
141158
}
142159
}
143160

144161
// fallback: use sdk init time span, as it will always have a start time set
145-
return getSdkInitTimeSpan();
162+
return validateAppStartSpan(getSdkInitTimeSpan());
163+
}
164+
165+
private @NotNull TimeSpan validateAppStartSpan(final @NotNull TimeSpan appStartSpan) {
166+
// If the app launch took too long or it was launched in the background we return an empty span
167+
if (appLaunchTooLong || !appLaunchedInForeground) {
168+
return new TimeSpan();
169+
}
170+
return appStartSpan;
146171
}
147172

148173
@TestOnly
@@ -158,6 +183,9 @@ public void clear() {
158183
}
159184
appStartProfiler = null;
160185
appStartSamplingDecision = null;
186+
appLaunchTooLong = false;
187+
appLaunchedInForeground = false;
188+
onCreateTime = null;
161189
}
162190

163191
public @Nullable ITransactionProfiler getAppStartProfiler() {
@@ -195,7 +223,46 @@ public static void onApplicationCreate(final @NotNull Application application) {
195223
final @NotNull AppStartMetrics instance = getInstance();
196224
if (instance.applicationOnCreate.hasNotStarted()) {
197225
instance.applicationOnCreate.setStartedAt(now);
198-
instance.appLaunchedInForeground = ContextUtils.isForegroundImportance();
226+
instance.appLaunchedInForeground =
227+
instance.appLaunchedInForeground || ContextUtils.isForegroundImportance();
228+
instance.registerApplicationForegroundCheck(application);
229+
}
230+
}
231+
232+
/**
233+
* Register a callback to check if an activity was started after the application was created
234+
*
235+
* @param application The application object to register the callback to
236+
*/
237+
public void registerApplicationForegroundCheck(final @NotNull Application application) {
238+
application.registerActivityLifecycleCallbacks(instance);
239+
new Handler(Looper.getMainLooper())
240+
.post(
241+
() -> {
242+
// if no activity has ever been created, app was launched in background
243+
if (onCreateTime == null) {
244+
appLaunchedInForeground = false;
245+
}
246+
});
247+
}
248+
249+
@Override
250+
public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) {
251+
// An activity already called onCreate()
252+
if (!appLaunchedInForeground || onCreateTime != null) {
253+
return;
254+
}
255+
onCreateTime = new SentryNanotimeDate();
256+
257+
final long spanStartMillis = appStartSpan.getStartTimestampMs();
258+
final long spanEndMillis =
259+
appStartSpan.hasStopped()
260+
? appStartSpan.getProjectedStopTimestampMs()
261+
: System.currentTimeMillis();
262+
final long durationMillis = spanEndMillis - spanStartMillis;
263+
// If the app was launched more than 1 minute ago, it's likely wrong
264+
if (durationMillis > TimeUnit.MINUTES.toMillis(1)) {
265+
appLaunchTooLong = true;
199266
}
200267
}
201268

sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt

+49-3
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ import org.robolectric.shadow.api.Shadow
5454
import org.robolectric.shadows.ShadowActivityManager
5555
import java.util.Date
5656
import java.util.concurrent.Future
57+
import java.util.concurrent.TimeUnit
5758
import kotlin.test.AfterTest
5859
import kotlin.test.BeforeTest
5960
import kotlin.test.Test
@@ -94,6 +95,7 @@ class ActivityLifecycleIntegrationTest {
9495

9596
whenever(hub.options).thenReturn(options)
9697

98+
AppStartMetrics.getInstance().isAppLaunchedInForeground = true
9799
// We let the ActivityLifecycleIntegration create the proper transaction here
98100
val optionCaptor = argumentCaptor<TransactionOptions>()
99101
val contextCaptor = argumentCaptor<TransactionContext>()
@@ -940,6 +942,46 @@ class ActivityLifecycleIntegrationTest {
940942
assertEquals(span.startDate.nanoTimestamp(), date.nanoTimestamp())
941943
}
942944

945+
@Test
946+
fun `When firstActivityCreated is true and app started more than 1 minute ago, app start spans are dropped`() {
947+
val sut = fixture.getSut()
948+
fixture.options.tracesSampleRate = 1.0
949+
sut.register(fixture.hub, fixture.options)
950+
951+
val date = SentryNanotimeDate(Date(1), 0)
952+
val duration = TimeUnit.MINUTES.toMillis(1) + 2
953+
val durationNanos = TimeUnit.MILLISECONDS.toNanos(duration)
954+
val stopDate = SentryNanotimeDate(Date(duration), durationNanos)
955+
setAppStartTime(date, stopDate)
956+
957+
val activity = mock<Activity>()
958+
sut.onActivityCreated(activity, null)
959+
960+
val appStartSpan = fixture.transaction.children.firstOrNull {
961+
it.description == "Cold Start"
962+
}
963+
assertNull(appStartSpan)
964+
}
965+
966+
@Test
967+
fun `When firstActivityCreated is true and app started in background, app start spans are dropped`() {
968+
val sut = fixture.getSut()
969+
AppStartMetrics.getInstance().isAppLaunchedInForeground = false
970+
fixture.options.tracesSampleRate = 1.0
971+
sut.register(fixture.hub, fixture.options)
972+
973+
val date = SentryNanotimeDate(Date(1), 0)
974+
setAppStartTime(date)
975+
976+
val activity = mock<Activity>()
977+
sut.onActivityCreated(activity, null)
978+
979+
val appStartSpan = fixture.transaction.children.firstOrNull {
980+
it.description == "Cold Start"
981+
}
982+
assertNull(appStartSpan)
983+
}
984+
943985
@Test
944986
fun `When firstActivityCreated is false, start transaction but not with given appStartTime`() {
945987
val sut = fixture.getSut()
@@ -1412,18 +1454,22 @@ class ActivityLifecycleIntegrationTest {
14121454
shadowOf(Looper.getMainLooper()).idle()
14131455
}
14141456

1415-
private fun setAppStartTime(date: SentryDate = SentryNanotimeDate(Date(1), 0)) {
1457+
private fun setAppStartTime(date: SentryDate = SentryNanotimeDate(Date(1), 0), stopDate: SentryDate? = null) {
14161458
// set by SentryPerformanceProvider so forcing it here
14171459
val sdkAppStartTimeSpan = AppStartMetrics.getInstance().sdkInitTimeSpan
14181460
val appStartTimeSpan = AppStartMetrics.getInstance().appStartTimeSpan
14191461
val millis = DateUtils.nanosToMillis(date.nanoTimestamp().toDouble()).toLong()
1462+
val stopMillis = DateUtils.nanosToMillis(stopDate?.nanoTimestamp()?.toDouble() ?: 0.0).toLong()
14201463

14211464
sdkAppStartTimeSpan.setStartedAt(millis)
14221465
sdkAppStartTimeSpan.setStartUnixTimeMs(millis)
1423-
sdkAppStartTimeSpan.setStoppedAt(0)
1466+
sdkAppStartTimeSpan.setStoppedAt(stopMillis)
14241467

14251468
appStartTimeSpan.setStartedAt(millis)
14261469
appStartTimeSpan.setStartUnixTimeMs(millis)
1427-
appStartTimeSpan.setStoppedAt(0)
1470+
appStartTimeSpan.setStoppedAt(stopMillis)
1471+
if (stopDate != null) {
1472+
AppStartMetrics.getInstance().onActivityCreated(mock(), mock())
1473+
}
14281474
}
14291475
}

0 commit comments

Comments
 (0)