Skip to main content

Desktop Feature API (JS and C++)

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 Desktop
  • newtab, The about:newtab page in Desktop

In your code, you will use the Nimbus SDK to access variables associated with those features. e.g.

const { screens, skipFocus } = NimbusFeatures.aboutwelcome.getAllVariables();

Configuration sources

Note

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:

  1. Experiment value: First, we check if a Nimbus experiment is activated that changes the feature.
  2. 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.
  3. 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, you will need to choose an identifier and add it to the manifest in FeatureManifest.yaml: After adding the feature a build step is required to update the appropriate header file.

# In FeatureManifest.yaml
# Our feature name
aboutwelcome:
description: The about:welcome page
# 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
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);

Importing the Feature API

Import the NimbusFeatures module:

const lazy = {}
ChromeUtils.defineESModuleGetters(lazy, {
NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
});

API Reference Guide

getVariable()

getVariable(variableName: string): FeatureValue

Returns the value of a single feature variable.

// 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");

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.

const { foo, bar } = NimbusFeatures.myFeature.getAllVariables({
defaultValues: { foo: true, bar: false },
});

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.

NimbusFeatures.myFeature.recordExposureEvent();

// Only sends once per session, even if this function is called multiple times
NimbusFeatures.myFeature.recordExposureEvent({ once: true });

ready()

ready(): Promise (JS Only)

Wait for the remote experiment and defaults stores to be synced before checking values.

await NimbusFeatures.myFeature.ready();
const { foo } = NimbusFeatures.myFeature.getAllVariables();

onUpdate()

Listen for changes, include to remote defaults or pref values.

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);
});

off()

Stop listening for changes.

NimbusFeatures.myFeature.onUpdate(aListener);

// Later
NimbusFeatures.myFeature.offUpdate(aListener);

Experiment Metadata

If you need to know whether an experiment is active or get access to the experiment or branch identifier (for example, to report in utm_params), you can use ExperimentAPI.getExperimentMetaData:

const lazy = {}
ChromeUtils.defineESModuleGetters(lazy, {
NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
});

const data = lazy.ExperimentAPI.getExperimentMetaData({ featureId: "myFeature" });

// If there is no experiment, data will be null.
const slug = data?.slug;
const branchSlug = data?.branch?.slug;

if (experimentSlug && branchSlug) {
sendSomeTelemetry(
`The experiment identifier is ${slug} and the branch identifier is ${branchSlug}`,
);
}