diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c55dcc5d73..880a09c53ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ - Fallback to distinct-id as user.id logging attribute when user is not set ([#4847](https://github.com/getsentry/sentry-java/pull/4847)) - Report Timber.tag() as `timber.tag` log attribute ([#4845](https://github.com/getsentry/sentry-java/pull/4845)) - Session Replay: Add screenshot strategy serialization to RRWeb events ([#4851](https://github.com/getsentry/sentry-java/pull/4851)) +- Android: Flush log when app enters background ([#4873](https://github.com/getsentry/sentry-java/pull/4873)) ### Dependencies diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AppLifecycleIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/AppLifecycleIntegration.java index 9fd90b23099..b99c03fc072 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AppLifecycleIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AppLifecycleIntegration.java @@ -45,7 +45,8 @@ public void register(final @NotNull IScopes scopes, final @NotNull SentryOptions this.options.isEnableAppLifecycleBreadcrumbs()); if (this.options.isEnableAutoSessionTracking() - || this.options.isEnableAppLifecycleBreadcrumbs()) { + || this.options.isEnableAppLifecycleBreadcrumbs() + || this.options.getLogs().isEnabled()) { try (final ISentryLifecycleToken ignored = lock.acquire()) { if (watcher != null) { return; @@ -56,7 +57,8 @@ public void register(final @NotNull IScopes scopes, final @NotNull SentryOptions scopes, this.options.getSessionTrackingIntervalMillis(), this.options.isEnableAutoSessionTracking(), - this.options.isEnableAppLifecycleBreadcrumbs()); + this.options.isEnableAppLifecycleBreadcrumbs(), + this.options.getLogs().isEnabled()); AppState.getInstance().addAppStateListener(watcher); } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/LifecycleWatcher.java b/sentry-android-core/src/main/java/io/sentry/android/core/LifecycleWatcher.java index 3d4cedb1b53..a59dc7783d3 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/LifecycleWatcher.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/LifecycleWatcher.java @@ -5,6 +5,7 @@ import io.sentry.ISentryLifecycleToken; import io.sentry.SentryLevel; import io.sentry.Session; +import io.sentry.logger.LoggerBatchProcessor; import io.sentry.transport.CurrentDateProvider; import io.sentry.transport.ICurrentDateProvider; import io.sentry.util.AutoClosableReentrantLock; @@ -29,18 +30,22 @@ final class LifecycleWatcher implements AppState.AppStateListener { private final boolean enableSessionTracking; private final boolean enableAppLifecycleBreadcrumbs; + private final boolean enableLogFlushing; + private final @NotNull ICurrentDateProvider currentDateProvider; LifecycleWatcher( final @NotNull IScopes scopes, final long sessionIntervalMillis, final boolean enableSessionTracking, - final boolean enableAppLifecycleBreadcrumbs) { + final boolean enableAppLifecycleBreadcrumbs, + final boolean enableLogFlushing) { this( scopes, sessionIntervalMillis, enableSessionTracking, enableAppLifecycleBreadcrumbs, + enableLogFlushing, CurrentDateProvider.getInstance()); } @@ -49,10 +54,12 @@ final class LifecycleWatcher implements AppState.AppStateListener { final long sessionIntervalMillis, final boolean enableSessionTracking, final boolean enableAppLifecycleBreadcrumbs, + final boolean enableLogFlushing, final @NotNull ICurrentDateProvider currentDateProvider) { this.sessionIntervalMillis = sessionIntervalMillis; this.enableSessionTracking = enableSessionTracking; this.enableAppLifecycleBreadcrumbs = enableAppLifecycleBreadcrumbs; + this.enableLogFlushing = enableLogFlushing; this.scopes = scopes; this.currentDateProvider = currentDateProvider; } @@ -101,6 +108,29 @@ public void onBackground() { scheduleEndSession(); addAppBreadcrumb("background"); + + if (enableLogFlushing) { + try { + scopes + .getOptions() + .getExecutorService() + .submit( + new Runnable() { + @Override + public void run() { + scopes + .getGlobalScope() + .getClient() + .flushLogs(LoggerBatchProcessor.FLUSH_AFTER_MS); + } + }); + } catch (Throwable t) { + scopes + .getOptions() + .getLogger() + .log(SentryLevel.ERROR, t, "Failed to submit log flush runnable"); + } + } } private void scheduleEndSession() { diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AppLifecycleIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AppLifecycleIntegrationTest.kt index 896673085c2..323d60bbf90 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AppLifecycleIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AppLifecycleIntegrationTest.kt @@ -34,11 +34,12 @@ class AppLifecycleIntegrationTest { } @Test - fun `When SessionTracking and AppLifecycle breadcrumbs are disabled, lifecycle watcher should not be started`() { + fun `When SessionTracking and AppLifecycle breadcrumbs and Logs are disabled, lifecycle watcher should not be started`() { val sut = fixture.getSut() fixture.options.apply { isEnableAppLifecycleBreadcrumbs = false isEnableAutoSessionTracking = false + logs.isEnabled = false } sut.register(fixture.scopes, fixture.options) diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/LifecycleWatcherTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/LifecycleWatcherTest.kt index 5149f167129..72d7bf853e7 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/LifecycleWatcherTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/LifecycleWatcherTest.kt @@ -5,12 +5,14 @@ import io.sentry.DateUtils import io.sentry.IContinuousProfiler import io.sentry.IScope import io.sentry.IScopes +import io.sentry.ISentryClient import io.sentry.ReplayController import io.sentry.ScopeCallback import io.sentry.SentryLevel import io.sentry.SentryOptions import io.sentry.Session import io.sentry.Session.State +import io.sentry.test.ImmediateExecutorService import io.sentry.transport.ICurrentDateProvider import kotlin.test.BeforeTest import kotlin.test.Test @@ -36,10 +38,13 @@ class LifecycleWatcherTest { val replayController = mock() val continuousProfiler = mock() + val client = mock() + fun getSUT( sessionIntervalMillis: Long = 0L, enableAutoSessionTracking: Boolean = true, enableAppLifecycleBreadcrumbs: Boolean = true, + enableLogFlushing: Boolean = true, session: Session? = null, ): LifecycleWatcher { val argumentCaptor: ArgumentCaptor = @@ -49,15 +54,20 @@ class LifecycleWatcherTest { whenever(scopes.configureScope(argumentCaptor.capture())).thenAnswer { argumentCaptor.value.run(scope) } + whenever(scope.client).thenReturn(client) + options.setReplayController(replayController) options.setContinuousProfiler(continuousProfiler) + options.executorService = ImmediateExecutorService() whenever(scopes.options).thenReturn(options) + whenever(scopes.globalScope).thenReturn(scope) return LifecycleWatcher( scopes, sessionIntervalMillis, enableAutoSessionTracking, enableAppLifecycleBreadcrumbs, + enableLogFlushing, dateProvider, ) } @@ -295,4 +305,27 @@ class LifecycleWatcherTest { watcher.onBackground() verify(fixture.replayController, timeout(10000)).stop() } + + @Test + fun `flush logs when going in background`() { + val watcher = fixture.getSUT(enableLogFlushing = true) + + watcher.onForeground() + watcher.onBackground() + + watcher.onForeground() + watcher.onBackground() + + verify(fixture.client, times(2)).flushLogs(any()) + } + + @Test + fun `do not flush logs when going in background when logging is disabled`() { + val watcher = fixture.getSUT(enableLogFlushing = false) + + watcher.onForeground() + watcher.onBackground() + + verify(fixture.client, never()).flushLogs(any()) + } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SessionTrackingIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SessionTrackingIntegrationTest.kt index bdb328e2421..e4bc271deb0 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SessionTrackingIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SessionTrackingIntegrationTest.kt @@ -142,6 +142,10 @@ class SessionTrackingIntegrationTest { TODO("Not yet implemented") } + override fun flushLogs(timeoutMillis: Long) { + TODO("Not yet implemented") + } + override fun captureFeedback(feedback: Feedback, hint: Hint?, scope: IScope): SentryId { TODO("Not yet implemented") } diff --git a/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml b/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml index afc6db9029f..92bdb038e36 100644 --- a/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml +++ b/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml @@ -1,193 +1,191 @@ + xmlns:tools="http://schemas.android.com/tools"> - - + + - + - - - - + + + + - + - - - - - - - - - - - - - - - - - - - - - - + android:name=".MyApplication" + android:icon="@mipmap/ic_launcher" + android:label="@string/app_name" + android:roundIcon="@mipmap/ic_launcher_round" + android:theme="@style/AppTheme" + android:networkSecurityConfig="@xml/network" + tools:ignore="GoogleAppIndexingWarning, UnusedAttribute"> + + + android:name=".MainActivity" + android:exported="true"> + + - + + + - + + + + + + + - - + + + + + + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - - + + - - + + + - - - - - - + + - - + + + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - + + - - - - + + + + diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 2211f053912..0d2f4d03cf3 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -1041,6 +1041,7 @@ public abstract interface class io/sentry/ISentryClient { public abstract fun close ()V public abstract fun close (Z)V public abstract fun flush (J)V + public abstract fun flushLogs (J)V public abstract fun getRateLimiter ()Lio/sentry/transport/RateLimiter; public abstract fun isEnabled ()Z public fun isHealthy ()Z @@ -2824,6 +2825,7 @@ public final class io/sentry/SentryClient : io/sentry/ISentryClient { public fun close ()V public fun close (Z)V public fun flush (J)V + public fun flushLogs (J)V public fun getRateLimiter ()Lio/sentry/transport/RateLimiter; public fun isEnabled ()Z public fun isHealthy ()Z diff --git a/sentry/src/main/java/io/sentry/ISentryClient.java b/sentry/src/main/java/io/sentry/ISentryClient.java index c2bc05516f4..09a3fe8724f 100644 --- a/sentry/src/main/java/io/sentry/ISentryClient.java +++ b/sentry/src/main/java/io/sentry/ISentryClient.java @@ -47,6 +47,8 @@ public interface ISentryClient { */ void flush(long timeoutMillis); + void flushLogs(long timeoutMillis); + /** * Captures the event. * diff --git a/sentry/src/main/java/io/sentry/NoOpSentryClient.java b/sentry/src/main/java/io/sentry/NoOpSentryClient.java index 17e4becbc71..13163e004f2 100644 --- a/sentry/src/main/java/io/sentry/NoOpSentryClient.java +++ b/sentry/src/main/java/io/sentry/NoOpSentryClient.java @@ -38,6 +38,9 @@ public void close() {} @Override public void flush(long timeoutMillis) {} + @Override + public void flushLogs(long timeoutMillis) {} + @Override public @NotNull SentryId captureFeedback( @NotNull Feedback feedback, @Nullable Hint hint, @NotNull IScope scope) { diff --git a/sentry/src/main/java/io/sentry/SentryClient.java b/sentry/src/main/java/io/sentry/SentryClient.java index bfcf4e780be..97f27ef0425 100644 --- a/sentry/src/main/java/io/sentry/SentryClient.java +++ b/sentry/src/main/java/io/sentry/SentryClient.java @@ -1542,10 +1542,15 @@ public void close(final boolean isRestarting) { @Override public void flush(final long timeoutMillis) { - loggerBatchProcessor.flush(timeoutMillis); + flushLogs(timeoutMillis); transport.flush(timeoutMillis); } + @Override + public void flushLogs(final long timeoutMillis) { + loggerBatchProcessor.flush(timeoutMillis); + } + @Override public @Nullable RateLimiter getRateLimiter() { return transport.getRateLimiter();