From e4f9874f60fe82ea1edbf9c8d76335c7def2581d Mon Sep 17 00:00:00 2001 From: Andrey Sokolov Date: Mon, 24 Oct 2022 10:43:45 +0400 Subject: [PATCH] fix: restore previous session data on app opening (#87) --- .../java/com/amplitude/android/Amplitude.kt | 55 ++-- .../com/amplitude/android/AmplitudeTest.kt | 273 +++++++++++++----- .../main/java/com/amplitude/core/Amplitude.kt | 4 +- .../core/utilities/InMemoryStorage.kt | 5 +- 4 files changed, 226 insertions(+), 111 deletions(-) diff --git a/android/src/main/java/com/amplitude/android/Amplitude.kt b/android/src/main/java/com/amplitude/android/Amplitude.kt index a047737e..fdddba9c 100644 --- a/android/src/main/java/com/amplitude/android/Amplitude.kt +++ b/android/src/main/java/com/amplitude/android/Amplitude.kt @@ -23,17 +23,22 @@ open class Amplitude( ) : Amplitude(configuration) { internal var inForeground = false - var sessionId: Long = -1 + var sessionId: Long private set - internal var lastEventId: Long = 0 - var lastEventTime: Long = -1 - private var previousSessionId: Long = -1 + internal var lastEventId: Long + var lastEventTime: Long private lateinit var androidContextPlugin: AndroidContextPlugin + init { + storage = configuration.storageProvider.getStorage(this) + + this.sessionId = storage.read(Storage.Constants.PREVIOUS_SESSION_ID)?.toLong() ?: -1 + this.lastEventId = storage.read(Storage.Constants.LAST_EVENT_ID)?.toLong() ?: 0 + this.lastEventTime = storage.read(Storage.Constants.LAST_EVENT_TIME)?.toLong() ?: -1 + } + override fun build(): Deferred { - val client = this val built = amplitudeScope.async(amplitudeDispatcher) { - storage = configuration.storageProvider.getStorage(client) val storageDirectory = (configuration as Configuration).context.getDir("${FileStorage.STORAGE_PREFIX}-${configuration.instanceName}", Context.MODE_PRIVATE) idContainer = IdentityContainer.getInstance( IdentityConfiguration( @@ -48,12 +53,6 @@ open class Amplitude( if (idContainer.identityManager.isInitialized()) { listener.onIdentityChanged(idContainer.identityManager.getIdentity(), IdentityUpdateType.Initialized) } - previousSessionId = storage.read(Storage.Constants.PREVIOUS_SESSION_ID)?.toLong() ?: -1 - if (previousSessionId >= 0) { - sessionId = previousSessionId - } - lastEventId = storage.read(Storage.Constants.LAST_EVENT_ID)?.toLong() ?: 0 - lastEventTime = storage.read(Storage.Constants.LAST_EVENT_TIME)?.toLong() ?: -1 androidContextPlugin = AndroidContextPlugin() add(androidContextPlugin) add(GetAmpliExtrasPlugin()) @@ -125,24 +124,19 @@ open class Amplitude( } fun onEnterForeground(timestamp: Long) { - amplitudeScope.launch(amplitudeDispatcher) { - isBuilt.await() - startNewSessionIfNeeded(timestamp) ?. let { - it.forEach { event -> process(event) } - } - inForeground = true + startNewSessionIfNeeded(timestamp) ?. let { + it.forEach { event -> process(event) } } + inForeground = true } fun onExitForeground() { + inForeground = false amplitudeScope.launch(amplitudeDispatcher) { isBuilt.await() - inForeground = false if ((configuration as Configuration).flushEventsOnClose) { flush() } - storage.write(Storage.Constants.PREVIOUS_SESSION_ID, sessionId.toString()) - storage.write(Storage.Constants.LAST_EVENT_TIME, lastEventTime.toString()) } } @@ -157,27 +151,13 @@ open class Amplitude( return startNewSession(timestamp) } - // no current session - check for previous session - if (isWithinMinTimeBetweenSessions(timestamp)) { - if (previousSessionId == -1L) { - return startNewSession(timestamp) - } - - // extend previous session - setSessionId(previousSessionId) - refreshSessionTime(timestamp) - return null - } - return startNewSession(timestamp) } private fun setSessionId(timestamp: Long) { sessionId = timestamp - previousSessionId = timestamp amplitudeScope.launch(amplitudeDispatcher) { - isBuilt.await() - storage.write(Storage.Constants.PREVIOUS_SESSION_ID, timestamp.toString()) + storage.write(Storage.Constants.PREVIOUS_SESSION_ID, sessionId.toString()) } } @@ -213,8 +193,7 @@ open class Amplitude( } lastEventTime = timestamp amplitudeScope.launch(amplitudeDispatcher) { - isBuilt.await() - storage.write(Storage.Constants.LAST_EVENT_TIME, timestamp.toString()) + storage.write(Storage.Constants.LAST_EVENT_TIME, lastEventTime.toString()) } } diff --git a/android/src/test/java/com/amplitude/android/AmplitudeTest.kt b/android/src/test/java/com/amplitude/android/AmplitudeTest.kt index 4914f69c..8e6acadd 100644 --- a/android/src/test/java/com/amplitude/android/AmplitudeTest.kt +++ b/android/src/test/java/com/amplitude/android/AmplitudeTest.kt @@ -4,9 +4,13 @@ import android.app.Application import android.content.Context import com.amplitude.android.plugins.AndroidLifecyclePlugin import com.amplitude.common.android.AndroidContextProvider +import com.amplitude.core.Storage +import com.amplitude.core.StorageProvider import com.amplitude.core.events.BaseEvent import com.amplitude.core.platform.EventPlugin import com.amplitude.core.platform.Plugin +import com.amplitude.core.utilities.ConsoleLoggerProvider +import com.amplitude.core.utilities.InMemoryStorage import com.amplitude.core.utilities.InMemoryStorageProvider import com.amplitude.id.IMIdentityStorageProvider import com.amplitude.id.IdentityConfiguration @@ -70,13 +74,14 @@ class AmplitudeTest { amplitudeDispatcherField.set(amplitude, dispatcher) } - private fun createConfiguration(minTimeBetweenSessionsMillis: Long? = null): Configuration { + private fun createConfiguration(minTimeBetweenSessionsMillis: Long? = null, storageProvider: StorageProvider = InMemoryStorageProvider()): Configuration { val configuration = Configuration( apiKey = "api-key", context = context!!, instanceName = "testInstance", - storageProvider = InMemoryStorageProvider(), + storageProvider = storageProvider, trackingSessionEvents = minTimeBetweenSessionsMillis != null, + loggerProvider = ConsoleLoggerProvider(), ) if (minTimeBetweenSessionsMillis != null) { @@ -148,80 +153,214 @@ class AmplitudeTest { fun amplitude_tracking_session() = runTest { setDispatcher(testScheduler) - amplitude = Amplitude(createConfiguration(100)) + val amplitude = Amplitude(createConfiguration(100)) val mockedPlugin = spyk(StubPlugin()) - amplitude?.add(mockedPlugin) + amplitude.add(mockedPlugin) - if (amplitude?.isBuilt!!.await()) { - val event1 = BaseEvent() - event1.eventType = "test event 1" - event1.timestamp = 1000 - amplitude!!.track(event1) - - val event2 = BaseEvent() - event2.eventType = "test event 2" - event2.timestamp = 1050 - amplitude!!.track(event2) - - val event3 = BaseEvent() - event3.eventType = "test event 3" - event3.timestamp = 1200 - amplitude!!.track(event3) - - val event4 = BaseEvent() - event4.eventType = "test event 4" - event4.timestamp = 1350 - amplitude!!.track(event4) + amplitude.isBuilt!!.await() - advanceUntilIdle() + val event1 = BaseEvent() + event1.eventType = "test event 1" + event1.timestamp = 1000 + amplitude.track(event1) - val tracks = mutableListOf() + val event2 = BaseEvent() + event2.eventType = "test event 2" + event2.timestamp = 1050 + amplitude.track(event2) - verify { - mockedPlugin.track(capture(tracks)) - } + val event3 = BaseEvent() + event3.eventType = "test event 3" + event3.timestamp = 1200 + amplitude.track(event3) - tracks.sortBy { event -> event.eventId } + val event4 = BaseEvent() + event4.eventType = "test event 4" + event4.timestamp = 1350 + amplitude.track(event4) - Assertions.assertEquals(9, tracks.count()) + amplitude.onEnterForeground(1500) - tracks[0].let { - Assertions.assertEquals("session_start", it.eventType) - Assertions.assertEquals(1000L, it.timestamp) - } - tracks[1].let { - Assertions.assertEquals("test event 1", it.eventType) - Assertions.assertEquals(1000L, it.timestamp) - } - tracks[2].let { - Assertions.assertEquals("test event 2", it.eventType) - Assertions.assertEquals(1050L, it.timestamp) - } - tracks[3].let { - Assertions.assertEquals("session_end", it.eventType) - Assertions.assertEquals(1050L, it.timestamp) - } - tracks[4].let { - Assertions.assertEquals("session_start", it.eventType) - Assertions.assertEquals(1200L, it.timestamp) - } - tracks[5].let { - Assertions.assertEquals("test event 3", it.eventType) - Assertions.assertEquals(1200L, it.timestamp) - } - tracks[6].let { - Assertions.assertEquals("session_end", it.eventType) - Assertions.assertEquals(1200L, it.timestamp) - } - tracks[7].let { - Assertions.assertEquals("session_start", it.eventType) - Assertions.assertEquals(1350L, it.timestamp) - } - tracks[8].let { - Assertions.assertEquals("test event 4", it.eventType) - Assertions.assertEquals(1350L, it.timestamp) - } + val event5 = BaseEvent() + event5.eventType = "test event 5" + event5.timestamp = 1700 + amplitude.track(event5) + + amplitude.onExitForeground() + + val event6 = BaseEvent() + event6.eventType = "test event 6" + event6.timestamp = 1750 + amplitude.track(event6) + + val event7 = BaseEvent() + event7.eventType = "test event 7" + event7.timestamp = 2000 + amplitude.track(event7) + + amplitude.onEnterForeground(2050) + + val event8 = BaseEvent() + event8.eventType = "test event 8" + event8.timestamp = 2200 + amplitude.track(event8) + + advanceUntilIdle() + + val tracks = mutableListOf() + + verify { + mockedPlugin.track(capture(tracks)) + } + + tracks.sortBy { event -> event.eventId } + + Assertions.assertEquals(17, tracks.count()) + + tracks[0].let { + Assertions.assertEquals("session_start", it.eventType) + Assertions.assertEquals(1000L, it.timestamp) + Assertions.assertEquals(1000L, it.sessionId) + } + tracks[1].let { + Assertions.assertEquals("test event 1", it.eventType) + Assertions.assertEquals(1000L, it.timestamp) + Assertions.assertEquals(1000L, it.sessionId) + } + tracks[2].let { + Assertions.assertEquals("test event 2", it.eventType) + Assertions.assertEquals(1050L, it.timestamp) + Assertions.assertEquals(1000L, it.sessionId) + } + tracks[3].let { + Assertions.assertEquals("session_end", it.eventType) + Assertions.assertEquals(1050L, it.timestamp) + Assertions.assertEquals(1000L, it.sessionId) + } + + tracks[4].let { + Assertions.assertEquals("session_start", it.eventType) + Assertions.assertEquals(1200L, it.timestamp) + Assertions.assertEquals(1200L, it.sessionId) + } + tracks[5].let { + Assertions.assertEquals("test event 3", it.eventType) + Assertions.assertEquals(1200L, it.timestamp) + Assertions.assertEquals(1200L, it.sessionId) + } + tracks[6].let { + Assertions.assertEquals("session_end", it.eventType) + Assertions.assertEquals(1200L, it.timestamp) + Assertions.assertEquals(1200L, it.sessionId) + } + + tracks[7].let { + Assertions.assertEquals("session_start", it.eventType) + Assertions.assertEquals(1350L, it.timestamp) + Assertions.assertEquals(1350L, it.sessionId) + } + tracks[8].let { + Assertions.assertEquals("test event 4", it.eventType) + Assertions.assertEquals(1350L, it.timestamp) + Assertions.assertEquals(1350L, it.sessionId) + } + tracks[9].let { + Assertions.assertEquals("session_end", it.eventType) + Assertions.assertEquals(1350L, it.timestamp) + Assertions.assertEquals(1350L, it.sessionId) + } + + tracks[10].let { + Assertions.assertEquals("session_start", it.eventType) + Assertions.assertEquals(1500L, it.timestamp) + Assertions.assertEquals(1500L, it.sessionId) } + tracks[11].let { + Assertions.assertEquals("test event 5", it.eventType) + Assertions.assertEquals(1700L, it.timestamp) + Assertions.assertEquals(1500L, it.sessionId) + } + tracks[12].let { + Assertions.assertEquals("test event 6", it.eventType) + Assertions.assertEquals(1750L, it.timestamp) + Assertions.assertEquals(1500L, it.sessionId) + } + tracks[13].let { + Assertions.assertEquals("session_end", it.eventType) + Assertions.assertEquals(1750L, it.timestamp) + Assertions.assertEquals(1500L, it.sessionId) + } + + tracks[14].let { + Assertions.assertEquals("session_start", it.eventType) + Assertions.assertEquals(2000L, it.timestamp) + Assertions.assertEquals(2000L, it.sessionId) + } + tracks[15].let { + Assertions.assertEquals("test event 7", it.eventType) + Assertions.assertEquals(2000L, it.timestamp) + Assertions.assertEquals(2000L, it.sessionId) + } + tracks[16].let { + Assertions.assertEquals("test event 8", it.eventType) + Assertions.assertEquals(2200L, it.timestamp) + Assertions.assertEquals(2000L, it.sessionId) + } + } + + @Test + fun amplitude_session_restore() = runTest { + setDispatcher(testScheduler) + + val storage = InMemoryStorage(amplitude!!) + + val amplitude1 = Amplitude(createConfiguration(100, InstanceStorageProvider(storage))) + amplitude1.isBuilt.await() + + amplitude1.onEnterForeground(1000) + + Assertions.assertEquals(1000L, amplitude1.sessionId) + Assertions.assertEquals(1L, amplitude1.lastEventId) + Assertions.assertEquals(1000L, amplitude1.lastEventTime) + + val event1 = BaseEvent() + event1.eventType = "test event 1" + event1.timestamp = 1200 + amplitude1.track(event1) + + Assertions.assertEquals(1000L, amplitude1.sessionId) + Assertions.assertEquals(2L, amplitude1.lastEventId) + Assertions.assertEquals(1200L, amplitude1.lastEventTime) + + advanceUntilIdle() + + val amplitude2 = Amplitude(createConfiguration(100, InstanceStorageProvider(storage))) + amplitude2.isBuilt.await() + + Assertions.assertEquals(1000L, amplitude2.sessionId) + Assertions.assertEquals(2L, amplitude2.lastEventId) + Assertions.assertEquals(1200L, amplitude2.lastEventTime) + + advanceUntilIdle() + + val amplitude3 = Amplitude(createConfiguration(100, InstanceStorageProvider(storage))) + amplitude3.isBuilt.await() + + Assertions.assertEquals(1000L, amplitude3.sessionId) + Assertions.assertEquals(2L, amplitude3.lastEventId) + Assertions.assertEquals(1200L, amplitude3.lastEventTime) + + amplitude3.onEnterForeground(1400) + + Assertions.assertEquals(1400L, amplitude3.sessionId) + Assertions.assertEquals(4L, amplitude3.lastEventId) + Assertions.assertEquals(1400L, amplitude3.lastEventTime) + } +} + +class InstanceStorageProvider(private val instance: Storage) : StorageProvider { + override fun getStorage(amplitude: com.amplitude.core.Amplitude): Storage { + return instance } } diff --git a/core/src/main/java/com/amplitude/core/Amplitude.kt b/core/src/main/java/com/amplitude/core/Amplitude.kt index 238036cc..ca042825 100644 --- a/core/src/main/java/com/amplitude/core/Amplitude.kt +++ b/core/src/main/java/com/amplitude/core/Amplitude.kt @@ -321,8 +321,8 @@ open class Amplitude internal constructor( return } - val sessionEvents = processEvent(event) - sessionEvents ?. let { + val beforeEvents = processEvent(event) + beforeEvents ?. let { it.forEach { e -> amplitudeScope.launch(amplitudeDispatcher) { isBuilt.await() diff --git a/core/src/main/java/com/amplitude/core/utilities/InMemoryStorage.kt b/core/src/main/java/com/amplitude/core/utilities/InMemoryStorage.kt index 999c671b..a91a3487 100644 --- a/core/src/main/java/com/amplitude/core/utilities/InMemoryStorage.kt +++ b/core/src/main/java/com/amplitude/core/utilities/InMemoryStorage.kt @@ -32,10 +32,7 @@ class InMemoryStorage( } override fun read(key: Storage.Constants): String? { - if (!valuesMap.contains(key.rawVal)) { - return null - } - return valuesMap.get(key.rawVal) + return valuesMap[key.rawVal] } override fun readEventsContent(): List {