Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement pixels for feature discoverability #5703

Merged
merged 7 commits into from
Mar 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ import com.duckduckgo.downloads.api.DownloadStateListener
import com.duckduckgo.downloads.api.FileDownloader
import com.duckduckgo.downloads.api.FileDownloader.PendingFileDownload
import com.duckduckgo.duckchat.api.DuckChat
import com.duckduckgo.duckchat.impl.DuckChatPixelName
import com.duckduckgo.duckplayer.api.DuckPlayer
import com.duckduckgo.duckplayer.api.DuckPlayer.DuckPlayerOrigin.AUTO
import com.duckduckgo.duckplayer.api.DuckPlayer.DuckPlayerOrigin.OVERLAY
Expand Down Expand Up @@ -5783,6 +5784,28 @@ class BrowserTabViewModelTest {
assertCommandNotIssued<Command.SendResponseToJs>()
}

@Test
fun whenDuckChatMenuItemClickedAndItWasntUsedBeforeThenOpenDuckChatAndSendPixel() = runTest {
whenever(mockDuckChat.wasOpenedBefore()).thenReturn(false)

testee.onDuckChatMenuClicked()

verify(mockPixel).fire(DuckChatPixelName.DUCK_CHAT_OPEN)
verify(mockPixel).fire(DuckChatPixelName.DUCK_CHAT_OPEN_BROWSER_MENU, mapOf("was_used_before" to "0"))
verify(mockDuckChat).openDuckChat()
}

@Test
fun whenDuckChatMenuItemClickedAndItWasUsedBeforeThenOpenDuckChatAndSendPixel() = runTest {
whenever(mockDuckChat.wasOpenedBefore()).thenReturn(true)

testee.onDuckChatMenuClicked()

verify(mockPixel).fire(DuckChatPixelName.DUCK_CHAT_OPEN)
verify(mockPixel).fire(DuckChatPixelName.DUCK_CHAT_OPEN_BROWSER_MENU, mapOf("was_used_before" to "1"))
verify(mockDuckChat).openDuckChat()
}

