Skip to content

PANA-8613: View traversal and decomposition - #3728

Open
jonathanmos wants to merge 1 commit into
codex/sr-extracted-prerequisitesfrom
jmoskovich/03-sr-new-pipeline
Open

jonathanmos wants to merge 1 commit into
codex/sr-extracted-prerequisitesfrom
jmoskovich/03-sr-new-pipeline

Conversation

@jonathanmos

@jonathanmos jonathanmos commented Aug 19, 2026 •

Copy link
Copy Markdown
Member

What does this PR do?

Adds native View traversal and snapshot production to the composition-based Session Replay pipeline.

  • Captures text and background shapes, with placeholders for hidden views and WebViews.
  • Traverses attached, main-thread-owned windows using an explicit stack, handling child drawing order, clipping and opacity.
  • Reads the hierarchy synchronously in one main-thread pass, with cooperative generation-deadline and cancellation checks.
  • Publishes touch-privacy overrides only after a complete capture, rejecting results when the RUM scope or window membership changes.
  • Refreshes window interception when tracked activity roots attach.

Motivation

Connect the existing capture orchestration to the Android View hierarchy so the composition pipeline can produce native snapshots.

Review checklist (to be filled by reviewers)

  • Feature or bugfix MUST have appropriate tests (unit, integration, e2e)
  • Make sure you discussed the feature or bugfix with the maintaining team in an Issue
  • Make sure each commit and the PR mention the Issue number (cf the CONTRIBUTING doc)

@datadog-official

datadog-official Bot commented Aug 19, 2026 •

Copy link
Copy Markdown

Tests

✅ All CI checks and tests passed. Datadog automation helped this PR pass.

🎉 All green!

🧪 All tests passed
❄️ No new flaky tests detected

🔄 Datadog retried 2 tests - 2 passed on retry View in Datadog

🎯 Code Coverage (details)
• Patch Coverage: 91.09%
• Overall Coverage: 72.55% (+0.32%)

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: ff3645c | Docs | View more details | Give us feedback!

@codecov-commenter

codecov-commenter commented Aug 19, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 77.62040% with 79 lines in your changes missing coverage. Please review.
✅ Project coverage is 73.39%. Comparing base (50400d0) to head (5ce8684).

Files with missing lines Patch % Lines
...ernal/composition/mapper/CapturedTextViewMapper.kt 40.96% 44 Missing and 5 partials ⚠️
...lay/internal/composition/AndroidWindowTraversal.kt 88.79% 2 Missing and 10 partials ⚠️
...nal/composition/AndroidCapturedSnapshotProducer.kt 83.33% 3 Missing and 4 partials ⚠️
...eplay/internal/composition/RumViewScopeProvider.kt 50.00% 5 Missing ⚠️
.../sessionreplay/internal/DefaultRecorderProvider.kt 92.86% 2 Missing and 1 partial ⚠️
...l/composition/mapper/CapturedViewMapperRegistry.kt 87.50% 1 Missing and 1 partial ⚠️
...ion/mapper/CapturedBackgroundShapeStyleResolver.kt 85.71% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@                        Coverage Diff                        @@
##           jmoskovich/02-sr-new-pipeline    #3728      +/-   ##
=================================================================
+ Coverage                          73.35%   73.39%   +0.05%     
=================================================================
  Files                               1021     1032      +11     
  Lines                              37902    38221     +319     
  Branches                            6599     6659      +60     
