From fbaf0bb26397aa498eb9156f06d5a6fe34dd7dd8 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 03:14:29 +0200 Subject: Merging upstream version 125.0.1. Signed-off-by: Daniel Baumann --- toolkit/components/nimbus/FeatureManifest.yaml | 321 ++++++++++++++++++--- .../nimbus/generate/generate_feature_manifest.py | 25 +- .../nimbus/lib/ExperimentManager.sys.mjs | 76 ++++- .../components/nimbus/lib/ExperimentStore.sys.mjs | 2 +- .../lib/RemoteSettingsExperimentLoader.sys.mjs | 2 +- toolkit/components/nimbus/metrics.yaml | 38 ++- .../nimbus/schemas/ExperimentFeature.schema.json | 125 ++++++++ .../schemas/ExperimentFeatureManifest.schema.json | 123 +------- .../test/unit/test_ExperimentManager_unenroll.js | 112 +++++++ .../nimbus/test/unit/test_SharedDataMap.js | 5 +- 10 files changed, 635 insertions(+), 194 deletions(-) create mode 100644 toolkit/components/nimbus/schemas/ExperimentFeature.schema.json (limited to 'toolkit/components/nimbus') 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" @@ -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"); @@ -755,6 +799,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. * 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"); -- cgit v1.2.3