summaryrefslogtreecommitdiffstats
path: root/browser/modules/PartnerLinkAttribution.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'browser/modules/PartnerLinkAttribution.sys.mjs')
-rw-r--r--browser/modules/PartnerLinkAttribution.sys.mjs217
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}`;
+}