Back out 8 changesets (bug 1235345, bug 1234526, bug 1234522, bug 1237700) for OS X 10.10 debug xpcshell timeouts in test_ocsp_stapling.js and test_ocsp_stapling_expired.js

CLOSED TREE

Backed out changeset f26c050a39a1 (bug 1235345)
Backed out changeset c7689b72d3fa (bug 1234526)
Backed out changeset 3124025d1147 (bug 1234526)
Backed out changeset 096d46bdaf86 (bug 1234526)
Backed out changeset 96e0326e7985 (bug 1234522)
Backed out changeset c3b6bf176f86 (bug 1234522)
Backed out changeset 3e7dc6d87325 (bug 1234522)
Backed out changeset f6447d37d113 (bug 1237700)
This commit is contained in:
Phil Ringnalda 2016-01-08 20:25:27 -08:00
parent 4ea469e1e1
commit 43d54ab19e
99 changed files with 15236 additions and 328 deletions

View File

@ -19,6 +19,7 @@ MOZ_OFFICIAL_BRANDING_DIRECTORY=b2g/branding/official
MOZ_SAFE_BROWSING=1
MOZ_SERVICES_COMMON=1
MOZ_SERVICES_METRICS=1
MOZ_WEBSMS_BACKEND=1
MOZ_NO_SMART_CARDS=1

View File

@ -26,6 +26,7 @@ MOZ_OFFICIAL_BRANDING_DIRECTORY=b2g/branding/official
MOZ_SAFE_BROWSING=1
MOZ_SERVICES_COMMON=1
MOZ_SERVICES_METRICS=1
MOZ_CAPTIVEDETECT=1
MOZ_WEBSMS_BACKEND=1

View File

@ -631,6 +631,10 @@
#endif
@RESPATH@/components/servicesComponents.manifest
@RESPATH@/components/cryptoComponents.manifest
#ifdef MOZ_SERVICES_HEALTHREPORT
@RESPATH@/components/HealthReportComponents.manifest
@RESPATH@/components/HealthReportService.js
#endif
@RESPATH@/components/CaptivePortalDetectComponents.manifest
@RESPATH@/components/captivedetect.js
@RESPATH@/components/TelemetryStartup.js

View File

