mirror of
				https://github.com/mozilla/gecko-dev.git
				synced 2025-11-04 02:09:05 +02:00 
			
		
		
		
	
		
			
				
	
	
		
			785 lines
		
	
	
	
		
			23 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			785 lines
		
	
	
	
		
			23 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/* This Source Code Form is subject to the terms of the Mozilla Public
 | 
						|
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 | 
						|
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 | 
						|
 | 
						|
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
 | 
						|
 | 
						|
const lazy = {};
 | 
						|
 | 
						|
ChromeUtils.defineESModuleGetters(lazy, {
 | 
						|
  BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
 | 
						|
  InteractionsBlocklist:
 | 
						|
    "moz-src:///browser/components/places/InteractionsBlocklist.sys.mjs",
 | 
						|
  PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
 | 
						|
  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
 | 
						|
  clearTimeout: "resource://gre/modules/Timer.sys.mjs",
 | 
						|
  setTimeout: "resource://gre/modules/Timer.sys.mjs",
 | 
						|
});
 | 
						|
 | 
						|
ChromeUtils.defineLazyGetter(lazy, "logConsole", function () {
 | 
						|
  return console.createInstance({
 | 
						|
    prefix: "InteractionsManager",
 | 
						|
    maxLogLevel: Services.prefs.getBoolPref(
 | 
						|
      "browser.places.interactions.log",
 | 
						|
      false
 | 
						|
    )
 | 
						|
      ? "Debug"
 | 
						|
      : "Warn",
 | 
						|
  });
 | 
						|
});
 | 
						|
 | 
						|
XPCOMUtils.defineLazyServiceGetters(lazy, {
 | 
						|
  idleService: ["@mozilla.org/widget/useridleservice;1", "nsIUserIdleService"],
 | 
						|
});
 | 
						|
 | 
						|
XPCOMUtils.defineLazyPreferenceGetter(
 | 
						|
  lazy,
 | 
						|
  "pageViewIdleTime",
 | 
						|
  "browser.places.interactions.pageViewIdleTime",
 | 
						|
  60
 | 
						|
);
 | 
						|
 | 
						|
XPCOMUtils.defineLazyPreferenceGetter(
 | 
						|
  lazy,
 | 
						|
  "saveInterval",
 | 
						|
  "browser.places.interactions.saveInterval",
 | 
						|
  10000
 | 
						|
);
 | 
						|
 | 
						|
XPCOMUtils.defineLazyPreferenceGetter(
 | 
						|
  lazy,
 | 
						|
  "isHistoryEnabled",
 | 
						|
  "places.history.enabled",
 | 
						|
  false
 | 
						|
);
 | 
						|
 | 
						|
XPCOMUtils.defineLazyPreferenceGetter(
 | 
						|
  lazy,
 | 
						|
  "breakupIfNoUpdatesForSeconds",
 | 
						|
  "browser.places.interactions.breakupIfNoUpdatesForSeconds",
 | 
						|
  60 * 60
 | 
						|
);
 | 
						|
 | 
						|
const DOMWINDOW_OPENED_TOPIC = "domwindowopened";
 | 
						|
 | 
						|
/**
 | 
						|
 * Returns a monotonically increasing timestamp, that is critical to distinguish
 | 
						|
 * database entries by creation time.
 | 
						|
 */
 | 
						|
let gLastTime = 0;
 | 
						|
