summaryrefslogtreecommitdiffstats
path: root/toolkit/components/telemetry/app/TelemetryControllerParent.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/telemetry/app/TelemetryControllerParent.jsm')
-rw-r--r--toolkit/components/telemetry/app/TelemetryControllerParent.jsm1450
1 files changed, 1450 insertions, 0 deletions
diff --git a/toolkit/components/telemetry/app/TelemetryControllerParent.jsm b/toolkit/components/telemetry/app/TelemetryControllerParent.jsm
new file mode 100644
index 0000000000..832b9e0b28
--- /dev/null
+++ b/toolkit/components/telemetry/app/TelemetryControllerParent.jsm
@@ -0,0 +1,1450 @@
+/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */
+/* 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/. */
+
+"use strict";
+
+const { AppConstants } = ChromeUtils.import(
+ "resource://gre/modules/AppConstants.jsm"
+);
+const { PromiseUtils } = ChromeUtils.import(
+ "resource://gre/modules/PromiseUtils.jsm"
+);
+const { DeferredTask } = ChromeUtils.import(
+ "resource://gre/modules/DeferredTask.jsm"
+);
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { TelemetryUtils } = ChromeUtils.import(
+ "resource://gre/modules/TelemetryUtils.jsm"
+);
+const { TelemetryControllerBase } = ChromeUtils.import(
+ "resource://gre/modules/TelemetryControllerBase.jsm"
+);
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+
+const Utils = TelemetryUtils;
+
+const PING_FORMAT_VERSION = 4;
+
+// Delay before intializing telemetry (ms)
+const TELEMETRY_DELAY =
+ Services.prefs.getIntPref("toolkit.telemetry.initDelay", 60) * 1000;
+// Delay before initializing telemetry if we're testing (ms)
+const TELEMETRY_TEST_DELAY = 1;
+
+// How long to wait (ms) before sending the new profile ping on the first
+// run of a new profile.
+const NEWPROFILE_PING_DEFAULT_DELAY = 30 * 60 * 1000;
+
+// Ping types.
+const PING_TYPE_MAIN = "main";
+const PING_TYPE_DELETION_REQUEST = "deletion-request";
+const PING_TYPE_UNINSTALL = "uninstall";
+
+// Session ping reasons.
+const REASON_GATHER_PAYLOAD = "gather-payload";
+const REASON_GATHER_SUBSESSION_PAYLOAD = "gather-subsession-payload";
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "Telemetry",
+ "@mozilla.org/base/telemetry;1",
+ "nsITelemetry"
+);
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "jwcrypto",
+ "resource://services-crypto/jwcrypto.jsm"
+);
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ ClientID: "resource://gre/modules/ClientID.jsm",
+ CoveragePing: "resource://gre/modules/CoveragePing.jsm",
+ AsyncShutdown: "resource://gre/modules/AsyncShutdown.jsm",
+ TelemetryStorage: "resource://gre/modules/TelemetryStorage.jsm",
+ TelemetryEnvironment: "resource://gre/modules/TelemetryEnvironment.jsm",
+ TelemetryArchive: "resource://gre/modules/TelemetryArchive.jsm",
+ TelemetrySession: "resource://gre/modules/TelemetrySession.jsm",
+ TelemetrySend: "resource://gre/modules/TelemetrySend.jsm",
+ TelemetryReportingPolicy:
+ "resource://gre/modules/TelemetryReportingPolicy.jsm",
+ TelemetryModules: "resource://gre/modules/ModulesPing.jsm",
+ TelemetryUntrustedModulesPing:
+ "resource://gre/modules/UntrustedModulesPing.jsm",
+ 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",
+});
+
+/**
+ * This is a policy object used to override behavior for testing.
+ */
+var Policy = {
+ now: () => new Date(),
+ generatePingId: () => Utils.generateUUID(),
+ getCachedClientID: () => ClientID.getCachedClientID(),
+};
+
+var EXPORTED_SYMBOLS = ["TelemetryController"];
+
+var TelemetryController = Object.freeze({
+ /**
+ * Used only for testing purposes.
+ */
+ testInitLogging() {
+ TelemetryControllerBase.configureLogging();
+ },
+
+ /**
+ * Used only for testing purposes.
+ */
+ testReset() {
+ return Impl.reset();
+ },
+
+ /**
+ * Used only for testing purposes.
+ */
+ testSetup() {
+ return Impl.setupTelemetry(true);
+ },
+
+ /**
+ * Used only for testing purposes.
+ */
+ testShutdown() {
+ return Impl.shutdown();
+ },
+
+ /**
+ * Used only for testing purposes.
+ */
+ testPromiseJsProbeRegistration() {
+ return Promise.resolve(Impl._probeRegistrationPromise);
+ },
+
+ /**
+ * Register 'dynamic builtin' probes from the JSON definition files.
+ * This is needed to support adding new probes in developer builds
+ * without rebuilding the whole codebase.
+ *
+ * This is not meant to be used outside of local developer builds.
+ */
+ testRegisterJsProbes() {
+ return Impl.registerJsProbes();
+ },
+
+ /**
+ * Used only for testing purposes.
+ */
+ testPromiseDeletionRequestPingSubmitted() {
+ return Promise.resolve(Impl._deletionRequestPingSubmittedPromise);
+ },
+
+ /**
+ * Send a notification.
+ */
+ observe(aSubject, aTopic, aData) {
+ return Impl.observe(aSubject, aTopic, aData);
+ },
+
+ /**
+ * Submit ping payloads to Telemetry. This will assemble a complete ping, adding
+ * environment data, client id and some general info.
+ * Depending on configuration, the ping will be sent to the server (immediately or later)
+ * and archived locally.
+ *
+ * To identify the different pings and to be able to query them pings have a type.
+ * A type is a string identifier that should be unique to the type ping that is being submitted,
+ * it should only contain alphanumeric characters and '-' for separation, i.e. satisfy:
+ * /^[a-z0-9][a-z0-9-]+[a-z0-9]$/i
+ *
+ * @param {String} aType The type of the ping.
+ * @param {Object} aPayload The actual data payload for the ping.
+ * @param {Object} [aOptions] Options object.
+ * @param {Boolean} [aOptions.addClientId=false] true if the ping should contain the client
+ * id, false otherwise.
+ * @param {Boolean} [aOptions.addEnvironment=false] true if the ping should contain the
+ * environment data.
+ * @param {Object} [aOptions.overrideEnvironment=null] set to override the environment data.
+ * @param {Boolean} [aOptions.usePingSender=false] if true, send the ping using the PingSender.
+ * @param {String} [aOptions.overrideClientId=undefined] if set, override the
+ * client id to the provided value. Implies aOptions.addClientId=true.
+ * @returns {Promise} Test-only - a promise that resolves with the ping id once the ping is stored or sent.
+ */
+ submitExternalPing(aType, aPayload, aOptions = {}) {
+ aOptions.addClientId = aOptions.addClientId || false;
+ aOptions.addEnvironment = aOptions.addEnvironment || false;
+ aOptions.usePingSender = aOptions.usePingSender || false;
+
+ return Impl.submitExternalPing(aType, aPayload, aOptions);
+ },
+
+ /**
+ * Get the current session ping data as it would be sent out or stored.
+ *
+ * @param {bool} aSubsession Whether to get subsession data. Optional, defaults to false.
+ * @return {object} The current ping data if Telemetry is enabled, null otherwise.
+ */
+ getCurrentPingData(aSubsession = false) {
+ return Impl.getCurrentPingData(aSubsession);
+ },
+
+ /**
+ * Save a ping to disk.
+ *
+ * @param {String} aType The type of the ping.
+ * @param {Object} aPayload The actual data payload for the ping.
+ * @param {Object} [aOptions] Options object.
+ * @param {Boolean} [aOptions.addClientId=false] true if the ping should contain the client
+ * id, false otherwise.
+ * @param {Boolean} [aOptions.addEnvironment=false] true if the ping should contain the
+ * environment data.
+ * @param {Boolean} [aOptions.overwrite=false] true overwrites a ping with the same name,
+ * if found.
+ * @param {Object} [aOptions.overrideEnvironment=null] set to override the environment data.
+ * @param {String} [aOptions.overrideClientId=undefined] if set, override the
+ * client id to the provided value. Implies aOptions.addClientId=true.
+ *
+ * @returns {Promise} A promise that resolves with the ping id when the ping is saved to
+ * disk.
+ */
+ addPendingPing(aType, aPayload, aOptions = {}) {
+ let options = aOptions;
+ options.addClientId = aOptions.addClientId || false;
+ options.addEnvironment = aOptions.addEnvironment || false;
+ options.overwrite = aOptions.overwrite || false;
+
+ return Impl.addPendingPing(aType, aPayload, options);
+ },
+
+ /**
+ * Check if we have an aborted-session ping from a previous session.
+ * If so, submit and then remove it.
+ *
+ * @return {Promise} Promise that is resolved when the ping is saved.
+ */
+ checkAbortedSessionPing() {
+ return Impl.checkAbortedSessionPing();
+ },
+
+ /**
+ * Save an aborted-session ping to disk without adding it to the pending pings.
+ *
+ * @param {Object} aPayload The ping payload data.
+ * @return {Promise} Promise that is resolved when the ping is saved.
+ */
+ saveAbortedSessionPing(aPayload) {
+ return Impl.saveAbortedSessionPing(aPayload);
+ },
+
+ /**
+ * Remove the aborted-session ping if any exists.
+ *
+ * @return {Promise} Promise that is resolved when the ping was removed.
+ */
+ removeAbortedSessionPing() {
+ return Impl.removeAbortedSessionPing();
+ },
+
+ /**
+ * Create an uninstall ping and write it to disk, replacing any already present.
+ * This is stored independently from other pings, and only read by
+ * the Windows uninstaller.
+ *
+ * WINDOWS ONLY, does nothing and resolves immediately on other platforms.
+ *
+ * @return {Promise} Resolved when the ping has been saved.
+ */
+ saveUninstallPing() {
+ return Impl.saveUninstallPing();
+ },
+
+ /**
+ * Allows the sync ping to tell the controller that it is initializing, so
+ * should be included in the orderly shutdown process.
+ *
+ * @param {Function} aFnShutdown The function to call as telemetry shuts down.
+
+ */
+ registerSyncPingShutdown(afnShutdown) {
+ Impl.registerSyncPingShutdown(afnShutdown);
+ },
+
+ /**
+ * Allows waiting for TelemetryControllers delayed initialization to complete.
+ * The returned promise is guaranteed to resolve before TelemetryController is shutting down.
+ * @return {Promise} Resolved when delayed TelemetryController initialization completed.
+ */
+ promiseInitialized() {
+ return Impl.promiseInitialized();
+ },
+});
+
+var Impl = {
+ _initialized: false,
+ _initStarted: false, // Whether we started setting up TelemetryController.
+ _shuttingDown: false, // Whether the browser is shutting down.
+ _shutDown: false, // Whether the browser has shut down.
+ _logger: null,
+ _prevValues: {},
+ // The previous build ID, if this is the first run with a new build.
+ // Undefined if this is not the first run, or the previous build ID is unknown.
+ _previousBuildID: undefined,
+ _clientID: null,
+ // A task performing delayed initialization
+ _delayedInitTask: null,
+ // The deferred promise resolved when the initialization task completes.
+ _delayedInitTaskDeferred: null,
+
+ // This is a public barrier Telemetry clients can use to add blockers to the shutdown
+ // of TelemetryController.
+ // After this barrier, clients can not submit Telemetry pings anymore.
+ _shutdownBarrier: new AsyncShutdown.Barrier(
+ "TelemetryController: Waiting for clients."
+ ),
+ // This is a private barrier blocked by pending async ping activity (sending & saving).
+ _connectionsBarrier: new AsyncShutdown.Barrier(
+ "TelemetryController: Waiting for pending ping activity"
+ ),
+ // This is true when running in the test infrastructure.
+ _testMode: false,
+ // The task performing the delayed sending of the "new-profile" ping.
+ _delayedNewPingTask: null,
+ // The promise used to wait for the JS probe registration (dynamic builtin).
+ _probeRegistrationPromise: null,
+ // The promise of any outstanding task sending the "deletion-request" ping.
+ _deletionRequestPingSubmittedPromise: null,
+ // A function to shutdown the sync/fxa ping, or null if that ping has not
+ // self-initialized.
+ _fnSyncPingShutdown: null,
+
+ get _log() {
+ return TelemetryControllerBase.log;
+ },
+
+ /**
+ * Get the data for the "application" section of the ping.
+ */
+ _getApplicationSection() {
+ // Querying architecture and update channel can throw. Make sure to recover and null
+ // those fields.
+ let arch = null;
+ try {
+ arch = Services.sysinfo.get("arch");
+ } catch (e) {
+ this._log.trace(
+ "_getApplicationSection - Unable to get system architecture.",
+ e
+ );
+ }
+
+ let updateChannel = null;
+ try {
+ updateChannel = Utils.getUpdateChannel();
+ } catch (e) {
+ this._log.trace(
+ "_getApplicationSection - Unable to get update channel.",
+ e
+ );
+ }
+
+ return {
+ architecture: arch,
+ buildId: Services.appinfo.appBuildID,
+ name: Services.appinfo.name,
+ version: Services.appinfo.version,
+ displayVersion: AppConstants.MOZ_APP_VERSION_DISPLAY,
+ vendor: Services.appinfo.vendor,
+ platformVersion: Services.appinfo.platformVersion,
+ xpcomAbi: Services.appinfo.XPCOMABI,
+ channel: updateChannel,
+ };
+ },
+
+ /**
+ * Assemble a complete ping following the common ping format specification.
+ *
+ * @param {String} aType The type of the ping.
+ * @param {Object} aPayload The actual data payload for the ping.
+ * @param {Object} aOptions Options object.
+ * @param {Boolean} aOptions.addClientId true if the ping should contain the client
+ * id, false otherwise.
+ * @param {Boolean} aOptions.addEnvironment true if the ping should contain the
+ * environment data.
+ * @param {Object} [aOptions.overrideEnvironment=null] set to override the environment data.
+ * @param {String} [aOptions.overrideClientId=undefined] if set, override the
+ * client id to the provided value. Implies aOptions.addClientId=true.
+ * @param {Boolean} [aOptions.useEncryption=false] if true, encrypt data client-side before sending.
+ * @param {Object} [aOptions.publicKey=null] the public key to use if encryption is enabled (JSON Web Key).
+ * @param {String} [aOptions.encryptionKeyId=null] the public key ID to use if encryption is enabled.
+ * @param {String} [aOptions.studyName=null] the study name to use.
+ * @param {String} [aOptions.schemaName=null] the schema name to use if encryption is enabled.
+ * @param {String} [aOptions.schemaNamespace=null] the schema namespace to use if encryption is enabled.
+ * @param {String} [aOptions.schemaVersion=null] the schema version to use if encryption is enabled.
+ * @param {Boolean} [aOptions.addPioneerId=false] true if the ping should contain the Pioneer id, false otherwise.
+ * @param {Boolean} [aOptions.overridePioneerId=undefined] if set, override the
+ * pioneer id to the provided value. Only works if aOptions.addPioneerId=true.
+ * @returns {Object} An object that contains the assembled ping data.
+ */
+ assemblePing: function assemblePing(aType, aPayload, aOptions = {}) {
+ this._log.trace(
+ "assemblePing - Type " + aType + ", aOptions " + JSON.stringify(aOptions)
+ );
+
+ // Clone the payload data so we don't race against unexpected changes in subobjects that are
+ // still referenced by other code.
+ // We can't trust all callers to do this properly on their own.
+ let payload = Cu.cloneInto(aPayload, {});
+
+ // Fill the common ping fields.
+ let pingData = {
+ type: aType,
+ id: Policy.generatePingId(),
+ creationDate: Policy.now().toISOString(),
+ version: PING_FORMAT_VERSION,
+ application: this._getApplicationSection(),
+ payload,
+ };
+
+ if (aOptions.addClientId || aOptions.overrideClientId) {
+ pingData.clientId = aOptions.overrideClientId || this._clientID;
+ }
+
+ if (aOptions.addEnvironment) {
+ pingData.environment =
+ aOptions.overrideEnvironment || TelemetryEnvironment.currentEnvironment;
+
+ // On Android store a flag if the client ID was reset from a canary ID.
+ if (AppConstants.platform == "android" && ClientID.wasCanaryClientID()) {
+ pingData.environment.profile.wasCanary = true;
+ }
+ }
+
+ return pingData;
+ },
+
+ /**
+ * Track any pending ping send and save tasks through the promise passed here.
+ * This is needed to block shutdown on any outstanding ping activity.
+ */
+ _trackPendingPingTask(aPromise) {
+ this._connectionsBarrier.client.addBlocker(
+ "Waiting for ping task",
+ aPromise
+ );
+ },
+
+ /**
+ * Internal function to assemble a complete ping, adding environment data, client id
+ * and some general info. This waits on the client id to be loaded/generated if it's
+ * not yet available. Note that this function is synchronous unless we need to load
+ * the client id.
+ * Depending on configuration, the ping will be sent to the server (immediately or later)
+ * and archived locally.
+ *
+ * @param {String} aType The type of the ping.
+ * @param {Object} aPayload The actual data payload for the ping.
+ * @param {Object} [aOptions] Options object.
+ * @param {Boolean} [aOptions.addClientId=false] true if the ping should contain the client
+ * id, false otherwise.
+ * @param {Boolean} [aOptions.addEnvironment=false] true if the ping should contain the
+ * environment data.
+ * @param {Object} [aOptions.overrideEnvironment=null] set to override the environment data.
+ * @param {Boolean} [aOptions.usePingSender=false] if true, send the ping using the PingSender.
+ * @param {Boolean} [aOptions.useEncryption=false] if true, encrypt data client-side before sending.
+ * @param {Object} [aOptions.publicKey=null] the public key to use if encryption is enabled (JSON Web Key).
+ * @param {String} [aOptions.encryptionKeyId=null] the public key ID to use if encryption is enabled.
+ * @param {String} [aOptions.studyName=null] the study name to use.
+ * @param {String} [aOptions.schemaName=null] the schema name to use if encryption is enabled.
+ * @param {String} [aOptions.schemaNamespace=null] the schema namespace to use if encryption is enabled.
+ * @param {String} [aOptions.schemaVersion=null] the schema version to use if encryption is enabled.
+ * @param {Boolean} [aOptions.addPioneerId=false] true if the ping should contain the Pioneer id, false otherwise.
+ * @param {Boolean} [aOptions.overridePioneerId=undefined] if set, override the
+ * pioneer id to the provided value. Only works if aOptions.addPioneerId=true.
+ * @param {String} [aOptions.overrideClientId=undefined] if set, override the
+ * client id to the provided value. Implies aOptions.addClientId=true.
+ * @returns {Promise} Test-only - a promise that is resolved with the ping id once the ping is stored or sent.
+ */
+ async _submitPingLogic(aType, aPayload, aOptions) {
+ // Make sure to have a clientId if we need one. This cover the case of submitting
+ // a ping early during startup, before Telemetry is initialized, if no client id was
+ // cached.
+ if (!this._clientID && aOptions.addClientId && !aOptions.overrideClientId) {
+ this._log.trace("_submitPingLogic - Waiting on client id");
+ Telemetry.getHistogramById(
+ "TELEMETRY_PING_SUBMISSION_WAITING_CLIENTID"
+ ).add();
+ // We can safely call |getClientID| here and during initialization: we would still
+ // spawn and return one single loading task.
+ this._clientID = await ClientID.getClientID();
+ }
+
+ let pingData = this.assemblePing(aType, aPayload, aOptions);
+ this._log.trace("submitExternalPing - ping assembled, id: " + pingData.id);
+
+ if (aOptions.useEncryption === true) {
+ try {
+ if (!aOptions.publicKey) {
+ throw new Error("Public key is required when using encryption.");
+ }
+
+ if (
+ !(
+ aOptions.schemaName &&
+ aOptions.schemaNamespace &&
+ aOptions.schemaVersion
+ )
+ ) {
+ throw new Error(
+ "Schema name, namespace, and version are required when using encryption."
+ );
+ }
+
+ const payload = {};
+ payload.encryptedData = await jwcrypto.generateJWE(
+ aOptions.publicKey,
+ new TextEncoder("utf-8").encode(JSON.stringify(aPayload))
+ );
+
+ payload.schemaVersion = aOptions.schemaVersion;
+ payload.schemaName = aOptions.schemaName;
+ payload.schemaNamespace = aOptions.schemaNamespace;
+
+ payload.encryptionKeyId = aOptions.encryptionKeyId;
+
+ if (aOptions.addPioneerId === true) {
+ if (aOptions.overridePioneerId) {
+ // The caller provided a substitute id, let's use that
+ // instead of querying the pref.
+ payload.pioneerId = aOptions.overridePioneerId;
+ } else {
+ // This will throw if there is no pioneer ID set.
+ payload.pioneerId = Services.prefs.getStringPref(
+ "toolkit.telemetry.pioneerId"
+ );
+ }
+ payload.studyName = aOptions.studyName;
+ }
+
+ pingData.payload = payload;
+ } catch (e) {
+ this._log.error("_submitPingLogic - Unable to encrypt ping", e);
+ // Do not attempt to continue
+ throw e;
+ }
+ }
+
+ // Always persist the pings if we are allowed to. We should not yield on any of the
+ // following operations to keep this function synchronous for the majority of the calls.
+ let archivePromise = TelemetryArchive.promiseArchivePing(
+ pingData
+ ).catch(e =>
+ this._log.error(
+ "submitExternalPing - Failed to archive ping " + pingData.id,
+ e
+ )
+ );
+ let p = [archivePromise];
+
+ p.push(
+ TelemetrySend.submitPing(pingData, {
+ usePingSender: aOptions.usePingSender,
+ })
+ );
+
+ return Promise.all(p).then(() => pingData.id);
+ },
+
+ /**
+ * Submit ping payloads to Telemetry.
+ *
+ * @param {String} aType The type of the ping.
+ * @param {Object} aPayload The actual data payload for the ping.
+ * @param {Object} [aOptions] Options object.
+ * @param {Boolean} [aOptions.addClientId=false] true if the ping should contain the client
+ * id, false otherwise.
+ * @param {Boolean} [aOptions.addEnvironment=false] true if the ping should contain the
+ * environment data.
+ * @param {Object} [aOptions.overrideEnvironment=null] set to override the environment data.
+ * @param {Boolean} [aOptions.usePingSender=false] if true, send the ping using the PingSender.
+ * @param {Boolean} [aOptions.useEncryption=false] if true, encrypt data client-side before sending.
+ * @param {Object} [aOptions.publicKey=null] the public key to use if encryption is enabled (JSON Web Key).
+ * @param {String} [aOptions.encryptionKeyId=null] the public key ID to use if encryption is enabled.
+ * @param {String} [aOptions.studyName=null] the study name to use.
+ * @param {String} [aOptions.schemaName=null] the schema name to use if encryption is enabled.
+ * @param {String} [aOptions.schemaNamespace=null] the schema namespace to use if encryption is enabled.
+ * @param {String} [aOptions.schemaVersion=null] the schema version to use if encryption is enabled.
+ * @param {Boolean} [aOptions.addPioneerId=false] true if the ping should contain the Pioneer id, false otherwise.
+ * @param {Boolean} [aOptions.overridePioneerId=undefined] if set, override the
+ * pioneer id to the provided value. Only works if aOptions.addPioneerId=true.
+ * @param {String} [aOptions.overrideClientId=undefined] if set, override the
+ * client id to the provided value. Implies aOptions.addClientId=true.
+ * @returns {Promise} Test-only - a promise that is resolved with the ping id once the ping is stored or sent.
+ */
+ submitExternalPing: function send(aType, aPayload, aOptions) {
+ this._log.trace(
+ "submitExternalPing - type: " +
+ aType +
+ ", aOptions: " +
+ JSON.stringify(aOptions)
+ );
+
+ // Reject pings sent after shutdown.
+ if (this._shutDown) {
+ const errorMessage =
+ "submitExternalPing - Submission is not allowed after shutdown, discarding ping of type: " +
+ aType;
+ this._log.error(errorMessage);
+ return Promise.reject(new Error(errorMessage));
+ }
+
+ // Enforce the type string to only contain sane characters.
+ const typeUuid = /^[a-z0-9][a-z0-9-]+[a-z0-9]$/i;
+ if (!typeUuid.test(aType)) {
+ this._log.error("submitExternalPing - invalid ping type: " + aType);
+ let histogram = Telemetry.getKeyedHistogramById(
+ "TELEMETRY_INVALID_PING_TYPE_SUBMITTED"
+ );
+ histogram.add(aType, 1);
+ return Promise.reject(new Error("Invalid type string submitted."));
+ }
+ // Enforce that the payload is an object.
+ if (
+ aPayload === null ||
+ typeof aPayload !== "object" ||
+ Array.isArray(aPayload)
+ ) {
+ this._log.error(
+ "submitExternalPing - invalid payload type: " + typeof aPayload
+ );
+ let histogram = Telemetry.getHistogramById(
+ "TELEMETRY_INVALID_PAYLOAD_SUBMITTED"
+ );
+ histogram.add(1);
+ return Promise.reject(new Error("Invalid payload type submitted."));
+ }
+
+ // We're trying to track down missing sync pings (bug 1663573), so record
+ // a temporary cross-checking counter.
+ if (aType == "sync" && aPayload.why == "shutdown") {
+ Telemetry.scalarSet("telemetry.sync_shutdown_ping_sent", true);
+ }
+
+ let promise = this._submitPingLogic(aType, aPayload, aOptions);
+ this._trackPendingPingTask(promise);
+ return promise;
+ },
+
+ /**
+ * Save a ping to disk.
+ *
+ * @param {String} aType The type of the ping.
+ * @param {Object} aPayload The actual data payload for the ping.
+ * @param {Object} aOptions Options object.
+ * @param {Boolean} aOptions.addClientId true if the ping should contain the client id,
+ * false otherwise.
+ * @param {Boolean} aOptions.addEnvironment true if the ping should contain the
+ * environment data.
+ * @param {Boolean} aOptions.overwrite true overwrites a ping with the same name, if found.
+ * @param {Object} [aOptions.overrideEnvironment=null] set to override the environment data.
+ * @param {String} [aOptions.overrideClientId=undefined] if set, override the
+ * client id to the provided value. Implies aOptions.addClientId=true.
+ *
+ * @returns {Promise} A promise that resolves with the ping id when the ping is saved to
+ * disk.
+ */
+ addPendingPing: function addPendingPing(aType, aPayload, aOptions) {
+ this._log.trace(
+ "addPendingPing - Type " +
+ aType +
+ ", aOptions " +
+ JSON.stringify(aOptions)
+ );
+
+ let pingData = this.assemblePing(aType, aPayload, aOptions);
+
+ let savePromise = TelemetryStorage.savePendingPing(pingData);
+ let archivePromise = TelemetryArchive.promiseArchivePing(pingData).catch(
+ e => {
+ this._log.error(
+ "addPendingPing - Failed to archive ping " + pingData.id,
+ e
+ );
+ }
+ );
+
+ // Wait for both the archiving and ping persistence to complete.
+ let promises = [savePromise, archivePromise];
+ return Promise.all(promises).then(() => pingData.id);
+ },
+
+ /**
+ * Check whether we have an aborted-session ping. If so add it to the pending pings and archive it.
+ *
+ * @return {Promise} Promise that is resolved when the ping is submitted and archived.
+ */
+ async checkAbortedSessionPing() {
+ let ping = await TelemetryStorage.loadAbortedSessionPing();
+ this._log.trace(
+ "checkAbortedSessionPing - found aborted-session ping: " + !!ping
+ );
+ if (!ping) {
+ return;
+ }
+
+ try {
+ // Previous aborted-session might have been with a canary client ID.
+ // Don't send it.
+ if (ping.clientId != Utils.knownClientID) {
+ await TelemetryStorage.addPendingPing(ping);
+ await TelemetryArchive.promiseArchivePing(ping);
+ }
+ } catch (e) {
+ this._log.error(
+ "checkAbortedSessionPing - Unable to add the pending ping",
+ e
+ );
+ } finally {
+ await TelemetryStorage.removeAbortedSessionPing();
+ }
+ },
+
+ /**
+ * Save an aborted-session ping to disk without adding it to the pending pings.
+ *
+ * @param {Object} aPayload The ping payload data.
+ * @return {Promise} Promise that is resolved when the ping is saved.
+ */
+ saveAbortedSessionPing(aPayload) {
+ this._log.trace("saveAbortedSessionPing");
+ const options = { addClientId: true, addEnvironment: true };
+ const pingData = this.assemblePing(PING_TYPE_MAIN, aPayload, options);
+ return TelemetryStorage.saveAbortedSessionPing(pingData);
+ },
+
+ removeAbortedSessionPing() {
+ return TelemetryStorage.removeAbortedSessionPing();
+ },
+
+ async saveUninstallPing() {
+ if (AppConstants.platform != "win") {
+ return undefined;
+ }
+
+ this._log.trace("saveUninstallPing");
+
+ let payload = {};
+ try {
+ payload.otherInstalls = UninstallPing.getOtherInstallsCount();
+ this._log.info(
+ "saveUninstallPing - otherInstalls",
+ payload.otherInstalls
+ );
+ } catch (e) {
+ this._log.warn("saveUninstallPing - getOtherInstallCount failed", e);
+ }
+ const options = { addClientId: true, addEnvironment: true };
+ const pingData = this.assemblePing(PING_TYPE_UNINSTALL, payload, options);
+
+ return TelemetryStorage.saveUninstallPing(pingData);
+ },
+
+ /**
+ * This triggers basic telemetry initialization and schedules a full initialized for later
+ * for performance reasons.
+ *
+ * This delayed initialization means TelemetryController init can be in the following states:
+ * 1) setupTelemetry was never called
+ * or it was called and
+ * 2) _delayedInitTask was scheduled, but didn't run yet.
+ * 3) _delayedInitTask is currently running.
+ * 4) _delayedInitTask finished running and is nulled out.
+ *
+ * @return {Promise} Resolved when TelemetryController and TelemetrySession are fully
+ * initialized. This is only used in tests.
+ */
+ setupTelemetry: function setupTelemetry(testing) {
+ this._initStarted = true;
+ this._shuttingDown = false;
+ this._shutDown = false;
+ this._testMode = testing;
+
+ this._log.trace("setupTelemetry");
+
+ if (this._delayedInitTask) {
+ this._log.error("setupTelemetry - init task already running");
+ return this._delayedInitTaskDeferred.promise;
+ }
+
+ if (this._initialized && !this._testMode) {
+ this._log.error("setupTelemetry - already initialized");
+ return Promise.resolve();
+ }
+
+ // Enable adding scalars in artifact builds and build faster modes.
+ // The function is async: we intentionally don't wait for it to complete
+ // as we don't want to delay startup.
+ this._probeRegistrationPromise = this.registerJsProbes();
+
+ // This will trigger displaying the datachoices infobar.
+ TelemetryReportingPolicy.setup();
+
+ if (!TelemetryControllerBase.enableTelemetryRecording()) {
+ this._log.config(
+ "setupChromeProcess - Telemetry recording is disabled, skipping Chrome process setup."
+ );
+ return Promise.resolve();
+ }
+
+ this._attachObservers();
+
+ // Perform a lightweight, early initialization for the component, just registering
+ // a few observers and initializing the session.
+ TelemetrySession.earlyInit(this._testMode);
+ Services.telemetry.earlyInit();
+
+ // Annotate crash reports so that we get pings for startup crashes
+ TelemetrySend.earlyInit();
+
+ // For very short session durations, we may never load the client
+ // id from disk.
+ // We try to cache it in prefs to avoid this, even though this may
+ // lead to some stale client ids.
+ this._clientID = ClientID.getCachedClientID();
+
+ // Init the update ping telemetry as early as possible. This won't have
+ // an impact on startup.
+ UpdatePing.earlyInit();
+
+ // Delay full telemetry initialization to give the browser time to
+ // run various late initializers. Otherwise our gathered memory
+ // footprint and other numbers would be too optimistic.
+ this._delayedInitTaskDeferred = PromiseUtils.defer();
+ this._delayedInitTask = new DeferredTask(
+ async () => {
+ try {
+ // TODO: This should probably happen after all the delayed init here.
+ this._initialized = true;
+ await TelemetryEnvironment.delayedInit();
+
+ // Load the ClientID.
+ this._clientID = await ClientID.getClientID();
+
+ // Fix-up a canary client ID if detected.
+ const uploadEnabled = Services.prefs.getBoolPref(
+ TelemetryUtils.Preferences.FhrUploadEnabled,
+ false
+ );
+ if (uploadEnabled && this._clientID == Utils.knownClientID) {
+ this._log.trace(
+ "Upload enabled, but got canary client ID. Resetting."
+ );
+ await ClientID.removeClientIDs();
+ 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();
+ this._clientID = await ClientID.getClientID();
+ }
+
+ await TelemetrySend.setup(this._testMode);
+
+ // Perform TelemetrySession delayed init.
+ await TelemetrySession.delayedInit();
+ await Services.telemetry.delayedInit();
+
+ if (
+ Services.prefs.getBoolPref(
+ TelemetryUtils.Preferences.NewProfilePingEnabled,
+ false
+ ) &&
+ !TelemetrySession.newProfilePingSent
+ ) {
+ // Kick off the scheduling of the new-profile ping.
+ this.scheduleNewProfilePing();
+ }
+
+ // Purge the pings archive by removing outdated pings. We don't wait for
+ // this task to complete, but TelemetryStorage blocks on it during
+ // shutdown.
+ TelemetryStorage.runCleanPingArchiveTask();
+
+ // Now that FHR/healthreporter is gone, make sure to remove FHR's DB from
+ // the profile directory. This is a temporary measure that we should drop
+ // in the future.
+ TelemetryStorage.removeFHRDatabase();
+
+ // The init sequence is forced to run on shutdown for short sessions and
+ // we don't want to start TelemetryModules as the timer registration will fail.
+ if (!this._shuttingDown) {
+ // Report the modules loaded in the Firefox process.
+ TelemetryModules.start();
+
+ // Send coverage ping.
+ await CoveragePing.startup();
+
+ // Start the untrusted modules ping, which reports events where
+ // untrusted modules were loaded into the Firefox process.
+ if (AppConstants.platform == "win") {
+ TelemetryUntrustedModulesPing.start();
+ }
+ }
+
+ TelemetryEventPing.startup();
+ EcosystemTelemetry.startup();
+ TelemetryPrioPing.startup();
+
+ if (uploadEnabled) {
+ await this.saveUninstallPing().catch(e =>
+ this._log.warn("_delayedInitTask - saveUninstallPing failed", e)
+ );
+ } else {
+ await TelemetryStorage.removeUninstallPings().catch(e =>
+ this._log.warn("_delayedInitTask - saveUninstallPing", e)
+ );
+ }
+
+ this._delayedInitTaskDeferred.resolve();
+ } catch (e) {
+ this._delayedInitTaskDeferred.reject(e);
+ } finally {
+ this._delayedInitTask = null;
+ }
+ },
+ this._testMode ? TELEMETRY_TEST_DELAY : TELEMETRY_DELAY,
+ this._testMode ? 0 : undefined
+ );
+
+ AsyncShutdown.sendTelemetry.addBlocker(
+ "TelemetryController: shutting down",
+ () => this.shutdown(),
+ () => this._getState()
+ );
+
+ this._delayedInitTask.arm();
+ return this._delayedInitTaskDeferred.promise;
+ },
+
+ // Do proper shutdown waiting and cleanup.
+ async _cleanupOnShutdown() {
+ if (!this._initialized) {
+ return;
+ }
+
+ this._detachObservers();
+
+ // Now do an orderly shutdown.
+ try {
+ if (this._delayedNewPingTask) {
+ await this._delayedNewPingTask.finalize();
+ }
+
+ UpdatePing.shutdown();
+
+ TelemetryEventPing.shutdown();
+ EcosystemTelemetry.shutdown();
+ await TelemetryPrioPing.shutdown();
+
+ // Shutdown the sync ping if it is initialized - this is likely, but not
+ // guaranteed, to submit a "shutdown" sync ping.
+ if (this._fnSyncPingShutdown) {
+ this._fnSyncPingShutdown();
+ }
+
+ // Stop the datachoices infobar display.
+ TelemetryReportingPolicy.shutdown();
+ TelemetryEnvironment.shutdown();
+
+ // Stop any ping sending.
+ await TelemetrySend.shutdown();
+
+ // Send latest data.
+ await TelemetryHealthPing.shutdown();
+
+ await TelemetrySession.shutdown();
+ await Services.telemetry.shutdown();
+
+ // First wait for clients processing shutdown.
+ await this._shutdownBarrier.wait();
+
+ // ... and wait for any outstanding async ping activity.
+ await this._connectionsBarrier.wait();
+
+ if (AppConstants.platform !== "android") {
+ // No PingSender on Android.
+ TelemetrySend.flushPingSenderBatch();
+ }
+
+ // Perform final shutdown operations.
+ await TelemetryStorage.shutdown();
+ } finally {
+ // Reset state.
+ this._initialized = false;
+ this._initStarted = false;
+ this._shutDown = true;
+ }
+ },
+
+ shutdown() {
+ this._log.trace("shutdown");
+
+ this._shuttingDown = true;
+
+ // We can be in one the following states here:
+ // 1) setupTelemetry was never called
+ // or it was called and
+ // 2) _delayedInitTask was scheduled, but didn't run yet.
+ // 3) _delayedInitTask is running now.
+ // 4) _delayedInitTask finished running already.
+
+ // This handles 1).
+ if (!this._initStarted) {
+ this._shutDown = true;
+ return Promise.resolve();
+ }
+
+ // This handles 4).
+ if (!this._delayedInitTask) {
+ // We already ran the delayed initialization.
+ return this._cleanupOnShutdown();
+ }
+
+ // This handles 2) and 3).
+ return this._delayedInitTask
+ .finalize()
+ .then(() => this._cleanupOnShutdown());
+ },
+
+ /**
+ * This observer drives telemetry.
+ */
+ observe(aSubject, aTopic, aData) {
+ // The logger might still be not available at this point.
+ if (aTopic == "profile-after-change") {
+ // If we don't have a logger, we need to make sure |Log.repository.getLogger()| is
+ // called before |getLoggerWithMessagePrefix|. Otherwise logging won't work.
+ TelemetryControllerBase.configureLogging();
+ }
+
+ this._log.trace(`observe - ${aTopic} notified.`);
+
+ switch (aTopic) {
+ case "profile-after-change":
+ // profile-after-change is only registered for chrome processes.
+ return this.setupTelemetry();
+ case "nsPref:changed":
+ if (aData == TelemetryUtils.Preferences.FhrUploadEnabled) {
+ return this._onUploadPrefChange();
+ }
+ }
+ return undefined;
+ },
+
+ /**
+ * Register the sync ping's shutdown handler.
+ */
+ registerSyncPingShutdown(fnShutdown) {
+ if (this._fnSyncPingShutdown) {
+ throw new Error("The sync ping shutdown handler is already registered.");
+ }
+ this._fnSyncPingShutdown = fnShutdown;
+ },
+
+ /**
+ * Get an object describing the current state of this module for AsyncShutdown diagnostics.
+ */
+ _getState() {
+ return {
+ initialized: this._initialized,
+ initStarted: this._initStarted,
+ haveDelayedInitTask: !!this._delayedInitTask,
+ shutdownBarrier: this._shutdownBarrier.state,
+ connectionsBarrier: this._connectionsBarrier.state,
+ sendModule: TelemetrySend.getShutdownState(),
+ haveDelayedNewProfileTask: !!this._delayedNewPingTask,
+ };
+ },
+
+ /**
+ * Called whenever the FHR Upload preference changes (e.g. when user disables FHR from
+ * the preferences panel), this triggers sending the "deletion-request" ping.
+ */
+ _onUploadPrefChange() {
+ const uploadEnabled = Services.prefs.getBoolPref(
+ TelemetryUtils.Preferences.FhrUploadEnabled,
+ false
+ );
+ if (uploadEnabled) {
+ this._log.trace(
+ "_onUploadPrefChange - upload was enabled again. Resetting client ID"
+ );
+
+ // Delete cached client ID immediately, so other usage is forced to refetch it.
+ this._clientID = null;
+
+ // Generate a new client ID and make sure this module uses the new version
+ let p = (async () => {
+ await ClientID.removeClientIDs();
+ let id = await ClientID.getClientID();
+ this._clientID = id;
+ Telemetry.scalarSet("telemetry.data_upload_optin", true);
+
+ await this.saveUninstallPing().catch(e =>
+ this._log.warn("_onUploadPrefChange - saveUninstallPing failed", e)
+ );
+ })();
+
+ this._shutdownBarrier.client.addBlocker(
+ "TelemetryController: resetting client ID after data upload was enabled",
+ p
+ );
+
+ return;
+ }
+
+ let p = (async () => {
+ try {
+ // 1. Cancel the current pings.
+ // 2. Clear unpersisted pings
+ await TelemetrySend.clearCurrentPings();
+
+ // 3. Remove all pending pings
+ await TelemetryStorage.removeAppDataPings();
+ await TelemetryStorage.runRemovePendingPingsTask();
+ await TelemetryStorage.removeUninstallPings();
+ } catch (e) {
+ this._log.error(
+ "_onUploadPrefChange - error clearing pending pings",
+ e
+ );
+ } finally {
+ // 4. Reset session and subsession counter
+ TelemetrySession.resetSubsessionCounter();
+
+ // 5. Collect any additional identifiers we want to send in the
+ // deletion request.
+ const scalars = Telemetry.getSnapshotForScalars(
+ "deletion-request",
+ /* clear */ true
+ );
+
+ // 6. Set ClientID to a known value
+ let oldClientId = await ClientID.getClientID();
+ await ClientID.setCanaryClientIDs();
+ this._clientID = await ClientID.getClientID();
+
+ // 7. Send the deletion-request ping.
+ this._log.trace("_onUploadPrefChange - Sending deletion-request ping.");
+ this.submitExternalPing(
+ PING_TYPE_DELETION_REQUEST,
+ { scalars },
+ { overrideClientId: oldClientId }
+ );
+ this._deletionRequestPingSubmittedPromise = null;
+ }
+ })();
+
+ this._deletionRequestPingSubmittedPromise = p;
+ this._shutdownBarrier.client.addBlocker(
+ "TelemetryController: removing pending pings after data upload was disabled",
+ p
+ );
+
+ Services.obs.notifyObservers(
+ null,
+ TelemetryUtils.TELEMETRY_UPLOAD_DISABLED_TOPIC
+ );
+ },
+
+ QueryInterface: ChromeUtils.generateQI(["nsISupportsWeakReference"]),
+
+ _attachObservers() {
+ if (TelemetryControllerBase.IS_UNIFIED_TELEMETRY) {
+ // Watch the FHR upload setting to trigger "deletion-request" pings.
+ Services.prefs.addObserver(
+ TelemetryUtils.Preferences.FhrUploadEnabled,
+ this,
+ true
+ );
+ }
+ },
+
+ /**
+ * Remove the preference observer to avoid leaks.
+ */
+ _detachObservers() {
+ if (TelemetryControllerBase.IS_UNIFIED_TELEMETRY) {
+ Services.prefs.removeObserver(
+ TelemetryUtils.Preferences.FhrUploadEnabled,
+ this
+ );
+ }
+ },
+
+ /**
+ * Allows waiting for TelemetryControllers delayed initialization to complete.
+ * This will complete before TelemetryController is shutting down.
+ * @return {Promise} Resolved when delayed TelemetryController initialization completed.
+ */
+ promiseInitialized() {
+ return this._delayedInitTaskDeferred.promise;
+ },
+
+ getCurrentPingData(aSubsession) {
+ this._log.trace("getCurrentPingData - subsession: " + aSubsession);
+
+ // Telemetry is disabled, don't gather any data.
+ if (!Telemetry.canRecordBase) {
+ return null;
+ }
+
+ const reason = aSubsession
+ ? REASON_GATHER_SUBSESSION_PAYLOAD
+ : REASON_GATHER_PAYLOAD;
+ const type = PING_TYPE_MAIN;
+ const payload = TelemetrySession.getPayload(reason);
+ const options = { addClientId: true, addEnvironment: true };
+ const ping = this.assemblePing(type, payload, options);
+
+ return ping;
+ },
+
+ async reset() {
+ this._clientID = null;
+ this._fnSyncPingShutdown = null;
+ this._detachObservers();
+
+ let sessionReset = TelemetrySession.testReset();
+
+ this._connectionsBarrier = new AsyncShutdown.Barrier(
+ "TelemetryController: Waiting for pending ping activity"
+ );
+ this._shutdownBarrier = new AsyncShutdown.Barrier(
+ "TelemetryController: Waiting for clients."
+ );
+
+ // We need to kick of the controller setup first for tests that check the
+ // cached client id.
+ let controllerSetup = this.setupTelemetry(true);
+
+ await sessionReset;
+ await TelemetrySend.reset();
+ await TelemetryStorage.reset();
+ await TelemetryEnvironment.testReset();
+
+ await controllerSetup;
+ },
+
+ /**
+ * Schedule sending the "new-profile" ping.
+ */
+ scheduleNewProfilePing() {
+ this._log.trace("scheduleNewProfilePing");
+
+ const sendDelay = Services.prefs.getIntPref(
+ TelemetryUtils.Preferences.NewProfilePingDelay,
+ NEWPROFILE_PING_DEFAULT_DELAY
+ );
+
+ this._delayedNewPingTask = new DeferredTask(async () => {
+ try {
+ await this.sendNewProfilePing();
+ } finally {
+ this._delayedNewPingTask = null;
+ }
+ }, sendDelay);
+
+ this._delayedNewPingTask.arm();
+ },
+
+ /**
+ * Generate and send the new-profile ping
+ */
+ async sendNewProfilePing() {
+ this._log.trace(
+ "sendNewProfilePing - shutting down: " + this._shuttingDown
+ );
+
+ const scalars = Telemetry.getSnapshotForScalars(
+ "new-profile",
+ /* clear */ true
+ );
+
+ // Generate the payload.
+ const payload = {
+ reason: this._shuttingDown ? "shutdown" : "startup",
+ processes: {
+ parent: {
+ scalars: scalars.parent,
+ },
+ },
+ };
+
+ // Generate and send the "new-profile" ping. This uses the
+ // pingsender if we're shutting down.
+ let options = {
+ addClientId: true,
+ addEnvironment: true,
+ usePingSender: this._shuttingDown,
+ };
+ // TODO: we need to be smarter about when to send the ping (and save the
+ // state to file). |requestIdleCallback| is currently only accessible
+ // through DOM. See bug 1361996.
+ await TelemetryController.submitExternalPing(
+ "new-profile",
+ payload,
+ options
+ ).then(
+ () => TelemetrySession.markNewProfilePingSent(),
+ e =>
+ this._log.error(
+ "sendNewProfilePing - failed to submit new-profile ping",
+ e
+ )
+ );
+ },
+
+ /**
+ * Register 'dynamic builtin' probes from the JSON definition files.
+ * This is needed to support adding new probes in developer builds
+ * without rebuilding the whole codebase.
+ *
+ * This is not meant to be used outside of local developer builds.
+ */
+ async registerJsProbes() {
+ // We don't support this outside of developer builds.
+ if (AppConstants.MOZILLA_OFFICIAL && !this._testMode) {
+ return;
+ }
+
+ this._log.trace("registerJsProbes - registering builtin JS probes");
+
+ await this.registerScalarProbes();
+ await this.registerEventProbes();
+ },
+
+ _loadProbeDefinitions(filename) {
+ let probeFile = Services.dirsvc.get("GreD", Ci.nsIFile);
+ probeFile.append(filename);
+ if (!probeFile.exists()) {
+ this._log.trace(
+ `loadProbeDefinitions - no builtin JS probe file ${filename}`
+ );
+ return null;
+ }
+
+ return OS.File.read(probeFile.path, { encoding: "utf-8" });
+ },
+
+ async registerScalarProbes() {
+ this._log.trace(
+ "registerScalarProbes - registering scalar builtin JS probes"
+ );
+
+ // Load the scalar probes JSON file.
+ const scalarProbeFilename = "ScalarArtifactDefinitions.json";
+ let scalarJSProbes = {};
+ try {
+ let fileContent = await this._loadProbeDefinitions(scalarProbeFilename);
+ scalarJSProbes = JSON.parse(fileContent, (property, value) => {
+ // Fixup the "kind" property: it's a string, and we need the constant
+ // coming from nsITelemetry.
+ if (property !== "kind" || typeof value != "string") {
+ return value;
+ }
+
+ let newValue;
+ switch (value) {
+ case "nsITelemetry::SCALAR_TYPE_COUNT":
+ newValue = Telemetry.SCALAR_TYPE_COUNT;
+ break;
+ case "nsITelemetry::SCALAR_TYPE_BOOLEAN":
+ newValue = Telemetry.SCALAR_TYPE_BOOLEAN;
+ break;
+ case "nsITelemetry::SCALAR_TYPE_STRING":
+ newValue = Telemetry.SCALAR_TYPE_STRING;
+ break;
+ }
+ return newValue;
+ });
+ } catch (ex) {
+ this._log.error(
+ `registerScalarProbes - there was an error loading ${scalarProbeFilename}`,
+ ex
+ );
+ }
+
+ // Register the builtin probes.
+ for (let category in scalarJSProbes) {
+ // Expire the expired scalars
+ for (let name in scalarJSProbes[category]) {
+ let def = scalarJSProbes[category][name];
+ if (
+ !def ||
+ !def.expires ||
+ def.expires == "never" ||
+ def.expires == "default"
+ ) {
+ continue;
+ }
+ if (
+ Services.vc.compare(AppConstants.MOZ_APP_VERSION, def.expires) >= 0
+ ) {
+ def.expired = true;
+ }
+ }
+ Telemetry.registerBuiltinScalars(category, scalarJSProbes[category]);
+ }
+ },
+
+ async registerEventProbes() {
+ this._log.trace(
+ "registerEventProbes - registering builtin JS Event probes"
+ );
+
+ // Load the event probes JSON file.
+ const eventProbeFilename = "EventArtifactDefinitions.json";
+ let eventJSProbes = {};
+ try {
+ let fileContent = await this._loadProbeDefinitions(eventProbeFilename);
+ eventJSProbes = JSON.parse(fileContent);
+ } catch (ex) {
+ this._log.error(
+ `registerEventProbes - there was an error loading ${eventProbeFilename}`,
+ ex
+ );
+ }
+
+ // Register the builtin probes.
+ for (let category in eventJSProbes) {
+ for (let name in eventJSProbes[category]) {
+ let def = eventJSProbes[category][name];
+ if (
+ !def ||
+ !def.expires ||
+ def.expires == "never" ||
+ def.expires == "default"
+ ) {
+ continue;
+ }
+ if (
+ Services.vc.compare(AppConstants.MOZ_APP_VERSION, def.expires) >= 0
+ ) {
+ def.expired = true;
+ }
+ }
+ Telemetry.registerBuiltinEvents(category, eventJSProbes[category]);
+ }
+ },
+};