Skip to content

feat: [SDK-5390] send a session end when a sessions API session ends - #2781

Open
fadi-george wants to merge 3 commits into
new-session-eventsfrom
fadi/SDK-5390-android-session-end
Open

fadi-george wants to merge 3 commits into
new-session-eventsfrom
fadi/SDK-5390-android-session-end

Conversation

@fadi-george

@fadi-george fadi-george commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Description

One Line Summary

When the sessions API flag is on, send a session end PATCH with end_time and the final duration when a session ends.

Details

Motivation

Part of the SDK Sessions stack (SDK-5390). Ended sessions need a PATCH /apps/{app_id}/sessions/{session_id} with end_time and the final cumulative duration_seconds. Based on new-session-events.

Scope

  • SessionService.endSession() (timeout expiry in the background job, or cold start ending a prior session) builds an end UpdateSessionOperation when the session's usesSessionsApi is true:
    • end_time is the backgrounded-at time, tracked in a new persisted SessionModel.unfocusTime. An app killed while focused has no unfocus after its last focus, so the last focus time is used instead.
    • Duration is the session's final activeDuration.
    • Session ID is the session's localSessionId. The queue swaps in the server ID once the create succeeds.
  • End replaces unsent updates and blocks later ones through the existing SessionQueuePolicy and its persisted session state, which also gives the end the IDs its create sent, so no queue changes.
  • Android 15 background network access: in backgroundRun() the end is sent with enqueueAndWait(flush = true) and a 30 s timeout, so the request starts while the SyncJobService job is still running instead of after the 5 s batching delay. Flushing is allowed here because it happens at most once per session, and session start already flushes. On timeout the end stays queued and replays on the next app open.
  • Cold start enqueues the end without waiting, since the app is in the foreground.
  • Flag off: nothing new is sent. The legacy TrackSessionEndOperation is unchanged in both cases; turning off legacy reporting is SDK-5388.
  • Create and end op building moved to private top-level helpers to keep SessionService under detekt limits.

Testing

Unit testing

Added SessionServiceTests cases: end fields and flushed wait in the background run, returning on timeout, cold start ending the previous session before the new create, end time after a kill while focused, no end when the session uses the legacy path, and no end without a push subscription ID. unfocusTime added to the persistence test.

spotlessCheck, detekt, apiCheck, testReleaseUnitTest (core), and assembleRelease pass locally.

Manual testing

Not tested on device yet, including the Android 15 background send; end-to-end verification is planned in the Android Rollout & QA ticket (SDK-5405).

Affected code checklist

  • Notifications
    • Display
    • Open
    • Push Processing
    • Confirm Deliveries
  • Outcomes
  • Sessions
  • In-App Messaging
  • REST API requests
  • Public API changes

Checklist

Overview

  • I have filled out all REQUIRED sections above
  • PR does one thing
  • Any Public API changes are explained in the PR details and conform to existing APIs

Testing

  • I have included test coverage for these changes, or explained why they are not needed
  • All automated tests pass, or I explained why that is not possible
  • I have personally tested this on my device, or explained why that is not possible

Final pass

  • Code is as readable as possible.
  • I have reviewed this PR myself, ensuring it meets each checklist item

🤖 Generated with Claude Code

@fadi-george
fadi-george requested a review from a team as a code owner October 9, 2026 22:18
@fadi-george
fadi-george added this pull request to stack #2776 October 9, 2026 22:18
@github-actions

github-actions Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

📊 Diff Coverage Report

Diff 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

  • SessionModel.kt: 3/3 touched executable lines (100.0%) (9 touched lines in diff)
  • SessionService.kt: 32/32 touched executable lines (100.0%) (71 touched lines in diff)

Overall (aggregate gate)

35/35 touched executable lines covered (100.0% — requires ≥ 80%)

📥 View workflow run

@abdulraqeeb33 abdulraqeeb33 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Requesting changes.

The end uses the same id check as the create in #2780. If any id is missing, toEndOperation returns null, then the session is invalidated and activeDuration is zeroed. The end is gone, and a later retry has nothing left to send.

Same fix as #2780: enqueue and let the executor wait, and only clear the session once the end is queued.

@fadi-george
fadi-george force-pushed the fadi/SDK-5390-android-session-end branch 2 times, most recently from 454a642 to 24ec2e1 Compare October 16, 2026 00:23
@fadi-george
fadi-george removed this pull request from stack #2776 October 16, 2026 00:26
@fadi-george
fadi-george added this pull request to stack #2798 October 16, 2026 00:29

@abdulraqeeb33 abdulraqeeb33 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The missing-id finding is closed. initUser runs before startup, create and end share the id check, and the queue drops an end whose create was never queued.

One blocker: backgroundRun is the first suspend in this method, and BackgroundManager runs the background services on Dispatchers.Unconfined. When enqueueAndWait resumes, the rest of that loop, including scheduleBackground and its JobScheduler calls, continues on the thread that woke the waiter. On success that is the operation-repo thread, so those binder calls stall the queue. Wrap the wait in withContext(Dispatchers.IO).

Low: a new session does not clear unfocusTime. endTime is maxOf(unfocusTime, focusTime), so a backward clock step plus a kill during the next focus reports the previous session's unfocus. Set unfocusTime to null with the other new-session fields.

Base automatically changed from fadi/sdk-5382 to new-session-events October 20, 2026 17:53
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>
@fadi-george
fadi-george force-pushed the fadi/SDK-5390-android-session-end branch from 24ec2e1 to 6ffdc9f Compare October 20, 2026 19:44
… on a new session

BackgroundManager runs background services on Dispatchers.Unconfined, so the
rest of its loop resumed on the operation repo thread after the end was sent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@fadi-george

Copy link
Copy Markdown
Contributor Author

Thanks, both fixed in e06a1da.

  • The wait now runs in withContext(Dispatchers.IO), so the rest of the background loop resumes on an IO thread instead of the operation repo thread. I kept the timeout outside the switch so the timeout test still runs on virtual time.
  • A new session now clears unfocusTime.

@abdulraqeeb33 abdulraqeeb33 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The IO wrap fixes both resumes. A successful wake and the 30s timeout both continue BackgroundManager on an IO thread. unfocusTime is cleared on the new session.

The timeout test is a race, and it is why CI is red.

runTest advanced past the 30s timeout while the send was parked on the real
IO dispatcher, so the wait sometimes never started.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@abdulraqeeb33 abdulraqeeb33 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The timeout test stays on the virtual clock. currentTime hits 30s because the wait was entered and then cancelled, and CI is green. Production still waits on Dispatchers.IO.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants