Skip to content

Commit abe79c5

Browse files
authored
Bookmark import flow observability (#7007)
Task/Issue URL: https://app.asana.com/1/137249556945/project/488551667048375/task/1211724585727142?focus=true ### Description Adds observability around bookmark import flow and pre-import dialog. ### Steps to test this PR Logcat filter: `package:mine message~:"Bookmark-import: |Pixel sent: bookmark_import_from_google"` #### Import flow (success) - [x] Fresh install, and visit `Saved Site Dev Settings` - [x] Tap on `Launch Google Takeout import flow` - [x] Verify you see `bookmark_import_from_google_flow_started` in the logs, params should match spec - [x] Successfully import, and verify you see `bookmark_import_from_google_flow_success`, params should match spec (e.g., correct duration buckets) #### Import flow (cancellation) - [x] Start the import again, and this time cancel. - [x] Verify you see `bookmark_import_from_google_flow_cancelled` in the logs and it includes the `stepReached` alongside durations. #### Import flow (simulate error) - [x] Apply patch from below - [x] Start the import flow, and wait 2 seconds - [x] Verify you see `bookmark_import_from_google_flow_error` and `errorReason` starts with `webView-` then has the step. (e.g., `webView-takeout-first`) #### Pre-import dialogs <img width="20%" height="283" alt="Screenshot 2025-10-28 at 13 07 14" src="https://github.com/user-attachments/assets/132acbe5-b189-4bea-a9eb-ed9d6e73df82" /> - [x] Visit Bookmarks activity, and tap on `Import` (from empty state or overflow; doesn't matter) - [x] Verify `bookmark_import_from_google_dialog_shown` appears - [x] Tap the ✖️ to cancel the dialog; verify `bookmark_import_from_google_dialog_dismissed` appears - [x] Tap to show it again, and this time use the back button to dismiss; verifying same log appears - [x] Tap to show it again, and this time tap outside of the dialog to dismiss; verifying same log appears - [x] Tap to show it again, and this time choose `Select Bookmarks File`; verify `bookmark_import_from_google_dialog_fromfile` appeas - [x] Finally, choose `Import From Google` button and verify `bookmark_import_from_google_dialog_startflow` ### Patch to force a crash during import ``` Index: autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/importing/takeout/webflow/ImportGoogleBookmarksWebFlowFragment.kt IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/importing/takeout/webflow/ImportGoogleBookmarksWebFlowFragment.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/importing/takeout/webflow/ImportGoogleBookmarksWebFlowFragment.kt --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/importing/takeout/webflow/ImportGoogleBookmarksWebFlowFragment.kt (revision Staged) +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/importing/takeout/webflow/ImportGoogleBookmarksWebFlowFragment.kt (date 1761651295473) @@ -76,6 +76,7 @@ import com.duckduckgo.common.utils.plugins.PluginPoint import com.duckduckgo.di.scopes.FragmentScope import com.duckduckgo.user.agent.api.UserAgentProvider +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch @@ -163,6 +164,13 @@ viewModel.firstPageLoading() } } + + // force a deliberate crash to test + lifecycleScope.launch { + delay(2000) + binding?.webView?.loadUrl("chrome://crash") + } + } private fun observeViewState() { ```
1 parent 121cb77 commit abe79c5

File tree

16 files changed

+877
-157
lines changed

16 files changed

+877
-157
lines changed

autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/importing/ImportFromGoogleImpl.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ class ImportFromGoogleImpl @Inject constructor(
4242
override suspend fun getBookmarksImportLaunchIntent(): Intent? {
4343
return withContext(dispatchers.io()) {
4444
if (autofillFeature.canImportBookmarksFromGoogleTakeout().isEnabled()) {
45-
globalActivityStarter.startIntent(context, ImportBookmarksViaGoogleTakeoutScreen)
45+
val launchSource = getLaunchSource()
46+
globalActivityStarter.startIntent(context, ImportBookmarksViaGoogleTakeoutScreen(launchSource))
4647
} else {
4748
null
4849
}
@@ -69,4 +70,9 @@ class ImportFromGoogleImpl @Inject constructor(
6970
}
7071
}
7172
}
73+
74+
private fun getLaunchSource(): String {
75+
// launchSource can only be bookmarks screen or dev settings currently, so hardcoding to bookmarks screen for now
76+
return "bookmarks_screen"
77+
}
7278
}

autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/importing/takeout/webflow/ImportGoogleBookmarkResult.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,5 +49,5 @@ sealed interface UserCannotImportReason : Parcelable {
4949
data object Unknown : UserCannotImportReason
5050

5151
@Parcelize
52-
data object WebViewError : UserCannotImportReason
52+
data class WebViewError(val step: String) : UserCannotImportReason
5353
}

autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/importing/takeout/webflow/ImportGoogleBookmarksWebFlowActivity.kt

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,28 +27,32 @@ import com.duckduckgo.appbuildconfig.api.AppBuildConfig
2727
import com.duckduckgo.autofill.impl.R
2828
import com.duckduckgo.autofill.impl.databinding.ActivityImportGoogleBookmarksWebflowBinding
2929
import com.duckduckgo.autofill.impl.importing.takeout.webflow.UserCannotImportReason.Unknown
30+
import com.duckduckgo.autofill.impl.importing.takeout.webflow.journey.ImportGoogleBookmarksJourney
3031
import com.duckduckgo.common.ui.DuckDuckGoActivity
3132
import com.duckduckgo.common.ui.viewbinding.viewBinding
3233
import com.duckduckgo.di.scopes.ActivityScope
3334
import com.duckduckgo.navigation.api.GlobalActivityStarter
35+
import com.duckduckgo.navigation.api.getActivityParams
3436
import logcat.logcat
3537
import javax.inject.Inject
3638

3739
/**
3840
* Launch the Google Bookmarks import flow
3941
*/
40-
data object ImportBookmarksViaGoogleTakeoutScreen : GlobalActivityStarter.ActivityParams {
41-
private fun readResolve(): Any = ImportBookmarksViaGoogleTakeoutScreen
42-
}
42+
data class ImportBookmarksViaGoogleTakeoutScreen(val launchSource: String) : GlobalActivityStarter.ActivityParams
4343

4444
@InjectWith(ActivityScope::class)
4545
@ContributeToActivityStarter(ImportBookmarksViaGoogleTakeoutScreen::class)
4646
class ImportGoogleBookmarksWebFlowActivity :
4747
DuckDuckGoActivity(),
4848
ImportGoogleBookmarksWebFlowFragment.WebViewVisibilityListener {
49+
4950
@Inject
5051
lateinit var appBuildConfig: AppBuildConfig
5152

53+
@Inject
54+
lateinit var importJourney: ImportGoogleBookmarksJourney
55+
5256
val binding: ActivityImportGoogleBookmarksWebflowBinding by viewBinding()
5357

5458
private var isOverlayCurrentlyShown = false
@@ -60,14 +64,16 @@ class ImportGoogleBookmarksWebFlowActivity :
6064
configureToolbar()
6165
configureResultListeners()
6266
launchWebFlow()
67+
if (savedInstanceState == null) {
68+
importJourney.started(getLaunchSource())
69+
}
6370
}
6471

6572
private fun launchWebFlow() {
6673
logcat { "Bookmark-import: Starting webflow" }
6774
isOnResultScreen = false
6875
replaceFragment(ImportGoogleBookmarksWebFlowFragment())
6976
updateToolbarTitle()
70-
invalidateOptionsMenu()
7177
}
7278

7379
private fun showSuccessFragment(bookmarkCount: Int) {
@@ -82,7 +88,6 @@ class ImportGoogleBookmarksWebFlowActivity :
8288
isOverlayCurrentlyShown = false
8389
isOnResultScreen = true
8490
updateToolbarTitle()
85-
invalidateOptionsMenu()
8691
}
8792

8893
private fun showErrorFragment(errorReason: UserCannotImportReason) {
@@ -99,7 +104,6 @@ class ImportGoogleBookmarksWebFlowActivity :
99104
isOverlayCurrentlyShown = false
100105
isOnResultScreen = true
101106
updateToolbarTitle()
102-
invalidateOptionsMenu()
103107
}
104108

105109
private fun replaceFragment(fragment: Fragment) {
@@ -149,16 +153,19 @@ class ImportGoogleBookmarksWebFlowActivity :
149153
when (result) {
150154
is ImportGoogleBookmarkResult.Success -> {
151155
logcat { "Bookmark-import: ${javaClass.simpleName}, WebFlow succeeded with ${result.importedCount} bookmarks" }
156+
importJourney.finishedWithSuccess()
152157
showSuccessFragment(result.importedCount)
153158
}
154159

155160
is ImportGoogleBookmarkResult.UserCancelled -> {
156161
logcat { "Bookmark-import: ${javaClass.simpleName}, User cancelled at ${result.stage}" }
162+
importJourney.cancelled(result.stage)
157163
exitUserCancelled(result.stage)
158164
}
159165

160166
is ImportGoogleBookmarkResult.Error -> {
161167
logcat { "Bookmark-import: ${javaClass.simpleName}, Import failed with reason: ${result.reason}" }
168+
importJourney.finishedWithError(error = result.reason)
162169
showErrorFragment(result.reason)
163170
}
164171

@@ -223,14 +230,21 @@ class ImportGoogleBookmarksWebFlowActivity :
223230
}
224231

225232
override fun showLoadingState() {
233+
importJourney.startedWaitingForExport()
226234
showProgressOverlay()
227235
}
228236

229237
override fun hideLoadingState() {
238+
importJourney.stoppedWaitingForExport()
230239
hideProgressOverlay()
231240
}
232241

242+
private fun getLaunchSource(): String {
243+
return intent.getActivityParams(ImportBookmarksViaGoogleTakeoutScreen::class.java)?.launchSource ?: UNKNOWN_LAUNCH_SOURCE
244+
}
245+
233246
companion object {
234247
private const val PROGRESS_OVERLAY_TAG = "progress_overlay"
248+
private const val UNKNOWN_LAUNCH_SOURCE = "unknown"
235249
}
236250
}

autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/importing/takeout/webflow/ImportGoogleBookmarksWebFlowFragment.kt

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,6 @@ import com.duckduckgo.autofill.impl.importing.takeout.webflow.ImportGoogleBookma
6363
import com.duckduckgo.autofill.impl.importing.takeout.webflow.ImportGoogleBookmarksWebFlowViewModel.ViewState.ShowWebPage
6464
import com.duckduckgo.autofill.impl.importing.takeout.webflow.ImportGoogleBookmarksWebFlowViewModel.ViewState.UserCancelledImportFlow
6565
import com.duckduckgo.autofill.impl.importing.takeout.webflow.ImportGoogleBookmarksWebFlowViewModel.ViewState.UserFinishedCannotImport
66-
import com.duckduckgo.autofill.impl.importing.takeout.webflow.UserCannotImportReason.DownloadError
67-
import com.duckduckgo.autofill.impl.importing.takeout.webflow.UserCannotImportReason.ErrorParsingBookmarks
68-
import com.duckduckgo.autofill.impl.importing.takeout.webflow.UserCannotImportReason.Unknown
69-
import com.duckduckgo.autofill.impl.importing.takeout.webflow.UserCannotImportReason.WebAutomationError
70-
import com.duckduckgo.autofill.impl.importing.takeout.webflow.UserCannotImportReason.WebViewError
7166
import com.duckduckgo.autofill.impl.jsbridge.request.SupportedAutofillInputSubType
7267
import com.duckduckgo.autofill.impl.jsbridge.request.SupportedAutofillInputSubType.PASSWORD
7368
import com.duckduckgo.autofill.impl.store.ReAuthenticationDetails
@@ -388,7 +383,7 @@ class ImportGoogleBookmarksWebFlowFragment :
388383

389384
override fun onFatalWebViewError() {
390385
logcat(WARN) { "Bookmark-import: Fatal WebView error received" }
391-
exitFlowAsError(WebViewError)
386+
viewModel.onFatalWebViewError()
392387
}
393388

394389
private fun configureBackButtonHandler() {
@@ -435,7 +430,7 @@ class ImportGoogleBookmarksWebFlowFragment :
435430
}
436431

437432
private fun exitFlowAsError(reason: UserCannotImportReason) {
438-
logcat { "Bookmark-import: Flow error at stage: ${reason.mapToStage()}" }
433+
logcat { "Bookmark-import: Flow error at stage: $reason" }
439434
onWebFlowEnding()
440435

441436
lifecycleScope.launch {
@@ -576,12 +571,3 @@ class ImportGoogleBookmarksWebFlowFragment :
576571
private const val SELECT_CREDENTIALS_FRAGMENT_TAG = "autofillSelectCredentialsDialog"
577572
}
578573
}
579-
580-
private fun UserCannotImportReason.mapToStage(): String =
581-
when (this) {
582-
is DownloadError -> "zip-download-error"
583-
is ErrorParsingBookmarks -> "zip-parse-error"
584-
is Unknown -> "import-error-unknown"
585-
is WebViewError -> "webview-error"
586-
is WebAutomationError -> "web-automation-step-failure-${this.step}"
587-
}

autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/importing/takeout/webflow/ImportGoogleBookmarksWebFlowViewModel.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,13 @@ class ImportGoogleBookmarksWebFlowViewModel @Inject constructor(
260260
}
261261
}
262262

