tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

SmartShortcutsFeed.sys.mjs (3542B)


      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 const lazy = {};
      6 ChromeUtils.defineESModuleGetters(lazy, {
      7  PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
      8 });
      9 
     10 import { actionTypes as at } from "resource://newtab/common/Actions.mjs";
     11 
     12 const PREF_SYSTEM_SHORTCUTS_PERSONALIZATION =
     13  "discoverystream.shortcuts.personalization.enabled";
     14 
     15 const PREF_SYSTEM_SHORTCUTS_LOG = "discoverystream.shortcuts.force_log.enabled";
     16 
     17 function timeMSToSeconds(timeMS) {
     18  return Math.round(timeMS / 1000);
     19 }
     20 
     21 /**
     22 * A feature that periodically generates an interest vector for personalized shortcuts.
     23 */
     24 export class SmartShortcutsFeed {
     25  constructor() {
     26    this.loaded = false;
     27  }
     28 
     29  isEnabled() {
     30    const { values } = this.store.getState().Prefs;
     31    const systemPref = values[PREF_SYSTEM_SHORTCUTS_PERSONALIZATION];
     32    const experimentVariable = values.trainhopConfig?.smartShortcuts?.enabled;
     33    const systemLogPref = values[PREF_SYSTEM_SHORTCUTS_LOG];
     34    const experimentLogPref = values.trainhopConfig?.smartShortcuts?.force_log;
     35 
     36    return (
     37      systemPref || experimentVariable || systemLogPref || experimentLogPref
     38    );
     39  }
     40 
     41  async init() {
     42    if (!this.isEnabled()) {
     43      return;
     44    }
     45    this.loaded = true;
     46  }
     47 
     48  async reset() {
     49    this.loaded = false;
     50  }
     51 
     52  async recordShortcutsInteraction(event_type, data) {
     53    // We don't need to worry about interactions that don't have a guid.
     54    if (!data.guid) {
     55      return;
     56    }
     57    const insertValues = {
     58      guid: data.guid,
     59      event_type,
     60      timestamp_s: timeMSToSeconds(this.Date().now()),
     61      pinned: data.isPinned ? 1 : 0,
     62      tile_position: data.position,
     63    };
     64 
     65    let sql = `
     66      INSERT INTO moz_newtab_shortcuts_interaction (
     67        place_id, event_type, timestamp_s, pinned, tile_position
     68      )
     69      SELECT
     70        id, :event_type, :timestamp_s, :pinned, :tile_position
     71      FROM moz_places
     72      WHERE guid = :guid
     73    `;
     74 
     75    await lazy.PlacesUtils.withConnectionWrapper(
     76      "newtab/lib/SmartShortcutsFeed.sys.mjs: recordShortcutsInteraction",
     77      async db => {
     78        await db.execute(sql, insertValues);
     79      }
     80    );
     81  }
     82 
     83  async handleTopSitesOrganicImpressionStats(action) {
     84    switch (action.data?.type) {
     85      case "impression": {
     86        await this.recordShortcutsInteraction(0, action.data);
     87        break;
     88      }
     89      case "click": {
     90        await this.recordShortcutsInteraction(1, action.data);
     91        break;
     92      }
     93    }
     94  }
     95 
     96  async onPrefChangedAction(action) {
     97    switch (action.data.name) {
     98      case PREF_SYSTEM_SHORTCUTS_PERSONALIZATION: {
     99        await this.init();
    100        break;
    101      }
    102    }
    103  }
    104 
    105  async onAction(action) {
    106    switch (action.type) {
    107      case at.INIT:
    108        await this.init();
    109        break;
    110      case at.UNINIT:
    111        await this.reset();
    112        break;
    113      case at.TOP_SITES_ORGANIC_IMPRESSION_STATS:
    114        if (this.isEnabled()) {
    115          await this.handleTopSitesOrganicImpressionStats(action);
    116        }
    117        break;
    118      case at.PREF_CHANGED:
    119        this.onPrefChangedAction(action);
    120        if (action.data.name === "trainhopConfig") {
    121          await this.init();
    122        }
    123        break;
    124    }
    125  }
    126 }
    127 
    128 /**
    129 * Creating a thin wrapper around Date.
    130 * This makes it easier for us to write automated tests that simulate responses.
    131 */
    132 SmartShortcutsFeed.prototype.Date = () => {
    133  return Date;
    134 };