Bug 808219 - Firefox Health Reporter service; r=rnewman

This commit is contained in:
Gregory Szorc 2012-11-13 20:22:09 -08:00
Родитель c5073721ca
Коммит 9fe6a8cad5
14 изменённых файлов: 945 добавлений и 6 удалений

Просмотреть файл

@ -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]