Skip to content

fix(replay): text layouts with center/end alignment return incorrect masking bounding box#5218

Open
markushi wants to merge 8 commits intomainfrom
markushi/fix/jpc-masking-mismatch-weighted-text
Open

fix(replay): text layouts with center/end alignment return incorrect masking bounding box#5218
markushi wants to merge 8 commits intomainfrom
markushi/fix/jpc-masking-mismatch-weighted-text

Conversation

@markushi
Copy link
Member

@markushi markushi commented Mar 20, 2026

Description

Fix Compose text masking for layouts where text alignment (TextAlign.Center, TextAlign.End) or weighted layouts produce incorrect masking bounding boxes.

Root Cause

Compose's MultiParagraph always uses constraints.maxWidth as the paragraph width, but the Text composable may size its node to a smaller content width. This causes getLineLeft/getLineRight to return positions in the paragraph coordinate system that don't match the node's actual bounds, resulting in masking rects placed at wrong positions (e.g. off-screen for end-aligned weighted text).

Changes

  • Simplified TextLayout interface: replaced getPrimaryHorizontal, getEllipsisCount, getLineVisibleEnd, getLineStart with getLineLeft/getLineRight — directly using the layout's line bounds instead of computing them from character offsets
  • Fixed paragraph/node width mismatch: when paragraph width exceeds the node width, fall back to lineWidth starting from x=0, since text alignment has no visible effect in content-wrapped nodes
  • Removed hasFillModifier workaround: no longer needed since we use getLineLeft/getLineRight directly
  • removed obsolete fill modifier detection

Motivation and Context

The Fill modifier detection was a fragile workaround that didn't cover all cases (e.g. weighted text). The new approach correctly handles all text alignment and sizing scenarios by comparing the paragraph width against the actual node width.

How did you test it?

  • Tested with the Android sample app using various Compose text layouts: blank, fillMaxWidth, center-aligned, end-aligned, weighted, and multiline
  • Existing screenshot snapshot tests pass with updated baselines

Checklist

  • I added GH Issue ID & Linear ID
  • I added tests to verify the changes.
  • No new PII added or SDK only sends newly added PII if sendDefaultPII is enabled.
  • I updated the docs if needed.
  • I updated the wizard if needed.
  • No breaking change or entry added to the changelog.
  • No breaking change for hybrid SDKs or communicated to hybrid SDKs.

@github-actions
Copy link
Contributor

github-actions bot commented Mar 20, 2026

Semver Impact of This PR

🟢 Patch (bug fixes)

📋 Changelog Preview

This is how your changes will appear in the changelog.
Entries from this PR are highlighted with a left border (blockquote style).


New Features ✨

  • (replay) Add beforeErrorSampling callback to Session Replay by romtsn in #5214

Bug Fixes 🐛

  • (replay) Text layouts with center/end alignment return incorrect masking bounding box by markushi in #5218

Internal Changes 🔧

  • (deps) Update Native SDK to v0.13.3 by github-actions in #5215

🤖 This preview updates automatically when you update the PR.

@sentry
Copy link

sentry bot commented Mar 20, 2026

Sentry Build Distribution

App Name App ID Version Configuration Install Page
SDK Size io.sentry.tests.size 8.36.0 (1) release Install Build

@github-actions
Copy link
Contributor

github-actions bot commented Mar 20, 2026

Performance metrics 🚀

  Plain With Sentry Diff
Startup time 337.76 ms 386.96 ms 49.20 ms
Size 0 B 0 B 0 B

Baseline results on branch: main

Startup times

