browser/modules/ShieldStudySavant.jsm
author Bianca Danforth <bdanforth@mozilla.com>
Wed, 23 May 2018 07:19:12 -0700
changeset 803320 49e28f5799e12e8ecf963473717efa43161fcba0
parent 803319 04e7182ed09a4fb2139859687cb48bd43de6446c
permissions -rw-r--r--
Bug 1462725 - Dupe 'search' event probe under 'savant' category; r=rhelmer The duplication is so that this study’s use of the probe does not affect and is not affected by other uses of the probe. MozReview-Commit-ID: ILYu2izhMDB

/* 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";

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";
  }

  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.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.TelemetryEvents.enableCollection();
      } else {
        // The pref has been turned off
        this.endStudy("expired");
      }
    }
  }

  sendEvent(method, object, value, extra) {
    this.TelemetryEvents.sendEvent(method, object, value, extra);
  }

  endStudy(reason) {
    this.TelemetryEvents.disableCollection();
    // TODO: send endStudy ping with reason code
    this.uninit();
  }

  // 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);
  }
}