diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt index b1fb9e4eb357..af53fa5d3c3a 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt @@ -609,7 +609,7 @@ open class BrowserActivity : DuckDuckGoActivity() { is Command.OpenInNewTab -> launchNewTab(command.url) is Command.OpenSavedSite -> currentTab?.submitQuery(command.url) is Command.ShowSetAsDefaultBrowserDialog -> showSetAsDefaultBrowserDialog() - is Command.HideSetAsDefaultBrowserDialog -> hideSetAsDefaultBrowserDialog() + is Command.DismissSetAsDefaultBrowserDialog -> dismissSetAsDefaultBrowserDialog() is ShowSystemDefaultAppsActivity -> showSystemDefaultAppsActivity(command.intent) is ShowSystemDefaultBrowserDialog -> showSystemDefaultBrowserDialog(command.intent) } @@ -985,8 +985,8 @@ open class BrowserActivity : DuckDuckGoActivity() { viewModel.onSetDefaultBrowserDialogShown() } - override fun onDismissed() { - viewModel.onSetDefaultBrowserDismissed() + override fun onCanceled() { + viewModel.onSetDefaultBrowserDialogCanceled() } override fun onSetBrowserButtonClicked() { @@ -1001,7 +1001,7 @@ open class BrowserActivity : DuckDuckGoActivity() { setAsDefaultBrowserDialog = dialog } - private fun hideSetAsDefaultBrowserDialog() { + private fun dismissSetAsDefaultBrowserDialog() { setAsDefaultBrowserDialog?.dismiss() setAsDefaultBrowserDialog = null } @@ -1009,7 +1009,6 @@ open class BrowserActivity : DuckDuckGoActivity() { private fun showSystemDefaultAppsActivity(intent: Intent) { try { startDefaultAppsSystemActivityForResult.launch(intent) - viewModel.onSystemDefaultAppsActivityOpened() } catch (ex: Exception) { Timber.e(ex) } diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt index 97631f5482ee..1c60d24b4e9b 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt @@ -24,12 +24,13 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.duckduckgo.anvil.annotations.ContributesRemoteFeature import com.duckduckgo.anvil.annotations.ContributesViewModel -import com.duckduckgo.app.browser.BrowserViewModel.Command.HideSetAsDefaultBrowserDialog +import com.duckduckgo.app.browser.BrowserViewModel.Command.DismissSetAsDefaultBrowserDialog import com.duckduckgo.app.browser.defaultbrowsing.DefaultBrowserDetector import com.duckduckgo.app.browser.defaultbrowsing.prompts.DefaultBrowserPromptsExperiment import com.duckduckgo.app.browser.defaultbrowsing.prompts.DefaultBrowserPromptsExperiment.Command.OpenMessageDialog import com.duckduckgo.app.browser.defaultbrowsing.prompts.DefaultBrowserPromptsExperiment.Command.OpenSystemDefaultAppsActivity import com.duckduckgo.app.browser.defaultbrowsing.prompts.DefaultBrowserPromptsExperiment.Command.OpenSystemDefaultBrowserDialog +import com.duckduckgo.app.browser.defaultbrowsing.prompts.DefaultBrowserPromptsExperiment.SetAsDefaultActionTrigger import com.duckduckgo.app.browser.omnibar.OmnibarEntryConverter import com.duckduckgo.app.fire.DataClearer import com.duckduckgo.app.generalsettings.showonapplaunch.ShowOnAppLaunchFeature @@ -115,7 +116,7 @@ class BrowserViewModel @Inject constructor( data class OpenInNewTab(val url: String) : Command() data class OpenSavedSite(val url: String) : Command() data object ShowSetAsDefaultBrowserDialog : Command() - data object HideSetAsDefaultBrowserDialog : Command() + data object DismissSetAsDefaultBrowserDialog : Command() data class ShowSystemDefaultBrowserDialog(val intent: Intent) : Command() data class ShowSystemDefaultAppsActivity(val intent: Intent) : Command() } @@ -173,6 +174,9 @@ class BrowserViewModel @Inject constructor( } } + private var lastSystemDefaultAppsTrigger: SetAsDefaultActionTrigger = SetAsDefaultActionTrigger.UNKNOWN + private var lastSystemDefaultBrowserDialogTrigger: SetAsDefaultActionTrigger = SetAsDefaultActionTrigger.UNKNOWN + init { appEnjoymentPromptEmitter.promptType.observeForever(appEnjoymentObserver) viewModelScope.launch { @@ -183,10 +187,12 @@ class BrowserViewModel @Inject constructor( } is OpenSystemDefaultAppsActivity -> { + lastSystemDefaultAppsTrigger = it.trigger command.value = Command.ShowSystemDefaultAppsActivity(it.intent) } is OpenSystemDefaultBrowserDialog -> { + lastSystemDefaultBrowserDialogTrigger = it.trigger command.value = Command.ShowSystemDefaultBrowserDialog(it.intent) } } @@ -381,17 +387,17 @@ class BrowserViewModel @Inject constructor( defaultBrowserPromptsExperiment.onMessageDialogShown() } - fun onSetDefaultBrowserDismissed() { - defaultBrowserPromptsExperiment.onMessageDialogDismissed() + fun onSetDefaultBrowserDialogCanceled() { + defaultBrowserPromptsExperiment.onMessageDialogCanceled() } fun onSetDefaultBrowserConfirmationButtonClicked() { - command.value = HideSetAsDefaultBrowserDialog + command.value = DismissSetAsDefaultBrowserDialog defaultBrowserPromptsExperiment.onMessageDialogConfirmationButtonClicked() } fun onSetDefaultBrowserNotNowButtonClicked() { - command.value = HideSetAsDefaultBrowserDialog + command.value = DismissSetAsDefaultBrowserDialog defaultBrowserPromptsExperiment.onMessageDialogNotNowButtonClicked() } @@ -400,19 +406,15 @@ class BrowserViewModel @Inject constructor( } fun onSystemDefaultBrowserDialogSuccess() { - defaultBrowserPromptsExperiment.onSystemDefaultBrowserDialogSuccess() + defaultBrowserPromptsExperiment.onSystemDefaultBrowserDialogSuccess(lastSystemDefaultBrowserDialogTrigger) } fun onSystemDefaultBrowserDialogCanceled() { - defaultBrowserPromptsExperiment.onSystemDefaultBrowserDialogCanceled() - } - - fun onSystemDefaultAppsActivityOpened() { - defaultBrowserPromptsExperiment.onSystemDefaultAppsActivityOpened() + defaultBrowserPromptsExperiment.onSystemDefaultBrowserDialogCanceled(lastSystemDefaultBrowserDialogTrigger) } fun onSystemDefaultAppsActivityClosed() { - defaultBrowserPromptsExperiment.onSystemDefaultAppsActivityClosed() + defaultBrowserPromptsExperiment.onSystemDefaultAppsActivityClosed(lastSystemDefaultAppsTrigger) } fun onTabsSwiped() { diff --git a/app/src/main/java/com/duckduckgo/app/browser/defaultbrowsing/prompts/DefaultBrowserPromptsExperiment.kt b/app/src/main/java/com/duckduckgo/app/browser/defaultbrowsing/prompts/DefaultBrowserPromptsExperiment.kt index 634407325204..3052031e00a7 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/defaultbrowsing/prompts/DefaultBrowserPromptsExperiment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/defaultbrowsing/prompts/DefaultBrowserPromptsExperiment.kt @@ -30,20 +30,32 @@ interface DefaultBrowserPromptsExperiment { fun onSetAsDefaultPopupMenuItemSelected() fun onMessageDialogShown() - fun onMessageDialogDismissed() + fun onMessageDialogCanceled() fun onMessageDialogConfirmationButtonClicked() fun onMessageDialogNotNowButtonClicked() fun onSystemDefaultBrowserDialogShown() - fun onSystemDefaultBrowserDialogSuccess() - fun onSystemDefaultBrowserDialogCanceled() + fun onSystemDefaultBrowserDialogSuccess(trigger: SetAsDefaultActionTrigger) + fun onSystemDefaultBrowserDialogCanceled(trigger: SetAsDefaultActionTrigger) - fun onSystemDefaultAppsActivityOpened() - fun onSystemDefaultAppsActivityClosed() + fun onSystemDefaultAppsActivityClosed(trigger: SetAsDefaultActionTrigger) sealed class Command { data object OpenMessageDialog : Command() - data class OpenSystemDefaultBrowserDialog(val intent: Intent) : Command() - data class OpenSystemDefaultAppsActivity(val intent: Intent) : Command() + data class OpenSystemDefaultBrowserDialog( + val intent: Intent, + val trigger: SetAsDefaultActionTrigger, + ) : Command() + + data class OpenSystemDefaultAppsActivity( + val intent: Intent, + val trigger: SetAsDefaultActionTrigger, + ) : Command() + } + + enum class SetAsDefaultActionTrigger { + DIALOG, + MENU, + UNKNOWN, } } diff --git a/app/src/main/java/com/duckduckgo/app/browser/defaultbrowsing/prompts/DefaultBrowserPromptsExperimentImpl.kt b/app/src/main/java/com/duckduckgo/app/browser/defaultbrowsing/prompts/DefaultBrowserPromptsExperimentImpl.kt index 9662be4c1fb4..495eed2b3c44 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/defaultbrowsing/prompts/DefaultBrowserPromptsExperimentImpl.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/defaultbrowsing/prompts/DefaultBrowserPromptsExperimentImpl.kt @@ -23,8 +23,13 @@ import com.duckduckgo.app.browser.defaultbrowsing.DefaultBrowserDetector import com.duckduckgo.app.browser.defaultbrowsing.DefaultBrowserSystemSettings import com.duckduckgo.app.browser.defaultbrowsing.prompts.DefaultBrowserPromptsExperiment.Command import com.duckduckgo.app.browser.defaultbrowsing.prompts.DefaultBrowserPromptsExperiment.Command.OpenMessageDialog +import com.duckduckgo.app.browser.defaultbrowsing.prompts.DefaultBrowserPromptsExperiment.SetAsDefaultActionTrigger +import com.duckduckgo.app.browser.defaultbrowsing.prompts.DefaultBrowserPromptsExperiment.SetAsDefaultActionTrigger.DIALOG +import com.duckduckgo.app.browser.defaultbrowsing.prompts.DefaultBrowserPromptsExperiment.SetAsDefaultActionTrigger.MENU +import com.duckduckgo.app.browser.defaultbrowsing.prompts.DefaultBrowserPromptsExperiment.SetAsDefaultActionTrigger.UNKNOWN import com.duckduckgo.app.browser.defaultbrowsing.prompts.DefaultBrowserPromptsFeatureToggles.AdditionalPromptsCohortName import com.duckduckgo.app.browser.defaultbrowsing.prompts.store.DefaultBrowserPromptsDataStore +import com.duckduckgo.app.browser.defaultbrowsing.prompts.store.DefaultBrowserPromptsDataStore.ExperimentStage import com.duckduckgo.app.browser.defaultbrowsing.prompts.store.DefaultBrowserPromptsDataStore.ExperimentStage.CONVERTED import com.duckduckgo.app.browser.defaultbrowsing.prompts.store.DefaultBrowserPromptsDataStore.ExperimentStage.ENROLLED import com.duckduckgo.app.browser.defaultbrowsing.prompts.store.DefaultBrowserPromptsDataStore.ExperimentStage.NOT_ENROLLED @@ -36,10 +41,13 @@ import com.duckduckgo.app.global.DefaultRoleBrowserDialog import com.duckduckgo.app.lifecycle.MainProcessLifecycleObserver import com.duckduckgo.app.onboarding.store.AppStage import com.duckduckgo.app.onboarding.store.UserStageStore +import com.duckduckgo.app.pixels.AppPixelName +import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.app.usage.app.AppDaysUsedRepository import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.common.utils.plugins.PluginPoint import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.feature.toggles.api.MetricsPixel import com.duckduckgo.feature.toggles.api.Toggle import com.duckduckgo.privacy.config.api.PrivacyConfigCallbackPlugin import com.squareup.anvil.annotations.ContributesBinding @@ -95,6 +103,8 @@ class DefaultBrowserPromptsExperimentImpl @Inject constructor( private val userStageStore: UserStageStore, private val defaultBrowserPromptsDataStore: DefaultBrowserPromptsDataStore, private val experimentStageEvaluatorPluginPoint: PluginPoint, + private val metrics: DefaultBrowserPromptsExperimentMetrics, + private val pixel: Pixel, moshi: Moshi, ) : DefaultBrowserPromptsExperiment, MainProcessLifecycleObserver, PrivacyConfigCallbackPlugin { @@ -249,6 +259,24 @@ class DefaultBrowserPromptsExperimentImpl @Inject constructor( if (newExperimentStage != null) { defaultBrowserPromptsDataStore.storeExperimentStage(newExperimentStage) + when (newExperimentStage) { + STAGE_1 -> { + metrics.getStageImpressionForStage1()?.fire() + } + + STAGE_2 -> { + metrics.getStageImpressionForStage2()?.fire() + } + + CONVERTED -> { + fireConversionPixels(currentExperimentStage) + } + + else -> { + // no-op + } + } + val action = experimentStageEvaluatorPluginPoint.getPlugins().first { it.targetCohort == activeCohortName }.evaluate(newExperimentStage) if (action.showMessageDialog) { _commands.send(OpenMessageDialog) @@ -265,20 +293,25 @@ class DefaultBrowserPromptsExperimentImpl @Inject constructor( } override fun onSetAsDefaultPopupMenuItemSelected() { - launchBestSelectionWindow() + fireInteractionPixel(AppPixelName.SET_AS_DEFAULT_IN_MENU_CLICK) + launchBestSelectionWindow(trigger = MENU) } override fun onMessageDialogShown() { + fireInteractionPixel(AppPixelName.SET_AS_DEFAULT_PROMPT_IMPRESSION) } - override fun onMessageDialogDismissed() { + override fun onMessageDialogCanceled() { + fireInteractionPixel(AppPixelName.SET_AS_DEFAULT_PROMPT_DISMISSED) } override fun onMessageDialogConfirmationButtonClicked() { - launchBestSelectionWindow() + fireInteractionPixel(AppPixelName.SET_AS_DEFAULT_PROMPT_CLICK) + launchBestSelectionWindow(trigger = DIALOG) } override fun onMessageDialogNotNowButtonClicked() { + fireInteractionPixel(AppPixelName.SET_AS_DEFAULT_PROMPT_DISMISSED) } override fun onSystemDefaultBrowserDialogShown() { @@ -287,35 +320,52 @@ class DefaultBrowserPromptsExperimentImpl @Inject constructor( } } - override fun onSystemDefaultBrowserDialogSuccess() { + override fun onSystemDefaultBrowserDialogSuccess(trigger: SetAsDefaultActionTrigger) { + appCoroutineScope.launch { + when (trigger) { + DIALOG -> metrics.getDefaultSetViaDialog()?.fire() + MENU -> metrics.getDefaultSetViaMenu()?.fire() + UNKNOWN -> { + Timber.e("Trigger for default browser dialog result wasn't provided.") + } + } + } } - override fun onSystemDefaultBrowserDialogCanceled() { + override fun onSystemDefaultBrowserDialogCanceled(trigger: SetAsDefaultActionTrigger) { if (browserSelectionWindowFallbackDeferred?.isActive == true) { browserSelectionWindowFallbackDeferred?.cancel() - launchSystemDefaultAppsActivity() + launchSystemDefaultAppsActivity(trigger) } } - override fun onSystemDefaultAppsActivityOpened() { - } - - override fun onSystemDefaultAppsActivityClosed() { + override fun onSystemDefaultAppsActivityClosed(trigger: SetAsDefaultActionTrigger) { + if (defaultBrowserDetector.isDefaultBrowser()) { + appCoroutineScope.launch { + when (trigger) { + DIALOG -> metrics.getDefaultSetViaDialog()?.fire() + MENU -> metrics.getDefaultSetViaMenu()?.fire() + UNKNOWN -> { + Timber.e("Trigger for default apps result wasn't provided.") + } + } + } + } } - private fun launchBestSelectionWindow() { + private fun launchBestSelectionWindow(trigger: SetAsDefaultActionTrigger) { val command = defaultRoleBrowserDialog.createIntent(applicationContext)?.let { - Command.OpenSystemDefaultBrowserDialog(intent = it) + Command.OpenSystemDefaultBrowserDialog(intent = it, trigger) } if (command != null) { _commands.trySend(command) } else { - launchSystemDefaultAppsActivity() + launchSystemDefaultAppsActivity(trigger) } } - private fun launchSystemDefaultAppsActivity() { - val command = Command.OpenSystemDefaultAppsActivity(DefaultBrowserSystemSettings.intent()) + private fun launchSystemDefaultAppsActivity(trigger: SetAsDefaultActionTrigger) { + val command = Command.OpenSystemDefaultAppsActivity(DefaultBrowserSystemSettings.intent(), trigger) _commands.trySend(command) } @@ -345,7 +395,41 @@ class DefaultBrowserPromptsExperimentImpl @Inject constructor( return null } + private suspend fun fireConversionPixels(currentExperimentStage: ExperimentStage) { + when (currentExperimentStage) { + STAGE_1 -> { + metrics.getDefaultSetForStage1()?.fire() + } + + STAGE_2 -> { + metrics.getDefaultSetForStage2()?.fire() + } + + else -> { + // no-op + } + } + } + + private fun fireInteractionPixel(pixelName: AppPixelName) = appCoroutineScope.launch { + val variant = defaultBrowserPromptsFeatureToggles.defaultBrowserAdditionalPrompts202501().getCohort()?.name?.lowercase() ?: "" + val stage = defaultBrowserPromptsDataStore.experimentStage.first().toString().lowercase() + pixel.fire( + pixel = pixelName, + parameters = mapOf( + PIXEL_PARAM_KEY_VARIANT to variant, + PIXEL_PARAM_KEY_STAGE to stage, + ), + ) + } + + private fun MetricsPixel.fire() = getPixelDefinitions().forEach { + pixel.fire(it.pixelName, it.params) + } + companion object { const val FALLBACK_TO_DEFAULT_APPS_SCREEN_THRESHOLD_MILLIS = 500L + const val PIXEL_PARAM_KEY_VARIANT = "expVar" + const val PIXEL_PARAM_KEY_STAGE = "expStage" } } diff --git a/app/src/main/java/com/duckduckgo/app/browser/defaultbrowsing/prompts/DefaultBrowserPromptsExperimentMetrics.kt b/app/src/main/java/com/duckduckgo/app/browser/defaultbrowsing/prompts/DefaultBrowserPromptsExperimentMetrics.kt new file mode 100644 index 000000000000..7f8b8d35f82e --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/browser/defaultbrowsing/prompts/DefaultBrowserPromptsExperimentMetrics.kt @@ -0,0 +1,107 @@ +/* + * 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.browser.defaultbrowsing.prompts + +import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.feature.toggles.api.ConversionWindow +import com.duckduckgo.feature.toggles.api.MetricsPixel +import com.duckduckgo.feature.toggles.api.MetricsPixelPlugin +import com.squareup.anvil.annotations.ContributesMultibinding +import dagger.SingleInstanceIn +import javax.inject.Inject + +@ContributesMultibinding(AppScope::class) +@SingleInstanceIn(AppScope::class) +class DefaultBrowserPromptsExperimentMetrics @Inject constructor( + private val defaultBrowserPromptsFeatureToggles: DefaultBrowserPromptsFeatureToggles, +) : MetricsPixelPlugin { + override suspend fun getMetrics(): List { + return listOf( + MetricsPixel( + metric = METRIC_DEFAULT_SET, + value = METRIC_VALUE_STAGE_1, + toggle = defaultBrowserPromptsFeatureToggles.defaultBrowserAdditionalPrompts202501(), + conversionWindow = (20..60 step 20).map { ConversionWindow(lowerWindow = 1, upperWindow = it) }, + ), + MetricsPixel( + metric = METRIC_DEFAULT_SET, + value = METRIC_VALUE_STAGE_2, + toggle = defaultBrowserPromptsFeatureToggles.defaultBrowserAdditionalPrompts202501(), + conversionWindow = (20..60 step 20).map { ConversionWindow(lowerWindow = 1, upperWindow = it) }, + ), + MetricsPixel( + metric = METRIC_DEFAULT_SET_VIA_CTA, + value = METRIC_VALUE_DIALOG, + toggle = defaultBrowserPromptsFeatureToggles.defaultBrowserAdditionalPrompts202501(), + conversionWindow = (20..60 step 20).map { ConversionWindow(lowerWindow = 1, upperWindow = it) }, + ), + MetricsPixel( + metric = METRIC_DEFAULT_SET_VIA_CTA, + value = METRIC_VALUE_MENU, + toggle = defaultBrowserPromptsFeatureToggles.defaultBrowserAdditionalPrompts202501(), + conversionWindow = (20..60 step 20).map { ConversionWindow(lowerWindow = 1, upperWindow = it) }, + ), + MetricsPixel( + metric = METRIC_STAGE_IMPRESSION, + value = METRIC_VALUE_STAGE_1, + toggle = defaultBrowserPromptsFeatureToggles.defaultBrowserAdditionalPrompts202501(), + conversionWindow = (20..60 step 20).map { ConversionWindow(lowerWindow = 1, upperWindow = it) }, + ), + MetricsPixel( + metric = METRIC_STAGE_IMPRESSION, + value = METRIC_VALUE_STAGE_2, + toggle = defaultBrowserPromptsFeatureToggles.defaultBrowserAdditionalPrompts202501(), + conversionWindow = (20..60 step 20).map { ConversionWindow(lowerWindow = 1, upperWindow = it) }, + ), + ) + } + + suspend fun getDefaultSetForStage1(): MetricsPixel? { + return this.getMetrics().firstOrNull { it.metric == METRIC_DEFAULT_SET && it.value == METRIC_VALUE_STAGE_1 } + } + + suspend fun getDefaultSetForStage2(): MetricsPixel? { + return this.getMetrics().firstOrNull { it.metric == METRIC_DEFAULT_SET && it.value == METRIC_VALUE_STAGE_2 } + } + + suspend fun getDefaultSetViaDialog(): MetricsPixel? { + return this.getMetrics().firstOrNull { it.metric == METRIC_DEFAULT_SET_VIA_CTA && it.value == METRIC_VALUE_DIALOG } + } + + suspend fun getDefaultSetViaMenu(): MetricsPixel? { + return this.getMetrics().firstOrNull { it.metric == METRIC_DEFAULT_SET_VIA_CTA && it.value == METRIC_VALUE_MENU } + } + + suspend fun getStageImpressionForStage1(): MetricsPixel? { + return this.getMetrics().firstOrNull { it.metric == METRIC_STAGE_IMPRESSION && it.value == METRIC_VALUE_STAGE_1 } + } + + suspend fun getStageImpressionForStage2(): MetricsPixel? { + return this.getMetrics().firstOrNull { it.metric == METRIC_STAGE_IMPRESSION && it.value == METRIC_VALUE_STAGE_2 } + } + + companion object { + const val METRIC_DEFAULT_SET = "defaultSet" + const val METRIC_DEFAULT_SET_VIA_CTA = "defaultSetViaCta" + const val METRIC_STAGE_IMPRESSION = "stageImpression" + + const val METRIC_VALUE_STAGE_1 = "stage_1" + const val METRIC_VALUE_STAGE_2 = "stage_2" + const val METRIC_VALUE_DIALOG = "dialog" + const val METRIC_VALUE_MENU = "menu" + } +} diff --git a/app/src/main/java/com/duckduckgo/app/browser/defaultbrowsing/prompts/ui/DefaultBrowserBottomSheetDialog.kt b/app/src/main/java/com/duckduckgo/app/browser/defaultbrowsing/prompts/ui/DefaultBrowserBottomSheetDialog.kt index 9691254472d2..61ed7e8515bd 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/defaultbrowsing/prompts/ui/DefaultBrowserBottomSheetDialog.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/defaultbrowsing/prompts/ui/DefaultBrowserBottomSheetDialog.kt @@ -47,8 +47,8 @@ class DefaultBrowserBottomSheetDialog(private val context: Context) : BottomShee setRoundCorners(dialogInterface) eventListener?.onShown() } - setOnDismissListener { - eventListener?.onDismissed() + setOnCancelListener { + eventListener?.onCanceled() } binding.defaultBrowserBottomSheetDialogPrimaryButton.setOnClickListener { eventListener?.onSetBrowserButtonClicked() @@ -77,7 +77,7 @@ class DefaultBrowserBottomSheetDialog(private val context: Context) : BottomShee interface EventListener { fun onShown() - fun onDismissed() + fun onCanceled() fun onSetBrowserButtonClicked() fun onNotNowButtonClicked() } diff --git a/app/src/main/java/com/duckduckgo/app/global/api/PixelParamRemovalInterceptor.kt b/app/src/main/java/com/duckduckgo/app/global/api/PixelParamRemovalInterceptor.kt index d97f05e1ecc6..d2c748a31be0 100644 --- a/app/src/main/java/com/duckduckgo/app/global/api/PixelParamRemovalInterceptor.kt +++ b/app/src/main/java/com/duckduckgo/app/global/api/PixelParamRemovalInterceptor.kt @@ -95,6 +95,10 @@ object PixelInterceptorPixelsRequiringDataCleaning : PixelParamRemovalPlugin { SITE_NOT_WORKING_WEBSITE_BROKEN.pixelName to PixelParameter.removeAtb(), AppPixelName.APP_VERSION_AT_SEARCH_TIME.pixelName to PixelParameter.removeAll(), AppPixelName.MALICIOUS_SITE_PROTECTION_SETTING_TOGGLED.pixelName to PixelParameter.removeAtb(), + AppPixelName.SET_AS_DEFAULT_PROMPT_IMPRESSION.pixelName to PixelParameter.removeAll(), + AppPixelName.SET_AS_DEFAULT_PROMPT_CLICK.pixelName to PixelParameter.removeAll(), + AppPixelName.SET_AS_DEFAULT_PROMPT_DISMISSED.pixelName to PixelParameter.removeAll(), + AppPixelName.SET_AS_DEFAULT_IN_MENU_CLICK.pixelName to PixelParameter.removeAll(), ) } } diff --git a/app/src/main/java/com/duckduckgo/app/pixels/AppPixelName.kt b/app/src/main/java/com/duckduckgo/app/pixels/AppPixelName.kt index f909754f8e9e..0b5349bd275c 100644 --- a/app/src/main/java/com/duckduckgo/app/pixels/AppPixelName.kt +++ b/app/src/main/java/com/duckduckgo/app/pixels/AppPixelName.kt @@ -387,4 +387,9 @@ enum class AppPixelName(override val pixelName: String) : Pixel.PixelName { DEDICATED_WEBVIEW_URL_EXTRACTION_FAILED("m_dedicated_webview_url_extraction_failed"), BLOCKLIST_TDS_FAILURE("blocklist_experiment_tds_download_failure"), + + SET_AS_DEFAULT_PROMPT_IMPRESSION("m_set-as-default_prompt_impression"), + SET_AS_DEFAULT_PROMPT_CLICK("m_set-as-default_prompt_click"), + SET_AS_DEFAULT_PROMPT_DISMISSED("m_set-as-default_prompt_dismissed"), + SET_AS_DEFAULT_IN_MENU_CLICK("m_set-as-default_in-menu_click"), } diff --git a/app/src/test/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt b/app/src/test/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt index dea3c20a18e2..4935566dc49b 100644 --- a/app/src/test/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt +++ b/app/src/test/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt @@ -23,6 +23,7 @@ import androidx.lifecycle.Observer import com.duckduckgo.app.browser.BrowserViewModel.Command import com.duckduckgo.app.browser.defaultbrowsing.DefaultBrowserDetector import com.duckduckgo.app.browser.defaultbrowsing.prompts.DefaultBrowserPromptsExperiment +import com.duckduckgo.app.browser.defaultbrowsing.prompts.DefaultBrowserPromptsExperiment.SetAsDefaultActionTrigger import com.duckduckgo.app.browser.omnibar.OmnibarEntryConverter import com.duckduckgo.app.fire.DataClearer import com.duckduckgo.app.generalsettings.showonapplaunch.ShowOnAppLaunchFeature @@ -342,7 +343,8 @@ class BrowserViewModelTest { @Test fun `when default browser prompts experiment OpenSystemDefaultBrowserDialog command, then propagate it to consumers`() = runTest { val intent: Intent = mock() - defaultBrowserPromptsExperimentCommandsFlow.send(DefaultBrowserPromptsExperiment.Command.OpenSystemDefaultBrowserDialog(intent)) + val trigger: SetAsDefaultActionTrigger = mock() + defaultBrowserPromptsExperimentCommandsFlow.send(DefaultBrowserPromptsExperiment.Command.OpenSystemDefaultBrowserDialog(intent, trigger)) verify(mockCommandObserver).onChanged(commandCaptor.capture()) assertEquals(Command.ShowSystemDefaultBrowserDialog(intent), commandCaptor.lastValue) @@ -351,7 +353,8 @@ class BrowserViewModelTest { @Test fun `when default browser prompts experiment OpenSystemDefaultAppsActivity command, then propagate it to consumers`() = runTest { val intent: Intent = mock() - defaultBrowserPromptsExperimentCommandsFlow.send(DefaultBrowserPromptsExperiment.Command.OpenSystemDefaultAppsActivity(intent)) + val trigger: SetAsDefaultActionTrigger = mock() + defaultBrowserPromptsExperimentCommandsFlow.send(DefaultBrowserPromptsExperiment.Command.OpenSystemDefaultAppsActivity(intent, trigger)) verify(mockCommandObserver).onChanged(commandCaptor.capture()) assertEquals(Command.ShowSystemDefaultAppsActivity(intent), commandCaptor.lastValue) @@ -365,28 +368,28 @@ class BrowserViewModelTest { } @Test - fun `when onSetDefaultBrowserDismissed called, then pass that information to the experiment`() { - testee.onSetDefaultBrowserDismissed() + fun `when onSetDefaultBrowserDialogCanceled called, then pass that information to the experiment`() { + testee.onSetDefaultBrowserDialogCanceled() - verify(mockDefaultBrowserPromptsExperiment).onMessageDialogDismissed() + verify(mockDefaultBrowserPromptsExperiment).onMessageDialogCanceled() } @Test - fun `when onSetDefaultBrowserConfirmationButtonClicked called, then pass that information to the experiment`() { + fun `when onSetDefaultBrowserConfirmationButtonClicked called, then pass that information to the experiment and dismiss dialog`() { testee.onSetDefaultBrowserConfirmationButtonClicked() verify(mockDefaultBrowserPromptsExperiment).onMessageDialogConfirmationButtonClicked() verify(mockCommandObserver).onChanged(commandCaptor.capture()) - assertEquals(Command.HideSetAsDefaultBrowserDialog, commandCaptor.lastValue) + assertEquals(Command.DismissSetAsDefaultBrowserDialog, commandCaptor.lastValue) } @Test - fun `when onSetDefaultBrowserNotNowButtonClicked called, then pass that information to the experiment`() { + fun `when onSetDefaultBrowserNotNowButtonClicked called, then pass that information to the experiment and dismiss dialog`() { testee.onSetDefaultBrowserNotNowButtonClicked() verify(mockDefaultBrowserPromptsExperiment).onMessageDialogNotNowButtonClicked() verify(mockCommandObserver).onChanged(commandCaptor.capture()) - assertEquals(Command.HideSetAsDefaultBrowserDialog, commandCaptor.lastValue) + assertEquals(Command.DismissSetAsDefaultBrowserDialog, commandCaptor.lastValue) } @Test @@ -397,31 +400,36 @@ class BrowserViewModelTest { } @Test - fun `when onSystemDefaultBrowserDialogSuccess called, then pass that information to the experiment`() { + fun `when onSystemDefaultBrowserDialogSuccess called, then pass that information to the experiment`() = runTest { + val intent: Intent = mock() + val trigger: SetAsDefaultActionTrigger = mock() + defaultBrowserPromptsExperimentCommandsFlow.send(DefaultBrowserPromptsExperiment.Command.OpenSystemDefaultBrowserDialog(intent, trigger)) + testee.onSystemDefaultBrowserDialogSuccess() - verify(mockDefaultBrowserPromptsExperiment).onSystemDefaultBrowserDialogSuccess() + verify(mockDefaultBrowserPromptsExperiment).onSystemDefaultBrowserDialogSuccess(trigger) } @Test - fun `when onSystemDefaultBrowserDialogCanceled called, then pass that information to the experiment`() { + fun `when onSystemDefaultBrowserDialogCanceled called, then pass that information to the experiment`() = runTest { + val intent: Intent = mock() + val trigger: SetAsDefaultActionTrigger = mock() + defaultBrowserPromptsExperimentCommandsFlow.send(DefaultBrowserPromptsExperiment.Command.OpenSystemDefaultBrowserDialog(intent, trigger)) + testee.onSystemDefaultBrowserDialogCanceled() - verify(mockDefaultBrowserPromptsExperiment).onSystemDefaultBrowserDialogCanceled() + verify(mockDefaultBrowserPromptsExperiment).onSystemDefaultBrowserDialogCanceled(trigger) } @Test - fun `when onSystemDefaultAppsActivityOpened called, then pass that information to the experiment`() { - testee.onSystemDefaultAppsActivityOpened() - - verify(mockDefaultBrowserPromptsExperiment).onSystemDefaultAppsActivityOpened() - } + fun `when onSystemDefaultAppsActivityClosed called, then pass that information to the experiment`() = runTest { + val intent: Intent = mock() + val trigger: SetAsDefaultActionTrigger = mock() + defaultBrowserPromptsExperimentCommandsFlow.send(DefaultBrowserPromptsExperiment.Command.OpenSystemDefaultAppsActivity(intent, trigger)) - @Test - fun `when onSystemDefaultAppsActivityClosed called, then pass that information to the experiment`() { testee.onSystemDefaultAppsActivityClosed() - verify(mockDefaultBrowserPromptsExperiment).onSystemDefaultAppsActivityClosed() + verify(mockDefaultBrowserPromptsExperiment).onSystemDefaultAppsActivityClosed(trigger) } @Test diff --git a/app/src/test/java/com/duckduckgo/app/browser/defaultbrowsing/prompts/DefaultBrowserPromptsExperimentImplTest.kt b/app/src/test/java/com/duckduckgo/app/browser/defaultbrowsing/prompts/DefaultBrowserPromptsExperimentImplTest.kt index 6416e3927f3f..163fe00a2c7d 100644 --- a/app/src/test/java/com/duckduckgo/app/browser/defaultbrowsing/prompts/DefaultBrowserPromptsExperimentImplTest.kt +++ b/app/src/test/java/com/duckduckgo/app/browser/defaultbrowsing/prompts/DefaultBrowserPromptsExperimentImplTest.kt @@ -22,7 +22,11 @@ import androidx.lifecycle.LifecycleOwner import com.duckduckgo.app.browser.defaultbrowsing.DefaultBrowserDetector import com.duckduckgo.app.browser.defaultbrowsing.DefaultBrowserSystemSettings import com.duckduckgo.app.browser.defaultbrowsing.prompts.DefaultBrowserPromptsExperiment.Command +import com.duckduckgo.app.browser.defaultbrowsing.prompts.DefaultBrowserPromptsExperiment.SetAsDefaultActionTrigger.DIALOG +import com.duckduckgo.app.browser.defaultbrowsing.prompts.DefaultBrowserPromptsExperiment.SetAsDefaultActionTrigger.MENU import com.duckduckgo.app.browser.defaultbrowsing.prompts.DefaultBrowserPromptsExperimentImpl.Companion.FALLBACK_TO_DEFAULT_APPS_SCREEN_THRESHOLD_MILLIS +import com.duckduckgo.app.browser.defaultbrowsing.prompts.DefaultBrowserPromptsExperimentImpl.Companion.PIXEL_PARAM_KEY_STAGE +import com.duckduckgo.app.browser.defaultbrowsing.prompts.DefaultBrowserPromptsExperimentImpl.Companion.PIXEL_PARAM_KEY_VARIANT import com.duckduckgo.app.browser.defaultbrowsing.prompts.DefaultBrowserPromptsExperimentImpl.FeatureSettings import com.duckduckgo.app.browser.defaultbrowsing.prompts.DefaultBrowserPromptsFeatureToggles.AdditionalPromptsCohortName import com.duckduckgo.app.browser.defaultbrowsing.prompts.store.DefaultBrowserPromptsDataStore @@ -30,10 +34,17 @@ import com.duckduckgo.app.browser.defaultbrowsing.prompts.store.DefaultBrowserPr import com.duckduckgo.app.global.DefaultRoleBrowserDialog import com.duckduckgo.app.onboarding.store.AppStage import com.duckduckgo.app.onboarding.store.UserStageStore +import com.duckduckgo.app.pixels.AppPixelName.SET_AS_DEFAULT_IN_MENU_CLICK +import com.duckduckgo.app.pixels.AppPixelName.SET_AS_DEFAULT_PROMPT_CLICK +import com.duckduckgo.app.pixels.AppPixelName.SET_AS_DEFAULT_PROMPT_DISMISSED +import com.duckduckgo.app.pixels.AppPixelName.SET_AS_DEFAULT_PROMPT_IMPRESSION +import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.app.usage.app.AppDaysUsedRepository import com.duckduckgo.common.test.CoroutineTestRule import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.common.utils.plugins.PluginPoint +import com.duckduckgo.feature.toggles.api.MetricsPixel +import com.duckduckgo.feature.toggles.api.PixelDefinition import com.duckduckgo.feature.toggles.api.Toggle import com.duckduckgo.feature.toggles.api.Toggle.State.Cohort import com.squareup.moshi.JsonAdapter @@ -49,6 +60,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.toList import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals @@ -98,6 +110,30 @@ class DefaultBrowserPromptsExperimentImplTest { @Mock private lateinit var systemDefaultBrowserDialogIntentMock: Intent + @Mock private lateinit var metricsMock: DefaultBrowserPromptsExperimentMetrics + + @Mock private lateinit var pixelMock: Pixel + + @Mock private lateinit var stageImpressionForStage1MetricMock: MetricsPixel + private val stageImpressionForStage1Pixels = listOf( + PixelDefinition("stageImpressionForStage1Pixels", mapOf("stageImpressionForStage1PixelsParam" to "set")), + ) + + @Mock private lateinit var stageImpressionForStage2MetricMock: MetricsPixel + private val stageImpressionForStage2Pixels = listOf( + PixelDefinition("stageImpressionForStage2Pixels", mapOf("stageImpressionForStage2PixelsParam" to "set")), + ) + + @Mock private lateinit var defaultSetForStage1Metric: MetricsPixel + private val defaultSetForStage1MetricPixels = listOf( + PixelDefinition("defaultSetForStage1MetricPixels", mapOf("defaultSetForStage1MetricPixelsParam" to "set")), + ) + + @Mock private lateinit var defaultSetForStage2Metric: MetricsPixel + private val defaultSetForStage2MetricPixels = listOf( + PixelDefinition("defaultSetForStage2MetricPixels", mapOf("defaultSetForStage2MetricPixelsParam" to "set")), + ) + private lateinit var dataStoreMock: DefaultBrowserPromptsDataStore private val fakeEnrollmentDateETString = "2025-01-16T00:00-05:00[America/New_York]" @@ -115,6 +151,9 @@ class DefaultBrowserPromptsExperimentImplTest { whenever(moshiMock.adapter(any())).thenReturn(featureSettingsJsonAdapterMock) fakeUserAppStageFlow = MutableSharedFlow() whenever(userStageStoreMock.userAppStageFlow()).thenReturn(fakeUserAppStageFlow) + runBlocking { + mockMetrics() + } } @Test @@ -157,7 +196,7 @@ class DefaultBrowserPromptsExperimentImplTest { fun `when popup menu item clicked, then launch browser selection system dialog`() = runTest { val testee = createTestee() val expectedUpdates = listOf( - Command.OpenSystemDefaultBrowserDialog(systemDefaultBrowserDialogIntentMock), + Command.OpenSystemDefaultBrowserDialog(systemDefaultBrowserDialogIntentMock, trigger = MENU), ) val actualUpdates = mutableListOf() coroutinesTestRule.testScope.launch { @@ -195,7 +234,7 @@ class DefaultBrowserPromptsExperimentImplTest { fun `when message dialog confirmation clicked, then launch browser selection system dialog`() = runTest { val testee = createTestee() val expectedUpdates = listOf( - Command.OpenSystemDefaultBrowserDialog(systemDefaultBrowserDialogIntentMock), + Command.OpenSystemDefaultBrowserDialog(systemDefaultBrowserDialogIntentMock, trigger = DIALOG), ) val actualUpdates = mutableListOf() coroutinesTestRule.testScope.launch { @@ -247,8 +286,8 @@ class DefaultBrowserPromptsExperimentImplTest { testee.onSystemDefaultBrowserDialogShown() advanceTimeBy(FALLBACK_TO_DEFAULT_APPS_SCREEN_THRESHOLD_MILLIS - 1) // canceled before threshold - testee.onSystemDefaultBrowserDialogCanceled() - testee.onSystemDefaultBrowserDialogCanceled() // verifies that repeated cancellation won't keep opening new screens + testee.onSystemDefaultBrowserDialogCanceled(trigger = DIALOG) + testee.onSystemDefaultBrowserDialogCanceled(trigger = DIALOG) // verifies that repeated cancellation won't keep opening new screens assertEquals(1, actualUpdates.size) assertTrue(actualUpdates[0] is Command.OpenSystemDefaultAppsActivity) @@ -265,7 +304,7 @@ class DefaultBrowserPromptsExperimentImplTest { testee.onSystemDefaultBrowserDialogShown() advanceTimeBy(FALLBACK_TO_DEFAULT_APPS_SCREEN_THRESHOLD_MILLIS + 1) // canceled after threshold - testee.onSystemDefaultBrowserDialogCanceled() + testee.onSystemDefaultBrowserDialogCanceled(trigger = DIALOG) assertTrue(actualUpdates.isEmpty()) } @@ -417,6 +456,8 @@ class DefaultBrowserPromptsExperimentImplTest { verify(dataStoreMock, never()).storeExperimentStage(any()) verify(evaluatorMock, never()).evaluate(any()) + verify(pixelMock, never()).fire(any(), any(), any(), any()) + verify(pixelMock, never()).fire(any(), any(), any(), any()) } @Test @@ -451,6 +492,9 @@ class DefaultBrowserPromptsExperimentImplTest { verify(dataStoreMock).storeExperimentStage(ExperimentStage.STAGE_1) verify(evaluatorMock).evaluate(ExperimentStage.STAGE_1) + stageImpressionForStage1Pixels.forEach { + verify(pixelMock).fire(it.pixelName, it.params) + } } @Test @@ -481,6 +525,8 @@ class DefaultBrowserPromptsExperimentImplTest { verify(dataStoreMock, never()).storeExperimentStage(any()) verify(evaluatorMock, never()).evaluate(any()) + verify(pixelMock, never()).fire(any(), any(), any(), any()) + verify(pixelMock, never()).fire(any(), any(), any(), any()) } @Test @@ -515,6 +561,9 @@ class DefaultBrowserPromptsExperimentImplTest { verify(dataStoreMock).storeExperimentStage(ExperimentStage.STAGE_2) verify(evaluatorMock).evaluate(ExperimentStage.STAGE_2) + stageImpressionForStage2Pixels.forEach { + verify(pixelMock).fire(it.pixelName, it.params) + } } @Test @@ -545,6 +594,8 @@ class DefaultBrowserPromptsExperimentImplTest { verify(dataStoreMock, never()).storeExperimentStage(any()) verify(evaluatorMock, never()).evaluate(any()) + verify(pixelMock, never()).fire(any(), any(), any(), any()) + verify(pixelMock, never()).fire(any(), any(), any(), any()) } @Test @@ -581,34 +632,6 @@ class DefaultBrowserPromptsExperimentImplTest { verify(evaluatorMock).evaluate(ExperimentStage.STOPPED) } - @Test - fun `evaluate - if enrolled and browser set as default, then convert`() = runTest { - val dataStoreMock = createDataStoreFake( - initialExperimentStage = ExperimentStage.ENROLLED, - ) - val testee = createTestee( - defaultBrowserPromptsDataStore = dataStoreMock, - ) - whenever(userStageStoreMock.getUserAppStage()).thenReturn(AppStage.ESTABLISHED) - whenever(defaultBrowserDetectorMock.isDefaultBrowser()).thenReturn(true) - mockActiveCohort(cohortName = AdditionalPromptsCohortName.VARIANT_2) - val evaluatorMock = mockStageEvaluator( - forNewStage = ExperimentStage.CONVERTED, - forCohortName = AdditionalPromptsCohortName.VARIANT_2, - returnsAction = DefaultBrowserPromptsExperimentStageAction( - showMessageDialog = false, - showSetAsDefaultPopupMenuItem = false, - highlightPopupMenu = false, - ), - ) - whenever(experimentStageEvaluatorPluginPointMock.getPlugins()).thenReturn(setOf(evaluatorMock)) - - testee.onResume(lifecycleOwnerMock) - - verify(dataStoreMock).storeExperimentStage(ExperimentStage.CONVERTED) - verify(evaluatorMock).evaluate(ExperimentStage.CONVERTED) - } - @Test fun `evaluate - if stage 1 and browser set as default, then convert`() = runTest { val dataStoreMock = createDataStoreFake( @@ -635,6 +658,9 @@ class DefaultBrowserPromptsExperimentImplTest { verify(dataStoreMock).storeExperimentStage(ExperimentStage.CONVERTED) verify(evaluatorMock).evaluate(ExperimentStage.CONVERTED) + defaultSetForStage1MetricPixels.forEach { + verify(pixelMock).fire(it.pixelName, it.params) + } } @Test @@ -663,6 +689,9 @@ class DefaultBrowserPromptsExperimentImplTest { verify(dataStoreMock).storeExperimentStage(ExperimentStage.CONVERTED) verify(evaluatorMock).evaluate(ExperimentStage.CONVERTED) + defaultSetForStage2MetricPixels.forEach { + verify(pixelMock).fire(it.pixelName, it.params) + } } @Test @@ -827,6 +856,111 @@ class DefaultBrowserPromptsExperimentImplTest { verify(dataStoreMock).storeHighlightPopupMenuState(highlight = true) } + @Test + fun `if message dialog shown, then send a pixel`() = runTest { + val expectedParams = mapOf( + PIXEL_PARAM_KEY_VARIANT to "variant_2", + PIXEL_PARAM_KEY_STAGE to "stage_1", + ) + val dataStoreMock = createDataStoreFake( + initialExperimentStage = ExperimentStage.STAGE_1, + ) + val testee = createTestee( + defaultBrowserPromptsDataStore = dataStoreMock, + ) + mockActiveCohort( + cohortName = AdditionalPromptsCohortName.VARIANT_2, + ) + + testee.onMessageDialogShown() + + verify(pixelMock).fire(SET_AS_DEFAULT_PROMPT_IMPRESSION, expectedParams) + } + + @Test + fun `if message dialog canceled, then send a pixel`() = runTest { + val expectedParams = mapOf( + PIXEL_PARAM_KEY_VARIANT to "variant_2", + PIXEL_PARAM_KEY_STAGE to "stage_1", + ) + val dataStoreMock = createDataStoreFake( + initialExperimentStage = ExperimentStage.STAGE_1, + ) + val testee = createTestee( + defaultBrowserPromptsDataStore = dataStoreMock, + ) + mockActiveCohort( + cohortName = AdditionalPromptsCohortName.VARIANT_2, + ) + + testee.onMessageDialogCanceled() + + verify(pixelMock).fire(SET_AS_DEFAULT_PROMPT_DISMISSED, expectedParams) + } + + @Test + fun `if message dialog not now clicked, then send a pixel`() = runTest { + val expectedParams = mapOf( + PIXEL_PARAM_KEY_VARIANT to "variant_2", + PIXEL_PARAM_KEY_STAGE to "stage_1", + ) + val dataStoreMock = createDataStoreFake( + initialExperimentStage = ExperimentStage.STAGE_1, + ) + val testee = createTestee( + defaultBrowserPromptsDataStore = dataStoreMock, + ) + mockActiveCohort( + cohortName = AdditionalPromptsCohortName.VARIANT_2, + ) + + testee.onMessageDialogNotNowButtonClicked() + + verify(pixelMock).fire(SET_AS_DEFAULT_PROMPT_DISMISSED, expectedParams) + } + + @Test + fun `if message dialog confirmation clicked, then send a pixel`() = runTest { + val expectedParams = mapOf( + PIXEL_PARAM_KEY_VARIANT to "variant_2", + PIXEL_PARAM_KEY_STAGE to "stage_1", + ) + val dataStoreMock = createDataStoreFake( + initialExperimentStage = ExperimentStage.STAGE_1, + ) + val testee = createTestee( + defaultBrowserPromptsDataStore = dataStoreMock, + ) + mockActiveCohort( + cohortName = AdditionalPromptsCohortName.VARIANT_2, + ) + + testee.onMessageDialogConfirmationButtonClicked() + + verify(pixelMock).fire(SET_AS_DEFAULT_PROMPT_CLICK, expectedParams) + } + + @Test + fun `if menu item clicked, then send a pixel`() = runTest { + val expectedParams = mapOf( + PIXEL_PARAM_KEY_VARIANT to "variant_2", + PIXEL_PARAM_KEY_STAGE to "stage_1", + ) + val dataStoreMock = createDataStoreFake( + initialExperimentStage = ExperimentStage.STAGE_1, + ) + val testee = createTestee( + defaultBrowserPromptsDataStore = dataStoreMock, + ) + mockActiveCohort( + cohortName = AdditionalPromptsCohortName.VARIANT_2, + ) + + testee.onSetAsDefaultPopupMenuItemSelected() + + verify(pixelMock).fire(SET_AS_DEFAULT_IN_MENU_CLICK, expectedParams) + } + private fun createTestee( appCoroutineScope: CoroutineScope = coroutinesTestRule.testScope, dispatchers: DispatcherProvider = coroutinesTestRule.testDispatcherProvider, @@ -838,6 +972,8 @@ class DefaultBrowserPromptsExperimentImplTest { userStageStore: UserStageStore = userStageStoreMock, defaultBrowserPromptsDataStore: DefaultBrowserPromptsDataStore = dataStoreMock, experimentStageEvaluatorPluginPoint: PluginPoint = experimentStageEvaluatorPluginPointMock, + metrics: DefaultBrowserPromptsExperimentMetrics = metricsMock, + pixel: Pixel = pixelMock, moshi: Moshi = moshiMock, ) = DefaultBrowserPromptsExperimentImpl( appCoroutineScope = appCoroutineScope, @@ -850,6 +986,8 @@ class DefaultBrowserPromptsExperimentImplTest { userStageStore = userStageStore, defaultBrowserPromptsDataStore = defaultBrowserPromptsDataStore, experimentStageEvaluatorPluginPoint = experimentStageEvaluatorPluginPoint, + metrics = metrics, + pixel = pixel, moshi = moshi, ) @@ -902,6 +1040,20 @@ class DefaultBrowserPromptsExperimentImplTest { whenever(evaluatorMock.evaluate(forNewStage)).thenReturn(returnsAction) return evaluatorMock } + + private suspend fun mockMetrics() { + whenever(metricsMock.getStageImpressionForStage1()).thenReturn(stageImpressionForStage1MetricMock) + whenever(stageImpressionForStage1MetricMock.getPixelDefinitions()).thenReturn(stageImpressionForStage1Pixels) + + whenever(metricsMock.getStageImpressionForStage2()).thenReturn(stageImpressionForStage2MetricMock) + whenever(stageImpressionForStage2MetricMock.getPixelDefinitions()).thenReturn(stageImpressionForStage2Pixels) + + whenever(metricsMock.getDefaultSetForStage1()).thenReturn(defaultSetForStage1Metric) + whenever(defaultSetForStage1Metric.getPixelDefinitions()).thenReturn(defaultSetForStage1MetricPixels) + + whenever(metricsMock.getDefaultSetForStage2()).thenReturn(defaultSetForStage2Metric) + whenever(defaultSetForStage2Metric.getPixelDefinitions()).thenReturn(defaultSetForStage2MetricPixels) + } } class DefaultBrowserPromptsDataStoreMock(