Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions PixelDefinitions/pixels/voice_search.json5
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"m_voice_search_serp_done": {
"description": "User completed a voice search with the Search mode selected.",
"owners": ["joshliebe"],
"triggers": ["other"],
"suffixes": ["form_factor"],
"parameters": ["appVersion"]
},
"m_voice_search_aichat_done": {
"description": "User completed a voice search with the Duck.ai mode selected.",
"owners": ["joshliebe"],
"triggers": ["other"],
"suffixes": ["form_factor"],
"parameters": ["appVersion"]
},
"m_voice_search_cancelled": {
"description": "User canceled a voice search.",
"owners": ["joshliebe"],
"triggers": ["other"],
"suffixes": ["form_factor"],
"parameters": ["appVersion"]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -76,13 +76,27 @@ class RealVoiceSearchActivityLauncher @Inject constructor(
Request.ResultFromVoiceSearch { code, data ->
if (code == Activity.RESULT_OK) {
if (data.isNotEmpty()) {
if (duckAiFeatureState.showVoiceSearchToggle.value) {
val pixelName = when (mode) {
VoiceSearchMode.SEARCH -> VoiceSearchPixelNames.VOICE_SEARCH_SERP_DONE
VoiceSearchMode.DUCK_AI -> VoiceSearchPixelNames.VOICE_SEARCH_AICHAT_DONE
}
pixel.fire(
pixel = pixelName,
parameters = mapOf(KEY_PARAM_SOURCE to _source?.paramValueName.orEmpty()),
)
}
pixel.fire(
pixel = VoiceSearchPixelNames.VOICE_SEARCH_DONE,
parameters = mapOf(KEY_PARAM_SOURCE to _source?.paramValueName.orEmpty()),
)
voiceSearchRepository.resetVoiceSearchDismissed()
onEvent(Event.VoiceRecognitionSuccess(data))
} else {
pixel.fire(
pixel = VoiceSearchPixelNames.VOICE_SEARCH_CANCELLED,
parameters = mapOf(KEY_PARAM_SOURCE to _source?.paramValueName.orEmpty()),
)
onEvent(Event.SearchCancelled)
}
} else {
Expand All @@ -102,6 +116,10 @@ class RealVoiceSearchActivityLauncher @Inject constructor(
snackbar.show()
}
} else {
pixel.fire(
pixel = VoiceSearchPixelNames.VOICE_SEARCH_CANCELLED,
parameters = mapOf(KEY_PARAM_SOURCE to _source?.paramValueName.orEmpty()),
)
onEvent(Event.SearchCancelled)
}
voiceSearchRepository.dismissVoiceSearch()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,14 @@ enum class VoiceSearchPixelNames(override val pixelName: String) : Pixel.PixelNa
VOICE_SEARCH_PRIVACY_DIALOG_REJECTED("m_voice_search_privacy_dialog_rejected"),
VOICE_SEARCH_STARTED("m_voice_search_started"),
VOICE_SEARCH_DONE("m_voice_search_done"),
VOICE_SEARCH_SERP_DONE("m_voice_search_serp_done"),
VOICE_SEARCH_AICHAT_DONE("m_voice_search_aichat_done"),
VOICE_SEARCH_ON("m_voice_search_on"),
VOICE_SEARCH_OFF("m_voice_search_off"),
VOICE_SEARCH_GENERAL_SETTINGS_ON("m_settings_general_voice_search_on"),
VOICE_SEARCH_GENERAL_SETTINGS_OFF("m_settings_general_voice_search_off"),
VOICE_SEARCH_ERROR("m_voice_search_error"),
VOICE_SEARCH_CANCELLED("m_voice_search_cancelled"),
VOICE_SEARCH_REMOVE_DIALOG_SEEN("m_voice_search_remove_dialog_seen"),
VOICE_SEARCH_REMOVE_DIALOG_REMOVE("m_voice_search_remove_dialog_remove"),
VOICE_SEARCH_REMOVE_DIALOG_CANCEL("m_voice_search_remove_dialog_cancel"),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright (c) 2025 DuckDuckGo
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.duckduckgo.voice.impl

import com.duckduckgo.common.utils.plugins.pixel.PixelParamRemovalPlugin
import com.duckduckgo.common.utils.plugins.pixel.PixelParamRemovalPlugin.PixelParameter
import com.duckduckgo.di.scopes.AppScope
import com.squareup.anvil.annotations.ContributesMultibinding
import javax.inject.Inject

@ContributesMultibinding(AppScope::class)
class VoiceSearchPixelParamRemovalPlugin @Inject constructor() : PixelParamRemovalPlugin {
override fun names(): List<Pair<String, Set<PixelParameter>>> {
return listOf(
VoiceSearchPixelNames.VOICE_SEARCH_SERP_DONE.pixelName to PixelParameter.removeAtb(),
VoiceSearchPixelNames.VOICE_SEARCH_AICHAT_DONE.pixelName to PixelParameter.removeAtb(),
VoiceSearchPixelNames.VOICE_SEARCH_CANCELLED.pixelName to PixelParameter.removeAtb(),
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -173,10 +173,7 @@ class RealVoiceSearchActivityLauncherTest {

@Test
fun whenResultFromVoiceSearchIsOkThenResetDismissedCounter() {
var lastKnownEvent: Event? = null
testee.registerResultsCallback(mock(), mock(), BROWSER) {
lastKnownEvent = it
}
testee.registerResultsCallback(mock(), mock(), BROWSER) { }
whenever(voiceSearchRepository.countVoiceSearchDismissed()).thenReturn(1)

val lastKnownRequest = activityResultLauncherWrapper.lastKnownRequest as ActivityResultLauncherWrapper.Request.ResultFromVoiceSearch
Expand All @@ -186,6 +183,50 @@ class RealVoiceSearchActivityLauncherTest {
verify(voiceSearchRepository, never()).dismissVoiceSearch()
}

@Test
fun whenResultFromVoiceSearchWithSearchModeAndToggleEnabledThenFireSerpDonePixel() {
showVoiceSearchToggleFlow.value = true
testee.registerResultsCallback(mock(), mock(), BROWSER) { }

val lastKnownRequest = activityResultLauncherWrapper.lastKnownRequest as ActivityResultLauncherWrapper.Request.ResultFromVoiceSearch
lastKnownRequest.onResult(Activity.RESULT_OK, "Result", VoiceSearchMode.SEARCH)

verify(pixel).fire(VoiceSearchPixelNames.VOICE_SEARCH_SERP_DONE, mapOf("source" to "browser"))
}

@Test
fun whenResultFromVoiceSearchWithSearchModeAndToggleDisabledThenDoNotFireSerpDonePixel() {
showVoiceSearchToggleFlow.value = false
testee.registerResultsCallback(mock(), mock(), BROWSER) { }

val lastKnownRequest = activityResultLauncherWrapper.lastKnownRequest as ActivityResultLauncherWrapper.Request.ResultFromVoiceSearch
lastKnownRequest.onResult(Activity.RESULT_OK, "Result", VoiceSearchMode.SEARCH)

verify(pixel, never()).fire(VoiceSearchPixelNames.VOICE_SEARCH_SERP_DONE, mapOf("source" to "browser"))
}

@Test
fun whenResultFromVoiceSearchWithDuckAiModeAndToggleEnabledThenFireAiChatDonePixel() {
showVoiceSearchToggleFlow.value = true
testee.registerResultsCallback(mock(), mock(), BROWSER) { }

val lastKnownRequest = activityResultLauncherWrapper.lastKnownRequest as ActivityResultLauncherWrapper.Request.ResultFromVoiceSearch
lastKnownRequest.onResult(Activity.RESULT_OK, "Result", VoiceSearchMode.DUCK_AI)

verify(pixel).fire(VoiceSearchPixelNames.VOICE_SEARCH_AICHAT_DONE, mapOf("source" to "browser"))
}

@Test
fun whenResultFromVoiceSearchWithDuckAiModeAndToggleDisabledThenDoNotFireAiChatDonePixel() {
showVoiceSearchToggleFlow.value = false
testee.registerResultsCallback(mock(), mock(), BROWSER) { }

val lastKnownRequest = activityResultLauncherWrapper.lastKnownRequest as ActivityResultLauncherWrapper.Request.ResultFromVoiceSearch
lastKnownRequest.onResult(Activity.RESULT_OK, "Result", VoiceSearchMode.DUCK_AI)

verify(pixel, never()).fire(VoiceSearchPixelNames.VOICE_SEARCH_AICHAT_DONE, mapOf("source" to "browser"))
}

@Test
fun whenBrowserVoiceSearchLaunchedThenEmitStartedPixelAndCallLaunchVoiceSearch() {
testee.registerResultsCallback(mock(), mock(), BROWSER) { }
Expand Down
Loading