Source code
Revision control
Copy as Markdown
Other Tools
/* 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
/*
* This module collects data on send failures and other critical issues with Telemetry submissions.
*/
import { TelemetryUtils } from "resource://gre/modules/TelemetryUtils.sys.mjs";
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
Log: "resource://gre/modules/Log.sys.mjs",
TelemetryController: "resource://gre/modules/TelemetryController.sys.mjs",
clearTimeout: "resource://gre/modules/Timer.sys.mjs",
setTimeout: "resource://gre/modules/Timer.sys.mjs",
});
const Utils = TelemetryUtils;
const MS_IN_A_MINUTE = 60 * 1000;
// 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::";
export var Policy = {
setSchedulerTickTimeout: (callback, delayMs) =>
lazy.setTimeout(callback, delayMs),
clearSchedulerTickTimeout: id => lazy.clearTimeout(id),
};
export 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 (!TelemetryHealthPing.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(
lazy.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 = lazy.Log.repository.getLoggerWithMessagePrefix(
LOGGER_NAME,
LOGGER_PREFIX + "::"
);
}
return this._logger;
},
};
XPCOMUtils.defineLazyPreferenceGetter(
TelemetryHealthPing,
"enabled",
TelemetryUtils.Preferences.HealthPingEnabled,
true
);