Skip to content

Commit a414dd9

Browse files
authored
support enable/disable analytics (#169)
* remove print stacktrace * check if deeplinks are hierarchical * support enable/disable analytics * add unit tests * fix android unit tests * remove redundant variable
1 parent 4e595de commit a414dd9

File tree

12 files changed

+179
-48
lines changed

12 files changed

+179
-48
lines changed

android/src/main/java/com/segment/analytics/kotlin/android/utilities/DeepLinkUtils.kt

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,13 @@ class DeepLinkUtils(val analytics: Analytics) {
1919
put("referrer", it)
2020
}
2121

22-
val uri = intent.data
23-
uri?.let {
24-
for (parameter in uri.queryParameterNames) {
25-
val value = uri.getQueryParameter(parameter)
26-
if (value != null && value.trim().isNotEmpty()) {
27-
put(parameter, value)
22+
intent.data?.let { uri ->
23+
if (uri.isHierarchical) {
24+
for (parameter in uri.queryParameterNames) {
25+
val value = uri.getQueryParameter(parameter)
26+
if (value != null && value.trim().isNotEmpty()) {
27+
put(parameter, value)
28+
}
2829
}
2930
}
3031
put("url", uri.toString())

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@ class StorageTests {
5959
configuration = Configuration("123"),
6060
settings = Settings(),
6161
running = false,
62-
initialSettingsDispatched = false
62+
initialSettingsDispatched = false,
63+
enabled = true
6364
)
6465
)
6566

@@ -115,7 +116,8 @@ class StorageTests {
115116
edgeFunction = emptyJsonObject
116117
),
117118
running = false,
118-
initialSettingsDispatched = false
119+
initialSettingsDispatched = false,
120+
enabled = true
119121
)
120122
}
121123
}

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,14 @@ open class Analytics protected constructor(
5555

5656
internal var userInfo: UserInfo = UserInfo.defaultState(storage)
5757

58+
var enabled = true
59+
set(value) {
60+
field = value
61+
analyticsScope.launch(analyticsDispatcher) {
62+
store.dispatch(System.ToggleEnabledAction(value), System::class)
63+
}
64+
}
65+
5866
companion object {
5967
var debugLogsEnabled: Boolean = false
6068

@@ -465,6 +473,8 @@ open class Analytics protected constructor(
465473
}
466474

467475
fun process(event: BaseEvent) {
476+
if (!enabled) return
477+
468478
event.applyBaseData()
469479

470480
log("applying base attributes on ${Thread.currentThread().name}")

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

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ data class System(
2020
var configuration: Configuration = Configuration(""),
2121
var settings: Settings?,
2222
var running: Boolean,
23-
var initialSettingsDispatched: Boolean
23+
var initialSettingsDispatched: Boolean,
24+
var enabled: Boolean
2425
) : State {
2526

2627
companion object {
@@ -37,7 +38,8 @@ data class System(
3738
configuration = configuration,
3839
settings = settings,
3940
running = false,
40-
initialSettingsDispatched = false
41+
initialSettingsDispatched = false,
42+
enabled = true
4143
)
4244
}
4345
}
@@ -48,7 +50,8 @@ data class System(
4850
state.configuration,
4951
settings,
5052
state.running,
51-
state.initialSettingsDispatched
53+
state.initialSettingsDispatched,
54+
state.enabled
5255
)
5356
}
5457
}
@@ -59,7 +62,8 @@ data class System(
5962
state.configuration,
6063
state.settings,
6164
running,
62-
state.initialSettingsDispatched
65+
state.initialSettingsDispatched,
66+
state.enabled
6367
)
6468
}
6569
}
@@ -77,7 +81,8 @@ data class System(
7781
state.configuration,
7882
newSettings,
7983
state.running,
80-
state.initialSettingsDispatched
84+
state.initialSettingsDispatched,
85+
state.enabled
8186
)
8287
}
8388
}
@@ -90,7 +95,20 @@ data class System(
9095
state.configuration,
9196
state.settings,
9297
state.running,
93-
dispatched
98+
dispatched,
99+
state.enabled
100+
)
101+
}
102+
}
103+
104+
class ToggleEnabledAction(val enabled: Boolean): Action<System> {
105+
override fun reduce(state: System): System {
106+
return System(
107+
state.configuration,
108+
state.settings,
109+
state.running,
110+
state.initialSettingsDispatched,
111+
enabled
94112
)
95113
}
96114
}

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

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -18,30 +18,18 @@ import java.util.function.Consumer
1818
* This class is merely a wrapper of {@link Analytics com.segment.analytics.kotlin.core.Analytics}
1919
* for Java compatibility purpose.
2020
*/
21-
class JavaAnalytics private constructor() {
21+
class JavaAnalytics(val analytics: Analytics) {
2222

23-
/**
24-
* A constructor that takes a configuration
25-
* @param configuration an instance of configuration that can be build
26-
* through {@link ConfigurationBuilder com.segment.analytics.kotlin.core.compat.ConfigurationBuilder}
27-
*/
28-
constructor(configuration: Configuration): this() {
29-
analytics = Analytics(configuration)
23+
init {
3024
setup(analytics)
3125
}
3226

3327
/**
34-
* A constructor takes an instance of {@link Analytics com.segment.analytics.kotlin.core.Analytics}
35-
* @param analytics an instance of Analytics object.
36-
* This constructor wrappers it and provides a JavaAnalytics for Java compatibility.
28+
* A constructor that takes a configuration
29+
* @param configuration an instance of configuration that can be build
30+
* through {@link ConfigurationBuilder com.segment.analytics.kotlin.core.compat.ConfigurationBuilder}
3731
*/
38-
constructor(analytics: Analytics): this() {
39-
this.analytics = analytics
40-
setup(analytics)
41-
}
42-
43-
internal lateinit var analytics: Analytics
44-
private set
32+
constructor(configuration: Configuration): this(Analytics(configuration))
4533

4634
lateinit var store: Store
4735
private set
@@ -52,6 +40,8 @@ class JavaAnalytics private constructor() {
5240
lateinit var analyticsScope: CoroutineScope
5341
private set
5442

43+
var enabled by analytics::enabled
44+
5545
/**
5646
* The track method is how you record any actions your users perform. Each action is known by a
5747
* name, like 'Purchased a T-Shirt'. You can also record properties specific to those actions.

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

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@ internal class EventPipeline(
2727
var apiHost: String = Constants.DEFAULT_API_HOST
2828
) {
2929

30-
private val writeChannel: Channel<BaseEvent>
30+
private var writeChannel: Channel<BaseEvent>
3131

32-
private val uploadChannel: Channel<String>
32+
private var uploadChannel: Channel<String>
3333

3434
private val httpClient: HTTPClient = HTTPClient(apiKey)
3535

@@ -64,20 +64,30 @@ internal class EventPipeline(
6464
}
6565

6666
fun start() {
67+
if (running) return
6768
running = true
69+
70+
// avoid to re-establish a channel if the pipeline just gets created
71+
if (writeChannel.isClosedForSend || writeChannel.isClosedForReceive) {
72+
writeChannel = Channel(UNLIMITED)
73+
uploadChannel = Channel(UNLIMITED)
74+
}
75+
6876
schedule()
6977
write()
7078
upload()
7179
}
7280

7381
fun stop() {
82+
if (!running) return
83+
running = false
84+
7485
uploadChannel.cancel()
7586
writeChannel.cancel()
7687
unschedule()
77-
running = false
7888
}
7989

80-
fun stringifyBaseEvent(payload: BaseEvent): String {
90+
internal fun stringifyBaseEvent(payload: BaseEvent): String {
8191
val finalPayload = EncodeDefaultsJson.encodeToJsonElement(payload)
8292
.jsonObject.filterNot { (k, v) ->
8393
// filter out empty userId and traits values
@@ -186,7 +196,6 @@ internal class EventPipeline(
186196
| msg=${e.message}
187197
""".trimMargin(), kind = LogKind.ERROR
188198
)
189-
e.printStackTrace()
190199
}
191200

192201
return shouldCleanup

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

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ import com.segment.analytics.kotlin.core.platform.VersionedPlugin
88
import com.segment.analytics.kotlin.core.platform.policies.CountBasedFlushPolicy
99
import com.segment.analytics.kotlin.core.platform.policies.FlushPolicy
1010
import com.segment.analytics.kotlin.core.platform.policies.FrequencyFlushPolicy
11+
import kotlinx.coroutines.launch
1112
import kotlinx.serialization.Serializable
13+
import sovran.kotlin.Subscriber
1214

1315
@Serializable
1416
data class SegmentSettings(
@@ -23,9 +25,9 @@ data class SegmentSettings(
2325
* - We store events into a file with the batch api format (@link {https://segment.com/docs/connections/sources/catalog/libraries/server/http-api/#batch})
2426
* - We upload events on a dedicated thread using the batch api
2527
*/
26-
class SegmentDestination: DestinationPlugin(), VersionedPlugin {
28+
class SegmentDestination: DestinationPlugin(), VersionedPlugin, Subscriber {
2729

28-
private lateinit var pipeline: EventPipeline
30+
private var pipeline: EventPipeline? = null
2931
var flushPolicies: List<FlushPolicy> = emptyList()
3032
override val key: String = "Segment.io"
3133

@@ -56,7 +58,7 @@ class SegmentDestination: DestinationPlugin(), VersionedPlugin {
5658

5759

5860
private fun enqueue(payload: BaseEvent) {
59-
pipeline.put(payload)
61+
pipeline?.put(payload)
6062
}
6163

6264
override fun setup(analytics: Analytics) {
@@ -83,7 +85,15 @@ class SegmentDestination: DestinationPlugin(), VersionedPlugin {
8385
flushPolicies,
8486
configuration.apiHost
8587
)
86-
pipeline.start()
88+
89+
analyticsScope.launch(analyticsDispatcher) {
90+
store.subscribe(
91+
subscriber = this@SegmentDestination,
92+
stateClazz = System::class,
93+
initialState = true,
94+
handler = this@SegmentDestination::onEnableToggled
95+
)
96+
}
8797
}
8898
}
8999

@@ -92,16 +102,25 @@ class SegmentDestination: DestinationPlugin(), VersionedPlugin {
92102
if (settings.hasIntegrationSettings(this)) {
93103
// only populate the apiHost value if it exists
94104
settings.destinationSettings<SegmentSettings>(key)?.apiHost?.let {
95-
pipeline.apiHost = it
105+
pipeline?.apiHost = it
96106
}
97107
}
98108
}
99109

100110
override fun flush() {
101-
pipeline.flush()
111+
pipeline?.flush()
102112
}
103113

104114
override fun version(): String {
105115
return Constants.LIBRARY_VERSION
106116
}
117+
118+
internal fun onEnableToggled(state: System) {
119+
if (state.enabled) {
120+
pipeline?.start()
121+
}
122+
else {
123+
pipeline?.stop()
124+
}
125+
}
107126
}

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

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@ package com.segment.analytics.kotlin.core
33
import com.segment.analytics.kotlin.core.platform.DestinationPlugin
44
import com.segment.analytics.kotlin.core.platform.Plugin
55
import com.segment.analytics.kotlin.core.platform.plugins.ContextPlugin
6+
import com.segment.analytics.kotlin.core.platform.plugins.SegmentDestination
67
import com.segment.analytics.kotlin.core.utilities.dateTimeNowString
78
import com.segment.analytics.kotlin.core.utils.StubPlugin
89
import com.segment.analytics.kotlin.core.utils.TestRunPlugin
910
import com.segment.analytics.kotlin.core.utils.clearPersistentStorage
1011
import com.segment.analytics.kotlin.core.utils.mockHTTPClient
1112
import com.segment.analytics.kotlin.core.utils.testAnalytics
12-
import io.mockk.coEvery
1313
import io.mockk.every
1414
import io.mockk.mockk
1515
import io.mockk.mockkStatic
@@ -519,6 +519,32 @@ class AnalyticsTests {
519519
assertEquals(Constants.LIBRARY_VERSION, analytics.version())
520520
}
521521

522+
@Test
523+
fun `disable analytics prevents event being processed`() {
524+
val segmentDestination = spyk(SegmentDestination())
525+
analytics.add(segmentDestination)
526+
val state = mutableListOf<System>()
527+
528+
analytics.enabled = false
529+
analytics.track("test")
530+
531+
verify(exactly = 0) {
532+
segmentDestination.track(any())
533+
segmentDestination.execute(any())
534+
}
535+
verify { segmentDestination.onEnableToggled(capture(state)) }
536+
assertEquals(false, state[1].enabled)
537+
538+
analytics.enabled = true
539+
analytics.track("test")
540+
verify(exactly = 1) {
541+
segmentDestination.track(any())
542+
segmentDestination.execute(any())
543+
}
544+
verify { segmentDestination.onEnableToggled(capture(state)) }
545+
assertEquals(true, state[2].enabled)
546+
}
547+
522548
private fun BaseEvent.populate() = apply {
523549
anonymousId = "qwerty-qwerty-123"
524550
messageId = "qwerty-qwerty-123"

0 commit comments

Comments
 (0)