Skip to content

Commit 1bd4e99

Browse files
LIBMOBILE-1060 (#124)
Use a UserInfo cache for userId, anonymousId, and traits.
1 parent 4a5d2be commit 1bd4e99

File tree

10 files changed

+198
-63
lines changed

10 files changed

+198
-63
lines changed

core/build.gradle

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,14 @@ java {
99
targetCompatibility = JavaVersion.VERSION_1_8
1010
}
1111

12+
compileKotlin {
13+
kotlinOptions {
14+
freeCompilerArgs += [
15+
"-Xopt-in=kotlin.RequiresOptIn"
16+
]
17+
}
18+
}
19+
1220
test {
1321
useJUnitPlatform()
1422
}

core/src/main/java/com/segment/analytics/kotlin/core/Analytics.kt

Lines changed: 48 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import com.segment.analytics.kotlin.core.platform.Timeline
77
import com.segment.analytics.kotlin.core.platform.plugins.ContextPlugin
88
import com.segment.analytics.kotlin.core.platform.plugins.SegmentDestination
99
import com.segment.analytics.kotlin.core.platform.plugins.StartupQueue
10+
import com.segment.analytics.kotlin.core.platform.plugins.UserInfoPlugin
1011
import com.segment.analytics.kotlin.core.platform.plugins.logger.SegmentLog
1112
import com.segment.analytics.kotlin.core.platform.plugins.logger.log
1213
import kotlinx.coroutines.*
@@ -19,6 +20,7 @@ import kotlinx.serialization.json.jsonObject
1920
import kotlinx.serialization.serializer
2021
import sovran.kotlin.Store
2122
import sovran.kotlin.Subscriber
23+
import java.util.*
2224
import java.util.concurrent.Executors
2325
import kotlin.reflect.KClass
2426

@@ -52,6 +54,8 @@ open class Analytics protected constructor(
5254
)
5355
}
5456

