123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925 |
- /* 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/. */
- /* globals Services */
- "use strict";
- const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
- const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
- const {actionTypes: at, actionUtils: au} = ChromeUtils.import("resource://activity-stream/common/Actions.jsm");
- const {Prefs} = ChromeUtils.import("resource://activity-stream/lib/ActivityStreamPrefs.jsm");
- const {classifySite} = ChromeUtils.import("resource://activity-stream/lib/SiteClassifier.jsm");
- ChromeUtils.defineModuleGetter(this, "ASRouterPreferences",
- "resource://activity-stream/lib/ASRouterPreferences.jsm");
- ChromeUtils.defineModuleGetter(this, "perfService",
- "resource://activity-stream/common/PerfService.jsm");
- ChromeUtils.defineModuleGetter(this, "PingCentre",
- "resource:///modules/PingCentre.jsm");
- ChromeUtils.defineModuleGetter(this, "UTEventReporting",
- "resource://activity-stream/lib/UTEventReporting.jsm");
- ChromeUtils.defineModuleGetter(this, "UpdateUtils",
- "resource://gre/modules/UpdateUtils.jsm");
- ChromeUtils.defineModuleGetter(this, "HomePage",
- "resource:///modules/HomePage.jsm");
- ChromeUtils.defineModuleGetter(this, "ExtensionSettingsStore",
- "resource://gre/modules/ExtensionSettingsStore.jsm");
- ChromeUtils.defineModuleGetter(this, "PrivateBrowsingUtils",
- "resource://gre/modules/PrivateBrowsingUtils.jsm");
- XPCOMUtils.defineLazyServiceGetters(this, {
- gUUIDGenerator: ["@mozilla.org/uuid-generator;1", "nsIUUIDGenerator"],
- aboutNewTabService: ["@mozilla.org/browser/aboutnewtab-service;1", "nsIAboutNewTabService"],
- });
- const ACTIVITY_STREAM_ID = "activity-stream";
- const ACTIVITY_STREAM_ENDPOINT_PREF = "browser.newtabpage.activity-stream.telemetry.ping.endpoint";
- const ACTIVITY_STREAM_ROUTER_ID = "activity-stream-router";
- const DOMWINDOW_OPENED_TOPIC = "domwindowopened";
- const DOMWINDOW_UNLOAD_TOPIC = "unload";
- const TAB_PINNED_EVENT = "TabPinned";
- // This is a mapping table between the user preferences and its encoding code
- const USER_PREFS_ENCODING = {
- "showSearch": 1 << 0,
- "feeds.topsites": 1 << 1,
- "feeds.section.topstories": 1 << 2,
- "feeds.section.highlights": 1 << 3,
- "feeds.snippets": 1 << 4,
- "showSponsored": 1 << 5,
- "asrouter.userprefs.cfr.addons": 1 << 6,
- "asrouter.userprefs.cfr.features": 1 << 7,
- };
- const PREF_IMPRESSION_ID = "impressionId";
- const TELEMETRY_PREF = "telemetry";
- const EVENTS_TELEMETRY_PREF = "telemetry.ut.events";
- const STRUCTURED_INGESTION_TELEMETRY_PREF = "telemetry.structuredIngestion";
- const STRUCTURED_INGESTION_ENDPOINT_PREF = "telemetry.structuredIngestion.endpoint";
- this.TelemetryFeed = class TelemetryFeed {
- constructor(options) {
- this.sessions = new Map();
- this._prefs = new Prefs();
- this._impressionId = this.getOrCreateImpressionId();
- this._aboutHomeSeen = false;
- this._classifySite = classifySite;
- this._addWindowListeners = this._addWindowListeners.bind(this);
- this.handleEvent = this.handleEvent.bind(this);
- }
- get telemetryEnabled() {
- return this._prefs.get(TELEMETRY_PREF);
- }
- get eventTelemetryEnabled() {
- return this._prefs.get(EVENTS_TELEMETRY_PREF);
- }
- get structuredIngestionTelemetryEnabled() {
- return this._prefs.get(STRUCTURED_INGESTION_TELEMETRY_PREF);
- }
- get structuredIngestionEndpointBase() {
- return this._prefs.get(STRUCTURED_INGESTION_ENDPOINT_PREF);
- }
- init() {
- Services.obs.addObserver(this.browserOpenNewtabStart, "browser-open-newtab-start");
- // Add pin tab event listeners on future windows
- Services.obs.addObserver(this._addWindowListeners, DOMWINDOW_OPENED_TOPIC);
- // Listen for pin tab events on all open windows
- for (let win of Services.wm.getEnumerator("navigator:browser")) {
- this._addWindowListeners(win);
- }
- }
- handleEvent(event) {
- switch (event.type) {
- case TAB_PINNED_EVENT:
- this.countPinnedTab(event.target);
- break;
- case DOMWINDOW_UNLOAD_TOPIC:
- this._removeWindowListeners(event.target);
- break;
- }
- }
- _removeWindowListeners(win) {
- win.removeEventListener(DOMWINDOW_UNLOAD_TOPIC, this.handleEvent);
- win.removeEventListener(TAB_PINNED_EVENT, this.handleEvent);
- }
- _addWindowListeners(win) {
- win.addEventListener(DOMWINDOW_UNLOAD_TOPIC, this.handleEvent);
- win.addEventListener(TAB_PINNED_EVENT, this.handleEvent);
- }
- countPinnedTab(target, source = "TAB_CONTEXT_MENU") {
- const win = target.ownerGlobal;
- if (PrivateBrowsingUtils.isWindowPrivate(win)) {
- return;
- }
- const event = Object.assign(
- this.createPing(),
- {
- action: "activity_stream_user_event",
- event: TAB_PINNED_EVENT.toUpperCase(),
- value: {total_pinned_tabs: this.countTotalPinnedTabs()},
- source,
- // These fields are required but not relevant for this ping
- page: "n/a",
- session_id: "n/a",
- },
- );
- this.sendEvent(event);
- }
- countTotalPinnedTabs() {
- let pinnedTabs = 0;
- for (let win of Services.wm.getEnumerator("navigator:browser")) {
- if (win.closed || PrivateBrowsingUtils.isWindowPrivate(win)) {
- continue;
- }
- for (let tab of win.gBrowser.tabs) {
- pinnedTabs += tab.pinned ? 1 : 0;
- }
- }
- return pinnedTabs;
- }
- getOrCreateImpressionId() {
- let impressionId = this._prefs.get(PREF_IMPRESSION_ID);
- if (!impressionId) {
- impressionId = String(gUUIDGenerator.generateUUID());
- this._prefs.set(PREF_IMPRESSION_ID, impressionId);
- }
- return impressionId;
- }
- browserOpenNewtabStart() {
- perfService.mark("browser-open-newtab-start");
- }
- setLoadTriggerInfo(port) {
- // XXX note that there is a race condition here; we're assuming that no
- // other tab will be interleaving calls to browserOpenNewtabStart and
- // when at.NEW_TAB_INIT gets triggered by RemotePages and calls this
- // method. For manually created windows, it's hard to imagine us hitting
- // this race condition.
- //
- // However, for session restore, where multiple windows with multiple tabs
- // might be restored much closer together in time, it's somewhat less hard,
- // though it should still be pretty rare.
- //
- // The fix to this would be making all of the load-trigger notifications
- // return some data with their notifications, and somehow propagate that
- // data through closures into the tab itself so that we could match them
- //
- // As of this writing (very early days of system add-on perf telemetry),
- // the hypothesis is that hitting this race should be so rare that makes
- // more sense to live with the slight data inaccuracy that it would
- // introduce, rather than doing the correct but complicated thing. It may
- // well be worth reexamining this hypothesis after we have more experience
- // with the data.
- let data_to_save;
- try {
- data_to_save = {
- load_trigger_ts: perfService.getMostRecentAbsMarkStartByName("browser-open-newtab-start"),
- load_trigger_type: "menu_plus_or_keyboard",
- };
- } catch (e) {
- // if no mark was returned, we have nothing to save
- return;
- }
- this.saveSessionPerfData(port, data_to_save);
- }
- /**
- * Lazily initialize PingCentre for Activity Stream to send pings
- */
- get pingCentre() {
- Object.defineProperty(this, "pingCentre",
- {
- value: new PingCentre({
- topic: ACTIVITY_STREAM_ID,
- overrideEndpointPref: ACTIVITY_STREAM_ENDPOINT_PREF,
- }),
- });
- return this.pingCentre;
- }
- /**
- * Lazily initialize a PingCentre client for Activity Stream Router to send pings.
- *
- * Unlike the PingCentre client for Activity Stream, Activity Stream Router
- * uses a separate client with the standard PingCentre endpoint.
- */
- get pingCentreForASRouter() {
- Object.defineProperty(this, "pingCentreForASRouter",
- {value: new PingCentre({topic: ACTIVITY_STREAM_ROUTER_ID})});
- return this.pingCentreForASRouter;
- }
- /**
- * Lazily initialize UTEventReporting to send pings
- */
- get utEvents() {
- Object.defineProperty(this, "utEvents", {value: new UTEventReporting()});
- return this.utEvents;
- }
- /**
- * Get encoded user preferences, multiple prefs will be combined via bitwise OR operator
- */
- get userPreferences() {
- let prefs = 0;
- for (const pref of Object.keys(USER_PREFS_ENCODING)) {
- if (this._prefs.get(pref)) {
- prefs |= USER_PREFS_ENCODING[pref];
- }
- }
- return prefs;
- }
- /**
- * Check if it is in the CFR experiment cohort. ASRouterPreferences lazily parses AS router pref.
- */
- get isInCFRCohort() {
- for (let provider of ASRouterPreferences.providers) {
- if (provider.id === "cfr" && provider.enabled && provider.cohort) {
- return true;
- }
- }
- return false;
- }
- /**
- * addSession - Start tracking a new session
- *
- * @param {string} id the portID of the open session
- * @param {string} the URL being loaded for this session (optional)
- * @return {obj} Session object
- */
- addSession(id, url) {
- // XXX refactor to use setLoadTriggerInfo or saveSessionPerfData
- // "unexpected" will be overwritten when appropriate
- let load_trigger_type = "unexpected";
- let load_trigger_ts;
- if (!this._aboutHomeSeen && url === "about:home") {
- this._aboutHomeSeen = true;
- // XXX note that this will be incorrectly set in the following cases:
- // session_restore following by clicking on the toolbar button,
- // or someone who has changed their default home page preference to
- // something else and later clicks the toolbar. It will also be
- // incorrectly unset if someone changes their "Home Page" preference to
- // about:newtab.
- //
- // That said, the ratio of these mistakes to correct cases should
- // be very small, and these issues should follow away as we implement
- // the remaining load_trigger_type values for about:home in issue 3556.
- //
- // XXX file a bug to implement remaining about:home cases so this
- // problem will go away and link to it here.
- load_trigger_type = "first_window_opened";
- // The real perceived trigger of first_window_opened is the OS-level
- // clicking of the icon. We use perfService.timeOrigin because it's the
- // earliest number on this time scale that's easy to get.; We could
- // actually use 0, but maybe that could be before the browser started?
- // [bug 1401406](https://bugzilla.mozilla.org/show_bug.cgi?id=1401406)
- // getting sorted out may help clarify. Even better, presumably, would be
- // to use the process creation time for the main process, which is
- // available, but somewhat harder to get. However, these are all more or
- // less proxies for the same thing, so it's not clear how much the better
- // numbers really matter, since we (activity stream) only control a
- // relatively small amount of the code that's executing between the
- // OS-click and when the first <browser> element starts loading. That
- // said, it's conceivable that it could help us catch regressions in the
- // number of cycles early chrome code takes to execute, but it's likely
- // that there are more direct ways to measure that.
- load_trigger_ts = perfService.timeOrigin;
- }
- const session = {
- session_id: String(gUUIDGenerator.generateUUID()),
- // "unknown" will be overwritten when appropriate
- page: url ? url : "unknown",
- perf: {
- load_trigger_type,
- is_preloaded: false,
- },
- };
- if (load_trigger_ts) {
- session.perf.load_trigger_ts = load_trigger_ts;
- }
- this.sessions.set(id, session);
- return session;
- }
- /**
- * endSession - Stop tracking a session
- *
- * @param {string} portID the portID of the session that just closed
- */
- endSession(portID) {
- const session = this.sessions.get(portID);
- if (!session) {
- // It's possible the tab was never visible – in which case, there was no user session.
- return;
- }
- this.sendDiscoveryStreamLoadedContent(portID, session);
- this.sendDiscoveryStreamImpressions(portID, session);
- if (session.perf.visibility_event_rcvd_ts) {
- session.session_duration = Math.round(perfService.absNow() - session.perf.visibility_event_rcvd_ts);
- } else {
- // This session was never shown (i.e. the hidden preloaded newtab), there was no user session either.
- this.sessions.delete(portID);
- return;
- }
- let sessionEndEvent = this.createSessionEndEvent(session);
- this.sendEvent(sessionEndEvent);
- this.sendUTEvent(sessionEndEvent, this.utEvents.sendSessionEndEvent);
- this.sessions.delete(portID);
- }
- /**
- * Send impression pings for Discovery Stream for a given session.
- *
- * @note the impression reports are stored in session.impressionSets for different
- * sources, and will be sent separately accordingly.
- *
- * @param {String} port The session port with which this is associated
- * @param {Object} session The session object
- */
- sendDiscoveryStreamImpressions(port, session) {
- const {impressionSets} = session;
- if (!impressionSets) {
- return;
- }
- Object.keys(impressionSets).forEach(source => {
- const payload = this.createImpressionStats(port, {source, tiles: impressionSets[source]});
- this.sendEvent(payload);
- this.sendStructuredIngestionEvent(payload, "impression-stats", "1");
- });
- }
- /**
- * Send loaded content pings for Discovery Stream for a given session.
- *
- * @note the loaded content reports are stored in session.loadedContentSets for different
- * sources, and will be sent separately accordingly.
- *
- * @param {String} port The session port with which this is associated
- * @param {Object} session The session object
- */
- sendDiscoveryStreamLoadedContent(port, session) {
- const {loadedContentSets} = session;
- if (!loadedContentSets) {
- return;
- }
- Object.keys(loadedContentSets).forEach(source => {
- const tiles = loadedContentSets[source];
- const payload = this.createImpressionStats(port, {
- source,
- tiles,
- loaded: tiles.length,
- });
- this.sendEvent(payload);
- this.sendStructuredIngestionEvent(payload, "impression-stats", "1");
- });
- }
- /**
- * handleNewTabInit - Handle NEW_TAB_INIT, which creates a new session and sets the a flag
- * for session.perf based on whether or not this new tab is preloaded
- *
- * @param {obj} action the Action object
- */
- handleNewTabInit(action) {
- const session = this.addSession(au.getPortIdOfSender(action), action.data.url);
- session.perf.is_preloaded = action.data.browser.getAttribute("preloadedState") === "preloaded";
- }
- /**
- * createPing - Create a ping with common properties
- *
- * @param {string} id The portID of the session, if a session is relevant (optional)
- * @return {obj} A telemetry ping
- */
- createPing(portID) {
- const ping = {
- addon_version: Services.appinfo.appBuildID,
- locale: Services.locale.appLocaleAsLangTag,
- user_prefs: this.userPreferences,
- };
- // If the ping is part of a user session, add session-related info
- if (portID) {
- const session = this.sessions.get(portID) || this.addSession(portID);
- Object.assign(ping, {session_id: session.session_id});
- if (session.page) {
- Object.assign(ping, {page: session.page});
- }
- }
- return ping;
- }
- /**
- * createImpressionStats - Create a ping for an impression stats
- *
- * @param {string} portID The portID of the open session
- * @param {ob} data The data object to be included in the ping.
- * @return {obj} A telemetry ping
- */
- createImpressionStats(portID, data) {
- return Object.assign(
- this.createPing(portID),
- data,
- {
- action: "activity_stream_impression_stats",
- impression_id: this._impressionId,
- client_id: "n/a",
- session_id: "n/a",
- }
- );
- }
- createSpocsFillPing(data) {
- return Object.assign(
- this.createPing(null),
- data,
- {
- impression_id: this._impressionId,
- client_id: "n/a",
- session_id: "n/a",
- }
- );
- }
- createUserEvent(action) {
- return Object.assign(
- this.createPing(au.getPortIdOfSender(action)),
- action.data,
- {action: "activity_stream_user_event"}
- );
- }
- createUndesiredEvent(action) {
- return Object.assign(
- this.createPing(au.getPortIdOfSender(action)),
- {value: 0}, // Default value
- action.data,
- {action: "activity_stream_undesired_event"}
- );
- }
- createPerformanceEvent(action) {
- return Object.assign(
- this.createPing(),
- action.data,
- {action: "activity_stream_performance_event"}
- );
- }
- createSessionEndEvent(session) {
- return Object.assign(
- this.createPing(),
- {
- session_id: session.session_id,
- page: session.page,
- session_duration: session.session_duration,
- action: "activity_stream_session",
- perf: session.perf,
- }
- );
- }
- /**
- * Create a ping for AS router event. The client_id is set to "n/a" by default,
- * different component can override this by its own telemetry collection policy.
- */
- createASRouterEvent(action) {
- const ping = {
- client_id: "n/a",
- addon_version: Services.appinfo.appBuildID,
- locale: Services.locale.appLocaleAsLangTag,
- impression_id: this._impressionId,
- };
- const event = Object.assign(ping, action.data);
- if (event.action === "cfr_user_event") {
- return this.applyCFRPolicy(event);
- } else if (event.action === "snippets_user_event") {
- return this.applySnippetsPolicy(event);
- } else if (event.action === "onboarding_user_event") {
- return this.applyOnboardingPolicy(event);
- }
- return event;
- }
- /**
- * Per Bug 1484035, CFR metrics comply with following policies:
- * 1). In release, it collects impression_id, and treats bucket_id as message_id
- * 2). In prerelease, it collects client_id and message_id
- * 3). In shield experiments conducted in release, it collects client_id and message_id
- */
- applyCFRPolicy(ping) {
- if (UpdateUtils.getUpdateChannel(true) === "release" && !this.isInCFRCohort) {
- ping.message_id = ping.bucket_id || "n/a";
- ping.client_id = "n/a";
- ping.impression_id = this._impressionId;
- } else {
- ping.impression_id = "n/a";
- // Ping-centre client will fill in the client_id if it's not provided in the ping.
- delete ping.client_id;
- }
- // bucket_id is no longer needed
- delete ping.bucket_id;
- return ping;
- }
- /**
- * Per Bug 1485069, all the metrics for Snippets in AS router use client_id in
- * all the release channels
- */
- applySnippetsPolicy(ping) {
- // Ping-centre client will fill in the client_id if it's not provided in the ping.
- delete ping.client_id;
- ping.impression_id = "n/a";
- return ping;
- }
- /**
- * Per Bug 1482134, all the metrics for Onboarding in AS router use client_id in
- * all the release channels
- */
- applyOnboardingPolicy(ping) {
- // Ping-centre client will fill in the client_id if it's not provided in the ping.
- delete ping.client_id;
- ping.impression_id = "n/a";
- return ping;
- }
- sendEvent(event_object) {
- if (this.telemetryEnabled) {
- this.pingCentre.sendPing(event_object,
- {filter: ACTIVITY_STREAM_ID});
- }
- }
- sendUTEvent(event_object, eventFunction) {
- if (this.telemetryEnabled && this.eventTelemetryEnabled) {
- eventFunction(event_object);
- }
- }
- /**
- * Generates an endpoint for Structured Ingestion telemetry pipeline. Note that
- * Structured Ingestion requires a different endpoint for each ping. See more
- * details about endpoint schema at:
- * https://github.com/mozilla/gcp-ingestion/blob/master/docs/edge.md#postput-request
- *
- * @param {String} pingType Type of the ping, such as "impression-stats".
- * @param {String} version Endpoint version for this ping type.
- */
- _generateStructuredIngestionEndpoint(pingType, version) {
- const uuid = gUUIDGenerator.generateUUID().toString();
- // Structured Ingestion does not support the UUID generated by gUUIDGenerator,
- // because it contains leading and trailing braces. Need to trim them first.
- const docID = uuid.slice(1, -1);
- const extension = `${pingType}/${version}/${docID}`;
- return `${this.structuredIngestionEndpointBase}/${extension}`;
- }
- sendStructuredIngestionEvent(event_object, pingType, version) {
- if (this.telemetryEnabled && this.structuredIngestionTelemetryEnabled) {
- this.pingCentre.sendStructuredIngestionPing(event_object,
- this._generateStructuredIngestionEndpoint(pingType, version),
- {filter: ACTIVITY_STREAM_ID});
- }
- }
- sendASRouterEvent(event_object) {
- if (this.telemetryEnabled) {
- this.pingCentreForASRouter.sendPing(event_object,
- {filter: ACTIVITY_STREAM_ID});
- }
- }
- handleImpressionStats(action) {
- const payload = this.createImpressionStats(au.getPortIdOfSender(action), action.data);
- this.sendEvent(payload);
- this.sendStructuredIngestionEvent(payload, "impression-stats", "1");
- }
- handleUserEvent(action) {
- let userEvent = this.createUserEvent(action);
- this.sendEvent(userEvent);
- this.sendUTEvent(userEvent, this.utEvents.sendUserEvent);
- }
- handleASRouterUserEvent(action) {
- let event = this.createASRouterEvent(action);
- this.sendASRouterEvent(event);
- }
- handleUndesiredEvent(action) {
- this.sendEvent(this.createUndesiredEvent(action));
- }
- handleTrailheadEnrollEvent(action) {
- // Unlike `sendUTEvent`, we always send the event if AS's telemetry is enabled
- // regardless of `this.eventTelemetryEnabled`.
- if (this.telemetryEnabled) {
- this.utEvents.sendTrailheadEnrollEvent(action.data);
- }
- }
- async sendPageTakeoverData() {
- if (this.telemetryEnabled) {
- const value = {};
- let newtabAffected = false;
- let homeAffected = false;
- // Check whether or not about:home and about:newtab are set to a custom URL.
- // If so, classify them.
- if (Services.prefs.getBoolPref("browser.newtabpage.enabled") &&
- aboutNewTabService.overridden &&
- !aboutNewTabService.newTabURL.startsWith("moz-extension://")) {
- value.newtab_url_category = await this._classifySite(aboutNewTabService.newTabURL);
- newtabAffected = true;
- }
- // Check if the newtab page setting is controlled by an extension.
- await ExtensionSettingsStore.initialize();
- const newtabExtensionInfo = ExtensionSettingsStore.getSetting("url_overrides", "newTabURL");
- if (newtabExtensionInfo && newtabExtensionInfo.id) {
- value.newtab_extension_id = newtabExtensionInfo.id;
- newtabAffected = true;
- }
- const homePageURL = HomePage.get();
- if (!["about:home", "about:blank"].includes(homePageURL) &&
- !homePageURL.startsWith("moz-extension://")) {
- value.home_url_category = await this._classifySite(homePageURL);
- homeAffected = true;
- }
- const homeExtensionInfo = ExtensionSettingsStore.getSetting("prefs", "homepage_override");
- if (homeExtensionInfo && homeExtensionInfo.id) {
- value.home_extension_id = homeExtensionInfo.id;
- homeAffected = true;
- }
- let page;
- if (newtabAffected && homeAffected) {
- page = "both";
- } else if (newtabAffected) {
- page = "about:newtab";
- } else if (homeAffected) {
- page = "about:home";
- }
- if (page) {
- const event = Object.assign(
- this.createPing(),
- {
- action: "activity_stream_user_event",
- event: "PAGE_TAKEOVER_DATA",
- value,
- page,
- session_id: "n/a",
- },
- );
- this.sendEvent(event);
- }
- }
- }
- onAction(action) {
- switch (action.type) {
- case at.INIT:
- this.init();
- this.sendPageTakeoverData();
- break;
- case at.NEW_TAB_INIT:
- this.handleNewTabInit(action);
- break;
- case at.NEW_TAB_UNLOAD:
- this.endSession(au.getPortIdOfSender(action));
- break;
- case at.SAVE_SESSION_PERF_DATA:
- this.saveSessionPerfData(au.getPortIdOfSender(action), action.data);
- break;
- case at.TELEMETRY_IMPRESSION_STATS:
- this.handleImpressionStats(action);
- break;
- case at.DISCOVERY_STREAM_IMPRESSION_STATS:
- this.handleDiscoveryStreamImpressionStats(au.getPortIdOfSender(action), action.data);
- break;
- case at.DISCOVERY_STREAM_LOADED_CONTENT:
- this.handleDiscoveryStreamLoadedContent(au.getPortIdOfSender(action), action.data);
- break;
- case at.DISCOVERY_STREAM_SPOCS_FILL:
- this.handleDiscoveryStreamSpocsFill(action.data);
- break;
- case at.TELEMETRY_UNDESIRED_EVENT:
- this.handleUndesiredEvent(action);
- break;
- case at.TELEMETRY_USER_EVENT:
- this.handleUserEvent(action);
- break;
- case at.AS_ROUTER_TELEMETRY_USER_EVENT:
- this.handleASRouterUserEvent(action);
- break;
- case at.TELEMETRY_PERFORMANCE_EVENT:
- this.sendEvent(this.createPerformanceEvent(action));
- break;
- case at.TRAILHEAD_ENROLL_EVENT:
- this.handleTrailheadEnrollEvent(action);
- break;
- case at.UNINIT:
- this.uninit();
- break;
- }
- }
- /**
- * Handle impression stats actions from Discovery Stream. The data will be
- * stored into the session.impressionSets object for the given port, so that
- * it is sent to the server when the session ends.
- *
- * @note session.impressionSets will be keyed on `source` of the `data`,
- * all the data will be appended to an array for the same source.
- *
- * @param {String} port The session port with which this is associated
- * @param {Object} data The impression data structured as {source: "SOURCE", tiles: [{id: 123}]}
- *
- */
- handleDiscoveryStreamImpressionStats(port, data) {
- let session = this.sessions.get(port);
- if (!session) {
- throw new Error("Session does not exist.");
- }
- const impressionSets = session.impressionSets || {};
- const impressions = impressionSets[data.source] || [];
- // The payload might contain other properties, we need `id` and `pos` here.
- data.tiles.forEach(tile => impressions.push({id: tile.id, pos: tile.pos}));
- impressionSets[data.source] = impressions;
- session.impressionSets = impressionSets;
- }
- /**
- * Handle loaded content actions from Discovery Stream. The data will be
- * stored into the session.loadedContentSets object for the given port, so that
- * it is sent to the server when the session ends.
- *
- * @note session.loadedContentSets will be keyed on `source` of the `data`,
- * all the data will be appended to an array for the same source.
- *
- * @param {String} port The session port with which this is associated
- * @param {Object} data The loaded content structured as {source: "SOURCE", tiles: [{id: 123}]}
- *
- */
- handleDiscoveryStreamLoadedContent(port, data) {
- let session = this.sessions.get(port);
- if (!session) {
- throw new Error("Session does not exist.");
- }
- const loadedContentSets = session.loadedContentSets || {};
- const loadedContents = loadedContentSets[data.source] || [];
- // The payload might contain other properties, we need `id` and `pos` here.
- data.tiles.forEach(tile => loadedContents.push({id: tile.id, pos: tile.pos}));
- loadedContentSets[data.source] = loadedContents;
- session.loadedContentSets = loadedContentSets;
- }
- /**
- * Handl SPOCS Fill actions from Discovery Stream.
- *
- * @param {Object} data
- * The SPOCS Fill event structured as:
- * {
- * spoc_fills: [
- * {
- * id: 123,
- * displayed: 0,
- * reason: "frequency_cap",
- * full_recalc: 1
- * },
- * {
- * id: 124,
- * displayed: 1,
- * reason: "n/a",
- * full_recalc: 1
- * }
- * ]
- * }
- */
- handleDiscoveryStreamSpocsFill(data) {
- const payload = this.createSpocsFillPing(data);
- this.sendStructuredIngestionEvent(payload, "spoc-fills", "1");
- }
- /**
- * Take all enumerable members of the data object and merge them into
- * the session.perf object for the given port, so that it is sent to the
- * server when the session ends. All members of the data object should
- * be valid values of the perf object, as defined in pings.js and the
- * data*.md documentation.
- *
- * @note Any existing keys with the same names already in the
- * session perf object will be overwritten by values passed in here.
- *
- * @param {String} port The session with which this is associated
- * @param {Object} data The perf data to be
- */
- saveSessionPerfData(port, data) {
- // XXX should use try/catch and send a bad state indicator if this
- // get blows up.
- let session = this.sessions.get(port);
- // XXX Partial workaround for #3118; avoids the worst incorrect associations
- // of times with browsers, by associating the load trigger with the
- // visibility event as the user is most likely associating the trigger to
- // the tab just shown. This helps avoid associating with a preloaded
- // browser as those don't get the event until shown. Better fix for more
- // cases forthcoming.
- //
- // XXX the about:home check (and the corresponding test) should go away
- // once the load_trigger stuff in addSession is refactored into
- // setLoadTriggerInfo.
- //
- if (data.visibility_event_rcvd_ts && session.page !== "about:home") {
- this.setLoadTriggerInfo(port);
- }
- let timestamp = data.topsites_first_painted_ts;
- if (timestamp &&
- session.page === "about:home" &&
- !HomePage.overridden &&
- Services.prefs.getIntPref("browser.startup.page") === 1) {
- aboutNewTabService.maybeRecordTopsitesPainted(timestamp);
- }
- Object.assign(session.perf, data);
- }
- uninit() {
- try {
- Services.obs.removeObserver(this.browserOpenNewtabStart,
- "browser-open-newtab-start");
- Services.obs.removeObserver(this._addWindowListeners,
- DOMWINDOW_OPENED_TOPIC);
- } catch (e) {
- // Operation can fail when uninit is called before
- // init has finished setting up the observer
- }
- // Only uninit if the getter has initialized it
- if (Object.prototype.hasOwnProperty.call(this, "pingCentre")) {
- this.pingCentre.uninit();
- }
- if (Object.prototype.hasOwnProperty.call(this, "utEvents")) {
- this.utEvents.uninit();
- }
- if (Object.prototype.hasOwnProperty.call(this, "pingCentreForASRouter")) {
- this.pingCentreForASRouter.uninit();
- }
- // TODO: Send any unfinished sessions
- }
- };
- const EXPORTED_SYMBOLS = [
- "TelemetryFeed",
- "USER_PREFS_ENCODING",
- "PREF_IMPRESSION_ID",
- "TELEMETRY_PREF",
- "EVENTS_TELEMETRY_PREF",
- "STRUCTURED_INGESTION_TELEMETRY_PREF",
- "STRUCTURED_INGESTION_ENDPOINT_PREF",
- ];
|