First use of Nimbus FML plugin (#23400)
* Consume Nimbus FML plugin * Convert Homescreen to use FML * Convert nimbusValidation to use FML * Convert legacy experiments to use the feature API and FML Remove dead helper code and documentation * Fixup failing test Co-authored-by: Grisha Kruglov <gkruglov@mozilla.com> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>upstream-sync
parent
e92fe26df7
commit
82a6f8cae4
@ -0,0 +1,59 @@
|
|||||||
|
{
|
||||||
|
"search-term-groups": {
|
||||||
|
"description": "A feature allowing the grouping of URLs around the search term that it came from.",
|
||||||
|
"hasExposure": true,
|
||||||
|
"exposureDescription": "",
|
||||||
|
"variables": {
|
||||||
|
"enabled": {
|
||||||
|
"description": "If true, the feature shows up on the homescreen and on the new tab screen.",
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"default-browser-message": {
|
||||||
|
"description": "A small feature allowing experiments on the placement of a default browser message.",
|
||||||
|
"hasExposure": true,
|
||||||
|
"exposureDescription": "",
|
||||||
|
"variables": {
|
||||||
|
"message-location": {
|
||||||
|
"description": "Where is the message to be put.",
|
||||||
|
"enum": [
|
||||||
|
"homescreen-banner",
|
||||||
|
"settings",
|
||||||
|
"app-menu-item"
|
||||||
|
],
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nimbus-validation": {
|
||||||
|
"description": "A feature that does not correspond to an application feature suitable for showing that Nimbus is working. This should never be used in production.",
|
||||||
|
"hasExposure": true,
|
||||||
|
"exposureDescription": "",
|
||||||
|
"variables": {
|
||||||
|
"settings-icon": {
|
||||||
|
"description": "The drawable displayed in the app menu for Settings",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"settings-punctuation": {
|
||||||
|
"description": "The emoji displayed in the Settings screen title.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"settings-title": {
|
||||||
|
"description": "The title of displayed in the Settings screen and app menu.",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"homescreen": {
|
||||||
|
"description": "The homescreen that the user goes to when they press home or new tab.",
|
||||||
|
"hasExposure": true,
|
||||||
|
"exposureDescription": "",
|
||||||
|
"variables": {
|
||||||
|
"sections-enabled": {
|
||||||
|
"description": "This property provides a lookup table of whether or not the given section should be enabled. If the section is enabled, it should be toggleable in the settings screen, and on by default.",
|
||||||
|
"type": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,35 +0,0 @@
|
|||||||
/* 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.components.metrics
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Build
|
|
||||||
import mozilla.components.lib.dataprotect.SecurePrefsReliabilityExperiment
|
|
||||||
import mozilla.components.service.nimbus.NimbusApi
|
|
||||||
import org.mozilla.fenix.experiments.ExperimentBranch
|
|
||||||
import org.mozilla.fenix.experiments.FeatureId
|
|
||||||
import org.mozilla.fenix.ext.withExperiment
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Allows starting a quick test of ACs SecureAbove22Preferences that will emit Facts
|
|
||||||
* for the basic operations and allow us to log them for later evaluation of APIs stability.
|
|
||||||
*/
|
|
||||||
class SecurePrefsTelemetry(
|
|
||||||
private val appContext: Context,
|
|
||||||
private val experiments: NimbusApi
|
|
||||||
) {
|
|
||||||
fun startTests() {
|
|
||||||
// The Android Keystore is used to secure the shared prefs only on API 23+
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
||||||
// These tests should run only if the experiment is live
|
|
||||||
experiments.withExperiment(FeatureId.ANDROID_KEYSTORE) { experimentBranch ->
|
|
||||||
// .. and this device is not in the control group.
|
|
||||||
if (experimentBranch == ExperimentBranch.TREATMENT) {
|
|
||||||
SecurePrefsReliabilityExperiment(appContext)()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,35 +0,0 @@
|
|||||||
/* 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
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enums to identify features in the app. These will likely grow and shrink depending
|
|
||||||
* on the experiments we want to perform.
|
|
||||||
*
|
|
||||||
* @property jsonName the kebab-case version of the feature id as represented in the Nimbus
|
|
||||||
* experiment JSON.
|
|
||||||
*/
|
|
||||||
enum class FeatureId(val jsonName: String) {
|
|
||||||
NIMBUS_VALIDATION("nimbus-validation"),
|
|
||||||
ANDROID_KEYSTORE("fenix-android-keystore"),
|
|
||||||
DEFAULT_BROWSER("fenix-default-browser"),
|
|
||||||
HOME_PAGE("homescreen")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Experiment branches are becoming less interesting, though we collect some well
|
|
||||||
* defined ones here.
|
|
||||||
*/
|
|
||||||
class ExperimentBranch {
|
|
||||||
companion object {
|
|
||||||
const val TREATMENT = "treatment"
|
|
||||||
const val CONTROL = "control"
|
|
||||||
const val A1 = "a1"
|
|
||||||
const val A2 = "a2"
|
|
||||||
const val DEFAULT_BROWSER_TOOLBAR_MENU = "default_browser_toolbar_menu"
|
|
||||||
const val DEFAULT_BROWSER_NEW_TAB_BANNER = "default_browser_newtab_banner"
|
|
||||||
const val DEFAULT_BROWSER_SETTINGS_MENU = "default_browser_settings_menu"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,119 +0,0 @@
|
|||||||
/* 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.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(true),
|
|
||||||
jumpBackIn(true),
|
|
||||||
pocket(true),
|
|
||||||
recentExplorations(true);
|
|
||||||
|
|
||||||
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 value = if (section == pocket) {
|
|
||||||
FeatureFlags.isPocketRecommendationsFeatureEnabled(context)
|
|
||||||
} else {
|
|
||||||
section.default
|
|
||||||
}
|
|
||||||
section to value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,110 +0,0 @@
|
|||||||
/* 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.ext
|
|
||||||
|
|
||||||
import mozilla.components.service.nimbus.NimbusApi
|
|
||||||
import mozilla.components.support.base.log.logger.Logger
|
|
||||||
import org.mozilla.experiments.nimbus.Variables
|
|
||||||
import org.mozilla.fenix.experiments.FeatureId
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the branch name of an experiment acting on the feature given `featureId`, and transforms it
|
|
||||||
* with given closure.
|
|
||||||
*
|
|
||||||
* You are probably looking for `withVariables`.
|
|
||||||
*
|
|
||||||
* If we're enrolled in an experiment, the transform is passed the branch id/slug as a `String`.
|
|
||||||
*
|
|
||||||
* If we're not enrolled in the experiment, or the experiment is not valid then the transform
|
|
||||||
* is passed a `null`.
|
|
||||||
*/
|
|
||||||
fun <T> NimbusApi.withExperiment(featureId: FeatureId, transform: (String?) -> T): T {
|
|
||||||
return transform(withExperiment(featureId))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The synonym for [getExperimentBranch] to complement [withExperiment(String, (String?) -> T))].
|
|
||||||
*
|
|
||||||
* Short-hand for ` org.mozilla.experiments.nimbus.NimbusApi.getExperimentBranch`.
|
|
||||||
*/
|
|
||||||
@Suppress("TooGenericExceptionCaught")
|
|
||||||
fun NimbusApi.withExperiment(featureId: FeatureId) =
|
|
||||||
try {
|
|
||||||
getExperimentBranch(featureId.jsonName)
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Logger.error("Failed to getExperimentBranch(${featureId.jsonName})", e)
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the variables needed to configure the feature given by `featureId`.
|
|
||||||
*
|
|
||||||
* @param featureId The feature id that identifies the feature under experiment.
|
|
||||||
*
|
|
||||||
* @param sendExposureEvent Passing `true` to this parameter will record the exposure event
|
|
||||||
* automatically if the client is enrolled in an experiment for the given [featureId].
|
|
||||||
* Passing `false` here indicates that the application will manually record the exposure
|
|
||||||
* event by calling the `sendExposureEvent` function at the time of the exposure to the
|
|
||||||
* feature.
|
|
||||||
*
|
|
||||||
* See [sendExposureEvent] for more information on manually recording the event.
|
|
||||||
*
|
|
||||||
* @return a [Variables] object used to configure the feature.
|
|
||||||
*/
|
|
||||||
fun NimbusApi.getVariables(featureId: FeatureId, sendExposureEvent: Boolean = true) =
|
|
||||||
getVariables(featureId.jsonName, sendExposureEvent)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A synonym for `getVariables(featureId, sendExposureEvent)`.
|
|
||||||
*
|
|
||||||
* This exists as a complement to the `withVariable(featureId, sendExposureEvent, transform)` method.
|
|
||||||
*
|
|
||||||
* @param featureId the id of the feature as it appears in `Experimenter`
|
|
||||||
* @param sendExposureEvent by default `true`. This logs an event that the user was exposed to an experiment
|
|
||||||
* involving this feature.
|
|
||||||
* @return a `Variables` object providing typed accessors to a remotely configured JSON object.
|
|
||||||
*/
|
|
||||||
fun NimbusApi.withVariables(featureId: FeatureId, sendExposureEvent: Boolean = true) =
|
|
||||||
getVariables(featureId, sendExposureEvent)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a `Variables` object for this feature and use that to configure the feature itself or a
|
|
||||||
* more type safe configuration object.
|
|
||||||
*
|
|
||||||
* @param featureId the id of the feature as it appears in `Experimenter`
|
|
||||||
* @param sendExposureEvent by default `true`. This logs an event that the user was exposed to an experiment
|
|
||||||
* involving this feature.
|
|
||||||
*/
|
|
||||||
fun <T> NimbusApi.withVariables(featureId: FeatureId, sendExposureEvent: Boolean = true, transform: (Variables) -> T) =
|
|
||||||
transform(getVariables(featureId, sendExposureEvent))
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Records the `exposure` event in telemetry.
|
|
||||||
*
|
|
||||||
* This is a manual function to accomplish the same purpose as passing `true` as the
|
|
||||||
* `sendExposureEvent` property of the `getVariables` function. It is intended to be used
|
|
||||||
* when requesting feature variables must occur at a different time than the actual user's
|
|
||||||
* exposure to the feature within the app.
|
|
||||||
*
|
|
||||||
* - Examples:
|
|
||||||
* - If the `Variables` are needed at a different time than when the exposure to the feature
|
|
||||||
* actually happens, such as constructing a menu happening at a different time than the
|
|
||||||
* user seeing the menu.
|
|
||||||
* - If `getVariables` is required to be called multiple times for the same feature and it is
|
|
||||||
* desired to only record the exposure once, such as if `getVariables` were called
|
|
||||||
* with every keystroke.
|
|
||||||
*
|
|
||||||
* In the case where the use of this function is required, then the `getVariables` function
|
|
||||||
* should be called with `false` so that the exposure event is not recorded when the variables
|
|
||||||
* are fetched.
|
|
||||||
*
|
|
||||||
* This function is safe to call even when there is no active experiment for the feature. The SDK
|
|
||||||
* will ensure that an event is only recorded for active experiments.
|
|
||||||
*
|
|
||||||
* @param featureId string representing the id of the feature for which to record the exposure
|
|
||||||
* event.
|
|
||||||
*/
|
|
||||||
fun NimbusApi.recordExposureEvent(featureId: FeatureId) =
|
|
||||||
recordExposureEvent(featureId.jsonName)
|
|
@ -0,0 +1,89 @@
|
|||||||
|
---
|
||||||
|
channels:
|
||||||
|
- release
|
||||||
|
- beta
|
||||||
|
- nightly
|
||||||
|
- developer
|
||||||
|
features:
|
||||||
|
homescreen:
|
||||||
|
description: The homescreen that the user goes to when they press home or new tab.
|
||||||
|
variables:
|
||||||
|
sections-enabled:
|
||||||
|
description: "This property provides a lookup table of whether or not the given section should be enabled.
|
||||||
|
If the section is enabled, it should be toggleable in the settings screen, and on by default."
|
||||||
|
type: Map<HomeScreenSection, Boolean>
|
||||||
|
default:
|
||||||
|
{
|
||||||
|
"top-sites": true,
|
||||||
|
"jump-back-in": true,
|
||||||
|
"recently-saved": true,
|
||||||
|
"recent-explorations": true,
|
||||||
|
"pocket": true
|
||||||
|
}
|
||||||
|
defaults:
|
||||||
|
- channel: nightly
|
||||||
|
value: {
|
||||||
|
"sections-enabled": {
|
||||||
|
"top-sites": true,
|
||||||
|
"jump-back-in": true,
|
||||||
|
"recently-saved": true,
|
||||||
|
"recent-explorations": true,
|
||||||
|
"pocket": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nimbus-validation:
|
||||||
|
description: "A feature that does not correspond to an application feature suitable for showing
|
||||||
|
that Nimbus is working. This should never be used in production."
|
||||||
|
variables:
|
||||||
|
settings-title:
|
||||||
|
description: The title of displayed in the Settings screen and app menu.
|
||||||
|
type: Text
|
||||||
|
default: browser_menu_settings
|
||||||
|
settings-punctuation:
|
||||||
|
description: The emoji displayed in the Settings screen title.
|
||||||
|
type: String
|
||||||
|
default: ""
|
||||||
|
settings-icon:
|
||||||
|
description: The drawable displayed in the app menu for Settings
|
||||||
|
type: String
|
||||||
|
default: mozac_ic_settings
|
||||||
|
search-term-groups:
|
||||||
|
description: A feature allowing the grouping of URLs around the search term that it came from.
|
||||||
|
variables:
|
||||||
|
enabled:
|
||||||
|
description: If true, the feature shows up on the homescreen and on the new tab screen.
|
||||||
|
type: Boolean
|
||||||
|
default: false
|
||||||
|
default-browser-message:
|
||||||
|
description: A small feature allowing experiments on the placement of a default browser message.
|
||||||
|
variables:
|
||||||
|
message-location:
|
||||||
|
description: Where is the message to be put.
|
||||||
|
type: MessageSurfaceId
|
||||||
|
default: homescreen-banner
|
||||||
|
types:
|
||||||
|
objects: {}
|
||||||
|
enums:
|
||||||
|
HomeScreenSection:
|
||||||
|
description: The identifiers for the sections of the homescreen.
|
||||||
|
variants:
|
||||||
|
top-sites:
|
||||||
|
description: The frecency and pinned sites.
|
||||||
|
recently-saved:
|
||||||
|
description: The sites the user has bookmarked recently.
|
||||||
|
jump-back-in:
|
||||||
|
description: The tabs the user was looking immediately before being interrupted.
|
||||||
|
recent-explorations:
|
||||||
|
description: The tab groups
|
||||||
|
pocket:
|
||||||
|
description: The pocket section. This should only be available in the US.
|
||||||
|
MessageSurfaceId:
|
||||||
|
description: The identity of a message surface, used in the default browser experiments
|
||||||
|
variants:
|
||||||
|
app-menu-item:
|
||||||
|
description: An item in the default toolbar menu.
|
||||||
|
settings:
|
||||||
|
description: A setting in the settings screen.
|
||||||
|
homescreen-banner:
|
||||||
|
description: A banner in the homescreen.
|
||||||
|
|
Loading…
Reference in New Issue