For #21618: Integrate Nimbus with MR2 Home Page to enable experimentation

This commit is contained in:
Arturo Mejia 2021-09-21 12:02:35 -04:00 committed by mergify[bot]
parent ebd336501b
commit 2b363b9868
12 changed files with 246 additions and 237 deletions

View File

@ -4,46 +4,56 @@
"appId": "org.mozilla.fenix",
"appName": "fenix",
"channel": "nightly",
"branches": [{
"slug": "control",
"branches": [
{
"slug": "no-mr2",
"ratio": 0,
"feature": {
"value": {
"sections-enabled": {
"topSites": true,
"recentExplorations": true,
"recentlySaved": false,
"jumpBackIn": false,
"pocket": false
}
},
"enabled": true,
"featureId": "homescreen"
}
},
{
"slug": "full-mr2",
"ratio": 100,
"feature": {
"value": {},
"value": {
"sections-enabled": {
"topSites": true,
"recentExplorations": true,
"recentlySaved": true,
"jumpBackIn": true,
"pocket": true
}
},
"enabled": true,
"featureId": "nimbus-validation"
"featureId": "homescreen"
}
},
{
"slug": "fancy-settings",
"slug": "distraction-free",
"ratio": 0,
"feature": {
"value": {
"settings-title": "Fancy Settings"
"sections-enabled": {
"topSites": true,
"recentExplorations": false,
"recentlySaved": false,
"jumpBackIn": false,
"pocket": false
}
},
"enabled": true,
"featureId": "nimbus-validation"
}
},
{
"slug": "smiley",
"ratio": 0,
"feature": {
"value": {
"settings-title-punctuation": "\uD83D\uDE03"
},
"enabled": true,
"featureId": "nimbus-validation"
}
},
{
"slug": "bundled-text",
"ratio": 0,
"feature": {
"value": {
"settings-title": "preferences_category_general"
},
"enabled": true,
"featureId": "nimbus-validation"
"featureId": "homescreen"
}
}
],
@ -53,7 +63,7 @@
"startDate": null,
"targeting": "true",
"featureIds": [
"nimbus-validation"
"homescreen"
],
"application": "org.mozilla.firefox_beta",
"bucketConfig": {
@ -64,181 +74,12 @@
"randomizationUnit": "nimbus_id"
},
"schemaVersion": "1.5.0",
"userFacingName": "Nimbus Text Variables Validation",
"userFacingName": "Home screen sections test",
"referenceBranch": "control",
"proposedDuration": 14,
"isEnrollmentPaused": false,
"proposedEnrollment": 7,
"userFacingDescription": "Demonstration experiment to make trivial visible changes to text in Settings",
"last_modified": 1621443780172
},
{
"slug": "feature-icon-variables-validation-android",
"appId": "org.mozilla.fenix",
"appName": "fenix",
"channel": "nightly",
"branches": [{
"slug": "control",
"ratio": 100,
"feature": {
"value": {},
"enabled": true,
"featureId": "nimbus-validation"
}
},
{
"slug": "edit-menu-icon",
"ratio": 0,
"feature": {
"value": {
"settings-title": "preferences_category_general",
"settings-icon": "ic_edit"
},
"enabled": true,
"featureId": "nimbus-validation"
}
}
],
"outcomes": [],
"arguments": {},
"probeSets": [],
"startDate": null,
"targeting": "true",
"featureIds": [
"nimbus-validation"
],
"application": "org.mozilla.firefox_beta",
"bucketConfig": {
"count": 0,
"start": 0,
"total": 10000,
"namespace": "nimbus-validation-2",
"randomizationUnit": "nimbus_id"
},
"schemaVersion": "1.5.0",
"userFacingName": "Nimbus Icon Variables Validation",
"referenceBranch": "control",
"proposedDuration": 14,
"isEnrollmentPaused": false,
"proposedEnrollment": 7,
"userFacingDescription": "Demonstration experiment to make trivial visible changes to icons in Settings",
"last_modified": 1621443780172
},
{
"slug": "feature-text-variables-validation-ios",
"appId": "org.mozilla.ios.Fennec",
"appName": "firefox_ios",
"channel": "nightly",
"branches": [{
"slug": "control",
"ratio": 100,
"feature": {
"value": {},
"enabled": true,
"featureId": "nimbus-validation"
}
},
{
"slug": "a1",
"ratio": 0,
"feature": {
"value": {
"settings-title": "Menu/Menu.OpenSettingsAction.Title",
"settings-title-punctuation": "…"
},
"enabled": true,
"featureId": "nimbus-validation"
}
},
{
"slug": "a2",
"ratio": 0,
"feature": {
"value": {
"settings-title": "Settings.General.SectionName",
"settings-title-punctuation": "!"
},
"enabled": true,
"featureId": "nimbus-validation"
}
}
],
"outcomes": [],
"arguments": {},
"probeSets": [],
"startDate": null,
"targeting": "true",
"featureIds": [
"nimbus-validation"
],
"application": "org.mozilla.ios.Fennec",
"bucketConfig": {
"count": 0,
"start": 0,
"total": 10000,
"namespace": "nimbus-validation-2",
"randomizationUnit": "nimbus_id"
},
"schemaVersion": "1.5.0",
"userFacingName": "Nimbus Text Variables Validation",
"referenceBranch": "control",
"proposedDuration": 14,
"isEnrollmentPaused": false,
"proposedEnrollment": 7,
"userFacingDescription": "Demonstration experiment to make trivial visible changes to text in Settings",
"last_modified": 1621443780172
},
{
"slug": "feature-icon-variables-validation-ios",
"appId": "org.mozilla.ios.Fennec",
"appName": "firefox_ios",
"channel": "nightly",
"branches": [{
"slug": "control",
"ratio": 100,
"feature": {
"value": {},
"enabled": true,
"featureId": "nimbus-validation"
}
},
{
"slug": "treatment",
"ratio": 0,
"feature": {
"value": {
"settings-title": "Fancy Settings",
"settings-icon": "menu-ViewMobile"
},
"enabled": true,
"featureId": "nimbus-validation"
}
}
],
"outcomes": [],
"arguments": {},
"probeSets": [],
"startDate": null,
"targeting": "true",
"featureIds": [
"nimbus-validation"
],
"application": "org.mozilla.ios.Fennec",
"bucketConfig": {
"count": 0,
"start": 0,
"total": 10000,
"namespace": "nimbus-validation-2",
"randomizationUnit": "nimbus_id"
},
"schemaVersion": "1.5.0",
"userFacingName": "Nimbus Icon Variables Validation",
"referenceBranch": "control",
"proposedDuration": 14,
"isEnrollmentPaused": false,
"proposedEnrollment": 7,
"userFacingDescription": "Demonstration experiment to make trivial visible changes to icons in Settings",
"userFacingDescription": "Experiment to test the home screen configurations",
"last_modified": 1621443780172
}
]

