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 };