Skip to content

Commit

Permalink
fix: queue attempts to run all tasks on each run
Browse files Browse the repository at this point in the history
  • Loading branch information
levibostian committed May 5, 2022
1 parent 81eea4e commit e180dea
Show file tree
Hide file tree
Showing 20 changed files with 468 additions and 205 deletions.
2 changes: 2 additions & 0 deletions common-test/build.gradle
Expand Up @@ -39,4 +39,6 @@ dependencies {
api Dependencies.okhttpMockWebserver

api Dependencies.junit4

api Dependencies.coroutinesTest
}
32 changes: 29 additions & 3 deletions common-test/src/main/java/io/customer/common_test/BaseTest.kt
Expand Up @@ -9,8 +9,12 @@ import io.customer.sdk.data.model.Region
import io.customer.sdk.data.store.DeviceStore
import io.customer.sdk.di.CustomerIOComponent
import io.customer.sdk.util.CioLogLevel
import io.customer.sdk.util.DateUtil
import io.customer.sdk.util.JsonAdapter
import kotlinx.coroutines.test.TestCoroutineDispatcher
import okhttp3.ResponseBody.Companion.toResponseBody
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
import retrofit2.HttpException
import retrofit2.Response
Expand All @@ -29,11 +33,13 @@ abstract class BaseTest {
protected val application: Application
get() = ApplicationProvider.getApplicationContext()

protected val cioConfig: CustomerIOConfig
get() = CustomerIOConfig(siteId, "xyz", Region.EU, 100, null, true, true, 10, 30.0, CioLogLevel.DEBUG)
protected lateinit var cioConfig: CustomerIOConfig

protected val deviceStore: DeviceStore = DeviceStoreStub().deviceStore

// when you need a CoroutineDispatcher in a test function, use this as it runs your tests synchronous.
protected val testDispatcher = TestCoroutineDispatcher()

protected lateinit var di: CustomerIOComponent
protected val jsonAdapter: JsonAdapter
get() = di.jsonAdapter
Expand All @@ -42,14 +48,34 @@ abstract class BaseTest {
protected val http500Error: HttpException
get() = HttpException(Response.error<String>(500, "{}".toResponseBody()))

protected lateinit var mockWebServer: MockWebServer
protected lateinit var dateUtilStub: DateUtilStub

@Before
open fun setup() {
cioConfig = CustomerIOConfig(siteId, "xyz", Region.EU, 100, null, true, true, 10, 30.0, CioLogLevel.DEBUG)

// Initialize the mock web server before constructing DI graph as dependencies may require information such as hostname.
mockWebServer = MockWebServer().apply {
start()
}
cioConfig.trackingApiUrl = mockWebServer.url("/").toString()

di = CustomerIOComponent(
sdkConfig = cioConfig,
context = this@BaseTest.context
)
di.fileStorage.deleteAllSdkFiles()

di.fileStorage.deleteAllSdkFiles()
di.sharedPreferenceRepository.clearAll()

dateUtilStub = DateUtilStub().also {
di.overrideDependency(DateUtil::class.java, it)
}
}

@After
open fun teardown() {
mockWebServer.shutdown()
}
}
Expand Up @@ -4,7 +4,16 @@ import io.customer.sdk.util.JsonAdapter
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.ResponseBody
import okhttp3.ResponseBody.Companion.toResponseBody
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import okhttp3.mockwebserver.SocketPolicy

fun <F : Any> F.toResponseBody(jsonAdapter: JsonAdapter): ResponseBody {
return jsonAdapter.toJson(this).toResponseBody("application/json".toMediaType())
}

fun MockWebServer.enqueueNoInternetConnection() {
// throws an IOException which is a network error
// https://github.com/square/okhttp/issues/3533
enqueue(MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AT_START))
}
8 changes: 8 additions & 0 deletions sdk/src/debug/AndroidManifest.xml
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="io.customer.sdk">