@ -23,6 +23,10 @@ var healthReportWrapper = {
let iframe = document.getElementById("remote-report");
iframe.addEventListener("load", healthReportWrapper.initRemotePage, false);
iframe.src = this._getReportURI().spec;
iframe.onload = () => {
MozSelfSupport.getHealthReportPayload().then(this.updatePayload,
this.handleInitFailure);
};
prefs.observe("uploadEnabled", this.updatePrefState, healthReportWrapper);
},
@ -99,6 +103,15 @@ var healthReportWrapper = {
});
},
refreshPayload: function () {
MozSelfSupport.getHealthReportPayload().then(this.updatePayload,
this.handlePayloadFailure);
},
updatePayload: function (payload) {
healthReportWrapper.injectData("payload", JSON.stringify(payload));
},
injectData: function (type, content) {
let report = this._getReportURI();
@ -126,6 +139,9 @@ var healthReportWrapper = {
case "RequestCurrentPrefs":
this.updatePrefState();
break;
case "RequestCurrentPayload":
this.refreshPayload();
break;
case "RequestTelemetryPingList":
this.sendTelemetryPingList();
break;

View File

@ -2,9 +2,6 @@
* 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/. */
const LOGGER_NAME = "Toolkit.Telemetry";
const LOGGER_PREFIX = "DataNotificationInfoBar::";
/**
* Represents an info bar that shows a data submission notification.
*/
@ -24,7 +21,7 @@ var gDataNotificationInfoBar = {
get _log() {
let Log = Cu.import("resource://gre/modules/Log.jsm", {}).Log;
delete this._log;
return this._log = Log.repository.getLoggerWithMessagePrefix(LOGGER_NAME, LOGGER_PREFIX);
return this._log = Log.repository.getLogger("Services.DataReporting.InfoBar");
},
init: function() {

View File

@ -3633,7 +3633,7 @@ const BrowserSearch = {
loadSearchFromContext: function (terms) {
let engine = BrowserSearch._loadSearch(terms, true, "contextmenu");
if (engine) {
BrowserSearch.recordSearchInTelemetry(engine, "contextmenu");
BrowserSearch.recordSearchInHealthReport(engine, "contextmenu");
}
},
@ -3657,26 +3657,10 @@ const BrowserSearch = {
openUILinkIn(searchEnginesURL, where);
},
_getSearchEngineId: function (engine) {
if (!engine) {
return "other";
}
if (engine.identifier) {
return engine.identifier;
}
if (!("name" in engine) || engine.name === undefined) {
return "other";
}
return "other-" + engine.name;
},
/**
* Helper to record a search with Telemetry.
* Helper to record a search with Firefox Health Report.
*
* Telemetry records only search counts and nothing pertaining to the search itself.
* FHR records only search counts and nothing pertaining to the search itself.
*
* @param engine
* (nsISearchEngine) The engine handling the search.
@ -3688,7 +3672,45 @@ const BrowserSearch = {
* the search was a suggested search, this indicates where the
* item was in the suggestion list and how the user selected it.
*/
recordSearchInTelemetry: function (engine, source, selection) {
recordSearchInHealthReport: function (engine, source, selection) {
BrowserUITelemetry.countSearchEvent(source, null, selection);
this.recordSearchInTelemetry(engine, source);
let reporter = AppConstants.MOZ_SERVICES_HEALTHREPORT
? Cc["@mozilla.org/datareporting/service;1"]
.getService()
.wrappedJSObject
.healthReporter
: null;
// This can happen if the FHR component of the data reporting service is
// disabled. This is controlled by a pref that most will never use.
if (!reporter) {
return;
}
reporter.onInit().then(function record() {
try {
reporter.getProvider("org.mozilla.searches").recordSearch(engine, source);
} catch (ex) {
Cu.reportError(ex);
}
});
},
_getSearchEngineId: function (engine) {
if (!engine) {
return "other";
}
if (engine.identifier) {
return engine.identifier;
}
return "other-" + engine.name;
},
recordSearchInTelemetry: function (engine, source) {
const SOURCES = [
"abouthome",
"contextmenu",
@ -3697,8 +3719,6 @@ const BrowserSearch = {
"urlbar",
];
BrowserUITelemetry.countSearchEvent(source, null, selection);
if (SOURCES.indexOf(source) == -1) {
Cu.reportError("Unknown source for search: " + source);
return;

View File

@ -286,6 +286,8 @@ skip-if = e10s # Bug 1094510 - test hits the network in e10s mode only
[browser_contextSearchTabPosition.js]
skip-if = os == "mac" || e10s # bug 967013; e10s: bug 1094761 - test hits the network in e10s, causing next test to crash
[browser_ctrlTab.js]
[browser_datareporting_notification.js]
skip-if = !datareporting
[browser_datachoices_notification.js]
skip-if = !datareporting
[browser_devedition.js]
@ -478,6 +480,7 @@ skip-if = os == "linux" # Bug 1073339 - Investigate autocomplete test unreliabil
[browser_urlbarStop.js]
[browser_urlbarTrimURLs.js]
[browser_urlbar_autoFill_backspaced.js]
[browser_urlbar_search_healthreport.js]
[browser_urlbar_searchsettings.js]
[browser_utilityOverlay.js]
[browser_viewSourceInTabOnViewSource.js]

View File

@ -1,143 +1,154 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/
*/
XPCOMUtils.defineLazyModuleGetter(this, "Promise",
"resource://gre/modules/Promise.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm");
const CHROME_BASE = "chrome://mochitests/content/browser/browser/base/content/test/general/";
const HTTPS_BASE = "https://example.com/browser/browser/base/content/test/general/";
const TELEMETRY_LOG_PREF = "toolkit.telemetry.log.level";
const telemetryOriginalLogPref = Preferences.get(TELEMETRY_LOG_PREF, null);
const originalReportUrl = Services.prefs.getCharPref("datareporting.healthreport.about.reportUrl");
const originalReportUrlUnified = Services.prefs.getCharPref("datareporting.healthreport.about.reportUrlUnified");
registerCleanupFunction(function() {
// Ensure we don't pollute prefs for next tests.
if (telemetryOriginalLogPref) {
Preferences.set(TELEMETRY_LOG_PREF, telemetryOriginalLogPref);
} else {
Preferences.reset(TELEMETRY_LOG_PREF);
}
try {
Services.prefs.setCharPref("datareporting.healthreport.about.reportUrl", originalReportUrl);
Services.prefs.setCharPref("datareporting.healthreport.about.reportUrlUnified", originalReportUrlUnified);
Services.prefs.setBoolPref("datareporting.healthreport.uploadEnabled", true);
} catch (ex) {}
});
function fakeTelemetryNow(...args) {
let date = new Date(...args);
let scope = {};
const modules = [
Cu.import("resource://gre/modules/TelemetrySession.jsm", scope),
Cu.import("resource://gre/modules/TelemetryEnvironment.jsm", scope),
Cu.import("resource://gre/modules/TelemetryController.jsm", scope),
];
for (let m of modules) {
m.Policy.now = () => new Date(date);
}
return date;
}
function setupPingArchive() {
let scope = {};
Cu.import("resource://gre/modules/TelemetryController.jsm", scope);
Cc["@mozilla.org/moz/jssubscript-loader;1"].getService(Ci.mozIJSSubScriptLoader)
.loadSubScript(CHROME_BASE + "healthreport_pingData.js", scope);
for (let p of scope.TEST_PINGS) {
fakeTelemetryNow(p.date);
p.id = yield scope.TelemetryController.submitExternalPing(p.type, p.payload);
}
}
var gTests = [
{
desc: "Test the remote commands",
setup: Task.async(function*()
{
Preferences.set(TELEMETRY_LOG_PREF, "Trace");
yield setupPingArchive();
Preferences.set("datareporting.healthreport.about.reportUrl",
HTTPS_BASE + "healthreport_testRemoteCommands.html");
Preferences.set("datareporting.healthreport.about.reportUrlUnified",
HTTPS_BASE + "healthreport_testRemoteCommands.html");
}),
run: function (iframe)
{
let deferred = Promise.defer();
let results = 0;
try {
iframe.contentWindow.addEventListener("FirefoxHealthReportTestResponse", function evtHandler(event) {
let data = event.detail.data;
if (data.type == "testResult") {
ok(data.pass, data.info);
results++;
}
else if (data.type == "testsComplete") {
is(results, data.count, "Checking number of results received matches the number of tests that should have run");
iframe.contentWindow.removeEventListener("FirefoxHealthReportTestResponse", evtHandler, true);
deferred.resolve();
}
}, true);
} catch(e) {
ok(false, "Failed to get all commands");
deferred.reject();
}
return deferred.promise;
}
},
]; // gTests
function test()
{
waitForExplicitFinish();
// xxxmpc leaving this here until we resolve bug 854038 and bug 854060
requestLongerTimeout(10);
Task.spawn(function () {
for (let test of gTests) {
info(test.desc);
yield test.setup();
let iframe = yield promiseNewTabLoadEvent("about:healthreport");
yield test.run(iframe);
gBrowser.removeCurrentTab();
}
finish();
});
}
function promiseNewTabLoadEvent(aUrl, aEventType="load")
{
let deferred = Promise.defer();
let tab = gBrowser.selectedTab = gBrowser.addTab(aUrl);
tab.linkedBrowser.addEventListener(aEventType, function load(event) {
tab.linkedBrowser.removeEventListener(aEventType, load, true);
let iframe = tab.linkedBrowser.contentDocument.getElementById("remote-report");
iframe.addEventListener("load", function frameLoad(e) {
if (iframe.contentWindow.location.href == "about:blank" ||
e.target != iframe) {
return;
}
iframe.removeEventListener("load", frameLoad, false);
deferred.resolve(iframe);
}, false);
}, true);
return deferred.promise;
}
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/
*/
XPCOMUtils.defineLazyModuleGetter(this, "Promise",
"resource://gre/modules/Promise.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm");
const CHROME_BASE = "chrome://mochitests/content/browser/browser/base/content/test/general/";
const HTTPS_BASE = "https://example.com/browser/browser/base/content/test/general/";
const TELEMETRY_LOG_PREF = "toolkit.telemetry.log.level";
const telemetryOriginalLogPref = Preferences.get(TELEMETRY_LOG_PREF, null);
const originalReportUrl = Services.prefs.getCharPref("datareporting.healthreport.about.reportUrl");
const originalReportUrlUnified = Services.prefs.getCharPref("datareporting.healthreport.about.reportUrlUnified");
registerCleanupFunction(function() {
// Ensure we don't pollute prefs for next tests.
if (telemetryOriginalLogPref) {
Preferences.set(TELEMETRY_LOG_PREF, telemetryOriginalLogPref);
} else {
Preferences.reset(TELEMETRY_LOG_PREF);
}
try {
Services.prefs.setCharPref("datareporting.healthreport.about.reportUrl", originalReportUrl);
Services.prefs.setCharPref("datareporting.healthreport.about.reportUrlUnified", originalReportUrlUnified);
let policy = Cc["@mozilla.org/datareporting/service;1"]
.getService(Ci.nsISupports)
.wrappedJSObject
.policy;
policy.recordHealthReportUploadEnabled(true,
"Resetting after tests.");
} catch (ex) {}
});
function fakeTelemetryNow(...args) {
let date = new Date(...args);
let scope = {};
const modules = [
Cu.import("resource://gre/modules/TelemetrySession.jsm", scope),
Cu.import("resource://gre/modules/TelemetryEnvironment.jsm", scope),
Cu.import("resource://gre/modules/TelemetryController.jsm", scope),
];
for (let m of modules) {
m.Policy.now = () => new Date(date);
}
return date;
}
function setupPingArchive() {
let scope = {};
Cu.import("resource://gre/modules/TelemetryController.jsm", scope);
Cc["@mozilla.org/moz/jssubscript-loader;1"].getService(Ci.mozIJSSubScriptLoader)
.loadSubScript(CHROME_BASE + "healthreport_pingData.js", scope);
for (let p of scope.TEST_PINGS) {
fakeTelemetryNow(p.date);
p.id = yield scope.TelemetryController.submitExternalPing(p.type, p.payload);
}
}
var gTests = [
{
desc: "Test the remote commands",
setup: Task.async(function*()
{
Preferences.set(TELEMETRY_LOG_PREF, "Trace");
yield setupPingArchive();
Preferences.set("datareporting.healthreport.about.reportUrl",
HTTPS_BASE + "healthreport_testRemoteCommands.html");
Preferences.set("datareporting.healthreport.about.reportUrlUnified",
HTTPS_BASE + "healthreport_testRemoteCommands.html");
}),
run: function (iframe)
{
let deferred = Promise.defer();
let policy = Cc["@mozilla.org/datareporting/service;1"]
.getService(Ci.nsISupports)
.wrappedJSObject
.policy;
let results = 0;
try {
iframe.contentWindow.addEventListener("FirefoxHealthReportTestResponse", function evtHandler(event) {
let data = event.detail.data;
if (data.type == "testResult") {
ok(data.pass, data.info);
results++;
}
else if (data.type == "testsComplete") {
is(results, data.count, "Checking number of results received matches the number of tests that should have run");
iframe.contentWindow.removeEventListener("FirefoxHealthReportTestResponse", evtHandler, true);
deferred.resolve();
}
}, true);
} catch(e) {
ok(false, "Failed to get all commands");
deferred.reject();
}
return deferred.promise;
}
},
]; // gTests
function test()
{
waitForExplicitFinish();
// xxxmpc leaving this here until we resolve bug 854038 and bug 854060
requestLongerTimeout(10);
Task.spawn(function () {
for (let test of gTests) {
info(test.desc);
yield test.setup();
let iframe = yield promiseNewTabLoadEvent("about:healthreport");
yield test.run(iframe);
gBrowser.removeCurrentTab();
}
finish();
});
}
function promiseNewTabLoadEvent(aUrl, aEventType="load")
{
let deferred = Promise.defer();
let tab = gBrowser.selectedTab = gBrowser.addTab(aUrl);
tab.linkedBrowser.addEventListener(aEventType, function load(event) {
tab.linkedBrowser.removeEventListener(aEventType, load, true);
let iframe = tab.linkedBrowser.contentDocument.getElementById("remote-report");
iframe.addEventListener("load", function frameLoad(e) {
if (iframe.contentWindow.location.href == "about:blank" ||
e.target != iframe) {
return;
}
iframe.removeEventListener("load", frameLoad, false);
deferred.resolve(iframe);
}, false);
}, true);
return deferred.promise;
}

View File

@ -78,11 +78,24 @@ var gTests = [
}
},
// Disabled on Linux for intermittent issues with FHR, see Bug 945667.
{
desc: "Check that performing a search fires a search event and records to " +
"Telemetry.",
"Firefox Health Report.",
setup: function () { },
run: function* () {
// Skip this test on Linux.
if (navigator.platform.indexOf("Linux") == 0) {
return Promise.resolve();
}
try {
let cm = Cc["@mozilla.org/categorymanager;1"].getService(Ci.nsICategoryManager);
cm.getCategoryEntry("healthreport-js-provider-default", "SearchesProvider");
} catch (ex) {
// Health Report disabled, or no SearchesProvider.
return Promise.resolve();
}
let engine = yield promiseNewEngine("searchSuggestionEngine.xml");
// Make this actually work in healthreport by giving it an ID:
@ -100,32 +113,23 @@ var gTests = [
is(engine.name, engineName, "Engine name in DOM should match engine we just added");
// Get the current number of recorded searches.
let histogramKey = engine.identifier + ".abouthome";
try {
let hs = Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS").snapshot();
if (histogramKey in hs) {
numSearchesBefore = hs[histogramKey].sum;
}
} catch (ex) {
// No searches performed yet, not a problem, |numSearchesBefore| is 0.
}
// Perform a search to increase the SEARCH_COUNT histogram.
let searchStr = "a search";
info("Perform a search.");
doc.getElementById("searchText").value = searchStr;
doc.getElementById("searchSubmit").click();
getNumberOfSearchesInFHR(engineName, "abouthome").then(num => {
numSearchesBefore = num;
info("Perform a search.");
doc.getElementById("searchText").value = searchStr;
doc.getElementById("searchSubmit").click();
});
let expectedURL = Services.search.currentEngine.
getSubmission(searchStr, null, "homepage").
uri.spec;
let loadPromise = waitForDocLoadAndStopIt(expectedURL).then(() => {
// Make sure the SEARCH_COUNTS histogram has the right key and count.
let hs = Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS").snapshot();
Assert.ok(histogramKey in hs, "histogram with key should be recorded");
Assert.equal(hs[histogramKey].sum, numSearchesBefore + 1,
"histogram sum should be incremented");
searchEventDeferred.resolve();
getNumberOfSearchesInFHR(engineName, "abouthome").then(num => {
is(num, numSearchesBefore + 1, "One more search recorded.");
searchEventDeferred.resolve();
});
});
try {

View File

@ -2,35 +2,16 @@
* 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/. */
add_task(function* test() {
// Will need to be changed if Google isn't the default search engine.
// Note: geoSpecificDefaults are disabled for mochitests, so this is the
// non-US en-US default.
let histogramKey = "google.contextmenu";
let numSearchesBefore = 0;
try {
let hs = Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS").snapshot();
if (histogramKey in hs) {
numSearchesBefore = hs[histogramKey].sum;
}
} catch (ex) {
// No searches performed yet, not a problem, |numSearchesBefore| is 0.
}
let tabs = [];
let tabsLoadedDeferred = Promise.defer();
function test() {
waitForExplicitFinish();
function tabAdded(event) {
let tab = event.target;
tabs.push(tab);
// We wait for the blank tab and the two context searches tabs to open.
if (tabs.length == 3) {
tabsLoadedDeferred.resolve();
}
}
let tabs = [];
let container = gBrowser.tabContainer;
container.addEventListener("TabOpen", tabAdded, false);
@ -38,9 +19,6 @@ add_task(function* test() {
BrowserSearch.loadSearchFromContext("mozilla");
BrowserSearch.loadSearchFromContext("firefox");
// Wait for all the tabs to open.
yield tabsLoadedDeferred.promise;
is(tabs[0], gBrowser.tabs[3], "blank tab has been pushed to the end");
is(tabs[1], gBrowser.tabs[1], "first search tab opens next to the current tab");
is(tabs[2], gBrowser.tabs[2], "second search tab opens next to the first search tab");
@ -48,9 +26,45 @@ add_task(function* test() {
container.removeEventListener("TabOpen", tabAdded, false);
tabs.forEach(gBrowser.removeTab, gBrowser);
// Make sure that the context searches are correctly recorded.
let hs = Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS").snapshot();
Assert.ok(histogramKey in hs, "The histogram must contain the correct key");
Assert.equal(hs[histogramKey].sum, numSearchesBefore + 2,
"The histogram must contain the correct search count");
});
try {
let cm = Components.classes["@mozilla.org/categorymanager;1"]
.getService(Components.interfaces.nsICategoryManager);
cm.getCategoryEntry("healthreport-js-provider-default", "SearchesProvider");
} catch (ex) {
// Health Report disabled, or no SearchesProvider.
finish();
return;
}
let reporter = Components.classes["@mozilla.org/datareporting/service;1"]
.getService()
.wrappedJSObject
.healthReporter;
// reporter should always be available in automation.
ok(reporter, "Health Reporter available.");
reporter.onInit().then(function onInit() {
let provider = reporter.getProvider("org.mozilla.searches");
ok(provider, "Searches provider is available.");
let m = provider.getMeasurement("counts", 3);
m.getValues().then(function onValues(data) {
let now = new Date();
ok(data.days.hasDay(now), "Have data for today.");
let day = data.days.getDay(now);
// Will need to be changed if Google isn't the default search engine.
// Note: geoSpecificDefaults are disabled for mochitests, so this is the
// non-US en-US default.
let defaultProviderID = "google";
let field = defaultProviderID + ".contextmenu";
ok(day.has(field), "Have search recorded for context menu.");
// If any other mochitests perform a context menu search, this will fail.
// The solution will be to look up count at test start and ensure it is
// incremented by two.
is(day.get(field), 2, "2 searches recorded in FHR.");
finish();
});
});
}

View File

@ -10,7 +10,13 @@ var Preferences = Cu.import("resource://gre/modules/Preferences.jsm", {}).Prefer
var TelemetryReportingPolicy =
Cu.import("resource://gre/modules/TelemetryReportingPolicy.jsm", {}).TelemetryReportingPolicy;
XPCOMUtils.defineLazyGetter(this, "gDatareportingService",
() => Cc["@mozilla.org/datareporting/service;1"]
.getService(Ci.nsISupports)
.wrappedJSObject);
const PREF_BRANCH = "datareporting.policy.";
const PREF_DRS_ENABLED = "datareporting.healthreport.service.enabled";
const PREF_BYPASS_NOTIFICATION = PREF_BRANCH + "dataSubmissionPolicyBypassNotification";
const PREF_CURRENT_POLICY_VERSION = PREF_BRANCH + "currentPolicyVersion";
const PREF_ACCEPTED_POLICY_VERSION = PREF_BRANCH + "dataSubmissionPolicyAcceptedVersion";
@ -97,21 +103,31 @@ var checkInfobarButton = Task.async(function* (aNotification) {
});
add_task(function* setup(){
const drsEnabled = Preferences.get(PREF_DRS_ENABLED, true);
const bypassNotification = Preferences.get(PREF_BYPASS_NOTIFICATION, true);
const currentPolicyVersion = Preferences.get(PREF_CURRENT_POLICY_VERSION, 1);
// Register a cleanup function to reset our preferences.
registerCleanupFunction(() => {
Preferences.set(PREF_DRS_ENABLED, drsEnabled);
Preferences.set(PREF_BYPASS_NOTIFICATION, bypassNotification);
Preferences.set(PREF_CURRENT_POLICY_VERSION, currentPolicyVersion);
// Start polling again.
gDatareportingService.policy.startPolling();
return closeAllNotifications();
});
// Disable Healthreport/Data reporting service.
Preferences.set(PREF_DRS_ENABLED, false);
// Don't skip the infobar visualisation.
Preferences.set(PREF_BYPASS_NOTIFICATION, false);
// Set the current policy version.
Preferences.set(PREF_CURRENT_POLICY_VERSION, TEST_POLICY_VERSION);
// Stop the polling to make sure no policy gets displayed by FHR.
gDatareportingService.policy.stopPolling();
});
function clearAcceptedPolicy() {

View File

@ -0,0 +1,213 @@
/* 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/. */
var originalPolicy = null;
/**
* Display a datareporting notification to the user.
*
* @param {String} name
*/
function sendNotifyRequest(name) {
let ns = {};
Cu.import("resource://gre/modules/services/datareporting/policy.jsm", ns);
Cu.import("resource://gre/modules/Preferences.jsm", ns);
let service = Cc["@mozilla.org/datareporting/service;1"]
.getService(Ci.nsISupports)
.wrappedJSObject;
ok(service.healthReporter, "Health Reporter instance is available.");
Cu.import("resource://gre/modules/Promise.jsm", ns);
let deferred = ns.Promise.defer();
if (!originalPolicy) {
originalPolicy = service.policy;
}
let policyPrefs = new ns.Preferences("testing." + name + ".");
ok(service._prefs, "Health Reporter prefs are available.");
let hrPrefs = service._prefs;
let policy = new ns.DataReportingPolicy(policyPrefs, hrPrefs, service);
policy.dataSubmissionPolicyBypassNotification = false;
service.policy = policy;
policy.firstRunDate = new Date(Date.now() - 24 * 60 * 60 * 1000);
service.healthReporter.onInit().then(function onSuccess () {
is(policy.ensureUserNotified(), false, "User not notified about data policy on init.");
ok(policy._userNotifyPromise, "_userNotifyPromise defined.");
policy._userNotifyPromise.then(
deferred.resolve.bind(deferred),
deferred.reject.bind(deferred)
);
}.bind(this), deferred.reject.bind(deferred));
return [policy, deferred.promise];
}
var dumpAppender, rootLogger;
function test() {
registerCleanupFunction(cleanup);
waitForExplicitFinish();
let ns = {};
Components.utils.import("resource://gre/modules/Log.jsm", ns);
rootLogger = ns.Log.repository.rootLogger;
dumpAppender = new ns.Log.DumpAppender();
dumpAppender.level = ns.Log.Level.All;
rootLogger.addAppender(dumpAppender);
closeAllNotifications().then(function onSuccess () {
let notification = document.getElementById("global-notificationbox");
notification.addEventListener("AlertActive", function active() {
notification.removeEventListener("AlertActive", active, true);
is(notification.allNotifications.length, 1, "Notification Displayed.");
executeSoon(function afterNotification() {
waitForNotificationClose(notification.currentNotification, function onClose() {
is(notification.allNotifications.length, 0, "No notifications remain.");
is(policy.dataSubmissionPolicyAcceptedVersion, 1, "Version pref set.");
ok(policy.dataSubmissionPolicyNotifiedDate.getTime() > -1, "Date pref set.");
test_multiple_windows();
});
notification.currentNotification.close();
});
}, true);
let [policy, promise] = sendNotifyRequest("single_window_notified");
is(policy.dataSubmissionPolicyAcceptedVersion, 0, "No version should be set on init.");
is(policy.dataSubmissionPolicyNotifiedDate.getTime(), 0, "No date should be set on init.");
is(policy.userNotifiedOfCurrentPolicy, false, "User not notified about datareporting policy.");
promise.then(function () {
is(policy.dataSubmissionPolicyAcceptedVersion, 1, "Policy version set.");
is(policy.dataSubmissionPolicyNotifiedDate.getTime() > 0, true, "Policy date set.");
is(policy.userNotifiedOfCurrentPolicy, true, "User notified about datareporting policy.");
}.bind(this), function (err) {
throw err;
});
}.bind(this), function onError (err) {
throw err;
});
}
function test_multiple_windows() {
// Ensure we see the notification on all windows and that action on one window
// results in dismiss on every window.
let window2 = OpenBrowserWindow();
whenDelayedStartupFinished(window2, function onWindow() {
let notification1 = document.getElementById("global-notificationbox");
let notification2 = window2.document.getElementById("global-notificationbox");
ok(notification2, "2nd window has a global notification box.");
let [policy, promise] = sendNotifyRequest("multiple_window_behavior");
let displayCount = 0;
let prefWindowOpened = false;
let mutationObserversRemoved = false;
function onAlertDisplayed() {
displayCount++;
if (displayCount != 2) {
return;
}
ok(true, "Data reporting info bar displayed on all open windows.");
// We register two independent observers and we need both to clean up
// properly. This handles gating for test completion.
function maybeFinish() {
if (!prefWindowOpened) {
dump("Not finishing test yet because pref pane hasn't yet appeared.\n");
return;
}
if (!mutationObserversRemoved) {
dump("Not finishing test yet because mutation observers haven't been removed yet.\n");
return;
}
window2.close();
dump("Finishing multiple window test.\n");
rootLogger.removeAppender(dumpAppender);
dumpAppender = null;
rootLogger = null;
finish();
}
let closeCount = 0;
function onAlertClose() {
closeCount++;
if (closeCount != 2) {
return;
}
ok(true, "Closing info bar on one window closed them on all.");
is(policy.userNotifiedOfCurrentPolicy, true, "Data submission policy accepted.");
is(notification1.allNotifications.length, 0, "No notifications remain on main window.");
is(notification2.allNotifications.length, 0, "No notifications remain on 2nd window.");
mutationObserversRemoved = true;
maybeFinish();
}
waitForNotificationClose(notification1.currentNotification, onAlertClose);
waitForNotificationClose(notification2.currentNotification, onAlertClose);
// While we're here, we dual purpose this test to check that pressing the
// button does the right thing.
let buttons = notification2.currentNotification.getElementsByTagName("button");
is(buttons.length, 1, "There is 1 button in the data reporting notification.");
let button = buttons[0];
// Add an observer to ensure the "advanced" pane opened (but don't bother
// closing it - we close the entire window when done.)
Services.obs.addObserver(function observer(prefWin, topic, data) {
Services.obs.removeObserver(observer, "advanced-pane-loaded");
ok(true, "Advanced preferences opened on info bar button press.");
executeSoon(function soon() {
prefWindowOpened = true;
maybeFinish();
});
}, "advanced-pane-loaded", false);
button.click();
}
notification1.addEventListener("AlertActive", function active1() {
notification1.removeEventListener("AlertActive", active1, true);
executeSoon(onAlertDisplayed);
}, true);
notification2.addEventListener("AlertActive", function active2() {
notification2.removeEventListener("AlertActive", active2, true);
executeSoon(onAlertDisplayed);
}, true);
promise.then(null, function onError(err) {
throw err;
});
});
}
function cleanup () {
// In case some test fails.
if (originalPolicy) {
let service = Cc["@mozilla.org/datareporting/service;1"]
.getService(Ci.nsISupports)
.wrappedJSObject;
service.policy = originalPolicy;
}
return closeAllNotifications();
}

View File

@ -103,6 +103,7 @@ function* compareCounts(clickCallback) {
// FHR -- first make sure the engine has an identifier so that FHR is happy.
Object.defineProperty(engine.wrappedJSObject, "identifier",
{ value: engineID });
let fhrCount = yield getNumberOfSearchesInFHR(engine.name, "urlbar");
gURLBar.focus();
yield clickCallback();
@ -125,6 +126,10 @@ function* compareCounts(clickCallback) {
Assert.ok(histogramKey in snapshot, "histogram with key should be recorded");
Assert.equal(snapshot[histogramKey].sum, histogramCount + 1,
"histogram sum should be incremented");
// FHR
let newFHRCount = yield getNumberOfSearchesInFHR(engine.name, "urlbar");
Assert.equal(newFHRCount, fhrCount + 1, "should be recorded in FHR");
}
/**

View File

@ -0,0 +1,85 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
add_task(function* test_healthreport_search_recording() {
try {
let cm = Cc["@mozilla.org/categorymanager;1"].getService(Ci.nsICategoryManager);
cm.getCategoryEntry("healthreport-js-provider-default", "SearchesProvider");
} catch (ex) {
// Health Report disabled, or no SearchesProvider.
ok(true, "Firefox Health Report is not enabled.");
return;
}
let reporter = Cc["@mozilla.org/datareporting/service;1"]
.getService()
.wrappedJSObject
.healthReporter;
ok(reporter, "Health Reporter available.");
yield reporter.onInit();
let provider = reporter.getProvider("org.mozilla.searches");
ok(provider, "Searches provider is available.");
let m = provider.getMeasurement("counts", 3);
let data = yield m.getValues();
let now = new Date();
let oldCount = 0;
// This will to be need changed if default search engine is not Google.
// Note: geoSpecificDefaults are disabled for mochitests, so this is the
// non-US en-US default.
let defaultEngineID = "google";
let field = defaultEngineID + ".urlbar";
if (data.days.hasDay(now)) {
let day = data.days.getDay(now);
if (day.has(field)) {
oldCount = day.get(field);
}
}
let tab = gBrowser.addTab("about:blank");
yield BrowserTestUtils.browserLoaded(tab.linkedBrowser);
gBrowser.selectedTab = tab;
let searchStr = "firefox health report";
let expectedURL = Services.search.currentEngine.
getSubmission(searchStr, "", "keyword").uri.spec;
// Expect the search URL to load but stop it as soon as it starts.
let docLoadPromise = waitForDocLoadAndStopIt(expectedURL);
// Trigger the search.
gURLBar.value = searchStr;
gURLBar.handleCommand();
yield docLoadPromise;
data = yield m.getValues();
ok(data.days.hasDay(now), "We have a search measurement for today.");
let day = data.days.getDay(now);
ok(day.has(field), "Have a search count for the urlbar.");
let newCount = day.get(field);
is(newCount, oldCount + 1, "We recorded one new search.");
// We should record the default search engine if Telemetry is enabled.
let oldTelemetry = Services.prefs.getBoolPref("toolkit.telemetry.enabled");
Services.prefs.setBoolPref("toolkit.telemetry.enabled", true);
m = provider.getMeasurement("engines", 2);
yield provider.collectDailyData();
data = yield m.getValues();
ok(data.days.hasDay(now), "Have engines data when Telemetry is enabled.");
day = data.days.getDay(now);
ok(day.has("default"), "We have default engine data.");
is(day.get("default"), defaultEngineID, "The default engine is reported properly.");
// Restore.
Services.prefs.setBoolPref("toolkit.telemetry.enabled", oldTelemetry);
gBrowser.removeTab(tab);
});

View File

@ -1202,3 +1202,61 @@ function promiseCrashReport(expectedExtra) {
}
});
}
/**
* Retrieves the number of searches recorded in FHR for the current day.
*
* @param aEngineName
* name of the setup search engine.
* @param aSource
* The FHR "source" name for the search, like "abouthome" or "urlbar".
*
* @return {Promise} Returns a promise resolving to the number of searches.
*/
function getNumberOfSearchesInFHR(aEngineName, aSource) {
let reporter = Components.classes["@mozilla.org/datareporting/service;1"]
.getService()
.wrappedJSObject
.healthReporter;
ok(reporter, "Health Reporter instance available.");
return reporter.onInit().then(function onInit() {
let provider = reporter.getProvider("org.mozilla.searches");
ok(provider, "Searches provider is available.");
let m = provider.getMeasurement("counts", 3);
return m.getValues().then(data => {
let now = new Date();
let yday = new Date(now);
yday.setDate(yday.getDate() - 1);
// Add the number of searches recorded yesterday to the number of searches
// recorded today. This makes the test not fail intermittently when it is
// run at midnight and we accidentally compare the number of searches from
// different days. Tests are always run with an empty profile so there
// are no searches from yesterday, normally. Should the test happen to run
// past midnight we make sure to count them in as well.
return getNumberOfSearchesInFHRByDate(aEngineName, aSource, data, now) +
getNumberOfSearchesInFHRByDate(aEngineName, aSource, data, yday);
});
});
}
/**
* Helper for getNumberOfSearchesInFHR. You probably don't want to call this
* directly.
*/
function getNumberOfSearchesInFHRByDate(aEngineName, aSource, aData, aDate) {
if (aData.days.hasDay(aDate)) {
let id = Services.search.getEngineByName(aEngineName).identifier;
let day = aData.days.getDay(aDate);
let field = id + "." + aSource;
if (day.has(field)) {
return day.get(field) || 0;
}
}
return 0; // No records found.
}

View File

@ -7,14 +7,35 @@
<script type="application/javascript;version=1.7">
function init() {
window.addEventListener("message", doTest, false);
doTest();
window.addEventListener("message", function process(e) {
// The init function of abouthealth.js schedules an initial payload event,
// which will be sent after the payload data has been collected. This extra
// event can cause unexpected successes/failures in this test, so we wait
// for the extra event to arrive here before progressing with the actual
// test.
if (e.data.type == "payload") {
window.removeEventListener("message", process, false);
window.addEventListener("message", doTest, false);
doTest();
}
}, false);
}
function checkSubmissionValue(payload, expectedValue) {
return payload.enabled == expectedValue;
}
function validatePayload(payload) {
payload = JSON.parse(payload);
// xxxmpc - this is some pretty low-bar validation, but we have plenty of tests of that API elsewhere
if (!payload.thisPingDate)
return false;
return true;
}
function isArray(arg) {
return Object.prototype.toString.call(arg) === '[object Array]';
}
@ -120,11 +141,11 @@ var tests = [
},
},
{
info: "Verifying that we can get the current ping data while submission is disabled",
event: "RequestCurrentPingData",
payloadType: "telemetry-current-ping-data",
info: "Verifying we can get a payload while submission is disabled",
event: "RequestCurrentPayload",
payloadType: "payload",
validateResponse: function(payload) {
return validateCurrentTelemetryPingData(payload);
return validatePayload(payload);
},
},
{
@ -143,6 +164,14 @@ var tests = [
return checkSubmissionValue(payload, true);
},
},
{
info: "Verifying we can get a payload after re-enabling",
event: "RequestCurrentPayload",
payloadType: "payload",
validateResponse: function(payload) {
return validatePayload(payload);
},
},
{
info: "Verifying that we can get the current Telemetry environment data",
event: "RequestCurrentEnvironment",

View File

@ -454,7 +454,7 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
<body><![CDATA[
let engine =
Services.search.getEngineByName(action.params.engineName);
BrowserSearch.recordSearchInTelemetry(engine, "urlbar");
BrowserSearch.recordSearchInHealthReport(engine, "urlbar");
let query = action.params.searchSuggestion ||
action.params.searchQuery;
let submission = engine.getSubmission(query, null, "keyword");

View File

@ -431,7 +431,7 @@ BrowserGlue.prototype = {
Cu.reportError(ex);
}
let win = RecentWindow.getMostRecentBrowserWindow();
win.BrowserSearch.recordSearchInTelemetry(engine, "urlbar");
win.BrowserSearch.recordSearchInHealthReport(engine, "urlbar");
break;
case "browser-search-engine-modified":
// Ensure we cleanup the hiddenOneOffs pref when removing

View File

@ -7,8 +7,6 @@ Components.utils.import("resource://gre/modules/DownloadUtils.jsm");
Components.utils.import("resource://gre/modules/LoadContextInfo.jsm");
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
const PREF_UPLOAD_ENABLED = "datareporting.healthreport.uploadEnabled";
var gAdvancedPane = {
_inited: false,
@ -291,23 +289,38 @@ var gAdvancedPane = {
initSubmitHealthReport: function () {
this._setupLearnMoreLink("datareporting.healthreport.infoURL", "FHRLearnMore");
let policy = Components.classes["@mozilla.org/datareporting/service;1"]
.getService(Components.interfaces.nsISupports)
.wrappedJSObject
.policy;
let checkbox = document.getElementById("submitHealthReportBox");
if (Services.prefs.prefIsLocked(PREF_UPLOAD_ENABLED)) {
if (!policy || policy.healthReportUploadLocked) {
checkbox.setAttribute("disabled", "true");
return;
}
checkbox.checked = Services.prefs.getBoolPref(PREF_UPLOAD_ENABLED);
checkbox.checked = policy.healthReportUploadEnabled;
this.setTelemetrySectionEnabled(checkbox.checked);
},
/**
* Update the health report preference with state from checkbox.
* Update the health report policy acceptance with state from checkbox.
*/
updateSubmitHealthReport: function () {
let policy = Components.classes["@mozilla.org/datareporting/service;1"]
.getService(Components.interfaces.nsISupports)
.wrappedJSObject
.policy;
if (!policy) {
return;
}
let checkbox = document.getElementById("submitHealthReportBox");
Services.prefs.setBoolPref(PREF_UPLOAD_ENABLED, checkbox.checked);
policy.recordHealthReportUploadEnabled(checkbox.checked,
"Checkbox from preferences pane");
this.setTelemetrySectionEnabled(checkbox.checked);
},
#endif

View File

@ -18,7 +18,7 @@ skip-if = os != "win" # This test tests the windows-specific app selection dialo
[browser_connection_bug388287.js]
[browser_cookies_exceptions.js]
[browser_healthreport.js]
skip-if = true || !healthreport # Bug 1185403 for the "true"
skip-if = true || !healthreport || (os == 'linux' && debug) # Bug 1185403 for the "true"
[browser_homepages_filter_aboutpreferences.js]
[browser_notifications_do_not_disturb.js]
[browser_permissions_urlFieldHidden.js]

View File

@ -3,8 +3,6 @@
"use strict";
const FHR_UPLOAD_ENABLED = "datareporting.healthreport.uploadEnabled";
function runPaneTest(fn) {
open_preferences((win) => {
let doc = win.document;
@ -12,7 +10,14 @@ function runPaneTest(fn) {
let advancedPrefs = doc.getElementById("advancedPrefs");
let tab = doc.getElementById("dataChoicesTab");
advancedPrefs.selectedTab = tab;
fn(win, doc);
let policy = Components.classes["@mozilla.org/datareporting/service;1"]
.getService(Components.interfaces.nsISupports)
.wrappedJSObject
.policy;
ok(policy, "Policy object is defined.");
fn(win, doc, policy);
});
}
@ -23,9 +28,8 @@ function test() {
runPaneTest(testBasic);
}
function testBasic(win, doc) {
is(Services.prefs.getBoolPref(FHR_UPLOAD_ENABLED), true,
"Health Report upload enabled on app first run.");
function testBasic(win, doc, policy) {
is(policy.healthReportUploadEnabled, true, "Health Report upload enabled on app first run.");
let checkbox = doc.getElementById("submitHealthReportBox");
ok(checkbox);
@ -33,30 +37,28 @@ function testBasic(win, doc) {
checkbox.checked = false;
checkbox.doCommand();
is(Services.prefs.getBoolPref(FHR_UPLOAD_ENABLED), false,
"Unchecking checkbox opts out of FHR upload.");
is(policy.healthReportUploadEnabled, false, "Unchecking checkbox opts out of FHR upload.");
checkbox.checked = true;
checkbox.doCommand();
is(Services.prefs.getBoolPref(FHR_UPLOAD_ENABLED), true,
"Checking checkbox allows FHR upload.");
is(policy.healthReportUploadEnabled, true, "Checking checkbox allows FHR upload.");
win.close();
Services.prefs.lockPref(FHR_UPLOAD_ENABLED);
Services.prefs.lockPref("datareporting.healthreport.uploadEnabled");
runPaneTest(testUploadDisabled);
}
function testUploadDisabled(win, doc) {
ok(Services.prefs.prefIsLocked(FHR_UPLOAD_ENABLED), "Upload enabled flag is locked.");
function testUploadDisabled(win, doc, policy) {
ok(policy.healthReportUploadLocked, "Upload enabled flag is locked.");
let checkbox = doc.getElementById("submitHealthReportBox");
is(checkbox.getAttribute("disabled"), "true", "Checkbox is disabled if upload flag is locked.");
Services.prefs.unlockPref(FHR_UPLOAD_ENABLED);
policy._healthReportPrefs.unlock("uploadEnabled");
win.close();
finish();
}
function resetPreferences() {
Services.prefs.clearUserPref(FHR_UPLOAD_ENABLED);
Services.prefs.clearUserPref("datareporting.healthreport.uploadEnabled");
}

View File

@ -420,7 +420,7 @@
if (telemetrySearchDetails && telemetrySearchDetails.index == -1) {
telemetrySearchDetails = null;
}
BrowserSearch.recordSearchInTelemetry(engine, "searchbar", telemetrySearchDetails);
BrowserSearch.recordSearchInHealthReport(engine, "searchbar", telemetrySearchDetails);
// null parameter below specifies HTML response for search
let params = {
postData: submission.postData,

View File

@ -3,50 +3,75 @@
"use strict";
var Preferences = Cu.import("resource://gre/modules/Preferences.jsm", {}).Preferences;
function test() {
requestLongerTimeout(2);
waitForExplicitFinish();
resetPreferences();
function testTelemetry() {
// Find the right bucket for the "Foo" engine.
let engine = Services.search.getEngineByName("Foo");
let histogramKey = (engine.identifier || "other-Foo") + ".searchbar";
let numSearchesBefore = 0;
try {
let hs = Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS").snapshot();
if (histogramKey in hs) {
numSearchesBefore = hs[histogramKey].sum;
}
} catch (ex) {
// No searches performed yet, not a problem, |numSearchesBefore| is 0.
}
try {
let cm = Components.classes["@mozilla.org/categorymanager;1"]
.getService(Components.interfaces.nsICategoryManager);
cm.getCategoryEntry("healthreport-js-provider-default", "SearchesProvider");
} catch (ex) {
// Health Report disabled, or no SearchesProvider.
// We need a test or else we'll be marked as failure.
ok(true, "Firefox Health Report is not enabled.");
finish();
return;
}
// Now perform a search and ensure the count is incremented.
let tab = gBrowser.addTab();
gBrowser.selectedTab = tab;
let searchBar = BrowserSearch.searchBar;
function testFHR() {
let reporter = Components.classes["@mozilla.org/datareporting/service;1"]
.getService()
.wrappedJSObject
.healthReporter;
ok(reporter, "Health Reporter available.");
reporter.onInit().then(function onInit() {
let provider = reporter.getProvider("org.mozilla.searches");
let m = provider.getMeasurement("counts", 3);
searchBar.value = "firefox health report";
searchBar.focus();
function afterSearch() {
searchBar.value = "";
gBrowser.removeTab(tab);
// Make sure that the context searches are correctly recorded.
let hs = Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS").snapshot();
Assert.ok(histogramKey in hs, "The histogram must contain the correct key");
Assert.equal(hs[histogramKey].sum, numSearchesBefore + 1,
"Performing a search increments the related SEARCH_COUNTS key by 1.");
m.getValues().then(function onData(data) {
let now = new Date();
let oldCount = 0;
// Find the right bucket for the "Foo" engine.
let engine = Services.search.getEngineByName("Foo");
Services.search.removeEngine(engine);
}
let field = (engine.identifier || "other-Foo") + ".searchbar";
EventUtils.synthesizeKey("VK_RETURN", {});
executeSoon(() => executeSoon(afterSearch));
if (data.days.hasDay(now)) {
let day = data.days.getDay(now);
if (day.has(field)) {
oldCount = day.get(field);
}
}
// Now perform a search and ensure the count is incremented.
let tab = gBrowser.addTab();
gBrowser.selectedTab = tab;
let searchBar = BrowserSearch.searchBar;
searchBar.value = "firefox health report";
searchBar.focus();
function afterSearch() {
searchBar.value = "";
gBrowser.removeTab(tab);
m.getValues().then(function onData(data) {
ok(data.days.hasDay(now), "Have data for today.");
let day = data.days.getDay(now);
is(day.get(field), oldCount + 1, "Performing a search increments FHR count by 1.");
let engine = Services.search.getEngineByName("Foo");
Services.search.removeEngine(engine);
});
}
EventUtils.synthesizeKey("VK_RETURN", {});
executeSoon(() => executeSoon(afterSearch));
});
});
}
function observer(subject, topic, data) {
@ -59,7 +84,7 @@ function test() {
case "engine-current":
is(Services.search.currentEngine.name, "Foo", "Current engine is Foo");
testTelemetry();
testFHR();
break;
case "engine-removed":
@ -76,6 +101,9 @@ function test() {
}
function resetPreferences() {
Preferences.resetBranch("datareporting.policy.");
Preferences.set("datareporting.policy.dataSubmissionPolicyBypassNotification", true);
let service = Components.classes["@mozilla.org/datareporting/service;1"]
.getService(Components.interfaces.nsISupports)
.wrappedJSObject;
service.policy._prefs.resetBranch("datareporting.policy.");
service.policy.dataSubmissionPolicyBypassNotification = true;
}

View File

@ -12,6 +12,24 @@ Cu.import("resource://gre/modules/Preferences.jsm");
const PREF_FHR_UPLOAD_ENABLED = "datareporting.healthreport.uploadEnabled";
XPCOMUtils.defineLazyGetter(this, "gPolicy", () => {
try {
return Cc["@mozilla.org/datareporting/service;1"]
.getService(Ci.nsISupports)
.wrappedJSObject
.policy;
} catch (e) {
return undefined;
}
});
XPCOMUtils.defineLazyGetter(this, "reporter", () => {
return Cc["@mozilla.org/datareporting/service;1"]
.getService(Ci.nsISupports)
.wrappedJSObject
.healthReporter;
});
XPCOMUtils.defineLazyModuleGetter(this, "TelemetryArchive",
"resource://gre/modules/TelemetryArchive.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "TelemetryEnvironment",
@ -35,13 +53,45 @@ MozSelfSupportInterface.prototype = {
},
get healthReportDataSubmissionEnabled() {
if (gPolicy) {
return gPolicy.healthReportUploadEnabled;
}
// The datareporting service is unavailable or disabled.
return Preferences.get(PREF_FHR_UPLOAD_ENABLED, false);
},
set healthReportDataSubmissionEnabled(enabled) {
if (gPolicy) {
let reason = "Self-support interface sent " +
(enabled ? "opt-in" : "opt-out") +
" command.";
gPolicy.recordHealthReportUploadEnabled(enabled, reason);
return;
}
// The datareporting service is unavailable or disabled.
Preferences.set(PREF_FHR_UPLOAD_ENABLED, enabled);
},
getHealthReportPayload: function () {
return new this._window.Promise(function (aResolve, aReject) {
if (reporter) {
let resolvePayload = function () {
reporter.collectAndObtainJSONPayload(true).then(aResolve, aReject);
};
if (reporter.initialized) {
resolvePayload();
} else {
reporter.onInit().then(resolvePayload, aReject);
}
} else {
aReject(new Error("No reporter"));
}
}.bind(this));
},
resetPref: function(name) {
Services.prefs.clearUserPref(name);
},

View File

@ -31,6 +31,7 @@ MOZ_SAFE_BROWSING=1
MOZ_SERVICES_COMMON=1
MOZ_SERVICES_CRYPTO=1
MOZ_SERVICES_HEALTHREPORT=1
MOZ_SERVICES_METRICS=1
MOZ_SERVICES_SYNC=1
MOZ_SERVICES_CLOUDSYNC=1
MOZ_APP_VERSION=$FIREFOX_VERSION

View File

@ -19,6 +19,7 @@ XPCOMUtils.defineLazyModuleGetter(this, "CommonUtils",
const PREF_EXPERIMENTS_ENABLED = "experiments.enabled";
const PREF_ACTIVE_EXPERIMENT = "experiments.activeExperiment"; // whether we have an active experiment
const PREF_HEALTHREPORT_ENABLED = "datareporting.healthreport.service.enabled";
const PREF_TELEMETRY_ENABLED = "toolkit.telemetry.enabled";
const PREF_TELEMETRY_UNIFIED = "toolkit.telemetry.unified";
const DELAY_INIT_MS = 30 * 1000;
@ -37,7 +38,8 @@ XPCOMUtils.defineLazyGetter(
// We can enable experiments if either unified Telemetry or FHR is on, and the user
// has opted into Telemetry.
return gPrefs.get(PREF_EXPERIMENTS_ENABLED, false) &&
IS_UNIFIED_TELEMETRY && gPrefs.get(PREF_TELEMETRY_ENABLED, false);
(gPrefs.get(PREF_HEALTHREPORT_ENABLED, false) || IS_UNIFIED_TELEMETRY) &&
gPrefs.get(PREF_TELEMETRY_ENABLED, false);
});
XPCOMUtils.defineLazyGetter(

View File

@ -501,7 +501,12 @@
@RESPATH@/components/nsINIProcessor.js
@RESPATH@/components/nsPrompter.manifest
@RESPATH@/components/nsPrompter.js
#ifdef MOZ_DATA_REPORTING
@RESPATH@/components/DataReporting.manifest
@RESPATH@/components/DataReportingService.js
#endif
#ifdef MOZ_SERVICES_HEALTHREPORT
@RESPATH@/components/HealthReportComponents.manifest
@RESPATH@/browser/components/SelfSupportService.manifest
@RESPATH@/browser/components/SelfSupportService.js
#endif

View File

@ -303,8 +303,8 @@ this.ContentSearch = {
};
win.openUILinkIn(submission.uri.spec, where, params);
}
win.BrowserSearch.recordSearchInTelemetry(engine, data.healthReportKey,
data.selection || null);
win.BrowserSearch.recordSearchInHealthReport(engine, data.healthReportKey,
data.selection || null);
return Promise.resolve();
},

View File

@ -23,6 +23,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "HiddenFrame",
const PREF_ENABLED = "browser.selfsupport.enabled";
// Url to open in the Self Support browser, in the urlFormatter service format.
const PREF_URL = "browser.selfsupport.url";
// FHR status.
const PREF_FHR_ENABLED = "datareporting.healthreport.service.enabled";
// Unified Telemetry status.
const PREF_TELEMETRY_UNIFIED = "toolkit.telemetry.unified";
// UITour status.
@ -82,8 +84,8 @@ var SelfSupportBackendInternal = {
Preferences.observe(PREF_BRANCH_LOG, this._configureLogging, this);
// Only allow to use SelfSupport if Unified Telemetry is enabled.
let reportingEnabled = IS_UNIFIED_TELEMETRY;
// Only allow to use SelfSupport if either FHR or Unified Telemetry is enabled.
let reportingEnabled = Preferences.get(PREF_FHR_ENABLED, false) || IS_UNIFIED_TELEMETRY;
if (!reportingEnabled) {
this._log.config("init - Disabling SelfSupport because FHR and Unified Telemetry are disabled.");
return;

View File

@ -20,6 +20,26 @@ interface MozSelfSupport
*/
attribute boolean healthReportDataSubmissionEnabled;
/**
* Retrieves the FHR payload object, which is of the form:
*
* {
* version: Number,
* clientID: String,
* clientIDVersion: Number,
* thisPingDate: String,
* geckoAppInfo: Object,
* data: Object
* }
*
* Refer to the getJSONPayload function in healthreporter.jsm for more
* information.
*
* @return Promise<Object>
* Resolved when the FHR payload data has been collected.
*/
Promise<object> getHealthReportPayload();
/**
* Retrieve a list of the archived Telemetry pings.
* This contains objects with ping info, which are of the form:

View File

@ -12,6 +12,7 @@ MOZ_PLACES=1
MOZ_EXTENSIONS_DEFAULT=" gio"
MOZ_SERVICES_COMMON=1
MOZ_SERVICES_CRYPTO=1
MOZ_SERVICES_METRICS=1
MOZ_SERVICES_SYNC=1
MOZ_MEDIA_NAVIGATOR=1
MOZ_SERVICES_HEALTHREPORT=1

View File

@ -432,6 +432,11 @@
@BINPATH@/components/PeerConnection.manifest
#endif
#ifdef MOZ_SERVICES_HEALTHREPORT
@BINPATH@/components/HealthReportComponents.manifest
@BINPATH@/components/HealthReportService.js
#endif
@BINPATH@/components/CaptivePortalDetectComponents.manifest
@BINPATH@/components/captivedetect.js

View File

@ -1,12 +1,12 @@
#include ../../netwerk/base/security-prefs.js
#include init/all.js
#ifdef MOZ_DATA_REPORTING
#include ../../toolkit/components/telemetry/datareporting-prefs.js
#include ../../services/datareporting/datareporting-prefs.js
#endif
#ifdef MOZ_SERVICES_HEALTHREPORT
#if MOZ_WIDGET_TOOLKIT == android
#include ../../mobile/android/chrome/content/healthreport-prefs.js
#else
#include ../../toolkit/components/telemetry/healthreport-prefs.js
#include ../../services/healthreport/healthreport-prefs.js
#endif
#endif

View File

@ -0,0 +1,16 @@
# 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}
# graphene: {d1bfe7d9-c01e-4237-998b-7b5f960a4314}
# The Data Reporting Service drives collection and submission of metrics
# and other useful data to Mozilla. It drives the display of the data
# submission notification info bar and thus is required by Firefox Health
# Report and Telemetry.
component {41f6ae36-a79f-4613-9ac3-915e70f83789} DataReportingService.js
contract @mozilla.org/datareporting/service;1 {41f6ae36-a79f-4613-9ac3-915e70f83789}
category app-startup DataReportingService service,@mozilla.org/datareporting/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} application={d1bfe7d9-c01e-4237-998b-7b5f960a4314}

View File

@ -0,0 +1,296 @@
/* 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/ClientID.jsm");
Cu.import("resource://gre/modules/Preferences.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource://gre/modules/osfile.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Log",
"resource://gre/modules/Log.jsm");
const ROOT_BRANCH = "datareporting.";
const POLICY_BRANCH = ROOT_BRANCH + "policy.";
const HEALTHREPORT_BRANCH = ROOT_BRANCH + "healthreport.";
const HEALTHREPORT_LOGGING_BRANCH = HEALTHREPORT_BRANCH + "logging.";
const DEFAULT_LOAD_DELAY_MSEC = 10 * 1000;
const DEFAULT_LOAD_DELAY_FIRST_RUN_MSEC = 60 * 1000;
/**
* The Firefox Health Report XPCOM service.
*
* External consumers will be interested in the "reporter" property of this
* service. This property is a `HealthReporter` instance that powers the
* service. The property may be null if the Health Report service is not
* enabled.
*
* EXAMPLE USAGE
* =============
*
* let reporter = Cc["@mozilla.org/datareporting/service;1"]
* .getService(Ci.nsISupports)
* .wrappedJSObject
* .healthReporter;
*
* if (reporter.haveRemoteData) {
* // ...
* }
*
* IMPLEMENTATION NOTES
* ====================
*
* In order to not adversely impact application start time, the `HealthReporter`
* instance is not initialized until a few seconds after "final-ui-startup."
* The exact delay is configurable via preferences so it can be adjusted with
* a hotfix extension if the default value is ever problematic. Because of the
* overhead with the initial creation of the database, the first run is delayed
* even more than subsequent runs. This does mean that the first moments of
* browser activity may be lost by FHR.
*
* Shutdown of the `HealthReporter` instance is handled completely within the
* instance (it registers observers on initialization). See the notes on that
* type for more.
*/
this.DataReportingService = function () {
this.wrappedJSObject = this;
this._quitting = false;
this._os = Cc["@mozilla.org/observer-service;1"]
.getService(Ci.nsIObserverService);
}
DataReportingService.prototype = Object.freeze({
classID: Components.ID("{41f6ae36-a79f-4613-9ac3-915e70f83789}"),
QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
Ci.nsISupportsWeakReference]),
//---------------------------------------------
// Start of policy listeners.
//---------------------------------------------
/**
* Called when policy requests data upload.
*/
onRequestDataUpload: function (request) {
if (!this.healthReporter) {
return;
}
this.healthReporter.requestDataUpload(request);
},
onNotifyDataPolicy: function (request) {
Observers.notify("datareporting:notify-data-policy:request", request);
},
onRequestRemoteDelete: function (request) {
if (!this.healthReporter) {
return;
}
this.healthReporter.deleteRemoteData(request);
},
//---------------------------------------------
// End of policy listeners.
//---------------------------------------------
observe: function observe(subject, topic, data) {
switch (topic) {
case "app-startup":
this._os.addObserver(this, "profile-after-change", true);
break;
case "profile-after-change":
this._os.removeObserver(this, "profile-after-change");
try {
this._prefs = new Preferences(HEALTHREPORT_BRANCH);
// We can't interact with prefs until after the profile is present.
let policyPrefs = new Preferences(POLICY_BRANCH);
this.policy = new DataReportingPolicy(policyPrefs, this._prefs, this);
this._os.addObserver(this, "sessionstore-windows-restored", true);
} catch (ex) {
Cu.reportError("Exception when initializing data reporting service: " +
Log.exceptionStr(ex));
}
break;
case "sessionstore-windows-restored":
this._os.removeObserver(this, "sessionstore-windows-restored");
this._os.addObserver(this, "quit-application", false);
let policy = this.policy;
policy.startPolling();
// Don't initialize Firefox Health Reporter collection and submission
// service unless it is enabled.
if (!this._prefs.get("service.enabled", true)) {
return;
}
let haveFirstRun = this._prefs.get("service.firstRun", false);
let delayInterval;
if (haveFirstRun) {
delayInterval = this._prefs.get("service.loadDelayMsec") ||
DEFAULT_LOAD_DELAY_MSEC;
} else {
delayInterval = this._prefs.get("service.loadDelayFirstRunMsec") ||
DEFAULT_LOAD_DELAY_FIRST_RUN_MSEC;
}
// 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() {
delete this.timer;
// There could be a race between "quit-application" firing and
// this callback being invoked. We close that door.
if (this._quitting) {
return;
}
// Side effect: instantiates the reporter instance if not already
// accessed.
//
// The instance installs its own shutdown observers. So, we just
// fire and forget: it will clean itself up.
let reporter = this.healthReporter;
policy.ensureUserNotified();
}.bind(this),
}, delayInterval, this.timer.TYPE_ONE_SHOT);
break;
case "quit-application":
this._os.removeObserver(this, "quit-application");
this._quitting = true;
// Shutdown doesn't clear pending timers. So, we need to explicitly
// cancel our health reporter initialization timer or else it will
// attempt initialization after shutdown has commenced. This would
// likely lead to stalls or crashes.
if (this.timer) {
this.timer.cancel();
}
if (this.policy) {
this.policy.stopPolling();
}
break;
}
},
/**
* The HealthReporter instance associated with this service.
*
* If the service is disabled, this will return null.
*
* The obtained instance may not be fully initialized.
*/
get healthReporter() {
if (!this._prefs.get("service.enabled", true)) {
return null;
}
if ("_healthReporter" in this) {
return this._healthReporter;
}
try {
this._loadHealthReporter();
} catch (ex) {
this._healthReporter = null;
Cu.reportError("Exception when obtaining health reporter: " +
Log.exceptionStr(ex));
}
return this._healthReporter;
},
_loadHealthReporter: function () {
// This should never happen. It was added to help trace down bug 924307.
if (!this.policy) {
throw new Error("this.policy not set.");
}
let ns = {};
// Lazy import so application startup isn't adversely affected.
Cu.import("resource://gre/modules/HealthReport.jsm", ns);
// How many times will we rewrite this code before rolling it up into a
// generic module? See also bug 451283.
const LOGGERS = [
"Services.DataReporting",
"Services.HealthReport",
"Services.Metrics",
"Services.BagheeraClient",
"Sqlite.Connection.healthreport",
];
let loggingPrefs = new Preferences(HEALTHREPORT_LOGGING_BRANCH);
if (loggingPrefs.get("consoleEnabled", true)) {
let level = loggingPrefs.get("consoleLevel", "Warn");
let appender = new Log.ConsoleAppender();
appender.level = Log.Level[level] || Log.Level.Warn;
for (let name of LOGGERS) {
let logger = Log.repository.getLogger(name);
logger.addAppender(appender);
}
}
if (loggingPrefs.get("dumpEnabled", false)) {
let level = loggingPrefs.get("dumpLevel", "Debug");
let appender = new Log.DumpAppender();
appender.level = Log.Level[level] || Log.Level.Debug;
for (let name of LOGGERS) {
let logger = Log.repository.getLogger(name);
logger.addAppender(appender);
}
}
this._healthReporter = new ns.HealthReporter(HEALTHREPORT_BRANCH, this.policy);
// Wait for initialization to finish so if a shutdown occurs before init
// has finished we don't adversely affect app startup on next run.
this._healthReporter.init().then(function onInit() {
this._prefs.set("service.firstRun", true);
}.bind(this));
},
/**
* This returns a promise resolving to the the stable client ID we use for
* data reporting (FHR & Telemetry). Previously exising FHR client IDs are
* migrated to this.
*
* @return Promise<string> The stable client ID.
*/
getClientID: function() {
return ClientID.getClientID();
},
});
this.NSGetFactory = XPCOMUtils.generateNSGetFactory([DataReportingService]);
#define MERGED_COMPARTMENT
#include ../common/observers.js
;
#include policy.jsm
;

View File

@ -3,6 +3,8 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
pref("datareporting.policy.dataSubmissionEnabled", true);
pref("datareporting.policy.dataSubmissionEnabled.v2", false);
pref("datareporting.policy.firstRunTime", "0");
pref("datareporting.policy.dataSubmissionPolicyNotifiedTime", "0");
pref("datareporting.policy.dataSubmissionPolicyAcceptedVersion", 0);
pref("datareporting.policy.dataSubmissionPolicyBypassNotification", false);

View File

@ -0,0 +1,52 @@
/* 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 = ["MockPolicyListener"];
const {utils: Cu} = Components;
Cu.import("resource://gre/modules/Log.jsm");
this.MockPolicyListener = function MockPolicyListener() {
this._log = Log.repository.getLogger("Services.DataReporting.Testing.MockPolicyListener");
this._log.level = Log.Level["Debug"];
this.requestDataUploadCount = 0;
this.lastDataRequest = null;
this.requestRemoteDeleteCount = 0;
this.lastRemoteDeleteRequest = null;
this.notifyUserCount = 0;
this.lastNotifyRequest = null;
}
MockPolicyListener.prototype = {
onRequestDataUpload: function (request) {
this._log.info("onRequestDataUpload invoked.");
this.requestDataUploadCount++;
this.lastDataRequest = request;
},
onRequestRemoteDelete: function (request) {
this._log.info("onRequestRemoteDelete invoked.");
this.requestRemoteDeleteCount++;
this.lastRemoteDeleteRequest = request;
},
onNotifyDataPolicy: function (request, rejectMessage=null) {
this._log.info("onNotifyDataPolicy invoked.");
this.notifyUserCount++;
this.lastNotifyRequest = request;
if (rejectMessage) {
request.onUserNotifyFailed(rejectMessage);
} else {
request.onUserNotifyComplete();
}
},
};

View File

@ -0,0 +1,23 @@
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# 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/.
XPCSHELL_TESTS_MANIFESTS += ['tests/xpcshell/xpcshell.ini']
EXTRA_COMPONENTS += [
'DataReporting.manifest',
]
EXTRA_PP_COMPONENTS += [
'DataReportingService.js',
]
EXTRA_PP_JS_MODULES.services.datareporting += [
'policy.jsm',
]
TESTING_JS_MODULES.services.datareporting += [
'modules-testing/mocks.jsm',
]

View File

@ -0,0 +1,927 @@
/* 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/. */
/**
* This file is in transition. Most of its content needs to be moved under
* /services/healthreport.
*/
#ifndef MERGED_COMPARTMENT
"use strict";
this.EXPORTED_SYMBOLS = [
"DataSubmissionRequest", // For test use only.
"DataReportingPolicy",
"DATAREPORTING_POLICY_VERSION",
];
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
#endif
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-common/utils.js");
Cu.import("resource://gre/modules/UpdateUtils.jsm");
// The current policy version number. If the version number stored in the prefs
// is smaller than this, data upload will be disabled until the user is re-notified
// about the policy changes.
const DATAREPORTING_POLICY_VERSION = 1;
const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
// Used as a sanity lower bound for dates stored in prefs. This module was
// implemented in 2012, so any earlier dates indicate an incorrect clock.
const OLDEST_ALLOWED_YEAR = 2012;
/**
* Represents a request to display data policy.
*
* Receivers of these instances are expected to call one or more of the on*
* functions when events occur.
*
* When one of these requests is received, the first thing a callee should do
* is present notification to the user of the data policy. When the notice
* is displayed to the user, the callee should call `onUserNotifyComplete`.
*
* If for whatever reason the callee could not display a notice,
* it should call `onUserNotifyFailed`.
*
* @param policy
* (DataReportingPolicy) The policy instance this request came from.
* @param deferred
* (deferred) The promise that will be fulfilled when display occurs.
*/
function NotifyPolicyRequest(policy, deferred) {
this.policy = policy;
this.deferred = deferred;
}
NotifyPolicyRequest.prototype = Object.freeze({
/**
* Called when the user is notified of the policy.
*/
onUserNotifyComplete: function () {
return this.deferred.resolve();
},
/**
* Called when there was an error notifying the user about the policy.
*
* @param error
* (Error) Explains what went wrong.
*/
onUserNotifyFailed: function (error) {
return this.deferred.reject(error);
},
});
/**
* Represents a request to submit data.
*
* Instances of this are created when the policy requests data upload or
* deletion.
*
* Receivers are expected to call one of the provided on* functions to signal
* completion of the request.
*
* Instances of this type should not be instantiated outside of this file.
* Receivers of instances of this type should not attempt to do anything with
* the instance except call one of the on* methods.
*/
this.DataSubmissionRequest = function (promise, expiresDate, isDelete) {
this.promise = promise;
this.expiresDate = expiresDate;
this.isDelete = isDelete;
this.state = null;
this.reason = null;
}
this.DataSubmissionRequest.prototype = Object.freeze({
NO_DATA_AVAILABLE: "no-data-available",
SUBMISSION_SUCCESS: "success",
SUBMISSION_FAILURE_SOFT: "failure-soft",
SUBMISSION_FAILURE_HARD: "failure-hard",
UPLOAD_IN_PROGRESS: "upload-in-progress",
/**
* No submission was attempted because no data was available.
*
* In the case of upload, this means there is no data to upload (perhaps
* it isn't available yet). In case of remote deletion, it means that there
* is no remote data to delete.
*/
onNoDataAvailable: function onNoDataAvailable() {
this.state = this.NO_DATA_AVAILABLE;
this.promise.resolve(this);
return this.promise.promise;
},
/**
* Data submission has completed successfully.
*
* In case of upload, this means the upload completed successfully. In case
* of deletion, the data was deleted successfully.
*
* @param date
* (Date) When data submission occurred.
*/
onSubmissionSuccess: function onSubmissionSuccess(date) {
this.state = this.SUBMISSION_SUCCESS;
this.submissionDate = date;
this.promise.resolve(this);
return this.promise.promise;
},
/**
* There was a recoverable failure when submitting data.
*
* Perhaps the server was down. Perhaps the network wasn't available. The
* policy may request submission again after a short delay.
*
* @param reason
* (string) Why the failure occurred. For logging purposes only.
*/
onSubmissionFailureSoft: function onSubmissionFailureSoft(reason=null) {
this.state = this.SUBMISSION_FAILURE_SOFT;
this.reason = reason;
this.promise.resolve(this);
return this.promise.promise;
},
/**
* There was an unrecoverable failure when submitting data.
*
* Perhaps the client is misconfigured. Perhaps the server rejected the data.
* Attempts at performing submission again will yield the same result. So,
* the policy should not try again (until the next day).
*
* @param reason
* (string) Why the failure occurred. For logging purposes only.
*/
onSubmissionFailureHard: function onSubmissionFailureHard(reason=null) {
this.state = this.SUBMISSION_FAILURE_HARD;
this.reason = reason;
this.promise.resolve(this);
return this.promise.promise;
},
/**
* The request was aborted because an upload was already in progress.
*/
onUploadInProgress: function (reason=null) {
this.state = this.UPLOAD_IN_PROGRESS;
this.reason = reason;
this.promise.resolve(this);
return this.promise.promise;
},
});
/**
* Manages scheduling of Firefox Health Report data submission.
*
* The rules of data submission are as follows:
*
* 1. Do not submit data more than once every 24 hours.
* 2. Try to submit as close to 24 hours apart as possible.
* 3. Do not submit too soon after application startup so as to not negatively
* impact performance at startup.
* 4. Before first ever data submission, the user should be notified about
* data collection practices.
* 5. User should have opportunity to react to this notification before
* data submission.
* 6. If data submission fails, try at most 2 additional times before giving
* up on that day's submission.
*
* The listener passed into the instance must have the following properties
* (which are callbacks that will be invoked at certain key events):
*
* * onRequestDataUpload(request) - Called when the policy is requesting
* data to be submitted. The function is passed a `DataSubmissionRequest`.
* The listener should call one of the special resolving functions on that
* instance (see the documentation for that type).
*
* * onRequestRemoteDelete(request) - Called when the policy is requesting
* deletion of remotely stored data. The function is passed a
* `DataSubmissionRequest`. The listener should call one of the special
* resolving functions on that instance (just like `onRequestDataUpload`).
*
* * onNotifyDataPolicy(request) - Called when the policy is requesting the
* user to be notified that data submission will occur. The function
* receives a `NotifyPolicyRequest` instance. The callee should call one or
* more of the functions on that instance when specific events occur. See
* the documentation for that type for more.
*
* Note that the notification method is abstracted. Different applications
* can have different mechanisms by which they notify the user of data
* submission practices.
*
* @param policyPrefs
* (Preferences) Handle on preferences branch on which state will be
* queried and stored.
* @param healthReportPrefs
* (Preferences) Handle on preferences branch holding Health Report state.
* @param listener
* (object) Object with callbacks that will be invoked at certain key
* events.
*/
this.DataReportingPolicy = function (prefs, healthReportPrefs, listener) {
this._log = Log.repository.getLogger("Services.DataReporting.Policy");
this._log.level = Log.Level["Debug"];
for (let handler of this.REQUIRED_LISTENERS) {
if (!listener[handler]) {
throw new Error("Passed listener does not contain required handler: " +
handler);
}
}
this._prefs = prefs;
this._healthReportPrefs = healthReportPrefs;
this._listener = listener;
this._userNotifyPromise = null;
this._migratePrefs();
if (!this.firstRunDate.getTime()) {
// If we've never run before, record the current time.
this.firstRunDate = this.now();
}
// Install an observer so that we can act on changes from external
// code (such as Android UI).
// Use a function because this is the only place where the Preferences
// abstraction is way less usable than nsIPrefBranch.
//
// Hang on to the observer here so that tests can reach it.
this.uploadEnabledObserver = function onUploadEnabledChanged() {
if (this.pendingDeleteRemoteData || this.healthReportUploadEnabled) {
// Nothing to do: either we're already deleting because the caller
// came through the front door (rHRUE), or they set the flag to true.
return;
}
this._log.info("uploadEnabled pref changed. Scheduling deletion.");
this.deleteRemoteData();
}.bind(this);
healthReportPrefs.observe("uploadEnabled", this.uploadEnabledObserver);
// Ensure we are scheduled to submit.
if (!this.nextDataSubmissionDate.getTime()) {
this.nextDataSubmissionDate = this._futureDate(MILLISECONDS_PER_DAY);
}
// Record when we last requested for submitted data to be sent. This is
// to avoid having multiple outstanding requests.
this._inProgressSubmissionRequest = null;
};
this.DataReportingPolicy.prototype = Object.freeze({
/**
* How often to poll to see if we need to do something.
*
* The interval needs to be short enough such that short-lived applications
* have an opportunity to submit data. But, it also needs to be long enough
* to not negatively impact performance.
*
* The random bit is to ensure that other systems scheduling around the same
* interval don't all get scheduled together.
*/
POLL_INTERVAL_MSEC: (60 * 1000) + Math.floor(2.5 * 1000 * Math.random()),
/**
* How long individual data submission requests live before expiring.
*
* Data submission requests have this long to complete before we give up on
* them and try again.
*
* We want this to be short enough that we retry frequently enough but long
* enough to give slow networks and systems time to handle it.
*/
SUBMISSION_REQUEST_EXPIRE_INTERVAL_MSEC: 10 * 60 * 1000,
/**
* Our backoff schedule in case of submission failure.
*
* This dictates both the number of times we retry a daily submission and
* when to retry after each failure.
*
* Each element represents how long to wait after each recoverable failure.
* After the first failure, we wait the time in element 0 before trying
* again. After the second failure, we wait the time in element 1. Once
* we run out of values in this array, we give up on that day's submission
* and schedule for a day out.
*/
FAILURE_BACKOFF_INTERVALS: [
15 * 60 * 1000,
60 * 60 * 1000,
],
REQUIRED_LISTENERS: [
"onRequestDataUpload",
"onRequestRemoteDelete",
"onNotifyDataPolicy",
],
/**
* The first time the health report policy came into existence.
*
* This is used for scheduling of the initial submission.
*/
get firstRunDate() {
return CommonUtils.getDatePref(this._prefs, "firstRunTime", 0, this._log,
OLDEST_ALLOWED_YEAR);
},
set firstRunDate(value) {
this._log.debug("Setting first-run date: " + value);
CommonUtils.setDatePref(this._prefs, "firstRunTime", value,
OLDEST_ALLOWED_YEAR);
},
get dataSubmissionPolicyNotifiedDate() {
return CommonUtils.getDatePref(this._prefs,
"dataSubmissionPolicyNotifiedTime", 0,
this._log, OLDEST_ALLOWED_YEAR);
},
set dataSubmissionPolicyNotifiedDate(value) {
this._log.debug("Setting user notified date: " + value);
CommonUtils.setDatePref(this._prefs, "dataSubmissionPolicyNotifiedTime",
value, OLDEST_ALLOWED_YEAR);
},
get dataSubmissionPolicyBypassNotification() {
return this._prefs.get("dataSubmissionPolicyBypassNotification", false);
},
set dataSubmissionPolicyBypassNotification(value) {
return this._prefs.set("dataSubmissionPolicyBypassNotification", !!value);
},
/**
* Whether submission of data is allowed.
*
* This is the master switch for remote server communication. If it is
* false, we never request upload or deletion.
*/
get dataSubmissionEnabled() {
// Default is true because we are opt-out.
return this._prefs.get("dataSubmissionEnabled", true);
},
set dataSubmissionEnabled(value) {
this._prefs.set("dataSubmissionEnabled", !!value);
},
/**
* Whether submission of data is allowed for v2.
*
* This is used to gently turn off data submission for FHR v2 in Firefox 42+.
*/
get dataSubmissionEnabledV2() {
// Default is true because we are opt-out.
return this._prefs.get("dataSubmissionEnabled.v2", true);
},
get currentPolicyVersion() {
return this._prefs.get("currentPolicyVersion", DATAREPORTING_POLICY_VERSION);
},
/**
* The minimum policy version which for dataSubmissionPolicyAccepted to
* to be valid.
*/
get minimumPolicyVersion() {
// First check if the current channel has an ove
let channel = UpdateUtils.getUpdateChannel(false);
let channelPref = this._prefs.get("minimumPolicyVersion.channel-" + channel);
return channelPref !== undefined ?
channelPref : this._prefs.get("minimumPolicyVersion", 1);
},
get dataSubmissionPolicyAcceptedVersion() {
return this._prefs.get("dataSubmissionPolicyAcceptedVersion", 0);
},
set dataSubmissionPolicyAcceptedVersion(value) {
this._prefs.set("dataSubmissionPolicyAcceptedVersion", value);
},
/**
* Checks to see if the user has been notified about data submission
* @return {bool}
*/
get userNotifiedOfCurrentPolicy() {
return this.dataSubmissionPolicyNotifiedDate.getTime() > 0 &&
this.dataSubmissionPolicyAcceptedVersion >= this.currentPolicyVersion;
},
/**
* When this policy last requested data submission.
*
* This is used mainly for forensics purposes and should have no bearing
* on scheduling or run-time behavior.
*/
get lastDataSubmissionRequestedDate() {
return CommonUtils.getDatePref(this._healthReportPrefs,
"lastDataSubmissionRequestedTime", 0,
this._log, OLDEST_ALLOWED_YEAR);
},
set lastDataSubmissionRequestedDate(value) {
CommonUtils.setDatePref(this._healthReportPrefs,
"lastDataSubmissionRequestedTime",
value, OLDEST_ALLOWED_YEAR);
},
/**
* When the last data submission actually occurred.
*
* This is used mainly for forensics purposes and should have no bearing on
* actual scheduling.
*/
get lastDataSubmissionSuccessfulDate() {
return CommonUtils.getDatePref(this._healthReportPrefs,
"lastDataSubmissionSuccessfulTime", 0,
this._log, OLDEST_ALLOWED_YEAR);
},
set lastDataSubmissionSuccessfulDate(value) {
CommonUtils.setDatePref(this._healthReportPrefs,
"lastDataSubmissionSuccessfulTime",
value, OLDEST_ALLOWED_YEAR);
},
/**
* When we last encountered a submission failure.
*
* This is used for forensics purposes and should have no bearing on
* scheduling.
*/
get lastDataSubmissionFailureDate() {
return CommonUtils.getDatePref(this._healthReportPrefs,
"lastDataSubmissionFailureTime",
0, this._log, OLDEST_ALLOWED_YEAR);
},
set lastDataSubmissionFailureDate(value) {
CommonUtils.setDatePref(this._healthReportPrefs,
"lastDataSubmissionFailureTime",
value, OLDEST_ALLOWED_YEAR);
},
/**
* When the next data submission is scheduled to occur.
*
* This is maintained internally by this type. External users should not
* mutate this value.
*/
get nextDataSubmissionDate() {
return CommonUtils.getDatePref(this._healthReportPrefs,
"nextDataSubmissionTime", 0,
this._log, OLDEST_ALLOWED_YEAR);
},
set nextDataSubmissionDate(value) {
CommonUtils.setDatePref(this._healthReportPrefs,
"nextDataSubmissionTime", value,
OLDEST_ALLOWED_YEAR);
},
/**
* The number of submission failures for this day's upload.
*
* This is used to drive backoff and scheduling.
*/
get currentDaySubmissionFailureCount() {
let v = this._healthReportPrefs.get("currentDaySubmissionFailureCount", 0);
if (!Number.isInteger(v)) {
v = 0;
}
return v;
},
set currentDaySubmissionFailureCount(value) {
if (!Number.isInteger(value)) {
throw new Error("Value must be integer: " + value);
}
this._healthReportPrefs.set("currentDaySubmissionFailureCount", value);
},
/**
* Whether a request to delete remote data is awaiting completion.
*
* If this is true, the policy will request that remote data be deleted.
* Furthermore, no new data will be uploaded (if it's even allowed) until
* the remote deletion is fulfilled.
*/
get pendingDeleteRemoteData() {
return !!this._healthReportPrefs.get("pendingDeleteRemoteData", false);
},
set pendingDeleteRemoteData(value) {
this._healthReportPrefs.set("pendingDeleteRemoteData", !!value);
},
/**
* Whether upload of Firefox Health Report data is enabled.
*/
get healthReportUploadEnabled() {
return !!this._healthReportPrefs.get("uploadEnabled", true);
},
// External callers should update this via `recordHealthReportUploadEnabled`
// to ensure appropriate side-effects are performed.
set healthReportUploadEnabled(value) {
this._healthReportPrefs.set("uploadEnabled", !!value);
},
/**
* Whether the FHR upload enabled setting is locked and can't be changed.
*/
get healthReportUploadLocked() {
return this._healthReportPrefs.locked("uploadEnabled");
},
/**
* Record the user's intent for whether FHR should upload data.
*
* This is the preferred way for XUL applications to record a user's
* preference on whether Firefox Health Report should upload data to
* a server.
*
* If upload is disabled through this API, a request for remote data
* deletion is initiated automatically.
*
* If upload is being disabled and this operation is scheduled to
* occur immediately, a promise will be returned. This promise will be
* fulfilled when the deletion attempt finishes. If upload is being
* disabled and a promise is not returned, callers must poll
* `haveRemoteData` on the HealthReporter instance to see if remote
* data has been deleted.
*
* @param flag
* (bool) Whether data submission is enabled or disabled.
* @param reason
* (string) Why this value is being adjusted. For logging
* purposes only.
*/
recordHealthReportUploadEnabled: function (flag, reason="no-reason") {
let result = null;
if (!flag) {
result = this.deleteRemoteData(reason);
}
this.healthReportUploadEnabled = flag;
return result;
},
/**
* Request that remote data be deleted.
*
* This will record an intent that previously uploaded data is to be deleted.
* The policy will eventually issue a request to the listener for data
* deletion. It will keep asking for deletion until the listener acknowledges
* that data has been deleted.
*/
deleteRemoteData: function deleteRemoteData(reason="no-reason") {
this._log.info("Remote data deletion requested: " + reason);
this.pendingDeleteRemoteData = true;
// 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();
return this.checkStateAndTrigger();
},
/**
* Start background polling for activity.
*
* This will set up a recurring timer that will periodically check if
* activity is warranted.
*
* You typically call this function for each constructed instance.
*/
startPolling: function startPolling() {
this.stopPolling();
this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
this._timer.initWithCallback({
notify: function notify() {
this.checkStateAndTrigger();
}.bind(this)
}, this.POLL_INTERVAL_MSEC, this._timer.TYPE_REPEATING_SLACK);
},
/**
* Stop background polling for activity.
*
* This should be called when the instance is no longer needed.
*/
stopPolling: function stopPolling() {
if (this._timer) {
this._timer.cancel();
this._timer = null;
}
},
/**
* Abstraction for obtaining current time.
*
* The purpose of this is to facilitate testing. Testing code can monkeypatch
* this on instances instead of modifying the singleton Date object.
*/
now: function now() {
return new Date();
},
/**
* Check state and trigger actions, if necessary.
*
* This is what enforces the submission and notification policy detailed
* above. You can think of this as the driver for health report data
* submission.
*
* Typically this function is called automatically by the background polling.
* But, it can safely be called manually as needed.
*/
checkStateAndTrigger: function checkStateAndTrigger() {
// If the master data submission kill switch is toggled, we have nothing
// to do. We don't notify about data policies because this would have
// no effect.
if (!this.dataSubmissionEnabled || !this.dataSubmissionEnabledV2) {
this._log.debug("Data submission is disabled. Doing nothing.");
return;
}
let now = this.now();
let nowT = now.getTime();
let nextSubmissionDate = this.nextDataSubmissionDate;
// If the system clock were ever set to a time in the distant future,
// it's possible our next schedule date is far out as well. We know
// we shouldn't schedule for more than a day out, so we reset the next
// scheduled date appropriately. 3 days was chosen arbitrarily.
if (nextSubmissionDate.getTime() >= nowT + 3 * MILLISECONDS_PER_DAY) {
this._log.warn("Next data submission time is far away. Was the system " +
"clock recently readjusted? " + nextSubmissionDate);
// It shouldn't really matter what we set this to. 1 day in the future
// should be pretty safe.
this._moveScheduleForward24h();
// Fall through since we may have other actions.
}
// Tend to any in progress work.
if (this._processInProgressSubmission()) {
return;
}
// Requests to delete remote data take priority above everything else.
if (this.pendingDeleteRemoteData) {
if (nowT < nextSubmissionDate.getTime()) {
this._log.debug("Deletion request is scheduled for the future: " +
nextSubmissionDate);
return;
}
return this._dispatchSubmissionRequest("onRequestRemoteDelete", true);
}
if (!this.healthReportUploadEnabled) {
this._log.debug("Data upload is disabled. Doing nothing.");
return;
}
if (!this.ensureUserNotified()) {
this._log.warn("The user has not been notified about the data submission " +
"policy. Not attempting upload.");
return;
}
// Data submission is allowed to occur. Now comes the scheduling part.
if (nowT < nextSubmissionDate.getTime()) {
this._log.debug("Next data submission is scheduled in the future: " +
nextSubmissionDate);
return;
}
return this._dispatchSubmissionRequest("onRequestDataUpload", false);
},
/**
* Ensure that the data policy notification has been displayed.
*
* This must be called before data submission. If the policy has not been
* displayed, data submission must not occur.
*
* @return bool Whether the notification has been displayed.
*/
ensureUserNotified: function () {
if (this.userNotifiedOfCurrentPolicy || this.dataSubmissionPolicyBypassNotification) {
return true;
}
// The user has not been notified yet, but is in the process of being notified.
if (this._userNotifyPromise) {
return false;
}
let deferred = Promise.defer();
deferred.promise.then((function onSuccess() {
this._recordDataPolicyNotification(this.now(), this.currentPolicyVersion);
this._userNotifyPromise = null;
}).bind(this), ((error) => {
this._log.warn("Data policy notification presentation failed", error);
this._userNotifyPromise = null;
}).bind(this));
this._log.info("Requesting display of data policy.");
let request = new NotifyPolicyRequest(this, deferred);
try {
this._listener.onNotifyDataPolicy(request);
} catch (ex) {
this._log.warn("Exception when calling onNotifyDataPolicy", ex);
}
this._userNotifyPromise = deferred.promise;
return false;
},
_recordDataPolicyNotification: function (date, version) {
this._log.debug("Recording data policy notification to version " + version +
" on date " + date);
this.dataSubmissionPolicyNotifiedDate = date;
this.dataSubmissionPolicyAcceptedVersion = version;
},
_migratePrefs: function () {
// Current prefs are mostly the same than the old ones, except for some deprecated ones.
this._prefs.reset([
"dataSubmissionPolicyAccepted",
"dataSubmissionPolicyBypassAcceptance",
"dataSubmissionPolicyResponseType",
"dataSubmissionPolicyResponseTime"
]);
},
_processInProgressSubmission: function _processInProgressSubmission() {
if (!this._inProgressSubmissionRequest) {
return false;
}
let now = this.now().getTime();
if (this._inProgressSubmissionRequest.expiresDate.getTime() > now) {
this._log.info("Waiting on in-progress submission request to finish.");
return true;
}
this._log.warn("Old submission request has expired from no activity.");
this._inProgressSubmissionRequest.promise.reject(new Error("Request has expired."));
this._inProgressSubmissionRequest = null;
this._handleSubmissionFailure();
return false;
},
_dispatchSubmissionRequest: function _dispatchSubmissionRequest(handler, isDelete) {
let now = this.now();
// We're past our scheduled next data submission date, so let's do it!
this.lastDataSubmissionRequestedDate = now;
let deferred = Promise.defer();
let requestExpiresDate =
this._futureDate(this.SUBMISSION_REQUEST_EXPIRE_INTERVAL_MSEC);
this._inProgressSubmissionRequest = new DataSubmissionRequest(deferred,
requestExpiresDate,
isDelete);
let onSuccess = function onSuccess(result) {
this._inProgressSubmissionRequest = null;
this._handleSubmissionResult(result);
}.bind(this);
let onError = function onError(error) {
this._log.error("Error when handling data submission result", error);
this._inProgressSubmissionRequest = null;
this._handleSubmissionFailure();
}.bind(this);
let chained = deferred.promise.then(onSuccess, onError);
this._log.info("Requesting data submission. Will expire at " +
requestExpiresDate);
try {
let promise = this._listener[handler](this._inProgressSubmissionRequest);
chained = chained.then(() => promise, null);
} catch (ex) {
this._log.warn("Exception when calling " + handler, ex);
this._inProgressSubmissionRequest = null;
this._handleSubmissionFailure();
return;
}
return chained;
},
_handleSubmissionResult: function _handleSubmissionResult(request) {
let state = request.state;
let reason = request.reason || "no reason";
this._log.info("Got submission request result: " + state);
if (state == request.SUBMISSION_SUCCESS) {
if (request.isDelete) {
this.pendingDeleteRemoteData = false;
this._log.info("Successful data delete reported.");
} else {
this._log.info("Successful data upload reported.");
}
this.lastDataSubmissionSuccessfulDate = request.submissionDate;
let nextSubmissionDate =
new Date(request.submissionDate.getTime() + MILLISECONDS_PER_DAY);
// Schedule pending deletes immediately. This has potential to overload
// the server. However, the frequency of delete requests across all
// clients should be low, so this shouldn't pose a problem.
if (this.pendingDeleteRemoteData) {
nextSubmissionDate = this.now();
}
this.nextDataSubmissionDate = nextSubmissionDate;
this.currentDaySubmissionFailureCount = 0;
return;
}
if (state == request.NO_DATA_AVAILABLE) {
if (request.isDelete) {
this._log.info("Remote data delete requested but no remote data was stored.");
this.pendingDeleteRemoteData = false;
return;
}
this._log.info("No data was available to submit. May try later.");
this._handleSubmissionFailure();
return;
}
// We don't special case request.isDelete for these failures because it
// likely means there was a server error.
if (state == request.SUBMISSION_FAILURE_SOFT) {
this._log.warn("Soft error submitting data: " + reason);
this.lastDataSubmissionFailureDate = this.now();
this._handleSubmissionFailure();
return;
}
if (state == request.SUBMISSION_FAILURE_HARD) {
this._log.warn("Hard error submitting data: " + reason);
this.lastDataSubmissionFailureDate = this.now();
this._moveScheduleForward24h();
return;
}
throw new Error("Unknown state on DataSubmissionRequest: " + request.state);
},
_handleSubmissionFailure: function _handleSubmissionFailure() {
if (this.currentDaySubmissionFailureCount >= this.FAILURE_BACKOFF_INTERVALS.length) {
this._log.warn("Reached the limit of daily submission attempts. " +
"Rescheduling for tomorrow.");
this._moveScheduleForward24h();
return false;
}
let offset = this.FAILURE_BACKOFF_INTERVALS[this.currentDaySubmissionFailureCount];
this.nextDataSubmissionDate = this._futureDate(offset);
this.currentDaySubmissionFailureCount++;
return true;
},
_moveScheduleForward24h: function _moveScheduleForward24h() {
let d = this._futureDate(MILLISECONDS_PER_DAY);
this._log.info("Setting next scheduled data submission for " + d);
this.nextDataSubmissionDate = d;
this.currentDaySubmissionFailureCount = 0;
},
_futureDate: function _futureDate(offset) {
return new Date(this.now().getTime() + offset);
},
});

View File

@ -0,0 +1,16 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// We need to initialize the profile or OS.File may not work. See bug 810543.
do_get_profile();
(function initTestingInfrastructure() {
let ns = {};
Components.utils.import("resource://testing-common/services/common/logging.js",
ns);
ns.initTestLogging();
}).call(this);

View File

@ -0,0 +1,689 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
var {utils: Cu} = Components;
Cu.import("resource://gre/modules/Preferences.jsm");
Cu.import("resource://gre/modules/services/datareporting/policy.jsm");
Cu.import("resource://testing-common/services/datareporting/mocks.jsm");
Cu.import("resource://gre/modules/UpdateUtils.jsm");
Cu.import("resource://gre/modules/Task.jsm");
function getPolicy(name,
aCurrentPolicyVersion = 1,
aMinimumPolicyVersion = 1,
aBranchMinimumVersionOverride) {
let branch = "testing.datareporting." + name;
// The version prefs should not be removed on reset, so set them in the
// default branch.
let defaultPolicyPrefs = new Preferences({ branch: branch + ".policy."
, defaultBranch: true });
defaultPolicyPrefs.set("currentPolicyVersion", aCurrentPolicyVersion);
defaultPolicyPrefs.set("minimumPolicyVersion", aMinimumPolicyVersion);
let branchOverridePrefName = "minimumPolicyVersion.channel-" + UpdateUtils.getUpdateChannel(false);
if (aBranchMinimumVersionOverride !== undefined)
defaultPolicyPrefs.set(branchOverridePrefName, aBranchMinimumVersionOverride);
else
defaultPolicyPrefs.reset(branchOverridePrefName);
let policyPrefs = new Preferences(branch + ".policy.");
let healthReportPrefs = new Preferences(branch + ".healthreport.");
let listener = new MockPolicyListener();
let policy = new DataReportingPolicy(policyPrefs, healthReportPrefs, listener);
return [policy, policyPrefs, healthReportPrefs, listener];
}
/**
* Ensure that the notification has been displayed to the user therefore having
* policy.ensureUserNotified() === true, which will allow for a successful
* data upload and afterwards does a call to policy.checkStateAndTrigger()
* @param {Policy} policy
* @return {Promise}
*/
function ensureUserNotifiedAndTrigger(policy) {
return Task.spawn(function* ensureUserNotifiedAndTrigger () {
policy.ensureUserNotified();
yield policy._listener.lastNotifyRequest.deferred.promise;
do_check_true(policy.userNotifiedOfCurrentPolicy);
policy.checkStateAndTrigger();
});
}
function defineNow(policy, now) {
print("Adjusting fake system clock to " + now);
Object.defineProperty(policy, "now", {
value: function customNow() {
return now;
},
writable: true,
});
}
function run_test() {
run_next_test();
}
add_test(function test_constructor() {
let policyPrefs = new Preferences("foo.bar.policy.");
let hrPrefs = new Preferences("foo.bar.healthreport.");
let listener = {
onRequestDataUpload: function() {},
onRequestRemoteDelete: function() {},
onNotifyDataPolicy: function() {},
};
let policy = new DataReportingPolicy(policyPrefs, hrPrefs, listener);
do_check_true(Date.now() - policy.firstRunDate.getTime() < 1000);
let tomorrow = Date.now() + 24 * 60 * 60 * 1000;
do_check_true(tomorrow - policy.nextDataSubmissionDate.getTime() < 1000);
do_check_eq(policy.dataSubmissionPolicyAcceptedVersion, 0);
do_check_false(policy.userNotifiedOfCurrentPolicy);
run_next_test();
});
add_test(function test_prefs() {
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("prefs");
let now = new Date();
let nowT = now.getTime();
policy.firstRunDate = now;
do_check_eq(policyPrefs.get("firstRunTime"), nowT);
do_check_eq(policy.firstRunDate.getTime(), nowT);
policy.dataSubmissionPolicyNotifiedDate = now;
do_check_eq(policyPrefs.get("dataSubmissionPolicyNotifiedTime"), nowT);
do_check_neq(policy.dataSubmissionPolicyNotifiedDate, null);
do_check_eq(policy.dataSubmissionPolicyNotifiedDate.getTime(), nowT);
policy.dataSubmissionEnabled = false;
do_check_false(policyPrefs.get("dataSubmissionEnabled", true));
do_check_false(policy.dataSubmissionEnabled);
let new_version = DATAREPORTING_POLICY_VERSION + 1;
policy.dataSubmissionPolicyAcceptedVersion = new_version;
do_check_eq(policyPrefs.get("dataSubmissionPolicyAcceptedVersion"), new_version);
do_check_false(policy.dataSubmissionPolicyBypassNotification);
policy.dataSubmissionPolicyBypassNotification = true;
do_check_true(policy.dataSubmissionPolicyBypassNotification);
do_check_true(policyPrefs.get("dataSubmissionPolicyBypassNotification"));
policy.lastDataSubmissionRequestedDate = now;
do_check_eq(hrPrefs.get("lastDataSubmissionRequestedTime"), nowT);
do_check_eq(policy.lastDataSubmissionRequestedDate.getTime(), nowT);
policy.lastDataSubmissionSuccessfulDate = now;
do_check_eq(hrPrefs.get("lastDataSubmissionSuccessfulTime"), nowT);
do_check_eq(policy.lastDataSubmissionSuccessfulDate.getTime(), nowT);
policy.lastDataSubmissionFailureDate = now;
do_check_eq(hrPrefs.get("lastDataSubmissionFailureTime"), nowT);
do_check_eq(policy.lastDataSubmissionFailureDate.getTime(), nowT);
policy.nextDataSubmissionDate = now;
do_check_eq(hrPrefs.get("nextDataSubmissionTime"), nowT);
do_check_eq(policy.nextDataSubmissionDate.getTime(), nowT);
policy.currentDaySubmissionFailureCount = 2;
do_check_eq(hrPrefs.get("currentDaySubmissionFailureCount", 0), 2);
do_check_eq(policy.currentDaySubmissionFailureCount, 2);
policy.pendingDeleteRemoteData = true;
do_check_true(hrPrefs.get("pendingDeleteRemoteData"));
do_check_true(policy.pendingDeleteRemoteData);
policy.healthReportUploadEnabled = false;
do_check_false(hrPrefs.get("uploadEnabled"));
do_check_false(policy.healthReportUploadEnabled);
do_check_false(policy.healthReportUploadLocked);
hrPrefs.lock("uploadEnabled");
do_check_true(policy.healthReportUploadLocked);
hrPrefs.unlock("uploadEnabled");
do_check_false(policy.healthReportUploadLocked);
run_next_test();
});
add_task(function test_migratePrefs () {
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("migratePrefs");
let outdated_prefs = {
dataSubmissionPolicyAccepted: true,
dataSubmissionPolicyBypassAcceptance: true,
dataSubmissionPolicyResponseType: "something",
dataSubmissionPolicyResponseTime: Date.now() + "",
};
// Test removal of old prefs.
for (let name in outdated_prefs) {
policyPrefs.set(name, outdated_prefs[name]);
}
policy._migratePrefs();
for (let name in outdated_prefs) {
do_check_false(policyPrefs.has(name));
}
});
add_task(function test_userNotifiedOfCurrentPolicy () {
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("initial_submission_notification");
do_check_false(policy.userNotifiedOfCurrentPolicy,
"The initial state should be unnotified.");
do_check_eq(policy.dataSubmissionPolicyNotifiedDate.getTime(), 0);
policy.dataSubmissionPolicyAcceptedVersion = DATAREPORTING_POLICY_VERSION;
do_check_false(policy.userNotifiedOfCurrentPolicy,
"The default state of the date should have a time of 0 and it should therefore fail");
do_check_eq(policy.dataSubmissionPolicyNotifiedDate.getTime(), 0,
"Updating the accepted version should not set a notified date.");
policy._recordDataPolicyNotification(new Date(), DATAREPORTING_POLICY_VERSION);
do_check_true(policy.userNotifiedOfCurrentPolicy,
"Using the proper API causes user notification to report as true.");
// It is assumed that later versions of the policy will incorporate previous
// ones, therefore this should also return true.
policy._recordDataPolicyNotification(new Date(), DATAREPORTING_POLICY_VERSION);
policy.dataSubmissionPolicyAcceptedVersion = DATAREPORTING_POLICY_VERSION + 1;
do_check_true(policy.userNotifiedOfCurrentPolicy, 'A future version of the policy should pass.');
policy._recordDataPolicyNotification(new Date(), DATAREPORTING_POLICY_VERSION);
policy.dataSubmissionPolicyAcceptedVersion = DATAREPORTING_POLICY_VERSION - 1;
do_check_false(policy.userNotifiedOfCurrentPolicy, 'A previous version of the policy should fail.');
});
add_task(function* test_notification_displayed () {
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("notification_accept_displayed");
do_check_eq(listener.requestDataUploadCount, 0);
do_check_eq(listener.notifyUserCount, 0);
do_check_eq(policy.dataSubmissionPolicyNotifiedDate.getTime(), 0);
// Uploads will trigger user notifications as needed.
policy.checkStateAndTrigger();
do_check_eq(listener.notifyUserCount, 1);
do_check_eq(listener.requestDataUploadCount, 0);
yield ensureUserNotifiedAndTrigger(policy);
do_check_eq(listener.notifyUserCount, 1);
do_check_true(policy.dataSubmissionPolicyNotifiedDate.getTime() > 0);
do_check_true(policy.userNotifiedOfCurrentPolicy);
});
add_task(function* test_submission_kill_switch() {
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("submission_kill_switch");
policy.nextDataSubmissionDate = new Date(Date.now() - 24 * 60 * 60 * 1000);
policy.checkStateAndTrigger();
do_check_eq(listener.requestDataUploadCount, 0);
yield ensureUserNotifiedAndTrigger(policy);
do_check_eq(listener.requestDataUploadCount, 1);
defineNow(policy,
new Date(Date.now() + policy.SUBMISSION_REQUEST_EXPIRE_INTERVAL_MSEC + 100));
policy.dataSubmissionEnabled = false;
policy.checkStateAndTrigger();
do_check_eq(listener.requestDataUploadCount, 1);
});
add_task(function* test_upload_kill_switch() {
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("upload_kill_switch");
yield ensureUserNotifiedAndTrigger(policy);
defineNow(policy, policy.nextDataSubmissionDate);
// So that we don't trigger deletions, which cause uploads to be delayed.
hrPrefs.ignore("uploadEnabled", policy.uploadEnabledObserver);
policy.healthReportUploadEnabled = false;
yield policy.checkStateAndTrigger();
do_check_eq(listener.requestDataUploadCount, 0);
policy.healthReportUploadEnabled = true;
yield ensureUserNotifiedAndTrigger(policy);
do_check_eq(listener.requestDataUploadCount, 1);
});
add_task(function* test_data_submission_no_data() {
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("data_submission_no_data");
let now = new Date(policy.nextDataSubmissionDate.getTime() + 1);
defineNow(policy, now);
do_check_eq(listener.requestDataUploadCount, 0);
yield ensureUserNotifiedAndTrigger(policy);
do_check_eq(listener.requestDataUploadCount, 1);
listener.lastDataRequest.onNoDataAvailable();
// The next trigger should try again.
defineNow(policy, new Date(now.getTime() + 155 * 60 * 1000));
policy.checkStateAndTrigger();
do_check_eq(listener.requestDataUploadCount, 2);
});
add_task(function* test_data_submission_submit_failure_hard() {
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("data_submission_submit_failure_hard");
let nextDataSubmissionDate = policy.nextDataSubmissionDate;
let now = new Date(policy.nextDataSubmissionDate.getTime() + 1);
defineNow(policy, now);
yield ensureUserNotifiedAndTrigger(policy);
do_check_eq(listener.requestDataUploadCount, 1);
yield listener.lastDataRequest.onSubmissionFailureHard();
do_check_eq(listener.lastDataRequest.state,
listener.lastDataRequest.SUBMISSION_FAILURE_HARD);
let expected = new Date(now.getTime() + 24 * 60 * 60 * 1000);
do_check_eq(policy.nextDataSubmissionDate.getTime(), expected.getTime());
defineNow(policy, new Date(now.getTime() + 10));
policy.checkStateAndTrigger();
do_check_eq(listener.requestDataUploadCount, 1);
});
add_task(function* test_data_submission_submit_try_again() {
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("data_submission_failure_soft");
let nextDataSubmissionDate = policy.nextDataSubmissionDate;
let now = new Date(policy.nextDataSubmissionDate.getTime());
defineNow(policy, now);
yield ensureUserNotifiedAndTrigger(policy);
yield listener.lastDataRequest.onSubmissionFailureSoft();
do_check_eq(policy.nextDataSubmissionDate.getTime(),
nextDataSubmissionDate.getTime() + 15 * 60 * 1000);
});
add_task(function* test_submission_daily_scheduling() {
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("submission_daily_scheduling");
let nextDataSubmissionDate = policy.nextDataSubmissionDate;
// Skip ahead to next submission date. We should get a submission request.
let now = new Date(policy.nextDataSubmissionDate.getTime());
defineNow(policy, now);
yield ensureUserNotifiedAndTrigger(policy);
do_check_eq(listener.requestDataUploadCount, 1);
do_check_eq(policy.lastDataSubmissionRequestedDate.getTime(), now.getTime());
let finishedDate = new Date(now.getTime() + 250);
defineNow(policy, new Date(finishedDate.getTime() + 50));
yield listener.lastDataRequest.onSubmissionSuccess(finishedDate);
do_check_eq(policy.lastDataSubmissionSuccessfulDate.getTime(), finishedDate.getTime());
// Next scheduled submission should be exactly 1 day after the reported
// submission success.
let nextScheduled = new Date(finishedDate.getTime() + 24 * 60 * 60 * 1000);
do_check_eq(policy.nextDataSubmissionDate.getTime(), nextScheduled.getTime());
// Fast forward some arbitrary time. We shouldn't do any work yet.
defineNow(policy, new Date(now.getTime() + 40000));
policy.checkStateAndTrigger();
do_check_eq(listener.requestDataUploadCount, 1);
defineNow(policy, nextScheduled);
policy.checkStateAndTrigger();
do_check_eq(listener.requestDataUploadCount, 2);
yield listener.lastDataRequest.onSubmissionSuccess(new Date(nextScheduled.getTime() + 200));
do_check_eq(policy.nextDataSubmissionDate.getTime(),
new Date(nextScheduled.getTime() + 24 * 60 * 60 * 1000 + 200).getTime());
});
add_task(function* test_submission_far_future_scheduling() {
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("submission_far_future_scheduling");
let now = new Date(Date.now() - 24 * 60 * 60 * 1000);
defineNow(policy, now);
yield ensureUserNotifiedAndTrigger(policy);
let nextDate = policy._futureDate(3 * 24 * 60 * 60 * 1000 - 1);
policy.nextDataSubmissionDate = nextDate;
policy.checkStateAndTrigger();
do_check_true(policy.dataSubmissionPolicyAcceptedVersion >= DATAREPORTING_POLICY_VERSION);
do_check_eq(listener.requestDataUploadCount, 0);
do_check_eq(policy.nextDataSubmissionDate.getTime(), nextDate.getTime());
policy.nextDataSubmissionDate = new Date(nextDate.getTime() + 1);
policy.checkStateAndTrigger();
do_check_eq(listener.requestDataUploadCount, 0);
do_check_eq(policy.nextDataSubmissionDate.getTime(),
policy._futureDate(24 * 60 * 60 * 1000).getTime());
});
add_task(function* test_submission_backoff() {
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("submission_backoff");
do_check_eq(policy.FAILURE_BACKOFF_INTERVALS.length, 2);
let now = new Date(policy.nextDataSubmissionDate.getTime());
defineNow(policy, now);
yield ensureUserNotifiedAndTrigger(policy);
do_check_eq(listener.requestDataUploadCount, 1);
do_check_eq(policy.currentDaySubmissionFailureCount, 0);
now = new Date(now.getTime() + 5000);
defineNow(policy, now);
// On first soft failure we should back off by scheduled interval.
yield listener.lastDataRequest.onSubmissionFailureSoft();
do_check_eq(policy.currentDaySubmissionFailureCount, 1);
do_check_eq(policy.nextDataSubmissionDate.getTime(),
new Date(now.getTime() + policy.FAILURE_BACKOFF_INTERVALS[0]).getTime());
do_check_eq(policy.lastDataSubmissionFailureDate.getTime(), now.getTime());
// Should not request submission until scheduled.
now = new Date(policy.nextDataSubmissionDate.getTime() - 1);
defineNow(policy, now);
policy.checkStateAndTrigger();
do_check_eq(listener.requestDataUploadCount, 1);
// 2nd request for submission.
now = new Date(policy.nextDataSubmissionDate.getTime());
defineNow(policy, now);
policy.checkStateAndTrigger();
do_check_eq(listener.requestDataUploadCount, 2);
now = new Date(now.getTime() + 5000);
defineNow(policy, now);
// On second failure we should back off by more.
yield listener.lastDataRequest.onSubmissionFailureSoft();
do_check_eq(policy.currentDaySubmissionFailureCount, 2);
do_check_eq(policy.nextDataSubmissionDate.getTime(),
new Date(now.getTime() + policy.FAILURE_BACKOFF_INTERVALS[1]).getTime());
now = new Date(policy.nextDataSubmissionDate.getTime());
defineNow(policy, now);
policy.checkStateAndTrigger();
do_check_eq(listener.requestDataUploadCount, 3);
now = new Date(now.getTime() + 5000);
defineNow(policy, now);
// On 3rd failure we should back off by a whole day.
yield listener.lastDataRequest.onSubmissionFailureSoft();
do_check_eq(policy.currentDaySubmissionFailureCount, 0);
do_check_eq(policy.nextDataSubmissionDate.getTime(),
new Date(now.getTime() + 24 * 60 * 60 * 1000).getTime());
});
// Ensure that only one submission request can be active at a time.
add_task(function* test_submission_expiring() {
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("submission_expiring");
let nextDataSubmission = policy.nextDataSubmissionDate;
let now = new Date(policy.nextDataSubmissionDate.getTime());
defineNow(policy, now);
yield ensureUserNotifiedAndTrigger(policy);
do_check_eq(listener.requestDataUploadCount, 1);
defineNow(policy, new Date(now.getTime() + 500));
policy.checkStateAndTrigger();
do_check_eq(listener.requestDataUploadCount, 1);
defineNow(policy, new Date(policy.now().getTime() +
policy.SUBMISSION_REQUEST_EXPIRE_INTERVAL_MSEC));
policy.checkStateAndTrigger();
do_check_eq(listener.requestDataUploadCount, 2);
});
add_task(function* test_delete_remote_data() {
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("delete_remote_data");
do_check_false(policy.pendingDeleteRemoteData);
let nextSubmissionDate = policy.nextDataSubmissionDate;
let now = new Date();
defineNow(policy, now);
policy.deleteRemoteData();
do_check_true(policy.pendingDeleteRemoteData);
do_check_neq(nextSubmissionDate.getTime(),
policy.nextDataSubmissionDate.getTime());
do_check_eq(now.getTime(), policy.nextDataSubmissionDate.getTime());
do_check_eq(listener.requestRemoteDeleteCount, 1);
do_check_true(listener.lastRemoteDeleteRequest.isDelete);
defineNow(policy, policy._futureDate(1000));
yield listener.lastRemoteDeleteRequest.onSubmissionSuccess(policy.now());
do_check_false(policy.pendingDeleteRemoteData);
});
// Ensure that deletion requests take priority over regular data submission.
add_task(function* test_delete_remote_data_priority() {
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("delete_remote_data_priority");
let now = new Date();
defineNow(policy, new Date(now.getTime() + 3 * 24 * 60 * 60 * 1000));
yield ensureUserNotifiedAndTrigger(policy);
do_check_eq(listener.requestDataUploadCount, 1);
policy._inProgressSubmissionRequest = null;
policy.deleteRemoteData();
policy.checkStateAndTrigger();
do_check_eq(listener.requestRemoteDeleteCount, 1);
do_check_eq(listener.requestDataUploadCount, 1);
});
add_test(function test_delete_remote_data_backoff() {
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("delete_remote_data_backoff");
let now = new Date();
defineNow(policy, now);
policy.nextDataSubmissionDate = now;
policy.deleteRemoteData();
policy.checkStateAndTrigger();
do_check_eq(listener.requestRemoteDeleteCount, 1);
defineNow(policy, policy._futureDate(1000));
policy.checkStateAndTrigger();
do_check_eq(listener.requestDataUploadCount, 0);
do_check_eq(listener.requestRemoteDeleteCount, 1);
defineNow(policy, policy._futureDate(500));
listener.lastRemoteDeleteRequest.onSubmissionFailureSoft();
defineNow(policy, policy._futureDate(50));
policy.checkStateAndTrigger();
do_check_eq(listener.requestRemoteDeleteCount, 1);
defineNow(policy, policy._futureDate(policy.FAILURE_BACKOFF_INTERVALS[0] - 50));
policy.checkStateAndTrigger();
do_check_eq(listener.requestRemoteDeleteCount, 2);
run_next_test();
});
// If we request delete while an upload is in progress, delete should be
// scheduled immediately after upload.
add_task(function* test_delete_remote_data_in_progress_upload() {
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("delete_remote_data_in_progress_upload");
defineNow(policy, policy.nextDataSubmissionDate);
yield ensureUserNotifiedAndTrigger(policy);
do_check_eq(listener.requestDataUploadCount, 1);
defineNow(policy, policy._futureDate(50 * 1000));
// If we request a delete during a pending request, nothing should be done.
policy.deleteRemoteData();
policy.checkStateAndTrigger();
do_check_eq(listener.requestDataUploadCount, 1);
do_check_eq(listener.requestRemoteDeleteCount, 0);
// Now wait a little bit and finish the request.
defineNow(policy, policy._futureDate(10 * 1000));
yield listener.lastDataRequest.onSubmissionSuccess(policy._futureDate(1000));
defineNow(policy, policy._futureDate(5000));
policy.checkStateAndTrigger();
do_check_eq(listener.requestDataUploadCount, 1);
do_check_eq(listener.requestRemoteDeleteCount, 1);
});
add_test(function test_polling() {
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("polling");
let intended = 500;
let acceptable = 250; // Because nsITimer doesn't guarantee times.
// Ensure checkStateAndTrigger is called at a regular interval.
let then = Date.now();
print("Starting run: " + then);
Object.defineProperty(policy, "POLL_INTERVAL_MSEC", {
value: intended,
});
let count = 0;
Object.defineProperty(policy, "checkStateAndTrigger", {
value: function fakeCheckStateAndTrigger() {
let now = Date.now();
let after = now - then;
count++;
print("Polled at " + now + " after " + after + "ms, intended " + intended);
do_check_true(after >= acceptable);
DataReportingPolicy.prototype.checkStateAndTrigger.call(policy);
if (count >= 2) {
policy.stopPolling();
do_check_eq(listener.requestDataUploadCount, 0);
run_next_test();
}
// "Specified timer period will be at least the time between when
// processing for last firing the callback completes and when the next
// firing occurs."
//
// That means we should set 'then' at the *end* of our handler, not
// earlier.
then = Date.now();
}
});
policy.startPolling();
});
add_task(function* test_record_health_report_upload_enabled() {
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("record_health_report_upload_enabled");
// Preconditions.
do_check_false(policy.pendingDeleteRemoteData);
do_check_true(policy.healthReportUploadEnabled);
do_check_eq(listener.requestRemoteDeleteCount, 0);
// User intent to disable should immediately result in a pending
// delete request.
policy.recordHealthReportUploadEnabled(false, "testing 1 2 3");
do_check_false(policy.healthReportUploadEnabled);
do_check_true(policy.pendingDeleteRemoteData);
do_check_eq(listener.requestRemoteDeleteCount, 1);
// Fulfilling it should make it go away.
yield listener.lastRemoteDeleteRequest.onNoDataAvailable();
do_check_false(policy.pendingDeleteRemoteData);
// User intent to enable should get us back to default state.
policy.recordHealthReportUploadEnabled(true, "testing 1 2 3");
do_check_false(policy.pendingDeleteRemoteData);
do_check_true(policy.healthReportUploadEnabled);
});
add_test(function test_pref_change_initiates_deletion() {
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("record_health_report_upload_enabled");
// Preconditions.
do_check_false(policy.pendingDeleteRemoteData);
do_check_true(policy.healthReportUploadEnabled);
do_check_eq(listener.requestRemoteDeleteCount, 0);
// User intent to disable should indirectly result in a pending
// delete request, because the policy is watching for the pref
// to change.
Object.defineProperty(policy, "deleteRemoteData", {
value: function deleteRemoteDataProxy() {
do_check_false(policy.healthReportUploadEnabled);
do_check_false(policy.pendingDeleteRemoteData); // Just called.
run_next_test();
},
});
hrPrefs.set("uploadEnabled", false);
});
add_task(function* test_policy_version() {
let policy, policyPrefs, hrPrefs, listener, now, firstRunTime;
function createPolicy(shouldBeNotified = false,
currentPolicyVersion = 1, minimumPolicyVersion = 1,
branchMinimumVersionOverride) {
[policy, policyPrefs, hrPrefs, listener] =
getPolicy("policy_version_test", currentPolicyVersion,
minimumPolicyVersion, branchMinimumVersionOverride);
let firstRun = now === undefined;
if (firstRun) {
firstRunTime = policy.firstRunDate.getTime();
do_check_true(firstRunTime > 0);
now = new Date(policy.firstRunDate.getTime());
}
else {
// The first-run time should not be reset even after policy-version
// upgrades.
do_check_eq(policy.firstRunDate.getTime(), firstRunTime);
}
defineNow(policy, now);
do_check_eq(policy.userNotifiedOfCurrentPolicy, shouldBeNotified);
}
function* triggerPolicyCheckAndEnsureNotified(notified = true) {
policy.checkStateAndTrigger();
do_check_eq(listener.notifyUserCount, Number(notified));
if (notified) {
policy.ensureUserNotified();
yield listener.lastNotifyRequest.deferred.promise;
do_check_true(policy.userNotifiedOfCurrentPolicy);
do_check_eq(policyPrefs.get("dataSubmissionPolicyAcceptedVersion"),
policyPrefs.get("currentPolicyVersion"));
}
}
createPolicy();
yield triggerPolicyCheckAndEnsureNotified();
// We shouldn't be notified again if the current version is still valid;
createPolicy(true);
yield triggerPolicyCheckAndEnsureNotified(false);
// Just increasing the current version isn't enough. The minimum
// version must be changed.
let currentPolicyVersion = policyPrefs.get("currentPolicyVersion");
let minimumPolicyVersion = policyPrefs.get("minimumPolicyVersion");
createPolicy(false, ++currentPolicyVersion, minimumPolicyVersion);
yield triggerPolicyCheckAndEnsureNotified(true);
do_check_eq(policyPrefs.get("dataSubmissionPolicyAcceptedVersion"), currentPolicyVersion);
// Increase the minimum policy version and check if we're notified.
createPolicy(true, currentPolicyVersion, ++minimumPolicyVersion);
do_check_true(policyPrefs.has("dataSubmissionPolicyAcceptedVersion"));
yield triggerPolicyCheckAndEnsureNotified(false);
// Test increasing the minimum version just on the current channel.
createPolicy(true, currentPolicyVersion, minimumPolicyVersion);
yield triggerPolicyCheckAndEnsureNotified(false);
createPolicy(false, ++currentPolicyVersion, minimumPolicyVersion, minimumPolicyVersion + 1);
yield triggerPolicyCheckAndEnsureNotified(true);
});

View File

@ -0,0 +1,6 @@
[DEFAULT]
head = head.js
tail =
skip-if = toolkit == 'android' || toolkit == 'gonk'
[test_policy.js]

View File

@ -0,0 +1,28 @@
.. _data_reporting_service:
======================
Data Reporting Service
======================
``/services/datareporting`` contains files related to an XPCOM service
that collects and reports data within Gecko applications.
The important files in this directory are:
DataReportingService.js
An XPCOM service that coordinates collection and reporting of data.
policy.jsm
A module containing the logic for coordinating and driving collection
and upload of data.
sessions.jsm
Records Gecko application session history. This is loaded as part of
the XPCOM service because it needs to capture state from very early in
the application lifecycle. Bug 841561 tracks implementing this in C++.
There is other code in the tree that collects and uploads data. The
original intent of this directory and XPCOM service was to serve as a
focal point for the coordination of all this activity so that it could
all be done consistently and properly. This vision may or may not be fully
realized.

17
services/docs/index.rst Normal file
View File

@ -0,0 +1,17 @@
=======================
Firefox Services Module
=======================
The ``/services`` directory contains code for a variety of application
features that communicate with external services - hence its name.
It was originally created to hold code for Firefox Sync. Later, it
became the location for code written by the Mozilla Services Client team
and thus includes :ref:`healthreport`. This team no longer exists, but
the directory remains.
.. toctree::
:maxdepth: 1
metrics
datareporting

130
services/docs/metrics.rst Normal file
View File

@ -0,0 +1,130 @@
.. _services_metrics:
============================
Metrics Collection Framework
============================
The ``services/metrics`` directory contains a generic data metrics
collecting and persisting framework for Gecko applications.
Overview
========
The Metrics framework by itself doesn't do much: it simply provides a
generic mechanism for collecting and persisting data. It is up to users
of this framework to drive collection and do something with the obtained
data. A consumer of this framework is :ref:`healthreport`.
Relationship to Telemetry
-------------------------
Telemetry provides similar features to code in this directory. The two
may be unified in the future.
Usage
=====
To use the code in this directory, import Metrics.jsm. e.g.
Components.utils.import("resource://gre/modules/Metrics.jsm");
This exports a *Metrics* object which holds references to the main JS
types and functions provided by this feature. Read below for what those
types are.
Metrics Types
=============
``Metrics.jsm`` exports a number of types. They are documented in the
sections below.
Metrics.Provider
----------------
``Metrics.Provider`` is an entity that collects and manages data. Providers
are typically domain-specific: if you need to collect a new type of data,
you create a ``Metrics.Provider`` type that does this.
Metrics.Measurement
-------------------
A ``Metrics.Measurement`` represents a collection of related pieces/fields
of data.
All data recorded by the metrics framework is modeled as
``Metrics.Measurement`` instances. Instances of ``Metrics.Measurement``
are essentially data structure descriptors.
Each ``Metrics.Measurement`` consists of a name and version to identify
itself (and its data) as well as a list of *fields* that this measurement
holds. A *field* is effectively an entry in a data structure. It consists
of a name and strongly enumerated type.
Metrics.Storage
---------------
This entity is responsible for persisting collected data and state.
It currently uses SQLite to store data, but this detail is abstracted away
in order to facilitate swapping of storage backends.
Metrics.ProviderManager
-----------------------
High-level entity coordinating activity among several ``Metrics.Provider``
instances.
Providers and Measurements
==========================
The most important types in this framework are ``Metrics.Provider`` and
``Metrics.Measurement``, henceforth known as ``Provider`` and
``Measurement``, respectively. As you will see, these two types go
hand in hand.
A ``Provider`` is an entity that *provides* data about a specific subsystem
or feature. They do this by recording data to specific ``Measurement``
types. Both ``Provider`` and ``Measurement`` are abstract base types.
A ``Measurement`` implementation defines a name and version. More
importantly, it also defines its storage requirements and how
previously-stored values are serialized.
Storage allocation is performed by communicating with the SQLite
backend. There is a startup function that tells SQLite what fields the
measurement is recording. The storage backend then registers these in
the database. Internally, this is creating a new primary key for
individual fields so later storage operations can directly reference
these primary keys in order to retrieve data without having to perform
complicated joins.
A ``Provider`` can be thought of as a collection of ``Measurement``
implementations. e.g. an Addons provider may consist of a measurement
for all *current* add-ons as well as a separate measurement for
historical counts of add-ons. A provider's primary role is to take
metrics data and write it to various measurements. This effectively
persists the data to SQLite.
Data is emitted from providers in either a push or pull based mechanism.
In push-based scenarios, the provider likely subscribes to external
events (e.g. observer notifications). An event of interest can occur at
any time. When it does, the provider immediately writes the event of
interest to storage or buffers it for eventual writing. In pull-based
scenarios, the provider is periodically queried and asked to populate
data.
SQLite Storage
==============
``Metrics.Storage`` provides an interface for persisting metrics data to a
SQLite database.
The storage API organizes values by fields. A field is a named member of
a ``Measurement`` that has specific type and retention characteristics.
Some example field types include:
* Last text value
* Last numeric value for a given day
* Discrete text values for a given day
See ``storage.jsm`` for more.

View File

@ -0,0 +1,43 @@
/* 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",
"AddonsProvider",
"AppInfoProvider",
"CrashesProvider",
"HealthReportProvider",
"HotfixProvider",
"Metrics",
"PlacesProvider",
"ProfileMetadataProvider",
"SearchesProvider",
"SessionsProvider",
"SysInfoProvider",
];
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
// We concatenate the JSMs together to eliminate compartment overhead.
// This is a giant hack until compartment overhead is no longer an
// issue.
#define MERGED_COMPARTMENT
#include ../common/async.js
;
#include ../common/bagheeraclient.js
;
#include ../metrics/Metrics.jsm
;
#include healthreporter.jsm
;
#include profile.jsm
;
#include providers.jsm
;

View File

@ -0,0 +1,16 @@
# Register Firefox Health Report providers.
category healthreport-js-provider-default AddonsProvider resource://gre/modules/HealthReport.jsm
category healthreport-js-provider-default AppInfoProvider resource://gre/modules/HealthReport.jsm
#ifdef MOZ_CRASHREPORTER
category healthreport-js-provider-default CrashesProvider resource://gre/modules/HealthReport.jsm
#endif
category healthreport-js-provider-default HealthReportProvider resource://gre/modules/HealthReport.jsm
category healthreport-js-provider-default HotfixProvider resource://gre/modules/HealthReport.jsm
category healthreport-js-provider-default PlacesProvider resource://gre/modules/HealthReport.jsm
category healthreport-js-provider-default ProfileMetadataProvider resource://gre/modules/HealthReport.jsm
category healthreport-js-provider-default SearchesProvider resource://gre/modules/HealthReport.jsm
category healthreport-js-provider-default SessionsProvider resource://gre/modules/HealthReport.jsm
category healthreport-js-provider-default SysInfoProvider resource://gre/modules/HealthReport.jsm
# No Aurora or Beta providers yet; use the categories
# "healthreport-js-provider-aurora", "healthreport-js-provider-beta".

View File

@ -1,11 +1,11 @@
.. _healthreport:
================================
Firefox Health Report (Obsolete)
================================
=====================
Firefox Health Report
=====================
**Firefox Health Report (FHR) is obsolete and no longer ships with Firefox.
This documentation will live here for a few more cycles.**
``/services/healthreport`` 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. The core

View File

@ -0,0 +1,38 @@
#filter substitution
/* 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("datareporting.healthreport.currentDaySubmissionFailureCount", 0);
pref("datareporting.healthreport.documentServerURI", "https://fhr.data.mozilla.com/");
pref("datareporting.healthreport.documentServerNamespace", "metrics");
pref("datareporting.healthreport.infoURL", "https://www.mozilla.org/legal/privacy/firefox.html#health-report");
pref("datareporting.healthreport.logging.consoleEnabled", true);
pref("datareporting.healthreport.logging.consoleLevel", "Warn");
pref("datareporting.healthreport.logging.dumpEnabled", false);
pref("datareporting.healthreport.logging.dumpLevel", "Debug");
pref("datareporting.healthreport.lastDataSubmissionFailureTime", "0");
pref("datareporting.healthreport.lastDataSubmissionRequestedTime", "0");
pref("datareporting.healthreport.lastDataSubmissionSuccessfulTime", "0");
pref("datareporting.healthreport.nextDataSubmissionTime", "0");
pref("datareporting.healthreport.pendingDeleteRemoteData", false);
// Health Report is enabled by default on all channels.
pref("datareporting.healthreport.uploadEnabled", true);
pref("datareporting.healthreport.service.enabled", true);
pref("datareporting.healthreport.service.loadDelayMsec", 10000);
pref("datareporting.healthreport.service.loadDelayFirstRunMsec", 60000);
pref("datareporting.healthreport.service.providerCategories",
#if MOZ_UPDATE_CHANNEL == release
"healthreport-js-provider-default"
#elif MOZ_UPDATE_CHANNEL == default
"healthreport-js-provider-default"
#else
"healthreport-js-provider-default,healthreport-js-provider-@MOZ_UPDATE_CHANNEL@"
#endif
);
pref("datareporting.healthreport.about.reportUrl", "https://fhr.cdn.mozilla.net/%LOCALE%/v4/");
pref("datareporting.healthreport.about.reportUrlUnified", "https://fhr.cdn.mozilla.net/%LOCALE%/v4/");

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,219 @@
/* 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 = [
"getAppInfo",
"updateAppInfo",
"createFakeCrash",
"InspectedHealthReporter",
"getHealthReporter",
];
const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
Cu.import("resource://gre/modules/Preferences.jsm");
Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource://gre/modules/FileUtils.jsm");
Cu.import("resource://gre/modules/osfile.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/services-common/utils.js");
Cu.import("resource://gre/modules/services/datareporting/policy.jsm");
Cu.import("resource://gre/modules/services/healthreport/healthreporter.jsm");
Cu.import("resource://gre/modules/services/healthreport/providers.jsm");
Cu.import("resource://testing-common/services/datareporting/mocks.jsm");
var APP_INFO = {
vendor: "Mozilla",
name: "xpcshell",
ID: "xpcshell@tests.mozilla.org",
version: "1",
appBuildID: "20121107",
platformVersion: "p-ver",
platformBuildID: "20121106",
inSafeMode: false,
logConsoleErrors: true,
OS: "XPCShell",
XPCOMABI: "noarch-spidermonkey",
QueryInterface: XPCOMUtils.generateQI([Ci.nsIXULAppInfo, Ci.nsIXULRuntime]),
invalidateCachesOnRestart: function() {},
};
/**
* Obtain a reference to the current object used to define XULAppInfo.
*/
this.getAppInfo = function () { return APP_INFO; }
/**
* Update the current application info.
*
* If the argument is defined, it will be the object used. Else, APP_INFO is
* used.
*
* To change the current XULAppInfo, simply call this function. If there was
* a previously registered app info object, it will be unloaded and replaced.
*/
this.updateAppInfo = function (obj) {
obj = obj || APP_INFO;
APP_INFO = obj;
let id = Components.ID("{fbfae60b-64a4-44ef-a911-08ceb70b9f31}");
let cid = "@mozilla.org/xre/app-info;1";
let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
// Unregister an existing factory if one exists.
try {
let existing = Components.manager.getClassObjectByContractID(cid, Ci.nsIFactory);
registrar.unregisterFactory(id, existing);
} catch (ex) {}
let factory = {
createInstance: function (outer, iid) {
if (outer != null) {
throw Cr.NS_ERROR_NO_AGGREGATION;
}
return obj.QueryInterface(iid);
},
};
registrar.registerFactory(id, "XULAppInfo", cid, factory);
};
/**
* Creates a fake crash in the Crash Reports directory.
*
* Currently, we just create a dummy file. A more robust implementation would
* create something that actually resembles a crash report file.
*
* This is very similar to code in crashreporter/tests/browser/head.js.
*
* FUTURE consolidate code in a shared JSM.
*/
this.createFakeCrash = function (submitted=false, date=new Date()) {
let id = CommonUtils.generateUUID();
let filename;
let paths = ["Crash Reports"];
let mode;
if (submitted) {
paths.push("submitted");
filename = "bp-" + id + ".txt";
mode = OS.Constants.libc.S_IRUSR | OS.Constants.libc.S_IWUSR |
OS.Constants.libc.S_IRGRP | OS.Constants.libc.S_IROTH;
} else {
paths.push("pending");
filename = id + ".dmp";
mode = OS.Constants.libc.S_IRUSR | OS.Constants.libc.S_IWUSR;
}
paths.push(filename);
let file = FileUtils.getFile("UAppData", paths, true);
file.create(file.NORMAL_FILE_TYPE, mode);
file.lastModifiedTime = date.getTime();
dump("Created fake crash: " + id + "\n");
return id;
};
/**
* A HealthReporter that is probed with various callbacks and counters.
*
* The purpose of this type is to aid testing of startup and shutdown.
*/
this.InspectedHealthReporter = function (branch, policy, stateLeaf) {
HealthReporter.call(this, branch, policy, stateLeaf);
this.onStorageCreated = null;
this.onProviderManagerInitialized = null;
this.providerManagerShutdownCount = 0;
this.storageCloseCount = 0;
}
InspectedHealthReporter.prototype = {
__proto__: HealthReporter.prototype,
_onStorageCreated: function (storage) {
if (this.onStorageCreated) {
this.onStorageCreated(storage);
}
return HealthReporter.prototype._onStorageCreated.call(this, storage);
},
_initializeProviderManager: Task.async(function* () {
yield HealthReporter.prototype._initializeProviderManager.call(this);
if (this.onInitializeProviderManagerFinished) {
this.onInitializeProviderManagerFinished();
}
}),
_onProviderManagerInitialized: function () {
if (this.onProviderManagerInitialized) {
this.onProviderManagerInitialized();
}
return HealthReporter.prototype._onProviderManagerInitialized.call(this);
},
_onProviderManagerShutdown: function () {
this.providerManagerShutdownCount++;
return HealthReporter.prototype._onProviderManagerShutdown.call(this);
},
_onStorageClose: function () {
this.storageCloseCount++;
return HealthReporter.prototype._onStorageClose.call(this);
},
};
const DUMMY_URI="http://localhost:62013/";
this.getHealthReporter = function (name, uri=DUMMY_URI, inspected=false) {
// The healthreporters use the client id from the datareporting service,
// so we need to ensure it is initialized.
let drs = Cc["@mozilla.org/datareporting/service;1"]
.getService(Ci.nsISupports)
.wrappedJSObject;
drs.observe(null, "app-startup", null);
drs.observe(null, "profile-after-change", null);
let branch = "healthreport.testing." + name + ".";
let prefs = new Preferences(branch + "healthreport.");
prefs.set("documentServerURI", uri);
prefs.set("dbName", name);
let reporter;
let policyPrefs = new Preferences(branch + "policy.");
let listener = new MockPolicyListener();
listener.onRequestDataUpload = function (request) {
let promise = reporter.requestDataUpload(request);
MockPolicyListener.prototype.onRequestDataUpload.call(this, request);
return promise;
}
listener.onRequestRemoteDelete = function (request) {
let promise = reporter.deleteRemoteData(request);
MockPolicyListener.prototype.onRequestRemoteDelete.call(this, request);
return promise;
}
let policy = new DataReportingPolicy(policyPrefs, prefs, listener);
let type = inspected ? InspectedHealthReporter : HealthReporter;
reporter = new type(branch + "healthreport.", policy,
"state-" + name + ".json");
return reporter;
};

View File

@ -0,0 +1,27 @@
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# 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/.
SPHINX_TREES['healthreport'] = 'docs'
XPCSHELL_TESTS_MANIFESTS += ['tests/xpcshell/xpcshell.ini']
EXTRA_PP_COMPONENTS += [
'HealthReportComponents.manifest',
]
EXTRA_PP_JS_MODULES += [
'HealthReport.jsm',
]
EXTRA_PP_JS_MODULES.services.healthreport += [
'healthreporter.jsm',
'profile.jsm',
'providers.jsm',
]
TESTING_JS_MODULES.services.healthreport += [
'modules-testing/utils.jsm',
]

View File

@ -0,0 +1,124 @@
/* 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/. */
#ifndef MERGED_COMPARTMENT
"use strict";
this.EXPORTED_SYMBOLS = ["ProfileMetadataProvider"];
const {utils: Cu, classes: Cc, interfaces: Ci} = Components;
const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
Cu.import("resource://gre/modules/Metrics.jsm");
#endif
const DEFAULT_PROFILE_MEASUREMENT_NAME = "age";
const DEFAULT_PROFILE_MEASUREMENT_VERSION = 2;
const REQUIRED_UINT32_TYPE = {type: "TYPE_UINT32"};
Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource://gre/modules/osfile.jsm")
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://gre/modules/ProfileAge.jsm");
/**
* Measurements pertaining to the user's profile.
*/
// This is "version 1" of the metadata measurement - it must remain, but
// it's currently unused - see bug 1063714 comment 12 for why.
function ProfileMetadataMeasurement() {
Metrics.Measurement.call(this);
}
ProfileMetadataMeasurement.prototype = {
__proto__: Metrics.Measurement.prototype,
name: DEFAULT_PROFILE_MEASUREMENT_NAME,
version: 1,
fields: {
// Profile creation date. Number of days since Unix epoch.
profileCreation: {type: Metrics.Storage.FIELD_LAST_NUMERIC},
},
};
// This is the current measurement - it adds the profileReset value.
function ProfileMetadataMeasurement2() {
Metrics.Measurement.call(this);
}
ProfileMetadataMeasurement2.prototype = {
__proto__: Metrics.Measurement.prototype,
name: DEFAULT_PROFILE_MEASUREMENT_NAME,
version: DEFAULT_PROFILE_MEASUREMENT_VERSION,
fields: {
// Profile creation date. Number of days since Unix epoch.
profileCreation: {type: Metrics.Storage.FIELD_LAST_NUMERIC},
// Profile reset date. Number of days since Unix epoch.
profileReset: {type: Metrics.Storage.FIELD_LAST_NUMERIC},
},
};
/**
* Turn a millisecond timestamp into a day timestamp.
*
* @param msec a number of milliseconds since epoch.
* @return the number of whole days denoted by the input.
*/
function truncate(msec) {
return Math.floor(msec / MILLISECONDS_PER_DAY);
}
/**
* A Metrics.Provider for profile metadata, such as profile creation and
* reset time.
*/
this.ProfileMetadataProvider = function() {
Metrics.Provider.call(this);
}
this.ProfileMetadataProvider.prototype = {
__proto__: Metrics.Provider.prototype,
name: "org.mozilla.profile",
measurementTypes: [ProfileMetadataMeasurement2],
pullOnly: true,
getProfileDays: Task.async(function* () {
let result = {};
let accessor = new ProfileAge(null, this._log);
let created = yield accessor.created;
result["profileCreation"] = truncate(created);
let reset = yield accessor.reset;
if (reset) {
result["profileReset"] = truncate(reset);
}
return result;
}),
collectConstantData: function () {
let m = this.getMeasurement(DEFAULT_PROFILE_MEASUREMENT_NAME,
DEFAULT_PROFILE_MEASUREMENT_VERSION);
return Task.spawn(function* collectConstants() {
let days = yield this.getProfileDays();
yield this.enqueueStorageOperation(function storeDays() {
return Task.spawn(function* () {
yield m.setLastNumeric("profileCreation", days["profileCreation"]);
if (days["profileReset"]) {
yield m.setLastNumeric("profileReset", days["profileReset"]);
}
});
});
}.bind(this));
},
};

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,21 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// We need to initialize the profile or OS.File may not work. See bug 810543.
do_get_profile();
(function initMetricsTestingInfrastructure() {
let ns = {};
Components.utils.import("resource://testing-common/services/common/logging.js",
ns);
ns.initTestLogging();
}).call(this);
(function createAppInfo() {
let ns = {};
Components.utils.import("resource://testing-common/services/healthreport/utils.jsm", ns);
ns.updateAppInfo();
}).call(this);

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,20 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const modules = [
"healthreporter.jsm",
"profile.jsm",
"providers.jsm",
];
function run_test() {
for (let m of modules) {
let resource = "resource://gre/modules/services/healthreport/" + m;
Components.utils.import(resource, {});
}
Components.utils.import("resource://gre/modules/HealthReport.jsm", {});
}

View File

@ -0,0 +1,258 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
var {utils: Cu} = Components;
const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
// Create profile directory before use.
// It can be no older than a day ago….
var profile_creation_lower = Date.now() - MILLISECONDS_PER_DAY;
do_get_profile();
Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource://gre/modules/Metrics.jsm");
Cu.import("resource://gre/modules/services/healthreport/profile.jsm");
Cu.import("resource://gre/modules/ProfileAge.jsm");
Cu.import("resource://gre/modules/Task.jsm");
function MockProfileMetadataProvider(name="MockProfileMetadataProvider") {
this.name = name;
ProfileMetadataProvider.call(this);
}
MockProfileMetadataProvider.prototype = {
__proto__: ProfileMetadataProvider.prototype,
includeProfileReset: false,
getProfileDays: function getProfileDays() {
let result = {profileCreation: 1234};
if (this.includeProfileReset) {
result.profileReset = 5678;
}
return Promise.resolve(result);
},
};
function run_test() {
run_next_test();
}
/**
* Ensure that OS.File works in our environment.
* This test can go once there are xpcshell tests for OS.File.
*/
add_test(function use_os_file() {
Cu.import("resource://gre/modules/osfile.jsm")
// Ensure that we get constants, too.
do_check_neq(OS.Constants.Path.profileDir, null);
let iterator = new OS.File.DirectoryIterator(".");
iterator.forEach(function onEntry(entry) {
print("Got " + entry.path);
}).then(function onSuccess() {
iterator.close();
print("Done.");
run_next_test();
}, function onFail() {
iterator.close();
do_throw("Iterating over current directory failed.");
});
});
function getAccessor() {
let acc = new ProfileAge();
print("Profile is " + acc.profilePath);
return acc;
}
add_test(function test_time_accessor_no_file() {
let acc = getAccessor();
// There should be no file yet.
acc.readTimes()
.then(function onSuccess(json) {
do_throw("File existed!");
},
function onFailure() {
run_next_test();
});
});
add_task(function test_time_accessor_named_file() {
let acc = getAccessor();
// There should be no file yet.
yield acc.writeTimes({created: 12345}, "test.json");
let json = yield acc.readTimes("test.json")
print("Read: " + JSON.stringify(json));
do_check_eq(12345, json.created);
});
add_task(function test_time_accessor_creates_file() {
let lower = profile_creation_lower;
// Ensure that provided contents are merged, and existing
// files can be overwritten. These two things occur if we
// read and then decide that we have to write.
let acc = getAccessor();
let existing = {abc: "123", easy: "abc"};
let expected;
let created = yield acc.computeAndPersistCreated(existing, "test2.json")
let upper = Date.now() + 1000;
print(lower + " < " + created + " <= " + upper);
do_check_true(lower < created);
do_check_true(upper >= created);
expected = created;
let json = yield acc.readTimes("test2.json")
print("Read: " + JSON.stringify(json));
do_check_eq("123", json.abc);
do_check_eq("abc", json.easy);
do_check_eq(expected, json.created);
});
add_task(function test_time_accessor_all() {
let lower = profile_creation_lower;
let acc = getAccessor();
let expected;
let created = yield acc.created
let upper = Date.now() + 1000;
do_check_true(lower < created);
do_check_true(upper >= created);
expected = created;
let again = yield acc.created
do_check_eq(expected, again);
});
add_task(function* test_time_reset() {
let lower = profile_creation_lower;
let acc = getAccessor();
let testTime = 100000;
yield acc.recordProfileReset(testTime);
let reset = yield acc.reset;
Assert.equal(reset, testTime);
});
add_test(function test_constructor() {
let provider = new ProfileMetadataProvider("named");
run_next_test();
});
add_test(function test_profile_files() {
let provider = new ProfileMetadataProvider();
function onSuccess(answer) {
let now = Date.now() / MILLISECONDS_PER_DAY;
print("Got " + answer.profileCreation + ", versus now = " + now);
Assert.ok(answer.profileCreation < now);
run_next_test();
}
function onFailure(ex) {
do_throw("Directory iteration failed: " + ex);
}
provider.getProfileDays().then(onSuccess, onFailure);
});
// A generic test helper. We use this with both real
// and mock providers in these tests.
function test_collect_constant(provider, expectReset) {
return Task.spawn(function* () {
yield provider.collectConstantData();
let m = provider.getMeasurement("age", 2);
Assert.notEqual(m, null);
let values = yield m.getValues();
Assert.ok(values.singular.has("profileCreation"));
let createValue = values.singular.get("profileCreation")[1];
let resetValue;
if (expectReset) {
Assert.equal(values.singular.size, 2);
Assert.ok(values.singular.has("profileReset"));
resetValue = values.singular.get("profileReset")[1];
} else {
Assert.equal(values.singular.size, 1);
Assert.ok(!values.singular.has("profileReset"));
}
return [createValue, resetValue];
});
}
add_task(function* test_collect_constant_mock_no_reset() {
let storage = yield Metrics.Storage("collect_constant_mock");
let provider = new MockProfileMetadataProvider();
yield provider.init(storage);
let v = yield test_collect_constant(provider, false);
Assert.equal(v.length, 2);
Assert.equal(v[0], 1234);
Assert.equal(v[1], undefined);
yield storage.close();
});
add_task(function* test_collect_constant_mock_with_reset() {
let storage = yield Metrics.Storage("collect_constant_mock");
let provider = new MockProfileMetadataProvider();
provider.includeProfileReset = true;
yield provider.init(storage);
let v = yield test_collect_constant(provider, true);
Assert.equal(v.length, 2);
Assert.equal(v[0], 1234);
Assert.equal(v[1], 5678);
yield storage.close();
});
add_task(function* test_collect_constant_real_no_reset() {
let provider = new ProfileMetadataProvider();
let storage = yield Metrics.Storage("collect_constant_real");
yield provider.init(storage);
let vals = yield test_collect_constant(provider, false);
let created = vals[0];
let reset = vals[1];
Assert.equal(reset, undefined);
let ms = created * MILLISECONDS_PER_DAY;
let lower = profile_creation_lower;
let upper = Date.now() + 1000;
print("Day: " + created);
print("msec: " + ms);
print("Lower: " + lower);
print("Upper: " + upper);
Assert.ok(lower <= ms);
Assert.ok(upper >= ms);
yield storage.close();
});
add_task(function* test_collect_constant_real_with_reset() {
let now = Date.now();
let acc = getAccessor();
yield acc.writeTimes({created: now-MILLISECONDS_PER_DAY, // yesterday
reset: Date.now()}); // today
let provider = new ProfileMetadataProvider();
let storage = yield Metrics.Storage("collect_constant_real");
yield provider.init(storage);
let [created, reset] = yield test_collect_constant(provider, true);
// we've already tested truncate() works as expected, so here just check
// we got values.
Assert.ok(created);
Assert.ok(reset);
Assert.ok(created <= reset);
yield storage.close();
});

View File

@ -0,0 +1,339 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
var {utils: Cu, classes: Cc, interfaces: Ci} = Components;
Cu.import("resource://gre/modules/Metrics.jsm");
Cu.import("resource://gre/modules/services/healthreport/providers.jsm");
// The hack, it burns. This could go away if extensions code exposed its
// test environment setup functions as a testing-only JSM. See similar
// code in Sync's head_helpers.js.
var gGlobalScope = this;
function loadAddonManager() {
let ns = {};
Cu.import("resource://gre/modules/Services.jsm", ns);
let head = "../../../../toolkit/mozapps/extensions/test/xpcshell/head_addons.js";
let file = do_get_file(head);
let uri = ns.Services.io.newFileURI(file);
ns.Services.scriptloader.loadSubScript(uri.spec, gGlobalScope);
createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
startupManager();
}
function run_test() {
loadAddonManager();
run_next_test();
}
add_test(function test_constructor() {
let provider = new AddonsProvider();
run_next_test();
});
add_task(function test_init() {
let storage = yield Metrics.Storage("init");
let provider = new AddonsProvider();
yield provider.init(storage);
yield provider.shutdown();
yield storage.close();
});
function monkeypatchAddons(provider, addons) {
if (!Array.isArray(addons)) {
throw new Error("Must define array of addon objects.");
}
Object.defineProperty(provider, "_createDataStructure", {
value: function _createDataStructure() {
return AddonsProvider.prototype._createDataStructure.call(provider, addons);
},
});
}
add_task(function test_collect() {
let storage = yield Metrics.Storage("collect");
let provider = new AddonsProvider();
yield provider.init(storage);
let now = new Date();
// FUTURE install add-on via AddonManager and don't use monkeypatching.
let testAddons = [
{
id: "addon0",
userDisabled: false,
appDisabled: false,
version: "1",
type: "extension",
scope: 1,
foreignInstall: false,
hasBinaryComponents: false,
installDate: now,
updateDate: now,
},
// This plugin entry should get ignored.
{
id: "addon1",
userDisabled: false,
appDisabled: false,
version: "2",
type: "plugin",
scope: 1,
foreignInstall: false,
hasBinaryComponents: false,
installDate: now,
updateDate: now,
},
// Is counted but full details are omitted because it is a theme.
{
id: "addon2",
userDisabled: false,
appDisabled: false,
version: "3",
type: "theme",
scope: 1,
foreignInstall: false,
hasBinaryComponents: false,
installDate: now,
updateDate: now,
},
{
id: "addon3",
userDisabled: false,
appDisabled: false,
version: "4",
type: "service",
scope: 1,
foreignInstall: false,
hasBinaryComponents: false,
installDate: now,
updateDate: now,
description: "addon3 description"
},
{
// Should be excluded from the report completely
id: "pluginfake",
type: "plugin",
userDisabled: false,
appDisabled: false,
},
{
// Should be in gm-plugins
id: "gmp-testgmp",
type: "plugin",
userDisabled: false,
version: "7.2",
isGMPlugin: true,
},
];
monkeypatchAddons(provider, testAddons);
let testPlugins = {
"Test Plug-in":
{
"version": "1.0.0.0",
"description": "Plug-in for testing purposes.™ (हिन्दी 中文 العربية)",
"blocklisted": false,
"disabled": false,
"clicktoplay": false,
"mimeTypes":[
"application/x-test"
],
},
"Second Test Plug-in":
{
"version": "1.0.0.0",
"description": "Second plug-in for testing purposes.",
"blocklisted": false,
"disabled": false,
"clicktoplay": false,
"mimeTypes":[
"application/x-second-test"
],
},
"Java Test Plug-in":
{
"version": "1.0.0.0",
"description": "Dummy Java plug-in for testing purposes.",
"blocklisted": false,
"disabled": false,
"clicktoplay": false,
"mimeTypes":[
"application/x-java-test"
],
},
"Third Test Plug-in":
{
"version": "1.0.0.0",
"description": "Third plug-in for testing purposes.",
"blocklisted": false,
"disabled": false,
"clicktoplay": false,
"mimeTypes":[
"application/x-third-test"
],
},
"Flash Test Plug-in":
{
"version": "1.0.0.0",
"description": "Flash plug-in for testing purposes.",
"blocklisted": false,
"disabled": false,
"clicktoplay": false,
"mimeTypes":[
"application/x-shockwave-flash-test"
],
},
"Silverlight Test Plug-in":
{
"version": "1.0.0.0",
"description": "Silverlight plug-in for testing purposes.",
"blocklisted": false,
"disabled": false,
"clicktoplay": false,
"mimeTypes":[
"application/x-silverlight-test"
],
},
};
let pluginTags = Cc["@mozilla.org/plugin/host;1"]
.getService(Ci.nsIPluginHost)
.getPluginTags({});
for (let tag of pluginTags) {
if (tag.name in testPlugins) {
let p = testPlugins[tag.name];
p.id = tag.filename+":"+tag.name+":"+p.version+":"+p.description;
}
}
yield provider.collectConstantData();
// Test addons measurement.
let addons = provider.getMeasurement("addons", 2);
let data = yield addons.getValues();
do_check_eq(data.days.size, 0);
do_check_eq(data.singular.size, 1);
do_check_true(data.singular.has("addons"));
let json = data.singular.get("addons")[1];
let value = JSON.parse(json);
do_check_eq(typeof(value), "object");
do_check_eq(Object.keys(value).length, 2);
do_check_true("addon0" in value);
do_check_true(!("addon1" in value));
do_check_true(!("addon2" in value));
do_check_true("addon3" in value);
do_check_true(!("pluginfake" in value));
do_check_true(!("gmp-testgmp" in value));
let serializer = addons.serializer(addons.SERIALIZE_JSON);
let serialized = serializer.singular(data.singular);
do_check_eq(typeof(serialized), "object");
do_check_eq(Object.keys(serialized).length, 3); // Our entries, plus _v.
do_check_true("addon0" in serialized);
do_check_true("addon3" in serialized);
do_check_eq(serialized._v, 2);
// Test plugins measurement.
let plugins = provider.getMeasurement("plugins", 1);
data = yield plugins.getValues();
do_check_eq(data.days.size, 0);
do_check_eq(data.singular.size, 1);
do_check_true(data.singular.has("plugins"));
json = data.singular.get("plugins")[1];
value = JSON.parse(json);
do_check_eq(typeof(value), "object");
do_check_eq(Object.keys(value).length, pluginTags.length);
do_check_true(testPlugins["Test Plug-in"].id in value);
do_check_true(testPlugins["Second Test Plug-in"].id in value);
do_check_true(testPlugins["Java Test Plug-in"].id in value);
for (let id in value) {
let item = value[id];
let testData = testPlugins[item.name];
for (let prop in testData) {
if (prop == "mimeTypes" || prop == "id") {
continue;
}
do_check_eq(testData[prop], item[prop]);
}
for (let mime of testData.mimeTypes) {
do_check_true(item.mimeTypes.indexOf(mime) != -1);
}
}
serializer = plugins.serializer(plugins.SERIALIZE_JSON);
serialized = serializer.singular(data.singular);
do_check_eq(typeof(serialized), "object");
do_check_eq(Object.keys(serialized).length, pluginTags.length+1); // Our entries, plus _v.
for (let name in testPlugins) {
// Special case for bug 1165981. There is a test plugin that
// exists to make sure we don't load it on certain platforms.
// We skip the check for that plugin here, as it will work on some
// platforms but not others.
if (name == "Third Test Plug-in") {
continue;
}
do_check_true(testPlugins[name].id in serialized);
}
do_check_eq(serialized._v, 1);
// Test GMP plugins measurement.
let gmPlugins = provider.getMeasurement("gm-plugins", 1);
data = yield gmPlugins.getValues();
do_check_eq(data.days.size, 0);
do_check_eq(data.singular.size, 1);
do_check_true(data.singular.has("gm-plugins"));
json = data.singular.get("gm-plugins")[1];
value = JSON.parse(json);
do_print("value: " + json);
do_check_eq(typeof(value), "object");
do_check_eq(Object.keys(value).length, 1);
do_check_eq(value["gmp-testgmp"].version, "7.2");
do_check_eq(value["gmp-testgmp"].userDisabled, false);
serializer = gmPlugins.serializer(plugins.SERIALIZE_JSON);
serialized = serializer.singular(data.singular);
do_check_eq(typeof(serialized), "object");
do_check_eq(serialized["gmp-testgmp"].version, "7.2");
do_check_eq(serialized._v, 1);
// Test counts measurement.
let counts = provider.getMeasurement("counts", 2);
data = yield counts.getValues();
do_check_eq(data.days.size, 1);
do_check_eq(data.singular.size, 0);
do_check_true(data.days.hasDay(now));
value = data.days.getDay(now);
do_check_eq(value.size, 4);
do_check_eq(value.get("extension"), 1);
do_check_eq(value.get("plugin"), pluginTags.length);
do_check_eq(value.get("theme"), 1);
do_check_eq(value.get("service"), 1);
yield provider.shutdown();
yield storage.close();
});

View File

@ -0,0 +1,281 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
var {interfaces: Ci, results: Cr, utils: Cu, classes: Cc} = Components;
Cu.import("resource://gre/modules/Metrics.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/services/healthreport/providers.jsm");
Cu.import("resource://testing-common/services/healthreport/utils.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyGetter(this, "gDatareportingService",
() => Cc["@mozilla.org/datareporting/service;1"]
.getService(Ci.nsISupports)
.wrappedJSObject);
function run_test() {
do_get_profile();
// Send the needed startup notifications to the datareporting service
// to ensure that it has been initialized.
gDatareportingService.observe(null, "app-startup", null);
gDatareportingService.observe(null, "profile-after-change", null);
run_next_test();
}
add_test(function test_constructor() {
let provider = new AppInfoProvider();
run_next_test();
});
add_task(function test_collect_smoketest() {
let storage = yield Metrics.Storage("collect_smoketest");
let provider = new AppInfoProvider();
yield provider.init(storage);
let now = new Date();
yield provider.collectConstantData();
let m = provider.getMeasurement("appinfo", 2);
let data = yield storage.getMeasurementValues(m.id);
let serializer = m.serializer(m.SERIALIZE_JSON);
let d = serializer.singular(data.singular);
do_check_eq(d._v, 2);
do_check_eq(d.vendor, "Mozilla");
do_check_eq(d.name, "xpcshell");
do_check_eq(d.id, "xpcshell@tests.mozilla.org");
do_check_eq(d.version, "1");
do_check_eq(d.appBuildID, "20121107");
do_check_eq(d.platformVersion, "p-ver");
do_check_eq(d.platformBuildID, "20121106");
do_check_eq(d.os, "XPCShell");
do_check_eq(d.xpcomabi, "noarch-spidermonkey");
do_check_eq(data.days.size, 1);
do_check_true(data.days.hasDay(now));
let day = data.days.getDay(now);
do_check_eq(day.size, 3);
do_check_true(day.has("isDefaultBrowser"));
do_check_true(day.has("isTelemetryEnabled"));
do_check_true(day.has("isBlocklistEnabled"));
// TODO Bug 827189 Actually test this properly. On some local builds, this
// is always -1 (the service throws). On buildbot, it seems to always be 0.
do_check_neq(day.get("isDefaultBrowser"), 1);
yield provider.shutdown();
yield storage.close();
});
add_task(function test_record_version() {
let storage = yield Metrics.Storage("record_version");
let provider = new AppInfoProvider();
let now = new Date();
yield provider.init(storage);
// The provider records information on startup.
let m = provider.getMeasurement("versions", 2);
let data = yield m.getValues();
do_check_true(data.days.hasDay(now));
let day = data.days.getDay(now);
do_check_eq(day.size, 4);
do_check_true(day.has("appVersion"));
do_check_true(day.has("platformVersion"));
do_check_true(day.has("appBuildID"));
do_check_true(day.has("platformBuildID"));
let value = day.get("appVersion");
do_check_true(Array.isArray(value));
do_check_eq(value.length, 1);
let ai = getAppInfo();
do_check_eq(value[0], ai.version);
value = day.get("platformVersion");
do_check_true(Array.isArray(value));
do_check_eq(value.length, 1);
do_check_eq(value[0], ai.platformVersion);
value = day.get("appBuildID");
do_check_true(Array.isArray(value));
do_check_eq(value.length, 1);
do_check_eq(value[0], ai.appBuildID);
value = day.get("platformBuildID");
do_check_true(Array.isArray(value));
do_check_eq(value.length, 1);
do_check_eq(value[0], ai.platformBuildID);
yield provider.shutdown();
yield storage.close();
});
add_task(function test_record_version_change() {
let storage = yield Metrics.Storage("record_version_change");
let provider = new AppInfoProvider();
let now = new Date();
yield provider.init(storage);
yield provider.shutdown();
let ai = getAppInfo();
ai.version = "new app version";
ai.platformVersion = "new platform version";
ai.appBuildID = "new app id";
ai.platformBuildID = "new platform id";
updateAppInfo(ai);
provider = new AppInfoProvider();
yield provider.init(storage);
// There should be 2 records in the versions history.
let m = provider.getMeasurement("versions", 2);
let data = yield m.getValues();
do_check_true(data.days.hasDay(now));
let day = data.days.getDay(now);
let value = day.get("appVersion");
do_check_true(Array.isArray(value));
do_check_eq(value.length, 2);
do_check_eq(value[1], "new app version");
value = day.get("platformVersion");
do_check_true(Array.isArray(value));
do_check_eq(value.length, 2);
do_check_eq(value[1], "new platform version");
// There should be 2 records in the buildID history.
value = day.get("appBuildID");
do_check_true(Array.isArray(value));
do_check_eq(value.length, 2);
do_check_eq(value[1], "new app id");
value = day.get("platformBuildID");
do_check_true(Array.isArray(value));
do_check_eq(value.length, 2);
do_check_eq(value[1], "new platform id");
yield provider.shutdown();
yield storage.close();
});
add_task(function test_record_telemetry() {
let storage = yield Metrics.Storage("record_telemetry");
let provider;
let now = new Date();
Services.prefs.setBoolPref("toolkit.telemetry.enabled", true);
provider = new AppInfoProvider();
yield provider.init(storage);
yield provider.collectConstantData();
let m = provider.getMeasurement("appinfo", 2);
let data = yield m.getValues();
let d = yield m.serializer(m.SERIALIZE_JSON).daily(data.days.getDay(now));
do_check_eq(1, d.isTelemetryEnabled);
yield provider.shutdown();
Services.prefs.setBoolPref("toolkit.telemetry.enabled", false);
provider = new AppInfoProvider();
yield provider.init(storage);
yield provider.collectConstantData();
m = provider.getMeasurement("appinfo", 2);
data = yield m.getValues();
d = yield m.serializer(m.SERIALIZE_JSON).daily(data.days.getDay(now));
do_check_eq(0, d.isTelemetryEnabled);
yield provider.shutdown();
yield storage.close();
});
add_task(function test_record_blocklist() {
let storage = yield Metrics.Storage("record_blocklist");
let now = new Date();
Services.prefs.setBoolPref("extensions.blocklist.enabled", true);
let provider = new AppInfoProvider();
yield provider.init(storage);
yield provider.collectConstantData();
let m = provider.getMeasurement("appinfo", 2);
let data = yield m.getValues();
let d = yield m.serializer(m.SERIALIZE_JSON).daily(data.days.getDay(now));
do_check_eq(d.isBlocklistEnabled, 1);
yield provider.shutdown();
Services.prefs.setBoolPref("extensions.blocklist.enabled", false);
provider = new AppInfoProvider();
yield provider.init(storage);
yield provider.collectConstantData();
m = provider.getMeasurement("appinfo", 2);
data = yield m.getValues();
d = yield m.serializer(m.SERIALIZE_JSON).daily(data.days.getDay(now));
do_check_eq(d.isBlocklistEnabled, 0);
yield provider.shutdown();
yield storage.close();
});
add_task(function test_record_app_update () {
let storage = yield Metrics.Storage("record_update");
Services.prefs.setBoolPref("app.update.enabled", true);
Services.prefs.setBoolPref("app.update.auto", true);
let provider = new AppInfoProvider();
yield provider.init(storage);
let now = new Date();
yield provider.collectDailyData();
let m = provider.getMeasurement("update", 1);
let data = yield m.getValues();
let d = yield m.serializer(m.SERIALIZE_JSON).daily(data.days.getDay(now));
do_check_eq(d.enabled, 1);
do_check_eq(d.autoDownload, 1);
Services.prefs.setBoolPref("app.update.enabled", false);
Services.prefs.setBoolPref("app.update.auto", false);
yield provider.collectDailyData();
data = yield m.getValues();
d = yield m.serializer(m.SERIALIZE_JSON).daily(data.days.getDay(now));
do_check_eq(d.enabled, 0);
do_check_eq(d.autoDownload, 0);
yield provider.shutdown();
yield storage.close();
});
add_task(function test_healthreporter_integration () {
let reporter = getHealthReporter("healthreporter_integration");
yield reporter.init();
try {
yield reporter._providerManager.registerProviderFromType(AppInfoProvider);
yield reporter.collectMeasurements();
let payload = yield reporter.getJSONPayload(true);
let days = payload['data']['days'];
for (let [day, measurements] in Iterator(days)) {
do_check_eq(Object.keys(measurements).length, 3);
do_check_true("org.mozilla.appInfo.appinfo" in measurements);
do_check_true("org.mozilla.appInfo.update" in measurements);
do_check_true("org.mozilla.appInfo.versions" in measurements);
}
} finally {
yield reporter._shutdown();
}
});

View File

@ -0,0 +1,137 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
var {utils: Cu} = Components;
Cu.import("resource://gre/modules/Metrics.jsm");
Cu.import("resource://gre/modules/services/healthreport/providers.jsm");
Cu.import("resource://testing-common/AppData.jsm");
Cu.import("resource://testing-common/services/healthreport/utils.jsm");
Cu.import("resource://testing-common/CrashManagerTest.jsm");
function run_test() {
run_next_test();
}
add_task(function* init() {
do_get_profile();
yield makeFakeAppDir();
});
add_task(function test_constructor() {
let provider = new CrashesProvider();
});
add_task(function* test_init() {
let storage = yield Metrics.Storage("init");
let provider = new CrashesProvider();
yield provider.init(storage);
yield provider.shutdown();
yield storage.close();
});
add_task(function* test_collect() {
let storage = yield Metrics.Storage("collect");
let provider = new CrashesProvider();
yield provider.init(storage);
// Install custom manager so we don't interfere with other tests.
let manager = yield getManager();
provider._manager = manager;
let day1 = new Date(2014, 0, 1, 0, 0, 0);
let day2 = new Date(2014, 0, 3, 0, 0, 0);
yield manager.addCrash(manager.PROCESS_TYPE_MAIN,
manager.CRASH_TYPE_CRASH,
"mc1", day1, { OOMAllocationSize: 1073741824 });
yield manager.addCrash(manager.PROCESS_TYPE_MAIN,
manager.CRASH_TYPE_CRASH,
"mc2", day1);
yield manager.addCrash(manager.PROCESS_TYPE_CONTENT,
manager.CRASH_TYPE_HANG,
"ch", day1);
yield manager.addCrash(manager.PROCESS_TYPE_PLUGIN,
manager.CRASH_TYPE_CRASH,
"pc", day1);
yield manager.addSubmissionAttempt("mc1", "sub1", day1);
yield manager.addSubmissionResult("mc1", "sub1", day1,
manager.SUBMISSION_RESULT_OK);
yield manager.addSubmissionAttempt("ch", "sub1", day1);
yield manager.addSubmissionResult("ch", "sub1", day1,
manager.SUBMISSION_RESULT_FAILED);
yield manager.addSubmissionAttempt("ch", "sub2", day1);
yield manager.addSubmissionResult("ch", "sub2", day1,
manager.SUBMISSION_RESULT_FAILED);
yield manager.addSubmissionAttempt("ch", "sub3", day1);
yield manager.addSubmissionResult("ch", "sub3", day1,
manager.SUBMISSION_RESULT_OK);
yield manager.addCrash(manager.PROCESS_TYPE_MAIN,
manager.CRASH_TYPE_HANG,
"mh", day2);
yield manager.addCrash(manager.PROCESS_TYPE_CONTENT,
manager.CRASH_TYPE_CRASH,
"cc", day2);
yield manager.addCrash(manager.PROCESS_TYPE_PLUGIN,
manager.CRASH_TYPE_HANG,
"ph", day2);
yield manager.addCrash(manager.PROCESS_TYPE_GMPLUGIN,
manager.CRASH_TYPE_CRASH,
"gmpc", day2);
yield provider.collectDailyData();
let m = provider.getMeasurement("crashes", 6);
let values = yield m.getValues();
do_check_eq(values.days.size, 2);
do_check_true(values.days.hasDay(day1));
do_check_true(values.days.hasDay(day2));
let value = values.days.getDay(day1);
do_check_true(value.has("main-crash"));
do_check_eq(value.get("main-crash"), 2);
do_check_true(value.has("main-crash-oom"));
do_check_eq(value.get("main-crash-oom"), 1);
do_check_true(value.has("content-hang"));
do_check_eq(value.get("content-hang"), 1);
do_check_true(value.has("plugin-crash"));
do_check_eq(value.get("plugin-crash"), 1);
do_check_true(value.has("main-crash-submission-succeeded"));
do_check_eq(value.get("main-crash-submission-succeeded"), 1);
do_check_true(value.has("content-hang-submission-failed"));
do_check_eq(value.get("content-hang-submission-failed"), 2);
do_check_true(value.has("content-hang-submission-succeeded"));
do_check_eq(value.get("content-hang-submission-succeeded"), 1);
value = values.days.getDay(day2);
do_check_true(value.has("main-hang"));
do_check_eq(value.get("main-hang"), 1);
do_check_true(value.has("content-crash"));
do_check_eq(value.get("content-crash"), 1);
do_check_true(value.has("plugin-hang"));
do_check_eq(value.get("plugin-hang"), 1);
do_check_true(value.has("gmplugin-crash"));
do_check_eq(value.get("gmplugin-crash"), 1);
// Check that adding a new crash increments counter on next collect.
yield manager.addCrash(manager.PROCESS_TYPE_MAIN,
manager.CRASH_TYPE_HANG,
"mc3", day2);
yield provider.collectDailyData();
values = yield m.getValues();
value = values.days.getDay(day2);
do_check_eq(value.get("main-hang"), 2);
yield provider.shutdown();
yield storage.close();
});

View File

@ -0,0 +1,179 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
var {utils: Cu} = Components;
Cu.import("resource://gre/modules/osfile.jsm");
Cu.import("resource://gre/modules/Metrics.jsm");
Cu.import("resource://gre/modules/services/healthreport/providers.jsm");
const EXAMPLE_2014052701 = {
"upgradedFrom":"13.0.1",
"uninstallReason":"SUCCESSFUL_UPGRADE",
"_entityID":null,
"forensicsID":"29525548-b653-49db-bfb8-a160cdfbeb4a",
"_installInProgress":false,
"_everCompatible":true,
"reportedWindowsVersion":[6,1,1],
"actualWindowsVersion":[6,1,1],
"firstNotifyDay":0,
"lastNotifyDay":0,
"downloadAttempts":1,
"downloadFailures":0,
"installAttempts":1,
"installSuccesses":1,
"installLauncherFailures":0,
"installFailures":0,
"notificationsShown":0,
"notificationsClicked":0,
"notificationsDismissed":0,
"notificationsRemoved":0,
"launcherExitCodes":{"0":1}
};
function run_test() {
run_next_test();
}
add_task(function* init() {
do_get_profile();
});
add_task(function test_constructor() {
new HotfixProvider();
});
add_task(function* test_init() {
let storage = yield Metrics.Storage("init");
let provider = new HotfixProvider();
yield provider.init(storage);
yield provider.shutdown();
yield storage.close();
});
add_task(function* test_collect_empty() {
let storage = yield Metrics.Storage("collect_empty");
let provider = new HotfixProvider();
yield provider.init(storage);
yield provider.collectDailyData();
let m = provider.getMeasurement("update", 1);
let data = yield m.getValues();
Assert.equal(data.singular.size, 0);
Assert.equal(data.days.size, 0);
yield storage.close();
});
add_task(function* test_collect_20140527() {
let storage = yield Metrics.Storage("collect_20140527");
let provider = new HotfixProvider();
yield provider.init(storage);
let path = OS.Path.join(OS.Constants.Path.profileDir,
"hotfix.v20140527.01.json");
let encoder = new TextEncoder();
yield OS.File.writeAtomic(path,
encoder.encode(JSON.stringify(EXAMPLE_2014052701)));
yield provider.collectDailyData();
let m = provider.getMeasurement("update", 1);
let data = yield m.getValues();
let s = data.singular;
Assert.equal(s.size, 7);
Assert.equal(s.get("v20140527.upgradedFrom")[1], "13.0.1");
Assert.equal(s.get("v20140527.uninstallReason")[1], "SUCCESSFUL_UPGRADE");
Assert.equal(s.get("v20140527.downloadAttempts")[1], 1);
Assert.equal(s.get("v20140527.downloadFailures")[1], 0);
Assert.equal(s.get("v20140527.installAttempts")[1], 1);
Assert.equal(s.get("v20140527.installFailures")[1], 0);
Assert.equal(s.get("v20140527.notificationsShown")[1], 0);
// Ensure the dynamic fields get serialized.
let serializer = m.serializer(m.SERIALIZE_JSON);
let d = serializer.singular(s);
Assert.deepEqual(d, {
"_v": 1,
"v20140527.upgradedFrom": "13.0.1",
"v20140527.uninstallReason": "SUCCESSFUL_UPGRADE",
"v20140527.downloadAttempts": 1,
"v20140527.downloadFailures": 0,
"v20140527.installAttempts": 1,
"v20140527.installFailures": 0,
"v20140527.notificationsShown": 0,
});
// Don't interfere with next test.
yield OS.File.remove(path);
yield storage.close();
});
add_task(function* test_collect_multiple_versions() {
let storage = yield Metrics.Storage("collect_multiple_versions");
let provider = new HotfixProvider();
yield provider.init(storage);
let p1 = {
upgradedFrom: "12.0",
uninstallReason: "SUCCESSFUL_UPGRADE",
downloadAttempts: 3,
downloadFailures: 1,
installAttempts: 1,
installFailures: 1,
notificationsShown: 2,
};
let p2 = {
downloadAttempts: 5,
downloadFailures: 3,
installAttempts: 2,
installFailures: 2,
uninstallReason: null,
notificationsShown: 1,
};
let path1 = OS.Path.join(OS.Constants.Path.profileDir, "updateHotfix.v20140601.json");
let path2 = OS.Path.join(OS.Constants.Path.profileDir, "updateHotfix.v20140701.json");
let encoder = new TextEncoder();
yield OS.File.writeAtomic(path1, encoder.encode(JSON.stringify(p1)));
yield OS.File.writeAtomic(path2, encoder.encode(JSON.stringify(p2)));
yield provider.collectDailyData();
let m = provider.getMeasurement("update", 1);
let data = yield m.getValues();
let serializer = m.serializer(m.SERIALIZE_JSON);
let d = serializer.singular(data.singular);
Assert.deepEqual(d, {
"_v": 1,
"v20140601.upgradedFrom": "12.0",
"v20140601.uninstallReason": "SUCCESSFUL_UPGRADE",
"v20140601.downloadAttempts": 3,
"v20140601.downloadFailures": 1,
"v20140601.installAttempts": 1,
"v20140601.installFailures": 1,
"v20140601.notificationsShown": 2,
"v20140701.uninstallReason": "STILL_INSTALLED",
"v20140701.downloadAttempts": 5,
"v20140701.downloadFailures": 3,
"v20140701.installAttempts": 2,
"v20140701.installFailures": 2,
"v20140701.notificationsShown": 1,
});
// Don't interfere with next test.
yield OS.File.remove(path1);
yield OS.File.remove(path2);
yield storage.close();
});

View File

@ -0,0 +1,46 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
var {utils: Cu} = Components;
Cu.import("resource://gre/modules/Metrics.jsm");
Cu.import("resource://gre/modules/services/healthreport/providers.jsm");
function run_test() {
run_next_test();
}
add_test(function test_constructor() {
let provider = new PlacesProvider();
run_next_test();
});
add_task(function test_collect_smoketest() {
let storage = yield Metrics.Storage("collect_smoketest");
let provider = new PlacesProvider();
yield provider.init(storage);
let now = new Date();
yield provider.collectDailyData();
let m = provider.getMeasurement("places", 1);
let data = yield storage.getMeasurementValues(m.id);
do_check_eq(data.days.size, 1);
do_check_true(data.days.hasDay(now));
let serializer = m.serializer(m.SERIALIZE_JSON);
let day = serializer.daily(data.days.getDay(now));
do_check_eq(day._v, 1);
do_check_eq(Object.keys(day).length, 3);
do_check_eq(day.pages, 0);
do_check_eq(day.bookmarks, 0);
yield storage.close();
});

View File

@ -0,0 +1,187 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
var {utils: Cu} = Components;
Cu.import("resource://gre/modules/Metrics.jsm");
Cu.import("resource://gre/modules/Services.jsm");
var bsp = Cu.import("resource://gre/modules/services/healthreport/providers.jsm");
const DEFAULT_ENGINES = [
{name: "Amazon.com", identifier: "amazondotcom"},
{name: "Bing", identifier: "bing"},
{name: "Google", identifier: "google"},
{name: "Yahoo", identifier: "yahoo"},
{name: "Foobar Search", identifier: "foobar"},
];
function MockSearchCountMeasurement() {
bsp.SearchCountMeasurement3.call(this);
}
MockSearchCountMeasurement.prototype = {
__proto__: bsp.SearchCountMeasurement3.prototype,
};
function MockSearchesProvider() {
SearchesProvider.call(this);
}
MockSearchesProvider.prototype = {
__proto__: SearchesProvider.prototype,
measurementTypes: [MockSearchCountMeasurement],
};
function run_test() {
// Tell the search service we are running in the US. This also has the
// desired side-effect of preventing our geoip lookup.
Services.prefs.setBoolPref("browser.search.isUS", true);
Services.prefs.setCharPref("browser.search.countryCode", "US");
run_next_test();
}
add_test(function test_constructor() {
let provider = new SearchesProvider();
run_next_test();
});
add_task(function* test_record() {
let storage = yield Metrics.Storage("record");
let provider = new MockSearchesProvider();
yield provider.init(storage);
let now = new Date();
// Record searches for all but one of our defaults, and one engine that's
// not a default.
for (let engine of DEFAULT_ENGINES.concat([{name: "Not Default", identifier: "notdef"}])) {
if (engine.identifier == "yahoo") {
continue;
}
yield provider.recordSearch(engine, "abouthome");
yield provider.recordSearch(engine, "contextmenu");
yield provider.recordSearch(engine, "newtab");
yield provider.recordSearch(engine, "searchbar");
yield provider.recordSearch(engine, "urlbar");
}
// Invalid sources should throw.
let errored = false;
try {
yield provider.recordSearch(DEFAULT_ENGINES[0], "bad source");
} catch (ex) {
errored = true;
} finally {
do_check_true(errored);
}
let m = provider.getMeasurement("counts", 3);
let data = yield m.getValues();
do_check_eq(data.days.size, 1);
do_check_true(data.days.hasDay(now));
let day = data.days.getDay(now);
for (let engine of DEFAULT_ENGINES) {
let identifier = engine.identifier;
let expected = identifier != "yahoo";
for (let source of ["abouthome", "contextmenu", "searchbar", "urlbar"]) {
let field = identifier + "." + source;
if (expected) {
do_check_true(day.has(field));
do_check_eq(day.get(field), 1);
} else {
do_check_false(day.has(field));
}
}
}
// Also, check that our non-default engine contributed, with a computed
// identifier.
let identifier = "notdef";
for (let source of ["abouthome", "contextmenu", "searchbar", "urlbar"]) {
let field = identifier + "." + source;
do_check_true(day.has(field));
}
yield storage.close();
});
add_task(function* test_includes_other_fields() {
let storage = yield Metrics.Storage("includes_other_fields");
let provider = new MockSearchesProvider();
yield provider.init(storage);
let m = provider.getMeasurement("counts", 3);
// Register a search against a provider that isn't live in this session.
let id = yield m.storage.registerField(m.id, "test.searchbar",
Metrics.Storage.FIELD_DAILY_COUNTER);
let testField = "test.searchbar";
let now = new Date();
yield m.storage.incrementDailyCounterFromFieldID(id, now);
// Make sure we don't know about it.
do_check_false(testField in m.fields);
// But we want to include it in payloads.
do_check_true(m.shouldIncludeField(testField));
// And we do so.
let data = yield provider.storage.getMeasurementValues(m.id);
let serializer = m.serializer(m.SERIALIZE_JSON);
let formatted = serializer.daily(data.days.getDay(now));
do_check_true(testField in formatted);
do_check_eq(formatted[testField], 1);
yield storage.close();
});
add_task(function* test_default_search_engine() {
let storage = yield Metrics.Storage("default_search_engine");
let provider = new SearchesProvider();
yield provider.init(storage);
let m = provider.getMeasurement("engines", 2);
let now = new Date();
yield provider.collectDailyData();
let data = yield m.getValues();
Assert.ok(data.days.hasDay(now));
let day = data.days.getDay(now);
Assert.equal(day.size, 1);
Assert.ok(day.has("default"));
// test environment doesn't have a default engine.
Assert.equal(day.get("default"), "NONE");
Services.search.addEngineWithDetails("testdefault",
"http://localhost/icon.png",
null,
"test description",
"GET",
"http://localhost/search/%s");
let engine1 = Services.search.getEngineByName("testdefault");
Assert.ok(engine1);
Services.search.defaultEngine = engine1;
yield provider.collectDailyData();
data = yield m.getValues();
Assert.equal(data.days.getDay(now).get("default"), "other-testdefault");
// If no cohort identifier is set, we shouldn't report a cohort.
Assert.equal(data.days.getDay(now).get("cohort"), undefined);
// Set a cohort identifier and verify we record it.
Services.prefs.setCharPref("browser.search.cohort", "testcohort");
yield provider.collectDailyData();
data = yield m.getValues();
Assert.equal(data.days.getDay(now).get("cohort"), "testcohort");
yield storage.close();
});

View File

@ -0,0 +1,217 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
var {utils: Cu} = Components;
Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource://gre/modules/Metrics.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/services-common/utils.js");
Cu.import("resource://gre/modules/SessionRecorder.jsm");
Cu.import("resource://gre/modules/services/healthreport/providers.jsm");
function run_test() {
run_next_test();
}
add_test(function test_constructor() {
let provider = new SessionsProvider();
run_next_test();
});
add_task(function test_init() {
let storage = yield Metrics.Storage("init");
let provider = new SessionsProvider();
yield provider.init(storage);
yield provider.shutdown();
yield storage.close();
});
function monkeypatchStartupInfo(recorder, start=new Date(), offset=500) {
Object.defineProperty(recorder, "_getStartupInfo", {
value: function _getStartupInfo() {
return {
process: start,
main: new Date(start.getTime() + offset),
firstPaint: new Date(start.getTime() + 2 * offset),
sessionRestored: new Date(start.getTime() + 3 * offset),
};
}
});
}
function sleep(wait) {
let deferred = Promise.defer();
let timer = CommonUtils.namedTimer(function onTimer() {
deferred.resolve();
}, wait, deferred.promise, "_sleepTimer");
return deferred.promise;
}
function getProvider(name, now=new Date(), init=true) {
return Task.spawn(function () {
let storage = yield Metrics.Storage(name);
let provider = new SessionsProvider();
let recorder = new SessionRecorder("testing." + name + ".sessions.");
monkeypatchStartupInfo(recorder, now);
provider.healthReporter = {sessionRecorder: recorder};
recorder.onStartup();
if (init) {
yield provider.init(storage);
}
throw new Task.Result([provider, storage, recorder]);
});
}
add_task(function test_current_session() {
let now = new Date();
let [provider, storage, recorder] = yield getProvider("current_session", now);
yield sleep(25);
recorder.onActivity(true);
let current = provider.getMeasurement("current", 3);
let values = yield current.getValues();
let fields = values.singular;
for (let field of ["startDay", "activeTicks", "totalTime", "main", "firstPaint", "sessionRestored"]) {
do_check_true(fields.has(field));
}
do_check_eq(fields.get("startDay")[1], Metrics.dateToDays(now));
do_check_eq(fields.get("totalTime")[1], recorder.totalTime);
do_check_eq(fields.get("activeTicks")[1], 1);
do_check_eq(fields.get("main")[1], 500);
do_check_eq(fields.get("firstPaint")[1], 1000);
do_check_eq(fields.get("sessionRestored")[1], 1500);
yield provider.shutdown();
yield storage.close();
});
add_task(function test_collect() {
let now = new Date();
let [provider, storage, recorder] = yield getProvider("collect");
recorder.onShutdown();
yield sleep(25);
for (let i = 0; i < 5; i++) {
let recorder2 = new SessionRecorder("testing.collect.sessions.");
recorder2.onStartup();
yield sleep(25);
recorder2.onShutdown();
yield sleep(25);
}
recorder = new SessionRecorder("testing.collect.sessions.");
recorder.onStartup();
// Collecting the provider should prune all previous sessions.
let sessions = recorder.getPreviousSessions();
do_check_eq(Object.keys(sessions).length, 6);
yield provider.collectConstantData();
sessions = recorder.getPreviousSessions();
do_check_eq(Object.keys(sessions).length, 0);
// And those previous sessions should make it to storage.
let daily = provider.getMeasurement("previous", 3);
let values = yield daily.getValues();
do_check_true(values.days.hasDay(now));
do_check_eq(values.days.size, 1);
let day = values.days.getDay(now);
do_check_eq(day.size, 5);
let previousStorageCount = day.get("main").length;
for (let field of ["cleanActiveTicks", "cleanTotalTime", "main", "firstPaint", "sessionRestored"]) {
do_check_true(day.has(field));
do_check_true(Array.isArray(day.get(field)));
do_check_eq(day.get(field).length, 6);
}
let lastIndex = yield provider.getState("lastSession");
do_check_eq(lastIndex, "" + (previousStorageCount - 1)); // 0-indexed
// Fake an aborted session. If we create a 2nd recorder against the same
// prefs branch as a running one, this simulates what would happen if the
// first recorder didn't shut down.
let recorder2 = new SessionRecorder("testing.collect.sessions.");
recorder2.onStartup();
do_check_eq(Object.keys(recorder.getPreviousSessions()).length, 1);
yield provider.collectConstantData();
do_check_eq(Object.keys(recorder.getPreviousSessions()).length, 0);
values = yield daily.getValues();
day = values.days.getDay(now);
do_check_eq(day.size, previousStorageCount + 1);
previousStorageCount = day.get("main").length;
for (let field of ["abortedActiveTicks", "abortedTotalTime"]) {
do_check_true(day.has(field));
do_check_true(Array.isArray(day.get(field)));
do_check_eq(day.get(field).length, 1);
}
lastIndex = yield provider.getState("lastSession");
do_check_eq(lastIndex, "" + (previousStorageCount - 1));
recorder.onShutdown();
recorder2.onShutdown();
// If we try to insert a already-inserted session, it will be ignored.
recorder = new SessionRecorder("testing.collect.sessions.");
recorder._currentIndex = recorder._currentIndex - 1;
recorder._prunedIndex = recorder._currentIndex;
recorder.onStartup();
// Session is left over from recorder2.
sessions = recorder.getPreviousSessions();
do_check_eq(Object.keys(sessions).length, 1);
do_check_true(previousStorageCount - 1 in sessions);
yield provider.collectConstantData();
lastIndex = yield provider.getState("lastSession");
do_check_eq(lastIndex, "" + (previousStorageCount - 1));
values = yield daily.getValues();
day = values.days.getDay(now);
// We should not get additional entry.
do_check_eq(day.get("main").length, previousStorageCount);
recorder.onShutdown();
yield provider.shutdown();
yield storage.close();
});
add_task(function test_serialization() {
let [provider, storage, recorder] = yield getProvider("serialization");
yield sleep(1025);
recorder.onActivity(true);
let current = provider.getMeasurement("current", 3);
let data = yield current.getValues();
do_check_true("singular" in data);
let serializer = current.serializer(current.SERIALIZE_JSON);
let fields = serializer.singular(data.singular);
do_check_eq(fields._v, 3);
do_check_eq(fields.activeTicks, 1);
do_check_eq(fields.startDay, Metrics.dateToDays(recorder.startDate));
do_check_eq(fields.main, 500);
do_check_eq(fields.firstPaint, 1000);
do_check_eq(fields.sessionRestored, 1500);
do_check_true(fields.totalTime > 0);
yield provider.shutdown();
yield storage.close();
});

View File

@ -0,0 +1,41 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
var {interfaces: Ci, results: Cr, utils: Cu} = Components;
Cu.import("resource://gre/modules/Metrics.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/services/healthreport/providers.jsm");
function run_test() {
run_next_test();
}
add_test(function test_constructor() {
let provider = new SysInfoProvider();
run_next_test();
});
add_task(function test_collect_smoketest() {
let storage = yield Metrics.Storage("collect_smoketest");
let provider = new SysInfoProvider();
yield provider.init(storage);
yield provider.collectConstantData();
let m = provider.getMeasurement("sysinfo", 2);
let data = yield storage.getMeasurementValues(m.id);
let serializer = m.serializer(m.SERIALIZE_JSON);
let d = serializer.singular(data.singular);
do_check_eq(d._v, 2);
do_check_true(d.cpuCount > 0);
do_check_neq(d.name, null);
yield storage.close();
});

View File

@ -0,0 +1,20 @@
[DEFAULT]
head = head.js
tail =
skip-if = toolkit == 'android' || toolkit == 'gonk'
[test_load_modules.js]
[test_profile.js]
[test_healthreporter.js]
[test_provider_addons.js]
skip-if = buildapp == 'mulet'
tags = addons
[test_provider_appinfo.js]
[test_provider_crashes.js]
skip-if = !crashreporter
[test_provider_hotfix.js]
[test_provider_places.js]
[test_provider_searches.js]
[test_provider_sysinfo.js]
[test_provider_sessions.js]

View File

@ -0,0 +1,38 @@
/* 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/. */
#ifndef MERGED_COMPARTMENT
"use strict";
this.EXPORTED_SYMBOLS = ["Metrics"];
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
#endif
// We concatenate the JSMs together to eliminate compartment overhead.
// This is a giant hack until compartment overhead is no longer an
// issue.
#define MERGED_COMPARTMENT
#include providermanager.jsm
;
#include dataprovider.jsm
;
#include storage.jsm
;
this.Metrics = {
ProviderManager: ProviderManager,
DailyValues: DailyValues,
Measurement: Measurement,
Provider: Provider,
Storage: MetricsStorageBackend,
dateToDays: dateToDays,
daysToDate: daysToDate,
};

View File

@ -0,0 +1,727 @@
/* 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/. */
#ifndef MERGED_COMPARTMENT
"use strict";
this.EXPORTED_SYMBOLS = [
"Measurement",
"Provider",
];
const {utils: Cu} = Components;
const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
#endif
Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource://gre/modules/Preferences.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-common/utils.js");
/**
* Represents a collection of related pieces/fields of data.
*
* This is an abstract base type.
*
* This type provides the primary interface for storing, retrieving, and
* serializing data.
*
* Each measurement consists of a set of named fields. Each field is primarily
* identified by a string name, which must be unique within the measurement.
*
* Each derived type must define the following properties:
*
* name -- String name of this measurement. This is the primary way
* measurements are distinguished within a provider.
*
* version -- Integer version of this measurement. This is a secondary
* identifier for a measurement within a provider. The version denotes
* the behavior of this measurement and the composition of its fields over
* time. When a new field is added or the behavior of an existing field
* changes, the version should be incremented. The initial version of a
* measurement is typically 1.
*
* fields -- Object defining the fields this measurement holds. Keys in the
* object are string field names. Values are objects describing how the
* field works. The following properties are recognized:
*
* type -- The string type of this field. This is typically one of the
* FIELD_* constants from the Metrics.Storage type.
*
*
* FUTURE: provide hook points for measurements to supplement with custom
* storage needs.
*/
this.Measurement = function () {
if (!this.name) {
throw new Error("Measurement must have a name.");
}
if (!this.version) {
throw new Error("Measurement must have a version.");
}
if (!Number.isInteger(this.version)) {
throw new Error("Measurement's version must be an integer: " + this.version);
}
if (!this.fields) {
throw new Error("Measurement must define fields.");
}
for (let [name, info] in Iterator(this.fields)) {
if (!info) {
throw new Error("Field does not contain metadata: " + name);
}
if (!info.type) {
throw new Error("Field is missing required type property: " + name);
}
}
this._log = Log.repository.getLogger("Services.Metrics.Measurement." + this.name);
this.id = null;
this.storage = null;
this._fields = {};
this._serializers = {};
this._serializers[this.SERIALIZE_JSON] = {
singular: this._serializeJSONSingular.bind(this),
daily: this._serializeJSONDay.bind(this),
};
}
Measurement.prototype = Object.freeze({
SERIALIZE_JSON: "json",
/**
* Obtain a serializer for this measurement.
*
* Implementations should return an object with the following keys:
*
* singular -- Serializer for singular data.
* daily -- Serializer for daily data.
*
* Each item is a function that takes a single argument: the data to
* serialize. The passed data is a subset of that returned from
* this.getValues(). For "singular," data.singular is passed. For "daily",
* data.days.get(<day>) is passed.
*
* This function receives a single argument: the serialization format we
* are requesting. This is one of the SERIALIZE_* constants on this base type.
*
* For SERIALIZE_JSON, the function should return an object that
* JSON.stringify() knows how to handle. This could be an anonymous object or
* array or any object with a property named `toJSON` whose value is a
* function. The returned object will be added to a larger document
* containing the results of all `serialize` calls.
*
* The default implementation knows how to serialize built-in types using
* very simple logic. If small encoding size is a goal, the default
* implementation may not be suitable. If an unknown field type is
* encountered, the default implementation will error.
*
* @param format
* (string) A SERIALIZE_* constant defining what serialization format
* to use.
*/
serializer: function (format) {
if (!(format in this._serializers)) {
throw new Error("Don't know how to serialize format: " + format);
}
return this._serializers[format];
},
/**
* Whether this measurement contains the named field.
*
* @param name
* (string) Name of field.
*
* @return bool
*/
hasField: function (name) {
return name in this.fields;
},
/**
* The unique identifier for a named field.
*
* This will throw if the field is not known.
*
* @param name
* (string) Name of field.
*/
fieldID: function (name) {
let entry = this._fields[name];
if (!entry) {
throw new Error("Unknown field: " + name);
}
return entry[0];
},
fieldType: function (name) {
let entry = this._fields[name];
if (!entry) {
throw new Error("Unknown field: " + name);
}
return entry[1];
},
_configureStorage: function () {
let missing = [];
for (let [name, info] in Iterator(this.fields)) {
if (this.storage.hasFieldFromMeasurement(this.id, name)) {
this._fields[name] =
[this.storage.fieldIDFromMeasurement(this.id, name), info.type];
continue;
}
missing.push([name, info.type]);
}
if (!missing.length) {
return CommonUtils.laterTickResolvingPromise();
}
// We only perform a transaction if we have work to do (to avoid
// extra SQLite overhead).
return this.storage.enqueueTransaction(function registerFields() {
for (let [name, type] of missing) {
this._log.debug("Registering field: " + name + " " + type);
let id = yield this.storage.registerField(this.id, name, type);
this._fields[name] = [id, type];
}
}.bind(this));
},
//---------------------------------------------------------------------------
// Data Recording Functions
//
// Functions in this section are used to record new values against this
// measurement instance.
//
// Generally speaking, these functions will throw if the specified field does
// not exist or if the storage function requested is not appropriate for the
// type of that field. These functions will also return a promise that will
// be resolved when the underlying storage operation has completed.
//---------------------------------------------------------------------------
/**
* Increment a daily counter field in this measurement by 1.
*
* By default, the counter for the current day will be incremented.
*
* If the field is not known or is not a daily counter, this will throw.
*
*
*
* @param field
* (string) The name of the field whose value to increment.
* @param date
* (Date) Day on which to increment the counter.
* @param by
* (integer) How much to increment by.
* @return Promise<>
*/
incrementDailyCounter: function (field, date=new Date(), by=1) {
return this.storage.incrementDailyCounterFromFieldID(this.fieldID(field),
date, by);
},
/**
* Record a new numeric value for a daily discrete numeric field.
*
* @param field
* (string) The name of the field to append a value to.
* @param value
* (Number) Number to append.
* @param date
* (Date) Day on which to append the value.
*
* @return Promise<>
*/
addDailyDiscreteNumeric: function (field, value, date=new Date()) {
return this.storage.addDailyDiscreteNumericFromFieldID(
this.fieldID(field), value, date);
},
/**
* Record a new text value for a daily discrete text field.
*
* This is like `addDailyDiscreteNumeric` but for daily discrete text fields.
*/
addDailyDiscreteText: function (field, value, date=new Date()) {
return this.storage.addDailyDiscreteTextFromFieldID(
this.fieldID(field), value, date);
},
/**
* Record the last seen value for a last numeric field.
*
* @param field
* (string) The name of the field to set the value of.
* @param value
* (Number) The value to set.
* @param date
* (Date) When this value was recorded.
*
* @return Promise<>
*/
setLastNumeric: function (field, value, date=new Date()) {
return this.storage.setLastNumericFromFieldID(this.fieldID(field), value,
date);
},
/**
* Record the last seen value for a last text field.
*
* This is like `setLastNumeric` except for last text fields.
*/
setLastText: function (field, value, date=new Date()) {
return this.storage.setLastTextFromFieldID(this.fieldID(field), value,
date);
},
/**
* Record the most recent value for a daily last numeric field.
*
* @param field
* (string) The name of a daily last numeric field.
* @param value
* (Number) The value to set.
* @param date
* (Date) Day on which to record the last value.
*
* @return Promise<>
*/
setDailyLastNumeric: function (field, value, date=new Date()) {
return this.storage.setDailyLastNumericFromFieldID(this.fieldID(field),
value, date);
},
/**
* Record the most recent value for a daily last text field.
*
* This is like `setDailyLastNumeric` except for a daily last text field.
*/
setDailyLastText: function (field, value, date=new Date()) {
return this.storage.setDailyLastTextFromFieldID(this.fieldID(field),
value, date);
},
//---------------------------------------------------------------------------
// End of data recording APIs.
//---------------------------------------------------------------------------
/**
* Obtain all values stored for this measurement.
*
* The default implementation obtains all known types from storage. If the
* measurement provides custom types or stores values somewhere other than
* storage, it should define its own implementation.
*
* This returns a promise that resolves to a data structure which is
* understood by the measurement's serialize() function.
*/
getValues: function () {
return this.storage.getMeasurementValues(this.id);
},
deleteLastNumeric: function (field) {
return this.storage.deleteLastNumericFromFieldID(this.fieldID(field));
},
deleteLastText: function (field) {
return this.storage.deleteLastTextFromFieldID(this.fieldID(field));
},
/**
* This method is used by the default serializers to control whether a field
* is included in the output.
*
* There could be legacy fields in storage we no longer care about.
*
* This method is a hook to allow measurements to change this behavior, e.g.,
* to implement a dynamic fieldset.
*
* You will also need to override `fieldType`.
*
* @return (boolean) true if the specified field should be included in
* payload output.
*/
shouldIncludeField: function (field) {
return field in this._fields;
},
_serializeJSONSingular: function (data) {
let result = {"_v": this.version};
for (let [field, value] of data) {
// There could be legacy fields in storage we no longer care about.
if (!this.shouldIncludeField(field)) {
continue;
}
let type = this.fieldType(field);
switch (type) {
case this.storage.FIELD_LAST_NUMERIC:
case this.storage.FIELD_LAST_TEXT:
result[field] = value[1];
break;
case this.storage.FIELD_DAILY_COUNTER:
case this.storage.FIELD_DAILY_DISCRETE_NUMERIC:
case this.storage.FIELD_DAILY_DISCRETE_TEXT:
case this.storage.FIELD_DAILY_LAST_NUMERIC:
case this.storage.FIELD_DAILY_LAST_TEXT:
continue;
default:
throw new Error("Unknown field type: " + type);
}
}
return result;
},
_serializeJSONDay: function (data) {
let result = {"_v": this.version};
for (let [field, value] of data) {
if (!this.shouldIncludeField(field)) {
continue;
}
let type = this.fieldType(field);
switch (type) {
case this.storage.FIELD_DAILY_COUNTER:
case this.storage.FIELD_DAILY_DISCRETE_NUMERIC:
case this.storage.FIELD_DAILY_DISCRETE_TEXT:
case this.storage.FIELD_DAILY_LAST_NUMERIC:
case this.storage.FIELD_DAILY_LAST_TEXT:
result[field] = value;
break;
case this.storage.FIELD_LAST_NUMERIC:
case this.storage.FIELD_LAST_TEXT:
continue;
default:
throw new Error("Unknown field type: " + type);
}
}
return result;
},
});
/**
* An entity that emits data.
*
* A `Provider` consists of a string name (must be globally unique among all
* known providers) and a set of `Measurement` instances.
*
* The main role of a `Provider` is to produce metrics data and to store said
* data in the storage backend.
*
* Metrics data collection is initiated either by a manager calling a
* `collect*` function on `Provider` instances or by the `Provider` registering
* to some external event and then reacting whenever they occur.
*
* `Provider` implementations interface directly with a storage backend. For
* common stored values (daily counters, daily discrete values, etc),
* implementations should interface with storage via the various helper
* functions on the `Measurement` instances. For custom stored value types,
* implementations will interact directly with the low-level storage APIs.
*
* Because multiple providers exist and could be responding to separate
* external events simultaneously and because not all operations performed by
* storage can safely be performed in parallel, writing directly to storage at
* event time is dangerous. Therefore, interactions with storage must be
* deferred until it is safe to perform them.
*
* This typically looks something like:
*
* // This gets called when an external event worthy of recording metrics
* // occurs. The function receives a numeric value associated with the event.
* function onExternalEvent (value) {
* let now = new Date();
* let m = this.getMeasurement("foo", 1);
*
* this.enqueueStorageOperation(function storeExternalEvent() {
*
* // We interface with storage via the `Measurement` helper functions.
* // These each return a promise that will be resolved when the
* // operation finishes. We rely on behavior of storage where operations
* // are executed single threaded and sequentially. Therefore, we only
* // need to return the final promise.
* m.incrementDailyCounter("foo", now);
* return m.addDailyDiscreteNumericValue("my_value", value, now);
* }.bind(this));
*
* }
*
*
* `Provider` is an abstract base class. Implementations must define a few
* properties:
*
* name
* The `name` property should be a string defining the provider's name. The
* name must be globally unique for the application. The name is used as an
* identifier to distinguish providers from each other.
*
* measurementTypes
* This must be an array of `Measurement`-derived types. Note that elements
* in the array are the type functions, not instances. Instances of the
* `Measurement` are created at run-time by the `Provider` and are bound
* to the provider and to a specific storage backend.
*/
this.Provider = function () {
if (!this.name) {
throw new Error("Provider must define a name.");
}
if (!Array.isArray(this.measurementTypes)) {
throw new Error("Provider must define measurement types.");
}
this._log = Log.repository.getLogger("Services.Metrics.Provider." + this.name);
this.measurements = null;
this.storage = null;
}
Provider.prototype = Object.freeze({
/**
* Whether the provider only pulls data from other sources.
*
* If this is true, the provider pulls data from other sources. By contrast,
* "push-based" providers subscribe to foreign sources and record/react to
* external events as they happen.
*
* Pull-only providers likely aren't instantiated until a data collection
* is performed. Thus, implementations cannot rely on a provider instance
* always being alive. This is an optimization so provider instances aren't
* dead weight while the application is running.
*
* This must be set on the prototype to have an effect.
*/
pullOnly: false,
/**
* Obtain a `Measurement` from its name and version.
*
* If the measurement is not found, an Error is thrown.
*/
getMeasurement: function (name, version) {
if (!Number.isInteger(version)) {
throw new Error("getMeasurement expects an integer version. Got: " + version);
}
let m = this.measurements.get([name, version].join(":"));
if (!m) {
throw new Error("Unknown measurement: " + name + " v" + version);
}
return m;
},
init: function (storage) {
if (this.storage !== null) {
throw new Error("Provider() not called. Did the sub-type forget to call it?");
}
if (this.storage) {
throw new Error("Provider has already been initialized.");
}
this.measurements = new Map();
this.storage = storage;
let self = this;
return Task.spawn(function init() {
let pre = self.preInit();
if (!pre || typeof(pre.then) != "function") {
throw new Error("preInit() does not return a promise.");
}
yield pre;
for (let measurementType of self.measurementTypes) {
let measurement = new measurementType();
measurement.provider = self;
measurement.storage = self.storage;
let id = yield storage.registerMeasurement(self.name, measurement.name,
measurement.version);
measurement.id = id;
yield measurement._configureStorage();
self.measurements.set([measurement.name, measurement.version].join(":"),
measurement);
}
let post = self.postInit();
if (!post || typeof(post.then) != "function") {
throw new Error("postInit() does not return a promise.");
}
yield post;
});
},
shutdown: function () {
let promise = this.onShutdown();
if (!promise || typeof(promise.then) != "function") {
throw new Error("onShutdown implementation does not return a promise.");
}
return promise;
},
/**
* Hook point for implementations to perform pre-initialization activity.
*
* This method will be called before measurement registration.
*
* Implementations should return a promise which is resolved when
* initialization activities have completed.
*/
preInit: function () {
return CommonUtils.laterTickResolvingPromise();
},
/**
* Hook point for implementations to perform post-initialization activity.
*
* This method will be called after `preInit` and measurement registration,
* but before initialization is finished.
*
* If a `Provider` instance needs to register observers, etc, it should
* implement this function.
*
* Implementations should return a promise which is resolved when
* initialization activities have completed.
*/
postInit: function () {
return CommonUtils.laterTickResolvingPromise();
},
/**
* Hook point for shutdown of instances.
*
* This is the opposite of `onInit`. If a `Provider` needs to unregister
* observers, etc, this is where it should do it.
*
* Implementations should return a promise which is resolved when
* shutdown activities have completed.
*/
onShutdown: function () {
return CommonUtils.laterTickResolvingPromise();
},
/**
* Collects data that doesn't change during the application's lifetime.
*
* Implementations should return a promise that resolves when all data has
* been collected and storage operations have been finished.
*
* @return Promise<>
*/
collectConstantData: function () {
return CommonUtils.laterTickResolvingPromise();
},
/**
* Collects data approximately every day.
*
* For long-running applications, this is called approximately every day.
* It may or may not be called every time the application is run. It also may
* be called more than once per day.
*
* Implementations should return a promise that resolves when all data has
* been collected and storage operations have completed.
*
* @return Promise<>
*/
collectDailyData: function () {
return CommonUtils.laterTickResolvingPromise();
},
/**
* Queue a deferred storage operation.
*
* Deferred storage operations are the preferred method for providers to
* interact with storage. When collected data is to be added to storage,
* the provider creates a function that performs the necessary storage
* interactions and then passes that function to this function. Pending
* storage operations will be executed sequentially by a coordinator.
*
* The passed function should return a promise which will be resolved upon
* completion of storage interaction.
*/
enqueueStorageOperation: function (func) {
return this.storage.enqueueOperation(func);
},
/**
* Obtain persisted provider state.
*
* Provider state consists of key-value pairs of string names and values.
* Providers can stuff whatever they want into state. They are encouraged to
* store as little as possible for performance reasons.
*
* State is backed by storage and is robust.
*
* These functions do not enqueue on storage automatically, so they should
* be guarded by `enqueueStorageOperation` or some other mutex.
*
* @param key
* (string) The property to retrieve.
*
* @return Promise<string|null> String value on success. null if no state
* is available under this key.
*/
getState: function (key) {
return this.storage.getProviderState(this.name, key);
},
/**
* Set state for this provider.
*
* This is the complementary API for `getState` and obeys the same
* storage restrictions.
*/
setState: function (key, value) {
return this.storage.setProviderState(this.name, key, value);
},
_dateToDays: function (date) {
return Math.floor(date.getTime() / MILLISECONDS_PER_DAY);
},
_daysToDate: function (days) {
return new Date(days * MILLISECONDS_PER_DAY);
},
});

View File

@ -0,0 +1,154 @@
/* 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 = [
"DummyMeasurement",
"DummyProvider",
"DummyConstantProvider",
"DummyPullOnlyThrowsOnInitProvider",
"DummyThrowOnInitProvider",
"DummyThrowOnShutdownProvider",
];
const {utils: Cu} = Components;
Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource://gre/modules/Metrics.jsm");
Cu.import("resource://gre/modules/Task.jsm");
this.DummyMeasurement = function DummyMeasurement(name="DummyMeasurement") {
this.name = name;
Metrics.Measurement.call(this);
}
DummyMeasurement.prototype = {
__proto__: Metrics.Measurement.prototype,
version: 1,
fields: {
"daily-counter": {type: Metrics.Storage.FIELD_DAILY_COUNTER},
"daily-discrete-numeric": {type: Metrics.Storage.FIELD_DAILY_DISCRETE_NUMERIC},
"daily-discrete-text": {type: Metrics.Storage.FIELD_DAILY_DISCRETE_TEXT},
"daily-last-numeric": {type: Metrics.Storage.FIELD_DAILY_LAST_NUMERIC},
"daily-last-text": {type: Metrics.Storage.FIELD_DAILY_LAST_TEXT},
"last-numeric": {type: Metrics.Storage.FIELD_LAST_NUMERIC},
"last-text": {type: Metrics.Storage.FIELD_LAST_TEXT},
},
};
this.DummyProvider = function DummyProvider(name="DummyProvider") {
Object.defineProperty(this, "name", {
value: name,
});
this.measurementTypes = [DummyMeasurement];
Metrics.Provider.call(this);
this.constantMeasurementName = "DummyMeasurement";
this.collectConstantCount = 0;
this.throwDuringCollectConstantData = null;
this.throwDuringConstantPopulate = null;
this.collectDailyCount = 0;
this.havePushedMeasurements = true;
}
DummyProvider.prototype = {
__proto__: Metrics.Provider.prototype,
name: "DummyProvider",
collectConstantData: function () {
this.collectConstantCount++;
if (this.throwDuringCollectConstantData) {
throw new Error(this.throwDuringCollectConstantData);
}
return this.enqueueStorageOperation(function doStorage() {
if (this.throwDuringConstantPopulate) {
throw new Error(this.throwDuringConstantPopulate);
}
let m = this.getMeasurement("DummyMeasurement", 1);
let now = new Date();
m.incrementDailyCounter("daily-counter", now);
m.addDailyDiscreteNumeric("daily-discrete-numeric", 1, now);
m.addDailyDiscreteNumeric("daily-discrete-numeric", 2, now);
m.addDailyDiscreteText("daily-discrete-text", "foo", now);
m.addDailyDiscreteText("daily-discrete-text", "bar", now);
m.setDailyLastNumeric("daily-last-numeric", 3, now);
m.setDailyLastText("daily-last-text", "biz", now);
m.setLastNumeric("last-numeric", 4, now);
return m.setLastText("last-text", "bazfoo", now);
}.bind(this));
},
collectDailyData: function () {
this.collectDailyCount++;
return Promise.resolve();
},
};
this.DummyConstantProvider = function () {
DummyProvider.call(this, this.name);
}
DummyConstantProvider.prototype = {
__proto__: DummyProvider.prototype,
name: "DummyConstantProvider",
pullOnly: true,
};
this.DummyThrowOnInitProvider = function () {
DummyProvider.call(this, "DummyThrowOnInitProvider");
throw new Error("Dummy Error");
};
this.DummyThrowOnInitProvider.prototype = {
__proto__: DummyProvider.prototype,
name: "DummyThrowOnInitProvider",
};
this.DummyPullOnlyThrowsOnInitProvider = function () {
DummyConstantProvider.call(this);
throw new Error("Dummy Error");
};
this.DummyPullOnlyThrowsOnInitProvider.prototype = {
__proto__: DummyConstantProvider.prototype,
name: "DummyPullOnlyThrowsOnInitProvider",
};
this.DummyThrowOnShutdownProvider = function () {
DummyProvider.call(this, "DummyThrowOnShutdownProvider");
};
this.DummyThrowOnShutdownProvider.prototype = {
__proto__: DummyProvider.prototype,
name: "DummyThrowOnShutdownProvider",
pullOnly: true,
onShutdown: function () {
throw new Error("Dummy shutdown error");
},
};

View File

@ -0,0 +1,23 @@
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# 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/.
XPCSHELL_TESTS_MANIFESTS += ['tests/xpcshell/xpcshell.ini']
# We install Metrics.jsm into the "main" JSM repository and the rest in
# services. External consumers should only go through Metrics.jsm.
EXTRA_PP_JS_MODULES += [
'Metrics.jsm',
]
EXTRA_PP_JS_MODULES.services.metrics += [
'dataprovider.jsm',
'providermanager.jsm',
'storage.jsm',
]
TESTING_JS_MODULES.services.metrics += [
'modules-testing/mocks.jsm',
]

View File

@ -0,0 +1,558 @@
/* 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/. */
#ifndef MERGED_COMPARTMENT
"use strict";
this.EXPORTED_SYMBOLS = ["ProviderManager"];
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://gre/modules/services/metrics/dataprovider.jsm");
#endif
Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-common/utils.js");
/**
* Handles and coordinates the collection of metrics data from providers.
*
* This provides an interface for managing `Metrics.Provider` instances. It
* provides APIs for bulk collection of data.
*/
this.ProviderManager = function (storage) {
this._log = Log.repository.getLogger("Services.Metrics.ProviderManager");
this._providers = new Map();
this._storage = storage;
this._providerInitQueue = [];
this._providerInitializing = false;
this._pullOnlyProviders = {};
this._pullOnlyProvidersRegisterCount = 0;
this._pullOnlyProvidersState = this.PULL_ONLY_NOT_REGISTERED;
this._pullOnlyProvidersCurrentPromise = null;
// Callback to allow customization of providers after they are constructed
// but before they call out into their initialization code.
this.onProviderInit = null;
}
this.ProviderManager.prototype = Object.freeze({
PULL_ONLY_NOT_REGISTERED: "none",
PULL_ONLY_REGISTERING: "registering",
PULL_ONLY_UNREGISTERING: "unregistering",
PULL_ONLY_REGISTERED: "registered",
get providers() {
let providers = [];
for (let [name, entry] of this._providers) {
providers.push(entry.provider);
}
return providers;
},
/**
* Obtain a provider from its name.
*/
getProvider: function (name) {
let provider = this._providers.get(name);
if (!provider) {
return null;
}
return provider.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 `Metrics.Provider` 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-default FooProvider resource://gre/modules/foo.jsm
* category healthreport-js-provider-nightly EyeballProvider resource://gre/modules/eyeball.jsm
*
* Then to load them:
*
* let reporter = getHealthReporter("healthreport.");
* reporter.registerProvidersFromCategoryManager("healthreport-js-provider-default");
*
* If the category has no defined members, this call has no effect, and no error is raised.
*
* @param category
* (string) Name of category from which to query and load.
* @param providerDiagnostic
* (function) Optional, called with the name of the provider currently being initialized.
* @return a newly spawned Task.
*/
registerProvidersFromCategoryManager: function (category, providerDiagnostic) {
this._log.info("Registering providers from category: " + category);
let cm = Cc["@mozilla.org/categorymanager;1"]
.getService(Ci.nsICategoryManager);
let promiseList = [];
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 promise = this.registerProviderFromType(ns[entry]);
if (promise) {
promiseList.push({name: entry, promise: promise});
}
} catch (ex) {
this._recordProviderError(entry,
"Error registering provider from category manager",
ex);
continue;
}
}
return Task.spawn(function* wait() {
for (let entry of promiseList) {
if (providerDiagnostic) {
providerDiagnostic(entry.name);
}
yield entry.promise;
}
});
},
/**
* Registers a `MetricsProvider` with this manager.
*
* Once a `MetricsProvider` is registered, data will be collected from it
* whenever we collect data.
*
* The returned value is a promise that will be resolved once registration
* is complete.
*
* Providers are initialized as part of registration by calling
* provider.init().
*
* @param provider
* (Metrics.Provider) The provider instance to register.
*
* @return Promise<null>
*/
registerProvider: function (provider) {
// We should perform an instanceof check here. However, due to merged
// compartments, the Provider type may belong to one of two JSMs
// isinstance gets confused depending on which module Provider comes
// from. Some code references Provider from dataprovider.jsm; others from
// Metrics.jsm.
if (!provider.name) {
throw new Error("Provider is not valid: does not have a name.");
}
if (this._providers.has(provider.name)) {
return CommonUtils.laterTickResolvingPromise();
}
let deferred = Promise.defer();
this._providerInitQueue.push([provider, deferred]);
if (this._providerInitQueue.length == 1) {
this._popAndInitProvider();
}
return deferred.promise;
},
/**
* Registers a provider from its constructor function.
*
* If the provider is pull-only, it will be stashed away and
* initialized later. Null will be returned.
*
* If it is not pull-only, it will be initialized immediately and a
* promise will be returned. The promise will be resolved when the
* provider has finished initializing.
*/
registerProviderFromType: function (type) {
let proto = type.prototype;
if (proto.pullOnly) {
this._log.info("Provider is pull-only. Deferring initialization: " +
proto.name);
this._pullOnlyProviders[proto.name] = type;
return null;
}
let provider = this._initProviderFromType(type);
return this.registerProvider(provider);
},
/**
* Initializes a provider from its type.
*
* This is how a constructor function should be turned into a provider
* instance.
*
* A side-effect is the provider is registered with the manager.
*/
_initProviderFromType: function (type) {
let provider = new type();
if (this.onProviderInit) {
this.onProviderInit(provider);
}
return provider;
},
/**
* Remove a named provider from the manager.
*
* It is the caller's responsibility to shut down the provider
* instance.
*/
unregisterProvider: function (name) {
this._providers.delete(name);
},
/**
* Ensure that pull-only providers are registered.
*/
ensurePullOnlyProvidersRegistered: function () {
let state = this._pullOnlyProvidersState;
this._pullOnlyProvidersRegisterCount++;
if (state == this.PULL_ONLY_REGISTERED) {
this._log.debug("Requested pull-only provider registration and " +
"providers are already registered.");
return CommonUtils.laterTickResolvingPromise();
}
// If we're in the process of registering, chain off that request.
if (state == this.PULL_ONLY_REGISTERING) {
this._log.debug("Requested pull-only provider registration and " +
"registration is already in progress.");
return this._pullOnlyProvidersCurrentPromise;
}
this._log.debug("Pull-only provider registration requested.");
// A side-effect of setting this is that an active unregistration will
// effectively short circuit and finish as soon as the in-flight
// unregistration (if any) finishes.
this._pullOnlyProvidersState = this.PULL_ONLY_REGISTERING;
let inFlightPromise = this._pullOnlyProvidersCurrentPromise;
this._pullOnlyProvidersCurrentPromise =
Task.spawn(function registerPullProviders() {
if (inFlightPromise) {
this._log.debug("Waiting for in-flight pull-only provider activity " +
"to finish before registering.");
try {
yield inFlightPromise;
} catch (ex) {
this._log.warn("Error when waiting for existing pull-only promise", ex);
}
}
for (let name in this._pullOnlyProviders) {
let providerType = this._pullOnlyProviders[name];
// Short-circuit if we're no longer registering.
if (this._pullOnlyProvidersState != this.PULL_ONLY_REGISTERING) {
this._log.debug("Aborting pull-only provider registration.");
break;
}
try {
let provider = this._initProviderFromType(providerType);
// This is a no-op if the provider is already registered. So, the
// only overhead is constructing an instance. This should be cheap
// and isn't worth optimizing.
yield this.registerProvider(provider);
} catch (ex) {
this._recordProviderError(providerType.prototype.name,
"Error registering pull-only provider",
ex);
}
}
// It's possible we changed state while registering. Only mark as
// registered if we didn't change state.
if (this._pullOnlyProvidersState == this.PULL_ONLY_REGISTERING) {
this._pullOnlyProvidersState = this.PULL_ONLY_REGISTERED;
this._pullOnlyProvidersCurrentPromise = null;
}
}.bind(this));
return this._pullOnlyProvidersCurrentPromise;
},
ensurePullOnlyProvidersUnregistered: function () {
let state = this._pullOnlyProvidersState;
// If we're not registered, this is a no-op.
if (state == this.PULL_ONLY_NOT_REGISTERED) {
this._log.debug("Requested pull-only provider unregistration but none " +
"are registered.");
return CommonUtils.laterTickResolvingPromise();
}
// If we're currently unregistering, recycle the promise from last time.
if (state == this.PULL_ONLY_UNREGISTERING) {
this._log.debug("Requested pull-only provider unregistration and " +
"unregistration is in progress.");
this._pullOnlyProvidersRegisterCount =
Math.max(0, this._pullOnlyProvidersRegisterCount - 1);
return this._pullOnlyProvidersCurrentPromise;
}
// We ignore this request while multiple entities have requested
// registration because we don't want a request from an "inner,"
// short-lived request to overwrite the desire of the "parent,"
// longer-lived request.
if (this._pullOnlyProvidersRegisterCount > 1) {
this._log.debug("Requested pull-only provider unregistration while " +
"other callers still want them registered. Ignoring.");
this._pullOnlyProvidersRegisterCount--;
return CommonUtils.laterTickResolvingPromise();
}
// We are either fully registered or registering with a single consumer.
// In both cases we are authoritative and can commence unregistration.
this._log.debug("Pull-only providers being unregistered.");
this._pullOnlyProvidersRegisterCount =
Math.max(0, this._pullOnlyProvidersRegisterCount - 1);
this._pullOnlyProvidersState = this.PULL_ONLY_UNREGISTERING;
let inFlightPromise = this._pullOnlyProvidersCurrentPromise;
this._pullOnlyProvidersCurrentPromise =
Task.spawn(function unregisterPullProviders() {
if (inFlightPromise) {
this._log.debug("Waiting for in-flight pull-only provider activity " +
"to complete before unregistering.");
try {
yield inFlightPromise;
} catch (ex) {
this._log.warn("Error when waiting for existing pull-only promise", ex);
}
}
for (let provider of this.providers) {
if (this._pullOnlyProvidersState != this.PULL_ONLY_UNREGISTERING) {
return;
}
if (!provider.pullOnly) {
continue;
}
this._log.info("Shutting down pull-only provider: " +
provider.name);
try {
yield provider.shutdown();
} catch (ex) {
this._recordProviderError(provider.name,
"Error when shutting down provider",
ex);
} finally {
this.unregisterProvider(provider.name);
}
}
if (this._pullOnlyProvidersState == this.PULL_ONLY_UNREGISTERING) {
this._pullOnlyProvidersState = this.PULL_ONLY_NOT_REGISTERED;
this._pullOnlyProvidersCurrentPromise = null;
}
}.bind(this));
return this._pullOnlyProvidersCurrentPromise;
},
_popAndInitProvider: function () {
if (!this._providerInitQueue.length || this._providerInitializing) {
return;
}
let [provider, deferred] = this._providerInitQueue.shift();
this._providerInitializing = true;
this._log.info("Initializing provider with storage: " + provider.name);
Task.spawn(function initProvider() {
try {
let result = yield provider.init(this._storage);
this._log.info("Provider successfully initialized: " + provider.name);
this._providers.set(provider.name, {
provider: provider,
constantsCollected: false,
});
deferred.resolve(result);
} catch (ex) {
this._recordProviderError(provider.name, "Failed to initialize", ex);
deferred.reject(ex);
} finally {
this._providerInitializing = false;
this._popAndInitProvider();
}
}.bind(this));
},
/**
* Collects all constant measurements from all providers.
*
* Returns a Promise that will be fulfilled once all data providers have
* provided their constant data. A side-effect of this promise fulfillment
* is that the manager is populated with the obtained collection results.
* The resolved value to the promise is this `ProviderManager` instance.
*
* @param providerDiagnostic
* (function) Optional, called with the name of the provider currently being initialized.
*/
collectConstantData: function (providerDiagnostic=null) {
let entries = [];
for (let [name, entry] of this._providers) {
if (entry.constantsCollected) {
this._log.trace("Provider has already provided constant data: " +
name);
continue;
}
entries.push(entry);
}
let onCollect = function (entry, result) {
entry.constantsCollected = true;
};
return this._callCollectOnProviders(entries, "collectConstantData",
onCollect, providerDiagnostic);
},
/**
* Calls collectDailyData on all providers.
*/
collectDailyData: function (providerDiagnostic=null) {
return this._callCollectOnProviders(this._providers.values(),
"collectDailyData",
null,
providerDiagnostic);
},
_callCollectOnProviders: function (entries, fnProperty, onCollect=null, providerDiagnostic=null) {
let promises = [];
for (let entry of entries) {
let provider = entry.provider;
let collectPromise;
try {
collectPromise = provider[fnProperty].call(provider);
} catch (ex) {
this._recordProviderError(provider.name, "Exception when calling " +
"collect function: " + fnProperty, ex);
continue;
}
if (!collectPromise) {
this._recordProviderError(provider.name, "Does not return a promise " +
"from " + fnProperty + "()");
continue;
}
let promise = collectPromise.then(function onCollected(result) {
if (onCollect) {
try {
onCollect(entry, result);
} catch (ex) {
this._log.warn("onCollect callback threw", ex);
}
}
return CommonUtils.laterTickResolvingPromise(result);
});
promises.push([provider.name, promise]);
}
return this._handleCollectionPromises(promises, providerDiagnostic);
},
/**
* Handles promises returned by the collect* functions.
*
* This consumes the data resolved by the promises and returns a new promise
* that will be resolved once all promises have been resolved.
*
* The promise is resolved even if one of the underlying collection
* promises is rejected.
*/
_handleCollectionPromises: function (promises, providerDiagnostic=null) {
return Task.spawn(function waitForPromises() {
for (let [name, promise] of promises) {
if (providerDiagnostic) {
providerDiagnostic(name);
}
try {
yield promise;
this._log.debug("Provider collected successfully: " + name);
} catch (ex) {
this._recordProviderError(name, "Failed to collect", ex);
}
}
throw new Task.Result(this);
}.bind(this));
},
/**
* Record an error that occurred operating on a provider.
*/
_recordProviderError: function (name, msg, ex) {
msg = "Provider error: " + name + ": " + msg;
if (ex) {
msg += ": " + Log.exceptionStr(ex);
}
this._log.warn(msg);
if (this.onProviderError) {
try {
this.onProviderError(msg);
} catch (callError) {
this._log.warn("Exception when calling onProviderError callback", callError);
}
}
},
});

2186
services/metrics/storage.jsm Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,15 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
(function initMetricsTestingInfrastructure() {
do_get_profile();
let ns = {};
Components.utils.import("resource://testing-common/services/common/logging.js",
ns);
ns.initTestLogging("Trace");
}).call(this);

View File

@ -0,0 +1,31 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const modules = [
"dataprovider.jsm",
"providermanager.jsm",
"storage.jsm",
];
const test_modules = [
"mocks.jsm",
];
function run_test() {
for (let m of modules) {
let resource = "resource://gre/modules/services/metrics/" + m;
Components.utils.import(resource, {});
}
Components.utils.import("resource://gre/modules/Metrics.jsm", {});
for (let m of test_modules) {
let resource = "resource://testing-common/services/metrics/" + m;
Components.utils.import(resource, {});
}
Components.utils.import("resource://gre/modules/Metrics.jsm", {});
}

View File

@ -0,0 +1,297 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
var {utils: Cu} = Components;
Cu.import("resource://gre/modules/Metrics.jsm");
Cu.import("resource://gre/modules/Preferences.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://testing-common/services/metrics/mocks.jsm");
const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
function getProvider(storageName) {
return Task.spawn(function () {
let provider = new DummyProvider();
let storage = yield Metrics.Storage(storageName);
yield provider.init(storage);
throw new Task.Result(provider);
});
}
function run_test() {
run_next_test();
};
add_test(function test_constructor() {
let failed = false;
try {
new Metrics.Provider();
} catch(ex) {
do_check_true(ex.message.startsWith("Provider must define a name"));
failed = true;
}
finally {
do_check_true(failed);
}
run_next_test();
});
add_task(function test_init() {
let provider = new DummyProvider();
let storage = yield Metrics.Storage("init");
yield provider.init(storage);
let m = provider.getMeasurement("DummyMeasurement", 1);
do_check_true(m instanceof Metrics.Measurement);
do_check_eq(m.id, 1);
do_check_eq(Object.keys(m._fields).length, 7);
do_check_true(m.hasField("daily-counter"));
do_check_false(m.hasField("does-not-exist"));
yield storage.close();
});
add_task(function test_default_collectors() {
let provider = new DummyProvider();
let storage = yield Metrics.Storage("default_collectors");
yield provider.init(storage);
for (let property in Metrics.Provider.prototype) {
if (!property.startsWith("collect")) {
continue;
}
let result = provider[property]();
do_check_neq(result, null);
do_check_eq(typeof(result.then), "function");
}
yield storage.close();
});
add_task(function test_measurement_storage_basic() {
let provider = yield getProvider("measurement_storage_basic");
let m = provider.getMeasurement("DummyMeasurement", 1);
let now = new Date();
let yesterday = new Date(now.getTime() - MILLISECONDS_PER_DAY);
// Daily counter.
let counterID = m.fieldID("daily-counter");
yield m.incrementDailyCounter("daily-counter", now);
yield m.incrementDailyCounter("daily-counter", now);
yield m.incrementDailyCounter("daily-counter", yesterday);
let count = yield provider.storage.getDailyCounterCountFromFieldID(counterID, now);
do_check_eq(count, 2);
count = yield provider.storage.getDailyCounterCountFromFieldID(counterID, yesterday);
do_check_eq(count, 1);
yield m.incrementDailyCounter("daily-counter", now, 4);
count = yield provider.storage.getDailyCounterCountFromFieldID(counterID, now);
do_check_eq(count, 6);
// Daily discrete numeric.
let dailyDiscreteNumericID = m.fieldID("daily-discrete-numeric");
yield m.addDailyDiscreteNumeric("daily-discrete-numeric", 5, now);
yield m.addDailyDiscreteNumeric("daily-discrete-numeric", 6, now);
yield m.addDailyDiscreteNumeric("daily-discrete-numeric", 7, yesterday);
let values = yield provider.storage.getDailyDiscreteNumericFromFieldID(
dailyDiscreteNumericID, now);
do_check_eq(values.size, 1);
do_check_true(values.hasDay(now));
let actual = values.getDay(now);
do_check_eq(actual.length, 2);
do_check_eq(actual[0], 5);
do_check_eq(actual[1], 6);
values = yield provider.storage.getDailyDiscreteNumericFromFieldID(
dailyDiscreteNumericID, yesterday);
do_check_eq(values.size, 1);
do_check_true(values.hasDay(yesterday));
do_check_eq(values.getDay(yesterday)[0], 7);
// Daily discrete text.
let dailyDiscreteTextID = m.fieldID("daily-discrete-text");
yield m.addDailyDiscreteText("daily-discrete-text", "foo", now);
yield m.addDailyDiscreteText("daily-discrete-text", "bar", now);
yield m.addDailyDiscreteText("daily-discrete-text", "biz", yesterday);
values = yield provider.storage.getDailyDiscreteTextFromFieldID(
dailyDiscreteTextID, now);
do_check_eq(values.size, 1);
do_check_true(values.hasDay(now));
actual = values.getDay(now);
do_check_eq(actual.length, 2);
do_check_eq(actual[0], "foo");
do_check_eq(actual[1], "bar");
values = yield provider.storage.getDailyDiscreteTextFromFieldID(
dailyDiscreteTextID, yesterday);
do_check_true(values.hasDay(yesterday));
do_check_eq(values.getDay(yesterday)[0], "biz");
// Daily last numeric.
let lastDailyNumericID = m.fieldID("daily-last-numeric");
yield m.setDailyLastNumeric("daily-last-numeric", 5, now);
yield m.setDailyLastNumeric("daily-last-numeric", 6, yesterday);
let result = yield provider.storage.getDailyLastNumericFromFieldID(
lastDailyNumericID, now);
do_check_eq(result.size, 1);
do_check_true(result.hasDay(now));
do_check_eq(result.getDay(now), 5);
result = yield provider.storage.getDailyLastNumericFromFieldID(
lastDailyNumericID, yesterday);
do_check_true(result.hasDay(yesterday));
do_check_eq(result.getDay(yesterday), 6);
yield m.setDailyLastNumeric("daily-last-numeric", 7, now);
result = yield provider.storage.getDailyLastNumericFromFieldID(
lastDailyNumericID, now);
do_check_eq(result.getDay(now), 7);
// Daily last text.
let lastDailyTextID = m.fieldID("daily-last-text");
yield m.setDailyLastText("daily-last-text", "foo", now);
yield m.setDailyLastText("daily-last-text", "bar", yesterday);
result = yield provider.storage.getDailyLastTextFromFieldID(
lastDailyTextID, now);
do_check_eq(result.size, 1);
do_check_true(result.hasDay(now));
do_check_eq(result.getDay(now), "foo");
result = yield provider.storage.getDailyLastTextFromFieldID(
lastDailyTextID, yesterday);
do_check_true(result.hasDay(yesterday));
do_check_eq(result.getDay(yesterday), "bar");
yield m.setDailyLastText("daily-last-text", "biz", now);
result = yield provider.storage.getDailyLastTextFromFieldID(
lastDailyTextID, now);
do_check_eq(result.getDay(now), "biz");
// Last numeric.
let lastNumericID = m.fieldID("last-numeric");
yield m.setLastNumeric("last-numeric", 1, now);
result = yield provider.storage.getLastNumericFromFieldID(lastNumericID);
do_check_eq(result[1], 1);
do_check_true(result[0].getTime() < now.getTime());
do_check_true(result[0].getTime() > yesterday.getTime());
yield m.setLastNumeric("last-numeric", 2, now);
result = yield provider.storage.getLastNumericFromFieldID(lastNumericID);
do_check_eq(result[1], 2);
// Last text.
let lastTextID = m.fieldID("last-text");
yield m.setLastText("last-text", "foo", now);
result = yield provider.storage.getLastTextFromFieldID(lastTextID);
do_check_eq(result[1], "foo");
do_check_true(result[0].getTime() < now.getTime());
do_check_true(result[0].getTime() > yesterday.getTime());
yield m.setLastText("last-text", "bar", now);
result = yield provider.storage.getLastTextFromFieldID(lastTextID);
do_check_eq(result[1], "bar");
yield provider.storage.close();
});
add_task(function test_serialize_json_default() {
let provider = yield getProvider("serialize_json_default");
let now = new Date();
let yesterday = new Date(now.getTime() - MILLISECONDS_PER_DAY);
let m = provider.getMeasurement("DummyMeasurement", 1);
m.incrementDailyCounter("daily-counter", now);
m.incrementDailyCounter("daily-counter", now);
m.incrementDailyCounter("daily-counter", yesterday);
m.addDailyDiscreteNumeric("daily-discrete-numeric", 1, now);
m.addDailyDiscreteNumeric("daily-discrete-numeric", 2, now);
m.addDailyDiscreteNumeric("daily-discrete-numeric", 3, yesterday);
m.addDailyDiscreteText("daily-discrete-text", "foo", now);
m.addDailyDiscreteText("daily-discrete-text", "bar", now);
m.addDailyDiscreteText("daily-discrete-text", "baz", yesterday);
m.setDailyLastNumeric("daily-last-numeric", 4, now);
m.setDailyLastNumeric("daily-last-numeric", 5, yesterday);
m.setDailyLastText("daily-last-text", "apple", now);
m.setDailyLastText("daily-last-text", "orange", yesterday);
m.setLastNumeric("last-numeric", 6, now);
yield m.setLastText("last-text", "hello", now);
let data = yield provider.storage.getMeasurementValues(m.id);
let serializer = m.serializer(m.SERIALIZE_JSON);
let formatted = serializer.singular(data.singular);
do_check_eq(Object.keys(formatted).length, 3); // Our keys + _v.
do_check_true("last-numeric" in formatted);
do_check_true("last-text" in formatted);
do_check_eq(formatted["last-numeric"], 6);
do_check_eq(formatted["last-text"], "hello");
do_check_eq(formatted["_v"], 1);
formatted = serializer.daily(data.days.getDay(now));
do_check_eq(Object.keys(formatted).length, 6); // Our keys + _v.
do_check_eq(formatted["daily-counter"], 2);
do_check_eq(formatted["_v"], 1);
do_check_true(Array.isArray(formatted["daily-discrete-numeric"]));
do_check_eq(formatted["daily-discrete-numeric"].length, 2);
do_check_eq(formatted["daily-discrete-numeric"][0], 1);
do_check_eq(formatted["daily-discrete-numeric"][1], 2);
do_check_true(Array.isArray(formatted["daily-discrete-text"]));
do_check_eq(formatted["daily-discrete-text"].length, 2);
do_check_eq(formatted["daily-discrete-text"][0], "foo");
do_check_eq(formatted["daily-discrete-text"][1], "bar");
do_check_eq(formatted["daily-last-numeric"], 4);
do_check_eq(formatted["daily-last-text"], "apple");
formatted = serializer.daily(data.days.getDay(yesterday));
do_check_eq(formatted["daily-last-numeric"], 5);
do_check_eq(formatted["daily-last-text"], "orange");
// Now let's turn off a field so that it's present in the DB
// but not present in the output.
let called = false;
let excluded = "daily-last-numeric";
Object.defineProperty(m, "shouldIncludeField", {
value: function fakeShouldIncludeField(field) {
called = true;
return field != excluded;
},
});
let limited = serializer.daily(data.days.getDay(yesterday));
do_check_true(called);
do_check_false(excluded in limited);
do_check_eq(formatted["daily-last-text"], "orange");
yield provider.storage.close();
});

View File

@ -0,0 +1,357 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource://gre/modules/Metrics.jsm");
Cu.import("resource://testing-common/services/metrics/mocks.jsm");
const PULL_ONLY_TESTING_CATEGORY = "testing-only-pull-only-providers";
function run_test() {
let cm = Cc["@mozilla.org/categorymanager;1"]
.getService(Ci.nsICategoryManager);
cm.addCategoryEntry(PULL_ONLY_TESTING_CATEGORY, "DummyProvider",
"resource://testing-common/services/metrics/mocks.jsm",
false, true);
cm.addCategoryEntry(PULL_ONLY_TESTING_CATEGORY, "DummyConstantProvider",
"resource://testing-common/services/metrics/mocks.jsm",
false, true);
run_next_test();
};
add_task(function test_constructor() {
let storage = yield Metrics.Storage("constructor");
let manager = new Metrics.ProviderManager(storage);
yield storage.close();
});
add_task(function test_register_provider() {
let storage = yield Metrics.Storage("register_provider");
let manager = new Metrics.ProviderManager(storage);
let dummy = new DummyProvider();
yield manager.registerProvider(dummy);
do_check_eq(manager._providers.size, 1);
yield manager.registerProvider(dummy);
do_check_eq(manager._providers.size, 1);
do_check_eq(manager.getProvider(dummy.name), dummy);
let failed = false;
try {
manager.registerProvider({});
} catch (ex) {
do_check_true(ex.message.startsWith("Provider is not valid"));
failed = true;
} finally {
do_check_true(failed);
failed = false;
}
manager.unregisterProvider(dummy.name);
do_check_eq(manager._providers.size, 0);
do_check_null(manager.getProvider(dummy.name));
yield storage.close();
});
add_task(function test_register_providers_from_category_manager() {
const category = "metrics-providers-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 storage = yield Metrics.Storage("register_providers_from_category_manager");
let manager = new Metrics.ProviderManager(storage);
try {
do_check_eq(manager._providers.size, 0);
yield manager.registerProvidersFromCategoryManager(category);
do_check_eq(manager._providers.size, 1);
} finally {
yield storage.close();
}
});
add_task(function test_collect_constant_data() {
let storage = yield Metrics.Storage("collect_constant_data");
let errorCount = 0;
let manager= new Metrics.ProviderManager(storage);
manager.onProviderError = function () { errorCount++; }
let provider = new DummyProvider();
yield manager.registerProvider(provider);
do_check_eq(provider.collectConstantCount, 0);
yield manager.collectConstantData();
do_check_eq(provider.collectConstantCount, 1);
do_check_true(manager._providers.get("DummyProvider").constantsCollected);
yield storage.close();
do_check_eq(errorCount, 0);
});
add_task(function test_collect_constant_throws() {
let storage = yield Metrics.Storage("collect_constant_throws");
let manager = new Metrics.ProviderManager(storage);
let errors = [];
manager.onProviderError = function (error) { errors.push(error); };
let provider = new DummyProvider();
provider.throwDuringCollectConstantData = "Fake error during collect";
yield manager.registerProvider(provider);
yield manager.collectConstantData();
do_check_eq(errors.length, 1);
do_check_true(errors[0].includes(provider.throwDuringCollectConstantData));
yield storage.close();
});
add_task(function test_collect_constant_populate_throws() {
let storage = yield Metrics.Storage("collect_constant_populate_throws");
let manager = new Metrics.ProviderManager(storage);
let errors = [];
manager.onProviderError = function (error) { errors.push(error); };
let provider = new DummyProvider();
provider.throwDuringConstantPopulate = "Fake error during constant populate";
yield manager.registerProvider(provider);
yield manager.collectConstantData();
do_check_eq(errors.length, 1);
do_check_true(errors[0].includes(provider.throwDuringConstantPopulate));
do_check_false(manager._providers.get(provider.name).constantsCollected);
yield storage.close();
});
add_task(function test_collect_constant_onetime() {
let storage = yield Metrics.Storage("collect_constant_onetime");
let manager = new Metrics.ProviderManager(storage);
let provider = new DummyProvider();
yield manager.registerProvider(provider);
yield manager.collectConstantData();
do_check_eq(provider.collectConstantCount, 1);
yield manager.collectConstantData();
do_check_eq(provider.collectConstantCount, 1);
yield storage.close();
});
add_task(function test_collect_multiple() {
let storage = yield Metrics.Storage("collect_multiple");
let manager = new Metrics.ProviderManager(storage);
for (let i = 0; i < 10; i++) {
yield manager.registerProvider(new DummyProvider("provider" + i));
}
do_check_eq(manager._providers.size, 10);
yield manager.collectConstantData();
yield storage.close();
});
add_task(function test_collect_daily() {
let storage = yield Metrics.Storage("collect_daily");
let manager = new Metrics.ProviderManager(storage);
let provider1 = new DummyProvider("DP1");
let provider2 = new DummyProvider("DP2");
yield manager.registerProvider(provider1);
yield manager.registerProvider(provider2);
yield manager.collectDailyData();
do_check_eq(provider1.collectDailyCount, 1);
do_check_eq(provider2.collectDailyCount, 1);
yield manager.collectDailyData();
do_check_eq(provider1.collectDailyCount, 2);
do_check_eq(provider2.collectDailyCount, 2);
yield storage.close();
});
add_task(function test_pull_only_not_initialized() {
let storage = yield Metrics.Storage("pull_only_not_initialized");
let manager = new Metrics.ProviderManager(storage);
yield manager.registerProvidersFromCategoryManager(PULL_ONLY_TESTING_CATEGORY);
do_check_eq(manager.providers.length, 1);
do_check_eq(manager.providers[0].name, "DummyProvider");
yield storage.close();
});
add_task(function test_pull_only_registration() {
let storage = yield Metrics.Storage("pull_only_registration");
let manager = new Metrics.ProviderManager(storage);
yield manager.registerProvidersFromCategoryManager(PULL_ONLY_TESTING_CATEGORY);
do_check_eq(manager.providers.length, 1);
// Simple registration and unregistration.
yield manager.ensurePullOnlyProvidersRegistered();
do_check_eq(manager.providers.length, 2);
do_check_neq(manager.getProvider("DummyConstantProvider"), null);
yield manager.ensurePullOnlyProvidersUnregistered();
do_check_eq(manager.providers.length, 1);
do_check_null(manager.getProvider("DummyConstantProvider"));
// Multiple calls to register work.
yield manager.ensurePullOnlyProvidersRegistered();
do_check_eq(manager.providers.length, 2);
yield manager.ensurePullOnlyProvidersRegistered();
do_check_eq(manager.providers.length, 2);
// Unregister with 2 requests for registration should not unregister.
yield manager.ensurePullOnlyProvidersUnregistered();
do_check_eq(manager.providers.length, 2);
// But the 2nd one will.
yield manager.ensurePullOnlyProvidersUnregistered();
do_check_eq(manager.providers.length, 1);
yield storage.close();
});
add_task(function test_pull_only_register_while_registering() {
let storage = yield Metrics.Storage("pull_only_register_will_registering");
let manager = new Metrics.ProviderManager(storage);
yield manager.registerProvidersFromCategoryManager(PULL_ONLY_TESTING_CATEGORY);
manager.ensurePullOnlyProvidersRegistered();
manager.ensurePullOnlyProvidersRegistered();
yield manager.ensurePullOnlyProvidersRegistered();
do_check_eq(manager.providers.length, 2);
manager.ensurePullOnlyProvidersUnregistered();
manager.ensurePullOnlyProvidersUnregistered();
yield manager.ensurePullOnlyProvidersUnregistered();
do_check_eq(manager.providers.length, 1);
yield storage.close();
});
add_task(function test_pull_only_unregister_while_registering() {
let storage = yield Metrics.Storage("pull_only_unregister_while_registering");
let manager = new Metrics.ProviderManager(storage);
yield manager.registerProvidersFromCategoryManager(PULL_ONLY_TESTING_CATEGORY);
manager.ensurePullOnlyProvidersRegistered();
yield manager.ensurePullOnlyProvidersUnregistered();
do_check_eq(manager.providers.length, 1);
yield storage.close();
});
add_task(function test_pull_only_register_while_unregistering() {
let storage = yield Metrics.Storage("pull_only_register_while_unregistering");
let manager = new Metrics.ProviderManager(storage);
yield manager.registerProvidersFromCategoryManager(PULL_ONLY_TESTING_CATEGORY);
yield manager.ensurePullOnlyProvidersRegistered();
manager.ensurePullOnlyProvidersUnregistered();
yield manager.ensurePullOnlyProvidersRegistered();
do_check_eq(manager.providers.length, 2);
yield storage.close();
});
// Re-use database for perf reasons.
const REGISTRATION_ERRORS_DB = "registration_errors";
add_task(function test_category_manager_registration_error() {
let storage = yield Metrics.Storage(REGISTRATION_ERRORS_DB);
let manager = new Metrics.ProviderManager(storage);
let cm = Cc["@mozilla.org/categorymanager;1"]
.getService(Ci.nsICategoryManager);
cm.addCategoryEntry("registration-errors", "DummyThrowOnInitProvider",
"resource://testing-common/services/metrics/mocks.jsm",
false, true);
let deferred = Promise.defer();
let errorCount = 0;
manager.onProviderError = function (msg) {
errorCount++;
deferred.resolve(msg);
};
yield manager.registerProvidersFromCategoryManager("registration-errors");
do_check_eq(manager.providers.length, 0);
do_check_eq(errorCount, 1);
let msg = yield deferred.promise;
do_check_true(msg.includes("Provider error: DummyThrowOnInitProvider: "
+ "Error registering provider from category manager: "
+ "Error: Dummy Error"));
yield storage.close();
});
add_task(function test_pull_only_registration_error() {
let storage = yield Metrics.Storage(REGISTRATION_ERRORS_DB);
let manager = new Metrics.ProviderManager(storage);
let deferred = Promise.defer();
let errorCount = 0;
manager.onProviderError = function (msg) {
errorCount++;
deferred.resolve(msg);
};
yield manager.registerProviderFromType(DummyPullOnlyThrowsOnInitProvider);
do_check_eq(errorCount, 0);
yield manager.ensurePullOnlyProvidersRegistered();
do_check_eq(errorCount, 1);
let msg = yield deferred.promise;
do_check_true(msg.includes("Provider error: DummyPullOnlyThrowsOnInitProvider: " +
"Error registering pull-only provider: Error: Dummy Error"));
yield storage.close();
});
add_task(function test_error_during_shutdown() {
let storage = yield Metrics.Storage(REGISTRATION_ERRORS_DB);
let manager = new Metrics.ProviderManager(storage);
let deferred = Promise.defer();
let errorCount = 0;
manager.onProviderError = function (msg) {
errorCount++;
deferred.resolve(msg);
};
yield manager.registerProviderFromType(DummyThrowOnShutdownProvider);
yield manager.registerProviderFromType(DummyProvider);
do_check_eq(errorCount, 0);
do_check_eq(manager.providers.length, 1);
yield manager.ensurePullOnlyProvidersRegistered();
do_check_eq(errorCount, 0);
yield manager.ensurePullOnlyProvidersUnregistered();
do_check_eq(errorCount, 1);
let msg = yield deferred.promise;
do_check_true(msg.includes("Provider error: DummyThrowOnShutdownProvider: " +
"Error when shutting down provider: Error: Dummy shutdown error"));
yield storage.close();
});

View File

@ -0,0 +1,839 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
var {utils: Cu} = Components;
Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource://gre/modules/Metrics.jsm");
Cu.import("resource://services-common/utils.js");
const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
function run_test() {
run_next_test();
}
add_test(function test_days_date_conversion() {
let toDays = Metrics.dateToDays;
let toDate = Metrics.daysToDate;
let d = new Date(0);
do_check_eq(toDays(d), 0);
d = new Date(MILLISECONDS_PER_DAY);
do_check_eq(toDays(d), 1);
d = new Date(MILLISECONDS_PER_DAY - 1);
do_check_eq(toDays(d), 0);
d = new Date("1970-12-31T23:59:59.999Z");
do_check_eq(toDays(d), 364);
d = new Date("1971-01-01T00:00:00Z");
do_check_eq(toDays(d), 365);
d = toDate(0);
do_check_eq(d.getTime(), 0);
d = toDate(1);
do_check_eq(d.getTime(), MILLISECONDS_PER_DAY);
d = toDate(365);
do_check_eq(d.getUTCFullYear(), 1971);
do_check_eq(d.getUTCMonth(), 0);
do_check_eq(d.getUTCDate(), 1);
do_check_eq(d.getUTCHours(), 0);
do_check_eq(d.getUTCMinutes(), 0);
do_check_eq(d.getUTCSeconds(), 0);
do_check_eq(d.getUTCMilliseconds(), 0);
run_next_test();
});
add_task(function test_get_sqlite_backend() {
let backend = yield Metrics.Storage("get_sqlite_backend.sqlite");
do_check_neq(backend._connection, null);
// Ensure WAL and auto checkpoint are enabled.
do_check_neq(backend._enabledWALCheckpointPages, null);
let rows = yield backend._connection.execute("PRAGMA journal_mode");
do_check_eq(rows[0].getResultByIndex(0), "wal");
rows = yield backend._connection.execute("PRAGMA wal_autocheckpoint");
do_check_eq(rows[0].getResultByIndex(0), backend._enabledWALCheckpointPages);
yield backend.close();
do_check_null(backend._connection);
});
add_task(function test_reconnect() {
let backend = yield Metrics.Storage("reconnect");
yield backend.close();
let backend2 = yield Metrics.Storage("reconnect");
yield backend2.close();
});
add_task(function test_future_schema_errors() {
let backend = yield Metrics.Storage("future_schema_errors");
yield backend._connection.setSchemaVersion(2);
yield backend.close();
let backend2;
let failed = false;
try {
backend2 = yield Metrics.Storage("future_schema_errors");
} catch (ex) {
failed = true;
do_check_true(ex.message.startsWith("Unknown database schema"));
}
do_check_null(backend2);
do_check_true(failed);
});
add_task(function test_checkpoint_apis() {
let backend = yield Metrics.Storage("checkpoint_apis");
let c = backend._connection;
let count = c._connectionData._statementCounter;
yield backend.setAutoCheckpoint(0);
do_check_eq(c._connectionData._statementCounter, count + 1);
let rows = yield c.execute("PRAGMA wal_autocheckpoint");
do_check_eq(rows[0].getResultByIndex(0), 0);
count = c._connectionData._statementCounter;
yield backend.setAutoCheckpoint(1);
do_check_eq(c._connectionData._statementCounter, count + 1);
rows = yield c.execute("PRAGMA wal_autocheckpoint");
do_check_eq(rows[0].getResultByIndex(0), backend._enabledWALCheckpointPages);
count = c._connectionData._statementCounter;
yield backend.checkpoint();
do_check_eq(c._connectionData._statementCounter, count + 1);
yield backend.checkpoint();
do_check_eq(c._connectionData._statementCounter, count + 2);
yield backend.close();
});
add_task(function test_measurement_registration() {
let backend = yield Metrics.Storage("measurement_registration");
do_check_false(backend.hasProvider("foo"));
do_check_false(backend.hasMeasurement("foo", "bar", 1));
let id = yield backend.registerMeasurement("foo", "bar", 1);
do_check_eq(id, 1);
do_check_true(backend.hasProvider("foo"));
do_check_true(backend.hasMeasurement("foo", "bar", 1));
do_check_eq(backend.measurementID("foo", "bar", 1), id);
do_check_false(backend.hasMeasurement("foo", "bar", 2));
let id2 = yield backend.registerMeasurement("foo", "bar", 2);
do_check_eq(id2, 2);
do_check_true(backend.hasMeasurement("foo", "bar", 2));
do_check_eq(backend.measurementID("foo", "bar", 2), id2);
yield backend.close();
});
add_task(function test_field_registration_basic() {
let backend = yield Metrics.Storage("field_registration_basic");
do_check_false(backend.hasField("foo", "bar", 1, "baz"));
let mID = yield backend.registerMeasurement("foo", "bar", 1);
do_check_false(backend.hasField("foo", "bar", 1, "baz"));
do_check_false(backend.hasFieldFromMeasurement(mID, "baz"));
let bazID = yield backend.registerField(mID, "baz",
backend.FIELD_DAILY_COUNTER);
do_check_true(backend.hasField("foo", "bar", 1, "baz"));
do_check_true(backend.hasFieldFromMeasurement(mID, "baz"));
let bar2ID = yield backend.registerMeasurement("foo", "bar2", 1);
yield backend.registerField(bar2ID, "baz",
backend.FIELD_DAILY_DISCRETE_NUMERIC);
do_check_true(backend.hasField("foo", "bar2", 1, "baz"));
yield backend.close();
});
// Ensure changes types of fields results in fatal error.
add_task(function test_field_registration_changed_type() {
let backend = yield Metrics.Storage("field_registration_changed_type");
let mID = yield backend.registerMeasurement("bar", "bar", 1);
let id = yield backend.registerField(mID, "baz",
backend.FIELD_DAILY_COUNTER);
let caught = false;
try {
yield backend.registerField(mID, "baz",
backend.FIELD_DAILY_DISCRETE_NUMERIC);
} catch (ex) {
caught = true;
do_check_true(ex.message.startsWith("Field already defined with different type"));
}
do_check_true(caught);
yield backend.close();
});
add_task(function test_field_registration_repopulation() {
let backend = yield Metrics.Storage("field_registration_repopulation");
let mID1 = yield backend.registerMeasurement("foo", "bar", 1);
let mID2 = yield backend.registerMeasurement("foo", "bar", 2);
let mID3 = yield backend.registerMeasurement("foo", "biz", 1);
let mID4 = yield backend.registerMeasurement("baz", "foo", 1);
let fID1 = yield backend.registerField(mID1, "foo", backend.FIELD_DAILY_COUNTER);
let fID2 = yield backend.registerField(mID1, "bar", backend.FIELD_DAILY_DISCRETE_NUMERIC);
let fID3 = yield backend.registerField(mID4, "foo", backend.FIELD_LAST_TEXT);
yield backend.close();
backend = yield Metrics.Storage("field_registration_repopulation");
do_check_true(backend.hasProvider("foo"));
do_check_true(backend.hasProvider("baz"));
do_check_true(backend.hasMeasurement("foo", "bar", 1));
do_check_eq(backend.measurementID("foo", "bar", 1), mID1);
do_check_true(backend.hasMeasurement("foo", "bar", 2));
do_check_eq(backend.measurementID("foo", "bar", 2), mID2);
do_check_true(backend.hasMeasurement("foo", "biz", 1));
do_check_eq(backend.measurementID("foo", "biz", 1), mID3);
do_check_true(backend.hasMeasurement("baz", "foo", 1));
do_check_eq(backend.measurementID("baz", "foo", 1), mID4);
do_check_true(backend.hasField("foo", "bar", 1, "foo"));
do_check_eq(backend.fieldID("foo", "bar", 1, "foo"), fID1);
do_check_true(backend.hasField("foo", "bar", 1, "bar"));
do_check_eq(backend.fieldID("foo", "bar", 1, "bar"), fID2);
do_check_true(backend.hasField("baz", "foo", 1, "foo"));
do_check_eq(backend.fieldID("baz", "foo", 1, "foo"), fID3);
yield backend.close();
});
add_task(function test_enqueue_operation_execution_order() {
let backend = yield Metrics.Storage("enqueue_operation_execution_order");
let executionCount = 0;
let fns = {
op1: function () {
do_check_eq(executionCount, 1);
},
op2: function () {
do_check_eq(executionCount, 2);
},
op3: function () {
do_check_eq(executionCount, 3);
},
};
function enqueuedOperation(fn) {
let deferred = Promise.defer();
CommonUtils.nextTick(function onNextTick() {
executionCount++;
fn();
deferred.resolve();
});
return deferred.promise;
}
let promises = [];
for (let i = 1; i <= 3; i++) {
let fn = fns["op" + i];
promises.push(backend.enqueueOperation(enqueuedOperation.bind(this, fn)));
}
for (let promise of promises) {
yield promise;
}
yield backend.close();
});
add_task(function test_enqueue_operation_many() {
let backend = yield Metrics.Storage("enqueue_operation_many");
let promises = [];
for (let i = 0; i < 100; i++) {
promises.push(backend.registerMeasurement("foo", "bar" + i, 1));
}
for (let promise of promises) {
yield promise;
}
yield backend.close();
});
// If the operation did not return a promise, everything should still execute.
add_task(function test_enqueue_operation_no_return_promise() {
let backend = yield Metrics.Storage("enqueue_operation_no_return_promise");
let mID = yield backend.registerMeasurement("foo", "bar", 1);
let fID = yield backend.registerField(mID, "baz", backend.FIELD_DAILY_COUNTER);
let now = new Date();
let promises = [];
for (let i = 0; i < 10; i++) {
promises.push(backend.enqueueOperation(function op() {
backend.incrementDailyCounterFromFieldID(fID, now);
}));
}
let deferred = Promise.defer();
let finished = 0;
for (let promise of promises) {
promise.then(
do_throw.bind(this, "Unexpected resolve."),
function onError() {
finished++;
if (finished == promises.length) {
backend.getDailyCounterCountFromFieldID(fID, now).then(function onCount(count) {
// There should not be a race condition here because storage
// serializes all statements. So, for the getDailyCounterCount
// query to finish means that all counter update statements must
// have completed.
do_check_eq(count, promises.length);
deferred.resolve();
});
}
}
);
}
yield deferred.promise;
yield backend.close();
});
// If an operation throws, subsequent operations should still execute.
add_task(function test_enqueue_operation_throw_exception() {
let backend = yield Metrics.Storage("enqueue_operation_rejected_promise");
let mID = yield backend.registerMeasurement("foo", "bar", 1);
let fID = yield backend.registerField(mID, "baz", backend.FIELD_DAILY_COUNTER);
let now = new Date();
let deferred = Promise.defer();
backend.enqueueOperation(function bad() {
throw new Error("I failed.");
}).then(do_throw, function onError(error) {
do_check_true(error.message.includes("I failed."));
deferred.resolve();
});
let promise = backend.enqueueOperation(function () {
return backend.incrementDailyCounterFromFieldID(fID, now);
});
yield deferred.promise;
yield promise;
let count = yield backend.getDailyCounterCountFromFieldID(fID, now);
do_check_eq(count, 1);
yield backend.close();
});
// If an operation rejects, subsequent operations should still execute.
add_task(function test_enqueue_operation_reject_promise() {
let backend = yield Metrics.Storage("enqueue_operation_reject_promise");
let mID = yield backend.registerMeasurement("foo", "bar", 1);
let fID = yield backend.registerField(mID, "baz", backend.FIELD_DAILY_COUNTER);
let now = new Date();
let deferred = Promise.defer();
backend.enqueueOperation(function reject() {
let d = Promise.defer();
CommonUtils.nextTick(function nextTick() {
d.reject("I failed.");
});
return d.promise;
}).then(do_throw, function onError(error) {
deferred.resolve();
});
let promise = backend.enqueueOperation(function () {
return backend.incrementDailyCounterFromFieldID(fID, now);
});
yield deferred.promise;
yield promise;
let count = yield backend.getDailyCounterCountFromFieldID(fID, now);
do_check_eq(count, 1);
yield backend.close();
});
add_task(function test_enqueue_transaction() {
let backend = yield Metrics.Storage("enqueue_transaction");
let mID = yield backend.registerMeasurement("foo", "bar", 1);
let fID = yield backend.registerField(mID, "baz", backend.FIELD_DAILY_COUNTER);
let now = new Date();
yield backend.incrementDailyCounterFromFieldID(fID, now);
yield backend.enqueueTransaction(function transaction() {
yield backend.incrementDailyCounterFromFieldID(fID, now);
});
let count = yield backend.getDailyCounterCountFromFieldID(fID, now);
do_check_eq(count, 2);
let errored = false;
try {
yield backend.enqueueTransaction(function aborted() {
yield backend.incrementDailyCounterFromFieldID(fID, now);
throw new Error("Some error.");
});
} catch (ex) {
errored = true;
} finally {
do_check_true(errored);
}
count = yield backend.getDailyCounterCountFromFieldID(fID, now);
do_check_eq(count, 2);
yield backend.close();
});
add_task(function test_increment_daily_counter_basic() {
let backend = yield Metrics.Storage("increment_daily_counter_basic");
let mID = yield backend.registerMeasurement("foo", "bar", 1);
let fieldID = yield backend.registerField(mID, "baz",
backend.FIELD_DAILY_COUNTER);
let now = new Date();
yield backend.incrementDailyCounterFromFieldID(fieldID, now);
let count = yield backend.getDailyCounterCountFromFieldID(fieldID, now);
do_check_eq(count, 1);
yield backend.incrementDailyCounterFromFieldID(fieldID, now);
count = yield backend.getDailyCounterCountFromFieldID(fieldID, now);
do_check_eq(count, 2);
yield backend.incrementDailyCounterFromFieldID(fieldID, now, 10);
count = yield backend.getDailyCounterCountFromFieldID(fieldID, now);
do_check_eq(count, 12);
yield backend.close();
});
add_task(function test_increment_daily_counter_multiple_days() {
let backend = yield Metrics.Storage("increment_daily_counter_multiple_days");
let mID = yield backend.registerMeasurement("foo", "bar", 1);
let fieldID = yield backend.registerField(mID, "baz",
backend.FIELD_DAILY_COUNTER);
let days = [];
let now = Date.now();
for (let i = 0; i < 100; i++) {
days.push(new Date(now - i * MILLISECONDS_PER_DAY));
}
for (let day of days) {
yield backend.incrementDailyCounterFromFieldID(fieldID, day);
}
let result = yield backend.getDailyCounterCountsFromFieldID(fieldID);
do_check_eq(result.size, 100);
for (let day of days) {
do_check_true(result.hasDay(day));
do_check_eq(result.getDay(day), 1);
}
let fields = yield backend.getMeasurementDailyCountersFromMeasurementID(mID);
do_check_eq(fields.size, 1);
do_check_true(fields.has("baz"));
do_check_eq(fields.get("baz").size, 100);
for (let day of days) {
do_check_true(fields.get("baz").hasDay(day));
do_check_eq(fields.get("baz").getDay(day), 1);
}
yield backend.close();
});
add_task(function test_last_values() {
let backend = yield Metrics.Storage("set_last");
let mID = yield backend.registerMeasurement("foo", "bar", 1);
let numberID = yield backend.registerField(mID, "number",
backend.FIELD_LAST_NUMERIC);
let textID = yield backend.registerField(mID, "text",
backend.FIELD_LAST_TEXT);
let now = new Date();
let nowDay = new Date(Math.floor(now.getTime() / MILLISECONDS_PER_DAY) * MILLISECONDS_PER_DAY);
yield backend.setLastNumericFromFieldID(numberID, 42, now);
yield backend.setLastTextFromFieldID(textID, "hello world", now);
let result = yield backend.getLastNumericFromFieldID(numberID);
do_check_true(Array.isArray(result));
do_check_eq(result[0].getTime(), nowDay.getTime());
do_check_eq(typeof(result[1]), "number");
do_check_eq(result[1], 42);
result = yield backend.getLastTextFromFieldID(textID);
do_check_true(Array.isArray(result));
do_check_eq(result[0].getTime(), nowDay.getTime());
do_check_eq(typeof(result[1]), "string");
do_check_eq(result[1], "hello world");
let missingID = yield backend.registerField(mID, "missing",
backend.FIELD_LAST_NUMERIC);
do_check_null(yield backend.getLastNumericFromFieldID(missingID));
let fields = yield backend.getMeasurementLastValuesFromMeasurementID(mID);
do_check_eq(fields.size, 2);
do_check_true(fields.has("number"));
do_check_true(fields.has("text"));
do_check_eq(fields.get("number")[1], 42);
do_check_eq(fields.get("text")[1], "hello world");
yield backend.close();
});
add_task(function test_discrete_values_basic() {
let backend = yield Metrics.Storage("discrete_values_basic");
let mID = yield backend.registerMeasurement("foo", "bar", 1);
let numericID = yield backend.registerField(mID, "numeric",
backend.FIELD_DAILY_DISCRETE_NUMERIC);
let textID = yield backend.registerField(mID, "text",
backend.FIELD_DAILY_DISCRETE_TEXT);
let now = new Date();
let expectedNumeric = [];
let expectedText = [];
for (let i = 0; i < 100; i++) {
expectedNumeric.push(i);
expectedText.push("value" + i);
yield backend.addDailyDiscreteNumericFromFieldID(numericID, i, now);
yield backend.addDailyDiscreteTextFromFieldID(textID, "value" + i, now);
}
let values = yield backend.getDailyDiscreteNumericFromFieldID(numericID);
do_check_eq(values.size, 1);
do_check_true(values.hasDay(now));
do_check_true(Array.isArray(values.getDay(now)));
do_check_eq(values.getDay(now).length, expectedNumeric.length);
for (let i = 0; i < expectedNumeric.length; i++) {
do_check_eq(values.getDay(now)[i], expectedNumeric[i]);
}
values = yield backend.getDailyDiscreteTextFromFieldID(textID);
do_check_eq(values.size, 1);
do_check_true(values.hasDay(now));
do_check_true(Array.isArray(values.getDay(now)));
do_check_eq(values.getDay(now).length, expectedText.length);
for (let i = 0; i < expectedText.length; i++) {
do_check_eq(values.getDay(now)[i], expectedText[i]);
}
let fields = yield backend.getMeasurementDailyDiscreteValuesFromMeasurementID(mID);
do_check_eq(fields.size, 2);
do_check_true(fields.has("numeric"));
do_check_true(fields.has("text"));
let numeric = fields.get("numeric");
let text = fields.get("text");
do_check_true(numeric.hasDay(now));
do_check_true(text.hasDay(now));
do_check_eq(numeric.getDay(now).length, expectedNumeric.length);
do_check_eq(text.getDay(now).length, expectedText.length);
for (let i = 0; i < expectedNumeric.length; i++) {
do_check_eq(numeric.getDay(now)[i], expectedNumeric[i]);
}
for (let i = 0; i < expectedText.length; i++) {
do_check_eq(text.getDay(now)[i], expectedText[i]);
}
yield backend.close();
});
add_task(function test_discrete_values_multiple_days() {
let backend = yield Metrics.Storage("discrete_values_multiple_days");
let mID = yield backend.registerMeasurement("foo", "bar", 1);
let id = yield backend.registerField(mID, "baz",
backend.FIELD_DAILY_DISCRETE_NUMERIC);
let now = new Date();
let dates = [];
for (let i = 0; i < 50; i++) {
let date = new Date(now.getTime() + i * MILLISECONDS_PER_DAY);
dates.push(date);
yield backend.addDailyDiscreteNumericFromFieldID(id, i, date);
}
let values = yield backend.getDailyDiscreteNumericFromFieldID(id);
do_check_eq(values.size, 50);
let i = 0;
for (let date of dates) {
do_check_true(values.hasDay(date));
do_check_eq(values.getDay(date)[0], i);
i++;
}
let fields = yield backend.getMeasurementDailyDiscreteValuesFromMeasurementID(mID);
do_check_eq(fields.size, 1);
do_check_true(fields.has("baz"));
let baz = fields.get("baz");
do_check_eq(baz.size, 50);
i = 0;
for (let date of dates) {
do_check_true(baz.hasDay(date));
do_check_eq(baz.getDay(date).length, 1);
do_check_eq(baz.getDay(date)[0], i);
i++;
}
yield backend.close();
});
add_task(function test_daily_last_values() {
let backend = yield Metrics.Storage("daily_last_values");
let mID = yield backend.registerMeasurement("foo", "bar", 1);
let numericID = yield backend.registerField(mID, "numeric",
backend.FIELD_DAILY_LAST_NUMERIC);
let textID = yield backend.registerField(mID, "text",
backend.FIELD_DAILY_LAST_TEXT);
let now = new Date();
let yesterday = new Date(now.getTime() - MILLISECONDS_PER_DAY);
let dayBefore = new Date(yesterday.getTime() - MILLISECONDS_PER_DAY);
yield backend.setDailyLastNumericFromFieldID(numericID, 1, yesterday);
yield backend.setDailyLastNumericFromFieldID(numericID, 2, now);
yield backend.setDailyLastNumericFromFieldID(numericID, 3, dayBefore);
yield backend.setDailyLastTextFromFieldID(textID, "foo", now);
yield backend.setDailyLastTextFromFieldID(textID, "bar", yesterday);
yield backend.setDailyLastTextFromFieldID(textID, "baz", dayBefore);
let days = yield backend.getDailyLastNumericFromFieldID(numericID);
do_check_eq(days.size, 3);
do_check_eq(days.getDay(yesterday), 1);
do_check_eq(days.getDay(now), 2);
do_check_eq(days.getDay(dayBefore), 3);
days = yield backend.getDailyLastTextFromFieldID(textID);
do_check_eq(days.size, 3);
do_check_eq(days.getDay(now), "foo");
do_check_eq(days.getDay(yesterday), "bar");
do_check_eq(days.getDay(dayBefore), "baz");
yield backend.setDailyLastNumericFromFieldID(numericID, 4, yesterday);
days = yield backend.getDailyLastNumericFromFieldID(numericID);
do_check_eq(days.getDay(yesterday), 4);
yield backend.setDailyLastTextFromFieldID(textID, "biz", yesterday);
days = yield backend.getDailyLastTextFromFieldID(textID);
do_check_eq(days.getDay(yesterday), "biz");
days = yield backend.getDailyLastNumericFromFieldID(numericID, yesterday);
do_check_eq(days.size, 1);
do_check_eq(days.getDay(yesterday), 4);
days = yield backend.getDailyLastTextFromFieldID(textID, yesterday);
do_check_eq(days.size, 1);
do_check_eq(days.getDay(yesterday), "biz");
let fields = yield backend.getMeasurementDailyLastValuesFromMeasurementID(mID);
do_check_eq(fields.size, 2);
do_check_true(fields.has("numeric"));
do_check_true(fields.has("text"));
let numeric = fields.get("numeric");
let text = fields.get("text");
do_check_true(numeric.hasDay(yesterday));
do_check_true(numeric.hasDay(dayBefore));
do_check_true(numeric.hasDay(now));
do_check_true(text.hasDay(yesterday));
do_check_true(text.hasDay(dayBefore));
do_check_true(text.hasDay(now));
do_check_eq(numeric.getDay(yesterday), 4);
do_check_eq(text.getDay(yesterday), "biz");
yield backend.close();
});
add_task(function test_prune_data_before() {
let backend = yield Metrics.Storage("prune_data_before");
let mID = yield backend.registerMeasurement("foo", "bar", 1);
let counterID = yield backend.registerField(mID, "baz",
backend.FIELD_DAILY_COUNTER);
let text1ID = yield backend.registerField(mID, "one_text_1",
backend.FIELD_LAST_TEXT);
let text2ID = yield backend.registerField(mID, "one_text_2",
backend.FIELD_LAST_TEXT);
let numeric1ID = yield backend.registerField(mID, "one_numeric_1",
backend.FIELD_LAST_NUMERIC);
let numeric2ID = yield backend.registerField(mID, "one_numeric_2",
backend.FIELD_LAST_NUMERIC);
let text3ID = yield backend.registerField(mID, "daily_last_text_1",
backend.FIELD_DAILY_LAST_TEXT);
let text4ID = yield backend.registerField(mID, "daily_last_text_2",
backend.FIELD_DAILY_LAST_TEXT);
let numeric3ID = yield backend.registerField(mID, "daily_last_numeric_1",
backend.FIELD_DAILY_LAST_NUMERIC);
let numeric4ID = yield backend.registerField(mID, "daily_last_numeric_2",
backend.FIELD_DAILY_LAST_NUMERIC);
let now = new Date();
let yesterday = new Date(now.getTime() - MILLISECONDS_PER_DAY);
let dayBefore = new Date(yesterday.getTime() - MILLISECONDS_PER_DAY);
yield backend.incrementDailyCounterFromFieldID(counterID, now);
yield backend.incrementDailyCounterFromFieldID(counterID, yesterday);
yield backend.incrementDailyCounterFromFieldID(counterID, dayBefore);
yield backend.setLastTextFromFieldID(text1ID, "hello", dayBefore);
yield backend.setLastTextFromFieldID(text2ID, "world", yesterday);
yield backend.setLastNumericFromFieldID(numeric1ID, 42, dayBefore);
yield backend.setLastNumericFromFieldID(numeric2ID, 43, yesterday);
yield backend.setDailyLastTextFromFieldID(text3ID, "foo", dayBefore);
yield backend.setDailyLastTextFromFieldID(text3ID, "bar", yesterday);
yield backend.setDailyLastTextFromFieldID(text4ID, "hello", dayBefore);
yield backend.setDailyLastTextFromFieldID(text4ID, "world", yesterday);
yield backend.setDailyLastNumericFromFieldID(numeric3ID, 40, dayBefore);
yield backend.setDailyLastNumericFromFieldID(numeric3ID, 41, yesterday);
yield backend.setDailyLastNumericFromFieldID(numeric4ID, 42, dayBefore);
yield backend.setDailyLastNumericFromFieldID(numeric4ID, 43, yesterday);
let days = yield backend.getDailyCounterCountsFromFieldID(counterID);
do_check_eq(days.size, 3);
yield backend.pruneDataBefore(yesterday);
days = yield backend.getDailyCounterCountsFromFieldID(counterID);
do_check_eq(days.size, 2);
do_check_false(days.hasDay(dayBefore));
do_check_null(yield backend.getLastTextFromFieldID(text1ID));
do_check_null(yield backend.getLastNumericFromFieldID(numeric1ID));
let result = yield backend.getLastTextFromFieldID(text2ID);
do_check_true(Array.isArray(result));
do_check_eq(result[1], "world");
result = yield backend.getLastNumericFromFieldID(numeric2ID);
do_check_true(Array.isArray(result));
do_check_eq(result[1], 43);
result = yield backend.getDailyLastNumericFromFieldID(numeric3ID);
do_check_eq(result.size, 1);
do_check_true(result.hasDay(yesterday));
result = yield backend.getDailyLastTextFromFieldID(text3ID);
do_check_eq(result.size, 1);
do_check_true(result.hasDay(yesterday));
yield backend.close();
});
add_task(function test_provider_state() {
let backend = yield Metrics.Storage("provider_state");
yield backend.registerMeasurement("foo", "bar", 1);
yield backend.setProviderState("foo", "apple", "orange");
let value = yield backend.getProviderState("foo", "apple");
do_check_eq(value, "orange");
yield backend.setProviderState("foo", "apple", "pear");
value = yield backend.getProviderState("foo", "apple");
do_check_eq(value, "pear");
yield backend.close();
});
add_task(function test_get_measurement_values() {
let backend = yield Metrics.Storage("get_measurement_values");
let mID = yield backend.registerMeasurement("foo", "bar", 1);
let id1 = yield backend.registerField(mID, "id1", backend.FIELD_DAILY_COUNTER);
let id2 = yield backend.registerField(mID, "id2", backend.FIELD_DAILY_DISCRETE_NUMERIC);
let id3 = yield backend.registerField(mID, "id3", backend.FIELD_DAILY_DISCRETE_TEXT);
let id4 = yield backend.registerField(mID, "id4", backend.FIELD_DAILY_LAST_NUMERIC);
let id5 = yield backend.registerField(mID, "id5", backend.FIELD_DAILY_LAST_TEXT);
let id6 = yield backend.registerField(mID, "id6", backend.FIELD_LAST_NUMERIC);
let id7 = yield backend.registerField(mID, "id7", backend.FIELD_LAST_TEXT);
let now = new Date();
let yesterday = new Date(now.getTime() - MILLISECONDS_PER_DAY);
yield backend.incrementDailyCounterFromFieldID(id1, now);
yield backend.addDailyDiscreteNumericFromFieldID(id2, 3, now);
yield backend.addDailyDiscreteNumericFromFieldID(id2, 4, now);
yield backend.addDailyDiscreteNumericFromFieldID(id2, 5, yesterday);
yield backend.addDailyDiscreteNumericFromFieldID(id2, 6, yesterday);
yield backend.addDailyDiscreteTextFromFieldID(id3, "1", now);
yield backend.addDailyDiscreteTextFromFieldID(id3, "2", now);
yield backend.addDailyDiscreteTextFromFieldID(id3, "3", yesterday);
yield backend.addDailyDiscreteTextFromFieldID(id3, "4", yesterday);
yield backend.setDailyLastNumericFromFieldID(id4, 1, now);
yield backend.setDailyLastNumericFromFieldID(id4, 2, yesterday);
yield backend.setDailyLastTextFromFieldID(id5, "foo", now);
yield backend.setDailyLastTextFromFieldID(id5, "bar", yesterday);
yield backend.setLastNumericFromFieldID(id6, 42, now);
yield backend.setLastTextFromFieldID(id7, "foo", now);
let fields = yield backend.getMeasurementValues(mID);
do_check_eq(Object.keys(fields).length, 2);
do_check_true("days" in fields);
do_check_true("singular" in fields);
do_check_eq(fields.days.size, 2);
do_check_true(fields.days.hasDay(now));
do_check_true(fields.days.hasDay(yesterday));
do_check_eq(fields.days.getDay(now).size, 5);
do_check_eq(fields.days.getDay(yesterday).size, 4);
do_check_eq(fields.days.getDay(now).get("id3")[0], 1);
do_check_eq(fields.days.getDay(yesterday).get("id4"), 2);
do_check_eq(fields.singular.size, 2);
do_check_eq(fields.singular.get("id6")[1], 42);
do_check_eq(fields.singular.get("id7")[1], "foo");
yield backend.close();
});

View File

@ -0,0 +1,9 @@
[DEFAULT]
head = head.js
tail =
skip-if = toolkit == 'android' || toolkit == 'gonk'
[test_load_modules.js]
[test_metrics_provider.js]
[test_metrics_provider_manager.js]
[test_metrics_storage.js]

View File

@ -12,6 +12,18 @@ DIRS += [
if CONFIG['MOZ_WIDGET_TOOLKIT'] != 'android' or CONFIG['MOZ_B2GDROID']:
DIRS += ['fxaccounts']
if CONFIG['MOZ_WIDGET_TOOLKIT'] != 'android':
# MOZ_SERVICES_HEALTHREPORT and therefore MOZ_DATA_REPORTING are
# defined on Android, but these features are implemented using Java.
if CONFIG['MOZ_SERVICES_HEALTHREPORT']:
DIRS += ['healthreport']
if CONFIG['MOZ_DATA_REPORTING']:
DIRS += ['datareporting']
if CONFIG['MOZ_SERVICES_METRICS']:
DIRS += ['metrics']
if CONFIG['MOZ_SERVICES_SYNC']:
DIRS += ['sync']
@ -20,3 +32,5 @@ if CONFIG['MOZ_B2G'] or CONFIG['MOZ_B2GDROID']:
if CONFIG['MOZ_SERVICES_CLOUDSYNC']:
DIRS += ['cloudsync']
SPHINX_TREES['services'] = 'docs'

View File

@ -31,7 +31,10 @@ class GeckoInstance(object):
"browser.urlbar.userMadeSearchSuggestionsChoice": True,
"browser.warnOnQuit": False,
"dom.ipc.reportProcessHangs": False,
"datareporting.healthreport.service.enabled": False,
"datareporting.healthreport.uploadEnabled": False,
"datareporting.healthreport.service.firstRun": False,
"datareporting.healthreport.logging.consoleEnabled": False,
"datareporting.policy.dataSubmissionEnabled": False,
"datareporting.policy.dataSubmissionPolicyAccepted": False,
"focusmanager.testmode": True,

View File

@ -3544,6 +3544,7 @@ static MOZ_CONSTEXPR_VAR TrackedDBEntry kTrackedDBs[] = {
TRACKEDDB_ENTRY("downloads.sqlite"),
TRACKEDDB_ENTRY("extensions.sqlite"),
TRACKEDDB_ENTRY("formhistory.sqlite"),
TRACKEDDB_ENTRY("healthreport.sqlite"),
TRACKEDDB_ENTRY("index.sqlite"),
TRACKEDDB_ENTRY("netpredictions.sqlite"),
TRACKEDDB_ENTRY("permissions.sqlite"),

View File

@ -36,6 +36,7 @@ const PREF_SERVER = PREF_BRANCH + "server";
const PREF_LOG_LEVEL = PREF_BRANCH_LOG + "level";
const PREF_LOG_DUMP = PREF_BRANCH_LOG + "dump";
const PREF_CACHED_CLIENTID = PREF_BRANCH + "cachedClientID";
const PREF_FHR_ENABLED = "datareporting.healthreport.service.enabled";
const PREF_FHR_UPLOAD_ENABLED = "datareporting.healthreport.uploadEnabled";
const PREF_SESSIONS_BRANCH = "datareporting.sessions.";
const PREF_UNIFIED = PREF_BRANCH + "unified";
@ -705,6 +706,15 @@ var Impl = {
return Promise.resolve();
}
// Only initialize the session recorder if FHR is enabled.
// TODO: move this after the |enableTelemetryRecording| block and drop the
// PREF_FHR_ENABLED check once we permanently switch over to unified Telemetry.
if (!this._sessionRecorder &&
(Preferences.get(PREF_FHR_ENABLED, true) || IS_UNIFIED_TELEMETRY)) {
this._sessionRecorder = new SessionRecorder(PREF_SESSIONS_BRANCH);
this._sessionRecorder.onStartup();
}
// This will trigger displaying the datachoices infobar.
TelemetryReportingPolicy.setup();
@ -713,12 +723,6 @@ var Impl = {
return Promise.resolve();
}
// Initialize the session recorder.
if (!this._sessionRecorder) {
this._sessionRecorder = new SessionRecorder(PREF_SESSIONS_BRANCH);
this._sessionRecorder.onStartup();
}
this._attachObservers();
// For very short session durations, we may never load the client

View File

@ -1779,6 +1779,9 @@ var Impl = {
reset();
}.bind(this));
reset();
return Promise.resolve();
};
// We can be in one the following states here:

View File

@ -1,12 +0,0 @@
#filter substitution
/* 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("datareporting.healthreport.infoURL", "https://www.mozilla.org/legal/privacy/firefox.html#health-report");
// Health Report is enabled by default on all channels.
pref("datareporting.healthreport.uploadEnabled", true);
pref("datareporting.healthreport.about.reportUrl", "https://fhr.cdn.mozilla.net/%LOCALE%/v4/");
pref("datareporting.healthreport.about.reportUrlUnified", "https://fhr.cdn.mozilla.net/%LOCALE%/v4/");

View File

@ -81,4 +81,3 @@ LOCAL_INCLUDES += [
]
SPHINX_TREES['telemetry'] = 'docs'
SPHINX_TREES['healthreport'] = 'docs/fhr'

View File

@ -22,6 +22,8 @@ const MILLISECONDS_PER_MINUTE = 60 * 1000;
const MILLISECONDS_PER_HOUR = 60 * MILLISECONDS_PER_MINUTE;
const MILLISECONDS_PER_DAY = 24 * MILLISECONDS_PER_HOUR;
const HAS_DATAREPORTINGSERVICE = "@mozilla.org/datareporting/service;1" in Cc;
const PREF_TELEMETRY_ENABLED = "toolkit.telemetry.enabled";
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;

View File

@ -31,6 +31,7 @@ const PREF_BRANCH = "toolkit.telemetry.";
const PREF_ENABLED = PREF_BRANCH + "enabled";
const PREF_ARCHIVE_ENABLED = PREF_BRANCH + "archive.enabled";
const PREF_FHR_UPLOAD_ENABLED = "datareporting.healthreport.uploadEnabled";
const PREF_FHR_SERVICE_ENABLED = "datareporting.healthreport.service.enabled";
const PREF_UNIFIED = PREF_BRANCH + "unified";
const PREF_OPTOUT_SAMPLE = PREF_BRANCH + "optoutSample";
@ -99,6 +100,7 @@ function run_test() {
Services.prefs.setBoolPref(PREF_ENABLED, true);
Services.prefs.setBoolPref(PREF_FHR_UPLOAD_ENABLED, true);
Services.prefs.setBoolPref(PREF_FHR_SERVICE_ENABLED, true);
Telemetry.asyncFetchTelemetryData(wrapWithExceptionHandler(run_next_test));
}
@ -206,7 +208,11 @@ add_task(function* test_pingHasClientId() {
let ping = yield PingServer.promiseNextPing();
checkPingFormat(ping, TEST_PING_TYPE, true, false);
Assert.equal(ping.clientId, gClientID, "The correct clientId must be reported.");
if (HAS_DATAREPORTINGSERVICE &&
Services.prefs.getBoolPref(PREF_FHR_UPLOAD_ENABLED)) {
Assert.equal(ping.clientId, gClientID,
"The correct clientId must be reported.");
}
});
add_task(function* test_pingHasEnvironment() {
@ -228,7 +234,11 @@ add_task(function* test_pingHasEnvironmentAndClientId() {
// Test a field in the environment build section.
Assert.equal(ping.application.buildId, ping.environment.build.buildId);
// Test that we have the correct clientId.
Assert.equal(ping.clientId, gClientID, "The correct clientId must be reported.");
if (HAS_DATAREPORTINGSERVICE &&
Services.prefs.getBoolPref(PREF_FHR_UPLOAD_ENABLED)) {
Assert.equal(ping.clientId, gClientID,
"The correct clientId must be reported.");
}
});
add_task(function* test_archivePings() {

View File

@ -20,6 +20,11 @@ Cu.import("resource://gre/modules/TelemetryController.jsm", this);
Cu.import("resource://gre/modules/TelemetrySession.jsm", this);
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyGetter(this, "gDatareportingService",
() => Cc["@mozilla.org/datareporting/service;1"]
.getService(Ci.nsISupports)
.wrappedJSObject);
// Force the Telemetry enabled preference so that TelemetrySession.reset() doesn't exit early.
Services.prefs.setBoolPref(PREF_TELEMETRY_ENABLED, true);
@ -66,5 +71,12 @@ function run_test() {
// Make sure we have a profile directory.
do_get_profile();
// Send the needed startup notifications to the datareporting service
// to ensure that it has been initialized.
if ("@mozilla.org/datareporting/service;1" in Cc) {
gDatareportingService.observe(null, "app-startup", null);
gDatareportingService.observe(null, "profile-after-change", null);
}
run_next_test();
}

View File

@ -34,6 +34,16 @@ function run_test() {
Services.prefs.setBoolPref(PREF_TELEMETRY_ENABLED, true);
Services.prefs.setBoolPref(PREF_FHR_UPLOAD_ENABLED, true);
// Send the needed startup notifications to the datareporting service
// to ensure that it has been initialized.
if (HAS_DATAREPORTINGSERVICE) {
let drs = Cc["@mozilla.org/datareporting/service;1"]
.getService(Ci.nsISupports)
.wrappedJSObject;
drs.observe(null, "app-startup", null);
drs.observe(null, "profile-after-change", null);
}
run_next_test();
}

View File

@ -17,6 +17,7 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
const PREF_BRANCH = "toolkit.telemetry.";
const PREF_SERVER = PREF_BRANCH + "server";
const PREF_DRS_ENABLED = "datareporting.healthreport.service.enabled";
const TEST_CHANNEL = "TestChannelABC";
@ -46,6 +47,8 @@ function run_test() {
loadAddonManager("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
Services.prefs.setBoolPref(PREF_TELEMETRY_ENABLED, true);
// We need to disable FHR in order to use the policy from telemetry.
Services.prefs.setBoolPref(PREF_DRS_ENABLED, false);
// Don't bypass the notifications in this test, we'll fake it.
Services.prefs.setBoolPref(PREF_BYPASS_NOTIFICATION, false);

View File

@ -19,6 +19,11 @@ Cu.import("resource://gre/modules/Task.jsm", this);
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
var {OS: {File, Path, Constants}} = Cu.import("resource://gre/modules/osfile.jsm", {});
XPCOMUtils.defineLazyGetter(this, "gDatareportingService",
() => Cc["@mozilla.org/datareporting/service;1"]
.getService(Ci.nsISupports)
.wrappedJSObject);
// We increment TelemetryStorage's MAX_PING_FILE_AGE and
// OVERDUE_PING_FILE_AGE by 1 minute so that our test pings exceed
// those points in time, even taking into account file system imprecision.
@ -160,6 +165,11 @@ function run_test() {
do_get_profile();
loadAddonManager("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
// Send the needed startup notifications to the datareporting service
// to ensure that it has been initialized.
gDatareportingService.observe(null, "app-startup", null);
gDatareportingService.observe(null, "profile-after-change", null);
Services.prefs.setBoolPref(PREF_TELEMETRY_ENABLED, true);
Services.prefs.setCharPref(TelemetryController.Constants.PREF_SERVER,
"http://localhost:" + PingServer.port);

View File

@ -63,6 +63,7 @@ const MS_IN_ONE_DAY = 24 * MS_IN_ONE_HOUR;
const PREF_BRANCH = "toolkit.telemetry.";
const PREF_SERVER = PREF_BRANCH + "server";
const PREF_FHR_UPLOAD_ENABLED = "datareporting.healthreport.uploadEnabled";
const PREF_FHR_SERVICE_ENABLED = "datareporting.healthreport.service.enabled";
const DATAREPORTING_DIR = "datareporting";
const ABORTED_PING_FILE_NAME = "aborted-session-ping";

View File

@ -9,6 +9,11 @@ Cu.import("resource://gre/modules/TelemetryController.jsm", this);
Cu.import("resource://gre/modules/TelemetrySession.jsm", this);
Cu.import('resource://gre/modules/XPCOMUtils.jsm');
XPCOMUtils.defineLazyGetter(this, "gDatareportingService",
() => Cc["@mozilla.org/datareporting/service;1"]
.getService(Ci.nsISupports)
.wrappedJSObject);
// The @mozilla/xre/app-info;1 XPCOM object provided by the xpcshell test harness doesn't
// implement the nsIAppInfo interface, which is needed by Services.jsm and
// TelemetrySession.jsm. updateAppInfo() creates and registers a minimal mock app-info.
@ -32,6 +37,13 @@ function getSimpleMeasurementsFromTelemetryController() {
}
function initialiseTelemetry() {
// Send the needed startup notifications to the datareporting service
// to ensure that it has been initialized.
if ("@mozilla.org/datareporting/service;1" in Cc) {
gDatareportingService.observe(null, "app-startup", null);
gDatareportingService.observe(null, "profile-after-change", null);
}
return TelemetryController.setup().then(TelemetrySession.setup);
}

View File

@ -22,9 +22,6 @@ const STARTUP_RETRY_INTERVAL_MS = 5000;
// Wait up to 5 minutes for startup measurements before giving up.
const MAX_STARTUP_TRIES = 300000 / STARTUP_RETRY_INTERVAL_MS;
const LOGGER_NAME = "Toolkit.Telemetry";
const LOGGER_PREFIX = "SessionRecorder::";
/**
* Records information about browser sessions.
*
@ -67,7 +64,7 @@ this.SessionRecorder = function (branch) {
throw new Error("branch argument must end with '.': " + branch);
}
this._log = Log.repository.getLoggerWithMessagePrefix(LOGGER_NAME, LOGGER_PREFIX);
this._log = Log.repository.getLogger("Services.DataReporting.SessionRecorder");
this._prefs = new Preferences(branch);
this._lastActivityWasInactive = false;

View File

@ -14,6 +14,7 @@ MOZ_EXTENSIONS_DEFAULT=" gio"
MOZ_URL_CLASSIFIER=1
MOZ_SERVICES_COMMON=1
MOZ_SERVICES_CRYPTO=1
MOZ_SERVICES_METRICS=1
MOZ_SERVICES_SYNC=1
MOZ_MEDIA_NAVIGATOR=1
MOZ_SERVICES_HEALTHREPORT=1