Skip to content

Commit d5d4210

Browse files
authored
add api for identify using only traits (#107)
* add api for identify using only traits * add compat for Java
1 parent 6584e34 commit d5d4210

File tree

4 files changed

+197
-20
lines changed

4 files changed

+197
-20
lines changed

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

+99-20
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,13 @@ 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 kotlinx.coroutines.*
11-
import com.segment.analytics.kotlin.core.platform.plugins.logger.*
10+
import com.segment.analytics.kotlin.core.platform.plugins.logger.SegmentLog
11+
import com.segment.analytics.kotlin.core.platform.plugins.logger.log
12+
import kotlinx.coroutines.CoroutineScope
13+
import kotlinx.coroutines.SupervisorJob
14+
import kotlinx.coroutines.asCoroutineDispatcher
15+
import kotlinx.coroutines.launch
16+
import kotlinx.coroutines.runBlocking
1217
import kotlinx.serialization.DeserializationStrategy
1318
import kotlinx.serialization.SerializationStrategy
1419
import kotlinx.serialization.json.Json
@@ -32,7 +37,7 @@ import kotlin.reflect.KClass
3237
*/
3338
open class Analytics protected constructor(
3439
val configuration: Configuration,
35-
coroutineConfig: CoroutineConfiguration
40+
coroutineConfig: CoroutineConfiguration,
3641
) : Subscriber, CoroutineConfiguration by coroutineConfig {
3742

3843
// use lazy to avoid the instance being leak before fully initialized
@@ -74,13 +79,17 @@ open class Analytics protected constructor(
7479
* Public constructor of Analytics.
7580
* @property configuration configuration that analytics can use
7681
*/
77-
constructor(configuration: Configuration): this(configuration, object : CoroutineConfiguration{
78-
override val store = Store()
79-
override val analyticsScope = CoroutineScope(SupervisorJob())
80-
override val analyticsDispatcher = Executors.newCachedThreadPool().asCoroutineDispatcher()
81-
override val networkIODispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
82-
override val fileIODispatcher = Executors.newFixedThreadPool(2).asCoroutineDispatcher()
83-
})
82+
constructor(configuration: Configuration) : this(configuration,
83+
object : CoroutineConfiguration {
84+
override val store = Store()
85+
override val analyticsScope = CoroutineScope(SupervisorJob())
86+
override val analyticsDispatcher =
87+
Executors.newCachedThreadPool().asCoroutineDispatcher()
88+
override val networkIODispatcher =
89+
Executors.newSingleThreadExecutor().asCoroutineDispatcher()
90+
override val fileIODispatcher =
91+
Executors.newFixedThreadPool(2).asCoroutineDispatcher()
92+
})
8493

8594
// This function provides a default state to the store & attaches the storage and store instances
8695
// Initiates the initial call to settings and adds default system plugins
@@ -89,7 +98,7 @@ open class Analytics protected constructor(
8998
add(SegmentLog())
9099
add(StartupQueue())
91100
add(ContextPlugin())
92-
101+
93102
// Setup store
94103
analyticsScope.launch(analyticsDispatcher) {
95104
store.also {
@@ -138,7 +147,7 @@ open class Analytics protected constructor(
138147
fun <T : Any> track(
139148
name: String,
140149
properties: T,
141-
serializationStrategy: SerializationStrategy<T>
150+
serializationStrategy: SerializationStrategy<T>,
142151
) {
143152
track(name, Json.encodeToJsonElement(serializationStrategy, properties).jsonObject)
144153
}
@@ -154,7 +163,7 @@ open class Analytics protected constructor(
154163
*/
155164
inline fun <reified T : Any> track(
156165
name: String,
157-
properties: T
166+
properties: T,
158167
) {
159168
track(name, properties, Json.serializersModule.serializer())
160169
}
@@ -204,11 +213,81 @@ open class Analytics protected constructor(
204213
fun <T : Any> identify(
205214
userId: String,
206215
traits: T,
207-
serializationStrategy: SerializationStrategy<T>
216+
serializationStrategy: SerializationStrategy<T>,
208217
) {
209218
identify(userId, Json.encodeToJsonElement(serializationStrategy, traits).jsonObject)
210219
}
211220

221+
/**
222+
* Identify lets you tie one of your users and their actions to a recognizable {@code userId}.
223+
* It also lets you record {@code traits} about the user, like their email, name, account type,
224+
* etc.
225+
*
226+
* <p>Traits and userId will be automatically cached and available on future sessions for the
227+
* same user. To update a trait on the server, call identify with the same user id.
228+
* You can also use {@link #identify(Traits)} for this purpose.
229+
*
230+
* In the case when user logs out, make sure to call {@link #reset()} to clear user's identity
231+
* info.
232+
*
233+
* @param traits [Traits] about the user. Needs to be [serializable](https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/serializers.md)
234+
* @see <a href="https://segment.com/docs/spec/identify/">Identify Documentation</a>
235+
*/
236+
inline fun <reified T : Any> identify(
237+
traits: T,
238+
) {
239+
identify(traits, Json.serializersModule.serializer())
240+
}
241+
242+
/**
243+
* Identify lets you record {@code traits} about the user, like their email, name, account type,
244+
* etc.
245+
*
246+
* <p>Traits and userId will be automatically cached and available on future sessions for the
247+
* same user. To update a trait on the server, call identify with the same user id.
248+
* You can also use {@link #identify(Traits)} for this purpose.
249+
*
250+
* In the case when user logs out, make sure to call {@link #reset()} to clear user's identity
251+
* info.
252+
*
253+
* @param traits [Traits] about the user.
254+
* @see <a href="https://segment.com/docs/spec/identify/">Identify Documentation</a>
255+
*/
256+
@JvmOverloads
257+
fun identify(traits: JsonObject = emptyJsonObject) {
258+
analyticsScope.launch(analyticsDispatcher) {
259+
store.dispatch(UserInfo.SetTraitsAction(traits), UserInfo::class)
260+
}
261+
val event = IdentifyEvent(
262+
userId = "", // using "" for userId, which will get filled down the pipe
263+
traits = traits
264+
)
265+
process(event)
266+
}
267+
268+
/**
269+
* Identify lets you tie one of your users and their actions to a recognizable {@code userId}.
270+
* It also lets you record {@code traits} about the user, like their email, name, account type,
271+
* etc.
272+
*
273+
* <p>Traits and userId will be automatically cached and available on future sessions for the
274+
* same user. To update a trait on the server, call identify with the same user id.
275+
* You can also use {@link #identify(Traits)} for this purpose.
276+
*
277+
* In the case when user logs out, make sure to call {@link #reset()} to clear user's identity
278+
* info.
279+
*
280+
* @param traits [Traits] about the user. Needs to be [serializable](https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/serializers.md)
281+
* @param serializationStrategy strategy to serialize [traits]
282+
* @see <a href="https://segment.com/docs/spec/identify/">Identify Documentation</a>
283+
*/
284+
fun <T : Any> identify(
285+
traits: T,
286+
serializationStrategy: SerializationStrategy<T>,
287+
) {
288+
identify(Json.encodeToJsonElement(serializationStrategy, traits).jsonObject)
289+
}
290+
212291
/**
213292
* Identify lets you tie one of your users and their actions to a recognizable {@code userId}.
214293
* It also lets you record {@code traits} about the user, like their email, name, account type,
@@ -246,7 +325,7 @@ open class Analytics protected constructor(
246325
fun screen(
247326
title: String,
248327
properties: JsonObject = emptyJsonObject,
249-
category: String = ""
328+
category: String = "",
250329
) {
251330
val event = ScreenEvent(name = title, category = category, properties = properties)
252331
process(event)
@@ -267,7 +346,7 @@ open class Analytics protected constructor(
267346
title: String,
268347
properties: T,
269348
serializationStrategy: SerializationStrategy<T>,
270-
category: String = ""
349+
category: String = "",
271350
) {
272351
screen(
273352
title,
@@ -326,7 +405,7 @@ open class Analytics protected constructor(
326405
fun <T : Any> group(
327406
groupId: String,
328407
traits: T,
329-
serializationStrategy: SerializationStrategy<T>
408+
serializationStrategy: SerializationStrategy<T>,
330409
) {
331410
group(groupId, Json.encodeToJsonElement(serializationStrategy, traits).jsonObject)
332411
}
@@ -404,7 +483,7 @@ open class Analytics protected constructor(
404483
* 2. or the first instance of subclass of the given class/interface
405484
* @param plugin [KClass]
406485
*/
407-
fun <T: Plugin> find(plugin: KClass<T>): T? = this.timeline.find(plugin)
486+
fun <T : Plugin> find(plugin: KClass<T>): T? = this.timeline.find(plugin)
408487

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

423502
/**
424503
* Remove a plugin from the analytics timeline using its name
@@ -552,7 +631,7 @@ open class Analytics protected constructor(
552631
/**
553632
* Retrieve the version of this library in use.
554633
* - Returns: A string representing the version in "BREAKING.FEATURE.FIX" format.
555-
*/
634+
*/
556635
fun version() = Analytics.version()
557636
}
558637

core/src/main/java/com/segment/analytics/kotlin/core/compat/JavaAnalytics.kt

+35
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,41 @@ class JavaAnalytics private constructor() {
114114
*/
115115
fun identify(userId: String, serializable: JsonSerializable) = analytics.identify(userId, serializable.serialize())
116116

117+
/**
118+
* Identify lets you record {@code traits} about the user, like their email, name, account type,
119+
* etc.
120+
*
121+
* <p>Traits and userId will be automatically cached and available on future sessions for the
122+
* same user. To update a trait on the server, call identify with the same user id (or null).
123+
* You can also use {@link #identify(Traits)} for this purpose.
124+
*
125+
* In the case when user logs out, make sure to call {@link #reset()} to clear user's identity
126+
* info.
127+
*
128+
* @param traits [Traits] about the user in JsonObject form. can be built by
129+
* {@link Builder com.segment.analytics.kotlin.core.compat.Builder}
130+
* @see <a href="https://segment.com/docs/spec/identify/">Identify Documentation</a>
131+
*/
132+
@JvmOverloads
133+
fun identify(traits: JsonObject = emptyJsonObject) = analytics.identify(traits)
134+
135+
/**
136+
* Identify lets you record {@code traits} about the user, like their email, name, account type,
137+
* etc.
138+
*
139+
* <p>Traits and userId will be automatically cached and available on future sessions for the
140+
* same user. To update a trait on the server, call identify with the same user id (or null).
141+
* You can also use {@link #identify(Traits)} for this purpose.
142+
*
143+
* In the case when user logs out, make sure to call {@link #reset()} to clear user's identity
144+
* info.
145+
*
146+
* @param serializable an object that implements {@link JsonSerializable com.segment.analytics.kotlin.core.compat.JsonSerializable}
147+
* @see <a href="https://segment.com/docs/spec/identify/">Identify Documentation</a>
148+
*/
149+
fun identify(serializable: JsonSerializable) = analytics.identify(serializable.serialize())
150+
151+
117152
/**
118153
* The screen methods let your record whenever a user sees a screen of your mobile app, and
119154
* attach a name, category or properties to the screen. Either category or name must be

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

+29
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,35 @@ class AnalyticsTests {
291291
), newUserInfo
292292
)
293293
}
294+
295+
@Test
296+
fun `identify() overwrites traits`() = runTest {
297+
analytics.store.dispatch(
298+
UserInfo.SetUserIdAndTraitsAction(
299+
"oldUserId",
300+
buildJsonObject { put("behaviour", "bad") }),
301+
UserInfo::class
302+
)
303+
val curUserInfo = analytics.store.currentState(UserInfo::class)
304+
assertEquals(
305+
UserInfo(
306+
userId = "oldUserId",
307+
traits = buildJsonObject { put("behaviour", "bad") },
308+
anonymousId = "qwerty-qwerty-123"
309+
), curUserInfo
310+
)
311+
312+
analytics.identify( buildJsonObject { put("behaviour", "good") })
313+
314+
val newUserInfo = analytics.store.currentState(UserInfo::class)
315+
assertEquals(
316+
UserInfo(
317+
userId = "oldUserId",
318+
traits = buildJsonObject { put("behaviour", "good") },
319+
anonymousId = "qwerty-qwerty-123"
320+
), newUserInfo
321+
)
322+
}
294323
}
295324

296325
@Nested

core/src/test/kotlin/com/segment/analytics/kotlin/core/compat/JavaAnalyticsTest.kt

+34
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import com.segment.analytics.kotlin.core.ScreenEvent
1010
import com.segment.analytics.kotlin.core.Settings
1111
import com.segment.analytics.kotlin.core.System
1212
import com.segment.analytics.kotlin.core.TrackEvent
13+
import com.segment.analytics.kotlin.core.UserInfo
1314
import com.segment.analytics.kotlin.core.emptyJsonObject
1415
import com.segment.analytics.kotlin.core.platform.DestinationPlugin
1516
import com.segment.analytics.kotlin.core.platform.Plugin
@@ -350,6 +351,39 @@ internal class JavaAnalyticsTest {
350351
identify.captured
351352
)
352353
}
354+
355+
@Test
356+
fun `identify with only serializable traits`() = runTest {
357+
analytics.store.dispatch(UserInfo.SetUserIdAction(userId), UserInfo::class)
358+
359+
analytics.add(mockPlugin)
360+
analytics.identify(serializable)
361+
362+
verify { mockPlugin.identify(capture(identify)) }
363+
assertEquals(
364+
IdentifyEvent("", json).populate().apply {
365+
userId = this@Identify.userId
366+
},
367+
identify.captured
368+
)
369+
}
370+
371+
@Test
372+
fun `identify with only json traits`() = runTest {
373+
analytics.store.dispatch(UserInfo.SetUserIdAction(userId), UserInfo::class)
374+
375+
analytics.add(mockPlugin)
376+
analytics.identify(json)
377+
378+
val identify = slot<IdentifyEvent>()
379+
verify { mockPlugin.identify(capture(identify)) }
380+
assertEquals(
381+
IdentifyEvent("", json).populate().apply {
382+
userId = this@Identify.userId
383+
},
384+
identify.captured
385+
)
386+
}
353387
}
354388

355389

0 commit comments

Comments
 (0)