-
Notifications
You must be signed in to change notification settings - Fork 5
/
StudyTelemetryCollector.jsm
395 lines (292 loc) · 14.6 KB
/
StudyTelemetryCollector.jsm
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
"use strict";
/* global studyUtils */
/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "(EXPORTED_SYMBOLS|StudyTelemetryCollector)" }]*/
const { utils: Cu } = Components;
Cu.import("resource://gre/modules/Console.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/Preferences.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/PromiseUtils.jsm");
const EXPORTED_SYMBOLS = this.EXPORTED_SYMBOLS = ["StudyTelemetryCollector"];
/*
Note: All combinations of below ended up with Component returned failure code: 0x8000ffff (NS_ERROR_UNEXPECTED) [nsIXPCComponents_Utils.import]
when trying to access the studyUtils object
const BASERESOURCE = "esper-pioneer-shield-study";
// const STUDYUTILSPATH = `resource://${BASERESOURCE}/StudyUtils.jsm`;
// const STUDYUTILSPATH = `${__SCRIPT_URI_SPEC__}/../../${studyConfig.studyUtilsPath}`;
// const { studyUtils } = Cu.import(STUDYUTILSPATH, {});
// XPCOMUtils.defineLazyModuleGetter(this, "studyUtils", STUDYUTILSPATH);
*/
// telemetry utils
const { TelemetryController } = Cu.import("resource://gre/modules/TelemetryController.jsm", null);
const { TelemetrySession } = Cu.import("resource://gre/modules/TelemetrySession.jsm", null);
const { TelemetryEnvironment } = Cu.import("resource://gre/modules/TelemetryEnvironment.jsm", null);
XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
"resource://gre/modules/PlacesUtils.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "Telemetry",
"@mozilla.org/base/telemetry;1", "nsITelemetry");
/**
* Note: Setting studyUtils in this constructor due to above comments
*/
class StudyTelemetryCollector {
constructor(studyUtils, variation) {
this.variation = variation;
this.studyUtils = studyUtils;
}
async start() {
this.telemetry({ event: "esper-init" });
// Ensure that we collect telemetry payloads only after it is fully initialized
// See http://searchfox.org/mozilla-central/rev/423b2522c48e1d654e30ffc337164d677f934ec3/toolkit/components/telemetry/TelemetryController.jsm#295
TelemetryController.promiseInitialized().then(() => {
try {
this.collectAndSendTelemetry();
} catch (ex) {
// TODO: how are errors during study execution reported?
// this.studyUtils.telemetryError();
Components.utils.reportError(ex);
}
});
}
telemetry(payload) {
if (StudyTelemetryCollector.allowedToSendTelemetry()) {
this.studyUtils.telemetry(payload);
} else {
console.log('ESPER telemetry not sent due to privacy preferences', payload);
}
}
static allowedToSendTelemetry() {
// Main Telemetry preference that determines whether Telemetry data is collected and uploaded.
const basicTelemetryEnabled = Preferences.get("datareporting.healthreport.uploadEnabled", false);
console.log('allowedToSendTelemetry: basicTelemetryEnabled', basicTelemetryEnabled);
// This preference determines the build. True means pre-release version of Firefox, false means release version of Firefox.
const extendedTelemetryEnabled = Preferences.get("toolkit.telemetry.enabled", false);
console.log('allowedToSendTelemetry: extendedTelemetryEnabled', extendedTelemetryEnabled);
// Allow shield studies
const shieldStudiesTelemetryEnabled = Preferences.get("app.shield.optoutstudies.enabled", false);
console.log('allowedToSendTelemetry: shieldStudiesTelemetryEnabled', shieldStudiesTelemetryEnabled);
// do not run study if basic telemetry is disabled
if (basicTelemetryEnabled !== true) {
return false;
}
// do not care if extended telemetry is disabled or enabled
/*
if (extendedTelemetryEnabled !== true) {
return false;
}
*/
// do not run study if shield studies are disabled
if (shieldStudiesTelemetryEnabled !== true) {
return false;
}
return true;
}
async collectAndSendTelemetry() {
const telemetryEnvironmentBasedAttributes = StudyTelemetryCollector.collectTelemetryEnvironmentBasedAttributes();
const telemetrySubsessionPayloadBasedAttributes = StudyTelemetryCollector.collectTelemetrySubsessionPayloadBasedAttributes();
const telemetryScalarBasedAttributes = StudyTelemetryCollector.collectTelemetryScalarBasedAttributes();
console.log("telemetryEnvironmentBasedAttributes", telemetryEnvironmentBasedAttributes);
console.log("telemetrySubsessionPayloadBasedAttributes", telemetrySubsessionPayloadBasedAttributes);
console.log("telemetryScalarBasedAttributes", telemetryScalarBasedAttributes);
StudyTelemetryCollector.collectPlacesDbBasedAttributes().then((placesDbBasedAttributes) => {
console.log("placesDbBasedAttributes", placesDbBasedAttributes);
const shieldPingAttributes = {
event: "telemetry-payload",
...telemetryEnvironmentBasedAttributes,
...telemetrySubsessionPayloadBasedAttributes,
...telemetryScalarBasedAttributes,
...placesDbBasedAttributes,
};
const shieldPingPayload = StudyTelemetryCollector.createShieldPingPayload(shieldPingAttributes);
console.log("shieldPingPayload", shieldPingPayload);
this.telemetry(shieldPingPayload);
});
}
// TODO: @glind: move to shield study utils?
static createShieldPingPayload(shieldPingAttributes) {
const shieldPingPayload = {};
// shield ping attributes must be strings
for (const attribute in shieldPingAttributes) {
let attributeValue = shieldPingAttributes[attribute];
if (typeof attributeValue === "undefined") {
attributeValue = "null";
}
if (typeof attributeValue === "object") {
attributeValue = JSON.stringify(attributeValue);
}
if (typeof attributeValue !== "string") {
attributeValue = String(attributeValue);
}
shieldPingPayload[attribute] = attributeValue;
}
return shieldPingPayload;
}
/**
* These attributes are already sent as part of the telemetry ping envelope
* @returns {{}}
*/
static collectTelemetryEnvironmentBasedAttributes() {
const environment = TelemetryEnvironment.currentEnvironment;
console.log("TelemetryEnvironment.currentEnvironment", environment);
return {
"default_search_engine": environment.settings.defaultSearchEngine,
"locale": environment.settings.locale,
"os": environment.system.os.name,
"normalized_channel": environment.settings.update.channel,
"profile_creation_date": environment.profile.creationDate,
"app_version": environment.build.version,
"system.memory_mb": environment.system.memoryMB,
"system_cpu.cores": environment.system.cpu.cores,
"system_cpu.speed_mhz": environment.system.cpu.speedMHz,
"os_version": environment.system.os.version,
"system_gfx.monitors[1].screen_width": environment.system.gfx.monitors[0] ? environment.system.gfx.monitors[0].screenWidth : undefined,
"system_gfx.monitors[1].screen_width_zero_indexed": environment.system.gfx.monitors[1] ? environment.system.gfx.monitors[1].screenWidth : undefined,
};
}
/**
* Scalars are only submitted if data was added to them, and are only reported with subsession pings.
* Thus, we use the current telemetry subsession payload for these attributes
* @returns {{}}
*/
static collectTelemetrySubsessionPayloadBasedAttributes() {
const telemetrySubsessionCurrentPingData = TelemetryController.getCurrentPingData(true);
console.log("telemetrySubsessionCurrentPingData", telemetrySubsessionCurrentPingData);
const payload = telemetrySubsessionCurrentPingData.payload;
const attributes = {};
attributes.uptime = payload.simpleMeasurements.uptime;
attributes.total_time = payload.simpleMeasurements.totalTime;
attributes.profile_subsession_counter = payload.info.profileSubsessionCounter;
attributes.subsession_start_date = payload.info.subsessionStartDate;
attributes.timezone_offset = payload.info.timezoneOffset;
// firefox/toolkit/components/places/PlacesDBUtils.jsm
// Collect the histogram payloads if present
attributes.places_bookmarks_count_histogram = payload.processes.content.histograms.PLACES_BOOKMARKS_COUNT;
attributes.places_pages_count_histogram = payload.processes.content.histograms.PLACES_PAGES_COUNT;
// firefox/browser/modules/BrowserUsageTelemetry.jsm
attributes.search_counts = payload.keyedHistograms.SEARCH_COUNTS;
return attributes;
}
static collectTelemetryScalarBasedAttributes() {
const attributes = {};
// firefox/browser/modules/test/browser/head.js
function getParentProcessScalars(aChannel, aKeyed = false, aClear = false) {
const scalars = aKeyed ?
Services.telemetry.snapshotKeyedScalars(aChannel, aClear).parent :
Services.telemetry.snapshotScalars(aChannel, aClear).parent;
return scalars || {};
}
// firefox/browser/modules/test/browser/browser_UsageTelemetry.js
const MAX_CONCURRENT_TABS = "browser.engagement.max_concurrent_tab_count";
const TAB_EVENT_COUNT = "browser.engagement.tab_open_event_count";
const MAX_CONCURRENT_WINDOWS = "browser.engagement.max_concurrent_window_count";
const WINDOW_OPEN_COUNT = "browser.engagement.window_open_event_count";
const TOTAL_URI_COUNT = "browser.engagement.total_uri_count";
const UNIQUE_DOMAINS_COUNT = "browser.engagement.unique_domains_count";
const UNFILTERED_URI_COUNT = "browser.engagement.unfiltered_uri_count";
const scalars = getParentProcessScalars(Telemetry.DATASET_RELEASE_CHANNEL_OPTIN);
function getScalar(scalars, scalarName) {
if (!(scalarName in scalars)) {
console.log(`Scalar ${scalarName} is not set`);
return;
}
return scalars[scalarName];
}
console.log('scalars', scalars);
attributes.scalar_parent_browser_engagement_max_concurrent_window_count = getScalar(scalars, MAX_CONCURRENT_WINDOWS);
attributes.scalar_parent_browser_engagement_max_concurrent_tab_count = getScalar(scalars, MAX_CONCURRENT_TABS);
attributes.scalar_parent_browser_engagement_tab_open_event_count = getScalar(scalars, TAB_EVENT_COUNT);
attributes.scalar_parent_browser_engagement_window_open_event_count = getScalar(scalars, WINDOW_OPEN_COUNT);
attributes.scalar_parent_browser_engagement_unique_domains_count = getScalar(scalars, UNIQUE_DOMAINS_COUNT);
attributes.scalar_parent_browser_engagement_total_uri_count = getScalar(scalars, TOTAL_URI_COUNT);
attributes.scalar_parent_browser_engagement_unfiltered_uri_count = getScalar(scalars, UNFILTERED_URI_COUNT);
function getKeyedScalar(scalars, scalarName, key) {
if (!(scalarName in scalars)) {
console.log(`Keyed scalar ${scalarName} is not set`);
return;
}
if (!(key in scalars[scalarName])) {
console.log(`Keyed scalar ${scalarName} has not key ${key}`);
return;
}
return scalars[scalarName][key];
}
const keyedScalars = getParentProcessScalars(Telemetry.DATASET_RELEASE_CHANNEL_OPTIN, true, false);
console.log('keyedScalars', keyedScalars);
// firefox/browser/modules/test/browser/browser_UsageTelemetry_searchbar.js
const SCALAR_SEARCHBAR = "browser.engagement.navigation.searchbar";
attributes.scalar_parent_browser_engagement_navigation_searchbar = getKeyedScalar(keyedScalars, SCALAR_SEARCHBAR, "search_enter");
// firefox/browser/modules/test/browser/browser_UsageTelemetry_content.js
const BASE_PROBE_NAME = "browser.engagement.navigation.";
const SCALAR_CONTEXT_MENU = BASE_PROBE_NAME + "contextmenu";
const SCALAR_ABOUT_NEWTAB = BASE_PROBE_NAME + "about_newtab";
attributes.scalar_parent_browser_engagement_navigation_about_newtab = getKeyedScalar(keyedScalars, SCALAR_ABOUT_NEWTAB, "search_enter");
attributes.scalar_parent_browser_engagement_navigation_contextmenu = getKeyedScalar(keyedScalars, SCALAR_CONTEXT_MENU, "search");
// firefox/browser/modules/test/browser/browser_UsageTelemetry_urlbar.js
const SCALAR_URLBAR = "browser.engagement.navigation.urlbar";
attributes.scalar_parent_browser_engagement_navigation_urlbar = getKeyedScalar(keyedScalars, SCALAR_URLBAR, "search_enter");
return attributes;
}
/**
* Collects the places-db-related telemetry by querying the database directly rather relying on telemetry since, due to performance reasons,
* regular telemetry only collects this telemetry occasionally.
*
* An earlier attempt to gather the places-db-related telemetry was to await PlacesDBUtils.telemetry() and then fetch the populated histograms
* but PlacesDBUtils.telemetry() does not return a promise, it just updates the current telemetry session with additional histograms when db queries are ready
* and I found no reliant way to ensure these histograms being available in the current telemetry session.
*
* @returns {Promise}
*/
static async collectPlacesDbBasedAttributes() {
return new Promise((resolve, reject) => {
StudyTelemetryCollector.queryPlacesDbTelemetry().then((placesDbTelemetryResults) => {
resolve({
places_pages_count: placesDbTelemetryResults[0][1],
places_bookmarks_count: placesDbTelemetryResults[1][1],
});
}).catch(ex => reject(ex));
});
}
/**
* Based on code in firefox/toolkit/components/places/PlacesDBUtils.jsm
*
* @returns {Promise.<A|Promise.<*[]>|Promise.<Array.<T>>>}
*/
static async queryPlacesDbTelemetry() {
const probes = [
{
histogram: "PLACES_PAGES_COUNT",
query: "SELECT count(*) FROM moz_places",
},
{
histogram: "PLACES_BOOKMARKS_COUNT",
query: `SELECT count(*) FROM moz_bookmarks b
JOIN moz_bookmarks t ON t.id = b.parent
AND t.parent <> :tags_folder
WHERE b.type = :type_bookmark`,
},
];
const params = {
tags_folder: PlacesUtils.tagsFolderId,
type_folder: PlacesUtils.bookmarks.TYPE_FOLDER,
type_bookmark: PlacesUtils.bookmarks.TYPE_BOOKMARK,
places_root: PlacesUtils.placesRootId,
};
const promises = [];
for (let i = 0; i < probes.length; i++) {
const probe = probes[i];
const promiseDone = new Promise((resolve, reject) => {
const filteredParams = {};
for (const p in params) {
if (probe.query.includes(`:${p}`)) {
filteredParams[p] = params[p];
}
}
PlacesUtils.promiseDBConnection()
.then(db => db.execute(probe.query, filteredParams))
.then(rows => resolve([probe, rows[0].getResultByIndex(0)]))
.catch(() => reject(new Error("Unable to get telemetry from database.")));
});
promises.push(promiseDone);
}
return Promise.all(promises);
}
}