diff --git a/CHANGELOG.md b/CHANGELOG.md index 98b64e35d81..f00f2d73736 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## Unreleased + +### Features + +- Add onDiscard to enable users to track the type and amount of data discarded before reaching Sentry ([#4612](https://github.com/getsentry/sentry-java/pull/4612)) + - Stub for setting the callback on `Sentry.init`: + ```java + Sentry.init(options -> { + ... + options.setOnDiscard( + (reason, category, number) -> { + // Your logic to process discarded data + }); + }); + ``` + ## 8.19.1 ### Fixes diff --git a/sentry-samples/sentry-samples-console/src/main/java/io/sentry/samples/console/Main.java b/sentry-samples/sentry-samples-console/src/main/java/io/sentry/samples/console/Main.java index 7ee46649e97..9d6a8fd7a9e 100644 --- a/sentry-samples/sentry-samples-console/src/main/java/io/sentry/samples/console/Main.java +++ b/sentry-samples/sentry-samples-console/src/main/java/io/sentry/samples/console/Main.java @@ -1,20 +1,15 @@ package io.sentry.samples.console; -import io.sentry.Breadcrumb; -import io.sentry.EventProcessor; -import io.sentry.Hint; -import io.sentry.ISpan; -import io.sentry.ITransaction; -import io.sentry.Sentry; -import io.sentry.SentryEvent; -import io.sentry.SentryLevel; -import io.sentry.SpanStatus; +import io.sentry.*; +import io.sentry.clientreport.DiscardReason; import io.sentry.protocol.Message; import io.sentry.protocol.User; import java.util.Collections; public class Main { + private static long numberOfDiscardedSpansDueToOverflow = 0; + public static void main(String[] args) throws InterruptedException { Sentry.init( options -> { @@ -59,6 +54,18 @@ public static void main(String[] args) throws InterruptedException { return breadcrumb; }); + // Record data being discarded, including the reason, type of data, and the number of + // items dropped + options.setOnDiscard( + (reason, category, number) -> { + // Only record the number of lost spans due to overflow conditions + if ((reason.equals(DiscardReason.CACHE_OVERFLOW) + || reason.equals(DiscardReason.QUEUE_OVERFLOW)) + && category.equals(DataCategory.Span)) { + numberOfDiscardedSpansDueToOverflow += number; + } + }); + // Configure the background worker which sends events to sentry: // Wait up to 5 seconds before shutdown while there are events to send. options.setShutdownTimeoutMillis(5000); diff --git a/sentry-samples/sentry-samples-servlet/src/main/java/io/sentry/samples/servlet/SentryInitializer.java b/sentry-samples/sentry-samples-servlet/src/main/java/io/sentry/samples/servlet/SentryInitializer.java index d007056c66e..9f73b796e69 100644 --- a/sentry-samples/sentry-samples-servlet/src/main/java/io/sentry/samples/servlet/SentryInitializer.java +++ b/sentry-samples/sentry-samples-servlet/src/main/java/io/sentry/samples/servlet/SentryInitializer.java @@ -1,6 +1,8 @@ package io.sentry.samples.servlet; +import io.sentry.DataCategory; import io.sentry.Sentry; +import io.sentry.clientreport.DiscardReason; import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; import javax.servlet.annotation.WebListener; @@ -9,6 +11,8 @@ @WebListener public final class SentryInitializer implements ServletContextListener { + private static long numberOfDiscardedSpansDueToOverflow = 0; + @Override public void contextInitialized(ServletContextEvent sce) { Sentry.init( @@ -57,6 +61,18 @@ public void contextInitialized(ServletContextEvent sce) { return breadcrumb; }); + // Record data being discarded, including the reason, type of data, and the number of + // items dropped + options.setOnDiscard( + (reason, category, number) -> { + // Only record the number of lost spans due to overflow conditions + if ((reason.equals(DiscardReason.CACHE_OVERFLOW) + || reason.equals(DiscardReason.QUEUE_OVERFLOW)) + && category.equals(DataCategory.Span)) { + numberOfDiscardedSpansDueToOverflow += number; + } + }); + // Configure the background worker which sends events to sentry: // Wait up to 5 seconds before shutdown while there are events to send. options.setShutdownTimeoutMillis(5000); diff --git a/sentry-spring-boot-jakarta/src/main/java/io/sentry/spring/boot/jakarta/SentryAutoConfiguration.java b/sentry-spring-boot-jakarta/src/main/java/io/sentry/spring/boot/jakarta/SentryAutoConfiguration.java index fcaaa5f2645..9ae80432eed 100644 --- a/sentry-spring-boot-jakarta/src/main/java/io/sentry/spring/boot/jakarta/SentryAutoConfiguration.java +++ b/sentry-spring-boot-jakarta/src/main/java/io/sentry/spring/boot/jakarta/SentryAutoConfiguration.java @@ -107,6 +107,7 @@ static class HubConfiguration { beforeSendLogsCallback, final @NotNull ObjectProvider beforeBreadcrumbCallback, + final @NotNull ObjectProvider onDiscardCallback, final @NotNull ObjectProvider tracesSamplerCallback, final @NotNull List eventProcessors, final @NotNull List integrations, @@ -118,6 +119,7 @@ static class HubConfiguration { beforeSendTransactionCallback.ifAvailable(options::setBeforeSendTransaction); beforeSendLogsCallback.ifAvailable(callback -> options.getLogs().setBeforeSend(callback)); beforeBreadcrumbCallback.ifAvailable(options::setBeforeBreadcrumb); + onDiscardCallback.ifAvailable(options::setOnDiscard); tracesSamplerCallback.ifAvailable(options::setTracesSampler); eventProcessors.forEach(options::addEventProcessor); integrations.forEach(options::addIntegration); 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 7b290a42880..6c424c12321 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 @@ -4,6 +4,7 @@ import com.acme.MainBootClass import io.opentelemetry.api.OpenTelemetry import io.sentry.AsyncHttpTransportFactory import io.sentry.Breadcrumb +import io.sentry.DataCategory import io.sentry.EventProcessor import io.sentry.FilterString import io.sentry.Hint @@ -19,6 +20,7 @@ import io.sentry.SentryLevel import io.sentry.SentryLogEvent import io.sentry.SentryOptions import io.sentry.checkEvent +import io.sentry.clientreport.DiscardReason import io.sentry.opentelemetry.SentryAutoConfigurationCustomizerProvider import io.sentry.opentelemetry.agent.AgentMarker import io.sentry.protocol.SentryTransaction @@ -373,6 +375,17 @@ class SentryAutoConfigurationTest { } } + @Test + fun `registers onDiscardCallback on SentryOptions`() { + contextRunner + .withPropertyValues("sentry.dsn=http://key@localhost/proj") + .withUserConfiguration(CustomOnDiscardCallbackConfiguration::class.java) + .run { + assertThat(it.getBean(SentryOptions::class.java).onDiscard) + .isInstanceOf(CustomOnDiscardCallback::class.java) + } + } + @Test fun `registers event processor on SentryOptions`() { contextRunner @@ -1137,6 +1150,16 @@ class SentryAutoConfigurationTest { override fun execute(breadcrumb: Breadcrumb, hint: Hint): Breadcrumb? = null } + @Configuration(proxyBeanMethods = false) + open class CustomOnDiscardCallbackConfiguration { + + @Bean open fun onDiscardCallback() = CustomOnDiscardCallback() + } + + class CustomOnDiscardCallback : SentryOptions.OnDiscardCallback { + override fun execute(reason: DiscardReason, category: DataCategory, countToAdd: Long) {} + } + @Configuration(proxyBeanMethods = false) open class CustomEventProcessorConfiguration { diff --git a/sentry-spring-boot/src/main/java/io/sentry/spring/boot/SentryAutoConfiguration.java b/sentry-spring-boot/src/main/java/io/sentry/spring/boot/SentryAutoConfiguration.java index af3abe64807..ec7998eaa88 100644 --- a/sentry-spring-boot/src/main/java/io/sentry/spring/boot/SentryAutoConfiguration.java +++ b/sentry-spring-boot/src/main/java/io/sentry/spring/boot/SentryAutoConfiguration.java @@ -105,6 +105,7 @@ static class HubConfiguration { beforeSendLogsCallback, final @NotNull ObjectProvider beforeBreadcrumbCallback, + final @NotNull ObjectProvider onDiscardCallback, final @NotNull ObjectProvider tracesSamplerCallback, final @NotNull List eventProcessors, final @NotNull List integrations, @@ -116,6 +117,7 @@ static class HubConfiguration { beforeSendTransactionCallback.ifAvailable(options::setBeforeSendTransaction); beforeSendLogsCallback.ifAvailable(callback -> options.getLogs().setBeforeSend(callback)); beforeBreadcrumbCallback.ifAvailable(options::setBeforeBreadcrumb); + onDiscardCallback.ifAvailable(options::setOnDiscard); tracesSamplerCallback.ifAvailable(options::setTracesSampler); eventProcessors.forEach(options::addEventProcessor); integrations.forEach(options::addIntegration); 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 1c47df15bb0..43331e8a267 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 @@ -4,6 +4,7 @@ import com.acme.MainBootClass import io.opentelemetry.api.OpenTelemetry import io.sentry.AsyncHttpTransportFactory import io.sentry.Breadcrumb +import io.sentry.DataCategory import io.sentry.EventProcessor import io.sentry.FilterString import io.sentry.Hint @@ -19,6 +20,7 @@ import io.sentry.SentryLevel import io.sentry.SentryLogEvent import io.sentry.SentryOptions import io.sentry.checkEvent +import io.sentry.clientreport.DiscardReason import io.sentry.opentelemetry.SentryAutoConfigurationCustomizerProvider import io.sentry.opentelemetry.agent.AgentMarker import io.sentry.protocol.SentryTransaction @@ -372,6 +374,17 @@ class SentryAutoConfigurationTest { } } + @Test + fun `registers onDiscardCallback on SentryOptions`() { + contextRunner + .withPropertyValues("sentry.dsn=http://key@localhost/proj") + .withUserConfiguration(CustomOnDiscardCallbackConfiguration::class.java) + .run { + assertThat(it.getBean(SentryOptions::class.java).onDiscard) + .isInstanceOf(CustomOnDiscardCallback::class.java) + } + } + @Test fun `registers event processor on SentryOptions`() { contextRunner @@ -1063,6 +1076,16 @@ class SentryAutoConfigurationTest { override fun execute(breadcrumb: Breadcrumb, hint: Hint): Breadcrumb? = null } + @Configuration(proxyBeanMethods = false) + open class CustomOnDiscardCallbackConfiguration { + + @Bean open fun onDiscardCallback() = CustomOnDiscardCallback() + } + + class CustomOnDiscardCallback : SentryOptions.OnDiscardCallback { + override fun execute(reason: DiscardReason, category: DataCategory, countToAdd: Long) {} + } + @Configuration(proxyBeanMethods = false) open class CustomEventProcessorConfiguration { diff --git a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/SentryInitBeanPostProcessor.java b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/SentryInitBeanPostProcessor.java index d33dfca8d8a..4d9e63a151b 100644 --- a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/SentryInitBeanPostProcessor.java +++ b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/SentryInitBeanPostProcessor.java @@ -61,6 +61,9 @@ public SentryInitBeanPostProcessor() { applicationContext .getBeanProvider(SentryOptions.BeforeBreadcrumbCallback.class) .ifAvailable(options::setBeforeBreadcrumb); + applicationContext + .getBeanProvider(SentryOptions.OnDiscardCallback.class) + .ifAvailable(options::setOnDiscard); applicationContext .getBeansOfType(EventProcessor.class) .values() diff --git a/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/EnableSentryTest.kt b/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/EnableSentryTest.kt index 3fa5a38ac22..52a626d4642 100644 --- a/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/EnableSentryTest.kt +++ b/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/EnableSentryTest.kt @@ -223,6 +223,11 @@ class EnableSentryTest { @Bean fun beforeBreadcrumbCallback() = mock() } + @EnableSentry(dsn = "http://key@localhost/proj") + class AppConfigWithCustomOnDiscardCallback { + @Bean fun onDiscardCallback() = mock() + } + @EnableSentry(dsn = "http://key@localhost/proj") class AppConfigWithCustomEventProcessors { @Bean fun firstProcessor() = mock() diff --git a/sentry-spring/src/main/java/io/sentry/spring/SentryInitBeanPostProcessor.java b/sentry-spring/src/main/java/io/sentry/spring/SentryInitBeanPostProcessor.java index ca431aae149..60cb61cd038 100644 --- a/sentry-spring/src/main/java/io/sentry/spring/SentryInitBeanPostProcessor.java +++ b/sentry-spring/src/main/java/io/sentry/spring/SentryInitBeanPostProcessor.java @@ -61,6 +61,9 @@ public SentryInitBeanPostProcessor() { applicationContext .getBeanProvider(SentryOptions.BeforeBreadcrumbCallback.class) .ifAvailable(options::setBeforeBreadcrumb); + applicationContext + .getBeanProvider(SentryOptions.OnDiscardCallback.class) + .ifAvailable(options::setOnDiscard); applicationContext .getBeansOfType(EventProcessor.class) .values() diff --git a/sentry-spring/src/test/kotlin/io/sentry/spring/EnableSentryTest.kt b/sentry-spring/src/test/kotlin/io/sentry/spring/EnableSentryTest.kt index 0804bf1a118..f2277128ec8 100644 --- a/sentry-spring/src/test/kotlin/io/sentry/spring/EnableSentryTest.kt +++ b/sentry-spring/src/test/kotlin/io/sentry/spring/EnableSentryTest.kt @@ -223,6 +223,11 @@ class EnableSentryTest { @Bean fun beforeBreadcrumbCallback() = mock() } + @EnableSentry(dsn = "http://key@localhost/proj") + class AppConfigWithCustomOnDiscardCallback { + @Bean fun onDiscardCallback() = mock() + } + @EnableSentry(dsn = "http://key@localhost/proj") class AppConfigWithCustomEventProcessors { @Bean fun firstProcessor() = mock() diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 4cf0cd34408..85fdd34a503 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -3340,6 +3340,7 @@ public class io/sentry/SentryOptions { public fun getMaxSpans ()I public fun getMaxTraceFileSize ()J public fun getModulesLoader ()Lio/sentry/internal/modules/IModulesLoader; + public fun getOnDiscard ()Lio/sentry/SentryOptions$OnDiscardCallback; public fun getOpenTelemetryMode ()Lio/sentry/SentryOpenTelemetryMode; public fun getOptionsObservers ()Ljava/util/List; public fun getOutboxPath ()Ljava/lang/String; @@ -3482,6 +3483,7 @@ public class io/sentry/SentryOptions { public fun setMaxSpans (I)V public fun setMaxTraceFileSize (J)V public fun setModulesLoader (Lio/sentry/internal/modules/IModulesLoader;)V + public fun setOnDiscard (Lio/sentry/SentryOptions$OnDiscardCallback;)V public fun setOpenTelemetryMode (Lio/sentry/SentryOpenTelemetryMode;)V public fun setPrintUncaughtStackTrace (Z)V public fun setProfileLifecycle (Lio/sentry/ProfileLifecycle;)V @@ -3575,6 +3577,10 @@ public abstract interface class io/sentry/SentryOptions$Logs$BeforeSendLogCallba public abstract fun execute (Lio/sentry/SentryLogEvent;)Lio/sentry/SentryLogEvent; } +public abstract interface class io/sentry/SentryOptions$OnDiscardCallback { + public abstract fun execute (Lio/sentry/clientreport/DiscardReason;Lio/sentry/DataCategory;Ljava/lang/Long;)V +} + public abstract interface class io/sentry/SentryOptions$ProfilesSamplerCallback { public abstract fun sample (Lio/sentry/SamplingContext;)Ljava/lang/Double; } diff --git a/sentry/src/main/java/io/sentry/DataCategory.java b/sentry/src/main/java/io/sentry/DataCategory.java index f6b13b62485..6f741775b71 100644 --- a/sentry/src/main/java/io/sentry/DataCategory.java +++ b/sentry/src/main/java/io/sentry/DataCategory.java @@ -1,9 +1,7 @@ package io.sentry; -import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; -@ApiStatus.Internal public enum DataCategory { All("__all__"), Default("default"), // same as Error diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index 0a8227408ad..419094067a5 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -6,6 +6,7 @@ import io.sentry.cache.IEnvelopeCache; import io.sentry.cache.PersistingScopeObserver; import io.sentry.clientreport.ClientReportRecorder; +import io.sentry.clientreport.DiscardReason; import io.sentry.clientreport.IClientReportRecorder; import io.sentry.clientreport.NoOpClientReportRecorder; import io.sentry.internal.debugmeta.IDebugMetaLoader; @@ -179,6 +180,9 @@ public class SentryOptions { */ private @Nullable BeforeBreadcrumbCallback beforeBreadcrumb; + /** Invoked when some data from the SDK is dropped before being consumed by Sentry */ + private @Nullable OnDiscardCallback onDiscard; + /** The cache dir. path for caching offline events */ private @Nullable String cacheDirPath; @@ -904,6 +908,24 @@ public void setBeforeBreadcrumb(@Nullable BeforeBreadcrumbCallback beforeBreadcr this.beforeBreadcrumb = beforeBreadcrumb; } + /** + * Returns the onDiscard callback + * + * @return the onDiscard callback or null if not set + */ + public @Nullable OnDiscardCallback getOnDiscard() { + return onDiscard; + } + + /** + * Sets the onDiscard callback + * + * @param onDiscard the onDiscard callback + */ + public void setOnDiscard(@Nullable OnDiscardCallback onDiscard) { + this.onDiscard = onDiscard; + } + /** * Returns the cache dir. path if set * @@ -2982,6 +3004,20 @@ public interface BeforeBreadcrumbCallback { Breadcrumb execute(@NotNull Breadcrumb breadcrumb, @NotNull Hint hint); } + /** The OnDiscard callback */ + public interface OnDiscardCallback { + + /** + * Best-effort record of data discarded before reaching Sentry + * + * @param reason the reason data was dropped + * @param category the type of data discarded + * @param number the number of discarded data items + */ + void execute( + @NotNull DiscardReason reason, @NotNull DataCategory category, @NotNull Long number); + } + /** The traces sampler callback. */ public interface TracesSamplerCallback { diff --git a/sentry/src/main/java/io/sentry/clientreport/ClientReportRecorder.java b/sentry/src/main/java/io/sentry/clientreport/ClientReportRecorder.java index bc91ed143b1..4e2383eb107 100644 --- a/sentry/src/main/java/io/sentry/clientreport/ClientReportRecorder.java +++ b/sentry/src/main/java/io/sentry/clientreport/ClientReportRecorder.java @@ -96,9 +96,11 @@ public void recordLostEnvelopeItem( // since Relay extracts an additional span from the transaction. recordLostEventInternal( reason.getReason(), DataCategory.Span.getCategory(), spans.size() + 1L); + executeOnDiscard(reason, DataCategory.Span, spans.size() + 1L); } } recordLostEventInternal(reason.getReason(), itemCategory.getCategory(), 1L); + executeOnDiscard(reason, itemCategory, 1L); } } catch (Throwable e) { options.getLogger().log(SentryLevel.ERROR, e, "Unable to record lost envelope item."); @@ -115,11 +117,23 @@ public void recordLostEvent( @NotNull DiscardReason reason, @NotNull DataCategory category, long count) { try { recordLostEventInternal(reason.getReason(), category.getCategory(), count); + executeOnDiscard(reason, category, count); } catch (Throwable e) { options.getLogger().log(SentryLevel.ERROR, e, "Unable to record lost event."); } } + private void executeOnDiscard( + @NotNull DiscardReason reason, @NotNull DataCategory category, @NotNull Long countToAdd) { + if (options.getOnDiscard() != null) { + try { + options.getOnDiscard().execute(reason, category, countToAdd); + } catch (Throwable e) { + options.getLogger().log(SentryLevel.ERROR, "The onDiscard callback threw an exception.", e); + } + } + } + private void recordLostEventInternal( @NotNull String reason, @NotNull String category, @NotNull Long countToAdd) { final ClientReportKey key = new ClientReportKey(reason, category); diff --git a/sentry/src/main/java/io/sentry/clientreport/DiscardReason.java b/sentry/src/main/java/io/sentry/clientreport/DiscardReason.java index 91a56cf3135..01031fbb3b7 100644 --- a/sentry/src/main/java/io/sentry/clientreport/DiscardReason.java +++ b/sentry/src/main/java/io/sentry/clientreport/DiscardReason.java @@ -1,8 +1,5 @@ package io.sentry.clientreport; -import org.jetbrains.annotations.ApiStatus; - -@ApiStatus.Internal public enum DiscardReason { QUEUE_OVERFLOW("queue_overflow"), CACHE_OVERFLOW("cache_overflow"), diff --git a/sentry/src/test/java/io/sentry/SentryClientTest.kt b/sentry/src/test/java/io/sentry/SentryClientTest.kt index a56aed6bcac..878eb2d4aa7 100644 --- a/sentry/src/test/java/io/sentry/SentryClientTest.kt +++ b/sentry/src/test/java/io/sentry/SentryClientTest.kt @@ -997,6 +997,9 @@ class SentryClientTest { fun `transaction dropped by beforeSendTransaction is recorded`() { fixture.sentryOptions.setBeforeSendTransaction { transaction, hint -> null } + val onDiscardMock = mock() + fixture.sentryOptions.onDiscard = onDiscardMock + val transaction = SentryTransaction(fixture.sentryTracer) fixture.getSut().captureTransaction(transaction, fixture.sentryTracer.traceContext()) @@ -1008,10 +1011,16 @@ class SentryClientTest { DiscardedEvent(DiscardReason.BEFORE_SEND.reason, DataCategory.Span.category, 2), ), ) + + verify(onDiscardMock, times(1)).execute(DiscardReason.BEFORE_SEND, DataCategory.Transaction, 1) + verify(onDiscardMock).execute(DiscardReason.BEFORE_SEND, DataCategory.Span, 2) } @Test fun `transaction dropped by scope event processor is recorded`() { + val onDiscardMock = mock() + fixture.sentryOptions.onDiscard = onDiscardMock + val transaction = SentryTransaction(fixture.sentryTracer) val scope = createScope() scope.addEventProcessor(DropEverythingEventProcessor()) @@ -1027,10 +1036,17 @@ class SentryClientTest { DiscardedEvent(DiscardReason.EVENT_PROCESSOR.reason, DataCategory.Span.category, 2), ), ) + + verify(onDiscardMock, times(1)) + .execute(DiscardReason.EVENT_PROCESSOR, DataCategory.Transaction, 1) + verify(onDiscardMock).execute(DiscardReason.EVENT_PROCESSOR, DataCategory.Span, 2) } @Test fun `span dropped by event processor is recorded`() { + val onDiscardMock = mock() + fixture.sentryOptions.onDiscard = onDiscardMock + fixture.sentryTracer.startChild("dropped span", "span1").finish() fixture.sentryTracer.startChild("dropped span", "span2").finish() val transaction = SentryTransaction(fixture.sentryTracer) @@ -1053,11 +1069,15 @@ class SentryClientTest { fixture.sentryOptions.clientReportRecorder, listOf(DiscardedEvent(DiscardReason.EVENT_PROCESSOR.reason, DataCategory.Span.category, 2)), ) + + verify(onDiscardMock, times(1)).execute(DiscardReason.EVENT_PROCESSOR, DataCategory.Span, 2) } @Test fun `event dropped by global event processor is recorded`() { fixture.sentryOptions.addEventProcessor(DropEverythingEventProcessor()) + val onDiscardMock = mock() + fixture.sentryOptions.onDiscard = onDiscardMock val event = SentryEvent() @@ -1067,10 +1087,15 @@ class SentryClientTest { fixture.sentryOptions.clientReportRecorder, listOf(DiscardedEvent(DiscardReason.EVENT_PROCESSOR.reason, DataCategory.Error.category, 1)), ) + + verify(onDiscardMock, times(1)).execute(DiscardReason.EVENT_PROCESSOR, DataCategory.Error, 1) } @Test fun `event dropped by scope event processor is recorded`() { + val onDiscardMock = mock() + fixture.sentryOptions.onDiscard = onDiscardMock + assertClientReport( fixture.sentryOptions.clientReportRecorder, listOf(DiscardedEvent(DiscardReason.EVENT_PROCESSOR.reason, DataCategory.Error.category, 0)), @@ -1087,6 +1112,8 @@ class SentryClientTest { fixture.sentryOptions.clientReportRecorder, listOf(DiscardedEvent(DiscardReason.EVENT_PROCESSOR.reason, DataCategory.Error.category, 1)), ) + + verify(onDiscardMock, times(1)).execute(DiscardReason.EVENT_PROCESSOR, DataCategory.Error, 1) } @Test @@ -1106,6 +1133,8 @@ class SentryClientTest { @Test fun `when beforeSendTransaction is returns null, event is dropped`() { fixture.sentryOptions.setBeforeSendTransaction { _: SentryTransaction, _: Any? -> null } + val onDiscardMock = mock() + fixture.sentryOptions.onDiscard = onDiscardMock val transaction = SentryTransaction(fixture.sentryTracer) fixture.getSut().captureTransaction(transaction, fixture.sentryTracer.traceContext()) @@ -1119,6 +1148,9 @@ class SentryClientTest { DiscardedEvent(DiscardReason.BEFORE_SEND.reason, DataCategory.Span.category, 2), ), ) + + verify(onDiscardMock, times(1)).execute(DiscardReason.BEFORE_SEND, DataCategory.Transaction, 1) + verify(onDiscardMock).execute(DiscardReason.BEFORE_SEND, DataCategory.Span, 2) } @Test @@ -1147,6 +1179,9 @@ class SentryClientTest { exception.stackTrace.toString() fixture.sentryOptions.setBeforeSendTransaction { _, _ -> throw exception } + val onDiscardMock = mock() + fixture.sentryOptions.onDiscard = onDiscardMock + val transaction = SentryTransaction(fixture.sentryTracer) val id = fixture.getSut().captureTransaction(transaction, fixture.sentryTracer.traceContext()) @@ -1159,6 +1194,9 @@ class SentryClientTest { DiscardedEvent(DiscardReason.BEFORE_SEND.reason, DataCategory.Span.category, 2), ), ) + + verify(onDiscardMock, times(1)).execute(DiscardReason.BEFORE_SEND, DataCategory.Transaction, 1) + verify(onDiscardMock).execute(DiscardReason.BEFORE_SEND, DataCategory.Span, 2) } @Test @@ -1172,6 +1210,9 @@ class SentryClientTest { } } + val onDiscardMock = mock() + fixture.sentryOptions.onDiscard = onDiscardMock + val transaction = SentryTransaction(fixture.sentryTracer) fixture.getSut().captureTransaction(transaction, fixture.sentryTracer.traceContext()) @@ -1181,6 +1222,8 @@ class SentryClientTest { fixture.sentryOptions.clientReportRecorder, listOf(DiscardedEvent(DiscardReason.BEFORE_SEND.reason, DataCategory.Span.category, 2)), ) + + verify(onDiscardMock, times(1)).execute(DiscardReason.BEFORE_SEND, DataCategory.Span, 2) } @Test @@ -2083,6 +2126,7 @@ class SentryClientTest { @Test fun `ignored exceptions are checked before other filter mechanisms`() { val beforeSendMock = mock() + val onDiscardMock = mock() val scopedEventProcessorMock = mock() val globalEventProcessorMock = mock() @@ -2090,6 +2134,8 @@ class SentryClientTest { whenever(globalEventProcessorMock.process(any(), anyOrNull())).thenReturn(null) whenever(beforeSendMock.execute(any(), anyOrNull())).thenReturn(null) + fixture.sentryOptions.onDiscard = onDiscardMock + val sut = fixture.getSut { options -> options.sampleRate = 0.000000000001 @@ -2110,14 +2156,19 @@ class SentryClientTest { fixture.sentryOptions.clientReportRecorder, listOf(DiscardedEvent(DiscardReason.EVENT_PROCESSOR.reason, DataCategory.Error.category, 1)), ) + + verify(onDiscardMock, times(1)).execute(DiscardReason.EVENT_PROCESSOR, DataCategory.Error, 1) } @Test fun `sampling is last filter mechanism`() { val beforeSendMock = mock() + val onDiscardMock = mock() val scopedEventProcessorMock = mock() val globalEventProcessorMock = mock() + fixture.sentryOptions.onDiscard = onDiscardMock + whenever(scopedEventProcessorMock.process(any(), anyOrNull())).doAnswer { it.arguments.first() as SentryEvent } @@ -2150,14 +2201,19 @@ class SentryClientTest { fixture.sentryOptions.clientReportRecorder, listOf(DiscardedEvent(DiscardReason.SAMPLE_RATE.reason, DataCategory.Error.category, 1)), ) + + verify(onDiscardMock, times(1)).execute(DiscardReason.SAMPLE_RATE, DataCategory.Error, 1) } @Test fun `filter mechanism order check for beforeSend`() { val beforeSendMock = mock() + val onDiscardMock = mock() val scopedEventProcessorMock = mock() val globalEventProcessorMock = mock() + fixture.sentryOptions.onDiscard = onDiscardMock + whenever(scopedEventProcessorMock.process(any(), anyOrNull())).doAnswer { it.arguments.first() as SentryEvent } @@ -2188,14 +2244,19 @@ class SentryClientTest { fixture.sentryOptions.clientReportRecorder, listOf(DiscardedEvent(DiscardReason.BEFORE_SEND.reason, DataCategory.Error.category, 1)), ) + + verify(onDiscardMock, times(1)).execute(DiscardReason.BEFORE_SEND, DataCategory.Error, 1) } @Test fun `filter mechanism order check for scoped eventProcessor`() { val beforeSendMock = mock() + val onDiscardMock = mock() val scopedEventProcessorMock = mock() val globalEventProcessorMock = mock() + fixture.sentryOptions.onDiscard = onDiscardMock + whenever(scopedEventProcessorMock.process(any(), anyOrNull())).thenReturn(null) whenever(globalEventProcessorMock.process(any(), anyOrNull())).thenReturn(null) whenever(beforeSendMock.execute(any(), anyOrNull())).thenReturn(null) @@ -2222,14 +2283,19 @@ class SentryClientTest { fixture.sentryOptions.clientReportRecorder, listOf(DiscardedEvent(DiscardReason.EVENT_PROCESSOR.reason, DataCategory.Error.category, 1)), ) + + verify(onDiscardMock, times(1)).execute(DiscardReason.EVENT_PROCESSOR, DataCategory.Error, 1) } @Test fun `filter mechanism order check for global eventProcessor`() { val beforeSendMock = mock() + val onDiscardMock = mock() val scopedEventProcessorMock = mock() val globalEventProcessorMock = mock() + fixture.sentryOptions.onDiscard = onDiscardMock + whenever(scopedEventProcessorMock.process(any(), anyOrNull())).doAnswer { it.arguments.first() as SentryEvent } @@ -2258,6 +2324,8 @@ class SentryClientTest { fixture.sentryOptions.clientReportRecorder, listOf(DiscardedEvent(DiscardReason.EVENT_PROCESSOR.reason, DataCategory.Error.category, 1)), ) + + verify(onDiscardMock, times(1)).execute(DiscardReason.EVENT_PROCESSOR, DataCategory.Error, 1) } @Test @@ -2900,6 +2968,10 @@ class SentryClientTest { @Test fun `when replay event is dropped, captures client report with datacategory replay`() { + val onDiscardMock = mock() + + fixture.sentryOptions.onDiscard = onDiscardMock + fixture.sentryOptions.addEventProcessor(DropEverythingEventProcessor()) val sut = fixture.getSut() val replayEvent = createReplayEvent() @@ -2910,6 +2982,8 @@ class SentryClientTest { fixture.sentryOptions.clientReportRecorder, listOf(DiscardedEvent(DiscardReason.EVENT_PROCESSOR.reason, DataCategory.Replay.category, 1)), ) + + verify(onDiscardMock, times(1)).execute(DiscardReason.EVENT_PROCESSOR, DataCategory.Replay, 1) } @Test @@ -3056,6 +3130,9 @@ class SentryClientTest { fun `when beforeSendReplay returns null, event is dropped`() { fixture.sentryOptions.setBeforeSendReplay { replay: SentryReplayEvent, _: Hint -> null } + val onDiscardMock = mock() + fixture.sentryOptions.onDiscard = onDiscardMock + fixture.getSut().captureReplayEvent(SentryReplayEvent(), Scope(fixture.sentryOptions), Hint()) verify(fixture.transport, never()).send(any(), anyOrNull()) @@ -3064,6 +3141,8 @@ class SentryClientTest { fixture.sentryOptions.clientReportRecorder, listOf(DiscardedEvent(DiscardReason.BEFORE_SEND.reason, DataCategory.Replay.category, 1)), ) + + verify(onDiscardMock, times(1)).execute(DiscardReason.BEFORE_SEND, DataCategory.Replay, 1) } @Test @@ -3091,6 +3170,9 @@ class SentryClientTest { exception.stackTrace.toString() fixture.sentryOptions.setBeforeSendReplay { _, _ -> throw exception } + val onDiscardMock = mock() + fixture.sentryOptions.onDiscard = onDiscardMock + val id = fixture.getSut().captureReplayEvent(SentryReplayEvent(), Scope(fixture.sentryOptions), Hint()) @@ -3100,6 +3182,8 @@ class SentryClientTest { fixture.sentryOptions.clientReportRecorder, listOf(DiscardedEvent(DiscardReason.BEFORE_SEND.reason, DataCategory.Replay.category, 1)), ) + + verify(onDiscardMock, times(1)).execute(DiscardReason.BEFORE_SEND, DataCategory.Replay, 1) } // endregion @@ -3230,6 +3314,9 @@ class SentryClientTest { @Test fun `when beforeSendFeedback returns null, feedback is dropped`() { fixture.sentryOptions.setBeforeSendFeedback { event: SentryEvent, _: Hint -> null } + val onDiscardMock = mock() + fixture.sentryOptions.onDiscard = onDiscardMock + fixture.getSut().captureFeedback(Feedback("message"), null, createScope()) verify(fixture.transport, never()).send(any(), anyOrNull()) @@ -3237,6 +3324,8 @@ class SentryClientTest { fixture.sentryOptions.clientReportRecorder, listOf(DiscardedEvent(DiscardReason.BEFORE_SEND.reason, DataCategory.Feedback.category, 1)), ) + + verify(onDiscardMock, times(1)).execute(DiscardReason.BEFORE_SEND, DataCategory.Feedback, 1) } @Test @@ -3261,6 +3350,10 @@ class SentryClientTest { fun `when beforeSendFeedback throws an exception, feedback is dropped`() { val exception = Exception("test") fixture.sentryOptions.setBeforeSendFeedback { _, _ -> throw exception } + + val onDiscardMock = mock() + fixture.sentryOptions.onDiscard = onDiscardMock + val id = fixture.getSut().captureFeedback(Feedback("message"), null, Scope(fixture.sentryOptions)) assertEquals(SentryId.EMPTY_ID, id) @@ -3269,11 +3362,16 @@ class SentryClientTest { fixture.sentryOptions.clientReportRecorder, listOf(DiscardedEvent(DiscardReason.BEFORE_SEND.reason, DataCategory.Feedback.category, 1)), ) + + verify(onDiscardMock, times(1)).execute(DiscardReason.BEFORE_SEND, DataCategory.Feedback, 1) } @Test fun `when feedback is dropped, captures client report with datacategory feedback`() { fixture.sentryOptions.addEventProcessor(DropEverythingEventProcessor()) + val onDiscardMock = mock() + fixture.sentryOptions.onDiscard = onDiscardMock + val sut = fixture.getSut() sut.captureFeedback(Feedback("message"), null, createScope()) @@ -3283,6 +3381,8 @@ class SentryClientTest { DiscardedEvent(DiscardReason.EVENT_PROCESSOR.reason, DataCategory.Feedback.category, 1) ), ) + + verify(onDiscardMock, times(1)).execute(DiscardReason.EVENT_PROCESSOR, DataCategory.Feedback, 1) } // endregion diff --git a/sentry/src/test/java/io/sentry/clientreport/ClientReportTest.kt b/sentry/src/test/java/io/sentry/clientreport/ClientReportTest.kt index c3734c429c0..2b34fd839b0 100644 --- a/sentry/src/test/java/io/sentry/clientreport/ClientReportTest.kt +++ b/sentry/src/test/java/io/sentry/clientreport/ClientReportTest.kt @@ -41,6 +41,8 @@ import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue import org.mockito.kotlin.mock +import org.mockito.kotlin.times +import org.mockito.kotlin.verify import org.mockito.kotlin.whenever class ClientReportTest { @@ -303,6 +305,48 @@ class ClientReportTest { ) } + @Test + fun `recording envelope with lost client report does not duplicate onDiscard executions`() { + val onDiscardMock = mock() + givenClientReportRecorder { options -> options.onDiscard = onDiscardMock } + + clientReportRecorder.recordLostEvent(DiscardReason.CACHE_OVERFLOW, DataCategory.Attachment) + clientReportRecorder.recordLostEvent(DiscardReason.CACHE_OVERFLOW, DataCategory.Attachment) + clientReportRecorder.recordLostEvent(DiscardReason.RATELIMIT_BACKOFF, DataCategory.Error) + clientReportRecorder.recordLostEvent(DiscardReason.QUEUE_OVERFLOW, DataCategory.Error) + clientReportRecorder.recordLostEvent(DiscardReason.BEFORE_SEND, DataCategory.Profile) + + val envelope = clientReportRecorder.attachReportToEnvelope(testHelper.newEnvelope()) + clientReportRecorder.recordLostEnvelope(DiscardReason.EVENT_PROCESSOR, envelope) + + verify(onDiscardMock, times(2)) + .execute(DiscardReason.CACHE_OVERFLOW, DataCategory.Attachment, 1) + verify(onDiscardMock, times(1)).execute(DiscardReason.RATELIMIT_BACKOFF, DataCategory.Error, 1) + verify(onDiscardMock, times(1)).execute(DiscardReason.QUEUE_OVERFLOW, DataCategory.Error, 1) + verify(onDiscardMock, times(1)).execute(DiscardReason.BEFORE_SEND, DataCategory.Profile, 1) + } + + @Test + fun `recording lost client report does not duplicate onDiscard executions`() { + val onDiscardMock = mock() + givenClientReportRecorder { options -> options.onDiscard = onDiscardMock } + + clientReportRecorder.recordLostEvent(DiscardReason.CACHE_OVERFLOW, DataCategory.Attachment) + clientReportRecorder.recordLostEvent(DiscardReason.CACHE_OVERFLOW, DataCategory.Attachment) + clientReportRecorder.recordLostEvent(DiscardReason.RATELIMIT_BACKOFF, DataCategory.Error) + clientReportRecorder.recordLostEvent(DiscardReason.QUEUE_OVERFLOW, DataCategory.Error) + clientReportRecorder.recordLostEvent(DiscardReason.BEFORE_SEND, DataCategory.Profile) + + val envelope = clientReportRecorder.attachReportToEnvelope(testHelper.newEnvelope()) + clientReportRecorder.recordLostEnvelopeItem(DiscardReason.NETWORK_ERROR, envelope.items.first()) + + verify(onDiscardMock, times(2)) + .execute(DiscardReason.CACHE_OVERFLOW, DataCategory.Attachment, 1) + verify(onDiscardMock, times(1)).execute(DiscardReason.RATELIMIT_BACKOFF, DataCategory.Error, 1) + verify(onDiscardMock, times(1)).execute(DiscardReason.QUEUE_OVERFLOW, DataCategory.Error, 1) + verify(onDiscardMock, times(1)).execute(DiscardReason.BEFORE_SEND, DataCategory.Profile, 1) + } + private fun givenClientReportRecorder( callback: Sentry.OptionsConfiguration? = null ) {