Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
01b74ec
feat(android): Warn for legacy Metrics metadata
adinauer Aug 13, 2026
5f068d3
feat(core): Warn for legacy Metrics configuration
adinauer Aug 13, 2026
61f485a
feat(spring): Warn for legacy Metrics property
adinauer Aug 13, 2026
170abc9
perf(core): Start Metrics batch worker on first use
adinauer Aug 13, 2026
def8c18
Merge branch 'feat/remove-metrics-enable-flag' into feat/warn-legacy-…
adinauer Aug 13, 2026
35c93b5
Merge branch 'feat/warn-legacy-metrics-manifest' into feat/warn-legac…
adinauer Aug 13, 2026
9545c0c
Merge branch 'feat/warn-legacy-metrics-external' into feat/warn-legac…
adinauer Aug 13, 2026
53c1405
Merge branch 'feat/warn-legacy-metrics-spring' into perf/metrics-batc…
adinauer Aug 13, 2026
b87e331
fix(core): Keep Metrics test constructor internal
adinauer Aug 13, 2026
281f533
Merge branch 'feat/remove-metrics-enable-flag' into feat/warn-legacy-…
adinauer Aug 13, 2026
57aee1b
Merge branch 'feat/warn-legacy-metrics-manifest' into feat/warn-legac…
adinauer Aug 13, 2026
532ecf2
Merge branch 'feat/warn-legacy-metrics-external' into feat/warn-legac…
adinauer Aug 13, 2026
78f04f7
Merge branch 'feat/warn-legacy-metrics-spring' into perf/metrics-batc…
adinauer Aug 13, 2026
fb24f93
chore(stack): Merge preceding stack changes
adinauer Sep 29, 2026
d0923b2
chore(stack): Merge preceding stack changes
adinauer Sep 29, 2026
ec1a6e2
chore(stack): Merge preceding stack changes
adinauer Sep 29, 2026
9583935
chore(stack): Merge preceding stack changes
adinauer Sep 29, 2026
5ebb4c6
chore(stack): Merge preceding stack changes
adinauer Sep 29, 2026
8cf1b6a
fix(android): Use metadata helper for legacy Metrics option
adinauer Sep 29, 2026
809fd1f
chore(stack): Merge preceding stack changes
adinauer Sep 29, 2026
59221c9
chore(stack): Merge preceding stack changes
adinauer Sep 29, 2026
cafd4d2
chore(stack): Merge preceding stack changes
adinauer Sep 29, 2026
2a2032f
chore(stack): Merge preceding stack changes
adinauer Sep 30, 2026
06ebc5c
chore(stack): Merge preceding stack changes
adinauer Sep 30, 2026
687862f
chore(stack): Merge preceding stack changes
adinauer Sep 30, 2026
58989ab
chore(stack): Merge preceding stack changes
adinauer Sep 30, 2026
02c5be6
chore(stack): Merge preceding stack changes
adinauer Sep 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package io.sentry.android.core

import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
import io.sentry.ISentryClient
import io.sentry.SentryMetricsEvent
import io.sentry.SentryOptions
import io.sentry.protocol.SentryId
import io.sentry.test.ImmediateExecutorService
import io.sentry.test.getProperty
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.test.AfterTest
import kotlin.test.BeforeTest
import kotlin.test.Test
Expand All @@ -14,6 +17,7 @@ import kotlin.test.assertTrue
import org.junit.runner.RunWith
import org.mockito.kotlin.any
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever

Expand Down Expand Up @@ -54,6 +58,16 @@ class AndroidMetricsBatchProcessorTest {
assertNotNull(AppState.getInstance().lifecycleObserver)
}

@Test
fun `onBackground does not flush before first accepted item`() {
val sut = fixture.getSut(useImmediateExecutor = true)

sut.onBackground()

assertThat(sut.getProperty<AtomicBoolean>("hasScheduled").get()).isFalse()
verify(fixture.client, never()).captureBatchedMetricsEvents(any())
}

