Skip to content

Commit ac27439

Browse files
fix: Implement Google Consent, Refactor Deprecated CHECKOUT_OPTION (#103)
1 parent d76aed4 commit ac27439

File tree

4 files changed

+794
-7
lines changed

4 files changed

+794
-7
lines changed

build.gradle

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,17 @@ android {
3838
defaultConfig {
3939
minSdkVersion 16
4040
}
41+
testOptions {
42+
unitTests.all {
43+
jvmArgs += ['--add-opens', 'java.base/java.lang=ALL-UNNAMED']
44+
45+
}
46+
}
4147
}
4248

4349
dependencies {
4450
testImplementation files('libs/java-json.jar')
4551
testImplementation files('libs/test-utils.aar')
46-
compileOnly 'com.google.firebase:firebase-analytics:[17.3.0,21.0.0)'
52+
testImplementation 'com.google.android.gms:play-services-measurement-api:21.5.1'
53+
compileOnly 'com.google.firebase:firebase-analytics:[21.5.1,)'
4754
}

src/main/kotlin/com/mparticle/kits/GoogleAnalyticsFirebaseKit.kt

Lines changed: 191 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@ import com.mparticle.identity.MParticleUser
1414
import com.mparticle.internal.Logger
1515
import com.mparticle.kits.KitIntegration.CommerceListener
1616
import com.mparticle.kits.KitIntegration.IdentityListener
17+
import org.json.JSONArray
18+
import org.json.JSONException
19+
import org.json.JSONObject
1720
import java.math.BigDecimal
21+
import java.util.EnumMap
1822

1923
class GoogleAnalyticsFirebaseKit : KitIntegration(), KitIntegration.EventListener, IdentityListener,
2024
CommerceListener, KitIntegration.UserAttributeListener {
@@ -26,6 +30,10 @@ class GoogleAnalyticsFirebaseKit : KitIntegration(), KitIntegration.EventListene
2630
context: Context
2731
): List<ReportingMessage> {
2832
Logger.info("$name Kit relies on a functioning instance of Firebase Analytics. If your Firebase Analytics instance is not configured properly, this Kit will not work")
33+
val userConsentState = currentUser?.consentState
34+
userConsentState?.let {
35+
setConsent(currentUser.consentState)
36+
}
2937
return emptyList()
3038
}
3139

@@ -88,7 +96,35 @@ class GoogleAnalyticsFirebaseKit : KitIntegration(), KitIntegration.EventListene
8896
Product.REFUND -> FirebaseAnalytics.Event.REFUND
8997
Product.REMOVE_FROM_CART -> FirebaseAnalytics.Event.REMOVE_FROM_CART
9098
Product.CLICK -> FirebaseAnalytics.Event.SELECT_CONTENT
91-
Product.CHECKOUT_OPTION -> FirebaseAnalytics.Event.SET_CHECKOUT_OPTION
99+
Product.CHECKOUT_OPTION -> {
100+
val warningMessage = WARNING_MESSAGE
101+
val customFlags = commerceEvent.customFlags
102+
if ((customFlags != null) && customFlags.containsKey(CF_COMMERCE_EVENT_TYPE)
103+
) {
104+
val commerceEventTypes =
105+
customFlags[CF_COMMERCE_EVENT_TYPE]
106+
if (!commerceEventTypes.isNullOrEmpty()) {
107+
when (commerceEventTypes[0]) {
108+
FirebaseAnalytics.Event.ADD_SHIPPING_INFO -> {
109+
FirebaseAnalytics.Event.ADD_SHIPPING_INFO
110+
}
111+
FirebaseAnalytics.Event.ADD_PAYMENT_INFO -> {
112+
FirebaseAnalytics.Event.ADD_PAYMENT_INFO
113+
}
114+
else -> {
115+
Logger.warning(warningMessage)
116+
return emptyList()
117+
}
118+
}
119+
} else {
120+
Logger.warning(warningMessage)
121+
return emptyList()
122+
}
123+
} else {
124+
Logger.warning(warningMessage)
125+
return emptyList()
126+
}
127+
}
92128
Product.DETAIL -> FirebaseAnalytics.Event.VIEW_ITEM
93129
else -> return emptyList()
94130
}
@@ -198,12 +234,31 @@ class GoogleAnalyticsFirebaseKit : KitIntegration(), KitIntegration.EventListene
198234
pickyBundle.putString(attributes.key, attributes.value.toString())
199235
}
200236
}
237+
val customFlags = commerceEvent.customFlags
238+
if (customFlags != null && customFlags.containsKey(CF_COMMERCE_EVENT_TYPE)) {
239+
val commerceEventTypeList = customFlags[CF_COMMERCE_EVENT_TYPE]
240+
if (!commerceEventTypeList.isNullOrEmpty()) {
241+
val commerceEventType = commerceEventTypeList[0]
242+
if (commerceEventType == FirebaseAnalytics.Event.ADD_SHIPPING_INFO) {
243+
val shippingTier = customFlags[CF_SHIPPING_TIER]
244+
if (!shippingTier.isNullOrEmpty()) {
245+
pickyBundle.putString(
246+
FirebaseAnalytics.Param.SHIPPING_TIER,
247+
shippingTier[0]
248+
)
249+
}
250+
} else if (commerceEventType == FirebaseAnalytics.Event.ADD_PAYMENT_INFO) {
251+
val paymentType = customFlags[CF_PAYMENT_TYPE]
252+
if (!paymentType.isNullOrEmpty()) {
253+
pickyBundle.putString(FirebaseAnalytics.Param.PAYMENT_TYPE, paymentType[0])
254+
}
255+
}
256+
}
257+
}
201258

