Skip to content
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@

Breaking Changes:
* Enchancement: SentryExceptionResolver should not send handled errors by default (#1248).
* Ref: Optimize DuplicateEventDetectionEventProcessor performance (#1247).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

should also go under the breaking change section

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It's changing the algorithm that sits under the hood. I am not sure why this would be qualified as a breaking change.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

its a behavior change, there's a bufferSize now and there is the possibility to report the same exception twice, right?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Unlikely, only if applications sends hundreds/thousands errors per second. Buffer size is hidden from the user. I can add it to breaking change if you like but I don't believe we should bump a version because of this.

* Enchancement: Add overloads for startTransaction taking op and description (#1244)


# 4.1.0

* Improve Kotlin compatibility for SdkVersion (#1213)
Expand Down
2 changes: 2 additions & 0 deletions sentry/api/sentry.api
Original file line number Diff line number Diff line change
Expand Up @@ -750,6 +750,7 @@ public class io/sentry/SentryOptions {
public fun isAttachStacktrace ()Z
public fun isAttachThreads ()Z
public fun isDebug ()Z
public fun isEnableDeduplication ()Z
public fun isEnableExternalConfiguration ()Z
public fun isEnableNdk ()Z
public fun isEnableScopeSync ()Z
Expand All @@ -769,6 +770,7 @@ public class io/sentry/SentryOptions {
public fun setDist (Ljava/lang/String;)V
public fun setDistinctId (Ljava/lang/String;)V
public fun setDsn (Ljava/lang/String;)V
public fun setEnableDeduplication (Ljava/lang/Boolean;)V
public fun setEnableExternalConfiguration (Z)V
public fun setEnableNdk (Z)V
public fun setEnableScopeSync (Z)V
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import io.sentry.util.Objects;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
Expand All @@ -10,7 +11,8 @@

/** Deduplicates events containing throwable that has been already processed. */
public final class DuplicateEventDetectionEventProcessor implements EventProcessor {
private final WeakHashMap<Throwable, Object> capturedObjects = new WeakHashMap<>();
private final Map<Throwable, Object> capturedObjects =
Collections.synchronizedMap(new WeakHashMap<>());
private final SentryOptions options;

public DuplicateEventDetectionEventProcessor(final @NotNull SentryOptions options) {
Expand All @@ -19,20 +21,24 @@ public DuplicateEventDetectionEventProcessor(final @NotNull SentryOptions option

@Override
public SentryEvent process(final @NotNull SentryEvent event, final @Nullable Object hint) {
final Throwable throwable = event.getOriginThrowable();
if (throwable != null) {
if (capturedObjects.containsKey(throwable)
|| containsAnyKey(capturedObjects, allCauses(throwable))) {
options
.getLogger()
.log(
SentryLevel.DEBUG,
"Duplicate Exception detected. Event %s will be discarded.",
event.getEventId());
return null;
} else {
capturedObjects.put(throwable, null);
if (options.isEnableDeduplication()) {
Comment thread
marandaneto marked this conversation as resolved.
final Throwable throwable = event.getOriginThrowable();
if (throwable != null) {
if (capturedObjects.containsKey(throwable)
|| containsAnyKey(capturedObjects, allCauses(throwable))) {
options
.getLogger()
.log(
SentryLevel.DEBUG,
"Duplicate Exception detected. Event %s will be discarded.",
event.getEventId());
return null;
} else {
capturedObjects.put(throwable, null);
}
}
} else {
options.getLogger().log(SentryLevel.DEBUG, "Event deduplication is disabled.");
}
return event;
}
Expand Down
38 changes: 38 additions & 0 deletions sentry/src/main/java/io/sentry/SentryOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,13 @@ public class SentryOptions {
/** max attachment size in bytes. */
private long maxAttachmentSize = 20 * 1024 * 1024;

/**
* Enables event deduplication with {@link DuplicateEventDetectionEventProcessor}. Event
* deduplication prevents from receiving the same exception multiple times when there is more than
* one framework active that captures errors, for example Logback and Spring Boot.
*/
private Boolean enableDeduplication = true;

/**
* Creates {@link SentryOptions} from properties provided by a {@link PropertiesProvider}.
*
Expand All @@ -271,6 +278,7 @@ public class SentryOptions {
propertiesProvider.getBooleanProperty("uncaught.handler.enabled"));
options.setTracesSampleRate(propertiesProvider.getDoubleProperty("traces-sample-rate"));
options.setDebug(propertiesProvider.getBooleanProperty("debug"));
options.setEnableDeduplication(propertiesProvider.getBooleanProperty("enable-deduplication"));
final Map<String, String> tags = propertiesProvider.getMap("tags");
for (final Map.Entry<String, String> tag : tags.entrySet()) {
options.setTag(tag.getKey(), tag.getValue());
Expand Down Expand Up @@ -1224,6 +1232,33 @@ public void setMaxAttachmentSize(long maxAttachmentSize) {
this.maxAttachmentSize = maxAttachmentSize;
}

/**
* Returns if event deduplication is turned on.
*
* @return if event deduplication is turned on.
*/
public boolean isEnableDeduplication() {
return Boolean.TRUE.equals(enableDeduplication);
}

/**
* Returns if event deduplication is turned on or of or {@code null} if not specified.
*
* @return if event deduplication is turned on or of or {@code null} if not specified.
*/
private @Nullable Boolean getEnableDeduplication() {
return enableDeduplication;
}

/**
* Enables or disables event deduplication.
*
* @param enableDeduplication true if enabled false otherwise
*/
public void setEnableDeduplication(final @Nullable Boolean enableDeduplication) {
this.enableDeduplication = enableDeduplication;
}

/** The BeforeSend callback */
public interface BeforeSendCallback {

Expand Down Expand Up @@ -1318,6 +1353,9 @@ void merge(final @NotNull SentryOptions options) {
if (options.getDebug() != null) {
setDebug(options.getDebug());
}
if (options.getEnableDeduplication() != null) {
setEnableDeduplication(options.getEnableDeduplication());
}
final Map<String, String> tags = new HashMap<>(options.getTags());
for (final Map.Entry<String, String> tag : tags.entrySet()) {
this.tags.put(tag.getKey(), tag.getValue());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,22 @@ import kotlin.test.assertNull

class DuplicateEventDetectionEventProcessorTest {

val processor = DuplicateEventDetectionEventProcessor(SentryOptions())
class Fixture {
fun getSut(enableDeduplication: Boolean? = null): DuplicateEventDetectionEventProcessor {
val options = SentryOptions().apply {
if (enableDeduplication != null) {
this.setEnableDeduplication(enableDeduplication)
}
}
Comment on lines +14 to +18

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

you could init SentryOptions outside of getSut but still inside of Fixture and call options setters directly on each test, I think that's the way we've been doing so far.
getSut only takes params if the Sut (eg DuplicateEventDetectionEventProcessor) itself requires those params

return DuplicateEventDetectionEventProcessor(options)
}
}

var fixture = Fixture()

@Test
fun `does not drop event if no previous event with same exception was processed`() {
val processor = fixture.getSut()
processor.process(SentryEvent(), null)

val result = processor.process(SentryEvent(RuntimeException()), null)
Expand All @@ -22,6 +34,7 @@ class DuplicateEventDetectionEventProcessorTest {

@Test
fun `drops event with the same exception`() {
val processor = fixture.getSut()
val event = SentryEvent(RuntimeException())
processor.process(event, null)

Expand All @@ -31,6 +44,7 @@ class DuplicateEventDetectionEventProcessorTest {

@Test
fun `drops event with mechanism exception having an exception that has already been processed`() {
val processor = fixture.getSut()
val event = SentryEvent(RuntimeException())
processor.process(event, null)

Expand All @@ -40,6 +54,7 @@ class DuplicateEventDetectionEventProcessorTest {

@Test
fun `drops event with exception that has already been processed with event with mechanism exception`() {
val processor = fixture.getSut()
val sentryEvent = SentryEvent(ExceptionMechanismException(Mechanism(), RuntimeException(), Thread.currentThread()))
processor.process(sentryEvent, null)

Expand All @@ -50,6 +65,7 @@ class DuplicateEventDetectionEventProcessorTest {

@Test
fun `drops event with the cause equal to exception in already processed event`() {
val processor = fixture.getSut()
val event = SentryEvent(RuntimeException())
processor.process(event, null)

Expand All @@ -60,11 +76,20 @@ class DuplicateEventDetectionEventProcessorTest {

@Test
fun `drops event with any of the causes has been already processed`() {
val processor = fixture.getSut()
val event = SentryEvent(RuntimeException())
processor.process(event, null)

val result = processor.process(SentryEvent(RuntimeException(RuntimeException(event.throwable))), null)

assertNull(result)
}

@Test
fun `does not deduplicate is deduplication is disabled`() {
val processor = fixture.getSut(enableDeduplication = false)
val event = SentryEvent(RuntimeException())
assertNotNull(processor.process(event, null))
assertNotNull(processor.process(event, null))
}
}
18 changes: 18 additions & 0 deletions sentry/src/test/java/io/sentry/SentryOptionsTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,24 @@ class SentryOptionsTest {
}
}

@Test
fun `creates options with enableDeduplication using external properties`() {
// create a sentry.properties file in temporary folder
val temporaryFolder = TemporaryFolder()
temporaryFolder.create()
val file = temporaryFolder.newFile("sentry.properties")
file.appendText("enable-deduplication=true")
// set location of the sentry.properties file
System.setProperty("sentry.properties.file", file.absolutePath)

try {
val options = SentryOptions.from(PropertiesProviderFactory.create())
assertTrue(options.isEnableDeduplication)
} finally {
temporaryFolder.delete()
}
}

@Test
fun `when options are initialized, maxAttachmentSize is 20`() {
assertEquals(20 * 1024 * 1024, SentryOptions().maxAttachmentSize)
Expand Down