1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/services/healthreport/providers.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,1446 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +/** 1.9 + * This file contains metrics data providers for the Firefox Health 1.10 + * Report. Ideally each provider in this file exists in separate modules 1.11 + * and lives close to the code it is querying. However, because of the 1.12 + * overhead of JS compartments (which are created for each module), we 1.13 + * currently have all the code in one file. When the overhead of 1.14 + * compartments reaches a reasonable level, this file should be split 1.15 + * up. 1.16 + */ 1.17 + 1.18 +"use strict"; 1.19 + 1.20 +#ifndef MERGED_COMPARTMENT 1.21 + 1.22 +this.EXPORTED_SYMBOLS = [ 1.23 + "AddonsProvider", 1.24 + "AppInfoProvider", 1.25 +#ifdef MOZ_CRASHREPORTER 1.26 + "CrashesProvider", 1.27 +#endif 1.28 + "HealthReportProvider", 1.29 + "PlacesProvider", 1.30 + "SearchesProvider", 1.31 + "SessionsProvider", 1.32 + "SysInfoProvider", 1.33 +]; 1.34 + 1.35 +const {classes: Cc, interfaces: Ci, utils: Cu} = Components; 1.36 + 1.37 +Cu.import("resource://gre/modules/Metrics.jsm"); 1.38 + 1.39 +#endif 1.40 + 1.41 +Cu.import("resource://gre/modules/Promise.jsm"); 1.42 +Cu.import("resource://gre/modules/osfile.jsm"); 1.43 +Cu.import("resource://gre/modules/Preferences.jsm"); 1.44 +Cu.import("resource://gre/modules/Services.jsm"); 1.45 +Cu.import("resource://gre/modules/Task.jsm"); 1.46 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.47 +Cu.import("resource://services-common/utils.js"); 1.48 + 1.49 +XPCOMUtils.defineLazyModuleGetter(this, "AddonManager", 1.50 + "resource://gre/modules/AddonManager.jsm"); 1.51 +XPCOMUtils.defineLazyModuleGetter(this, "UpdateChannel", 1.52 + "resource://gre/modules/UpdateChannel.jsm"); 1.53 +XPCOMUtils.defineLazyModuleGetter(this, "PlacesDBUtils", 1.54 + "resource://gre/modules/PlacesDBUtils.jsm"); 1.55 + 1.56 + 1.57 +const LAST_NUMERIC_FIELD = {type: Metrics.Storage.FIELD_LAST_NUMERIC}; 1.58 +const LAST_TEXT_FIELD = {type: Metrics.Storage.FIELD_LAST_TEXT}; 1.59 +const DAILY_DISCRETE_NUMERIC_FIELD = {type: Metrics.Storage.FIELD_DAILY_DISCRETE_NUMERIC}; 1.60 +const DAILY_LAST_NUMERIC_FIELD = {type: Metrics.Storage.FIELD_DAILY_LAST_NUMERIC}; 1.61 +const DAILY_LAST_TEXT_FIELD = {type: Metrics.Storage.FIELD_DAILY_LAST_TEXT}; 1.62 +const DAILY_COUNTER_FIELD = {type: Metrics.Storage.FIELD_DAILY_COUNTER}; 1.63 + 1.64 +const TELEMETRY_PREF = "toolkit.telemetry.enabled"; 1.65 + 1.66 +function isTelemetryEnabled(prefs) { 1.67 + return prefs.get(TELEMETRY_PREF, false); 1.68 +} 1.69 + 1.70 +/** 1.71 + * Represents basic application state. 1.72 + * 1.73 + * This is roughly a union of nsIXULAppInfo, nsIXULRuntime, with a few extra 1.74 + * pieces thrown in. 1.75 + */ 1.76 +function AppInfoMeasurement() { 1.77 + Metrics.Measurement.call(this); 1.78 +} 1.79 + 1.80 +AppInfoMeasurement.prototype = Object.freeze({ 1.81 + __proto__: Metrics.Measurement.prototype, 1.82 + 1.83 + name: "appinfo", 1.84 + version: 2, 1.85 + 1.86 + fields: { 1.87 + vendor: LAST_TEXT_FIELD, 1.88 + name: LAST_TEXT_FIELD, 1.89 + id: LAST_TEXT_FIELD, 1.90 + version: LAST_TEXT_FIELD, 1.91 + appBuildID: LAST_TEXT_FIELD, 1.92 + platformVersion: LAST_TEXT_FIELD, 1.93 + platformBuildID: LAST_TEXT_FIELD, 1.94 + os: LAST_TEXT_FIELD, 1.95 + xpcomabi: LAST_TEXT_FIELD, 1.96 + updateChannel: LAST_TEXT_FIELD, 1.97 + distributionID: LAST_TEXT_FIELD, 1.98 + distributionVersion: LAST_TEXT_FIELD, 1.99 + hotfixVersion: LAST_TEXT_FIELD, 1.100 + locale: LAST_TEXT_FIELD, 1.101 + isDefaultBrowser: {type: Metrics.Storage.FIELD_DAILY_LAST_NUMERIC}, 1.102 + isTelemetryEnabled: {type: Metrics.Storage.FIELD_DAILY_LAST_NUMERIC}, 1.103 + isBlocklistEnabled: {type: Metrics.Storage.FIELD_DAILY_LAST_NUMERIC}, 1.104 + }, 1.105 +}); 1.106 + 1.107 +/** 1.108 + * Legacy version of app info before Telemetry was added. 1.109 + * 1.110 + * The "last" fields have all been removed. We only report the longitudinal 1.111 + * field. 1.112 + */ 1.113 +function AppInfoMeasurement1() { 1.114 + Metrics.Measurement.call(this); 1.115 +} 1.116 + 1.117 +AppInfoMeasurement1.prototype = Object.freeze({ 1.118 + __proto__: Metrics.Measurement.prototype, 1.119 + 1.120 + name: "appinfo", 1.121 + version: 1, 1.122 + 1.123 + fields: { 1.124 + isDefaultBrowser: {type: Metrics.Storage.FIELD_DAILY_LAST_NUMERIC}, 1.125 + }, 1.126 +}); 1.127 + 1.128 + 1.129 +function AppVersionMeasurement1() { 1.130 + Metrics.Measurement.call(this); 1.131 +} 1.132 + 1.133 +AppVersionMeasurement1.prototype = Object.freeze({ 1.134 + __proto__: Metrics.Measurement.prototype, 1.135 + 1.136 + name: "versions", 1.137 + version: 1, 1.138 + 1.139 + fields: { 1.140 + version: {type: Metrics.Storage.FIELD_DAILY_DISCRETE_TEXT}, 1.141 + }, 1.142 +}); 1.143 + 1.144 +// Version 2 added the build ID. 1.145 +function AppVersionMeasurement2() { 1.146 + Metrics.Measurement.call(this); 1.147 +} 1.148 + 1.149 +AppVersionMeasurement2.prototype = Object.freeze({ 1.150 + __proto__: Metrics.Measurement.prototype, 1.151 + 1.152 + name: "versions", 1.153 + version: 2, 1.154 + 1.155 + fields: { 1.156 + appVersion: {type: Metrics.Storage.FIELD_DAILY_DISCRETE_TEXT}, 1.157 + platformVersion: {type: Metrics.Storage.FIELD_DAILY_DISCRETE_TEXT}, 1.158 + appBuildID: {type: Metrics.Storage.FIELD_DAILY_DISCRETE_TEXT}, 1.159 + platformBuildID: {type: Metrics.Storage.FIELD_DAILY_DISCRETE_TEXT}, 1.160 + }, 1.161 +}); 1.162 + 1.163 +/** 1.164 + * Holds data on the application update functionality. 1.165 + */ 1.166 +function AppUpdateMeasurement1() { 1.167 + Metrics.Measurement.call(this); 1.168 +} 1.169 + 1.170 +AppUpdateMeasurement1.prototype = Object.freeze({ 1.171 + __proto__: Metrics.Measurement.prototype, 1.172 + 1.173 + name: "update", 1.174 + version: 1, 1.175 + 1.176 + fields: { 1.177 + enabled: {type: Metrics.Storage.FIELD_DAILY_LAST_NUMERIC}, 1.178 + autoDownload: {type: Metrics.Storage.FIELD_DAILY_LAST_NUMERIC}, 1.179 + }, 1.180 +}); 1.181 + 1.182 +this.AppInfoProvider = function AppInfoProvider() { 1.183 + Metrics.Provider.call(this); 1.184 + 1.185 + this._prefs = new Preferences({defaultBranch: null}); 1.186 +} 1.187 +AppInfoProvider.prototype = Object.freeze({ 1.188 + __proto__: Metrics.Provider.prototype, 1.189 + 1.190 + name: "org.mozilla.appInfo", 1.191 + 1.192 + measurementTypes: [ 1.193 + AppInfoMeasurement, 1.194 + AppInfoMeasurement1, 1.195 + AppUpdateMeasurement1, 1.196 + AppVersionMeasurement1, 1.197 + AppVersionMeasurement2, 1.198 + ], 1.199 + 1.200 + pullOnly: true, 1.201 + 1.202 + appInfoFields: { 1.203 + // From nsIXULAppInfo. 1.204 + vendor: "vendor", 1.205 + name: "name", 1.206 + id: "ID", 1.207 + version: "version", 1.208 + appBuildID: "appBuildID", 1.209 + platformVersion: "platformVersion", 1.210 + platformBuildID: "platformBuildID", 1.211 + 1.212 + // From nsIXULRuntime. 1.213 + os: "OS", 1.214 + xpcomabi: "XPCOMABI", 1.215 + }, 1.216 + 1.217 + postInit: function () { 1.218 + return Task.spawn(this._postInit.bind(this)); 1.219 + }, 1.220 + 1.221 + _postInit: function () { 1.222 + let recordEmptyAppInfo = function () { 1.223 + this._setCurrentAppVersion(""); 1.224 + this._setCurrentPlatformVersion(""); 1.225 + this._setCurrentAppBuildID(""); 1.226 + return this._setCurrentPlatformBuildID(""); 1.227 + }.bind(this); 1.228 + 1.229 + // Services.appInfo should always be defined for any reasonably behaving 1.230 + // Gecko app. If it isn't, we insert a empty string sentinel value. 1.231 + let ai; 1.232 + try { 1.233 + ai = Services.appinfo; 1.234 + } catch (ex) { 1.235 + this._log.error("Could not obtain Services.appinfo: " + 1.236 + CommonUtils.exceptionStr(ex)); 1.237 + yield recordEmptyAppInfo(); 1.238 + return; 1.239 + } 1.240 + 1.241 + if (!ai) { 1.242 + this._log.error("Services.appinfo is unavailable."); 1.243 + yield recordEmptyAppInfo(); 1.244 + return; 1.245 + } 1.246 + 1.247 + let currentAppVersion = ai.version; 1.248 + let currentPlatformVersion = ai.platformVersion; 1.249 + let currentAppBuildID = ai.appBuildID; 1.250 + let currentPlatformBuildID = ai.platformBuildID; 1.251 + 1.252 + // State's name doesn't contain "app" for historical compatibility. 1.253 + let lastAppVersion = yield this.getState("lastVersion"); 1.254 + let lastPlatformVersion = yield this.getState("lastPlatformVersion"); 1.255 + let lastAppBuildID = yield this.getState("lastAppBuildID"); 1.256 + let lastPlatformBuildID = yield this.getState("lastPlatformBuildID"); 1.257 + 1.258 + if (currentAppVersion != lastAppVersion) { 1.259 + yield this._setCurrentAppVersion(currentAppVersion); 1.260 + } 1.261 + 1.262 + if (currentPlatformVersion != lastPlatformVersion) { 1.263 + yield this._setCurrentPlatformVersion(currentPlatformVersion); 1.264 + } 1.265 + 1.266 + if (currentAppBuildID != lastAppBuildID) { 1.267 + yield this._setCurrentAppBuildID(currentAppBuildID); 1.268 + } 1.269 + 1.270 + if (currentPlatformBuildID != lastPlatformBuildID) { 1.271 + yield this._setCurrentPlatformBuildID(currentPlatformBuildID); 1.272 + } 1.273 + }, 1.274 + 1.275 + _setCurrentAppVersion: function (version) { 1.276 + this._log.info("Recording new application version: " + version); 1.277 + let m = this.getMeasurement("versions", 2); 1.278 + m.addDailyDiscreteText("appVersion", version); 1.279 + 1.280 + // "app" not encoded in key for historical compatibility. 1.281 + return this.setState("lastVersion", version); 1.282 + }, 1.283 + 1.284 + _setCurrentPlatformVersion: function (version) { 1.285 + this._log.info("Recording new platform version: " + version); 1.286 + let m = this.getMeasurement("versions", 2); 1.287 + m.addDailyDiscreteText("platformVersion", version); 1.288 + return this.setState("lastPlatformVersion", version); 1.289 + }, 1.290 + 1.291 + _setCurrentAppBuildID: function (build) { 1.292 + this._log.info("Recording new application build ID: " + build); 1.293 + let m = this.getMeasurement("versions", 2); 1.294 + m.addDailyDiscreteText("appBuildID", build); 1.295 + return this.setState("lastAppBuildID", build); 1.296 + }, 1.297 + 1.298 + _setCurrentPlatformBuildID: function (build) { 1.299 + this._log.info("Recording new platform build ID: " + build); 1.300 + let m = this.getMeasurement("versions", 2); 1.301 + m.addDailyDiscreteText("platformBuildID", build); 1.302 + return this.setState("lastPlatformBuildID", build); 1.303 + }, 1.304 + 1.305 + 1.306 + collectConstantData: function () { 1.307 + return this.storage.enqueueTransaction(this._populateConstants.bind(this)); 1.308 + }, 1.309 + 1.310 + _populateConstants: function () { 1.311 + let m = this.getMeasurement(AppInfoMeasurement.prototype.name, 1.312 + AppInfoMeasurement.prototype.version); 1.313 + 1.314 + let ai; 1.315 + try { 1.316 + ai = Services.appinfo; 1.317 + } catch (ex) { 1.318 + this._log.warn("Could not obtain Services.appinfo: " + 1.319 + CommonUtils.exceptionStr(ex)); 1.320 + throw ex; 1.321 + } 1.322 + 1.323 + if (!ai) { 1.324 + this._log.warn("Services.appinfo is unavailable."); 1.325 + throw ex; 1.326 + } 1.327 + 1.328 + for (let [k, v] in Iterator(this.appInfoFields)) { 1.329 + try { 1.330 + yield m.setLastText(k, ai[v]); 1.331 + } catch (ex) { 1.332 + this._log.warn("Error obtaining Services.appinfo." + v); 1.333 + } 1.334 + } 1.335 + 1.336 + try { 1.337 + yield m.setLastText("updateChannel", UpdateChannel.get()); 1.338 + } catch (ex) { 1.339 + this._log.warn("Could not obtain update channel: " + 1.340 + CommonUtils.exceptionStr(ex)); 1.341 + } 1.342 + 1.343 + yield m.setLastText("distributionID", this._prefs.get("distribution.id", "")); 1.344 + yield m.setLastText("distributionVersion", this._prefs.get("distribution.version", "")); 1.345 + yield m.setLastText("hotfixVersion", this._prefs.get("extensions.hotfix.lastVersion", "")); 1.346 + 1.347 + try { 1.348 + let locale = Cc["@mozilla.org/chrome/chrome-registry;1"] 1.349 + .getService(Ci.nsIXULChromeRegistry) 1.350 + .getSelectedLocale("global"); 1.351 + yield m.setLastText("locale", locale); 1.352 + } catch (ex) { 1.353 + this._log.warn("Could not obtain application locale: " + 1.354 + CommonUtils.exceptionStr(ex)); 1.355 + } 1.356 + 1.357 + // FUTURE this should be retrieved periodically or at upload time. 1.358 + yield this._recordIsTelemetryEnabled(m); 1.359 + yield this._recordIsBlocklistEnabled(m); 1.360 + yield this._recordDefaultBrowser(m); 1.361 + }, 1.362 + 1.363 + _recordIsTelemetryEnabled: function (m) { 1.364 + let enabled = isTelemetryEnabled(this._prefs); 1.365 + this._log.debug("Recording telemetry enabled (" + TELEMETRY_PREF + "): " + enabled); 1.366 + yield m.setDailyLastNumeric("isTelemetryEnabled", enabled ? 1 : 0); 1.367 + }, 1.368 + 1.369 + _recordIsBlocklistEnabled: function (m) { 1.370 + let enabled = this._prefs.get("extensions.blocklist.enabled", false); 1.371 + this._log.debug("Recording blocklist enabled: " + enabled); 1.372 + yield m.setDailyLastNumeric("isBlocklistEnabled", enabled ? 1 : 0); 1.373 + }, 1.374 + 1.375 + _recordDefaultBrowser: function (m) { 1.376 + let shellService; 1.377 + try { 1.378 + shellService = Cc["@mozilla.org/browser/shell-service;1"] 1.379 + .getService(Ci.nsIShellService); 1.380 + } catch (ex) { 1.381 + this._log.warn("Could not obtain shell service: " + 1.382 + CommonUtils.exceptionStr(ex)); 1.383 + } 1.384 + 1.385 + let isDefault = -1; 1.386 + 1.387 + if (shellService) { 1.388 + try { 1.389 + // This uses the same set of flags used by the pref pane. 1.390 + isDefault = shellService.isDefaultBrowser(false, true) ? 1 : 0; 1.391 + } catch (ex) { 1.392 + this._log.warn("Could not determine if default browser: " + 1.393 + CommonUtils.exceptionStr(ex)); 1.394 + } 1.395 + } 1.396 + 1.397 + return m.setDailyLastNumeric("isDefaultBrowser", isDefault); 1.398 + }, 1.399 + 1.400 + collectDailyData: function () { 1.401 + return this.storage.enqueueTransaction(function getDaily() { 1.402 + let m = this.getMeasurement(AppUpdateMeasurement1.prototype.name, 1.403 + AppUpdateMeasurement1.prototype.version); 1.404 + 1.405 + let enabled = this._prefs.get("app.update.enabled", false); 1.406 + yield m.setDailyLastNumeric("enabled", enabled ? 1 : 0); 1.407 + 1.408 + let auto = this._prefs.get("app.update.auto", false); 1.409 + yield m.setDailyLastNumeric("autoDownload", auto ? 1 : 0); 1.410 + }.bind(this)); 1.411 + }, 1.412 +}); 1.413 + 1.414 + 1.415 +function SysInfoMeasurement() { 1.416 + Metrics.Measurement.call(this); 1.417 +} 1.418 + 1.419 +SysInfoMeasurement.prototype = Object.freeze({ 1.420 + __proto__: Metrics.Measurement.prototype, 1.421 + 1.422 + name: "sysinfo", 1.423 + version: 2, 1.424 + 1.425 + fields: { 1.426 + cpuCount: {type: Metrics.Storage.FIELD_LAST_NUMERIC}, 1.427 + memoryMB: {type: Metrics.Storage.FIELD_LAST_NUMERIC}, 1.428 + manufacturer: LAST_TEXT_FIELD, 1.429 + device: LAST_TEXT_FIELD, 1.430 + hardware: LAST_TEXT_FIELD, 1.431 + name: LAST_TEXT_FIELD, 1.432 + version: LAST_TEXT_FIELD, 1.433 + architecture: LAST_TEXT_FIELD, 1.434 + isWow64: LAST_NUMERIC_FIELD, 1.435 + }, 1.436 +}); 1.437 + 1.438 + 1.439 +this.SysInfoProvider = function SysInfoProvider() { 1.440 + Metrics.Provider.call(this); 1.441 +}; 1.442 + 1.443 +SysInfoProvider.prototype = Object.freeze({ 1.444 + __proto__: Metrics.Provider.prototype, 1.445 + 1.446 + name: "org.mozilla.sysinfo", 1.447 + 1.448 + measurementTypes: [SysInfoMeasurement], 1.449 + 1.450 + pullOnly: true, 1.451 + 1.452 + sysInfoFields: { 1.453 + cpucount: "cpuCount", 1.454 + memsize: "memoryMB", 1.455 + manufacturer: "manufacturer", 1.456 + device: "device", 1.457 + hardware: "hardware", 1.458 + name: "name", 1.459 + version: "version", 1.460 + arch: "architecture", 1.461 + isWow64: "isWow64", 1.462 + }, 1.463 + 1.464 + collectConstantData: function () { 1.465 + return this.storage.enqueueTransaction(this._populateConstants.bind(this)); 1.466 + }, 1.467 + 1.468 + _populateConstants: function () { 1.469 + let m = this.getMeasurement(SysInfoMeasurement.prototype.name, 1.470 + SysInfoMeasurement.prototype.version); 1.471 + 1.472 + let si = Cc["@mozilla.org/system-info;1"] 1.473 + .getService(Ci.nsIPropertyBag2); 1.474 + 1.475 + for (let [k, v] in Iterator(this.sysInfoFields)) { 1.476 + try { 1.477 + if (!si.hasKey(k)) { 1.478 + this._log.debug("Property not available: " + k); 1.479 + continue; 1.480 + } 1.481 + 1.482 + let value = si.getProperty(k); 1.483 + let method = "setLastText"; 1.484 + 1.485 + if (["cpucount", "memsize"].indexOf(k) != -1) { 1.486 + let converted = parseInt(value, 10); 1.487 + if (Number.isNaN(converted)) { 1.488 + continue; 1.489 + } 1.490 + 1.491 + value = converted; 1.492 + method = "setLastNumeric"; 1.493 + } 1.494 + 1.495 + switch (k) { 1.496 + case "memsize": 1.497 + // Round memory to mebibytes. 1.498 + value = Math.round(value / 1048576); 1.499 + break; 1.500 + case "isWow64": 1.501 + // Property is only present on Windows. hasKey() skipping from 1.502 + // above ensures undefined or null doesn't creep in here. 1.503 + value = value ? 1 : 0; 1.504 + method = "setLastNumeric"; 1.505 + break; 1.506 + } 1.507 + 1.508 + yield m[method](v, value); 1.509 + } catch (ex) { 1.510 + this._log.warn("Error obtaining system info field: " + k + " " + 1.511 + CommonUtils.exceptionStr(ex)); 1.512 + } 1.513 + } 1.514 + }, 1.515 +}); 1.516 + 1.517 + 1.518 +/** 1.519 + * Holds information about the current/active session. 1.520 + * 1.521 + * The fields within the current session are moved to daily session fields when 1.522 + * the application is shut down. 1.523 + * 1.524 + * This measurement is backed by the SessionRecorder, not the database. 1.525 + */ 1.526 +function CurrentSessionMeasurement() { 1.527 + Metrics.Measurement.call(this); 1.528 +} 1.529 + 1.530 +CurrentSessionMeasurement.prototype = Object.freeze({ 1.531 + __proto__: Metrics.Measurement.prototype, 1.532 + 1.533 + name: "current", 1.534 + version: 3, 1.535 + 1.536 + // Storage is in preferences. 1.537 + fields: {}, 1.538 + 1.539 + /** 1.540 + * All data is stored in prefs, so we have a custom implementation. 1.541 + */ 1.542 + getValues: function () { 1.543 + let sessions = this.provider.healthReporter.sessionRecorder; 1.544 + 1.545 + let fields = new Map(); 1.546 + let now = new Date(); 1.547 + fields.set("startDay", [now, Metrics.dateToDays(sessions.startDate)]); 1.548 + fields.set("activeTicks", [now, sessions.activeTicks]); 1.549 + fields.set("totalTime", [now, sessions.totalTime]); 1.550 + fields.set("main", [now, sessions.main]); 1.551 + fields.set("firstPaint", [now, sessions.firstPaint]); 1.552 + fields.set("sessionRestored", [now, sessions.sessionRestored]); 1.553 + 1.554 + return CommonUtils.laterTickResolvingPromise({ 1.555 + days: new Metrics.DailyValues(), 1.556 + singular: fields, 1.557 + }); 1.558 + }, 1.559 + 1.560 + _serializeJSONSingular: function (data) { 1.561 + let result = {"_v": this.version}; 1.562 + 1.563 + for (let [field, value] of data) { 1.564 + result[field] = value[1]; 1.565 + } 1.566 + 1.567 + return result; 1.568 + }, 1.569 +}); 1.570 + 1.571 +/** 1.572 + * Records a history of all application sessions. 1.573 + */ 1.574 +function PreviousSessionsMeasurement() { 1.575 + Metrics.Measurement.call(this); 1.576 +} 1.577 + 1.578 +PreviousSessionsMeasurement.prototype = Object.freeze({ 1.579 + __proto__: Metrics.Measurement.prototype, 1.580 + 1.581 + name: "previous", 1.582 + version: 3, 1.583 + 1.584 + fields: { 1.585 + // Milliseconds of sessions that were properly shut down. 1.586 + cleanActiveTicks: DAILY_DISCRETE_NUMERIC_FIELD, 1.587 + cleanTotalTime: DAILY_DISCRETE_NUMERIC_FIELD, 1.588 + 1.589 + // Milliseconds of sessions that were not properly shut down. 1.590 + abortedActiveTicks: DAILY_DISCRETE_NUMERIC_FIELD, 1.591 + abortedTotalTime: DAILY_DISCRETE_NUMERIC_FIELD, 1.592 + 1.593 + // Startup times in milliseconds. 1.594 + main: DAILY_DISCRETE_NUMERIC_FIELD, 1.595 + firstPaint: DAILY_DISCRETE_NUMERIC_FIELD, 1.596 + sessionRestored: DAILY_DISCRETE_NUMERIC_FIELD, 1.597 + }, 1.598 +}); 1.599 + 1.600 + 1.601 +/** 1.602 + * Records information about the current browser session. 1.603 + * 1.604 + * A browser session is defined as an application/process lifetime. We 1.605 + * start a new session when the application starts (essentially when 1.606 + * this provider is instantiated) and end the session on shutdown. 1.607 + * 1.608 + * As the application runs, we record basic information about the 1.609 + * "activity" of the session. Activity is defined by the presence of 1.610 + * physical input into the browser (key press, mouse click, touch, etc). 1.611 + * 1.612 + * We differentiate between regular sessions and "aborted" sessions. An 1.613 + * aborted session is one that does not end expectedly. This is often the 1.614 + * result of a crash. We detect aborted sessions by storing the current 1.615 + * session separate from completed sessions. We normally move the 1.616 + * current session to completed sessions on application shutdown. If a 1.617 + * current session is present on application startup, that means that 1.618 + * the previous session was aborted. 1.619 + */ 1.620 +this.SessionsProvider = function () { 1.621 + Metrics.Provider.call(this); 1.622 +}; 1.623 + 1.624 +SessionsProvider.prototype = Object.freeze({ 1.625 + __proto__: Metrics.Provider.prototype, 1.626 + 1.627 + name: "org.mozilla.appSessions", 1.628 + 1.629 + measurementTypes: [CurrentSessionMeasurement, PreviousSessionsMeasurement], 1.630 + 1.631 + pullOnly: true, 1.632 + 1.633 + collectConstantData: function () { 1.634 + let previous = this.getMeasurement("previous", 3); 1.635 + 1.636 + return this.storage.enqueueTransaction(this._recordAndPruneSessions.bind(this)); 1.637 + }, 1.638 + 1.639 + _recordAndPruneSessions: function () { 1.640 + this._log.info("Moving previous sessions from session recorder to storage."); 1.641 + let recorder = this.healthReporter.sessionRecorder; 1.642 + let sessions = recorder.getPreviousSessions(); 1.643 + this._log.debug("Found " + Object.keys(sessions).length + " previous sessions."); 1.644 + 1.645 + let daily = this.getMeasurement("previous", 3); 1.646 + 1.647 + // Please note the coupling here between the session recorder and our state. 1.648 + // If the pruned index or the current index of the session recorder is ever 1.649 + // deleted or reset to 0, our stored state of a later index would mean that 1.650 + // new sessions would never be captured by this provider until the session 1.651 + // recorder index catches up to our last session ID. This should not happen 1.652 + // under normal circumstances, so we don't worry too much about it. We 1.653 + // should, however, consider this as part of implementing bug 841561. 1.654 + let lastRecordedSession = yield this.getState("lastSession"); 1.655 + if (lastRecordedSession === null) { 1.656 + lastRecordedSession = -1; 1.657 + } 1.658 + this._log.debug("The last recorded session was #" + lastRecordedSession); 1.659 + 1.660 + for (let [index, session] in Iterator(sessions)) { 1.661 + if (index <= lastRecordedSession) { 1.662 + this._log.warn("Already recorded session " + index + ". Did the last " + 1.663 + "session crash or have an issue saving the prefs file?"); 1.664 + continue; 1.665 + } 1.666 + 1.667 + let type = session.clean ? "clean" : "aborted"; 1.668 + let date = session.startDate; 1.669 + yield daily.addDailyDiscreteNumeric(type + "ActiveTicks", session.activeTicks, date); 1.670 + yield daily.addDailyDiscreteNumeric(type + "TotalTime", session.totalTime, date); 1.671 + 1.672 + for (let field of ["main", "firstPaint", "sessionRestored"]) { 1.673 + yield daily.addDailyDiscreteNumeric(field, session[field], date); 1.674 + } 1.675 + 1.676 + lastRecordedSession = index; 1.677 + } 1.678 + 1.679 + yield this.setState("lastSession", "" + lastRecordedSession); 1.680 + recorder.pruneOldSessions(new Date()); 1.681 + }, 1.682 +}); 1.683 + 1.684 +/** 1.685 + * Stores the set of active addons in storage. 1.686 + * 1.687 + * We do things a little differently than most other measurements. Because 1.688 + * addons are difficult to shoehorn into distinct fields, we simply store a 1.689 + * JSON blob in storage in a text field. 1.690 + */ 1.691 +function ActiveAddonsMeasurement() { 1.692 + Metrics.Measurement.call(this); 1.693 + 1.694 + this._serializers = {}; 1.695 + this._serializers[this.SERIALIZE_JSON] = { 1.696 + singular: this._serializeJSONSingular.bind(this), 1.697 + // We don't need a daily serializer because we have none of this data. 1.698 + }; 1.699 +} 1.700 + 1.701 +ActiveAddonsMeasurement.prototype = Object.freeze({ 1.702 + __proto__: Metrics.Measurement.prototype, 1.703 + 1.704 + name: "addons", 1.705 + version: 2, 1.706 + 1.707 + fields: { 1.708 + addons: LAST_TEXT_FIELD, 1.709 + }, 1.710 + 1.711 + _serializeJSONSingular: function (data) { 1.712 + if (!data.has("addons")) { 1.713 + this._log.warn("Don't have addons info. Weird."); 1.714 + return null; 1.715 + } 1.716 + 1.717 + // Exceptions are caught in the caller. 1.718 + let result = JSON.parse(data.get("addons")[1]); 1.719 + result._v = this.version; 1.720 + return result; 1.721 + }, 1.722 +}); 1.723 + 1.724 +/** 1.725 + * Stores the set of active plugins in storage. 1.726 + * 1.727 + * This stores the data in a JSON blob in a text field similar to the 1.728 + * ActiveAddonsMeasurement. 1.729 + */ 1.730 +function ActivePluginsMeasurement() { 1.731 + Metrics.Measurement.call(this); 1.732 + 1.733 + this._serializers = {}; 1.734 + this._serializers[this.SERIALIZE_JSON] = { 1.735 + singular: this._serializeJSONSingular.bind(this), 1.736 + // We don't need a daily serializer because we have none of this data. 1.737 + }; 1.738 +} 1.739 + 1.740 +ActivePluginsMeasurement.prototype = Object.freeze({ 1.741 + __proto__: Metrics.Measurement.prototype, 1.742 + 1.743 + name: "plugins", 1.744 + version: 1, 1.745 + 1.746 + fields: { 1.747 + plugins: LAST_TEXT_FIELD, 1.748 + }, 1.749 + 1.750 + _serializeJSONSingular: function (data) { 1.751 + if (!data.has("plugins")) { 1.752 + this._log.warn("Don't have plugins info. Weird."); 1.753 + return null; 1.754 + } 1.755 + 1.756 + // Exceptions are caught in the caller. 1.757 + let result = JSON.parse(data.get("plugins")[1]); 1.758 + result._v = this.version; 1.759 + return result; 1.760 + }, 1.761 +}); 1.762 + 1.763 + 1.764 +function AddonCountsMeasurement() { 1.765 + Metrics.Measurement.call(this); 1.766 +} 1.767 + 1.768 +AddonCountsMeasurement.prototype = Object.freeze({ 1.769 + __proto__: Metrics.Measurement.prototype, 1.770 + 1.771 + name: "counts", 1.772 + version: 2, 1.773 + 1.774 + fields: { 1.775 + theme: DAILY_LAST_NUMERIC_FIELD, 1.776 + lwtheme: DAILY_LAST_NUMERIC_FIELD, 1.777 + plugin: DAILY_LAST_NUMERIC_FIELD, 1.778 + extension: DAILY_LAST_NUMERIC_FIELD, 1.779 + service: DAILY_LAST_NUMERIC_FIELD, 1.780 + }, 1.781 +}); 1.782 + 1.783 + 1.784 +/** 1.785 + * Legacy version of addons counts before services was added. 1.786 + */ 1.787 +function AddonCountsMeasurement1() { 1.788 + Metrics.Measurement.call(this); 1.789 +} 1.790 + 1.791 +AddonCountsMeasurement1.prototype = Object.freeze({ 1.792 + __proto__: Metrics.Measurement.prototype, 1.793 + 1.794 + name: "counts", 1.795 + version: 1, 1.796 + 1.797 + fields: { 1.798 + theme: DAILY_LAST_NUMERIC_FIELD, 1.799 + lwtheme: DAILY_LAST_NUMERIC_FIELD, 1.800 + plugin: DAILY_LAST_NUMERIC_FIELD, 1.801 + extension: DAILY_LAST_NUMERIC_FIELD, 1.802 + }, 1.803 +}); 1.804 + 1.805 + 1.806 +this.AddonsProvider = function () { 1.807 + Metrics.Provider.call(this); 1.808 + 1.809 + this._prefs = new Preferences({defaultBranch: null}); 1.810 +}; 1.811 + 1.812 +AddonsProvider.prototype = Object.freeze({ 1.813 + __proto__: Metrics.Provider.prototype, 1.814 + 1.815 + // Whenever these AddonListener callbacks are called, we repopulate 1.816 + // and store the set of addons. Note that these events will only fire 1.817 + // for restartless add-ons. For actions that require a restart, we 1.818 + // will catch the change after restart. The alternative is a lot of 1.819 + // state tracking here, which isn't desirable. 1.820 + ADDON_LISTENER_CALLBACKS: [ 1.821 + "onEnabled", 1.822 + "onDisabled", 1.823 + "onInstalled", 1.824 + "onUninstalled", 1.825 + ], 1.826 + 1.827 + // Add-on types for which full details are uploaded in the 1.828 + // ActiveAddonsMeasurement. All other types are ignored. 1.829 + FULL_DETAIL_TYPES: [ 1.830 + "extension", 1.831 + "service", 1.832 + ], 1.833 + 1.834 + name: "org.mozilla.addons", 1.835 + 1.836 + measurementTypes: [ 1.837 + ActiveAddonsMeasurement, 1.838 + ActivePluginsMeasurement, 1.839 + AddonCountsMeasurement1, 1.840 + AddonCountsMeasurement, 1.841 + ], 1.842 + 1.843 + postInit: function () { 1.844 + let listener = {}; 1.845 + 1.846 + for (let method of this.ADDON_LISTENER_CALLBACKS) { 1.847 + listener[method] = this._collectAndStoreAddons.bind(this); 1.848 + } 1.849 + 1.850 + this._listener = listener; 1.851 + AddonManager.addAddonListener(this._listener); 1.852 + 1.853 + return CommonUtils.laterTickResolvingPromise(); 1.854 + }, 1.855 + 1.856 + onShutdown: function () { 1.857 + AddonManager.removeAddonListener(this._listener); 1.858 + this._listener = null; 1.859 + 1.860 + return CommonUtils.laterTickResolvingPromise(); 1.861 + }, 1.862 + 1.863 + collectConstantData: function () { 1.864 + return this._collectAndStoreAddons(); 1.865 + }, 1.866 + 1.867 + _collectAndStoreAddons: function () { 1.868 + let deferred = Promise.defer(); 1.869 + 1.870 + AddonManager.getAllAddons(function onAllAddons(addons) { 1.871 + let data; 1.872 + let addonsField; 1.873 + let pluginsField; 1.874 + try { 1.875 + data = this._createDataStructure(addons); 1.876 + addonsField = JSON.stringify(data.addons); 1.877 + pluginsField = JSON.stringify(data.plugins); 1.878 + } catch (ex) { 1.879 + this._log.warn("Exception when populating add-ons data structure: " + 1.880 + CommonUtils.exceptionStr(ex)); 1.881 + deferred.reject(ex); 1.882 + return; 1.883 + } 1.884 + 1.885 + let now = new Date(); 1.886 + let addons = this.getMeasurement("addons", 2); 1.887 + let plugins = this.getMeasurement("plugins", 1); 1.888 + let counts = this.getMeasurement(AddonCountsMeasurement.prototype.name, 1.889 + AddonCountsMeasurement.prototype.version); 1.890 + 1.891 + this.enqueueStorageOperation(function storageAddons() { 1.892 + for (let type in data.counts) { 1.893 + try { 1.894 + counts.fieldID(type); 1.895 + } catch (ex) { 1.896 + this._log.warn("Add-on type without field: " + type); 1.897 + continue; 1.898 + } 1.899 + 1.900 + counts.setDailyLastNumeric(type, data.counts[type], now); 1.901 + } 1.902 + 1.903 + return addons.setLastText("addons", addonsField).then( 1.904 + function onSuccess() { 1.905 + return plugins.setLastText("plugins", pluginsField).then( 1.906 + function onSuccess() { deferred.resolve(); }, 1.907 + function onError(error) { deferred.reject(error); } 1.908 + ); 1.909 + }, 1.910 + function onError(error) { deferred.reject(error); } 1.911 + ); 1.912 + }.bind(this)); 1.913 + }.bind(this)); 1.914 + 1.915 + return deferred.promise; 1.916 + }, 1.917 + 1.918 + COPY_ADDON_FIELDS: [ 1.919 + "userDisabled", 1.920 + "appDisabled", 1.921 + "name", 1.922 + "version", 1.923 + "type", 1.924 + "scope", 1.925 + "description", 1.926 + "foreignInstall", 1.927 + "hasBinaryComponents", 1.928 + ], 1.929 + 1.930 + COPY_PLUGIN_FIELDS: [ 1.931 + "name", 1.932 + "version", 1.933 + "description", 1.934 + "blocklisted", 1.935 + "disabled", 1.936 + "clicktoplay", 1.937 + ], 1.938 + 1.939 + _createDataStructure: function (addons) { 1.940 + let data = { 1.941 + addons: {}, 1.942 + plugins: {}, 1.943 + counts: {} 1.944 + }; 1.945 + 1.946 + for (let addon of addons) { 1.947 + let type = addon.type; 1.948 + 1.949 + // We count plugins separately below. 1.950 + if (addon.type == "plugin") 1.951 + continue; 1.952 + 1.953 + data.counts[type] = (data.counts[type] || 0) + 1; 1.954 + 1.955 + if (this.FULL_DETAIL_TYPES.indexOf(addon.type) == -1) { 1.956 + continue; 1.957 + } 1.958 + 1.959 + let obj = {}; 1.960 + for (let field of this.COPY_ADDON_FIELDS) { 1.961 + obj[field] = addon[field]; 1.962 + } 1.963 + 1.964 + if (addon.installDate) { 1.965 + obj.installDay = this._dateToDays(addon.installDate); 1.966 + } 1.967 + 1.968 + if (addon.updateDate) { 1.969 + obj.updateDay = this._dateToDays(addon.updateDate); 1.970 + } 1.971 + 1.972 + data.addons[addon.id] = obj; 1.973 + } 1.974 + 1.975 + let pluginTags = Cc["@mozilla.org/plugin/host;1"]. 1.976 + getService(Ci.nsIPluginHost). 1.977 + getPluginTags({}); 1.978 + 1.979 + for (let tag of pluginTags) { 1.980 + let obj = { 1.981 + mimeTypes: tag.getMimeTypes({}), 1.982 + }; 1.983 + 1.984 + for (let field of this.COPY_PLUGIN_FIELDS) { 1.985 + obj[field] = tag[field]; 1.986 + } 1.987 + 1.988 + // Plugins need to have a filename and a name, so this can't be empty. 1.989 + let id = tag.filename + ":" + tag.name + ":" + tag.version + ":" 1.990 + + tag.description; 1.991 + data.plugins[id] = obj; 1.992 + } 1.993 + 1.994 + data.counts["plugin"] = pluginTags.length; 1.995 + 1.996 + return data; 1.997 + }, 1.998 +}); 1.999 + 1.1000 +#ifdef MOZ_CRASHREPORTER 1.1001 + 1.1002 +function DailyCrashesMeasurement1() { 1.1003 + Metrics.Measurement.call(this); 1.1004 +} 1.1005 + 1.1006 +DailyCrashesMeasurement1.prototype = Object.freeze({ 1.1007 + __proto__: Metrics.Measurement.prototype, 1.1008 + 1.1009 + name: "crashes", 1.1010 + version: 1, 1.1011 + 1.1012 + fields: { 1.1013 + pending: DAILY_COUNTER_FIELD, 1.1014 + submitted: DAILY_COUNTER_FIELD, 1.1015 + }, 1.1016 +}); 1.1017 + 1.1018 +function DailyCrashesMeasurement2() { 1.1019 + Metrics.Measurement.call(this); 1.1020 +} 1.1021 + 1.1022 +DailyCrashesMeasurement2.prototype = Object.freeze({ 1.1023 + __proto__: Metrics.Measurement.prototype, 1.1024 + 1.1025 + name: "crashes", 1.1026 + version: 2, 1.1027 + 1.1028 + fields: { 1.1029 + mainCrash: DAILY_LAST_NUMERIC_FIELD, 1.1030 + }, 1.1031 +}); 1.1032 + 1.1033 +this.CrashesProvider = function () { 1.1034 + Metrics.Provider.call(this); 1.1035 + 1.1036 + // So we can unit test. 1.1037 + this._manager = Services.crashmanager; 1.1038 +}; 1.1039 + 1.1040 +CrashesProvider.prototype = Object.freeze({ 1.1041 + __proto__: Metrics.Provider.prototype, 1.1042 + 1.1043 + name: "org.mozilla.crashes", 1.1044 + 1.1045 + measurementTypes: [ 1.1046 + DailyCrashesMeasurement1, 1.1047 + DailyCrashesMeasurement2, 1.1048 + ], 1.1049 + 1.1050 + pullOnly: true, 1.1051 + 1.1052 + collectDailyData: function () { 1.1053 + return this.storage.enqueueTransaction(this._populateCrashCounts.bind(this)); 1.1054 + }, 1.1055 + 1.1056 + _populateCrashCounts: function () { 1.1057 + this._log.info("Grabbing crash counts from crash manager."); 1.1058 + let crashCounts = yield this._manager.getCrashCountsByDay(); 1.1059 + let fields = { 1.1060 + "main-crash": "mainCrash", 1.1061 + }; 1.1062 + 1.1063 + let m = this.getMeasurement("crashes", 2); 1.1064 + 1.1065 + for (let [day, types] of crashCounts) { 1.1066 + let date = Metrics.daysToDate(day); 1.1067 + for (let [type, count] of types) { 1.1068 + if (!(type in fields)) { 1.1069 + this._log.warn("Unknown crash type encountered: " + type); 1.1070 + continue; 1.1071 + } 1.1072 + 1.1073 + yield m.setDailyLastNumeric(fields[type], count, date); 1.1074 + } 1.1075 + } 1.1076 + }, 1.1077 +}); 1.1078 + 1.1079 +#endif 1.1080 + 1.1081 + 1.1082 +/** 1.1083 + * Holds basic statistics about the Places database. 1.1084 + */ 1.1085 +function PlacesMeasurement() { 1.1086 + Metrics.Measurement.call(this); 1.1087 +} 1.1088 + 1.1089 +PlacesMeasurement.prototype = Object.freeze({ 1.1090 + __proto__: Metrics.Measurement.prototype, 1.1091 + 1.1092 + name: "places", 1.1093 + version: 1, 1.1094 + 1.1095 + fields: { 1.1096 + pages: DAILY_LAST_NUMERIC_FIELD, 1.1097 + bookmarks: DAILY_LAST_NUMERIC_FIELD, 1.1098 + }, 1.1099 +}); 1.1100 + 1.1101 + 1.1102 +/** 1.1103 + * Collects information about Places. 1.1104 + */ 1.1105 +this.PlacesProvider = function () { 1.1106 + Metrics.Provider.call(this); 1.1107 +}; 1.1108 + 1.1109 +PlacesProvider.prototype = Object.freeze({ 1.1110 + __proto__: Metrics.Provider.prototype, 1.1111 + 1.1112 + name: "org.mozilla.places", 1.1113 + 1.1114 + measurementTypes: [PlacesMeasurement], 1.1115 + 1.1116 + collectDailyData: function () { 1.1117 + return this.storage.enqueueTransaction(this._collectData.bind(this)); 1.1118 + }, 1.1119 + 1.1120 + _collectData: function () { 1.1121 + let now = new Date(); 1.1122 + let data = yield this._getDailyValues(); 1.1123 + 1.1124 + let m = this.getMeasurement("places", 1); 1.1125 + 1.1126 + yield m.setDailyLastNumeric("pages", data.PLACES_PAGES_COUNT); 1.1127 + yield m.setDailyLastNumeric("bookmarks", data.PLACES_BOOKMARKS_COUNT); 1.1128 + }, 1.1129 + 1.1130 + _getDailyValues: function () { 1.1131 + let deferred = Promise.defer(); 1.1132 + 1.1133 + PlacesDBUtils.telemetry(null, function onResult(data) { 1.1134 + deferred.resolve(data); 1.1135 + }); 1.1136 + 1.1137 + return deferred.promise; 1.1138 + }, 1.1139 +}); 1.1140 + 1.1141 +function SearchCountMeasurement1() { 1.1142 + Metrics.Measurement.call(this); 1.1143 +} 1.1144 + 1.1145 +SearchCountMeasurement1.prototype = Object.freeze({ 1.1146 + __proto__: Metrics.Measurement.prototype, 1.1147 + 1.1148 + name: "counts", 1.1149 + version: 1, 1.1150 + 1.1151 + // We only record searches for search engines that have partner agreements 1.1152 + // with Mozilla. 1.1153 + fields: { 1.1154 + "amazon.com.abouthome": DAILY_COUNTER_FIELD, 1.1155 + "amazon.com.contextmenu": DAILY_COUNTER_FIELD, 1.1156 + "amazon.com.searchbar": DAILY_COUNTER_FIELD, 1.1157 + "amazon.com.urlbar": DAILY_COUNTER_FIELD, 1.1158 + "bing.abouthome": DAILY_COUNTER_FIELD, 1.1159 + "bing.contextmenu": DAILY_COUNTER_FIELD, 1.1160 + "bing.searchbar": DAILY_COUNTER_FIELD, 1.1161 + "bing.urlbar": DAILY_COUNTER_FIELD, 1.1162 + "google.abouthome": DAILY_COUNTER_FIELD, 1.1163 + "google.contextmenu": DAILY_COUNTER_FIELD, 1.1164 + "google.searchbar": DAILY_COUNTER_FIELD, 1.1165 + "google.urlbar": DAILY_COUNTER_FIELD, 1.1166 + "yahoo.abouthome": DAILY_COUNTER_FIELD, 1.1167 + "yahoo.contextmenu": DAILY_COUNTER_FIELD, 1.1168 + "yahoo.searchbar": DAILY_COUNTER_FIELD, 1.1169 + "yahoo.urlbar": DAILY_COUNTER_FIELD, 1.1170 + "other.abouthome": DAILY_COUNTER_FIELD, 1.1171 + "other.contextmenu": DAILY_COUNTER_FIELD, 1.1172 + "other.searchbar": DAILY_COUNTER_FIELD, 1.1173 + "other.urlbar": DAILY_COUNTER_FIELD, 1.1174 + }, 1.1175 +}); 1.1176 + 1.1177 +/** 1.1178 + * Records search counts per day per engine and where search initiated. 1.1179 + * 1.1180 + * We want to record granular details for individual locale-specific search 1.1181 + * providers, but only if they're Mozilla partners. In order to do this, we 1.1182 + * track the nsISearchEngine identifier, which denotes shipped search engines, 1.1183 + * and intersect those with our partner list. 1.1184 + * 1.1185 + * We don't use the search engine name directly, because it is shared across 1.1186 + * locales; e.g., eBay-de and eBay both share the name "eBay". 1.1187 + */ 1.1188 +function SearchCountMeasurementBase() { 1.1189 + this._fieldSpecs = {}; 1.1190 + Metrics.Measurement.call(this); 1.1191 +} 1.1192 + 1.1193 +SearchCountMeasurementBase.prototype = Object.freeze({ 1.1194 + __proto__: Metrics.Measurement.prototype, 1.1195 + 1.1196 + 1.1197 + // Our fields are dynamic. 1.1198 + get fields() { 1.1199 + return this._fieldSpecs; 1.1200 + }, 1.1201 + 1.1202 + /** 1.1203 + * Override the default behavior: serializers should include every counter 1.1204 + * field from the DB, even if we don't currently have it registered. 1.1205 + * 1.1206 + * Do this so we don't have to register several hundred fields to match 1.1207 + * various Firefox locales. 1.1208 + * 1.1209 + * We use the "provider.type" syntax as a rudimentary check for validity. 1.1210 + * 1.1211 + * We trust that measurement versioning is sufficient to exclude old provider 1.1212 + * data. 1.1213 + */ 1.1214 + shouldIncludeField: function (name) { 1.1215 + return name.contains("."); 1.1216 + }, 1.1217 + 1.1218 + /** 1.1219 + * The measurement type mechanism doesn't introspect the DB. Override it 1.1220 + * so that we can assume all unknown fields are counters. 1.1221 + */ 1.1222 + fieldType: function (name) { 1.1223 + if (name in this.fields) { 1.1224 + return this.fields[name].type; 1.1225 + } 1.1226 + 1.1227 + // Default to a counter. 1.1228 + return Metrics.Storage.FIELD_DAILY_COUNTER; 1.1229 + }, 1.1230 + 1.1231 + SOURCES: [ 1.1232 + "abouthome", 1.1233 + "contextmenu", 1.1234 + "newtab", 1.1235 + "searchbar", 1.1236 + "urlbar", 1.1237 + ], 1.1238 +}); 1.1239 + 1.1240 +function SearchCountMeasurement2() { 1.1241 + SearchCountMeasurementBase.call(this); 1.1242 +} 1.1243 + 1.1244 +SearchCountMeasurement2.prototype = Object.freeze({ 1.1245 + __proto__: SearchCountMeasurementBase.prototype, 1.1246 + name: "counts", 1.1247 + version: 2, 1.1248 +}); 1.1249 + 1.1250 +function SearchCountMeasurement3() { 1.1251 + SearchCountMeasurementBase.call(this); 1.1252 +} 1.1253 + 1.1254 +SearchCountMeasurement3.prototype = Object.freeze({ 1.1255 + __proto__: SearchCountMeasurementBase.prototype, 1.1256 + name: "counts", 1.1257 + version: 3, 1.1258 + 1.1259 + getEngines: function () { 1.1260 + return Services.search.getEngines(); 1.1261 + }, 1.1262 + 1.1263 + getEngineID: function (engine) { 1.1264 + if (!engine) { 1.1265 + return "other"; 1.1266 + } 1.1267 + if (engine.identifier) { 1.1268 + return engine.identifier; 1.1269 + } 1.1270 + return "other-" + engine.name; 1.1271 + }, 1.1272 +}); 1.1273 + 1.1274 +function SearchEnginesMeasurement1() { 1.1275 + Metrics.Measurement.call(this); 1.1276 +} 1.1277 + 1.1278 +SearchEnginesMeasurement1.prototype = Object.freeze({ 1.1279 + __proto__: Metrics.Measurement.prototype, 1.1280 + 1.1281 + name: "engines", 1.1282 + version: 1, 1.1283 + 1.1284 + fields: { 1.1285 + default: DAILY_LAST_TEXT_FIELD, 1.1286 + }, 1.1287 +}); 1.1288 + 1.1289 +this.SearchesProvider = function () { 1.1290 + Metrics.Provider.call(this); 1.1291 + 1.1292 + this._prefs = new Preferences({defaultBranch: null}); 1.1293 +}; 1.1294 + 1.1295 +this.SearchesProvider.prototype = Object.freeze({ 1.1296 + __proto__: Metrics.Provider.prototype, 1.1297 + 1.1298 + name: "org.mozilla.searches", 1.1299 + measurementTypes: [ 1.1300 + SearchCountMeasurement1, 1.1301 + SearchCountMeasurement2, 1.1302 + SearchCountMeasurement3, 1.1303 + SearchEnginesMeasurement1, 1.1304 + ], 1.1305 + 1.1306 + /** 1.1307 + * Initialize the search service before our measurements are touched. 1.1308 + */ 1.1309 + preInit: function (storage) { 1.1310 + // Initialize search service. 1.1311 + let deferred = Promise.defer(); 1.1312 + Services.search.init(function onInitComplete () { 1.1313 + deferred.resolve(); 1.1314 + }); 1.1315 + return deferred.promise; 1.1316 + }, 1.1317 + 1.1318 + collectDailyData: function () { 1.1319 + return this.storage.enqueueTransaction(function getDaily() { 1.1320 + // We currently only record this if Telemetry is enabled. 1.1321 + if (!isTelemetryEnabled(this._prefs)) { 1.1322 + return; 1.1323 + } 1.1324 + 1.1325 + let m = this.getMeasurement(SearchEnginesMeasurement1.prototype.name, 1.1326 + SearchEnginesMeasurement1.prototype.version); 1.1327 + 1.1328 + let engine; 1.1329 + try { 1.1330 + engine = Services.search.defaultEngine; 1.1331 + } catch (e) {} 1.1332 + let name; 1.1333 + 1.1334 + if (!engine) { 1.1335 + name = "NONE"; 1.1336 + } else if (engine.identifier) { 1.1337 + name = engine.identifier; 1.1338 + } else if (engine.name) { 1.1339 + name = "other-" + engine.name; 1.1340 + } else { 1.1341 + name = "UNDEFINED"; 1.1342 + } 1.1343 + 1.1344 + yield m.setDailyLastText("default", name); 1.1345 + }.bind(this)); 1.1346 + }, 1.1347 + 1.1348 + /** 1.1349 + * Record that a search occurred. 1.1350 + * 1.1351 + * @param engine 1.1352 + * (nsISearchEngine) The search engine used. 1.1353 + * @param source 1.1354 + * (string) Where the search was initiated from. Must be one of the 1.1355 + * SearchCountMeasurement2.SOURCES values. 1.1356 + * 1.1357 + * @return Promise<> 1.1358 + * The promise is resolved when the storage operation completes. 1.1359 + */ 1.1360 + recordSearch: function (engine, source) { 1.1361 + let m = this.getMeasurement("counts", 3); 1.1362 + 1.1363 + if (m.SOURCES.indexOf(source) == -1) { 1.1364 + throw new Error("Unknown source for search: " + source); 1.1365 + } 1.1366 + 1.1367 + let field = m.getEngineID(engine) + "." + source; 1.1368 + if (this.storage.hasFieldFromMeasurement(m.id, field, 1.1369 + this.storage.FIELD_DAILY_COUNTER)) { 1.1370 + let fieldID = this.storage.fieldIDFromMeasurement(m.id, field); 1.1371 + return this.enqueueStorageOperation(function recordSearchKnownField() { 1.1372 + return this.storage.incrementDailyCounterFromFieldID(fieldID); 1.1373 + }.bind(this)); 1.1374 + } 1.1375 + 1.1376 + // Otherwise, we first need to create the field. 1.1377 + return this.enqueueStorageOperation(function recordFieldAndSearch() { 1.1378 + // This function has to return a promise. 1.1379 + return Task.spawn(function () { 1.1380 + let fieldID = yield this.storage.registerField(m.id, field, 1.1381 + this.storage.FIELD_DAILY_COUNTER); 1.1382 + yield this.storage.incrementDailyCounterFromFieldID(fieldID); 1.1383 + }.bind(this)); 1.1384 + }.bind(this)); 1.1385 + }, 1.1386 +}); 1.1387 + 1.1388 +function HealthReportSubmissionMeasurement1() { 1.1389 + Metrics.Measurement.call(this); 1.1390 +} 1.1391 + 1.1392 +HealthReportSubmissionMeasurement1.prototype = Object.freeze({ 1.1393 + __proto__: Metrics.Measurement.prototype, 1.1394 + 1.1395 + name: "submissions", 1.1396 + version: 1, 1.1397 + 1.1398 + fields: { 1.1399 + firstDocumentUploadAttempt: DAILY_COUNTER_FIELD, 1.1400 + continuationUploadAttempt: DAILY_COUNTER_FIELD, 1.1401 + uploadSuccess: DAILY_COUNTER_FIELD, 1.1402 + uploadTransportFailure: DAILY_COUNTER_FIELD, 1.1403 + uploadServerFailure: DAILY_COUNTER_FIELD, 1.1404 + uploadClientFailure: DAILY_COUNTER_FIELD, 1.1405 + }, 1.1406 +}); 1.1407 + 1.1408 +function HealthReportSubmissionMeasurement2() { 1.1409 + Metrics.Measurement.call(this); 1.1410 +} 1.1411 + 1.1412 +HealthReportSubmissionMeasurement2.prototype = Object.freeze({ 1.1413 + __proto__: Metrics.Measurement.prototype, 1.1414 + 1.1415 + name: "submissions", 1.1416 + version: 2, 1.1417 + 1.1418 + fields: { 1.1419 + firstDocumentUploadAttempt: DAILY_COUNTER_FIELD, 1.1420 + continuationUploadAttempt: DAILY_COUNTER_FIELD, 1.1421 + uploadSuccess: DAILY_COUNTER_FIELD, 1.1422 + uploadTransportFailure: DAILY_COUNTER_FIELD, 1.1423 + uploadServerFailure: DAILY_COUNTER_FIELD, 1.1424 + uploadClientFailure: DAILY_COUNTER_FIELD, 1.1425 + uploadAlreadyInProgress: DAILY_COUNTER_FIELD, 1.1426 + }, 1.1427 +}); 1.1428 + 1.1429 +this.HealthReportProvider = function () { 1.1430 + Metrics.Provider.call(this); 1.1431 +} 1.1432 + 1.1433 +HealthReportProvider.prototype = Object.freeze({ 1.1434 + __proto__: Metrics.Provider.prototype, 1.1435 + 1.1436 + name: "org.mozilla.healthreport", 1.1437 + 1.1438 + measurementTypes: [ 1.1439 + HealthReportSubmissionMeasurement1, 1.1440 + HealthReportSubmissionMeasurement2, 1.1441 + ], 1.1442 + 1.1443 + recordEvent: function (event, date=new Date()) { 1.1444 + let m = this.getMeasurement("submissions", 2); 1.1445 + return this.enqueueStorageOperation(function recordCounter() { 1.1446 + return m.incrementDailyCounter(event, date); 1.1447 + }); 1.1448 + }, 1.1449 +});