From d83b12aba69cf651d853a71d391b2cfdd9ba0bf0 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Tue, 29 Sep 2026 10:02:59 +0200 Subject: [PATCH 1/9] fix(core): Capture manual telemetry without enable flags Capture direct Logs and Metrics API calls regardless of their legacy enable flags while preserving those flags for automatic integrations in v8. Always create the configured batch processors and keep their workers lazy until the first accepted item. Keep automatic Timber forwarding gated and avoid unused Android background flush submissions. Fixes #6170 Co-Authored-By: Claude --- .../core/AndroidLoggerBatchProcessor.java | 3 ++ .../core/AndroidMetricsBatchProcessor.java | 3 ++ .../core/AndroidLoggerBatchProcessorTest.kt | 15 +++++++++- .../core/AndroidMetricsBatchProcessorTest.kt | 15 +++++++++- .../sentry/android/timber/SentryTimberTree.kt | 4 +++ .../timber/SentryTimberIntegrationTest.kt | 4 +++ .../android/timber/SentryTimberTreeTest.kt | 15 ++++++++++ .../src/main/AndroidManifest.xml | 2 +- sentry/api/sentry.api | 2 ++ .../src/main/java/io/sentry/SentryClient.java | 17 ++--------- .../main/java/io/sentry/SentryOptions.java | 24 ++++++++------- .../main/java/io/sentry/logger/LoggerApi.java | 7 ----- .../sentry/logger/LoggerBatchProcessor.java | 17 +++++++---- .../java/io/sentry/metrics/MetricsApi.java | 9 ------ .../sentry/metrics/MetricsBatchProcessor.java | 17 +++++++---- sentry/src/test/java/io/sentry/ScopesTest.kt | 19 +++++++++--- .../test/java/io/sentry/SentryClientTest.kt | 11 +++++++ sentry/src/test/java/io/sentry/SentryTest.kt | 27 +++++++++++++++++ .../sentry/logger/LoggerBatchProcessorTest.kt | 28 +++++++++++++++++ .../metrics/MetricsBatchProcessorTest.kt | 30 +++++++++++++++++++ 20 files changed, 210 insertions(+), 59 deletions(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidLoggerBatchProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidLoggerBatchProcessor.java index 13b12dc702a..b81666e2f38 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidLoggerBatchProcessor.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidLoggerBatchProcessor.java @@ -24,6 +24,9 @@ public void onForeground() { @Override public void onBackground() { + if (!hasAcceptedItem) { + return; + } try { options .getExecutorService() diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidMetricsBatchProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidMetricsBatchProcessor.java index 290f2a9d4ed..d92c4369139 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidMetricsBatchProcessor.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidMetricsBatchProcessor.java @@ -24,6 +24,9 @@ public void onForeground() { @Override public void onBackground() { + if (!hasAcceptedItem) { + return; + } try { options .getExecutorService() diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidLoggerBatchProcessorTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidLoggerBatchProcessorTest.kt index 369f7f6a148..18bf67cd11c 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidLoggerBatchProcessorTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidLoggerBatchProcessorTest.kt @@ -2,6 +2,7 @@ package io.sentry.android.core import androidx.test.ext.junit.runners.AndroidJUnit4 import io.sentry.ISentryClient +import io.sentry.ISentryExecutorService import io.sentry.SentryLogEvent import io.sentry.SentryLogLevel import io.sentry.SentryOptions @@ -16,6 +17,7 @@ import org.junit.runner.RunWith import org.mockito.kotlin.any import org.mockito.kotlin.mock import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions import org.mockito.kotlin.whenever @RunWith(AndroidJUnit4::class) @@ -66,13 +68,24 @@ class AndroidLoggerBatchProcessorTest { verify(fixture.client).captureBatchedLogEvents(any()) } + @Test + fun `onBackground does not submit shared executor work without accepted items`() { + val sharedExecutor = mock() + val sut = fixture.getSut { options -> options.executorService = sharedExecutor } + + sut.onBackground() + + verifyNoInteractions(sharedExecutor) + } + @Test fun `onBackground handles executor exception gracefully`() { val sut = fixture.getSut { options -> - val rejectingExecutor = mock() + val rejectingExecutor = mock() whenever(rejectingExecutor.submit(any())).thenThrow(RuntimeException("Rejected")) options.executorService = rejectingExecutor } + sut.add(SentryLogEvent(SentryId(), 1.0, "test", SentryLogLevel.INFO)) // Should not throw sut.onBackground() diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidMetricsBatchProcessorTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidMetricsBatchProcessorTest.kt index 7d85502d149..a965c066279 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidMetricsBatchProcessorTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidMetricsBatchProcessorTest.kt @@ -2,6 +2,7 @@ package io.sentry.android.core import androidx.test.ext.junit.runners.AndroidJUnit4 import io.sentry.ISentryClient +import io.sentry.ISentryExecutorService import io.sentry.SentryMetricsEvent import io.sentry.SentryOptions import io.sentry.protocol.SentryId @@ -15,6 +16,7 @@ import org.junit.runner.RunWith import org.mockito.kotlin.any import org.mockito.kotlin.mock import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions import org.mockito.kotlin.whenever @RunWith(AndroidJUnit4::class) @@ -65,13 +67,24 @@ class AndroidMetricsBatchProcessorTest { verify(fixture.client).captureBatchedMetricsEvents(any()) } + @Test + fun `onBackground does not submit shared executor work without accepted items`() { + val sharedExecutor = mock() + val sut = fixture.getSut { options -> options.executorService = sharedExecutor } + + sut.onBackground() + + verifyNoInteractions(sharedExecutor) + } + @Test fun `onBackground handles executor exception gracefully`() { val sut = fixture.getSut { options -> - val rejectingExecutor = mock() + val rejectingExecutor = mock() whenever(rejectingExecutor.submit(any())).thenThrow(RuntimeException("Rejected")) options.executorService = rejectingExecutor } + sut.add(SentryMetricsEvent(SentryId(), 1.0, "test", "counter", 3.0)) // Should not throw sut.onBackground() diff --git a/sentry-android-timber/src/main/java/io/sentry/android/timber/SentryTimberTree.kt b/sentry-android-timber/src/main/java/io/sentry/android/timber/SentryTimberTree.kt index 61b1f99fb16..707fc28d965 100644 --- a/sentry-android-timber/src/main/java/io/sentry/android/timber/SentryTimberTree.kt +++ b/sentry-android-timber/src/main/java/io/sentry/android/timber/SentryTimberTree.kt @@ -246,6 +246,10 @@ public class SentryTimberTree( throwable: Throwable?, vararg args: Any?, ) { + if (!scopes.options.logs.isEnabled) { + return + } + // checks the log level if (isLoggable(sentryLogLevel, minLogLevel)) { val attributes = tag?.let { diff --git a/sentry-android-timber/src/test/java/io/sentry/android/timber/SentryTimberIntegrationTest.kt b/sentry-android-timber/src/test/java/io/sentry/android/timber/SentryTimberIntegrationTest.kt index 7c21eca8ef0..758e6568c10 100644 --- a/sentry-android-timber/src/test/java/io/sentry/android/timber/SentryTimberIntegrationTest.kt +++ b/sentry-android-timber/src/test/java/io/sentry/android/timber/SentryTimberIntegrationTest.kt @@ -24,6 +24,10 @@ class SentryTimberIntegrationTest { val scopes = mock() val options = SentryOptions().apply { sdkVersion = SdkVersion("test", "1.2.3") } + init { + whenever(scopes.options).thenReturn(options) + } + fun getSut( minEventLevel: SentryLevel = SentryLevel.ERROR, minBreadcrumbLevel: SentryLevel = SentryLevel.INFO, diff --git a/sentry-android-timber/src/test/java/io/sentry/android/timber/SentryTimberTreeTest.kt b/sentry-android-timber/src/test/java/io/sentry/android/timber/SentryTimberTreeTest.kt index f1d6d5a51bd..8ef43a9b691 100644 --- a/sentry-android-timber/src/test/java/io/sentry/android/timber/SentryTimberTreeTest.kt +++ b/sentry-android-timber/src/test/java/io/sentry/android/timber/SentryTimberTreeTest.kt @@ -5,6 +5,7 @@ import io.sentry.Breadcrumb import io.sentry.Scopes import io.sentry.SentryLevel import io.sentry.SentryLogLevel +import io.sentry.SentryOptions import io.sentry.logger.ILoggerApi import io.sentry.logger.SentryLogParameters import kotlin.test.BeforeTest @@ -31,9 +32,12 @@ class SentryTimberTreeTest { minEventLevel: SentryLevel = SentryLevel.ERROR, minBreadcrumbLevel: SentryLevel = SentryLevel.INFO, minLogsLevel: SentryLogLevel = SentryLogLevel.INFO, + logsEnabled: Boolean = true, ): SentryTimberTree { logs = mock() scopes = mock() + val options = SentryOptions().apply { logs.isEnabled = logsEnabled } + whenever(scopes.options).thenReturn(options) whenever(scopes.logger()).thenReturn(logs) return SentryTimberTree(scopes, minEventLevel, minBreadcrumbLevel, minLogsLevel) } @@ -332,6 +336,17 @@ class SentryTimberTreeTest { verifyNoInteractions(fixture.logs) } + @Test + fun `Tree does not add a log when automatic Logs are disabled`() { + val sut = fixture.getSut(logsEnabled = false) + + sut.e("message") + + verifyNoInteractions(fixture.logs) + verify(fixture.scopes).captureEvent(any()) + verify(fixture.scopes).addBreadcrumb(any()) + } + @Test fun `Tree adds an info log`() { val sut = fixture.getSut() diff --git a/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml b/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml index 6426d03b814..ad925fae82e 100644 --- a/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml +++ b/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml @@ -147,7 +147,7 @@ android:name="io.sentry.debug" android:value="${sentryDebug}" /> - + diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 53452c4c3b9..b6d2cc5dc33 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -5547,6 +5547,7 @@ public class io/sentry/logger/LoggerBatchProcessor : io/sentry/logger/ILoggerBat public static final field FLUSH_AFTER_MS I public static final field MAX_BATCH_SIZE I public static final field MAX_QUEUE_SIZE I + protected field hasAcceptedItem Z protected final field options Lio/sentry/SentryOptions; public fun (Lio/sentry/SentryOptions;Lio/sentry/ISentryClient;)V public fun (Lio/sentry/SentryOptions;Lio/sentry/ISentryClient;Lio/sentry/ISentryExecutorService;)V @@ -5635,6 +5636,7 @@ public class io/sentry/metrics/MetricsBatchProcessor : io/sentry/metrics/IMetric public static final field FLUSH_AFTER_MS I public static final field MAX_BATCH_SIZE I public static final field MAX_QUEUE_SIZE I + protected field hasAcceptedItem Z protected final field options Lio/sentry/SentryOptions; public fun (Lio/sentry/SentryOptions;Lio/sentry/ISentryClient;)V public fun add (Lio/sentry/SentryMetricsEvent;)V diff --git a/sentry/src/main/java/io/sentry/SentryClient.java b/sentry/src/main/java/io/sentry/SentryClient.java index 4bba195feea..012587eaa59 100644 --- a/sentry/src/main/java/io/sentry/SentryClient.java +++ b/sentry/src/main/java/io/sentry/SentryClient.java @@ -9,9 +9,7 @@ import io.sentry.hints.DiskFlushNotification; import io.sentry.hints.TransactionEnd; import io.sentry.logger.ILoggerBatchProcessor; -import io.sentry.logger.NoOpLoggerBatchProcessor; import io.sentry.metrics.IMetricsBatchProcessor; -import io.sentry.metrics.NoOpMetricsBatchProcessor; import io.sentry.protocol.Contexts; import io.sentry.protocol.DebugMeta; import io.sentry.protocol.FeatureFlags; @@ -60,18 +58,9 @@ public SentryClient(final @NotNull SentryOptions options) { final RequestDetailsResolver requestDetailsResolver = new RequestDetailsResolver(options); transport = transportFactory.create(options, requestDetailsResolver.resolve()); - if (options.getLogs().isEnabled()) { - loggerBatchProcessor = - options.getLogs().getLoggerBatchProcessorFactory().create(options, this); - } else { - loggerBatchProcessor = NoOpLoggerBatchProcessor.getInstance(); - } - if (options.getMetrics().isEnabled()) { - metricsBatchProcessor = - options.getMetrics().getMetricsBatchProcessorFactory().create(options, this); - } else { - metricsBatchProcessor = NoOpMetricsBatchProcessor.getInstance(); - } + loggerBatchProcessor = options.getLogs().getLoggerBatchProcessorFactory().create(options, this); + metricsBatchProcessor = + options.getMetrics().getMetricsBatchProcessorFactory().create(options, this); } private boolean shouldApplyScopeData( diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index 6dacb527276..f077ba16276 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -4092,7 +4092,7 @@ public void setDefaultRecoveryThreshold(@Nullable Long defaultRecoveryThreshold) public static final class Logs { - /** Whether Sentry Logs feature is enabled and Sentry.logger() usages are sent to Sentry. */ + /** Whether automatic logging integrations send Logs to Sentry. */ private boolean enable = false; /** @@ -4105,18 +4105,20 @@ public static final class Logs { new DefaultLoggerBatchProcessorFactory(); /** - * Whether Sentry Logs feature is enabled and Sentry.logger() usages are sent to Sentry. + * Whether automatic logging integrations send Logs to Sentry. Direct {@code Sentry.logger()} + * calls are always captured when the SDK is enabled. * - * @return true if Sentry Logs should be enabled + * @return true if automatic Logs should be enabled */ public boolean isEnabled() { return enable; } /** - * Whether Sentry Logs feature is enabled and Sentry.logger() usages are sent to Sentry. + * Whether automatic logging integrations send Logs to Sentry. Direct {@code Sentry.logger()} + * calls are always captured when the SDK is enabled. * - * @param enableLogs true if Sentry Logs should be enabled + * @param enableLogs true if automatic Logs should be enabled */ public void setEnabled(boolean enableLogs) { this.enable = enableLogs; @@ -4171,7 +4173,7 @@ public interface BeforeSendLogCallback { public static final class Metrics { - /** Whether Sentry Metrics feature is enabled and metrics are sent to Sentry. */ + /** Whether automatic metrics integrations send Metrics to Sentry. */ private boolean enable = true; /** @@ -4184,18 +4186,20 @@ public static final class Metrics { new DefaultMetricsBatchProcessorFactory(); /** - * Whether Sentry Metrics feature is enabled and metrics are sent to Sentry. + * Whether automatic metrics integrations send Metrics to Sentry. Direct {@code + * Sentry.metrics()} calls are always captured when the SDK is enabled. * - * @return true if Sentry Metrics should be enabled + * @return true if automatic Metrics should be enabled */ public boolean isEnabled() { return enable; } /** - * Whether Sentry Metrics feature is enabled and metrics are sent to Sentry. + * Whether automatic metrics integrations send Metrics to Sentry. Direct {@code + * Sentry.metrics()} calls are always captured when the SDK is enabled. * - * @param enableMetrics true if Sentry Metrics should be enabled + * @param enableMetrics true if automatic Metrics should be enabled */ public void setEnabled(final boolean enableMetrics) { this.enable = enableMetrics; diff --git a/sentry/src/main/java/io/sentry/logger/LoggerApi.java b/sentry/src/main/java/io/sentry/logger/LoggerApi.java index c203dcbfb8f..3741ddc6209 100644 --- a/sentry/src/main/java/io/sentry/logger/LoggerApi.java +++ b/sentry/src/main/java/io/sentry/logger/LoggerApi.java @@ -104,13 +104,6 @@ private void captureLog( return; } - if (!options.getLogs().isEnabled()) { - options - .getLogger() - .log(SentryLevel.WARNING, "Sentry Log is disabled and this 'logger' call is a no-op."); - return; - } - if (message == null) { return; } diff --git a/sentry/src/main/java/io/sentry/logger/LoggerBatchProcessor.java b/sentry/src/main/java/io/sentry/logger/LoggerBatchProcessor.java index 71877c21dae..605e9c137ce 100644 --- a/sentry/src/main/java/io/sentry/logger/LoggerBatchProcessor.java +++ b/sentry/src/main/java/io/sentry/logger/LoggerBatchProcessor.java @@ -36,6 +36,7 @@ public class LoggerBatchProcessor implements ILoggerBatchProcessor { private final @NotNull Queue queue; private final @NotNull ISentryExecutorService executorService; private final @NotNull AtomicBoolean hasScheduled = new AtomicBoolean(false); + protected volatile boolean hasAcceptedItem = false; private volatile boolean isShuttingDown = false; private final @NotNull ReusableCountLatch pendingCount = new ReusableCountLatch(); @@ -75,6 +76,7 @@ public void add(final @NotNull SentryLogEvent logEvent) { } pendingCount.increment(); queue.offer(logEvent); + hasAcceptedItem = true; maybeSchedule(false); } @@ -82,14 +84,14 @@ public void add(final @NotNull SentryLogEvent logEvent) { @Override public void close(final boolean isRestarting) { isShuttingDown = true; - if (isRestarting) { + if (isRestarting && hasAcceptedItem) { maybeSchedule(true); executorService.submit(() -> executorService.close(options.getShutdownTimeoutMillis())); - } else { - executorService.close(options.getShutdownTimeoutMillis()); - while (!queue.isEmpty()) { - flushBatch(); - } + return; + } + executorService.close(options.getShutdownTimeoutMillis()); + while (!queue.isEmpty()) { + flushBatch(); } } @@ -114,6 +116,9 @@ private void maybeSchedule(boolean immediately) { @Override public void flush(long timeoutMillis) { + if (!hasAcceptedItem) { + return; + } maybeSchedule(true); try { pendingCount.waitTillZero(timeoutMillis, TimeUnit.MILLISECONDS); diff --git a/sentry/src/main/java/io/sentry/metrics/MetricsApi.java b/sentry/src/main/java/io/sentry/metrics/MetricsApi.java index cebcad9735c..d70b4ab5c97 100644 --- a/sentry/src/main/java/io/sentry/metrics/MetricsApi.java +++ b/sentry/src/main/java/io/sentry/metrics/MetricsApi.java @@ -118,15 +118,6 @@ private void captureMetrics( return; } - if (!options.getMetrics().isEnabled()) { - options - .getLogger() - .log( - SentryLevel.WARNING, - "Sentry Metrics is disabled and this 'metrics' call is a no-op."); - return; - } - if (name == null) { return; } diff --git a/sentry/src/main/java/io/sentry/metrics/MetricsBatchProcessor.java b/sentry/src/main/java/io/sentry/metrics/MetricsBatchProcessor.java index 3c744dbe3c5..a8b58b722a2 100644 --- a/sentry/src/main/java/io/sentry/metrics/MetricsBatchProcessor.java +++ b/sentry/src/main/java/io/sentry/metrics/MetricsBatchProcessor.java @@ -34,6 +34,7 @@ public class MetricsBatchProcessor implements IMetricsBatchProcessor { private final @NotNull Queue queue; private final @NotNull ISentryExecutorService executorService; private final @NotNull AtomicBoolean hasScheduled = new AtomicBoolean(false); + protected volatile boolean hasAcceptedItem = false; private volatile boolean isShuttingDown = false; private final @NotNull ReusableCountLatch pendingCount = new ReusableCountLatch(); @@ -65,6 +66,7 @@ public void add(final @NotNull SentryMetricsEvent metricsEvent) { } pendingCount.increment(); queue.offer(metricsEvent); + hasAcceptedItem = true; maybeSchedule(false); } @@ -72,14 +74,14 @@ public void add(final @NotNull SentryMetricsEvent metricsEvent) { @Override public void close(final boolean isRestarting) { isShuttingDown = true; - if (isRestarting) { + if (isRestarting && hasAcceptedItem) { maybeSchedule(true); executorService.submit(() -> executorService.close(options.getShutdownTimeoutMillis())); - } else { - executorService.close(options.getShutdownTimeoutMillis()); - while (!queue.isEmpty()) { - flushBatch(); - } + return; + } + executorService.close(options.getShutdownTimeoutMillis()); + while (!queue.isEmpty()) { + flushBatch(); } } @@ -106,6 +108,9 @@ private void maybeSchedule(boolean immediately) { @Override public void flush(long timeoutMillis) { + if (!hasAcceptedItem) { + return; + } maybeSchedule(true); try { pendingCount.waitTillZero(timeoutMillis, TimeUnit.MILLISECONDS); diff --git a/sentry/src/test/java/io/sentry/ScopesTest.kt b/sentry/src/test/java/io/sentry/ScopesTest.kt index d1cb38c6495..13a57ebe70a 100644 --- a/sentry/src/test/java/io/sentry/ScopesTest.kt +++ b/sentry/src/test/java/io/sentry/ScopesTest.kt @@ -2536,11 +2536,21 @@ class ScopesTest { } @Test - fun `when logging is not enabled, do nothing`() { + fun `when Logs option is omitted, capture manual log`() { val (sut, mockClient) = getEnabledScopes() sut.logger().warn("test message") - verify(mockClient, never()).captureLog(any(), anyOrNull()) + + verify(mockClient).captureLog(any(), anyOrNull()) + } + + @Test + fun `when Logs option is disabled, capture manual log`() { + val (sut, mockClient) = getEnabledScopes { it.logs.isEnabled = false } + + sut.logger().warn("test message") + + verify(mockClient).captureLog(any(), anyOrNull()) } @Test @@ -3316,11 +3326,12 @@ class ScopesTest { } @Test - fun `when metrics is not enabled, do nothing`() { + fun `when Metrics option is disabled, capture manual metric`() { val (sut, mockClient) = getEnabledScopes { it.metrics.isEnabled = false } sut.metrics().count("metric name") - verify(mockClient, never()).captureMetric(any(), anyOrNull(), anyOrNull()) + + verify(mockClient).captureMetric(any(), anyOrNull(), anyOrNull()) } @Test diff --git a/sentry/src/test/java/io/sentry/SentryClientTest.kt b/sentry/src/test/java/io/sentry/SentryClientTest.kt index 61181ee96a6..d9ed1df0f2d 100644 --- a/sentry/src/test/java/io/sentry/SentryClientTest.kt +++ b/sentry/src/test/java/io/sentry/SentryClientTest.kt @@ -153,6 +153,17 @@ class SentryClientTest { assertTrue(sut.isEnabled) } + @Test + fun `when Logs and Metrics options are disabled, batch processors are still created`() { + val sut = fixture.getSut { + it.logs.isEnabled = false + it.metrics.isEnabled = false + } + + verify(fixture.loggerBatchProcessorFactory).create(fixture.sentryOptions, sut) + verify(fixture.metricsBatchProcessorFactory).create(fixture.sentryOptions, sut) + } + @Test fun `when dsn is an invalid string, client throws`() { fixture.sentryOptions.dsn = "invalid-dsn" diff --git a/sentry/src/test/java/io/sentry/SentryTest.kt b/sentry/src/test/java/io/sentry/SentryTest.kt index 3d91c0f8ead..72400f3e339 100644 --- a/sentry/src/test/java/io/sentry/SentryTest.kt +++ b/sentry/src/test/java/io/sentry/SentryTest.kt @@ -22,6 +22,7 @@ import io.sentry.test.NonOverridableNoOpSentryExecutorService import io.sentry.test.createSentryClientMock import io.sentry.test.initForTest import io.sentry.test.injectForField +import io.sentry.transport.ITransport import io.sentry.util.PlatformTestManipulator import io.sentry.util.thread.IThreadChecker import io.sentry.util.thread.ThreadChecker @@ -52,6 +53,8 @@ import org.junit.rules.TemporaryFolder import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.argThat +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.atLeastOnce import org.mockito.kotlin.check import org.mockito.kotlin.eq import org.mockito.kotlin.inOrder @@ -174,6 +177,30 @@ class SentryTest { assertFalse(Sentry.getGlobalScope().client.isEnabled) } + @Test + fun `manual Logs and Metrics reach transport when automatic collection is disabled`() { + val transport = mock() + val transportFactory = mock() + whenever(transportFactory.create(any(), any())).thenReturn(transport) + initForTest { + it.dsn = dsn + it.logs.isEnabled = false + it.metrics.isEnabled = false + it.setTransportFactory(transportFactory) + it.shutdownTimeoutMillis = 5_000 + } + + Sentry.logger().info("manual log") + Sentry.metrics().count("manual metric") + Sentry.flush(5_000) + + val envelopes = argumentCaptor() + verify(transport, atLeastOnce()).send(envelopes.capture()) + val itemTypes = + envelopes.allValues.flatMap { envelope -> envelope.items.map { it.header.type } } + assertThat(itemTypes).containsAtLeast(SentryItemType.Log, SentryItemType.TraceMetric) + } + @Test fun `close calls scopes close with isRestarting false`() { val scopes = mock() diff --git a/sentry/src/test/java/io/sentry/logger/LoggerBatchProcessorTest.kt b/sentry/src/test/java/io/sentry/logger/LoggerBatchProcessorTest.kt index 01b8afdb4a3..28637b4f5e8 100644 --- a/sentry/src/test/java/io/sentry/logger/LoggerBatchProcessorTest.kt +++ b/sentry/src/test/java/io/sentry/logger/LoggerBatchProcessorTest.kt @@ -3,6 +3,7 @@ package io.sentry.logger import com.google.common.truth.Truth.assertThat import io.sentry.DataCategory import io.sentry.ISentryClient +import io.sentry.ISentryExecutorService import io.sentry.SentryLogEvent import io.sentry.SentryLogEvents import io.sentry.SentryLogLevel @@ -19,13 +20,40 @@ import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertTrue +import org.mockito.kotlin.any import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.atLeast import org.mockito.kotlin.mock +import org.mockito.kotlin.never import org.mockito.kotlin.times import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions class LoggerBatchProcessorTest { + @Test + fun `flush without accepted items does not submit processor work`() { + val mockExecutor = mock() + val processor = LoggerBatchProcessor(SentryOptions(), mock(), mockExecutor) + + processor.flush(0) + + verifyNoInteractions(mockExecutor) + } + + @Test + fun `close without accepted items does not submit processor work`() { + for (isRestarting in listOf(false, true)) { + val mockExecutor = mock() + val processor = LoggerBatchProcessor(SentryOptions(), mock(), mockExecutor) + + processor.close(isRestarting) + + verify(mockExecutor).close(any()) + verify(mockExecutor, never()).schedule(any(), any()) + verify(mockExecutor, never()).submit(any()) + } + } + @Test fun `schedules another flush after previous flush has run`() { val mockClient = mock() diff --git a/sentry/src/test/java/io/sentry/metrics/MetricsBatchProcessorTest.kt b/sentry/src/test/java/io/sentry/metrics/MetricsBatchProcessorTest.kt index d8320d9b1a6..fafefa5e9fb 100644 --- a/sentry/src/test/java/io/sentry/metrics/MetricsBatchProcessorTest.kt +++ b/sentry/src/test/java/io/sentry/metrics/MetricsBatchProcessorTest.kt @@ -3,6 +3,7 @@ package io.sentry.metrics import com.google.common.truth.Truth.assertThat import io.sentry.DataCategory import io.sentry.ISentryClient +import io.sentry.ISentryExecutorService import io.sentry.SentryMetricsEvent import io.sentry.SentryMetricsEvents import io.sentry.SentryNanotimeDate @@ -18,13 +19,42 @@ import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertTrue +import org.mockito.kotlin.any import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.atLeast import org.mockito.kotlin.mock +import org.mockito.kotlin.never import org.mockito.kotlin.times import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions class MetricsBatchProcessorTest { + @Test + fun `flush without accepted items does not submit processor work`() { + val mockExecutor = mock() + val processor = MetricsBatchProcessor(SentryOptions(), mock()) + processor.injectForField("executorService", mockExecutor) + + processor.flush(0) + + verifyNoInteractions(mockExecutor) + } + + @Test + fun `close without accepted items does not submit processor work`() { + for (isRestarting in listOf(false, true)) { + val mockExecutor = mock() + val processor = MetricsBatchProcessor(SentryOptions(), mock()) + processor.injectForField("executorService", mockExecutor) + + processor.close(isRestarting) + + verify(mockExecutor).close(any()) + verify(mockExecutor, never()).schedule(any(), any()) + verify(mockExecutor, never()).submit(any()) + } + } + @Test fun `schedules another flush after previous flush has run`() { val mockClient = mock() From 2405019dd870b1671384f6a8dcef8080d0346fb2 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Tue, 29 Sep 2026 10:06:30 +0200 Subject: [PATCH 2/9] changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5476f6d9831..d3461ff25cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ - Deprecate `sendDefaultPii` in favor of `dataCollection` ahead of its removal in 9.0 ([#6158](https://github.com/getsentry/sentry-java/pull/6158)) - Make the tombstone merge time threshold configurable via `SentryAndroidOptions.setTombstoneMergeTimeThresholdMillis` and the `io.sentry.tombstone.merge-time-threshold-millis` manifest option ([#6154](https://github.com/getsentry/sentry-java/pull/6154)) +### Fixes + +- Capture direct `Sentry.logger()` and `Sentry.metrics()` calls regardless of the legacy enable flags. The flags continue to control automatic integrations; use the before-send callbacks to filter manually emitted telemetry ([#6184](https://github.com/getsentry/sentry-java/pull/6184)) + ### Improvements - Recover Android 17 `MemoryLimiter` app exits recorded as `ApplicationExitInfo.REASON_MEMORY_LIMITER` ([#6174](https://github.com/getsentry/sentry-java/pull/6174)) From c7b1cc3ca069036ba2b8bd7ddacfa76cbc42c766 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Tue, 29 Sep 2026 11:53:27 +0200 Subject: [PATCH 3/9] docs: Clarify manual telemetry changelog guidance Separate the automatic-integration behavior from manual filtering instructions. Name the Logs and Metrics before-send APIs and explain how to drop manually emitted telemetry. Co-Authored-By: Claude --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d3461ff25cc..d8a822a0694 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,9 @@ ### Fixes -- Capture direct `Sentry.logger()` and `Sentry.metrics()` calls regardless of the legacy enable flags. The flags continue to control automatic integrations; use the before-send callbacks to filter manually emitted telemetry ([#6184](https://github.com/getsentry/sentry-java/pull/6184)) +- Capture direct `Sentry.logger()` and `Sentry.metrics()` calls regardless of the legacy enable flags ([#6184](https://github.com/getsentry/sentry-java/pull/6184)) + - The flags continue to control automatic integrations. + - Use `options.getLogs().setBeforeSend(...)` and `options.getMetrics().setBeforeSend(...)` to filter manually emitted telemetry. Return `null` from either callback to drop it. ### Improvements From d222b1e47b84b233f27090f2b250e61ba05dcf72 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Tue, 29 Sep 2026 15:51:19 +0200 Subject: [PATCH 4/9] docs: Announce telemetry option removal Clarify that the Logs and Metrics enable options will be removed in the next major release. Note that logging integrations will receive individual opt-in flags. Co-Authored-By: Claude --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8a822a0694..9f3b20586e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,8 +9,8 @@ ### Fixes -- Capture direct `Sentry.logger()` and `Sentry.metrics()` calls regardless of the legacy enable flags ([#6184](https://github.com/getsentry/sentry-java/pull/6184)) - - The flags continue to control automatic integrations. +- Capture direct `Sentry.logger()` and `Sentry.metrics()` calls regardless of `options.getLogs().isEnabled()` and `options.getMetrics().isEnabled()` ([#6184](https://github.com/getsentry/sentry-java/pull/6184)) + - `options.getLogs().setEnabled(...)` and `options.getMetrics().setEnabled(...)` continue to control automatic integrations, but will be removed in the upcoming major release. Each logging integration will then have a separate opt-in flag. - Use `options.getLogs().setBeforeSend(...)` and `options.getMetrics().setBeforeSend(...)` to filter manually emitted telemetry. Return `null` from either callback to drop it. ### Improvements From ddabd20f43b9f35dd4420b8be8d0e5ee476e5fa5 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 30 Sep 2026 13:35:42 +0200 Subject: [PATCH 5/9] docs(android): Mention Timber in Logs manifest example Clarify that io.sentry.logs.enabled controls automatic logs from both Logcat and Timber integrations. Co-Authored-By: Claude --- .../sentry-samples-android/src/main/AndroidManifest.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml b/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml index ad925fae82e..cf0bc55e5e0 100644 --- a/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml +++ b/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml @@ -147,7 +147,7 @@ android:name="io.sentry.debug" android:value="${sentryDebug}" /> - + From 6644031999c1690aa5c5fa0e494bb06cf9d3e9cc Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 30 Sep 2026 14:33:19 +0200 Subject: [PATCH 6/9] docs: Mark telemetry ungating as a breaking change Move the manual Logs and Metrics behavior change into the breaking changes section so its impact is clear to users. Co-Authored-By: Claude --- CHANGELOG.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f3b20586e3..544f2fb166a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,17 +2,17 @@ ## Unreleased -### Features - -- Deprecate `sendDefaultPii` in favor of `dataCollection` ahead of its removal in 9.0 ([#6158](https://github.com/getsentry/sentry-java/pull/6158)) -- Make the tombstone merge time threshold configurable via `SentryAndroidOptions.setTombstoneMergeTimeThresholdMillis` and the `io.sentry.tombstone.merge-time-threshold-millis` manifest option ([#6154](https://github.com/getsentry/sentry-java/pull/6154)) - -### Fixes +### Breaking Changes - Capture direct `Sentry.logger()` and `Sentry.metrics()` calls regardless of `options.getLogs().isEnabled()` and `options.getMetrics().isEnabled()` ([#6184](https://github.com/getsentry/sentry-java/pull/6184)) - `options.getLogs().setEnabled(...)` and `options.getMetrics().setEnabled(...)` continue to control automatic integrations, but will be removed in the upcoming major release. Each logging integration will then have a separate opt-in flag. - Use `options.getLogs().setBeforeSend(...)` and `options.getMetrics().setBeforeSend(...)` to filter manually emitted telemetry. Return `null` from either callback to drop it. +### Features + +- Deprecate `sendDefaultPii` in favor of `dataCollection` ahead of its removal in 9.0 ([#6158](https://github.com/getsentry/sentry-java/pull/6158)) +- Make the tombstone merge time threshold configurable via `SentryAndroidOptions.setTombstoneMergeTimeThresholdMillis` and the `io.sentry.tombstone.merge-time-threshold-millis` manifest option ([#6154](https://github.com/getsentry/sentry-java/pull/6154)) + ### Improvements - Recover Android 17 `MemoryLimiter` app exits recorded as `ApplicationExitInfo.REASON_MEMORY_LIMITER` ([#6174](https://github.com/getsentry/sentry-java/pull/6174)) From 2815147fd7e3a3024afc51fd83506e5abedac212 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 30 Sep 2026 14:38:19 +0200 Subject: [PATCH 7/9] docs(core): Clarify Metrics enable option behavior Document that the Metrics enable option currently has no effect because there are no automatic Metrics integrations. Co-Authored-By: Claude --- CHANGELOG.md | 2 +- .../src/main/java/io/sentry/SentryOptions.java | 18 +++++++++++------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 544f2fb166a..5c4a8902eae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ ### Breaking Changes - Capture direct `Sentry.logger()` and `Sentry.metrics()` calls regardless of `options.getLogs().isEnabled()` and `options.getMetrics().isEnabled()` ([#6184](https://github.com/getsentry/sentry-java/pull/6184)) - - `options.getLogs().setEnabled(...)` and `options.getMetrics().setEnabled(...)` continue to control automatic integrations, but will be removed in the upcoming major release. Each logging integration will then have a separate opt-in flag. + - `options.getLogs().setEnabled(...)` continues to control automatic logging integrations. `options.getMetrics().setEnabled(...)` currently has no effect because there are no automatic Metrics integrations. Both options will be removed in the upcoming major release, and each logging integration will then have a separate opt-in flag. - Use `options.getLogs().setBeforeSend(...)` and `options.getMetrics().setBeforeSend(...)` to filter manually emitted telemetry. Return `null` from either callback to drop it. ### Features diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index f077ba16276..2b2f9d370a0 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -4173,7 +4173,7 @@ public interface BeforeSendLogCallback { public static final class Metrics { - /** Whether automatic metrics integrations send Metrics to Sentry. */ + /** The configured state for automatic Metrics integrations, which currently do not exist. */ private boolean enable = true; /** @@ -4186,20 +4186,24 @@ public static final class Metrics { new DefaultMetricsBatchProcessorFactory(); /** - * Whether automatic metrics integrations send Metrics to Sentry. Direct {@code - * Sentry.metrics()} calls are always captured when the SDK is enabled. + * Returns the configured state for automatic Metrics integrations. * - * @return true if automatic Metrics should be enabled + *

