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