Skip to content

support enable/disable analytics #169

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jun 5, 2023
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
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,13 @@ class DeepLinkUtils(val analytics: Analytics) {
put("referrer", it)
}

val uri = intent.data
uri?.let {
for (parameter in uri.queryParameterNames) {
val value = uri.getQueryParameter(parameter)
if (value != null && value.trim().isNotEmpty()) {
put(parameter, value)
intent.data?.let { uri ->
if (uri.isHierarchical) {
for (parameter in uri.queryParameterNames) {
val value = uri.getQueryParameter(parameter)
Comment on lines +24 to +25
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe use "it" instead of "uri" to keep consistent?

if (value != null && value.trim().isNotEmpty()) {
put(parameter, value)
}
}
}
put("url", uri.toString())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ class StorageTests {
configuration = Configuration("123"),
settings = Settings(),
running = false,
initialSettingsDispatched = false
initialSettingsDispatched = false,
enabled = true
)
)

Expand Down Expand Up @@ -115,7 +116,8 @@ class StorageTests {
edgeFunction = emptyJsonObject
),
running = false,
initialSettingsDispatched = false
initialSettingsDispatched = false,
enabled = true
)
}
}
Expand Down
10 changes: 10 additions & 0 deletions core/src/main/java/com/segment/analytics/kotlin/core/Analytics.kt
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,14 @@ open class Analytics protected constructor(

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

var enabled = true
set(value) {
field = value
analyticsScope.launch(analyticsDispatcher) {
store.dispatch(System.ToggleEnabledAction(value), System::class)
}
}

companion object {
var debugLogsEnabled: Boolean = false

Expand Down Expand Up @@ -465,6 +473,8 @@ open class Analytics protected constructor(
}

fun process(event: BaseEvent) {
if (!enabled) return

event.applyBaseData()

log("applying base attributes on ${Thread.currentThread().name}")
Expand Down
30 changes: 24 additions & 6 deletions core/src/main/java/com/segment/analytics/kotlin/core/State.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ data class System(
var configuration: Configuration = Configuration(""),
var settings: Settings?,
var running: Boolean,
var initialSettingsDispatched: Boolean
var initialSettingsDispatched: Boolean,
var enabled: Boolean
) : State {

companion object {
Expand All @@ -37,7 +38,8 @@ data class System(
configuration = configuration,
settings = settings,
running = false,
initialSettingsDispatched = false
initialSettingsDispatched = false,
enabled = true
)
}
}
Expand All @@ -48,7 +50,8 @@ data class System(
state.configuration,
settings,
state.running,
state.initialSettingsDispatched
state.initialSettingsDispatched,
state.enabled
)
}
}
Expand All @@ -59,7 +62,8 @@ data class System(
state.configuration,
state.settings,
running,
state.initialSettingsDispatched
state.initialSettingsDispatched,
state.enabled
)
}
}
Expand All @@ -77,7 +81,8 @@ data class System(
state.configuration,
newSettings,
state.running,
state.initialSettingsDispatched
state.initialSettingsDispatched,
state.enabled
)
}
}
Expand All @@ -90,7 +95,20 @@ data class System(
state.configuration,
state.settings,
state.running,
dispatched
dispatched,
state.enabled
)
}
}

class ToggleEnabledAction(val enabled: Boolean): Action<System> {
override fun reduce(state: System): System {
return System(
state.configuration,
state.settings,
state.running,
state.initialSettingsDispatched,
enabled
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,30 +18,18 @@ import java.util.function.Consumer
* This class is merely a wrapper of {@link Analytics com.segment.analytics.kotlin.core.Analytics}
* for Java compatibility purpose.
*/
class JavaAnalytics private constructor() {
class JavaAnalytics(val analytics: Analytics) {

/**
* A constructor that takes a configuration
* @param configuration an instance of configuration that can be build
* through {@link ConfigurationBuilder com.segment.analytics.kotlin.core.compat.ConfigurationBuilder}
*/
constructor(configuration: Configuration): this() {
analytics = Analytics(configuration)
init {
setup(analytics)
}

/**
* A constructor takes an instance of {@link Analytics com.segment.analytics.kotlin.core.Analytics}
* @param analytics an instance of Analytics object.
* This constructor wrappers it and provides a JavaAnalytics for Java compatibility.
* A constructor that takes a configuration
* @param configuration an instance of configuration that can be build
* through {@link ConfigurationBuilder com.segment.analytics.kotlin.core.compat.ConfigurationBuilder}
*/
constructor(analytics: Analytics): this() {
this.analytics = analytics
setup(analytics)
}

internal lateinit var analytics: Analytics
private set
constructor(configuration: Configuration): this(Analytics(configuration))

lateinit var store: Store
private set
Expand All @@ -52,6 +40,8 @@ class JavaAnalytics private constructor() {
lateinit var analyticsScope: CoroutineScope
private set

var enabled by analytics::enabled

/**
* The track method is how you record any actions your users perform. Each action is known by a
* name, like 'Purchased a T-Shirt'. You can also record properties specific to those actions.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ internal class EventPipeline(
var apiHost: String = Constants.DEFAULT_API_HOST
) {

private val writeChannel: Channel<BaseEvent>
private var writeChannel: Channel<BaseEvent>

private val uploadChannel: Channel<String>
private var uploadChannel: Channel<String>

private val httpClient: HTTPClient = HTTPClient(apiKey)

Expand Down Expand Up @@ -64,20 +64,30 @@ internal class EventPipeline(
}

fun start() {
if (running) return
running = true

// avoid to re-establish a channel if the pipeline just gets created
if (writeChannel.isClosedForSend || writeChannel.isClosedForReceive) {
writeChannel = Channel(UNLIMITED)
uploadChannel = Channel(UNLIMITED)
}
Comment on lines +71 to +74
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we use a new channel is possible we leave messages that are stuck in the pipe and loose them when we create a new pipe?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah. that's possible. but we're using channel.cancel in stop, buffered events are going to be cleared anyway

the only workaround is not to close/cancel channel at all, but that increases the complexity, since we would have to distinguish pause vs stop. if stop, we would want to close anything ongoing completely, for example on application shutdown.


schedule()
write()
upload()
}

fun stop() {
if (!running) return
running = false

uploadChannel.cancel()
writeChannel.cancel()
unschedule()
running = false
}

fun stringifyBaseEvent(payload: BaseEvent): String {
internal fun stringifyBaseEvent(payload: BaseEvent): String {
val finalPayload = EncodeDefaultsJson.encodeToJsonElement(payload)
.jsonObject.filterNot { (k, v) ->
// filter out empty userId and traits values
Expand Down Expand Up @@ -186,7 +196,6 @@ internal class EventPipeline(
| msg=${e.message}
""".trimMargin(), kind = LogKind.ERROR
)
e.printStackTrace()
}

return shouldCleanup
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import com.segment.analytics.kotlin.core.platform.VersionedPlugin
import com.segment.analytics.kotlin.core.platform.policies.CountBasedFlushPolicy
import com.segment.analytics.kotlin.core.platform.policies.FlushPolicy
import com.segment.analytics.kotlin.core.platform.policies.FrequencyFlushPolicy
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
import sovran.kotlin.Subscriber

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

private lateinit var pipeline: EventPipeline
private var pipeline: EventPipeline? = null
var flushPolicies: List<FlushPolicy> = emptyList()
override val key: String = "Segment.io"

Expand Down Expand Up @@ -56,7 +58,7 @@ class SegmentDestination: DestinationPlugin(), VersionedPlugin {


private fun enqueue(payload: BaseEvent) {
pipeline.put(payload)
pipeline?.put(payload)
}

override fun setup(analytics: Analytics) {
Expand All @@ -83,7 +85,15 @@ class SegmentDestination: DestinationPlugin(), VersionedPlugin {
flushPolicies,
configuration.apiHost
)
pipeline.start()

analyticsScope.launch(analyticsDispatcher) {
store.subscribe(
subscriber = this@SegmentDestination,
stateClazz = System::class,
initialState = true,
handler = this@SegmentDestination::onEnableToggled
)
}
}
}

Expand All @@ -92,16 +102,25 @@ class SegmentDestination: DestinationPlugin(), VersionedPlugin {
if (settings.hasIntegrationSettings(this)) {
// only populate the apiHost value if it exists
settings.destinationSettings<SegmentSettings>(key)?.apiHost?.let {
pipeline.apiHost = it
pipeline?.apiHost = it
}
}
}

override fun flush() {
pipeline.flush()
pipeline?.flush()
}

override fun version(): String {
return Constants.LIBRARY_VERSION
}

internal fun onEnableToggled(state: System) {
if (state.enabled) {
pipeline?.start()
}
else {
pipeline?.stop()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ package com.segment.analytics.kotlin.core
import com.segment.analytics.kotlin.core.platform.DestinationPlugin
import com.segment.analytics.kotlin.core.platform.Plugin
import com.segment.analytics.kotlin.core.platform.plugins.ContextPlugin
import com.segment.analytics.kotlin.core.platform.plugins.SegmentDestination
import com.segment.analytics.kotlin.core.utilities.dateTimeNowString
import com.segment.analytics.kotlin.core.utils.StubPlugin
import com.segment.analytics.kotlin.core.utils.TestRunPlugin
import com.segment.analytics.kotlin.core.utils.clearPersistentStorage
import com.segment.analytics.kotlin.core.utils.mockHTTPClient
import com.segment.analytics.kotlin.core.utils.testAnalytics
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
Expand Down Expand Up @@ -519,6 +519,32 @@ class AnalyticsTests {
assertEquals(Constants.LIBRARY_VERSION, analytics.version())
}

@Test
fun `disable analytics prevents event being processed`() {
val segmentDestination = spyk(SegmentDestination())
analytics.add(segmentDestination)
val state = mutableListOf<System>()

analytics.enabled = false
analytics.track("test")

verify(exactly = 0) {
segmentDestination.track(any())
segmentDestination.execute(any())
}
verify { segmentDestination.onEnableToggled(capture(state)) }
assertEquals(false, state[1].enabled)

analytics.enabled = true
analytics.track("test")
verify(exactly = 1) {
segmentDestination.track(any())
segmentDestination.execute(any())
}
verify { segmentDestination.onEnableToggled(capture(state)) }
assertEquals(true, state[2].enabled)
}

private fun BaseEvent.populate() = apply {
anonymousId = "qwerty-qwerty-123"
messageId = "qwerty-qwerty-123"
Expand Down
Loading