diff options
Diffstat (limited to 'browser/modules/PartnerLinkAttribution.sys.mjs')
-rw-r--r-- | browser/modules/PartnerLinkAttribution.sys.mjs | 217 |
1 files changed, 217 insertions, 0 deletions
diff --git a/browser/modules/PartnerLinkAttribution.sys.mjs b/browser/modules/PartnerLinkAttribution.sys.mjs new file mode 100644 index 0000000000..4b8135aab7 --- /dev/null +++ b/browser/modules/PartnerLinkAttribution.sys.mjs @@ -0,0 +1,217 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + Region: "resource://gre/modules/Region.sys.mjs", +}); + +XPCOMUtils.defineLazyModuleGetters(lazy, { + PingCentre: "resource:///modules/PingCentre.jsm", +}); + +// Endpoint base URL for Structured Ingestion +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "structuredIngestionEndpointBase", + "browser.newtabpage.activity-stream.telemetry.structuredIngestion.endpoint", + "" +); +const NAMESPACE_CONTEXUAL_SERVICES = "contextual-services"; + +// PingCentre client to send custom pings +XPCOMUtils.defineLazyGetter(lazy, "pingcentre", () => { + return new lazy.PingCentre({ topic: "contextual-services" }); +}); + +// `contextId` is a unique identifier used by Contextual Services +const CONTEXT_ID_PREF = "browser.contextual-services.contextId"; +XPCOMUtils.defineLazyGetter(lazy, "contextId", () => { + let _contextId = Services.prefs.getStringPref(CONTEXT_ID_PREF, null); + if (!_contextId) { + _contextId = String(Services.uuid.generateUUID()); + Services.prefs.setStringPref(CONTEXT_ID_PREF, _contextId); + } + return _contextId; +}); + +export const CONTEXTUAL_SERVICES_PING_TYPES = { + TOPSITES_IMPRESSION: "topsites-impression", + TOPSITES_SELECTION: "topsites-click", + QS_BLOCK: "quicksuggest-block", + QS_IMPRESSION: "quicksuggest-impression", + QS_SELECTION: "quicksuggest-click", +}; + +export var PartnerLinkAttribution = { + /** + * Sends an attribution request to an anonymizing proxy. + * + * @param {string} targetURL + * The URL we are routing through the anonmyzing proxy. + * @param {string} source + * The source of the anonmized request, e.g. "urlbar". + * @param {string} [campaignID] + * The campaign ID for attribution. This should be a valid path on the + * anonymizing proxy. For example, if `campaignID` was `foo`, we'd send an + * attribution request to https://topsites.mozilla.com/cid/foo. + * Optional. If it's not provided, we default to the topsites campaign. + */ + async makeRequest({ targetURL, source, campaignID }) { + let partner = targetURL.match(/^https?:\/\/(?:www.)?([^.]*)/)[1]; + + function record(method, objectString) { + recordTelemetryEvent({ + method, + objectString, + value: partner, + }); + } + record("click", source); + + let attributionUrl = Services.prefs.getStringPref( + "browser.partnerlink.attributionURL" + ); + if (!attributionUrl) { + record("attribution", "abort"); + return; + } + + // The default campaign is topsites. + if (!campaignID) { + campaignID = Services.prefs.getStringPref( + "browser.partnerlink.campaign.topsites" + ); + } + attributionUrl = attributionUrl + campaignID; + let result = await sendRequest(attributionUrl, source, targetURL); + record("attribution", result ? "success" : "failure"); + }, + + /** + * Makes a request to the attribution URL for a search engine search. + * + * @param {nsISearchEngine} engine + * The search engine to save the attribution for. + * @param {nsIURI} targetUrl + * The target URL to filter and include in the attribution. + */ + async makeSearchEngineRequest(engine, targetUrl) { + let cid; + if (engine.attribution?.cid) { + cid = engine.attribution.cid; + } else if (engine.sendAttributionRequest) { + cid = Services.prefs.getStringPref( + "browser.partnerlink.campaign.topsites" + ); + } else { + return; + } + + let searchUrlQueryParamName = engine.searchUrlQueryParamName; + if (!searchUrlQueryParamName) { + console.error("makeSearchEngineRequest can't find search terms key"); + return; + } + + let url = targetUrl; + if (typeof url == "string") { + url = Services.io.newURI(url); + } + + let targetParams = new URLSearchParams(url.query); + if (!targetParams.has(searchUrlQueryParamName)) { + console.error("makeSearchEngineRequest can't remove target search terms"); + return; + } + + let attributionUrl = Services.prefs.getStringPref( + "browser.partnerlink.attributionURL", + "" + ); + attributionUrl = attributionUrl + cid; + + targetParams.delete(searchUrlQueryParamName); + let strippedTargetUrl = `${url.prePath}${url.filePath}`; + let newParams = targetParams.toString(); + if (newParams) { + strippedTargetUrl += "?" + newParams; + } + + 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)) { + console.error("Invalid Contextual Services ping type"); + return; + } + + const endpoint = makeEndpointUrl(pingType, "1"); + payload.context_id = lazy.contextId; + lazy.pingcentre.sendStructuredIngestionPing(payload, endpoint); + }, + + /** + * Gets the underlying PingCentre client, only used for tests. + */ + get _pingCentre() { + return lazy.pingcentre; + }, +}; + +async function sendRequest(attributionUrl, source, targetURL) { + const request = new Request(attributionUrl); + request.headers.set("X-Region", lazy.Region.home); + request.headers.set("X-Source", source); + request.headers.set("X-Target-URL", targetURL); + const response = await fetch(request); + return response.ok; +} + +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 = Services.uuid + .generateUUID() + .toString() + .slice(1, -1); + const extension = `${NAMESPACE_CONTEXUAL_SERVICES}/${pingType}/${version}/${docID}`; + return `${lazy.structuredIngestionEndpointBase}/${extension}`; +} |