From b19576dcda6290ea92255c11b004b694bfd3121f Mon Sep 17 00:00:00 2001 From: Jared Lockhart <119884+jaredlockhart@users.noreply.github.com> Date: Tue, 24 Feb 2026 12:30:27 -0500 Subject: [PATCH 1/2] docs: update desktop Feature API reference to match current source MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Because * The `getExperimentMetaData()` method documented in feature-api.mdx no longer exists in the source code — it was replaced by `getEnrollmentMetadata()` on each NimbusFeatures instance * Three new methods (`getEnrollmentMetadata`, `getAllEnrollments`, `getAllEnrollmentMetadata`) are undocumented * Desktop supports co-enrollment via `allowCoenrollment` in the feature manifest but the docs only cover mobile * The `off()` heading didn't match the actual method name `offUpdate()` * The FeatureManifest.yaml example was missing required fields This commit * Replaces the removed `getExperimentMetaData` section with `getEnrollmentMetadata()` documenting all three query modes * Adds `getAllEnrollments()` and `getAllEnrollmentMetadata()` sections for co-enrolling features * Documents the `slug` parameter on `recordExposureEvent()` for co-enrolling features * Fixes `off()` heading to `offUpdate()` * Enriches the manifest example with `owner`, `hasExposure`, `exposureDescription`, and `allowCoenrollment` * Adds Desktop co-enrollment support to coenrolling-features.mdx with manifest example, JS API example, and cross-links Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/platform-guides/desktop/feature-api.mdx | 76 +++++++++++++++---- .../fml/coenrolling-features.mdx | 33 +++++++- 2 files changed, 93 insertions(+), 16 deletions(-) diff --git a/docs/platform-guides/desktop/feature-api.mdx b/docs/platform-guides/desktop/feature-api.mdx index 8c51560e4..d2b62a538 100644 --- a/docs/platform-guides/desktop/feature-api.mdx +++ b/docs/platform-guides/desktop/feature-api.mdx @@ -75,9 +75,16 @@ After adding the feature a build step is required to update the appropriate head # 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 @@ -216,6 +223,10 @@ 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" }); ``` @@ -305,7 +316,7 @@ NimbusFeatures::OnUpdate("aboutwelcome"_ns, "skipFocus"_ns, -### `off()` +### `offUpdate()` Stop listening for changes. @@ -342,25 +353,62 @@ NimbusFeatures::OffUpdate("aboutwelcome"_ns, "skipFocus"_ns, -## Experiment Metadata +### `getEnrollmentMetadata()` -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`: +`getEnrollmentMetadata(enrollmentType?: "experiment" | "rollout"): { slug: string, branch: string, isRollout: boolean } | null` (JS Only) -```js -const lazy = {} -ChromeUtils.defineESModuleGetters(lazy, { - NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs", -}); +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: -const data = lazy.ExperimentAPI.getExperimentMetaData({ featureId: "myFeature" }); +- **No argument** (default) — returns the active experiment if one exists, otherwise falls back to the active rollout. +- **`"experiment"`** — returns only an active experiment enrollment, or `null`. +- **`"rollout"`** — returns only an active rollout enrollment, or `null`. -// If there is no experiment, data will be null. -const slug = data?.slug; -const branchSlug = data?.branch?.slug; +```js +const data = NimbusFeatures.myFeature.getEnrollmentMetadata(); -if (experimentSlug && branchSlug) { +// If there is no enrollment, data will be null. +if (data) { sendSomeTelemetry( - `The experiment identifier is ${slug} and the branch identifier is ${branchSlug}`, + `Enrolled in ${data.slug}, branch: ${data.branch}, isRollout: ${data.isRollout}`, ); } + +// Query for a specific enrollment type: +const rollout = NimbusFeatures.myFeature.getEnrollmentMetadata("rollout"); +``` + +:::note +This method throws for [co-enrolling features](/technical-reference/fml/coenrolling-features). Use `getAllEnrollmentMetadata()` instead. +::: + +### `getAllEnrollments()` + +`getAllEnrollments(): Array<{ 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](/technical-reference/fml/coenrolling-features) where a client may be enrolled in multiple experiments 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). + +```js +const enrollments = NimbusFeatures.myFeature.getAllEnrollments(); + +for (const { meta, value } of enrollments) { + console.log(`${meta.slug} (branch: ${meta.branch}):`, value); +} +``` + +### `getAllEnrollmentMetadata()` + +`getAllEnrollmentMetadata(): Array<{ 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. + +```js +const enrollments = NimbusFeatures.myFeature.getAllEnrollmentMetadata(); + +for (const { slug, branch, isRollout } of enrollments) { + sendSomeTelemetry(`Enrolled in ${slug}, branch: ${branch}`); +} ``` diff --git a/docs/technical-reference/fml/coenrolling-features.mdx b/docs/technical-reference/fml/coenrolling-features.mdx index 99903cd8f..960608755 100644 --- a/docs/technical-reference/fml/coenrolling-features.mdx +++ b/docs/technical-reference/fml/coenrolling-features.mdx @@ -10,10 +10,13 @@ A feature which allows co-enrollment allows a client to be enrolled in any numbe **Features supporting co-enrollment** * [Messaging](/messaging/desktop/mobile-messaging) (Fenix, Firefox iOS, Focus for Android, Focus for iOS) +* Firefox Desktop — any feature with `allowCoenrollment: true` in [FeatureManifest.yaml](https://searchfox.org/mozilla-central/source/toolkit/components/nimbus/FeatureManifest.yaml) (e.g., `prefFlips`) ## How to define a co-enrolling feature -A feature can be marked as allowing co-enrollment with a boolean flag in its feature definition in a Feature Manifest. For example, in your `messaging.fml.yaml` file: +A feature can be marked as allowing co-enrollment with a boolean flag in its feature definition in a Feature Manifest. + +**Mobile (FML):** In your `.fml.yaml` file: ```yaml features: @@ -28,7 +31,18 @@ features: default: {} ``` -Once your messaging feature is defined as a co-enrolling feature, a client can be enrolled in any number of messaging experiments/rollouts. +**Desktop:** In `FeatureManifest.yaml`: + +```yaml +prefFlips: + description: Pref flips for incident response + allowCoenrollment: true + variables: + prefs: + type: json +``` + +Once your feature is defined as a co-enrolling feature, a client can be enrolled in any number of experiments/rollouts for that feature. ## Recording exposure @@ -56,6 +70,21 @@ val slug = message.data.experiment ?: return message messagingFeature.recordExperimentExposure(slug) ``` +### Desktop + +On Desktop, co-enrolling features use the `{slug}` option on `recordExposureEvent()`: + +```js +NimbusFeatures.myFeature.recordExposureEvent({ slug: "experiment-slug" }); +``` + +Standard per-feature methods like `getVariable()` and `getAllVariables()` throw for co-enrolling features. Instead, use these APIs: + +- **`getAllEnrollments()`** — returns `Array<{ meta: { slug, branch, isRollout }, value: object }>` with both metadata and resolved feature values for each enrollment. +- **`getAllEnrollmentMetadata()`** — returns `Array<{ slug, branch, isRollout }>` (metadata only). + +See the [Desktop Feature API reference](/platform-guides/desktop/feature-api) for full details. + ## Other things to note about co-enrollment * Experiment feature values still take precedence over rollout feature values From 073c7f76d5d9a8c63ab4475355193244bb23a977 Mon Sep 17 00:00:00 2001 From: Jared Lockhart <119884+jaredlockhart@users.noreply.github.com> Date: Tue, 24 Feb 2026 12:52:05 -0500 Subject: [PATCH 2/2] docs: address review feedback on desktop API docs Because * Reviewer suggested using EnrollmentType constants instead of string literals to encourage best practices * T[] syntax preferred over Array for type signatures * "and/or" more accurate than "or" for co-enrollment scenarios This commit * Uses EnrollmentType constants in getEnrollmentMetadata() signature, parameter docs, and code examples * Adds import comment showing EnrollmentType is from the same module * Switches all type signatures to T[] syntax * Updates wording to "experiments and/or rollouts" Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/platform-guides/desktop/feature-api.mdx | 19 ++++++++++++------- .../fml/coenrolling-features.mdx | 4 ++-- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/docs/platform-guides/desktop/feature-api.mdx b/docs/platform-guides/desktop/feature-api.mdx index d2b62a538..bdac45c18 100644 --- a/docs/platform-guides/desktop/feature-api.mdx +++ b/docs/platform-guides/desktop/feature-api.mdx @@ -355,17 +355,22 @@ NimbusFeatures::OffUpdate("aboutwelcome"_ns, "skipFocus"_ns, ### `getEnrollmentMetadata()` -`getEnrollmentMetadata(enrollmentType?: "experiment" | "rollout"): { slug: string, branch: string, isRollout: boolean } | null` (JS Only) +`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. -- **`"experiment"`** — returns only an active experiment enrollment, or `null`. -- **`"rollout"`** — returns only an active rollout enrollment, or `null`. +- **`EnrollmentType.EXPERIMENT`** — returns only an active experiment enrollment, or `null`. +- **`EnrollmentType.ROLLOUT`** — returns only an active rollout enrollment, or `null`. ```js +// 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. @@ -376,7 +381,7 @@ if (data) { } // Query for a specific enrollment type: -const rollout = NimbusFeatures.myFeature.getEnrollmentMetadata("rollout"); +const rollout = NimbusFeatures.myFeature.getEnrollmentMetadata(lazy.EnrollmentType.ROLLOUT); ``` :::note @@ -385,9 +390,9 @@ This method throws for [co-enrolling features](/technical-reference/fml/coenroll ### `getAllEnrollments()` -`getAllEnrollments(): Array<{ meta: { slug, branch, isRollout }, value: object }>` (JS Only) +`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](/technical-reference/fml/coenrolling-features) where a client may be enrolled in multiple experiments or rollouts for the same feature simultaneously. +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](/technical-reference/fml/coenrolling-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). @@ -401,7 +406,7 @@ for (const { meta, value } of enrollments) { ### `getAllEnrollmentMetadata()` -`getAllEnrollmentMetadata(): Array<{ slug: string, branch: string, isRollout: boolean }>` (JS Only) +`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. diff --git a/docs/technical-reference/fml/coenrolling-features.mdx b/docs/technical-reference/fml/coenrolling-features.mdx index 960608755..0177eb2fb 100644 --- a/docs/technical-reference/fml/coenrolling-features.mdx +++ b/docs/technical-reference/fml/coenrolling-features.mdx @@ -80,8 +80,8 @@ NimbusFeatures.myFeature.recordExposureEvent({ slug: "experiment-slug" }); Standard per-feature methods like `getVariable()` and `getAllVariables()` throw for co-enrolling features. Instead, use these APIs: -- **`getAllEnrollments()`** — returns `Array<{ meta: { slug, branch, isRollout }, value: object }>` with both metadata and resolved feature values for each enrollment. -- **`getAllEnrollmentMetadata()`** — returns `Array<{ slug, branch, isRollout }>` (metadata only). +- **`getAllEnrollments()`** — returns `{ meta: { slug, branch, isRollout }, value: object }[]` with both metadata and resolved feature values for each enrollment. +- **`getAllEnrollmentMetadata()`** — returns `{ slug, branch, isRollout }[]` (metadata only). See the [Desktop Feature API reference](/platform-guides/desktop/feature-api) for full details.