From 4ae5928a619d4bdc49c09c0fe64d1c678d5985c4 Mon Sep 17 00:00:00 2001 From: Abhishek Pandey <64667840+1abhishekpandey@users.noreply.github.com> Date: Thu, 30 Jan 2025 22:41:18 +0530 Subject: [PATCH 01/11] feat: provide getter and setter for the persisted values SDK persists `userId`, `anonymousId` and `traits`. In future, we are not going to persist the `externalId`. Therefore, I omitted that. --- .../rudderstack/sdk/kotlin/core/Analytics.kt | 30 +------- .../models/useridentity/UserIdentity.kt | 73 +++++++++++++++++++ .../kotlin/core/internals/queue/EventQueue.kt | 3 +- 3 files changed, 76 insertions(+), 30 deletions(-) diff --git a/core/src/main/kotlin/com/rudderstack/sdk/kotlin/core/Analytics.kt b/core/src/main/kotlin/com/rudderstack/sdk/kotlin/core/Analytics.kt index 295bc4bde..7efafe533 100644 --- a/core/src/main/kotlin/com/rudderstack/sdk/kotlin/core/Analytics.kt +++ b/core/src/main/kotlin/com/rudderstack/sdk/kotlin/core/Analytics.kt @@ -16,7 +16,6 @@ import com.rudderstack.sdk.kotlin.core.internals.models.TrackEvent import com.rudderstack.sdk.kotlin.core.internals.models.connectivity.ConnectivityState import com.rudderstack.sdk.kotlin.core.internals.models.emptyJsonObject import com.rudderstack.sdk.kotlin.core.internals.models.useridentity.ResetUserIdentityAction -import com.rudderstack.sdk.kotlin.core.internals.models.useridentity.SetAnonymousIdAction import com.rudderstack.sdk.kotlin.core.internals.models.useridentity.SetUserIdForAliasEvent import com.rudderstack.sdk.kotlin.core.internals.models.useridentity.SetUserIdTraitsAndExternalIdsAction import com.rudderstack.sdk.kotlin.core.internals.models.useridentity.UserIdentity @@ -342,33 +341,6 @@ open class Analytics protected constructor( this.pluginChain.add(plugin) } - /** - * Sets or updates the anonymous ID for the current user identity. - * - * The `setAnonymousId` method is used to update the `anonymousID` value within the `UserIdentityStore`. - * This ID is typically generated automatically to track users who have not yet been identified - * (e.g., before they log in or sign up). This function dispatches an action to modify the `UserIdentityState`, - * ensuring that the new ID is correctly stored and managed. - * - * @param anonymousId The new anonymous ID to be set for the current user. This ID should be a unique, - * non-null string used to represent the user anonymously. - */ - fun setAnonymousId(anonymousId: String) { - if (!isAnalyticsActive()) return - - userIdentityState.dispatch(SetAnonymousIdAction(anonymousId)) - storeAnonymousId() - } - - /** - * The `getAnonymousId` method always retrieves the current anonymous ID. - */ - fun getAnonymousId(): String? { - if (!isAnalyticsActive()) return null - - return userIdentityState.value.anonymousId - } - /** * Resets the user identity, clearing the user ID, traits, and external IDs. * If clearAnonymousId is true, clears the existing anonymous ID and generate a new one. @@ -404,7 +376,7 @@ open class Analytics protected constructor( } } - private fun storeAnonymousId() { + internal fun storeAnonymousId() { analyticsScope.launch(storageDispatcher) { userIdentityState.value.storeAnonymousId(storage = storage) } diff --git a/core/src/main/kotlin/com/rudderstack/sdk/kotlin/core/internals/models/useridentity/UserIdentity.kt b/core/src/main/kotlin/com/rudderstack/sdk/kotlin/core/internals/models/useridentity/UserIdentity.kt index f420fca44..e7ef7ca6b 100644 --- a/core/src/main/kotlin/com/rudderstack/sdk/kotlin/core/internals/models/useridentity/UserIdentity.kt +++ b/core/src/main/kotlin/com/rudderstack/sdk/kotlin/core/internals/models/useridentity/UserIdentity.kt @@ -1,5 +1,6 @@ package com.rudderstack.sdk.kotlin.core.internals.models.useridentity +import com.rudderstack.sdk.kotlin.core.Analytics import com.rudderstack.sdk.kotlin.core.internals.models.ExternalId import com.rudderstack.sdk.kotlin.core.internals.models.RudderTraits import com.rudderstack.sdk.kotlin.core.internals.models.emptyJsonObject @@ -8,6 +9,7 @@ import com.rudderstack.sdk.kotlin.core.internals.storage.Storage import com.rudderstack.sdk.kotlin.core.internals.storage.StorageKeys import com.rudderstack.sdk.kotlin.core.internals.utils.empty import com.rudderstack.sdk.kotlin.core.internals.utils.generateUUID +import com.rudderstack.sdk.kotlin.core.internals.utils.isAnalyticsActive import com.rudderstack.sdk.kotlin.core.internals.utils.readValuesOrDefault /** @@ -50,3 +52,74 @@ data class UserIdentity( internal sealed interface UserIdentityAction : FlowAction } + +/** + * Update or get the stored anonymous ID. + * + * The `analyticsInstance.anonymousId` is used to update and get the `anonymousID` value. + * This ID is typically generated automatically to track users who have not yet been identified + * (e.g., before they log in or sign up). + * + * This can return null if the analytics is shut down. + * + * Set the anonymousId: + * ```kotlin + * analyticsInstance.anonymousId = "Custom Anonymous ID" + * ``` + * + * Get the anonymousId: + * ```kotlin + * val anonymousId = analyticsInstance.anonymousId + * ``` + */ +var Analytics.anonymousId: String? + get() { + if (!isAnalyticsActive()) return null + return userIdentityState.value.anonymousId + } + set(value) { + if (!isAnalyticsActive()) return + + value?.let { anonymousId -> + userIdentityState.dispatch(SetAnonymousIdAction(anonymousId)) + storeAnonymousId() + } + } + +/** + * Get the user ID. + * + * The `analyticsInstance.userId` is used to get the `userId` value. + * This ID is assigned when an identify event is made. + * + * This can return null if the analytics is shut down. + * + * Get the userId: + * ```kotlin + * val userId = analyticsInstance.userId + * ``` + */ +val Analytics.userId: String? + get() { + if (!isAnalyticsActive()) return null + return userIdentityState.value.userId + } + +/** + * Get the user traits. + * + * The `analyticsInstance.traits` is used to get the `traits` value. + * This traits is assigned when an identify event is made. + * + * This can return null if the analytics is shut down. + * + * Get the traits: + * ```kotlin + * val traits = analyticsInstance.traits + * ``` + */ +val Analytics.traits: RudderTraits? + get() { + if (!isAnalyticsActive()) return null + return userIdentityState.value.traits + } diff --git a/core/src/main/kotlin/com/rudderstack/sdk/kotlin/core/internals/queue/EventQueue.kt b/core/src/main/kotlin/com/rudderstack/sdk/kotlin/core/internals/queue/EventQueue.kt index 96571aa0b..0cb79a45f 100644 --- a/core/src/main/kotlin/com/rudderstack/sdk/kotlin/core/internals/queue/EventQueue.kt +++ b/core/src/main/kotlin/com/rudderstack/sdk/kotlin/core/internals/queue/EventQueue.kt @@ -3,6 +3,7 @@ package com.rudderstack.sdk.kotlin.core.internals.queue import com.rudderstack.sdk.kotlin.core.Analytics import com.rudderstack.sdk.kotlin.core.internals.logger.LoggerAnalytics import com.rudderstack.sdk.kotlin.core.internals.models.Event +import com.rudderstack.sdk.kotlin.core.internals.models.useridentity.anonymousId import com.rudderstack.sdk.kotlin.core.internals.network.HttpClient import com.rudderstack.sdk.kotlin.core.internals.network.HttpClientImpl import com.rudderstack.sdk.kotlin.core.internals.policies.FlushPoliciesFacade @@ -41,7 +42,7 @@ internal class EventQueue( endPoint = BATCH_ENDPOINT, authHeaderString = writeKey.encodeToBase64(), isGZIPEnabled = gzipEnabled, - anonymousIdHeaderString = analytics.getAnonymousId() ?: String.empty() + anonymousIdHeaderString = analytics.anonymousId ?: String.empty() ) } ) { From 5700d6f92beacec9c5298e75f887b53c82f72d7f Mon Sep 17 00:00:00 2001 From: Abhishek Pandey <64667840+1abhishekpandey@users.noreply.github.com> Date: Thu, 30 Jan 2025 22:43:11 +0530 Subject: [PATCH 02/11] chore: add buttons to get the persisted values --- .../sampleapp/mainview/MainActivity.kt | 11 +++++++++ .../sampleapp/mainview/MainViewModel.kt | 23 +++++++++++++++++++ .../sampleapp/mainview/MainViewModelState.kt | 4 ++++ 3 files changed, 38 insertions(+) diff --git a/app/src/main/java/com/rudderstack/sampleapp/mainview/MainActivity.kt b/app/src/main/java/com/rudderstack/sampleapp/mainview/MainActivity.kt index bce9fec94..5b4e3635f 100644 --- a/app/src/main/java/com/rudderstack/sampleapp/mainview/MainActivity.kt +++ b/app/src/main/java/com/rudderstack/sampleapp/mainview/MainActivity.kt @@ -146,6 +146,17 @@ class MainActivity : ComponentActivity() { weight = .5f, viewModel = viewModel ) + Spacer(modifier = Modifier.height(2.dp)) + CreateRowOfApis( + names = arrayOf( + AnalyticsState.SetAnonymousId, + AnalyticsState.GetAnonymousId, + AnalyticsState.GetUserId, + AnalyticsState.GetTraits, + ), + weight = .5f, + viewModel = viewModel + ) CreateLogcat(state.logDataList) } } diff --git a/app/src/main/java/com/rudderstack/sampleapp/mainview/MainViewModel.kt b/app/src/main/java/com/rudderstack/sampleapp/mainview/MainViewModel.kt index fd39a3586..957bacb8e 100644 --- a/app/src/main/java/com/rudderstack/sampleapp/mainview/MainViewModel.kt +++ b/app/src/main/java/com/rudderstack/sampleapp/mainview/MainViewModel.kt @@ -6,6 +6,9 @@ import com.rudderstack.sdk.kotlin.core.internals.models.ExternalId import com.rudderstack.sdk.kotlin.core.internals.models.Properties import com.rudderstack.sdk.kotlin.core.internals.models.RudderOption import com.rudderstack.sampleapp.analytics.RudderAnalyticsUtils +import com.rudderstack.sdk.kotlin.core.internals.models.useridentity.anonymousId +import com.rudderstack.sdk.kotlin.core.internals.models.useridentity.traits +import com.rudderstack.sdk.kotlin.core.internals.models.useridentity.userId import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update @@ -118,6 +121,26 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { RudderAnalyticsUtils.initialize(getApplication()) "SDK initialized" } + + AnalyticsState.SetAnonymousId -> { + RudderAnalyticsUtils.analytics.anonymousId = "Custom Anonymous ID" + "Anonymous ID is set as: ${RudderAnalyticsUtils.analytics.anonymousId}" + } + + AnalyticsState.GetAnonymousId -> { + val anonymousId = RudderAnalyticsUtils.analytics.anonymousId + "Anonymous ID: $anonymousId" + } + + AnalyticsState.GetUserId -> { + val userId = RudderAnalyticsUtils.analytics.userId + "User ID: $userId" + } + + AnalyticsState.GetTraits -> { + val traits = RudderAnalyticsUtils.analytics.traits + "Traits: $traits" + } } if (log.isNotEmpty()) addLogData(LogData(Date(), log)) } diff --git a/app/src/main/java/com/rudderstack/sampleapp/mainview/MainViewModelState.kt b/app/src/main/java/com/rudderstack/sampleapp/mainview/MainViewModelState.kt index 0c144e821..bee324940 100644 --- a/app/src/main/java/com/rudderstack/sampleapp/mainview/MainViewModelState.kt +++ b/app/src/main/java/com/rudderstack/sampleapp/mainview/MainViewModelState.kt @@ -22,4 +22,8 @@ sealed class AnalyticsState(val eventName: String) { object StartSession: AnalyticsState("Start Session") object StartSessionWithCustomId: AnalyticsState("Start Session with custom id") object EndSession: AnalyticsState("End Session") + object SetAnonymousId: AnalyticsState("Set Anonymous Id") + object GetAnonymousId: AnalyticsState("Get Anonymous Id") + object GetUserId: AnalyticsState("Get User Id") + object GetTraits: AnalyticsState("Get Traits") } From a8b335e107ba76210d99e9048d2e71751d8d5bd8 Mon Sep 17 00:00:00 2001 From: Abhishek Pandey <64667840+1abhishekpandey@users.noreply.github.com> Date: Fri, 31 Jan 2025 14:34:07 +0530 Subject: [PATCH 03/11] refactor(test): mock getting of anonymousId value --- .../sdk/kotlin/core/plugins/RudderStackDataplanePluginTest.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/src/test/kotlin/com/rudderstack/sdk/kotlin/core/plugins/RudderStackDataplanePluginTest.kt b/core/src/test/kotlin/com/rudderstack/sdk/kotlin/core/plugins/RudderStackDataplanePluginTest.kt index 18555358c..57a489973 100644 --- a/core/src/test/kotlin/com/rudderstack/sdk/kotlin/core/plugins/RudderStackDataplanePluginTest.kt +++ b/core/src/test/kotlin/com/rudderstack/sdk/kotlin/core/plugins/RudderStackDataplanePluginTest.kt @@ -5,6 +5,7 @@ import com.rudderstack.sdk.kotlin.core.internals.models.TrackEvent import com.rudderstack.sdk.kotlin.core.internals.queue.EventQueue import com.rudderstack.sdk.kotlin.core.mockAnalytics import io.mockk.coVerify +import io.mockk.every import io.mockk.mockk import io.mockk.spyk import io.mockk.unmockkAll @@ -22,6 +23,7 @@ import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test +private const val ANONYMOUS_ID = "anonymousId" @OptIn(ExperimentalCoroutinesApi::class) class RudderStackDataplanePluginTest { private val testDispatcher = StandardTestDispatcher() @@ -36,6 +38,7 @@ class RudderStackDataplanePluginTest { Dispatchers.setMain(testDispatcher) mockAnalytics = mockAnalytics(testScope, testDispatcher) mockEventQueue = mockk(relaxed = true) + every { mockAnalytics.userIdentityState.value.anonymousId } returns ANONYMOUS_ID plugin = spyk(RudderStackDataplanePlugin()) From 30eb01787c6c415b6c15ea3782aadb3cdeff0816 Mon Sep 17 00:00:00 2001 From: Abhishek Pandey <64667840+1abhishekpandey@users.noreply.github.com> Date: Fri, 31 Jan 2025 16:52:47 +0530 Subject: [PATCH 04/11] test: add test cases for anonymousId, userId and traits --- .../sdk/kotlin/core/AnalyticsTest.kt | 121 ++++++++++++++++++ .../core/internals/utils/MockMemoryStorage.kt | 86 +++++++++++++ 2 files changed, 207 insertions(+) create mode 100644 core/src/test/kotlin/com/rudderstack/sdk/kotlin/core/AnalyticsTest.kt create mode 100644 core/src/test/kotlin/com/rudderstack/sdk/kotlin/core/internals/utils/MockMemoryStorage.kt diff --git a/core/src/test/kotlin/com/rudderstack/sdk/kotlin/core/AnalyticsTest.kt b/core/src/test/kotlin/com/rudderstack/sdk/kotlin/core/AnalyticsTest.kt new file mode 100644 index 000000000..13bee215e --- /dev/null +++ b/core/src/test/kotlin/com/rudderstack/sdk/kotlin/core/AnalyticsTest.kt @@ -0,0 +1,121 @@ +package com.rudderstack.sdk.kotlin.core + +import com.rudderstack.sdk.kotlin.core.internals.models.emptyJsonObject +import com.rudderstack.sdk.kotlin.core.internals.models.useridentity.anonymousId +import com.rudderstack.sdk.kotlin.core.internals.models.useridentity.traits +import com.rudderstack.sdk.kotlin.core.internals.models.useridentity.userId +import com.rudderstack.sdk.kotlin.core.internals.storage.Storage +import com.rudderstack.sdk.kotlin.core.internals.storage.provideBasicStorage +import com.rudderstack.sdk.kotlin.core.internals.utils.MockMemoryStorage +import com.rudderstack.sdk.kotlin.core.internals.utils.empty +import io.mockk.every +import io.mockk.mockkStatic +import junit.framework.TestCase.assertNull +import junit.framework.TestCase.assertTrue +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put +import org.junit.After +import org.junit.Before +import org.junit.Test + +private const val CUSTOM_ANONYMOUS_ID = "custom-anonymous-id" +private const val USER_ID = "user-id" +private val TRAITS: JsonObject = buildJsonObject { put("key-1", "value-1") } + +class AnalyticsTest { + + private val configuration = provideConfiguration() + + private lateinit var analytics: Analytics + private lateinit var mockStorage: Storage + + @Before + fun setup() { + mockStorage = MockMemoryStorage() + + mockkStatic(::provideBasicStorage) + every { provideBasicStorage(any()) } returns mockStorage + + analytics = Analytics(configuration = configuration) + } + + @After + fun tearDown() { + mockStorage.close() + } + + @Test + fun `when anonymousId is fetched, then it should return UUID as the anonymousId`() { + val anonymousId = analytics.anonymousId + + // This pattern ensures the string follows the UUID v4 format. + val uuidRegex = Regex("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$") + assertTrue(anonymousId?.matches(uuidRegex) == true) + } + + @Test + fun `given custom anonymousId is set, when anonymousId is fetched, then it should return the custom anonymousId`() { + analytics.anonymousId = CUSTOM_ANONYMOUS_ID + + val anonymousId = analytics.anonymousId + + assertTrue(anonymousId == CUSTOM_ANONYMOUS_ID) + } + + @Test + fun `given empty string is set as custom anonymousId, when anonymousId is fetched, then it should return the empty anonymousId`() { + analytics.anonymousId = String.empty() + + val anonymousId = analytics.anonymousId + + assertTrue(anonymousId == String.empty()) + } + + @Test + fun `given sdk is shutdown, when anonymousId is fetched, then it should return null`() { + analytics.shutdown() + + val anonymousId = analytics.anonymousId + + assertNull(anonymousId) + } + + @Test + fun `given userId and traits are set, when they are fetched, then proper values are returned`() { + analytics.identify(userId = USER_ID, traits = TRAITS) + + val userId = analytics.userId + val traits = analytics.traits + + assertTrue(userId == USER_ID) + assertTrue(traits == TRAITS) + } + + @Test + fun `given userId and traits are not set, when they are fetched, then empty values are returned`() { + val userId = analytics.userId + val traits = analytics.traits + + assertTrue(userId == String.empty()) + assertTrue(traits == emptyJsonObject) + } + + @Test + fun `given userId and traits are not set and sdk is shutdown, when they are fetched, then it should return null`() { + analytics.identify(userId = USER_ID, traits = TRAITS) + analytics.shutdown() + + val userId = analytics.userId + val traits = analytics.traits + + assertNull(userId) + assertNull(traits) + } +} + +private fun provideConfiguration() = + Configuration( + writeKey = "", + dataPlaneUrl = "https://hosted.rudderlabs.com", + ) diff --git a/core/src/test/kotlin/com/rudderstack/sdk/kotlin/core/internals/utils/MockMemoryStorage.kt b/core/src/test/kotlin/com/rudderstack/sdk/kotlin/core/internals/utils/MockMemoryStorage.kt new file mode 100644 index 000000000..b3be1e595 --- /dev/null +++ b/core/src/test/kotlin/com/rudderstack/sdk/kotlin/core/internals/utils/MockMemoryStorage.kt @@ -0,0 +1,86 @@ +package com.rudderstack.sdk.kotlin.core.internals.utils + +import com.rudderstack.sdk.kotlin.core.internals.storage.LibraryVersion +import com.rudderstack.sdk.kotlin.core.internals.storage.Storage +import com.rudderstack.sdk.kotlin.core.internals.storage.StorageKeys + +internal class MockMemoryStorage : Storage { + + private val propertiesMap: MutableMap = mutableMapOf() + private val messageBatchMap: MutableList = mutableListOf() + + override suspend fun write(key: StorageKeys, value: Boolean) { + if (key != StorageKeys.EVENT) { + propertiesMap[key.key] = value + } + } + + override suspend fun write(key: StorageKeys, value: Int) { + if (key != StorageKeys.EVENT) { + propertiesMap[key.key] = value + } + } + + override suspend fun write(key: StorageKeys, value: Long) { + if (key != StorageKeys.EVENT) { + propertiesMap[key.key] = value + } + } + + override suspend fun write(key: StorageKeys, value: String) { + if (key == StorageKeys.EVENT) { + messageBatchMap.add(value) + } else { + propertiesMap[key.key] = value + } + } + + override suspend fun remove(key: StorageKeys) { + propertiesMap.remove(key.key) + } + + override fun remove(filePath: String) { + messageBatchMap.remove(filePath) + } + + override suspend fun rollover() { + messageBatchMap.clear() + } + + override fun close() { + messageBatchMap.clear() + propertiesMap.clear() + } + + override fun readInt(key: StorageKeys, defaultVal: Int): Int { + return (propertiesMap[key.key] as? Int) ?: defaultVal + } + + override fun readBoolean(key: StorageKeys, defaultVal: Boolean): Boolean { + return (propertiesMap[key.key] as? Boolean) ?: defaultVal + } + + override fun readLong(key: StorageKeys, defaultVal: Long): Long { + return (propertiesMap[key.key] as? Long) ?: defaultVal + } + + override fun readString(key: StorageKeys, defaultVal: String): String { + return if (key == StorageKeys.EVENT) { + messageBatchMap.joinToString() + } else { + (propertiesMap[key.key] as? String) ?: defaultVal + } + } + + override fun readFileList(): List { + return messageBatchMap + } + + override fun getLibraryVersion(): LibraryVersion { + return object : LibraryVersion { + override fun getPackageName(): String = "com.rudderstack.sdk.kotlin.core" + + override fun getVersionName() = "1.0.0" + } + } +} From 7f9241a5bb2566db1142c9b3aca46737295c1ff4 Mon Sep 17 00:00:00 2001 From: Abhishek Pandey <64667840+1abhishekpandey@users.noreply.github.com> Date: Fri, 31 Jan 2025 16:53:35 +0530 Subject: [PATCH 05/11] test(android): clear propertiesMap in mock storage Previously, only messageBatchMap was getting cleared. --- .../rudderstack/sdk/kotlin/android/utils/MockMemoryStorage.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/android/src/test/kotlin/com/rudderstack/sdk/kotlin/android/utils/MockMemoryStorage.kt b/android/src/test/kotlin/com/rudderstack/sdk/kotlin/android/utils/MockMemoryStorage.kt index 3cf4dddf1..d63c66856 100644 --- a/android/src/test/kotlin/com/rudderstack/sdk/kotlin/android/utils/MockMemoryStorage.kt +++ b/android/src/test/kotlin/com/rudderstack/sdk/kotlin/android/utils/MockMemoryStorage.kt @@ -49,6 +49,7 @@ internal class MockMemoryStorage : Storage { override fun close() { messageBatchMap.clear() + propertiesMap.clear() } override fun readInt(key: StorageKeys, defaultVal: Int): Int { From dea3dd98afc7a5b398da698fb9b7cbd93efe9bfb Mon Sep 17 00:00:00 2001 From: Abhishek Pandey <64667840+1abhishekpandey@users.noreply.github.com> Date: Fri, 31 Jan 2025 18:45:05 +0530 Subject: [PATCH 06/11] chore(test): use placeholder for dataPlaneUrl --- .../kotlin/com/rudderstack/sdk/kotlin/core/AnalyticsTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/test/kotlin/com/rudderstack/sdk/kotlin/core/AnalyticsTest.kt b/core/src/test/kotlin/com/rudderstack/sdk/kotlin/core/AnalyticsTest.kt index 13bee215e..a3f2fb9d2 100644 --- a/core/src/test/kotlin/com/rudderstack/sdk/kotlin/core/AnalyticsTest.kt +++ b/core/src/test/kotlin/com/rudderstack/sdk/kotlin/core/AnalyticsTest.kt @@ -117,5 +117,5 @@ class AnalyticsTest { private fun provideConfiguration() = Configuration( writeKey = "", - dataPlaneUrl = "https://hosted.rudderlabs.com", + dataPlaneUrl = "", ) From 751fef3449bcfbd40a812eff7f6969d38fd8ee66 Mon Sep 17 00:00:00 2001 From: Abhishek Pandey <64667840+1abhishekpandey@users.noreply.github.com> Date: Fri, 31 Jan 2025 18:49:19 +0530 Subject: [PATCH 07/11] chore: improve indentation --- .../sdk/kotlin/core/plugins/RudderStackDataplanePluginTest.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/src/test/kotlin/com/rudderstack/sdk/kotlin/core/plugins/RudderStackDataplanePluginTest.kt b/core/src/test/kotlin/com/rudderstack/sdk/kotlin/core/plugins/RudderStackDataplanePluginTest.kt index 57a489973..096dee2f6 100644 --- a/core/src/test/kotlin/com/rudderstack/sdk/kotlin/core/plugins/RudderStackDataplanePluginTest.kt +++ b/core/src/test/kotlin/com/rudderstack/sdk/kotlin/core/plugins/RudderStackDataplanePluginTest.kt @@ -24,8 +24,10 @@ import org.junit.Before import org.junit.Test private const val ANONYMOUS_ID = "anonymousId" + @OptIn(ExperimentalCoroutinesApi::class) class RudderStackDataplanePluginTest { + private val testDispatcher = StandardTestDispatcher() private val testScope = TestScope(testDispatcher) From ca71544b14aa6259664e9b16c6c04537ec8d7bcf Mon Sep 17 00:00:00 2001 From: Abhishek Pandey <64667840+1abhishekpandey@users.noreply.github.com> Date: Fri, 31 Jan 2025 18:53:22 +0530 Subject: [PATCH 08/11] chore: remove unnecessary code --- .../kotlin/com/rudderstack/sdk/kotlin/core/AnalyticsTest.kt | 6 ------ 1 file changed, 6 deletions(-) diff --git a/core/src/test/kotlin/com/rudderstack/sdk/kotlin/core/AnalyticsTest.kt b/core/src/test/kotlin/com/rudderstack/sdk/kotlin/core/AnalyticsTest.kt index a3f2fb9d2..58fa8c260 100644 --- a/core/src/test/kotlin/com/rudderstack/sdk/kotlin/core/AnalyticsTest.kt +++ b/core/src/test/kotlin/com/rudderstack/sdk/kotlin/core/AnalyticsTest.kt @@ -15,7 +15,6 @@ import junit.framework.TestCase.assertTrue import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.put -import org.junit.After import org.junit.Before import org.junit.Test @@ -40,11 +39,6 @@ class AnalyticsTest { analytics = Analytics(configuration = configuration) } - @After - fun tearDown() { - mockStorage.close() - } - @Test fun `when anonymousId is fetched, then it should return UUID as the anonymousId`() { val anonymousId = analytics.anonymousId From 1ac6a0d4725eed555aba3f2ec23e628001ccbcea Mon Sep 17 00:00:00 2001 From: Abhishek Pandey <64667840+1abhishekpandey@users.noreply.github.com> Date: Fri, 31 Jan 2025 19:19:33 +0530 Subject: [PATCH 09/11] chore: improve tests --- .../com/rudderstack/sdk/kotlin/core/AnalyticsTest.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/src/test/kotlin/com/rudderstack/sdk/kotlin/core/AnalyticsTest.kt b/core/src/test/kotlin/com/rudderstack/sdk/kotlin/core/AnalyticsTest.kt index 58fa8c260..fab2be6a6 100644 --- a/core/src/test/kotlin/com/rudderstack/sdk/kotlin/core/AnalyticsTest.kt +++ b/core/src/test/kotlin/com/rudderstack/sdk/kotlin/core/AnalyticsTest.kt @@ -58,7 +58,7 @@ class AnalyticsTest { } @Test - fun `given empty string is set as custom anonymousId, when anonymousId is fetched, then it should return the empty anonymousId`() { + fun `given empty string is set as anonymousId, when anonymousId is fetched, then it should return the empty value`() { analytics.anonymousId = String.empty() val anonymousId = analytics.anonymousId @@ -76,7 +76,8 @@ class AnalyticsTest { } @Test - fun `given userId and traits are set, when they are fetched, then proper values are returned`() { + fun `given userId and traits are set, when they are fetched, then the set values are returned`() { + // userId and traits can be set only through identify api analytics.identify(userId = USER_ID, traits = TRAITS) val userId = analytics.userId @@ -96,8 +97,7 @@ class AnalyticsTest { } @Test - fun `given userId and traits are not set and sdk is shutdown, when they are fetched, then it should return null`() { - analytics.identify(userId = USER_ID, traits = TRAITS) + fun `given sdk is shutdown, when userId and traits are fetched, then it should return null`() { analytics.shutdown() val userId = analytics.userId From 7c87e4cb6af8bd422a5ad4e924ba05b6d861e1a2 Mon Sep 17 00:00:00 2001 From: Abhishek Pandey <64667840+1abhishekpandey@users.noreply.github.com> Date: Tue, 4 Feb 2025 16:36:58 +0530 Subject: [PATCH 10/11] refactor: move corresponding persisted getter and setter to the core Analytics class This is to make it Java interoperable. --- .../sampleapp/mainview/MainViewModel.kt | 3 - .../rudderstack/sdk/kotlin/core/Analytics.kt | 78 ++++++++++++++++++- .../models/useridentity/UserIdentity.kt | 73 ----------------- .../kotlin/core/internals/queue/EventQueue.kt | 1 - .../sdk/kotlin/core/AnalyticsTest.kt | 3 - 5 files changed, 75 insertions(+), 83 deletions(-) diff --git a/app/src/main/java/com/rudderstack/sampleapp/mainview/MainViewModel.kt b/app/src/main/java/com/rudderstack/sampleapp/mainview/MainViewModel.kt index 957bacb8e..1b6cc5f0e 100644 --- a/app/src/main/java/com/rudderstack/sampleapp/mainview/MainViewModel.kt +++ b/app/src/main/java/com/rudderstack/sampleapp/mainview/MainViewModel.kt @@ -6,9 +6,6 @@ import com.rudderstack.sdk.kotlin.core.internals.models.ExternalId import com.rudderstack.sdk.kotlin.core.internals.models.Properties import com.rudderstack.sdk.kotlin.core.internals.models.RudderOption import com.rudderstack.sampleapp.analytics.RudderAnalyticsUtils -import com.rudderstack.sdk.kotlin.core.internals.models.useridentity.anonymousId -import com.rudderstack.sdk.kotlin.core.internals.models.useridentity.traits -import com.rudderstack.sdk.kotlin.core.internals.models.useridentity.userId import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update diff --git a/core/src/main/kotlin/com/rudderstack/sdk/kotlin/core/Analytics.kt b/core/src/main/kotlin/com/rudderstack/sdk/kotlin/core/Analytics.kt index 7efafe533..aaefdb8f2 100644 --- a/core/src/main/kotlin/com/rudderstack/sdk/kotlin/core/Analytics.kt +++ b/core/src/main/kotlin/com/rudderstack/sdk/kotlin/core/Analytics.kt @@ -16,6 +16,7 @@ import com.rudderstack.sdk.kotlin.core.internals.models.TrackEvent import com.rudderstack.sdk.kotlin.core.internals.models.connectivity.ConnectivityState import com.rudderstack.sdk.kotlin.core.internals.models.emptyJsonObject import com.rudderstack.sdk.kotlin.core.internals.models.useridentity.ResetUserIdentityAction +import com.rudderstack.sdk.kotlin.core.internals.models.useridentity.SetAnonymousIdAction import com.rudderstack.sdk.kotlin.core.internals.models.useridentity.SetUserIdForAliasEvent import com.rudderstack.sdk.kotlin.core.internals.models.useridentity.SetUserIdTraitsAndExternalIdsAction import com.rudderstack.sdk.kotlin.core.internals.models.useridentity.UserIdentity @@ -376,11 +377,82 @@ open class Analytics protected constructor( } } - internal fun storeAnonymousId() { + override fun getPlatformType(): PlatformType = PlatformType.Server + + /** + * Update or get the stored anonymous ID. + * + * The `analyticsInstance.anonymousId` is used to update and get the `anonymousID` value. + * This ID is typically generated automatically to track users who have not yet been identified + * (e.g., before they log in or sign up). + * + * This can return null if the analytics is shut down. + * + * Set the anonymousId: + * ```kotlin + * analyticsInstance.anonymousId = "Custom Anonymous ID" + * ``` + * + * Get the anonymousId: + * ```kotlin + * val anonymousId = analyticsInstance.anonymousId + * ``` + */ + var anonymousId: String? + get() { + if (!isAnalyticsActive()) return null + return userIdentityState.value.anonymousId + } + set(value) { + if (!isAnalyticsActive()) return + + value?.let { anonymousId -> + userIdentityState.dispatch(SetAnonymousIdAction(anonymousId)) + storeAnonymousId() + } + } + + /** + * Get the user ID. + * + * The `analyticsInstance.userId` is used to get the `userId` value. + * This ID is assigned when an identify event is made. + * + * This can return null if the analytics is shut down. + * + * Get the userId: + * ```kotlin + * val userId = analyticsInstance.userId + * ``` + */ + val userId: String? + get() { + if (!isAnalyticsActive()) return null + return userIdentityState.value.userId + } + + /** + * Get the user traits. + * + * The `analyticsInstance.traits` is used to get the `traits` value. + * This traits is assigned when an identify event is made. + * + * This can return null if the analytics is shut down. + * + * Get the traits: + * ```kotlin + * val traits = analyticsInstance.traits + * ``` + */ + val traits: RudderTraits? + get() { + if (!isAnalyticsActive()) return null + return userIdentityState.value.traits + } + + private fun storeAnonymousId() { analyticsScope.launch(storageDispatcher) { userIdentityState.value.storeAnonymousId(storage = storage) } } - - override fun getPlatformType(): PlatformType = PlatformType.Server } diff --git a/core/src/main/kotlin/com/rudderstack/sdk/kotlin/core/internals/models/useridentity/UserIdentity.kt b/core/src/main/kotlin/com/rudderstack/sdk/kotlin/core/internals/models/useridentity/UserIdentity.kt index e7ef7ca6b..f420fca44 100644 --- a/core/src/main/kotlin/com/rudderstack/sdk/kotlin/core/internals/models/useridentity/UserIdentity.kt +++ b/core/src/main/kotlin/com/rudderstack/sdk/kotlin/core/internals/models/useridentity/UserIdentity.kt @@ -1,6 +1,5 @@ package com.rudderstack.sdk.kotlin.core.internals.models.useridentity -import com.rudderstack.sdk.kotlin.core.Analytics import com.rudderstack.sdk.kotlin.core.internals.models.ExternalId import com.rudderstack.sdk.kotlin.core.internals.models.RudderTraits import com.rudderstack.sdk.kotlin.core.internals.models.emptyJsonObject @@ -9,7 +8,6 @@ import com.rudderstack.sdk.kotlin.core.internals.storage.Storage import com.rudderstack.sdk.kotlin.core.internals.storage.StorageKeys import com.rudderstack.sdk.kotlin.core.internals.utils.empty import com.rudderstack.sdk.kotlin.core.internals.utils.generateUUID -import com.rudderstack.sdk.kotlin.core.internals.utils.isAnalyticsActive import com.rudderstack.sdk.kotlin.core.internals.utils.readValuesOrDefault /** @@ -52,74 +50,3 @@ data class UserIdentity( internal sealed interface UserIdentityAction : FlowAction } - -/** - * Update or get the stored anonymous ID. - * - * The `analyticsInstance.anonymousId` is used to update and get the `anonymousID` value. - * This ID is typically generated automatically to track users who have not yet been identified - * (e.g., before they log in or sign up). - * - * This can return null if the analytics is shut down. - * - * Set the anonymousId: - * ```kotlin - * analyticsInstance.anonymousId = "Custom Anonymous ID" - * ``` - * - * Get the anonymousId: - * ```kotlin - * val anonymousId = analyticsInstance.anonymousId - * ``` - */ -var Analytics.anonymousId: String? - get() { - if (!isAnalyticsActive()) return null - return userIdentityState.value.anonymousId - } - set(value) { - if (!isAnalyticsActive()) return - - value?.let { anonymousId -> - userIdentityState.dispatch(SetAnonymousIdAction(anonymousId)) - storeAnonymousId() - } - } - -/** - * Get the user ID. - * - * The `analyticsInstance.userId` is used to get the `userId` value. - * This ID is assigned when an identify event is made. - * - * This can return null if the analytics is shut down. - * - * Get the userId: - * ```kotlin - * val userId = analyticsInstance.userId - * ``` - */ -val Analytics.userId: String? - get() { - if (!isAnalyticsActive()) return null - return userIdentityState.value.userId - } - -/** - * Get the user traits. - * - * The `analyticsInstance.traits` is used to get the `traits` value. - * This traits is assigned when an identify event is made. - * - * This can return null if the analytics is shut down. - * - * Get the traits: - * ```kotlin - * val traits = analyticsInstance.traits - * ``` - */ -val Analytics.traits: RudderTraits? - get() { - if (!isAnalyticsActive()) return null - return userIdentityState.value.traits - } diff --git a/core/src/main/kotlin/com/rudderstack/sdk/kotlin/core/internals/queue/EventQueue.kt b/core/src/main/kotlin/com/rudderstack/sdk/kotlin/core/internals/queue/EventQueue.kt index 0cb79a45f..5b57a79b2 100644 --- a/core/src/main/kotlin/com/rudderstack/sdk/kotlin/core/internals/queue/EventQueue.kt +++ b/core/src/main/kotlin/com/rudderstack/sdk/kotlin/core/internals/queue/EventQueue.kt @@ -3,7 +3,6 @@ package com.rudderstack.sdk.kotlin.core.internals.queue import com.rudderstack.sdk.kotlin.core.Analytics import com.rudderstack.sdk.kotlin.core.internals.logger.LoggerAnalytics import com.rudderstack.sdk.kotlin.core.internals.models.Event -import com.rudderstack.sdk.kotlin.core.internals.models.useridentity.anonymousId import com.rudderstack.sdk.kotlin.core.internals.network.HttpClient import com.rudderstack.sdk.kotlin.core.internals.network.HttpClientImpl import com.rudderstack.sdk.kotlin.core.internals.policies.FlushPoliciesFacade diff --git a/core/src/test/kotlin/com/rudderstack/sdk/kotlin/core/AnalyticsTest.kt b/core/src/test/kotlin/com/rudderstack/sdk/kotlin/core/AnalyticsTest.kt index fab2be6a6..b915cd0c1 100644 --- a/core/src/test/kotlin/com/rudderstack/sdk/kotlin/core/AnalyticsTest.kt +++ b/core/src/test/kotlin/com/rudderstack/sdk/kotlin/core/AnalyticsTest.kt @@ -1,9 +1,6 @@ package com.rudderstack.sdk.kotlin.core import com.rudderstack.sdk.kotlin.core.internals.models.emptyJsonObject -import com.rudderstack.sdk.kotlin.core.internals.models.useridentity.anonymousId -import com.rudderstack.sdk.kotlin.core.internals.models.useridentity.traits -import com.rudderstack.sdk.kotlin.core.internals.models.useridentity.userId import com.rudderstack.sdk.kotlin.core.internals.storage.Storage import com.rudderstack.sdk.kotlin.core.internals.storage.provideBasicStorage import com.rudderstack.sdk.kotlin.core.internals.utils.MockMemoryStorage From a9b03fc75eddd14b8e12abde4b519e4e292aa479 Mon Sep 17 00:00:00 2001 From: Abhishek Pandey <64667840+1abhishekpandey@users.noreply.github.com> Date: Tue, 4 Feb 2025 16:40:58 +0530 Subject: [PATCH 11/11] refactor(test): use assertEquals instead of assertTrue --- .../rudderstack/sdk/kotlin/core/AnalyticsTest.kt | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/core/src/test/kotlin/com/rudderstack/sdk/kotlin/core/AnalyticsTest.kt b/core/src/test/kotlin/com/rudderstack/sdk/kotlin/core/AnalyticsTest.kt index b915cd0c1..101d12e3f 100644 --- a/core/src/test/kotlin/com/rudderstack/sdk/kotlin/core/AnalyticsTest.kt +++ b/core/src/test/kotlin/com/rudderstack/sdk/kotlin/core/AnalyticsTest.kt @@ -7,6 +7,7 @@ import com.rudderstack.sdk.kotlin.core.internals.utils.MockMemoryStorage import com.rudderstack.sdk.kotlin.core.internals.utils.empty import io.mockk.every import io.mockk.mockkStatic +import junit.framework.TestCase.assertEquals import junit.framework.TestCase.assertNull import junit.framework.TestCase.assertTrue import kotlinx.serialization.json.JsonObject @@ -51,7 +52,7 @@ class AnalyticsTest { val anonymousId = analytics.anonymousId - assertTrue(anonymousId == CUSTOM_ANONYMOUS_ID) + assertEquals(CUSTOM_ANONYMOUS_ID, anonymousId) } @Test @@ -60,7 +61,7 @@ class AnalyticsTest { val anonymousId = analytics.anonymousId - assertTrue(anonymousId == String.empty()) + assertEquals(String.empty(), anonymousId) } @Test @@ -80,8 +81,8 @@ class AnalyticsTest { val userId = analytics.userId val traits = analytics.traits - assertTrue(userId == USER_ID) - assertTrue(traits == TRAITS) + assertEquals(USER_ID, userId) + assertEquals(TRAITS, traits) } @Test @@ -89,8 +90,8 @@ class AnalyticsTest { val userId = analytics.userId val traits = analytics.traits - assertTrue(userId == String.empty()) - assertTrue(traits == emptyJsonObject) + assertEquals(String.empty(), userId) + assertEquals(emptyJsonObject, traits) } @Test