From 4ef2dc30bfae57c8966ca5f7aa1ef956f0349dae Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Mon, 2 Feb 2026 13:36:39 +0100 Subject: [PATCH 01/11] feat(replay): Track custom masking usage via fake integration --- .../java/io/sentry/SentryReplayOptions.java | 25 +++++-- .../java/io/sentry/SentryReplayOptionsTest.kt | 70 +++++++++++++++++++ 2 files changed, 88 insertions(+), 7 deletions(-) diff --git a/sentry/src/main/java/io/sentry/SentryReplayOptions.java b/sentry/src/main/java/io/sentry/SentryReplayOptions.java index 367b150c92..638c25e9d0 100644 --- a/sentry/src/main/java/io/sentry/SentryReplayOptions.java +++ b/sentry/src/main/java/io/sentry/SentryReplayOptions.java @@ -1,5 +1,7 @@ package io.sentry; +import static io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion; + import io.sentry.protocol.SdkVersion; import io.sentry.util.SampleRateUtils; import java.util.ArrayList; @@ -16,6 +18,8 @@ public final class SentryReplayOptions { + private static final String CUSTOM_MASKING_INTEGRATION_NAME = "ReplayCustomMasking"; + public static final String TEXT_VIEW_CLASS_NAME = "android.widget.TextView"; public static final String IMAGE_VIEW_CLASS_NAME = "android.widget.ImageView"; public static final String WEB_VIEW_CLASS_NAME = "android.webkit.WebView"; @@ -209,8 +213,9 @@ public enum SentryReplayQuality { public SentryReplayOptions(final boolean empty, final @Nullable SdkVersion sdkVersion) { if (!empty) { - setMaskAllText(true); - setMaskAllImages(true); + // Add default mask classes directly without setting usingCustomMasking flag + maskViewClasses.add(TEXT_VIEW_CLASS_NAME); + maskViewClasses.add(IMAGE_VIEW_CLASS_NAME); maskViewClasses.add(WEB_VIEW_CLASS_NAME); maskViewClasses.add(VIDEO_VIEW_CLASS_NAME); maskViewClasses.add(ANDROIDX_MEDIA_VIEW_CLASS_NAME); @@ -275,11 +280,12 @@ public void setSessionSampleRate(final @Nullable Double sessionSampleRate) { *

Default is enabled. */ public void setMaskAllText(final boolean maskAllText) { + addIntegrationToSdkVersion(CUSTOM_MASKING_INTEGRATION_NAME); if (maskAllText) { - addMaskViewClass(TEXT_VIEW_CLASS_NAME); + maskViewClasses.add(TEXT_VIEW_CLASS_NAME); unmaskViewClasses.remove(TEXT_VIEW_CLASS_NAME); } else { - addUnmaskViewClass(TEXT_VIEW_CLASS_NAME); + unmaskViewClasses.add(TEXT_VIEW_CLASS_NAME); maskViewClasses.remove(TEXT_VIEW_CLASS_NAME); } } @@ -293,11 +299,12 @@ public void setMaskAllText(final boolean maskAllText) { *

Default is enabled. */ public void setMaskAllImages(final boolean maskAllImages) { + addIntegrationToSdkVersion(CUSTOM_MASKING_INTEGRATION_NAME); if (maskAllImages) { - addMaskViewClass(IMAGE_VIEW_CLASS_NAME); + maskViewClasses.add(IMAGE_VIEW_CLASS_NAME); unmaskViewClasses.remove(IMAGE_VIEW_CLASS_NAME); } else { - addUnmaskViewClass(IMAGE_VIEW_CLASS_NAME); + unmaskViewClasses.add(IMAGE_VIEW_CLASS_NAME); maskViewClasses.remove(IMAGE_VIEW_CLASS_NAME); } } @@ -308,6 +315,7 @@ public Set getMaskViewClasses() { } public void addMaskViewClass(final @NotNull String className) { + addIntegrationToSdkVersion(CUSTOM_MASKING_INTEGRATION_NAME); this.maskViewClasses.add(className); } @@ -317,6 +325,7 @@ public Set getUnmaskViewClasses() { } public void addUnmaskViewClass(final @NotNull String className) { + addIntegrationToSdkVersion(CUSTOM_MASKING_INTEGRATION_NAME); this.unmaskViewClasses.add(className); } @@ -351,12 +360,14 @@ public long getSessionDuration() { @ApiStatus.Internal public void setMaskViewContainerClass(@NotNull String containerClass) { - addMaskViewClass(containerClass); + addIntegrationToSdkVersion(CUSTOM_MASKING_INTEGRATION_NAME); + maskViewClasses.add(containerClass); maskViewContainerClass = containerClass; } @ApiStatus.Internal public void setUnmaskViewContainerClass(@NotNull String containerClass) { + addIntegrationToSdkVersion(CUSTOM_MASKING_INTEGRATION_NAME); unmaskViewContainerClass = containerClass; } diff --git a/sentry/src/test/java/io/sentry/SentryReplayOptionsTest.kt b/sentry/src/test/java/io/sentry/SentryReplayOptionsTest.kt index cf96bd5d7d..c68bf4ac21 100644 --- a/sentry/src/test/java/io/sentry/SentryReplayOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/SentryReplayOptionsTest.kt @@ -1,10 +1,18 @@ package io.sentry +import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFalse import kotlin.test.assertTrue class SentryReplayOptionsTest { + + @BeforeTest + fun setup() { + SentryIntegrationPackageStorage.getInstance().clearStorage() + } + @Test fun `uses medium quality as default`() { val replayOptions = SentryReplayOptions(true, null) @@ -126,4 +134,66 @@ class SentryReplayOptionsTest { assertTrue(headers.contains("X-Response-Header")) assertTrue(headers.contains("X-Debug-Header")) } + + // Custom Masking Integration Tests + + private fun hasCustomMaskingIntegration(): Boolean { + return SentryIntegrationPackageStorage.getInstance() + .integrations + .contains("ReplayCustomMasking") + } + + @Test + fun `default options does not add ReplayCustomMasking integration`() { + SentryReplayOptions(false, null) + assertFalse(hasCustomMaskingIntegration()) + } + + @Test + fun `empty options does not add ReplayCustomMasking integration`() { + SentryReplayOptions(true, null) + assertFalse(hasCustomMaskingIntegration()) + } + + @Test + fun `addUnmaskViewClass adds ReplayCustomMasking integration`() { + val options = SentryReplayOptions(false, null) + options.addUnmaskViewClass("com.example.MyTextView") + assertTrue(hasCustomMaskingIntegration()) + } + + @Test + fun `setMaskViewContainerClass adds ReplayCustomMasking integration`() { + val options = SentryReplayOptions(false, null) + options.setMaskViewContainerClass("com.example.MyContainer") + assertTrue(hasCustomMaskingIntegration()) + } + + @Test + fun `setUnmaskViewContainerClass adds ReplayCustomMasking integration`() { + val options = SentryReplayOptions(false, null) + options.setUnmaskViewContainerClass("com.example.MyContainer") + assertTrue(hasCustomMaskingIntegration()) + } + + @Test + fun `addMaskViewClass adds ReplayCustomMasking integration`() { + val options = SentryReplayOptions(false, null) + options.addMaskViewClass("com.example.MySensitiveView") + assertTrue(hasCustomMaskingIntegration()) + } + + @Test + fun `setMaskAllText adds ReplayCustomMasking integration`() { + val options = SentryReplayOptions(false, null) + options.setMaskAllText(false) + assertTrue(hasCustomMaskingIntegration()) + } + + @Test + fun `setMaskAllImages adds ReplayCustomMasking integration`() { + val options = SentryReplayOptions(false, null) + options.setMaskAllImages(false) + assertTrue(hasCustomMaskingIntegration()) + } } From d515e2fdc919280879b14e115fe680b35021693c Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Mon, 2 Feb 2026 13:38:49 +0100 Subject: [PATCH 02/11] chore(changelog): Add PR #5070 to changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 72431094bc..a097a1ebb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ ### Internal +- Add integration to track session replay custom masking ([#5070](https://github.com/getsentry/sentry-java/pull/5070)) - Establish new native exception mechanisms to differentiate events generated by `sentry-native` from `ApplicationExitInfo`. ([#5052](https://github.com/getsentry/sentry-java/pull/5052)) - Set `write` permission for `statuses` in the changelog preview GHA workflow. ([#5053](https://github.com/getsentry/sentry-java/pull/5053)) From 936a8b4f65b14f0d9f7b38b40362c6a2363b431a Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Tue, 10 Feb 2026 08:41:28 +0100 Subject: [PATCH 03/11] fix(replay): Address PR feedback for custom masking tracking Remove tracking from container class setters (RN sets them unconditionally) and add tracking when custom view tags or Compose privacy modifiers are encountered. Uses a volatile flag to avoid repeated integration additions per view. Co-Authored-By: Claude Opus 4.6 --- .../viewhierarchy/ComposeViewHierarchyNode.kt | 2 ++ .../replay/viewhierarchy/ViewHierarchyNode.kt | 3 +++ .../java/io/sentry/SentryReplayOptions.java | 17 +++++++++++++-- .../java/io/sentry/SentryReplayOptionsTest.kt | 21 ++++++++++++++++--- 4 files changed, 38 insertions(+), 5 deletions(-) diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ComposeViewHierarchyNode.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ComposeViewHierarchyNode.kt index cbcadc0391..c2c6b2ea4d 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ComposeViewHierarchyNode.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ComposeViewHierarchyNode.kt @@ -85,10 +85,12 @@ internal object ComposeViewHierarchyNode { ): Boolean { val sentryPrivacyModifier = this?.getOrNull(SentryReplayModifiers.SentryPrivacy) if (sentryPrivacyModifier == "unmask") { + SentryReplayOptions.trackCustomMaskingTag() return false } if (sentryPrivacyModifier == "mask") { + SentryReplayOptions.trackCustomMaskingTag() return true } diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ViewHierarchyNode.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ViewHierarchyNode.kt index 2cde60f4fb..a6c3a10052 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ViewHierarchyNode.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ViewHierarchyNode.kt @@ -8,6 +8,7 @@ import android.view.ViewParent import android.widget.ImageView import android.widget.TextView import io.sentry.SentryOptions +import io.sentry.SentryReplayOptions import io.sentry.android.replay.R import io.sentry.android.replay.util.AndroidTextLayout import io.sentry.android.replay.util.TextLayout @@ -291,6 +292,7 @@ internal sealed class ViewHierarchyNode( (tag as? String)?.lowercase()?.contains(SENTRY_UNMASK_TAG) == true || getTag(R.id.sentry_privacy) == "unmask" ) { + SentryReplayOptions.trackCustomMaskingTag() return false } @@ -298,6 +300,7 @@ internal sealed class ViewHierarchyNode( (tag as? String)?.lowercase()?.contains(SENTRY_MASK_TAG) == true || getTag(R.id.sentry_privacy) == "mask" ) { + SentryReplayOptions.trackCustomMaskingTag() return true } diff --git a/sentry/src/main/java/io/sentry/SentryReplayOptions.java b/sentry/src/main/java/io/sentry/SentryReplayOptions.java index 638c25e9d0..94d60b8926 100644 --- a/sentry/src/main/java/io/sentry/SentryReplayOptions.java +++ b/sentry/src/main/java/io/sentry/SentryReplayOptions.java @@ -15,10 +15,12 @@ import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.TestOnly; public final class SentryReplayOptions { private static final String CUSTOM_MASKING_INTEGRATION_NAME = "ReplayCustomMasking"; + private static volatile boolean customMaskingTagTracked = false; public static final String TEXT_VIEW_CLASS_NAME = "android.widget.TextView"; public static final String IMAGE_VIEW_CLASS_NAME = "android.widget.ImageView"; @@ -360,14 +362,12 @@ public long getSessionDuration() { @ApiStatus.Internal public void setMaskViewContainerClass(@NotNull String containerClass) { - addIntegrationToSdkVersion(CUSTOM_MASKING_INTEGRATION_NAME); maskViewClasses.add(containerClass); maskViewContainerClass = containerClass; } @ApiStatus.Internal public void setUnmaskViewContainerClass(@NotNull String containerClass) { - addIntegrationToSdkVersion(CUSTOM_MASKING_INTEGRATION_NAME); unmaskViewContainerClass = containerClass; } @@ -381,6 +381,19 @@ public void setUnmaskViewContainerClass(@NotNull String containerClass) { return unmaskViewContainerClass; } + @ApiStatus.Internal + public static void trackCustomMaskingTag() { + if (!customMaskingTagTracked) { + customMaskingTagTracked = true; + addIntegrationToSdkVersion(CUSTOM_MASKING_INTEGRATION_NAME); + } + } + + @TestOnly + public static void resetCustomMaskingTagTracked() { + customMaskingTagTracked = false; + } + @ApiStatus.Internal public boolean isTrackConfiguration() { return trackConfiguration; diff --git a/sentry/src/test/java/io/sentry/SentryReplayOptionsTest.kt b/sentry/src/test/java/io/sentry/SentryReplayOptionsTest.kt index c68bf4ac21..ba2be51571 100644 --- a/sentry/src/test/java/io/sentry/SentryReplayOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/SentryReplayOptionsTest.kt @@ -11,6 +11,7 @@ class SentryReplayOptionsTest { @BeforeTest fun setup() { SentryIntegrationPackageStorage.getInstance().clearStorage() + SentryReplayOptions.resetCustomMaskingTagTracked() } @Test @@ -163,17 +164,31 @@ class SentryReplayOptionsTest { } @Test - fun `setMaskViewContainerClass adds ReplayCustomMasking integration`() { + fun `setMaskViewContainerClass does not add ReplayCustomMasking integration`() { val options = SentryReplayOptions(false, null) options.setMaskViewContainerClass("com.example.MyContainer") - assertTrue(hasCustomMaskingIntegration()) + assertFalse(hasCustomMaskingIntegration()) } @Test - fun `setUnmaskViewContainerClass adds ReplayCustomMasking integration`() { + fun `setUnmaskViewContainerClass does not add ReplayCustomMasking integration`() { val options = SentryReplayOptions(false, null) options.setUnmaskViewContainerClass("com.example.MyContainer") + assertFalse(hasCustomMaskingIntegration()) + } + + @Test + fun `trackCustomMaskingTag adds ReplayCustomMasking integration`() { + SentryReplayOptions.trackCustomMaskingTag() + assertTrue(hasCustomMaskingIntegration()) + } + + @Test + fun `trackCustomMaskingTag only adds integration once`() { + SentryReplayOptions.trackCustomMaskingTag() + SentryReplayOptions.trackCustomMaskingTag() assertTrue(hasCustomMaskingIntegration()) + assertEquals(1, SentryIntegrationPackageStorage.getInstance().integrations.count { it == "ReplayCustomMasking" }) } @Test From 4c170162dd542955f55386e5710aaf52dc53e537 Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Tue, 10 Feb 2026 08:46:09 +0100 Subject: [PATCH 04/11] fix(replay): Use trackCustomMaskingTag() consistently in all masking methods Replace direct addIntegrationToSdkVersion calls with trackCustomMaskingTag() in setMaskAllText, setMaskAllImages, addMaskViewClass, and addUnmaskViewClass so all entry points benefit from the volatile flag optimization. Co-Authored-By: Claude Opus 4.6 --- sentry/src/main/java/io/sentry/SentryReplayOptions.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sentry/src/main/java/io/sentry/SentryReplayOptions.java b/sentry/src/main/java/io/sentry/SentryReplayOptions.java index 94d60b8926..f393371585 100644 --- a/sentry/src/main/java/io/sentry/SentryReplayOptions.java +++ b/sentry/src/main/java/io/sentry/SentryReplayOptions.java @@ -282,7 +282,7 @@ public void setSessionSampleRate(final @Nullable Double sessionSampleRate) { *

Default is enabled. */ public void setMaskAllText(final boolean maskAllText) { - addIntegrationToSdkVersion(CUSTOM_MASKING_INTEGRATION_NAME); + trackCustomMaskingTag(); if (maskAllText) { maskViewClasses.add(TEXT_VIEW_CLASS_NAME); unmaskViewClasses.remove(TEXT_VIEW_CLASS_NAME); @@ -301,7 +301,7 @@ public void setMaskAllText(final boolean maskAllText) { *

Default is enabled. */ public void setMaskAllImages(final boolean maskAllImages) { - addIntegrationToSdkVersion(CUSTOM_MASKING_INTEGRATION_NAME); + trackCustomMaskingTag(); if (maskAllImages) { maskViewClasses.add(IMAGE_VIEW_CLASS_NAME); unmaskViewClasses.remove(IMAGE_VIEW_CLASS_NAME); @@ -317,7 +317,7 @@ public Set getMaskViewClasses() { } public void addMaskViewClass(final @NotNull String className) { - addIntegrationToSdkVersion(CUSTOM_MASKING_INTEGRATION_NAME); + trackCustomMaskingTag(); this.maskViewClasses.add(className); } @@ -327,7 +327,7 @@ public Set getUnmaskViewClasses() { } public void addUnmaskViewClass(final @NotNull String className) { - addIntegrationToSdkVersion(CUSTOM_MASKING_INTEGRATION_NAME); + trackCustomMaskingTag(); this.unmaskViewClasses.add(className); } From 89dd12249cea7d40ebf90ffd145de1457afbcf78 Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Tue, 10 Feb 2026 08:50:40 +0100 Subject: [PATCH 05/11] refactor(replay): Rename custom masking tracking methods - customMaskingTagTracked -> customMaskingTracked - trackCustomMaskingTag() -> trackCustomMasking() - resetCustomMaskingTagTracked() -> resetCustomMaskingTracked() Co-Authored-By: Claude Opus 4.6 --- .../viewhierarchy/ComposeViewHierarchyNode.kt | 4 ++-- .../replay/viewhierarchy/ViewHierarchyNode.kt | 4 ++-- .../java/io/sentry/SentryReplayOptions.java | 20 +++++++++---------- .../java/io/sentry/SentryReplayOptionsTest.kt | 12 +++++------ 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ComposeViewHierarchyNode.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ComposeViewHierarchyNode.kt index c2c6b2ea4d..dcef27d523 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ComposeViewHierarchyNode.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ComposeViewHierarchyNode.kt @@ -85,12 +85,12 @@ internal object ComposeViewHierarchyNode { ): Boolean { val sentryPrivacyModifier = this?.getOrNull(SentryReplayModifiers.SentryPrivacy) if (sentryPrivacyModifier == "unmask") { - SentryReplayOptions.trackCustomMaskingTag() + SentryReplayOptions.trackCustomMasking() return false } if (sentryPrivacyModifier == "mask") { - SentryReplayOptions.trackCustomMaskingTag() + SentryReplayOptions.trackCustomMasking() return true } diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ViewHierarchyNode.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ViewHierarchyNode.kt index a6c3a10052..08add79d62 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ViewHierarchyNode.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ViewHierarchyNode.kt @@ -292,7 +292,7 @@ internal sealed class ViewHierarchyNode( (tag as? String)?.lowercase()?.contains(SENTRY_UNMASK_TAG) == true || getTag(R.id.sentry_privacy) == "unmask" ) { - SentryReplayOptions.trackCustomMaskingTag() + SentryReplayOptions.trackCustomMasking() return false } @@ -300,7 +300,7 @@ internal sealed class ViewHierarchyNode( (tag as? String)?.lowercase()?.contains(SENTRY_MASK_TAG) == true || getTag(R.id.sentry_privacy) == "mask" ) { - SentryReplayOptions.trackCustomMaskingTag() + SentryReplayOptions.trackCustomMasking() return true } diff --git a/sentry/src/main/java/io/sentry/SentryReplayOptions.java b/sentry/src/main/java/io/sentry/SentryReplayOptions.java index f393371585..c4e112f8b2 100644 --- a/sentry/src/main/java/io/sentry/SentryReplayOptions.java +++ b/sentry/src/main/java/io/sentry/SentryReplayOptions.java @@ -20,7 +20,7 @@ public final class SentryReplayOptions { private static final String CUSTOM_MASKING_INTEGRATION_NAME = "ReplayCustomMasking"; - private static volatile boolean customMaskingTagTracked = false; + private static volatile boolean customMaskingTracked = false; public static final String TEXT_VIEW_CLASS_NAME = "android.widget.TextView"; public static final String IMAGE_VIEW_CLASS_NAME = "android.widget.ImageView"; @@ -282,7 +282,7 @@ public void setSessionSampleRate(final @Nullable Double sessionSampleRate) { *

Default is enabled. */ public void setMaskAllText(final boolean maskAllText) { - trackCustomMaskingTag(); + trackCustomMasking(); if (maskAllText) { maskViewClasses.add(TEXT_VIEW_CLASS_NAME); unmaskViewClasses.remove(TEXT_VIEW_CLASS_NAME); @@ -301,7 +301,7 @@ public void setMaskAllText(final boolean maskAllText) { *

Default is enabled. */ public void setMaskAllImages(final boolean maskAllImages) { - trackCustomMaskingTag(); + trackCustomMasking(); if (maskAllImages) { maskViewClasses.add(IMAGE_VIEW_CLASS_NAME); unmaskViewClasses.remove(IMAGE_VIEW_CLASS_NAME); @@ -317,7 +317,7 @@ public Set getMaskViewClasses() { } public void addMaskViewClass(final @NotNull String className) { - trackCustomMaskingTag(); + trackCustomMasking(); this.maskViewClasses.add(className); } @@ -327,7 +327,7 @@ public Set getUnmaskViewClasses() { } public void addUnmaskViewClass(final @NotNull String className) { - trackCustomMaskingTag(); + trackCustomMasking(); this.unmaskViewClasses.add(className); } @@ -382,16 +382,16 @@ public void setUnmaskViewContainerClass(@NotNull String containerClass) { } @ApiStatus.Internal - public static void trackCustomMaskingTag() { - if (!customMaskingTagTracked) { - customMaskingTagTracked = true; + public static void trackCustomMasking() { + if (!customMaskingTracked) { + customMaskingTracked = true; addIntegrationToSdkVersion(CUSTOM_MASKING_INTEGRATION_NAME); } } @TestOnly - public static void resetCustomMaskingTagTracked() { - customMaskingTagTracked = false; + public static void resetCustomMaskingTracked() { + customMaskingTracked = false; } @ApiStatus.Internal diff --git a/sentry/src/test/java/io/sentry/SentryReplayOptionsTest.kt b/sentry/src/test/java/io/sentry/SentryReplayOptionsTest.kt index ba2be51571..e1391d0b40 100644 --- a/sentry/src/test/java/io/sentry/SentryReplayOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/SentryReplayOptionsTest.kt @@ -11,7 +11,7 @@ class SentryReplayOptionsTest { @BeforeTest fun setup() { SentryIntegrationPackageStorage.getInstance().clearStorage() - SentryReplayOptions.resetCustomMaskingTagTracked() + SentryReplayOptions.resetCustomMaskingTracked() } @Test @@ -178,15 +178,15 @@ class SentryReplayOptionsTest { } @Test - fun `trackCustomMaskingTag adds ReplayCustomMasking integration`() { - SentryReplayOptions.trackCustomMaskingTag() + fun `trackCustomMasking adds ReplayCustomMasking integration`() { + SentryReplayOptions.trackCustomMasking() assertTrue(hasCustomMaskingIntegration()) } @Test - fun `trackCustomMaskingTag only adds integration once`() { - SentryReplayOptions.trackCustomMaskingTag() - SentryReplayOptions.trackCustomMaskingTag() + fun `trackCustomMasking only adds integration once`() { + SentryReplayOptions.trackCustomMasking() + SentryReplayOptions.trackCustomMasking() assertTrue(hasCustomMaskingIntegration()) assertEquals(1, SentryIntegrationPackageStorage.getInstance().integrations.count { it == "ReplayCustomMasking" }) } From cc57cc509266aa4c8592e943466912496cac6083 Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Tue, 10 Feb 2026 08:52:48 +0100 Subject: [PATCH 06/11] chore(changelog): Update PR reference to #5088 Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a097a1ebb4..33deafe6e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,7 @@ ### Internal -- Add integration to track session replay custom masking ([#5070](https://github.com/getsentry/sentry-java/pull/5070)) +- Add integration to track session replay custom masking ([#5088](https://github.com/getsentry/sentry-java/pull/5088)) - Establish new native exception mechanisms to differentiate events generated by `sentry-native` from `ApplicationExitInfo`. ([#5052](https://github.com/getsentry/sentry-java/pull/5052)) - Set `write` permission for `statuses` in the changelog preview GHA workflow. ([#5053](https://github.com/getsentry/sentry-java/pull/5053)) From 9467210f84a1d57d305212e7d451d7956a53e7f3 Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Tue, 10 Feb 2026 07:56:26 +0000 Subject: [PATCH 07/11] Format code --- sentry/src/test/java/io/sentry/SentryReplayOptionsTest.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/sentry/src/test/java/io/sentry/SentryReplayOptionsTest.kt b/sentry/src/test/java/io/sentry/SentryReplayOptionsTest.kt index e1391d0b40..5cf70c8e26 100644 --- a/sentry/src/test/java/io/sentry/SentryReplayOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/SentryReplayOptionsTest.kt @@ -188,7 +188,12 @@ class SentryReplayOptionsTest { SentryReplayOptions.trackCustomMasking() SentryReplayOptions.trackCustomMasking() assertTrue(hasCustomMaskingIntegration()) - assertEquals(1, SentryIntegrationPackageStorage.getInstance().integrations.count { it == "ReplayCustomMasking" }) + assertEquals( + 1, + SentryIntegrationPackageStorage.getInstance().integrations.count { + it == "ReplayCustomMasking" + }, + ) } @Test From f886f4936c7122c3b9c0f6ce8d8c6b2265111667 Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Tue, 10 Feb 2026 09:03:29 +0100 Subject: [PATCH 08/11] Fix Changelog --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b914d48047..da84c2be70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ - Add `installGroupsOverride` parameter to Build Distribution SDK for programmatic filtering, with support for configuration via properties file using `io.sentry.distribution.install-groups-override` ([#5066](https://github.com/getsentry/sentry-java/pull/5066)) +### Internal + +- Add integration to track session replay custom masking ([#5070](https://github.com/getsentry/sentry-java/pull/5070)) + ## 8.32.0 ### Features @@ -52,7 +56,6 @@ ### Internal -- Add integration to track session replay custom masking ([#5088](https://github.com/getsentry/sentry-java/pull/5088)) - Establish new native exception mechanisms to differentiate events generated by `sentry-native` from `ApplicationExitInfo`. ([#5052](https://github.com/getsentry/sentry-java/pull/5052)) - Set `write` permission for `statuses` in the changelog preview GHA workflow. ([#5053](https://github.com/getsentry/sentry-java/pull/5053)) From d5a08355363c2a81ebc1f6e0a964f8e6e6efe379 Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Tue, 10 Feb 2026 09:27:47 +0100 Subject: [PATCH 09/11] fix slop --- .../replay/viewhierarchy/ComposeViewHierarchyNode.kt | 4 ++-- .../replay/viewhierarchy/ViewHierarchyNode.kt | 5 ++--- sentry/api/sentry.api | 1 + .../src/main/java/io/sentry/SentryReplayOptions.java | 10 ++-------- .../test/java/io/sentry/SentryReplayOptionsTest.kt | 12 +++--------- 5 files changed, 10 insertions(+), 22 deletions(-) diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ComposeViewHierarchyNode.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ComposeViewHierarchyNode.kt index dcef27d523..a24a40a294 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ComposeViewHierarchyNode.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ComposeViewHierarchyNode.kt @@ -85,12 +85,12 @@ internal object ComposeViewHierarchyNode { ): Boolean { val sentryPrivacyModifier = this?.getOrNull(SentryReplayModifiers.SentryPrivacy) if (sentryPrivacyModifier == "unmask") { - SentryReplayOptions.trackCustomMasking() + options.sessionReplay.trackCustomMasking() return false } if (sentryPrivacyModifier == "mask") { - SentryReplayOptions.trackCustomMasking() + options.sessionReplay.trackCustomMasking() return true } diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ViewHierarchyNode.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ViewHierarchyNode.kt index 08add79d62..6b66bcef4b 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ViewHierarchyNode.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ViewHierarchyNode.kt @@ -8,7 +8,6 @@ import android.view.ViewParent import android.widget.ImageView import android.widget.TextView import io.sentry.SentryOptions -import io.sentry.SentryReplayOptions import io.sentry.android.replay.R import io.sentry.android.replay.util.AndroidTextLayout import io.sentry.android.replay.util.TextLayout @@ -292,7 +291,7 @@ internal sealed class ViewHierarchyNode( (tag as? String)?.lowercase()?.contains(SENTRY_UNMASK_TAG) == true || getTag(R.id.sentry_privacy) == "unmask" ) { - SentryReplayOptions.trackCustomMasking() + options.sessionReplay.trackCustomMasking() return false } @@ -300,7 +299,7 @@ internal sealed class ViewHierarchyNode( (tag as? String)?.lowercase()?.contains(SENTRY_MASK_TAG) == true || getTag(R.id.sentry_privacy) == "mask" ) { - SentryReplayOptions.trackCustomMasking() + options.sessionReplay.trackCustomMasking() return true } diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 4933a32f40..70ff460f3b 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -3953,6 +3953,7 @@ public final class io/sentry/SentryReplayOptions { public fun setSessionSampleRate (Ljava/lang/Double;)V public fun setTrackConfiguration (Z)V public fun setUnmaskViewContainerClass (Ljava/lang/String;)V + public fun trackCustomMasking ()V } public final class io/sentry/SentryReplayOptions$SentryReplayQuality : java/lang/Enum { diff --git a/sentry/src/main/java/io/sentry/SentryReplayOptions.java b/sentry/src/main/java/io/sentry/SentryReplayOptions.java index c4e112f8b2..fab26b07e8 100644 --- a/sentry/src/main/java/io/sentry/SentryReplayOptions.java +++ b/sentry/src/main/java/io/sentry/SentryReplayOptions.java @@ -15,12 +15,11 @@ import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import org.jetbrains.annotations.TestOnly; public final class SentryReplayOptions { private static final String CUSTOM_MASKING_INTEGRATION_NAME = "ReplayCustomMasking"; - private static volatile boolean customMaskingTracked = false; + private volatile boolean customMaskingTracked = false; public static final String TEXT_VIEW_CLASS_NAME = "android.widget.TextView"; public static final String IMAGE_VIEW_CLASS_NAME = "android.widget.ImageView"; @@ -382,18 +381,13 @@ public void setUnmaskViewContainerClass(@NotNull String containerClass) { } @ApiStatus.Internal - public static void trackCustomMasking() { + public void trackCustomMasking() { if (!customMaskingTracked) { customMaskingTracked = true; addIntegrationToSdkVersion(CUSTOM_MASKING_INTEGRATION_NAME); } } - @TestOnly - public static void resetCustomMaskingTracked() { - customMaskingTracked = false; - } - @ApiStatus.Internal public boolean isTrackConfiguration() { return trackConfiguration; diff --git a/sentry/src/test/java/io/sentry/SentryReplayOptionsTest.kt b/sentry/src/test/java/io/sentry/SentryReplayOptionsTest.kt index 5cf70c8e26..22f53b62f3 100644 --- a/sentry/src/test/java/io/sentry/SentryReplayOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/SentryReplayOptionsTest.kt @@ -11,7 +11,6 @@ class SentryReplayOptionsTest { @BeforeTest fun setup() { SentryIntegrationPackageStorage.getInstance().clearStorage() - SentryReplayOptions.resetCustomMaskingTracked() } @Test @@ -177,16 +176,11 @@ class SentryReplayOptionsTest { assertFalse(hasCustomMaskingIntegration()) } - @Test - fun `trackCustomMasking adds ReplayCustomMasking integration`() { - SentryReplayOptions.trackCustomMasking() - assertTrue(hasCustomMaskingIntegration()) - } - @Test fun `trackCustomMasking only adds integration once`() { - SentryReplayOptions.trackCustomMasking() - SentryReplayOptions.trackCustomMasking() + val options = SentryReplayOptions(false, null) + options.setMaskAllText(true) + options.setMaskAllImages(true) assertTrue(hasCustomMaskingIntegration()) assertEquals( 1, From 50a7d5170b3304abaeac498f49fadbe2ab9bd20d Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Tue, 10 Feb 2026 12:15:46 +0100 Subject: [PATCH 10/11] Address PR comments --- .../src/main/java/io/sentry/SentryReplayOptions.java | 4 ++-- .../test/java/io/sentry/SentryReplayOptionsTest.kt | 11 +++++++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/sentry/src/main/java/io/sentry/SentryReplayOptions.java b/sentry/src/main/java/io/sentry/SentryReplayOptions.java index fab26b07e8..23fbe2cb07 100644 --- a/sentry/src/main/java/io/sentry/SentryReplayOptions.java +++ b/sentry/src/main/java/io/sentry/SentryReplayOptions.java @@ -281,11 +281,11 @@ public void setSessionSampleRate(final @Nullable Double sessionSampleRate) { *

Default is enabled. */ public void setMaskAllText(final boolean maskAllText) { - trackCustomMasking(); if (maskAllText) { maskViewClasses.add(TEXT_VIEW_CLASS_NAME); unmaskViewClasses.remove(TEXT_VIEW_CLASS_NAME); } else { + trackCustomMasking(); unmaskViewClasses.add(TEXT_VIEW_CLASS_NAME); maskViewClasses.remove(TEXT_VIEW_CLASS_NAME); } @@ -300,11 +300,11 @@ public void setMaskAllText(final boolean maskAllText) { *

Default is enabled. */ public void setMaskAllImages(final boolean maskAllImages) { - trackCustomMasking(); if (maskAllImages) { maskViewClasses.add(IMAGE_VIEW_CLASS_NAME); unmaskViewClasses.remove(IMAGE_VIEW_CLASS_NAME); } else { + trackCustomMasking(); unmaskViewClasses.add(IMAGE_VIEW_CLASS_NAME); maskViewClasses.remove(IMAGE_VIEW_CLASS_NAME); } diff --git a/sentry/src/test/java/io/sentry/SentryReplayOptionsTest.kt b/sentry/src/test/java/io/sentry/SentryReplayOptionsTest.kt index 22f53b62f3..b704a0d760 100644 --- a/sentry/src/test/java/io/sentry/SentryReplayOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/SentryReplayOptionsTest.kt @@ -177,10 +177,18 @@ class SentryReplayOptionsTest { } @Test - fun `trackCustomMasking only adds integration once`() { + fun `setMaskAllText true does not set custom integration`() { val options = SentryReplayOptions(false, null) options.setMaskAllText(true) options.setMaskAllImages(true) + assertFalse(hasCustomMaskingIntegration()) + } + + @Test + fun `trackCustomMasking only adds integration once`() { + val options = SentryReplayOptions(false, null) + options.setMaskAllText(false) + options.setMaskAllImages(false) assertTrue(hasCustomMaskingIntegration()) assertEquals( 1, @@ -189,7 +197,6 @@ class SentryReplayOptionsTest { }, ) } - @Test fun `addMaskViewClass adds ReplayCustomMasking integration`() { val options = SentryReplayOptions(false, null) From b923bdd8f5b25ac0b830807febc93b98c1fb93ac Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Tue, 10 Feb 2026 11:19:49 +0000 Subject: [PATCH 11/11] Format code --- sentry/src/test/java/io/sentry/SentryReplayOptionsTest.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/sentry/src/test/java/io/sentry/SentryReplayOptionsTest.kt b/sentry/src/test/java/io/sentry/SentryReplayOptionsTest.kt index b704a0d760..114ef702e4 100644 --- a/sentry/src/test/java/io/sentry/SentryReplayOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/SentryReplayOptionsTest.kt @@ -197,6 +197,7 @@ class SentryReplayOptionsTest { }, ) } + @Test fun `addMaskViewClass adds ReplayCustomMasking integration`() { val options = SentryReplayOptions(false, null)