author | Norisz Fay <nfay@mozilla.com> |
Fri, 08 Apr 2022 12:22:39 +0300 | |
changeset 684162 | f9afaaf33533d59d88eb31c159cf5e6676808c30 |
parent 684160 | 80c34c70f1aecc16049554c92dac66975493998c (current diff) |
parent 684153 | ed124d06d67623a6227a41ecffd8533733755976 (diff) |
child 684179 | d62c69b51528bc1a7c555d666ec3b6d5ca22cc01 |
push id | 16598 |
push user | ffxbld-merge |
push date | Mon, 02 May 2022 14:23:32 +0000 |
treeherder | mozilla-beta@de86a81c7a63 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | merge |
milestone | 101.0a1 |
first release with | nightly linux32
f9afaaf33533
/
101.0a1
/
20220408094506
/
files
nightly linux64
f9afaaf33533
/
101.0a1
/
20220408094506
/
files
nightly mac
f9afaaf33533
/
101.0a1
/
20220408094506
/
files
nightly win32
f9afaaf33533
/
101.0a1
/
20220408094506
/
files
nightly win64
f9afaaf33533
/
101.0a1
/
20220408094506
/
files
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
releases | nightly linux32
101.0a1
/
20220408094506
/
pushlog to previous
nightly linux64
101.0a1
/
20220408094506
/
pushlog to previous
nightly mac
101.0a1
/
20220408094506
/
pushlog to previous
nightly win32
101.0a1
/
20220408094506
/
pushlog to previous
nightly win64
101.0a1
/
20220408094506
/
pushlog to previous
|
--- a/.eslintrc.js +++ b/.eslintrc.js @@ -191,18 +191,16 @@ module.exports = { "modules/libjar/zipwriter/test/unit/test_alignment.js", "modules/libjar/zipwriter/test/unit/test_bug419769_2.js", "modules/libjar/zipwriter/test/unit/test_storedata.js", "modules/libjar/zipwriter/test/unit/test_zippermissions.js", "modules/libpref/test/unit/test_changeType.js", "modules/libpref/test/unit/test_dirtyPrefs.js", "toolkit/crashreporter/test/unit/test_crash_AsyncShutdown.js", "toolkit/mozapps/update/tests/unit_aus_update/testConstants.js", - "xpcom/tests/unit/test_hidden_files.js", - "xpcom/tests/unit/test_localfile.js", // These are more complicated bugs which may require some in-depth // investigation or different solutions. They are also likely to be // a reasonable size. "browser/components/**", "browser/modules/**", "dom/**", "netwerk/**",
--- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -424,16 +424,24 @@ pref("browser.urlbar.quicksuggest.nonSpo // Whether Remote Settings is enabled as a quick suggest source. pref("browser.urlbar.quicksuggest.remoteSettings.enabled", true); // Whether quick suggest results can be shown in position specified in the // suggestions. pref("browser.urlbar.quicksuggest.allowPositionInSuggestions", true); +// Whether non-sponsored quick suggest results are subject to impression +// frequency caps. +pref("browser.urlbar.quicksuggest.impressionCaps.nonSponsoredEnabled", false); + +// Whether sponsored quick suggest results are subject to impression frequency +// caps. +pref("browser.urlbar.quicksuggest.impressionCaps.sponsoredEnabled", false); + // Whether unit conversion is enabled. #ifdef NIGHTLY_BUILD pref("browser.urlbar.unitConversion.enabled", true); #else pref("browser.urlbar.unitConversion.enabled", false); #endif // Whether to show search suggestions before general results like history and @@ -2696,8 +2704,13 @@ pref("svg.context-properties.content.all // last decimal are prefixed by `_score` and reference the functions called in // SnapshotScorer. pref("browser.snapshots.score.Visit", 1); pref("browser.snapshots.score.CurrentSession", 1); pref("browser.snapshots.score.InNavigation", 3); pref("browser.snapshots.score.IsOverlappingVisit", 3); pref("browser.snapshots.score.IsUserPersisted", 1); pref("browser.snapshots.score.IsUsedRemoved", -10); + +// Expiration days for snapshots. +pref("browser.places.snapshots.expiration.days", 210); +// For user managed snapshots we use more than a year, to support yearly tasks. +pref("browser.places.snapshots.expiration.userManaged.days", 420);
--- a/browser/base/content/browser-places.js +++ b/browser/base/content/browser-places.js @@ -2048,72 +2048,16 @@ var BookmarkingUI = { }, onPanelMenuViewHiding: function BUI_onViewHiding(aEvent) { this._panelMenuView.uninit(); delete this._panelMenuView; aEvent.target.removeEventListener("ViewHiding", this); }, - toggleMenuButtonInToolbar(triggerNode) { - let placement = CustomizableUI.getPlacementOfWidget( - this.BOOKMARK_BUTTON_ID - ); - const area = CustomizableUI.AREA_NAVBAR; - if (!placement) { - // Button is in the palette, so we can move it to the navbar. - let pos; - let widgetIDs = CustomizableUI.getWidgetIdsInArea( - CustomizableUI.AREA_NAVBAR - ); - // If there's a spring inside the navbar, find it and use that as the - // placement marker. - let lastSpringID = null; - for (let i = widgetIDs.length - 1; i >= 0; --i) { - let id = widgetIDs[i]; - if (CustomizableUI.isSpecialWidget(id) && /spring/.test(id)) { - lastSpringID = id; - break; - } - } - if (lastSpringID) { - pos = CustomizableUI.getPlacementOfWidget(lastSpringID).position + 1; - } else { - // Next alternative is to use the searchbar as the placement marker. - const searchWidgetID = "search-container"; - if (widgetIDs.includes(searchWidgetID)) { - pos = - CustomizableUI.getPlacementOfWidget(searchWidgetID).position + 1; - } else { - // Last alternative is to use the navbar as the placement marker. - pos = - CustomizableUI.getPlacementOfWidget("urlbar-container").position + - 1; - } - } - - CustomizableUI.addWidgetToArea(this.BOOKMARK_BUTTON_ID, area, pos); - BrowserUsageTelemetry.recordWidgetChange( - this.BOOKMARK_BUTTON_ID, - area, - "bookmark-tools" - ); - } else { - // Move it back to the palette. - CustomizableUI.removeWidgetFromArea(this.BOOKMARK_BUTTON_ID); - BrowserUsageTelemetry.recordWidgetChange( - this.BOOKMARK_BUTTON_ID, - null, - "bookmark-tools" - ); - } - triggerNode.setAttribute("checked", !placement); - updateToggleControlLabel(triggerNode); - }, - handlePlacesEvents(aEvents) { let isStarUpdateNeeded = false; let affectsOtherBookmarksFolder = false; for (let ev of aEvents) { switch (ev.type) { case "bookmark-added": // Only need to update the UI if it wasn't marked as starred before:
--- a/browser/base/content/tabbrowser-tabs.js +++ b/browser/base/content/tabbrowser-tabs.js @@ -2088,17 +2088,20 @@ return this.tabbox.tabpanels.firstElementChild; } // If the tab's browser is lazy, we need to `_insertBrowser` in order // to have a linkedPanel. This will also serve to bind the browser // and make it ready to use. We only do this if the tab is selected // because otherwise, callers might end up unintentionally binding the // browser for lazy background tabs. - if (aTab.selected) { + if (!aTab.linkedPanel) { + if (!aTab.selected) { + return null; + } gBrowser._insertBrowser(aTab); } return document.getElementById(aTab.linkedPanel); } _updateNewTabVisibility() { // Helper functions to help deal with customize mode wrapping some items let wrap = n =>
--- a/browser/base/content/test/general/browser.ini +++ b/browser/base/content/test/general/browser.ini @@ -231,16 +231,17 @@ skip-if = toolkit == "windows" # Disable # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD. [browser_menuButtonFitts.js] # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD. [browser_middleMouse_noJSPaste.js] https_first_disabled = true skip-if = apple_silicon && !debug # Bug 1724711 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD. [browser_minimize.js] +skip-if = apple_silicon && !debug # Bug 1725756 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD. fail-if = (os == 'linux' && os_version == '18.04') # Bug 1600177 [browser_modifiedclick_inherit_principal.js] https_first_disabled = true # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD. [browser_new_http_window_opened_from_file_tab.js] https_first_disabled = true # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
--- a/browser/components/aboutlogins/content/aboutLogins.css +++ b/browser/components/aboutlogins/content/aboutLogins.css @@ -88,8 +88,12 @@ login-item[data-editing="true"] + login- } body > section { display: grid; grid-template-rows: auto 1fr; overflow-y: hidden; overflow-x: auto; } + +login-intro { + overflow-y: scroll; +}
--- a/browser/components/enterprisepolicies/tests/browser/disable_developer_tools/browser.ini +++ b/browser/components/enterprisepolicies/tests/browser/disable_developer_tools/browser.ini @@ -1,8 +1,7 @@ [DEFAULT] prefs = browser.policies.alternatePath='<test-root>/browser/components/enterprisepolicies/tests/browser/disable_developer_tools/config_disable_developer_tools.json' support-files = - ../head.js config_disable_developer_tools.json [browser_policy_disable_developer_tools.js]
--- a/browser/components/enterprisepolicies/tests/browser/disable_developer_tools/browser_policy_disable_developer_tools.js +++ b/browser/components/enterprisepolicies/tests/browser/disable_developer_tools/browser_policy_disable_developer_tools.js @@ -1,14 +1,16 @@ /* Any copyright is dedicated to the Public Domain. * http://creativecommons.org/publicdomain/zero/1.0/ */ -/* import-globals-from ../head.js */ +"use strict"; -"use strict"; +const { EnterprisePolicyTesting } = ChromeUtils.import( + "resource://testing-common/EnterprisePolicyTesting.jsm" +); var updateService = Cc["@mozilla.org/updates/update-service;1"].getService( Ci.nsIApplicationUpdateService ); add_task(async function test_updates_post_policy() { is( Services.policies.isAllowed("devtools"), false, @@ -53,8 +55,33 @@ add_task(async function test_updates_pos document.getElementById("appmenu-developer-tools-view").children.length, 2, "The developer tools are properly populated" ); window.PanelUI.hide(); BrowserTestUtils.removeTab(tab); }); + +// Copied from ../head.js. head.js was never intended to be used with tests +// that use a JSON file versus calling setupPolicyEngineWithJson so I have +// to copy this function here versus including it. +async function testPageBlockedByPolicy(page, policyJSON) { + if (policyJSON) { + await EnterprisePolicyTesting.setupPolicyEngineWithJson(policyJSON); + } + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:blank" }, + async browser => { + BrowserTestUtils.loadURI(browser, page); + await BrowserTestUtils.browserLoaded(browser, false, page, true); + await SpecialPowers.spawn(browser, [page], async function(innerPage) { + ok( + content.document.documentURI.startsWith( + "about:neterror?e=blockedByPolicy" + ), + content.document.documentURI + + " should start with about:neterror?e=blockedByPolicy" + ); + }); + } + ); +}
--- a/browser/components/enterprisepolicies/tests/browser/head.js +++ b/browser/components/enterprisepolicies/tests/browser/head.js @@ -168,20 +168,16 @@ async function check_homepage({ '"Restore defaults" button disabled status should match expected' ); } ); await BrowserTestUtils.removeTab(tab); } add_setup(async function policies_headjs_startWithCleanSlate() { - if (Services.prefs.getPrefType("browser.policies.alternatePath")) { - // This allows tests that use a JSON File to use head.js - return; - } if (Services.policies.status != Ci.nsIEnterprisePolicies.INACTIVE) { await setupPolicyEngineWithJson(""); } is( Services.policies.status, Ci.nsIEnterprisePolicies.INACTIVE, "Engine is inactive at the start of the test" );
--- a/browser/components/extensions/ext-browser.json +++ b/browser/components/extensions/ext-browser.json @@ -113,16 +113,18 @@ "menusChild": { "schema": "chrome://browser/content/schemas/menus_child.json", "scopes": ["addon_child", "content_child", "devtools_child"] }, "menusInternal": { "url": "chrome://browser/content/parent/ext-menus.js", "schema": "chrome://browser/content/schemas/menus.json", "scopes": ["addon_parent"], + "events": ["startup"], + "permissions": ["menus", "contextMenus"], "paths": [ ["contextMenus"], ["menus"], ["menusInternal"] ] }, "normandyAddonStudy": { "url": "chrome://browser/content/parent/ext-normandyAddonStudy.js",
--- a/browser/components/extensions/parent/ext-menus.js +++ b/browser/components/extensions/parent/ext-menus.js @@ -19,25 +19,31 @@ ChromeUtils.defineModuleGetter( ); var { DefaultMap, ExtensionError, parseMatchPatterns } = ExtensionUtils; var { ExtensionParent } = ChromeUtils.import( "resource://gre/modules/ExtensionParent.jsm" ); -var { IconDetails } = ExtensionParent; +var { IconDetails, StartupCache } = ExtensionParent; const ACTION_MENU_TOP_LEVEL_LIMIT = 6; // Map[Extension -> Map[ID -> MenuItem]] // Note: we want to enumerate all the menu items so // this cannot be a weak map. var gMenuMap = new Map(); +// Map[Extension -> Map[ID -> MenuCreateProperties]] +// The map object for each extension is a reference to the same +// object in StartupCache.menus. This provides a non-async +// getter for that object. +var gStartupCache = new Map(); + // Map[Extension -> MenuItem] var gRootItems = new Map(); // Map[Extension -> ID[]] // Menu IDs that were eligible for being shown in the current menu. var gShownMenuItems = new DefaultMap(() => []); // Map[Extension -> Set[Contexts]] @@ -706,48 +712,52 @@ function addMenuEventInfo(info, contextD // If the context was overridden, then frameUrl should be the URL of the // document in which the menu was opened (instead of undefined, even if that // document is not in a frame). if (contextData.originalViewUrl) { info.frameUrl = contextData.originalViewUrl; } } -function MenuItem(extension, createProperties, isRoot = false) { - this.extension = extension; - this.children = []; - this.parent = null; - this.tabManager = extension.tabManager; +class MenuItem { + constructor(extension, createProperties, isRoot = false) { + this.extension = extension; + this.children = []; + this.parent = null; + this.tabManager = extension.tabManager; - this.setDefaults(); - this.setProps(createProperties); + this.setDefaults(); + this.setProps(createProperties); - if (!this.hasOwnProperty("_id")) { - this.id = gNextMenuItemID++; + if (!this.hasOwnProperty("_id")) { + this.id = gNextMenuItemID++; + } + // If the item is not the root and has no parent + // it must be a child of the root. + if (!isRoot && !this.parent) { + this.root.addChild(this); + } } - // If the item is not the root and has no parent - // it must be a child of the root. - if (!isRoot && !this.parent) { - this.root.addChild(this); - } -} -MenuItem.prototype = { - setProps(createProperties) { - for (let propName in createProperties) { - if (createProperties[propName] === null) { + static mergeProps(obj, properties) { + for (let propName in properties) { + if (properties[propName] === null) { // Omitted optional argument. continue; } - this[propName] = createProperties[propName]; + obj[propName] = properties[propName]; } - if ("icons" in createProperties && createProperties.icons === null) { - this.icons = null; + if ("icons" in properties && properties.icons === null && obj.icons) { + obj.icons = null; } + } + + setProps(createProperties) { + MenuItem.mergeProps(this, createProperties); if (createProperties.documentUrlPatterns != null) { this.documentUrlMatchPattern = parseMatchPatterns( this.documentUrlPatterns, { restrictSchemes: this.extension.restrictSchemes, } ); @@ -761,54 +771,54 @@ MenuItem.prototype = { }); } // If a child MenuItem does not specify any contexts, then it should // inherit the contexts specified from its parent. if (createProperties.parentId && !createProperties.contexts) { this.contexts = this.parent.contexts; } - }, + } setDefaults() { this.setProps({ type: "normal", checked: false, contexts: ["all"], enabled: true, visible: true, }); - }, + } set id(id) { if (this.hasOwnProperty("_id")) { throw new ExtensionError("ID of a MenuItem cannot be changed"); } let isIdUsed = gMenuMap.get(this.extension).has(id); if (isIdUsed) { throw new ExtensionError(`ID already exists: ${id}`); } this._id = id; - }, + } get id() { return this._id; - }, + } get elementId() { let id = this.id; // If the ID is an integer, it is auto-generated and globally unique. // If the ID is a string, it is only unique within one extension and the // ID needs to be concatenated with the extension ID. if (typeof id !== "number") { // To avoid collisions with numeric IDs, add a prefix to string IDs. id = `_${id}`; } return `${makeWidgetId(this.extension.id)}-menuitem-${id}`; - }, + } ensureValidParentId(parentId) { if (parentId === undefined) { return; } let menuMap = gMenuMap.get(this.extension); if (!menuMap.has(parentId)) { throw new ExtensionError( @@ -817,83 +827,105 @@ MenuItem.prototype = { } for (let item = menuMap.get(parentId); item; item = item.parent) { if (item === this) { throw new ExtensionError( "MenuItem cannot be an ancestor (or self) of its new parent." ); } } - }, + } + + /** + * When updating menu properties we need to ensure parents exist + * in the cache map before children. That allows the menus to be + * created in the correct sequence on startup. This reparents the + * tree starting from this instance of MenuItem. + */ + reparentInCache() { + let { id, extension } = this; + let cachedMap = gStartupCache.get(extension); + let createProperties = cachedMap.get(id); + cachedMap.delete(id); + cachedMap.set(id, createProperties); + + for (let child of this.children) { + child.reparentInCache(); + } + } set parentId(parentId) { this.ensureValidParentId(parentId); if (this.parent) { this.parent.detachChild(this); } if (parentId === undefined) { this.root.addChild(this); } else { let menuMap = gMenuMap.get(this.extension); menuMap.get(parentId).addChild(this); } - }, + } get parentId() { return this.parent ? this.parent.id : undefined; - }, + } addChild(child) { if (child.parent) { throw new Error("Child MenuItem already has a parent."); } this.children.push(child); child.parent = this; - }, + } detachChild(child) { let idx = this.children.indexOf(child); if (idx < 0) { throw new Error("Child MenuItem not found, it cannot be removed."); } this.children.splice(idx, 1); child.parent = null; - }, + } get root() { let extension = this.extension; if (!gRootItems.has(extension)) { let root = new MenuItem( extension, { title: extension.name }, /* isRoot = */ true ); gRootItems.set(extension, root); } return gRootItems.get(extension); - }, + } remove() { if (this.parent) { this.parent.detachChild(this); } let children = this.children.slice(0); for (let child of children) { child.remove(); } let menuMap = gMenuMap.get(this.extension); menuMap.delete(this.id); + // Menu items are saved if !extension.persistentBackground. + if (gStartupCache.get(this.extension)?.delete(this.id)) { + StartupCache.save(); + } if (this.root == this) { gRootItems.delete(this.extension); } - }, + } getClickInfo(contextData, wasChecked) { let info = { menuItemId: this.id, }; if (this.parent) { info.parentMenuItemId = this.parentId; } @@ -901,17 +933,17 @@ MenuItem.prototype = { addMenuEventInfo(info, contextData, this.extension, true); if (this.type === "checkbox" || this.type === "radio") { info.checked = this.checked; info.wasChecked = wasChecked; } return info; - }, + } enabledForContext(contextData) { if (!this.visible) { return false; } let contexts = getMenuContexts(contextData); if (!this.contexts.some(n => contexts.has(n))) { return false; @@ -963,18 +995,18 @@ MenuItem.prototype = { targetURIs.push(contextData.linkURI); } if (!targetURIs.some(targetURI => targetPattern.matches(targetURI))) { return false; } } return true; - }, -}; + } +} // windowTracker only looks as browser windows, but we're also interested in // the Library window. Helper for menuTracker below. const libraryTracker = { libraryWindowType: "Places:Organizer", isLibraryWindow(window) { let winType = window.document.documentElement.getAttribute("windowtype"); @@ -1194,23 +1226,56 @@ this.menusInternal = class extends Exten super(extension); if (!gMenuMap.size) { menuTracker.register(); } gMenuMap.set(extension, new Map()); } + restoreFromCache() { + let { extension } = this; + // ensure extension has not shutdown + if (!this.extension) { + return; + } + for (let createProperties of gStartupCache.get(extension).values()) { + // The order of menu creation is significant, see reparentInCache. + let menuItem = new MenuItem(extension, createProperties); + gMenuMap.get(extension).set(menuItem.id, menuItem); + } + // Used for testing + extension.emit("webext-menus-created", gMenuMap.get(extension)); + } + + async onStartup() { + let { extension } = this; + if (extension.persistentBackground) { + return; + } + // Using the map retains insertion order. + let cachedMenus = await StartupCache.menus.get(extension.id, () => { + return new Map(); + }); + gStartupCache.set(extension, cachedMenus); + if (!cachedMenus.size) { + return; + } + + this.restoreFromCache(); + } + onShutdown() { let { extension } = this; if (gMenuMap.has(extension)) { gMenuMap.delete(extension); gRootItems.delete(extension); gShownMenuItems.delete(extension); + gStartupCache.delete(extension); gOnShownSubscribers.delete(extension); if (!gMenuMap.size) { menuTracker.unregister(); } } } PERSISTENT_EVENTS = { @@ -1328,57 +1393,89 @@ this.menusInternal = class extends Exten extensionApi: this, }).api(), }; return { contextMenus: menus, menus, menusInternal: { - create: function(createProperties) { + create(createProperties) { // event pages require id if (!extension.persistentBackground) { if (!createProperties.id) { throw new ExtensionError( "menus.create requires an id for non-persistent background scripts." ); } if (gMenuMap.get(extension).has(createProperties.id)) { throw new ExtensionError( `The menu id ${createProperties.id} already exists in menus.create.` ); } } // Note that the id is required by the schema. If the addon did not set - // it, the implementation of menus.create in the child should - // have added it. + // it, the implementation of menus.create in the child will add it for + // extensions with persistent backgrounds, but not otherwise. let menuItem = new MenuItem(extension, createProperties); gMenuMap.get(extension).set(menuItem.id, menuItem); - }, - - update: function(id, updateProperties) { - let menuItem = gMenuMap.get(extension).get(id); - if (menuItem) { - menuItem.setProps(updateProperties); + if (!extension.persistentBackground) { + // Only cache properties that are necessary. + let cached = {}; + MenuItem.mergeProps(cached, createProperties); + gStartupCache.get(extension).set(menuItem.id, cached); + StartupCache.save(); } }, - remove: function(id) { + update(id, updateProperties) { + let menuItem = gMenuMap.get(extension).get(id); + if (!menuItem) { + return; + } + menuItem.setProps(updateProperties); + + // Update the startup cache for non-persistent extensions. + if (extension.persistentBackground) { + return; + } + + let cached = gStartupCache.get(extension).get(id); + let reparent = + updateProperties.parentId != null && + cached.parentId != updateProperties.parentId; + MenuItem.mergeProps(cached, updateProperties); + if (reparent) { + // The order of menu creation is significant, see reparentInCache. + menuItem.reparentInCache(); + } + StartupCache.save(); + }, + + remove(id) { let menuItem = gMenuMap.get(extension).get(id); if (menuItem) { menuItem.remove(); } }, - removeAll: function() { + removeAll() { let root = gRootItems.get(extension); if (root) { root.remove(); } + // Should be empty, just extra assurance. + if (!extension.persistentBackground) { + let cached = gStartupCache.get(extension); + if (cached.size) { + cached.clear(); + StartupCache.save(); + } + } }, onClicked: new EventManager({ context, module: "menusInternal", event: "onClicked", name: "menus.onClicked", extensionApi: this,
new file mode 100644 --- /dev/null +++ b/browser/components/extensions/test/xpcshell/test_ext_menu_startup.js @@ -0,0 +1,439 @@ +"use strict"; + +ChromeUtils.defineModuleGetter( + this, + "ExtensionParent", + "resource://gre/modules/ExtensionParent.jsm" +); + +ChromeUtils.defineModuleGetter( + this, + "Management", + "resource://gre/modules/Extension.jsm" +); + +const { AddonTestUtils } = ChromeUtils.import( + "resource://testing-common/AddonTestUtils.jsm" +); + +AddonTestUtils.init(this); +AddonTestUtils.overrideCertDB(); +AddonTestUtils.createAppInfo( + "xpcshell@tests.mozilla.org", + "XPCShell", + "42", + "42" +); + +Services.prefs.setBoolPref("extensions.eventPages.enabled", true); + +function getExtension(id, background, useAddonManager) { + return ExtensionTestUtils.loadExtension({ + useAddonManager, + manifest: { + applications: { gecko: { id } }, + permissions: ["menus"], + background: { persistent: false }, + }, + background, + }); +} + +async function expectCached(extension, expect) { + let { StartupCache } = ExtensionParent; + let cached = await StartupCache.menus.get(extension.id); + let createProperties = Array.from(cached.values()); + equal(cached.size, expect.length, "menus saved in cache"); + // The menus startupCache is a map and the order is significant + // for recreating menus on startup. Ensure that they are in + // the expected order. We only verify specific keys here rather + // than all menu properties. + for (let i in createProperties) { + Assert.deepEqual( + createProperties[i], + expect[i], + "expected cached properties exist" + ); + } +} + +function promiseExtensionEvent(wrapper, event) { + return new Promise(resolve => { + wrapper.extension.once(event, (kind, data) => { + resolve(data); + }); + }); +} + +add_setup(async () => { + await AddonTestUtils.promiseStartupManager(); +}); + +add_task(async function test_menu_onInstalled() { + async function background() { + browser.runtime.onInstalled.addListener(async () => { + const parentId = browser.menus.create({ + contexts: ["all"], + title: "parent", + id: "test-parent", + }); + browser.menus.create({ + parentId, + title: "click A", + id: "test-click-a", + }); + browser.menus.create( + { + parentId, + title: "click B", + id: "test-click-b", + }, + () => { + browser.test.sendMessage("onInstalled"); + } + ); + }); + browser.menus.create( + { + contexts: ["tab"], + title: "top-level", + id: "test-top-level", + }, + () => { + browser.test.sendMessage("create", browser.runtime.lastError?.message); + } + ); + + browser.test.onMessage.addListener(async msg => { + browser.test.log(`onMessage ${msg}`); + if (msg == "updatemenu") { + await browser.menus.update("test-click-a", { title: "click updated" }); + } else if (msg == "removemenu") { + await browser.menus.remove("test-click-b"); + } else if (msg == "removeall") { + await browser.menus.removeAll(); + } + browser.test.sendMessage("updated"); + }); + } + + const extension = getExtension( + "test-persist@mochitest", + background, + "permanent" + ); + + await extension.startup(); + let lastError = await extension.awaitMessage("create"); + Assert.equal(lastError, undefined, "no error creating menu"); + await extension.awaitMessage("onInstalled"); + await extension.terminateBackground(); + + await expectCached(extension, [ + { + contexts: ["tab"], + id: "test-top-level", + title: "top-level", + }, + { contexts: ["all"], id: "test-parent", title: "parent" }, + { + id: "test-click-a", + parentId: "test-parent", + title: "click A", + }, + { + id: "test-click-b", + parentId: "test-parent", + title: "click B", + }, + ]); + + await extension.wakeupBackground(); + lastError = await extension.awaitMessage("create"); + Assert.equal( + lastError, + "The menu id test-top-level already exists in menus.create.", + "correct error creating menu" + ); + + await AddonTestUtils.promiseRestartManager(); + await extension.awaitStartup(); + + // verify the startupcache + await expectCached(extension, [ + { + contexts: ["tab"], + id: "test-top-level", + title: "top-level", + }, + { contexts: ["all"], id: "test-parent", title: "parent" }, + { + id: "test-click-a", + parentId: "test-parent", + title: "click A", + }, + { + id: "test-click-b", + parentId: "test-parent", + title: "click B", + }, + ]); + + equal( + extension.extension.backgroundState, + "stopped", + "background is not running" + ); + await extension.wakeupBackground(); + lastError = await extension.awaitMessage("create"); + Assert.equal( + lastError, + "The menu id test-top-level already exists in menus.create.", + "correct error creating menu" + ); + + extension.sendMessage("updatemenu"); + await extension.awaitMessage("updated"); + await extension.terminateBackground(); + + // Title change is cached + await expectCached(extension, [ + { + contexts: ["tab"], + id: "test-top-level", + title: "top-level", + }, + { contexts: ["all"], id: "test-parent", title: "parent" }, + { + id: "test-click-a", + parentId: "test-parent", + title: "click updated", + }, + { + id: "test-click-b", + parentId: "test-parent", + title: "click B", + }, + ]); + + await extension.wakeupBackground(); + lastError = await extension.awaitMessage("create"); + Assert.equal( + lastError, + "The menu id test-top-level already exists in menus.create.", + "correct error creating menu" + ); + + extension.sendMessage("removemenu"); + await extension.awaitMessage("updated"); + await extension.terminateBackground(); + + // menu removed + await expectCached(extension, [ + { + contexts: ["tab"], + id: "test-top-level", + title: "top-level", + }, + { contexts: ["all"], id: "test-parent", title: "parent" }, + { + id: "test-click-a", + parentId: "test-parent", + title: "click updated", + }, + ]); + + await extension.wakeupBackground(); + lastError = await extension.awaitMessage("create"); + Assert.equal( + lastError, + "The menu id test-top-level already exists in menus.create.", + "correct error creating menu" + ); + + extension.sendMessage("removeall"); + await extension.awaitMessage("updated"); + await extension.terminateBackground(); + + // menus removed + await expectCached(extension, []); + + await extension.unload(); +}); + +add_task(async function test_menu_nested() { + async function background() { + browser.test.onMessage.addListener(async (action, properties) => { + browser.test.log(`onMessage ${action}`); + switch (action) { + case "create": + await new Promise(resolve => { + browser.menus.create(properties, resolve); + }); + break; + case "update": + { + let { id, ...update } = properties; + await browser.menus.update(id, update); + } + break; + case "remove": + { + let { id } = properties; + await browser.menus.remove(id); + } + break; + case "removeAll": + await browser.menus.removeAll(); + break; + } + browser.test.sendMessage("updated"); + }); + } + + const extension = getExtension( + "test-nesting@mochitest", + background, + "permanent" + ); + await extension.startup(); + + extension.sendMessage("create", { + id: "first", + contexts: ["all"], + title: "first", + }); + await extension.awaitMessage("updated"); + await expectCached(extension, [ + { contexts: ["all"], id: "first", title: "first" }, + ]); + + extension.sendMessage("create", { + id: "second", + contexts: ["all"], + title: "second", + }); + await extension.awaitMessage("updated"); + await expectCached(extension, [ + { contexts: ["all"], id: "first", title: "first" }, + { contexts: ["all"], id: "second", title: "second" }, + ]); + + extension.sendMessage("create", { + id: "third", + contexts: ["all"], + title: "third", + parentId: "first", + }); + await extension.awaitMessage("updated"); + await expectCached(extension, [ + { contexts: ["all"], id: "first", title: "first" }, + { contexts: ["all"], id: "second", title: "second" }, + { + contexts: ["all"], + id: "third", + parentId: "first", + title: "third", + }, + ]); + + extension.sendMessage("create", { + id: "fourth", + contexts: ["all"], + title: "fourth", + }); + await extension.awaitMessage("updated"); + await expectCached(extension, [ + { contexts: ["all"], id: "first", title: "first" }, + { contexts: ["all"], id: "second", title: "second" }, + { + contexts: ["all"], + id: "third", + parentId: "first", + title: "third", + }, + { contexts: ["all"], id: "fourth", title: "fourth" }, + ]); + + extension.sendMessage("update", { + id: "first", + parentId: "second", + }); + await extension.awaitMessage("updated"); + await expectCached(extension, [ + { contexts: ["all"], id: "second", title: "second" }, + { contexts: ["all"], id: "fourth", title: "fourth" }, + { + contexts: ["all"], + id: "first", + title: "first", + parentId: "second", + }, + { + contexts: ["all"], + id: "third", + parentId: "first", + title: "third", + }, + ]); + + await AddonTestUtils.promiseShutdownManager(); + // We need to attach an event listener before the + // startup event is emitted. Fortunately, we + // emit via Management before emitting on extension. + let promiseMenus; + Management.once("startup", (kind, ext) => { + info(`management ${kind} ${ext.id}`); + promiseMenus = promiseExtensionEvent( + { extension: ext }, + "webext-menus-created" + ); + }); + await AddonTestUtils.promiseStartupManager(); + await extension.awaitStartup(); + await extension.wakeupBackground(); + + await expectCached(extension, [ + { contexts: ["all"], id: "second", title: "second" }, + { contexts: ["all"], id: "fourth", title: "fourth" }, + { + contexts: ["all"], + id: "first", + title: "first", + parentId: "second", + }, + { + contexts: ["all"], + id: "third", + parentId: "first", + title: "third", + }, + ]); + // validate nesting + let menus = await promiseMenus; + equal(menus.get("first").parentId, "second", "menuitem parent is correct"); + equal( + menus.get("second").children.length, + 1, + "menuitem parent has correct number of children" + ); + equal( + menus.get("second").root.children.length, + 2, // second and forth + "menuitem root has correct number of children" + ); + + extension.sendMessage("remove", { + id: "second", + }); + await extension.awaitMessage("updated"); + await expectCached(extension, [ + { contexts: ["all"], id: "fourth", title: "fourth" }, + ]); + + extension.sendMessage("removeAll"); + await extension.awaitMessage("updated"); + await expectCached(extension, []); + + await extension.unload(); +});
--- a/browser/components/extensions/test/xpcshell/xpcshell.ini +++ b/browser/components/extensions/test/xpcshell/xpcshell.ini @@ -15,16 +15,17 @@ skip-if = tsan # Times out, bug 1612707 [test_ext_distribution_popup.js] [test_ext_history.js] [test_ext_homepage_overrides_private.js] [test_ext_manifest.js] [test_ext_manifest_commands.js] [test_ext_manifest_omnibox.js] [test_ext_manifest_permissions.js] [test_ext_menu_caller.js] +[test_ext_menu_startup.js] [test_ext_normandyAddonStudy.js] [test_ext_pageAction_shutdown.js] [test_ext_pkcs11_management.js] [test_ext_settings_overrides_defaults.js] support-files = data/test/manifest.json data/test2/manifest.json [test_ext_settings_overrides_search.js]
--- a/browser/components/newtab/aboutwelcome/content/aboutwelcome.css +++ b/browser/components/newtab/aboutwelcome/content/aboutwelcome.css @@ -137,16 +137,27 @@ body[lwt-newtab-brighttext] { background-color: rgba(21, 20, 26, 0.5); display: flex; position: relative; flex-flow: row nowrap; height: 100%; min-height: 500px; overflow: hidden; } +.onboardingContainer .screen:is(.UPGRADE_PIN_FIREFOX, .UPGRADE_ONLY_DEFAULT, .UPGRADE_GET_STARTED) .brand-logo { + margin-top: 120px; +} +@media (prefers-reduced-motion: reduce) { + .onboardingContainer .screen:is(.UPGRADE_PIN_FIREFOX, .UPGRADE_ONLY_DEFAULT, .UPGRADE_GET_STARTED) .brand-logo { + background-image: url("chrome://activity-stream/content/data/content/assets/heart.svg") !important; + } +} +.onboardingContainer .screen:is(.UPGRADE_PIN_FIREFOX, .UPGRADE_ONLY_DEFAULT, .UPGRADE_GET_STARTED) .no-steps { + padding-bottom: 12px; +} .onboardingContainer .screen.light-text { --in-content-page-color: rgb(251, 251, 254); --in-content-primary-button-text-color: rgb(43, 42, 51); --in-content-primary-button-text-color-hover: rgb(43, 42, 51); --in-content-primary-button-background: rgb(0, 221, 255); --in-content-primary-button-background-hover: rgb(128, 235, 255); --in-content-primary-button-background-active: rgb(170, 242, 255); --in-content-link-color: var(--in-content-primary-button-background); @@ -228,19 +239,17 @@ body[lwt-newtab-brighttext] { font-size: 36px; } .onboardingContainer .welcome-text.slim h1 { font-weight: 276; } .onboardingContainer .welcome-text.fancy h1 { background-image: linear-gradient(90deg, #9059FF, #FF4AA2, #FF8C00, #FF4AA2, #9059FF); background-size: 400% auto; - color: #000; background-clip: text; - -webkit-background-clip: text; animation: shine 50s linear infinite; } @media (prefers-contrast: no-preference) { .onboardingContainer .welcome-text.fancy h1 { -webkit-text-fill-color: transparent; } } @media (prefers-color-scheme: dark) { @@ -664,16 +673,17 @@ body[lwt-newtab-brighttext] { .onboardingContainer .mobile-download-buttons li button { display: inline-block; height: 45px; width: 152px; background-repeat: no-repeat; background-size: contain; background-position: center; box-shadow: none; + border: 0; } .onboardingContainer .mobile-download-buttons li:not(:first-child) { margin-inline: 5px 0; } .onboardingContainer .dismiss-button { padding: 0; margin-block: 30px -45px; margin-inline: 0 30px;
--- a/browser/components/newtab/content-src/aboutwelcome/aboutwelcome.scss +++ b/browser/components/newtab/content-src/aboutwelcome/aboutwelcome.scss @@ -142,16 +142,31 @@ body { background-color: rgba(21, 20, 26, 0.5); display: flex; position: relative; flex-flow: row nowrap; height: 100%; min-height: 500px; overflow: hidden; + &:is(.UPGRADE_PIN_FIREFOX, .UPGRADE_ONLY_DEFAULT, .UPGRADE_GET_STARTED) { + .brand-logo { + margin-top: 120px; + + @media (prefers-reduced-motion: reduce) { + // sass-lint:disable-block no-important + background-image: url('chrome://activity-stream/content/data/content/assets/heart.svg') !important; + } + } + + .no-steps { + padding-bottom: 12px; + } + } + &.light-text { --in-content-page-color: rgb(251, 251, 254); --in-content-primary-button-text-color: rgb(43, 42, 51); --in-content-primary-button-text-color-hover: rgb(43, 42, 51); --in-content-primary-button-background: rgb(0, 221, 255); --in-content-primary-button-background-hover: rgb(128, 235, 255); --in-content-primary-button-background-active: rgb(170, 242, 255); --in-content-link-color: var(--in-content-primary-button-background); @@ -252,19 +267,17 @@ body { font-weight: 276; } } &.fancy { h1 { background-image: linear-gradient(90deg, #9059FF, #FF4AA2, #FF8C00, #FF4AA2, #9059FF); background-size: 400% auto; - color: #000; background-clip: text; - -webkit-background-clip: text; animation: shine 50s linear infinite; @media (prefers-contrast: no-preference) { -webkit-text-fill-color: transparent; } @media (prefers-color-scheme: dark) { background-image: linear-gradient(90deg, #C688FF, #FF84C0, #FFBD4F, #FF84C0, #C688FF); } } @@ -821,16 +834,17 @@ body { button { display: inline-block; height: 45px; width: 152px; background-repeat: no-repeat; background-size: contain; background-position: center; box-shadow: none; + border: 0; } &:not(:first-child) { margin-inline: 5px 0; } } }
deleted file mode 100644 index 31f8c74dca5d6c76125efdf60cdd550a1ab563d1..0000000000000000000000000000000000000000 GIT binary patch literal 0 Hc$@<O00001
new file mode 100644 --- /dev/null +++ b/browser/components/newtab/data/content/assets/heart.svg @@ -0,0 +1,48 @@ +<!-- 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/. --> +<svg width="83" height="67" fill="none" xmlns="http://www.w3.org/2000/svg"> + <path d="M76.675 6.774A21.93 21.93 0 0 0 60.989 0h-.18a21.902 21.902 0 0 0-15.592 6.438L27.549 24.126a7.533 7.533 0 0 0 0 10.658l3.276 3.269a7.568 7.568 0 0 0 10.68 0l-8.591-8.505L50.56 11.86a14.355 14.355 0 0 1 10.25-4.242h.136a14.356 14.356 0 0 1 10.278 4.427c5.43 5.63 5.15 14.9-.615 20.672L45.467 57.931a5.544 5.544 0 0 1-7.868 0l4.041 4.048.651.658a6.602 6.602 0 0 0 8.805.45l24.82-24.991c8.748-8.676 9.041-22.724.759-31.322Z" fill="url(#a)"/> + <path d="M70.58 32.724 45.439 57.938a5.543 5.543 0 0 1-7.868 0l4.07 4.041.651.658a6.602 6.602 0 0 0 8.805.45l24.82-24.991-5.335-5.372Z" fill="url(#b)"/> + <path d="M41.533 38.032h-.036a7.56 7.56 0 0 1-10.672 0l5.344 5.336a7.548 7.548 0 0 0 10.671 0l8.584-8.584a7.567 7.567 0 0 0 0-10.679L37.806 6.481A21.923 21.923 0 0 0 22.213.043h-.207A21.945 21.945 0 0 0 6.32 6.773c-8.283 8.584-7.961 22.646.715 31.33L31.483 62.53a14.305 14.305 0 0 0 19.67.565 6.602 6.602 0 0 1-8.804-.45l-5.48-5.487-24.448-24.434C6.65 26.952 6.37 17.704 11.8 12.053a14.342 14.342 0 0 1 10.279-4.435h.107a14.355 14.355 0 0 1 10.235 4.234l17.66 17.575-8.533 8.583-.014.022Z" fill="url(#c)"/> + <path d="M41.533 38.032h-.036a7.56 7.56 0 0 1-10.672 0l5.344 5.336a7.548 7.548 0 0 0 10.671 0l8.584-8.584a7.567 7.567 0 0 0 0-10.679L37.806 6.481A21.923 21.923 0 0 0 22.213.043h-.207A21.945 21.945 0 0 0 6.32 6.773c-8.283 8.584-7.961 22.646.715 31.33L31.483 62.53a14.305 14.305 0 0 0 19.67.565 6.602 6.602 0 0 1-8.804-.45l-5.48-5.487-24.448-24.434C6.65 26.952 6.37 17.704 11.8 12.053a14.342 14.342 0 0 1 10.279-4.435h.107a14.355 14.355 0 0 1 10.235 4.234l17.66 17.575-8.533 8.583-.014.022Z" fill="url(#d)"/> + <path d="M45.188 6.48 27.55 24.127a7.533 7.533 0 0 0 0 10.658l3.276 3.269a7.568 7.568 0 0 0 10.68 0l-8.591-8.505L50.56 11.86l-5.372-5.378Z" fill="url(#e)"/> + <defs> + <linearGradient id="a" x1="54.008" y1="64.583" x2="54.008" y2="0" gradientUnits="userSpaceOnUse"> + <stop stop-color="#3A8EE6"/> + <stop offset=".24" stop-color="#5C79F0"/> + <stop offset=".63" stop-color="#9059FF"/> + <stop offset="1" stop-color="#C139E6"/> + </linearGradient> + <linearGradient id="b" x1="75.959" y1="48.653" x2="37.613" y2="48.653" gradientUnits="userSpaceOnUse"> + <stop offset=".14" stop-color="#6A2BEA" stop-opacity="0"/> + <stop offset=".34" stop-color="#642DE4" stop-opacity=".03"/> + <stop offset=".55" stop-color="#5131D3" stop-opacity=".12"/> + <stop offset=".76" stop-color="#3139B7" stop-opacity=".27"/> + <stop offset=".98" stop-color="#054490" stop-opacity=".48"/> + <stop offset="1" stop-color="#00458B" stop-opacity=".5"/> + </linearGradient> + <linearGradient id="c" x1="43.421" y1="8.848" x2="16.942" y2="54.719" gradientUnits="userSpaceOnUse"> + <stop stop-color="#FF980E"/> + <stop offset=".27" stop-color="#FF851B"/> + <stop offset=".56" stop-color="#FF7F1F"/> + <stop offset=".77" stop-color="#FF3750"/> + <stop offset=".9" stop-color="#F92261"/> + <stop offset="1" stop-color="#F5156C"/> + </linearGradient> + <linearGradient id="d" x1="43.421" y1="8.848" x2="16.942" y2="54.719" gradientUnits="userSpaceOnUse"> + <stop stop-color="#FFF261" stop-opacity=".8"/> + <stop offset=".06" stop-color="#FFF261" stop-opacity=".68"/> + <stop offset=".19" stop-color="#FFF261" stop-opacity=".48"/> + <stop offset=".31" stop-color="#FFF261" stop-opacity=".31"/> + <stop offset=".42" stop-color="#FFF261" stop-opacity=".17"/> + <stop offset=".53" stop-color="#FFF261" stop-opacity=".08"/> + <stop offset=".63" stop-color="#FFF261" stop-opacity=".02"/> + <stop offset=".72" stop-color="#FFF261" stop-opacity="0"/> + </linearGradient> + <linearGradient id="e" x1="37.95" y1="40.285" x2="37.95" y2="6.481" gradientUnits="userSpaceOnUse"> + <stop stop-color="#6E008B" stop-opacity=".5"/> + <stop offset=".5" stop-color="#C846CB" stop-opacity="0"/> + </linearGradient> + </defs> +</svg>
new file mode 100644 index 0000000000000000000000000000000000000000..fb9f7fdca58bd737b603713acbf1d8e718fc997a GIT binary patch literal 100396 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
--- a/browser/components/newtab/lib/ASRouter.jsm +++ b/browser/components/newtab/lib/ASRouter.jsm @@ -1427,24 +1427,22 @@ class _ASRouter { resetGroupsState() { const newGroupImpressions = {}; for (let { id } of this.state.groups) { newGroupImpressions[id] = []; } // Update storage this._storage.set("groupImpressions", newGroupImpressions); - // The groups parameter below can be removed once this method has test coverage return this.setState(({ groups }) => ({ groupImpressions: newGroupImpressions, })); } - // Until this method has test coverage, it should only be used for testing - _resetMessageState() { + resetMessageState() { const newMessageImpressions = {}; for (let { id } of this.state.messages) { newMessageImpressions[id] = []; } // Update storage this._storage.set("messageImpressions", newMessageImpressions); return this.setState(() => ({ messageImpressions: newMessageImpressions,
--- a/browser/components/newtab/lib/OnboardingMessageProvider.jsm +++ b/browser/components/newtab/lib/OnboardingMessageProvider.jsm @@ -55,17 +55,17 @@ const ONBOARDING_MESSAGES = () => [ transitions: true, screens: [ { id: "UPGRADE_PIN_FIREFOX", order: 0, content: { logo: { imageURL: - "chrome://activity-stream/content/data/content/assets/heart.gif", + "chrome://activity-stream/content/data/content/assets/heart.webp", height: "73px", }, has_noodles: true, title: { string_id: "fx100-upgrade-thanks-header", }, title_style: "fancy larger", background:
--- a/browser/components/newtab/test/browser/browser_aboutwelcome_configurable_ui.js +++ b/browser/components/newtab/test/browser/browser_aboutwelcome_configurable_ui.js @@ -173,20 +173,16 @@ add_task(async function test_aboutwelcom await test_element_styles( browser, "#mainContentHeader", // Expected styles: { "font-weight": "276", "font-size": "36px", animation: "50s linear 0s infinite normal none running shine", - }, - // Unexpected styles: - { - color: "rgb(21, 20, 26)", } ); }); /** * Test rendering a screen with an image for the dialog window's background */ add_task(async function test_aboutwelcome_with_background() {
--- a/browser/components/newtab/test/unit/asrouter/ASRouter.test.js +++ b/browser/components/newtab/test/unit/asrouter/ASRouter.test.js @@ -2863,9 +2863,51 @@ describe("ASRouter", () => { .get(() => "unkown"); sandbox.stub(global.RemoteL10n, "isLocaleSupported").returns(false); await MessageLoaderUtils._remoteSettingsLoader(provider, {}); assert.notCalled(spy); }); }); + describe("#resetMessageState", () => { + it("should reset all message impressions", async () => { + await Router.setState({ + messages: [{ id: "1" }, { id: "2" }], + }); + await Router.setState({ + messageImpressions: { "1": [0, 1, 2], "2": [0, 1, 2] }, + }); // Add impressions for test messages + let impressions = Object.values(Router.state.messageImpressions); + assert.equal(impressions.filter(i => i.length).length, 2); // Both messages have impressions + + Router.resetMessageState(); + impressions = Object.values(Router.state.messageImpressions); + + assert.isEmpty(impressions.filter(i => i.length)); // Both messages now have zero impressions + assert.calledWithExactly(Router._storage.set, "messageImpressions", { + "1": [], + "2": [], + }); + }); + }); + describe("#resetGroupsState", () => { + it("should reset all group impressions", async () => { + await Router.setState({ + groups: [{ id: "1" }, { id: "2" }], + }); + await Router.setState({ + groupImpressions: { "1": [0, 1, 2], "2": [0, 1, 2] }, + }); // Add impressions for test groups + let impressions = Object.values(Router.state.groupImpressions); + assert.equal(impressions.filter(i => i.length).length, 2); // Both groups have impressions + + Router.resetGroupsState(); + impressions = Object.values(Router.state.groupImpressions); + + assert.isEmpty(impressions.filter(i => i.length)); // Both groups now have zero impressions + assert.calledWithExactly(Router._storage.set, "groupImpressions", { + "1": [], + "2": [], + }); + }); + }); });
--- a/browser/components/places/SnapshotMonitor.jsm +++ b/browser/components/places/SnapshotMonitor.jsm @@ -15,26 +15,48 @@ XPCOMUtils.defineLazyModuleGetters(this, Services: "resource://gre/modules/Services.jsm", setTimeout: "resource://gre/modules/Timer.jsm", Snapshots: "resource:///modules/Snapshots.jsm", }); XPCOMUtils.defineLazyPreferenceGetter( this, "SNAPSHOT_ADDED_TIMER_DELAY", - "browser.places.snapshot.monitorDelayAdded", + "browser.places.snapshots.monitorDelayAdded", 5000 ); XPCOMUtils.defineLazyPreferenceGetter( this, "SNAPSHOT_REMOVED_TIMER_DELAY", - "browser.places.snapshot.monitorDelayRemoved", + "browser.places.snapshots.monitorDelayRemoved", 1000 ); +// Expiration days for automatic and user managed snapshots. +XPCOMUtils.defineLazyPreferenceGetter( + this, + "SNAPSHOT_EXPIRE_DAYS", + "browser.places.snapshots.expiration.days", + 210 +); +XPCOMUtils.defineLazyPreferenceGetter( + this, + "SNAPSHOT_USERMANAGED_EXPIRE_DAYS", + "browser.places.snapshots.expiration.userManaged.days", + 420 +); +// We expire on the next idle after a snapshot was added or removed, and +// idle-daily, but we don't want to expire too often or rarely. +// Thus we define both a mininum and maximum time in the session among which +// we'll expire chunks of snapshots. +const EXPIRE_EVERY_MIN_MS = 60 * 60000; // 1 Hour. +const EXPIRE_EVERY_MAX_MS = 120 * 60000; // 2 Hours. +// The number of snapshots to expire at once. +const EXPIRE_CHUNK_SIZE = 10; + /** * Monitors changes in snapshots (additions, deletions, etc) and triggers * the snapshot group builders to run as necessary. */ const SnapshotMonitor = new (class SnapshotMonitor { /** * @type {number} */ @@ -77,16 +99,25 @@ const SnapshotMonitor = new (class Snaps * Test-only. Used to specify one or more builders to use instead of the * built-in group builders. * * @type {object[]} */ testGroupBuilders = null; /** + * The time of the last snapshots expiration. + */ + #lastExpirationTime = 0; + /** + * How many snapshots to expire per chunk. + */ + #expirationChunkSize = EXPIRE_CHUNK_SIZE; + + /** * Internal getter to get the builders used. * * @returns {object[]} */ get #groupBuilders() { if (this.testGroupBuilders) { return this.testGroupBuilders; } @@ -165,16 +196,91 @@ const SnapshotMonitor = new (class Snaps } } this.#addedItems.clear(); this.#removedUrls.clear(); } /** + * Triggers expiration of a chunk of snapshots. + * We differentiate snapshots depending on whether they are user managed: + * 1. manually created by the user + * 2. part of a group + * TODO: evaluate whether we want to consider user managed only snapshots + * that are part of a user curated group, rather than any group. + * User managed snapshots will expire if their last interaction is older than + * browser.snapshots.expiration.userManaged.days, while others will expire + * after browser.snapshots.expiration.days. + * Snapshots that have a tombstone (removed_at is set) should not be expired. + * + * @param {boolean} onIdle + * Whether this is running on idle. When it's false expiration is + * rescheduled for the next idle. + */ + async #expireSnapshotsChunk(onIdle = false) { + let now = Date.now(); + if (now - this.#lastExpirationTime < EXPIRE_EVERY_MIN_MS) { + return; + } + let instance = (this._expireInstance = {}); + let skip = false; + if (!onIdle) { + // Wait for the next idle. + skip = await new Promise(resolve => + ChromeUtils.idleDispatch(deadLine => { + // Skip if we couldn't find an idle, unless we're over max waiting time. + resolve( + deadLine.didTimeout && + now - this.#lastExpirationTime < EXPIRE_EVERY_MAX_MS + ); + }) + ); + } + if (skip || instance != this._expireInstance) { + return; + } + + this.#lastExpirationTime = now; + let urls = ( + await Snapshots.query({ + includeUserPersisted: false, + includeTombstones: false, + group: null, + lastInteractionBefore: now - SNAPSHOT_EXPIRE_DAYS * 86400000, + limit: this.#expirationChunkSize, + }) + ).map(s => s.url); + if (instance != this._expireInstance) { + return; + } + + if (urls.length < this.#expirationChunkSize) { + // If we couldn't find enough automatic snapshots, check if there's any + // user managed ones we can expire. + urls.push( + ...( + await Snapshots.query({ + includeUserPersisted: true, + includeTombstones: false, + lastInteractionBefore: + now - SNAPSHOT_USERMANAGED_EXPIRE_DAYS * 86400000, + limit: this.#expirationChunkSize - urls.length, + }) + ).map(s => s.url) + ); + } + if (instance != this._expireInstance) { + return; + } + + await Snapshots.delete([...new Set(urls)], true); + } + + /** * Sets a timer ensuring that if the new timeout would occur sooner than the * current target time, the timer is changed to the sooner time. * * @param {number} timeout * The timeout in milliseconds to use. */ #setTimer(timeout) { let targetTime = Date.now() + timeout; @@ -183,37 +289,49 @@ const SnapshotMonitor = new (class Snaps return; } if (this.#timer) { clearTimeout(this.#timer); } this.#currentTargetTime = targetTime; - this.#timer = setTimeout( - () => this.#triggerBuilders().catch(console.error), - timeout - ); + this.#timer = setTimeout(() => { + this.#expireSnapshotsChunk().catch(console.error); + this.#triggerBuilders().catch(console.error); + }, timeout); } /** * observe function for nsIObserver. This is async so that we can call it in * tests and know that the triggerBuilders for idle-daily has finished. * * @param {object} subject * @param {string} topic * @param {nsISupports} data */ async observe(subject, topic, data) { - if (topic == "places-snapshots-added") { - this.#onSnapshotAdded(JSON.parse(data)); - } else if (topic == "places-snapshots-deleted") { - this.#onSnapshotRemoved(JSON.parse(data)); - } else if (topic == "idle-daily") { - await this.#triggerBuilders(true); + switch (topic) { + case "places-snapshots-added": + this.#onSnapshotAdded(JSON.parse(data)); + break; + case "places-snapshots-deleted": + this.#onSnapshotRemoved(JSON.parse(data)); + break; + case "idle-daily": + await this.#expireSnapshotsChunk(true); + await this.#triggerBuilders(true); + break; + case "test-expiration": + this.#lastExpirationTime = + subject.lastExpirationTime || this.#lastExpirationTime; + this.#expirationChunkSize = + subject.expirationChunkSize || this.#expirationChunkSize; + await this.#expireSnapshotsChunk(subject.onIdle); + break; } } /** * Handles snapshots being added - adds to the internal list and sets the * timer. * * @param {object[]} items
--- a/browser/components/places/Snapshots.jsm +++ b/browser/components/places/Snapshots.jsm @@ -391,43 +391,68 @@ const Snapshots = new (class Snapshots { if (placeId) { await this.#addPageData([{ placeId, url }]); this.#notify("places-snapshots-added", [{ url, userPersisted }]); } } /** - * Deletes a snapshot, creating a tombstone. Note, the caller is expected - * to take account of the userPersisted value for a Snapshot when appropriate. + * Deletes one or more snapshots. + * By default this creates a tombstone rather than removing the entry, so that + * heuristics can take into account user removed snapshots. + * Note, the caller is expected to take account of the userPersisted value + * for a Snapshot when appropriate. * - * @param {string} url - * The url of the snapshot to delete. + * @param {string|Array<string>} urls + * The url of the snapshot to delete, or an Array of urls. + * @param {boolean} removeFromStore + * Whether the snapshot should actually be removed rather than tombston-ed. */ - async delete(url) { - url = this.stripFragments(url); + async delete(urls, removeFromStore = false) { + if (!Array.isArray(urls)) { + urls = [urls]; + } + urls = urls.map(this.stripFragments); + + let placeIdsSQLFragment = ` + SELECT id FROM moz_places + WHERE url_hash IN (${PlacesUtils.sqlBindPlaceholders( + urls, + "hash(", + ")" + )}) AND url IN (${PlacesUtils.sqlBindPlaceholders(urls)})`; + let queryArgs = removeFromStore + ? [ + `DELETE FROM moz_places_metadata_snapshots + WHERE place_id IN (${placeIdsSQLFragment}) + RETURNING place_id`, + [...urls, ...urls], + ] + : [ + `UPDATE moz_places_metadata_snapshots + SET removed_at = ? + WHERE place_id IN (${placeIdsSQLFragment}) + RETURNING place_id`, + [Date.now(), ...urls, ...urls], + ]; + await PlacesUtils.withConnectionWrapper("Snapshots: delete", async db => { - let placeId = ( - await db.executeCached( - `UPDATE moz_places_metadata_snapshots - SET removed_at = :removedAt - WHERE place_id = (SELECT id FROM moz_places WHERE url_hash = hash(:url) AND url = :url) - RETURNING place_id`, - { removedAt: Date.now(), url } - ) - )[0].getResultByName("place_id"); + let placeIds = (await db.executeCached(...queryArgs)).map(r => + r.getResultByName("place_id") + ); // Remove orphan page data. await db.executeCached( `DELETE FROM moz_places_metadata_snapshots_extra - WHERE place_id = :placeId`, - { placeId } + WHERE place_id IN (${PlacesUtils.sqlBindPlaceholders(placeIds)})`, + placeIds ); }); - this.#notify("places-snapshots-deleted", [url]); + this.#notify("places-snapshots-deleted", urls); } /** * Gets the details for a particular snapshot based on the url. * * @param {string} url * The url of the snapshot to obtain. * @param {boolean} [includeTombstones] @@ -474,54 +499,75 @@ const Snapshots = new (class Snapshots { * @param {number} [options.limit] * A numerical limit to the number of snapshots to retrieve, defaults to 100. * -1 may be used to get all snapshots, e.g. for use by the group builders. * @param {boolean} [options.includeTombstones] * Whether to include tombstones in the snapshots to obtain. * @param {number} [options.type] * Restrict the snapshots to those with a particular type of page data available. * @param {number} [options.group] - * Restrict the snapshots to those within a particular group. + * Restrict the snapshots to those within a particular group. Pass null + * to get all the snapshots that are not part of a group. * @param {boolean} [options.includeHiddenInGroup] * Only applies when querying a particular group. Pass true to include * snapshots that are hidden in the group. + * @param {boolean} [options.includeUserPersisted] + * Whether to include user persisted snapshots. + * @param {number} [options.lastInteractionBefore] + * Restrict to snaphots whose last interaction was before the given time. * @param {boolean} [options.sortDescending] * Whether or not to sortDescending. Defaults to true. * @param {string} [options.sortBy] * A string to choose what to sort the snapshots by, e.g. "last_interaction_at" * By default results are sorted by last_interaction_at. * @returns {Snapshot[]} * Returns snapshots in order of descending last interaction time. */ async query({ limit = 100, includeTombstones = false, type = undefined, group = undefined, includeHiddenInGroup = false, + includeUserPersisted = true, + lastInteractionBefore = undefined, sortDescending = true, sortBy = "last_interaction_at", } = {}) { let db = await PlacesUtils.promiseDBConnection(); let clauses = []; let bindings = {}; let joins = []; let limitStatement = ""; if (!includeTombstones) { clauses.push("removed_at IS NULL"); } + if (!includeUserPersisted) { + clauses.push("user_persisted = :user_persisted"); + bindings.user_persisted = this.USER_PERSISTED.NO; + } + if (lastInteractionBefore) { + clauses.push("last_interaction_at < :last_interaction_before"); + bindings.last_interaction_before = lastInteractionBefore; + } + if (type) { clauses.push("type = :type"); bindings.type = type; } - if (group) { + if (group === null) { + clauses.push("group_id IS NULL"); + joins.push( + "LEFT JOIN moz_places_metadata_groups_to_snapshots g USING(place_id)" + ); + } else if (group) { clauses.push("group_id = :group"); if (!includeHiddenInGroup) { clauses.push("g.hidden = 0"); } bindings.group = group; joins.push( "LEFT JOIN moz_places_metadata_groups_to_snapshots g USING(place_id)" );
new file mode 100644 --- /dev/null +++ b/browser/components/places/tests/unit/interactions/test_snapshots_expiration.js @@ -0,0 +1,187 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests for the expiration of snapshots. + */ + +XPCOMUtils.defineLazyPreferenceGetter( + this, + "SNAPSHOT_EXPIRE_DAYS", + "browser.places.snapshots.expiration.days", + 210 +); +XPCOMUtils.defineLazyPreferenceGetter( + this, + "SNAPSHOT_USERMANAGED_EXPIRE_DAYS", + "browser.places.snapshots.expiration.userManaged.days", + 420 +); + +// For each snapshot define its url, whether it should be expired, whether it +// should be a tomstone and a type. +// The type may be: +// - manual: user_persisted +// - auto: created automatically by some heuristic +// - group: part of a group +let gSnapshots = [ + { + url: "https://example.com/1", + type: "manual", + expired: true, + tombstone: false, + }, + { + url: "https://example.com/2", + type: "manual", + expired: false, + tombstone: false, + }, + { + url: "https://example.com/3", + type: "group", + expired: true, + tombstone: false, + }, + { + url: "https://example.com/4", + type: "auto", + expired: true, + tombstone: false, + }, + { + url: "https://example.com/5", + type: "auto", + expired: false, + tombstone: false, + }, + { + url: "https://example.com/6", + type: "auto", + expired: true, + tombstone: true, + }, +]; + +add_task(async function setup() { + let now = Date.now(); + let interactions = gSnapshots.map(s => { + if (s.expired) { + s.created_at = + now - + (1 + s.type != "auto" + ? SNAPSHOT_USERMANAGED_EXPIRE_DAYS + : SNAPSHOT_EXPIRE_DAYS) * + 86400000; + } else { + s.created_at = + now - (s.type != "auto" ? SNAPSHOT_EXPIRE_DAYS : 1) * 86400000; + } + return s; + }); + await addInteractions(interactions); + + let groupSerial = 1; + for (let snapshot of gSnapshots) { + if (snapshot.type == "manual") { + snapshot.userPersisted = Snapshots.USER_PERSISTED.MANUAL; + } + await Snapshots.add(snapshot); + if (snapshot.type == "group") { + snapshot.group = await SnapshotGroups.add( + { + title: `test-group-${groupSerial++}`, + builder: "test", + }, + [snapshot.url] + ); + } + if (snapshot.tombstone) { + await Snapshots.delete(snapshot.url); + } + } + + Services.prefs.setBoolPref("browser.places.interactions.enabled", true); + + SnapshotMonitor.init(); +}); + +add_task(async function test_idle_expiration() { + await SnapshotMonitor.observe({ onIdle: true }, "test-expiration"); + + let remaining = await Snapshots.query({ includeTombstones: true }); + for (let snapshot of gSnapshots) { + let index = remaining.findIndex(s => s.url == snapshot.url); + if (!snapshot.expired || snapshot.tombstone) { + Assert.greater(index, -1, `${snapshot.url} should not have been removed`); + remaining.splice(index, 1); + } else { + Assert.equal(index, -1, `${snapshot.url} should have been removed`); + } + } + Assert.ok( + !remaining.length, + `All the snapshots should be processed: ${JSON.stringify(remaining)}` + ); +}); + +add_task(async function test_active_limited_expiration() { + // Add 2 expirable snapshots. + let now = Date.now(); + let expiredSnapshots = [ + { + url: "https://example.com/7", + created_at: now - (SNAPSHOT_USERMANAGED_EXPIRE_DAYS + 1) * 86400000, + }, + { + url: "https://example.com/8", + created_at: now - (SNAPSHOT_USERMANAGED_EXPIRE_DAYS + 1) * 86400000, + }, + ]; + for (let snapshot of expiredSnapshots) { + await addInteractions([snapshot]); + await Snapshots.add(snapshot); + } + + let snapshots = await Snapshots.query({ includeTombstones: true }); + + info("expire again without setting lastExpirationTime, should be a no-op"); + let expirationChunkSize = 1; + await SnapshotMonitor.observe( + { + expirationChunkSize, + }, + "test-expiration" + ); + + // Since expiration just ran, nothing should have been expired. + Assert.equal( + (await Snapshots.query({ includeTombstones: true })).length, + snapshots.length, + "No snapshot should have been expired." + ); + + info("expire again, for real"); + await SnapshotMonitor.observe( + { + expirationChunkSize, + lastExpirationTime: now - 24 * 86400000, + }, + "test-expiration" + ); + + let remaining = await Snapshots.query({ includeTombstones: true }); + + let count = 0; + for (let snapshot of expiredSnapshots) { + let index = remaining.findIndex(s => s.url == snapshot.url); + if (index == -1) { + count++; + } + } + Assert.equal( + count, + expiredSnapshots.length - expirationChunkSize, + "Check the expected number of snapshots have been expired" + ); +});
--- a/browser/components/places/tests/unit/interactions/xpcshell.ini +++ b/browser/components/places/tests/unit/interactions/xpcshell.ini @@ -15,16 +15,17 @@ skip-if = toolkit == 'android' # bug 173 [test_pinnedgroupbuilder.js] [test_snapshot_added_no_interaction.js] [test_snapshot_groups.js] [test_snapshot_group_triggers.js] [test_snapshots_basics.js] [test_snapshots_common_referrer_queries.js] [test_snapshots_create_allow_protocols.js] [test_snapshots_create_criteria.js] +[test_snapshots_expiration.js] [test_snapshots_fragments.js] [test_snapshots_overlapping_queries.js] [test_snapshots_pagedata.js] [test_snapshots_page_image.js] [test_snapshots_protocols.js] [test_snapshots_queries.js] [test_snapshotscorer_combining.js] [test_snapshotscorer.js]
--- a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_focus_promo.js +++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_focus_promo.js @@ -8,17 +8,17 @@ const intialCurrentRegion = Region._curr // Helper to run tests for specific regions async function setupRegions(home, current) { Region._setHomeRegion(home || ""); Region._setCurrentRegion(current || ""); } add_task(async function test_focus_promo_in_allowed_region() { - await ASRouter._resetMessageState(); + ASRouter.resetMessageState(); const allowedRegion = "ES"; // Spain setupRegions(allowedRegion, allowedRegion); const { win, tab } = await openTabAndWaitForRender(); await SpecialPowers.spawn(tab, [], async function() { const promoContainer = content.document.querySelector(".promo"); // container which is present if promo is enabled and should show @@ -26,17 +26,17 @@ add_task(async function test_focus_promo ok(promoContainer, "Focus promo is shown for allowed region"); }); await BrowserTestUtils.closeWindow(win); setupRegions(initialHomeRegion, intialCurrentRegion); // revert changes to regions }); add_task(async function test_focus_promo_in_disallowed_region() { - await ASRouter._resetMessageState(); + ASRouter.resetMessageState(); const disallowedRegion = "CN"; // China setupRegions(disallowedRegion); const { win, tab } = await openTabAndWaitForRender(); await SpecialPowers.spawn(tab, [], async function() { const promoContainer = content.document.querySelector(".promo"); // container which is removed if promo is disabled and/or should not show
--- a/browser/components/sessionstore/SessionFile.jsm +++ b/browser/components/sessionstore/SessionFile.jsm @@ -335,45 +335,48 @@ var SessionFileInternal = { } this._readOrigin = result.origin; result.noFilesFound = noFilesFound; return result; }, - // Initialize SessionWriter. - // This should be called _before_ any other methods on SessionWriter (see - // `_callWriter()`). - _initWriter() { - if (this._initialized) { - return; - } + // Initialize SessionWriter and return it as a resolved promise. + getWriter() { + if (!this._initialized) { + if (!this._readOrigin) { + return Promise.reject( + "SessionFileInternal.getWriter() called too early! Please read the session file from disk first." + ); + } - if (!this._readOrigin) { - throw new Error( - "_initWriter called too early! Please read the session file from disk first." + this._initialized = true; + SessionWriter.init( + this._readOrigin, + this._usingOldExtension, + this.Paths, + { + maxUpgradeBackups: Services.prefs.getIntPref( + PREF_MAX_UPGRADE_BACKUPS, + 3 + ), + maxSerializeBack: Services.prefs.getIntPref( + PREF_MAX_SERIALIZE_BACK, + 10 + ), + maxSerializeForward: Services.prefs.getIntPref( + PREF_MAX_SERIALIZE_FWD, + -1 + ), + } ); } - this._initialized = true; - SessionWriter.init(this._readOrigin, this._usingOldExtension, this.Paths, { - maxUpgradeBackups: Services.prefs.getIntPref(PREF_MAX_UPGRADE_BACKUPS, 3), - maxSerializeBack: Services.prefs.getIntPref(PREF_MAX_SERIALIZE_BACK, 10), - maxSerializeForward: Services.prefs.getIntPref( - PREF_MAX_SERIALIZE_FWD, - -1 - ), - }); - }, - - // Call a method of SessionWriter, making sure that it has been initialized first. - _callWriter(method, args = []) { - this._initWriter(); - return SessionWriter[method](...args); + return Promise.resolve(SessionWriter); }, write(aData) { if (RunState.isClosed) { return Promise.reject(new Error("SessionFile is closed")); } let isFinalWrite = false; @@ -383,17 +386,17 @@ var SessionFileInternal = { isFinalWrite = true; RunState.setClosed(); } let performShutdownCleanup = isFinalWrite && !SessionStore.willAutoRestore; this._attempts++; let options = { isFinalWrite, performShutdownCleanup }; - let promise = this._callWriter("write", [aData, options]); + let promise = this.getWriter().then(writer => writer.write(aData, options)); // Wait until the write is done. promise = promise.then( msg => { // Record how long the write took. this._recordTelemetry(msg.telemetry); this._successes++; if (msg.result.upgradeBackup) { @@ -441,22 +444,22 @@ var SessionFileInternal = { Services.obs.notifyObservers( null, "sessionstore-final-state-write-complete" ); } }); }, - wipe() { - return this._callWriter("wipe").then(() => { - // After a wipe, we need to make sure to re-initialize upon the next read(), - // because the state variables as sent to the writer have changed. - this._initialized = false; - }); + async wipe() { + const writer = await this.getWriter(); + await writer.wipe(); + // After a wipe, we need to make sure to re-initialize upon the next read(), + // because the state variables as sent to the writer have changed. + this._initialized = false; }, _recordTelemetry(telemetry) { for (let id of Object.keys(telemetry)) { let value = telemetry[id]; let samples = []; if (Array.isArray(value)) { samples.push(...value);
--- a/browser/components/urlbar/UrlbarPrefs.jsm +++ b/browser/components/urlbar/UrlbarPrefs.jsm @@ -223,16 +223,30 @@ const PREF_URLBAR_DEFAULTS = new Map([ // JSON'ed array of blocked quick suggest URL digests. ["quickSuggest.blockedDigests", ""], // Global toggle for whether the quick suggest feature is enabled, i.e., // sponsored and recommended results related to the user's search string. ["quicksuggest.enabled", false], + // Whether non-sponsored quick suggest results are subject to impression + // frequency caps. This pref is a fallback for the Nimbus variable + // `quickSuggestImpressionCapsNonSponsoredEnabled`. + ["quicksuggest.impressionCaps.nonSponsoredEnabled", false], + + // Whether sponsored quick suggest results are subject to impression frequency + // caps. This pref is a fallback for the Nimbus variable + // `quickSuggestImpressionCapsSponsoredEnabled`. + ["quicksuggest.impressionCaps.sponsoredEnabled", false], + + // JSON'ed object of quick suggest impression stats. Used for implementing + // impression frequency caps for quick suggest suggestions. + ["quicksuggest.impressionCaps.stats", ""], + // Whether to show QuickSuggest related logs. ["quicksuggest.log", false], // The user's response to the Firefox Suggest online opt-in dialog. ["quicksuggest.onboardingDialogChoice", ""], // If the user has gone through a quick suggest prefs migration, then this // pref will have a user-branch value that records the latest prefs version.
--- a/browser/components/urlbar/UrlbarProviderQuickSuggest.jsm +++ b/browser/components/urlbar/UrlbarProviderQuickSuggest.jsm @@ -60,32 +60,52 @@ const TELEMETRY_SCALARS = { HELP_SPONSORED_BEST_MATCH: "contextual.services.quicksuggest.help_sponsored_bestmatch", HELP_NONSPONSORED_BEST_MATCH: "contextual.services.quicksuggest.help_nonsponsored_bestmatch", }; 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", +}; + // Identifies the source of the QuickSuggest suggestion. const QUICK_SUGGEST_SOURCE = { REMOTE_SETTINGS: "remote-settings", MERINO: "merino", }; /** * A provider that returns a suggested url to the user based on what * they have currently typed so they can navigate directly. */ class ProviderQuickSuggest extends UrlbarProvider { constructor(...args) { super(...args); - this._updateExperimentState(); + + UrlbarQuickSuggest.init(); + UrlbarQuickSuggest.on("config-set", () => this._validateImpressionStats()); + + this._updateFeatureState(); + NimbusFeatures.urlbar.onUpdate(() => this._updateFeatureState()); + UrlbarPrefs.addObserver(this); - NimbusFeatures.urlbar.onUpdate(() => this._updateExperimentState()); } /** * Returns the name of this provider. * @returns {string} the name of this provider. */ get name() { return "UrlbarProviderQuickSuggest"; @@ -189,16 +209,26 @@ class ProviderQuickSuggest extends Urlba if ( UrlbarPrefs.get("merinoEnabled") && UrlbarPrefs.get("quicksuggest.dataCollection.enabled") && queryContext.allowRemoteResults() ) { promises.push(this._fetchMerinoSuggestions(queryContext, searchString)); } + // While we're waiting on suggestions, opportunistically reset elapsed + // impression counters and record "reset" telemetry as appropriate. If we + // didn't need to record telemetry for periods with no impressions, then we + // could simply reset elapsed counters on each impression instead of doing + // it here. But since we do need to record telemetry for periods with no + // impressions, we need to reset counters more often. Doing it here means no + // telemetry will be recorded as long as the user doesn't do any searches, + // but the alternative is to use one or more long-lived timers. + this._resetElapsedImpressionCounters(); + // Wait for both sources to finish before adding a suggestion. let allSuggestions = await Promise.all(promises); if (instance != this.queryInstance) { return; } // Filter suggestions, keeping in mind both the remote settings and Merino // fetches return null when there are no matches. Take the remaining one @@ -416,17 +446,17 @@ class ProviderQuickSuggest extends Urlba * it describes the search string and picked result. */ onEngagement(isPrivate, state, queryContext, details) { if (!this._addedResultInLastQuery) { return; } this._addedResultInLastQuery = false; - // Per spec, we update telemetry only when the user picks a result, i.e., + // Per spec, we count impressions only when the user picks a result, i.e., // when `state` is "engagement". if (state != "engagement") { return; } // Get the index of the quick suggest result. Usually it will be last, so to // avoid an O(n) lookup in the common case, check the last result first. It // may not be last if `browser.urlbar.showSearchSuggestionsFirst` is false @@ -439,16 +469,19 @@ class ProviderQuickSuggest extends Urlba ); if (resultIndex < 0) { this.logger.error(`Could not find quick suggest result`); return; } result = queryContext.results[resultIndex]; } + // Update impression stats. + this._updateImpressionStats(result.payload.isSponsored); + // Record telemetry. We want to record the 1-based index of the result, so // add 1 to the 0-based resultIndex. let telemetryResultIndex = resultIndex + 1; // impression scalars Services.telemetry.keyedScalarAdd( TELEMETRY_SCALARS.IMPRESSION, telemetryResultIndex, @@ -541,21 +574,27 @@ class ProviderQuickSuggest extends Urlba * 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": - this.logger.debug( - "browser.urlbar.quickSuggest.blockedDigests changed, loading digests" - ); + 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 (!UrlbarPrefs.updatingFirefoxSuggestScenario) { Services.telemetry.recordEvent( TELEMETRY_EVENT_CATEGORY, "data_collect_toggled", UrlbarPrefs.get(pref) ? "enabled" : "disabled" ); } @@ -836,28 +875,61 @@ class ProviderQuickSuggest extends Urlba })); } /** * Returns whether a given suggestion can be added for a query, assuming the * provider itself should be active. * * @param {object} suggestion - * A suggestion object fetched from UrlbarQuickSuggest. * @returns {boolean} * Whether the suggestion can be added. */ async _canAddSuggestion(suggestion) { - return ( - ((suggestion.is_sponsored && - UrlbarPrefs.get("suggest.quicksuggest.sponsored")) || - (!suggestion.is_sponsored && - UrlbarPrefs.get("suggest.quicksuggest.nonsponsored"))) && - !(await this.isSuggestionBlocked(suggestion.url)) - ); + this.logger.info("Checking if suggestion can be added"); + this.logger.debug(JSON.stringify({ suggestion })); + + // Return false if suggestions are disabled. + if ( + (suggestion.is_sponsored && + !UrlbarPrefs.get("suggest.quicksuggest.sponsored")) || + (!suggestion.is_sponsored && + !UrlbarPrefs.get("suggest.quicksuggest.nonsponsored")) + ) { + this.logger.info("Suggestions disabled, not adding suggestion"); + return false; + } + + // Return false if an impression cap has been hit. + if ( + (suggestion.is_sponsored && + UrlbarPrefs.get("quickSuggestImpressionCapsSponsoredEnabled")) || + (!suggestion.is_sponsored && + UrlbarPrefs.get("quickSuggestImpressionCapsNonSponsoredEnabled")) + ) { + let type = suggestion.is_sponsored ? "sponsored" : "nonsponsored"; + let stats = this._impressionStats?.[type]; + if (stats) { + let hitStats = stats.filter(s => s.maxCount <= s.count); + if (hitStats.length) { + this.logger.info("Impression cap(s) hit, not adding suggestion"); + this.logger.debug(JSON.stringify({ type, hitStats })); + return false; + } + } + } + + // Return false if the suggestion is blocked. + if (await this.isSuggestionBlocked(suggestion.url)) { + this.logger.info("Suggestion blocked, not adding suggestion"); + return false; + } + + this.logger.info("Suggestion can be added"); + return true; } /** * 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 @@ -893,16 +965,332 @@ class ProviderQuickSuggest extends Urlba 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 {boolean} isSponsored + * Whether the impression was recorded for a sponsored suggestion. + */ + _updateImpressionStats(isSponsored) { + this.logger.info("Starting impression stats update"); + this.logger.debug( + JSON.stringify({ + isSponsored, + currentStats: this._impressionStats, + impression_caps: UrlbarQuickSuggest.config.impression_caps, + }) + ); + + // Don't bother recording anything if caps are disabled. + if ( + (isSponsored && + !UrlbarPrefs.get("quickSuggestImpressionCapsSponsoredEnabled")) || + (!isSponsored && + !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 type = isSponsored ? "sponsored" : "nonsponsored"; + 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 { + 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 })); + } + + /** + * Loads and validates impression stats. + */ + _loadImpressionStats() { + let json = UrlbarPrefs.get("quicksuggest.impressionCaps.stats"); + if (!json) { + this._impressionStats = null; + } 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 } = 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: 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) { + this.logger.info( + `Resetting impression counter for interval ${stat.intervalSeconds}s` + ); + this.logger.debug( + JSON.stringify({ type, stat, elapsedMs, elapsedIntervalCount }) + ); + + // Record a telemetry event for each elapsed interval period. + let startDateMs = stat.startDateMs; + for (let i = 0; i < elapsedIntervalCount; i++) { + let endDateMs = startDateMs + intervalMs; + this._recordImpressionCapEvent({ + eventType: "reset", + suggestionType: type, + eventDateMs: endDateMs, + stat: { + ...stat, + startDateMs, + // There were `stat.count` impressions in the first elapsed + // period and zero in all subsequent periods because if that + // were not the case then we would have recorded telemetry for + // the subsequent impression(s). + count: i == 0 ? stat.count : 0, + }, + }); + startDateMs += intervalMs; + } + + // Reset the stat. + let remainderMs = elapsedMs - elapsedIntervalCount * intervalMs; + stat.startDateMs = now - remainderMs; + stat.count = 0; + } + } + } + + this.logger.debug(JSON.stringify({ newStats: this._impressionStats })); + } + + /** + * Records an impression cap telemetry event. + * + * @param {string} eventType + * One of: "hit", "reset" + * @param {string} suggestionType + * One of: "sponsored", "nonsponsored" + * @param {object} stat + * The stats object whose max count was hit or whose counter was reset. + * @param {number} 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, + eventDateMs = Date.now(), + }) { + // All `extra` object values must be strings. + let extra = { + type: suggestionType, + eventDate: String(eventDateMs), + endDate: String(stat.startDateMs + 1000 * stat.intervalSeconds), + }; + 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 + ); + } + + /** * 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 = UrlbarPrefs.get("quickSuggest.blockedDigests"); this.logger.debug( @@ -935,37 +1323,96 @@ class ProviderQuickSuggest extends Urlba 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. - * Enable/disable event telemetry and ensure QuickSuggest module is loaded - * when enabled. */ - _updateExperimentState() { + _updateFeatureState() { + let enabled = 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, - UrlbarPrefs.get("quickSuggestEnabled") + enabled ); - - // QuickSuggest is only loaded by the UrlBar on it's first query, however - // there is work it can preload when idle instead of starting it on user - // input. Referencing it here will trigger its import and init. - if (UrlbarPrefs.get("quickSuggestEnabled")) { - UrlbarQuickSuggest; // eslint-disable-line no-unused-expressions + 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; + // Whether we added a result during the most recent query. _addedResultInLastQuery = 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 = null; + + // 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
--- a/browser/components/urlbar/UrlbarQuickSuggest.jsm +++ b/browser/components/urlbar/UrlbarQuickSuggest.jsm @@ -7,16 +7,17 @@ const EXPORTED_SYMBOLS = ["ONBOARDING_CHOICE", "UrlbarQuickSuggest"]; const { XPCOMUtils } = ChromeUtils.import( "resource://gre/modules/XPCOMUtils.jsm" ); XPCOMUtils.defineLazyModuleGetters(this, { BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.jsm", + EventEmitter: "resource://gre/modules/EventEmitter.jsm", NimbusFeatures: "resource://nimbus/ExperimentAPI.jsm", QUICK_SUGGEST_SOURCE: "resource:///modules/UrlbarProviderQuickSuggest.jsm", RemoteSettings: "resource://services-settings/remote-settings.js", Services: "resource://gre/modules/Services.jsm", TaskQueue: "resource:///modules/UrlbarUtils.jsm", UrlbarPrefs: "resource:///modules/UrlbarPrefs.jsm", UrlbarProviderQuickSuggest: "resource:///modules/UrlbarProviderQuickSuggest.jsm", @@ -61,18 +62,18 @@ const ONBOARDING_URI = // suggestions don't have a natural score so we hardcode a value, and we choose // a low value to allow Merino to experiment with a broad range of scores. const SUGGESTION_SCORE = 0.2; /** * Fetches the suggestions data from RemoteSettings and builds the structures * to provide suggestions for UrlbarProviderQuickSuggest. */ -class Suggestions { - constructor() { +class QuickSuggest extends EventEmitter { + init() { UrlbarPrefs.addObserver(this); NimbusFeatures.urlbar.onUpdate(() => this._queueSettingsSetup()); this._settingsTaskQueue.queue(() => { return new Promise(resolve => { Services.tm.idleDispatchToMainThread(() => { this._queueSettingsSetup(); resolve(); @@ -97,21 +98,36 @@ class Suggestions { */ get readyPromise() { return this._settingsTaskQueue.emptyPromise; } /** * @returns {object} * Global quick suggest configuration from remote settings: + * * { * best_match: { * min_search_string_length, * blocked_suggestion_ids, * }, + * impression_caps: { + * nonsponsored: { + * lifetime, + * custom: [ + * { interval_s, max_count }, + * ], + * }, + * sponsored: { + * lifetime, + * custom: [ + * { interval_s, max_count }, + * ], + * }, + * }, * } */ get config() { return this._config; } /** * Handle queries from the Urlbar. @@ -344,17 +360,17 @@ class Suggestions { // Task queue for serializing access to remote settings and related data. // Methods in this class should use this when they need to to modify or access // the settings client. It ensures settings accesses are serialized, do not // overlap, and happen only one at a time. It also lets clients, especially // tests, use this class without having to worry about whether a settings sync // or initialization is ongoing; see `readyPromise`. _settingsTaskQueue = new TaskQueue(); - // Configuration data synced from remote settings. + // Configuration data synced from remote settings. See the `config` getter. _config = {}; // Maps from keywords to their corresponding results. Keywords are unique in // the underlying data, so a keyword will only ever map to one result. _resultsByKeyword = new Map(); /** * Queues a task to ensure our remote settings client is initialized or torn @@ -404,31 +420,41 @@ class Suggestions { this._rs .get({ filters: { type: "icon" } }) .then(icons => Promise.all(icons.map(i => this._rs.attachments.download(i))) ), ]); log.debug("Got configuration:", configArray); - this._config = configArray?.[0]?.configuration || {}; + this._setConfig(configArray?.[0]?.configuration || {}); this._resultsByKeyword.clear(); for (let record of data) { let { buffer } = await this._rs.attachments.download(record, { useCache: true, }); let results = JSON.parse(new TextDecoder("utf-8").decode(buffer)); this._addResults(results); } }); } /** + * Sets the quick suggest config and emits a "config-set" event. + * + * @param {object} config + */ + _setConfig(config) { + this._config = config || {}; + this.emit("config-set"); + } + + /** * Adds a list of result objects to the results map. This method is also used * by tests to set up mock suggestions. * * @param {array} results * Array of result objects. */ _addResults(results) { for (let result of results) { @@ -455,9 +481,9 @@ class Suggestions { ).pop(); if (!record) { return null; } return this._rs.attachments.download(record); } } -let UrlbarQuickSuggest = new Suggestions(); +let UrlbarQuickSuggest = new QuickSuggest();
--- a/browser/components/urlbar/docs/firefox-suggest-telemetry.rst +++ b/browser/components/urlbar/docs/firefox-suggest-telemetry.rst @@ -328,16 +328,62 @@ Changelog The event is no longer recorded when the user interacts with the online modal dialog since the ``browser.urlbar.suggest.quicksuggest.nonsponsored`` pref is no longer set when the user opts in or out. [Bug 1740965_] .. _1693126: https://bugzilla.mozilla.org/show_bug.cgi?id=1693126 .. _1735976: https://bugzilla.mozilla.org/show_bug.cgi?id=1735976 .. _1740965: https://bugzilla.mozilla.org/show_bug.cgi?id=1740965 +contextservices.quicksuggest.impression_cap +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This event is recorded when an event related to an impression cap occurs. The +event's objects are the following possible values: + +:hit: + Recorded when an impression cap is hit. +:reset: + Recorded when a cap's counter is reset because its interval period has + elapsed. + +The event's ``extra`` object value contains the following properties: + +:count: + The number of impressions during the cap's interval period. +:endDate: + The timestamp at which the cap's interval period will end (for "hit" events) + or did end (for "reset" events), in number of milliseconds since Unix epoch. + For lifetime caps, this value will be "Infinity". +:eventDate: + The event's timestamp, in number of milliseconds since Unix epoch. For "reset" + events, this may be earlier than the timestamp on the event itself because the + implementation sometimes batches and records these events at a later date. + This ``eventDate`` value should be preferred over the timestamp on the event + itself. +:impressionDate: + The timestamp of the most recent impression, in number of milliseconds since + Unix epoch. +:intervalSeconds: + The number of seconds in the cap's interval period. For lifetime caps, this + value will be "Infinity". +:maxCount: + The maximum number of impressions allowed in the cap's interval period. +:startDate: + The timestamp at which the cap's interval period started, in number of seconds + since Unix epoch. +:type: + The type of cap, one of: "sponsored", "nonsponsored" + +Changelog + Firefox 101.0 + Introduced. [Bug 1761058_] + +.. _1761058: https://bugzilla.mozilla.org/show_bug.cgi?id=1761058 + contextservices.quicksuggest.opt_in_dialog ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ This event is recorded when the user interacts with the online modal dialog. The event's objects are the following: :accept: The user accepted the dialog and opted in. This object was removed in Firefox
--- a/browser/components/urlbar/tests/quicksuggest/QuickSuggestTestUtils.jsm +++ b/browser/components/urlbar/tests/quicksuggest/QuickSuggestTestUtils.jsm @@ -196,17 +196,17 @@ class QSTestUtils { /** * Sets the quick suggest configuration. You should call this again with * `DEFAULT_CONFIG` before your test finishes. See also `withConfig()`. * * @param {object} config */ setConfig(config) { - UrlbarQuickSuggest._config = config; + UrlbarQuickSuggest._setConfig(config); } /** * Sets the quick suggest configuration, calls your callback, and restores the * default configuration. * * @param {object} config * @param {function} callback
--- a/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_configuration.js +++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_configuration.js @@ -20,66 +20,66 @@ const POLICY_PREF = "suggest.quicksugges let gDefaultBranch = Services.prefs.getDefaultBranch("browser.urlbar."); let gUserBranch = Services.prefs.getBranch("browser.urlbar."); add_task(async function init() { await QuickSuggestTestUtils.ensureQuickSuggestInit(); }); -// Makes sure `UrlbarProviderQuickSuggest._updateExperimentState()` is called +// Makes sure `UrlbarProviderQuickSuggest._updateFeatureState()` is called // when the `browser.urlbar.quicksuggest.enabled` pref is changed. -add_task(async function test_updateExperimentState_pref() { +add_task(async function test_updateFeatureState_pref() { Assert.ok( UrlbarPrefs.get("quicksuggest.enabled"), "Sanity check: quicksuggest.enabled is true by default" ); let sandbox = sinon.createSandbox(); - let spy = sandbox.spy(UrlbarProviderQuickSuggest, "_updateExperimentState"); + let spy = sandbox.spy(UrlbarProviderQuickSuggest, "_updateFeatureState"); UrlbarPrefs.set("quicksuggest.enabled", false); await UrlbarQuickSuggest.readyPromise; Assert.equal( spy.callCount, 1, - "_updateExperimentState called once after changing pref" + "_updateFeatureState called once after changing pref" ); UrlbarPrefs.clear("quicksuggest.enabled"); await UrlbarQuickSuggest.readyPromise; Assert.equal( spy.callCount, 2, - "_updateExperimentState called again after clearing pref" + "_updateFeatureState called again after clearing pref" ); sandbox.restore(); }); -// Makes sure `UrlbarProviderQuickSuggest._updateExperimentState()` is called +// Makes sure `UrlbarProviderQuickSuggest._updateFeatureState()` is called // when a Nimbus experiment is installed and uninstalled. -add_task(async function test_updateExperimentState_experiment() { +add_task(async function test_updateFeatureState_experiment() { let sandbox = sinon.createSandbox(); - let spy = sandbox.spy(UrlbarProviderQuickSuggest, "_updateExperimentState"); + let spy = sandbox.spy(UrlbarProviderQuickSuggest, "_updateFeatureState"); await QuickSuggestTestUtils.withExperiment({ callback: () => { Assert.equal( spy.callCount, 1, - "_updateExperimentState called once after installing experiment" + "_updateFeatureState called once after installing experiment" ); }, }); Assert.equal( spy.callCount, 2, - "_updateExperimentState called again after uninstalling experiment" + "_updateFeatureState called again after uninstalling experiment" ); sandbox.restore(); }); add_task(async function test_indexes() { await QuickSuggestTestUtils.withExperiment({ valueOverrides: {
--- a/browser/components/urlbar/tests/quicksuggest/unit/head.js +++ b/browser/components/urlbar/tests/quicksuggest/unit/head.js @@ -1,13 +1,20 @@ /* Any copyright is dedicated to the Public Domain. * http://creativecommons.org/publicdomain/zero/1.0/ */ /* import-globals-from ../../unit/head.js */ +XPCOMUtils.defineLazyModuleGetters(this, { + TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.jsm", + UrlbarProviderQuickSuggest: + "resource:///modules/UrlbarProviderQuickSuggest.jsm", + UrlbarQuickSuggest: "resource:///modules/UrlbarQuickSuggest.jsm", +}); + /** * Tests quick suggest prefs migrations. * * @param {object} testOverrides * An object that modifies how migration is performed. It has the following * properties, and all are optional: * * {number} migrationVersion
--- a/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest.js +++ b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest.js @@ -2,22 +2,16 @@ * 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/. */ // Basic tests for the quick suggest provider using the remote settings source. // See also test_quicksuggest_merino.js. "use strict"; -XPCOMUtils.defineLazyModuleGetters(this, { - UrlbarProviderQuickSuggest: - "resource:///modules/UrlbarProviderQuickSuggest.jsm", - UrlbarQuickSuggest: "resource:///modules/UrlbarQuickSuggest.jsm", -}); - const TELEMETRY_REMOTE_SETTINGS_LATENCY = "FX_URLBAR_QUICK_SUGGEST_REMOTE_SETTINGS_LATENCY_MS"; const SPONSORED_SEARCH_STRING = "frab"; const NONSPONSORED_SEARCH_STRING = "nonspon"; const HTTP_SEARCH_STRING = "http prefix"; const HTTPS_SEARCH_STRING = "https prefix";
--- a/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_bestMatch.js +++ b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_bestMatch.js @@ -1,21 +1,16 @@ /* 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/. */ // Tests best match quick suggest results. "use strict"; -XPCOMUtils.defineLazyModuleGetters(this, { - UrlbarProviderQuickSuggest: - "resource:///modules/UrlbarProviderQuickSuggest.jsm", -}); - const MAX_RESULT_COUNT = UrlbarPrefs.get("maxRichResults"); // This search string length needs to be >= 4 to trigger its suggestion as a // best match instead of a usual quick suggest. const BEST_MATCH_POSITION_SEARCH_STRING = "bestmatchposition"; const BEST_MATCH_POSITION = Math.round(MAX_RESULT_COUNT / 2); const SUGGESTIONS = [
new file mode 100644 --- /dev/null +++ b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_impressionCaps.js @@ -0,0 +1,3336 @@ +/* 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/. */ + +// Tests impression frequency capping for quick suggest results. + +"use strict"; + +const SUGGESTIONS = [ + { + id: 1, + url: "http://example.com/sponsored", + title: "Sponsored suggestion", + keywords: ["sponsored"], + click_url: "http://example.com/click", + impression_url: "http://example.com/impression", + advertiser: "TestAdvertiser", + }, + { + id: 2, + url: "http://example.com/nonsponsored", + title: "Non-sponsored suggestion", + keywords: ["nonsponsored"], + click_url: "http://example.com/click", + impression_url: "http://example.com/impression", + advertiser: "TestAdvertiser", + iab_category: "5 - Education", + }, +]; + +const EXPECTED_SPONSORED_RESULT = { + type: UrlbarUtils.RESULT_TYPE.URL, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + heuristic: false, + payload: { + url: "http://example.com/sponsored", + originalUrl: "http://example.com/sponsored", + displayUrl: "http://example.com/sponsored", + title: "Sponsored suggestion", + qsSuggestion: "sponsored", + icon: null, + isSponsored: true, + sponsoredImpressionUrl: "http://example.com/impression", + sponsoredClickUrl: "http://example.com/click", + sponsoredBlockId: 1, + sponsoredAdvertiser: "TestAdvertiser", + helpUrl: UrlbarProviderQuickSuggest.helpUrl, + helpL10nId: "firefox-suggest-urlbar-learn-more", + source: "remote-settings", + }, +}; + +const EXPECTED_NONSPONSORED_RESULT = { + type: UrlbarUtils.RESULT_TYPE.URL, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + heuristic: false, + payload: { + url: "http://example.com/nonsponsored", + originalUrl: "http://example.com/nonsponsored", + displayUrl: "http://example.com/nonsponsored", + title: "Non-sponsored suggestion", + qsSuggestion: "nonsponsored", + icon: null, + isSponsored: false, + sponsoredImpressionUrl: "http://example.com/impression", + sponsoredClickUrl: "http://example.com/click", + sponsoredBlockId: 2, + sponsoredAdvertiser: "TestAdvertiser", + helpUrl: UrlbarProviderQuickSuggest.helpUrl, + helpL10nId: "firefox-suggest-urlbar-learn-more", + source: "remote-settings", + }, +}; + +let gSandbox; +let gDateNowStub; + +add_task(async function init() { + UrlbarPrefs.set("quicksuggest.enabled", true); + UrlbarPrefs.set("quicksuggest.impressionCaps.sponsoredEnabled", true); + UrlbarPrefs.set("quicksuggest.impressionCaps.nonSponsoredEnabled", true); + UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); + UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); + UrlbarPrefs.set("bestMatch.enabled", false); + + // Disable search suggestions so we don't hit the network. + Services.prefs.setBoolPref("browser.search.suggest.enabled", false); + + await QuickSuggestTestUtils.ensureQuickSuggestInit(SUGGESTIONS); + + // Set up a sinon stub for the `Date.now()` implementation inside of + // UrlbarProviderQuickSuggest. This lets us test searches performed at + // specific times. See `doTimedCallbacks()` for more info. + gSandbox = sinon.createSandbox(); + gDateNowStub = gSandbox.stub( + Cu.getGlobalForObject(UrlbarProviderQuickSuggest).Date, + "now" + ); +}); + +// Tests a single interval. +add_task(async function oneInterval() { + await doTest({ + config: { + impression_caps: { + sponsored: { + custom: [{ interval_s: 3, max_count: 1 }], + }, + }, + }, + callback: async () => { + await doTimedSearches("sponsored", { + 0: { + results: [[EXPECTED_SPONSORED_RESULT], []], + telemetry: { + events: [ + { + object: "hit", + extra: { + eventDate: "0", + intervalSeconds: "3", + maxCount: "1", + startDate: "0", + endDate: "3000", + impressionDate: "0", + count: "1", + type: "sponsored", + }, + }, + ], + }, + }, + 1: { + results: [[]], + }, + 2: { + results: [[]], + }, + 3: { + results: [[EXPECTED_SPONSORED_RESULT], []], + telemetry: { + events: [ + { + object: "reset", + extra: { + eventDate: "3000", + intervalSeconds: "3", + maxCount: "1", + startDate: "0", + endDate: "3000", + impressionDate: "0", + count: "1", + type: "sponsored", + }, + }, + { + object: "hit", + extra: { + eventDate: "3000", + intervalSeconds: "3", + maxCount: "1", + startDate: "3000", + endDate: "6000", + impressionDate: "3000", + count: "1", + type: "sponsored", + }, + }, + ], + }, + }, + 4: { + results: [[]], + }, + 5: { + results: [[]], + }, + }); + }, + }); +}); + +// Tests multiple intervals. +add_task(async function multipleIntervals() { + await doTest({ + config: { + impression_caps: { + sponsored: { + custom: [ + { interval_s: 1, max_count: 1 }, + { interval_s: 5, max_count: 3 }, + { interval_s: 10, max_count: 5 }, + ], + }, + }, + }, + callback: async () => { + await doTimedSearches("sponsored", { + // 0s: 1 new impression; 1 impression total + 0: { + results: [[EXPECTED_SPONSORED_RESULT], []], + telemetry: { + events: [ + // hit: interval_s: 1, max_count: 1 + { + object: "hit", + extra: { + eventDate: "0", + intervalSeconds: "1", + maxCount: "1", + startDate: "0", + endDate: "1000", + impressionDate: "0", + count: "1", + type: "sponsored", + }, + }, + ], + }, + }, + // 1s: 1 new impression; 2 impressions total + 1: { + results: [[EXPECTED_SPONSORED_RESULT], []], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "1000", + intervalSeconds: "1", + maxCount: "1", + startDate: "0", + endDate: "1000", + impressionDate: "0", + count: "1", + type: "sponsored", + }, + }, + // hit: interval_s: 1, max_count: 1 + { + object: "hit", + extra: { + eventDate: "1000", + intervalSeconds: "1", + maxCount: "1", + startDate: "1000", + endDate: "2000", + impressionDate: "1000", + count: "1", + type: "sponsored", + }, + }, + ], + }, + }, + // 2s: 1 new impression; 3 impressions total + 2: { + results: [[EXPECTED_SPONSORED_RESULT], []], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "2000", + intervalSeconds: "1", + maxCount: "1", + startDate: "1000", + endDate: "2000", + impressionDate: "1000", + count: "1", + type: "sponsored", + }, + }, + // hit: interval_s: 1, max_count: 1 + { + object: "hit", + extra: { + eventDate: "2000", + intervalSeconds: "1", + maxCount: "1", + startDate: "2000", + endDate: "3000", + impressionDate: "2000", + count: "1", + type: "sponsored", + }, + }, + // hit: interval_s: 5, max_count: 3 + { + object: "hit", + extra: { + eventDate: "2000", + intervalSeconds: "5", + maxCount: "3", + startDate: "0", + endDate: "5000", + impressionDate: "2000", + count: "3", + type: "sponsored", + }, + }, + ], + }, + }, + // 3s: no new impressions; 3 impressions total + 3: { + results: [[]], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "3000", + intervalSeconds: "1", + maxCount: "1", + startDate: "2000", + endDate: "3000", + impressionDate: "2000", + count: "1", + type: "sponsored", + }, + }, + ], + }, + }, + // 4s: no new impressions; 3 impressions total + 4: { + results: [[]], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "4000", + intervalSeconds: "1", + maxCount: "1", + startDate: "3000", + endDate: "4000", + impressionDate: "2000", + count: "0", + type: "sponsored", + }, + }, + ], + }, + }, + // 5s: 1 new impression; 4 impressions total + 5: { + results: [[EXPECTED_SPONSORED_RESULT], []], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "5000", + intervalSeconds: "1", + maxCount: "1", + startDate: "4000", + endDate: "5000", + impressionDate: "2000", + count: "0", + type: "sponsored", + }, + }, + // reset: interval_s: 5, max_count: 3 + { + object: "reset", + extra: { + eventDate: "5000", + intervalSeconds: "5", + maxCount: "3", + startDate: "0", + endDate: "5000", + impressionDate: "2000", + count: "3", + type: "sponsored", + }, + }, + // hit: interval_s: 1, max_count: 1 + { + object: "hit", + extra: { + eventDate: "5000", + intervalSeconds: "1", + maxCount: "1", + startDate: "5000", + endDate: "6000", + impressionDate: "5000", + count: "1", + type: "sponsored", + }, + }, + ], + }, + }, + // 6s: 1 new impression; 5 impressions total + 6: { + results: [[EXPECTED_SPONSORED_RESULT], []], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "6000", + intervalSeconds: "1", + maxCount: "1", + startDate: "5000", + endDate: "6000", + impressionDate: "5000", + count: "1", + type: "sponsored", + }, + }, + // hit: interval_s: 1, max_count: 1 + { + object: "hit", + extra: { + eventDate: "6000", + intervalSeconds: "1", + maxCount: "1", + startDate: "6000", + endDate: "7000", + impressionDate: "6000", + count: "1", + type: "sponsored", + }, + }, + // hit: interval_s: 10, max_count: 5 + { + object: "hit", + extra: { + eventDate: "6000", + intervalSeconds: "10", + maxCount: "5", + startDate: "0", + endDate: "10000", + impressionDate: "6000", + count: "5", + type: "sponsored", + }, + }, + ], + }, + }, + // 7s: no new impressions; 5 impressions total + 7: { + results: [[]], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "7000", + intervalSeconds: "1", + maxCount: "1", + startDate: "6000", + endDate: "7000", + impressionDate: "6000", + count: "1", + type: "sponsored", + }, + }, + ], + }, + }, + // 8s: no new impressions; 5 impressions total + 8: { + results: [[]], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "8000", + intervalSeconds: "1", + maxCount: "1", + startDate: "7000", + endDate: "8000", + impressionDate: "6000", + count: "0", + type: "sponsored", + }, + }, + ], + }, + }, + // 9s: no new impressions; 5 impressions total + 9: { + results: [[]], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "9000", + intervalSeconds: "1", + maxCount: "1", + startDate: "8000", + endDate: "9000", + impressionDate: "6000", + count: "0", + type: "sponsored", + }, + }, + ], + }, + }, + // 10s: 1 new impression; 6 impressions total + 10: { + results: [[EXPECTED_SPONSORED_RESULT], []], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "10000", + intervalSeconds: "1", + maxCount: "1", + startDate: "9000", + endDate: "10000", + impressionDate: "6000", + count: "0", + type: "sponsored", + }, + }, + // reset: interval_s: 5, max_count: 3 + { + object: "reset", + extra: { + eventDate: "10000", + intervalSeconds: "5", + maxCount: "3", + startDate: "5000", + endDate: "10000", + impressionDate: "6000", + count: "2", + type: "sponsored", + }, + }, + // reset: interval_s: 10, max_count: 5 + { + object: "reset", + extra: { + eventDate: "10000", + intervalSeconds: "10", + maxCount: "5", + startDate: "0", + endDate: "10000", + impressionDate: "6000", + count: "5", + type: "sponsored", + }, + }, + // hit: interval_s: 1, max_count: 1 + { + object: "hit", + extra: { + eventDate: "10000", + intervalSeconds: "1", + maxCount: "1", + startDate: "10000", + endDate: "11000", + impressionDate: "10000", + count: "1", + type: "sponsored", + }, + }, + ], + }, + }, + // 11s: 1 new impression; 7 impressions total + 11: { + results: [[EXPECTED_SPONSORED_RESULT], []], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "11000", + intervalSeconds: "1", + maxCount: "1", + startDate: "10000", + endDate: "11000", + impressionDate: "10000", + count: "1", + type: "sponsored", + }, + }, + // hit: interval_s: 1, max_count: 1 + { + object: "hit", + extra: { + eventDate: "11000", + intervalSeconds: "1", + maxCount: "1", + startDate: "11000", + endDate: "12000", + impressionDate: "11000", + count: "1", + type: "sponsored", + }, + }, + ], + }, + }, + // 12s: 1 new impression; 8 impressions total + 12: { + results: [[EXPECTED_SPONSORED_RESULT], []], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "12000", + intervalSeconds: "1", + maxCount: "1", + startDate: "11000", + endDate: "12000", + impressionDate: "11000", + count: "1", + type: "sponsored", + }, + }, + // hit: interval_s: 1, max_count: 1 + { + object: "hit", + extra: { + eventDate: "12000", + intervalSeconds: "1", + maxCount: "1", + startDate: "12000", + endDate: "13000", + impressionDate: "12000", + count: "1", + type: "sponsored", + }, + }, + // hit: interval_s: 5, max_count: 3 + { + object: "hit", + extra: { + eventDate: "12000", + intervalSeconds: "5", + maxCount: "3", + startDate: "10000", + endDate: "15000", + impressionDate: "12000", + count: "3", + type: "sponsored", + }, + }, + ], + }, + }, + // 13s: no new impressions; 8 impressions total + 13: { + results: [[]], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "13000", + intervalSeconds: "1", + maxCount: "1", + startDate: "12000", + endDate: "13000", + impressionDate: "12000", + count: "1", + type: "sponsored", + }, + }, + ], + }, + }, + // 14s: no new impressions; 8 impressions total + 14: { + results: [[]], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "14000", + intervalSeconds: "1", + maxCount: "1", + startDate: "13000", + endDate: "14000", + impressionDate: "12000", + count: "0", + type: "sponsored", + }, + }, + ], + }, + }, + // 15s: 1 new impression; 9 impressions total + 15: { + results: [[EXPECTED_SPONSORED_RESULT], []], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "15000", + intervalSeconds: "1", + maxCount: "1", + startDate: "14000", + endDate: "15000", + impressionDate: "12000", + count: "0", + type: "sponsored", + }, + }, + // reset: interval_s: 5, max_count: 3 + { + object: "reset", + extra: { + eventDate: "15000", + intervalSeconds: "5", + maxCount: "3", + startDate: "10000", + endDate: "15000", + impressionDate: "12000", + count: "3", + type: "sponsored", + }, + }, + // hit: interval_s: 1, max_count: 1 + { + object: "hit", + extra: { + eventDate: "15000", + intervalSeconds: "1", + maxCount: "1", + startDate: "15000", + endDate: "16000", + impressionDate: "15000", + count: "1", + type: "sponsored", + }, + }, + ], + }, + }, + // 16s: 1 new impression; 10 impressions total + 16: { + results: [[EXPECTED_SPONSORED_RESULT], []], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "16000", + intervalSeconds: "1", + maxCount: "1", + startDate: "15000", + endDate: "16000", + impressionDate: "15000", + count: "1", + type: "sponsored", + }, + }, + // hit: interval_s: 1, max_count: 1 + { + object: "hit", + extra: { + eventDate: "16000", + intervalSeconds: "1", + maxCount: "1", + startDate: "16000", + endDate: "17000", + impressionDate: "16000", + count: "1", + type: "sponsored", + }, + }, + // hit: interval_s: 10, max_count: 5 + { + object: "hit", + extra: { + eventDate: "16000", + intervalSeconds: "10", + maxCount: "5", + startDate: "10000", + endDate: "20000", + impressionDate: "16000", + count: "5", + type: "sponsored", + }, + }, + ], + }, + }, + // 17s: no new impressions; 10 impressions total + 17: { + results: [[]], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "17000", + intervalSeconds: "1", + maxCount: "1", + startDate: "16000", + endDate: "17000", + impressionDate: "16000", + count: "1", + type: "sponsored", + }, + }, + ], + }, + }, + // 18s: no new impressions; 10 impressions total + 18: { + results: [[]], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "18000", + intervalSeconds: "1", + maxCount: "1", + startDate: "17000", + endDate: "18000", + impressionDate: "16000", + count: "0", + type: "sponsored", + }, + }, + ], + }, + }, + // 19s: no new impressions; 10 impressions total + 19: { + results: [[]], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "19000", + intervalSeconds: "1", + maxCount: "1", + startDate: "18000", + endDate: "19000", + impressionDate: "16000", + count: "0", + type: "sponsored", + }, + }, + ], + }, + }, + // 20s: 1 new impression; 11 impressions total + 20: { + results: [[EXPECTED_SPONSORED_RESULT], []], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "20000", + intervalSeconds: "1", + maxCount: "1", + startDate: "19000", + endDate: "20000", + impressionDate: "16000", + count: "0", + type: "sponsored", + }, + }, + // reset: interval_s: 5, max_count: 3 + { + object: "reset", + extra: { + eventDate: "20000", + intervalSeconds: "5", + maxCount: "3", + startDate: "15000", + endDate: "20000", + impressionDate: "16000", + count: "2", + type: "sponsored", + }, + }, + // reset: interval_s: 10, max_count: 5 + { + object: "reset", + extra: { + eventDate: "20000", + intervalSeconds: "10", + maxCount: "5", + startDate: "10000", + endDate: "20000", + impressionDate: "16000", + count: "5", + type: "sponsored", + }, + }, + // hit: interval_s: 1, max_count: 1 + { + object: "hit", + extra: { + eventDate: "20000", + intervalSeconds: "1", + maxCount: "1", + startDate: "20000", + endDate: "21000", + impressionDate: "20000", + count: "1", + type: "sponsored", + }, + }, + ], + }, + }, + }); + }, + }); +}); + +// Tests a lifetime cap. +add_task(async function lifetime() { + await doTest({ + config: { + impression_caps: { + sponsored: { + lifetime: 3, + }, + }, + }, + callback: async () => { + await doTimedSearches("sponsored", { + 0: { + results: [ + [EXPECTED_SPONSORED_RESULT], + [EXPECTED_SPONSORED_RESULT], + [EXPECTED_SPONSORED_RESULT], + [], + ], + telemetry: { + events: [ + { + object: "hit", + extra: { + eventDate: "0", + intervalSeconds: "Infinity", + maxCount: "3", + startDate: "0", + endDate: "Infinity", + impressionDate: "0", + count: "3", + type: "sponsored", + }, + }, + ], + }, + }, + 1: { + results: [[]], + }, + }); + }, + }); +}); + +// Tests one interval and a lifetime cap together. +add_task(async function intervalAndLifetime() { + await doTest({ + config: { + impression_caps: { + sponsored: { + lifetime: 3, + custom: [{ interval_s: 1, max_count: 1 }], + }, + }, + }, + callback: async () => { + await doTimedSearches("sponsored", { + // 0s: 1 new impression; 1 impression total + 0: { + results: [[EXPECTED_SPONSORED_RESULT], []], + telemetry: { + events: [ + // hit: interval_s: 1, max_count: 1 + { + object: "hit", + extra: { + eventDate: "0", + intervalSeconds: "1", + maxCount: "1", + startDate: "0", + endDate: "1000", + impressionDate: "0", + count: "1", + type: "sponsored", + }, + }, + ], + }, + }, + // 1s: 1 new impression; 2 impressions total + 1: { + results: [[EXPECTED_SPONSORED_RESULT], []], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "1000", + intervalSeconds: "1", + maxCount: "1", + startDate: "0", + endDate: "1000", + impressionDate: "0", + count: "1", + type: "sponsored", + }, + }, + // hit: interval_s: 1, max_count: 1 + { + object: "hit", + extra: { + eventDate: "1000", + intervalSeconds: "1", + maxCount: "1", + startDate: "1000", + endDate: "2000", + impressionDate: "1000", + count: "1", + type: "sponsored", + }, + }, + ], + }, + }, + // 2s: 1 new impression; 3 impressions total + 2: { + results: [[EXPECTED_SPONSORED_RESULT], []], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "2000", + intervalSeconds: "1", + maxCount: "1", + startDate: "1000", + endDate: "2000", + impressionDate: "1000", + count: "1", + type: "sponsored", + }, + }, + // hit: interval_s: 1, max_count: 1 + { + object: "hit", + extra: { + eventDate: "2000", + intervalSeconds: "1", + maxCount: "1", + startDate: "2000", + endDate: "3000", + impressionDate: "2000", + count: "1", + type: "sponsored", + }, + }, + // hit: interval_s: Infinity, max_count: 3 + { + object: "hit", + extra: { + eventDate: "2000", + intervalSeconds: "Infinity", + maxCount: "3", + startDate: "0", + endDate: "Infinity", + impressionDate: "2000", + count: "3", + type: "sponsored", + }, + }, + ], + }, + }, + 3: { + results: [[]], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "3000", + intervalSeconds: "1", + maxCount: "1", + startDate: "2000", + endDate: "3000", + impressionDate: "2000", + count: "1", + type: "sponsored", + }, + }, + ], + }, + }, + }); + }, + }); +}); + +// Tests multiple intervals and a lifetime cap together. +add_task(async function multipleIntervalsAndLifetime() { + await doTest({ + config: { + impression_caps: { + sponsored: { + lifetime: 4, + custom: [ + { interval_s: 1, max_count: 1 }, + { interval_s: 5, max_count: 3 }, + ], + }, + }, + }, + callback: async () => { + await doTimedSearches("sponsored", { + // 0s: 1 new impression; 1 impression total + 0: { + results: [[EXPECTED_SPONSORED_RESULT], []], + telemetry: { + events: [ + // hit: interval_s: 1, max_count: 1 + { + object: "hit", + extra: { + eventDate: "0", + intervalSeconds: "1", + maxCount: "1", + startDate: "0", + endDate: "1000", + impressionDate: "0", + count: "1", + type: "sponsored", + }, + }, + ], + }, + }, + // 1s: 1 new impression; 2 impressions total + 1: { + results: [[EXPECTED_SPONSORED_RESULT], []], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "1000", + intervalSeconds: "1", + maxCount: "1", + startDate: "0", + endDate: "1000", + impressionDate: "0", + count: "1", + type: "sponsored", + }, + }, + // hit: interval_s: 1, max_count: 1 + { + object: "hit", + extra: { + eventDate: "1000", + intervalSeconds: "1", + maxCount: "1", + startDate: "1000", + endDate: "2000", + impressionDate: "1000", + count: "1", + type: "sponsored", + }, + }, + ], + }, + }, + // 2s: 1 new impression; 3 impressions total + 2: { + results: [[EXPECTED_SPONSORED_RESULT], []], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "2000", + intervalSeconds: "1", + maxCount: "1", + startDate: "1000", + endDate: "2000", + impressionDate: "1000", + count: "1", + type: "sponsored", + }, + }, + // hit: interval_s: 1, max_count: 1 + { + object: "hit", + extra: { + eventDate: "2000", + intervalSeconds: "1", + maxCount: "1", + startDate: "2000", + endDate: "3000", + impressionDate: "2000", + count: "1", + type: "sponsored", + }, + }, + // hit: interval_s: 5, max_count: 3 + { + object: "hit", + extra: { + eventDate: "2000", + intervalSeconds: "5", + maxCount: "3", + startDate: "0", + endDate: "5000", + impressionDate: "2000", + count: "3", + type: "sponsored", + }, + }, + ], + }, + }, + // 3s: no new impressions; 3 impressions total + 3: { + results: [[]], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "3000", + intervalSeconds: "1", + maxCount: "1", + startDate: "2000", + endDate: "3000", + impressionDate: "2000", + count: "1", + type: "sponsored", + }, + }, + ], + }, + }, + // 4s: no new impressions; 3 impressions total + 4: { + results: [[]], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "4000", + intervalSeconds: "1", + maxCount: "1", + startDate: "3000", + endDate: "4000", + impressionDate: "2000", + count: "0", + type: "sponsored", + }, + }, + ], + }, + }, + // 5s: 1 new impression; 4 impressions total + 5: { + results: [[EXPECTED_SPONSORED_RESULT], []], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "5000", + intervalSeconds: "1", + maxCount: "1", + startDate: "4000", + endDate: "5000", + impressionDate: "2000", + count: "0", + type: "sponsored", + }, + }, + // reset: interval_s: 5, max_count: 3 + { + object: "reset", + extra: { + eventDate: "5000", + intervalSeconds: "5", + maxCount: "3", + startDate: "0", + endDate: "5000", + impressionDate: "2000", + count: "3", + type: "sponsored", + }, + }, + // hit: interval_s: 1, max_count: 1 + { + object: "hit", + extra: { + eventDate: "5000", + intervalSeconds: "1", + maxCount: "1", + startDate: "5000", + endDate: "6000", + impressionDate: "5000", + count: "1", + type: "sponsored", + }, + }, + // hit: interval_s: Infinity, max_count: 4 + { + object: "hit", + extra: { + eventDate: "5000", + intervalSeconds: "Infinity", + maxCount: "4", + startDate: "0", + endDate: "Infinity", + impressionDate: "5000", + count: "4", + type: "sponsored", + }, + }, + ], + }, + }, + // 6s: no new impressions; 4 impressions total + 6: { + results: [[]], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "6000", + intervalSeconds: "1", + maxCount: "1", + startDate: "5000", + endDate: "6000", + impressionDate: "5000", + count: "1", + type: "sponsored", + }, + }, + ], + }, + }, + // 7s: no new impressions; 4 impressions total + 7: { + results: [[]], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "7000", + intervalSeconds: "1", + maxCount: "1", + startDate: "6000", + endDate: "7000", + impressionDate: "5000", + count: "0", + type: "sponsored", + }, + }, + ], + }, + }, + }); + }, + }); +}); + +// Smoke test for non-sponsored caps. Most tasks use sponsored results and caps, +// but sponsored and non-sponsored should behave the same since they use the +// same code paths. +add_task(async function nonsponsored() { + await doTest({ + config: { + impression_caps: { + nonsponsored: { + lifetime: 4, + custom: [ + { interval_s: 1, max_count: 1 }, + { interval_s: 5, max_count: 3 }, + ], + }, + }, + }, + callback: async () => { + await doTimedSearches("nonsponsored", { + // 0s: 1 new impression; 1 impression total + 0: { + results: [[EXPECTED_NONSPONSORED_RESULT], []], + telemetry: { + events: [ + // hit: interval_s: 1, max_count: 1 + { + object: "hit", + extra: { + eventDate: "0", + intervalSeconds: "1", + maxCount: "1", + startDate: "0", + endDate: "1000", + impressionDate: "0", + count: "1", + type: "nonsponsored", + }, + }, + ], + }, + }, + // 1s: 1 new impression; 2 impressions total + 1: { + results: [[EXPECTED_NONSPONSORED_RESULT], []], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "1000", + intervalSeconds: "1", + maxCount: "1", + startDate: "0", + endDate: "1000", + impressionDate: "0", + count: "1", + type: "nonsponsored", + }, + }, + // hit: interval_s: 1, max_count: 1 + { + object: "hit", + extra: { + eventDate: "1000", + intervalSeconds: "1", + maxCount: "1", + startDate: "1000", + endDate: "2000", + impressionDate: "1000", + count: "1", + type: "nonsponsored", + }, + }, + ], + }, + }, + // 2s: 1 new impression; 3 impressions total + 2: { + results: [[EXPECTED_NONSPONSORED_RESULT], []], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "2000", + intervalSeconds: "1", + maxCount: "1", + startDate: "1000", + endDate: "2000", + impressionDate: "1000", + count: "1", + type: "nonsponsored", + }, + }, + // hit: interval_s: 1, max_count: 1 + { + object: "hit", + extra: { + eventDate: "2000", + intervalSeconds: "1", + maxCount: "1", + startDate: "2000", + endDate: "3000", + impressionDate: "2000", + count: "1", + type: "nonsponsored", + }, + }, + // hit: interval_s: 5, max_count: 3 + { + object: "hit", + extra: { + eventDate: "2000", + intervalSeconds: "5", + maxCount: "3", + startDate: "0", + endDate: "5000", + impressionDate: "2000", + count: "3", + type: "nonsponsored", + }, + }, + ], + }, + }, + // 3s: no new impressions; 3 impressions total + 3: { + results: [[]], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "3000", + intervalSeconds: "1", + maxCount: "1", + startDate: "2000", + endDate: "3000", + impressionDate: "2000", + count: "1", + type: "nonsponsored", + }, + }, + ], + }, + }, + // 4s: no new impressions; 3 impressions total + 4: { + results: [[]], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "4000", + intervalSeconds: "1", + maxCount: "1", + startDate: "3000", + endDate: "4000", + impressionDate: "2000", + count: "0", + type: "nonsponsored", + }, + }, + ], + }, + }, + // 5s: 1 new impression; 4 impressions total + 5: { + results: [[EXPECTED_NONSPONSORED_RESULT], []], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "5000", + intervalSeconds: "1", + maxCount: "1", + startDate: "4000", + endDate: "5000", + impressionDate: "2000", + count: "0", + type: "nonsponsored", + }, + }, + // reset: interval_s: 5, max_count: 3 + { + object: "reset", + extra: { + eventDate: "5000", + intervalSeconds: "5", + maxCount: "3", + startDate: "0", + endDate: "5000", + impressionDate: "2000", + count: "3", + type: "nonsponsored", + }, + }, + // hit: interval_s: 1, max_count: 1 + { + object: "hit", + extra: { + eventDate: "5000", + intervalSeconds: "1", + maxCount: "1", + startDate: "5000", + endDate: "6000", + impressionDate: "5000", + count: "1", + type: "nonsponsored", + }, + }, + // hit: interval_s: Infinity, max_count: 4 + { + object: "hit", + extra: { + eventDate: "5000", + intervalSeconds: "Infinity", + maxCount: "4", + startDate: "0", + endDate: "Infinity", + impressionDate: "5000", + count: "4", + type: "nonsponsored", + }, + }, + ], + }, + }, + // 6s: no new impressions; 4 impressions total + 6: { + results: [[]], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "6000", + intervalSeconds: "1", + maxCount: "1", + startDate: "5000", + endDate: "6000", + impressionDate: "5000", + count: "1", + type: "nonsponsored", + }, + }, + ], + }, + }, + // 7s: no new impressions; 4 impressions total + 7: { + results: [[]], + telemetry: { + events: [ + // reset: interval_s: 1, max_count: 1 + { + object: "reset", + extra: { + eventDate: "7000", + intervalSeconds: "1", + maxCount: "1", + startDate: "6000", + endDate: "7000", + impressionDate: "5000", + count: "0", + type: "nonsponsored", + }, + }, + ], + }, + }, + }); + }, + }); +}); + +// Smoke test for sponsored and non-sponsored caps together. Most tasks use only +// sponsored results and caps, but sponsored and non-sponsored should behave the +// same since they use the same code paths. +add_task(async function sponsoredAndNonsponsored() { + await doTest({ + config: { + impression_caps: { + sponsored: { + lifetime: 2, + }, + nonsponsored: { + lifetime: 3, + }, + }, + }, + callback: async () => { + // 1st searches + await checkSearch({ + name: "sponsored 1", + searchString: "sponsored", + expectedResults: [EXPECTED_SPONSORED_RESULT], + }); + await checkSearch({ + name: "nonsponsored 1", + searchString: "nonsponsored", + expectedResults: [EXPECTED_NONSPONSORED_RESULT], + }); + await checkTelemetryEvents([]); + + // 2nd searches + await checkSearch({ + name: "sponsored 2", + searchString: "sponsored", + expectedResults: [EXPECTED_SPONSORED_RESULT], + }); + await checkSearch({ + name: "nonsponsored 2", + searchString: "nonsponsored", + expectedResults: [EXPECTED_NONSPONSORED_RESULT], + }); + await checkTelemetryEvents([ + { + object: "hit", + extra: { + eventDate: "0", + intervalSeconds: "Infinity", + maxCount: "2", + startDate: "0", + endDate: "Infinity", + impressionDate: "0", + count: "2", + type: "sponsored", + }, + }, + ]); + + // 3rd searches + await checkSearch({ + name: "sponsored 3", + searchString: "sponsored", + expectedResults: [], + }); + await checkSearch({ + name: "nonsponsored 3", + searchString: "nonsponsored", + expectedResults: [EXPECTED_NONSPONSORED_RESULT], + }); + await checkTelemetryEvents([ + { + object: "hit", + extra: { + eventDate: "0", + intervalSeconds: "Infinity", + maxCount: "3", + startDate: "0", + endDate: "Infinity", + impressionDate: "0", + count: "3", + type: "nonsponsored", + }, + }, + ]); + + // 4th searches + await checkSearch({ + name: "sponsored 4", + searchString: "sponsored", + expectedResults: [], + }); + await checkSearch({ + name: "nonsponsored 4", + searchString: "nonsponsored", + expectedResults: [], + }); + await checkTelemetryEvents([]); + }, + }); +}); + +// Tests with an empty config to make sure results are not capped. +add_task(async function emptyConfig() { + await doTest({ + config: {}, + callback: async () => { + for (let i = 0; i < 2; i++) { + await checkSearch({ + name: "sponsored " + i, + searchString: "sponsored", + expectedResults: [EXPECTED_SPONSORED_RESULT], + }); + await checkSearch({ + name: "nonsponsored " + i, + searchString: "nonsponsored", + expectedResults: [EXPECTED_NONSPONSORED_RESULT], + }); + } + await checkTelemetryEvents([]); + }, + }); +}); + +// Tests with sponsored caps disabled. Non-sponsored should still be capped. +add_task(async function sponsoredCapsDisabled() { + UrlbarPrefs.set("quicksuggest.impressionCaps.sponsoredEnabled", false); + await doTest({ + config: { + impression_caps: { + sponsored: { + lifetime: 0, + }, + nonsponsored: { + lifetime: 3, + }, + }, + }, + callback: async () => { + for (let i = 0; i < 3; i++) { + await checkSearch({ + name: "sponsored " + i, + searchString: "sponsored", + expectedResults: [EXPECTED_SPONSORED_RESULT], + }); + await checkSearch({ + name: "nonsponsored " + i, + searchString: "nonsponsored", + expectedResults: [EXPECTED_NONSPONSORED_RESULT], + }); + } + await checkTelemetryEvents([ + { + object: "hit", + extra: { + eventDate: "0", + intervalSeconds: "Infinity", + maxCount: "3", + startDate: "0", + endDate: "Infinity", + impressionDate: "0", + count: "3", + type: "nonsponsored", + }, + }, + ]); + + await checkSearch({ + name: "sponsored additional", + searchString: "sponsored", + expectedResults: [EXPECTED_SPONSORED_RESULT], + }); + await checkSearch({ + name: "nonsponsored additional", + searchString: "nonsponsored", + expectedResults: [], + }); + await checkTelemetryEvents([]); + }, + }); + UrlbarPrefs.set("quicksuggest.impressionCaps.sponsoredEnabled", true); +}); + +// Tests with non-sponsored caps disabled. Sponsored should still be capped. +add_task(async function nonsponsoredCapsDisabled() { + UrlbarPrefs.set("quicksuggest.impressionCaps.nonSponsoredEnabled", false); + await doTest({ + config: { + impression_caps: { + sponsored: { + lifetime: 3, + }, + nonsponsored: { + lifetime: 0, + }, + }, + }, + callback: async () => { + for (let i = 0; i < 3; i++) { + await checkSearch({ + name: "sponsored " + i, + searchString: "sponsored", + expectedResults: [EXPECTED_SPONSORED_RESULT], + }); + await checkSearch({ + name: "nonsponsored " + i, + searchString: "nonsponsored", + expectedResults: [EXPECTED_NONSPONSORED_RESULT], + }); + } + await checkTelemetryEvents([ + { + object: "hit", + extra: { + eventDate: "0", + intervalSeconds: "Infinity", + maxCount: "3", + startDate: "0", + endDate: "Infinity", + impressionDate: "0", + count: "3", + type: "sponsored", + }, + }, + ]); + + await checkSearch({ + name: "sponsored additional", + searchString: "sponsored", + expectedResults: [], + }); + await checkSearch({ + name: "nonsponsored additional", + searchString: "nonsponsored", + expectedResults: [EXPECTED_NONSPONSORED_RESULT], + }); + await checkTelemetryEvents([]); + }, + }); + UrlbarPrefs.set("quicksuggest.impressionCaps.nonSponsoredEnabled", true); +}); + +// Tests a config change: 1 interval -> same interval with lower cap, with the +// old cap already reached +add_task(async function configChange_sameIntervalLowerCap_1() { + await doTest({ + config: { + impression_caps: { + sponsored: { + custom: [{ interval_s: 3, max_count: 3 }], + }, + }, + }, + callback: async () => { + await doTimedCallbacks({ + 0: async () => { + for (let i = 0; i < 3; i++) { + await checkSearch({ + name: "0s " + i, + searchString: "sponsored", + expectedResults: [EXPECTED_SPONSORED_RESULT], + }); + } + await checkSearch({ + name: "0s additional", + searchString: "sponsored", + expectedResults: [], + }); + await checkTelemetryEvents([ + { + object: "hit", + extra: { + eventDate: "0", + intervalSeconds: "3", + maxCount: "3", + startDate: "0", + endDate: "3000", + impressionDate: "0", + count: "3", + type: "sponsored", + }, + }, + ]); + QuickSuggestTestUtils.setConfig({ + impression_caps: { + sponsored: { + custom: [{ interval_s: 3, max_count: 1 }], + }, + }, + }); + }, + 1: async () => { + await checkSearch({ + name: "1s", + searchString: "sponsored", + expectedResults: [], + }); + await checkTelemetryEvents([]); + }, + 3: async () => { + await checkSearch({ + name: "3s 0", + searchString: "sponsored", + expectedResults: [EXPECTED_SPONSORED_RESULT], + }); + await checkSearch({ + name: "3s additional", + searchString: "sponsored", + expectedResults: [], + }); + await checkTelemetryEvents([ + { + object: "reset", + extra: { + eventDate: "3000", + intervalSeconds: "3", + maxCount: "1", + startDate: "0", + endDate: "3000", + impressionDate: "0", + count: "3", + type: "sponsored", + }, + }, + { + object: "hit", + extra: { + eventDate: "3000", + intervalSeconds: "3", + maxCount: "1", + startDate: "3000", + endDate: "6000", + impressionDate: "3000", + count: "1", + type: "sponsored", + }, + }, + ]); + }, + }); + }, + }); +}); + +// Tests a config change: 1 interval -> same interval with lower cap, with the +// old cap not reached +add_task(async function configChange_sameIntervalLowerCap_2() { + await doTest({ + config: { + impression_caps: { + sponsored: { + custom: [{ interval_s: 3, max_count: 3 }], + }, + }, + }, + callback: async () => { + await doTimedCallbacks({ + 0: async () => { + for (let i = 0; i < 2; i++) { + await checkSearch({ + name: "0s " + i, + searchString: "sponsored", + expectedResults: [EXPECTED_SPONSORED_RESULT], + }); + } + await checkTelemetryEvents([]); + QuickSuggestTestUtils.setConfig({ + impression_caps: { + sponsored: { + custom: [{ interval_s: 3, max_count: 1 }], + }, + }, + }); + }, + 1: async () => { + await checkSearch({ + name: "1s", + searchString: "sponsored", + expectedResults: [], + }); + }, + 3: async () => { + await checkSearch({ + name: "3s 0", + searchString: "sponsored", + expectedResults: [EXPECTED_SPONSORED_RESULT], + }); + await checkSearch({ + name: "3s additional", + searchString: "sponsored", + expectedResults: [], + }); + await checkTelemetryEvents([ + { + object: "reset", + extra: { + eventDate: "3000", + intervalSeconds: "3", + maxCount: "1", + startDate: "0", + endDate: "3000", + impressionDate: "0", + count: "2", + type: "sponsored", + }, + }, + { + object: "hit", + extra: { + eventDate: "3000", + intervalSeconds: "3", + maxCount: "1", + startDate: "3000", + endDate: "6000", + impressionDate: "3000", + count: "1", + type: "sponsored", + }, + }, + ]); + }, + }); + }, + }); +}); + +// Tests a config change: 1 interval -> same interval with higher cap +add_task(async function configChange_sameIntervalHigherCap() { + await doTest({ + config: { + impression_caps: { + sponsored: { + custom: [{ interval_s: 3, max_count: 3 }], + }, + }, + }, + callback: async () => { + await doTimedCallbacks({ + 0: async () => { + for (let i = 0; i < 3; i++) { + await checkSearch({ + name: "0s " + i, + searchString: "sponsored", + expectedResults: [EXPECTED_SPONSORED_RESULT], + }); + } + await checkSearch({ + name: "0s additional", + searchString: "sponsored", + expectedResults: [], + }); + await checkTelemetryEvents([ + { + object: "hit", + extra: { + eventDate: "0", + intervalSeconds: "3", + maxCount: "3", + startDate: "0", + endDate: "3000", + impressionDate: "0", + count: "3", + type: "sponsored", + }, + }, + ]); + QuickSuggestTestUtils.setConfig({ + impression_caps: { + sponsored: { + custom: [{ interval_s: 3, max_count: 5 }], + }, + }, + }); + }, + 1: async () => { + for (let i = 0; i < 2; i++) { + await checkSearch({ + name: "1s " + i, + searchString: "sponsored", + expectedResults: [EXPECTED_SPONSORED_RESULT], + }); + } + await checkSearch({ + name: "1s additional", + searchString: "sponsored", + expectedResults: [], + }); + await checkTelemetryEvents([ + { + object: "hit", + extra: { + eventDate: "1000", + intervalSeconds: "3", + maxCount: "5", + startDate: "0", + endDate: "3000", + impressionDate: "1000", + count: "5", + type: "sponsored", + }, + }, + ]); + }, + 3: async () => { + for (let i = 0; i < 5; i++) { + await checkSearch({ + name: "3s " + i, + searchString: "sponsored", + expectedResults: [EXPECTED_SPONSORED_RESULT], + }); + } + await checkSearch({ + name: "3s additional", + searchString: "sponsored", + expectedResults: [], + }); + await checkTelemetryEvents([ + { + object: "reset", + extra: { + eventDate: "3000", + intervalSeconds: "3", + maxCount: "5", + startDate: "0", + endDate: "3000", + impressionDate: "1000", + count: "5", + type: "sponsored", + }, + }, + { + object: "hit", + extra: { + eventDate: "3000", + intervalSeconds: "3", + maxCount: "5", + startDate: "3000", + endDate: "6000", + impressionDate: "3000", + count: "5", + type: "sponsored", + }, + }, + ]); + }, + }); + }, + }); +}); + +// Tests a config change: 1 interval -> 2 new intervals with higher timeouts. +// Impression counts for the old interval should contribute to the new +// intervals. +add_task(async function configChange_1IntervalTo2NewIntervalsHigher() { + await doTest({ + config: { + impression_caps: { + sponsored: { + custom: [{ interval_s: 3, max_count: 3 }], + }, + }, + }, + callback: async () => { + await doTimedCallbacks({ + 0: async () => { + for (let i = 0; i < 3; i++) { + await checkSearch({ + name: "0s " + i, + searchString: "sponsored", + expectedResults: [EXPECTED_SPONSORED_RESULT], + }); + } + await checkTelemetryEvents([ + { + object: "hit", + extra: { + eventDate: "0", + intervalSeconds: "3", + maxCount: "3", + startDate: "0", + endDate: "3000", + impressionDate: "0", + count: "3", + type: "sponsored", + }, + }, + ]); + QuickSuggestTestUtils.setConfig({ + impression_caps: { + sponsored: { + custom: [ + { interval_s: 5, max_count: 3 }, + { interval_s: 10, max_count: 5 }, + ], + }, + }, + }); + }, + 3: async () => { + await checkSearch({ + name: "3s", + searchString: "sponsored", + expectedResults: [], + }); + await checkTelemetryEvents([]); + }, + 4: async () => { + await checkSearch({ + name: "4s", + searchString: "sponsored", + expectedResults: [], + }); + await checkTelemetryEvents([]); + }, + 5: async () => { + for (let i = 0; i < 2; i++) { + await checkSearch({ + name: "5s " + i, + searchString: "sponsored", + expectedResults: [EXPECTED_SPONSORED_RESULT], + }); + } + await checkSearch({ + name: "5s additional", + searchString: "sponsored", + expectedResults: [], + }); + await checkTelemetryEvents([ + { + object: "reset", + extra: { + eventDate: "5000", + intervalSeconds: "5", + maxCount: "3", + startDate: "0", + endDate: "5000", + impressionDate: "0", + count: "3", + type: "sponsored", + }, + }, + { + object: "hit", + extra: { + eventDate: "5000", + intervalSeconds: "10", + maxCount: "5", + startDate: "0", + endDate: "10000", + impressionDate: "5000", + count: "5", + type: "sponsored", + }, + }, + ]); + }, + }); + }, + }); +}); + +// Tests a config change: 2 intervals -> 1 new interval with higher timeout. +// Impression counts for the old intervals should contribute to the new +// interval. +add_task(async function configChange_2IntervalsTo1NewIntervalHigher() { + await doTest({ + config: { + impression_caps: { + sponsored: { + custom: [ + { interval_s: 2, max_count: 2 }, + { interval_s: 4, max_count: 4 }, + ], + }, + }, + }, + callback: async () => { + await doTimedCallbacks({ + 0: async () => { + for (let i = 0; i < 2; i++) { + await checkSearch({ + name: "0s " + i, + searchString: "sponsored", + expectedResults: [EXPECTED_SPONSORED_RESULT], + }); + } + await checkTelemetryEvents([ + { + object: "hit", + extra: { + eventDate: "0", + intervalSeconds: "2", + maxCount: "2", + startDate: "0", + endDate: "2000", + impressionDate: "0", + count: "2", + type: "sponsored", + }, + }, + ]); + }, + 2: async () => { + for (let i = 0; i < 2; i++) { + await checkSearch({ + name: "2s " + i, + searchString: "sponsored", + expectedResults: [EXPECTED_SPONSORED_RESULT], + }); + } + await checkTelemetryEvents([ + { + object: "reset", + extra: { + eventDate: "2000", + intervalSeconds: "2", + maxCount: "2", + startDate: "0", + endDate: "2000", + impressionDate: "0", + count: "2", + type: "sponsored", + }, + }, + { + object: "hit", + extra: { + eventDate: "2000", + intervalSeconds: "2", + maxCount: "2", + startDate: "2000", + endDate: "4000", + impressionDate: "2000", + count: "2", + type: "sponsored", + }, + }, + { + object: "hit", + extra: { + eventDate: "2000", + intervalSeconds: "4", + maxCount: "4", + startDate: "0", + endDate: "4000", + impressionDate: "2000", + count: "4", + type: "sponsored", + }, + }, + ]); + QuickSuggestTestUtils.setConfig({ + impression_caps: { + sponsored: { + custom: [{ interval_s: 6, max_count: 5 }], + }, + }, + }); + }, + 4: async () => { + await checkSearch({ + name: "4s 0", + searchString: "sponsored", + expectedResults: [EXPECTED_SPONSORED_RESULT], + }); + await checkSearch({ + name: "4s 1", + searchString: "sponsored", + expectedResults: [], + }); + await checkTelemetryEvents([ + { + object: "hit", + extra: { + eventDate: "4000", + intervalSeconds: "6", + maxCount: "5", + startDate: "0", + endDate: "6000", + impressionDate: "4000", + count: "5", + type: "sponsored", + }, + }, + ]); + }, + 5: async () => { + await checkSearch({ + name: "5s", + searchString: "sponsored", + expectedResults: [], + }); + await checkTelemetryEvents([]); + }, + 6: async () => { + for (let i = 0; i < 5; i++) { + await checkSearch({ + name: "6s " + i, + searchString: "sponsored", + expectedResults: [EXPECTED_SPONSORED_RESULT], + }); + } + await checkSearch({ + name: "6s additional", + searchString: "sponsored", + expectedResults: [], + }); + await checkTelemetryEvents([ + { + object: "reset", + extra: { + eventDate: "6000", + intervalSeconds: "6", + maxCount: "5", + startDate: "0", + endDate: "6000", + impressionDate: "4000", + count: "5", + type: "sponsored", + }, + }, + { + object: "hit", + extra: { + eventDate: "6000", + intervalSeconds: "6", + maxCount: "5", + startDate: "6000", + endDate: "12000", + impressionDate: "6000", + count: "5", + type: "sponsored", + }, + }, + ]); + }, + }); + }, + }); +}); + +// Tests a config change: 1 interval -> 1 new interval with lower timeout. +// Impression counts for the old interval should not contribute to the new +// interval since the new interval has a lower timeout. +add_task(async function configChange_1IntervalTo1NewIntervalLower() { + await doTest({ + config: { + impression_caps: { + sponsored: { + custom: [{ interval_s: 5, max_count: 3 }], + }, + }, + }, + callback: async () => { + await doTimedCallbacks({ + 0: async () => { + for (let i = 0; i < 3; i++) { + await checkSearch({ + name: "0s " + i, + searchString: "sponsored", + expectedResults: [EXPECTED_SPONSORED_RESULT], + }); + } + await checkTelemetryEvents([ + { + object: "hit", + extra: { + eventDate: "0", + intervalSeconds: "5", + maxCount: "3", + startDate: "0", + endDate: "5000", + impressionDate: "0", + count: "3", + type: "sponsored", + }, + }, + ]); + QuickSuggestTestUtils.setConfig({ + impression_caps: { + sponsored: { + custom: [{ interval_s: 3, max_count: 3 }], + }, + }, + }); + }, + 1: async () => { + for (let i = 0; i < 3; i++) { + await checkSearch({ + name: "3s " + i, + searchString: "sponsored", + expectedResults: [EXPECTED_SPONSORED_RESULT], + }); + } + await checkSearch({ + name: "3s additional", + searchString: "sponsored", + expectedResults: [], + }); + await checkTelemetryEvents([ + { + object: "hit", + extra: { + eventDate: "1000", + intervalSeconds: "3", + maxCount: "3", + startDate: "0", + endDate: "3000", + impressionDate: "1000", + count: "3", + type: "sponsored", + }, + }, + ]); + }, + }); + }, + }); +}); + +// Tests a config change: 1 interval -> lifetime. +// Impression counts for the old interval should contribute to the new lifetime +// cap. +add_task(async function configChange_1IntervalToLifetime() { + await doTest({ + config: { + impression_caps: { + sponsored: { + custom: [{ interval_s: 3, max_count: 3 }], + }, + }, + }, + callback: async () => { + await doTimedCallbacks({ + 0: async () => { + for (let i = 0; i < 3; i++) { + await checkSearch({ + name: "0s " + i, + searchString: "sponsored", + expectedResults: [EXPECTED_SPONSORED_RESULT], + }); + } + await checkTelemetryEvents([ + { + object: "hit", + extra: { + eventDate: "0", + intervalSeconds: "3", + maxCount: "3", + startDate: "0", + endDate: "3000", + impressionDate: "0", + count: "3", + type: "sponsored", + }, + }, + ]); + QuickSuggestTestUtils.setConfig({ + impression_caps: { + sponsored: { + lifetime: 3, + }, + }, + }); + }, + 3: async () => { + await checkSearch({ + name: "3s", + searchString: "sponsored", + expectedResults: [], + }); + await checkTelemetryEvents([]); + }, + }); + }, + }); +}); + +// Tests a config change: lifetime cap -> higher lifetime cap +add_task(async function configChange_lifetimeCapHigher() { + await doTest({ + config: { + impression_caps: { + sponsored: { + lifetime: 3, + }, + }, + }, + callback: async () => { + await doTimedCallbacks({ + 0: async () => { + for (let i = 0; i < 3; i++) { + await checkSearch({ + name: "0s " + i, + searchString: "sponsored", + expectedResults: [EXPECTED_SPONSORED_RESULT], + }); + } + await checkSearch({ + name: "0s additional", + searchString: "sponsored", + expectedResults: [], + }); + await checkTelemetryEvents([ + { + object: "hit", + extra: { + eventDate: "0", + intervalSeconds: "Infinity", + maxCount: "3", + startDate: "0", + endDate: "Infinity", + impressionDate: "0", + count: "3", + type: "sponsored", + }, + }, + ]); + QuickSuggestTestUtils.setConfig({ + impression_caps: { + sponsored: { + lifetime: 5, + }, + }, + }); + }, + 1: async () => { + for (let i = 0; i < 2; i++) { + await checkSearch({ + name: "1s " + i, + searchString: "sponsored", + expectedResults: [EXPECTED_SPONSORED_RESULT], + }); + } + await checkSearch({ + name: "1s additional", + searchString: "sponsored", + expectedResults: [], + }); + await checkTelemetryEvents([ + { + object: "hit", + extra: { + eventDate: "1000", + intervalSeconds: "Infinity", + maxCount: "5", + startDate: "0", + endDate: "Infinity", + impressionDate: "1000", + count: "5", + type: "sponsored", + }, + }, + ]); + }, + }); + }, + }); +}); + +// Tests a config change: lifetime cap -> lower lifetime cap +add_task(async function configChange_lifetimeCapLower() { + await doTest({ + config: { + impression_caps: { + sponsored: { + lifetime: 3, + }, + }, + }, + callback: async () => { + await doTimedCallbacks({ + 0: async () => { + for (let i = 0; i < 3; i++) { + await checkSearch({ + name: "0s " + i, + searchString: "sponsored", + expectedResults: [EXPECTED_SPONSORED_RESULT], + }); + } + await checkSearch({ + name: "0s additional", + searchString: "sponsored", + expectedResults: [], + }); + await checkTelemetryEvents([ + { + object: "hit", + extra: { + eventDate: "0", + intervalSeconds: "Infinity", + maxCount: "3", + startDate: "0", + endDate: "Infinity", + impressionDate: "0", + count: "3", + type: "sponsored", + }, + }, + ]); + QuickSuggestTestUtils.setConfig({ + impression_caps: { + sponsored: { + lifetime: 1, + }, + }, + }); + }, + 1: async () => { + await checkSearch({ + name: "1s", + searchString: "sponsored", + expectedResults: [], + }); + await checkTelemetryEvents([]); + }, + }); + }, + }); +}); + +// Makes sure stats are serialized to and from the pref correctly. +add_task(async function prefSync() { + await doTest({ + config: { + impression_caps: { + sponsored: { + lifetime: 5, + custom: [ + { interval_s: 3, max_count: 2 }, + { interval_s: 5, max_count: 4 }, + ], + }, + }, + }, + callback: async () => { + for (let i = 0; i < 2; i++) { + await checkSearch({ + name: i, + searchString: "sponsored", + expectedResults: [EXPECTED_SPONSORED_RESULT], + }); + } + + let json = UrlbarPrefs.get("quicksuggest.impressionCaps.stats"); + Assert.ok(json, "JSON is non-empty"); + Assert.deepEqual( + JSON.parse(json), + { + sponsored: [ + { + intervalSeconds: 3, + count: 2, + maxCount: 2, + startDateMs: 0, + impressionDateMs: 0, + }, + { + intervalSeconds: 5, + count: 2, + maxCount: 4, + startDateMs: 0, + impressionDateMs: 0, + }, + { + intervalSeconds: null, + count: 2, + maxCount: 5, + startDateMs: 0, + impressionDateMs: 0, + }, + ], + }, + "JSON is correct" + ); + + UrlbarProviderQuickSuggest._impressionStats = null; + UrlbarProviderQuickSuggest._loadImpressionStats(); + Assert.deepEqual( + UrlbarProviderQuickSuggest._impressionStats, + { + sponsored: [ + { + intervalSeconds: 3, + count: 2, + maxCount: 2, + startDateMs: 0, + impressionDateMs: 0, + }, + { + intervalSeconds: 5, + count: 2, + maxCount: 4, + startDateMs: 0, + impressionDateMs: 0, + }, + { + intervalSeconds: Infinity, + count: 2, + maxCount: 5, + startDateMs: 0, + impressionDateMs: 0, + }, + ], + }, + "Impression stats were properly restored from the pref" + ); + }, + }); +}); + +// Tests direct changes to the stats pref. +add_task(async function prefDirectlyChanged() { + await doTest({ + config: { + impression_caps: { + sponsored: { + lifetime: 5, + custom: [{ interval_s: 3, max_count: 3 }], + }, + }, + }, + callback: async () => { + let expectedStats = { + sponsored: [ + { + intervalSeconds: 3, + count: 0, + maxCount: 3, + startDateMs: 0, + impressionDateMs: 0, + }, + { + intervalSeconds: Infinity, + count: 0, + maxCount: 5, + startDateMs: 0, + impressionDateMs: 0, + }, + ], + }; + + UrlbarPrefs.set("quicksuggest.impressionCaps.stats", "bogus"); + Assert.deepEqual( + UrlbarProviderQuickSuggest._impressionStats, + expectedStats, + "Expected stats for 'bogus'" + ); + + UrlbarPrefs.set("quicksuggest.impressionCaps.stats", JSON.stringify({})); + Assert.deepEqual( + UrlbarProviderQuickSuggest._impressionStats, + expectedStats, + "Expected stats for {}" + ); + + UrlbarPrefs.set( + "quicksuggest.impressionCaps.stats", + JSON.stringify({ sponsored: "bogus" }) + ); + Assert.deepEqual( + UrlbarProviderQuickSuggest._impressionStats, + expectedStats, + "Expected stats for { sponsored: 'bogus' }" + ); + + UrlbarPrefs.set( + "quicksuggest.impressionCaps.stats", + JSON.stringify({ + sponsored: [ + { + intervalSeconds: 3, + count: 0, + maxCount: 3, + startDateMs: 0, + impressionDateMs: 0, + }, + { + intervalSeconds: "bogus", + count: 0, + maxCount: 99, + startDateMs: 0, + impressionDateMs: 0, + }, + { + intervalSeconds: Infinity, + count: 0, + maxCount: 5, + startDateMs: 0, + impressionDateMs: 0, + }, + ], + }) + ); + Assert.deepEqual( + UrlbarProviderQuickSuggest._impressionStats, + expectedStats, + "Expected stats with intervalSeconds: 'bogus'" + ); + + UrlbarPrefs.set( + "quicksuggest.impressionCaps.stats", + JSON.stringify({ + sponsored: [ + { + intervalSeconds: 3, + count: 0, + maxCount: 123, + startDateMs: 0, + impressionDateMs: 0, + }, + { + intervalSeconds: Infinity, + count: 0, + maxCount: 456, + startDateMs: 0, + impressionDateMs: 0, + }, + ], + }) + ); + Assert.deepEqual( + UrlbarProviderQuickSuggest._impressionStats, + expectedStats, + "Expected stats with `maxCount` values different from caps" + ); + + let stats = { + sponsored: [ + { + intervalSeconds: 3, + count: 1, + maxCount: 3, + startDateMs: 99, + impressionDateMs: 99, + }, + { + intervalSeconds: Infinity, + count: 7, + maxCount: 5, + startDateMs: 1337, + impressionDateMs: 1337, + }, + ], + }; + UrlbarPrefs.set( + "quicksuggest.impressionCaps.stats", + JSON.stringify(stats) + ); + Assert.deepEqual( + UrlbarProviderQuickSuggest._impressionStats, + stats, + "Expected stats with valid JSON" + ); + }, + }); +}); + +// Tests multiple interval periods where the cap is not hit. Telemetry should be +// recorded for these periods. +add_task(async function intervalsElapsedButCapNotHit() { + await doTest({ + config: { + impression_caps: { + sponsored: { + custom: [{ interval_s: 1, max_count: 3 }], + }, + }, + }, + callback: async () => { + await doTimedCallbacks({ + // 1s + 1: async () => { + await checkSearch({ + name: "1s", + searchString: "sponsored", + expectedResults: [EXPECTED_SPONSORED_RESULT], + }); + }, + // 10s + 10: async () => { + // Impression counter resets are only triggered by `startQuery()` in + // the provider, so we need to do a search to trigger the events. + await checkSearch({ + name: "reset trigger", + searchString: "this shouldn't match any suggestion", + expectedResults: [], + }); + + let expectedEvents = [ + // 1s: reset with count = 0 + { + object: "reset", + extra: { + eventDate: "1000", + intervalSeconds: "1", + maxCount: "3", + startDate: "0", + endDate: "1000", + impressionDate: "0", + count: "0", + type: "sponsored", + }, + }, + // 2s: reset with count = 1 + { + object: "reset", + extra: { + eventDate: "2000", + intervalSeconds: "1", + maxCount: "3", + startDate: "1000", + endDate: "2000", + impressionDate: "1000", + count: "1", + type: "sponsored", + }, + }, + ]; + // 3s to 10s: reset with count = 0 + for (let i = 3; i <= 10; i++) { + expectedEvents.push({ + object: "reset", + extra: { + eventDate: String(1000 * i), + intervalSeconds: "1", + maxCount: "3", + startDate: String(1000 * (i - 1)), + endDate: String(1000 * i), + impressionDate: "1000", + count: "0", + type: "sponsored", + }, + }); + } + await checkTelemetryEvents(expectedEvents); + }, + }); + }, + }); +}); + +/** + * Main test helper. Sets up state, calls your callback, and resets state. + * + * @param {object} config + * The quick suggest config to use during the test. + * @param {function} callback + */ +async function doTest({ config, callback }) { + Services.telemetry.clearEvents(); + + // Make `Date.now()` return 0 to start with. It's necessary to do this before + // calling `withConfig()` because when a new config is set, the provider + // validates its impression stats, whose `startDateMs` values depend on + // `Date.now()`. + gDateNowStub.returns(0); + + await QuickSuggestTestUtils.withConfig({ config, callback }); + UrlbarPrefs.clear("quicksuggest.impressionCaps.stats"); +} + +/** + * Does a series of timed searches and checks their results and telemetry. This + * function relies on `doTimedCallbacks()`, so it may be helpful to look at it + * too. + * + * @param {string} searchString + * @param {object} expectedBySecond + + * An object that maps from seconds to objects that describe the searches to + * perform, their expected results, and the expected telemetry. For a given + * entry `S -> E` in this object, searches are performed S seconds after this + * function is called. `E` is an object that looks like this: + * + * { results, telemetry } + * + * {array} results + * An array of arrays. A search is performed for each sub-array in + * `results`, and the contents of the sub-array are the expected results + * for that search. + * {object} telemetry + * An object like this: { events } + * {array} events + * An array of expected telemetry events after all searches are done. + * Telemetry events are cleared after checking these. If not present, + * then it will be asserted that no events were recorded. + * + * Example: + * + * { + * 0: { + * results: [[R1], []], + * telemetry: { + * events: [ + * someExpectedEvent, + * ], + * }, + * } + * 1: { + * results: [[]], + * }, + * } + * + * 0 seconds after `doTimedSearches()` is called, two searches are + * performed. The first one is expected to return a single result R1, and + * the second search is expected to return no results. After the searches + * are done, one telemetry event is expected to be recorded. + * + * 1 second after `doTimedSearches()` is called, one search is performed. + * It's expected to return no results, and no telemetry is expected to be + * recorded. + */ +async function doTimedSearches(searchString, expectedBySecond) { + await doTimedCallbacks( + Object.entries(expectedBySecond).reduce( + (memo, [second, { results, telemetry }]) => { + memo[second] = async () => { + for (let i = 0; i < results.length; i++) { + let expectedResults = results[i]; + await checkSearch({ + searchString, + expectedResults, + name: `${second}s search ${i + 1} of ${results.length}`, + }); + } + let { events } = telemetry || {}; + await checkTelemetryEvents(events || []); + }; + return memo; + }, + {} + ) + ); +} + +/** + * Takes a series a callbacks and times at which they should be called, and + * calls them accordingly. This function is specifically designed for + * UrlbarProviderQuickSuggest and its impression capping implementation because + * it works by stubbing `Date.now()` within UrlbarProviderQuickSuggest. The + * callbacks are not actually called at the given times but instead `Date.now()` + * is stubbed so that UrlbarProviderQuickSuggest will think they are being + * called at the given times. + * + * A more general implementation of this helper function that isn't tailored to + * UrlbarProviderQuickSuggest is commented out below, and unfortunately it + * doesn't work properly on macOS. + * + * @param {object} callbacksBySecond + * An object that maps from seconds to callback functions. For a given entry + * `S -> F` in this object, the callback F is called S seconds after + * `doTimedCallbacks()` is called. + */ +async function doTimedCallbacks(callbacksBySecond) { + let entries = Object.entries(callbacksBySecond).sort(([t1], [t2]) => t1 - t2); + for (let [timeoutSeconds, callback] of entries) { + gDateNowStub.returns(1000 * timeoutSeconds); + await callback(); + } +} + +/* +// This is the original implementation of `doTimedCallbacks()`, left here for +// reference or in case the macOS problem described below is fixed. Instead of +// stubbing `Date.now()` within UrlbarProviderQuickSuggest, it starts parallel +// timers so that the callbacks are actually called at appropriate times. This +// version of `doTimedCallbacks()` is therefore more generally useful, but it +// has the drawback that your test has to run in real time. e.g., if one of your +// callbacks needs to run 10s from now, the test must actually wait 10s. +// +// Unfortunately macOS seems to have some kind of limit of ~33 total 1-second +// timers during any xpcshell test -- not 33 simultaneous timers but 33 total +// timers. After that, timers fire randomly and with huge timeout periods that +// are often exactly 10s greater than the specified period, as if some 10s +// timeout internal to macOS is being hit. This problem does not seem to happen +// when running the full browser, only during xpcshell tests. In fact the +// problem can be reproduced in an xpcshell test that simply creates an interval +// timer whose period is 1s (e.g., using `setInterval()` from Timer.jsm). After +// ~33 ticks, the timer's period jumps to ~10s. +async function doTimedCallbacks(callbacksBySecond) { + await Promise.all( + Object.entries(callbacksBySecond).map( + ([timeoutSeconds, callback]) => new Promise( + resolve => setTimeout( + () => callback().then(resolve), + 1000 * parseInt(timeoutSeconds) + ) + ) + ) + ); +} +*/ + +/** + * Does a search, triggers an engagement, and checks the results. + * + * @param {string} name + * This value is the name of the search and will be logged in messages to make + * debugging easier. + * @param {string} searchString + * @param {array} expectedResults + */ +async function checkSearch({ name, searchString, expectedResults }) { + info(`Preparing search "${name}" with search string "${searchString}"`); + let context = createContext(searchString, { + providers: [UrlbarProviderQuickSuggest.name], + isPrivate: false, + }); + info(`Doing search: ${name}`); + await check_results({ + context, + matches: expectedResults, + }); + info(`Finished search: ${name}`); + + // Impression stats are updated only on engagement, so force one now. + // `selIndex` doesn't really matter but since we're not trying to simulate a + // click on the suggestion, pass in -1 to ensure we don't record a click. Pass + // in true for `isPrivate` so we don't attempt to record the impression ping + // because otherwise the following PingCentre error is logged: + // "Structured Ingestion ping failure with error: undefined" + let isPrivate = true; + UrlbarProviderQuickSuggest.onEngagement(isPrivate, "engagement", context, { + selIndex: -1, + }); +} + +async function checkTelemetryEvents(expectedEvents) { + TelemetryTestUtils.assertEvents( + expectedEvents.map(event => ({ + ...event, + category: QuickSuggestTestUtils.TELEMETRY_EVENT_CATEGORY, + method: "impression_cap", + })) + ); + Services.telemetry.clearEvents(); +}
--- a/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_merino.js +++ b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_merino.js @@ -3,19 +3,16 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ // Tests Merino integration with the quick suggest feature/provider. "use strict"; XPCOMUtils.defineLazyModuleGetters(this, { TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.jsm", - UrlbarProviderQuickSuggest: - "resource:///modules/UrlbarProviderQuickSuggest.jsm", - UrlbarQuickSuggest: "resource:///modules/UrlbarQuickSuggest.jsm", }); // We set the Merino timeout to a large value to avoid intermittent failures in // CI, especially TV tests, where the Merino fetch unexpectedly doesn't finish // before the default timeout. const TEST_MERINO_TIMEOUT_MS = 1000; // relative to `browser.urlbar`
--- a/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_positionInSuggestions.js +++ b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_positionInSuggestions.js @@ -6,18 +6,16 @@ /** * Tests for quick suggest result position specified in suggestions. */ XPCOMUtils.defineLazyModuleGetters(this, { UrlbarProviderHeuristicFallback: "resource:///modules/UrlbarProviderHeuristicFallback.jsm", UrlbarProviderPlaces: "resource:///modules/UrlbarProviderPlaces.jsm", - UrlbarProviderQuickSuggest: - "resource:///modules/UrlbarProviderQuickSuggest.jsm", UrlbarProviderTabToSearch: "resource:///modules/UrlbarProviderTabToSearch.jsm", }); const SPONSORED_SECOND_POSITION_SUGGEST = { id: 1, url: "http://example.com/?q=sponsored-second", title: "sponsored second",
--- a/browser/components/urlbar/tests/quicksuggest/unit/xpcshell.ini +++ b/browser/components/urlbar/tests/quicksuggest/unit/xpcshell.ini @@ -1,12 +1,13 @@ [DEFAULT] skip-if = toolkit == 'android' # bug 1730213 -head = head.js ../../unit/head.js +head = ../../unit/head.js head.js firefox-appdir = browser [test_quicksuggest.js] [test_quicksuggest_bestMatch.js] +[test_quicksuggest_impressionCaps.js] [test_quicksuggest_merino.js] [test_quicksuggest_migrate_v1.js] [test_quicksuggest_migrate_v2.js] [test_quicksuggest_offlineDefault.js] [test_quicksuggest_positionInSuggestions.js]
--- a/browser/themes/shared/privatebrowsing/aboutPrivateBrowsing.css +++ b/browser/themes/shared/privatebrowsing/aboutPrivateBrowsing.css @@ -344,24 +344,24 @@ p { background: url("chrome://global/skin/icons/close.svg") center no-repeat; cursor: pointer; -moz-context-properties: fill; fill: currentColor; position: relative; top: -90px; } -.promo-dismiss:hover { - background-color: var(--in-content-button-background) !important; -} - @media not (prefers-contrast) { .promo-dismiss { opacity: 0.5; } + + .promo-dismiss:hover { + background-color: var(--in-content-button-background) !important; + } } .promo-content { width: 100%; } .promo-image-large img { width: 100%; @@ -378,16 +378,17 @@ p { padding: 11px 15px; margin: 8px 0; appearance: button; background-color: var(--in-content-primary-button-background); color: var(--in-content-primary-button-text-color); border-radius: 4px; font-weight: 600; cursor: pointer; + border: 1px solid var(--in-content-primary-button-background); } .promo-cta .button:hover { background-color: var(--in-content-primary-button-background-hover); color: var(--in-content-primary-button-text-color-hover); } .promo.bottom .promo-cta {
--- a/build/win32/orderfile.txt +++ b/build/win32/orderfile.txt @@ -6175,17 +6175,16 @@ XPCOMService_GetSocketTransportService ?GetAsAUTF8String@?$Variant@V?$nsTString@D@@$0A@@storage@mozilla@@UAG?AW4nsresult@@AAV?$nsTSubstring@D@@@Z ?GetDetailedDescription@nsLocalHandlerApp@@UAG?AW4nsresult@@AAV?$nsTSubstring@_S@@@Z ?Add@SharedStringMapBuilder@ipc@dom@mozilla@@QAEXABV?$nsTString@D@@ABV?$nsTString@_S@@@Z ??0SharedStringMap@ipc@dom@mozilla@@QAE@$$QAVSharedStringMapBuilder@123@@Z ??4FileDescriptor@ipc@mozilla@@QAEAAV012@$$QAV012@@Z ?BroadcastStringBundle@ContentParent@dom@mozilla@@SAXABVStringBundleDescriptor@23@@Z ?Get@SharedStringMap@ipc@dom@mozilla@@QAE_NABV?$nsTString@D@@AAV?$nsTSubstring@_S@@@Z ?ReinitForContent@ImageBridgeChild@layers@mozilla@@SA_N$$QAV?$Endpoint@VPImageBridgeChild@layers@mozilla@@@ipc@3@I@Z -?RecvAudioSessionData@widget@mozilla@@YA?AW4nsresult@@ABUnsID@@ABV?$nsTString@_S@@1@Z ?Release@WakeLock@dom@mozilla@@UAGKXZ ?GetInstance@PowerManagerService@power@dom@mozilla@@SA?AU?$already_AddRefed@VPowerManagerService@power@dom@mozilla@@@@XZ ?RegisterWakeLockObserver@hal@mozilla@@YAXPAV?$Observer@VWakeLockInformation@hal@mozilla@@@2@@Z ?EndAnimationFrame@XRFrame@dom@mozilla@@QAEXXZ ?EnableWakeLockNotifications@hal_impl@mozilla@@YAXXZ ?FirePendingEvents@nsDOMOfflineResourceList@@QAEXXZ ?Run@nsBaseAppShell@@UAG?AW4nsresult@@XZ ??0AutoNoJSAPI@dom@mozilla@@AAE@PAUJSContext@@@Z
--- a/build/win64/orderfile.txt +++ b/build/win64/orderfile.txt @@ -6459,17 +6459,16 @@ ZN11encoding_rs3mem17utf16_valid_up_to17 ?GetAsAUTF8String@?$Variant@V?$nsTString@D@@$0A@@storage@mozilla@@UEAA?AW4nsresult@@AEAV?$nsTSubstring@D@@@Z ?GetDetailedDescription@nsLocalHandlerApp@@UEAA?AW4nsresult@@AEAV?$nsTSubstring@_S@@@Z ?Add@SharedStringMapBuilder@ipc@dom@mozilla@@QEAAXAEBV?$nsTString@D@@AEBV?$nsTString@_S@@@Z ??0SharedStringMap@ipc@dom@mozilla@@QEAA@$$QEAVSharedStringMapBuilder@123@@Z ??4FileDescriptor@ipc@mozilla@@QEAAAEAV012@$$QEAV012@@Z ?BroadcastStringBundle@ContentParent@dom@mozilla@@SAXAEBVStringBundleDescriptor@23@@Z ?Get@SharedStringMap@ipc@dom@mozilla@@QEAA_NAEBV?$nsTString@D@@AEAV?$nsTSubstring@_S@@@Z ?GetAndResetReleaseFence@RenderCompositor@wr@mozilla@@UEAA?AVFileDescriptor@ipc@3@XZ -?RecvAudioSessionData@widget@mozilla@@YA?AW4nsresult@@AEBUnsID@@AEBV?$nsTString@_S@@1@Z ?Release@WakeLock@dom@mozilla@@UEAAKXZ ?GetInstance@PowerManagerService@power@dom@mozilla@@SA?AU?$already_AddRefed@VPowerManagerService@power@dom@mozilla@@@@XZ ?RegisterWakeLockObserver@hal@mozilla@@YAXPEAV?$Observer@VWakeLockInformation@hal@mozilla@@@2@@Z ??4?$nsTArray_Impl@_KUnsTArrayInfallibleAllocator@@@@QEAAAEAV0@$$QEAV0@@Z ?EnableWakeLockNotifications@hal_impl@mozilla@@YAXXZ ?FirePendingEvents@nsDOMOfflineResourceList@@QEAAXXZ ?Run@nsBaseAppShell@@UEAA?AW4nsresult@@XZ ?BeforeProcessTask@CycleCollectedJSContext@mozilla@@UEAAX_N@Z
--- a/dom/bindings/test/test_large_imageData.html +++ b/dom/bindings/test/test_large_imageData.html @@ -46,14 +46,23 @@ https://bugzilla.mozilla.org/show_bug.cg try { ctx.getImageData(0, 0, 23175, 23175); } catch (e) { ex = e; } ok(ex.toString().includes("negative or greater than the allowed amount"), "Expected getImageData exception"); + ex = null; + try { + new ImageData(23175, 23175); + } catch (e) { + ex = e; + } + ok(ex.toString().includes("negative or greater than the allowed amount"), + "Expected ImageData constructor exception"); + SimpleTest.finish(); } go(); </script> </body> </html>
--- a/dom/canvas/ImageData.cpp +++ b/dom/canvas/ImageData.cpp @@ -45,18 +45,21 @@ NS_IMPL_CYCLE_COLLECTION_UNLINK_END already_AddRefed<ImageData> ImageData::Constructor(const GlobalObject& aGlobal, const uint32_t aWidth, const uint32_t aHeight, ErrorResult& aRv) { if (aWidth == 0 || aHeight == 0) { aRv.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR); return nullptr; } + + // Restrict the typed array length to INT32_MAX because that's all we support + // in dom::TypedArray::ComputeState. CheckedInt<uint32_t> length = CheckedInt<uint32_t>(aWidth) * aHeight * 4; - if (!length.isValid()) { + if (!length.isValid() || length.value() > INT32_MAX) { aRv.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR); return nullptr; } js::AssertSameCompartment(aGlobal.Context(), aGlobal.Get()); JSObject* data = Uint8ClampedArray::Create(aGlobal.Context(), length.value()); if (!data) { aRv.Throw(NS_ERROR_OUT_OF_MEMORY); return nullptr;
--- a/dom/ipc/ContentChild.cpp +++ b/dom/ipc/ContentChild.cpp @@ -221,17 +221,16 @@ #if defined(MOZ_WIDGET_ANDROID) # include "APKOpen.h" #endif #ifdef XP_WIN # include <process.h> # define getpid _getpid # include "mozilla/WinDllServices.h" -# include "mozilla/widget/AudioSession.h" # include "mozilla/widget/WinContentSystemParameters.h" #endif #if defined(XP_MACOSX) # include "nsMacUtilsImpl.h" #endif /* XP_MACOSX */ #ifdef MOZ_X11 @@ -3087,20 +3086,16 @@ void ContentChild::ShutdownInternal() { nsCOMPtr<nsIObserverService> os = services::GetObserverService(); if (os) { CrashReporter::AnnotateCrashReport( CrashReporter::Annotation::IPCShutdownState, "content-child-shutdown started"_ns); os->NotifyObservers(ToSupports(this), "content-child-shutdown", nullptr); } -#if defined(XP_WIN) - mozilla::widget::StopAudioSession(); -#endif - GetIPCChannel()->SetAbortOnError(false); if (mProfilerController) { const bool isProfiling = profiler_is_active(); CrashReporter::AnnotateCrashReport( CrashReporter::Annotation::ProfilerChildShutdownPhase, isProfiling ? "Profiling - GrabShutdownProfileAndShutdown"_ns : "Not profiling - GrabShutdownProfileAndShutdown"_ns);
--- a/dom/media/MediaDecoderStateMachine.cpp +++ b/dom/media/MediaDecoderStateMachine.cpp @@ -12,16 +12,17 @@ #include "mediasink/AudioSinkWrapper.h" #include "mediasink/DecodedStream.h" #include "mediasink/VideoSink.h" #include "mozilla/Logging.h" #include "mozilla/MathAlgorithms.h" #include "mozilla/NotNull.h" #include "mozilla/Preferences.h" #include "mozilla/ProfilerLabels.h" +#include "mozilla/ProfilerMarkers.h" #include "mozilla/ProfilerMarkerTypes.h" #include "mozilla/SharedThreadPool.h" #include "mozilla/Sprintf.h" #include "mozilla/StaticPrefs_media.h" #include "mozilla/Telemetry.h" #include "mozilla/TaskQueue.h" #include "mozilla/Tuple.h" #include "nsIMemoryReporter.h" @@ -2521,24 +2522,33 @@ void MediaDecoderStateMachine::DecodingS // Don't enter buffering while prerolling so that the decoder has a chance to // enqueue some decoded data before we give up and start buffering. if (!mMaster->IsPlaying()) { return; } // Note we could have a wait promise pending when playing non-MSE EME. - if ((mMaster->OutOfDecodedAudio() && mMaster->IsWaitingAudioData()) || - (mMaster->OutOfDecodedVideo() && mMaster->IsWaitingVideoData())) { + if (mMaster->OutOfDecodedAudio() && mMaster->IsWaitingAudioData()) { + PROFILER_MARKER_TEXT("MDSM::StartBuffering", MEDIA_PLAYBACK, {}, + "OutOfDecodedAudio"); + SetState<BufferingState>(); + return; + } + if (mMaster->OutOfDecodedVideo() && mMaster->IsWaitingVideoData()) { + PROFILER_MARKER_TEXT("MDSM::StartBuffering", MEDIA_PLAYBACK, {}, + "OutOfDecodedVideo"); SetState<BufferingState>(); return; } if (Reader()->UseBufferingHeuristics() && mMaster->HasLowDecodedData() && mMaster->HasLowBufferedData() && !mMaster->mCanPlayThrough) { + PROFILER_MARKER_TEXT("MDSM::StartBuffering", MEDIA_PLAYBACK, {}, + "BufferingHeuristics"); SetState<BufferingState>(); } } void MediaDecoderStateMachine::LoopingDecodingState::HandleError( const MediaResult& aError) { SLOG("audio looping failed, aError=%s", aError.ErrorName().get()); switch (aError.Code()) {
--- a/dom/media/webvtt/test/reftest/reftest.list +++ b/dom/media/webvtt/test/reftest/reftest.list @@ -1,2 +1,3 @@ skip-if(Android) fuzzy-if(/^Windows\x20NT\x2010\.0/.test(http.oscpu)&&/^aarch64-msvc/.test(xulRuntime.XPCOMABI),0-136,0-427680) == vtt_update_display_after_removed_cue.html vtt_update_display_after_removed_cue_ref.html skip-if(Android) fuzzy-if(winWidget,0-170,0-170) == vtt_overlapping_time.html vtt_overlapping_time-ref.html +skip-if(Android) != vtt_reflow_display.html vtt_reflow_display-ref.html
new file mode 100644 --- /dev/null +++ b/dom/media/webvtt/test/reftest/vtt_reflow_display-ref.html @@ -0,0 +1,28 @@ +<!DOCTYPE HTML> +<html> +<link rel="stylesheet" href="vtt_reflow_display.css"> +<body> +<div class="video-player"> + <div class="video-layer"> + <video id="v1" autoplay controls></video> + </div> +</div> +<script> +/** + * Simply play and pause a video without any cues. + */ +async function testDisplayCueDuringFrequentReflowRef() { + const video = document.getElementById("v1"); + video.src = "white.webm"; + video.onplay = _ => { + video.onplay = null; + video.pause(); + document.documentElement.removeAttribute('class'); + } +}; + +window.addEventListener("MozReftestInvalidate", + testDisplayCueDuringFrequentReflowRef); +</script> +</body> +</html>
new file mode 100644 --- /dev/null +++ b/dom/media/webvtt/test/reftest/vtt_reflow_display.css @@ -0,0 +1,33 @@ +body { + display: flex; + flex-direction: column; + align-items: center; + max-height: 100%; + width: 100vw; + height: 100vh; +} +.video-player { + display: flex; + max-height: calc(100% - 400px); + flex: 1 1 0; + flex-direction: column; + position: relative; + max-width: 100%; + height: 0; +} +.video-layer { + position: relative; + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + flex: 1 1 0; +} +video { + object-fit: contain; + display: flex; + flex: auto; + max-width: 100%; + min-height: 0; + min-width: 0; +}
new file mode 100644 --- /dev/null +++ b/dom/media/webvtt/test/reftest/vtt_reflow_display.html @@ -0,0 +1,37 @@ +<!DOCTYPE HTML> +<html class="reftest-wait"> +<head> +</head> +<link rel="stylesheet" href="vtt_reflow_display.css"> +<body> +<div class="video-player"> + <div class="video-layer"> + <video id="v1" autoplay controls></video> + </div> +</div> +<script> +/** + * In bug 1733232, setting some CSS properties (from bug 1733232 comment17) + * would cause video frame's reflow called very frequently, which crashed the + * video control and caused no cue showing. We compare this test with another + * white video without any cues, and they should NOT be equal. + */ +function testDisplayCueDuringFrequentReflow() { + let video = document.getElementById("v1"); + video.src = "white.webm"; + let cue = new VTTCue(0, 4, "hello testing"); + cue.onenter = _ => { + cue.onenter = null; + video.pause(); + document.documentElement.removeAttribute('class'); + } + let track = video.addTextTrack("captions"); + track.mode = "showing"; + track.addCue(cue); +}; + +window.addEventListener("MozReftestInvalidate", + testDisplayCueDuringFrequentReflow); +</script> +</body> +</html>
new file mode 100644 index 0000000000000000000000000000000000000000..bbacad7ffd8244de8d8082b21818aa5ab77021c4 GIT binary patch literal 10880 zc%1FpTS!xJ9KiA4nR6F{HkVVAV6&Id#7h!MMIEP6k;7m^_ogeC4c*0UmR;F7Iy*f? zzM5UE86n=0E;OM)qyht_2cZiUK`ZFNXkjm9&Yj`*a(<6J^gHa~u+RUuKl?v@AO86V z-iH}jd$cA#blP#u1*S`!%oO5ed6r;H!isEJ=Kg@V&TEc{Z#v6xxBl?Z3CoC(_yW$S zS&ntAb=`iY*%nJ=-3q6_T}v^Yb*5$2R>UX1MRITdU~h56{pe$D`Dp3h-35mRhU$tI zC%ZHzw&t?H&Ln=%9K3zd67V_fE{D(PZR==px{aqCKEKoBHkOr?mz7kMLqk>H`Wmx6 z;Pd!f?M7Eer^5#|c0nF{n)&H-4aEO~xc8nh@ny#%$8>Rwe^$`UwZC?`PG0yRigJQy z*vH&bPfvYg{n1LWxO?iI%?=M3svmSD)~M7`4#I4h0|=Js;09B~GD{#=szM<l)Gg?F zP~^y$6<qVJ);ImzD$o6k&)FFK`MDen-{hbG7G$8+jzAvd*-B%e3d$=QM}2kI{Qz;V zM!XsE5aRa`e~$Q9(X*St-t=T1wu1KM#rrSzwa=|Nvq5^EDMs9kxCilG#3vDdg?LQb zWqQPSA#RgCU)dRCz8~>v#NQ#lBJCMQ#H$cLj(9iXcM*RkeZCoAr03;o#J3|}i?|E% ztB6k^zJT}-Y0u0?e5dsJW;P)6orw1#9zpyq;=iR`VL+Tk+>Uq`;)BxXtC&URqtdQa zAzq015yaaNzk>L<gr`Ko#wf3xALdnhDg!D5Dg!D5Dg!D5Dg!Emco}F0R?8rt%7Ds% z%7Ds%%7Ds%%7DrsUIw{dDg!D5Dg!D5Dg!D5Dg!EmWEmJ(Dg!D5Dg!D5Dg!D5Due&I z4B}TU#S4nP$t#$frZ{+*ytTN@G2eM+f6}t2c;$RLs;w!g8A!)0cM9fu({Zzqf(AAn H3-0^{T6}6q
--- a/dom/quota/ActorsParent.cpp +++ b/dom/quota/ActorsParent.cpp @@ -172,29 +172,33 @@ // The amount of time, in milliseconds, that our IO thread will stay alive // after the last event it processes. #define DEFAULT_THREAD_TIMEOUT_MS 30000 /** * If shutdown takes this long, kill actors of a quota client, to avoid reaching * the crash timeout. */ -#define SHUTDOWN_FORCE_KILL_TIMEOUT_MS 5000 +#define SHUTDOWN_KILL_ACTORS_TIMEOUT_MS 5000 /** * Automatically crash the browser if shutdown of a quota client takes this * long. We've chosen a value that is long enough that it is unlikely for the * problem to be falsely triggered by slow system I/O. We've also chosen a * value long enough so that automated tests should time out and fail if * shutdown of a quota client takes too long. Also, this value is long enough * so that testers can notice the timeout; we want to know about the timeouts, * not hide them. On the other hand this value is less than 60 seconds which is * used by nsTerminator to crash a hung main process. */ -#define SHUTDOWN_FORCE_CRASH_TIMEOUT_MS 45000 +#define SHUTDOWN_CRASH_BROWSER_TIMEOUT_MS 45000 + +static_assert( + SHUTDOWN_CRASH_BROWSER_TIMEOUT_MS > SHUTDOWN_KILL_ACTORS_TIMEOUT_MS, + "The kill actors timeout must be shorter than the crash browser one."); // profile-before-change, when we need to shut down quota manager #define PROFILE_BEFORE_CHANGE_QM_OBSERVER_ID "profile-before-change-qm" #define KB *1024ULL #define MB *1024ULL KB #define GB *1024ULL MB @@ -3695,23 +3699,16 @@ nsresult QuotaManager::Init() { do_Init(mDefaultStoragePath), GetPathForStorage(*baseDir, nsLiteralString(DEFAULT_DIRECTORY_NAME))); QM_TRY_UNWRAP(do_Init(mIOThread), MOZ_TO_RESULT_INVOKE_TYPED( nsCOMPtr<nsIThread>, MOZ_SELECT_OVERLOAD(NS_NewNamedThread), "QuotaManager IO")); - // Make a timer here to avoid potential failures later. We don't actually - // initialize the timer until shutdown. - nsCOMPtr shutdownTimer = NS_NewTimer(); - QM_TRY(OkIf(shutdownTimer), Err(NS_ERROR_FAILURE)); - - mShutdownTimer.init(WrapNotNullUnchecked(std::move(shutdownTimer))); - static_assert(Client::IDB == 0 && Client::DOMCACHE == 1 && Client::SDB == 2 && Client::LS == 3 && Client::TYPE_MAX == 4, "Fix the registration!"); // Register clients. auto clients = decltype(mClients)::ValueType{}; clients.AppendElement(indexedDB::CreateQuotaClient()); clients.AppendElement(cache::CreateQuotaClient()); @@ -3756,22 +3753,30 @@ void QuotaManager::SafeMaybeRecordQuotaC auto* const quotaManager = QuotaManager::Get(); if (quotaManager && quotaManager->ShutdownStarted()) { quotaManager->RecordShutdownStep(Some(aClientType), aStepDescription); } } +void QuotaManager::RecordQuotaManagerShutdownStep( + const nsACString& aStepDescription) { + // Callable on any thread. + MOZ_ASSERT(mShutdownStarted); + + RecordShutdownStep(Nothing{}, aStepDescription); +} + void QuotaManager::MaybeRecordQuotaManagerShutdownStep( const nsACString& aStepDescription) { // Callable on any thread. if (ShutdownStarted()) { - RecordShutdownStep(Nothing{}, aStepDescription); + RecordQuotaManagerShutdownStep(aStepDescription); } } bool QuotaManager::ShutdownStarted() const { return mShutdownStarted; } void QuotaManager::RecordShutdownStep(const Maybe<Client::Type> aClientType, const nsACString& aStepDescription) { MOZ_ASSERT(mShutdownStarted); @@ -3806,150 +3811,215 @@ void QuotaManager::RecordShutdownStep(co #endif } void QuotaManager::Shutdown() { AssertIsOnOwningThread(); MOZ_ASSERT(!mShutdownStarted); MOZ_DIAGNOSTIC_ASSERT(!gShutdown); - // Setting this flag prevents the service from being recreated and prevents - // further storagess from being created. - gShutdown = true; - + // Define some local helper functions + + auto flagShutdownStarted = [this]() { + // Setting this flag prevents the service from being recreated and prevents + // further storages from being created. + // XXX: Harmonize QM shutdown flags, see bug 1726714 + gShutdown = true; + + // StopIdleMaintenance used to happen before mShutdownStarted is set true + // but it is just an internal flag for the recording of shutdown steps + // and not evaluated elsewhere. + + mShutdownStartedAt.init(TimeStamp::NowLoRes()); + mShutdownStarted = true; + }; + + nsCOMPtr<nsITimer> crashBrowserTimer; + + auto crashBrowserTimerCallback = [](nsITimer* aTimer, void* aClosure) { + auto* const quotaManager = static_cast<QuotaManager*>(aClosure); + + nsCString annotation; + + for (Client::Type type : quotaManager->AllClientTypes()) { + auto& quotaClient = *(*quotaManager->mClients)[type]; + + if (!quotaClient.IsShutdownCompleted()) { + annotation.AppendPrintf("%s: %s\nIntermediate steps:\n%s\n\n", + Client::TypeToText(type).get(), + quotaClient.GetShutdownStatus().get(), + quotaManager->mShutdownSteps[type].get()); + } + } + + { + MutexAutoLock lock(quotaManager->mQuotaMutex); + + annotation.AppendPrintf("QM: %zu normal origin ops pending\n", + gNormalOriginOps->Length()); +#ifdef MOZ_COLLECTING_RUNNABLE_TELEMETRY + for (const auto& op : *gNormalOriginOps) { + nsCString name; + op->GetName(name); + annotation.AppendPrintf("Op: %s pending\n", name.get()); + } +#endif + annotation.AppendPrintf("Intermediate steps:\n%s\n", + quotaManager->mQuotaManagerShutdownSteps.get()); + } + + CrashReporter::AnnotateCrashReport( + CrashReporter::Annotation::QuotaManagerShutdownTimeout, annotation); + + MOZ_CRASH("Quota manager shutdown timed out"); + }; + + auto startCrashBrowserTimer = [&]() { + crashBrowserTimer = NS_NewTimer(); + MOZ_ASSERT(crashBrowserTimer); + if (crashBrowserTimer) { + RecordQuotaManagerShutdownStep("startCrashBrowserTimer"_ns); + MOZ_ALWAYS_SUCCEEDS(crashBrowserTimer->InitWithNamedFuncCallback( + crashBrowserTimerCallback, this, SHUTDOWN_CRASH_BROWSER_TIMEOUT_MS, + nsITimer::TYPE_ONE_SHOT, + "quota::QuotaManager::Shutdown::crashBrowserTimer")); + } + }; + + auto stopCrashBrowserTimer = [&]() { + if (crashBrowserTimer) { + RecordQuotaManagerShutdownStep("stopCrashBrowserTimer"_ns); + QM_WARNONLY_TRY(QM_TO_RESULT(crashBrowserTimer->Cancel())); + } + }; + + auto initiateShutdownWorkThreads = [this]() { + RecordQuotaManagerShutdownStep("initiateShutdownWorkThreads"_ns); + bool needsToWait = false; + for (Client::Type type : AllClientTypes()) { + // Clients are supposed to also AbortAllOperations from this point on + // to speed up shutdown, if possible. Thus pending operations + // might not be executed anymore. + needsToWait |= (*mClients)[type]->InitiateShutdownWorkThreads(); + } + + return needsToWait; + }; + + nsCOMPtr<nsITimer> killActorsTimer; + + auto killActorsTimerCallback = [](nsITimer* aTimer, void* aClosure) { + auto* const quotaManager = static_cast<QuotaManager*>(aClosure); + + quotaManager->RecordQuotaManagerShutdownStep("killActorsTimerCallback"_ns); + + // XXX: This abort is a workaround to unblock shutdown, which + // ought to be removed by bug 1682326. We probably need more + // checks to immediately abort new operations during + // shutdown. + quotaManager->GetClient(Client::IDB)->AbortAllOperations(); + + for (Client::Type type : quotaManager->AllClientTypes()) { + quotaManager->GetClient(type)->ForceKillActors(); + } + }; + + auto startKillActorsTimer = [&]() { + killActorsTimer = NS_NewTimer(); + MOZ_ASSERT(killActorsTimer); + if (killActorsTimer) { + RecordQuotaManagerShutdownStep("startKillActorsTimer"_ns); + MOZ_ALWAYS_SUCCEEDS(killActorsTimer->InitWithNamedFuncCallback( + killActorsTimerCallback, this, SHUTDOWN_KILL_ACTORS_TIMEOUT_MS, + nsITimer::TYPE_ONE_SHOT, + "quota::QuotaManager::Shutdown::killActorsTimer")); + } + }; + + auto stopKillActorsTimer = [&]() { + if (killActorsTimer) { + RecordQuotaManagerShutdownStep("stopKillActorsTimer"_ns); + QM_WARNONLY_TRY(QM_TO_RESULT(killActorsTimer->Cancel())); + } + }; + + auto isAllClientsShutdownComplete = [this] { + return std::all_of(AllClientTypes().cbegin(), AllClientTypes().cend(), + [&self = *this](const auto type) { + return (*self.mClients)[type]->IsShutdownCompleted(); + }); + }; + + auto shutdownAndJoinWorkThreads = [this]() { + RecordQuotaManagerShutdownStep("shutdownAndJoinWorkThreads"_ns); + for (Client::Type type : AllClientTypes()) { + (*mClients)[type]->FinalizeShutdownWorkThreads(); + } + }; + + auto shutdownAndJoinIOThread = [this]() { + RecordQuotaManagerShutdownStep("shutdownAndJoinIOThread"_ns); + // NB: It's very important that runnable is destroyed on this thread + // (i.e. after we join the IO thread) because we can't release the + // QuotaManager on the IO thread. This should probably use + // NewNonOwningRunnableMethod ... + RefPtr<Runnable> runnable = + NewRunnableMethod("dom::quota::QuotaManager::ShutdownStorage", this, + &QuotaManager::ShutdownStorage); + MOZ_ASSERT(runnable); + + // Give clients a chance to cleanup IO thread only objects. + QM_WARNONLY_TRY( + QM_TO_RESULT((*mIOThread)->Dispatch(runnable, NS_DISPATCH_NORMAL))); + + // Make sure to join with our IO thread. + QM_WARNONLY_TRY(QM_TO_RESULT((*mIOThread)->Shutdown())); + }; + + auto invalidatePendingDirectoryLocks = [this]() { + RecordQuotaManagerShutdownStep("invalidatePendingDirectoryLocks"_ns); + for (RefPtr<DirectoryLockImpl>& lock : mPendingDirectoryLocks) { + lock->Invalidate(); + } + }; + + // Body of the function + + flagShutdownStarted(); + + startCrashBrowserTimer(); + + // XXX: StopIdleMaintenance now just notifies all clients to abort any + // maintenance work. + // This could be done as part of QuotaClient::AbortAllOperations. StopIdleMaintenance(); - mShutdownStartedAt.init(TimeStamp::NowLoRes()); - mShutdownStarted = true; - - const auto& allClientTypes = AllClientTypes(); - - bool needsToWait = false; - for (Client::Type type : allClientTypes) { - needsToWait |= (*mClients)[type]->InitiateShutdownWorkThreads(); - } - needsToWait |= static_cast<bool>(gNormalOriginOps); + const bool needsToWait = + initiateShutdownWorkThreads() | static_cast<bool>(gNormalOriginOps); // If any clients cannot shutdown immediately, spin the event loop while we - // wait on all the threads to close. Our timer may fire during that loop. + // wait on all the threads to close. if (needsToWait) { - MOZ_ALWAYS_SUCCEEDS( - (*mShutdownTimer) - ->InitWithNamedFuncCallback( - [](nsITimer* aTimer, void* aClosure) { - auto* const quotaManager = - static_cast<QuotaManager*>(aClosure); - - for (Client::Type type : quotaManager->AllClientTypes()) { - // XXX This is a workaround to unblock shutdown, which ought - // to be removed by Bug 1682326. - if (type == Client::IDB) { - (*quotaManager->mClients)[type]->AbortAllOperations(); - } - - (*quotaManager->mClients)[type]->ForceKillActors(); - } - - MOZ_ALWAYS_SUCCEEDS(aTimer->InitWithNamedFuncCallback( - [](nsITimer* aTimer, void* aClosure) { - auto* const quotaManager = - static_cast<QuotaManager*>(aClosure); - - nsCString annotation; - - { - for (Client::Type type : - quotaManager->AllClientTypes()) { - auto& quotaClient = - *(*quotaManager->mClients)[type]; - - if (!quotaClient.IsShutdownCompleted()) { - annotation.AppendPrintf( - "%s: %s\nIntermediate steps:\n%s\n\n", - Client::TypeToText(type).get(), - quotaClient.GetShutdownStatus().get(), - quotaManager->mShutdownSteps[type].get()); - } - } - - if (gNormalOriginOps) { - MutexAutoLock lock(quotaManager->mQuotaMutex); - - annotation.AppendPrintf( - "QM: %zu normal origin ops pending\n", - gNormalOriginOps->Length()); -#ifdef MOZ_COLLECTING_RUNNABLE_TELEMETRY - for (const auto& op : *gNormalOriginOps) { - nsCString name; - op->GetName(name); - annotation.AppendPrintf("Op: %s pending\n", - name.get()); - } -#endif - annotation.AppendPrintf( - "Intermediate steps:\n%s\n", - quotaManager->mQuotaManagerShutdownSteps.get()); - } - } - - // We expect that at least one quota client didn't - // complete its shutdown. - MOZ_DIAGNOSTIC_ASSERT(!annotation.IsEmpty()); - - CrashReporter::AnnotateCrashReport( - CrashReporter::Annotation:: - QuotaManagerShutdownTimeout, - annotation); - - MOZ_CRASH("Quota manager shutdown timed out"); - }, - aClosure, SHUTDOWN_FORCE_CRASH_TIMEOUT_MS, - nsITimer::TYPE_ONE_SHOT, - "quota::QuotaManager::ForceCrashTimer")); - }, - this, SHUTDOWN_FORCE_KILL_TIMEOUT_MS, nsITimer::TYPE_ONE_SHOT, - "quota::QuotaManager::ForceKillTimer")); + startKillActorsTimer(); MOZ_ALWAYS_TRUE(SpinEventLoopUntil( - "QuotaManager::Shutdown"_ns, [this, &allClientTypes] { - return !gNormalOriginOps && - std::all_of( - allClientTypes.cbegin(), allClientTypes.cend(), - [&self = *this](const auto type) { - return (*self.mClients)[type]->IsShutdownCompleted(); - }); + "QuotaManager::Shutdown"_ns, [isAllClientsShutdownComplete]() { + return !gNormalOriginOps && isAllClientsShutdownComplete(); })); - } - - for (Client::Type type : allClientTypes) { - (*mClients)[type]->FinalizeShutdownWorkThreads(); - } - - // Cancel the timer regardless of whether it actually fired. - QM_WARNONLY_TRY(QM_TO_RESULT((*mShutdownTimer)->Cancel())); - - // NB: It's very important that runnable is destroyed on this thread - // (i.e. after we join the IO thread) because we can't release the - // QuotaManager on the IO thread. This should probably use - // NewNonOwningRunnableMethod ... - RefPtr<Runnable> runnable = - NewRunnableMethod("dom::quota::QuotaManager::ShutdownStorage", this, - &QuotaManager::ShutdownStorage); - MOZ_ASSERT(runnable); - - // Give clients a chance to cleanup IO thread only objects. - QM_WARNONLY_TRY( - QM_TO_RESULT((*mIOThread)->Dispatch(runnable, NS_DISPATCH_NORMAL))); - - // Make sure to join with our IO thread. - QM_WARNONLY_TRY(QM_TO_RESULT((*mIOThread)->Shutdown())); - - for (RefPtr<DirectoryLockImpl>& lock : mPendingDirectoryLocks) { - lock->Invalidate(); - } + + stopKillActorsTimer(); + } + + shutdownAndJoinWorkThreads(); + + shutdownAndJoinIOThread(); + + invalidatePendingDirectoryLocks(); + + stopCrashBrowserTimer(); } void QuotaManager::InitQuotaForOrigin( const FullOriginMetadata& aFullOriginMetadata, const ClientUsageArray& aClientUsages, uint64_t aUsageBytes) { AssertIsOnIOThread(); MOZ_ASSERT(IsBestEffortPersistenceType(aFullOriginMetadata.mPersistenceType));
--- a/dom/quota/QuotaManager.h +++ b/dom/quota/QuotaManager.h @@ -376,16 +376,19 @@ class QuotaManager final : public Backgr static void MaybeRecordQuotaClientShutdownStep( const Client::Type aClientType, const nsACString& aStepDescription); // Record a quota client shutdown step, if shutting down. // Checks if the QuotaManager singleton is alive. static void SafeMaybeRecordQuotaClientShutdownStep( Client::Type aClientType, const nsACString& aStepDescription); + // Record a quota manager shutdown step, use only if shutdown is active. + void RecordQuotaManagerShutdownStep(const nsACString& aStepDescription); + // Record a quota manager shutdown step, if shutting down. void MaybeRecordQuotaManagerShutdownStep(const nsACString& aStepDescription); template <typename F> void MaybeRecordQuotaManagerShutdownStepWith(F&& aFunc); static void GetStorageId(PersistenceType aPersistenceType, const nsACString& aOrigin, Client::Type aClientType, @@ -582,19 +585,16 @@ class QuotaManager final : public Backgr static OriginInfosFlatTraversable CollectLRUOriginInfosUntil( Collect&& aCollect, Pred&& aPred); // Thread on which IO is performed. LazyInitializedOnceNotNull<const nsCOMPtr<nsIThread>> mIOThread; <