Repository navigation
feat: [SDK-5396] send a heartbeat every 30 minutes of foreground time - #2782
fadi-george wants to merge 13 commits into
Conversation
…tion queue Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…return Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ard updates after in-flight end or for orphaned sessions Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… order Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…he session executor Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ssion pins in OperationRepo, drop unsendable ops under IV - Session ops drop after 5 failed attempts the backend answered. The count is persisted, and network errors don't count, so offline still retries until reconnect. - OperationRepo writes each ID translation it applies into the current session's pinned IDs and server session ID, replacing SessionService's PinnedLocalIdTranslator. - Under required identity verification, session ops for a local OneSignal ID that no identified login will create are discarded at enqueue and dropped with the anonymous purge. - CreateSessionOperation and UpdateSessionOperation share a SessionOperation base. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…cope session subscription translation, join pin write - Coalesced updates keep the highest failedAttempts, so a session that keeps enqueueing updates still reaches the attempt limit. - The session's pinned subscription ID is only translated when the same response translates its pinned OneSignal ID, since login reuses a local push subscription for the new user. - The pin write is joined again so IDs are saved before a successful create is removed from the store, and it is skipped before the first session starts. - LoginUserFromSubscriptionOperation counts as a login that will create the user under identity verification. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… race on queue order Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When the session uses the sessions API, enqueue a CreateSessionOperation with the start time and pinned IDs. Continuing within the session timeout sends none, and the legacy start path is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When the session uses the sessions API, enqueue an end UpdateSessionOperation with the backgrounded-at time and final duration. In the background job, flush and wait for it with a timeout so Android 15+ doesn't block the request after the job finishes. Flag off leaves the legacy end unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
While a sessions API session is focused, enqueue a duration-only UpdateSessionOperation every 30 minutes of active time. The timer is cancelled on unfocus and resumes on focus, so background time doesn't count. Flag off starts no timer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
📊 Diff Coverage ReportDiff Coverage Report (Changed Lines Only)Gate: aggregate coverage on changed executable lines must be ≥ 80% (JaCoCo line data for lines touched in the diff). Changed Files Coverage
Overall (aggregate gate)38/38 touched executable lines covered (100.0% — requires ≥ 80%) |
…last heartbeat An app killed while focused never adds its last focus to activeDuration, so the end on the next cold start could report less duration than a heartbeat already sent. Persist the last heartbeat's duration and time on the session and use them as a floor for the end's duration and end time. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
abdulraqeeb33
left a comment
There was a problem hiding this comment.
Approving.
Foreground time only, remaining time recomputed after wake, and a kill while focused ends at max(activeDuration, lastHeartbeatDuration). This depends on #2780 and #2781 holding creates and ends that are missing ids. Those two still drop them.
One nit: lastHeartbeatDuration is written before the enqueue. If the ids are missing, the tick is skipped and the next try waits another 30 minutes. A later successful tick still sends the cumulative duration, so this delays the update rather than losing it.
| continue | ||
| } | ||
| Logging.debug("SessionService: heartbeat with activeDuration: $duration") | ||
| session.lastHeartbeatDuration = duration |
There was a problem hiding this comment.
This advances lastHeartbeatDuration even when toUpdateOperation returns null. A missing id then waits another full interval. Set it only after the update is enqueued, or leave it unchanged so the next tick retries.
Description
One Line Summary
When the sessions API flag is on, send a duration-only session
PATCHevery 30 minutes of foreground time.Details
Motivation
Part of the SDK Sessions stack (SDK-5396). Keeps long foreground sessions alive on the server's inactivity timer and bounds duration loss if the app crashes while foregrounded. Based on #2781.
Scope
SessionServicestarts a heartbeat coroutine on focus for sessions whoseusesSessionsApiis true, and cancels it on unfocus.elapsedRealtime) is 30 minutes past the last heartbeat. Background time doesn't count, and each tick's duration is always higher than the last, so unchanged durations are never sent.UpdateSessionOperationwith the cumulative duration and noend_time. The existing coalescing folds unsent heartbeats together and discards them once the end is queued, so no queue changes.SessionModel. An app killed while focused never adds its last focus toactiveDuration, so the end on the next cold start uses the larger ofactiveDurationand the last heartbeat's duration, and the latest of the unfocus, focus, and heartbeat times asend_time. Otherwise the end could report less than the server already has.toUpdateOperation(appId, activeDuration, endTime?)and shared with the heartbeat.Testing
Unit testing
Added
SessionServiceTestscases on a virtual clock: one heartbeat after 30 minutes, two after 60, background pauses the timer and only foreground time counts, server session ID once created, new session restarts the interval, no timer on the legacy path, no heartbeat (and no busy loop) without a push subscription ID, cold start after a kill following a heartbeat ends with the heartbeat's duration and time, and a new session clears the last heartbeat. Heartbeat fields added to the persistence test.spotlessCheck,detekt, andtestDebugUnitTest(core) pass locally.Manual testing
Not tested on device yet; end-to-end verification is planned in the Android Rollout & QA ticket (SDK-5405).
Affected code checklist
Checklist
Overview
Testing
Final pass
🤖 Generated with Claude Code