Skip to content

fix(replay): start a new session on opt-in whenever replay was wanted - #915

Merged
turnipdabeets merged 3 commits into
mainfrom
fix/opt-in-rotate-session-when-replay-wanted
Oct 30, 2026
Merged

turnipdabeets merged 3 commits into
mainfrom
fix/opt-in-rotate-session-when-replay-wanted

Conversation

@turnipdabeets

@turnipdabeets turnipdabeets commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

💡 Motivation and Context

In short: optIn() now starts a new session whenever the app wanted session replay at opt-out, even if the linked flag or remote config isn't ready to start replay yet. Before, Android only rotated the session once replay actually started, so a start after flags loaded resumed the session that was active while the user was opted out.

How it failed before (reproduced on an emulator, see below):

  1. Replay is on, linked to a flag that is off for this user.
  2. The user opts out, then opts in. No new session starts, because the linked-flag check returns before startSessionReplayInternal(resumeCurrent = false) rotates.
  3. The flag turns on and flags reload. internalOnFeatureFlagsLoaded resumes replay on the current session, so post-consent frames go to the session from before opt-out.

posthog-ios (#959) and React Native (posthog-js#5363) already rotate whenever replay was wanted. This makes Android match, and matches the opt-in spec in sdk-specs#138.

Change

  • optIn() rotates the session (endSession() + startSession()) when sessionReplayWanted, before reinstalling integrations, under the same setupLock.
  • startSessionReplayInternal takes rotateSession. The opt-in restart passes false, so it still takes the fresh-start recorder path (forced state reset and redraw, so the new session starts with meta + full snapshot) without rotating twice.
  • Unchanged: a recording the app stopped before opt-out keeps its session. Under React Native the session manager ignores the rotation, so the JS layer still owns the session id.

Behavior change: apps reading getSessionId() across optOut()/optIn() now see a new id whenever replay was wanted, including when the linked flag holds replay off.

Known issue, not changed here

If the app calls stopSessionReplay() while sessionReplay = true, Android restarts replay on the next remote-config re-evaluation. optIn() triggers one, and so does any flags reload, so this also happens without an opt-out. It reproduces on main and is unchanged by this PR. Fixing it changes what a stop means in general, so it belongs in its own PR.

💚 How did you test it?

  • make test passes, and make format is clean.
  • New test: "optIn starts a new session when replay is wanted but its flag holds it off". It fails without the fix.
  • New test: "optIn keeps the session when the app stopped replay before opting out".
  • Emulator (Pixel 9) against a local mock server that controls remote config and /flags and records every request:
Scenario This branch main
Recording → opt-out → opt-in New session, starts with meta + full snapshot Same
Linked flag off → opt-out → opt-in → flag on + reload New session after opt-in; replay starts on it Stays on the opted-out session, and replay records there
Stop while opted out → opt-in Restarts in a new session —
Manual start with sessionReplay = false → opt-out → opt-in Restarts in a new session —
Launched opted out → opt-in Records —
10 rapid opt-out/opt-in Ends opted in; one new session; no crash or ANR —

No events or snapshots were sent while opted out in any run.

📝 Checklist

  • All commits are signed (see Commit signing)
  • I reviewed the submitted code.
  • I added tests to verify the changes.
  • I updated the docs if needed.
  • No breaking change or entry added to the changelog.

If releasing new changes

  • Ran pnpm changeset to generate a changeset file

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

  • Claude Code compared opt-in replay behavior across iOS, Android, React Native and Flutter while updating sdk-specs#138, and found Android alone rotated only after the replay gates.
  • Two rounds of pre-PR review and triage, plus emulator runs before and after the fix. The review flagged the stop issue above as a separate fix.

🤖 Generated with Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

posthog-android Compliance Report

Date: 2026-10-29 22:00:31 UTC
Duration: 118073ms

✅ All Tests Passed!

46/46 tests passed


Capture Tests

✅ 29/29 tests passed

View Details
Test Status Duration
Format Validation.Event Has Required Fields ✅ 305ms
Format Validation.Event Has Uuid ✅ 25ms
Format Validation.Event Has Lib Properties ✅ 26ms
Format Validation.Distinct Id Is String ✅ 19ms
Format Validation.Token Is Present ✅ 21ms
Format Validation.Custom Properties Preserved ✅ 21ms
Format Validation.Event Has Timestamp ✅ 19ms
Retry Behavior.Retries On 503 ✅ 7025ms
Retry Behavior.Does Not Retry On 400 ✅ 4019ms
Retry Behavior.Does Not Retry On 401 ✅ 4021ms
Retry Behavior.Respects Retry After Header ✅ 7022ms
Retry Behavior.Implements Backoff ✅ 17032ms
Retry Behavior.Retries On 500 ✅ 7019ms
Retry Behavior.Retries On 502 ✅ 7016ms
Retry Behavior.Retries On 504 ✅ 7013ms
Retry Behavior.Max Retries Respected ✅ 17031ms
Deduplication.Generates Unique Uuids ✅ 30ms
Deduplication.Preserves Uuid On Retry ✅ 7013ms
Deduplication.Preserves Uuid And Timestamp On Retry ✅ 12029ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry ✅ 7014ms
Deduplication.No Duplicate Events In Batch ✅ 30ms
Deduplication.Different Events Have Different Uuids ✅ 20ms
Compression.Sends Gzip When Enabled ✅ 18ms
Batch Format.Uses Proper Batch Structure ✅ 16ms
Batch Format.Flush With No Events Sends Nothing ✅ 13ms
Batch Format.Multiple Events Batched Together ✅ 27ms
Error Handling.Does Not Retry On 403 ✅ 4017ms
Error Handling.Does Not Retry On 413 ✅ 4018ms
Error Handling.Retries On 408 ✅ 5024ms

Feature_Flags Tests

✅ 17/17 tests passed

View Details
Test Status Duration
Request Payload.Request With Person Properties Device Id ✅ 29ms
Request Payload.Flags Request Uses V2 Query Param ✅ 20ms
Request Payload.Flags Request Hits Flags Path Not Decide ✅ 23ms
Request Payload.Flags Request Omits Authorization Header ✅ 22ms
Request Payload.Token In Flags Body Matches Init ✅ 19ms
Request Payload.Groups Round Trip ✅ 20ms
Request Payload.Groups Default To Empty Object ✅ 20ms
Request Payload.Disable Geoip False Propagates As Geoip Disable False ✅ 20ms
Request Payload.Disable Geoip Omitted Defaults To False ✅ 19ms
Request Payload.Flag Keys To Evaluate Contains Only Requested Key ✅ 32ms
Request Lifecycle.No Flags Request On Init Alone ✅ 8ms
Request Lifecycle.No Flags Request On Normal Capture ✅ 18ms
Request Lifecycle.Two Flag Calls Produce Two Remote Requests ✅ 36ms
Request Lifecycle.Mock Response Value Is Returned To Caller ✅ 19ms
Retry Behavior.Retries Flags On 502 ✅ 320ms
Retry Behavior.Retries Flags On 504 ✅ 319ms
Side Effect Events.Get Feature Flag Captures Feature Flag Called Event ✅ 20ms

@marandaneto marandaneto left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The code looks good; no qualifying findings. Verdict: correct.

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

🤖 An AI agent wrote this on behalf of @ioannisj.

Nothing to flag. I checked the rotation in optIn() against the three startSessionReplayInternal callers and the recorder's uninstall() reset, and the new session lands before any restart path can resume the old one. Matches what iOS and RN do, so I'm happy with it.

@turnipdabeets
turnipdabeets merged commit 0310daf into main Oct 30, 2026
18 checks passed
@turnipdabeets
turnipdabeets deleted the fix/opt-in-rotate-session-when-replay-wanted branch October 30, 2026 14:20
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.

3 participants