Summary
On Android, the replay REPLAY_DIVERGENCE screen.refs (and any consumer of collectSettleChromeRefs over a non-raw divergence capture) leak the status-bar systemui chrome — the top-of-screen clock/wifi/battery/carrier nodes are retained instead of dropped, consuming the 20-ref budget and pushing real app controls out of the divergence's screen.refs.
This is the Android analogue of the iOS keyboard case handled in #1233, but for the systemui status bar rather than the IME. IME exclusion works correctly (see #1233 evidence); this is a separate, pre-existing gap in the systemui-chrome classifier's applicability to the divergence's non-raw capture (introduced with the systemui filtering in #1198).
Live repro (Pixel_9_Pro_XL_API_37, test-app Checkout form, Gboard up)
A drifted get text .ad replayed against the running app (keyboard/IME up) → REPLAY_DIVERGENCE:
- Device raw snapshot: 229 nodes — 147 IME (
com.google.android.inputmethod.latin), 33 systemui (com.android.systemui), 49 app.
- Divergence
screen.refs (cap 20): 0 of 147 IME refs leaked (IME exclusion works), but the top-20 is filled by systemui status-bar nodes — clock "7:03", mobile_combo "T-Mobile", wifi_signal, battery, etc. The app's form fields sit at non-IME document position 74+ and fall outside the 20 refs.
Root cause
collectSettleChromeRefs → collectAndroidSystemChromeRunIndexes drops a systemui run only when a member carries a com.android.systemui:id/status_bar* / navigation_bar* resource-id marker.
- On a
--raw capture of the identical screen, the filter is perfect: all 33 systemui + 147 IME refs excluded, 0 leak. The status_bar, status_bar_container, status_bar_contents, … markers are present.
- Stripping
identifier from that same raw tree reproduces the divergence leak exactly: IME still excluded (bundleId-based), all 33 systemui leak (marker-based, now unreachable).
So the divergence's non-raw capture carries bundleId but not the systemui status_bar* resource-id markers the filter needs. buildSnapshotState's non-raw path (pruneGroupNodes → normalizeSnapshotTree) preserves the markers when re-fed a raw tree, so the gap is upstream in the divergence's backend dispatch (session-replay-divergence.ts dispatches snapshot with {...action.flags, snapshotInteractiveOnly}, no snapshotRaw). The CLI can't reproduce the divergence node set for inspection (default / --force-full compact to ~10 nodes; only --raw yields the full 229) — pinning backend-resource-id-gating vs. wrapper-pruning needs a one-line instrumentation in the divergence capture.
Scope
- Affects
get / is / wait divergences (requiresRect: false → full capture). press / fill use interactive capture (status bar is non-interactive, so it's not captured).
- Also blocks the "actionable SystemUI survives" case (volume panel, media/output pickers): without systemui resource-ids the divergence capture can't distinguish a droppable status/nav bar from a keep-worthy actionable overlay, so that distinction is untestable via divergence until this is fixed.
Fix direction (options)
- Capture-side (preferred): have the divergence classify chrome against a capture that carries systemui resource-ids — i.e. align the divergence snapshot with whatever the
--settle path uses so status_bar* markers reach collectSettleChromeRefs. Smallest change; keeps the classifier untouched.
- Filter-side: make systemui status-bar detection robust to missing resource-ids (structural: systemui + top-of-screen band + non-hittable), while keeping actionable systemui (hittable, e.g. volume slider). More invasive; higher risk of misclassifying actionable overlays.
Not a #1233 regression
#1233 (exclude keyboard/IME chrome from divergence screen.refs) is a strict improvement — before it, both IME and status-bar chrome leaked; after it, IME is excluded on both platforms (verified live). This issue is the remaining systemui half, tracked separately per maintainer decision.
Summary
On Android, the replay
REPLAY_DIVERGENCEscreen.refs(and any consumer ofcollectSettleChromeRefsover a non-raw divergence capture) leak the status-bar systemui chrome — the top-of-screen clock/wifi/battery/carrier nodes are retained instead of dropped, consuming the 20-ref budget and pushing real app controls out of the divergence'sscreen.refs.This is the Android analogue of the iOS keyboard case handled in #1233, but for the systemui status bar rather than the IME. IME exclusion works correctly (see #1233 evidence); this is a separate, pre-existing gap in the systemui-chrome classifier's applicability to the divergence's non-raw capture (introduced with the systemui filtering in #1198).
Live repro (Pixel_9_Pro_XL_API_37, test-app Checkout form, Gboard up)
A drifted
get text.adreplayed against the running app (keyboard/IME up) →REPLAY_DIVERGENCE:com.google.android.inputmethod.latin), 33 systemui (com.android.systemui), 49 app.screen.refs(cap 20): 0 of 147 IME refs leaked (IME exclusion works), but the top-20 is filled by systemui status-bar nodes —clock"7:03",mobile_combo"T-Mobile",wifi_signal,battery, etc. The app's form fields sit at non-IME document position 74+ and fall outside the 20 refs.Root cause
collectSettleChromeRefs→collectAndroidSystemChromeRunIndexesdrops a systemui run only when a member carries acom.android.systemui:id/status_bar*/navigation_bar*resource-id marker.--rawcapture of the identical screen, the filter is perfect: all 33 systemui + 147 IME refs excluded, 0 leak. Thestatus_bar,status_bar_container,status_bar_contents, … markers are present.identifierfrom that same raw tree reproduces the divergence leak exactly: IME still excluded (bundleId-based), all 33 systemui leak (marker-based, now unreachable).So the divergence's non-raw capture carries
bundleIdbut not the systemuistatus_bar*resource-id markers the filter needs.buildSnapshotState's non-raw path (pruneGroupNodes→normalizeSnapshotTree) preserves the markers when re-fed a raw tree, so the gap is upstream in the divergence's backend dispatch (session-replay-divergence.tsdispatchessnapshotwith{...action.flags, snapshotInteractiveOnly}, nosnapshotRaw). The CLI can't reproduce the divergence node set for inspection (default /--force-fullcompact to ~10 nodes; only--rawyields the full 229) — pinning backend-resource-id-gating vs. wrapper-pruning needs a one-line instrumentation in the divergence capture.Scope
get/is/waitdivergences (requiresRect: false→ full capture).press/filluse interactive capture (status bar is non-interactive, so it's not captured).Fix direction (options)
--settlepath uses sostatus_bar*markers reachcollectSettleChromeRefs. Smallest change; keeps the classifier untouched.Not a #1233 regression
#1233 (exclude keyboard/IME chrome from divergence
screen.refs) is a strict improvement — before it, both IME and status-bar chrome leaked; after it, IME is excluded on both platforms (verified live). This issue is the remaining systemui half, tracked separately per maintainer decision.