View File

@ -31,7 +31,7 @@ object FeatureFlags {
/**
* Enables the Home button in the browser toolbar to navigate back to the home screen.
*/
val showHomeButtonFeature = Config.channel.isNightlyOrDebug
val showHomeButtonFeature = Config.channel.isNightlyOrDebug || Config.channel.isBeta
/**
* Enables the Start On Home feature in the settings page.
@ -41,17 +41,17 @@ object FeatureFlags {
/**
* Enables the "recent" tabs feature in the home screen.
*/
val showRecentTabsFeature = Config.channel.isNightlyOrDebug
val showRecentTabsFeature = Config.channel.isNightlyOrDebug || Config.channel.isBeta
/**
* Enables UI features based on history metadata.
*/
val historyMetadataUIFeature = Config.channel.isNightlyOrDebug
val historyMetadataUIFeature = Config.channel.isNightlyOrDebug || Config.channel.isBeta
/**
* Enables the recently saved bookmarks feature in the home screen.
*/
val recentBookmarksFeature = Config.channel.isNightlyOrDebug
val recentBookmarksFeature = Config.channel.isNightlyOrDebug || Config.channel.isBeta
/**
* Identifies and separates the tabs list with a secondary section containing least used tabs.
@ -66,7 +66,7 @@ object FeatureFlags {
/**
* Enables customizing the home screen
*/
val customizeHome = Config.channel.isNightlyOrDebug
val customizeHome = Config.channel.isNightlyOrDebug || Config.channel.isBeta
/**
* Identifies and separates the tabs list with a group containing search term tabs.
@ -82,7 +82,9 @@ object FeatureFlags {
* Show Pocket recommended stories on home.
*/
fun isPocketRecommendationsFeatureEnabled(context: Context): Boolean {
return Config.channel.isNightlyOrDebug &&
"en-US" == LocaleManager.getCurrentLocale(context)?.toLanguageTag() ?: getSystemDefault().toLanguageTag()
return Config.channel.isBeta || (
Config.channel.isNightlyOrDebug && "en-US" == LocaleManager.getCurrentLocale(context)
?.toLanguageTag() ?: getSystemDefault().toLanguageTag()
)
}
}

