Bug 1133536 - Detect & report aborted sessions in Telemetry. r=vladan

This commit is contained in:
Alessio Placitelli 2015-03-24 14:43:20 +01:00
parent 2e3be5fc6b
commit 0b6208db94
3 changed files with 525 additions and 136 deletions

View File

@ -123,6 +123,28 @@ this.TelemetryFile = {
return Promise.all(p);
},
/**
* Add a ping to the saved pings directory so that it gets along with other pings. Note
* that the original ping file will not be modified.
*
* @param {String} aFilePath The path to the ping file that needs to be added to the
* saved pings directory.
* @return {Promise} A promise resolved when the ping is saved to the pings directory.
*/
addPendingPing: function(aPingPath) {
// Pings in the saved ping directory need to have the ping id or slug (old format) as
// the file name. We load the ping content, check that it is valid, and use it to save
// the ping file with the correct file name.
return loadPingFile(aPingPath).then(ping => {
// Append the ping to the pending list.
pendingPings.push(ping);
// Since we read a ping successfully, update the related histogram.
Telemetry.getHistogramById("READ_SAVED_PING_SUCCESS").add(1);
// Save the ping to the saved pings directory.
return this.savePing(ping, false);
});
},
/**
* Remove the file for a ping
*
@ -277,29 +299,37 @@ function getPingDirectory() {
});
}
/**
* Loads a ping file.
* @param {String} aFilePath The path of the ping file.
* @return {Promise<Object>} A promise resolved with the ping content or rejected if the
* ping contains invalid data.
*/
let loadPingFile = Task.async(function* (aFilePath) {
let array = yield OS.File.read(aFilePath);
let decoder = new TextDecoder();
let string = decoder.decode(array);
let ping = JSON.parse(string);
// The ping's payload used to be stringified JSON. Deal with that.
if (typeof(ping.payload) == "string") {
ping.payload = JSON.parse(ping.payload);
}
return ping;
});
function addToPendingPings(file) {
function onLoad(success) {
let success_histogram = Telemetry.getHistogramById("READ_SAVED_PING_SUCCESS");
success_histogram.add(success);
}
return Task.spawn(function*() {
try {
let array = yield OS.File.read(file);
let decoder = new TextDecoder();
let string = decoder.decode(array);
let ping = JSON.parse(string);
// The ping's payload used to be stringified JSON. Deal with that.
if (typeof(ping.payload) == "string") {
ping.payload = JSON.parse(ping.payload);
}
return loadPingFile(file).then(ping => {
pendingPings.push(ping);
onLoad(true);
} catch (e) {
},
() => {
onLoad(false);
yield OS.File.remove(file);
}
});
return OS.File.remove(file);
});
}

View File

