Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

### 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))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,12 @@ internal object ComposeViewHierarchyNode {
): Boolean {
val sentryPrivacyModifier = this?.getOrNull(SentryReplayModifiers.SentryPrivacy)
if (sentryPrivacyModifier == "unmask") {
SentryReplayOptions.trackCustomMasking()
return false
}

if (sentryPrivacyModifier == "mask") {
SentryReplayOptions.trackCustomMasking()
return true
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -291,13 +292,15 @@ internal sealed class ViewHierarchyNode(
(tag as? String)?.lowercase()?.contains(SENTRY_UNMASK_TAG) == true ||
getTag(R.id.sentry_privacy) == "unmask"
) {
SentryReplayOptions.trackCustomMasking()
return false
}

if (
(tag as? String)?.lowercase()?.contains(SENTRY_MASK_TAG) == true ||
getTag(R.id.sentry_privacy) == "mask"
) {
SentryReplayOptions.trackCustomMasking()
return true
}

Expand Down
38 changes: 31 additions & 7 deletions sentry/src/main/java/io/sentry/SentryReplayOptions.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -13,9 +15,13 @@
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;

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";
Expand Down Expand Up @@ -209,8 +215,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);
Expand Down Expand Up @@ -275,11 +282,12 @@ public void setSessionSampleRate(final @Nullable Double sessionSampleRate) {
* <p>Default is enabled.
*/
public void setMaskAllText(final boolean maskAllText) {
trackCustomMasking();
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);
}
}
Expand All @@ -293,11 +301,12 @@ public void setMaskAllText(final boolean maskAllText) {
* <p>Default is enabled.
*/
public void setMaskAllImages(final boolean maskAllImages) {
trackCustomMasking();
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);
}
}
Expand All @@ -308,6 +317,7 @@ public Set<String> getMaskViewClasses() {
}

public void addMaskViewClass(final @NotNull String className) {
trackCustomMasking();
this.maskViewClasses.add(className);
}

Expand All @@ -317,6 +327,7 @@ public Set<String> getUnmaskViewClasses() {
}

public void addUnmaskViewClass(final @NotNull String className) {
trackCustomMasking();
this.unmaskViewClasses.add(className);
}

Expand Down Expand Up @@ -351,7 +362,7 @@ public long getSessionDuration() {

@ApiStatus.Internal
public void setMaskViewContainerClass(@NotNull String containerClass) {
addMaskViewClass(containerClass);
maskViewClasses.add(containerClass);
maskViewContainerClass = containerClass;
}

Expand All @@ -370,6 +381,19 @@ public void setUnmaskViewContainerClass(@NotNull String containerClass) {
return unmaskViewContainerClass;
}

@ApiStatus.Internal
public static 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;
Expand Down
85 changes: 85 additions & 0 deletions sentry/src/test/java/io/sentry/SentryReplayOptionsTest.kt
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
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()
SentryReplayOptions.resetCustomMaskingTracked()
}

@Test
fun `uses medium quality as default`() {
val replayOptions = SentryReplayOptions(true, null)
Expand Down Expand Up @@ -126,4 +135,80 @@ 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 does not add ReplayCustomMasking integration`() {
val options = SentryReplayOptions(false, null)
options.setMaskViewContainerClass("com.example.MyContainer")
assertFalse(hasCustomMaskingIntegration())
}

@Test
fun `setUnmaskViewContainerClass does not add ReplayCustomMasking integration`() {
val options = SentryReplayOptions(false, null)
options.setUnmaskViewContainerClass("com.example.MyContainer")
assertFalse(hasCustomMaskingIntegration())
}

@Test
fun `trackCustomMasking adds ReplayCustomMasking integration`() {
SentryReplayOptions.trackCustomMasking()
assertTrue(hasCustomMaskingIntegration())
}

@Test
fun `trackCustomMasking only adds integration once`() {
SentryReplayOptions.trackCustomMasking()
SentryReplayOptions.trackCustomMasking()
assertTrue(hasCustomMaskingIntegration())
assertEquals(1, SentryIntegrationPackageStorage.getInstance().integrations.count { it == "ReplayCustomMasking" })
}

@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())
}
}
Loading