[PANA-8059] Add navigation and toolbar visual effect fallbacks#3066
Conversation
There was a problem hiding this comment.
More details
Portal layer resolution successfully implemented with comprehensive edge-case handling and strong test coverage. Portal layers are now properly replaced with their source hierarchies before rendering, eliminating unreliable image snapshot requests. New visual effect types (scroll pockets, automatic capsules) are correctly detected and handled. No behavioral regressions identified.
🤖 Datadog Autotest · Commit d672ede · What is Autotest? · Any feedback? Reach out in #autotest
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d672ede10d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| var source = identifiedSnapshots[portal.sourceReplayID] | ||
| else { | ||
| return resolveAsCompositorSupport(snapshot) |
There was a problem hiding this comment.
Do not drop portals whose source was pruned
When a visible CAPortalLayer points at a source layer that was not captured into this root snapshot (for example, the source is offscreen, in another subtree, or otherwise pruned by CALayerSnapshot before resolution), this lookup fails and the portal is converted to compositor support with no sublayers. That makes Session Replay omit content that is actually visible through the portal; the previous path could still render from sourceLayer, so this needs an on-demand source snapshot or a fallback instead of dropping the portal.
Useful? React with 👍 / 👎.
| offsetAbsoluteFrames( | ||
| x: destinationFrame.minX - sourceOrigin.x, | ||
| y: destinationFrame.minY - sourceOrigin.y | ||
| ) |
There was a problem hiding this comment.
Keep moved portal snapshots at the portal position
When matchesPosition is false and a moved source (or one of its descendants) produces a content snapshot whose dependencies expand renderBounds beyond bounds, ContentSnapshotRequest.resolved recomputes the frame from the live CALayer coordinates whenever renderBounds != bounds. The offset applied here only changes the captured absoluteFrame, so those image wireframes are emitted at the hidden source's original location instead of at the portal; carry the portal offset through request resolution or avoid using live coordinates for moved snapshots.
Useful? React with 👍 / 👎.
| var result = snapshot | ||
| result.observation = .init(semantics: .layer) | ||
| result.sublayers = [source] |
There was a problem hiding this comment.
Preserve portal privacy when reparenting source layers
When a portal is under a view with stricter Session Replay privacy than the source layer, this attaches the source snapshot with its original textAndInputPrivacyLevel / imagePrivacyLevel. Downstream redaction reads those fields from each emitted source snapshot, so content shown through a portal inside a masked subtree can be sent unmasked; the previous portal image request used the portal snapshot's resolved privacy, so apply the portal privacy to the cloned source subtree before attaching it.
Useful? React with 👍 / 👎.
|
|
||
| var result = snapshot | ||
| result.observation = .init(semantics: .layer) | ||
| result.sublayers = [source] |
There was a problem hiding this comment.
Clip reparented portal content to the portal bounds
When a supported portal has masksToBounds == false and sourceRect is only a slice of a larger source, attaching the entire source hierarchy here lets content outside the portal rectangle render in replay. A portal displays the selected source rect inside its own bounds; after replacing that with sublayers, the resolver needs to force an equivalent clip or the hidden source can bleed around navigation/tool bar controls.
Useful? React with 👍 / 👎.
| guard hasLayerSemantics else { | ||
| return false |
There was a problem hiding this comment.
Keep redacting portal text snapshots under maskAll
When text visible through a portal is rendered by a layer the regular recorder does not classify as static text (for example custom-drawn text), maskAll no longer runs OCR redaction because portal image snapshots were removed and this guard now only allows layer-semantic snapshots. The deleted portal branch used to redact the flattened portal bitmap, so this can upload text through CAPortalLayer that was previously masked.
Useful? React with 👍 / 👎.
What and why?
This PR improves how the Core Animation recording pipeline represents navigation bars and toolbars that use iOS 26 visual effects.
These effects cannot be reproduced directly from the captured layer properties, so the pipeline uses stable fallbacks that keep controls visible and preserve the relevant content. This work builds on #3061 and remains behind the internal layer recording feature flag.
How?
Portal layers initially produced image snapshot requests. This did not work reliably because
CAPortalLayerdoes not render useful pixels when captured directly. Flattening its source into one image also loses the source tree's semantics, filters, and redaction context.Instead, the pipeline replaces supported portal layers with the captured source hierarchy and moves that hierarchy to the portal location. The regular composition tree and wireframe logic can then preserve content, filters, and privacy handling.
This PR intentionally approximates platform glass effects rather than reproducing blur and refraction. It resolves portals that hide their source and currently supports one portal per source. Horizontal scroll pockets are also out of scope.
Review checklist
make api-surfacewhen adding new APIs