Skip to content
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
2 changes: 1 addition & 1 deletion lib/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ android {

dependencies {
implementation("com.segment:sovran-kotlin:1.3.1")
implementation("com.segment.analytics.kotlin:android:1.16.1")
implementation("com.segment.analytics.kotlin:android:1.16.3")
implementation("androidx.multidex:multidex:2.0.1")
implementation("androidx.core:core-ktx:1.10.1")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ import com.segment.analytics.kotlin.destinations.consent.Constants.EVENT_SEGMENT
import com.segment.analytics.kotlin.destinations.consent.Constants.SEGMENT_IO_KEY
import kotlinx.serialization.json.JsonObject
import sovran.kotlin.SynchronousStore
import com.segment.analytics.kotlin.destinations.consent.Constants.CATEGORY_PREFERENCE_KEY
import com.segment.analytics.kotlin.destinations.consent.Constants.CONSENT_KEY


internal const val CONSENT_SETTINGS = "consent"
internal const val CATEGORY_PREFERENCE = "categoryPreference"

open class ConsentBlocker(
var destinationKey: String,
Expand All @@ -30,7 +30,7 @@ open class ConsentBlocker(

if (requiredConsentCategories != null && requiredConsentCategories.isNotEmpty()) {

val consentJsonArray = getConsentCategoriesFromEvent(event)
val consentJsonArray = getConsentedCategoriesFromEvent(event)

// Look for a missing consent category
requiredConsentCategories.forEach {
Expand All @@ -53,13 +53,17 @@ open class ConsentBlocker(
return event
}

private fun getConsentCategoriesFromEvent(event: BaseEvent): Set<String> {
/**
* Returns the set of consented categories in the event. Only categories with set to 'true'
* will be returned.
*/
internal fun getConsentedCategoriesFromEvent(event: BaseEvent): Set<String> {
val consentJsonArray = HashSet<String>()

val consentSettingsJson = event.context[CONSENT_SETTINGS]
val consentSettingsJson = event.context[CONSENT_KEY]
if (consentSettingsJson != null) {
val consentJsonObject = (consentSettingsJson as JsonObject)
val categoryPreferenceJson = consentJsonObject[CATEGORY_PREFERENCE]
val categoryPreferenceJson = consentJsonObject[CATEGORY_PREFERENCE_KEY]
if (categoryPreferenceJson != null) {
val categoryPreferenceJsonObject = categoryPreferenceJson as JsonObject
categoryPreferenceJsonObject.forEach { category, consentGiven ->
Expand All @@ -83,4 +87,22 @@ open class ConsentBlocker(
}


class SegmentConsentBlocker(store: SynchronousStore): ConsentBlocker(SEGMENT_IO_KEY, store) {}
class SegmentConsentBlocker(store: SynchronousStore): ConsentBlocker(SEGMENT_IO_KEY, store) {
override fun execute(event: BaseEvent): BaseEvent? {

val currentState = store.currentState(ConsentState::class)
val hasUnmappedDestinations = currentState?.hasUnmappedDestinations

// IF we have no unmapped destinations and we have not consented to any categories block (drop)
// the event.
if (hasUnmappedDestinations == false) {
val consentedCategoriesSet = getConsentedCategoriesFromEvent(event)
if (consentedCategoriesSet.isEmpty()) {
// Drop the event
return null
}
}

return event
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import com.segment.analytics.kotlin.core.platform.Plugin
import com.segment.analytics.kotlin.core.utilities.getBoolean
import com.segment.analytics.kotlin.core.utilities.safeJsonObject
import com.segment.analytics.kotlin.core.utilities.toJsonElement
import com.segment.analytics.kotlin.destinations.consent.Constants.ALL_CATEGORIES_KEY
import com.segment.analytics.kotlin.destinations.consent.Constants.CATEGORIES_KEY
import com.segment.analytics.kotlin.destinations.consent.Constants.CATEGORY_PREFERENCE_KEY
import com.segment.analytics.kotlin.destinations.consent.Constants.CONSENT_KEY
Expand Down Expand Up @@ -88,6 +89,7 @@ class ConsentManager(

val destinationMapping = mutableMapOf<String, Array<String>>()
var hasUnmappedDestinations = true
val allCategories = mutableListOf<String>()
var enabledAtSegment = true

// Add all mappings
Expand All @@ -110,8 +112,13 @@ class ConsentManager(
it.jsonObject.getBoolean(HAS_UNMAPPED_DESTINATIONS_KEY)
?.let { serverHasUnmappedDestinations ->
println("hasUnmappedDestinations jsonElement: $serverHasUnmappedDestinations")
hasUnmappedDestinations = serverHasUnmappedDestinations == true
hasUnmappedDestinations = serverHasUnmappedDestinations
}

val allCategoriesJson = it.jsonObject.get(ALL_CATEGORIES_KEY)
allCategoriesJson?.let {
it.jsonObject.values.forEach { jsonElement -> allCategories.add(jsonElement.toString())}
}
}
} catch (t: Throwable) {
println("Couldn't parse settings object to check for 'hasUnmappedDestinations'")
Expand All @@ -126,7 +133,7 @@ class ConsentManager(
println("Couldn't parse settings object to check if 'enabledAtSegment'.")
}

return ConsentState(destinationMapping, hasUnmappedDestinations, enabledAtSegment)
return ConsentState(destinationMapping, hasUnmappedDestinations, allCategories, enabledAtSegment)
}

override fun execute(event: BaseEvent): BaseEvent? {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ object Constants {
const val EVENT_SEGMENT_CONSENT_PREFERENCE = "Segment Consent Preference Updated"
const val CONSENT_SETTINGS_KEY = "consentSettings"
const val CONSENT_KEY = "consent"
const val CATEGORY_PREFERENCE_KEY = "categoryPreference"
const val CATEGORY_PREFERENCE_KEY = "categoryPreferences"
const val CATEGORIES_KEY = "categories"
const val ALL_CATEGORIES_KEY = "allCategories"
const val HAS_UNMAPPED_DESTINATIONS_KEY = "hasUnmappedDestinations"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ import sovran.kotlin.State
class ConsentState(
var destinationCategoryMap: Map<String, Array<String>>,
var hasUnmappedDestinations: Boolean,
var allCategories: List<String>,
var enabledAtSegment: Boolean
) : State {

companion object {
val defaultState = ConsentState(mutableMapOf(), true, true)
val defaultState = ConsentState(mutableMapOf(), true, mutableListOf(),true)
}
}

Expand All @@ -20,7 +21,7 @@ class UpdateConsentStateActionFull(var value: ConsentState) : Action<ConsentStat

// New state override any old state.
val newState = ConsentState(
value.destinationCategoryMap, value.hasUnmappedDestinations, value.enabledAtSegment
value.destinationCategoryMap, value.hasUnmappedDestinations, value.allCategories, value.enabledAtSegment
)

return newState
Expand All @@ -29,7 +30,7 @@ class UpdateConsentStateActionFull(var value: ConsentState) : Action<ConsentStat

class UpdateConsentStateActionMappings(var mappings: Map<String, Array<String>>) : Action<ConsentState> {
override fun reduce(state: ConsentState): ConsentState {
val newState = ConsentState(mappings, state.hasUnmappedDestinations, state.enabledAtSegment)
val newState = ConsentState(mappings, state.hasUnmappedDestinations, state.allCategories, state.enabledAtSegment)
return newState
}
}
Expand All @@ -39,7 +40,7 @@ class UpdateConsentStateActionHasUnmappedDestinations(var hasUnmappedDestination

// New state override any old state.
val newState = ConsentState(
state.destinationCategoryMap, hasUnmappedDestinations, state.enabledAtSegment
state.destinationCategoryMap, hasUnmappedDestinations, state.allCategories, state.enabledAtSegment
)

return newState
Expand All @@ -51,7 +52,7 @@ class UpdateConsentStateActionEnabledAtSegment(var enabledAtSegment: Boolean) :

// New state override any old state.
val newState = ConsentState(
state.destinationCategoryMap, state.hasUnmappedDestinations, enabledAtSegment
state.destinationCategoryMap, state.hasUnmappedDestinations, state.allCategories, enabledAtSegment
)

return newState
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,15 @@ class ConsentBlockerTests {
store.provide(ConsentState.defaultState)
var mappings: MutableMap<String, Array<String>> = HashMap()
mappings["foo"] = arrayOf("cat1", "cat2")
val state = ConsentState(mappings, false, true)
val state = ConsentState(mappings, false, mutableListOf<String>(),true)
store.dispatch(UpdateConsentStateActionFull(state), ConsentState::class)
val blockingPlugin = ConsentBlocker("foo", store)

// All categories correct
var stampedEvent = TrackEvent(properties = emptyJsonObject, event = "MyEvent")
stampedEvent.context = buildJsonObject {
put(CONSENT_SETTINGS, buildJsonObject {
put(CATEGORY_PREFERENCE, buildJsonObject {
put(Constants.CONSENT_KEY, buildJsonObject {
put(Constants.CATEGORY_PREFERENCE_KEY, buildJsonObject {
put("cat1", JsonPrimitive(true))
put("cat2", JsonPrimitive(true))
})
Expand All @@ -38,8 +38,8 @@ class ConsentBlockerTests {

stampedEvent = TrackEvent(properties = emptyJsonObject, event = "MyEvent")
stampedEvent.context = buildJsonObject {
put(CONSENT_SETTINGS, buildJsonObject {
put(CATEGORY_PREFERENCE, buildJsonObject {
put(Constants.CONSENT_KEY, buildJsonObject {
put(Constants.CATEGORY_PREFERENCE_KEY, buildJsonObject {
put("cat1", JsonPrimitive(true))
put("cat2", JsonPrimitive(true))
put("cat3", JsonPrimitive(true))
Expand All @@ -56,7 +56,7 @@ class ConsentBlockerTests {
store.provide(ConsentState.defaultState)
var mappings: MutableMap<String, Array<String>> = HashMap()
mappings["foo"] = arrayOf("cat1", "cat2")
val state = ConsentState(mappings, false, true)
val state = ConsentState(mappings, false, mutableListOf<String>(),true)
store.dispatch(UpdateConsentStateActionFull(state), ConsentState::class)
val blockingPlugin = ConsentBlocker("foo", store)

Expand All @@ -68,16 +68,16 @@ class ConsentBlockerTests {

// Context with empty consentSettings
unstamppedEvent.context = buildJsonObject {
put(CONSENT_SETTINGS, emptyJsonObject)
put(Constants.CONSENT_KEY, emptyJsonObject)
}
processedEvent = blockingPlugin.execute(unstamppedEvent)
assertNull(processedEvent)

// Stamped Event with all categories false
var stamppedEvent = TrackEvent(properties = emptyJsonObject, event = "MyEvent")
stamppedEvent.context = buildJsonObject {
put(CONSENT_SETTINGS, buildJsonObject {
put(CATEGORY_PREFERENCE, buildJsonObject {
put(Constants.CONSENT_KEY, buildJsonObject {
put(Constants.CATEGORY_PREFERENCE_KEY, buildJsonObject {
put("cat1", JsonPrimitive(false))
put("cat2", JsonPrimitive(false))
})
Expand All @@ -101,8 +101,8 @@ class ConsentBlockerTests {

var stamppedEvent = TrackEvent(properties = emptyJsonObject, event = "MyEvent")
stamppedEvent.context = buildJsonObject {
put(CONSENT_SETTINGS, buildJsonObject {
put(CATEGORY_PREFERENCE, buildJsonObject {
put(Constants.CONSENT_KEY, buildJsonObject {
put(Constants.CATEGORY_PREFERENCE_KEY, buildJsonObject {
put("cat1", JsonPrimitive(false))
put("cat2", JsonPrimitive(false))
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@ import io.mockk.mockkStatic
import io.mockk.spyk
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.buildJsonArray
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.jsonObject
import org.junit.Before
import org.junit.jupiter.api.Assertions.*
import org.junit.Test
Expand All @@ -36,6 +38,25 @@ class ConsentManagerTests {
private val testDispatcher = UnconfinedTestDispatcher()
private val testScope = TestScope(testDispatcher)

fun createConsentEvent(name: String, consentMap: Map<String, Boolean>, properties: Properties = emptyJsonObject, context: AnalyticsContext = emptyJsonObject): BaseEvent {
var event = TrackEvent(properties, name)
event.context = buildJsonObject {
// Add all context items
context.forEach { prop, elem -> put(prop, elem) }

// Add (potentially overriding from context) the consentMap values
put(Constants.CONSENT_KEY, buildJsonObject {
put(Constants.CATEGORY_PREFERENCE_KEY, buildJsonObject {
consentMap.forEach { category, isConsented ->
put(category, JsonPrimitive(isConsented))
}
})
})
}

return event
}

@Before
fun setUp() {
appContext = spyk(InstrumentationRegistry.getInstrumentation().targetContext)
Expand Down Expand Up @@ -78,12 +99,13 @@ class ConsentManagerTests {
val consentManager = ConsentManager(store, cp)
consentManager.start()

// Refactor
var event = TrackEvent(emptyJsonObject, "MyEvent")
event.context = emptyJsonObject

val expectedContext = buildJsonObject {
put(CONSENT_SETTINGS, buildJsonObject {
put(CATEGORY_PREFERENCE, buildJsonObject {
put(Constants.CONSENT_KEY, buildJsonObject {
put(Constants.CATEGORY_PREFERENCE_KEY, buildJsonObject {
put("cat1", JsonPrimitive(true))
put("cat2", JsonPrimitive(false))
})
Expand Down Expand Up @@ -130,15 +152,15 @@ class ConsentManagerTests {
val integrations = buildJsonObject {
put(KEY_SEGMENTIO, buildJsonObject {
put("apiKey", JsonPrimitive("foo"))
put("consentSettings", buildJsonObject {
put("categories", buildJsonArray { "foo" })
put(Constants.CONSENT_SETTINGS_KEY, buildJsonObject {
put(Constants.CATEGORIES_KEY, buildJsonArray { "foo" })
})
})

put(KEY_TEST_DESTINATION, buildJsonObject {
put("apiKey", JsonPrimitive("foo"))
put("consentSettings", buildJsonObject {
put("categories", buildJsonArray { "foo" })
put(Constants.CONSENT_SETTINGS_KEY, buildJsonObject {
put(Constants.CATEGORIES_KEY, buildJsonArray { "foo" })
})
})
}
Expand All @@ -147,7 +169,14 @@ class ConsentManagerTests {
integrations = integrations,
plan = buildJsonObject { put("foo", JsonPrimitive("bar")) },
middlewareSettings = buildJsonObject { put("foo", JsonPrimitive("bar")) },
edgeFunction = buildJsonObject { put("foo", JsonPrimitive("bar")) }
edgeFunction = buildJsonObject { put("foo", JsonPrimitive("bar")) },
consentSettings = buildJsonObject {
put(Constants.ALL_CATEGORIES_KEY, buildJsonArray {
add(JsonPrimitive("foo"))
})

put(Constants.HAS_UNMAPPED_DESTINATIONS_KEY, JsonPrimitive(false))
}
)

consentManager.update(settings, Plugin.UpdateType.Refresh)
Expand All @@ -166,4 +195,55 @@ class ConsentManagerTests {
assertEquals(1, testConsentBlockers?.size)
}

@Test
fun `SegmentConsentBlocker does not block when we have unmapped destinations and no consent rule`() {
val store = SynchronousStore()

// We _have_ unmapped destinations and there are no rules for the for the segment destination
// so we should ALLOW the event to proceed.
store.provide(ConsentState(mutableMapOf(), true, mutableListOf(),true))
var event = createConsentEvent("MyConsentEvent", mapOf( "foo" to false))
var segmentBlocker = SegmentConsentBlocker(store)
var resultingEvent = segmentBlocker.execute(event)
assertNotNull(resultingEvent)
}

@Test
fun `SegmentConsentBlocker blocks when we have no unmapped destinations and event has no consent`() {
val store = SynchronousStore()

// We have NO unmapped destinations and there are no rules for the for the segment destination
// so we should BLOCK the event to proceed.
store.provide(ConsentState(mutableMapOf(), false, mutableListOf(),true))
var event = createConsentEvent("MyConsentEvent", mapOf( "foo" to false))
var segmentBlocker = SegmentConsentBlocker(store)
var resultingEvent = segmentBlocker.execute(event)
assertNull(resultingEvent)
}

@Test
fun `SegmentConsentBlocker blocks when event missing required consent`() {
val store = SynchronousStore()

// We _have_ unmapped destinations but there are required consent categories for the
// segment destination so we should BLOCK the event to proceed.
store.provide(ConsentState(mutableMapOf("Segment.io" to arrayOf("foo")), false, mutableListOf(),true))
var event = createConsentEvent("MyConsentEvent", mapOf( "foo" to false))
var segmentBlocker = SegmentConsentBlocker(store)
var resultingEvent = segmentBlocker.execute(event)
assertNull(resultingEvent)
}

@Test
fun `SegmentConsentBlocker does not block when event has required consent`() {
val store = SynchronousStore()

// We _have_ unmapped destinations but there are required consent categories for the
// segment destination so we should BLOCK the event to proceed.
store.provide(ConsentState(mutableMapOf("Segment.io" to arrayOf("foo")), false, mutableListOf(),true))
var event = createConsentEvent("MyConsentEvent", mapOf( "foo" to true))
var segmentBlocker = SegmentConsentBlocker(store)
var resultingEvent = segmentBlocker.execute(event)
assertNotNull(resultingEvent)
}
}
Loading