Skip to content

Commit 4e5a867

Browse files
adinauerclaude
andcommitted
fix(core): Support dedup exclusions in option sources
Allow deduplication exclusions to be configured through external properties, Android manifest metadata, and Spring Boot binding. Preserve built-in defaults when unset and replace them when explicitly configured, including empty lists. Refs GH-6365 Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 7a9e14e commit 4e5a867

10 files changed

Lines changed: 191 additions & 0 deletions

File tree

‎sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,9 @@ final class ManifestMetadataReader {
188188

189189
static final String IGNORED_ERRORS = "io.sentry.ignored-errors";
190190

191+
static final String IGNORED_ERRORS_FOR_DEDUPLICATION =
192+
"io.sentry.ignored-errors-for-deduplication";
193+
191194
static final String IN_APP_INCLUDES = "io.sentry.in-app-includes";
192195

193196
static final String IN_APP_EXCLUDES = "io.sentry.in-app-excludes";
@@ -736,6 +739,12 @@ static void applyMetadata(
736739

737740
options.setIgnoredErrors(readList(metadata, logger, IGNORED_ERRORS));
738741

742+
final @Nullable List<String> ignoredErrorsForDeduplication =
743+
readList(metadata, logger, IGNORED_ERRORS_FOR_DEDUPLICATION);
744+
if (ignoredErrorsForDeduplication != null) {
745+
options.setIgnoredErrorsForDeduplication(ignoredErrorsForDeduplication);
746+
}
747+
739748
final @Nullable List<String> includes = readList(metadata, logger, IN_APP_INCLUDES);
740749
if (includes != null && !includes.isEmpty()) {
741750
for (final @NotNull String include : includes) {

‎sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2113,6 +2113,45 @@ class ManifestMetadataReaderTest {
21132113
)
21142114
}
21152115

2116+
@Test
2117+
fun `applyMetadata reads ignored errors for deduplication if found`() {
2118+
val bundle =
2119+
bundleOf(
2120+
ManifestMetadataReader.IGNORED_ERRORS_FOR_DEDUPLICATION to
2121+
"example.FirstException,example.SecondException"
2122+
)
2123+
val context = fixture.getContext(metaData = bundle)
2124+
2125+
ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)
2126+
2127+
assertThat(fixture.options.ignoredErrorsForDeduplication)
2128+
.containsExactly(
2129+
FilterString("example.FirstException"),
2130+
FilterString("example.SecondException"),
2131+
)
2132+
.inOrder()
2133+
}
2134+
2135+
@Test
2136+
fun `applyMetadata preserves default ignored errors for deduplication if missing`() {
2137+
val context = fixture.getContext()
2138+
2139+
ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)
2140+
2141+
assertThat(fixture.options.ignoredErrorsForDeduplication)
2142+
.containsExactly(FilterString("io.netty.handler.timeout.ReadTimeoutException"))
2143+
}
2144+
2145+
@Test
2146+
fun `applyMetadata empty ignored errors for deduplication removes the default`() {
2147+
val bundle = bundleOf(ManifestMetadataReader.IGNORED_ERRORS_FOR_DEDUPLICATION to "")
2148+
val context = fixture.getContext(metaData = bundle)
2149+
2150+
ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)
2151+
2152+
assertThat(fixture.options.ignoredErrorsForDeduplication).isEmpty()
2153+
}
2154+
21162155
@Test
21172156
fun `applyMetadata reads inAppIncludes to options and sets the value if found`() {
21182157
// Arrange

‎sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentryAutoConfigurationTest.kt‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,7 @@ class SentryAutoConfigurationTest {
228228
"sentry.send-modules=false",
229229
"sentry.ignored-checkins=slug1,slugB",
230230
"sentry.ignored-errors=Some error,Another .*",
231+
"sentry.ignored-errors-for-deduplication=example.FirstException,example.SecondException",
231232
"sentry.ignored-transactions=transactionName1,transactionNameB",
232233
"sentry.enable-backpressure-handling=false",
233234
"sentry.enable-database-transaction-tracing=true",
@@ -283,6 +284,11 @@ class SentryAutoConfigurationTest {
283284
.containsOnly(FilterString("slug1"), FilterString("slugB"))
284285
assertThat(options.ignoredErrors)
285286
.containsOnly(FilterString("Some error"), FilterString("Another .*"))
287+
assertThat(options.ignoredErrorsForDeduplication)
288+
.containsOnly(
289+
FilterString("example.FirstException"),
290+
FilterString("example.SecondException"),
291+
)
286292
assertThat(options.ignoredTransactions)
287293
.containsOnly(FilterString("transactionName1"), FilterString("transactionNameB"))
288294
assertThat(options.isEnableBackpressureHandling).isEqualTo(false)
@@ -357,6 +363,19 @@ class SentryAutoConfigurationTest {
357363
}
358364
}
359365

366+
@Test
367+
fun `empty ignored errors for deduplication removes the default`() {
368+
contextRunner
369+
.withPropertyValues(
370+
"sentry.dsn=http://key@localhost/proj",
371+
"sentry.ignored-errors-for-deduplication=",
372+
)
373+
.run {
374+
val options = it.getBean(SentryProperties::class.java)
375+
assertThat(options.ignoredErrorsForDeduplication).isEmpty()
376+
}
377+
}
378+
360379
@Test
361380
fun `when traces sample rate is set to null and tracing is enabled, traces sample rate should be set to 0`() {
362381
contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj").run {

‎sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryAutoConfigurationTest.kt‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@ class SentryAutoConfigurationTest {
231231
"sentry.send-modules=false",
232232
"sentry.ignored-checkins=slug1,slugB",
233233
"sentry.ignored-errors=Some error,Another .*",
234+
"sentry.ignored-errors-for-deduplication=example.FirstException,example.SecondException",
234235
"sentry.ignored-transactions=transactionName1,transactionNameB",
235236
"sentry.enable-backpressure-handling=false",
236237
"sentry.enable-database-transaction-tracing=true",
@@ -288,6 +289,11 @@ class SentryAutoConfigurationTest {
288289
.containsOnly(FilterString("slug1"), FilterString("slugB"))
289290
assertThat(options.ignoredErrors)
290291
.containsOnly(FilterString("Some error"), FilterString("Another .*"))
292+
assertThat(options.ignoredErrorsForDeduplication)
293+
.containsOnly(
294+
FilterString("example.FirstException"),
295+
FilterString("example.SecondException"),
296+
)
291297
assertThat(options.ignoredTransactions)
292298
.containsOnly(FilterString("transactionName1"), FilterString("transactionNameB"))
293299
assertThat(options.isEnableBackpressureHandling).isEqualTo(false)
@@ -365,6 +371,19 @@ class SentryAutoConfigurationTest {
365371
}
366372
}
367373

374+
@Test
375+
fun `empty ignored errors for deduplication removes the default`() {
376+
contextRunner
377+
.withPropertyValues(
378+
"sentry.dsn=http://key@localhost/proj",
379+
"sentry.ignored-errors-for-deduplication=",
380+
)
381+
.run {
382+
val options = it.getBean(SentryProperties::class.java)
383+
assertThat(options.ignoredErrorsForDeduplication).isEmpty()
384+
}
385+
}
386+
368387
@Test
369388
fun `when traces sample rate is set to null and tracing is enabled, traces sample rate should be set to 0`() {
370389
contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj").run {

‎sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryAutoConfigurationTest.kt‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,7 @@ class SentryAutoConfigurationTest {
229229
"sentry.send-modules=false",
230230
"sentry.ignored-checkins=slug1,slugB",
231231
"sentry.ignored-errors=Some error,Another .*",
232+
"sentry.ignored-errors-for-deduplication=example.FirstException,example.SecondException",
232233
"sentry.ignored-transactions=transactionName1,transactionNameB",
233234
"sentry.enable-backpressure-handling=false",
234235
"sentry.enable-database-transaction-tracing=true",
@@ -286,6 +287,11 @@ class SentryAutoConfigurationTest {
286287
.containsOnly(FilterString("slug1"), FilterString("slugB"))
287288
assertThat(options.ignoredErrors)
288289
.containsOnly(FilterString("Some error"), FilterString("Another .*"))
290+
assertThat(options.ignoredErrorsForDeduplication)
291+
.containsOnly(
292+
FilterString("example.FirstException"),
293+
FilterString("example.SecondException"),
294+
)
289295
assertThat(options.ignoredTransactions)
290296
.containsOnly(FilterString("transactionName1"), FilterString("transactionNameB"))
291297
assertThat(options.isEnableBackpressureHandling).isEqualTo(false)
@@ -363,6 +369,19 @@ class SentryAutoConfigurationTest {
363369
}
364370
}
365371

372+
@Test
373+
fun `empty ignored errors for deduplication removes the default`() {
374+
contextRunner
375+
.withPropertyValues(
376+
"sentry.dsn=http://key@localhost/proj",
377+
"sentry.ignored-errors-for-deduplication=",
378+
)
379+
.run {
380+
val options = it.getBean(SentryProperties::class.java)
381+
assertThat(options.ignoredErrorsForDeduplication).isEmpty()
382+
}
383+
}
384+
366385
@Test
367386
fun `when traces sample rate is set to null and tracing is enabled, traces sample rate should be set to 0`() {
368387
dsnEnabledRunner.run {

‎sentry/api/sentry.api‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -564,6 +564,7 @@ public final class io/sentry/ExternalOptions {
564564
public fun getIdleTimeout ()Ljava/lang/Long;
565565
public fun getIgnoredCheckIns ()Ljava/util/List;
566566
public fun getIgnoredErrors ()Ljava/util/List;
567+
public fun getIgnoredErrorsForDeduplication ()Ljava/util/List;
567568
public fun getIgnoredExceptionsForType ()Ljava/util/Set;
568569
public fun getIgnoredTransactions ()Ljava/util/List;
569570
public fun getInAppExcludes ()Ljava/util/List;
@@ -621,6 +622,7 @@ public final class io/sentry/ExternalOptions {
621622
public fun setIdleTimeout (Ljava/lang/Long;)V
622623
public fun setIgnoredCheckIns (Ljava/util/List;)V
623624
public fun setIgnoredErrors (Ljava/util/List;)V
625+
public fun setIgnoredErrorsForDeduplication (Ljava/util/List;)V
624626
public fun setIgnoredTransactions (Ljava/util/List;)V
625627
public fun setMaxRequestBodySize (Lio/sentry/SentryOptions$RequestSize;)V
626628
public fun setOrgId (Ljava/lang/String;)V

‎sentry/src/main/java/io/sentry/ExternalOptions.java‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ public final class ExternalOptions {
4040
private final @NotNull Set<Class<? extends Throwable>> ignoredExceptionsForType =
4141
new CopyOnWriteArraySet<>();
4242
private @Nullable List<String> ignoredErrors;
43+
private @Nullable List<String> ignoredErrorsForDeduplication;
4344
private @Nullable Boolean printUncaughtStackTrace;
4445
private @Nullable Boolean sendClientReports;
4546
private @NotNull Set<String> bundleIds = new CopyOnWriteArraySet<>();
@@ -148,6 +149,8 @@ public final class ExternalOptions {
148149
propertiesProvider.getLongProperty("session-flush-timeout-millis"));
149150

150151
options.setIgnoredErrors(propertiesProvider.getListOrNull("ignored-errors"));
152+
options.setIgnoredErrorsForDeduplication(
153+
propertiesProvider.getListOrNull("ignored-errors-for-deduplication"));
151154

152155
options.setEnabled(propertiesProvider.getBooleanProperty("enabled"));
153156

@@ -549,6 +552,15 @@ public void setIgnoredErrors(final @Nullable List<String> ignoredErrors) {
549552
this.ignoredErrors = ignoredErrors;
550553
}
551554

555+
public @Nullable List<String> getIgnoredErrorsForDeduplication() {
556+
return ignoredErrorsForDeduplication;
557+
}
558+
559+
public void setIgnoredErrorsForDeduplication(
560+
final @Nullable List<String> ignoredErrorsForDeduplication) {
561+
this.ignoredErrorsForDeduplication = ignoredErrorsForDeduplication;
562+
}
563+
552564
public @Nullable Boolean getSendClientReports() {
553565
return sendClientReports;
554566
}

‎sentry/src/main/java/io/sentry/SentryOptions.java‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3809,6 +3809,11 @@ public void merge(final @NotNull ExternalOptions options) {
38093809
final List<String> ignoredExceptions = new ArrayList<>(options.getIgnoredErrors());
38103810
setIgnoredErrors(ignoredExceptions);
38113811
}
3812+
if (options.getIgnoredErrorsForDeduplication() != null) {
3813+
final List<String> ignoredErrorsForDeduplication =
3814+
new ArrayList<>(options.getIgnoredErrorsForDeduplication());
3815+
setIgnoredErrorsForDeduplication(ignoredErrorsForDeduplication);
3816+
}
38123817
if (options.isEnableBackpressureHandling() != null) {
38133818
setEnableBackpressureHandling(options.isEnableBackpressureHandling());
38143819
}

‎sentry/src/test/java/io/sentry/ExternalOptionsTest.kt‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,29 @@ class ExternalOptionsTest {
333333
withPropertiesFile("Another .*", logger) { options -> assertNull(options.ignoredErrors) }
334334
}
335335

336+
@Test
337+
fun `creates options with ignored errors for deduplication using external properties`() {
338+
withPropertiesFile(
339+
"ignored-errors-for-deduplication=example.FirstException,example.SecondException"
340+
) { options ->
341+
assertThat(options.ignoredErrorsForDeduplication)
342+
.containsExactly("example.FirstException", "example.SecondException")
343+
.inOrder()
344+
}
345+
}
346+
347+
@Test
348+
fun `creates options with empty ignored errors for deduplication using external properties`() {
349+
withPropertiesFile("ignored-errors-for-deduplication=") { options ->
350+
assertThat(options.ignoredErrorsForDeduplication).containsExactly("")
351+
}
352+
}
353+
354+
@Test
355+
fun `creates options with null ignored errors for deduplication if missing`() {
356+
withPropertiesFile { options -> assertThat(options.ignoredErrorsForDeduplication).isNull() }
357+
}
358+
336359
@Test
337360
fun `creates options with single bundle ID using external properties`() {
338361
withPropertiesFile("bundle-ids=12ea7a02-46ac-44c0-a5bb-6d1fd9586411") { options ->

‎sentry/src/test/java/io/sentry/SentryOptionsTest.kt‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -571,6 +571,8 @@ class SentryOptionsTest {
571571
externalOptions.ignoredCheckIns = listOf("slug1", "slug-B")
572572
externalOptions.ignoredTransactions = listOf("transactionName1", "transaction-name-B")
573573
externalOptions.ignoredErrors = listOf("Some error", "Another .*")
574+
externalOptions.ignoredErrorsForDeduplication =
575+
listOf("example.FirstException", "example.SecondException")
574576
externalOptions.isEnableBackpressureHandling = false
575577
externalOptions.isEnableDatabaseTransactionTracing = true
576578
externalOptions.isEnableCacheTracing = true
@@ -635,6 +637,10 @@ class SentryOptionsTest {
635637
listOf(FilterString("Some error"), FilterString("Another .*")),
636638
options.ignoredErrors,
637639
)
640+
assertEquals(
641+
listOf(FilterString("example.FirstException"), FilterString("example.SecondException")),
642+
options.ignoredErrorsForDeduplication,
643+
)
638644
assertFalse(options.isEnableBackpressureHandling)
639645
assertTrue(options.isEnableDatabaseTransactionTracing)
640646
assertTrue(options.isEnableCacheTracing)
@@ -1070,6 +1076,44 @@ class SentryOptionsTest {
10701076
.inOrder()
10711077
}
10721078

1079+
@Test
1080+
fun `merging options without ignored errors for deduplication preserves the default`() {
1081+
val options = SentryOptions()
1082+
1083+
options.merge(ExternalOptions())
1084+
1085+
assertThat(options.ignoredErrorsForDeduplication)
1086+
.containsExactly(FilterString("io.netty.handler.timeout.ReadTimeoutException"))
1087+
}
1088+
1089+
@Test
1090+
fun `merging ignored errors for deduplication replaces the default`() {
1091+
val options = SentryOptions()
1092+
val externalOptions =
1093+
ExternalOptions().apply {
1094+
ignoredErrorsForDeduplication = listOf("example.FirstException", "example.SecondException")
1095+
}
1096+
1097+
options.merge(externalOptions)
1098+
1099+
assertThat(options.ignoredErrorsForDeduplication)
1100+
.containsExactly(
1101+
FilterString("example.FirstException"),
1102+
FilterString("example.SecondException"),
1103+
)
1104+
.inOrder()
1105+
}
1106+
1107+
@Test
1108+
fun `merging empty ignored errors for deduplication removes the default`() {
1109+
val options = SentryOptions()
1110+
val externalOptions = ExternalOptions().apply { ignoredErrorsForDeduplication = listOf("") }
1111+
1112+
options.merge(externalOptions)
1113+
1114+
assertThat(options.ignoredErrorsForDeduplication).isEmpty()
1115+
}
1116+
10731117
@Test
10741118
fun `merging options when ignoredErrors is not set preserves the previous value`() {
10751119
val externalOptions = ExternalOptions()

0 commit comments

Comments
 (0)