private fun aCredential(): LoginCredentials {
return LoginCredentials(domain = null, username = null, password = null)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,6 @@ import com.duckduckgo.downloads.api.DownloadsFileActions
import com.duckduckgo.downloads.api.FileDownloader
import com.duckduckgo.downloads.api.FileDownloader.PendingFileDownload
import com.duckduckgo.duckchat.api.DuckChat
import com.duckduckgo.duckchat.impl.DuckChatPixelName
import com.duckduckgo.duckplayer.api.DuckPlayer
import com.duckduckgo.duckplayer.api.DuckPlayerSettingsNoParams
import com.duckduckgo.js.messaging.api.JsCallbackData
Expand Down Expand Up @@ -1028,8 +1027,7 @@ class BrowserTabFragment :
onOmnibarNewTabRequested()
}
onMenuItemClicked(duckChatMenuItem) {
pixel.fire(DuckChatPixelName.DUCK_CHAT_OPEN)
duckChat.openDuckChat()
viewModel.onDuckChatMenuClicked()
}
onMenuItemClicked(bookmarksMenuItem) {
browserActivity?.launchBookmarks()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,7 @@ import com.duckduckgo.common.utils.SingleLiveEvent
import com.duckduckgo.common.utils.baseHost
import com.duckduckgo.common.utils.device.DeviceInfo
import com.duckduckgo.common.utils.extensions.asLocationPermissionOrigin
import com.duckduckgo.common.utils.extensions.toBinaryString
import com.duckduckgo.common.utils.isMobileSite
import com.duckduckgo.common.utils.plugins.PluginPoint
import com.duckduckgo.common.utils.plugins.headers.CustomHeadersProvider
Expand All @@ -295,6 +296,7 @@ import com.duckduckgo.downloads.api.DownloadStateListener
import com.duckduckgo.downloads.api.FileDownloader
import com.duckduckgo.downloads.api.FileDownloader.PendingFileDownload
import com.duckduckgo.duckchat.api.DuckChat
import com.duckduckgo.duckchat.impl.DuckChatPixelName
import com.duckduckgo.duckplayer.api.DuckPlayer
import com.duckduckgo.duckplayer.api.DuckPlayer.DuckPlayerState.ENABLED
import com.duckduckgo.history.api.NavigationHistory
Expand Down Expand Up @@ -3829,6 +3831,18 @@ class BrowserTabViewModel @Inject constructor(
command.value = Command.SwitchToTab(tabId)
}

fun onDuckChatMenuClicked() {
viewModelScope.launch {
pixel.fire(DuckChatPixelName.DUCK_CHAT_OPEN)

val wasUsedBefore = duckChat.wasOpenedBefore()
val params = mapOf("was_used_before" to wasUsedBefore.toBinaryString())
pixel.fire(DuckChatPixelName.DUCK_CHAT_OPEN_BROWSER_MENU, parameters = params)

duckChat.openDuckChat()
}
}

companion object {
private const val FIXED_PROGRESS = 50

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,13 @@ import com.duckduckgo.app.pixels.AppPixelName.SETTINGS_APPEARANCE_PRESSED
import com.duckduckgo.app.pixels.AppPixelName.SETTINGS_APPTP_PRESSED
import com.duckduckgo.app.pixels.AppPixelName.SETTINGS_COOKIE_POPUP_PROTECTION_PRESSED
import com.duckduckgo.app.pixels.AppPixelName.SETTINGS_DEFAULT_BROWSER_PRESSED
import com.duckduckgo.app.pixels.AppPixelName.SETTINGS_EMAIL_PROTECTION_PRESSED
import com.duckduckgo.app.pixels.AppPixelName.SETTINGS_FIRE_BUTTON_PRESSED
import com.duckduckgo.app.pixels.AppPixelName.SETTINGS_GENERAL_PRESSED
import com.duckduckgo.app.pixels.AppPixelName.SETTINGS_NEXT_STEPS_ADDRESS_BAR
import com.duckduckgo.app.pixels.AppPixelName.SETTINGS_NEXT_STEPS_VOICE_SEARCH
import com.duckduckgo.app.pixels.AppPixelName.SETTINGS_OPENED
import com.duckduckgo.app.pixels.AppPixelName.SETTINGS_PERMISSIONS_PRESSED
import com.duckduckgo.app.pixels.AppPixelName.SETTINGS_PRIVATE_SEARCH_PRESSED
import com.duckduckgo.app.pixels.AppPixelName.SETTINGS_SYNC_PRESSED
import com.duckduckgo.app.pixels.AppPixelName.SETTINGS_WEB_TRACKING_PROTECTION_PRESSED
import com.duckduckgo.app.settings.NewSettingsViewModel.Command.LaunchAboutScreen
import com.duckduckgo.app.settings.NewSettingsViewModel.Command.LaunchAccessibilitySettings
Expand Down Expand Up @@ -106,6 +104,7 @@ class NewSettingsViewModel @Inject constructor(
private val duckChat: DuckChat,
private val voiceSearchAvailability: VoiceSearchAvailability,
private val privacyProUnifiedFeedback: PrivacyProUnifiedFeedback,
private val settingsPixelDispatcher: SettingsPixelDispatcher,
) : ViewModel(), DefaultLifecycleObserver {

data class ViewState(
Expand Down Expand Up @@ -281,7 +280,7 @@ class NewSettingsViewModel @Inject constructor(
}
[email protected](command)
}
pixel.fire(SETTINGS_EMAIL_PROTECTION_PRESSED)
settingsPixelDispatcher.fireEmailPressed()
}

fun onAppTPSettingClicked() {
Expand All @@ -301,7 +300,7 @@ class NewSettingsViewModel @Inject constructor(

fun onSyncSettingClicked() {
viewModelScope.launch { command.send(LaunchSyncSettings) }
pixel.fire(SETTINGS_SYNC_PRESSED)
settingsPixelDispatcher.fireSyncPressed()
}

fun onFireButtonSettingClicked() {
Expand All @@ -316,6 +315,7 @@ class NewSettingsViewModel @Inject constructor(

fun onDuckChatSettingClicked() {
viewModelScope.launch { command.send(LaunchDuckChatScreen) }
settingsPixelDispatcher.fireDuckChatPressed()
}

fun onAppearanceSettingClicked() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* 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.app.settings

import com.duckduckgo.app.di.AppCoroutineScope
import com.duckduckgo.app.pixels.AppPixelName.SETTINGS_EMAIL_PROTECTION_PRESSED
import com.duckduckgo.app.pixels.AppPixelName.SETTINGS_SYNC_PRESSED
import com.duckduckgo.app.statistics.pixels.Pixel
import com.duckduckgo.autofill.api.email.EmailManager
import com.duckduckgo.common.utils.extensions.toBinaryString
import com.duckduckgo.di.scopes.AppScope
import com.duckduckgo.duckchat.api.DuckChat
import com.duckduckgo.duckchat.impl.DuckChatPixelName.DUCK_CHAT_SETTINGS_PRESSED
import com.duckduckgo.sync.api.SyncState.OFF
import com.duckduckgo.sync.api.SyncStateMonitor
import com.squareup.anvil.annotations.ContributesBinding
import dagger.SingleInstanceIn
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch

/**
* Utility to dispatch pixels triggered by interactions on the settings screen.
*/
interface SettingsPixelDispatcher {
fun fireSyncPressed()
fun fireDuckChatPressed()
fun fireEmailPressed()
}

@ContributesBinding(scope = AppScope::class)
@SingleInstanceIn(AppScope::class)
class SettingsPixelDispatcherImpl @Inject constructor(
@AppCoroutineScope private val appCoroutineScope: CoroutineScope,
private val pixel: Pixel,
private val syncStateMonitor: SyncStateMonitor,
private val duckChat: DuckChat,
private val emailManager: EmailManager,
) : SettingsPixelDispatcher {

override fun fireSyncPressed() {
appCoroutineScope.launch {
val syncState = syncStateMonitor.syncState().firstOrNull()
val isEnabled = syncState != null && syncState != OFF
pixel.fire(
pixel = SETTINGS_SYNC_PRESSED,
parameters = mapOf(
PARAM_SYNC_IS_ENABLED to isEnabled.toBinaryString(),
),
)
}
}

override fun fireDuckChatPressed() {
appCoroutineScope.launch {
val wasUsedBefore = duckChat.wasOpenedBefore()
pixel.fire(
pixel = DUCK_CHAT_SETTINGS_PRESSED,
parameters = mapOf(
PARAM_DUCK_CHAT_USED_BEFORE to wasUsedBefore.toBinaryString(),
),
)
}
}

override fun fireEmailPressed() {
appCoroutineScope.launch {
val isSignedIn = emailManager.isSignedIn()
pixel.fire(
pixel = SETTINGS_EMAIL_PROTECTION_PRESSED,
parameters = mapOf(
PARAM_EMAIL_IS_SIGNED_IN to isSignedIn.toBinaryString(),
),
)
}
}

private companion object {
const val PARAM_SYNC_IS_ENABLED = "is_enabled"
const val PARAM_DUCK_CHAT_USED_BEFORE = "was_used_before"
const val PARAM_EMAIL_IS_SIGNED_IN = "is_signed_in"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@ import com.duckduckgo.common.ui.view.show
import com.duckduckgo.common.utils.DispatcherProvider
import com.duckduckgo.di.scopes.ActivityScope
import com.duckduckgo.duckchat.api.DuckChat
import com.duckduckgo.duckchat.impl.DuckChatPixelName
import com.google.android.material.snackbar.BaseTransientBottomBar
import com.google.android.material.snackbar.Snackbar
import javax.inject.Inject
Expand Down Expand Up @@ -333,8 +332,7 @@ class TabSwitcherActivity : DuckDuckGoActivity(), TabSwitcherListener, Coroutine
R.id.newTab -> onNewTabRequested(fromOverflowMenu = false)
R.id.newTabOverflow -> onNewTabRequested(fromOverflowMenu = true)
R.id.duckChat -> {
pixel.fire(DuckChatPixelName.DUCK_CHAT_OPEN)
duckChat.openDuckChat()
viewModel.onDuckChatMenuClicked()
}
R.id.closeAllTabs -> closeAllTabs()
R.id.downloads -> showDownloads()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ import com.duckduckgo.app.tabs.model.TabSwitcherData.LayoutType.GRID
import com.duckduckgo.app.tabs.model.TabSwitcherData.LayoutType.LIST
import com.duckduckgo.common.utils.DispatcherProvider
import com.duckduckgo.common.utils.SingleLiveEvent
import com.duckduckgo.common.utils.extensions.toBinaryString
import com.duckduckgo.di.scopes.ActivityScope
import com.duckduckgo.duckchat.api.DuckChat
import com.duckduckgo.duckchat.impl.DuckChatPixelName
import javax.inject.Inject
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.first
Expand All @@ -50,6 +53,7 @@ class TabSwitcherViewModel @Inject constructor(
private val dispatcherProvider: DispatcherProvider,
private val pixel: Pixel,
private val swipingTabsFeature: SwipingTabsFeatureProvider,
private val duckChat: DuckChat,
) : ViewModel() {
val tabSwitcherItems: LiveData<List<TabSwitcherItem>> = tabRepository.liveTabs.map { tabEntities ->
tabEntities.map { TabSwitcherItem.Tab(it) }
Expand Down Expand Up @@ -188,4 +192,16 @@ class TabSwitcherViewModel @Inject constructor(
tabRepository.setTabLayoutType(newLayoutType)
}
}

fun onDuckChatMenuClicked() {
viewModelScope.launch {
pixel.fire(DuckChatPixelName.DUCK_CHAT_OPEN)

val wasUsedBefore = duckChat.wasOpenedBefore()
val params = mapOf("was_used_before" to wasUsedBefore.toBinaryString())
pixel.fire(DuckChatPixelName.DUCK_CHAT_OPEN_NEW_TAB_MENU, parameters = params)

duckChat.openDuckChat()
}
}
}
Loading
Loading