Skip to content
Closed
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@

## Unreleased

### Fixes

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

### Changes

- Expose `instrumentStateGraph` for manual LangGraph instrumentation ([#6520](https://github.com/getsentry/sentry-react-native/pull/6520))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<String> 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.
Expand Down Expand Up @@ -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,
Comment on lines +410 to +412

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The new logic to filter background app starts in fetchNativeAppStart() lacks integration test coverage. A regression could go undetected, reintroducing inflated app start metrics.
Severity: LOW

Suggested Fix

Add an integration test for fetchNativeAppStart(). This test should mock an AppStartMetrics object where getAppStartReason() returns a background reason (e.g., "push") and isAppLaunchedInForeground() is true. Verify that the promise is resolved with null, confirming the app start metric is correctly dropped.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location:
packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java#L410-L412

Potential issue: The new logic in `fetchNativeAppStart` filters out app start metrics
from background processes to prevent metric inflation. However, this critical filtering
path is not covered by any integration tests. While a unit test validates the
`isBackgroundAppStartReason()` helper method, there is no test that simulates a scenario
where `isAppLaunchedInForeground()` is `true` but `getAppStartReason()` returns a
background reason like `"push"`. This means a future regression in the interaction
between these checks within `fetchNativeAppStart()` would go undetected, potentially
reintroducing the metric inflation issue this change aims to solve.

Also affects:

  • packages/core/android/src/test/java/io/sentry/react/RNSentryAppStartTest.java:1~45

"Invalid app start data: app was launched in the background (reason: "
+ appStartReason
+ ").");
promise.resolve(null);
return;
}
Comment on lines +414 to +418

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The code assumes metrics.getAppStartReason() returns a lowercase string, but the actual format from the external library is unverified and may differ, causing the feature to fail silently.
Severity: MEDIUM

Suggested Fix

Add an integration test to verify the actual return value and format of metrics.getAppStartReason() from the sentry-android dependency. Adjust the isBackgroundAppStartReason() method to handle the actual output format, whether it's an integer, an uppercase string, or something else.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location:
packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java#L414-L418

Potential issue: The code assumes that the `getAppStartReason()` method from the
external `sentry-android` library returns a lowercase string (e.g., `"push"`,
`"alarm"`). However, the actual return format is unverified. The underlying Android API
returns integer constants, and it's unknown how the Sentry library transforms them. If
the return value is an integer, an uppercase string, or another format, the
`isBackgroundAppStartReason()` check will always fail. This would cause the feature to
silently not work, meaning background-triggered app starts would not be filtered out,
leading to inflated app start metrics.

Did we get this right? 👍 / 👎 to inform future reviews.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warm starts wrongly dropped

Medium Severity

isBackgroundAppStartReason keys off the process start reason, which stays fixed for the process lifetime. After a background spawn (e.g. push), the first inflated open is correctly dropped, but later warm starts in the same process still report that reason and are dropped too, even though those spans are re-anchored at activity creation and are not inflated.

Fix in Cursor Fix in Web

Triggered by project rule: PR Review Guidelines for Cursor Bot

Reviewed by Cursor Bugbot for commit 0925ea0. Configure here.


WritableMap mutableMeasurement =
(WritableMap) RNSentryMapConverter.convertToWritable(metricsDataBag);

Expand Down
Original file line number Diff line number Diff line change
@@ -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"));
}
}
Loading