Tue, 06 Jan 2015 21:39:09 +0100
Conditionally force memory storage according to privacy.thirdparty.isolate;
This solves Tor bug #9701, complying with disk avoidance documented in
https://www.torproject.org/projects/torbrowser/design/#disk-avoidance.
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 /**
6 * This file contains metrics data providers for the Firefox Health
7 * Report. Ideally each provider in this file exists in separate modules
8 * and lives close to the code it is querying. However, because of the
9 * overhead of JS compartments (which are created for each module), we
10 * currently have all the code in one file. When the overhead of
11 * compartments reaches a reasonable level, this file should be split
12 * up.
13 */
15 "use strict";
17 #ifndef MERGED_COMPARTMENT
19 this.EXPORTED_SYMBOLS = [
20 "AddonsProvider",
21 "AppInfoProvider",
22 #ifdef MOZ_CRASHREPORTER
23 "CrashesProvider",
24 #endif
25 "HealthReportProvider",
26 "PlacesProvider",
27 "SearchesProvider",
28 "SessionsProvider",
29 "SysInfoProvider",
30 ];
32 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
34 Cu.import("resource://gre/modules/Metrics.jsm");
36 #endif
38 Cu.import("resource://gre/modules/Promise.jsm");
39 Cu.import("resource://gre/modules/osfile.jsm");
40 Cu.import("resource://gre/modules/Preferences.jsm");
41 Cu.import("resource://gre/modules/Services.jsm");
42 Cu.import("resource://gre/modules/Task.jsm");
43 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
44 Cu.import("resource://services-common/utils.js");
46 XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
47 "resource://gre/modules/AddonManager.jsm");
48 XPCOMUtils.defineLazyModuleGetter(this, "UpdateChannel",
49 "resource://gre/modules/UpdateChannel.jsm");
50 XPCOMUtils.defineLazyModuleGetter(this, "PlacesDBUtils",
51 "resource://gre/modules/PlacesDBUtils.jsm");
54 const LAST_NUMERIC_FIELD = {type: Metrics.Storage.FIELD_LAST_NUMERIC};
55 const LAST_TEXT_FIELD = {type: Metrics.Storage.FIELD_LAST_TEXT};
56 const DAILY_DISCRETE_NUMERIC_FIELD = {type: Metrics.Storage.FIELD_DAILY_DISCRETE_NUMERIC};
57 const DAILY_LAST_NUMERIC_FIELD = {type: Metrics.Storage.FIELD_DAILY_LAST_NUMERIC};
58 const DAILY_LAST_TEXT_FIELD = {type: Metrics.Storage.FIELD_DAILY_LAST_TEXT};
59 const DAILY_COUNTER_FIELD = {type: Metrics.Storage.FIELD_DAILY_COUNTER};
61 const TELEMETRY_PREF = "toolkit.telemetry.enabled";
63 function isTelemetryEnabled(prefs) {
64 return prefs.get(TELEMETRY_PREF, false);
65 }
67 /**
68 * Represents basic application state.
69 *
70 * This is roughly a union of nsIXULAppInfo, nsIXULRuntime, with a few extra
71 * pieces thrown in.
72 */
73 function AppInfoMeasurement() {
74 Metrics.Measurement.call(this);
75 }
77 AppInfoMeasurement.prototype = Object.freeze({
78 __proto__: Metrics.Measurement.prototype,
80 name: "appinfo",
81 version: 2,
83 fields: {
84 vendor: LAST_TEXT_FIELD,
85 name: LAST_TEXT_FIELD,
86 id: LAST_TEXT_FIELD,
87 version: LAST_TEXT_FIELD,
88 appBuildID: LAST_TEXT_FIELD,
89 platformVersion: LAST_TEXT_FIELD,
90 platformBuildID: LAST_TEXT_FIELD,
91 os: LAST_TEXT_FIELD,
92 xpcomabi: LAST_TEXT_FIELD,
93 updateChannel: LAST_TEXT_FIELD,
94 distributionID: LAST_TEXT_FIELD,
95 distributionVersion: LAST_TEXT_FIELD,
96 hotfixVersion: LAST_TEXT_FIELD,
97 locale: LAST_TEXT_FIELD,
98 isDefaultBrowser: {type: Metrics.Storage.FIELD_DAILY_LAST_NUMERIC},
99 isTelemetryEnabled: {type: Metrics.Storage.FIELD_DAILY_LAST_NUMERIC},
100 isBlocklistEnabled: {type: Metrics.Storage.FIELD_DAILY_LAST_NUMERIC},
101 },
102 });
104 /**
105 * Legacy version of app info before Telemetry was added.
106 *
107 * The "last" fields have all been removed. We only report the longitudinal
108 * field.
109 */
110 function AppInfoMeasurement1() {
111 Metrics.Measurement.call(this);
112 }
114 AppInfoMeasurement1.prototype = Object.freeze({
115 __proto__: Metrics.Measurement.prototype,
117 name: "appinfo",
118 version: 1,
120 fields: {
121 isDefaultBrowser: {type: Metrics.Storage.FIELD_DAILY_LAST_NUMERIC},
122 },
123 });
126 function AppVersionMeasurement1() {
127 Metrics.Measurement.call(this);
128 }
130 AppVersionMeasurement1.prototype = Object.freeze({
131 __proto__: Metrics.Measurement.prototype,
133 name: "versions",
134 version: 1,
136 fields: {
137 version: {type: Metrics.Storage.FIELD_DAILY_DISCRETE_TEXT},
138 },
139 });
141 // Version 2 added the build ID.
142 function AppVersionMeasurement2() {
143 Metrics.Measurement.call(this);
144 }
146 AppVersionMeasurement2.prototype = Object.freeze({
147 __proto__: Metrics.Measurement.prototype,
149 name: "versions",
150 version: 2,
152 fields: {
153 appVersion: {type: Metrics.Storage.FIELD_DAILY_DISCRETE_TEXT},
154 platformVersion: {type: Metrics.Storage.FIELD_DAILY_DISCRETE_TEXT},
155 appBuildID: {type: Metrics.Storage.FIELD_DAILY_DISCRETE_TEXT},
156 platformBuildID: {type: Metrics.Storage.FIELD_DAILY_DISCRETE_TEXT},
157 },
158 });
160 /**
161 * Holds data on the application update functionality.
162 */
163 function AppUpdateMeasurement1() {
164 Metrics.Measurement.call(this);
165 }
167 AppUpdateMeasurement1.prototype = Object.freeze({
168 __proto__: Metrics.Measurement.prototype,
170 name: "update",
171 version: 1,
173 fields: {
174 enabled: {type: Metrics.Storage.FIELD_DAILY_LAST_NUMERIC},
175 autoDownload: {type: Metrics.Storage.FIELD_DAILY_LAST_NUMERIC},
176 },
177 });
179 this.AppInfoProvider = function AppInfoProvider() {
180 Metrics.Provider.call(this);
182 this._prefs = new Preferences({defaultBranch: null});
183 }
184 AppInfoProvider.prototype = Object.freeze({
185 __proto__: Metrics.Provider.prototype,
187 name: "org.mozilla.appInfo",
189 measurementTypes: [
190 AppInfoMeasurement,
191 AppInfoMeasurement1,
192 AppUpdateMeasurement1,
193 AppVersionMeasurement1,
194 AppVersionMeasurement2,
195 ],
197 pullOnly: true,
199 appInfoFields: {
200 // From nsIXULAppInfo.
201 vendor: "vendor",
202 name: "name",
203 id: "ID",
204 version: "version",
205 appBuildID: "appBuildID",
206 platformVersion: "platformVersion",
207 platformBuildID: "platformBuildID",
209 // From nsIXULRuntime.
210 os: "OS",
211 xpcomabi: "XPCOMABI",
212 },
214 postInit: function () {
215 return Task.spawn(this._postInit.bind(this));
216 },
218 _postInit: function () {
219 let recordEmptyAppInfo = function () {
220 this._setCurrentAppVersion("");
221 this._setCurrentPlatformVersion("");
222 this._setCurrentAppBuildID("");
223 return this._setCurrentPlatformBuildID("");
224 }.bind(this);
226 // Services.appInfo should always be defined for any reasonably behaving
227 // Gecko app. If it isn't, we insert a empty string sentinel value.
228 let ai;
229 try {
230 ai = Services.appinfo;
231 } catch (ex) {
232 this._log.error("Could not obtain Services.appinfo: " +
233 CommonUtils.exceptionStr(ex));
234 yield recordEmptyAppInfo();
235 return;
236 }
238 if (!ai) {
239 this._log.error("Services.appinfo is unavailable.");
240 yield recordEmptyAppInfo();
241 return;
242 }
244 let currentAppVersion = ai.version;
245 let currentPlatformVersion = ai.platformVersion;
246 let currentAppBuildID = ai.appBuildID;
247 let currentPlatformBuildID = ai.platformBuildID;
249 // State's name doesn't contain "app" for historical compatibility.
250 let lastAppVersion = yield this.getState("lastVersion");
251 let lastPlatformVersion = yield this.getState("lastPlatformVersion");
252 let lastAppBuildID = yield this.getState("lastAppBuildID");
253 let lastPlatformBuildID = yield this.getState("lastPlatformBuildID");
255 if (currentAppVersion != lastAppVersion) {
256 yield this._setCurrentAppVersion(currentAppVersion);
257 }
259 if (currentPlatformVersion != lastPlatformVersion) {
260 yield this._setCurrentPlatformVersion(currentPlatformVersion);
261 }
263 if (currentAppBuildID != lastAppBuildID) {
264 yield this._setCurrentAppBuildID(currentAppBuildID);
265 }
267 if (currentPlatformBuildID != lastPlatformBuildID) {
268 yield this._setCurrentPlatformBuildID(currentPlatformBuildID);
269 }
270 },
272 _setCurrentAppVersion: function (version) {
273 this._log.info("Recording new application version: " + version);
274 let m = this.getMeasurement("versions", 2);
275 m.addDailyDiscreteText("appVersion", version);
277 // "app" not encoded in key for historical compatibility.
278 return this.setState("lastVersion", version);
279 },
281 _setCurrentPlatformVersion: function (version) {
282 this._log.info("Recording new platform version: " + version);
283 let m = this.getMeasurement("versions", 2);
284 m.addDailyDiscreteText("platformVersion", version);
285 return this.setState("lastPlatformVersion", version);
286 },
288 _setCurrentAppBuildID: function (build) {
289 this._log.info("Recording new application build ID: " + build);
290 let m = this.getMeasurement("versions", 2);
291 m.addDailyDiscreteText("appBuildID", build);
292 return this.setState("lastAppBuildID", build);
293 },
295 _setCurrentPlatformBuildID: function (build) {
296 this._log.info("Recording new platform build ID: " + build);
297 let m = this.getMeasurement("versions", 2);
298 m.addDailyDiscreteText("platformBuildID", build);
299 return this.setState("lastPlatformBuildID", build);
300 },
303 collectConstantData: function () {
304 return this.storage.enqueueTransaction(this._populateConstants.bind(this));
305 },
307 _populateConstants: function () {
308 let m = this.getMeasurement(AppInfoMeasurement.prototype.name,
309 AppInfoMeasurement.prototype.version);
311 let ai;
312 try {
313 ai = Services.appinfo;
314 } catch (ex) {
315 this._log.warn("Could not obtain Services.appinfo: " +
316 CommonUtils.exceptionStr(ex));
317 throw ex;
318 }
320 if (!ai) {
321 this._log.warn("Services.appinfo is unavailable.");
322 throw ex;
323 }
325 for (let [k, v] in Iterator(this.appInfoFields)) {
326 try {
327 yield m.setLastText(k, ai[v]);
328 } catch (ex) {
329 this._log.warn("Error obtaining Services.appinfo." + v);
330 }
331 }
333 try {
334 yield m.setLastText("updateChannel", UpdateChannel.get());
335 } catch (ex) {
336 this._log.warn("Could not obtain update channel: " +
337 CommonUtils.exceptionStr(ex));
338 }
340 yield m.setLastText("distributionID", this._prefs.get("distribution.id", ""));
341 yield m.setLastText("distributionVersion", this._prefs.get("distribution.version", ""));
342 yield m.setLastText("hotfixVersion", this._prefs.get("extensions.hotfix.lastVersion", ""));
344 try {
345 let locale = Cc["@mozilla.org/chrome/chrome-registry;1"]
346 .getService(Ci.nsIXULChromeRegistry)
347 .getSelectedLocale("global");
348 yield m.setLastText("locale", locale);
349 } catch (ex) {
350 this._log.warn("Could not obtain application locale: " +
351 CommonUtils.exceptionStr(ex));
352 }
354 // FUTURE this should be retrieved periodically or at upload time.
355 yield this._recordIsTelemetryEnabled(m);
356 yield this._recordIsBlocklistEnabled(m);
357 yield this._recordDefaultBrowser(m);
358 },
360 _recordIsTelemetryEnabled: function (m) {
361 let enabled = isTelemetryEnabled(this._prefs);
362 this._log.debug("Recording telemetry enabled (" + TELEMETRY_PREF + "): " + enabled);
363 yield m.setDailyLastNumeric("isTelemetryEnabled", enabled ? 1 : 0);
364 },
366 _recordIsBlocklistEnabled: function (m) {
367 let enabled = this._prefs.get("extensions.blocklist.enabled", false);
368 this._log.debug("Recording blocklist enabled: " + enabled);
369 yield m.setDailyLastNumeric("isBlocklistEnabled", enabled ? 1 : 0);
370 },
372 _recordDefaultBrowser: function (m) {
373 let shellService;
374 try {
375 shellService = Cc["@mozilla.org/browser/shell-service;1"]
376 .getService(Ci.nsIShellService);
377 } catch (ex) {
378 this._log.warn("Could not obtain shell service: " +
379 CommonUtils.exceptionStr(ex));
380 }
382 let isDefault = -1;
384 if (shellService) {
385 try {
386 // This uses the same set of flags used by the pref pane.
387 isDefault = shellService.isDefaultBrowser(false, true) ? 1 : 0;
388 } catch (ex) {
389 this._log.warn("Could not determine if default browser: " +
390 CommonUtils.exceptionStr(ex));
391 }
392 }
394 return m.setDailyLastNumeric("isDefaultBrowser", isDefault);
395 },
397 collectDailyData: function () {
398 return this.storage.enqueueTransaction(function getDaily() {
399 let m = this.getMeasurement(AppUpdateMeasurement1.prototype.name,
400 AppUpdateMeasurement1.prototype.version);
402 let enabled = this._prefs.get("app.update.enabled", false);
403 yield m.setDailyLastNumeric("enabled", enabled ? 1 : 0);
405 let auto = this._prefs.get("app.update.auto", false);
406 yield m.setDailyLastNumeric("autoDownload", auto ? 1 : 0);
407 }.bind(this));
408 },
409 });
412 function SysInfoMeasurement() {
413 Metrics.Measurement.call(this);
414 }
416 SysInfoMeasurement.prototype = Object.freeze({
417 __proto__: Metrics.Measurement.prototype,
419 name: "sysinfo",
420 version: 2,
422 fields: {
423 cpuCount: {type: Metrics.Storage.FIELD_LAST_NUMERIC},
424 memoryMB: {type: Metrics.Storage.FIELD_LAST_NUMERIC},
425 manufacturer: LAST_TEXT_FIELD,
426 device: LAST_TEXT_FIELD,
427 hardware: LAST_TEXT_FIELD,
428 name: LAST_TEXT_FIELD,
429 version: LAST_TEXT_FIELD,
430 architecture: LAST_TEXT_FIELD,
431 isWow64: LAST_NUMERIC_FIELD,
432 },
433 });
436 this.SysInfoProvider = function SysInfoProvider() {
437 Metrics.Provider.call(this);
438 };
440 SysInfoProvider.prototype = Object.freeze({
441 __proto__: Metrics.Provider.prototype,
443 name: "org.mozilla.sysinfo",
445 measurementTypes: [SysInfoMeasurement],
447 pullOnly: true,
449 sysInfoFields: {
450 cpucount: "cpuCount",
451 memsize: "memoryMB",
452 manufacturer: "manufacturer",
453 device: "device",
454 hardware: "hardware",
455 name: "name",
456 version: "version",
457 arch: "architecture",
458 isWow64: "isWow64",
459 },
461 collectConstantData: function () {
462 return this.storage.enqueueTransaction(this._populateConstants.bind(this));
463 },
465 _populateConstants: function () {
466 let m = this.getMeasurement(SysInfoMeasurement.prototype.name,
467 SysInfoMeasurement.prototype.version);
469 let si = Cc["@mozilla.org/system-info;1"]
470 .getService(Ci.nsIPropertyBag2);
472 for (let [k, v] in Iterator(this.sysInfoFields)) {
473 try {
474 if (!si.hasKey(k)) {
475 this._log.debug("Property not available: " + k);
476 continue;
477 }
479 let value = si.getProperty(k);
480 let method = "setLastText";
482 if (["cpucount", "memsize"].indexOf(k) != -1) {
483 let converted = parseInt(value, 10);
484 if (Number.isNaN(converted)) {
485 continue;
486 }
488 value = converted;
489 method = "setLastNumeric";
490 }
492 switch (k) {
493 case "memsize":
494 // Round memory to mebibytes.
495 value = Math.round(value / 1048576);
496 break;
497 case "isWow64":
498 // Property is only present on Windows. hasKey() skipping from
499 // above ensures undefined or null doesn't creep in here.
500 value = value ? 1 : 0;
501 method = "setLastNumeric";
502 break;
503 }
505 yield m[method](v, value);
506 } catch (ex) {
507 this._log.warn("Error obtaining system info field: " + k + " " +
508 CommonUtils.exceptionStr(ex));
509 }
510 }
511 },
512 });
515 /**
516 * Holds information about the current/active session.
517 *
518 * The fields within the current session are moved to daily session fields when
519 * the application is shut down.
520 *
521 * This measurement is backed by the SessionRecorder, not the database.
522 */
523 function CurrentSessionMeasurement() {
524 Metrics.Measurement.call(this);
525 }
527 CurrentSessionMeasurement.prototype = Object.freeze({
528 __proto__: Metrics.Measurement.prototype,
530 name: "current",
531 version: 3,
533 // Storage is in preferences.
534 fields: {},
536 /**
537 * All data is stored in prefs, so we have a custom implementation.
538 */
539 getValues: function () {
540 let sessions = this.provider.healthReporter.sessionRecorder;
542 let fields = new Map();
543 let now = new Date();
544 fields.set("startDay", [now, Metrics.dateToDays(sessions.startDate)]);
545 fields.set("activeTicks", [now, sessions.activeTicks]);
546 fields.set("totalTime", [now, sessions.totalTime]);
547 fields.set("main", [now, sessions.main]);
548 fields.set("firstPaint", [now, sessions.firstPaint]);
549 fields.set("sessionRestored", [now, sessions.sessionRestored]);
551 return CommonUtils.laterTickResolvingPromise({
552 days: new Metrics.DailyValues(),
553 singular: fields,
554 });
555 },
557 _serializeJSONSingular: function (data) {
558 let result = {"_v": this.version};
560 for (let [field, value] of data) {
561 result[field] = value[1];
562 }
564 return result;
565 },
566 });
568 /**
569 * Records a history of all application sessions.
570 */
571 function PreviousSessionsMeasurement() {
572 Metrics.Measurement.call(this);
573 }
575 PreviousSessionsMeasurement.prototype = Object.freeze({
576 __proto__: Metrics.Measurement.prototype,
578 name: "previous",
579 version: 3,
581 fields: {
582 // Milliseconds of sessions that were properly shut down.
583 cleanActiveTicks: DAILY_DISCRETE_NUMERIC_FIELD,
584 cleanTotalTime: DAILY_DISCRETE_NUMERIC_FIELD,
586 // Milliseconds of sessions that were not properly shut down.
587 abortedActiveTicks: DAILY_DISCRETE_NUMERIC_FIELD,
588 abortedTotalTime: DAILY_DISCRETE_NUMERIC_FIELD,
590 // Startup times in milliseconds.
591 main: DAILY_DISCRETE_NUMERIC_FIELD,
592 firstPaint: DAILY_DISCRETE_NUMERIC_FIELD,
593 sessionRestored: DAILY_DISCRETE_NUMERIC_FIELD,
594 },
595 });
598 /**
599 * Records information about the current browser session.
600 *
601 * A browser session is defined as an application/process lifetime. We
602 * start a new session when the application starts (essentially when
603 * this provider is instantiated) and end the session on shutdown.
604 *
605 * As the application runs, we record basic information about the
606 * "activity" of the session. Activity is defined by the presence of
607 * physical input into the browser (key press, mouse click, touch, etc).
608 *
609 * We differentiate between regular sessions and "aborted" sessions. An
610 * aborted session is one that does not end expectedly. This is often the
611 * result of a crash. We detect aborted sessions by storing the current
612 * session separate from completed sessions. We normally move the
613 * current session to completed sessions on application shutdown. If a
614 * current session is present on application startup, that means that
615 * the previous session was aborted.
616 */
617 this.SessionsProvider = function () {
618 Metrics.Provider.call(this);
619 };
621 SessionsProvider.prototype = Object.freeze({
622 __proto__: Metrics.Provider.prototype,
624 name: "org.mozilla.appSessions",
626 measurementTypes: [CurrentSessionMeasurement, PreviousSessionsMeasurement],
628 pullOnly: true,
630 collectConstantData: function () {
631 let previous = this.getMeasurement("previous", 3);
633 return this.storage.enqueueTransaction(this._recordAndPruneSessions.bind(this));
634 },
636 _recordAndPruneSessions: function () {
637 this._log.info("Moving previous sessions from session recorder to storage.");
638 let recorder = this.healthReporter.sessionRecorder;
639 let sessions = recorder.getPreviousSessions();
640 this._log.debug("Found " + Object.keys(sessions).length + " previous sessions.");
642 let daily = this.getMeasurement("previous", 3);
644 // Please note the coupling here between the session recorder and our state.
645 // If the pruned index or the current index of the session recorder is ever
646 // deleted or reset to 0, our stored state of a later index would mean that
647 // new sessions would never be captured by this provider until the session
648 // recorder index catches up to our last session ID. This should not happen
649 // under normal circumstances, so we don't worry too much about it. We
650 // should, however, consider this as part of implementing bug 841561.
651 let lastRecordedSession = yield this.getState("lastSession");
652 if (lastRecordedSession === null) {
653 lastRecordedSession = -1;
654 }
655 this._log.debug("The last recorded session was #" + lastRecordedSession);
657 for (let [index, session] in Iterator(sessions)) {
658 if (index <= lastRecordedSession) {
659 this._log.warn("Already recorded session " + index + ". Did the last " +
660 "session crash or have an issue saving the prefs file?");
661 continue;
662 }
664 let type = session.clean ? "clean" : "aborted";
665 let date = session.startDate;
666 yield daily.addDailyDiscreteNumeric(type + "ActiveTicks", session.activeTicks, date);
667 yield daily.addDailyDiscreteNumeric(type + "TotalTime", session.totalTime, date);
669 for (let field of ["main", "firstPaint", "sessionRestored"]) {
670 yield daily.addDailyDiscreteNumeric(field, session[field], date);
671 }
673 lastRecordedSession = index;
674 }
676 yield this.setState("lastSession", "" + lastRecordedSession);
677 recorder.pruneOldSessions(new Date());
678 },
679 });
681 /**
682 * Stores the set of active addons in storage.
683 *
684 * We do things a little differently than most other measurements. Because
685 * addons are difficult to shoehorn into distinct fields, we simply store a
686 * JSON blob in storage in a text field.
687 */
688 function ActiveAddonsMeasurement() {
689 Metrics.Measurement.call(this);
691 this._serializers = {};
692 this._serializers[this.SERIALIZE_JSON] = {
693 singular: this._serializeJSONSingular.bind(this),
694 // We don't need a daily serializer because we have none of this data.
695 };
696 }
698 ActiveAddonsMeasurement.prototype = Object.freeze({
699 __proto__: Metrics.Measurement.prototype,
701 name: "addons",
702 version: 2,
704 fields: {
705 addons: LAST_TEXT_FIELD,
706 },
708 _serializeJSONSingular: function (data) {
709 if (!data.has("addons")) {
710 this._log.warn("Don't have addons info. Weird.");
711 return null;
712 }
714 // Exceptions are caught in the caller.
715 let result = JSON.parse(data.get("addons")[1]);
716 result._v = this.version;
717 return result;
718 },
719 });
721 /**
722 * Stores the set of active plugins in storage.
723 *
724 * This stores the data in a JSON blob in a text field similar to the
725 * ActiveAddonsMeasurement.
726 */
727 function ActivePluginsMeasurement() {
728 Metrics.Measurement.call(this);
730 this._serializers = {};
731 this._serializers[this.SERIALIZE_JSON] = {
732 singular: this._serializeJSONSingular.bind(this),
733 // We don't need a daily serializer because we have none of this data.
734 };
735 }
737 ActivePluginsMeasurement.prototype = Object.freeze({
738 __proto__: Metrics.Measurement.prototype,
740 name: "plugins",
741 version: 1,
743 fields: {
744 plugins: LAST_TEXT_FIELD,
745 },
747 _serializeJSONSingular: function (data) {
748 if (!data.has("plugins")) {
749 this._log.warn("Don't have plugins info. Weird.");
750 return null;
751 }
753 // Exceptions are caught in the caller.
754 let result = JSON.parse(data.get("plugins")[1]);
755 result._v = this.version;
756 return result;
757 },
758 });
761 function AddonCountsMeasurement() {
762 Metrics.Measurement.call(this);
763 }
765 AddonCountsMeasurement.prototype = Object.freeze({
766 __proto__: Metrics.Measurement.prototype,
768 name: "counts",
769 version: 2,
771 fields: {
772 theme: DAILY_LAST_NUMERIC_FIELD,
773 lwtheme: DAILY_LAST_NUMERIC_FIELD,
774 plugin: DAILY_LAST_NUMERIC_FIELD,
775 extension: DAILY_LAST_NUMERIC_FIELD,
776 service: DAILY_LAST_NUMERIC_FIELD,
777 },
778 });
781 /**
782 * Legacy version of addons counts before services was added.
783 */
784 function AddonCountsMeasurement1() {
785 Metrics.Measurement.call(this);
786 }
788 AddonCountsMeasurement1.prototype = Object.freeze({
789 __proto__: Metrics.Measurement.prototype,
791 name: "counts",
792 version: 1,
794 fields: {
795 theme: DAILY_LAST_NUMERIC_FIELD,
796 lwtheme: DAILY_LAST_NUMERIC_FIELD,
797 plugin: DAILY_LAST_NUMERIC_FIELD,
798 extension: DAILY_LAST_NUMERIC_FIELD,
799 },
800 });
803 this.AddonsProvider = function () {
804 Metrics.Provider.call(this);
806 this._prefs = new Preferences({defaultBranch: null});
807 };
809 AddonsProvider.prototype = Object.freeze({
810 __proto__: Metrics.Provider.prototype,
812 // Whenever these AddonListener callbacks are called, we repopulate
813 // and store the set of addons. Note that these events will only fire
814 // for restartless add-ons. For actions that require a restart, we
815 // will catch the change after restart. The alternative is a lot of
816 // state tracking here, which isn't desirable.
817 ADDON_LISTENER_CALLBACKS: [
818 "onEnabled",
819 "onDisabled",
820 "onInstalled",
821 "onUninstalled",
822 ],
824 // Add-on types for which full details are uploaded in the
825 // ActiveAddonsMeasurement. All other types are ignored.
826 FULL_DETAIL_TYPES: [
827 "extension",
828 "service",
829 ],
831 name: "org.mozilla.addons",
833 measurementTypes: [
834 ActiveAddonsMeasurement,
835 ActivePluginsMeasurement,
836 AddonCountsMeasurement1,
837 AddonCountsMeasurement,
838 ],
840 postInit: function () {
841 let listener = {};
843 for (let method of this.ADDON_LISTENER_CALLBACKS) {
844 listener[method] = this._collectAndStoreAddons.bind(this);
845 }
847 this._listener = listener;
848 AddonManager.addAddonListener(this._listener);
850 return CommonUtils.laterTickResolvingPromise();
851 },
853 onShutdown: function () {
854 AddonManager.removeAddonListener(this._listener);
855 this._listener = null;
857 return CommonUtils.laterTickResolvingPromise();
858 },
860 collectConstantData: function () {
861 return this._collectAndStoreAddons();
862 },
864 _collectAndStoreAddons: function () {
865 let deferred = Promise.defer();
867 AddonManager.getAllAddons(function onAllAddons(addons) {
868 let data;
869 let addonsField;
870 let pluginsField;
871 try {
872 data = this._createDataStructure(addons);
873 addonsField = JSON.stringify(data.addons);
874 pluginsField = JSON.stringify(data.plugins);
875 } catch (ex) {
876 this._log.warn("Exception when populating add-ons data structure: " +
877 CommonUtils.exceptionStr(ex));
878 deferred.reject(ex);
879 return;
880 }
882 let now = new Date();
883 let addons = this.getMeasurement("addons", 2);
884 let plugins = this.getMeasurement("plugins", 1);
885 let counts = this.getMeasurement(AddonCountsMeasurement.prototype.name,
886 AddonCountsMeasurement.prototype.version);
888 this.enqueueStorageOperation(function storageAddons() {
889 for (let type in data.counts) {
890 try {
891 counts.fieldID(type);
892 } catch (ex) {
893 this._log.warn("Add-on type without field: " + type);
894 continue;
895 }
897 counts.setDailyLastNumeric(type, data.counts[type], now);
898 }
900 return addons.setLastText("addons", addonsField).then(
901 function onSuccess() {
902 return plugins.setLastText("plugins", pluginsField).then(
903 function onSuccess() { deferred.resolve(); },
904 function onError(error) { deferred.reject(error); }
905 );
906 },
907 function onError(error) { deferred.reject(error); }
908 );
909 }.bind(this));
910 }.bind(this));
912 return deferred.promise;
913 },
915 COPY_ADDON_FIELDS: [
916 "userDisabled",
917 "appDisabled",
918 "name",
919 "version",
920 "type",
921 "scope",
922 "description",
923 "foreignInstall",
924 "hasBinaryComponents",
925 ],
927 COPY_PLUGIN_FIELDS: [
928 "name",
929 "version",
930 "description",
931 "blocklisted",
932 "disabled",
933 "clicktoplay",
934 ],
936 _createDataStructure: function (addons) {
937 let data = {
938 addons: {},
939 plugins: {},
940 counts: {}
941 };
943 for (let addon of addons) {
944 let type = addon.type;
946 // We count plugins separately below.
947 if (addon.type == "plugin")
948 continue;
950 data.counts[type] = (data.counts[type] || 0) + 1;
952 if (this.FULL_DETAIL_TYPES.indexOf(addon.type) == -1) {
953 continue;
954 }
956 let obj = {};
957 for (let field of this.COPY_ADDON_FIELDS) {
958 obj[field] = addon[field];
959 }
961 if (addon.installDate) {
962 obj.installDay = this._dateToDays(addon.installDate);
963 }
965 if (addon.updateDate) {
966 obj.updateDay = this._dateToDays(addon.updateDate);
967 }
969 data.addons[addon.id] = obj;
970 }
972 let pluginTags = Cc["@mozilla.org/plugin/host;1"].
973 getService(Ci.nsIPluginHost).
974 getPluginTags({});
976 for (let tag of pluginTags) {
977 let obj = {
978 mimeTypes: tag.getMimeTypes({}),
979 };
981 for (let field of this.COPY_PLUGIN_FIELDS) {
982 obj[field] = tag[field];
983 }
985 // Plugins need to have a filename and a name, so this can't be empty.
986 let id = tag.filename + ":" + tag.name + ":" + tag.version + ":"
987 + tag.description;
988 data.plugins[id] = obj;
989 }
991 data.counts["plugin"] = pluginTags.length;
993 return data;
994 },
995 });
997 #ifdef MOZ_CRASHREPORTER
999 function DailyCrashesMeasurement1() {
1000 Metrics.Measurement.call(this);
1001 }
1003 DailyCrashesMeasurement1.prototype = Object.freeze({
1004 __proto__: Metrics.Measurement.prototype,
1006 name: "crashes",
1007 version: 1,
1009 fields: {
1010 pending: DAILY_COUNTER_FIELD,
1011 submitted: DAILY_COUNTER_FIELD,
1012 },
1013 });
1015 function DailyCrashesMeasurement2() {
1016 Metrics.Measurement.call(this);
1017 }
1019 DailyCrashesMeasurement2.prototype = Object.freeze({
1020 __proto__: Metrics.Measurement.prototype,
1022 name: "crashes",
1023 version: 2,
1025 fields: {
1026 mainCrash: DAILY_LAST_NUMERIC_FIELD,
1027 },
1028 });
1030 this.CrashesProvider = function () {
1031 Metrics.Provider.call(this);
1033 // So we can unit test.
1034 this._manager = Services.crashmanager;
1035 };
1037 CrashesProvider.prototype = Object.freeze({
1038 __proto__: Metrics.Provider.prototype,
1040 name: "org.mozilla.crashes",
1042 measurementTypes: [
1043 DailyCrashesMeasurement1,
1044 DailyCrashesMeasurement2,
1045 ],
1047 pullOnly: true,
1049 collectDailyData: function () {
1050 return this.storage.enqueueTransaction(this._populateCrashCounts.bind(this));
1051 },
1053 _populateCrashCounts: function () {
1054 this._log.info("Grabbing crash counts from crash manager.");
1055 let crashCounts = yield this._manager.getCrashCountsByDay();
1056 let fields = {
1057 "main-crash": "mainCrash",
1058 };
1060 let m = this.getMeasurement("crashes", 2);
1062 for (let [day, types] of crashCounts) {
1063 let date = Metrics.daysToDate(day);
1064 for (let [type, count] of types) {
1065 if (!(type in fields)) {
1066 this._log.warn("Unknown crash type encountered: " + type);
1067 continue;
1068 }
1070 yield m.setDailyLastNumeric(fields[type], count, date);
1071 }
1072 }
1073 },
1074 });
1076 #endif
1079 /**
1080 * Holds basic statistics about the Places database.
1081 */
1082 function PlacesMeasurement() {
1083 Metrics.Measurement.call(this);
1084 }
1086 PlacesMeasurement.prototype = Object.freeze({
1087 __proto__: Metrics.Measurement.prototype,
1089 name: "places",
1090 version: 1,
1092 fields: {
1093 pages: DAILY_LAST_NUMERIC_FIELD,
1094 bookmarks: DAILY_LAST_NUMERIC_FIELD,
1095 },
1096 });
1099 /**
1100 * Collects information about Places.
1101 */
1102 this.PlacesProvider = function () {
1103 Metrics.Provider.call(this);
1104 };
1106 PlacesProvider.prototype = Object.freeze({
1107 __proto__: Metrics.Provider.prototype,
1109 name: "org.mozilla.places",
1111 measurementTypes: [PlacesMeasurement],
1113 collectDailyData: function () {
1114 return this.storage.enqueueTransaction(this._collectData.bind(this));
1115 },
1117 _collectData: function () {
1118 let now = new Date();
1119 let data = yield this._getDailyValues();
1121 let m = this.getMeasurement("places", 1);
1123 yield m.setDailyLastNumeric("pages", data.PLACES_PAGES_COUNT);
1124 yield m.setDailyLastNumeric("bookmarks", data.PLACES_BOOKMARKS_COUNT);
1125 },
1127 _getDailyValues: function () {
1128 let deferred = Promise.defer();
1130 PlacesDBUtils.telemetry(null, function onResult(data) {
1131 deferred.resolve(data);
1132 });
1134 return deferred.promise;
1135 },
1136 });
1138 function SearchCountMeasurement1() {
1139 Metrics.Measurement.call(this);
1140 }
1142 SearchCountMeasurement1.prototype = Object.freeze({
1143 __proto__: Metrics.Measurement.prototype,
1145 name: "counts",
1146 version: 1,
1148 // We only record searches for search engines that have partner agreements
1149 // with Mozilla.
1150 fields: {
1151 "amazon.com.abouthome": DAILY_COUNTER_FIELD,
1152 "amazon.com.contextmenu": DAILY_COUNTER_FIELD,
1153 "amazon.com.searchbar": DAILY_COUNTER_FIELD,
1154 "amazon.com.urlbar": DAILY_COUNTER_FIELD,
1155 "bing.abouthome": DAILY_COUNTER_FIELD,
1156 "bing.contextmenu": DAILY_COUNTER_FIELD,
1157 "bing.searchbar": DAILY_COUNTER_FIELD,
1158 "bing.urlbar": DAILY_COUNTER_FIELD,
1159 "google.abouthome": DAILY_COUNTER_FIELD,
1160 "google.contextmenu": DAILY_COUNTER_FIELD,
1161 "google.searchbar": DAILY_COUNTER_FIELD,
1162 "google.urlbar": DAILY_COUNTER_FIELD,
1163 "yahoo.abouthome": DAILY_COUNTER_FIELD,
1164 "yahoo.contextmenu": DAILY_COUNTER_FIELD,
1165 "yahoo.searchbar": DAILY_COUNTER_FIELD,
1166 "yahoo.urlbar": DAILY_COUNTER_FIELD,
1167 "other.abouthome": DAILY_COUNTER_FIELD,
1168 "other.contextmenu": DAILY_COUNTER_FIELD,
1169 "other.searchbar": DAILY_COUNTER_FIELD,
1170 "other.urlbar": DAILY_COUNTER_FIELD,
1171 },
1172 });
1174 /**
1175 * Records search counts per day per engine and where search initiated.
1176 *
1177 * We want to record granular details for individual locale-specific search
1178 * providers, but only if they're Mozilla partners. In order to do this, we
1179 * track the nsISearchEngine identifier, which denotes shipped search engines,
1180 * and intersect those with our partner list.
1181 *
1182 * We don't use the search engine name directly, because it is shared across
1183 * locales; e.g., eBay-de and eBay both share the name "eBay".
1184 */
1185 function SearchCountMeasurementBase() {
1186 this._fieldSpecs = {};
1187 Metrics.Measurement.call(this);
1188 }
1190 SearchCountMeasurementBase.prototype = Object.freeze({
1191 __proto__: Metrics.Measurement.prototype,
1194 // Our fields are dynamic.
1195 get fields() {
1196 return this._fieldSpecs;
1197 },
1199 /**
1200 * Override the default behavior: serializers should include every counter
1201 * field from the DB, even if we don't currently have it registered.
1202 *
1203 * Do this so we don't have to register several hundred fields to match
1204 * various Firefox locales.
1205 *
1206 * We use the "provider.type" syntax as a rudimentary check for validity.
1207 *
1208 * We trust that measurement versioning is sufficient to exclude old provider
1209 * data.
1210 */
1211 shouldIncludeField: function (name) {
1212 return name.contains(".");
1213 },
1215 /**
1216 * The measurement type mechanism doesn't introspect the DB. Override it
1217 * so that we can assume all unknown fields are counters.
1218 */
1219 fieldType: function (name) {
1220 if (name in this.fields) {
1221 return this.fields[name].type;
1222 }
1224 // Default to a counter.
1225 return Metrics.Storage.FIELD_DAILY_COUNTER;
1226 },
1228 SOURCES: [
1229 "abouthome",
1230 "contextmenu",
1231 "newtab",
1232 "searchbar",
1233 "urlbar",
1234 ],
1235 });
1237 function SearchCountMeasurement2() {
1238 SearchCountMeasurementBase.call(this);
1239 }
1241 SearchCountMeasurement2.prototype = Object.freeze({
1242 __proto__: SearchCountMeasurementBase.prototype,
1243 name: "counts",
1244 version: 2,
1245 });
1247 function SearchCountMeasurement3() {
1248 SearchCountMeasurementBase.call(this);
1249 }
1251 SearchCountMeasurement3.prototype = Object.freeze({
1252 __proto__: SearchCountMeasurementBase.prototype,
1253 name: "counts",
1254 version: 3,
1256 getEngines: function () {
1257 return Services.search.getEngines();
1258 },
1260 getEngineID: function (engine) {
1261 if (!engine) {
1262 return "other";
1263 }
1264 if (engine.identifier) {
1265 return engine.identifier;
1266 }
1267 return "other-" + engine.name;
1268 },
1269 });
1271 function SearchEnginesMeasurement1() {
1272 Metrics.Measurement.call(this);
1273 }
1275 SearchEnginesMeasurement1.prototype = Object.freeze({
1276 __proto__: Metrics.Measurement.prototype,
1278 name: "engines",
1279 version: 1,
1281 fields: {
1282 default: DAILY_LAST_TEXT_FIELD,
1283 },
1284 });
1286 this.SearchesProvider = function () {
1287 Metrics.Provider.call(this);
1289 this._prefs = new Preferences({defaultBranch: null});
1290 };
1292 this.SearchesProvider.prototype = Object.freeze({
1293 __proto__: Metrics.Provider.prototype,
1295 name: "org.mozilla.searches",
1296 measurementTypes: [
1297 SearchCountMeasurement1,
1298 SearchCountMeasurement2,
1299 SearchCountMeasurement3,
1300 SearchEnginesMeasurement1,
1301 ],
1303 /**
1304 * Initialize the search service before our measurements are touched.
1305 */
1306 preInit: function (storage) {
1307 // Initialize search service.
1308 let deferred = Promise.defer();
1309 Services.search.init(function onInitComplete () {
1310 deferred.resolve();
1311 });
1312 return deferred.promise;
1313 },
1315 collectDailyData: function () {
1316 return this.storage.enqueueTransaction(function getDaily() {
1317 // We currently only record this if Telemetry is enabled.
1318 if (!isTelemetryEnabled(this._prefs)) {
1319 return;
1320 }
1322 let m = this.getMeasurement(SearchEnginesMeasurement1.prototype.name,
1323 SearchEnginesMeasurement1.prototype.version);
1325 let engine;
1326 try {
1327 engine = Services.search.defaultEngine;
1328 } catch (e) {}
1329 let name;
1331 if (!engine) {
1332 name = "NONE";
1333 } else if (engine.identifier) {
1334 name = engine.identifier;
1335 } else if (engine.name) {
1336 name = "other-" + engine.name;
1337 } else {
1338 name = "UNDEFINED";
1339 }
1341 yield m.setDailyLastText("default", name);
1342 }.bind(this));
1343 },
1345 /**
1346 * Record that a search occurred.
1347 *
1348 * @param engine
1349 * (nsISearchEngine) The search engine used.
1350 * @param source
1351 * (string) Where the search was initiated from. Must be one of the
1352 * SearchCountMeasurement2.SOURCES values.
1353 *
1354 * @return Promise<>
1355 * The promise is resolved when the storage operation completes.
1356 */
1357 recordSearch: function (engine, source) {
1358 let m = this.getMeasurement("counts", 3);
1360 if (m.SOURCES.indexOf(source) == -1) {
1361 throw new Error("Unknown source for search: " + source);
1362 }
1364 let field = m.getEngineID(engine) + "." + source;
1365 if (this.storage.hasFieldFromMeasurement(m.id, field,
1366 this.storage.FIELD_DAILY_COUNTER)) {
1367 let fieldID = this.storage.fieldIDFromMeasurement(m.id, field);
1368 return this.enqueueStorageOperation(function recordSearchKnownField() {
1369 return this.storage.incrementDailyCounterFromFieldID(fieldID);
1370 }.bind(this));
1371 }
1373 // Otherwise, we first need to create the field.
1374 return this.enqueueStorageOperation(function recordFieldAndSearch() {
1375 // This function has to return a promise.
1376 return Task.spawn(function () {
1377 let fieldID = yield this.storage.registerField(m.id, field,
1378 this.storage.FIELD_DAILY_COUNTER);
1379 yield this.storage.incrementDailyCounterFromFieldID(fieldID);
1380 }.bind(this));
1381 }.bind(this));
1382 },
1383 });
1385 function HealthReportSubmissionMeasurement1() {
1386 Metrics.Measurement.call(this);
1387 }
1389 HealthReportSubmissionMeasurement1.prototype = Object.freeze({
1390 __proto__: Metrics.Measurement.prototype,
1392 name: "submissions",
1393 version: 1,
1395 fields: {
1396 firstDocumentUploadAttempt: DAILY_COUNTER_FIELD,
1397 continuationUploadAttempt: DAILY_COUNTER_FIELD,
1398 uploadSuccess: DAILY_COUNTER_FIELD,
1399 uploadTransportFailure: DAILY_COUNTER_FIELD,
1400 uploadServerFailure: DAILY_COUNTER_FIELD,
1401 uploadClientFailure: DAILY_COUNTER_FIELD,
1402 },
1403 });
1405 function HealthReportSubmissionMeasurement2() {
1406 Metrics.Measurement.call(this);
1407 }
1409 HealthReportSubmissionMeasurement2.prototype = Object.freeze({
1410 __proto__: Metrics.Measurement.prototype,
1412 name: "submissions",
1413 version: 2,
1415 fields: {
1416 firstDocumentUploadAttempt: DAILY_COUNTER_FIELD,
1417 continuationUploadAttempt: DAILY_COUNTER_FIELD,
1418 uploadSuccess: DAILY_COUNTER_FIELD,
1419 uploadTransportFailure: DAILY_COUNTER_FIELD,
1420 uploadServerFailure: DAILY_COUNTER_FIELD,
1421 uploadClientFailure: DAILY_COUNTER_FIELD,
1422 uploadAlreadyInProgress: DAILY_COUNTER_FIELD,
1423 },
1424 });
1426 this.HealthReportProvider = function () {
1427 Metrics.Provider.call(this);
1428 }
1430 HealthReportProvider.prototype = Object.freeze({
1431 __proto__: Metrics.Provider.prototype,
1433 name: "org.mozilla.healthreport",
1435 measurementTypes: [
1436 HealthReportSubmissionMeasurement1,
1437 HealthReportSubmissionMeasurement2,
1438 ],
1440 recordEvent: function (event, date=new Date()) {
1441 let m = this.getMeasurement("submissions", 2);
1442 return this.enqueueStorageOperation(function recordCounter() {
1443 return m.incrementDailyCounter(event, date);
1444 });
1445 },
1446 });