Skip to content

fix(android): Decide session rotation on a monotonic clock (JAVA-573) - #6096

Open
runningcode wants to merge 4 commits into
mainfrom
no/java-573-session-interval-monotonic
Open

fix(android): Decide session rotation on a monotonic clock (JAVA-573)#6096
runningcode wants to merge 4 commits into
mainfrom
no/java-573-session-interval-monotonic

Conversation

@runningcode

@runningcode runningcode commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

📜 Description

Background context:
The SDK decides it is a new session if you leave the app for more than 30 seconds. So if you come back under 30 seconds you use the same session. More than 30 seconds you get a new session.

The problem is, yup, you guessed it, we're using a wall clock time to determine these 30 seconds. The wall clock could jump backwards or forwards within these 30 seconds and then we make an incorrect decision on whether or not create a new session.

This swaps the logic to use a monotonic clock and a Deadline.

The code is also simpler now. Before there were two places that would check to end the session. One was to check lastUpdatedSession against the currentTimeMillis which is the WallClock. The other was the .schedule(endSession, sessionIntervalMillis) which is a monotonic clock. They could both have different values.

Not fixed here, deliberately: the other half of JAVA-573. A session that slept through its background window is still stamped when the SDK noticed rather than when the window is due, so its duration is inflated by the suspend. That value is serialized which is why we aren't fixing it here. #6091 is a template for fixing it. Its Expiry is the shape to reuse, and probably wants lifting out of SentryTracer first.

💡 Motivation and Context

Part of the clock semantics hardening tracked in JAVA-557 §B2. Note that §B2's premise is stale on one point: java.util.Timer was already replaced by the shared timer executor in #5819, so this PR is about which clock the window is measured on, not about the timer type.

💚 How did you test it?

Its hard to reproduce deep sleep and clock pinning but the logic is tested with some new unit tests.

📝 Checklist

  • I added GH Issue ID & Linear ID
  • I added tests to verify the changes.
  • No new PII added or SDK only sends newly added PII if sendDefaultPII is enabled.
  • I updated the docs if needed.
  • I updated the wizard if needed.
  • Review from the native team if needed.
  • No breaking change or entry added to the changelog.
  • No breaking change for hybrid SDKs or communicated to hybrid SDKs.
  • Public API changes reviewed by another Mobile SDK team member or implemented according to the develop docs spec.

No .api diff — LifecycleWatcher is package-private.

🔮 Next steps

Stamping the session end at the instant the window is due, so a suspended session stops reporting the sleep as app usage.

🤖 Generated with Claude Code

Whether foregrounding rotates the session was a comparison between two
System.currentTimeMillis() readings taken up to 30 seconds apart. The gap
between two wall-clock reads is not the time that passed — the device syncs
its clock and the interval comes out too long, too short, or negative — so a
step forward started a session that should have been resumed, and a step back
resumed one that should have ended. Android steps the wall clock most often in
the first seconds after boot, which is exactly where a cold start's background
window lives.

The background window is a duration, so it now lives on a Deadline over
options.getMonotonicTicker(), which on Android is CLOCK_BOOTTIME and so keeps
counting through deep sleep. One deadline serves both halves of the window: the
end-session task is scheduled for its remaining(), and a foreground arriving
first asks whether it hasPassed(). Measuring one window in two places is what
allowed two readings to disagree.

The staleness of a session already on the scope stays on the wall clock and is
now named for it. That path is only reached before the app has been backgrounded
in this process, and the only record of when that session started is
Session.getStarted() — a serialized epoch instant, which no tick can be
compared against. A TODO [MAJOR] marks the real fix: a session that records the
tick it started on.

Passing the clocks in from AppLifecycleIntegration, which already holds the
options being registered, removes the second constructor rather than adding a
third parameter to it.

Two tests were pinning the old behavior: `if last started session is before
interval` stubbed the clock with (2, 1) and passed only because the wall clock
ran backwards. They become a real background-foreground cycle on a
TestMonotonicTicker, with one regression test per step direction.

This leaves the other half of JAVA-573 open: a session that slept through its
window is still stamped when the SDK noticed rather than when the window fell
due, so its duration is inflated by the suspend. That value is serialized, and
#6091 is the template for fixing it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@linear-code

linear-code Bot commented Sep 11, 2026

Copy link
Copy Markdown

JAVA-573

@sentry

sentry Bot commented Sep 11, 2026

Copy link
Copy Markdown

📲 Install Builds

Android

🔗 App Name App ID Version Configuration
SDK Size io.sentry.tests.size 8.56.0 (1) release

⚙️ sentry-android Build Distribution Settings

runningcode and others added 2 commits September 11, 2026 15:28
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…(JAVA-573)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@runningcode
runningcode marked this pull request as ready for review September 11, 2026 13:36
@runningcode runningcode added the sanity-check PR needs a lightweight review for obvious issues label Sep 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

sanity-check PR needs a lightweight review for obvious issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant