Skip to content
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

feat: add option to disable screen view usage #474

Merged
merged 6 commits into from
Dec 17, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
38 changes: 36 additions & 2 deletions datapipelines/api/datapipelines.api
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
public final class io/customer/datapipelines/config/DataPipelinesModuleConfig : io/customer/sdk/core/module/CustomerIOModuleConfig {
public fun <init> (Ljava/lang/String;Lio/customer/sdk/data/model/Region;Ljava/lang/String;Ljava/lang/String;IILjava/util/List;ZZZZLjava/lang/String;)V
public synthetic fun <init> (Ljava/lang/String;Lio/customer/sdk/data/model/Region;Ljava/lang/String;Ljava/lang/String;IILjava/util/List;ZZZZLjava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (Ljava/lang/String;Lio/customer/sdk/data/model/Region;Ljava/lang/String;Ljava/lang/String;IILjava/util/List;ZZZZLjava/lang/String;Lio/customer/datapipelines/config/ScreenView;)V
public synthetic fun <init> (Ljava/lang/String;Lio/customer/sdk/data/model/Region;Ljava/lang/String;Ljava/lang/String;IILjava/util/List;ZZZZLjava/lang/String;Lio/customer/datapipelines/config/ScreenView;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun getApiHost ()Ljava/lang/String;
public final fun getAutoAddCustomerIODestination ()Z
public final fun getAutoTrackActivityScreens ()Z
Expand All @@ -11,9 +11,24 @@ public final class io/customer/datapipelines/config/DataPipelinesModuleConfig :
public final fun getFlushInterval ()I
public final fun getFlushPolicies ()Ljava/util/List;
public final fun getMigrationSiteId ()Ljava/lang/String;
public final fun getScreenViewUse ()Lio/customer/datapipelines/config/ScreenView;
public final fun getTrackApplicationLifecycleEvents ()Z
}

public final class io/customer/datapipelines/config/ScreenView : java/lang/Enum {
public static final field Analytics Lio/customer/datapipelines/config/ScreenView;
public static final field Companion Lio/customer/datapipelines/config/ScreenView$Companion;
public static final field InApp Lio/customer/datapipelines/config/ScreenView;
public static fun valueOf (Ljava/lang/String;)Lio/customer/datapipelines/config/ScreenView;
public static fun values ()[Lio/customer/datapipelines/config/ScreenView;
}

public final class io/customer/datapipelines/config/ScreenView$Companion {
public final fun getScreenView (Ljava/lang/String;)Lio/customer/datapipelines/config/ScreenView;
public final fun getScreenView (Ljava/lang/String;Lio/customer/datapipelines/config/ScreenView;)Lio/customer/datapipelines/config/ScreenView;
public static synthetic fun getScreenView$default (Lio/customer/datapipelines/config/ScreenView$Companion;Ljava/lang/String;Lio/customer/datapipelines/config/ScreenView;ILjava/lang/Object;)Lio/customer/datapipelines/config/ScreenView;
}

public final class io/customer/datapipelines/extensions/JsonExtensionsKt {
public static final fun toJsonArray (Lorg/json/JSONArray;)Lkotlinx/serialization/json/JsonArray;
public static final fun toJsonObject (Lorg/json/JSONObject;)Lkotlinx/serialization/json/JsonObject;
Expand Down Expand Up @@ -136,6 +151,24 @@ public final class io/customer/datapipelines/plugins/PluginExtensionsKt {
public static final fun findInContextAtPath (Lcom/segment/analytics/kotlin/core/BaseEvent;Ljava/lang/String;)Ljava/util/List;
}

public final class io/customer/datapipelines/plugins/ScreenFilterPlugin : com/segment/analytics/kotlin/core/platform/EventPlugin {
public field analytics Lcom/segment/analytics/kotlin/core/Analytics;
public fun <init> (Lio/customer/datapipelines/config/ScreenView;)V
public fun alias (Lcom/segment/analytics/kotlin/core/AliasEvent;)Lcom/segment/analytics/kotlin/core/BaseEvent;
public fun execute (Lcom/segment/analytics/kotlin/core/BaseEvent;)Lcom/segment/analytics/kotlin/core/BaseEvent;
public fun flush ()V
public fun getAnalytics ()Lcom/segment/analytics/kotlin/core/Analytics;
public fun getType ()Lcom/segment/analytics/kotlin/core/platform/Plugin$Type;
public fun group (Lcom/segment/analytics/kotlin/core/GroupEvent;)Lcom/segment/analytics/kotlin/core/BaseEvent;
public fun identify (Lcom/segment/analytics/kotlin/core/IdentifyEvent;)Lcom/segment/analytics/kotlin/core/BaseEvent;
public fun reset ()V
public fun screen (Lcom/segment/analytics/kotlin/core/ScreenEvent;)Lcom/segment/analytics/kotlin/core/BaseEvent;
public fun setAnalytics (Lcom/segment/analytics/kotlin/core/Analytics;)V
public fun setup (Lcom/segment/analytics/kotlin/core/Analytics;)V
public fun track (Lcom/segment/analytics/kotlin/core/TrackEvent;)Lcom/segment/analytics/kotlin/core/BaseEvent;
public fun update (Lcom/segment/analytics/kotlin/core/Settings;Lcom/segment/analytics/kotlin/core/platform/Plugin$UpdateType;)V
}

public final class io/customer/datapipelines/plugins/StringExtensionsKt {
public static final fun getScreenNameFromActivity (Ljava/lang/String;)Ljava/lang/String;
}
Expand Down Expand Up @@ -183,6 +216,7 @@ public final class io/customer/sdk/CustomerIOBuilder {
public final fun logLevel (Lio/customer/sdk/core/util/CioLogLevel;)Lio/customer/sdk/CustomerIOBuilder;
public final fun migrationSiteId (Ljava/lang/String;)Lio/customer/sdk/CustomerIOBuilder;
public final fun region (Lio/customer/sdk/data/model/Region;)Lio/customer/sdk/CustomerIOBuilder;
public final fun screenViewUse (Lio/customer/datapipelines/config/ScreenView;)Lio/customer/sdk/CustomerIOBuilder;
public final fun trackApplicationLifecycleEvents (Z)Lio/customer/sdk/CustomerIOBuilder;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ class DataPipelinesModuleConfig(
// Track screen views for Activities
val autoTrackActivityScreens: Boolean,
// Configuration options required for migration from earlier versions
val migrationSiteId: String? = null
val migrationSiteId: String? = null,
// Determines how SDK should handle screen view events
val screenViewUse: ScreenView
) : CustomerIOModuleConfig {
val apiHost: String = apiHostOverride ?: region.apiHost()
val cdnHost: String = cdnHostOverride ?: region.cdnHost()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package io.customer.datapipelines.config

/**
* Enum class to define how CustomerIO SDK should handle screen view events.
*/
enum class ScreenView {
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this mean it's a mutually exclusive choice? I can either choose analytics or in-app? But what if a customer needs both?

Copy link
Contributor

Choose a reason for hiding this comment

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

Curious why we are opting for making this an enum instead of:

  • Boolean to allow screen analytics
  • If in app is enabled, we always use screen views for in-app

/**
* Screen view events are sent to our back end servers for analytics purposes.
mrehan27 marked this conversation as resolved.
Show resolved Hide resolved
*/
Analytics,

/**
* Screen view events are kept on device only. They are used to display in-app messages based on page rules. Events are not sent to our back end servers.
*/
InApp;

companion object {
/**
* Returns the [ScreenView] enum constant for the given name.
* Returns fallback if the specified enum type has no constant with the given name.
* Defaults to [Analytics].
*/
@JvmOverloads
fun getScreenView(screenView: String?, fallback: ScreenView = Analytics): ScreenView {

Check warning on line 24 in datapipelines/src/main/kotlin/io/customer/datapipelines/config/ScreenView.kt

View check run for this annotation

Codecov / codecov/patch

datapipelines/src/main/kotlin/io/customer/datapipelines/config/ScreenView.kt#L24

Added line #L24 was not covered by tests
return values().firstOrNull { it.name.equals(screenView, ignoreCase = true) } ?: fallback
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package io.customer.datapipelines.plugins

import com.segment.analytics.kotlin.core.Analytics
import com.segment.analytics.kotlin.core.BaseEvent
import com.segment.analytics.kotlin.core.ScreenEvent
import com.segment.analytics.kotlin.core.platform.EventPlugin
import com.segment.analytics.kotlin.core.platform.Plugin
import io.customer.datapipelines.config.ScreenView

/**
* Plugin to filter screen events based on the configuration provided by customer app.
* This plugin is used to filter out screen events that should not be processed further.
*/
class ScreenFilterPlugin(private val screenViewUse: ScreenView) : EventPlugin {
mrehan27 marked this conversation as resolved.
Show resolved Hide resolved
override lateinit var analytics: Analytics
override val type: Plugin.Type = Plugin.Type.Enrichment

override fun screen(payload: ScreenEvent): BaseEvent? {
// Filter out screen events based on the configuration provided by customer app
// Using when expression so it enforce right check for all possible values of ScreenView in future
return when (screenViewUse) {
ScreenView.Analytics -> payload
// Do not send screen events to server if ScreenView is not Analytics
ScreenView.InApp -> null
}
}
}
4 changes: 4 additions & 0 deletions datapipelines/src/main/kotlin/io/customer/sdk/CustomerIO.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import io.customer.datapipelines.plugins.AutomaticActivityScreenTrackingPlugin
import io.customer.datapipelines.plugins.ContextPlugin
import io.customer.datapipelines.plugins.CustomerIODestination
import io.customer.datapipelines.plugins.DataPipelinePublishedEvents
import io.customer.datapipelines.plugins.ScreenFilterPlugin
import io.customer.datapipelines.util.EventNames
import io.customer.sdk.communication.Event
import io.customer.sdk.communication.subscribe
Expand Down Expand Up @@ -121,6 +122,9 @@ class CustomerIO private constructor(
// Add plugin to publish events to EventBus for other modules to consume
analytics.add(DataPipelinePublishedEvents())

// Add plugin to filter events based on SDK configuration
analytics.add(ScreenFilterPlugin(moduleConfig.screenViewUse))

// subscribe to journey events emitted from push/in-app module to send them via data pipelines
subscribeToJourneyEvents()
// if profile is already identified, republish identifier for late-added modules.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package io.customer.sdk
import android.app.Application
import com.segment.analytics.kotlin.core.platform.policies.FlushPolicy
import io.customer.datapipelines.config.DataPipelinesModuleConfig
import io.customer.datapipelines.config.ScreenView
import io.customer.sdk.core.di.SDKComponent
import io.customer.sdk.core.di.setupAndroidComponent
import io.customer.sdk.core.module.CustomerIOModule
Expand Down Expand Up @@ -69,6 +70,9 @@ class CustomerIOBuilder(
// Configuration options required for migration from earlier versions
private var migrationSiteId: String? = null

// Determines how SDK should handle screen view events
private var screenViewUse: ScreenView = ScreenView.Analytics

/**
* Specifies the log level for the SDK.
* Default value is [CioLogLevel.ERROR].
Expand Down Expand Up @@ -167,6 +171,16 @@ class CustomerIOBuilder(
return this
}

/**
* Set the screen view configuration for the SDK.
*
* @see ScreenView for more details.
*/
fun screenViewUse(screenView: ScreenView): CustomerIOBuilder {
this.screenViewUse = screenView
return this
}

fun <Config : CustomerIOModuleConfig> addCustomerIOModule(module: CustomerIOModule<Config>): CustomerIOBuilder {
registeredModules.add(module)
return this
Expand All @@ -191,7 +205,8 @@ class CustomerIOBuilder(
trackApplicationLifecycleEvents = trackApplicationLifecycleEvents,
autoTrackDeviceAttributes = autoTrackDeviceAttributes,
autoTrackActivityScreens = autoTrackActivityScreens,
migrationSiteId = migrationSiteId
migrationSiteId = migrationSiteId,
screenViewUse = screenViewUse
)

// Initialize CustomerIO instance before initializing the modules
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package io.customer.datapipelines.plugins

import io.customer.commontest.config.TestConfig
import io.customer.commontest.extensions.random
import io.customer.datapipelines.config.ScreenView
import io.customer.datapipelines.testutils.core.DataPipelinesTestConfig
import io.customer.datapipelines.testutils.core.JUnitTest
import io.customer.datapipelines.testutils.core.testConfiguration
import io.customer.datapipelines.testutils.extensions.shouldMatchTo
import io.customer.datapipelines.testutils.utils.OutputReaderPlugin
import io.customer.datapipelines.testutils.utils.screenEvents
import io.customer.sdk.data.model.CustomAttributes
import org.amshove.kluent.shouldBeEmpty
import org.amshove.kluent.shouldBeEqualTo
import org.amshove.kluent.shouldHaveSingleItem
import org.junit.jupiter.api.Test

class ScreenFilterPluginTest : JUnitTest() {
private lateinit var outputReaderPlugin: OutputReaderPlugin

override fun setup(testConfig: TestConfig) {
// Keep setup empty to avoid calling super.setup() as it will initialize the SDK
// and we want to test the SDK with different configurations in each test
}

private fun setupWithConfig(screenViewUse: ScreenView, testConfig: DataPipelinesTestConfig = testConfiguration {}) {
super.setup(
testConfiguration {
analytics { add(ScreenFilterPlugin(screenViewUse = screenViewUse)) }
} + testConfig
)

outputReaderPlugin = OutputReaderPlugin()
analytics.add(outputReaderPlugin)
}

@Test
fun process_givenScreenViewUseAnalytics_expectScreenEventWithoutPropertiesProcessed() {
setupWithConfig(screenViewUse = ScreenView.Analytics)

val givenScreenTitle = String.random
sdkInstance.screen(givenScreenTitle)

val result = outputReaderPlugin.screenEvents.shouldHaveSingleItem()
result.name shouldBeEqualTo givenScreenTitle
result.properties.shouldBeEmpty()
}

@Test
fun process_givenScreenViewUseAnalytics_expectScreenEventWithPropertiesProcessed() {
setupWithConfig(screenViewUse = ScreenView.Analytics)

val givenScreenTitle = String.random
val givenProperties: CustomAttributes = mapOf("source" to "push", "discount" to 10)
sdkInstance.screen(givenScreenTitle, givenProperties)

val screenEvent = outputReaderPlugin.screenEvents.shouldHaveSingleItem()
screenEvent.name shouldBeEqualTo givenScreenTitle
screenEvent.properties shouldMatchTo givenProperties
}

@Test
fun process_givenScreenViewUseInApp_expectAllScreenEventsIgnored() {
setupWithConfig(screenViewUse = ScreenView.InApp)

for (i in 1..5) {
sdkInstance.screen(String.random)
}

outputReaderPlugin.allEvents.shouldBeEmpty()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import io.customer.commontest.extensions.assertCalledNever
import io.customer.commontest.extensions.assertCalledOnce
import io.customer.commontest.extensions.random
import io.customer.commontest.module.CustomerIOGenericModule
import io.customer.datapipelines.config.ScreenView
import io.customer.datapipelines.plugins.AutomaticActivityScreenTrackingPlugin
import io.customer.datapipelines.plugins.CustomerIODestination
import io.customer.datapipelines.plugins.DataPipelinePublishedEvents
import io.customer.datapipelines.plugins.ScreenFilterPlugin
import io.customer.sdk.CustomerIO
import io.customer.sdk.CustomerIOBuilder
import io.customer.sdk.core.di.SDKComponent
Expand Down Expand Up @@ -105,13 +107,15 @@ class CustomerIOBuilderTest : RobolectricTest() {
dataPipelinesModuleConfig.apiHost shouldBe "cdp.customer.io/v1"
dataPipelinesModuleConfig.cdnHost shouldBe "cdp.customer.io/v1"
dataPipelinesModuleConfig.autoAddCustomerIODestination shouldBe true
dataPipelinesModuleConfig.screenViewUse shouldBe ScreenView.Analytics
}

@Test
fun build_givenConfiguration_expectSameDataPipelinesModuleConfig() {
val givenCdpApiKey = String.random
val givenMigrationSiteId = String.random
val givenRegion = Region.EU
val givenScreenViewUse = ScreenView.InApp

createCustomerIOBuilder(givenCdpApiKey)
.logLevel(CioLogLevel.DEBUG)
Expand All @@ -123,6 +127,7 @@ class CustomerIOBuilderTest : RobolectricTest() {
.flushAt(100)
.flushInterval(2)
.flushPolicies(emptyList())
.screenViewUse(givenScreenViewUse)
.build()

// verify the customerIOBuilder config with DataPipelinesModuleConfig
Expand All @@ -137,6 +142,7 @@ class CustomerIOBuilderTest : RobolectricTest() {
dataPipelinesModuleConfig.flushInterval shouldBe 2
dataPipelinesModuleConfig.apiHost shouldBe "cdp-eu.customer.io/v1"
dataPipelinesModuleConfig.cdnHost shouldBe "cdp-eu.customer.io/v1"
dataPipelinesModuleConfig.screenViewUse shouldBe givenScreenViewUse

// verify the shared logger has updated log level
SDKComponent.logger.logLevel shouldBe CioLogLevel.DEBUG
Expand Down Expand Up @@ -165,6 +171,13 @@ class CustomerIOBuilderTest : RobolectricTest() {
CustomerIO.instance().analytics.find(AutomaticActivityScreenTrackingPlugin::class) shouldBe null
}

@Test
fun build_givenModuleInitialized_expectScreenFilterPluginPluginAdded() {
createCustomerIOBuilder().build()

CustomerIO.instance().analytics.find(ScreenFilterPlugin::class) shouldNotBe null
}

@Test
fun build_givenHostConfiguration_expectCorrectHostDataPipelinesModuleConfig() {
val givenRegion = Region.EU
Expand Down
Loading