Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions android-agent/api/android-agent.api
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,32 @@ public final class io/opentelemetry/android/agent/dsl/instrumentation/SlowRender
public fun enabled (Z)V
}

public final class io/opentelemetry/android/agent/session/SessionConfig {
public static final field Companion Lio/opentelemetry/android/agent/session/SessionConfig$Companion;
public synthetic fun <init> (JJILkotlin/jvm/internal/DefaultConstructorMarker;)V
public synthetic fun <init> (JJLkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1-UwyO8pc ()J
public final fun component2-UwyO8pc ()J
public final fun copy-QTBD994 (JJ)Lio/opentelemetry/android/agent/session/SessionConfig;
public static synthetic fun copy-QTBD994$default (Lio/opentelemetry/android/agent/session/SessionConfig;JJILjava/lang/Object;)Lio/opentelemetry/android/agent/session/SessionConfig;
public fun equals (Ljava/lang/Object;)Z
public final fun getBackgroundInactivityTimeout-UwyO8pc ()J
public final fun getMaxLifetime-UwyO8pc ()J
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
public static final fun withDefaults ()Lio/opentelemetry/android/agent/session/SessionConfig;
}

public final class io/opentelemetry/android/agent/session/SessionConfig$Companion {
public final fun withDefaults ()Lio/opentelemetry/android/agent/session/SessionConfig;
}

public class io/opentelemetry/android/agent/session/factory/SessionManagerFactory : io/opentelemetry/android/agent/session/factory/SessionProviderFactory {
public fun <init> ()V
public fun createSessionProvider (Landroid/app/Application;Lio/opentelemetry/android/agent/session/SessionConfig;)Lio/opentelemetry/android/session/SessionProvider;
}

public abstract interface class io/opentelemetry/android/agent/session/factory/SessionProviderFactory {
public abstract fun createSessionProvider (Landroid/app/Application;Lio/opentelemetry/android/agent/session/SessionConfig;)Lio/opentelemetry/android/session/SessionProvider;
}

Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,69 @@

package io.opentelemetry.android.agent.session

import io.opentelemetry.android.Incubating
import kotlin.time.Duration
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.minutes

internal class SessionConfig(
/**
* Configures session management behavior in the OpenTelemetry Android SDK.
*
* Sessions provide a way to group related telemetry data (spans, logs, metrics) that occur during
* a logical user interaction or application usage period. This configuration controls when sessions
* expire and new sessions are created.
*
* ## Session Lifecycle
*
* A session can end due to two conditions:
* 1. **Background Inactivity**: When the app goes to background and remains inactive for longer than [backgroundInactivityTimeout]
* 2. **Maximum Lifetime**: When a session has been active for longer than [maxLifetime], regardless of app state
*
* When a session ends, a new session will be created on the next telemetry operation, and the previous
* session ID will be tracked for correlation purposes.
*
* ## Usage Example
*
* ```kotlin
* // Use default configuration (15 minutes background timeout, 4 hours max lifetime)
* val config = SessionConfig.withDefaults()
*
* // Custom configuration for shorter sessions
* val shortSessionConfig = SessionConfig(
* backgroundInactivityTimeout = 5.minutes,
* maxLifetime = 1.hours
* )
* ```
*
* @param backgroundInactivityTimeout duration of app backgrounding after which the session expires.
* Default is 15 minutes, meaning if the app stays in background for 15+ minutes, the current session
* ends and a new one will be created when the app becomes active again.
*
* @param maxLifetime maximum duration a session can remain active regardless of app activity.
* Default is 4 hours, meaning even if the app stays in foreground continuously, sessions will
* rotate every 4 hours to prevent unbounded session growth and ensure fresh session context.
*
* @see io.opentelemetry.android.agent.session.SessionManager
* @see io.opentelemetry.android.session.SessionProvider
*/
@Incubating
data class SessionConfig(
val backgroundInactivityTimeout: Duration = 15.minutes,
val maxLifetime: Duration = 4.hours,
) {
companion object {
/**
* Creates a SessionConfig with default values.
*
* Default configuration:
* - Background inactivity timeout: 15 minutes
* - Maximum session lifetime: 4 hours
*
* These defaults balance session continuity with memory efficiency and provide
* reasonable session boundaries for most mobile applications.
*
* @return a new SessionConfig instance with default timeout values.
*/
@JvmStatic
fun withDefaults(): SessionConfig = SessionConfig()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import io.opentelemetry.android.session.SessionProvider
import io.opentelemetry.android.session.SessionPublisher
import io.opentelemetry.sdk.common.Clock
import java.util.Collections.synchronizedList
import java.util.concurrent.atomic.AtomicReference
import kotlin.random.Random
import kotlin.time.Duration

Expand All @@ -23,51 +24,63 @@ internal class SessionManager(
private val maxSessionLifetime: Duration,
) : SessionProvider,
SessionPublisher {
// TODO: Make thread safe / wrap with AtomicReference?
private var session: Session = Session.NONE
private var session: AtomicReference<Session> = AtomicReference(Session.NONE)
private var previousSession: AtomicReference<Session> = AtomicReference(Session.NONE)
private val observers = synchronizedList(ArrayList<SessionObserver>())

init {
sessionStorage.save(session)
sessionStorage.save(session.get())
}

override fun addObserver(observer: SessionObserver) {
observers.add(observer)
}

override fun getSessionId(): String {
// value will never be null
var newSession = session
val currentSession = session.get()

if (sessionHasExpired() || timeoutHandler.hasTimedOut()) {
// Check if we need to create a new session.
return if (sessionHasExpired() || timeoutHandler.hasTimedOut()) {
val newId = idGenerator.generateSessionId()
val newSession = Session.DefaultSession(newId, clock.now())

// TODO FIXME: This is not threadsafe -- if two threads call getSessionId()
// at the same time while timed out, two new sessions are created
// Could require SessionStorage impls to be atomic/threadsafe or
// do the locking in this class?

newSession = Session.DefaultSession(newId, clock.now())
sessionStorage.save(newSession)
// Atomically update the session only if it hasn't been changed by another thread.
if (session.compareAndSet(currentSession, newSession)) {
sessionStorage.save(newSession)
timeoutHandler.bump()
// Track the previous session for session transition correlation
previousSession.set(currentSession)
// Observers need to be called after bumping the timer because it may create a new
// span.
notifyObserversOfSessionUpdate(currentSession, newSession)
newSession.getId()
} else {
// Another thread accessed this function prior to creating a new session. Use the
// current session.
timeoutHandler.bump()
session.get().getId()
}
} else {
// No new session needed, just bump the timeout and return current session ID
timeoutHandler.bump()
currentSession.getId()
}
}

timeoutHandler.bump()
override fun getPreviousSessionId(): String = previousSession.get().getId()

// observers need to be called after bumping the timer because it may
// create a new span
if (newSession != session) {
val previousSession = session
session = newSession
observers.forEach {
it.onSessionEnded(previousSession)
it.onSessionStarted(session, previousSession)
}
private fun notifyObserversOfSessionUpdate(
currentSession: Session,
newSession: Session,
) {
observers.forEach {
it.onSessionEnded(currentSession)
it.onSessionStarted(newSession, currentSession)
}
return session.getId()
}

private fun sessionHasExpired(): Boolean {
val elapsedTime = clock.now() - session.getStartTimestamp()
val elapsedTime = clock.now() - session.get().getStartTimestamp()
return elapsedTime >= maxSessionLifetime.inWholeNanoseconds
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.android.agent.session.factory

import android.app.Application
import io.opentelemetry.android.Incubating
import io.opentelemetry.android.agent.session.SessionConfig
import io.opentelemetry.android.agent.session.SessionIdTimeoutHandler
import io.opentelemetry.android.agent.session.SessionManager
import io.opentelemetry.android.internal.services.Services
import io.opentelemetry.android.session.SessionProvider

/**
* A [SessionProviderFactory] that creates a [SessionManager].
*
* This interface is the interface for the factory in the Factory design pattern.
* @see <a href="https://www.tutorialspoint.com/design_pattern/factory_pattern.htm">Factory Design Pattern</a>
*/
@OptIn(Incubating::class)
open class SessionManagerFactory : SessionProviderFactory {
/**
* Creates a [SessionManager] with the [application] and [sessionConfig].
* @param application for watching application states.
* @param sessionConfig for configuring the session management.
* @return the newly created provider.
*/
override fun createSessionProvider(
application: Application,
sessionConfig: SessionConfig,
): SessionProvider {
val timeoutHandler = SessionIdTimeoutHandler(sessionConfig)
Services.get(application).appLifecycle.registerListener(timeoutHandler)
return SessionManager.create(timeoutHandler, sessionConfig)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.android.agent.session.factory

import android.app.Application
import io.opentelemetry.android.Incubating
import io.opentelemetry.android.agent.session.SessionConfig
import io.opentelemetry.android.session.SessionProvider

/**
* Creates a session Provider instance.
*
* This interface is the interface for the factory in the Factory design pattern.
* @see <a href="https://www.tutorialspoint.com/design_pattern/factory_pattern.htm">Factory Design Pattern</a>
*/
@OptIn(Incubating::class)
interface SessionProviderFactory {
/**
* Creates a session Provider with the [application] and [sessionConfig].
* @param application for watching application states.
* @param sessionConfig for configuring the session management.
* @return the newly created provider.
*/
fun createSessionProvider(
application: Application,
sessionConfig: SessionConfig,
): SessionProvider
}
Loading