From 695315ceabcabb1e68aea0e24df23d4791cd13af Mon Sep 17 00:00:00 2001 From: Jared Lockhart <119884+jaredlockhart@users.noreply.github.com> Date: Thu, 9 Apr 2026 12:01:21 -0400 Subject: [PATCH 1/7] docs: document enrollmentsMap 12-month retention limit Because * The enrollmentsMap targeting attribute includes both active and inactive enrollment records, but inactive records are permanently deleted after 12 months (365.25 days) from the original enrollment date * This caused a production issue where a rollout depending on a holdback via enrollmentsMap started losing clients when the holdback's enrollment records aged out of the store * The existing documentation did not mention the retention behavior or the difference between enrollmentsMap and activeExperiments/activeRollouts This commit * Expands the enrollmentsMap entry in the targeting attributes table * Adds a detailed subsection explaining enrollmentsMap behavior, the 12-month retention limit, and how to handle holdback transitions * Adds a comparison table showing the differences between the three enrollment-related targeting attributes --- docs/platform-guides/desktop/targeting.md | 36 +++++++++++++++++++++-- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/docs/platform-guides/desktop/targeting.md b/docs/platform-guides/desktop/targeting.md index f5d7a3939..a364e63ac 100644 --- a/docs/platform-guides/desktop/targeting.md +++ b/docs/platform-guides/desktop/targeting.md @@ -169,9 +169,9 @@ os.isMac | Attribute | Type | Description | Example | |-----------|------|-------------|---------| -| `activeExperiments` | `string[]` | Currently enrolled experiment slugs | `'my-experiment' in activeExperiments` | -| `activeRollouts` | `string[]` | Currently enrolled rollout slugs | `!('some-rollout' in activeRollouts)` | -| `enrollmentsMap` | `array` | All enrollments as `{experimentSlug, branchSlug}` entries | Used for branch-level exclusion | +| `activeExperiments` | `string[]` | Currently enrolled experiment slugs (active only) | `'my-experiment' in activeExperiments` | +| `activeRollouts` | `string[]` | Currently enrolled rollout slugs (active only) | `!('some-rollout' in activeRollouts)` | +| `enrollmentsMap` | `object` | Map of experiment/rollout slug → branch slug for all enrollments (active and inactive). See [enrollmentsMap details](#enrollmentsmap) below. | `enrollmentsMap['my-holdback'] == 'treatment'` | ### New Tab & Home Page @@ -439,6 +439,36 @@ userMonthlyActivity|length >= 14 && userMonthlyActivity|length < 21 !('other-experiment-slug' in activeExperiments) ``` +### Branch-level dependency with `enrollmentsMap` {#enrollmentsmap} + +`enrollmentsMap` is an object that maps experiment/rollout slugs to branch slugs. Unlike `activeExperiments` and `activeRollouts` (which only contain currently active enrollments), `enrollmentsMap` includes **both active and inactive** enrollment records. This makes it useful for targeting based on a client's enrollment history — for example, requiring that a client was previously enrolled in a specific branch of a holdback experiment. + +``` +// Require enrollment in the delivery branch of a holdback +enrollmentsMap['long-term-holdback-2026-growth-desktop'] == 'delivery' +``` + +:::warning 12-Month Retention Limit +Inactive enrollment records are **permanently deleted from the client's store after 12 months** (365.25 days), measured from the original enrollment date. Once deleted, the key is removed from `enrollmentsMap` and any targeting expression that references it will no longer match. + +This means if experiment B's targeting depends on enrollment in experiment A via `enrollmentsMap`, and experiment A ends, you have a **12-month window** before clients start losing the enrollment record. After that point, clients will begin unenrolling from experiment B with `targeting-mismatch` as their `enrollmentsMap` entries for experiment A are cleaned up. + +If a long-term holdback is replaced by a successor (e.g., a 2025 holdback replaced by a 2026 holdback), update dependent targeting expressions to accept **both** holdbacks before the 12-month window expires: + +``` +enrollmentsMap['long-term-holdback-2025h1-growth-desktop'] == 'delivery' +|| enrollmentsMap['long-term-holdback-2026-growth-desktop'] == 'delivery' +``` +::: + +**Key differences between enrollment attributes:** + +| Attribute | Includes Active | Includes Inactive | Provides Branch | Retention | +|-----------|:-:|:-:|:-:|---| +| `activeExperiments` | ✅ | ❌ | ❌ | Current session only | +| `activeRollouts` | ✅ | ❌ | ❌ | Current session only | +| `enrollmentsMap` | ✅ | ✅ | ✅ | 12 months from enrollment date | + ## Recorded Targeting Context (Telemetry) Firefox records a snapshot of all the targeting attribute values listed above in the `nimbus-targeting-context` ping, which lands in BigQuery at `mozdata.firefox_desktop.nimbus_targeting_context`. This is used for: From 0b4e040c0630fc62bf45bc14c3876325aa0e40f7 Mon Sep 17 00:00:00 2001 From: Jared Lockhart <119884+jaredlockhart@users.noreply.github.com> Date: Thu, 9 Apr 2026 12:03:26 -0400 Subject: [PATCH 2/7] docs: clarify clone-and-replace pattern for holdback transitions Because * Targeting cannot be modified on already-launched experiments/rollouts This commit * Replaces "update dependent targeting expressions" with the correct workflow: clone the dependent experiment with updated targeting, launch the clone, then end the original --- docs/platform-guides/desktop/targeting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/platform-guides/desktop/targeting.md b/docs/platform-guides/desktop/targeting.md index a364e63ac..4ed861563 100644 --- a/docs/platform-guides/desktop/targeting.md +++ b/docs/platform-guides/desktop/targeting.md @@ -453,7 +453,7 @@ Inactive enrollment records are **permanently deleted from the client's store af This means if experiment B's targeting depends on enrollment in experiment A via `enrollmentsMap`, and experiment A ends, you have a **12-month window** before clients start losing the enrollment record. After that point, clients will begin unenrolling from experiment B with `targeting-mismatch` as their `enrollmentsMap` entries for experiment A are cleaned up. -If a long-term holdback is replaced by a successor (e.g., a 2025 holdback replaced by a 2026 holdback), update dependent targeting expressions to accept **both** holdbacks before the 12-month window expires: +If a long-term holdback is replaced by a successor (e.g., a 2025 holdback replaced by a 2026 holdback), you cannot modify the targeting of already-launched experiments or rollouts. Instead, **clone the dependent experiment/rollout** with updated targeting that accepts both holdbacks, launch the clone, and then end the original: ``` enrollmentsMap['long-term-holdback-2025h1-growth-desktop'] == 'delivery' From d151a07bf0721d1e82b35623e4bee220b2e9a20b Mon Sep 17 00:00:00 2001 From: Jared Lockhart <119884+jaredlockhart@users.noreply.github.com> Date: Thu, 9 Apr 2026 12:05:29 -0400 Subject: [PATCH 3/7] docs: clarify 12-month limit only applies after experiment ends Because * Active enrollments are never cleaned up regardless of age * The previous wording could be misread as a hard 12-month cap on all enrollment records This commit * Clarifies that enrollmentsMap entries persist indefinitely while the experiment is live * Specifies the 12-month cleanup only starts after the experiment ends * Notes that clients who enrolled earlier are affected first * Updates the retention column in the comparison table --- docs/platform-guides/desktop/targeting.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/platform-guides/desktop/targeting.md b/docs/platform-guides/desktop/targeting.md index 4ed861563..db2a9a54a 100644 --- a/docs/platform-guides/desktop/targeting.md +++ b/docs/platform-guides/desktop/targeting.md @@ -448,10 +448,10 @@ userMonthlyActivity|length >= 14 && userMonthlyActivity|length < 21 enrollmentsMap['long-term-holdback-2026-growth-desktop'] == 'delivery' ``` -:::warning 12-Month Retention Limit -Inactive enrollment records are **permanently deleted from the client's store after 12 months** (365.25 days), measured from the original enrollment date. Once deleted, the key is removed from `enrollmentsMap` and any targeting expression that references it will no longer match. +:::warning 12-Month Retention After Ending +Enrollment records stay in `enrollmentsMap` **indefinitely while the experiment/rollout is live** — there is no time limit for active enrollments. The retention limit only applies after an experiment **ends**: once an enrollment becomes inactive, its record is permanently deleted from the client's store **12 months (365.25 days) after the original enrollment date**. Once deleted, the key is removed from `enrollmentsMap` and any targeting expression that references it will no longer match. -This means if experiment B's targeting depends on enrollment in experiment A via `enrollmentsMap`, and experiment A ends, you have a **12-month window** before clients start losing the enrollment record. After that point, clients will begin unenrolling from experiment B with `targeting-mismatch` as their `enrollmentsMap` entries for experiment A are cleaned up. +This means if experiment B's targeting depends on enrollment in experiment A via `enrollmentsMap`, and experiment A ends, you have a **12-month window** (from when each client originally enrolled in A) before clients start losing the record. After that point, clients will begin unenrolling from experiment B with `targeting-mismatch` as their `enrollmentsMap` entries for experiment A are cleaned up. Clients who enrolled earlier in experiment A's lifetime will be affected first. If a long-term holdback is replaced by a successor (e.g., a 2025 holdback replaced by a 2026 holdback), you cannot modify the targeting of already-launched experiments or rollouts. Instead, **clone the dependent experiment/rollout** with updated targeting that accepts both holdbacks, launch the clone, and then end the original: @@ -467,7 +467,7 @@ enrollmentsMap['long-term-holdback-2025h1-growth-desktop'] == 'delivery' |-----------|:-:|:-:|:-:|---| | `activeExperiments` | ✅ | ❌ | ❌ | Current session only | | `activeRollouts` | ✅ | ❌ | ❌ | Current session only | -| `enrollmentsMap` | ✅ | ✅ | ✅ | 12 months from enrollment date | +| `enrollmentsMap` | ✅ | ✅ | ✅ | Indefinite while active; 12 months from enrollment date after ending | ## Recorded Targeting Context (Telemetry) From cddc42aeede4716c41995a406ac506013f9dea68 Mon Sep 17 00:00:00 2001 From: Jared Lockhart <119884+jaredlockhart@users.noreply.github.com> Date: Thu, 9 Apr 2026 12:06:29 -0400 Subject: [PATCH 4/7] docs: remove prescriptive adaptation pattern from enrollmentsMap section Because * The docs should describe behavior, not recommend specific workflows This commit * Removes the holdback transition example and clone-and-replace guidance --- docs/platform-guides/desktop/targeting.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/docs/platform-guides/desktop/targeting.md b/docs/platform-guides/desktop/targeting.md index db2a9a54a..feebb53d0 100644 --- a/docs/platform-guides/desktop/targeting.md +++ b/docs/platform-guides/desktop/targeting.md @@ -453,12 +453,6 @@ Enrollment records stay in `enrollmentsMap` **indefinitely while the experiment/ This means if experiment B's targeting depends on enrollment in experiment A via `enrollmentsMap`, and experiment A ends, you have a **12-month window** (from when each client originally enrolled in A) before clients start losing the record. After that point, clients will begin unenrolling from experiment B with `targeting-mismatch` as their `enrollmentsMap` entries for experiment A are cleaned up. Clients who enrolled earlier in experiment A's lifetime will be affected first. -If a long-term holdback is replaced by a successor (e.g., a 2025 holdback replaced by a 2026 holdback), you cannot modify the targeting of already-launched experiments or rollouts. Instead, **clone the dependent experiment/rollout** with updated targeting that accepts both holdbacks, launch the clone, and then end the original: - -``` -enrollmentsMap['long-term-holdback-2025h1-growth-desktop'] == 'delivery' -|| enrollmentsMap['long-term-holdback-2026-growth-desktop'] == 'delivery' -``` ::: **Key differences between enrollment attributes:** From 2b5f3e5b35240570df988b9db3b975af32af6e2f Mon Sep 17 00:00:00 2001 From: Jared Lockhart <119884+jaredlockhart@users.noreply.github.com> Date: Thu, 9 Apr 2026 12:10:29 -0400 Subject: [PATCH 5/7] docs: link to _cleanupOldRecipes() source for 12-month retention Because * Review feedback from Beth to link to the actual code This commit * Adds a Searchfox link to _cleanupOldRecipes() in ExperimentStore.sys.mjs --- docs/platform-guides/desktop/targeting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/platform-guides/desktop/targeting.md b/docs/platform-guides/desktop/targeting.md index feebb53d0..0297f88f3 100644 --- a/docs/platform-guides/desktop/targeting.md +++ b/docs/platform-guides/desktop/targeting.md @@ -449,7 +449,7 @@ enrollmentsMap['long-term-holdback-2026-growth-desktop'] == 'delivery' ``` :::warning 12-Month Retention After Ending -Enrollment records stay in `enrollmentsMap` **indefinitely while the experiment/rollout is live** — there is no time limit for active enrollments. The retention limit only applies after an experiment **ends**: once an enrollment becomes inactive, its record is permanently deleted from the client's store **12 months (365.25 days) after the original enrollment date**. Once deleted, the key is removed from `enrollmentsMap` and any targeting expression that references it will no longer match. +Enrollment records stay in `enrollmentsMap` **indefinitely while the experiment/rollout is live** — there is no time limit for active enrollments. The retention limit only applies after an experiment **ends**: once an enrollment becomes inactive, its record is permanently deleted from the client's store **12 months (365.25 days) after the original enrollment date** (see [`_cleanupOldRecipes()`](https://searchfox.org/mozilla-central/source/toolkit/components/nimbus/lib/ExperimentStore.sys.mjs#390)). Once deleted, the key is removed from `enrollmentsMap` and any targeting expression that references it will no longer match. This means if experiment B's targeting depends on enrollment in experiment A via `enrollmentsMap`, and experiment A ends, you have a **12-month window** (from when each client originally enrolled in A) before clients start losing the record. After that point, clients will begin unenrolling from experiment B with `targeting-mismatch` as their `enrollmentsMap` entries for experiment A are cleaned up. Clients who enrolled earlier in experiment A's lifetime will be affected first. From d30950f1629899cd55a906b4876fa2e4b3850749 Mon Sep 17 00:00:00 2001 From: Jared Lockhart <119884+jaredlockhart@users.noreply.github.com> Date: Thu, 9 Apr 2026 12:15:48 -0400 Subject: [PATCH 6/7] fix: add missing resolved/integrity for webpack in yarn.lock Because * The webpack entry in yarn.lock was missing its resolved URL and integrity hash, causing yarn to install 5.106.0 instead of the pinned 5.89.0 * webpack 5.106.0 has a breaking change in ProgressPlugin that is incompatible with webpackbar 5.0.2, failing the build This commit * Adds the resolved URL and integrity hash for webpack 5.89.0 --- yarn.lock | 2 ++ 1 file changed, 2 insertions(+) diff --git a/yarn.lock b/yarn.lock index 79fe25590..d55310461 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8121,6 +8121,8 @@ webpack-sources@^3.2.2, webpack-sources@^3.2.3: "webpack@^4.0.0 || ^5.0.0", "webpack@^4.37.0 || ^5.0.0", webpack@^5.0.0, webpack@^5.1.0, webpack@^5.20.0, webpack@^5.73.0, "webpack@>= 4", webpack@>=2, "webpack@>=4.41.1 || 5.x", "webpack@3 || 4 || 5": version "5.89.0" + resolved "https://registry.npmjs.org/webpack/-/webpack-5.89.0.tgz" + integrity sha512-qyfIC10pOr70V+jkmud8tMfajraGCZMBWJtrmuBymQKCrLTRejBI8STDp1MCyZu/QTdZSeacCQYpYNQVOzX5kw== dependencies: "@types/eslint-scope" "^3.7.3" "@types/estree" "^1.0.0" From 0bd5de1f941dfb117eff731b056dee1332f88031 Mon Sep 17 00:00:00 2001 From: Jared Lockhart <119884+jaredlockhart@users.noreply.github.com> Date: Thu, 9 Apr 2026 12:18:35 -0400 Subject: [PATCH 7/7] docs: use pinned revision link for _cleanupOldRecipes Because * The mozilla-central source link could shift as lines are added/removed This commit * Uses a specific revision hash in the Searchfox link so it stays stable --- docs/platform-guides/desktop/targeting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/platform-guides/desktop/targeting.md b/docs/platform-guides/desktop/targeting.md index 0297f88f3..5aab71463 100644 --- a/docs/platform-guides/desktop/targeting.md +++ b/docs/platform-guides/desktop/targeting.md @@ -449,7 +449,7 @@ enrollmentsMap['long-term-holdback-2026-growth-desktop'] == 'delivery' ``` :::warning 12-Month Retention After Ending -Enrollment records stay in `enrollmentsMap` **indefinitely while the experiment/rollout is live** — there is no time limit for active enrollments. The retention limit only applies after an experiment **ends**: once an enrollment becomes inactive, its record is permanently deleted from the client's store **12 months (365.25 days) after the original enrollment date** (see [`_cleanupOldRecipes()`](https://searchfox.org/mozilla-central/source/toolkit/components/nimbus/lib/ExperimentStore.sys.mjs#390)). Once deleted, the key is removed from `enrollmentsMap` and any targeting expression that references it will no longer match. +Enrollment records stay in `enrollmentsMap` **indefinitely while the experiment/rollout is live** — there is no time limit for active enrollments. The retention limit only applies after an experiment **ends**: once an enrollment becomes inactive, its record is permanently deleted from the client's store **12 months (365.25 days) after the original enrollment date** (see [`_cleanupOldRecipes()`](https://searchfox.org/firefox-main/rev/62b11cf9d978f8c6f8144156feebf42af5d71cf9/toolkit/components/nimbus/lib/ExperimentStore.sys.mjs#390)). Once deleted, the key is removed from `enrollmentsMap` and any targeting expression that references it will no longer match. This means if experiment B's targeting depends on enrollment in experiment A via `enrollmentsMap`, and experiment A ends, you have a **12-month window** (from when each client originally enrolled in A) before clients start losing the record. After that point, clients will begin unenrolling from experiment B with `targeting-mismatch` as their `enrollmentsMap` entries for experiment A are cleaned up. Clients who enrolled earlier in experiment A's lifetime will be affected first.