diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a78300088..097c379ea0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Features - Add time-to-initial-display span to Activity transactions ([#2369](https://github.com/getsentry/sentry-java/pull/2369)) +- Start a session after init if AutoSessionTracking is enabled ([#2356](https://github.com/getsentry/sentry-java/pull/2356)) ### Dependencies 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 c2b100b7ff..995d7f9f19 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,11 +5,12 @@ import io.sentry.Breadcrumb; import io.sentry.IHub; import io.sentry.SentryLevel; +import io.sentry.Session; +import io.sentry.android.core.internal.util.BreadcrumbFactory; import io.sentry.transport.CurrentDateProvider; import io.sentry.transport.ICurrentDateProvider; import java.util.Timer; import java.util.TimerTask; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -27,7 +28,6 @@ final class LifecycleWatcher implements DefaultLifecycleObserver { private final @NotNull IHub hub; private final boolean enableSessionTracking; private final boolean enableAppLifecycleBreadcrumbs; - private final @NotNull AtomicBoolean runningSession = new AtomicBoolean(); private final @NotNull ICurrentDateProvider currentDateProvider; @@ -74,15 +74,24 @@ private void startSession() { cancelTask(); final long currentTimeMillis = currentDateProvider.getCurrentTimeMillis(); - final long lastUpdatedSession = this.lastUpdatedSession.get(); - if (lastUpdatedSession == 0L - || (lastUpdatedSession + sessionIntervalMillis) <= currentTimeMillis) { - addSessionBreadcrumb("start"); - hub.startSession(); - runningSession.set(true); - } - this.lastUpdatedSession.set(currentTimeMillis); + hub.withScope( + scope -> { + long lastUpdatedSession = this.lastUpdatedSession.get(); + if (lastUpdatedSession == 0L) { + @Nullable Session currentSession = scope.getSession(); + if (currentSession != null && currentSession.getStarted() != null) { + lastUpdatedSession = currentSession.getStarted().getTime(); + } + } + + if (lastUpdatedSession == 0L + || (lastUpdatedSession + sessionIntervalMillis) <= currentTimeMillis) { + addSessionBreadcrumb("start"); + hub.startSession(); + } + this.lastUpdatedSession.set(currentTimeMillis); + }); } } @@ -110,7 +119,6 @@ private void scheduleEndSession() { public void run() { addSessionBreadcrumb("end"); hub.endSession(); - runningSession.set(false); } }; @@ -140,20 +148,10 @@ private void addAppBreadcrumb(final @NotNull String state) { } private void addSessionBreadcrumb(final @NotNull String state) { - final Breadcrumb breadcrumb = new Breadcrumb(); - breadcrumb.setType("session"); - breadcrumb.setData("state", state); - breadcrumb.setCategory("app.lifecycle"); - breadcrumb.setLevel(SentryLevel.INFO); + final Breadcrumb breadcrumb = BreadcrumbFactory.forSession(state); hub.addBreadcrumb(breadcrumb); } - @TestOnly - @NotNull - AtomicBoolean isRunningSession() { - return runningSession; - } - @TestOnly @Nullable TimerTask getTimerTask() { diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java index e9e8a75c1f..9d046cbdb7 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java @@ -3,12 +3,14 @@ import android.content.Context; import android.os.SystemClock; import io.sentry.DateUtils; +import io.sentry.IHub; import io.sentry.ILogger; import io.sentry.Integration; import io.sentry.OptionsContainer; import io.sentry.Sentry; import io.sentry.SentryLevel; import io.sentry.SentryOptions; +import io.sentry.android.core.internal.util.BreadcrumbFactory; import io.sentry.android.fragment.FragmentLifecycleIntegration; import io.sentry.android.timber.SentryTimberIntegration; import java.lang.reflect.InvocationTargetException; @@ -119,6 +121,12 @@ public static synchronized void init( deduplicateIntegrations(options, isFragmentAvailable, isTimberAvailable); }, true); + + final @NotNull IHub hub = Sentry.getCurrentHub(); + if (hub.getOptions().isEnableAutoSessionTracking()) { + hub.addBreadcrumb(BreadcrumbFactory.forSession("session.start")); + hub.startSession(); + } } catch (IllegalAccessException e) { logger.log(SentryLevel.FATAL, "Fatal error during SentryAndroid.init(...)", e); diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/gestures/SentryGestureListener.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/gestures/SentryGestureListener.java index a076bd6bf4..72a2d3983c 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/gestures/SentryGestureListener.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/gestures/SentryGestureListener.java @@ -181,6 +181,11 @@ private void addBreadcrumb( final @NotNull String eventType, final @NotNull Map additionalData, final @NotNull MotionEvent motionEvent) { + + if ((!options.isEnableUserInteractionBreadcrumbs())) { + return; + } + @NotNull String className; @Nullable String canonicalName = target.getClass().getCanonicalName(); if (canonicalName != null) { diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/BreadcrumbFactory.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/BreadcrumbFactory.java new file mode 100644 index 0000000000..04cabc9430 --- /dev/null +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/BreadcrumbFactory.java @@ -0,0 +1,19 @@ +package io.sentry.android.core.internal.util; + +import io.sentry.Breadcrumb; +import io.sentry.SentryLevel; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +@ApiStatus.Internal +public class BreadcrumbFactory { + + public static @NotNull Breadcrumb forSession(@NotNull String state) { + final Breadcrumb breadcrumb = new Breadcrumb(); + breadcrumb.setType("session"); + breadcrumb.setData("state", state); + breadcrumb.setCategory("app.lifecycle"); + breadcrumb.setLevel(SentryLevel.INFO); + return breadcrumb; + } +} 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 4802b44da4..740d340a7e 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 @@ -2,17 +2,24 @@ package io.sentry.android.core import androidx.lifecycle.LifecycleOwner import io.sentry.Breadcrumb +import io.sentry.DateUtils import io.sentry.IHub +import io.sentry.Scope +import io.sentry.ScopeCallback import io.sentry.SentryLevel +import io.sentry.Session +import io.sentry.Session.State import io.sentry.transport.ICurrentDateProvider -import org.awaitility.kotlin.await +import org.mockito.ArgumentCaptor import org.mockito.kotlin.any import org.mockito.kotlin.check import org.mockito.kotlin.mock import org.mockito.kotlin.never +import org.mockito.kotlin.timeout import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.whenever +import java.util.* import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotNull @@ -25,8 +32,26 @@ class LifecycleWatcherTest { val hub = mock() val dateProvider = mock() - fun getSUT(sessionIntervalMillis: Long = 0L, enableAutoSessionTracking: Boolean = true, enableAppLifecycleBreadcrumbs: Boolean = true): LifecycleWatcher { - return LifecycleWatcher(hub, sessionIntervalMillis, enableAutoSessionTracking, enableAppLifecycleBreadcrumbs, dateProvider) + fun getSUT( + sessionIntervalMillis: Long = 0L, + enableAutoSessionTracking: Boolean = true, + enableAppLifecycleBreadcrumbs: Boolean = true, + session: Session? = null + ): LifecycleWatcher { + val argumentCaptor: ArgumentCaptor = ArgumentCaptor.forClass(ScopeCallback::class.java) + val scope = mock() + whenever(scope.session).thenReturn(session) + whenever(hub.withScope(argumentCaptor.capture())).thenAnswer { + argumentCaptor.value.run(scope) + } + + return LifecycleWatcher( + hub, + sessionIntervalMillis, + enableAutoSessionTracking, + enableAppLifecycleBreadcrumbs, + dateProvider + ) } } @@ -62,8 +87,7 @@ class LifecycleWatcherTest { val watcher = fixture.getSUT(enableAppLifecycleBreadcrumbs = false) watcher.onStart(fixture.ownerMock) watcher.onStop(fixture.ownerMock) - await.untilFalse(watcher.isRunningSession) - verify(fixture.hub).endSession() + verify(fixture.hub, timeout(10000)).endSession() } @Test @@ -112,9 +136,8 @@ class LifecycleWatcherTest { @Test fun `When session tracking is enabled, add breadcrumb on stop`() { val watcher = fixture.getSUT(enableAppLifecycleBreadcrumbs = false) - watcher.isRunningSession.set(true) watcher.onStop(fixture.ownerMock) - await.untilFalse(watcher.isRunningSession) + verify(fixture.hub, timeout(10000)).endSession() verify(fixture.hub).addBreadcrumb( check { assertEquals("app.lifecycle", it.category) @@ -193,4 +216,54 @@ class LifecycleWatcherTest { val watcher = fixture.getSUT(enableAutoSessionTracking = false, enableAppLifecycleBreadcrumbs = false) assertNull(watcher.timer) } + + @Test + fun `if the hub has already a fresh session running, don't start new one`() { + val watcher = fixture.getSUT( + enableAppLifecycleBreadcrumbs = false, + session = Session( + State.Ok, + DateUtils.getCurrentDateTime(), + DateUtils.getCurrentDateTime(), + 0, + "abc", + UUID.fromString("3c1ffc32-f68f-4af2-a1ee-dd72f4d62d17"), + true, + 0, + 10.0, + null, + null, + null, + "release" + ) + ) + + watcher.onStart(fixture.ownerMock) + verify(fixture.hub, never()).startSession() + } + + @Test + fun `if the hub has a long running session, start new one`() { + val watcher = fixture.getSUT( + enableAppLifecycleBreadcrumbs = false, + session = Session( + State.Ok, + DateUtils.getDateTime(-1), + DateUtils.getDateTime(-1), + 0, + "abc", + UUID.fromString("3c1ffc32-f68f-4af2-a1ee-dd72f4d62d17"), + true, + 0, + 10.0, + null, + null, + null, + "release" + ) + ) + + watcher.onStart(fixture.ownerMock) + verify(fixture.hub).startSession() + } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt index ebbfd13b1f..a00b57072c 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt @@ -30,6 +30,7 @@ import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertNotNull +import kotlin.test.assertNull import kotlin.test.assertTrue @RunWith(AndroidJUnit4::class) @@ -186,6 +187,26 @@ class SentryAndroidTest { assertEquals(expectedCacheDir, (options!!.envelopeDiskCache as AndroidEnvelopeCache).directory.absolutePath) } + @Test + fun `init starts a session if auto session tracking is enabled`() { + fixture.initSut { options -> + options.isEnableAutoSessionTracking = true + } + Sentry.getCurrentHub().withScope { scope -> + assertNotNull(scope.session) + } + } + + @Test + fun `init does not start a session by if auto session tracking is disabled`() { + fixture.initSut { options -> + options.isEnableAutoSessionTracking = false + } + Sentry.getCurrentHub().withScope { scope -> + assertNull(scope.session) + } + } + private class CustomEnvelopCache : IEnvelopeCache { override fun iterator(): MutableIterator = TODO() override fun store(envelope: SentryEnvelope, hint: Hint) = Unit diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryGestureListenerScrollTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryGestureListenerScrollTest.kt index 7e935893ae..414538c657 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryGestureListenerScrollTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryGestureListenerScrollTest.kt @@ -34,6 +34,7 @@ class SentryGestureListenerScrollTest { val resources = mock() val options = SentryAndroidOptions().apply { dsn = "https://key@sentry.io/proj" + isEnableUserInteractionBreadcrumbs = true } val hub = mock() diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 85f6c24022..be126c57cb 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -989,6 +989,7 @@ public final class io/sentry/Scope { public fun getContexts ()Lio/sentry/protocol/Contexts; public fun getLevel ()Lio/sentry/SentryLevel; public fun getRequest ()Lio/sentry/protocol/Request; + public fun getSession ()Lio/sentry/Session; public fun getSpan ()Lio/sentry/ISpan; public fun getTags ()Ljava/util/Map; public fun getTransaction ()Lio/sentry/ITransaction; diff --git a/sentry/src/main/java/io/sentry/Scope.java b/sentry/src/main/java/io/sentry/Scope.java index 0d5904fc6a..ac27668611 100644 --- a/sentry/src/main/java/io/sentry/Scope.java +++ b/sentry/src/main/java/io/sentry/Scope.java @@ -762,6 +762,11 @@ public void withTransaction(final @NotNull IWithTransaction callback) { } } + @ApiStatus.Internal + public @Nullable Session getSession() { + return session; + } + /** the IWithTransaction callback */ @ApiStatus.Internal public interface IWithTransaction {