Skip to content

Commit e7491b8

Browse files
authored
Merge pull request #15484 from woocommerce/woomob-2073-woo-posfts-add-analytics-tracking-for-search
[WOOMOB-2073] Add analytics tracking for POS FTS search. Removes FF
2 parents 8b39e4f + 2f3a9ed commit e7491b8

19 files changed

+314
-278
lines changed

RELEASE-NOTES.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
24.4
66
-----
77

8+
- [Internal] Add full-text search for POS product search with analytics tracking [https://github.com/woocommerce/woocommerce-android/pull/15484]
89

910
24.3
1011
-----

WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/featureflags/IsPosProductsFtsEnabled.kt

Lines changed: 0 additions & 8 deletions
This file was deleted.

WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/WooPosItemsSearchHelper.kt

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package com.woocommerce.android.ui.woopos.home.items
22

33
import com.woocommerce.android.R
44
import com.woocommerce.android.ui.woopos.common.composeui.component.WooPosSearchInputState
5-
import com.woocommerce.android.ui.woopos.featureflags.IsPosProductsFtsEnabled
65
import com.woocommerce.android.ui.woopos.home.ChildToParentEvent
76
import com.woocommerce.android.ui.woopos.home.ParentToChildrenEvent
87
import com.woocommerce.android.ui.woopos.home.WooPosChildrenToParentEventSender
@@ -22,7 +21,6 @@ class WooPosItemsSearchHelper @Inject constructor(
2221
private val childToParentEventSender: WooPosChildrenToParentEventSender,
2322
private val parentToChildrenEventReceiver: WooPosParentToChildrenEventReceiver,
2423
private val productsDataSource: WooPosProductsDataSource,
25-
private val isFtsEnabled: IsPosProductsFtsEnabled,
2624
) {
2725
private lateinit var coroutineScope: CoroutineScope
2826
private lateinit var viewStateFlow: MutableStateFlow<WooPosItemsToolbarViewState>
@@ -127,13 +125,7 @@ class WooPosItemsSearchHelper @Inject constructor(
127125
wasLastStateClosed = false
128126

129127
val searchHintStringRes = when (viewStateFlow.value) {
130-
is WooPosItemsToolbarViewState.ProductList -> {
131-
if (isFtsEnabled()) {
132-
R.string.woopos_search_products_and_variations
133-
} else {
134-
R.string.woopos_search_products
135-
}
136-
}
128+
is WooPosItemsToolbarViewState.ProductList -> R.string.woopos_search_products_and_variations
137129
is WooPosItemsToolbarViewState.CouponList -> R.string.woopos_search_coupons
138130
is WooPosItemsToolbarViewState.VariationList -> error("Search is not applicable for variations list")
139131
}

WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/products/WooPosProductsDataSource.kt

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -301,8 +301,14 @@ class WooPosProductsInDbDataSource @Inject constructor(
301301
val result = localCatalogSearchDataSource.searchProducts(query)
302302

303303
result.fold(
304-
onSuccess = { products ->
305-
emit(SearchProductsResult.Local(products))
304+
onSuccess = { dbSearchResult ->
305+
emit(
306+
SearchProductsResult.Local(
307+
products = dbSearchResult.products,
308+
searchTimeMillis = dbSearchResult.searchTimeMillis,
309+
searchMethod = dbSearchResult.searchMethod,
310+
)
311+
)
306312
},
307313
onFailure = { error ->
308314
emit(SearchProductsResult.Remote(Result.failure(error), 0L))
@@ -614,7 +620,13 @@ class WooPosProductsRemoteDataSource @Inject constructor(
614620
override fun searchProducts(query: String): Flow<SearchProductsResult> = flow {
615621
val localProducts = searchProductsDataSource.searchLocalProducts(query)
616622
if (localProducts.isNotEmpty()) {
617-
emit(SearchProductsResult.Local(localProducts))
623+
emit(
624+
SearchProductsResult.Local(
625+
products = localProducts,
626+
searchTimeMillis = 0L,
627+
searchMethod = "in_memory_cache",
628+
)
629+
)
618630
}
619631

620632
var remoteResult: Result<List<WooPosProductModel>>

WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/products/WooPosProductsDataSourceInterface.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,14 @@ import kotlinx.coroutines.flow.Flow
1010
import org.wordpress.android.fluxc.model.LocalOrRemoteId
1111

1212
sealed class SearchProductsResult {
13-
data class Local(val products: List<WooPosProductModel>) : SearchProductsResult()
13+
data class Local(
14+
val products: List<WooPosProductModel>,
15+
val searchTimeMillis: Long,
16+
val searchMethod: String,
17+
) : SearchProductsResult()
1418
data class Remote(
1519
val productsResult: Result<List<WooPosProductModel>>,
16-
val searchTimeMillis: Long = 0L
20+
val searchTimeMillis: Long,
1721
) : SearchProductsResult()
1822
}
1923

WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/search/WooPosItemsSearchAnalyticsTracker.kt

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package com.woocommerce.android.ui.woopos.home.items.search
33
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.ItemsNextPageLoaded
44
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.PreSearchRecentTermTapped
55
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.SearchRemoteResultsFetched
6+
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.SearchResultTapped
7+
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.SearchResultsFetched
68
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEventConstant
79
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsTracker
810
import com.woocommerce.android.ui.woopos.util.analytics.WooPosGetTotalProductCount
@@ -39,6 +41,28 @@ class WooPosItemsSearchAnalyticsTracker @Inject constructor(
3941
analyticsTracker.track(event)
4042
}
4143

44+
suspend fun trackLocalSearchResults(
45+
resultsCount: Int,
46+
searchTimeMillis: Long,
47+
searchMethod: String
48+
) {
49+
val event = SearchResultsFetched(
50+
millisecondsSinceRequestSent = searchTimeMillis,
51+
resultsCount = resultsCount,
52+
source = "purchasable_items",
53+
searchMethod = searchMethod,
54+
)
55+
analyticsTracker.track(event)
56+
}
57+
58+
suspend fun trackSearchResultTapped(resultPosition: Int, resultType: String) {
59+
val event = SearchResultTapped(
60+
resultPosition = resultPosition,
61+
resultType = resultType,
62+
)
63+
analyticsTracker.track(event)
64+
}
65+
4266
fun isProductInTheLocalSearchResult(productId: Long): Boolean = localSearchProductIds.get().contains(productId)
4367

4468
fun storedLocalSearchResultIds(ids: List<Long>) {

WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/search/WooPosItemsSearchViewModel.kt

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,11 @@ class WooPosItemsSearchViewModel @Inject constructor(
180180
showLoadingState: Boolean
181181
) {
182182
analyticsTracker.storedLocalSearchResultIds(searchResult.products.map { it.remoteId })
183+
analyticsTracker.trackLocalSearchResults(
184+
resultsCount = searchResult.products.size,
185+
searchTimeMillis = searchResult.searchTimeMillis,
186+
searchMethod = searchResult.searchMethod,
187+
)
183188

184189
if (searchResult.products.isEmpty()) {
185190
_viewState.value = if (showLoadingState) {
@@ -274,6 +279,8 @@ class WooPosItemsSearchViewModel @Inject constructor(
274279
item: WooPosItemSelectionViewState,
275280
sourceType: WooPosAnalyticsEventConstant.ItemsListSourceType
276281
) {
282+
trackSearchResultTapped(item)
283+
277284
when (item) {
278285
is WooPosItemSelectionViewState.Product.Simple -> {
279286
viewModelScope.launch {
@@ -343,6 +350,24 @@ class WooPosItemsSearchViewModel @Inject constructor(
343350
storeRecentSearch()
344351
}
345352

353+
private fun trackSearchResultTapped(item: WooPosItemSelectionViewState) {
354+
val contentState = _viewState.value as? WooPosItemsSearchViewState.Content ?: return
355+
val position = contentState.items.indexOfFirst { it.id == item.id }
356+
if (position < 0) return
357+
358+
val resultType = when (item) {
359+
is WooPosItemSelectionViewState.Product.VariationSearchResult -> "variation"
360+
else -> "product"
361+
}
362+
363+
viewModelScope.launch {
364+
analyticsTracker.trackSearchResultTapped(
365+
resultPosition = position,
366+
resultType = resultType,
367+
)
368+
}
369+
}
370+
346371
private fun storeRecentSearch() {
347372
(_viewState.value as? WooPosItemsSearchViewState.Content)?.let {
348373
viewModelScope.launch {

WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/search/WooPosProductsSearchInDbDataSource.kt

Lines changed: 16 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import com.woocommerce.android.tools.SelectedSite
44
import com.woocommerce.android.ui.woopos.common.data.models.WooPosProductModel
55
import com.woocommerce.android.ui.woopos.common.data.models.WooPosProductModelMapper
66
import com.woocommerce.android.ui.woopos.common.util.WooPosLogWrapper
7-
import com.woocommerce.android.ui.woopos.featureflags.IsPosProductsFtsEnabled
87
import kotlinx.coroutines.Dispatchers
98
import kotlinx.coroutines.withContext
109
import org.wordpress.android.fluxc.model.LocalOrRemoteId
@@ -20,13 +19,18 @@ class WooPosProductsSearchInDbDataSource @Inject constructor(
2019
private val posLocalCatalogStore: WooPosLocalCatalogStore,
2120
private val selectedSite: SelectedSite,
2221
private val productMapper: WooPosProductModelMapper,
23-
private val isFtsEnabled: IsPosProductsFtsEnabled,
2422
private val logger: WooPosLogWrapper,
2523
) {
2624
companion object {
2725
private const val PAGE_SIZE = 15
2826
}
2927

28+
data class DbSearchResult(
29+
val products: List<WooPosProductModel>,
30+
val searchTimeMillis: Long,
31+
val searchMethod: String,
32+
)
33+
3034
private val searchOffset = AtomicInteger(0)
3135
private val canLoadMoreResults = AtomicBoolean(false)
3236
private var currentQuery: String = ""
@@ -35,7 +39,7 @@ class WooPosProductsSearchInDbDataSource @Inject constructor(
3539
val hasMorePages: Boolean
3640
get() = canLoadMoreResults.get()
3741

38-
suspend fun searchProducts(query: String): Result<List<WooPosProductModel>> = withContext(Dispatchers.IO) {
42+
suspend fun searchProducts(query: String): Result<DbSearchResult> = withContext(Dispatchers.IO) {
3943
searchOffset.set(0)
4044
currentQuery = query
4145
accumulatedResults.clear()
@@ -47,26 +51,15 @@ class WooPosProductsSearchInDbDataSource @Inject constructor(
4751
return@withContext Result.success(accumulatedResults.toList())
4852
}
4953

50-
performSearch(query)
54+
performSearch(query).map { it.products }
5155
}
5256

53-
private suspend fun performSearch(query: String): Result<List<WooPosProductModel>> {
57+
private suspend fun performSearch(query: String): Result<DbSearchResult> {
5458
val siteModel = selectedSite.getOrNull() ?: return Result.failure(
5559
IllegalStateException("No site selected")
5660
)
5761
val siteId = LocalOrRemoteId.LocalId(siteModel.id)
5862

59-
return if (isFtsEnabled()) {
60-
performFtsSearch(siteId, query)
61-
} else {
62-
performLikeSearch(siteId, query)
63-
}
64-
}
65-
66-
private suspend fun performFtsSearch(
67-
siteId: LocalOrRemoteId.LocalId,
68-
query: String
69-
): Result<List<WooPosProductModel>> {
7063
val startTime = System.currentTimeMillis()
7164
val offset = searchOffset.get()
7265
logger.d("performFtsSearch: query=\"$query\", offset=$offset, pageSize=$PAGE_SIZE")
@@ -100,7 +93,13 @@ class WooPosProductsSearchInDbDataSource @Inject constructor(
10093
"(${accumulatedResults.size} total). Duration: ${duration}ms"
10194
)
10295

103-
Result.success(accumulatedResults.toList())
96+
Result.success(
97+
DbSearchResult(
98+
products = accumulatedResults.toList(),
99+
searchTimeMillis = duration,
100+
searchMethod = "fts",
101+
)
102+
)
104103
},
105104
onFailure = { error ->
106105
val duration = System.currentTimeMillis() - startTime
@@ -109,33 +108,4 @@ class WooPosProductsSearchInDbDataSource @Inject constructor(
109108
}
110109
)
111110
}
112-
113-
private suspend fun performLikeSearch(
114-
siteId: LocalOrRemoteId.LocalId,
115-
query: String
116-
): Result<List<WooPosProductModel>> {
117-
val result = posLocalCatalogStore.searchProducts(
118-
siteId = siteId,
119-
searchQuery = query,
120-
pageSize = PAGE_SIZE,
121-
offset = searchOffset.get()
122-
)
123-
124-
return result.fold(
125-
onSuccess = { products ->
126-
val mappedProducts = products.map { entity ->
127-
productMapper.fromEntity(entity)
128-
}
129-
130-
accumulatedResults.addAll(mappedProducts)
131-
canLoadMoreResults.set(products.size == PAGE_SIZE)
132-
searchOffset.addAndGet(PAGE_SIZE)
133-
134-
Result.success(accumulatedResults.toList())
135-
},
136-
onFailure = { error ->
137-
Result.failure(error)
138-
}
139-
)
140-
}
141111
}

WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/localcatalog/WooPosFileBasedSyncAction.kt

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package com.woocommerce.android.ui.woopos.localcatalog
22

33
import com.woocommerce.android.ui.woopos.common.util.WooPosLogWrapper
4+
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent
5+
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsTracker
46
import com.woocommerce.android.ui.woopos.util.datastore.WooPosPreferencesRepository
57
import com.woocommerce.android.ui.woopos.util.datastore.WooPosSyncTimestampManager
68
import kotlinx.coroutines.delay
@@ -20,6 +22,7 @@ class WooPosFileBasedSyncAction @Inject constructor(
2022
private val preferencesRepository: WooPosPreferencesRepository,
2123
private val syncTimestampManager: WooPosSyncTimestampManager,
2224
private val logger: WooPosLogWrapper,
25+
private val analyticsTracker: WooPosAnalyticsTracker,
2326
) {
2427
companion object {
2528
private const val INITIAL_POLL_INTERVAL_MS = 3000L
@@ -212,11 +215,7 @@ class WooPosFileBasedSyncAction @Inject constructor(
212215
)
213216
}
214217

215-
syncWithFts.syncFtsForFullSync(
216-
siteIdString = site.localId().value.toString(),
217-
products = parsedData.products,
218-
variations = parsedData.variations
219-
)
218+
syncFtsAndTrack(site, parsedData)
220219

221220
catalogFileDownloader.cleanupOldCatalogFiles(keepLatest = downloadedFile)
222221

@@ -253,6 +252,23 @@ class WooPosFileBasedSyncAction @Inject constructor(
253252
)
254253
}
255254

255+
private suspend fun syncFtsAndTrack(site: SiteModel, parsedData: WooPosCatalogFileParser.ParsedCatalogData) {
256+
val ftsSyncResult = syncWithFts.syncFtsForFullSync(
257+
siteIdString = site.localId().value.toString(),
258+
products = parsedData.products,
259+
variations = parsedData.variations
260+
)
261+
ftsSyncResult?.let {
262+
analyticsTracker.track(
263+
WooPosAnalyticsEvent.Event.FtsIndexBuilt(
264+
syncType = "full",
265+
indexDurationMs = it.durationMs,
266+
productsIndexed = it.productsIndexed,
267+
)
268+
)
269+
}
270+
}
271+
256272
private fun <T> Result<T>.onFailureLog(context: String): Result<T> {
257273
onFailure { logger.e("WooPosFileBasedSyncAction: $context: ${it.message}") }
258274
return this

0 commit comments

Comments
 (0)