Skip to content

Commit 05c100a

Browse files
authored
Improve password observability (#6618)
Task/Issue URL: https://app.asana.com/1/137249556945/project/488551667048375/task/1209004824659117?focus=true ### Description Adds the same `last_used` concept we have for Email Protection for passwords, too. ### Steps to test this PR Add logcat filter: `message~:"Pixel url request:.*m_autofill_logins_fill_login_inline_autoprompt_confirmed|Pixel url request:.*m_autofill_logins_fill_login_inline_manual_confirmed|autofilled today|upserting|Pixel sent: m_autofill_activeuser|Pixel ignored:m_autofill"` - [x] Follow steps in [testing steps](https://app.asana.com/1/137249556945/project/608920331025315/task/1211089760574191?focus=true) Co-authored-by: Craig Russell <[email protected]>
1 parent 9f19ec0 commit 05c100a

File tree

5 files changed

+104
-7
lines changed

5 files changed

+104
-7
lines changed

autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/engagement/store/AutofillEngagementRepository.kt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,11 @@ import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_SERVICE_DI
2626
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_SERVICE_ENABLED_DAU
2727
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_TOGGLED_OFF_SEARCH
2828
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_TOGGLED_ON_SEARCH
29+
import com.duckduckgo.autofill.impl.pixel.AutofillPixelParameters.LAST_USED_PIXEL_KEY
2930
import com.duckduckgo.autofill.impl.securestorage.SecureStorage
3031
import com.duckduckgo.autofill.impl.service.store.AutofillServiceStore
3132
import com.duckduckgo.autofill.impl.store.InternalAutofillStore
33+
import com.duckduckgo.autofill.store.AutofillPrefsStore
3234
import com.duckduckgo.autofill.store.engagement.AutofillEngagementDao
3335
import com.duckduckgo.autofill.store.engagement.AutofillEngagementDatabase
3436
import com.duckduckgo.autofill.store.engagement.AutofillEngagementEntity
@@ -73,6 +75,7 @@ class DefaultAutofillEngagementRepository @Inject constructor(
7375
private val secureStorage: SecureStorage,
7476
private val deviceAuthenticator: DeviceAuthenticator,
7577
private val autofillServiceStore: AutofillServiceStore,
78+
private val autofillPrefsStore: AutofillPrefsStore,
7679
) : AutofillEngagementRepository {
7780

7881
private val autofillEngagementDao: AutofillEngagementDao = engagementDb.autofillEngagementDao()
@@ -82,6 +85,7 @@ class DefaultAutofillEngagementRepository @Inject constructor(
8285
val engagement = todaysEngagement().copy(autofilled = true)
8386
logcat(VERBOSE) { "upserting $engagement because user autofilled" }
8487
processEvent(engagement)
88+
autofillPrefsStore.dataLastAutofilledDate = engagement.date
8589
}
8690
}
8791

@@ -121,9 +125,13 @@ class DefaultAutofillEngagementRepository @Inject constructor(
121125

122126
private suspend fun AutofillEngagementEntity.sendPixelsIfCriteriaMet() {
123127
val numberStoredPasswords = getNumberStoredPasswords()
128+
val lastUsed = autofillPrefsStore.dataLastAutofilledDate
124129

125130
if (autofilled && searched) {
126-
pixel.fire(AUTOFILL_ENGAGEMENT_ACTIVE_USER, type = Daily())
131+
logcat { "User autofilled and searched today, sending engagement pixels. lastAutofilled=$lastUsed" }
132+
133+
val activeUserParams = if (lastUsed == null) emptyMap() else mapOf(LAST_USED_PIXEL_KEY to lastUsed)
134+
pixel.fire(AUTOFILL_ENGAGEMENT_ACTIVE_USER, parameters = activeUserParams, type = Daily())
127135

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

autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/pixel/AutofillPixelNames.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_ONBOARDING
4040
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_ONBOARDING_SAVE_PROMPT_EXCLUDE
4141
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_ONBOARDING_SAVE_PROMPT_SAVED
4242
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_ONBOARDING_SAVE_PROMPT_SHOWN
43+
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_SELECT_LOGIN_AUTOPROMPT_SELECTED
44+
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_SELECT_LOGIN_PROMPT_SELECTED
4345
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_SERVICE_CRASH
4446
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_SERVICE_DISABLED
4547
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_SERVICE_DISABLED_DAU
@@ -206,6 +208,10 @@ enum class AutofillPixelNames(override val pixelName: String) : Pixel.PixelName
206208
AUTOFILL_NEVER_SAVE_FOR_THIS_SITE_OVERFLOW_MENU("autofill_reset_excluded_overflow_menu_tapped"),
207209
}
208210

211+
object AutofillPixelParameters {
212+
const val LAST_USED_PIXEL_KEY = "last_used"
213+
}
214+
209215
@ContributesMultibinding(
210216
scope = AppScope::class,
211217
boundType = PixelParamRemovalPlugin::class,
@@ -236,6 +242,9 @@ object AutofillPixelsRequiringDataCleaning : PixelParamRemovalPlugin {
236242
AUTOFILL_IMPORT_PASSWORDS_USER_JOURNEY_STARTED.pixelName to PixelParameter.removeAtb(),
237243
AUTOFILL_IMPORT_PASSWORDS_USER_JOURNEY_RESTARTED.pixelName to PixelParameter.removeAtb(),
238244

245+
AUTOFILL_SELECT_LOGIN_AUTOPROMPT_SELECTED.pixelName to PixelParameter.removeAtb(),
246+
AUTOFILL_SELECT_LOGIN_PROMPT_SELECTED.pixelName to PixelParameter.removeAtb(),
247+
239248
AUTOFILL_SITE_BREAKAGE_REPORT.pixelName to PixelParameter.removeAtb(),
240249
AUTOFILL_SITE_BREAKAGE_REPORT.pixelName to PixelParameter.removeAtb(),
241250
AUTOFILL_SITE_BREAKAGE_REPORT.pixelName to PixelParameter.removeAtb(),

autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/selecting/AutofillSelectCredentialsDialogFragment.kt

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import android.view.ViewGroup
2525
import androidx.fragment.app.setFragmentResult
2626
import com.duckduckgo.anvil.annotations.InjectWith
2727
import com.duckduckgo.app.browser.favicon.FaviconManager
28+
import com.duckduckgo.app.di.AppCoroutineScope
2829
import com.duckduckgo.app.statistics.pixels.Pixel
2930
import com.duckduckgo.autofill.api.CredentialAutofillPickerDialog
3031
import com.duckduckgo.autofill.api.domain.app.LoginCredentials
@@ -39,17 +40,22 @@ import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_SELECT_LOG
3940
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_SELECT_LOGIN_PROMPT_DISMISSED
4041
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_SELECT_LOGIN_PROMPT_SELECTED
4142
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_SELECT_LOGIN_PROMPT_SHOWN
43+
import com.duckduckgo.autofill.impl.pixel.AutofillPixelParameters.LAST_USED_PIXEL_KEY
4244
import com.duckduckgo.autofill.impl.ui.credential.dialog.animateClosed
4345
import com.duckduckgo.autofill.impl.ui.credential.selecting.AutofillSelectCredentialsDialogFragment.DialogEvent.Dismissed
4446
import com.duckduckgo.autofill.impl.ui.credential.selecting.AutofillSelectCredentialsDialogFragment.DialogEvent.Selected
4547
import com.duckduckgo.autofill.impl.ui.credential.selecting.AutofillSelectCredentialsDialogFragment.DialogEvent.Shown
4648
import com.duckduckgo.autofill.impl.ui.credential.selecting.CredentialsPickerRecyclerAdapter.ListItem
49+
import com.duckduckgo.autofill.store.AutofillPrefsStore
50+
import com.duckduckgo.common.utils.DispatcherProvider
4751
import com.duckduckgo.di.scopes.FragmentScope
4852
import com.google.android.material.bottomsheet.BottomSheetBehavior
4953
import com.google.android.material.bottomsheet.BottomSheetDialog
5054
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
5155
import dagger.android.support.AndroidSupportInjection
5256
import javax.inject.Inject
57+
import kotlinx.coroutines.CoroutineScope
58+
import kotlinx.coroutines.launch
5359
import logcat.LogPriority.VERBOSE
5460
import logcat.logcat
5561

@@ -76,6 +82,16 @@ class AutofillSelectCredentialsDialogFragment : BottomSheetDialogFragment(), Cre
7682
@Inject
7783
lateinit var autofillSelectCredentialsListBuilder: AutofillSelectCredentialsListBuilder
7884

85+
@Inject
86+
@AppCoroutineScope
87+
lateinit var appCoroutineScope: CoroutineScope
88+
89+
@Inject
90+
lateinit var dispatchers: DispatcherProvider
91+
92+
@Inject
93+
lateinit var autofillPrefsStore: AutofillPrefsStore
94+
7995
override fun onAttach(context: Context) {
8096
AndroidSupportInjection.inject(this)
8197
super.onAttach(context)
@@ -128,7 +144,13 @@ class AutofillSelectCredentialsDialogFragment : BottomSheetDialogFragment(), Cre
128144
listItems = credentials,
129145
) { selectedCredentials ->
130146

131-
pixelNameDialogEvent(Selected)?.let { pixel.fire(it) }
147+
pixelNameDialogEvent(Selected)?.let {
148+
appCoroutineScope.launch(dispatchers.io()) {
149+
val lastUsed = autofillPrefsStore.dataLastAutofilledDate
150+
val params = if (lastUsed == null) emptyMap() else mapOf(LAST_USED_PIXEL_KEY to lastUsed)
151+
pixel.fire(it, parameters = params)
152+
}
153+
}
132154

133155
val result = Bundle().also {
134156
it.putBoolean(CredentialAutofillPickerDialog.KEY_CANCELLED, false)

autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/store/AutofillPrefsStore.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ interface AutofillPrefsStore {
4242
var timestampUserLastPromptedToDisableAutofill: Long?
4343
var domainTargetDatasetVersion: Long
4444
var inBrowserImportPromoShownCount: Int
45+
var dataLastAutofilledDate: String?
4546

4647
/**
4748
* Returns if Autofill was enabled by default.
@@ -198,6 +199,12 @@ class RealAutofillPrefsStore(
198199
}
199200
}
200201

202+
override var dataLastAutofilledDate: String?
203+
get() = prefs.getString(DATA_LAST_AUTOFILLED_DATE, null)
204+
set(value) {
205+
prefs.edit { putString(DATA_LAST_AUTOFILLED_DATE, value) }
206+
}
207+
201208
companion object {
202209
const val FILENAME = "com.duckduckgo.autofill.store.autofill_store"
203210
const val AUTOFILL_ENABLED = "autofill_enabled"
@@ -212,5 +219,6 @@ class RealAutofillPrefsStore(
212219
const val MONITOR_AUTOFILL_DECLINES = "monitor_autofill_declines"
213220
const val ORIGINAL_AUTOFILL_DEFAULT_STATE_ENABLED = "original_autofill_default_state_enabled"
214221
const val DOMAIN_TARGET_DATASET_VERSION = "domain_target_dataset_version"
222+
const val DATA_LAST_AUTOFILLED_DATE = "data_last_autofilled_date"
215223
}
216224
}

autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/engagement/store/DefaultAutofillEngagementRepositoryTest.kt

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,26 @@ import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_SERVICE_DI
1515
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_SERVICE_ENABLED_DAU
1616
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_TOGGLED_OFF_SEARCH
1717
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_TOGGLED_ON_SEARCH
18+
import com.duckduckgo.autofill.impl.pixel.AutofillPixelParameters.LAST_USED_PIXEL_KEY
1819
import com.duckduckgo.autofill.impl.securestorage.SecureStorage
1920
import com.duckduckgo.autofill.impl.service.store.AutofillServiceStore
2021
import com.duckduckgo.autofill.impl.store.InternalAutofillStore
22+
import com.duckduckgo.autofill.store.AutofillPrefsStore
2123
import com.duckduckgo.autofill.store.engagement.AutofillEngagementDatabase
2224
import com.duckduckgo.common.test.CoroutineTestRule
25+
import java.time.format.DateTimeFormatter
2326
import kotlinx.coroutines.flow.flowOf
2427
import kotlinx.coroutines.test.runTest
28+
import org.junit.Assert.assertEquals
2529
import org.junit.Assert.assertFalse
2630
import org.junit.Assert.assertTrue
2731
import org.junit.Before
2832
import org.junit.Rule
2933
import org.junit.Test
3034
import org.junit.runner.RunWith
35+
import org.mockito.kotlin.eq
3136
import org.mockito.kotlin.mock
37+
import org.mockito.kotlin.verify
3238
import org.mockito.kotlin.whenever
3339

3440
@RunWith(AndroidJUnit4::class)
@@ -48,6 +54,7 @@ class DefaultAutofillEngagementRepositoryTest {
4854
private val autofillServiceStore: AutofillServiceStore = FakeAutofillServiceStore()
4955
private val secureStorage: SecureStorage = mock()
5056
private val deviceAuthenticator: DeviceAuthenticator = mock()
57+
private val autofillPrefsStore: AutofillPrefsStore = mock()
5158

5259
@Before
5360
fun setup() {
@@ -67,8 +74,15 @@ class DefaultAutofillEngagementRepositoryTest {
6774
secureStorage = secureStorage,
6875
deviceAuthenticator = deviceAuthenticator,
6976
autofillServiceStore = autofillServiceStore,
77+
autofillPrefsStore = autofillPrefsStore,
7078
)
7179

80+
@Test
81+
fun whenAutofilledThenLastUsedDateIsUpdated() = runTest {
82+
testee.recordAutofilledToday()
83+
verify(autofillPrefsStore).dataLastAutofilledDate = eq(todayString())
84+
}
85+
7286
@Test
7387
fun whenAutofilledButNotSearchedThenActiveUserPixelNotSent() = runTest {
7488
testee.recordAutofilledToday()
@@ -88,6 +102,28 @@ class DefaultAutofillEngagementRepositoryTest {
88102
AUTOFILL_ENGAGEMENT_ACTIVE_USER.verifySent()
89103
}
90104

105+
@Test
106+
fun whenActiveUserPixelSentWithNoLastUsedThenOmittedFromParameters() = runTest {
107+
whenever(autofillPrefsStore.dataLastAutofilledDate).thenReturn(null)
108+
testee.recordSearchedToday()
109+
testee.recordAutofilledToday()
110+
111+
val pixelParams = AUTOFILL_ENGAGEMENT_ACTIVE_USER.getParametersForFiredPixel()!!
112+
assertFalse(pixelParams.contains(LAST_USED_PIXEL_KEY))
113+
}
114+
115+
@Test
116+
fun whenActiveUserPixelSentWithLastUsedThenIncludedInParameters() = runTest {
117+
val lastUsedDate = yesterdayString()
118+
whenever(autofillPrefsStore.dataLastAutofilledDate).thenReturn(lastUsedDate)
119+
120+
testee.recordSearchedToday()
121+
testee.recordAutofilledToday()
122+
123+
val pixelParams = AUTOFILL_ENGAGEMENT_ACTIVE_USER.getParametersForFiredPixel()!!
124+
assertEquals(lastUsedDate, pixelParams[LAST_USED_PIXEL_KEY])
125+
}
126+
91127
@Test
92128
fun whenSearchedAutofillEnabledAndMoreThan9PasswordsThenEnabledUserPixelSent() = runTest {
93129
givenUserHasPasswords(10)
@@ -175,17 +211,31 @@ class DefaultAutofillEngagementRepositoryTest {
175211
assertFalse(pixel.firedPixels.contains(pixelName))
176212
}
177213

214+
private fun AutofillPixelNames.getParametersForFiredPixel(): Map<String, String>? = pixel.firedPixels[this.pixelName]
215+
216+
private fun yesterdayString(): String {
217+
return DATE_FORMATTER.format(java.time.LocalDate.now().minusDays(1))
218+
}
219+
220+
private fun todayString(): String {
221+
return DATE_FORMATTER.format(java.time.LocalDate.now())
222+
}
223+
224+
companion object {
225+
private val DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd")
226+
}
227+
178228
private class FakePixel : Pixel {
179229

180-
val firedPixels = mutableListOf<String>()
230+
val firedPixels = mutableMapOf<String, Map<String, String>>()
181231

182232
override fun fire(
183233
pixel: PixelName,
184234
parameters: Map<String, String>,
185235
encodedParameters: Map<String, String>,
186236
type: PixelType,
187237
) {
188-
firedPixels.add(pixel.pixelName)
238+
firedPixels[pixel.pixelName] = parameters
189239
}
190240

191241
override fun fire(
@@ -194,23 +244,23 @@ class DefaultAutofillEngagementRepositoryTest {
194244
encodedParameters: Map<String, String>,
195245
type: PixelType,
196246
) {
197-
firedPixels.add(pixelName)
247+
firedPixels[pixelName] = parameters
198248
}
199249

200250
override fun enqueueFire(
201251
pixel: PixelName,
202252
parameters: Map<String, String>,
203253
encodedParameters: Map<String, String>,
204254
) {
205-
firedPixels.add(pixel.pixelName)
255+
firedPixels[pixel.pixelName] = parameters
206256
}
207257

208258
override fun enqueueFire(
209259
pixelName: String,
210260
parameters: Map<String, String>,
211261
encodedParameters: Map<String, String>,
212262
) {
213-
firedPixels.add(pixelName)
263+
firedPixels[pixelName] = parameters
214264
}
215265
}
216266
}

0 commit comments

Comments
 (0)