gecko-dev/browser/actors/SearchTelemetryChild.jsm

195 строки
5.3 KiB
JavaScript

/* 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/. */
"use strict";
var EXPORTED_SYMBOLS = ["SearchTelemetryChild", "ADLINK_CHECK_TIMEOUT_MS"];
const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
);
XPCOMUtils.defineLazyModuleGetters(this, {
ActorChild: "resource://gre/modules/ActorChild.jsm",
clearTimeout: "resource://gre/modules/Timer.jsm",
Services: "resource://gre/modules/Services.jsm",
setTimeout: "resource://gre/modules/Timer.jsm",
});
const SHARED_DATA_KEY = "SearchTelemetry:ProviderInfo";
const ADLINK_CHECK_TIMEOUT_MS = 1000;
/**
* SearchProviders looks after keeping track of the search provider information
* received from the main process.
*
* It is separate to SearchTelemetryChild so that it is not constructed for each
* tab, but once per process.
*/
class SearchProviders {
constructor() {
this._searchProviderInfo = null;
Services.cpmm.sharedData.addEventListener("change", this);
}
/**
* Gets the search provider information for any provider with advert information.
* If there is nothing in the cache, it will obtain it from shared data.
*
* @returns {object} Returns the search provider information. @see SearchTelemetry.jsm
*/
get info() {
if (this._searchProviderInfo) {
return this._searchProviderInfo;
}
this._searchProviderInfo = Services.cpmm.sharedData.get(SHARED_DATA_KEY);
if (!this._searchProviderInfo) {
return null;
}
// Filter-out non-ad providers so that we're not trying to match against
// those unnecessarily.
for (let [providerName, info] of Object.entries(this._searchProviderInfo)) {
if (!("extraAdServersRegexps" in info)) {
delete this._searchProviderInfo[providerName];
}
}
return this._searchProviderInfo;
}
/**
* Handles events received from sharedData notifications.
*
* @param {object} event The event details.
*/
handleEvent(event) {
switch (event.type) {
case "change": {
if (event.changedKeys.includes(SHARED_DATA_KEY)) {
// Just null out the provider information for now, we'll fetch it next
// time we need it.
this._searchProviderInfo = null;
}
break;
}
}
}
}
const searchProviders = new SearchProviders();
/**
* SearchTelemetryChild monitors for pages that are partner searches, and
* looks through them to find links which looks like adverts and sends back
* a notification to SearchTelemetry for possible telemetry reporting.
*
* Only the partner details and the fact that at least one ad was found on the
* page are returned to SearchTelemetry. If no ads are found, no notification is
* given.
*/
class SearchTelemetryChild extends ActorChild {
/**
* Determines if there is a provider that matches the supplied URL and returns
* the information associated with that provider.
*
* @param {string} url The url to check
* @returns {array|null} Returns null if there's no match, otherwise an array
* of provider name and the provider information.
*/
_getProviderInfoForUrl(url) {
return Object.entries(searchProviders.info || []).find(([_, info]) =>
info.regexp.test(url)
);
}
/**
* Checks to see if the page is a partner and has an ad link within it. If so,
* it will notify SearchTelemetry.
*/
_checkForAdLink() {
if (!this.content) {
return;
}
let doc = this.content.document;
let url = doc.documentURI;
let providerInfo = this._getProviderInfoForUrl(url);
if (!providerInfo) {
return;
}
let regexps = providerInfo[1].extraAdServersRegexps;
let anchors = doc.getElementsByTagName("a");
let hasAds = false;
for (let anchor of anchors) {
if (!anchor.href) {
continue;
}
for (let regexp of regexps) {
if (regexp.test(anchor.href)) {
hasAds = true;
break;
}
}
if (hasAds) {
break;
}
}
if (hasAds) {
this.sendAsyncMessage("SearchTelemetry:PageInfo", {
hasAds: true,
url,
});
}
}
/**
* Handles events received from the actor child notifications.
*
* @param {object} event The event details.
*/
handleEvent(event) {
// We are only interested in the top-level frame.
if (event.target.ownerGlobal != this.content) {
return;
}
const cancelCheck = () => {
if (this._waitForContentTimeout) {
clearTimeout(this._waitForContentTimeout);
}
};
const check = () => {
cancelCheck();
this._waitForContentTimeout = setTimeout(() => {
this._checkForAdLink();
}, ADLINK_CHECK_TIMEOUT_MS);
};
switch (event.type) {
case "pageshow": {
// If a page is loaded from the bfcache, we won't get a "DOMContentLoaded"
// event, so we need to rely on "pageshow" in this case. Note: we do this
// so that we remain consistent with the *.in-content:sap* count for the
// SEARCH_COUNTS histogram.
if (event.persisted) {
check();
}
break;
}
case "DOMContentLoaded": {
check();
break;
}
case "unload": {
cancelCheck();
break;
}
}
}
}