Skip to content

Commit e65564b

Browse files
authored
Merge bf5e505 into 25f1ca4
2 parents 25f1ca4 + bf5e505 commit e65564b

File tree

6 files changed

+367
-82
lines changed

6 files changed

+367
-82
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
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

Lines changed: 10 additions & 1 deletion
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 : android/app/Application$ActivityLifecycleCallbacks {
429429
public fun <init> ()V
430430
public fun addActivityLifecycleTimeSpans (Lio/sentry/android/core/performance/ActivityLifecycleTimeSpan;)V
431431
public fun clear ()V
@@ -441,10 +441,19 @@ 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
445+
public fun onActivityDestroyed (Landroid/app/Activity;)V
446+
public fun onActivityPaused (Landroid/app/Activity;)V
447+
public fun onActivityResumed (Landroid/app/Activity;)V
448+
public fun onActivitySaveInstanceState (Landroid/app/Activity;Landroid/os/Bundle;)V
449+
public fun onActivityStarted (Landroid/app/Activity;)V
450+
public fun onActivityStopped (Landroid/app/Activity;)V
444451
public static fun onApplicationCreate (Landroid/app/Application;)V
445452
public static fun onApplicationPostCreate (Landroid/app/Application;)V
446453
public static fun onContentProviderCreate (Landroid/content/ContentProvider;)V
447454
public static fun onContentProviderPostCreate (Landroid/content/ContentProvider;)V
455+
public fun setAppLaunchTooLong (Z)V
456+
public fun setAppLaunchedInForeground (Z)V
448457
public fun setAppStartProfiler (Lio/sentry/ITransactionProfiler;)V
449458
public fun setAppStartSamplingDecision (Lio/sentry/TracesSamplingDecision;)V
450459
public fun setAppStartType (Lio/sentry/android/core/performance/AppStartMetrics$AppStartType;)V

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

Lines changed: 77 additions & 3 deletions
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 implements Application.ActivityLifecycleCallbacks {
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

@@ -102,6 +113,11 @@ public boolean isAppLaunchedInForeground() {
102113
return appLaunchedInForeground;
103114
}
104115

116+
@VisibleForTesting
117+
public void setAppLaunchedInForeground(final boolean appLaunchedInForeground) {
118+
this.appLaunchedInForeground = appLaunchedInForeground;
119+
}
120+
105121
/**
106122
* Provides all collected content provider onCreate time spans
107123
*
@@ -137,12 +153,20 @@ public long getClassLoadedUptimeMs() {
137153
// Only started when sdk version is >= N
138154
final @NotNull TimeSpan appStartSpan = getAppStartTimeSpan();
139155
if (appStartSpan.hasStarted()) {
140-
return appStartSpan;
156+
return validateAppStartSpan(appStartSpan);
141157
}
142158
}
143159

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

148172
@TestOnly
@@ -158,6 +182,9 @@ public void clear() {
158182
}
159183
appStartProfiler = null;
160184
appStartSamplingDecision = null;
185+
appLaunchTooLong = false;
186+
appLaunchedInForeground = false;
187+
onCreateTime = null;
161188
}
162189

163190
public @Nullable ITransactionProfiler getAppStartProfiler() {
@@ -195,10 +222,57 @@ public static void onApplicationCreate(final @NotNull Application application) {
195222
final @NotNull AppStartMetrics instance = getInstance();
196223
if (instance.applicationOnCreate.hasNotStarted()) {
197224
instance.applicationOnCreate.setStartedAt(now);
225+
application.registerActivityLifecycleCallbacks(instance);
198226
instance.appLaunchedInForeground = ContextUtils.isForegroundImportance();
227+
new Handler(Looper.getMainLooper())
228+
.post(
229+
() -> {
230+
// if no activity has ever been created, app was launched in background
231+
if (instance.onCreateTime == null) {
232+
instance.appLaunchedInForeground = false;
233+
}
234+
});
235+
}
236+
}
237+
238+
@Override
239+
public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) {
240+
// An activity already called onCreate()
241+
if (!appLaunchedInForeground || onCreateTime != null) {
242+
return;
243+
}
244+
onCreateTime = new SentryNanotimeDate();
245+
246+
final long spanStartMillis = appStartSpan.getStartTimestampMs();
247+
final long spanEndMillis =
248+
appStartSpan.hasStopped()
249+
? appStartSpan.getProjectedStopTimestampMs()
250+
: System.currentTimeMillis();
251+
final long durationMillis = spanEndMillis - spanStartMillis;
252+
// If the app was launched more than 1 minute ago, it's likely wrong
253+
if (durationMillis > TimeUnit.MINUTES.toMillis(1)) {
254+
appLaunchTooLong = true;
199255
}
200256
}
201257

258+
@Override
259+
public void onActivityStarted(@NonNull Activity activity) {}
260+
261+
@Override
262+
public void onActivityResumed(@NonNull Activity activity) {}
263+
264+
@Override
265+
public void onActivityPaused(@NonNull Activity activity) {}
266+
267+
@Override
268+
public void onActivityStopped(@NonNull Activity activity) {}
269+
270+
@Override
271+
public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) {}
272+
273+
@Override
274+
public void onActivityDestroyed(@NonNull Activity activity) {}
275+
202276
/**
203277
* Called by instrumentation
204278
*

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

Lines changed: 49 additions & 3 deletions
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)