diff --git a/README.md b/README.md
new file mode 100644
index 0000000..307b966
--- /dev/null
+++ b/README.md
@@ -0,0 +1,2 @@
+# Search Shield Study
+A shield Study add-on testing the search experience.
diff --git a/chrome.manifest b/chrome.manifest
new file mode 100644
index 0000000..1ebdf15
--- /dev/null
+++ b/chrome.manifest
@@ -0,0 +1 @@
+content unified-urlbar chrome/content/
diff --git a/code/content/Panel.jsm b/chrome/content/Panel.jsm
old mode 100755
new mode 100644
similarity index 80%
rename from code/content/Panel.jsm
rename to chrome/content/Panel.jsm
index e92b5a4..f92f7c0
--- a/code/content/Panel.jsm
+++ b/chrome/content/Panel.jsm
@@ -1,17 +1,25 @@
+"use strict";
+
this.EXPORTED_SYMBOLS = [
- "Panel",
+ "Panel"
];
const EXISTING_FOOTER_ID = "urlbar-search-footer";
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("chrome://unified-urlbar/content/Telemetry.jsm");
+Cu.import("resource://gre/modules/Preferences.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "UnifiedUrlbar",
+ "chrome://unified-urlbar/content/UnifiedUrlbar.jsm");
this.Panel = function (panelElt) {
this.panelElement = panelElt;
this._initPanelElement();
+ this.panelElement.addEventListener("popupshowing", this);
+ this.panelElement.addEventListener("popuphiding", this);
this._initKeyHandler();
this.urlbar.addEventListener("input", this);
};
@@ -39,11 +47,105 @@ this.Panel.prototype = {
this.urlbar.removeEventListener("input", this);
this.panelElement.removeEventListener("popupshowing", this);
+ this.panelElement.removeEventListener("popuphiding", this);
this.footer.remove();
if (this._existingFooter) {
this._existingFooterParent.appendChild(this._existingFooter);
}
+
+ this._uninitTipElement();
+ },
+
+ _shouldShowHint() {
+ const SHOW_TIP_DEFAULT_COUNT = 5;
+ let tipShownCount = SHOW_TIP_DEFAULT_COUNT;
+ if (Preferences.has("browser.urlbar.experiment.unified-urlbar.tipShownCount")) {
+ tipShownCount = Preferences.get("browser.urlbar.experiment.unified-urlbar.tipShownCount",
+ SHOW_TIP_DEFAULT_COUNT);
+ }
+
+ if (tipShownCount > 0) {
+ Preferences.set("browser.urlbar.experiment.unified-urlbar.tipShownCount",
+ --tipShownCount);
+ }
+
+ return tipShownCount > 0 &&
+ Preferences.get("browser.urlbar.suggest.searches", false);
+ },
+
+ _ensureTipElement() {
+ if (this.tipContainer)
+ return;
+
+ let tipContainer = this.document.createElementNS(XUL_NS, "hbox");
+ tipContainer.id = "urlbar-tip-container";
+ tipContainer.setAttribute("flex", "1");
+ tipContainer.setAttribute("align", "center");
+ this.tipContainer = tipContainer;
+
+ let icon = this.document.createElementNS(XUL_NS, "image");
+ icon.className = "ac-site-icon";
+ tipContainer.appendChild(icon);
+ this.tipIcon = icon;
+
+ let titleBox = this.document.createElementNS(XUL_NS, "hbox");
+ titleBox.id = "urlbar-tip-title";
+ tipContainer.appendChild(titleBox);
+
+ let title = this.document.createElementNS(XUL_NS, "description");
+ title.className = "ac-title-text";
+ title.textContent = "Firefox";
+ titleBox.appendChild(title);
+
+ let tipBox = this.document.createElementNS(XUL_NS, "hbox");
+ tipBox.id = "urlbar-tip-box";
+ tipContainer.setAttribute("align", "center");
+ tipContainer.appendChild(tipBox);
+
+ let tip = this.document.createElementNS(XUL_NS, "description");
+ tip.innerHTML = `💡Tip: Results with a magnifying glass are search suggestions. They might be what you're looking for!😄`;
+ tipBox.appendChild(tip);
+
+ let notification = this.panelElement.searchSuggestionsNotification;
+ for (let child of notification.childNodes) {
+ child.collapsed = true;
+ }
+ notification.setAttribute("tip", "true");
+ notification.appendChild(tipContainer);
+ },
+
+ _uninitTipElement() {
+ if (this.tipContainer) {
+ this.tipContainer.remove();
+ delete this.tipContainer;
+ }
+ let notification = this.panelElement.searchSuggestionsNotification;
+ notification.removeAttribute("tip");
+ for (let child of notification.childNodes) {
+ child.collapsed = false;
+ }
+ this.document.getAnonymousElementByAttribute(this.panelElement, "anonid", "search-suggestions-notification")
+ .style.visibility = "collapse";
+ },
+
+ _updateTip() {
+ if (!this._shouldShowHint()) {
+ if (this.tipContainer) {
+ this._uninitTipElement();
+ }
+ return;
+ }
+
+ this._ensureTipElement();
+
+ let iconStart = this.panelElement.siteIconStart;
+ if (iconStart) {
+ this.tipIcon.style.marginInlineStart = iconStart + "px";
+ }
+ this.document.getAnonymousElementByAttribute(this.panelElement, "anonid", "search-suggestions-notification")
+ .style.visibility = "visible";
+ this.tipContainer.setAttribute("animate", "true");
},
_initPanelElement() {
@@ -71,8 +173,6 @@ this.Panel.prototype = {
hbox.appendChild(this.settingsButton);
this.panelElement.appendChild(footer);
-
- this.panelElement.addEventListener("popupshowing", this);
},
_makeFooter() {
@@ -260,9 +360,6 @@ this.Panel.prototype = {
return;
}
- if (this.selectedButton != this.settingsButton) {
- Telemetry.incrementValue("oneOffButtonSelectedByKeypress");
- }
event.preventDefault();
},
@@ -279,7 +376,7 @@ this.Panel.prototype = {
return;
}
if (this.selectedButton == this.settingsButton) {
- Telemetry.incrementValue("searchSettingsClicked");
+ UnifiedUrlbar.reportTelemetryValue("searchSettingsClicked");
this.window.openPreferences("paneSearch");
} else {
this._doSearchFromButton(this.selectedButton, event);
@@ -300,6 +397,13 @@ this.Panel.prototype = {
this.selectedButton = null;
this._buildButtonList();
this._updateHeader();
+ this._updateTip();
+ },
+
+ _onPopuphiding(event) {
+ if (this.tipContainer) {
+ this.tipContainer.removeAttribute("animate");
+ }
},
_onMouseover(event) {
@@ -312,7 +416,6 @@ this.Panel.prototype = {
!target.classList.contains("dummy")) ||
target.classList.contains("addengine-item") ||
target.classList.contains("search-setting-button")) {
- Telemetry.incrementValue("oneOffButtonSelectedByMouseover");
this.selectedButton = target;
}
},
@@ -344,9 +447,9 @@ this.Panel.prototype = {
let win = button.ownerDocument.defaultView;
if (event instanceof win.KeyboardEvent) {
- Telemetry.incrementValue("searchByReturnKeyOnOneOffButton", { engine } );
+ UnifiedUrlbar.reportTelemetryValue("searchByReturnKeyOnOneOffButton", { engine } );
} else if (event instanceof win.MouseEvent) {
- Telemetry.incrementValue("searchByClickOnOneOffButton", { engine });
+ UnifiedUrlbar.reportTelemetryValue("searchByClickOnOneOffButton", { engine });
}
let query = this.urlbar.controller.searchString;
@@ -367,8 +470,6 @@ this.Panel.prototype = {
this.buttonList.firstChild.remove();
}
- let Preferences =
- Cu.import("resource://gre/modules/Preferences.jsm", {}).Preferences;
let pref = Preferences.get("browser.search.hiddenOneOffs");
let hiddenList = pref ? pref.split(",") : [];
@@ -448,14 +549,6 @@ this.Panel.prototype = {
this.buttonList.appendChild(button);
}
-
- // Add a data point for the engine count if it's changed (or this is the
- // first time reaching here).
- this._engineCount = this._engineCount || 0;
- if (engines.length != this._engineCount) {
- this._engineCount = engines.length;
- Telemetry.setValue("engineCount", engines.length);
- }
},
get numButtons() {
diff --git a/chrome/content/UnifiedUrlbar.jsm b/chrome/content/UnifiedUrlbar.jsm
new file mode 100644
index 0000000..9645b84
--- /dev/null
+++ b/chrome/content/UnifiedUrlbar.jsm
@@ -0,0 +1,398 @@
+"use strict";
+
+this.EXPORTED_SYMBOLS = [
+ "UnifiedUrlbar"
+];
+
+const STYLE_URL = "chrome://unified-urlbar/content/style.css";
+const SEARCH_BAR_WIDGET_ID = "search-container";
+const XHTML_NS = "http://www.w3.org/1999/xhtml";
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Preferences.jsm");
+Cu.import("resource:///modules/CustomizableUI.jsm");
+Cu.import("resource:///modules/BrowserUITelemetry.jsm");
+Cu.import("chrome://unified-urlbar/content/Panel.jsm");
+
+var gOptions = {
+ forceSearchSuggestions: true,
+ addOneOffAndOnboarding: true,
+ removeSearchbar: true,
+};
+
+var gBrowsers = null;
+var gForcedSuggestions = false;
+var gDisabledOneOffs = false;
+
+this.UnifiedUrlbar = Object.freeze({
+ isUserEligible() {
+ // Users having a version of Firefox that includes the NEW one-off buttons
+ // implementation are not eligible for this experiment.
+ let vc = Cc["@mozilla.org/xpcom/version-comparator;1"]
+ .getService(Ci.nsIVersionComparator);
+ let appInfo = Cc["@mozilla.org/xre/app-info;1"]
+ .getService(Ci.nsIXULAppInfo);
+ if (vc.compare(appInfo.version, "51") >= 0) {
+ return false;
+ }
+
+ // Exclude users who already removed the search bar from the UI.
+ if (CustomizableUI.getPlacementOfWidget("search-container") === null) {
+ return false;
+ }
+
+ return true;
+ },
+
+ init(options = gOptions) {
+ if (gBrowsers) {
+ Cu.reportError("UnifiedUrlbar.init() was invoked multiple times?");
+ return;
+ }
+
+ gOptions = options;
+
+ if (("forceSearchSuggestions" in gOptions) && gOptions.forceSearchSuggestions) {
+ forceSearchSuggestions();
+ }
+ if (("addOneOffAndOnboarding" in gOptions) && gOptions.addOneOffAndOnboarding) {
+ disableNewOneOffs();
+ }
+
+ Services.obs.addObserver(this, "autocomplete-did-enter-text", false);
+
+ gBrowsers = new Set();
+ getBrowserWindows();
+ Services.ww.registerNotification(this);
+ },
+
+ destroy() {
+ if (!gBrowsers) {
+ Cu.reportError("UnifiedUrlbar.destroy() was invoked multiple times?");
+ return;
+ }
+
+ restoreSearchSuggestions();
+ restoreNewOneOffs();
+
+ Services.obs.removeObserver(this, "autocomplete-did-enter-text", false);
+
+ Services.ww.unregisterNotification(this);
+ for (let browser of gBrowsers) {
+ browser.destroy();
+ }
+ gBrowsers.clear();
+ gBrowsers = null;
+ },
+
+ observe(subject, topic, data) {
+ if (topic == "autocomplete-did-enter-text") {
+ trackAutocompleteEnter(subject.QueryInterface(Ci.nsIAutoCompleteInput));
+ return;
+ }
+
+ let win = subject.QueryInterface(Ci.nsIDOMWindow);
+ if (!win) {
+ return;
+ }
+
+ if (topic == "domwindowopened") {
+ whenWindowLoaded(win, () => {
+ if (isValidBrowserWindow(win)) {
+ gBrowsers.add(new Browser(win));
+ }
+ });
+ } else if (topic == "domwindowclosed") {
+ for (let browser of gBrowsers) {
+ if (browser.window == win) {
+ browser.destroy();
+ gBrowsers.delete(browser);
+ break;
+ }
+ }
+ }
+ },
+
+ reportTelemetryValue(key, optionalData={}) {
+ reportTelemetryValue(key, optionalData);
+ }
+});
+
+function Browser(win, branch) {
+ this.window = win;
+
+ // Per window toggles.
+ if (("removeSearchbar" in gOptions) && gOptions.removeSearchbar) {
+ this._removeSearchBar();
+ }
+ if (("addOneOffAndOnboarding" in gOptions) && gOptions.addOneOffAndOnboarding) {
+ this._initPanel();
+ }
+
+ win.gBrowser.addProgressListener(this);
+}
+
+Browser.prototype = {
+ get document() {
+ return this.window.document;
+ },
+
+ destroy() {
+ this.window.gBrowser.removeProgressListener(this);
+
+ if (this._styleLink) {
+ this._styleLink.remove();
+ delete this._styleLink;
+ }
+ if (this._panel) {
+ this._panel.destroy();
+ }
+ if (this._searchbarPlacement) {
+ CustomizableUI.addWidgetToArea(SEARCH_BAR_WIDGET_ID,
+ this._searchbarPlacement.area,
+ this._searchbarPlacement.position);
+ }
+ },
+
+ _initPanel() {
+ // Inject style.
+ this._styleLink = this.document.createElementNS(XHTML_NS, "link");
+ this._styleLink.setAttribute("href", STYLE_URL);
+ this._styleLink.setAttribute("rel", "stylesheet");
+ this.document.documentElement.appendChild(this._styleLink);
+
+ let elt = this.document.getElementById("PopupAutoCompleteRichResult");
+ this._panel = new Panel(this.window.gURLBar.popup);
+ },
+
+ _removeSearchBar() {
+ this._searchbarPlacement =
+ CustomizableUI.getPlacementOfWidget(SEARCH_BAR_WIDGET_ID);
+ CustomizableUI.removeWidgetFromArea(SEARCH_BAR_WIDGET_ID);
+ },
+
+ onLocationChange(webProgress, request, uri, flags) {
+ try {
+ if (webProgress.isTopLevel && uri.host) {
+ let host = uri.host.replace(/^www./, "").replace(/^search./, "");
+ if (gEngines.has(host)) {
+ let rv = Services.search.parseSubmissionURL(uri.spec);
+ // HACK: try to not count result pages we generated and subpages.
+ // This is tricky and working until the engines keep same param names.
+ if (rv.engine &&
+ !["hspart=mozilla", // Yahoo tracking
+ "&b=", // Yahoo paging
+ "client=firefox", // Google tracking
+ "&start=", // Google paging
+ "pc=MOZI", // Bing tracking
+ "&first=", // Bing paging
+ ].some(str => uri.path.includes(str))) {
+ reportTelemetryValue("userVisitedEngineResult",
+ { engine: rv.engine });
+ }
+ }
+ }
+ } catch (ex) {}
+ },
+ onProgressChange() {},
+ onSecurityChange() {},
+ onStateChange(webProgress, request, flags, status) {
+ try {
+ if (webProgress.isTopLevel &&
+ flags & Ci.nsIWebProgressListener.STATE_START &&
+ flags & Ci.nsIWebProgressListener.STATE_IS_NETWORK &&
+ (request && (request instanceof Ci.nsIChannel || "URI" in request)) &&
+ request.URI.path == "/") {
+ let host = request.URI.host.replace(/^www./, "").replace(/^search./, "");
+ if (gEngines.has(host)) {
+ reportTelemetryValue("userVisitedEngineHost",
+ { engine: gEngines.get(host) });
+ }
+ }
+ } catch (ex) {}
+ },
+ onStatusChange() {},
+
+ QueryInterface: XPCOMUtils.generateQI([ Ci.nsIWebProgressListener ])
+};
+
+XPCOMUtils.defineLazyGetter(this, "gEngines", () => {
+ let engines = new Map();
+ for (let engineName of [ "Google", "Yahoo", "Bing"]) {
+ let engine = Services.search.getEngineByName(engineName);
+ if (engine) {
+ try {
+ let engineHost = Services.io.newURI(engine.searchForm, null, null).host;
+ engines.set(engineHost.replace(/^www./, "").replace(/^search./, ""),
+ engine);
+ } catch (ex) {}
+ }
+ }
+ return engines;
+});
+
+function forceSearchSuggestions() {
+ // Enable search suggestions for everyone.
+ let suggestions = Preferences.get("browser.urlbar.suggest.searches", false);
+ if (!suggestions) {
+ gForcedSuggestions = true;
+ Preferences.set("browser.urlbar.suggest.searches", true);
+ }
+}
+
+function disableNewOneOffs() {
+ // Disable the one-off buttons NEW implementation, to replce it with ours.
+ let oneoffs = Preferences.get("browser.urlbar.oneOffSearches", false);
+ if (oneoffs) {
+ gDisabledOneOffs = true;
+ Preferences.set("browser.urlbar.oneOffSearches", false);
+ }
+}
+
+function restoreSearchSuggestions() {
+ if (gForcedSuggestions) {
+ Preferences.set("browser.urlbar.suggest.searches", false);
+ gForcedSuggestions = false;
+ }
+}
+
+function restoreNewOneOffs() {
+ if (gDisabledOneOffs) {
+ Preferences.set("browser.urlbar.oneOffSearches", true);
+ gDisabledOneOffs = false;
+ }
+}
+
+function isValidBrowserWindow(win) {
+ return !win.closed && win.toolbar.visible &&
+ win.document.documentElement.getAttribute("windowtype") == "navigator:browser";
+}
+
+function getBrowserWindows() {
+ let wins = Services.ww.getWindowEnumerator();
+ while (wins.hasMoreElements()) {
+ let win = wins.getNext().QueryInterface(Ci.nsIDOMWindow);
+ whenWindowLoaded(win, () => {
+ if (isValidBrowserWindow(win)) {
+ gBrowsers.add(new Browser(win));
+ }
+ });
+ }
+}
+
+function whenWindowLoaded(win, callback) {
+ if (win.document.readyState == "complete") {
+ callback();
+ return;
+ }
+ win.addEventListener("load", function onLoad(event) {
+ if (event.target == win.document) {
+ win.removeEventListener("load", onLoad, true);
+ win.setTimeout(callback, 0);
+ }
+ }, true);
+}
+
+function trackAutocompleteEnter(input) {
+ if (!input || input.id != "urlbar" || input.inPrivateContext) {
+ return;
+ }
+
+ let controller = input.popup.view.QueryInterface(Ci.nsIAutoCompleteController);
+
+ let idx = input.popup.selectedIndex;
+ if (idx == -1) {
+ return;
+ }
+ let value = controller.getValueAt(idx);
+ let action = input._parseActionUrl(value);
+ if (action.type != "searchengine")
+ return;
+
+ if ("searchSuggestion" in action.params) {
+ reportTelemetryValue("searchBySuggestion");
+ } else if ("alias" in action.params) {
+ let engine = Services.search.getEngineByAlias(action.params.alias);
+ if (engine)
+ reportTelemetryValue("searchByAlias", { engine });
+ } else {
+ reportTelemetryValue("searchByDefaultEngine");
+ }
+}
+
+/**
+ * Registers the presence of an event.
+ *
+ * @param eventName The data is logged with this name.
+ */
+function reportTelemetryValue(key, optionalData={}) {
+ // Since we only care about search volume, ignore the rare cases where a
+ // search opens in a new tab (currently onlye CTRL + Go button) and always
+ // set where to "current".
+ function recordOneOff(engine, source="unknown", type="unknown") {
+ let engineId = engine ? engine.identifier ? engine.identifier
+ : "other-" + engine.name
+ : "other";
+ BrowserUITelemetry.countOneoffSearchEvent(`${engineId}.${source}`, type,
+ "current");
+ }
+
+ // Increment "search" / "urlbar" countable.
+ // Since we only care about search volume, don't report details, like the
+ // suggestion index clicked, to countSearchEvent.
+ function recordSearch(engine=Services.search.currentEngine, source) {
+ let isInContentSearch = source.includes("content");
+ if (isInContentSearch) {
+ // Add engine name to the probe name.
+ source += "-" + engine.name.toLowerCase();
+ }
+
+ BrowserUITelemetry.countSearchEvent(source, null);
+
+ if (!isInContentSearch) {
+ let engineId = engine ? engine.identifier ? engine.identifier
+ : "other-" + engine.name
+ : "other";
+ let count = Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS");
+ count.add(`${engineId}.${source}`);
+ }
+ }
+
+ switch(key) {
+ case "userVisitedEngineHost":
+ recordSearch(optionalData.engine, "content");
+ break;
+ case "userVisitedEngineResult":
+ recordSearch(optionalData.engine, "content-result");
+ break;
+ case "searchSettingsClicked":
+ BrowserUITelemetry.countSearchSettingsEvent("urlbar");
+ break;
+ case "searchByReturnKeyOnOneOffButton":
+ recordOneOff(optionalData.engine, "urlbar-oneoff", "key");
+ recordSearch(optionalData.engine, "urlbar");
+ break;
+ case "searchByClickOnOneOffButton":
+ recordOneOff(optionalData.engine, "urlbar-oneoff", "mouse");
+ recordSearch(optionalData.engine, "urlbar");
+ break;
+ case "searchBySuggestions":
+ recordSearch(optionalData.engine, "urlbar-suggestion");
+ break;
+ case "searchByDefaultEngine":
+ // If it's a direct Enter actions it already increments "search" / "urlbar"
+ // countable, so we should only increment oneoff countable for the current
+ // engine (with unknown source, like the searchbar).
+ recordOneOff(Services.search.currentEngine, "urlbar", "unknown");
+ break;
+ case "searchByAlias":
+ // Due to 1313080 aliases are not accounted by default, so do that here.
+ recordOneOff(optionalData.engine, "urlbar", "unknown");
+ recordSearch(optionalData.engine, "urlbar");
+ break;
+ default:
+ Cu.reportError("reportTelemetryValue() got an unknown value");
+ }
+}
diff --git a/code/content/gear.svg b/chrome/content/gear.svg
old mode 100755
new mode 100644
similarity index 100%
rename from code/content/gear.svg
rename to chrome/content/gear.svg
diff --git a/chrome/content/style.css b/chrome/content/style.css
new file mode 100644
index 0000000..9727f38
--- /dev/null
+++ b/chrome/content/style.css
@@ -0,0 +1,166 @@
+.urlbar-header {
+ font-size: 10px;
+ font-weight: normal;
+ margin: 0;
+ padding: 3px 6px;
+ color: #666;
+}
+
+.urlbar-header > label {
+ margin-top: 2px !important;
+ margin-bottom: 2px !important;
+}
+
+.urlbar-current-input > label {
+ margin: 2px 0 !important;
+}
+
+.urlbar-input-value {
+ color: black;
+}
+
+.urlbar-one-offs {
+ margin: 0 0 !important;
+}
+
+.urlbar-engine-one-off-item {
+ -moz-appearance: none;
+ display: inline-block;
+ border: none;
+ min-width: 48px;
+ height: 32px;
+ margin: 0 0;
+ padding: 0 0;
+}
+
+.urlbar-engine-one-off-item:not(.dummy) {
+ background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAWCAYAAAABxvaqAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gofECQNNVW2/AAAABBJREFUGFdjOHPmzH8GehEA/KpKg9YTf4AAAAAASUVORK5CYII=');
+ background-repeat: no-repeat;
+ background-position: right center;
+}
+
+.urlbar-engine-one-off-item:not(.last-row) {
+ box-sizing: content-box;
+ border-bottom: 1px solid #ccc;
+}
+
+.urlbar-engine-one-off-item.last-of-row {
+ background-image: none;
+}
+
+.urlbar-engine-one-off-item[selected] {
+ background-color: Highlight;
+ background-image: none;
+}
+
+.urlbar-engine-one-off-item > .button-box > .button-text {
+ display: none;
+}
+
+.urlbar-engine-one-off-item > .button-box > .button-icon {
+ -moz-margin-start: 0;
+ width: 16px;
+ height: 16px;
+}
+
+#urlbar-search-settings2 {
+ list-style-image: url("chrome://unified-urlbar/content/gear.svg#gear");
+ background-position: left center;
+ border-bottom: none;
+}
+
+#urlbar-search-settings2:hover {
+ list-style-image: url("chrome://unified-urlbar/content/gear.svg#gear-inverted");
+ background-color: Highlight;
+}
+
+#urlbar-search-footer2 {
+ border-top: 1px solid hsla(210, 4%, 10%, 0.14);
+ background-color: hsla(210, 4%, 10%, 0.07);
+}
+
+#urlbar-tip-container {
+ font-size: 14px;
+}
+
+#urlbar-tip-box {
+ min-height: 3em;
+ padding: 4px 8px;
+ background-color: #ffeebe;
+ border: 1px solid #ffdf81;
+ border-radius: 4px;
+ color: #7d3500;
+ font: message-box;
+ opacity: 0;
+}
+
+#urlbar-tip-box span.bold {
+ font-weight: bold;
+}
+
+#urlbar-tip-box span.emoji {
+ margin: 0 0.5ch;
+ font-size: 120%;
+}
+
+#urlbar-tip-container > .ac-site-icon {
+ list-style-image: url(chrome://global/skin/icons/autocomplete-search.svg#search-icon);
+}
+
+#urlbar-tip-container[animate="true"] > #urlbar-tip-box {
+ margin-left: 160px;
+ animation-name: buildIn-tip;
+ animation-duration: 250ms;
+ animation-delay: 1000ms;
+ animation-iteration-count: 1;
+ animation-fill-mode: forwards;
+}
+
+#urlbar-tip-container[animate="true"] > .ac-site-icon {
+ transform: scale(0);
+ animation-name: buildIn-grow;
+ animation-duration: 500ms;
+ animation-delay: 750ms;
+ animation-iteration-count: 1;
+ animation-timing-function: ease-in-out;
+ animation-fill-mode: forwards;
+}
+
+#urlbar-tip-container[animate="true"] > #urlbar-tip-title > .ac-title-text {
+ text-overflow: clip;
+}
+
+#urlbar-tip-container[animate="true"] > #urlbar-tip-title {
+ overflow: hidden;
+ max-width: 8ch;
+ width: 0;
+ animation-name: typing;
+ animation-duration: 750ms;
+ animation-delay: 300ms;
+ animation-iteration-count: 1;
+ animation-fill-mode: forwards;
+}
+
+@keyframes buildIn-tip {
+ 0% { margin-left: 160px; opacity: 0; }
+ 100% { margin-left: 0; opacity: 1; }
+}
+
+@keyframes buildIn-grow {
+ 0% { transform: scale(0); }
+ 40% { transform: scale(1.5); }
+ 60% { transform: scale(1); }
+ 80% { transform: scale(1.25); }
+ 100% { transform: scale(1); }
+}
+
+@keyframes typing {
+ from { width: 0; }
+ to { width: 8ch; }
+}
+
+#PopupAutoCompleteRichResult > hbox[anonid="search-suggestions-notification"][tip="true"] {
+ background-image: none;
+ padding-inline-start: 0;
+ padding-inline-end: 0;
+}
diff --git a/code/bootstrap.js b/code/bootstrap.js
deleted file mode 100755
index b6fbc46..0000000
--- a/code/bootstrap.js
+++ /dev/null
@@ -1,79 +0,0 @@
-const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
-Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource:///modules/experiments/Experiments.jsm");
-Cu.import("resource:///modules/CustomizableUI.jsm");
-
-XPCOMUtils.defineLazyModuleGetter(this, "Telemetry",
- "chrome://unified-urlbar/content/Telemetry.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "BrowserListener",
- "chrome://unified-urlbar/content/BrowserListener.jsm");
-
-var gStarted = false;
-
-function startup(data, reason) {
- // Seems startup() function is launched twice after install, we're
- // unsure why so far. We only want it to run once.
- if (gStarted) {
- return;
- }
- gStarted = true;
-
- // Workaround until bug 1228359 is fixed.
- //Components.manager.addBootstrappedManifestLocation(data.installPath);
-
- ensureExperimentBranch().then(branch => {
- BrowserListener.init(branch);
- Telemetry.init(branch);
- });
-}
-
-function shutdown(data, reason) {
- // Workaround until bug 1228359 is fixed.
- //Components.manager.removeBootstrappedManifestLocation(data.installPath);
-
- BrowserListener.destroy();
- Telemetry.destroy();
-}
-
-function install(data, reason) {}
-function uninstall(data, reason) {}
-
-/**
- * Ensures that the experiment branch is set and returns it.
- *
- * @return Promise Resolved with the branch.
- */
-function ensureExperimentBranch() {
- return new Promise(resolve => {
- // TESTING CODE
- try {
- let forcedBranch =
- Services.prefs.getCharPref("browser.urlbar.experiment.unified-urlbar.branch");
- resolve(forcedBranch);
- } catch (ex) {}
-
- let experiments = Experiments.instance();
- // This experiment has 3 user groups:
- // * "control" : Users with default search bar setup.
- // No UI changes.
- // * "customized": Users who customized the search bar position.
- // Add one-off buttons.
- // * "unified" : Add one-off search buttons to the location bar and
- // customize away the search bar.
- let branch = experiments.getActiveExperimentBranch();
- if (branch) {
- resolve(branch);
- return;
- }
- let placement = CustomizableUI.getPlacementOfWidget("search-container");
- if (!placement || placement.area != "nav-bar") {
- branch = "customized";
- } else {
- let coinFlip = Math.floor(2 * Math.random());
- branch = coinFlip ? "control" : "unified";
- }
- let id = experiments.getActiveExperimentID();
- experiments.setExperimentBranch(id, branch).then(() => resolve(branch));
- });
-}
diff --git a/code/chrome.manifest b/code/chrome.manifest
deleted file mode 100755
index da37f4e..0000000
--- a/code/chrome.manifest
+++ /dev/null
@@ -1 +0,0 @@
-content unified-urlbar content/
diff --git a/code/content/BrowserListener.jsm b/code/content/BrowserListener.jsm
deleted file mode 100755
index c725556..0000000
--- a/code/content/BrowserListener.jsm
+++ /dev/null
@@ -1,153 +0,0 @@
-this.EXPORTED_SYMBOLS = [
- "BrowserListener",
-];
-
-const STYLE_URL = "chrome://unified-urlbar/content/style.css";
-const SEARCH_BAR_WIDGET_ID = "search-container";
-const XHTML_NS = "http://www.w3.org/1999/xhtml";
-
-const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource:///modules/CustomizableUI.jsm");
-Cu.import("chrome://unified-urlbar/content/Panel.jsm");
-
-var gBranch = "control";
-var gBrowsers = null;
-
-this.BrowserListener = Object.freeze({
- init(branch) {
- gBranch = branch;
-
- if (gBrowsers) {
- return;
- }
- gBrowsers = new Set();
- getBrowserWindows();
- Services.ww.registerNotification(this);
- },
-
- destroy() {
- if (!gBrowsers) {
- return;
- }
-
- Services.ww.unregisterNotification(this);
- for (let browser of gBrowsers) {
- browser.destroy();
- }
- gBrowsers.clear();
- gBrowsers = null;
- },
-
- observe(subj, topic, data) {
- let win = subj.QueryInterface(Ci.nsIDOMWindow);
- if (!win) {
- return;
- }
-
- if (topic == "domwindowopened") {
- whenWindowLoaded(win, () => {
- if (isValidBrowserWindow(win)) {
- gBrowsers.add(new Browser(win));
- }
- });
- } else if (topic == "domwindowclosed") {
- for (let browser of gBrowsers) {
- if (browser.window == win) {
- browser.destroy();
- gBrowsers.delete(browser);
- break;
- }
- }
- }
- },
-});
-
-function Browser(win) {
- this.window = win;
-
- switch (gBranch) {
- case "unified":
- this._moveSearchBar();
- // Fall through.
- case "customized":
- this._injectStyle();
- this._initPanel();
- break;
- default:
- // Nothing!
- break;
- }
-}
-
-Browser.prototype = {
- get document() {
- return this.window.document;
- },
-
- destroy() {
- if (this._styleLink) {
- this._styleLink.remove();
- delete this._styleLink;
- }
-
- if (this._panel) {
- this._panel.destroy();
- }
-
- if (this._searchbarPlacement) {
- CustomizableUI.addWidgetToArea(SEARCH_BAR_WIDGET_ID,
- this._searchbarPlacement.area,
- this._searchbarPlacement.position);
- }
- },
-
- _injectStyle() {
- this._styleLink = this.document.createElementNS(XHTML_NS, "link");
- this._styleLink.setAttribute("href", STYLE_URL);
- this._styleLink.setAttribute("rel", "stylesheet");
- this.document.documentElement.appendChild(this._styleLink);
- },
-
- _initPanel() {
- let elt = this.document.getElementById("PopupAutoCompleteRichResult");
- this._panel = new Panel(elt);
- },
-
- _moveSearchBar() {
- this._searchbarPlacement =
- CustomizableUI.getPlacementOfWidget(SEARCH_BAR_WIDGET_ID);
- CustomizableUI.removeWidgetFromArea(SEARCH_BAR_WIDGET_ID);
- },
-};
-
-function isValidBrowserWindow(win) {
- return !win.closed && win.toolbar.visible &&
- win.document.documentElement.getAttribute("windowtype") == "navigator:browser";
-}
-
-function getBrowserWindows() {
- let wins = Services.ww.getWindowEnumerator();
- while (wins.hasMoreElements()) {
- let win = wins.getNext().QueryInterface(Ci.nsIDOMWindow);
- whenWindowLoaded(win, () => {
- if (isValidBrowserWindow(win)) {
- gBrowsers.add(new Browser(win));
- }
- });
- }
-}
-
-function whenWindowLoaded(win, callback) {
- if (win.document.readyState == "complete") {
- callback();
- return;
- }
- win.addEventListener("load", function onLoad(event) {
- if (event.target == win.document) {
- win.removeEventListener("load", onLoad, true);
- win.setTimeout(callback, 0);
- }
- }, true);
-}
diff --git a/code/content/Telemetry.jsm b/code/content/Telemetry.jsm
deleted file mode 100755
index d293b81..0000000
--- a/code/content/Telemetry.jsm
+++ /dev/null
@@ -1,137 +0,0 @@
-this.EXPORTED_SYMBOLS = [
- "Telemetry",
-];
-
-const SEARCH_SUGGESTIONS_OPT_IN_CHOICE_PREF =
- "browser.urlbar.userMadeSearchSuggestionsChoice";
-
-const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/Preferences.jsm");
-Cu.import("resource:///modules/BrowserUITelemetry.jsm");
-Cu.import("resource://gre/modules/Services.jsm");
-
-var gBranch = "control";
-var gValues = new Map();
-
-this.Telemetry = Object.freeze({
-
- init(branch) {
- gBranch = branch;
- addSearchSuggestionsOptInTelemetry();
- Services.obs.addObserver(this, "autocomplete-did-enter-text", false);
- },
-
- destroy() {
- Services.obs.removeObserver(this, "autocomplete-did-enter-text", false);
- Preferences.ignore(SEARCH_SUGGESTIONS_OPT_IN_CHOICE_PREF);
- gValues.clear();
- },
-
- observe: function(aSubject, aTopic, aData) {
- let input = aSubject.QueryInterface(Ci.nsIAutoCompleteInput);
- if (!input || input.id != "urlbar" || input.inPrivateContext ||
- input.popup.selectedIndex == -1) {
- return;
- }
- let controller = input.popup.view.QueryInterface(Ci.nsIAutoCompleteController);
- let idx = input.popup.selectedIndex;
- let value = controller.getValueAt(idx);
- let action = input._parseActionUrl(value);
- let actionType;
- if (action) {
- actionType = action.type == "searchengine" && action.params.searchSuggestion ?
- "searchsuggestion" : action.type;
- }
- if (actionType == "searchengine") {
- this.incrementValue("searchByDefaultEngine");
- } else if (actionType == "searchSuggestion") {
- this.incrementValue("searchBySuggestion");
- }
- },
-
- /**
- * Registers the presence of an event.
- *
- * @param eventName The data is logged with this name.
- */
- incrementValue(key, optionalData={}) {
- // Since we only care about search volume, ignore the rare cases where a
- // search opens in a new tab (currently onlye CTRL + Go button) and always
- // set where to "current".
- function recordOneOff(engine, source="unknown", type="unknown") {
- let engineId = engine ? engine.identifier ? engine.identifier
- : "other-" + engine.name
- : "other";
- BrowserUITelemetry.countOneoffSearchEvent(`${engineId}.${source}`, type,
- "current");
- }
-
- // Increment "search" / "urlbar" countable.
- // Since we only care about search volume, don't report details, like the
- // suggestion index clicked, to countSearchEvent.
- function recordSearch(engine=Services.search.currentEngine, source) {
- BrowserUITelemetry.countSearchEvent(source, null);
- let engineId = engine ? engine.identifier ? engine.identifier
- : "other-" + engine.name
- : "other";
- let count = Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS");
- count.add(`${engineId}.${source}`);
- }
-
- switch(key) {
- case "searchSettingsClicked":
- BrowserUITelemetry.countSearchSettingsEvent("urlbar");
- break;
- case "searchByReturnKeyOnOneOffButton":
- recordOneOff(optionalData.engine, "urlbar-oneoff", "key");
- recordSearch(optionalData.engine, "urlbar");
- break;
- case "searchByClickOnOneOffButton":
- recordOneOff(optionalData.engine, "urlbar-oneoff", "mouse");
- recordSearch(optionalData.engine, "urlbar");
- break;
- case "searchByDefaultEngine":
- // Already increments "search" / "urlbar" countable, it must also
- // increment oneoff countable for the current engine.
- // We don't have data about mouse or keyboard interaction here yet, so
- // just pass unknown for the type.
- recordOneOff(Services.search.currentEngine, "urlbar", "unknown");
- break;
- default:
- // This data is currently unused, but stored for reference.
- let val = gValues.get(key) || 0;
- gValues.set(key, val);
- }
- },
-
- /**
- * Registers a key-value.
- *
- * @param key The data is logged with this name.
- * @param value The value of the data.
- */
- setValue(key, value) {
- // This data is currently unused, but stored for reference.
- gValues.set(key, value);
- },
-
- QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver ]),
-});
-
-/**
- * Collects data about the user's urlbar search suggestions opt-in choice, or
- * if none has been made yet, adds a pref observer for it.
- */
-function addSearchSuggestionsOptInTelemetry() {
- let userMadeChoice = Preferences.get(SEARCH_SUGGESTIONS_OPT_IN_CHOICE_PREF);
- Telemetry.setValue("userMadeSuggestionsChoice", userMadeChoice);
- if (!userMadeChoice) {
- Preferences.observe(SEARCH_SUGGESTIONS_OPT_IN_CHOICE_PREF, () => {
- Preferences.ignore(SEARCH_SUGGESTIONS_OPT_IN_CHOICE_PREF);
- addSearchSuggestionsOptInTelemetry();
- });
- }
- let optedIn = Preferences.get("browser.urlbar.suggest.searches");
- Telemetry.setValue("suggestionsEnabled", optedIn);
-}
diff --git a/code/content/style.css b/code/content/style.css
deleted file mode 100755
index 4903b6c..0000000
--- a/code/content/style.css
+++ /dev/null
@@ -1,80 +0,0 @@
-.urlbar-header {
- font-size: 10px;
- font-weight: normal;
- margin: 0;
- padding: 3px 6px;
- color: #666;
-}
-
-.urlbar-header > label {
- margin-top: 2px !important;
- margin-bottom: 2px !important;
-}
-
-.urlbar-current-input > label {
- margin: 2px 0 !important;
-}
-
-.urlbar-input-value {
- color: black;
-}
-
-.urlbar-one-offs {
- margin: 0 0 !important;
-}
-
-.urlbar-engine-one-off-item {
- -moz-appearance: none;
- display: inline-block;
- border: none;
- min-width: 48px;
- height: 32px;
- margin: 0 0;
- padding: 0 0;
-}
-
-.urlbar-engine-one-off-item:not(.dummy) {
- background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAWCAYAAAABxvaqAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gofECQNNVW2/AAAABBJREFUGFdjOHPmzH8GehEA/KpKg9YTf4AAAAAASUVORK5CYII=');
- background-repeat: no-repeat;
- background-position: right center;
-}
-
-.urlbar-engine-one-off-item:not(.last-row) {
- box-sizing: content-box;
- border-bottom: 1px solid #ccc;
-}
-
-.urlbar-engine-one-off-item.last-of-row {
- background-image: none;
-}
-
-.urlbar-engine-one-off-item[selected] {
- background-color: Highlight;
- background-image: none;
-}
-
-.urlbar-engine-one-off-item > .button-box > .button-text {
- display: none;
-}
-
-.urlbar-engine-one-off-item > .button-box > .button-icon {
- -moz-margin-start: 0;
- width: 16px;
- height: 16px;
-}
-
-#urlbar-search-settings2 {
- list-style-image: url("chrome://unified-urlbar/content/gear.svg#gear");
- background-position: left center;
- border-bottom: none;
-}
-
-#urlbar-search-settings2:hover {
- list-style-image: url("chrome://unified-urlbar/content/gear.svg#gear-inverted");
- background-color: Highlight;
-}
-
-#urlbar-search-footer2 {
- border-top: 1px solid hsla(210, 4%, 10%, 0.14);
- background-color: hsla(210, 4%, 10%, 0.07);
-}
diff --git a/code/install.rdf b/code/install.rdf
deleted file mode 100755
index 72ce290..0000000
--- a/code/install.rdf
+++ /dev/null
@@ -1,26 +0,0 @@
-
-
-
- unified-urlbar@experiments.mozilla.org
- 1.0.0
- 128
- true
- false
-
-
-
-
- {ec8030f7-c20a-464f-9b0e-13a3a9e97384}
- 43.0
- 47.0
-
-
-
- Unified Urlbar Experiment
-
- An experimental add-on testing the unified urlbar experience.
-
- https://bugzilla.mozilla.org/show_bug.cgi?id=1219505
-
-
diff --git a/docs/metrics.md b/docs/metrics.md
new file mode 100644
index 0000000..940ffb2
--- /dev/null
+++ b/docs/metrics.md
@@ -0,0 +1,128 @@
+```JSON
+{
+ "payload": {
+ "ver": 4,
+ "simpleMeasurements": {
+ "UITelemetry": {
+ "toolbars": {
+ "defaultKept": [
+ "edit-controls",
+ "zoom-controls",
+ "save-page-button",
+ "print-button",
+ "history-panelmenu",
+ "fullscreen-button",
+ "preferences-button",
+ "add-ons-button",
+ "privatebrowsing-button",
+ "urlbar-container",
+ "search-container",
+ "bookmarks-menu-button",
+ "downloads-button",
+ "menubar-items",
+ "tabbrowser-tabs",
+ "new-tab-button",
+ "alltabs-button",
+ "personal-bookmarks"
+ ],
+ "defaultMoved": [
+ "developer-button"
+ ],
+ "nondefaultAdded": [
+ "feed-button"
+ ],
+ "defaultRemoved": [
+ "sync-button",
+ "find-button",
+ "social-share-button",
+ "pocket-button",
+ "new-window-button",
+ "home-button"
+ ],
+ "currentSearchEngine": "engine_name",
+ "countableEvents": {
+ "__DEFAULT__": {
+ "click-builtin-item": {
+ "urlbar": {
+ "search-settings": 1
+ },
+ "searchbar": {
+ "search-settings": 1
+ }
+ },
+ "search": {
+ "urlbar": 3,
+ "searchbar": 3,
+ "newtab": 3,
+ "abouthome": 3,
+ "content-bing": 3,
+ "content-google": 3,
+ "content-yahoo": 3,
+ "content-result-bing": 3,
+ "content-result-google": 3,
+ "content-result-yahoo": 3,
+ "autocomplete-other": 3,
+ "autocomplete-default": 3,
+ },
+ "search-oneoff": {
+ "engine_name.ui_source": {
+ "mouse": {
+ "current": 1
+ },
+ "key": {
+ "current": 1
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "histograms": {
+ "FX_URLBAR_SELECTED_RESULT_TYPE": {
+ "range": [
+ 1,
+ 14
+ ],
+ "bucket_count": 15,
+ "histogram_type": 1,
+ "values": {
+ "0": 4,
+ "1": 2,
+ "2": 8,
+ "3": 6,
+ "8": 2,
+ "9": 0
+ },
+ "sum": 52
+ }
+ },
+ "keyedHistograms": {
+ "SEARCH_COUNTS": {
+ "engine_name.ui_source": {
+ "range": [
+ 1,
+ 2
+ ],
+ "bucket_count": 3,
+ "histogram_type": 4,
+ "values": {
+ "0": 1,
+ "1": 0
+ },
+ "sum": 1
+ }
+ }
+ }
+ },
+ "environment": {
+ "settings": {
+ "userPrefs": {
+ "browser.urlbar.suggest.searches": true,
+ "browser.urlbar.userMadeSearchSuggestionsChoice": true,
+ }
+ }
+ }
+}
+```
diff --git a/docs/telemetry.md b/docs/telemetry.md
new file mode 100644
index 0000000..4f0f7b7
--- /dev/null
+++ b/docs/telemetry.md
@@ -0,0 +1,58 @@
+environment/settings/userPrefs/browser.urlbar.suggest.searches
+* tracks the search suggestions enabled pref
+
+environment/settings/userPrefs/browser.urlbar.userMadeSearchSuggestionsChoice
+* tracks the pref stating whether the user made a choice for search suggestions opt-in
+
+payload/simpleMeasurements/UITelemetry/toolbars/countableEvents/__DEFAULT__/search/searchbar
+* total number of searches started from the search bar
+
+payload/simpleMeasurements/UITelemetry/toolbars/countableEvents/__DEFAULT__/search/abouthome
+* total number of searches started from about:home
+
+payload/simpleMeasurements/UITelemetry/toolbars/countableEvents/__DEFAULT__/search/newtab
+* total number of searches started from about:newtab
+
+payload/simpleMeasurements/UITelemetry/toolbars/countableEvents/__DEFAULT__/search/urlbar
+* total number of searches started from the urlbar
+
+payload/simpleMeasurements/UITelemetry/toolbars/countableEvents/__DEFAULT__/search/content-bing
+payload/simpleMeasurements/UITelemetry/toolbars/countableEvents/__DEFAULT__/search/content-google
+payload/simpleMeasurements/UITelemetry/toolbars/countableEvents/__DEFAULT__/search/content-yahoo
+* In-content direct accesses to "google", "bing", "yahoo" root domains
+
+payload/simpleMeasurements/UITelemetry/toolbars/countableEvents/__DEFAULT__/search/content-result-bing
+payload/simpleMeasurements/UITelemetry/toolbars/countableEvents/__DEFAULT__/search/content-result-google
+payload/simpleMeasurements/UITelemetry/toolbars/countableEvents/__DEFAULT__/search/content-result-yahoo
+* In-content accesses to a "google", "bing", "yahoo" first result page (heuristic)
+
+payload/simpleMeasurements/UITelemetry/toolbars/countableEvents/__DEFAULT__/search/autocomplete-other
+* number of searches to a non-current engine started by revisiting a previous search result in the urlbar
+
+payload/simpleMeasurements/UITelemetry/toolbars/countableEvents/__DEFAULT__/search/autocomplete-default
+* number of searches to the current engine started by revisiting a previous search result in the urlbar
+
+payload/simpleMeasurements/UITelemetry/toolbars/defaultKept
+payload/simpleMeasurements/UITelemetry/toolbars/defaultMoved
+payload/simpleMeasurements/UITelemetry/toolbars/defaultRemMoved
+* widget positions on toolbar, look for "search-container"
+
+payload/histograms/FX_URLBAR_SELECTED_RESULT_TYPE
+* type of result selected from the urlbar dropdown, see nsBroweserGlue.js::_handleURLBarTelemetry for enums
+
+payload/simpleMeasurements/UITelemetry/toolbars/countableEvents/__DEFAULT__/search-oneoff/
+* engine.unknown
+ * Serches done by just typing something and pressing Enter
+* engine.oneoff-searchbar
+ * searches from searchbar one-off buttons
+* engine.oneoff-urlbar
+ * searches from urlbar one-off buttons
+
+payload/simpleMeasurements/UITelemetry/toolbars/countableEvents/__DEFAULT__/click-builtin-item/searchbar/search-settings
+* number of times the searchbar search settings button is used
+
+payload/simpleMeasurements/UITelemetry/toolbars/countableEvents/__DEFAULT__/click-builtin-item/urlbar/search-settings
+* number of times the urlbar search settings button is used
+
+payload/keyedHistograms/SEARCH_COUNTS
+* should ideally be the sum of all the searches from urlbar, searchbar, abouthome, newtab
diff --git a/docs/testing.md b/docs/testing.md
new file mode 100644
index 0000000..454114e
--- /dev/null
+++ b/docs/testing.md
@@ -0,0 +1,15 @@
+Since UITelemetry is only collected after a restart, it's required to use the --profile and --no-copy options to ensure the profile persists across runs.
+It's also necessary to use an unbranded build (from https://wiki.mozilla.org/Add-ons/Extension_Signing) to test this on Beta 50 using the Shield cli.
+
+For example, after cloning the git report and entering the add-on folder:
+```Bash
+npm install -g shield-study-cli jpm
+npm install --save-dev jpm
+shield run . unified -- --profile path_to_profile --no-copy
+```
+After the first run, just close the browser and shield run again, UITelemetry should be now collected properly.
+
+unified is one of the three variations:
+ * unified: add oneoffs, remove searchbar
+ * oneoff: add oneoff
+ * control: no changes
diff --git a/filter.js b/filter.js
deleted file mode 100755
index 4606e32..0000000
--- a/filter.js
+++ /dev/null
@@ -1,3 +0,0 @@
-function filter(c) {
- return c.telemetryEnvironment.settings.telemetryEnabled;
-}
diff --git a/lib/feature.js b/lib/feature.js
new file mode 100644
index 0000000..247b4aa
--- /dev/null
+++ b/lib/feature.js
@@ -0,0 +1,73 @@
+var {Cc, Ci, Cu} = require("chrome");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "UnifiedUrlbar",
+ "chrome://unified-urlbar/content/UnifiedUrlbar.jsm");
+const prefSvc = require('sdk/preferences/service');
+
+const feature = {
+ isEligible() {
+ return UnifiedUrlbar.isUserEligible();
+ },
+
+ checkHasConflicts() {
+ // Exclude users having the Activity Stream add-on.
+ const { AddonManager } = Cu.import("resource://gre/modules/AddonManager.jsm");
+ return new Promise(resolve => {
+ AddonManager.getAddonsByIDs(["@activity-streams", "universal-search@mozilla.com"],
+ results => resolve(results.some(r => !!r)));
+ });
+ },
+
+ install(variation) {
+ // Enable extended telemetry if needed, but note this requires a restart
+ // before we can start collecting data.
+ if (!prefSvc.isSet("browser.urlbar.experiment.unified-urlbar.telemetry.enabled.mirror")) {
+ let telemetryEnabled = prefSvc.get("toolkit.telemetry.enabled", false);
+ if (!telemetryEnabled) {
+ prefSvc.set("browser.urlbar.experiment.unified-urlbar.telemetry.enabled.mirror", telemetryEnabled);
+ prefSvc.set("toolkit.telemetry.enabled", true);
+ }
+ }
+
+ switch (variation) {
+ case "unified":
+ UnifiedUrlbar.init({
+ forceSearchSuggestions: true,
+ addOneOffAndOnboarding: true,
+ removeSearchbar: true,
+ });
+ break;
+
+ case "oneoff":
+ UnifiedUrlbar.init({
+ forceSearchSuggestions: true,
+ addOneOffAndOnboarding: true,
+ removeSearchbar: false,
+ });
+ break;
+
+ default:
+ UnifiedUrlbar.init({
+ forceSearchSuggestions: false,
+ addOneOffAndOnboarding: false,
+ removeSearchbar: false,
+ });
+ break;
+ }
+ },
+
+ cleanup() {
+ UnifiedUrlbar.destroy();
+ },
+
+ uninstall() {
+ if (prefSvc.isSet("browser.urlbar.experiment.unified-urlbar.telemetry.enabled.mirror")) {
+ let telemetryEnabled = prefSvc.get("browser.urlbar.experiment.unified-urlbar.telemetry.enabled.mirror", false);
+ prefSvc.reset("browser.urlbar.experiment.unified-urlbar.telemetry.enabled.mirror");
+ prefSvc.set("toolkit.telemetry.enabled", telemetryEnabled);
+ }
+ prefSvc.reset("browser.urlbar.experiment.unified-urlbar.tipShownCount");
+ }
+};
+
+exports.feature = feature;
diff --git a/lib/index.js b/lib/index.js
new file mode 100644
index 0000000..f760e0e
--- /dev/null
+++ b/lib/index.js
@@ -0,0 +1,8 @@
+const self = require("sdk/self");
+const { study, checkHasConflicts } = require("./study");
+
+checkHasConflicts().then(hasConflicts => {
+ if (!hasConflicts) {
+ study.startup(self.loadReason);
+ }
+});
diff --git a/lib/study.js b/lib/study.js
new file mode 100644
index 0000000..58b959d
--- /dev/null
+++ b/lib/study.js
@@ -0,0 +1,66 @@
+const self = require("sdk/self");
+const shield = require("shield-studies-addon-utils");
+const { when: unload } = require("sdk/system/unload");
+const { feature } = require('./feature');
+const prefSvc = require('sdk/preferences/service');
+
+const studyConfig = {
+ name: self.addonId,
+ duration: 14,
+ variations: {
+ "unified": () => feature.install("unified"),
+ "oneoff": () => feature.install("oneoff"),
+ "control": () => feature.install("control")
+ },
+ surveyUrls: {
+ 'end-of-study': "https://qsurvey.mozilla.com/s3/search-study-1",
+ 'user-ended-study': "https://qsurvey.mozilla.com/s3/search-study-1",
+ 'ineligible': null
+ },
+}
+
+class OurStudy extends shield.Study {
+ constructor(config) {
+ super(config);
+ }
+
+ isEligible() {
+ return super.isEligible() && feature.isEligible();
+ }
+
+ whenIneligible () {
+ // Additional actions when the user isn't eligible.
+ super.whenIneligible();
+ }
+
+ whenInstalled() {
+ // Additional actions when the study gets installed.
+ super.whenInstalled();
+ }
+
+ cleanup() {
+ super.cleanup();
+ feature.cleanup();
+ }
+
+ whenComplete () {
+ // Additional actions when the study is naturally completed.
+ super.whenComplete(); // calls survey, uninstalls
+ }
+
+ whenUninstalled () {
+ // Additional actions when the user uninstalls the study.
+ super.whenUninstalled();
+ feature.uninstall();
+ }
+
+ decideVariation () {
+ return super.decideVariation(); // chooses at random
+ }
+}
+
+const thisStudy = new OurStudy(studyConfig);
+unload((reason) => thisStudy.shutdown(reason))
+
+exports.study = thisStudy;
+exports.checkHasConflicts = feature.checkHasConflicts;
diff --git a/manifest.json b/manifest.json
deleted file mode 100755
index 64dc6e7..0000000
--- a/manifest.json
+++ /dev/null
@@ -1,19 +0,0 @@
-{
- "publish" : true,
- "priority" : 2,
- "name" : "Unified Search in Urlbar",
- "description" : "Adds search features to the urlbar and moves the search bar to the customization panel",
- "info" : "Related bug
",
- "manifest" : {
- "id" : "unified-urlbar@experiments.mozilla.org",
- "startTime" : 1452470400,
- "endTime" : 1456272000,
- "disabled" : true,
- "maxActiveSeconds" : 1209600,
- "appName" : ["Firefox"],
- "channel" : ["beta"],
- "minVersion" : "44.0",
- "maxVersion" : "45.*",
- "sample" : 0.1
- }
-}
diff --git a/node_modules/shield-studies-addon-utils/LICENSE b/node_modules/shield-studies-addon-utils/LICENSE
new file mode 100644
index 0000000..a612ad9
--- /dev/null
+++ b/node_modules/shield-studies-addon-utils/LICENSE
@@ -0,0 +1,373 @@
+Mozilla Public License Version 2.0
+==================================
+
+1. Definitions
+--------------
+
+1.1. "Contributor"
+ means each individual or legal entity that creates, contributes to
+ the creation of, or owns Covered Software.
+
+1.2. "Contributor Version"
+ means the combination of the Contributions of others (if any) used
+ by a Contributor and that particular Contributor's Contribution.
+
+1.3. "Contribution"
+ means Covered Software of a particular Contributor.
+
+1.4. "Covered Software"
+ means Source Code Form to which the initial Contributor has attached
+ the notice in Exhibit A, the Executable Form of such Source Code
+ Form, and Modifications of such Source Code Form, in each case
+ including portions thereof.
+
+1.5. "Incompatible With Secondary Licenses"
+ means
+
+ (a) that the initial Contributor has attached the notice described
+ in Exhibit B to the Covered Software; or
+
+ (b) that the Covered Software was made available under the terms of
+ version 1.1 or earlier of the License, but not also under the
+ terms of a Secondary License.
+
+1.6. "Executable Form"
+ means any form of the work other than Source Code Form.
+
+1.7. "Larger Work"
+ means a work that combines Covered Software with other material, in
+ a separate file or files, that is not Covered Software.
+
+1.8. "License"
+ means this document.
+
+1.9. "Licensable"
+ means having the right to grant, to the maximum extent possible,
+ whether at the time of the initial grant or subsequently, any and
+ all of the rights conveyed by this License.
+
+1.10. "Modifications"
+ means any of the following:
+
+ (a) any file in Source Code Form that results from an addition to,
+ deletion from, or modification of the contents of Covered
+ Software; or
+
+ (b) any new file in Source Code Form that contains any Covered
+ Software.
+
+1.11. "Patent Claims" of a Contributor
+ means any patent claim(s), including without limitation, method,
+ process, and apparatus claims, in any patent Licensable by such
+ Contributor that would be infringed, but for the grant of the
+ License, by the making, using, selling, offering for sale, having
+ made, import, or transfer of either its Contributions or its
+ Contributor Version.
+
+1.12. "Secondary License"
+ means either the GNU General Public License, Version 2.0, the GNU
+ Lesser General Public License, Version 2.1, the GNU Affero General
+ Public License, Version 3.0, or any later versions of those
+ licenses.
+
+1.13. "Source Code Form"
+ means the form of the work preferred for making modifications.
+
+1.14. "You" (or "Your")
+ means an individual or a legal entity exercising rights under this
+ License. For legal entities, "You" includes any entity that
+ controls, is controlled by, or is under common control with You. For
+ purposes of this definition, "control" means (a) the power, direct
+ or indirect, to cause the direction or management of such entity,
+ whether by contract or otherwise, or (b) ownership of more than
+ fifty percent (50%) of the outstanding shares or beneficial
+ ownership of such entity.
+
+2. License Grants and Conditions
+--------------------------------
+
+2.1. Grants
+
+Each Contributor hereby grants You a world-wide, royalty-free,
+non-exclusive license:
+
+(a) under intellectual property rights (other than patent or trademark)
+ Licensable by such Contributor to use, reproduce, make available,
+ modify, display, perform, distribute, and otherwise exploit its
+ Contributions, either on an unmodified basis, with Modifications, or
+ as part of a Larger Work; and
+
+(b) under Patent Claims of such Contributor to make, use, sell, offer
+ for sale, have made, import, and otherwise transfer either its
+ Contributions or its Contributor Version.
+
+2.2. Effective Date
+
+The licenses granted in Section 2.1 with respect to any Contribution
+become effective for each Contribution on the date the Contributor first
+distributes such Contribution.
+
+2.3. Limitations on Grant Scope
+
+The licenses granted in this Section 2 are the only rights granted under
+this License. No additional rights or licenses will be implied from the
+distribution or licensing of Covered Software under this License.
+Notwithstanding Section 2.1(b) above, no patent license is granted by a
+Contributor:
+
+(a) for any code that a Contributor has removed from Covered Software;
+ or
+
+(b) for infringements caused by: (i) Your and any other third party's
+ modifications of Covered Software, or (ii) the combination of its
+ Contributions with other software (except as part of its Contributor
+ Version); or
+
+(c) under Patent Claims infringed by Covered Software in the absence of
+ its Contributions.
+
+This License does not grant any rights in the trademarks, service marks,
+or logos of any Contributor (except as may be necessary to comply with
+the notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+No Contributor makes additional grants as a result of Your choice to
+distribute the Covered Software under a subsequent version of this
+License (see Section 10.2) or under the terms of a Secondary License (if
+permitted under the terms of Section 3.3).
+
+2.5. Representation
+
+Each Contributor represents that the Contributor believes its
+Contributions are its original creation(s) or it has sufficient rights
+to grant the rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+This License is not intended to limit any rights You have under
+applicable copyright doctrines of fair use, fair dealing, or other
+equivalents.
+
+2.7. Conditions
+
+Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
+in Section 2.1.
+
+3. Responsibilities
+-------------------
+
+3.1. Distribution of Source Form
+
+All distribution of Covered Software in Source Code Form, including any
+Modifications that You create or to which You contribute, must be under
+the terms of this License. You must inform recipients that the Source
+Code Form of the Covered Software is governed by the terms of this
+License, and how they can obtain a copy of this License. You may not
+attempt to alter or restrict the recipients' rights in the Source Code
+Form.
+
+3.2. Distribution of Executable Form
+
+If You distribute Covered Software in Executable Form then:
+
+(a) such Covered Software must also be made available in Source Code
+ Form, as described in Section 3.1, and You must inform recipients of
+ the Executable Form how they can obtain a copy of such Source Code
+ Form by reasonable means in a timely manner, at a charge no more
+ than the cost of distribution to the recipient; and
+
+(b) You may distribute such Executable Form under the terms of this
+ License, or sublicense it under different terms, provided that the
+ license for the Executable Form does not attempt to limit or alter
+ the recipients' rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+You may create and distribute a Larger Work under terms of Your choice,
+provided that You also comply with the requirements of this License for
+the Covered Software. If the Larger Work is a combination of Covered
+Software with a work governed by one or more Secondary Licenses, and the
+Covered Software is not Incompatible With Secondary Licenses, this
+License permits You to additionally distribute such Covered Software
+under the terms of such Secondary License(s), so that the recipient of
+the Larger Work may, at their option, further distribute the Covered
+Software under the terms of either this License or such Secondary
+License(s).
+
+3.4. Notices
+
+You may not remove or alter the substance of any license notices
+(including copyright notices, patent notices, disclaimers of warranty,
+or limitations of liability) contained within the Source Code Form of
+the Covered Software, except that You may alter any license notices to
+the extent required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+You may choose to offer, and to charge a fee for, warranty, support,
+indemnity or liability obligations to one or more recipients of Covered
+Software. However, You may do so only on Your own behalf, and not on
+behalf of any Contributor. You must make it absolutely clear that any
+such warranty, support, indemnity, or liability obligation is offered by
+You alone, and You hereby agree to indemnify every Contributor for any
+liability incurred by such Contributor as a result of warranty, support,
+indemnity or liability terms You offer. You may include additional
+disclaimers of warranty and limitations of liability specific to any
+jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+---------------------------------------------------
+
+If it is impossible for You to comply with any of the terms of this
+License with respect to some or all of the Covered Software due to
+statute, judicial order, or regulation then You must: (a) comply with
+the terms of this License to the maximum extent possible; and (b)
+describe the limitations and the code they affect. Such description must
+be placed in a text file included with all distributions of the Covered
+Software under this License. Except to the extent prohibited by statute
+or regulation, such description must be sufficiently detailed for a
+recipient of ordinary skill to be able to understand it.
+
+5. Termination
+--------------
+
+5.1. The rights granted under this License will terminate automatically
+if You fail to comply with any of its terms. However, if You become
+compliant, then the rights granted under this License from a particular
+Contributor are reinstated (a) provisionally, unless and until such
+Contributor explicitly and finally terminates Your grants, and (b) on an
+ongoing basis, if such Contributor fails to notify You of the
+non-compliance by some reasonable means prior to 60 days after You have
+come back into compliance. Moreover, Your grants from a particular
+Contributor are reinstated on an ongoing basis if such Contributor
+notifies You of the non-compliance by some reasonable means, this is the
+first time You have received notice of non-compliance with this License
+from such Contributor, and You become compliant prior to 30 days after
+Your receipt of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+infringement claim (excluding declaratory judgment actions,
+counter-claims, and cross-claims) alleging that a Contributor Version
+directly or indirectly infringes any patent, then the rights granted to
+You by any and all Contributors for the Covered Software under Section
+2.1 of this License shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all
+end user license agreements (excluding distributors and resellers) which
+have been validly granted by You or Your distributors under this License
+prior to termination shall survive termination.
+
+************************************************************************
+* *
+* 6. Disclaimer of Warranty *
+* ------------------------- *
+* *
+* Covered Software is provided under this License on an "as is" *
+* basis, without warranty of any kind, either expressed, implied, or *
+* statutory, including, without limitation, warranties that the *
+* Covered Software is free of defects, merchantable, fit for a *
+* particular purpose or non-infringing. The entire risk as to the *
+* quality and performance of the Covered Software is with You. *
+* Should any Covered Software prove defective in any respect, You *
+* (not any Contributor) assume the cost of any necessary servicing, *
+* repair, or correction. This disclaimer of warranty constitutes an *
+* essential part of this License. No use of any Covered Software is *
+* authorized under this License except under this disclaimer. *
+* *
+************************************************************************
+
+************************************************************************
+* *
+* 7. Limitation of Liability *
+* -------------------------- *
+* *
+* Under no circumstances and under no legal theory, whether tort *
+* (including negligence), contract, or otherwise, shall any *
+* Contributor, or anyone who distributes Covered Software as *
+* permitted above, be liable to You for any direct, indirect, *
+* special, incidental, or consequential damages of any character *
+* including, without limitation, damages for lost profits, loss of *
+* goodwill, work stoppage, computer failure or malfunction, or any *
+* and all other commercial damages or losses, even if such party *
+* shall have been informed of the possibility of such damages. This *
+* limitation of liability shall not apply to liability for death or *
+* personal injury resulting from such party's negligence to the *
+* extent applicable law prohibits such limitation. Some *
+* jurisdictions do not allow the exclusion or limitation of *
+* incidental or consequential damages, so this exclusion and *
+* limitation may not apply to You. *
+* *
+************************************************************************
+
+8. Litigation
+-------------
+
+Any litigation relating to this License may be brought only in the
+courts of a jurisdiction where the defendant maintains its principal
+place of business and such litigation shall be governed by laws of that
+jurisdiction, without reference to its conflict-of-law provisions.
+Nothing in this Section shall prevent a party's ability to bring
+cross-claims or counter-claims.
+
+9. Miscellaneous
+----------------
+
+This License represents the complete agreement concerning the subject
+matter hereof. If any provision of this License is held to be
+unenforceable, such provision shall be reformed only to the extent
+necessary to make it enforceable. Any law or regulation which provides
+that the language of a contract shall be construed against the drafter
+shall not be used to construe this License against a Contributor.
+
+10. Versions of the License
+---------------------------
+
+10.1. New Versions
+
+Mozilla Foundation is the license steward. Except as provided in Section
+10.3, no one other than the license steward has the right to modify or
+publish new versions of this License. Each version will be given a
+distinguishing version number.
+
+10.2. Effect of New Versions
+
+You may distribute the Covered Software under the terms of the version
+of the License under which You originally received the Covered Software,
+or under the terms of any subsequent version published by the license
+steward.
+
+10.3. Modified Versions
+
+If you create software not governed by this License, and you want to
+create a new license for such software, you may create and use a
+modified version of this License if you rename the license and remove
+any references to the name of the license steward (except to note that
+such modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary
+Licenses
+
+If You choose to distribute Source Code Form that is Incompatible With
+Secondary Licenses under the terms of this version of the License, the
+notice described in Exhibit B of this License must be attached.
+
+Exhibit A - Source Code Form License Notice
+-------------------------------------------
+
+ 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/.
+
+If it is not possible or desirable to put the notice in a particular
+file, then You may include the notice in a location (such as a LICENSE
+file in a relevant directory) where a recipient would be likely to look
+for such a notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - "Incompatible With Secondary Licenses" Notice
+---------------------------------------------------------
+
+ This Source Code Form is "Incompatible With Secondary Licenses", as
+ defined by the Mozilla Public License, v. 2.0.
diff --git a/node_modules/shield-studies-addon-utils/README.md b/node_modules/shield-studies-addon-utils/README.md
new file mode 100644
index 0000000..1fa2557
--- /dev/null
+++ b/node_modules/shield-studies-addon-utils/README.md
@@ -0,0 +1,61 @@
+# Shield Studies Addon Utils [](https://travis-ci.org/tombell/travis-ci-status)
+
+- Instrument your Firefox Addon!
+- Build Shield Study (Normandy) compatible addons without having to think very much.
+
+## install
+
+```
+npm install --save-dev shield-studies-addon-utils
+```
+
+## Tutorial and Full Usage
+
+[fully worked tutorial - How To Shield Study](./howToShieldStudy.md)
+
+## Examples
+
+See `examples` directory.
+
+## Summary
+
+### Design Case
+
+Your Study is:
+
+- side-by-side variations (1 or more)
+- 'one-phase'. No warm up periods. If you want that stuff, handle it yourself, or file bugs
+
+### Benefits
+
+Using this, you get this analysis FOR FREE (and it's fast!)
+
+- Branch x channel x VARIATION x experiment-id x PHASE (install, reject, alive etc) using UNIFIED TELEMETRY
+
+- In particular, this library has 100% test coverage for lots of **startup** and **shutdown** cases, so that your addon does the Right Thing across restarts.
+
+ - maintains same variation across restarts
+ - testable, tested code
+ - doesn't care much about your variations, so long as they are 'multi-callable' safely.
+
+
+## Development
+
+- open an issue
+- hack and file a PR
+
+
+## Gotchas, Opinions, Side Effects, and Misfeatures
+
+1. This assumes `jetpack` (`jpm`) style addons, with
+
+ - `require`
+ - `jpm` startup and shutdown semantics
+
+2. Some prefs will be set and running during an experiment in the "addon-specific" pref tree.
+
+3. Disable also uninstalls (and cleans up)
+
+4. Undoubtedly, there are others. It scratches my itch. I have built a lot of things in the past.
+
+
diff --git a/node_modules/shield-studies-addon-utils/lib/event-target.js b/node_modules/shield-studies-addon-utils/lib/event-target.js
new file mode 100644
index 0000000..4335d8c
--- /dev/null
+++ b/node_modules/shield-studies-addon-utils/lib/event-target.js
@@ -0,0 +1,55 @@
+/**
+ * Drop-in replacement for {@link external:sdk/event/target.EventTarget} for use
+ * with es6 classes.
+ * @module event-target
+ * @author Martin Giger
+ * @license MPL-2.0
+ */
+ /**
+ * An SDK class that add event reqistration methods
+ * @external sdk/event/target
+ * @requires sdk/event/target
+ */
+/**
+ * @class EventTarget
+ * @memberof external:sdk/event/target
+ * @see {@link https://developer.mozilla.org/en-US/Add-ons/SDK/Low-Level_APIs/event_target#EventTarget}
+ */
+
+// slightly modified from: https://raw.githubusercontent.com/freaktechnik/justintv-stream-notifications/master/lib/event-target.js
+
+"use strict";
+
+const { on, once, off, setListeners } = require("sdk/event/core");
+
+/* istanbul ignore next */
+/**
+ * @class
+ */
+class EventTarget {
+ constructor(options) {
+ setListeners(this, options);
+ }
+
+ on(...args) {
+ on(this, ...args);
+ return this;
+ }
+
+ once(...args) {
+ once(this, ...args);
+ return this;
+ }
+
+ off(...args) {
+ off(this, ...args);
+ return this;
+ }
+
+ removeListener(...args) {
+ off(this, ...args);
+ return this;
+ }
+}
+
+exports.EventTarget = EventTarget;
diff --git a/node_modules/shield-studies-addon-utils/lib/index.js b/node_modules/shield-studies-addon-utils/lib/index.js
new file mode 100644
index 0000000..9d4ec95
--- /dev/null
+++ b/node_modules/shield-studies-addon-utils/lib/index.js
@@ -0,0 +1,428 @@
+"use strict";
+
+// Chrome privileged
+const {Cu} = require("chrome");
+const { Services } = Cu.import("resource://gre/modules/Services.jsm");
+const { TelemetryController } = Cu.import("resource://gre/modules/TelemetryController.jsm");
+const CID = Cu.import("resource://gre/modules/ClientID.jsm");
+
+// sdk
+const { merge } = require("sdk/util/object");
+const querystring = require("sdk/querystring");
+const { prefs } = require("sdk/simple-prefs");
+const prefSvc = require("sdk/preferences/service");
+const { setInterval } = require("sdk/timers");
+const tabs = require("sdk/tabs");
+const { URL } = require("sdk/url");
+
+const { EventTarget } = require("./event-target");
+const { emit } = require("sdk/event/core");
+const self = require("sdk/self");
+
+const DAY = 86400*1000;
+
+// ongoing within-addon fuses / timers
+let lastDailyPing = Date.now();
+
+/* Functional, self-contained utils */
+
+// equal probability choices from a list "choices"
+function chooseVariation(choices,rng=Math.random()) {
+ let l = choices.length;
+ return choices[Math.floor(l*Math.random())];
+}
+
+function dateToUTC(date) {
+ return Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds());
+}
+
+function generateTelemetryIdIfNeeded() {
+ let id = TelemetryController.clientID;
+ /* istanbul ignore next */
+ if (id == undefined) {
+ return CID.ClientIDImpl._doLoadClientID()
+ } else {
+ return Promise.resolve(id)
+ }
+}
+
+function userId () {
+ return prefSvc.get("toolkit.telemetry.cachedClientID","unknown");
+}
+
+var Reporter = new EventTarget().on("report",
+ (d) => prefSvc.get('shield.debug') && console.log("report",d)
+);
+
+function report(data, src="addon", bucket="shield-study") {
+ data = merge({}, data , {
+ study_version: self.version,
+ about: {
+ _src: src,
+ _v: 2
+ }
+ });
+ if (prefSvc.get('shield.testing')) data.testing = true
+
+ emit(Reporter, "report", data);
+ let telOptions = {addClientId: true, addEnvironment: true}
+ return TelemetryController.submitExternalPing(bucket, data, telOptions);
+}
+
+function survey (url, queryArgs={}) {
+ if (! url) return
+
+ let U = new URL(url);
+ let q = U.search;
+ if (q) {
+ url = U.href.split(q)[0];
+ q = querystring.parse(querystring.unescape(q.slice(1)));
+ } else {
+ q = {};
+ }
+ // get user info.
+ let newArgs = merge({},
+ q,
+ queryArgs
+ );
+ let searchstring = querystring.stringify(newArgs);
+ url = url + "?" + searchstring;
+ return url;
+}
+
+
+function setOrGetFirstrun () {
+ let firstrun = prefs["shield.firstrun"];
+ if (firstrun === undefined) {
+ firstrun = prefs["shield.firstrun"] = String(dateToUTC(new Date())) // in utc, user set
+ }
+ return Number(firstrun)
+}
+
+function reuseVariation (choices) {
+ return prefs["shield.variation"];
+}
+
+function setVariation (choice) {
+ prefs["shield.variation"] = choice
+ return choice
+}
+
+function die (addonId=self.id) {
+ /* istanbul ignore else */
+ if (prefSvc.get("shield.fakedie")) return;
+ /* istanbul ignore next */
+ require("sdk/addon/installer").uninstall(addonId);
+}
+
+// TODO: GRL vulnerable to clock time issues #1
+function expired (xconfig, now = Date.now() ) {
+ return ((now - Number(xconfig.firstrun))/ DAY) > xconfig.days;
+}
+
+function resetShieldPrefs () {
+ delete prefs['shield.firstrun'];
+ delete prefs['shield.variation'];
+}
+
+function cleanup () {
+ prefSvc.keys(`extensions.${self.preferencesBranch}`).forEach (
+ (p) => {
+ delete prefs[p];
+ })
+}
+
+function telemetrySubset (xconfig) {
+ return {
+ study_name: xconfig.name,
+ branch: xconfig.variation,
+ }
+}
+
+class Study extends EventTarget {
+ constructor (config) {
+ super();
+ this.config = merge({
+ name: self.addonId,
+ variations: {'observe-only': () => {}},
+ surveyUrls: {},
+ days: 7
+ },config);
+
+ this.config.firstrun = setOrGetFirstrun();
+
+ let variation = reuseVariation();
+ if (variation === undefined) {
+ variation = this.decideVariation();
+ if (!(variation in this.config.variations)) {
+ // chaijs doesn't think this is an instanceof Error
+ // freaktechnik and gregglind debugged for a while.
+ // sdk errors might not be 'Errors' or chai is wack, who knows.
+ // https://dxr.mozilla.org/mozilla-central/search?q=regexp%3AError%5Cs%3F(%3A%7C%3D)+path%3Aaddon-sdk%2Fsource%2F&redirect=false would list
+ throw new Error("Study Error: chosen variation must be in config.variations")
+ }
+ setVariation(variation);
+ }
+ this.config.variation = variation;
+
+ this.flags = {
+ ineligibleDie: undefined
+ };
+ this.states = [];
+ // all these work, but could be cleaner. I hate the `bind` stuff.
+ this.on(
+ "change", (function (newstate) {
+ prefSvc.get('shield.debug') && console.log(newstate, this.states);
+ this.states.push(newstate);
+ emit(this, newstate); // could have checks here.
+ }).bind(this)
+ )
+ this.on(
+ "starting", (function () {
+ this.changeState("modifying");
+ }).bind(this)
+ )
+ this.on(
+ "maybe-installing", (function () {
+ if (!this.isEligible()) {
+ this.changeState("ineligible-die");
+ } else {
+ this.changeState("installed")
+ }
+ }).bind(this)
+ )
+ this.on(
+ "ineligible-die", (function () {
+ try {this.whenIneligible()} catch (err) {/*ok*/} finally { /*ok*/ }
+ this.flags.ineligibleDie = true;
+ this.report(merge({}, telemetrySubset(this.config), {study_state: "ineligible"}), "shield");
+ this.final();
+ die();
+ }).bind(this)
+ )
+ this.on(
+ "installed", (function () {
+ try {this.whenInstalled()} catch (err) {/*ok*/} finally { /*ok*/ }
+ this.report(merge({}, telemetrySubset(this.config), {study_state: "install"}), "shield");
+ this.changeState("modifying");
+ }).bind(this)
+ )
+ this.on(
+ "modifying", (function () {
+ var mybranchname = this.variation;
+ this.config.variations[mybranchname](); // do the effect
+ this.changeState("running");
+ }).bind(this)
+ )
+ this.on( // the one 'many'
+ "running", (function () {
+ // report success
+ this.report(merge({}, telemetrySubset(this.config), {study_state: "running"}), "shield");
+ this.final();
+ }).bind(this)
+ )
+ this.on(
+ "normal-shutdown", (function () {
+ this.flags.dying = true;
+ this.report(merge({}, telemetrySubset(this.config), {study_state: "shutdown"}), "shield");
+ this.final();
+ }).bind(this)
+ )
+ this.on(
+ "end-of-study", (function () {
+ if (this.flags.expired) { // safe to call multiple times
+ this.final();
+ return;
+ } else {
+ // first time seen.
+ this.flags.expired = true;
+ try {this.whenComplete()} catch (err) { /*ok*/ } finally { /*ok*/ }
+ this.report(merge({}, telemetrySubset(this.config) ,{study_state: "end-of-study"}), "shield");
+ // survey for end of study
+ let that = this;
+ generateTelemetryIdIfNeeded().then(()=>that.showSurvey("end-of-study"));
+ try {this.cleanup()} catch (err) {/*ok*/} finally { /*ok*/ }
+ this.final();
+ die();
+ }
+ }).bind(this)
+ )
+ this.on(
+ "user-uninstall-disable", (function () {
+ if (this.flags.dying) {
+ this.final();
+ return;
+ }
+ this.flags.dying = true;
+ this.report(merge({}, telemetrySubset(this.config), {study_state: "user-ended-study"}), "shield");
+ let that = this;
+ generateTelemetryIdIfNeeded().then(()=>that.showSurvey("user-ended-study"));
+ try {this.cleanup()} catch (err) {/*ok*/} finally { /*ok*/ }
+ this.final();
+ die();
+ }).bind(this)
+ )
+ }
+
+ get state () {
+ let n = this.states.length;
+ return n ? this.states[n-1] : undefined
+ }
+
+ get variation () {
+ return this.config.variation;
+ }
+
+ get firstrun () {
+ return this.config.firstrun;
+ }
+
+ dieIfExpired () {
+ let xconfig = this.config;
+ if (expired(xconfig)) {
+ emit(this, "change", "end-of-study");
+ return true
+ } else {
+ return false
+ }
+ }
+
+ alivenessPulse (last=lastDailyPing) {
+ // check for new day, phone home if true.
+ let t = Date.now();
+ if ((t - last) >= DAY) {
+ lastDailyPing = t;
+ // phone home
+ emit(this,"change","running");
+ }
+ // check expiration, and die with report if needed
+ return this.dieIfExpired();
+ }
+
+ changeState (newstate) {
+ emit(this,'change', newstate);
+ }
+
+ final () {
+ emit(this,'final', {});
+ }
+
+ startup (reason) {
+ // https://developer.mozilla.org/en-US/Add-ons/SDK/Tutorials/Listening_for_load_and_unload
+
+ // check expiry first, before anything, quit and die if so
+
+ // check once, right away, short circuit both install and startup
+ // to prevent modifications from happening.
+ if (this.dieIfExpired()) return this
+
+ switch (reason) {
+ case "install":
+ emit(this, "change", "maybe-installing");
+ break;
+
+ case "enable":
+ case "startup":
+ case "upgrade":
+ case "downgrade":
+ emit(this, "change", "starting");
+ }
+
+ if (! this._pulseTimer) this._pulseTimer = setInterval(this.alivenessPulse.bind(this), 5*60*1000 /*5 minutes */)
+ return this;
+ }
+
+ shutdown (reason) {
+ // https://developer.mozilla.org/en-US/Add-ons/SDK/Tutorials/Listening_for_load_and_unload
+ if (this.flags.ineligibleDie ||
+ this.flags.expired ||
+ this.flags.dying
+ ) { return this } // special cases.
+
+ switch (reason) {
+ case "uninstall":
+ case "disable":
+ emit(this, "change", "user-uninstall-disable");
+ break;
+
+ // 5. usual end of session.
+ case "shutdown":
+ case "upgrade":
+ case "downgrade":
+ emit(this, "change", "normal-shutdown")
+ break;
+ }
+ return this;
+ }
+
+ cleanup () {
+ // do the simple prefs and simplestorage cleanup
+ // extend by extension
+ resetShieldPrefs();
+ cleanup();
+ }
+
+ isEligible () {
+ return true;
+ }
+
+ whenIneligible () {
+ // empty function unless overrided
+ }
+
+ whenInstalled () {
+ // empty unless overrided
+ }
+
+ whenComplete () {
+ // when the study expires
+ }
+
+ /**
+ * equal choice from varations, by default. override to get unequal
+ */
+ decideVariation (rng=Math.random()) {
+ return chooseVariation(Object.keys(this.config.variations), rng);
+ }
+
+ get surveyQueryArgs () {
+ return {
+ variation: this.variation,
+ xname: this.config.name,
+ who: userId(),
+ updateChannel: Services.appinfo.defaultUpdateChannel,
+ fxVersion: Services.appinfo.version,
+ }
+ }
+
+ showSurvey(reason) {
+ let partial = this.config.surveyUrls[reason];
+
+ let queryArgs = this.surveyQueryArgs;
+ queryArgs.reason = reason;
+ if (partial) {
+ let url = survey(partial, queryArgs);
+ tabs.open(url);
+ return url
+ } else {
+ return
+ }
+ }
+
+ report () { // convenience only
+ return report.apply(null, arguments);
+ }
+}
+
+module.exports = {
+ chooseVariation: chooseVariation,
+ die: die,
+ expired: expired,
+ generateTelemetryIdIfNeeded: generateTelemetryIdIfNeeded,
+ report: report,
+ Reporter: Reporter,
+ resetShieldPrefs: resetShieldPrefs,
+ Study: Study,
+ cleanup: cleanup,
+ survey: survey
+}
diff --git a/node_modules/shield-studies-addon-utils/package.json b/node_modules/shield-studies-addon-utils/package.json
new file mode 100644
index 0000000..34eaa81
--- /dev/null
+++ b/node_modules/shield-studies-addon-utils/package.json
@@ -0,0 +1,70 @@
+{
+ "name": "shield-studies-addon-utils",
+ "version": "2.0.0",
+ "description": "Utilities for building Shield-Study Mozilla Firefox addons.",
+ "main": "lib/index.js",
+ "scripts": {
+ "test": "grunt test && istanbul check-coverage --statements 100 --functions 100 --branches 100 --lines 100 coverage/reports/coverage.json"
+ },
+ "author": {
+ "name": "Gregg Lind",
+ "email": "glind@mozilla.com"
+ },
+ "license": "MPL-2.0",
+ "dependencies": {},
+ "devDependencies": {
+ "chai": "3.5.0",
+ "grunt": "1.0.1",
+ "grunt-babel": "6.0.0",
+ "grunt-cli": "1.2.0",
+ "grunt-eslint": "18.1.0",
+ "grunt-explainjs": "0.0.4",
+ "grunt-istanbul": "0.7.0",
+ "grunt-shell": "1.3.0",
+ "istanbul-jpm": "0.1.0",
+ "jpm": "1.1.4"
+ },
+ "bugs": {
+ "url": "https://github.com/gregglind/shield-studies-addon-utils/issues"
+ },
+ "files": [
+ "lib/"
+ ],
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/mozilla/shield-studies-addon-utils.git"
+ },
+ "keywords": [
+ "mozilla",
+ "addon",
+ "shield",
+ "shield-study"
+ ],
+ "homepage": "https://github.com/mozilla/shield-studies-addon-utils#readme",
+ "gitHead": "e12b0dee92afe4bfc26b3aa0c1fac22311d4caaf",
+ "_id": "shield-studies-addon-utils@2.0.0",
+ "_shasum": "cde2579ca6b8444fe5bd69f64443464cdcdcbcf6",
+ "_from": "shield-studies-addon-utils@latest",
+ "_npmVersion": "3.8.3",
+ "_nodeVersion": "5.10.1",
+ "_npmUser": {
+ "name": "gregglind",
+ "email": "gregg.lind@gmail.com"
+ },
+ "maintainers": [
+ {
+ "name": "gregglind",
+ "email": "gregg.lind@gmail.com"
+ }
+ ],
+ "dist": {
+ "shasum": "cde2579ca6b8444fe5bd69f64443464cdcdcbcf6",
+ "tarball": "https://registry.npmjs.org/shield-studies-addon-utils/-/shield-studies-addon-utils-2.0.0.tgz"
+ },
+ "_npmOperationalInternal": {
+ "host": "packages-12-west.internal.npmjs.com",
+ "tmp": "tmp/shield-studies-addon-utils-2.0.0.tgz_1473277641626_0.6047592030372471"
+ },
+ "directories": {},
+ "_resolved": "https://registry.npmjs.org/shield-studies-addon-utils/-/shield-studies-addon-utils-2.0.0.tgz"
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..27ef0a2
--- /dev/null
+++ b/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "unified-urlbar-shield-study",
+ "description": "A Shield Study add-on testing the search experience.",
+ "version": "2.1.1",
+ "author": "Marco Bonardo ",
+ "devDependencies": {
+ "jpm": "^1.0.7",
+ "shield-studies-addon-utils": "^2.0.0"
+ },
+ "engines": {
+ "firefox": ">=50.0a1"
+ },
+ "keywords": [
+ "jetpack",
+ "shield-study"
+ ],
+ "permissions": {
+ "multiprocess": true
+ },
+ "license": "MPL-2.0",
+ "main": "lib/index.js",
+ "title": "Search Shield Study"
+}