Skip to content

Commit adf93a8

Browse files
Added core session management infrastructure to SDK and implemented session tracking with automatic session ID injection into spans, logs, and events. Includes SessionManager, SessionProvider with previous session ID support, processors for automatic attribute injection, and extension functions for manual session ID management.
1 parent fdebaab commit adf93a8

22 files changed

Lines changed: 1231 additions & 35 deletions

File tree

android-agent/api/android-agent.api

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
public abstract interface annotation class io/opentelemetry/android/Incubating : java/lang/annotation/Annotation {
2-
}
3-
41
public final class io/opentelemetry/android/agent/OpenTelemetryRumInitializer {
52
public static final field INSTANCE Lio/opentelemetry/android/agent/OpenTelemetryRumInitializer;
63
public static final fun initialize (Landroid/content/Context;Lkotlin/jvm/functions/Function1;)Lio/opentelemetry/android/OpenTelemetryRum;
@@ -102,3 +99,32 @@ public final class io/opentelemetry/android/agent/dsl/instrumentation/SlowRender
10299
public fun enabled (Z)V
103100
}
104101

102+
public final class io/opentelemetry/android/agent/session/SessionConfig {
103+
public static final field Companion Lio/opentelemetry/android/agent/session/SessionConfig$Companion;
104+
public synthetic fun <init> (JJILkotlin/jvm/internal/DefaultConstructorMarker;)V
105+
public synthetic fun <init> (JJLkotlin/jvm/internal/DefaultConstructorMarker;)V
106+
public final fun component1-UwyO8pc ()J
107+
public final fun component2-UwyO8pc ()J
108+
public final fun copy-QTBD994 (JJ)Lio/opentelemetry/android/agent/session/SessionConfig;
109+
public static synthetic fun copy-QTBD994$default (Lio/opentelemetry/android/agent/session/SessionConfig;JJILjava/lang/Object;)Lio/opentelemetry/android/agent/session/SessionConfig;
110+
public fun equals (Ljava/lang/Object;)Z
111+
public final fun getBackgroundInactivityTimeout-UwyO8pc ()J
112+
public final fun getMaxLifetime-UwyO8pc ()J
113+
public fun hashCode ()I
114+
public fun toString ()Ljava/lang/String;
115+
public static final fun withDefaults ()Lio/opentelemetry/android/agent/session/SessionConfig;
116+
}
117+
118+
public final class io/opentelemetry/android/agent/session/SessionConfig$Companion {
119+
public final fun withDefaults ()Lio/opentelemetry/android/agent/session/SessionConfig;
120+
}
121+
122+
public class io/opentelemetry/android/agent/session/factory/SessionManagerFactory : io/opentelemetry/android/agent/session/factory/SessionProviderFactory {
123+
public fun <init> ()V
124+
public fun createSessionProvider (Landroid/app/Application;Lio/opentelemetry/android/agent/session/SessionConfig;)Lio/opentelemetry/android/session/SessionProvider;
125+
}
126+
127+
public abstract interface class io/opentelemetry/android/agent/session/factory/SessionProviderFactory {
128+
public abstract fun createSessionProvider (Landroid/app/Application;Lio/opentelemetry/android/agent/session/SessionConfig;)Lio/opentelemetry/android/session/SessionProvider;
129+
}
130+

android-agent/src/main/kotlin/io/opentelemetry/android/agent/session/SessionConfig.kt

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,63 @@ import kotlin.time.Duration
99
import kotlin.time.Duration.Companion.hours
1010
import kotlin.time.Duration.Companion.minutes
1111

