Skip to content

Commit

Permalink
Add pixels for importing google passwords metrics
Browse files Browse the repository at this point in the history
  • Loading branch information
CDRussell committed Nov 18, 2024
1 parent 720a9dc commit ef5e18b
Show file tree
Hide file tree
Showing 11 changed files with 83 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import com.squareup.anvil.annotations.ContributesBinding
import javax.inject.Inject

interface AutofillEngagementBucketing {
fun bucketNumberOfSavedPasswords(savedPasswords: Int): String
fun bucketNumberOfCredentials(numberOfCredentials: Int): String

companion object {
const val NONE = "none"
Expand All @@ -40,12 +40,12 @@ interface AutofillEngagementBucketing {
@ContributesBinding(AppScope::class)
class DefaultAutofillEngagementBucketing @Inject constructor() : AutofillEngagementBucketing {

override fun bucketNumberOfSavedPasswords(savedPasswords: Int): String {
override fun bucketNumberOfCredentials(numberOfCredentials: Int): String {
return when {
savedPasswords == 0 -> NONE
savedPasswords < 4 -> FEW
savedPasswords < 11 -> SOME
savedPasswords < 50 -> MANY
numberOfCredentials == 0 -> NONE
numberOfCredentials < 4 -> FEW
numberOfCredentials < 11 -> SOME
numberOfCredentials < 50 -> MANY
else -> LOTS
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ class DefaultAutofillEngagementRepository @Inject constructor(

val numberStoredPasswords = getNumberStoredPasswords()
val togglePixel = if (autofillStore.autofillEnabled) AUTOFILL_TOGGLED_ON_SEARCH else AUTOFILL_TOGGLED_OFF_SEARCH
val bucket = engagementBucketing.bucketNumberOfSavedPasswords(numberStoredPasswords)
val bucket = engagementBucketing.bucketNumberOfCredentials(numberStoredPasswords)
pixel.fire(togglePixel, mapOf("count_bucket" to bucket), type = Daily())
}

Expand All @@ -113,7 +113,7 @@ class DefaultAutofillEngagementRepository @Inject constructor(
if (autofilled && searched) {
pixel.fire(AUTOFILL_ENGAGEMENT_ACTIVE_USER, type = Daily())

val bucket = engagementBucketing.bucketNumberOfSavedPasswords(numberStoredPasswords)
val bucket = engagementBucketing.bucketNumberOfCredentials(numberStoredPasswords)
pixel.fire(AUTOFILL_ENGAGEMENT_STACKED_LOGINS, mapOf("count_bucket" to bucket), type = Daily())
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,17 @@ enum class AutofillPixelNames(override val pixelName: String) : Pixel.PixelName
AUTOFILL_TOGGLED_ON_SEARCH("m_autofill_toggled_on"),
AUTOFILL_TOGGLED_OFF_SEARCH("m_autofill_toggled_off"),

AUTOFILL_IMPORT_GOOGLE_PASSWORDS_EMPTY_STATE_CTA_BUTTON_TAPPED("autofill_import_google_passwords_import_button_tapped"),
AUTOFILL_IMPORT_GOOGLE_PASSWORDS_EMPTY_STATE_CTA_BUTTON_SHOWN("autofill_import_google_passwords_import_button_shown"),
AUTOFILL_IMPORT_GOOGLE_PASSWORDS_OVERFLOW_MENU("autofill_import_google_passwords_overflow_menu_tapped"),
AUTOFILL_IMPORT_GOOGLE_PASSWORDS_PREIMPORT_PROMPT_DISPLAYED("autofill_import_google_passwords_preimport_prompt_displayed"),
AUTOFILL_IMPORT_GOOGLE_PASSWORDS_PREIMPORT_PROMPT_DISMISSED("autofill_import_google_passwords_preimport_prompt_dismissed"),
AUTOFILL_IMPORT_GOOGLE_PASSWORDS_PREIMPORT_PROMPT_CONFIRMED("autofill_import_google_passwords_preimport_prompt_confirmed"),
AUTOFILL_IMPORT_GOOGLE_PASSWORDS_RESULT_FAILURE_ERROR_ENCRYPTION("autofill_import_google_passwords_result_failure_encryption"),
AUTOFILL_IMPORT_GOOGLE_PASSWORDS_RESULT_FAILURE_ERROR_PARSING("autofill_import_google_passwords_result_parsing"),
AUTOFILL_IMPORT_GOOGLE_PASSWORDS_RESULT_FAILURE_USER_CANCELLED("autofill_import_google_passwords_result_user_cancelled"),
AUTOFILL_IMPORT_GOOGLE_PASSWORDS_RESULT_SUCCESS("autofill_import_google_passwords_result_success"),

AUTOFILL_SYNC_DESKTOP_PASSWORDS_CTA_BUTTON("m_autofill_logins_import_no_passwords"),
AUTOFILL_SYNC_DESKTOP_PASSWORDS_OVERFLOW_MENU("m_autofill_logins_import"),
AUTOFILL_IMPORT_PASSWORDS_GET_DESKTOP_BROWSER("m_autofill_logins_import_get_desktop"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_DELETE_LOGIN
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_ENABLE_AUTOFILL_TOGGLE_MANUALLY_DISABLED
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_ENABLE_AUTOFILL_TOGGLE_MANUALLY_ENABLED
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_IMPORT_GOOGLE_PASSWORDS_EMPTY_STATE_CTA_BUTTON_SHOWN
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_MANAGEMENT_SCREEN_OPENED
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_MANUALLY_SAVE_CREDENTIAL
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_NEVER_SAVE_FOR_THIS_SITE_CONFIRMATION_PROMPT_CONFIRMED
Expand Down Expand Up @@ -796,8 +797,7 @@ class AutofillSettingsViewModel @Inject constructor(
fun recordImportGooglePasswordButtonShown() {
if (!importGooglePasswordButtonShownPixelSent) {
importGooglePasswordButtonShownPixelSent = true

// pixel to show import button would fire here
pixel.fire(AUTOFILL_IMPORT_GOOGLE_PASSWORDS_EMPTY_STATE_CTA_BUTTON_SHOWN)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ import com.duckduckgo.autofill.impl.deviceauth.AutofillAuthorizationGracePeriod
import com.duckduckgo.autofill.impl.importing.CredentialImporter
import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePassword
import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePasswordResult
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_IMPORT_GOOGLE_PASSWORDS_PREIMPORT_PROMPT_CONFIRMED
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_IMPORT_GOOGLE_PASSWORDS_PREIMPORT_PROMPT_DISMISSED
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_IMPORT_GOOGLE_PASSWORDS_PREIMPORT_PROMPT_DISPLAYED
import com.duckduckgo.autofill.impl.ui.credential.dialog.animateClosed
import com.duckduckgo.autofill.impl.ui.credential.management.importpassword.google.ImportFromGooglePasswordsDialogViewModel.ViewMode.ImportError
import com.duckduckgo.autofill.impl.ui.credential.management.importpassword.google.ImportFromGooglePasswordsDialogViewModel.ViewMode.ImportSuccess
Expand Down Expand Up @@ -190,6 +193,8 @@ class ImportFromGooglePasswordsDialog : BottomSheetDialogFragment() {
container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
pixel.fire(AUTOFILL_IMPORT_GOOGLE_PASSWORDS_PREIMPORT_PROMPT_DISPLAYED)

_binding = ContentImportFromGooglePasswordDialogBinding.inflate(inflater, container, false)
configureViews(binding)
observeViewModel()
Expand Down Expand Up @@ -244,13 +249,18 @@ class ImportFromGooglePasswordsDialog : BottomSheetDialogFragment() {
ImportGooglePassword.AutofillImportViaGooglePasswordManagerScreen,
)
importGooglePasswordsFlowLauncher.launch(intent)

pixel.fire(AUTOFILL_IMPORT_GOOGLE_PASSWORDS_PREIMPORT_PROMPT_CONFIRMED)
}

override fun onCancel(dialog: DialogInterface) {
if (ignoreCancellationEvents) {
Timber.v("onCancel: Ignoring cancellation event")
return
}

pixel.fire(AUTOFILL_IMPORT_GOOGLE_PASSWORDS_PREIMPORT_PROMPT_DISMISSED)

dismiss()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,15 @@ package com.duckduckgo.autofill.impl.ui.credential.management.importpassword.goo
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.duckduckgo.anvil.annotations.ContributesViewModel
import com.duckduckgo.app.statistics.pixels.Pixel
import com.duckduckgo.autofill.impl.engagement.store.AutofillEngagementBucketing
import com.duckduckgo.autofill.impl.importing.CredentialImporter
import com.duckduckgo.autofill.impl.importing.CredentialImporter.ImportResult
import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePasswordsWebFlowViewModel.UserCannotImportReason
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_IMPORT_GOOGLE_PASSWORDS_RESULT_FAILURE_ERROR_ENCRYPTION
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_IMPORT_GOOGLE_PASSWORDS_RESULT_FAILURE_ERROR_PARSING
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_IMPORT_GOOGLE_PASSWORDS_RESULT_FAILURE_USER_CANCELLED
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_IMPORT_GOOGLE_PASSWORDS_RESULT_SUCCESS
import com.duckduckgo.autofill.impl.ui.credential.management.importpassword.google.ImportFromGooglePasswordsDialogViewModel.ViewMode.Importing
import com.duckduckgo.autofill.impl.ui.credential.management.importpassword.google.ImportFromGooglePasswordsDialogViewModel.ViewMode.PreImport
import com.duckduckgo.common.utils.DispatcherProvider
Expand All @@ -36,6 +42,8 @@ import timber.log.Timber
class ImportFromGooglePasswordsDialogViewModel @Inject constructor(
private val credentialImporter: CredentialImporter,
private val dispatchers: DispatcherProvider,
private val engagementBucketing: AutofillEngagementBucketing,
private val pixel: Pixel,
) : ViewModel() {

fun onImportFlowFinishedSuccessfully() {
Expand All @@ -54,17 +62,38 @@ class ImportFromGooglePasswordsDialogViewModel @Inject constructor(

is ImportResult.Finished -> {
Timber.d("Import finished: ${it.savedCredentials} imported. ${it.numberSkipped} skipped.")
fireImportSuccessPixel(savedCredentials = it.savedCredentials, numberSkipped = it.numberSkipped)
_viewState.value = ViewState(viewMode = ViewMode.ImportSuccess(it))
}
}
}
}

fun onImportFlowFinishedWithError(reason: UserCannotImportReason) {
fireImportFailedPixel(reason)
_viewState.value = ViewState(viewMode = ViewMode.ImportError)
}

fun onImportFlowCancelledByUser() {
pixel.fire(AUTOFILL_IMPORT_GOOGLE_PASSWORDS_RESULT_FAILURE_USER_CANCELLED)
}

private fun fireImportSuccessPixel(savedCredentials: Int, numberSkipped: Int) {
val savedCredentialsBucketed = engagementBucketing.bucketNumberOfCredentials(savedCredentials)
val skippedCredentialsBucketed = engagementBucketing.bucketNumberOfCredentials(numberSkipped)
val params = mapOf(
"saved_credentials" to savedCredentialsBucketed,
"skipped_credentials" to skippedCredentialsBucketed,
)
pixel.fire(AUTOFILL_IMPORT_GOOGLE_PASSWORDS_RESULT_SUCCESS, params)
}

private fun fireImportFailedPixel(reason: UserCannotImportReason) {
val pixelName = when (reason) {
UserCannotImportReason.ErrorParsingCsv -> AUTOFILL_IMPORT_GOOGLE_PASSWORDS_RESULT_FAILURE_ERROR_PARSING
UserCannotImportReason.EncryptedPassphrase -> AUTOFILL_IMPORT_GOOGLE_PASSWORDS_RESULT_FAILURE_ERROR_ENCRYPTION
}
pixel.fire(pixelName)
}

private val _viewState = MutableStateFlow(ViewState())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ class AutofillSurveyImpl @Inject constructor(
.appendQueryParameter(SurveyParams.MODEL, appBuildConfig.model)
.appendQueryParameter(SurveyParams.SOURCE, IN_APP)
.appendQueryParameter(SurveyParams.LAST_ACTIVE_DATE, appDaysUsedRepository.getLastActiveDay())
.appendQueryParameter(SurveyParams.NUMBER_PASSWORDS, passwordBucketing.bucketNumberOfSavedPasswords(passwordsSaved))
.appendQueryParameter(SurveyParams.NUMBER_PASSWORDS, passwordBucketing.bucketNumberOfCredentials(passwordsSaved))

urlBuilder.build().toString()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ import com.duckduckgo.autofill.impl.databinding.FragmentAutofillManagementListMo
import com.duckduckgo.autofill.impl.deviceauth.DeviceAuthenticator
import com.duckduckgo.autofill.impl.deviceauth.DeviceAuthenticator.AuthConfiguration
import com.duckduckgo.autofill.impl.deviceauth.DeviceAuthenticator.AuthResult.Success
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_IMPORT_GOOGLE_PASSWORDS_EMPTY_STATE_CTA_BUTTON_TAPPED
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_IMPORT_GOOGLE_PASSWORDS_OVERFLOW_MENU
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_SYNC_DESKTOP_PASSWORDS_CTA_BUTTON
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_SYNC_DESKTOP_PASSWORDS_OVERFLOW_MENU
import com.duckduckgo.autofill.impl.ui.credential.management.AutofillManagementActivity
Expand Down Expand Up @@ -246,6 +248,7 @@ class AutofillManagementListMode : DuckDuckGoFragment(R.layout.fragment_autofill
private fun configureImportPasswordsButton() {
binding.emptyStateLayout.importPasswordsFromGoogleButton.setOnClickListener {
viewModel.onImportPasswords()
pixel.fire(AUTOFILL_IMPORT_GOOGLE_PASSWORDS_EMPTY_STATE_CTA_BUTTON_TAPPED)
}

binding.emptyStateLayout.importPasswordsViaDesktopSyncButton.setOnClickListener {
Expand Down Expand Up @@ -299,6 +302,7 @@ class AutofillManagementListMode : DuckDuckGoFragment(R.layout.fragment_autofill

R.id.importGooglePasswords -> {
viewModel.onImportPasswords()
pixel.fire(AUTOFILL_IMPORT_GOOGLE_PASSWORDS_OVERFLOW_MENU)
true
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,31 +13,31 @@ class DefaultAutofillEngagementBucketingTest {

@Test
fun whenZeroSavedPasswordsThenBucketIsNone() {
assertEquals(NONE, testee.bucketNumberOfSavedPasswords(0))
assertEquals(NONE, testee.bucketNumberOfCredentials(0))
}

@Test
fun whenBetweenOneAndThreeSavedPasswordThenBucketIsFew() {
assertEquals(FEW, testee.bucketNumberOfSavedPasswords(1))
assertEquals(FEW, testee.bucketNumberOfSavedPasswords(2))
assertEquals(FEW, testee.bucketNumberOfSavedPasswords(3))
assertEquals(FEW, testee.bucketNumberOfCredentials(1))
assertEquals(FEW, testee.bucketNumberOfCredentials(2))
assertEquals(FEW, testee.bucketNumberOfCredentials(3))
}

@Test
fun whenBetweenFourAndTenSavedPasswordThenBucketIsSome() {
assertEquals(SOME, testee.bucketNumberOfSavedPasswords(4))
assertEquals(SOME, testee.bucketNumberOfSavedPasswords(10))
assertEquals(SOME, testee.bucketNumberOfCredentials(4))
assertEquals(SOME, testee.bucketNumberOfCredentials(10))
}

@Test
fun whenBetweenElevenAndFortyNineSavedPasswordThenBucketIsMany() {
assertEquals(MANY, testee.bucketNumberOfSavedPasswords(11))
assertEquals(MANY, testee.bucketNumberOfSavedPasswords(49))
assertEquals(MANY, testee.bucketNumberOfCredentials(11))
assertEquals(MANY, testee.bucketNumberOfCredentials(49))
}

@Test
fun whenFiftyOrOverThenBucketIsMany() {
assertEquals(LOTS, testee.bucketNumberOfSavedPasswords(50))
assertEquals(LOTS, testee.bucketNumberOfSavedPasswords(Int.MAX_VALUE))
assertEquals(LOTS, testee.bucketNumberOfCredentials(50))
assertEquals(LOTS, testee.bucketNumberOfCredentials(Int.MAX_VALUE))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package com.duckduckgo.autofill.impl.ui.credential.management.importpassword.goo

import app.cash.turbine.TurbineTestContext
import app.cash.turbine.test
import com.duckduckgo.app.statistics.pixels.Pixel
import com.duckduckgo.autofill.impl.engagement.store.AutofillEngagementBucketing
import com.duckduckgo.autofill.impl.importing.CredentialImporter
import com.duckduckgo.autofill.impl.importing.CredentialImporter.ImportResult.Finished
import com.duckduckgo.autofill.impl.importing.CredentialImporter.ImportResult.InProgress
Expand Down Expand Up @@ -29,10 +31,15 @@ class ImportFromGooglePasswordsDialogViewModelTest {
@get:Rule
val coroutineTestRule: CoroutineTestRule = CoroutineTestRule(StandardTestDispatcher())

private val engagementBucketing: AutofillEngagementBucketing = mock()
private val pixel: Pixel = mock()

private val credentialImporter: CredentialImporter = mock()
private val testee = ImportFromGooglePasswordsDialogViewModel(
credentialImporter = credentialImporter,
dispatchers = coroutineTestRule.testDispatcherProvider,
engagementBucketing = engagementBucketing,
pixel = pixel,
)

@Before
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ class AutofillSurveyImplTest {

@Test
fun whenSurveyLaunchedThenSavedPasswordQueryParamAdded() = runTest {
whenever(passwordBucketing.bucketNumberOfSavedPasswords(any())).thenReturn("fromBucketing")
whenever(passwordBucketing.bucketNumberOfCredentials(any())).thenReturn("fromBucketing")
val survey = getAvailableSurvey()
val savedPasswordsBucket = survey.url.toUri().getQueryParameter("saved_passwords")
assertEquals("fromBucketing", savedPasswordsBucket)
Expand Down

0 comments on commit ef5e18b

Please sign in to comment.