Skip to content

Commit ed93ed6

Browse files
authored
Fix session tracking (#2609)
1 parent d0c4495 commit ed93ed6

File tree

5 files changed

+183
-15
lines changed

5 files changed

+183
-15
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@
2525
- Add version to sentryClientName used in auth header ([#2596](https://github.com/getsentry/sentry-java/pull/2596))
2626
- Keep integration names from being obfuscated ([#2599](https://github.com/getsentry/sentry-java/pull/2599))
2727
- Change log level from INFO to WARN for error message indicating a failed Log4j2 Sentry.init ([#2606](https://github.com/getsentry/sentry-java/pull/2606))
28-
- The log message was often not visible as our docs suggest a minimum log level of WARN
28+
- The log message was often not visible as our docs suggest a minimum log level of WARN
29+
- Fix session tracking on Android ([#2609](https://github.com/getsentry/sentry-java/pull/2609))
30+
- Incorrect number of session has been sent. In addition, some of the sessions were not properly ended, messing up Session Health Metrics.
2931

3032
### Dependencies
3133

sentry-android-core/src/main/java/io/sentry/android/core/LifecycleWatcher.java

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -79,23 +79,23 @@ private void startSession() {
7979

8080
final long currentTimeMillis = currentDateProvider.getCurrentTimeMillis();
8181

82-
hub.withScope(
82+
hub.configureScope(
8383
scope -> {
84-
long lastUpdatedSession = this.lastUpdatedSession.get();
85-
if (lastUpdatedSession == 0L) {
86-
@Nullable Session currentSession = scope.getSession();
84+
if (lastUpdatedSession.get() == 0L) {
85+
final @Nullable Session currentSession = scope.getSession();
8786
if (currentSession != null && currentSession.getStarted() != null) {
88-
lastUpdatedSession = currentSession.getStarted().getTime();
87+
lastUpdatedSession.set(currentSession.getStarted().getTime());
8988
}
9089
}
91-
92-
if (lastUpdatedSession == 0L
93-
|| (lastUpdatedSession + sessionIntervalMillis) <= currentTimeMillis) {
94-
addSessionBreadcrumb("start");
95-
hub.startSession();
96-
}
97-
this.lastUpdatedSession.set(currentTimeMillis);
9890
});
91+
92+
final long lastUpdatedSession = this.lastUpdatedSession.get();
93+
if (lastUpdatedSession == 0L
94+
|| (lastUpdatedSession + sessionIntervalMillis) <= currentTimeMillis) {
95+
addSessionBreadcrumb("start");
96+
hub.startSession();
97+
}
98+
this.lastUpdatedSession.set(currentTimeMillis);
9999
}
100100
}
101101

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ class LifecycleWatcherTest {
4444
val argumentCaptor: ArgumentCaptor<ScopeCallback> = ArgumentCaptor.forClass(ScopeCallback::class.java)
4545
val scope = mock<Scope>()
4646
whenever(scope.session).thenReturn(session)
47-
whenever(hub.withScope(argumentCaptor.capture())).thenAnswer {
47+
whenever(hub.configureScope(argumentCaptor.capture())).thenAnswer {
4848
argumentCaptor.value.run(scope)
4949
}
5050

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
package io.sentry.android.core
2+
3+
import android.content.Context
4+
import androidx.lifecycle.Lifecycle.Event.ON_START
5+
import androidx.lifecycle.Lifecycle.Event.ON_STOP
6+
import androidx.lifecycle.LifecycleRegistry
7+
import androidx.test.core.app.ApplicationProvider
8+
import androidx.test.ext.junit.runners.AndroidJUnit4
9+
import io.sentry.Hint
10+
import io.sentry.ISentryClient
11+
import io.sentry.ProfilingTraceData
12+
import io.sentry.Scope
13+
import io.sentry.Sentry
14+
import io.sentry.SentryEnvelope
15+
import io.sentry.SentryEvent
16+
import io.sentry.SentryOptions
17+
import io.sentry.Session
18+
import io.sentry.Session.State.Exited
19+
import io.sentry.Session.State.Ok
20+
import io.sentry.TraceContext
21+
import io.sentry.UserFeedback
22+
import io.sentry.protocol.SentryId
23+
import io.sentry.protocol.SentryTransaction
24+
import org.junit.runner.RunWith
25+
import org.mockito.kotlin.mock
26+
import org.robolectric.annotation.Config
27+
import java.util.LinkedList
28+
import kotlin.test.BeforeTest
29+
import kotlin.test.Test
30+
import kotlin.test.assertTrue
31+
32+
@RunWith(AndroidJUnit4::class)
33+
@Config(sdk = [31])
34+
class SessionTrackingIntegrationTest {
35+
36+
private lateinit var context: Context
37+
38+
@BeforeTest
39+
fun `set up`() {
40+
context = ApplicationProvider.getApplicationContext()
41+
}
42+
43+
@Test
44+
fun `session tracking works properly with multiple backgrounds and foregrounds`() {
45+
lateinit var options: SentryAndroidOptions
46+
SentryAndroid.init(context) {
47+
it.dsn = "https://[email protected]/proj"
48+
it.release = "[email protected]"
49+
it.environment = "production"
50+
it.sessionTrackingIntervalMillis = 0L
51+
options = it
52+
}
53+
val client = CapturingSentryClient()
54+
Sentry.bindClient(client)
55+
val lifecycle = setupLifecycle(options)
56+
val initSid = lastSessionId()
57+
58+
lifecycle.handleLifecycleEvent(ON_START)
59+
val sidAfterFirstStart = lastSessionId()
60+
Thread.sleep(100L)
61+
lifecycle.handleLifecycleEvent(ON_STOP)
62+
Thread.sleep(100L)
63+
val sidAfterFirstStop = lastSessionId()
64+
65+
Thread.sleep(100L)
66+
67+
lifecycle.handleLifecycleEvent(ON_START)
68+
val sidAfterSecondStart = lastSessionId()
69+
Thread.sleep(100L)
70+
lifecycle.handleLifecycleEvent(ON_STOP)
71+
Thread.sleep(100L)
72+
val sidAfterSecondStop = lastSessionId()
73+
// we bind our CapturingSentryClient only after .init is called, so we'll be able to capture
74+
// only the Exited status of the session started in .init
75+
val initSessionUpdate = client.sessionUpdates.pop()
76+
assertTrue {
77+
initSid == initSessionUpdate.sessionId.toString() &&
78+
Session.State.Exited == initSessionUpdate.status
79+
}
80+
81+
val afterFirstStartSessionUpdate = client.sessionUpdates.pop()
82+
assertTrue {
83+
sidAfterFirstStart == afterFirstStartSessionUpdate.sessionId.toString() &&
84+
Session.State.Ok == afterFirstStartSessionUpdate.status
85+
}
86+
87+
val afterFirstStopSessionUpdate = client.sessionUpdates.pop()
88+
assertTrue {
89+
"null" == sidAfterFirstStop &&
90+
sidAfterFirstStart == afterFirstStopSessionUpdate.sessionId.toString() &&
91+
Session.State.Exited == afterFirstStopSessionUpdate.status
92+
}
93+
94+
val afterSecondStartSessionUpdate = client.sessionUpdates.pop()
95+
assertTrue {
96+
sidAfterSecondStart == afterSecondStartSessionUpdate.sessionId.toString() &&
97+
Session.State.Ok == afterSecondStartSessionUpdate.status
98+
}
99+
100+
val afterSecondStopSessionUpdate = client.sessionUpdates.pop()
101+
assertTrue {
102+
"null" == sidAfterSecondStop &&
103+
sidAfterSecondStart == afterSecondStopSessionUpdate.sessionId.toString() &&
104+
Session.State.Exited == afterSecondStopSessionUpdate.status
105+
}
106+
}
107+
108+
private fun lastSessionId(): String? {
109+
var sid: String? = null
110+
Sentry.configureScope { scope ->
111+
sid = scope.session?.sessionId.toString()
112+
}
113+
return sid
114+
}
115+
116+
private fun setupLifecycle(options: SentryOptions): LifecycleRegistry {
117+
val lifecycle = LifecycleRegistry(mock())
118+
val lifecycleWatcher = (
119+
options.integrations.find {
120+
it is AppLifecycleIntegration
121+
} as AppLifecycleIntegration
122+
).watcher
123+
lifecycle.addObserver(lifecycleWatcher!!)
124+
return lifecycle
125+
}
126+
127+
private class CapturingSentryClient : ISentryClient {
128+
val sessionUpdates = LinkedList<Session>()
129+
130+
override fun isEnabled(): Boolean = true
131+
132+
override fun captureEvent(event: SentryEvent, scope: Scope?, hint: Hint?): SentryId {
133+
TODO("Not yet implemented")
134+
}
135+
136+
override fun close() {
137+
TODO("Not yet implemented")
138+
}
139+
140+
override fun flush(timeoutMillis: Long) {
141+
TODO("Not yet implemented")
142+
}
143+
144+
override fun captureUserFeedback(userFeedback: UserFeedback) {
145+
TODO("Not yet implemented")
146+
}
147+
148+
override fun captureSession(session: Session, hint: Hint?) {
149+
sessionUpdates.add(session)
150+
}
151+
152+
override fun captureEnvelope(envelope: SentryEnvelope, hint: Hint?): SentryId? {
153+
TODO("Not yet implemented")
154+
}
155+
156+
override fun captureTransaction(
157+
transaction: SentryTransaction,
158+
traceContext: TraceContext?,
159+
scope: Scope?,
160+
hint: Hint?,
161+
profilingTraceData: ProfilingTraceData?
162+
): SentryId {
163+
TODO("Not yet implemented")
164+
}
165+
}
166+
}

sentry-samples/sentry-samples-android/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ android {
1212
minSdk = Config.Android.minSdkVersionCompose
1313
targetSdk = Config.Android.targetSdkVersion
1414
versionCode = 2
15-
versionName = "1.1.0"
15+
versionName = project.version.toString()
1616

1717
externalNativeBuild {
1818
val sentryNativeSrc = if (File("${project.projectDir}/../../sentry-android-ndk/sentry-native-local").exists()) {

0 commit comments

Comments
 (0)