From 59c6a49f3a63d958daf555313d4f533a6dde1d6c Mon Sep 17 00:00:00 2001 From: Nan Jiang Date: Wed, 24 Feb 2021 16:57:27 +0000 Subject: [PATCH] Bug 1693393 - add telemetry for sponsored TopSites in Urlbar r=dao,harry Differential Revision: https://phabricator.services.mozilla.com/D105639 --- browser/components/urlbar/UrlbarInput.jsm | 24 +++ .../urlbar/UrlbarProviderTopSites.jsm | 125 +++++++++++-- browser/components/urlbar/UrlbarUtils.jsm | 6 + browser/components/urlbar/docs/telemetry.rst | 63 +++++++ .../urlbar/tests/browser/browser.ini | 2 + ...ser_urlbar_telemetry_sponsored_topsites.js | 177 ++++++++++++++++++ browser/modules/PartnerLinkAttribution.jsm | 98 +++++++++- .../test/unit/test_PartnerLinkAttribution.js | 52 +++++ browser/modules/test/unit/xpcshell.ini | 1 + 9 files changed, 527 insertions(+), 21 deletions(-) create mode 100644 browser/components/urlbar/tests/browser/browser_urlbar_telemetry_sponsored_topsites.js create mode 100644 browser/modules/test/unit/test_PartnerLinkAttribution.js diff --git a/browser/components/urlbar/UrlbarInput.jsm b/browser/components/urlbar/UrlbarInput.jsm index 0103dfd722be..a94b8da44d5f 100644 --- a/browser/components/urlbar/UrlbarInput.jsm +++ b/browser/components/urlbar/UrlbarInput.jsm @@ -14,6 +14,8 @@ XPCOMUtils.defineLazyModuleGetters(this, { AppConstants: "resource://gre/modules/AppConstants.jsm", BrowserSearchTelemetry: "resource:///modules/BrowserSearchTelemetry.jsm", BrowserUIUtils: "resource:///modules/BrowserUIUtils.jsm", + CONTEXTUAL_SERVICES_PING_TYPES: + "resource:///modules/PartnerLinkAttribution.jsm", ExtensionSearchHandler: "resource://gre/modules/ExtensionSearchHandler.jsm", ObjectUtils: "resource://gre/modules/ObjectUtils.jsm", PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm", @@ -44,6 +46,9 @@ XPCOMUtils.defineLazyServiceGetter( const DEFAULT_FORM_HISTORY_NAME = "searchbar-history"; const SEARCH_BUTTON_ID = "urlbar-search-button"; +// The scalar category of TopSites click for Contextual Services +const SCALAR_CATEGORY_TOPSITES = "contextual.services.topsites.click"; + let getBoundsWithoutFlushing = element => element.ownerGlobal.windowUtils.getBoundsWithoutFlushing(element); let px = number => number.toFixed(2) + "px"; @@ -967,6 +972,25 @@ class UrlbarInput { "browser.partnerlink.campaign.topsites" ), }); + if (!this.isPrivate && result.providerName === "UrlbarProviderTopSites") { + // The position is 1-based for telemetry + const position = selIndex + 1; + Services.telemetry.keyedScalarAdd( + SCALAR_CATEGORY_TOPSITES, + `urlbar_${position}`, + 1 + ); + PartnerLinkAttribution.sendContextualServicesPing( + { + position, + source: "urlbar", + tile_id: result.payload.sponsoredTileId || -1, + reporting_url: result.payload.sponsoredClickUrl, + advertiser: result.payload.title.toLocaleLowerCase(), + }, + CONTEXTUAL_SERVICES_PING_TYPES.TOPSITES_SELECTION + ); + } } this._loadURL( diff --git a/browser/components/urlbar/UrlbarProviderTopSites.jsm b/browser/components/urlbar/UrlbarProviderTopSites.jsm index 37d3e3c87386..61b8833b3beb 100644 --- a/browser/components/urlbar/UrlbarProviderTopSites.jsm +++ b/browser/components/urlbar/UrlbarProviderTopSites.jsm @@ -12,6 +12,9 @@ const { XPCOMUtils } = ChromeUtils.import( XPCOMUtils.defineLazyModuleGetters(this, { AboutNewTab: "resource:///modules/AboutNewTab.jsm", + CONTEXTUAL_SERVICES_PING_TYPES: + "resource:///modules/PartnerLinkAttribution.jsm", + PartnerLinkAttribution: "resource:///modules/PartnerLinkAttribution.jsm", PlacesUtils: "resource://gre/modules/PlacesUtils.jsm", PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm", Services: "resource://gre/modules/Services.jsm", @@ -25,6 +28,9 @@ XPCOMUtils.defineLazyModuleGetters(this, { TOP_SITES_DEFAULT_ROWS: "resource://activity-stream/common/Reducers.jsm", }); +// The scalar category of TopSites impression for Contextual Services +const SCALAR_CATEGORY_TOPSITES = "contextual.services.topsites.impression"; + /** * This module exports a provider returning the user's newtab Top Sites. */ @@ -137,33 +143,70 @@ class ProviderTopSites extends UrlbarProvider { ); sites = sites.slice(0, numTopSites); - sites = sites.map(link => ({ - type: link.searchTopSite ? "search" : "url", - url: link.url_urlbar || link.url, - isPinned: !!link.isPinned, - isSponsored: !!link.sponsored_position, - // The newtab page allows the user to set custom site titles, which - // are stored in `label`, so prefer it. Search top sites currently - // don't have titles but `hostname` instead. - title: link.label || link.title || link.hostname || "", - favicon: link.smallFavicon || link.favicon || undefined, - sendAttributionRequest: !!link.sendAttributionRequest, - })); + let sponsoredSites = []; + let index = 1; + sites = sites.map(link => { + let site = { + type: link.searchTopSite ? "search" : "url", + url: link.url_urlbar || link.url, + isPinned: !!link.isPinned, + isSponsored: !!link.sponsored_position, + // The newtab page allows the user to set custom site titles, which + // are stored in `label`, so prefer it. Search top sites currently + // don't have titles but `hostname` instead. + title: link.label || link.title || link.hostname || "", + favicon: link.smallFavicon || link.favicon || undefined, + sendAttributionRequest: !!link.sendAttributionRequest, + }; + if (site.isSponsored) { + let { + sponsored_tile_id, + sponsored_impression_url, + sponsored_click_url, + } = link; + site = { + ...site, + sponsoredTileId: sponsored_tile_id, + sponsoredImpressionUrl: sponsored_impression_url, + sponsoredClickUrl: sponsored_click_url, + position: index, + }; + sponsoredSites.push(site); + } + index++; + return site; + }); + + // Store Sponsored Top Sites so we can use it in `onEngagement` + if (sponsoredSites.length) { + this.sponsoredSites = sponsoredSites; + } for (let site of sites) { switch (site.type) { case "url": { + let payload = { + title: site.title, + url: site.url, + icon: site.favicon, + isPinned: site.isPinned, + isSponsored: site.isSponsored, + sendAttributionRequest: site.sendAttributionRequest, + }; + if (site.isSponsored) { + payload = { + ...payload, + sponsoredTileId: site.sponsoredTileId, + sponsoredClickUrl: site.sponsoredClickUrl, + }; + } let result = new UrlbarResult( UrlbarUtils.RESULT_TYPE.URL, UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, - ...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, { - title: site.title, - url: site.url, - icon: site.favicon, - isPinned: site.isPinned, - isSponsored: site.isSponsored, - sendAttributionRequest: site.sendAttributionRequest, - }) + ...UrlbarResult.payloadAndSimpleHighlights( + queryContext.tokens, + payload + ) ); let allowTabSwitch = @@ -244,6 +287,48 @@ class ProviderTopSites extends UrlbarProvider { } } } + + /** + * Called when the user starts and ends an engagement with the urlbar. We send + * the impression ping for the sponsored TopSites, the impression scalar is + * recorded as well. + * + * Note: + * * No telemetry recording in private browsing mode + * * The impression is only recorded for the "engagement" and "abandonment" + * states + * + * @param {boolean} isPrivate True if the engagement is in a private context. + * @param {string} state The state of the engagement, one of: start, + * engagement, abandonment, discard. + */ + onEngagement(isPrivate, state) { + if ( + !isPrivate && + this.sponsoredSites && + ["engagement", "abandonment"].includes(state) + ) { + for (let site of this.sponsoredSites) { + Services.telemetry.keyedScalarAdd( + SCALAR_CATEGORY_TOPSITES, + `urlbar_${site.position}`, + 1 + ); + PartnerLinkAttribution.sendContextualServicesPing( + { + source: "urlbar", + tile_id: site.sponsoredTileId || -1, + position: site.position, + reporting_url: site.sponsoredImpressionUrl, + advertiser: site.title.toLocaleLowerCase(), + }, + CONTEXTUAL_SERVICES_PING_TYPES.TOPSITES_IMPRESSION + ); + } + } + + this.sponsoredSites = null; + } } var UrlbarProviderTopSites = new ProviderTopSites(); diff --git a/browser/components/urlbar/UrlbarUtils.jsm b/browser/components/urlbar/UrlbarUtils.jsm index 6afed8487929..d516ab2ee340 100644 --- a/browser/components/urlbar/UrlbarUtils.jsm +++ b/browser/components/urlbar/UrlbarUtils.jsm @@ -1198,6 +1198,12 @@ UrlbarUtils.RESULT_PAYLOAD_SCHEMA = { sendAttributionRequest: { type: "boolean", }, + sponsoredClickUrl: { + type: "string", + }, + sponsoredTileId: { + type: "number", + }, tags: { type: "array", items: { diff --git a/browser/components/urlbar/docs/telemetry.rst b/browser/components/urlbar/docs/telemetry.rst index f77b6acd22c9..3a3e24517cc7 100644 --- a/browser/components/urlbar/docs/telemetry.rst +++ b/browser/components/urlbar/docs/telemetry.rst @@ -358,6 +358,61 @@ Event Extra .. _URLBar provider experiments: experiments.html#developing-address-bar-extensions + +Custom pings for Contextual Services +------------------------------------ + +Contextual Services currently has two features running within the Urlbar: TopSites +and QuickSuggest. We send various pings as the `custom pings`_ to record the impressions +and clicks of these two features. + + .. _custom pings: https://docs.telemetry.mozilla.org/cookbooks/new_ping.html#sending-a-custom-ping + +TopSites Impression + This records an impression when a sponsored TopSite is shown. + + - ``context_id`` + A UUID representing this user. Note that it's not client_id, nor can it be used to link to a client_id. + - ``tile_id`` + A unique identifier for the sponsored TopSite. + - ``source`` + The browser location where the impression was displayed. + - ``position`` + The placement of the TopSite (1-based). + - ``advertiser`` + The Name of the advertiser. + - ``reporting_url`` + The reporting URL of the sponsored TopSite, normally pointing to the ad partner's reporting endpoint. + - ``version`` + Firefox version. + - ``release_channel`` + Firefox release channel. + - ``locale`` + User's current locale. + +TopSites Click + This records a click ping when a sponsored TopSite is clicked by the user. + + - ``context_id`` + A UUID representing this user. Note that it's not client_id, nor can it be used to link to a client_id. + - ``tile_id`` + A unique identifier for the sponsored TopSite. + - ``source`` + The browser location where the click was tirggered. + - ``position`` + The placement of the TopSite (1-based). + - ``advertiser`` + The Name of the advertiser. + - ``reporting_url`` + The reporting URL of the sponsored TopSite, normally pointing to the ad partner's reporting endpoint. + - ``version`` + Firefox version. + - ``release_channel`` + Firefox release channel. + - ``locale`` + User's current locale. + + Search probes relevant to the Address Bar ----------------------------------------- @@ -421,6 +476,14 @@ browser.engagement.navigation.* For ``urlbar`` or ``searchbar``, indicates the user confirmed a search suggestion. +contextual.services.topsites.* + These keyed scalars instrument the impressions and clicks for sponsored TopSites + in the urlbar. + The key is a combination of the source and the placement of the TopSites link + (1-based) such as 'urlbar_1'. For each key, it records the counter of the + impression or click. + Note that these scalars are shared with the TopSites on the newtab page. + Obsolete probes --------------- diff --git a/browser/components/urlbar/tests/browser/browser.ini b/browser/components/urlbar/tests/browser/browser.ini index e33bbb05a7b4..0168f11f03b7 100644 --- a/browser/components/urlbar/tests/browser/browser.ini +++ b/browser/components/urlbar/tests/browser/browser.ini @@ -270,6 +270,8 @@ tags = search-telemetry support-files = urlbarTelemetrySearchSuggestions.sjs urlbarTelemetrySearchSuggestions.xml +[browser_urlbar_telemetry_sponsored_topsites.js] +tags = search-telemetry [browser_urlbar_telemetry_tabtosearch.js] tags = search-telemetry [browser_urlbar_telemetry_tip.js] diff --git a/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_sponsored_topsites.js b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_sponsored_topsites.js new file mode 100644 index 000000000000..4362ae2c0e44 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_sponsored_topsites.js @@ -0,0 +1,177 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +XPCOMUtils.defineLazyModuleGetters(this, { + CONTEXTUAL_SERVICES_PING_TYPES: + "resource:///modules/PartnerLinkAttribution.jsm", + HttpServer: "resource://testing-common/httpd.js", + PartnerLinkAttribution: "resource:///modules/PartnerLinkAttribution.jsm", + NewTabUtils: "resource://gre/modules/NewTabUtils.jsm", +}); + +const EN_US_TOPSITES = + "https://www.youtube.com/,https://www.facebook.com/,https://www.amazon.com/,https://www.reddit.com/,https://www.wikipedia.org/,https://twitter.com/"; + +// This is used for "sendAttributionRequest" +var gHttpServer = null; +var gRequests = []; + +function submitHandler(request, response) { + gRequests.push(request); + response.setStatusLine(request.httpVersion, 200, "Ok"); +} + +// Spy for telemetry sender +let spy; + +add_task(async function setup() { + sandbox = sinon.createSandbox(); + spy = sandbox.spy( + PartnerLinkAttribution._pingCentre, + "sendStructuredIngestionPing" + ); + + let topsitesAttribution = Services.prefs.getStringPref( + "browser.partnerlink.campaign.topsites" + ); + gHttpServer = new HttpServer(); + gHttpServer.registerPathHandler(`/cid/${topsitesAttribution}`, submitHandler); + gHttpServer.start(-1); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.suggest.topsites", true], + ["browser.newtabpage.activity-stream.default.sites", EN_US_TOPSITES], + [ + "browser.partnerlink.attributionURL", + `http://localhost:${gHttpServer.identity.primaryPort}/cid/`, + ], + ], + }); + + await updateTopSites( + sites => sites && sites.length == EN_US_TOPSITES.split(",").length + ); + + registerCleanupFunction(async () => { + sandbox.restore(); + await gHttpServer.stop(); + gHttpServer = null; + }); +}); + +add_task(async function send_impression_and_click() { + await BrowserTestUtils.withNewTab("about:blank", async () => { + let link = { + label: "test_label", + url: "http://example.com/", + sponsored_position: 1, + sendAttributionRequest: true, + sponsored_tile_id: 42, + sponsored_impression_url: "http://impression.test.com/", + sponsored_click_url: "http://click.test.com/", + }; + // Pin a sponsored TopSite to set up the test fixture + NewTabUtils.pinnedLinks.pin(link, 0); + + await updateTopSites(sites => sites && sites[0] && sites[0].isPinned); + + await UrlbarTestUtils.promisePopupOpen(window, () => { + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}); + }); + + await UrlbarTestUtils.promiseSearchComplete(window); + + // Select the first result and confirm it. + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + EventUtils.synthesizeKey("KEY_ArrowDown"); + + let loadPromise = BrowserTestUtils.waitForDocLoadAndStopIt( + result.url, + gBrowser.selectedBrowser + ); + EventUtils.synthesizeKey("KEY_Enter"); + await loadPromise; + + Assert.ok( + spy.calledTwice, + "Should send an impression ping and a click ping" + ); + + // Validate the impression ping + let [payload, endpoint] = spy.firstCall.args; + Assert.ok( + endpoint.includes(CONTEXTUAL_SERVICES_PING_TYPES.TOPSITES_IMPRESSION), + "Should set the endpoint for TopSites impression" + ); + Assert.ok(!!payload.context_id, "Should set the context_id"); + Assert.equal(payload.advertiser, "test_label", "Should set the advertiser"); + Assert.equal( + payload.reporting_url, + "http://impression.test.com/", + "Should set the impression reporting URL" + ); + Assert.equal(payload.tile_id, 42, "Should set the tile_id"); + Assert.equal(payload.position, 1, "Should set the position"); + + // Validate the click ping + [payload, endpoint] = spy.secondCall.args; + Assert.ok( + endpoint.includes(CONTEXTUAL_SERVICES_PING_TYPES.TOPSITES_SELECTION), + "Should set the endpoint for TopSites click" + ); + Assert.ok(!!payload.context_id, "Should set the context_id"); + Assert.equal( + payload.reporting_url, + "http://click.test.com/", + "Should set the click reporting URL" + ); + Assert.equal(payload.tile_id, 42, "Should set the tile_id"); + Assert.equal(payload.position, 1, "Should set the position"); + + await UrlbarTestUtils.promisePopupClose(window, () => { + gURLBar.blur(); + }); + + NewTabUtils.pinnedLinks.unpin(link); + }); +}); + +add_task(async function zero_ping() { + await BrowserTestUtils.withNewTab("about:blank", async () => { + spy.resetHistory(); + + // Reload the TopSites + await updateTopSites( + sites => sites && sites.length == EN_US_TOPSITES.split(",").length + ); + + await UrlbarTestUtils.promisePopupOpen(window, () => { + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}); + }); + + await UrlbarTestUtils.promiseSearchComplete(window); + + // Select the first result and confirm it. + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + EventUtils.synthesizeKey("KEY_ArrowDown"); + + let loadPromise = BrowserTestUtils.waitForDocLoadAndStopIt( + result.url, + gBrowser.selectedBrowser + ); + EventUtils.synthesizeKey("KEY_Enter"); + await loadPromise; + + Assert.ok( + spy.notCalled, + "Should not send any ping if there is no sponsored Top Site" + ); + + await UrlbarTestUtils.promisePopupClose(window, () => { + gURLBar.blur(); + }); + }); +}); diff --git a/browser/modules/PartnerLinkAttribution.jsm b/browser/modules/PartnerLinkAttribution.jsm index 8ba58fd865e6..a15d98eaf1b8 100644 --- a/browser/modules/PartnerLinkAttribution.jsm +++ b/browser/modules/PartnerLinkAttribution.jsm @@ -6,7 +6,10 @@ Cu.importGlobalProperties(["fetch"]); -var EXPORTED_SYMBOLS = ["PartnerLinkAttribution"]; +var EXPORTED_SYMBOLS = [ + "PartnerLinkAttribution", + "CONTEXTUAL_SERVICES_PING_TYPES", +]; const { XPCOMUtils } = ChromeUtils.import( "resource://gre/modules/XPCOMUtils.jsm" @@ -15,8 +18,45 @@ const { XPCOMUtils } = ChromeUtils.import( XPCOMUtils.defineLazyModuleGetters(this, { Services: "resource://gre/modules/Services.jsm", Region: "resource://gre/modules/Region.jsm", + PingCentre: "resource:///modules/PingCentre.jsm", }); +XPCOMUtils.defineLazyServiceGetters(this, { + gUUIDGenerator: ["@mozilla.org/uuid-generator;1", "nsIUUIDGenerator"], +}); + +// Endpoint base URL for Structured Ingestion +XPCOMUtils.defineLazyPreferenceGetter( + this, + "structuredIngestionEndpointBase", + "browser.newtabpage.activity-stream.telemetry.structuredIngestion.endpoint", + "" +); +const NAMESPACE_CONTEXUAL_SERVICES = "contextual-services"; + +// PingCentre client to send custom pings +XPCOMUtils.defineLazyGetter(this, "pingcentre", () => { + return new PingCentre({ topic: "contextual-services" }); +}); + +// `contextId` is a unique identifier used by Contextual Services +const CONTEXT_ID_PREF = "browser.contextual-services.contextId"; +XPCOMUtils.defineLazyGetter(this, "contextId", () => { + let _contextId = Services.prefs.getStringPref(CONTEXT_ID_PREF, null); + if (!_contextId) { + _contextId = String(gUUIDGenerator.generateUUID()); + Services.prefs.setStringPref(CONTEXT_ID_PREF, _contextId); + } + return _contextId; +}); + +const CONTEXTUAL_SERVICES_PING_TYPES = { + TOPSITES_IMPRESSION: "topsites-impression", + TOPSITES_SELECTION: "topsites-click", + QS_IMPRESSION: "quicksuggest-impression", + QS_SELECTION: "quicksuggest-click", +}; + var PartnerLinkAttribution = { /** * Sends an attribution request to an anonymizing proxy. @@ -116,6 +156,39 @@ var PartnerLinkAttribution = { await sendRequest(attributionUrl, "searchurl", strippedTargetUrl); }, + + /** + * Sends a Contextual Services ping to the Mozilla data pipeline. + * + * Note: + * * All Contextual Services pings are sent as custom pings + * (https://docs.telemetry.mozilla.org/cookbooks/new_ping.html#sending-a-custom-ping) + * + * * The full event list can be found at https://github.com/mozilla-services/mozilla-pipeline-schemas + * under the "contextual-services" namespace + * + * @param {object} payload + * The ping payload to be sent to the Mozilla Structured Ingestion endpoint + * @param {String} pingType + * The ping type. Must be one of CONTEXTUAL_SERVICES_PING_TYPES + */ + sendContextualServicesPing(payload, pingType) { + if (!Object.values(CONTEXTUAL_SERVICES_PING_TYPES).includes(pingType)) { + Cu.reportError("Invalid Contextual Services ping type"); + return; + } + + const endpoint = makeEndpointUrl(pingType, "1"); + payload.context_id = contextId; + pingcentre.sendStructuredIngestionPing(payload, endpoint); + }, + + /** + * Gets the underlying PingCentre client, only used for tests. + */ + get _pingCentre() { + return pingcentre; + }, }; async function sendRequest(attributionUrl, source, targetURL) { @@ -131,3 +204,26 @@ function recordTelemetryEvent({ method, objectString, value }) { Services.telemetry.setEventRecordingEnabled("partner_link", true); Services.telemetry.recordEvent("partner_link", method, objectString, value); } + +/** + * Makes a new endpoint URL for a ping submission. Note that each submission + * to Structured Ingesttion requires a new endpoint. See more details about + * the specs: + * + * https://docs.telemetry.mozilla.org/concepts/pipeline/http_edge_spec.html?highlight=docId#postput-request + * + * @param {String} pingType + * The ping type. Must be one of CONTEXTUAL_SERVICES_PING_TYPES + * @param {String} version + * The schema version of the ping. + */ +function makeEndpointUrl(pingType, version) { + // Structured Ingestion does not support the UUID generated by gUUIDGenerator. + // Stripping off the leading and trailing braces to make it happy. + const docID = gUUIDGenerator + .generateUUID() + .toString() + .slice(1, -1); + const extension = `${NAMESPACE_CONTEXUAL_SERVICES}/${pingType}/${version}/${docID}`; + return `${structuredIngestionEndpointBase}/${extension}`; +} diff --git a/browser/modules/test/unit/test_PartnerLinkAttribution.js b/browser/modules/test/unit/test_PartnerLinkAttribution.js new file mode 100644 index 000000000000..449c8fb7c2cd --- /dev/null +++ b/browser/modules/test/unit/test_PartnerLinkAttribution.js @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +"use strict"; + +const { + PartnerLinkAttribution, + CONTEXTUAL_SERVICES_PING_TYPES, +} = ChromeUtils.import("resource:///modules/PartnerLinkAttribution.jsm"); + +const { sinon } = ChromeUtils.import("resource://testing-common/Sinon.jsm"); + +const FAKE_PING = { tile_id: 1, position: 1 }; + +let sandbox; +let stub; + +add_task(function setup() { + sandbox = sinon.createSandbox(); + stub = sandbox.stub( + PartnerLinkAttribution._pingCentre, + "sendStructuredIngestionPing" + ); + stub.returns(200); + + registerCleanupFunction(() => { + sandbox.restore(); + }); +}); + +add_task(function test_sendContextualService_success() { + for (const type of Object.values(CONTEXTUAL_SERVICES_PING_TYPES)) { + PartnerLinkAttribution.sendContextualServicesPing(FAKE_PING, type); + + Assert.ok(stub.calledOnce, `Should send the ping for ${type}`); + + const [payload, endpoint] = stub.firstCall.args; + Assert.ok(!!payload.context_id, "Should add context_id to the payload"); + Assert.ok( + endpoint.includes(type), + "Should include the ping type in the endpoint URL" + ); + stub.resetHistory(); + } +}); + +add_task(function test_rejectUnknownPingType() { + PartnerLinkAttribution.sendContextualServicesPing(FAKE_PING, "unknown-type"); + + Assert.ok(stub.notCalled, "Should not send the ping with unknown ping type"); +}); diff --git a/browser/modules/test/unit/xpcshell.ini b/browser/modules/test/unit/xpcshell.ini index c9ce370451b5..c30914a5acbd 100644 --- a/browser/modules/test/unit/xpcshell.ini +++ b/browser/modules/test/unit/xpcshell.ini @@ -16,3 +16,4 @@ skip-if = toolkit == 'android' skip-if = os != 'win' # Test of a Windows-specific feature [test_InstallationTelemetry.js] skip-if = os != 'win' # Test of a Windows-specific feature +[test_PartnerLinkAttribution.js]