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

feat: support for custom device attributes and config #77

Merged
merged 4 commits into from Mar 24, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 8 additions & 1 deletion app/src/main/java/io/customer/example/MainActivity.kt
Expand Up @@ -28,10 +28,17 @@ class MainActivity : AppCompatActivity() {
// log events
// makeEventsRequests()

// add custom attributes
makeAddCustomDeviceAttributesRequest()

// register device
makeRegisterDeviceRequest()
}

private fun makeAddCustomDeviceAttributesRequest() {
CustomerIO.instance().deviceAttributes.putAll(mapOf("bingo" to "heyaa"))
}

private fun makeRegisterDeviceRequest() {
CustomerIO.instance().registerDeviceToken("token").enqueue(outputCallback)
}
Expand Down Expand Up @@ -99,7 +106,7 @@ class MainActivity : AppCompatActivity() {
when (
val result =
CustomerIO.instance().identify(
identifier = "support-ticket-test",
identifier = "device-attri",
Shahroz16 marked this conversation as resolved.
Show resolved Hide resolved
mapOf("created_at" to 1642659790)
).execute()
) {
Expand Down
25 changes: 23 additions & 2 deletions sdk/src/main/java/io/customer/sdk/CustomerIO.kt
Expand Up @@ -48,6 +48,7 @@ class CustomerIO internal constructor(
private var timeout = 6000L
private var urlHandler: CustomerIOUrlHandler? = null
private var shouldAutoRecordScreenViews: Boolean = false
private var autoTrackDeviceAttributes: Boolean = true

private lateinit var activityLifecycleCallback: CustomerIOActivityLifecycleCallbacks

Expand All @@ -66,6 +67,11 @@ class CustomerIO internal constructor(
return this
}

fun autoTrackDeviceAttributes(shouldTrackDeviceAttributes: Boolean): Builder {
this.autoTrackDeviceAttributes = shouldTrackDeviceAttributes
return this
levibostian marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Override url/deep link handling
*
Expand All @@ -92,7 +98,8 @@ class CustomerIO internal constructor(
region = region,
timeout = timeout,
urlHandler = urlHandler,
autoTrackScreenViews = shouldAutoRecordScreenViews
autoTrackScreenViews = shouldAutoRecordScreenViews,
autoTrackDeviceAttributes = autoTrackDeviceAttributes,
)

val customerIoComponent =
Expand Down Expand Up @@ -178,7 +185,15 @@ class CustomerIO internal constructor(
* is no active customer, this will fail to register the device
*/
fun registerDeviceToken(deviceToken: String): Action<Unit> =
api.registerDeviceToken(deviceToken, store.deviceStore.buildDeviceAttributes())
api.registerDeviceToken(deviceToken, collectDeviceAttributes())

private fun collectDeviceAttributes(): Map<String, Any> {
return if (config.autoTrackDeviceAttributes) {
deviceAttributes + store.deviceStore.buildDeviceAttributes()
} else {
deviceAttributes
}
}

/**
* Delete the currently registered device token
Expand All @@ -198,6 +213,12 @@ class CustomerIO internal constructor(
deviceToken = deviceToken
)

/**
* Use to provide additional and custom device attributes
* apart from the ones the SDK is programmed to send to customer workspace.
*/
val deviceAttributes: MutableMap<String, Any> = mutableMapOf()

private fun recordScreenViews(activity: Activity, attributes: Map<String, Any>): Action<Unit> {
val packageManager = activity.packageManager
return try {
Expand Down
5 changes: 3 additions & 2 deletions sdk/src/main/java/io/customer/sdk/CustomerIOConfig.kt
Expand Up @@ -3,11 +3,12 @@ package io.customer.sdk
import io.customer.sdk.data.communication.CustomerIOUrlHandler
import io.customer.sdk.data.model.Region

class CustomerIOConfig(
data class CustomerIOConfig(
val siteId: String,
val apiKey: String,
val region: Region,
val timeout: Long,
val urlHandler: CustomerIOUrlHandler?,
val autoTrackScreenViews: Boolean
val autoTrackScreenViews: Boolean,
val autoTrackDeviceAttributes: Boolean,
)
Expand Up @@ -8,7 +8,7 @@ interface ApplicationStore {
val customerAppName: String
val customerAppVersion: String

val isPushSubscribed: Boolean
val isPushEnabled: Boolean
}

internal class ApplicationStoreImp(val context: Context) : ApplicationStore {
Expand All @@ -19,7 +19,7 @@ internal class ApplicationStoreImp(val context: Context) : ApplicationStore {
get() = appInfo.first
override val customerAppVersion: String
get() = appInfo.second
override val isPushSubscribed: Boolean
override val isPushEnabled: Boolean
get() = NotificationManagerCompat.from(context).areNotificationsEnabled()

private fun getAppInformation(): Pair<String, String> {
Expand Down
4 changes: 2 additions & 2 deletions sdk/src/main/java/io/customer/sdk/data/store/BuildStore.kt
Expand Up @@ -17,7 +17,7 @@ interface BuildStore {
// Android SDK Version: 21
val deviceOSVersion: Int

// Device locale: en
// Device locale: en-US
Copy link
Member

Choose a reason for hiding this comment

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

From conversations with LJ, I believe the backend is looking for en_US (notice underscore).

val deviceLocale: String
}

Expand All @@ -32,5 +32,5 @@ internal class BuildStoreImp : BuildStore {
override val deviceOSVersion: Int
get() = Build.VERSION.SDK_INT
override val deviceLocale: String
get() = Locale.getDefault().language
get() = Locale.getDefault().toLanguageTag()
}
6 changes: 3 additions & 3 deletions sdk/src/main/java/io/customer/sdk/data/store/DeviceStore.kt
Expand Up @@ -39,8 +39,8 @@ internal class DeviceStoreImp(
get() = applicationStore.customerAppName
override val customerAppVersion: String
get() = applicationStore.customerAppVersion
override val isPushSubscribed: Boolean
get() = applicationStore.isPushSubscribed
override val isPushEnabled: Boolean
get() = applicationStore.isPushEnabled
override val customerIOVersion: String
get() = version

Expand All @@ -60,7 +60,7 @@ internal class DeviceStoreImp(
"app_version" to customerAppVersion,
"cio_sdk_version" to customerIOVersion,
"device_locale" to deviceLocale,
"push_subscribed" to isPushSubscribed
"push_enabled" to isPushEnabled
)
}
}
103 changes: 95 additions & 8 deletions sdk/src/test/java/io/customer/sdk/CustomerIOTest.kt
@@ -1,18 +1,24 @@
package io.customer.sdk

import android.net.Uri
import io.customer.base.data.ErrorResult
import io.customer.base.error.ErrorDetail
import io.customer.base.error.StatusCode
import io.customer.base.utils.ActionUtils.Companion.getEmptyAction
import io.customer.base.utils.ActionUtils.Companion.getErrorAction
import io.customer.sdk.MockCustomerIOBuilder.Companion.defaultConfig
import io.customer.sdk.data.communication.CustomerIOUrlHandler
import io.customer.sdk.data.model.Region
import io.customer.sdk.data.request.MetricEvent
import io.customer.sdk.utils.verifyError
import io.customer.sdk.utils.verifySuccess
import org.amshove.kluent.`should be equal to`
import org.amshove.kluent.shouldBeEqualTo
import org.amshove.kluent.shouldNotBeEqualTo
import org.junit.Before
import org.junit.Test
import org.mockito.Mockito.`when`
import org.mockito.kotlin.any
import org.mockito.kotlin.verify

internal class CustomerIOTest : BaseTest() {

Expand All @@ -28,13 +34,44 @@ internal class CustomerIOTest : BaseTest() {
}

@Test
fun `verify SDK configuration is correct`() {
customerIO.config.siteId `should be equal to` MockCustomerIOBuilder.siteId
customerIO.config.apiKey `should be equal to` MockCustomerIOBuilder.apiKey
customerIO.config.timeout `should be equal to` MockCustomerIOBuilder.timeout.toLong()
customerIO.config.region `should be equal to` MockCustomerIOBuilder.region
customerIO.config.urlHandler `should be equal to` MockCustomerIOBuilder.urlHandler
customerIO.config.autoTrackScreenViews `should be equal to` MockCustomerIOBuilder.shouldAutoRecordScreenViews
fun `verify SDK default configuration is correct`() {
customerIO.config.siteId shouldBeEqualTo MockCustomerIOBuilder.siteId
customerIO.config.apiKey shouldBeEqualTo MockCustomerIOBuilder.apiKey
customerIO.config.timeout shouldBeEqualTo MockCustomerIOBuilder.timeout.toLong()
customerIO.config.region shouldBeEqualTo MockCustomerIOBuilder.region
customerIO.config.urlHandler shouldBeEqualTo MockCustomerIOBuilder.urlHandler
customerIO.config.autoTrackScreenViews shouldBeEqualTo MockCustomerIOBuilder.shouldAutoRecordScreenViews
customerIO.config.autoTrackDeviceAttributes shouldBeEqualTo MockCustomerIOBuilder.autoTrackDeviceAttributes
}

@Test
fun verify_onUpdatingBuilderConfigurations_expectCustomerIOOConfigToBeUpdated() {

val mockCustomerIOBuilder =
MockCustomerIOBuilder(
defaultConfig.copy(
autoTrackDeviceAttributes = false,
autoTrackScreenViews = true,
siteId = "new-id",
apiKey = "new-key",
region = Region.EU,
urlHandler = object : CustomerIOUrlHandler {
override fun handleCustomerIOUrl(uri: Uri): Boolean {
return true
}
},
timeout = 9000
)
)
customerIO = mockCustomerIOBuilder.build()

customerIO.config.siteId shouldNotBeEqualTo defaultConfig.siteId
customerIO.config.apiKey shouldNotBeEqualTo defaultConfig.apiKey
customerIO.config.timeout shouldNotBeEqualTo defaultConfig.timeout
customerIO.config.region shouldNotBeEqualTo defaultConfig.region
customerIO.config.urlHandler shouldNotBeEqualTo defaultConfig.urlHandler
customerIO.config.autoTrackScreenViews shouldNotBeEqualTo defaultConfig.autoTrackScreenViews
customerIO.config.autoTrackDeviceAttributes shouldNotBeEqualTo defaultConfig.autoTrackDeviceAttributes
}

@Test
Expand Down Expand Up @@ -156,6 +193,56 @@ internal class CustomerIOTest : BaseTest() {
verifySuccess(response, Unit)
}

@Test
fun verify_bothDefaultAndCustomAttributesGetsAdded_withRegisterToken() {

`when`(
mockCustomerIO.api.registerDeviceToken(
any(),
any(),
)
).thenReturn(getEmptyAction())

customerIO.deviceAttributes.putAll(mapOf("test" to "value"))

val expectedToken = "token"

val expectedAttributes = customerIO.deviceAttributes + deviceStore.buildDeviceAttributes()
Copy link
Member

Choose a reason for hiding this comment

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

I have an idea to improve the quality of this test.

For the expected value, have as little logic involved in generating the expected value is a good idea. What I mean by logic is:

// logic is involved to create the expected value
val expected = 2 + 2 // <-- if this had a bug in it, the test could give a false positive

// compared to no logic
val expected = 4 // <-- no logic == less chance of encountering a false positive. 

In my past experience, I have encountered times where my test functions gave false positives because what I truly expected to be the expected value was incorrect. That was because it had too much logic involved where the logic actually had a bug in it.

So, I would suggest that this line of code instead says:

// instead of having logic generate the expected value, hard-code the expected value in. 
val expectedAttributes = mapOf("test" to "value, "push_enabled" to "true)

yes, this would require modifying the returned value from deviceStore.buildDeviceAttriutes() to something hard-coded/predictable but stubs should give the ability to modify returned values so it shouldn't be a problem.


val response = customerIO.registerDeviceToken(expectedToken).execute()

verify(mockCustomerIO.api).registerDeviceToken(expectedToken, expectedAttributes)

verifySuccess(response, Unit)
}

@Test
fun verify_defaultAttributesGetsSkipped_onBasisOfConfig() {

val mockCustomerIO =
MockCustomerIOBuilder(MockCustomerIOBuilder.defaultConfig.copy(autoTrackDeviceAttributes = false))
customerIO = mockCustomerIO.build()

`when`(mockCustomerIO.store.deviceStore).thenReturn(deviceStore)

`when`(
mockCustomerIO.api.registerDeviceToken(
any(),
any(),
)
).thenReturn(getEmptyAction())

customerIO.deviceAttributes.putAll(mapOf("test" to "value"))

val expectedToken = "token"

val expectedAttributes = mapOf("test" to "value")

customerIO.registerDeviceToken(expectedToken).execute()

verify(mockCustomerIO.api).registerDeviceToken(expectedToken, expectedAttributes)
}

@Test
fun `verify SDK returns error when adding device request fails`() {
`when`(
Expand Down
13 changes: 8 additions & 5 deletions sdk/src/test/java/io/customer/sdk/MockCustomerIOBuilder.kt
Expand Up @@ -5,7 +5,7 @@ import io.customer.sdk.data.model.Region
import io.customer.sdk.data.store.CustomerIOStore
import org.mockito.kotlin.mock

internal class MockCustomerIOBuilder {
internal class MockCustomerIOBuilder(private val customerIOConfig: CustomerIOConfig = defaultConfig) {

lateinit var api: CustomerIOApi
lateinit var store: CustomerIOStore
Expand All @@ -18,17 +18,20 @@ internal class MockCustomerIOBuilder {
const val timeout = 6000
val urlHandler = null
const val shouldAutoRecordScreenViews = false
}
const val autoTrackDeviceAttributes = true
levibostian marked this conversation as resolved.
Show resolved Hide resolved

fun build(): CustomerIO {
val customerIOConfig = CustomerIOConfig(
val defaultConfig = CustomerIOConfig(
apiKey = "mock-key",
siteId = "mock-site",
region = Region.US,
timeout = 6000,
urlHandler = urlHandler,
autoTrackScreenViews = shouldAutoRecordScreenViews
autoTrackScreenViews = shouldAutoRecordScreenViews,
autoTrackDeviceAttributes = autoTrackDeviceAttributes
)
}

fun build(): CustomerIO {

api = mock()
store = mock()
Expand Down
6 changes: 3 additions & 3 deletions sdk/src/test/java/io/customer/sdk/store/DeviceStoreTest.kt
Expand Up @@ -13,7 +13,7 @@ internal class DeviceStoreTest : BaseTest() {
deviceStore.deviceModel `should be equal to` "Pixel 6"
deviceStore.deviceManufacturer `should be equal to` "Google"
deviceStore.deviceOSVersion `should be equal to` 30
deviceStore.deviceLocale `should be equal to` "en"
deviceStore.deviceLocale `should be equal to` "en-US"
}

@Test
Expand All @@ -36,8 +36,8 @@ internal class DeviceStoreTest : BaseTest() {
"device_model" to "Pixel 6",
"app_version" to "1.0",
"cio_sdk_version" to "1.0.0-alpha.6",
"device_locale" to "en",
"push_subscribed" to true
"device_locale" to "en-US",
"push_enabled" to true
)

resultDeviceAttributes shouldBeEqualTo expectedDeviceAttributes
Expand Down
4 changes: 2 additions & 2 deletions sdk/src/test/java/io/customer/sdk/utils/DeviceStoreStub.kt
Expand Up @@ -20,14 +20,14 @@ class DeviceStoreStub {
override val deviceOSVersion: Int
get() = 30
override val deviceLocale: String
get() = Locale.US.language
get() = Locale.US.toLanguageTag()
},
applicationStore = object : ApplicationStore {
override val customerAppName: String
get() = "User App"
override val customerAppVersion: String
get() = "1.0"
override val isPushSubscribed: Boolean
override val isPushEnabled: Boolean
get() = true
},
version = "1.0.0-alpha.6"
Expand Down