Skip to content

Commit 65d5419

Browse files
authored
Integrations Logic fix + Copy mechanism for payloads (#59)
* move logic for integrations into SegmentIO and fixup usages of APIs * add payload copy implementation + test fixes * fix tests * fixup plugins not receiving initial settings update * fix tests * address some PR comments * disable roboelectric tests * enable with diff configs * optimize finding destinations * add java compat + tests for new settings api * fix manually enabling logic + add tests
1 parent c80089d commit 65d5419

File tree

23 files changed

+705
-258
lines changed

23 files changed

+705
-258
lines changed

android/src/test/java/com/segment/analytics/kotlin/android/StorageTests.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,9 @@ class StorageTests {
4646
store.provide(
4747
System(
4848
configuration = Configuration("123"),
49-
integrations = emptyJsonObject,
5049
settings = Settings(),
51-
false
50+
running = false,
51+
initialSettingsDispatched = false
5252
)
5353
)
5454

@@ -89,7 +89,6 @@ class StorageTests {
8989
override fun reduce(state: System): System {
9090
return System(
9191
configuration = state.configuration,
92-
integrations = state.integrations,
9392
settings = Settings(
9493
integrations = buildJsonObject {
9594
put(
@@ -104,7 +103,8 @@ class StorageTests {
104103
plan = emptyJsonObject,
105104
edgeFunction = emptyJsonObject
106105
),
107-
false
106+
running = false,
107+
initialSettingsDispatched = false
108108
)
109109
}
110110
}

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

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -382,11 +382,6 @@ class Analytics internal constructor(
382382
*/
383383
fun add(plugin: Plugin): Analytics {
384384
this.timeline.add(plugin)
385-
if (plugin is DestinationPlugin && plugin !is SegmentDestination) {
386-
analyticsScope.launch(analyticsDispatcher) {
387-
store.dispatch(System.AddIntegrationAction(plugin.key), System::class)
388-
}
389-
}
390385
return this
391386
}
392387

@@ -412,11 +407,6 @@ class Analytics internal constructor(
412407
*/
413408
fun remove(plugin: Plugin): Analytics {
414409
this.timeline.remove(plugin)
415-
if (plugin is DestinationPlugin && plugin !is SegmentDestination) {
416-
analyticsScope.launch(analyticsDispatcher) {
417-
store.dispatch(System.RemoveIntegrationAction(plugin.key), System::class)
418-
}
419-
}
420410
return this
421411
}
422412

@@ -503,6 +493,24 @@ class Analytics internal constructor(
503493
decodeFromJsonElement(deserializationStrategy, it)
504494
}
505495
}
496+
497+
/**
498+
* Retrieve the settings in a blocking way.
499+
* Note: this method invokes `runBlocking` internal, it's not recommended to be used
500+
* in coroutines.
501+
*/
502+
@BlockingApi
503+
fun settings(): Settings? = runBlocking {
504+
settingsAsync()
505+
}
506+
507+
/**
508+
* Retrieve the settings
509+
*/
510+
suspend fun settingsAsync(): Settings? {
511+
val system = store.currentState(System::class)
512+
return system?.settings
513+
}
506514
}
507515

508516
// constructor function to build analytics in dsl format with config options

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

Lines changed: 153 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,17 +90,70 @@ sealed class BaseEvent {
9090
}
9191

9292
internal suspend fun applyBaseEventData(store: Store) {
93-
val system = store.currentState(System::class) ?: return
9493
val userInfo = store.currentState(UserInfo::class) ?: return
9594

9695
this.anonymousId = userInfo.anonymousId
97-
this.integrations = system.integrations ?: emptyJsonObject
96+
this.integrations = emptyJsonObject
9897

9998
if (this.userId.isBlank()) {
10099
// attach system userId if present
101100
this.userId = userInfo.userId ?: ""
102101
}
103102
}
103+
104+
// Create a shallow copy of this event payload
105+
fun <T : BaseEvent> copy(): T {
106+
val original = this
107+
val copy = when (this) {
108+
is AliasEvent -> AliasEvent(userId = this.userId, previousId = this.previousId)
109+
is GroupEvent -> GroupEvent(groupId = this.groupId, traits = this.traits)
110+
is IdentifyEvent -> IdentifyEvent(userId = this.userId, traits = this.traits)
111+
is ScreenEvent -> ScreenEvent(
112+
name = this.name,
113+
category = this.category,
114+
properties = this.properties
115+
)
116+
is TrackEvent -> TrackEvent(event = this.event, properties = this.properties)
117+
}.apply {
118+
// type = original.type
119+
anonymousId = original.anonymousId
120+
messageId = original.messageId
121+
timestamp = original.timestamp
122+
context = original.context
123+
integrations = original.integrations
124+
userId = original.userId
125+
}
126+
@Suppress("UNCHECKED_CAST")
127+
return copy as T // This is ok because resultant type will be same as input type
128+
}
129+
130+
override fun equals(other: Any?): Boolean {
131+
if (this === other) return true
132+
if (javaClass != other?.javaClass) return false
133+
134+
other as BaseEvent
135+
136+
if (type != other.type) return false
137+
if (anonymousId != other.anonymousId) return false
138+
if (messageId != other.messageId) return false
139+
if (timestamp != other.timestamp) return false
140+
if (context != other.context) return false
141+
if (integrations != other.integrations) return false
142+
if (userId != other.userId) return false
143+
144+
return true
145+
}
146+
147+
override fun hashCode(): Int {
148+
var result = type.hashCode()
149+
result = 31 * result + anonymousId.hashCode()
150+
result = 31 * result + messageId.hashCode()
151+
result = 31 * result + timestamp.hashCode()
152+
result = 31 * result + context.hashCode()
153+
result = 31 * result + integrations.hashCode()
154+
result = 31 * result + userId.hashCode()
155+
return result
156+
}
104157
}
105158

106159
@Serializable
@@ -117,6 +170,27 @@ data class TrackEvent(
117170
override var userId: String = ""
118171

119172
override lateinit var timestamp: String
173+
174+
override fun equals(other: Any?): Boolean {
175+
if (this === other) return true
176+
if (javaClass != other?.javaClass) return false
177+
if (!super.equals(other)) return false
178+
179+
other as TrackEvent
180+
181+
if (properties != other.properties) return false
182+
if (event != other.event) return false
183+
184+
return true
185+
}
186+
187+
override fun hashCode(): Int {
188+
var result = super.hashCode()
189+
result = 31 * result + properties.hashCode()
190+
result = 31 * result + event.hashCode()
191+
return result
192+
}
193+
120194
}
121195

122196
@Serializable
@@ -132,6 +206,24 @@ data class IdentifyEvent(
132206
override lateinit var context: AnalyticsContext
133207

134208
override lateinit var timestamp: String
209+
210+
override fun equals(other: Any?): Boolean {
211+
if (this === other) return true
212+
if (javaClass != other?.javaClass) return false
213+
if (!super.equals(other)) return false
214+
215+
other as IdentifyEvent
216+
217+
if (traits != other.traits) return false
218+
219+
return true
220+
}
221+
222+
override fun hashCode(): Int {
223+
var result = super.hashCode()
224+
result = 31 * result + traits.hashCode()
225+
return result
226+
}
135227
}
136228

137229
@Serializable
@@ -148,6 +240,25 @@ data class GroupEvent(
148240
override var userId: String = ""
149241

150242
override lateinit var timestamp: String
243+
override fun equals(other: Any?): Boolean {
244+
if (this === other) return true
245+
if (javaClass != other?.javaClass) return false
246+
if (!super.equals(other)) return false
247+
248+
other as GroupEvent
249+
250+
if (groupId != other.groupId) return false
251+
if (traits != other.traits) return false
252+
253+
return true
254+
}
255+
256+
override fun hashCode(): Int {
257+
var result = super.hashCode()
258+
result = 31 * result + groupId.hashCode()
259+
result = 31 * result + traits.hashCode()
260+
return result
261+
}
151262
}
152263

153264
@Serializable
@@ -163,6 +274,24 @@ data class AliasEvent(
163274
override lateinit var context: AnalyticsContext
164275

165276
override lateinit var timestamp: String
277+
278+
override fun equals(other: Any?): Boolean {
279+
if (this === other) return true
280+
if (javaClass != other?.javaClass) return false
281+
if (!super.equals(other)) return false
282+
283+
other as AliasEvent
284+
285+
if (previousId != other.previousId) return false
286+
287+
return true
288+
}
289+
290+
override fun hashCode(): Int {
291+
var result = super.hashCode()
292+
result = 31 * result + previousId.hashCode()
293+
return result
294+
}
166295
}
167296

168297
@Serializable
@@ -180,4 +309,26 @@ data class ScreenEvent(
180309
override var userId: String = ""
181310

182311
override lateinit var timestamp: String
312+
313+
override fun equals(other: Any?): Boolean {
314+
if (this === other) return true
315+
if (javaClass != other?.javaClass) return false
316+
if (!super.equals(other)) return false
317+
318+
other as ScreenEvent
319+
320+
if (name != other.name) return false
321+
if (category != other.category) return false
322+
if (properties != other.properties) return false
323+
324+
return true
325+
}
326+
327+
override fun hashCode(): Int {
328+
var result = super.hashCode()
329+
result = 31 * result + name.hashCode()
330+
result = 31 * result + category.hashCode()
331+
result = 31 * result + properties.hashCode()
332+
return result
333+
}
183334
}

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

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@ package com.segment.analytics.kotlin.core
22

33
import com.segment.analytics.kotlin.core.platform.DestinationPlugin
44
import com.segment.analytics.kotlin.core.platform.Plugin
5-
import com.segment.analytics.kotlin.core.platform.plugins.logger.*
5+
import com.segment.analytics.kotlin.core.platform.plugins.logger.LogFilterKind
6+
import com.segment.analytics.kotlin.core.platform.plugins.logger.log
7+
import com.segment.analytics.kotlin.core.platform.plugins.logger.segmentLog
68
import com.segment.analytics.kotlin.core.utilities.LenientJson
79
import com.segment.analytics.kotlin.core.utilities.safeJsonObject
10+
import kotlinx.coroutines.launch
811
import kotlinx.coroutines.withContext
912
import kotlinx.serialization.DeserializationStrategy
1013
import kotlinx.serialization.Serializable
@@ -22,29 +25,50 @@ data class Settings(
2225
) {
2326
inline fun <reified T : Any> destinationSettings(
2427
name: String,
25-
strategy: DeserializationStrategy<T> = Json.serializersModule.serializer()
28+
strategy: DeserializationStrategy<T> = Json.serializersModule.serializer(),
2629
): T? {
2730
val integrationData = integrations[name]?.safeJsonObject ?: return null
2831
val typedSettings = LenientJson.decodeFromJsonElement(strategy, integrationData)
2932
return typedSettings
3033
}
3134

32-
fun isDestinationEnabled(name: String): Boolean {
33-
return integrations.containsKey(name)
35+
fun hasIntegrationSettings(plugin: DestinationPlugin): Boolean {
36+
return hasIntegrationSettings(plugin.key)
37+
}
38+
39+
fun hasIntegrationSettings(key: String): Boolean {
40+
return integrations.containsKey(key)
3441
}
3542
}
3643

3744
internal fun Analytics.update(settings: Settings, type: Plugin.UpdateType) {
3845
timeline.applyClosure { plugin ->
3946
if (plugin is DestinationPlugin) {
40-
plugin.enabled = settings.isDestinationEnabled(plugin.key)
47+
plugin.enabled = settings.hasIntegrationSettings(plugin)
4148
}
4249
// tell all top level plugins to update.
4350
// For destination plugins they auto-handle propagation to sub-plugins
4451
plugin.update(settings, type)
4552
}
4653
}
4754

55+
/**
56+
* Manually enable a destination plugin. This is useful when a given DestinationPlugin doesn't have any Segment tie-ins at all.
57+
* This will allow the destination to be processed in the same way within this library.
58+
*/
59+
fun Analytics.manuallyEnableDestination(plugin: DestinationPlugin) {
60+
analyticsScope.launch(analyticsDispatcher) {
61+
store.dispatch(
62+
System.AddDestinationToSettingsAction(destinationKey = plugin.key),
63+
System::class
64+
)
65+
// Differs from swift, bcos kotlin can store `enabled` state. ref: https://git.io/J1bhJ
66+
// finding it in timeline rather than using the ref that is provided to cover our bases
67+
find(plugin::class)?.enabled = true
68+
}
69+
}
70+
71+
4872
/**
4973
* Make analytics client call into Segment's settings API, to refresh certain configurations.
5074
*/
@@ -53,12 +77,13 @@ suspend fun Analytics.checkSettings() {
5377
val cdnHost = configuration.cdnHost
5478

5579
// check current system state to determine whether it's initial or refresh
56-
val systemState = store.currentState(System::class)
57-
val hasSettings = systemState?.settings?.integrations != null &&
58-
systemState.settings?.plan != null
59-
val updateType = if (hasSettings) Plugin.UpdateType.Refresh else Plugin.UpdateType.Initial
80+
val systemState = store.currentState(System::class) ?: return
81+
val updateType = if (systemState.initialSettingsDispatched) {
82+
Plugin.UpdateType.Refresh
83+
} else {
84+
Plugin.UpdateType.Initial
85+
}
6086

61-
// stop things; queue in case our settings have changed.
6287
store.dispatch(System.ToggleRunningAction(running = false), System::class)
6388

6489
withContext(networkIODispatcher) {
@@ -67,10 +92,13 @@ suspend fun Analytics.checkSettings() {
6792
val connection = HTTPClient(writeKey).settings(cdnHost)
6893
val settingsString =
6994
connection.inputStream?.bufferedReader()?.use(BufferedReader::readText) ?: ""
70-
log( "Fetched Settings: $settingsString")
95+
log("Fetched Settings: $settingsString")
7196
LenientJson.decodeFromString(settingsString)
7297
} catch (ex: Exception) {
73-
Analytics.segmentLog("${ex.message}: failed to fetch settings", kind = LogFilterKind.ERROR)
98+
Analytics.segmentLog(
99+
"${ex.message}: failed to fetch settings",
100+
kind = LogFilterKind.ERROR
101+
)
74102
null
75103
}
76104

@@ -79,6 +107,7 @@ suspend fun Analytics.checkSettings() {
79107
log("Dispatching update settings on ${Thread.currentThread().name}")
80108
store.dispatch(System.UpdateSettingsAction(settingsObj), System::class)
81109
update(settingsObj, updateType)
110+
store.dispatch(System.ToggleSettingsDispatch(dispatched = true), System::class)
82111
}
83112

84113
// we're good to go back to a running state.

0 commit comments

Comments
 (0)