Skip to content

Commit 2409236

Browse files
committed
feat(android): Add log flushing on app backgrounding
1 parent ee35ac3 commit 2409236

13 files changed

+260
-5
lines changed

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,18 @@ public final class io/sentry/android/core/AndroidLogger : io/sentry/ILogger {
8282
public fun log (Lio/sentry/SentryLevel;Ljava/lang/Throwable;Ljava/lang/String;[Ljava/lang/Object;)V
8383
}
8484

85+
public final class io/sentry/android/core/AndroidLoggerBatchProcessor : io/sentry/logger/LoggerBatchProcessor, io/sentry/android/core/AppState$AppStateListener {
86+
public fun <init> (Lio/sentry/SentryOptions;Lio/sentry/ISentryClient;)V
87+
public fun close (Z)V
88+
public fun onBackground ()V
89+
public fun onForeground ()V
90+
}
91+
92+
public final class io/sentry/android/core/AndroidLoggerBatchProcessorFactory : io/sentry/logger/ILoggerBatchProcessorFactory {
93+
public fun <init> ()V
94+
public fun create (Lio/sentry/SentryOptions;Lio/sentry/SentryClient;)Lio/sentry/logger/ILoggerBatchProcessor;
95+
}
96+
8597
public class io/sentry/android/core/AndroidMemoryCollector : io/sentry/IPerformanceSnapshotCollector {
8698
public fun <init> ()V
8799
public fun collect (Lio/sentry/PerformanceCollectionData;)V
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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.logger.LoggerBatchProcessor;
7+
import org.jetbrains.annotations.ApiStatus;
8+
import org.jetbrains.annotations.NotNull;
9+
10+
@ApiStatus.Internal
11+
public final class AndroidLoggerBatchProcessor extends LoggerBatchProcessor
12+
implements AppState.AppStateListener {
13+
14+
public AndroidLoggerBatchProcessor(
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(LoggerBatchProcessor.FLUSH_AFTER_MS);
35+
}
36+
});
37+
} catch (Throwable t) {
38+
options.getLogger().log(SentryLevel.ERROR, t, "Failed to submit log flush in onBackground()");
39+
}
40+
}
41+
42+
@Override
43+
public void close(boolean isRestarting) {
44+
AppState.getInstance().removeAppStateListener(this);
45+
super.close(isRestarting);
46+
}
47+
}
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.logger.ILoggerBatchProcessor;
6+
import io.sentry.logger.ILoggerBatchProcessorFactory;
7+
import org.jetbrains.annotations.NotNull;
8+
9+
public final class AndroidLoggerBatchProcessorFactory implements ILoggerBatchProcessorFactory {
10+
@Override
11+
public @NotNull ILoggerBatchProcessor create(
12+
@NotNull SentryOptions options, @NotNull SentryClient client) {
13+
return new AndroidLoggerBatchProcessor(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
@@ -123,6 +123,7 @@ static void loadDefaultAndMetadataOptions(
123123
options.setOpenTelemetryMode(SentryOpenTelemetryMode.OFF);
124124
options.setDateProvider(new SentryAndroidDateProvider());
125125
options.setRuntimeManager(new AndroidRuntimeManager());
126+
options.getLogs().setLoggerBatchProcessorFactory(new AndroidLoggerBatchProcessorFactory());
126127

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

sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -774,6 +774,15 @@ class AndroidOptionsInitializerTest {
774774
assertTrue { fixture.sentryOptions.socketTagger is AndroidSocketTagger }
775775
}
776776

777+
@Test
778+
fun `AndroidLoggerBatchProcessorFactory is set to options`() {
779+
fixture.initSut()
780+
781+
assertTrue {
782+
fixture.sentryOptions.logs.loggerBatchProcessorFactory is AndroidLoggerBatchProcessorFactory
783+
}
784+
}
785+
777786
@Test
778787
fun `does not install ComposeGestureTargetLocator, if sentry-compose is not available`() {
779788
fixture.initSutWithClassLoader()

sentry/api/sentry.api

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3669,9 +3669,11 @@ public final class io/sentry/SentryOptions$DistributionOptions {
36693669
public final class io/sentry/SentryOptions$Logs {
36703670
public fun <init> ()V
36713671
public fun getBeforeSend ()Lio/sentry/SentryOptions$Logs$BeforeSendLogCallback;
3672+
public fun getLoggerBatchProcessorFactory ()Lio/sentry/logger/ILoggerBatchProcessorFactory;
36723673
public fun isEnabled ()Z
36733674
public fun setBeforeSend (Lio/sentry/SentryOptions$Logs$BeforeSendLogCallback;)V
36743675
public fun setEnabled (Z)V
3676+
public fun setLoggerBatchProcessorFactory (Lio/sentry/logger/ILoggerBatchProcessorFactory;)V
36753677
}
36763678

36773679
public abstract interface class io/sentry/SentryOptions$Logs$BeforeSendLogCallback {
@@ -5021,6 +5023,11 @@ public abstract interface class io/sentry/internal/viewhierarchy/ViewHierarchyEx
50215023
public abstract fun export (Lio/sentry/protocol/ViewHierarchyNode;Ljava/lang/Object;)Z
50225024
}
50235025

5026+
public final class io/sentry/logger/DefaultLoggerBatchProcessorFactory : io/sentry/logger/ILoggerBatchProcessorFactory {
5027+
public fun <init> ()V
5028+
public fun create (Lio/sentry/SentryOptions;Lio/sentry/SentryClient;)Lio/sentry/logger/ILoggerBatchProcessor;
5029+
}
5030+
50245031
public abstract interface class io/sentry/logger/ILoggerApi {
50255032
public abstract fun debug (Ljava/lang/String;[Ljava/lang/Object;)V
50265033
public abstract fun error (Ljava/lang/String;[Ljava/lang/Object;)V
@@ -5039,6 +5046,10 @@ public abstract interface class io/sentry/logger/ILoggerBatchProcessor {
50395046
public abstract fun flush (J)V
50405047
}
50415048

5049+
public abstract interface class io/sentry/logger/ILoggerBatchProcessorFactory {
5050+
public abstract fun create (Lio/sentry/SentryOptions;Lio/sentry/SentryClient;)Lio/sentry/logger/ILoggerBatchProcessor;
5051+
}
5052+
50425053
public final class io/sentry/logger/LoggerApi : io/sentry/logger/ILoggerApi {
50435054
public fun <init> (Lio/sentry/Scopes;)V
50445055
public fun debug (Ljava/lang/String;[Ljava/lang/Object;)V
@@ -5052,10 +5063,11 @@ public final class io/sentry/logger/LoggerApi : io/sentry/logger/ILoggerApi {
50525063
public fun warn (Ljava/lang/String;[Ljava/lang/Object;)V
50535064
}
50545065

5055-
public final class io/sentry/logger/LoggerBatchProcessor : io/sentry/logger/ILoggerBatchProcessor {
5066+
public class io/sentry/logger/LoggerBatchProcessor : io/sentry/logger/ILoggerBatchProcessor {
50565067
public static final field FLUSH_AFTER_MS I
50575068
public static final field MAX_BATCH_SIZE I
50585069
public static final field MAX_QUEUE_SIZE I
5070+
protected final field options Lio/sentry/SentryOptions;
50595071
public fun <init> (Lio/sentry/SentryOptions;Lio/sentry/ISentryClient;)V
50605072
public fun add (Lio/sentry/SentryLogEvent;)V
50615073
public fun close (Z)V

sentry/src/main/java/io/sentry/SentryClient.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import io.sentry.hints.DiskFlushNotification;
1010
import io.sentry.hints.TransactionEnd;
1111
import io.sentry.logger.ILoggerBatchProcessor;
12-
import io.sentry.logger.LoggerBatchProcessor;
1312
import io.sentry.logger.NoOpLoggerBatchProcessor;
1413
import io.sentry.protocol.Contexts;
1514
import io.sentry.protocol.DebugMeta;
@@ -62,7 +61,8 @@ public SentryClient(final @NotNull SentryOptions options) {
6261
final RequestDetailsResolver requestDetailsResolver = new RequestDetailsResolver(options);
6362
transport = transportFactory.create(options, requestDetailsResolver.resolve());
6463
if (options.getLogs().isEnabled()) {
65-
loggerBatchProcessor = new LoggerBatchProcessor(options, this);
64+
loggerBatchProcessor =
65+
options.getLogs().getLoggerBatchProcessorFactory().create(options, this);
6666
} else {
6767
loggerBatchProcessor = NoOpLoggerBatchProcessor.getInstance();
6868
}

sentry/src/main/java/io/sentry/SentryOptions.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
import io.sentry.internal.modules.IModulesLoader;
1616
import io.sentry.internal.modules.NoOpModulesLoader;
1717
import io.sentry.internal.viewhierarchy.ViewHierarchyExporter;
18+
import io.sentry.logger.DefaultLoggerBatchProcessorFactory;
19+
import io.sentry.logger.ILoggerBatchProcessorFactory;
1820
import io.sentry.protocol.SdkVersion;
1921
import io.sentry.protocol.SentryTransaction;
2022
import io.sentry.transport.ITransport;
@@ -3672,6 +3674,9 @@ public static final class Logs {
36723674
*/
36733675
private @Nullable BeforeSendLogCallback beforeSend;
36743676

3677+
private @NotNull ILoggerBatchProcessorFactory loggerBatchProcessorFactory =
3678+
new DefaultLoggerBatchProcessorFactory();
3679+
36753680
/**
36763681
* Whether Sentry Logs feature is enabled and Sentry.logger() usages are sent to Sentry.
36773682
*
@@ -3708,6 +3713,17 @@ public void setBeforeSend(@Nullable BeforeSendLogCallback beforeSendLog) {
37083713
this.beforeSend = beforeSendLog;
37093714
}
37103715

3716+
@ApiStatus.Internal
3717+
public @NotNull ILoggerBatchProcessorFactory getLoggerBatchProcessorFactory() {
3718+
return loggerBatchProcessorFactory;
3719+
}
3720+
3721+
@ApiStatus.Internal
3722+
public void setLoggerBatchProcessorFactory(
3723+
final @NotNull ILoggerBatchProcessorFactory loggerBatchProcessorFactory) {
3724+
this.loggerBatchProcessorFactory = loggerBatchProcessorFactory;
3725+
}
3726+
37113727
/** The BeforeSendLog callback */
37123728
public interface BeforeSendLogCallback {
37133729

0 commit comments

Comments
 (0)