Skip to content

Commit

Permalink
Start a session after init if AutoSessionTracking is enabled (#2356)
Browse files Browse the repository at this point in the history
Co-authored-by: Roman Zavarnitsyn <rom4ek93@gmail.com>
  • Loading branch information
markushi and romtsn authored Dec 13, 2022
1 parent 703d523 commit d00c464
Show file tree
Hide file tree
Showing 10 changed files with 164 additions and 29 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### Features

- Start a session after init if AutoSessionTracking is enabled ([#2356](https://github.com/getsentry/sentry-java/pull/2356))

### Dependencies

- Bump Native SDK from v0.5.2 to v0.5.3 ([#2423](https://github.com/getsentry/sentry-java/pull/2423))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -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);
});
}
}

Expand Down Expand Up @@ -110,7 +119,6 @@ private void scheduleEndSession() {
public void run() {
addSessionBreadcrumb("end");
hub.endSession();
runningSession.set(false);
}
};

Expand Down Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,11 @@ private void addBreadcrumb(
final @NotNull String eventType,
final @NotNull Map<String, Object> additionalData,
final @NotNull MotionEvent motionEvent) {

if ((!options.isEnableUserInteractionBreadcrumbs())) {
return;
}

@NotNull String className;
@Nullable String canonicalName = target.getClass().getCanonicalName();
if (canonicalName != null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -25,8 +32,26 @@ class LifecycleWatcherTest {
val hub = mock<IHub>()
val dateProvider = mock<ICurrentDateProvider>()

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<ScopeCallback> = ArgumentCaptor.forClass(ScopeCallback::class.java)
val scope = mock<Scope>()
whenever(scope.session).thenReturn(session)
whenever(hub.withScope(argumentCaptor.capture())).thenAnswer {
argumentCaptor.value.run(scope)
}

return LifecycleWatcher(
hub,
sessionIntervalMillis,
enableAutoSessionTracking,
enableAppLifecycleBreadcrumbs,
dateProvider
)
}
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<Breadcrumb> {
assertEquals("app.lifecycle", it.category)
Expand Down Expand Up @@ -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()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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<SentryEnvelope> = TODO()
override fun store(envelope: SentryEnvelope, hint: Hint) = Unit
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class SentryGestureListenerScrollTest {
val resources = mock<Resources>()
val options = SentryAndroidOptions().apply {
dsn = "https://key@sentry.io/proj"
isEnableUserInteractionBreadcrumbs = true
}
val hub = mock<IHub>()

Expand Down
1 change: 1 addition & 0 deletions sentry/api/sentry.api
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
5 changes: 5 additions & 0 deletions sentry/src/main/java/io/sentry/Scope.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down

0 comments on commit d00c464

Please sign in to comment.