Skip to content

fix(replay): isolate Android decor window streams - #754

Closed
dustinbyrne wants to merge 2 commits into
mainfrom
fix/session-replay-window-ids
Closed

fix(replay): isolate Android decor window streams#754
dustinbyrne wants to merge 2 commits into
mainfrom
fix/session-replay-window-ids

Conversation

@dustinbyrne

@dustinbyrne dustinbyrne commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

💡 Motivation and Context

Fixes #752.

$window_id is the document identity used by PostHog's replay pipeline. Snapshot ingestion groups events by it, and the player feeds each group to rrweb as one document timeline. A full snapshot rebuilds that timeline's document and node mirror; later incremental snapshots apply mutations to node IDs from that same document.

Android previously fell back to $window_id = $session_id for every native window. An Activity and a Dialog in the same recording therefore collapsed into one rrweb timeline. When the Dialog emitted its full snapshot, rrweb replaced the Activity document and node mirror with the Dialog's. After the Dialog closed, subsequent Activity mutations still referenced nodes from the original Activity document, so rrweb could not apply them and the recording became incomplete or appeared frozen. The window ID is therefore not optional metadata: each concurrently represented native document needs a distinct, stable value, while $session_id remains shared across the recording.

This change gives each tracked decor view a stable replay window ID and preserves that identity across snapshot-state resets. Snapshot batches and native touches use their owning decor ID. Network and logcat replay events use the foreground native decor at emission time, falling back to the existing session-derived ID when no native decor is eligible. Flutter-owned replay streams retain that fallback, while explicit native bridge snapshots use their decor ID.

💚 How did you test it?

  • make test
  • make compile
  • make checkFormat
  • ./gradlew apiCheck
  • Published core and Android artifacts to Maven Local under isolated test versions and exercised a fresh wireframe app on an API 36 emulator.
  • Captured Activity full snapshot, Dialog full snapshot, Activity update, and Activity add envelopes as window sequence A, B, A, A under one session.
  • Replayed the captured streams through PostHog's mobile transformer and rrweb player. Both documents reconstructed independently, Activity mutations applied, and no missing-node warnings were emitted.

📝 Checklist

  • 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

  • Added a changeset for both affected packages

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

Implemented and validated with the Pi coding agent using Gradle, ADB, a local capture server, and the PostHog replay transformer/player. Fresh Pi reviewer agents reviewed the design and implementation. The human directed the investigation and window-identity contract, discussed the implementation throughout, and has been good to me. Reviewer feedback identified and corrected a lifecycle race where synchronous removal during touch dispatch could otherwise restore a stale foreground window.

@dustinbyrne dustinbyrne self-assigned this Sep 2, 2026
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

posthog-android Compliance Report

Date: 2026-09-02 15:55:53 UTC
Duration: 118510ms

✅ All Tests Passed!

46/46 tests passed


Capture Tests

29/29 tests passed

View Details
Test Status Duration
Format Validation.Event Has Required Fields 381ms
Format Validation.Event Has Uuid 46ms
Format Validation.Event Has Lib Properties 43ms
Format Validation.Distinct Id Is String 29ms
Format Validation.Token Is Present 30ms
Format Validation.Custom Properties Preserved 30ms
Format Validation.Event Has Timestamp 27ms
Retry Behavior.Retries On 503 7035ms
Retry Behavior.Does Not Retry On 400 4026ms
Retry Behavior.Does Not Retry On 401 4027ms
Retry Behavior.Respects Retry After Header 7026ms
Retry Behavior.Implements Backoff 17037ms
Retry Behavior.Retries On 500 7019ms
Retry Behavior.Retries On 502 7015ms
Retry Behavior.Retries On 504 7020ms
Retry Behavior.Max Retries Respected 17038ms
Deduplication.Generates Unique Uuids 41ms
Deduplication.Preserves Uuid On Retry 7017ms
Deduplication.Preserves Uuid And Timestamp On Retry 12030ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry 7020ms
Deduplication.No Duplicate Events In Batch 37ms
Deduplication.Different Events Have Different Uuids 25ms
Compression.Sends Gzip When Enabled 17ms
Batch Format.Uses Proper Batch Structure 20ms
Batch Format.Flush With No Events Sends Nothing 14ms
Batch Format.Multiple Events Batched Together 37ms
Error Handling.Does Not Retry On 403 4019ms
Error Handling.Does Not Retry On 413 4021ms
Error Handling.Retries On 408 5029ms

Feature_Flags Tests

17/17 tests passed

View Details
Test Status Duration
Request Payload.Request With Person Properties Device Id 41ms
Request Payload.Flags Request Uses V2 Query Param 26ms
Request Payload.Flags Request Hits Flags Path Not Decide 24ms
Request Payload.Flags Request Omits Authorization Header 31ms
Request Payload.Token In Flags Body Matches Init 24ms
Request Payload.Groups Round Trip 28ms
Request Payload.Groups Default To Empty Object 27ms
Request Payload.Disable Geoip False Propagates As Geoip Disable False 25ms
Request Payload.Disable Geoip Omitted Defaults To False 22ms
Request Payload.Flag Keys To Evaluate Contains Only Requested Key 24ms
Request Lifecycle.No Flags Request On Init Alone 14ms
Request Lifecycle.No Flags Request On Normal Capture 45ms
Request Lifecycle.Two Flag Calls Produce Two Remote Requests 44ms
Request Lifecycle.Mock Response Value Is Returned To Caller 25ms
Retry Behavior.Retries Flags On 502 328ms
Retry Behavior.Retries Flags On 504 324ms
Side Effect Events.Get Feature Flag Captures Feature Flag Called Event 23ms

@dustinbyrne

Copy link
Copy Markdown
Contributor Author

this is not the right fix

@dustinbyrne dustinbyrne closed this Sep 3, 2026
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.

Wireframe replay: all windows share one $window_id (= session id), so any Dialog's full snapshot wipes the Activity for the rest of the session

1 participant