diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
commit | 2aa4a82499d4becd2284cdb482213d541b8804dd (patch) | |
tree | b80bf8bf13c3766139fbacc530efd0dd9d54394c /toolkit/components/telemetry/pings | |
parent | Initial commit. (diff) | |
download | firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.zip |
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/telemetry/pings')
-rw-r--r-- | toolkit/components/telemetry/pings/CoveragePing.jsm | 171 | ||||
-rw-r--r-- | toolkit/components/telemetry/pings/EcosystemTelemetry.jsm | 404 | ||||
-rw-r--r-- | toolkit/components/telemetry/pings/EventPing.jsm | 259 | ||||
-rw-r--r-- | toolkit/components/telemetry/pings/HealthPing.jsm | 291 | ||||
-rw-r--r-- | toolkit/components/telemetry/pings/ModulesPing.jsm | 137 | ||||
-rw-r--r-- | toolkit/components/telemetry/pings/PrioPing.jsm | 156 | ||||
-rw-r--r-- | toolkit/components/telemetry/pings/TelemetrySession.jsm | 1441 | ||||
-rw-r--r-- | toolkit/components/telemetry/pings/UninstallPing.jsm | 104 | ||||
-rw-r--r-- | toolkit/components/telemetry/pings/UntrustedModulesPing.jsm | 76 | ||||
-rw-r--r-- | toolkit/components/telemetry/pings/UpdatePing.jsm | 185 |
10 files changed, 3224 insertions, 0 deletions
diff --git a/toolkit/components/telemetry/pings/CoveragePing.jsm b/toolkit/components/telemetry/pings/CoveragePing.jsm new file mode 100644 index 0000000000..21893d3dae --- /dev/null +++ b/toolkit/components/telemetry/pings/CoveragePing.jsm @@ -0,0 +1,171 @@ +/* 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 { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +ChromeUtils.defineModuleGetter( + this, + "CommonUtils", + "resource://services-common/utils.js" +); +ChromeUtils.defineModuleGetter(this, "Log", "resource://gre/modules/Log.jsm"); +ChromeUtils.defineModuleGetter( + this, + "PromiseUtils", + "resource://gre/modules/PromiseUtils.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "ServiceRequest", + "resource://gre/modules/ServiceRequest.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "UpdateUtils", + "resource://gre/modules/UpdateUtils.jsm" +); + +var EXPORTED_SYMBOLS = ["CoveragePing"]; + +const COVERAGE_VERSION = "2"; + +const COVERAGE_ENABLED_PREF = "toolkit.coverage.enabled"; +const LOG_LEVEL_PREF = "toolkit.coverage.log-level"; +const OPT_OUT_PREF = "toolkit.coverage.opt-out"; +const ALREADY_RUN_PREF = `toolkit.coverage.already-run.v${COVERAGE_VERSION}`; +const COVERAGE_UUID_PREF = `toolkit.coverage.uuid.v${COVERAGE_VERSION}`; +const TELEMETRY_ENABLED_PREF = "datareporting.healthreport.uploadEnabled"; +const REPORTING_ENDPOINT_BASE_PREF = `toolkit.coverage.endpoint.base`; +const REPORTING_ENDPOINT = "submit/coverage/coverage"; +const PING_SUBMISSION_TIMEOUT = 30 * 1000; // 30 seconds + +const log = Log.repository.getLogger("Telemetry::CoveragePing"); +log.level = Services.prefs.getIntPref(LOG_LEVEL_PREF, Log.Level.Error); +log.addAppender(new Log.ConsoleAppender(new Log.BasicFormatter())); + +var CoveragePing = Object.freeze({ + async startup() { + if (!Services.prefs.getBoolPref(COVERAGE_ENABLED_PREF, false)) { + log.debug("coverage not enabled"); + return; + } + + if (Services.prefs.getBoolPref(OPT_OUT_PREF, false)) { + log.debug("user has set opt-out pref"); + return; + } + + if (Services.prefs.getBoolPref(ALREADY_RUN_PREF, false)) { + log.debug("already run on this profile"); + return; + } + + if (!Services.prefs.getCharPref(REPORTING_ENDPOINT_BASE_PREF, null)) { + log.error("no endpoint base set"); + return; + } + + try { + await this.reportTelemetrySetting(); + } catch (e) { + log.error("unable to upload payload", e); + } + }, + + // NOTE - this does not use existing Telemetry code or honor Telemetry opt-out prefs, + // by design. It also sends no identifying data like the client ID. See the "coverage ping" + // documentation for details. + reportTelemetrySetting() { + const enabled = Services.prefs.getBoolPref(TELEMETRY_ENABLED_PREF, false); + + const payload = { + appVersion: Services.appinfo.version, + appUpdateChannel: UpdateUtils.getUpdateChannel(false), + osName: Services.sysinfo.getProperty("name"), + osVersion: Services.sysinfo.getProperty("version"), + telemetryEnabled: enabled, + }; + + let cachedUuid = Services.prefs.getCharPref(COVERAGE_UUID_PREF, null); + if (!cachedUuid) { + // Totally random UUID, just for detecting duplicates. + cachedUuid = CommonUtils.generateUUID(); + Services.prefs.setCharPref(COVERAGE_UUID_PREF, cachedUuid); + } + + let reportingEndpointBase = Services.prefs.getCharPref( + REPORTING_ENDPOINT_BASE_PREF, + null + ); + + let endpoint = `${reportingEndpointBase}/${REPORTING_ENDPOINT}/${COVERAGE_VERSION}/${cachedUuid}`; + + log.debug(`putting to endpoint ${endpoint} with payload:`, payload); + + let deferred = PromiseUtils.defer(); + + let request = new ServiceRequest({ mozAnon: true }); + request.mozBackgroundRequest = true; + request.timeout = PING_SUBMISSION_TIMEOUT; + + request.open("PUT", endpoint, true); + request.overrideMimeType("text/plain"); + request.setRequestHeader("Content-Type", "application/json; charset=UTF-8"); + request.setRequestHeader("Date", new Date().toUTCString()); + + let errorhandler = event => { + let failure = event.type; + log.error(`error making request to ${endpoint}: ${failure}`); + deferred.reject(event); + }; + + request.onerror = errorhandler; + request.ontimeout = errorhandler; + request.onabort = errorhandler; + + request.onloadend = event => { + let status = request.status; + let statusClass = status - (status % 100); + let success = false; + + if (statusClass === 200) { + // We can treat all 2XX as success. + log.info(`successfully submitted, status: ${status}`); + success = true; + } else if (statusClass === 400) { + // 4XX means that something with the request was broken. + + // TODO: we should handle this better, but for now we should avoid resubmitting + // broken requests by pretending success. + success = true; + log.error( + `error submitting to ${endpoint}, status: ${status} - ping request broken?` + ); + } else if (statusClass === 500) { + // 5XX means there was a server-side error and we should try again later. + log.error( + `error submitting to ${endpoint}, status: ${status} - server error, should retry later` + ); + } else { + // We received an unexpected status code. + log.error( + `error submitting to ${endpoint}, status: ${status}, type: ${event.type}` + ); + } + + if (success) { + Services.prefs.setBoolPref(ALREADY_RUN_PREF, true); + log.debug(`result from PUT: ${request.responseText}`); + deferred.resolve(); + } else { + deferred.reject(event); + } + }; + + request.send(JSON.stringify(payload)); + + return deferred.promise; + }, +}); diff --git a/toolkit/components/telemetry/pings/EcosystemTelemetry.jsm b/toolkit/components/telemetry/pings/EcosystemTelemetry.jsm new file mode 100644 index 0000000000..de1d12ff7d --- /dev/null +++ b/toolkit/components/telemetry/pings/EcosystemTelemetry.jsm @@ -0,0 +1,404 @@ +/* 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"]; + +ChromeUtils.import("resource://gre/modules/XPCOMUtils.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", + 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(); + }, +}; diff --git a/toolkit/components/telemetry/pings/EventPing.jsm b/toolkit/components/telemetry/pings/EventPing.jsm new file mode 100644 index 0000000000..1219ac3635 --- /dev/null +++ b/toolkit/components/telemetry/pings/EventPing.jsm @@ -0,0 +1,259 @@ +/* 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 Telemetry Events periodically: + * https://firefox-source-docs.mozilla.org/toolkit/components/telemetry/telemetry/data/event-ping.html + */ + +"use strict"; + +var EXPORTED_SYMBOLS = ["TelemetryEventPing"]; + +ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm", this); + +XPCOMUtils.defineLazyModuleGetters(this, { + TelemetrySession: "resource://gre/modules/TelemetrySession.jsm", + TelemetryController: "resource://gre/modules/TelemetryController.jsm", + Log: "resource://gre/modules/Log.jsm", +}); + +XPCOMUtils.defineLazyServiceGetters(this, { + Telemetry: ["@mozilla.org/base/telemetry;1", "nsITelemetry"], +}); + +ChromeUtils.defineModuleGetter( + this, + "setTimeout", + "resource://gre/modules/Timer.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "clearTimeout", + "resource://gre/modules/Timer.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "TelemetryUtils", + "resource://gre/modules/TelemetryUtils.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "Services", + "resource://gre/modules/Services.jsm" +); + +const Utils = TelemetryUtils; + +const MS_IN_A_MINUTE = 60 * 1000; + +const DEFAULT_EVENT_LIMIT = 1000; +const DEFAULT_MIN_FREQUENCY_MS = 60 * MS_IN_A_MINUTE; +const DEFAULT_MAX_FREQUENCY_MS = 10 * MS_IN_A_MINUTE; + +const LOGGER_NAME = "Toolkit.Telemetry"; +const LOGGER_PREFIX = "TelemetryEventPing::"; + +const EVENT_LIMIT_REACHED_TOPIC = "event-telemetry-storage-limit-reached"; + +var Policy = { + setTimeout: (callback, delayMs) => setTimeout(callback, delayMs), + clearTimeout: id => clearTimeout(id), + sendPing: (type, payload, options) => + TelemetryController.submitExternalPing(type, payload, options), +}; + +var TelemetryEventPing = { + Reason: Object.freeze({ + PERIODIC: "periodic", // Sent the ping containing events from the past periodic interval (default one hour). + MAX: "max", // Sent the ping containing the maximum number (default 1000) of event records, earlier than the periodic interval. + SHUTDOWN: "shutdown", // Recorded data was sent on shutdown. + }), + + EVENT_PING_TYPE: "event", + + _logger: null, + + _testing: false, + + // So that if we quickly reach the max limit we can immediately send. + _lastSendTime: -DEFAULT_MIN_FREQUENCY_MS, + + _processStartTimestamp: 0, + + get dataset() { + return Telemetry.canRecordPrereleaseData + ? Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS + : Ci.nsITelemetry.DATASET_ALL_CHANNELS; + }, + + startup() { + this._log.trace("Starting up."); + + // Calculate process creation once. + this._processStartTimestamp = + Math.round( + (Date.now() - TelemetryUtils.monotonicNow()) / MS_IN_A_MINUTE + ) * MS_IN_A_MINUTE; + + Services.obs.addObserver(this, EVENT_LIMIT_REACHED_TOPIC); + + XPCOMUtils.defineLazyPreferenceGetter( + this, + "maxFrequency", + Utils.Preferences.EventPingMaximumFrequency, + DEFAULT_MAX_FREQUENCY_MS + ); + XPCOMUtils.defineLazyPreferenceGetter( + this, + "minFrequency", + Utils.Preferences.EventPingMinimumFrequency, + DEFAULT_MIN_FREQUENCY_MS + ); + + this._startTimer(); + }, + + shutdown() { + this._log.trace("Shutting down."); + // removeObserver may throw, which could interrupt shutdown. + try { + Services.obs.removeObserver(this, EVENT_LIMIT_REACHED_TOPIC); + } catch (ex) {} + + this._submitPing(this.Reason.SHUTDOWN, true /* discardLeftovers */); + this._clearTimer(); + }, + + observe(aSubject, aTopic, aData) { + switch (aTopic) { + case EVENT_LIMIT_REACHED_TOPIC: + this._log.trace("event limit reached"); + let now = Utils.monotonicNow(); + if (now - this._lastSendTime < this.maxFrequency) { + this._log.trace("can't submit ping immediately as it's too soon"); + this._startTimer( + this.maxFrequency - this._lastSendTime, + this.Reason.MAX, + true /* discardLeftovers*/ + ); + } else { + this._log.trace("submitting ping immediately"); + this._submitPing(this.Reason.MAX); + } + break; + } + }, + + _startTimer( + delay = this.minFrequency, + reason = this.Reason.PERIODIC, + discardLeftovers = false + ) { + this._clearTimer(); + this._timeoutId = Policy.setTimeout( + () => TelemetryEventPing._submitPing(reason, discardLeftovers), + delay + ); + }, + + _clearTimer() { + if (this._timeoutId) { + Policy.clearTimeout(this._timeoutId); + this._timeoutId = null; + } + }, + + /** + * Submits an "event" ping and restarts the timer for the next interval. + * + * @param {String} reason The reason we're sending the ping. One of TelemetryEventPing.Reason. + * @param {bool} discardLeftovers Whether to discard event records left over from a previous ping. + */ + _submitPing(reason, discardLeftovers = false) { + this._log.trace("_submitPing"); + + if (reason !== this.Reason.SHUTDOWN) { + this._startTimer(); + } + + let snapshot = Telemetry.snapshotEvents( + this.dataset, + true /* clear */, + DEFAULT_EVENT_LIMIT + ); + + if (!this._testing) { + for (let process of Object.keys(snapshot)) { + snapshot[process] = snapshot[process].filter( + ([, category]) => !category.startsWith("telemetry.test") + ); + } + } + + let eventCount = Object.values(snapshot).reduce( + (acc, val) => acc + val.length, + 0 + ); + if (eventCount === 0) { + // Don't send a ping if we haven't any events. + this._log.trace("not sending event ping due to lack of events"); + return; + } + + // The reason doesn't matter as it will just be echo'd back. + let sessionMeta = TelemetrySession.getMetadata(reason); + + let payload = { + reason, + processStartTimestamp: this._processStartTimestamp, + sessionId: sessionMeta.sessionId, + subsessionId: sessionMeta.subsessionId, + lostEventsCount: 0, + events: snapshot, + }; + + if (discardLeftovers) { + // Any leftovers must be discarded, the count submitted in the ping. + // This can happen on shutdown or if our max was reached before faster + // than our maxFrequency. + let leftovers = Telemetry.snapshotEvents(this.dataset, true /* clear */); + let leftoverCount = Object.values(leftovers).reduce( + (acc, val) => acc + val.length, + 0 + ); + payload.lostEventsCount = leftoverCount; + } + + const options = { + addClientId: true, + addEnvironment: true, + usePingSender: reason == this.Reason.SHUTDOWN, + }; + + this._lastSendTime = Utils.monotonicNow(); + Telemetry.getHistogramById("TELEMETRY_EVENT_PING_SENT").add(reason); + Policy.sendPing(this.EVENT_PING_TYPE, payload, options); + }, + + /** + * Test-only, restore to initial state. + */ + testReset() { + this._lastSendTime = -DEFAULT_MIN_FREQUENCY_MS; + this._clearTimer(); + this._testing = true; + }, + + get _log() { + if (!this._logger) { + this._logger = Log.repository.getLoggerWithMessagePrefix( + LOGGER_NAME, + LOGGER_PREFIX + "::" + ); + } + + return this._logger; + }, +}; diff --git a/toolkit/components/telemetry/pings/HealthPing.jsm b/toolkit/components/telemetry/pings/HealthPing.jsm new file mode 100644 index 0000000000..bf7999f153 --- /dev/null +++ b/toolkit/components/telemetry/pings/HealthPing.jsm @@ -0,0 +1,291 @@ +/* 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 collects data on send failures and other critical issues with Telemetry submissions. + */ + +"use strict"; + +var EXPORTED_SYMBOLS = ["TelemetryHealthPing"]; + +ChromeUtils.defineModuleGetter( + this, + "TelemetryController", + "resource://gre/modules/TelemetryController.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "setTimeout", + "resource://gre/modules/Timer.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "clearTimeout", + "resource://gre/modules/Timer.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "TelemetryUtils", + "resource://gre/modules/TelemetryUtils.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "Services", + "resource://gre/modules/Services.jsm" +); +ChromeUtils.defineModuleGetter(this, "Log", "resource://gre/modules/Log.jsm"); +ChromeUtils.defineModuleGetter( + this, + "Preferences", + "resource://gre/modules/Preferences.jsm" +); + +const Utils = TelemetryUtils; + +const MS_IN_A_MINUTE = 60 * 1000; +const IS_HEALTH_PING_ENABLED = Preferences.get( + TelemetryUtils.Preferences.HealthPingEnabled, + true +); + +// Send health ping every hour +const SEND_TICK_DELAY = 60 * MS_IN_A_MINUTE; + +// Send top 10 discarded pings only to minimize health ping size +const MAX_SEND_DISCARDED_PINGS = 10; + +const LOGGER_NAME = "Toolkit.Telemetry"; +const LOGGER_PREFIX = "TelemetryHealthPing::"; + +var Policy = { + setSchedulerTickTimeout: (callback, delayMs) => setTimeout(callback, delayMs), + clearSchedulerTickTimeout: id => clearTimeout(id), +}; + +var TelemetryHealthPing = { + Reason: Object.freeze({ + IMMEDIATE: "immediate", // Ping was sent immediately after recording with no delay. + DELAYED: "delayed", // Recorded data was sent after a delay. + SHUT_DOWN: "shutdown", // Recorded data was sent on shutdown. + }), + + FailureType: Object.freeze({ + DISCARDED_FOR_SIZE: "pingDiscardedForSize", + SEND_FAILURE: "sendFailure", + }), + + OsInfo: Object.freeze({ + name: Services.appinfo.OS, + version: + Services.sysinfo.get("kernel_version") || Services.sysinfo.get("version"), + }), + + HEALTH_PING_TYPE: "health", + + _logger: null, + + // The health ping is sent every every SEND_TICK_DELAY. + // Initialize this so that first failures are sent immediately. + _lastSendTime: -SEND_TICK_DELAY, + + /** + * This stores reported send failures with the following structure: + * { + * type1: { + * subtype1: value, + * ... + * subtypeN: value + * }, + * ... + * } + */ + _failures: {}, + _timeoutId: null, + + /** + * Record a failure to send a ping out. + * @param {String} failureType The type of failure (e.g. "timeout", ...). + * @returns {Promise} Test-only, resolved when the ping is stored or sent. + */ + recordSendFailure(failureType) { + return this._addToFailure(this.FailureType.SEND_FAILURE, failureType); + }, + + /** + * Record that a ping was discarded and its type. + * @param {String} pingType The type of discarded ping (e.g. "main", ...). + * @returns {Promise} Test-only, resolved when the ping is stored or sent. + */ + recordDiscardedPing(pingType) { + return this._addToFailure(this.FailureType.DISCARDED_FOR_SIZE, pingType); + }, + + /** + * Assemble payload. + * @param {String} reason A string indicating the triggering reason (e.g. "immediate", "delayed", "shutdown"). + * @returns {Object} The assembled payload. + */ + _assemblePayload(reason) { + this._log.trace("_assemblePayload()"); + let payload = { + os: this.OsInfo, + reason, + }; + + for (let key of Object.keys(this._failures)) { + if (key === this.FailureType.DISCARDED_FOR_SIZE) { + payload[key] = this._getTopDiscardFailures(this._failures[key]); + } else { + payload[key] = this._failures[key]; + } + } + + return payload; + }, + + /** + * Sort input dictionary descending by value. + * @param {Object} failures A dictionary of failures subtype and count. + * @returns {Object} Sorted failures by value. + */ + _getTopDiscardFailures(failures) { + this._log.trace("_getTopDiscardFailures()"); + let sortedItems = Object.entries(failures).sort((first, second) => { + return second[1] - first[1]; + }); + + let result = {}; + sortedItems.slice(0, MAX_SEND_DISCARDED_PINGS).forEach(([key, value]) => { + result[key] = value; + }); + + return result; + }, + + /** + * Assemble the failure information and submit it. + * @param {String} reason A string indicating the triggering reason (e.g. "immediate", "delayed", "shutdown"). + * @returns {Promise} Test-only promise that resolves when the ping was stored or sent (if any). + */ + _submitPing(reason) { + if (!IS_HEALTH_PING_ENABLED || !this._hasDataToSend()) { + return Promise.resolve(); + } + + this._log.trace("_submitPing(" + reason + ")"); + let payload = this._assemblePayload(reason); + this._clearData(); + this._lastSendTime = Utils.monotonicNow(); + + let options = { + addClientId: true, + usePingSender: reason === this.Reason.SHUT_DOWN, + }; + + return new Promise(r => + // If we submit the health ping immediately, the send task would be triggered again + // before discarding oversized pings from the queue. + // To work around this, we send the ping on the next tick. + Services.tm.dispatchToMainThread(() => + r( + TelemetryController.submitExternalPing( + this.HEALTH_PING_TYPE, + payload, + options + ) + ) + ) + ); + }, + + /** + * Accumulate failure information and trigger a ping immediately or on timeout. + * @param {String} failureType The type of failure (e.g. "timeout", ...). + * @param {String} failureSubType The subtype of failure (e.g. ping type, ...). + * @returns {Promise} Test-only, resolved when the ping is stored or sent. + */ + _addToFailure(failureType, failureSubType) { + this._log.trace( + "_addToFailure() - with type and subtype: " + + failureType + + " : " + + failureSubType + ); + + if (!(failureType in this._failures)) { + this._failures[failureType] = {}; + } + + let current = this._failures[failureType][failureSubType] || 0; + this._failures[failureType][failureSubType] = current + 1; + + const now = Utils.monotonicNow(); + if (now - this._lastSendTime >= SEND_TICK_DELAY) { + return this._submitPing(this.Reason.IMMEDIATE); + } + + let submissionDelay = SEND_TICK_DELAY - now - this._lastSendTime; + this._timeoutId = Policy.setSchedulerTickTimeout( + () => TelemetryHealthPing._submitPing(this.Reason.DELAYED), + submissionDelay + ); + return Promise.resolve(); + }, + + /** + * @returns {boolean} Check the availability of recorded failures data. + */ + _hasDataToSend() { + return Object.keys(this._failures).length !== 0; + }, + + /** + * Clear recorded failures data. + */ + _clearData() { + this._log.trace("_clearData()"); + this._failures = {}; + }, + + /** + * Clear and reset timeout. + */ + _resetTimeout() { + if (this._timeoutId) { + Policy.clearSchedulerTickTimeout(this._timeoutId); + this._timeoutId = null; + } + }, + + /** + * Submit latest ping on shutdown. + * @returns {Promise} Test-only, resolved when the ping is stored or sent. + */ + shutdown() { + this._log.trace("shutdown()"); + this._resetTimeout(); + return this._submitPing(this.Reason.SHUT_DOWN); + }, + + /** + * Test-only, restore to initial state. + */ + testReset() { + this._lastSendTime = -SEND_TICK_DELAY; + this._clearData(); + this._resetTimeout(); + }, + + get _log() { + if (!this._logger) { + this._logger = Log.repository.getLoggerWithMessagePrefix( + LOGGER_NAME, + LOGGER_PREFIX + "::" + ); + } + + return this._logger; + }, +}; diff --git a/toolkit/components/telemetry/pings/ModulesPing.jsm b/toolkit/components/telemetry/pings/ModulesPing.jsm new file mode 100644 index 0000000000..a6b16c6734 --- /dev/null +++ b/toolkit/components/telemetry/pings/ModulesPing.jsm @@ -0,0 +1,137 @@ +/* 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 { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" +); + +ChromeUtils.defineModuleGetter( + this, + "Preferences", + "resource://gre/modules/Preferences.jsm" +); +ChromeUtils.defineModuleGetter(this, "Log", "resource://gre/modules/Log.jsm"); +ChromeUtils.defineModuleGetter( + this, + "TelemetryController", + "resource://gre/modules/TelemetryController.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "AppConstants", + "resource://gre/modules/AppConstants.jsm" +); + +XPCOMUtils.defineLazyServiceGetter( + this, + "gUpdateTimerManager", + "@mozilla.org/updates/timer-manager;1", + "nsIUpdateTimerManager" +); +XPCOMUtils.defineLazyServiceGetter( + this, + "Telemetry", + "@mozilla.org/base/telemetry;1", + "nsITelemetry" +); + +var EXPORTED_SYMBOLS = ["TelemetryModules"]; + +const LOGGER_NAME = "Toolkit.Telemetry"; +const LOGGER_PREFIX = "TelemetryModules::"; + +// The default is 1 week. +const MODULES_PING_INTERVAL_SECONDS = 7 * 24 * 60 * 60; +const MODULES_PING_INTERVAL_PREFERENCE = + "toolkit.telemetry.modulesPing.interval"; + +const MAX_MODULES_NUM = 512; +const MAX_NAME_LENGTH = 64; +const TRUNCATION_DELIMITER = "\u2026"; + +var TelemetryModules = Object.freeze({ + _log: Log.repository.getLoggerWithMessagePrefix(LOGGER_NAME, LOGGER_PREFIX), + + start() { + // The list of loaded modules is obtainable only when the profiler is enabled. + // If it isn't, we don't want to send the ping at all. + if (!AppConstants.MOZ_GECKO_PROFILER) { + return; + } + + // Use nsIUpdateTimerManager for a long-duration timer that survives across sessions. + let interval = Preferences.get( + MODULES_PING_INTERVAL_PREFERENCE, + MODULES_PING_INTERVAL_SECONDS + ); + gUpdateTimerManager.registerTimer( + "telemetry_modules_ping", + this, + interval, + interval != 0 // only skip the first interval if the interval is non-0 + ); + }, + + /** + * Called when the 'telemetry_modules_ping' timer fires. + */ + notify() { + try { + Telemetry.getLoadedModules().then( + modules => { + modules = modules.filter(module => !!module.name.length); + + // Cut the list of modules to MAX_MODULES_NUM entries. + if (modules.length > MAX_MODULES_NUM) { + modules = modules.slice(0, MAX_MODULES_NUM); + } + + // Cut the file names of the modules to MAX_NAME_LENGTH characters. + for (let module of modules) { + if (module.name.length > MAX_NAME_LENGTH) { + module.name = + module.name.substr(0, MAX_NAME_LENGTH - 1) + + TRUNCATION_DELIMITER; + } + + if ( + module.debugName !== null && + module.debugName.length > MAX_NAME_LENGTH + ) { + module.debugName = + module.debugName.substr(0, MAX_NAME_LENGTH - 1) + + TRUNCATION_DELIMITER; + } + + if ( + module.certSubject !== undefined && + module.certSubject.length > MAX_NAME_LENGTH + ) { + module.certSubject = + module.certSubject.substr(0, MAX_NAME_LENGTH - 1) + + TRUNCATION_DELIMITER; + } + } + + TelemetryController.submitExternalPing( + "modules", + { + version: 1, + modules, + }, + { + addClientId: true, + addEnvironment: true, + } + ); + }, + err => this._log.error("notify - promise failed", err) + ); + } catch (ex) { + this._log.error("notify - caught exception", ex); + } + }, +}); diff --git a/toolkit/components/telemetry/pings/PrioPing.jsm b/toolkit/components/telemetry/pings/PrioPing.jsm new file mode 100644 index 0000000000..240910e063 --- /dev/null +++ b/toolkit/components/telemetry/pings/PrioPing.jsm @@ -0,0 +1,156 @@ +/* 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 Origin Telemetry periodically: + * https://firefox-source-docs.mozilla.org/toolkit/components/telemetry/telemetry/data/prio-ping.html + */ + +"use strict"; + +var EXPORTED_SYMBOLS = ["TelemetryPrioPing"]; + +ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm", this); + +XPCOMUtils.defineLazyModuleGetters(this, { + TelemetryController: "resource://gre/modules/TelemetryController.jsm", + Log: "resource://gre/modules/Log.jsm", +}); + +XPCOMUtils.defineLazyServiceGetters(this, { + Telemetry: ["@mozilla.org/base/telemetry;1", "nsITelemetry"], +}); + +const { TelemetryUtils } = ChromeUtils.import( + "resource://gre/modules/TelemetryUtils.jsm" +); +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +const Utils = TelemetryUtils; + +const LOGGER_NAME = "Toolkit.Telemetry"; +const LOGGER_PREFIX = "TelemetryPrioPing"; + +// Triggered from native Origin Telemetry storage. +const PRIO_LIMIT_REACHED_TOPIC = "origin-telemetry-storage-limit-reached"; + +const PRIO_PING_VERSION = "1"; + +var Policy = { + sendPing: (type, payload, options) => + TelemetryController.submitExternalPing(type, payload, options), + getEncodedOriginSnapshot: async aClear => + Telemetry.getEncodedOriginSnapshot(aClear), +}; + +var TelemetryPrioPing = { + Reason: Object.freeze({ + PERIODIC: "periodic", // Sent the ping containing Origin Telemetry from the past periodic interval (default 24h). + MAX: "max", // Sent the ping containing at least the maximum number (default 10) of prioData elements, earlier than the periodic interval. + SHUTDOWN: "shutdown", // Recorded data was sent on shutdown. + }), + + PRIO_PING_TYPE: "prio", + + _logger: null, + _testing: false, + _timeoutId: null, + + startup() { + if (!this._testing && !Telemetry.canRecordPrereleaseData) { + this._log.trace("Extended collection disabled. Prio ping disabled."); + return; + } + + if ( + !this._testing && + !Services.prefs.getBoolPref(Utils.Preferences.PrioPingEnabled, true) + ) { + this._log.trace("Prio ping disabled by pref."); + return; + } + this._log.trace("Starting up."); + + Services.obs.addObserver(this, PRIO_LIMIT_REACHED_TOPIC); + }, + + async shutdown() { + this._log.trace("Shutting down."); + // removeObserver may throw, which could interrupt shutdown. + try { + Services.obs.removeObserver(this, PRIO_LIMIT_REACHED_TOPIC); + } catch (ex) {} + + await this._submitPing(this.Reason.SHUTDOWN); + }, + + observe(aSubject, aTopic, aData) { + switch (aTopic) { + case PRIO_LIMIT_REACHED_TOPIC: + this._log.trace("prio limit reached"); + this._submitPing(this.Reason.MAX); + break; + } + }, + + periodicPing() { + this._log.trace("periodic ping triggered"); + this._submitPing(this.Reason.PERIODIC); + }, + + /** + * Submits an "prio" ping and restarts the timer for the next interval. + * + * @param {String} reason The reason we're sending the ping. One of TelemetryPrioPing.Reason. + */ + async _submitPing(reason) { + this._log.trace("_submitPing"); + + let snapshot = await Policy.getEncodedOriginSnapshot(true /* clear */); + + if (!this._testing) { + snapshot = snapshot.filter( + ({ encoding }) => !encoding.startsWith("telemetry.test") + ); + } + + if (snapshot.length === 0) { + // Don't send a ping if we haven't anything to send + this._log.trace("nothing to send"); + return; + } + + let payload = { + version: PRIO_PING_VERSION, + reason, + prioData: snapshot, + }; + + const options = { + addClientId: false, + addEnvironment: false, + usePingSender: reason === this.Reason.SHUTDOWN, + }; + + Policy.sendPing(this.PRIO_PING_TYPE, payload, options); + }, + + /** + * Test-only, restore to initial state. + */ + testReset() { + this._testing = true; + }, + + get _log() { + if (!this._logger) { + this._logger = Log.repository.getLoggerWithMessagePrefix( + LOGGER_NAME, + LOGGER_PREFIX + "::" + ); + } + + return this._logger; + }, +}; diff --git a/toolkit/components/telemetry/pings/TelemetrySession.jsm b/toolkit/components/telemetry/pings/TelemetrySession.jsm new file mode 100644 index 0000000000..8f77441876 --- /dev/null +++ b/toolkit/components/telemetry/pings/TelemetrySession.jsm @@ -0,0 +1,1441 @@ +/* -*- 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 { Log } = ChromeUtils.import("resource://gre/modules/Log.jsm"); +ChromeUtils.import("resource://gre/modules/Services.jsm", this); +ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm", this); +ChromeUtils.import("resource://gre/modules/TelemetryUtils.jsm", this); +const { AppConstants } = ChromeUtils.import( + "resource://gre/modules/AppConstants.jsm" +); + +XPCOMUtils.defineLazyModuleGetters(this, { + AddonManagerPrivate: "resource://gre/modules/AddonManager.jsm", + TelemetryController: "resource://gre/modules/TelemetryController.jsm", + TelemetryStorage: "resource://gre/modules/TelemetryStorage.jsm", + UITelemetry: "resource://gre/modules/UITelemetry.jsm", + TelemetryEnvironment: "resource://gre/modules/TelemetryEnvironment.jsm", + TelemetryReportingPolicy: + "resource://gre/modules/TelemetryReportingPolicy.jsm", + TelemetryScheduler: "resource://gre/modules/TelemetryScheduler.jsm", +}); + +const Utils = TelemetryUtils; + +const myScope = this; + +// When modifying the payload in incompatible ways, please bump this version number +const PAYLOAD_VERSION = 4; +const PING_TYPE_MAIN = "main"; +const PING_TYPE_SAVED_SESSION = "saved-session"; + +const REASON_ABORTED_SESSION = "aborted-session"; +const REASON_DAILY = "daily"; +const REASON_SAVED_SESSION = "saved-session"; +const REASON_GATHER_PAYLOAD = "gather-payload"; +const REASON_GATHER_SUBSESSION_PAYLOAD = "gather-subsession-payload"; +const REASON_TEST_PING = "test-ping"; +const REASON_ENVIRONMENT_CHANGE = "environment-change"; +const REASON_SHUTDOWN = "shutdown"; + +const ENVIRONMENT_CHANGE_LISTENER = "TelemetrySession::onEnvironmentChange"; + +const MIN_SUBSESSION_LENGTH_MS = + Services.prefs.getIntPref("toolkit.telemetry.minSubsessionLength", 5 * 60) * + 1000; + +const LOGGER_NAME = "Toolkit.Telemetry"; +const LOGGER_PREFIX = + "TelemetrySession" + (Utils.isContentProcess ? "#content::" : "::"); + +// Whether the FHR/Telemetry unification features are enabled. +// Changing this pref requires a restart. +const IS_UNIFIED_TELEMETRY = Services.prefs.getBoolPref( + TelemetryUtils.Preferences.Unified, + false +); + +var gWasDebuggerAttached = false; + +XPCOMUtils.defineLazyServiceGetters(this, { + Telemetry: ["@mozilla.org/base/telemetry;1", "nsITelemetry"], +}); + +function generateUUID() { + let str = Cc["@mozilla.org/uuid-generator;1"] + .getService(Ci.nsIUUIDGenerator) + .generateUUID() + .toString(); + // strip {} + return str.substring(1, str.length - 1); +} + +/** + * This is a policy object used to override behavior for testing. + */ +var Policy = { + now: () => new Date(), + monotonicNow: Utils.monotonicNow, + generateSessionUUID: () => generateUUID(), + generateSubsessionUUID: () => generateUUID(), +}; + +/** + * Get the ping type based on the payload. + * @param {Object} aPayload The ping payload. + * @return {String} A string representing the ping type. + */ +function getPingType(aPayload) { + // To remain consistent with server-side ping handling, set "saved-session" as the ping + // type for "saved-session" payload reasons. + if (aPayload.info.reason == REASON_SAVED_SESSION) { + return PING_TYPE_SAVED_SESSION; + } + + return PING_TYPE_MAIN; +} + +/** + * Annotate the current session ID with the crash reporter to map potential + * crash pings with the related main ping. + */ +function annotateCrashReport(sessionId) { + try { + const cr = Cc["@mozilla.org/toolkit/crash-reporter;1"]; + if (cr) { + cr.getService(Ci.nsICrashReporter).annotateCrashReport( + "TelemetrySessionId", + sessionId + ); + } + } catch (e) { + // Ignore errors when crash reporting is disabled + } +} + +/** + * Read current process I/O counters. + */ +var processInfo = { + _initialized: false, + _IO_COUNTERS: null, + _kernel32: null, + _GetProcessIoCounters: null, + _GetCurrentProcess: null, + getCounters() { + let isWindows = "@mozilla.org/windows-registry-key;1" in Cc; + if (isWindows) { + return this.getCounters_Windows(); + } + return null; + }, + getCounters_Windows() { + if (!this._initialized) { + var { ctypes } = ChromeUtils.import("resource://gre/modules/ctypes.jsm"); + this._IO_COUNTERS = new ctypes.StructType("IO_COUNTERS", [ + { readOps: ctypes.unsigned_long_long }, + { writeOps: ctypes.unsigned_long_long }, + { otherOps: ctypes.unsigned_long_long }, + { readBytes: ctypes.unsigned_long_long }, + { writeBytes: ctypes.unsigned_long_long }, + { otherBytes: ctypes.unsigned_long_long }, + ]); + try { + this._kernel32 = ctypes.open("Kernel32.dll"); + this._GetProcessIoCounters = this._kernel32.declare( + "GetProcessIoCounters", + ctypes.winapi_abi, + ctypes.bool, // return + ctypes.voidptr_t, // hProcess + this._IO_COUNTERS.ptr + ); // lpIoCounters + this._GetCurrentProcess = this._kernel32.declare( + "GetCurrentProcess", + ctypes.winapi_abi, + ctypes.voidptr_t + ); // return + this._initialized = true; + } catch (err) { + return null; + } + } + let io = new this._IO_COUNTERS(); + if (!this._GetProcessIoCounters(this._GetCurrentProcess(), io.address())) { + return null; + } + return [parseInt(io.readBytes), parseInt(io.writeBytes)]; + }, +}; + +var EXPORTED_SYMBOLS = ["TelemetrySession"]; + +var TelemetrySession = Object.freeze({ + /** + * Send a ping to a test server. Used only for testing. + */ + testPing() { + return Impl.testPing(); + }, + /** + * Returns the current telemetry payload. + * @param reason Optional, the reason to trigger the payload. + * @param clearSubsession Optional, whether to clear subsession specific data. + * @returns Object + */ + getPayload(reason, clearSubsession = false) { + return Impl.getPayload(reason, clearSubsession); + }, + /** + * Save the session state to a pending file. + * Used only for testing purposes. + */ + testSavePendingPing() { + return Impl.testSavePendingPing(); + }, + /** + * Collect and store information about startup. + */ + gatherStartup() { + return Impl.gatherStartup(); + }, + /** + * Inform the ping which AddOns are installed. + * + * @param aAddOns - The AddOns. + */ + setAddOns(aAddOns) { + return Impl.setAddOns(aAddOns); + }, + /** + * Descriptive metadata + * + * @param reason + * The reason for the telemetry ping, this will be included in the + * returned metadata, + * @return The metadata as a JS object + */ + getMetadata(reason) { + return Impl.getMetadata(reason); + }, + + /** + * Reset the subsession and profile subsession counter. + * This should only be called when the profile should be considered completely new, + * e.g. after opting out of sending Telemetry + */ + resetSubsessionCounter() { + Impl._subsessionCounter = 0; + Impl._profileSubsessionCounter = 0; + }, + + /** + * Used only for testing purposes. + */ + testReset() { + Impl._newProfilePingSent = false; + Impl._sessionId = null; + Impl._subsessionId = null; + Impl._previousSessionId = null; + Impl._previousSubsessionId = null; + Impl._subsessionCounter = 0; + Impl._profileSubsessionCounter = 0; + Impl._subsessionStartActiveTicks = 0; + Impl._sessionActiveTicks = 0; + Impl._isUserActive = true; + Impl._subsessionStartTimeMonotonic = 0; + Impl._lastEnvironmentChangeDate = Policy.monotonicNow(); + this.testUninstall(); + }, + /** + * Triggers shutdown of the module. + */ + shutdown() { + return Impl.shutdownChromeProcess(); + }, + /** + * Used only for testing purposes. + */ + testUninstall() { + try { + Impl.uninstall(); + } catch (ex) { + // Ignore errors + } + }, + /** + * Lightweight init function, called as soon as Firefox starts. + */ + earlyInit(aTesting = false) { + return Impl.earlyInit(aTesting); + }, + /** + * Does the "heavy" Telemetry initialization later on, so we + * don't impact startup performance. + * @return {Promise} Resolved when the initialization completes. + */ + delayedInit() { + return Impl.delayedInit(); + }, + /** + * Send a notification. + */ + observe(aSubject, aTopic, aData) { + return Impl.observe(aSubject, aTopic, aData); + }, + /** + * Marks the "new-profile" ping as sent in the telemetry state file. + * @return {Promise} A promise resolved when the new telemetry state is saved to disk. + */ + markNewProfilePingSent() { + return Impl.markNewProfilePingSent(); + }, + /** + * Returns if the "new-profile" ping has ever been sent for this profile. + * Please note that the returned value is trustworthy only after the delayed setup. + * + * @return {Boolean} True if the new profile ping was sent on this profile, + * false otherwise. + */ + get newProfilePingSent() { + return Impl._newProfilePingSent; + }, + + saveAbortedSessionPing(aProvidedPayload) { + return Impl._saveAbortedSessionPing(aProvidedPayload); + }, + + sendDailyPing() { + return Impl._sendDailyPing(); + }, +}); + +var Impl = { + _initialized: false, + _logger: null, + _slowSQLStartup: {}, + // The activity state for the user. If false, don't count the next + // active tick. Otherwise, increment the active ticks as usual. + _isUserActive: true, + _startupIO: {}, + // The previous build ID, if this is the first run with a new build. + // Null if this is the first run, or the previous build ID is unknown. + _previousBuildId: null, + // Unique id that identifies this session so the server can cope with duplicate + // submissions, orphaning and other oddities. The id is shared across subsessions. + _sessionId: null, + // Random subsession id. + _subsessionId: null, + // Session id of the previous session, null on first run. + _previousSessionId: null, + // Subsession id of the previous subsession (even if it was in a different session), + // null on first run. + _previousSubsessionId: null, + // The running no. of subsessions since the start of the browser session + _subsessionCounter: 0, + // The running no. of all subsessions for the whole profile life time + _profileSubsessionCounter: 0, + // Date of the last session split + _subsessionStartDate: null, + // Start time of the current subsession using a monotonic clock for the subsession + // length measurements. + _subsessionStartTimeMonotonic: 0, + // The active ticks counted when the subsession starts + _subsessionStartActiveTicks: 0, + // Active ticks in the whole session. + _sessionActiveTicks: 0, + // A task performing delayed initialization of the chrome process + _delayedInitTask: null, + _testing: false, + // An accumulator of total memory across all processes. Only valid once the final child reports. + _lastEnvironmentChangeDate: 0, + // We save whether the "new-profile" ping was sent yet, to + // survive profile refresh and migrations. + _newProfilePingSent: false, + // Keep track of the active observers + _observedTopics: new Set(), + + addObserver(aTopic) { + Services.obs.addObserver(this, aTopic); + this._observedTopics.add(aTopic); + }, + + removeObserver(aTopic) { + Services.obs.removeObserver(this, aTopic); + this._observedTopics.delete(aTopic); + }, + + get _log() { + if (!this._logger) { + this._logger = Log.repository.getLoggerWithMessagePrefix( + LOGGER_NAME, + LOGGER_PREFIX + ); + } + return this._logger; + }, + + /** + * Gets a series of simple measurements (counters). At the moment, this + * only returns startup data from nsIAppStartup.getStartupInfo(). + * @param {Boolean} isSubsession True if this is a subsession, false otherwise. + * @param {Boolean} clearSubsession True if a new subsession is being started, false otherwise. + * + * @return simple measurements as a dictionary. + */ + getSimpleMeasurements: function getSimpleMeasurements( + forSavedSession, + isSubsession, + clearSubsession + ) { + let si = Services.startup.getStartupInfo(); + + // Measurements common to chrome and content processes. + let elapsedTime = Date.now() - si.process; + var ret = { + totalTime: Math.round(elapsedTime / 1000), // totalTime, in seconds + }; + + // Look for app-specific timestamps + var appTimestamps = {}; + try { + let o = {}; + ChromeUtils.import("resource://gre/modules/TelemetryTimestamps.jsm", o); + appTimestamps = o.TelemetryTimestamps.get(); + } catch (ex) {} + + // Only submit this if the extended set is enabled. + if (!Utils.isContentProcess && Telemetry.canRecordExtended) { + try { + ret.addonManager = AddonManagerPrivate.getSimpleMeasures(); + } catch (ex) {} + } + + if (si.process) { + for (let field of Object.keys(si)) { + if (field == "process") { + continue; + } + ret[field] = si[field] - si.process; + } + + for (let p in appTimestamps) { + if (!(p in ret) && appTimestamps[p]) { + ret[p] = appTimestamps[p] - si.process; + } + } + } + + ret.startupInterrupted = Number(Services.startup.interrupted); + + let maximalNumberOfConcurrentThreads = + Telemetry.maximalNumberOfConcurrentThreads; + if (maximalNumberOfConcurrentThreads) { + ret.maximalNumberOfConcurrentThreads = maximalNumberOfConcurrentThreads; + } + + if (Utils.isContentProcess) { + return ret; + } + + // Measurements specific to chrome process + + // Update debuggerAttached flag + let debugService = Cc["@mozilla.org/xpcom/debug;1"].getService( + Ci.nsIDebug2 + ); + let isDebuggerAttached = debugService.isDebuggerAttached; + gWasDebuggerAttached = gWasDebuggerAttached || isDebuggerAttached; + ret.debuggerAttached = Number(gWasDebuggerAttached); + + let shutdownDuration = Telemetry.lastShutdownDuration; + if (shutdownDuration) { + ret.shutdownDuration = shutdownDuration; + } + + let failedProfileLockCount = Telemetry.failedProfileLockCount; + if (failedProfileLockCount) { + ret.failedProfileLockCount = failedProfileLockCount; + } + + for (let ioCounter in this._startupIO) { + ret[ioCounter] = this._startupIO[ioCounter]; + } + + let activeTicks = this._sessionActiveTicks; + if (isSubsession) { + activeTicks = this._sessionActiveTicks - this._subsessionStartActiveTicks; + } + + if (clearSubsession) { + this._subsessionStartActiveTicks = this._sessionActiveTicks; + } + + ret.activeTicks = activeTicks; + + return ret; + }, + + getHistograms: function getHistograms(clearSubsession) { + return Telemetry.getSnapshotForHistograms( + "main", + clearSubsession, + !this._testing + ); + }, + + getKeyedHistograms(clearSubsession) { + return Telemetry.getSnapshotForKeyedHistograms( + "main", + clearSubsession, + !this._testing + ); + }, + + /** + * Get a snapshot of the scalars and clear them. + * @param {subsession} If true, then we collect the data for a subsession. + * @param {clearSubsession} If true, we need to clear the subsession. + * @param {keyed} Take a snapshot of keyed or non keyed scalars. + * @return {Object} The scalar data as a Javascript object, including the + * data from child processes, in the following format: + * {'content': { 'scalarName': ... }, 'gpu': { ... } } + */ + getScalars(subsession, clearSubsession, keyed) { + if (!subsession) { + // We only support scalars for subsessions. + this._log.trace("getScalars - We only support scalars in subsessions."); + return {}; + } + + let scalarsSnapshot = keyed + ? Telemetry.getSnapshotForKeyedScalars( + "main", + clearSubsession, + !this._testing + ) + : Telemetry.getSnapshotForScalars( + "main", + clearSubsession, + !this._testing + ); + + return scalarsSnapshot; + }, + + /** + * Descriptive metadata + * + * @param reason + * The reason for the telemetry ping, this will be included in the + * returned metadata, + * @return The metadata as a JS object + */ + getMetadata: function getMetadata(reason) { + const sessionStartDate = Utils.toLocalTimeISOString( + Utils.truncateToHours(this._sessionStartDate) + ); + const subsessionStartDate = Utils.toLocalTimeISOString( + Utils.truncateToHours(this._subsessionStartDate) + ); + const monotonicNow = Policy.monotonicNow(); + + let ret = { + reason, + revision: AppConstants.SOURCE_REVISION_URL, + + // Date.getTimezoneOffset() unintuitively returns negative values if we are ahead of + // UTC and vice versa (e.g. -60 for UTC+1). We invert the sign here. + timezoneOffset: -this._subsessionStartDate.getTimezoneOffset(), + previousBuildId: this._previousBuildId, + + sessionId: this._sessionId, + subsessionId: this._subsessionId, + previousSessionId: this._previousSessionId, + previousSubsessionId: this._previousSubsessionId, + + subsessionCounter: this._subsessionCounter, + profileSubsessionCounter: this._profileSubsessionCounter, + + sessionStartDate, + subsessionStartDate, + + // Compute the session and subsession length in seconds. + // We use monotonic clocks as Date() is affected by jumping clocks (leading + // to negative lengths and other issues). + sessionLength: Math.floor(monotonicNow / 1000), + subsessionLength: Math.floor( + (monotonicNow - this._subsessionStartTimeMonotonic) / 1000 + ), + }; + + // TODO: Remove this when bug 1201837 lands. + if (this._addons) { + ret.addons = this._addons; + } + + // TODO: Remove this when bug 1201837 lands. + let flashVersion = this.getFlashVersion(); + if (flashVersion) { + ret.flashVersion = flashVersion; + } + + return ret; + }, + + /** + * Get the current session's payload using the provided + * simpleMeasurements and info, which are typically obtained by a call + * to |this.getSimpleMeasurements| and |this.getMetadata|, + * respectively. + */ + assemblePayloadWithMeasurements( + simpleMeasurements, + info, + reason, + clearSubsession + ) { + const isSubsession = IS_UNIFIED_TELEMETRY && !this._isClassicReason(reason); + clearSubsession = IS_UNIFIED_TELEMETRY && clearSubsession; + this._log.trace( + "assemblePayloadWithMeasurements - reason: " + + reason + + ", submitting subsession data: " + + isSubsession + ); + + // This allows wrapping data retrieval calls in a try-catch block so that + // failures don't break the rest of the ping assembly. + const protect = (fn, defaultReturn = null) => { + try { + return fn(); + } catch (ex) { + this._log.error( + "assemblePayloadWithMeasurements - caught exception", + ex + ); + return defaultReturn; + } + }; + + // Payload common to chrome and content processes. + let payloadObj = { + ver: PAYLOAD_VERSION, + simpleMeasurements, + }; + + // Add extended set measurements common to chrome & content processes + if (Telemetry.canRecordExtended) { + payloadObj.log = []; + } + + if (Utils.isContentProcess) { + return payloadObj; + } + + // Additional payload for chrome process. + let measurements = { + histograms: protect(() => this.getHistograms(clearSubsession), {}), + keyedHistograms: protect( + () => this.getKeyedHistograms(clearSubsession), + {} + ), + scalars: protect( + () => this.getScalars(isSubsession, clearSubsession), + {} + ), + keyedScalars: protect( + () => this.getScalars(isSubsession, clearSubsession, true), + {} + ), + }; + + let measurementsContainGPU = Object.keys(measurements).some( + key => "gpu" in measurements[key] + ); + + let measurementsContainSocket = Object.keys(measurements).some( + key => "socket" in measurements[key] + ); + + payloadObj.processes = {}; + let processTypes = ["parent", "content", "extension", "dynamic"]; + // Only include the GPU process if we've accumulated data for it. + if (measurementsContainGPU) { + processTypes.push("gpu"); + } + if (measurementsContainSocket) { + processTypes.push("socket"); + } + + // Collect per-process measurements. + for (const processType of processTypes) { + let processPayload = {}; + + for (const key in measurements) { + let payloadLoc = processPayload; + // Parent histograms are added to the top-level payload object instead of the process payload. + if ( + processType == "parent" && + (key == "histograms" || key == "keyedHistograms") + ) { + payloadLoc = payloadObj; + } + // The Dynamic process only collects scalars and keyed scalars. + if ( + processType == "dynamic" && + key !== "scalars" && + key !== "keyedScalars" + ) { + continue; + } + + // Process measurements can be empty, set a default value. + payloadLoc[key] = measurements[key][processType] || {}; + } + + // Add process measurements to payload. + payloadObj.processes[processType] = processPayload; + } + + payloadObj.info = info; + + // Add extended set measurements for chrome process. + if (Telemetry.canRecordExtended) { + payloadObj.slowSQL = protect(() => Telemetry.slowSQL); + payloadObj.fileIOReports = protect(() => Telemetry.fileIOReports); + payloadObj.lateWrites = protect(() => Telemetry.lateWrites); + + payloadObj.addonDetails = protect(() => + AddonManagerPrivate.getTelemetryDetails() + ); + + let clearUIsession = !( + reason == REASON_GATHER_PAYLOAD || + reason == REASON_GATHER_SUBSESSION_PAYLOAD + ); + + if (AppConstants.platform == "android") { + payloadObj.UIMeasurements = protect(() => + UITelemetry.getUIMeasurements(clearUIsession) + ); + } + + if ( + this._slowSQLStartup && + !!Object.keys(this._slowSQLStartup).length && + (Object.keys(this._slowSQLStartup.mainThread).length || + Object.keys(this._slowSQLStartup.otherThreads).length) + ) { + payloadObj.slowSQLStartup = this._slowSQLStartup; + } + } + + return payloadObj; + }, + + /** + * Start a new subsession. + */ + startNewSubsession() { + this._subsessionStartDate = Policy.now(); + this._subsessionStartTimeMonotonic = Policy.monotonicNow(); + this._previousSubsessionId = this._subsessionId; + this._subsessionId = Policy.generateSubsessionUUID(); + this._subsessionCounter++; + this._profileSubsessionCounter++; + }, + + getSessionPayload: function getSessionPayload(reason, clearSubsession) { + this._log.trace( + "getSessionPayload - reason: " + + reason + + ", clearSubsession: " + + clearSubsession + ); + + let payload; + try { + const isMobile = AppConstants.platform == "android"; + const isSubsession = isMobile ? false : !this._isClassicReason(reason); + + Telemetry.scalarSet( + "browser.engagement.session_time_including_suspend", + Telemetry.msSinceProcessStartIncludingSuspend() + ); + Telemetry.scalarSet( + "browser.engagement.session_time_excluding_suspend", + Telemetry.msSinceProcessStartExcludingSuspend() + ); + + if (isMobile) { + clearSubsession = false; + } + + let measurements = this.getSimpleMeasurements( + reason == REASON_SAVED_SESSION, + isSubsession, + clearSubsession + ); + let info = !Utils.isContentProcess ? this.getMetadata(reason) : null; + payload = this.assemblePayloadWithMeasurements( + measurements, + info, + reason, + clearSubsession + ); + } catch (ex) { + Telemetry.getHistogramById("TELEMETRY_ASSEMBLE_PAYLOAD_EXCEPTION").add(1); + throw ex; + } finally { + if (!Utils.isContentProcess && clearSubsession) { + this.startNewSubsession(); + // Persist session data to disk (don't wait until it completes). + let sessionData = this._getSessionDataObject(); + TelemetryStorage.saveSessionData(sessionData); + + // Notify that there was a subsession split in the parent process. This is an + // internal topic and is only meant for internal Telemetry usage. + Services.obs.notifyObservers( + null, + "internal-telemetry-after-subsession-split" + ); + } + } + + return payload; + }, + + /** + * Send data to the server. Record success/send-time in histograms + */ + send: async function send(reason) { + this._log.trace("send - Reason " + reason); + // populate histograms one last time + await Services.telemetry.gatherMemory(); + + const isSubsession = !this._isClassicReason(reason); + let payload = this.getSessionPayload(reason, isSubsession); + let options = { + addClientId: true, + addEnvironment: true, + }; + return TelemetryController.submitExternalPing( + getPingType(payload), + payload, + options + ); + }, + + /** + * Attaches the needed observers during Telemetry early init, in the + * chrome process. + */ + attachEarlyObservers() { + this.addObserver("sessionstore-windows-restored"); + if (AppConstants.platform === "android") { + this.addObserver("application-background"); + } + this.addObserver("xul-window-visible"); + + // Attach the active-ticks related observers. + this.addObserver("user-interaction-active"); + this.addObserver("user-interaction-inactive"); + }, + + /** + * Lightweight init function, called as soon as Firefox starts. + */ + earlyInit(testing) { + this._log.trace("earlyInit"); + + this._initStarted = true; + this._testing = testing; + + if (this._initialized && !testing) { + this._log.error("earlyInit - already initialized"); + return; + } + + if (!Telemetry.canRecordBase && !testing) { + this._log.config( + "earlyInit - Telemetry recording is disabled, skipping Chrome process setup." + ); + return; + } + + // Generate a unique id once per session so the server can cope with duplicate + // submissions, orphaning and other oddities. The id is shared across subsessions. + this._sessionId = Policy.generateSessionUUID(); + this.startNewSubsession(); + // startNewSubsession sets |_subsessionStartDate| to the current date/time. Use + // the very same value for |_sessionStartDate|. + this._sessionStartDate = this._subsessionStartDate; + + annotateCrashReport(this._sessionId); + + // Record old value and update build ID preference if this is the first + // run with a new build ID. + let previousBuildId = Services.prefs.getStringPref( + TelemetryUtils.Preferences.PreviousBuildID, + null + ); + let thisBuildID = Services.appinfo.appBuildID; + // If there is no previousBuildId preference, we send null to the server. + if (previousBuildId != thisBuildID) { + this._previousBuildId = previousBuildId; + Services.prefs.setStringPref( + TelemetryUtils.Preferences.PreviousBuildID, + thisBuildID + ); + } + + this.attachEarlyObservers(); + }, + + /** + * Does the "heavy" Telemetry initialization later on, so we + * don't impact startup performance. + * @return {Promise} Resolved when the initialization completes. + */ + delayedInit() { + this._log.trace("delayedInit"); + + this._delayedInitTask = (async () => { + try { + this._initialized = true; + + await this._loadSessionData(); + // Update the session data to keep track of new subsessions created before + // the initialization. + await TelemetryStorage.saveSessionData(this._getSessionDataObject()); + + this.addObserver("idle-daily"); + await Services.telemetry.gatherMemory(); + + Telemetry.asyncFetchTelemetryData(function() {}); + + if (IS_UNIFIED_TELEMETRY) { + // Check for a previously written aborted session ping. + await TelemetryController.checkAbortedSessionPing(); + + // Write the first aborted-session ping as early as possible. Just do that + // if we are not testing, since calling Telemetry.reset() will make a previous + // aborted ping a pending ping. + if (!this._testing) { + await this._saveAbortedSessionPing(); + } + + // The last change date for the environment, used to throttle environment changes. + this._lastEnvironmentChangeDate = Policy.monotonicNow(); + TelemetryEnvironment.registerChangeListener( + ENVIRONMENT_CHANGE_LISTENER, + (reason, data) => this._onEnvironmentChange(reason, data) + ); + + // Start the scheduler. + // We skip this if unified telemetry is off, so we don't + // trigger the new unified ping types. + TelemetryScheduler.init(); + } + + this._delayedInitTask = null; + } catch (e) { + this._delayedInitTask = null; + throw e; + } + })(); + + return this._delayedInitTask; + }, + + getFlashVersion: function getFlashVersion() { + if (AppConstants.MOZ_APP_NAME == "firefox") { + let host = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost); + let tags = host.getPluginTags(); + + for (let i = 0; i < tags.length; i++) { + if (tags[i].name == "Shockwave Flash") { + return tags[i].version; + } + } + } + + return null; + }, + + /** + * On Desktop: Save the "shutdown" ping to disk. + * On Android: Save the "saved-session" ping to disk. + * This needs to be called after TelemetrySend shuts down otherwise pings + * would be sent instead of getting persisted to disk. + */ + saveShutdownPings() { + this._log.trace("saveShutdownPings"); + + // We append the promises to this list and wait + // on all pings to be saved after kicking off their collection. + let p = []; + + if (IS_UNIFIED_TELEMETRY) { + let shutdownPayload = this.getSessionPayload(REASON_SHUTDOWN, false); + + // Only send the shutdown ping using the pingsender from the second + // browsing session on, to mitigate issues with "bot" profiles (see bug 1354482). + const sendOnThisSession = + Services.prefs.getBoolPref( + Utils.Preferences.ShutdownPingSenderFirstSession, + false + ) || !TelemetryReportingPolicy.isFirstRun(); + let sendWithPingsender = + Services.prefs.getBoolPref( + TelemetryUtils.Preferences.ShutdownPingSender, + false + ) && sendOnThisSession; + + let options = { + addClientId: true, + addEnvironment: true, + usePingSender: sendWithPingsender, + }; + p.push( + TelemetryController.submitExternalPing( + getPingType(shutdownPayload), + shutdownPayload, + options + ).catch(e => + this._log.error( + "saveShutdownPings - failed to submit shutdown ping", + e + ) + ) + ); + + // Send a duplicate of first-shutdown pings as a new ping type, in order to properly + // evaluate first session profiles (see bug 1390095). + const sendFirstShutdownPing = + Services.prefs.getBoolPref( + Utils.Preferences.ShutdownPingSender, + false + ) && + Services.prefs.getBoolPref( + Utils.Preferences.FirstShutdownPingEnabled, + false + ) && + TelemetryReportingPolicy.isFirstRun(); + + if (sendFirstShutdownPing) { + let options = { + addClientId: true, + addEnvironment: true, + usePingSender: true, + }; + p.push( + TelemetryController.submitExternalPing( + "first-shutdown", + shutdownPayload, + options + ).catch(e => + this._log.error( + "saveShutdownPings - failed to submit first shutdown ping", + e + ) + ) + ); + } + } + + if (AppConstants.platform == "android" && Telemetry.canRecordExtended) { + let payload = this.getSessionPayload(REASON_SAVED_SESSION, false); + + let options = { + addClientId: true, + addEnvironment: true, + }; + p.push( + TelemetryController.submitExternalPing( + getPingType(payload), + payload, + options + ).catch(e => + this._log.error( + "saveShutdownPings - failed to submit saved-session ping", + e + ) + ) + ); + } + + // Wait on pings to be saved. + return Promise.all(p); + }, + + testSavePendingPing() { + let payload = this.getSessionPayload(REASON_SAVED_SESSION, false); + let options = { + addClientId: true, + addEnvironment: true, + overwrite: true, + }; + return TelemetryController.addPendingPing( + getPingType(payload), + payload, + options + ); + }, + + /** + * Do some shutdown work that is common to all process types. + */ + uninstall() { + for (let topic of this._observedTopics) { + try { + // Tests may flip Telemetry.canRecordExtended on and off. It can be the case + // that the observer TOPIC_CYCLE_COLLECTOR_BEGIN was not added. + this.removeObserver(topic); + } catch (e) { + this._log.warn("uninstall - Failed to remove " + topic, e); + } + } + }, + + getPayload: function getPayload(reason, clearSubsession) { + this._log.trace("getPayload - clearSubsession: " + clearSubsession); + reason = reason || REASON_GATHER_PAYLOAD; + // This function returns the current Telemetry payload to the caller. + // We only gather startup info once. + if (!Object.keys(this._slowSQLStartup).length) { + this._slowSQLStartup = Telemetry.slowSQL; + } + Services.telemetry.gatherMemory(); + return this.getSessionPayload(reason, clearSubsession); + }, + + gatherStartup: function gatherStartup() { + this._log.trace("gatherStartup"); + let counters = processInfo.getCounters(); + if (counters) { + [ + this._startupIO.startupSessionRestoreReadBytes, + this._startupIO.startupSessionRestoreWriteBytes, + ] = counters; + } + this._slowSQLStartup = Telemetry.slowSQL; + }, + + setAddOns: function setAddOns(aAddOns) { + this._addons = aAddOns; + }, + + testPing: function testPing() { + return this.send(REASON_TEST_PING); + }, + + /** + * Tracks the number of "ticks" the user was active in. + */ + _onActiveTick(aUserActive) { + const needsUpdate = aUserActive && this._isUserActive; + this._isUserActive = aUserActive; + + // Don't count the first active tick after we get out of + // inactivity, because it is just the start of this active tick. + if (needsUpdate) { + this._sessionActiveTicks++; + Telemetry.scalarAdd("browser.engagement.active_ticks", 1); + } + }, + + /** + * This observer drives telemetry. + */ + observe(aSubject, aTopic, aData) { + this._log.trace("observe - " + aTopic + " notified."); + + switch (aTopic) { + case "xul-window-visible": + this.removeObserver("xul-window-visible"); + var counters = processInfo.getCounters(); + if (counters) { + [ + this._startupIO.startupWindowVisibleReadBytes, + this._startupIO.startupWindowVisibleWriteBytes, + ] = counters; + } + break; + case "sessionstore-windows-restored": + this.removeObserver("sessionstore-windows-restored"); + // Check whether debugger was attached during startup + let debugService = Cc["@mozilla.org/xpcom/debug;1"].getService( + Ci.nsIDebug2 + ); + gWasDebuggerAttached = debugService.isDebuggerAttached; + this.gatherStartup(); + break; + case "idle-daily": + // Enqueue to main-thread, otherwise components may be inited by the + // idle-daily category and miss the gather-telemetry notification. + Services.tm.dispatchToMainThread(function() { + // Notify that data should be gathered now. + // TODO: We are keeping this behaviour for now but it will be removed as soon as + // bug 1127907 lands. + Services.obs.notifyObservers(null, "gather-telemetry"); + }); + break; + + case "application-background": + if (AppConstants.platform !== "android") { + break; + } + // On Android, we can get killed without warning once we are in the background, + // but we may also submit data and/or come back into the foreground without getting + // killed. To deal with this, we save the current session data to file when we are + // put into the background. This handles the following post-backgrounding scenarios: + // 1) We are killed immediately. In this case the current session data (which we + // save to a file) will be loaded and submitted on a future run. + // 2) We submit the data while in the background, and then are killed. In this case + // the file that we saved will be deleted by the usual process in + // finishPingRequest after it is submitted. + // 3) We submit the data, and then come back into the foreground. Same as case (2). + // 4) We do not submit the data, but come back into the foreground. In this case + // we have the option of either deleting the file that we saved (since we will either + // send the live data while in the foreground, or create the file again on the next + // backgrounding), or not (in which case we will delete it on submit, or overwrite + // it on the next backgrounding). Not deleting it is faster, so that's what we do. + let payload = this.getSessionPayload(REASON_SAVED_SESSION, false); + let options = { + addClientId: true, + addEnvironment: true, + overwrite: true, + }; + TelemetryController.addPendingPing( + getPingType(payload), + payload, + options + ); + break; + case "user-interaction-active": + this._onActiveTick(true); + break; + case "user-interaction-inactive": + this._onActiveTick(false); + break; + } + return undefined; + }, + + /** + * This tells TelemetrySession to uninitialize and save any pending pings. + */ + shutdownChromeProcess() { + this._log.trace("shutdownChromeProcess"); + + let cleanup = () => { + if (IS_UNIFIED_TELEMETRY) { + TelemetryEnvironment.unregisterChangeListener( + ENVIRONMENT_CHANGE_LISTENER + ); + TelemetryScheduler.shutdown(); + } + this.uninstall(); + + let reset = () => { + this._initStarted = false; + this._initialized = false; + }; + + return (async () => { + await this.saveShutdownPings(); + + if (IS_UNIFIED_TELEMETRY) { + await TelemetryController.removeAbortedSessionPing(); + } + + reset(); + })(); + }; + + // We can be in one the following states here: + // 1) delayedInit was never called + // or it was called and + // 2) _delayedInitTask is running now. + // 3) _delayedInitTask finished running already. + + // This handles 1). + if (!this._initStarted) { + return Promise.resolve(); + } + + // This handles 3). + if (!this._delayedInitTask) { + // We already ran the delayed initialization. + return cleanup(); + } + + // This handles 2). + return this._delayedInitTask.then(cleanup); + }, + + /** + * Gather and send a daily ping. + * @return {Promise} Resolved when the ping is sent. + */ + _sendDailyPing() { + this._log.trace("_sendDailyPing"); + let payload = this.getSessionPayload(REASON_DAILY, true); + + let options = { + addClientId: true, + addEnvironment: true, + }; + + let promise = TelemetryController.submitExternalPing( + getPingType(payload), + payload, + options + ); + + // Also save the payload as an aborted session. If we delay this, aborted-session can + // lag behind for the profileSubsessionCounter and other state, complicating analysis. + if (IS_UNIFIED_TELEMETRY) { + this._saveAbortedSessionPing(payload).catch(e => + this._log.error( + "_sendDailyPing - Failed to save the aborted session ping", + e + ) + ); + } + + return promise; + }, + + /** Loads session data from the session data file. + * @return {Promise<object>} A promise which is resolved with an object when + * loading has completed, with null otherwise. + */ + async _loadSessionData() { + let data = await TelemetryStorage.loadSessionData(); + + if (!data) { + return null; + } + + if ( + !("profileSubsessionCounter" in data) || + !(typeof data.profileSubsessionCounter == "number") || + !("subsessionId" in data) || + !("sessionId" in data) + ) { + this._log.error("_loadSessionData - session data is invalid"); + Telemetry.getHistogramById("TELEMETRY_SESSIONDATA_FAILED_VALIDATION").add( + 1 + ); + return null; + } + + this._previousSessionId = data.sessionId; + this._previousSubsessionId = data.subsessionId; + // Add |_subsessionCounter| to the |_profileSubsessionCounter| to account for + // new subsession while loading still takes place. This will always be exactly + // 1 - the current subsessions. + this._profileSubsessionCounter = + data.profileSubsessionCounter + this._subsessionCounter; + // If we don't have this flag in the state file, it means that this is an old profile. + // We don't want to send the "new-profile" ping on new profile, so se this to true. + this._newProfilePingSent = + "newProfilePingSent" in data ? data.newProfilePingSent : true; + return data; + }, + + /** + * Get the session data object to serialise to disk. + */ + _getSessionDataObject() { + return { + sessionId: this._sessionId, + subsessionId: this._subsessionId, + profileSubsessionCounter: this._profileSubsessionCounter, + newProfilePingSent: this._newProfilePingSent, + }; + }, + + _onEnvironmentChange(reason, oldEnvironment) { + this._log.trace("_onEnvironmentChange", reason); + + let now = Policy.monotonicNow(); + let timeDelta = now - this._lastEnvironmentChangeDate; + if (timeDelta <= MIN_SUBSESSION_LENGTH_MS) { + this._log.trace( + `_onEnvironmentChange - throttling; last change was ${Math.round( + timeDelta / 1000 + )}s ago.` + ); + return; + } + + this._lastEnvironmentChangeDate = now; + let payload = this.getSessionPayload(REASON_ENVIRONMENT_CHANGE, true); + TelemetryScheduler.rescheduleDailyPing(payload); + + let options = { + addClientId: true, + addEnvironment: true, + overrideEnvironment: oldEnvironment, + }; + TelemetryController.submitExternalPing( + getPingType(payload), + payload, + options + ); + }, + + _isClassicReason(reason) { + const classicReasons = [ + REASON_SAVED_SESSION, + REASON_GATHER_PAYLOAD, + REASON_TEST_PING, + ]; + return classicReasons.includes(reason); + }, + + /** + * Get an object describing the current state of this module for AsyncShutdown diagnostics. + */ + _getState() { + return { + initialized: this._initialized, + initStarted: this._initStarted, + haveDelayedInitTask: !!this._delayedInitTask, + }; + }, + + /** + * Saves the aborted session ping to disk. + * @param {Object} [aProvidedPayload=null] A payload object to be used as an aborted + * session ping. The reason of this payload is changed to aborted-session. + * If not provided, a new payload is gathered. + */ + _saveAbortedSessionPing(aProvidedPayload = null) { + this._log.trace("_saveAbortedSessionPing"); + + let payload = null; + if (aProvidedPayload) { + payload = Cu.cloneInto(aProvidedPayload, myScope); + // Overwrite the original reason. + payload.info.reason = REASON_ABORTED_SESSION; + } else { + payload = this.getSessionPayload(REASON_ABORTED_SESSION, false); + } + + return TelemetryController.saveAbortedSessionPing(payload); + }, + + async markNewProfilePingSent() { + this._log.trace("markNewProfilePingSent"); + this._newProfilePingSent = true; + return TelemetryStorage.saveSessionData(this._getSessionDataObject()); + }, +}; diff --git a/toolkit/components/telemetry/pings/UninstallPing.jsm b/toolkit/components/telemetry/pings/UninstallPing.jsm new file mode 100644 index 0000000000..a69438cfc0 --- /dev/null +++ b/toolkit/components/telemetry/pings/UninstallPing.jsm @@ -0,0 +1,104 @@ +/* -*- 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"; + +ChromeUtils.import("resource://gre/modules/Services.jsm", this); + +var EXPORTED_SYMBOLS = ["UninstallPing"]; + +/** + * The Windows-only "uninstall" ping, which is saved to disk for the uninstaller to find. + * The ping is actually assembled by TelemetryControllerParent.saveUninstallPing(). + */ +var UninstallPing = { + /** + * Maximum number of other installs to count (see + * toolkit/components/telemetry/docs/data/uninstall-ping.rst for motivation) + */ + MAX_OTHER_INSTALLS: 11, + + /** + * Count other installs of this app, based on the values in the TaskBarIDs registry key. + * + */ + getOtherInstallsCount(rootKey) { + // This is somewhat more complicated than just counting the number of values on the key: + // 1) the same install can be listed in both HKCU and HKLM + // 2) the current path may not be listed + // + // The strategy is to add all paths to a Set (to deduplicate) and use that size. + + // Add the names of the values under `rootKey\subKey` to `set`, until the set has `maxCount`. + // All strings are lower cased first, as Windows paths are not case-sensitive. + function collectValues(rootKey, wowFlag, subKey, set, maxCount) { + if (set.size >= maxCount) { + return; + } + + const key = Cc["@mozilla.org/windows-registry-key;1"].createInstance( + Ci.nsIWindowsRegKey + ); + + try { + key.open(rootKey, subKey, key.ACCESS_READ | wowFlag); + } catch (_e) { + // The key may not exist, ignore. + // (nsWindowsRegKey::Open doesn't provide detailed error codes) + return; + } + const valueCount = key.valueCount; + + try { + for (let i = 0; i < valueCount && set.size < maxCount; ++i) { + set.add(key.getValueName(i).toLowerCase()); + } + } finally { + key.close(); + } + } + + const subKeyName = `Software\\Mozilla\\${Services.appinfo.name}\\TaskBarIDs`; + + const paths = new Set(); + + // The current install path may not have a value in TaskBarIDs. It is simpler to + // pre-add it and always include it in the total, than to check for it everywhere and + // sometimes include it. + paths.add(Services.dirsvc.get("GreBinD", Ci.nsIFile).path.toLowerCase()); + + const initialPathsCount = paths.size; + const maxPathsCount = initialPathsCount + this.MAX_OTHER_INSTALLS; + + // First collect from HKLM for both 32-bit and 64-bit installs regardless of the architecture + // of the current application. + for (const wowFlag of [ + Ci.nsIWindowsRegKey.WOW64_32, + Ci.nsIWindowsRegKey.WOW64_64, + ]) { + collectValues( + Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE, + wowFlag, + subKeyName, + paths, + maxPathsCount + ); + } + + // Then collect from HKCU. + // HKCU\Software is shared between 32 and 64 so nothing special is needed for WOW64, + // ref https://docs.microsoft.com/en-us/windows/win32/winprog64/shared-registry-keys + collectValues( + Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, + 0 /* wowFlag */, + subKeyName, + paths, + maxPathsCount + ); + + // Subtract our pre-added path, which is not an "other" install. + return paths.size - initialPathsCount; + }, +}; diff --git a/toolkit/components/telemetry/pings/UntrustedModulesPing.jsm b/toolkit/components/telemetry/pings/UntrustedModulesPing.jsm new file mode 100644 index 0000000000..a59a161e2a --- /dev/null +++ b/toolkit/components/telemetry/pings/UntrustedModulesPing.jsm @@ -0,0 +1,76 @@ +/* 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 periodically sends a Telemetry ping containing information + * about untrusted module loads on Windows. + * + * https://firefox-source-docs.mozilla.org/toolkit/components/telemetry/telemetry/data/third-party-modules-ping.html + */ + +"use strict"; + +var EXPORTED_SYMBOLS = ["TelemetryUntrustedModulesPing"]; + +ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm", this); + +XPCOMUtils.defineLazyModuleGetters(this, { + Log: "resource://gre/modules/Log.jsm", + Services: "resource://gre/modules/Services.jsm", + TelemetryController: "resource://gre/modules/TelemetryController.jsm", + TelemetryUtils: "resource://gre/modules/TelemetryUtils.jsm", +}); + +XPCOMUtils.defineLazyServiceGetters(this, { + Telemetry: ["@mozilla.org/base/telemetry;1", "nsITelemetry"], + UpdateTimerManager: [ + "@mozilla.org/updates/timer-manager;1", + "nsIUpdateTimerManager", + ], +}); + +const DEFAULT_INTERVAL_SECONDS = 24 * 60 * 60; // 1 day + +const LOGGER_NAME = "Toolkit.Telemetry"; +const LOGGER_PREFIX = "TelemetryUntrustedModulesPing::"; +const TIMER_NAME = "telemetry_untrustedmodules_ping"; +const PING_SUBMISSION_NAME = "third-party-modules"; + +var TelemetryUntrustedModulesPing = Object.freeze({ + _log: Log.repository.getLoggerWithMessagePrefix(LOGGER_NAME, LOGGER_PREFIX), + + start() { + UpdateTimerManager.registerTimer( + TIMER_NAME, + this, + Services.prefs.getIntPref( + TelemetryUtils.Preferences.UntrustedModulesPingFrequency, + DEFAULT_INTERVAL_SECONDS + ) + ); + }, + + notify() { + try { + Telemetry.getUntrustedModuleLoadEvents().then(payload => { + try { + if (payload) { + TelemetryController.submitExternalPing( + PING_SUBMISSION_NAME, + payload, + { + addClientId: true, + addEnvironment: true, + } + ); + } + } catch (ex) { + this._log.error("payload handler caught an exception", ex); + } + }); + } catch (ex) { + this._log.error("notify() caught an exception", ex); + } + }, +}); diff --git a/toolkit/components/telemetry/pings/UpdatePing.jsm b/toolkit/components/telemetry/pings/UpdatePing.jsm new file mode 100644 index 0000000000..5cd4145bc4 --- /dev/null +++ b/toolkit/components/telemetry/pings/UpdatePing.jsm @@ -0,0 +1,185 @@ +/* -*- 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"; + +ChromeUtils.import("resource://gre/modules/Log.jsm", this); +ChromeUtils.import("resource://gre/modules/Services.jsm", this); +ChromeUtils.import("resource://gre/modules/TelemetryUtils.jsm", this); + +ChromeUtils.defineModuleGetter( + this, + "TelemetryController", + "resource://gre/modules/TelemetryController.jsm" +); + +const LOGGER_NAME = "Toolkit.Telemetry"; +const PING_TYPE = "update"; +const UPDATE_DOWNLOADED_TOPIC = "update-downloaded"; +const UPDATE_STAGED_TOPIC = "update-staged"; + +var EXPORTED_SYMBOLS = ["UpdatePing"]; + +/** + * This module is responsible for listening to all the relevant update + * signals, gathering the needed information and assembling the "update" + * ping. + */ +var UpdatePing = { + _enabled: false, + + earlyInit() { + this._log = Log.repository.getLoggerWithMessagePrefix( + LOGGER_NAME, + "UpdatePing::" + ); + this._enabled = Services.prefs.getBoolPref( + TelemetryUtils.Preferences.UpdatePing, + false + ); + + this._log.trace("init - enabled: " + this._enabled); + + if (!this._enabled) { + return; + } + + Services.obs.addObserver(this, UPDATE_DOWNLOADED_TOPIC); + Services.obs.addObserver(this, UPDATE_STAGED_TOPIC); + }, + + /** + * Get the information about the update we're going to apply/was just applied + * from the update manager. + * + * @return {nsIUpdate} The information about the update, if available, or null. + */ + _getActiveUpdate() { + let updateManager = Cc["@mozilla.org/updates/update-manager;1"].getService( + Ci.nsIUpdateManager + ); + if (!updateManager || !updateManager.readyUpdate) { + return null; + } + + return updateManager.readyUpdate; + }, + + /** + * Generate an "update" ping with reason "success" and dispatch it + * to the Telemetry system. + * + * @param {String} aPreviousVersion The browser version we updated from. + * @param {String} aPreviousBuildId The browser build id we updated from. + */ + handleUpdateSuccess(aPreviousVersion, aPreviousBuildId) { + if (!this._enabled) { + return; + } + + this._log.trace("handleUpdateSuccess"); + + // An update could potentially change the update channel. Moreover, + // updates can only be applied if the update's channel matches with the build channel. + // There's no way to pass this information from the caller nor the environment as, + // in that case, the environment would report the "new" channel. However, the + // update manager should still have information about the active update: given the + // previous assumptions, we can simply get the channel from the update and assume + // it matches with the state previous to the update. + let update = this._getActiveUpdate(); + + const payload = { + reason: "success", + previousChannel: update ? update.channel : null, + previousVersion: aPreviousVersion, + previousBuildId: aPreviousBuildId, + }; + + const options = { + addClientId: true, + addEnvironment: true, + usePingSender: false, + }; + + TelemetryController.submitExternalPing( + PING_TYPE, + payload, + options + ).catch(e => + this._log.error("handleUpdateSuccess - failed to submit update ping", e) + ); + }, + + /** + * Generate an "update" ping with reason "ready" and dispatch it + * to the Telemetry system. + * + * @param {String} aUpdateState The state of the downloaded patch. See + * nsIUpdateService.idl for a list of possible values. + */ + _handleUpdateReady(aUpdateState) { + const ALLOWED_STATES = [ + "applied", + "applied-service", + "pending", + "pending-service", + "pending-elevate", + ]; + if (!ALLOWED_STATES.includes(aUpdateState)) { + this._log.trace("Unexpected update state: " + aUpdateState); + return; + } + + // Get the information about the update we're going to apply from the + // update manager. + let update = this._getActiveUpdate(); + if (!update) { + this._log.trace( + "Cannot get the update manager or no update is currently active." + ); + return; + } + + const payload = { + reason: "ready", + targetChannel: update.channel, + targetVersion: update.appVersion, + targetBuildId: update.buildID, + targetDisplayVersion: update.displayVersion, + }; + + const options = { + addClientId: true, + addEnvironment: true, + usePingSender: true, + }; + + TelemetryController.submitExternalPing( + PING_TYPE, + payload, + options + ).catch(e => + this._log.error("_handleUpdateReady - failed to submit update ping", e) + ); + }, + + /** + * The notifications handler. + */ + observe(aSubject, aTopic, aData) { + this._log.trace("observe - aTopic: " + aTopic); + if (aTopic == UPDATE_DOWNLOADED_TOPIC || aTopic == UPDATE_STAGED_TOPIC) { + this._handleUpdateReady(aData); + } + }, + + shutdown() { + if (!this._enabled) { + return; + } + Services.obs.removeObserver(this, UPDATE_DOWNLOADED_TOPIC); + Services.obs.removeObserver(this, UPDATE_STAGED_TOPIC); + }, +}; |