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 @@ + + + + + + + Page will do a redirect + + + + + diff --git a/browser/components/search/test/browser/searchTelemetryAd_components_text.html b/browser/components/search/test/browser/searchTelemetryAd_components_text.html index 403ac6b5f94d..192812c699b4 100644 --- a/browser/components/search/test/browser/searchTelemetryAd_components_text.html +++ b/browser/components/search/test/browser/searchTelemetryAd_components_text.html @@ -15,7 +15,6 @@

Example Result

-

Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Cras ac velit sed tellus facilisis euismod.

Ad link that says there are 10 Locations nearby
@@ -47,10 +46,9 @@
ad_link
- +

Example Shop

-

Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Cras ac velit sed tellus facilisis euismod.

Home Page Products @@ -62,23 +60,23 @@

Example Shop

-

Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Cras ac velit sed tellus facilisis euismod.

-
non_ads_link
-

Example of a cached non ad

-
-
-
-
non_ads_link
+ Example of a cached non ad +
+ + Example of a redirected non ad link +
+ + Example of a redirected non ad link that isn't initially top level loaded +
+ + Example of a redirected non ad link that's redirected multiple times +
-

Example of a non ad

+ Example of a non ad with special characters in path
@@ -96,5 +94,19 @@
+ + diff --git a/browser/components/search/test/browser/searchTelemetryAd_nonAdsLink_redirect_nonTopLoaded.html b/browser/components/search/test/browser/searchTelemetryAd_nonAdsLink_redirect_nonTopLoaded.html new file mode 100644 index 000000000000..1c5c31cb3822 --- /dev/null +++ b/browser/components/search/test/browser/searchTelemetryAd_nonAdsLink_redirect_nonTopLoaded.html @@ -0,0 +1,17 @@ + + + + + + + Page will do a redirect without doing it in a top load + + + + + + diff --git a/browser/components/search/test/browser/searchTelemetryAd_nonAdsLink_redirect_nonTopLoaded.html^headers^ b/browser/components/search/test/browser/searchTelemetryAd_nonAdsLink_redirect_nonTopLoaded.html^headers^ new file mode 100644 index 000000000000..419697b05090 --- /dev/null +++ b/browser/components/search/test/browser/searchTelemetryAd_nonAdsLink_redirect_nonTopLoaded.html^headers^ @@ -0,0 +1,4 @@ +Cache-Control: no-cache, must-revalidate +Pragma: no-cache +Expires: Fri, 01 Jan 1990 00:00:00 GMT +Content-Type: text/html; charset=ISO-8859-1