Bug 1942694 - Remove toolkit/components/featuregates r=settings-reviewers,firefox-desktop-core-reviewers ,frontend-codestyle-reviewers,Gijs,android-reviewers,Roger

Differential Revision: https://phabricator.services.mozilla.com/D239672
This commit is contained in:
Beth Rennie
2025-03-11 18:16:50 +00:00
parent cf27fc4c61
commit 91d6531c40
39 changed files with 3 additions and 2025 deletions

View File

@@ -362,7 +362,6 @@ const rollouts = [
"toolkit/components/crashmonitor/CrashMonitor.sys.mjs",
"toolkit/components/credentialmanagement/IdentityCredentialPromptService.sys.mjs",
"toolkit/components/downloads/**",
"toolkit/components/featuregates/FeatureGate*.*",
"toolkit/components/forgetaboutsite/test/unit/test_removeDataFromDomain.js",
"toolkit/components/glean/tests/browser/**",
"toolkit/components/kvstore/kvstore.sys.mjs",
@@ -513,7 +512,6 @@ const rollouts = [
"toolkit/components/downloads/**",
"toolkit/components/enterprisepolicies/EnterprisePolicies*.sys.mjs",
"toolkit/components/extensions/**",
"toolkit/components/featuregates/**",
"toolkit/components/forgetaboutsite/**",
"toolkit/components/formautofill/**",
"toolkit/components/glean/tests/browser/**",

View File

@@ -37,7 +37,6 @@ ChromeUtils.defineESModuleGetters(lazy, {
DownloadsViewableInternally:
"resource:///modules/DownloadsViewableInternally.sys.mjs",
ExtensionsUI: "resource:///modules/ExtensionsUI.sys.mjs",
FeatureGate: "resource://featuregates/FeatureGate.sys.mjs",
FirefoxBridgeExtensionUtils:
"resource:///modules/FirefoxBridgeExtensionUtils.sys.mjs",
// FilePickerCrashed is used by the `listeners` object below.
@@ -2387,8 +2386,6 @@ BrowserGlue.prototype = {
if (AppConstants.MOZ_CRASHREPORTER) {
lazy.UnsubmittedCrashHandler.init();
lazy.UnsubmittedCrashHandler.scheduleCheckForUnsubmittedCrashReports();
lazy.FeatureGate.annotateCrashReporter();
lazy.FeatureGate.observePrefChangesForCrashReportAnnotation();
}
if (AppConstants.ASAN_REPORTER) {

View File

@@ -74,7 +74,6 @@ js_source_path = [
"../toolkit/actors",
"../toolkit/components/extensions",
"../toolkit/components/extensions/parent",
"../toolkit/components/featuregates",
"../toolkit/components/ml/content/backends/ONNXPipeline.mjs",
"../toolkit/mozapps/extensions",
"../toolkit/components/prompts/src",

View File

@@ -822,24 +822,6 @@ memory:
environment:
experimental_features:
type: string_list
description: >
Specifies the enabled experimental features from
about:preferences#experimental.
notification_emails:
- crash-reporting-wg@mozilla.org
- stability@mozilla.org
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1830098
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1830098
data_sensitivity:
- technical
expires: never
send_in_pings:
- crash
headless_mode:
type: boolean
description: >

View File

@@ -40,7 +40,6 @@ import mozilla.components.support.ktx.android.content.isMainProcess
import mozilla.telemetry.glean.private.BooleanMetricType
import mozilla.telemetry.glean.private.ObjectMetricType
import mozilla.telemetry.glean.private.QuantityMetricType
import mozilla.telemetry.glean.private.StringListMetricType
import mozilla.telemetry.glean.private.StringMetricType
import java.io.File
import java.io.FileOutputStream
@@ -149,9 +148,6 @@ class GleanCrashReporterService(
// Overrides the original `startup` parameter to `Ping` when present
GleanCrash.startup.setIfNonNull(extras["StartupCrash"])
GleanEnvironment.experimentalFeatures.setExperimentalFeaturesIfNonNull(
extras["ExperimentalFeatures"],
)
GleanEnvironment.headlessMode.setIfNonNull(extras["HeadlessMode"])
GleanMemory.availableCommit.setIfNonNull(extras["AvailablePageFile"])
@@ -180,15 +176,6 @@ class GleanCrashReporterService(
element?.jsonPrimitive?.long?.let(::set)
}
private fun StringListMetricType.setExperimentalFeaturesIfNonNull(
element: JsonElement?,
) {
element?.let {
// Split on commas
set(it.jsonPrimitive.content.split(',').filter(String::isNotEmpty))
}
}
private fun ObjectMetricType<AsyncShutdownTimeoutObject>.setAsyncShutdownTimeoutIfNonNull(
element: JsonElement?,
) {

View File

@@ -639,7 +639,6 @@ class GleanCrashReporterServiceTest {
"Version": "123.0.0",
"StartupCrash": "1",
"TotalPhysicalMemory": 100,
"ExperimentalFeatures": "expa,expb",
"AsyncShutdownTimeout": "{\"phase\":\"abcd\",\"conditions\":[{\"foo\":\"bar\"}],\"brokenAddBlockers\":[\"foo\"]}",
"QuotaManagerShutdownTimeout": "line1\nline2\nline3",
"StackTraces": $stackTracesAnnotation
@@ -677,10 +676,6 @@ class GleanCrashReporterServiceTest {
assertEquals("beta", GleanCrash.appChannel.testGetValue())
assertEquals("123.0.0", GleanCrash.appDisplayVersion.testGetValue())
assertEquals(100L, GleanMemory.totalPhysical.testGetValue())
assertEquals(
listOf("expa", "expb"),
GleanEnvironment.experimentalFeatures.testGetValue(),
)
assertEquals(
JsonObject(
mapOf(

View File

@@ -63,7 +63,6 @@ exclude = [
"testing/runtimes/writeruntimes.py",
"testing/tools/iceserver/iceserver.py",
"testing/tools/websocketprocessbridge/websocketprocessbridge.py",
"toolkit/components/featuregates",
"toolkit/content/tests/chrome/file_about_networking_wsh.py",
"toolkit/library/build/dependentlibs.py",
"toolkit/mozapps",

View File

@@ -534,28 +534,6 @@ condprof:
- 'testing/condprofile/condprof**'
- 'testing/condprofile/setup.py'
featuregates:
description: featuregates Python unit tests
platform:
- linux1804-64/opt
- windows11-64/opt
python-version: [3]
treeherder:
symbol: fg
run:
using: python-test
subsuite: featuregates
fetches:
toolchain:
by-platform:
linux1804-64/opt:
- linux64-node
windows11-64/opt:
- win64-node
when:
files-changed:
- 'toolkit/components/featuregates/**'
skip-fails:
description: testing/skip-fails unit tests
always-target: false

View File

@@ -869,7 +869,6 @@ CrashManager.prototype = Object.freeze({
user32LoadedBefore: t(bool, "User32BeforeBlocklist"),
},
environment: {
experimentalFeatures: t(comma_list, cap),
headlessMode: t(bool, cap),
nimbusEnrollments: t(comma_list, cap),
uptime: t(seconds, "UptimeTS"),

View File

@@ -832,24 +832,6 @@ environment:
send_in_pings:
- crash
experimental_features:
type: string_list
description: >
Specifies the enabled experimental features from
about:preferences#experimental.
notification_emails:
- crash-reporting-wg@mozilla.org
- stability@mozilla.org
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1830098
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1830098
data_sensitivity:
- technical
expires: never
send_in_pings:
- crash
headless_mode:
type: boolean
description: >

View File

@@ -975,10 +975,6 @@ add_task(async function test_glean_crash_ping() {
]);
Assert.equal(Glean.dllBlocklist.initFailed.testGetValue(), true);
Assert.equal(Glean.dllBlocklist.user32LoadedBefore.testGetValue(), true);
Assert.deepEqual(Glean.environment.experimentalFeatures.testGetValue(), [
"feature 1",
"feature 2",
]);
Assert.equal(Glean.environment.headlessMode.testGetValue(), true);
Assert.deepEqual(Glean.environment.nimbusEnrollments.testGetValue(), [
"foo:control",
@@ -1022,7 +1018,6 @@ add_task(async function test_glean_crash_ping() {
BlockedDllList: "Foo.dll;bar.dll;rawr.dll",
BlocklistInitFailed: "1",
EventLoopNestingLevel: 5,
ExperimentalFeatures: "feature 1,feature 2",
FontName: "Helvetica",
GPUProcessLaunchCount: 10,
HeadlessMode: "1",

View File

@@ -1,279 +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/. */
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
// This is an unfortunate exception where we depend on ASRouter because
// Nimbus has this dependency.
// This implementation is written in a way where it will avoid requiring
// this module if it's not available.
ASRouterTargeting:
// eslint-disable-next-line mozilla/no-browser-refs-in-toolkit
"resource:///modules/asrouter/ASRouterTargeting.sys.mjs",
FeatureGateImplementation:
"resource://featuregates/FeatureGateImplementation.sys.mjs",
ExperimentManager: "resource://nimbus/lib/ExperimentManager.sys.mjs",
TargetingContext: "resource://messaging-system/targeting/Targeting.sys.mjs",
});
ChromeUtils.defineLazyGetter(lazy, "gFeatureDefinitionsPromise", async () => {
const url = "resource://featuregates/feature_definitions.json";
return fetchFeatureDefinitions(url);
});
const kCustomTargeting = {
// For default values, although something like `channel == 'nightly'` kinda
// works, local builds don't have that update channel set in that way so it
// doesn't, and then tests fail because the defaults for the FeatureGate
// do not match the default value in the prefs code.
// We may in future want other things from AppConstants here, too.
nightly_build: AppConstants.NIGHTLY_BUILD,
thunderbird: AppConstants.MOZ_APP_NAME == "thunderbird",
};
ChromeUtils.defineLazyGetter(lazy, "defaultContexts", () => {
let ASRouterEnv = {};
try {
ASRouterEnv = lazy.ASRouterTargeting.Environment;
} catch (ex) {
// No ASRouter; just keep going.
}
return [
kCustomTargeting,
lazy.ExperimentManager.createTargetingContext(),
ASRouterEnv,
];
});
function getCombinedContext(...contexts) {
let combined = lazy.TargetingContext.combineContexts(
...lazy.defaultContexts,
...contexts
);
return new lazy.TargetingContext(combined, {
source: "featuregate",
});
}
async function fetchFeatureDefinitions(url) {
const res = await fetch(url);
let definitionsJson = await res.json();
return new Map(Object.entries(definitionsJson));
}
async function buildFeatureGateImplementation(definition) {
const targetValueKeys = ["defaultValue", "isPublic"];
for (const key of targetValueKeys) {
definition[key] = await FeatureGate.evaluateJexlValue(
definition[key + "Jexl"]
);
}
return new lazy.FeatureGateImplementation(definition);
}
let featureGatePrefObserver = {
onChange() {
FeatureGate.annotateCrashReporter();
},
// Ignore onEnable and onDisable since onChange is called in both cases.
onEnable() {},
onDisable() {},
};
const kFeatureGateCache = new Map();
/** A high level control for turning features on and off. */
export class FeatureGate {
/*
* This is structured as a class with static methods to that sphinx-js can
* easily document it. This constructor is required for sphinx-js to detect
* this class for documentation.
*/
constructor() {}
/**
* Constructs a feature gate object that is defined in ``Features.toml``.
* This is the primary way to create a ``FeatureGate``.
*
* @param {string} id The ID of the feature's definition in `Features.toml`.
* @param {string} testDefinitionsUrl A URL from which definitions can be fetched. Only use this in tests.
* @throws If the ``id`` passed is not defined in ``Features.toml``.
*/
static async fromId(id, testDefinitionsUrl = undefined) {
let featureDefinitions;
if (testDefinitionsUrl) {
featureDefinitions = await fetchFeatureDefinitions(testDefinitionsUrl);
} else {
featureDefinitions = await lazy.gFeatureDefinitionsPromise;
}
if (!featureDefinitions.has(id)) {
throw new Error(
`Unknown feature id ${id}. Features must be defined in toolkit/components/featuregates/Features.toml`
);
}
// Make a copy of the definition, since we are about to modify it
return buildFeatureGateImplementation({ ...featureDefinitions.get(id) });
}
/**
* Constructs feature gate objects for each of the definitions in ``Features.toml``.
* @param {string} testDefinitionsUrl A URL from which definitions can be fetched. Only use this in tests.
*/
static async all(testDefinitionsUrl = undefined) {
let featureDefinitions;
if (testDefinitionsUrl) {
featureDefinitions = await fetchFeatureDefinitions(testDefinitionsUrl);
} else {
featureDefinitions = await lazy.gFeatureDefinitionsPromise;
}
let definitions = [];
for (let definition of featureDefinitions.values()) {
// Make a copy of the definition, since we are about to modify it
definitions[definitions.length] = await buildFeatureGateImplementation(
Object.assign({}, definition)
);
}
return definitions;
}
static async observePrefChangesForCrashReportAnnotation(
testDefinitionsUrl = undefined
) {
let featureDefinitions = await FeatureGate.all(testDefinitionsUrl);
for (let definition of featureDefinitions.values()) {
FeatureGate.addObserver(
definition.id,
featureGatePrefObserver,
testDefinitionsUrl
);
}
}
static async annotateCrashReporter() {
if (!Services.appinfo.crashReporterEnabled) {
return;
}
let features = await FeatureGate.all();
let enabledFeatures = [];
for (let feature of features) {
if (await feature.getValue()) {
enabledFeatures.push(feature.preference);
}
}
Services.appinfo.annotateCrashReport(
"ExperimentalFeatures",
enabledFeatures.join(",")
);
}
/**
* Add an observer for a feature gate by ID. If the feature is of type
* boolean and currently enabled, `onEnable` will be called.
*
* The underlying feature gate instance will be shared with all other callers
* of this function, and share an observer.
*
* @param {string} id The ID of the feature's definition in `Features.toml`.
* @param {object} observer Functions to be called when the feature changes.
* All observer functions are optional.
* @param {Function()} [observer.onEnable] Called when the feature becomes enabled.
* @param {Function()} [observer.onDisable] Called when the feature becomes disabled.
* @param {Function(newValue)} [observer.onChange] Called when the
* feature's state changes to any value. The new value will be passed to the
* function.
* @param {string} testDefinitionsUrl A URL from which definitions can be fetched. Only use this in tests.
* @returns {Promise<boolean>} The current value of the feature.
*/
static async addObserver(id, observer, testDefinitionsUrl = undefined) {
if (!kFeatureGateCache.has(id)) {
kFeatureGateCache.set(
id,
await FeatureGate.fromId(id, testDefinitionsUrl)
);
}
const feature = kFeatureGateCache.get(id);
return feature.addObserver(observer);
}
/**
* Remove an observer of changes from this feature
* @param {string} id The ID of the feature's definition in `Features.toml`.
* @param observer Then observer that was passed to addObserver to remove.
*/
static async removeObserver(id, observer) {
let feature = kFeatureGateCache.get(id);
if (!feature) {
return;
}
feature.removeObserver(observer);
if (feature._observers.size === 0) {
kFeatureGateCache.delete(id);
}
}
/**
* Get the current value of this feature gate. Implementors should avoid
* storing the result to avoid missing changes to the feature's value.
* Consider using :func:`addObserver` if it is necessary to store the value
* of the feature.
*
* @async
* @param {string} id The ID of the feature's definition in `Features.toml`.
* @returns {Promise<boolean>} A promise for the value associated with this feature.
*/
static async getValue(id, testDefinitionsUrl = undefined) {
let feature = kFeatureGateCache.get(id);
if (!feature) {
feature = await FeatureGate.fromId(id, testDefinitionsUrl);
}
return feature.getValue();
}
/**
* An alias of `getValue` for boolean typed feature gates.
*
* @async
* @param {string} id The ID of the feature's definition in `Features.toml`.
* @returns {Promise<boolean>} A promise for the value associated with this feature.
* @throws {Error} If the feature is not a boolean.
*/
static async isEnabled(id, testDefinitionsUrl = undefined) {
let feature = kFeatureGateCache.get(id);
if (!feature) {
feature = await FeatureGate.fromId(id, testDefinitionsUrl);
}
return feature.isEnabled();
}
/**
* Take a jexl expression and evaluate it against the standard Nimbus
* context, extended with some additional properties defined in
* kCustomTargeting.
*
* @param {String} jexlExpression The expression to evaluate.
* @param {Object[]?} additionalContexts Any additional context properties
* that should be taken into account.
*
* @returns {Promise<boolean>} Resolves to either true or false if successful,
* or null if there was some problem with the jexl expression (which
* will also log an error to the console).
*/
static async evaluateJexlValue(jexlExpression, ...additionalContexts) {
let result = null;
let context = getCombinedContext(...additionalContexts);
try {
result = !!(await context.evalWithDefault(jexlExpression));
} catch (ex) {
console.error(ex);
}
return result;
}
}

View File

@@ -1,245 +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/. */
/** An individual feature gate that can be re-used for more advanced usage. */
export class FeatureGateImplementation {
// Note that the following comment is *not* a jsdoc. Making it a jsdoc would
// makes sphinx-js expose it to users. This feature shouldn't be used by
// users, and so should not be in the docs. Sphinx-js does not respect the
// @private marker on a constructor (https://github.com/erikrose/sphinx-js/issues/71).
/*
* This constructor should only be used directly in tests.
* ``FeatureGate.fromId`` should be used instead for most cases.
*
* @private
*
* @param {object} definition Description of the feature gate.
* @param {string} definition.id
* @param {string} definition.title
* @param {string} definition.description
* @param {string} definition.descriptionLinks
* @param {boolean} definition.restartRequired
* @param {string} definition.type
* @param {string} definition.preference
* @param {string} definition.defaultValue
* @param {object} definition.isPublic
* @param {object} definition.bugNumbers
*/
constructor(definition) {
this._definition = definition;
this._observers = new Set();
}
// The below are all getters instead of direct access to make it easy to provide JSDocs.
/**
* A short string used to refer to this feature in code.
* @type string
*/
get id() {
return this._definition.id;
}
/**
* A Fluent string ID that will resolve to some text to identify this feature's group to users.
* @type string
*/
get group() {
return this._definition.group;
}
/**
* A Fluent string ID that will resolve to some text to identify this feature to users.
* @type string
*/
get title() {
return this._definition.title;
}
/**
* A Fluent string ID that will resolve to a longer string to show to users that explains the feature.
* @type string
*/
get description() {
return this._definition.description;
}
get descriptionLinks() {
return this._definition.descriptionLinks;
}
/**
* Whether this feature requires a browser restart to take effect after toggling.
* @type boolean
*/
get restartRequired() {
return this._definition.restartRequired;
}
/**
* The type of feature. Currently only booleans are supported. This may be
* richer than JS types in the future, such as enum values.
* @type string
*/
get type() {
return this._definition.type;
}
/**
* The name of the preference that stores the value of this feature.
*
* This preference should not be read directly, but instead its values should
* be accessed via FeatureGate#addObserver or FeatureGate#getValue. This
* property is provided for backwards compatibility.
*
* @type string
*/
get preference() {
return this._definition.preference;
}
/**
* The default value for the feature gate for this update channel.
* @type boolean
*/
get defaultValue() {
return this._definition.defaultValue;
}
/**
* If this feature should be exposed to users in an advanced settings panel
* for this build of Firefox.
*
* @type boolean
*/
get isPublic() {
return this._definition.isPublic;
}
/**
* Bug numbers associated with this feature.
* @type Array<number>
*/
get bugNumbers() {
return this._definition.bugNumbers;
}
/**
* Get the current value of this feature gate. Implementors should avoid
* storing the result to avoid missing changes to the feature's value.
* Consider using :func:`addObserver` if it is necessary to store the value
* of the feature.
*
* @async
* @returns {Promise<boolean>} A promise for the value associated with this feature.
*/
// Note that this is async for potential future use of a storage backend besides preferences.
async getValue() {
return Services.prefs.getBoolPref(this.preference, this.defaultValue);
}
/**
* An alias of `getValue` for boolean typed feature gates.
*
* @async
* @returns {Promise<boolean>} A promise for the value associated with this feature.
* @throws {Error} If the feature is not a boolean.
*/
// Note that this is async for potential future use of a storage backend besides preferences.
async isEnabled() {
if (this.type !== "boolean") {
throw new Error(
`Tried to call isEnabled when type is not boolean (it is ${this.type})`
);
}
return this.getValue();
}
/**
* Add an observer for changes to this feature. When the observer is added,
* `onChange` will asynchronously be called with the current value of the
* preference. If the feature is of type boolean and currently enabled,
* `onEnable` will additionally be called.
*
* @param {object} observer Functions to be called when the feature changes.
* All observer functions are optional.
* @param {Function()} [observer.onEnable] Called when the feature becomes enabled.
* @param {Function()} [observer.onDisable] Called when the feature becomes disabled.
* @param {Function(newValue: boolean)} [observer.onChange] Called when the
* feature's state changes to any value. The new value will be passed to the
* function.
* @returns {Promise<boolean>} The current value of the feature.
*/
async addObserver(observer) {
if (this._observers.size === 0) {
Services.prefs.addObserver(this.preference, this);
}
this._observers.add(observer);
if (this.type === "boolean" && (await this.isEnabled())) {
this._callObserverMethod(observer, "onEnable");
}
// onDisable should not be called, because features should be assumed
// disabled until onEnabled is called for the first time.
return this.getValue();
}
/**
* Remove an observer of changes from this feature
* @param observer The observer that was passed to addObserver to remove.
*/
removeObserver(observer) {
this._observers.delete(observer);
if (this._observers.size === 0) {
Services.prefs.removeObserver(this.preference, this);
}
}
/**
* Removes all observers from this instance of the feature gate.
*/
removeAllObservers() {
if (this._observers.size > 0) {
this._observers.clear();
Services.prefs.removeObserver(this.preference, this);
}
}
_callObserverMethod(observer, method, ...args) {
if (method in observer) {
try {
observer[method](...args);
} catch (err) {
console.error(err);
}
}
}
/**
* Observes changes to the preference storing the enabled state of the
* feature. The observer is dynamically added only when observer have been
* added.
* @private
*/
async observe(aSubject, aTopic, aData) {
if (aTopic === "nsPref:changed" && aData === this.preference) {
const value = await this.getValue();
for (const observer of this._observers) {
this._callObserverMethod(observer, "onChange", value);
if (value) {
this._callObserverMethod(observer, "onEnable");
} else {
this._callObserverMethod(observer, "onDisable");
}
}
} else {
console.error(
new Error(`Unexpected event observed: ${aSubject}, ${aTopic}, ${aData}`)
);
}
}
}

View File

@@ -1,49 +0,0 @@
# is-public-jexl and default-value-jexl are both JEXL expressions.
# They are evaluated using the Experimenter environment, plus some
# additional variables defined by ASRouter and FeatureGate.
# The result of evaluating is-public-jexl is exposed as `isPublic`
# and controls whether the feature is shown to the user in e.g.
# the relevant pane in Firefox's settings.
# The result of evaluating default-value-jexl is exposed as
# `defaultValue` and should match the default value of the pref
# as set in the relevant .js prefs file or StaticPrefList.yaml.
#
# NOTE ON ORDERING
# The ordering of the groups and controls in about:preferences is
# determined by the ordering in this file. Groups will be ordered
# based on the order of appearance in this file. Settings within
# a group will also be determined by their order in this file.
[auto-pip]
group = "experimental-features-group-customize-browsing"
title = "experimental-features-auto-pip"
description = "experimental-features-auto-pip-description"
restart-required = false
preference = "media.videocontrols.picture-in-picture.enable-when-switching-tabs.enabled"
type = "boolean"
bug-numbers = [1647800]
is-public-jexl = "true"
default-value-jexl = "false"
[url-bar-ime-search]
group = "experimental-features-group-customize-browsing"
title = "experimental-features-ime-search"
description = "experimental-features-ime-search-description"
restart-required = false
preference = "browser.urlbar.keepPanelOpenDuringImeComposition"
type = "boolean"
bug-numbers = [1673971]
is-public-jexl = "true"
default-value-jexl = "false"
[media-jxl]
group = "experimental-features-group-webpage-display"
title = "experimental-features-media-jxl"
description = "experimental-features-media-jxl-description"
description-links = {bugzilla = "https://bugzilla.mozilla.org/show_bug.cgi?id=1539075"}
restart-required = false
preference = "image.jxl.enabled"
type = "boolean"
bug-numbers = [1539075]
is-public-jexl = "nightly_build"
default-value-jexl = "false"

View File

@@ -1,179 +0,0 @@
.. _components/featuregates:
=============
Feature Gates
=============
A feature gate is a high level tool to turn features on and off. It provides
metadata about features, a simple, opinionated API, and avoid many potential
pitfalls of other systems, such as using preferences directly. It is designed
to be compatible with tools that want to know and affect the state of
features in Firefox over time and in the wild.
Feature Definitions
===================
All features must have a definition, specified in
``toolkit/components/featuregates/Features.toml``. These definitions include
data such as references to title and description strings (to be shown to users),
and bug numbers (to track the development of the feature over time). Here is an
example feature definition with an id of ``demo-feature``:
.. code-block:: toml
[demo-feature]
title = "experimental-features-demo-feature"
description = "experimental-features-demo-feature-description"
restart-required = false
bug-numbers = [1479127]
type = "boolean"
is-public = {default = false, nightly = true}
default-value = {default = false, nightly = true}
The references defined in the `title` and `description` values point to strings
stored in ``toolkit/locales/en-US/toolkit/featuregates/features.ftl``. The `title`
string should specify the user-facing string as the `label` attribute.
.. _targeted value:
Targeted values
---------------
Several fields can take a value that indicates it varies by channel and OS.
These are known as *targeted values*. The simplest computed value is to
simply provide the value:
.. code-block:: toml
default-value = true
A more interesting example is to make a feature default to true on Nightly,
but be disabled otherwise. That would look like this:
.. code-block:: toml
default-value = {default = false, nightly = true}
Values can depend on multiple conditions. For example, to enable a feature
only on Nightly running on Windows:
.. code-block:: toml
default-value = {default = false, "nightly,win" = true}
Multiple sets of conditions can be specified, however use caution here: if
multiple sets could match (except ``default``), the set chosen is undefined.
An example of safely using multiple conditions:
.. code-block:: toml
default-value = {default = false, nightly = true, "beta,win" = true}
The ``default`` condition is required. It is used as a fallback in case no
more-specific case matches. The conditions allowed are
* ``default``
* ``release``
* ``beta``
* ``dev-edition``
* ``nightly``
* ``esr``
* ``win``
* ``mac``
* ``linux``
* ``android``
Fields
------
title
Required. The string ID of the human readable name for the feature, meant to be shown to
users. Should fit onto a single line. The actual string should be defined in
``toolkit/locales/en-US/toolkit/featuregates/features.ftl`` with the user-facing value
defined as the `label` attribute of the string.
description
Required. The string ID of the human readable description for the feature, meant to be shown to
users. Should be at most a paragraph. The actual string should be defined in
``toolkit/locales/en-US/toolkit/featuregates/features.ftl``.
description-links
Optional. A dictionary of key-value pairs that are referenced in the description. The key
name must appear in the description localization text as
<a data-l10n-name="key-name">. For example in Features.toml:
.. code-block:: toml
[demo-feature]
title = "experimental-features-demo-feature"
description = "experimental-features-demo-feature-description"
description-links = {exampleCom = "https://example.com", exampleOrg = "https://example.org"}
restart-required = false
bug-numbers = [1479127]
type = "boolean"
is-public = {default = false, nightly = true}
default-value = {default = false, nightly = true}
and in features.ftl:
.. code-block:: fluent
experimental-features-demo-feature =
.label = Example Demo Feature
experimental-features-demo-feature-description = Example demo feature that can point to <a data-l10n-name="exampleCom">.com</a> links and <a data-l10n-name="exampleOrg">.org</a> links.
bug-numbers
Required. A list of bug numbers related to this feature. This should
likely be the metabug for the the feature, but any related bugs can be
included. At least one bug is required.
restart-required
Required. If this feature requires a the browser to be restarted for changes
to take effect, this field should be ``true``. Otherwise, the field should
be ``false``. Features should aspire to not require restarts and react to
changes to the preference dynamically.
type
Required. The type of value this feature relates to. The only legal value is
``boolean``, but more may be added in the future.
preference
Optional. The preference used to track the feature. If a preference is not
provided, one will be automatically generated based on the feature ID. It is
not recommended to specify a preference directly, except to integrate with
older code. In the future, alternate storage mechanisms may be used if a
preference is not supplied.
default-value
Optional. This is a `targeted value`_ describing
the value for the feature if no other changes have been made, such as in
a fresh profile. If not provided, the default for a boolean type feature
gate will be ``false`` for all profiles.
is-public
Optional. This is a `targeted value`_ describing
on which branches this feature should be exposed to users. When a feature
is made public, it may show up in a future UI that allows users to opt-in
to experimental features. This is not related to ``about:preferences`` or
``about:config``. If not provided, the default is to make a feature
private for all channels.
Feature Gate API
================
..
(comment) The below lists should be kept in sync with the contents of the
classes they are documenting. An explicit list is used so that the
methods can be put in a particular order.
.. js:autoclass:: FeatureGate
:members: addObserver, removeObserver, isEnabled, fromId
.. js:autoclass:: FeatureGateImplementation
:members: id, title, description, type, bugNumbers, isPublic, defaultValue, restartRequired, preference, addObserver, removeObserver, removeAllObservers, getValue, isEnabled
Feature implementors should use the methods :func:`fromId`,
:func:`addListener`, :func:`removeListener` and
:func:`removeAllListeners`. Additionally, metadata is available for UI and
analysis.

View File

@@ -1,165 +0,0 @@
#!/usr/bin/env python
# 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/.
import json
import re
import sys
import toml
import voluptuous
import voluptuous.humanize
from voluptuous import Schema, Optional, Any, All, Required, Length, Range, Msg, Match
Text = Any(str, bytes)
id_regex = re.compile(r"^[a-z0-9-]+$")
feature_schema = Schema(
{
Match(id_regex): {
Required("group"): All(Text, Length(min=1)),
Required("title"): All(Text, Length(min=1)),
Required("description"): All(Text, Length(min=1)),
Required("bug-numbers"): All(Length(min=1), [All(int, Range(min=1))]),
Required("restart-required"): bool,
Required("type"): "boolean", # In the future this may include other types
Optional("preference"): Text,
Optional("default-value-jexl"): Text,
Optional("is-public-jexl"): Text,
Optional("description-links"): dict,
},
}
)
EXIT_OK = 0
EXIT_ERROR = 1
def main(output, *filenames):
features = {}
errors = False
try:
features = process_files(filenames)
json.dump(features, output)
except ExceptionGroup as error_group:
print(str(error_group))
return EXIT_ERROR
return EXIT_OK
class ExceptionGroup(Exception):
def __init__(self, errors):
self.errors = errors
def __str__(self):
rv = ["There were errors while processing feature definitions:"]
for error in self.errors:
# indent the message
s = "\n".join(" " + line for line in str(error).split("\n"))
# add a * at the beginning of the first line
s = " * " + s[4:]
rv.append(s)
return "\n".join(rv)
class FeatureGateException(Exception):
def __init__(self, message, filename=None):
super(FeatureGateException, self).__init__(message)
self.filename = filename
def __str__(self):
message = super(FeatureGateException, self).__str__()
rv = ["In"]
if self.filename is None:
rv.append("unknown file:")
else:
rv.append('file "{}":\n'.format(self.filename))
rv.append(message)
return " ".join(rv)
def __repr__(self):
# Turn "FeatureGateExcept(<message>,)" into "FeatureGateException(<message>, filename=<filename>)"
original = super(FeatureGateException, self).__repr__()
with_comma = original[:-1]
# python 2 adds a trailing comma and python 3 does not, so we need to conditionally reinclude it
if len(with_comma) > 0 and with_comma[-1] != ",":
with_comma = with_comma + ","
return with_comma + " filename={!r})".format(self.filename)
def process_files(filenames):
features = {}
errors = []
for filename in filenames:
try:
with open(filename, "r") as f:
feature_data = toml.load(f)
voluptuous.humanize.validate_with_humanized_errors(
feature_data, feature_schema
)
for feature_id, feature in feature_data.items():
feature["id"] = feature_id
features[feature_id] = expand_feature(feature)
except (
voluptuous.error.Error,
IOError,
FeatureGateException,
toml.TomlDecodeError,
) as e:
# Wrap errors in enough information to know which file they came from
errors.append(FeatureGateException(e, filename))
if errors:
raise ExceptionGroup(errors)
return features
def hyphens_to_camel_case(s):
"""Convert names-with-hyphens to namesInCamelCase"""
rv = ""
for part in s.split("-"):
if rv == "":
rv = part.lower()
else:
rv += part[0].upper() + part[1:].lower()
return rv
def expand_feature(feature):
"""Fill in default values for optional fields"""
# convert all names-with-hyphens to namesInCamelCase
key_changes = []
for key in feature.keys():
if "-" in key:
new_key = hyphens_to_camel_case(key)
key_changes.append((key, new_key))
for old_key, new_key in key_changes:
feature[new_key] = feature[old_key]
del feature[old_key]
if feature["type"] == "boolean":
feature.setdefault("preference", "features.{}.enabled".format(feature["id"]))
# set default value to None so that we can test for preferences where we forgot to set the default value
feature.setdefault("defaultValueJexl", None)
elif "preference" not in feature:
raise FeatureGateException(
"Features of type {} must specify an explicit preference name".format(
feature["type"]
)
)
feature.setdefault("isPublicJexl", "false")
return feature
if __name__ == "__main__":
sys.exit(main(sys.stdout, *sys.argv[1:]))

View File

@@ -1,9 +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/.
toolkit.jar:
% resource featuregates %featuregates/
featuregates/FeatureGate.sys.mjs (./FeatureGate.sys.mjs)
featuregates/FeatureGateImplementation.sys.mjs (./FeatureGateImplementation.sys.mjs)
featuregates/feature_definitions.json (./feature_definitions.json)

View File

@@ -1,20 +0,0 @@
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# 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/.
with Files("**"):
BUG_COMPONENT = ("Toolkit", "FeatureGate")
SPHINX_TREES["featuregates"] = "docs"
XPCSHELL_TESTS_MANIFESTS += ["test/unit/xpcshell.toml"]
JAR_MANIFESTS += ["jar.mn"]
GeneratedFile(
"feature_definitions.json",
script="gen_feature_definitions.py",
inputs=["Features.toml"],
)

View File

@@ -1 +0,0 @@
[empty-feature]

View File

@@ -1,18 +0,0 @@
[demo-feature]
title = "demo-feature-title"
group = "demo-feature-group"
description = "demo-feature-description"
restart-required = false
preference = "foo.bar.baz"
type = "boolean"
bug-numbers = [1479127]
is-public-jexl = "true"
default-value-jexl = "false"
[minimal-feature]
title = "minimal-feature-title"
group = "minimal-feature-group"
description = "minimal-feature-description"
restart-required = true
type = "boolean"
bug-numbers = [1479127]

View File

@@ -1 +0,0 @@
this: is: not: valid: toml

View File

@@ -1,4 +0,0 @@
[DEFAULT]
subsuite = "featuregates"
["test_gen_feature_definitions.py"]

View File

@@ -1,281 +0,0 @@
import json
import sys
import unittest
from os import path
from textwrap import dedent
import mozunit
import toml
import voluptuous
from io import StringIO
FEATURE_GATES_ROOT_PATH = path.abspath(
path.join(path.dirname(__file__), path.pardir, path.pardir)
)
sys.path.append(FEATURE_GATES_ROOT_PATH)
from gen_feature_definitions import (
ExceptionGroup,
expand_feature,
feature_schema,
FeatureGateException,
hyphens_to_camel_case,
main,
process_files,
)
def make_test_file_path(name):
return path.join(FEATURE_GATES_ROOT_PATH, "test", "python", "data", name + ".toml")
def minimal_definition(**kwargs):
defaults = {
"id": "test-feature",
"title": "Test Feature",
"group": "Test Feature Group",
"description": "A feature for testing things",
"bug-numbers": [1479127],
"restart-required": False,
"type": "boolean",
}
defaults.update(dict([(k.replace("_", "-"), v) for k, v in kwargs.items()]))
return defaults
class TestHyphensToCamelCase(unittest.TestCase):
simple_cases = [
("", ""),
("singleword", "singleword"),
("more-than-one-word", "moreThanOneWord"),
]
def test_simple_cases(self):
for in_string, out_string in self.simple_cases:
assert hyphens_to_camel_case(in_string) == out_string
class TestExceptionGroup(unittest.TestCase):
def test_str_indentation_of_grouped_lines(self):
errors = [
Exception("single line error 1"),
Exception("single line error 2"),
Exception("multiline\nerror 1"),
Exception("multiline\nerror 2"),
]
assert str(ExceptionGroup(errors)) == dedent(
"""\
There were errors while processing feature definitions:
* single line error 1
* single line error 2
* multiline
error 1
* multiline
error 2"""
)
class TestFeatureGateException(unittest.TestCase):
def test_str_no_file(self):
error = FeatureGateException("oops")
assert str(error) == "In unknown file: oops"
def test_str_with_file(self):
error = FeatureGateException("oops", filename="some/bad/file.txt")
assert str(error) == 'In file "some/bad/file.txt":\n oops'
def test_repr_no_file(self):
error = FeatureGateException("oops")
assert repr(error) == "FeatureGateException('oops', filename=None)"
def test_repr_with_file(self):
error = FeatureGateException("oops", filename="some/bad/file.txt")
assert (
repr(error) == "FeatureGateException('oops', filename='some/bad/file.txt')"
)
class TestProcessFiles(unittest.TestCase):
def test_valid_file(self):
filename = make_test_file_path("good")
result = process_files([filename])
assert result == {
"demo-feature": {
"id": "demo-feature",
"title": "demo-feature-title",
"group": "demo-feature-group",
"description": "demo-feature-description",
"restartRequired": False,
"preference": "foo.bar.baz",
"type": "boolean",
"bugNumbers": [1479127],
"isPublicJexl": "true",
"defaultValueJexl": "false",
},
"minimal-feature": {
"id": "minimal-feature",
"title": "minimal-feature-title",
"group": "minimal-feature-group",
"description": "minimal-feature-description",
"restartRequired": True,
"preference": "features.minimal-feature.enabled",
"type": "boolean",
"bugNumbers": [1479127],
"isPublicJexl": "false",
"defaultValueJexl": None,
},
}
def test_invalid_toml(self):
filename = make_test_file_path("invalid_toml")
with self.assertRaises(ExceptionGroup) as context:
process_files([filename])
error_group = context.exception
assert len(error_group.errors) == 1
assert type(error_group.errors[0]) == FeatureGateException
def test_empty_feature(self):
filename = make_test_file_path("empty_feature")
with self.assertRaises(ExceptionGroup) as context:
process_files([filename])
error_group = context.exception
assert len(error_group.errors) == 1
assert type(error_group.errors[0]) == FeatureGateException
assert "required key not provided" in str(error_group.errors[0])
def test_missing_file(self):
filename = make_test_file_path("file_does_not_exist")
with self.assertRaises(ExceptionGroup) as context:
process_files([filename])
error_group = context.exception
assert len(error_group.errors) == 1
assert type(error_group.errors[0]) == FeatureGateException
assert "No such file or directory" in str(error_group.errors[0])
class TestFeatureSchema(unittest.TestCase):
def make_test_features(self, *overrides):
if len(overrides) == 0:
overrides = [{}]
features = {}
for override in overrides:
feature = minimal_definition(**override)
feature_id = feature.pop("id")
features[feature_id] = feature
return features
def test_minimal_valid(self):
definition = self.make_test_features()
# should not raise an exception
feature_schema(definition)
def test_extra_keys_not_allowed(self):
definition = self.make_test_features({"unexpected_key": "oh no!"})
with self.assertRaises(voluptuous.Error) as context:
feature_schema(definition)
assert "extra keys not allowed" in str(context.exception)
def test_required_fields(self):
required_keys = [
"title",
"description",
"bug-numbers",
"restart-required",
"type",
]
for key in required_keys:
definition = self.make_test_features({"id": "test-feature"})
del definition["test-feature"][key]
with self.assertRaises(voluptuous.Error) as context:
feature_schema(definition)
assert "required key not provided" in str(context.exception)
assert key in str(context.exception)
def test_nonempty_keys(self):
test_parameters = [("title", ""), ("description", ""), ("bug-numbers", [])]
for key, empty in test_parameters:
definition = self.make_test_features({key: empty})
with self.assertRaises(voluptuous.Error) as context:
feature_schema(definition)
assert "length of value must be at least" in str(context.exception)
assert "['{}']".format(key) in str(context.exception)
class ExpandFeatureTests(unittest.TestCase):
def test_hyphenation_to_snake_case(self):
feature = minimal_definition()
assert "bug-numbers" in feature
assert "bugNumbers" in expand_feature(feature)
def test_default_value_default(self):
feature = minimal_definition(type="boolean")
assert "default-value" not in feature
assert "defaultValue" not in feature
assert "default-value-jexl" not in feature
assert "defaultValueJexl" not in feature
assert expand_feature(feature)["defaultValueJexl"] == None
def test_default_value_override_constant(self):
feature = minimal_definition(type="boolean", default_value_jexl="true")
assert expand_feature(feature)["defaultValueJexl"] == "true"
def test_default_value_override_configured_value(self):
feature = minimal_definition(
type="boolean", default_value_jexl="channel == nightly"
)
assert expand_feature(feature)["defaultValueJexl"] == "channel == nightly"
def test_preference_default(self):
feature = minimal_definition(type="boolean")
assert "preference" not in feature
assert expand_feature(feature)["preference"] == "features.test-feature.enabled"
def test_preference_override(self):
feature = minimal_definition(preference="test.feature.a")
assert expand_feature(feature)["preference"] == "test.feature.a"
class MainTests(unittest.TestCase):
def test_it_outputs_json(self):
output = StringIO()
filename = make_test_file_path("good")
main(output, filename)
output.seek(0)
results = json.load(output)
assert results == {
"demo-feature": {
"id": "demo-feature",
"title": "demo-feature-title",
"group": "demo-feature-group",
"description": "demo-feature-description",
"restartRequired": False,
"preference": "foo.bar.baz",
"type": "boolean",
"bugNumbers": [1479127],
"isPublicJexl": "true",
"defaultValueJexl": "false",
},
"minimal-feature": {
"id": "minimal-feature",
"title": "minimal-feature-title",
"group": "minimal-feature-group",
"description": "minimal-feature-description",
"restartRequired": True,
"preference": "features.minimal-feature.enabled",
"type": "boolean",
"bugNumbers": [1479127],
"isPublicJexl": "false",
"defaultValueJexl": None,
},
}
def test_it_returns_1_for_errors(self):
output = StringIO()
filename = make_test_file_path("invalid_toml")
assert main(output, filename) == 1
assert output.getvalue() == ""
if __name__ == "__main__":
mozunit.main(*sys.argv[1:])

View File

@@ -1,3 +0,0 @@
var { sinon } = ChromeUtils.importESModule(
"resource://testing-common/Sinon.sys.mjs"
);

View File

@@ -1,412 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const { FeatureGate } = ChromeUtils.importESModule(
"resource://featuregates/FeatureGate.sys.mjs"
);
const { HttpServer } = ChromeUtils.importESModule(
"resource://testing-common/httpd.sys.mjs"
);
const { AppConstants } = ChromeUtils.importESModule(
"resource://gre/modules/AppConstants.sys.mjs"
);
const kDefinitionDefaults = {
id: "test-feature",
title: "Test Feature",
description: "A feature for testing",
restartRequired: false,
type: "boolean",
preference: "test.feature",
defaultValue: false,
isPublic: false,
};
function definitionFactory(override = {}) {
return Object.assign({}, kDefinitionDefaults, override);
}
class DefinitionServer {
constructor(definitionOverrides = []) {
this.server = new HttpServer();
this.server.registerPathHandler("/definitions.json", this);
this.definitions = {};
for (const override of definitionOverrides) {
this.addDefinition(override);
}
this.server.start();
registerCleanupFunction(
() => new Promise(resolve => this.server.stop(resolve))
);
}
// for nsIHttpRequestHandler
handle(request, response) {
// response.setHeader("Content-Type", "application/json");
response.write(JSON.stringify(this.definitions));
}
get definitionsUrl() {
const { primaryScheme, primaryHost, primaryPort } = this.server.identity;
return `${primaryScheme}://${primaryHost}:${primaryPort}/definitions.json`;
}
addDefinition(overrides = {}) {
const definition = definitionFactory(overrides);
// convert targeted values, used by fromId
definition.isPublicJexl = definition.isPublic ? "!testFact" : "testFact";
definition.defaultValueJexl = definition.defaultValue
? "!testFact"
: "testFact";
this.definitions[definition.id] = definition;
return definition;
}
}
// ============================================================================
add_task(async function testReadAll() {
const server = new DefinitionServer();
let ids = ["test-featureA", "test-featureB", "test-featureC"];
for (let id of ids) {
server.addDefinition({ id });
}
let sortedIds = ids.sort();
const features = await FeatureGate.all(server.definitionsUrl);
for (let feature of features) {
equal(
feature.id,
sortedIds.shift(),
"Features are returned in order of definition"
);
}
equal(sortedIds.length, 0, "All features are returned when calling all()");
});
// The getters and setters should read correctly from the definition
add_task(async function testReadFromDefinition() {
const server = new DefinitionServer();
const definition = server.addDefinition({ id: "test-feature" });
const feature = await FeatureGate.fromId(
"test-feature",
server.definitionsUrl
);
// simple fields
equal(feature.id, definition.id, "id should be read from definition");
equal(
feature.title,
definition.title,
"title should be read from definition"
);
equal(
feature.description,
definition.description,
"description should be read from definition"
);
equal(
feature.restartRequired,
definition.restartRequired,
"restartRequired should be read from definition"
);
equal(feature.type, definition.type, "type should be read from definition");
equal(
feature.preference,
definition.preference,
"preference should be read from definition"
);
// targeted fields
equal(
feature.defaultValue,
false,
"defaultValue should be false because testFact was not provided."
);
equal(
feature.isPublic,
false,
"isPublic should be false because testFact was not provided."
);
// cleanup
Services.prefs.getDefaultBranch("").deleteBranch("test.feature");
});
// Targeted values should return the correct value
add_task(async function testTargetedValues() {
const targetingFacts = {
true1: true,
true2: true,
false1: false,
false2: false,
};
Assert.equal(
await FeatureGate.evaluateJexlValue("true1", targetingFacts),
true,
"A true value should be reflected"
);
Assert.equal(
await FeatureGate.evaluateJexlValue("false1", targetingFacts),
false,
"A false value should be reflected"
);
Assert.equal(
await FeatureGate.evaluateJexlValue("false"),
false,
"Boolean literal false should work"
);
Assert.equal(
await FeatureGate.evaluateJexlValue("true"),
true,
"Boolean literal true should work"
);
Assert.equal(
await FeatureGate.evaluateJexlValue("false2 || true1", targetingFacts),
true,
"Compound expressions work."
);
Assert.equal(
await FeatureGate.evaluateJexlValue("testFact", {}),
false,
"Non-existing terms in the expression get coerced to bool false."
);
Assert.equal(
await FeatureGate.evaluateJexlValue("testFact", { testFact: true }),
true,
"Providing testFact=true, the expression returns true."
);
});
// getValue should work
add_task(async function testGetValue() {
equal(
Services.prefs.getPrefType("test.feature.1"),
Services.prefs.PREF_INVALID,
"Before creating the feature gate, the preference should not exist"
);
const server = new DefinitionServer([
{ id: "test-feature-1", defaultValue: false, preference: "test.feature.1" },
{ id: "test-feature-2", defaultValue: true, preference: "test.feature.2" },
]);
equal(
await FeatureGate.getValue("test-feature-1", server.definitionsUrl),
false,
"getValue() starts by returning the default value"
);
equal(
await FeatureGate.getValue("test-feature-2", server.definitionsUrl),
true,
"getValue() starts by returning the default value"
);
Services.prefs.setBoolPref("test.feature.1", true);
equal(
await FeatureGate.getValue("test-feature-1", server.definitionsUrl),
true,
"getValue() return the new value"
);
Services.prefs.setBoolPref("test.feature.1", false);
equal(
await FeatureGate.getValue("test-feature-1", server.definitionsUrl),
false,
"getValue() should return the second value"
);
// cleanup
Services.prefs.getDefaultBranch("").deleteBranch("test.feature.");
});
// getValue should work
add_task(async function testGetValue() {
const server = new DefinitionServer([
{ id: "test-feature-1", defaultValue: false, preference: "test.feature.1" },
{ id: "test-feature-2", defaultValue: true, preference: "test.feature.2" },
]);
equal(
Services.prefs.getPrefType("test.feature.1"),
Services.prefs.PREF_INVALID,
"Before creating the feature gate, the first preference should not exist"
);
equal(
Services.prefs.getPrefType("test.feature.2"),
Services.prefs.PREF_INVALID,
"Before creating the feature gate, the second preference should not exist"
);
equal(
await FeatureGate.isEnabled("test-feature-1", server.definitionsUrl),
false,
"isEnabled() starts by returning the default value"
);
equal(
await FeatureGate.isEnabled("test-feature-2", server.definitionsUrl),
true,
"isEnabled() starts by returning the default value"
);
Services.prefs.setBoolPref("test.feature.1", true);
equal(
await FeatureGate.isEnabled("test-feature-1", server.definitionsUrl),
true,
"isEnabled() return the new value"
);
Services.prefs.setBoolPref("test.feature.1", false);
equal(
await FeatureGate.isEnabled("test-feature-1", server.definitionsUrl),
false,
"isEnabled() should return the second value"
);
// cleanup
Services.prefs.getDefaultBranch("").deleteBranch("test.feature.");
});
// adding and removing event observers should work
add_task(async function testGetValue() {
const preference = "test.pref";
const server = new DefinitionServer([
{ id: "test-feature", defaultValue: false, preference },
]);
const observer = {
onChange: sinon.stub(),
onEnable: sinon.stub(),
onDisable: sinon.stub(),
};
let rv = await FeatureGate.addObserver(
"test-feature",
observer,
server.definitionsUrl
);
equal(rv, false, "addObserver returns the current value");
Assert.deepEqual(observer.onChange.args, [], "onChange should not be called");
Assert.deepEqual(observer.onEnable.args, [], "onEnable should not be called");
Assert.deepEqual(
observer.onDisable.args,
[],
"onDisable should not be called"
);
Services.prefs.setBoolPref(preference, true);
await Promise.resolve(); // Allow events to be called async
Assert.deepEqual(
observer.onChange.args,
[[true]],
"onChange should be called with the new value"
);
Assert.deepEqual(observer.onEnable.args, [[]], "onEnable should be called");
Assert.deepEqual(
observer.onDisable.args,
[],
"onDisable should not be called"
);
Services.prefs.setBoolPref(preference, false);
await Promise.resolve(); // Allow events to be called async
Assert.deepEqual(
observer.onChange.args,
[[true], [false]],
"onChange should be called again with the new value"
);
Assert.deepEqual(
observer.onEnable.args,
[[]],
"onEnable should not be called a second time"
);
Assert.deepEqual(
observer.onDisable.args,
[[]],
"onDisable should be called for the first time"
);
Services.prefs.setBoolPref(preference, false);
await Promise.resolve(); // Allow events to be called async
Assert.deepEqual(
observer.onChange.args,
[[true], [false]],
"onChange should not be called if the value did not change"
);
Assert.deepEqual(
observer.onEnable.args,
[[]],
"onEnable should not be called again if the value did not change"
);
Assert.deepEqual(
observer.onDisable.args,
[[]],
"onDisable should not be called if the value did not change"
);
// remove the listener and make sure the observer isn't called again
FeatureGate.removeObserver("test-feature", observer);
await Promise.resolve(); // Allow events to be called async
Services.prefs.setBoolPref(preference, true);
await Promise.resolve(); // Allow events to be called async
Assert.deepEqual(
observer.onChange.args,
[[true], [false]],
"onChange should not be called after observer was removed"
);
Assert.deepEqual(
observer.onEnable.args,
[[]],
"onEnable should not be called after observer was removed"
);
Assert.deepEqual(
observer.onDisable.args,
[[]],
"onDisable should not be called after observer was removed"
);
// cleanup
Services.prefs.getDefaultBranch("").deleteBranch(preference);
});
if (AppConstants.platform != "android") {
// All preferences should have default values.
add_task(async function testAllHaveDefault() {
const featuresList = await FeatureGate.all();
for (let feature of featuresList) {
notEqual(
typeof feature.defaultValue,
"undefined",
`Feature ${feature.id} should have a defined default value!`
);
notEqual(
feature.defaultValue,
null,
`Feature ${feature.id} should have a non-null default value!`
);
}
});
// All preference defaults should match service pref defaults
add_task(async function testAllDefaultsMatchSettings() {
const featuresList = await FeatureGate.all();
for (let feature of featuresList) {
let value = Services.prefs
.getDefaultBranch("")
.getBoolPref(feature.preference);
equal(
feature.defaultValue,
value,
`Feature ${feature.preference} should match runtime value.`
);
}
});
}

View File

@@ -1,141 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const { FeatureGateImplementation } = ChromeUtils.importESModule(
"resource://featuregates/FeatureGateImplementation.sys.mjs"
);
const kDefinitionDefaults = {
id: "test-feature",
title: "Test Feature",
description: "A feature for testing",
restartRequired: false,
type: "boolean",
preference: "test.feature",
defaultValue: false,
isPublic: false,
};
function definitionFactory(override = {}) {
return Object.assign({}, kDefinitionDefaults, override);
}
// getValue should work
add_task(async function testGetValue() {
const preference = "test.pref";
equal(
Services.prefs.getPrefType(preference),
Services.prefs.PREF_INVALID,
"Before creating the feature gate, the preference should not exist"
);
const feature = new FeatureGateImplementation(
definitionFactory({ preference, defaultValue: false })
);
equal(
Services.prefs.getPrefType(preference),
Services.prefs.PREF_INVALID,
"Instantiating a feature gate should not set its default value"
);
equal(
await feature.getValue(),
false,
"getValue() should return the feature gate's default"
);
Services.prefs.setBoolPref(preference, true);
equal(
await feature.getValue(),
true,
"getValue() should return the new value"
);
Services.prefs.setBoolPref(preference, false);
equal(
await feature.getValue(),
false,
"getValue() should return the third value"
);
// cleanup
Services.prefs.getDefaultBranch("").deleteBranch(preference);
});
// event observers should work
add_task(async function testGetValue() {
const preference = "test.pref";
const feature = new FeatureGateImplementation(
definitionFactory({ preference, defaultValue: false })
);
const observer = {
onChange: sinon.stub(),
onEnable: sinon.stub(),
onDisable: sinon.stub(),
};
let rv = await feature.addObserver(observer);
equal(rv, false, "addObserver returns the current value");
Assert.deepEqual(observer.onChange.args, [], "onChange should not be called");
Assert.deepEqual(observer.onEnable.args, [], "onEnable should not be called");
Assert.deepEqual(
observer.onDisable.args,
[],
"onDisable should not be called"
);
Services.prefs.setBoolPref(preference, true);
await Promise.resolve(); // Allow events to be called async
Assert.deepEqual(
observer.onChange.args,
[[true]],
"onChange should be called with the new value"
);
Assert.deepEqual(observer.onEnable.args, [[]], "onEnable should be called");
Assert.deepEqual(
observer.onDisable.args,
[],
"onDisable should not be called"
);
Services.prefs.setBoolPref(preference, false);
await Promise.resolve(); // Allow events to be called async
Assert.deepEqual(
observer.onChange.args,
[[true], [false]],
"onChange should be called again with the new value"
);
Assert.deepEqual(
observer.onEnable.args,
[[]],
"onEnable should not be called a second time"
);
Assert.deepEqual(
observer.onDisable.args,
[[]],
"onDisable should be called for the first time"
);
Services.prefs.setBoolPref(preference, false);
await Promise.resolve(); // Allow events to be called async
Assert.deepEqual(
observer.onChange.args,
[[true], [false]],
"onChange should not be called if the value did not change"
);
Assert.deepEqual(
observer.onEnable.args,
[[]],
"onEnable should not be called again if the value did not change"
);
Assert.deepEqual(
observer.onDisable.args,
[[]],
"onDisable should not be called if the value did not change"
);
// cleanup
feature.removeAllObservers();
Services.prefs.getDefaultBranch("").deleteBranch(preference);
});

View File

@@ -1,12 +0,0 @@
[DEFAULT]
head = "head.js"
tags = "featuregates"
firefox-appdir = "browser"
["test_FeatureGate.js"]
# Ignore platforms which the use the update channel 'default' on non-nightly
# platforms because it gets compared to preference values guarded by variables
# like RELEASE_OR_BETA which are set based on the build channel.
skip-if = ["!nightly_build && (asan || debug)"]
["test_FeatureGateImplementation.js"]

View File

@@ -134,7 +134,7 @@ if CONFIG["MOZ_WIDGET_TOOLKIT"] not in ("android", "windows"):
DIRS += ["aboutwebauthn"]
if CONFIG["MOZ_BUILD_APP"] == "browser":
DIRS += ["featuregates", "messaging-system", "normandy"]
DIRS += ["messaging-system", "normandy"]
DIRS += ["nimbus"]

View File

@@ -68,7 +68,6 @@ Structure:
CrashTime: <time>, // Seconds since the Epoch
DOMFissionEnabled: "1", // Optional, if set indicates that a Fission window had been opened
EventLoopNestingLevel: <levels>, // Optional, present only if >0, indicates the nesting level of the event-loop
ExperimentalFeatures: <features>, // Optional, a comma-separated string that specifies the enabled experimental features from about:preferences#experimental
FontName: <name>, // Optional, the font family name that is being loaded when the crash occurred
GPUProcessLaunchCount: <num>, // Number of times the GPU process was launched
HeadlessMode: "1", // Optional, "1" if the app was invoked in headless mode via `--headless ...` or `--backgroundtask ...`
@@ -268,3 +267,4 @@ Version History
- Firefox 107: Added UtilityActorsName (`bug 1788596 <https://bugzilla.mozilla.org/show_bug.cgi?id=1788596>`_).
- Firefox 119: Added WindowsFileDialogErrorCode (`bug 1837079 <https://bugzilla.mozilla.org/show_bug.cgi?id=1837079>`_)
- Firefox 137: Added NimbusEnrollments (`bug 1950661 <https://bugzilla.mozilla.org/show_bug.cgi?id=1950661>`_).
- Firefox 138: Removed ExperimentalFeatures (`bug 1942694 <https://bugzilla.mozilla.org/show_bug.cgi?id=1942694>`_).

View File

@@ -381,33 +381,6 @@ var snapshotFormatters = {
}
},
async experimentalFeatures(data) {
if (!data) {
return;
}
let titleL10nIds = data.map(([titleL10nId]) => titleL10nId);
let titleL10nObjects = await document.l10n.formatMessages(titleL10nIds);
if (titleL10nObjects.length != data.length) {
throw Error("Missing localized title strings in experimental features");
}
for (let i = 0; i < titleL10nObjects.length; i++) {
let localizedTitle = titleL10nObjects[i].attributes.find(
a => a.name == "label"
).value;
data[i] = [localizedTitle, data[i][1], data[i][2]];
}
$.append(
$("experimental-features-tbody"),
data.map(function ([title, pref, value]) {
return $.new("tr", [
$.new("td", `${title} (${pref})`, "pref-name"),
$.new("td", value, "pref-value"),
]);
})
);
},
environmentVariables(data) {
if (!data) {
return;

View File

@@ -28,9 +28,6 @@
<link rel="localization" href="toolkit/about/aboutSupport.ftl"/>
<link rel="localization" href="toolkit/global/resetProfile.ftl"/>
<link rel="localization" href="toolkit/global/processTypes.ftl"/>
#ifndef ANDROID
<link rel="localization" href="toolkit/featuregates/features.ftl"/>
#endif
</head>
<body class="wide-container">
@@ -586,24 +583,6 @@
</tbody>
</table>
#ifndef ANDROID
<!-- - - - - - - - - - - - - - - - - - - - - -->
<h2 class="major-section" id="experimental-features" data-l10n-id="experimental-features-title"/>
<table class="prefs-table">
<thead class="no-copy">
<th class="name" data-l10n-id="experimental-features-name"/>
<th class="value" data-l10n-id="experimental-features-value"/>
</thead>
<tbody id="experimental-features-tbody">
</tbody>
</table>
#endif
<!-- - - - - - - - - - - - - - - - - - - - - -->
<h2 class="major-section" id="remote-settings" data-l10n-id="support-remote-settings-title"/>

View File

@@ -326,12 +326,6 @@ EventLoopNestingLevel:
ping: true
skip_if: "0"
ExperimentalFeatures:
description: >
Comma-separated list of enabled experimental features from about:preferences#experimental.
type: string
ping: true
FontName:
description: >
Set before attempting to load a font to help diagnose crashes during loading.

View File

@@ -111,9 +111,8 @@ pub fn set_crash_ping_metrics(
user32_loaded_before: bool = "User32BeforeBlocklist"
}
environment {
experimental_features: (string_list ',') = "ExperimentalFeatures"
nimbus_enrollments: (string_list ',') = "NimbusEnrollments"
headless_mode: bool = "HeadlessMode"
nimbus_enrollments: (string_list ',') = "NimbusEnrollments"
uptime: seconds = "UptimeTS"
}
memory {

View File

@@ -12,7 +12,6 @@ This is the nascent documentation of the Toolkit code that is shared across Fire
components/backgroundtasks/index
components/crashes/crash-manager/index
crashreporter/crashreporter/index
components/featuregates/featuregates/index
internal-urls
search/index
components/normandy/normandy/index

View File

@@ -97,9 +97,6 @@ show-dir-label =
environment-variables-title = Environment Variables
environment-variables-name = Name
environment-variables-value = Value
experimental-features-title = Experimental Features
experimental-features-name = Name
experimental-features-value = Value
modified-key-prefs-title = Important Modified Preferences
modified-prefs-name = Name
modified-prefs-value = Value

View File

@@ -450,27 +450,6 @@ var dataProviders = {
done(data);
},
async experimentalFeatures(done) {
if (AppConstants.MOZ_BUILD_APP != "browser") {
done();
return;
}
let { FeatureGate } = ChromeUtils.importESModule(
"resource://featuregates/FeatureGate.sys.mjs"
);
let gates = await FeatureGate.all();
done(
gates.map(gate => {
return [
gate.title,
gate.preference,
Services.prefs.getBoolPref(gate.preference),
];
})
);
},
async legacyUserStylesheets(done) {
if (AppConstants.platform == "android") {
done({ active: false, types: [] });

View File

@@ -13,9 +13,6 @@ const { sinon } = ChromeUtils.importESModule(
"resource://testing-common/Sinon.sys.mjs"
);
const { FeatureGate } = ChromeUtils.importESModule(
"resource://featuregates/FeatureGate.sys.mjs"
);
const { PreferenceExperiments } = ChromeUtils.importESModule(
"resource://normandy/lib/PreferenceExperiments.sys.mjs"
);
@@ -41,31 +38,6 @@ add_task(async function snapshotSchema() {
}
});
add_task(async function experimentalFeatures() {
let featureGates = await FeatureGate.all();
ok(featureGates.length, "Should be at least one FeatureGate");
let snapshot = await Troubleshoot.snapshot();
for (let i = 0; i < snapshot.experimentalFeatures.length; i++) {
let experimentalFeature = snapshot.experimentalFeatures[i];
is(
experimentalFeature[0],
featureGates[i].title,
"The first item in the array should be the title's l10n-id of the FeatureGate"
);
is(
experimentalFeature[1],
featureGates[i].preference,
"The second item in the array should be the preference name for the FeatureGate"
);
is(
experimentalFeature[2],
Services.prefs.getBoolPref(featureGates[i].preference),
"The third item in the array should be the preference value of the FeatureGate"
);
}
});
add_task(async function modifiedPreferences() {
let prefs = [
"javascript.troubleshoot",
@@ -481,10 +453,6 @@ const SNAPSHOT_SCHEMA = {
},
},
},
experimentalFeatures: {
required: true,
type: "array",
},
environmentVariables: {
required: true,
type: "object",

View File

@@ -14,7 +14,6 @@ PYTHON_UNITTEST_MANIFESTS += [
"/testing/mochitest/tests/python/python.toml",
"/testing/raptor/test/python.toml",
"/testing/talos/talos/unittests/python.toml",
"/toolkit/components/featuregates/test/python/python.toml",
]
DIRS += [

View File

@@ -11,7 +11,6 @@ test-manifest-toml:
- 'testing/mozbase/manifestparser/tests/'
- 'testing/raptor/raptor/tests/'
- 'third_party/rust/'
- 'toolkit/components/featuregates/test/python/data/'
- '**/.*ruff.toml'
- '**/Cargo.toml'
- '**/Cross.toml'