Skip to content

Commit

Permalink
feat: manual screen tracking (customerio#51)
Browse files Browse the repository at this point in the history
  • Loading branch information
Shahroz16 committed Dec 14, 2021
1 parent c8d8c3f commit 213a278
Show file tree
Hide file tree
Showing 14 changed files with 139 additions and 27 deletions.
5 changes: 4 additions & 1 deletion app/src/main/java/io/customer/example/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,10 @@ class MainActivity : AppCompatActivity() {
CustomerIO.instance().track(
name = "custom class event",
attributes = mapOf("value" to Fol(a = "aa", c = 1))
)
).enqueue(outputCallback)
CustomerIO.instance().screen(
name = "MainActivity"
).enqueue(outputCallback)
}

private fun makeAsynchronousRequest() {
Expand Down
16 changes: 14 additions & 2 deletions sdk/src/main/java/io/customer/sdk/CustomerIO.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package io.customer.sdk

import android.content.Context
import io.customer.base.comunication.Action
import io.customer.sdk.api.CustomerIoApi
import io.customer.sdk.api.CustomerIOApi
import io.customer.sdk.data.communication.CustomerIOUrlHandler
import io.customer.sdk.data.model.Region
import io.customer.sdk.data.request.MetricEvent
Expand All @@ -25,7 +25,7 @@ After the instance is created you can access it via singleton instance: `Custome
class CustomerIO internal constructor(
val config: CustomerIOConfig,
val store: CustomerIOStore,
private val api: CustomerIoApi,
private val api: CustomerIOApi,
) {
companion object {
private var instance: CustomerIO? = null
Expand Down Expand Up @@ -129,6 +129,18 @@ class CustomerIO internal constructor(
attributes: Map<String, Any> = emptyMap()
) = api.track(name, attributes)

/**
* Track screen
* @param name Name of the screen you want to track.
* @param attributes Optional event body in Map format used as JSON object
* @return Action<Unit> which can be accessed via `execute` or `enqueue`
*/
fun screen(
name: String,
attributes: Map<String, Any> = emptyMap()
) = api.screen(name, attributes)

/**
* Stop identifying the currently persisted customer. All future calls to the SDK will no longer
* be associated with the previously identified customer.
Expand Down
20 changes: 17 additions & 3 deletions sdk/src/main/java/io/customer/sdk/CustomerIOClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ package io.customer.sdk
import io.customer.base.comunication.Action
import io.customer.base.data.Result
import io.customer.base.data.Success
import io.customer.sdk.api.CustomerIoApi
import io.customer.sdk.api.CustomerIOApi
import io.customer.sdk.data.model.EventType
import io.customer.sdk.data.request.MetricEvent
import io.customer.sdk.repository.IdentityRepository
import io.customer.sdk.repository.PreferenceRepository
Expand All @@ -19,7 +20,7 @@ internal class CustomerIOClient(
private val preferenceRepository: PreferenceRepository,
private val trackingRepository: TrackingRepository,
private val pushNotificationRepository: PushNotificationRepository
) : CustomerIoApi {
) : CustomerIOApi {

override fun identify(identifier: String, attributes: Map<String, Any>): Action<Unit> {
return object : Action<Unit> {
Expand Down Expand Up @@ -48,8 +49,17 @@ internal class CustomerIOClient(
}

override fun track(name: String, attributes: Map<String, Any>): Action<Unit> {
return track(EventType.event, name, attributes)
}

fun track(eventType: EventType, name: String, attributes: Map<String, Any>): Action<Unit> {
val identifier = preferenceRepository.getIdentifier()
return trackingRepository.track(identifier, name, attributes)
return trackingRepository.track(
identifier = identifier,
type = eventType,
name = name,
attributes = attributes
)
}

override fun clearIdentify() {
Expand Down Expand Up @@ -131,4 +141,8 @@ internal class CustomerIOClient(
event: MetricEvent,
deviceToken: String
) = pushNotificationRepository.trackMetric(deliveryID, event, deviceToken)

override fun screen(name: String, attributes: Map<String, Any>): Action<Unit> {
return track(EventType.screen, name, attributes)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ import io.customer.sdk.data.request.MetricEvent
/**
* Apis exposed to clients
*/
internal interface CustomerIoApi {
internal interface CustomerIOApi {
fun identify(identifier: String, attributes: Map<String, Any>): Action<Unit>
fun track(name: String, attributes: Map<String, Any>): Action<Unit>
fun clearIdentify()
fun registerDeviceToken(deviceToken: String): Action<Unit>
fun deleteDeviceToken(): Action<Unit>
fun trackMetric(deliveryID: String, event: MetricEvent, deviceToken: String): Action<Unit>
fun screen(name: String, attributes: Map<String, Any>): Action<Unit>
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.customer.sdk.api.retrofit

import io.customer.base.comunication.Call
import io.customer.base.comunication.Action
import retrofit2.Call
import retrofit2.CallAdapter
import retrofit2.Retrofit
import java.lang.reflect.ParameterizedType
Expand All @@ -11,9 +12,9 @@ import java.lang.reflect.Type
*/
internal class CustomerIoCallAdapter<T : Any>(
private val responseType: Type
) : CallAdapter<T, CustomerIoCall<T>> {
) : CallAdapter<T, Action<T>> {

override fun adapt(call: retrofit2.Call<T>): CustomerIoCall<T> {
override fun adapt(call: Call<T>): Action<T> {
return CustomerIoCall(call)
}

Expand All @@ -32,13 +33,16 @@ internal class CustomerIoCallAdapterFactory private constructor() : CallAdapter.
annotations: Array<out Annotation>,
retrofit: Retrofit
): CallAdapter<*, *>? {
if (getRawType(returnType) != Call::class.java) {
// ensure enclosing type is 'CustomerIoCall'
if (getRawType(returnType) != CustomerIoCall::class.java) {
return null
}

if (returnType !is ParameterizedType) {
throw IllegalArgumentException("Call return type must be parameterized as Call<Foo>")
}
val responseType: Type = getParameterUpperBound(0, returnType)
return CustomerIoCallAdapter<Any>(responseType)

val type: Type = getParameterUpperBound(0, returnType)
return CustomerIoCallAdapter<Any>(type)
}
}
8 changes: 8 additions & 0 deletions sdk/src/main/java/io/customer/sdk/data/model/EventType.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package io.customer.sdk.data.model

import com.squareup.moshi.JsonClass

@JsonClass(generateAdapter = false)
enum class EventType {
event, screen
}
5 changes: 4 additions & 1 deletion sdk/src/main/java/io/customer/sdk/data/request/Event.kt
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package io.customer.sdk.data.request

import com.squareup.moshi.JsonClass
import io.customer.sdk.data.model.EventType

@JsonClass(generateAdapter = true)
internal data class Event(
val name: String,
val data: Map<String, Any>
val type: EventType,
val data: Map<String, Any>,
val timestamp: Long? = null
)
4 changes: 2 additions & 2 deletions sdk/src/main/java/io/customer/sdk/di/CustomerIOComponent.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import io.customer.sdk.BuildConfig
import io.customer.sdk.CustomerIOClient
import io.customer.sdk.CustomerIOConfig
import io.customer.sdk.Version
import io.customer.sdk.api.CustomerIoApi
import io.customer.sdk.api.CustomerIOApi
import io.customer.sdk.api.interceptors.HeadersInterceptor
import io.customer.sdk.api.retrofit.CustomerIoCallAdapterFactory
import io.customer.sdk.api.service.CustomerService
Expand All @@ -28,7 +28,7 @@ internal class CustomerIOComponent(
private val context: Context
) {

fun buildApi(): CustomerIoApi {
fun buildApi(): CustomerIOApi {
return CustomerIOClient(
identityRepository = IdentityRepositoryImpl(
customerService = buildRetrofitApi<CustomerService>(),
Expand Down
15 changes: 13 additions & 2 deletions sdk/src/main/java/io/customer/sdk/repository/TrackingRepository.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,16 @@ package io.customer.sdk.repository
import io.customer.base.comunication.Action
import io.customer.base.utils.ActionUtils
import io.customer.sdk.api.service.CustomerService
import io.customer.sdk.data.model.EventType
import io.customer.sdk.data.request.Event

internal interface TrackingRepository {
fun track(identifier: String?, name: String, attributes: Map<String, Any>): Action<Unit>
fun track(
identifier: String?,
type: EventType,
name: String,
attributes: Map<String, Any>
): Action<Unit>
}

internal class TrackingRepositoryImp(
Expand All @@ -16,14 +22,19 @@ internal class TrackingRepositoryImp(

override fun track(
identifier: String?,
type: EventType,
name: String,
attributes: Map<String, Any>
): Action<Unit> {
return if (identifier == null) {
return ActionUtils.getUnidentifiedUserAction()
} else customerService.track(
identifier = identifier,
body = Event(name = name, data = attributesRepository.mapToJson(attributes))
body = Event(
type = type,
name = name,
data = attributesRepository.mapToJson(attributes)
)
)
}
}
17 changes: 15 additions & 2 deletions sdk/src/test/java/io/customer/sdk/CustomerIOClientTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ internal class CustomerIOClientTest {
@Test
fun `verify client sends error when tracking repo fails in tracking event`() {
`when`(
trackingRepository.track(any(), any(), any())
trackingRepository.track(any(), any(), any(), any())
).thenReturn(
ActionUtils.getErrorAction(
errorResult = ErrorResult(
Expand All @@ -186,7 +186,7 @@ internal class CustomerIOClientTest {
@Test
fun `verify client sends success when tracking repo succeed in tracking event`() {
`when`(
trackingRepository.track(any(), any(), any())
trackingRepository.track(any(), any(), any(), any())
).thenReturn(ActionUtils.getEmptyAction())

`when`(preferenceRepository.getIdentifier()).thenReturn("identify")
Expand All @@ -195,4 +195,17 @@ internal class CustomerIOClientTest {

verifySuccess(result, Unit)
}

@Test
fun `verify client sends success when tracking repo succeed in screen tracking`() {
`when`(
trackingRepository.track(any(), any(), any(), any())
).thenReturn(ActionUtils.getEmptyAction())

`when`(preferenceRepository.getIdentifier()).thenReturn("identify")

val result = customerIOClient.screen("Home", emptyMap()).execute()

verifySuccess(result, Unit)
}
}
35 changes: 35 additions & 0 deletions sdk/src/test/java/io/customer/sdk/CustomerIOTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,20 @@ internal class CustomerIOTest {
verifySuccess(response, Unit)
}

@Test
fun `verify SDK returns success when screen is tracked`() {
`when`(
mockCustomerIO.api.screen(
name = any(),
attributes = any()
)
).thenReturn(getEmptyAction())

val response = customerIO.screen("Login", mapOf("key" to "value")).execute()

verifySuccess(response, Unit)
}

@Test
fun `verify SDK returns error when event tracking request fails`() {
`when`(
Expand All @@ -104,6 +118,27 @@ internal class CustomerIOTest {
verifyError(response, StatusCode.InternalServerError)
}

@Test
fun `verify SDK returns error when screen tracking request fails`() {
`when`(
mockCustomerIO.api.screen(
name = any(),
attributes = any()
)
).thenReturn(
getErrorAction(
errorResult = ErrorResult(
error =
ErrorDetail(statusCode = StatusCode.BadRequest)
)
)
)

val response = customerIO.screen("Login", emptyMap()).execute()

verifyError(response, StatusCode.BadRequest)
}

@Test
fun `verify SDK returns success when device is added`() {
`when`(
Expand Down
4 changes: 2 additions & 2 deletions sdk/src/test/java/io/customer/sdk/MockCustomerIOBuilder.kt
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package io.customer.sdk

import io.customer.sdk.api.CustomerIoApi
import io.customer.sdk.api.CustomerIOApi
import io.customer.sdk.data.model.Region
import io.customer.sdk.data.store.CustomerIOStore
import org.mockito.kotlin.mock

internal class MockCustomerIOBuilder {

lateinit var api: CustomerIoApi
lateinit var api: CustomerIOApi
lateinit var store: CustomerIOStore
private lateinit var customerIO: CustomerIO

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package io.customer.sdk.api

import io.customer.base.comunication.Call
import io.customer.sdk.api.retrofit.CustomerIoCall
import io.customer.sdk.api.retrofit.CustomerIoCallAdapterFactory
import okhttp3.mockwebserver.MockWebServer
import org.amshove.kluent.fail
Expand Down Expand Up @@ -37,7 +37,7 @@ internal class CustomerIOCallAdapterTest {
@Test
fun `When returning raw call Then should throw an exception`() {
try {
factory[Call::class.java, emptyArray(), retrofit]
factory[CustomerIoCall::class.java, emptyArray(), retrofit]
fail("Assertion failed")
} catch (e: IllegalArgumentException) {
e.message shouldBeEqualTo "Call return type must be parameterized as Call<Foo>"
Expand All @@ -46,7 +46,7 @@ internal class CustomerIOCallAdapterTest {

@Test
fun `When returning raw response type Then adapter should have the same response type`() {
val type: Type = typeOf<Call<String>>().javaType
val type: Type = typeOf<CustomerIoCall<String>>().javaType
val callAdapter = factory[type, emptyArray(), retrofit]

callAdapter.shouldNotBeNull()
Expand All @@ -55,7 +55,7 @@ internal class CustomerIOCallAdapterTest {

@Test
fun `When returning generic response type Then adapter should have the same response type`() {
val type = typeOf<Call<List<String>>>().javaType
val type = typeOf<CustomerIoCall<List<String>>>().javaType
val callAdapter = factory[type, emptyArray(), retrofit]

callAdapter.shouldNotBeNull()
Expand Down
Loading

0 comments on commit 213a278

Please sign in to comment.