@ -14,6 +14,7 @@ Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://gre/modules/debug.js", this);
Cu.import("resource://gre/modules/Services.jsm", this);
Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
Cu.import("resource://gre/modules/osfile.jsm", this);
Cu.import("resource://gre/modules/Promise.jsm", this);
Cu.import("resource://gre/modules/DeferredTask.jsm", this);
Cu.import("resource://gre/modules/Preferences.jsm");
@ -145,6 +146,19 @@ this.TelemetryPing = Object.freeze({
return Impl.setServer(aServer);
},
/**
* Adds a ping to the pending ping list by moving it to the saved pings directory
* and adding it to the pending ping list.
*
* @param {String} aPingPath The path of the ping to add to the pending ping list.
* @param {Boolean} [aRemoveOriginal] If true, deletes the ping at aPingPath after adding
* it to the saved pings directory.
* @return {Promise} Resolved when the ping is correctly moved to the saved pings directory.
*/
addPendingPing: function(aPingPath, aRemoveOriginal) {
return Impl.addPendingPing(aPingPath, aRemoveOriginal);
},
/**
* Send payloads to the server.
*
@ -205,8 +219,11 @@ this.TelemetryPing = Object.freeze({
* environment data.
* @param {Boolean} [aOptions.overwrite=false] true overwrites a ping with the same name,
* if found.
* @param {String} [aOptions.filePath] The path to save the ping to. Will save to default
* ping location if not provided.
*
* @returns {Promise} A promise that resolves when the ping is saved to disk.
* @returns {Promise<Integer>} A promise that resolves with the ping id when the ping is
* saved to disk.
*/
savePing: function(aType, aPayload, aOptions = {}) {
let options = aOptions;
@ -218,36 +235,6 @@ this.TelemetryPing = Object.freeze({
return Impl.savePing(aType, aPayload, options);
},
/**
* Only used for testing. Saves a ping to disk and return the ping id once done.
*
* @param {String} aType The type of the ping.
* @param {Object} aPayload The actual data payload for the ping.
* @param {Object} [aOptions] Options object.
* @param {Number} [aOptions.retentionDays=14] The number of days to keep the ping on disk
* if sending fails.
* @param {Boolean} [aOptions.addClientId=false] true if the ping should contain the client
* id, false otherwise.
* @param {Boolean} [aOptions.addEnvironment=false] true if the ping should contain the
* environment data.
* @param {Boolean} [aOptions.overwrite=false] true overwrites a ping with the same name,
* if found.
* @param {String} [aOptions.filePath] The path to save the ping to. Will save to default
* ping location if not provided.
*
* @returns {Promise<Integer>} A promise that resolves with the ping id when the ping is
* saved to disk.
*/
testSavePingToFile: function(aType, aPayload, aOptions = {}) {
let options = aOptions;
options.retentionDays = aOptions.retentionDays || DEFAULT_RETENTION_DAYS;
options.addClientId = aOptions.addClientId || false;
options.addEnvironment = aOptions.addEnvironment || false;
options.overwrite = aOptions.overwrite || false;
return Impl.testSavePingToFile(aType, aPayload, options);
},
/**
* The client id send with the telemetry ping.
*
@ -377,6 +364,23 @@ let Impl = {
this._server = aServer;
},
/**
* Adds a ping to the pending ping list by moving it to the saved pings directory
* and adding it to the pending ping list.
*
* @param {String} aPingPath The path of the ping to add to the pending ping list.
* @param {Boolean} [aRemoveOriginal] If true, deletes the ping at aPingPath after adding
* it to the saved pings directory.
* @return {Promise} Resolved when the ping is correctly moved to the saved pings directory.
*/
addPendingPing: function(aPingPath, aRemoveOriginal) {
return TelemetryFile.addPendingPing(aPingPath).then(() => {
if (aRemoveOriginal) {
return OS.File.remove(aPingPath);
}
}, error => this._log.error("addPendingPing - Unable to add the pending ping", error));
},
/**
* Build a complete ping and send data to the server. Record success/send-time in
* histograms.
@ -458,50 +462,26 @@ let Impl = {
* @param {Boolean} aOptions.addEnvironment true if the ping should contain the
* environment data.
* @param {Boolean} aOptions.overwrite true overwrites a ping with the same name, if found.
*
* @returns {Promise} A promise that resolves when the ping is saved to disk.
*/
savePing: function savePing(aType, aPayload, aOptions) {
this._log.trace("savePing - Type " + aType + ", Server " + this._server +
", aOptions " + JSON.stringify(aOptions));
return this.assemblePing(aType, aPayload, aOptions)
.then(pingData => TelemetryFile.savePing(pingData, aOptions.overwrite),
error => this._log.error("savePing - Rejection", error));
},
/**
* Save a ping to disk and return the ping id when done.
*
* @param {String} aType The type of the ping.
* @param {Object} aPayload The actual data payload for the ping.
* @param {Object} aOptions Options object.
* @param {Number} aOptions.retentionDays The number of days to keep the ping on disk
* if sending fails.
* @param {Boolean} aOptions.addClientId true if the ping should contain the client id,
* false otherwise.
* @param {Boolean} aOptions.addEnvironment true if the ping should contain the
* environment data.
* @param {Boolean} aOptions.overwrite true overwrites a ping with the same name, if found.
* @param {String} [aOptions.filePath] The path to save the ping to. Will save to default
* ping location if not provided.
*
* @returns {Promise} A promise that resolves with the ping id when the ping is saved to
* disk.
*/
testSavePingToFile: function testSavePingToFile(aType, aPayload, aOptions) {
this._log.trace("testSavePingToFile - Type " + aType + ", Server " + this._server +
savePing: function savePing(aType, aPayload, aOptions) {
this._log.trace("savePing - Type " + aType + ", Server " + this._server +
", aOptions " + JSON.stringify(aOptions));
return this.assemblePing(aType, aPayload, aOptions)
.then(pingData => {
if (aOptions.filePath) {
return TelemetryFile.savePingToFile(pingData, aOptions.filePath, aOptions.overwrite)
.then(() => { return pingData.id; });
} else {
return TelemetryFile.savePing(pingData, aOptions.overwrite)
.then(() => { return pingData.id; });
}
}, error => this._log.error("testSavePing - Rejection", error));
.then(pingData => {
if ("filePath" in aOptions) {
return TelemetryFile.savePingToFile(pingData, aOptions.filePath, aOptions.overwrite)
.then(() => { return pingData.id; });
} else {
return TelemetryFile.savePing(pingData, aOptions.overwrite)
.then(() => { return pingData.id; });
}
}, error => this._log.error("savePing - Rejection", error));
},
finishPingRequest: function finishPingRequest(success, startTime, ping, isPersisted) {

View File

@ -21,6 +21,8 @@ Cu.import("resource://gre/modules/Preferences.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/Timer.jsm");
const myScope = this;
const IS_CONTENT_PROCESS = (function() {
// We cannot use Services.appinfo here because in telemetry xpcshell tests,
// appinfo is initially unavailable, and becomes available only later on.
@ -34,6 +36,7 @@ const PING_TYPE_MAIN = "main";
const PING_TYPE_SAVED_SESSION = "saved-session";
const RETENTION_DAYS = 14;
const REASON_ABORTED_SESSION = "aborted-session";
const REASON_DAILY = "daily";
const REASON_SAVED_SESSION = "saved-session";
const REASON_IDLE_DAILY = "idle-daily";
@ -67,6 +70,9 @@ const PREF_ASYNC_PLUGIN_INIT = "dom.ipc.plugins.asyncInit";
const MESSAGE_TELEMETRY_PAYLOAD = "Telemetry:Payload";
const MESSAGE_TELEMETRY_GET_CHILD_PAYLOAD = "Telemetry:GetChildPayload";
const DATAREPORTING_DIRECTORY = "datareporting";
const ABORTED_SESSION_FILE_NAME = "aborted-session-ping";
const SESSION_STATE_FILE_NAME = "session-state.json";
// Maximum number of content payloads that we are willing to store.
@ -78,12 +84,27 @@ const TELEMETRY_INTERVAL = 60000;
const TELEMETRY_DELAY = 60000;
// Delay before initializing telemetry if we're testing (ms)
const TELEMETRY_TEST_DELAY = 100;
// Execute a scheduler tick every 5 minutes.
const SCHEDULER_TICK_INTERVAL_MS = 5 * 60 * 1000;
// The maximum number of times a scheduled operation can fail.
const SCHEDULER_RETRY_ATTEMPTS = 3;
// The tolerance we have when checking if it's midnight (15 minutes).
const SCHEDULER_MIDNIGHT_TOLERANCE_MS = 15 * 60 * 1000;
// Coalesce the daily and aborted-session pings if they are both due within
// two minutes from each other.
const SCHEDULER_COALESCE_THRESHOLD_MS = 2 * 60 * 1000;
// Seconds of idle time before pinging.
// On idle-daily a gather-telemetry notification is fired, during it probes can
// start asynchronous tasks to gather data. On the next idle the data is sent.
const IDLE_TIMEOUT_SECONDS = 5 * 60;
// The frequency at which we persist session data to the disk to prevent data loss
// in case of aborted sessions (currently 5 minutes).
const ABORTED_SESSION_UPDATE_INTERVAL_MS = 5 * 60 * 1000;
var gLastMemoryPoll = null;
let gWasDebuggerAttached = false;
@ -147,8 +168,8 @@ let Policy = {
now: () => new Date(),
generateSessionUUID: () => generateUUID(),
generateSubsessionUUID: () => generateUUID(),
setDailyTimeout: (callback, delayMs) => setTimeout(callback, delayMs),
clearDailyTimeout: (id) => clearTimeout(id),
setSchedulerTickTimeout: (callback, delayMs) => setTimeout(callback, delayMs),
clearSchedulerTickTimeout: id => clearTimeout(id),
};
/**
@ -161,6 +182,38 @@ function truncateToDays(date) {
0, 0, 0, 0);
}
/**
* Check if the difference between the times is within the provided tolerance.
* @param {Number} t1 A time in milliseconds.
* @param {Number} t2 A time in milliseconds.
* @param {Number} tolerance The tolerance, in milliseconds.
* @return {Boolean} True if the absolute time difference is within the tolerance, false
* otherwise.
*/
function areTimesClose(t1, t2, tolerance) {
return Math.abs(t1 - t2) <= tolerance;
}
/**
* Get the midnight which is closer to the provided date.
* @param {Object} date The date object to check.
* @return {Object} The Date object representing the closes midnight, or null if midnight
* is not within the midnight tolerance.
*/
function getNearestMidnight(date) {
let lastMidnight = truncateToDays(date);
if (areTimesClose(date.getTime(), lastMidnight.getTime(), SCHEDULER_MIDNIGHT_TOLERANCE_MS)) {
return lastMidnight;
}
let nextMidnightDate = new Date(lastMidnight);
nextMidnightDate.setDate(nextMidnightDate.getDate() + 1);
if (areTimesClose(date.getTime(), nextMidnightDate.getTime(), SCHEDULER_MIDNIGHT_TOLERANCE_MS)) {
return nextMidnightDate;
}
return null;
}
/**
* Get the ping type based on the payload.
* @param {Object} aPayload The ping payload.
@ -258,11 +311,13 @@ let processInfo = {
* We are using this to synchronize saving to the file that TelemetrySession persists
* its state in.
*/
let gStateSaveSerializer = {
_queuedOperations: [],
_queuedInProgress: false,
_log: Log.repository.getLoggerWithMessagePrefix(LOGGER_NAME, LOGGER_PREFIX),
function SaveSerializer() {
this._queuedOperations = [];
this._queuedInProgress = false;
this._log = Log.repository.getLoggerWithMessagePrefix(LOGGER_NAME, LOGGER_PREFIX);
}
SaveSerializer.prototype = {
/**
* Enqueues an operation to a list to serialise their execution in order to prevent race
* conditions. Useful to serialise access to disk.
@ -340,6 +395,265 @@ let gStateSaveSerializer = {
},
};
/**
* TelemetryScheduler contains a single timer driving all regularly-scheduled
* Telemetry related jobs. Having a single place with this logic simplifies
* reasoning about scheduling actions in a single place, making it easier to
* coordinate jobs and coalesce them.
*/
let TelemetryScheduler = {
_lastDailyPingTime: 0,
_lastSessionCheckpointTime: 0,
// For sanity checking.
_lastAdhocPingTime: 0,
_lastTickTime: 0,
_log: null,
// The number of times a daily ping fails.
_dailyPingRetryAttempts: 0,
// The timer which drives the scheduler.
_schedulerTimer: null,
_shuttingDown: true,
/**
* Initialises the scheduler and schedules the first daily/aborted session pings.
*/
init: function() {
this._log = Log.repository.getLoggerWithMessagePrefix(LOGGER_NAME, "TelemetryScheduler::");
this._log.trace("init");
this._shuttingDown = false;
// Initialize the last daily ping and aborted session last due times to the current time.
// Otherwise, we might end up sending daily pings even if the subsession is not long enough.
let now = Policy.now();
this._lastDailyPingTime = now.getTime();
this._lastSessionCheckpointTime = now.getTime();
this._rescheduleTimeout();
},
/**
* Reschedules the tick timer.
*/
_rescheduleTimeout: function() {
this._log.trace("_rescheduleTimeout");
if (this._shuttingDown) {
this._log.warn("_rescheduleTimeout - already shutdown");
return;
}
if (this._schedulerTimer) {
Policy.clearSchedulerTickTimeout(this._schedulerTimer);
}
this._schedulerTimer =
Policy.setSchedulerTickTimeout(() => this._onSchedulerTick(), SCHEDULER_TICK_INTERVAL_MS);
},
/**
* Checks if we can send a daily ping or not.
* @param {Object} nowDate A date object.
* @return {Boolean} True if we can send the daily ping, false otherwise.
*/
_isDailyPingDue: function(nowDate) {
let nearestMidnight = getNearestMidnight(nowDate);
if (nearestMidnight) {
let subsessionLength = Math.abs(nowDate.getTime() - this._lastDailyPingTime);
if (subsessionLength < MIN_SUBSESSION_LENGTH_MS) {
// Generating a daily ping now would create a very short subsession.
return false;
} else if (areTimesClose(this._lastDailyPingTime, nearestMidnight.getTime(),
SCHEDULER_MIDNIGHT_TOLERANCE_MS)) {
// We've already sent a ping for this midnight.
return false;
}
return true;
}
let lastDailyPingDate = truncateToDays(new Date(this._lastDailyPingTime));
// This is today's date and also the previous midnight (0:00).
let todayDate = truncateToDays(nowDate);
// Check that _lastDailyPingTime isn't today nor within SCHEDULER_MIDNIGHT_TOLERANCE_MS of the
// *previous* midnight.
if ((lastDailyPingDate.getTime() != todayDate.getTime()) &&
!areTimesClose(this._lastDailyPingTime, todayDate.getTime(), SCHEDULER_MIDNIGHT_TOLERANCE_MS)) {
// Computer must have gone to sleep, the daily ping is overdue.
return true;
}
return false;
},
/**
* An helper function to save an aborted-session ping.
* @param {Number} now The current time, in milliseconds.
* @param {Object} [competingPayload=null] If we are coalescing the daily and the
* aborted-session pings, this is the payload for the former. Note
* that the reason field of this payload will be changed.
* @return {Promise} A promise resolved when the ping is saved.
*/
_saveAbortedPing: function(now, competingPayload=null) {
this._lastSessionCheckpointTime = now;
return Impl._saveAbortedSessionPing(competingPayload)
.catch(e => this._log.error("_saveAbortedPing - Failed", e));
},
/**
* Performs a scheduler tick. This function manages Telemetry recurring operations.
* @return {Promise} A promise, only used when testing, resolved when the scheduled
* operation completes.
*/
_onSchedulerTick: function() {
if (this._shuttingDown) {
this._log.warn("_onSchedulerTick - already shutdown.");
return;
}
let promise = Promise.resolve();
try {
promise = this._schedulerTickLogic();
} catch (e) {
this._log.error("_onSchedulerTick - There was an exception", e);
} finally {
this._rescheduleTimeout();
}
// This promise is returned to make testing easier.
return promise;
},
/**
* Implements the scheduler logic.
* @return {Promise} Resolved when the scheduled task completes. Only used in tests.
*/
_schedulerTickLogic: function() {
this._log.trace("_schedulerTickLogic");
let nowDate = Policy.now();
let now = nowDate.getTime();
if (now - this._lastTickTime > 1.1 * SCHEDULER_TICK_INTERVAL_MS) {
this._log.trace("_schedulerTickLogic - First scheduler tick after sleep or startup.");
}
this._lastTickTime = now;
// Check if aborted-session ping is due.
let isAbortedPingDue =
(now - this._lastSessionCheckpointTime) >= ABORTED_SESSION_UPDATE_INTERVAL_MS;
// Check if daily ping is due.
let shouldSendDaily = this._isDailyPingDue(nowDate);
// We can combine the daily-ping and the aborted-session ping in the following cases:
// - If both the daily and the aborted session pings are due (a laptop that wakes
// up after a few hours).
// - If either the daily ping is due and the other one would follow up shortly
// (whithin the coalescence threshold).
let nextSessionCheckpoint =
this._lastSessionCheckpointTime + ABORTED_SESSION_UPDATE_INTERVAL_MS;
let combineActions = (shouldSendDaily && isAbortedPingDue) || (shouldSendDaily &&
areTimesClose(now, nextSessionCheckpoint, SCHEDULER_COALESCE_THRESHOLD_MS));
if (combineActions) {
this._log.trace("_schedulerTickLogic - Combining pings.");
// Send the daily ping and also save its payload as an aborted-session ping.
return Impl._sendDailyPing(true).then(() => this._dailyPingSucceeded(now),
() => this._dailyPingFailed(now));
} else if (shouldSendDaily) {
this._log.trace("_schedulerTickLogic - Daily ping due.");
return Impl._sendDailyPing().then(() => this._dailyPingSucceeded(now),
() => this._dailyPingFailed(now));
} else if (isAbortedPingDue) {
this._log.trace("_schedulerTickLogic - Aborted session ping due.");
return this._saveAbortedPing(now);
}
// No ping is due.
this._log.trace("_schedulerTickLogic - No ping due.");
// It's possible, because of sleeps, that we're no longer within midnight tolerance for
// daily pings. Because of that, daily retry attempts would not be 0 on the next midnight.
// Reset that count on do-nothing ticks.
this._dailyPingRetryAttempts = 0;
return Promise.resolve();
},
/**
* Update the scheduled pings if some other ping was sent.
* @param {String} reason The reason of the ping that was sent.
* @param {Object} [competingPayload=null] The payload of the ping that was sent. The
* reason of this payload will be changed.
*/
reschedulePings: function(reason, competingPayload = null) {
if (this._shuttingDown) {
this._log.error("reschedulePings - already shutdown");
return;
}
this._log.trace("reschedulePings - reason: " + reason);
let now = Policy.now();
this._lastAdhocPingTime = now.getTime();
if (reason == REASON_ENVIRONMENT_CHANGE) {
// We just generated an environment-changed ping, save it as an aborted session and
// update the schedules.
this._saveAbortedPing(now.getTime(), competingPayload);
// If we're close to midnight, skip today's daily ping and reschedule it for tomorrow.
let nearestMidnight = getNearestMidnight(now);
if (nearestMidnight) {
this._lastDailyPingTime = now.getTime();
}
}
this._rescheduleTimeout();
},
/**
* Called when a scheduled operation successfully completes (ping sent or saved).
* @param {Number} now The current time, in milliseconds.
*/
_dailyPingSucceeded: function(now) {
this._log.trace("_dailyPingSucceeded");
this._lastDailyPingTime = now;
this._dailyPingRetryAttempts = 0;
},
/**
* Called when a scheduled operation fails (ping sent or saved).
* @param {Number} now The current time, in milliseconds.
*/
_dailyPingFailed: function(now) {
this._log.error("_dailyPingFailed");
this._dailyPingRetryAttempts++;
// If we reach the maximum number of retry attempts for a daily ping, log the error
// and skip this daily ping.
if (this._dailyPingRetryAttempts >= SCHEDULER_RETRY_ATTEMPTS) {
this._log.error("_pingFailed - The daily ping failed too many times. Skipping it.");
this._dailyPingRetryAttempts = 0;
this._lastDailyPingTime = now;
}
},
/**
* Stops the scheduler.
*/
shutdown: function() {
if (this._shuttingDown) {
if (this._log) {
this._log.error("shutdown - Already shut down");
} else {
Cu.reportError("TelemetryScheduler.shutdown - Already shut down");
}
return;
}
this._log.trace("shutdown");
if (this._schedulerTimer) {
Policy.clearSchedulerTickTimeout(this._schedulerTimer);
this._schedulerTimer = null;
}
this._shuttingDown = true;
}
};
this.EXPORTED_SYMBOLS = ["TelemetrySession"];
this.TelemetrySession = Object.freeze({
@ -490,12 +804,14 @@ let Impl = {
_profileSubsessionCounter: 0,
// Date of the last session split
_subsessionStartDate: null,
// The timer used for daily collections.
_dailyTimerId: null,
// A task performing delayed initialization of the chrome process
_delayedInitTask: null,
// The deferred promise resolved when the initialization task completes.
_delayedInitTaskDeferred: null,
// Used to serialize session state writes to disk.
_stateSaveSerializer: new SaveSerializer(),
// Used to serialize aborted session ping writes to disk.
_abortedSessionSerializer: new SaveSerializer(),
/**
* Gets a series of simple measurements (counters). At the moment, this
@ -1014,8 +1330,7 @@ let Impl = {
this.startNewSubsession();
// Persist session data to disk (don't wait until it completes).
let sessionData = this._getSessionDataObject();
gStateSaveSerializer.enqueueTask(() => this._saveSessionData(sessionData));
this._rescheduleDailyTimer();
this._stateSaveSerializer.enqueueTask(() => this._saveSessionData(sessionData));
}
return payload;
@ -1168,9 +1483,20 @@ let Impl = {
Telemetry.asyncFetchTelemetryData(function () {});
#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
this._rescheduleDailyTimer();
// Check for a previously written aborted session ping.
yield this._checkAbortedSessionPing();
TelemetryEnvironment.registerChangeListener(ENVIRONMENT_CHANGE_LISTENER,
() => this._onEnvironmentChange());
// 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 (!testing) {
yield this._saveAbortedSessionPing();
}
// Start the scheduler.
TelemetryScheduler.init();
#endif
this._delayedInitTaskDeferred.resolve();
@ -1321,7 +1647,7 @@ let Impl = {
overwrite: true,
filePath: file.path,
};
return TelemetryPing.testSavePingToFile(getPingType(payload), payload, options);
return TelemetryPing.savePing(getPingType(payload), payload, options);
},
/**
@ -1501,11 +1827,8 @@ let Impl = {
let cleanup = () => {
#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
TelemetryEnvironment.unregisterChangeListener(ENVIRONMENT_CHANGE_LISTENER);
TelemetryScheduler.shutdown();
#endif
if (this._dailyTimerId) {
Policy.clearDailyTimeout(this._dailyTimerId);
this._dailyTimerId = null;
}
this.uninstall();
let reset = () => {
@ -1515,7 +1838,11 @@ let Impl = {
if (Telemetry.canSend || testing) {
return this.savePendingPings()
.then(() => gStateSaveSerializer.flushTasks())
.then(() => this._stateSaveSerializer.flushTasks())
#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
.then(() => this._abortedSessionSerializer
.enqueueTask(() => this._removeAbortedSessionPing()))
#endif
.then(reset);
}
@ -1545,35 +1872,14 @@ let Impl = {
return this._delayedInitTask.finalize().then(cleanup);
},
_rescheduleDailyTimer: function() {
if (this._dailyTimerId) {
this._log.trace("_rescheduleDailyTimer - clearing existing timeout");
Policy.clearDailyTimeout(this._dailyTimerId);
}
let now = Policy.now();
let midnight = truncateToDays(now).getTime() + MS_IN_ONE_DAY;
let msUntilCollection = midnight - now.getTime();
if (msUntilCollection < MIN_SUBSESSION_LENGTH_MS) {
msUntilCollection += MS_IN_ONE_DAY;
}
this._log.trace("_rescheduleDailyTimer - now: " + now
+ ", scheduled: " + new Date(now.getTime() + msUntilCollection));
this._dailyTimerId = Policy.setDailyTimeout(() => this._onDailyTimer(), msUntilCollection);
},
_onDailyTimer: function() {
if (!this._initStarted) {
if (this._log) {
this._log.warn("_onDailyTimer - not initialized");
} else {
Cu.reportError("TelemetrySession._onDailyTimer - not initialized");
}
return;
}
this._log.trace("_onDailyTimer");
/**
* Gather and send a daily ping.
* @param {Boolean} [saveAsAborted=false] Also saves the payload as an aborted-session
* ping.
* @return {Promise} Resolved when the ping is sent.
*/
_sendDailyPing: function(saveAsAborted = false) {
this._log.trace("_sendDailyPing");
let payload = this.getSessionPayload(REASON_DAILY, true);
let options = {
@ -1581,10 +1887,14 @@ let Impl = {
addClientId: true,
addEnvironment: true,
};
let promise = TelemetryPing.send(getPingType(payload), payload, options);
this._rescheduleDailyTimer();
// Return the promise so tests can wait on the ping submission.
let promise = TelemetryPing.send(getPingType(payload), payload, options);
#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
// If required, also save the payload as an aborted session.
if (saveAsAborted) {
return promise.then(() => this._saveAbortedSessionPing(payload));
}
#endif
return promise;
},
@ -1594,7 +1904,7 @@ let Impl = {
* loading has completed, with false otherwise.
*/
_loadSessionData: Task.async(function* () {
let dataFile = OS.Path.join(OS.Constants.Path.profileDir, "datareporting",
let dataFile = OS.Path.join(OS.Constants.Path.profileDir, DATAREPORTING_DIRECTORY,
SESSION_STATE_FILE_NAME);
// Try to load the "profileSubsessionCounter" from the state file.
@ -1632,7 +1942,7 @@ let Impl = {
* Saves session data to disk.
*/
_saveSessionData: Task.async(function* (sessionData) {
let dataDir = OS.Path.join(OS.Constants.Path.profileDir, "datareporting");
let dataDir = OS.Path.join(OS.Constants.Path.profileDir, DATAREPORTING_DIRECTORY);
yield OS.File.makeDir(dataDir);
let filePath = OS.Path.join(dataDir, SESSION_STATE_FILE_NAME);
@ -1647,12 +1957,15 @@ let Impl = {
this._log.trace("_onEnvironmentChange");
let payload = this.getSessionPayload(REASON_ENVIRONMENT_CHANGE, true);
let clonedPayload = Cu.cloneInto(payload, myScope);
TelemetryScheduler.reschedulePings(REASON_ENVIRONMENT_CHANGE, clonedPayload);
let options = {
retentionDays: RETENTION_DAYS,
addClientId: true,
addEnvironment: true,
};
let promise = TelemetryPing.send(getPingType(payload), payload, options);
TelemetryPing.send(getPingType(payload), payload, options);
},
_isClassicReason: function(reason) {
@ -1673,7 +1986,73 @@ let Impl = {
initialized: this._initialized,
initStarted: this._initStarted,
haveDelayedInitTask: !!this._delayedInitTask,
dailyTimerScheduled: !!this._dailyTimerId,
};
},
/**
* Deletes the aborted session ping. This is called during shutdown.
* @return {Promise} Resolved when the aborted session ping is removed or if it doesn't
* exist.
*/
_removeAbortedSessionPing: function() {
const FILE_PATH = OS.Path.join(OS.Constants.Path.profileDir, DATAREPORTING_DIRECTORY,
ABORTED_SESSION_FILE_NAME);
try {
return OS.File.remove(FILE_PATH);
} catch (ex if ex.becauseNoSuchFile) { }
return Promise.resolve();
},
/**
* Check if there's any aborted session ping available. If so, tell TelemetryPing about
* it.
*/
_checkAbortedSessionPing: Task.async(function* () {
// Create the subdirectory that will contain te aborted session ping. We put it in a
// subdirectory so that it doesn't get picked up as a pending ping. Please note that
// this does nothing if the directory does not already exist.
const ABORTED_SESSIONS_DIR =
OS.Path.join(OS.Constants.Path.profileDir, DATAREPORTING_DIRECTORY);
yield OS.File.makeDir(ABORTED_SESSIONS_DIR, { ignoreExisting: true });
const FILE_PATH = OS.Path.join(OS.Constants.Path.profileDir, DATAREPORTING_DIRECTORY,
ABORTED_SESSION_FILE_NAME);
let abortedExists = yield OS.File.exists(FILE_PATH);
if (abortedExists) {
this._log.trace("_checkAbortedSessionPing - aborted session found: " + FILE_PATH);
yield this._abortedSessionSerializer.enqueueTask(
() => TelemetryPing.addPendingPing(FILE_PATH, true));
}
}),
/**
* 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: function(aProvidedPayload = null) {
const FILE_PATH = OS.Path.join(OS.Constants.Path.profileDir, DATAREPORTING_DIRECTORY,
ABORTED_SESSION_FILE_NAME);
this._log.trace("_saveAbortedSessionPing - ping path: " + FILE_PATH);
let payload = null;
if (aProvidedPayload) {
payload = aProvidedPayload;
// Overwrite the original reason.
payload.info.reason = REASON_ABORTED_SESSION;
} else {
payload = this.getSessionPayload(REASON_ABORTED_SESSION, false);
}
let options = {
retentionDays: RETENTION_DAYS,
addClientId: true,
addEnvironment: true,
overwrite: true,
filePath: FILE_PATH,
};
return this._abortedSessionSerializer.enqueueTask(() =>
TelemetryPing.savePing(getPingType(payload), payload, options));
},
};