diff --git a/app/src/main/java/io/customer/example/MainActivity.kt b/app/src/main/java/io/customer/example/MainActivity.kt index 35b45ed99..932ae3d23 100644 --- a/app/src/main/java/io/customer/example/MainActivity.kt +++ b/app/src/main/java/io/customer/example/MainActivity.kt @@ -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() { diff --git a/sdk/src/main/java/io/customer/sdk/CustomerIO.kt b/sdk/src/main/java/io/customer/sdk/CustomerIO.kt index e2b16ec4f..0dcb37e35 100644 --- a/sdk/src/main/java/io/customer/sdk/CustomerIO.kt +++ b/sdk/src/main/java/io/customer/sdk/CustomerIO.kt @@ -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 @@ -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 @@ -129,6 +129,18 @@ class CustomerIO internal constructor( attributes: Map = 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 which can be accessed via `execute` or `enqueue` + */ + fun screen( + name: String, + attributes: Map = 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. diff --git a/sdk/src/main/java/io/customer/sdk/CustomerIOClient.kt b/sdk/src/main/java/io/customer/sdk/CustomerIOClient.kt index c67667a4e..d4ee5ed43 100644 --- a/sdk/src/main/java/io/customer/sdk/CustomerIOClient.kt +++ b/sdk/src/main/java/io/customer/sdk/CustomerIOClient.kt @@ -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 @@ -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): Action { return object : Action { @@ -48,8 +49,17 @@ internal class CustomerIOClient( } override fun track(name: String, attributes: Map): Action { + return track(EventType.event, name, attributes) + } + + fun track(eventType: EventType, name: String, attributes: Map): Action { val identifier = preferenceRepository.getIdentifier() - return trackingRepository.track(identifier, name, attributes) + return trackingRepository.track( + identifier = identifier, + type = eventType, + name = name, + attributes = attributes + ) } override fun clearIdentify() { @@ -131,4 +141,8 @@ internal class CustomerIOClient( event: MetricEvent, deviceToken: String ) = pushNotificationRepository.trackMetric(deliveryID, event, deviceToken) + + override fun screen(name: String, attributes: Map): Action { + return track(EventType.screen, name, attributes) + } } diff --git a/sdk/src/main/java/io/customer/sdk/api/CustomerIoApi.kt b/sdk/src/main/java/io/customer/sdk/api/CustomerIOApi.kt similarity index 83% rename from sdk/src/main/java/io/customer/sdk/api/CustomerIoApi.kt rename to sdk/src/main/java/io/customer/sdk/api/CustomerIOApi.kt index 77bc53b6e..db03329ad 100644 --- a/sdk/src/main/java/io/customer/sdk/api/CustomerIoApi.kt +++ b/sdk/src/main/java/io/customer/sdk/api/CustomerIOApi.kt @@ -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): Action fun track(name: String, attributes: Map): Action fun clearIdentify() fun registerDeviceToken(deviceToken: String): Action fun deleteDeviceToken(): Action fun trackMetric(deliveryID: String, event: MetricEvent, deviceToken: String): Action + fun screen(name: String, attributes: Map): Action } diff --git a/sdk/src/main/java/io/customer/sdk/api/retrofit/CustomerIoCallAdapter.kt b/sdk/src/main/java/io/customer/sdk/api/retrofit/CustomerIoCallAdapter.kt index 08d1a22a4..7d04ddd30 100644 --- a/sdk/src/main/java/io/customer/sdk/api/retrofit/CustomerIoCallAdapter.kt +++ b/sdk/src/main/java/io/customer/sdk/api/retrofit/CustomerIoCallAdapter.kt @@ -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 @@ -11,9 +12,9 @@ import java.lang.reflect.Type */ internal class CustomerIoCallAdapter( private val responseType: Type -) : CallAdapter> { +) : CallAdapter> { - override fun adapt(call: retrofit2.Call): CustomerIoCall { + override fun adapt(call: Call): Action { return CustomerIoCall(call) } @@ -32,13 +33,16 @@ internal class CustomerIoCallAdapterFactory private constructor() : CallAdapter. annotations: Array, 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") } - val responseType: Type = getParameterUpperBound(0, returnType) - return CustomerIoCallAdapter(responseType) + + val type: Type = getParameterUpperBound(0, returnType) + return CustomerIoCallAdapter(type) } } diff --git a/sdk/src/main/java/io/customer/sdk/data/model/EventType.kt b/sdk/src/main/java/io/customer/sdk/data/model/EventType.kt new file mode 100644 index 000000000..887245cf1 --- /dev/null +++ b/sdk/src/main/java/io/customer/sdk/data/model/EventType.kt @@ -0,0 +1,8 @@ +package io.customer.sdk.data.model + +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = false) +enum class EventType { + event, screen +} diff --git a/sdk/src/main/java/io/customer/sdk/data/request/Event.kt b/sdk/src/main/java/io/customer/sdk/data/request/Event.kt index 72ec5359f..d15c385f9 100644 --- a/sdk/src/main/java/io/customer/sdk/data/request/Event.kt +++ b/sdk/src/main/java/io/customer/sdk/data/request/Event.kt @@ -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 + val type: EventType, + val data: Map, + val timestamp: Long? = null ) diff --git a/sdk/src/main/java/io/customer/sdk/di/CustomerIOComponent.kt b/sdk/src/main/java/io/customer/sdk/di/CustomerIOComponent.kt index fb1bdc284..a2e74cb15 100644 --- a/sdk/src/main/java/io/customer/sdk/di/CustomerIOComponent.kt +++ b/sdk/src/main/java/io/customer/sdk/di/CustomerIOComponent.kt @@ -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 @@ -28,7 +28,7 @@ internal class CustomerIOComponent( private val context: Context ) { - fun buildApi(): CustomerIoApi { + fun buildApi(): CustomerIOApi { return CustomerIOClient( identityRepository = IdentityRepositoryImpl( customerService = buildRetrofitApi(), diff --git a/sdk/src/main/java/io/customer/sdk/repository/TrackingRepository.kt b/sdk/src/main/java/io/customer/sdk/repository/TrackingRepository.kt index 620cf4350..98504a25f 100644 --- a/sdk/src/main/java/io/customer/sdk/repository/TrackingRepository.kt +++ b/sdk/src/main/java/io/customer/sdk/repository/TrackingRepository.kt @@ -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): Action + fun track( + identifier: String?, + type: EventType, + name: String, + attributes: Map + ): Action } internal class TrackingRepositoryImp( @@ -16,6 +22,7 @@ internal class TrackingRepositoryImp( override fun track( identifier: String?, + type: EventType, name: String, attributes: Map ): Action { @@ -23,7 +30,11 @@ internal class TrackingRepositoryImp( 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) + ) ) } } diff --git a/sdk/src/test/java/io/customer/sdk/CustomerIOClientTest.kt b/sdk/src/test/java/io/customer/sdk/CustomerIOClientTest.kt index f01594800..4fc81e2dc 100644 --- a/sdk/src/test/java/io/customer/sdk/CustomerIOClientTest.kt +++ b/sdk/src/test/java/io/customer/sdk/CustomerIOClientTest.kt @@ -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( @@ -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") @@ -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) + } } diff --git a/sdk/src/test/java/io/customer/sdk/CustomerIOTest.kt b/sdk/src/test/java/io/customer/sdk/CustomerIOTest.kt index f68d71938..b8294311b 100644 --- a/sdk/src/test/java/io/customer/sdk/CustomerIOTest.kt +++ b/sdk/src/test/java/io/customer/sdk/CustomerIOTest.kt @@ -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`( @@ -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`( diff --git a/sdk/src/test/java/io/customer/sdk/MockCustomerIOBuilder.kt b/sdk/src/test/java/io/customer/sdk/MockCustomerIOBuilder.kt index 0afb93ce1..bf739c935 100644 --- a/sdk/src/test/java/io/customer/sdk/MockCustomerIOBuilder.kt +++ b/sdk/src/test/java/io/customer/sdk/MockCustomerIOBuilder.kt @@ -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 diff --git a/sdk/src/test/java/io/customer/sdk/api/CustomerIOCallAdapterTest.kt b/sdk/src/test/java/io/customer/sdk/api/CustomerIOCallAdapterTest.kt index 215af8e2e..0b41af4f1 100644 --- a/sdk/src/test/java/io/customer/sdk/api/CustomerIOCallAdapterTest.kt +++ b/sdk/src/test/java/io/customer/sdk/api/CustomerIOCallAdapterTest.kt @@ -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 @@ -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" @@ -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>().javaType + val type: Type = typeOf>().javaType val callAdapter = factory[type, emptyArray(), retrofit] callAdapter.shouldNotBeNull() @@ -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>>().javaType + val type = typeOf>>().javaType val callAdapter = factory[type, emptyArray(), retrofit] callAdapter.shouldNotBeNull() diff --git a/sdk/src/test/java/io/customer/sdk/repositories/TrackingRepositoryTest.kt b/sdk/src/test/java/io/customer/sdk/repositories/TrackingRepositoryTest.kt index 4c113258c..605914d15 100644 --- a/sdk/src/test/java/io/customer/sdk/repositories/TrackingRepositoryTest.kt +++ b/sdk/src/test/java/io/customer/sdk/repositories/TrackingRepositoryTest.kt @@ -2,6 +2,7 @@ package io.customer.sdk.repositories import io.customer.base.error.StatusCode import io.customer.sdk.api.service.CustomerService +import io.customer.sdk.data.model.EventType import io.customer.sdk.data.moshi.CustomerIOParser import io.customer.sdk.data.moshi.CustomerIOParserImpl import io.customer.sdk.repository.AttributesRepository @@ -38,7 +39,11 @@ internal class TrackingRepositoryTest { fun `Unverified user error thrown when identifier is null`() { val result = - trackingRepository.track(identifier = null, name = "event", attributes = emptyMap()) + trackingRepository.track( + identifier = null, + type = EventType.event, + name = "event", attributes = emptyMap() + ) .execute() verifyError(result, StatusCode.UnIdentifiedUser) @@ -52,6 +57,7 @@ internal class TrackingRepositoryTest { val result = trackingRepository.track( identifier = "identifier", + type = EventType.event, name = "event", attributes = emptyMap() ).execute() @@ -67,6 +73,7 @@ internal class TrackingRepositoryTest { val result = trackingRepository.track( identifier = "identifier", + type = EventType.event, name = "event", attributes = emptyMap() ).execute() @@ -82,6 +89,7 @@ internal class TrackingRepositoryTest { val result = trackingRepository.track( identifier = "identifier", + type = EventType.screen, name = "event", attributes = mapOf("key" to Unit) ).execute()