Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 


Quelle  Interactions.sys.mjs   Sprache: unbekannt

 
/* 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: "resource:///modules/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
);

export const TIMERS = {
  tabSelectIdleTimeMs: 1000 * 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 timePassedSinceUpdate = Date.now() - interaction.updated_at;
      if (timePassedSinceUpdate >= TIMERS.tabSelectIdleTimeMs) {
        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");
  }
}

[ Dauer der Verarbeitung: 0.37 Sekunden  (vorverarbeitet)  ]

                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Software

     Produkte
     Quellcodebibliothek

Aktivitäten

     Artikel über Sicherheit
     Anleitung zur Aktivierung von SSL

Muße

     Gedichte
     Musik
     Bilder

Jenseits des Üblichen ....
    

Besucherstatistik

Besucherstatistik

Monitoring

Montastic status badge