Skip to content

Commit 2ab9d50

Browse files
authored
feat(android): Add log flushing on app backgrounding (#4951)
1 parent ee35ac3 commit 2ab9d50

15 files changed

+275
-5
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
### Features
66

7+
- Android: Flush logs when app enters background ([#4951](https://github.com/getsentry/sentry-java/pull/4951))
78
- Add option to capture additional OkHttp network request/response details in session replays ([#4919](https://github.com/getsentry/sentry-java/pull/4919))
89
- Depends on `SentryOkHttpInterceptor` to intercept the request and extract request/response bodies
910
- To enable, add url regexes via the `io.sentry.session-replay.network-detail-allow-urls` metadata tag in AndroidManifest ([code sample](https://github.com/getsentry/sentry-java/blob/b03edbb1b0d8b871c62a09bc02cbd8a4e1f6fea1/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml#L196-L205))

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: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
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.SentryOptions
8+
import io.sentry.protocol.SentryId
9+
import io.sentry.test.ImmediateExecutorService
10+
import kotlin.test.AfterTest
11+
import kotlin.test.BeforeTest
12+
import kotlin.test.Test
13+
import kotlin.test.assertNotNull
14+
import kotlin.test.assertTrue
15+
import org.junit.runner.RunWith
16+
import org.mockito.kotlin.any
17+
import org.mockito.kotlin.mock
18+
import org.mockito.kotlin.verify
19+
import org.mockito.kotlin.whenever
20+
21+
@RunWith(AndroidJUnit4::class)
22+
class AndroidLoggerBatchProcessorTest {
23+
24+
private class Fixture {
25+
val options = SentryAndroidOptions()
26+
val client: ISentryClient = mock()
27+
28+
fun getSut(
29+
useImmediateExecutor: Boolean = false,
30+
config: ((SentryOptions) -> Unit)? = null,
31+
): AndroidLoggerBatchProcessor {
32+
if (useImmediateExecutor) {
33+
options.executorService = ImmediateExecutorService()
34+
}
35+
config?.invoke(options)
36+
return AndroidLoggerBatchProcessor(options, client)
37+
}
38+
}
39+
40+
private val fixture = Fixture()
41+
42+
@BeforeTest
43+
fun `set up`() {
44+
AppState.getInstance().resetInstance()
45+
}
46+
47+
@AfterTest
48+
fun `tear down`() {
49+
AppState.getInstance().resetInstance()
50+
}
51+
52+
@Test
53+
fun `constructor registers as AppState listener`() {
54+
fixture.getSut()
55+
assertNotNull(AppState.getInstance().lifecycleObserver)
56+
}
57+
58+
@Test
59+
fun `onBackground schedules flush`() {
60+
val sut = fixture.getSut(useImmediateExecutor = true)
61+
val logEvent = SentryLogEvent(SentryId(), 1.0, "test", SentryLogLevel.INFO)
62+
sut.add(logEvent)
63+
64+
sut.onBackground()
65+
66+
verify(fixture.client).captureBatchedLogEvents(any())
67+
}
68+
69+
@Test
70+
fun `onBackground handles executor exception gracefully`() {
71+
val sut =
72+
fixture.getSut { options ->
73+
val rejectingExecutor = mock<io.sentry.ISentryExecutorService>()
74+
whenever(rejectingExecutor.submit(any())).thenThrow(RuntimeException("Rejected"))
75+
options.executorService = rejectingExecutor
76+
}
77+
78+
// Should not throw
79+
sut.onBackground()
80+
}
81+
82+
@Test
83+
fun `close removes AppState listener`() {
84+
val sut = fixture.getSut()
85+
sut.close(false)
86+
87+
assertTrue(AppState.getInstance().lifecycleObserver.listeners.isEmpty())
88+
}
89+
90+
@Test
91+
fun `close with isRestarting true still removes listener`() {
92+
val sut = fixture.getSut()
93+
sut.close(true)
94+
95+
assertTrue(AppState.getInstance().lifecycleObserver.listeners.isEmpty())
96+
}
97+
}

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
}

0 commit comments

Comments
 (0)