Skip to content

Commit c7356ce

Browse files
authored
Merge d5a0835 into 9ea89e8
2 parents 9ea89e8 + d5a0835 commit c7356ce

File tree

6 files changed

+118
-7
lines changed

6 files changed

+118
-7
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66

77
- 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))
88

9+
### Internal
10+
11+
- Add integration to track session replay custom masking ([#5070](https://github.com/getsentry/sentry-java/pull/5070))
12+
913
## 8.32.0
1014

1115
### 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);
@@ -275,11 +281,12 @@ public void setSessionSampleRate(final @Nullable Double sessionSampleRate) {
275281
* <p>Default is enabled.
276282
*/
277283
public void setMaskAllText(final boolean maskAllText) {
284+
trackCustomMasking();
278285
if (maskAllText) {
279-
addMaskViewClass(TEXT_VIEW_CLASS_NAME);
286+
maskViewClasses.add(TEXT_VIEW_CLASS_NAME);
280287
unmaskViewClasses.remove(TEXT_VIEW_CLASS_NAME);
281288
} else {
282-
addUnmaskViewClass(TEXT_VIEW_CLASS_NAME);
289+
unmaskViewClasses.add(TEXT_VIEW_CLASS_NAME);
283290
maskViewClasses.remove(TEXT_VIEW_CLASS_NAME);
284291
}
285292
}
@@ -293,11 +300,12 @@ public void setMaskAllText(final boolean maskAllText) {
293300
* <p>Default is enabled.
294301
*/
295302
public void setMaskAllImages(final boolean maskAllImages) {
303+
trackCustomMasking();
296304
if (maskAllImages) {
297-
addMaskViewClass(IMAGE_VIEW_CLASS_NAME);
305+
maskViewClasses.add(IMAGE_VIEW_CLASS_NAME);
298306
unmaskViewClasses.remove(IMAGE_VIEW_CLASS_NAME);
299307
} else {
300-
addUnmaskViewClass(IMAGE_VIEW_CLASS_NAME);
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: 84 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,80 @@ 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 `trackCustomMasking only adds integration once`() {
181+
val options = SentryReplayOptions(false, null)
182+
options.setMaskAllText(true)
183+
options.setMaskAllImages(true)
184+
assertTrue(hasCustomMaskingIntegration())
185+
assertEquals(
186+
1,
187+
SentryIntegrationPackageStorage.getInstance().integrations.count {
188+
it == "ReplayCustomMasking"
189+
},
190+
)
191+
}
192+
193+
@Test
194+
fun `addMaskViewClass adds ReplayCustomMasking integration`() {
195+
val options = SentryReplayOptions(false, null)
196+
options.addMaskViewClass("com.example.MySensitiveView")
197+
assertTrue(hasCustomMaskingIntegration())
198+
}
199+
200+
@Test
201+
fun `setMaskAllText adds ReplayCustomMasking integration`() {
202+
val options = SentryReplayOptions(false, null)
203+
options.setMaskAllText(false)
204+
assertTrue(hasCustomMaskingIntegration())
205+
}
206+
207+
@Test
208+
fun `setMaskAllImages adds ReplayCustomMasking integration`() {
209+
val options = SentryReplayOptions(false, null)
210+
options.setMaskAllImages(false)
211+
assertTrue(hasCustomMaskingIntegration())
212+
}
129213
}

0 commit comments

Comments
 (0)