Skip to content

Commit a724630

Browse files
fix: Wait for User Attributes to persist before Rokt selectPlacements forwards to the kit
1 parent 6c2079e commit a724630

3 files changed

Lines changed: 123 additions & 27 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77

88
## Unreleased
99

10+
### Fixed
11+
12+
- Restore async wait for user attributes to persist before Rokt `selectPlacements` delegates to the kit, fixing a regression where placement attributes could be missing in mParticle and Rokt.
13+
1014
### Added
1115

1216
- Add device-based consent APIs (`MParticle.getDeviceConsentState()`, `MParticle.setDeviceConsentState()`) and `MParticleOptions.Builder.deviceBasedConsentEnabled()` so consent can be stored and applied at the device level, overriding MPID-based consent for kit forwarding rules and event uploads.

android-kit-base/src/main/kotlin/com/mparticle/kits/RoktKitApiImpl.kt

Lines changed: 62 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import android.graphics.Typeface
44
import com.mparticle.MParticle
55
import com.mparticle.MpRoktEventCallback
66
import com.mparticle.RoktEvent
7+
import com.mparticle.TypedUserAttributeListener
78
import com.mparticle.identity.IdentityApi
89
import com.mparticle.identity.IdentityApiRequest
910
import com.mparticle.identity.MParticleUser
@@ -50,17 +51,20 @@ internal class RoktKitApiImpl(private val roktListener: KitIntegration.RoktListe
5051
val kitConfig = kitIntegration.configuration
5152

5253
confirmEmail(email, hashedEmail, user, instance.Identity(), kitConfig) {
53-
val finalAttributes = prepareAttributes(mutableAttributes, user)
54-
roktListener.selectPlacements(
55-
viewName,
56-
finalAttributes,
57-
mpRoktEventCallback,
58-
placeHolders,
59-
fontTypefaces,
60-
FilteredMParticleUser.getInstance(user?.id ?: 0L, kitIntegration),
61-
config,
62-
options,
63-
)
54+
val finalAttributes = applyPlacementAttributeMapping(mutableAttributes)
55+
setRoktAttributesOnUser(finalAttributes, user) {
56+
ensureSandboxMode(finalAttributes)
57+
roktListener.selectPlacements(
58+
viewName,
59+
finalAttributes,
60+
mpRoktEventCallback,
61+
placeHolders,
62+
fontTypefaces,
63+
FilteredMParticleUser.getInstance(user?.id ?: 0L, kitIntegration),
64+
config,
65+
options,
66+
)
67+
}
6468
}
6569
} catch (e: Exception) {
6670
Logger.warning("Failed to call execute for Rokt Kit: ${e.message}")
@@ -115,16 +119,19 @@ internal class RoktKitApiImpl(private val roktListener: KitIntegration.RoktListe
115119
return
116120
}
117121
val user = instance.Identity().currentUser
118-
val email = mutableAttributes["email"]
122+
val email = getValueIgnoreCase(mutableAttributes, "email")
119123
val hashedEmail = getValueIgnoreCase(mutableAttributes, "emailsha256")
120124
val kitConfig = kitIntegration.configuration
121125

122126
confirmEmail(email, hashedEmail, user, instance.Identity(), kitConfig) {
123-
val finalAttributes = prepareAttributes(mutableAttributes, user)
124-
roktListener.enrichAttributes(
125-
finalAttributes,
126-
FilteredMParticleUser.getInstance(user?.id ?: 0L, kitIntegration),
127-
)
127+
val finalAttributes = applyPlacementAttributeMapping(mutableAttributes)
128+
setRoktAttributesOnUser(finalAttributes, user) {
129+
ensureSandboxMode(finalAttributes)
130+
roktListener.enrichAttributes(
131+
finalAttributes,
132+
FilteredMParticleUser.getInstance(user?.id ?: 0L, kitIntegration),
133+
)
134+
}
128135
}
129136
} catch (e: Exception) {
130137
Logger.warning("Failed to call prepareAttributesAsync for Rokt Kit: ${e.message}")
@@ -142,7 +149,7 @@ internal class RoktKitApiImpl(private val roktListener: KitIntegration.RoktListe
142149
return null
143150
}
144151

145-
private fun prepareAttributes(finalAttributes: MutableMap<String, String>, user: MParticleUser?): MutableMap<String, String> {
152+
private fun applyPlacementAttributeMapping(attributes: MutableMap<String, String>): MutableMap<String, String> {
146153
val kitConfig = kitIntegration.configuration
147154
val jsonArray = try {
148155
kitConfig?.placementAttributesMapping ?: org.json.JSONArray()
@@ -155,27 +162,55 @@ internal class RoktKitApiImpl(private val roktListener: KitIntegration.RoktListe
155162
val obj = jsonArray.optJSONObject(i) ?: continue
156163
val mapFrom = obj.optString("map")
157164
val mapTo = obj.optString("value")
158-
if (finalAttributes.containsKey(mapFrom)) {
159-
val value = finalAttributes.remove(mapFrom)
165+
if (attributes.containsKey(mapFrom)) {
166+
val value = attributes.remove(mapFrom)
160167
if (value != null) {
161-
finalAttributes[mapTo] = value
168+
attributes[mapTo] = value
162169
}
163170
}
164171
}
172+
return attributes
173+
}
165174

175+
private fun ensureSandboxMode(finalAttributes: MutableMap<String, String>) {
176+
if (!finalAttributes.containsKey(Constants.MessageKey.SANDBOX_MODE_ROKT)) {
177+
finalAttributes[Constants.MessageKey.SANDBOX_MODE_ROKT] =
178+
Objects.toString(MPUtility.isDevEnv(), "false")
179+
}
180+
}
181+
182+
/**
183+
* Persists placement attributes on the mParticle user, then invokes [onReady] after the
184+
* attributes have been applied. This ensures the Rokt kit reads an up-to-date user profile
185+
* when merging attributes for the placement.
186+
*/
187+
private fun setRoktAttributesOnUser(
188+
finalAttributes: Map<String, String>,
189+
user: MParticleUser?,
190+
onReady: Runnable,
191+
) {
166192
val objectAttributes = mutableMapOf<String, Any>()
167193
for ((key, value) in finalAttributes) {
168194
if (key != Constants.MessageKey.SANDBOX_MODE_ROKT) {
169195
objectAttributes[key] = value
170196
}
171197
}
172-
user?.setUserAttributes(objectAttributes)
173-
174-
if (!finalAttributes.containsKey(Constants.MessageKey.SANDBOX_MODE_ROKT)) {
175-
finalAttributes[Constants.MessageKey.SANDBOX_MODE_ROKT] =
176-
Objects.toString(MPUtility.isDevEnv(), "false")
198+
if (user != null) {
199+
user.setUserAttributes(objectAttributes)
200+
user.getUserAttributes(
201+
object : TypedUserAttributeListener {
202+
override fun onUserAttributesReceived(
203+
userAttributes: Map<String, Any?>,
204+
userAttributeLists: Map<String, List<String?>?>,
205+
mpid: Long,
206+
) {
207+
onReady.run()
208+
}
209+
},
210+
)
211+
} else {
212+
onReady.run()
177213
}
178-
return finalAttributes
179214
}
180215

181216
private fun confirmEmail(

android-kit-base/src/test/kotlin/com/mparticle/kits/RoktKitApiImplTest.kt

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.mparticle.kits
22

33
import com.mparticle.MParticle
44
import com.mparticle.identity.IdentityApi
5+
import com.mparticle.identity.MParticleUser
56
import com.mparticle.internal.MPUtility
67
import com.mparticle.mock.MockMParticle
78
import com.mparticle.rokt.PlacementOptions
@@ -15,6 +16,7 @@ import org.junit.Test
1516
import org.mockito.ArgumentCaptor
1617
import org.mockito.ArgumentMatchers.any
1718
import org.mockito.Mockito.mock
19+
import org.mockito.Mockito.never
1820
import org.mockito.Mockito.verify
1921
import org.mockito.Mockito.`when`
2022
import org.mockito.Mockito.withSettings
@@ -82,6 +84,61 @@ class RoktKitApiImplTest {
8284
assertEquals(MPUtility.isDevEnv().toString(), captured["sandbox"])
8385
}
8486

87+
@Test
88+
fun testSelectPlacements_waitsForUserAttributesBeforeDelegating() {
89+
val kitConfig = KitConfiguration.createKitConfiguration(JSONObject().put("id", 42))
90+
val kitIntegration =
91+
mock(
92+
KitIntegration::class.java,
93+
withSettings().extraInterfaces(KitIntegration.RoktListener::class.java),
94+
)
95+
`when`(kitIntegration.configuration).thenReturn(kitConfig)
96+
val roktListener = kitIntegration as KitIntegration.RoktListener
97+
val roktApi = RoktKitApiImpl(roktListener, kitIntegration)
98+
99+
val identityApi = mock(IdentityApi::class.java)
100+
val user = mock(MParticleUser::class.java)
101+
`when`(user.id).thenReturn(12345L)
102+
`when`(identityApi.currentUser).thenReturn(user)
103+
val instance = MockMParticle()
104+
instance.setIdentityApi(identityApi)
105+
MParticle.setInstance(instance)
106+
107+
var capturedListener: com.mparticle.TypedUserAttributeListener? = null
108+
`when`(user.getUserAttributes(any())).thenAnswer { invocation ->
109+
capturedListener = invocation.arguments[0] as com.mparticle.TypedUserAttributeListener
110+
null
111+
}
112+
113+
val attributes = mapOf("country" to "US")
114+
roktApi.selectPlacements("Test", attributes, null, null, null, null, null)
115+
116+
verify(user).setUserAttributes(any())
117+
verify(roktListener, never()).selectPlacements(
118+
any(),
119+
any(),
120+
any(),
121+
any(),
122+
any(),
123+
any(),
124+
any(),
125+
any(),
126+
)
127+
128+
capturedListener!!.onUserAttributesReceived(emptyMap(), emptyMap(), 12345L)
129+
130+
verify(roktListener).selectPlacements(
131+
any(),
132+
any(),
133+
any(),
134+
any(),
135+
any(),
136+
any(),
137+
any(),
138+
any(),
139+
)
140+
}
141+
85142
@Test
86143
fun testSelectPlacements_passesPlacementOptions() {
87144
val kitConfig = KitConfiguration.createKitConfiguration(JSONObject().put("id", 42))

0 commit comments

Comments
 (0)