TelemetryFeed.jsm 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925
  1. /* This Source Code Form is subject to the terms of the Mozilla Public
  2. * License, v. 2.0. If a copy of the MPL was not distributed with this
  3. * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
  4. /* globals Services */
  5. "use strict";
  6. const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
  7. const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
  8. const {actionTypes: at, actionUtils: au} = ChromeUtils.import("resource://activity-stream/common/Actions.jsm");
  9. const {Prefs} = ChromeUtils.import("resource://activity-stream/lib/ActivityStreamPrefs.jsm");
  10. const {classifySite} = ChromeUtils.import("resource://activity-stream/lib/SiteClassifier.jsm");
  11. ChromeUtils.defineModuleGetter(this, "ASRouterPreferences",
  12. "resource://activity-stream/lib/ASRouterPreferences.jsm");
  13. ChromeUtils.defineModuleGetter(this, "perfService",
  14. "resource://activity-stream/common/PerfService.jsm");
  15. ChromeUtils.defineModuleGetter(this, "PingCentre",
  16. "resource:///modules/PingCentre.jsm");
  17. ChromeUtils.defineModuleGetter(this, "UTEventReporting",
  18. "resource://activity-stream/lib/UTEventReporting.jsm");
  19. ChromeUtils.defineModuleGetter(this, "UpdateUtils",
  20. "resource://gre/modules/UpdateUtils.jsm");
  21. ChromeUtils.defineModuleGetter(this, "HomePage",
  22. "resource:///modules/HomePage.jsm");
  23. ChromeUtils.defineModuleGetter(this, "ExtensionSettingsStore",
  24. "resource://gre/modules/ExtensionSettingsStore.jsm");
  25. ChromeUtils.defineModuleGetter(this, "PrivateBrowsingUtils",
  26. "resource://gre/modules/PrivateBrowsingUtils.jsm");
  27. XPCOMUtils.defineLazyServiceGetters(this, {
  28. gUUIDGenerator: ["@mozilla.org/uuid-generator;1", "nsIUUIDGenerator"],
  29. aboutNewTabService: ["@mozilla.org/browser/aboutnewtab-service;1", "nsIAboutNewTabService"],
  30. });
  31. const ACTIVITY_STREAM_ID = "activity-stream";
  32. const ACTIVITY_STREAM_ENDPOINT_PREF = "browser.newtabpage.activity-stream.telemetry.ping.endpoint";
  33. const ACTIVITY_STREAM_ROUTER_ID = "activity-stream-router";
  34. const DOMWINDOW_OPENED_TOPIC = "domwindowopened";
  35. const DOMWINDOW_UNLOAD_TOPIC = "unload";
  36. const TAB_PINNED_EVENT = "TabPinned";
  37. // This is a mapping table between the user preferences and its encoding code
  38. const USER_PREFS_ENCODING = {
  39. "showSearch": 1 << 0,
  40. "feeds.topsites": 1 << 1,
  41. "feeds.section.topstories": 1 << 2,
  42. "feeds.section.highlights": 1 << 3,
  43. "feeds.snippets": 1 << 4,
  44. "showSponsored": 1 << 5,
  45. "asrouter.userprefs.cfr.addons": 1 << 6,
  46. "asrouter.userprefs.cfr.features": 1 << 7,
  47. };
  48. const PREF_IMPRESSION_ID = "impressionId";
  49. const TELEMETRY_PREF = "telemetry";
  50. const EVENTS_TELEMETRY_PREF = "telemetry.ut.events";
  51. const STRUCTURED_INGESTION_TELEMETRY_PREF = "telemetry.structuredIngestion";
  52. const STRUCTURED_INGESTION_ENDPOINT_PREF = "telemetry.structuredIngestion.endpoint";
  53. this.TelemetryFeed = class TelemetryFeed {
  54. constructor(options) {
  55. this.sessions = new Map();
  56. this._prefs = new Prefs();
  57. this._impressionId = this.getOrCreateImpressionId();
  58. this._aboutHomeSeen = false;
  59. this._classifySite = classifySite;
  60. this._addWindowListeners = this._addWindowListeners.bind(this);
  61. this.handleEvent = this.handleEvent.bind(this);
  62. }
  63. get telemetryEnabled() {
  64. return this._prefs.get(TELEMETRY_PREF);
  65. }
  66. get eventTelemetryEnabled() {
  67. return this._prefs.get(EVENTS_TELEMETRY_PREF);
  68. }
  69. get structuredIngestionTelemetryEnabled() {
  70. return this._prefs.get(STRUCTURED_INGESTION_TELEMETRY_PREF);
  71. }
  72. get structuredIngestionEndpointBase() {
  73. return this._prefs.get(STRUCTURED_INGESTION_ENDPOINT_PREF);
  74. }
  75. init() {
  76. Services.obs.addObserver(this.browserOpenNewtabStart, "browser-open-newtab-start");
  77. // Add pin tab event listeners on future windows
  78. Services.obs.addObserver(this._addWindowListeners, DOMWINDOW_OPENED_TOPIC);
  79. // Listen for pin tab events on all open windows
  80. for (let win of Services.wm.getEnumerator("navigator:browser")) {
  81. this._addWindowListeners(win);
  82. }
  83. }
  84. handleEvent(event) {
  85. switch (event.type) {
  86. case TAB_PINNED_EVENT:
  87. this.countPinnedTab(event.target);
  88. break;
  89. case DOMWINDOW_UNLOAD_TOPIC:
  90. this._removeWindowListeners(event.target);
  91. break;
  92. }
  93. }
  94. _removeWindowListeners(win) {
  95. win.removeEventListener(DOMWINDOW_UNLOAD_TOPIC, this.handleEvent);
  96. win.removeEventListener(TAB_PINNED_EVENT, this.handleEvent);
  97. }
  98. _addWindowListeners(win) {
  99. win.addEventListener(DOMWINDOW_UNLOAD_TOPIC, this.handleEvent);
  100. win.addEventListener(TAB_PINNED_EVENT, this.handleEvent);
  101. }
  102. countPinnedTab(target, source = "TAB_CONTEXT_MENU") {
  103. const win = target.ownerGlobal;
  104. if (PrivateBrowsingUtils.isWindowPrivate(win)) {
  105. return;
  106. }
  107. const event = Object.assign(
  108. this.createPing(),
  109. {
  110. action: "activity_stream_user_event",
  111. event: TAB_PINNED_EVENT.toUpperCase(),
  112. value: {total_pinned_tabs: this.countTotalPinnedTabs()},
  113. source,
  114. // These fields are required but not relevant for this ping
  115. page: "n/a",
  116. session_id: "n/a",
  117. },
  118. );
  119. this.sendEvent(event);
  120. }
  121. countTotalPinnedTabs() {
  122. let pinnedTabs = 0;
  123. for (let win of Services.wm.getEnumerator("navigator:browser")) {
  124. if (win.closed || PrivateBrowsingUtils.isWindowPrivate(win)) {
  125. continue;
  126. }
  127. for (let tab of win.gBrowser.tabs) {
  128. pinnedTabs += tab.pinned ? 1 : 0;
  129. }
  130. }
  131. return pinnedTabs;
  132. }
  133. getOrCreateImpressionId() {
  134. let impressionId = this._prefs.get(PREF_IMPRESSION_ID);
  135. if (!impressionId) {
  136. impressionId = String(gUUIDGenerator.generateUUID());
  137. this._prefs.set(PREF_IMPRESSION_ID, impressionId);
  138. }
  139. return impressionId;
  140. }
  141. browserOpenNewtabStart() {
  142. perfService.mark("browser-open-newtab-start");
  143. }
  144. setLoadTriggerInfo(port) {
  145. // XXX note that there is a race condition here; we're assuming that no
  146. // other tab will be interleaving calls to browserOpenNewtabStart and
  147. // when at.NEW_TAB_INIT gets triggered by RemotePages and calls this
  148. // method. For manually created windows, it's hard to imagine us hitting
  149. // this race condition.
  150. //
  151. // However, for session restore, where multiple windows with multiple tabs
  152. // might be restored much closer together in time, it's somewhat less hard,
  153. // though it should still be pretty rare.
  154. //
  155. // The fix to this would be making all of the load-trigger notifications
  156. // return some data with their notifications, and somehow propagate that
  157. // data through closures into the tab itself so that we could match them
  158. //
  159. // As of this writing (very early days of system add-on perf telemetry),
  160. // the hypothesis is that hitting this race should be so rare that makes
  161. // more sense to live with the slight data inaccuracy that it would
  162. // introduce, rather than doing the correct but complicated thing. It may
  163. // well be worth reexamining this hypothesis after we have more experience
  164. // with the data.
  165. let data_to_save;
  166. try {
  167. data_to_save = {
  168. load_trigger_ts: perfService.getMostRecentAbsMarkStartByName("browser-open-newtab-start"),
  169. load_trigger_type: "menu_plus_or_keyboard",
  170. };
  171. } catch (e) {
  172. // if no mark was returned, we have nothing to save
  173. return;
  174. }
  175. this.saveSessionPerfData(port, data_to_save);
  176. }
  177. /**
  178. * Lazily initialize PingCentre for Activity Stream to send pings
  179. */
  180. get pingCentre() {
  181. Object.defineProperty(this, "pingCentre",
  182. {
  183. value: new PingCentre({
  184. topic: ACTIVITY_STREAM_ID,
  185. overrideEndpointPref: ACTIVITY_STREAM_ENDPOINT_PREF,
  186. }),
  187. });
  188. return this.pingCentre;
  189. }
  190. /**
  191. * Lazily initialize a PingCentre client for Activity Stream Router to send pings.
  192. *
  193. * Unlike the PingCentre client for Activity Stream, Activity Stream Router
  194. * uses a separate client with the standard PingCentre endpoint.
  195. */
  196. get pingCentreForASRouter() {
  197. Object.defineProperty(this, "pingCentreForASRouter",
  198. {value: new PingCentre({topic: ACTIVITY_STREAM_ROUTER_ID})});
  199. return this.pingCentreForASRouter;
  200. }
  201. /**
  202. * Lazily initialize UTEventReporting to send pings
  203. */
  204. get utEvents() {
  205. Object.defineProperty(this, "utEvents", {value: new UTEventReporting()});
  206. return this.utEvents;
  207. }
  208. /**
  209. * Get encoded user preferences, multiple prefs will be combined via bitwise OR operator
  210. */
  211. get userPreferences() {
  212. let prefs = 0;
  213. for (const pref of Object.keys(USER_PREFS_ENCODING)) {
  214. if (this._prefs.get(pref)) {
  215. prefs |= USER_PREFS_ENCODING[pref];
  216. }
  217. }
  218. return prefs;
  219. }
  220. /**
  221. * Check if it is in the CFR experiment cohort. ASRouterPreferences lazily parses AS router pref.
  222. */
  223. get isInCFRCohort() {
  224. for (let provider of ASRouterPreferences.providers) {
  225. if (provider.id === "cfr" && provider.enabled && provider.cohort) {
  226. return true;
  227. }
  228. }
  229. return false;
  230. }
  231. /**
  232. * addSession - Start tracking a new session
  233. *
  234. * @param {string} id the portID of the open session
  235. * @param {string} the URL being loaded for this session (optional)
  236. * @return {obj} Session object
  237. */
  238. addSession(id, url) {
  239. // XXX refactor to use setLoadTriggerInfo or saveSessionPerfData
  240. // "unexpected" will be overwritten when appropriate
  241. let load_trigger_type = "unexpected";
  242. let load_trigger_ts;
  243. if (!this._aboutHomeSeen && url === "about:home") {
  244. this._aboutHomeSeen = true;
  245. // XXX note that this will be incorrectly set in the following cases:
  246. // session_restore following by clicking on the toolbar button,
  247. // or someone who has changed their default home page preference to
  248. // something else and later clicks the toolbar. It will also be
  249. // incorrectly unset if someone changes their "Home Page" preference to
  250. // about:newtab.
  251. //
  252. // That said, the ratio of these mistakes to correct cases should
  253. // be very small, and these issues should follow away as we implement
  254. // the remaining load_trigger_type values for about:home in issue 3556.
  255. //
  256. // XXX file a bug to implement remaining about:home cases so this
  257. // problem will go away and link to it here.
  258. load_trigger_type = "first_window_opened";
  259. // The real perceived trigger of first_window_opened is the OS-level
  260. // clicking of the icon. We use perfService.timeOrigin because it's the
  261. // earliest number on this time scale that's easy to get.; We could
  262. // actually use 0, but maybe that could be before the browser started?
  263. // [bug 1401406](https://bugzilla.mozilla.org/show_bug.cgi?id=1401406)
  264. // getting sorted out may help clarify. Even better, presumably, would be
  265. // to use the process creation time for the main process, which is
  266. // available, but somewhat harder to get. However, these are all more or
  267. // less proxies for the same thing, so it's not clear how much the better
  268. // numbers really matter, since we (activity stream) only control a
  269. // relatively small amount of the code that's executing between the
  270. // OS-click and when the first <browser> element starts loading. That
  271. // said, it's conceivable that it could help us catch regressions in the
  272. // number of cycles early chrome code takes to execute, but it's likely
  273. // that there are more direct ways to measure that.
  274. load_trigger_ts = perfService.timeOrigin;
  275. }
  276. const session = {
  277. session_id: String(gUUIDGenerator.generateUUID()),
  278. // "unknown" will be overwritten when appropriate
  279. page: url ? url : "unknown",
  280. perf: {
  281. load_trigger_type,
  282. is_preloaded: false,
  283. },
  284. };
  285. if (load_trigger_ts) {
  286. session.perf.load_trigger_ts = load_trigger_ts;
  287. }
  288. this.sessions.set(id, session);
  289. return session;
  290. }
  291. /**
  292. * endSession - Stop tracking a session
  293. *
  294. * @param {string} portID the portID of the session that just closed
  295. */
  296. endSession(portID) {
  297. const session = this.sessions.get(portID);
  298. if (!session) {
  299. // It's possible the tab was never visible – in which case, there was no user session.
  300. return;
  301. }
  302. this.sendDiscoveryStreamLoadedContent(portID, session);
  303. this.sendDiscoveryStreamImpressions(portID, session);
  304. if (session.perf.visibility_event_rcvd_ts) {
  305. session.session_duration = Math.round(perfService.absNow() - session.perf.visibility_event_rcvd_ts);
  306. } else {
  307. // This session was never shown (i.e. the hidden preloaded newtab), there was no user session either.
  308. this.sessions.delete(portID);
  309. return;
  310. }
  311. let sessionEndEvent = this.createSessionEndEvent(session);
  312. this.sendEvent(sessionEndEvent);
  313. this.sendUTEvent(sessionEndEvent, this.utEvents.sendSessionEndEvent);
  314. this.sessions.delete(portID);
  315. }
  316. /**
  317. * Send impression pings for Discovery Stream for a given session.
  318. *
  319. * @note the impression reports are stored in session.impressionSets for different
  320. * sources, and will be sent separately accordingly.
  321. *
  322. * @param {String} port The session port with which this is associated
  323. * @param {Object} session The session object
  324. */
  325. sendDiscoveryStreamImpressions(port, session) {
  326. const {impressionSets} = session;
  327. if (!impressionSets) {
  328. return;
  329. }
  330. Object.keys(impressionSets).forEach(source => {
  331. const payload = this.createImpressionStats(port, {source, tiles: impressionSets[source]});
  332. this.sendEvent(payload);
  333. this.sendStructuredIngestionEvent(payload, "impression-stats", "1");
  334. });
  335. }
  336. /**
  337. * Send loaded content pings for Discovery Stream for a given session.
  338. *
  339. * @note the loaded content reports are stored in session.loadedContentSets for different
  340. * sources, and will be sent separately accordingly.
  341. *
  342. * @param {String} port The session port with which this is associated
  343. * @param {Object} session The session object
  344. */
  345. sendDiscoveryStreamLoadedContent(port, session) {
  346. const {loadedContentSets} = session;
  347. if (!loadedContentSets) {
  348. return;
  349. }
  350. Object.keys(loadedContentSets).forEach(source => {
  351. const tiles = loadedContentSets[source];
  352. const payload = this.createImpressionStats(port, {
  353. source,
  354. tiles,
  355. loaded: tiles.length,
  356. });
  357. this.sendEvent(payload);
  358. this.sendStructuredIngestionEvent(payload, "impression-stats", "1");
  359. });
  360. }
  361. /**
  362. * handleNewTabInit - Handle NEW_TAB_INIT, which creates a new session and sets the a flag
  363. * for session.perf based on whether or not this new tab is preloaded
  364. *
  365. * @param {obj} action the Action object
  366. */
  367. handleNewTabInit(action) {
  368. const session = this.addSession(au.getPortIdOfSender(action), action.data.url);
  369. session.perf.is_preloaded = action.data.browser.getAttribute("preloadedState") === "preloaded";
  370. }
  371. /**
  372. * createPing - Create a ping with common properties
  373. *
  374. * @param {string} id The portID of the session, if a session is relevant (optional)
  375. * @return {obj} A telemetry ping
  376. */
  377. createPing(portID) {
  378. const ping = {
  379. addon_version: Services.appinfo.appBuildID,
  380. locale: Services.locale.appLocaleAsLangTag,
  381. user_prefs: this.userPreferences,
  382. };
  383. // If the ping is part of a user session, add session-related info
  384. if (portID) {
  385. const session = this.sessions.get(portID) || this.addSession(portID);
  386. Object.assign(ping, {session_id: session.session_id});
  387. if (session.page) {
  388. Object.assign(ping, {page: session.page});
  389. }
  390. }
  391. return ping;
  392. }
  393. /**
  394. * createImpressionStats - Create a ping for an impression stats
  395. *
  396. * @param {string} portID The portID of the open session
  397. * @param {ob} data The data object to be included in the ping.
  398. * @return {obj} A telemetry ping
  399. */
  400. createImpressionStats(portID, data) {
  401. return Object.assign(
  402. this.createPing(portID),
  403. data,
  404. {
  405. action: "activity_stream_impression_stats",
  406. impression_id: this._impressionId,
  407. client_id: "n/a",
  408. session_id: "n/a",
  409. }
  410. );
  411. }
  412. createSpocsFillPing(data) {
  413. return Object.assign(
  414. this.createPing(null),
  415. data,
  416. {
  417. impression_id: this._impressionId,
  418. client_id: "n/a",
  419. session_id: "n/a",
  420. }
  421. );
  422. }
  423. createUserEvent(action) {
  424. return Object.assign(
  425. this.createPing(au.getPortIdOfSender(action)),
  426. action.data,
  427. {action: "activity_stream_user_event"}
  428. );
  429. }
  430. createUndesiredEvent(action) {
  431. return Object.assign(
  432. this.createPing(au.getPortIdOfSender(action)),
  433. {value: 0}, // Default value
  434. action.data,
  435. {action: "activity_stream_undesired_event"}
  436. );
  437. }
  438. createPerformanceEvent(action) {
  439. return Object.assign(
  440. this.createPing(),
  441. action.data,
  442. {action: "activity_stream_performance_event"}
  443. );
  444. }
  445. createSessionEndEvent(session) {
  446. return Object.assign(
  447. this.createPing(),
  448. {
  449. session_id: session.session_id,
  450. page: session.page,
  451. session_duration: session.session_duration,
  452. action: "activity_stream_session",
  453. perf: session.perf,
  454. }
  455. );
  456. }
  457. /**
  458. * Create a ping for AS router event. The client_id is set to "n/a" by default,
  459. * different component can override this by its own telemetry collection policy.
  460. */
  461. createASRouterEvent(action) {
  462. const ping = {
  463. client_id: "n/a",
  464. addon_version: Services.appinfo.appBuildID,
  465. locale: Services.locale.appLocaleAsLangTag,
  466. impression_id: this._impressionId,
  467. };
  468. const event = Object.assign(ping, action.data);
  469. if (event.action === "cfr_user_event") {
  470. return this.applyCFRPolicy(event);
  471. } else if (event.action === "snippets_user_event") {
  472. return this.applySnippetsPolicy(event);
  473. } else if (event.action === "onboarding_user_event") {
  474. return this.applyOnboardingPolicy(event);
  475. }
  476. return event;
  477. }
  478. /**
  479. * Per Bug 1484035, CFR metrics comply with following policies:
  480. * 1). In release, it collects impression_id, and treats bucket_id as message_id
  481. * 2). In prerelease, it collects client_id and message_id
  482. * 3). In shield experiments conducted in release, it collects client_id and message_id
  483. */
  484. applyCFRPolicy(ping) {
  485. if (UpdateUtils.getUpdateChannel(true) === "release" && !this.isInCFRCohort) {
  486. ping.message_id = ping.bucket_id || "n/a";
  487. ping.client_id = "n/a";
  488. ping.impression_id = this._impressionId;
  489. } else {
  490. ping.impression_id = "n/a";
  491. // Ping-centre client will fill in the client_id if it's not provided in the ping.
  492. delete ping.client_id;
  493. }
  494. // bucket_id is no longer needed
  495. delete ping.bucket_id;
  496. return ping;
  497. }
  498. /**
  499. * Per Bug 1485069, all the metrics for Snippets in AS router use client_id in
  500. * all the release channels
  501. */
  502. applySnippetsPolicy(ping) {
  503. // Ping-centre client will fill in the client_id if it's not provided in the ping.
  504. delete ping.client_id;
  505. ping.impression_id = "n/a";
  506. return ping;
  507. }
  508. /**
  509. * Per Bug 1482134, all the metrics for Onboarding in AS router use client_id in
  510. * all the release channels
  511. */
  512. applyOnboardingPolicy(ping) {
  513. // Ping-centre client will fill in the client_id if it's not provided in the ping.
  514. delete ping.client_id;
  515. ping.impression_id = "n/a";
  516. return ping;
  517. }
  518. sendEvent(event_object) {
  519. if (this.telemetryEnabled) {
  520. this.pingCentre.sendPing(event_object,
  521. {filter: ACTIVITY_STREAM_ID});
  522. }
  523. }
  524. sendUTEvent(event_object, eventFunction) {
  525. if (this.telemetryEnabled && this.eventTelemetryEnabled) {
  526. eventFunction(event_object);
  527. }
  528. }
  529. /**
  530. * Generates an endpoint for Structured Ingestion telemetry pipeline. Note that
  531. * Structured Ingestion requires a different endpoint for each ping. See more
  532. * details about endpoint schema at:
  533. * https://github.com/mozilla/gcp-ingestion/blob/master/docs/edge.md#postput-request
  534. *
  535. * @param {String} pingType Type of the ping, such as "impression-stats".
  536. * @param {String} version Endpoint version for this ping type.
  537. */
  538. _generateStructuredIngestionEndpoint(pingType, version) {
  539. const uuid = gUUIDGenerator.generateUUID().toString();
  540. // Structured Ingestion does not support the UUID generated by gUUIDGenerator,
  541. // because it contains leading and trailing braces. Need to trim them first.
  542. const docID = uuid.slice(1, -1);
  543. const extension = `${pingType}/${version}/${docID}`;
  544. return `${this.structuredIngestionEndpointBase}/${extension}`;
  545. }
  546. sendStructuredIngestionEvent(event_object, pingType, version) {
  547. if (this.telemetryEnabled && this.structuredIngestionTelemetryEnabled) {
  548. this.pingCentre.sendStructuredIngestionPing(event_object,
  549. this._generateStructuredIngestionEndpoint(pingType, version),
  550. {filter: ACTIVITY_STREAM_ID});
  551. }
  552. }
  553. sendASRouterEvent(event_object) {
  554. if (this.telemetryEnabled) {
  555. this.pingCentreForASRouter.sendPing(event_object,
  556. {filter: ACTIVITY_STREAM_ID});
  557. }
  558. }
  559. handleImpressionStats(action) {
  560. const payload = this.createImpressionStats(au.getPortIdOfSender(action), action.data);
  561. this.sendEvent(payload);
  562. this.sendStructuredIngestionEvent(payload, "impression-stats", "1");
  563. }
  564. handleUserEvent(action) {
  565. let userEvent = this.createUserEvent(action);
  566. this.sendEvent(userEvent);
  567. this.sendUTEvent(userEvent, this.utEvents.sendUserEvent);
  568. }
  569. handleASRouterUserEvent(action) {
  570. let event = this.createASRouterEvent(action);
  571. this.sendASRouterEvent(event);
  572. }
  573. handleUndesiredEvent(action) {
  574. this.sendEvent(this.createUndesiredEvent(action));
  575. }
  576. handleTrailheadEnrollEvent(action) {
  577. // Unlike `sendUTEvent`, we always send the event if AS's telemetry is enabled
  578. // regardless of `this.eventTelemetryEnabled`.
  579. if (this.telemetryEnabled) {
  580. this.utEvents.sendTrailheadEnrollEvent(action.data);
  581. }
  582. }
  583. async sendPageTakeoverData() {
  584. if (this.telemetryEnabled) {
  585. const value = {};
  586. let newtabAffected = false;
  587. let homeAffected = false;
  588. // Check whether or not about:home and about:newtab are set to a custom URL.
  589. // If so, classify them.
  590. if (Services.prefs.getBoolPref("browser.newtabpage.enabled") &&
  591. aboutNewTabService.overridden &&
  592. !aboutNewTabService.newTabURL.startsWith("moz-extension://")) {
  593. value.newtab_url_category = await this._classifySite(aboutNewTabService.newTabURL);
  594. newtabAffected = true;
  595. }
  596. // Check if the newtab page setting is controlled by an extension.
  597. await ExtensionSettingsStore.initialize();
  598. const newtabExtensionInfo = ExtensionSettingsStore.getSetting("url_overrides", "newTabURL");
  599. if (newtabExtensionInfo && newtabExtensionInfo.id) {
  600. value.newtab_extension_id = newtabExtensionInfo.id;
  601. newtabAffected = true;
  602. }
  603. const homePageURL = HomePage.get();
  604. if (!["about:home", "about:blank"].includes(homePageURL) &&
  605. !homePageURL.startsWith("moz-extension://")) {
  606. value.home_url_category = await this._classifySite(homePageURL);
  607. homeAffected = true;
  608. }
  609. const homeExtensionInfo = ExtensionSettingsStore.getSetting("prefs", "homepage_override");
  610. if (homeExtensionInfo && homeExtensionInfo.id) {
  611. value.home_extension_id = homeExtensionInfo.id;
  612. homeAffected = true;
  613. }
  614. let page;
  615. if (newtabAffected && homeAffected) {
  616. page = "both";
  617. } else if (newtabAffected) {
  618. page = "about:newtab";
  619. } else if (homeAffected) {
  620. page = "about:home";
  621. }
  622. if (page) {
  623. const event = Object.assign(
  624. this.createPing(),
  625. {
  626. action: "activity_stream_user_event",
  627. event: "PAGE_TAKEOVER_DATA",
  628. value,
  629. page,
  630. session_id: "n/a",
  631. },
  632. );
  633. this.sendEvent(event);
  634. }
  635. }
  636. }
  637. onAction(action) {
  638. switch (action.type) {
  639. case at.INIT:
  640. this.init();
  641. this.sendPageTakeoverData();
  642. break;
  643. case at.NEW_TAB_INIT:
  644. this.handleNewTabInit(action);
  645. break;
  646. case at.NEW_TAB_UNLOAD:
  647. this.endSession(au.getPortIdOfSender(action));
  648. break;
  649. case at.SAVE_SESSION_PERF_DATA:
  650. this.saveSessionPerfData(au.getPortIdOfSender(action), action.data);
  651. break;
  652. case at.TELEMETRY_IMPRESSION_STATS:
  653. this.handleImpressionStats(action);
  654. break;
  655. case at.DISCOVERY_STREAM_IMPRESSION_STATS:
  656. this.handleDiscoveryStreamImpressionStats(au.getPortIdOfSender(action), action.data);
  657. break;
  658. case at.DISCOVERY_STREAM_LOADED_CONTENT:
  659. this.handleDiscoveryStreamLoadedContent(au.getPortIdOfSender(action), action.data);
  660. break;
  661. case at.DISCOVERY_STREAM_SPOCS_FILL:
  662. this.handleDiscoveryStreamSpocsFill(action.data);
  663. break;
  664. case at.TELEMETRY_UNDESIRED_EVENT:
  665. this.handleUndesiredEvent(action);
  666. break;
  667. case at.TELEMETRY_USER_EVENT:
  668. this.handleUserEvent(action);
  669. break;
  670. case at.AS_ROUTER_TELEMETRY_USER_EVENT:
  671. this.handleASRouterUserEvent(action);
  672. break;
  673. case at.TELEMETRY_PERFORMANCE_EVENT:
  674. this.sendEvent(this.createPerformanceEvent(action));
  675. break;
  676. case at.TRAILHEAD_ENROLL_EVENT:
  677. this.handleTrailheadEnrollEvent(action);
  678. break;
  679. case at.UNINIT:
  680. this.uninit();
  681. break;
  682. }
  683. }
  684. /**
  685. * Handle impression stats actions from Discovery Stream. The data will be
  686. * stored into the session.impressionSets object for the given port, so that
  687. * it is sent to the server when the session ends.
  688. *
  689. * @note session.impressionSets will be keyed on `source` of the `data`,
  690. * all the data will be appended to an array for the same source.
  691. *
  692. * @param {String} port The session port with which this is associated
  693. * @param {Object} data The impression data structured as {source: "SOURCE", tiles: [{id: 123}]}
  694. *
  695. */
  696. handleDiscoveryStreamImpressionStats(port, data) {
  697. let session = this.sessions.get(port);
  698. if (!session) {
  699. throw new Error("Session does not exist.");
  700. }
  701. const impressionSets = session.impressionSets || {};
  702. const impressions = impressionSets[data.source] || [];
  703. // The payload might contain other properties, we need `id` and `pos` here.
  704. data.tiles.forEach(tile => impressions.push({id: tile.id, pos: tile.pos}));
  705. impressionSets[data.source] = impressions;
  706. session.impressionSets = impressionSets;
  707. }
  708. /**
  709. * Handle loaded content actions from Discovery Stream. The data will be
  710. * stored into the session.loadedContentSets object for the given port, so that
  711. * it is sent to the server when the session ends.
  712. *
  713. * @note session.loadedContentSets will be keyed on `source` of the `data`,
  714. * all the data will be appended to an array for the same source.
  715. *
  716. * @param {String} port The session port with which this is associated
  717. * @param {Object} data The loaded content structured as {source: "SOURCE", tiles: [{id: 123}]}
  718. *
  719. */
  720. handleDiscoveryStreamLoadedContent(port, data) {
  721. let session = this.sessions.get(port);
  722. if (!session) {
  723. throw new Error("Session does not exist.");
  724. }
  725. const loadedContentSets = session.loadedContentSets || {};
  726. const loadedContents = loadedContentSets[data.source] || [];
  727. // The payload might contain other properties, we need `id` and `pos` here.
  728. data.tiles.forEach(tile => loadedContents.push({id: tile.id, pos: tile.pos}));
  729. loadedContentSets[data.source] = loadedContents;
  730. session.loadedContentSets = loadedContentSets;
  731. }
  732. /**
  733. * Handl SPOCS Fill actions from Discovery Stream.
  734. *
  735. * @param {Object} data
  736. * The SPOCS Fill event structured as:
  737. * {
  738. * spoc_fills: [
  739. * {
  740. * id: 123,
  741. * displayed: 0,
  742. * reason: "frequency_cap",
  743. * full_recalc: 1
  744. * },
  745. * {
  746. * id: 124,
  747. * displayed: 1,
  748. * reason: "n/a",
  749. * full_recalc: 1
  750. * }
  751. * ]
  752. * }
  753. */
  754. handleDiscoveryStreamSpocsFill(data) {
  755. const payload = this.createSpocsFillPing(data);
  756. this.sendStructuredIngestionEvent(payload, "spoc-fills", "1");
  757. }
  758. /**
  759. * Take all enumerable members of the data object and merge them into
  760. * the session.perf object for the given port, so that it is sent to the
  761. * server when the session ends. All members of the data object should
  762. * be valid values of the perf object, as defined in pings.js and the
  763. * data*.md documentation.
  764. *
  765. * @note Any existing keys with the same names already in the
  766. * session perf object will be overwritten by values passed in here.
  767. *
  768. * @param {String} port The session with which this is associated
  769. * @param {Object} data The perf data to be
  770. */
  771. saveSessionPerfData(port, data) {
  772. // XXX should use try/catch and send a bad state indicator if this
  773. // get blows up.
  774. let session = this.sessions.get(port);
  775. // XXX Partial workaround for #3118; avoids the worst incorrect associations
  776. // of times with browsers, by associating the load trigger with the
  777. // visibility event as the user is most likely associating the trigger to
  778. // the tab just shown. This helps avoid associating with a preloaded
  779. // browser as those don't get the event until shown. Better fix for more
  780. // cases forthcoming.
  781. //
  782. // XXX the about:home check (and the corresponding test) should go away
  783. // once the load_trigger stuff in addSession is refactored into
  784. // setLoadTriggerInfo.
  785. //
  786. if (data.visibility_event_rcvd_ts && session.page !== "about:home") {
  787. this.setLoadTriggerInfo(port);
  788. }
  789. let timestamp = data.topsites_first_painted_ts;
  790. if (timestamp &&
  791. session.page === "about:home" &&
  792. !HomePage.overridden &&
  793. Services.prefs.getIntPref("browser.startup.page") === 1) {
  794. aboutNewTabService.maybeRecordTopsitesPainted(timestamp);
  795. }
  796. Object.assign(session.perf, data);
  797. }
  798. uninit() {
  799. try {
  800. Services.obs.removeObserver(this.browserOpenNewtabStart,
  801. "browser-open-newtab-start");
  802. Services.obs.removeObserver(this._addWindowListeners,
  803. DOMWINDOW_OPENED_TOPIC);
  804. } catch (e) {
  805. // Operation can fail when uninit is called before
  806. // init has finished setting up the observer
  807. }
  808. // Only uninit if the getter has initialized it
  809. if (Object.prototype.hasOwnProperty.call(this, "pingCentre")) {
  810. this.pingCentre.uninit();
  811. }
  812. if (Object.prototype.hasOwnProperty.call(this, "utEvents")) {
  813. this.utEvents.uninit();
  814. }
  815. if (Object.prototype.hasOwnProperty.call(this, "pingCentreForASRouter")) {
  816. this.pingCentreForASRouter.uninit();
  817. }
  818. // TODO: Send any unfinished sessions
  819. }
  820. };
  821. const EXPORTED_SYMBOLS = [
  822. "TelemetryFeed",
  823. "USER_PREFS_ENCODING",
  824. "PREF_IMPRESSION_ID",
  825. "TELEMETRY_PREF",
  826. "EVENTS_TELEMETRY_PREF",
  827. "STRUCTURED_INGESTION_TELEMETRY_PREF",
  828. "STRUCTURED_INGESTION_ENDPOINT_PREF",
  829. ];