ref(tracing): Tidy forceFinish's return annotation and Timestamp.hashCode - #6091
Closed
runningcode wants to merge 9 commits into
Closed
runningcode wants to merge 9 commits into
runningcode wants to merge 9 commits into
Conversation
…AVA-642) The idle and deadline timers run on a thread that is frozen while the device is in deep sleep or the process is cached, so a timeout scheduled for 30s can fire hours later. Both callbacks stamped the spans with the wake-up time, turning an app start the user walked away from into a multi-hour transaction. Record when each timeout falls due at scheduling time and stamp with that instead, whenever the timer runs late. The deadline path passes the clamped timestamp down to forceFinish, which stamps every child before the root finishes and so otherwise defeats trimEnd. Fixes #5752 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
📲 Install BuildsAndroid
|
Whether a timeout expired was decided by comparing a fresh wall-clock reading against the projected due date. That is a duration measured from two independent wall-clock readings, which the clock can lengthen, shorten or make negative. A backward step while the timer waits made an expired timeout look pending, so the transaction was stamped with the wake-up time again; a forward step truncated a transaction whose timeout had not expired. Measure expiry on io.sentry.time.Deadline, which runs on the monotonic ticker and, on Android, on CLOCK_BOOTTIME so the interval includes deep sleep. The instant to end at stays a wall-clock timestamp, projected once from the same reading that sets the deadline. An expired timeout now always ends the transaction at the instant it fell due, including an on-time fire, which drops the scheduler jitter from the reported duration. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The tracer's timeout logic now works in io.sentry.time terms throughout: the duration on a Deadline, the instant as a Timestamp read from the options' EpochClock. Timestamp.toSentryDate bridges to the type the span API still takes, at the single point of use. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
FixedEpochClock, TestMonotonicTicker and DeferredExecutorService already existed; the tests were hand-rolling a date-provider fake and waiting on the real timer. Driving the ticker and the timer directly also removes the race between the test advancing its clock and the 20ms timer actually firing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rename Timestamp.toSentryDate to getSentryDate so Kotlin callers reach it as expiry?.expiredAt()?.sentryDate, and note at the call sites what the bridge is for and where the tracer reaches into options for its clocks. Also drops a meaningless @NotNull on forceFinish's void return. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The paragraph on which class holds the instant restated the field types. What is left is the part the code cannot say: why a late timer is possible at all, and why only a monotonic deadline can tell. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
runningcode
marked this pull request as ready for review
September 10, 2026 16:22
runningcode
requested review from
0xadam-brown,
adinauer,
markushi and
romtsn
as code owners
September 10, 2026 16:22
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit edd56b0. Configure here.
|
|
||
| final @NotNull SentryDate finishTimestamp = scopes.getOptions().getDateProvider().now(); | ||
| final @NotNull SentryDate finishTimestamp = | ||
| finishDate != null ? finishDate : scopes.getOptions().getDateProvider().now(); |
There was a problem hiding this comment.
Child timestamps ignore timeout cutoff
Medium Severity
When a late timeout backdates the transaction, children that already finished keep timestamps after that cutoff and extend past the parent. Unfinished children started after the due instant are ended at that earlier time, so their finish precedes their start.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit edd56b0. Configure here.
9 tasks
…l due Reverts b9fe709 and the refinements on top of it: a timed-out transaction is stamped with the wake-up time again, as it was before. Clamping changes every reported timeout duration, and it is the whole substance of this branch rather than a byproduct of its cleanups, so it goes out as its own PR instead of riding along here. What is kept are the two cosmetics the branch picked up on the way: forceFinish's void return, and Timestamp.hashCode deferring to Long.hashCode. JAVA-642 and #5752 stay open. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
runningcode
added a commit
that referenced
this pull request
Sep 18, 2026
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


📜 Description
Two cosmetic leftovers from this branch's attempt at #5752:
SentryTracer.forceFinishannotated@NotNullon avoidreturn.ITransactionandNoOpTransactionboth declare it plainvoid.Timestamp.hashCodehand-rolled the long fold thatLong.hashCodealready does.No behavioral change, no public API change, no changelog entry.
💡 Motivation and Context
This branch originally ended a timed-out transaction at the instant its timeout fell due, so that a device dozing through an idle or deadline timeout would not report an hours-long startup transaction (#5752, JAVA-642).
That clamp changes every reported timeout duration, and it was the whole substance of the branch rather than a byproduct of these cleanups, so it is reverted here (429c0eb) and will go out as its own PR. #5752 and JAVA-642 stay open.
See also #5755, which addresses the same problem by discarding the transaction instead.
💚 How did you test it?
./gradlew :sentry:test—SentryTracerTest(115) andTimestampTest(2) pass unchanged. Neither change is observable, so there is nothing new to test:@NotNullon avoidreturn is inert, andLong.hashCodecomputes the identical fold.📝 Checklist
sendDefaultPIIis enabled.🔮 Next steps
Re-land the timeout clamp as its own PR, decided on a monotonic
Deadlineas it is here.🤖 Generated with Claude Code