Skip to content

Commit 33a938f

Browse files
authored
fix: crash when build attributes are nullable (#584)
1 parent 4a5e1c4 commit 33a938f

File tree

5 files changed

+59
-33
lines changed

5 files changed

+59
-33
lines changed

common-test/src/main/java/io/customer/commontest/util/DeviceStoreStub.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,15 @@ class DeviceStoreStub {
1212
fun getDeviceStore(client: Client): DeviceStore {
1313
return DeviceStoreImpl(
1414
buildStore = object : BuildStore {
15-
override val deviceBrand: String
15+
override val deviceBrand: String?
1616
get() = "Google"
17-
override val deviceModel: String
17+
override val deviceModel: String?
1818
get() = "Pixel 6"
19-
override val deviceManufacturer: String
19+
override val deviceManufacturer: String?
2020
get() = "Google"
21-
override val deviceOSVersion: Int
21+
override val deviceOSVersion: Int?
2222
get() = 30
23-
override val deviceLocale: String
23+
override val deviceLocale: String?
2424
get() = Locale.US.toLanguageTag()
2525
},
2626
applicationStore = object : ApplicationStore {

core/src/main/kotlin/io/customer/sdk/data/store/BuildStore.kt

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,31 +6,31 @@ import java.util.*
66
interface BuildStore {
77

88
// Brand : Google
9-
val deviceBrand: String
9+
val deviceBrand: String?
1010

1111
// Device model: Pixel
12-
val deviceModel: String
12+
val deviceModel: String?
1313

1414
// Hardware manufacturer: Samsung
15-
val deviceManufacturer: String
15+
val deviceManufacturer: String?
1616

1717
// Android SDK Version: 21
18-
val deviceOSVersion: Int
18+
val deviceOSVersion: Int?
1919

2020
// Device locale: en-US
21-
val deviceLocale: String
21+
val deviceLocale: String?
2222
}
2323

2424
internal class BuildStoreImpl : BuildStore {
2525

26-
override val deviceBrand: String
26+
override val deviceBrand: String?
2727
get() = Build.BRAND
28-
override val deviceModel: String
28+
override val deviceModel: String?
2929
get() = Build.MODEL
30-
override val deviceManufacturer: String
30+
override val deviceManufacturer: String?
3131
get() = Build.MANUFACTURER
32-
override val deviceOSVersion: Int
32+
override val deviceOSVersion: Int?
3333
get() = Build.VERSION.SDK_INT
34-
override val deviceLocale: String
34+
override val deviceLocale: String?
3535
get() = Locale.getDefault().toLanguageTag()
3636
}

core/src/main/kotlin/io/customer/sdk/data/store/DeviceStore.kt

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ interface DeviceStore : BuildStore, ApplicationStore {
1818
* `Customer.io Android Client/1.0.0-alpha.6`
1919
*/
2020
fun buildUserAgent(): String
21-
fun buildDeviceAttributes(): Map<String, Any>
21+
fun buildDeviceAttributes(): Map<String, Any?>
2222
}
2323

2424
class DeviceStoreImpl(
@@ -28,15 +28,15 @@ class DeviceStoreImpl(
2828
version: String = client.sdkVersion
2929
) : DeviceStore {
3030

31-
override val deviceBrand: String
31+
override val deviceBrand: String?
3232
get() = buildStore.deviceBrand
33-
override val deviceModel: String
33+
override val deviceModel: String?
3434
get() = buildStore.deviceModel
35-
override val deviceManufacturer: String
35+
override val deviceManufacturer: String?
3636
get() = buildStore.deviceManufacturer
37-
override val deviceOSVersion: Int
37+
override val deviceOSVersion: Int?
3838
get() = buildStore.deviceOSVersion
39-
override val deviceLocale: String
39+
override val deviceLocale: String?
4040
get() = buildStore.deviceLocale
4141
override val customerAppName: String?
4242
get() = applicationStore.customerAppName
@@ -57,7 +57,7 @@ class DeviceStoreImpl(
5757
}
5858
}
5959

60-
override fun buildDeviceAttributes(): Map<String, Any> {
60+
override fun buildDeviceAttributes(): Map<String, Any?> {
6161
return mapOf(
6262
"device_os" to deviceOSVersion,
6363
"device_model" to deviceModel,

datapipelines/src/test/java/io/customer/datapipelines/DataPipelinesCompatibilityTests.kt

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import kotlinx.coroutines.test.runTest
2828
import kotlinx.serialization.json.Json
2929
import kotlinx.serialization.json.JsonArray
3030
import kotlinx.serialization.json.JsonElement
31+
import kotlinx.serialization.json.JsonNull
3132
import kotlinx.serialization.json.JsonObject
3233
import kotlinx.serialization.json.buildJsonObject
3334
import kotlinx.serialization.json.jsonArray
@@ -432,6 +433,36 @@ class DataPipelinesCompatibilityTests : JUnitTest() {
432433
payloadContext.containsKey("platform") shouldBeEqualTo false
433434
}
434435

436+
@Test
437+
fun device_givenTokenRegisteredWithNullableDeviceAttributes_expectDeviceAttributesAreSent() = runTest {
438+
val givenToken = String.random
439+
val givenAttributes = mapOf(
440+
"cio_sdk_version" to "2.0.0-alpha.10",
441+
"app_release" to "1.0",
442+
"app_build" to "100",
443+
"app_namespace" to "io.customer.android",
444+
"device_os" to 13,
445+
"device_manufacturer" to null,
446+
"device_model" to "Pixel 5"
447+
)
448+
449+
every { deviceStore.buildDeviceAttributes() } returns givenAttributes
450+
sdkInstance.registerDeviceToken(givenToken)
451+
every { globalPreferenceStore.getDeviceToken() } returns givenToken
452+
453+
val queuedEvents = getQueuedEvents()
454+
// 1. Identify event
455+
// 2. Device registration event
456+
queuedEvents.count() shouldBeEqualTo 1
457+
458+
val payload = queuedEvents.last().jsonObject
459+
payload.eventType shouldBeEqualTo "track"
460+
payload.eventName shouldBeEqualTo EventNames.DEVICE_UPDATE
461+
462+
val payloadProperties = payload["properties"]?.jsonObject.shouldNotBeNull()
463+
payloadProperties["device_manufacturer"] shouldBeEqualTo JsonNull
464+
}
465+
435466
@Test
436467
fun device_givenAttributesUpdated_expectFinalJSONHasCustomDeviceAttributes() = runTest {
437468
val givenToken = String.random

datapipelines/src/test/java/io/customer/datapipelines/testutils/extensions/JsonExtensions.kt

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package io.customer.datapipelines.testutils.extensions
33
import com.segment.analytics.kotlin.core.emptyJsonObject
44
import io.customer.datapipelines.extensions.toJsonObject
55
import io.customer.datapipelines.plugins.findAtPath
6-
import io.customer.sdk.data.model.CustomAttributes
76
import kotlinx.serialization.ExperimentalSerializationApi
87
import kotlinx.serialization.json.Json
98
import kotlinx.serialization.json.JsonElement
@@ -17,9 +16,9 @@ import org.amshove.kluent.internal.assertEquals
1716
import org.json.JSONObject
1817

1918
/**
20-
* Similar to Kluent's `shouldBeEqualTo` but for comparing JSON objects with custom attributes map.
19+
* Similar to Kluent's `shouldBeEqualTo` but for comparing JSON objects with map with nullable values.
2120
*/
22-
infix fun JsonObject.shouldMatchTo(expected: CustomAttributes): JsonObject {
21+
infix fun JsonObject.shouldMatchTo(expected: Map<String, Any?>): JsonObject {
2322
return this.apply { assertEquals(expected.toJsonObject(), this) }
2423
}
2524

@@ -38,16 +37,12 @@ inline fun <K, reified V> Pair<K, V>.encodeToJsonValue(): Pair<K, JsonElement> {
3837
}
3938

4039
/**
41-
* Converts a map of custom attributes to a JSON object.
40+
* Converts a map with nullable values to a JSON object.
4241
* If the map is empty, it returns an empty JSON object.
4342
*/
44-
fun CustomAttributes?.toJsonObject(): JsonObject {
45-
val encodedMap = if (this.isNullOrEmpty()) {
46-
emptyMap()
47-
} else {
48-
with(Json) {
49-
mapValues { (_, value) -> encode(value) }
50-
}
43+
fun Map<String, Any?>.toJsonObject(): JsonObject {
44+
val encodedMap = with(Json) {
45+
mapValues { (_, value) -> encode(value) }
5146
}
5247
return JsonObject(encodedMap)
5348
}

0 commit comments

Comments
 (0)