diff --git a/CHANGELOG.md b/CHANGELOG.md index c0dc15d200e..7e8a44bbbde 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ ## Unreleased +### Features + +- Add `options.ignoredErrors` to filter out errors that match a certain String or Regex ([#4083](https://github.com/getsentry/sentry-java/pull/4083)) + - The matching is attempted on `event.message`, `event.formatted`, and `{event.throwable.class.name}: {event.throwable.message}` + - Can be set in `sentry.properties`, e.g. `ignored-errors=Some error,Another .*` + - Can be set in environment variables, e.g. `SENTRY_IGNORED_ERRORS=Some error,Another .*` + - For Spring Boot, it can be set in `application.properties`, e.g. `sentry.ignored-errors=Some error,Another .*` + ### Fixes - Avoid logging an error when a float is passed in the manifest ([#4031](https://github.com/getsentry/sentry-java/pull/4031)) 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 e559502e626..3498c903194 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 @@ -174,6 +174,7 @@ class SentryAutoConfigurationTest { "sentry.enabled=false", "sentry.send-modules=false", "sentry.ignored-checkins=slug1,slugB", + "sentry.ignored-errors=Some error,Another .*", "sentry.ignored-transactions=transactionName1,transactionNameB", "sentry.enable-backpressure-handling=false", "sentry.enable-spotlight=true", @@ -215,6 +216,7 @@ class SentryAutoConfigurationTest { assertThat(options.isEnabled).isEqualTo(false) assertThat(options.isSendModules).isEqualTo(false) assertThat(options.ignoredCheckIns).containsOnly(FilterString("slug1"), FilterString("slugB")) + assertThat(options.ignoredErrors).containsOnly(FilterString("Some error"), FilterString("Another .*")) assertThat(options.ignoredTransactions).containsOnly(FilterString("transactionName1"), FilterString("transactionNameB")) assertThat(options.isEnableBackpressureHandling).isEqualTo(false) assertThat(options.isForceInit).isEqualTo(true) 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 2512fe1f11e..cd105dab445 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 @@ -173,6 +173,7 @@ class SentryAutoConfigurationTest { "sentry.enabled=false", "sentry.send-modules=false", "sentry.ignored-checkins=slug1,slugB", + "sentry.ignored-errors=Some error,Another .*", "sentry.ignored-transactions=transactionName1,transactionNameB", "sentry.enable-backpressure-handling=false", "sentry.enable-spotlight=true", @@ -214,6 +215,7 @@ class SentryAutoConfigurationTest { assertThat(options.isEnabled).isEqualTo(false) assertThat(options.isSendModules).isEqualTo(false) assertThat(options.ignoredCheckIns).containsOnly(FilterString("slug1"), FilterString("slugB")) + assertThat(options.ignoredErrors).containsOnly(FilterString("Some error"), FilterString("Another .*")) assertThat(options.ignoredTransactions).containsOnly(FilterString("transactionName1"), FilterString("transactionNameB")) assertThat(options.isEnableBackpressureHandling).isEqualTo(false) assertThat(options.isForceInit).isEqualTo(true) diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index a0d559f12f5..642b67ec06f 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -452,6 +452,7 @@ public final class io/sentry/ExternalOptions { public fun getEnvironment ()Ljava/lang/String; public fun getIdleTimeout ()Ljava/lang/Long; public fun getIgnoredCheckIns ()Ljava/util/List; + public fun getIgnoredErrors ()Ljava/util/List; public fun getIgnoredExceptionsForType ()Ljava/util/Set; public fun getIgnoredTransactions ()Ljava/util/List; public fun getInAppExcludes ()Ljava/util/List; @@ -491,6 +492,7 @@ public final class io/sentry/ExternalOptions { public fun setGlobalHubMode (Ljava/lang/Boolean;)V public fun setIdleTimeout (Ljava/lang/Long;)V public fun setIgnoredCheckIns (Ljava/util/List;)V + public fun setIgnoredErrors (Ljava/util/List;)V public fun setIgnoredTransactions (Ljava/util/List;)V public fun setMaxRequestBodySize (Lio/sentry/SentryOptions$RequestSize;)V public fun setPrintUncaughtStackTrace (Ljava/lang/Boolean;)V @@ -2814,6 +2816,7 @@ public class io/sentry/SentryOptions { public fun addContextTag (Ljava/lang/String;)V public fun addEventProcessor (Lio/sentry/EventProcessor;)V public fun addIgnoredCheckIn (Ljava/lang/String;)V + public fun addIgnoredError (Ljava/lang/String;)V public fun addIgnoredExceptionForType (Ljava/lang/Class;)V public fun addIgnoredSpanOrigin (Ljava/lang/String;)V public fun addIgnoredTransaction (Ljava/lang/String;)V @@ -2855,6 +2858,7 @@ public class io/sentry/SentryOptions { public fun getGestureTargetLocators ()Ljava/util/List; public fun getIdleTimeout ()Ljava/lang/Long; public fun getIgnoredCheckIns ()Ljava/util/List; + public fun getIgnoredErrors ()Ljava/util/List; public fun getIgnoredExceptionsForType ()Ljava/util/Set; public fun getIgnoredSpanOrigins ()Ljava/util/List; public fun getIgnoredTransactions ()Ljava/util/List; @@ -2987,6 +2991,7 @@ public class io/sentry/SentryOptions { public fun setGlobalHubMode (Ljava/lang/Boolean;)V public fun setIdleTimeout (Ljava/lang/Long;)V public fun setIgnoredCheckIns (Ljava/util/List;)V + public fun setIgnoredErrors (Ljava/util/List;)V public fun setIgnoredSpanOrigins (Ljava/util/List;)V public fun setIgnoredTransactions (Ljava/util/List;)V public fun setInitPriority (Lio/sentry/InitPriority;)V @@ -6047,6 +6052,11 @@ public final class io/sentry/util/DebugMetaPropertiesApplier { public static fun getProguardUuid (Ljava/util/Properties;)Ljava/lang/String; } +public final class io/sentry/util/ErrorUtils { + public fun ()V + public static fun isIgnored (Ljava/util/List;Lio/sentry/SentryEvent;)Z +} + public final class io/sentry/util/EventProcessorUtils { public fun ()V public static fun unwrap (Ljava/util/List;)Ljava/util/List; @@ -6055,6 +6065,7 @@ public final class io/sentry/util/EventProcessorUtils { public final class io/sentry/util/ExceptionUtils { public fun ()V public static fun findRootCause (Ljava/lang/Throwable;)Ljava/lang/Throwable; + public static fun isIgnored (Ljava/util/Set;Ljava/lang/Throwable;)Z } public final class io/sentry/util/FileUtils { diff --git a/sentry/src/main/java/io/sentry/ExternalOptions.java b/sentry/src/main/java/io/sentry/ExternalOptions.java index d9b075e1c89..ed2b4e1103a 100644 --- a/sentry/src/main/java/io/sentry/ExternalOptions.java +++ b/sentry/src/main/java/io/sentry/ExternalOptions.java @@ -1,10 +1,7 @@ package io.sentry; import io.sentry.config.PropertiesProvider; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; +import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArraySet; @@ -39,6 +36,7 @@ public final class ExternalOptions { private @Nullable Long idleTimeout; private final @NotNull Set> ignoredExceptionsForType = new CopyOnWriteArraySet<>(); + private @Nullable List ignoredErrors; private @Nullable Boolean printUncaughtStackTrace; private @Nullable Boolean sendClientReports; private @NotNull Set bundleIds = new CopyOnWriteArraySet<>(); @@ -130,6 +128,8 @@ public final class ExternalOptions { } options.setIdleTimeout(propertiesProvider.getLongProperty("idle-timeout")); + options.setIgnoredErrors(propertiesProvider.getList("ignored-errors")); + options.setEnabled(propertiesProvider.getBooleanProperty("enabled")); options.setEnablePrettySerializationOutput( @@ -373,6 +373,14 @@ public void setIdleTimeout(final @Nullable Long idleTimeout) { this.idleTimeout = idleTimeout; } + public @Nullable List getIgnoredErrors() { + return ignoredErrors; + } + + public void setIgnoredErrors(final @Nullable List ignoredErrors) { + this.ignoredErrors = ignoredErrors; + } + public @Nullable Boolean getSendClientReports() { return sendClientReports; } diff --git a/sentry/src/main/java/io/sentry/SentryClient.java b/sentry/src/main/java/io/sentry/SentryClient.java index 1cfbf60313d..277be87aeef 100644 --- a/sentry/src/main/java/io/sentry/SentryClient.java +++ b/sentry/src/main/java/io/sentry/SentryClient.java @@ -11,12 +11,7 @@ import io.sentry.protocol.SentryTransaction; import io.sentry.transport.ITransport; import io.sentry.transport.RateLimiter; -import io.sentry.util.CheckInUtils; -import io.sentry.util.HintUtils; -import io.sentry.util.Objects; -import io.sentry.util.Random; -import io.sentry.util.SentryRandom; -import io.sentry.util.TracingUtils; +import io.sentry.util.*; import java.io.Closeable; import java.io.IOException; import java.util.ArrayList; @@ -103,7 +98,8 @@ private boolean shouldApplyScopeData(final @NotNull CheckIn event, final @NotNul if (event != null) { final Throwable eventThrowable = event.getThrowable(); - if (eventThrowable != null && options.containsIgnoredExceptionForType(eventThrowable)) { + if (eventThrowable != null + && ExceptionUtils.isIgnored(options.getIgnoredExceptionsForType(), eventThrowable)) { options .getLogger() .log( @@ -115,6 +111,19 @@ private boolean shouldApplyScopeData(final @NotNull CheckIn event, final @NotNul .recordLostEvent(DiscardReason.EVENT_PROCESSOR, DataCategory.Error); return SentryId.EMPTY_ID; } + + if (ErrorUtils.isIgnored(options.getIgnoredErrors(), event)) { + options + .getLogger() + .log( + SentryLevel.DEBUG, + "Event was dropped as it matched a string/pattern in ignoredErrors", + event.getMessage()); + options + .getClientReportRecorder() + .recordLostEvent(DiscardReason.EVENT_PROCESSOR, DataCategory.Error); + return SentryId.EMPTY_ID; + } } if (shouldApplyScopeData(event, hint)) { diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index 3a6d75789a8..2617cd3dfa5 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -69,6 +69,12 @@ public class SentryOptions { private final @NotNull Set> ignoredExceptionsForType = new CopyOnWriteArraySet<>(); + /** + * Strings or regex patterns that possible error messages for an event will be tested against. If + * there is a match, the captured event will not be sent to Sentry. + */ + private @Nullable List ignoredErrors = null; + /** * Code that provides middlewares, bindings or hooks into certain frameworks or environments, * along with code that inserts those bindings and activates them. @@ -1572,6 +1578,55 @@ boolean containsIgnoredExceptionForType(final @NotNull Throwable throwable) { return this.ignoredExceptionsForType.contains(throwable.getClass()); } + /** + * Returns the list of strings/regex patterns that `event.message`, `event.formatted`, and + * `{event.throwable.class.name}: {event.throwable.message}` are checked against to determine if + * an event shall be sent to Sentry or ignored. + * + * @return the list of strings/regex patterns that `event.message`, `event.formatted`, and + * `{event.throwable.class.name}: {event.throwable.message}` are checked against to determine + * if an event shall be sent to Sentry or ignored + */ + public @Nullable List getIgnoredErrors() { + return ignoredErrors; + } + + /** + * Sets the list of strings/regex patterns that `event.message`, `event.formatted`, and + * `{event.throwable.class.name}: {event.throwable.message}` are checked against to determine if + * an event shall be sent to Sentry or ignored. + * + * @param ignoredErrors the list of strings/regex patterns + */ + public void setIgnoredErrors(final @Nullable List ignoredErrors) { + if (ignoredErrors == null) { + this.ignoredErrors = null; + } else { + @NotNull final List patterns = new ArrayList<>(); + for (String pattern : ignoredErrors) { + if (pattern != null && !pattern.isEmpty()) { + patterns.add(new FilterString(pattern)); + } + } + + this.ignoredErrors = patterns; + } + } + + /** + * Adds an item to the list of strings/regex patterns that `event.message`, `event.formatted`, and + * `{event.throwable.class.name}: {event.throwable.message}` are checked against to determine if + * an event shall be sent to Sentry or ignored. + * + * @param pattern the string/regex pattern + */ + public void addIgnoredError(final @NotNull String pattern) { + if (ignoredErrors == null) { + ignoredErrors = new ArrayList<>(); + } + ignoredErrors.add(new FilterString(pattern)); + } + /** * Returns the maximum number of spans that can be attached to single transaction. * @@ -2801,6 +2856,10 @@ public void merge(final @NotNull ExternalOptions options) { final List ignoredTransactions = new ArrayList<>(options.getIgnoredTransactions()); setIgnoredTransactions(ignoredTransactions); } + if (options.getIgnoredErrors() != null) { + final List ignoredExceptions = new ArrayList<>(options.getIgnoredErrors()); + setIgnoredErrors(ignoredExceptions); + } if (options.isEnableBackpressureHandling() != null) { setEnableBackpressureHandling(options.isEnableBackpressureHandling()); } diff --git a/sentry/src/main/java/io/sentry/util/ErrorUtils.java b/sentry/src/main/java/io/sentry/util/ErrorUtils.java new file mode 100644 index 00000000000..cb8b3dbce93 --- /dev/null +++ b/sentry/src/main/java/io/sentry/util/ErrorUtils.java @@ -0,0 +1,57 @@ +package io.sentry.util; + +import io.sentry.FilterString; +import io.sentry.SentryEvent; +import io.sentry.protocol.Message; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public final class ErrorUtils { + + /** Checks if an error has been ignored. */ + @ApiStatus.Internal + public static boolean isIgnored( + final @Nullable List ignoredErrors, final @NotNull SentryEvent event) { + if (event == null || ignoredErrors == null || ignoredErrors.isEmpty()) { + return false; + } + + final @NotNull Set possibleMessages = new HashSet<>(); + + final @Nullable Message eventMessage = event.getMessage(); + if (eventMessage != null) { + final @Nullable String stringMessage = eventMessage.getMessage(); + if (stringMessage != null) { + possibleMessages.add(stringMessage); + } + final @Nullable String formattedMessage = eventMessage.getFormatted(); + if (formattedMessage != null) { + possibleMessages.add(formattedMessage); + } + } + final @Nullable Throwable throwable = event.getThrowable(); + if (throwable != null) { + possibleMessages.add(throwable.toString()); + } + + for (final @NotNull FilterString filter : ignoredErrors) { + if (possibleMessages.contains(filter.getFilterString())) { + return true; + } + } + + for (final @NotNull FilterString filter : ignoredErrors) { + for (final @NotNull String message : possibleMessages) { + if (filter.matches(message)) { + return true; + } + } + } + + return false; + } +} diff --git a/sentry/src/main/java/io/sentry/util/ExceptionUtils.java b/sentry/src/main/java/io/sentry/util/ExceptionUtils.java index 04285751c10..9d6033a96c3 100644 --- a/sentry/src/main/java/io/sentry/util/ExceptionUtils.java +++ b/sentry/src/main/java/io/sentry/util/ExceptionUtils.java @@ -1,5 +1,6 @@ package io.sentry.util; +import java.util.Set; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -20,4 +21,12 @@ public final class ExceptionUtils { } return rootCause; } + + /** Checks if an exception has been ignored. */ + @ApiStatus.Internal + public static boolean isIgnored( + final @NotNull Set> ignoredExceptionsForType, + final @NotNull Throwable throwable) { + return ignoredExceptionsForType.contains(throwable.getClass()); + } } diff --git a/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt b/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt index b25f67405cf..f32b6cf8c01 100644 --- a/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt @@ -209,6 +209,15 @@ class ExternalOptionsTest { } } + @Test + fun `creates options with ignored error patterns using external properties`() { + val logger = mock() + withPropertiesFile("ignored-errors=Some error,Another .*", logger) { options -> + assertTrue(options.ignoredErrors!!.contains("Some error")) + assertTrue(options.ignoredErrors!!.contains("Another .*")) + } + } + @Test fun `creates options with single bundle ID using external properties`() { withPropertiesFile("bundle-ids=12ea7a02-46ac-44c0-a5bb-6d1fd9586411") { options -> diff --git a/sentry/src/test/java/io/sentry/SentryClientTest.kt b/sentry/src/test/java/io/sentry/SentryClientTest.kt index da57f5376e6..63052022ebc 100644 --- a/sentry/src/test/java/io/sentry/SentryClientTest.kt +++ b/sentry/src/test/java/io/sentry/SentryClientTest.kt @@ -15,6 +15,7 @@ import io.sentry.hints.DiskFlushNotification import io.sentry.hints.TransactionEnd import io.sentry.protocol.Contexts import io.sentry.protocol.Mechanism +import io.sentry.protocol.Message import io.sentry.protocol.Request import io.sentry.protocol.SdkVersion import io.sentry.protocol.SentryException @@ -1758,6 +1759,65 @@ class SentryClientTest { verify(fixture.transport, never()).send(any(), anyOrNull()) } + @Test + fun `when event message matches string in ignoredErrors, capturing event does not send it`() { + fixture.sentryOptions.addIgnoredError("hello") + val sut = fixture.getSut() + val event = SentryEvent() + val message = Message() + message.message = "hello" + event.setMessage(message) + sut.captureEvent(event) + verify(fixture.transport, never()).send(any(), anyOrNull()) + } + + @Test + fun `when event message matches regex pattern in ignoredErrors, capturing event does not send it`() { + fixture.sentryOptions.addIgnoredError("hello .*") + val sut = fixture.getSut() + val event = SentryEvent() + val message = Message() + message.message = "hello world" + event.setMessage(message) + sut.captureEvent(event) + verify(fixture.transport, never()).send(any(), anyOrNull()) + } + + @Test + fun `when event message does not match regex pattern in ignoredErrors, capturing event sends it`() { + fixture.sentryOptions.addIgnoredError("hello .*") + val sut = fixture.getSut() + val event = SentryEvent() + val message = Message() + message.message = "test" + event.setMessage(message) + sut.captureEvent(event) + verify(fixture.transport).send(any(), anyOrNull()) + } + + @Test + fun `when exception message matches regex pattern in ignoredErrors, capturing event does not send it`() { + fixture.sentryOptions.addIgnoredError(".*hello .*") + val sut = fixture.getSut() + sut.captureException(RuntimeException("hello world")) + verify(fixture.transport, never()).send(any(), anyOrNull()) + } + + @Test + fun `when class matches regex pattern in ignoredErrors, capturing event does not send it`() { + fixture.sentryOptions.addIgnoredError("java\\.lang\\..*") + val sut = fixture.getSut() + sut.captureException(RuntimeException("hello world")) + verify(fixture.transport, never()).send(any(), anyOrNull()) + } + + @Test + fun `when ignoredExceptionsForType and ignoredErrors are not explicitly specified, capturing event sends event`() { + val sut = fixture.getSut() + sut.captureException(RuntimeException("test")) + verify(fixture.transport).send(any(), anyOrNull()) + } + @Test fun `screenshot is added to the envelope from the hint`() { val sut = fixture.getSut() diff --git a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt index 46482a10833..278c3519162 100644 --- a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt @@ -326,6 +326,7 @@ class SentryOptionsTest { externalOptions.isSendModules = false externalOptions.ignoredCheckIns = listOf("slug1", "slug-B") externalOptions.ignoredTransactions = listOf("transactionName1", "transaction-name-B") + externalOptions.ignoredErrors = listOf("Some error", "Another .*") externalOptions.isEnableBackpressureHandling = false externalOptions.maxRequestBodySize = SentryOptions.RequestSize.MEDIUM externalOptions.isSendDefaultPii = true @@ -370,6 +371,7 @@ class SentryOptionsTest { assertFalse(options.isSendModules) assertEquals(listOf(FilterString("slug1"), FilterString("slug-B")), options.ignoredCheckIns) assertEquals(listOf(FilterString("transactionName1"), FilterString("transaction-name-B")), options.ignoredTransactions) + assertEquals(listOf(FilterString("Some error"), FilterString("Another .*")), options.ignoredErrors) assertFalse(options.isEnableBackpressureHandling) assertTrue(options.isForceInit) assertNotNull(options.cron)