Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a95805a
WIP: Adding first batch of analytics
toupper Oct 29, 2025
8d5fb97
add milliseconds parameter
toupper Oct 29, 2025
383f133
Merge branch 'trunk' into feat/WOOMOB-1155-pos-orders-analytics
toupper Oct 30, 2025
293db1b
Analytics event for POS Orders pull to refresh
toupper Oct 30, 2025
0b49a94
Analytics Event tracking for the loading the next page in orders
toupper Oct 30, 2025
464a70d
Track tap on the orders list row
toupper Oct 30, 2025
5fffc0e
Track search button tapped
toupper Oct 30, 2025
e6a0a96
Track order list search results fetched
toupper Oct 30, 2025
4406f38
Track order details loaded
toupper Oct 30, 2025
9c28366
Track email receipt button tap event
toupper Oct 30, 2025
92a5ea3
Fix and add new tests
toupper Oct 30, 2025
b482de6
Move all events to view model
toupper Oct 30, 2025
0e993ca
Remove analytics tracker
toupper Oct 30, 2025
5324ffe
Add tests for new events in view model
toupper Oct 30, 2025
5eddcae
Use kotlin time instead of android function
toupper Oct 30, 2025
ca2e917
Fix detekt issues
toupper Oct 30, 2025
389fe12
Bring accidentally removed event tracking back
toupper Oct 30, 2025
a2fefa9
Bring accidentally removed code back
toupper Oct 30, 2025
9a0cf09
Revert previous changes
toupper Oct 30, 2025
a8dec03
Simplify and clean code
toupper Oct 31, 2025
5e1dd87
Merge branch 'trunk' into feat/WOOMOB-1155-pos-orders-analytics
toupper Oct 31, 2025
d3abf2e
Further changes related to the patch
toupper Oct 31, 2025
71554f1
Merge branch 'feat/WOOMOB-1155-pos-orders-analytics' of https://githu…
toupper Oct 31, 2025
70de4e3
Fix detekt issues
toupper Oct 31, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import com.woocommerce.android.ui.woopos.home.toolbar.WooPosHomeFloatingToolbarU
import com.woocommerce.android.ui.woopos.home.toolbar.WooPosHomeFloatingToolbarUIEvent.OnToolbarMenuClicked
import com.woocommerce.android.ui.woopos.util.WooPosNetworkStatus
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.ExitTapped
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.GoToOrdersTapped
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsTracker
import com.woocommerce.android.viewmodel.ResourceProvider
import dagger.hilt.android.lifecycle.HiltViewModel
Expand Down Expand Up @@ -87,6 +88,7 @@ class WooPosHomeFloatingToolbarViewModel @Inject constructor(
R.string.woopos_orders_title -> {
viewModelScope.launch {
childrenToParentEventSender.sendToParent(ChildToParentEvent.NavigationEvent.ToOrders)
analyticsTracker.track(GoToOrdersTapped)
}
}
R.string.woopos_settings_title -> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package com.woocommerce.android.ui.woopos.orders

import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.OrderDetailsEmailReceiptTapped
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.OrderDetailsLoaded
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.OrdersListFetched
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.OrdersListNextPageLoaded
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.OrdersListPullToRefreshTriggered
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.OrdersListRowTapped
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.OrdersListSearchButtonTapped
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.OrdersListSearchResultsFetched
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsTracker
import org.apache.commons.lang3.time.DateUtils.MILLIS_PER_DAY
import javax.inject.Inject

class WooPosOrdersAnalyticsTracker @Inject constructor(
private val analyticsTracker: WooPosAnalyticsTracker
) {
suspend fun trackOrdersListFetched(elapsedMs: Long) {
analyticsTracker.track(OrdersListFetched(elapsedMs))
}

suspend fun trackOrdersListRowTapped(
orderId: Long,
orderStatus: String,
listPosition: Int,
createdAtMillis: Long
) {
val daysSinceCreated = calculateDaysSinceCreated(createdAtMillis)
analyticsTracker.track(
OrdersListRowTapped(
orderId = orderId,
orderStatus = orderStatus,
listPosition = listPosition,
daysSinceCreated = daysSinceCreated
)
)
}

suspend fun trackOrderDetailsLoaded(
orderId: Long,
orderStatus: String,
createdAtMillis: Long
) {
val daysSinceCreated = calculateDaysSinceCreated(createdAtMillis)
analyticsTracker.track(
OrderDetailsLoaded(
orderId = orderId,
orderStatus = orderStatus,
daysSinceCreated = daysSinceCreated
)
)
}

suspend fun trackOrdersListPullToRefreshTriggered() {
analyticsTracker.track(OrdersListPullToRefreshTriggered)
}

suspend fun trackOrderDetailsEmailReceiptTapped() {
analyticsTracker.track(OrderDetailsEmailReceiptTapped)
}

suspend fun trackOrdersListNextPageLoaded() {
analyticsTracker.track(OrdersListNextPageLoaded)
}

suspend fun trackOrdersListSearchButtonTapped() {
analyticsTracker.track(OrdersListSearchButtonTapped)
}

suspend fun trackOrdersListSearchResultsFetched(elapsedMs: Long) {
analyticsTracker.track(OrdersListSearchResultsFetched(elapsedMs))
}

private fun calculateDaysSinceCreated(createdAtMillis: Long): Int {
return ((System.currentTimeMillis() - createdAtMillis) / MILLIS_PER_DAY)
.toInt()
.coerceAtLeast(0)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -497,7 +497,9 @@ fun WooPosOrdersScreenPreview() {
status = PosOrderStatus(
text = "Completed",
colorKey = OrderStatusColorKey.COMPLETED
)
),
statusSlug = "Completed",
createdAtMillis = 1
)
val item2 = OrderItemViewState(
id = 2,
Expand All @@ -509,7 +511,9 @@ fun WooPosOrdersScreenPreview() {
status = PosOrderStatus(
text = "Processing",
colorKey = OrderStatusColorKey.PROCESSING
)
),
statusSlug = "Completed",
createdAtMillis = 1
)

val details1 = sampleOrderDetails(id = 1L, number = "#014")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@ data class OrderItemViewState(
val total: String,
val customerEmail: String?,
val isSelected: Boolean,
val status: PosOrderStatus
val status: PosOrderStatus,
val statusSlug: String,
val createdAtMillis: Long
)

@Immutable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import kotlinx.coroutines.launch
import java.math.BigDecimal
import java.util.Locale
import javax.inject.Inject
import kotlin.time.TimeSource.Monotonic

@HiltViewModel
class WooPosOrdersViewModel @Inject constructor(
Expand All @@ -39,6 +40,7 @@ class WooPosOrdersViewModel @Inject constructor(
private val childrenToParentEventSender: WooPosChildrenToParentEventSender,
private val formatPrice: WooPosFormatPrice,
private val getOrderRefunds: WooPosGetOrderRefundsByOrderId,
private val ordersAnalyticsTracker: WooPosOrdersAnalyticsTracker
) : ViewModel() {

private val _state = MutableStateFlow<WooPosOrdersState>(
Expand Down Expand Up @@ -72,6 +74,26 @@ class WooPosOrdersViewModel @Inject constructor(
val current = _state.value as? WooPosOrdersState.Content ?: return
val loadedItems = current.items as? WooPosOrdersState.Content.Items.Loaded ?: return

val keys = loadedItems.items.keys.toList()
val position = keys.indexOfFirst { it.id == orderId }.coerceAtLeast(0)
val selectedItem = keys.firstOrNull { it.id == orderId }

selectedItem?.let {
viewModelScope.launch {
ordersAnalyticsTracker.trackOrdersListRowTapped(
orderId = it.id,
orderStatus = it.statusSlug,
listPosition = position,
createdAtMillis = it.createdAtMillis
)
ordersAnalyticsTracker.trackOrderDetailsLoaded(
orderId = it.id,
orderStatus = it.statusSlug,
createdAtMillis = it.createdAtMillis
)
}
}

val updatedItems = loadedItems.items.mapKeys { (item, _) ->
item.copy(isSelected = item.id == orderId)
}
Expand All @@ -87,6 +109,10 @@ class WooPosOrdersViewModel @Inject constructor(
}

fun onRefresh() {
viewModelScope.launch {
ordersAnalyticsTracker.trackOrdersListPullToRefreshTriggered()
}

val currentState = _state.value
_state.value = when (currentState) {
is WooPosOrdersState.Content -> currentState.copy(
Expand Down Expand Up @@ -129,6 +155,7 @@ class WooPosOrdersViewModel @Inject constructor(

fun onEmailReceiptButtonClicked(orderId: Long) {
viewModelScope.launch {
ordersAnalyticsTracker.trackOrderDetailsEmailReceiptTapped()
childrenToParentEventSender.sendToParent(
ToEmailReceipt(orderId)
)
Expand Down Expand Up @@ -171,6 +198,7 @@ class WooPosOrdersViewModel @Inject constructor(
val result = ordersDataSource.loadMore(normalizedQuery)

if (result.isSuccess) {
ordersAnalyticsTracker.trackOrdersListNextPageLoaded()
appendOrders(result.getOrThrow())
} else {
_state.value = newState.copy(paginationState = WooPosPaginationState.Error)
Expand All @@ -181,6 +209,10 @@ class WooPosOrdersViewModel @Inject constructor(
fun onSearchEvent(event: WooPosSearchUIEvent) {
when (event) {
is WooPosSearchUIEvent.SearchIconClicked -> {
viewModelScope.launch {
ordersAnalyticsTracker.trackOrdersListSearchButtonTapped()
}

updateSearchState(
WooPosSearchInputState.Open(
input = WooPosSearchInputState.Open.Input.Hint(
Expand Down Expand Up @@ -286,6 +318,8 @@ class WooPosOrdersViewModel @Inject constructor(
paginationState = WooPosPaginationState.None
)
}

val mark = Monotonic.markNow()
val result = ordersDataSource.searchOrders(query)
when (result) {
is SearchOrdersResult.Error -> {
Expand All @@ -302,6 +336,9 @@ class WooPosOrdersViewModel @Inject constructor(
}

is SearchOrdersResult.Success -> {
val elapsedMs = mark.elapsedNow().inWholeMilliseconds
ordersAnalyticsTracker.trackOrdersListSearchResultsFetched(elapsedMs)

if (result.orders.isEmpty()) {
_state.value = WooPosOrdersState.Content(
items = WooPosOrdersState.Content.Items.NothingFound(
Expand All @@ -323,6 +360,7 @@ class WooPosOrdersViewModel @Inject constructor(

private fun loadOrders() {
cancelJobs()
val mark = Monotonic.markNow()
loadingJob = viewModelScope.launch {
ordersDataSource.loadOrders().collect { result ->
when (result) {
Expand All @@ -344,6 +382,9 @@ class WooPosOrdersViewModel @Inject constructor(
}

is LoadOrdersResult.SuccessRemote -> {
val elapsedMs = mark.elapsedNow().inWholeMilliseconds
ordersAnalyticsTracker.trackOrdersListFetched(elapsedMs)

if (result.orders.isEmpty()) {
_state.value = WooPosOrdersState.Empty(
searchInputState = WooPosSearchInputState.Closed
Expand Down Expand Up @@ -429,7 +470,9 @@ class WooPosOrdersViewModel @Inject constructor(
status = PosOrderStatus(
text = statusText,
colorKey = OrderStatusColorKey.fromStatus(order.status)
)
),
statusSlug = order.status.toString(),
createdAtMillis = order.dateCreated.time
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,87 @@ sealed class WooPosAnalyticsEvent : IAnalyticsEvent {
data object InteractionWithCustomerStarted : Event() {
override val name: String = "interaction_with_customer_started"
}
data object GoToOrdersTapped : Event() {
override val name: String = "orders_menu_item_tapped"
}

data object OrdersListPullToRefreshTriggered : Event() {
override val name: String = "orders_list_pull_to_refresh"
}

data object OrdersListNextPageLoaded : Event() {
override val name: String = "orders_list_next_page_loaded"
}

data object OrderDetailsEmailReceiptTapped : Event() {
override val name: String = "order_details_email_receipt_tapped"
}

data class OrdersListRowTapped(
val orderId: Long,
val orderStatus: String,
val listPosition: Int,
val daysSinceCreated: Int
) : Event() {
override val name: String = "orders_list_row_tapped"

init {
addProperties(
mapOf(
"order_id" to orderId.toString(),
"order_status" to orderStatus,
"list_position" to listPosition.toString(),
"days_since_created" to daysSinceCreated.toString()
)
)
}
}

data class OrderDetailsLoaded(
val orderId: Long,
val orderStatus: String,
val daysSinceCreated: Int
) : Event() {
override val name: String = "pos_order_details_loaded"

init {
addProperties(
mapOf(
"order_id" to orderId.toString(),
"order_status" to orderStatus,
"days_since_created" to daysSinceCreated.toString()
)
)
}
}

data class OrdersListFetched(val milimetersSinceRequestSent: Long) : Event() {
override val name: String = "orders_list_fetched"

init {
addProperties(
mapOf(
"milliseconds_since_request_sent" to milimetersSinceRequestSent.toString()
)
)
}
}

data class OrdersListSearchResultsFetched(val milimetersSinceRequestSent: Long) : Event() {
override val name: String = "pos_orders_list_search_results_fetched"

init {
addProperties(
mapOf(
"milliseconds_since_request_sent" to milimetersSinceRequestSent.toString()
)
)
}
}

data object OrdersListSearchButtonTapped : Event() {
override val name: String = "pos_orders_list_search_button_tapped"
}

data class BarcodeScanned(
val scanDurationMs: Long,
Expand Down
Loading