From 601d947f96cfadb11062de2f2e896d0bc1162de7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matu=CC=81s=CC=8C=20Tomlein?= Date: Mon, 10 Feb 2025 10:11:01 +0100 Subject: [PATCH] Add an option to continue previously persisted session when the app restarts rather than starting a new one (close #340) --- .../snowplowdemokotlin/Demo.kt | 5 +- .../internal/tracker/ConfigurationTest.kt | 3 + .../snowplow/tracker/SessionTest.kt | 57 ++++++-- .../core/constants/Parameters.kt | 1 + .../snowplowanalytics/core/session/Session.kt | 130 ++++++++++-------- .../session/SessionConfigurationInterface.kt | 8 ++ .../core/session/SessionControllerImpl.kt | 20 +++ .../core/tracker/ServiceProvider.kt | 1 + .../snowplowanalytics/core/tracker/Tracker.kt | 36 +++-- .../core/tracker/TrackerDefaults.kt | 1 + .../configuration/SessionConfiguration.kt | 10 ++ .../snowplow/entity/ClientSessionEntity.kt | 9 ++ .../snowplow/tracker/SessionState.kt | 95 +++++++++---- 13 files changed, 271 insertions(+), 105 deletions(-) diff --git a/snowplow-demo-kotlin/src/main/java/com/snowplowanalytics/snowplowdemokotlin/Demo.kt b/snowplow-demo-kotlin/src/main/java/com/snowplowanalytics/snowplowdemokotlin/Demo.kt index 0a10a06f0..6a4f361bf 100644 --- a/snowplow-demo-kotlin/src/main/java/com/snowplowanalytics/snowplowdemokotlin/Demo.kt +++ b/snowplow-demo-kotlin/src/main/java/com/snowplowanalytics/snowplowdemokotlin/Demo.kt @@ -279,9 +279,10 @@ class Demo : Activity(), LoggerDelegate { .installAutotracking(true) .diagnosticAutotracking(true) val sessionConfiguration = SessionConfiguration( - TimeMeasure(6, TimeUnit.SECONDS), - TimeMeasure(30, TimeUnit.SECONDS) + TimeMeasure(30, TimeUnit.MINUTES), + TimeMeasure(30, TimeUnit.MINUTES) ) + .continueSessionOnRestart(true) .onSessionUpdate { state: SessionState -> updateLogger( """ diff --git a/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/internal/tracker/ConfigurationTest.kt b/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/internal/tracker/ConfigurationTest.kt index 1b058e2a0..299fe4378 100644 --- a/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/internal/tracker/ConfigurationTest.kt +++ b/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/internal/tracker/ConfigurationTest.kt @@ -73,6 +73,7 @@ class ConfigurationTest { Assert.assertEquals(protocol, scheme) Assert.assertEquals(trackerConfiguration.appId, tracker.appId) Assert.assertEquals("namespace", tracker.namespace) + Assert.assertEquals(tracker.session!!.continueSessionOnRestart, false) } @Test @@ -83,12 +84,14 @@ class ConfigurationTest { val networkConfig = NetworkConfiguration("fake-url", HttpMethod.POST) val trackerConfig = TrackerConfiguration("appId") val sessionConfig = SessionConfiguration(expectedForeground, expectedBackground) + sessionConfig.continueSessionOnRestart = true val tracker = createTracker(context, "namespace", networkConfig, trackerConfig, sessionConfig) val foreground = tracker.session!!.foregroundTimeout val background = tracker.session!!.backgroundTimeout Assert.assertEquals(expectedForeground, foreground) Assert.assertEquals(expectedBackground, background) + Assert.assertEquals(tracker.session!!.continueSessionOnRestart, true) } @Test diff --git a/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/tracker/SessionTest.kt b/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/tracker/SessionTest.kt index 062d575c9..1d3378ac2 100644 --- a/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/tracker/SessionTest.kt +++ b/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/tracker/SessionTest.kt @@ -52,14 +52,14 @@ class SessionTest { Assert.assertEquals(300000, session.backgroundTimeout) Assert.assertNull(sessionState) Assert.assertNotNull(session.userId) - val sdj = session.getSessionContext("first-id-1", timestamp, false) + val sdj = session.getAndUpdateSessionForEvent("first-id-1", timestamp, false) sessionState = session.state Assert.assertNotNull(sdj) Assert.assertNotNull(sessionState) Assert.assertEquals("first-id-1", sessionState!!.firstEventId) Assert.assertEquals(timestampDateTime, sessionState.firstEventTimestamp) - session.getSessionContext("second-id-2", timestamp + 10000, false) + session.getAndUpdateSessionForEvent("second-id-2", timestamp + 10000, false) Assert.assertEquals("first-id-1", sessionState.firstEventId) Assert.assertEquals(timestampDateTime, sessionState.firstEventTimestamp) Assert.assertEquals(TrackerConstants.SESSION_SCHEMA, sdj!!.map["schema"]) @@ -342,8 +342,8 @@ class SessionTest { val tracker2 = Tracker(emitter, "tracker2", "app", context = context, builder = trackerBuilder) val session1 = tracker1.session val session2 = tracker2.session - session1!!.getSessionContext("session1-fake-id1", timestamp, false) - session2!!.getSessionContext("session2-fake-id1", timestamp, false) + session1!!.getAndUpdateSessionForEvent("session1-fake-id1", timestamp, false) + session2!!.getAndUpdateSessionForEvent("session2-fake-id1", timestamp, false) val initialValue1 = session1.sessionIndex?.toLong() val id1 = session1.state!!.sessionId val initialValue2 = session2.sessionIndex?.toLong() @@ -351,12 +351,12 @@ class SessionTest { // Retrigger session in tracker1 // The timeout is 20s, this sleep is only 2s - it's still the same session Thread.sleep(2000) - session1.getSessionContext("session1-fake-id2", timestamp, false) + session1.getAndUpdateSessionForEvent("session1-fake-id2", timestamp, false) // Retrigger timedout session in tracker2 // 20s has then passed. Session must be updated, increasing the sessionIndex by 1 Thread.sleep(18000) - session2.getSessionContext("session2-fake-id2", timestamp, false) + session2.getAndUpdateSessionForEvent("session2-fake-id2", timestamp, false) // Check sessions have the correct state Assert.assertEquals(0, session1.sessionIndex!! - initialValue1!!) @@ -365,7 +365,7 @@ class SessionTest { // Recreate tracker2 val tracker2b = Tracker(emitter, "tracker2", "app", context = context, builder = trackerBuilder) - tracker2b.session!!.getSessionContext("session2b-fake-id3", timestamp, false) + tracker2b.session!!.getAndUpdateSessionForEvent("session2b-fake-id3", timestamp, false) val initialValue2b = tracker2b.session!!.sessionIndex?.toLong() val previousId2b = tracker2b.session!!.state!!.previousSessionId @@ -388,6 +388,47 @@ class SessionTest { Assert.assertNull(context[Parameters.SESSION_PREVIOUS_ID]) } + @Test + fun testStartsNewSessionOnRestartByDefault() { + val session1 = Session(foregroundTimeout = 3, backgroundTimeout = 3, namespace = "t1", timeUnit = TimeUnit.SECONDS, context = context) + val firstSession = session1.getAndUpdateSessionForEvent("event_1", eventTimestamp = 1654496481345, userAnonymisation = false) + + val session2 = Session(foregroundTimeout = 3, backgroundTimeout = 3, namespace = "t1", timeUnit = TimeUnit.SECONDS, context = context) + val secondSession = session2.getAndUpdateSessionForEvent("event_2", eventTimestamp = 1654496481345, userAnonymisation = false) + + Assert.assertNotNull(firstSession?.sessionId) + Assert.assertNotEquals(firstSession?.sessionId, secondSession?.sessionId) + Assert.assertEquals(firstSession?.sessionId, secondSession?.previousSessionId) + } + + @Test + fun testResumesPreviouslyPersistedSessionIfEnabled() { + val session1 = Session(foregroundTimeout = 3, backgroundTimeout = 3, namespace = "t1", continueSessionOnRestart = true, timeUnit = TimeUnit.SECONDS, context = context) + session1.getAndUpdateSessionForEvent("event_1", eventTimestamp = 1654496481345, userAnonymisation = false) + val firstSession = session1.getAndUpdateSessionForEvent("event_2", eventTimestamp = 1654496481346, userAnonymisation = false) + + val session2 = Session(foregroundTimeout = 3, backgroundTimeout = 3, namespace = "t1", continueSessionOnRestart = true, timeUnit = TimeUnit.SECONDS, context = context) + val secondSession = session2.getAndUpdateSessionForEvent("event_3", eventTimestamp = 1654496481347, userAnonymisation = false) + + Assert.assertNotNull(firstSession?.sessionId) + Assert.assertEquals(firstSession?.sessionId, secondSession?.sessionId) + Assert.assertEquals(secondSession?.eventIndex, 3) + } + + @Test + fun testStartsNewSessionOnRestartOnTimeout() { + val session1 = Session(foregroundTimeout = 100, backgroundTimeout = 100, namespace = "t1", continueSessionOnRestart = true, timeUnit = TimeUnit.MILLISECONDS, context = context) + val firstSession = session1.getAndUpdateSessionForEvent("event_1", eventTimestamp = 1654496481345, userAnonymisation = false) + + Thread.sleep(500) + + val session2 = Session(foregroundTimeout = 100, backgroundTimeout = 100, namespace = "t1", continueSessionOnRestart = true, timeUnit = TimeUnit.MILLISECONDS, context = context) + val secondSession = session2.getAndUpdateSessionForEvent("event_2", eventTimestamp = 1654496481345, userAnonymisation = false) + + Assert.assertNotEquals(firstSession?.sessionId, secondSession?.sessionId) + Assert.assertEquals(firstSession?.sessionId, secondSession?.previousSessionId) + } + // Private methods private fun getSession(foregroundTimeout: Long, backgroundTimeout: Long): Session { context.getSharedPreferences(TrackerConstants.SNOWPLOW_SESSION_VARS, Context.MODE_PRIVATE) @@ -403,7 +444,7 @@ class SessionTest { eventTimestamp: Long, userAnonymisation: Boolean ): Map? { - return session!!.getSessionContext( + return session!!.getAndUpdateSessionForEvent( eventId, eventTimestamp, userAnonymisation diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/constants/Parameters.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/constants/Parameters.kt index 0a5ac0d7e..8c52d395f 100755 --- a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/constants/Parameters.kt +++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/constants/Parameters.kt @@ -227,6 +227,7 @@ object Parameters { const val SESSION_STORAGE = "storageMechanism" const val SESSION_FIRST_ID = "firstEventId" const val SESSION_FIRST_TIMESTAMP = "firstEventTimestamp" + const val SESSION_LAST_UPDATE = "lastUpdate" // Screen Context const val SCREEN_NAME = "name" diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/session/Session.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/session/Session.kt index ebc89fd92..d12056bca 100644 --- a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/session/Session.kt +++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/session/Session.kt @@ -20,11 +20,10 @@ import androidx.core.util.Consumer import com.snowplowanalytics.core.constants.Parameters import com.snowplowanalytics.core.constants.TrackerConstants import com.snowplowanalytics.core.tracker.Logger +import com.snowplowanalytics.core.tracker.TrackerDefaults import com.snowplowanalytics.core.utils.Util import com.snowplowanalytics.snowplow.entity.ClientSessionEntity -import com.snowplowanalytics.snowplow.payload.SelfDescribingJson import com.snowplowanalytics.snowplow.tracker.SessionState -import com.snowplowanalytics.snowplow.tracker.SessionState.Companion.build import org.json.JSONException import org.json.JSONObject import java.util.concurrent.TimeUnit @@ -48,12 +47,14 @@ class Session @SuppressLint("ApplySharedPref") constructor( backgroundTimeout: Long, timeUnit: TimeUnit, namespace: String?, - context: Context + context: Context, + + /// If enabled, will persist all session updates (also changes to eventIndex) and will be able to continue the previous session when the app is closed and reopened. + var continueSessionOnRestart: Boolean = TrackerDefaults.continueSessionOnRestart, ) { // Session Variables var userId: String private set - private var eventIndex = 0 @Volatile var backgroundIndex = 0 @@ -69,7 +70,6 @@ class Session @SuppressLint("ApplySharedPref") constructor( val isBackground: Boolean get() = _isBackground.get() - private var lastSessionCheck: Long = 0 private val isNewSession = AtomicBoolean(true) @Volatile @@ -92,6 +92,8 @@ class Session @SuppressLint("ApplySharedPref") constructor( this.foregroundTimeout = timeUnit.toMillis(foregroundTimeout) this.backgroundTimeout = timeUnit.toMillis(backgroundTimeout) isSessionCheckerEnabled = true + + isNewSession.set(!continueSessionOnRestart) var sessionVarsName = TrackerConstants.SNOWPLOW_SESSION_VARS if (namespace != null && namespace.isNotEmpty()) { @@ -105,11 +107,10 @@ class Session @SuppressLint("ApplySharedPref") constructor( if (sessionInfo == null) { Logger.track(TAG, "No previous session info available") } else { - state = build(sessionInfo) + state = SessionState.build(sessionInfo) } userId = retrieveUserId(context, state) sharedPreferences = context.getSharedPreferences(sessionVarsName, Context.MODE_PRIVATE) - lastSessionCheck = System.currentTimeMillis() } finally { StrictMode.setThreadPolicy(oldPolicy) } @@ -122,34 +123,40 @@ class Session @SuppressLint("ApplySharedPref") constructor( * @return a SelfDescribingJson containing the session context */ @Synchronized - fun getSessionContext( + fun getAndUpdateSessionForEvent( eventId: String, eventTimestamp: Long, userAnonymisation: Boolean - ): SelfDescribingJson? { + ): ClientSessionEntity? { Logger.v(TAG, "Getting session context...") - if (isSessionCheckerEnabled) { - if (shouldUpdateSession()) { - Logger.d(TAG, "Update session information.") - updateSession(eventId, eventTimestamp) - if (isBackground) { // timed out in background - executeEventCallback(backgroundTimeoutCallback) - } else { // timed out in foreground - executeEventCallback(foregroundTimeoutCallback) - } + if (isSessionCheckerEnabled && shouldStartNewSession()) { + Logger.d(TAG, "Update session information.") + startNewSession(eventId, eventTimestamp) + + state?.let { + callOnSessionUpdateCallback(it) + } + + if (isBackground) { // timed out in background + executeEventCallback(backgroundTimeoutCallback) + } else { // timed out in foreground + executeEventCallback(foregroundTimeoutCallback) } - lastSessionCheck = System.currentTimeMillis() + + if (!continueSessionOnRestart) { persist() } } - eventIndex += 1 - + val state = state ?: run { Logger.v(TAG, "Session state not present") return null } + + state.updateForNextEvent(isSessionCheckerEnabled) + // persist every session update + if (continueSessionOnRestart) { persist() } val sessionValues = state.sessionValues val sessionCopy: MutableMap = HashMap(sessionValues) - sessionCopy[Parameters.SESSION_EVENT_INDEX] = eventIndex if (userAnonymisation) { sessionCopy[Parameters.SESSION_USER_ID] = "00000000-0000-0000-0000-000000000000" @@ -158,51 +165,48 @@ class Session @SuppressLint("ApplySharedPref") constructor( return ClientSessionEntity(sessionCopy) } - private fun shouldUpdateSession(): Boolean { + private fun shouldStartNewSession(): Boolean { if (isNewSession.get()) { return true } - val now = System.currentTimeMillis() - val timeout = if (isBackground) backgroundTimeout else foregroundTimeout - return now < lastSessionCheck || now - lastSessionCheck > timeout + val lastSessionCheck = state?.lastUpdate + if (lastSessionCheck !== null) { + val now = System.currentTimeMillis() + val timeout = if (isBackground) backgroundTimeout else foregroundTimeout + return now < lastSessionCheck || now - lastSessionCheck > timeout + } + return true } @Synchronized - private fun updateSession(eventId: String, eventTimestamp: Long) { + private fun startNewSession(eventId: String, eventTimestamp: Long) { isNewSession.set(false) - val currentSessionId = Util.uUIDString() - val eventTimestampDateTime = Util.getDateTimeFromTimestamp(eventTimestamp) - - var sessionIndex = 1 - eventIndex = 0 - var previousSessionId: String? = null - var storage = "LOCAL_STORAGE" - state?.let { - sessionIndex = it.sessionIndex + 1 - previousSessionId = it.sessionId - storage = it.storage - } - state = SessionState( - eventId, - eventTimestampDateTime, - currentSessionId, - previousSessionId, - sessionIndex, - userId, - storage - ) - state?.let { - storeSessionState(it) - callOnSessionUpdateCallback(it) + + if (state == null) { + state = SessionState( + firstEventId = eventId, + firstEventTimestamp = Util.getDateTimeFromTimestamp(eventTimestamp), + sessionId = Util.uUIDString(), + previousSessionId = null, + sessionIndex = 1, + userId = userId, + ) + } else { + state?.startNewSession( + eventId = eventId, + eventTimestamp = eventTimestamp + ) } } - private fun storeSessionState(state: SessionState) { - val jsonObject = JSONObject(state.sessionValues) - val jsonString = jsonObject.toString() - val editor = sharedPreferences.edit() - editor.putString(TrackerConstants.SESSION_STATE, jsonString) - editor.apply() + private fun persist() { + state?.let { state -> + val jsonObject = JSONObject(state.dataToPersist) + val jsonString = jsonObject.toString() + val editor = sharedPreferences.edit() + editor.putString(TrackerConstants.SESSION_STATE, jsonString) + editor.apply() + } } private fun callOnSessionUpdateCallback(state: SessionState) { @@ -324,10 +328,18 @@ class Session @SuppressLint("ApplySharedPref") constructor( backgroundTimeout: Long, timeUnit: TimeUnit, namespace: String?, - sessionCallbacks: Array? + sessionCallbacks: Array?, + continueSessionOnRestart: Boolean ): Session { val session = - Session(foregroundTimeout, backgroundTimeout, timeUnit, namespace, context) + Session( + foregroundTimeout = foregroundTimeout, + backgroundTimeout = backgroundTimeout, + timeUnit = timeUnit, + namespace = namespace, + context = context, + continueSessionOnRestart = continueSessionOnRestart + ) var callbacks: Array? = arrayOf(null, null, null, null) if (sessionCallbacks != null && sessionCallbacks.size == 4) { callbacks = sessionCallbacks diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/session/SessionConfigurationInterface.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/session/SessionConfigurationInterface.kt index a7c9d6abf..1f2115c8e 100644 --- a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/session/SessionConfigurationInterface.kt +++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/session/SessionConfigurationInterface.kt @@ -34,4 +34,12 @@ interface SessionConfigurationInterface { * The callback called every time the session is updated. */ var onSessionUpdate: Consumer? + + + /** + * If enabled, will be able to continue the previous session when the app is closed and reopened (if it doesn't timeout). + * Disabled by default, which means that every restart of the app starts a new session. + * When enabled, every event will result in the session being updated in the UserDefaults. + */ + var continueSessionOnRestart: Boolean } diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/session/SessionControllerImpl.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/session/SessionControllerImpl.kt index 6d723900e..d401a762c 100644 --- a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/session/SessionControllerImpl.kt +++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/session/SessionControllerImpl.kt @@ -18,6 +18,7 @@ import com.snowplowanalytics.core.Controller import com.snowplowanalytics.core.tracker.Logger import com.snowplowanalytics.core.tracker.ServiceProviderInterface import com.snowplowanalytics.core.tracker.Tracker +import com.snowplowanalytics.core.tracker.TrackerDefaults import com.snowplowanalytics.snowplow.configuration.SessionConfiguration import com.snowplowanalytics.snowplow.controller.SessionController import com.snowplowanalytics.snowplow.tracker.SessionState @@ -169,6 +170,25 @@ class SessionControllerImpl // Constructors session.onSessionUpdate = onSessionUpdate } + override var continueSessionOnRestart: Boolean + get() { + val session = session + if (session == null) { + Logger.track(TAG, "Attempt to access SessionController fields when disabled") + return TrackerDefaults.continueSessionOnRestart + } + return session.continueSessionOnRestart + } + set(value) { + val session = session + if (session == null) { + Logger.track(TAG, "Attempt to access SessionController fields when disabled") + return + } + dirtyConfig.continueSessionOnRestart = value + session.continueSessionOnRestart = value + } + // Service method val isEnabled: Boolean get() = tracker.session != null diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/ServiceProvider.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/ServiceProvider.kt index ff765b26c..f3ba12d87 100644 --- a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/ServiceProvider.kt +++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/ServiceProvider.kt @@ -295,6 +295,7 @@ class ServiceProvider( tracker.backgroundTimeout = sessionConfiguration.backgroundTimeout.convert(TimeUnit.SECONDS) tracker.foregroundTimeout = sessionConfiguration.foregroundTimeout.convert(TimeUnit.SECONDS) + tracker.continueSessionOnRestart = sessionConfiguration.continueSessionOnRestart for (plugin in pluginConfigurations) { tracker.addOrReplaceStateMachine(plugin.toStateMachine()) diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/Tracker.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/Tracker.kt index 961a0ebb8..4a00c574e 100755 --- a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/Tracker.kt +++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/Tracker.kt @@ -130,6 +130,14 @@ class Tracker( } } + var continueSessionOnRestart: Boolean = TrackerDefaults.continueSessionOnRestart + set(continueSession) { + if (!builderFinished) { + field = continueSession + session?.continueSessionOnRestart = continueSession + } + } + /** * This configuration option is not published in the TrackerConfiguration class. * Create a Tracker directly, not via the Snowplow interface, to configure timeUnit. @@ -242,12 +250,13 @@ class Tracker( callbacks = sessionCallbacks } session = getInstance( - context, - foregroundTimeout, - backgroundTimeout, - timeUnit, - namespace, - callbacks + context = context, + foregroundTimeout = foregroundTimeout, + backgroundTimeout = backgroundTimeout, + timeUnit = timeUnit, + namespace = namespace, + sessionCallbacks = callbacks, + continueSessionOnRestart = continueSessionOnRestart, ) } } @@ -401,12 +410,13 @@ class Tracker( callbacks = sessionCallbacks } session = getInstance( - context, - foregroundTimeout, - backgroundTimeout, - timeUnit, - namespace, - callbacks + context = context, + foregroundTimeout = foregroundTimeout, + backgroundTimeout = backgroundTimeout, + timeUnit = timeUnit, + namespace = namespace, + sessionCallbacks = callbacks, + continueSessionOnRestart = continueSessionOnRestart ) } @@ -641,7 +651,7 @@ class Tracker( return } val sessionContextJson = - sessionManager.getSessionContext(eventId, eventTimestamp, userAnonymisation) + sessionManager.getAndUpdateSessionForEvent(eventId, eventTimestamp, userAnonymisation) sessionContextJson?.let { event.entities.add(it) } } } diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/TrackerDefaults.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/TrackerDefaults.kt index 6926caeb8..53b7dd6d7 100644 --- a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/TrackerDefaults.kt +++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/TrackerDefaults.kt @@ -36,4 +36,5 @@ object TrackerDefaults { var screenEngagementAutotracking = true var installAutotracking = true var userAnonymisation = false + var continueSessionOnRestart = false } diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/configuration/SessionConfiguration.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/configuration/SessionConfiguration.kt index d6a2647b5..717f8297b 100644 --- a/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/configuration/SessionConfiguration.kt +++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/configuration/SessionConfiguration.kt @@ -64,6 +64,11 @@ open class SessionConfiguration : SessionConfigurationInterface, Configuration { get() = _onSessionUpdate ?: sourceConfig?.onSessionUpdate set(value) { _onSessionUpdate = value } + private var _continueSessionOnRestart: Boolean? = null + override var continueSessionOnRestart: Boolean + get() = _continueSessionOnRestart ?: sourceConfig?.continueSessionOnRestart ?: TrackerDefaults.continueSessionOnRestart + set(value) { _continueSessionOnRestart = value } + /** * This will set up the session behaviour of the tracker. * @param foregroundTimeout The timeout set for the inactivity of app when in foreground. @@ -81,6 +86,11 @@ open class SessionConfiguration : SessionConfigurationInterface, Configuration { return this } + fun continueSessionOnRestart(continueSession: Boolean): SessionConfiguration { + this.continueSessionOnRestart = continueSession + return this + } + // Copyable override fun copy(): Configuration { return SessionConfiguration(foregroundTimeout, backgroundTimeout) diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/entity/ClientSessionEntity.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/entity/ClientSessionEntity.kt index 8eaf8c634..6eae48cdd 100644 --- a/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/entity/ClientSessionEntity.kt +++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/entity/ClientSessionEntity.kt @@ -25,4 +25,13 @@ class ClientSessionEntity(private val values: Map) : val userId: String? get() = values[Parameters.SESSION_USER_ID] as String? + + val sessionId: String? + get() = values[Parameters.SESSION_ID] as String? + + val previousSessionId: String? + get() = values[Parameters.SESSION_PREVIOUS_ID] as String? + + val eventIndex: Int? + get() = values[Parameters.SESSION_EVENT_INDEX] as Int? } diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/tracker/SessionState.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/tracker/SessionState.kt index 9ee25bca5..3c1e8f923 100644 --- a/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/tracker/SessionState.kt +++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/tracker/SessionState.kt @@ -14,6 +14,7 @@ package com.snowplowanalytics.snowplow.tracker import com.snowplowanalytics.core.constants.Parameters import com.snowplowanalytics.core.statemachine.State +import com.snowplowanalytics.core.utils.Util /** * Stores the current Session information. Used in creating the client_session entity when @@ -22,30 +23,47 @@ import com.snowplowanalytics.core.statemachine.State * @see com.snowplowanalytics.snowplow.configuration.TrackerConfiguration */ class SessionState( - val firstEventId: String, - val firstEventTimestamp: String, - val sessionId: String, - val previousSessionId: String?, //$ On iOS it has to be set nullable on constructor - val sessionIndex: Int, - val userId: String, - val storage: String + var firstEventId: String, + var firstEventTimestamp: String, + var sessionId: String, + var previousSessionId: String?, //$ On iOS it has to be set nullable on constructor + var sessionIndex: Int, + var userId: String, + var storage: String = "LOCAL_STORAGE", + var eventIndex: Int? = null, + var lastUpdate: Long? = null ) : State { - private val sessionContext = HashMap() - - init { - sessionContext[Parameters.SESSION_FIRST_ID] = firstEventId - sessionContext[Parameters.SESSION_FIRST_TIMESTAMP] = - firstEventTimestamp - sessionContext[Parameters.SESSION_ID] = sessionId - sessionContext[Parameters.SESSION_PREVIOUS_ID] = - previousSessionId - sessionContext[Parameters.SESSION_INDEX] = sessionIndex - sessionContext[Parameters.SESSION_USER_ID] = userId - sessionContext[Parameters.SESSION_STORAGE] = storage - } val sessionValues: Map - get() = sessionContext + get() { + val sessionContext = HashMap() + sessionContext[Parameters.SESSION_FIRST_ID] = firstEventId + sessionContext[Parameters.SESSION_FIRST_TIMESTAMP] = + firstEventTimestamp + sessionContext[Parameters.SESSION_ID] = sessionId + sessionContext[Parameters.SESSION_PREVIOUS_ID] = + previousSessionId + sessionContext[Parameters.SESSION_INDEX] = sessionIndex + sessionContext[Parameters.SESSION_USER_ID] = userId + sessionContext[Parameters.SESSION_STORAGE] = storage + + eventIndex?.let { + sessionContext[Parameters.SESSION_EVENT_INDEX] = it + } + return sessionContext + } + + + val dataToPersist: Map + get() { + val dictionary = sessionValues.toMutableMap() + + lastUpdate?.let { + dictionary[Parameters.SESSION_LAST_UPDATE] = it + } + + return dictionary + } companion object { @JvmStatic @@ -79,8 +97,39 @@ class SessionState( value = storedState[Parameters.SESSION_STORAGE] if (value !is String) return null val storage = value - - return SessionState(firstEventId, firstEventTimestamp, sessionId, previousSessionId, sessionIndex, userId, storage) + + val eventIndex = storedState[Parameters.SESSION_EVENT_INDEX] as? Int + val lastUpdate = storedState[Parameters.SESSION_LAST_UPDATE] as? Long + + return SessionState( + firstEventId=firstEventId, + firstEventTimestamp=firstEventTimestamp, + sessionId=sessionId, + previousSessionId=previousSessionId, + sessionIndex=sessionIndex, + userId=userId, + storage=storage, + eventIndex=eventIndex, + lastUpdate=lastUpdate + ) + } + } + + fun startNewSession(eventId: String, eventTimestamp: Long) { + this.previousSessionId = this.sessionId + this.sessionId = Util.uUIDString() + this.sessionIndex = this.sessionIndex + 1 + this.eventIndex = 0 + this.firstEventId = eventId + this.firstEventTimestamp = Util.getDateTimeFromTimestamp(eventTimestamp) + + this.lastUpdate = System.currentTimeMillis() + } + + fun updateForNextEvent(isSessionCheckerEnabled: Boolean) { + this.eventIndex = (this.eventIndex ?: 0) + 1 + if (isSessionCheckerEnabled) { + this.lastUpdate = System.currentTimeMillis() } } }