Feature API
This guide will help you use the Nimbus Feature API in Desktop Firefox to run experiments, set values remotely, and manage user preferences. If you are new to using Nimbus, here's a video overview of how the code interacts to control your feature. The video until 5 minutes 44 seconds focuses on getting started. Starting at 5:58 it switches to considerations when controlling preferences.
If you are familiar with Normandy and are trying to migrate a feature, you may want to check out the Migration Guide for Pref Experiments.
About the Feature API
Can I Use This?
For the JS implementation you can import ExperimentAPI.sys.mjs in the parent process or a privileged child process. We do support First run experiments on Windows, holdbacks, and rollouts.
For the C++ implementation you can import #include "mozilla/browser/NimbusFeatures.h" and we support early startup experiments and holdbacks.
If you have a usecase that isn't supported, please reach out in #ask-experimenter on Slack.
What is a feature?
In the Nimbus ecosystem, a feature is an area of code instrumented for experiments and remote configuration. It can be as small as a single function or as complex as a whole about: page. Some examples:
aboutwelcome, The about:welcome page in Desktopnewtab, The about:newtab page in Desktop
In your code, you will use the Nimbus SDK to access variables associated with those features. e.g.
- JavaScript
- C++
const { screens, skipFocus } = NimbusFeatures.aboutwelcome.getAllVariables();
NimbusFeatures::GetBool("aboutwelcome"_ns, "enabled"_ns, false);
Configuration sources
This section is relevant only for the JS API.
The Nimbus Feature API will return the correct configuration for a feature given a few different inputs in this order:
- Experiment value: First, we check if a Nimbus experiment is activated that changes the feature.
- Remotely-configured value: If no experiment is set, we check if there is a remotely-defined value. This is a mechanism that allows us to roll-out changes quickly between releases.
- Local default: Finally, we will return the current value of preferences in the manifest, if they are defined in firefox.js.
Registering a new feature
To register a new feature, choose an identifier and add it to FeatureManifest.yaml. After adding the feature, a build step is required to update the generated header file.
# In FeatureManifest.yaml
# Our feature name
aboutwelcome:
description: The about:welcome page
owner: user@mozilla.com
hasExposure: true
exposureDescription: >
Recorded when the about:welcome page is first shown to the user.
# Include this if you need synchronous access / very early access at startup
# or if you are registering this to use for platform experiments.
isEarlyStartup: true
# Set to true to allow a client to be enrolled in multiple experiments/rollouts
# for this feature simultaneously. See "Co-enrolling Features" docs.
# allowCoenrollment: false
variables:
# Additional (optional) values that we can control
# The name of these variables is up to you
enabled:
type: boolean
fallbackPref: browser.aboutwelcome.enabled
skipFocus:
type: boolean
// In firefox.js
pref("browser.aboutwelcome.enabled", true);
See the Desktop Feature Manifest Reference for the complete schema, including all feature and variable properties, setPref vs fallbackPref, enum constraints, and validation.
Importing the Feature API
Import the NimbusFeatures module:
- JavaScript
- C++
const lazy = {}
ChromeUtils.defineESModuleGetters(lazy, {
NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
});
#include "mozilla/browser/NimbusFeatures.h"
API Reference Guide
getVariable()
getVariable(variableName: string): FeatureValue
Returns the value of a single feature variable.
- JavaScript
- C++
// Warning: **This function will throw in Nightly and CI build** if you do not define `variableName` in the Nimbus manifest.
const foo = NimbusFeatures.myFeature.getVariable("foo");
// notAVariable is not defined in the manifest, so this will throw in CI
const baz = NimbusFeatures.myFeature.getVariable("notAVariable");
// feature name and variable name to retrieve the value
NimbusFeatures::GetInt("aboutwelcome"_ns, "skipFocus"_ns, false);
getAllVariables()
getAllVariables({ defaultValues }): FeatureValue (JS Only)
Returns the value of all variables for a feature. Note that variables will be merged between sources.
If options.defaultValues is defined, it will be preferred before default branch fallback values but after experiment, remote, and user-set preference values.
- JavaScript
- C++
const { foo, bar } = NimbusFeatures.myFeature.getAllVariables({
defaultValues: { foo: true, bar: false },
});
// There is no equivalent for this in C++. Instead you can use
// NimbusFeatures::GetInt("featurename"_ns, "variablename"_ns, 0)
// NimbusFeatures::GetBool("featurename"_ns, "variablename"_ns, false)
// To access a single variable value at a time
recordExposureEvent()
Use this to send an exposure event. By default this will send one exposure event per function call, but you can add an options object of {once: true} to only send it once per session.
Note that you should add an exposureDescription to the manifest describing when/how this event is sent.
- JavaScript
- C++
NimbusFeatures.myFeature.recordExposureEvent();
// Only sends once per session, even if this function is called multiple times
NimbusFeatures.myFeature.recordExposureEvent({ once: true });
// For co-enrolling features, you must specify which enrollment to record exposure for:
NimbusFeatures.myFeature.recordExposureEvent({ slug: "experiment-slug" });
NimbusFeatures.myFeature.recordExposureEvent({ once: true, slug: "experiment-slug" });
// aOnce=true specifies that we only want to send this event once per browsing
// session
NimbusFeatures::RecordExposureEvent("featurename"_ns, true);
ready()
ready(): Promise (JS Only)
Wait for the remote experiment and defaults stores to be synced before checking values.
- JavaScript
- C++
await NimbusFeatures.myFeature.ready();
const { foo } = NimbusFeatures.myFeature.getAllVariables();
// This is not needed by the C++ API and has no equivalent, instead by
// registering your feature as `isEarlyStartup` you can ensure that `GetBool`
// and `GetInt` will return the experiment value at startup
onUpdate()
Listen for changes, include to remote defaults or pref values.
- JavaScript
- C++
NimbusFeatures.myFeature.onUpdate((event, reason) => {
/**
* `reason` is a string that can be used to identify the source
* of the update event.
* This list of reasons:
* 1. `feature-experiment-loaded` or `feature-rollout-loaded` this
* is triggered when the Nimbus feature has finished loading
* (when .ready() resolves). It is not relevant for isEarlyStartup=true features
* 2. `experiment-updated` or `rollout-updated` client recipe for this
* feature was changed (activated or deactivated)
* 3. `pref-updated` the value of the fallback pref for the feature
* variable was changed
*/
const newValue = NimbusFeatures.myFeature.getAllVariables();
updateUI(newValue);
});
NimbusFeatures::OnUpdate("aboutwelcome"_ns, "skipFocus"_ns,
[](const char*, void*){}, void*);
offUpdate()
Stop listening for changes.
- JavaScript
- C++
NimbusFeatures.myFeature.onUpdate(aListener);
// Later
NimbusFeatures.myFeature.offUpdate(aListener);
// Provide feature name and value to watch for changes
NimbusFeatures::OnUpdate("aboutwelcome"_ns, "skipFocus"_ns,
[](const char*, void*){}, void*);
// Later
NimbusFeatures::OffUpdate("aboutwelcome"_ns, "skipFocus"_ns,
[](const char*, void*){}, void*);
getEnrollmentMetadata()
getEnrollmentMetadata(enrollmentType?: EnrollmentType): { slug: string, branch: string, isRollout: boolean } | null (JS Only)
Returns metadata about the active enrollment for this feature, or null if there is no active enrollment.
The optional enrollmentType parameter controls what kind of enrollment to look for:
- No argument (default) — returns the active experiment if one exists, otherwise falls back to the active rollout.
EnrollmentType.EXPERIMENT— returns only an active experiment enrollment, ornull.EnrollmentType.ROLLOUT— returns only an active rollout enrollment, ornull.
// EnrollmentType is exported from the same module as NimbusFeatures:
// ChromeUtils.defineESModuleGetters(lazy, {
// EnrollmentType: "resource://nimbus/ExperimentAPI.sys.mjs",
// });
const data = NimbusFeatures.myFeature.getEnrollmentMetadata();
// If there is no enrollment, data will be null.
if (data) {
sendSomeTelemetry(
`Enrolled in ${data.slug}, branch: ${data.branch}, isRollout: ${data.isRollout}`,
);
}
// Query for a specific enrollment type:
const rollout = NimbusFeatures.myFeature.getEnrollmentMetadata(lazy.EnrollmentType.ROLLOUT);
This method throws for co-enrolling features. Use getAllEnrollmentMetadata() instead.
getAllEnrollments()
getAllEnrollments(): { meta: { slug, branch, isRollout }, value: object }[] (JS Only)
Returns an array of all active enrollments for this feature, each with metadata and the resolved feature variable values. This is the primary data access method for co-enrolling features where a client may be enrolled in multiple experiments and/or rollouts for the same feature simultaneously.
For non-co-enrolling features, this also works but will return at most two entries (one experiment and one rollout).
const enrollments = NimbusFeatures.myFeature.getAllEnrollments();
for (const { meta, value } of enrollments) {
console.log(`${meta.slug} (branch: ${meta.branch}):`, value);
}
getAllEnrollmentMetadata()
getAllEnrollmentMetadata(): { slug: string, branch: string, isRollout: boolean }[] (JS Only)
Returns metadata for all active enrollments for this feature, without the resolved feature values. Use this when you only need enrollment identifiers (e.g., for telemetry) and not the feature configuration.
const enrollments = NimbusFeatures.myFeature.getAllEnrollmentMetadata();
for (const { slug, branch, isRollout } of enrollments) {
sendSomeTelemetry(`Enrolled in ${slug}, branch: ${branch}`);
}