Skip to content

Commit c3c0f3b

Browse files
committed
Android Metrics batch processor and factory
1 parent 6788ab9 commit c3c0f3b

File tree

6 files changed

+196
-0
lines changed

6 files changed

+196
-0
lines changed

sentry-android-core/api/sentry-android-core.api

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,18 @@ public class io/sentry/android/core/AndroidMemoryCollector : io/sentry/IPerforma
100100
public fun setup ()V
101101
}
102102

103+
public final class io/sentry/android/core/AndroidMetricsBatchProcessor : io/sentry/metrics/MetricsBatchProcessor, io/sentry/android/core/AppState$AppStateListener {
104+
public fun <init> (Lio/sentry/SentryOptions;Lio/sentry/ISentryClient;)V
105+
public fun close (Z)V
106+
public fun onBackground ()V
107+
public fun onForeground ()V
108+
}
109+
110+
public final class io/sentry/android/core/AndroidMetricsBatchProcessorFactory : io/sentry/metrics/IMetricsBatchProcessorFactory {
111+
public fun <init> ()V
112+
public fun create (Lio/sentry/SentryOptions;Lio/sentry/SentryClient;)Lio/sentry/metrics/IMetricsBatchProcessor;
113+
}
114+
103115
public class io/sentry/android/core/AndroidProfiler {
104116
protected final field lock Lio/sentry/util/AutoClosableReentrantLock;
105117
public fun <init> (Ljava/lang/String;ILio/sentry/android/core/internal/util/SentryFrameMetricsCollector;Lio/sentry/ISentryExecutorService;Lio/sentry/ILogger;)V
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package io.sentry.android.core;
2+
3+
import io.sentry.ISentryClient;
4+
import io.sentry.SentryLevel;
5+
import io.sentry.SentryOptions;
6+
import io.sentry.metrics.MetricsBatchProcessor;
7+
import org.jetbrains.annotations.ApiStatus;
8+
import org.jetbrains.annotations.NotNull;
9+
10+
@ApiStatus.Internal
11+
public final class AndroidMetricsBatchProcessor extends MetricsBatchProcessor
12+
implements AppState.AppStateListener {
13+
14+
public AndroidMetricsBatchProcessor(
15+
@NotNull SentryOptions options, @NotNull ISentryClient client) {
16+
super(options, client);
17+
AppState.getInstance().addAppStateListener(this);
18+
}
19+
20+
@Override
21+
public void onForeground() {
22+
// no-op
23+
}
24+
25+
@Override
26+
public void onBackground() {
27+
try {
28+
options
29+
.getExecutorService()
30+
.submit(
31+
new Runnable() {
32+
@Override
33+
public void run() {
34+
flush(MetricsBatchProcessor.FLUSH_AFTER_MS);
35+
}
36+
});
37+
} catch (Throwable t) {
38+
options
39+
.getLogger()
40+
.log(SentryLevel.ERROR, t, "Failed to submit metrics flush in onBackground()");
41+
}
42+
}
43+
44+
@Override
45+
public void close(boolean isRestarting) {
46+
AppState.getInstance().removeAppStateListener(this);
47+
super.close(isRestarting);
48+
}
49+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package io.sentry.android.core;
2+
3+
import io.sentry.SentryClient;
4+
import io.sentry.SentryOptions;
5+
import io.sentry.metrics.IMetricsBatchProcessor;
6+
import io.sentry.metrics.IMetricsBatchProcessorFactory;
7+
import org.jetbrains.annotations.NotNull;
8+
9+
public final class AndroidMetricsBatchProcessorFactory implements IMetricsBatchProcessorFactory {
10+
@Override
11+
public @NotNull IMetricsBatchProcessor create(
12+
@NotNull SentryOptions options, @NotNull SentryClient client) {
13+
return new AndroidMetricsBatchProcessor(options, client);
14+
}
15+
}

sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ static void loadDefaultAndMetadataOptions(
124124
options.setDateProvider(new SentryAndroidDateProvider());
125125
options.setRuntimeManager(new AndroidRuntimeManager());
126126
options.getLogs().setLoggerBatchProcessorFactory(new AndroidLoggerBatchProcessorFactory());
127+
options.getMetrics().setMetricsBatchProcessorFactory(new AndroidMetricsBatchProcessorFactory());
127128

128129
// set a lower flush timeout on Android to avoid ANRs
129130
options.setFlushTimeoutMillis(DEFAULT_FLUSH_TIMEOUT_MS);
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package io.sentry.android.core
2+
3+
import androidx.test.ext.junit.runners.AndroidJUnit4
4+
import io.sentry.SentryClient
5+
import kotlin.test.Test
6+
import kotlin.test.assertIs
7+
import org.junit.runner.RunWith
8+
import org.mockito.kotlin.mock
9+
10+
@RunWith(AndroidJUnit4::class)
11+
class AndroidMetricsBatchProcessorFactoryTest {
12+
13+
@Test
14+
fun `create returns AndroidMetricsBatchProcessor instance`() {
15+
val factory = AndroidMetricsBatchProcessorFactory()
16+
val options = SentryAndroidOptions()
17+
val client: SentryClient = mock()
18+
19+
val processor = factory.create(options, client)
20+
21+
assertIs<AndroidMetricsBatchProcessor>(processor)
22+
}
23+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package io.sentry.android.core
2+
3+
import androidx.test.ext.junit.runners.AndroidJUnit4
4+
import io.sentry.ISentryClient
5+
import io.sentry.SentryMetricsEvent
6+
import io.sentry.SentryOptions
7+
import io.sentry.protocol.SentryId
8+
import io.sentry.test.ImmediateExecutorService
9+
import kotlin.test.AfterTest
10+
import kotlin.test.BeforeTest
11+
import kotlin.test.Test
12+
import kotlin.test.assertNotNull
13+
import kotlin.test.assertTrue
14+
import org.junit.runner.RunWith
15+
import org.mockito.kotlin.any
16+
import org.mockito.kotlin.mock
17+
import org.mockito.kotlin.verify
18+
import org.mockito.kotlin.whenever
19+
20+
@RunWith(AndroidJUnit4::class)
21+
class AndroidMetricsBatchProcessorTest {
22+
23+
private class Fixture {
24+
val options = SentryAndroidOptions()
25+
val client: ISentryClient = mock()
26+
27+
fun getSut(
28+
useImmediateExecutor: Boolean = false,
29+
config: ((SentryOptions) -> Unit)? = null,
30+
): AndroidMetricsBatchProcessor {
31+
if (useImmediateExecutor) {
32+
options.executorService = ImmediateExecutorService()
33+
}
34+
config?.invoke(options)
35+
return AndroidMetricsBatchProcessor(options, client)
36+
}
37+
}
38+
39+
private val fixture = Fixture()
40+
41+
@BeforeTest
42+
fun `set up`() {
43+
AppState.getInstance().resetInstance()
44+
}
45+
46+
@AfterTest
47+
fun `tear down`() {
48+
AppState.getInstance().resetInstance()
49+
}
50+
51+
@Test
52+
fun `constructor registers as AppState listener`() {
53+
fixture.getSut()
54+
assertNotNull(AppState.getInstance().lifecycleObserver)
55+
}
56+
57+
@Test
58+
fun `onBackground schedules flush`() {
59+
val sut = fixture.getSut(useImmediateExecutor = true)
60+
val metricsEvent = SentryMetricsEvent(SentryId(), 1.0, "test", "counter", 3.0)
61+
sut.add(metricsEvent)
62+
63+
sut.onBackground()
64+
65+
verify(fixture.client).captureBatchedMetricsEvents(any())
66+
}
67+
68+
@Test
69+
fun `onBackground handles executor exception gracefully`() {
70+
val sut =
71+
fixture.getSut { options ->
72+
val rejectingExecutor = mock<io.sentry.ISentryExecutorService>()
73+
whenever(rejectingExecutor.submit(any())).thenThrow(RuntimeException("Rejected"))
74+
options.executorService = rejectingExecutor
75+
}
76+
77+
// Should not throw
78+
sut.onBackground()
79+
}
80+
81+
@Test
82+
fun `close removes AppState listener`() {
83+
val sut = fixture.getSut()
84+
sut.close(false)
85+
86+
assertTrue(AppState.getInstance().lifecycleObserver.listeners.isEmpty())
87+
}
88+
89+
@Test
90+
fun `close with isRestarting true still removes listener`() {
91+
val sut = fixture.getSut()
92+
sut.close(true)
93+
94+
assertTrue(AppState.getInstance().lifecycleObserver.listeners.isEmpty())
95+
}
96+
}

0 commit comments

Comments
 (0)