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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Fixes

- Prevent inflated cold app start when the OS spawns the process in the background (e.g. FCM push) on API 35+ ([#5841](https://github.com/getsentry/sentry-java/pull/5841))
- Avoid a CPU busy-loop when recording discarded log or metric envelopes under rate limiting ([#5835](https://github.com/getsentry/sentry-java/pull/5835))
- `ClientReportRecorder` now reads the item count from the envelope item header instead of deserializing the payload, which under sustained rate limiting could pin CPU cores while repeatedly throwing exceptions

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;
import io.sentry.IContinuousProfiler;
import io.sentry.ISentryLifecycleToken;
Expand Down Expand Up @@ -208,6 +209,35 @@ public void setAppStartType(final @NotNull AppStartType appStartType) {
}
}

/**
* Whether {@link ApplicationStartInfo#getReason()} indicates the OS spawned the app process
* because of an intentional user interaction.
*
* @return true if the user actively launched the app, false if the app was launched in
* background, and null if unknown.
*/
@RequiresApi(api = Build.VERSION_CODES.VANILLA_ICE_CREAM)
private static @Nullable Boolean isForegroundStartReason(final int reason) {
switch (reason) {
case ApplicationStartInfo.START_REASON_LAUNCHER:
case ApplicationStartInfo.START_REASON_LAUNCHER_RECENTS:
case ApplicationStartInfo.START_REASON_START_ACTIVITY:
return true;
case ApplicationStartInfo.START_REASON_ALARM:
case ApplicationStartInfo.START_REASON_BACKUP:
case ApplicationStartInfo.START_REASON_BOOT_COMPLETE:
case ApplicationStartInfo.START_REASON_BROADCAST:
case ApplicationStartInfo.START_REASON_CONTENT_PROVIDER:
case ApplicationStartInfo.START_REASON_JOB:
case ApplicationStartInfo.START_REASON_PUSH:
case ApplicationStartInfo.START_REASON_SERVICE:
return false;
case ApplicationStartInfo.START_REASON_OTHER:
default:
return null;
}
}

public boolean isAppLaunchedInForeground() {
return appLaunchedInForeground.getValue();
}
Expand Down Expand Up @@ -499,6 +529,7 @@ public void registerLifecycleCallbacks(final @NotNull Application application) {
} else {
appStartType = AppStartType.WARM;
}
appLaunchedInForeground.setValue(isForegroundStartReason(info.getReason()));
Comment thread
markushi marked this conversation as resolved.
}
}
} catch (RuntimeException ignored) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package io.sentry.android.core.performance

import android.app.Activity
import android.app.ActivityManager.RunningAppProcessInfo
import android.app.Application
import android.app.ApplicationStartInfo
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.os.SystemClock
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.sentry.android.core.SentryShadowActivityManager
Expand All @@ -16,6 +18,7 @@ import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNull
import kotlin.test.assertTrue
import org.junit.Before
import org.junit.runner.RunWith
import org.mockito.kotlin.mock
Expand Down Expand Up @@ -263,6 +266,115 @@ class AppStartMetricsTestApi35 {
assertNull(metrics.appStartReason)
}

@Test
fun `background start reason marks app as not launched in foreground`() {
val mockStartInfo = mock<ApplicationStartInfo>()
whenever(mockStartInfo.startupState).thenReturn(ApplicationStartInfo.STARTUP_STATE_STARTED)
whenever(mockStartInfo.startType).thenReturn(ApplicationStartInfo.START_TYPE_COLD)
whenever(mockStartInfo.reason).thenReturn(ApplicationStartInfo.START_REASON_PUSH)
SentryShadowActivityManager.setHistoricalProcessStartReasons(listOf(mockStartInfo))
val metrics = AppStartMetrics.getInstance()

val app = ApplicationProvider.getApplicationContext<Application>()
metrics.registerLifecycleCallbacks(app)

assertFalse(metrics.isAppLaunchedInForeground)
}