<application
android:networkSecurityConfig="@xml/allow_localhost_cleartext" />
</manifest>
7 changes: 7 additions & 0 deletions sdk/src/debug/res/xml/allow_localhost_cleartext.xml
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<!-- Note: This is only used to allow localhost connections for automated tests to allow connecting to localhost with MockWebServer. This file should only be referred to in the *debug* manifest file and not shipped in the production SDK. -->
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">localhost</domain>
</domain-config>
</network-security-config>
52 changes: 5 additions & 47 deletions sdk/src/main/java/io/customer/sdk/CustomerIOClient.kt
Expand Up @@ -5,17 +5,9 @@ import io.customer.sdk.data.model.CustomAttributes
import io.customer.sdk.data.model.EventType
import io.customer.sdk.data.model.verify
import io.customer.sdk.data.request.Device
import io.customer.sdk.data.request.Event
import io.customer.sdk.data.request.Metric
import io.customer.sdk.data.request.MetricEvent
import io.customer.sdk.data.store.DeviceStore
import io.customer.sdk.queue.Queue
import io.customer.sdk.queue.taskdata.DeletePushNotificationQueueTaskData
import io.customer.sdk.queue.taskdata.IdentifyProfileQueueTaskData
import io.customer.sdk.queue.taskdata.RegisterPushNotificationQueueTaskData
import io.customer.sdk.queue.taskdata.TrackEventQueueTaskData
import io.customer.sdk.queue.type.QueueTaskGroup
import io.customer.sdk.queue.type.QueueTaskType
import io.customer.sdk.repository.PreferenceRepository
import io.customer.sdk.util.DateUtil
import io.customer.sdk.util.Logger
Expand Down Expand Up @@ -52,18 +44,7 @@ internal class CustomerIOClient(
}
}

// If SDK previously identified profile X and X is being identified again, no use blocking the queue with a queue group.
val queueGroupStart = if (isFirstTimeIdentifying || isChangingIdentifiedProfile) QueueTaskGroup.IdentifyProfile(identifier) else null
// If there was a previously identified profile, or, we are just adding attributes to an existing profile, we need to block
// this operation until the previous identify runs successfully.
val blockingGroups = if (currentlyIdentifiedProfileIdentifier != null) listOf(QueueTaskGroup.IdentifyProfile(currentlyIdentifiedProfileIdentifier)) else null

val queueStatus = backgroundQueue.addTask(
QueueTaskType.IdentifyProfile,
IdentifyProfileQueueTaskData(identifier, attributes),
groupStart = queueGroupStart,
blockingGroups = blockingGroups
)
val queueStatus = backgroundQueue.queueIdentifyProfile(identifier, currentlyIdentifiedProfileIdentifier, attributes)

// don't modify the state of the SDK until we confirm we added a queue task successfully.
if (!queueStatus.success) {
Expand Down Expand Up @@ -106,11 +87,7 @@ internal class CustomerIOClient(
return
}

backgroundQueue.addTask(
QueueTaskType.TrackEvent,
TrackEventQueueTaskData(identifier, Event(name, eventType, attributes, dateUtil.nowUnixTimestamp)),
blockingGroups = listOf(QueueTaskGroup.IdentifyProfile(identifier))
)
backgroundQueue.queueTrack(identifier, name, eventType, attributes)
}

override fun clearIdentify() {
Expand Down Expand Up @@ -143,12 +120,7 @@ internal class CustomerIOClient(
attributes = attributes
)

backgroundQueue.addTask(
QueueTaskType.RegisterDeviceToken,
RegisterPushNotificationQueueTaskData(identifiedProfileId, device),
groupStart = QueueTaskGroup.RegisterPushToken(deviceToken),
blockingGroups = listOf(QueueTaskGroup.IdentifyProfile(identifiedProfileId))
)
backgroundQueue.queueRegisterDevice(identifiedProfileId, device)
}

private fun createDeviceAttributes(customAddedAttributes: CustomAttributes): Map<String, Any> {
Expand Down Expand Up @@ -177,12 +149,7 @@ internal class CustomerIOClient(
return
}

backgroundQueue.addTask(
QueueTaskType.DeletePushToken,
DeletePushNotificationQueueTaskData(identifiedProfileId, existingDeviceToken),
// only delete a device token after it has successfully been registered.
blockingGroups = listOf(QueueTaskGroup.RegisterPushToken(existingDeviceToken))
)
backgroundQueue.queueDeletePushToken(identifiedProfileId, existingDeviceToken)
}

