Bug 1462725 - Add TelemetryEvents class to Savant JSM; r=rhelmer
This class will handle enabling/disabling the "savant" category of event telemetry.
It also has a utility method to generate a deterministic hash for a flow_id. The flow_id will be used to associate related events in a series.
MozReview-Commit-ID: F7sjsscOcV6
--- a/browser/modules/ShieldStudySavant.jsm
+++ b/browser/modules/ShieldStudySavant.jsm
@@ -4,72 +4,126 @@
"use strict";
var EXPORTED_SYMBOLS = ["ShieldStudySavant"];
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
ChromeUtils.import("resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetters(this, {
+ ClientID: "resource://gre/modules/ClientID.jsm"
+});
+
// See LOG_LEVELS in Console.jsm. Examples: "all", "info", "warn", & "error".
const PREF_LOG_LEVEL = "shield.savant.loglevel";
// Create a new instance of the ConsoleAPI so we can control the maxLogLevel with a pref.
XPCOMUtils.defineLazyGetter(this, "log", () => {
let ConsoleAPI = ChromeUtils.import("resource://gre/modules/Console.jsm", {}).ConsoleAPI;
let consoleOptions = {
maxLogLevelPref: PREF_LOG_LEVEL,
prefix: "ShieldStudySavant",
};
return new ConsoleAPI(consoleOptions);
});
class ShieldStudySavantClass {
constructor() {
this.SHIELD_STUDY_SAVANT_PREF = "shield.savant.enabled";
+ this.STUDY_TELEMETRY_CATEGORY = "savant";
}
- init() {
+ async init() {
+ this.TELEMETRY_CLIENT_ID = await ClientID.getClientID();
+ this.TelemetryEvents = new TelemetryEvents(this.STUDY_TELEMETRY_CATEGORY,
+ this.TELEMETRY_CLIENT_ID);
+
+ // TODO: implement eligibility (#13)
+ const isEligible = true;
+ if (!isEligible) {
+ this.endStudy("ineligible");
+ return;
+ }
// check the pref in case Normandy flipped it on before we could add the pref listener
this.shouldCollect = Services.prefs.getBoolPref(this.SHIELD_STUDY_SAVANT_PREF);
if (this.shouldCollect) {
- this.enableCollection();
+ this.TelemetryEvents.enableCollection();
}
Services.prefs.addObserver(this.SHIELD_STUDY_SAVANT_PREF, this);
}
observe(subject, topic, data) {
if (topic === "nsPref:changed" && data === this.SHIELD_STUDY_SAVANT_PREF) {
// toggle state of the pref
this.shouldCollect = !this.shouldCollect;
if (this.shouldCollect) {
- this.enableCollection();
+ this.TelemetryEvents.enableCollection();
} else {
- // Normandy has flipped off the pref
+ // The pref has been turned off
this.endStudy("expired");
}
}
}
- enableCollection() {
- log.debug("Study has been enabled; turning on data collection.");
- // TODO: enable data collection
+ sendEvent(method, object, value, extra) {
+ this.TelemetryEvents.sendEvent(method, object, value, extra);
}
endStudy(reason) {
- this.disableCollection();
+ this.TelemetryEvents.disableCollection();
// TODO: send endStudy ping with reason code
this.uninit();
}
- disableCollection() {
- log.debug("Study has been disabled; turning off data collection.");
- // TODO: disable data collection
- }
-
+ // Called on every Firefox shutdown and endStudy
uninit() {
+ // TODO: Make sure uninit() is called on every Firefox shutdown (look inside
+ // nsBrowserGlue.js to see where Normandy uninits)
+ // TODO: See what happens during Normandy's uninit method to ensure nothing
+ // is forgotten.
Services.prefs.removeObserver(this.SHIELD_STUDY_SAVANT_PREF, this);
Services.prefs.clearUserPref(this.SHIELD_STUDY_SAVANT_PREF);
Services.prefs.clearUserPref(PREF_LOG_LEVEL);
}
-};
+
+ async getFlowID(str) {
+ return this.TelemetryEvents.getFlowID(str);
+ }
+}
const ShieldStudySavant = new ShieldStudySavantClass();
+
+// references:
+// - https://hg.mozilla.org/mozilla-central/file/tip/toolkit/components/normandy/lib/TelemetryEvents.jsm
+// - https://hg.mozilla.org/mozilla-central/file/tip/toolkit/components/normandy/lib/PreferenceExperiments.jsm#l357
+class TelemetryEvents {
+ constructor(studyCategory, clientID) {
+ this.STUDY_TELEMETRY_CATEGORY = studyCategory;
+ this.TELEMETRY_CLIENT_ID = clientID;
+ }
+
+ sendEvent(method, object, value, extra) {
+ Services.telemetry.recordEvent(this.STUDY_TELEMETRY_CATEGORY, method, object, value, extra);
+ }
+
+ enableCollection() {
+ log.debug("Study has been enabled; turning ON data collection.");
+ // Services.telemetry.setEventRecordingEnabled(this.STUDY_TELEMETRY_CATEGORY, true);
+ }
+
+ disableCollection() {
+ log.debug("Study has been disabled; turning OFF data collection.");
+ // Services.telemetry.setEventRecordingEnabled(this.STUDY_TELEMETRY_CATEGORY, false);
+ }
+
+ // Returns the first 10 characters of a hash of the telemetry client ID and addon ID
+ // Reference: https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest
+ async getFlowID(str) {
+ log.debug(`Hashing string, ${ str }, with telemetry client ID, ${ this.TELEMETRY_CLIENT_ID }`);
+ const saltedStr = `${ str }${ this.TELEMETRY_CLIENT_ID }`;
+ const messageBuffer = new TextEncoder("utf-8").encode(saltedStr);
+ const hashBuffer = await crypto.subtle.digest("SHA-256", messageBuffer);
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
+ const hashHex = hashArray.map(b => ("00" + b.toString(16)).slice(-2)).join("");
+ return hashHex.substring(0, 10);
+ }
+}