TelemetryFeed.sys.mjs (69238B)
1 // We're using console.error() to debug, so we'll be keeping this rule handy 2 /* eslint no-console: ["error", { allow: ["error"] }] */ 3 4 /* This Source Code Form is subject to the terms of the Mozilla Public 5 * License, v. 2.0. If a copy of the MPL was not distributed with this 6 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 7 8 // We use importESModule here instead of static import so that the Karma test 9 // environment won't choke on these module. This is because the Karma test 10 // environment already stubs out XPCOMUtils and RemoteSettings, and overrides 11 // importESModule to be a no-op (which can't be done for a static import 12 // statement). 13 // eslint-disable-next-line mozilla/use-static-import 14 const { XPCOMUtils } = ChromeUtils.importESModule( 15 "resource://gre/modules/XPCOMUtils.sys.mjs" 16 ); 17 18 import { 19 actionTypes as at, 20 actionUtils as au, 21 } from "resource://newtab/common/Actions.mjs"; 22 import { Prefs } from "resource://newtab/lib/ActivityStreamPrefs.sys.mjs"; 23 import { classifySite } from "resource://newtab/lib/SiteClassifier.sys.mjs"; 24 25 const lazy = {}; 26 27 ChromeUtils.defineESModuleGetters(lazy, { 28 AboutNewTab: "resource:///modules/AboutNewTab.sys.mjs", 29 ClientEnvironmentBase: 30 "resource://gre/modules/components-utils/ClientEnvironment.sys.mjs", 31 ClientID: "resource://gre/modules/ClientID.sys.mjs", 32 ContextId: "moz-src:///browser/modules/ContextId.sys.mjs", 33 ExtensionSettingsStore: 34 "resource://gre/modules/ExtensionSettingsStore.sys.mjs", 35 ExtensionUtils: "resource://gre/modules/ExtensionUtils.sys.mjs", 36 HomePage: "resource:///modules/HomePage.sys.mjs", 37 ObliviousHTTP: "resource://gre/modules/ObliviousHTTP.sys.mjs", 38 Region: "resource://gre/modules/Region.sys.mjs", 39 TelemetryEnvironment: "resource://gre/modules/TelemetryEnvironment.sys.mjs", 40 UTEventReporting: "resource://newtab/lib/UTEventReporting.sys.mjs", 41 NewTabContentPing: "resource://newtab/lib/NewTabContentPing.sys.mjs", 42 NewTabUtils: "resource://gre/modules/NewTabUtils.sys.mjs", 43 NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs", 44 }); 45 46 export const PREF_IMPRESSION_ID = "impressionId"; 47 export const TELEMETRY_PREF = "telemetry"; 48 export const EVENTS_TELEMETRY_PREF = "telemetry.ut.events"; 49 export const PREF_UNIFIED_ADS_SPOCS_ENABLED = "unifiedAds.spocs.enabled"; 50 export const PREF_UNIFIED_ADS_TILES_ENABLED = "unifiedAds.tiles.enabled"; 51 const PREF_ENDPOINTS = "discoverystream.endpoints"; 52 const PREF_SHOW_SPONSORED_STORIES = "showSponsored"; 53 const PREF_SHOW_SPONSORED_TOPSITES = "showSponsoredTopSites"; 54 const BLANK_HOMEPAGE_URL = "chrome://browser/content/blanktab.html"; 55 const PREF_PRIVATE_PING_ENABLED = "telemetry.privatePing.enabled"; 56 const PREF_REDACT_NEWTAB_PING_NEABLED = 57 "telemetry.privatePing.redactNewtabPing.enabled"; 58 const PREF_PRIVATE_PING_INFERRED_ENABLED = 59 "telemetry.privatePing.inferredInterests.enabled"; 60 const PREF_NEWTAB_PING_ENABLED = "browser.newtabpage.ping.enabled"; 61 const PREF_USER_INFERRED_PERSONALIZATION = 62 "discoverystream.sections.personalization.inferred.user.enabled"; 63 const PREF_SYSTEM_INFERRED_PERSONALIZATION = 64 "discoverystream.sections.personalization.inferred.enabled"; 65 const PREF_SECTIONS_PERSONALIZATION_ENABLED = 66 "discoverystream.sections.personalization.enabled"; 67 const PREF_SOV_FRECENCY_EXPOSURE = "sov.frecency.exposure"; 68 69 const TOP_STORIES_SECTION_NAME = "top_stories_section"; 70 71 /** 72 Additional parameters defined in the newTabTrainHop experimenter method 73 74 trainhopConfig.newtabPrivatePing.randomContentProbabilityEpsilonMicro 75 Epsilon for randomizing content impression and click telemetry using the RandomizedReponse method 76 in the newtab_content ping , as integer multipled by 1e6 77 78 trainhopConfig.newtabPrivatePing.dailyEventCap 79 Maximum newtab_content events that can be sent in 24 hour period. 80 */ 81 const TRAINHOP_PREF_RANDOM_CLICK_PROBABILITY_MICRO = 82 "randomContentClickProbabilityEpsilonMicro"; 83 84 /** 85 * Maximum newtab_content events that can be sent in 24 hour period. 86 */ 87 const TRAINHOP_PREF_DAILY_EVENT_CAP = "dailyEventCap"; 88 89 const TRAINHOP_PREF_DAILY_CLICK_EVENT_CAP = "dailyClickEventCap"; 90 const TRAINHOP_PREF_WEEKLY_CLICK_EVENT_CAP = "weeklyClickEventCap"; 91 92 // This is a mapping table between the user preferences and its encoding code 93 export const USER_PREFS_ENCODING = { 94 showSearch: 1 << 0, 95 "feeds.topsites": 1 << 1, 96 "feeds.section.topstories": 1 << 2, 97 "feeds.section.highlights": 1 << 3, 98 [PREF_SHOW_SPONSORED_STORIES]: 1 << 5, 99 "asrouter.userprefs.cfr.addons": 1 << 6, 100 "asrouter.userprefs.cfr.features": 1 << 7, 101 [PREF_SHOW_SPONSORED_TOPSITES]: 1 << 8, 102 }; 103 104 const PRIVATE_PING_SURFACE_COUNTRY_MAP = { 105 // This will be expanded to other surfaces as we expand the reach of the private content ping 106 NEW_TAB_EN_US: ["US", "CA"], 107 NEW_TAB_DE_DE: ["DE", "CH", "AT"], 108 NEW_TAB_EN_GB: ["GB", "IE"], 109 NEW_TAB_FR_FR: ["FR", "BE"], 110 }; 111 112 // Used as the missing value for timestamps in the session ping 113 const TIMESTAMP_MISSING_VALUE = -1; 114 115 // Page filter for onboarding telemetry, any value other than these will 116 // be set as "other" 117 const ONBOARDING_ALLOWED_PAGE_VALUES = [ 118 "about:welcome", 119 "about:home", 120 "about:newtab", 121 ]; 122 123 const PREF_SURFACE_ID = "telemetry.surfaceId"; 124 125 const CONTENT_PING_VERSION = 2; 126 127 const ACTIVITY_STREAM_PREF_BRANCH = "browser.newtabpage.activity-stream."; 128 129 const NEWTAB_PING_PREFS = { 130 showSearch: Glean.newtabSearch.enabled, 131 "feeds.topsites": Glean.topsites.enabled, 132 [PREF_SHOW_SPONSORED_TOPSITES]: Glean.topsites.sponsoredEnabled, 133 "feeds.section.highlights": Glean.newtab.highlightsEnabled, 134 "feeds.section.topstories": Glean.pocket.enabled, 135 [PREF_SHOW_SPONSORED_STORIES]: Glean.pocket.sponsoredStoriesEnabled, 136 topSitesRows: Glean.topsites.rows, 137 showWeather: Glean.newtab.weatherEnabled, 138 }; 139 140 const TOP_SITES_BLOCKED_SPONSORS_PREF = "browser.topsites.blockedSponsors"; 141 const TOPIC_SELECTION_SELECTED_TOPICS_PREF = 142 "browser.newtabpage.activity-stream.discoverystream.topicSelection.selectedTopics"; 143 export class TelemetryFeed { 144 constructor() { 145 this.sessions = new Map(); 146 this._prefs = new Prefs(); 147 this._impressionId = this.getOrCreateImpressionId(); 148 this._aboutHomeSeen = false; 149 this._classifySite = classifySite; 150 this._browserOpenNewtabStart = null; 151 this._privateRandomContentTelemetryProbablityValues = {}; 152 153 this.newtabContentPing = new lazy.NewTabContentPing(); 154 this._initialized = false; 155 156 XPCOMUtils.defineLazyPreferenceGetter( 157 this, 158 "SHOW_SPONSORED_STORIES_ENABLED", 159 `${ACTIVITY_STREAM_PREF_BRANCH}${PREF_SHOW_SPONSORED_STORIES}`, 160 false 161 ); 162 163 XPCOMUtils.defineLazyPreferenceGetter( 164 this, 165 "SHOW_SPONSORED_TOPSITES_ENABLED", 166 `${ACTIVITY_STREAM_PREF_BRANCH}${PREF_SHOW_SPONSORED_TOPSITES}`, 167 false 168 ); 169 } 170 171 get telemetryEnabled() { 172 return this._prefs.get(TELEMETRY_PREF); 173 } 174 175 get eventTelemetryEnabled() { 176 return this._prefs.get(EVENTS_TELEMETRY_PREF); 177 } 178 179 get privatePingEnabled() { 180 return this._prefs.get(PREF_PRIVATE_PING_ENABLED); 181 } 182 183 get redactNewTabPingEnabled() { 184 return this._prefs.get(PREF_REDACT_NEWTAB_PING_NEABLED); 185 } 186 187 get privatePingInferredInterestsEnabled() { 188 return ( 189 this._prefs.get(PREF_PRIVATE_PING_INFERRED_ENABLED) && 190 this._prefs.get(PREF_USER_INFERRED_PERSONALIZATION) && 191 this._prefs.get(PREF_SYSTEM_INFERRED_PERSONALIZATION) 192 ); 193 } 194 195 get sectionsPersonalizationEnabled() { 196 return this._prefs.get(PREF_SECTIONS_PERSONALIZATION_ENABLED); 197 } 198 199 get inferredInterests() { 200 return this.store.getState()?.InferredPersonalization 201 ?.coarsePrivateInferredInterests; 202 } 203 204 get clientInfo() { 205 return lazy.ClientEnvironmentBase; 206 } 207 208 get canSendUnifiedAdsSpocCallbacks() { 209 const unifiedAdsSpocsEnabled = this._prefs.get( 210 PREF_UNIFIED_ADS_SPOCS_ENABLED 211 ); 212 213 return unifiedAdsSpocsEnabled && this.SHOW_SPONSORED_STORIES_ENABLED; 214 } 215 216 get canSendUnifiedAdsTilesCallbacks() { 217 const unifiedAdsTilesEnabled = this._prefs.get( 218 PREF_UNIFIED_ADS_TILES_ENABLED 219 ); 220 221 return unifiedAdsTilesEnabled && this.SHOW_SPONSORED_TOPSITES_ENABLED; 222 } 223 224 get telemetryClientId() { 225 Object.defineProperty(this, "telemetryClientId", { 226 value: lazy.ClientID.getClientID(), 227 }); 228 return this.telemetryClientId; 229 } 230 231 get processStartTs() { 232 let startupInfo = Services.startup.getStartupInfo(); 233 let processStartTs = startupInfo.process.getTime(); 234 235 Object.defineProperty(this, "processStartTs", { 236 value: processStartTs, 237 }); 238 return this.processStartTs; 239 } 240 241 init() { 242 // TODO: It looks like (at least) browser_newtab_glean.js and 243 // browser_newtab_ping.js depend on most of the following to be executed 244 // even if init() is called more than once. That feels fragile. 245 246 this._beginObservingNewtabPingPrefs(); 247 248 if (!this._initialized) { 249 this._initialized = true; 250 Services.obs.addObserver( 251 this.browserOpenNewtabStart, 252 "browser-open-newtab-start" 253 ); 254 } 255 256 // Set two scalars for the "deletion-request" ping (See bug 1602064 and 1729474) 257 Glean.deletionRequest.impressionId.set(this._impressionId); 258 if (!lazy.ContextId.rotationEnabled) { 259 Glean.deletionRequest.contextId.set( 260 lazy.ContextId.requestSynchronously() 261 ); 262 } 263 Glean.newtab.locale.set(Services.locale.appLocaleAsBCP47); 264 } 265 266 getOrCreateImpressionId() { 267 let impressionId = this._prefs.get(PREF_IMPRESSION_ID); 268 if (!impressionId) { 269 impressionId = String(Services.uuid.generateUUID()); 270 this._prefs.set(PREF_IMPRESSION_ID, impressionId); 271 } 272 return impressionId; 273 } 274 275 browserOpenNewtabStart() { 276 let now = ChromeUtils.now(); 277 this._browserOpenNewtabStart = Math.round(this.processStartTs + now); 278 279 ChromeUtils.addProfilerMarker( 280 "UserTiming", 281 now, 282 "browser-open-newtab-start" 283 ); 284 } 285 286 /** 287 * Retrieves most recently followed sections (maximum 2 sections) 288 * 289 * @returns {string[]} comma separated string of section UUID's 290 */ 291 getFollowedSections() { 292 const sections = 293 this.store?.getState()?.DiscoveryStream.sectionPersonalization; 294 if (sections) { 295 // filter to only include followedTopics 296 const followed = Object.entries(sections).filter( 297 ([, info]) => info.isFollowed 298 ); 299 // sort from most recently followed to oldest. If followedAt is falsey, treat it as the oldest 300 followed.sort((a, b) => { 301 const aDate = a[1].followedAt ? new Date(a[1].followedAt) : 0; 302 const bDate = b[1].followedAt ? new Date(b[1].followedAt) : 0; 303 return bDate - aDate; 304 }); 305 306 return followed.slice(0, 2).map(([sectionId]) => sectionId); 307 } 308 return []; 309 } 310 311 setLoadTriggerInfo(port) { 312 // XXX note that there is a race condition here; we're assuming that no 313 // other tab will be interleaving calls to browserOpenNewtabStart and 314 // when at.NEW_TAB_INIT gets triggered by RemotePages and calls this 315 // method. For manually created windows, it's hard to imagine us hitting 316 // this race condition. 317 // 318 // However, for session restore, where multiple windows with multiple tabs 319 // might be restored much closer together in time, it's somewhat less hard, 320 // though it should still be pretty rare. 321 // 322 // The fix to this would be making all of the load-trigger notifications 323 // return some data with their notifications, and somehow propagate that 324 // data through closures into the tab itself so that we could match them 325 // 326 // As of this writing (very early days of system add-on perf telemetry), 327 // the hypothesis is that hitting this race should be so rare that makes 328 // more sense to live with the slight data inaccuracy that it would 329 // introduce, rather than doing the correct but complicated thing. It may 330 // well be worth reexamining this hypothesis after we have more experience 331 // with the data. 332 333 let data_to_save; 334 try { 335 if (!this._browserOpenNewtabStart) { 336 throw new Error("No browser-open-newtab-start recorded."); 337 } 338 data_to_save = { 339 load_trigger_ts: this._browserOpenNewtabStart, 340 load_trigger_type: "menu_plus_or_keyboard", 341 }; 342 } catch (e) { 343 // if no mark was returned, we have nothing to save 344 return; 345 } 346 this.saveSessionPerfData(port, data_to_save); 347 } 348 349 /** 350 * Lazily initialize UTEventReporting to send pings 351 */ 352 get utEvents() { 353 Object.defineProperty(this, "utEvents", { 354 value: new lazy.UTEventReporting(), 355 }); 356 return this.utEvents; 357 } 358 359 /** 360 * Get encoded user preferences, multiple prefs will be combined via bitwise OR operator 361 */ 362 get userPreferences() { 363 let prefs = 0; 364 365 for (const pref of Object.keys(USER_PREFS_ENCODING)) { 366 if (this._prefs.get(pref)) { 367 prefs |= USER_PREFS_ENCODING[pref]; 368 } 369 } 370 return prefs; 371 } 372 373 /** 374 * Removes fields that link to any user content preference. 375 * Redactions only occur if the appropriate pref is enabled. 376 * 377 * @param {*} pingDict Input dictionary 378 * @param {boolean} isSponsored Is this in ad, in which case there is nothing we can redact currently 379 * @returns {*} Possibly redacted dictionary 380 */ 381 redactNewTabPing(pingDict, isSponsored = false) { 382 if (this.redactNewTabPingEnabled && !isSponsored) { 383 const { 384 // eslint-disable-next-line no-unused-vars 385 corpus_item_id, 386 // eslint-disable-next-line no-unused-vars 387 scheduled_corpus_item_id, 388 // eslint-disable-next-line no-unused-vars 389 section, 390 // eslint-disable-next-line no-unused-vars 391 selected_topics, 392 // eslint-disable-next-line no-unused-vars 393 tile_id, 394 // eslint-disable-next-line no-unused-vars 395 topic, 396 ...result 397 } = pingDict; 398 result.content_redacted = true; 399 return result; 400 } 401 // For spocs we need to retain the tile id. 402 if (this.redactNewTabPingEnabled && isSponsored) { 403 const { 404 // eslint-disable-next-line no-unused-vars 405 section, 406 // eslint-disable-next-line no-unused-vars 407 selected_topics, 408 // eslint-disable-next-line no-unused-vars 409 topic, 410 ...result 411 } = pingDict; 412 result.content_redacted = true; 413 return result; 414 } 415 416 return pingDict; // No modification 417 } 418 419 /** 420 * addSession - Start tracking a new session 421 * 422 * @param {string} id the portID of the open session 423 * @param {string} the URL being loaded for this session (optional) 424 * @return {obj} Session object 425 */ 426 addSession(id, url) { 427 // XXX refactor to use setLoadTriggerInfo or saveSessionPerfData 428 429 // "unexpected" will be overwritten when appropriate 430 let load_trigger_type = "unexpected"; 431 let load_trigger_ts; 432 433 if (!this._aboutHomeSeen && url === "about:home") { 434 this._aboutHomeSeen = true; 435 436 // XXX note that this will be incorrectly set in the following cases: 437 // session_restore following by clicking on the toolbar button, 438 // or someone who has changed their default home page preference to 439 // something else and later clicks the toolbar. It will also be 440 // incorrectly unset if someone changes their "Home Page" preference to 441 // about:newtab. 442 // 443 // That said, the ratio of these mistakes to correct cases should 444 // be very small, and these issues should follow away as we implement 445 // the remaining load_trigger_type values for about:home in issue 3556. 446 // 447 // XXX file a bug to implement remaining about:home cases so this 448 // problem will go away and link to it here. 449 load_trigger_type = "first_window_opened"; 450 451 // The real perceived trigger of first_window_opened is the OS-level 452 // clicking of the icon. We express this by using the process start 453 // absolute timestamp. 454 load_trigger_ts = this.processStartTs; 455 } 456 457 const session = { 458 session_id: String(Services.uuid.generateUUID()), 459 // "unknown" will be overwritten when appropriate 460 page: url ? url : "unknown", 461 perf: { 462 load_trigger_type, 463 is_preloaded: false, 464 }, 465 }; 466 467 if (load_trigger_ts) { 468 session.perf.load_trigger_ts = load_trigger_ts; 469 } 470 471 this.sessions.set(id, session); 472 return session; 473 } 474 475 /** 476 * endSession - Stop tracking a session 477 * 478 * @param {string} portID the portID of the session that just closed 479 */ 480 async endSession(portID) { 481 const session = this.sessions.get(portID); 482 if (!session) { 483 // It's possible the tab was never visible – in which case, there was no user session. 484 return; 485 } 486 487 Glean.newtab.closed.record({ newtab_visit_id: session.session_id }); 488 if ( 489 this.telemetryEnabled && 490 Services.prefs.getBoolPref(PREF_NEWTAB_PING_ENABLED, true) 491 ) { 492 GleanPings.newtab.submit("newtab_session_end"); 493 if (this.privatePingEnabled) { 494 this.configureContentPing(); 495 } 496 } 497 498 if (session.perf.visibility_event_rcvd_ts) { 499 let absNow = this.processStartTs + ChromeUtils.now(); 500 session.session_duration = Math.round( 501 absNow - session.perf.visibility_event_rcvd_ts 502 ); 503 504 // Rounding all timestamps in perf to ease the data processing on the backend. 505 // NB: use `TIMESTAMP_MISSING_VALUE` if the value is missing. 506 session.perf.visibility_event_rcvd_ts = Math.round( 507 session.perf.visibility_event_rcvd_ts 508 ); 509 session.perf.load_trigger_ts = Math.round( 510 session.perf.load_trigger_ts || TIMESTAMP_MISSING_VALUE 511 ); 512 session.perf.topsites_first_painted_ts = Math.round( 513 session.perf.topsites_first_painted_ts || TIMESTAMP_MISSING_VALUE 514 ); 515 } else { 516 // This session was never shown (i.e. the hidden preloaded newtab), there was no user session either. 517 this.sessions.delete(portID); 518 return; 519 } 520 521 let sessionEndEvent = this.createSessionEndEvent(session); 522 this.sendUTEvent(sessionEndEvent, this.utEvents.sendSessionEndEvent); 523 this.sessions.delete(portID); 524 } 525 526 /** 527 * handleNewTabInit - Handle NEW_TAB_INIT, which creates a new session and sets the a flag 528 * for session.perf based on whether or not this new tab is preloaded 529 * 530 * @param {obj} action the Action object 531 */ 532 handleNewTabInit(action) { 533 const session = this.addSession( 534 au.getPortIdOfSender(action), 535 action.data.url 536 ); 537 session.perf.is_preloaded = 538 action.data.browser.getAttribute("preloadedState") === "preloaded"; 539 } 540 541 /** 542 * createPing - Create a ping with common properties 543 * 544 * @param {string} id The portID of the session, if a session is relevant (optional) 545 * @return {obj} A telemetry ping 546 */ 547 createPing(portID) { 548 const ping = { 549 addon_version: Services.appinfo.appBuildID, 550 locale: Services.locale.appLocaleAsBCP47, 551 user_prefs: this.userPreferences, 552 }; 553 554 // If the ping is part of a user session, add session-related info 555 if (portID) { 556 const session = this.sessions.get(portID) || this.addSession(portID); 557 Object.assign(ping, { session_id: session.session_id }); 558 559 if (session.page) { 560 Object.assign(ping, { page: session.page }); 561 } 562 } 563 return ping; 564 } 565 566 createUserEvent(action) { 567 return Object.assign( 568 this.createPing(au.getPortIdOfSender(action)), 569 action.data, 570 { action: "activity_stream_user_event" } 571 ); 572 } 573 574 createSessionEndEvent(session) { 575 return Object.assign(this.createPing(), { 576 session_id: session.session_id, 577 page: session.page, 578 session_duration: session.session_duration, 579 action: "activity_stream_session", 580 perf: session.perf, 581 profile_creation_date: 582 lazy.TelemetryEnvironment.currentEnvironment.profile.resetDate || 583 lazy.TelemetryEnvironment.currentEnvironment.profile.creationDate, 584 }); 585 } 586 587 sendUTEvent(event_object, eventFunction) { 588 if (this.telemetryEnabled && this.eventTelemetryEnabled) { 589 eventFunction(event_object); 590 } 591 } 592 593 sovEnabled() { 594 const { values } = this.store?.getState()?.Prefs || {}; 595 const trainhopSovEnabled = values?.trainhopConfig?.sov?.enabled; 596 return trainhopSovEnabled; 597 } 598 599 frecencyBoostedHasExposure() { 600 const { values } = this.store?.getState()?.Prefs || {}; 601 return values?.[PREF_SOV_FRECENCY_EXPOSURE]; 602 } 603 604 async handleTopSitesSponsoredImpressionStats(action) { 605 const { data } = action; 606 const { 607 type, 608 position, 609 source, 610 advertiser: advertiser_name, 611 tile_id, 612 visible_topsites, 613 frecency_boosted = false, 614 } = data; 615 // Legacy telemetry expects 1-based tile positions. 616 const legacyTelemetryPosition = position + 1; 617 618 const unifiedAdsTilesEnabled = this._prefs.get( 619 PREF_UNIFIED_ADS_TILES_ENABLED 620 ); 621 622 let pingType; 623 const session = this.sessions.get(au.getPortIdOfSender(action)); 624 625 if (type === "impression") { 626 pingType = "topsites-impression"; 627 Glean.contextualServicesTopsites.impression[ 628 `${source}_${legacyTelemetryPosition}` 629 ].add(1); 630 if (session) { 631 if (this.sovEnabled()) { 632 if (this.privatePingEnabled) { 633 this.newtabContentPing.recordEvent("topSitesImpression", { 634 advertiser_name, 635 tile_id, 636 is_sponsored: true, 637 position, 638 visible_topsites, 639 frecency_boosted, 640 frecency_boosted_has_exposure: this.frecencyBoostedHasExposure(), 641 }); 642 } 643 } else { 644 Glean.topsites.impression.record({ 645 advertiser_name, 646 tile_id, 647 newtab_visit_id: session.session_id, 648 is_sponsored: true, 649 position, 650 visible_topsites, 651 }); 652 } 653 } 654 } else if (type === "click") { 655 pingType = "topsites-click"; 656 Glean.contextualServicesTopsites.click[ 657 `${source}_${legacyTelemetryPosition}` 658 ].add(1); 659 if (session) { 660 if (this.sovEnabled()) { 661 if (this.privatePingEnabled) { 662 this.newtabContentPing.recordEvent("topSitesClick", { 663 advertiser_name, 664 tile_id, 665 is_sponsored: true, 666 position, 667 visible_topsites, 668 frecency_boosted, 669 frecency_boosted_has_exposure: this.frecencyBoostedHasExposure(), 670 }); 671 } 672 } else { 673 Glean.topsites.click.record({ 674 advertiser_name, 675 tile_id, 676 newtab_visit_id: session.session_id, 677 is_sponsored: true, 678 position, 679 visible_topsites, 680 }); 681 } 682 } 683 } else { 684 console.error("Unknown ping type for sponsored TopSites impression"); 685 return; 686 } 687 688 if (this.sovEnabled()) { 689 Glean.topSites.pingType.set(pingType); 690 Glean.topSites.position.set(legacyTelemetryPosition); 691 Glean.topSites.source.set(source); 692 Glean.topSites.tileId.set(tile_id); 693 if (data.reporting_url && !unifiedAdsTilesEnabled) { 694 Glean.topSites.reportingUrl.set(data.reporting_url); 695 } 696 Glean.topSites.advertiser.set(advertiser_name); 697 Glean.topSites.contextId.set(await lazy.ContextId.request()); 698 GleanPings.topSites.submit(); 699 } 700 701 if (data.reporting_url && this.canSendUnifiedAdsTilesCallbacks) { 702 // Send callback events to MARS unified ads api 703 this.sendUnifiedAdsCallbackEvent({ 704 url: data.reporting_url, 705 position, 706 }); 707 } 708 } 709 710 handleTopSitesOrganicImpressionStats(action) { 711 const session = this.sessions.get(au.getPortIdOfSender(action)); 712 if (!session) { 713 return; 714 } 715 const visible_topsites = action.data?.visible_topsites; 716 717 switch (action.data?.type) { 718 case "impression": 719 Glean.topsites.impression.record({ 720 newtab_visit_id: session.session_id, 721 is_sponsored: false, 722 position: action.data.position, 723 is_pinned: !!action.data.isPinned, 724 visible_topsites, 725 smart_scores: JSON.stringify(action.data.smartScores), 726 smart_weights: JSON.stringify(action.data.smartWeights), 727 }); 728 break; 729 730 case "click": 731 Glean.topsites.click.record({ 732 newtab_visit_id: session.session_id, 733 is_sponsored: false, 734 position: action.data.position, 735 is_pinned: !!action.data.isPinned, 736 visible_topsites, 737 smart_scores: JSON.stringify(action.data.smartScores), 738 smart_weights: JSON.stringify(action.data.smartWeights), 739 }); 740 break; 741 742 default: 743 break; 744 } 745 } 746 747 /** 748 * Records the duration that spoc (ads) placeholders were visible to the user. 749 * This tracks how long placeholder content is shown before being replaced 750 * with actual sponsored content when using onDemand mode. 751 * 752 * @param {number} action.data.duration - Duration in milliseconds 753 */ 754 handleSpocPlaceholderDuration(action) { 755 const { duration } = action.data; 756 if (duration !== undefined && duration >= 0) { 757 Glean.pocket.spocPlaceholderDuration.accumulateSingleSample(duration); 758 } 759 } 760 761 handleUserEvent(action) { 762 let userEvent = this.createUserEvent(action); 763 try { 764 this.sendUTEvent(userEvent, this.utEvents.sendUserEvent); 765 } catch (error) {} 766 767 const session = this.sessions.get(au.getPortIdOfSender(action)); 768 if (!session) { 769 return; 770 } 771 772 switch (action.data?.event) { 773 case "PIN": { 774 Glean.topsites.pin.record({ 775 newtab_visit_id: session.session_id, 776 is_sponsored: false, 777 position: action.data.action_position, 778 }); 779 break; 780 } 781 case "UNPIN": { 782 Glean.topsites.unpin.record({ 783 newtab_visit_id: session.session_id, 784 is_sponsored: false, 785 position: action.data.action_position, 786 }); 787 break; 788 } 789 case "TOP_SITES_ADD": { 790 Glean.topsites.add.record({ 791 newtab_visit_id: session.session_id, 792 is_sponsored: false, 793 position: action.data.action_position, 794 }); 795 break; 796 } 797 case "TOP_SITES_EDIT": { 798 Glean.topsites.edit.record({ 799 newtab_visit_id: session.session_id, 800 is_sponsored: false, 801 position: action.data.action_position, 802 has_title_changed: action.data.hasTitleChanged, 803 has_url_changed: action.data.hasURLChanged, 804 }); 805 break; 806 } 807 case "WEATHER_DETECT_LOCATION": { 808 Glean.newtab.weatherDetectLocation.record({ 809 newtab_visit_id: session.session_id, 810 }); 811 break; 812 } 813 } 814 } 815 816 /** 817 * @returns Flat list of all articles for the New Tab. Does not include spocs (ads) 818 */ 819 getAllRecommendations() { 820 const merinoData = this.store?.getState()?.DiscoveryStream?.feeds.data; 821 return Object.values(merinoData ?? {}).flatMap( 822 feed => feed?.data?.recommendations ?? [] 823 ); 824 } 825 826 /** 827 * @returns Number of articles for the New Tab. Does not include spocs (ads) 828 */ 829 getRecommendationCount() { 830 const merinoData = this.store?.getState()?.DiscoveryStream?.feeds.data; 831 return Object.values(merinoData ?? {}).reduce( 832 (count, feed) => count + (feed.data?.recommendations?.length || 0), 833 0 834 ); 835 } 836 837 /** 838 * Occasionally replaces a content item with another that is in the feed. 839 * 840 * @param {*} item 841 * @returns Same item, but another item occasionally based on probablility setting. 842 * Sponsored items are unchanged 843 */ 844 randomizeOrganicContentEvent(item) { 845 if (item.is_sponsored) { 846 return item; // Don't alter spocs 847 } 848 const epsilon = 849 this._privateRandomContentTelemetryProbablityValues?.epsilon ?? 0; 850 if (!epsilon) { 851 return item; 852 } 853 if (!("n" in this._privateRandomContentTelemetryProbablityValues)) { 854 // We cache the number of items in the feed because it's computationally expensive to compute. 855 // This may not be ideal, but the number of content items typically is very similar over reloads 856 this._privateRandomContentTelemetryProbablityValues.n = 857 this.getRecommendationCount(); 858 } 859 const { n } = this._privateRandomContentTelemetryProbablityValues; 860 if (!n || n < 10) { 861 // None or very view articles. We're in an intermediate or error state. 862 return item; 863 } 864 const cache_key = `probability_${epsilon}_${n}`; // Lookup of probability for a item size 865 if (!(cache_key in this._privateRandomContentTelemetryProbablityValues)) { 866 this._privateRandomContentTelemetryProbablityValues[cache_key] = { 867 p: Math.exp(epsilon) / (Math.exp(epsilon) + n - 1), 868 }; 869 } 870 871 const { p } = 872 this._privateRandomContentTelemetryProbablityValues[cache_key]; 873 if (lazy.NewTabContentPing.decideWithProbability(p)) { 874 return item; 875 } 876 const allRecs = this.getAllRecommendations(); // Number of recommendations has changed 877 if (!allRecs.length) { 878 return item; 879 } 880 881 // Update number of recs for next round of checks for next round 882 this._privateRandomContentTelemetryProbablityValues.n = allRecs.length; 883 884 const randomIndex = lazy.NewTabContentPing.secureRandIntInRange( 885 allRecs.length 886 ); 887 let randomItem = allRecs[randomIndex]; 888 const resultItem = { 889 ...item, 890 topic: randomItem.topic, 891 corpus_item_id: randomItem.corpus_item_id, 892 }; 893 // If we're replacing a non top stories item, then assign the appropriate 894 // section to the item 895 if ( 896 resultItem.section && 897 resultItem.section !== TOP_STORIES_SECTION_NAME && 898 randomItem.section 899 ) { 900 resultItem.section = randomItem.section; 901 resultItem.section_position = randomItem.section_position; 902 } 903 return resultItem; 904 } 905 906 handleDiscoveryStreamUserEvent(action) { 907 this.handleUserEvent({ 908 ...action, 909 data: { 910 ...(action.data || {}), 911 value: { 912 ...(action.data?.value || {}), 913 }, 914 }, 915 }); 916 const session = this.sessions.get(au.getPortIdOfSender(action)); 917 918 switch (action.data?.event) { 919 // TODO: Determine if private window should be tracked? 920 // case "OPEN_PRIVATE_WINDOW": 921 case "OPEN_NEW_WINDOW": 922 case "CLICK": { 923 const { 924 card_type, 925 corpus_item_id, 926 event_source, 927 feature, 928 fetchTimestamp, 929 firstVisibleTimestamp, 930 format, 931 is_section_followed, 932 layout_name, 933 matches_selected_topic, 934 received_rank, 935 recommendation_id, 936 recommended_at, 937 scheduled_corpus_item_id, 938 section_position, 939 section, 940 selected_topics, 941 shim, 942 tile_id, 943 topic, 944 } = action.data.value ?? {}; 945 946 if ( 947 action.data.source === "POPULAR_TOPICS" || 948 card_type === "topics_widget" 949 ) { 950 Glean.pocket.topicClick.record({ 951 newtab_visit_id: session.session_id, 952 topic, 953 }); 954 } else if (action.data.source === "FEATURE_HIGHLIGHT") { 955 Glean.newtab.tooltipClick.record({ 956 newtab_visit_id: session.session_id, 957 feature, 958 }); 959 } else if (["spoc", "organic"].includes(card_type)) { 960 const is_sponsored = card_type === "spoc"; 961 const gleanData = { 962 newtab_visit_id: session.session_id, 963 is_sponsored, 964 ...(format ? { format } : {}), 965 ...(section 966 ? { 967 section, 968 section_position, 969 ...(this.sectionsPersonalizationEnabled 970 ? { is_section_followed: !!is_section_followed } 971 : {}), 972 layout_name, 973 } 974 : {}), 975 matches_selected_topic, 976 selected_topics, 977 topic, 978 position: action.data.action_position, 979 tile_id, 980 event_source, 981 // We conditionally add in a few props. 982 ...(corpus_item_id ? { corpus_item_id } : {}), 983 ...(scheduled_corpus_item_id ? { scheduled_corpus_item_id } : {}), 984 ...(corpus_item_id || scheduled_corpus_item_id 985 ? { 986 received_rank, 987 recommended_at, 988 } 989 : { 990 recommendation_id, 991 }), 992 }; 993 Glean.pocket.click.record({ 994 ...this.redactNewTabPing(gleanData, is_sponsored), 995 newtab_visit_id: session.session_id, 996 }); 997 if (this.privatePingEnabled) { 998 this.newtabContentPing.recordEvent( 999 "click", 1000 this.randomizeOrganicContentEvent(gleanData) 1001 ); 1002 } 1003 if (shim) { 1004 if (this.canSendUnifiedAdsSpocCallbacks) { 1005 // Send unified ads callback event 1006 this.sendUnifiedAdsCallbackEvent({ 1007 url: shim, 1008 position: action.data.action_position, 1009 }); 1010 } else { 1011 Glean.pocket.shim.set(shim); 1012 if (fetchTimestamp) { 1013 Glean.pocket.fetchTimestamp.set(fetchTimestamp * 1000); 1014 } 1015 if (firstVisibleTimestamp) { 1016 Glean.pocket.newtabCreationTimestamp.set( 1017 firstVisibleTimestamp * 1000 1018 ); 1019 } 1020 GleanPings.spoc.submit("click"); 1021 } 1022 } 1023 } 1024 1025 break; 1026 } 1027 // Bug 1969452 - Feature Highlight Telemetry Events 1028 case "FEATURE_HIGHLIGHT_DISMISS": 1029 case "FEATURE_HIGHLIGHT_IMPRESSION": 1030 case "FEATURE_HIGHLIGHT_OPEN": { 1031 // Note that Feature Highlight CLICK events are covered via newtab.tooltipClick Glean event 1032 const { feature } = action.data.value ?? {}; 1033 1034 if (!feature) { 1035 throw new Error( 1036 `Feature ID parameter is missing from ${action.data?.event}` 1037 ); 1038 } 1039 1040 if (action.data.event === "FEATURE_HIGHLIGHT_DISMISS") { 1041 Glean.newtab.featureHighlightDismiss.record({ 1042 newtab_visit_id: session.session_id, 1043 feature, 1044 }); 1045 } else if (action.data.event === "FEATURE_HIGHLIGHT_IMPRESSION") { 1046 Glean.newtab.featureHighlightImpression.record({ 1047 newtab_visit_id: session.session_id, 1048 feature, 1049 }); 1050 } else if (action.data.event === "FEATURE_HIGHLIGHT_OPEN") { 1051 Glean.newtab.featureHighlightOpen.record({ 1052 newtab_visit_id: session.session_id, 1053 feature, 1054 }); 1055 } 1056 1057 break; 1058 } 1059 } 1060 } 1061 1062 /** 1063 * This function submits callback events to the MARS unified ads service. 1064 */ 1065 1066 async sendUnifiedAdsCallbackEvent(data = { url: null, position: null }) { 1067 if (!data.url) { 1068 throw new Error( 1069 `[Unified ads callback] Missing argument (No url). Cannot send telemetry event.` 1070 ); 1071 } 1072 1073 // data.position can be 0 (0) 1074 if (!data.position && data.position !== 0) { 1075 throw new Error( 1076 `[Unified ads callback] Missing argument (No position). Cannot send telemetry event.` 1077 ); 1078 } 1079 1080 // Make sure the callback endpoint is allowed 1081 const allowed = 1082 this._prefs 1083 .get(PREF_ENDPOINTS) 1084 .split(",") 1085 .map(item => item.trim()) 1086 .filter(item => item) || []; 1087 if (!allowed.some(prefix => data.url.startsWith(prefix))) { 1088 throw new Error( 1089 `[Unified ads callback] Not one of allowed prefixes (${allowed})` 1090 ); 1091 } 1092 1093 const url = new URL(data.url); 1094 url.searchParams.append("position", data.position); 1095 1096 const marsOhttpEnabled = Services.prefs.getBoolPref( 1097 "browser.newtabpage.activity-stream.unifiedAds.ohttp.enabled", 1098 false 1099 ); 1100 const ohttpRelayURL = Services.prefs.getStringPref( 1101 "browser.newtabpage.activity-stream.discoverystream.ohttp.relayURL", 1102 "" 1103 ); 1104 const ohttpConfigURL = Services.prefs.getStringPref( 1105 "browser.newtabpage.activity-stream.discoverystream.ohttp.configURL", 1106 "" 1107 ); 1108 1109 let fetchPromise; 1110 const fetchUrl = url.toString(); 1111 1112 if (marsOhttpEnabled) { 1113 if (!ohttpRelayURL) { 1114 console.error( 1115 new Error( 1116 `OHTTP was configured for ${fetchUrl} but we didn't have a valid ohttpRelayURL` 1117 ) 1118 ); 1119 } 1120 if (!ohttpConfigURL) { 1121 console.error( 1122 new Error( 1123 `OHTTP was configured for ${fetchUrl} but we didn't have a valid ohttpConfigURL` 1124 ) 1125 ); 1126 } 1127 1128 const headers = new Headers(); 1129 const controller = new AbortController(); 1130 const { signal } = controller; 1131 1132 const options = { 1133 method: "GET", 1134 headers, 1135 signal, 1136 }; 1137 1138 let config = await lazy.ObliviousHTTP.getOHTTPConfig(ohttpConfigURL); 1139 if (!config) { 1140 console.error( 1141 new Error( 1142 `OHTTP was configured for ${fetchUrl} but we couldn't fetch a valid config` 1143 ) 1144 ); 1145 } 1146 1147 fetchPromise = lazy.ObliviousHTTP.ohttpRequest( 1148 ohttpRelayURL, 1149 config, 1150 fetchUrl, 1151 options 1152 ); 1153 } else { 1154 fetchPromise = fetch(fetchUrl); 1155 } 1156 1157 try { 1158 await fetchPromise; 1159 } catch (error) { 1160 console.error("Error:", error); 1161 } 1162 } 1163 1164 async sendPageTakeoverData() { 1165 if (this.telemetryEnabled) { 1166 const value = {}; 1167 let homeAffected = false; 1168 let newtabCategory = "disabled"; 1169 let homePageCategory = "disabled"; 1170 1171 // Check whether or not about:home and about:newtab are set to a custom URL. 1172 // If so, classify them. 1173 if (Services.prefs.getBoolPref("browser.newtabpage.enabled")) { 1174 newtabCategory = "enabled"; 1175 if ( 1176 lazy.AboutNewTab.newTabURLOverridden && 1177 !lazy.ExtensionUtils.isExtensionUrl(lazy.AboutNewTab.newTabURL) 1178 ) { 1179 value.newtab_url_category = await this._classifySite( 1180 lazy.AboutNewTab.newTabURL 1181 ); 1182 newtabCategory = value.newtab_url_category; 1183 } 1184 } 1185 // Check if the newtab page setting is controlled by an extension. 1186 await lazy.ExtensionSettingsStore.initialize(); 1187 const newtabExtensionInfo = lazy.ExtensionSettingsStore.getSetting( 1188 "url_overrides", 1189 "newTabURL" 1190 ); 1191 if (newtabExtensionInfo && newtabExtensionInfo.id) { 1192 value.newtab_extension_id = newtabExtensionInfo.id; 1193 newtabCategory = "extension"; 1194 } 1195 1196 const homePageURL = lazy.HomePage.get(); 1197 if ( 1198 !["about:home", "about:blank", BLANK_HOMEPAGE_URL].includes( 1199 homePageURL 1200 ) && 1201 !lazy.ExtensionUtils.isExtensionUrl(homePageURL) 1202 ) { 1203 value.home_url_category = await this._classifySite(homePageURL); 1204 homeAffected = true; 1205 homePageCategory = value.home_url_category; 1206 } 1207 const homeExtensionInfo = lazy.ExtensionSettingsStore.getSetting( 1208 "prefs", 1209 "homepage_override" 1210 ); 1211 if (homeExtensionInfo && homeExtensionInfo.id) { 1212 value.home_extension_id = homeExtensionInfo.id; 1213 homeAffected = true; 1214 homePageCategory = "extension"; 1215 } 1216 if (!homeAffected && !lazy.HomePage.overridden) { 1217 homePageCategory = "enabled"; 1218 } 1219 1220 Glean.newtab.newtabCategory.set(newtabCategory); 1221 Glean.newtab.homepageCategory.set(homePageCategory); 1222 1223 if (Services.prefs.getBoolPref(PREF_NEWTAB_PING_ENABLED, true)) { 1224 if (this.privatePingEnabled) { 1225 this.configureContentPing(); 1226 } 1227 GleanPings.newtab.submit("component_init"); 1228 } 1229 } 1230 } 1231 1232 /** 1233 * Populates the newtab-content ping with metrics, and the schedules 1234 * submission of the ping via NewTabContentPing. 1235 */ 1236 async configureContentPing() { 1237 let privateMetrics = {}; 1238 const prefs = this.store.getState()?.Prefs.values; // Needed for experimenter configs 1239 const inferredInterests = 1240 this.privatePingInferredInterestsEnabled && this.inferredInterests; 1241 if (inferredInterests) { 1242 privateMetrics.inferredInterests = inferredInterests; 1243 } 1244 this._privateRandomContentTelemetryProbablityValues = { 1245 epsilon: 1246 (prefs?.trainhopConfig?.newtabPrivatePing?.[ 1247 TRAINHOP_PREF_RANDOM_CLICK_PROBABILITY_MICRO 1248 ] || 0) / 1e6, 1249 }; 1250 const privatePingConfig = prefs?.trainhopConfig?.newtabPrivatePing || {}; 1251 // Set the daily cap for content pings 1252 const impressionCap = privatePingConfig[TRAINHOP_PREF_DAILY_EVENT_CAP] || 0; 1253 this.newtabContentPing.setMaxEventsPerDay(impressionCap); 1254 const clickDailyCap = 1255 privatePingConfig[TRAINHOP_PREF_DAILY_CLICK_EVENT_CAP] || 0; 1256 this.newtabContentPing.setMaxClickEventsPerDay(clickDailyCap); 1257 const weeklyClickCap = 1258 privatePingConfig[TRAINHOP_PREF_WEEKLY_CLICK_EVENT_CAP] || 0; 1259 this.newtabContentPing.setMaxClickEventsPerWeek(weeklyClickCap); 1260 1261 // When we have a coarse interest vector we want to make sure there isn't 1262 // anything additionaly identifable as a unique identifier. Therefore, 1263 // when interest vectors are used we reduce our context profile somewhat. 1264 const reduceTrackingInformation = !!inferredInterests; 1265 1266 if (!reduceTrackingInformation) { 1267 const followed = this.getFollowedSections(); 1268 privateMetrics.followedSections = followed; 1269 } 1270 const surfaceId = this._prefs.get(PREF_SURFACE_ID); 1271 privateMetrics.surfaceId = surfaceId; 1272 1273 const curCountry = lazy.Region.home; 1274 if (PRIVATE_PING_SURFACE_COUNTRY_MAP[surfaceId]) { 1275 // This is a market that supports inferred 1276 // Only include supported current countries for the surface to reduce identifiability. 1277 // Default to first country on the list 1278 privateMetrics.country = PRIVATE_PING_SURFACE_COUNTRY_MAP[ 1279 surfaceId 1280 ].includes(curCountry) 1281 ? curCountry 1282 : PRIVATE_PING_SURFACE_COUNTRY_MAP[surfaceId][0]; 1283 } 1284 1285 if (prefs.inferredPersonalizationConfig?.normalized_time_zone_offset) { 1286 privateMetrics.utcOffset = lazy.NewTabUtils.getUtcOffset(surfaceId); 1287 } 1288 // To prevent fingerprinting we only send one current experiment / branch 1289 const experimentMetadata = 1290 lazy.NimbusFeatures.pocketNewtab.getEnrollmentMetadata(); 1291 privateMetrics.experimentName = experimentMetadata?.slug ?? ""; 1292 privateMetrics.experimentBranch = experimentMetadata?.branch ?? ""; 1293 privateMetrics.pingVersion = CONTENT_PING_VERSION; 1294 this.newtabContentPing.scheduleSubmission(privateMetrics); 1295 } 1296 1297 async onAction(action) { 1298 switch (action.type) { 1299 case at.INIT: 1300 this.init(); 1301 await this.sendPageTakeoverData(); 1302 break; 1303 case at.NEW_TAB_INIT: 1304 this.handleNewTabInit(action); 1305 break; 1306 case at.NEW_TAB_UNLOAD: 1307 this.endSession(au.getPortIdOfSender(action)); 1308 break; 1309 case at.SAVE_SESSION_PERF_DATA: 1310 this.saveSessionPerfData(au.getPortIdOfSender(action), action.data); 1311 break; 1312 case at.DISCOVERY_STREAM_IMPRESSION_STATS: 1313 this.handleDiscoveryStreamImpressionStats( 1314 au.getPortIdOfSender(action), 1315 action.data 1316 ); 1317 break; 1318 case at.DISCOVERY_STREAM_SPOC_PLACEHOLDER_DURATION: 1319 this.handleSpocPlaceholderDuration(action); 1320 break; 1321 case at.DISCOVERY_STREAM_USER_EVENT: 1322 this.handleDiscoveryStreamUserEvent(action); 1323 break; 1324 case at.TELEMETRY_USER_EVENT: 1325 this.handleUserEvent(action); 1326 break; 1327 case at.TOP_SITES_SPONSORED_IMPRESSION_STATS: 1328 this.handleTopSitesSponsoredImpressionStats(action); 1329 break; 1330 case at.TOP_SITES_ORGANIC_IMPRESSION_STATS: 1331 this.handleTopSitesOrganicImpressionStats(action); 1332 break; 1333 case at.UNINIT: 1334 this.uninit(); 1335 break; 1336 case at.ABOUT_SPONSORED_TOP_SITES: 1337 this.handleAboutSponsoredTopSites(action); 1338 break; 1339 case at.BLOCK_URL: 1340 this.handleBlockUrl(action); 1341 break; 1342 case at.WALLPAPER_CATEGORY_CLICK: 1343 case at.WALLPAPER_CLICK: 1344 case at.WALLPAPERS_FEATURE_HIGHLIGHT_DISMISSED: 1345 case at.WALLPAPERS_FEATURE_HIGHLIGHT_CTA_CLICKED: 1346 case at.WALLPAPER_UPLOAD: 1347 this.handleWallpaperUserEvent(action); 1348 break; 1349 case at.SET_PREF: 1350 this.handleSetPref(action); 1351 break; 1352 case at.WEATHER_IMPRESSION: 1353 case at.WEATHER_LOAD_ERROR: 1354 case at.WEATHER_OPEN_PROVIDER_URL: 1355 case at.WEATHER_LOCATION_DATA_UPDATE: 1356 case at.WEATHER_OPT_IN_PROMPT_SELECTION: 1357 this.handleWeatherUserEvent(action); 1358 break; 1359 case at.TOPIC_SELECTION_USER_OPEN: 1360 case at.TOPIC_SELECTION_USER_DISMISS: 1361 case at.TOPIC_SELECTION_USER_SAVE: 1362 this.handleTopicSelectionUserEvent(action); 1363 break; 1364 case at.BLOCK_SECTION: 1365 // Intentional fall-through 1366 case at.CARD_SECTION_IMPRESSION: 1367 // Intentional fall-through 1368 case at.FOLLOW_SECTION: 1369 // Intentional fall-through 1370 case at.UNBLOCK_SECTION: 1371 // Intentional fall-through 1372 case at.UNFOLLOW_SECTION: { 1373 this.handleCardSectionUserEvent(action); 1374 break; 1375 } 1376 case at.INLINE_SELECTION_CLICK: 1377 // Intentional fall-through 1378 case at.INLINE_SELECTION_IMPRESSION: 1379 this.handleInlineSelectionUserEvent(action); 1380 break; 1381 case at.REPORT_AD_OPEN: 1382 case at.REPORT_AD_SUBMIT: 1383 this.handleReportAdUserEvent(action); 1384 break; 1385 case at.REPORT_CONTENT_OPEN: 1386 case at.REPORT_CONTENT_SUBMIT: 1387 this.handleReportContentUserEvent(action); 1388 break; 1389 case at.WIDGETS_LISTS_USER_EVENT: 1390 case at.WIDGETS_LISTS_USER_IMPRESSION: 1391 case at.WIDGETS_TIMER_USER_EVENT: 1392 case at.WIDGETS_TIMER_USER_IMPRESSION: 1393 this.handleWidgetsUserEvent(action); 1394 break; 1395 case at.PROMO_CARD_CLICK: 1396 case at.PROMO_CARD_DISMISS: 1397 case at.PROMO_CARD_IMPRESSION: 1398 this.handlePromoCardUserEvent(action); 1399 break; 1400 } 1401 } 1402 1403 handlePromoCardUserEvent(action) { 1404 const session = this.sessions.get(au.getPortIdOfSender(action)); 1405 if (session) { 1406 const payload = { 1407 newtab_visit_id: session.visit_id, 1408 }; 1409 1410 switch (action.type) { 1411 case at.PROMO_CARD_CLICK: 1412 Glean.newtab.promoCardClick.record(payload); 1413 break; 1414 case at.PROMO_CARD_DISMISS: 1415 Glean.newtab.promoCardDismiss.record(payload); 1416 break; 1417 case at.PROMO_CARD_IMPRESSION: 1418 Glean.newtab.promoCardImpression.record(payload); 1419 break; 1420 } 1421 } 1422 } 1423 1424 handleWidgetsUserEvent(action) { 1425 const session = this.sessions.get(au.getPortIdOfSender(action)); 1426 if (session) { 1427 const payload = { 1428 newtab_visit_id: session.visit_id, 1429 }; 1430 switch (action.type) { 1431 case "WIDGETS_LISTS_USER_EVENT": 1432 Glean.newtab.widgetsListsUserEvent.record({ 1433 ...payload, 1434 user_action: action.data.userAction, 1435 }); 1436 break; 1437 case "WIDGETS_LISTS_USER_IMPRESSION": 1438 Glean.newtab.widgetsListsImpression.record(payload); 1439 break; 1440 case "WIDGETS_TIMER_USER_EVENT": 1441 Glean.newtab.widgetsTimerUserEvent.record({ 1442 ...payload, 1443 user_action: action.data.userAction, 1444 }); 1445 break; 1446 case "WIDGETS_TIMER_USER_IMPRESSION": 1447 Glean.newtab.widgetsTimerImpression.record(payload); 1448 break; 1449 } 1450 } 1451 } 1452 1453 async handleReportAdUserEvent(action) { 1454 const { placement_id, position, report_reason, reporting_url } = 1455 action.data || {}; 1456 1457 const url = new URL(reporting_url); 1458 url.searchParams.append("placement_id", placement_id); 1459 url.searchParams.append("reason", report_reason); 1460 url.searchParams.append("position", position); 1461 const adResponse = url.toString(); 1462 1463 const allowed = 1464 this._prefs 1465 .get(PREF_ENDPOINTS) 1466 .split(",") 1467 .map(item => item.trim()) 1468 .filter(item => item) || []; 1469 1470 if (!allowed.some(prefix => adResponse.startsWith(prefix))) { 1471 throw new Error( 1472 `[Unified ads callback] Not one of allowed prefixes (${allowed})` 1473 ); 1474 } 1475 1476 try { 1477 await fetch(adResponse); 1478 } catch (error) { 1479 console.error("Error:", error); 1480 } 1481 } 1482 1483 handleReportContentUserEvent(action) { 1484 const session = this.sessions.get(au.getPortIdOfSender(action)); 1485 const { 1486 card_type, 1487 corpus_item_id, 1488 report_reason, 1489 scheduled_corpus_item_id, 1490 section_position, 1491 section, 1492 title, 1493 topic, 1494 url, 1495 } = action.data || {}; 1496 1497 if (session) { 1498 switch (action.type) { 1499 case "REPORT_CONTENT_OPEN": { 1500 if (!this.privatePingEnabled) { 1501 return; 1502 } 1503 1504 const gleanData = { 1505 corpus_item_id, 1506 scheduled_corpus_item_id, 1507 }; 1508 1509 Glean.newtabContent.reportContentOpen.record(gleanData); 1510 1511 break; 1512 } 1513 case "REPORT_CONTENT_SUBMIT": { 1514 const gleanData = { 1515 card_type, 1516 corpus_item_id, 1517 report_reason, 1518 scheduled_corpus_item_id, 1519 section_position, 1520 section, 1521 title, 1522 topic, 1523 url, 1524 }; 1525 1526 if (this.privatePingEnabled) { 1527 Glean.newtabContent.reportContentSubmit.record(gleanData); 1528 } 1529 break; 1530 } 1531 } 1532 } 1533 } 1534 1535 handleCardSectionUserEvent(action) { 1536 const session = this.sessions.get(au.getPortIdOfSender(action)); 1537 if (session) { 1538 const { 1539 section, 1540 section_position, 1541 event_source, 1542 is_section_followed, 1543 layout_name, 1544 } = action.data; 1545 const gleanDataForPrivatePing = { 1546 section, 1547 section_position, 1548 event_source, 1549 }; 1550 1551 const gleanDataForNewtabPing = { 1552 ...gleanDataForPrivatePing, 1553 newtab_visit_id: session.session_id, 1554 }; 1555 1556 switch (action.type) { 1557 case "BLOCK_SECTION": 1558 Glean.newtab.sectionsBlockSection.record( 1559 this.redactNewTabPing(gleanDataForNewtabPing) 1560 ); 1561 if (this.privatePingEnabled) { 1562 this.newtabContentPing.recordEvent( 1563 "sectionsBlockSection", 1564 gleanDataForPrivatePing 1565 ); 1566 } 1567 break; 1568 case "UNBLOCK_SECTION": 1569 Glean.newtab.sectionsUnblockSection.record( 1570 this.redactNewTabPing(gleanDataForNewtabPing) 1571 ); 1572 if (this.privatePingEnabled) { 1573 this.newtabContentPing.recordEvent( 1574 "sectionsUnblockSection", 1575 gleanDataForPrivatePing 1576 ); 1577 } 1578 break; 1579 case "CARD_SECTION_IMPRESSION": 1580 { 1581 const gleanData = { 1582 newtab_visit_id: session.session_id, 1583 section, 1584 section_position, 1585 ...(this.sectionsPersonalizationEnabled 1586 ? { is_section_followed: !!is_section_followed } 1587 : {}), 1588 layout_name, 1589 }; 1590 Glean.newtab.sectionsImpression.record( 1591 this.redactNewTabPing(gleanData) 1592 ); 1593 if (this.privatePingEnabled) { 1594 this.newtabContentPing.recordEvent("sectionsImpression", { 1595 section, 1596 section_position, 1597 ...(this.sectionsPersonalizationEnabled 1598 ? { is_section_followed: !!is_section_followed } 1599 : {}), 1600 }); 1601 } 1602 } 1603 break; 1604 case "FOLLOW_SECTION": { 1605 Glean.newtab.sectionsFollowSection.record( 1606 this.redactNewTabPing(gleanDataForNewtabPing) 1607 ); 1608 if (this.privatePingEnabled) { 1609 this.newtabContentPing.recordEvent( 1610 "sectionsFollowSection", 1611 gleanDataForPrivatePing 1612 ); 1613 } 1614 break; 1615 } 1616 case "UNFOLLOW_SECTION": 1617 Glean.newtab.sectionsUnfollowSection.record( 1618 this.redactNewTabPing(gleanDataForNewtabPing) 1619 ); 1620 if (this.privatePingEnabled) { 1621 this.newtabContentPing.recordEvent( 1622 "sectionsUnfollowSection", 1623 gleanDataForPrivatePing 1624 ); 1625 } 1626 break; 1627 default: 1628 break; 1629 } 1630 } 1631 } 1632 1633 handleInlineSelectionUserEvent(action) { 1634 const session = this.sessions.get(au.getPortIdOfSender(action)); 1635 if (session) { 1636 switch (action.type) { 1637 case "INLINE_SELECTION_CLICK": { 1638 const { topic, section_position, position, is_followed } = 1639 action.data; 1640 Glean.newtab.inlineSelectionClick.record({ 1641 newtab_visit_id: session.session_id, 1642 topic, 1643 section_position, 1644 position, 1645 is_followed, 1646 }); 1647 break; 1648 } 1649 case "INLINE_SELECTION_IMPRESSION": 1650 Glean.newtab.inlineSelectionImpression.record({ 1651 newtab_visit_id: session.session_id, 1652 section_position: action.data.section_position, 1653 }); 1654 break; 1655 } 1656 } 1657 } 1658 1659 handleTopicSelectionUserEvent(action) { 1660 const session = this.sessions.get(au.getPortIdOfSender(action)); 1661 if (session) { 1662 switch (action.type) { 1663 case "TOPIC_SELECTION_USER_OPEN": 1664 Glean.newtab.topicSelectionOpen.record({ 1665 newtab_visit_id: session.session_id, 1666 }); 1667 break; 1668 case "TOPIC_SELECTION_USER_DISMISS": 1669 Glean.newtab.topicSelectionDismiss.record({ 1670 newtab_visit_id: session.session_id, 1671 }); 1672 break; 1673 case "TOPIC_SELECTION_USER_SAVE": 1674 Glean.newtab.topicSelectionTopicsSaved.record({ 1675 newtab_visit_id: session.session_id, 1676 topics: action.data.topics, 1677 previous_topics: action.data.previous_topics, 1678 first_save: action.data.first_save, 1679 }); 1680 break; 1681 default: 1682 break; 1683 } 1684 } 1685 } 1686 1687 handleSetPref(action) { 1688 const session = this.sessions.get(au.getPortIdOfSender(action)); 1689 if (!session) { 1690 return; 1691 } 1692 switch (action.data.name) { 1693 case "weather.display": 1694 Glean.newtab.weatherChangeDisplay.record({ 1695 newtab_visit_id: session.session_id, 1696 weather_display_mode: action.data.value, 1697 }); 1698 break; 1699 case "widgets.lists.enabled": 1700 Glean.newtab.widgetsListsChangeDisplay.record({ 1701 newtab_visit_id: session.session_id, 1702 display_status: action.data.value, 1703 }); 1704 break; 1705 case "widgets.focusTimer.enabled": 1706 Glean.newtab.widgetsTimerChangeDisplay.record({ 1707 newtab_visit_id: session.session_id, 1708 display_status: action.data.value, 1709 }); 1710 break; 1711 } 1712 } 1713 1714 handleWeatherUserEvent(action) { 1715 const session = this.sessions.get(au.getPortIdOfSender(action)); 1716 1717 if (!session) { 1718 return; 1719 } 1720 1721 // Weather specific telemtry events can be added and parsed here. 1722 switch (action.type) { 1723 case "WEATHER_IMPRESSION": 1724 Glean.newtab.weatherImpression.record({ 1725 newtab_visit_id: session.session_id, 1726 }); 1727 break; 1728 case "WEATHER_LOAD_ERROR": 1729 Glean.newtab.weatherLoadError.record({ 1730 newtab_visit_id: session.session_id, 1731 }); 1732 break; 1733 case "WEATHER_OPEN_PROVIDER_URL": 1734 Glean.newtab.weatherOpenProviderUrl.record({ 1735 newtab_visit_id: session.session_id, 1736 }); 1737 break; 1738 case "WEATHER_LOCATION_DATA_UPDATE": 1739 Glean.newtab.weatherLocationSelected.record({ 1740 newtab_visit_id: session.session_id, 1741 }); 1742 break; 1743 case "WEATHER_OPT_IN_PROMPT_SELECTION": 1744 Glean.newtab.weatherOptInSelection.record({ 1745 newtab_visit_id: session.session_id, 1746 user_selection: action.data, 1747 }); 1748 break; 1749 default: 1750 break; 1751 } 1752 } 1753 1754 handleWallpaperUserEvent(action) { 1755 const session = this.sessions.get(au.getPortIdOfSender(action)); 1756 1757 if (!session) { 1758 return; 1759 } 1760 1761 const { data } = action; 1762 1763 // Wallpaper specific telemtry events can be added and parsed here. 1764 switch (action.type) { 1765 case "WALLPAPER_CATEGORY_CLICK": 1766 Glean.newtab.wallpaperCategoryClick.record({ 1767 newtab_visit_id: session.session_id, 1768 selected_category: action.data, 1769 }); 1770 break; 1771 case "WALLPAPER_CLICK": 1772 { 1773 const { 1774 selected_wallpaper, 1775 had_previous_wallpaper, 1776 had_uploaded_previously, 1777 } = data; 1778 1779 // if either of the wallpaper prefs are truthy, they had a previous wallpaper 1780 Glean.newtab.wallpaperClick.record({ 1781 newtab_visit_id: session.session_id, 1782 selected_wallpaper, 1783 had_previous_wallpaper, 1784 had_uploaded_previously, 1785 }); 1786 } 1787 break; 1788 case "WALLPAPERS_FEATURE_HIGHLIGHT_CTA_CLICKED": 1789 Glean.newtab.wallpaperHighlightCtaClick.record({ 1790 newtab_visit_id: session.session_id, 1791 }); 1792 break; 1793 case "WALLPAPERS_FEATURE_HIGHLIGHT_DISMISSED": 1794 Glean.newtab.wallpaperHighlightDismissed.record({ 1795 newtab_visit_id: session.session_id, 1796 }); 1797 break; 1798 default: 1799 break; 1800 } 1801 } 1802 1803 handleBlockUrl(action) { 1804 const session = this.sessions.get(au.getPortIdOfSender(action)); 1805 // TODO: Do we want to not send this unless there's a newtab_visit_id? 1806 if (!session) { 1807 return; 1808 } 1809 1810 // Despite the action name, this is actually a bulk dismiss action: 1811 // it can be applied to multiple topsites simultaneously. 1812 const { data } = action; 1813 for (const datum of data) { 1814 const { corpus_item_id, scheduled_corpus_item_id } = datum; 1815 1816 if (datum.is_pocket_card) { 1817 const gleanData = { 1818 is_sponsored: datum.card_type === "spoc", 1819 ...(datum.format ? { format: datum.format } : {}), 1820 position: datum.position, 1821 tile_id: datum.id || datum.tile_id, 1822 ...(datum.section 1823 ? { 1824 section: datum.section, 1825 section_position: datum.section_position, 1826 ...(this.sectionsPersonalizationEnabled 1827 ? { is_section_followed: !!datum.is_section_followed } 1828 : {}), 1829 } 1830 : {}), 1831 // We conditionally add in a few props. 1832 ...(corpus_item_id ? { corpus_item_id } : {}), 1833 ...(scheduled_corpus_item_id ? { scheduled_corpus_item_id } : {}), 1834 ...(corpus_item_id || scheduled_corpus_item_id 1835 ? { 1836 received_rank: datum.received_rank, 1837 recommended_at: datum.recommended_at, 1838 } 1839 : { 1840 recommendation_id: datum.recommendation_id, 1841 }), 1842 }; 1843 1844 Glean.pocket.dismiss.record({ 1845 ...this.redactNewTabPing(gleanData, gleanData.is_sponsored), 1846 newtab_visit_id: session.session_id, 1847 }); 1848 1849 if (this.privatePingEnabled) { 1850 this.newtabContentPing.recordEvent("dismiss", gleanData); 1851 } 1852 continue; 1853 } 1854 // Only log a topsites.dismiss telemetry event if the action came from TopSites section 1855 if (action.source === "TOP_SITES") { 1856 const { position, advertiser_name, tile_id, isSponsoredTopSite } = 1857 datum; 1858 if (this.sovEnabled() && isSponsoredTopSite) { 1859 if (this.privatePingEnabled) { 1860 this.newtabContentPing.recordEvent("topSitesDismiss", { 1861 advertiser_name, 1862 tile_id, 1863 is_sponsored: !!isSponsoredTopSite, 1864 position, 1865 }); 1866 } 1867 } else { 1868 Glean.topsites.dismiss.record({ 1869 advertiser_name, 1870 tile_id, 1871 newtab_visit_id: session.session_id, 1872 is_sponsored: !!isSponsoredTopSite, 1873 position, 1874 }); 1875 } 1876 } 1877 } 1878 } 1879 1880 handleAboutSponsoredTopSites(action) { 1881 const session = this.sessions.get(au.getPortIdOfSender(action)); 1882 const { data } = action; 1883 const { position, advertiser_name, tile_id } = data; 1884 1885 if (session) { 1886 if (this.sovEnabled()) { 1887 if (this.privatePingEnabled) { 1888 this.newtabContentPing.recordEvent("topSitesShowPrivacyClick", { 1889 advertiser_name, 1890 tile_id, 1891 position, 1892 }); 1893 } 1894 } else { 1895 Glean.topsites.showPrivacyClick.record({ 1896 advertiser_name, 1897 tile_id, 1898 newtab_visit_id: session.session_id, 1899 position, 1900 }); 1901 } 1902 } 1903 } 1904 1905 /** 1906 * Handle impression stats actions from Discovery Stream. 1907 * 1908 * @param {string} port The session port with which this is associated 1909 * @param {object} data The impression data structured as {source: "SOURCE", tiles: [{id: 123}]} 1910 */ 1911 handleDiscoveryStreamImpressionStats(port, data) { 1912 let session = this.sessions.get(port); 1913 1914 if (!session) { 1915 throw new Error("Session does not exist."); 1916 } 1917 1918 const { tiles } = data; 1919 1920 tiles.forEach(tile => { 1921 const { corpus_item_id, scheduled_corpus_item_id } = tile; 1922 const is_sponsored = tile.type === "spoc"; 1923 const gleanData = { 1924 is_sponsored, 1925 ...(tile.format ? { format: tile.format } : {}), 1926 ...(tile.section 1927 ? { 1928 section: tile.section, 1929 section_position: tile.section_position, 1930 ...(this.sectionsPersonalizationEnabled 1931 ? { is_section_followed: !!tile.is_section_followed } 1932 : {}), 1933 layout_name: tile.layout_name, 1934 } 1935 : {}), 1936 position: tile.pos, 1937 tile_id: tile.id, 1938 topic: tile.topic, 1939 selected_topics: tile.selectedTopics, 1940 is_list_card: tile.is_list_card, 1941 // We conditionally add in a few props. 1942 ...(corpus_item_id ? { corpus_item_id } : {}), 1943 ...(scheduled_corpus_item_id ? { scheduled_corpus_item_id } : {}), 1944 ...(corpus_item_id || scheduled_corpus_item_id 1945 ? { 1946 received_rank: tile.received_rank, 1947 recommended_at: tile.recommended_at, 1948 } 1949 : { 1950 recommendation_id: tile.recommendation_id, 1951 }), 1952 }; 1953 Glean.pocket.impression.record({ 1954 ...this.redactNewTabPing(gleanData, is_sponsored), 1955 newtab_visit_id: session.session_id, 1956 }); 1957 if (this.privatePingEnabled) { 1958 this.newtabContentPing.recordEvent("impression", gleanData); 1959 } 1960 1961 if (tile.shim) { 1962 if (this.canSendUnifiedAdsSpocCallbacks) { 1963 // Send unified ads callback event 1964 this.sendUnifiedAdsCallbackEvent({ 1965 url: tile.shim, 1966 position: tile.pos, 1967 }); 1968 } else { 1969 Glean.pocket.shim.set(tile.shim); 1970 if (tile.fetchTimestamp) { 1971 Glean.pocket.fetchTimestamp.set(tile.fetchTimestamp * 1000); 1972 } 1973 if (data.firstVisibleTimestamp) { 1974 Glean.pocket.newtabCreationTimestamp.set( 1975 data.firstVisibleTimestamp * 1000 1976 ); 1977 } 1978 GleanPings.spoc.submit("impression"); 1979 } 1980 } 1981 }); 1982 } 1983 1984 /** 1985 * Take all enumerable members of the data object and merge them into 1986 * the session.perf object for the given port, so that it is sent to the 1987 * server when the session ends. All members of the data object should 1988 * be valid values of the perf object, as defined in pings.js and the 1989 * data*.md documentation. 1990 * 1991 * Note: Any existing keys with the same names already in the 1992 * session perf object will be overwritten by values passed in here. 1993 * 1994 * @param {string} port The session with which this is associated 1995 * @param {object} data The perf data to be 1996 */ 1997 saveSessionPerfData(port, data) { 1998 // XXX should use try/catch and send a bad state indicator if this 1999 // get blows up. 2000 let session = this.sessions.get(port); 2001 2002 // XXX Partial workaround for #3118; avoids the worst incorrect associations 2003 // of times with browsers, by associating the load trigger with the 2004 // visibility event as the user is most likely associating the trigger to 2005 // the tab just shown. This helps avoid associating with a preloaded 2006 // browser as those don't get the event until shown. Better fix for more 2007 // cases forthcoming. 2008 // 2009 // XXX the about:home check (and the corresponding test) should go away 2010 // once the load_trigger stuff in addSession is refactored into 2011 // setLoadTriggerInfo. 2012 // 2013 if (data.visibility_event_rcvd_ts && session.page !== "about:home") { 2014 this.setLoadTriggerInfo(port); 2015 } 2016 2017 let timestamp = data.topsites_first_painted_ts; 2018 2019 if ( 2020 timestamp && 2021 session.page === "about:home" && 2022 !lazy.HomePage.overridden && 2023 Services.prefs.getIntPref("browser.startup.page") === 1 2024 ) { 2025 lazy.AboutNewTab.maybeRecordTopsitesPainted(timestamp); 2026 } 2027 2028 Object.assign(session.perf, data); 2029 2030 if (data.visibility_event_rcvd_ts && !session.newtabOpened) { 2031 session.newtabOpened = true; 2032 const source = ONBOARDING_ALLOWED_PAGE_VALUES.includes(session.page) 2033 ? session.page 2034 : "other"; 2035 Glean.newtab.opened.record({ 2036 newtab_visit_id: session.session_id, 2037 source, 2038 window_inner_height: data.window_inner_height, 2039 window_inner_width: data.window_inner_width, 2040 }); 2041 } 2042 } 2043 2044 _beginObservingNewtabPingPrefs() { 2045 Services.prefs.addObserver(ACTIVITY_STREAM_PREF_BRANCH, this); 2046 2047 for (const pref of Object.keys(NEWTAB_PING_PREFS)) { 2048 const fullPrefName = ACTIVITY_STREAM_PREF_BRANCH + pref; 2049 this._setNewtabPrefMetrics(fullPrefName, false); 2050 } 2051 2052 Services.prefs.addObserver(TOP_SITES_BLOCKED_SPONSORS_PREF, this); 2053 this._setBlockedSponsorsMetrics(); 2054 2055 Services.prefs.addObserver(TOPIC_SELECTION_SELECTED_TOPICS_PREF, this); 2056 this._setTopicSelectionSelectedTopicsMetrics(); 2057 } 2058 2059 _stopObservingNewtabPingPrefs() { 2060 Services.prefs.removeObserver(ACTIVITY_STREAM_PREF_BRANCH, this); 2061 Services.prefs.removeObserver(TOP_SITES_BLOCKED_SPONSORS_PREF, this); 2062 Services.prefs.removeObserver(TOPIC_SELECTION_SELECTED_TOPICS_PREF, this); 2063 } 2064 2065 observe(subject, topic, data) { 2066 if (data === TOP_SITES_BLOCKED_SPONSORS_PREF) { 2067 this._setBlockedSponsorsMetrics(); 2068 } else if (data === TOPIC_SELECTION_SELECTED_TOPICS_PREF) { 2069 this._setTopicSelectionSelectedTopicsMetrics(); 2070 } else { 2071 this._setNewtabPrefMetrics(data, true); 2072 } 2073 } 2074 2075 async _setNewtabPrefMetrics(fullPrefName, isChanged) { 2076 const pref = fullPrefName.slice(ACTIVITY_STREAM_PREF_BRANCH.length); 2077 if (!Object.hasOwn(NEWTAB_PING_PREFS, pref)) { 2078 return; 2079 } 2080 const metric = NEWTAB_PING_PREFS[pref]; 2081 switch (Services.prefs.getPrefType(fullPrefName)) { 2082 case Services.prefs.PREF_BOOL: 2083 metric.set(Services.prefs.getBoolPref(fullPrefName)); 2084 break; 2085 2086 case Services.prefs.PREF_INT: 2087 metric.set(Services.prefs.getIntPref(fullPrefName)); 2088 break; 2089 } 2090 if (isChanged) { 2091 switch (fullPrefName) { 2092 case `${ACTIVITY_STREAM_PREF_BRANCH}feeds.topsites`: 2093 case `${ACTIVITY_STREAM_PREF_BRANCH}${PREF_SHOW_SPONSORED_TOPSITES}`: 2094 Glean.topsites.prefChanged.record({ 2095 pref_name: fullPrefName, 2096 new_value: Services.prefs.getBoolPref(fullPrefName), 2097 }); 2098 break; 2099 } 2100 } 2101 } 2102 2103 _setBlockedSponsorsMetrics() { 2104 let blocklist; 2105 try { 2106 blocklist = JSON.parse( 2107 Services.prefs.getStringPref(TOP_SITES_BLOCKED_SPONSORS_PREF, "[]") 2108 ); 2109 } catch (e) {} 2110 if (blocklist) { 2111 Glean.newtab.blockedSponsors.set(blocklist); 2112 } 2113 } 2114 2115 _setTopicSelectionSelectedTopicsMetrics() { 2116 let topiclist; 2117 try { 2118 topiclist = Services.prefs.getStringPref( 2119 TOPIC_SELECTION_SELECTED_TOPICS_PREF, 2120 "" 2121 ); 2122 } catch (e) {} 2123 if (topiclist) { 2124 // Note: Beacuse Glean is expecting a string list, the 2125 // value of the pref needs to be converted to an array 2126 topiclist = topiclist.split(",").map(s => s.trim()); 2127 Glean.newtab.selectedTopics.set(topiclist); 2128 } 2129 } 2130 2131 uninit() { 2132 this._stopObservingNewtabPingPrefs(); 2133 this.newtabContentPing.uninit(); 2134 if (this._initialized) { 2135 Services.obs.removeObserver( 2136 this.browserOpenNewtabStart, 2137 "browser-open-newtab-start" 2138 ); 2139 this._initialized = false; 2140 } 2141 2142 // TODO: Send any unfinished sessions 2143 } 2144 }