Bug 1460893 - Add event telemetry to preference study error cases draft
authorMike Cooper <mcooper@mozilla.com>
Mon, 21 May 2018 13:21:27 -0700
changeset 797840 980f043e1c9487f0834028e38e587d5572ec11ce
parent 797745 77c06979d9e88979ec96263eccdbd750cb9221a4
child 797841 9dc0b81d15b79966934f833bd920d8e178b2016c
push id110597
push userbmo:mcooper@mozilla.com
push dateMon, 21 May 2018 20:22:07 +0000
bugs1460893
milestone62.0a1
Bug 1460893 - Add event telemetry to preference study error cases MozReview-Commit-ID: 95wBJ9KqnMG
toolkit/components/normandy/lib/PreferenceExperiments.jsm
toolkit/components/normandy/lib/TelemetryEvents.jsm
toolkit/components/normandy/test/browser/browser_PreferenceExperiments.js
--- a/toolkit/components/normandy/lib/PreferenceExperiments.jsm
+++ b/toolkit/components/normandy/lib/PreferenceExperiments.jsm
@@ -290,69 +290,75 @@ var PreferenceExperiments = {
     preferenceBranchType,
     preferenceType,
     experimentType = "exp",
   }) {
     log.debug(`PreferenceExperiments.start(${name}, ${branch})`);
 
     const store = await ensureStorage();
     if (name in store.data) {
+      TelemetryEvents.sendEvent("enrollFailed", "preference_study", name, {reason: "name-conflict"});
       throw new Error(`A preference experiment named "${name}" already exists.`);
     }
 
     const activeExperiments = Object.values(store.data).filter(e => !e.expired);
     const hasConflictingExperiment = activeExperiments.some(
       e => e.preferenceName === preferenceName
     );
     if (hasConflictingExperiment) {
+      TelemetryEvents.sendEvent("enrollFailed", "preference_study", name, {reason: "pref-conflict"});
       throw new Error(
         `Another preference experiment for the pref "${preferenceName}" is currently active.`
       );
     }
 
     const preferences = PreferenceBranchType[preferenceBranchType];
     if (!preferences) {
+      TelemetryEvents.sendEvent("enrollFailed", "preference_study", name, {reason: "invalid-branch"});
       throw new Error(`Invalid value for preferenceBranchType: ${preferenceBranchType}`);
     }
 
     if (experimentType.length > MAX_EXPERIMENT_SUBTYPE_LENGTH) {
+      TelemetryEvents.sendEvent("enrollFailed", "preference_study", name, {reason: "experiment-type-too-long"})
       throw new Error(
         `experimentType must be less than ${MAX_EXPERIMENT_SUBTYPE_LENGTH} characters. ` +
         `"${experimentType}" is ${experimentType.length} long.`
       );
     }
 
+    const prevPrefType = Services.prefs.getPrefType(preferenceName);
+    const givenPrefType = PREFERENCE_TYPE_MAP[preferenceType];
+
+    if (!preferenceType || !givenPrefType) {
+      TelemetryEvents.sendEvent("enrollFailed", "preference_study", name, {reason: "invalid-type"});
+      throw new Error(`Invalid preferenceType provided (given "${preferenceType}")`);
+    }
+
+    if (prevPrefType !== Services.prefs.PREF_INVALID && prevPrefType !== givenPrefType) {
+      TelemetryEvents.sendEvent("enrollFailed", "preference_study", name, {reason: "invalid-type"});
+      throw new Error(
+        `Previous preference value is of type "${prevPrefType}", but was given ` +
+        `"${givenPrefType}" (${preferenceType})`
+      );
+    }
+
     /** @type {Experiment} */
     const experiment = {
       name,
       branch,
       expired: false,
       lastSeen: new Date().toJSON(),
       preferenceName,
       preferenceValue,
       preferenceType,
       previousPreferenceValue: getPref(preferences, preferenceName, preferenceType),
       preferenceBranchType,
       experimentType,
     };
 
-    const prevPrefType = Services.prefs.getPrefType(preferenceName);
-    const givenPrefType = PREFERENCE_TYPE_MAP[preferenceType];
-
-    if (!preferenceType || !givenPrefType) {
-      throw new Error(`Invalid preferenceType provided (given "${preferenceType}")`);
-    }
-
-    if (prevPrefType !== Services.prefs.PREF_INVALID && prevPrefType !== givenPrefType) {
-      throw new Error(
-        `Previous preference value is of type "${prevPrefType}", but was given ` +
-        `"${givenPrefType}" (${preferenceType})`
-      );
-    }
-
     setPref(preferences, preferenceName, preferenceType, preferenceValue);
     PreferenceExperiments.startObserver(name, preferenceName, preferenceType, preferenceValue);
     store.data[name] = experiment;
     store.saveSoon();
 
     TelemetryEnvironment.setExperimentActive(name, branch, {type: EXPERIMENT_TYPE_PREFIX + experimentType});
     TelemetryEvents.sendEvent("enroll", "preference_study", name, {experimentType, branch});
     await this.saveStartupPrefs();
@@ -468,21 +474,23 @@ var PreferenceExperiments = {
   async stop(experimentName, {resetValue = true, reason = "unknown"} = {}) {
     log.debug(`PreferenceExperiments.stop(${experimentName}, {resetValue: ${resetValue}, reason: ${reason}})`);
     if (reason === "unknown") {
       log.warn(`experiment ${experimentName} ending for unknown reason`);
     }
 
     const store = await ensureStorage();
     if (!(experimentName in store.data)) {
+      TelemetryEvents.sendEvent("unenrollFailed", "preference_study", experimentName, {reason: "does-not-exist"});
       throw new Error(`Could not find a preference experiment named "${experimentName}"`);
     }
 
     const experiment = store.data[experimentName];
     if (experiment.expired) {
+      TelemetryEvents.sendEvent("unenrollFailed", "preference_study", experimentName, {reason: "already-unenrolled"});
       throw new Error(
         `Cannot stop preference experiment "${experimentName}" because it is already expired`
       );
     }
 
     if (PreferenceExperiments.hasObserver(experimentName)) {
       PreferenceExperiments.stopObserver(experimentName);
     }
--- a/toolkit/components/normandy/lib/TelemetryEvents.jsm
+++ b/toolkit/components/normandy/lib/TelemetryEvents.jsm
@@ -16,17 +16,17 @@ const TelemetryEvents = {
       methods: ["enroll"],
       objects: ["preference_study", "addon_study", "preference_rollout"],
       extra_keys: ["experimentType", "branch", "addonId", "addonVersion"],
       record_on_release: true,
     },
 
     enroll_failed: {
       methods: ["enrollFailed"],
-      objects: ["addon_study", "preference_rollout"],
+      objects: ["addon_study", "preference_rollout", "preference_study"],
       extra_keys: ["reason", "preference"],
       record_on_release: true,
     },
 
     update: {
       methods: ["update"],
       objects: ["preference_rollout"],
       extra_keys: ["previousState"],
@@ -37,17 +37,17 @@ const TelemetryEvents = {
       methods: ["unenroll"],
       objects: ["preference_study", "addon_study", "preference_rollback"],
       extra_keys: ["reason", "didResetValue", "addonId", "addonVersion", "branch"],
       record_on_release: true,
     },
 
     unenroll_failed: {
       methods: ["unenrollFailed"],
-      objects: ["preference_rollback"],
+      objects: ["preference_rollback", "preference_study"],
       extra_keys: ["reason"],
       record_on_release: true,
     },
 
     graduate: {
       methods: ["graduate"],
       objects: ["preference_rollout"],
       extra_keys: [],
--- a/toolkit/components/normandy/test/browser/browser_PreferenceExperiments.js
+++ b/toolkit/components/normandy/test/browser/browser_PreferenceExperiments.js
@@ -38,66 +38,90 @@ decorate_task(
       "clearAllExperimentStorage removed all stored experiments",
     );
   }
 );
 
 // start should throw if an experiment with the given name already exists
 decorate_task(
   withMockExperiments,
-  async function(experiments) {
+  withSendEventStub,
+  async function(experiments, sendEventStub) {
     experiments.test = experimentFactory({name: "test"});
     await Assert.rejects(
       PreferenceExperiments.start({
         name: "test",
         branch: "branch",
         preferenceName: "fake.preference",
         preferenceValue: "value",
         preferenceType: "string",
         preferenceBranchType: "default",
       }),
+      /test.*already exists/,
       "start threw an error due to a conflicting experiment name",
     );
+
+    Assert.deepEqual(
+      sendEventStub.args,
+      [["enrollFailed", "preference_study", "test", {reason: "name-conflict"}]],
+      "event should be sent for failure",
+    );
   }
 );
 
 // start should throw if an experiment for the given preference is active
 decorate_task(
   withMockExperiments,
-  async function(experiments) {
+  withSendEventStub,
+  async function(experiments, sendEventStub) {
     experiments.test = experimentFactory({name: "test", preferenceName: "fake.preference"});
     await Assert.rejects(
       PreferenceExperiments.start({
         name: "different",
         branch: "branch",
         preferenceName: "fake.preference",
         preferenceValue: "value",
         preferenceType: "string",
         preferenceBranchType: "default",
       }),
+      /another.*is currently active/i,
       "start threw an error due to an active experiment for the given preference",
     );
+
+    Assert.deepEqual(
+      sendEventStub.args,
+      [["enrollFailed", "preference_study", "different", {reason: "pref-conflict"}]],
+      "event should be sent for failure",
+    );
   }
 );
 
 // start should throw if an invalid preferenceBranchType is given
 decorate_task(
   withMockExperiments,
-  async function() {
+  withSendEventStub,
+  async function(experiments, sendEventStub) {
     await Assert.rejects(
       PreferenceExperiments.start({
         name: "test",
         branch: "branch",
         preferenceName: "fake.preference",
         preferenceValue: "value",
         preferenceType: "string",
         preferenceBranchType: "invalid",
       }),
+      /invalid value for preferenceBranchType: invalid/i,
       "start threw an error due to an invalid preference branch type",
     );
+
+    Assert.deepEqual(
+      sendEventStub.args,
+      [["enrollFailed", "preference_study", "test", {reason: "invalid-branch"}]],
+      "event should be sent for failure",
+    );
   }
 );
 
 // start should save experiment data, modify the preference, and register a
 // watcher.
 decorate_task(
   withMockExperiments,
   withMockPreferences,
@@ -197,41 +221,50 @@ decorate_task(
     );
     is(Preferences.get("fake.preference"), "newvalue", "start modified the user preference");
   }
 );
 
 // start should detect if a new preference value type matches the previous value type
 decorate_task(
   withMockPreferences,
-  async function(mockPreferences) {
+  withSendEventStub,
+  async function(mockPreferences, sendEventStub) {
     mockPreferences.set("fake.type_preference", "oldvalue");
 
     await Assert.rejects(
       PreferenceExperiments.start({
         name: "test",
         branch: "branch",
         preferenceName: "fake.type_preference",
         preferenceBranchType: "user",
         preferenceValue: 12345,
         preferenceType: "integer",
       }),
+      /previous preference value is of type/i,
       "start threw error for incompatible preference type"
     );
+
+    Assert.deepEqual(
+      sendEventStub.args,
+      [["enrollFailed", "preference_study", "test", {reason: "invalid-type"}]],
+      "event should be sent for failure",
+    );
   }
 );
 
 // startObserver should throw if an observer for the experiment is already
 // active.
 decorate_task(
   withMockExperiments,
   async function() {
     PreferenceExperiments.startObserver("test", "fake.preference", "string", "newvalue");
     Assert.throws(
       () => PreferenceExperiments.startObserver("test", "another.fake", "string", "othervalue"),
+      /observer.*is already active/i,
       "startObserver threw due to a conflicting active observer",
     );
     PreferenceExperiments.stopAllObservers();
   }
 );
 
 // startObserver should register an observer that calls stop when a preference
 // changes from its experimental value.
@@ -282,16 +315,17 @@ decorate_task(
 );
 
 // stopObserver should throw if there is no observer active for it to stop.
 decorate_task(
   withMockExperiments,
   async function() {
     Assert.throws(
       () => PreferenceExperiments.stopObserver("neveractive", "another.fake", "othervalue"),
+      /no observer.*found/i,
       "stopObserver threw because there was not matching active observer",
     );
   }
 );
 
 // stopObserver should cancel an active observer.
 decorate_task(
   withMockExperiments,
@@ -355,16 +389,17 @@ decorate_task(
 );
 
 // markLastSeen should throw if it can't find a matching experiment
 decorate_task(
   withMockExperiments,
   async function() {
     await Assert.rejects(
       PreferenceExperiments.markLastSeen("neveractive"),
+      /could not find/i,
       "markLastSeen threw because there was not a matching experiment",
     );
   }
 );
 
 // markLastSeen should update the lastSeen date
 decorate_task(
   withMockExperiments,
@@ -378,33 +413,49 @@ decorate_task(
       "markLastSeen updated the experiment lastSeen date",
     );
   }
 );
 
 // stop should throw if an experiment with the given name doesn't exist
 decorate_task(
   withMockExperiments,
-  async function() {
+  withSendEventStub,
+  async function(experiments, sendEventStub) {
     await Assert.rejects(
       PreferenceExperiments.stop("test"),
+      /could not find/i,
       "stop threw an error because there are no experiments with the given name",
     );
+
+    Assert.deepEqual(
+      sendEventStub.args,
+      [["unenrollFailed", "preference_study", "test", {reason: "does-not-exist"}]],
+      "event should be sent for failure",
+    );
   }
 );
 
 // stop should throw if the experiment is already expired
 decorate_task(
   withMockExperiments,
-  async function(experiments) {
+  withSendEventStub,
+  async function(experiments, sendEventStub) {
     experiments.test = experimentFactory({name: "test", expired: true});
     await Assert.rejects(
       PreferenceExperiments.stop("test"),
+      /already expired/,
       "stop threw an error because the experiment was already expired",
     );
+
+    Assert.deepEqual(
+      sendEventStub.args,
+      [["unenrollFailed", "preference_study", "test", {reason: "already-unenrolled"}]],
+      "event should be sent for failure",
+    );
   }
 );
 
 // stop should mark the experiment as expired, stop its observer, and revert the
 // preference value.
 decorate_task(
   withMockExperiments,
   withMockPreferences,
@@ -555,16 +606,17 @@ decorate_task(
 );
 
 // get should throw if no experiment exists with the given name
 decorate_task(
   withMockExperiments,
   async function() {
     await Assert.rejects(
       PreferenceExperiments.get("neverexisted"),
+      /could not find/i,
       "get rejects if no experiment with the given name is found",
     );
   }
 );
 
 // get
 decorate_task(
   withMockExperiments,
@@ -894,16 +946,17 @@ decorate_task(
   async function testSaveStartupPrefsError(experiments) {
     experiments.test = experimentFactory({
       preferenceName: "fake.invalidValue",
       preferenceValue: new Date(),
     });
 
     await Assert.rejects(
       PreferenceExperiments.saveStartupPrefs(),
+      /invalid preference type/i,
       "saveStartupPrefs throws if an experiment has an invalid preference value type",
     );
   },
 );
 
 // test that default branch prefs restore to the right value if the default pref changes
 decorate_task(
   withMockExperiments,