diff --git a/browser/actors/SearchSERPTelemetryChild.sys.mjs b/browser/actors/SearchSERPTelemetryChild.sys.mjs index da756f3a1f97..f572507dc9a2 100644 --- a/browser/actors/SearchSERPTelemetryChild.sys.mjs +++ b/browser/actors/SearchSERPTelemetryChild.sys.mjs @@ -67,9 +67,6 @@ class SearchProviders { extraAdServersRegexps: p.extraAdServersRegexps.map( r => new RegExp(r) ), - nonAdsLinkRegexps: p.nonAdsLinkRegexps?.length - ? p.nonAdsLinkRegexps.map(r => new RegExp(r)) - : [], }; }); @@ -388,13 +385,6 @@ class SearchAdImpression { if (result.relatedElements?.length) { this.#addEventListenerToElements(result.relatedElements, result.type); } - // If an anchor doesn't match any component, and it doesn't have a non - // ads link regexp, cache the anchor so the parent process can observe - // them. - } else if (!this.#providerInfo.nonAdsLinkRegexps.length) { - this.#recordElementData(anchor, { - type: "non_ads_link", - }); } } } diff --git a/browser/components/search/SearchSERPTelemetry.sys.mjs b/browser/components/search/SearchSERPTelemetry.sys.mjs index ab6c192e0cf2..a2f690afaa43 100644 --- a/browser/components/search/SearchSERPTelemetry.sys.mjs +++ b/browser/components/search/SearchSERPTelemetry.sys.mjs @@ -925,9 +925,16 @@ class ContentHandler { } let wrappedChannel = ChannelWrapper.get(channel); + // The channel we're observing might be a redirect of a channel we've + // observed before. if (wrappedChannel._adClickRecorded) { lazy.logConsole.debug("Ad click already recorded"); return; + // When _adClickRecorded is false but _recordedClick is true, it means we + // recorded a non-ad link click, and it is being re-directed. + } else if (wrappedChannel._recordedClick) { + lazy.logConsole.debug("Non ad-click already recorded"); + return; } Services.tm.dispatchToMainThread(() => { @@ -953,15 +960,35 @@ class ContentHandler { return provider.telemetryId == providerInfo; }); - // The SERP "clicked" action is implied if a user loads another page from - // the context of a SERP. At this point, we don't know if the request is - // from a SERP but we want to avoid inspecting requests that are not - // documents, or not a top level load. + // Some channels re-direct by loading pages that return 200. The result + // is the channel will have an originURL that changes from the SERP to + // either a nonAdsRegexp or an extraAdServersRegexps. This is typical + // for loading a page in a new tab. The channel will have changed so any + // properties attached to them to record state (e.g. _recordedClick) + // won't be present. + if ( + info.nonAdsLinkRegexps.some(r => r.test(originURL)) || + info.extraAdServersRegexps.some(r => r.test(originURL)) + ) { + return; + } + + // A click event is recorded if a user loads a resource from an + // originURL that is a SERP. + // + // Typically, we only want top level loads containing documents to avoid + // recording any event on an in-page resource a SERP might load + // (e.g. CSS files). + // + // The exception to this is if a subframe loads a resource that matches + // a non ad link. Some SERPs encode non ad search results with a URL + // that gets loaded into an iframe, which then tells the container of + // the iframe to change the location of the page. if ( lazy.serpEventsEnabled && channel.isDocument && - channel.loadInfo.isTopLevelLoad && - !wrappedChannel._countedClick + (channel.loadInfo.isTopLevelLoad || + info.nonAdsLinkRegexps.some(r => r.test(URL))) ) { let start = Cu.now(); @@ -1018,27 +1045,12 @@ class ContentHandler { } } } - // Check if the href matches a non-ads link. Do this after looking at - // hrefToComponentMap because a link that looks like a non-ad might - // have a more specific component type. + + // Default value for URLs that don't match any components categorized + // on the page. if (!type) { - type = info.nonAdsLinkRegexps.some(r => r.test(URL)) - ? SearchSERPTelemetryUtils.COMPONENTS.NON_ADS_LINK - : ""; - } - // The SERP may have moved onto another page that matches a SERP page - // e.g. Related Search - if (!type && isSerp) { type = SearchSERPTelemetryUtils.COMPONENTS.NON_ADS_LINK; } - // There might be other types of pages on a SERP that don't fall - // neatly into expected non-ad expressions or SERPs, such as Image - // Search, Maps, etc. - if (!type) { - type = info.extraPageRegexps?.some(r => r.test(URL)) - ? SearchSERPTelemetryUtils.COMPONENTS.NON_ADS_LINK - : ""; - } if (isSerp && isFromNewtab) { SearchSERPTelemetry.setBrowserContentSource( @@ -1047,35 +1059,22 @@ class ContentHandler { ); } - // Step 3: If we have a type, record an engagement. - // Exceptions: - // - Related searches on some SERPs can be encoded with a URL that - // match a nonAdsLinkRegexp. This means we'll have seen the link - // twice, once with the nonAdsLinkRegexp and again with a SERP URL - // matching a searchPageRegexp. We don't want to record the - // engagement twice, so if the origin of the request was - // nonAdsLinkRegexp, skip the categorization. The reason why we - // don't do this check earlier is because if the final URL is a - // SERP, we'll want to define the source property of the subsequent - // SERP impression. - if (type && !info.nonAdsLinkRegexps.some(r => r.test(originURL))) { - impressionIdsWithoutEngagementsSet.delete( - telemetryState.impressionId - ); - Glean.serp.engagement.record({ - impression_id: telemetryState.impressionId, - action: SearchSERPTelemetryUtils.ACTIONS.CLICKED, - target: type, - }); - lazy.logConsole.debug("Counting click:", { - impressionId: telemetryState.impressionId, - type, - URL, - }); - wrappedChannel._countedClick = true; - } else if (!type) { - lazy.logConsole.warn(`Could not find a component type for ${URL}`); - } + // Step 3: Record the engagement. + impressionIdsWithoutEngagementsSet.delete( + telemetryState.impressionId + ); + Glean.serp.engagement.record({ + impression_id: telemetryState.impressionId, + action: SearchSERPTelemetryUtils.ACTIONS.CLICKED, + target: type, + }); + lazy.logConsole.debug("Counting click:", { + impressionId: telemetryState.impressionId, + type, + URL, + }); + // Prevent re-directed channels from being examined more than once. + wrappedChannel._recordedClick = true; } ChromeUtils.addProfilerMarker( "SearchSERPTelemetry._observeActivity", diff --git a/browser/components/search/test/browser/browser.ini b/browser/components/search/test/browser/browser.ini index 5c63e169c30b..e3721b8cd229 100644 --- a/browser/components/search/test/browser/browser.ini +++ b/browser/components/search/test/browser/browser.ini @@ -99,13 +99,30 @@ tags = search-telemetry support-files = searchTelemetryAd_searchbox_with_content.html searchTelemetryAd_searchbox_with_content.html^headers^ +[browser_search_telemetry_engagement_non_ad.js] +tags = search-telemetry +support-files = + searchTelemetryAd_searchbox_with_content.html + searchTelemetryAd_searchbox_with_content.html^headers^ + serp.css +[browser_search_telemetry_engagement_redirect.js] +tags = search-telemetry +support-files = + redirect_final.sjs + redirect_once.sjs + redirect_thrice.sjs + redirect_twice.sjs + searchTelemetryAd_adsLink_redirect.html + searchTelemetryAd_components_text.html + searchTelemetryAd_nonAdsLink_redirect.html + searchTelemetryAd_nonAdsLink_redirect.html^headers^ + searchTelemetryAd_nonAdsLink_redirect_nonTopLoaded.html + searchTelemetryAd_nonAdsLink_redirect_nonTopLoaded.html^headers^ serp.css [browser_search_telemetry_engagement_target.js] tags = search-telemetry support-files = searchTelemetryAd_components_text.html - searchTelemetryAd_nonAdsLink_redirect.html - searchTelemetryAd_nonAdsLink_redirect.html^headers^ searchTelemetryAd_searchbox.html searchTelemetryAd_searchbox.html^headers^ serp.css diff --git a/browser/components/search/test/browser/browser_search_telemetry_engagement_cached.js b/browser/components/search/test/browser/browser_search_telemetry_engagement_cached.js index 502e3d7a8617..6def0ef3ad60 100644 --- a/browser/components/search/test/browser/browser_search_telemetry_engagement_cached.js +++ b/browser/components/search/test/browser/browser_search_telemetry_engagement_cached.js @@ -19,9 +19,7 @@ const TEST_PROVIDER_INFO = [ codeParamName: "abc", taggedCodes: ["ff"], adServerAttributes: ["mozAttr"], - nonAdsLinkRegexps: [ - /^https:\/\/example.com\/browser\/browser\/components\/search\/test\/browser\//, - ], + nonAdsLinkRegexps: [], extraAdServersRegexps: [/^https:\/\/example\.com\/ad/], components: [ { diff --git a/browser/components/search/test/browser/browser_search_telemetry_engagement_content.js b/browser/components/search/test/browser/browser_search_telemetry_engagement_content.js index cefff61a323c..1be562a808c5 100644 --- a/browser/components/search/test/browser/browser_search_telemetry_engagement_content.js +++ b/browser/components/search/test/browser/browser_search_telemetry_engagement_content.js @@ -323,11 +323,8 @@ add_task(async function test_click_related_search_in_new_tab() { BrowserTestUtils.removeTab(tab2); }); -// We consider regular expressions in nonAdsLinkRegexps and -// searchPageRegexp/extraPageRegexps as valid non ads links when recording -// an engagement event. However, if a nonAdsLinkRegexp leads to a -// searchPageRegexp/extraPageRegexps, than we risk double counting in the case -// of a re-direct occuring in a new tab. +// We consider regular expressions in nonAdsLinkRegexps and searchPageRegexp +// as valid non ads links when recording an engagement event. add_task(async function test_click_redirect_search_in_newtab() { resetTelemetry(); let url = getSERPUrl("searchTelemetryAd_searchbox_with_content.html"); diff --git a/browser/components/search/test/browser/browser_search_telemetry_engagement_non_ad.js b/browser/components/search/test/browser/browser_search_telemetry_engagement_non_ad.js new file mode 100644 index 000000000000..ebec383e926b --- /dev/null +++ b/browser/components/search/test/browser/browser_search_telemetry_engagement_non_ad.js @@ -0,0 +1,164 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * These tests load SERPs and click on links that are non ads. Non ads can have + * slightly different behavior from ads. + */ + +"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\/searchTelemetryAd_/, + queryParamName: "s", + codeParamName: "abc", + taggedCodes: ["ff"], + adServerAttributes: ["mozAttr"], + nonAdsLinkRegexps: [], + extraAdServersRegexps: [/^https:\/\/example\.com\/ad/], + components: [ + { + type: SearchSERPTelemetryUtils.COMPONENTS.AD_LINK, + default: true, + }, + ], + }, +]; + +function getSERPUrl(page, organic = false) { + let url = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.org" + ) + page; + return `${url}?s=test${organic ? "" : "&abc=ff"}`; +} + +async function promiseImpressionReceived() { + return TestUtils.waitForCondition(() => { + let adImpressions = Glean.serp.adImpression.testGetValue() ?? []; + return adImpressions.length; + }, "Should have received an ad impression."); +} + +async function waitForIdle() { + for (let i = 0; i < 10; i++) { + await new Promise(resolve => Services.tm.idleDispatchToMainThread(resolve)); + } +} + +add_setup(async function () { + SearchSERPTelemetry.overrideSearchTelemetryForTests(TEST_PROVIDER_INFO); + await waitForIdle(); + // Enable local telemetry recording for the duration of the tests. + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.search.log", true], + ["browser.search.serpEventTelemetry.enabled", true], + ], + }); + + registerCleanupFunction(async () => { + SearchSERPTelemetry.overrideSearchTelemetryForTests(); + resetTelemetry(); + }); +}); + +// If an anchor is a non_ads_link and it doesn't match a non-ads regular +// expression, it should still be categorize it as a non ad. +add_task(async function test_click_non_ads_link() { + await waitForIdle(); + + resetTelemetry(); + let url = getSERPUrl("searchTelemetryAd_components_text.html"); + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url); + await waitForPageWithAdImpressions(); + + // Click a non ad. + let pageLoadPromise = BrowserTestUtils.waitForLocationChange(gBrowser); + await BrowserTestUtils.synthesizeMouseAtCenter( + "#non_ads_link", + {}, + tab.linkedBrowser + ); + await pageLoadPromise; + + assertImpressionEvents([ + { + impression: { + provider: "example", + tagged: "true", + partner_code: "ff", + source: "unknown", + is_shopping_page: "false", + shopping_tab_displayed: "false", + }, + engagements: [ + { + action: SearchSERPTelemetryUtils.ACTIONS.CLICKED, + target: SearchSERPTelemetryUtils.COMPONENTS.NON_ADS_LINK, + }, + ], + }, + ]); + + BrowserTestUtils.removeTab(tab); + + // Reset state for other tests. + SearchSERPTelemetry.overrideSearchTelemetryForTests(TEST_PROVIDER_INFO); + await waitForIdle(); +}); + +// Click on an non-ad element while no ads are present. +add_task(async function test_click_non_ad_with_no_ads() { + await waitForIdle(); + + resetTelemetry(); + + let url = getSERPUrl("searchTelemetryAd_searchbox.html"); + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url); + await waitForPageWithAdImpressions(); + + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + true, + "https://example.com/hello_world" + ); + await BrowserTestUtils.synthesizeMouseAtCenter( + "#non_ads_link", + {}, + tab.linkedBrowser + ); + await browserLoadedPromise; + + assertImpressionEvents([ + { + impression: { + provider: "example", + tagged: "true", + partner_code: "ff", + source: "unknown", + is_shopping_page: "false", + shopping_tab_displayed: "false", + }, + engagements: [ + { + action: SearchSERPTelemetryUtils.ACTIONS.CLICKED, + target: SearchSERPTelemetryUtils.COMPONENTS.NON_ADS_LINK, + }, + ], + }, + ]); + + BrowserTestUtils.removeTab(tab); + + // Reset state for other tests. + SearchSERPTelemetry.overrideSearchTelemetryForTests(TEST_PROVIDER_INFO); + await waitForIdle(); +}); diff --git a/browser/components/search/test/browser/browser_search_telemetry_engagement_redirect.js b/browser/components/search/test/browser/browser_search_telemetry_engagement_redirect.js new file mode 100644 index 000000000000..c12d9944a6ae --- /dev/null +++ b/browser/components/search/test/browser/browser_search_telemetry_engagement_redirect.js @@ -0,0 +1,346 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * These tests load SERPs and click on both ad and non-ad links that can be + * redirected. + */ + +"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\/searchTelemetryAd_/, + queryParamName: "s", + codeParamName: "abc", + taggedCodes: ["ff"], + adServerAttributes: ["mozAttr"], + nonAdsLinkRegexps: [ + /^https:\/\/example.org\/browser\/browser\/components\/search\/test\/browser\/searchTelemetryAd_nonAdsLink_redirect.html/, + ], + extraAdServersRegexps: [ + /^https:\/\/example\.com\/ad/, + /^https:\/\/example.org\/browser\/browser\/components\/search\/test\/browser\/searchTelemetryAd_adsLink_redirect.html/, + ], + components: [ + { + type: SearchSERPTelemetryUtils.COMPONENTS.AD_LINK, + default: true, + }, + ], + }, +]; + +function getSERPUrl(page, organic = false) { + let url = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.org" + ) + page; + return `${url}?s=test${organic ? "" : "&abc=ff"}`; +} + +async function promiseImpressionReceived() { + return TestUtils.waitForCondition(() => { + let adImpressions = Glean.serp.adImpression.testGetValue() ?? []; + return adImpressions.length; + }, "Should have received an ad impression."); +} + +async function waitForIdle() { + for (let i = 0; i < 10; i++) { + await new Promise(resolve => Services.tm.idleDispatchToMainThread(resolve)); + } +} + +add_setup(async function () { + SearchSERPTelemetry.overrideSearchTelemetryForTests(TEST_PROVIDER_INFO); + await waitForIdle(); + // Enable local telemetry recording for the duration of the tests. + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.search.log", true], + ["browser.search.serpEventTelemetry.enabled", true], + ], + }); + + registerCleanupFunction(async () => { + SearchSERPTelemetry.overrideSearchTelemetryForTests(); + resetTelemetry(); + }); +}); + +add_task(async function test_click_non_ads_link_redirected() { + resetTelemetry(); + + let url = getSERPUrl("searchTelemetryAd_components_text.html"); + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url); + await waitForPageWithAdImpressions(); + + let targetUrl = "https://example.com/hello_world"; + + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + true, + targetUrl + ); + await BrowserTestUtils.synthesizeMouseAtCenter( + "#non_ads_link_redirected", + {}, + tab.linkedBrowser + ); + + await browserLoadedPromise; + + assertImpressionEvents([ + { + impression: { + provider: "example", + tagged: "true", + partner_code: "ff", + source: "unknown", + is_shopping_page: "false", + shopping_tab_displayed: "false", + }, + engagements: [ + { + action: SearchSERPTelemetryUtils.ACTIONS.CLICKED, + target: SearchSERPTelemetryUtils.COMPONENTS.NON_ADS_LINK, + }, + ], + }, + ]); + + BrowserTestUtils.removeTab(tab); +}); + +// If a provider does a re-direct and we open it in a new tab, we should +// record the click and have the correct number of engagements. +add_task(async function test_click_non_ads_link_redirected_new_tab() { + resetTelemetry(); + + let url = getSERPUrl("searchTelemetryAd_components_text.html"); + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url); + await waitForPageWithAdImpressions(); + + let redirectUrl = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.org" + ) + "searchTelemetryAd_nonAdsLink_redirect.html"; + let targetUrl = "https://example.com/hello_world"; + + let tabPromise = BrowserTestUtils.waitForNewTab(gBrowser, targetUrl, true); + + await SpecialPowers.spawn(tab.linkedBrowser, [redirectUrl], urls => { + content.document + .getElementById(["non_ads_link"]) + .addEventListener("click", e => { + e.preventDefault(); + content.window.open([urls], "_blank"); + }); + content.document.getElementById("non_ads_link").click(); + }); + let tab2 = await tabPromise; + + assertImpressionEvents([ + { + impression: { + provider: "example", + tagged: "true", + partner_code: "ff", + source: "unknown", + is_shopping_page: "false", + shopping_tab_displayed: "false", + }, + engagements: [ + { + action: SearchSERPTelemetryUtils.ACTIONS.CLICKED, + target: SearchSERPTelemetryUtils.COMPONENTS.NON_ADS_LINK, + }, + ], + }, + ]); + + BrowserTestUtils.removeTab(tab); + BrowserTestUtils.removeTab(tab2); +}); + +// Some providers load a URL of a non ad within a subframe before loading the +// target website in the top level frame. +add_task(async function test_click_non_ads_link_redirect_non_top_level() { + resetTelemetry(); + + let url = getSERPUrl("searchTelemetryAd_components_text.html"); + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url); + await waitForPageWithAdImpressions(); + + let targetUrl = "https://example.com/hello_world"; + + let browserPromise = BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + true, + targetUrl + ); + await BrowserTestUtils.synthesizeMouseAtCenter( + "#non_ads_link_redirected_no_top_level", + {}, + tab.linkedBrowser + ); + + await browserPromise; + + assertImpressionEvents([ + { + impression: { + provider: "example", + tagged: "true", + partner_code: "ff", + source: "unknown", + is_shopping_page: "false", + shopping_tab_displayed: "false", + }, + engagements: [ + { + action: SearchSERPTelemetryUtils.ACTIONS.CLICKED, + target: SearchSERPTelemetryUtils.COMPONENTS.NON_ADS_LINK, + }, + ], + }, + ]); + + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_multiple_redirects_non_ad_link() { + resetTelemetry(); + + let url = getSERPUrl("searchTelemetryAd_components_text.html"); + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url); + await waitForPageWithAdImpressions(); + + let targetUrl = "https://example.com/hello_world"; + + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + true, + targetUrl + ); + await BrowserTestUtils.synthesizeMouseAtCenter( + "#non_ads_link_multiple_redirects", + {}, + tab.linkedBrowser + ); + + await browserLoadedPromise; + + assertImpressionEvents([ + { + impression: { + provider: "example", + tagged: "true", + partner_code: "ff", + source: "unknown", + is_shopping_page: "false", + shopping_tab_displayed: "false", + }, + engagements: [ + { + action: SearchSERPTelemetryUtils.ACTIONS.CLICKED, + target: SearchSERPTelemetryUtils.COMPONENTS.NON_ADS_LINK, + }, + ], + }, + ]); + + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_click_ad_link_redirected() { + resetTelemetry(); + + let url = getSERPUrl("searchTelemetryAd_components_text.html"); + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url); + await waitForPageWithAdImpressions(); + + let targetUrl = "https://example.com/hello_world"; + + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + true, + targetUrl + ); + await BrowserTestUtils.synthesizeMouseAtCenter( + "#ad_link_redirect", + {}, + tab.linkedBrowser + ); + + await browserLoadedPromise; + + assertImpressionEvents([ + { + impression: { + provider: "example", + tagged: "true", + partner_code: "ff", + source: "unknown", + is_shopping_page: "false", + shopping_tab_displayed: "false", + }, + engagements: [ + { + action: SearchSERPTelemetryUtils.ACTIONS.CLICKED, + target: SearchSERPTelemetryUtils.COMPONENTS.AD_LINK, + }, + ], + }, + ]); + + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_click_ad_link_redirected_new_tab() { + resetTelemetry(); + + let url = getSERPUrl("searchTelemetryAd_components_text.html"); + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url); + await waitForPageWithAdImpressions(); + + let targetUrl = "https://example.com/hello_world"; + + let tabPromise = BrowserTestUtils.waitForNewTab(gBrowser, targetUrl, true); + await BrowserTestUtils.synthesizeMouseAtCenter( + "#ad_link_redirect", + { button: 1 }, + tab.linkedBrowser + ); + let tab2 = await tabPromise; + + assertImpressionEvents([ + { + impression: { + provider: "example", + tagged: "true", + partner_code: "ff", + source: "unknown", + is_shopping_page: "false", + shopping_tab_displayed: "false", + }, + engagements: [ + { + action: SearchSERPTelemetryUtils.ACTIONS.CLICKED, + target: SearchSERPTelemetryUtils.COMPONENTS.AD_LINK, + }, + ], + }, + ]); + + BrowserTestUtils.removeTab(tab); + BrowserTestUtils.removeTab(tab2); +}); diff --git a/browser/components/search/test/browser/browser_search_telemetry_engagement_target.js b/browser/components/search/test/browser/browser_search_telemetry_engagement_target.js index fc186ada84b9..d29169b776b4 100644 --- a/browser/components/search/test/browser/browser_search_telemetry_engagement_target.js +++ b/browser/components/search/test/browser/browser_search_telemetry_engagement_target.js @@ -22,7 +22,7 @@ const TEST_PROVIDER_INFO = [ taggedCodes: ["ff"], adServerAttributes: ["mozAttr"], nonAdsLinkRegexps: [ - /^https:\/\/example.org\/browser\/browser\/components\/search\/test\/browser\/searchTelemetryAd_nonAdsLink_redirect/, + /^https:\/\/example.org\/browser\/browser\/components\/search\/test\/browser\/searchTelemetryAd_nonAdsLink_redirect.html/, ], extraAdServersRegexps: [/^https:\/\/example\.com\/ad/], components: [ @@ -190,112 +190,6 @@ add_task(async function test_click_second_ad_in_component() { BrowserTestUtils.removeTab(tab); }); -// If a provider does a re-direct and we open it in a new tab, we should -// record the click and have the correct number of engagements. -add_task(async function test_click_non_ads_link_redirected_new_tab() { - resetTelemetry(); - - let url = getSERPUrl("searchTelemetryAd_components_text.html"); - let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url); - await waitForPageWithAdImpressions(); - - let redirectUrl = - getRootDirectory(gTestPath).replace( - "chrome://mochitests/content", - "https://example.org" - ) + "searchTelemetryAd_nonAdsLink_redirect.html"; - let targetUrl = "https://example.com/hello_world"; - - let tabPromise = BrowserTestUtils.waitForNewTab(gBrowser, targetUrl, true); - - await SpecialPowers.spawn(tab.linkedBrowser, [redirectUrl], urls => { - content.document - .getElementById(["non_ads_link"]) - .addEventListener("click", e => { - e.preventDefault(); - content.window.open([urls], "_blank"); - }); - content.document.getElementById("non_ads_link").click(); - }); - let tab2 = await tabPromise; - - assertImpressionEvents([ - { - impression: { - provider: "example", - tagged: "true", - partner_code: "ff", - source: "unknown", - is_shopping_page: "false", - shopping_tab_displayed: "false", - }, - engagements: [ - { - action: SearchSERPTelemetryUtils.ACTIONS.CLICKED, - target: SearchSERPTelemetryUtils.COMPONENTS.NON_ADS_LINK, - }, - ], - }, - ]); - - BrowserTestUtils.removeTab(tab2); - BrowserTestUtils.removeTab(tab); -}); - -// If a provider does a re-direct in the same tab, we may not be able to -// determine the component based on the URLs on the page, because the initial -// redirect URL won't be known, and by the time the page is loaded, the cached -// data might be invalidated. So this should use regular expressions to make -// an inference. -add_task(async function test_click_non_ads_link_redirected() { - resetTelemetry(); - - let url = getSERPUrl("searchTelemetryAd_components_text.html"); - let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url); - await waitForPageWithAdImpressions(); - - let redirectUrl = - getRootDirectory(gTestPath).replace( - "chrome://mochitests/content", - "https://example.org" - ) + "searchTelemetryAd_nonAdsLink_redirect.html"; - let targetUrl = "https://example.com/hello_world"; - - let browserLoadedPromise = BrowserTestUtils.browserLoaded( - tab.linkedBrowser, - true, - targetUrl - ); - await SpecialPowers.spawn(tab.linkedBrowser, [redirectUrl], urls => { - content.document - .getElementById(["non_ads_link"]) - .setAttribute("href", [urls]); - content.document.getElementById("non_ads_link").click(); - }); - await browserLoadedPromise; - - assertImpressionEvents([ - { - impression: { - provider: "example", - tagged: "true", - partner_code: "ff", - source: "unknown", - is_shopping_page: "false", - shopping_tab_displayed: "false", - }, - engagements: [ - { - action: SearchSERPTelemetryUtils.ACTIONS.CLICKED, - target: SearchSERPTelemetryUtils.COMPONENTS.NON_ADS_LINK, - }, - ], - }, - ]); - - BrowserTestUtils.removeTab(tab); -}); - // If a provider appends query parameters to a link after the page has been // parsed, we should still be able to record the click. add_task(async function test_click_ads_link_modified() { @@ -337,54 +231,6 @@ add_task(async function test_click_ads_link_modified() { BrowserTestUtils.removeTab(tab); }); -// If an anchor is a non_ads_link and we don't have a matching regular -// expression, we should still be categorize it as non ads. -add_task(async function test_click_non_ads_link() { - SearchSERPTelemetry.overrideSearchTelemetryForTests( - TEST_PROVIDER_INFO_NO_NON_ADS_REGEXP - ); - await waitForIdle(); - - resetTelemetry(); - let url = getSERPUrl("searchTelemetryAd_components_text.html"); - let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url); - await waitForPageWithAdImpressions(); - - // Click a non ad. - let pageLoadPromise = BrowserTestUtils.waitForLocationChange(gBrowser); - await BrowserTestUtils.synthesizeMouseAtCenter( - "#non_ads_link", - {}, - tab.linkedBrowser - ); - await pageLoadPromise; - - assertImpressionEvents([ - { - impression: { - provider: "example", - tagged: "true", - partner_code: "ff", - source: "unknown", - is_shopping_page: "false", - shopping_tab_displayed: "false", - }, - engagements: [ - { - action: SearchSERPTelemetryUtils.ACTIONS.CLICKED, - target: SearchSERPTelemetryUtils.COMPONENTS.NON_ADS_LINK, - }, - ], - }, - ]); - - BrowserTestUtils.removeTab(tab); - - // Reset state for other tests. - SearchSERPTelemetry.overrideSearchTelemetryForTests(TEST_PROVIDER_INFO); - await waitForIdle(); -}); - // Search box is a special case which has to be tracked in the child process. add_task(async function test_click_and_submit_incontent_searchbox() { resetTelemetry(); @@ -531,58 +377,6 @@ add_task(async function test_click_carousel_expand() { BrowserTestUtils.removeTab(tab); }); -// Click on an non-ad element while no ads are present. -add_task(async function test_click_non_ad_with_no_ads() { - // Use a provider that doesn't a stored non-ads regexp. - SearchSERPTelemetry.overrideSearchTelemetryForTests( - TEST_PROVIDER_INFO_NO_NON_ADS_REGEXP - ); - await waitForIdle(); - - resetTelemetry(); - - let url = getSERPUrl("searchTelemetryAd_searchbox.html"); - let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url); - await waitForPageWithAdImpressions(); - - let browserLoadedPromise = BrowserTestUtils.browserLoaded( - tab.linkedBrowser, - true, - "https://example.com/hello_world" - ); - await BrowserTestUtils.synthesizeMouseAtCenter( - "#non_ads_link", - {}, - tab.linkedBrowser - ); - await browserLoadedPromise; - - assertImpressionEvents([ - { - impression: { - provider: "example", - tagged: "true", - partner_code: "ff", - source: "unknown", - is_shopping_page: "false", - shopping_tab_displayed: "false", - }, - engagements: [ - { - action: SearchSERPTelemetryUtils.ACTIONS.CLICKED, - target: SearchSERPTelemetryUtils.COMPONENTS.NON_ADS_LINK, - }, - ], - }, - ]); - - BrowserTestUtils.removeTab(tab); - - // Reset state for other tests. - SearchSERPTelemetry.overrideSearchTelemetryForTests(TEST_PROVIDER_INFO); - await waitForIdle(); -}); - // This test clicks a link that has apostrophes in the both the path and list // of query parameters, and uses search telemetry with no nonAdsRegexps defined, // which will force us to cache every non ads link in a map and pass it back to diff --git a/browser/components/search/test/browser/redirect_final.sjs b/browser/components/search/test/browser/redirect_final.sjs new file mode 100644 index 000000000000..14debde6ba5c --- /dev/null +++ b/browser/components/search/test/browser/redirect_final.sjs @@ -0,0 +1,9 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function handleRequest(request, response) { + response.setStatusLine("1.1", 302, "Found"); + response.setHeader("Location", "https://example.com/hello_world", false); +} diff --git a/browser/components/search/test/browser/redirect_once.sjs b/browser/components/search/test/browser/redirect_once.sjs new file mode 100644 index 000000000000..d15f3afe6d96 --- /dev/null +++ b/browser/components/search/test/browser/redirect_once.sjs @@ -0,0 +1,9 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function handleRequest(request, response) { + response.setStatusLine("1.1", 302, "Found"); + response.setHeader("Location", "redirect_final.sjs", false); +} diff --git a/browser/components/search/test/browser/redirect_thrice.sjs b/browser/components/search/test/browser/redirect_thrice.sjs new file mode 100644 index 000000000000..b7c706916224 --- /dev/null +++ b/browser/components/search/test/browser/redirect_thrice.sjs @@ -0,0 +1,9 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function handleRequest(request, response) { + response.setStatusLine("1.1", 302, "Found"); + response.setHeader("Location", "redirect_twice.sjs", false); +} diff --git a/browser/components/search/test/browser/redirect_twice.sjs b/browser/components/search/test/browser/redirect_twice.sjs new file mode 100644 index 000000000000..099d20022ecd --- /dev/null +++ b/browser/components/search/test/browser/redirect_twice.sjs @@ -0,0 +1,9 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function handleRequest(request, response) { + response.setStatusLine("1.1", 302, "Found"); + response.setHeader("Location", "redirect_once.sjs", false); +} diff --git a/browser/components/search/test/browser/searchTelemetryAd_adsLink_redirect.html b/browser/components/search/test/browser/searchTelemetryAd_adsLink_redirect.html new file mode 100644 index 000000000000..71bbe2a07009 --- /dev/null +++ b/browser/components/search/test/browser/searchTelemetryAd_adsLink_redirect.html @@ -0,0 +1,12 @@ + + +
+ + + +Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Cras ac velit sed tellus facilisis euismod.
Ad link that says there are 10 Locations nearbyLorem ipsum dolor sit amet, consectetuer adipiscing elit. Cras ac velit sed tellus facilisis euismod.
Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Cras ac velit sed tellus facilisis euismod.
-