/* -*- 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/. */ import { Log } from "resource://gre/modules/Log.sys.mjs"; import { TelemetryUtils } from "resource://gre/modules/TelemetryUtils.sys.mjs"; import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { AddonManagerPrivate: "resource://gre/modules/AddonManager.sys.mjs", TelemetryController: "resource://gre/modules/TelemetryController.sys.mjs", TelemetryEnvironment: "resource://gre/modules/TelemetryEnvironment.sys.mjs", TelemetryReportingPolicy: "resource://gre/modules/TelemetryReportingPolicy.sys.mjs", TelemetryScheduler: "resource://gre/modules/TelemetryScheduler.sys.mjs", TelemetryStorage: "resource://gre/modules/TelemetryStorage.sys.mjs", }); const Utils = TelemetryUtils; // 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_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; function generateUUID() { let str = Services.uuid.generateUUID().toString(); // strip {} return str.substring(1, str.length - 1); } /** * This is a policy object used to override behavior for testing. */ export 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 { Services.appinfo.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.importESModule( "resource://gre/modules/ctypes.sys.mjs" ); 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)]; }, }; export 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(); }, testOnEnvironmentChange(...args) { return Impl._onEnvironmentChange(...args); }, }); 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 { TelemetryTimestamps } = ChromeUtils.importESModule( "resource://gre/modules/TelemetryTimestamps.sys.mjs" ); appTimestamps = TelemetryTimestamps.get(); } catch (ex) {} // Only submit this if the extended set is enabled. if (!Utils.isContentProcess && Services.telemetry.canRecordExtended) { try { ret.addonManager = lazy.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); 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 = Services.telemetry.lastShutdownDuration; if (shutdownDuration) { ret.shutdownDuration = shutdownDuration; } let failedProfileLockCount = Services.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 Services.telemetry.getSnapshotForHistograms( "main", clearSubsession, !this._testing ); }, getKeyedHistograms(clearSubsession) { return Services.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 ? Services.telemetry.getSnapshotForKeyedScalars( "main", clearSubsession, !this._testing ) : Services.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; } 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 (Services.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] ); let measurementsContainUtility = Object.keys(measurements).some( key => "utility" 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"); } if (measurementsContainUtility) { processTypes.push("utility"); } // 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 (Services.telemetry.canRecordExtended) { payloadObj.slowSQL = protect(() => Services.telemetry.slowSQL); payloadObj.fileIOReports = protect( () => Services.telemetry.fileIOReports ); payloadObj.lateWrites = protect(() => Services.telemetry.lateWrites); payloadObj.addonDetails = protect(() => lazy.AddonManagerPrivate.getTelemetryDetails() ); 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); // The order of the next two msSinceProcessStart* calls is somewhat // important. In theory, `session_time_including_suspend` is supposed to // ALWAYS be lower or equal than `session_time_excluding_suspend` (because // the former is a temporal superset of the latter). When a device has not // been suspended since boot, we want the previous property to hold, // regardless of the delay during or between the two // `msSinceProcessStart*` calls. Services.telemetry.scalarSet( "browser.engagement.session_time_excluding_suspend", Services.telemetry.msSinceProcessStartExcludingSuspend() ); Services.telemetry.scalarSet( "browser.engagement.session_time_including_suspend", Services.telemetry.msSinceProcessStartIncludingSuspend() ); 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) { Services.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(); lazy.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 lazy.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 (!Services.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 lazy.TelemetryStorage.saveSessionData( this._getSessionDataObject() ); this.addObserver("idle-daily"); await Services.telemetry.gatherMemory(); Services.telemetry.asyncFetchTelemetryData(function () {}); if (IS_UNIFIED_TELEMETRY) { // Check for a previously written aborted session ping. await lazy.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(); lazy.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. lazy.TelemetryScheduler.init(); } this._delayedInitTask = null; } catch (e) { this._delayedInitTask = null; throw e; } })(); return this._delayedInitTask; }, /** * 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 ) || !lazy.TelemetryReportingPolicy.isFirstRun(); let sendWithPingsender = Services.prefs.getBoolPref( TelemetryUtils.Preferences.ShutdownPingSender, false ) && sendOnThisSession; let options = { addClientId: true, addEnvironment: true, usePingSender: sendWithPingsender, }; p.push( lazy.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 ) && lazy.TelemetryReportingPolicy.isFirstRun(); if (sendFirstShutdownPing) { let options = { addClientId: true, addEnvironment: true, usePingSender: true, }; p.push( lazy.TelemetryController.submitExternalPing( "first-shutdown", shutdownPayload, options ).catch(e => this._log.error( "saveShutdownPings - failed to submit first shutdown ping", e ) ) ); } } if ( AppConstants.platform == "android" && Services.telemetry.canRecordExtended ) { let payload = this.getSessionPayload(REASON_SAVED_SESSION, false); let options = { addClientId: true, addEnvironment: true, }; p.push( lazy.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 lazy.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 = Services.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 = Services.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++; Services.telemetry.scalarAdd("browser.engagement.active_ticks", 1); Glean.browserEngagement.activeTicks.add(1); } }, /** * This observer drives telemetry. */ observe(aSubject, aTopic) { 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, }; lazy.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) { lazy.TelemetryEnvironment.unregisterChangeListener( ENVIRONMENT_CHANGE_LISTENER ); lazy.TelemetryScheduler.shutdown(); } this.uninstall(); let reset = () => { this._initStarted = false; this._initialized = false; }; return (async () => { await this.saveShutdownPings(); if (IS_UNIFIED_TELEMETRY) { await lazy.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 = lazy.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} A promise which is resolved with an object when * loading has completed, with null otherwise. */ async _loadSessionData() { let data = await lazy.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"); Services.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); lazy.TelemetryScheduler.rescheduleDailyPing(payload); let options = { addClientId: true, addEnvironment: true, overrideEnvironment: oldEnvironment, }; lazy.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, {}); // Overwrite the original reason. payload.info.reason = REASON_ABORTED_SESSION; } else { payload = this.getSessionPayload(REASON_ABORTED_SESSION, false); } return lazy.TelemetryController.saveAbortedSessionPing(payload); }, async markNewProfilePingSent() { this._log.trace("markNewProfilePingSent"); this._newProfilePingSent = true; return lazy.TelemetryStorage.saveSessionData(this._getSessionDataObject()); }, };