=================================================================
+ Hits                               27800    28052     +252     
- Misses                              8183     8227      +44     
- Partials                            1919     1942      +23     
Files with missing lines Coverage Δ
...sessionreplay/internal/composition/CapturedTree.kt 100.00% <100.00%> (+9.76%) ⬆️
...nal/composition/mapper/CapturedHiddenViewMapper.kt 100.00% <100.00%> (ø)
...al/composition/mapper/CapturedMapperTypeWrapper.kt 100.00% <100.00%> (ø)
...position/mapper/CapturedViewGroupFallbackMapper.kt 100.00% <100.00%> (ø)
.../internal/composition/mapper/CapturedViewMapper.kt 100.00% <100.00%> (ø)
...ternal/composition/mapper/CapturedWebViewMapper.kt 100.00% <100.00%> (ø)
...ion/mapper/CapturedBackgroundShapeStyleResolver.kt 85.71% <85.71%> (ø)
...l/composition/mapper/CapturedViewMapperRegistry.kt 87.50% <87.50%> (ø)
.../sessionreplay/internal/DefaultRecorderProvider.kt 92.59% <92.86%> (+0.59%) ⬆️
...eplay/internal/composition/RumViewScopeProvider.kt 50.00% <50.00%> (ø)
... and 3 more

... and 37 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@jonathanmos
jonathanmos force-pushed the jmoskovich/02-sr-new-pipeline branch from 1dd5cfc to 50400d0 Compare August 19, 2026 10:38
@jonathanmos
jonathanmos force-pushed the jmoskovich/03-sr-new-pipeline branch from 92ae883 to 5ce8684 Compare August 19, 2026 10:46
@jonathanmos
jonathanmos force-pushed the jmoskovich/02-sr-new-pipeline branch from 50400d0 to 35e15c5 Compare August 27, 2026 12:34
@jonathanmos
jonathanmos force-pushed the jmoskovich/03-sr-new-pipeline branch 2 times, most recently from 6e5525f to e67102f Compare August 27, 2026 14:23
@jonathanmos
jonathanmos force-pushed the jmoskovich/02-sr-new-pipeline branch 4 times, most recently from 06dc1f8 to 783469c Compare September 2, 2026 13:59
@jonathanmos
jonathanmos force-pushed the jmoskovich/03-sr-new-pipeline branch 2 times, most recently from 0168738 to a4083d3 Compare September 6, 2026 18:21
@jonathanmos
jonathanmos force-pushed the jmoskovich/02-sr-new-pipeline branch from 783469c to 902b1be Compare September 6, 2026 20:44
@jonathanmos
jonathanmos force-pushed the jmoskovich/03-sr-new-pipeline branch from a4083d3 to 5287820 Compare September 7, 2026 08:48
@jonathanmos
jonathanmos force-pushed the jmoskovich/02-sr-new-pipeline branch 2 times, most recently from a21f246 to 357bf07 Compare September 7, 2026 11:57
@jonathanmos
jonathanmos force-pushed the jmoskovich/02-sr-new-pipeline branch 4 times, most recently from 294da51 to fd9fa08 Compare September 24, 2026 12:45
@jonathanmos
jonathanmos force-pushed the jmoskovich/03-sr-new-pipeline branch from 5287820 to 5b481bc Compare September 24, 2026 13:07
@jonathanmos
jonathanmos force-pushed the jmoskovich/02-sr-new-pipeline branch from bd69260 to 751e07a Compare September 29, 2026 07:10
Base automatically changed from jmoskovich/02-sr-new-pipeline to feature/sr-new-pipeline September 30, 2026 11:56
@jonathanmos
jonathanmos force-pushed the jmoskovich/03-sr-new-pipeline branch from 883ec03 to b94ea39 Compare October 6, 2026 10:01
@jonathanmos

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-18T19:36:16.223956Z ff3645c New commits
🔒 Security Review ✅ Completed 2026-10-18T19:32:21.936954Z ff3645c New commits

Security findings

Advisory findings (1)

ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b94ea39e26

ℹ️ 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".

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛡️ Codex Security Review · Automatically triggered

Here are some automated security review suggestions for this pull request.

Reviewed commit: b94ea39e26

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

@jonathanmos
jonathanmos marked this pull request as ready for review October 7, 2026 09:52
@jonathanmos
jonathanmos force-pushed the jmoskovich/03-sr-new-pipeline branch from 561fb40 to 96247a0 Compare October 8, 2026 09:53

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 96247a0233

