summaryrefslogtreecommitdiffstats
path: root/toolkit/components/telemetry/pings/EventPing.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/telemetry/pings/EventPing.jsm')
-rw-r--r--toolkit/components/telemetry/pings/EventPing.jsm259
1 files changed, 259 insertions, 0 deletions
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;
+ },
+};