202259
pickyBundle
203260
.putString(FirebaseAnalytics.Param.CURRENCY, currency)
204261
.putBundleList(FirebaseAnalytics.Param.ITEMS, getProductBundles(commerceEvent))
205-
.putString(FirebaseAnalytics.Event.SET_CHECKOUT_OPTION, commerceEvent.checkoutOptions)
206-
.putInt(FirebaseAnalytics.Event.CHECKOUT_PROGRESS, commerceEvent.checkoutStep)
207262

208263
return pickyBundle
209264
}
@@ -339,6 +394,120 @@ class GoogleAnalyticsFirebaseKit : KitIntegration(), KitIntegration.EventListene
339394
consentState1: ConsentState,
340395
filteredMParticleUser: FilteredMParticleUser
341396
) {
397+
setConsent(consentState1)
398+
}
399+
400+
private fun setConsent(consentState: ConsentState) {
401+
val consentMap: MutableMap<FirebaseAnalytics.ConsentType, FirebaseAnalytics.ConsentStatus> =
402+
EnumMap(
403+
FirebaseAnalytics.ConsentType::class.java
404+
)
405+
googleConsentMapSettings.forEach { it ->
406+
val mpConsentSetting = settings[it.value]
407+
if (!mpConsentSetting.isNullOrEmpty()) {
408+
if (mpConsentSetting == GoogleConsentValues.GRANTED.consentValue) {
409+
consentMap[it.key] = FirebaseAnalytics.ConsentStatus.GRANTED
410+
} else if (mpConsentSetting == GoogleConsentValues.DENIED.consentValue) {
411+
consentMap[it.key] = FirebaseAnalytics.ConsentStatus.DENIED
412+
}
413+
}
414+
}
415+
416+
val clientConsentSettings = parseToNestedMap(consentState.toString())
417+
418+
parseConsentMapping(settings[consentMappingSDK]).forEach { currentConsent ->
419+
420+
val isConsentAvailable =
421+
searchKeyInNestedMap(clientConsentSettings, key = currentConsent.key)
422+
423+
if (isConsentAvailable != null) {
424+
val isConsentGranted: Boolean =
425+
JSONObject(isConsentAvailable.toString()).opt("consented") as Boolean
426+
val consentStatus =
427+
if (isConsentGranted) FirebaseAnalytics.ConsentStatus.GRANTED else FirebaseAnalytics.ConsentStatus.DENIED
428+
429+
430+
when (currentConsent.value) {
431+
"ad_storage" -> consentMap[FirebaseAnalytics.ConsentType.AD_STORAGE] =
432+
consentStatus
433+
434+
"ad_user_data" -> consentMap[FirebaseAnalytics.ConsentType.AD_USER_DATA] =
435+
consentStatus
436+
437+
"ad_personalization" -> consentMap[FirebaseAnalytics.ConsentType.AD_PERSONALIZATION] =
438+
consentStatus
439+
440+
"analytics_storage" -> consentMap[FirebaseAnalytics.ConsentType.ANALYTICS_STORAGE] =
441+
consentStatus
442+
}
443+
}
444+
}
445+
if (consentMap.isNotEmpty()) {
446+
FirebaseAnalytics.getInstance(context).setConsent(consentMap)
447+
}
448+
}
449+
private fun parseConsentMapping(json: String?): Map<String, String> {
450+
if (json.isNullOrEmpty()) {
451+
return emptyMap()
452+
}
453+
val jsonWithFormat = json.replace("\\", "")
454+
455+
return try {
456+
JSONArray(jsonWithFormat)
457+
.let { jsonArray ->
458+
(0 until jsonArray.length())
459+
.associate {
460+
val jsonObject = jsonArray.getJSONObject(it)
461+
val map = jsonObject.getString("map")
462+
val value = jsonObject.getString("value")
463+
map to value
464+
}
465+
}
466+
} catch (jse: JSONException) {
467+
Logger.warning(jse, "The Google Firebase kit threw an exception while searching for the configured consent purpose mapping in the current user's consent status.")
468+
emptyMap()
469+
}
470+
}
471+
472+
private fun parseToNestedMap(jsonString: String): Map<String, Any> {
473+
val topLevelMap = mutableMapOf<String, Any>()
474+
try {
475+
val jsonObject = JSONObject(jsonString)
476+
477+
for (key in jsonObject.keys()) {
478+
val value = jsonObject.get(key)
479+
if (value is JSONObject) {
480+
topLevelMap[key] = parseToNestedMap(value.toString())
481+
} else {
482+
topLevelMap[key] = value
483+
}
484+
}
485+
} catch (e: Exception) {
486+
Logger.error(e, "The Google Firebase kit was unable to parse the user's ConsentState, consent may not be set correctly on the Google Analytics SDK")
487+
}
488+
return topLevelMap
489+
}
490+
491+
private fun searchKeyInNestedMap(map: Map<*, *>, key: Any): Any? {
492+
if (map.isNullOrEmpty()) {
493+
return null
494+
}
495+
try {
496+
for ((mapKey, mapValue) in map) {
497+
if (mapKey.toString().equals(key.toString(), ignoreCase = true)) {
498+
return mapValue
499+
}
500+
if (mapValue is Map<*, *>) {
501+
val foundValue = searchKeyInNestedMap(mapValue, key)
502+
if (foundValue != null) {
503+
return foundValue
504+
}
505+
}
506+
}
507+
} catch (e: Exception) {
508+
Logger.error(e, "The Google Firebase kit threw an exception while searching for the configured consent purpose mapping in the current user's consent status.")
509+
}
510+
return null
342511
}
343512

