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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 99 additions & 20 deletions core/src/main/java/com/segment/analytics/kotlin/core/Analytics.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,13 @@ import com.segment.analytics.kotlin.core.platform.Timeline
import com.segment.analytics.kotlin.core.platform.plugins.ContextPlugin
import com.segment.analytics.kotlin.core.platform.plugins.SegmentDestination
import com.segment.analytics.kotlin.core.platform.plugins.StartupQueue
import kotlinx.coroutines.*
import com.segment.analytics.kotlin.core.platform.plugins.logger.*
import com.segment.analytics.kotlin.core.platform.plugins.logger.SegmentLog
import com.segment.analytics.kotlin.core.platform.plugins.logger.log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.SerializationStrategy
import kotlinx.serialization.json.Json
Expand All @@ -32,7 +37,7 @@ import kotlin.reflect.KClass
*/
open class Analytics protected constructor(
val configuration: Configuration,
coroutineConfig: CoroutineConfiguration
coroutineConfig: CoroutineConfiguration,
) : Subscriber, CoroutineConfiguration by coroutineConfig {

// use lazy to avoid the instance being leak before fully initialized
Expand Down Expand Up @@ -74,13 +79,17 @@ open class Analytics protected constructor(
* Public constructor of Analytics.
* @property configuration configuration that analytics can use
*/
constructor(configuration: Configuration): this(configuration, object : CoroutineConfiguration{
override val store = Store()
override val analyticsScope = CoroutineScope(SupervisorJob())
override val analyticsDispatcher = Executors.newCachedThreadPool().asCoroutineDispatcher()
override val networkIODispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
override val fileIODispatcher = Executors.newFixedThreadPool(2).asCoroutineDispatcher()
})
constructor(configuration: Configuration) : this(configuration,
object : CoroutineConfiguration {
override val store = Store()
override val analyticsScope = CoroutineScope(SupervisorJob())
override val analyticsDispatcher =
Executors.newCachedThreadPool().asCoroutineDispatcher()
override val networkIODispatcher =
Executors.newSingleThreadExecutor().asCoroutineDispatcher()
override val fileIODispatcher =
Executors.newFixedThreadPool(2).asCoroutineDispatcher()
})

// This function provides a default state to the store & attaches the storage and store instances
// Initiates the initial call to settings and adds default system plugins
Expand All @@ -89,7 +98,7 @@ open class Analytics protected constructor(
add(SegmentLog())
add(StartupQueue())
add(ContextPlugin())

// Setup store
analyticsScope.launch(analyticsDispatcher) {
store.also {
Expand Down Expand Up @@ -138,7 +147,7 @@ open class Analytics protected constructor(
fun <T : Any> track(
name: String,
properties: T,
serializationStrategy: SerializationStrategy<T>
serializationStrategy: SerializationStrategy<T>,
) {
track(name, Json.encodeToJsonElement(serializationStrategy, properties).jsonObject)
}
Expand All @@ -154,7 +163,7 @@ open class Analytics protected constructor(
*/
inline fun <reified T : Any> track(
name: String,
properties: T
properties: T,
) {
track(name, properties, Json.serializersModule.serializer())
}
Expand Down Expand Up @@ -204,11 +213,81 @@ open class Analytics protected constructor(
fun <T : Any> identify(
userId: String,
traits: T,
serializationStrategy: SerializationStrategy<T>
serializationStrategy: SerializationStrategy<T>,
) {
identify(userId, Json.encodeToJsonElement(serializationStrategy, traits).jsonObject)
}

/**
* Identify lets you tie one of your users and their actions to a recognizable {@code userId}.
* It also lets you record {@code traits} about the user, like their email, name, account type,
* etc.
*
* <p>Traits and userId will be automatically cached and available on future sessions for the
* same user. To update a trait on the server, call identify with the same user id.
* You can also use {@link #identify(Traits)} for this purpose.
*
* In the case when user logs out, make sure to call {@link #reset()} to clear user's identity
* info.
*
* @param traits [Traits] about the user. Needs to be [serializable](https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/serializers.md)
* @see <a href="https://segment.com/docs/spec/identify/">Identify Documentation</a>
*/
inline fun <reified T : Any> identify(
traits: T,
) {
identify(traits, Json.serializersModule.serializer())
}

/**
* Identify lets you record {@code traits} about the user, like their email, name, account type,
* etc.
*
* <p>Traits and userId will be automatically cached and available on future sessions for the
* same user. To update a trait on the server, call identify with the same user id.
* You can also use {@link #identify(Traits)} for this purpose.
*
* In the case when user logs out, make sure to call {@link #reset()} to clear user's identity
* info.
*
* @param traits [Traits] about the user.
* @see <a href="https://segment.com/docs/spec/identify/">Identify Documentation</a>
*/
@JvmOverloads
fun identify(traits: JsonObject = emptyJsonObject) {
analyticsScope.launch(analyticsDispatcher) {
store.dispatch(UserInfo.SetTraitsAction(traits), UserInfo::class)
}
val event = IdentifyEvent(
userId = "", // using "" for userId, which will get filled down the pipe
traits = traits
)
process(event)
}

/**
* Identify lets you tie one of your users and their actions to a recognizable {@code userId}.
* It also lets you record {@code traits} about the user, like their email, name, account type,
* etc.
*
* <p>Traits and userId will be automatically cached and available on future sessions for the
* same user. To update a trait on the server, call identify with the same user id.
* You can also use {@link #identify(Traits)} for this purpose.
*
* In the case when user logs out, make sure to call {@link #reset()} to clear user's identity
* info.
*
* @param traits [Traits] about the user. Needs to be [serializable](https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/serializers.md)
* @param serializationStrategy strategy to serialize [traits]
* @see <a href="https://segment.com/docs/spec/identify/">Identify Documentation</a>
*/
fun <T : Any> identify(
traits: T,
serializationStrategy: SerializationStrategy<T>,
) {
identify(Json.encodeToJsonElement(serializationStrategy, traits).jsonObject)
}

/**
* Identify lets you tie one of your users and their actions to a recognizable {@code userId}.
* It also lets you record {@code traits} about the user, like their email, name, account type,
Expand Down Expand Up @@ -246,7 +325,7 @@ open class Analytics protected constructor(
fun screen(
title: String,
properties: JsonObject = emptyJsonObject,
category: String = ""
category: String = "",
) {
val event = ScreenEvent(name = title, category = category, properties = properties)
process(event)
Expand All @@ -267,7 +346,7 @@ open class Analytics protected constructor(
title: String,
properties: T,
serializationStrategy: SerializationStrategy<T>,
category: String = ""
category: String = "",
) {
screen(
title,
Expand Down Expand Up @@ -326,7 +405,7 @@ open class Analytics protected constructor(
fun <T : Any> group(
groupId: String,
traits: T,
serializationStrategy: SerializationStrategy<T>
serializationStrategy: SerializationStrategy<T>,
) {
group(groupId, Json.encodeToJsonElement(serializationStrategy, traits).jsonObject)
}
Expand Down Expand Up @@ -404,7 +483,7 @@ open class Analytics protected constructor(
* 2. or the first instance of subclass of the given class/interface
* @param plugin [KClass]
*/
fun <T: Plugin> find(plugin: KClass<T>): T? = this.timeline.find(plugin)
fun <T : Plugin> find(plugin: KClass<T>): T? = this.timeline.find(plugin)

/**
* Retrieve the first match of registered destination plugin by key. It finds
Expand All @@ -418,7 +497,7 @@ open class Analytics protected constructor(
* 2. and all instances of subclass of the given class/interface
* @param plugin [KClass]
*/
fun <T: Plugin> findAll(plugin: KClass<T>): List<T> = this.timeline.findAll(plugin)
fun <T : Plugin> findAll(plugin: KClass<T>): List<T> = this.timeline.findAll(plugin)

/**
* Remove a plugin from the analytics timeline using its name
Expand Down Expand Up @@ -552,7 +631,7 @@ open class Analytics protected constructor(
/**
* Retrieve the version of this library in use.
* - Returns: A string representing the version in "BREAKING.FEATURE.FIX" format.
*/
*/
fun version() = Analytics.version()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,41 @@ class JavaAnalytics private constructor() {
*/
fun identify(userId: String, serializable: JsonSerializable) = analytics.identify(userId, serializable.serialize())

/**
* Identify lets you record {@code traits} about the user, like their email, name, account type,
* etc.
*
* <p>Traits and userId will be automatically cached and available on future sessions for the
* same user. To update a trait on the server, call identify with the same user id (or null).
* You can also use {@link #identify(Traits)} for this purpose.
*
* In the case when user logs out, make sure to call {@link #reset()} to clear user's identity
* info.
*
* @param traits [Traits] about the user in JsonObject form. can be built by
* {@link Builder com.segment.analytics.kotlin.core.compat.Builder}
* @see <a href="https://segment.com/docs/spec/identify/">Identify Documentation</a>
*/
@JvmOverloads
fun identify(traits: JsonObject = emptyJsonObject) = analytics.identify(traits)

/**
* Identify lets you record {@code traits} about the user, like their email, name, account type,
* etc.
*
* <p>Traits and userId will be automatically cached and available on future sessions for the
* same user. To update a trait on the server, call identify with the same user id (or null).
* You can also use {@link #identify(Traits)} for this purpose.
*
* In the case when user logs out, make sure to call {@link #reset()} to clear user's identity
* info.
*
* @param serializable an object that implements {@link JsonSerializable com.segment.analytics.kotlin.core.compat.JsonSerializable}
* @see <a href="https://segment.com/docs/spec/identify/">Identify Documentation</a>
*/
fun identify(serializable: JsonSerializable) = analytics.identify(serializable.serialize())


/**
* The screen methods let your record whenever a user sees a screen of your mobile app, and
* attach a name, category or properties to the screen. Either category or name must be
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,35 @@ class AnalyticsTests {
), newUserInfo
)
}

@Test
fun `identify() overwrites traits`() = runTest {
analytics.store.dispatch(
UserInfo.SetUserIdAndTraitsAction(
"oldUserId",
buildJsonObject { put("behaviour", "bad") }),
UserInfo::class
)
val curUserInfo = analytics.store.currentState(UserInfo::class)
assertEquals(
UserInfo(
userId = "oldUserId",
traits = buildJsonObject { put("behaviour", "bad") },
anonymousId = "qwerty-qwerty-123"
), curUserInfo
)

analytics.identify( buildJsonObject { put("behaviour", "good") })

val newUserInfo = analytics.store.currentState(UserInfo::class)
assertEquals(
UserInfo(
userId = "oldUserId",
traits = buildJsonObject { put("behaviour", "good") },
anonymousId = "qwerty-qwerty-123"
), newUserInfo
)
}
}

@Nested
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import com.segment.analytics.kotlin.core.ScreenEvent
import com.segment.analytics.kotlin.core.Settings
import com.segment.analytics.kotlin.core.System
import com.segment.analytics.kotlin.core.TrackEvent
import com.segment.analytics.kotlin.core.UserInfo
import com.segment.analytics.kotlin.core.emptyJsonObject
import com.segment.analytics.kotlin.core.platform.DestinationPlugin
import com.segment.analytics.kotlin.core.platform.Plugin
Expand Down Expand Up @@ -350,6 +351,39 @@ internal class JavaAnalyticsTest {
identify.captured
)
}

@Test
fun `identify with only serializable traits`() = runTest {
analytics.store.dispatch(UserInfo.SetUserIdAction(userId), UserInfo::class)

analytics.add(mockPlugin)
analytics.identify(serializable)

verify { mockPlugin.identify(capture(identify)) }
assertEquals(
IdentifyEvent("", json).populate().apply {
userId = [email protected]
},
identify.captured
)
}

@Test
fun `identify with only json traits`() = runTest {
analytics.store.dispatch(UserInfo.SetUserIdAction(userId), UserInfo::class)

analytics.add(mockPlugin)
analytics.identify(json)

val identify = slot<IdentifyEvent>()
verify { mockPlugin.identify(capture(identify)) }
assertEquals(
IdentifyEvent("", json).populate().apply {
userId = [email protected]
},
identify.captured
)
}
}


Expand Down