From 270068fd8da1b1c0ac8a258396f2b72e3d8ad6ad Mon Sep 17 00:00:00 2001 From: Stanca Serban Date: Sat, 7 Oct 2023 00:17:27 +0300 Subject: [PATCH] Backed out 2 changesets (bug 1853910) for causing mochitests failures in browser_ext_urlbar_attributionURL.js. CLOSED TREE Backed out changeset eea288462cbb (bug 1853910) Backed out changeset 75543d7d4250 (bug 1853910) --- browser/app/profile/firefox.js | 2 + browser/components/extensions/parent/ext-urlbar.js | 4 +- .../extensions/test/browser/browser_ext_urlbar.js | 4 + .../extensions/test/xpcshell/test_ext_urlbar.js | 54 + .../test/browser/browser_rich_suggestions.js | 19 + browser/components/urlbar/UrlbarController.sys.mjs | 42 + browser/components/urlbar/UrlbarPrefs.sys.mjs | 3 + browser/components/urlbar/docs/preferences.rst | 4 + browser/components/urlbar/docs/telemetry.rst | 117 ++ .../urlbar/tests/browser-tips/browser_picks.js | 19 + .../browser-tips/browser_searchTips_interaction.js | 157 ++- .../components/urlbar/tests/browser/browser.toml | 20 + .../browser_urlbar_event_telemetry_abandonment.js | 357 +++++ .../browser_urlbar_event_telemetry_engagement.js | 1377 ++++++++++++++++++++ .../browser_urlbar_event_telemetry_noEvent.js | 81 ++ browser/components/urlbar/tests/ext/api.js | 15 +- .../urlbar/tests/ext/browser/browser.toml | 2 + .../browser_ext_urlbar_engagementTelemetry.js | 18 + toolkit/components/telemetry/Events.yaml | 50 + 19 files changed, 2328 insertions(+), 17 deletions(-) create mode 100644 browser/components/urlbar/tests/browser/browser_urlbar_event_telemetry_abandonment.js create mode 100644 browser/components/urlbar/tests/browser/browser_urlbar_event_telemetry_engagement.js create mode 100644 browser/components/urlbar/tests/browser/browser_urlbar_event_telemetry_noEvent.js create mode 100644 browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_engagementTelemetry.js diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js index 2f123d3759fe..ef8b62bb64e8 100644 --- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -584,6 +584,8 @@ pref("browser.urlbar.shortcuts.bookmarks", true); pref("browser.urlbar.shortcuts.tabs", true); pref("browser.urlbar.shortcuts.history", true); +pref("browser.urlbar.eventTelemetry.enabled", false); + // When we send events to Urlbar extensions, we wait this amount of time in // milliseconds for them to respond before timing out. pref("browser.urlbar.extension.timeout", 400); diff --git a/browser/components/extensions/parent/ext-urlbar.js b/browser/components/extensions/parent/ext-urlbar.js index 850d526a7db1..163255846170 100644 --- a/browser/components/extensions/parent/ext-urlbar.js +++ b/browser/components/extensions/parent/ext-urlbar.js @@ -5,6 +5,7 @@ "use strict"; ChromeUtils.defineESModuleGetters(this, { + UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs", UrlbarProviderExtension: "resource:///modules/UrlbarProviderExtension.sys.mjs", }); @@ -144,8 +145,7 @@ this.urlbar = class extends ExtensionAPI { engagementTelemetry: getSettingsAPI({ context, name: "engagementTelemetry", - readOnly: true, - callback: () => false, + callback: () => UrlbarPrefs.get("eventTelemetry.enabled"), }), }, }; diff --git a/browser/components/extensions/test/browser/browser_ext_urlbar.js b/browser/components/extensions/test/browser/browser_ext_urlbar.js index fc3f14a2ab54..5a2c84e5e724 100644 --- a/browser/components/extensions/test/browser/browser_ext_urlbar.js +++ b/browser/components/extensions/test/browser/browser_ext_urlbar.js @@ -489,6 +489,9 @@ add_task(async function closeView() { add_task(async function onEngagement() { gURLBar.blur(); + // Enable engagement telemetry. + Services.prefs.setBoolPref("browser.urlbar.eventTelemetry.enabled", true); + let ext = ExtensionTestUtils.loadExtension({ manifest: { permissions: ["urlbar"], @@ -599,4 +602,5 @@ add_task(async function onEngagement() { Assert.equal(state, "engagement"); await ext.unload(); + Services.prefs.clearUserPref("browser.urlbar.eventTelemetry.enabled"); }); diff --git a/browser/components/extensions/test/xpcshell/test_ext_urlbar.js b/browser/components/extensions/test/xpcshell/test_ext_urlbar.js index f156ef36b08d..94c370630da6 100644 --- a/browser/components/extensions/test/xpcshell/test_ext_urlbar.js +++ b/browser/components/extensions/test/xpcshell/test_ext_urlbar.js @@ -5,6 +5,7 @@ const { AddonTestUtils } = ChromeUtils.importESModule( ); ChromeUtils.defineESModuleGetters(this, { + ExtensionParent: "resource://gre/modules/ExtensionParent.sys.mjs", SearchTestUtils: "resource://testing-common/SearchTestUtils.sys.mjs", UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs", UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.sys.mjs", @@ -31,6 +32,17 @@ AddonTestUtils.createAppInfo( SearchTestUtils.init(this); SearchTestUtils.initXPCShellAddonManager(this, "system"); +function promiseUninstallCompleted(extensionId) { + return new Promise(resolve => { + // eslint-disable-next-line mozilla/balanced-listeners + ExtensionParent.apiManager.on("uninstall-complete", (type, { id }) => { + if (id === extensionId) { + executeSoon(resolve); + } + }); + }); +} + function getPayload(result) { let payload = {}; for (let [key, value] of Object.entries(result.payload)) { @@ -1450,3 +1462,45 @@ add_task(async function test_nonPrivateBrowsing() { await ext.unload(); }); + +// Tests the engagementTelemetry property. +add_task(async function test_engagementTelemetry() { + let getPrefValue = () => UrlbarPrefs.get("eventTelemetry.enabled"); + + Assert.equal( + getPrefValue(), + false, + "Engagement telemetry should be disabled by default" + ); + + let ext = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["urlbar"], + }, + isPrivileged: true, + incognitoOverride: "spanning", + useAddonManager: "temporary", + async background() { + await browser.urlbar.engagementTelemetry.set({ value: true }); + browser.test.sendMessage("ready"); + }, + }); + await ext.startup(); + await ext.awaitMessage("ready"); + + Assert.equal( + getPrefValue(), + true, + "Successfully enabled the engagement telemetry" + ); + + let completed = promiseUninstallCompleted(ext.id); + await ext.unload(); + await completed; + + Assert.equal( + getPrefValue(), + false, + "Engagement telemetry should be reset after unloading the add-on" + ); +}); diff --git a/browser/components/search/test/browser/browser_rich_suggestions.js b/browser/components/search/test/browser/browser_rich_suggestions.js index 98adedcee592..cad5b9e2a4b9 100644 --- a/browser/components/search/test/browser/browser_rich_suggestions.js +++ b/browser/components/search/test/browser/browser_rich_suggestions.js @@ -30,6 +30,7 @@ add_setup(async () => { ["browser.urlbar.suggest.searches", true], ["browser.urlbar.trending.featureGate", true], ["browser.urlbar.trending.requireSearchMode", false], + ["browser.urlbar.eventTelemetry.enabled", true], // Bug 1775917: Disable the persisted-search-terms search tip because if // not dismissed, it can cause issues with other search tests. ["browser.urlbar.tipShownCount.searchTip_persist", 999], @@ -84,6 +85,24 @@ async function check_results({ featureEnabled = false }) { EventUtils.synthesizeKey("KEY_ArrowDown", {}, window); EventUtils.synthesizeKey("VK_RETURN", {}, window); + let event = { + category: "urlbar", + method: "engagement", + object: "enter", + value: "typed", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "0", + numWords: "0", + selIndex: "0", + selType: featureEnabled ? "trending_rich" : "trending", + provider: "SearchSuggestions", + }, + }; + + TelemetryTestUtils.assertEvents([event], { + category: "urlbar", + }); let scalars = TelemetryTestUtils.getProcessScalars("parent", false, true); TelemetryTestUtils.assertScalar(scalars, "urlbar.engagement", 1); diff --git a/browser/components/urlbar/UrlbarController.sys.mjs b/browser/components/urlbar/UrlbarController.sys.mjs index 26c6900de808..3c23162371b8 100644 --- a/browser/components/urlbar/UrlbarController.sys.mjs +++ b/browser/components/urlbar/UrlbarController.sys.mjs @@ -995,6 +995,48 @@ class TelemetryEvent { return; } + if (action == "go_button") { + // Fall back since the conventional telemetry dones't support "go_button" action. + action = "click"; + } + + let endTime = (event && event.timeStamp) || Cu.now(); + let startTime = startEventInfo.timeStamp || endTime; + // Synthesized events in tests may have a bogus timeStamp, causing a + // subtraction between monotonic and non-monotonic timestamps; that's why + // abs is necessary here. It should only happen in tests, anyway. + let elapsed = Math.abs(Math.round(endTime - startTime)); + + // Rather than listening to the pref, just update status when we record an + // event, if the pref changed from the last time. + let recordingEnabled = lazy.UrlbarPrefs.get("eventTelemetry.enabled"); + if (this._eventRecordingEnabled != recordingEnabled) { + this._eventRecordingEnabled = recordingEnabled; + Services.telemetry.setEventRecordingEnabled("urlbar", recordingEnabled); + } + + let extra = { + elapsed: elapsed.toString(), + numChars, + numWords, + }; + + if (method == "engagement") { + extra.selIndex = details.selIndex.toString(); + extra.selType = details.selType; + extra.provider = details.provider || ""; + } + + // We invoke recordEvent regardless, if recording is disabled this won't + // report the events remotely, but will count it in the event_counts scalar. + Services.telemetry.recordEvent( + this._category, + method, + action, + startEventInfo.interactionType, + extra + ); + Services.telemetry.scalarAdd( method == "engagement" ? TELEMETRY_SCALAR_ENGAGEMENT diff --git a/browser/components/urlbar/UrlbarPrefs.sys.mjs b/browser/components/urlbar/UrlbarPrefs.sys.mjs index 4d12a0b05163..00ccd5b6db3e 100644 --- a/browser/components/urlbar/UrlbarPrefs.sys.mjs +++ b/browser/components/urlbar/UrlbarPrefs.sys.mjs @@ -105,6 +105,9 @@ const PREF_URLBAR_DEFAULTS = new Map([ // 0 - never resolve; 1 - use heuristics (default); 2 - always resolve ["dnsResolveSingleWordsAfterSearch", 0], + // Whether telemetry events should be recorded. + ["eventTelemetry.enabled", false], + // Whether we expand the font size when when the urlbar is // focused. ["experimental.expandTextOnFocus", false], diff --git a/browser/components/urlbar/docs/preferences.rst b/browser/components/urlbar/docs/preferences.rst index 80a5ebb9348e..fca0850bfb83 100644 --- a/browser/components/urlbar/docs/preferences.rst +++ b/browser/components/urlbar/docs/preferences.rst @@ -131,6 +131,10 @@ browser.urlbar.dnsResolveSingleWordsAfterSearch (number, default: 0) "Did you mean to go to 'host'" prompt. Set to 0. 0: Never resolve, 1: Use heuristics, 2. Always resolve. +browser.urlbar.eventTelemetry.enabled (boolean, default: false) + Whether telemetry events should be recorded. This is expensive and should only + be enabled by experiments with a small population. + browser.urlbar.extension.timeout (integer, default: 400) When sending events to extensions, they have this amount of time in milliseconds to respond before timing out. This affects the omnibox API. diff --git a/browser/components/urlbar/docs/telemetry.rst b/browser/components/urlbar/docs/telemetry.rst index 3080b34207eb..2912264acf04 100644 --- a/browser/components/urlbar/docs/telemetry.rst +++ b/browser/components/urlbar/docs/telemetry.rst @@ -650,3 +650,120 @@ Firefox Suggest :doc:`firefox-suggest-telemetry` document. .. _search telemetry: /browser/search/telemetry.html + +Event Telemetry +--------------- + + .. note:: + This is a legacy event telemetry. For the current telemetry, please see + `Search Engagement Telemetry`_. These legacy events were disabled by default + and required enabling through a preference or a Urlbar WebExtension + experimental API. + +.. _Search Engagement Telemetry: #search-engagement-telemetry + +The event telemetry is grouped under the ``urlbar`` category. + +Event Method +~~~~~~~~~~~~ + + There are two methods to describe the interaction with the urlbar: + + - ``engagement`` + It is defined as a completed action in urlbar, where a user inserts text + and executes one of the actions described in the Event Object. + - ``abandonment`` + It is defined as an action where the user inserts text but does not + complete an engagement action, usually unfocusing the urlbar. This also + happens when the user switches to another window, regardless of urlbar + focus. + +Event Value +~~~~~~~~~~~ + + This is how the user interaction started + + - ``typed``: The text was typed into the urlbar. + - ``dropped``: The text was drag and dropped into the urlbar. + - ``pasted``: The text was pasted into the urlbar. + - ``topsites``: The user opened the urlbar view without typing, dropping, + or pasting. + In these cases, if the urlbar input is showing the URL of the loaded page + and the user has not modified the input’s content, the urlbar views shows + the user’s top sites. Otherwise, if the user had modified the input’s + content, the urlbar view shows results based on what the user has typed. + To tell whether top sites were shown, it's enough to check whether value is + ``topsites``. To know whether the user actually picked a top site, check + check that ``numChars`` == 0. If ``numChars`` > 0, the user initially opened + top sites, but then they started typing and confirmed a different result. + - ``returned``: The user abandoned a search, for example by switching to + another tab/window, or focusing something else, then came back to it + and continued. We consider a search continued if the user kept at least the + first char of the original search string. + - ``restarted``: The user abandoned a search, for example by switching to + another tab/window, or focusing something else, then came back to it, + cleared it and then typed a new string. + +Event Object +~~~~~~~~~~~~ + + These describe actions in the urlbar: + + - ``click`` + The user clicked on a result. + - ``enter`` + The user confirmed a result with Enter. + - ``drop_go`` + The user dropped text on the input field. + - ``paste_go`` + The user used Paste and Go feature. It is not the same as paste and Enter. + - ``blur`` + The user unfocused the urlbar. This is only valid for ``abandonment``. + +Event Extra +~~~~~~~~~~~ + + This object contains additional information about the interaction. + Extra is a key-value store, where all the keys and values are strings. + + - ``elapsed`` + Time in milliseconds from the initial interaction to an action. + - ``numChars`` + Number of input characters the user typed or pasted at the time of + submission. + - ``numWords`` + Number of words in the input. The measurement is taken from a trimmed input + split up by its spaces. This is not a perfect measurement, since it will + return an incorrect value for languages that do not use spaces or URLs + containing spaces in its query parameters, for example. + - ``selType`` + The type of the selected result at the time of submission. + This is only present for ``engagement`` events. + It can be one of: ``none``, ``autofill``, ``visiturl``, ``bookmark``, + ``bookmark_adaptive`` ``history``, ``history_adaptive``, ``keyword``, + ``searchengine``, ``searchsuggestion``, ``switchtab``, ``remotetab``, + ``extension``, ``oneoff``, ``keywordoffer``, ``canonized``, ``tip``, + ``tiphelp``, ``formhistory``, ``tabtosearch``, ``help``, ``block``, + ``quicksuggest``, ``unknown`` + In practice, ``tabtosearch`` should not appear in real event telemetry. + Opening a tab-to-search result enters search mode and entering search mode + does not currently mark the end of an engagement. It is noted here for + completeness. Similarly, ``block`` indicates a result was blocked or deleted + but should not appear because blocking a result does not end the engagement. + - ``selIndex`` + Index of the selected result in the urlbar panel, or -1 for no selection. + There won't be a selection when a one-off button is the only selection, and + for the ``paste_go`` or ``drop_go`` objects. There may also not be a + selection if the system was busy and results arrived too late, then we + directly decide whether to search or visit the given string without having + a fully built result. + This is only present for ``engagement`` events. + - ``provider`` + The name of the result provider for the selected result. Existing values + are: ``HeuristicFallback``, ``Autofill``, ``Places``, + ``TokenAliasEngines``, ``SearchSuggestions``, ``UrlbarProviderTopSites``. + Data from before Firefox 91 will also list ``UnifiedComplete`` as a + provider. This is equivalent to ``Places``. + Values can also be defined by `URLBar provider experiments`_. + + .. _URLBar provider experiments: experiments.html#developing-address-bar-extensions diff --git a/browser/components/urlbar/tests/browser-tips/browser_picks.js b/browser/components/urlbar/tests/browser-tips/browser_picks.js index 9c097fd48b0b..60a7676668e6 100644 --- a/browser/components/urlbar/tests/browser-tips/browser_picks.js +++ b/browser/components/urlbar/tests/browser-tips/browser_picks.js @@ -14,6 +14,10 @@ add_setup(async function () { window.windowUtils.disableNonTestMouseEvents(false); }); Services.telemetry.clearScalars(); + Services.telemetry.clearEvents(); + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.eventTelemetry.enabled", true]], + }); }); add_task(async function enter_mainButton_url() { @@ -173,6 +177,21 @@ async function doTest({ click, buttonUrl = undefined, helpUrl = undefined }) { helpUrl ? "test-help" : "test-picked", 1 ); + TelemetryTestUtils.assertEvents( + [ + { + category: "urlbar", + method: "engagement", + object: + click && !(helpUrl && UrlbarPrefs.get("resultMenu")) + ? "click" + : "enter", + value: "typed", + }, + ], + { category: "urlbar" } + ); + // Done. UrlbarProvidersManager.unregisterProvider(provider); if (tab) { diff --git a/browser/components/urlbar/tests/browser-tips/browser_searchTips_interaction.js b/browser/components/urlbar/tests/browser-tips/browser_searchTips_interaction.js index 4af990eb142c..6fe29e7d0736 100644 --- a/browser/components/urlbar/tests/browser-tips/browser_searchTips_interaction.js +++ b/browser/components/urlbar/tests/browser-tips/browser_searchTips_interaction.js @@ -101,6 +101,11 @@ add_setup(async function () { // be not to be shown again in any session. Telemetry should be updated. add_task(async function pickButton_onboard() { UrlbarProviderSearchTips.disableTipsForCurrentSession = false; + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.eventTelemetry.enabled", true]], + }); + + Services.telemetry.clearEvents(); let tab = await BrowserTestUtils.openNewForegroundTab({ gBrowser, url: "about:newtab", @@ -124,6 +129,17 @@ add_task(async function pickButton_onboard() { `${UrlbarProviderSearchTips.TIP_TYPE.ONBOARD}-picked`, 1 ); + TelemetryTestUtils.assertEvents( + [ + { + category: "urlbar", + method: "engagement", + object: "click", + value: "typed", + }, + ], + { category: "urlbar" } + ); Assert.equal( UrlbarPrefs.get( @@ -136,12 +152,18 @@ add_task(async function pickButton_onboard() { resetSearchTipsProvider(); BrowserTestUtils.removeTab(tab); + await SpecialPowers.popPrefEnv(); }); // Picking the tip's button should cause the Urlbar to blank out and the tip to // be not to be shown again in any session. Telemetry should be updated. add_task(async function pickButton_redirect() { UrlbarProviderSearchTips.disableTipsForCurrentSession = false; + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.eventTelemetry.enabled", true]], + }); + Services.telemetry.clearEvents(); + await setDefaultEngine("Google"); await BrowserTestUtils.withNewTab("about:blank", async () => { await withDNSRedirect("www.google.com", "/", async url => { @@ -167,6 +189,17 @@ add_task(async function pickButton_redirect() { `${UrlbarProviderSearchTips.TIP_TYPE.REDIRECT}-picked`, 1 ); + TelemetryTestUtils.assertEvents( + [ + { + category: "urlbar", + method: "engagement", + object: "click", + value: "typed", + }, + ], + { category: "urlbar" } + ); Assert.equal( UrlbarPrefs.get( @@ -177,6 +210,7 @@ add_task(async function pickButton_redirect() { ); Assert.equal(gURLBar.value, "", "The Urlbar should be empty."); resetSearchTipsProvider(); + await SpecialPowers.popPrefEnv(); }); // Picking the tip's button should cause the Urlbar to keep its current @@ -185,8 +219,12 @@ add_task(async function pickButton_redirect() { add_task(async function pickButton_persist() { UrlbarProviderSearchTips.disableTipsForCurrentSession = false; await SpecialPowers.pushPrefEnv({ - set: [["browser.urlbar.showSearchTerms.featureGate", true]], + set: [ + ["browser.urlbar.eventTelemetry.enabled", true], + ["browser.urlbar.showSearchTerms.featureGate", true], + ], }); + Services.telemetry.clearEvents(); await setDefaultEngine("Example"); @@ -226,6 +264,17 @@ add_task(async function pickButton_persist() { `${UrlbarProviderSearchTips.TIP_TYPE.PERSIST}-picked`, 1 ); + TelemetryTestUtils.assertEvents( + [ + { + category: "urlbar", + method: "engagement", + object: "click", + value: "typed", + }, + ], + { category: "urlbar" } + ); Assert.equal( UrlbarPrefs.get( @@ -243,6 +292,11 @@ add_task(async function pickButton_persist() { // effect as picking the tip. add_task(async function clickInInput_onboard() { UrlbarProviderSearchTips.disableTipsForCurrentSession = false; + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.eventTelemetry.enabled", true]], + }); + Services.telemetry.clearEvents(); + await setDefaultEngine("Google"); let tab = await BrowserTestUtils.openNewForegroundTab({ gBrowser, @@ -265,6 +319,17 @@ add_task(async function clickInInput_onboard() { `${UrlbarProviderSearchTips.TIP_TYPE.ONBOARD}-picked`, 1 ); + TelemetryTestUtils.assertEvents( + [ + { + category: "urlbar", + method: "engagement", + object: "click", + value: "typed", + }, + ], + { category: "urlbar" } + ); Assert.equal( UrlbarPrefs.get( @@ -275,13 +340,20 @@ add_task(async function clickInInput_onboard() { ); Assert.equal(gURLBar.value, "", "The Urlbar should be empty."); resetSearchTipsProvider(); + BrowserTestUtils.removeTab(tab); + await SpecialPowers.popPrefEnv(); }); // Pressing Ctrl+L (the open location command) while the onboard tip is showing // should have the same effect as picking the tip. add_task(async function openLocation_onboard() { UrlbarProviderSearchTips.disableTipsForCurrentSession = false; + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.eventTelemetry.enabled", true]], + }); + Services.telemetry.clearEvents(); + await setDefaultEngine("Google"); let tab = await BrowserTestUtils.openNewForegroundTab({ gBrowser, @@ -304,6 +376,17 @@ add_task(async function openLocation_onboard() { `${UrlbarProviderSearchTips.TIP_TYPE.ONBOARD}-picked`, 1 ); + TelemetryTestUtils.assertEvents( + [ + { + category: "urlbar", + method: "engagement", + object: "enter", + value: "typed", + }, + ], + { category: "urlbar" } + ); Assert.equal( UrlbarPrefs.get( @@ -314,13 +397,20 @@ add_task(async function openLocation_onboard() { ); Assert.equal(gURLBar.value, "", "The Urlbar should be empty."); resetSearchTipsProvider(); + BrowserTestUtils.removeTab(tab); + await SpecialPowers.popPrefEnv(); }); // Clicking in the input while the redirect tip is showing should have the same // effect as picking the tip. add_task(async function clickInInput_redirect() { UrlbarProviderSearchTips.disableTipsForCurrentSession = false; + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.eventTelemetry.enabled", true]], + }); + Services.telemetry.clearEvents(); + await setDefaultEngine("Google"); await BrowserTestUtils.withNewTab("about:blank", async () => { await withDNSRedirect("www.google.com", "/", async url => { @@ -344,6 +434,17 @@ add_task(async function clickInInput_redirect() { `${UrlbarProviderSearchTips.TIP_TYPE.REDIRECT}-picked`, 1 ); + TelemetryTestUtils.assertEvents( + [ + { + category: "urlbar", + method: "engagement", + object: "click", + value: "typed", + }, + ], + { category: "urlbar" } + ); Assert.equal( UrlbarPrefs.get( @@ -354,6 +455,7 @@ add_task(async function clickInInput_redirect() { ); Assert.equal(gURLBar.value, "", "The Urlbar should be empty."); resetSearchTipsProvider(); + await SpecialPowers.popPrefEnv(); }); // Clicking in the input while the persist tip is showing should have the same @@ -361,8 +463,12 @@ add_task(async function clickInInput_redirect() { add_task(async function clickInInput_persist() { UrlbarProviderSearchTips.disableTipsForCurrentSession = false; await SpecialPowers.pushPrefEnv({ - set: [["browser.urlbar.showSearchTerms.featureGate", true]], + set: [ + ["browser.urlbar.eventTelemetry.enabled", true], + ["browser.urlbar.showSearchTerms.featureGate", true], + ], }); + Services.telemetry.clearEvents(); await setDefaultEngine("Example"); await BrowserTestUtils.withNewTab("about:blank", async () => { @@ -398,6 +504,17 @@ add_task(async function clickInInput_persist() { `${UrlbarProviderSearchTips.TIP_TYPE.PERSIST}-picked`, 1 ); + TelemetryTestUtils.assertEvents( + [ + { + category: "urlbar", + method: "engagement", + object: "click", + value: "typed", + }, + ], + { category: "urlbar" } + ); Assert.equal( UrlbarPrefs.get( @@ -408,12 +525,18 @@ add_task(async function clickInInput_persist() { ); Assert.equal(gURLBar.value, "", "The Urlbar should be empty."); resetSearchTipsProvider(); + await SpecialPowers.popPrefEnv(); }); // Pressing Ctrl+L (the open location command) while the redirect tip is showing // should have the same effect as picking the tip. add_task(async function openLocation_redirect() { UrlbarProviderSearchTips.disableTipsForCurrentSession = false; + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.eventTelemetry.enabled", true]], + }); + Services.telemetry.clearEvents(); + await setDefaultEngine("Google"); await BrowserTestUtils.withNewTab("about:blank", async () => { await withDNSRedirect("www.google.com", "/", async url => { @@ -437,6 +560,17 @@ add_task(async function openLocation_redirect() { `${UrlbarProviderSearchTips.TIP_TYPE.REDIRECT}-picked`, 1 ); + TelemetryTestUtils.assertEvents( + [ + { + category: "urlbar", + method: "engagement", + object: "enter", + value: "typed", + }, + ], + { category: "urlbar" } + ); Assert.equal( UrlbarPrefs.get( @@ -447,6 +581,7 @@ add_task(async function openLocation_redirect() { ); Assert.equal(gURLBar.value, "", "The Urlbar should be empty."); resetSearchTipsProvider(); + await SpecialPowers.popPrefEnv(); }); // Pressing Ctrl+L (the open location command) while the persist tip is showing @@ -454,8 +589,13 @@ add_task(async function openLocation_redirect() { add_task(async function openLocation_persist() { UrlbarProviderSearchTips.disableTipsForCurrentSession = false; await SpecialPowers.pushPrefEnv({ - set: [["browser.urlbar.showSearchTerms.featureGate", true]], + set: [ + ["browser.urlbar.eventTelemetry.enabled", true], + ["browser.urlbar.showSearchTerms.featureGate", true], + ], }); + Services.telemetry.clearEvents(); + await setDefaultEngine("Example"); await BrowserTestUtils.withNewTab("about:blank", async () => { let browserLoadedPromise = BrowserTestUtils.browserLoaded( @@ -490,6 +630,17 @@ add_task(async function openLocation_persist() { `${UrlbarProviderSearchTips.TIP_TYPE.PERSIST}-picked`, 1 ); + TelemetryTestUtils.assertEvents( + [ + { + category: "urlbar", + method: "engagement", + object: "enter", + value: "typed", + }, + ], + { category: "urlbar" } + ); Assert.equal( UrlbarPrefs.get( diff --git a/browser/components/urlbar/tests/browser/browser.toml b/browser/components/urlbar/tests/browser/browser.toml index 8e1962070f06..51875f3944e1 100644 --- a/browser/components/urlbar/tests/browser/browser.toml +++ b/browser/components/urlbar/tests/browser/browser.toml @@ -609,6 +609,26 @@ https_first_disabled = true ["browser_urlbar_annotation.js"] support-files = ["redirect_to.sjs"] +["browser_urlbar_event_telemetry_abandonment.js"] +support-files = [ + "searchSuggestionEngine.xml", + "searchSuggestionEngine.sjs", +] + +["browser_urlbar_event_telemetry_engagement.js"] +https_first_disabled = true +skip-if = [ + "apple_catalina", # Bug 1625690 + "apple_silicon", # Disabled due to bleedover with other tests when run in regular suites; passes in "failures" jobs + "os == 'linux'", # Bug 1748986, bug 1775824 +] +support-files = [ + "searchSuggestionEngine.xml", + "searchSuggestionEngine.sjs", +] + +["browser_urlbar_event_telemetry_noEvent.js"] + ["browser_urlbar_selection.js"] skip-if = ["(os == 'mac')"] # bug 1570474 diff --git a/browser/components/urlbar/tests/browser/browser_urlbar_event_telemetry_abandonment.js b/browser/components/urlbar/tests/browser/browser_urlbar_event_telemetry_abandonment.js new file mode 100644 index 000000000000..6f30392e48bd --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_urlbar_event_telemetry_abandonment.js @@ -0,0 +1,357 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + UrlbarUtils: "resource:///modules/UrlbarUtils.sys.mjs", +}); + +const TEST_ENGINE_NAME = "Test"; +const TEST_ENGINE_ALIAS = "@test"; +const TEST_ENGINE_DOMAIN = "example.com"; + +// Each test is a function that executes an urlbar action and returns the +// expected event object. +const tests = [ + async function (win) { + info("Type something, blur."); + win.gURLBar.select(); + EventUtils.synthesizeKey("x", {}, win); + win.gURLBar.blur(); + return { + category: "urlbar", + method: "abandonment", + object: "blur", + value: "typed", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "1", + numWords: "1", + }, + }; + }, + + async function (win) { + info("Open the panel with DOWN, don't type, blur it."); + await addTopSite("http://example.org/"); + win.gURLBar.value = ""; + win.gURLBar.select(); + await UrlbarTestUtils.promisePopupOpen(win, () => { + EventUtils.synthesizeKey("KEY_ArrowDown", {}, win); + }); + win.gURLBar.blur(); + return { + category: "urlbar", + method: "abandonment", + object: "blur", + value: "topsites", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "0", + numWords: "0", + }, + }; + }, + + async function (win) { + info("With pageproxystate=valid, autoopen the panel, don't type, blur it."); + win.gURLBar.value = ""; + await UrlbarTestUtils.promisePopupOpen(win, () => { + win.document.getElementById("Browser:OpenLocation").doCommand(); + }); + win.gURLBar.blur(); + return { + category: "urlbar", + method: "abandonment", + object: "blur", + value: "topsites", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "0", + numWords: "0", + }, + }; + }, + + async function (win) { + info("Enter search mode from Top Sites."); + await updateTopSites(sites => true, /* enableSearchShorcuts */ true); + + win.gURLBar.value = ""; + win.gURLBar.select(); + + await BrowserTestUtils.waitForCondition(async () => { + await UrlbarTestUtils.promisePopupOpen(win, () => { + EventUtils.synthesizeKey("KEY_ArrowDown", {}, win); + }); + + if (UrlbarTestUtils.getResultCount(win) > 1) { + return true; + } + + win.gURLBar.view.close(); + return false; + }); + + while (win.gURLBar.searchMode?.engineName != "Google") { + EventUtils.synthesizeKey("KEY_ArrowDown", {}, win); + } + + let element = UrlbarTestUtils.getSelectedRow(win); + Assert.ok( + element.result.source == UrlbarUtils.RESULT_SOURCE.SEARCH, + "The selected result is a search Top Site." + ); + + let engine = element.result.payload.engine; + let searchPromise = UrlbarTestUtils.promiseSearchComplete(win); + EventUtils.synthesizeMouseAtCenter(element, {}, win); + await searchPromise; + await UrlbarTestUtils.assertSearchMode(win, { + engineName: engine, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + entry: "topsites_urlbar", + }); + + await UrlbarTestUtils.exitSearchMode(win); + + // To avoid needing to add a custom search shortcut Top Site, we just + // abandon this interaction. + await UrlbarTestUtils.promisePopupClose(win, () => { + win.gURLBar.blur(); + }); + + return [ + // engagement on the top sites search engine to enter search mode + { + category: "urlbar", + method: "engagement", + object: "click", + value: "topsites", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "0", + numWords: "0", + selIndex: "0", + selType: "searchengine", + provider: "UrlbarProviderTopSites", + }, + }, + // abandonment + { + category: "urlbar", + method: "abandonment", + object: "blur", + value: "topsites", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "0", + numWords: "0", + }, + }, + ]; + }, + + async function (win) { + info("Open search mode from a tab-to-search result."); + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.tabToSearch.onboard.interactionsLeft", 0]], + }); + + await PlacesUtils.history.clear(); + for (let i = 0; i < 3; i++) { + await PlacesTestUtils.addVisits([`https://${TEST_ENGINE_DOMAIN}/`]); + } + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: TEST_ENGINE_DOMAIN.slice(0, 4), + }); + + let tabToSearchResult = ( + await UrlbarTestUtils.waitForAutocompleteResultAt(win, 1) + ).result; + Assert.equal( + tabToSearchResult.providerName, + "TabToSearch", + "The second result is a tab-to-search result." + ); + + // Select the tab-to-search result. + EventUtils.synthesizeKey("KEY_ArrowDown", {}, win); + let searchPromise = UrlbarTestUtils.promiseSearchComplete(win); + EventUtils.synthesizeKey("KEY_Enter", {}, win); + await searchPromise; + await UrlbarTestUtils.assertSearchMode(win, { + engineName: TEST_ENGINE_NAME, + entry: "tabtosearch", + }); + + // Abandon the interaction since simply entering search mode is not + // considered the end of an engagement. + await UrlbarTestUtils.promisePopupClose(win, () => { + win.gURLBar.blur(); + }); + + await PlacesUtils.history.clear(); + await SpecialPowers.popPrefEnv(); + + return [ + // engagement on the tab-to-search to enter search mode + { + category: "urlbar", + method: "engagement", + object: "enter", + value: "typed", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "4", + numWords: "1", + selIndex: "1", + selType: "tabtosearch", + provider: "TabToSearch", + }, + }, + // abandonment + { + category: "urlbar", + method: "abandonment", + object: "blur", + value: "typed", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "0", + numWords: "0", + }, + }, + ]; + }, + + async function (win) { + info( + "With pageproxystate=invalid, open retained results, don't type, blur it." + ); + win.gURLBar.value = "mochi.test"; + win.gURLBar.setPageProxyState("invalid"); + await UrlbarTestUtils.promisePopupOpen(win, () => { + win.document.getElementById("Browser:OpenLocation").doCommand(); + }); + win.gURLBar.blur(); + return { + category: "urlbar", + method: "abandonment", + object: "blur", + value: "returned", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "10", + numWords: "1", + }, + }; + }, +]; + +add_setup(async function () { + await PlacesUtils.history.clear(); + + // Create a new search engine and mark it as default + let engine = await SearchTestUtils.promiseNewSearchEngine({ + url: getRootDirectory(gTestPath) + "searchSuggestionEngine.xml", + setAsDefault: true, + }); + await Services.search.moveEngine(engine, 0); + + await SearchTestUtils.installSearchExtension({ + name: TEST_ENGINE_NAME, + keyword: TEST_ENGINE_ALIAS, + search_url: `https://${TEST_ENGINE_DOMAIN}/`, + }); + + // This test used to rely on the initial timer of + // TestUtils.waitForCondition. See bug 1667216. + let originalWaitForCondition = TestUtils.waitForCondition; + TestUtils.waitForCondition = async function ( + condition, + msg, + interval = 100, + maxTries = 50 + ) { + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 100)); + + return originalWaitForCondition(condition, msg, interval, maxTries); + }; + + registerCleanupFunction(async function () { + await PlacesUtils.history.clear(); + TestUtils.waitForCondition = originalWaitForCondition; + }); +}); + +async function doTest(eventTelemetryEnabled) { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.eventTelemetry.enabled", eventTelemetryEnabled]], + }); + + const win = await BrowserTestUtils.openNewBrowserWindow(); + + // This is not necessary after each loop, because assertEvents does it. + Services.telemetry.clearEvents(); + Services.telemetry.clearScalars(); + + for (let i = 0; i < tests.length; i++) { + info(`Running test at index ${i}`); + let events = await tests[i](win); + if (!Array.isArray(events)) { + events = [events]; + } + // Always blur to ensure it's not accounted as an additional abandonment. + win.gURLBar.setSearchMode({}); + win.gURLBar.blur(); + TelemetryTestUtils.assertEvents(eventTelemetryEnabled ? events : [], { + category: "urlbar", + }); + + // Scalars should be recorded regardless of `eventTelemetry.enabled`. + let scalars = TelemetryTestUtils.getProcessScalars("parent", false, true); + TelemetryTestUtils.assertScalar( + scalars, + "urlbar.engagement", + events.filter(e => e.method == "engagement").length || undefined + ); + TelemetryTestUtils.assertScalar( + scalars, + "urlbar.abandonment", + events.filter(e => e.method == "abandonment").length || undefined + ); + + await UrlbarTestUtils.formHistory.clear(win); + } + + await BrowserTestUtils.closeWindow(win); + await SpecialPowers.popPrefEnv(); +} + +add_task(async function enabled() { + await doTest(true); +}); + +add_task(async function disabled() { + await doTest(false); +}); + +/** + * Replaces the contents of Top Sites with the specified site. + * + * @param {string} site + * A site to add to Top Sites. + */ +async function addTopSite(site) { + await PlacesUtils.history.clear(); + for (let i = 0; i < 5; i++) { + await PlacesTestUtils.addVisits(site); + } + + await updateTopSites(sites => sites && sites[0] && sites[0].url == site); +} diff --git a/browser/components/urlbar/tests/browser/browser_urlbar_event_telemetry_engagement.js b/browser/components/urlbar/tests/browser/browser_urlbar_event_telemetry_engagement.js new file mode 100644 index 000000000000..6c9c2231f4bd --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_urlbar_event_telemetry_engagement.js @@ -0,0 +1,1377 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + UrlbarProvider: "resource:///modules/UrlbarUtils.sys.mjs", + UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.sys.mjs", + UrlbarUtils: "resource:///modules/UrlbarUtils.sys.mjs", +}); + +const TEST_ENGINE_NAME = "Test"; +const TEST_ENGINE_ALIAS = "@test"; +const TEST_ENGINE_DOMAIN = "example.com"; + +// This test has many subtests and can time out in verify mode. +requestLongerTimeout(5); + +// Each test is a function that executes an urlbar action and returns the +// expected event object. +const tests = [ + async function (win) { + info("Type something, press Enter."); + win.gURLBar.select(); + let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "x", + fireInputEvent: true, + }); + EventUtils.synthesizeKey("VK_RETURN", {}, win); + await promise; + return { + category: "urlbar", + method: "engagement", + object: "enter", + value: "typed", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "1", + numWords: "1", + selIndex: "0", + selType: "searchengine", + provider: "HeuristicFallback", + }, + }; + }, + + async function (win) { + info("Type a multi-word query, press Enter."); + win.gURLBar.select(); + let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "multi word query ", + fireInputEvent: true, + }); + EventUtils.synthesizeKey("VK_RETURN", {}, win); + await promise; + return { + category: "urlbar", + method: "engagement", + object: "enter", + value: "typed", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "17", + numWords: "3", + selIndex: "0", + selType: "searchengine", + provider: "HeuristicFallback", + }, + }; + }, + + async function (win) { + info("Paste something, press Enter."); + win.gURLBar.select(); + let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + await SimpleTest.promiseClipboardChange("test", () => { + clipboardHelper.copyString("test"); + }); + win.document.commandDispatcher + .getControllerForCommand("cmd_paste") + .doCommand("cmd_paste"); + EventUtils.synthesizeKey("VK_RETURN", {}, win); + await promise; + return { + category: "urlbar", + method: "engagement", + object: "enter", + value: "pasted", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "4", + numWords: "1", + selIndex: "0", + selType: "searchengine", + provider: "HeuristicFallback", + }, + }; + }, + + async function (win) { + info("Type something, click one-off and press enter."); + win.gURLBar.select(); + let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "test", + fireInputEvent: true, + }); + + let searchPromise = UrlbarTestUtils.promiseSearchComplete(win); + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }, win); + let selectedOneOff = + UrlbarTestUtils.getOneOffSearchButtons(win).selectedButton; + selectedOneOff.click(); + await searchPromise; + await UrlbarTestUtils.assertSearchMode(win, { + engineName: selectedOneOff.engine.name, + entry: "oneoff", + }); + + EventUtils.synthesizeKey("VK_RETURN", {}, win); + await promise; + return { + category: "urlbar", + method: "engagement", + object: "enter", + value: "typed", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "4", + numWords: "1", + selIndex: "0", + selType: "searchengine", + provider: "HeuristicFallback", + }, + }; + }, + + async function (win) { + info( + "Type something, select one-off with enter, and select result with enter." + ); + win.gURLBar.select(); + + let searchPromise = UrlbarTestUtils.promiseSearchComplete(win); + + let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "test", + fireInputEvent: true, + }); + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }, win); + let selectedOneOff = + UrlbarTestUtils.getOneOffSearchButtons(win).selectedButton; + Assert.ok(selectedOneOff); + EventUtils.synthesizeKey("VK_RETURN", {}, win); + await searchPromise; + + EventUtils.synthesizeKey("VK_RETURN", {}, win); + await promise; + return { + category: "urlbar", + method: "engagement", + object: "enter", + value: "typed", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "4", + numWords: "1", + selIndex: "0", + selType: "searchengine", + provider: "HeuristicFallback", + }, + }; + }, + + async function (win) { + info("Type something, ESC, type something else, press Enter."); + win.gURLBar.select(); + let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + EventUtils.synthesizeKey("x", {}, win); + EventUtils.synthesizeKey("VK_ESCAPE", {}, win); + EventUtils.synthesizeKey("y", {}, win); + EventUtils.synthesizeKey("VK_RETURN", {}, win); + await promise; + return { + category: "urlbar", + method: "engagement", + object: "enter", + value: "typed", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "1", + numWords: "1", + selIndex: "0", + selType: "searchengine", + provider: "HeuristicFallback", + }, + }; + }, + + async function (win) { + info("Type a keyword, Enter."); + win.gURLBar.select(); + let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "kw test", + fireInputEvent: true, + }); + EventUtils.synthesizeKey("VK_RETURN", {}, win); + await promise; + return { + category: "urlbar", + method: "engagement", + object: "enter", + value: "typed", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "7", + numWords: "2", + selIndex: "0", + selType: "keyword", + provider: "BookmarkKeywords", + }, + }; + }, + + async function (win) { + let tipProvider = registerTipProvider(); + info("Selecting a tip's main button, enter."); + win.gURLBar.search("x"); + await UrlbarTestUtils.promiseSearchComplete(win); + EventUtils.synthesizeKey("KEY_ArrowDown", {}, win); + EventUtils.synthesizeKey("KEY_ArrowDown", {}, win); + EventUtils.synthesizeKey("VK_RETURN", {}, win); + unregisterTipProvider(tipProvider); + return { + category: "urlbar", + method: "engagement", + object: "enter", + value: "typed", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "1", + numWords: "1", + selIndex: "1", + selType: "tip", + provider: tipProvider.name, + }, + }; + }, + + async function (win) { + let tipProvider = registerTipProvider(); + info("Selecting a tip's help option."); + let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + win.gURLBar.search("x"); + await UrlbarTestUtils.promiseSearchComplete(win); + EventUtils.synthesizeKey("KEY_ArrowDown", {}, win); + EventUtils.synthesizeKey("KEY_ArrowDown", {}, win); + if (UrlbarPrefs.get("resultMenu")) { + await UrlbarTestUtils.openResultMenuAndPressAccesskey(win, "h"); + } else { + EventUtils.synthesizeKey("KEY_Tab", {}, win); + EventUtils.synthesizeKey("VK_RETURN", {}, win); + } + await promise; + unregisterTipProvider(tipProvider); + return { + category: "urlbar", + method: "engagement", + object: "enter", + value: "typed", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "1", + numWords: "1", + selIndex: "1", + selType: "tiphelp", + provider: tipProvider.name, + }, + }; + }, + + async function (win) { + info("Type something and canonize"); + win.gURLBar.select(); + const promise = BrowserTestUtils.waitForDocLoadAndStopIt( + "https://www.example.com/", + win.gBrowser.selectedBrowser + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "example", + fireInputEvent: true, + }); + EventUtils.synthesizeKey("VK_RETURN", { ctrlKey: true }, win); + await promise; + return { + category: "urlbar", + method: "engagement", + object: "enter", + value: "typed", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "7", + numWords: "1", + selIndex: "0", + selType: "canonized", + provider: "Autofill", + }, + }; + }, + + async function (win) { + info("Type something, click on bookmark entry."); + // Add a clean bookmark. + const bookmark = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.com/bookmark", + title: "bookmark", + }); + + win.gURLBar.select(); + let url = "http://example.com/bookmark"; + let promise = BrowserTestUtils.browserLoaded( + win.gBrowser.selectedBrowser, + false, + url + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "boo", + fireInputEvent: true, + }); + while (win.gURLBar.untrimmedValue != url) { + EventUtils.synthesizeKey("KEY_ArrowDown", {}, win); + } + let element = UrlbarTestUtils.getSelectedRow(win); + EventUtils.synthesizeMouseAtCenter(element, {}, win); + await promise; + await PlacesUtils.bookmarks.remove(bookmark); + return { + category: "urlbar", + method: "engagement", + object: "click", + value: "typed", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "3", + numWords: "1", + selIndex: val => parseInt(val) > 0, + selType: "bookmark", + provider: "Places", + }, + }; + }, + + async function (win) { + info("Type an autofilled string, Enter."); + win.gURLBar.select(); + let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "exa", + fireInputEvent: true, + }); + // Check it's autofilled. + Assert.equal(win.gURLBar.selectionStart, 3); + Assert.equal(win.gURLBar.selectionEnd, 12); + EventUtils.synthesizeKey("VK_RETURN", {}, win); + await promise; + return { + category: "urlbar", + method: "engagement", + object: "enter", + value: "typed", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "3", + numWords: "1", + selIndex: "0", + selType: "autofill_origin", + provider: "Autofill", + }, + }; + }, + + async function (win) { + info("Type something, select bookmark entry, Enter."); + + // Add a clean bookmark and the input history in order to detect in InputHistory + // provider and to not show adaptive history autofill. + const bookmark = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.com/bookmark", + title: "bookmark", + }); + await UrlbarUtils.addToInputHistory( + "http://example.com/bookmark", + "bookmark" + ); + + win.gURLBar.select(); + let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "boo", + fireInputEvent: true, + }); + while (win.gURLBar.untrimmedValue != "http://example.com/bookmark") { + EventUtils.synthesizeKey("KEY_ArrowDown", {}, win); + } + EventUtils.synthesizeKey("VK_RETURN", {}, win); + await promise; + await PlacesUtils.bookmarks.remove(bookmark); + return { + category: "urlbar", + method: "engagement", + object: "enter", + value: "typed", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "3", + numWords: "1", + selIndex: val => parseInt(val) > 0, + selType: "bookmark_adaptive", + provider: "InputHistory", + }, + }; + }, + + async function (win) { + info("Type an input used before, select an input history item, Enter."); + let url = "http://example.com/hello_world"; + let searchString = "hello"; + await PlacesTestUtils.addVisits(url); + await UrlbarUtils.addToInputHistory(url, searchString); + win.gURLBar.select(); + let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: searchString, + fireInputEvent: true, + }); + while (win.gURLBar.untrimmedValue != url) { + EventUtils.synthesizeKey("KEY_ArrowDown", {}, win); + } + EventUtils.synthesizeKey("VK_RETURN", {}, win); + await promise; + return { + category: "urlbar", + method: "engagement", + object: "enter", + value: "typed", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "5", + numWords: "1", + selIndex: val => parseInt(val) > 0, + selType: "history_adaptive", + provider: "InputHistory", + }, + }; + }, + + async function (win) { + info("Type something, select remote search suggestion, Enter."); + win.gURLBar.select(); + let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "foo", + fireInputEvent: true, + }); + while (win.gURLBar.untrimmedValue != "foofoo") { + EventUtils.synthesizeKey("KEY_ArrowDown", {}, win); + } + EventUtils.synthesizeKey("VK_RETURN", {}, win); + await promise; + return { + category: "urlbar", + method: "engagement", + object: "enter", + value: "typed", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "3", + numWords: "1", + selIndex: val => parseInt(val) > 0, + selType: "searchsuggestion", + provider: "SearchSuggestions", + }, + }; + }, + + async function (win) { + info("Type something, select form history, Enter."); + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.maxHistoricalSearchSuggestions", 2]], + }); + await UrlbarTestUtils.formHistory.add(["foofoo", "foobar"]); + win.gURLBar.select(); + let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "foo", + fireInputEvent: true, + }); + while (win.gURLBar.untrimmedValue != "foofoo") { + EventUtils.synthesizeKey("KEY_ArrowDown", {}, win); + } + EventUtils.synthesizeKey("VK_RETURN", {}, win); + await promise; + await SpecialPowers.popPrefEnv(); + return { + category: "urlbar", + method: "engagement", + object: "enter", + value: "typed", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "3", + numWords: "1", + selIndex: val => parseInt(val) > 0, + selType: "formhistory", + provider: "SearchSuggestions", + }, + }; + }, + + async function (win) { + info("Type @, enter on a keywordoffer, then search and press enter."); + win.gURLBar.select(); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "@", + fireInputEvent: true, + }); + + while (win.gURLBar.searchMode?.engineName != TEST_ENGINE_NAME) { + EventUtils.synthesizeKey("KEY_ArrowDown", {}, win); + } + EventUtils.synthesizeKey("VK_RETURN", {}, win); + await UrlbarTestUtils.promiseSearchComplete(win); + + let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "moz", + fireInputEvent: true, + }); + EventUtils.synthesizeKey("VK_RETURN", {}, win); + await promise; + + return [ + // engagement on the keyword offer result to enter search mode + { + category: "urlbar", + method: "engagement", + object: "enter", + value: "typed", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "1", + numWords: "1", + selIndex: "6", + selType: "searchengine", + provider: "TokenAliasEngines", + }, + }, + // engagement on the search heuristic + { + category: "urlbar", + method: "engagement", + object: "enter", + value: "typed", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "3", + numWords: "1", + selIndex: "0", + selType: "searchengine", + provider: "HeuristicFallback", + }, + }, + ]; + }, + + async function (win) { + info("Type an @alias, then space, then search and press enter."); + const alias = "testalias"; + await SearchTestUtils.installSearchExtension({ + name: "AliasTest", + keyword: alias, + }); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: `${alias} `, + }); + + await UrlbarTestUtils.assertSearchMode(win, { + engineName: "AliasTest", + entry: "typed", + }); + + let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "moz", + fireInputEvent: true, + }); + EventUtils.synthesizeKey("VK_RETURN", {}, win); + await promise; + + return { + category: "urlbar", + method: "engagement", + object: "enter", + value: "typed", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "3", + numWords: "1", + selIndex: "0", + selType: "searchengine", + provider: "HeuristicFallback", + }, + }; + }, + + async function (win) { + info("Drop something."); + let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + EventUtils.synthesizeDrop( + win.document.getElementById("back-button"), + win.gURLBar.inputField, + [[{ type: "text/plain", data: "www.example.com" }]], + "copy", + win + ); + await promise; + return { + category: "urlbar", + method: "engagement", + object: "drop_go", + value: "dropped", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "15", + numWords: "1", + selIndex: "-1", + selType: "none", + provider: "", + }, + }; + }, + + async function (win) { + info("Paste and Go something."); + let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + await SimpleTest.promiseClipboardChange("www.example.com", () => { + clipboardHelper.copyString("www.example.com"); + }); + let inputBox = win.gURLBar.querySelector("moz-input-box"); + let cxmenu = inputBox.menupopup; + let cxmenuPromise = BrowserTestUtils.waitForEvent(cxmenu, "popupshown"); + EventUtils.synthesizeMouseAtCenter( + win.gURLBar.inputField, + { + type: "contextmenu", + button: 2, + }, + win + ); + await cxmenuPromise; + let menuitem = inputBox.getMenuItem("paste-and-go"); + cxmenu.activateItem(menuitem); + await promise; + return { + category: "urlbar", + method: "engagement", + object: "paste_go", + value: "pasted", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "15", + numWords: "1", + selIndex: "-1", + selType: "none", + provider: "", + }, + }; + }, + + // The URLs in the down arrow/autoOpen tests must vary from test to test, + // else the first Top Site results will be a switch-to-tab result and a page + // load will not occur. + async function (win) { + info("Open the panel with DOWN, select with DOWN, Enter."); + await addTopSite("http://example.org/"); + win.gURLBar.value = ""; + win.gURLBar.select(); + let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + await UrlbarTestUtils.promisePopupOpen(win, () => { + EventUtils.synthesizeKey("KEY_ArrowDown", {}, win); + }); + await UrlbarTestUtils.promiseSearchComplete(win); + while (win.gURLBar.untrimmedValue != "http://example.org/") { + EventUtils.synthesizeKey("KEY_ArrowDown", {}, win); + } + EventUtils.synthesizeKey("VK_RETURN", {}, win); + await promise; + return { + category: "urlbar", + method: "engagement", + object: "enter", + value: "topsites", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "0", + numWords: "0", + selType: "history", + selIndex: val => parseInt(val) >= 0, + provider: "UrlbarProviderTopSites", + }, + }; + }, + + async function (win) { + info("Open the panel with DOWN, click on entry."); + await addTopSite("http://example.com/"); + win.gURLBar.value = ""; + win.gURLBar.select(); + let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + await UrlbarTestUtils.promisePopupOpen(win, () => { + EventUtils.synthesizeKey("KEY_ArrowDown", {}, win); + }); + while (win.gURLBar.untrimmedValue != "http://example.com/") { + EventUtils.synthesizeKey("KEY_ArrowDown", {}, win); + } + let element = UrlbarTestUtils.getSelectedRow(win); + EventUtils.synthesizeMouseAtCenter(element, {}, win); + await promise; + return { + category: "urlbar", + method: "engagement", + object: "click", + value: "topsites", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "0", + numWords: "0", + selType: "history", + selIndex: "0", + provider: "UrlbarProviderTopSites", + }, + }; + }, + + // The URLs in the autoOpen tests must vary from test to test, else + // the first Top Site results will be a switch-to-tab result and a page load + // will not occur. + async function (win) { + info( + "With pageproxystate=valid, autoopen the panel, select with DOWN, Enter." + ); + await addTopSite("http://example.org/"); + win.gURLBar.value = ""; + let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + await UrlbarTestUtils.promisePopupOpen(win, () => { + win.document.getElementById("Browser:OpenLocation").doCommand(); + }); + await UrlbarTestUtils.promiseSearchComplete(win); + while (win.gURLBar.untrimmedValue != "http://example.org/") { + EventUtils.synthesizeKey("KEY_ArrowDown", {}, win); + } + EventUtils.synthesizeKey("VK_RETURN", {}, win); + await promise; + return { + category: "urlbar", + method: "engagement", + object: "enter", + value: "topsites", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "0", + numWords: "0", + selType: "history", + selIndex: val => parseInt(val) >= 0, + provider: "UrlbarProviderTopSites", + }, + }; + }, + + async function (win) { + info("With pageproxystate=valid, autoopen the panel, click on entry."); + await addTopSite("http://example.com/"); + win.gURLBar.value = ""; + let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + await UrlbarTestUtils.promisePopupOpen(win, () => { + win.document.getElementById("Browser:OpenLocation").doCommand(); + }); + await UrlbarTestUtils.promiseSearchComplete(win); + while (win.gURLBar.untrimmedValue != "http://example.com/") { + EventUtils.synthesizeKey("KEY_ArrowDown", {}, win); + } + let element = UrlbarTestUtils.getSelectedRow(win); + EventUtils.synthesizeMouseAtCenter(element, {}, win); + await promise; + return { + category: "urlbar", + method: "engagement", + object: "click", + value: "topsites", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "0", + numWords: "0", + selType: "history", + selIndex: "0", + provider: "UrlbarProviderTopSites", + }, + }; + }, + + async function (win) { + info("With pageproxystate=invalid, open retained results, Enter."); + await addTopSite("http://example.org/"); + win.gURLBar.value = "example.org"; + win.gURLBar.setPageProxyState("invalid"); + let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + await UrlbarTestUtils.promisePopupOpen(win, () => { + win.document.getElementById("Browser:OpenLocation").doCommand(); + }); + await UrlbarTestUtils.promiseSearchComplete(win); + EventUtils.synthesizeKey("VK_RETURN", {}, win); + await promise; + return { + category: "urlbar", + method: "engagement", + object: "enter", + value: "returned", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "11", + numWords: "1", + selType: "autofill_origin", + selIndex: "0", + provider: "Autofill", + }, + }; + }, + + async function (win) { + info("With pageproxystate=invalid, open retained results, click on entry."); + // This value must be different from the previous test, to avoid reopening + // the view. + win.gURLBar.value = "example.com"; + win.gURLBar.setPageProxyState("invalid"); + let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + await UrlbarTestUtils.promisePopupOpen(win, () => { + win.document.getElementById("Browser:OpenLocation").doCommand(); + }); + await UrlbarTestUtils.promiseSearchComplete(win); + let element = UrlbarTestUtils.getSelectedRow(win); + EventUtils.synthesizeMouseAtCenter(element, {}, win); + await promise; + return { + category: "urlbar", + method: "engagement", + object: "click", + value: "returned", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "11", + numWords: "1", + selType: "autofill_origin", + selIndex: "0", + provider: "Autofill", + }, + }; + }, + + async function (win) { + info("Reopen the view: type, blur, focus, confirm."); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "search", + fireInputEvent: true, + }); + await UrlbarTestUtils.promisePopupClose(win, () => { + win.gURLBar.blur(); + }); + await UrlbarTestUtils.promisePopupOpen(win, () => { + win.document.getElementById("Browser:OpenLocation").doCommand(); + }); + await UrlbarTestUtils.promiseSearchComplete(win); + let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + EventUtils.synthesizeKey("VK_RETURN", {}, win); + await promise; + return [ + { + category: "urlbar", + method: "abandonment", + object: "blur", + value: "typed", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "6", + numWords: "1", + }, + }, + { + category: "urlbar", + method: "engagement", + object: "enter", + value: "returned", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "6", + numWords: "1", + selType: "searchengine", + selIndex: "0", + provider: "HeuristicFallback", + }, + }, + ]; + }, + + async function (win) { + info("Open search mode with a keyboard shortcut."); + // Bug 1797801: If the search mode used is the same as the default engine and + // showSearchTerms is enabled, the chiclet will remain in the urlbar on the search. + // Subsequent tests rely on search mode not already been selected. + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.showSearchTerms.featureGate", false]], + }); + let defaultEngine = await Services.search.getDefault(); + win.gURLBar.select(); + EventUtils.synthesizeKey("k", { accelKey: true }, win); + await UrlbarTestUtils.assertSearchMode(win, { + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + engineName: defaultEngine.name, + entry: "shortcut", + }); + + // Execute a search to finish the engagement. + let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "moz", + fireInputEvent: true, + }); + EventUtils.synthesizeKey("VK_RETURN", {}, win); + await promise; + + await SpecialPowers.popPrefEnv(); + + return { + category: "urlbar", + method: "engagement", + object: "enter", + value: "typed", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "3", + numWords: "1", + selIndex: "0", + selType: "searchengine", + provider: "HeuristicFallback", + }, + }; + }, + + async function (win) { + info("Open search mode from a tab-to-search result."); + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.tabToSearch.onboard.interactionsLeft", 0]], + }); + + await PlacesUtils.history.clear(); + for (let i = 0; i < 3; i++) { + await PlacesTestUtils.addVisits([`https://${TEST_ENGINE_DOMAIN}/`]); + } + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: TEST_ENGINE_DOMAIN.slice(0, 4), + }); + + let tabToSearchResult = ( + await UrlbarTestUtils.waitForAutocompleteResultAt(win, 1) + ).result; + Assert.equal( + tabToSearchResult.providerName, + "TabToSearch", + "The second result is a tab-to-search result." + ); + + // Select the tab-to-search result. + EventUtils.synthesizeKey("KEY_ArrowDown", {}, win); + let searchPromise = UrlbarTestUtils.promiseSearchComplete(win); + EventUtils.synthesizeKey("KEY_Enter", {}, win); + await searchPromise; + await UrlbarTestUtils.assertSearchMode(win, { + engineName: TEST_ENGINE_NAME, + entry: "tabtosearch", + }); + + // Execute a search to finish the engagement. + let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "moz", + fireInputEvent: true, + }); + EventUtils.synthesizeKey("VK_RETURN", {}, win); + await promise; + + await PlacesUtils.history.clear(); + await SpecialPowers.popPrefEnv(); + + return [ + // engagement on the tab-to-search to enter search mode + { + category: "urlbar", + method: "engagement", + object: "enter", + value: "typed", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "4", + numWords: "1", + selIndex: "1", + selType: "tabtosearch", + provider: "TabToSearch", + }, + }, + // engagement on the search heuristic + { + category: "urlbar", + method: "engagement", + object: "enter", + value: "typed", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "3", + numWords: "1", + selIndex: "0", + selType: "searchengine", + provider: "HeuristicFallback", + }, + }, + ]; + }, + + async function (win) { + info("Sanity check we are not stuck on 'returned'"); + win.gURLBar.select(); + let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "x", + fireInputEvent: true, + }); + EventUtils.synthesizeKey("VK_RETURN", {}, win); + await promise; + return { + category: "urlbar", + method: "engagement", + object: "enter", + value: "typed", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "1", + numWords: "1", + selIndex: "0", + selType: "searchengine", + provider: "HeuristicFallback", + }, + }; + }, + + async function (win) { + info("Reopen the view: type, blur, focus, backspace, type, confirm."); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "search", + fireInputEvent: true, + }); + await UrlbarTestUtils.promisePopupClose(win, () => { + win.gURLBar.blur(); + }); + await UrlbarTestUtils.promisePopupOpen(win, () => { + win.document.getElementById("Browser:OpenLocation").doCommand(); + }); + EventUtils.synthesizeKey("VK_RIGHT", {}, win); + EventUtils.synthesizeKey("VK_BACK_SPACE", {}, win); + EventUtils.synthesizeKey("x", {}, win); + await UrlbarTestUtils.promiseSearchComplete(win); + let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + EventUtils.synthesizeKey("VK_RETURN", {}, win); + await promise; + return [ + { + category: "urlbar", + method: "abandonment", + object: "blur", + value: "typed", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "6", + numWords: "1", + }, + }, + { + category: "urlbar", + method: "engagement", + object: "enter", + value: "returned", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "6", + numWords: "1", + selType: "searchengine", + selIndex: "0", + provider: "HeuristicFallback", + }, + }, + ]; + }, + + async function (win) { + info("Reopen the view: type, blur, focus, type (overwrite), confirm."); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "search", + fireInputEvent: true, + }); + await UrlbarTestUtils.promisePopupClose(win, () => { + win.gURLBar.blur(); + }); + await UrlbarTestUtils.promisePopupOpen(win, () => { + win.document.getElementById("Browser:OpenLocation").doCommand(); + }); + EventUtils.synthesizeKey("x", {}, win); + await UrlbarTestUtils.promiseSearchComplete(win); + let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + EventUtils.synthesizeKey("VK_RETURN", {}, win); + await promise; + return [ + { + category: "urlbar", + method: "abandonment", + object: "blur", + value: "typed", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "6", + numWords: "1", + }, + }, + { + category: "urlbar", + method: "engagement", + object: "enter", + value: "restarted", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "1", + numWords: "1", + selType: "searchengine", + selIndex: "0", + provider: "HeuristicFallback", + }, + }, + ]; + }, + + async function (win) { + info("Sanity check we are not stuck on 'restarted'"); + win.gURLBar.select(); + let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "x", + fireInputEvent: true, + }); + EventUtils.synthesizeKey("VK_RETURN", {}, win); + await promise; + return { + category: "urlbar", + method: "engagement", + object: "enter", + value: "typed", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "1", + numWords: "1", + selIndex: "0", + selType: "searchengine", + provider: "HeuristicFallback", + }, + }; + }, +]; + +add_setup(async function () { + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + + // Create a new search engine and mark it as default + let engine = await SearchTestUtils.promiseNewSearchEngine({ + url: getRootDirectory(gTestPath) + "searchSuggestionEngine.xml", + setAsDefault: true, + }); + await Services.search.moveEngine(engine, 0); + + await SearchTestUtils.installSearchExtension({ + name: TEST_ENGINE_NAME, + keyword: TEST_ENGINE_ALIAS, + search_url: `https://${TEST_ENGINE_DOMAIN}/`, + }); + + // Add a bookmark and a keyword. + let bm = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.com/?q=%s", + title: "test", + }); + await PlacesUtils.keywords.insert({ + keyword: "kw", + url: "http://example.com/?q=%s", + }); + + registerCleanupFunction(async function () { + await PlacesUtils.keywords.remove("kw"); + await PlacesUtils.bookmarks.remove(bm); + await PlacesUtils.history.clear(); + }); +}); + +async function doTest(eventTelemetryEnabled) { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.eventTelemetry.enabled", eventTelemetryEnabled], + ["browser.urlbar.suggest.searches", true], + ], + }); + + const win = await BrowserTestUtils.openNewBrowserWindow(); + + // This is not necessary after each loop, because assertEvents does it. + Services.telemetry.clearEvents(); + Services.telemetry.clearScalars(); + + for (let i = 0; i < tests.length; i++) { + info(`Running test at index ${i}`); + let events = await tests[i](win); + if (events === null) { + info("Skipping test"); + continue; + } + if (!Array.isArray(events)) { + events = [events]; + } + // Always blur to ensure it's not accounted as an additional abandonment. + win.gURLBar.setSearchMode({}); + win.gURLBar.blur(); + TelemetryTestUtils.assertEvents(eventTelemetryEnabled ? events : [], { + category: "urlbar", + }); + + // Scalars should be recorded regardless of `eventTelemetry.enabled`. + let scalars = TelemetryTestUtils.getProcessScalars("parent", false, true); + TelemetryTestUtils.assertScalar( + scalars, + "urlbar.engagement", + events.filter(e => e.method == "engagement").length || undefined + ); + TelemetryTestUtils.assertScalar( + scalars, + "urlbar.abandonment", + events.filter(e => e.method == "abandonment").length || undefined + ); + + await UrlbarTestUtils.formHistory.clear(win); + } + + await BrowserTestUtils.closeWindow(win); + await SpecialPowers.popPrefEnv(); +} + +add_task(async function enabled() { + await doTest(true); +}); + +add_task(async function disabled() { + await doTest(false); +}); + +/** + * Replaces the contents of Top Sites with the specified site. + * + * @param {string} site + * A site to add to Top Sites. + */ +async function addTopSite(site) { + await PlacesUtils.history.clear(); + for (let i = 0; i < 5; i++) { + await PlacesTestUtils.addVisits(site); + } + + await updateTopSites(sites => sites && sites[0] && sites[0].url == site); +} + +function registerTipProvider() { + let provider = new UrlbarTestUtils.TestProvider({ + results: tipMatches, + priority: 1, + }); + UrlbarProvidersManager.registerProvider(provider); + return provider; +} + +function unregisterTipProvider(provider) { + UrlbarProvidersManager.unregisterProvider(provider); +} + +let tipMatches = [ + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: "http://mozilla.org/a" } + ), + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.TIP, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { + helpUrl: "http://example.com/", + helpL10n: { + id: UrlbarPrefs.get("resultMenu") + ? "urlbar-result-menu-tip-get-help" + : "urlbar-tip-help-icon", + }, + type: "test", + titleL10n: { id: "urlbar-search-tips-confirm" }, + buttons: [ + { + url: "http://example.com/", + l10n: { id: "urlbar-search-tips-confirm" }, + }, + ], + } + ), + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: "http://mozilla.org/b" } + ), + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: "http://mozilla.org/c" } + ), +]; diff --git a/browser/components/urlbar/tests/browser/browser_urlbar_event_telemetry_noEvent.js b/browser/components/urlbar/tests/browser/browser_urlbar_event_telemetry_noEvent.js new file mode 100644 index 000000000000..bdba6888b73b --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_urlbar_event_telemetry_noEvent.js @@ -0,0 +1,81 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const tests = [ + async function (win) { + info("Type something, click on search settings."); + await BrowserTestUtils.withNewTab( + { gBrowser: win.gBrowser, url: "about:blank" }, + async browser => { + win.gURLBar.select(); + const promise = onSyncPaneLoaded(); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "x", + fireInputEvent: true, + }); + UrlbarTestUtils.getOneOffSearchButtons(win).settingsButton.click(); + await promise; + } + ); + return null; + }, + + async function (win) { + info("Type something, Up, Enter on search settings."); + await BrowserTestUtils.withNewTab( + { gBrowser: win.gBrowser, url: "about:blank" }, + async browser => { + win.gURLBar.select(); + const promise = onSyncPaneLoaded(); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "x", + fireInputEvent: true, + }); + EventUtils.synthesizeKey("KEY_ArrowUp", {}, win); + Assert.ok( + UrlbarTestUtils.getOneOffSearchButtons( + win + ).selectedButton.classList.contains("search-setting-button"), + "Should have selected the settings button" + ); + EventUtils.synthesizeKey("VK_RETURN", {}, win); + await promise; + } + ); + return null; + }, +]; + +function onSyncPaneLoaded() { + return new Promise(resolve => { + Services.obs.addObserver(function panesLoadedObs() { + Services.obs.removeObserver(panesLoadedObs, "sync-pane-loaded"); + resolve(); + }, "sync-pane-loaded"); + }); +} + +add_task(async function test() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.eventTelemetry.enabled", true]], + }); + + const win = await BrowserTestUtils.openNewBrowserWindow(); + + // This is not necessary after each loop, because assertEvents does it. + Services.telemetry.clearEvents(); + + for (let i = 0; i < tests.length; i++) { + info(`Running no event test at index ${i}`); + await tests[i](win); + // Always blur to ensure it's not accounted as an additional abandonment. + win.gURLBar.blur(); + TelemetryTestUtils.assertEvents([], { category: "urlbar" }); + } + + await BrowserTestUtils.closeWindow(win); +}); diff --git a/browser/components/urlbar/tests/ext/api.js b/browser/components/urlbar/tests/ext/api.js index 235c0a4331c6..a48815177ae8 100644 --- a/browser/components/urlbar/tests/ext/api.js +++ b/browser/components/urlbar/tests/ext/api.js @@ -10,12 +10,6 @@ const { XPCOMUtils } = ChromeUtils.importESModule( "resource://gre/modules/XPCOMUtils.sys.mjs" ); -const { ExtensionPreferenceManager } = ChromeUtils.importESModule( - "resource://gre/modules/ExtensionPreferencesManager.sys.mjs" -); - -var { getSettingsAPI } = ExtensionPreferenceManager; - ChromeUtils.defineESModuleGetters(this, { BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs", Preferences: "resource://gre/modules/Preferences.sys.mjs", @@ -56,12 +50,9 @@ this.experiments_urlbar = class extends ExtensionAPI { window.gURLBar.setPageProxyState("invalid"); }, - engagementTelemetry: getSettingsAPI({ - context, - name: "engagementTelemetry", - readOnly: true, - callback: () => false, - }), + engagementTelemetry: this._getDefaultSettingsAPI( + "browser.urlbar.eventTelemetry.enabled" + ), extensionTimeout: this._getDefaultSettingsAPI( "browser.urlbar.extension.timeout" diff --git a/browser/components/urlbar/tests/ext/browser/browser.toml b/browser/components/urlbar/tests/ext/browser/browser.toml index 874616e6794b..5e2b6df3d5a7 100644 --- a/browser/components/urlbar/tests/ext/browser/browser.toml +++ b/browser/components/urlbar/tests/ext/browser/browser.toml @@ -18,4 +18,6 @@ prefs = ["browser.bookmarks.testing.skipDefaultBookmarksImport=true"] ["browser_ext_urlbar_dynamicResult.js"] support-files = ["dynamicResult.css"] +["browser_ext_urlbar_engagementTelemetry.js"] + ["browser_ext_urlbar_extensionTimeout.js"] diff --git a/browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_engagementTelemetry.js b/browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_engagementTelemetry.js new file mode 100644 index 000000000000..50ded14d4e01 --- /dev/null +++ b/browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_engagementTelemetry.js @@ -0,0 +1,18 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* global browser */ + +// This tests the browser.experiments.urlbar.engagementTelemetry WebExtension +// Experiment API. + +"use strict"; + +add_settings_tasks("browser.urlbar.eventTelemetry.enabled", "boolean", () => { + browser.test.onMessage.addListener(async (method, arg) => { + let result = await browser.experiments.urlbar.engagementTelemetry[method]( + arg + ); + browser.test.sendMessage("done", result); + }); +}); diff --git a/toolkit/components/telemetry/Events.yaml b/toolkit/components/telemetry/Events.yaml index cc4ab197036e..34bc93fe3ce0 100644 --- a/toolkit/components/telemetry/Events.yaml +++ b/toolkit/components/telemetry/Events.yaml @@ -756,6 +756,56 @@ aboutpreferences: - "gijs@mozilla.com" expiry_version: never +urlbar: + engagement: + objects: ["click", "enter", "paste_go", "drop_go"] + release_channel_collection: opt-out + products: + - "firefox" + record_in_processes: ["main"] + description: > + This is recorded on urlbar engagement, that is when the user picks a + search result. + The value field records the initial interaction type. One of: + "typed", "dropped", "pasted", "topsites" + bug_numbers: [1559136, 1671404, 1466103] + notification_emails: + - "fx-search-telemetry@mozilla.com" + expiry_version: never + extra_keys: + elapsed: engagement time in milliseconds. + numChars: number of input characters. + numWords: number of words in the input. + selIndex: index of the selected result in the urlbar panel, or -1. + selType: > + type of the selected result in the urlbar panel. One of: + "autofill", "visit", "bookmark", "bookmark_adaptive", "history", + "history_adaptive", "keyword", "search", "searchsuggestion", + "searchsuggestion_rich", "switchtab", "remotetab", "extension", + "oneoff", "keywordoffer", "canonized", "tip", "tiphelp", + "formhistory", "tabtosearch", "quicksuggest", "weather", + "clipboard", "dynamic-wikipedia", "navigational", "none" + provider: The name of the provider that presented the result. + abandonment: + objects: ["blur"] + release_channel_collection: opt-out + products: + - "firefox" + record_in_processes: ["main"] + description: > + This is recorded on urlbar search abandon, that is when the user starts + an interaction but then blurs the urlbar. + The value field records the initial interaction type. One of: + "typed", "dropped", "pasted", "topsites" + bug_numbers: [1559136] + notification_emails: + - "fx-search-telemetry@mozilla.com" + expiry_version: never + extra_keys: + elapsed: abandonement time in milliseconds. + numChars: number of input characters. + numWords: number of words in the input. + normandy: enroll: objects: ["preference_study", "addon_study", "preference_rollout", "addon_rollout", "nimbus_experiment"] -- 2.11.4.GIT