Skip to content

Commit 85cf4c3

Browse files
authored
Autocomplete: Fix pixels un Duck.ai (#6750)
Task/Issue URL: https://app.asana.com/1/137249556945/project/1174433894299346/task/1211199636994638?focus=true ### Description This PR unifies to pixel logic so it’s used in Autocomplete and DuckChat
1 parent 4daa734 commit 85cf4c3

File tree

10 files changed

+272
-182
lines changed

10 files changed

+272
-182
lines changed

app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt

Lines changed: 8 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -209,19 +209,17 @@ import com.duckduckgo.brokensite.api.BrokenSitePrompt
209209
import com.duckduckgo.brokensite.api.RefreshPattern
210210
import com.duckduckgo.browser.api.UserBrowserProperties
211211
import com.duckduckgo.browser.api.autocomplete.AutoComplete
212-
import com.duckduckgo.browser.api.autocomplete.AutoComplete.AutoCompleteResult
213212
import com.duckduckgo.browser.api.autocomplete.AutoComplete.AutoCompleteSuggestion.AutoCompleteDefaultSuggestion
214213
import com.duckduckgo.browser.api.autocomplete.AutoComplete.AutoCompleteSuggestion.AutoCompleteHistoryRelatedSuggestion.AutoCompleteHistorySearchSuggestion
215214
import com.duckduckgo.browser.api.autocomplete.AutoComplete.AutoCompleteSuggestion.AutoCompleteHistoryRelatedSuggestion.AutoCompleteHistorySuggestion
216-
import com.duckduckgo.browser.api.autocomplete.AutoComplete.AutoCompleteSuggestion.AutoCompleteSearchSuggestion
217-
import com.duckduckgo.browser.api.autocomplete.AutoComplete.AutoCompleteSuggestion.AutoCompleteUrlSuggestion.AutoCompleteBookmarkSuggestion
218215
import com.duckduckgo.browser.api.autocomplete.AutoComplete.AutoCompleteSuggestion.AutoCompleteUrlSuggestion.AutoCompleteSwitchToTabSuggestion
219216
import com.duckduckgo.browser.api.autocomplete.AutoCompleteSettings
220217
import com.duckduckgo.browser.api.brokensite.BrokenSiteContext
221218
import com.duckduckgo.common.test.CoroutineTestRule
222219
import com.duckduckgo.common.test.InstantSchedulersRule
223220
import com.duckduckgo.common.ui.tabs.SwipingTabsFeature
224221
import com.duckduckgo.common.ui.tabs.SwipingTabsFeatureProvider
222+
import com.duckduckgo.common.utils.DefaultDispatcherProvider
225223
import com.duckduckgo.common.utils.DispatcherProvider
226224
import com.duckduckgo.common.utils.baseHost
227225
import com.duckduckgo.common.utils.device.DeviceInfo
@@ -551,6 +549,7 @@ class BrowserTabViewModelTest {
551549
private val swipingTabsFeature = FakeFeatureToggleFactory.create(SwipingTabsFeature::class.java)
552550
private val swipingTabsFeatureProvider = SwipingTabsFeatureProvider(swipingTabsFeature)
553551
private val mockDuckChat: DuckChat = mock()
552+
private val mockHistory: NavigationHistory = mock()
554553

555554
private val defaultBrowserPromptsExperimentShowPopupMenuItemFlow = MutableStateFlow(false)
556555
private val mockAdditionalDefaultBrowserPrompts: AdditionalDefaultBrowserPrompts = mock()
@@ -615,6 +614,9 @@ class BrowserTabViewModelTest {
615614
mockUserStageStore,
616615
mockAutocompleteTabsFeature,
617616
mockDuckChat,
617+
mockHistory,
618+
DefaultDispatcherProvider(),
619+
mockPixel,
618620
)
619621
val fireproofWebsiteRepositoryImpl = FireproofWebsiteRepositoryImpl(
620622
fireproofWebsiteDao,
@@ -2454,115 +2456,6 @@ class BrowserTabViewModelTest {
24542456
assertCommandIssued<Command.ShowWebContent>()
24552457
}
24562458

2457-
@Test
2458-
fun whenBookmarkSuggestionSubmittedThenAutoCompleteBookmarkSelectionPixelSent() = runTest {
2459-
whenever(mockSavedSitesRepository.hasBookmarks()).thenReturn(true)
2460-
whenever(mockNavigationHistory.hasHistory()).thenReturn(false)
2461-
val suggestion = AutoCompleteBookmarkSuggestion("example", "Example", "https://example.com")
2462-
testee.autoCompleteViewState.value = autoCompleteViewState().copy(searchResults = AutoCompleteResult("", listOf(suggestion)))
2463-
testee.fireAutocompletePixel(suggestion)
2464-
val argumentCaptor = argumentCaptor<Map<String, String>>()
2465-
verify(mockPixel).fire(eq(AppPixelName.AUTOCOMPLETE_BOOKMARK_SELECTION), argumentCaptor.capture(), any(), any())
2466-
2467-
assertEquals("true", argumentCaptor.firstValue[PixelParameter.SHOWED_BOOKMARKS])
2468-
assertEquals("true", argumentCaptor.firstValue[PixelParameter.BOOKMARK_CAPABLE])
2469-
}
2470-
2471-
@Test
2472-
fun whenBookmarkFavoriteSubmittedThenAutoCompleteFavoriteSelectionPixelSent() = runTest {
2473-
whenever(mockSavedSitesRepository.hasBookmarks()).thenReturn(true)
2474-
whenever(mockSavedSitesRepository.hasFavorites()).thenReturn(true)
2475-
whenever(mockNavigationHistory.hasHistory()).thenReturn(false)
2476-
val suggestion = AutoCompleteBookmarkSuggestion("example", "Example", "https://example.com", isFavorite = true)
2477-
testee.autoCompleteViewState.value = autoCompleteViewState().copy(searchResults = AutoCompleteResult("", listOf(suggestion)))
2478-
testee.fireAutocompletePixel(suggestion)
2479-
2480-
val argumentCaptor = argumentCaptor<Map<String, String>>()
2481-
verify(mockPixel).fire(eq(AppPixelName.AUTOCOMPLETE_FAVORITE_SELECTION), argumentCaptor.capture(), any(), any())
2482-
2483-
assertEquals("false", argumentCaptor.firstValue[PixelParameter.SHOWED_BOOKMARKS])
2484-
assertEquals("true", argumentCaptor.firstValue[PixelParameter.SHOWED_FAVORITES])
2485-
assertEquals("true", argumentCaptor.firstValue[PixelParameter.BOOKMARK_CAPABLE])
2486-
assertEquals("true", argumentCaptor.firstValue[PixelParameter.FAVORITE_CAPABLE])
2487-
}
2488-
2489-
@Test
2490-
fun whenHistorySubmittedThenAutoCompleteHistorySelectionPixelSent() = runTest {
2491-
whenever(mockSavedSitesRepository.hasBookmarks()).thenReturn(true)
2492-
whenever(mockNavigationHistory.hasHistory()).thenReturn(true)
2493-
val suggestion = AutoCompleteHistorySearchSuggestion("example", true)
2494-
testee.autoCompleteViewState.value = autoCompleteViewState().copy(searchResults = AutoCompleteResult("", listOf(suggestion)))
2495-
testee.fireAutocompletePixel(suggestion)
2496-
2497-
val argumentCaptor = argumentCaptor<Map<String, String>>()
2498-
verify(mockPixel).fire(eq(AppPixelName.AUTOCOMPLETE_HISTORY_SEARCH_SELECTION), argumentCaptor.capture(), any(), any())
2499-
2500-
assertEquals("false", argumentCaptor.firstValue[PixelParameter.SHOWED_BOOKMARKS])
2501-
assertEquals("true", argumentCaptor.firstValue[PixelParameter.BOOKMARK_CAPABLE])
2502-
assertEquals("true", argumentCaptor.firstValue[PixelParameter.SHOWED_HISTORY])
2503-
assertEquals("true", argumentCaptor.firstValue[PixelParameter.HISTORY_CAPABLE])
2504-
}
2505-
2506-
@Test
2507-
fun whenSearchSuggestionSubmittedWithBookmarksThenAutoCompleteSearchSelectionPixelSent() = runTest {
2508-
whenever(mockSavedSitesRepository.hasBookmarks()).thenReturn(true)
2509-
whenever(mockNavigationHistory.hasHistory()).thenReturn(false)
2510-
val suggestions = listOf(AutoCompleteSearchSuggestion("", false, false), AutoCompleteBookmarkSuggestion("", "", ""))
2511-
testee.autoCompleteViewState.value = autoCompleteViewState().copy(searchResults = AutoCompleteResult("", suggestions))
2512-
testee.fireAutocompletePixel(AutoCompleteSearchSuggestion("example", false, false))
2513-
2514-
val argumentCaptor = argumentCaptor<Map<String, String>>()
2515-
verify(mockPixel).fire(eq(AppPixelName.AUTOCOMPLETE_SEARCH_PHRASE_SELECTION), argumentCaptor.capture(), any(), any())
2516-
2517-
assertEquals("true", argumentCaptor.firstValue[PixelParameter.SHOWED_BOOKMARKS])
2518-
assertEquals("true", argumentCaptor.firstValue[PixelParameter.BOOKMARK_CAPABLE])
2519-
}
2520-
2521-
@Test
2522-
fun whenSearchSuggestionSubmittedWithoutBookmarksThenAutoCompleteSearchSelectionPixelSent() = runTest {
2523-
whenever(mockSavedSitesRepository.hasBookmarks()).thenReturn(false)
2524-
whenever(mockNavigationHistory.hasHistory()).thenReturn(false)
2525-
testee.autoCompleteViewState.value = autoCompleteViewState().copy(searchResults = AutoCompleteResult("", emptyList()))
2526-
testee.fireAutocompletePixel(AutoCompleteSearchSuggestion("example", false, false))
2527-
2528-
val argumentCaptor = argumentCaptor<Map<String, String>>()
2529-
verify(mockPixel).fire(eq(AppPixelName.AUTOCOMPLETE_SEARCH_PHRASE_SELECTION), argumentCaptor.capture(), any(), any())
2530-
2531-
assertEquals("false", argumentCaptor.firstValue[PixelParameter.SHOWED_BOOKMARKS])
2532-
assertEquals("false", argumentCaptor.firstValue[PixelParameter.BOOKMARK_CAPABLE])
2533-
}
2534-
2535-
@Test
2536-
fun whenSearchSuggestionSubmittedWithTabsThenAutoCompleteSearchSelectionPixelSent() = runTest {
2537-
whenever(mockSavedSitesRepository.hasBookmarks()).thenReturn(false)
2538-
whenever(mockNavigationHistory.hasHistory()).thenReturn(false)
2539-
tabsLiveData.value = listOf(TabEntity("1", "https://example.com", position = 0), TabEntity("2", "https://example.com", position = 1))
2540-
val suggestions = listOf(AutoCompleteSwitchToTabSuggestion("example", "", "", ""))
2541-
testee.autoCompleteViewState.value = autoCompleteViewState().copy(searchResults = AutoCompleteResult("", suggestions))
2542-
testee.fireAutocompletePixel(AutoCompleteSwitchToTabSuggestion("example", "", "", ""))
2543-
2544-
val argumentCaptor = argumentCaptor<Map<String, String>>()
2545-
verify(mockPixel).fire(eq(AppPixelName.AUTOCOMPLETE_SWITCH_TO_TAB_SELECTION), argumentCaptor.capture(), any(), any())
2546-
2547-
assertEquals("true", argumentCaptor.firstValue[PixelParameter.SHOWED_SWITCH_TO_TAB])
2548-
assertEquals("true", argumentCaptor.firstValue[PixelParameter.SWITCH_TO_TAB_CAPABLE])
2549-
}
2550-
2551-
@Test
2552-
fun whenSearchSuggestionSubmittedWithoutTabsThenAutoCompleteSearchSelectionPixelSent() = runTest {
2553-
whenever(mockSavedSitesRepository.hasBookmarks()).thenReturn(false)
2554-
whenever(mockNavigationHistory.hasHistory()).thenReturn(false)
2555-
tabsLiveData.value = listOf(TabEntity("1", "https://example.com", position = 0))
2556-
testee.autoCompleteViewState.value = autoCompleteViewState().copy(searchResults = AutoCompleteResult("", emptyList()))
2557-
testee.fireAutocompletePixel(AutoCompleteSwitchToTabSuggestion("example", "", "", ""))
2558-
2559-
val argumentCaptor = argumentCaptor<Map<String, String>>()
2560-
verify(mockPixel).fire(eq(AppPixelName.AUTOCOMPLETE_SWITCH_TO_TAB_SELECTION), argumentCaptor.capture(), any(), any())
2561-
2562-
assertEquals("false", argumentCaptor.firstValue[PixelParameter.SHOWED_SWITCH_TO_TAB])
2563-
assertEquals("false", argumentCaptor.firstValue[PixelParameter.SWITCH_TO_TAB_CAPABLE])
2564-
}
2565-
25662459
@Test
25672460
fun whenUserSelectToEditQueryThenMoveCaretToTheEnd() = runTest {
25682461
testee.onUserSelectedToEditQuery("foo")
@@ -6029,7 +5922,10 @@ class BrowserTabViewModelTest {
60295922
fun whenUserSelectedAutocompleteWithAutoCompleteSwitchToTabSuggestionThenSwitchToTabCommandSentWithTabId() = runTest {
60305923
val tabId = "tabId"
60315924
val suggestion = AutoCompleteSwitchToTabSuggestion(phrase = "phrase", title = "title", url = "https://www.example.com", tabId = tabId)
5925+
5926+
whenever(mockDuckChat.wasOpenedBefore()).thenReturn(true)
60325927
whenever(mockSavedSitesRepository.hasBookmarks()).thenReturn(false)
5928+
whenever(mockSavedSitesRepository.hasFavorites()).thenReturn(false)
60335929
whenever(mockNavigationHistory.hasHistory()).thenReturn(false)
60345930

60355931
testee.userSelectedAutocomplete(suggestion)

app/src/main/java/com/duckduckgo/app/autocomplete/api/AutoComplete.kt

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,13 @@ import android.net.Uri
2020
import androidx.annotation.VisibleForTesting
2121
import androidx.core.net.toUri
2222
import com.duckduckgo.app.autocomplete.AutocompleteTabsFeature
23+
import com.duckduckgo.app.autocomplete.impl.AutoCompletePixelNames
2324
import com.duckduckgo.app.autocomplete.impl.AutoCompleteRepository
2425
import com.duckduckgo.app.browser.UriString
2526
import com.duckduckgo.app.onboarding.store.AppStage
2627
import com.duckduckgo.app.onboarding.store.UserStageStore
28+
import com.duckduckgo.app.statistics.pixels.Pixel
29+
import com.duckduckgo.app.statistics.pixels.Pixel.PixelParameter
2730
import com.duckduckgo.app.tabs.model.TabEntity
2831
import com.duckduckgo.app.tabs.model.TabRepository
2932
import com.duckduckgo.browser.api.autocomplete.AutoComplete
@@ -40,6 +43,7 @@ import com.duckduckgo.browser.api.autocomplete.AutoComplete.AutoCompleteSuggesti
4043
import com.duckduckgo.browser.api.autocomplete.AutoComplete.AutoCompleteSuggestion.AutoCompleteUrlSuggestion.AutoCompleteSwitchToTabSuggestion
4144
import com.duckduckgo.common.utils.AppUrl
4245
import com.duckduckgo.common.utils.AppUrl.Url
46+
import com.duckduckgo.common.utils.DispatcherProvider
4347
import com.duckduckgo.common.utils.UrlScheme
4448
import com.duckduckgo.common.utils.baseHost
4549
import com.duckduckgo.common.utils.toStringDropScheme
@@ -62,6 +66,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged
6266
import kotlinx.coroutines.flow.flow
6367
import kotlinx.coroutines.flow.flowOf
6468
import kotlinx.coroutines.flow.map
69+
import kotlinx.coroutines.withContext
6570

6671
const val maximumNumberOfSuggestions = 12
6772
const val maximumNumberOfTopHits = 2
@@ -78,6 +83,9 @@ class AutoCompleteApi @Inject constructor(
7883
private val userStageStore: UserStageStore,
7984
private val autocompleteTabsFeature: AutocompleteTabsFeature,
8085
private val duckChat: DuckChat,
86+
private val history: NavigationHistory,
87+
private val dispatchers: DispatcherProvider,
88+
private val pixel: Pixel,
8189
) : AutoComplete {
8290

8391
private var isAutocompleteTabsFeatureEnabled: Boolean? = null
@@ -229,6 +237,68 @@ class AutoCompleteApi @Inject constructor(
229237
autoCompleteRepository.submitUserSeenHistoryIAM()
230238
}
231239

240+
override suspend fun fireAutocompletePixel(
241+
suggestions: List<AutoCompleteSuggestion>,
242+
suggestion: AutoCompleteSuggestion,
243+
experimentalInputScreen: Boolean,
244+
) {
245+
val hasBookmarks = withContext(dispatchers.io()) {
246+
savedSitesRepository.hasBookmarks()
247+
}
248+
val hasFavorites = withContext(dispatchers.io()) {
249+
savedSitesRepository.hasFavorites()
250+
}
251+
val hasHistory = withContext(dispatchers.io()) {
252+
history.hasHistory()
253+
}
254+
val hasTabs = withContext(dispatchers.io()) {
255+
(tabRepository.liveTabs.value?.size ?: 0) > 1
256+
}
257+
258+
val hasBookmarkResults = suggestions.any { it is AutoCompleteBookmarkSuggestion && !it.isFavorite }
259+
val hasFavoriteResults = suggestions.any { it is AutoCompleteBookmarkSuggestion && it.isFavorite }
260+
val hasHistoryResults = suggestions.any { it is AutoCompleteHistorySuggestion || it is AutoCompleteHistorySearchSuggestion }
261+
val hasSwitchToTabResults = suggestions.any { it is AutoCompleteSwitchToTabSuggestion }
262+
val params = mapOf(
263+
PixelParameter.SHOWED_BOOKMARKS to hasBookmarkResults.toString(),
264+
PixelParameter.SHOWED_FAVORITES to hasFavoriteResults.toString(),
265+
PixelParameter.BOOKMARK_CAPABLE to hasBookmarks.toString(),
266+
PixelParameter.FAVORITE_CAPABLE to hasFavorites.toString(),
267+
PixelParameter.HISTORY_CAPABLE to hasHistory.toString(),
268+
PixelParameter.SHOWED_HISTORY to hasHistoryResults.toString(),
269+
PixelParameter.SWITCH_TO_TAB_CAPABLE to hasTabs.toString(),
270+
PixelParameter.SHOWED_SWITCH_TO_TAB to hasSwitchToTabResults.toString(),
271+
)
272+
val pixelName = when (suggestion) {
273+
is AutoCompleteBookmarkSuggestion -> {
274+
if (suggestion.isFavorite) {
275+
AutoCompletePixelNames.AUTOCOMPLETE_FAVORITE_SELECTION
276+
} else {
277+
AutoCompletePixelNames.AUTOCOMPLETE_BOOKMARK_SELECTION
278+
}
279+
}
280+
281+
is AutoCompleteSearchSuggestion -> if (suggestion.isUrl) {
282+
AutoCompletePixelNames.AUTOCOMPLETE_SEARCH_WEBSITE_SELECTION
283+
} else {
284+
AutoCompletePixelNames.AUTOCOMPLETE_SEARCH_PHRASE_SELECTION
285+
}
286+
287+
is AutoCompleteHistorySuggestion -> AutoCompletePixelNames.AUTOCOMPLETE_HISTORY_SITE_SELECTION
288+
is AutoCompleteHistorySearchSuggestion -> AutoCompletePixelNames.AUTOCOMPLETE_HISTORY_SEARCH_SELECTION
289+
is AutoCompleteSwitchToTabSuggestion -> AutoCompletePixelNames.AUTOCOMPLETE_SWITCH_TO_TAB_SELECTION
290+
is AutoCompleteSuggestion.AutoCompleteDuckAIPrompt -> if (experimentalInputScreen) {
291+
AutoCompletePixelNames.AUTOCOMPLETE_DUCKAI_PROMPT_EXPERIMENTAL_SELECTION
292+
} else {
293+
AutoCompletePixelNames.AUTOCOMPLETE_DUCKAI_PROMPT_LEGACY_SELECTION
294+
}
295+
296+
else -> return
297+
}
298+
299+
pixel.fire(pixelName, params)
300+
}
301+
232302
private fun isAllowedInTopHits(entry: HistoryEntry): Boolean {
233303
return entry.visits.size > 3 || entry.url.isRoot()
234304
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright (c) 2025 DuckDuckGo
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.duckduckgo.app.autocomplete.impl
18+
19+
import com.duckduckgo.app.statistics.pixels.Pixel
20+
21+
enum class AutoCompletePixelNames(override val pixelName: String) : Pixel.PixelName {
22+
23+
AUTOCOMPLETE_BOOKMARK_SELECTION("m_autocomplete_click_bookmark"),
24+
AUTOCOMPLETE_FAVORITE_SELECTION("m_autocomplete_click_favorite"),
25+
AUTOCOMPLETE_SEARCH_PHRASE_SELECTION("m_autocomplete_click_phrase"),
26+
AUTOCOMPLETE_SEARCH_WEBSITE_SELECTION("m_autocomplete_click_website"),
27+
28+
AUTOCOMPLETE_HISTORY_SEARCH_SELECTION("m_autocomplete_click_history_search"),
29+
AUTOCOMPLETE_HISTORY_SITE_SELECTION("m_autocomplete_click_history_site"),
30+
31+
AUTOCOMPLETE_SWITCH_TO_TAB_SELECTION("m_autocomplete_click_switch_to_tab"),
32+
33+
AUTOCOMPLETE_DUCKAI_PROMPT_EXPERIMENTAL_SELECTION("m_autocomplete_click_duckai_experimental"),
34+
AUTOCOMPLETE_DUCKAI_PROMPT_LEGACY_SELECTION("m_autocomplete_click_duckai_legacy"),
35+
}

0 commit comments

Comments
 (0)