This option currently has no effect because there are no automatic Metrics integrations. + * Direct {@code Sentry.metrics()} calls are always captured when the SDK is enabled. + * + * @return the configured value for automatic Metrics integrations */ public boolean isEnabled() { return enable; } /** - * Whether automatic metrics integrations send Metrics to Sentry. Direct {@code - * Sentry.metrics()} calls are always captured when the SDK is enabled. + * Configures whether automatic Metrics integrations send Metrics to Sentry. + * + *

This option currently has no effect because there are no automatic Metrics integrations. + * Direct {@code Sentry.metrics()} calls are always captured when the SDK is enabled. * - * @param enableMetrics true if automatic Metrics should be enabled + * @param enableMetrics whether automatic Metrics integrations should be enabled */ public void setEnabled(final boolean enableMetrics) { this.enable = enableMetrics; From d6d22b7b19499c2fbd2d8ed86adeb191a76aa0c1 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Thu, 1 Oct 2026 06:22:21 +0200 Subject: [PATCH 8/9] docs(core): Explain synchronous processor shutdown Clarify that restarting an unused Logs or Metrics processor takes the synchronous shutdown path because its executor has no scheduled work. Co-Authored-By: Claude --- sentry/src/main/java/io/sentry/logger/LoggerBatchProcessor.java | 2 ++ .../src/main/java/io/sentry/metrics/MetricsBatchProcessor.java | 2 ++ 2 files changed, 4 insertions(+) diff --git a/sentry/src/main/java/io/sentry/logger/LoggerBatchProcessor.java b/sentry/src/main/java/io/sentry/logger/LoggerBatchProcessor.java index 605e9c137ce..b9c32f4c0ff 100644 --- a/sentry/src/main/java/io/sentry/logger/LoggerBatchProcessor.java +++ b/sentry/src/main/java/io/sentry/logger/LoggerBatchProcessor.java @@ -89,6 +89,8 @@ public void close(final boolean isRestarting) { executorService.submit(() -> executorService.close(options.getShutdownTimeoutMillis())); return; } + // On restart, reaching this path means the executor never had anything scheduled, so + // synchronous shutdown should be immediate. executorService.close(options.getShutdownTimeoutMillis()); while (!queue.isEmpty()) { flushBatch(); diff --git a/sentry/src/main/java/io/sentry/metrics/MetricsBatchProcessor.java b/sentry/src/main/java/io/sentry/metrics/MetricsBatchProcessor.java index a8b58b722a2..8ac387c8b8e 100644 --- a/sentry/src/main/java/io/sentry/metrics/MetricsBatchProcessor.java +++ b/sentry/src/main/java/io/sentry/metrics/MetricsBatchProcessor.java @@ -79,6 +79,8 @@ public void close(final boolean isRestarting) { executorService.submit(() -> executorService.close(options.getShutdownTimeoutMillis())); return; } + // On restart, reaching this path means the executor never had anything scheduled, so + // synchronous shutdown should be immediate. executorService.close(options.getShutdownTimeoutMillis()); while (!queue.isEmpty()) { flushBatch(); From 6aca059e0aa633519a195a595ce836ea7f9838a3 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Thu, 1 Oct 2026 06:24:29 +0200 Subject: [PATCH 9/9] docs: Clarify automatic Logs integration enablement Document that automatic logging integrations remain gated by the Logs enable option while direct logger calls bypass it. Co-Authored-By: Claude --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 88b225c2aae..61dec289bd3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ ### Breaking Changes - Capture direct `Sentry.logger()` and `Sentry.metrics()` calls regardless of `options.getLogs().isEnabled()` and `options.getMetrics().isEnabled()` ([#6184](https://github.com/getsentry/sentry-java/pull/6184)) - - `options.getLogs().setEnabled(...)` continues to control automatic logging integrations. `options.getMetrics().setEnabled(...)` currently has no effect because there are no automatic Metrics integrations. Both options will be removed in the upcoming major release, and each logging integration will then have a separate opt-in flag. + - Automatic logging integrations still only capture logs when `options.getLogs().isEnabled()` is `true`. `options.getMetrics().setEnabled(...)` currently has no effect because there are no automatic Metrics integrations. Both enable options will be removed in the upcoming major release, and each logging integration will then have a separate opt-in flag. - Use `options.getLogs().setBeforeSend(...)` and `options.getMetrics().setBeforeSend(...)` to filter manually emitted telemetry. Return `null` from either callback to drop it. ### Fixes