ℹ️ 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".

@jonathanmos
jonathanmos force-pushed the jmoskovich/03-sr-new-pipeline branch from 96247a0 to a27f444 Compare October 8, 2026 10:37

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a27f444b06

ℹ️ 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".

Comment on lines +180 to +182
val inheritedClip = if (item.clipToBounds) item.ancestorClip.intersectWith(bounds) else item.ancestorClip
val clip = view.clipBoundsOnScreen(state.screenDensity)?.let { inheritedClip.intersectWith(it) }
?: inheritedClip

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Apply clipToOutline to the captured subtree

When a view uses clipToOutline = true, such as a rounded card or an oval container, Android clips the view and its descendants to the outline, but this path only incorporates rectangular ancestor and clipBounds constraints. The replay therefore renders child content and backgrounds into corners or regions that are absent on screen. Resolve the view outline into a layer clip modifier, or otherwise fall back when the outline cannot be represented.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

will be addressed in a followup pr

Comment on lines +196 to +198
val orderedWindows = orderedDecorViews.mapNotNull { root ->
trackedByDecorView[root] ?: windowFromDecorView(root)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Intercept touches for unresolved untracked roots

When an untracked root cannot be converted to a Window—notably a PopupWindow root, or a dialog decor when private-field reflection is blocked—it remains in decorViews and is captured visually, but mapNotNull silently omits it from windows. No CompositionWindowTouchCallback is then installed for that root, so every interaction inside the displayed popup or dialog is absent from the replay. Provide a root-level touch fallback for unresolved roots rather than dropping them only from touch interception.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

will be addressed in a followup pr

@jonathanmos jonathanmos added the ready-for-human-review All agentic review feedback was addressed and PR is ready for the human review. label Oct 12, 2026
@jonathanmos
jonathanmos force-pushed the jmoskovich/03-sr-new-pipeline branch from a27f444 to b4e4f19 Compare October 13, 2026 14:33

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b4e4f19040

ℹ️ 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".


collectTouchOverrideArea(view, state)

val bounds = viewBoundsResolver.resolveViewGlobalBounds(view, state.screenDensity).toCaptured()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve scale and rotation in captured layers

For any view with non-default scaleX, scaleY, or rotation—such as during a common property animation—the bounds resolver still returns the untransformed width and height, and the resulting layer records only an opacity modifier. Its text, background, and subtree are therefore replayed as an unscaled, unrotated rectangle even though Android renders a transformed view. Encode the spatial transform on the layer or capture the transformed output instead.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

will be handled in a later pr

@jonathanmos
jonathanmos force-pushed the jmoskovich/03-sr-new-pipeline branch from 18b7912 to 047ce90 Compare October 14, 2026 14:09

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 047ce90f99

ℹ️ 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".

Comment on lines +29 to +31
override fun map(view: View, mappingContext: CapturedMappingContext): CapturedViewMapperResult {
val style = backgroundShapeStyleResolver.resolve(view, internalLogger)
?: return CapturedViewMapperResult.None

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Capture ViewGroup foregrounds above their children

For a ViewGroup with a foreground drawable—such as a FrameLayout using a translucent scrim, selector, or ripple over its contents—the fallback mapper resolves only view.background, so the foreground is absent from the replay. This cannot be fixed by simply adding it to the returned wireframes because traversal appends all mapper output before child layers, while Android draws the foreground after the children. Emit a distinct foreground wireframe from the layer-finishing step so it retains its frontmost paint order.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

will be handled in a later pr

@jonathanmos
jonathanmos force-pushed the jmoskovich/03-sr-new-pipeline branch from 047ce90 to 4a37ef8 Compare October 15, 2026 07:39

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4a37ef8384

ℹ️ 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".

Comment on lines +29 to +31
override fun map(view: View, mappingContext: CapturedMappingContext): CapturedViewMapperResult {
val style = backgroundShapeStyleResolver.resolve(view, internalLogger)
?: return CapturedViewMapperResult.None

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Capture ImageView drawables instead of returning an empty layer

When composition-tree recording is enabled, the registry has dedicated mappers only for WebView and TextView, so a normal ImageView reaches this fallback. The fallback examines only view.background; an ImageView commonly has no background because its visible icon or photo is stored in ImageView.drawable, causing it to return None and emit an empty layer. As a result, standard images and image buttons disappear from replay; add an ImageView mapper or pixel fallback that captures the displayed drawable subject to image privacy.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

will be handled in a later pr

@jonathanmos
jonathanmos force-pushed the jmoskovich/03-sr-new-pipeline branch from 4a37ef8 to dbee73a Compare October 15, 2026 08:58

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

val newWindows = synchronized(lock) { windows.filterNot(wrappedWindows::containsKey) }
newWindows.forEach(::wrap)

P2 Badge Re-wrap callbacks replaced on still-active windows

When another library replaces a tracked window's Window.Callback without delegating to the previously installed callback, subsequent periodic and lifecycle refreshes continue to classify the window as already wrapped solely because it remains a key in wrappedWindows. Touch recording then stays disabled for that window indefinitely, even though it remains active. During intercept, also verify that the installed callback is still the stored wrapper and re-wrap the current callback when it is not.

ℹ️ 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".

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5a6b4cd5d7

ℹ️ 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".

@jonathanmos
jonathanmos force-pushed the jmoskovich/03-sr-new-pipeline branch from 5a6b4cd to 09c4dad Compare October 15, 2026 10:39

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 09c4dadc30

ℹ️ 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".

@jonathanmos
jonathanmos force-pushed the jmoskovich/03-sr-new-pipeline branch from 09c4dad to 0539057 Compare October 15, 2026 11:17

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 05390571af

ℹ️ 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".

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 42859873bf

ℹ️ 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".

wireframes += CapturedWireframe.Text(
identity = mappingContext.identityFactory.textWireframe(mappingContext.ownerIdentity),
bounds = capturedBounds,
text = resolveLayoutText(view),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Render hints for empty text fields

When an empty EditText or TextView displays a hint, Android renders that hint through a separate hint layout while layout.text/text remains empty. Serializing only resolveLayoutText(view) therefore emits an empty text wireframe, so placeholders such as form-field labels disappear from the replay even though the hint color is captured. Select the visible hint when the text is empty.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

will be handled in a later pr

@jonathanmos
jonathanmos force-pushed the jmoskovich/03-sr-new-pipeline branch from 4285987 to 1de9370 Compare October 15, 2026 14:28
@jonathanmos
jonathanmos changed the base branch from feature/sr-new-pipeline to codex/sr-extracted-prerequisites October 15, 2026 14:37
@chatgpt-codex-connector

Copy link
Copy Markdown

💡 Codex Review

val bounds = Rect(location[0], location[1], location[0] + view.width, location[1] + view.height)

P1 Badge Map touch overrides through the view transform

When a HIDE-tagged view is scaled or rotated while global touch privacy is SHOW, getLocationOnScreen() transforms only the local origin, while appending the untransformed width and height does not describe the view's rendered footprint. Touches in the portion outside this rectangle are therefore recorded despite the explicit HIDE override; compute the override from the view's transformed global bounds (conservatively bounding all transformed corners).

ℹ️ 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".

@datadog-official datadog-official Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bits Code Review: FAIL

Enabling composition recording allows unmasked text to reach the upload path despite the configured privacy policy. Occlusion also drops visible content during legacy View animations because their drawing transformations are not considered.

Open Bits AI session

🤖 Bits Code Review · Commit 1de9370

windowSource,
rumContextProvider
->
AndroidCapturedSnapshotProducer(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Gate snapshot publication until text privacy is applied

Enabling composition recording with the default TextAndInputPrivacy.MASK_ALL now captures unmasked TextView and EditText contents. ImmediateCapturedSnapshotProcessor forwards these snapshots unchanged, and DefaultSnapshotCompletionProcessor writes their text to RecordWriter. Although masking is planned for a later workstream, publication is already enabled here, allowing sensitive text to be uploaded despite the configured privacy policy. Apply privacy before publication or keep these snapshots out of the write path until masking exists.

React: 👍 useful · 👎 wrong · 😕 unclear · 👀 right, but not acting on it

What each reaction means
  • 👍 Useful: correct, and you're fixing it or will.
  • 👎 Wrong: the finding is incorrect, for example the bug can't happen or the code was misread.
  • 😕 Unclear: you can't tell what it means or why it matters.
  • 👀 Right, not acting: correct, but intended, out of scope for this PR, a known risk, or too minor.

🤖 Bits Code Review · Open Bits AI session

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

will be handled in a later pr

for (i in childCount - 1 downTo 0) {
val child = children[i]
// A transformed child cannot be represented by its layout rectangle in either direction.
if (hasScaleOrRotation(child)) continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Exclude legacy view animations from occlusion comparisons

A legacy TranslateAnimation or AlphaAnimation changes drawing without updating the View properties checked by this detector. An opaque sibling can therefore be treated as covering its original rectangle while actually moved or translucent, causing visible content beneath it to be omitted from replay. Skip animated children in both directions of the occlusion comparison unless their effective transformation and opacity are known.

Suggested change
if (hasScaleOrRotation(child)) continue
if (child.animation != null || hasScaleOrRotation(child)) continue

React: 👍 useful · 👎 wrong · 😕 unclear · 👀 right, but not acting on it

What each reaction means
  • 👍 Useful: correct, and you're fixing it or will.
  • 👎 Wrong: the finding is incorrect, for example the bug can't happen or the code was misread.
  • 😕 Unclear: you can't tell what it means or why it matters.
  • 👀 Right, not acting: correct, but intended, out of scope for this PR, a known risk, or too minor.

🤖 Bits Code Review · Open Bits AI session

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

will be handled in a later pr

@jonathanmos
jonathanmos force-pushed the codex/sr-extracted-prerequisites branch from a6081e5 to de0128e Compare October 18, 2026 12:02
@jonathanmos
jonathanmos force-pushed the jmoskovich/03-sr-new-pipeline branch from 1de9370 to 978c258 Compare October 18, 2026 16:17

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 978c258201

ℹ️ 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".

Comment on lines +47 to +50
traversal = AndroidWindowTraversal(
mapperRegistry = builtInCapturedMappers(sdkCore.internalLogger, customDrawableMappers),
internalLogger = sdkCore.internalLogger
),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Wire Compose capture into the default traversal

When this pipeline is enabled for a Compose screen, the default factory constructs AndroidWindowTraversal without a composeHostCallback, leaving its default value null. ComposeView is therefore never handed off for semantic decomposition and instead reaches the background-only fallback; because its rendered Compose content is not represented by ordinary native child wireframes, Compose-only screens produce effectively blank replays. Supply the Compose capture callback here or retain a pixel fallback for unmapped Compose hosts.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

handled in a later branch

@jonathanmos
jonathanmos force-pushed the codex/sr-extracted-prerequisites branch from de0128e to 0f3bad7 Compare October 18, 2026 19:00
@jonathanmos
jonathanmos force-pushed the jmoskovich/03-sr-new-pipeline branch from 978c258 to ff3645c Compare October 18, 2026 19:28
* WebView slot). Traversal must not also capture that view's native children independently.
*/
data class Wireframes(
val wireframes: List<CapturedWireframe>,

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.

nit: don't you though of declaring this parameter as vararg CapturedW ireframe? Then for the single only item you wouldn't need to create list of one element?

Or maybe add a secondary constructor to make call site more simple?
Also could be applied to other classes like that

private val drawableToColorMapper: DrawableToColorMapper = DrawableToColorMapper.getDefault()
) {
fun resolve(view: View, internalLogger: InternalLogger): CapturedShapeStyle? {
val color = view.background?.let { drawableToColorMapper.mapDrawableToColor(it, internalLogger) }

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.

nit: internalLogger is only forwarded to drawableToColorMapper.mapDrawableToColor(...), and it is already available at wiring time in DefaultCompositionPipelineFactory.builtInCapturedMappers — could we inject it in the constructor instead of threading it through every resolve(...) call? resolve then loses the parameter, and CapturedTextViewMapper / CapturedViewGroupFallbackMapper can drop their own internalLogger as well, since they only pass it along.

internal class CapturedBackgroundShapeStyleResolver(
    private val internalLogger: InternalLogger,
    private val colorStringFormatter: ColorStringFormatter = DefaultColorStringFormatter,
    private val drawableToColorMapper: DrawableToColorMapper = DrawableToColorMapper.getDefault()
) {
    fun resolve(view: View): CapturedShapeStyle? =
        view.background
            ?.let { drawableToColorMapper.mapDrawableToColor(it, internalLogger) }
            ?.let { color ->
                // View alpha belongs to the owning layer so text and descendants share it exactly once.
                CapturedShapeStyle(
                    backgroundColor = colorStringFormatter.formatColorAsHexString(color)
                )
            }
}

Factory wiring:

val backgroundResolver = CapturedBackgroundShapeStyleResolver(
    internalLogger = internalLogger,
    drawableToColorMapper = DrawableToColorMapper.getDefault(customDrawableMappers)
)

and the call sites become just backgroundShapeStyleResolver.resolve(view).

Typeface.SANS_SERIF -> SANS_SERIF_FAMILY_NAME
Typeface.MONOSPACE -> MONOSPACE_FAMILY_NAME
Typeface.SERIF -> SERIF_FAMILY_NAME
else -> SANS_SERIF_FAMILY_NAME

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.

should we log such case into telemetry for the any update in the future ?

}

private fun resolvePadding(textView: TextView, pixelsDensity: Float): CapturedPadding {
return if (textView.layout != null) {

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.

please add a comment that explains why we 're checking for the layout here

messageBuilder = { "No mapper found for view $viewType" },
throwable = null,
onlyOnce = true,
additionalProperties = mapOf("replay.widget.type" to viewType)

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.

Just in case: I see that log is going to be written only once, but map construction logic would be executed for every call anyway. In case if this code would be executed frequently - it might add an additional pressure to the GC. Consider check such case and maybe move attributes to the message?

}
is WorkItem.VisitTouchPrivacy -> visitTouchPrivacy(item, stack, state)
is WorkItem.Finish -> {
completeLayer(

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.

maybe it's easier to just pass item as a parameter to the method? or make completeLayer an extension for item ?


@UiThread
private fun tryVisit(view: View, expectedParent: ViewParent?, state: TraversalState): Boolean =
!viewUtilsInternal.isNotVisible(view) &&

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.

double negative statement. maybe could be replaced with something like viewUtilsInternal.isVisible(view)?

}

private sealed interface ResultSink {
data class ChildOf(val list: MutableList<CapturedChild>) : ResultSink

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.

having mutable list inside data class is an anti-pattern

private fun <T> ArrayDeque<T>.pop(): T = removeLast()

@Suppress("UnsafeThirdPartyFunctionCall") // pure size check, cannot throw
private fun <T> ArrayDeque<T>.isNotStackEmpty(): Boolean = isNotEmpty()

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.

why isNotEmpty is not enough here?

CapturedMapperTypeWrapper(WebView::class.java, CapturedWebViewMapper()),
CapturedMapperTypeWrapper(
TextView::class.java,
CapturedTextViewMapper(

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.

Not sure that we need dedicated CapturedMapperTypeWrapper here. Maybe it's worth to make supportsView a method of *Mapper?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-human-review All agentic review feedback was addressed and PR is ready for the human review.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants