Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: improve push delivery for Android 14 and above #493

Merged
merged 7 commits into from
Feb 18, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions core/api/core.api
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,38 @@ public final class io/customer/sdk/data/model/Region$US : io/customer/sdk/data/m
public static final field INSTANCE Lio/customer/sdk/data/model/Region$US;
}

public final class io/customer/sdk/data/model/Settings {
public static final field Companion Lio/customer/sdk/data/model/Settings$Companion;
public synthetic fun <init> (ILjava/lang/String;Ljava/lang/String;Lkotlinx/serialization/internal/SerializationConstructorMarker;)V
public fun <init> (Ljava/lang/String;Ljava/lang/String;)V
public final fun component1 ()Ljava/lang/String;
public final fun component2 ()Ljava/lang/String;
public final fun copy (Ljava/lang/String;Ljava/lang/String;)Lio/customer/sdk/data/model/Settings;
public static synthetic fun copy$default (Lio/customer/sdk/data/model/Settings;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lio/customer/sdk/data/model/Settings;
public fun equals (Ljava/lang/Object;)Z
public final fun getApiHost ()Ljava/lang/String;
public final fun getWriteKey ()Ljava/lang/String;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
public static final fun write$Self (Lio/customer/sdk/data/model/Settings;Lkotlinx/serialization/encoding/CompositeEncoder;Lkotlinx/serialization/descriptors/SerialDescriptor;)V
}

public final class io/customer/sdk/data/model/Settings$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
public static final field INSTANCE Lio/customer/sdk/data/model/Settings$$serializer;
public static final synthetic field descriptor Lkotlinx/serialization/descriptors/SerialDescriptor;
public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/customer/sdk/data/model/Settings;
public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/customer/sdk/data/model/Settings;)V
public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
}

public final class io/customer/sdk/data/model/Settings$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}

