Repository navigation
iOS: keep secure rectangles and the view tree on windows away from the screen's origin - #30
Conversation
There was a problem hiding this comment.
Stale comment
Deep code review
This PR moves iOS secure rectangles and the managed view-tree origin from the React root window’s points onto the screen, matching Android’s tracker/pulls split and the unreleased cocoa SDK that composites every window. The store, rounding, versioning, and on-main pull refresh are carefully built. Two issues should be fixed before merge.
Findings
P1 High —
BGSRNSdkComposesScreen()treats every real iOS device as the new compositor, but this repo still pins 7.0.0-beta3. Beta3 records the key window’s scene and draws rectangles / native nodes in scene points (frame.origin). Adding the screen origin inverts the Stage Manager / Split View bug this PR is fixing: masks and the React tree sit one window-offset away from the content, so secret UI is recorded in the clear. iPhone (origin {0,0}) is unchanged; iPad multitasking is not. Task 6.6 review I1 previously requiredframe.originfor this reason.P2 Medium —
setSecureRectanglespublishes coordinates on the TurboModule queue (origin still {0,0} until a root has been found) and only thendispatch_asyncs the origin read. The SDK pulls on main and will re-read on the version bump from that publish. Until the hop runs, Stage Manager windows are served unmoved.Overall risk: High
High because the P1 is a privacy defect on the currently declared SDK, in the exact iPad configuration the PR exists to fix. Against the unreleased compositor SDK the design looks right (and the iPad Stage Manager
secure-componentrun is good evidence). I did not run the Support XCTests or e2e here (Linux agent, no iOS toolchain).Merge recommendation
Do not merge onto main while
native-versions.json/ios/Support/Package.swiftstill pin 7.0.0-beta3. Land it with the compositor SDK (bugsee-cocoa #170–#172), or gateBGSRNWindowRecordedOrigin/ window walking on that SDK so beta3 keepsframe.origin.Most important to fix
- Tie screen-space origin to the SDK that actually composites, and bump the pin in the same change.
- Publish rectangles only after the origin has been applied, on main, so the SDK never observes an unmoved version.
- Still missing: managed view-tree on a device window that is not at the screen origin (called out in the test plan). Simulator e2e cannot distinguish old vs new origin.
What looks solid
- Served-buffer versioning only when the moved rectangles change; outward floor/ceil for a fractional origin; int32 saturation.
- Weak cached root and no search on the SDK pull (brownfield-safe); on-main refresh-before-snapshot is stricter than Android’s post-to-UI-thread model.
- Support tests cover store origin/version, tracker cache vs search, and pull throttling.
Sent by Cursor Automation: Bugsee code review
There was a problem hiding this comment.
Stale comment
Deep code review
Re-review of 282233b (prior review was 4af4f55). The store, rounding, versioning, weak root cache, and on-main pull refresh are still carefully built. One of the two earlier issues is fixed; the other is not.
Prior findings
P2 — fully addressed in
99012d7.setSecureRectanglesnow hops to main and-publishCoordinates:forDisplay:reads the window’s place before it writes the rectangles.testAPublishReadsThePlaceBeforeItWritesTheRectanglesfails if the store already has the new rects during the origin read. That publish-order leak is gone.P1 High — not addressed.
BGSRNSdkComposesScreen()is still “not Catalyst / not iOS-on-Mac”. The pin is still 7.0.0-beta3. Beta3 records the key window’s scene and draws rectangles / native nodes in scene points (frame.origin). Adding the screen origin inverts the Stage Manager / Split View bug this PR exists to fix: masks and the React tree sit one window-offset away from the content, so secret UI is recorded in the clear. iPhone (origin {0,0}) is unchanged; simulator e2e cannot tell the origins apart. The PR body states the compositor SDK requirement; the later commits only document pull cadence and the one-root tracker limit.No new P0–P3 on the tracker, pulls, or store. I did not run the Support XCTests or e2e here (Linux agent, no iOS toolchain). CI on this head was still pending at review time.
Overall risk: High
High because the remaining P1 is a privacy defect on the currently declared SDK, in the exact iPad configuration the PR is meant to fix. Against the unreleased compositor SDK the design looks right (the iPad Stage Manager
secure-componentrun is good evidence).Merge recommendation
Do not merge onto main while
native-versions.json/ios/Support/Package.swiftstill pin 7.0.0-beta3. Land it with the compositor SDK (bugsee-cocoa #170–#172), or gateBGSRNWindowRecordedOrigin/ window walking on that SDK so beta3 keepsframe.origin.Most important to fix
- Tie screen-space origin to the SDK that actually composites, and bump the pin in the same change.
- Still missing: managed view-tree on a device window that is not at the screen origin (called out in the test plan).
What looks solid
- Publish is origin-then-write on main; a pull cannot observe new rectangles at a stale origin.
- Served-buffer versioning only when the moved rectangles change; outward floor/ceil for a fractional origin; int32 saturation.
- Weak cached root and no search on the SDK pull (brownfield-safe); on-main refresh-before-snapshot is stricter than Android’s post-to-UI-thread model.
- Support tests cover store origin/version, tracker cache vs search, publish order, and pull throttling.
Sent by Cursor Automation: Bugsee code review
Review fix round 1 (C1, C2, I1, I2, M1, M3, M4, M5).
C1, Android: the origin of a Modal's DialogRootViewGroup is its
locationOnScreen alone. Only a ReactRoot (the activity root) has its
viewport offset applied by Fabric; the dialog root is a RootView that is
not one, and the modal host node's transform is the identity.
ReactRootOriginTracker.surfaceOrigin makes the choice, and the JVM dialog
test now feeds a -51 viewport (a translucent status bar) and expects
locationOnScreen.
C2: no native call per node, and none on a timer. JS reads a view's surface
off the fiber tree: the React tag of the nearest RCTModalHostView host above
it, or the main surface (src/secure/surface.ts). <BugseeSecure> reads it once
per mount and publishes {surface, rects}. The vh walk carries an origin down
the tree and asks secureSurfaceOrigin once per Modal per walk. The spec
loses secureSurfaceKey. Android watches a Modal's dialog root when JS first
publishes on it (found from the host's first child), retries on every
refresh until the root exists, and answers secureSurfaceOrigin from that
cache. The activity root is the main surface and is watched once (M5).
I2, fail closed: a surface other than the main one whose origin has not
been read serves one full-display rectangle on both platforms. A Modal
whose host tag cannot be read publishes on surface -1, never the main one.
Nothing falls back to the main surface on a timeout.
I1, iOS: each Modal has its own lane. Its origin is the presented view
controller's view in window coordinates plus the window's frame.origin,
read at pull time, so a pageSheet is placed inside the window. Per lane the
store takes #30's semantics: a CGPoint origin, edges rounded outward and
saturating, and re-recording the same origin costs nothing (M3). The
comments M4 found deleted are back.
M1: an empty Modal lane is dropped when its dialog root detaches (Android)
or its host is gone (iOS).
Tests: Jest 2093 passed (surface.test.ts, the BugseeSecure, registry, walk
and requests surface cases); JVM 287 root and 317 example-Gradle;
BugseeRNSupport XCTests 246.
Mutation: applying the activity formula to every root fails
aDialogRootsOriginIsItsLocationOnScreenAlone and
aWatchedSurfacesOriginMovesOnlyItsRectangles; on the WOD_LX1 (edge-to-edge
off) the same build fails secure-modal-translucent: 8128 cyan pixels of the
Modal's secure view in the report, served sheet 611-811 against 560-760 on
screen.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
…closed on every lane Re-review fix round 2 (N1-N4). N1: a lane with an unknown origin serves the display's real bounds, not a 2^20 square. Android records Display.getRealSize from the root's display on every origin read and from DisplayManager on the first publish; iOS records UIScreen.mainScreen.bounds on every pull. Android also clamps every served rectangle to the display and drops one left with no area: the SDK's native video mask (jni/video.c hideRects) clamps rows to the frame but not a rectangle's right edge to a row, so an oversized one blacked out the start of the next row on every row. Before the size is known the fallback is a 16384 square. N2 (Android): a dialog root the tracker forgets (seen detached on a refresh, or dropped by detach() on host destroy) while its surface holds rectangles has its origin reset to unknown, so it fails closed, and goes back to pending, so a later refresh re-watches it. No origin is recorded from a root that is not attached to its window. N3 (Android): only a non-empty publish watches a surface, and a pending surface whose rectangles JS has cleared is dropped with its empty lane instead of being retried. N4 (ruling): the main lane fails closed too. Until its origin has been read once, its rectangles are served as one display-bounds rectangle. iOS: a note at BGSRNModalSurfaceOrigin for #30's rebase. Tests: JVM root and example-Gradle 332 tests, 0 failures; BugseeRNSupport 261, 0 failures. Mutations, each reverted: main lane not failing closed (13 JVM failures, 4 XCTest), no clamp (everyServedRectangleIsClampedToTheDisplay), no forgetOrigin (2), recording an unattached root's origin (2), no pending prune (2), iOS screen size ignored (4). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
… Modal's own origin (#48) * fix(android): serve each React surface's secure rects by its own origin Fabric measureInWindow is relative to the measured node's nearest RootNodeKind ancestor, so a rectangle inside a <Modal> is relative to the dialog's DialogRootViewGroup, not the activity root. SecureRectangleStore now keeps one lane per surface (raw rects + that root's locationOnScreen - viewportOffset) and serves them merged in surface-key order; the legacy single-surface set/setOrigin keep meaning the main surface. ReactRootOriginTracker watches extra dialog roots, refreshes their origins with the activity root, mirrors the activity origin onto MAIN_SURFACE for manual writers, and keeps and releases each watched root's layout-listener token. Ported from feat/phase-6-modal (9ec12a2, e6e886f) onto main. TDD: SecureRectangleStoreTest's two-surface cases failed against a surface-ignoring stub (expected 2 rectangles, was 1); tracker tests did not compile without RootHandle.surfaceKey / watchNow. Mutation: publishing a watched dialog root with the activity root's origin fails aWatchedSurfacesOriginMovesOnlyItsRectangles (expected 140, was 100); reverted. 🤖 Generated with [Claude Code](https://claude.com/claude-code) * fix(ios): keep per-surface secure lanes with their own origin BGSRNSecureRectangles stores each surface's raw rectangles with the origin that places them and serves the lanes merged in surface-key order; the version moves only when the served bytes change, and an origin-only write that leaves the set empty still reports version 1. The legacy single-surface API means BGSRNSecureMainSurface. On iOS a Fabric <Modal> is presentViewController: on the same UIWindow, so it shares the main surface and that window's frame.origin; only Android dialogs use a second key. Ported from feat/phase-6-modal (9ec12a2, e6e886f) onto main. TDD: the two-surface and version-1 XCTests did not compile against main's store (no surface/origin API). Mutation: translating every lane by the main surface's origin fails testTheMainOriginDoesNotMoveAnotherSurfacesRectangle and testAnotherSurfacesOwnOriginMovesOnlyItsRectangle; reverted. BugseeRNSupport: 237 tests, 0 failures. 🤖 Generated with [Claude Code](https://claude.com/claude-code) * fix(privacy): publish <BugseeSecure> and managed nodes on their own surface Fabric measureInWindow is relative to the measured node's nearest RootNodeKind ancestor, and a <Modal> is one (an Android Dialog). The spec gains setSecureRectanglesOnSurface, secureSurfaceKey and secureSurfaceOrigin. The registry keeps one union per display and surface and publishes a non-main surface through setSecureRectanglesOnSurface; <BugseeSecure> asks native for the surface key of the root that holds its view (main surface when it has no tag or the answer is not an integer); the view-tree walk places each node by the origin of its own React root, falling back to the request's origin. Android resolves the root from the view tag on the UI thread, watches it, and returns that root's locationOnScreen - viewportOffset. iOS Fabric presents a Modal on the same UIWindow, so it answers the main surface and the hosting window's frame.origin, and refreshes that origin on every pull. New log lines carry a class name only. Ported from feat/phase-6-modal (9ec12a2, e6e886f) onto main. TDD: the per-surface registry tests failed against a surface-ignoring stub (8 of 9); the BugseeSecure surface tests failed 3 of 15 before the lookup; the walk and requests env tests failed to compile / 15 of 15 before the env fields existed. Mutation: placing every node by the request origin fails 3 walk/requests tests; reverted. Stryker on the four changed files: 95.69%, survivors in new code are equivalent (a no-op republish, a null read inside a try). 🤖 Generated with [Claude Code](https://claude.com/claude-code) * test(e2e): cover <BugseeSecure> inside a <Modal> on both surfaces Scenario secure-modal mounts a white <BugseeSecure> on the activity root and another inside a transparent <Modal>, logs both measureInWindow rectangles and uploads. secure-modal.test.ts asserts both regions are black in the report's screenshot while a control below the main one stays bright, and on Android that the served set covers the main rectangle (from JS, scaled and moved by the activity origin) and the sheet (from uiautomator) within 2 px. Ported from feat/phase-6-modal (9ec12a2); main already had E2E_METRO_HOST and its own metro watchFolders, so neither is re-added. The unused MAIN_LABEL constant is dropped for lint. Device: WOD_LX1 edge-to-edge on and off, iPhone 17 Pro simulator: 1/1 each, mainInner and sheetInner luma 0. 🤖 Generated with [Claude Code](https://claude.com/claude-code) * fix(privacy): place each Modal by its own root, keyed off the fiber tree Review fix round 1 (C1, C2, I1, I2, M1, M3, M4, M5). C1, Android: the origin of a Modal's DialogRootViewGroup is its locationOnScreen alone. Only a ReactRoot (the activity root) has its viewport offset applied by Fabric; the dialog root is a RootView that is not one, and the modal host node's transform is the identity. ReactRootOriginTracker.surfaceOrigin makes the choice, and the JVM dialog test now feeds a -51 viewport (a translucent status bar) and expects locationOnScreen. C2: no native call per node, and none on a timer. JS reads a view's surface off the fiber tree: the React tag of the nearest RCTModalHostView host above it, or the main surface (src/secure/surface.ts). <BugseeSecure> reads it once per mount and publishes {surface, rects}. The vh walk carries an origin down the tree and asks secureSurfaceOrigin once per Modal per walk. The spec loses secureSurfaceKey. Android watches a Modal's dialog root when JS first publishes on it (found from the host's first child), retries on every refresh until the root exists, and answers secureSurfaceOrigin from that cache. The activity root is the main surface and is watched once (M5). I2, fail closed: a surface other than the main one whose origin has not been read serves one full-display rectangle on both platforms. A Modal whose host tag cannot be read publishes on surface -1, never the main one. Nothing falls back to the main surface on a timeout. I1, iOS: each Modal has its own lane. Its origin is the presented view controller's view in window coordinates plus the window's frame.origin, read at pull time, so a pageSheet is placed inside the window. Per lane the store takes #30's semantics: a CGPoint origin, edges rounded outward and saturating, and re-recording the same origin costs nothing (M3). The comments M4 found deleted are back. M1: an empty Modal lane is dropped when its dialog root detaches (Android) or its host is gone (iOS). Tests: Jest 2093 passed (surface.test.ts, the BugseeSecure, registry, walk and requests surface cases); JVM 287 root and 317 example-Gradle; BugseeRNSupport XCTests 246. Mutation: applying the activity formula to every root fails aDialogRootsOriginIsItsLocationOnScreenAlone and aWatchedSurfacesOriginMovesOnlyItsRectangles; on the WOD_LX1 (edge-to-edge off) the same build fails secure-modal-translucent: 8128 cyan pixels of the Modal's secure view in the report, served sheet 611-811 against 560-760 on screen. 🤖 Generated with [Claude Code](https://claude.com/claude-code) * test(e2e): find each secure view by its colour, and add the discriminating Modals Review fix round 1 (C1, I1, I3). The secure-modal* scenarios paint the main secure view #FF00FF and the Modal's #00FFFF. Ground truth no longer comes from what the app measured: the device's own screenshot (adb screencap, simctl io screenshot) finds where each colour really is, the report's screenshot must be dark there, and it must not contain either colour anywhere (on the iPhone, which has no command-line screenshot, only the second check runs). New scenarios: secure-modal-translucent (Android; statusBarTranslucent, so with edge-to-edge off the dialog's origin is 0 while the activity root's is the status bar) and secure-modal-sheet (iOS; an opaque pageSheet, inset inside the window). The Android main-rectangle lookup matches the activity root's `kind=activity` origin line instead of `surface=0`, so it cannot be built from the Modal's origin (I3). 🤖 Generated with [Claude Code](https://claude.com/claude-code) * test(e2e): an opaque pageSheet hides the main secure view On an iPhone the pageSheet covers the presenting view, so the device screenshot shows only the Modal's colour. The sheet case now requires the main colour to be absent from the device screenshot and checks the report over the sheet alone; the bright control sits inside the Modal's white backdrop below its secure view in every case. Simulator: secure-modal 2/2 and secure-modal-sheet 2/2, sheet found at 104,342-304,442 pt (62 pt below its measureInWindow), sheetInner 0, no secure colour in the report. With every Modal placed at the window origin (the pre-I1 rule) the sheet case fails: 17760 cyan pixels, sheetInner 114.9. 🤖 Generated with [Claude Code](https://claude.com/claude-code) * test(secure): expect the error's name in BugseeSecure's warning PR 45 (0eb3cf0) made <BugseeSecure> warn with the error's name rather than the error itself. The surface case added on this branch now expects the same shape. 🤖 Generated with [Claude Code](https://claude.com/claude-code) * fix(privacy): bound and clamp what the secure store serves, and fail closed on every lane Re-review fix round 2 (N1-N4). N1: a lane with an unknown origin serves the display's real bounds, not a 2^20 square. Android records Display.getRealSize from the root's display on every origin read and from DisplayManager on the first publish; iOS records UIScreen.mainScreen.bounds on every pull. Android also clamps every served rectangle to the display and drops one left with no area: the SDK's native video mask (jni/video.c hideRects) clamps rows to the frame but not a rectangle's right edge to a row, so an oversized one blacked out the start of the next row on every row. Before the size is known the fallback is a 16384 square. N2 (Android): a dialog root the tracker forgets (seen detached on a refresh, or dropped by detach() on host destroy) while its surface holds rectangles has its origin reset to unknown, so it fails closed, and goes back to pending, so a later refresh re-watches it. No origin is recorded from a root that is not attached to its window. N3 (Android): only a non-empty publish watches a surface, and a pending surface whose rectangles JS has cleared is dropped with its empty lane instead of being retried. N4 (ruling): the main lane fails closed too. Until its origin has been read once, its rectangles are served as one display-bounds rectangle. iOS: a note at BGSRNModalSurfaceOrigin for #30's rebase. Tests: JVM root and example-Gradle 332 tests, 0 failures; BugseeRNSupport 261, 0 failures. Mutations, each reverted: main lane not failing closed (13 JVM failures, 4 XCTest), no clamp (everyServedRectangleIsClampedToTheDisplay), no forgetOrigin (2), recording an unattached root's origin (2), no pending prune (2), iOS screen size ignored (4). 🤖 Generated with [Claude Code](https://claude.com/claude-code) * test(e2e): check the Modal's secure view in every video frame too Re-review fix round 2 (N6). The Modal holds a red witness view, not secure. On targets with a device screenshot, the test finds the witness there, crops it and the Modal's secure view from every interior video frame (letterbox mapped and checked against video.aux on Android, width-fit on iOS, as secure-component.test.ts does), and requires the secure view to be dark in every frame that shows the witness, with the Modal still up in the last frame. Both crops come from the same frames, so the video's timestamp lag does not matter. 🤖 Generated with [Claude Code](https://claude.com/claude-code) * fix(privacy): drop a JS runtime's Modal surfaces when it goes away Re-review 2 fix (N7). After a JS reload with a secure Modal open, the old runtime's Modal lane failed closed for the rest of the process: dispose forgot its root and nothing retried it, so every recording was black. A Modal surface's key is a React tag only its runtime knows, so the store now drops every surface but the main one when a runtime goes: the new module claims the store on construction (Android constructor, iOS init) and the old one releases it on invalidate. Claim and release are serialised, and a release by a module whose claim is no longer current drops nothing, so an old module invalidated after the new one started cannot drop the new runtime's surfaces. A disposed Android tracker keeps nothing pending. The main lane keeps the N4 behaviour. N8 (ruled): the store headers now say the main surface stays one whole-display rectangle while no React root can be found. Tests: JVM claimingANewRuntimeDropsEveryModalSurfaceButKeepsTheMainOne, releasingTheCurrentRuntimeDropsItsModalSurfaces, releasingAnOldRuntimeLeavesTheNewRuntimesSurfaces, disposingWithAnOpenModalLeavesNothingPending, and the three XCTest twins. Mutations, each reverted: claim without dropping (2 failures), release ignoring the claim (1), a disposed tracker re-pending (1). 🤖 Generated with [Claude Code](https://claude.com/claude-code) * chore(android): log at debug what a JS runtime's claim and release drop Two debug-level lines (BugseeRN, numbers only): how many Modal surfaces with rectangles a new runtime's claim found left behind, and how many the released runtime held and left. They are the device evidence for N7: a dev reload on the WOD_LX1 logs `released: 0 ... held, 0 left` then `claimed: dropped 0`, since RN's teardown has already cleared the Modal. 🤖 Generated with [Claude Code](https://claude.com/claude-code) * fix(privacy,android): ignore secure-store writes from a runtime whose claim is stale A reload can construct the new BugseeModule before the old one is invalidated, and Fabric numbers React tags from 1 again. The old module's late publish could put back a Modal lane the new claim dropped, or its unmount's empty publish could clear the new runtime's lane on the same tag and uncover the new Modal. Every lane write a runtime makes now carries its claim: the module's publish (publishForRuntime) and its tracker's setOrigin, forgetOrigin and dropSurfaceIfEmpty. The store ignores a write whose claim is not current, under the same lock as claimRuntime/releaseRuntime, and logs it at debug with the two claim numbers only. Addresses PR 48 review thread 4182289966. 🤖 Generated with [Claude Code](https://claude.com/claude-code) * fix(privacy,ios): ignore secure-store writes from a runtime whose claim is stale The iOS twin of the Android fix. The module's rectangle writes now carry its claim (setCoordinates:count:forDisplay:surface:runtime:). The store ignores one whose claim is no longer current, under the same lock as claimRuntime/releaseRuntime, and logs it at debug with the two claim numbers only. So the old runtime's late publish can neither put back a dropped Modal lane nor clear the new runtime's lane on the same React tag. Origins, display sizes and empty-lane drops come from the wrapper's pull, which reads the live view tree and belongs to no runtime, so they are not claim-gated. Addresses PR 48 review thread 4182289966. 🤖 Generated with [Claude Code](https://claude.com/claude-code) * fix(ios): find a Modal host by class and React tag, not viewWithTag: viewWithTag: returns the first view in a window with that tag. A native view with the same small integer ahead of the RCTModalHostViewComponentView made the lookup skip the rest of that window, so the Modal's origin was never read and its lane failed closed over the whole screen. BGSRNTaggedView walks each window breadth-first, bounded per window, and matches a view that is both the host class and has the tag; a wrong-class view with the tag does not end the search. The Fabric registry is not used: a module reaches it only through viewRegistry_DEPRECATED, and the lookup runs in the wrapper's pull, which has no module. The read stays on main, behind the existing weak host cache. Addresses PR 48 review thread 4182289985. 🤖 Generated with [Claude Code](https://claude.com/claude-code) * fix(android): a runtime claim empties the main surface's rectangles Once stale writes are ignored, the old runtime's unmount [] cannot clear the main set, so a new tree that never publishes on the main surface kept masking where the old <BugseeSecure> was until the process died. claimRuntime now empties the main lane's rectangles and keeps its origin, so the new runtime's first publish is the only main set. Stale empty writes stay ignored. Addresses PR 48 review thread 4184201603. 🤖 Generated with [Claude Code](https://claude.com/claude-code) * fix(ios): a runtime claim empties the main surface's rectangles The iOS twin of the Android fix: claimRuntime empties the main lane's rectangles and keeps its origin, so the old runtime's main set cannot outlive it when its clearing write is ignored as stale and the new tree never publishes on the main surface. Stale empty writes stay ignored. Addresses PR 48 review thread 4184201603. 🤖 Generated with [Claude Code](https://claude.com/claude-code) * fix(privacy,ios): place a Modal lane by the current runtime's own host The pull found a Modal's origin by searching the windows for a host with the lane's React tag, and cached the first hit. During a reload Fabric numbers tags from 1 again, so the old runtime's still-presented host can share the tag, and its origin (a sheet inset, say) would place the new runtime's rectangles somewhere else: a leak, not a blackout. The module now names its own runtime's lookup with each Modal publish: its viewRegistry_DEPRECATED (RCTViewRegistry, per runtime), claim-gated in the store like every other write. Fabric mounts the host after JS has published inside it, so the pull asks that lookup on main until it finds an RCTModalHostViewComponentView with the tag, then holds it weakly and reads the origin from it, the shape of Android's watchSurface. A claim forgets every host and lookup; a surface's go with its last lane. Without a registry, or while it finds nothing, the pull falls back to the bounded class+tag walk, which now gives no origin for several matches or for a match that is not presented: the lane stays unknown and is served as the whole display. The weak host cache is gone. Addresses PR 48 review thread 4184201590. 🤖 Generated with [Claude Code](https://claude.com/claude-code) * fix(privacy,ios): a named Modal host that is not found yet fails closed Fabric mounts a Modal's host after the publish that names this runtime's lookup, so the lookup finds nothing for the first pulls of every new Modal. The pull then fell back to the class+tag walk, and during a reload the old runtime's still-presented host can be its only match: for about a second its origin placed the new runtime's rectangles. Once the current runtime has named a host or lookup for a surface (isHostNamedForSurface:), a lookup that finds nothing means the origin is unknown: the lane stays fail-closed, served as the whole display, until the lookup finds this runtime's host. The walk runs only when nothing was ever named, such as without a view registry. Addresses PR 48 review thread 4184201590 (round 2 review, issue 1). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
…om the screen's origin JS measures with `measureInWindow`, in the window's points. The iOS SDK that composes every window of the app on the screen (bugsee-cocoa #170-#172, #186) draws secure rectangles and places native view-tree nodes in screen points, so in a window away from the screen's origin (iPad Stage Manager, the right-hand side of Split View, iPhone Duo side by side) a rectangle moved only by the window's `frame.origin` lands off the view it covers, and the view is recorded in the clear. Every lane now takes the window's place on its screen, read the way the SDK places windows (through the screen's fixed space): the main surface, each <Modal> lane from #48 (the presented view's origin in its window plus that place), and the `vh` origin. On a Mac the SDK records the key window's scene alone, and the place stays `frame.origin`. The key window and the windows searched follow the SDK's current pick: the application's key window while its scene is in the foreground, and every foreground scene's windows on that screen when there are several. The SDK pulls the rectangles once per captured frame, and the main surface's origin is re-read on every pull. BGSRNReactRootOriginTracker keeps the React root found last, weakly, and reads its window's place; only without one does a pull search, at most once per 0.5 s (a brownfield app's native screens have no root). A JS publish on the main surface and a `vh` request search at once. Needs the iOS SDK release with that composition; ships with the pin bump. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
282233b to
02499fa
Compare
There was a problem hiding this comment.
Stale comment
Deep code review
Re-review of 02499fa (one-commit rebuild on
mainafter #48). Prior heads were 282233b and 4af4f55. The tracker, window placement, and Modal term are carefully built against #48’s lanes. The merge gate from the last two reviews is still open.Prior findings
P2 — fully addressed (still). #48 records
originKnownand serves the whole display until an origin is set, and the SDK pull re-reads every origin before it snapshots. The earlier99012d7publish-on-main ordering was dropped on purpose; that does not reintroduce unmoved rectangles. A firstsetSecureRectangleson the TurboModule queue is fail-closed until the main-thread hop (or the next pull) records the place.P1 High — not addressed.
BGSRNSdkComposesScreen()is still “not Catalyst / not iOS-on-Mac”. The pin is still 7.0.0-beta3. Beta3 records the key window’s scene and draws rectangles / native nodes in scene points (frame.origin). Adding the screen origin inverts the Stage Manager / Split View bug this PR exists to fix: masks and the React tree sit one window-offset away from the content, so secret UI is recorded in the clear. iPhone (origin {0,0}) is unchanged; simulator e2e cannot tell the origins apart. The PR body still names 7.0.0-beta4 (cocoa #170–#172 and #186) as the gate and keeps the PR in draft until that pin lands in this change.No new P0–P3 on the tracker, Modal window term, key-window pick, or pull path.
Overall risk: High
High because the remaining P1 is a privacy defect on the currently declared SDK, in the exact iPad configuration the PR is meant to fix. Against the unreleased compositor SDK the design looks right.
Merge recommendation
Do not merge onto main while
native-versions.json/ios/Support/Package.swiftstill pin 7.0.0-beta3. Keep the draft until the compositor SDK is pinned in this PR, as the description already states. I did not run the Support XCTests or e2e on this Linux agent. CI on this SHA already reportedios unit (BugseeRNSupport),ios (cocoapods), andios (spm)SUCCESS — those jobs resolve beta3, so green CI does not close P1.Most important to fix
- Tie screen-space origin (and the multi-scene window walk) to the SDK that actually composites, and bump the pin in the same change.
- Still missing on this SHA: managed view-tree and
secure-component/secure-modalon a device window that is not at the screen origin (called out in the test plan).What looks solid
- Weak cached React root; a pull only searches when there is none, at most once per 0.5 s (brownfield-safe). A JS publish on the main surface and a
vhrequest search at once.- Every pull re-reads the cached root’s place, so a Stage Manager drag is followed with no extra search.
- Modal origin is the presented view in its window plus
BGSRNWindowRecordedOrigin, and a window with no screen is no origin (fail closed) rather thanframe.origin.- #48’s host naming / unique-tag walk is unchanged; tests now pass the window term explicitly because the test runner’s windows are on no screen.
Sent by Cursor Automation: Bugsee code review
There was a problem hiding this comment.
Stale comment
Deep code review
Re-review of 02499fa (ready-for-review; prior reviews were of 282233b / 4af4f55, then this same rebuild). The tracker, fail-closed store, pull-time origin refresh, and Modal window-placement term are carefully built against the compositor SDK. The merge gate this PR stated is not met.
Prior findings
P2 — fully addressed.
setSecureRectanglesstill writes coordinates on the TurboModule queue and hops to main for the origin. That is safe here because #48’soriginKnownserves the whole display until an origin is recorded, andsecureRectanglesForDisplay:re-reads every origin on the SDK pull before it snapshots. The dropped 99012d7 publish-on-main order does not re-open a leak of unmoved rectangles.P1 High — not addressed.
BGSRNSdkComposesScreen()is still “not Catalyst / not iOS-on-Mac”. The pin is still 7.0.0-beta3. Beta3 records the key window’s scene and draws rectangles / native nodes in scene points (frame.origin). Adding the screen origin inverts the Stage Manager / Split View bug this PR exists to fix: masks and the React tree sit one window-offset away from the content, so secret UI is recorded in the clear. Marking the PR ready does not change what a consumer of this branch resolves.bugsee/spmstill has no7.0.0-beta4tag.No new P0–P3 on the tracker (weak root, 0.5 s pull-search cap, immediate search on publish/
vh), the ModalwindowOriginterm, or window lookup. I did not run the Support XCTests or e2e on this Linux agent. CI on this head:ios unit (BugseeRNSupport),ios (cocoapods),ios (spm), andios e2e (simulator)are green — those delivery jobs resolve beta3, and the simulator cannot distinguish scene vs screen origin.Overall risk: High
High because the remaining P1 is a privacy defect on the currently declared SDK, in the exact iPad configuration the PR is meant to fix. Against the unreleased compositor SDK the design looks right.
Merge recommendation
Do not merge onto main while
native-versions.json/ios/Support/Package.swiftstill pin 7.0.0-beta3. Return the PR to draft until the compositor SDK is pinned in the same change, or gateBGSRNWindowRecordedOrigin/ the multi-scene walk so beta3 keepsframe.origin.Most important to fix
- Tie screen-space origin (and the composed window walk) to the SDK that actually composites, and bump the pin in the same change.
- Still missing from the test plan on this SHA: iPad Stage Manager
secure-component/secure-modal/ managed view-tree against that SDK.What looks solid
- Pull refreshes origins on main before snapshot; unknown origin fails closed over the display rather than serving window-space rectangles.
- Weak cached React root, no per-frame search while it stays in a window, brownfield-safe 0.5 s cap, immediate search on JS publish and
vh.- Modal origin is presented-content-in-window plus the same window placement as the main surface; a window with no place yields no origin.
- Mac / Catalyst still uses
frame.origin, matching the SDK recording the key window’s scene alone.Sent by Cursor Automation: Bugsee code review
7.0.0-beta4 is the release that composes every window of the app on the screen (bugsee-cocoa #170-#172, #186), which the previous commit places secure rectangles and the view tree for. - Core: native-versions.json, ios/Support/Package.swift and Package.resolved (revision e467f57c, the bugsee/spm tag). - Feedback: both exact SPM pins and the README. An app using both packages through SPM cannot resolve two different exact pins on bugsee/spm. - The design's Goals and the plan's Global Constraints name the live pin (docs-versions check). - e2e secure-component and secure-modal: from beta4 the iOS SDK writes video.aux version 2. A full-screen app's frame is the screen, in points, with no letterbox padding, so the width-fit mapping still holds; the check now asserts that instead of no video.aux at all. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Deep code review
Re-review of 885bcf1 (pin to 7.0.0-beta4 on top of the 02499fa rebuild). Prior heads were 282233b, 4af4f55, then 02499fa still on beta3. The tracker, window placement, and Modal term were already aligned with the compositor SDK; the merge gate is now met in the declared pin.
Prior findings
P2 Medium — fully addressed (still). setSecureRectangles still writes coordinates on the TurboModule queue and hops to main for the origin. That remains safe because #48’s originKnown serves the whole display until an origin is recorded, and secureRectanglesForDisplay: re-reads every origin on the SDK pull before it snapshots.
P1 High — fully addressed. native-versions.json, ios/Support/Package.swift, Package.resolved (revision e467f57c), and the feedback package’s SPM pins all name 7.0.0-beta4. The bugsee/spm and bugsee/feedback-spm tags exist; the xcframework zip at download.bugsee.com answers HTTP 200. BGSRNSdkComposesScreen() is still only “not Catalyst / not iOS-on-Mac”, which is the correct gate for this pin: beta4 is the release that composes every app window onto the screen (cocoa #170–#172 and #186). Adding the window’s place through fixedCoordinateSpace → screen.coordinateSpace now matches what the SDK draws, instead of inverting it.
No new P0–P3 on the tracker, Modal windowOrigin term, key-window pick, or pull path. I did not run the Support XCTests on this Linux host. CI on this SHA already reports ios unit (BugseeRNSupport), ios (cocoapods), and lint, typecheck, unit SUCCESS. ios e2e (simulator) and ios (spm) were still running at review time.
Overall risk: Low
The privacy defect against beta3 is closed. Against the pinned compositor SDK the design matches the cocoa contract (window → fixed space → screen space; nil when the window is on no screen).
Merge recommendation
Safe to merge once ios e2e (simulator) and ios (spm) are green on this SHA. Those jobs are the first delivery path that actually resolve beta4 and assert video.aux v2. Do not treat the earlier green e2e on 02499fa (beta3, no video.aux) as coverage of this pin.
Most important remaining work (not merge blockers for the stated one-window case)
- The test plan’s iPad Stage Manager / Duo
secure-componentandsecure-modalpass is still unchecked on this SHA (the earlier device check used a locally built cocoa SDK). - Simulator e2e cannot tell scene origin from screen origin, because the scene fills the screen.
What looks solid
- Weak cached React root; a pull only searches when it has none, at most once per 0.5 s (brownfield-safe). A JS publish on the main surface and a
vhrequest search at once. Every pull re-reads the cached root’s place, so a Stage Manager drag is followed with no extra search. - Modal origin is the presented view in its window plus
BGSRNWindowRecordedOrigin; a window with no screen is no origin (fail closed) rather thanframe.origin. - Mac / Catalyst still uses
frame.origin, matching the SDK recording the key window’s scene alone. - Fail-closed store: unknown origin still covers the display instead of serving window-space rectangles.
Sent by Cursor Automation: Bugsee code review
iPad check on
|
| phase | expected (window + measureInWindow) |
measured |
|---|---|---|
| mounted | (552.3, 436.9) 180×90 | (552.1, 436.5) 179.4×89.7 |
| scrolled | (552.3, 336.9) 180×90 | (552.1, 336.9) 179.4×89.7 |
| unmounted | no mask | no mask |
Video, last frame of each report. The MOV is a portrait canvas with the content rotated, so the mask is measured relative to the witness:
| phase | expected offset from the witness | measured |
|---|---|---|
| mounted | (40, −183) | (40, −184) |
| scrolled | (40, −283) | (40, −284) |
With the previous frame.origin placement, which is {0, 0} in Stage Manager, the mask would have been at (60, 260) on the screen, outside the window.
secure-modal-sheet (pageSheet)
- Leaks: not a single
#FF00FF(root) or#00FFFF(sheet) pixel in the report screenshot or in any of the 77 video frames. - Witness: the red witness is visible, so the sheet was presented.
- Placement: both masks sit on their views within about 3 pt. The root mask is at (532.7, 296.7) 158×79 for an expected (532, 297) 160×80. The sheet mask is at (596.7, 509.1) 199×101 for an expected (596, 512) 200×100, with the sheet's content starting at about (492, 232).
view-tree
- Both
vhrequests answeredorigin=493,176. On the simulator this is (0, 0). - In the report's
viewtree.json, the managedReactNativeroot is[493, 176, 650, 603], the same as the nativeUIWindowandRCTSurfaceHostingProxyRootView. - The managed
vh-openhost is[523, 476, 150, 60], exactly where the SDK places its nativeRCTViewComponentView(frame[30, 300, 150, 60]in the window).
video.aux on the iPad is {"version":2, "frameW":1180, "frameH":820, "density":2} with no padding.
Not covered: a real iPhone Duo, Split View, and a window dragged during recording. Each run kept the window in one place.
🤖 Generated with Claude Code
…e.lock #30 pinned 7.0.0-beta4. This is what that pin left behind. Verified against the published artifact: bugsee/spm 7.0.0-beta4 -> e467f57c, Bugsee-7.0.0-beta4.zip SHA-256 90a923ec...f6ec87b (= the Package.swift checksum), build stamp bb2f0e7e-12. Headers and module maps are byte-identical to beta3's in the ios-arm64 and simulator slices; every iOS slice is minos 15.0; the simulator slice still has no crash reporter. bugsee/feedback-spm 7.0.0-beta4 (f23afe34) has the same sources as beta3. - Comments that said Bugsee supports iOS below 15 (podspec, the Support manifest, platform-floors.ts and its test) now say what is true: its floor is 15.0 since beta2, re-checked on beta4. - Comments that cite the vendored header (report and feedback appearance) name beta4; the ones describing behaviour beta4 did not change say so (deleteCollectedDataOnDevice, the simulator slice without a crash reporter). - Podfile.lock: a plain `pod install`. The committed lock predated the feedback pod. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
* build(ios): beta4 follow-ups: floor comments, header comments, Podfile.lock #30 pinned 7.0.0-beta4. This is what that pin left behind. Verified against the published artifact: bugsee/spm 7.0.0-beta4 -> e467f57c, Bugsee-7.0.0-beta4.zip SHA-256 90a923ec...f6ec87b (= the Package.swift checksum), build stamp bb2f0e7e-12. Headers and module maps are byte-identical to beta3's in the ios-arm64 and simulator slices; every iOS slice is minos 15.0; the simulator slice still has no crash reporter. bugsee/feedback-spm 7.0.0-beta4 (f23afe34) has the same sources as beta3. - Comments that said Bugsee supports iOS below 15 (podspec, the Support manifest, platform-floors.ts and its test) now say what is true: its floor is 15.0 since beta2, re-checked on beta4. - Comments that cite the vendored header (report and feedback appearance) name beta4; the ones describing behaviour beta4 did not change say so (deleteCollectedDataOnDevice, the simulator slice without a crash reporter). - Podfile.lock: a plain `pod install`. The committed lock predated the feedback pod. 🤖 Generated with [Claude Code](https://claude.com/claude-code) * test(e2e): attributes case 3 is a plain it on iOS now that beta4 seeds reports 7.0.0-beta4 carries bugsee-cocoa#164 (fe857ad97): a new report starts from the global attributes and the user identifier. On the iOS simulator (6FA9B3E8, beta4 bb2f0e7e-12) the `it.failing` case 3 failed with "Failing test passed even though it was supposed to fail"; as a plain `it` all six cases pass. Android already ran it as a plain `it`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) * test(e2e): iOS recovers the stored JS crash on beta4, so those cases are plain it 7.0.0-beta4 claims override_report.plcrash ahead of the live crash (bugsee-cocoa #177, 7bca39c59). On the iPhone XS (KRSFT, iOS 18.7.9, SDK bb2f0e7e-12) the three `itIosDevice.failing` cases failed with "Failing test passed even though it was supposed to fail": - exc-fatal: the relaunch recovered one `crash` bundle, a ReactNativeWebException carrying "E2E fatal <nonce>"; - exc-root: one `crash` bundle, a ReactNativeWebException; - Release gate: the console ended, no RCTFatalException bundle, and exactly one ReactNativeWebException crash. They are plain `it` now. BugseeModule's logUnhandledException comment no longer says the stored report is dropped. 🤖 Generated with [Claude Code](https://claude.com/claude-code) * docs: say iOS recovers the stored JS crash intermittently, not always Review (cursor, #56): the exceptions comments and logUnhandledException's read as if beta4 always recovers override_report.plcrash. On the iPhone XS 2 of 5 relaunches left it unclaimed. The cases stay a plain `it` asserting the correct outcome; they do not relaunch again, because a second launch would hide the SDK miss the device run exists to show. 🤖 Generated with [Claude Code](https://claude.com/claude-code)


Summary
Rebuilt on
mainafter #48:02499fa(the fix; the earlier six commits were on the pre-#48 base) and885bcf1(the pin to 7.0.0-beta4). #48's per-<Modal>lanes stay as they are; this PR changes where every lane's window is placed.measureInWindow, in window points. The iOS SDK that composes every app window on the screen draws secure rectangles in screen points. Every lane now adds the window's place on its screen, read through the screen's fixed space the way the SDK places windows (BGSRNWindowRecordedOrigin). That covers the main surface and each<Modal>lane (the presented view's origin in its window plus that place, where Privacy: secure rectangles and view-tree nodes inside a Modal use the Modal's own origin #48 left the rebase note). On a Mac the SDK records only the key window's scene, so the place staysframe.origin. On an iPhone, or an iPad app filling its screen, the place is {0, 0} and nothing changes.vhorigin is the same window place.BGSRNSdkKeyWindowandBGSRNSdkWalkedWindowsfollow the SDK's current key-window pick and its composed window list.BGSContracts.h), and Privacy: secure rectangles and view-tree nodes inside a Modal use the Modal's own origin #48 re-reads every origin on each pull.BGSRNReactRootOriginTrackerkeeps the React root it found last (weakly) and reads its window's place. A pull searches only when it has no such root, at most once per 0.5 s; on a brownfield app's native screens there is no root to find. A JS publish on the main surface and avhrequest search at once, so the first publish does not wait for a pull.CGPointorigin, outward rounding and saturation.Pins the iOS SDK that composes the app's windows on the screen (bugsee-cocoa #170–#172 and #186): 7.0.0-beta4, published 2026-10-06. With beta3 a window away from the screen's origin would be off the other way, so the fix and the pin ship together.
885bcf1moves the core pins (native-versions.json,ios/Support/Package.swift,Package.resolved→e467f57c) and the feedback package's SPM pins with them; an app using both through SPM cannot resolve two exact pins onbugsee/spm. From beta4 the iOS SDK writesvideo.auxversion 2, so the iOS e2e now checks that its frame is the screen in points with no letterbox padding, instead of checking that there is novideo.aux.Test plan
xcodebuild test -scheme BugseeRNSupport, iPhone 17 Pro, iOS 27.0): 308 tests, 0 failuresvh, the root not kept alive, no guess when the place cannot be readyarn test: 2939 passed, 12 skipped, 0 failed;yarn lintandyarn typecheckclean. The raw-message scanner caught an exception logged with its text; it now logs the class only, like the rest of the package.02499fa(pin still beta3), including e2e and the SPM build885bcf1(beta4):ios e2e (simulator)passes with thevideo.auxv2 check, plus the SPM and CocoaPods buildsios e2e (simulator)on885bcf1)vh) on the same iPad window,view-treescenario: both requests answeredorigin=493,176(the simulator answers 0,0). In the report'sviewtree.json:ReactNativeroot is[493, 176, 650, 603], the same as the nativeUIWindowandRCTSurfaceHostingProxyRootView;vh-openhost is[523, 476, 150, 60], exactly where its nativeRCTViewComponentView(frame[30, 300, 150, 60]in the window) is placed.885bcf1with the released 7.0.0-beta4 (Bugsee IOS SDK ver:7.0.0-beta4 build:bb2f0e7e-12), dead endpoint, bundles pulled from the device. The window sits at (492.3, 176.9) on the 1180×820 screen, measured from the witness:secure-component(report screenshot): the mask is at (552.1, 436.5) 179.4×89.7 for an expected (552.3, 436.9) 180×90 when mounted, and at (552.1, 336.9) for an expected (552.3, 336.9) after the scroll; it is gone after unmount. The oldframe.originplacement would put it at (60, 260), outside the window.secure-component(video, last frame of each report): the mask sits 40 pt across and 184 pt up from the witness, against an expected 40 and 183 (284 against 283 after the scroll).secure-modal-sheet(pageSheet): no#FF00FFor#00FFFFpixel in the screenshot or in any of the 77 video frames. The red witness is visible, so the sheet was up. Both masks sit on their views within about 3 pt.7.0.0-beta4🤖 Generated with Claude Code