Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 12 additions & 0 deletions sentry-android-core/api/sentry-android-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,18 @@ public class io/sentry/android/core/AndroidMemoryCollector : io/sentry/IPerforma
public fun setup ()V
}

public final class io/sentry/android/core/AndroidMetricsBatchProcessor : io/sentry/metrics/MetricsBatchProcessor, io/sentry/android/core/AppState$AppStateListener {
public fun <init> (Lio/sentry/SentryOptions;Lio/sentry/ISentryClient;)V
public fun close (Z)V
public fun onBackground ()V
public fun onForeground ()V
}

public final class io/sentry/android/core/AndroidMetricsBatchProcessorFactory : io/sentry/metrics/IMetricsBatchProcessorFactory {
public fun <init> ()V
public fun create (Lio/sentry/SentryOptions;Lio/sentry/SentryClient;)Lio/sentry/metrics/IMetricsBatchProcessor;
}

public class io/sentry/android/core/AndroidProfiler {
protected final field lock Lio/sentry/util/AutoClosableReentrantLock;
public fun <init> (Ljava/lang/String;ILio/sentry/android/core/internal/util/SentryFrameMetricsCollector;Lio/sentry/ISentryExecutorService;Lio/sentry/ILogger;)V
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package io.sentry.android.core;

import io.sentry.ISentryClient;
import io.sentry.SentryLevel;
import io.sentry.SentryOptions;
import io.sentry.metrics.MetricsBatchProcessor;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;

@ApiStatus.Internal
public final class AndroidMetricsBatchProcessor extends MetricsBatchProcessor
implements AppState.AppStateListener {

public AndroidMetricsBatchProcessor(
@NotNull SentryOptions options, @NotNull ISentryClient client) {
super(options, client);
AppState.getInstance().addAppStateListener(this);
}

@Override
public void onForeground() {
// no-op
}

@Override
public void onBackground() {
try {
options
.getExecutorService()
.submit(
new Runnable() {
@Override
public void run() {
flush(MetricsBatchProcessor.FLUSH_AFTER_MS);
}
});
} catch (Throwable t) {
options
.getLogger()
.log(SentryLevel.ERROR, t, "Failed to submit metrics flush in onBackground()");
}
}

@Override
public void close(boolean isRestarting) {
AppState.getInstance().removeAppStateListener(this);
super.close(isRestarting);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.sentry.android.core;

import io.sentry.SentryClient;
import io.sentry.SentryOptions;
import io.sentry.metrics.IMetricsBatchProcessor;
import io.sentry.metrics.IMetricsBatchProcessorFactory;
import org.jetbrains.annotations.NotNull;

public final class AndroidMetricsBatchProcessorFactory implements IMetricsBatchProcessorFactory {
@Override
public @NotNull IMetricsBatchProcessor create(
@NotNull SentryOptions options, @NotNull SentryClient client) {
return new AndroidMetricsBatchProcessor(options, client);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ static void loadDefaultAndMetadataOptions(
options.setDateProvider(new SentryAndroidDateProvider());
options.setRuntimeManager(new AndroidRuntimeManager());
options.getLogs().setLoggerBatchProcessorFactory(new AndroidLoggerBatchProcessorFactory());
options.getMetrics().setMetricsBatchProcessorFactory(new AndroidMetricsBatchProcessorFactory());

// set a lower flush timeout on Android to avoid ANRs
options.setFlushTimeoutMillis(DEFAULT_FLUSH_TIMEOUT_MS);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package io.sentry.android.core

import androidx.test.ext.junit.runners.AndroidJUnit4
import io.sentry.SentryClient
import kotlin.test.Test
import kotlin.test.assertIs
import org.junit.runner.RunWith
import org.mockito.kotlin.mock

@RunWith(AndroidJUnit4::class)
class AndroidMetricsBatchProcessorFactoryTest {

@Test
fun `create returns AndroidMetricsBatchProcessor instance`() {
val factory = AndroidMetricsBatchProcessorFactory()
val options = SentryAndroidOptions()
val client: SentryClient = mock()

val processor = factory.create(options, client)

assertIs<AndroidMetricsBatchProcessor>(processor)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package io.sentry.android.core

import androidx.test.ext.junit.runners.AndroidJUnit4
import io.sentry.ISentryClient
import io.sentry.SentryMetricsEvent
import io.sentry.SentryOptions
import io.sentry.protocol.SentryId
import io.sentry.test.ImmediateExecutorService
import kotlin.test.AfterTest
import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
import org.junit.runner.RunWith
import org.mockito.kotlin.any
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever

@RunWith(AndroidJUnit4::class)
class AndroidMetricsBatchProcessorTest {

private class Fixture {
val options = SentryAndroidOptions()
val client: ISentryClient = mock()

fun getSut(
useImmediateExecutor: Boolean = false,
config: ((SentryOptions) -> Unit)? = null,
): AndroidMetricsBatchProcessor {
if (useImmediateExecutor) {
options.executorService = ImmediateExecutorService()
}
config?.invoke(options)
return AndroidMetricsBatchProcessor(options, client)
}
}

private val fixture = Fixture()

@BeforeTest
fun `set up`() {
AppState.getInstance().resetInstance()
}

@AfterTest
fun `tear down`() {
AppState.getInstance().resetInstance()
}

@Test
fun `constructor registers as AppState listener`() {
fixture.getSut()
assertNotNull(AppState.getInstance().lifecycleObserver)
}

@Test
fun `onBackground schedules flush`() {
val sut = fixture.getSut(useImmediateExecutor = true)
val metricsEvent = SentryMetricsEvent(SentryId(), 1.0, "test", "counter", 3.0)
sut.add(metricsEvent)

sut.onBackground()

verify(fixture.client).captureBatchedMetricsEvents(any())
}

@Test
fun `onBackground handles executor exception gracefully`() {
val sut =
fixture.getSut { options ->
val rejectingExecutor = mock<io.sentry.ISentryExecutorService>()
whenever(rejectingExecutor.submit(any())).thenThrow(RuntimeException("Rejected"))
options.executorService = rejectingExecutor
}

// Should not throw
sut.onBackground()
}

@Test
fun `close removes AppState listener`() {
val sut = fixture.getSut()
sut.close(false)

assertTrue(AppState.getInstance().lifecycleObserver.listeners.isEmpty())
}

@Test
fun `close with isRestarting true still removes listener`() {
val sut = fixture.getSut()
sut.close(true)

assertTrue(AppState.getInstance().lifecycleObserver.listeners.isEmpty())
}
}
Loading