override fun trackMetric(
Expand All @@ -193,15 +160,6 @@ internal class CustomerIOClient(
logger.info("push metric ${event.name}")
logger.debug("delivery id $deliveryID device token $deviceToken")

backgroundQueue.addTask(
QueueTaskType.TrackPushMetric,
Metric(
deliveryID = deliveryID,
deviceToken = deviceToken,
event = event,
timestamp = dateUtil.now
),
blockingGroups = listOf(QueueTaskGroup.RegisterPushToken(deviceToken))
)
backgroundQueue.queueTrackMetric(deliveryID, deviceToken, event)
}
}
19 changes: 17 additions & 2 deletions sdk/src/main/java/io/customer/sdk/CustomerIOConfig.kt
Expand Up @@ -23,5 +23,20 @@ data class CustomerIOConfig(
* We do not recommend modifying this value because it impacts battery life of mobile device.
*/
val backgroundQueueSecondsDelay: Double,
val logLevel: CioLogLevel
)
val logLevel: CioLogLevel,
/**
* Base URL to use for the Customer.io track API. You will more then likely not modify this value.
If you override this value, `Region` set when initializing the SDK will be ignored.
*/
var trackingApiUrl: String? = null
) {
internal val trackingApiHostname: String
get() {
return this.trackingApiUrl ?: this.region.let { selectedRegion ->
when (selectedRegion) {
Region.US -> "https://track-sdk.customer.io/"
Region.EU -> "https://track-sdk-eu.customer.io/"
}
}
}
}
6 changes: 3 additions & 3 deletions sdk/src/main/java/io/customer/sdk/data/model/Region.kt
Expand Up @@ -4,11 +4,11 @@ package io.customer.sdk.data.model
* Region that your Customer.io Workspace is located in.
* The SDK will route traffic to the correct data center location depending on the `Region` that you use.
*/
sealed class Region(val code: String, val baseUrl: String) {
sealed class Region(val code: String) {

// Note: These URLs are meant to be used specifically by the official
// mobile SDKs. View our API docs: https://customer.io/docs/api/
// to find the correct hostname for what you're trying to do.
object US : Region(code = "us", baseUrl = "https://track-sdk.customer.io/")
object EU : Region(code = "eu", baseUrl = "https://track-sdk-eu.customer.io/")
object US : Region(code = "us")
object EU : Region(code = "eu")
}
2 changes: 1 addition & 1 deletion sdk/src/main/java/io/customer/sdk/data/request/Device.kt
Expand Up @@ -6,7 +6,7 @@ import io.customer.sdk.data.model.CustomAttributes
import java.util.*

@JsonClass(generateAdapter = true)
internal data class Device(
data class Device(
@field:Json(name = "id") val token: String,
val platform: String = "android",
val lastUsed: Date,
Expand Down
10 changes: 5 additions & 5 deletions sdk/src/main/java/io/customer/sdk/di/CustomerIOComponent.kt
Expand Up @@ -65,11 +65,11 @@ class CustomerIOComponent(

val queue: Queue
get() = override() ?: QueueImpl.getInstanceOrCreate {
QueueImpl(dispatcher = Dispatchers.IO, uiDispatcher = Dispatchers.Main, queueStorage, queueRunRequest, jsonAdapter, sdkConfig, timer, logger)
QueueImpl(dispatcher = Dispatchers.IO, queueStorage, queueRunRequest, jsonAdapter, sdkConfig, timer, logger, dateUtil)
}

val queueQueryRunner: QueueQueryRunner
get() = override() ?: QueueQueryRunnerImpl()
get() = override() ?: QueueQueryRunnerImpl(logger)

val queueRunRequest: QueueRunRequest
get() = override() ?: QueueRunRequestImpl(queueRunner, queueStorage, logger, queueQueryRunner)
Expand All @@ -90,7 +90,7 @@ class CustomerIOComponent(
get() = override() ?: DateUtilImpl()

val timer: SimpleTimer
get() = AndroidSimpleTimer(logger)
get() = AndroidSimpleTimer(logger, uiDispatcher = Dispatchers.Main)

internal fun buildApi(): CustomerIOApi {
return override() ?: CustomerIOClient(
Expand Down Expand Up @@ -122,10 +122,10 @@ class CustomerIOComponent(
)
}

inline fun <reified T> buildRetrofitApi(): T {
private inline fun <reified T> buildRetrofitApi(): T {
val apiClass = T::class.java
return override() ?: buildRetrofit(
sdkConfig.region.baseUrl,
sdkConfig.trackingApiHostname,
sdkConfig.timeout,
).create(apiClass)
}
Expand Down

0 comments on commit e180dea

Please sign in to comment.