tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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:
Mtoolkit/components/nimbus/FeatureManifest.yaml | 17+++++++++++++++++
Mtoolkit/components/telemetry/app/TelemetryControllerParent.sys.mjs | 22++++++++++++++++++++++
Atoolkit/components/telemetry/tests/unit/test_TelemetryController_pingDisablement.js | 215+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtoolkit/components/telemetry/tests/unit/xpcshell.toml | 5+++++
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 = [