Revision Plain With Sentry Diff
abfcc92 337.38 ms 427.39 ms 90.00 ms
b3d8889 371.69 ms 432.96 ms 61.26 ms
27d7cf8 397.90 ms 498.65 ms 100.75 ms
b750b96 421.25 ms 444.09 ms 22.84 ms
0eaac1e 320.04 ms 369.52 ms 49.48 ms
f634d01 375.06 ms 420.04 ms 44.98 ms
bbc35bb 298.53 ms 372.17 ms 73.64 ms
1564554 323.06 ms 336.68 ms 13.62 ms
f064536 349.86 ms 417.66 ms 67.80 ms
ee747ae 554.98 ms 611.50 ms 56.52 ms

App size

Revision Plain With Sentry Diff
abfcc92 1.58 MiB 2.13 MiB 557.31 KiB
b3d8889 1.58 MiB 2.10 MiB 535.06 KiB
27d7cf8 1.58 MiB 2.12 MiB 549.42 KiB
b750b96 1.58 MiB 2.10 MiB 533.20 KiB
0eaac1e 1.58 MiB 2.19 MiB 619.17 KiB
f634d01 1.58 MiB 2.10 MiB 533.40 KiB
bbc35bb 1.58 MiB 2.12 MiB 553.01 KiB
1564554 1.58 MiB 2.20 MiB 635.33 KiB
f064536 1.58 MiB 2.20 MiB 633.90 KiB
ee747ae 1.58 MiB 2.10 MiB 530.95 KiB

Previous results on branch: markushi/fix/jpc-masking-mismatch-weighted-text

Startup times

Revision Plain With Sentry Diff
4af8342 390.10 ms 494.80 ms 104.69 ms
c20e90d 301.13 ms 349.74 ms 48.61 ms
0f34885 305.96 ms 359.02 ms 53.07 ms

App size

Revision Plain With Sentry Diff
4af8342 0 B 0 B 0 B
c20e90d 0 B 0 B 0 B
0f34885 0 B 0 B 0 B

@markushi markushi changed the title fix(replay): Remove Fill modifier workaround and shortcut single-line text masking fix(replay): text layouts with center/end alignment return incorrect masking bounding box Mar 20, 2026
@markushi markushi marked this pull request as ready for review March 23, 2026 14:31
Copy link
Member

@romtsn romtsn left a comment

Choose a reason for hiding this comment

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

sick!

Comment on lines +202 to +212
layout.getLineLeft(line)
}
}

override fun getLineVisibleEnd(line: Int): Int = layout.getLineVisibleEnd(line)
override fun getLineRight(line: Int): Float {
return if (layout.ellipsizedWidth > 0 && layout.ellipsizedWidth < layout.width) {
layout.ellipsizedWidth.toFloat()
} else {
layout.getLineRight(line)
}
}
Copy link

Choose a reason for hiding this comment

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

Bug: The global ellipsization check in getLineLeft()/getLineRight() breaks masking for non-ellipsized lines in a multiline TextView by incorrectly applying fixed bounds to all lines.
Severity: MEDIUM

Suggested Fix

Modify getLineLeft() and getLineRight() to check for ellipsization on a per-line basis, similar to the previous implementation that used getEllipsisCount(i). This ensures that only the lines that are actually ellipsized receive the fixed bounds, while other lines retain their correct, alignment-based bounds. Add a new test case with a multiline, aligned TextView where only some lines are ellipsized to prevent regressions.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location:
sentry-android-replay/src/main/java/io/sentry/android/replay/util/Views.kt#L198-L212

Potential issue: In `AndroidTextLayout.getLineLeft()` and `getLineRight()`, the code
checks for ellipsization globally using `layout.ellipsizedWidth`. For a multiline
`TextView` with alignment (e.g., center) where only some lines are ellipsized, this new
logic incorrectly applies the fixed ellipsized bounds (e.g., `0f` to
`layout.ellipsizedWidth`) to all lines. This overrides the correct, aligned bounds for
lines that are not ellipsized, leading to incorrect masking rectangles for those lines
during session replay capture. The existing tests do not cover this multiline scenario,
as they only use single-line `TextView`s.

Did we get this right? 👍 / 👎 to inform future reviews.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants