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 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.jsm 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. End-user-setting: If the user branch of any preferences in the manifest are set, this will override experiment values or defaults. We believe this is important to respect user choice.
  2. Experiment value: Next, we check if a Nimbus experiment is activated that changes the feature.
  3. 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.
  4. Local default: Finally, we will return the default branch 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 nameaboutwelcome:  description: The about:welcome page  # Include this if you need sychronous 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.jspref("browser.aboutwelcome.enabled", true);

Importing the Feature API#

Import the NimbusFeatures module:

XPCOMUtils.defineLazyModuleGetters(this, {  NimbusFeatures: "resource://nimbus/ExperimentAPI.jsm",});

API Reference Guide#

isEnabled()#

isEnabled(): boolean (JS Only)

Short form for .getVariable("enabled")

Note that you should have an enabled property defined in the manifest if you plan to use this.

const isEnabled = NimbusFeatures.myFeature.isEnabled();

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 CIconst 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 beftween 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 defualt 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 muliple timesNimbusFeatures.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);
// LaterNimbusFeatures.myFeature.off(aListener);

Experiment Metadata#

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

XPCOMUtils.defineLazyModuleGetters(this, {  ExperimentAPI: "resource://nimbus/ExperimentAPI.jsm",});
const data = 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}`,  );}