Skip to content

Commit

Permalink
Merge pull request #12320 from woocommerce/12261-pos-analytics-part-1
Browse files Browse the repository at this point in the history
[Woo POS] Analytics – part 1
  • Loading branch information
AnirudhBhat authored Aug 16, 2024
2 parents a8fa628 + 7cd3561 commit 4bc91d9
Show file tree
Hide file tree
Showing 7 changed files with 139 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import com.woocommerce.android.ui.woopos.home.WooPosParentToChildrenEventReceive
import com.woocommerce.android.ui.woopos.home.cart.WooPosCartStatus.CHECKOUT
import com.woocommerce.android.ui.woopos.home.cart.WooPosCartStatus.EDITABLE
import com.woocommerce.android.ui.woopos.home.cart.WooPosCartStatus.EMPTY
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsTracker
import com.woocommerce.android.ui.woopos.util.format.WooPosFormatPrice
import com.woocommerce.android.viewmodel.ResourceProvider
import com.woocommerce.android.viewmodel.getStateFlow
Expand All @@ -32,6 +34,7 @@ class WooPosCartViewModel @Inject constructor(
private val getProductById: WooPosGetProductById,
private val resourceProvider: ResourceProvider,
private val formatPrice: WooPosFormatPrice,
private val analyticsTracker: WooPosAnalyticsTracker,
savedState: SavedStateHandle,
) : ViewModel() {
private val _state = savedState.getStateFlow(
Expand Down Expand Up @@ -127,6 +130,7 @@ class WooPosCartViewModel @Inject constructor(
)
)
}
analyticsTracker.track(WooPosAnalyticsEvent.Event.ItemAddedToCart)
}

