From 1786e17a864c2415bb4b5c3c0b25330b7d273af9 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Fri, 24 Jul 2026 15:33:18 +0200 Subject: [PATCH] fix(spring): Bind Data Collection key-value policies Make KeyValueCollectionBehavior JavaBean-bindable so Spring Boot properties correctly configure cookie, header, and query policies. Cover binding across all supported Spring Boot generations. Co-Authored-By: Claude --- .../boot4/SentryAutoConfigurationTest.kt | 30 +++++++++++++++++++ .../jakarta/SentryAutoConfigurationTest.kt | 30 +++++++++++++++++++ .../boot/SentryAutoConfigurationTest.kt | 30 +++++++++++++++++++ sentry/api/sentry.api | 3 ++ .../io/sentry/KeyValueCollectionBehavior.java | 19 +++++++++--- .../sentry/KeyValueCollectionBehaviorTest.kt | 21 +++++++++++++ 6 files changed, 129 insertions(+), 4 deletions(-) diff --git a/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentryAutoConfigurationTest.kt b/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentryAutoConfigurationTest.kt index ef1f12aeecf..8e4cc6119d2 100644 --- a/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentryAutoConfigurationTest.kt +++ b/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentryAutoConfigurationTest.kt @@ -12,6 +12,7 @@ import io.sentry.IProfileConverter import io.sentry.IScopes import io.sentry.ITransportFactory import io.sentry.Integration +import io.sentry.KeyValueCollectionBehavior import io.sentry.NoOpContinuousProfiler import io.sentry.NoOpProfileConverter import io.sentry.NoOpTransportFactory @@ -306,6 +307,35 @@ class SentryAutoConfigurationTest { } } + @Test + fun `data collection key value properties are applied to SentryOptions`() { + contextRunner + .withPropertyValues( + "sentry.dsn=http://key@localhost/proj", + "sentry.data-collection.cookies.mode=off", + "sentry.data-collection.query-params.mode=allow-list", + "sentry.data-collection.query-params.terms=page,sort", + "sentry.data-collection.http-headers.request.mode=deny-list", + "sentry.data-collection.http-headers.request.terms=forwarded,-ip", + "sentry.data-collection.http-headers.response.mode=allow-list", + "sentry.data-collection.http-headers.response.terms=content-type,x-request-id", + ) + .run { + val dataCollection = it.getBean(SentryProperties::class.java).dataCollection + assertThat(dataCollection.cookies!!.mode).isEqualTo(KeyValueCollectionBehavior.Mode.OFF) + assertThat(dataCollection.queryParams!!.mode) + .isEqualTo(KeyValueCollectionBehavior.Mode.ALLOW_LIST) + assertThat(dataCollection.queryParams!!.terms).containsExactly("page", "sort") + assertThat(dataCollection.httpHeaders.request!!.mode) + .isEqualTo(KeyValueCollectionBehavior.Mode.DENY_LIST) + assertThat(dataCollection.httpHeaders.request!!.terms).containsExactly("forwarded", "-ip") + assertThat(dataCollection.httpHeaders.response!!.mode) + .isEqualTo(KeyValueCollectionBehavior.Mode.ALLOW_LIST) + assertThat(dataCollection.httpHeaders.response!!.terms) + .containsExactly("content-type", "x-request-id") + } + } + @Test fun `when tracePropagationTargets are not set, default is returned`() { contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj").run { diff --git a/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryAutoConfigurationTest.kt b/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryAutoConfigurationTest.kt index 91677d16b4e..308e623cba2 100644 --- a/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryAutoConfigurationTest.kt +++ b/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryAutoConfigurationTest.kt @@ -13,6 +13,7 @@ import io.sentry.IProfileConverter import io.sentry.IScopes import io.sentry.ITransportFactory import io.sentry.Integration +import io.sentry.KeyValueCollectionBehavior import io.sentry.NoOpContinuousProfiler import io.sentry.NoOpProfileConverter import io.sentry.NoOpTransportFactory @@ -314,6 +315,35 @@ class SentryAutoConfigurationTest { } } + @Test + fun `data collection key value properties are applied to SentryOptions`() { + contextRunner + .withPropertyValues( + "sentry.dsn=http://key@localhost/proj", + "sentry.data-collection.cookies.mode=off", + "sentry.data-collection.query-params.mode=allow-list", + "sentry.data-collection.query-params.terms=page,sort", + "sentry.data-collection.http-headers.request.mode=deny-list", + "sentry.data-collection.http-headers.request.terms=forwarded,-ip", + "sentry.data-collection.http-headers.response.mode=allow-list", + "sentry.data-collection.http-headers.response.terms=content-type,x-request-id", + ) + .run { + val dataCollection = it.getBean(SentryProperties::class.java).dataCollection + assertThat(dataCollection.cookies!!.mode).isEqualTo(KeyValueCollectionBehavior.Mode.OFF) + assertThat(dataCollection.queryParams!!.mode) + .isEqualTo(KeyValueCollectionBehavior.Mode.ALLOW_LIST) + assertThat(dataCollection.queryParams!!.terms).containsExactly("page", "sort") + assertThat(dataCollection.httpHeaders.request!!.mode) + .isEqualTo(KeyValueCollectionBehavior.Mode.DENY_LIST) + assertThat(dataCollection.httpHeaders.request!!.terms).containsExactly("forwarded", "-ip") + assertThat(dataCollection.httpHeaders.response!!.mode) + .isEqualTo(KeyValueCollectionBehavior.Mode.ALLOW_LIST) + assertThat(dataCollection.httpHeaders.response!!.terms) + .containsExactly("content-type", "x-request-id") + } + } + @Test fun `when tracePropagationTargets are not set, default is returned`() { contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj").run { diff --git a/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryAutoConfigurationTest.kt b/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryAutoConfigurationTest.kt index d9e598d0473..5eebd1bcc6b 100644 --- a/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryAutoConfigurationTest.kt +++ b/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryAutoConfigurationTest.kt @@ -13,6 +13,7 @@ import io.sentry.IProfileConverter import io.sentry.IScopes import io.sentry.ITransportFactory import io.sentry.Integration +import io.sentry.KeyValueCollectionBehavior import io.sentry.NoOpContinuousProfiler import io.sentry.NoOpProfileConverter import io.sentry.NoOpTransportFactory @@ -312,6 +313,35 @@ class SentryAutoConfigurationTest { } } + @Test + fun `data collection key value properties are applied to SentryOptions`() { + contextRunner + .withPropertyValues( + "sentry.dsn=http://key@localhost/proj", + "sentry.data-collection.cookies.mode=off", + "sentry.data-collection.query-params.mode=allow-list", + "sentry.data-collection.query-params.terms=page,sort", + "sentry.data-collection.http-headers.request.mode=deny-list", + "sentry.data-collection.http-headers.request.terms=forwarded,-ip", + "sentry.data-collection.http-headers.response.mode=allow-list", + "sentry.data-collection.http-headers.response.terms=content-type,x-request-id", + ) + .run { + val dataCollection = it.getBean(SentryProperties::class.java).dataCollection + assertThat(dataCollection.cookies!!.mode).isEqualTo(KeyValueCollectionBehavior.Mode.OFF) + assertThat(dataCollection.queryParams!!.mode) + .isEqualTo(KeyValueCollectionBehavior.Mode.ALLOW_LIST) + assertThat(dataCollection.queryParams!!.terms).containsExactly("page", "sort") + assertThat(dataCollection.httpHeaders.request!!.mode) + .isEqualTo(KeyValueCollectionBehavior.Mode.DENY_LIST) + assertThat(dataCollection.httpHeaders.request!!.terms).containsExactly("forwarded", "-ip") + assertThat(dataCollection.httpHeaders.response!!.mode) + .isEqualTo(KeyValueCollectionBehavior.Mode.ALLOW_LIST) + assertThat(dataCollection.httpHeaders.response!!.terms) + .containsExactly("content-type", "x-request-id") + } + } + @Test fun `when tracePropagationTargets are not set, default is returned`() { dsnEnabledRunner.run { diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 2a4ab4f0c1a..6e61380f81b 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -1447,6 +1447,7 @@ public abstract interface class io/sentry/JsonUnknown { } public final class io/sentry/KeyValueCollectionBehavior { + public fun ()V public static fun allowList ([Ljava/lang/String;)Lio/sentry/KeyValueCollectionBehavior; public static fun denyList ([Ljava/lang/String;)Lio/sentry/KeyValueCollectionBehavior; public fun equals (Ljava/lang/Object;)Z @@ -1454,6 +1455,8 @@ public final class io/sentry/KeyValueCollectionBehavior { public fun getTerms ()Ljava/util/List; public fun hashCode ()I public static fun off ()Lio/sentry/KeyValueCollectionBehavior; + public fun setMode (Lio/sentry/KeyValueCollectionBehavior$Mode;)V + public fun setTerms (Ljava/util/List;)V } public final class io/sentry/KeyValueCollectionBehavior$Mode : java/lang/Enum { diff --git a/sentry/src/main/java/io/sentry/KeyValueCollectionBehavior.java b/sentry/src/main/java/io/sentry/KeyValueCollectionBehavior.java index d9daeb9bc02..35cc0896719 100644 --- a/sentry/src/main/java/io/sentry/KeyValueCollectionBehavior.java +++ b/sentry/src/main/java/io/sentry/KeyValueCollectionBehavior.java @@ -20,12 +20,15 @@ public enum Mode { ALLOW_LIST } - private final @NotNull Mode mode; - private final @NotNull List terms; + private @NotNull Mode mode = Mode.DENY_LIST; + private @NotNull List terms = Collections.emptyList(); + + /** Creates a behavior that collects values using the built-in sensitive deny-list. */ + public KeyValueCollectionBehavior() {} private KeyValueCollectionBehavior(final @NotNull Mode mode, final @NotNull List terms) { - this.mode = mode; - this.terms = Collections.unmodifiableList(new ArrayList<>(terms)); + setMode(mode); + setTerms(terms); } /** Disables collection of the category. */ @@ -53,10 +56,18 @@ private KeyValueCollectionBehavior(final @NotNull Mode mode, final @NotNull List return mode; } + public void setMode(final @NotNull Mode mode) { + this.mode = mode; + } + public @NotNull List getTerms() { return terms; } + public void setTerms(final @NotNull List terms) { + this.terms = Collections.unmodifiableList(new ArrayList<>(terms)); + } + @Override public boolean equals(final Object other) { if (this == other) { diff --git a/sentry/src/test/java/io/sentry/KeyValueCollectionBehaviorTest.kt b/sentry/src/test/java/io/sentry/KeyValueCollectionBehaviorTest.kt index 7e014eec504..09cd92f4fce 100644 --- a/sentry/src/test/java/io/sentry/KeyValueCollectionBehaviorTest.kt +++ b/sentry/src/test/java/io/sentry/KeyValueCollectionBehaviorTest.kt @@ -4,6 +4,27 @@ import com.google.common.truth.Truth.assertThat import kotlin.test.Test class KeyValueCollectionBehaviorTest { + @Test + fun `default constructor uses deny list with no terms`() { + val behavior = KeyValueCollectionBehavior() + + assertThat(behavior.mode).isEqualTo(KeyValueCollectionBehavior.Mode.DENY_LIST) + assertThat(behavior.terms).isEmpty() + } + + @Test + fun `setters update mode and defensively copy terms`() { + val terms = mutableListOf("token") + val behavior = KeyValueCollectionBehavior() + + behavior.mode = KeyValueCollectionBehavior.Mode.ALLOW_LIST + behavior.terms = terms + terms[0] = "password" + + assertThat(behavior.mode).isEqualTo(KeyValueCollectionBehavior.Mode.ALLOW_LIST) + assertThat(behavior.terms).containsExactly("token") + } + @Test fun `off has no terms`() { val behavior = KeyValueCollectionBehavior.off()