@@ -18,6 +18,12 @@ import kotlinx.coroutines.flow.map
1818import kotlinx.coroutines.flow.merge
1919import kotlinx.coroutines.flow.onEach
2020import org.nitri.opentopo.model.PlaceSearchResult
21+ import org.nitri.opentopo.analytics.AnalyticsTracker
22+ import org.nitri.opentopo.analytics.NoOpAnalyticsTracker
23+ import org.nitri.opentopo.analytics.OrsOutcome
24+ import org.nitri.opentopo.analytics.classifyOrsError
25+ import org.nitri.opentopo.analytics.durationBucket
26+ import org.nitri.opentopo.analytics.queryLengthBucket
2127import org.nitri.opentopo.ors.mapGeocodeFeaturesToPlaceSearchResults
2228import org.nitri.ors.OrsClient
2329import kotlin.coroutines.cancellation.CancellationException
@@ -34,9 +40,13 @@ sealed interface PlaceSearchUiState {
3440class PlaceSearchViewModel (
3541 private val clientProvider : () -> OrsClient ? ,
3642 private val focusLon : Double? ,
37- private val focusLat : Double?
43+ private val focusLat : Double? ,
44+ private val analytics : AnalyticsTracker = NoOpAnalyticsTracker ,
45+ private val clockMillis : () -> Long = { System .nanoTime() / 1_000_000 }
3846) : ViewModel() {
3947
48+ private data class SearchRequest (val query : String , val isRetry : Boolean )
49+
4050 private val _query = MutableStateFlow (" " )
4151 val query: StateFlow <String > = _query .asStateFlow()
4252
@@ -52,12 +62,14 @@ class PlaceSearchViewModel(
5262 .debounce { normalizedQuery ->
5363 if (normalizedQuery.length >= 3 ) 400L else 0L
5464 }
65+ .map { SearchRequest (it, false ) }
5566
56- val retryQuery = _retryTrigger .map { _query .value.trim() }
67+ val retryQuery = _retryTrigger .map { SearchRequest ( _query .value.trim(), true ) }
5768
5869 merge(debouncedQuery, retryQuery)
59- .flatMapLatest { normalizedQuery ->
70+ .flatMapLatest { request ->
6071 flow {
72+ val normalizedQuery = request.query
6173 if (normalizedQuery.length < 3 ) {
6274 emit(PlaceSearchUiState .QueryTooShort )
6375 return @flow
@@ -71,6 +83,7 @@ class PlaceSearchViewModel(
7183
7284 emit(PlaceSearchUiState .Loading )
7385
86+ val startedAt = clockMillis()
7487 try {
7588 val response = client.geocodeAutocomplete(
7689 text = normalizedQuery,
@@ -79,6 +92,10 @@ class PlaceSearchViewModel(
7992 size = 10
8093 )
8194 val results = mapGeocodeFeaturesToPlaceSearchResults(response.features)
95+ val outcome = if (results.isEmpty()) OrsOutcome .EMPTY else OrsOutcome .SUCCESS
96+ analytics.trackOrsSearchResult(outcome, results.size.coerceAtMost(10 ),
97+ durationBucket(clockMillis() - startedAt), queryLengthBucket(normalizedQuery.length),
98+ request.isRetry)
8299 if (results.isEmpty()) {
83100 emit(PlaceSearchUiState .Empty )
84101 } else {
@@ -87,7 +104,10 @@ class PlaceSearchViewModel(
87104 } catch (e: CancellationException ) {
88105 throw e
89106 } catch (e: Throwable ) {
90- emit(PlaceSearchUiState .Error (e.localizedMessage))
107+ analytics.trackOrsSearchResult(OrsOutcome .ERROR , 0 ,
108+ durationBucket(clockMillis() - startedAt), queryLengthBucket(normalizedQuery.length),
109+ request.isRetry, classifyOrsError(e))
110+ emit(PlaceSearchUiState .Error (null ))
91111 }
92112 }
93113 }
@@ -108,11 +128,12 @@ class PlaceSearchViewModel(
108128 class Factory (
109129 private val clientProvider : () -> OrsClient ? ,
110130 private val focusLon : Double? ,
111- private val focusLat : Double?
131+ private val focusLat : Double? ,
132+ private val analytics : AnalyticsTracker = NoOpAnalyticsTracker
112133 ) : ViewModelProvider.Factory {
113134 @Suppress(" UNCHECKED_CAST" )
114135 override fun <T : ViewModel > create (modelClass : Class <T >): T {
115- return PlaceSearchViewModel (clientProvider, focusLon, focusLat) as T
136+ return PlaceSearchViewModel (clientProvider, focusLon, focusLat, analytics ) as T
116137 }
117138 }
118139}
0 commit comments