summaryrefslogtreecommitdiffstats
path: root/browser/components/search/test/unit
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/search/test/unit')
-rw-r--r--browser/components/search/test/unit/test_search_telemetry_config_validation.js82
-rw-r--r--browser/components/search/test/unit/test_urlTelemetry.js310
-rw-r--r--browser/components/search/test/unit/test_urlTelemetry_generic.js323
-rw-r--r--browser/components/search/test/unit/xpcshell.ini9
4 files changed, 724 insertions, 0 deletions
diff --git a/browser/components/search/test/unit/test_search_telemetry_config_validation.js b/browser/components/search/test/unit/test_search_telemetry_config_validation.js
new file mode 100644
index 0000000000..1c243cfc82
--- /dev/null
+++ b/browser/components/search/test/unit/test_search_telemetry_config_validation.js
@@ -0,0 +1,82 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ AppConstants: "resource://gre/modules/AppConstants.sys.mjs",
+ RemoteSettings: "resource://services-settings/remote-settings.sys.mjs",
+ TELEMETRY_SETTINGS_KEY: "resource:///modules/SearchSERPTelemetry.sys.mjs",
+ JsonSchema: "resource://gre/modules/JsonSchema.sys.mjs",
+});
+
+/**
+ * Checks to see if a value is an object or not.
+ *
+ * @param {*} value
+ * The value to check.
+ * @returns {boolean}
+ */
+function isObject(value) {
+ return value != null && typeof value == "object" && !Array.isArray(value);
+}
+
+/**
+ * This function modifies the schema to prevent allowing additional properties
+ * on objects. This is used to enforce that the schema contains everything that
+ * we deliver via the search configuration.
+ *
+ * These checks are not enabled in-product, as we want to allow older versions
+ * to keep working if we add new properties for whatever reason.
+ *
+ * @param {object} section
+ * The section to check to see if an additionalProperties flag should be added.
+ */
+function disallowAdditionalProperties(section) {
+ // It is generally acceptable for new properties to be added to the
+ // configuration as older builds will ignore them.
+ //
+ // As a result, we only check for new properties on nightly builds, and this
+ // avoids us having to uplift schema changes. This also helps preserve the
+ // schemas as documentation of "what was supported in this version".
+ if (!AppConstants.NIGHTLY_BUILD) {
+ info("Skipping additional properties validation.");
+ return;
+ }
+
+ if (section.type == "object") {
+ section.additionalProperties = false;
+ }
+ for (let value of Object.values(section)) {
+ if (isObject(value)) {
+ disallowAdditionalProperties(value);
+ }
+ }
+}
+
+add_task(async function test_search_config_validates_to_schema() {
+ let schema = await IOUtils.readJSON(
+ PathUtils.join(do_get_cwd().path, "search-telemetry-schema.json")
+ );
+ disallowAdditionalProperties(schema);
+
+ let data = await RemoteSettings(TELEMETRY_SETTINGS_KEY).get();
+
+ let validator = new JsonSchema.Validator(schema);
+
+ for (let entry of data) {
+ // Records in Remote Settings contain additional properties independent of
+ // the schema. Hence, we don't want to validate their presence.
+ delete entry.schema;
+ delete entry.id;
+ delete entry.last_modified;
+ delete entry.filter_expression;
+
+ let result = validator.validate(entry);
+ let message = `Should validate ${entry.telemetryId}`;
+ if (!result.valid) {
+ message += `:\n${JSON.stringify(result.errors, null, 2)}`;
+ }
+ Assert.ok(result.valid, message);
+ }
+});
diff --git a/browser/components/search/test/unit/test_urlTelemetry.js b/browser/components/search/test/unit/test_urlTelemetry.js
new file mode 100644
index 0000000000..bd46f39e5b
--- /dev/null
+++ b/browser/components/search/test/unit/test_urlTelemetry.js
@@ -0,0 +1,310 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ BrowserSearchTelemetry: "resource:///modules/BrowserSearchTelemetry.sys.mjs",
+ SearchSERPTelemetry: "resource:///modules/SearchSERPTelemetry.sys.mjs",
+ SearchUtils: "resource://gre/modules/SearchUtils.sys.mjs",
+ TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.sys.mjs",
+ sinon: "resource://testing-common/Sinon.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ NetUtil: "resource://gre/modules/NetUtil.jsm",
+});
+
+const TESTS = [
+ {
+ title: "Google search access point",
+ trackingUrl:
+ "https://www.google.com/search?q=test&ie=utf-8&oe=utf-8&client=firefox-b-1-ab",
+ expectedSearchCountEntry: "google:tagged:firefox-b-1-ab",
+ expectedAdKey: "google:tagged",
+ adUrls: [
+ "https://www.googleadservices.com/aclk=foobar",
+ "https://www.googleadservices.com/pagead/aclk=foobar",
+ "https://www.google.com/aclk=foobar",
+ "https://www.google.com/pagead/aclk=foobar",
+ ],
+ nonAdUrls: [
+ "https://www.googleadservices.com/?aclk=foobar",
+ "https://www.googleadservices.com/bar",
+ "https://www.google.com/image",
+ ],
+ },
+ {
+ title: "Google search access point follow-on",
+ trackingUrl:
+ "https://www.google.com/search?client=firefox-b-1-ab&ei=EI_VALUE&q=test2&oq=test2&gs_l=GS_L_VALUE",
+ expectedSearchCountEntry: "google:tagged-follow-on:firefox-b-1-ab",
+ },
+ {
+ title: "Google organic",
+ trackingUrl:
+ "https://www.google.com/search?client=firefox-b-d-invalid&source=hp&ei=EI_VALUE&q=test&oq=test&gs_l=GS_L_VALUE",
+ expectedSearchCountEntry: "google:organic:other",
+ expectedAdKey: "google:organic",
+ adUrls: ["https://www.googleadservices.com/aclk=foobar"],
+ nonAdUrls: ["https://www.googleadservices.com/?aclk=foobar"],
+ },
+ {
+ title: "Google organic no code",
+ trackingUrl:
+ "https://www.google.com/search?source=hp&ei=EI_VALUE&q=test&oq=test&gs_l=GS_L_VALUE",
+ expectedSearchCountEntry: "google:organic:none",
+ expectedAdKey: "google:organic",
+ adUrls: ["https://www.googleadservices.com/aclk=foobar"],
+ nonAdUrls: ["https://www.googleadservices.com/?aclk=foobar"],
+ },
+ {
+ title: "Google organic UK",
+ trackingUrl:
+ "https://www.google.co.uk/search?source=hp&ei=EI_VALUE&q=test&oq=test&gs_l=GS_L_VALUE",
+ expectedSearchCountEntry: "google:organic:none",
+ },
+ {
+ title: "Bing search access point",
+ trackingUrl: "https://www.bing.com/search?q=test&pc=MOZI&form=MOZLBR",
+ expectedSearchCountEntry: "bing:tagged:MOZI",
+ expectedAdKey: "bing:tagged",
+ adUrls: [
+ "https://www.bing.com/aclick?ld=foo",
+ "https://www.bing.com/aclk?ld=foo",
+ ],
+ nonAdUrls: [
+ "https://www.bing.com/fd/ls/ls.gif?IG=foo",
+ "https://www.bing.com/fd/ls/l?IG=bar",
+ "https://www.bing.com/aclook?",
+ "https://www.bing.com/fd/ls/GLinkPingPost.aspx?IG=baz&url=%2Fvideos%2Fsearch%3Fq%3Dfoo",
+ "https://www.bing.com/fd/ls/GLinkPingPost.aspx?IG=bar&url=https%3A%2F%2Fwww.bing.com%2Faclick",
+ "https://www.bing.com/fd/ls/GLinkPingPost.aspx?IG=bar&url=https%3A%2F%2Fwww.bing.com%2Faclk",
+ ],
+ },
+ {
+ setUp() {
+ Services.cookies.removeAll();
+ Services.cookies.add(
+ "www.bing.com",
+ "/",
+ "SRCHS",
+ "PC=MOZI",
+ false,
+ false,
+ false,
+ Date.now() + 1000 * 60 * 60,
+ {},
+ Ci.nsICookie.SAMESITE_NONE,
+ Ci.nsICookie.SCHEME_HTTPS
+ );
+ },
+ tearDown() {
+ Services.cookies.removeAll();
+ },
+ title: "Bing search access point follow-on",
+ trackingUrl:
+ "https://www.bing.com/search?q=test&qs=n&form=QBRE&sp=-1&pq=&sc=0-0&sk=&cvid=CVID_VALUE",
+ expectedSearchCountEntry: "bing:tagged-follow-on:MOZI",
+ },
+ {
+ title: "Bing organic",
+ trackingUrl: "https://www.bing.com/search?q=test&pc=MOZIfoo&form=MOZLBR",
+ expectedSearchCountEntry: "bing:organic:other",
+ expectedAdKey: "bing:organic",
+ adUrls: ["https://www.bing.com/aclick?ld=foo"],
+ nonAdUrls: ["https://www.bing.com/fd/ls/ls.gif?IG=foo"],
+ },
+ {
+ title: "Bing organic no code",
+ trackingUrl:
+ "https://www.bing.com/search?q=test&qs=n&form=QBLH&sp=-1&pq=&sc=0-0&sk=&cvid=CVID_VALUE",
+ expectedSearchCountEntry: "bing:organic:none",
+ expectedAdKey: "bing:organic",
+ adUrls: ["https://www.bing.com/aclick?ld=foo"],
+ nonAdUrls: ["https://www.bing.com/fd/ls/ls.gif?IG=foo"],
+ },
+ {
+ title: "DuckDuckGo search access point",
+ trackingUrl: "https://duckduckgo.com/?q=test&t=ffab",
+ expectedSearchCountEntry: "duckduckgo:tagged:ffab",
+ expectedAdKey: "duckduckgo:tagged",
+ adUrls: [
+ "https://duckduckgo.com/y.js?ad_provider=foo",
+ "https://duckduckgo.com/y.js?f=bar&ad_provider=foo",
+ "https://www.amazon.co.uk/foo?tag=duckduckgo-ffab-uk-32-xk",
+ ],
+ nonAdUrls: [
+ "https://duckduckgo.com/?q=foo&t=ffab&ia=images&iax=images",
+ "https://duckduckgo.com/y.js?ifu=foo",
+ "https://improving.duckduckgo.com/t/bar",
+ ],
+ },
+ {
+ title: "DuckDuckGo organic",
+ trackingUrl: "https://duckduckgo.com/?q=test&t=other&ia=news",
+ expectedSearchCountEntry: "duckduckgo:organic:other",
+ expectedAdKey: "duckduckgo:organic",
+ adUrls: ["https://duckduckgo.com/y.js?ad_provider=foo"],
+ nonAdUrls: ["https://duckduckgo.com/?q=foo&t=ffab&ia=images&iax=images"],
+ },
+ {
+ title: "DuckDuckGo expected organic code",
+ trackingUrl: "https://duckduckgo.com/?q=test&t=h_&ia=news",
+ expectedSearchCountEntry: "duckduckgo:organic:none",
+ expectedAdKey: "duckduckgo:organic",
+ adUrls: ["https://duckduckgo.com/y.js?ad_provider=foo"],
+ nonAdUrls: ["https://duckduckgo.com/?q=foo&t=ffab&ia=images&iax=images"],
+ },
+ {
+ title: "DuckDuckGo expected organic code 2",
+ trackingUrl: "https://duckduckgo.com/?q=test&t=hz&ia=news",
+ expectedSearchCountEntry: "duckduckgo:organic:none",
+ expectedAdKey: "duckduckgo:organic",
+ adUrls: ["https://duckduckgo.com/y.js?ad_provider=foo"],
+ nonAdUrls: ["https://duckduckgo.com/?q=foo&t=ffab&ia=images&iax=images"],
+ },
+ {
+ title: "DuckDuckGo organic no code",
+ trackingUrl: "https://duckduckgo.com/?q=test&ia=news",
+ expectedSearchCountEntry: "duckduckgo:organic:none",
+ expectedAdKey: "duckduckgo:organic",
+ adUrls: ["https://duckduckgo.com/y.js?ad_provider=foo"],
+ nonAdUrls: ["https://duckduckgo.com/?q=foo&t=ffab&ia=images&iax=images"],
+ },
+ {
+ title: "Baidu search access point",
+ trackingUrl: "https://www.baidu.com/baidu?wd=test&tn=monline_7_dg&ie=utf-8",
+ expectedSearchCountEntry: "baidu:tagged:monline_7_dg",
+ expectedAdKey: "baidu:tagged",
+ adUrls: ["https://www.baidu.com/baidu.php?url=encoded"],
+ nonAdUrls: ["https://www.baidu.com/link?url=encoded"],
+ },
+ {
+ title: "Baidu search access point follow-on",
+ trackingUrl:
+ "https://www.baidu.com/s?ie=utf-8&f=8&rsv_bp=1&tn=monline_7_dg&wd=test2&oq=test&rsv_pq=RSV_PQ_VALUE&rsv_t=RSV_T_VALUE&rqlang=cn&rsv_enter=1&rsv_sug3=2&rsv_sug2=0&inputT=227&rsv_sug4=397",
+ expectedSearchCountEntry: "baidu:tagged-follow-on:monline_7_dg",
+ },
+ {
+ title: "Baidu organic",
+ trackingUrl:
+ "https://www.baidu.com/s?ie=utf-8&f=8&rsv_bp=1&rsv_idx=1&ch=&tn=baidu&bar=&wd=test&rn=&oq&rsv_pq=RSV_PQ_VALUE&rsv_t=RSV_T_VALUE&rqlang=cn",
+ expectedSearchCountEntry: "baidu:organic:other",
+ },
+ {
+ title: "Baidu organic no code",
+ trackingUrl:
+ "https://www.baidu.com/s?ie=utf-8&f=8&rsv_bp=1&rsv_idx=1&ch=&bar=&wd=test&rn=&oq&rsv_pq=RSV_PQ_VALUE&rsv_t=RSV_T_VALUE&rqlang=cn",
+ expectedSearchCountEntry: "baidu:organic:none",
+ },
+ {
+ title: "Ecosia search access point",
+ trackingUrl: "https://www.ecosia.org/search?tt=mzl&q=foo",
+ expectedSearchCountEntry: "ecosia:tagged:mzl",
+ expectedAdKey: "ecosia:tagged",
+ adUrls: ["https://www.bing.com/aclick?ld=foo"],
+ nonAdUrls: [],
+ },
+ {
+ title: "Ecosia organic",
+ trackingUrl: "https://www.ecosia.org/search?method=index&q=foo",
+ expectedSearchCountEntry: "ecosia:organic:none",
+ expectedAdKey: "ecosia:organic",
+ adUrls: ["https://www.bing.com/aclick?ld=foo"],
+ nonAdUrls: [],
+ },
+];
+
+/**
+ * This function is primarily for testing the Ad URL regexps that are triggered
+ * when a URL is clicked on. These regexps are also used for the `with_ads`
+ * probe. However, we test the ad_clicks route as that is easier to hit.
+ *
+ * @param {string} serpUrl
+ * The url to simulate where the page the click came from.
+ * @param {string} adUrl
+ * The ad url to simulate being clicked.
+ * @param {string} [expectedAdKey]
+ * The expected key to be logged for the scalar. Omit if no scalar should be
+ * logged.
+ */
+async function testAdUrlClicked(serpUrl, adUrl, expectedAdKey) {
+ info(`Testing Ad URL: ${adUrl}`);
+ let channel = NetUtil.newChannel({
+ uri: NetUtil.newURI(adUrl),
+ triggeringPrincipal: Services.scriptSecurityManager.createContentPrincipal(
+ NetUtil.newURI(serpUrl),
+ {}
+ ),
+ loadUsingSystemPrincipal: true,
+ });
+ SearchSERPTelemetry._contentHandler.observeActivity(
+ channel,
+ Ci.nsIHttpActivityObserver.ACTIVITY_TYPE_HTTP_TRANSACTION,
+ Ci.nsIHttpActivityObserver.ACTIVITY_SUBTYPE_TRANSACTION_CLOSE
+ );
+ // Since the content handler takes a moment to allow the channel information
+ // to settle down, wait the same amount of time here.
+ await new Promise(resolve => Services.tm.dispatchToMainThread(resolve));
+
+ const scalars = TelemetryTestUtils.getProcessScalars("parent", true, true);
+ if (!expectedAdKey) {
+ Assert.ok(
+ !("browser.search.adclicks.unknown" in scalars),
+ "Should not have recorded an ad click"
+ );
+ } else {
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ "browser.search.adclicks.unknown",
+ expectedAdKey,
+ 1
+ );
+ }
+}
+
+do_get_profile();
+
+add_task(async function setup() {
+ Services.prefs.setBoolPref(SearchUtils.BROWSER_SEARCH_PREF + "log", true);
+ await SearchSERPTelemetry.init();
+ sinon.stub(BrowserSearchTelemetry, "shouldRecordSearchCount").returns(true);
+});
+
+add_task(async function test_parsing_search_urls() {
+ for (const test of TESTS) {
+ info(`Running ${test.title}`);
+ if (test.setUp) {
+ test.setUp();
+ }
+ SearchSERPTelemetry.updateTrackingStatus(
+ {
+ getTabBrowser: () => {},
+ },
+ test.trackingUrl
+ );
+ let scalars = TelemetryTestUtils.getProcessScalars("parent", true, true);
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ "browser.search.content.unknown",
+ test.expectedSearchCountEntry,
+ 1
+ );
+
+ if ("adUrls" in test) {
+ for (const adUrl of test.adUrls) {
+ await testAdUrlClicked(test.trackingUrl, adUrl, test.expectedAdKey);
+ }
+ for (const nonAdUrls of test.nonAdUrls) {
+ await testAdUrlClicked(test.trackingUrl, nonAdUrls);
+ }
+ }
+
+ if (test.tearDown) {
+ test.tearDown();
+ }
+ }
+});
diff --git a/browser/components/search/test/unit/test_urlTelemetry_generic.js b/browser/components/search/test/unit/test_urlTelemetry_generic.js
new file mode 100644
index 0000000000..610dd56e3a
--- /dev/null
+++ b/browser/components/search/test/unit/test_urlTelemetry_generic.js
@@ -0,0 +1,323 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ BrowserSearchTelemetry: "resource:///modules/BrowserSearchTelemetry.sys.mjs",
+ SearchSERPTelemetry: "resource:///modules/SearchSERPTelemetry.sys.mjs",
+ SearchSERPTelemetryUtils: "resource:///modules/SearchSERPTelemetry.sys.mjs",
+ SearchUtils: "resource://gre/modules/SearchUtils.sys.mjs",
+ TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.sys.mjs",
+ sinon: "resource://testing-common/Sinon.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ NetUtil: "resource://gre/modules/NetUtil.jsm",
+});
+
+const TEST_PROVIDER_INFO = [
+ {
+ telemetryId: "example",
+ searchPageRegexp: /^https:\/\/www\.example\.com\/search/,
+ queryParamName: "q",
+ codeParamName: "abc",
+ taggedCodes: ["ff", "tb"],
+ expectedOrganicCodes: ["baz"],
+ organicCodes: ["foo"],
+ followOnParamNames: ["a"],
+ extraAdServersRegexps: [/^https:\/\/www\.example\.com\/ad2/],
+ shoppingTab: {
+ regexp: "&site=shop",
+ },
+ components: [
+ {
+ type: SearchSERPTelemetryUtils.COMPONENTS.AD_LINK,
+ default: true,
+ },
+ ],
+ },
+ {
+ telemetryId: "example2",
+ searchPageRegexp: /^https:\/\/www\.example2\.com\/search/,
+ queryParamName: "q",
+ codeParamName: "abc",
+ taggedCodes: ["ff", "tb"],
+ expectedOrganicCodes: ["baz"],
+ organicCodes: ["foo"],
+ followOnParamNames: ["a"],
+ extraAdServersRegexps: [/^https:\/\/www\.example\.com\/ad2/],
+ components: [
+ {
+ type: SearchSERPTelemetryUtils.COMPONENTS.AD_LINK,
+ default: true,
+ },
+ ],
+ },
+];
+
+const TESTS = [
+ {
+ title: "Tagged search",
+ trackingUrl: "https://www.example.com/search?q=test&abc=ff",
+ expectedSearchCountEntry: "example:tagged:ff",
+ expectedAdKey: "example:tagged",
+ adUrls: ["https://www.example.com/ad2"],
+ nonAdUrls: ["https://www.example.com/ad3"],
+ impression: {
+ provider: "example",
+ tagged: "true",
+ partner_code: "ff",
+ is_shopping_page: "false",
+ shopping_tab_displayed: "false",
+ source: "unknown",
+ },
+ },
+ {
+ title: "Tagged search with shopping",
+ trackingUrl: "https://www.example.com/search?q=test&abc=ff&site=shop",
+ expectedSearchCountEntry: "example:tagged:ff",
+ expectedAdKey: "example:tagged",
+ adUrls: ["https://www.example.com/ad2"],
+ nonAdUrls: ["https://www.example.com/ad3"],
+ impression: {
+ provider: "example",
+ tagged: "true",
+ partner_code: "ff",
+ is_shopping_page: "true",
+ shopping_tab_displayed: "false",
+ source: "unknown",
+ },
+ },
+ {
+ title: "Tagged follow-on",
+ trackingUrl: "https://www.example.com/search?q=test&abc=tb&a=next",
+ expectedSearchCountEntry: "example:tagged-follow-on:tb",
+ expectedAdKey: "example:tagged-follow-on",
+ adUrls: ["https://www.example.com/ad2"],
+ nonAdUrls: ["https://www.example.com/ad3"],
+ impression: {
+ provider: "example",
+ tagged: "true",
+ partner_code: "tb",
+ is_shopping_page: "false",
+ shopping_tab_displayed: "false",
+ source: "unknown",
+ },
+ },
+ {
+ title: "Organic search matched code",
+ trackingUrl: "https://www.example.com/search?q=test&abc=foo",
+ expectedSearchCountEntry: "example:organic:foo",
+ expectedAdKey: "example:organic",
+ adUrls: ["https://www.example.com/ad2"],
+ nonAdUrls: ["https://www.example.com/ad3"],
+ impression: {
+ provider: "example",
+ tagged: "false",
+ partner_code: "foo",
+ is_shopping_page: "false",
+ shopping_tab_displayed: "false",
+ source: "unknown",
+ },
+ },
+ {
+ title: "Organic search non-matched code",
+ trackingUrl: "https://www.example.com/search?q=test&abc=ff123",
+ expectedSearchCountEntry: "example:organic:other",
+ expectedAdKey: "example:organic",
+ adUrls: ["https://www.example.com/ad2"],
+ nonAdUrls: ["https://www.example.com/ad3"],
+ impression: {
+ provider: "example",
+ tagged: "false",
+ partner_code: "other",
+ is_shopping_page: "false",
+ shopping_tab_displayed: "false",
+ source: "unknown",
+ },
+ },
+ {
+ title: "Organic search non-matched code 2",
+ trackingUrl: "https://www.example.com/search?q=test&abc=foo123",
+ expectedSearchCountEntry: "example:organic:other",
+ expectedAdKey: "example:organic",
+ adUrls: ["https://www.example.com/ad2"],
+ nonAdUrls: ["https://www.example.com/ad3"],
+ impression: {
+ provider: "example",
+ tagged: "false",
+ partner_code: "other",
+ is_shopping_page: "false",
+ shopping_tab_displayed: "false",
+ source: "unknown",
+ },
+ },
+ {
+ title: "Organic search expected organic matched code",
+ trackingUrl: "https://www.example.com/search?q=test&abc=baz",
+ expectedSearchCountEntry: "example:organic:none",
+ expectedAdKey: "example:organic",
+ adUrls: ["https://www.example.com/ad2"],
+ nonAdUrls: ["https://www.example.com/ad3"],
+ impression: {
+ provider: "example",
+ tagged: "false",
+ partner_code: "",
+ is_shopping_page: "false",
+ shopping_tab_displayed: "false",
+ source: "unknown",
+ },
+ },
+ {
+ title: "Organic search no codes",
+ trackingUrl: "https://www.example.com/search?q=test",
+ expectedSearchCountEntry: "example:organic:none",
+ expectedAdKey: "example:organic",
+ adUrls: ["https://www.example.com/ad2"],
+ nonAdUrls: ["https://www.example.com/ad3"],
+ impression: {
+ provider: "example",
+ tagged: "false",
+ partner_code: "",
+ is_shopping_page: "false",
+ shopping_tab_displayed: "false",
+ source: "unknown",
+ },
+ },
+ {
+ title: "Different engines using the same adUrl",
+ trackingUrl: "https://www.example2.com/search?q=test",
+ expectedSearchCountEntry: "example2:organic:none",
+ expectedAdKey: "example2:organic",
+ adUrls: ["https://www.example.com/ad2"],
+ nonAdUrls: ["https://www.example.com/ad3"],
+ impression: {
+ provider: "example2",
+ tagged: "false",
+ partner_code: "",
+ is_shopping_page: "false",
+ shopping_tab_displayed: "false",
+ source: "unknown",
+ },
+ },
+];
+
+/**
+ * This function is primarily for testing the Ad URL regexps that are triggered
+ * when a URL is clicked on. These regexps are also used for the `withads`
+ * probe. However, we test the adclicks route as that is easier to hit.
+ *
+ * @param {string} serpUrl
+ * The url to simulate where the page the click came from.
+ * @param {string} adUrl
+ * The ad url to simulate being clicked.
+ * @param {string} [expectedAdKey]
+ * The expected key to be logged for the scalar. Omit if no scalar should be
+ * logged.
+ */
+async function testAdUrlClicked(serpUrl, adUrl, expectedAdKey) {
+ info(`Testing Ad URL: ${adUrl}`);
+ let channel = NetUtil.newChannel({
+ uri: NetUtil.newURI(adUrl),
+ triggeringPrincipal: Services.scriptSecurityManager.createContentPrincipal(
+ NetUtil.newURI(serpUrl),
+ {}
+ ),
+ loadUsingSystemPrincipal: true,
+ });
+ SearchSERPTelemetry._contentHandler.observeActivity(
+ channel,
+ Ci.nsIHttpActivityObserver.ACTIVITY_TYPE_HTTP_TRANSACTION,
+ Ci.nsIHttpActivityObserver.ACTIVITY_SUBTYPE_TRANSACTION_CLOSE
+ );
+ // Since the content handler takes a moment to allow the channel information
+ // to settle down, wait the same amount of time here.
+ await new Promise(resolve => Services.tm.dispatchToMainThread(resolve));
+
+ const scalars = TelemetryTestUtils.getProcessScalars("parent", true, true);
+ if (!expectedAdKey) {
+ Assert.ok(
+ !("browser.search.adclicks.unknown" in scalars),
+ "Should not have recorded an ad click"
+ );
+ } else {
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ "browser.search.adclicks.unknown",
+ expectedAdKey,
+ 1
+ );
+ }
+}
+
+do_get_profile();
+
+add_task(async function setup() {
+ Services.prefs.setBoolPref(SearchUtils.BROWSER_SEARCH_PREF + "log", true);
+ Services.prefs.setBoolPref(
+ SearchUtils.BROWSER_SEARCH_PREF + "serpEventTelemetry.enabled",
+ true
+ );
+ Services.fog.initializeFOG();
+ await SearchSERPTelemetry.init();
+ SearchSERPTelemetry.overrideSearchTelemetryForTests(TEST_PROVIDER_INFO);
+ sinon.stub(BrowserSearchTelemetry, "shouldRecordSearchCount").returns(true);
+});
+
+add_task(async function test_parsing_search_urls() {
+ for (const test of TESTS) {
+ info(`Running ${test.title}`);
+ if (test.setUp) {
+ test.setUp();
+ }
+ let browser = {
+ getTabBrowser: () => {},
+ };
+ SearchSERPTelemetry.updateTrackingStatus(browser, test.trackingUrl);
+ SearchSERPTelemetry.reportPageImpression(
+ {
+ url: test.trackingUrl,
+ shoppingTabDisplayed: false,
+ },
+ browser
+ );
+ let scalars = TelemetryTestUtils.getProcessScalars("parent", true, true);
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ "browser.search.content.unknown",
+ test.expectedSearchCountEntry,
+ 1
+ );
+
+ if ("adUrls" in test) {
+ for (const adUrl of test.adUrls) {
+ await testAdUrlClicked(test.trackingUrl, adUrl, test.expectedAdKey);
+ }
+ for (const nonAdUrls of test.nonAdUrls) {
+ await testAdUrlClicked(test.trackingUrl, nonAdUrls);
+ }
+ }
+
+ let recordedEvents = Glean.serp.impression.testGetValue();
+
+ Assert.equal(
+ recordedEvents.length,
+ 1,
+ "should only see one impression event"
+ );
+
+ // To allow deep equality.
+ test.impression.impression_id = recordedEvents[0].extra.impression_id;
+ Assert.deepEqual(recordedEvents[0].extra, test.impression);
+
+ if (test.tearDown) {
+ test.tearDown();
+ }
+
+ // We need to clear Glean events so they don't accumulate for each iteration.
+ Services.fog.testResetFOG();
+ }
+});
diff --git a/browser/components/search/test/unit/xpcshell.ini b/browser/components/search/test/unit/xpcshell.ini
new file mode 100644
index 0000000000..7feeb6d38c
--- /dev/null
+++ b/browser/components/search/test/unit/xpcshell.ini
@@ -0,0 +1,9 @@
+[DEFAULT]
+skip-if = toolkit == 'android' # bug 1730213
+firefox-appdir = browser
+
+[test_search_telemetry_config_validation.js]
+support-files =
+ ../../schema/search-telemetry-schema.json
+[test_urlTelemetry.js]
+[test_urlTelemetry_generic.js]