diff --git a/README.md b/README.md index 03968df6b..1498d0c10 100644 --- a/README.md +++ b/README.md @@ -37,15 +37,15 @@ send them timely push notifications via FCM. ```kotlin // build.gradle.kts dependencies { - implementation("com.github.klaviyo.klaviyo-android-sdk:analytics:1.3.4") - implementation("com.github.klaviyo.klaviyo-android-sdk:push-fcm:1.3.4") + implementation("com.github.klaviyo.klaviyo-android-sdk:analytics:1.3.5") + implementation("com.github.klaviyo.klaviyo-android-sdk:push-fcm:1.3.5") } ``` ```groovy // build.gradle dependencies { - implementation "com.github.klaviyo.klaviyo-android-sdk:analytics:1.3.4" - implementation "com.github.klaviyo.klaviyo-android-sdk:push-fcm:1.3.4" + implementation "com.github.klaviyo.klaviyo-android-sdk:analytics:1.3.5" + implementation "com.github.klaviyo.klaviyo-android-sdk:push-fcm:1.3.5" } ``` @@ -351,4 +351,4 @@ This feature is supported in version 1.3.1 and above of the Klaviyo Android SDK. [//]: # (REMINDER: Update documentation branch with new dokka docs when updating version number) Browse complete code documentation autogenerated with -Dokka [here](https://klaviyo.github.io/klaviyo-android-sdk/1.3.4) +Dokka [here](https://klaviyo.github.io/klaviyo-android-sdk/1.3.5) diff --git a/sdk/analytics/src/main/java/com/klaviyo/analytics/networking/KlaviyoApiClient.kt b/sdk/analytics/src/main/java/com/klaviyo/analytics/networking/KlaviyoApiClient.kt index 1e6c3b5d5..490bdb5f1 100644 --- a/sdk/analytics/src/main/java/com/klaviyo/analytics/networking/KlaviyoApiClient.kt +++ b/sdk/analytics/src/main/java/com/klaviyo/analytics/networking/KlaviyoApiClient.kt @@ -113,15 +113,18 @@ internal object KlaviyoApiClient : ApiClient { initBatch() } - for (request in requests) { + var addedRequest = false + requests.forEach { request -> if (!apiQueue.contains(request)) { apiQueue.offer(request) + Registry.dataStore.store(request.uuid, request.toString()) + broadcastApiRequest(request) + addedRequest = true } - Registry.dataStore.store(request.uuid, request.toString()) - broadcastApiRequest(request) } - - persistQueue() + if (addedRequest) { + persistQueue() + } } /** @@ -135,9 +138,7 @@ internal object KlaviyoApiClient : ApiClient { * Reset the in-memory queue to the queue from data store */ override fun restoreQueue() { - while (apiQueue.isNotEmpty()) { - apiQueue.remove() - } + apiQueue.clear() // Keep track if there's any errors restoring from persistent store var wasMutated = false @@ -233,7 +234,8 @@ internal object KlaviyoApiClient : ApiClient { /** * Stop all jobs on our handler thread */ - private fun stopBatch() = handler?.removeCallbacksAndMessages(null).also { + private fun stopBatch() { + handler?.removeCallbacksAndMessages(null) Registry.log.info("Stopped background handler") } diff --git a/sdk/analytics/src/main/java/com/klaviyo/analytics/networking/requests/EventApiRequest.kt b/sdk/analytics/src/main/java/com/klaviyo/analytics/networking/requests/EventApiRequest.kt index 71d0e1e1f..92f9de84b 100644 --- a/sdk/analytics/src/main/java/com/klaviyo/analytics/networking/requests/EventApiRequest.kt +++ b/sdk/analytics/src/main/java/com/klaviyo/analytics/networking/requests/EventApiRequest.kt @@ -4,6 +4,7 @@ import com.klaviyo.analytics.DeviceProperties import com.klaviyo.analytics.model.Event import com.klaviyo.analytics.model.Profile import com.klaviyo.core.Registry +import org.json.JSONObject /** * Defines the content of an API request to track an [Event] for a given [Profile] @@ -40,6 +41,17 @@ internal class EventApiRequest( override val successCodes: IntRange get() = HTTP_ACCEPTED..HTTP_ACCEPTED + override var body: JSONObject? = null + get() { + // Update body to include Device metadata whenever the body is retrieved (typically during sending) so the latest data is included + field?.getJSONObject(DATA)?.getJSONObject(ATTRIBUTES)?.getJSONObject(PROPERTIES)?.apply { + DeviceProperties.buildEventMetaData().forEach { entry -> + put(entry.key, entry.value) + } + } + return field + } + constructor(event: Event, profile: Profile) : this() { body = jsonMapOf( DATA to mapOf( @@ -54,7 +66,7 @@ internal class EventApiRequest( ), VALUE to event.value, TIME to Registry.clock.isoTime(queuedTime), - PROPERTIES to event.toMap() + DeviceProperties.buildEventMetaData(), + PROPERTIES to event.toMap(), allowEmptyMaps = true ) ) diff --git a/sdk/analytics/src/main/java/com/klaviyo/analytics/networking/requests/ProfileApiRequest.kt b/sdk/analytics/src/main/java/com/klaviyo/analytics/networking/requests/ProfileApiRequest.kt index b6c845a72..57a1e8e5d 100644 --- a/sdk/analytics/src/main/java/com/klaviyo/analytics/networking/requests/ProfileApiRequest.kt +++ b/sdk/analytics/src/main/java/com/klaviyo/analytics/networking/requests/ProfileApiRequest.kt @@ -43,7 +43,6 @@ internal class ProfileApiRequest( extract(ProfileKey.ORGANIZATION), extract(ProfileKey.TITLE), extract(ProfileKey.IMAGE), - LOCATION to filteredMapOf( extract(ProfileKey.ADDRESS1), extract(ProfileKey.ADDRESS2), @@ -55,7 +54,6 @@ internal class ProfileApiRequest( extract(ProfileKey.ZIP), extract(ProfileKey.TIMEZONE) ), - PROPERTIES to properties // Any remaining custom keys are properties ) ) @@ -79,13 +77,6 @@ internal class ProfileApiRequest( override val successCodes: IntRange get() = HTTP_ACCEPTED..HTTP_ACCEPTED constructor(profile: Profile) : this() { - // Create a mutable copy of the profile - // We'll pop off all the enumerated keys as we build the body - // Then any remaining pairs are custom keys - val properties = profile.toMap().toMutableMap() - fun extract(key: ProfileKey): Pair = - key.name to properties.remove(key.name) - body = jsonMapOf(*formatBody(profile)) } } diff --git a/sdk/analytics/src/main/java/com/klaviyo/analytics/networking/requests/PushTokenApiRequest.kt b/sdk/analytics/src/main/java/com/klaviyo/analytics/networking/requests/PushTokenApiRequest.kt index 2cdf5fe82..34493284f 100644 --- a/sdk/analytics/src/main/java/com/klaviyo/analytics/networking/requests/PushTokenApiRequest.kt +++ b/sdk/analytics/src/main/java/com/klaviyo/analytics/networking/requests/PushTokenApiRequest.kt @@ -3,6 +3,7 @@ package com.klaviyo.analytics.networking.requests import com.klaviyo.analytics.DeviceProperties import com.klaviyo.analytics.model.Profile import com.klaviyo.core.Registry +import org.json.JSONObject /** * Defines the content of an API request to append a push token to a [Profile] @@ -55,20 +56,49 @@ internal class PushTokenApiRequest( override val successCodes: IntRange get() = HTTP_ACCEPTED..HTTP_ACCEPTED + override var body: JSONObject? = null + get() { + // Update body to include Device metadata whenever the body is retrieved (typically during sending) so the latest data is included + field?.getJSONObject(DATA)?.getJSONObject(ATTRIBUTES)?.apply { + put( + ENABLEMENT_STATUS, + if (DeviceProperties.notificationPermission) NOTIFICATIONS_ENABLED else NOTIFICATIONS_DISABLED + ) + put( + BACKGROUND, + if (DeviceProperties.backgroundData) BG_AVAILABLE else BG_UNAVAILABLE + ) + put(METADATA, JSONObject(DeviceProperties.buildMetaData())) + } + return field + } + + private lateinit var initialBody: String + constructor(token: String, profile: Profile) : this() { body = jsonMapOf( DATA to mapOf( TYPE to PUSH_TOKEN, ATTRIBUTES to filteredMapOf( + PROFILE to mapOf(*ProfileApiRequest.formatBody(profile)), TOKEN to token, PLATFORM to DeviceProperties.platform, - VENDOR to VENDOR_FCM, - ENABLEMENT_STATUS to if (DeviceProperties.notificationPermission) NOTIFICATIONS_ENABLED else NOTIFICATIONS_DISABLED, - BACKGROUND to if (DeviceProperties.backgroundData) BG_AVAILABLE else BG_UNAVAILABLE, - METADATA to DeviceProperties.buildMetaData(), - PROFILE to mapOf(*ProfileApiRequest.formatBody(profile)) + VENDOR to VENDOR_FCM ) ) - ) + ).also { + initialBody = it.toString() + } + } + + override fun equals(other: Any?): Boolean { + return when (other) { + is PushTokenApiRequest -> initialBody == other.initialBody + else -> super.equals(other) + } + } + + override fun hashCode(): Int { + return initialBody.hashCode() } } diff --git a/sdk/analytics/src/test/java/com/klaviyo/analytics/networking/KlaviyoApiClientTest.kt b/sdk/analytics/src/test/java/com/klaviyo/analytics/networking/KlaviyoApiClientTest.kt index 296fd83c4..47256262a 100644 --- a/sdk/analytics/src/test/java/com/klaviyo/analytics/networking/KlaviyoApiClientTest.kt +++ b/sdk/analytics/src/test/java/com/klaviyo/analytics/networking/KlaviyoApiClientTest.kt @@ -153,6 +153,21 @@ internal class KlaviyoApiClientTest : BaseRequestTest() { assertEquals(1, KlaviyoApiClient.getQueueSize()) } + @Test + fun `Enqueuing the same push token API call multiple times only queues the first`() { + assertEquals(0, KlaviyoApiClient.getQueueSize()) + + repeat(5) { + KlaviyoApiClient.enqueuePushToken( + PUSH_TOKEN, + Profile().setAnonymousId(ANON_ID) + ) + } + + assertEquals(1, KlaviyoApiClient.getQueueSize()) + verify(exactly = 1) { logSpy.info("Persisting queue") } + } + @Test fun `Enqueues an event API call`() { assertEquals(0, KlaviyoApiClient.getQueueSize()) diff --git a/sdk/analytics/src/test/java/com/klaviyo/analytics/networking/requests/PushTokenApiRequestTest.kt b/sdk/analytics/src/test/java/com/klaviyo/analytics/networking/requests/PushTokenApiRequestTest.kt index 991650af4..46177e371 100644 --- a/sdk/analytics/src/test/java/com/klaviyo/analytics/networking/requests/PushTokenApiRequestTest.kt +++ b/sdk/analytics/src/test/java/com/klaviyo/analytics/networking/requests/PushTokenApiRequestTest.kt @@ -51,6 +51,13 @@ internal class PushTokenApiRequestTest : BaseRequestTest() { compareJson(requestJson, revivedRequest.toJson()) } + @Test + fun `Requests are equal if the token and profile are equal`() { + val aRequest = PushTokenApiRequest(PUSH_TOKEN, stubProfile) + val bRequest = PushTokenApiRequest(PUSH_TOKEN, stubProfile) + assertEquals(aRequest, bRequest) + } + @Test fun `Builds body request`() { val expectJson = """ diff --git a/versions.gradle b/versions.gradle index 8b19c934e..43aa175fc 100644 --- a/versions.gradle +++ b/versions.gradle @@ -12,8 +12,8 @@ ext { targetSDKVersion = 33 // project versioning - versionCode = 6 - versionName = '1.3.4' // Reminder: Update version in README samples and update documentation branch + versionCode = 7 + versionName = '1.3.5' // Reminder: Update version in README samples and update documentation branch // dependencies coreKTXVersion = '1.9.0'