@Test
fun `onBackground schedules flush`() {
val sut = fixture.getSut(useImmediateExecutor = true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,24 @@ public class MetricsBatchProcessor implements IMetricsBatchProcessor {
private final @NotNull Queue<SentryMetricsEvent> queue;
private final @NotNull ISentryExecutorService executorService;
private final @NotNull AtomicBoolean hasScheduled = new AtomicBoolean(false);
private volatile boolean hasAcceptedItem = false;
private volatile boolean isShuttingDown = false;

private final @NotNull ReusableCountLatch pendingCount = new ReusableCountLatch();

public MetricsBatchProcessor(
final @NotNull SentryOptions options, final @NotNull ISentryClient client) {
this(options, client, new SentryExecutorService(options));
}

MetricsBatchProcessor(
final @NotNull SentryOptions options,
final @NotNull ISentryClient client,
final @NotNull ISentryExecutorService executorService) {
this.options = options;
this.client = client;
this.queue = new ConcurrentLinkedQueue<>();
this.executorService = new SentryExecutorService(options);
this.executorService = executorService;
}

@Override
Expand All @@ -65,21 +73,22 @@ public void add(final @NotNull SentryMetricsEvent metricsEvent) {
}
pendingCount.increment();
queue.offer(metricsEvent);
hasAcceptedItem = true;
maybeSchedule(false);
}

@SuppressWarnings("FutureReturnValueIgnored")
@Override
public void close(final boolean isRestarting) {
isShuttingDown = true;
if (isRestarting) {
if (isRestarting && hasAcceptedItem) {
maybeSchedule(true);
executorService.submit(() -> executorService.close(options.getShutdownTimeoutMillis()));
Comment thread
adinauer marked this conversation as resolved.
} else {
executorService.close(options.getShutdownTimeoutMillis());
while (!queue.isEmpty()) {
flushBatch();
}
return;
}
executorService.close(options.getShutdownTimeoutMillis());
while (!queue.isEmpty()) {
flushBatch();
}
}

Expand All @@ -106,6 +115,9 @@ private void maybeSchedule(boolean immediately) {

@Override
public void flush(long timeoutMillis) {
if (!hasAcceptedItem) {
return;
}
maybeSchedule(true);
try {
pendingCount.waitTillZero(timeoutMillis, TimeUnit.MILLISECONDS);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -12,19 +13,106 @@ import io.sentry.clientreport.DiscardReason
import io.sentry.clientreport.DiscardedEvent
import io.sentry.protocol.SentryId
import io.sentry.test.DeferredExecutorService
import io.sentry.test.getProperty
import io.sentry.test.injectForField
import io.sentry.transport.ReusableCountLatch
import io.sentry.util.JsonSerializationUtils
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 `constructor does not submit processor work`() {
val mockExecutor = mock<ISentryExecutorService>()

MetricsBatchProcessor(SentryOptions(), mock(), mockExecutor)

verifyNoInteractions(mockExecutor)
}

@Test
fun `empty flush does not submit processor work`() {
val mockExecutor = mock<ISentryExecutorService>()
val processor = MetricsBatchProcessor(SentryOptions(), mock(), mockExecutor)

processor.flush(0)

verifyNoInteractions(mockExecutor)
}

@Test
fun `close before first accepted item does not submit processor work`() {
val mockExecutor = mock<ISentryExecutorService>()
val processor = MetricsBatchProcessor(SentryOptions(), mock(), mockExecutor)

processor.close(false)

verify(mockExecutor).close(any())
verify(mockExecutor, never()).schedule(any(), any())
verify(mockExecutor, never()).submit(any<Runnable>())
}

@Test
fun `restart close before first accepted item does not submit processor work`() {
val mockExecutor = mock<ISentryExecutorService>()
val processor = MetricsBatchProcessor(SentryOptions(), mock(), mockExecutor)

processor.close(true)

verify(mockExecutor).close(any())
verify(mockExecutor, never()).schedule(any(), any())
verify(mockExecutor, never()).submit(any<Runnable>())
}

@Test
fun `item rejected during shutdown does not mark processor as used`() {
val mockExecutor = mock<ISentryExecutorService>()
val processor = MetricsBatchProcessor(SentryOptions(), mock(), mockExecutor)
processor.close(false)

processor.add(metricsEvent("rejected"))
processor.flush(0)

verify(mockExecutor, never()).schedule(any(), any())
verify(mockExecutor, never()).submit(any<Runnable>())
}

@Test
fun `item rejected due to queue capacity does not mark processor as used`() {
val mockExecutor = mock<ISentryExecutorService>()
val processor = MetricsBatchProcessor(SentryOptions(), mock(), mockExecutor)
val pendingCount = processor.getProperty<ReusableCountLatch>("pendingCount")
repeat(MetricsBatchProcessor.MAX_QUEUE_SIZE) { pendingCount.increment() }

processor.add(metricsEvent("rejected"))
processor.flush(0)

verifyNoInteractions(mockExecutor)
}

@Test
fun `flush and restart close submit processor work after first accepted item`() {
val mockExecutor = mock<ISentryExecutorService>()
val processor = MetricsBatchProcessor(SentryOptions(), mock(), mockExecutor)
processor.add(metricsEvent("accepted"))

processor.flush(0)
processor.close(true)

verify(mockExecutor, times(3)).schedule(any(), any())
verify(mockExecutor).submit(any<Runnable>())
}

@Test
fun `schedules another flush after previous flush has run`() {
val mockClient = mock<ISentryClient>()
Expand All @@ -46,6 +134,9 @@ class MetricsBatchProcessorTest {
.inOrder()
}

private fun metricsEvent(name: String) =
SentryMetricsEvent(SentryId(), SentryNanotimeDate(), name, "gauge", 1.0)

@Test
fun `drops metrics events after reaching MAX_QUEUE_SIZE limit`() {
// given
Expand Down
Loading