Skip to content

Latest commit

 

History

History
420 lines (311 loc) · 12.5 KB

File metadata and controls

420 lines (311 loc) · 12.5 KB
id desktop-feature-api
title Feature API
slug /platform-guides/desktop/feature-api

import Tabs from "@theme/Tabs"; import TabItem from "@theme/TabItem";

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.

<Tabs defaultValue="js" values={[ { label: "JavaScript", value: "js" }, { label: "C++", value: "cpp" }, ] }>

const { screens, skipFocus } = NimbusFeatures.aboutwelcome.getAllVariables();
NimbusFeatures::GetBool("aboutwelcome"_ns, "enabled"_ns, false);

Configuration sources

:::tip 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, 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:

<Tabs defaultValue="js" values={[ { label: "JavaScript", value: "js" }, { label: "C++", value: "cpp" }, ] }>

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.

<Tabs defaultValue="js" values={[ { label: "JavaScript", value: "js" }, { label: "C++", value: "cpp" }, ] }>

// 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.

<Tabs defaultValue="js" values={[ { label: "JavaScript", value: "js" }, { label: "C++", value: "cpp" }, ] }>

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.

<Tabs defaultValue="js" values={[ { label: "JavaScript", value: "js" }, { label: "C++", value: "cpp" }, ] }>

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.

<Tabs defaultValue="js" values={[ { label: "JavaScript", value: "js" }, { label: "C++", value: "cpp" }, ] }>

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.

<Tabs defaultValue="js" values={[ { label: "JavaScript", value: "js" }, { label: "C++", value: "cpp" }, ] }>

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.

<Tabs defaultValue="js" values={[ { label: "JavaScript", value: "js" }, { label: "C++", value: "cpp" }, ] }>

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, or null.
  • EnrollmentType.ROLLOUT — returns only an active rollout enrollment, or null.
// 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);

:::note 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}`);
}