Skip to content

Commit 6727e14

Browse files
markushiclaudegetsentry-bot
authored
feat(replay): Track custom masking usage via integration (#5070)
* feat(replay): Track custom masking usage via fake integration * chore(changelog): Add PR #5070 to changelog * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * refactor(replay): Rename custom masking tracking methods - customMaskingTagTracked -> customMaskingTracked - trackCustomMaskingTag() -> trackCustomMasking() - resetCustomMaskingTagTracked() -> resetCustomMaskingTracked() Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore(changelog): Update PR reference to #5088 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Format code * Fix Changelog * fix slop * Address PR comments * Format code --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Sentry Github Bot <bot+github-bot@sentry.io>
1 parent c9f9d33 commit 6727e14

File tree

6 files changed

+126
-7
lines changed

6 files changed

+126
-7
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212
- [changelog](https://github.com/getsentry/sentry-native/blob/master/CHANGELOG.md#0126)
1313
- [diff](https://github.com/getsentry/sentry-native/compare/0.12.4...0.12.6)
1414

15+
### Internal
16+
17+
- Add integration to track session replay custom masking ([#5070](https://github.com/getsentry/sentry-java/pull/5070))
18+
1519
## 8.32.0
1620

1721
### Features

sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ComposeViewHierarchyNode.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,10 +85,12 @@ internal object ComposeViewHierarchyNode {
8585
): Boolean {
8686
val sentryPrivacyModifier = this?.getOrNull(SentryReplayModifiers.SentryPrivacy)
8787
if (sentryPrivacyModifier == "unmask") {
88+
options.sessionReplay.trackCustomMasking()
8889
return false
8990
}
9091

9192
if (sentryPrivacyModifier == "mask") {
93+
options.sessionReplay.trackCustomMasking()
9294
return true
9395
}
9496

sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ViewHierarchyNode.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,13 +291,15 @@ internal sealed class ViewHierarchyNode(
291291
(tag as? String)?.lowercase()?.contains(SENTRY_UNMASK_TAG) == true ||
292292
getTag(R.id.sentry_privacy) == "unmask"
293293
) {
294+
options.sessionReplay.trackCustomMasking()
294295
return false
295296
}
296297

297298
if (
298299
(tag as? String)?.lowercase()?.contains(SENTRY_MASK_TAG) == true ||
299300
getTag(R.id.sentry_privacy) == "mask"
300301
) {
302+
options.sessionReplay.trackCustomMasking()
301303
return true
302304
}
303305

sentry/api/sentry.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3953,6 +3953,7 @@ public final class io/sentry/SentryReplayOptions {
39533953
public fun setSessionSampleRate (Ljava/lang/Double;)V
39543954
public fun setTrackConfiguration (Z)V
39553955
public fun setUnmaskViewContainerClass (Ljava/lang/String;)V
3956+
public fun trackCustomMasking ()V
39563957
}
39573958

39583959
public final class io/sentry/SentryReplayOptions$SentryReplayQuality : java/lang/Enum {

sentry/src/main/java/io/sentry/SentryReplayOptions.java

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package io.sentry;
22

3+
import static io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion;
4+
35
import io.sentry.protocol.SdkVersion;
46
import io.sentry.util.SampleRateUtils;
57
import java.util.ArrayList;
@@ -16,6 +18,9 @@
1618

1719
public final class SentryReplayOptions {
1820

21+
private static final String CUSTOM_MASKING_INTEGRATION_NAME = "ReplayCustomMasking";
22+
private volatile boolean customMaskingTracked = false;
23+
1924
public static final String TEXT_VIEW_CLASS_NAME = "android.widget.TextView";
2025
public static final String IMAGE_VIEW_CLASS_NAME = "android.widget.ImageView";
2126
public static final String WEB_VIEW_CLASS_NAME = "android.webkit.WebView";
@@ -209,8 +214,9 @@ public enum SentryReplayQuality {
209214

210215
public SentryReplayOptions(final boolean empty, final @Nullable SdkVersion sdkVersion) {
211216
if (!empty) {
212-
setMaskAllText(true);
213-
setMaskAllImages(true);
217+
// Add default mask classes directly without setting usingCustomMasking flag
218+
maskViewClasses.add(TEXT_VIEW_CLASS_NAME);
219+
maskViewClasses.add(IMAGE_VIEW_CLASS_NAME);
214220
maskViewClasses.add(WEB_VIEW_CLASS_NAME);
215221
maskViewClasses.add(VIDEO_VIEW_CLASS_NAME);
216222
maskViewClasses.add(ANDROIDX_MEDIA_VIEW_CLASS_NAME);
@@ -276,10 +282,11 @@ public void setSessionSampleRate(final @Nullable Double sessionSampleRate) {
276282
*/
277283
public void setMaskAllText(final boolean maskAllText) {
278284
if (maskAllText) {
279-
addMaskViewClass(TEXT_VIEW_CLASS_NAME);
285+
maskViewClasses.add(TEXT_VIEW_CLASS_NAME);
280286
unmaskViewClasses.remove(TEXT_VIEW_CLASS_NAME);
281287
} else {
282-
addUnmaskViewClass(TEXT_VIEW_CLASS_NAME);
288+
trackCustomMasking();
289+
unmaskViewClasses.add(TEXT_VIEW_CLASS_NAME);
283290
maskViewClasses.remove(TEXT_VIEW_CLASS_NAME);
284291
}
285292
}
@@ -294,10 +301,11 @@ public void setMaskAllText(final boolean maskAllText) {
294301
*/
295302
public void setMaskAllImages(final boolean maskAllImages) {
296303
if (maskAllImages) {
297-
addMaskViewClass(IMAGE_VIEW_CLASS_NAME);
304+
maskViewClasses.add(IMAGE_VIEW_CLASS_NAME);
298305
unmaskViewClasses.remove(IMAGE_VIEW_CLASS_NAME);
299306
} else {
300-
addUnmaskViewClass(IMAGE_VIEW_CLASS_NAME);
307+
trackCustomMasking();
308+
unmaskViewClasses.add(IMAGE_VIEW_CLASS_NAME);
301309
maskViewClasses.remove(IMAGE_VIEW_CLASS_NAME);
302310
}
303311
}
@@ -308,6 +316,7 @@ public Set<String> getMaskViewClasses() {
308316
}
309317

310318
public void addMaskViewClass(final @NotNull String className) {
319+
trackCustomMasking();
311320
this.maskViewClasses.add(className);
312321
}
313322

@@ -317,6 +326,7 @@ public Set<String> getUnmaskViewClasses() {
317326
}
318327

319328
public void addUnmaskViewClass(final @NotNull String className) {
329+
trackCustomMasking();
320330
this.unmaskViewClasses.add(className);
321331
}
322332

@@ -351,7 +361,7 @@ public long getSessionDuration() {
351361

352362
@ApiStatus.Internal
353363
public void setMaskViewContainerClass(@NotNull String containerClass) {
354-
addMaskViewClass(containerClass);
364+
maskViewClasses.add(containerClass);
355365
maskViewContainerClass = containerClass;
356366
}
357367

@@ -370,6 +380,14 @@ public void setUnmaskViewContainerClass(@NotNull String containerClass) {
370380
return unmaskViewContainerClass;
371381
}
372382

383+
@ApiStatus.Internal
384+
public void trackCustomMasking() {
385+
if (!customMaskingTracked) {
386+
customMaskingTracked = true;
387+
addIntegrationToSdkVersion(CUSTOM_MASKING_INTEGRATION_NAME);
388+
}
389+
}
390+
373391
@ApiStatus.Internal
374392
public boolean isTrackConfiguration() {
375393
return trackConfiguration;

sentry/src/test/java/io/sentry/SentryReplayOptionsTest.kt

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
11
package io.sentry
22

3+
import kotlin.test.BeforeTest
34
import kotlin.test.Test
45
import kotlin.test.assertEquals
6+
import kotlin.test.assertFalse
57
import kotlin.test.assertTrue
68

79
class SentryReplayOptionsTest {
10+
11+
@BeforeTest
12+
fun setup() {
13+
SentryIntegrationPackageStorage.getInstance().clearStorage()
14+
}
15+
816
@Test
917
fun `uses medium quality as default`() {
1018
val replayOptions = SentryReplayOptions(true, null)
@@ -126,4 +134,88 @@ class SentryReplayOptionsTest {
126134
assertTrue(headers.contains("X-Response-Header"))
127135
assertTrue(headers.contains("X-Debug-Header"))
128136
}
137+
138+
// Custom Masking Integration Tests
139+
140+
private fun hasCustomMaskingIntegration(): Boolean {
141+
return SentryIntegrationPackageStorage.getInstance()
142+
.integrations
143+
.contains("ReplayCustomMasking")
144+
}
145+
146+
@Test
147+
fun `default options does not add ReplayCustomMasking integration`() {
148+
SentryReplayOptions(false, null)
149+
assertFalse(hasCustomMaskingIntegration())
150+
}
151+
152+
@Test
153+
fun `empty options does not add ReplayCustomMasking integration`() {
154+
SentryReplayOptions(true, null)
155+
assertFalse(hasCustomMaskingIntegration())
156+
}
157+
158+
@Test
159+
fun `addUnmaskViewClass adds ReplayCustomMasking integration`() {
160+
val options = SentryReplayOptions(false, null)
161+
options.addUnmaskViewClass("com.example.MyTextView")
162+
assertTrue(hasCustomMaskingIntegration())
163+
}
164+
165+
@Test
166+
fun `setMaskViewContainerClass does not add ReplayCustomMasking integration`() {
167+
val options = SentryReplayOptions(false, null)
168+
options.setMaskViewContainerClass("com.example.MyContainer")
169+
assertFalse(hasCustomMaskingIntegration())
170+
}
171+
172+
@Test
173+
fun `setUnmaskViewContainerClass does not add ReplayCustomMasking integration`() {
174+
val options = SentryReplayOptions(false, null)
175+
options.setUnmaskViewContainerClass("com.example.MyContainer")
176+
assertFalse(hasCustomMaskingIntegration())
177+
}
178+
179+
@Test
180+
fun `setMaskAllText true does not set custom integration`() {
181+
val options = SentryReplayOptions(false, null)
182+
options.setMaskAllText(true)
183+
options.setMaskAllImages(true)
184+
assertFalse(hasCustomMaskingIntegration())
185+
}
186+
187+
@Test
188+
fun `trackCustomMasking only adds integration once`() {
189+
val options = SentryReplayOptions(false, null)
190+
options.setMaskAllText(false)
191+
options.setMaskAllImages(false)
192+
assertTrue(hasCustomMaskingIntegration())
193+
assertEquals(
194+
1,
195+
SentryIntegrationPackageStorage.getInstance().integrations.count {
196+
it == "ReplayCustomMasking"
197+
},
198+
)
199+
}
200+
201+
@Test
202+
fun `addMaskViewClass adds ReplayCustomMasking integration`() {
203+
val options = SentryReplayOptions(false, null)
204+
options.addMaskViewClass("com.example.MySensitiveView")
205+
assertTrue(hasCustomMaskingIntegration())
206+
}
207+
208+
@Test
209+
fun `setMaskAllText adds ReplayCustomMasking integration`() {
210+
val options = SentryReplayOptions(false, null)
211+
options.setMaskAllText(false)
212+
assertTrue(hasCustomMaskingIntegration())
213+
}
214+
215+
@Test
216+
fun `setMaskAllImages adds ReplayCustomMasking integration`() {
217+
val options = SentryReplayOptions(false, null)
218+
options.setMaskAllImages(false)
219+
assertTrue(hasCustomMaskingIntegration())
220+
}
129221
}

0 commit comments

Comments
 (0)