mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Merge m-c to inbound.
This commit is contained in:
commit
2f566e8212
@ -7,134 +7,9 @@ html, body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: window;
|
||||
color: windowtext;
|
||||
font-family: "Trebuchet MS", "Helvetica";
|
||||
}
|
||||
|
||||
#about-header {
|
||||
padding: 6px 20px;
|
||||
min-height: 60px;
|
||||
border-bottom: 1px solid #999999;
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background-image: linear-gradient(to bottom, #E66000, #BB2200);
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
#control-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#content-line {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#state-intro {
|
||||
background-image: linear-gradient(to bottom, #EAEFF2, #D4DDE4);
|
||||
border: 1px solid #999999;
|
||||
border-radius: 6px;
|
||||
margin: 12px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
#settings-controls {
|
||||
padding-top: 15px;
|
||||
}
|
||||
|
||||
#control-container {
|
||||
padding-top: 15px;
|
||||
}
|
||||
|
||||
#content[state="default"] #details-hide,
|
||||
#content[state="default"] #btn-optin,
|
||||
#content[state="default"] #intro-disabled {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#content[state="showDetails"] #details-show,
|
||||
#content[state="showDetails"] #btn-optin,
|
||||
#content[state="showDetails"] #intro-disabled {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#content[state="showReport"] #details-hide,
|
||||
#content[state="showReport"] #report-show,
|
||||
#content[state="showReport"] #btn-optin,
|
||||
#content[state="showReport"] #intro-disabled {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#content[state="disabled"] #details-hide,
|
||||
#content[state="disabled"] #details-show,
|
||||
#content[state="disabled"] #btn-optout,
|
||||
#content[state="disabled"] #intro-enabled {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#details-view,
|
||||
#report-view {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#data-view {
|
||||
height: auto;
|
||||
margin-top: 8px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid #999999;
|
||||
border-radius: 6px;
|
||||
margin: 12px;
|
||||
}
|
||||
|
||||
#remote-report,
|
||||
#report-view {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 600px;
|
||||
}
|
||||
|
||||
#report-show {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 60px;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
background-image: linear-gradient(to bottom, #80BB2E, #547D1C);
|
||||
color: #ffffff;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
#details-view {
|
||||
border: 1px solid #999999;
|
||||
border-radius: 6px;
|
||||
margin: 12px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
#content[state="showDetails"],
|
||||
#content[state="showReport"],
|
||||
#content[state="showDetails"] #details-view,
|
||||
#content[state="showReport"] #report-view {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#content[state="showReport"] #report-show {
|
||||
display: none;
|
||||
}
|
||||
#content[state="showReport"] #report-view,
|
||||
#remote-report {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
display: flex;
|
||||
}
|
||||
|
@ -7,6 +7,7 @@
|
||||
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
|
||||
|
||||
Cu.import("resource://services-common/preferences.js");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
const reporter = Cc["@mozilla.org/datareporting/service;1"]
|
||||
.getService(Ci.nsISupports)
|
||||
@ -18,99 +19,123 @@ const policy = Cc["@mozilla.org/datareporting/service;1"]
|
||||
.wrappedJSObject
|
||||
.policy;
|
||||
|
||||
const prefs = new Preferences("datareporting.healthreport.about.");
|
||||
const prefs = new Preferences("datareporting.healthreport.");
|
||||
|
||||
function getLocale() {
|
||||
return Cc["@mozilla.org/chrome/chrome-registry;1"]
|
||||
.getService(Ci.nsIXULChromeRegistry)
|
||||
.getSelectedLocale("global");
|
||||
|
||||
let healthReportWrapper = {
|
||||
init: function () {
|
||||
reporter.onInit().then(healthReportWrapper.refreshPayload,
|
||||
healthReportWrapper.handleInitFailure);
|
||||
|
||||
let iframe = document.getElementById("remote-report");
|
||||
iframe.addEventListener("load", healthReportWrapper.initRemotePage, false);
|
||||
let report = this._getReportURI();
|
||||
iframe.src = report.spec;
|
||||
prefs.observe("uploadEnabled", this.updatePrefState, healthReportWrapper);
|
||||
},
|
||||
|
||||
uninit: function () {
|
||||
prefs.ignore("uploadEnabled", this.updatePrefState, healthReportWrapper);
|
||||
},
|
||||
|
||||
_getReportURI: function () {
|
||||
let url = Services.urlFormatter.formatURLPref("datareporting.healthreport.about.reportUrl");
|
||||
return Services.io.newURI(url, null, null);
|
||||
},
|
||||
|
||||
onOptIn: function () {
|
||||
policy.recordHealthReportUploadEnabled(true,
|
||||
"Health report page sent opt-in command.");
|
||||
this.updatePrefState();
|
||||
},
|
||||
|
||||
onOptOut: function () {
|
||||
policy.recordHealthReportUploadEnabled(false,
|
||||
"Health report page sent opt-out command.");
|
||||
this.updatePrefState();
|
||||
},
|
||||
|
||||
updatePrefState: function () {
|
||||
try {
|
||||
let prefs = {
|
||||
enabled: policy.healthReportUploadEnabled,
|
||||
}
|
||||
this.injectData("prefs", prefs);
|
||||
} catch (e) {
|
||||
this.reportFailure(this.ERROR_PREFS_FAILED);
|
||||
}
|
||||
},
|
||||
|
||||
refreshPayload: function () {
|
||||
reporter.collectAndObtainJSONPayload().then(healthReportWrapper.updatePayload,
|
||||
healthReportWrapper.handlePayloadFailure);
|
||||
},
|
||||
|
||||
updatePayload: function (data) {
|
||||
healthReportWrapper.injectData("payload", data);
|
||||
},
|
||||
|
||||
injectData: function (type, content) {
|
||||
let report = this._getReportURI();
|
||||
|
||||
// file URIs can't be used for targetOrigin, so we use "*" for this special case
|
||||
// in all other cases, pass in the URL to the report so we properly restrict the message dispatch
|
||||
let reportUrl = report.scheme == "file" ? "*" : report.spec;
|
||||
|
||||
let data = {
|
||||
type: type,
|
||||
content: content
|
||||
}
|
||||
|
||||
let iframe = document.getElementById("remote-report");
|
||||
iframe.contentWindow.postMessage(data, reportUrl);
|
||||
},
|
||||
|
||||
handleRemoteCommand: function (evt) {
|
||||
switch (evt.detail.command) {
|
||||
case "DisableDataSubmission":
|
||||
this.onOptOut();
|
||||
break;
|
||||
case "EnableDataSubmission":
|
||||
this.onOptIn();
|
||||
break;
|
||||
case "RequestCurrentPrefs":
|
||||
this.updatePrefState();
|
||||
break;
|
||||
case "RequestCurrentPayload":
|
||||
this.refreshPayload();
|
||||
break;
|
||||
default:
|
||||
Cu.reportError("Unexpected remote command received: " + evt.detail.command + ". Ignoring command.");
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
initRemotePage: function () {
|
||||
let iframe = document.getElementById("remote-report").contentDocument;
|
||||
iframe.addEventListener("RemoteHealthReportCommand",
|
||||
function onCommand(e) {healthReportWrapper.handleRemoteCommand(e);},
|
||||
false);
|
||||
healthReportWrapper.updatePrefState();
|
||||
},
|
||||
|
||||
// error handling
|
||||
ERROR_INIT_FAILED: 1,
|
||||
ERROR_PAYLOAD_FAILED: 2,
|
||||
ERROR_PREFS_FAILED: 3,
|
||||
|
||||
reportFailure: function (error) {
|
||||
let details = {
|
||||
errorType: error,
|
||||
}
|
||||
healthReportWrapper.injectData("error", details);
|
||||
},
|
||||
|
||||
handleInitFailure: function () {
|
||||
healthReportWrapper.reportFailure(healthReportWrapper.ERROR_INIT_FAILED);
|
||||
},
|
||||
|
||||
handlePayloadFailure: function () {
|
||||
healthReportWrapper.reportFailure(healthReportWrapper.ERROR_PAYLOAD_FAILED);
|
||||
},
|
||||
}
|
||||
|
||||
function init() {
|
||||
refreshWithDataSubmissionFlag(policy.healthReportUploadEnabled);
|
||||
refreshJSONPayload();
|
||||
document.getElementById("details-link").href = prefs.get("glossaryUrl");
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the state of the page to reflect the current data submission state.
|
||||
*
|
||||
* @param enabled
|
||||
* (bool) Whether data submission is enabled.
|
||||
*/
|
||||
function refreshWithDataSubmissionFlag(enabled) {
|
||||
if (!enabled) {
|
||||
updateView("disabled");
|
||||
} else {
|
||||
updateView("default");
|
||||
}
|
||||
}
|
||||
|
||||
function updateView(state="default") {
|
||||
let content = document.getElementById("content");
|
||||
let controlContainer = document.getElementById("control-container");
|
||||
content.setAttribute("state", state);
|
||||
controlContainer.setAttribute("state", state);
|
||||
}
|
||||
|
||||
function refreshDataView(data) {
|
||||
let noData = document.getElementById("data-no-data");
|
||||
let dataEl = document.getElementById("raw-data");
|
||||
|
||||
noData.style.display = data ? "none" : "inline";
|
||||
dataEl.style.display = data ? "block" : "none";
|
||||
if (data) {
|
||||
dataEl.textContent = JSON.stringify(data, null, 2);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the page has the latest version of the uploaded JSON payload.
|
||||
*/
|
||||
function refreshJSONPayload() {
|
||||
reporter.getLastPayload().then(refreshDataView);
|
||||
}
|
||||
|
||||
function onOptInClick() {
|
||||
policy.recordHealthReportUploadEnabled(true,
|
||||
"Clicked opt in button on about page.");
|
||||
refreshWithDataSubmissionFlag(true);
|
||||
}
|
||||
|
||||
function onOptOutClick() {
|
||||
let prompts = Cc["@mozilla.org/embedcomp/prompt-service;1"]
|
||||
.getService(Ci.nsIPromptService);
|
||||
|
||||
let messages = document.getElementById("optout-confirmationPrompt");
|
||||
let title = messages.getAttribute("confirmationPrompt_title");
|
||||
let message = messages.getAttribute("confirmationPrompt_message");
|
||||
|
||||
if (!prompts.confirm(window, title, message)) {
|
||||
return;
|
||||
}
|
||||
|
||||
policy.recordHealthReportUploadEnabled(false,
|
||||
"Clicked opt out button on about page.");
|
||||
refreshWithDataSubmissionFlag(false);
|
||||
updateView("disabled");
|
||||
}
|
||||
|
||||
function onShowRawDataClick() {
|
||||
updateView("showDetails");
|
||||
refreshJSONPayload();
|
||||
}
|
||||
|
||||
function onHideRawDataClick() {
|
||||
updateView("default");
|
||||
}
|
||||
|
||||
function onShowReportClick() {
|
||||
updateView("showReport");
|
||||
document.getElementById("remote-report").src = prefs.get("reportUrl");
|
||||
}
|
||||
|
||||
function onHideReportClick() {
|
||||
updateView("default");
|
||||
document.getElementById("remote-report").src = "";
|
||||
}
|
||||
|
||||
|
@ -22,50 +22,9 @@
|
||||
<script type="text/javascript;version=1.8"
|
||||
src="chrome://browser/content/abouthealthreport/abouthealth.js" />
|
||||
</head>
|
||||
<body dir="&abouthealth.locale-direction;" class="aboutPageWideContainer" onload="init();">
|
||||
<div id="about-header">
|
||||
<h1>&abouthealth.header;</h1>
|
||||
<img src="chrome://branding/content/icon48.png"/>
|
||||
</div>
|
||||
|
||||
<div id="content">
|
||||
<div id="state-intro">
|
||||
<h3>&abouthealth.intro.title;</h3>
|
||||
<div id="content-line">
|
||||
<p id="intro-enabled">&abouthealth.intro-enabled;</p>
|
||||
<p id="intro-disabled">&abouthealth.intro-disabled;</p>
|
||||
<div id="control-container">
|
||||
<button id="btn-optin" onclick="onOptInClick();">&abouthealth.optin;</button>
|
||||
<button id="btn-optout" onclick="onOptOutClick();">&abouthealth.optout;</button>
|
||||
|
||||
<button id="details-show" onclick="onShowRawDataClick()">&abouthealth.show-raw-data;</button>
|
||||
<button id="details-hide" onclick="onHideRawDataClick()">&abouthealth.hide-raw-data;</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="details-view">
|
||||
<p id="details-description">
|
||||
&abouthealth.details.description-start;
|
||||
<a id="details-link">&abouthealth.details-link;</a>
|
||||
&abouthealth.details.description-end;
|
||||
</p>
|
||||
|
||||
<p id="data-no-data">&abouthealth.no-data-available;</p>
|
||||
<pre id="raw-data" style="display: none"></pre>
|
||||
</div>
|
||||
|
||||
<div id="data-view">
|
||||
<button id="report-show" onclick="onShowReportClick()">&abouthealth.show-report;</button>
|
||||
<div id="report-view">
|
||||
<iframe id="remote-report"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="hidden" id="optout-confirmationPrompt"
|
||||
confirmationPrompt_title="&abouthealth.optout.confirmationPrompt.title;"
|
||||
confirmationPrompt_message="&abouthealth.optout.confirmationPrompt.message;"
|
||||
/>
|
||||
<body onload="healthReportWrapper.init();"
|
||||
onunload="healthReportWrapper.uninit();">
|
||||
<iframe id="remote-report"/>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
@ -144,7 +144,7 @@ Site.prototype = {
|
||||
// Register drag-and-drop event handlers.
|
||||
this._node.addEventListener("dragstart", this, false);
|
||||
this._node.addEventListener("dragend", this, false);
|
||||
this._node.addEventListener("mouseenter", this, false);
|
||||
this._node.addEventListener("mouseover", this, false);
|
||||
|
||||
let controls = this.node.querySelectorAll(".newtab-control");
|
||||
for (let i = 0; i < controls.length; i++)
|
||||
@ -174,7 +174,8 @@ Site.prototype = {
|
||||
else
|
||||
this.pin();
|
||||
break;
|
||||
case "mouseenter":
|
||||
case "mouseover":
|
||||
this._node.removeEventListener("mouseover", this, false);
|
||||
this._speculativeConnect();
|
||||
break;
|
||||
case "dragstart":
|
||||
|
@ -316,7 +316,9 @@ _BROWSER_FILES = \
|
||||
browser_pluginCrashCommentAndURL.js \
|
||||
pluginCrashCommentAndURL.html \
|
||||
browser_private_no_prompt.js \
|
||||
browser_blob-channelname.js
|
||||
browser_blob-channelname.js \
|
||||
browser_aboutHealthReport.js \
|
||||
healthreport_testRemoteCommands.html \
|
||||
$(NULL)
|
||||
|
||||
# Disable test on Windows due to frequent failures (bug 841341)
|
||||
|
105
browser/base/content/test/browser_aboutHealthReport.js
Normal file
105
browser/base/content/test/browser_aboutHealthReport.js
Normal file
@ -0,0 +1,105 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/
|
||||
*/
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Promise",
|
||||
"resource://gre/modules/commonjs/sdk/core/promise.js");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Task",
|
||||
"resource://gre/modules/Task.jsm");
|
||||
|
||||
registerCleanupFunction(function() {
|
||||
// Ensure we don't pollute prefs for next tests.
|
||||
try {
|
||||
Services.prefs.clearUserPref("datareporting.healthreport.about.reportUrl");
|
||||
let policy = Cc["@mozilla.org/datareporting/service;1"]
|
||||
.getService(Ci.nsISupports)
|
||||
.wrappedJSObject
|
||||
.policy;
|
||||
policy.recordHealthReportUploadEnabled(true,
|
||||
"Resetting after tests.");
|
||||
} catch (ex) {}
|
||||
});
|
||||
|
||||
let gTests = [
|
||||
|
||||
{
|
||||
desc: "Test the remote commands",
|
||||
setup: function ()
|
||||
{
|
||||
Services.prefs.setCharPref("datareporting.healthreport.about.reportUrl",
|
||||
"https://example.com/browser/browser/base/content/test/healthreport_testRemoteCommands.html");
|
||||
},
|
||||
run: function ()
|
||||
{
|
||||
let deferred = Promise.defer();
|
||||
|
||||
let policy = Cc["@mozilla.org/datareporting/service;1"]
|
||||
.getService(Ci.nsISupports)
|
||||
.wrappedJSObject
|
||||
.policy;
|
||||
|
||||
let results = 0;
|
||||
try {
|
||||
let win = gBrowser.contentWindow;
|
||||
win.addEventListener("message", function testLoad(e) {
|
||||
if (e.data.type == "testResult") {
|
||||
ok(e.data.pass, e.data.info);
|
||||
results++;
|
||||
}
|
||||
else if (e.data.type == "testsComplete") {
|
||||
is(results, e.data.count, "Checking number of results received matches the number of tests that should have run");
|
||||
win.removeEventListener("message", testLoad, false, true);
|
||||
deferred.resolve();
|
||||
}
|
||||
|
||||
}, false, 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);
|
||||
test.setup();
|
||||
|
||||
yield promiseNewTabLoadEvent("about:healthreport");
|
||||
|
||||
yield test.run();
|
||||
|
||||
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) {
|
||||
iframe.removeEventListener("load", frameLoad, false);
|
||||
deferred.resolve();
|
||||
}, false);
|
||||
}, true);
|
||||
return deferred.promise;
|
||||
}
|
||||
|
128
browser/base/content/test/healthreport_testRemoteCommands.html
Normal file
128
browser/base/content/test/healthreport_testRemoteCommands.html
Normal file
@ -0,0 +1,128 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
|
||||
<script>
|
||||
|
||||
function init() {
|
||||
window.addEventListener("message", function process(e) {doTest(e)}, false);
|
||||
doTest();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
var tests = [
|
||||
{
|
||||
info: "Checking initial value is enabled",
|
||||
event: "RequestCurrentPrefs",
|
||||
payloadType: "prefs",
|
||||
validateResponse: function(payload) {
|
||||
return checkSubmissionValue(payload, true);
|
||||
},
|
||||
},
|
||||
{
|
||||
info: "Verifying disabling works",
|
||||
event: "DisableDataSubmission",
|
||||
payloadType: "prefs",
|
||||
validateResponse: function(payload) {
|
||||
return checkSubmissionValue(payload, false);
|
||||
},
|
||||
},
|
||||
{
|
||||
info: "Verifying we're still disabled",
|
||||
event: "RequestCurrentPrefs",
|
||||
payloadType: "prefs",
|
||||
validateResponse: function(payload) {
|
||||
return checkSubmissionValue(payload, false);
|
||||
},
|
||||
},
|
||||
{
|
||||
info: "Verifying we can get a payload while submission is disabled",
|
||||
event: "RequestCurrentPayload",
|
||||
payloadType: "payload",
|
||||
validateResponse: function(payload) {
|
||||
return validatePayload(payload);
|
||||
},
|
||||
},
|
||||
{
|
||||
info: "Verifying enabling works",
|
||||
event: "EnableDataSubmission",
|
||||
payloadType: "prefs",
|
||||
validateResponse: function(payload) {
|
||||
return checkSubmissionValue(payload, true);
|
||||
},
|
||||
},
|
||||
{
|
||||
info: "Verifying we're still re-enabled",
|
||||
event: "RequestCurrentPrefs",
|
||||
payloadType: "prefs",
|
||||
validateResponse: function(payload) {
|
||||
return checkSubmissionValue(payload, true);
|
||||
},
|
||||
},
|
||||
{
|
||||
info: "Verifying we can get a payload after re-enabling",
|
||||
event: "RequestCurrentPayload",
|
||||
payloadType: "payload",
|
||||
validateResponse: function(payload) {
|
||||
return validatePayload(payload);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
var currentTest = -1;
|
||||
function doTest(evt) {
|
||||
if (evt) {
|
||||
if (currentTest < 0 || !evt.data.content)
|
||||
return; // not yet testing
|
||||
|
||||
var test = tests[currentTest];
|
||||
if (evt.data.type != test.payloadType)
|
||||
return; // skip unrequested events
|
||||
|
||||
var error = JSON.stringify(evt.data.content);
|
||||
var pass = false;
|
||||
try {
|
||||
pass = test.validateResponse(evt.data.content)
|
||||
} catch (e) {}
|
||||
reportResult(test.info, pass, error);
|
||||
}
|
||||
// start the next test if there are any left
|
||||
if (tests[++currentTest])
|
||||
sendToBrowser(tests[currentTest].event);
|
||||
else
|
||||
reportFinished();
|
||||
}
|
||||
|
||||
function reportResult(info, pass, error) {
|
||||
var data = {type: "testResult", info: info, pass: pass, error: error};
|
||||
window.parent.postMessage(data, "*");
|
||||
}
|
||||
|
||||
function reportFinished(cmd) {
|
||||
var data = {type: "testsComplete", count: tests.length};
|
||||
window.parent.postMessage(data, "*");
|
||||
}
|
||||
|
||||
function sendToBrowser(type) {
|
||||
var event = new CustomEvent("RemoteHealthReportCommand", {detail: {command: type}, bubbles: true});
|
||||
document.dispatchEvent(event);
|
||||
}
|
||||
|
||||
</script>
|
||||
</head>
|
||||
<body onload="init()">
|
||||
</body>
|
||||
</html>
|
@ -2,30 +2,5 @@
|
||||
- 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/. -->
|
||||
|
||||
<!-- metrics.locale-direction instead of the usual local.dir entity, so RTL can skip translating page. -->
|
||||
<!ENTITY abouthealth.locale-direction "ltr">
|
||||
|
||||
<!-- LOCALIZATION NOTE (abouthealth.pagetitle): Firefox Health Report is a proper noun in en-US, please keep this in mind. -->
|
||||
<!ENTITY abouthealth.pagetitle "&brandShortName; Health Report">
|
||||
<!ENTITY abouthealth.header "&brandFullName; Health Report">
|
||||
|
||||
<!ENTITY abouthealth.intro.title "What is &brandShortName; Health Report?">
|
||||
|
||||
<!ENTITY abouthealth.intro-enabled "&brandFullName; collects some data about your computer and usage in order to provide you with a better browser experience.">
|
||||
<!ENTITY abouthealth.intro-disabled "You are currently not submitting usage data to &vendorShortName;. You can help us make &brandShortName; better by clicking the "&abouthealth.optin;" button.">
|
||||
|
||||
<!ENTITY abouthealth.optin "Help make &brandShortName; better">
|
||||
<!ENTITY abouthealth.optout "Turn Off Reporting">
|
||||
|
||||
<!ENTITY abouthealth.optout.confirmationPrompt.title "Stop data submission?">
|
||||
<!ENTITY abouthealth.optout.confirmationPrompt.message "Are you sure you want to opt out and delete all your anonymous product data stored on &vendorShortName; servers?">
|
||||
|
||||
<!ENTITY abouthealth.show-raw-data "Show Details">
|
||||
<!ENTITY abouthealth.hide-raw-data "Hide Details">
|
||||
|
||||
<!ENTITY abouthealth.show-report "Show &brandShortName; Report">
|
||||
|
||||
<!ENTITY abouthealth.details.description-start "This is the data &brandFullName; is submitting in order for &brandShortName; Health Report to function. You can ">
|
||||
<!ENTITY abouthealth.details-link "learn more">
|
||||
<!ENTITY abouthealth.details.description-end " about what we collect and submit.">
|
||||
<!ENTITY abouthealth.no-data-available "There is no previous submission to display. Please check back later.">
|
||||
|
@ -22,7 +22,7 @@ interface nsISearchSubmission : nsISupports
|
||||
readonly attribute nsIURI uri;
|
||||
};
|
||||
|
||||
[scriptable, uuid(6839f025-2e25-408e-892e-c2c2fa5650c5)]
|
||||
[scriptable, uuid(ccf6aa20-10a9-4a0c-a81d-31b10ea846de)]
|
||||
interface nsISearchEngine : nsISupports
|
||||
{
|
||||
/**
|
||||
@ -132,6 +132,12 @@ interface nsISearchEngine : nsISupports
|
||||
*/
|
||||
readonly attribute long type;
|
||||
|
||||
/**
|
||||
* An optional unique identifier for this search engine within the context of
|
||||
* the distribution, as provided by the distributing entity.
|
||||
*/
|
||||
readonly attribute AString identifier;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -692,10 +692,16 @@ add_test(function test_polling_implicit_acceptance() {
|
||||
});
|
||||
|
||||
let count = 0;
|
||||
|
||||
// Track JS elapsed time, so we can decide if we've waited for enough ticks.
|
||||
let start;
|
||||
Object.defineProperty(policy, "checkStateAndTrigger", {
|
||||
value: function CheckStateAndTriggerProxy() {
|
||||
count++;
|
||||
print("checkStateAndTrigger count: " + count);
|
||||
let now = Date.now();
|
||||
let delta = now - start;
|
||||
print("checkStateAndTrigger count: " + count + ", now " + now +
|
||||
", delta " + delta);
|
||||
|
||||
// Account for some slack.
|
||||
DataReportingPolicy.prototype.checkStateAndTrigger.call(policy);
|
||||
@ -707,6 +713,9 @@ add_test(function test_polling_implicit_acceptance() {
|
||||
// 3) still ~50ms away from implicit acceptance
|
||||
// 4) Implicit acceptance recorded. Data submission requested.
|
||||
// 5) Request still pending. No new submission requested.
|
||||
//
|
||||
// Note that, due to the inaccuracy of timers, 4 might not happen until 5
|
||||
// firings have occurred. Yay. So we watch times, not just counts.
|
||||
|
||||
do_check_eq(listener.notifyUserCount, 1);
|
||||
|
||||
@ -714,17 +723,17 @@ add_test(function test_polling_implicit_acceptance() {
|
||||
listener.lastNotifyRequest.onUserNotifyComplete();
|
||||
}
|
||||
|
||||
if (count < 4) {
|
||||
if (delta <= 750) {
|
||||
do_check_false(policy.dataSubmissionPolicyAccepted);
|
||||
do_check_eq(listener.requestDataUploadCount, 0);
|
||||
} else {
|
||||
} else if (count > 3) {
|
||||
do_check_true(policy.dataSubmissionPolicyAccepted);
|
||||
do_check_eq(policy.dataSubmissionPolicyResponseType,
|
||||
"accepted-implicit-time-elapsed");
|
||||
do_check_eq(listener.requestDataUploadCount, 1);
|
||||
}
|
||||
|
||||
if (count > 4) {
|
||||
if ((count > 4) && policy.dataSubmissionPolicyAccepted) {
|
||||
do_check_eq(listener.requestDataUploadCount, 1);
|
||||
policy.stopPolling();
|
||||
run_next_test();
|
||||
@ -734,6 +743,7 @@ add_test(function test_polling_implicit_acceptance() {
|
||||
|
||||
policy.firstRunDate = new Date(Date.now() - 4 * 24 * 60 * 60 * 1000);
|
||||
policy.nextDataSubmissionDate = new Date(Date.now());
|
||||
start = Date.now();
|
||||
policy.startPolling();
|
||||
});
|
||||
|
||||
|
@ -1,10 +1,12 @@
|
||||
# Register Firefox Health Report providers.
|
||||
category healthreport-js-provider AddonsProvider resource://gre/modules/HealthReport.jsm
|
||||
category healthreport-js-provider AppInfoProvider resource://gre/modules/HealthReport.jsm
|
||||
category healthreport-js-provider CrashesProvider resource://gre/modules/HealthReport.jsm
|
||||
category healthreport-js-provider SysInfoProvider resource://gre/modules/HealthReport.jsm
|
||||
category healthreport-js-provider ProfileMetadataProvider resource://gre/modules/HealthReport.jsm
|
||||
category healthreport-js-provider SearchesProvider resource://gre/modules/HealthReport.jsm
|
||||
category healthreport-js-provider SessionsProvider resource://gre/modules/HealthReport.jsm
|
||||
category healthreport-js-provider PlacesProvider resource://gre/modules/HealthReport.jsm
|
||||
category healthreport-js-provider-default AddonsProvider resource://gre/modules/HealthReport.jsm
|
||||
category healthreport-js-provider-default AppInfoProvider resource://gre/modules/HealthReport.jsm
|
||||
category healthreport-js-provider-default CrashesProvider 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".
|
||||
|
@ -20,7 +20,13 @@ 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", "healthreport-js-provider");
|
||||
|
||||
pref("datareporting.healthreport.about.glossaryUrl", "https://services.mozilla.com/healthreport/glossary.html");
|
||||
pref("datareporting.healthreport.about.reportUrl", "https://services.mozilla.com/healthreport/placeholder.html");
|
||||
pref("datareporting.healthreport.service.providerCategories",
|
||||
#if MOZ_UPDATE_CHANNEL == release
|
||||
"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%/");
|
||||
|
@ -53,6 +53,7 @@ const TELEMETRY_SHUTDOWN_DELAY = "HEALTHREPORT_SHUTDOWN_DELAY_MS";
|
||||
const TELEMETRY_COLLECT_CONSTANT = "HEALTHREPORT_COLLECT_CONSTANT_DATA_MS";
|
||||
const TELEMETRY_COLLECT_DAILY = "HEALTHREPORT_COLLECT_DAILY_MS";
|
||||
const TELEMETRY_SHUTDOWN = "HEALTHREPORT_SHUTDOWN_MS";
|
||||
const TELEMETRY_COLLECT_CHECKPOINT = "HEALTHREPORT_POST_COLLECT_CHECKPOINT_MS";
|
||||
|
||||
/**
|
||||
* This is the abstract base class of `HealthReporter`. It exists so that
|
||||
@ -534,6 +535,18 @@ AbstractHealthReporter.prototype = Object.freeze({
|
||||
}
|
||||
}
|
||||
|
||||
// Flush gathered data to disk. This will incur an fsync. But, if
|
||||
// there is ever a time we want to persist data to disk, it's
|
||||
// after a massive collection.
|
||||
try {
|
||||
TelemetryStopwatch.start(TELEMETRY_COLLECT_CHECKPOINT, this);
|
||||
yield this._storage.checkpoint();
|
||||
TelemetryStopwatch.finish(TELEMETRY_COLLECT_CHECKPOINT, this);
|
||||
} catch (ex) {
|
||||
TelemetryStopwatch.cancel(TELEMETRY_COLLECT_CHECKPOINT, this);
|
||||
throw ex;
|
||||
}
|
||||
|
||||
throw new Task.Result();
|
||||
}.bind(this));
|
||||
},
|
||||
|
@ -279,9 +279,7 @@ AppInfoProvider.prototype = Object.freeze({
|
||||
|
||||
|
||||
collectConstantData: function () {
|
||||
return this.enqueueStorageOperation(function collect() {
|
||||
return Task.spawn(this._populateConstants.bind(this));
|
||||
}.bind(this));
|
||||
return this.storage.enqueueTransaction(this._populateConstants.bind(this));
|
||||
},
|
||||
|
||||
_populateConstants: function () {
|
||||
@ -424,9 +422,7 @@ SysInfoProvider.prototype = Object.freeze({
|
||||
},
|
||||
|
||||
collectConstantData: function () {
|
||||
return this.enqueueStorageOperation(function collection() {
|
||||
return Task.spawn(this._populateConstants.bind(this));
|
||||
}.bind(this));
|
||||
return this.storage.enqueueTransaction(this._populateConstants.bind(this));
|
||||
},
|
||||
|
||||
_populateConstants: function () {
|
||||
@ -878,7 +874,7 @@ CrashesProvider.prototype = Object.freeze({
|
||||
pullOnly: true,
|
||||
|
||||
collectConstantData: function () {
|
||||
return Task.spawn(this._populateCrashCounts.bind(this));
|
||||
return this.storage.enqueueTransaction(this._populateCrashCounts.bind(this));
|
||||
},
|
||||
|
||||
_populateCrashCounts: function () {
|
||||
@ -888,13 +884,17 @@ CrashesProvider.prototype = Object.freeze({
|
||||
let pending = yield service.getPendingFiles();
|
||||
let submitted = yield service.getSubmittedFiles();
|
||||
|
||||
function getAgeLimit() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let lastCheck = yield this.getState("lastCheck");
|
||||
if (!lastCheck) {
|
||||
lastCheck = 0;
|
||||
lastCheck = getAgeLimit();
|
||||
} else {
|
||||
lastCheck = parseInt(lastCheck, 10);
|
||||
if (Number.isNaN(lastCheck)) {
|
||||
lastCheck = 0;
|
||||
lastCheck = getAgeLimit();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1071,15 +1071,11 @@ PlacesProvider.prototype = Object.freeze({
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Records search counts per day per engine and where search initiated.
|
||||
*/
|
||||
function SearchCountMeasurement() {
|
||||
function SearchCountMeasurement1() {
|
||||
Metrics.Measurement.call(this);
|
||||
}
|
||||
|
||||
SearchCountMeasurement.prototype = Object.freeze({
|
||||
SearchCountMeasurement1.prototype = Object.freeze({
|
||||
__proto__: Metrics.Measurement.prototype,
|
||||
|
||||
name: "counts",
|
||||
@ -1109,14 +1105,206 @@ SearchCountMeasurement.prototype = Object.freeze({
|
||||
"other.searchbar": DAILY_COUNTER_FIELD,
|
||||
"other.urlbar": DAILY_COUNTER_FIELD,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Records search counts per day per engine and where search initiated.
|
||||
*
|
||||
* We want to record granular details for individual locale-specific search
|
||||
* providers, but only if they're Mozilla partners. In order to do this, we
|
||||
* track the nsISearchEngine identifier, which denotes shipped search engines,
|
||||
* and intersect those with our partner list.
|
||||
*
|
||||
* We don't use the search engine name directly, because it is shared across
|
||||
* locales; e.g., eBay-de and eBay both share the name "eBay".
|
||||
*/
|
||||
function SearchCountMeasurement2() {
|
||||
this._fieldSpecs = null;
|
||||
this._interestingEngines = null; // Name -> ID. ("Amazon.com" -> "amazondotcom")
|
||||
|
||||
Metrics.Measurement.call(this);
|
||||
}
|
||||
|
||||
SearchCountMeasurement2.prototype = Object.freeze({
|
||||
__proto__: Metrics.Measurement.prototype,
|
||||
|
||||
name: "counts",
|
||||
version: 2,
|
||||
|
||||
/**
|
||||
* Default implementation; can be overridden by test helpers.
|
||||
*/
|
||||
getDefaultEngines: function () {
|
||||
return Services.search.getDefaultEngines();
|
||||
},
|
||||
|
||||
_initialize: function () {
|
||||
// Don't create all of these for every profile.
|
||||
// There are 61 partner engines, translating to 244 fields.
|
||||
// Instead, compute only those that are possible -- those for whom the
|
||||
// provider is one of the default search engines.
|
||||
// This set can grow over time, and change as users run different localized
|
||||
// Firefox instances.
|
||||
this._fieldSpecs = {};
|
||||
this._interestingEngines = {};
|
||||
|
||||
for (let source of this.SOURCES) {
|
||||
this._fieldSpecs["other." + source] = DAILY_COUNTER_FIELD;
|
||||
}
|
||||
|
||||
let engines = this.getDefaultEngines();
|
||||
for (let engine of engines) {
|
||||
let id = engine.identifier;
|
||||
if (!id || (this.PROVIDERS.indexOf(id) == -1)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this._interestingEngines[engine.name] = id;
|
||||
let fieldPrefix = id + ".";
|
||||
for (let source of this.SOURCES) {
|
||||
this._fieldSpecs[fieldPrefix + source] = DAILY_COUNTER_FIELD;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Our fields are dynamic, so we compute them into _fieldSpecs by looking at
|
||||
// the current set of interesting engines.
|
||||
get fields() {
|
||||
if (!this._fieldSpecs) {
|
||||
this._initialize();
|
||||
}
|
||||
return this._fieldSpecs;
|
||||
},
|
||||
|
||||
get interestingEngines() {
|
||||
if (!this._fieldSpecs) {
|
||||
this._initialize();
|
||||
}
|
||||
return this._interestingEngines;
|
||||
},
|
||||
|
||||
/**
|
||||
* Override the default behavior: serializers should include every counter
|
||||
* field from the DB, even if we don't currently have it registered.
|
||||
*
|
||||
* Do this so we don't have to register several hundred fields to match
|
||||
* various Firefox locales.
|
||||
*
|
||||
* We use the "provider.type" syntax as a rudimentary check for validity.
|
||||
*
|
||||
* We trust that measurement versioning is sufficient to exclude old provider
|
||||
* data.
|
||||
*/
|
||||
shouldIncludeField: function (name) {
|
||||
return name.indexOf(".") != -1;
|
||||
},
|
||||
|
||||
/**
|
||||
* The measurement type mechanism doesn't introspect the DB. Override it
|
||||
* so that we can assume all unknown fields are counters.
|
||||
*/
|
||||
fieldType: function (name) {
|
||||
if (name in this.fields) {
|
||||
return this.fields[name].type;
|
||||
}
|
||||
|
||||
// Default to a counter.
|
||||
return Metrics.Storage.FIELD_DAILY_COUNTER;
|
||||
},
|
||||
|
||||
// You can compute the total list of fields by unifying the entire l10n repo
|
||||
// set with the list of partners:
|
||||
//
|
||||
// sort -u */*/searchplugins/list.txt | tr -d '^M' | uniq | grep -f partners.txt
|
||||
//
|
||||
// where partners.txt contains
|
||||
//
|
||||
// amazon
|
||||
// aol
|
||||
// bing
|
||||
// eBay
|
||||
// google
|
||||
// mailru
|
||||
// mercadolibre
|
||||
// seznam
|
||||
// twitter
|
||||
// yahoo
|
||||
// yandex
|
||||
//
|
||||
// Please update this list as the set of partners changes.
|
||||
//
|
||||
PROVIDERS: [
|
||||
"amazon-co-uk",
|
||||
"amazon-de",
|
||||
"amazon-en-GB",
|
||||
"amazon-france",
|
||||
"amazon-it",
|
||||
"amazon-jp",
|
||||
"amazondotcn",
|
||||
"amazondotcom",
|
||||
"amazondotcom-de",
|
||||
|
||||
"aol-en-GB",
|
||||
"aol-web-search",
|
||||
|
||||
// If an engine is removed from this list, it may not be reported any more.
|
||||
// Verify side-effects are sane before removing an entry.
|
||||
PARTNER_ENGINES: [
|
||||
"amazon.com",
|
||||
"bing",
|
||||
|
||||
"eBay",
|
||||
"eBay-de",
|
||||
"eBay-en-GB",
|
||||
"eBay-es",
|
||||
"eBay-fi",
|
||||
"eBay-france",
|
||||
"eBay-hu",
|
||||
"eBay-in",
|
||||
"eBay-it",
|
||||
|
||||
"google",
|
||||
"google-jp",
|
||||
"google-ku",
|
||||
"google-maps-zh-TW",
|
||||
|
||||
"mailru",
|
||||
|
||||
"mercadolibre-ar",
|
||||
"mercadolibre-cl",
|
||||
"mercadolibre-mx",
|
||||
|
||||
"seznam-cz",
|
||||
|
||||
"twitter",
|
||||
"twitter-de",
|
||||
"twitter-ja",
|
||||
|
||||
"yahoo",
|
||||
"yahoo-NO",
|
||||
"yahoo-answer-zh-TW",
|
||||
"yahoo-ar",
|
||||
"yahoo-bid-zh-TW",
|
||||
"yahoo-br",
|
||||
"yahoo-ch",
|
||||
"yahoo-cl",
|
||||
"yahoo-de",
|
||||
"yahoo-en-GB",
|
||||
"yahoo-es",
|
||||
"yahoo-fi",
|
||||
"yahoo-france",
|
||||
"yahoo-fy-NL",
|
||||
"yahoo-id",
|
||||
"yahoo-in",
|
||||
"yahoo-it",
|
||||
"yahoo-jp",
|
||||
"yahoo-jp-auctions",
|
||||
"yahoo-mx",
|
||||
"yahoo-sv-SE",
|
||||
"yahoo-zh-TW",
|
||||
|
||||
"yandex",
|
||||
"yandex-ru",
|
||||
"yandex-slovari",
|
||||
"yandex-tr",
|
||||
"yandex.by",
|
||||
"yandex.ru-be",
|
||||
],
|
||||
|
||||
SOURCES: [
|
||||
@ -1135,7 +1323,10 @@ this.SearchesProvider.prototype = Object.freeze({
|
||||
__proto__: Metrics.Provider.prototype,
|
||||
|
||||
name: "org.mozilla.searches",
|
||||
measurementTypes: [SearchCountMeasurement],
|
||||
measurementTypes: [
|
||||
SearchCountMeasurement1,
|
||||
SearchCountMeasurement2,
|
||||
],
|
||||
|
||||
/**
|
||||
* Record that a search occurred.
|
||||
@ -1145,25 +1336,22 @@ this.SearchesProvider.prototype = Object.freeze({
|
||||
* the search will be attributed to "other".
|
||||
* @param source
|
||||
* (string) Where the search was initiated from. Must be one of the
|
||||
* SearchCountMeasurement.SOURCES values.
|
||||
* SearchCountMeasurement2.SOURCES values.
|
||||
*
|
||||
* @return Promise<>
|
||||
* The promise is resolved when the storage operation completes.
|
||||
*/
|
||||
recordSearch: function (engine, source) {
|
||||
let m = this.getMeasurement("counts", 1);
|
||||
let m = this.getMeasurement("counts", 2);
|
||||
|
||||
if (m.SOURCES.indexOf(source) == -1) {
|
||||
throw new Error("Unknown source for search: " + source);
|
||||
}
|
||||
|
||||
let normalizedEngine = engine.toLowerCase();
|
||||
if (m.PARTNER_ENGINES.indexOf(normalizedEngine) == -1) {
|
||||
normalizedEngine = "other";
|
||||
}
|
||||
|
||||
let id = m.interestingEngines[engine] || "other";
|
||||
let field = id + "." + source;
|
||||
return this.enqueueStorageOperation(function recordSearch() {
|
||||
return m.incrementDailyCounter(normalizedEngine + "." + source);
|
||||
return m.incrementDailyCounter(field);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
@ -6,8 +6,33 @@
|
||||
const {utils: Cu} = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/Metrics.jsm");
|
||||
Cu.import("resource://gre/modules/services/healthreport/providers.jsm");
|
||||
let 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.SearchCountMeasurement2.call(this);
|
||||
}
|
||||
MockSearchCountMeasurement.prototype = {
|
||||
__proto__: bsp.SearchCountMeasurement2.prototype,
|
||||
getDefaultEngines: function () {
|
||||
return DEFAULT_ENGINES;
|
||||
},
|
||||
};
|
||||
|
||||
function MockSearchesProvider() {
|
||||
SearchesProvider.call(this);
|
||||
}
|
||||
MockSearchesProvider.prototype = {
|
||||
__proto__: SearchesProvider.prototype,
|
||||
measurementTypes: [MockSearchCountMeasurement],
|
||||
};
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
@ -21,50 +46,43 @@ add_test(function test_constructor() {
|
||||
|
||||
add_task(function test_record() {
|
||||
let storage = yield Metrics.Storage("record");
|
||||
let provider = new SearchesProvider();
|
||||
let provider = new MockSearchesProvider();
|
||||
|
||||
yield provider.init(storage);
|
||||
|
||||
const ENGINES = [
|
||||
"amazon.com",
|
||||
"bing",
|
||||
"google",
|
||||
"yahoo",
|
||||
"foobar",
|
||||
];
|
||||
|
||||
let now = new Date();
|
||||
|
||||
for (let engine of ENGINES) {
|
||||
yield provider.recordSearch(engine, "abouthome");
|
||||
yield provider.recordSearch(engine, "contextmenu");
|
||||
yield provider.recordSearch(engine, "searchbar");
|
||||
yield provider.recordSearch(engine, "urlbar");
|
||||
for (let engine of DEFAULT_ENGINES) {
|
||||
yield provider.recordSearch(engine.name, "abouthome");
|
||||
yield provider.recordSearch(engine.name, "contextmenu");
|
||||
yield provider.recordSearch(engine.name, "searchbar");
|
||||
yield provider.recordSearch(engine.name, "urlbar");
|
||||
}
|
||||
|
||||
// Invalid sources should throw.
|
||||
let errored = false;
|
||||
try {
|
||||
yield provider.recordSearch("google", "bad source");
|
||||
yield provider.recordSearch(DEFAULT_ENGINES[0].name, "bad source");
|
||||
} catch (ex) {
|
||||
errored = true;
|
||||
} finally {
|
||||
do_check_true(errored);
|
||||
}
|
||||
|
||||
let m = provider.getMeasurement("counts", 1);
|
||||
let m = provider.getMeasurement("counts", 2);
|
||||
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 ENGINES) {
|
||||
if (engine == "foobar") {
|
||||
engine = "other";
|
||||
for (let engine of DEFAULT_ENGINES) {
|
||||
let identifier = engine.identifier;
|
||||
if (identifier == "foobar") {
|
||||
identifier = "other";
|
||||
}
|
||||
|
||||
for (let source of ["abouthome", "contextmenu", "searchbar", "urlbar"]) {
|
||||
let field = engine + "." + source;
|
||||
let field = identifier + "." + source;
|
||||
do_check_true(day.has(field));
|
||||
do_check_eq(day.get(field), 1);
|
||||
}
|
||||
@ -73,3 +91,33 @@ add_task(function test_record() {
|
||||
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", 2);
|
||||
|
||||
// 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();
|
||||
});
|
||||
|
@ -182,12 +182,28 @@ Measurement.prototype = Object.freeze({
|
||||
},
|
||||
|
||||
_configureStorage: function () {
|
||||
return Task.spawn(function configureFields() {
|
||||
for (let [name, info] in Iterator(this.fields)) {
|
||||
this._log.debug("Registering field: " + name + " " + info.type);
|
||||
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;
|
||||
}
|
||||
|
||||
let id = yield this.storage.registerField(this.id, name, info.type);
|
||||
this._fields[name] = [id, info.type];
|
||||
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));
|
||||
},
|
||||
@ -331,12 +347,30 @@ Measurement.prototype = Object.freeze({
|
||||
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, data] of data) {
|
||||
// There could be legacy fields in storage we no longer care about.
|
||||
if (!(field in this._fields)) {
|
||||
if (!this.shouldIncludeField(field)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -367,7 +401,7 @@ Measurement.prototype = Object.freeze({
|
||||
let result = {"_v": this.version};
|
||||
|
||||
for (let [field, data] of data) {
|
||||
if (!(field in this._fields)) {
|
||||
if (!this.shouldIncludeField(field)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -82,15 +82,19 @@ this.ProviderManager.prototype = Object.freeze({
|
||||
*
|
||||
* One can register entries in the application's .manifest file. e.g.
|
||||
*
|
||||
* category healthreport-js-provider FooProvider resource://gre/modules/foo.jsm
|
||||
* 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");
|
||||
* 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 to query and load from.
|
||||
* (string) Name of category from which to query and load.
|
||||
* @return a newly spawned Task.
|
||||
*/
|
||||
registerProvidersFromCategoryManager: function (category) {
|
||||
this._log.info("Registering providers from category: " + category);
|
||||
|
@ -731,6 +731,15 @@ function MetricsStorageSqliteBackend(connection) {
|
||||
}
|
||||
|
||||
MetricsStorageSqliteBackend.prototype = Object.freeze({
|
||||
// Max size (in kibibytes) the WAL log is allowed to grow to before it is
|
||||
// checkpointed.
|
||||
//
|
||||
// This was first deployed in bug 848136. We want a value large enough
|
||||
// that we aren't checkpointing all the time. However, we want it
|
||||
// small enough so we don't have to read so much when we open the
|
||||
// database.
|
||||
MAX_WAL_SIZE_KB: 512,
|
||||
|
||||
FIELD_DAILY_COUNTER: "daily-counter",
|
||||
FIELD_DAILY_DISCRETE_NUMERIC: "daily-discrete-numeric",
|
||||
FIELD_DAILY_DISCRETE_TEXT: "daily-discrete-text",
|
||||
@ -1105,6 +1114,43 @@ MetricsStorageSqliteBackend.prototype = Object.freeze({
|
||||
_init: function() {
|
||||
let self = this;
|
||||
return Task.spawn(function initTask() {
|
||||
// 0. Database file and connection configuration.
|
||||
|
||||
// This should never fail. But, we assume the default of 1024 in case it
|
||||
// does.
|
||||
let rows = yield self._connection.execute("PRAGMA page_size");
|
||||
let pageSize = 1024;
|
||||
if (rows.length) {
|
||||
pageSize = rows[0].getResultByIndex(0);
|
||||
}
|
||||
|
||||
self._log.debug("Page size is " + pageSize);
|
||||
|
||||
// Ensure temp tables are stored in memory, not on disk.
|
||||
yield self._connection.execute("PRAGMA temp_store=MEMORY");
|
||||
|
||||
let journalMode;
|
||||
rows = yield self._connection.execute("PRAGMA journal_mode=WAL");
|
||||
if (rows.length) {
|
||||
journalMode = rows[0].getResultByIndex(0);
|
||||
}
|
||||
|
||||
self._log.info("Journal mode is " + journalMode);
|
||||
|
||||
if (journalMode == "wal") {
|
||||
yield self._connection.execute("PRAGMA wal_autocheckpoint=" +
|
||||
Math.ceil(self.MAX_WAL_SIZE_KB * 1024 / pageSize));
|
||||
} else {
|
||||
if (journalMode != "truncate") {
|
||||
// Fall back to truncate (which is faster than delete).
|
||||
yield self._connection.execute("PRAGMA journal_mode=TRUNCATE");
|
||||
}
|
||||
|
||||
// And always use full synchronous mode to reduce possibility for data
|
||||
// loss.
|
||||
yield self._connection.execute("PRAGMA synchronous=FULL");
|
||||
}
|
||||
|
||||
// 1. Create the schema.
|
||||
yield self._connection.executeTransaction(function ensureSchema(conn) {
|
||||
let schema = conn.schemaVersion;
|
||||
@ -1134,19 +1180,29 @@ MetricsStorageSqliteBackend.prototype = Object.freeze({
|
||||
});
|
||||
|
||||
// 3. Populate built-in types with database.
|
||||
let missingTypes = [];
|
||||
for (let type of self._BUILTIN_TYPES) {
|
||||
type = self[type];
|
||||
if (self._typesByName.has(type)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let params = {name: type};
|
||||
yield self._connection.executeCached(SQL.addType, params);
|
||||
let rows = yield self._connection.executeCached(SQL.getTypeID, params);
|
||||
let id = rows[0].getResultByIndex(0);
|
||||
missingTypes.push(type);
|
||||
}
|
||||
|
||||
self._typesByID.set(id, type);
|
||||
self._typesByName.set(type, id);
|
||||
// Don't perform DB transaction unless there is work to do.
|
||||
if (missingTypes.length) {
|
||||
yield self._connection.executeTransaction(function populateBuiltinTypes() {
|
||||
for (let type of missingTypes) {
|
||||
let params = {name: type};
|
||||
yield self._connection.executeCached(SQL.addType, params);
|
||||
let rows = yield self._connection.executeCached(SQL.getTypeID, params);
|
||||
let id = rows[0].getResultByIndex(0);
|
||||
|
||||
self._typesByID.set(id, type);
|
||||
self._typesByName.set(type, id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 4. Obtain measurement info.
|
||||
@ -1223,6 +1279,19 @@ MetricsStorageSqliteBackend.prototype = Object.freeze({
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Checkpoint writes requiring flush to disk.
|
||||
*
|
||||
* This is called to persist queued and non-flushed writes to disk.
|
||||
* It will force an fsync, so it is expensive and should be used
|
||||
* sparingly.
|
||||
*/
|
||||
checkpoint: function () {
|
||||
return this.enqueueOperation(function checkpoint() {
|
||||
return this._connection.execute("PRAGMA wal_checkpoint");
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
* Ensure a field ID matches a specified type.
|
||||
*
|
||||
|
@ -273,5 +273,21 @@ add_task(function test_serialize_json_default() {
|
||||
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();
|
||||
});
|
||||
|
@ -1074,6 +1074,9 @@ Engine.prototype = {
|
||||
// The engine's alias (can be null). Initialized to |undefined| to indicate
|
||||
// not-initialized-from-engineMetadataService.
|
||||
_alias: undefined,
|
||||
// A distribution-unique identifier for the engine. Either null or set
|
||||
// when loaded. See getter.
|
||||
_identifier: undefined,
|
||||
// The data describing the engine. Is either an array of bytes, for Sherlock
|
||||
// files, or an XML document element, for XML plugins.
|
||||
_data: null,
|
||||
@ -2268,6 +2271,38 @@ Engine.prototype = {
|
||||
notifyAction(this, SEARCH_ENGINE_CHANGED);
|
||||
},
|
||||
|
||||
/**
|
||||
* Return the built-in identifier of app-provided engines.
|
||||
*
|
||||
* Note that this identifier is substantially similar to _id, with the
|
||||
* following exceptions:
|
||||
*
|
||||
* * There is no trailing file extension.
|
||||
* * There is no [app] prefix.
|
||||
*
|
||||
* @return a string identifier, or null.
|
||||
*/
|
||||
get identifier() {
|
||||
if (this._identifier !== undefined) {
|
||||
return this._identifier;
|
||||
}
|
||||
|
||||
// No identifier if If the engine isn't app-provided
|
||||
if (!this._isInAppDir && !this._isInJAR) {
|
||||
return this._identifier = null;
|
||||
}
|
||||
|
||||
let leaf = this._getLeafName();
|
||||
ENSURE_WARN(leaf, "identifier: app-provided engine has no leafName");
|
||||
|
||||
// Strip file extension.
|
||||
let ext = leaf.lastIndexOf(".");
|
||||
if (ext == -1) {
|
||||
return this._identifier = leaf;
|
||||
}
|
||||
return this._identifier = leaf.substring(0, ext);
|
||||
},
|
||||
|
||||
get description() {
|
||||
return this._description;
|
||||
},
|
||||
@ -2311,12 +2346,29 @@ Engine.prototype = {
|
||||
return "";
|
||||
},
|
||||
|
||||
/**
|
||||
* @return the leaf name of the filename or URI of this plugin,
|
||||
* or null if no file or URI is known.
|
||||
*/
|
||||
_getLeafName: function () {
|
||||
if (this._file) {
|
||||
return this._file.leafName;
|
||||
}
|
||||
if (this._uri && this._uri instanceof Ci.nsIURL) {
|
||||
return this._uri.fileName;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
// The file that the plugin is loaded from is a unique identifier for it. We
|
||||
// use this as the identifier to store data in the sqlite database
|
||||
__id: null,
|
||||
get _id() {
|
||||
if (this.__id)
|
||||
if (this.__id) {
|
||||
return this.__id;
|
||||
}
|
||||
|
||||
let leafName = this._getLeafName();
|
||||
|
||||
// Treat engines loaded from JARs the same way we treat app shipped
|
||||
// engines.
|
||||
@ -2328,28 +2380,25 @@ Engine.prototype = {
|
||||
// different engine name. People using the JAR functionality should be
|
||||
// careful not to do that!
|
||||
if (this._isInAppDir || this._isInJAR) {
|
||||
let leafName;
|
||||
if (this._file)
|
||||
leafName = this._file.leafName;
|
||||
else {
|
||||
// If we've reached this point, we must be loaded from a JAR, which
|
||||
// also means we should have a URL.
|
||||
ENSURE_WARN(this._isInJAR && (this._uri instanceof Ci.nsIURL),
|
||||
"_id: not inJAR, or no URI", Cr.NS_ERROR_UNEXPECTED);
|
||||
leafName = this._uri.fileName;
|
||||
}
|
||||
|
||||
// App dir and JAR engines should always have leafNames
|
||||
ENSURE_WARN(leafName, "_id: no leafName for appDir or JAR engine",
|
||||
Cr.NS_ERROR_UNEXPECTED);
|
||||
return this.__id = "[app]/" + leafName;
|
||||
}
|
||||
|
||||
ENSURE_WARN(this._file, "_id: no _file!", Cr.NS_ERROR_UNEXPECTED);
|
||||
if (this._isInProfile) {
|
||||
ENSURE_WARN(leafName, "_id: no leafName for profile engine",
|
||||
Cr.NS_ERROR_UNEXPECTED);
|
||||
return this.__id = "[profile]/" + leafName;
|
||||
}
|
||||
|
||||
if (this._isInProfile)
|
||||
return this.__id = "[profile]/" + this._file.leafName;
|
||||
// If the engine isn't a JAR engine, it should have a file.
|
||||
ENSURE_WARN(this._file, "_id: no _file for non-JAR engine",
|
||||
Cr.NS_ERROR_UNEXPECTED);
|
||||
|
||||
// We're not in the profile or appdir, so this must be an extension-shipped
|
||||
// plugin. Use the full filename.
|
||||
return this.__id = this._file.path;
|
||||
return this.__id = this._file.path;
|
||||
},
|
||||
|
||||
get _installLocation() {
|
||||
|
60
toolkit/components/search/tests/xpcshell/test_identifiers.js
Normal file
60
toolkit/components/search/tests/xpcshell/test_identifiers.js
Normal file
@ -0,0 +1,60 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
/*
|
||||
* Test that a search engine's identifier can be extracted from the filename.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const Ci = Components.interfaces;
|
||||
const SEARCH_APP_DIR = 1;
|
||||
|
||||
function run_test() {
|
||||
removeMetadata();
|
||||
removeCacheFile();
|
||||
do_load_manifest("data/chrome.manifest");
|
||||
|
||||
let url = "chrome://testsearchplugin/locale/searchplugins/";
|
||||
Services.prefs.setCharPref("browser.search.jarURIs", url);
|
||||
Services.prefs.setBoolPref("browser.search.loadFromJars", true);
|
||||
|
||||
updateAppInfo();
|
||||
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_test(function test_identifier() {
|
||||
let engineFile = gProfD.clone();
|
||||
engineFile.append("searchplugins");
|
||||
engineFile.append("test-search-engine.xml");
|
||||
engineFile.parent.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
|
||||
|
||||
// Copy the test engine to the test profile.
|
||||
let engineTemplateFile = do_get_file("data/engine.xml");
|
||||
engineTemplateFile.copyTo(engineFile.parent, "test-search-engine.xml");
|
||||
|
||||
let search = Services.search.init(function initComplete(aResult) {
|
||||
do_print("init'd search service");
|
||||
do_check_true(Components.isSuccessCode(aResult));
|
||||
|
||||
let profileEngine = Services.search.getEngineByName("Test search engine");
|
||||
let jarEngine = Services.search.getEngineByName("bug645970");
|
||||
|
||||
do_check_true(profileEngine instanceof Ci.nsISearchEngine);
|
||||
do_check_true(jarEngine instanceof Ci.nsISearchEngine);
|
||||
|
||||
// An engine loaded from the profile directory won't have an identifier,
|
||||
// because it's not built-in.
|
||||
do_check_eq(profileEngine.identifier, null);
|
||||
|
||||
// An engine loaded from a JAR will have an identifier corresponding to
|
||||
// the filename inside the JAR. (In this case it's the same as the name.)
|
||||
do_check_eq(jarEngine.identifier, "bug645970");
|
||||
|
||||
removeMetadata();
|
||||
removeCacheFile();
|
||||
run_next_test();
|
||||
});
|
||||
});
|
||||
|
@ -7,6 +7,7 @@ firefox-appdir = browser
|
||||
[test_645970.js]
|
||||
# Bug 845190: Too many intermittent assertions on Linux (ASSERTION: thread pool wasn't shutdown)
|
||||
skip-if = debug && os == "linux"
|
||||
[test_identifiers.js]
|
||||
[test_init_async_multiple.js]
|
||||
[test_init_async_multiple_then_sync.js]
|
||||
[test_json_cache.js]
|
||||
|
@ -3026,6 +3026,12 @@
|
||||
"n_buckets": 15,
|
||||
"description": "Time (ms) it takes FHR to shut down."
|
||||
},
|
||||
"HEALTHREPORT_POST_COLLECT_CHECKPOINT_MS": {
|
||||
"kind": "exponential",
|
||||
"high": "20000",
|
||||
"n_buckets": 15,
|
||||
"description": "Time (ms) for a WAL checkpoint after collecting all measurements."
|
||||
},
|
||||
"POPUP_NOTIFICATION_MAINACTION_TRIGGERED_MS": {
|
||||
"kind": "linear",
|
||||
"low": 25,
|
||||
|
Loading…
Reference in New Issue
Block a user