263+
fun onFatalWebViewError() {
264+
viewModelScope.launch {
265+
val stage = webFlowStepObserver.getCurrentStep()
266+
_commands.emit(ExitFlowAsFailure(UserCannotImportReason.WebViewError(stage)))
267+
}
268+
}
269+
263270
companion object {
264271
private const val TAKEOUT_ADDRESS = "takeout.google.com"
265272
private const val ACCOUNTS_ADDRESS = "accounts.google.com"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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.autofill.impl.importing.takeout.webflow.journey
18+
19+
import com.duckduckgo.di.scopes.AppScope
20+
import com.squareup.anvil.annotations.ContributesBinding
21+
import java.util.concurrent.TimeUnit
22+
import javax.inject.Inject
23+
24+
interface ImportGoogleBookmarksDurationBucketing {
25+
fun bucket(durationMillis: Long): String
26+
}
27+
28+
@ContributesBinding(AppScope::class)
29+
class RealImportGoogleBookmarksDurationBucketing @Inject constructor() : ImportGoogleBookmarksDurationBucketing {
30+
31+
override fun bucket(durationMillis: Long): String {
32+
if (durationMillis < 0) return "negative"
33+
val seconds = TimeUnit.MILLISECONDS.toSeconds(durationMillis)
34+
35+
return when (seconds) {
36+
in 0..19 -> "20"
37+
in 20..39 -> "40"
38+
in 40..59 -> "60"
39+
in 60..89 -> "90"
40+
in 90..119 -> "120"
41+
in 120..149 -> "150"
42+
in 150..179 -> "180"
43+
in 180..239 -> "240"
44+
in 240..299 -> "300"
45+
else -> "longer"
46+
}
47+
}
48+
}

0 commit comments

Comments
 (0)