From dd8f7bbe17fb836a2d4bf97b7398631bd0f20ad3 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Thu, 7 Aug 2025 13:21:41 +0200 Subject: [PATCH 01/10] Add callback to record the type and amount of data discarded before reaching Sentry --- CHANGELOG.md | 4 + .../java/io/sentry/samples/console/Main.java | 25 ++-- .../samples/servlet/SentryInitializer.java | 16 +++ .../boot/jakarta/SentryAutoConfiguration.java | 2 + .../jakarta/SentryAutoConfigurationTest.kt | 21 ++++ .../spring/boot/SentryAutoConfiguration.java | 2 + .../boot/SentryAutoConfigurationTest.kt | 21 ++++ .../jakarta/SentryInitBeanPostProcessor.java | 3 + .../sentry/spring/jakarta/EnableSentryTest.kt | 5 + .../spring/SentryInitBeanPostProcessor.java | 3 + .../io/sentry/spring/EnableSentryTest.kt | 5 + sentry/api/sentry.api | 6 + .../main/java/io/sentry/SentryOptions.java | 34 ++++++ .../clientreport/ClientReportRecorder.java | 3 + .../test/java/io/sentry/SentryClientTest.kt | 114 ++++++++++++++++++ .../clientreport/ClientReportRecorderTest.kt | 3 + 16 files changed, 258 insertions(+), 9 deletions(-) create mode 100644 sentry/src/test/java/io/sentry/clientreport/ClientReportRecorderTest.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d7a68f409c..ee9ff39936c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,10 @@ - Remove unused method in ManifestMetadataReader ([#4585](https://github.com/getsentry/sentry-java/pull/4585)) - Have single `NetworkCallback` registered at a time to reduce IPC calls ([#4562](https://github.com/getsentry/sentry-java/pull/4562)) +### Features + +- Add onDiscard to enable users to track the type and amount of data discarded before reaching Sentry ([#3652](https://github.com/getsentry/sentry-java/issues/3652)) + ## 8.18.0 ### Features 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..5ce3dc5bdb4 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 int 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.getReason()) + || reason.equals(DiscardReason.QUEUE_OVERFLOW.getReason())) + && category.equals(DataCategory.Span.getCategory())) { + 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..51d3bb435c9 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 int 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.getReason()) + || reason.equals(DiscardReason.QUEUE_OVERFLOW.getReason())) + && category.equals(DataCategory.Span.getCategory())) { + 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..7c16c662c03 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 @@ -373,6 +373,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 +1148,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: String, category: String, 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..ff316668076 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 @@ -372,6 +372,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 +1074,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: String, category: String, 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 b08be88fc65..5ec093d37e6 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -3337,6 +3337,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; @@ -3479,6 +3480,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 @@ -3572,6 +3574,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 (Ljava/lang/String;Ljava/lang/String;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/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index 1ca089d7885..90af3e912d1 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -179,6 +179,9 @@ public class SentryOptions { */ private @Nullable BeforeBreadcrumbCallback beforeBreadcrumb; + /** Invoked when some payload sent from the SDK is dropped before consumed by Sentry, */ + private @Nullable OnDiscardCallback onDiscard; + /** The cache dir. path for caching offline events */ private @Nullable String cacheDirPath; @@ -904,6 +907,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 +3003,19 @@ 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, corresponding to a DiscardReason + * @param category the type of data discarded, corresponding to a DataCategory + * @param number the number of discarded data items + */ + void execute(@NotNull String reason, @NotNull String 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..6619fd7a4f8 100644 --- a/sentry/src/main/java/io/sentry/clientreport/ClientReportRecorder.java +++ b/sentry/src/main/java/io/sentry/clientreport/ClientReportRecorder.java @@ -124,6 +124,9 @@ private void recordLostEventInternal( @NotNull String reason, @NotNull String category, @NotNull Long countToAdd) { final ClientReportKey key = new ClientReportKey(reason, category); storage.addCount(key, countToAdd); + if (options.getOnDiscard() != null) { + options.getOnDiscard().execute(reason, category, countToAdd); + } } @Nullable diff --git a/sentry/src/test/java/io/sentry/SentryClientTest.kt b/sentry/src/test/java/io/sentry/SentryClientTest.kt index a56aed6bcac..0645561256c 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,17 @@ class SentryClientTest { DiscardedEvent(DiscardReason.BEFORE_SEND.reason, DataCategory.Span.category, 2), ), ) + + verify(onDiscardMock) + .execute(DiscardReason.BEFORE_SEND.reason, DataCategory.Transaction.category, 1) + verify(onDiscardMock).execute(DiscardReason.BEFORE_SEND.reason, DataCategory.Span.category, 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 +1037,18 @@ class SentryClientTest { DiscardedEvent(DiscardReason.EVENT_PROCESSOR.reason, DataCategory.Span.category, 2), ), ) + + verify(onDiscardMock) + .execute(DiscardReason.EVENT_PROCESSOR.reason, DataCategory.Transaction.category, 1) + verify(onDiscardMock) + .execute(DiscardReason.EVENT_PROCESSOR.reason, DataCategory.Span.category, 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 +1071,16 @@ class SentryClientTest { fixture.sentryOptions.clientReportRecorder, listOf(DiscardedEvent(DiscardReason.EVENT_PROCESSOR.reason, DataCategory.Span.category, 2)), ) + + verify(onDiscardMock) + .execute(DiscardReason.EVENT_PROCESSOR.reason, DataCategory.Span.category, 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 +1090,16 @@ class SentryClientTest { fixture.sentryOptions.clientReportRecorder, listOf(DiscardedEvent(DiscardReason.EVENT_PROCESSOR.reason, DataCategory.Error.category, 1)), ) + + verify(onDiscardMock) + .execute(DiscardReason.EVENT_PROCESSOR.reason, DataCategory.Error.category, 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 +1116,9 @@ class SentryClientTest { fixture.sentryOptions.clientReportRecorder, listOf(DiscardedEvent(DiscardReason.EVENT_PROCESSOR.reason, DataCategory.Error.category, 1)), ) + + verify(onDiscardMock) + .execute(DiscardReason.EVENT_PROCESSOR.reason, DataCategory.Error.category, 1) } @Test @@ -1106,6 +1138,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 +1153,10 @@ class SentryClientTest { DiscardedEvent(DiscardReason.BEFORE_SEND.reason, DataCategory.Span.category, 2), ), ) + + verify(onDiscardMock) + .execute(DiscardReason.BEFORE_SEND.reason, DataCategory.Transaction.category, 1) + verify(onDiscardMock).execute(DiscardReason.BEFORE_SEND.reason, DataCategory.Span.category, 2) } @Test @@ -1147,6 +1185,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 +1200,10 @@ class SentryClientTest { DiscardedEvent(DiscardReason.BEFORE_SEND.reason, DataCategory.Span.category, 2), ), ) + + verify(onDiscardMock) + .execute(DiscardReason.BEFORE_SEND.reason, DataCategory.Transaction.category, 1) + verify(onDiscardMock).execute(DiscardReason.BEFORE_SEND.reason, DataCategory.Span.category, 2) } @Test @@ -1172,6 +1217,9 @@ class SentryClientTest { } } + val onDiscardMock = mock() + fixture.sentryOptions.onDiscard = onDiscardMock + val transaction = SentryTransaction(fixture.sentryTracer) fixture.getSut().captureTransaction(transaction, fixture.sentryTracer.traceContext()) @@ -1181,6 +1229,8 @@ class SentryClientTest { fixture.sentryOptions.clientReportRecorder, listOf(DiscardedEvent(DiscardReason.BEFORE_SEND.reason, DataCategory.Span.category, 2)), ) + + verify(onDiscardMock).execute(DiscardReason.BEFORE_SEND.reason, DataCategory.Span.category, 2) } @Test @@ -2083,6 +2133,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 +2141,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 +2163,20 @@ class SentryClientTest { fixture.sentryOptions.clientReportRecorder, listOf(DiscardedEvent(DiscardReason.EVENT_PROCESSOR.reason, DataCategory.Error.category, 1)), ) + + verify(onDiscardMock) + .execute(DiscardReason.EVENT_PROCESSOR.reason, DataCategory.Error.category, 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 +2209,19 @@ class SentryClientTest { fixture.sentryOptions.clientReportRecorder, listOf(DiscardedEvent(DiscardReason.SAMPLE_RATE.reason, DataCategory.Error.category, 1)), ) + + verify(onDiscardMock).execute(DiscardReason.SAMPLE_RATE.reason, DataCategory.Error.category, 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 +2252,19 @@ class SentryClientTest { fixture.sentryOptions.clientReportRecorder, listOf(DiscardedEvent(DiscardReason.BEFORE_SEND.reason, DataCategory.Error.category, 1)), ) + + verify(onDiscardMock).execute(DiscardReason.BEFORE_SEND.reason, DataCategory.Error.category, 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 +2291,20 @@ class SentryClientTest { fixture.sentryOptions.clientReportRecorder, listOf(DiscardedEvent(DiscardReason.EVENT_PROCESSOR.reason, DataCategory.Error.category, 1)), ) + + verify(onDiscardMock) + .execute(DiscardReason.EVENT_PROCESSOR.reason, DataCategory.Error.category, 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 +2333,9 @@ class SentryClientTest { fixture.sentryOptions.clientReportRecorder, listOf(DiscardedEvent(DiscardReason.EVENT_PROCESSOR.reason, DataCategory.Error.category, 1)), ) + + verify(onDiscardMock) + .execute(DiscardReason.EVENT_PROCESSOR.reason, DataCategory.Error.category, 1) } @Test @@ -2900,6 +2978,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 +2992,9 @@ class SentryClientTest { fixture.sentryOptions.clientReportRecorder, listOf(DiscardedEvent(DiscardReason.EVENT_PROCESSOR.reason, DataCategory.Replay.category, 1)), ) + + verify(onDiscardMock) + .execute(DiscardReason.EVENT_PROCESSOR.reason, DataCategory.Replay.category, 1) } @Test @@ -3056,6 +3141,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 +3152,8 @@ class SentryClientTest { fixture.sentryOptions.clientReportRecorder, listOf(DiscardedEvent(DiscardReason.BEFORE_SEND.reason, DataCategory.Replay.category, 1)), ) + + verify(onDiscardMock).execute(DiscardReason.BEFORE_SEND.reason, DataCategory.Replay.category, 1) } @Test @@ -3091,6 +3181,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 +3193,8 @@ class SentryClientTest { fixture.sentryOptions.clientReportRecorder, listOf(DiscardedEvent(DiscardReason.BEFORE_SEND.reason, DataCategory.Replay.category, 1)), ) + + verify(onDiscardMock).execute(DiscardReason.BEFORE_SEND.reason, DataCategory.Replay.category, 1) } // endregion @@ -3230,6 +3325,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 +3335,9 @@ class SentryClientTest { fixture.sentryOptions.clientReportRecorder, listOf(DiscardedEvent(DiscardReason.BEFORE_SEND.reason, DataCategory.Feedback.category, 1)), ) + + verify(onDiscardMock) + .execute(DiscardReason.BEFORE_SEND.reason, DataCategory.Feedback.category, 1) } @Test @@ -3261,6 +3362,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 +3374,17 @@ class SentryClientTest { fixture.sentryOptions.clientReportRecorder, listOf(DiscardedEvent(DiscardReason.BEFORE_SEND.reason, DataCategory.Feedback.category, 1)), ) + + verify(onDiscardMock) + .execute(DiscardReason.BEFORE_SEND.reason, DataCategory.Feedback.category, 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 +3394,9 @@ class SentryClientTest { DiscardedEvent(DiscardReason.EVENT_PROCESSOR.reason, DataCategory.Feedback.category, 1) ), ) + + verify(onDiscardMock) + .execute(DiscardReason.EVENT_PROCESSOR.reason, DataCategory.Feedback.category, 1) } // endregion diff --git a/sentry/src/test/java/io/sentry/clientreport/ClientReportRecorderTest.kt b/sentry/src/test/java/io/sentry/clientreport/ClientReportRecorderTest.kt new file mode 100644 index 00000000000..7211eb35725 --- /dev/null +++ b/sentry/src/test/java/io/sentry/clientreport/ClientReportRecorderTest.kt @@ -0,0 +1,3 @@ +package io.sentry.clientreport + +public class ClientReportRecorderTest {} From bfb7f0d666865fc4c142528cac6ca2c06caa8633 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Thu, 7 Aug 2025 13:41:38 +0200 Subject: [PATCH 02/10] Remove empty test file --- .../java/io/sentry/clientreport/ClientReportRecorderTest.kt | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 sentry/src/test/java/io/sentry/clientreport/ClientReportRecorderTest.kt diff --git a/sentry/src/test/java/io/sentry/clientreport/ClientReportRecorderTest.kt b/sentry/src/test/java/io/sentry/clientreport/ClientReportRecorderTest.kt deleted file mode 100644 index 7211eb35725..00000000000 --- a/sentry/src/test/java/io/sentry/clientreport/ClientReportRecorderTest.kt +++ /dev/null @@ -1,3 +0,0 @@ -package io.sentry.clientreport - -public class ClientReportRecorderTest {} From 1b53c4e9a089bacf7157d9b7b4e27002324fb342 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Thu, 7 Aug 2025 13:52:32 +0200 Subject: [PATCH 03/10] Use long variable in sample usage of onDiscard --- .../src/main/java/io/sentry/samples/console/Main.java | 2 +- .../main/java/io/sentry/samples/servlet/SentryInitializer.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 5ce3dc5bdb4..db112d9981d 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 @@ -8,7 +8,7 @@ public class Main { - private static int numberOfDiscardedSpansDueToOverflow = 0; + private static long numberOfDiscardedSpansDueToOverflow = 0; public static void main(String[] args) throws InterruptedException { Sentry.init( 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 51d3bb435c9..846e6a07b8a 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 @@ -11,7 +11,7 @@ @WebListener public final class SentryInitializer implements ServletContextListener { - private static int numberOfDiscardedSpansDueToOverflow = 0; + private static long numberOfDiscardedSpansDueToOverflow = 0; @Override public void contextInitialized(ServletContextEvent sce) { From 6baf63ea035cbe4b2c3358dd901ccabf2e1d6290 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Thu, 7 Aug 2025 14:01:35 +0200 Subject: [PATCH 04/10] Wrap onDiscard callback in a try-catch and fix docstring --- sentry/src/main/java/io/sentry/SentryOptions.java | 2 +- .../io/sentry/clientreport/ClientReportRecorder.java | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index 90af3e912d1..5972f254e32 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -179,7 +179,7 @@ public class SentryOptions { */ private @Nullable BeforeBreadcrumbCallback beforeBreadcrumb; - /** Invoked when some payload sent from the SDK is dropped before consumed by Sentry, */ + /** 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 */ diff --git a/sentry/src/main/java/io/sentry/clientreport/ClientReportRecorder.java b/sentry/src/main/java/io/sentry/clientreport/ClientReportRecorder.java index 6619fd7a4f8..710b1feca17 100644 --- a/sentry/src/main/java/io/sentry/clientreport/ClientReportRecorder.java +++ b/sentry/src/main/java/io/sentry/clientreport/ClientReportRecorder.java @@ -125,7 +125,16 @@ private void recordLostEventInternal( final ClientReportKey key = new ClientReportKey(reason, category); storage.addCount(key, countToAdd); if (options.getOnDiscard() != null) { - options.getOnDiscard().execute(reason, category, countToAdd); + try { + options.getOnDiscard().execute(reason, category, countToAdd); + } catch (Throwable e) { + options + .getLogger() + .log( + SentryLevel.ERROR, + "The onDiscard callback threw an exception. It will be added as breadcrumb and continue.", + e); + } } } From a436bf23067a51d64f10de1b390eb74bd0dd0218 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Thu, 7 Aug 2025 14:03:51 +0200 Subject: [PATCH 05/10] Reference pull request instead of issue in the changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b1d3313e14..687ac35c347 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,7 +25,7 @@ ### Features -- Add onDiscard to enable users to track the type and amount of data discarded before reaching Sentry ([#3652](https://github.com/getsentry/sentry-java/issues/3652)) +- 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)) ## 8.18.0 From 88fbca6a3ef3ac0fd49f2082fb982990c9ad44e8 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Fri, 8 Aug 2025 11:03:39 +0200 Subject: [PATCH 06/10] Remove Internal annotation from DiscardReason and DataCategory, update signature of onDiscard callback, and avoid duplicate invocation --- .../jakarta/SentryAutoConfigurationTest.kt | 4 +- .../boot/SentryAutoConfigurationTest.kt | 4 +- sentry/api/sentry.api | 2 +- .../src/main/java/io/sentry/DataCategory.java | 2 - .../main/java/io/sentry/SentryOptions.java | 8 +- .../clientreport/ClientReportRecorder.java | 17 +++-- .../io/sentry/clientreport/DiscardReason.java | 3 - .../test/java/io/sentry/SentryClientTest.kt | 74 +++++++++---------- .../sentry/clientreport/ClientReportTest.kt | 50 +++++++++++++ 9 files changed, 111 insertions(+), 53 deletions(-) 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 7c16c662c03..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 @@ -1155,7 +1157,7 @@ class SentryAutoConfigurationTest { } class CustomOnDiscardCallback : SentryOptions.OnDiscardCallback { - override fun execute(reason: String, category: String, countToAdd: Long) {} + override fun execute(reason: DiscardReason, category: DataCategory, countToAdd: Long) {} } @Configuration(proxyBeanMethods = false) 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 ff316668076..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 @@ -1081,7 +1083,7 @@ class SentryAutoConfigurationTest { } class CustomOnDiscardCallback : SentryOptions.OnDiscardCallback { - override fun execute(reason: String, category: String, countToAdd: Long) {} + override fun execute(reason: DiscardReason, category: DataCategory, countToAdd: Long) {} } @Configuration(proxyBeanMethods = false) diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 5ec093d37e6..cfa968f8a28 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -3575,7 +3575,7 @@ public abstract interface class io/sentry/SentryOptions$Logs$BeforeSendLogCallba } public abstract interface class io/sentry/SentryOptions$OnDiscardCallback { - public abstract fun execute (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;)V + public abstract fun execute (Lio/sentry/clientreport/DiscardReason;Lio/sentry/DataCategory;Ljava/lang/Long;)V } public abstract interface class io/sentry/SentryOptions$ProfilesSamplerCallback { 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 5972f254e32..b39384f539d 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; @@ -3009,11 +3010,12 @@ public interface OnDiscardCallback { /** * Best-effort record of data discarded before reaching Sentry * - * @param reason the reason data was dropped, corresponding to a DiscardReason - * @param category the type of data discarded, corresponding to a DataCategory + * @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 String reason, @NotNull String category, @NotNull Long number); + void execute( + @NotNull DiscardReason reason, @NotNull DataCategory category, @NotNull Long number); } /** The traces sampler callback. */ diff --git a/sentry/src/main/java/io/sentry/clientreport/ClientReportRecorder.java b/sentry/src/main/java/io/sentry/clientreport/ClientReportRecorder.java index 710b1feca17..ab34b628005 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,15 +117,14 @@ 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 recordLostEventInternal( - @NotNull String reason, @NotNull String category, @NotNull Long countToAdd) { - final ClientReportKey key = new ClientReportKey(reason, category); - storage.addCount(key, countToAdd); + private void executeOnDiscard( + @NotNull DiscardReason reason, @NotNull DataCategory category, @NotNull Long countToAdd) { if (options.getOnDiscard() != null) { try { options.getOnDiscard().execute(reason, category, countToAdd); @@ -132,12 +133,18 @@ private void recordLostEventInternal( .getLogger() .log( SentryLevel.ERROR, - "The onDiscard callback threw an exception. It will be added as breadcrumb and continue.", + "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); + storage.addCount(key, countToAdd); + } + @Nullable ClientReport resetCountsAndGenerateClientReport() { final Date currentDate = DateUtils.getCurrentDateTime(); 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 0645561256c..6702cb83913 100644 --- a/sentry/src/test/java/io/sentry/SentryClientTest.kt +++ b/sentry/src/test/java/io/sentry/SentryClientTest.kt @@ -1012,9 +1012,9 @@ class SentryClientTest { ), ) - verify(onDiscardMock) - .execute(DiscardReason.BEFORE_SEND.reason, DataCategory.Transaction.category, 1) - verify(onDiscardMock).execute(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 @@ -1038,10 +1038,10 @@ class SentryClientTest { ), ) + verify(onDiscardMock, times(1)) + .execute(DiscardReason.EVENT_PROCESSOR, DataCategory.Transaction, 1) verify(onDiscardMock) - .execute(DiscardReason.EVENT_PROCESSOR.reason, DataCategory.Transaction.category, 1) - verify(onDiscardMock) - .execute(DiscardReason.EVENT_PROCESSOR.reason, DataCategory.Span.category, 2) + .execute(DiscardReason.EVENT_PROCESSOR, DataCategory.Span, 2) } @Test @@ -1072,8 +1072,8 @@ class SentryClientTest { listOf(DiscardedEvent(DiscardReason.EVENT_PROCESSOR.reason, DataCategory.Span.category, 2)), ) - verify(onDiscardMock) - .execute(DiscardReason.EVENT_PROCESSOR.reason, DataCategory.Span.category, 2) + verify(onDiscardMock, times(1)) + .execute(DiscardReason.EVENT_PROCESSOR, DataCategory.Span, 2) } @Test @@ -1091,8 +1091,8 @@ class SentryClientTest { listOf(DiscardedEvent(DiscardReason.EVENT_PROCESSOR.reason, DataCategory.Error.category, 1)), ) - verify(onDiscardMock) - .execute(DiscardReason.EVENT_PROCESSOR.reason, DataCategory.Error.category, 1) + verify(onDiscardMock, times(1)) + .execute(DiscardReason.EVENT_PROCESSOR, DataCategory.Error, 1) } @Test @@ -1117,8 +1117,8 @@ class SentryClientTest { listOf(DiscardedEvent(DiscardReason.EVENT_PROCESSOR.reason, DataCategory.Error.category, 1)), ) - verify(onDiscardMock) - .execute(DiscardReason.EVENT_PROCESSOR.reason, DataCategory.Error.category, 1) + verify(onDiscardMock, times(1)) + .execute(DiscardReason.EVENT_PROCESSOR, DataCategory.Error, 1) } @Test @@ -1154,9 +1154,9 @@ class SentryClientTest { ), ) - verify(onDiscardMock) - .execute(DiscardReason.BEFORE_SEND.reason, DataCategory.Transaction.category, 1) - verify(onDiscardMock).execute(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 @@ -1201,9 +1201,9 @@ class SentryClientTest { ), ) - verify(onDiscardMock) - .execute(DiscardReason.BEFORE_SEND.reason, DataCategory.Transaction.category, 1) - verify(onDiscardMock).execute(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 @@ -1230,7 +1230,7 @@ class SentryClientTest { listOf(DiscardedEvent(DiscardReason.BEFORE_SEND.reason, DataCategory.Span.category, 2)), ) - verify(onDiscardMock).execute(DiscardReason.BEFORE_SEND.reason, DataCategory.Span.category, 2) + verify(onDiscardMock, times(1)).execute(DiscardReason.BEFORE_SEND, DataCategory.Span, 2) } @Test @@ -2164,8 +2164,8 @@ class SentryClientTest { listOf(DiscardedEvent(DiscardReason.EVENT_PROCESSOR.reason, DataCategory.Error.category, 1)), ) - verify(onDiscardMock) - .execute(DiscardReason.EVENT_PROCESSOR.reason, DataCategory.Error.category, 1) + verify(onDiscardMock, times(1)) + .execute(DiscardReason.EVENT_PROCESSOR, DataCategory.Error, 1) } @Test @@ -2210,7 +2210,7 @@ class SentryClientTest { listOf(DiscardedEvent(DiscardReason.SAMPLE_RATE.reason, DataCategory.Error.category, 1)), ) - verify(onDiscardMock).execute(DiscardReason.SAMPLE_RATE.reason, DataCategory.Error.category, 1) + verify(onDiscardMock, times(1)).execute(DiscardReason.SAMPLE_RATE, DataCategory.Error, 1) } @Test @@ -2253,7 +2253,7 @@ class SentryClientTest { listOf(DiscardedEvent(DiscardReason.BEFORE_SEND.reason, DataCategory.Error.category, 1)), ) - verify(onDiscardMock).execute(DiscardReason.BEFORE_SEND.reason, DataCategory.Error.category, 1) + verify(onDiscardMock, times(1)).execute(DiscardReason.BEFORE_SEND, DataCategory.Error, 1) } @Test @@ -2292,8 +2292,8 @@ class SentryClientTest { listOf(DiscardedEvent(DiscardReason.EVENT_PROCESSOR.reason, DataCategory.Error.category, 1)), ) - verify(onDiscardMock) - .execute(DiscardReason.EVENT_PROCESSOR.reason, DataCategory.Error.category, 1) + verify(onDiscardMock, times(1)) + .execute(DiscardReason.EVENT_PROCESSOR, DataCategory.Error, 1) } @Test @@ -2334,8 +2334,8 @@ class SentryClientTest { listOf(DiscardedEvent(DiscardReason.EVENT_PROCESSOR.reason, DataCategory.Error.category, 1)), ) - verify(onDiscardMock) - .execute(DiscardReason.EVENT_PROCESSOR.reason, DataCategory.Error.category, 1) + verify(onDiscardMock, times(1)) + .execute(DiscardReason.EVENT_PROCESSOR, DataCategory.Error, 1) } @Test @@ -2993,8 +2993,8 @@ class SentryClientTest { listOf(DiscardedEvent(DiscardReason.EVENT_PROCESSOR.reason, DataCategory.Replay.category, 1)), ) - verify(onDiscardMock) - .execute(DiscardReason.EVENT_PROCESSOR.reason, DataCategory.Replay.category, 1) + verify(onDiscardMock, times(1)) + .execute(DiscardReason.EVENT_PROCESSOR, DataCategory.Replay, 1) } @Test @@ -3153,7 +3153,7 @@ class SentryClientTest { listOf(DiscardedEvent(DiscardReason.BEFORE_SEND.reason, DataCategory.Replay.category, 1)), ) - verify(onDiscardMock).execute(DiscardReason.BEFORE_SEND.reason, DataCategory.Replay.category, 1) + verify(onDiscardMock, times(1)).execute(DiscardReason.BEFORE_SEND, DataCategory.Replay, 1) } @Test @@ -3194,7 +3194,7 @@ class SentryClientTest { listOf(DiscardedEvent(DiscardReason.BEFORE_SEND.reason, DataCategory.Replay.category, 1)), ) - verify(onDiscardMock).execute(DiscardReason.BEFORE_SEND.reason, DataCategory.Replay.category, 1) + verify(onDiscardMock, times(1)).execute(DiscardReason.BEFORE_SEND, DataCategory.Replay, 1) } // endregion @@ -3336,8 +3336,8 @@ class SentryClientTest { listOf(DiscardedEvent(DiscardReason.BEFORE_SEND.reason, DataCategory.Feedback.category, 1)), ) - verify(onDiscardMock) - .execute(DiscardReason.BEFORE_SEND.reason, DataCategory.Feedback.category, 1) + verify(onDiscardMock, times(1)) + .execute(DiscardReason.BEFORE_SEND, DataCategory.Feedback, 1) } @Test @@ -3375,8 +3375,8 @@ class SentryClientTest { listOf(DiscardedEvent(DiscardReason.BEFORE_SEND.reason, DataCategory.Feedback.category, 1)), ) - verify(onDiscardMock) - .execute(DiscardReason.BEFORE_SEND.reason, DataCategory.Feedback.category, 1) + verify(onDiscardMock, times(1)) + .execute(DiscardReason.BEFORE_SEND, DataCategory.Feedback, 1) } @Test @@ -3395,8 +3395,8 @@ class SentryClientTest { ), ) - verify(onDiscardMock) - .execute(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..1b9c5db6ded 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,54 @@ 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 ) { From 05978701232b6ce56a4427cb8a5f5383839c4940 Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Fri, 8 Aug 2025 09:05:28 +0000 Subject: [PATCH 07/10] Format code --- .../clientreport/ClientReportRecorder.java | 7 +--- .../test/java/io/sentry/SentryClientTest.kt | 42 +++++++------------ .../sentry/clientreport/ClientReportTest.kt | 18 +++----- 3 files changed, 21 insertions(+), 46 deletions(-) diff --git a/sentry/src/main/java/io/sentry/clientreport/ClientReportRecorder.java b/sentry/src/main/java/io/sentry/clientreport/ClientReportRecorder.java index ab34b628005..4e2383eb107 100644 --- a/sentry/src/main/java/io/sentry/clientreport/ClientReportRecorder.java +++ b/sentry/src/main/java/io/sentry/clientreport/ClientReportRecorder.java @@ -129,12 +129,7 @@ private void executeOnDiscard( try { options.getOnDiscard().execute(reason, category, countToAdd); } catch (Throwable e) { - options - .getLogger() - .log( - SentryLevel.ERROR, - "The onDiscard callback threw an exception.", - e); + options.getLogger().log(SentryLevel.ERROR, "The onDiscard callback threw an exception.", e); } } } diff --git a/sentry/src/test/java/io/sentry/SentryClientTest.kt b/sentry/src/test/java/io/sentry/SentryClientTest.kt index 6702cb83913..878eb2d4aa7 100644 --- a/sentry/src/test/java/io/sentry/SentryClientTest.kt +++ b/sentry/src/test/java/io/sentry/SentryClientTest.kt @@ -1012,8 +1012,7 @@ class SentryClientTest { ), ) - verify(onDiscardMock, times(1)) - .execute(DiscardReason.BEFORE_SEND, DataCategory.Transaction, 1) + verify(onDiscardMock, times(1)).execute(DiscardReason.BEFORE_SEND, DataCategory.Transaction, 1) verify(onDiscardMock).execute(DiscardReason.BEFORE_SEND, DataCategory.Span, 2) } @@ -1040,8 +1039,7 @@ class SentryClientTest { verify(onDiscardMock, times(1)) .execute(DiscardReason.EVENT_PROCESSOR, DataCategory.Transaction, 1) - verify(onDiscardMock) - .execute(DiscardReason.EVENT_PROCESSOR, DataCategory.Span, 2) + verify(onDiscardMock).execute(DiscardReason.EVENT_PROCESSOR, DataCategory.Span, 2) } @Test @@ -1072,8 +1070,7 @@ class SentryClientTest { listOf(DiscardedEvent(DiscardReason.EVENT_PROCESSOR.reason, DataCategory.Span.category, 2)), ) - verify(onDiscardMock, times(1)) - .execute(DiscardReason.EVENT_PROCESSOR, DataCategory.Span, 2) + verify(onDiscardMock, times(1)).execute(DiscardReason.EVENT_PROCESSOR, DataCategory.Span, 2) } @Test @@ -1091,8 +1088,7 @@ class SentryClientTest { listOf(DiscardedEvent(DiscardReason.EVENT_PROCESSOR.reason, DataCategory.Error.category, 1)), ) - verify(onDiscardMock, times(1)) - .execute(DiscardReason.EVENT_PROCESSOR, DataCategory.Error, 1) + verify(onDiscardMock, times(1)).execute(DiscardReason.EVENT_PROCESSOR, DataCategory.Error, 1) } @Test @@ -1117,8 +1113,7 @@ class SentryClientTest { listOf(DiscardedEvent(DiscardReason.EVENT_PROCESSOR.reason, DataCategory.Error.category, 1)), ) - verify(onDiscardMock, times(1)) - .execute(DiscardReason.EVENT_PROCESSOR, DataCategory.Error, 1) + verify(onDiscardMock, times(1)).execute(DiscardReason.EVENT_PROCESSOR, DataCategory.Error, 1) } @Test @@ -1154,8 +1149,7 @@ class SentryClientTest { ), ) - verify(onDiscardMock, times(1)) - .execute(DiscardReason.BEFORE_SEND, DataCategory.Transaction, 1) + verify(onDiscardMock, times(1)).execute(DiscardReason.BEFORE_SEND, DataCategory.Transaction, 1) verify(onDiscardMock).execute(DiscardReason.BEFORE_SEND, DataCategory.Span, 2) } @@ -1201,8 +1195,7 @@ class SentryClientTest { ), ) - verify(onDiscardMock, times(1)) - .execute(DiscardReason.BEFORE_SEND, DataCategory.Transaction, 1) + verify(onDiscardMock, times(1)).execute(DiscardReason.BEFORE_SEND, DataCategory.Transaction, 1) verify(onDiscardMock).execute(DiscardReason.BEFORE_SEND, DataCategory.Span, 2) } @@ -2164,8 +2157,7 @@ class SentryClientTest { listOf(DiscardedEvent(DiscardReason.EVENT_PROCESSOR.reason, DataCategory.Error.category, 1)), ) - verify(onDiscardMock, times(1)) - .execute(DiscardReason.EVENT_PROCESSOR, DataCategory.Error, 1) + verify(onDiscardMock, times(1)).execute(DiscardReason.EVENT_PROCESSOR, DataCategory.Error, 1) } @Test @@ -2292,8 +2284,7 @@ class SentryClientTest { listOf(DiscardedEvent(DiscardReason.EVENT_PROCESSOR.reason, DataCategory.Error.category, 1)), ) - verify(onDiscardMock, times(1)) - .execute(DiscardReason.EVENT_PROCESSOR, DataCategory.Error, 1) + verify(onDiscardMock, times(1)).execute(DiscardReason.EVENT_PROCESSOR, DataCategory.Error, 1) } @Test @@ -2334,8 +2325,7 @@ class SentryClientTest { listOf(DiscardedEvent(DiscardReason.EVENT_PROCESSOR.reason, DataCategory.Error.category, 1)), ) - verify(onDiscardMock, times(1)) - .execute(DiscardReason.EVENT_PROCESSOR, DataCategory.Error, 1) + verify(onDiscardMock, times(1)).execute(DiscardReason.EVENT_PROCESSOR, DataCategory.Error, 1) } @Test @@ -2993,8 +2983,7 @@ class SentryClientTest { listOf(DiscardedEvent(DiscardReason.EVENT_PROCESSOR.reason, DataCategory.Replay.category, 1)), ) - verify(onDiscardMock, times(1)) - .execute(DiscardReason.EVENT_PROCESSOR, DataCategory.Replay, 1) + verify(onDiscardMock, times(1)).execute(DiscardReason.EVENT_PROCESSOR, DataCategory.Replay, 1) } @Test @@ -3336,8 +3325,7 @@ class SentryClientTest { listOf(DiscardedEvent(DiscardReason.BEFORE_SEND.reason, DataCategory.Feedback.category, 1)), ) - verify(onDiscardMock, times(1)) - .execute(DiscardReason.BEFORE_SEND, DataCategory.Feedback, 1) + verify(onDiscardMock, times(1)).execute(DiscardReason.BEFORE_SEND, DataCategory.Feedback, 1) } @Test @@ -3375,8 +3363,7 @@ class SentryClientTest { listOf(DiscardedEvent(DiscardReason.BEFORE_SEND.reason, DataCategory.Feedback.category, 1)), ) - verify(onDiscardMock, times(1)) - .execute(DiscardReason.BEFORE_SEND, DataCategory.Feedback, 1) + verify(onDiscardMock, times(1)).execute(DiscardReason.BEFORE_SEND, DataCategory.Feedback, 1) } @Test @@ -3395,8 +3382,7 @@ class SentryClientTest { ), ) - verify(onDiscardMock, times(1)) - .execute(DiscardReason.EVENT_PROCESSOR, DataCategory.Feedback, 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 1b9c5db6ded..2b34fd839b0 100644 --- a/sentry/src/test/java/io/sentry/clientreport/ClientReportTest.kt +++ b/sentry/src/test/java/io/sentry/clientreport/ClientReportTest.kt @@ -321,12 +321,9 @@ class ClientReportTest { 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) + 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 @@ -345,12 +342,9 @@ class ClientReportTest { 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) + 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( From 6e1b2b0e4c7a082772a78531d531b7a0f48a0429 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Fri, 8 Aug 2025 11:16:42 +0200 Subject: [PATCH 08/10] Add stub for using onDiscard to the changelog --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 687ac35c347..a40c516dcaa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,16 @@ ### 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.18.0 From 80aa565d7f6ad4732db0ac5f3056b6a4d12033e6 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Fri, 8 Aug 2025 11:23:17 +0200 Subject: [PATCH 09/10] Switch samples to use enums instead of strings and lint --- .../java/io/sentry/samples/console/Main.java | 6 +-- .../samples/servlet/SentryInitializer.java | 6 +-- .../clientreport/ClientReportRecorder.java | 7 +--- .../test/java/io/sentry/SentryClientTest.kt | 42 +++++++------------ .../sentry/clientreport/ClientReportTest.kt | 18 +++----- 5 files changed, 27 insertions(+), 52 deletions(-) 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 db112d9981d..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 @@ -59,9 +59,9 @@ public static void main(String[] args) throws InterruptedException { options.setOnDiscard( (reason, category, number) -> { // Only record the number of lost spans due to overflow conditions - if ((reason.equals(DiscardReason.CACHE_OVERFLOW.getReason()) - || reason.equals(DiscardReason.QUEUE_OVERFLOW.getReason())) - && category.equals(DataCategory.Span.getCategory())) { + if ((reason.equals(DiscardReason.CACHE_OVERFLOW) + || reason.equals(DiscardReason.QUEUE_OVERFLOW)) + && category.equals(DataCategory.Span)) { numberOfDiscardedSpansDueToOverflow += number; } }); 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 846e6a07b8a..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 @@ -66,9 +66,9 @@ public void contextInitialized(ServletContextEvent sce) { options.setOnDiscard( (reason, category, number) -> { // Only record the number of lost spans due to overflow conditions - if ((reason.equals(DiscardReason.CACHE_OVERFLOW.getReason()) - || reason.equals(DiscardReason.QUEUE_OVERFLOW.getReason())) - && category.equals(DataCategory.Span.getCategory())) { + if ((reason.equals(DiscardReason.CACHE_OVERFLOW) + || reason.equals(DiscardReason.QUEUE_OVERFLOW)) + && category.equals(DataCategory.Span)) { numberOfDiscardedSpansDueToOverflow += number; } }); diff --git a/sentry/src/main/java/io/sentry/clientreport/ClientReportRecorder.java b/sentry/src/main/java/io/sentry/clientreport/ClientReportRecorder.java index ab34b628005..4e2383eb107 100644 --- a/sentry/src/main/java/io/sentry/clientreport/ClientReportRecorder.java +++ b/sentry/src/main/java/io/sentry/clientreport/ClientReportRecorder.java @@ -129,12 +129,7 @@ private void executeOnDiscard( try { options.getOnDiscard().execute(reason, category, countToAdd); } catch (Throwable e) { - options - .getLogger() - .log( - SentryLevel.ERROR, - "The onDiscard callback threw an exception.", - e); + options.getLogger().log(SentryLevel.ERROR, "The onDiscard callback threw an exception.", e); } } } diff --git a/sentry/src/test/java/io/sentry/SentryClientTest.kt b/sentry/src/test/java/io/sentry/SentryClientTest.kt index 6702cb83913..878eb2d4aa7 100644 --- a/sentry/src/test/java/io/sentry/SentryClientTest.kt +++ b/sentry/src/test/java/io/sentry/SentryClientTest.kt @@ -1012,8 +1012,7 @@ class SentryClientTest { ), ) - verify(onDiscardMock, times(1)) - .execute(DiscardReason.BEFORE_SEND, DataCategory.Transaction, 1) + verify(onDiscardMock, times(1)).execute(DiscardReason.BEFORE_SEND, DataCategory.Transaction, 1) verify(onDiscardMock).execute(DiscardReason.BEFORE_SEND, DataCategory.Span, 2) } @@ -1040,8 +1039,7 @@ class SentryClientTest { verify(onDiscardMock, times(1)) .execute(DiscardReason.EVENT_PROCESSOR, DataCategory.Transaction, 1) - verify(onDiscardMock) - .execute(DiscardReason.EVENT_PROCESSOR, DataCategory.Span, 2) + verify(onDiscardMock).execute(DiscardReason.EVENT_PROCESSOR, DataCategory.Span, 2) } @Test @@ -1072,8 +1070,7 @@ class SentryClientTest { listOf(DiscardedEvent(DiscardReason.EVENT_PROCESSOR.reason, DataCategory.Span.category, 2)), ) - verify(onDiscardMock, times(1)) - .execute(DiscardReason.EVENT_PROCESSOR, DataCategory.Span, 2) + verify(onDiscardMock, times(1)).execute(DiscardReason.EVENT_PROCESSOR, DataCategory.Span, 2) } @Test @@ -1091,8 +1088,7 @@ class SentryClientTest { listOf(DiscardedEvent(DiscardReason.EVENT_PROCESSOR.reason, DataCategory.Error.category, 1)), ) - verify(onDiscardMock, times(1)) - .execute(DiscardReason.EVENT_PROCESSOR, DataCategory.Error, 1) + verify(onDiscardMock, times(1)).execute(DiscardReason.EVENT_PROCESSOR, DataCategory.Error, 1) } @Test @@ -1117,8 +1113,7 @@ class SentryClientTest { listOf(DiscardedEvent(DiscardReason.EVENT_PROCESSOR.reason, DataCategory.Error.category, 1)), ) - verify(onDiscardMock, times(1)) - .execute(DiscardReason.EVENT_PROCESSOR, DataCategory.Error, 1) + verify(onDiscardMock, times(1)).execute(DiscardReason.EVENT_PROCESSOR, DataCategory.Error, 1) } @Test @@ -1154,8 +1149,7 @@ class SentryClientTest { ), ) - verify(onDiscardMock, times(1)) - .execute(DiscardReason.BEFORE_SEND, DataCategory.Transaction, 1) + verify(onDiscardMock, times(1)).execute(DiscardReason.BEFORE_SEND, DataCategory.Transaction, 1) verify(onDiscardMock).execute(DiscardReason.BEFORE_SEND, DataCategory.Span, 2) } @@ -1201,8 +1195,7 @@ class SentryClientTest { ), ) - verify(onDiscardMock, times(1)) - .execute(DiscardReason.BEFORE_SEND, DataCategory.Transaction, 1) + verify(onDiscardMock, times(1)).execute(DiscardReason.BEFORE_SEND, DataCategory.Transaction, 1) verify(onDiscardMock).execute(DiscardReason.BEFORE_SEND, DataCategory.Span, 2) } @@ -2164,8 +2157,7 @@ class SentryClientTest { listOf(DiscardedEvent(DiscardReason.EVENT_PROCESSOR.reason, DataCategory.Error.category, 1)), ) - verify(onDiscardMock, times(1)) - .execute(DiscardReason.EVENT_PROCESSOR, DataCategory.Error, 1) + verify(onDiscardMock, times(1)).execute(DiscardReason.EVENT_PROCESSOR, DataCategory.Error, 1) } @Test @@ -2292,8 +2284,7 @@ class SentryClientTest { listOf(DiscardedEvent(DiscardReason.EVENT_PROCESSOR.reason, DataCategory.Error.category, 1)), ) - verify(onDiscardMock, times(1)) - .execute(DiscardReason.EVENT_PROCESSOR, DataCategory.Error, 1) + verify(onDiscardMock, times(1)).execute(DiscardReason.EVENT_PROCESSOR, DataCategory.Error, 1) } @Test @@ -2334,8 +2325,7 @@ class SentryClientTest { listOf(DiscardedEvent(DiscardReason.EVENT_PROCESSOR.reason, DataCategory.Error.category, 1)), ) - verify(onDiscardMock, times(1)) - .execute(DiscardReason.EVENT_PROCESSOR, DataCategory.Error, 1) + verify(onDiscardMock, times(1)).execute(DiscardReason.EVENT_PROCESSOR, DataCategory.Error, 1) } @Test @@ -2993,8 +2983,7 @@ class SentryClientTest { listOf(DiscardedEvent(DiscardReason.EVENT_PROCESSOR.reason, DataCategory.Replay.category, 1)), ) - verify(onDiscardMock, times(1)) - .execute(DiscardReason.EVENT_PROCESSOR, DataCategory.Replay, 1) + verify(onDiscardMock, times(1)).execute(DiscardReason.EVENT_PROCESSOR, DataCategory.Replay, 1) } @Test @@ -3336,8 +3325,7 @@ class SentryClientTest { listOf(DiscardedEvent(DiscardReason.BEFORE_SEND.reason, DataCategory.Feedback.category, 1)), ) - verify(onDiscardMock, times(1)) - .execute(DiscardReason.BEFORE_SEND, DataCategory.Feedback, 1) + verify(onDiscardMock, times(1)).execute(DiscardReason.BEFORE_SEND, DataCategory.Feedback, 1) } @Test @@ -3375,8 +3363,7 @@ class SentryClientTest { listOf(DiscardedEvent(DiscardReason.BEFORE_SEND.reason, DataCategory.Feedback.category, 1)), ) - verify(onDiscardMock, times(1)) - .execute(DiscardReason.BEFORE_SEND, DataCategory.Feedback, 1) + verify(onDiscardMock, times(1)).execute(DiscardReason.BEFORE_SEND, DataCategory.Feedback, 1) } @Test @@ -3395,8 +3382,7 @@ class SentryClientTest { ), ) - verify(onDiscardMock, times(1)) - .execute(DiscardReason.EVENT_PROCESSOR, DataCategory.Feedback, 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 1b9c5db6ded..2b34fd839b0 100644 --- a/sentry/src/test/java/io/sentry/clientreport/ClientReportTest.kt +++ b/sentry/src/test/java/io/sentry/clientreport/ClientReportTest.kt @@ -321,12 +321,9 @@ class ClientReportTest { 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) + 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 @@ -345,12 +342,9 @@ class ClientReportTest { 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) + 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( From 8b18e8f3d0027a0ad7d02d5962a8fd7742183a5b Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Tue, 12 Aug 2025 08:40:52 +0200 Subject: [PATCH 10/10] Move feature out of release that appeared by merging main in the changelog --- CHANGELOG.md | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9bf98913b8f..b05285641c7 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.0 ### Features @@ -43,20 +59,6 @@ - [changelog](https://github.com/getsentry/sentry-native/blob/master/CHANGELOG.md#0100) - [diff](https://github.com/getsentry/sentry-native/compare/0.8.4...0.10.0) -### 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.18.0 ### Features