Skip to content
Draft
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@
- The form never used `AlertDialog` features. Code that calls `AlertDialog`-only methods on it, such as `setButton` or `setMessage`, no longer compiles.
- `sentry-android` no longer includes `sentry-android-ndk` as a default dependency. Instead, tombstone reporting is enabled by default, which provides more detailed native crashes on Android 11+ (API 30). If you're using sentry-android-ndk APIs, or need native crashes on Android 10 and lower, manually add the `io.sentry:sentry-android-ndk` dependency to your app. ([#6436](https://github.com/getsentry/sentry-java/pull/6436))

### Fixes

- Preserve independent Netty read timeout events when the exception singleton is reused ([#6465](https://github.com/getsentry/sentry-java/pull/6465))

### Improvements

- Promote check-in APIs from experimental to stable ([#6422](https://github.com/getsentry/sentry-java/pull/6422))
Expand Down
3 changes: 3 additions & 0 deletions sentry/api/sentry.api
Original file line number Diff line number Diff line change
Expand Up @@ -3480,6 +3480,7 @@ public class io/sentry/SentryOptions : io/sentry/transport/RateLimiterConfig {
public fun addEventProcessor (Lio/sentry/EventProcessor;)V
public fun addIgnoredCheckIn (Ljava/lang/String;)V
public fun addIgnoredError (Ljava/lang/String;)V
public fun addIgnoredErrorForDeduplication (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
Expand Down Expand Up @@ -3537,6 +3538,7 @@ public class io/sentry/SentryOptions : io/sentry/transport/RateLimiterConfig {
public fun getIdleTimeout ()Ljava/lang/Long;
public fun getIgnoredCheckIns ()Ljava/util/List;
public fun getIgnoredErrors ()Ljava/util/List;
public fun getIgnoredErrorsForDeduplication ()Ljava/util/List;
public fun getIgnoredExceptionsForType ()Ljava/util/Set;
public fun getIgnoredSpanOrigins ()Ljava/util/List;
public fun getIgnoredTransactions ()Ljava/util/List;
Expand Down Expand Up @@ -3708,6 +3710,7 @@ public class io/sentry/SentryOptions : io/sentry/transport/RateLimiterConfig {
public fun setIdleTimeout (Ljava/lang/Long;)V
public fun setIgnoredCheckIns (Ljava/util/List;)V
public fun setIgnoredErrors (Ljava/util/List;)V
public fun setIgnoredErrorsForDeduplication (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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,17 @@ public DuplicateEventDetectionEventProcessor(final @NotNull SentryOptions option
if (options.isEnableDeduplication()) {
final Throwable throwable = event.getThrowable();
if (throwable != null) {
if (capturedObjects.containsKey(throwable)
|| containsAnyKey(capturedObjects, allCauses(throwable))) {
final boolean ignoredForDeduplication = isIgnoredForDeduplication(throwable);
if ((!ignoredForDeduplication && capturedObjects.containsKey(throwable))
|| containsAnyKey(allCauses(throwable))) {
options
.getLogger()
.log(
SentryLevel.DEBUG,
"Duplicate Exception detected. Event %s will be discarded.",
event.getEventId());
return null;
} else {
} else if (!ignoredForDeduplication) {
capturedObjects.put(throwable, null);
}
}
Expand All @@ -45,10 +46,25 @@ public DuplicateEventDetectionEventProcessor(final @NotNull SentryOptions option
return event;
}

private static <T> boolean containsAnyKey(
final @NotNull Map<T, Object> map, final @NotNull List<T> list) {
for (T entry : list) {
if (map.containsKey(entry)) {
private boolean containsAnyKey(final @NotNull List<Throwable> throwables) {
for (final Throwable throwable : throwables) {
if (!isIgnoredForDeduplication(throwable) && capturedObjects.containsKey(throwable)) {
return true;
}
}
return false;
}

private boolean isIgnoredForDeduplication(final @NotNull Throwable throwable) {
final @Nullable List<FilterString> ignoredErrors = options.getIgnoredErrorsForDeduplication();
if (ignoredErrors == null || ignoredErrors.isEmpty()) {
return false;
}

final String exceptionClassName = throwable.getClass().getName();
for (final FilterString ignoredError : ignoredErrors) {
if (ignoredError.getFilterString().equals(exceptionClassName)
|| ignoredError.matches(exceptionClassName)) {
return true;
}
}
Expand Down
51 changes: 51 additions & 0 deletions sentry/src/main/java/io/sentry/SentryOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ public class SentryOptions implements RateLimiterConfig {
*/
private static final String DEFAULT_ENVIRONMENT = "production";

private static final String DEFAULT_IGNORED_ERROR_FOR_DEDUPLICATION =
"io.netty.handler.timeout.ReadTimeoutException";

/**
* Are callbacks that run for every event. They can either return a new event which in most cases
* means just adding data OR return null in case the event will be dropped and not sent.
Expand Down Expand Up @@ -387,6 +390,11 @@ public class SentryOptions implements RateLimiterConfig {
*/
private boolean enableDeduplication = true;

/** Exception types that are excluded from event deduplication. */
private @Nullable List<FilterString> ignoredErrorsForDeduplication =
new ArrayList<>(
Collections.singletonList(new FilterString(DEFAULT_IGNORED_ERROR_FOR_DEDUPLICATION)));

/**
* Enables event size limiting with {@link EventSizeLimitingEventProcessor}. When enabled, events
* exceeding 1MB will have breadcrumbs and stack frames reduced to stay under the limit.
Expand Down Expand Up @@ -1947,6 +1955,49 @@ public void setEnableDeduplication(final boolean enableDeduplication) {
this.enableDeduplication = enableDeduplication;
}

/**
* Returns the list of exception class names excluded from event deduplication.
*
* @return the list of exception class names excluded from event deduplication
*/
public @Nullable List<FilterString> getIgnoredErrorsForDeduplication() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if we should mark those APIs as internal for now, unless someone requests access.

return ignoredErrorsForDeduplication;
}

/**
* Sets the list of exception class names excluded from event deduplication. Entries may be exact
* class names or regular expressions.
*
* @param ignoredErrorsForDeduplication exception class names or regular expressions
*/
public void setIgnoredErrorsForDeduplication(
final @Nullable List<String> ignoredErrorsForDeduplication) {
if (ignoredErrorsForDeduplication == null) {
this.ignoredErrorsForDeduplication = null;
} else {
final @NotNull List<FilterString> patterns = new ArrayList<>();
for (final String pattern : ignoredErrorsForDeduplication) {
if (pattern != null && !pattern.isEmpty()) {
patterns.add(new FilterString(pattern));
}
}
this.ignoredErrorsForDeduplication = patterns;
}
}

/**
* Adds an exception class name or regular expression to the list excluded from event
* deduplication.
*
* @param pattern the exception class name or regular expression
*/
public void addIgnoredErrorForDeduplication(final @NotNull String pattern) {
if (ignoredErrorsForDeduplication == null) {
ignoredErrorsForDeduplication = new ArrayList<>();
}
ignoredErrorsForDeduplication.add(new FilterString(pattern));
}

/**
* Returns if event size limiting is enabled.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.sentry

import com.google.common.truth.Truth.assertThat
import io.sentry.exception.ExceptionMechanismException
import io.sentry.protocol.Mechanism
import java.lang.RuntimeException
Expand All @@ -8,6 +9,8 @@ import kotlin.test.assertNotNull
import kotlin.test.assertNull

class DuplicateEventDetectionEventProcessorTest {
private class IgnoredForDeduplicationException(cause: Throwable? = null) : RuntimeException(cause)

private class CircularCauseThrowable : RuntimeException() {
private var nextCause: Throwable? = null
private var causeReads = 0
Expand Down Expand Up @@ -57,6 +60,58 @@ class DuplicateEventDetectionEventProcessorTest {
assertNull(result)
}

@Test
fun `does not add ignored exception to captured exceptions`() {
val options =
SentryOptions().apply {
setIgnoredErrorsForDeduplication(listOf(IgnoredForDeduplicationException::class.java.name))
}
val processor = DuplicateEventDetectionEventProcessor(options)
val event = SentryEvent(IgnoredForDeduplicationException())

assertThat(processor.process(event, Hint())).isNotNull()
options.setIgnoredErrorsForDeduplication(emptyList())
assertThat(processor.process(event, Hint())).isNotNull()
assertThat(processor.process(event, Hint())).isNull()
}

@Test
fun `does not check ignored exception against captured exceptions`() {
val options = SentryOptions().apply { setIgnoredErrorsForDeduplication(emptyList()) }
val processor = DuplicateEventDetectionEventProcessor(options)
val event = SentryEvent(IgnoredForDeduplicationException())
assertThat(processor.process(event, Hint())).isNotNull()

options.addIgnoredErrorForDeduplication(IgnoredForDeduplicationException::class.java.name)

assertThat(processor.process(event, Hint())).isNotNull()
}

@Test
fun `does not check ignored cause against captured exceptions`() {
val options = SentryOptions().apply { setIgnoredErrorsForDeduplication(emptyList()) }
val processor = DuplicateEventDetectionEventProcessor(options)
val cause = IgnoredForDeduplicationException()
assertThat(processor.process(SentryEvent(cause), Hint())).isNotNull()

options.addIgnoredErrorForDeduplication(IgnoredForDeduplicationException::class.java.name)

assertThat(processor.process(SentryEvent(RuntimeException(cause)), Hint())).isNotNull()
}

@Test
fun `ignored exception patterns are matched against the fully qualified class name`() {
val options =
SentryOptions().apply {
setIgnoredErrorsForDeduplication(listOf(".*IgnoredForDeduplicationException"))
}
val processor = DuplicateEventDetectionEventProcessor(options)
val event = SentryEvent(IgnoredForDeduplicationException())

assertThat(processor.process(event, Hint())).isNotNull()
assertThat(processor.process(event, Hint())).isNotNull()
}

@Test
fun `drops event with mechanism exception having an exception that has already been processed`() {
val processor = fixture.getSut()
Expand Down
21 changes: 21 additions & 0 deletions sentry/src/test/java/io/sentry/SentryOptionsTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -1049,6 +1049,27 @@ class SentryOptionsTest {
assertEquals(SentryOptions().initPriority, InitPriority.MEDIUM)
}

@Test
fun `ignored errors for deduplication contains Netty read timeout by default`() {
assertThat(SentryOptions().ignoredErrorsForDeduplication)
.containsExactly(FilterString("io.netty.handler.timeout.ReadTimeoutException"))
}

@Test
fun `ignored errors for deduplication can be replaced and extended`() {
val options = SentryOptions()

options.setIgnoredErrorsForDeduplication(listOf("example.FirstException", ""))
options.addIgnoredErrorForDeduplication("example.SecondException")

assertThat(options.ignoredErrorsForDeduplication)
.containsExactly(
FilterString("example.FirstException"),
FilterString("example.SecondException"),
)
.inOrder()
}

@Test
fun `merging options when ignoredErrors is not set preserves the previous value`() {
val externalOptions = ExternalOptions()
Expand Down
Loading