commit afd1f9234246a29f20413c1efc1ad176c9fd4604
parent 965f7176dd09e26daca9efbfdf23dce16da43e97
Author: Chris H-C <chutten@mozilla.com>
Date: Tue, 2 Dec 2025 17:14:49 +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:
3 files changed, 147 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.js b/toolkit/components/telemetry/tests/unit/test_TelemetryController.js
@@ -11,6 +11,12 @@
const { ClientID } = ChromeUtils.importESModule(
"resource://gre/modules/ClientID.sys.mjs"
);
+const { ExperimentAPI, 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"
);
@@ -33,6 +39,8 @@ const { TestUtils } = ChromeUtils.importESModule(
"resource://testing-common/TestUtils.sys.mjs"
);
+NimbusTestUtils.init(this);
+
const PING_FORMAT_VERSION = 4;
const DELETION_REQUEST_PING_TYPE = "deletion-request";
const TEST_PING_TYPE = "test-ping-type";
@@ -897,6 +905,106 @@ add_task(async function test_sendNewProfile() {
PingServer.resetPingHandler();
});
+add_task({ skip_if: () => gIsAndroid }, 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);
+ await ExperimentAPI.ready();
+ 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(
+ { skip_if: () => gIsAndroid },
+ 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();
+ }
+);
+
// Testing shutdown and checking that pings sent afterwards are rejected.
add_task(async function test_pingRejection() {
await TelemetryController.testReset();