@Test
fun `all background start reasons mark app as not launched in foreground`() {
val backgroundReasons =
listOf(
ApplicationStartInfo.START_REASON_ALARM,
ApplicationStartInfo.START_REASON_BACKUP,
ApplicationStartInfo.START_REASON_BOOT_COMPLETE,
ApplicationStartInfo.START_REASON_BROADCAST,
ApplicationStartInfo.START_REASON_CONTENT_PROVIDER,
ApplicationStartInfo.START_REASON_JOB,
ApplicationStartInfo.START_REASON_PUSH,
ApplicationStartInfo.START_REASON_SERVICE,
)

val app = ApplicationProvider.getApplicationContext<Application>()
for (reason in backgroundReasons) {
AppStartMetrics.getInstance().clear()
SentryShadowActivityManager.reset()

val mockStartInfo = mock<ApplicationStartInfo>()
whenever(mockStartInfo.startupState).thenReturn(ApplicationStartInfo.STARTUP_STATE_STARTED)
whenever(mockStartInfo.startType).thenReturn(ApplicationStartInfo.START_TYPE_COLD)
whenever(mockStartInfo.reason).thenReturn(reason)
SentryShadowActivityManager.setHistoricalProcessStartReasons(listOf(mockStartInfo))

AppStartMetrics.getInstance().registerLifecycleCallbacks(app)

assertFalse(
AppStartMetrics.getInstance().isAppLaunchedInForeground,
"reason $reason should not be launched in foreground",
)
}
}

@Test
fun `user-initiated start reason keeps app launched in foreground`() {
val mockStartInfo = mock<ApplicationStartInfo>()
whenever(mockStartInfo.startupState).thenReturn(ApplicationStartInfo.STARTUP_STATE_STARTED)
whenever(mockStartInfo.startType).thenReturn(ApplicationStartInfo.START_TYPE_COLD)
whenever(mockStartInfo.reason).thenReturn(ApplicationStartInfo.START_REASON_LAUNCHER)
SentryShadowActivityManager.setHistoricalProcessStartReasons(listOf(mockStartInfo))
SentryShadowActivityManager.setImportance(RunningAppProcessInfo.IMPORTANCE_FOREGROUND)
val metrics = AppStartMetrics.getInstance()

val app = ApplicationProvider.getApplicationContext<Application>()
metrics.registerLifecycleCallbacks(app)

assertTrue(metrics.isAppLaunchedInForeground)
}

@Test
fun `unknown start reason falls back to foreground importance check`() {
val mockStartInfo = mock<ApplicationStartInfo>()
whenever(mockStartInfo.startupState).thenReturn(ApplicationStartInfo.STARTUP_STATE_STARTED)
whenever(mockStartInfo.startType).thenReturn(ApplicationStartInfo.START_TYPE_COLD)
whenever(mockStartInfo.reason).thenReturn(ApplicationStartInfo.START_REASON_OTHER)
SentryShadowActivityManager.setHistoricalProcessStartReasons(listOf(mockStartInfo))
SentryShadowActivityManager.setImportance(RunningAppProcessInfo.IMPORTANCE_FOREGROUND)
val metrics = AppStartMetrics.getInstance()

val app = ApplicationProvider.getApplicationContext<Application>()
metrics.registerLifecycleCallbacks(app)

assertTrue(metrics.isAppLaunchedInForeground)
}

@Test
fun `background-spawned start is re-classified as warm on the first activity`() {
val mockStartInfo = mock<ApplicationStartInfo>()
whenever(mockStartInfo.startupState).thenReturn(ApplicationStartInfo.STARTUP_STATE_STARTED)
whenever(mockStartInfo.startType).thenReturn(ApplicationStartInfo.START_TYPE_COLD)
whenever(mockStartInfo.reason).thenReturn(ApplicationStartInfo.START_REASON_PUSH)
SentryShadowActivityManager.setHistoricalProcessStartReasons(listOf(mockStartInfo))
val metrics = AppStartMetrics.getInstance()
// App start span anchored at background process creation.
metrics.appStartTimeSpan.setStartedAt(42)

val app = ApplicationProvider.getApplicationContext<Application>()
metrics.registerLifecycleCallbacks(app)

assertFalse(metrics.isAppLaunchedInForeground)
assertEquals(AppStartMetrics.AppStartType.COLD, metrics.appStartType)

// User opens the app 20s later (under the 1-minute warm threshold).
val activityCreatedUptimeMs = 20_000L
SystemClock.setCurrentTimeMillis(activityCreatedUptimeMs)
metrics.onActivityCreated(mock<Activity>(), null)

// Re-classified as a warm start re-anchored at activity creation.
assertEquals(AppStartMetrics.AppStartType.WARM, metrics.appStartType)
assertTrue(metrics.isAppLaunchedInForeground)
assertEquals(activityCreatedUptimeMs, metrics.appStartTimeSpan.startUptimeMs)
}

private fun waitForMainLooperIdle() {
Handler(Looper.getMainLooper()).post {}
Shadows.shadowOf(Looper.getMainLooper()).idle()
Expand Down
Loading