Skip to content

Commit 3c11cd3

Browse files
authored
Merge pull request #14863 from woocommerce/feat/WOOMOB-1155-pos-orders-analytics
[Woo POS][Historical Orders] Order Details: Analytics
2 parents 12c2cec + 70de4e3 commit 3c11cd3

File tree

7 files changed

+283
-5
lines changed

7 files changed

+283
-5
lines changed

WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/toolbar/WooPosHomeFloatingToolbarViewModel.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import com.woocommerce.android.ui.woopos.home.toolbar.WooPosHomeFloatingToolbarU
2020
import com.woocommerce.android.ui.woopos.home.toolbar.WooPosHomeFloatingToolbarUIEvent.OnToolbarMenuClicked
2121
import com.woocommerce.android.ui.woopos.util.WooPosNetworkStatus
2222
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.ExitTapped
23+
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.GoToOrdersTapped
2324
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsTracker
2425
import com.woocommerce.android.viewmodel.ResourceProvider
2526
import dagger.hilt.android.lifecycle.HiltViewModel
@@ -85,6 +86,7 @@ class WooPosHomeFloatingToolbarViewModel @Inject constructor(
8586
R.string.woopos_orders_title -> {
8687
viewModelScope.launch {
8788
childrenToParentEventSender.sendToParent(ChildToParentEvent.NavigationEvent.ToOrders)
89+
analyticsTracker.track(GoToOrdersTapped)
8890
}
8991
}
9092
R.string.woopos_settings_title -> {
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package com.woocommerce.android.ui.woopos.orders
2+
3+
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.OrderDetailsEmailReceiptTapped
4+
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.OrderDetailsLoaded
5+
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.OrdersListFetched
6+
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.OrdersListNextPageLoaded
7+
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.OrdersListPullToRefreshTriggered
8+
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.OrdersListRowTapped
9+
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.OrdersListSearchButtonTapped
10+
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.OrdersListSearchResultsFetched
11+
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsTracker
12+
import org.apache.commons.lang3.time.DateUtils.MILLIS_PER_DAY
13+
import javax.inject.Inject
14+
15+
class WooPosOrdersAnalyticsTracker @Inject constructor(
16+
private val analyticsTracker: WooPosAnalyticsTracker
17+
) {
18+
suspend fun trackOrdersListFetched(elapsedMs: Long) {
19+
analyticsTracker.track(OrdersListFetched(elapsedMs))
20+
}
21+
22+
suspend fun trackOrdersListRowTapped(
23+
orderId: Long,
24+
orderStatus: String,
25+
listPosition: Int,
26+
createdAtMillis: Long
27+
) {
28+
val daysSinceCreated = calculateDaysSinceCreated(createdAtMillis)
29+
analyticsTracker.track(
30+
OrdersListRowTapped(
31+
orderId = orderId,
32+
orderStatus = orderStatus,
33+
listPosition = listPosition,
34+
daysSinceCreated = daysSinceCreated
35+
)
36+
)
37+
}
38+
39+
suspend fun trackOrderDetailsLoaded(
40+
orderId: Long,
41+
orderStatus: String,
42+
createdAtMillis: Long
43+
) {
44+
val daysSinceCreated = calculateDaysSinceCreated(createdAtMillis)
45+
analyticsTracker.track(
46+
OrderDetailsLoaded(
47+
orderId = orderId,
48+
orderStatus = orderStatus,
49+
daysSinceCreated = daysSinceCreated
50+
)
51+
)
52+
}
53+
54+
suspend fun trackOrdersListPullToRefreshTriggered() {
55+
analyticsTracker.track(OrdersListPullToRefreshTriggered)
56+
}
57+
58+
suspend fun trackOrderDetailsEmailReceiptTapped() {
59+
analyticsTracker.track(OrderDetailsEmailReceiptTapped)
60+
}
61+
62+
suspend fun trackOrdersListNextPageLoaded() {
63+
analyticsTracker.track(OrdersListNextPageLoaded)
64+
}
65+
66+
suspend fun trackOrdersListSearchButtonTapped() {
67+
analyticsTracker.track(OrdersListSearchButtonTapped)
68+
}
69+
70+
suspend fun trackOrdersListSearchResultsFetched(elapsedMs: Long) {
71+
analyticsTracker.track(OrdersListSearchResultsFetched(elapsedMs))
72+
}
73+
74+
private fun calculateDaysSinceCreated(createdAtMillis: Long): Int {
75+
return ((System.currentTimeMillis() - createdAtMillis) / MILLIS_PER_DAY)
76+
.toInt()
77+
.coerceAtLeast(0)
78+
}
79+
}

WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosOrdersScreen.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -497,7 +497,9 @@ fun WooPosOrdersScreenPreview() {
497497
status = PosOrderStatus(
498498
text = "Completed",
499499
colorKey = OrderStatusColorKey.COMPLETED
500-
)
500+
),
501+
statusSlug = "Completed",
502+
createdAtMillis = 1
501503
)
502504
val item2 = OrderItemViewState(
503505
id = 2,
@@ -509,7 +511,9 @@ fun WooPosOrdersScreenPreview() {
509511
status = PosOrderStatus(
510512
text = "Processing",
511513
colorKey = OrderStatusColorKey.PROCESSING
512-
)
514+
),
515+
statusSlug = "Completed",
516+
createdAtMillis = 1
513517
)
514518

515519
val details1 = sampleOrderDetails(id = 1L, number = "#014")

WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosOrdersState.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,9 @@ data class OrderItemViewState(
4949
val total: String,
5050
val customerEmail: String?,
5151
val isSelected: Boolean,
52-
val status: PosOrderStatus
52+
val status: PosOrderStatus,
53+
val statusSlug: String,
54+
val createdAtMillis: Long
5355
)
5456

5557
@Immutable

WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosOrdersViewModel.kt

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import kotlinx.coroutines.launch
2929
import java.math.BigDecimal
3030
import java.util.Locale
3131
import javax.inject.Inject
32+
import kotlin.time.TimeSource.Monotonic
3233

3334
@HiltViewModel
3435
class WooPosOrdersViewModel @Inject constructor(
@@ -39,6 +40,7 @@ class WooPosOrdersViewModel @Inject constructor(
3940
private val childrenToParentEventSender: WooPosChildrenToParentEventSender,
4041
private val formatPrice: WooPosFormatPrice,
4142
private val getOrderRefunds: WooPosGetOrderRefundsByOrderId,
43+
private val ordersAnalyticsTracker: WooPosOrdersAnalyticsTracker
4244
) : ViewModel() {
4345

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

77+
val keys = loadedItems.items.keys.toList()
78+
val position = keys.indexOfFirst { it.id == orderId }.coerceAtLeast(0)
79+
val selectedItem = keys.firstOrNull { it.id == orderId }
80+
81+
selectedItem?.let {
82+
viewModelScope.launch {
83+
ordersAnalyticsTracker.trackOrdersListRowTapped(
84+
orderId = it.id,
85+
orderStatus = it.statusSlug,
86+
listPosition = position,
87+
createdAtMillis = it.createdAtMillis
88+
)
89+
ordersAnalyticsTracker.trackOrderDetailsLoaded(
90+
orderId = it.id,
91+
orderStatus = it.statusSlug,
92+
createdAtMillis = it.createdAtMillis
93+
)
94+
}
95+
}
96+
7597
val updatedItems = loadedItems.items.mapKeys { (item, _) ->
7698
item.copy(isSelected = item.id == orderId)
7799
}
@@ -87,6 +109,10 @@ class WooPosOrdersViewModel @Inject constructor(
87109
}
88110

89111
fun onRefresh() {
112+
viewModelScope.launch {
113+
ordersAnalyticsTracker.trackOrdersListPullToRefreshTriggered()
114+
}
115+
90116
val currentState = _state.value
91117
_state.value = when (currentState) {
92118
is WooPosOrdersState.Content -> currentState.copy(
@@ -129,6 +155,7 @@ class WooPosOrdersViewModel @Inject constructor(
129155

130156
fun onEmailReceiptButtonClicked(orderId: Long) {
131157
viewModelScope.launch {
158+
ordersAnalyticsTracker.trackOrderDetailsEmailReceiptTapped()
132159
childrenToParentEventSender.sendToParent(
133160
ToEmailReceipt(orderId)
134161
)
@@ -171,6 +198,7 @@ class WooPosOrdersViewModel @Inject constructor(
171198
val result = ordersDataSource.loadMore(normalizedQuery)
172199

173200
if (result.isSuccess) {
201+
ordersAnalyticsTracker.trackOrdersListNextPageLoaded()
174202
appendOrders(result.getOrThrow())
175203
} else {
176204
_state.value = newState.copy(paginationState = WooPosPaginationState.Error)
@@ -181,6 +209,10 @@ class WooPosOrdersViewModel @Inject constructor(
181209
fun onSearchEvent(event: WooPosSearchUIEvent) {
182210
when (event) {
183211
is WooPosSearchUIEvent.SearchIconClicked -> {
212+
viewModelScope.launch {
213+
ordersAnalyticsTracker.trackOrdersListSearchButtonTapped()
214+
}
215+
184216
updateSearchState(
185217
WooPosSearchInputState.Open(
186218
input = WooPosSearchInputState.Open.Input.Hint(
@@ -286,6 +318,8 @@ class WooPosOrdersViewModel @Inject constructor(
286318
paginationState = WooPosPaginationState.None
287319
)
288320
}
321+
322+
val mark = Monotonic.markNow()
289323
val result = ordersDataSource.searchOrders(query)
290324
when (result) {
291325
is SearchOrdersResult.Error -> {
@@ -302,6 +336,9 @@ class WooPosOrdersViewModel @Inject constructor(
302336
}
303337

304338
is SearchOrdersResult.Success -> {
339+
val elapsedMs = mark.elapsedNow().inWholeMilliseconds
340+
ordersAnalyticsTracker.trackOrdersListSearchResultsFetched(elapsedMs)
341+
305342
if (result.orders.isEmpty()) {
306343
_state.value = WooPosOrdersState.Content(
307344
items = WooPosOrdersState.Content.Items.NothingFound(
@@ -323,6 +360,7 @@ class WooPosOrdersViewModel @Inject constructor(
323360

324361
private fun loadOrders() {
325362
cancelJobs()
363+
val mark = Monotonic.markNow()
326364
loadingJob = viewModelScope.launch {
327365
ordersDataSource.loadOrders().collect { result ->
328366
when (result) {
@@ -344,6 +382,9 @@ class WooPosOrdersViewModel @Inject constructor(
344382
}
345383

346384
is LoadOrdersResult.SuccessRemote -> {
385+
val elapsedMs = mark.elapsedNow().inWholeMilliseconds
386+
ordersAnalyticsTracker.trackOrdersListFetched(elapsedMs)
387+
347388
if (result.orders.isEmpty()) {
348389
_state.value = WooPosOrdersState.Empty(
349390
searchInputState = WooPosSearchInputState.Closed
@@ -429,7 +470,9 @@ class WooPosOrdersViewModel @Inject constructor(
429470
status = PosOrderStatus(
430471
text = statusText,
431472
colorKey = OrderStatusColorKey.fromStatus(order.status)
432-
)
473+
),
474+
statusSlug = order.status.toString(),
475+
createdAtMillis = order.dateCreated.time
433476
)
434477
}
435478

WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/util/analytics/WooPosAnalyticsEvent.kt

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,87 @@ sealed class WooPosAnalyticsEvent : IAnalyticsEvent {
126126
data object InteractionWithCustomerStarted : Event() {
127127
override val name: String = "interaction_with_customer_started"
128128
}
129+
data object GoToOrdersTapped : Event() {
130+
override val name: String = "orders_menu_item_tapped"
131+
}
132+
133+
data object OrdersListPullToRefreshTriggered : Event() {
134+
override val name: String = "orders_list_pull_to_refresh"
135+
}
136+
137+
data object OrdersListNextPageLoaded : Event() {
138+
override val name: String = "orders_list_next_page_loaded"
139+
}
140+
141+
data object OrderDetailsEmailReceiptTapped : Event() {
142+
override val name: String = "order_details_email_receipt_tapped"
143+
}
144+
145+
data class OrdersListRowTapped(
146+
val orderId: Long,
147+
val orderStatus: String,
148+
val listPosition: Int,
149+
val daysSinceCreated: Int
150+
) : Event() {
151+
override val name: String = "orders_list_row_tapped"
152+
153+
init {
154+
addProperties(
155+
mapOf(
156+
"order_id" to orderId.toString(),
157+
"order_status" to orderStatus,
158+
"list_position" to listPosition.toString(),
159+
"days_since_created" to daysSinceCreated.toString()
160+
)
161+
)
162+
}
163+
}
164+
165+
data class OrderDetailsLoaded(
166+
val orderId: Long,
167+
val orderStatus: String,
168+
val daysSinceCreated: Int
169+
) : Event() {
170+
override val name: String = "pos_order_details_loaded"
171+
172+
init {
173+
addProperties(
174+
mapOf(
175+
"order_id" to orderId.toString(),
176+
"order_status" to orderStatus,
177+
"days_since_created" to daysSinceCreated.toString()
178+
)
179+
)
180+
}
181+
}
182+
183+
data class OrdersListFetched(val milimetersSinceRequestSent: Long) : Event() {
184+
override val name: String = "orders_list_fetched"
185+
186+
init {
187+
addProperties(
188+
mapOf(
189+
"milliseconds_since_request_sent" to milimetersSinceRequestSent.toString()
190+
)
191+
)
192+
}
193+
}
194+
195+
data class OrdersListSearchResultsFetched(val milimetersSinceRequestSent: Long) : Event() {
196+
override val name: String = "pos_orders_list_search_results_fetched"
197+
198+
init {
199+
addProperties(
200+
mapOf(
201+
"milliseconds_since_request_sent" to milimetersSinceRequestSent.toString()
202+
)
203+
)
204+
}
205+
}
206+
207+
data object OrdersListSearchButtonTapped : Event() {
208+
override val name: String = "pos_orders_list_search_button_tapped"
209+
}
129210

130211
data class BarcodeScanned(
131212
val scanDurationMs: Long,

0 commit comments

Comments
 (0)