mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-24 05:11:16 +00:00
Bug 1697555 - Remove Telemetry portions of Accounts Ecosystem Telemetry r=Dexter
Differential Revision: https://phabricator.services.mozilla.com/D110091
This commit is contained in:
parent
2aac69d13c
commit
ea2a18d757
@ -612,7 +612,6 @@ module.exports = {
|
||||
"toolkit/components/passwordmgr/test/unit/test_getUserNameAndPasswordFields.js",
|
||||
"toolkit/components/processsingleton/MainProcessSingleton.jsm",
|
||||
"toolkit/components/telemetry/tests/unit/head.js",
|
||||
"toolkit/components/telemetry/tests/unit/test_EcosystemTelemetry.js",
|
||||
"toolkit/components/telemetry/tests/unit/test_EventPing.js",
|
||||
"toolkit/components/telemetry/tests/unit/test_HealthPing.js",
|
||||
"toolkit/components/telemetry/tests/unit/test_PingAPI.js",
|
||||
|
@ -1681,8 +1681,6 @@ pref("toolkit.telemetry.newProfilePing.enabled", true);
|
||||
pref("toolkit.telemetry.updatePing.enabled", true);
|
||||
// Enables sending 'bhr' pings when the browser hangs.
|
||||
pref("toolkit.telemetry.bhrPing.enabled", true);
|
||||
// Whether to enable Ecosystem Telemetry, requires a restart.
|
||||
pref("toolkit.telemetry.ecosystemtelemetry.enabled", false);
|
||||
|
||||
// Ping Centre Telemetry settings.
|
||||
pref("browser.ping-centre.telemetry", true);
|
||||
|
@ -126,7 +126,7 @@ add_task(async function test_discovery() {
|
||||
equal(cookie.host, uri.host, "cookie exists for host");
|
||||
return true;
|
||||
});
|
||||
await ClientID.removeClientIDs();
|
||||
await ClientID.removeClientID();
|
||||
await ClientID.getClientID();
|
||||
await changed;
|
||||
|
||||
|
@ -68,7 +68,6 @@ class PingServer(Layer):
|
||||
"toolkit.telemetry.enabled": True,
|
||||
"toolkit.telemetry.unified": True,
|
||||
"toolkit.telemetry.shutdownPingSender.enabled": True,
|
||||
"toolkit.telemetry.ecosystemtelemetry.enabled": True,
|
||||
"datareporting.policy.dataSubmissionPolicyBypassNotification": True,
|
||||
"toolkit.telemetry.send.overrideOfficialCheck": True,
|
||||
}
|
||||
|
@ -12,7 +12,6 @@ pref("toolkit.telemetry.firstShutdownPing.enabled", false);
|
||||
pref("toolkit.telemetry.healthping.enabled", false);
|
||||
pref("toolkit.telemetry.newProfilePing.enabled", false);
|
||||
pref("toolkit.telemetry.eventping.enabled", false);
|
||||
pref("toolkit.telemetry.ecosystemtelemetry.enabled", false);
|
||||
pref("toolkit.telemetry.prioping.enabled", false);
|
||||
pref("datareporting.policy.dataSubmissionEnabled", false);
|
||||
pref("datareporting.healthreport.uploadEnabled", false);
|
||||
|
@ -47,9 +47,6 @@ XPCOMUtils.defineLazyGetter(this, "gStateFilePath", () => {
|
||||
|
||||
const PREF_CACHED_CLIENTID = "toolkit.telemetry.cachedClientID";
|
||||
|
||||
const SCALAR_DELETION_REQUEST_ECOSYSTEM_CLIENT_ID =
|
||||
"deletion.request.ecosystem_client_id";
|
||||
|
||||
/**
|
||||
* Checks if client ID has a valid format.
|
||||
*
|
||||
@ -78,16 +75,6 @@ var ClientID = Object.freeze({
|
||||
return ClientIDImpl.getClientID();
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns a promise resolving to the ecosystem client ID, used in ecosystem
|
||||
* pings as a stable identifier for this profile.
|
||||
*
|
||||
* @return {Promise<string>} The ecosystem client ID.
|
||||
*/
|
||||
getEcosystemClientID() {
|
||||
return ClientIDImpl.getEcosystemClientID();
|
||||
},
|
||||
|
||||
/**
|
||||
* This returns true if the client ID prior to the last client ID reset was a canary client ID.
|
||||
* Android only. Always returns null on Desktop.
|
||||
@ -111,62 +98,39 @@ var ClientID = Object.freeze({
|
||||
return ClientIDImpl.getCachedClientID();
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the in-memory cached ecosystem client ID if it was already loaded;
|
||||
* `null` otherwise.
|
||||
*/
|
||||
getCachedEcosystemClientID() {
|
||||
return ClientIDImpl.getCachedEcosystemClientID();
|
||||
},
|
||||
|
||||
async getClientIdHash() {
|
||||
return ClientIDImpl.getClientIdHash();
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the main and ecosystem client IDs to the canary (known) client ID,
|
||||
* writing them to disk and updating the cached versions.
|
||||
* Sets the client ID to the canary (known) client ID,
|
||||
* writing it to disk and updating the cached version.
|
||||
*
|
||||
* Use `removeClientIDs` followed by `get{Ecosystem}ClientID` to clear the
|
||||
* existing IDs and generate new, random ones if required.
|
||||
* Use `removeClientID` followed by `getClientID` to clear the
|
||||
* existing ID and generate a new, random one if required.
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
setCanaryClientIDs() {
|
||||
return ClientIDImpl.setCanaryClientIDs();
|
||||
setCanaryClientID() {
|
||||
return ClientIDImpl.setCanaryClientID();
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the ecosystem client IDs to a new random value while leaving other IDs
|
||||
* unchanged, writing the result to disk and updating the cached identifier.
|
||||
* This can be used when a user signs out, to avoid linking telemetry between
|
||||
* different accounts.
|
||||
*
|
||||
* Use `removeClientIDs` followed by `get{Ecosystem}ClientID` to reset *all* the
|
||||
* identifiers rather than just the ecosystem client id.
|
||||
*
|
||||
* @return {Promise<void>} Resolves when the change has been saved to disk.
|
||||
*/
|
||||
resetEcosystemClientID() {
|
||||
return ClientIDImpl.resetEcosystemClientID();
|
||||
},
|
||||
|
||||
/**
|
||||
* Clears the main and ecosystem client IDs asynchronously, removing them
|
||||
* from disk. Use `getClientID()` and `getEcosystemClientID()` to generate
|
||||
* fresh IDs after calling this method.
|
||||
* Clears the client ID asynchronously, removing it
|
||||
* from disk. Use `getClientID()` to generate
|
||||
* a fresh ID after calling this method.
|
||||
*
|
||||
* Should only be used if a reset is explicitly requested by the user.
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
removeClientIDs() {
|
||||
return ClientIDImpl.removeClientIDs();
|
||||
removeClientID() {
|
||||
return ClientIDImpl.removeClientID();
|
||||
},
|
||||
|
||||
/**
|
||||
* Only used for testing. Invalidates the cached client IDs so that they're
|
||||
* read again from file, but doesn't remove the existing IDs from disk.
|
||||
* Only used for testing. Invalidates the cached client ID so that it is
|
||||
* read again from file, but doesn't remove the existing ID from disk.
|
||||
*/
|
||||
_reset() {
|
||||
return ClientIDImpl._reset();
|
||||
@ -176,37 +140,34 @@ var ClientID = Object.freeze({
|
||||
var ClientIDImpl = {
|
||||
_clientID: null,
|
||||
_clientIDHash: null,
|
||||
_ecosystemClientID: null,
|
||||
_loadClientIdsTask: null,
|
||||
_saveClientIdsTask: null,
|
||||
_removeClientIdsTask: null,
|
||||
_loadClientIdTask: null,
|
||||
_saveClientIdTask: null,
|
||||
_removeClientIdTask: null,
|
||||
_logger: null,
|
||||
_wasCanary: null,
|
||||
|
||||
_loadClientIDs() {
|
||||
if (this._loadClientIdsTask) {
|
||||
return this._loadClientIdsTask;
|
||||
_loadClientID() {
|
||||
if (this._loadClientIdTask) {
|
||||
return this._loadClientIdTask;
|
||||
}
|
||||
|
||||
this._loadClientIdsTask = this._doLoadClientIDs();
|
||||
let clear = () => (this._loadClientIdsTask = null);
|
||||
this._loadClientIdsTask.then(clear, clear);
|
||||
return this._loadClientIdsTask;
|
||||
this._loadClientIdTask = this._doLoadClientID();
|
||||
let clear = () => (this._loadClientIdTask = null);
|
||||
this._loadClientIdTask.then(clear, clear);
|
||||
return this._loadClientIdTask;
|
||||
},
|
||||
|
||||
/**
|
||||
* Load the client IDs (Telemetry Client ID and Ecosystem Client ID) from the
|
||||
* DataReporting Service state file. If either ID is missing, we generate a
|
||||
* new one.
|
||||
* Load the client ID from the DataReporting Service state file. If it is
|
||||
* missing, we generate a new one.
|
||||
*/
|
||||
async _doLoadClientIDs() {
|
||||
this._log.trace(`_doLoadClientIDs`);
|
||||
async _doLoadClientID() {
|
||||
this._log.trace(`_doLoadClientID`);
|
||||
// If there's a removal in progress, let's wait for it
|
||||
await this._removeClientIdsTask;
|
||||
await this._removeClientIdTask;
|
||||
|
||||
// Try to load the client id from the DRS state file.
|
||||
let hasCurrentClientID = false;
|
||||
let hasCurrentEcosystemClientID = false;
|
||||
try {
|
||||
let state = await CommonUtils.readJSON(gStateFilePath);
|
||||
if (AppConstants.platform == "android" && state && "wasCanary" in state) {
|
||||
@ -230,14 +191,10 @@ var ClientIDImpl = {
|
||||
// This data collection's not that important.
|
||||
}
|
||||
hasCurrentClientID = this.updateClientID(state.clientID);
|
||||
hasCurrentEcosystemClientID = this.updateEcosystemClientID(
|
||||
state.ecosystemClientID
|
||||
);
|
||||
if (hasCurrentClientID && hasCurrentEcosystemClientID) {
|
||||
this._log.trace(`_doLoadClientIDs: Client IDs loaded from state.`);
|
||||
if (hasCurrentClientID) {
|
||||
this._log.trace(`_doLoadClientID: Client IDs loaded from state.`);
|
||||
return {
|
||||
clientID: this._clientID,
|
||||
ecosystemClientID: this._ecosystemClientID,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -256,27 +213,23 @@ var ClientIDImpl = {
|
||||
}
|
||||
}
|
||||
|
||||
// We're missing one or both IDs from the DRS state file and prefs.
|
||||
// Generate new ones.
|
||||
// We're missing the ID from the DRS state file and prefs.
|
||||
// Generate a new one.
|
||||
if (!hasCurrentClientID) {
|
||||
Services.telemetry.scalarSet("telemetry.generated_new_client_id", true);
|
||||
this.updateClientID(CommonUtils.generateUUID());
|
||||
}
|
||||
if (!hasCurrentEcosystemClientID) {
|
||||
this.updateEcosystemClientID(CommonUtils.generateUUID());
|
||||
}
|
||||
this._saveClientIdsTask = this._saveClientIDs();
|
||||
this._saveClientIdTask = this._saveClientID();
|
||||
|
||||
// Wait on persisting the id. Otherwise failure to save the ID would result in
|
||||
// the client creating and subsequently sending multiple IDs to the server.
|
||||
// This would appear as multiple clients submitting similar data, which would
|
||||
// result in orphaning.
|
||||
await this._saveClientIdsTask;
|
||||
await this._saveClientIdTask;
|
||||
|
||||
this._log.trace("_doLoadClientIDs: New client IDs loaded and persisted.");
|
||||
this._log.trace("_doLoadClientID: New client ID loaded and persisted.");
|
||||
return {
|
||||
clientID: this._clientID,
|
||||
ecosystemClientID: this._ecosystemClientID,
|
||||
};
|
||||
},
|
||||
|
||||
@ -285,12 +238,11 @@ var ClientIDImpl = {
|
||||
*
|
||||
* @return {Promise} A promise resolved when the client ID is saved to disk.
|
||||
*/
|
||||
async _saveClientIDs() {
|
||||
async _saveClientID() {
|
||||
try {
|
||||
this._log.trace(`_saveClientIDs`);
|
||||
this._log.trace(`_saveClientID`);
|
||||
let obj = {
|
||||
clientID: this._clientID,
|
||||
ecosystemClientID: this._ecosystemClientID,
|
||||
};
|
||||
// We detected a canary client ID when resetting, storing this as a flag
|
||||
if (AppConstants.platform == "android" && this._wasCanary) {
|
||||
@ -304,7 +256,7 @@ var ClientIDImpl = {
|
||||
}
|
||||
}
|
||||
await CommonUtils.writeJSON(obj, gStateFilePath);
|
||||
this._saveClientIdsTask = null;
|
||||
this._saveClientIdTask = null;
|
||||
} catch (ex) {
|
||||
Services.telemetry.scalarAdd("telemetry.state_file_save_errors", 1);
|
||||
throw ex;
|
||||
@ -319,22 +271,13 @@ var ClientIDImpl = {
|
||||
*/
|
||||
async getClientID() {
|
||||
if (!this._clientID) {
|
||||
let { clientID } = await this._loadClientIDs();
|
||||
let { clientID } = await this._loadClientID();
|
||||
return clientID;
|
||||
}
|
||||
|
||||
return Promise.resolve(this._clientID);
|
||||
},
|
||||
|
||||
async getEcosystemClientID() {
|
||||
if (!this._ecosystemClientID) {
|
||||
let { ecosystemClientID } = await this._loadClientIDs();
|
||||
return ecosystemClientID;
|
||||
}
|
||||
|
||||
return Promise.resolve(this._ecosystemClientID);
|
||||
},
|
||||
|
||||
/**
|
||||
* This returns true if the client ID prior to the last client ID reset was a canary client ID.
|
||||
* Android only. Always returns null on Desktop.
|
||||
@ -387,10 +330,6 @@ var ClientIDImpl = {
|
||||
return id;
|
||||
},
|
||||
|
||||
getCachedEcosystemClientID() {
|
||||
return this._ecosystemClientID;
|
||||
},
|
||||
|
||||
async getClientIdHash() {
|
||||
if (!this._clientIDHash) {
|
||||
let byteArr = new TextEncoder().encode(await this.getClientID());
|
||||
@ -402,70 +341,53 @@ var ClientIDImpl = {
|
||||
},
|
||||
|
||||
/*
|
||||
* Resets the provider. This is for testing only.
|
||||
* Resets the module. This is for testing only.
|
||||
*/
|
||||
async _reset() {
|
||||
await this._loadClientIdsTask;
|
||||
await this._saveClientIdsTask;
|
||||
await this._loadClientIdTask;
|
||||
await this._saveClientIdTask;
|
||||
this._clientID = null;
|
||||
this._clientIDHash = null;
|
||||
this._ecosystemClientID = null;
|
||||
},
|
||||
|
||||
async setCanaryClientIDs() {
|
||||
this._log.trace("setCanaryClientIDs");
|
||||
async setCanaryClientID() {
|
||||
this._log.trace("setCanaryClientID");
|
||||
this.updateClientID(CANARY_CLIENT_ID);
|
||||
this.updateEcosystemClientID(CANARY_CLIENT_ID);
|
||||
|
||||
this._saveClientIdsTask = this._saveClientIDs();
|
||||
await this._saveClientIdsTask;
|
||||
this._saveClientIdTask = this._saveClientID();
|
||||
await this._saveClientIdTask;
|
||||
return this._clientID;
|
||||
},
|
||||
|
||||
async resetEcosystemClientID() {
|
||||
this._log.trace("resetEcosystemClientID");
|
||||
this.updateEcosystemClientID(CommonUtils.generateUUID());
|
||||
this._saveClientIdsTask = this._saveClientIDs();
|
||||
await this._saveClientIdsTask;
|
||||
return this._ecosystemClientID;
|
||||
},
|
||||
async _doRemoveClientID() {
|
||||
this._log.trace("_doRemoveClientID");
|
||||
|
||||
async _doRemoveClientIDs() {
|
||||
this._log.trace("_doRemoveClientIDs");
|
||||
|
||||
// Reset the cached main and ecosystem client IDs.
|
||||
// Reset the cached client ID.
|
||||
this._clientID = null;
|
||||
this._clientIDHash = null;
|
||||
this._ecosystemClientID = null;
|
||||
|
||||
// Clear the client id from the preference cache.
|
||||
Services.prefs.clearUserPref(PREF_CACHED_CLIENTID);
|
||||
|
||||
// Clear the old ecosystem client ID from the deletion request scalar store.
|
||||
Services.telemetry.scalarSet(
|
||||
SCALAR_DELETION_REQUEST_ECOSYSTEM_CLIENT_ID,
|
||||
""
|
||||
);
|
||||
|
||||
// If there is a save in progress, wait for it to complete.
|
||||
await this._saveClientIdsTask;
|
||||
await this._saveClientIdTask;
|
||||
|
||||
// Remove the client id from disk
|
||||
// Remove the client-id-containing state file from disk
|
||||
await IOUtils.remove(gStateFilePath);
|
||||
},
|
||||
|
||||
async removeClientIDs() {
|
||||
this._log.trace("removeClientIDs");
|
||||
async removeClientID() {
|
||||
this._log.trace("removeClientID");
|
||||
Services.telemetry.scalarAdd("telemetry.removed_client_ids", 1);
|
||||
let oldClientId = this._clientID;
|
||||
|
||||
// Wait for the removal.
|
||||
// Asynchronous calls to getClientID will also be blocked on this.
|
||||
this._removeClientIdsTask = this._doRemoveClientIDs();
|
||||
let clear = () => (this._removeClientIdsTask = null);
|
||||
this._removeClientIdsTask.then(clear, clear);
|
||||
this._removeClientIdTask = this._doRemoveClientID();
|
||||
let clear = () => (this._removeClientIdTask = null);
|
||||
this._removeClientIdTask.then(clear, clear);
|
||||
|
||||
await this._removeClientIdsTask;
|
||||
await this._removeClientIdTask;
|
||||
|
||||
// On Android we detect resets after a canary client ID.
|
||||
if (AppConstants.platform == "android") {
|
||||
@ -494,22 +416,6 @@ var ClientIDImpl = {
|
||||
return true;
|
||||
},
|
||||
|
||||
updateEcosystemClientID(id) {
|
||||
if (!isValidClientID(id)) {
|
||||
this._log.error(
|
||||
"updateEcosystemClientID - invalid ecosystem client ID",
|
||||
id
|
||||
);
|
||||
return false;
|
||||
}
|
||||
this._ecosystemClientID = id;
|
||||
Services.telemetry.scalarSet(
|
||||
SCALAR_DELETION_REQUEST_ECOSYSTEM_CLIENT_ID,
|
||||
id
|
||||
);
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* A helper for getting access to telemetry logger.
|
||||
*/
|
||||
|
@ -78,7 +78,6 @@ XPCOMUtils.defineLazyModuleGetters(this, {
|
||||
UpdatePing: "resource://gre/modules/UpdatePing.jsm",
|
||||
TelemetryHealthPing: "resource://gre/modules/HealthPing.jsm",
|
||||
TelemetryEventPing: "resource://gre/modules/EventPing.jsm",
|
||||
EcosystemTelemetry: "resource://gre/modules/EcosystemTelemetry.jsm",
|
||||
TelemetryPrioPing: "resource://gre/modules/PrioPing.jsm",
|
||||
UninstallPing: "resource://gre/modules/UninstallPing.jsm",
|
||||
OS: "resource://gre/modules/osfile.jsm",
|
||||
@ -850,13 +849,13 @@ var Impl = {
|
||||
this._log.trace(
|
||||
"Upload enabled, but got canary client ID. Resetting."
|
||||
);
|
||||
await ClientID.removeClientIDs();
|
||||
await ClientID.removeClientID();
|
||||
this._clientID = await ClientID.getClientID();
|
||||
} else if (!uploadEnabled && this._clientID != Utils.knownClientID) {
|
||||
this._log.trace(
|
||||
"Upload disabled, but got a valid client ID. Setting canary client ID."
|
||||
);
|
||||
await ClientID.setCanaryClientIDs();
|
||||
await ClientID.setCanaryClientID();
|
||||
this._clientID = await ClientID.getClientID();
|
||||
}
|
||||
|
||||
@ -904,7 +903,6 @@ var Impl = {
|
||||
}
|
||||
|
||||
TelemetryEventPing.startup();
|
||||
EcosystemTelemetry.startup();
|
||||
TelemetryPrioPing.startup();
|
||||
|
||||
if (uploadEnabled) {
|
||||
@ -962,8 +960,6 @@ var Impl = {
|
||||
|
||||
this._shutdownStep = "Event" + now();
|
||||
TelemetryEventPing.shutdown();
|
||||
this._shutdownStep = "Ecosystem" + now();
|
||||
EcosystemTelemetry.shutdown();
|
||||
this._shutdownStep = "Prio" + now();
|
||||
await TelemetryPrioPing.shutdown();
|
||||
|
||||
@ -1118,7 +1114,7 @@ var Impl = {
|
||||
|
||||
// Generate a new client ID and make sure this module uses the new version
|
||||
let p = (async () => {
|
||||
await ClientID.removeClientIDs();
|
||||
await ClientID.removeClientID();
|
||||
let id = await ClientID.getClientID();
|
||||
this._clientID = id;
|
||||
Telemetry.scalarSet("telemetry.data_upload_optin", true);
|
||||
@ -1164,7 +1160,7 @@ var Impl = {
|
||||
|
||||
// 6. Set ClientID to a known value
|
||||
let oldClientId = await ClientID.getClientID();
|
||||
await ClientID.setCanaryClientIDs();
|
||||
await ClientID.setCanaryClientID();
|
||||
this._clientID = await ClientID.getClientID();
|
||||
|
||||
// 7. Send the deletion-request ping.
|
||||
|
@ -21,9 +21,6 @@ const { clearTimeout, setTimeout } = ChromeUtils.import(
|
||||
"resource://gre/modules/Timer.jsm"
|
||||
);
|
||||
// Other pings
|
||||
const { EcosystemTelemetry } = ChromeUtils.import(
|
||||
"resource://gre/modules/EcosystemTelemetry.jsm"
|
||||
);
|
||||
const { TelemetryPrioPing } = ChromeUtils.import(
|
||||
"resource://gre/modules/PrioPing.jsm"
|
||||
);
|
||||
@ -374,7 +371,6 @@ var TelemetryScheduler = {
|
||||
this._log.trace("_schedulerTickLogic - Periodic ping due.");
|
||||
this._lastPeriodicPingTime = now;
|
||||
// Send other pings.
|
||||
EcosystemTelemetry.periodicPing();
|
||||
TelemetryPrioPing.periodicPing();
|
||||
}
|
||||
|
||||
|
@ -82,11 +82,6 @@ var TelemetryUtils = {
|
||||
EventPingMinimumFrequency: "toolkit.telemetry.eventping.minimumFrequency",
|
||||
EventPingMaximumFrequency: "toolkit.telemetry.eventping.maximumFrequency",
|
||||
|
||||
// Ecosystem Telemetry Preferences
|
||||
EcosystemTelemetryEnabled: "toolkit.telemetry.ecosystemtelemetry.enabled",
|
||||
EcosystemTelemetryAllowForNonProductionFxA:
|
||||
"toolkit.telemetry.ecosystemtelemetry.allowForNonProductionFxA",
|
||||
|
||||
// Prio Ping Preferences
|
||||
PrioPingEnabled: "toolkit.telemetry.prioping.enabled",
|
||||
PrioPingDataLimit: "toolkit.telemetry.prioping.dataLimit",
|
||||
|
@ -148,20 +148,6 @@ Preferences
|
||||
The maximum frequency at which an :doc:`../data/event-ping` will be sent.
|
||||
Default is 10 (minutes).
|
||||
|
||||
``toolkit.telemetry.ecosystemtelemetry.enabled``
|
||||
|
||||
Whether :doc:`../data/ecosystem-telemetry` is enabled.
|
||||
Default is false. Change requires restart.
|
||||
|
||||
``toolkit.telemetry.ecosystemtelemetry.allowForNonProductionFx``
|
||||
|
||||
Whether :doc:`../data/ecosystem-telemetry` will be submitted if Firefox is
|
||||
configured to use non-production FxA servers. Non-production servers includes
|
||||
servers run by Mozilla (eg, the "staging" or "dev" instances) and servers run
|
||||
externally (eg, self-hosted users). The expectation is that this will
|
||||
primarily be used for QA.
|
||||
Default is false. Change requires restart.
|
||||
|
||||
``toolkit.telemetry.overrideUpdateChannel``
|
||||
|
||||
Override the ``channel`` value that is reported via Telemetry.
|
||||
|
@ -106,7 +106,6 @@ EXTRA_JS_MODULES += [
|
||||
"app/TelemetryTimestamps.jsm",
|
||||
"app/TelemetryUtils.jsm",
|
||||
"pings/CoveragePing.jsm",
|
||||
"pings/EcosystemTelemetry.jsm",
|
||||
"pings/EventPing.jsm",
|
||||
"pings/HealthPing.jsm",
|
||||
"pings/ModulesPing.jsm",
|
||||
|
@ -1,406 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* 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/. */
|
||||
|
||||
/*
|
||||
* This module sends the Telemetry Ecosystem pings periodically:
|
||||
* https://firefox-source-docs.mozilla.org/toolkit/components/telemetry/data/ecosystem-telemetry.html
|
||||
*
|
||||
* Note that ecosystem pings are only sent when the preference
|
||||
* `toolkit.telemetry.ecosystemtelemetry.enabled` is set to `true` - eventually
|
||||
* that will be the default, but you should check!
|
||||
*
|
||||
* Note also that these pings are currently only sent for users signed in to
|
||||
* Firefox with a Firefox account.
|
||||
*
|
||||
* If you are using the non-production FxA stack, pings are not sent by default.
|
||||
* To force them, you should set:
|
||||
* - toolkit.telemetry.ecosystemtelemetry.allowForNonProductionFxA: true
|
||||
*
|
||||
* If you are trying to debug this, you might also find the following
|
||||
* preferences useful:
|
||||
* - toolkit.telemetry.log.level: "Trace"
|
||||
* - toolkit.telemetry.log.dump: true
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var EXPORTED_SYMBOLS = ["EcosystemTelemetry"];
|
||||
|
||||
const { XPCOMUtils } = ChromeUtils.import(
|
||||
"resource://gre/modules/XPCOMUtils.jsm"
|
||||
);
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetters(this, {
|
||||
ONLOGIN_NOTIFICATION: "resource://gre/modules/FxAccountsCommon.js",
|
||||
ONLOGOUT_NOTIFICATION: "resource://gre/modules/FxAccountsCommon.js",
|
||||
ONVERIFIED_NOTIFICATION: "resource://gre/modules/FxAccountsCommon.js",
|
||||
ON_PRELOGOUT_NOTIFICATION: "resource://gre/modules/FxAccountsCommon.js",
|
||||
TelemetryController: "resource://gre/modules/TelemetryController.jsm",
|
||||
TelemetryUtils: "resource://gre/modules/TelemetryUtils.jsm",
|
||||
TelemetryEnvironment: "resource://gre/modules/TelemetryEnvironment.jsm",
|
||||
Log: "resource://gre/modules/Log.jsm",
|
||||
Services: "resource://gre/modules/Services.jsm",
|
||||
fxAccounts: "resource://gre/modules/FxAccounts.jsm",
|
||||
FxAccounts: "resource://gre/modules/FxAccounts.jsm",
|
||||
ClientID: "resource://gre/modules/ClientID.jsm",
|
||||
});
|
||||
|
||||
XPCOMUtils.defineLazyServiceGetters(this, {
|
||||
Telemetry: ["@mozilla.org/base/telemetry;1", "nsITelemetry"],
|
||||
});
|
||||
|
||||
const LOGGER_NAME = "Toolkit.Telemetry";
|
||||
const LOGGER_PREFIX = "EcosystemTelemetry::";
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "log", () => {
|
||||
return Log.repository.getLoggerWithMessagePrefix(LOGGER_NAME, LOGGER_PREFIX);
|
||||
});
|
||||
|
||||
var Policy = {
|
||||
sendPing: (type, payload, options) =>
|
||||
TelemetryController.submitExternalPing(type, payload, options),
|
||||
monotonicNow: () => TelemetryUtils.monotonicNow(),
|
||||
// Returns a promise that resolves with the Ecosystem anonymized id.
|
||||
// Never rejects - will log an error and resolve with null on error.
|
||||
async getEcosystemAnonId() {
|
||||
try {
|
||||
let userData = await fxAccounts.getSignedInUser();
|
||||
if (!userData || !userData.verified) {
|
||||
log.debug("No ecosystem anonymized ID - no user or unverified user");
|
||||
return null;
|
||||
}
|
||||
return await fxAccounts.telemetry.ensureEcosystemAnonId();
|
||||
} catch (ex) {
|
||||
log.error("Failed to fetch the ecosystem anonymized ID", ex);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
// Returns a promise that resolves with the current ecosystem client id.
|
||||
getEcosystemClientId() {
|
||||
return ClientID.getEcosystemClientID();
|
||||
},
|
||||
// Returns a promise that resolves when the ecosystem client id has been reset.
|
||||
resetEcosystemClientId() {
|
||||
return ClientID.resetEcosystemClientID();
|
||||
},
|
||||
};
|
||||
|
||||
var EcosystemTelemetry = {
|
||||
Reason: Object.freeze({
|
||||
PERIODIC: "periodic", // Send the ping in regular intervals
|
||||
SHUTDOWN: "shutdown", // Send the ping on shutdown
|
||||
LOGOUT: "logout", // Send after FxA logout
|
||||
}),
|
||||
PING_TYPE: "account-ecosystem",
|
||||
METRICS_STORE: "account-ecosystem",
|
||||
_lastSendTime: 0,
|
||||
// Indicates that the Ecosystem ping is configured and ready to send pings.
|
||||
_initialized: false,
|
||||
// The promise returned by Policy.getEcosystemAnonId()
|
||||
_promiseEcosystemAnonId: null,
|
||||
// Sets up _promiseEcosystemAnonId in the hope that it will be resolved by the
|
||||
// time we need it, and also already resolved when the user logs out.
|
||||
prepareEcosystemAnonId() {
|
||||
this._promiseEcosystemAnonId = Policy.getEcosystemAnonId();
|
||||
},
|
||||
|
||||
enabled() {
|
||||
// Never enabled when not Unified Telemetry (e.g. not enabled on Fennec)
|
||||
// If not enabled, then it doesn't become enabled until the preferences
|
||||
// are adjusted and the browser is restarted.
|
||||
// Not enabled is different to "should I send pings?" - if enabled, then
|
||||
// observers will still be setup so we are ready to transition from not
|
||||
// sending pings into sending them.
|
||||
if (
|
||||
!Services.prefs.getBoolPref(TelemetryUtils.Preferences.Unified, false)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
!Services.prefs.getBoolPref(
|
||||
TelemetryUtils.Preferences.EcosystemTelemetryEnabled,
|
||||
false
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
!FxAccounts.config.isProductionConfig() &&
|
||||
!Services.prefs.getBoolPref(
|
||||
TelemetryUtils.Preferences.EcosystemTelemetryAllowForNonProductionFxA,
|
||||
false
|
||||
)
|
||||
) {
|
||||
log.info("Ecosystem telemetry disabled due to FxA non-production user");
|
||||
return false;
|
||||
}
|
||||
// We are enabled (although may or may not want to send pings.)
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* In what is an unfortunate level of coupling, FxA has hacks to call this
|
||||
* function before it sends any account related notifications. This allows us
|
||||
* to work correctly when logging out by ensuring we have the anonymized
|
||||
* ecosystem ID by then (as *at* logout time it's too late)
|
||||
*/
|
||||
async prepareForFxANotification() {
|
||||
// Telemetry might not have initialized yet, so make sure we have.
|
||||
this.startup();
|
||||
// We need to ensure the promise fetching the anon ecosystem id has
|
||||
// resolved (but if we are pref'd off it will remain null.)
|
||||
if (this._promiseEcosystemAnonId) {
|
||||
await this._promiseEcosystemAnonId;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* On startup, register all observers.
|
||||
*/
|
||||
startup() {
|
||||
if (!this.enabled() || this._initialized) {
|
||||
return;
|
||||
}
|
||||
log.trace("Starting up.");
|
||||
|
||||
// We "prime" the ecosystem id here - if it's not currently available, it
|
||||
// will be done in the background, so should be ready by the time we
|
||||
// actually need it.
|
||||
this.prepareEcosystemAnonId();
|
||||
|
||||
this._addObservers();
|
||||
|
||||
this._initialized = true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Shutdown this ping.
|
||||
*
|
||||
* This will send a final ping with the SHUTDOWN reason.
|
||||
*/
|
||||
shutdown() {
|
||||
if (!this._initialized) {
|
||||
return;
|
||||
}
|
||||
log.trace("Shutting down.");
|
||||
this._submitPing(this.Reason.SHUTDOWN);
|
||||
|
||||
this._removeObservers();
|
||||
this._initialized = false;
|
||||
},
|
||||
|
||||
_addObservers() {
|
||||
// FxA login, verification and logout.
|
||||
Services.obs.addObserver(this, ONLOGIN_NOTIFICATION);
|
||||
Services.obs.addObserver(this, ONVERIFIED_NOTIFICATION);
|
||||
Services.obs.addObserver(this, ONLOGOUT_NOTIFICATION);
|
||||
Services.obs.addObserver(this, ON_PRELOGOUT_NOTIFICATION);
|
||||
},
|
||||
|
||||
_removeObservers() {
|
||||
try {
|
||||
// removeObserver may throw, which could interrupt shutdown.
|
||||
Services.obs.removeObserver(this, ONLOGIN_NOTIFICATION);
|
||||
Services.obs.removeObserver(this, ONVERIFIED_NOTIFICATION);
|
||||
Services.obs.removeObserver(this, ONLOGOUT_NOTIFICATION);
|
||||
Services.obs.removeObserver(this, ON_PRELOGOUT_NOTIFICATION);
|
||||
} catch (ex) {}
|
||||
},
|
||||
|
||||
observe(subject, topic, data) {
|
||||
log.trace(`observe, topic: ${topic}`);
|
||||
|
||||
switch (topic) {
|
||||
// This is a bit messy - an already verified user will get
|
||||
// ONLOGIN_NOTIFICATION but *not* ONVERIFIED_NOTIFICATION. However, an
|
||||
// unverified user can't do the ecosystem dance with the profile server.
|
||||
// The only way to determine if the user is verified or not is via an
|
||||
// async method, and this isn't async, so...
|
||||
// Sadly, we just end up kicking off prepareEcosystemAnonId() twice in
|
||||
// that scenario, which will typically be rare and is handled by FxA. Note
|
||||
// also that we are just "priming" the ecosystem id here - if it's not
|
||||
// currently available, it will be done in the background, so should be
|
||||
// ready by the time we actually need it.
|
||||
case ONLOGIN_NOTIFICATION:
|
||||
case ONVERIFIED_NOTIFICATION:
|
||||
// If we sent these pings for non-account users and this is a login
|
||||
// notification, we'd want to submit now, so we have a fresh set of data
|
||||
// for the user.
|
||||
// But for now, all we need to do is start the promise to fetch the anon
|
||||
// ID.
|
||||
this.prepareEcosystemAnonId();
|
||||
break;
|
||||
|
||||
case ONLOGOUT_NOTIFICATION:
|
||||
// On logout we submit what we have, then switch to the "no anon id"
|
||||
// state.
|
||||
// Returns the promise for tests.
|
||||
return this._submitPing(this.Reason.LOGOUT)
|
||||
.then(async () => {
|
||||
// Ensure _promiseEcosystemAnonId() is now going to resolve as null.
|
||||
this.prepareEcosystemAnonId();
|
||||
// Change the ecosystemClientId value on logout, so that if a different user signs in
|
||||
// we cannot link the two anon_id values together via a shared client_id.
|
||||
// (We are still confirming approval to perform such linking between accounts, and
|
||||
// this code can be removed once confirmed).
|
||||
await Policy.resetEcosystemClientId();
|
||||
})
|
||||
.catch(e => {
|
||||
log.error("ONLOGOUT promise chain failed", e);
|
||||
});
|
||||
|
||||
case ON_PRELOGOUT_NOTIFICATION:
|
||||
// We don't need to do anything here - everything was done in startup.
|
||||
// However, we keep this here so someone doesn't erroneously think the
|
||||
// notification serves no purposes - it's the `observerPreloads` in
|
||||
// FxAccounts that matters!
|
||||
break;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
// Called by TelemetryScheduler.jsm when periodic pings should be sent.
|
||||
periodicPing() {
|
||||
log.trace("periodic ping triggered");
|
||||
return this._submitPing(this.Reason.PERIODIC);
|
||||
},
|
||||
|
||||
/**
|
||||
* Submit an ecosystem ping.
|
||||
*
|
||||
* It will not send a ping if Ecosystem Telemetry is disabled
|
||||
* the module is not fully initialized or if the ping type is missing.
|
||||
*
|
||||
* It will automatically assemble the right payload and clear out Telemetry stores.
|
||||
*
|
||||
* @param {String} reason The reason we're sending the ping. One of TelemetryEcosystemPing.Reason.
|
||||
*/
|
||||
async _submitPing(reason) {
|
||||
if (!this.enabled()) {
|
||||
// It's possible we will end up here if FxA was using the production
|
||||
// stack at startup but no longer is.
|
||||
log.trace(`_submitPing was called, but ping is not enabled.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._initialized) {
|
||||
log.trace(`Not initialized when sending. Bug?`);
|
||||
return;
|
||||
}
|
||||
|
||||
log.trace(`_submitPing, reason: ${reason}`);
|
||||
|
||||
let now = Policy.monotonicNow();
|
||||
|
||||
// Duration in seconds
|
||||
let duration = Math.round((now - this._lastSendTime) / 1000);
|
||||
this._lastSendTime = now;
|
||||
|
||||
let payload = await this._payload(reason, duration);
|
||||
if (!payload) {
|
||||
// The reason for returning null will already have been logged.
|
||||
return;
|
||||
}
|
||||
|
||||
// Never include the client ID.
|
||||
// We provide our own environment.
|
||||
const options = {
|
||||
addClientId: false,
|
||||
addEnvironment: true,
|
||||
overrideEnvironment: this._environment(),
|
||||
usePingSender: reason === this.Reason.SHUTDOWN,
|
||||
};
|
||||
|
||||
let id = await Policy.sendPing(this.PING_TYPE, payload, options);
|
||||
log.info(`submitted ping ${id}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* Assemble payload for a new ping
|
||||
*
|
||||
* @param {String} reason The reason we're sending the ping. One of TelemetryEcosystemPing.Reason.
|
||||
* @param {Number} duration The duration since ping was last send in seconds.
|
||||
*/
|
||||
async _payload(reason, duration) {
|
||||
let ecosystemAnonId = await this._promiseEcosystemAnonId;
|
||||
if (!ecosystemAnonId) {
|
||||
// This typically just means no user is logged in, so don't make too
|
||||
// much noise.
|
||||
log.info("Unable to determine the ecosystem anon id; skipping this ping");
|
||||
return null;
|
||||
}
|
||||
|
||||
let payload = {
|
||||
reason,
|
||||
ecosystemAnonId,
|
||||
ecosystemClientId: await Policy.getEcosystemClientId(),
|
||||
duration,
|
||||
|
||||
scalars: Telemetry.getSnapshotForScalars(
|
||||
this.METRICS_STORE,
|
||||
/* clear */ true,
|
||||
/* filter test */ true
|
||||
),
|
||||
keyedScalars: Telemetry.getSnapshotForKeyedScalars(
|
||||
this.METRICS_STORE,
|
||||
/* clear */ true,
|
||||
/* filter test */ true
|
||||
),
|
||||
histograms: Telemetry.getSnapshotForHistograms(
|
||||
this.METRICS_STORE,
|
||||
/* clear */ true,
|
||||
/* filter test */ true
|
||||
),
|
||||
keyedHistograms: Telemetry.getSnapshotForKeyedHistograms(
|
||||
this.METRICS_STORE,
|
||||
/* clear */ true,
|
||||
/* filter test */ true
|
||||
),
|
||||
};
|
||||
|
||||
return payload;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the minimal environment to include in the ping
|
||||
*/
|
||||
_environment() {
|
||||
let currentEnv = TelemetryEnvironment.currentEnvironment;
|
||||
let environment = {
|
||||
settings: {
|
||||
locale: currentEnv.settings.locale,
|
||||
},
|
||||
system: {
|
||||
memoryMB: currentEnv.system.memoryMB,
|
||||
os: {
|
||||
name: currentEnv.system.os.name,
|
||||
version: currentEnv.system.os.version,
|
||||
locale: currentEnv.system.os.locale,
|
||||
},
|
||||
cpu: {
|
||||
speedMHz: currentEnv.system.cpu.speedMHz,
|
||||
},
|
||||
},
|
||||
profile: {}, // added conditionally
|
||||
};
|
||||
|
||||
if (currentEnv.profile.creationDate) {
|
||||
environment.profile.creationDate = currentEnv.profile.creationDate;
|
||||
}
|
||||
|
||||
if (currentEnv.profile.firstUseDate) {
|
||||
environment.profile.firstUseDate = currentEnv.profile.firstUseDate;
|
||||
}
|
||||
|
||||
return environment;
|
||||
},
|
||||
|
||||
testReset() {
|
||||
this._initialized = false;
|
||||
this._lastSendTime = 0;
|
||||
this.startup();
|
||||
},
|
||||
};
|
@ -527,13 +527,6 @@ if (runningInParent) {
|
||||
// Speed up child process accumulations
|
||||
Services.prefs.setIntPref(TelemetryUtils.Preferences.IPCBatchTimeout, 10);
|
||||
|
||||
// Make sure ecosystem telemetry is disabled, no matter which build
|
||||
// Individual tests will enable it when appropriate
|
||||
Services.prefs.setBoolPref(
|
||||
TelemetryUtils.Preferences.EcosystemTelemetryEnabled,
|
||||
false
|
||||
);
|
||||
|
||||
// Non-unified Telemetry (e.g. Fennec on Android) needs the preference to be set
|
||||
// in order to enable Telemetry.
|
||||
if (Services.prefs.getBoolPref(TelemetryUtils.Preferences.Unified, false)) {
|
||||
|
@ -1,430 +0,0 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
ChromeUtils.import("resource://gre/modules/TelemetryController.jsm", this);
|
||||
ChromeUtils.import("resource://gre/modules/TelemetryUtils.jsm", this);
|
||||
ChromeUtils.import("resource://gre/modules/Preferences.jsm", this);
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetters(this, {
|
||||
ONLOGIN_NOTIFICATION: "resource://gre/modules/FxAccountsCommon.js",
|
||||
ONLOGOUT_NOTIFICATION: "resource://gre/modules/FxAccountsCommon.js",
|
||||
ONVERIFIED_NOTIFICATION: "resource://gre/modules/FxAccountsCommon.js",
|
||||
});
|
||||
ChromeUtils.defineModuleGetter(
|
||||
this,
|
||||
"EcosystemTelemetry",
|
||||
"resource://gre/modules/EcosystemTelemetry.jsm"
|
||||
);
|
||||
|
||||
const TEST_PING_TYPE = "test-ping-type";
|
||||
|
||||
const RE_VALID_GUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
||||
|
||||
function fakeIdleNotification(topic) {
|
||||
let scheduler = ChromeUtils.import(
|
||||
"resource://gre/modules/TelemetryScheduler.jsm",
|
||||
null
|
||||
);
|
||||
return scheduler.TelemetryScheduler.observe(null, topic, null);
|
||||
}
|
||||
|
||||
async function promiseNoPing() {
|
||||
// We check there's not one of our pings pending by sending a test ping, then
|
||||
// immediately fetching a pending ping and checking it's that test one.
|
||||
TelemetryController.submitExternalPing(TEST_PING_TYPE, {}, {});
|
||||
let ping = await PingServer.promiseNextPing();
|
||||
Assert.equal(ping.type, TEST_PING_TYPE, "Should be a test ping.");
|
||||
}
|
||||
|
||||
function checkPingStructure(ping, reason) {
|
||||
Assert.equal(
|
||||
ping.type,
|
||||
EcosystemTelemetry.PING_TYPE,
|
||||
"Should be an ecosystem ping."
|
||||
);
|
||||
|
||||
Assert.ok(!("clientId" in ping), "Ping must not contain a client ID.");
|
||||
Assert.ok("environment" in ping, "Ping must contain an environment.");
|
||||
let environment = ping.environment;
|
||||
|
||||
// Check that the environment is indeed minimal
|
||||
const ALLOWED_ENVIRONMENT_KEYS = ["settings", "system", "profile"];
|
||||
Assert.deepEqual(
|
||||
ALLOWED_ENVIRONMENT_KEYS,
|
||||
Object.keys(environment),
|
||||
"Environment should only contain a limited set of keys."
|
||||
);
|
||||
|
||||
// Check that fields of the environment are indeed minimal
|
||||
Assert.deepEqual(
|
||||
["locale"],
|
||||
Object.keys(environment.settings),
|
||||
"Settings environment should only contain locale"
|
||||
);
|
||||
Assert.deepEqual(
|
||||
["cpu", "memoryMB", "os"],
|
||||
Object.keys(environment.system).sort(),
|
||||
"System environment should contain a limited set of keys"
|
||||
);
|
||||
Assert.deepEqual(
|
||||
["locale", "name", "version"],
|
||||
Object.keys(environment.system.os).sort(),
|
||||
"system.environment.os should contain a limited set of keys"
|
||||
);
|
||||
|
||||
// Check the payload for required fields.
|
||||
let payload = ping.payload;
|
||||
Assert.equal(payload.reason, reason, "Ping reason must match.");
|
||||
Assert.ok(
|
||||
payload.duration >= 0,
|
||||
"Payload must have a duration greater or equal to 0"
|
||||
);
|
||||
Assert.ok("ecosystemAnonId" in payload, "payload must have ecosystemAnonId");
|
||||
Assert.ok(
|
||||
RE_VALID_GUID.test(payload.ecosystemClientId),
|
||||
"ecosystemClientId must be a valid GUID"
|
||||
);
|
||||
|
||||
Assert.ok("scalars" in payload, "Payload must contain scalars");
|
||||
Assert.ok("keyedScalars" in payload, "Payload must contain keyed scalars");
|
||||
Assert.ok("histograms" in payload, "Payload must contain histograms");
|
||||
Assert.ok(
|
||||
"keyedHistograms" in payload,
|
||||
"Payload must contain keyed histograms"
|
||||
);
|
||||
}
|
||||
|
||||
function fakeAnonId(fn) {
|
||||
const m = ChromeUtils.import(
|
||||
"resource://gre/modules/EcosystemTelemetry.jsm",
|
||||
null
|
||||
);
|
||||
let oldFn = m.Policy.getEcosystemAnonId;
|
||||
m.Policy.getEcosystemAnonId = fn;
|
||||
return oldFn;
|
||||
}
|
||||
|
||||
registerCleanupFunction(function() {
|
||||
PingServer.stop();
|
||||
});
|
||||
|
||||
add_task(async function setup() {
|
||||
// Trigger a proper telemetry init.
|
||||
do_get_profile(true);
|
||||
// Make sure we don't generate unexpected pings due to pref changes.
|
||||
await setEmptyPrefWatchlist();
|
||||
|
||||
// Start the local ping server and setup Telemetry to use it during the tests.
|
||||
PingServer.start();
|
||||
Preferences.set(
|
||||
TelemetryUtils.Preferences.Server,
|
||||
"http://localhost:" + PingServer.port
|
||||
);
|
||||
TelemetrySend.setServer("http://localhost:" + PingServer.port);
|
||||
|
||||
await TelemetryController.testSetup();
|
||||
});
|
||||
|
||||
// We make absolute sure the Ecosystem ping is never triggered on Fennec/Non-unified Telemetry
|
||||
add_task(
|
||||
{
|
||||
skip_if: () => !gIsAndroid,
|
||||
},
|
||||
async function test_no_ecosystem_ping_on_fennec() {
|
||||
// Force preference to true, we should have an additional check on Android/Unified Telemetry
|
||||
Preferences.set(TelemetryUtils.Preferences.EcosystemTelemetryEnabled, true);
|
||||
EcosystemTelemetry.testReset();
|
||||
|
||||
// This is invoked in regular intervals by the timer.
|
||||
// Would trigger ping sending.
|
||||
EcosystemTelemetry.periodicPing();
|
||||
await promiseNoPing();
|
||||
}
|
||||
);
|
||||
|
||||
add_task(
|
||||
{
|
||||
skip_if: () => gIsAndroid,
|
||||
},
|
||||
async function test_disabled_non_fxa_production() {
|
||||
Preferences.set(TelemetryUtils.Preferences.EcosystemTelemetryEnabled, true);
|
||||
Assert.ok(EcosystemTelemetry.enabled(), "enabled by default");
|
||||
Preferences.set("identity.fxaccounts.autoconfig.uri", "http://");
|
||||
Assert.ok(!EcosystemTelemetry.enabled(), "disabled if non-prod");
|
||||
Preferences.set(
|
||||
TelemetryUtils.Preferences.EcosystemTelemetryAllowForNonProductionFxA,
|
||||
true
|
||||
);
|
||||
Assert.ok(
|
||||
EcosystemTelemetry.enabled(),
|
||||
"enabled for non-prod but preference override"
|
||||
);
|
||||
Preferences.reset("identity.fxaccounts.autoconfig.uri");
|
||||
Preferences.reset(
|
||||
TelemetryUtils.Preferences.EcosystemTelemetryAllowForNonProductionFxA
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
add_task(
|
||||
{
|
||||
skip_if: () => gIsAndroid,
|
||||
},
|
||||
async function test_nosending_if_disabled() {
|
||||
Preferences.set(
|
||||
TelemetryUtils.Preferences.EcosystemTelemetryEnabled,
|
||||
false
|
||||
);
|
||||
EcosystemTelemetry.testReset();
|
||||
|
||||
// This is invoked in regular intervals by the timer.
|
||||
// Would trigger ping sending.
|
||||
EcosystemTelemetry.periodicPing();
|
||||
await promiseNoPing();
|
||||
}
|
||||
);
|
||||
|
||||
add_task(
|
||||
{
|
||||
skip_if: () => gIsAndroid,
|
||||
},
|
||||
async function test_no_default_send() {
|
||||
// No user's logged in, nothing is mocked, so nothing is sent.
|
||||
Preferences.set(TelemetryUtils.Preferences.EcosystemTelemetryEnabled, true);
|
||||
EcosystemTelemetry.testReset();
|
||||
|
||||
// This is invoked in regular intervals by the timer.
|
||||
EcosystemTelemetry.periodicPing();
|
||||
|
||||
await promiseNoPing();
|
||||
}
|
||||
);
|
||||
|
||||
add_task(
|
||||
{
|
||||
skip_if: () => gIsAndroid,
|
||||
},
|
||||
async function test_login_workflow() {
|
||||
// Fake the whole login/logout workflow by triggering the events directly.
|
||||
|
||||
Preferences.set(TelemetryUtils.Preferences.EcosystemTelemetryEnabled, true);
|
||||
EcosystemTelemetry.testReset();
|
||||
|
||||
let originalAnonId = fakeAnonId(() => null);
|
||||
let ping;
|
||||
|
||||
// 1. No user, timer invoked
|
||||
EcosystemTelemetry.periodicPing();
|
||||
await promiseNoPing();
|
||||
|
||||
// 2. User logs in, but we fail to obtain a valid uid.
|
||||
// No ping will be generated.
|
||||
fakeAnonId(() => null);
|
||||
EcosystemTelemetry.observe(null, ONLOGIN_NOTIFICATION, null);
|
||||
|
||||
EcosystemTelemetry.periodicPing();
|
||||
await promiseNoPing();
|
||||
|
||||
// Once we've failed to get the ID, we don't try again until next startup
|
||||
// or another login-related event - so...
|
||||
// 3. uid becomes available after verification.
|
||||
fakeAnonId(() => "test_login_workflow:my.anon.id");
|
||||
EcosystemTelemetry.observe(null, ONVERIFIED_NOTIFICATION, null);
|
||||
print("triggering ping now that we have an anon-id");
|
||||
EcosystemTelemetry.periodicPing();
|
||||
ping = await PingServer.promiseNextPing();
|
||||
checkPingStructure(ping, "periodic");
|
||||
Assert.equal(
|
||||
ping.payload.ecosystemAnonId,
|
||||
"test_login_workflow:my.anon.id"
|
||||
);
|
||||
const origClientId = ping.payload.ecosystemClientId;
|
||||
|
||||
// 4. User disconnects account, should get an immediate ping.
|
||||
print("user disconnects");
|
||||
// We need to arrange for the new empty anonid before the notification.
|
||||
fakeAnonId(() => null);
|
||||
await EcosystemTelemetry.observe(null, ONLOGOUT_NOTIFICATION, null);
|
||||
ping = await PingServer.promiseNextPing();
|
||||
checkPingStructure(ping, "logout");
|
||||
Assert.equal(
|
||||
ping.payload.ecosystemAnonId,
|
||||
"test_login_workflow:my.anon.id",
|
||||
"should have been submitted with the old anonid"
|
||||
);
|
||||
Assert.equal(
|
||||
ping.payload.ecosystemClientId,
|
||||
origClientId,
|
||||
"should have been submitted with the old clientid"
|
||||
);
|
||||
Assert.equal(
|
||||
await EcosystemTelemetry.promiseEcosystemAnonId,
|
||||
null,
|
||||
"should resolve to null immediately after logout"
|
||||
);
|
||||
|
||||
// 5. No user, timer invoked
|
||||
print("timer fires after disconnection");
|
||||
EcosystemTelemetry.periodicPing();
|
||||
await promiseNoPing();
|
||||
|
||||
// 6. Transition back to logged in, pings should again be sent.
|
||||
fakeAnonId(() => "test_login_workflow:my.anon.id.2");
|
||||
EcosystemTelemetry.observe(null, ONVERIFIED_NOTIFICATION, null);
|
||||
print("triggering ping now the user has logged back in");
|
||||
EcosystemTelemetry.periodicPing();
|
||||
ping = await PingServer.promiseNextPing();
|
||||
checkPingStructure(ping, "periodic");
|
||||
Assert.equal(
|
||||
ping.payload.ecosystemAnonId,
|
||||
"test_login_workflow:my.anon.id.2"
|
||||
);
|
||||
Assert.notEqual(
|
||||
ping.payload.ecosystemClientId,
|
||||
origClientId,
|
||||
"should have a different clientid after signing out then back in"
|
||||
);
|
||||
|
||||
// Reset policy.
|
||||
fakeAnonId(originalAnonId);
|
||||
}
|
||||
);
|
||||
|
||||
add_task(
|
||||
{
|
||||
skip_if: () => gIsAndroid,
|
||||
},
|
||||
async function test_shutdown_logged_in() {
|
||||
// Check shutdown when a user's logged in does the right thing.
|
||||
Preferences.set(TelemetryUtils.Preferences.EcosystemTelemetryEnabled, true);
|
||||
EcosystemTelemetry.testReset();
|
||||
|
||||
let originalAnonId = fakeAnonId(() =>
|
||||
Promise.resolve("test_shutdown_logged_in:my.anon.id")
|
||||
);
|
||||
|
||||
EcosystemTelemetry.observe(null, ONLOGIN_NOTIFICATION, null);
|
||||
|
||||
// No ping expected yet.
|
||||
await promiseNoPing();
|
||||
|
||||
// Shutdown
|
||||
EcosystemTelemetry.shutdown();
|
||||
let ping = await PingServer.promiseNextPing();
|
||||
checkPingStructure(ping, "shutdown");
|
||||
Assert.equal(
|
||||
ping.payload.ecosystemAnonId,
|
||||
"test_shutdown_logged_in:my.anon.id",
|
||||
"our anon ID is in the ping"
|
||||
);
|
||||
fakeAnonId(originalAnonId);
|
||||
}
|
||||
);
|
||||
|
||||
add_task(
|
||||
{
|
||||
skip_if: () => gIsAndroid,
|
||||
},
|
||||
async function test_shutdown_not_logged_in() {
|
||||
// Check shutdown when no user is logged in does the right thing.
|
||||
Preferences.set(TelemetryUtils.Preferences.EcosystemTelemetryEnabled, true);
|
||||
EcosystemTelemetry.testReset();
|
||||
|
||||
let originalAnonId = fakeAnonId(() => Promise.resolve(null));
|
||||
|
||||
// No ping expected yet.
|
||||
await promiseNoPing();
|
||||
|
||||
// Shutdown
|
||||
EcosystemTelemetry.shutdown();
|
||||
|
||||
// Still no ping.
|
||||
await promiseNoPing();
|
||||
fakeAnonId(originalAnonId);
|
||||
}
|
||||
);
|
||||
|
||||
// Test that a periodic ping is triggered by the scheduler at midnight
|
||||
//
|
||||
// Based on `test_TelemetrySession#test_DailyDueAndIdle`.
|
||||
add_task(
|
||||
{
|
||||
skip_if: () => gIsAndroid,
|
||||
},
|
||||
async function test_periodic_ping() {
|
||||
await TelemetryStorage.testClearPendingPings();
|
||||
PingServer.clearRequests();
|
||||
|
||||
let receivedPing = null;
|
||||
// Register a ping handler that will assert when receiving multiple ecosystem pings.
|
||||
// We can ignore other pings, such as the periodic ping.
|
||||
PingServer.registerPingHandler(req => {
|
||||
const ping = decodeRequestPayload(req);
|
||||
if (ping.type == EcosystemTelemetry.PING_TYPE) {
|
||||
Assert.ok(
|
||||
!receivedPing,
|
||||
"Telemetry must only send one periodic ecosystem ping."
|
||||
);
|
||||
receivedPing = ping;
|
||||
}
|
||||
});
|
||||
|
||||
// Faking scheduler timer has to happen before resetting TelemetryController
|
||||
// to be effective.
|
||||
let schedulerTickCallback = null;
|
||||
let now = new Date(2040, 1, 1, 0, 0, 0);
|
||||
fakeNow(now);
|
||||
// Fake scheduler functions to control periodic collection flow in tests.
|
||||
fakeSchedulerTimer(
|
||||
callback => (schedulerTickCallback = callback),
|
||||
() => {}
|
||||
);
|
||||
await TelemetryController.testReset();
|
||||
|
||||
Preferences.set(TelemetryUtils.Preferences.EcosystemTelemetryEnabled, true);
|
||||
EcosystemTelemetry.testReset();
|
||||
|
||||
// Have to arrange for an anon-id to be configured.
|
||||
let originalAnonId = fakeAnonId(() => "test_periodic_ping:my.anon.id");
|
||||
EcosystemTelemetry.observe(null, ONLOGIN_NOTIFICATION, null);
|
||||
|
||||
// As a sanity check we trigger a keyedHistogram and scalar declared as
|
||||
// being in our ping, just to help ensure that the payload was assembled
|
||||
// in the correct shape.
|
||||
let h = Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS");
|
||||
h.add("test-key");
|
||||
Telemetry.scalarSet("browser.engagement.total_uri_count", 2);
|
||||
|
||||
// Trigger the periodic ecosystem ping.
|
||||
let firstPeriodicDue = new Date(2040, 1, 2, 0, 0, 0);
|
||||
fakeNow(firstPeriodicDue);
|
||||
|
||||
// Run a scheduler tick: it should trigger the periodic ping.
|
||||
Assert.ok(!!schedulerTickCallback);
|
||||
let tickPromise = schedulerTickCallback();
|
||||
|
||||
// Send an idle and then an active user notification.
|
||||
fakeIdleNotification("idle");
|
||||
fakeIdleNotification("active");
|
||||
|
||||
// Wait on the tick promise.
|
||||
await tickPromise;
|
||||
|
||||
await TelemetrySend.testWaitOnOutgoingPings();
|
||||
|
||||
// Decode the ping contained in the request and check that's a periodic ping.
|
||||
Assert.ok(receivedPing, "Telemetry must send one ecosystem periodic ping.");
|
||||
checkPingStructure(receivedPing, "periodic");
|
||||
// And check the content we expect is there.
|
||||
Assert.ok(receivedPing.payload.keyedHistograms.parent.SEARCH_COUNTS);
|
||||
Assert.equal(
|
||||
receivedPing.payload.scalars.parent["browser.engagement.total_uri_count"],
|
||||
2
|
||||
);
|
||||
|
||||
fakeAnonId(originalAnonId);
|
||||
}
|
||||
);
|
@ -265,8 +265,6 @@ add_task(async function test_disableDataUpload() {
|
||||
secondClientId,
|
||||
"The client id must have changed"
|
||||
);
|
||||
let secondEcosystemClientId = await ClientID.getEcosystemClientID();
|
||||
|
||||
// Simulate a failure in sending the deletion-request ping by disabling the HTTP server.
|
||||
await PingServer.stop();
|
||||
|
||||
@ -338,15 +336,6 @@ add_task(async function test_disableDataUpload() {
|
||||
ping.clientId,
|
||||
"Deletion must be requested for correct client id"
|
||||
);
|
||||
if (AppConstants.MOZ_APP_NAME != "thunderbird") {
|
||||
// We don't record the old ecosystem client ID on Thunderbird,
|
||||
// since the FxA and telemetry infrastructure is different there.
|
||||
Assert.equal(
|
||||
secondEcosystemClientId,
|
||||
ping.payload.scalars.parent["deletion.request.ecosystem_client_id"],
|
||||
"Deletion must be requested for correct ecosystem client ID"
|
||||
);
|
||||
}
|
||||
|
||||
// Wait on ping activity to settle before moving on to the next test. If we were
|
||||
// to shut down telemetry, even though the PingServer caught the expected pings,
|
||||
|
@ -11,9 +11,6 @@ const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm");
|
||||
|
||||
const PREF_CACHED_CLIENTID = "toolkit.telemetry.cachedClientID";
|
||||
|
||||
const SCALAR_DELETION_REQUEST_ECOSYSTEM_CLIENT_ID =
|
||||
"deletion.request.ecosystem_client_id";
|
||||
|
||||
var drsPath;
|
||||
|
||||
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||
@ -37,33 +34,6 @@ function run_test() {
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(async function test_ecosystemClientID() {
|
||||
await ClientID._reset();
|
||||
Assert.ok(!ClientID.getCachedEcosystemClientID());
|
||||
let ecosystemClientID = await ClientID.getEcosystemClientID();
|
||||
Assert.equal(typeof ecosystemClientID, "string");
|
||||
Assert.equal(ClientID.getCachedEcosystemClientID(), ecosystemClientID);
|
||||
|
||||
let clientID = await ClientID.getClientID();
|
||||
await ClientID._reset();
|
||||
await OS.File.writeAtomic(
|
||||
drsPath,
|
||||
JSON.stringify({
|
||||
clientID,
|
||||
}),
|
||||
{
|
||||
encoding: "utf-8",
|
||||
tmpPath: drsPath + ".tmp",
|
||||
}
|
||||
);
|
||||
|
||||
let newClientID = await ClientID.getClientID();
|
||||
Assert.equal(newClientID, clientID);
|
||||
|
||||
let newEcosystemClientID = await ClientID.getEcosystemClientID();
|
||||
Assert.notEqual(newEcosystemClientID, ecosystemClientID);
|
||||
});
|
||||
|
||||
add_task(async function test_client_id() {
|
||||
const invalidIDs = [
|
||||
[-1, "setIntPref"],
|
||||
@ -169,121 +139,27 @@ add_task(async function test_client_id() {
|
||||
}
|
||||
});
|
||||
|
||||
add_task(async function test_setCanaryClientIDs() {
|
||||
add_task(async function test_setCanaryClientID() {
|
||||
const KNOWN_UUID = "c0ffeec0-ffee-c0ff-eec0-ffeec0ffeec0";
|
||||
|
||||
await ClientID._reset();
|
||||
|
||||
// We should be able to set a valid UUID
|
||||
await ClientID.setCanaryClientIDs();
|
||||
await ClientID.setCanaryClientID();
|
||||
let clientID = await ClientID.getClientID();
|
||||
Assert.equal(KNOWN_UUID, clientID);
|
||||
});
|
||||
|
||||
add_task(async function test_resetEcosystemClientID() {
|
||||
await ClientID._reset();
|
||||
|
||||
let firstClientID = await ClientID.getClientID();
|
||||
let firstEcosystemClientID = await ClientID.getEcosystemClientID();
|
||||
Assert.ok(firstClientID);
|
||||
Assert.ok(firstEcosystemClientID);
|
||||
|
||||
// We should reset the ecosystem client id, but not the main client id.
|
||||
await ClientID.resetEcosystemClientID();
|
||||
let secondClientID = await ClientID.getClientID();
|
||||
let secondEcosystemClientID = await ClientID.getEcosystemClientID();
|
||||
Assert.equal(firstClientID, secondClientID);
|
||||
Assert.notEqual(firstEcosystemClientID, secondEcosystemClientID);
|
||||
|
||||
// The new id should have been persisted to disk.
|
||||
await ClientID._reset();
|
||||
let thirdClientID = await ClientID.getClientID();
|
||||
let thirdEcosystemClientID = await ClientID.getEcosystemClientID();
|
||||
Assert.equal(thirdClientID, secondClientID);
|
||||
Assert.equal(thirdEcosystemClientID, secondEcosystemClientID);
|
||||
});
|
||||
|
||||
add_task(async function test_removeClientIDs() {
|
||||
// We should get a valid UUID after reset
|
||||
await ClientID._reset();
|
||||
let firstClientID = await ClientID.getClientID();
|
||||
let firstEcosystemClientID = await ClientID.getEcosystemClientID();
|
||||
Assert.equal(typeof firstClientID, "string");
|
||||
Assert.equal(typeof firstEcosystemClientID, "string");
|
||||
Assert.ok(uuidRegex.test(firstClientID));
|
||||
Assert.ok(uuidRegex.test(firstEcosystemClientID));
|
||||
|
||||
await ClientID.removeClientIDs();
|
||||
|
||||
if (
|
||||
AppConstants.platform != "android" &&
|
||||
AppConstants.MOZ_APP_NAME != "thunderbird"
|
||||
) {
|
||||
// We don't record the old ecosystem client ID on Android or Thunderbird,
|
||||
// since the FxA and telemetry infrastructure is different there.
|
||||
let prefClientID = Services.prefs.getStringPref(PREF_CACHED_CLIENTID, null);
|
||||
let scalarsDeletionRequest = Services.telemetry.getSnapshotForScalars(
|
||||
"deletion-request"
|
||||
);
|
||||
Assert.ok(!prefClientID);
|
||||
Assert.ok(
|
||||
!scalarsDeletionRequest.parent?.[
|
||||
SCALAR_DELETION_REQUEST_ECOSYSTEM_CLIENT_ID
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// When resetting again we should get a new ID
|
||||
let nextClientID = await ClientID.getClientID();
|
||||
let nextEcosystemClientID = await ClientID.getEcosystemClientID();
|
||||
Assert.equal(typeof nextClientID, "string");
|
||||
Assert.equal(typeof nextEcosystemClientID, "string");
|
||||
Assert.ok(uuidRegex.test(nextClientID));
|
||||
Assert.ok(uuidRegex.test(nextEcosystemClientID));
|
||||
Assert.notEqual(
|
||||
firstClientID,
|
||||
nextClientID,
|
||||
"After reset client ID should be different."
|
||||
);
|
||||
Assert.notEqual(
|
||||
firstEcosystemClientID,
|
||||
nextEcosystemClientID,
|
||||
"After reset ecosystem client ID should be different."
|
||||
);
|
||||
|
||||
let cachedID = ClientID.getCachedClientID();
|
||||
Assert.equal(nextClientID, cachedID);
|
||||
|
||||
let cachedEcosystemID = ClientID.getCachedEcosystemClientID();
|
||||
Assert.equal(nextEcosystemClientID, cachedEcosystemID);
|
||||
|
||||
let prefClientID = Services.prefs.getStringPref(PREF_CACHED_CLIENTID, null);
|
||||
Assert.equal(nextClientID, prefClientID);
|
||||
|
||||
if (
|
||||
AppConstants.platform != "android" &&
|
||||
AppConstants.MOZ_APP_NAME != "thunderbird"
|
||||
) {
|
||||
let scalarsDeletionRequest = Services.telemetry.getSnapshotForScalars(
|
||||
"deletion-request"
|
||||
);
|
||||
Assert.equal(
|
||||
nextEcosystemClientID,
|
||||
scalarsDeletionRequest.parent[SCALAR_DELETION_REQUEST_ECOSYSTEM_CLIENT_ID]
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
add_task(async function test_removeParallelGet() {
|
||||
// We should get a valid UUID after reset
|
||||
await ClientID.removeClientIDs();
|
||||
await ClientID.removeClientID();
|
||||
let firstClientID = await ClientID.getClientID();
|
||||
|
||||
// We should get the same ID twice when requesting it in parallel to a reset.
|
||||
let promiseRemoveClientIDs = ClientID.removeClientIDs();
|
||||
let promiseRemoveClientID = ClientID.removeClientID();
|
||||
let p = ClientID.getClientID();
|
||||
let newClientID = await ClientID.getClientID();
|
||||
await promiseRemoveClientIDs;
|
||||
await promiseRemoveClientID;
|
||||
let otherClientID = await p;
|
||||
|
||||
Assert.notEqual(
|
||||
@ -306,19 +182,19 @@ add_task(
|
||||
const KNOWN_UUID = "c0ffeec0-ffee-c0ff-eec0-ffeec0ffeec0";
|
||||
|
||||
// We should get a valid UUID after reset
|
||||
await ClientID.removeClientIDs();
|
||||
await ClientID.removeClientID();
|
||||
let firstClientID = await ClientID.getClientID();
|
||||
Assert.notEqual(KNOWN_UUID, firstClientID, "Client ID should be random.");
|
||||
|
||||
// Set the canary client ID.
|
||||
await ClientID.setCanaryClientIDs();
|
||||
await ClientID.setCanaryClientID();
|
||||
Assert.equal(
|
||||
KNOWN_UUID,
|
||||
await ClientID.getClientID(),
|
||||
"Client ID should be known canary."
|
||||
);
|
||||
|
||||
await ClientID.removeClientIDs();
|
||||
await ClientID.removeClientID();
|
||||
let newClientID = await ClientID.getClientID();
|
||||
Assert.notEqual(
|
||||
KNOWN_UUID,
|
||||
@ -335,7 +211,7 @@ add_task(
|
||||
"After reset we should have detected a canary client ID"
|
||||
);
|
||||
|
||||
await ClientID.removeClientIDs();
|
||||
await ClientID.removeClientID();
|
||||
let clientID = await ClientID.getClientID();
|
||||
Assert.notEqual(
|
||||
KNOWN_UUID,
|
||||
|
@ -91,8 +91,6 @@ skip-if = (os == "android") || (os == "linux" && bits == 32)
|
||||
[test_TelemetryUtils.js]
|
||||
[test_ThirdPartyModulesPing.js]
|
||||
run-if = nightly_build && (os == 'win')
|
||||
[test_EcosystemTelemetry.js]
|
||||
skip-if = appname == "thunderbird"
|
||||
[test_EventPing.js]
|
||||
tags = coverage
|
||||
[test_CoveragePing.js]
|
||||
|
Loading…
Reference in New Issue
Block a user