12-
internal class SessionConfig(
12+
/**
13+
* Configures session management behavior in the OpenTelemetry Android SDK.
14+
*
15+
* Sessions provide a way to group related telemetry data (spans, logs, metrics) that occur during
16+
* a logical user interaction or application usage period. This configuration controls when sessions
17+
* expire and new sessions are created.
18+
*
19+
* ## Session Lifecycle
20+
*
21+
* A session can end due to two conditions:
22+
* 1. **Background Inactivity**: When the app goes to background and remains inactive for longer than [backgroundInactivityTimeout]
23+
* 2. **Maximum Lifetime**: When a session has been active for longer than [maxLifetime], regardless of app state
24+
*
25+
* When a session ends, a new session will be created on the next telemetry operation, and the previous
26+
* session ID will be tracked for correlation purposes.
27+
*
28+
* ## Usage Example
29+
*
30+
* ```kotlin
31+
* // Use default configuration (15 minutes background timeout, 4 hours max lifetime)
32+
* val config = SessionConfig.withDefaults()
33+
*
34+
* // Custom configuration for shorter sessions
35+
* val shortSessionConfig = SessionConfig(
36+
* backgroundInactivityTimeout = 5.minutes,
37+
* maxLifetime = 1.hours
38+
* )
39+
* ```
40+
*
41+
* @param backgroundInactivityTimeout duration of app backgrounding after which the session expires.
42+
* Default is 15 minutes, meaning if the app stays in background for 15+ minutes, the current session
43+
* ends and a new one will be created when the app becomes active again.
44+
*
45+
* @param maxLifetime maximum duration a session can remain active regardless of app activity.
46+
* Default is 4 hours, meaning even if the app stays in foreground continuously, sessions will
47+
* rotate every 4 hours to prevent unbounded session growth and ensure fresh session context.
48+
*
49+
* @see io.opentelemetry.android.agent.session.SessionManager
50+
* @see io.opentelemetry.android.session.SessionProvider
51+
*/
52+
data class SessionConfig(
1353
val backgroundInactivityTimeout: Duration = 15.minutes,
1454
val maxLifetime: Duration = 4.hours,
1555
) {
1656
companion object {
57+
/**
58+
* Creates a SessionConfig with default values.
59+
*
60+
* Default configuration:
61+
* - Background inactivity timeout: 15 minutes
62+
* - Maximum session lifetime: 4 hours
63+
*
64+
* These defaults balance session continuity with memory efficiency and provide
65+
* reasonable session boundaries for most mobile applications.
66+
*
67+
* @return a new SessionConfig instance with default timeout values.
68+
*/
1769
@JvmStatic
1870
fun withDefaults(): SessionConfig = SessionConfig()
1971
}

android-agent/src/main/kotlin/io/opentelemetry/android/agent/session/SessionManager.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ internal class SessionManager(
2424
private val maxSessionLifetime: Duration,
2525
) : SessionProvider,
2626
SessionPublisher {
27-
private var session: AtomicReference<Session> = AtomicReference(Session.NONE)
27+
private val session: AtomicReference<Session> = AtomicReference(Session.NONE)
28+
private val previousSession: AtomicReference<Session> = AtomicReference(Session.NONE)
2829
private val observers = synchronizedList(ArrayList<SessionObserver>())
2930

3031
init {
@@ -47,6 +48,10 @@ internal class SessionManager(
4748
if (session.compareAndSet(currentSession, newSession)) {
4849
sessionStorage.save(newSession)
4950
timeoutHandler.bump()
51+
52+
// Track the previous session for session transition correlation
53+
previousSession.set(currentSession)
54+
5055
// Observers need to be called after bumping the timer because it may create a new
5156
// span.
5257
notifyObserversOfSessionUpdate(currentSession, newSession)
@@ -79,6 +84,8 @@ internal class SessionManager(
7984
return elapsedTime >= maxSessionLifetime.inWholeNanoseconds
8085
}
8186

87+
override fun getPreviousSessionId(): String = previousSession.get().getId()
88+
8289
companion object {
8390
@OptIn(Incubating::class)
8491
@JvmStatic
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.android.agent.session.factory
7+
8+
import android.app.Application
9+
import io.opentelemetry.android.agent.session.SessionConfig
10+
import io.opentelemetry.android.agent.session.SessionIdTimeoutHandler
11+
import io.opentelemetry.android.agent.session.SessionManager
12+
import io.opentelemetry.android.internal.services.Services
13+
import io.opentelemetry.android.session.SessionProvider
14+
15+
/**
16+
* Default implementation of [SessionProviderFactory] that creates [SessionManager] instances.
17+
*
18+
* This implementation creates a [SessionManager] and wires it with:
19+
* - A timeout handler configured from the session config
20+
* - Application lifecycle integration for timeout management
21+
*
22+
* This class is open to allow custom implementations for specialized use cases.
23+
*
24+
* @see SessionProviderFactory
25+
* @see SessionManager
26+
* @see SessionConfig
27+
*/
28+
open class SessionManagerFactory : SessionProviderFactory {
29+
/**
30+
* Creates a session manager instance.
31+
*
32+
* This implementation creates a [SessionManager] with a timeout handler that is
33+
* registered with the application lifecycle service.
34+
*
35+
* @param application the Android application instance.
36+
* @param sessionConfig the configuration for session management.
37+
* @return a session manager instance.
38+
*/
39+
override fun createSessionProvider(
40+
application: Application,
41+
sessionConfig: SessionConfig,
42+
): SessionProvider {
43+
val timeoutHandler = SessionIdTimeoutHandler(sessionConfig)
44+
Services.get(application).appLifecycle.registerListener(timeoutHandler)
45+
return SessionManager.create(timeoutHandler, sessionConfig)
46+
}
47+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.android.agent.session.factory
7+
8+
import android.app.Application
9+
import io.opentelemetry.android.agent.session.SessionConfig
10+
import io.opentelemetry.android.session.SessionProvider
11+
12+
/**
13+
* Factory for creating [SessionProvider] instances.
14+
*
15+
* This interface follows the Factory design pattern to enable flexible dependency injection
16+
* and testing of session management components. Implementations can provide custom session
17+
* providers with different behaviors while maintaining a consistent creation interface.
18+
*
19+
* @see SessionManagerFactory
20+
* @see SessionProvider
21+
*/
22+
interface SessionProviderFactory {
23+
/**
24+
* Creates a session provider instance.
25+
*
26+
* @param application the Android application instance.
27+
* @param sessionConfig the configuration for session management.
28+
* @return a session provider instance.
29+
*/
30+
fun createSessionProvider(
31+
application: Application,
32+
sessionConfig: SessionConfig,
33+
): SessionProvider
34+
}
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.android.agent.session
7+
8+
import org.junit.jupiter.api.Assertions.assertEquals
9+
import org.junit.jupiter.api.Test
10+
import org.junit.jupiter.api.assertAll
11+
import kotlin.time.Duration.Companion.hours
12+
import kotlin.time.Duration.Companion.minutes
13+
import kotlin.time.Duration.Companion.seconds
14+
15+
private val ALTERNATIVE_BACKGROUND_TIMEOUT = 10.minutes
16+
private val ALTERNATIVE_MAX_LIFETIME = 2.hours
17+
private val DEFAULT_BACKGROUND_INACTIVITY_TIMEOUT = 15.minutes
18+
private val DEFAULT_MAX_LIFETIME = 4.hours
19+
20+
/**
21+
* Validates [SessionConfig] data class functionality including defaults, constructors,
22+
* and edge cases.
23+
*/
24+
class SessionConfigTest {
25+
@Test
26+
fun `withDefaults should create config with expected default values`() {
27+
// When
28+
val config = SessionConfig.withDefaults()
29+
30+
// Then
31+
assertAll(
32+
{ assertEquals(DEFAULT_BACKGROUND_INACTIVITY_TIMEOUT, config.backgroundInactivityTimeout) },
33+
{ assertEquals(DEFAULT_MAX_LIFETIME, config.maxLifetime) },
34+
)
35+
}
36+
37+
@Test
38+
fun `constructor should accept valid values`() {
39+
// When
40+
val config =
41+
SessionConfig(
42+
backgroundInactivityTimeout = ALTERNATIVE_BACKGROUND_TIMEOUT,
43+
maxLifetime = ALTERNATIVE_MAX_LIFETIME,
44+
)
45+
46+
// Then
47+
assertAll(
48+
{ assertEquals(ALTERNATIVE_BACKGROUND_TIMEOUT, config.backgroundInactivityTimeout) },
49+
{ assertEquals(ALTERNATIVE_MAX_LIFETIME, config.maxLifetime) },
50+
)
51+
}
52+
53+
@Test
54+
fun `constructor should allow equal values for both timeouts`() {
55+
// Given
56+
val equalTimeout = 30.minutes
57+
58+
// When
59+
val config =
60+
SessionConfig(
61+
backgroundInactivityTimeout = equalTimeout,
62+
maxLifetime = equalTimeout,
63+
)
64+
65+
// Then
66+
assertAll(
67+
{ assertEquals(equalTimeout, config.backgroundInactivityTimeout) },
68+
{ assertEquals(equalTimeout, config.maxLifetime) },
69+
)
70+
}
71+
72+
@Test
73+
fun `constructor should accept negative values`() {
74+
// Given
75+
val backgroundTimeout = (-5).minutes
76+
val maxLifetime = (-1).hours
77+
78+
// When
79+
val config =
80+
SessionConfig(
81+
backgroundInactivityTimeout = backgroundTimeout,
82+
maxLifetime = maxLifetime,
83+
)
84+
85+
// Then
86+
assertAll(
87+
{ assertEquals(backgroundTimeout, config.backgroundInactivityTimeout) },
88+
{ assertEquals(maxLifetime, config.maxLifetime) },
89+
)
90+
}
91+
92+
@Test
93+
fun `constructor should accept zero values`() {
94+
// Given
95+
val timeout = 0.seconds
96+
97+
// When
98+
val config =
99+
SessionConfig(
100+
backgroundInactivityTimeout = timeout,
101+
maxLifetime = timeout,
102+
)
103+
104+
// Then
105+
assertAll(
106+
{ assertEquals(timeout, config.backgroundInactivityTimeout) },
107+
{ assertEquals(timeout, config.maxLifetime) },
108+
)
109+
}
110+
111+
@Test
112+
fun `constructor should create instance with only backgroundInactivityTimeout specified`() {
113+
// Given
114+
val timeout = 10.minutes
115+
116+
// When
117+
val config = SessionConfig(backgroundInactivityTimeout = timeout)
118+
119+
// Then
120+
assertAll(
121+
{ assertEquals(timeout, config.backgroundInactivityTimeout) },
122+
{ assertEquals(DEFAULT_MAX_LIFETIME, config.maxLifetime) },
123+
)
124+
}
125+
126+
@Test
127+
fun `constructor should create instance with only maxLifetime specified`() {
128+
// When
129+
val config = SessionConfig(maxLifetime = ALTERNATIVE_MAX_LIFETIME)
130+
131+
// Then
132+
assertAll(
133+
{ assertEquals(DEFAULT_BACKGROUND_INACTIVITY_TIMEOUT, config.backgroundInactivityTimeout) },
134+
{ assertEquals(ALTERNATIVE_MAX_LIFETIME, config.maxLifetime) },
135+
)
136+
}
137+
138+
@Test
139+
fun `should handle edge case durations`() {
140+
// Given - very small durations
141+
val smallBackgroundTimeout = 1.seconds
142+
val smallMaxLifetime = 2.seconds
143+
144+
// When
145+
val smallConfig =
146+
SessionConfig(
147+
backgroundInactivityTimeout = smallBackgroundTimeout,
148+
maxLifetime = smallMaxLifetime,
149+
)
150+
151+
// Then
152+
assertAll(
153+
{ assertEquals(smallBackgroundTimeout, smallConfig.backgroundInactivityTimeout) },
154+
{ assertEquals(smallMaxLifetime, smallConfig.maxLifetime) },
155+
)
156+
157+
// Given - very large durations
158+
val largeBackgroundTimeout = 1000.hours
159+
val largeMaxLifetime = 2000.hours
160+
161+
// When
162+
val largeConfig =
163+
SessionConfig(
164+
backgroundInactivityTimeout = largeBackgroundTimeout,
165+
maxLifetime = largeMaxLifetime,
166+
)
167+
168+
// Then
169+
assertAll(
170+
{ assertEquals(largeBackgroundTimeout, largeConfig.backgroundInactivityTimeout) },
171+
{ assertEquals(largeMaxLifetime, largeConfig.maxLifetime) },
172+
)
173+
}
174+
}

0 commit comments

Comments
 (0)