SuggestFeature.sys.mjs (15573B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 /* eslint-disable no-unused-vars */ 6 7 const lazy = {}; 8 9 ChromeUtils.defineESModuleGetters(lazy, { 10 QuickSuggest: "moz-src:///browser/components/urlbar/QuickSuggest.sys.mjs", 11 UrlbarPrefs: "moz-src:///browser/components/urlbar/UrlbarPrefs.sys.mjs", 12 UrlbarUtils: "moz-src:///browser/components/urlbar/UrlbarUtils.sys.mjs", 13 }); 14 15 /** 16 * Base class for Suggest features. It can be extended to implement a feature 17 * that should be enabled only when Suggest is enabled. Most features should 18 * extend one of the `SuggestFeature` subclasses, however. 19 * 20 * Subclasses should be registered with `QuickSuggest` by adding them to the 21 * `FEATURES` const in `QuickSuggest.sys.mjs`. 22 */ 23 export class SuggestFeature { 24 // Methods designed for overriding below 25 26 /** 27 * @returns {Array} 28 * If the feature is conditioned on any prefs or Nimbus variables, the 29 * subclass should override this getter and return their names in this 30 * array. When one of these prefs or variables changes, Suggest will call 31 * `feature.update()`, which checks `feature.shouldEnable`. If the value of 32 * `shouldEnable` is different from the feature's current enabled status, 33 * then `feature.update()` calls `feature.enable()`. 34 * 35 * Pref and variable names should be recognized by `UrlbarPrefs`. i.e., pref 36 * names should be relative to the `browser.urlbar.` branch. For Nimbus 37 * variables with fallback prefs, include only the variable name. 38 */ 39 get enablingPreferences() { 40 return []; 41 } 42 43 /** 44 * @returns {Array} 45 * If there are any feature-specific prefs that are exposed to the user and 46 * allow the feature to be toggled on or off, the subclass should override 47 * this getter and return their names. They should also be included in 48 * `enablingPreferences`. The names should be recognized by `UrlbarPrefs`, 49 * i.e., they should be relative to the `browser.urlbar.` branch. 50 * 51 * If the feature is a `SuggestProvider`, typically this should include the 52 * pref that's named `suggest.mySuggestionType` and set to `false` when the 53 * user dismisses the entire suggestion type, i.e., the relevant 54 * These prefs should be controlled by the user, so they should never 55 * include the feature's `featureGate` pref. 56 * 57 * These prefs should control this feature specifically, so they should 58 * never include `suggest.quicksuggest.all` or 59 * `suggest.quicksuggest.sponsored`. If the feature has no such prefs, 60 * this getter should return an empty array. 61 */ 62 get primaryUserControlledPreferences() { 63 return []; 64 } 65 66 /** 67 * @returns {boolean} 68 * Whether the feature should be enabled, assuming Suggest is enabled. This 69 * base implementation returns true if every pref in `enablingPreferences` 70 * is truthy. The subclass should override it if it needs different logic. 71 * 72 * This getter will be called only when Suggest is enabled. 73 */ 74 get shouldEnable() { 75 return this.enablingPreferences.every(p => lazy.UrlbarPrefs.get(p)); 76 } 77 78 /** 79 * This method should initialize or uninitialize any state related to the 80 * feature. It will only be called when the enabled status changes, i.e., when 81 * it goes from false to true or true to false. 82 * 83 * @param {boolean} enabled 84 * Whether the feature should be enabled or not. 85 */ 86 enable(enabled) {} 87 88 // Methods not designed for overriding below 89 90 /** 91 * @returns {ConsoleInstance} 92 * The feature's logger. 93 */ 94 get logger() { 95 if (!this._logger) { 96 this._logger = lazy.UrlbarUtils.getLogger({ 97 prefix: `QuickSuggest.${this.name}`, 98 }); 99 } 100 return this._logger; 101 } 102 103 /** 104 * @returns {boolean} 105 * Whether the feature is enabled. The enabled status is automatically 106 * managed by `QuickSuggest` and subclasses should not override this. 107 */ 108 get isEnabled() { 109 return this.#isEnabled; 110 } 111 112 /** 113 * @returns {string} 114 * The feature's name. 115 */ 116 get name() { 117 return this.constructor.name; 118 } 119 120 /** 121 * Enables or disables the feature according to `shouldEnable` and whether 122 * Suggest is enabled. If the feature's enabled status changes, `enable()` is 123 * called with the new status; otherwise `enable()` is not called. 124 */ 125 update() { 126 let enable = 127 lazy.UrlbarPrefs.get("quickSuggestEnabled") && this.shouldEnable; 128 if (enable != this.isEnabled) { 129 this.logger.info("Feature enabled status changed", { 130 nowEnabled: enable, 131 }); 132 this.#isEnabled = enable; 133 this.enable(enable); 134 } 135 } 136 137 #isEnabled = false; 138 } 139 140 /** 141 * Base class for Suggest features that manage a suggestion type [1]. 142 * 143 * The same suggestion type can be served by multiple backends, and a single 144 * `SuggestProvider` subclass can manage the type regardless of backend by 145 * overriding the appropriate methods and getters. 146 * 147 * Subclasses should be registered with `QuickSuggest` by adding them to the 148 * `FEATURES` const in `QuickSuggest.sys.mjs`. 149 * 150 * [1] Typically a feature should manage only one type. In rare cases, it might 151 * make sense to manage multiple types, for example when a single Merino 152 * provider serves more than one type of suggestion. 153 */ 154 export class SuggestProvider extends SuggestFeature { 155 // Methods designed for overriding below 156 157 /** 158 * @returns {string} 159 * If the feature's suggestions are served by Merino, the subclass should 160 * override this getter and return the name of the Merino provider that 161 * serves them. 162 */ 163 get merinoProvider() { 164 return ""; 165 } 166 167 /** 168 * @returns {string} 169 * If the feature's suggestions are served by the Rust component, the 170 * subclass should override this getter and return their type name as 171 * defined by the `Suggestion` enum in the component. e.g., "Amp", 172 * "Wikipedia", "Mdn", etc. 173 */ 174 get rustSuggestionType() { 175 return ""; 176 } 177 178 /** 179 * @returns {Array} 180 * If the feature manages dynamic Rust suggestions, its `rustSuggestionType` 181 * getter should return "Dynamic", and it should override 182 * `dynamicRustSuggestionTypes` to return an array of the dynamic type 183 * names as defined by `suggestion_type` in the remote settings records. 184 */ 185 get dynamicRustSuggestionTypes() { 186 return []; 187 } 188 189 /** 190 * @returns {object|null} 191 * If the feature manages suggestions served by the Rust component that 192 * require provider constraints, the subclass should override this getter 193 * and return a plain JS object that can be passed to 194 * `SuggestionProviderConstraints()`. This getter will only be called if the 195 * feature is enabled. 196 */ 197 get rustProviderConstraints() { 198 if (this.dynamicRustSuggestionTypes?.length) { 199 return { 200 dynamicSuggestionTypes: this.dynamicRustSuggestionTypes, 201 }; 202 } 203 return null; 204 } 205 206 /** 207 * @returns {string} 208 * If the feature's suggestions are served by the ML backend, the subclass 209 * should override this getter and return the ML intent name as returned by 210 * `MLSuggest`. e.g., "yelp_intent" 211 */ 212 get mlIntent() { 213 return ""; 214 } 215 216 /** 217 * @returns {boolean} 218 * If the feature's suggestions are served by the ML backend, the subclass 219 * should override this getter and return true if the ML suggestions should 220 * be enabled and false otherwise. 221 */ 222 get isMlIntentEnabled() { 223 return false; 224 } 225 226 /** 227 * Subclasses should typically override this method. It should return the 228 * telemetry type for the given suggestion. A telemetry type uniquely 229 * identifies a type of Suggest suggestion independent of the backend that 230 * returned it. It's used to build the result type values that are recorded in 231 * urlbar telemetry. The telemetry type does not include the suggestion's 232 * source/backend. For example, "adm_sponsored" is the AMP suggestion 233 * telemetry type, not "rust_adm_sponsored". 234 * 235 * @param {object} suggestion 236 * A suggestion returned by one of the Suggest backends. 237 * @returns {string} 238 * The suggestion's telemetry type. 239 */ 240 getSuggestionTelemetryType(suggestion) { 241 return this.merinoProvider; 242 } 243 244 /** 245 * Gets the list of commands that should be shown in the result menu for a 246 * given result from the provider. All commands returned by this method should 247 * be handled by implementing `onEngagement()` with the possible exception of 248 * commands automatically handled by the urlbar, like "help". 249 * 250 * @returns {?UrlbarResultCommand[]} 251 */ 252 getResultCommand() { 253 return undefined; 254 } 255 256 /** 257 * The subclass should override this method if it manages any sponsored 258 * suggestion types. It should return true if the given suggestion should be 259 * considered sponsored. 260 * 261 * @param {object} suggestion 262 * A suggestion returned by one of the Suggest backends. 263 * @returns {boolean} 264 * Whether the suggestion should be considered sponsored. 265 */ 266 isSuggestionSponsored(suggestion) { 267 return false; 268 } 269 270 /** 271 * The subclass may override this method as necessary. It will be called once 272 * per query with all of the feature's suggestions that matched the query. It 273 * should filter out suggestions that should not be shown. This is useful in 274 * cases where a backend may return many of the feature's suggestions but only 275 * some of them should be shown, and the criteria for determining which to 276 * show are external to the backend. 277 * 278 * `makeResult()` can also be used to filter suggestions by returning null for 279 * suggestions that should be discarded. Use `filterSuggestions()` when you 280 * need to know all matching suggestions in order to decide which to show. 281 * 282 * @param {Array} suggestions 283 * All the feature's suggestions that matched a query. 284 * @returns {Promise<Array>} 285 * The subset of `suggestions` that should be shown (typically all). 286 */ 287 async filterSuggestions(suggestions) { 288 return suggestions; 289 } 290 291 /** 292 * The subclass should override this method. It should return either a new 293 * `UrlbarResult` for the given suggestion or null if the suggestion should 294 * not be shown. 295 * 296 * @param {UrlbarQueryContext} queryContext 297 * The query context. 298 * @param {object} suggestion 299 * A suggestion returned by one of the Suggest backends. 300 * @param {string} searchString 301 * The search string that was used to fetch the suggestion. It may be 302 * different from `queryContext.searchString` due to trimming, lower-casing, 303 * etc. This is included as a param in case it's useful. 304 * @returns {Promise<UrlbarResult|null>} 305 * A new result for the suggestion or null if a result should not be shown. 306 */ 307 async makeResult(queryContext, suggestion, searchString) { 308 return null; 309 } 310 311 /** 312 * The subclass may override this method as necessary. It's analogous to 313 * `UrlbarProvider.onImpression()` and will be called when one or more of the 314 * feature's results were visible at the end of a urlbar session. 315 * 316 * @param {string} state 317 * The user-interaction state. See `UrlbarProvider.onImpression()`. 318 * @param {UrlbarQueryContext} queryContext 319 * The urlbar session's query context. 320 * @param {UrlbarController} controller 321 * The controller. 322 * @param {Array} featureResults 323 * The feature's results that were visible at the end of the session. This 324 * will always be non-empty and will only contain results from the feature. 325 * @param {object|null} details 326 * Details about the engagement. See `UrlbarProvider.onImpression()`. 327 */ 328 onImpression(state, queryContext, controller, featureResults, details) {} 329 330 /** 331 * The subclass may override this method as necessary. It's analogous to 332 * `UrlbarProvider.onEngagement()` and will be called when the user engages 333 * with a result from the feature. 334 * 335 * @param {UrlbarQueryContext} queryContext 336 * The urlbar session's query context. 337 * @param {UrlbarController} controller 338 * The controller. 339 * @param {object|null} details 340 * See `UrlbarProvider.onEngagement()`. 341 * @param {string} searchString 342 * The actual search string used to fetch Suggest results. It might be 343 * slightly different from `queryContext.searchString`. e.g., it might be 344 * trimmed differently. 345 */ 346 onEngagement(queryContext, controller, details, searchString) {} 347 348 /** 349 * Some features may create result URLs that are potentially unique per query. 350 * Typically this is done by modifying an original suggestion URL at query 351 * time, for example by adding timestamps or query-specific search params. In 352 * that case, a single original suggestion URL will map to many result URLs. 353 * If this is true for the subclass, it should override this method and return 354 * whether the given URL and result URL both map back to the same original 355 * suggestion URL. 356 * 357 * @param {string} url 358 * The URL to check, typically from the user's history. 359 * @param {UrlbarResult} result 360 * The Suggest result. 361 * @returns {boolean} 362 * Whether `url` is equivalent to the result's URL. 363 */ 364 isUrlEquivalentToResultUrl(url, result) { 365 return url == result.payload.url; 366 } 367 368 // Methods not designed for overriding below 369 370 /** 371 * Enables or disables the feature. If the feature manages any Rust suggestion 372 * types that become enabled as a result, they will be ingested. 373 */ 374 update() { 375 super.update(); 376 lazy.QuickSuggest.rustBackend?.ingestEnabledSuggestions(this); 377 } 378 } 379 380 /** 381 * Base class for Suggest features that serve suggestions. None of the methods 382 * will be called when the backend is disabled. 383 * 384 * Subclasses should be registered with `QuickSuggest` by adding them to the 385 * `FEATURES` const in `QuickSuggest.sys.mjs`. 386 */ 387 export class SuggestBackend extends SuggestFeature { 388 // Methods designed for overriding below 389 390 /** 391 * The subclass should override this method. It should fetch and return 392 * matching suggestions. 393 * 394 * @param {string} searchString 395 * The search string. 396 * @param {object} [options] 397 * Options object. 398 * @param {UrlbarQueryContext} [options.queryContext] 399 * The query context. 400 * @param {?Array} [options.types] 401 * This is only intended to be used in special circumstances and normally 402 * should not be specified. Array of suggestion types to query. By default 403 * all enabled suggestion types are queried. 404 * @returns {Promise<Array>} 405 * Array of matching suggestions. An empty array should be returned if no 406 * suggestions matched or suggestions can't be fetched for any reason. 407 * @abstract 408 */ 409 async query(searchString, { queryContext, types = null } = {}) { 410 throw new Error("Trying to access the base class, must be overridden"); 411 } 412 413 /** 414 * The subclass should override this method if anything needs to be stopped or 415 * cleaned up when a query is canceled. 416 */ 417 cancelQuery() {} 418 419 /** 420 * The subclass should override this method as necessary. It's called on the 421 * backend in response to `UrlbarProviderQuickSuggest.onSearchSessionEnd()`. 422 * 423 * @param {UrlbarQueryContext} queryContext 424 * The query context. 425 * @param {UrlbarController} controller 426 * The controller. 427 * @param {object} details 428 * Details object. 429 */ 430 onSearchSessionEnd(queryContext, controller, details) {} 431 }