Bug 1462725 - Add TelemetryEvents class to Savant JSM; r=rhelmer draft
authorBianca Danforth <bdanforth@mozilla.com>
Wed, 23 May 2018 07:17:05 -0700
changeset 803319 04e7182ed09a4fb2139859687cb48bd43de6446c
parent 803196 cb8ede76462f9bc33f52940edc3be7b698145456
child 803320 49e28f5799e12e8ecf963473717efa43161fcba0
push id112069
push userbdanforth@mozilla.com
push dateSun, 03 Jun 2018 04:54:10 +0000
reviewersrhelmer
bugs1462725
milestone62.0a1
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
browser/modules/ShieldStudySavant.jsm
--- 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);
+  }
+}