public final class io/customer/sdk/events/Metric : java/lang/Enum {
public static final field Clicked Lio/customer/sdk/events/Metric;
public static final field Converted Lio/customer/sdk/events/Metric;
Expand Down
4 changes: 4 additions & 0 deletions core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ plugins {
id 'com.android.library'
id 'kotlin-android'
id 'com.twilio.apkscale'
id 'org.jetbrains.kotlin.plugin.serialization'
}

ext {
Expand Down Expand Up @@ -38,4 +39,7 @@ dependencies {
api project(":base")
api Dependencies.androidxCoreKtx
implementation Dependencies.coroutinesAndroid
// Use this as API so customers can provide objects serializations without
// needing to add it as a dependency to their app
api(Dependencies.kotlinxSerializationJson)
}
6 changes: 6 additions & 0 deletions core/src/main/kotlin/io/customer/sdk/data/model/Settings.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package io.customer.sdk.data.model

import kotlinx.serialization.Serializable

@Serializable
data class Settings(val writeKey: String, val apiHost: String)

Check warning on line 6 in core/src/main/kotlin/io/customer/sdk/data/model/Settings.kt

View check run for this annotation

Codecov / codecov/patch

core/src/main/kotlin/io/customer/sdk/data/model/Settings.kt#L6

Added line #L6 was not covered by tests
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@

import android.content.Context
import androidx.core.content.edit
import io.customer.sdk.data.model.Settings
import kotlinx.serialization.json.Json

/**
* Store for global preferences that are not tied to a specific api key, user
* or any other entity.
*/
interface GlobalPreferenceStore {
fun saveDeviceToken(token: String)
fun saveSettings(value: Settings)
fun getDeviceToken(): String?
fun getSettings(): Settings?
fun removeDeviceToken()
fun clear(key: String)
fun clearAll()
Expand All @@ -27,13 +31,27 @@
putString(KEY_DEVICE_TOKEN, token)
}

override fun saveSettings(value: Settings) = prefs.edit {
putString(KEY_CONFIG_SETTINGS, Json.encodeToString(Settings.serializer(), value))
}

Check warning on line 36 in core/src/main/kotlin/io/customer/sdk/data/store/GlobalPreferenceStore.kt

View check run for this annotation

Codecov / codecov/patch

core/src/main/kotlin/io/customer/sdk/data/store/GlobalPreferenceStore.kt#L34-L36

Added lines #L34 - L36 were not covered by tests

override fun getDeviceToken(): String? = prefs.read {
getString(KEY_DEVICE_TOKEN, null)
}

override fun getSettings(): Settings? = prefs.read {
runCatching {
Json.decodeFromString(
Settings.serializer(),

Check warning on line 45 in core/src/main/kotlin/io/customer/sdk/data/store/GlobalPreferenceStore.kt

View check run for this annotation

Codecov / codecov/patch

core/src/main/kotlin/io/customer/sdk/data/store/GlobalPreferenceStore.kt#L42-L45

Added lines #L42 - L45 were not covered by tests
getString(KEY_CONFIG_SETTINGS, null) ?: return null
)
}.getOrNull()
}

Check warning on line 49 in core/src/main/kotlin/io/customer/sdk/data/store/GlobalPreferenceStore.kt

View check run for this annotation

Codecov / codecov/patch

core/src/main/kotlin/io/customer/sdk/data/store/GlobalPreferenceStore.kt#L49

Added line #L49 was not covered by tests

override fun removeDeviceToken() = clear(KEY_DEVICE_TOKEN)

companion object {
private const val KEY_DEVICE_TOKEN = "device_token"
private const val KEY_CONFIG_SETTINGS = "config_settings"
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package io.customer.datapipelines.util
package io.customer.sdk.util

/**
* Event names to identify specific events in data pipelines so they can be
* reflected on Journeys.
*/
internal object EventNames {
object EventNames {
const val DEVICE_UPDATE = "Device Created or Updated"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious as to why we moved this to core? It seems like those events are very related to Data Pipelines

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because multiple modules are using it

const val DEVICE_DELETE = "Device Deleted"
const val METRIC_DELIVERY = "Report Delivery Event"
Expand Down
3 changes: 0 additions & 3 deletions datapipelines/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,4 @@ dependencies {

implementation(Dependencies.segment)
implementation Dependencies.androidxProcessLifecycle
// Use this as API so customers can provide objects serializations without
// needing to add it as a dependency to their app
api(Dependencies.kotlinxSerializationJson)
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ import com.segment.analytics.kotlin.core.platform.EnrichmentClosure
import com.segment.analytics.kotlin.core.utilities.putAll
import com.segment.analytics.kotlin.core.utilities.putInContextUnderKey
import io.customer.datapipelines.extensions.toJsonObject
import io.customer.datapipelines.util.EventNames
import io.customer.datapipelines.util.SegmentInstantFormatter
import io.customer.sdk.CustomerIO
import io.customer.sdk.core.di.SDKComponent
import io.customer.sdk.core.util.Logger
import io.customer.sdk.util.EventNames
import io.customer.tracking.migration.MigrationAssistant
import io.customer.tracking.migration.MigrationProcessor
import io.customer.tracking.migration.request.MigrationTask
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import com.segment.analytics.kotlin.core.BaseEvent
import com.segment.analytics.kotlin.core.TrackEvent
import com.segment.analytics.kotlin.core.platform.Plugin
import com.segment.analytics.kotlin.core.utilities.putAll
import io.customer.datapipelines.util.EventNames
import io.customer.sdk.util.EventNames
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.jsonObject

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import io.customer.datapipelines.plugins.ContextPlugin
import io.customer.datapipelines.plugins.CustomerIODestination
import io.customer.datapipelines.plugins.DataPipelinePublishedEvents
import io.customer.datapipelines.plugins.ScreenFilterPlugin
import io.customer.datapipelines.util.EventNames
import io.customer.sdk.communication.Event
import io.customer.sdk.communication.subscribe
import io.customer.sdk.core.di.AndroidSDKComponent
Expand All @@ -32,7 +31,9 @@ import io.customer.sdk.core.module.CustomerIOModule
import io.customer.sdk.core.util.CioLogLevel
import io.customer.sdk.core.util.Logger
import io.customer.sdk.data.model.CustomAttributes
import io.customer.sdk.data.model.Settings
import io.customer.sdk.events.TrackMetric
import io.customer.sdk.util.EventNames
import io.customer.tracking.migration.MigrationProcessor
import kotlinx.serialization.SerializationStrategy
import kotlinx.serialization.serializer
Expand Down Expand Up @@ -173,6 +174,12 @@ class CustomerIO private constructor(
logger.debug("CustomerIO SDK initialized with DataPipelines module.")
// Migrate unsent events from previous version
migrateTrackingEvents()

// save settings to storage
analytics.configuration.let { config ->
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this configuration object is available through analytics, what's the main reason for storing it locally? Is it for cases when the SDK hasn't been initialized yet?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

analytics is a dependency/part of data pipeline module, while push module doesn't rely on it. It just needs writeKey and endpoint that it can use to make the HTTP request.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add a test for this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me try adding test for these

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be nice to add these tests for the EU region as well

val settings = Settings(writeKey = config.writeKey, apiHost = config.apiHost)
globalPreferenceStore.saveSettings(settings)
}
}

override var profileAttributes: CustomAttributes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ import io.customer.datapipelines.testutils.extensions.deviceToken
import io.customer.datapipelines.testutils.extensions.encodeToJsonElement
import io.customer.datapipelines.testutils.extensions.shouldMatchTo
import io.customer.datapipelines.testutils.extensions.toJsonObject
import io.customer.datapipelines.util.EventNames
import io.customer.sdk.core.di.SDKComponent
import io.customer.sdk.data.model.CustomAttributes
import io.customer.sdk.data.store.DeviceStore
import io.customer.sdk.data.store.GlobalPreferenceStore
import io.customer.sdk.events.Metric
import io.customer.sdk.events.TrackMetric
import io.customer.sdk.util.EventNames
import io.mockk.every
import kotlinx.coroutines.test.runTest
import kotlinx.serialization.json.JsonArray
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,12 @@ import io.customer.datapipelines.testutils.utils.OutputReaderPlugin
import io.customer.datapipelines.testutils.utils.identifyEvents
import io.customer.datapipelines.testutils.utils.screenEvents
import io.customer.datapipelines.testutils.utils.trackEvents
import io.customer.datapipelines.util.EventNames
import io.customer.sdk.core.di.SDKComponent
import io.customer.sdk.data.model.CustomAttributes
import io.customer.sdk.data.model.Settings
import io.customer.sdk.data.store.DeviceStore
import io.customer.sdk.data.store.GlobalPreferenceStore
import io.customer.sdk.util.EventNames
import io.mockk.every
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
Expand Down Expand Up @@ -593,6 +594,11 @@ class DataPipelinesInteractionTests : JUnitTest() {
deviceRegisterEvent.context.deviceToken shouldBeEqualTo givenToken
}

@Test
fun device_givenSDKInitialized_expectSettingsToBeStored() {
assertCalledOnce { globalPreferenceStore.saveSettings(Settings(writeKey = analytics.configuration.writeKey, apiHost = analytics.configuration.apiHost)) }
}

@Test
fun device_givenRegisterTokenWhenNoProfileIdentified_expectStoreAndRegisterDeviceForAnonymousProfile() {
val givenToken = String.random
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ import io.customer.datapipelines.testutils.extensions.deviceToken
import io.customer.datapipelines.testutils.extensions.encodeToJsonValue
import io.customer.datapipelines.testutils.utils.OutputReaderPlugin
import io.customer.datapipelines.testutils.utils.trackEvents
import io.customer.datapipelines.util.EventNames
import io.customer.sdk.core.di.SDKComponent
import io.customer.sdk.data.store.GlobalPreferenceStore
import io.customer.sdk.util.EventNames
import io.mockk.every
import kotlinx.serialization.json.booleanOrNull
import kotlinx.serialization.json.intOrNull
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ import io.customer.datapipelines.testutils.utils.OutputReaderPlugin
import io.customer.datapipelines.testutils.utils.identifyEvents
import io.customer.datapipelines.testutils.utils.screenEvents
import io.customer.datapipelines.testutils.utils.trackEvents
import io.customer.datapipelines.util.EventNames
import io.customer.datapipelines.util.SegmentInstantFormatter
import io.customer.sdk.core.di.SDKComponent
import io.customer.sdk.data.store.GlobalPreferenceStore
import io.customer.sdk.events.Metric
import io.customer.sdk.events.serializedName
import io.customer.sdk.util.EventNames
import io.customer.tracking.migration.MigrationProcessor
import io.customer.tracking.migration.request.MigrationTask
import io.mockk.every
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package io.customer.messagingpush

import io.customer.messagingpush.di.httpClient
import io.customer.messagingpush.network.HttpClient
import io.customer.messagingpush.network.HttpRequestParams
import io.customer.sdk.core.di.SDKComponent
import io.customer.sdk.util.EventNames
import org.json.JSONObject

internal interface PushDeliveryTracker {
fun trackMetric(token: String, event: String, deliveryId: String, onComplete: ((Result<Unit>) -> Unit?)? = null)
}

internal class PushDeliveryTrackerImpl : PushDeliveryTracker {

private val httpClient: HttpClient
get() = SDKComponent.httpClient

/**
* Tracks a metric by performing a single POST request with JSON.
* Returns a `Result<Unit>`.
*/
override fun trackMetric(
token: String,
event: String,
deliveryId: String,
onComplete: ((Result<Unit>) -> Unit?)?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need a callback here? I'm not sure if the caller can do much in case of failure?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added it, in case we wanted to do some logging based on it or perform any operation based on its result.

) {
val propertiesJson = JSONObject().apply {
put("recipient", token)
put("metric", event.lowercase())
put("deliveryId", deliveryId)
}
val topLevelJson = JSONObject().apply {
put("anonymousId", deliveryId)
put("properties", propertiesJson)
put("event", EventNames.METRIC_DELIVERY)
}

val params = HttpRequestParams(
path = "/track",
headers = mapOf(
"Content-Type" to "application/json; charset=utf-8"
),
body = topLevelJson.toString()
)

// Perform request
httpClient.request(params) { result ->
val mappedResult = result.map { /* we only need success/failure */ }
if (onComplete != null) {
onComplete(mappedResult)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ package io.customer.messagingpush.di
import io.customer.base.internal.InternalCustomerIOApi
import io.customer.messagingpush.MessagingPushModuleConfig
import io.customer.messagingpush.ModuleMessagingPushFCM
import io.customer.messagingpush.PushDeliveryTracker
import io.customer.messagingpush.PushDeliveryTrackerImpl
import io.customer.messagingpush.network.HttpClient
import io.customer.messagingpush.network.HttpClientImpl
import io.customer.messagingpush.processor.PushMessageProcessor
import io.customer.messagingpush.processor.PushMessageProcessorImpl
import io.customer.messagingpush.provider.DeviceTokenProvider
Expand Down Expand Up @@ -45,3 +49,9 @@ internal val SDKComponent.pushMessageProcessor: PushMessageProcessor
deepLinkUtil = deepLinkUtil
)
}

internal val SDKComponent.httpClient: HttpClient
get() = singleton<HttpClient> { HttpClientImpl() }

internal val SDKComponent.pushDeliveryTracker: PushDeliveryTracker
get() = singleton<PushDeliveryTracker> { PushDeliveryTrackerImpl() }
Loading
Loading