Skip to content

[Woo POS][Product Search] Analytics #13992

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

Draft
wants to merge 14 commits into
base: trunk
Choose a base branch
from
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.woocommerce.android.ui.woopos.home

import com.woocommerce.android.ui.woopos.home.items.WooPosItemsViewModel
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.ItemAddedToCart.WooPosItemSource
import dagger.hilt.android.scopes.ActivityRetainedScoped
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
Expand All @@ -25,7 +26,10 @@ sealed class ChildToParentEvent {
) : ChildToParentEvent()

data object BackFromCheckoutToCartClicked : ChildToParentEvent()
data class ItemClickedInProductSelector(val itemData: WooPosItemsViewModel.ItemClickedData) : ChildToParentEvent()
data class ItemClickedInProductSelector(
val itemData: WooPosItemsViewModel.ItemClickedData,
val source: WooPosItemSource = WooPosItemSource.PRODUCT_LIST
) : ChildToParentEvent()
data object NewTransactionClicked : ChildToParentEvent()
data object PaymentCollecting : ChildToParentEvent()
data object PaymentInProgress : ChildToParentEvent()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.woocommerce.android.ui.woopos.home

import com.woocommerce.android.ui.woopos.home.items.WooPosItemsViewModel
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.ItemAddedToCart.WooPosItemSource
import dagger.hilt.android.scopes.ActivityRetainedScoped
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
Expand All @@ -22,7 +23,8 @@ class WooPosParentToChildrenCommunication @Inject constructor() :
sealed class ParentToChildrenEvent {
data object BackFromCheckoutToCartClicked : ParentToChildrenEvent()
data class ItemClickedInProductSelector(
val itemData: WooPosItemsViewModel.ItemClickedData
val itemData: WooPosItemsViewModel.ItemClickedData,
val source: WooPosItemSource
) : ParentToChildrenEvent()

data class CheckoutClicked(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ class WooPosHomeViewModel @Inject constructor(

is ChildToParentEvent.ItemClickedInProductSelector -> {
sendEventToChildren(
ItemClickedInProductSelector(event.itemData)
ItemClickedInProductSelector(itemData = event.itemData, source = event.source)
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Eve
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.CheckoutTapped
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.ClearCartTapped
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.InteractionWithCustomerStarted
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.ItemAddedToCart.WooPosItemSource
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.ItemRemovedFromCart
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEventConstant
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsTracker
Expand Down Expand Up @@ -211,12 +212,14 @@ class WooPosCartViewModel @Inject constructor(
analyticsTracker.track(InteractionWithCustomerStarted)
}
_state.value = updateStateWithNewItem(itemClicked.await())
WooPosAnalyticsEvent.Event.ItemAddedToCart.addProperties(
mapOf(
WooPosAnalyticsEventConstant.PRODUCT_TYPE to event.itemData.posItemNameForAnalytics()

val source = WooPosItemSource.toAnalyticsString(event.source)
val itemAddedEvent = WooPosAnalyticsEvent.Event.ItemAddedToCart(source).apply {
addProperties(
mapOf(WooPosAnalyticsEventConstant.PRODUCT_TYPE to event.itemData.posItemNameForAnalytics())
)
)
analyticsTracker.track(WooPosAnalyticsEvent.Event.ItemAddedToCart)
}
analyticsTracker.track(itemAddedEvent)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package com.woocommerce.android.ui.woopos.home.items

import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.ItemAddedToCart.WooPosItemSource

sealed class WooPosItemNavigationData(open val id: Long) {
data class VariableProductData(
override val id: Long,
val name: String,
val numOfVariations: Int,
val source: WooPosItemSource,
) : WooPosItemNavigationData(id)
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,26 @@ 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.home.items.WooPosItemsViewState.SearchState
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.SearchButtonTapped
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsTracker
import com.woocommerce.android.viewmodel.ResourceProvider
import dagger.hilt.android.scopes.ActivityRetainedScoped
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import javax.inject.Inject

@ActivityRetainedScoped
class WooPosItemsSearchHelper @Inject constructor(
private val resourceProvider: ResourceProvider,
private val childToParentEventSender: WooPosChildrenToParentEventSender,
private val parentToChildrenEventReceiver: WooPosParentToChildrenEventReceiver
private val parentToChildrenEventReceiver: WooPosParentToChildrenEventReceiver,
private val analyticsTracker: WooPosAnalyticsTracker,
) {
private lateinit var coroutineScope: CoroutineScope
private lateinit var viewStateFlow: MutableStateFlow<WooPosItemsViewState>
Expand All @@ -30,6 +38,7 @@ class WooPosItemsSearchHelper @Inject constructor(
this.coroutineScope = coroutineScope
this.viewStateFlow = viewStateFlow
listenEventsFromParent()
observeAndTrackSearchInputStateOpen(viewStateFlow, coroutineScope)
}

private fun listenEventsFromParent() {
Expand Down Expand Up @@ -165,6 +174,25 @@ class WooPosItemsSearchHelper @Inject constructor(
)
}

private fun observeAndTrackSearchInputStateOpen(
viewStateFlow: MutableStateFlow<WooPosItemsViewState>,
coroutineScope: CoroutineScope
) {
viewStateFlow
.map { it.search }
.distinctUntilChanged()
.map { it is SearchState.Visible && it.state is WooPosSearchInputState.Open }
.distinctUntilChanged()
.filter { it }
.onEach {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

onEach is invoked only when WooPosSearchInputState changes from Closed to Open.

val event = SearchButtonTapped.apply {
addProperties(mapOf("item_list_type" to "products"))
}
analyticsTracker.track(event)
}
.launchIn(coroutineScope)
}

private fun getCurrentContentState(): WooPosItemsViewState {
return viewStateFlow.value
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import com.woocommerce.android.ui.woopos.home.items.coupons.WooPosCouponsUIEvent
import com.woocommerce.android.ui.woopos.home.items.coupons.WooPosCouponsUIEvent.RetryTriggered
import com.woocommerce.android.ui.woopos.home.items.navigation.WooPosItemsNavigator
import com.woocommerce.android.ui.woopos.home.items.navigation.WooPosItemsNavigator.WooPosItemsScreenNavigationEvent
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.ItemAddedToCart.WooPosItemSource
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
Expand Down Expand Up @@ -96,7 +97,10 @@ class WooPosCouponsViewModel @Inject constructor(
viewModelScope.launch {
fromChildToParentEventSender.sendToParent(
// CouponsProject: rename ItemClickedInProductSelector to ItemClicked
ChildToParentEvent.ItemClickedInProductSelector(ItemClickedData.Coupon(event.couponId))
ChildToParentEvent.ItemClickedInProductSelector(
itemData = ItemClickedData.Coupon(event.couponId),
source = WooPosItemSource.COUPON_LIST
)
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import com.woocommerce.android.ui.woopos.home.items.WooPosProductsViewState
import com.woocommerce.android.ui.woopos.home.items.WooPosPullToRefreshState
import com.woocommerce.android.ui.woopos.home.items.navigation.WooPosItemsNavigator
import com.woocommerce.android.ui.woopos.home.items.navigation.WooPosItemsNavigator.WooPosItemsScreenNavigationEvent.NavigateToVariationsScreen
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.ItemAddedToCart.WooPosItemSource
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.ProductsPullToRefreshTriggered
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsTracker
import com.woocommerce.android.ui.woopos.util.format.WooPosFormatPrice
Expand Down Expand Up @@ -101,6 +102,7 @@ class WooPosProductsViewModel @Inject constructor(
id = event.item.id,
name = event.item.name,
numOfVariations = event.item.numOfVariations,
source = WooPosItemSource.PRODUCT_LIST
)
)
)
Expand Down Expand Up @@ -252,7 +254,12 @@ class WooPosProductsViewModel @Inject constructor(
}

private fun onItemClicked(itemData: ItemClickedData) {
sendEventToParent(ChildToParentEvent.ItemClickedInProductSelector(itemData))
sendEventToParent(
ChildToParentEvent.ItemClickedInProductSelector(
itemData = itemData,
source = WooPosItemSource.PRODUCT_LIST
)
)
}

private fun sendEventToParent(event: ChildToParentEvent) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ import com.woocommerce.android.ui.woopos.home.items.WooPosItemsViewModel.ItemCli
import com.woocommerce.android.ui.woopos.home.items.WooPosPaginationState
import com.woocommerce.android.ui.woopos.home.items.navigation.WooPosItemsNavigator
import com.woocommerce.android.ui.woopos.home.items.navigation.WooPosItemsNavigator.WooPosItemsScreenNavigationEvent.NavigateToVariationsScreen
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.ItemAddedToCart.WooPosItemSource
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.ItemsNextPageLoaded
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.PreSearchRecentTermTapped
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsTracker
import com.woocommerce.android.ui.woopos.util.format.WooPosFormatPrice
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
Expand All @@ -38,6 +42,7 @@ class WooPosItemsSearchViewModel @Inject constructor(
private val parentToChildrenEventReceiver: WooPosParentToChildrenEventReceiver,
private val navigator: WooPosItemsNavigator,
private val searchHelper: WooPosItemsSearchHelper,
private val analyticsTracker: WooPosAnalyticsTracker,
) : ViewModel() {
private val _viewState =
MutableStateFlow<WooPosItemsSearchViewState>(WooPosItemsSearchViewState.Empty)
Expand All @@ -62,7 +67,7 @@ class WooPosItemsSearchViewModel @Inject constructor(
fun onUIEvent(event: WooPosItemsSearchUiEvent) {
when (event) {
WooPosItemsSearchUiEvent.OnNextPageRequested -> onEndOfListReached()
is WooPosItemsSearchUiEvent.OnItemClicked -> handleItemClicked(event.item)
is WooPosItemsSearchUiEvent.OnItemClicked -> handleItemClicked(event.item, WooPosItemSource.SEARCH_RESULT)
WooPosItemsSearchUiEvent.LoadingErrorRetryButtonClicked -> {
val currentState = _viewState.value as? WooPosItemsSearchViewState.Error ?: return
performSearch(currentState.searchQuery)
Expand All @@ -81,7 +86,8 @@ class WooPosItemsSearchViewModel @Inject constructor(
is WooPosItemsSearchUiEvent.OnPopularItemClicked -> {
viewModelScope.launch {
emptyStateRepository.addPopularItemsToCache()
handleItemClicked(event.item)
handleItemClicked(event.item, WooPosItemSource.POPULAR_PRODUCTS)
trackPopularItemClicked()
}
}
}
Expand Down Expand Up @@ -189,6 +195,7 @@ class WooPosItemsSearchViewModel @Inject constructor(
loadMoreJob = viewModelScope.launch {
val result = dataSource.loadMore(query = currentState.searchQuery)
_viewState.value = if (result.isSuccess) {
trackItemsNextPageLoaded()
result.getOrThrow().toContentState(
searchQuery = currentState.searchQuery,
)
Expand All @@ -198,13 +205,37 @@ class WooPosItemsSearchViewModel @Inject constructor(
}
}

private fun handleItemClicked(item: WooPosItemSelectionViewState) {
private suspend fun trackItemsNextPageLoaded() {
val event = ItemsNextPageLoaded.apply {
addProperties(
mapOf(
"item_list_type" to "products",
"search" to "true"
)
)
}
analyticsTracker.track(event)
}

private suspend fun trackPopularItemClicked() {
val event = PreSearchRecentTermTapped.apply {
addProperties(
mapOf(
"item_list_type" to "products",
)
)
}
analyticsTracker.track(event)
}

private fun handleItemClicked(item: WooPosItemSelectionViewState, source: WooPosItemSource) {
when (item) {
is WooPosItemSelectionViewState.Product.Simple -> {
viewModelScope.launch {
childToParentEventSender.sendToParent(
ChildToParentEvent.ItemClickedInProductSelector(
ItemClickedData.Product.Simple(id = item.id)
itemData = ItemClickedData.Product.Simple(id = item.id),
source = source,
)
)
}
Expand All @@ -220,6 +251,7 @@ class WooPosItemsSearchViewModel @Inject constructor(
id = item.id,
name = item.name,
numOfVariations = item.numOfVariations,
source = WooPosItemSource.SEARCH_RESULT
)
)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import com.woocommerce.android.ui.woopos.home.items.WooPosItemsEmptyList
import com.woocommerce.android.ui.woopos.home.items.WooPosItemsLoadingIndicator
import com.woocommerce.android.ui.woopos.home.items.WooPosPullToRefreshState
import com.woocommerce.android.ui.woopos.home.items.WooPosVariationsViewState
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.ItemAddedToCart.WooPosItemSource
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow

Expand All @@ -61,7 +62,7 @@ fun WooPosVariationsScreen(
key = variableProductData.id.toString()
)
LaunchedEffect(variableProductData.id) {
viewModel.init(variableProductData.id)
viewModel.init(variableProductData.id, variableProductData.source)
}
val state = viewModel.viewState
WooPosVariationsScreens(
Expand Down Expand Up @@ -298,6 +299,7 @@ fun WooPosVariationsScreenPreview() {
id = 0,
name = "Variable Product",
numOfVariations = 20,
source = WooPosItemSource.PRODUCT_LIST,
),
state = productState,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import com.woocommerce.android.ui.woopos.home.items.WooPosItemsViewModel
import com.woocommerce.android.ui.woopos.home.items.WooPosPaginationState
import com.woocommerce.android.ui.woopos.home.items.WooPosPullToRefreshState
import com.woocommerce.android.ui.woopos.home.items.WooPosVariationsViewState
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.ItemAddedToCart.WooPosItemSource
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.ItemsNextPageLoaded
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.VariationsPullToRefreshTriggered
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsTracker
import com.woocommerce.android.ui.woopos.util.format.WooPosFormatPrice
Expand Down Expand Up @@ -47,11 +49,13 @@ class WooPosVariationsViewModel @Inject constructor(
)

private var fetchJob: Job? = null
private var variationsSource: WooPosItemSource = WooPosItemSource.PRODUCT_LIST

@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal var loadMoreJob: Job? = null

fun init(productId: Long) {
fun init(productId: Long, source: WooPosItemSource) {
this.variationsSource = source
viewModelScope.launch {
variationsDataSource.resetState()
}
Expand Down Expand Up @@ -167,6 +171,7 @@ class WooPosVariationsViewModel @Inject constructor(
loadMoreJob = viewModelScope.launch {
val result = variationsDataSource.loadMore(productId)
_viewState.value = if (result.isSuccess) {
trackItemsNextPageLoaded()
WooPosVariationsViewState.Content(
items = result.getOrThrow().map {
WooPosItemSelectionViewState.Product.Variation(
Expand All @@ -184,6 +189,19 @@ class WooPosVariationsViewModel @Inject constructor(
}
}

private suspend fun trackItemsNextPageLoaded() {
val event = ItemsNextPageLoaded.apply {
val isSearch: Boolean = variationsSource == WooPosItemSource.SEARCH_RESULT
addProperties(
mapOf(
"item_list_type" to "variations",
"search" to "$isSearch"
)
)
}
analyticsTracker.track(event)
}

fun onUIEvent(event: WooPosVariationsUIEvents) {
when (event) {
is WooPosVariationsUIEvents.EndOfItemsListReached -> {
Expand All @@ -208,7 +226,8 @@ class WooPosVariationsViewModel @Inject constructor(
private fun onVariationClicked(productId: Long, variationId: Long) {
sendEventToParent(
ChildToParentEvent.ItemClickedInProductSelector(
WooPosItemsViewModel.ItemClickedData.Product.Variation(productId, variationId)
itemData = WooPosItemsViewModel.ItemClickedData.Product.Variation(productId, variationId),
source = variationsSource
)
)
}
Expand Down
Loading