зеркало из https://github.com/mozilla/gecko-dev.git
Bug 808219 - Firefox Health Reporter service; r=rnewman
This commit is contained in:
Родитель
c5073721ca
Коммит
9fe6a8cad5
|
@ -483,6 +483,10 @@
|
|||
@BINPATH@/components/WeaveCrypto.manifest
|
||||
@BINPATH@/components/WeaveCrypto.js
|
||||
#endif
|
||||
#ifdef MOZ_SERVICES_HEALTHREPORT
|
||||
@BINPATH@/components/HealthReportComponents.manifest
|
||||
@BINPATH@/components/HealthReportService.js
|
||||
#endif
|
||||
@BINPATH@/components/TelemetryPing.js
|
||||
@BINPATH@/components/TelemetryPing.manifest
|
||||
@BINPATH@/components/Webapps.js
|
||||
|
@ -574,6 +578,9 @@
|
|||
#ifdef MOZ_SERVICES_SYNC
|
||||
@BINPATH@/@PREF_DIR@/services-sync.js
|
||||
#endif
|
||||
#ifdef MOZ_SERVICES_HEALTHREPORT
|
||||
@BINPATH@/@PREF_DIR@/healthreport-prefs.js
|
||||
#endif
|
||||
@BINPATH@/greprefs.js
|
||||
@BINPATH@/defaults/autoconfig/platform.js
|
||||
@BINPATH@/defaults/autoconfig/prefcalls.js
|
||||
|
|
|
@ -463,6 +463,10 @@
|
|||
@BINPATH@/components/AitcComponents.manifest
|
||||
@BINPATH@/components/Aitc.js
|
||||
#endif
|
||||
#ifdef MOZ_SERVICES_HEALTHREPORT
|
||||
@BINPATH@/components/HealthReportComponents.manifest
|
||||
@BINPATH@/components/HealthReportService.js
|
||||
#endif
|
||||
#ifdef MOZ_SERVICES_NOTIFICATIONS
|
||||
@BINPATH@/components/NotificationsComponents.manifest
|
||||
#endif
|
||||
|
@ -570,6 +574,9 @@
|
|||
#ifdef MOZ_SERVICES_SYNC
|
||||
@BINPATH@/@PREF_DIR@/services-sync.js
|
||||
#endif
|
||||
#ifdef MOZ_SERVICES_HEALTHREPORT
|
||||
@BINPATH@/@PREF_DIR@/healthreport-prefs.js
|
||||
#endif
|
||||
@BINPATH@/greprefs.js
|
||||
@BINPATH@/defaults/autoconfig/platform.js
|
||||
@BINPATH@/defaults/autoconfig/prefcalls.js
|
||||
|
|
|
@ -357,6 +357,11 @@
|
|||
@BINPATH@/components/TCPSocketParentIntermediary.js
|
||||
@BINPATH@/components/TCPSocket.manifest
|
||||
|
||||
#ifdef MOZ_SERVICES_HEALTHREPORT
|
||||
@BINPATH@/components/HealthReportComponents.manifest
|
||||
@BINPATH@/components/HealthReportService.js
|
||||
#endif
|
||||
|
||||
; Modules
|
||||
@BINPATH@/modules/*
|
||||
|
||||
|
@ -403,6 +408,9 @@
|
|||
@BINPATH@/@PREF_DIR@/mobile.js
|
||||
@BINPATH@/@PREF_DIR@/mobile-branding.js
|
||||
@BINPATH@/@PREF_DIR@/channel-prefs.js
|
||||
#ifdef MOZ_SERVICES_HEALTHREPORT
|
||||
@BINPATH@/@PREF_DIR@/healthreport-prefs.js
|
||||
#endif
|
||||
@BINPATH@/greprefs.js
|
||||
@BINPATH@/defaults/autoconfig/platform.js
|
||||
@BINPATH@/defaults/autoconfig/prefcalls.js
|
||||
|
|
|
@ -435,6 +435,10 @@
|
|||
@BINPATH@/components/WeaveCrypto.manifest
|
||||
@BINPATH@/components/WeaveCrypto.js
|
||||
#endif
|
||||
#ifdef MOZ_SERVICES_HEALTHREPORT
|
||||
@BINPATH@/components/HealthReportComponents.manifest
|
||||
@BINPATH@/components/HealthReportService.js
|
||||
#endif
|
||||
@BINPATH@/components/TelemetryPing.js
|
||||
@BINPATH@/components/TelemetryPing.manifest
|
||||
|
||||
|
@ -501,6 +505,9 @@
|
|||
#ifdef MOZ_SERVICES_SYNC
|
||||
@BINPATH@/@PREF_DIR@/services-sync.js
|
||||
#endif
|
||||
#ifdef MOZ_SERVICES_HEALTHREPORT
|
||||
@BINPATH@/@PREF_DIR@/healthreport-prefs.js
|
||||
#endif
|
||||
@BINPATH@/greprefs.js
|
||||
@BINPATH@/defaults/autoconfig/platform.js
|
||||
@BINPATH@/defaults/autoconfig/prefcalls.js
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
# b2g: {3c2e2abc-06d4-11e1-ac3b-374f68613e61}
|
||||
# browser: {ec8030f7-c20a-464f-9b0e-13a3a9e97384}
|
||||
# mobile/android: {aa3c5121-dab2-40e2-81ca-7ea25febc110}
|
||||
# mobile/xul: {a23983c0-fd0e-11dc-95ff-0800200c9a66}
|
||||
# suite (comm): {92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}
|
||||
# metro browser: {99bceaaa-e3c6-48c1-b981-ef9b46b67d60}
|
||||
|
||||
component {e354c59b-b252-4040-b6dd-b71864e3e35c} HealthReportService.js
|
||||
contract @mozilla.org/healthreport/service;1 {e354c59b-b252-4040-b6dd-b71864e3e35c}
|
||||
category app-startup HealthReportService service,@mozilla.org/healthreport/service;1 application={3c2e2abc-06d4-11e1-ac3b-374f68613e61} application={ec8030f7-c20a-464f-9b0e-13a3a9e97384} application={aa3c5121-dab2-40e2-81ca-7ea25febc110} application={a23983c0-fd0e-11dc-95ff-0800200c9a66}
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://services-common/preferences.js");
|
||||
|
||||
|
||||
const INITIAL_STARTUP_DELAY_MSEC = 10 * 1000;
|
||||
const BRANCH = "healthreport.";
|
||||
const JS_PROVIDERS_CATEGORY = "healthreport-js-provider";
|
||||
|
||||
|
||||
/**
|
||||
* The Firefox Health Report XPCOM service.
|
||||
*
|
||||
* This instantiates an instance of HealthReporter (assuming it is enabled)
|
||||
* and starts it upon application startup.
|
||||
*
|
||||
* One can obtain a reference to the underlying HealthReporter instance by
|
||||
* accessing .reporter. If this property is null, the reporter isn't running
|
||||
* yet or has been disabled.
|
||||
*/
|
||||
this.HealthReportService = function HealthReportService() {
|
||||
this.wrappedJSObject = this;
|
||||
|
||||
this.prefs = new Preferences(BRANCH);
|
||||
this._reporter = null;
|
||||
}
|
||||
|
||||
HealthReportService.prototype = {
|
||||
classID: Components.ID("{e354c59b-b252-4040-b6dd-b71864e3e35c}"),
|
||||
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
|
||||
Ci.nsISupportsWeakReference]),
|
||||
|
||||
observe: function observe(subject, topic, data) {
|
||||
// If the background service is disabled, don't do anything.
|
||||
if (!this.prefs.get("serviceEnabled", true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let os = Cc["@mozilla.org/observer-service;1"]
|
||||
.getService(Ci.nsIObserverService);
|
||||
|
||||
switch (topic) {
|
||||
case "app-startup":
|
||||
os.addObserver(this, "final-ui-startup", true);
|
||||
break;
|
||||
|
||||
case "final-ui-startup":
|
||||
os.removeObserver(this, "final-ui-startup");
|
||||
os.addObserver(this, "quit-application", true);
|
||||
|
||||
// Delay service loading a little more so things have an opportunity
|
||||
// to cool down first.
|
||||
this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
|
||||
this.timer.initWithCallback({
|
||||
notify: function notify() {
|
||||
// Side effect: instantiates the reporter instance if not already
|
||||
// accessed.
|
||||
let reporter = this.reporter;
|
||||
delete this.timer;
|
||||
}.bind(this),
|
||||
}, INITIAL_STARTUP_DELAY_MSEC, this.timer.TYPE_ONE_SHOT);
|
||||
|
||||
break;
|
||||
|
||||
case "quit-application-granted":
|
||||
if (this.reporter) {
|
||||
this.reporter.stop();
|
||||
}
|
||||
|
||||
os.removeObserver(this, "quit-application");
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* The HealthReporter instance associated with this service.
|
||||
*/
|
||||
get reporter() {
|
||||
if (!this.prefs.get("serviceEnabled", true)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this._reporter) {
|
||||
return this._reporter;
|
||||
}
|
||||
|
||||
// Lazy import so application startup isn't adversely affected.
|
||||
let ns = {};
|
||||
Cu.import("resource://services-common/log4moz.js", ns);
|
||||
Cu.import("resource://gre/modules/services/healthreport/healthreporter.jsm", ns);
|
||||
|
||||
// How many times will we rewrite this code before rolling it up into a
|
||||
// generic module? See also bug 451283.
|
||||
const LOGGERS = [
|
||||
"Metrics",
|
||||
"Services.HealthReport",
|
||||
"Services.Metrics",
|
||||
"Services.BagheeraClient",
|
||||
];
|
||||
|
||||
let prefs = new Preferences(BRANCH + "logging.");
|
||||
if (prefs.get("consoleEnabled", true)) {
|
||||
let level = prefs.get("consoleLevel", "Warn");
|
||||
let appender = new ns.Log4Moz.ConsoleAppender();
|
||||
appender.level = ns.Log4Moz.Level[level] || ns.Log4Moz.Level.Warn;
|
||||
|
||||
for (let name of LOGGERS) {
|
||||
let logger = ns.Log4Moz.repository.getLogger(name);
|
||||
logger.addAppender(appender);
|
||||
}
|
||||
}
|
||||
|
||||
this._reporter = new ns.HealthReporter(BRANCH);
|
||||
this._reporter.registerProvidersFromCategoryManager(JS_PROVIDERS_CATEGORY);
|
||||
this._reporter.start();
|
||||
|
||||
return this._reporter;
|
||||
},
|
||||
};
|
||||
|
||||
Object.freeze(HealthReportService.prototype);
|
||||
|
||||
this.NSGetFactory = XPCOMUtils.generateNSGetFactory([HealthReportService]);
|
||||
|
|
@ -10,6 +10,7 @@ VPATH = @srcdir@
|
|||
include $(DEPTH)/config/autoconf.mk
|
||||
|
||||
modules := \
|
||||
healthreporter.jsm \
|
||||
policy.jsm \
|
||||
$(NULL)
|
||||
|
||||
|
@ -26,4 +27,11 @@ INSTALL_TARGETS += MODULES
|
|||
TESTING_JS_MODULES := $(addprefix modules-testing/,$(testing_modules))
|
||||
TESTING_JS_MODULE_DIR := services/healthreport
|
||||
|
||||
EXTRA_COMPONENTS := \
|
||||
HealthReportComponents.manifest \
|
||||
HealthReportService.js \
|
||||
$(NULL)
|
||||
|
||||
PREF_JS_EXPORTS := healthreport-prefs.js
|
||||
|
||||
include $(topsrcdir)/config/rules.mk
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
=====================
|
||||
Firefox Health Report
|
||||
=====================
|
||||
|
||||
This directory contains the implementation of the Firefox Health Report
|
||||
(FHR).
|
||||
|
||||
Firefox Health Report is a background service that collects application
|
||||
metrics and periodically submits them to a central server.
|
||||
|
||||
Implementation Notes
|
||||
====================
|
||||
|
||||
The XPCOM service powering FHR is defined in HealthReportService.js. It
|
||||
simply instantiates an instance of HealthReporter from healthreporter.jsm.
|
||||
|
||||
All the logic for enforcing the privacy policy and for scheduling data
|
||||
submissions lives in policy.jsm.
|
||||
|
||||
Preferences
|
||||
===========
|
||||
|
||||
Preferences controlling behavior of Firefox Health Report live in the
|
||||
*healthreport.* branch.
|
||||
|
||||
Some important preferences are:
|
||||
|
||||
* **healthreport.serviceEnabled** - Controls whether the entire health report
|
||||
service runs. The overall service performs data collection, storing, and
|
||||
submission.
|
||||
|
||||
* **healthreport.policy.dataSubmissionEnabled** - Controls whether data
|
||||
submission is enabled. If this is *false*, data will still be collected
|
||||
and stored - it just won't ever be submitted to a remote server.
|
||||
|
||||
If the entire service is disabled, you lose data collection. This means that
|
||||
data analysis won't be available because there is no data to analyze!
|
||||
|
||||
Other Notes
|
||||
===========
|
||||
|
||||
There are many legal and privacy concerns with this code, especially
|
||||
around the data that is submitted. Changes to submitted data should be
|
||||
signed off by responsible parties.
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
pref("healthreport.documentServerURI", "https://data.mozilla.com/");
|
||||
pref("healthreport.documentServerNamespace", "metrics");
|
||||
pref("healthreport.serviceEnabled", true);
|
||||
pref("healthreport.logging.consoleEnabled", true);
|
||||
pref("healthreport.logging.consoleLevel", "Warn");
|
||||
pref("healthreport.policy.currentDaySubmissionFailureCount", 0);
|
||||
pref("healthreport.policy.dataSubmissionEnabled", true);
|
||||
pref("healthreport.policy.dataSubmissionPolicyAccepted", false);
|
||||
pref("healthreport.policy.dataSubmissionPolicyNotifiedTime", "0");
|
||||
pref("healthreport.policy.dataSubmissionPolicyResponseType", "");
|
||||
pref("healthreport.policy.dataSubmissionPolicyResponseTime", "0");
|
||||
pref("healthreport.policy.firstRunTime", "0");
|
||||
pref("healthreport.policy.lastDataSubmissionFailureTime", "0");
|
||||
pref("healthreport.policy.lastDataSubmissionRequestedTime", "0");
|
||||
pref("healthreport.policy.lastDataSubmissionSuccessfulTime", "0");
|
||||
pref("healthreport.policy.nextDataSubmissionTime", "0");
|
||||
|
|
@ -0,0 +1,427 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["HealthReporter"];
|
||||
|
||||
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
|
||||
|
||||
Cu.import("resource://services-common/bagheeraclient.js");
|
||||
Cu.import("resource://services-common/log4moz.js");
|
||||
Cu.import("resource://services-common/observers.js");
|
||||
Cu.import("resource://services-common/preferences.js");
|
||||
Cu.import("resource://services-common/utils.js");
|
||||
Cu.import("resource://gre/modules/commonjs/promise/core.js");
|
||||
Cu.import("resource://gre/modules/services/healthreport/policy.jsm");
|
||||
Cu.import("resource://gre/modules/services/metrics/collector.jsm");
|
||||
|
||||
|
||||
// Oldest year to allow in date preferences. This module was implemented in
|
||||
// 2012 and no dates older than that should be encountered.
|
||||
const OLDEST_ALLOWED_YEAR = 2012;
|
||||
|
||||
|
||||
/**
|
||||
* Coordinates collection and submission of metrics.
|
||||
*
|
||||
* This is the main type for Firefox Health Report. It glues all the
|
||||
* lower-level components (such as collection and submission) together.
|
||||
*
|
||||
* An instance of this type is created as an XPCOM service. See
|
||||
* HealthReportService.js and HealthReportComponents.manifest.
|
||||
*
|
||||
* It is theoretically possible to have multiple instances of this running
|
||||
* in the application. For example, this type may one day handle submission
|
||||
* of telemetry data as well. However, there is some moderate coupling between
|
||||
* this type and *the* Firefox Health Report (e.g. the policy). This could
|
||||
* be abstracted if needed.
|
||||
*
|
||||
* @param branch
|
||||
* (string) The preferences branch to use for state storage. The value
|
||||
* must end with a period (.).
|
||||
*/
|
||||
this.HealthReporter = function HealthReporter(branch) {
|
||||
if (!branch.endsWith(".")) {
|
||||
throw new Error("Branch argument must end with a period (.): " + branch);
|
||||
}
|
||||
|
||||
this._log = Log4Moz.repository.getLogger("Services.HealthReport.HealthReporter");
|
||||
|
||||
this._prefs = new Preferences(branch);
|
||||
|
||||
let policyBranch = new Preferences(branch + "policy.");
|
||||
this._policy = new HealthReportPolicy(policyBranch, this);
|
||||
this._collector = new MetricsCollector();
|
||||
|
||||
if (!this.serverURI) {
|
||||
throw new Error("No server URI defined. Did you forget to define the pref?");
|
||||
}
|
||||
|
||||
if (!this.serverNamespace) {
|
||||
throw new Error("No server namespace defined. Did you forget a pref?");
|
||||
}
|
||||
}
|
||||
|
||||
HealthReporter.prototype = {
|
||||
/**
|
||||
* When we last successfully submitted data to the server.
|
||||
*
|
||||
* This is sent as part of the upload. This is redundant with similar data
|
||||
* in the policy because we like the modules to be loosely coupled and the
|
||||
* similar data in the policy is only used for forensic purposes.
|
||||
*/
|
||||
get lastPingDate() {
|
||||
return CommonUtils.getDatePref(this._prefs, "lastPingTime", 0, this._log,
|
||||
OLDEST_ALLOWED_YEAR);
|
||||
},
|
||||
|
||||
set lastPingDate(value) {
|
||||
CommonUtils.setDatePref(this._prefs, "lastPingTime", value,
|
||||
OLDEST_ALLOWED_YEAR);
|
||||
},
|
||||
|
||||
/**
|
||||
* The base URI of the document server to which to submit data.
|
||||
*
|
||||
* This is typically a Bagheera server instance. It is the URI up to but not
|
||||
* including the version prefix. e.g. https://data.metrics.mozilla.com/
|
||||
*/
|
||||
get serverURI() {
|
||||
return this._prefs.get("documentServerURI", null);
|
||||
},
|
||||
|
||||
set serverURI(value) {
|
||||
if (!value) {
|
||||
throw new Error("serverURI must have a value.");
|
||||
}
|
||||
|
||||
if (typeof(value) != "string") {
|
||||
throw new Error("serverURI must be a string: " + value);
|
||||
}
|
||||
|
||||
this._prefs.set("documentServerURI", value);
|
||||
},
|
||||
|
||||
/**
|
||||
* The namespace on the document server to which we will be submitting data.
|
||||
*/
|
||||
get serverNamespace() {
|
||||
return this._prefs.get("documentServerNamespace", "metrics");
|
||||
},
|
||||
|
||||
set serverNamespace(value) {
|
||||
if (!value) {
|
||||
throw new Error("serverNamespace must have a value.");
|
||||
}
|
||||
|
||||
if (typeof(value) != "string") {
|
||||
throw new Error("serverNamespace must be a string: " + value);
|
||||
}
|
||||
|
||||
this._prefs.set("documentServerNamespace", value);
|
||||
},
|
||||
|
||||
/**
|
||||
* The document ID for data to be submitted to the server.
|
||||
*
|
||||
* This should be a UUID.
|
||||
*
|
||||
* We generate a new UUID when we upload data to the server. When we get a
|
||||
* successful response for that upload, we record that UUID in this value.
|
||||
* On the subsequent upload, this ID will be deleted from the server.
|
||||
*/
|
||||
get lastSubmitID() {
|
||||
return this._prefs.get("lastSubmitID", null) || null;
|
||||
},
|
||||
|
||||
set lastSubmitID(value) {
|
||||
this._prefs.set("lastSubmitID", value || "");
|
||||
},
|
||||
|
||||
/**
|
||||
* Whether remote data is currently stored.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
haveRemoteData: function haveRemoteData() {
|
||||
return !!this.lastSubmitID;
|
||||
},
|
||||
|
||||
/**
|
||||
* Start background functionality.
|
||||
*
|
||||
* If this isn't called, no data upload will occur.
|
||||
*/
|
||||
start: function start() {
|
||||
this._policy.startPolling();
|
||||
this._log.info("HealthReporter started.");
|
||||
},
|
||||
|
||||
/**
|
||||
* Stop background functionality.
|
||||
*/
|
||||
stop: function stop() {
|
||||
this._policy.stopPolling();
|
||||
},
|
||||
|
||||
/**
|
||||
* Register a `MetricsProvider` with this instance.
|
||||
*
|
||||
* This needs to be called or no data will be collected. See also
|
||||
* registerProvidersFromCategoryManager`.
|
||||
*
|
||||
* @param provider
|
||||
* (MetricsProvider) The provider to register for collection.
|
||||
*/
|
||||
registerProvider: function registerProvider(provider) {
|
||||
return this._collector.registerProvider(provider);
|
||||
},
|
||||
|
||||
/**
|
||||
* Registers providers from a category manager category.
|
||||
*
|
||||
* This examines the specified category entries and registers found
|
||||
* providers.
|
||||
*
|
||||
* Category entries are essentially JS modules and the name of the symbol
|
||||
* within that module that is a `MetricsProvider` instance.
|
||||
*
|
||||
* The category entry name is the name of the JS type for the provider. The
|
||||
* value is the resource:// URI to import which makes this type available.
|
||||
*
|
||||
* Example entry:
|
||||
*
|
||||
* FooProvider resource://gre/modules/foo.jsm
|
||||
*
|
||||
* One can register entries in the application's .manifest file. e.g.
|
||||
*
|
||||
* category healthreport-js-provider FooProvider resource://gre/modules/foo.jsm
|
||||
*
|
||||
* Then to load them:
|
||||
*
|
||||
* let reporter = new HealthReporter("healthreport.");
|
||||
* reporter.registerProvidersFromCategoryManager("healthreport-js-provider");
|
||||
*
|
||||
* @param category
|
||||
* (string) Name of category to query and load from.
|
||||
*/
|
||||
registerProvidersFromCategoryManager:
|
||||
function registerProvidersFromCategoryManager(category) {
|
||||
|
||||
let cm = Cc["@mozilla.org/categorymanager;1"]
|
||||
.getService(Ci.nsICategoryManager);
|
||||
|
||||
let enumerator = cm.enumerateCategory(category);
|
||||
while (enumerator.hasMoreElements()) {
|
||||
let entry = enumerator.getNext()
|
||||
.QueryInterface(Ci.nsISupportsCString)
|
||||
.toString();
|
||||
|
||||
let uri = cm.getCategoryEntry(category, entry);
|
||||
this._log.info("Attempting to load provider from category manager: " +
|
||||
entry + " from " + uri);
|
||||
|
||||
try {
|
||||
let ns = {};
|
||||
Cu.import(uri, ns);
|
||||
|
||||
let provider = new ns[entry]();
|
||||
this.registerProvider(provider);
|
||||
} catch (ex) {
|
||||
this._log.warn("Error registering provider from category manager: " +
|
||||
entry + "; " + CommonUtils.exceptionStr(ex));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Collect all measurements for all registered providers.
|
||||
*/
|
||||
collectMeasurements: function collectMeasurements() {
|
||||
return this._collector.collectConstantMeasurements();
|
||||
},
|
||||
|
||||
/**
|
||||
* Record the user's rejection of the data submission policy.
|
||||
*
|
||||
* This should be what everything uses to disable data submission.
|
||||
*
|
||||
* @param reason
|
||||
* (string) Why data submission is being disabled.
|
||||
*/
|
||||
recordPolicyRejection: function recordPolicyRejection(reason) {
|
||||
this._policy.recordUserRejection(reason);
|
||||
},
|
||||
|
||||
/**
|
||||
* Record the user's acceptance of the data submission policy.
|
||||
*
|
||||
* This should be what everything uses to enable data submission.
|
||||
*
|
||||
* @param reason
|
||||
* (string) Why data submission is being enabled.
|
||||
*/
|
||||
recordPolicyAcceptance: function recordPolicyAcceptance(reason) {
|
||||
this._policy.recordUserAcceptance(reason);
|
||||
},
|
||||
|
||||
/**
|
||||
* Whether the data submission policy has been accepted.
|
||||
*
|
||||
* If this is true, health data will be submitted unless one of the kill
|
||||
* switches is active.
|
||||
*/
|
||||
get dataSubmissionPolicyAccepted() {
|
||||
return this._policy.dataSubmissionPolicyAccepted;
|
||||
},
|
||||
|
||||
/**
|
||||
* Whether this health reporter will upload data to a server.
|
||||
*/
|
||||
get willUploadData() {
|
||||
return this._policy.dataSubmissionPolicyAccepted &&
|
||||
this._policy.dataUploadEnabled;
|
||||
},
|
||||
|
||||
/**
|
||||
* Request that server data be deleted.
|
||||
*
|
||||
* If deletion is scheduled to occur immediately, a promise will be returned
|
||||
* that will be fulfilled when the deletion attempt finishes. Otherwise,
|
||||
* callers should poll haveRemoteData() to determine when remote data is
|
||||
* deleted.
|
||||
*/
|
||||
requestDeleteRemoteData: function requestDeleteRemoteData(reason) {
|
||||
if (!this.lastSubmitID) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this._policy.deleteRemoteData(reason);
|
||||
},
|
||||
|
||||
getJSONPayload: function getJSONPayload() {
|
||||
let o = {
|
||||
version: 1,
|
||||
thisPingDate: this._formatDate(this._now()),
|
||||
providers: {},
|
||||
};
|
||||
|
||||
let lastPingDate = this.lastPingDate;
|
||||
if (lastPingDate.getTime() > 0) {
|
||||
o.lastPingDate = this._formatDate(lastPingDate);
|
||||
}
|
||||
|
||||
for (let [name, provider] of this._collector.collectionResults) {
|
||||
o.providers[name] = provider;
|
||||
}
|
||||
|
||||
return JSON.stringify(o);
|
||||
},
|
||||
|
||||
_onBagheeraResult: function _onBagheeraResult(request, isDelete, result) {
|
||||
this._log.debug("Received Bagheera result.");
|
||||
|
||||
let promise = Promise.resolve(null);
|
||||
|
||||
if (!result.transportSuccess) {
|
||||
request.onSubmissionFailureSoft("Network transport error.");
|
||||
return promise;
|
||||
}
|
||||
|
||||
if (!result.serverSuccess) {
|
||||
request.onSubmissionFailureHard("Server failure.");
|
||||
return promise;
|
||||
}
|
||||
|
||||
let now = this._now();
|
||||
|
||||
if (isDelete) {
|
||||
this.lastSubmitID = null;
|
||||
} else {
|
||||
this.lastSubmitID = result.id;
|
||||
this.lastPingDate = now;
|
||||
}
|
||||
|
||||
request.onSubmissionSuccess(now);
|
||||
|
||||
return promise;
|
||||
},
|
||||
|
||||
_onSubmitDataRequestFailure: function _onSubmitDataRequestFailure(error) {
|
||||
this._log.error("Error processing request to submit data: " +
|
||||
CommonUtils.exceptionStr(error));
|
||||
},
|
||||
|
||||
_formatDate: function _formatDate(date) {
|
||||
// Why, oh, why doesn't JS have a strftime() equivalent?
|
||||
return date.toISOString().substr(0, 10);
|
||||
},
|
||||
|
||||
|
||||
_uploadData: function _uploadData(request) {
|
||||
let id = CommonUtils.generateUUID();
|
||||
|
||||
this._log.info("Uploading data to server: " + this.serverURI + " " +
|
||||
this.serverNamespace + ":" + id);
|
||||
let client = new BagheeraClient(this.serverURI);
|
||||
|
||||
let payload = this.getJSONPayload();
|
||||
|
||||
let promise = client.uploadJSON(this.serverNamespace,
|
||||
id,
|
||||
payload,
|
||||
this.lastSubmitID);
|
||||
|
||||
return promise.then(this._onBagheeraResult.bind(this, request, false));
|
||||
},
|
||||
|
||||
_deleteRemoteData: function _deleteRemoteData(request) {
|
||||
if (!this.lastSubmitID) {
|
||||
this._log.info("Received request to delete remote data but no data stored.");
|
||||
request.onNoDataAvailable();
|
||||
return;
|
||||
}
|
||||
|
||||
this._log.warn("Deleting remote data.");
|
||||
let client = new BagheeraClient(this.serverURI);
|
||||
|
||||
return client.deleteDocument(this.serverNamespace, this.lastSubmitID)
|
||||
.then(this._onBagheeraResult.bind(this, request, true),
|
||||
this._onSubmitDataRequestFailure.bind(this));
|
||||
|
||||
},
|
||||
|
||||
_now: function _now() {
|
||||
return new Date();
|
||||
},
|
||||
|
||||
//-----------------------------
|
||||
// HealthReportPolicy listeners
|
||||
//-----------------------------
|
||||
|
||||
onRequestDataUpload: function onRequestDataSubmission(request) {
|
||||
this.collectMeasurements()
|
||||
.then(this._uploadData.bind(this, request),
|
||||
this._onSubmitDataRequestFailure.bind(this));
|
||||
},
|
||||
|
||||
onNotifyDataPolicy: function onNotifyDataPolicy(request) {
|
||||
// This isn't very loosely coupled. We may want to have this call
|
||||
// registered listeners instead.
|
||||
Observers.notify("healthreport:notify-data-policy:request", request);
|
||||
},
|
||||
|
||||
onRequestRemoteDelete: function onRequestRemoteDelete(request) {
|
||||
this._deleteRemoteData(request);
|
||||
},
|
||||
|
||||
//------------------------------------
|
||||
// End of HealthReportPolicy listeners
|
||||
//------------------------------------
|
||||
};
|
||||
|
||||
Object.freeze(HealthReporter.prototype);
|
||||
|
|
@ -5,6 +5,7 @@
|
|||
"use strict";
|
||||
|
||||
this.EXPORTED_SYMBOLS = [
|
||||
"DataSubmissionRequest", // For test use only.
|
||||
"HealthReportPolicy",
|
||||
];
|
||||
|
||||
|
@ -242,7 +243,7 @@ Object.freeze(DataSubmissionRequest.prototype);
|
|||
* events.
|
||||
*/
|
||||
this.HealthReportPolicy = function HealthReportPolicy(prefs, listener) {
|
||||
this._log = Log4Moz.repository.getLogger("HealthReport.Policy");
|
||||
this._log = Log4Moz.repository.getLogger("Services.HealthReport.Policy");
|
||||
this._log.level = Log4Moz.Level["Debug"];
|
||||
|
||||
for (let handler of this.REQUIRED_LISTENERS) {
|
||||
|
@ -644,7 +645,7 @@ HealthReportPolicy.prototype = {
|
|||
// We want delete deletion to occur as soon as possible. Move up any
|
||||
// pending scheduled data submission and try to trigger.
|
||||
this.nextDataSubmissionDate = this.now();
|
||||
this.checkStateAndTrigger();
|
||||
return this.checkStateAndTrigger();
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -739,8 +740,7 @@ HealthReportPolicy.prototype = {
|
|||
return;
|
||||
}
|
||||
|
||||
this._dispatchSubmissionRequest("onRequestRemoteDelete", true);
|
||||
return;
|
||||
return this._dispatchSubmissionRequest("onRequestRemoteDelete", true);
|
||||
}
|
||||
|
||||
if (!this.dataUploadEnabled) {
|
||||
|
@ -768,7 +768,7 @@ HealthReportPolicy.prototype = {
|
|||
return;
|
||||
}
|
||||
|
||||
this._dispatchSubmissionRequest("onRequestDataUpload", false);
|
||||
return this._dispatchSubmissionRequest("onRequestDataUpload", false);
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -885,7 +885,7 @@ HealthReportPolicy.prototype = {
|
|||
this._handleSubmissionFailure();
|
||||
}.bind(this);
|
||||
|
||||
deferred.promise.then(onSuccess, onError);
|
||||
let chained = deferred.promise.then(onSuccess, onError);
|
||||
|
||||
this._log.info("Requesting data submission. Will expire at " +
|
||||
requestExpiresDate);
|
||||
|
@ -898,6 +898,8 @@ HealthReportPolicy.prototype = {
|
|||
this._handleSubmissionFailure();
|
||||
return;
|
||||
}
|
||||
|
||||
return chained;
|
||||
},
|
||||
|
||||
_handleSubmissionResult: function _handleSubmissionResult(request) {
|
||||
|
|
|
@ -0,0 +1,262 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
|
||||
|
||||
Cu.import("resource://services-common/observers.js");
|
||||
Cu.import("resource://services-common/preferences.js");
|
||||
Cu.import("resource://gre/modules/commonjs/promise/core.js");
|
||||
Cu.import("resource://gre/modules/services/healthreport/healthreporter.jsm");
|
||||
Cu.import("resource://gre/modules/services/healthreport/policy.jsm");
|
||||
Cu.import("resource://testing-common/services-common/bagheeraserver.js");
|
||||
Cu.import("resource://testing-common/services/metrics/mocks.jsm");
|
||||
|
||||
|
||||
const SERVER_HOSTNAME = "localhost";
|
||||
const SERVER_PORT = 8080;
|
||||
const SERVER_URI = "http://" + SERVER_HOSTNAME + ":" + SERVER_PORT;
|
||||
|
||||
|
||||
function defineNow(policy, now) {
|
||||
print("Adjusting fake system clock to " + now);
|
||||
Object.defineProperty(policy, "now", {
|
||||
value: function customNow() {
|
||||
return now;
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
}
|
||||
|
||||
function getReporter(name, uri=SERVER_URI) {
|
||||
let branch = "healthreport.testing. " + name + ".";
|
||||
|
||||
let prefs = new Preferences(branch);
|
||||
prefs.set("documentServerURI", uri);
|
||||
|
||||
return new HealthReporter(branch);
|
||||
}
|
||||
|
||||
function getReporterAndServer(name, namespace="test") {
|
||||
let reporter = getReporter(name, SERVER_URI);
|
||||
reporter.serverNamespace = namespace;
|
||||
|
||||
let server = new BagheeraServer(SERVER_URI);
|
||||
server.createNamespace(namespace);
|
||||
|
||||
server.start(SERVER_PORT);
|
||||
|
||||
return [reporter, server];
|
||||
}
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_test(function test_constructor() {
|
||||
let reporter = getReporter("constructor");
|
||||
|
||||
do_check_eq(reporter.lastPingDate.getTime(), 0);
|
||||
do_check_null(reporter.lastSubmitID);
|
||||
|
||||
reporter.lastSubmitID = "foo";
|
||||
do_check_eq(reporter.lastSubmitID, "foo");
|
||||
reporter.lastSubmitID = null;
|
||||
do_check_null(reporter.lastSubmitID);
|
||||
|
||||
let failed = false;
|
||||
try {
|
||||
new HealthReporter("foo.bar");
|
||||
} catch (ex) {
|
||||
failed = true;
|
||||
do_check_true(ex.message.startsWith("Branch argument must end"));
|
||||
} finally {
|
||||
do_check_true(failed);
|
||||
failed = false;
|
||||
}
|
||||
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_test(function test_register_providers_from_category_manager() {
|
||||
const category = "healthreporter-js-modules";
|
||||
|
||||
let cm = Cc["@mozilla.org/categorymanager;1"]
|
||||
.getService(Ci.nsICategoryManager);
|
||||
cm.addCategoryEntry(category, "DummyProvider",
|
||||
"resource://testing-common/services/metrics/mocks.jsm",
|
||||
false, true);
|
||||
|
||||
let reporter = getReporter("category_manager");
|
||||
do_check_eq(reporter._collector._providers.length, 0);
|
||||
reporter.registerProvidersFromCategoryManager(category);
|
||||
do_check_eq(reporter._collector._providers.length, 1);
|
||||
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_test(function test_json_payload_simple() {
|
||||
let reporter = getReporter("json_payload_simple");
|
||||
|
||||
let now = new Date();
|
||||
let payload = reporter.getJSONPayload();
|
||||
let original = JSON.parse(payload);
|
||||
|
||||
do_check_eq(original.version, 1);
|
||||
do_check_eq(original.thisPingDate, reporter._formatDate(now));
|
||||
do_check_eq(Object.keys(original.providers).length, 0);
|
||||
|
||||
reporter.lastPingDate = new Date(now.getTime() - 24 * 60 * 60 * 1000 - 10);
|
||||
|
||||
original = JSON.parse(reporter.getJSONPayload());
|
||||
do_check_eq(original.lastPingDate, reporter._formatDate(reporter.lastPingDate));
|
||||
|
||||
// This could fail if we cross UTC day boundaries at the exact instance the
|
||||
// test is executed. Let's tempt fate.
|
||||
do_check_eq(original.thisPingDate, reporter._formatDate(now));
|
||||
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_test(function test_json_payload_dummy_provider() {
|
||||
let reporter = getReporter("json_payload_dummy_provider");
|
||||
|
||||
reporter.registerProvider(new DummyProvider());
|
||||
reporter.collectMeasurements().then(function onResult() {
|
||||
let o = JSON.parse(reporter.getJSONPayload());
|
||||
|
||||
do_check_eq(Object.keys(o.providers).length, 1);
|
||||
do_check_true("DummyProvider" in o.providers);
|
||||
do_check_true("measurements" in o.providers.DummyProvider);
|
||||
do_check_true("DummyMeasurement" in o.providers.DummyProvider.measurements);
|
||||
|
||||
run_next_test();
|
||||
});
|
||||
});
|
||||
|
||||
add_test(function test_notify_policy_observers() {
|
||||
let reporter = getReporter("notify_policy_observers");
|
||||
|
||||
Observers.add("healthreport:notify-data-policy:request",
|
||||
function onObserver(subject, data) {
|
||||
Observers.remove("healthreport:notify-data-policy:request", onObserver);
|
||||
|
||||
do_check_true("foo" in subject);
|
||||
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
reporter.onNotifyDataPolicy({foo: "bar"});
|
||||
});
|
||||
|
||||
add_test(function test_data_submission_transport_failure() {
|
||||
let reporter = getReporter("data_submission_transport_failure");
|
||||
reporter.serverURI = "http://localhost:8080/";
|
||||
reporter.serverNamespace = "test00";
|
||||
|
||||
let deferred = Promise.defer();
|
||||
deferred.promise.then(function onResult(request) {
|
||||
do_check_eq(request.state, request.SUBMISSION_FAILURE_SOFT);
|
||||
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
let request = new DataSubmissionRequest(deferred, new Date(Date.now + 30000));
|
||||
reporter.onRequestDataUpload(request);
|
||||
});
|
||||
|
||||
add_test(function test_data_submission_success() {
|
||||
let [reporter, server] = getReporterAndServer("data_submission_success");
|
||||
|
||||
do_check_eq(reporter.lastPingDate.getTime(), 0);
|
||||
do_check_false(reporter.haveRemoteData());
|
||||
|
||||
let deferred = Promise.defer();
|
||||
deferred.promise.then(function onResult(request) {
|
||||
do_check_eq(request.state, request.SUBMISSION_SUCCESS);
|
||||
do_check_neq(reporter.lastPingDate.getTime(), 0);
|
||||
do_check_true(reporter.haveRemoteData());
|
||||
|
||||
server.stop(run_next_test);
|
||||
});
|
||||
|
||||
let request = new DataSubmissionRequest(deferred, new Date());
|
||||
reporter.onRequestDataUpload(request);
|
||||
});
|
||||
|
||||
add_test(function test_recurring_daily_pings() {
|
||||
let [reporter, server] = getReporterAndServer("recurring_daily_pings");
|
||||
reporter.registerProvider(new DummyProvider());
|
||||
|
||||
let policy = reporter._policy;
|
||||
|
||||
defineNow(policy, policy._futureDate(-24 * 60 * 68 * 1000));
|
||||
policy.recordUserAcceptance();
|
||||
defineNow(policy, policy.nextDataSubmissionDate);
|
||||
let promise = policy.checkStateAndTrigger();
|
||||
do_check_neq(promise, null);
|
||||
|
||||
promise.then(function onUploadComplete() {
|
||||
let lastID = reporter.lastSubmitID;
|
||||
|
||||
do_check_neq(lastID, null);
|
||||
do_check_true(server.hasDocument(reporter.serverNamespace, lastID));
|
||||
|
||||
// Skip forward to next scheduled submission time.
|
||||
defineNow(policy, policy.nextDataSubmissionDate);
|
||||
let promise = policy.checkStateAndTrigger();
|
||||
do_check_neq(promise, null);
|
||||
promise.then(function onSecondUploadCOmplete() {
|
||||
do_check_neq(reporter.lastSubmitID, lastID);
|
||||
do_check_true(server.hasDocument(reporter.serverNamespace, reporter.lastSubmitID));
|
||||
do_check_false(server.hasDocument(reporter.serverNamespace, lastID));
|
||||
|
||||
server.stop(run_next_test);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
add_test(function test_request_remote_data_deletion() {
|
||||
let [reporter, server] = getReporterAndServer("request_remote_data_deletion");
|
||||
|
||||
let policy = reporter._policy;
|
||||
defineNow(policy, policy._futureDate(-24 * 60 * 60 * 1000));
|
||||
policy.recordUserAcceptance();
|
||||
defineNow(policy, policy.nextDataSubmissionDate);
|
||||
policy.checkStateAndTrigger().then(function onUploadComplete() {
|
||||
let id = reporter.lastSubmitID;
|
||||
do_check_neq(id, null);
|
||||
do_check_true(server.hasDocument(reporter.serverNamespace, id));
|
||||
|
||||
defineNow(policy, policy._futureDate(10 * 1000));
|
||||
|
||||
let promise = reporter.requestDeleteRemoteData();
|
||||
do_check_neq(promise, null);
|
||||
promise.then(function onDeleteComplete() {
|
||||
do_check_null(reporter.lastSubmitID);
|
||||
do_check_false(reporter.haveRemoteData());
|
||||
do_check_false(server.hasDocument(reporter.serverNamespace, id));
|
||||
|
||||
server.stop(run_next_test);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
add_test(function test_policy_accept_reject() {
|
||||
let [reporter, server] = getReporterAndServer("policy_accept_reject");
|
||||
|
||||
do_check_false(reporter.dataSubmissionPolicyAccepted);
|
||||
do_check_false(reporter.willUploadData);
|
||||
|
||||
reporter.recordPolicyAcceptance();
|
||||
do_check_true(reporter.dataSubmissionPolicyAccepted);
|
||||
do_check_true(reporter.willUploadData);
|
||||
|
||||
reporter.recordPolicyRejection();
|
||||
do_check_false(reporter.dataSubmissionPolicyAccepted);
|
||||
do_check_false(reporter.willUploadData);
|
||||
|
||||
server.stop(run_next_test);
|
||||
});
|
||||
|
|
@ -4,6 +4,7 @@
|
|||
"use strict";
|
||||
|
||||
const modules = [
|
||||
"healthreporter.jsm",
|
||||
"policy.jsm",
|
||||
];
|
||||
|
||||
|
|
|
@ -4,3 +4,4 @@ tail =
|
|||
|
||||
[test_load_modules.js]
|
||||
[test_policy.js]
|
||||
[test_healthreporter.js]
|
||||
|
|
Загрузка…
Ссылка в новой задаче