summaryrefslogtreecommitdiffstats
path: root/toolkit/components/nimbus
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/nimbus')
-rw-r--r--toolkit/components/nimbus/FeatureManifest.yaml321
-rw-r--r--toolkit/components/nimbus/generate/generate_feature_manifest.py25
-rw-r--r--toolkit/components/nimbus/lib/ExperimentManager.sys.mjs76
-rw-r--r--toolkit/components/nimbus/lib/ExperimentStore.sys.mjs2
-rw-r--r--toolkit/components/nimbus/lib/RemoteSettingsExperimentLoader.sys.mjs2
-rw-r--r--toolkit/components/nimbus/metrics.yaml38
-rw-r--r--toolkit/components/nimbus/schemas/ExperimentFeature.schema.json125
-rw-r--r--toolkit/components/nimbus/schemas/ExperimentFeatureManifest.schema.json123
-rw-r--r--toolkit/components/nimbus/test/unit/test_ExperimentManager_unenroll.js112
-rw-r--r--toolkit/components/nimbus/test/unit/test_SharedDataMap.js5
10 files changed, 635 insertions, 194 deletions
diff --git a/toolkit/components/nimbus/FeatureManifest.yaml b/toolkit/components/nimbus/FeatureManifest.yaml
index 746ac4738d..190e29d6db 100644
--- a/toolkit/components/nimbus/FeatureManifest.yaml
+++ b/toolkit/components/nimbus/FeatureManifest.yaml
@@ -2,6 +2,8 @@
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+# yaml-language-server: $schema=schemas/ExperimentFeatureManifest.schema.json
+
# Features must be added here to be accessible through the NimbusFeature API.
"no-feature-firefox-desktop":
@@ -51,7 +53,6 @@ nimbus-qa-1:
nimbus-qa-2:
description: A feature for testing pref-setting on the user branch.
owner: barret@mozilla.com
- isEarlyStartup: true
hasExposure: false
variables:
value:
@@ -315,7 +316,7 @@ urlbar:
Whether Firefox Suggest will use the new Rust backend instead of the
original JS backend.
quickSuggestScenario:
- # IMPORTANT: This should not have a fallbackPref. See UrlbarPrefs.jsm.
+ # IMPORTANT: This should not have a fallbackPref. See UrlbarPrefs.sys.mjs.
type: string
description: The Firefox Suggest scenario in which the user is enrolled
enum:
@@ -421,6 +422,11 @@ urlbar:
If neither Nimbus nor remote settings defines a cap, no cap will be
used, and the user will be able to increment the minimum length without
any limit.
+ weatherSimpleUI:
+ type: boolean
+ description: >-
+ If true, show the weather suggestion by simple UI edition as follows.
+ * Remove the forcast text from the summary text.
yelpMinKeywordLength:
type: int
fallbackPref: browser.urlbar.yelp.minKeywordLength
@@ -567,6 +573,13 @@ aboutwelcome:
fallbackPref: browser.aboutwelcome.newtabUrlBarFocus
description: >-
Should the urlbar be focused when the new tab page loads after new user onboarding
+ toolbarButtonEnabled:
+ type: boolean
+ setPref:
+ branch: user
+ pref: browser.aboutwelcome.toolbarButtonEnabled
+ description: >-
+ Should the return to about:welcome toolbar button be shown
moreFromMozilla:
description: "New page on about:preferences to suggest more Mozilla products"
@@ -604,7 +617,6 @@ windowsJumpList:
description: "Controls for the Windows Jump List integration."
owner: mconley@mozilla.com
hasExposure: false
- isEarlyStartup: true
variables:
legacyBackend:
type: boolean
@@ -862,10 +874,15 @@ pocketNewtab:
fallbackPref: >-
browser.newtabpage.activity-stream.discoverystream.ctaButtonSponsors
ctaButtonVariant:
- description: Specifies which veriant to use for any sponsors in ctaButtonSponsors
+ description: Specifies which variant to use for any sponsors in ctaButtonSponsors
type: string
fallbackPref: >-
browser.newtabpage.activity-stream.discoverystream.ctaButtonVariant
+ spocMessageVariant:
+ description: Adds some message dialogs explainging sponsored content to the user
+ type: string
+ fallbackPref: >-
+ browser.newtabpage.activity-stream.discoverystream.spocMessageVariant
regionStoriesConfig:
description: A comma-separated list of region to get stories for.
type: string
@@ -1064,7 +1081,6 @@ fullPageTranslation:
description: This feature opens a popup panel to offer to translate a page.
owner: gtatum@mozilla.com
hasExposure: false
- isEarlyStartup: true
variables:
boolean:
description: Set to true to enable the translations feature
@@ -1077,7 +1093,6 @@ fullPageTranslationAutomaticPopup:
description: Controls whether the popup automatically shows for translations.
owner: gtatum@mozilla.com
hasExposure: false
- isEarlyStartup: true
variables:
boolean:
description: Set to true to automatically popup, and false to only show the button.
@@ -1246,6 +1261,26 @@ fxms-message-11:
path: "browser/components/asrouter/content-src/schemas/MessagingExperiment.schema.json"
variables: {}
+whatsNewPage:
+ description: "A Firefox Messaging System message for the What's new page channel"
+ owner: omc@mozilla.com
+ hasExposure: true
+ exposureDescription: >-
+ "Exposure is sent if the message is about to be shown after trigger and targeting conditions on the message matched."
+ variables:
+ overrideUrl:
+ description: URL of the What's new page
+ type: string
+ setPref:
+ branch: user
+ pref: startup.homepage_override_url_nimbus
+ maxVersion:
+ description: Maximum Firefox update version
+ type: string
+ setPref:
+ branch: user
+ pref: startup.homepage_override_nimbus_maxVersion
+
pbNewtab:
description: "A Firefox Messaging System message for the pbNewtab message channel"
owner: omc@mozilla.com
@@ -1325,6 +1360,7 @@ glean:
gleanInternalSdk:
description: "The Glean internal SDK feature intended only for internal Glean Team use"
+ owner: glean-team@mozilla.com
hasExposure: false
# Some variables are used through the C++ API and thus require pref-storage.
# We rely on those values at Glean.init time, which happens at startup.
@@ -1664,7 +1700,6 @@ fxaButtonVisibility:
description: Prefs to control the visibility of the Firefox Accounts toolbar button when not signed in.
owner: mconley@mozilla.com
hasExposure: false
- isEarlyStartup: true
variables:
boolean:
description: True if the Firefox Accounts toolbar button should be visible when not signed in.
@@ -1802,7 +1837,6 @@ migrationWizard:
description: Prefs to control the Migration Wizard UI.
owner: mconley@mozilla.com
hasExposure: false
- isEarlyStartup: true
variables:
showImportAll:
description: True if the "Variant 2" of the Migration Wizard browser / profile selection UI should be used. This is only meaningful in the new Migration Wizard.
@@ -1891,36 +1925,205 @@ mixedContentUpgrading:
branch: default
pref: security.mixed_content.upgrade_display_content.video
-jsParallelParsing:
- description: Pref to toggle JS parallel parsing.
- owner: dpalmeiro@mozilla.com, nbp@mozilla.com
- isEarlyStartup: true
+gc:
+ description: Prefs that control gc heuristics.
+ owner: dpalmeiro@mozilla.com
hasExposure: false
variables:
- enabled:
- description: True to enable parallel parsing.
+ max_nursery_size:
+ description: Set the maximum size of the GC nursery, in kb.
+ type: int
+ setPref:
+ branch: user
+ pref: "javascript.options.mem.nursery.max_kb"
+ min_nursery_size:
+ description: Set the minimum size of the GC nursery, in kb.
+ type: int
+ setPref:
+ branch: user
+ pref: "javascript.options.mem.nursery.min_kb"
+ gc_allocation_threshold_mb:
+ description: Lower limit for collecting a zone, in MB.
+ type: int
+ setPref:
+ branch: user
+ pref: "javascript.options.mem.gc_allocation_threshold_mb"
+ gc_balanced_heap_limits:
+ description: Whether balanced heap limits are enabled.
type: boolean
setPref:
branch: user
- pref: "javascript.options.parallel_parsing"
+ pref: "javascript.options.mem.gc_balanced_heap_limits"
+ gc_compacting:
+ description: Whether compacting GC is enabled.
+ type: boolean
+ setPref:
+ branch: user
+ pref: "javascript.options.mem.gc_compacting"
+ gc_heap_growth_factor:
+ description: Heap growth parameter for balanced heap limit calculation.
+ type: int
+ setPref:
+ branch: user
+ pref: "javascript.options.mem.gc_heap_growth_factor"
+ gc_helper_thread_ratio:
+ description: Number of threads to use for parallel GC work.
+ type: int
+ setPref:
+ branch: user
+ pref: "javascript.options.mem.gc_helper_thread_ratio"
+ gc_high_frequency_large_heap_growth:
+ description: Heap growth factor for large heaps in the high-frequency GC state.
+ type: int
+ setPref:
+ branch: user
+ pref: "javascript.options.mem.gc_high_frequency_large_heap_growth"
+ gc_high_frequency_small_heap_growth:
+ description: Heap growth factor for small heaps in the high-frequency GC state.
+ type: int
+ setPref:
+ branch: user
+ pref: "javascript.options.mem.gc_high_frequency_small_heap_growth"
+ gc_high_frequency_time_limit_ms:
+ description: GCs less than this far apart in milliseconds will be
+ considered high-frequency GCs.
+ type: int
+ setPref:
+ branch: user
+ pref: "javascript.options.mem.gc_high_frequency_time_limit_ms"
+ gc_incremental:
+ description: Whether incremental GC is enabled. If not, GC will always run to completion.
+ type: boolean
+ setPref:
+ branch: user
+ pref: "javascript.options.mem.gc_incremental"
+ incremental_weakmap:
+ description: Enable incremental weakmap marking.
+ type: boolean
+ setPref:
+ branch: user
+ pref: "javascript.options.mem.incremental_weakmap"
+ gc_incremental_slice_ms:
+ description: Max milliseconds to spend in an incremental GC slice.
+ type: int
+ setPref:
+ branch: user
+ pref: "javascript.options.mem.gc_incremental_slice_ms"
+ gc_large_heap_incremental_limit:
+ description: Limit of how far over the incremental trigger threshold we allow the
+ heap to grow before finishing a collection non-incrementally, for large heaps.
+ type: int
+ setPref:
+ branch: user
+ pref: "javascript.options.mem.gc_large_heap_incremental_limit"
+ gc_large_heap_size_min_mb:
+ description: Lower limit for classifying a heap as large, in MB.
+ type: int
+ setPref:
+ branch: user
+ pref: "javascript.options.mem.gc_large_heap_size_min_mb"
+ gc_low_frequency_heap_growth:
+ description: Heap growth factor for low frequency GCs.
+ type: int
+ setPref:
+ branch: user
+ pref: "javascript.options.mem.gc_low_frequency_heap_growth"
+ gc_malloc_threshold_base_mb:
+ description: Set the malloc threshold base value in MB.
+ type: int
+ setPref:
+ branch: user
+ pref: "javascript.options.mem.gc_malloc_threshold_base_mb"
+ gc_max_empty_chunk_count:
+ description: Do not keep more than this many unused chunks in the free chunk pool.
+ type: int
+ setPref:
+ branch: user
+ pref: "javascript.options.mem.gc_max_empty_chunk_count"
+ gc_max_helper_threads:
+ description: The maximum number of background threads to use for parallel GC work.
+ type: int
+ setPref:
+ branch: user
+ pref: "javascript.options.mem.gc_max_helper_threads"
+ gc_min_empty_chunk_count:
+ description: We try to keep at least this many unused chunks in the free chunk
+ pool at all times, even after a shrinking GC.
+ type: int
+ setPref:
+ branch: user
+ pref: "javascript.options.mem.gc_min_empty_chunk_count"
+ gc_parallel_marking:
+ description: Enable parallel marking.
+ type: boolean
+ setPref:
+ branch: user
+ pref: "javascript.options.mem.gc_parallel_marking"
+ gc_parallel_marking_threshold_mb:
+ description: The heap size above which to use parallel marking.
+ type: int
+ setPref:
+ branch: user
+ pref: "javascript.options.mem.gc_parallel_marking_threshold_mb"
+ gc_per_zone:
+ description: Whether per-zone GC is enabled. If not, all zones are collected every time.
+ type: boolean
+ setPref:
+ branch: user
+ pref: "javascript.options.mem.gc_per_zone"
+ gc_small_heap_incremental_limit:
+ description: Limit of how far over the incremental trigger threshold we allow the heap
+ to grow before finishing a collection non-incrementally, for small heaps.
+ type: int
+ setPref:
+ branch: user
+ pref: "javascript.options.mem.gc_small_heap_incremental_limit"
+ gc_small_heap_size_max_mb:
+ description: Upper limit for classifying a heap as small, in MB.
+ type: int
+ setPref:
+ branch: user
+ pref: "javascript.options.mem.gc_small_heap_size_max_mb"
+ gc_urgent_threshold_mb:
+ description: Set the urgent threshold, in MB.
+ type: int
+ setPref:
+ branch: user
+ pref: "javascript.options.mem.gc_urgent_threshold_mb"
+ nursery_eager_collection_threshold_kb:
+ description: Set the eager collection threshold, in kb, for the nursery.
+ type: int
+ setPref:
+ branch: user
+ pref: "javascript.options.mem.nursery_eager_collection_threshold_kb"
+ nursery_eager_collection_threshold_percent:
+ description: Set the eager collection percent threshold for the nursery.
+ type: int
+ setPref:
+ branch: user
+ pref: "javascript.options.mem.nursery_eager_collection_threshold_percent"
+ nursery_eager_collection_timeout_ms:
+ description: Set the eager collection timeout, in ms, for the nursery.
+ type: int
+ setPref:
+ branch: user
+ pref: "javascript.options.mem.nursery_eager_collection_timeout_ms"
-gcParallelMarking:
- description: Pref to toggle parallel marking in the GC.
- owner: dpalmeiro@mozilla.com, jonco@mozilla.com
- isEarlyStartup: true
+jsParallelParsing:
+ description: Pref to toggle JS parallel parsing.
+ owner: dpalmeiro@mozilla.com, nbp@mozilla.com
hasExposure: false
variables:
enabled:
- description: True to enable parallel marking.
+ description: True to enable parallel parsing.
type: boolean
setPref:
branch: user
- pref: "javascript.options.mem.gc_parallel_marking"
+ pref: "javascript.options.parallel_parsing"
jitThresholds:
description: Prefs that control jit tier thresholds.
owner: dpalmeiro@mozilla.com, jdemooij@mozilla.com
- isEarlyStartup: true
hasExposure: false
variables:
blinterp_threshold:
@@ -1963,7 +2166,6 @@ jitThresholds:
jitHintsCache:
description: Pref to toggle the JIT hints cache.
owner: dpalmeiro@mozilla.com
- isEarlyStartup: true
hasExposure: false
variables:
enabled:
@@ -1997,17 +2199,6 @@ httpSpeculativeParallelLimit:
branch: default
pref: "network.http.speculative-parallel-limit"
-deviceMigration:
- description: Prefs to control aspects of the new device migration experiment
- owner: hjones@mozilla.com
- hasExposure: false
- isEarlyStartup: true
- variables:
- helpMenuHidden:
- description: True if new help menu item should be hidden
- type: boolean
- fallbackPref: browser.device-migration.help-menu.hidden
-
shopping2023:
description: Prefs to control the 2023 shopping experiment.
owner: jhirsch@mozilla.com
@@ -2069,7 +2260,6 @@ shoppingOHTTP:
opaqueResponseBlocking:
description: Prefs to enable Opaque Response Blocking
owner: farre@mozilla.com
- isEarlyStartup: true
hasExposure: true
exposureDescription: Exposure is sent when a response is blocked
variables:
@@ -2124,7 +2314,6 @@ powerSaver:
description: Prefs to control power saving behaviors
owner: florian@mozilla.com
hasExposure: false
- isEarlyStartup: true
variables:
reduceFrameRates:
type: int
@@ -2165,7 +2354,6 @@ backgroundUpdate:
feature is enabled and the service registry key (Mozilla Maintenance
Service) is *not* available for this installation. That is the first time
the feature can impact Firefox behaviour and the user experience.
- isEarlyStartup: true
variables:
enableUpdatesForUnelevatedInstallations:
description: >-
@@ -2174,7 +2362,7 @@ backgroundUpdate:
directory can be written.
type: boolean
setPref:
- branch: default
+ branch: user
pref: app.update.background.allowUpdatesForUnelevatedInstallations
defaultAgent:
@@ -2195,7 +2383,6 @@ bookmarks:
description: Prefs to control aspects of the bookmarks system.
owner: omc@mozilla.com
hasExposure: false
- isEarlyStartup: true
variables:
enableBookmarksToolbar:
type: string
@@ -2274,7 +2461,6 @@ backgroundThreads:
description: Prefs to control MacOS thread priorities for power savings.
owner: kwright@mozilla.com
hasExposure: false
- isEarlyStartup: true
variables:
use_low_power:
description: >-
@@ -2293,9 +2479,9 @@ backgroundThreads:
pref: threads.lower_mainthread_priority_in_background.enabled
reportBrokenSite:
- description: the Report Broken Site feature
+ description: The Report Broken Site feature
+ owner: twisniewski@mozilla.com
hasExposure: false
- isEarlyStartup: true
variables:
enabled:
type: boolean
@@ -2348,7 +2534,6 @@ phc:
description: Prefs to control the Probabalistic Heap Checker (PHC)
owner: pbone@mozilla.com
hasExposure: false
- isEarlyStartup: true
variables:
phcEnabled:
description: Whether to enable PHC
@@ -2413,3 +2598,53 @@ nimbusIsReady:
eventCount:
description: The number of events that should be sent.
type: int
+
+nimbusTelemetry:
+ description: A feature that enables or disables Nimbus telemetry.
+ owner: chumphreys@mozilla.com
+ hasExposure: false
+ applications:
+ - firefox-desktop
+ variables:
+ gleanMetricConfiguration:
+ description: A Glean metric configuration JSON blob.
+ type: json
+
+httpsFirst:
+ description: >-
+ Prefs for HTTPS-First, which upgrades all top-level page loads to HTTPS and
+ provides a automatic fallback to HTTP if the site isn't available via HTTPS.
+ owner: mjurgens@mozilla.com, seceng-telemetry@mozilla.com
+ hasExposure: false
+ variables:
+ enabled:
+ description: Enable HTTPS-First
+ type: boolean
+ setPref:
+ branch: default
+ pref: dom.security.https_first
+ enabledPbm:
+ description: Enable HTTPS-First in private browsing only
+ type: boolean
+ setPref:
+ branch: default
+ pref: dom.security.https_first_pbm
+ enabledSchemeless:
+ description: >-
+ Enables schemeless HTTPS-First, which will only apply HTTPS-First to address
+ bar inputs without a scheme. This essentially makes HTTPS the default
+ scheme in the address bar, while providing a fallback to HTTP.
+ type: boolean
+ setPref:
+ branch: default
+ pref: dom.security.https_first_schemeless
+ backgroundTimerMs:
+ description: >-
+ After a request gets upgraded to HTTPS, specifies the time after which a
+ second HTTP request is fired to check if the site is available via
+ HTTPS, but timing out via HTTPS. This also applies to HTTPS-Only, not
+ just HTTPS-First.
+ type: int
+ setPref:
+ branch: default
+ pref: dom.security.https_only_fire_http_request_background_timer_ms
diff --git a/toolkit/components/nimbus/generate/generate_feature_manifest.py b/toolkit/components/nimbus/generate/generate_feature_manifest.py
index c17f320184..14057493c5 100644
--- a/toolkit/components/nimbus/generate/generate_feature_manifest.py
+++ b/toolkit/components/nimbus/generate/generate_feature_manifest.py
@@ -14,7 +14,7 @@ HEADER_LINE = (
" DO NOT EDIT.\n"
)
-FEATURE_MANIFEST_SCHEMA = Path("schemas", "ExperimentFeatureManifest.schema.json")
+FEATURE_SCHEMA = Path("schemas", "ExperimentFeature.schema.json")
NIMBUS_FALLBACK_PREFS = (
"constexpr std::pair<nsLiteralCString, nsLiteralCString>"
@@ -26,36 +26,17 @@ NIMBUS_FALLBACK_PREFS = (
ALLOWED_ISEARLYSTARTUP_FEATURE_IDS = {
"abouthomecache",
"aboutwelcome",
- "backgroundThreads",
- "backgroundUpdate",
- "bookmarks",
"dapTelemetry",
- "deviceMigration",
- "frecency",
- "fullPageTranslation",
- "fullPageTranslationAutomaticPopup",
- "fxaButtonVisibility",
- "gcParallelMarking",
"gleanInternalSdk",
- "jitHintsCache",
- "jitThresholds",
- "jsParallelParsing",
"majorRelease2022",
- "migrationWizard",
"newtab",
- "nimbus-qa-2",
- "opaqueResponseBlocking",
- "phc",
"pocketNewtab",
- "powerSaver",
- "reportBrokenSite",
"saveToPocket",
"searchConfiguration",
"shellService",
"testFeature",
"updatePrompt",
"upgradeDialog",
- "windowsJumpList",
}
@@ -91,7 +72,7 @@ def validate_feature_manifest(schema_path, manifest_path, manifest):
f"Feature {feature_id} is not early startup but is in the allow list."
)
print("Please remove it from generate_feature_manifest.py")
- raise Exception("isEarlyStatup is deprecated")
+ raise Exception("isEarlyStartup is deprecated")
for variable, variable_def in feature.get("variables", {}).items():
set_pref = variable_def.get("setPref")
@@ -158,7 +139,7 @@ def generate_feature_manifest(fd, input_file):
manifest = yaml.safe_load(f)
validate_feature_manifest(
- Path(input_file).parent / FEATURE_MANIFEST_SCHEMA, input_file, manifest
+ Path(input_file).parent / FEATURE_SCHEMA, input_file, manifest
)
fd.write(f"export const FeatureManifest = {json.dumps(manifest)};")
diff --git a/toolkit/components/nimbus/lib/ExperimentManager.sys.mjs b/toolkit/components/nimbus/lib/ExperimentManager.sys.mjs
index 8e1acc4803..d0f313a2ae 100644
--- a/toolkit/components/nimbus/lib/ExperimentManager.sys.mjs
+++ b/toolkit/components/nimbus/lib/ExperimentManager.sys.mjs
@@ -32,6 +32,25 @@ const STUDIES_OPT_OUT_PREF = "app.shield.optoutstudies.enabled";
const STUDIES_ENABLED_CHANGED = "nimbus:studies-enabled-changed";
+const ENROLLMENT_STATUS = {
+ ENROLLED: "Enrolled",
+ NOT_ENROLLED: "NotEnrolled",
+ DISQUALIFIED: "Disqualified",
+ WAS_ENROLLED: "WasEnrolled",
+ ERROR: "Error",
+};
+
+const ENROLLMENT_STATUS_REASONS = {
+ QUALIFIED: "Qualified",
+ OPT_IN: "OptIn",
+ OPT_OUT: "OptOut",
+ NOT_SELECTED: "NotSelected",
+ NOT_TARGETED: "NotTargeted",
+ ENROLLMENTS_PAUSED: "EnrollmentsPaused",
+ FEATURE_CONFLICT: "FeatureConflict",
+ ERROR: "Error",
+};
+
function featuresCompat(branch) {
if (!branch || (!branch.feature && !branch.features)) {
return [];
@@ -182,6 +201,14 @@ export class _ExperimentManager {
}
this.observe();
+
+ lazy.NimbusFeatures.nimbusTelemetry.onUpdate(() => {
+ const cfg =
+ lazy.NimbusFeatures.nimbusTelemetry.getVariable(
+ "gleanMetricConfiguration"
+ ) ?? {};
+ Services.fog.setMetricsFeatureConfig(JSON.stringify(cfg));
+ });
}
/**
@@ -223,16 +250,22 @@ export class _ExperimentManager {
missingL10nIds
) {
for (const enrollment of enrollments) {
- const { slug, source } = enrollment;
+ const { slug, source, branch } = enrollment;
if (sourceToCheck !== source) {
continue;
}
+ const statusTelemetry = {
+ slug,
+ branch: branch.slug,
+ };
if (!this.sessions.get(source)?.has(slug)) {
lazy.log.debug(`Stopping study for recipe ${slug}`);
try {
let reason;
if (recipeMismatches.includes(slug)) {
reason = "targeting-mismatch";
+ statusTelemetry.status = ENROLLMENT_STATUS.DISQUALIFIED;
+ statusTelemetry.reason = ENROLLMENT_STATUS_REASONS.NOT_TARGETED;
} else if (invalidRecipes.includes(slug)) {
reason = "invalid-recipe";
} else if (invalidBranches.has(slug) || invalidFeatures.has(slug)) {
@@ -243,12 +276,23 @@ export class _ExperimentManager {
reason = "l10n-missing-entry";
} else {
reason = "recipe-not-seen";
+ statusTelemetry.status = ENROLLMENT_STATUS.WAS_ENROLLED;
+ statusTelemetry.branch = branch.slug;
+ }
+ if (!statusTelemetry.status) {
+ statusTelemetry.status = ENROLLMENT_STATUS.DISQUALIFIED;
+ statusTelemetry.reason = ENROLLMENT_STATUS_REASONS.ERROR;
+ statusTelemetry.error_string = reason;
}
this.unenroll(slug, reason);
} catch (err) {
console.error(err);
}
+ } else {
+ statusTelemetry.status = ENROLLMENT_STATUS.ENROLLED;
+ statusTelemetry.reason = ENROLLMENT_STATUS_REASONS.QUALIFIED;
}
+ this.sendEnrollmentStatusTelemetry(statusTelemetry);
}
}
@@ -685,7 +729,7 @@ export class _ExperimentManager {
/**
* Unenroll from all active studies if user opts out.
*/
- observe(aSubject, aTopic, aPrefName) {
+ observe() {
if (!this.studiesEnabled) {
for (const { slug } of this.store.getAllActiveExperiments()) {
this.unenroll(slug, "studies-opt-out");
@@ -756,6 +800,34 @@ export class _ExperimentManager {
}
/**
+ *
+ * @param {object} enrollmentStatus
+ * @param {string} enrollmentStatus.slug
+ * @param {string} enrollmentStatus.status
+ * @param {string?} enrollmentStatus.reason
+ * @param {string?} enrollmentStatus.branch
+ * @param {string?} enrollmentStatus.error_string
+ * @param {string?} enrollmentStatus.conflict_slug
+ */
+ sendEnrollmentStatusTelemetry({
+ slug,
+ status,
+ reason,
+ branch,
+ error_string,
+ conflict_slug,
+ }) {
+ Glean.nimbusEvents.enrollmentStatus.record({
+ slug,
+ status,
+ reason,
+ branch,
+ error_string,
+ conflict_slug,
+ });
+ }
+
+ /**
* Sets Telemetry when activating an experiment.
*
* @param {Enrollment} experiment
diff --git a/toolkit/components/nimbus/lib/ExperimentStore.sys.mjs b/toolkit/components/nimbus/lib/ExperimentStore.sys.mjs
index 7fd7fd987e..a7a3069fe5 100644
--- a/toolkit/components/nimbus/lib/ExperimentStore.sys.mjs
+++ b/toolkit/components/nimbus/lib/ExperimentStore.sys.mjs
@@ -237,7 +237,7 @@ export class ExperimentStore extends SharedDataMap {
async init() {
await super.init();
- this.getAllActiveExperiments().forEach(({ slug, branch, featureIds }) => {
+ this.getAllActiveExperiments().forEach(({ branch, featureIds }) => {
(featureIds || getAllBranchFeatureIds(branch)).forEach(featureId =>
this._emitFeatureUpdate(featureId, "feature-experiment-loaded")
);
diff --git a/toolkit/components/nimbus/lib/RemoteSettingsExperimentLoader.sys.mjs b/toolkit/components/nimbus/lib/RemoteSettingsExperimentLoader.sys.mjs
index 8e026e5cba..e372cf57a5 100644
--- a/toolkit/components/nimbus/lib/RemoteSettingsExperimentLoader.sys.mjs
+++ b/toolkit/components/nimbus/lib/RemoteSettingsExperimentLoader.sys.mjs
@@ -350,7 +350,7 @@ export class _RemoteSettingsExperimentLoader {
}
}
- observe(aSubect, aTopic, aData) {
+ observe(aSubect, aTopic) {
if (aTopic === STUDIES_ENABLED_CHANGED) {
this.onEnabledPrefChange();
}
diff --git a/toolkit/components/nimbus/metrics.yaml b/toolkit/components/nimbus/metrics.yaml
index 4faecce490..5b5ff4bd3a 100644
--- a/toolkit/components/nimbus/metrics.yaml
+++ b/toolkit/components/nimbus/metrics.yaml
@@ -223,10 +223,46 @@ nimbus_events:
An event sent when Nimbus is ready — sent upon completion of each update of the recipes.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1875510
- data_reviews: []
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1875510
data_sensitivity:
- technical
notification_emails:
- chumphreys@mozilla.com
- project-nimbus@mozilla.com
expires: 180
+
+ enrollment_status:
+ type: event
+ description: >
+ Recorded for each enrollment status each time the SDK completes application of pending experiments.
+ extra_keys:
+ slug:
+ type: string
+ description: The slug/unique identifier of the experiment
+ status:
+ type: string
+ description: The status of this enrollment
+ reason:
+ type: string
+ description: The reason the client is in the noted status
+ branch:
+ type: string
+ description: The branch slug/identifier that was randomly chosen (if the client is enrolled)
+ error_string:
+ type: string
+ description: If the enrollment resulted in an error, the associated error string
+ conflict_slug:
+ type: string
+ description: If the enrollment hit a feature conflict, the slug of the conflicting experiment/rollout
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1817481
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1817481
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - chumphreys@mozilla.com
+ - project-nimbus@mozilla.com
+ expires: never
+ disabled: true
diff --git a/toolkit/components/nimbus/schemas/ExperimentFeature.schema.json b/toolkit/components/nimbus/schemas/ExperimentFeature.schema.json
new file mode 100644
index 0000000000..977c18e09e
--- /dev/null
+++ b/toolkit/components/nimbus/schemas/ExperimentFeature.schema.json
@@ -0,0 +1,125 @@
+{
+ "$schema": "https://json-schema.org/draft/2019-09/schema",
+ "additionalProperties": false,
+ "type": "object",
+ "properties": {
+ "description": {
+ "type": "string"
+ },
+ "owner": {
+ "type": "string",
+ "description": "The owner of the feature."
+ },
+ "applications": {
+ "description": "The applications that can enroll in experiments for this feature. Defaults to firefox-desktop if not present.",
+ "type": "array",
+ "items": {
+ "type": "string",
+ "enum": ["firefox-desktop", "firefox-desktop-background-task"]
+ },
+ "minItems": 1
+ },
+ "hasExposure": {
+ "type": "boolean",
+ "description": "If the feature sends an exposure event."
+ },
+ "exposureDescription": {
+ "type": "string",
+ "description": "A description of the implementation details of the exposure event, if one is sent."
+ },
+ "isEarlyStartup": {
+ "type": "boolean",
+ "description": "If the feature values should be cached in prefs for fast early startup."
+ },
+ "schema": {
+ "type": "object",
+ "description": "For features with large number of variables we instead point to a JSONSchema file instead of specifying them in the variables field",
+ "properties": {
+ "uri": {
+ "type": "string",
+ "description": "A resource:// URI that can be loaded at runtime from within Firefox.",
+ "format": "uri"
+ },
+ "path": {
+ "type": "string",
+ "description": "The path to the schema file relative to the repository root"
+ }
+ },
+ "required": ["uri", "path"]
+ },
+ "variables": {
+ "additionalProperties": false,
+ "type": "object",
+ "patternProperties": {
+ "[a-zA-Z0-9_]+": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": ["json", "boolean", "int", "string"]
+ },
+ "fallbackPref": {
+ "type": "string",
+ "description": "A pref that provides the default value for a feature when none is present"
+ },
+ "setPref": {
+ "description": "A pref that should be set to the value of this variable when enrolling in experiments.",
+ "type": "object",
+ "properties": {
+ "branch": {
+ "type": "string",
+ "enum": ["default", "user"],
+ "description": "The branch the pref will be set on."
+ },
+ "pref": {
+ "type": "string",
+ "description": "The name of the pref."
+ }
+ },
+ "required": ["branch", "pref"],
+ "additionalProperties": false
+ },
+ "enum": {
+ "description": "Validate feature value using a list of possible options (for string only values)."
+ },
+ "description": {
+ "type": "string",
+ "description": "Explain how this value is being used"
+ }
+ },
+ "required": ["type", "description"],
+ "additionalProperties": false,
+ "dependentSchemas": {
+ "fallbackPref": {
+ "description": "setPref is mutually exclusive with fallbackPref",
+ "properties": {
+ "setPref": {
+ "const": null
+ }
+ }
+ },
+ "setPref": {
+ "description": "fallbackPref is mutually exclusive with setPref",
+ "properties": {
+ "fallbackPref": {
+ "const": null
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "required": ["description", "hasExposure", "owner", "variables"],
+ "if": {
+ "properties": {
+ "hasExposure": {
+ "const": true
+ }
+ }
+ },
+ "then": {
+ "required": ["exposureDescription"]
+ }
+}
diff --git a/toolkit/components/nimbus/schemas/ExperimentFeatureManifest.schema.json b/toolkit/components/nimbus/schemas/ExperimentFeatureManifest.schema.json
index c25b7dbf69..ee28f7d93e 100644
--- a/toolkit/components/nimbus/schemas/ExperimentFeatureManifest.schema.json
+++ b/toolkit/components/nimbus/schemas/ExperimentFeatureManifest.schema.json
@@ -1,125 +1,8 @@
{
"$schema": "https://json-schema.org/draft/2019-09/schema",
- "additionalProperties": false,
"type": "object",
- "properties": {
- "description": {
- "type": "string"
- },
- "owner": {
- "type": "string",
- "description": "The owner of the feature."
- },
- "applications": {
- "description": "The applications that can enroll in experiments for this feature. Defaults to firefox-desktop if not present.",
- "type": "array",
- "items": {
- "type": "string",
- "enum": ["firefox-desktop", "firefox-desktop-background-task"]
- },
- "minItems": 1
- },
- "hasExposure": {
- "type": "boolean",
- "description": "If the feature sends an exposure event."
- },
- "exposureDescription": {
- "type": "string",
- "description": "A description of the implementation details of the exposure event, if one is sent."
- },
- "isEarlyStartup": {
- "type": "boolean",
- "description": "If the feature values should be cached in prefs for fast early startup."
- },
- "schema": {
- "type": "object",
- "description": "For features with large number of variables we instead point to a JSONSchema file instead of specifying them in the variables field",
- "properties": {
- "uri": {
- "type": "string",
- "description": "A resource:// URI that can be loaded at runtime from within Firefox.",
- "format": "uri"
- },
- "path": {
- "type": "string",
- "description": "The path to the schema file relative to the repository root"
- }
- },
- "required": ["uri", "path"]
- },
- "variables": {
- "additionalProperties": false,
- "type": "object",
- "patternProperties": {
- "[a-zA-Z0-9_]+": {
- "type": "object",
- "properties": {
- "type": {
- "type": "string",
- "enum": ["json", "boolean", "int", "string"]
- },
- "fallbackPref": {
- "type": "string",
- "description": "A pref that provides the default value for a feature when none is present"
- },
- "setPref": {
- "description": "A pref that should be set to the value of this variable when enrolling in experiments.",
- "type": "object",
- "properties": {
- "branch": {
- "type": "string",
- "enum": ["default", "user"],
- "description": "The branch the pref will be set on."
- },
- "pref": {
- "type": "string",
- "description": "The name of the pref."
- }
- },
- "required": ["branch", "pref"],
- "additionalProperties": false
- },
- "enum": {
- "description": "Validate feature value using a list of possible options (for string only values)."
- },
- "description": {
- "type": "string",
- "description": "Explain how this value is being used"
- }
- },
- "required": ["type", "description"],
- "additionalProperties": false,
- "dependentSchemas": {
- "fallbackPref": {
- "description": "setPref is mutually exclusive with fallbackPref",
- "properties": {
- "setPref": {
- "const": null
- }
- }
- },
- "setPref": {
- "description": "fallbackPref is mutually exclusive with setPref",
- "properties": {
- "fallbackPref": {
- "const": null
- }
- }
- }
- }
- }
- }
- }
- },
- "required": ["description", "hasExposure", "variables"],
- "if": {
- "properties": {
- "hasExposure": {
- "const": true
- }
- }
- },
- "then": {
- "required": ["exposureDescription"]
+ "additionalProperties": false,
+ "patternProperties": {
+ "^[A-Za-z0-9_-]*$": { "$ref": "ExperimentFeature.schema.json" }
}
}
diff --git a/toolkit/components/nimbus/test/unit/test_ExperimentManager_unenroll.js b/toolkit/components/nimbus/test/unit/test_ExperimentManager_unenroll.js
index 3c53148c7a..a32de32cd6 100644
--- a/toolkit/components/nimbus/test/unit/test_ExperimentManager_unenroll.js
+++ b/toolkit/components/nimbus/test/unit/test_ExperimentManager_unenroll.js
@@ -6,6 +6,9 @@ const { TelemetryEvents } = ChromeUtils.importESModule(
const { TelemetryEnvironment } = ChromeUtils.importESModule(
"resource://gre/modules/TelemetryEnvironment.sys.mjs"
);
+const { ExperimentAPI } = ChromeUtils.importESModule(
+ "resource://nimbus/ExperimentAPI.sys.mjs"
+);
const STUDIES_OPT_OUT_PREF = "app.shield.optoutstudies.enabled";
const UPLOAD_ENABLED_PREF = "datareporting.healthreport.uploadEnabled";
@@ -487,3 +490,112 @@ add_task(async function test_rollout_telemetry_events() {
);
globalSandbox.restore();
});
+
+add_task(async function test_check_unseen_enrollments_telemetry_events() {
+ globalSandbox.restore();
+ const store = ExperimentFakes.store();
+ const manager = ExperimentFakes.manager(store);
+ const sandbox = sinon.createSandbox();
+ sandbox.stub(manager, "unenroll").returns();
+ sandbox.stub(ExperimentAPI, "_store").get(() => manager.store);
+ sandbox.stub(ExperimentAPI, "_manager").get(() => manager);
+
+ await manager.onStartup();
+ await manager.store.ready();
+
+ const experiment = ExperimentFakes.recipe("foo", {
+ branches: [
+ {
+ slug: "wsup",
+ ratio: 1,
+ features: [
+ {
+ featureId: "nimbusTelemetry",
+ value: {
+ gleanMetricConfiguration: {
+ "nimbus_events.enrollment_status": true,
+ },
+ },
+ },
+ ],
+ },
+ ],
+ bucketConfig: {
+ ...ExperimentFakes.recipe.bucketConfig,
+ count: 1000,
+ },
+ });
+
+ await manager.enroll(experiment, "aaa");
+
+ const source = "test";
+ const slugs = [],
+ experiments = [];
+ for (let i = 0; i < 7; i++) {
+ slugs.push(`slug-${i}`);
+ experiments.push({
+ slug: slugs[i],
+ source,
+ branch: {
+ slug: "control",
+ },
+ });
+ }
+
+ manager.sessions.set(source, new Set([slugs[0]]));
+
+ manager._checkUnseenEnrollments(
+ experiments,
+ source,
+ [slugs[1]],
+ [slugs[2]],
+ new Map([]),
+ new Map([[slugs[3], experiments[3]]]),
+ [slugs[4]],
+ new Map([[slugs[5], experiments[5]]])
+ );
+
+ const events = Glean.nimbusEvents.enrollmentStatus.testGetValue();
+
+ Assert.equal(events?.length, 7);
+
+ Assert.equal(events[0].extra.status, "Enrolled");
+ Assert.equal(events[0].extra.reason, "Qualified");
+ Assert.equal(events[0].extra.branch, "control");
+ Assert.equal(events[0].extra.slug, slugs[0]);
+
+ Assert.equal(events[1].extra.status, "Disqualified");
+ Assert.equal(events[1].extra.reason, "NotTargeted");
+ Assert.equal(events[1].extra.branch, "control");
+ Assert.equal(events[1].extra.slug, slugs[1]);
+
+ Assert.equal(events[2].extra.status, "Disqualified");
+ Assert.equal(events[2].extra.reason, "Error");
+ Assert.equal(events[2].extra.error_string, "invalid-recipe");
+ Assert.equal(events[2].extra.branch, "control");
+ Assert.equal(events[2].extra.slug, slugs[2]);
+
+ Assert.equal(events[3].extra.status, "Disqualified");
+ Assert.equal(events[3].extra.reason, "Error");
+ Assert.equal(events[3].extra.error_string, "invalid-branch");
+ Assert.equal(events[3].extra.branch, "control");
+ Assert.equal(events[3].extra.slug, slugs[3]);
+
+ Assert.equal(events[4].extra.status, "Disqualified");
+ Assert.equal(events[4].extra.reason, "Error");
+ Assert.equal(events[4].extra.error_string, "l10n-missing-locale");
+ Assert.equal(events[4].extra.branch, "control");
+ Assert.equal(events[4].extra.slug, slugs[4]);
+
+ Assert.equal(events[5].extra.status, "Disqualified");
+ Assert.equal(events[5].extra.reason, "Error");
+ Assert.equal(events[5].extra.error_string, "l10n-missing-entry");
+ Assert.equal(events[5].extra.branch, "control");
+ Assert.equal(events[5].extra.slug, slugs[5]);
+
+ Assert.equal(events[6].extra.status, "WasEnrolled");
+ Assert.equal(events[6].extra.branch, "control");
+ Assert.equal(events[6].extra.slug, slugs[6]);
+
+ sandbox.restore();
+});
diff --git a/toolkit/components/nimbus/test/unit/test_SharedDataMap.js b/toolkit/components/nimbus/test/unit/test_SharedDataMap.js
index 6186b41a40..84132ab73d 100644
--- a/toolkit/components/nimbus/test/unit/test_SharedDataMap.js
+++ b/toolkit/components/nimbus/test/unit/test_SharedDataMap.js
@@ -115,7 +115,6 @@ with_sharedDataMap(async function test_childInit({ instance, sandbox }) {
with_sharedDataMap(async function test_parentChildSync_synchronously({
instance: parentInstance,
- sandbox,
}) {
await parentInstance.init();
parentInstance.set("foo", { bar: 1 });
@@ -142,7 +141,6 @@ with_sharedDataMap(async function test_parentChildSync_synchronously({
with_sharedDataMap(async function test_parentChildSync_async({
instance: parentInstance,
- sandbox,
}) {
const childInstance = new SharedDataMap("xpcshell", {
path: PATH,
@@ -169,7 +167,6 @@ with_sharedDataMap(async function test_parentChildSync_async({
with_sharedDataMap(async function test_earlyChildSync({
instance: parentInstance,
- sandbox,
}) {
const childInstance = new SharedDataMap("xpcshell", {
path: PATH,
@@ -193,7 +190,7 @@ with_sharedDataMap(async function test_earlyChildSync({
);
});
-with_sharedDataMap(async function test_updateStoreData({ instance, sandbox }) {
+with_sharedDataMap(async function test_updateStoreData({ instance }) {
await instance.init();
Assert.ok(!instance.get("foo"), "No value initially");