зеркало из https://github.com/mozilla/gecko-dev.git
996 строки
31 KiB
JavaScript
996 строки
31 KiB
JavaScript
/* 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/. */
|
|
|
|
"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`, `pos` and potentially `shim` here.
|
|
data.tiles.forEach(tile =>
|
|
impressions.push({
|
|
id: tile.id,
|
|
pos: tile.pos,
|
|
...(tile.shim ? { shim: tile.shim } : {}),
|
|
})
|
|
);
|
|
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",
|
|
];
|