From d9400d23743a1ca08ab2474d3513dcbafb9b1493 Mon Sep 17 00:00:00 2001 From: Alexander Pantiukhov Date: Mon, 27 Jul 2026 14:44:15 +0200 Subject: [PATCH 1/2] fix(android): drop inflated app start for background-spawned processes On API 35+ use the process start reason (ApplicationStartInfo#getReason()) to detect background-spawned processes (e.g. FCM push) and drop the app start, avoiding inflated app_start_cold/warm. Fixes #6382 --- CHANGELOG.md | 4 ++ .../io/sentry/react/RNSentryModuleImpl.java | 50 +++++++++++++++++++ .../io/sentry/react/RNSentryAppStartTest.java | 45 +++++++++++++++++ 3 files changed, 99 insertions(+) create mode 100644 packages/core/android/src/test/java/io/sentry/react/RNSentryAppStartTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d0f01b16a..acbf75cad9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ ### Fixes +- Drop inflated Android cold/warm app starts for background-spawned processes (e.g. FCM push) ([#6382](https://github.com/getsentry/sentry-react-native/issues/6382)) + + When Android spawns the app process in the background (for example to handle an FCM push) and the user opens the app later, the native SDK keeps the app start anchored at background process creation, inflating `app_start_cold`/`app_start_warm` by the whole idle gap. On API 35+ the SDK now uses the process start reason (`ApplicationStartInfo#getReason()`) to detect these background launches and drops the app start, similar to how the Cocoa SDK drops pre-warmed starts. + - Make `copySentryJsonConfiguration` and the `*_SentryUpload` Gradle tasks compatible with the Gradle Configuration Cache ([#6469](https://github.com/getsentry/sentry-react-native/pull/6469)) These tasks previously read `project` state at execution time — `onlyIf` predicates resolving closures from `project.extra`, plus `project.rootDir`, `project.copy`, `project.logger`, and `Project.file` inside task actions — which fails the build with `Could not evaluate onlyIf predicate` when `org.gradle.configuration-cache=true` (Gradle 9 defaults to recommending it). Environment reads are now captured at configuration time, file copies use an injected `FileSystemOperations`, and task actions use the task's own `logger`. No behaviour change. Interim step ahead of the full SAGP migration (getsentry/sentry-android-gradle-plugin#796). diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java b/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java index f2e2e09a14..b8986407dd 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java @@ -76,11 +76,14 @@ import java.net.URISyntaxException; import java.nio.charset.Charset; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Properties; +import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; @@ -108,6 +111,36 @@ public class RNSentryModuleImpl { @VisibleForTesting static long lastStartTimestampMs = -1; + // App start reasons (from ApplicationStartInfo#getReason(), API 35+) that indicate the process + // was spawned by the system in the background rather than by a user-initiated launch. When the + // app is later brought to the foreground, the native SDK keeps the app start anchored at + // background process creation, which inflates app_start_cold/warm (see + // https://github.com/getsentry/sentry-react-native/issues/6382). We drop these app starts, + // similar to how the Cocoa SDK drops pre-warmed starts. + private static final Set BACKGROUND_APP_START_REASONS = + new HashSet<>( + Arrays.asList( + "alarm", + "backup", + "boot_complete", + "broadcast", + "content_provider", + "job", + "push", + "service")); + + /** + * @param appStartReason the process start reason from {@link + * io.sentry.android.core.performance.AppStartMetrics#getAppStartReason()} (API 35+, {@code + * null} on lower API levels or for an unmapped reason) + * @return {@code true} if the process was spawned by the system in the background rather than by + * a user-initiated launch, in which case the app start data should be dropped + */ + @VisibleForTesting + static boolean isBackgroundAppStartReason(final @Nullable String appStartReason) { + return appStartReason != null && BACKGROUND_APP_START_REASONS.contains(appStartReason); + } + // 700ms to constitute frozen frames. private static final int FROZEN_FRAME_THRESHOLD = 700; // 16ms (slower than 60fps) to constitute slow frames. @@ -367,6 +400,23 @@ protected void fetchNativeAppStart( return; } + // On API 35+ the process start reason tells us whether the process was spawned in the + // background (e.g. by an FCM push, a job or a service) rather than by the user opening the app. + // In that case the native app start remains anchored at background process creation, so the + // reported cold/warm start is the full idle gap until the user finally opens the app. We drop + // such app starts to avoid inflating app_start_cold/warm. Reason is null on API < 35, where we + // fall back to the existing foreground check above. + final @Nullable String appStartReason = metrics.getAppStartReason(); + if (isBackgroundAppStartReason(appStartReason)) { + logger.log( + SentryLevel.WARNING, + "Invalid app start data: app was launched in the background (reason: " + + appStartReason + + ")."); + promise.resolve(null); + return; + } + WritableMap mutableMeasurement = (WritableMap) RNSentryMapConverter.convertToWritable(metricsDataBag); diff --git a/packages/core/android/src/test/java/io/sentry/react/RNSentryAppStartTest.java b/packages/core/android/src/test/java/io/sentry/react/RNSentryAppStartTest.java new file mode 100644 index 0000000000..a55218a349 --- /dev/null +++ b/packages/core/android/src/test/java/io/sentry/react/RNSentryAppStartTest.java @@ -0,0 +1,45 @@ +package io.sentry.react; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +public class RNSentryAppStartTest { + + @Test + public void backgroundReasonsAreTreatedAsBackgroundAppStart() { + final String[] backgroundReasons = { + "alarm", "backup", "boot_complete", "broadcast", "content_provider", "job", "push", "service" + }; + + for (final String reason : backgroundReasons) { + assertTrue( + "Expected reason '" + reason + "' to be treated as a background app start", + RNSentryModuleImpl.isBackgroundAppStartReason(reason)); + } + } + + @Test + public void userLaunchReasonsAreNotTreatedAsBackgroundAppStart() { + final String[] userLaunchReasons = {"launcher", "launcher_recents", "start_activity", "other"}; + + for (final String reason : userLaunchReasons) { + assertFalse( + "Expected reason '" + reason + "' to be treated as a user-initiated app start", + RNSentryModuleImpl.isBackgroundAppStartReason(reason)); + } + } + + @Test + public void nullReasonIsNotTreatedAsBackgroundAppStart() { + // getAppStartReason() returns null on API < 35, where we fall back to the foreground check + // only. + assertFalse(RNSentryModuleImpl.isBackgroundAppStartReason(null)); + } + + @Test + public void unknownReasonIsNotTreatedAsBackgroundAppStart() { + assertFalse(RNSentryModuleImpl.isBackgroundAppStartReason("some_future_reason")); + } +} From 3e5b6dd4507f8889c4827e6e622df9bcbaa3ba13 Mon Sep 17 00:00:00 2001 From: Alexander Pantiukhov Date: Mon, 27 Jul 2026 14:45:50 +0200 Subject: [PATCH 2/2] docs: link changelog entry to PR --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index acbf75cad9..b311da048d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ ### Fixes -- Drop inflated Android cold/warm app starts for background-spawned processes (e.g. FCM push) ([#6382](https://github.com/getsentry/sentry-react-native/issues/6382)) +- Drop inflated Android cold/warm app starts for background-spawned processes (e.g. FCM push) ([#6526](https://github.com/getsentry/sentry-react-native/pull/6526)) When Android spawns the app process in the background (for example to handle an FCM push) and the user opens the app later, the native SDK keeps the app start anchored at background process creation, inflating `app_start_cold`/`app_start_warm` by the whole idle gap. On API 35+ the SDK now uses the process start reason (`ApplicationStartInfo#getReason()`) to detect these background launches and drops the app start, similar to how the Cocoa SDK drops pre-warmed starts.