mirror of
https://github.com/classilla/tenfourfox.git
synced 2024-09-10 11:54:40 +00:00
1801 lines
51 KiB
JavaScript
1801 lines
51 KiB
JavaScript
/* 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 file contains metrics data providers for the Firefox Health
|
|
* Report. Ideally each provider in this file exists in separate modules
|
|
* and lives close to the code it is querying. However, because of the
|
|
* overhead of JS compartments (which are created for each module), we
|
|
* currently have all the code in one file. When the overhead of
|
|
* compartments reaches a reasonable level, this file should be split
|
|
* up.
|
|
*/
|
|
|
|
#ifndef MERGED_COMPARTMENT
|
|
|
|
"use strict";
|
|
|
|
this.EXPORTED_SYMBOLS = [
|
|
"AddonsProvider",
|
|
"AppInfoProvider",
|
|
#ifdef MOZ_CRASHREPORTER
|
|
"CrashesProvider",
|
|
#endif
|
|
"HealthReportProvider",
|
|
"HotfixProvider",
|
|
"PlacesProvider",
|
|
"SearchesProvider",
|
|
"SessionsProvider",
|
|
"SysInfoProvider",
|
|
];
|
|
|
|
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
|
|
|
|
Cu.import("resource://gre/modules/Metrics.jsm");
|
|
|
|
#endif
|
|
|
|
Cu.import("resource://gre/modules/Promise.jsm");
|
|
Cu.import("resource://gre/modules/osfile.jsm");
|
|
Cu.import("resource://gre/modules/Preferences.jsm");
|
|
Cu.import("resource://gre/modules/Services.jsm");
|
|
Cu.import("resource://gre/modules/Task.jsm");
|
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
|
Cu.import("resource://services-common/utils.js");
|
|
|
|
XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
|
|
"resource://gre/modules/AddonManager.jsm");
|
|
XPCOMUtils.defineLazyModuleGetter(this, "UpdateUtils",
|
|
"resource://gre/modules/UpdateUtils.jsm");
|
|
XPCOMUtils.defineLazyModuleGetter(this, "PlacesDBUtils",
|
|
"resource://gre/modules/PlacesDBUtils.jsm");
|
|
|
|
|
|
const LAST_NUMERIC_FIELD = {type: Metrics.Storage.FIELD_LAST_NUMERIC};
|
|
const LAST_TEXT_FIELD = {type: Metrics.Storage.FIELD_LAST_TEXT};
|
|
const DAILY_DISCRETE_NUMERIC_FIELD = {type: Metrics.Storage.FIELD_DAILY_DISCRETE_NUMERIC};
|
|
const DAILY_LAST_NUMERIC_FIELD = {type: Metrics.Storage.FIELD_DAILY_LAST_NUMERIC};
|
|
const DAILY_LAST_TEXT_FIELD = {type: Metrics.Storage.FIELD_DAILY_LAST_TEXT};
|
|
const DAILY_COUNTER_FIELD = {type: Metrics.Storage.FIELD_DAILY_COUNTER};
|
|
|
|
const TELEMETRY_PREF = "toolkit.telemetry.enabled";
|
|
const SEARCH_COHORT_PREF = "browser.search.cohort";
|
|
|
|
function isTelemetryEnabled(prefs) {
|
|
return prefs.get(TELEMETRY_PREF, false);
|
|
}
|
|
|
|
/**
|
|
* Represents basic application state.
|
|
*
|
|
* This is roughly a union of nsIXULAppInfo, nsIXULRuntime, with a few extra
|
|
* pieces thrown in.
|
|
*/
|
|
function AppInfoMeasurement() {
|
|
Metrics.Measurement.call(this);
|
|
}
|
|
|
|
AppInfoMeasurement.prototype = Object.freeze({
|
|
__proto__: Metrics.Measurement.prototype,
|
|
|
|
name: "appinfo",
|
|
version: 2,
|
|
|
|
fields: {
|
|
vendor: LAST_TEXT_FIELD,
|
|
name: LAST_TEXT_FIELD,
|
|
id: LAST_TEXT_FIELD,
|
|
version: LAST_TEXT_FIELD,
|
|
appBuildID: LAST_TEXT_FIELD,
|
|
platformVersion: LAST_TEXT_FIELD,
|
|
platformBuildID: LAST_TEXT_FIELD,
|
|
os: LAST_TEXT_FIELD,
|
|
xpcomabi: LAST_TEXT_FIELD,
|
|
updateChannel: LAST_TEXT_FIELD,
|
|
distributionID: LAST_TEXT_FIELD,
|
|
distributionVersion: LAST_TEXT_FIELD,
|
|
hotfixVersion: LAST_TEXT_FIELD,
|
|
locale: LAST_TEXT_FIELD,
|
|
isDefaultBrowser: {type: Metrics.Storage.FIELD_DAILY_LAST_NUMERIC},
|
|
isTelemetryEnabled: {type: Metrics.Storage.FIELD_DAILY_LAST_NUMERIC},
|
|
isBlocklistEnabled: {type: Metrics.Storage.FIELD_DAILY_LAST_NUMERIC},
|
|
},
|
|
});
|
|
|
|
/**
|
|
* Legacy version of app info before Telemetry was added.
|
|
*
|
|
* The "last" fields have all been removed. We only report the longitudinal
|
|
* field.
|
|
*/
|
|
function AppInfoMeasurement1() {
|
|
Metrics.Measurement.call(this);
|
|
}
|
|
|
|
AppInfoMeasurement1.prototype = Object.freeze({
|
|
__proto__: Metrics.Measurement.prototype,
|
|
|
|
name: "appinfo",
|
|
version: 1,
|
|
|
|
fields: {
|
|
isDefaultBrowser: {type: Metrics.Storage.FIELD_DAILY_LAST_NUMERIC},
|
|
},
|
|
});
|
|
|
|
|
|
function AppVersionMeasurement1() {
|
|
Metrics.Measurement.call(this);
|
|
}
|
|
|
|
AppVersionMeasurement1.prototype = Object.freeze({
|
|
__proto__: Metrics.Measurement.prototype,
|
|
|
|
name: "versions",
|
|
version: 1,
|
|
|
|
fields: {
|
|
version: {type: Metrics.Storage.FIELD_DAILY_DISCRETE_TEXT},
|
|
},
|
|
});
|
|
|
|
// Version 2 added the build ID.
|
|
function AppVersionMeasurement2() {
|
|
Metrics.Measurement.call(this);
|
|
}
|
|
|
|
AppVersionMeasurement2.prototype = Object.freeze({
|
|
__proto__: Metrics.Measurement.prototype,
|
|
|
|
name: "versions",
|
|
version: 2,
|
|
|
|
fields: {
|
|
appVersion: {type: Metrics.Storage.FIELD_DAILY_DISCRETE_TEXT},
|
|
platformVersion: {type: Metrics.Storage.FIELD_DAILY_DISCRETE_TEXT},
|
|
appBuildID: {type: Metrics.Storage.FIELD_DAILY_DISCRETE_TEXT},
|
|
platformBuildID: {type: Metrics.Storage.FIELD_DAILY_DISCRETE_TEXT},
|
|
},
|
|
});
|
|
|
|
/**
|
|
* Holds data on the application update functionality.
|
|
*/
|
|
function AppUpdateMeasurement1() {
|
|
Metrics.Measurement.call(this);
|
|
}
|
|
|
|
AppUpdateMeasurement1.prototype = Object.freeze({
|
|
__proto__: Metrics.Measurement.prototype,
|
|
|
|
name: "update",
|
|
version: 1,
|
|
|
|
fields: {
|
|
enabled: {type: Metrics.Storage.FIELD_DAILY_LAST_NUMERIC},
|
|
autoDownload: {type: Metrics.Storage.FIELD_DAILY_LAST_NUMERIC},
|
|
},
|
|
});
|
|
|
|
this.AppInfoProvider = function AppInfoProvider() {
|
|
Metrics.Provider.call(this);
|
|
|
|
this._prefs = new Preferences({defaultBranch: null});
|
|
}
|
|
AppInfoProvider.prototype = Object.freeze({
|
|
__proto__: Metrics.Provider.prototype,
|
|
|
|
name: "org.mozilla.appInfo",
|
|
|
|
measurementTypes: [
|
|
AppInfoMeasurement,
|
|
AppInfoMeasurement1,
|
|
AppUpdateMeasurement1,
|
|
AppVersionMeasurement1,
|
|
AppVersionMeasurement2,
|
|
],
|
|
|
|
pullOnly: true,
|
|
|
|
appInfoFields: {
|
|
// From nsIXULAppInfo.
|
|
vendor: "vendor",
|
|
name: "name",
|
|
id: "ID",
|
|
version: "version",
|
|
appBuildID: "appBuildID",
|
|
platformVersion: "platformVersion",
|
|
platformBuildID: "platformBuildID",
|
|
|
|
// From nsIXULRuntime.
|
|
os: "OS",
|
|
xpcomabi: "XPCOMABI",
|
|
},
|
|
|
|
postInit: function () {
|
|
return Task.spawn(this._postInit.bind(this));
|
|
},
|
|
|
|
_postInit: function () {
|
|
let recordEmptyAppInfo = function () {
|
|
this._setCurrentAppVersion("");
|
|
this._setCurrentPlatformVersion("");
|
|
this._setCurrentAppBuildID("");
|
|
return this._setCurrentPlatformBuildID("");
|
|
}.bind(this);
|
|
|
|
// Services.appInfo should always be defined for any reasonably behaving
|
|
// Gecko app. If it isn't, we insert a empty string sentinel value.
|
|
let ai;
|
|
try {
|
|
ai = Services.appinfo;
|
|
} catch (ex) {
|
|
this._log.error("Could not obtain Services.appinfo: " +
|
|
CommonUtils.exceptionStr(ex));
|
|
yield recordEmptyAppInfo();
|
|
return;
|
|
}
|
|
|
|
if (!ai) {
|
|
this._log.error("Services.appinfo is unavailable.");
|
|
yield recordEmptyAppInfo();
|
|
return;
|
|
}
|
|
|
|
let currentAppVersion = ai.version;
|
|
let currentPlatformVersion = ai.platformVersion;
|
|
let currentAppBuildID = ai.appBuildID;
|
|
let currentPlatformBuildID = ai.platformBuildID;
|
|
|
|
// State's name doesn't contain "app" for historical compatibility.
|
|
let lastAppVersion = yield this.getState("lastVersion");
|
|
let lastPlatformVersion = yield this.getState("lastPlatformVersion");
|
|
let lastAppBuildID = yield this.getState("lastAppBuildID");
|
|
let lastPlatformBuildID = yield this.getState("lastPlatformBuildID");
|
|
|
|
if (currentAppVersion != lastAppVersion) {
|
|
yield this._setCurrentAppVersion(currentAppVersion);
|
|
}
|
|
|
|
if (currentPlatformVersion != lastPlatformVersion) {
|
|
yield this._setCurrentPlatformVersion(currentPlatformVersion);
|
|
}
|
|
|
|
if (currentAppBuildID != lastAppBuildID) {
|
|
yield this._setCurrentAppBuildID(currentAppBuildID);
|
|
}
|
|
|
|
if (currentPlatformBuildID != lastPlatformBuildID) {
|
|
yield this._setCurrentPlatformBuildID(currentPlatformBuildID);
|
|
}
|
|
},
|
|
|
|
_setCurrentAppVersion: function (version) {
|
|
this._log.info("Recording new application version: " + version);
|
|
let m = this.getMeasurement("versions", 2);
|
|
m.addDailyDiscreteText("appVersion", version);
|
|
|
|
// "app" not encoded in key for historical compatibility.
|
|
return this.setState("lastVersion", version);
|
|
},
|
|
|
|
_setCurrentPlatformVersion: function (version) {
|
|
this._log.info("Recording new platform version: " + version);
|
|
let m = this.getMeasurement("versions", 2);
|
|
m.addDailyDiscreteText("platformVersion", version);
|
|
return this.setState("lastPlatformVersion", version);
|
|
},
|
|
|
|
_setCurrentAppBuildID: function (build) {
|
|
this._log.info("Recording new application build ID: " + build);
|
|
let m = this.getMeasurement("versions", 2);
|
|
m.addDailyDiscreteText("appBuildID", build);
|
|
return this.setState("lastAppBuildID", build);
|
|
},
|
|
|
|
_setCurrentPlatformBuildID: function (build) {
|
|
this._log.info("Recording new platform build ID: " + build);
|
|
let m = this.getMeasurement("versions", 2);
|
|
m.addDailyDiscreteText("platformBuildID", build);
|
|
return this.setState("lastPlatformBuildID", build);
|
|
},
|
|
|
|
|
|
collectConstantData: function () {
|
|
return this.storage.enqueueTransaction(this._populateConstants.bind(this));
|
|
},
|
|
|
|
_populateConstants: function () {
|
|
let m = this.getMeasurement(AppInfoMeasurement.prototype.name,
|
|
AppInfoMeasurement.prototype.version);
|
|
|
|
let ai;
|
|
try {
|
|
ai = Services.appinfo;
|
|
} catch (ex) {
|
|
this._log.warn("Could not obtain Services.appinfo: " +
|
|
CommonUtils.exceptionStr(ex));
|
|
throw ex;
|
|
}
|
|
|
|
if (!ai) {
|
|
this._log.warn("Services.appinfo is unavailable.");
|
|
throw ex;
|
|
}
|
|
|
|
for (let [k, v] in Iterator(this.appInfoFields)) {
|
|
try {
|
|
yield m.setLastText(k, ai[v]);
|
|
} catch (ex) {
|
|
this._log.warn("Error obtaining Services.appinfo." + v);
|
|
}
|
|
}
|
|
|
|
try {
|
|
yield m.setLastText("updateChannel", UpdateUtils.UpdateChannel);
|
|
} catch (ex) {
|
|
this._log.warn("Could not obtain update channel: " +
|
|
CommonUtils.exceptionStr(ex));
|
|
}
|
|
|
|
yield m.setLastText("distributionID", this._prefs.get("distribution.id", ""));
|
|
yield m.setLastText("distributionVersion", this._prefs.get("distribution.version", ""));
|
|
yield m.setLastText("hotfixVersion", this._prefs.get("extensions.hotfix.lastVersion", ""));
|
|
|
|
try {
|
|
let locale = Cc["@mozilla.org/chrome/chrome-registry;1"]
|
|
.getService(Ci.nsIXULChromeRegistry)
|
|
.getSelectedLocale("global");
|
|
yield m.setLastText("locale", locale);
|
|
} catch (ex) {
|
|
this._log.warn("Could not obtain application locale: " +
|
|
CommonUtils.exceptionStr(ex));
|
|
}
|
|
|
|
// FUTURE this should be retrieved periodically or at upload time.
|
|
yield this._recordIsTelemetryEnabled(m);
|
|
yield this._recordIsBlocklistEnabled(m);
|
|
yield this._recordDefaultBrowser(m);
|
|
},
|
|
|
|
_recordIsTelemetryEnabled: function (m) {
|
|
let enabled = isTelemetryEnabled(this._prefs);
|
|
this._log.debug("Recording telemetry enabled (" + TELEMETRY_PREF + "): " + enabled);
|
|
yield m.setDailyLastNumeric("isTelemetryEnabled", enabled ? 1 : 0);
|
|
},
|
|
|
|
_recordIsBlocklistEnabled: function (m) {
|
|
let enabled = this._prefs.get("extensions.blocklist.enabled", false);
|
|
this._log.debug("Recording blocklist enabled: " + enabled);
|
|
yield m.setDailyLastNumeric("isBlocklistEnabled", enabled ? 1 : 0);
|
|
},
|
|
|
|
_recordDefaultBrowser: function (m) {
|
|
let shellService;
|
|
try {
|
|
shellService = Cc["@mozilla.org/browser/shell-service;1"]
|
|
.getService(Ci.nsIShellService);
|
|
} catch (ex) {
|
|
this._log.warn("Could not obtain shell service: " +
|
|
CommonUtils.exceptionStr(ex));
|
|
}
|
|
|
|
let isDefault = -1;
|
|
|
|
if (shellService) {
|
|
try {
|
|
// This uses the same set of flags used by the pref pane.
|
|
isDefault = shellService.isDefaultBrowser(false, true) ? 1 : 0;
|
|
} catch (ex) {
|
|
this._log.warn("Could not determine if default browser: " +
|
|
CommonUtils.exceptionStr(ex));
|
|
}
|
|
}
|
|
|
|
return m.setDailyLastNumeric("isDefaultBrowser", isDefault);
|
|
},
|
|
|
|
collectDailyData: function () {
|
|
return this.storage.enqueueTransaction(function getDaily() {
|
|
let m = this.getMeasurement(AppUpdateMeasurement1.prototype.name,
|
|
AppUpdateMeasurement1.prototype.version);
|
|
|
|
let enabled = this._prefs.get("app.update.enabled", false);
|
|
yield m.setDailyLastNumeric("enabled", enabled ? 1 : 0);
|
|
|
|
let auto = this._prefs.get("app.update.auto", false);
|
|
yield m.setDailyLastNumeric("autoDownload", auto ? 1 : 0);
|
|
}.bind(this));
|
|
},
|
|
});
|
|
|
|
|
|
function SysInfoMeasurement() {
|
|
Metrics.Measurement.call(this);
|
|
}
|
|
|
|
SysInfoMeasurement.prototype = Object.freeze({
|
|
__proto__: Metrics.Measurement.prototype,
|
|
|
|
name: "sysinfo",
|
|
version: 2,
|
|
|
|
fields: {
|
|
cpuCount: {type: Metrics.Storage.FIELD_LAST_NUMERIC},
|
|
memoryMB: {type: Metrics.Storage.FIELD_LAST_NUMERIC},
|
|
manufacturer: LAST_TEXT_FIELD,
|
|
device: LAST_TEXT_FIELD,
|
|
hardware: LAST_TEXT_FIELD,
|
|
name: LAST_TEXT_FIELD,
|
|
version: LAST_TEXT_FIELD,
|
|
architecture: LAST_TEXT_FIELD,
|
|
isWow64: LAST_NUMERIC_FIELD,
|
|
},
|
|
});
|
|
|
|
|
|
this.SysInfoProvider = function SysInfoProvider() {
|
|
Metrics.Provider.call(this);
|
|
};
|
|
|
|
SysInfoProvider.prototype = Object.freeze({
|
|
__proto__: Metrics.Provider.prototype,
|
|
|
|
name: "org.mozilla.sysinfo",
|
|
|
|
measurementTypes: [SysInfoMeasurement],
|
|
|
|
pullOnly: true,
|
|
|
|
sysInfoFields: {
|
|
cpucount: "cpuCount",
|
|
memsize: "memoryMB",
|
|
manufacturer: "manufacturer",
|
|
device: "device",
|
|
hardware: "hardware",
|
|
name: "name",
|
|
version: "version",
|
|
arch: "architecture",
|
|
isWow64: "isWow64",
|
|
},
|
|
|
|
collectConstantData: function () {
|
|
return this.storage.enqueueTransaction(this._populateConstants.bind(this));
|
|
},
|
|
|
|
_populateConstants: function () {
|
|
let m = this.getMeasurement(SysInfoMeasurement.prototype.name,
|
|
SysInfoMeasurement.prototype.version);
|
|
|
|
let si = Cc["@mozilla.org/system-info;1"]
|
|
.getService(Ci.nsIPropertyBag2);
|
|
|
|
for (let [k, v] in Iterator(this.sysInfoFields)) {
|
|
try {
|
|
if (!si.hasKey(k)) {
|
|
this._log.debug("Property not available: " + k);
|
|
continue;
|
|
}
|
|
|
|
let value = si.getProperty(k);
|
|
let method = "setLastText";
|
|
|
|
if (["cpucount", "memsize"].indexOf(k) != -1) {
|
|
let converted = parseInt(value, 10);
|
|
if (Number.isNaN(converted)) {
|
|
continue;
|
|
}
|
|
|
|
value = converted;
|
|
method = "setLastNumeric";
|
|
}
|
|
|
|
switch (k) {
|
|
case "memsize":
|
|
// Round memory to mebibytes.
|
|
value = Math.round(value / 1048576);
|
|
break;
|
|
case "isWow64":
|
|
// Property is only present on Windows. hasKey() skipping from
|
|
// above ensures undefined or null doesn't creep in here.
|
|
value = value ? 1 : 0;
|
|
method = "setLastNumeric";
|
|
break;
|
|
}
|
|
|
|
yield m[method](v, value);
|
|
} catch (ex) {
|
|
this._log.warn("Error obtaining system info field: " + k + " " +
|
|
CommonUtils.exceptionStr(ex));
|
|
}
|
|
}
|
|
},
|
|
});
|
|
|
|
|
|
/**
|
|
* Holds information about the current/active session.
|
|
*
|
|
* The fields within the current session are moved to daily session fields when
|
|
* the application is shut down.
|
|
*
|
|
* This measurement is backed by the SessionRecorder, not the database.
|
|
*/
|
|
function CurrentSessionMeasurement() {
|
|
Metrics.Measurement.call(this);
|
|
}
|
|
|
|
CurrentSessionMeasurement.prototype = Object.freeze({
|
|
__proto__: Metrics.Measurement.prototype,
|
|
|
|
name: "current",
|
|
version: 3,
|
|
|
|
// Storage is in preferences.
|
|
fields: {},
|
|
|
|
/**
|
|
* All data is stored in prefs, so we have a custom implementation.
|
|
*/
|
|
getValues: function () {
|
|
let sessions = this.provider.healthReporter.sessionRecorder;
|
|
|
|
let fields = new Map();
|
|
let now = new Date();
|
|
fields.set("startDay", [now, Metrics.dateToDays(sessions.startDate)]);
|
|
fields.set("activeTicks", [now, sessions.activeTicks]);
|
|
fields.set("totalTime", [now, sessions.totalTime]);
|
|
fields.set("main", [now, sessions.main]);
|
|
fields.set("firstPaint", [now, sessions.firstPaint]);
|
|
fields.set("sessionRestored", [now, sessions.sessionRestored]);
|
|
|
|
return CommonUtils.laterTickResolvingPromise({
|
|
days: new Metrics.DailyValues(),
|
|
singular: fields,
|
|
});
|
|
},
|
|
|
|
_serializeJSONSingular: function (data) {
|
|
let result = {"_v": this.version};
|
|
|
|
for (let [field, value] of data) {
|
|
result[field] = value[1];
|
|
}
|
|
|
|
return result;
|
|
},
|
|
});
|
|
|
|
/**
|
|
* Records a history of all application sessions.
|
|
*/
|
|
function PreviousSessionsMeasurement() {
|
|
Metrics.Measurement.call(this);
|
|
}
|
|
|
|
PreviousSessionsMeasurement.prototype = Object.freeze({
|
|
__proto__: Metrics.Measurement.prototype,
|
|
|
|
name: "previous",
|
|
version: 3,
|
|
|
|
fields: {
|
|
// Milliseconds of sessions that were properly shut down.
|
|
cleanActiveTicks: DAILY_DISCRETE_NUMERIC_FIELD,
|
|
cleanTotalTime: DAILY_DISCRETE_NUMERIC_FIELD,
|
|
|
|
// Milliseconds of sessions that were not properly shut down.
|
|
abortedActiveTicks: DAILY_DISCRETE_NUMERIC_FIELD,
|
|
abortedTotalTime: DAILY_DISCRETE_NUMERIC_FIELD,
|
|
|
|
// Startup times in milliseconds.
|
|
main: DAILY_DISCRETE_NUMERIC_FIELD,
|
|
firstPaint: DAILY_DISCRETE_NUMERIC_FIELD,
|
|
sessionRestored: DAILY_DISCRETE_NUMERIC_FIELD,
|
|
},
|
|
});
|
|
|
|
|
|
/**
|
|
* Records information about the current browser session.
|
|
*
|
|
* A browser session is defined as an application/process lifetime. We
|
|
* start a new session when the application starts (essentially when
|
|
* this provider is instantiated) and end the session on shutdown.
|
|
*
|
|
* As the application runs, we record basic information about the
|
|
* "activity" of the session. Activity is defined by the presence of
|
|
* physical input into the browser (key press, mouse click, touch, etc).
|
|
*
|
|
* We differentiate between regular sessions and "aborted" sessions. An
|
|
* aborted session is one that does not end expectedly. This is often the
|
|
* result of a crash. We detect aborted sessions by storing the current
|
|
* session separate from completed sessions. We normally move the
|
|
* current session to completed sessions on application shutdown. If a
|
|
* current session is present on application startup, that means that
|
|
* the previous session was aborted.
|
|
*/
|
|
this.SessionsProvider = function () {
|
|
Metrics.Provider.call(this);
|
|
};
|
|
|
|
SessionsProvider.prototype = Object.freeze({
|
|
__proto__: Metrics.Provider.prototype,
|
|
|
|
name: "org.mozilla.appSessions",
|
|
|
|
measurementTypes: [CurrentSessionMeasurement, PreviousSessionsMeasurement],
|
|
|
|
pullOnly: true,
|
|
|
|
collectConstantData: function () {
|
|
let previous = this.getMeasurement("previous", 3);
|
|
|
|
return this.storage.enqueueTransaction(this._recordAndPruneSessions.bind(this));
|
|
},
|
|
|
|
_recordAndPruneSessions: function () {
|
|
this._log.info("Moving previous sessions from session recorder to storage.");
|
|
let recorder = this.healthReporter.sessionRecorder;
|
|
let sessions = recorder.getPreviousSessions();
|
|
this._log.debug("Found " + Object.keys(sessions).length + " previous sessions.");
|
|
|
|
let daily = this.getMeasurement("previous", 3);
|
|
|
|
// Please note the coupling here between the session recorder and our state.
|
|
// If the pruned index or the current index of the session recorder is ever
|
|
// deleted or reset to 0, our stored state of a later index would mean that
|
|
// new sessions would never be captured by this provider until the session
|
|
// recorder index catches up to our last session ID. This should not happen
|
|
// under normal circumstances, so we don't worry too much about it. We
|
|
// should, however, consider this as part of implementing bug 841561.
|
|
let lastRecordedSession = yield this.getState("lastSession");
|
|
if (lastRecordedSession === null) {
|
|
lastRecordedSession = -1;
|
|
}
|
|
this._log.debug("The last recorded session was #" + lastRecordedSession);
|
|
|
|
for (let [index, session] in Iterator(sessions)) {
|
|
if (index <= lastRecordedSession) {
|
|
this._log.warn("Already recorded session " + index + ". Did the last " +
|
|
"session crash or have an issue saving the prefs file?");
|
|
continue;
|
|
}
|
|
|
|
let type = session.clean ? "clean" : "aborted";
|
|
let date = session.startDate;
|
|
yield daily.addDailyDiscreteNumeric(type + "ActiveTicks", session.activeTicks, date);
|
|
yield daily.addDailyDiscreteNumeric(type + "TotalTime", session.totalTime, date);
|
|
|
|
for (let field of ["main", "firstPaint", "sessionRestored"]) {
|
|
yield daily.addDailyDiscreteNumeric(field, session[field], date);
|
|
}
|
|
|
|
lastRecordedSession = index;
|
|
}
|
|
|
|
yield this.setState("lastSession", "" + lastRecordedSession);
|
|
recorder.pruneOldSessions(new Date());
|
|
},
|
|
});
|
|
|
|
/**
|
|
* Stores the set of active addons in storage.
|
|
*
|
|
* We do things a little differently than most other measurements. Because
|
|
* addons are difficult to shoehorn into distinct fields, we simply store a
|
|
* JSON blob in storage in a text field.
|
|
*/
|
|
function ActiveAddonsMeasurement() {
|
|
Metrics.Measurement.call(this);
|
|
|
|
this._serializers = {};
|
|
this._serializers[this.SERIALIZE_JSON] = {
|
|
singular: this._serializeJSONSingular.bind(this),
|
|
// We don't need a daily serializer because we have none of this data.
|
|
};
|
|
}
|
|
|
|
ActiveAddonsMeasurement.prototype = Object.freeze({
|
|
__proto__: Metrics.Measurement.prototype,
|
|
|
|
name: "addons",
|
|
version: 2,
|
|
|
|
fields: {
|
|
addons: LAST_TEXT_FIELD,
|
|
},
|
|
|
|
_serializeJSONSingular: function (data) {
|
|
if (!data.has("addons")) {
|
|
this._log.warn("Don't have addons info. Weird.");
|
|
return null;
|
|
}
|
|
|
|
// Exceptions are caught in the caller.
|
|
let result = JSON.parse(data.get("addons")[1]);
|
|
result._v = this.version;
|
|
return result;
|
|
},
|
|
});
|
|
|
|
/**
|
|
* Stores the set of active plugins in storage.
|
|
*
|
|
* This stores the data in a JSON blob in a text field similar to the
|
|
* ActiveAddonsMeasurement.
|
|
*/
|
|
function ActivePluginsMeasurement() {
|
|
Metrics.Measurement.call(this);
|
|
|
|
this._serializers = {};
|
|
this._serializers[this.SERIALIZE_JSON] = {
|
|
singular: this._serializeJSONSingular.bind(this),
|
|
// We don't need a daily serializer because we have none of this data.
|
|
};
|
|
}
|
|
|
|
ActivePluginsMeasurement.prototype = Object.freeze({
|
|
__proto__: Metrics.Measurement.prototype,
|
|
|
|
name: "plugins",
|
|
version: 1,
|
|
|
|
fields: {
|
|
plugins: LAST_TEXT_FIELD,
|
|
},
|
|
|
|
_serializeJSONSingular: function (data) {
|
|
if (!data.has("plugins")) {
|
|
this._log.warn("Don't have plugins info. Weird.");
|
|
return null;
|
|
}
|
|
|
|
// Exceptions are caught in the caller.
|
|
let result = JSON.parse(data.get("plugins")[1]);
|
|
result._v = this.version;
|
|
return result;
|
|
},
|
|
});
|
|
|
|
function ActiveGMPluginsMeasurement() {
|
|
Metrics.Measurement.call(this);
|
|
|
|
this._serializers = {};
|
|
this._serializers[this.SERIALIZE_JSON] = {
|
|
singular: this._serializeJSONSingular.bind(this),
|
|
};
|
|
}
|
|
|
|
ActiveGMPluginsMeasurement.prototype = Object.freeze({
|
|
__proto__: Metrics.Measurement.prototype,
|
|
|
|
name: "gm-plugins",
|
|
version: 1,
|
|
|
|
fields: {
|
|
"gm-plugins": LAST_TEXT_FIELD,
|
|
},
|
|
|
|
_serializeJSONSingular: function (data) {
|
|
if (!data.has("gm-plugins")) {
|
|
this._log.warn("Don't have GM plugins info. Weird.");
|
|
return null;
|
|
}
|
|
|
|
let result = JSON.parse(data.get("gm-plugins")[1]);
|
|
result._v = this.version;
|
|
return result;
|
|
},
|
|
});
|
|
|
|
function AddonCountsMeasurement() {
|
|
Metrics.Measurement.call(this);
|
|
}
|
|
|
|
AddonCountsMeasurement.prototype = Object.freeze({
|
|
__proto__: Metrics.Measurement.prototype,
|
|
|
|
name: "counts",
|
|
version: 2,
|
|
|
|
fields: {
|
|
theme: DAILY_LAST_NUMERIC_FIELD,
|
|
lwtheme: DAILY_LAST_NUMERIC_FIELD,
|
|
plugin: DAILY_LAST_NUMERIC_FIELD,
|
|
extension: DAILY_LAST_NUMERIC_FIELD,
|
|
service: DAILY_LAST_NUMERIC_FIELD,
|
|
},
|
|
});
|
|
|
|
|
|
/**
|
|
* Legacy version of addons counts before services was added.
|
|
*/
|
|
function AddonCountsMeasurement1() {
|
|
Metrics.Measurement.call(this);
|
|
}
|
|
|
|
AddonCountsMeasurement1.prototype = Object.freeze({
|
|
__proto__: Metrics.Measurement.prototype,
|
|
|
|
name: "counts",
|
|
version: 1,
|
|
|
|
fields: {
|
|
theme: DAILY_LAST_NUMERIC_FIELD,
|
|
lwtheme: DAILY_LAST_NUMERIC_FIELD,
|
|
plugin: DAILY_LAST_NUMERIC_FIELD,
|
|
extension: DAILY_LAST_NUMERIC_FIELD,
|
|
},
|
|
});
|
|
|
|
|
|
this.AddonsProvider = function () {
|
|
Metrics.Provider.call(this);
|
|
|
|
this._prefs = new Preferences({defaultBranch: null});
|
|
};
|
|
|
|
AddonsProvider.prototype = Object.freeze({
|
|
__proto__: Metrics.Provider.prototype,
|
|
|
|
// Whenever these AddonListener callbacks are called, we repopulate
|
|
// and store the set of addons. Note that these events will only fire
|
|
// for restartless add-ons. For actions that require a restart, we
|
|
// will catch the change after restart. The alternative is a lot of
|
|
// state tracking here, which isn't desirable.
|
|
ADDON_LISTENER_CALLBACKS: [
|
|
"onEnabled",
|
|
"onDisabled",
|
|
"onInstalled",
|
|
"onUninstalled",
|
|
],
|
|
|
|
// Add-on types for which full details are uploaded in the
|
|
// ActiveAddonsMeasurement. All other types are ignored.
|
|
FULL_DETAIL_TYPES: [
|
|
"extension",
|
|
"service",
|
|
],
|
|
|
|
name: "org.mozilla.addons",
|
|
|
|
measurementTypes: [
|
|
ActiveAddonsMeasurement,
|
|
ActivePluginsMeasurement,
|
|
ActiveGMPluginsMeasurement,
|
|
AddonCountsMeasurement1,
|
|
AddonCountsMeasurement,
|
|
],
|
|
|
|
postInit: function () {
|
|
let listener = {};
|
|
|
|
for (let method of this.ADDON_LISTENER_CALLBACKS) {
|
|
listener[method] = this._collectAndStoreAddons.bind(this);
|
|
}
|
|
|
|
this._listener = listener;
|
|
AddonManager.addAddonListener(this._listener);
|
|
|
|
return CommonUtils.laterTickResolvingPromise();
|
|
},
|
|
|
|
onShutdown: function () {
|
|
AddonManager.removeAddonListener(this._listener);
|
|
this._listener = null;
|
|
|
|
return CommonUtils.laterTickResolvingPromise();
|
|
},
|
|
|
|
collectConstantData: function () {
|
|
return this._collectAndStoreAddons();
|
|
},
|
|
|
|
_collectAndStoreAddons: function () {
|
|
let deferred = Promise.defer();
|
|
|
|
AddonManager.getAllAddons(function onAllAddons(allAddons) {
|
|
let data;
|
|
let addonsField;
|
|
let pluginsField;
|
|
let gmPluginsField;
|
|
try {
|
|
data = this._createDataStructure(allAddons);
|
|
addonsField = JSON.stringify(data.addons);
|
|
pluginsField = JSON.stringify(data.plugins);
|
|
gmPluginsField = JSON.stringify(data.gmPlugins);
|
|
} catch (ex) {
|
|
this._log.warn("Exception when populating add-ons data structure: " +
|
|
CommonUtils.exceptionStr(ex));
|
|
deferred.reject(ex);
|
|
return;
|
|
}
|
|
|
|
let now = new Date();
|
|
let addons = this.getMeasurement("addons", 2);
|
|
let plugins = this.getMeasurement("plugins", 1);
|
|
let gmPlugins = this.getMeasurement("gm-plugins", 1);
|
|
let counts = this.getMeasurement(AddonCountsMeasurement.prototype.name,
|
|
AddonCountsMeasurement.prototype.version);
|
|
|
|
this.enqueueStorageOperation(function storageAddons() {
|
|
for (let type in data.counts) {
|
|
try {
|
|
counts.fieldID(type);
|
|
} catch (ex) {
|
|
this._log.warn("Add-on type without field: " + type);
|
|
continue;
|
|
}
|
|
|
|
counts.setDailyLastNumeric(type, data.counts[type], now);
|
|
}
|
|
|
|
return addons.setLastText("addons", addonsField).then(
|
|
function onSuccess() {
|
|
return plugins.setLastText("plugins", pluginsField).then(
|
|
function onSuccess() {
|
|
return gmPlugins.setLastText("gm-plugins", gmPluginsField).then(
|
|
function onSuccess() {
|
|
deferred.resolve();
|
|
},
|
|
function onError(error) {
|
|
deferred.reject(error);
|
|
});
|
|
},
|
|
function onError(error) { deferred.reject(error); }
|
|
);
|
|
},
|
|
function onError(error) { deferred.reject(error); }
|
|
);
|
|
}.bind(this));
|
|
}.bind(this));
|
|
|
|
return deferred.promise;
|
|
},
|
|
|
|
COPY_ADDON_FIELDS: [
|
|
"userDisabled",
|
|
"appDisabled",
|
|
"name",
|
|
"version",
|
|
"type",
|
|
"scope",
|
|
"description",
|
|
"foreignInstall",
|
|
"hasBinaryComponents",
|
|
],
|
|
|
|
COPY_PLUGIN_FIELDS: [
|
|
"name",
|
|
"version",
|
|
"description",
|
|
"blocklisted",
|
|
"disabled",
|
|
"clicktoplay",
|
|
],
|
|
|
|
_createDataStructure: function (addons) {
|
|
let data = {
|
|
addons: {},
|
|
plugins: {},
|
|
gmPlugins: {},
|
|
counts: {}
|
|
};
|
|
|
|
for (let addon of addons) {
|
|
let type = addon.type;
|
|
|
|
// We count plugins separately below.
|
|
if (addon.type == "plugin") {
|
|
if (addon.isGMPlugin) {
|
|
data.gmPlugins[addon.id] = {
|
|
version: addon.version,
|
|
userDisabled: addon.userDisabled,
|
|
applyBackgroundUpdates: addon.applyBackgroundUpdates,
|
|
};
|
|
}
|
|
continue;
|
|
}
|
|
|
|
data.counts[type] = (data.counts[type] || 0) + 1;
|
|
|
|
if (this.FULL_DETAIL_TYPES.indexOf(addon.type) == -1) {
|
|
continue;
|
|
}
|
|
|
|
let obj = {};
|
|
for (let field of this.COPY_ADDON_FIELDS) {
|
|
obj[field] = addon[field];
|
|
}
|
|
|
|
if (addon.installDate) {
|
|
obj.installDay = this._dateToDays(addon.installDate);
|
|
}
|
|
|
|
if (addon.updateDate) {
|
|
obj.updateDay = this._dateToDays(addon.updateDate);
|
|
}
|
|
|
|
data.addons[addon.id] = obj;
|
|
}
|
|
|
|
let pluginTags = Cc["@mozilla.org/plugin/host;1"].
|
|
getService(Ci.nsIPluginHost).
|
|
getPluginTags({});
|
|
|
|
for (let tag of pluginTags) {
|
|
let obj = {
|
|
mimeTypes: tag.getMimeTypes({}),
|
|
};
|
|
|
|
for (let field of this.COPY_PLUGIN_FIELDS) {
|
|
obj[field] = tag[field];
|
|
}
|
|
|
|
// Plugins need to have a filename and a name, so this can't be empty.
|
|
let id = tag.filename + ":" + tag.name + ":" + tag.version + ":"
|
|
+ tag.description;
|
|
data.plugins[id] = obj;
|
|
}
|
|
|
|
data.counts["plugin"] = pluginTags.length;
|
|
|
|
return data;
|
|
},
|
|
});
|
|
|
|
#ifdef MOZ_CRASHREPORTER
|
|
|
|
function DailyCrashesMeasurement1() {
|
|
Metrics.Measurement.call(this);
|
|
}
|
|
|
|
DailyCrashesMeasurement1.prototype = Object.freeze({
|
|
__proto__: Metrics.Measurement.prototype,
|
|
|
|
name: "crashes",
|
|
version: 1,
|
|
|
|
fields: {
|
|
pending: DAILY_COUNTER_FIELD,
|
|
submitted: DAILY_COUNTER_FIELD,
|
|
},
|
|
});
|
|
|
|
function DailyCrashesMeasurement2() {
|
|
Metrics.Measurement.call(this);
|
|
}
|
|
|
|
DailyCrashesMeasurement2.prototype = Object.freeze({
|
|
__proto__: Metrics.Measurement.prototype,
|
|
|
|
name: "crashes",
|
|
version: 2,
|
|
|
|
fields: {
|
|
mainCrash: DAILY_LAST_NUMERIC_FIELD,
|
|
},
|
|
});
|
|
|
|
function DailyCrashesMeasurement3() {
|
|
Metrics.Measurement.call(this);
|
|
}
|
|
|
|
DailyCrashesMeasurement3.prototype = Object.freeze({
|
|
__proto__: Metrics.Measurement.prototype,
|
|
|
|
name: "crashes",
|
|
version: 3,
|
|
|
|
fields: {
|
|
"main-crash": DAILY_LAST_NUMERIC_FIELD,
|
|
"main-hang": DAILY_LAST_NUMERIC_FIELD,
|
|
"content-crash": DAILY_LAST_NUMERIC_FIELD,
|
|
"content-hang": DAILY_LAST_NUMERIC_FIELD,
|
|
"plugin-crash": DAILY_LAST_NUMERIC_FIELD,
|
|
"plugin-hang": DAILY_LAST_NUMERIC_FIELD,
|
|
},
|
|
});
|
|
|
|
function DailyCrashesMeasurement4() {
|
|
Metrics.Measurement.call(this);
|
|
}
|
|
|
|
DailyCrashesMeasurement4.prototype = Object.freeze({
|
|
__proto__: Metrics.Measurement.prototype,
|
|
|
|
name: "crashes",
|
|
version: 4,
|
|
|
|
fields: {
|
|
"main-crash": DAILY_LAST_NUMERIC_FIELD,
|
|
"main-crash-submission-succeeded": DAILY_LAST_NUMERIC_FIELD,
|
|
"main-crash-submission-failed": DAILY_LAST_NUMERIC_FIELD,
|
|
"main-hang": DAILY_LAST_NUMERIC_FIELD,
|
|
"main-hang-submission-succeeded": DAILY_LAST_NUMERIC_FIELD,
|
|
"main-hang-submission-failed": DAILY_LAST_NUMERIC_FIELD,
|
|
"content-crash": DAILY_LAST_NUMERIC_FIELD,
|
|
"content-crash-submission-succeeded": DAILY_LAST_NUMERIC_FIELD,
|
|
"content-crash-submission-failed": DAILY_LAST_NUMERIC_FIELD,
|
|
"content-hang": DAILY_LAST_NUMERIC_FIELD,
|
|
"content-hang-submission-succeeded": DAILY_LAST_NUMERIC_FIELD,
|
|
"content-hang-submission-failed": DAILY_LAST_NUMERIC_FIELD,
|
|
"plugin-crash": DAILY_LAST_NUMERIC_FIELD,
|
|
"plugin-crash-submission-succeeded": DAILY_LAST_NUMERIC_FIELD,
|
|
"plugin-crash-submission-failed": DAILY_LAST_NUMERIC_FIELD,
|
|
"plugin-hang": DAILY_LAST_NUMERIC_FIELD,
|
|
"plugin-hang-submission-succeeded": DAILY_LAST_NUMERIC_FIELD,
|
|
"plugin-hang-submission-failed": DAILY_LAST_NUMERIC_FIELD,
|
|
},
|
|
});
|
|
|
|
function DailyCrashesMeasurement5() {
|
|
Metrics.Measurement.call(this);
|
|
}
|
|
|
|
DailyCrashesMeasurement5.prototype = Object.freeze({
|
|
__proto__: Metrics.Measurement.prototype,
|
|
|
|
name: "crashes",
|
|
version: 5,
|
|
|
|
fields: {
|
|
"main-crash": DAILY_LAST_NUMERIC_FIELD,
|
|
"main-crash-submission-succeeded": DAILY_LAST_NUMERIC_FIELD,
|
|
"main-crash-submission-failed": DAILY_LAST_NUMERIC_FIELD,
|
|
"main-hang": DAILY_LAST_NUMERIC_FIELD,
|
|
"main-hang-submission-succeeded": DAILY_LAST_NUMERIC_FIELD,
|
|
"main-hang-submission-failed": DAILY_LAST_NUMERIC_FIELD,
|
|
"content-crash": DAILY_LAST_NUMERIC_FIELD,
|
|
"content-crash-submission-succeeded": DAILY_LAST_NUMERIC_FIELD,
|
|
"content-crash-submission-failed": DAILY_LAST_NUMERIC_FIELD,
|
|
"content-hang": DAILY_LAST_NUMERIC_FIELD,
|
|
"content-hang-submission-succeeded": DAILY_LAST_NUMERIC_FIELD,
|
|
"content-hang-submission-failed": DAILY_LAST_NUMERIC_FIELD,
|
|
"plugin-crash": DAILY_LAST_NUMERIC_FIELD,
|
|
"plugin-crash-submission-succeeded": DAILY_LAST_NUMERIC_FIELD,
|
|
"plugin-crash-submission-failed": DAILY_LAST_NUMERIC_FIELD,
|
|
"plugin-hang": DAILY_LAST_NUMERIC_FIELD,
|
|
"plugin-hang-submission-succeeded": DAILY_LAST_NUMERIC_FIELD,
|
|
"plugin-hang-submission-failed": DAILY_LAST_NUMERIC_FIELD,
|
|
"gmplugin-crash": DAILY_LAST_NUMERIC_FIELD,
|
|
"gmplugin-crash-submission-succeeded": DAILY_LAST_NUMERIC_FIELD,
|
|
"gmplugin-crash-submission-failed": DAILY_LAST_NUMERIC_FIELD,
|
|
},
|
|
});
|
|
|
|
function DailyCrashesMeasurement6() {
|
|
Metrics.Measurement.call(this);
|
|
}
|
|
|
|
DailyCrashesMeasurement6.prototype = Object.freeze({
|
|
__proto__: Metrics.Measurement.prototype,
|
|
|
|
name: "crashes",
|
|
version: 6,
|
|
|
|
fields: {
|
|
"main-crash": DAILY_LAST_NUMERIC_FIELD,
|
|
"main-crash-oom": DAILY_LAST_NUMERIC_FIELD,
|
|
"main-crash-submission-succeeded": DAILY_LAST_NUMERIC_FIELD,
|
|
"main-crash-submission-failed": DAILY_LAST_NUMERIC_FIELD,
|
|
"main-hang": DAILY_LAST_NUMERIC_FIELD,
|
|
"main-hang-submission-succeeded": DAILY_LAST_NUMERIC_FIELD,
|
|
"main-hang-submission-failed": DAILY_LAST_NUMERIC_FIELD,
|
|
"content-crash": DAILY_LAST_NUMERIC_FIELD,
|
|
"content-crash-submission-succeeded": DAILY_LAST_NUMERIC_FIELD,
|
|
"content-crash-submission-failed": DAILY_LAST_NUMERIC_FIELD,
|
|
"content-hang": DAILY_LAST_NUMERIC_FIELD,
|
|
"content-hang-submission-succeeded": DAILY_LAST_NUMERIC_FIELD,
|
|
"content-hang-submission-failed": DAILY_LAST_NUMERIC_FIELD,
|
|
"plugin-crash": DAILY_LAST_NUMERIC_FIELD,
|
|
"plugin-crash-submission-succeeded": DAILY_LAST_NUMERIC_FIELD,
|
|
"plugin-crash-submission-failed": DAILY_LAST_NUMERIC_FIELD,
|
|
"plugin-hang": DAILY_LAST_NUMERIC_FIELD,
|
|
"plugin-hang-submission-succeeded": DAILY_LAST_NUMERIC_FIELD,
|
|
"plugin-hang-submission-failed": DAILY_LAST_NUMERIC_FIELD,
|
|
"gmplugin-crash": DAILY_LAST_NUMERIC_FIELD,
|
|
"gmplugin-crash-submission-succeeded": DAILY_LAST_NUMERIC_FIELD,
|
|
"gmplugin-crash-submission-failed": DAILY_LAST_NUMERIC_FIELD,
|
|
},
|
|
});
|
|
|
|
this.CrashesProvider = function () {
|
|
Metrics.Provider.call(this);
|
|
|
|
// So we can unit test.
|
|
this._manager = Services.crashmanager;
|
|
};
|
|
|
|
CrashesProvider.prototype = Object.freeze({
|
|
__proto__: Metrics.Provider.prototype,
|
|
|
|
name: "org.mozilla.crashes",
|
|
|
|
measurementTypes: [
|
|
DailyCrashesMeasurement1,
|
|
DailyCrashesMeasurement2,
|
|
DailyCrashesMeasurement3,
|
|
DailyCrashesMeasurement4,
|
|
DailyCrashesMeasurement5,
|
|
DailyCrashesMeasurement6,
|
|
],
|
|
|
|
pullOnly: true,
|
|
|
|
collectDailyData: function () {
|
|
return this.storage.enqueueTransaction(this._populateCrashCounts.bind(this));
|
|
},
|
|
|
|
_populateCrashCounts: function () {
|
|
this._log.info("Grabbing crash counts from crash manager.");
|
|
let crashCounts = yield this._manager.getCrashCountsByDay();
|
|
|
|
// TODO: CrashManager no longer stores submissions as crashes, but we still
|
|
// want to send the submission data to FHR. As a temporary workaround, we
|
|
// populate |crashCounts| with the submission data to match past behaviour.
|
|
// See bug 1056160.
|
|
let crashes = yield this._manager.getCrashes();
|
|
for (let crash of crashes) {
|
|
for (let [submissionID, submission] of crash.submissions) {
|
|
if (!submission.responseDate) {
|
|
continue;
|
|
}
|
|
|
|
let day = Metrics.dateToDays(submission.responseDate);
|
|
if (!crashCounts.has(day)) {
|
|
crashCounts.set(day, new Map());
|
|
}
|
|
|
|
let succeeded =
|
|
submission.result == this._manager.SUBMISSION_RESULT_OK;
|
|
let type = crash.type + "-submission-" + (succeeded ? "succeeded" :
|
|
"failed");
|
|
|
|
let count = (crashCounts.get(day).get(type) || 0) + 1;
|
|
crashCounts.get(day).set(type, count);
|
|
}
|
|
}
|
|
|
|
let m = this.getMeasurement("crashes", 6);
|
|
let fields = DailyCrashesMeasurement6.prototype.fields;
|
|
|
|
for (let [day, types] of crashCounts) {
|
|
let date = Metrics.daysToDate(day);
|
|
for (let [type, count] of types) {
|
|
if (!(type in fields)) {
|
|
this._log.warn("Unknown crash type encountered: " + type);
|
|
continue;
|
|
}
|
|
|
|
yield m.setDailyLastNumeric(type, count, date);
|
|
}
|
|
}
|
|
},
|
|
});
|
|
|
|
#endif
|
|
|
|
/**
|
|
* Records data from update hotfixes.
|
|
*
|
|
* This measurement has dynamic fields. Field names are of the form
|
|
* <version>.<thing> where <version> is the hotfix version that produced
|
|
* the data. e.g. "v20140527". The sub-version of the hotfix is omitted
|
|
* because hotfixes can go through multiple minor versions during development
|
|
* and we don't want to introduce more fields than necessary. Furthermore,
|
|
* the subsequent dots make parsing field names slightly harder. By stripping,
|
|
* we can just split on the first dot.
|
|
*/
|
|
function UpdateHotfixMeasurement1() {
|
|
Metrics.Measurement.call(this);
|
|
}
|
|
|
|
UpdateHotfixMeasurement1.prototype = Object.freeze({
|
|
__proto__: Metrics.Measurement.prototype,
|
|
|
|
name: "update",
|
|
version: 1,
|
|
|
|
hotfixFieldTypes: {
|
|
"upgradedFrom": Metrics.Storage.FIELD_LAST_TEXT,
|
|
"uninstallReason": Metrics.Storage.FIELD_LAST_TEXT,
|
|
"downloadAttempts": Metrics.Storage.FIELD_LAST_NUMERIC,
|
|
"downloadFailures": Metrics.Storage.FIELD_LAST_NUMERIC,
|
|
"installAttempts": Metrics.Storage.FIELD_LAST_NUMERIC,
|
|
"installFailures": Metrics.Storage.FIELD_LAST_NUMERIC,
|
|
"notificationsShown": Metrics.Storage.FIELD_LAST_NUMERIC,
|
|
},
|
|
|
|
fields: { },
|
|
|
|
// Our fields have dynamic names from the hotfix version that supplied them.
|
|
// We need to override the default behavior to deal with unknown fields.
|
|
shouldIncludeField: function (name) {
|
|
return name.includes(".");
|
|
},
|
|
|
|
fieldType: function (name) {
|
|
for (let known in this.hotfixFieldTypes) {
|
|
if (name.endsWith(known)) {
|
|
return this.hotfixFieldTypes[known];
|
|
}
|
|
}
|
|
|
|
return Metrics.Measurement.prototype.fieldType.call(this, name);
|
|
},
|
|
});
|
|
|
|
this.HotfixProvider = function () {
|
|
Metrics.Provider.call(this);
|
|
};
|
|
|
|
HotfixProvider.prototype = Object.freeze({
|
|
__proto__: Metrics.Provider.prototype,
|
|
|
|
name: "org.mozilla.hotfix",
|
|
measurementTypes: [
|
|
UpdateHotfixMeasurement1,
|
|
],
|
|
|
|
pullOnly: true,
|
|
|
|
collectDailyData: function () {
|
|
return this.storage.enqueueTransaction(this._populateHotfixData.bind(this));
|
|
},
|
|
|
|
_populateHotfixData: function* () {
|
|
let m = this.getMeasurement("update", 1);
|
|
|
|
// The update hotfix retains its JSON state file after uninstall.
|
|
// The initial update hotfix had a hard-coded filename. We treat it
|
|
// specially. Subsequent update hotfixes named their files in a
|
|
// recognizeable pattern so we don't need to update this probe code to
|
|
// know about them.
|
|
let files = [
|
|
["v20140527", OS.Path.join(OS.Constants.Path.profileDir,
|
|
"hotfix.v20140527.01.json")],
|
|
];
|
|
|
|
let it = new OS.File.DirectoryIterator(OS.Constants.Path.profileDir);
|
|
try {
|
|
yield it.forEach((e, index, it) => {
|
|
let m = e.name.match(/^updateHotfix\.([a-zA-Z0-9]+)\.json$/);
|
|
if (m) {
|
|
files.push([m[1], e.path]);
|
|
}
|
|
});
|
|
} finally {
|
|
it.close();
|
|
}
|
|
|
|
let decoder = new TextDecoder();
|
|
for (let e of files) {
|
|
let [version, path] = e;
|
|
let p;
|
|
try {
|
|
let data = yield OS.File.read(path);
|
|
p = JSON.parse(decoder.decode(data));
|
|
} catch (ex if ex instanceof OS.File.Error && ex.becauseNoSuchFile) {
|
|
continue;
|
|
} catch (ex) {
|
|
this._log.warn("Error loading update hotfix payload: " + ex.message);
|
|
}
|
|
|
|
// Wrap just in case.
|
|
try {
|
|
for (let k in m.hotfixFieldTypes) {
|
|
if (!(k in p)) {
|
|
continue;
|
|
}
|
|
|
|
let value = p[k];
|
|
if (value === null && k == "uninstallReason") {
|
|
value = "STILL_INSTALLED";
|
|
}
|
|
|
|
let field = version + "." + k;
|
|
let fieldType;
|
|
let storageOp;
|
|
switch (typeof(value)) {
|
|
case "string":
|
|
fieldType = this.storage.FIELD_LAST_TEXT;
|
|
storageOp = "setLastTextFromFieldID";
|
|
break;
|
|
case "number":
|
|
fieldType = this.storage.FIELD_LAST_NUMERIC;
|
|
storageOp = "setLastNumericFromFieldID";
|
|
break;
|
|
default:
|
|
this._log.warn("Unknown value in hotfix state: " + k + "=" + value);
|
|
continue;
|
|
}
|
|
|
|
if (this.storage.hasFieldFromMeasurement(m.id, field, fieldType)) {
|
|
let fieldID = this.storage.fieldIDFromMeasurement(m.id, field);
|
|
yield this.storage[storageOp](fieldID, value);
|
|
} else {
|
|
let fieldID = yield this.storage.registerField(m.id, field,
|
|
fieldType);
|
|
yield this.storage[storageOp](fieldID, value);
|
|
}
|
|
}
|
|
|
|
} catch (ex) {
|
|
this._log.warn("Error processing update hotfix data: " + ex);
|
|
}
|
|
}
|
|
},
|
|
});
|
|
|
|
/**
|
|
* Holds basic statistics about the Places database.
|
|
*/
|
|
function PlacesMeasurement() {
|
|
Metrics.Measurement.call(this);
|
|
}
|
|
|
|
PlacesMeasurement.prototype = Object.freeze({
|
|
__proto__: Metrics.Measurement.prototype,
|
|
|
|
name: "places",
|
|
version: 1,
|
|
|
|
fields: {
|
|
pages: DAILY_LAST_NUMERIC_FIELD,
|
|
bookmarks: DAILY_LAST_NUMERIC_FIELD,
|
|
},
|
|
});
|
|
|
|
|
|
/**
|
|
* Collects information about Places.
|
|
*/
|
|
this.PlacesProvider = function () {
|
|
Metrics.Provider.call(this);
|
|
};
|
|
|
|
PlacesProvider.prototype = Object.freeze({
|
|
__proto__: Metrics.Provider.prototype,
|
|
|
|
name: "org.mozilla.places",
|
|
|
|
measurementTypes: [PlacesMeasurement],
|
|
|
|
collectDailyData: function () {
|
|
return this.storage.enqueueTransaction(this._collectData.bind(this));
|
|
},
|
|
|
|
_collectData: function () {
|
|
let now = new Date();
|
|
let data = yield this._getDailyValues();
|
|
|
|
let m = this.getMeasurement("places", 1);
|
|
|
|
yield m.setDailyLastNumeric("pages", data.PLACES_PAGES_COUNT);
|
|
yield m.setDailyLastNumeric("bookmarks", data.PLACES_BOOKMARKS_COUNT);
|
|
},
|
|
|
|
_getDailyValues: function () {
|
|
let deferred = Promise.defer();
|
|
|
|
PlacesDBUtils.telemetry(null, function onResult(data) {
|
|
deferred.resolve(data);
|
|
});
|
|
|
|
return deferred.promise;
|
|
},
|
|
});
|
|
|
|
function SearchCountMeasurement1() {
|
|
Metrics.Measurement.call(this);
|
|
}
|
|
|
|
SearchCountMeasurement1.prototype = Object.freeze({
|
|
__proto__: Metrics.Measurement.prototype,
|
|
|
|
name: "counts",
|
|
version: 1,
|
|
|
|
// We only record searches for search engines that have partner agreements
|
|
// with Mozilla.
|
|
fields: {
|
|
"amazon.com.abouthome": DAILY_COUNTER_FIELD,
|
|
"amazon.com.contextmenu": DAILY_COUNTER_FIELD,
|
|
"amazon.com.searchbar": DAILY_COUNTER_FIELD,
|
|
"amazon.com.urlbar": DAILY_COUNTER_FIELD,
|
|
"bing.abouthome": DAILY_COUNTER_FIELD,
|
|
"bing.contextmenu": DAILY_COUNTER_FIELD,
|
|
"bing.searchbar": DAILY_COUNTER_FIELD,
|
|
"bing.urlbar": DAILY_COUNTER_FIELD,
|
|
"google.abouthome": DAILY_COUNTER_FIELD,
|
|
"google.contextmenu": DAILY_COUNTER_FIELD,
|
|
"google.searchbar": DAILY_COUNTER_FIELD,
|
|
"google.urlbar": DAILY_COUNTER_FIELD,
|
|
"yahoo.abouthome": DAILY_COUNTER_FIELD,
|
|
"yahoo.contextmenu": DAILY_COUNTER_FIELD,
|
|
"yahoo.searchbar": DAILY_COUNTER_FIELD,
|
|
"yahoo.urlbar": DAILY_COUNTER_FIELD,
|
|
"other.abouthome": DAILY_COUNTER_FIELD,
|
|
"other.contextmenu": DAILY_COUNTER_FIELD,
|
|
"other.searchbar": DAILY_COUNTER_FIELD,
|
|
"other.urlbar": DAILY_COUNTER_FIELD,
|
|
},
|
|
});
|
|
|
|
/**
|
|
* Records search counts per day per engine and where search initiated.
|
|
*
|
|
* We want to record granular details for individual locale-specific search
|
|
* providers, but only if they're Mozilla partners. In order to do this, we
|
|
* track the nsISearchEngine identifier, which denotes shipped search engines,
|
|
* and intersect those with our partner list.
|
|
*
|
|
* We don't use the search engine name directly, because it is shared across
|
|
* locales; e.g., eBay-de and eBay both share the name "eBay".
|
|
*/
|
|
function SearchCountMeasurementBase() {
|
|
this._fieldSpecs = {};
|
|
Metrics.Measurement.call(this);
|
|
}
|
|
|
|
SearchCountMeasurementBase.prototype = Object.freeze({
|
|
__proto__: Metrics.Measurement.prototype,
|
|
|
|
|
|
// Our fields are dynamic.
|
|
get fields() {
|
|
return this._fieldSpecs;
|
|
},
|
|
|
|
/**
|
|
* Override the default behavior: serializers should include every counter
|
|
* field from the DB, even if we don't currently have it registered.
|
|
*
|
|
* Do this so we don't have to register several hundred fields to match
|
|
* various Firefox locales.
|
|
*
|
|
* We use the "provider.type" syntax as a rudimentary check for validity.
|
|
*
|
|
* We trust that measurement versioning is sufficient to exclude old provider
|
|
* data.
|
|
*/
|
|
shouldIncludeField: function (name) {
|
|
return name.includes(".");
|
|
},
|
|
|
|
/**
|
|
* The measurement type mechanism doesn't introspect the DB. Override it
|
|
* so that we can assume all unknown fields are counters.
|
|
*/
|
|
fieldType: function (name) {
|
|
if (name in this.fields) {
|
|
return this.fields[name].type;
|
|
}
|
|
|
|
// Default to a counter.
|
|
return Metrics.Storage.FIELD_DAILY_COUNTER;
|
|
},
|
|
|
|
SOURCES: [
|
|
"abouthome",
|
|
"contextmenu",
|
|
"newtab",
|
|
"searchbar",
|
|
"urlbar",
|
|
],
|
|
});
|
|
|
|
function SearchCountMeasurement2() {
|
|
SearchCountMeasurementBase.call(this);
|
|
}
|
|
|
|
SearchCountMeasurement2.prototype = Object.freeze({
|
|
__proto__: SearchCountMeasurementBase.prototype,
|
|
name: "counts",
|
|
version: 2,
|
|
});
|
|
|
|
function SearchCountMeasurement3() {
|
|
SearchCountMeasurementBase.call(this);
|
|
}
|
|
|
|
SearchCountMeasurement3.prototype = Object.freeze({
|
|
__proto__: SearchCountMeasurementBase.prototype,
|
|
name: "counts",
|
|
version: 3,
|
|
|
|
getEngines: function () {
|
|
return Services.search.getEngines();
|
|
},
|
|
|
|
getEngineID: function (engine) {
|
|
if (!engine) {
|
|
return "other";
|
|
}
|
|
if (engine.identifier) {
|
|
return engine.identifier;
|
|
}
|
|
return "other-" + engine.name;
|
|
},
|
|
});
|
|
|
|
function SearchEnginesMeasurement1() {
|
|
Metrics.Measurement.call(this);
|
|
}
|
|
|
|
SearchEnginesMeasurement1.prototype = Object.freeze({
|
|
__proto__: Metrics.Measurement.prototype,
|
|
|
|
name: "engines",
|
|
version: 2,
|
|
|
|
fields: {
|
|
default: DAILY_LAST_TEXT_FIELD,
|
|
cohort: DAILY_LAST_TEXT_FIELD,
|
|
},
|
|
});
|
|
|
|
this.SearchesProvider = function () {
|
|
Metrics.Provider.call(this);
|
|
|
|
this._prefs = new Preferences({defaultBranch: null});
|
|
};
|
|
|
|
this.SearchesProvider.prototype = Object.freeze({
|
|
__proto__: Metrics.Provider.prototype,
|
|
|
|
name: "org.mozilla.searches",
|
|
measurementTypes: [
|
|
SearchCountMeasurement1,
|
|
SearchCountMeasurement2,
|
|
SearchCountMeasurement3,
|
|
SearchEnginesMeasurement1,
|
|
],
|
|
|
|
/**
|
|
* Initialize the search service before our measurements are touched.
|
|
*/
|
|
preInit: function (storage) {
|
|
// Initialize search service.
|
|
let deferred = Promise.defer();
|
|
Services.search.init(function onInitComplete () {
|
|
deferred.resolve();
|
|
});
|
|
return deferred.promise;
|
|
},
|
|
|
|
collectDailyData: function () {
|
|
return this.storage.enqueueTransaction(function getDaily() {
|
|
let m = this.getMeasurement(SearchEnginesMeasurement1.prototype.name,
|
|
SearchEnginesMeasurement1.prototype.version);
|
|
|
|
let engine;
|
|
try {
|
|
engine = Services.search.defaultEngine;
|
|
} catch (e) {}
|
|
let name;
|
|
|
|
if (!engine) {
|
|
name = "NONE";
|
|
} else if (engine.identifier) {
|
|
name = engine.identifier;
|
|
} else if (engine.name) {
|
|
name = "other-" + engine.name;
|
|
} else {
|
|
name = "UNDEFINED";
|
|
}
|
|
|
|
yield m.setDailyLastText("default", name);
|
|
|
|
if (Services.prefs.prefHasUserValue(SEARCH_COHORT_PREF))
|
|
yield m.setDailyLastText("cohort", Services.prefs.getCharPref(SEARCH_COHORT_PREF));
|
|
}.bind(this));
|
|
},
|
|
|
|
/**
|
|
* Record that a search occurred.
|
|
*
|
|
* @param engine
|
|
* (nsISearchEngine) The search engine used.
|
|
* @param source
|
|
* (string) Where the search was initiated from. Must be one of the
|
|
* SearchCountMeasurement2.SOURCES values.
|
|
*
|
|
* @return Promise<>
|
|
* The promise is resolved when the storage operation completes.
|
|
*/
|
|
recordSearch: function (engine, source) {
|
|
let m = this.getMeasurement("counts", 3);
|
|
|
|
if (m.SOURCES.indexOf(source) == -1) {
|
|
throw new Error("Unknown source for search: " + source);
|
|
}
|
|
|
|
let field = m.getEngineID(engine) + "." + source;
|
|
if (this.storage.hasFieldFromMeasurement(m.id, field,
|
|
this.storage.FIELD_DAILY_COUNTER)) {
|
|
let fieldID = this.storage.fieldIDFromMeasurement(m.id, field);
|
|
return this.enqueueStorageOperation(function recordSearchKnownField() {
|
|
return this.storage.incrementDailyCounterFromFieldID(fieldID);
|
|
}.bind(this));
|
|
}
|
|
|
|
// Otherwise, we first need to create the field.
|
|
return this.enqueueStorageOperation(function recordFieldAndSearch() {
|
|
// This function has to return a promise.
|
|
return Task.spawn(function () {
|
|
let fieldID = yield this.storage.registerField(m.id, field,
|
|
this.storage.FIELD_DAILY_COUNTER);
|
|
yield this.storage.incrementDailyCounterFromFieldID(fieldID);
|
|
}.bind(this));
|
|
}.bind(this));
|
|
},
|
|
});
|
|
|
|
function HealthReportSubmissionMeasurement1() {
|
|
Metrics.Measurement.call(this);
|
|
}
|
|
|
|
HealthReportSubmissionMeasurement1.prototype = Object.freeze({
|
|
__proto__: Metrics.Measurement.prototype,
|
|
|
|
name: "submissions",
|
|
version: 1,
|
|
|
|
fields: {
|
|
firstDocumentUploadAttempt: DAILY_COUNTER_FIELD,
|
|
continuationUploadAttempt: DAILY_COUNTER_FIELD,
|
|
uploadSuccess: DAILY_COUNTER_FIELD,
|
|
uploadTransportFailure: DAILY_COUNTER_FIELD,
|
|
uploadServerFailure: DAILY_COUNTER_FIELD,
|
|
uploadClientFailure: DAILY_COUNTER_FIELD,
|
|
},
|
|
});
|
|
|
|
function HealthReportSubmissionMeasurement2() {
|
|
Metrics.Measurement.call(this);
|
|
}
|
|
|
|
HealthReportSubmissionMeasurement2.prototype = Object.freeze({
|
|
__proto__: Metrics.Measurement.prototype,
|
|
|
|
name: "submissions",
|
|
version: 2,
|
|
|
|
fields: {
|
|
firstDocumentUploadAttempt: DAILY_COUNTER_FIELD,
|
|
continuationUploadAttempt: DAILY_COUNTER_FIELD,
|
|
uploadSuccess: DAILY_COUNTER_FIELD,
|
|
uploadTransportFailure: DAILY_COUNTER_FIELD,
|
|
uploadServerFailure: DAILY_COUNTER_FIELD,
|
|
uploadClientFailure: DAILY_COUNTER_FIELD,
|
|
uploadAlreadyInProgress: DAILY_COUNTER_FIELD,
|
|
},
|
|
});
|
|
|
|
this.HealthReportProvider = function () {
|
|
Metrics.Provider.call(this);
|
|
}
|
|
|
|
HealthReportProvider.prototype = Object.freeze({
|
|
__proto__: Metrics.Provider.prototype,
|
|
|
|
name: "org.mozilla.healthreport",
|
|
|
|
measurementTypes: [
|
|
HealthReportSubmissionMeasurement1,
|
|
HealthReportSubmissionMeasurement2,
|
|
],
|
|
|
|
recordEvent: function (event, date=new Date()) {
|
|
let m = this.getMeasurement("submissions", 2);
|
|
return this.enqueueStorageOperation(function recordCounter() {
|
|
return m.incrementDailyCounter(event, date);
|
|
});
|
|
},
|
|
});
|