344513
fun standardizeAttributes(
@@ -438,11 +607,30 @@ class GoogleAnalyticsFirebaseKit : KitIntegration(), KitIntegration.EventListene
438607
const val USER_ID_MPID_VALUE = "mpid"
439608
private val forbiddenPrefixes = arrayOf("google_", "firebase_", "ga_")
440609
private const val CURRENCY_FIELD_NOT_SET = "Currency field required by Firebase was not set, defaulting to 'USD'"
610+
const val CF_COMMERCE_EVENT_TYPE = "Firebase.CommerceEventType"
611+
const val CF_PAYMENT_TYPE = "Firebase.PaymentType"
612+
const val CF_SHIPPING_TIER = "Firebase.ShippingTier"
613+
const val WARNING_MESSAGE =
614+
"Firebase no longer supports CHECKOUT_OPTION. To specify a different eventName, add CF_COMMERCE_EVENT_TYPE to your customFlags with a valid value"
441615
private const val USD = "USD"
442616
private const val eventMaxLength = 40
443617
private const val userAttributeMaxLength = 24
444618
private const val eventValMaxLength = 100
445619
private const val userAttributeValMaxLength = 36
446620
private const val KIT_NAME = "Google Analytics for Firebase"
621+
622+
//Constants for Read Consent
623+
private const val consentMappingSDK = "consentMappingSDK"
624+
enum class GoogleConsentValues(val consentValue: String) {
625+
GRANTED("Granted"),
626+
DENIED("Denied")
627+
}
628+
629+
val googleConsentMapSettings = mapOf(
630+
FirebaseAnalytics.ConsentType.AD_STORAGE to "defaultAdStorageConsentSDK",
631+
FirebaseAnalytics.ConsentType.AD_USER_DATA to "defaultAdUserDataConsentSDK",
632+
FirebaseAnalytics.ConsentType.AD_PERSONALIZATION to "defaultAdPersonalizationConsentSDK",
633+
FirebaseAnalytics.ConsentType.ANALYTICS_STORAGE to "defaultAnalyticsStorageConsentSDK"
634+
)
447635
}
448636
}

src/test/kotlin/com/google/firebase/analytics/FirebaseAnalytics.kt

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,12 @@ import android.os.Bundle
99
class FirebaseAnalytics {
1010
var loggedEvents: LinkedList<Map.Entry<String, Bundle>> = LinkedList()
1111
var currentScreenName: String? = null
12+
var consentStateMap:MutableMap<Any, Any> = mutableMapOf()
1213

13-
14+
object Event {
15+
const val ADD_PAYMENT_INFO = "add_payment_info"
16+
const val ADD_SHIPPING_INFO = "add_shipping_info"
17+
}
1418
fun logEvent(key: String, bundle: Bundle) {
1519
loggedEvents.add(SimpleEntry(key, bundle))
1620
}
@@ -19,6 +23,15 @@ class FirebaseAnalytics {
1923
currentScreenName = screenName
2024
}
2125

26+
fun setConsent(var1: MutableMap<Any, Any>) {
27+
consentStateMap.putAll(var1)
28+
}
29+
30+
fun getConsentState()
31+
: MutableMap<Any, Any> {
32+
return consentStateMap
33+
}
34+
2235
fun setUserProperty(key: String?, value: String?) {}
2336
fun getLoggedEvents(): List<Map.Entry<String, Bundle>> = loggedEvents
2437

0 commit comments

Comments
 (0)