is ParentToChildrenEvent.OrderSuccessfullyPaid -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import com.woocommerce.android.ui.woopos.home.ChildToParentEvent
import com.woocommerce.android.ui.woopos.home.ParentToChildrenEvent
import com.woocommerce.android.ui.woopos.home.WooPosChildrenToParentEventSender
import com.woocommerce.android.ui.woopos.home.WooPosParentToChildrenEventReceiver
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsTracker
import com.woocommerce.android.ui.woopos.util.format.WooPosFormatPrice
import com.woocommerce.android.util.WooLog
import com.woocommerce.android.util.WooLog.T
Expand All @@ -34,6 +36,7 @@ class WooPosTotalsViewModel @Inject constructor(
private val cardReaderFacade: WooPosCardReaderFacade,
private val totalsRepository: WooPosTotalsRepository,
private val priceFormat: WooPosFormatPrice,
private val analyticsTracker: WooPosAnalyticsTracker,
savedState: SavedStateHandle,
) : ViewModel() {

Expand Down Expand Up @@ -129,12 +132,20 @@ class WooPosTotalsViewModel @Inject constructor(
onSuccess = { order ->
dataState.value = dataState.value.copy(orderId = order.id)
uiState.value = buildWooPosTotalsViewState(order)
analyticsTracker.track(WooPosAnalyticsEvent.Event.OrderCreationSuccess)
},
onFailure = { error ->
WooLog.e(T.POS, "Order creation failed - $error")
uiState.value = WooPosTotalsViewState.Error(
resourceProvider.getString(R.string.woopos_totals_order_creation_error)
)
analyticsTracker.track(
WooPosAnalyticsEvent.Error.OrderCreationError(
errorContext = WooPosTotalsViewModel::class,
errorType = error::class.simpleName,
errorDescription = error.message
)
)
}
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package com.woocommerce.android.ui.woopos.util.analytics
import com.woocommerce.android.analytics.IAnalyticsEvent
import kotlin.reflect.KClass

sealed class WooPosAnalytics : IAnalyticsEvent {
sealed class WooPosAnalyticsEvent : IAnalyticsEvent {
override val siteless: Boolean = false
override val isPosEvent: Boolean = true

Expand All @@ -14,30 +14,33 @@ sealed class WooPosAnalytics : IAnalyticsEvent {
_properties.putAll(additionalProperties)
}

sealed class Error : WooPosAnalytics() {
abstract val errorContext: KClass<Any>
sealed class Error : WooPosAnalyticsEvent() {
abstract val errorContext: KClass<out Any>
abstract val errorType: String?
abstract val errorDescription: String?

data class Test(
override val errorContext: KClass<Any>,
data class OrderCreationError(
override val errorContext: KClass<out Any>,
override val errorType: String?,
override val errorDescription: String?,
) : Error() {
override val name: String = "WOO_POS_TEST_ERROR"
override val name: String = "order_creation_failed"
}
}

sealed class Event : WooPosAnalytics() {
data object Test : Event() {
override val name: String = "WOO_POS_TEST_EVENT"
sealed class Event : WooPosAnalyticsEvent() {
data object ItemAddedToCart : Event() {
override val name: String = "item_added_to_cart"
}
data object OrderCreationSuccess : Event() {
override val name: String = "order_creation_success"
}
}
}

internal fun IAnalyticsEvent.addProperties(additionalProperties: Map<String, String>) {
when (this) {
is WooPosAnalytics -> addProperties(additionalProperties)
is WooPosAnalyticsEvent -> addProperties(additionalProperties)
else -> error("Cannot add properties to non-WooPosAnalytics event")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ class WooPosAnalyticsTracker @Inject constructor(
withContext(Dispatchers.IO) {
analytics.addProperties(commonPropertiesProvider.commonProperties)
when (analytics) {
is WooPosAnalytics.Event -> {
is WooPosAnalyticsEvent.Event -> {
analyticsTrackerWrapper.track(
analytics,
analytics.properties
)
}

is WooPosAnalytics.Error -> {
is WooPosAnalyticsEvent.Error -> {
analyticsTrackerWrapper.track(
analytics,
analytics.properties,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import com.woocommerce.android.ui.woopos.home.ParentToChildrenEvent
import com.woocommerce.android.ui.woopos.home.WooPosChildrenToParentEventSender
import com.woocommerce.android.ui.woopos.home.WooPosParentToChildrenEventReceiver
import com.woocommerce.android.ui.woopos.util.WooPosCoroutineTestRule
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsTracker
import com.woocommerce.android.ui.woopos.util.format.WooPosFormatPrice
import com.woocommerce.android.util.captureValues
import com.woocommerce.android.viewmodel.ResourceProvider
Expand All @@ -19,6 +21,7 @@ import org.assertj.core.api.Assertions.assertThat
import org.junit.Rule
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
import java.math.BigDecimal
import kotlin.test.Test
Expand Down Expand Up @@ -52,6 +55,8 @@ class WooPosCartViewModelTest {
onBlocking { invoke(eq(BigDecimal("10.0"))) }.thenReturn("10.0$")
}

private val analyticsTracker: WooPosAnalyticsTracker = mock()

private val savedState: SavedStateHandle = SavedStateHandle()

@Test
Expand Down Expand Up @@ -359,13 +364,38 @@ class WooPosCartViewModelTest {
assertThat(toolbar.isClearAllButtonVisible).isFalse()
}

@Test
fun `when item added to cart, then should track analytics event`() = runTest {
// GIVEN
val product = ProductTestUtils.generateProduct(
productId = 23L,
productName = "title",
amount = "10.0"
).copy(firstImageUrl = "url")

val parentToChildrenEventsMutableFlow = MutableSharedFlow<ParentToChildrenEvent>()
whenever(parentToChildrenEventReceiver.events).thenReturn(parentToChildrenEventsMutableFlow)
whenever(getProductById(eq(product.remoteId))).thenReturn(product)
val sut = createSut()
sut.state.captureValues()

// WHEN
parentToChildrenEventsMutableFlow.emit(
ParentToChildrenEvent.ItemClickedInProductSelector(product.remoteId)
)

// THEN
verify(analyticsTracker).track(WooPosAnalyticsEvent.Event.ItemAddedToCart)
}

private fun createSut(): WooPosCartViewModel {
return WooPosCartViewModel(
childrenToParentEventSender,
parentToChildrenEventReceiver,
getProductById,
resourceProvider,
formatPrice,
analyticsTracker,
savedState
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import com.woocommerce.android.ui.woopos.home.ParentToChildrenEvent
import com.woocommerce.android.ui.woopos.home.WooPosChildrenToParentEventSender
import com.woocommerce.android.ui.woopos.home.WooPosParentToChildrenEventReceiver
import com.woocommerce.android.ui.woopos.util.WooPosCoroutineTestRule
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsTracker
import com.woocommerce.android.ui.woopos.util.format.WooPosFormatPrice
import com.woocommerce.android.viewmodel.ResourceProvider
import kotlinx.coroutines.ExperimentalCoroutinesApi
Expand Down Expand Up @@ -49,6 +51,7 @@ class WooPosTotalsViewModelTest {
}

private val cardReaderFacade: WooPosCardReaderFacade = mock()
private val analyticsTracker: WooPosAnalyticsTracker = mock()

private companion object {
private const val EMPTY_ORDER_ID = -1L
Expand Down Expand Up @@ -424,6 +427,75 @@ class WooPosTotalsViewModelTest {
verify(cardReaderFacade, times(5)).collectPayment(any())
}

@Test
fun `when order is created, then should track order creation success`() {
val productIds = listOf(1L, 2L, 3L)
val parentToChildrenEventFlow = MutableStateFlow(ParentToChildrenEvent.CheckoutClicked(productIds))
val parentToChildrenEventReceiver: WooPosParentToChildrenEventReceiver = mock {
on { events }.thenReturn(parentToChildrenEventFlow)
}
val order = Order.getEmptyOrder(
dateCreated = Date(),
dateModified = Date()
).copy(
totalTax = BigDecimal("2.00"),
items = listOf(
Order.Item.EMPTY.copy(
subtotal = BigDecimal("1.00"),
),
Order.Item.EMPTY.copy(
subtotal = BigDecimal("1.00"),
),
Order.Item.EMPTY.copy(
subtotal = BigDecimal("1.00"),
)
)
)
val totalsRepository: WooPosTotalsRepository = mock {
onBlocking { createOrderWithProducts(productIds = productIds) }.thenReturn(Result.success(order))
}

createViewModel(
parentToChildrenEventReceiver = parentToChildrenEventReceiver,
totalsRepository = totalsRepository,
)
}

@Test
fun `when fails to create order, then should track order creation failure`() = runTest {
val productIds = listOf(1L, 2L, 3L)
val parentToChildrenEventFlow = MutableStateFlow(ParentToChildrenEvent.CheckoutClicked(productIds))
val parentToChildrenEventReceiver: WooPosParentToChildrenEventReceiver = mock {
on { events }.thenReturn(parentToChildrenEventFlow)
}
val errorMessage = "Order creation failed"
val totalsRepository: WooPosTotalsRepository = mock {
onBlocking { createOrderWithProducts(productIds = productIds) }.thenReturn(
Result.failure(Exception(errorMessage))
)
}

val resourceProvider: ResourceProvider = mock {
on { getString(any()) }.thenReturn(errorMessage)
}

createViewModel(
resourceProvider = resourceProvider,
parentToChildrenEventReceiver = parentToChildrenEventReceiver,
totalsRepository = totalsRepository,
)

verify(
analyticsTracker
).track(
WooPosAnalyticsEvent.Error.OrderCreationError(
WooPosTotalsViewModel::class,
Exception::class.java.simpleName,
errorMessage
)
)
}

private fun createViewModel(
resourceProvider: ResourceProvider = mock(),
parentToChildrenEventReceiver: WooPosParentToChildrenEventReceiver = mock(),
Expand All @@ -438,6 +510,7 @@ class WooPosTotalsViewModelTest {
cardReaderFacade,
totalsRepository,
priceFormat,
savedState
analyticsTracker,
savedState,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import org.mockito.kotlin.whenever
import kotlin.test.Test
import kotlin.test.assertFails

class WooPosAnalyticsTrackerTest {
class WooPosAnalyticsEventTrackerTest {
private val analyticsTrackerWrapper: AnalyticsTrackerWrapper = mock()
private val commonPropertiesProvider: WooPosAnalyticsCommonPropertiesProvider = mock()

Expand All @@ -21,7 +21,7 @@ class WooPosAnalyticsTrackerTest {
@Test
fun `given an event, when track is called, then it should track the event via wrapper`() = runTest {
// GIVEN
val event = WooPosAnalytics.Event.Test
val event = WooPosAnalyticsEvent.Event.ItemAddedToCart

// WHEN
tracker.track(event)
Expand All @@ -36,7 +36,7 @@ class WooPosAnalyticsTrackerTest {
@Test
fun `given an err, when track is called, then it should track the error via wrapper`() = runTest {
// GIVEN
val error = WooPosAnalytics.Error.Test(
val error = WooPosAnalyticsEvent.Error.OrderCreationError(
errorContext = Any::class,
errorType = "test",
errorDescription = "test",
Expand All @@ -58,7 +58,7 @@ class WooPosAnalyticsTrackerTest {
@Test
fun `given an event and common properties, when track is called, then it should track the event with common properties`() = runTest {
// GIVEN
val event = WooPosAnalytics.Event.Test
val event = WooPosAnalyticsEvent.Event.ItemAddedToCart
val commonProperties = mapOf("test" to "test")
whenever(commonPropertiesProvider.commonProperties).thenReturn(commonProperties)

Expand All @@ -75,7 +75,7 @@ class WooPosAnalyticsTrackerTest {
@Test
fun `given an error and common properties, when track is called, then it should track the event with common properties`() = runTest {
// GIVEN
val error = WooPosAnalytics.Error.Test(
val error = WooPosAnalyticsEvent.Error.OrderCreationError(
errorContext = Any::class,
errorType = "test",
errorDescription = "test",
Expand Down

0 comments on commit 4bc91d9

Please sign in to comment.