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 [![Build Status](https://travis-ci.org/tombell/travis-ci-status.svg?branch=master)](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" +}