57+
internal lateinit var userInfo: UserInfo
58+
5559
companion object {
5660
var debugLogsEnabled: Boolean = false
5761
set(value) {
@@ -75,6 +79,7 @@ open class Analytics protected constructor(
7579
* Public constructor of Analytics.
7680
* @property configuration configuration that analytics can use
7781
*/
82+
@OptIn(ExperimentalCoroutinesApi::class)
7883
constructor(configuration: Configuration) : this(configuration,
7984
object : CoroutineConfiguration {
8085
override val store = Store()
@@ -94,11 +99,14 @@ open class Analytics protected constructor(
9499
add(SegmentLog())
95100
add(StartupQueue())
96101
add(ContextPlugin())
102+
add(UserInfoPlugin())
97103

98104
// Setup store
99105
analyticsScope.launch(analyticsDispatcher) {
100106
store.also {
101-
it.provide(UserInfo.defaultState(storage))
107+
// load memory with initial value
108+
userInfo = UserInfo.defaultState(storage)
109+
it.provide(userInfo)
102110
it.provide(System.defaultState(configuration, storage))
103111

104112
// subscribe to store after state is provided
@@ -523,8 +531,11 @@ open class Analytics protected constructor(
523531
* user logs out
524532
*/
525533
fun reset() {
534+
val newAnonymousId = UUID.randomUUID().toString()
535+
userInfo = UserInfo(newAnonymousId, null, null)
536+
526537
analyticsScope.launch(analyticsDispatcher) {
527-
store.dispatch(UserInfo.ResetAction(), UserInfo::class)
538+
store.dispatch(UserInfo.ResetAction(newAnonymousId), UserInfo::class)
528539
timeline.applyClosure {
529540
(it as? EventPlugin)?.reset()
530541
}
@@ -540,6 +551,7 @@ open class Analytics protected constructor(
540551
* CoroutineDispatchers and ExecutorService instances so they allow the container to shutdown
541552
* properly.
542553
*/
554+
@OptIn(ExperimentalCoroutinesApi::class)
543555
fun shutdown() {
544556
(analyticsDispatcher as CloseableCoroutineDispatcher).close()
545557
(networkIODispatcher as CloseableCoroutineDispatcher).close()
@@ -549,47 +561,44 @@ open class Analytics protected constructor(
549561
}
550562

551563
/**
552-
* Retrieve the userId registered by a previous `identify` call in a blocking way.
553-
* Note: this method invokes `runBlocking` internal, it's not recommended to be used
554-
* in coroutines.
564+
* Retrieve the userId registered by a previous `identify` call.
555565
*/
556-
@BlockingApi
557-
fun userId(): String? = runBlocking {
558-
userIdAsync()
566+
fun userId(): String? {
567+
return userInfo.userId
559568
}
560569

561570
/**
562571
* Retrieve the userId registered by a previous `identify` call
563572
*/
564-
suspend fun userIdAsync(): String? {
565-
val userInfo = store.currentState(UserInfo::class)
566-
return userInfo?.userId
573+
@Deprecated(
574+
"This function no longer serves a purpose and internally calls `userId()`.",
575+
ReplaceWith("userId()")
576+
)
577+
fun userIdAsync(): String? {
578+
return userId()
567579
}
568580

569581
/**
570-
* Retrieve the traits registered by a previous `identify` call in a blocking way.
571-
* Note: this method invokes `runBlocking` internal, it's not recommended to be used
572-
* in coroutines.
582+
* Retrieve the traits registered by a previous `identify` call.
573583
*/
574-
@BlockingApi
575-
fun traits(): JsonObject? = runBlocking {
576-
traitsAsync()
584+
fun traits(): JsonObject? {
585+
return userInfo.traits
577586
}
578587

579588
/**
580589
* Retrieve the traits registered by a previous `identify` call
581590
*/
582-
suspend fun traitsAsync(): JsonObject? {
583-
val userInfo = store.currentState(UserInfo::class)
584-
return userInfo?.traits
591+
@Deprecated(
592+
"This function no longer serves a purpose and internally calls `traits()`.",
593+
ReplaceWith("traits()")
594+
)
595+
fun traitsAsync(): JsonObject? {
596+
return traits()
585597
}
586598

587599
/**
588600
* Retrieve the traits registered by a previous `identify` call in a blocking way.
589-
* Note: this method invokes `runBlocking` internal, it's not recommended to be used
590-
* in coroutines.
591601
*/
592-
@BlockingApi
593602
inline fun <reified T : Any> traits(deserializationStrategy: DeserializationStrategy<T> = Json.serializersModule.serializer()): T? {
594603
return traits()?.let {
595604
decodeFromJsonElement(deserializationStrategy, it)
@@ -599,10 +608,12 @@ open class Analytics protected constructor(
599608
/**
600609
* Retrieve the traits registered by a previous `identify` call
601610
*/
602-
suspend inline fun <reified T : Any> traitsAsync(deserializationStrategy: DeserializationStrategy<T> = Json.serializersModule.serializer()): T? {
603-
return traitsAsync()?.let {
604-
decodeFromJsonElement(deserializationStrategy, it)
605-
}
611+
@Deprecated(
612+
"This function no longer serves a purpose and internally calls `traits(deserializationStrategy: DeserializationStrategy<T>)`.",
613+
ReplaceWith("traits(deserializationStrategy: DeserializationStrategy<T>)")
614+
)
615+
inline fun <reified T : Any> traitsAsync(deserializationStrategy: DeserializationStrategy<T> = Json.serializersModule.serializer()): T? {
616+
return traits(deserializationStrategy)
606617
}
607618

608619
/**
@@ -624,21 +635,21 @@ open class Analytics protected constructor(
624635
}
625636

626637
/**
627-
* Retrieve the anonymousId in a blocking way.
628-
* Note: this method invokes `runBlocking` internal, it's not recommended to be used
629-
* in coroutines.
638+
* Retrieve the anonymousId.
630639
*/
631-
@BlockingApi
632-
fun anonymousId(): String = runBlocking {
633-
anonymousIdAsync()
640+
fun anonymousId(): String {
641+
return userInfo.anonymousId
634642
}
635643

636644
/**
637645
* Retrieve the anonymousId
638646
*/
639-
suspend fun anonymousIdAsync(): String {
640-
val userInfo = store.currentState(UserInfo::class)
641-
return userInfo?.anonymousId ?: ""
647+
@Deprecated(
648+
"This function no longer serves a purpose and internally calls `anonymousId()`.",
649+
ReplaceWith("anonymousId()")
650+
)
651+
fun anonymousIdAsync(): String {
652+
return anonymousId()
642653
}
643654

644655
/**
@@ -679,4 +690,4 @@ internal fun isAndroid(): Boolean {
679690
} catch (e: ClassNotFoundException) {
680691
false
681692
}
682-
}
693+
}

core/src/main/java/com/segment/analytics/kotlin/core/State.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,9 +122,9 @@ data class UserInfo(
122122
}
123123
}
124124

125-
class ResetAction : Action<UserInfo> {
125+
class ResetAction(var anonymousId: String = UUID.randomUUID().toString()) : Action<UserInfo> {
126126
override fun reduce(state: UserInfo): UserInfo {
127-
return UserInfo(UUID.randomUUID().toString(), null, null)
127+
return UserInfo(anonymousId, null, null)
128128
}
129129
}
130130

core/src/main/java/com/segment/analytics/kotlin/core/platform/Mediator.kt

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
11
package com.segment.analytics.kotlin.core.platform
22

3-
import com.segment.analytics.kotlin.core.AliasEvent
43
import com.segment.analytics.kotlin.core.BaseEvent
5-
import com.segment.analytics.kotlin.core.GroupEvent
6-
import com.segment.analytics.kotlin.core.IdentifyEvent
7-
import com.segment.analytics.kotlin.core.ScreenEvent
8-
import com.segment.analytics.kotlin.core.TrackEvent
94
import kotlin.reflect.KClass
105

116
// Platform abstraction for managing plugins' execution (of a specific type)
@@ -35,6 +30,7 @@ internal class Mediator(internal val plugins: MutableList<Plugin>) {
3530
}
3631
}
3732
}
33+
3834
return result
3935
}
4036

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package com.segment.analytics.kotlin.core.platform.plugins
2+
3+
import com.segment.analytics.kotlin.core.*
4+
import com.segment.analytics.kotlin.core.platform.Plugin
5+
6+
/**
7+
* Analytics plugin used to populate events with basic UserInfo data.
8+
* Auto-added to analytics client on construction
9+
*/
10+
class UserInfoPlugin : Plugin {
11+
12+
override val type: Plugin.Type = Plugin.Type.Before
13+
override lateinit var analytics: Analytics
14+
15+
override fun execute(event: BaseEvent): BaseEvent {
16+
17+
if (event.type == EventType.Identify) {
18+
19+
analytics.userInfo.userId = event.userId
20+
analytics.userInfo.anonymousId = event.anonymousId
21+
analytics.userInfo.traits = (event as IdentifyEvent).traits
22+
23+
} else if (event.type === EventType.Alias) {
24+
25+
analytics.userInfo.anonymousId = event.anonymousId
26+
} else {
27+
28+
analytics.userInfo.userId?.let {
29+
event.userId = analytics.userInfo.userId.toString()
30+
}
31+
analytics.userInfo.anonymousId?.let {
32+
event.anonymousId = analytics.userInfo.anonymousId.toString()
33+
}
34+
}
35+
36+
return event
37+
}
38+
39+
}
40+

core/src/test/kotlin/com/segment/analytics/kotlin/core/AnalyticsTests.kt

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -494,12 +494,6 @@ class AnalyticsTests {
494494
fun `anonymousId fetches current Analytics anonymousId`() = runTest {
495495
assertEquals("qwerty-qwerty-123", analytics.anonymousIdAsync())
496496
}
497-
498-
@Test
499-
fun `anonymousId returns empty string when null`() = runTest {
500-
coEvery { analytics.store.currentState(UserInfo::class) } returns null
501-
assertEquals("", analytics.anonymousIdAsync())
502-
}
503497
}
504498

505499
@Test

core/src/test/kotlin/com/segment/analytics/kotlin/core/StateTest.kt

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -49,21 +49,21 @@ internal class StateTest {
4949
UserInfo::class
5050
)
5151

52-
assertEquals("oldUserId", analytics.userId())
53-
assertEquals(traits, analytics.traits())
52+
assertEquals("oldUserId", (analytics.store.currentState(UserInfo::class))?.userId)
53+
assertEquals(traits, (analytics.store.currentState(UserInfo::class))?.traits)
5454

5555
analytics.store.dispatch(UserInfo.ResetAction(), UserInfo::class)
56-
assertNull(analytics.userId())
57-
assertNull(analytics.traits())
56+
assertNull((analytics.store.currentState(UserInfo::class))?.userId)
57+
assertNull((analytics.store.currentState(UserInfo::class))?.userId)
5858
}
5959

6060
@Test
6161
fun setUserIdAction() = runTest {
6262
analytics.store.dispatch(UserInfo.SetUserIdAction("oldUserId"), UserInfo::class)
63-
assertEquals("oldUserId", analytics.userId())
63+
assertEquals("oldUserId", (analytics.store.currentState(UserInfo::class))?.userId)
6464

6565
analytics.store.dispatch(UserInfo.SetUserIdAction("newUserId"), UserInfo::class)
66-
assertEquals("newUserId", analytics.userId())
66+
assertEquals("newUserId", (analytics.store.currentState(UserInfo::class))?.userId)
6767
}
6868

6969
@Test
@@ -77,11 +77,11 @@ internal class StateTest {
7777
val traits = buildJsonObject { put("behaviour", "bad") }
7878

7979
analytics.store.dispatch(UserInfo.SetUserIdAction("oldUserId"), UserInfo::class)
80-
assertEquals("oldUserId", analytics.userId())
81-
assertEquals(emptyJsonObject, analytics.traits())
80+
assertEquals("oldUserId", (analytics.store.currentState(UserInfo::class))?.userId)
81+
assertEquals(emptyJsonObject, (analytics.store.currentState(UserInfo::class))?.traits)
8282

8383
analytics.store.dispatch(UserInfo.SetTraitsAction(traits), UserInfo::class)
84-
assertEquals(traits, analytics.traits())
84+
assertEquals(traits, (analytics.store.currentState(UserInfo::class))?.traits)
8585
}
8686

8787
@Test
@@ -94,8 +94,8 @@ internal class StateTest {
9494
UserInfo::class
9595
)
9696

97-
assertEquals("oldUserId", analytics.userId())
98-
assertEquals(traits, analytics.traits())
97+
assertEquals("oldUserId", (analytics.store.currentState(UserInfo::class))?.userId)
98+
assertEquals(traits, (analytics.store.currentState(UserInfo::class))?.traits)
9999
}
100100
}
101101
}

core/src/test/kotlin/com/segment/analytics/kotlin/core/platform/DestinationPluginTests.kt renamed to core/src/test/kotlin/com/segment/analytics/kotlin/core/platform/plugins/DestinationPluginTests.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1-
package com.segment.analytics.kotlin.core.platform
1+
package com.segment.analytics.kotlin.core.platform.plugins
22

33
import com.segment.analytics.kotlin.core.Analytics
44
import com.segment.analytics.kotlin.core.BaseEvent
55
import com.segment.analytics.kotlin.core.TrackEvent
66
import com.segment.analytics.kotlin.core.emptyJsonObject
7+
import com.segment.analytics.kotlin.core.platform.DestinationPlugin
8+
import com.segment.analytics.kotlin.core.platform.Plugin
9+
import com.segment.analytics.kotlin.core.platform.Timeline
710
import com.segment.analytics.kotlin.core.utilities.putInContext
811
import com.segment.analytics.kotlin.core.utils.mockAnalytics
912
import io.mockk.spyk

core/src/test/kotlin/com/segment/analytics/kotlin/core/platform/plugins/StartupQueueTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ internal class StartupQueueTest {
2727
internal fun setUp() {
2828
val config = Configuration(
2929
writeKey = "123",
30-
application = "Tetst",
30+
application = "Test",
3131
autoAddSegmentDestination = false
3232
)
3333
analytics = testAnalytics(config, testScope, testDispatcher)

0 commit comments

Comments
 (0)