function monotonicNow() {
 | 
						|
  let time = Date.now();
 | 
						|
  if (time == gLastTime) {
 | 
						|
    time++;
 | 
						|
  }
 | 
						|
  return (gLastTime = time);
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * @typedef {object} DocumentInfo
 | 
						|
 *   DocumentInfo is used to pass document information from the child process
 | 
						|
 *   to _Interactions.
 | 
						|
 * @property {boolean} isActive
 | 
						|
 *   Set to true if the document is active, i.e. visible.
 | 
						|
 * @property {string} url
 | 
						|
 *   The url of the page that was interacted with.
 | 
						|
 */
 | 
						|
 | 
						|
/**
 | 
						|
 * @typedef {object} InteractionInfo
 | 
						|
 *   InteractionInfo is used to store information associated with interactions.
 | 
						|
 * @property {number} totalViewTime
 | 
						|
 *   Time in milliseconds that the page has been actively viewed for.
 | 
						|
 * @property {string} url
 | 
						|
 *   The url of the page that was interacted with.
 | 
						|
 * @property {Interactions.DOCUMENT_TYPE} documentType
 | 
						|
 *   The type of the document.
 | 
						|
 * @property {number} typingTime
 | 
						|
 *   Time in milliseconds that the user typed on the page
 | 
						|
 * @property {number} keypresses
 | 
						|
 *   The number of keypresses made on the page
 | 
						|
 * @property {number} scrollingTime
 | 
						|
 *   Time in milliseconds that the user spent scrolling the page
 | 
						|
 * @property {number} scrollingDistance
 | 
						|
 *   The distance, in pixels, that the user scrolled the page
 | 
						|
 * @property {number} created_at
 | 
						|
 *   Creation time as the number of milliseconds since the epoch.
 | 
						|
 * @property {number} updated_at
 | 
						|
 *   Last updated time as the number of milliseconds since the epoch.
 | 
						|
 * @property {string} referrer
 | 
						|
 *   The referrer to the url of the page that was interacted with (may be empty)
 | 
						|
 */
 | 
						|
 | 
						|
/**
 | 
						|
 * The Interactions object sets up listeners and other approriate tools for
 | 
						|
 * obtaining interaction information and passing it to the InteractionsManager.
 | 
						|
 */
 | 
						|
class _Interactions {
 | 
						|
  DOCUMENT_TYPE = {
 | 
						|
    // Used when the document type is unknown.
 | 
						|
    GENERIC: 0,
 | 
						|
    // Used for pages serving media, e.g. videos.
 | 
						|
    MEDIA: 1,
 | 
						|
  };
 | 
						|
 | 
						|
  /**
 | 
						|
   * This is used to store potential interactions. It maps the browser
 | 
						|
   * to the current interaction information.
 | 
						|
   * The current interaction is updated to the database when it transitions
 | 
						|
   * to non-active, which occurs before a browser tab is closed, hence this
 | 
						|
   * can be a weak map.
 | 
						|
   *
 | 
						|
   * @type {WeakMap<browser, InteractionInfo>}
 | 
						|
   */
 | 
						|
  #interactions = new WeakMap();
 | 
						|
 | 
						|
  /**
 | 
						|
   * Tracks the currently active window so that we can avoid recording
 | 
						|
   * interactions in non-active windows.
 | 
						|
   *
 | 
						|
   * @type {DOMWindow}
 | 
						|
   */
 | 
						|
  #activeWindow = undefined;
 | 
						|
 | 
						|
  /**
 | 
						|
   * Tracks if the user is idle.
 | 
						|
   *
 | 
						|
   * @type {boolean}
 | 
						|
   */
 | 
						|
  #userIsIdle = false;
 | 
						|
 | 
						|
  /**
 | 
						|
   * This stores the page view start time of the current page view.
 | 
						|
   * For any single page view, this may be moved multiple times as the
 | 
						|
   * associated interaction is updated for the current total page view time.
 | 
						|
   *
 | 
						|
   * @type {number}
 | 
						|
   */
 | 
						|
  _pageViewStartTime = Cu.now();
 | 
						|
 | 
						|
  /**
 | 
						|
   * Stores interactions in the database, see the {@link InteractionsStore}
 | 
						|
   * class. This is created lazily, see the `store` getter.
 | 
						|
   *
 | 
						|
   * @type {InteractionsStore | undefined}
 | 
						|
   */
 | 
						|
  #store = undefined;
 | 
						|
 | 
						|
  /**
 | 
						|
   * Whether the component has been initialized.
 | 
						|
   */
 | 
						|
  #initialized = false;
 | 
						|
 | 
						|
  /**
 | 
						|
   * Initializes, sets up actors and observers.
 | 
						|
   */
 | 
						|
  init() {
 | 
						|
    if (
 | 
						|
      !Services.prefs.getBoolPref("browser.places.interactions.enabled", false)
 | 
						|
    ) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    ChromeUtils.registerWindowActor("Interactions", {
 | 
						|
      parent: {
 | 
						|
        esModuleURI: "resource:///actors/InteractionsParent.sys.mjs",
 | 
						|
      },
 | 
						|
      child: {
 | 
						|
        esModuleURI: "resource:///actors/InteractionsChild.sys.mjs",
 | 
						|
        events: {
 | 
						|
          DOMContentLoaded: {},
 | 
						|
          pagehide: { mozSystemGroup: true },
 | 
						|
        },
 | 
						|
      },
 | 
						|
      messageManagerGroups: ["browsers"],
 | 
						|
    });
 | 
						|
 | 
						|
    this.#activeWindow = Services.wm.getMostRecentBrowserWindow();
 | 
						|
 | 
						|
    for (let win of lazy.BrowserWindowTracker.orderedWindows) {
 | 
						|
      if (!win.closed) {
 | 
						|
        this.#registerWindow(win);
 | 
						|
      }
 | 
						|
    }
 | 
						|
    Services.obs.addObserver(this, DOMWINDOW_OPENED_TOPIC, true);
 | 
						|
    lazy.idleService.addIdleObserver(this, lazy.pageViewIdleTime);
 | 
						|
 | 
						|
    this.#initialized = true;
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Uninitializes, removes any observers that need cleaning up manually.
 | 
						|
   */
 | 
						|
  uninit() {
 | 
						|
    if (this.#initialized) {
 | 
						|
      lazy.idleService.removeIdleObserver(this, lazy.pageViewIdleTime);
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Resets any stored user or interaction state.
 | 
						|
   * Used by tests.
 | 
						|
   */
 | 
						|
  async reset() {
 | 
						|
    lazy.logConsole.debug("Database reset");
 | 
						|
    this.#interactions = new WeakMap();
 | 
						|
    this.#userIsIdle = false;
 | 
						|
    this._pageViewStartTime = Cu.now();
 | 
						|
    ChromeUtils.consumeInteractionData();
 | 
						|
    await _Interactions.interactionUpdatePromise;
 | 
						|
    await this.store.reset();
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Retrieve the underlying InteractionsStore object. This exists for testing
 | 
						|
   * purposes and should not be abused by production code (for example it'd be
 | 
						|
   * a bad idea to force flushes).
 | 
						|
   *
 | 
						|
   * @returns {InteractionsStore}
 | 
						|
   */
 | 
						|
  get store() {
 | 
						|
    if (!this.#store) {
 | 
						|
      this.#store = new InteractionsStore();
 | 
						|
    }
 | 
						|
    return this.#store;
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Registers the start of a new interaction.
 | 
						|
   *
 | 
						|
   * @param {Browser} browser
 | 
						|
   *   The browser object associated with the interaction.
 | 
						|
   * @param {DocumentInfo} docInfo
 | 
						|
   *   The document information of the page associated with the interaction.
 | 
						|
   */
 | 
						|
  registerNewInteraction(browser, docInfo) {
 | 
						|
    if (
 | 
						|
      !browser ||
 | 
						|
      !lazy.isHistoryEnabled ||
 | 
						|
      !browser.browsingContext.useGlobalHistory
 | 
						|
    ) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    let interaction = this.#interactions.get(browser);
 | 
						|
    if (interaction && interaction.url != docInfo.url) {
 | 
						|
      this.registerEndOfInteraction(browser);
 | 
						|
    }
 | 
						|
 | 
						|
    if (lazy.InteractionsBlocklist.isUrlBlocklisted(docInfo.url)) {
 | 
						|
      lazy.logConsole.debug(
 | 
						|
        "Ignoring a page as the URL is blocklisted",
 | 
						|
        docInfo
 | 
						|
      );
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    lazy.logConsole.debug("Tracking a new interaction", docInfo);
 | 
						|
    let now = monotonicNow();
 | 
						|
    interaction = {
 | 
						|
      url: docInfo.url,
 | 
						|
      referrer: docInfo.referrer,
 | 
						|
      totalViewTime: 0,
 | 
						|
      typingTime: 0,
 | 
						|
      keypresses: 0,
 | 
						|
      scrollingTime: 0,
 | 
						|
      scrollingDistance: 0,
 | 
						|
      created_at: now,
 | 
						|
      updated_at: now,
 | 
						|
    };
 | 
						|
    this.#interactions.set(browser, interaction);
 | 
						|
 | 
						|
    // Only reset the time if this is being loaded in the active tab of the
 | 
						|
    // active window.
 | 
						|
    if (docInfo.isActive && browser.ownerGlobal == this.#activeWindow) {
 | 
						|
      this._pageViewStartTime = Cu.now();
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Registers the end of an interaction, e.g. if the user navigates away
 | 
						|
   * from the page. This will store the final interaction details and clear
 | 
						|
   * the current interaction.
 | 
						|
   *
 | 
						|
   * @param {Browser} browser
 | 
						|
   *   The browser object associated with the interaction.
 | 
						|
   */
 | 
						|
  registerEndOfInteraction(browser) {
 | 
						|
    // Not having a browser passed to us probably means the tab has gone away
 | 
						|
    // before we received the notification - due to the tab being a background
 | 
						|
    // tab. Since that will be a non-active tab, it is acceptable that we don't
 | 
						|
    // update the interaction. When switching away from active tabs, a TabSelect
 | 
						|
    // notification is generated which we handle elsewhere.
 | 
						|
    if (
 | 
						|
      !browser ||
 | 
						|
      !lazy.isHistoryEnabled ||
 | 
						|
      !browser.browsingContext.useGlobalHistory
 | 
						|
    ) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    lazy.logConsole.debug("Saw the end of an interaction");
 | 
						|
 | 
						|
    this.#updateInteraction(browser);
 | 
						|
    this.#interactions.delete(browser);
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Updates the current interaction
 | 
						|
   *
 | 
						|
   * @param {Browser} [browser]
 | 
						|
   *   The browser object that has triggered the update, if known. This is
 | 
						|
   *   used to check if the browser is in the active window, and as an
 | 
						|
   *   optimization to avoid obtaining the browser object.
 | 
						|
   */
 | 
						|
  #updateInteraction(browser = undefined) {
 | 
						|
    _Interactions.#updateInteraction_async(
 | 
						|
      browser,
 | 
						|
      this.#activeWindow,
 | 
						|
      this.#userIsIdle,
 | 
						|
      this.#interactions,
 | 
						|
      this._pageViewStartTime,
 | 
						|
      this.store
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Stores the promise created in updateInteraction_async so that we can await its fulfillment
 | 
						|
   * when sychronization is needed.
 | 
						|
   */
 | 
						|
  static interactionUpdatePromise = Promise.resolve();
 | 
						|
 | 
						|
  /**
 | 
						|
   * Returns the interactions update promise to be used when sychronization is needed from tests.
 | 
						|
   *
 | 
						|
   * @returns {Promise<void>}
 | 
						|
   */
 | 
						|
  get interactionUpdatePromise() {
 | 
						|
    return _Interactions.interactionUpdatePromise;
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Updates the current interaction on fulfillment of the asynchronous collection of scrolling interactions.
 | 
						|
   *
 | 
						|
   * @param {Browser} browser
 | 
						|
   *   The browser object that has triggered the update, if known.
 | 
						|
   * @param {DOMWindow} activeWindow
 | 
						|
   *   The active window.
 | 
						|
   * @param {boolean} userIsIdle
 | 
						|
   *   Whether the user is idle.
 | 
						|
   * @param {WeakMap<Browser, InteractionInfo>} interactions
 | 
						|
   *   A map of interactions for each browser instance
 | 
						|
   * @param {number} pageViewStartTime
 | 
						|
   *   The time the page was loaded.
 | 
						|
   * @param {InteractionsStore} store
 | 
						|
   *   The interactions store.
 | 
						|
   */
 | 
						|
  static async #updateInteraction_async(
 | 
						|
    browser,
 | 
						|
    activeWindow,
 | 
						|
    userIsIdle,
 | 
						|
    interactions,
 | 
						|
    pageViewStartTime,
 | 
						|
    store
 | 
						|
  ) {
 | 
						|
    if (!activeWindow || (browser && browser.ownerGlobal != activeWindow)) {
 | 
						|
      lazy.logConsole.debug(
 | 
						|
        "Not updating interaction as there is no active window"
 | 
						|
      );
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    // We do not update the interaction when the user is idle, since we will
 | 
						|
    // have already updated it when idle was signalled.
 | 
						|
    // Sometimes an interaction may be signalled before idle is cleared, however
 | 
						|
    // worst case we'd only loose approx 2 seconds of interaction detail.
 | 
						|
    if (userIsIdle) {
 | 
						|
      lazy.logConsole.debug("Not updating interaction as the user is idle");
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    if (!browser) {
 | 
						|
      browser = activeWindow.gBrowser.selectedTab.linkedBrowser;
 | 
						|
    }
 | 
						|
 | 
						|
    let interaction = interactions.get(browser);
 | 
						|
    if (!interaction) {
 | 
						|
      lazy.logConsole.debug("No interaction to update");
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    interaction.totalViewTime += Cu.now() - pageViewStartTime;
 | 
						|
    Interactions._pageViewStartTime = Cu.now();
 | 
						|
 | 
						|
    const interactionData = ChromeUtils.consumeInteractionData();
 | 
						|
    const typing = interactionData.Typing;
 | 
						|
    if (typing) {
 | 
						|
      interaction.typingTime += typing.interactionTimeInMilliseconds;
 | 
						|
      interaction.keypresses += typing.interactionCount;
 | 
						|
    }
 | 
						|
 | 
						|
    // Collect the scrolling data and add the interaction to the store on completion
 | 
						|
    _Interactions.interactionUpdatePromise =
 | 
						|
      _Interactions.interactionUpdatePromise
 | 
						|
        .then(async () => ChromeUtils.collectScrollingData())
 | 
						|
        .then(
 | 
						|
          result => {
 | 
						|
            interaction.scrollingTime += result.interactionTimeInMilliseconds;
 | 
						|
            interaction.scrollingDistance += result.scrollingDistanceInPixels;
 | 
						|
          },
 | 
						|
          reason => {
 | 
						|
            console.error(reason);
 | 
						|
          }
 | 
						|
        )
 | 
						|
        .then(() => {
 | 
						|
          interaction.updated_at = monotonicNow();
 | 
						|
 | 
						|
          lazy.logConsole.debug("Add to store: ", interaction);
 | 
						|
          store.add(interaction);
 | 
						|
        });
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Handles a window becoming active.
 | 
						|
   *
 | 
						|
   * @param {DOMWindow} win
 | 
						|
   *   The window that has become active.
 | 
						|
   */
 | 
						|
  #onActivateWindow(win) {
 | 
						|
    lazy.logConsole.debug("Window activated");
 | 
						|
 | 
						|
    if (lazy.PrivateBrowsingUtils.isWindowPrivate(win)) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    this.#activeWindow = win;
 | 
						|
    this._pageViewStartTime = Cu.now();
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Handles a window going inactive.
 | 
						|
   */
 | 
						|
  #onDeactivateWindow() {
 | 
						|
    lazy.logConsole.debug("Window deactivate");
 | 
						|
 | 
						|
    this.#updateInteraction();
 | 
						|
    this.#activeWindow = undefined;
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Handles the TabSelect notification. If enough time has passed between the
 | 
						|
   * current time and the last time the current tab was selected and interacted
 | 
						|
   * with, the existing interaction will end, and a new one will begin. This
 | 
						|
   * approach accounts for scenarios where a user might leave a tab open for an
 | 
						|
   * extended period (e.g. pinned tabs), and engage in distinct sessions. A
 | 
						|
   * delay is used to prevent the creation of numerous short, separate
 | 
						|
   * interactions that may occur when a user quickly switches between tabs.
 | 
						|
   *
 | 
						|
   * @param {Browser} previousBrowser
 | 
						|
   *   The instance of the browser that the user switched away from.
 | 
						|
   */
 | 
						|
  #onTabSelect(previousBrowser) {
 | 
						|
    lazy.logConsole.debug("Tab switched");
 | 
						|
 | 
						|
    this.#updateInteraction(previousBrowser);
 | 
						|
 | 
						|
    this._pageViewStartTime = Cu.now();
 | 
						|
 | 
						|
    let browser = this.#activeWindow?.gBrowser.selectedBrowser;
 | 
						|
    if (browser && this.#interactions.has(browser)) {
 | 
						|
      let interaction = this.#interactions.get(browser);
 | 
						|
      let timePassedSinceUpdateSeconds =
 | 
						|
        (Date.now() - interaction.updated_at) / 1000;
 | 
						|
      if (timePassedSinceUpdateSeconds >= lazy.breakupIfNoUpdatesForSeconds) {
 | 
						|
        this.registerEndOfInteraction(browser);
 | 
						|
        this.registerNewInteraction(browser, {
 | 
						|
          url: browser.currentURI.spec,
 | 
						|
          referrer: null,
 | 
						|
          isActive: true,
 | 
						|
        });
 | 
						|
      }
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Handles various events and forwards them to appropriate functions.
 | 
						|
   *
 | 
						|
   * @param {DOMEvent} event
 | 
						|
   *   The event that will be handled
 | 
						|
   */
 | 
						|
  handleEvent(event) {
 | 
						|
    switch (event.type) {
 | 
						|
      case "TabSelect":
 | 
						|
        this.#onTabSelect(event.detail.previousTab.linkedBrowser);
 | 
						|
        break;
 | 
						|
      case "activate":
 | 
						|
        this.#onActivateWindow(event.target);
 | 
						|
        break;
 | 
						|
      case "deactivate":
 | 
						|
        this.#onDeactivateWindow(event.target);
 | 
						|
        break;
 | 
						|
      case "unload":
 | 
						|
        this.#unregisterWindow(event.target);
 | 
						|
        break;
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Handles notifications from the observer service.
 | 
						|
   *
 | 
						|
   * @param {nsISupports} subject
 | 
						|
   *   The subject of the notification.
 | 
						|
   * @param {string} topic
 | 
						|
   *   The topic of the notification.
 | 
						|
   */
 | 
						|
  observe(subject, topic) {
 | 
						|
    switch (topic) {
 | 
						|
      case DOMWINDOW_OPENED_TOPIC:
 | 
						|
        this.#onWindowOpen(subject);
 | 
						|
        break;
 | 
						|
      case "idle":
 | 
						|
        lazy.logConsole.debug("User went idle");
 | 
						|
        // We save the state of the current interaction when we are notified
 | 
						|
        // that the user is idle.
 | 
						|
        this.#updateInteraction();
 | 
						|
        this.#userIsIdle = true;
 | 
						|
        break;
 | 
						|
      case "active":
 | 
						|
        lazy.logConsole.debug("User became active");
 | 
						|
        this.#userIsIdle = false;
 | 
						|
        this._pageViewStartTime = Cu.now();
 | 
						|
        break;
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Handles registration of listeners in a new window.
 | 
						|
   *
 | 
						|
   * @param {DOMWindow} win
 | 
						|
   *   The window to register in.
 | 
						|
   */
 | 
						|
  #registerWindow(win) {
 | 
						|
    if (lazy.PrivateBrowsingUtils.isWindowPrivate(win)) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    win.addEventListener("TabSelect", this, true);
 | 
						|
    win.addEventListener("deactivate", this, true);
 | 
						|
    win.addEventListener("activate", this, true);
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Handles removing of listeners from a window.
 | 
						|
   *
 | 
						|
   * @param {DOMWindow} win
 | 
						|
   *   The window to remove listeners from.
 | 
						|
   */
 | 
						|
  #unregisterWindow(win) {
 | 
						|
    win.removeEventListener("TabSelect", this, true);
 | 
						|
    win.removeEventListener("deactivate", this, true);
 | 
						|
    win.removeEventListener("activate", this, true);
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Handles a new window being opened, waits for load and checks that
 | 
						|
   * it is a browser window, then adds listeners.
 | 
						|
   *
 | 
						|
   * @param {DOMWindow} win
 | 
						|
   *   The window being opened.
 | 
						|
   */
 | 
						|
  #onWindowOpen(win) {
 | 
						|
    win.addEventListener(
 | 
						|
      "load",
 | 
						|
      () => {
 | 
						|
        if (
 | 
						|
          win.document.documentElement.getAttribute("windowtype") !=
 | 
						|
          "navigator:browser"
 | 
						|
        ) {
 | 
						|
          return;
 | 
						|
        }
 | 
						|
        this.#registerWindow(win);
 | 
						|
      },
 | 
						|
      { once: true }
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  QueryInterface = ChromeUtils.generateQI([
 | 
						|
    "nsIObserver",
 | 
						|
    "nsISupportsWeakReference",
 | 
						|
  ]);
 | 
						|
}
 | 
						|
 | 
						|
export const Interactions = new _Interactions();
 | 
						|
 | 
						|
/**
 | 
						|
 * Store interactions data in the Places database.
 | 
						|
 * To improve performance the writes are buffered every `saveInterval`
 | 
						|
 * milliseconds. Even if this means we could be trying to write interaction for
 | 
						|
 * pages that in the meanwhile have been removed, that's not a problem because
 | 
						|
 * we won't be able to insert entries having a NULL place_id, they will just be
 | 
						|
 * ignored.
 | 
						|
 * Use .add(interaction) to request storing of an interaction.
 | 
						|
 * Use .pendingPromise to await for any pending writes to have happened.
 | 
						|
 */
 | 
						|
class InteractionsStore {
 | 
						|
  /**
 | 
						|
   * Timer to run database updates on.
 | 
						|
   */
 | 
						|
  #timer = undefined;
 | 
						|
  /**
 | 
						|
   * Tracks interactions replicating the unique index in the underlying schema.
 | 
						|
   * Interactions are keyed by url and then created_at.
 | 
						|
   *
 | 
						|
   * @type {Map<string, Map<number, InteractionInfo>>}
 | 
						|
   */
 | 
						|
  #interactions = new Map();
 | 
						|
  /**
 | 
						|
   * Used to unblock the queue of promises when the timer is cleared.
 | 
						|
   */
 | 
						|
  #timerResolve = undefined;
 | 
						|
 | 
						|
  constructor() {
 | 
						|
    // Block async shutdown to ensure the last write goes through.
 | 
						|
    this.progress = {};
 | 
						|
    lazy.PlacesUtils.history.shutdownClient.jsclient.addBlocker(
 | 
						|
      "Interactions.sys.mjs:: store",
 | 
						|
      async () => this.flush(),
 | 
						|
      { fetchState: () => this.progress }
 | 
						|
    );
 | 
						|
 | 
						|
    // Can be used to wait for the last pending write to have happened.
 | 
						|
    this.pendingPromise = Promise.resolve();
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Synchronizes the pending interactions with the storage device.
 | 
						|
   *
 | 
						|
   * @returns {Promise} resolved when the pending data is on disk.
 | 
						|
   */
 | 
						|
  async flush() {
 | 
						|
    if (this.#timer) {
 | 
						|
      lazy.clearTimeout(this.#timer);
 | 
						|
      this.#timerResolve();
 | 
						|
      await this.#updateDatabase();
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Completely clears the store and any pending writes.
 | 
						|
   * This exists for testing purposes.
 | 
						|
   */
 | 
						|
  async reset() {
 | 
						|
    await lazy.PlacesUtils.withConnectionWrapper(
 | 
						|
      "Interactions.sys.mjs::reset",
 | 
						|
      async db => {
 | 
						|
        await db.executeCached(`DELETE FROM moz_places_metadata`);
 | 
						|
      }
 | 
						|
    );
 | 
						|
    if (this.#timer) {
 | 
						|
      lazy.clearTimeout(this.#timer);
 | 
						|
      this.#timer = undefined;
 | 
						|
      this.#timerResolve();
 | 
						|
      this.#interactions.clear();
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Registers an interaction to be stored persistently. At the end of the call
 | 
						|
   * the interaction has not yet been added to the store, tests can await
 | 
						|
   * flushStore() for that.
 | 
						|
   *
 | 
						|
   * @param {InteractionInfo} interaction
 | 
						|
   *   The document information to write.
 | 
						|
   */
 | 
						|
  add(interaction) {
 | 
						|
    lazy.logConsole.debug("Preparing interaction for storage", interaction);
 | 
						|
 | 
						|
    let interactionsForUrl = this.#interactions.get(interaction.url);
 | 
						|
    if (!interactionsForUrl) {
 | 
						|
      interactionsForUrl = new Map();
 | 
						|
      this.#interactions.set(interaction.url, interactionsForUrl);
 | 
						|
    }
 | 
						|
    interactionsForUrl.set(interaction.created_at, interaction);
 | 
						|
 | 
						|
    if (!this.#timer) {
 | 
						|
      let promise = new Promise(resolve => {
 | 
						|
        this.#timerResolve = resolve;
 | 
						|
        this.#timer = lazy.setTimeout(() => {
 | 
						|
          this.#updateDatabase().catch(console.error).then(resolve);
 | 
						|
        }, lazy.saveInterval);
 | 
						|
      });
 | 
						|
      this.pendingPromise = this.pendingPromise.then(() => promise);
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  async #updateDatabase() {
 | 
						|
    this.#timer = undefined;
 | 
						|
 | 
						|
    // Reset the buffer.
 | 
						|
    let interactions = this.#interactions;
 | 
						|
    if (!interactions.size) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    // Don't clear() this, since that would also clear interactions.
 | 
						|
    this.#interactions = new Map();
 | 
						|
 | 
						|
    let params = {};
 | 
						|
    let SQLInsertFragments = [];
 | 
						|
    let i = 0;
 | 
						|
    for (let interactionsForUrl of interactions.values()) {
 | 
						|
      for (let interaction of interactionsForUrl.values()) {
 | 
						|
        params[`url${i}`] = interaction.url;
 | 
						|
        params[`referrer${i}`] = interaction.referrer;
 | 
						|
        params[`created_at${i}`] = interaction.created_at;
 | 
						|
        params[`updated_at${i}`] = interaction.updated_at;
 | 
						|
        params[`document_type${i}`] =
 | 
						|
          interaction.documentType ?? Interactions.DOCUMENT_TYPE.GENERIC;
 | 
						|
        params[`total_view_time${i}`] =
 | 
						|
          Math.round(interaction.totalViewTime) || 0;
 | 
						|
        params[`typing_time${i}`] = Math.round(interaction.typingTime) || 0;
 | 
						|
        params[`key_presses${i}`] = interaction.keypresses || 0;
 | 
						|
        params[`scrolling_time${i}`] =
 | 
						|
          Math.round(interaction.scrollingTime) || 0;
 | 
						|
        params[`scrolling_distance${i}`] =
 | 
						|
          Math.round(interaction.scrollingDistance) || 0;
 | 
						|
        SQLInsertFragments.push(`(
 | 
						|
          (SELECT id FROM moz_places_metadata
 | 
						|
            WHERE place_id = (SELECT id FROM moz_places WHERE url_hash = hash(:url${i}) AND url = :url${i})
 | 
						|
              AND created_at = :created_at${i}),
 | 
						|
          (SELECT id FROM moz_places WHERE url_hash = hash(:url${i}) AND url = :url${i}),
 | 
						|
          (SELECT id FROM moz_places WHERE url_hash = hash(:referrer${i}) AND url = :referrer${i} AND :referrer${i} != :url${i}),
 | 
						|
          :created_at${i},
 | 
						|
          :updated_at${i},
 | 
						|
          :document_type${i},
 | 
						|
          :total_view_time${i},
 | 
						|
          :typing_time${i},
 | 
						|
          :key_presses${i},
 | 
						|
          :scrolling_time${i},
 | 
						|
          :scrolling_distance${i}
 | 
						|
        )`);
 | 
						|
        i++;
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    lazy.logConsole.debug(`Storing ${i} entries in the database`);
 | 
						|
 | 
						|
    this.progress.pendingUpdates = i;
 | 
						|
    await lazy.PlacesUtils.withConnectionWrapper(
 | 
						|
      "Interactions.sys.mjs::updateDatabase",
 | 
						|
      async db => {
 | 
						|
        await db.executeCached(
 | 
						|
          `
 | 
						|
          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 (
 | 
						|
            VALUES ${SQLInsertFragments.join(", ")}
 | 
						|
          )
 | 
						|
          INSERT OR REPLACE INTO moz_places_metadata (
 | 
						|
            id, place_id, referrer_place_id, created_at, updated_at, document_type, total_view_time, typing_time, key_presses, scrolling_time, scrolling_distance
 | 
						|
          ) SELECT * FROM inserts WHERE place_id NOT NULL;
 | 
						|
        `,
 | 
						|
          params
 | 
						|
        );
 | 
						|
      }
 | 
						|
    );
 | 
						|
    this.progress.pendingUpdates = 0;
 | 
						|
 | 
						|
    Services.obs.notifyObservers(null, "places-metadata-updated");
 | 
						|
  }
 | 
						|
}
 |