Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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: String)
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: String) = prefs.edit {
Copy link
Contributor

Choose a reason for hiding this comment

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

Does it make sense for this method to accept Settings so that the encoding/decoding is encapsulated within this store?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Think it does make sense, i wanted to avoid strong coupling of serialization with storage, but since string format could it make it generic and prone to break, it probably wiser to make this method stricter.

putString(KEY_CONFIG_SETTINGS, 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 {
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_UPDATE = "Device Created or Updated"
const val DEVICE_DELETE = "Device Deleted"
const val METRIC_DELIVERY = "Report Delivery Event"
Expand Down
1 change: 1 addition & 0 deletions datapipelines/api/datapipelines.api
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ public final class io/customer/sdk/CustomerIO : io/customer/sdk/DataPipelineInst
public fun getProfileAttributes ()Ljava/util/Map;
public fun getRegisteredDeviceToken ()Ljava/lang/String;
public fun getUserId ()Ljava/lang/String;
public final fun getWriteKey ()Ljava/lang/String;
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't see this method actually in DataPipelinesModuleConfig, I might be missing something though

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch, it was an unused variable added to CustomerIO class.

public fun identify (Ljava/lang/String;Ljava/lang/Object;Lkotlinx/serialization/SerializationStrategy;)V
public fun initialize ()V
public static final fun instance ()Lio/customer/sdk/CustomerIO;
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
15 changes: 14 additions & 1 deletion datapipelines/src/main/kotlin/io/customer/sdk/CustomerIO.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
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,9 +31,12 @@
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.json.Json
import kotlinx.serialization.serializer

/**
Expand Down Expand Up @@ -173,6 +175,14 @@
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(
Json.encodeToString(Settings.serializer(), settings)
)
}
}

override var profileAttributes: CustomAttributes
Expand Down Expand Up @@ -282,6 +292,9 @@
override val userId: String?
get() = analytics.userId()

val writeKey: String
get() = analytics.configuration.writeKey

Check warning on line 296 in datapipelines/src/main/kotlin/io/customer/sdk/CustomerIO.kt

View check run for this annotation

Codecov / codecov/patch

datapipelines/src/main/kotlin/io/customer/sdk/CustomerIO.kt#L296

Added line #L296 was not covered by tests

override var deviceAttributes: CustomAttributes
get() = emptyMap()
set(value) {
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,11 @@ 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.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
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
13 changes: 13 additions & 0 deletions messagingpush/api/messagingpush.api
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,19 @@ public final class io/customer/messagingpush/ModuleMessagingPushFCM : io/custome
public final class io/customer/messagingpush/ModuleMessagingPushFCM$Companion {
}

public abstract interface class io/customer/messagingpush/PushDeliveryTracker {
public abstract fun trackMetric (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V
}

public final class io/customer/messagingpush/PushDeliveryTracker$DefaultImpls {
public static synthetic fun trackMetric$default (Lio/customer/messagingpush/PushDeliveryTracker;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
}

public final class io/customer/messagingpush/PushDeliveryTrackerImpl : io/customer/messagingpush/PushDeliveryTracker {
public fun <init> ()V
public fun trackMetric (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V
}

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 those to be exposed publicly?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I thought about it, made them public when I was adding test for it. But I am going to revisit it.

public final class io/customer/messagingpush/activity/NotificationClickReceiverActivity : android/app/Activity, io/customer/sdk/tracking/TrackableScreen {
public static final field Companion Lio/customer/messagingpush/activity/NotificationClickReceiverActivity$Companion;
public static final field NOTIFICATION_PAYLOAD_EXTRA Ljava/lang/String;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package io.customer.messagingpush

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

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

class PushDeliveryTrackerImpl : PushDeliveryTracker {
Copy link
Contributor

Choose a reason for hiding this comment

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

Do any of these types need to be public?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I thought about it, made them public when I was adding test for it. But I am going to revisit it.


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)

Check warning on line 52 in messagingpush/src/main/java/io/customer/messagingpush/PushDeliveryTracker.kt

View check run for this annotation

Codecov / codecov/patch

messagingpush/src/main/java/io/customer/messagingpush/PushDeliveryTracker.kt#L52

Added line #L52 was not covered by tests
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@

package io.customer.messagingpush.di

import HttpClient
import HttpClientImpl
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.processor.PushMessageProcessor
import io.customer.messagingpush.processor.PushMessageProcessorImpl
import io.customer.messagingpush.provider.DeviceTokenProvider
Expand All @@ -15,6 +19,7 @@ import io.customer.messagingpush.util.PushTrackingUtil
import io.customer.messagingpush.util.PushTrackingUtilImpl
import io.customer.sdk.core.di.AndroidSDKComponent
import io.customer.sdk.core.di.SDKComponent
import io.customer.sdk.core.di.SDKComponent.singleton

/*
This file contains a series of extensions to the common module's Dependency injection (DI) graph. All extensions in this file simply add internal classes for this module into the DI graph.
Expand Down Expand Up @@ -45,3 +50,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