Files
tubestation/browser/components/urlbar/QuickSuggest.sys.mjs
Drew Willcoxon 53f88e7241 Bug 1798595 - Refactor quick suggest [Part 1]: Move core code out of UrlbarProviderQuickSuggest into a new QuickSuggest module. r=daisuke
This does the following:

* Copies UrlbarProviderQuickSuggest.sys.mjs to a new QuickSuggest.sys.mjs file
* Removes everything related to UrlbarProvider from QuickSuggest
* Removes most everything not related to UrlbarProvider from
  UrlbarProviderQuickSuggest
* Updates consumers to use the new QuickSuggest module

Please see bug 1798595 for details.

Differential Revision: https://phabricator.services.mozilla.com/D160982
2022-11-02 06:50:13 +00:00

854 lines
30 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/. */
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
import {
TaskQueue,
UrlbarUtils,
} from "resource:///modules/UrlbarUtils.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
UrlbarQuickSuggest: "resource:///modules/UrlbarQuickSuggest.sys.mjs",
clearInterval: "resource://gre/modules/Timer.sys.mjs",
setInterval: "resource://gre/modules/Timer.sys.mjs",
});
XPCOMUtils.defineLazyModuleGetters(lazy, {
AsyncShutdown: "resource://gre/modules/AsyncShutdown.jsm",
NimbusFeatures: "resource://nimbus/ExperimentAPI.jsm",
});
const TIMESTAMP_TEMPLATE = "%YYYYMMDDHH%";
const TIMESTAMP_LENGTH = 10;
const TIMESTAMP_REGEXP = /^\d{10}$/;
const IMPRESSION_COUNTERS_RESET_INTERVAL_MS = 60 * 60 * 1000; // 1 hour
const TELEMETRY_EVENT_CATEGORY = "contextservices.quicksuggest";
// This object maps impression stats object keys to their corresponding keys in
// the `extra` object of impression cap telemetry events. The main reason this
// is necessary is because the keys of the `extra` object are limited to 15
// characters in length, which some stats object keys exceed. It also forces us
// to be deliberate about keys we add to the `extra` object, since the `extra`
// object is limited to 10 keys.
let TELEMETRY_IMPRESSION_CAP_EXTRA_KEYS = {
// stats object key -> `extra` telemetry event object key
intervalSeconds: "intervalSeconds",
startDateMs: "startDate",
count: "count",
maxCount: "maxCount",
impressionDateMs: "impressionDate",
};
/**
* This class manages the quick suggest feature (a.k.a Firefox Suggest) and has
* related helpers.
*/
class _QuickSuggest {
constructor() {
lazy.UrlbarQuickSuggest.on("config-set", () =>
this._validateImpressionStats()
);
this._updateFeatureState();
lazy.NimbusFeatures.urlbar.onUpdate(() => this._updateFeatureState());
lazy.UrlbarPrefs.addObserver(this);
// Periodically record impression counters reset telemetry.
this._setImpressionCountersResetInterval();
// On shutdown, record any final impression counters reset telemetry.
lazy.AsyncShutdown.profileChangeTeardown.addBlocker(
"QuickSuggest: Record impression counters reset telemetry",
() => this._resetElapsedImpressionCounters()
);
}
/**
* @returns {string} The name of the quick suggest telemetry event category.
*/
get TELEMETRY_EVENT_CATEGORY() {
return TELEMETRY_EVENT_CATEGORY;
}
/**
* @returns {string} The timestamp template string used in quick suggest URLs.
*/
get TIMESTAMP_TEMPLATE() {
return TIMESTAMP_TEMPLATE;
}
/**
* @returns {number} The length of the timestamp in quick suggest URLs.
*/
get TIMESTAMP_LENGTH() {
return TIMESTAMP_LENGTH;
}
get logger() {
if (!this._logger) {
this._logger = UrlbarUtils.getLogger({ prefix: "QuickSuggest" });
}
return this._logger;
}
/**
* Blocks a suggestion.
*
* @param {string} originalUrl
* The suggestion's original URL with its unreplaced timestamp template.
*/
async blockSuggestion(originalUrl) {
this.logger.debug(`Queueing blockSuggestion: ${originalUrl}`);
await this._blockTaskQueue.queue(async () => {
this.logger.info(`Blocking suggestion: ${originalUrl}`);
let digest = await this._getDigest(originalUrl);
this.logger.debug(`Got digest for '${originalUrl}': ${digest}`);
this._blockedDigests.add(digest);
let json = JSON.stringify([...this._blockedDigests]);
this._updatingBlockedDigests = true;
try {
lazy.UrlbarPrefs.set("quicksuggest.blockedDigests", json);
} finally {
this._updatingBlockedDigests = false;
}
this.logger.debug(`All blocked suggestions: ${json}`);
});
}
/**
* Gets whether a suggestion is blocked.
*
* @param {string} originalUrl
* The suggestion's original URL with its unreplaced timestamp template.
* @returns {boolean}
* Whether the suggestion is blocked.
*/
async isSuggestionBlocked(originalUrl) {
this.logger.debug(`Queueing isSuggestionBlocked: ${originalUrl}`);
return this._blockTaskQueue.queue(async () => {
this.logger.info(`Getting blocked status: ${originalUrl}`);
let digest = await this._getDigest(originalUrl);
this.logger.debug(`Got digest for '${originalUrl}': ${digest}`);
let isBlocked = this._blockedDigests.has(digest);
this.logger.info(`Blocked status for '${originalUrl}': ${isBlocked}`);
return isBlocked;
});
}
/**
* Unblocks all suggestions.
*/
async clearBlockedSuggestions() {
this.logger.debug(`Queueing clearBlockedSuggestions`);
await this._blockTaskQueue.queue(() => {
this.logger.info(`Clearing all blocked suggestions`);
this._blockedDigests.clear();
lazy.UrlbarPrefs.clear("quicksuggest.blockedDigests");
});
}
/**
* Called when a urlbar pref changes.
*
* @param {string} pref
* The name of the pref relative to `browser.urlbar`.
*/
onPrefChanged(pref) {
switch (pref) {
case "quicksuggest.blockedDigests":
if (!this._updatingBlockedDigests) {
this.logger.info(
"browser.urlbar.quicksuggest.blockedDigests changed"
);
this._loadBlockedDigests();
}
break;
case "quicksuggest.impressionCaps.stats":
if (!this._updatingImpressionStats) {
this.logger.info(
"browser.urlbar.quicksuggest.impressionCaps.stats changed"
);
this._loadImpressionStats();
}
break;
case "quicksuggest.dataCollection.enabled":
if (!lazy.UrlbarPrefs.updatingFirefoxSuggestScenario) {
Services.telemetry.recordEvent(
TELEMETRY_EVENT_CATEGORY,
"data_collect_toggled",
lazy.UrlbarPrefs.get(pref) ? "enabled" : "disabled"
);
}
break;
case "suggest.quicksuggest.nonsponsored":
if (!lazy.UrlbarPrefs.updatingFirefoxSuggestScenario) {
Services.telemetry.recordEvent(
TELEMETRY_EVENT_CATEGORY,
"enable_toggled",
lazy.UrlbarPrefs.get(pref) ? "enabled" : "disabled"
);
}
break;
case "suggest.quicksuggest.sponsored":
if (!lazy.UrlbarPrefs.updatingFirefoxSuggestScenario) {
Services.telemetry.recordEvent(
TELEMETRY_EVENT_CATEGORY,
"sponsored_toggled",
lazy.UrlbarPrefs.get(pref) ? "enabled" : "disabled"
);
}
break;
}
}
/**
* Returns whether a given URL and quick suggest's URL are equivalent. URLs
* are equivalent if they are identical except for substrings that replaced
* templates in the original suggestion URL.
*
* For example, a suggestion URL from the backing suggestions source might
* include a timestamp template "%YYYYMMDDHH%" like this:
*
* http://example.com/foo?bar=%YYYYMMDDHH%
*
* When a quick suggest result is created from this suggestion URL, it's
* created with a URL that is a copy of the suggestion URL but with the
* template replaced with a real timestamp value, like this:
*
* http://example.com/foo?bar=2021111610
*
* All URLs created from this single suggestion URL are considered equivalent
* regardless of their real timestamp values.
*
* @param {string} url
* The URL to check.
* @param {UrlbarResult} result
* The quick suggest result. Will compare {@link url} to `result.payload.url`
* @returns {boolean}
* Whether `url` is equivalent to `result.payload.url`.
*/
isURLEquivalentToResultURL(url, result) {
// If the URLs aren't the same length, they can't be equivalent.
let resultURL = result.payload.url;
if (resultURL.length != url.length) {
return false;
}
// If the result URL doesn't have a timestamp, then do a straight string
// comparison.
let { urlTimestampIndex } = result.payload;
if (typeof urlTimestampIndex != "number" || urlTimestampIndex < 0) {
return resultURL == url;
}
// Compare the first parts of the strings before the timestamps.
if (
resultURL.substring(0, urlTimestampIndex) !=
url.substring(0, urlTimestampIndex)
) {
return false;
}
// Compare the second parts of the strings after the timestamps.
let remainderIndex = urlTimestampIndex + TIMESTAMP_LENGTH;
if (resultURL.substring(remainderIndex) != url.substring(remainderIndex)) {
return false;
}
// Test the timestamp against the regexp.
let maybeTimestamp = url.substring(
urlTimestampIndex,
urlTimestampIndex + TIMESTAMP_LENGTH
);
return TIMESTAMP_REGEXP.test(maybeTimestamp);
}
/**
* Some suggestion properties like `url` and `click_url` include template
* substrings that must be replaced with real values. This method replaces
* templates with appropriate values in place.
*
* @param {object} suggestion
* A suggestion object fetched from remote settings or Merino.
*/
replaceSuggestionTemplates(suggestion) {
let now = new Date();
let timestampParts = [
now.getFullYear(),
now.getMonth() + 1,
now.getDate(),
now.getHours(),
];
let timestamp = timestampParts
.map(n => n.toString().padStart(2, "0"))
.join("");
for (let key of ["url", "click_url"]) {
let value = suggestion[key];
if (!value) {
continue;
}
let timestampIndex = value.indexOf(TIMESTAMP_TEMPLATE);
if (timestampIndex >= 0) {
if (key == "url") {
suggestion.urlTimestampIndex = timestampIndex;
}
// We could use replace() here but we need the timestamp index for
// `suggestion.urlTimestampIndex`, and since we already have that, avoid
// another O(n) substring search and manually replace the template with
// the timestamp.
suggestion[key] =
value.substring(0, timestampIndex) +
timestamp +
value.substring(timestampIndex + TIMESTAMP_TEMPLATE.length);
}
}
}
/**
* Increments the user's impression stats counters for the given type of
* suggestion. This should be called only when a suggestion impression is
* recorded.
*
* @param {string} type
* The suggestion type, one of: "sponsored", "nonsponsored"
*/
updateImpressionStats(type) {
this.logger.info("Starting impression stats update");
this.logger.debug(
JSON.stringify({
type,
currentStats: this._impressionStats,
impression_caps: lazy.UrlbarQuickSuggest.config.impression_caps,
})
);
// Don't bother recording anything if caps are disabled.
let isSponsored = type == "sponsored";
if (
(isSponsored &&
!lazy.UrlbarPrefs.get("quickSuggestImpressionCapsSponsoredEnabled")) ||
(!isSponsored &&
!lazy.UrlbarPrefs.get("quickSuggestImpressionCapsNonSponsoredEnabled"))
) {
this.logger.info("Impression caps disabled, skipping update");
return;
}
// Get the user's impression stats. Since stats are synced from caps, if the
// stats don't exist then the caps don't exist, and don't bother recording
// anything in that case.
let stats = this._impressionStats[type];
if (!stats) {
this.logger.info("Impression caps undefined, skipping update");
return;
}
// Increment counters.
for (let stat of stats) {
stat.count++;
stat.impressionDateMs = Date.now();
// Record a telemetry event for each newly hit cap.
if (stat.count == stat.maxCount) {
this.logger.info(`'${type}' impression cap hit`);
this.logger.debug(JSON.stringify({ type, hitStat: stat }));
this._recordImpressionCapEvent({
stat,
eventType: "hit",
suggestionType: type,
});
}
}
// Save the stats.
this._updatingImpressionStats = true;
try {
lazy.UrlbarPrefs.set(
"quicksuggest.impressionCaps.stats",
JSON.stringify(this._impressionStats)
);
} finally {
this._updatingImpressionStats = false;
}
this.logger.info("Finished impression stats update");
this.logger.debug(JSON.stringify({ newStats: this._impressionStats }));
}
/**
* Returns a non-null value if an impression cap has been reached for the
* given suggestion type and null otherwise. This method can therefore be used
* to tell whether a cap has been reached for a given type. The actual return
* value an object describing the impression stats that caused the cap to be
* reached.
*
* @param {string} type
* The suggestion type, one of: "sponsored", "nonsponsored"
* @returns {object}
* An impression stats object or null.
*/
impressionCapHitStats(type) {
this._resetElapsedImpressionCounters();
let stats = this._impressionStats[type];
if (stats) {
let hitStats = stats.filter(s => s.maxCount <= s.count);
if (hitStats.length) {
return hitStats;
}
}
return null;
}
/**
* Loads and validates impression stats.
*/
_loadImpressionStats() {
let json = lazy.UrlbarPrefs.get("quicksuggest.impressionCaps.stats");
if (!json) {
this._impressionStats = {};
} else {
try {
this._impressionStats = JSON.parse(
json,
// Infinity, which is the `intervalSeconds` for the lifetime cap, is
// stringified as `null` in the JSON, so convert it back to Infinity.
(key, value) =>
key == "intervalSeconds" && value === null ? Infinity : value
);
} catch (error) {}
}
this._validateImpressionStats();
}
/**
* Validates impression stats, which includes two things:
*
* - Type checks stats and discards any that are invalid. We do this because
* stats are stored in prefs where anyone can modify them.
* - Syncs stats with impression caps so that there is one stats object
* corresponding to each impression cap. See the `_impressionStats` comment
* for more info.
*/
_validateImpressionStats() {
let { impression_caps } = lazy.UrlbarQuickSuggest.config;
this.logger.info("Validating impression stats");
this.logger.debug(
JSON.stringify({
impression_caps,
currentStats: this._impressionStats,
})
);
if (!this._impressionStats || typeof this._impressionStats != "object") {
this._impressionStats = {};
}
for (let [type, cap] of Object.entries(impression_caps || {})) {
// Build a map from interval seconds to max counts in the caps.
let maxCapCounts = (cap.custom || []).reduce(
(map, { interval_s, max_count }) => {
map.set(interval_s, max_count);
return map;
},
new Map()
);
if (typeof cap.lifetime == "number") {
maxCapCounts.set(Infinity, cap.lifetime);
}
let stats = this._impressionStats[type];
if (!Array.isArray(stats)) {
stats = [];
this._impressionStats[type] = stats;
}
// Validate existing stats:
//
// * Discard stats with invalid properties.
// * Collect and remove stats with intervals that aren't in the caps. This
// should only happen when caps are changed or removed.
// * For stats with intervals that are in the caps:
// * Keep track of the max `stat.count` across all stats so we can
// update the lifetime stat below.
// * Set `stat.maxCount` to the max count in the corresponding cap.
let orphanStats = [];
let maxCountInStats = 0;
for (let i = 0; i < stats.length; ) {
let stat = stats[i];
if (
typeof stat.intervalSeconds != "number" ||
typeof stat.startDateMs != "number" ||
typeof stat.count != "number" ||
typeof stat.maxCount != "number" ||
typeof stat.impressionDateMs != "number"
) {
stats.splice(i, 1);
} else {
maxCountInStats = Math.max(maxCountInStats, stat.count);
let maxCount = maxCapCounts.get(stat.intervalSeconds);
if (maxCount === undefined) {
stats.splice(i, 1);
orphanStats.push(stat);
} else {
stat.maxCount = maxCount;
i++;
}
}
}
// Create stats for caps that don't already have corresponding stats.
for (let [intervalSeconds, maxCount] of maxCapCounts.entries()) {
if (!stats.some(s => s.intervalSeconds == intervalSeconds)) {
stats.push({
maxCount,
intervalSeconds,
startDateMs: Date.now(),
count: 0,
impressionDateMs: 0,
});
}
}
// Merge orphaned stats into other ones if possible. For each orphan, if
// its interval is no bigger than an existing stat's interval, then the
// orphan's count can contribute to the existing stat's count, so merge
// the two.
for (let orphan of orphanStats) {
for (let stat of stats) {
if (orphan.intervalSeconds <= stat.intervalSeconds) {
stat.count = Math.max(stat.count, orphan.count);
stat.startDateMs = Math.min(stat.startDateMs, orphan.startDateMs);
stat.impressionDateMs = Math.max(
stat.impressionDateMs,
orphan.impressionDateMs
);
}
}
}
// If the lifetime stat exists, make its count the max count found above.
// This is only necessary when the lifetime cap wasn't present before, but
// it doesn't hurt to always do it.
let lifetimeStat = stats.find(s => s.intervalSeconds == Infinity);
if (lifetimeStat) {
lifetimeStat.count = maxCountInStats;
}
// Sort the stats by interval ascending. This isn't necessary except that
// it guarantees an ordering for tests.
stats.sort((a, b) => a.intervalSeconds - b.intervalSeconds);
}
this.logger.debug(JSON.stringify({ newStats: this._impressionStats }));
}
/**
* Resets the counters of impression stats whose intervals have elapased.
*/
_resetElapsedImpressionCounters() {
this.logger.info("Checking for elapsed impression cap intervals");
this.logger.debug(
JSON.stringify({
currentStats: this._impressionStats,
impression_caps: lazy.UrlbarQuickSuggest.config.impression_caps,
})
);
let now = Date.now();
for (let [type, stats] of Object.entries(this._impressionStats)) {
for (let stat of stats) {
let elapsedMs = now - stat.startDateMs;
let intervalMs = 1000 * stat.intervalSeconds;
let elapsedIntervalCount = Math.floor(elapsedMs / intervalMs);
if (elapsedIntervalCount) {
// At least one interval period elapsed for the stat, so reset it. We
// may also need to record a telemetry event for the reset.
this.logger.info(
`Resetting impression counter for interval ${stat.intervalSeconds}s`
);
this.logger.debug(
JSON.stringify({ type, stat, elapsedMs, elapsedIntervalCount })
);
let newStartDateMs =
stat.startDateMs + elapsedIntervalCount * intervalMs;
// Compute the portion of `elapsedIntervalCount` that happened after
// startup. This will be the interval count we report in the telemetry
// event. By design we don't report intervals that elapsed while the
// app wasn't running. For example, if the user stopped using Firefox
// for a year, we don't want to report a year's worth of intervals.
//
// First, compute the count of intervals that elapsed before startup.
// This is the same arithmetic used above except here it's based on
// the startup date instead of `now`. Keep in mind that startup may be
// before the stat's start date. Then subtract that count from
// `elapsedIntervalCount` to get the portion after startup.
let startupDateMs = this._getStartupDateMs();
let elapsedIntervalCountBeforeStartup = Math.floor(
Math.max(0, startupDateMs - stat.startDateMs) / intervalMs
);
let elapsedIntervalCountAfterStartup =
elapsedIntervalCount - elapsedIntervalCountBeforeStartup;
if (elapsedIntervalCountAfterStartup) {
this._recordImpressionCapEvent({
eventType: "reset",
suggestionType: type,
eventDateMs: newStartDateMs,
eventCount: elapsedIntervalCountAfterStartup,
stat: {
...stat,
startDateMs:
stat.startDateMs +
elapsedIntervalCountBeforeStartup * intervalMs,
},
});
}
// Reset the stat.
stat.startDateMs = newStartDateMs;
stat.count = 0;
}
}
}
this.logger.debug(JSON.stringify({ newStats: this._impressionStats }));
}
/**
* Records an impression cap telemetry event.
*
* @param {object} options
* Options object
* @param {"hit" | "reset"} options.eventType
* One of: "hit", "reset"
* @param {string} options.suggestionType
* One of: "sponsored", "nonsponsored"
* @param {object} options.stat
* The stats object whose max count was hit or whose counter was reset.
* @param {number} options.eventCount
* The number of intervals that elapsed since the last event.
* @param {number} options.eventDateMs
* The `eventDate` that should be recorded in the event's `extra` object.
* We include this in `extra` even though events are timestamped because
* "reset" events are batched during periods where the user doesn't perform
* any searches and therefore impression counters are not reset.
*/
_recordImpressionCapEvent({
eventType,
suggestionType,
stat,
eventCount = 1,
eventDateMs = Date.now(),
}) {
// All `extra` object values must be strings.
let extra = {
type: suggestionType,
eventDate: String(eventDateMs),
eventCount: String(eventCount),
};
for (let [statKey, value] of Object.entries(stat)) {
let extraKey = TELEMETRY_IMPRESSION_CAP_EXTRA_KEYS[statKey];
if (!extraKey) {
throw new Error("Unrecognized stats object key: " + statKey);
}
extra[extraKey] = String(value);
}
Services.telemetry.recordEvent(
TELEMETRY_EVENT_CATEGORY,
"impression_cap",
eventType,
"",
extra
);
}
/**
* Creates a repeating timer that resets impression counters and records
* related telemetry. Since counters are also reset when suggestions are
* triggered, the only point of this is to make sure we record reset telemetry
* events in a timely manner during periods when suggestions aren't triggered.
*
* @param {number} ms
* The number of milliseconds in the interval.
*/
_setImpressionCountersResetInterval(
ms = IMPRESSION_COUNTERS_RESET_INTERVAL_MS
) {
if (this._impressionCountersResetInterval) {
lazy.clearInterval(this._impressionCountersResetInterval);
}
this._impressionCountersResetInterval = lazy.setInterval(
() => this._resetElapsedImpressionCounters(),
ms
);
}
/**
* Gets the timestamp of app startup in ms since Unix epoch. This is only
* defined as its own method so tests can override it to simulate arbitrary
* startups.
*
* @returns {number}
* Startup timestamp in ms since Unix epoch.
*/
_getStartupDateMs() {
return Services.startup.getStartupInfo().process.getTime();
}
/**
* Loads blocked suggestion digests from the pref into `_blockedDigests`.
*/
async _loadBlockedDigests() {
this.logger.debug(`Queueing _loadBlockedDigests`);
await this._blockTaskQueue.queue(() => {
this.logger.info(`Loading blocked suggestion digests`);
let json = lazy.UrlbarPrefs.get("quicksuggest.blockedDigests");
this.logger.debug(
`browser.urlbar.quicksuggest.blockedDigests value: ${json}`
);
if (!json) {
this.logger.info(`There are no blocked suggestion digests`);
this._blockedDigests.clear();
} else {
try {
this._blockedDigests = new Set(JSON.parse(json));
this.logger.info(`Successfully loaded blocked suggestion digests`);
} catch (error) {
this.logger.error(
`Error loading blocked suggestion digests: ${error}`
);
}
}
});
}
/**
* Returns the SHA-1 digest of a string as a 40-character hex-encoded string.
*
* @param {string} string
* The string to convert to SHA-1
* @returns {string}
* The hex-encoded digest of the given string.
*/
async _getDigest(string) {
let stringArray = new TextEncoder().encode(string);
let hashBuffer = await crypto.subtle.digest("SHA-1", stringArray);
let hashArray = new Uint8Array(hashBuffer);
return Array.from(hashArray, b => b.toString(16).padStart(2, "0")).join("");
}
/**
* Updates state based on the `browser.urlbar.quicksuggest.enabled` pref.
*/
_updateFeatureState() {
let enabled = lazy.UrlbarPrefs.get("quickSuggestEnabled");
if (enabled == this._quickSuggestEnabled) {
// This method is a Nimbus `onUpdate()` callback, which means it's called
// each time any pref is changed that is a fallback for a Nimbus variable.
// We have many such prefs. The point of this method is to set up and tear
// down state when quick suggest's enabled status changes, so ignore
// updates that do not modify `quickSuggestEnabled`.
return;
}
this._quickSuggestEnabled = enabled;
this.logger.info("Updating feature state, feature enabled: " + enabled);
Services.telemetry.setEventRecordingEnabled(
TELEMETRY_EVENT_CATEGORY,
enabled
);
if (enabled) {
this._loadImpressionStats();
this._loadBlockedDigests();
}
}
// The most recently cached value of `UrlbarPrefs.get("quickSuggestEnabled")`.
// The purpose of this property is only to detect changes in the feature's
// enabled status. To determine the current status, call
// `UrlbarPrefs.get("quickSuggestEnabled")` directly instead.
_quickSuggestEnabled = false;
// An object that keeps track of impression stats per sponsored and
// non-sponsored suggestion types. It looks like this:
//
// { sponsored: statsArray, nonsponsored: statsArray }
//
// The `statsArray` values are arrays of stats objects, one per impression
// cap, which look like this:
//
// { intervalSeconds, startDateMs, count, maxCount, impressionDateMs }
//
// {number} intervalSeconds
// The number of seconds in the corresponding cap's time interval.
// {number} startDateMs
// The timestamp at which the current interval period started and the
// object's `count` was reset to zero. This is a value returned from
// `Date.now()`. When the current date/time advances past `startDateMs +
// 1000 * intervalSeconds`, a new interval period will start and `count`
// will be reset to zero.
// {number} count
// The number of impressions during the current interval period.
// {number} maxCount
// The maximum number of impressions allowed during an interval period.
// This value is the same as the `max_count` value in the corresponding
// cap. It's stored in the stats object for convenience.
// {number} impressionDateMs
// The timestamp of the most recent impression, i.e., when `count` was
// last incremented.
//
// There are two types of impression caps: interval and lifetime. Interval
// caps are periodically reset, and lifetime caps are never reset. For stats
// objects corresponding to interval caps, `intervalSeconds` will be the
// `interval_s` value of the cap. For stats objects corresponding to lifetime
// caps, `intervalSeconds` will be `Infinity`.
//
// `_impressionStats` is kept in sync with impression caps, and there is a
// one-to-one relationship between stats objects and caps. A stats object's
// corresponding cap is the one with the same suggestion type (sponsored or
// non-sponsored) and interval. See `_validateImpressionStats()` for more.
//
// Impression caps are stored in the remote settings config. See
// `UrlbarQuickSuggest.confg.impression_caps`.
_impressionStats = {};
// Whether impression stats are currently being updated.
_updatingImpressionStats = false;
// Set of digests of the original URLs of blocked suggestions. A suggestion's
// "original URL" is its URL straight from the source with an unreplaced
// timestamp template. For details on the digests, see `_getDigest()`.
//
// The only reason we use URL digests is that suggestions currently do not
// have persistent IDs. We could use the URLs themselves but SHA-1 digests are
// only 40 chars long, so they save a little space. This is also consistent
// with how blocked tiles on the newtab page are stored, but they use MD5. We
// do *not* store digests for any security or obfuscation reason.
//
// This value is serialized as a JSON'ed array to the
// `browser.urlbar.quicksuggest.blockedDigests` pref.
_blockedDigests = new Set();
// Used to serialize access to blocked suggestions. This is only necessary
// because getting a suggestion's URL digest is async.
_blockTaskQueue = new TaskQueue();
// Whether blocked digests are currently being updated.
_updatingBlockedDigests = false;
}
export const QuickSuggest = new _QuickSuggest();