View File

@ -68,6 +68,8 @@ import mozilla.components.feature.autofill.AutofillUseCases
import mozilla.components.feature.search.ext.buildSearchUrl
import mozilla.components.feature.search.ext.waitForSelectedOrDefaultSearchEngine
import mozilla.components.service.fxa.manager.SyncEnginesStorage
import org.mozilla.experiments.nimbus.NimbusInterface
import org.mozilla.experiments.nimbus.internal.EnrolledExperiment
import org.mozilla.fenix.GleanMetrics.Addons
import org.mozilla.fenix.GleanMetrics.AndroidAutofill
import org.mozilla.fenix.GleanMetrics.CustomizeHome
@ -690,11 +692,20 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
}
)
}
CustomizeHome.jumpBackIn.set(settings.showRecentTabsFeature)
CustomizeHome.recentlySaved.set(settings.showRecentBookmarksFeature)
CustomizeHome.mostVisitedSites.set(settings.showTopFrecentSites)
CustomizeHome.recentlyVisited.set(settings.historyMetadataUIFeature)
CustomizeHome.pocket.set(settings.pocketRecommendations)
reportHomeScreenMetrics(settings)
}
@VisibleForTesting
internal fun reportHomeScreenMetrics(settings: Settings) {
components.analytics.experiments.register(object : NimbusInterface.Observer {
override fun onUpdatesApplied(updated: List<EnrolledExperiment>) {
CustomizeHome.jumpBackIn.set(settings.showRecentTabsFeature)
CustomizeHome.recentlySaved.set(settings.showRecentBookmarksFeature)
CustomizeHome.mostVisitedSites.set(settings.showTopFrecentSites)
CustomizeHome.recentlyVisited.set(settings.historyMetadataUIFeature)
CustomizeHome.pocket.set(settings.showPocketRecommendationsFeature)
}
})
}
protected fun recordOnInit() {

View File

@ -269,7 +269,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
components.core.requestInterceptor.setNavigationController(navHost.navController)
if (settings().pocketRecommendations) {
if (settings().showPocketRecommendationsFeature) {
components.core.pocketStoriesService.startPeriodicStoriesRefresh()
}

View File

@ -22,6 +22,7 @@ import org.mozilla.fenix.ReleaseChannel
import org.mozilla.fenix.components.metrics.AdjustMetricsService
import org.mozilla.fenix.components.metrics.GleanMetricsService
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.experiments.NimbusFeatures
import org.mozilla.fenix.experiments.createNimbus
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.perf.lazyMonitored
@ -103,6 +104,10 @@ class Analytics(
val experiments: NimbusApi by lazyMonitored {
createNimbus(context, BuildConfig.NIMBUS_ENDPOINT)
}
val features: NimbusFeatures by lazyMonitored {
NimbusFeatures(context)
}
}
fun isSentryEnabled() = !BuildConfig.SENTRY_TOKEN.isNullOrEmpty()

View File

@ -23,3 +23,26 @@ fun featureFlagPreference(key: String, default: Boolean, featureFlag: Boolean) =
} else {
DummyProperty()
}
private class LazyPreference(val key: String, val default: () -> Boolean) :
ReadWriteProperty<PreferencesHolder, Boolean> {
private val property: ReadWriteProperty<PreferencesHolder, Boolean> by lazy {
booleanPreference(key, default())
}
override fun getValue(thisRef: PreferencesHolder, property: KProperty<*>) =
this.property.getValue(thisRef, property)
override fun setValue(thisRef: PreferencesHolder, property: KProperty<*>, value: Boolean) =
this.property.setValue(thisRef, property, value)
}
/**
* Property delegate for getting and setting lazily a boolean shared preference gated by a feature flag.
*/
fun lazyFeatureFlagPreference(key: String, featureFlag: Boolean, default: () -> Boolean) =
if (featureFlag) {
LazyPreference(key, default)
} else {
DummyProperty()
}

View File

@ -14,7 +14,8 @@ package org.mozilla.fenix.experiments
enum class FeatureId(val jsonName: String) {
NIMBUS_VALIDATION("nimbus-validation"),
ANDROID_KEYSTORE("fenix-android-keystore"),
DEFAULT_BROWSER("fenix-default-browser")
DEFAULT_BROWSER("fenix-default-browser"),
HOME_PAGE("homescreen")
}
/**

View File

@ -0,0 +1,120 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.experiments
import android.content.Context
import org.mozilla.experiments.nimbus.mapKeysAsEnums
import org.mozilla.fenix.Config
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.getVariables
/**
* Component for exposing nimbus Feature Variables.
* For more information see https://experimenter.info/feature-variables-and-me
*
* @param context - A [Context] for accessing the feature variables from nimbus.
*/
class NimbusFeatures(private val context: Context) {
val homeScreen: HomeScreenFeatures by lazy {
HomeScreenFeatures(context)
}
/**
* Component that indicates which features should be active on the home screen.
*/
class HomeScreenFeatures(private val context: Context) {
/**
* `FeatureId.HOME_PAGE` feature; the complete JSON, is shown here:
*
* ```json
* {
* "sections-enabled": {
* "topSites": true,
* "recentlySaved": false,
* "jumpBackIn": false,
* "pocket": false,
* "recentExplorations": false
* }
* }
* ```
*/
/**
* This enum accompanies the `FeatureId.HOME_PAGE` feature.
*
* These names here should match the names of entries in the JSON.
*/
@Suppress("EnumNaming")
private enum class HomeScreenSection(val default: Boolean) {
topSites(true),
recentlySaved(false),
jumpBackIn(false),
pocket(false),
recentExplorations(false);
companion object {
/**
* CreateS a map with the corresponding default values for each sections.
*/
fun toMap(context: Context): Map<HomeScreenSection, Boolean> {
return values().associate { section ->
val channelDefault = if (section == pocket) {
FeatureFlags.isPocketRecommendationsFeatureEnabled(context)
} else {
Config.channel.isNightlyOrDebug
}
section to (channelDefault || section.default)
}
}
}
}
private val homeScreenFeatures: Map<HomeScreenSection, Boolean> by lazy {
val experiments = context.components.analytics.experiments
val variables = experiments.getVariables(FeatureId.HOME_PAGE, false)
val sections: Map<HomeScreenSection, Boolean> =
variables.getBoolMap("sections-enabled")?.mapKeysAsEnums()
?: HomeScreenSection.toMap(context)
sections
}
/**
* Indicates if the recently tabs feature is active.
*/
fun isRecentlyTabsActive(): Boolean {
return homeScreenFeatures[HomeScreenSection.jumpBackIn] == true
}
/**
* Indicates if the recently saved feature is active.
*/
fun isRecentlySavedActive(): Boolean {
return homeScreenFeatures[HomeScreenSection.recentlySaved] == true
}
/**
* Indicates if the recently exploration feature is active.
*/
fun isRecentExplorationsActive(): Boolean {
return homeScreenFeatures[HomeScreenSection.recentExplorations] == true
}
/**
* Indicates if the pocket recommendations feature is active.
*/
fun isPocketRecommendationsActive(): Boolean {
return homeScreenFeatures[HomeScreenSection.pocket] == true
}
/**
* Indicates if the top sites feature is active.
*/
fun isTopSitesActive(): Boolean {
return homeScreenFeatures[HomeScreenSection.topSites] == true
}
}
}

View File

@ -94,10 +94,12 @@ import org.mozilla.fenix.components.toolbar.FenixTabCounterMenu
import org.mozilla.fenix.components.toolbar.ToolbarPosition
import org.mozilla.fenix.databinding.FragmentHomeBinding
import org.mozilla.fenix.ext.asRecentTabs
import org.mozilla.fenix.experiments.FeatureId
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.hideToolbar
import org.mozilla.fenix.ext.metrics
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.recordExposureEvent
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.runIfFragmentIsAttached
import org.mozilla.fenix.ext.settings
@ -253,9 +255,7 @@ class HomeFragment : Fragment() {
}
lifecycleScope.launch(IO) {
if (FeatureFlags.isPocketRecommendationsFeatureEnabled(requireContext()) &&
requireContext().settings().pocketRecommendations
) {
if (requireContext().settings().showPocketRecommendationsFeature) {
val categories = components.core.pocketStoriesService.getStories()
.groupBy { story -> story.category }
.map { (category, stories) -> PocketRecommendedStoryCategory(category, stories) }
@ -370,6 +370,8 @@ class HomeFragment : Fragment() {
appBarLayout = binding.homeAppBar
activity.themeManager.applyStatusBarTheme(activity)
requireContext().components.analytics.experiments.recordExposureEvent(FeatureId.HOME_PAGE)
return binding.root
}

View File

@ -160,7 +160,7 @@ class CustomizationFragment : PreferenceFragmentCompat() {
requirePreference<SwitchPreference>(R.string.pref_key_pocket_homescreen_recommendations).apply {
isVisible = FeatureFlags.isPocketRecommendationsFeatureEnabled(context)
isChecked = context.settings().pocketRecommendations
isChecked = context.settings().showPocketRecommendationsFeature
onPreferenceChangeListener = CustomizeHomeMetricsUpdater()
}

View File

@ -32,6 +32,7 @@ import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.components.metrics.MozillaProductDetector
import org.mozilla.fenix.components.settings.counterPreference
import org.mozilla.fenix.components.settings.featureFlagPreference
import org.mozilla.fenix.components.settings.lazyFeatureFlagPreference
import org.mozilla.fenix.components.toolbar.ToolbarPosition
import org.mozilla.fenix.experiments.ExperimentBranch
import org.mozilla.fenix.experiments.FeatureId
@ -107,9 +108,10 @@ class Settings(private val appContext: Context) : PreferencesHolder {
override val preferences: SharedPreferences =
appContext.getSharedPreferences(FENIX_PREFERENCES, MODE_PRIVATE)
var showTopFrecentSites by booleanPreference(
var showTopFrecentSites by lazyFeatureFlagPreference(
appContext.getPreferenceKey(R.string.pref_key_enable_top_frecent_sites),
default = true
featureFlag = true,
default = { appContext.components.analytics.features.homeScreen.isTopSitesActive() }
)
var numberOfAppLaunches by intPreference(
@ -1140,9 +1142,9 @@ class Settings(private val appContext: Context) : PreferencesHolder {
default = false
)
var historyMetadataUIFeature by featureFlagPreference(
var historyMetadataUIFeature by lazyFeatureFlagPreference(
appContext.getPreferenceKey(R.string.pref_key_history_metadata_feature),
default = FeatureFlags.historyMetadataUIFeature,
default = { appContext.components.analytics.features.homeScreen.isRecentExplorationsActive() },
featureFlag = FeatureFlags.historyMetadataUIFeature || isHistoryMetadataEnabled
)
@ -1150,19 +1152,19 @@ class Settings(private val appContext: Context) : PreferencesHolder {
* Indicates if the recent tabs functionality should be visible.
* Returns true if the [FeatureFlags.showRecentTabsFeature] and [R.string.pref_key_recent_tabs] are true.
*/
var showRecentTabsFeature by featureFlagPreference(
var showRecentTabsFeature by lazyFeatureFlagPreference(
appContext.getPreferenceKey(R.string.pref_key_recent_tabs),
default = FeatureFlags.showRecentTabsFeature,
featureFlag = FeatureFlags.showRecentTabsFeature
featureFlag = FeatureFlags.showRecentTabsFeature,
default = { appContext.components.analytics.features.homeScreen.isRecentlyTabsActive() }
)
/**
* Indicates if the recent saved bookmarks functionality should be visible.
* Returns true if the [FeatureFlags.showRecentTabsFeature] and [R.string.pref_key_recent_bookmarks] are true.
*/
var showRecentBookmarksFeature by featureFlagPreference(
var showRecentBookmarksFeature by lazyFeatureFlagPreference(
appContext.getPreferenceKey(R.string.pref_key_recent_bookmarks),
default = FeatureFlags.recentBookmarksFeature,
default = { appContext.components.analytics.features.homeScreen.isRecentlySavedActive() },
featureFlag = FeatureFlags.recentBookmarksFeature
)
@ -1191,8 +1193,9 @@ class Settings(private val appContext: Context) : PreferencesHolder {
default = true
)
var pocketRecommendations by booleanPreference(
var showPocketRecommendationsFeature by lazyFeatureFlagPreference(
appContext.getPreferenceKey(R.string.pref_key_pocket_homescreen_recommendations),
default = true
featureFlag = FeatureFlags.isPocketRecommendationsFeatureEnabled(appContext),
default = { appContext.components.analytics.features.homeScreen.isPocketRecommendationsActive() },
)
}

View File

@ -5,8 +5,11 @@
package org.mozilla.fenix
import androidx.test.core.app.ApplicationProvider
import io.mockk.Runs
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.spyk
import io.mockk.verify
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.webextension.DisabledFlags
@ -21,7 +24,6 @@ import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.GleanMetrics.Addons
import org.mozilla.fenix.GleanMetrics.CustomizeHome
import org.mozilla.fenix.GleanMetrics.Metrics
import org.mozilla.fenix.GleanMetrics.Preferences
import org.mozilla.fenix.GleanMetrics.SearchDefaultEngine
@ -81,6 +83,8 @@ class FenixApplicationTest {
fun `WHEN setStartupMetrics is called THEN sets some base metrics`() {
val expectedAppName = "org.mozilla.fenix"
val settings: Settings = mockk()
val application = spyk(application)
every { browsersCache.all(any()).isDefaultBrowser } returns true
every { mozillaProductDetector.getMozillaBrowserDefault(any()) } returns expectedAppName
every { mozillaProductDetector.getInstalledMozillaProducts(any()) } returns listOf(expectedAppName)
@ -122,7 +126,9 @@ class FenixApplicationTest {
every { settings.showRecentBookmarksFeature } returns true
every { settings.showTopFrecentSites } returns true
every { settings.historyMetadataUIFeature } returns true
every { settings.pocketRecommendations } returns true
every { settings.showPocketRecommendationsFeature } returns true
every { settings.showPocketRecommendationsFeature } returns true
every { application.reportHomeScreenMetrics(settings) } just Runs
application.setStartupMetrics(browserStore, settings, browsersCache, mozillaProductDetector)
@ -158,11 +164,6 @@ class FenixApplicationTest {
assertEquals("fixed_top", Preferences.toolbarPositionSetting.testGetValue())
assertEquals("standard", Preferences.enhancedTrackingProtection.testGetValue())
assertEquals(listOf("switch", "touch exploration"), Preferences.accessibilityServices.testGetValue())
assertEquals(true, CustomizeHome.jumpBackIn.testGetValue())
assertEquals(true, CustomizeHome.recentlySaved.testGetValue())
assertEquals(true, CustomizeHome.mostVisitedSites.testGetValue())
assertEquals(true, CustomizeHome.recentlyVisited.testGetValue())
assertEquals(true, CustomizeHome.pocket.testGetValue())
// Verify that search engine defaults are NOT set. This test does
// not mock most of the objects telemetry is collected from.