Interactions.sys.mjs (25557B)
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 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; 6 7 const lazy = {}; 8 9 ChromeUtils.defineESModuleGetters(lazy, { 10 BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs", 11 InteractionsBlocklist: 12 "moz-src:///browser/components/places/InteractionsBlocklist.sys.mjs", 13 PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", 14 PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", 15 clearTimeout: "resource://gre/modules/Timer.sys.mjs", 16 setTimeout: "resource://gre/modules/Timer.sys.mjs", 17 }); 18 19 ChromeUtils.defineLazyGetter(lazy, "logConsole", function () { 20 return console.createInstance({ 21 prefix: "InteractionsManager", 22 maxLogLevel: Services.prefs.getBoolPref( 23 "browser.places.interactions.log", 24 false 25 ) 26 ? "Debug" 27 : "Warn", 28 }); 29 }); 30 31 XPCOMUtils.defineLazyServiceGetters(lazy, { 32 idleService: ["@mozilla.org/widget/useridleservice;1", Ci.nsIUserIdleService], 33 }); 34 35 XPCOMUtils.defineLazyPreferenceGetter( 36 lazy, 37 "pageViewIdleTime", 38 "browser.places.interactions.pageViewIdleTime", 39 60 40 ); 41 42 XPCOMUtils.defineLazyPreferenceGetter( 43 lazy, 44 "saveInterval", 45 "browser.places.interactions.saveInterval", 46 10000 47 ); 48 49 XPCOMUtils.defineLazyPreferenceGetter( 50 lazy, 51 "isHistoryEnabled", 52 "places.history.enabled", 53 false 54 ); 55 56 XPCOMUtils.defineLazyPreferenceGetter( 57 lazy, 58 "breakupIfNoUpdatesForSeconds", 59 "browser.places.interactions.breakupIfNoUpdatesForSeconds", 60 60 * 60 61 ); 62 63 const DOMWINDOW_OPENED_TOPIC = "domwindowopened"; 64 const RECENT_BROWSER_INTERACTION_EXPIRY_TIME_MS = 60000; 65 66 /** 67 * Returns a monotonically increasing timestamp, that is critical to distinguish 68 * database entries by creation time. 69 */ 70 let gLastTime = 0; 71 function monotonicNow() { 72 let time = Date.now(); 73 if (time == gLastTime) { 74 time++; 75 } 76 return (gLastTime = time); 77 } 78 79 /** 80 * @typedef {object} DocumentInfo 81 * DocumentInfo is used to pass document information from the child process 82 * to _Interactions. 83 * @property {boolean} isActive 84 * Set to true if the document is active, i.e. visible. 85 * @property {string} url 86 * The url of the page that was interacted with. 87 */ 88 89 /** 90 * @typedef {object} InteractionInfo 91 * InteractionInfo is used to store information associated with interactions. 92 * @property {number} totalViewTime 93 * Time in milliseconds that the page has been actively viewed for. 94 * @property {string} url 95 * The url of the page that was interacted with. 96 * @property {Interactions.DOCUMENT_TYPE} documentType 97 * The type of the document. 98 * @property {number} typingTime 99 * Time in milliseconds that the user typed on the page 100 * @property {number} keypresses 101 * The number of keypresses made on the page 102 * @property {number} scrollingTime 103 * Time in milliseconds that the user spent scrolling the page 104 * @property {number} scrollingDistance 105 * The distance, in pixels, that the user scrolled the page 106 * @property {number} created_at 107 * Creation time as the number of milliseconds since the epoch. 108 * @property {number} updated_at 109 * Last updated time as the number of milliseconds since the epoch. 110 * @property {string} referrer 111 * The referrer to the url of the page that was interacted with (may be empty) 112 */ 113 114 /** 115 * The Interactions object sets up listeners and other approriate tools for 116 * obtaining interaction information and passing it to the InteractionsManager. 117 */ 118 class _Interactions { 119 DOCUMENT_TYPE = { 120 // Used when the document type is unknown. 121 GENERIC: 0, 122 // Used for pages serving media, e.g. videos. 123 MEDIA: 1, 124 }; 125 126 /** 127 * This is used to store potential interactions. It maps the browser 128 * to the current interaction information. 129 * The current interaction is updated to the database when it transitions 130 * to non-active, which occurs before a browser tab is closed, hence this 131 * can be a weak map. 132 * 133 * @type {WeakMap<browser, InteractionInfo>} 134 */ 135 #interactions = new WeakMap(); 136 137 /** 138 * Tracks the currently active window so that we can avoid recording 139 * interactions in non-active windows. 140 * 141 * @type {DOMWindow} 142 */ 143 #activeWindow = undefined; 144 145 /** 146 * Tracks if the user is idle. 147 * 148 * @type {boolean} 149 */ 150 #userIsIdle = false; 151 152 /** 153 * This stores the page view start time of the current page view. 154 * For any single page view, this may be moved multiple times as the 155 * associated interaction is updated for the current total page view time. 156 * 157 * @type {number} 158 */ 159 _pageViewStartTime = ChromeUtils.now(); 160 161 /** 162 * Stores interactions in the database, see the {@link InteractionsStore} 163 * class. This is created lazily, see the `store` getter. 164 * 165 * @type {InteractionsStore | undefined} 166 */ 167 #store = undefined; 168 169 /** 170 * Whether the component has been initialized. 171 */ 172 #initialized = false; 173 174 /** 175 * Maps a browser to its interactions which are less than 176 * RECENT_BROWSER_INTERACTION_EXPIRY_TIME_MS old. 177 * 178 * @type {WeakMap<browser, InteractionInfo>} 179 */ 180 #recentInteractions = new WeakMap(); 181 182 /** 183 * Initializes, sets up actors and observers. 184 */ 185 init() { 186 if ( 187 !Services.prefs.getBoolPref("browser.places.interactions.enabled", false) 188 ) { 189 return; 190 } 191 192 ChromeUtils.registerWindowActor("Interactions", { 193 parent: { 194 esModuleURI: "resource:///actors/InteractionsParent.sys.mjs", 195 }, 196 child: { 197 esModuleURI: "resource:///actors/InteractionsChild.sys.mjs", 198 events: { 199 DOMContentLoaded: {}, 200 pagehide: { mozSystemGroup: true }, 201 }, 202 }, 203 messageManagerGroups: ["browsers"], 204 }); 205 206 this.#activeWindow = Services.wm.getMostRecentBrowserWindow(); 207 208 for (let win of lazy.BrowserWindowTracker.orderedWindows) { 209 if (!win.closed) { 210 this.#registerWindow(win); 211 } 212 } 213 Services.obs.addObserver(this, DOMWINDOW_OPENED_TOPIC, true); 214 lazy.idleService.addIdleObserver(this, lazy.pageViewIdleTime); 215 216 this.#initialized = true; 217 } 218 219 /** 220 * Uninitializes, removes any observers that need cleaning up manually. 221 */ 222 uninit() { 223 if (this.#initialized) { 224 lazy.idleService.removeIdleObserver(this, lazy.pageViewIdleTime); 225 } 226 } 227 228 /** 229 * Resets any stored user or interaction state. 230 * Used by tests. 231 */ 232 async reset() { 233 lazy.logConsole.debug("Database reset"); 234 this.#interactions = new WeakMap(); 235 this.#userIsIdle = false; 236 this._pageViewStartTime = ChromeUtils.now(); 237 ChromeUtils.consumeInteractionData(); 238 await _Interactions.interactionUpdatePromise; 239 await this.store.reset(); 240 } 241 242 /** 243 * Retrieve the underlying InteractionsStore object. This exists for testing 244 * purposes and should not be abused by production code (for example it'd be 245 * a bad idea to force flushes). 246 * 247 * @returns {InteractionsStore} 248 */ 249 get store() { 250 if (!this.#store) { 251 this.#store = new InteractionsStore(); 252 } 253 return this.#store; 254 } 255 256 /** 257 * Registers the start of a new interaction. 258 * 259 * @param {Browser} browser 260 * The browser object associated with the interaction. 261 * @param {DocumentInfo} docInfo 262 * The document information of the page associated with the interaction. 263 */ 264 registerNewInteraction(browser, docInfo) { 265 if ( 266 !browser || 267 !lazy.isHistoryEnabled || 268 !browser.browsingContext.useGlobalHistory 269 ) { 270 return; 271 } 272 let interaction = this.#interactions.get(browser); 273 if (interaction && interaction.url != docInfo.url) { 274 this.registerEndOfInteraction(browser); 275 } 276 277 if (lazy.InteractionsBlocklist.isUrlBlocklisted(docInfo.url)) { 278 lazy.logConsole.debug( 279 "Ignoring a page as the URL is blocklisted", 280 docInfo 281 ); 282 return; 283 } 284 285 lazy.logConsole.debug("Tracking a new interaction", docInfo); 286 let now = monotonicNow(); 287 interaction = { 288 url: docInfo.url, 289 referrer: docInfo.referrer, 290 totalViewTime: 0, 291 typingTime: 0, 292 keypresses: 0, 293 scrollingTime: 0, 294 scrollingDistance: 0, 295 created_at: now, 296 updated_at: now, 297 }; 298 this.#interactions.set(browser, interaction); 299 300 // Only reset the time if this is being loaded in the active tab of the 301 // active window. 302 if (docInfo.isActive && browser.ownerGlobal == this.#activeWindow) { 303 this._pageViewStartTime = ChromeUtils.now(); 304 } 305 306 this.#recentInteractions.set(browser, [ 307 ...(this.#recentInteractions.get(browser) ?? []), 308 interaction, 309 ]); 310 311 this.#pruneOldRecentInteractions(browser); 312 } 313 314 /** 315 * Registers the end of an interaction, e.g. if the user navigates away 316 * from the page. This will store the final interaction details and clear 317 * the current interaction. 318 * 319 * @param {Browser} browser 320 * The browser object associated with the interaction. 321 */ 322 registerEndOfInteraction(browser) { 323 // Not having a browser passed to us probably means the tab has gone away 324 // before we received the notification - due to the tab being a background 325 // tab. Since that will be a non-active tab, it is acceptable that we don't 326 // update the interaction. When switching away from active tabs, a TabSelect 327 // notification is generated which we handle elsewhere. 328 if ( 329 !browser || 330 !lazy.isHistoryEnabled || 331 !browser.browsingContext.useGlobalHistory 332 ) { 333 return; 334 } 335 lazy.logConsole.debug("Saw the end of an interaction"); 336 337 this.#updateInteraction(browser); 338 this.#interactions.delete(browser); 339 } 340 341 /** 342 * Updates the current interaction 343 * 344 * @param {Browser} [browser] 345 * The browser object that has triggered the update, if known. This is 346 * used to check if the browser is in the active window, and as an 347 * optimization to avoid obtaining the browser object. 348 */ 349 #updateInteraction(browser = undefined) { 350 _Interactions.#updateInteraction_async( 351 browser, 352 this.#activeWindow, 353 this.#userIsIdle, 354 this.#interactions, 355 this._pageViewStartTime, 356 this.store 357 ); 358 } 359 360 /** 361 * Fetches recent interactions for a browser 362 * 363 * @param {Browser} browser 364 * The browser object that we are fetching recent interactions for. 365 */ 366 async getRecentInteractionsForBrowser(browser) { 367 // We need to force update the active interaction's total view time 368 // to get an accurate reading. 369 this.#updateInteraction(); 370 await _Interactions.interactionUpdatePromise; 371 return this.#recentInteractions.get(browser); 372 } 373 374 /** 375 * Removes stale interactions from #recentInteractions that were updated 376 * more than RECENT_BROWSER_INTERACTION_EXPIRY_TIME_MS ago. 377 * 378 * @param {Browser} browser 379 * The browser object that we are pruning stale recent interactions for. 380 */ 381 #pruneOldRecentInteractions(browser) { 382 const now = Date.now(); 383 384 const interactions = this.#recentInteractions.get(browser); 385 if (!interactions) { 386 return; 387 } 388 389 const interactionstoTrack = interactions.filter( 390 interaction => 391 now - interaction.updated_at <= 392 RECENT_BROWSER_INTERACTION_EXPIRY_TIME_MS 393 ); 394 395 if (interactionstoTrack.length) { 396 this.#recentInteractions.set(browser, interactionstoTrack); 397 } else { 398 this.#recentInteractions.delete(browser); 399 } 400 } 401 402 /** 403 * Stores the promise created in updateInteraction_async so that we can await its fulfillment 404 * when sychronization is needed. 405 */ 406 static interactionUpdatePromise = Promise.resolve(); 407 408 /** 409 * Returns the interactions update promise to be used when sychronization is needed from tests. 410 * 411 * @returns {Promise<void>} 412 */ 413 get interactionUpdatePromise() { 414 return _Interactions.interactionUpdatePromise; 415 } 416 417 /** 418 * Updates the current interaction on fulfillment of the asynchronous collection of scrolling interactions. 419 * 420 * @param {Browser} browser 421 * The browser object that has triggered the update, if known. 422 * @param {DOMWindow} activeWindow 423 * The active window. 424 * @param {boolean} userIsIdle 425 * Whether the user is idle. 426 * @param {WeakMap<Browser, InteractionInfo>} interactions 427 * A map of interactions for each browser instance 428 * @param {number} pageViewStartTime 429 * The time the page was loaded. 430 * @param {InteractionsStore} store 431 * The interactions store. 432 */ 433 static async #updateInteraction_async( 434 browser, 435 activeWindow, 436 userIsIdle, 437 interactions, 438 pageViewStartTime, 439 store 440 ) { 441 if (!activeWindow || (browser && browser.ownerGlobal != activeWindow)) { 442 lazy.logConsole.debug( 443 "Not updating interaction as there is no active window" 444 ); 445 return; 446 } 447 448 // We do not update the interaction when the user is idle, since we will 449 // have already updated it when idle was signalled. 450 // Sometimes an interaction may be signalled before idle is cleared, however 451 // worst case we'd only loose approx 2 seconds of interaction detail. 452 if (userIsIdle) { 453 lazy.logConsole.debug("Not updating interaction as the user is idle"); 454 return; 455 } 456 457 if (!browser) { 458 browser = activeWindow.gBrowser.selectedTab.linkedBrowser; 459 } 460 461 let interaction = interactions.get(browser); 462 if (!interaction) { 463 lazy.logConsole.debug("No interaction to update"); 464 return; 465 } 466 467 interaction.totalViewTime += ChromeUtils.now() - pageViewStartTime; 468 Interactions._pageViewStartTime = ChromeUtils.now(); 469 470 const interactionData = ChromeUtils.consumeInteractionData(); 471 const typing = interactionData.Typing; 472 if (typing) { 473 interaction.typingTime += typing.interactionTimeInMilliseconds; 474 interaction.keypresses += typing.interactionCount; 475 } 476 477 // Collect the scrolling data and add the interaction to the store on completion 478 _Interactions.interactionUpdatePromise = 479 _Interactions.interactionUpdatePromise 480 .then(async () => ChromeUtils.collectScrollingData()) 481 .then( 482 result => { 483 interaction.scrollingTime += result.interactionTimeInMilliseconds; 484 interaction.scrollingDistance += result.scrollingDistanceInPixels; 485 }, 486 reason => { 487 console.error(reason); 488 } 489 ) 490 .then(() => { 491 interaction.updated_at = monotonicNow(); 492 493 lazy.logConsole.debug("Add to store: ", interaction); 494 store.add(interaction); 495 }); 496 } 497 498 /** 499 * Handles a window becoming active. 500 * 501 * @param {DOMWindow} win 502 * The window that has become active. 503 */ 504 #onActivateWindow(win) { 505 lazy.logConsole.debug("Window activated"); 506 507 if (lazy.PrivateBrowsingUtils.isWindowPrivate(win)) { 508 return; 509 } 510 511 this.#activeWindow = win; 512 this._pageViewStartTime = ChromeUtils.now(); 513 } 514 515 /** 516 * Handles a window going inactive. 517 */ 518 #onDeactivateWindow() { 519 lazy.logConsole.debug("Window deactivate"); 520 521 this.#updateInteraction(); 522 this.#activeWindow = undefined; 523 } 524 525 /** 526 * Handles the TabSelect notification. If enough time has passed between the 527 * current time and the last time the current tab was selected and interacted 528 * with, the existing interaction will end, and a new one will begin. This 529 * approach accounts for scenarios where a user might leave a tab open for an 530 * extended period (e.g. pinned tabs), and engage in distinct sessions. A 531 * delay is used to prevent the creation of numerous short, separate 532 * interactions that may occur when a user quickly switches between tabs. 533 * 534 * @param {Browser} previousBrowser 535 * The instance of the browser that the user switched away from. 536 */ 537 #onTabSelect(previousBrowser) { 538 lazy.logConsole.debug("Tab switched"); 539 540 this.#updateInteraction(previousBrowser); 541 542 this._pageViewStartTime = ChromeUtils.now(); 543 544 let browser = this.#activeWindow?.gBrowser.selectedBrowser; 545 if (browser && this.#interactions.has(browser)) { 546 let interaction = this.#interactions.get(browser); 547 let timePassedSinceUpdateSeconds = 548 (Date.now() - interaction.updated_at) / 1000; 549 if (timePassedSinceUpdateSeconds >= lazy.breakupIfNoUpdatesForSeconds) { 550 this.registerEndOfInteraction(browser); 551 this.registerNewInteraction(browser, { 552 url: browser.currentURI.spec, 553 referrer: null, 554 isActive: true, 555 }); 556 } 557 } 558 } 559 560 /** 561 * Handles various events and forwards them to appropriate functions. 562 * 563 * @param {DOMEvent} event 564 * The event that will be handled 565 */ 566 handleEvent(event) { 567 switch (event.type) { 568 case "TabSelect": 569 this.#onTabSelect(event.detail.previousTab.linkedBrowser); 570 break; 571 case "activate": 572 this.#onActivateWindow(event.target); 573 break; 574 case "deactivate": 575 this.#onDeactivateWindow(event.target); 576 break; 577 case "unload": 578 this.#unregisterWindow(event.target); 579 break; 580 } 581 } 582 583 /** 584 * Handles notifications from the observer service. 585 * 586 * @param {nsISupports} subject 587 * The subject of the notification. 588 * @param {string} topic 589 * The topic of the notification. 590 */ 591 observe(subject, topic) { 592 switch (topic) { 593 case DOMWINDOW_OPENED_TOPIC: 594 this.#onWindowOpen(subject); 595 break; 596 case "idle": 597 lazy.logConsole.debug("User went idle"); 598 // We save the state of the current interaction when we are notified 599 // that the user is idle. 600 this.#updateInteraction(); 601 this.#userIsIdle = true; 602 break; 603 case "active": 604 lazy.logConsole.debug("User became active"); 605 this.#userIsIdle = false; 606 this._pageViewStartTime = ChromeUtils.now(); 607 break; 608 } 609 } 610 611 /** 612 * Handles registration of listeners in a new window. 613 * 614 * @param {DOMWindow} win 615 * The window to register in. 616 */ 617 #registerWindow(win) { 618 if (lazy.PrivateBrowsingUtils.isWindowPrivate(win)) { 619 return; 620 } 621 622 win.addEventListener("TabSelect", this, true); 623 win.addEventListener("deactivate", this, true); 624 win.addEventListener("activate", this, true); 625 } 626 627 /** 628 * Handles removing of listeners from a window. 629 * 630 * @param {DOMWindow} win 631 * The window to remove listeners from. 632 */ 633 #unregisterWindow(win) { 634 win.removeEventListener("TabSelect", this, true); 635 win.removeEventListener("deactivate", this, true); 636 win.removeEventListener("activate", this, true); 637 } 638 639 /** 640 * Handles a new window being opened, waits for load and checks that 641 * it is a browser window, then adds listeners. 642 * 643 * @param {DOMWindow} win 644 * The window being opened. 645 */ 646 #onWindowOpen(win) { 647 win.addEventListener( 648 "load", 649 () => { 650 if ( 651 win.document.documentElement.getAttribute("windowtype") != 652 "navigator:browser" 653 ) { 654 return; 655 } 656 this.#registerWindow(win); 657 }, 658 { once: true } 659 ); 660 } 661 662 QueryInterface = ChromeUtils.generateQI([ 663 "nsIObserver", 664 "nsISupportsWeakReference", 665 ]); 666 } 667 668 export const Interactions = new _Interactions(); 669 670 /** 671 * Store interactions data in the Places database. 672 * To improve performance the writes are buffered every `saveInterval` 673 * milliseconds. Even if this means we could be trying to write interaction for 674 * pages that in the meanwhile have been removed, that's not a problem because 675 * we won't be able to insert entries having a NULL place_id, they will just be 676 * ignored. 677 * Use .add(interaction) to request storing of an interaction. 678 * Use .pendingPromise to await for any pending writes to have happened. 679 */ 680 class InteractionsStore { 681 /** 682 * Timer to run database updates on. 683 */ 684 #timer = undefined; 685 /** 686 * Tracks interactions replicating the unique index in the underlying schema. 687 * Interactions are keyed by url and then created_at. 688 * 689 * @type {Map<string, Map<number, InteractionInfo>>} 690 */ 691 #interactions = new Map(); 692 /** 693 * Used to unblock the queue of promises when the timer is cleared. 694 */ 695 #timerResolve = undefined; 696 697 constructor() { 698 // Block async shutdown to ensure the last write goes through. 699 this.progress = {}; 700 lazy.PlacesUtils.history.shutdownClient.jsclient.addBlocker( 701 "Interactions.sys.mjs:: store", 702 async () => this.flush(), 703 { fetchState: () => this.progress } 704 ); 705 706 // Can be used to wait for the last pending write to have happened. 707 this.pendingPromise = Promise.resolve(); 708 } 709 710 /** 711 * Synchronizes the pending interactions with the storage device. 712 * 713 * @returns {Promise} resolved when the pending data is on disk. 714 */ 715 async flush() { 716 if (this.#timer) { 717 lazy.clearTimeout(this.#timer); 718 this.#timerResolve(); 719 await this.#updateDatabase(); 720 } 721 } 722 723 /** 724 * Completely clears the store and any pending writes. 725 * This exists for testing purposes. 726 */ 727 async reset() { 728 await lazy.PlacesUtils.withConnectionWrapper( 729 "Interactions.sys.mjs::reset", 730 async db => { 731 await db.executeCached(`DELETE FROM moz_places_metadata`); 732 } 733 ); 734 if (this.#timer) { 735 lazy.clearTimeout(this.#timer); 736 this.#timer = undefined; 737 this.#timerResolve(); 738 this.#interactions.clear(); 739 } 740 } 741 742 /** 743 * Registers an interaction to be stored persistently. At the end of the call 744 * the interaction has not yet been added to the store, tests can await 745 * flushStore() for that. 746 * 747 * @param {InteractionInfo} interaction 748 * The document information to write. 749 */ 750 add(interaction) { 751 lazy.logConsole.debug("Preparing interaction for storage", interaction); 752 753 let interactionsForUrl = this.#interactions.get(interaction.url); 754 if (!interactionsForUrl) { 755 interactionsForUrl = new Map(); 756 this.#interactions.set(interaction.url, interactionsForUrl); 757 } 758 interactionsForUrl.set(interaction.created_at, interaction); 759 760 if (!this.#timer) { 761 let promise = new Promise(resolve => { 762 this.#timerResolve = resolve; 763 this.#timer = lazy.setTimeout(() => { 764 this.#updateDatabase().catch(console.error).then(resolve); 765 }, lazy.saveInterval); 766 }); 767 this.pendingPromise = this.pendingPromise.then(() => promise); 768 } 769 } 770 771 async #updateDatabase() { 772 this.#timer = undefined; 773 774 // Reset the buffer. 775 let interactions = this.#interactions; 776 if (!interactions.size) { 777 return; 778 } 779 // Don't clear() this, since that would also clear interactions. 780 this.#interactions = new Map(); 781 782 let params = {}; 783 let SQLInsertFragments = []; 784 let i = 0; 785 for (let interactionsForUrl of interactions.values()) { 786 for (let interaction of interactionsForUrl.values()) { 787 params[`url${i}`] = interaction.url; 788 params[`referrer${i}`] = interaction.referrer; 789 params[`created_at${i}`] = interaction.created_at; 790 params[`updated_at${i}`] = interaction.updated_at; 791 params[`document_type${i}`] = 792 interaction.documentType ?? Interactions.DOCUMENT_TYPE.GENERIC; 793 params[`total_view_time${i}`] = 794 Math.round(interaction.totalViewTime) || 0; 795 params[`typing_time${i}`] = Math.round(interaction.typingTime) || 0; 796 params[`key_presses${i}`] = interaction.keypresses || 0; 797 params[`scrolling_time${i}`] = 798 Math.round(interaction.scrollingTime) || 0; 799 params[`scrolling_distance${i}`] = 800 Math.round(interaction.scrollingDistance) || 0; 801 SQLInsertFragments.push(`( 802 (SELECT id FROM moz_places_metadata 803 WHERE place_id = (SELECT id FROM moz_places WHERE url_hash = hash(:url${i}) AND url = :url${i}) 804 AND created_at = :created_at${i}), 805 (SELECT id FROM moz_places WHERE url_hash = hash(:url${i}) AND url = :url${i}), 806 (SELECT id FROM moz_places WHERE url_hash = hash(:referrer${i}) AND url = :referrer${i} AND :referrer${i} != :url${i}), 807 :created_at${i}, 808 :updated_at${i}, 809 :document_type${i}, 810 :total_view_time${i}, 811 :typing_time${i}, 812 :key_presses${i}, 813 :scrolling_time${i}, 814 :scrolling_distance${i} 815 )`); 816 i++; 817 } 818 } 819 820 lazy.logConsole.debug(`Storing ${i} entries in the database`); 821 822 this.progress.pendingUpdates = i; 823 await lazy.PlacesUtils.withConnectionWrapper( 824 "Interactions.sys.mjs::updateDatabase", 825 async db => { 826 await db.executeCached( 827 ` 828 WITH inserts (id, place_id, referrer_place_id, created_at, updated_at, document_type, total_view_time, typing_time, key_presses, scrolling_time, scrolling_distance) AS ( 829 VALUES ${SQLInsertFragments.join(", ")} 830 ) 831 INSERT OR REPLACE INTO moz_places_metadata ( 832 id, place_id, referrer_place_id, created_at, updated_at, document_type, total_view_time, typing_time, key_presses, scrolling_time, scrolling_distance 833 ) SELECT * FROM inserts WHERE place_id NOT NULL; 834 `, 835 params 836 ); 837 } 838 ); 839 this.progress.pendingUpdates = 0; 840 841 Services.obs.notifyObservers(null, "places-metadata-updated"); 842 } 843 }