summaryrefslogtreecommitdiffstats
path: root/browser/components/search/test/browser/browser_search_telemetry_sources.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/search/test/browser/browser_search_telemetry_sources.js')
-rw-r--r--browser/components/search/test/browser/browser_search_telemetry_sources.js497
1 files changed, 497 insertions, 0 deletions
diff --git a/browser/components/search/test/browser/browser_search_telemetry_sources.js b/browser/components/search/test/browser/browser_search_telemetry_sources.js
new file mode 100644
index 0000000000..173bc09694
--- /dev/null
+++ b/browser/components/search/test/browser/browser_search_telemetry_sources.js
@@ -0,0 +1,497 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Main tests for SearchSERPTelemetry - general engine visiting and link clicking.
+ *
+ * NOTE: As this test file is already fairly long-running, adding to this file
+ * will likely cause timeout errors with test-verify jobs on Treeherder.
+ * Therefore, please do not add further tasks to this file.
+ */
+
+"use strict";
+
+const { SearchSERPTelemetry, SearchSERPTelemetryUtils } =
+ ChromeUtils.importESModule("resource:///modules/SearchSERPTelemetry.sys.mjs");
+
+const TEST_PROVIDER_INFO = [
+ {
+ telemetryId: "example",
+ searchPageRegexp:
+ /^https:\/\/example.org\/browser\/browser\/components\/search\/test\/browser\/searchTelemetry(?:Ad)?/,
+ queryParamName: "s",
+ codeParamName: "abc",
+ taggedCodes: ["ff"],
+ followOnParamNames: ["a"],
+ extraAdServersRegexps: [/^https:\/\/example\.com\/ad2?/],
+ components: [
+ {
+ type: SearchSERPTelemetryUtils.COMPONENTS.AD_LINK,
+ default: true,
+ },
+ ],
+ },
+];
+
+function getPageUrl(useAdPage = false) {
+ let page = useAdPage ? "searchTelemetryAd.html" : "searchTelemetry.html";
+ return `https://example.org/browser/browser/components/search/test/browser/${page}`;
+}
+
+/**
+ * Returns the index of the first search suggestion in the urlbar results.
+ *
+ * @returns {number} An index, or -1 if there are no search suggestions.
+ */
+async function getFirstSuggestionIndex() {
+ const matchCount = UrlbarTestUtils.getResultCount(window);
+ for (let i = 0; i < matchCount; i++) {
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ if (
+ result.type == UrlbarUtils.RESULT_TYPE.SEARCH &&
+ result.searchParams.suggestion
+ ) {
+ return i;
+ }
+ }
+ return -1;
+}
+
+// sharedData messages are only passed to the child on idle. Therefore
+// we wait for a few idles to try and ensure the messages have been able
+// to be passed across and handled.
+async function waitForIdle() {
+ for (let i = 0; i < 10; i++) {
+ await new Promise(resolve => Services.tm.idleDispatchToMainThread(resolve));
+ }
+}
+
+SearchTestUtils.init(this);
+
+add_setup(async function () {
+ SearchSERPTelemetry.overrideSearchTelemetryForTests(TEST_PROVIDER_INFO);
+ await waitForIdle();
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.suggest.searches", true],
+ [
+ "browser.newtabpage.activity-stream.improvesearch.handoffToAwesomebar",
+ true,
+ ],
+ // Ensure to add search suggestion telemetry as search_suggestion not search_formhistory.
+ ["browser.urlbar.maxHistoricalSearchSuggestions", 0],
+ ["browser.search.serpEventTelemetry.enabled", true],
+ ],
+ });
+ // Enable local telemetry recording for the duration of the tests.
+ let oldCanRecord = Services.telemetry.canRecordExtended;
+ Services.telemetry.canRecordExtended = true;
+ Services.prefs.setBoolPref("browser.search.log", true);
+
+ await SearchTestUtils.installSearchExtension(
+ {
+ search_url: getPageUrl(true),
+ search_url_get_params: "s={searchTerms}&abc=ff",
+ suggest_url:
+ "https://example.org/browser/browser/components/search/test/browser/searchSuggestionEngine.sjs",
+ suggest_url_get_params: "query={searchTerms}",
+ },
+ { setAsDefault: true }
+ );
+
+ await gCUITestUtils.addSearchBar();
+
+ registerCleanupFunction(async () => {
+ gCUITestUtils.removeSearchBar();
+ Services.prefs.clearUserPref("browser.search.log");
+ SearchSERPTelemetry.overrideSearchTelemetryForTests();
+ Services.telemetry.canRecordExtended = oldCanRecord;
+ resetTelemetry();
+ });
+});
+
+async function track_ad_click(
+ expectedHistogramSource,
+ expectedScalarSource,
+ searchAdsFn,
+ cleanupFn
+) {
+ searchCounts.clear();
+ Services.telemetry.clearScalars();
+
+ let expectedContentScalarKey = "example:tagged:ff";
+ let expectedScalarKey = "example:tagged";
+ let expectedHistogramSAPSourceKey = `other-Example.${expectedHistogramSource}`;
+ let expectedContentScalar = `browser.search.content.${expectedScalarSource}`;
+ let expectedWithAdsScalar = `browser.search.withads.${expectedScalarSource}`;
+ let expectedAdClicksScalar = `browser.search.adclicks.${expectedScalarSource}`;
+
+ let tab = await searchAdsFn();
+
+ await assertSearchSourcesTelemetry(
+ {
+ [expectedHistogramSAPSourceKey]: 1,
+ },
+ {
+ [expectedContentScalar]: { [expectedContentScalarKey]: 1 },
+ [expectedWithAdsScalar]: { [expectedScalarKey]: 1 },
+ }
+ );
+
+ assertImpressionEvents([
+ {
+ impression: {
+ provider: "example",
+ tagged: "true",
+ partner_code: "ff",
+ source: expectedScalarSource,
+ is_shopping_page: "false",
+ shopping_tab_displayed: "false",
+ },
+ },
+ ]);
+ // Ad impression data is needed to categorize ads on the page in order to
+ // register ad click events before a click occurs. We don't assert their
+ // precise values here because other tests cover that the component
+ // categorizations are valid.
+ await promiseAdImpressionReceived();
+
+ let pageLoadPromise = BrowserTestUtils.waitForLocationChange(gBrowser);
+ BrowserTestUtils.synthesizeMouseAtCenter("#ad1", {}, tab.linkedBrowser);
+ await pageLoadPromise;
+ await promiseWaitForAdLinkCheck();
+
+ await assertSearchSourcesTelemetry(
+ {
+ [expectedHistogramSAPSourceKey]: 1,
+ },
+ {
+ [expectedContentScalar]: { [expectedContentScalarKey]: 1 },
+ [expectedWithAdsScalar]: { [expectedScalarKey]: 1 },
+ [expectedAdClicksScalar]: { [expectedScalarKey]: 1 },
+ }
+ );
+
+ assertImpressionEvents([
+ {
+ impression: {
+ provider: "example",
+ tagged: "true",
+ partner_code: "ff",
+ source: expectedScalarSource,
+ is_shopping_page: "false",
+ shopping_tab_displayed: "false",
+ },
+ engagements: [
+ {
+ action: SearchSERPTelemetryUtils.ACTIONS.CLICKED,
+ target: SearchSERPTelemetryUtils.COMPONENTS.AD_LINK,
+ },
+ ],
+ },
+ ]);
+
+ await cleanupFn();
+
+ Services.fog.testResetFOG();
+}
+
+add_task(async function test_source_urlbar() {
+ let tab;
+ await track_ad_click(
+ "urlbar",
+ "urlbar",
+ async () => {
+ tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "searchSuggestion",
+ });
+ let idx = await getFirstSuggestionIndex();
+ Assert.greaterOrEqual(idx, 0, "there should be a first suggestion");
+ let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ while (idx--) {
+ EventUtils.sendKey("down");
+ }
+ EventUtils.sendKey("return");
+ await loadPromise;
+ return tab;
+ },
+ async () => {
+ BrowserTestUtils.removeTab(tab);
+ }
+ );
+});
+
+add_task(async function test_source_urlbar_handoff() {
+ let tab;
+ await track_ad_click(
+ "urlbar-handoff",
+ "urlbar_handoff",
+ async () => {
+ Services.fog.testResetFOG();
+ tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ BrowserTestUtils.loadURIString(tab.linkedBrowser, "about:newtab");
+ await BrowserTestUtils.browserStopped(tab.linkedBrowser, "about:newtab");
+
+ info("Focus on search input in newtab content");
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ ".fake-editable",
+ {},
+ tab.linkedBrowser
+ );
+
+ info("Get suggestions");
+ for (const c of "searchSuggestion".split("")) {
+ EventUtils.synthesizeKey(c);
+ /* eslint-disable mozilla/no-arbitrary-setTimeout */
+ await new Promise(r => setTimeout(r, 50));
+ }
+ await TestUtils.waitForCondition(async () => {
+ const index = await getFirstSuggestionIndex();
+ return index >= 0;
+ }, "Wait until suggestions are ready");
+
+ let idx = await getFirstSuggestionIndex();
+ Assert.greaterOrEqual(idx, 0, "there should be a first suggestion");
+ const onLoaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ while (idx--) {
+ EventUtils.sendKey("down");
+ }
+ EventUtils.sendKey("return");
+ await onLoaded;
+
+ return tab;
+ },
+ async () => {
+ const issueRecords = Glean.newtabSearch.issued.testGetValue();
+ Assert.ok(!!issueRecords, "Must have recorded a search issuance");
+ Assert.equal(issueRecords.length, 1, "One search, one event");
+ const newtabVisitId = issueRecords[0].extra.newtab_visit_id;
+ Assert.ok(!!newtabVisitId, "Must have a visit id");
+ Assert.deepEqual(
+ {
+ // Yes, this is tautological. But I want to use deepEqual.
+ newtab_visit_id: newtabVisitId,
+ search_access_point: "urlbar_handoff",
+ telemetry_id: "other-Example",
+ },
+ issueRecords[0].extra,
+ "Must have recorded the expected information."
+ );
+ const impRecords = Glean.newtabSearchAd.impression.testGetValue();
+ Assert.equal(impRecords.length, 1, "One impression, one event.");
+ Assert.deepEqual(
+ {
+ newtab_visit_id: newtabVisitId,
+ search_access_point: "urlbar_handoff",
+ telemetry_id: "example",
+ is_tagged: "true",
+ is_follow_on: "false",
+ },
+ impRecords[0].extra,
+ "Must have recorded the expected information."
+ );
+ const clickRecords = Glean.newtabSearchAd.click.testGetValue();
+ Assert.equal(clickRecords.length, 1, "One click, one event.");
+ Assert.deepEqual(
+ {
+ newtab_visit_id: newtabVisitId,
+ search_access_point: "urlbar_handoff",
+ telemetry_id: "example",
+ is_tagged: "true",
+ is_follow_on: "false",
+ },
+ clickRecords[0].extra,
+ "Must have recorded the expected information."
+ );
+ BrowserTestUtils.removeTab(tab);
+ }
+ );
+});
+
+add_task(async function test_source_searchbar() {
+ let tab;
+ await track_ad_click(
+ "searchbar",
+ "searchbar",
+ async () => {
+ tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ let sb = BrowserSearch.searchBar;
+ // Write the search query in the searchbar.
+ sb.focus();
+ sb.value = "searchSuggestion";
+ sb.textbox.controller.startSearch("searchSuggestion");
+ // Wait for the popup to show.
+ await BrowserTestUtils.waitForEvent(sb.textbox.popup, "popupshown");
+ // And then for the search to complete.
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ sb.textbox.controller.searchStatus >=
+ Ci.nsIAutoCompleteController.STATUS_COMPLETE_NO_MATCH,
+ "The search in the searchbar must complete."
+ );
+
+ let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ EventUtils.synthesizeKey("KEY_Enter");
+ await loadPromise;
+ return tab;
+ },
+ async () => {
+ BrowserTestUtils.removeTab(tab);
+ }
+ );
+});
+
+async function checkAboutPage(
+ page,
+ expectedHistogramSource,
+ expectedScalarSource
+) {
+ let tab;
+ await track_ad_click(
+ expectedHistogramSource,
+ expectedScalarSource,
+ async () => {
+ tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ BrowserTestUtils.loadURIString(tab.linkedBrowser, page);
+ await BrowserTestUtils.browserStopped(tab.linkedBrowser, page);
+
+ // Wait for the full load.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "browser.newtabpage.activity-stream.improvesearch.handoffToAwesomebar",
+ false,
+ ],
+ ],
+ });
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ await ContentTaskUtils.waitForCondition(
+ () => content.wrappedJSObject.gContentSearchController.defaultEngine
+ );
+ });
+
+ let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await typeInSearchField(
+ tab.linkedBrowser,
+ "test query",
+ "newtab-search-text"
+ );
+ await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, tab.linkedBrowser);
+ await p;
+ return tab;
+ },
+ async () => {
+ BrowserTestUtils.removeTab(tab);
+ await SpecialPowers.popPrefEnv();
+ }
+ );
+}
+
+add_task(async function test_source_about_home() {
+ await checkAboutPage("about:home", "abouthome", "about_home");
+});
+
+add_task(async function test_source_about_newtab() {
+ await checkAboutPage("about:newtab", "newtab", "about_newtab");
+});
+
+add_task(async function test_source_system() {
+ let tab;
+ await track_ad_click(
+ "system",
+ "system",
+ async () => {
+ tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+ // This is not quite the same as calling from the commandline, but close
+ // enough for this test.
+ BrowserSearch.loadSearchFromCommandLine(
+ "searchSuggestion",
+ false,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ gBrowser.selectedBrowser.csp
+ );
+
+ await loadPromise;
+ return tab;
+ },
+ async () => {
+ BrowserTestUtils.removeTab(tab);
+ }
+ );
+});
+
+add_task(async function test_source_webextension_search() {
+ /* global browser */
+ async function background(SEARCH_TERM) {
+ // Search with no tabId
+ browser.search.search({ query: "searchSuggestion", engine: "Example" });
+ }
+
+ let searchExtension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["search", "tabs"],
+ },
+ background,
+ useAddonManager: "temporary",
+ });
+
+ let tab;
+ await track_ad_click(
+ "webextension",
+ "webextension",
+ async () => {
+ let tabPromise = BrowserTestUtils.waitForNewTab(gBrowser, null, true);
+
+ await searchExtension.startup();
+
+ return (tab = await tabPromise);
+ },
+ async () => {
+ await searchExtension.unload();
+ BrowserTestUtils.removeTab(tab);
+ }
+ );
+});
+
+add_task(async function test_source_webextension_query() {
+ async function background(SEARCH_TERM) {
+ // Search with no tabId
+ browser.search.query({
+ text: "searchSuggestion",
+ disposition: "NEW_TAB",
+ });
+ }
+
+ let searchExtension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["search", "tabs"],
+ },
+ background,
+ useAddonManager: "temporary",
+ });
+
+ let tab;
+ await track_ad_click(
+ "webextension",
+ "webextension",
+ async () => {
+ let tabPromise = BrowserTestUtils.waitForNewTab(gBrowser, null, true);
+
+ await searchExtension.startup();
+
+ return (tab = await tabPromise);
+ },
+ async () => {
+ await searchExtension.unload();
+ BrowserTestUtils.removeTab(tab);
+ }
+ );
+});