diff --git a/CHANGELOG.md b/CHANGELOG.md index d5d1652573..68ac4ae42e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ - Added `Environment` to the `Scope`, allowing you to overwrite the `Environment` set on the options ([#5772](https://github.com/getsentry/sentry-java/pull/5772)) - The scope `Environment` is also applied to the trace envelope header (DSC), logs, metrics and profiles, so all telemetry of a trace reports the same environment +### Improvements + +- Speed up Session Replay and screenshot masking for large `maskViewClasses`/`unmaskViewClasses` lists by using constant-time class lookups ([#6410](https://github.com/getsentry/sentry-java/pull/6410)) + ### Fixes - Keep UI profiling running when `startProfiler` is called before a requested stop takes effect at the end of the current chunk ([#6291](https://github.com/getsentry/sentry-java/pull/6291)) diff --git a/sentry-android-replay/src/test/java/io/sentry/android/replay/capture/SessionCaptureStrategyTest.kt b/sentry-android-replay/src/test/java/io/sentry/android/replay/capture/SessionCaptureStrategyTest.kt index 281eb1da3b..4c31090cee 100644 --- a/sentry-android-replay/src/test/java/io/sentry/android/replay/capture/SessionCaptureStrategyTest.kt +++ b/sentry-android-replay/src/test/java/io/sentry/android/replay/capture/SessionCaptureStrategyTest.kt @@ -46,7 +46,6 @@ import java.util.concurrent.ScheduledExecutorService import java.util.concurrent.TimeUnit.MILLISECONDS import java.util.concurrent.TimeUnit.SECONDS import kotlin.test.Test -import kotlin.test.assertContentEquals import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertNull @@ -696,8 +695,8 @@ class SessionCaptureStrategyTest { assertEquals(true, optionsEvent[0].optionsPayload["maskAllText"]) assertEquals(false, optionsEvent[0].optionsPayload["maskAllImages"]) assertEquals("high", optionsEvent[0].optionsPayload["quality"]) - assertContentEquals( - listOf( + assertThat(optionsEvent[0].optionsPayload["maskedViewClasses"] as Collection<*>) + .containsExactly( "android.widget.TextView", "android.webkit.WebView", "android.widget.VideoView", @@ -706,13 +705,9 @@ class SessionCaptureStrategyTest { "com.google.android.exoplayer2.ui.PlayerView", "com.google.android.exoplayer2.ui.StyledPlayerView", "my.custom.View", - ), - optionsEvent[0].optionsPayload["maskedViewClasses"] as Collection<*>, - ) - assertContentEquals( - listOf("android.widget.ImageView"), - optionsEvent[0].optionsPayload["unmaskedViewClasses"] as Collection<*>, - ) + ) + assertThat(optionsEvent[0].optionsPayload["unmaskedViewClasses"] as Collection<*>) + .containsExactly("android.widget.ImageView") }, ) } diff --git a/sentry/src/main/java/io/sentry/SentryMaskingOptions.java b/sentry/src/main/java/io/sentry/SentryMaskingOptions.java index dd7c5f9a30..44edff39a3 100644 --- a/sentry/src/main/java/io/sentry/SentryMaskingOptions.java +++ b/sentry/src/main/java/io/sentry/SentryMaskingOptions.java @@ -1,7 +1,8 @@ package io.sentry; +import java.util.Collections; import java.util.Set; -import java.util.concurrent.CopyOnWriteArraySet; +import java.util.concurrent.ConcurrentHashMap; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -32,7 +33,7 @@ public abstract class SentryMaskingOptions { * *

Default is empty. */ - protected Set maskViewClasses = new CopyOnWriteArraySet<>(); + protected Set maskViewClasses = Collections.newSetFromMap(new ConcurrentHashMap<>()); /** * Ignore all views with the specified class names from masking. The class name is the fully @@ -44,7 +45,7 @@ public abstract class SentryMaskingOptions { * *

Default is empty. */ - protected Set unmaskViewClasses = new CopyOnWriteArraySet<>(); + protected Set unmaskViewClasses = Collections.newSetFromMap(new ConcurrentHashMap<>()); /** The class name of the view container that masks all of its children. */ protected @Nullable String maskViewContainerClass = null; @@ -92,6 +93,10 @@ public Set getMaskViewClasses() { } public void addMaskViewClass(final @NotNull String className) { + // Hybrid SDKs may pass null despite the annotation, and the backing set rejects null keys + if (className == null) { + return; + } this.maskViewClasses.add(className); this.unmaskViewClasses.remove(className); } @@ -102,6 +107,10 @@ public Set getUnmaskViewClasses() { } public void addUnmaskViewClass(final @NotNull String className) { + // Hybrid SDKs may pass null despite the annotation, and the backing set rejects null keys + if (className == null) { + return; + } this.unmaskViewClasses.add(className); this.maskViewClasses.remove(className); } diff --git a/sentry/src/test/java/io/sentry/SentryMaskingOptionsTest.kt b/sentry/src/test/java/io/sentry/SentryMaskingOptionsTest.kt index 9f661710dc..3f2a62361e 100644 --- a/sentry/src/test/java/io/sentry/SentryMaskingOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/SentryMaskingOptionsTest.kt @@ -40,6 +40,26 @@ class SentryMaskingOptionsTest { assertTrue(options.unmaskViewClasses.contains("com.example.MyView")) } + @Test + fun `addMaskViewClass ignores null`() { + val options = TestMaskingOptions() + SentryMaskingOptions::class + .java + .getMethod("addMaskViewClass", String::class.java) + .invoke(options, null) + assertTrue(options.maskViewClasses.isEmpty()) + } + + @Test + fun `addUnmaskViewClass ignores null`() { + val options = TestMaskingOptions() + SentryMaskingOptions::class + .java + .getMethod("addUnmaskViewClass", String::class.java) + .invoke(options, null) + assertTrue(options.unmaskViewClasses.isEmpty()) + } + @Test fun `setMaskAllText true adds TextView to maskViewClasses`() { val options = TestMaskingOptions()