commit 0e3ebebbc4800a65a9908ada3577e180f9c34a3a
parent 7adaf54896c4186b448df89c85a381a2922b44ea
Author: Chris H-C <chutten@mozilla.com>
Date: Mon, 8 Dec 2025 21:42:50 +0000
Bug 1999541 - Implement a Legacy Telemetry shutoff switch for pings controlled by Nimbus r=TravisLong
We preserve "main", "first-shutdown", "new-profile", and "deletion-request" pings
from being disable-able by this method to prevent unintended effects on KPIs or
our ability to self-serve data deletion.
Differential Revision: https://phabricator.services.mozilla.com/D274464
Diffstat:
4 files changed, 259 insertions(+), 0 deletions(-)
diff --git a/toolkit/components/nimbus/FeatureManifest.yaml b/toolkit/components/nimbus/FeatureManifest.yaml
@@ -2738,6 +2738,23 @@ gleanInternalSdk:
branch: user
pref: telemetry.glean.internal.maxPingsPerMinute
+legacyTelemetry:
+ description: "Controls for the Legacy Telemetry data collection system"
+ owner: chutten@mozilla.com
+ applications:
+ - firefox-desktop
+ hasExposure: false
+ variables:
+ disabledPings:
+ type: json
+ description: |
+ A list of Legacy Telemetry pings to disable.
+ Pings on this list will not be archived or uploaded.
+ On submit their payloads will be dropped.
+ Code that collects to and submits the ping will still operate as normal.
+ Cannot be used to disable the "main", "first-shutdown", "new-profile",
+ or "deletion-request" pings.
+
browserLowMemoryPrefs:
description: Prefs which control the browser's behaviour under low memory.
owner: haftandilian@mozilla.com
diff --git a/toolkit/components/telemetry/app/TelemetryControllerParent.sys.mjs b/toolkit/components/telemetry/app/TelemetryControllerParent.sys.mjs
@@ -38,6 +38,7 @@ const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
ClientID: "resource://gre/modules/ClientID.sys.mjs",
CoveragePing: "resource://gre/modules/CoveragePing.sys.mjs",
+ NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
TelemetryArchive: "resource://gre/modules/TelemetryArchive.sys.mjs",
TelemetryEnvironment: "resource://gre/modules/TelemetryEnvironment.sys.mjs",
TelemetryEventPing: "resource://gre/modules/EventPing.sys.mjs",
@@ -528,6 +529,27 @@ var Impl = {
JSON.stringify(aOptions)
);
+ const disabledPings =
+ lazy.NimbusFeatures.legacyTelemetry.getVariable("disabledPings") ?? [];
+ const UNCONTROLLABLE_PINGS = [
+ "main",
+ "first-shutdown",
+ "new-profile",
+ "deletion-request",
+ ];
+ if (disabledPings.includes(aType)) {
+ if (UNCONTROLLABLE_PINGS.includes(aType)) {
+ this._log.warn(
+ `submitExternalPing - type: ${aType} not controllable, but is in the list of disabledPings ${JSON.stringify(disabledPings)}. Ping will submit as normal. Please remove ping type "${aType}" from the Nimbus config.`
+ );
+ } else {
+ this._log.trace(
+ `submitExternalPing - type ${aType} disabled by Nimbus.`
+ );
+ return Promise.reject(new Error("Ping disabled."));
+ }
+ }
+
// Reject pings sent after shutdown.
if (this._shutDown) {
const errorMessage =
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryController_pingDisablement.js b/toolkit/components/telemetry/tests/unit/test_TelemetryController_pingDisablement.js
@@ -0,0 +1,215 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+
+const { NimbusFeatures } = ChromeUtils.importESModule(
+ "resource://nimbus/ExperimentAPI.sys.mjs"
+);
+const { NimbusTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/NimbusTestUtils.sys.mjs"
+);
+const { TelemetryController } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryController.sys.mjs"
+);
+const { TelemetrySend } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetrySend.sys.mjs"
+);
+const { TelemetryUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryUtils.sys.mjs"
+);
+
+NimbusTestUtils.init(this);
+
+const PING_FORMAT_VERSION = 4;
+const TEST_PING_TYPE = "test-ping-type";
+
+function sendPing(aSendClientId, aSendEnvironment) {
+ if (PingServer.started) {
+ const server = "http://localhost:" + PingServer.port;
+ TelemetrySend.setServer(server);
+ Services.prefs.setStringPref(TelemetryUtils.Preferences.Server, server);
+ } else {
+ TelemetrySend.setServer("http://doesnotexist");
+ }
+
+ let options = {
+ addClientId: aSendClientId,
+ addEnvironment: aSendEnvironment,
+ };
+ return TelemetryController.submitExternalPing(TEST_PING_TYPE, {}, options);
+}
+
+function checkPingFormat(aPing, aType, aHasClientId, aHasEnvironment) {
+ const MANDATORY_PING_FIELDS = [
+ "type",
+ "id",
+ "creationDate",
+ "version",
+ "application",
+ "payload",
+ ];
+
+ const APPLICATION_TEST_DATA = {
+ buildId: gAppInfo.appBuildID,
+ name: APP_NAME,
+ version: APP_VERSION,
+ displayVersion: AppConstants.MOZ_APP_VERSION_DISPLAY,
+ vendor: "Mozilla",
+ platformVersion: PLATFORM_VERSION,
+ xpcomAbi: "noarch-spidermonkey",
+ };
+
+ // Check that the ping contains all the mandatory fields.
+ for (let f of MANDATORY_PING_FIELDS) {
+ Assert.ok(f in aPing, f + " must be available.");
+ }
+
+ Assert.equal(aPing.type, aType, "The ping must have the correct type.");
+ Assert.equal(
+ aPing.version,
+ PING_FORMAT_VERSION,
+ "The ping must have the correct version."
+ );
+
+ // Test the application section.
+ for (let f in APPLICATION_TEST_DATA) {
+ Assert.equal(
+ aPing.application[f],
+ APPLICATION_TEST_DATA[f],
+ f + " must have the correct value."
+ );
+ }
+
+ // We can't check the values for channel and architecture. Just make
+ // sure they are in.
+ Assert.ok(
+ "architecture" in aPing.application,
+ "The application section must have an architecture field."
+ );
+ Assert.ok(
+ "channel" in aPing.application,
+ "The application section must have a channel field."
+ );
+
+ // Check the clientId and environment fields, as needed.
+ Assert.equal("clientId" in aPing, aHasClientId);
+ Assert.equal("profileGroupId" in aPing, aHasClientId);
+ Assert.equal("environment" in aPing, aHasEnvironment);
+}
+
+add_task(async function test_setup() {
+ // Addon manager needs a profile directory
+ do_get_profile();
+ await loadAddonManager(
+ "xpcshell@tests.mozilla.org",
+ "XPCShell",
+ "1",
+ "1.9.2"
+ );
+ finishAddonManagerStartup();
+ fakeIntlReady();
+ // Make sure we don't generate unexpected pings due to pref changes.
+ await setEmptyPrefWatchlist();
+
+ Services.prefs.setBoolPref(TelemetryUtils.Preferences.FhrUploadEnabled, true);
+
+ await TelemetryController.testSetup();
+ PingServer.start();
+});
+
+add_task(async function test_pingDisablement() {
+ await TelemetryController.testReset();
+ PingServer.clearRequests();
+
+ info("1. Ensure test pings can be sent.");
+ let docid = await sendPing(false, false);
+ let request = await PingServer.promiseNextRequest();
+
+ let ping = decodeRequestPayload(request);
+ Assert.equal(docid, ping.id, "Server delivered the ping we just submitted.");
+ checkPingFormat(ping, TEST_PING_TYPE, false, false);
+
+ info("2. Ensure we can disable a ping by name.");
+ const { cleanup } = await NimbusTestUtils.setupTest();
+ registerCleanupFunction(cleanup);
+
+ let nimbusCleanup = await NimbusTestUtils.enrollWithFeatureConfig({
+ featureId: NimbusFeatures.legacyTelemetry.featureId,
+ value: {
+ disabledPings: [TEST_PING_TYPE],
+ },
+ });
+
+ PingServer.registerPingHandler(() =>
+ Assert.ok(false, "Telemetry must not send the disabled ping.")
+ );
+ await Assert.rejects(
+ sendPing(true, true),
+ /Ping disabled/,
+ "Disabled ping should not send."
+ );
+
+ PingServer.resetPingHandler();
+
+ info("3. Ensure disabling one kind of ping doesn't disable others.");
+ const OTHER_PING_TYPE = TEST_PING_TYPE + "-other";
+ await TelemetryController.submitExternalPing(OTHER_PING_TYPE, {}, {});
+ request = await PingServer.promiseNextRequest();
+
+ ping = decodeRequestPayload(request);
+ checkPingFormat(ping, OTHER_PING_TYPE, false, false);
+
+ await nimbusCleanup();
+});
+
+add_task(async function test_cantDisableImportantPings() {
+ await TelemetryController.testReset();
+ PingServer.clearRequests();
+
+ const DO_NOT_DISABLE_THESE_PINGS = [
+ "main",
+ "first-shutdown",
+ "new-profile",
+ "deletion-request",
+ ];
+ const PINGS = [TEST_PING_TYPE, ...DO_NOT_DISABLE_THESE_PINGS];
+ let nimbusCleanup = await NimbusTestUtils.enrollWithFeatureConfig({
+ featureId: NimbusFeatures.legacyTelemetry.featureId,
+ value: {
+ disabledPings: PINGS,
+ },
+ });
+
+ for (const pingName of PINGS) {
+ info("Check " + pingName);
+ if (DO_NOT_DISABLE_THESE_PINGS.includes(pingName)) {
+ let docid = await TelemetryController.submitExternalPing(
+ pingName,
+ {},
+ {}
+ );
+ let request = await PingServer.promiseNextRequest();
+ let ping = decodeRequestPayload(request);
+ Assert.equal(
+ docid,
+ ping.id,
+ "Server delivered the ping we just submitted."
+ );
+ checkPingFormat(ping, pingName, false, false);
+ // Ensure we don't get throttled for too many pings in a row.
+ await TelemetrySend.reset();
+ } else {
+ PingServer.registerPingHandler(() =>
+ Assert.ok(false, "Telemetry must not send the disabled ping.")
+ );
+ await Assert.rejects(
+ TelemetryController.submitExternalPing(pingName, {}, {}),
+ /Ping disabled/,
+ "Disabled ping should not send."
+ );
+ PingServer.resetPingHandler();
+ }
+ }
+
+ await nimbusCleanup();
+});
diff --git a/toolkit/components/telemetry/tests/unit/xpcshell.toml b/toolkit/components/telemetry/tests/unit/xpcshell.toml
@@ -102,6 +102,11 @@ run-if = [
"os != 'android'", # Legacy telemetry is a lways disabled on Android
]
+["test_TelemetryController_pingDisablement.js"]
+run-if = [
+ "os != 'android'", # Legacy telemetry is always disabled on Android
+]
+
["test_TelemetryEnvironment.js"]
requesttimeoutfactor = 2 # Slow on Windows
skip-if = [