Skip to content

Commit 2fd2d75

Browse files
authored
Update logic for SegmentConsentBlocker pluging and update to latest analytics-kotlin(1.16.3) (#13)
1 parent e2f9010 commit 2fd2d75

File tree

8 files changed

+145
-35
lines changed

8 files changed

+145
-35
lines changed

lib/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ android {
4242

4343
dependencies {
4444
implementation("com.segment:sovran-kotlin:1.3.1")
45-
implementation("com.segment.analytics.kotlin:android:1.16.1")
45+
implementation("com.segment.analytics.kotlin:android:1.16.3")
4646
implementation("androidx.multidex:multidex:2.0.1")
4747
implementation("androidx.core:core-ktx:1.10.1")
4848

lib/src/main/java/com/segment/analytics/kotlin/destinations/consent/ConsentBlocker.kt

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ import com.segment.analytics.kotlin.destinations.consent.Constants.EVENT_SEGMENT
99
import com.segment.analytics.kotlin.destinations.consent.Constants.SEGMENT_IO_KEY
1010
import kotlinx.serialization.json.JsonObject
1111
import sovran.kotlin.SynchronousStore
12+
import com.segment.analytics.kotlin.destinations.consent.Constants.CATEGORY_PREFERENCE_KEY
13+
import com.segment.analytics.kotlin.destinations.consent.Constants.CONSENT_KEY
1214

1315

14-
internal const val CONSENT_SETTINGS = "consent"
15-
internal const val CATEGORY_PREFERENCE = "categoryPreference"
1616

1717
open class ConsentBlocker(
1818
var destinationKey: String,
@@ -30,7 +30,7 @@ open class ConsentBlocker(
3030

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

33-
val consentJsonArray = getConsentCategoriesFromEvent(event)
33+
val consentJsonArray = getConsentedCategoriesFromEvent(event)
3434

3535
// Look for a missing consent category
3636
requiredConsentCategories.forEach {
@@ -53,13 +53,17 @@ open class ConsentBlocker(
5353
return event
5454
}
5555

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

59-
val consentSettingsJson = event.context[CONSENT_SETTINGS]
63+
val consentSettingsJson = event.context[CONSENT_KEY]
6064
if (consentSettingsJson != null) {
6165
val consentJsonObject = (consentSettingsJson as JsonObject)
62-
val categoryPreferenceJson = consentJsonObject[CATEGORY_PREFERENCE]
66+
val categoryPreferenceJson = consentJsonObject[CATEGORY_PREFERENCE_KEY]
6367
if (categoryPreferenceJson != null) {
6468
val categoryPreferenceJsonObject = categoryPreferenceJson as JsonObject
6569
categoryPreferenceJsonObject.forEach { category, consentGiven ->
@@ -83,4 +87,22 @@ open class ConsentBlocker(
8387
}
8488

8589

86-
class SegmentConsentBlocker(store: SynchronousStore): ConsentBlocker(SEGMENT_IO_KEY, store) {}
90+
class SegmentConsentBlocker(store: SynchronousStore): ConsentBlocker(SEGMENT_IO_KEY, store) {
91+
override fun execute(event: BaseEvent): BaseEvent? {
92+
93+
val currentState = store.currentState(ConsentState::class)
94+
val hasUnmappedDestinations = currentState?.hasUnmappedDestinations
95+
96+
// IF we have no unmapped destinations and we have not consented to any categories block (drop)
97+
// the event.
98+
if (hasUnmappedDestinations == false) {
99+
val consentedCategoriesSet = getConsentedCategoriesFromEvent(event)
100+
if (consentedCategoriesSet.isEmpty()) {
101+
// Drop the event
102+
return null
103+
}
104+
}
105+
106+
return event
107+
}
108+
}

lib/src/main/java/com/segment/analytics/kotlin/destinations/consent/ConsentManager.kt

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import com.segment.analytics.kotlin.core.platform.Plugin
88
import com.segment.analytics.kotlin.core.utilities.getBoolean
99
import com.segment.analytics.kotlin.core.utilities.safeJsonObject
1010
import com.segment.analytics.kotlin.core.utilities.toJsonElement
11+
import com.segment.analytics.kotlin.destinations.consent.Constants.ALL_CATEGORIES_KEY
1112
import com.segment.analytics.kotlin.destinations.consent.Constants.CATEGORIES_KEY
1213
import com.segment.analytics.kotlin.destinations.consent.Constants.CATEGORY_PREFERENCE_KEY
1314
import com.segment.analytics.kotlin.destinations.consent.Constants.CONSENT_KEY
@@ -88,6 +89,7 @@ class ConsentManager(
8889

8990
val destinationMapping = mutableMapOf<String, Array<String>>()
9091
var hasUnmappedDestinations = true
92+
val allCategories = mutableListOf<String>()
9193
var enabledAtSegment = true
9294

9395
// Add all mappings
@@ -110,8 +112,13 @@ class ConsentManager(
110112
it.jsonObject.getBoolean(HAS_UNMAPPED_DESTINATIONS_KEY)
111113
?.let { serverHasUnmappedDestinations ->
112114
println("hasUnmappedDestinations jsonElement: $serverHasUnmappedDestinations")
113-
hasUnmappedDestinations = serverHasUnmappedDestinations == true
115+
hasUnmappedDestinations = serverHasUnmappedDestinations
114116
}
117+
118+
val allCategoriesJson = it.jsonObject.get(ALL_CATEGORIES_KEY)
119+
allCategoriesJson?.let {
120+
it.jsonObject.values.forEach { jsonElement -> allCategories.add(jsonElement.toString())}
121+
}
115122
}
116123
} catch (t: Throwable) {
117124
println("Couldn't parse settings object to check for 'hasUnmappedDestinations'")
@@ -126,7 +133,7 @@ class ConsentManager(
126133
println("Couldn't parse settings object to check if 'enabledAtSegment'.")
127134
}
128135

129-
return ConsentState(destinationMapping, hasUnmappedDestinations, enabledAtSegment)
136+
return ConsentState(destinationMapping, hasUnmappedDestinations, allCategories, enabledAtSegment)
130137
}
131138

132139
override fun execute(event: BaseEvent): BaseEvent? {

lib/src/main/java/com/segment/analytics/kotlin/destinations/consent/Constants.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ object Constants {
33
const val EVENT_SEGMENT_CONSENT_PREFERENCE = "Segment Consent Preference Updated"
44
const val CONSENT_SETTINGS_KEY = "consentSettings"
55
const val CONSENT_KEY = "consent"
6-
const val CATEGORY_PREFERENCE_KEY = "categoryPreference"
6+
const val CATEGORY_PREFERENCE_KEY = "categoryPreferences"
77
const val CATEGORIES_KEY = "categories"
88
const val ALL_CATEGORIES_KEY = "allCategories"
99
const val HAS_UNMAPPED_DESTINATIONS_KEY = "hasUnmappedDestinations"

lib/src/main/java/com/segment/analytics/kotlin/destinations/consent/State.kt

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@ import sovran.kotlin.State
77
class ConsentState(
88
var destinationCategoryMap: Map<String, Array<String>>,
99
var hasUnmappedDestinations: Boolean,
10+
var allCategories: List<String>,
1011
var enabledAtSegment: Boolean
1112
) : State {
1213

1314
companion object {
14-
val defaultState = ConsentState(mutableMapOf(), true, true)
15+
val defaultState = ConsentState(mutableMapOf(), true, mutableListOf(),true)
1516
}
1617
}
1718

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

2122
// New state override any old state.
2223
val newState = ConsentState(
23-
value.destinationCategoryMap, value.hasUnmappedDestinations, value.enabledAtSegment
24+
value.destinationCategoryMap, value.hasUnmappedDestinations, value.allCategories, value.enabledAtSegment
2425
)
2526

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

3031
class UpdateConsentStateActionMappings(var mappings: Map<String, Array<String>>) : Action<ConsentState> {
3132
override fun reduce(state: ConsentState): ConsentState {
32-
val newState = ConsentState(mappings, state.hasUnmappedDestinations, state.enabledAtSegment)
33+
val newState = ConsentState(mappings, state.hasUnmappedDestinations, state.allCategories, state.enabledAtSegment)
3334
return newState
3435
}
3536
}
@@ -39,7 +40,7 @@ class UpdateConsentStateActionHasUnmappedDestinations(var hasUnmappedDestination
3940

4041
// New state override any old state.
4142
val newState = ConsentState(
42-
state.destinationCategoryMap, hasUnmappedDestinations, state.enabledAtSegment
43+
state.destinationCategoryMap, hasUnmappedDestinations, state.allCategories, state.enabledAtSegment
4344
)
4445

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

5253
// New state override any old state.
5354
val newState = ConsentState(
54-
state.destinationCategoryMap, state.hasUnmappedDestinations, enabledAtSegment
55+
state.destinationCategoryMap, state.hasUnmappedDestinations, state.allCategories, enabledAtSegment
5556
)
5657

5758
return newState

lib/src/test/kotlin/com/segment/analytics/kotlin/destinations/consent/ConsentBlockerTests.kt

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,15 @@ class ConsentBlockerTests {
1919
store.provide(ConsentState.defaultState)
2020
var mappings: MutableMap<String, Array<String>> = HashMap()
2121
mappings["foo"] = arrayOf("cat1", "cat2")
22-
val state = ConsentState(mappings, false, true)
22+
val state = ConsentState(mappings, false, mutableListOf<String>(),true)
2323
store.dispatch(UpdateConsentStateActionFull(state), ConsentState::class)
2424
val blockingPlugin = ConsentBlocker("foo", store)
2525

2626
// All categories correct
2727
var stampedEvent = TrackEvent(properties = emptyJsonObject, event = "MyEvent")
2828
stampedEvent.context = buildJsonObject {
29-
put(CONSENT_SETTINGS, buildJsonObject {
30-
put(CATEGORY_PREFERENCE, buildJsonObject {
29+
put(Constants.CONSENT_KEY, buildJsonObject {
30+
put(Constants.CATEGORY_PREFERENCE_KEY, buildJsonObject {
3131
put("cat1", JsonPrimitive(true))
3232
put("cat2", JsonPrimitive(true))
3333
})
@@ -38,8 +38,8 @@ class ConsentBlockerTests {
3838

3939
stampedEvent = TrackEvent(properties = emptyJsonObject, event = "MyEvent")
4040
stampedEvent.context = buildJsonObject {
41-
put(CONSENT_SETTINGS, buildJsonObject {
42-
put(CATEGORY_PREFERENCE, buildJsonObject {
41+
put(Constants.CONSENT_KEY, buildJsonObject {
42+
put(Constants.CATEGORY_PREFERENCE_KEY, buildJsonObject {
4343
put("cat1", JsonPrimitive(true))
4444
put("cat2", JsonPrimitive(true))
4545
put("cat3", JsonPrimitive(true))
@@ -56,7 +56,7 @@ class ConsentBlockerTests {
5656
store.provide(ConsentState.defaultState)
5757
var mappings: MutableMap<String, Array<String>> = HashMap()
5858
mappings["foo"] = arrayOf("cat1", "cat2")
59-
val state = ConsentState(mappings, false, true)
59+
val state = ConsentState(mappings, false, mutableListOf<String>(),true)
6060
store.dispatch(UpdateConsentStateActionFull(state), ConsentState::class)
6161
val blockingPlugin = ConsentBlocker("foo", store)
6262

@@ -68,16 +68,16 @@ class ConsentBlockerTests {
6868

6969
// Context with empty consentSettings
7070
unstamppedEvent.context = buildJsonObject {
71-
put(CONSENT_SETTINGS, emptyJsonObject)
71+
put(Constants.CONSENT_KEY, emptyJsonObject)
7272
}
7373
processedEvent = blockingPlugin.execute(unstamppedEvent)
7474
assertNull(processedEvent)
7575

7676
// Stamped Event with all categories false
7777
var stamppedEvent = TrackEvent(properties = emptyJsonObject, event = "MyEvent")
7878
stamppedEvent.context = buildJsonObject {
79-
put(CONSENT_SETTINGS, buildJsonObject {
80-
put(CATEGORY_PREFERENCE, buildJsonObject {
79+
put(Constants.CONSENT_KEY, buildJsonObject {
80+
put(Constants.CATEGORY_PREFERENCE_KEY, buildJsonObject {
8181
put("cat1", JsonPrimitive(false))
8282
put("cat2", JsonPrimitive(false))
8383
})
@@ -101,8 +101,8 @@ class ConsentBlockerTests {
101101

102102
var stamppedEvent = TrackEvent(properties = emptyJsonObject, event = "MyEvent")
103103
stamppedEvent.context = buildJsonObject {
104-
put(CONSENT_SETTINGS, buildJsonObject {
105-
put(CATEGORY_PREFERENCE, buildJsonObject {
104+
put(Constants.CONSENT_KEY, buildJsonObject {
105+
put(Constants.CATEGORY_PREFERENCE_KEY, buildJsonObject {
106106
put("cat1", JsonPrimitive(false))
107107
put("cat2", JsonPrimitive(false))
108108
})

lib/src/test/kotlin/com/segment/analytics/kotlin/destinations/consent/ConsentManagerTests.kt

Lines changed: 87 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@ import io.mockk.mockkStatic
1515
import io.mockk.spyk
1616
import kotlinx.coroutines.test.TestScope
1717
import kotlinx.coroutines.test.UnconfinedTestDispatcher
18+
import kotlinx.serialization.json.JsonElement
1819
import kotlinx.serialization.json.JsonPrimitive
1920
import kotlinx.serialization.json.buildJsonArray
2021
import kotlinx.serialization.json.buildJsonObject
22+
import kotlinx.serialization.json.jsonObject
2123
import org.junit.Before
2224
import org.junit.jupiter.api.Assertions.*
2325
import org.junit.Test
@@ -36,6 +38,25 @@ class ConsentManagerTests {
3638
private val testDispatcher = UnconfinedTestDispatcher()
3739
private val testScope = TestScope(testDispatcher)
3840

41+
fun createConsentEvent(name: String, consentMap: Map<String, Boolean>, properties: Properties = emptyJsonObject, context: AnalyticsContext = emptyJsonObject): BaseEvent {
42+
var event = TrackEvent(properties, name)
43+
event.context = buildJsonObject {
44+
// Add all context items
45+
context.forEach { prop, elem -> put(prop, elem) }
46+
47+
// Add (potentially overriding from context) the consentMap values
48+
put(Constants.CONSENT_KEY, buildJsonObject {
49+
put(Constants.CATEGORY_PREFERENCE_KEY, buildJsonObject {
50+
consentMap.forEach { category, isConsented ->
51+
put(category, JsonPrimitive(isConsented))
52+
}
53+
})
54+
})
55+
}
56+
57+
return event
58+
}
59+
3960
@Before
4061
fun setUp() {
4162
appContext = spyk(InstrumentationRegistry.getInstrumentation().targetContext)
@@ -78,12 +99,13 @@ class ConsentManagerTests {
7899
val consentManager = ConsentManager(store, cp)
79100
consentManager.start()
80101

102+
// Refactor
81103
var event = TrackEvent(emptyJsonObject, "MyEvent")
82104
event.context = emptyJsonObject
83105

84106
val expectedContext = buildJsonObject {
85-
put(CONSENT_SETTINGS, buildJsonObject {
86-
put(CATEGORY_PREFERENCE, buildJsonObject {
107+
put(Constants.CONSENT_KEY, buildJsonObject {
108+
put(Constants.CATEGORY_PREFERENCE_KEY, buildJsonObject {
87109
put("cat1", JsonPrimitive(true))
88110
put("cat2", JsonPrimitive(false))
89111
})
@@ -130,15 +152,15 @@ class ConsentManagerTests {
130152
val integrations = buildJsonObject {
131153
put(KEY_SEGMENTIO, buildJsonObject {
132154
put("apiKey", JsonPrimitive("foo"))
133-
put("consentSettings", buildJsonObject {
134-
put("categories", buildJsonArray { "foo" })
155+
put(Constants.CONSENT_SETTINGS_KEY, buildJsonObject {
156+
put(Constants.CATEGORIES_KEY, buildJsonArray { "foo" })
135157
})
136158
})
137159

138160
put(KEY_TEST_DESTINATION, buildJsonObject {
139161
put("apiKey", JsonPrimitive("foo"))
140-
put("consentSettings", buildJsonObject {
141-
put("categories", buildJsonArray { "foo" })
162+
put(Constants.CONSENT_SETTINGS_KEY, buildJsonObject {
163+
put(Constants.CATEGORIES_KEY, buildJsonArray { "foo" })
142164
})
143165
})
144166
}
@@ -147,7 +169,14 @@ class ConsentManagerTests {
147169
integrations = integrations,
148170
plan = buildJsonObject { put("foo", JsonPrimitive("bar")) },
149171
middlewareSettings = buildJsonObject { put("foo", JsonPrimitive("bar")) },
150-
edgeFunction = buildJsonObject { put("foo", JsonPrimitive("bar")) }
172+
edgeFunction = buildJsonObject { put("foo", JsonPrimitive("bar")) },
173+
consentSettings = buildJsonObject {
174+
put(Constants.ALL_CATEGORIES_KEY, buildJsonArray {
175+
add(JsonPrimitive("foo"))
176+
})
177+
178+
put(Constants.HAS_UNMAPPED_DESTINATIONS_KEY, JsonPrimitive(false))
179+
}
151180
)
152181

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

198+
@Test
199+
fun `SegmentConsentBlocker does not block when we have unmapped destinations and no consent rule`() {
200+
val store = SynchronousStore()
201+
202+
// We _have_ unmapped destinations and there are no rules for the for the segment destination
203+
// so we should ALLOW the event to proceed.
204+
store.provide(ConsentState(mutableMapOf(), true, mutableListOf(),true))
205+
var event = createConsentEvent("MyConsentEvent", mapOf( "foo" to false))
206+
var segmentBlocker = SegmentConsentBlocker(store)
207+
var resultingEvent = segmentBlocker.execute(event)
208+
assertNotNull(resultingEvent)
209+
}
210+
211+
@Test
212+
fun `SegmentConsentBlocker blocks when we have no unmapped destinations and event has no consent`() {
213+
val store = SynchronousStore()
214+
215+
// We have NO unmapped destinations and there are no rules for the for the segment destination
216+
// so we should BLOCK the event to proceed.
217+
store.provide(ConsentState(mutableMapOf(), false, mutableListOf(),true))
218+
var event = createConsentEvent("MyConsentEvent", mapOf( "foo" to false))
219+
var segmentBlocker = SegmentConsentBlocker(store)
220+
var resultingEvent = segmentBlocker.execute(event)
221+
assertNull(resultingEvent)
222+
}
223+
224+
@Test
225+
fun `SegmentConsentBlocker blocks when event missing required consent`() {
226+
val store = SynchronousStore()
227+
228+
// We _have_ unmapped destinations but there are required consent categories for the
229+
// segment destination so we should BLOCK the event to proceed.
230+
store.provide(ConsentState(mutableMapOf("Segment.io" to arrayOf("foo")), false, mutableListOf(),true))
231+
var event = createConsentEvent("MyConsentEvent", mapOf( "foo" to false))
232+
var segmentBlocker = SegmentConsentBlocker(store)
233+
var resultingEvent = segmentBlocker.execute(event)
234+
assertNull(resultingEvent)
235+
}
236+
237+
@Test
238+
fun `SegmentConsentBlocker does not block when event has required consent`() {
239+
val store = SynchronousStore()
240+
241+
// We _have_ unmapped destinations but there are required consent categories for the
242+
// segment destination so we should BLOCK the event to proceed.
243+
store.provide(ConsentState(mutableMapOf("Segment.io" to arrayOf("foo")), false, mutableListOf(),true))
244+
var event = createConsentEvent("MyConsentEvent", mapOf( "foo" to true))
245+
var segmentBlocker = SegmentConsentBlocker(store)
246+
var resultingEvent = segmentBlocker.execute(event)
247+
assertNotNull(resultingEvent)
248+
}
169249
}

0 commit comments

Comments
 (0)