Bug 1535962: Introduce a sample rate for reporting uptake telemetry events r=leplatrem a=pascalc
authorEthan Glasser-Camp <ethan@betacantrips.com>
Wed, 20 Mar 2019 14:30:39 +0000
changeset 525660 327cadd9820c9f7ef6580d229ac4c8ce1ff5f324
parent 525659 475f82f18288433b2a972c4d28a784bf323fe70b
child 525661 2211f0befe5480ceb814f96274e48680b0e7ff0e
push id2032
push userffxbld-merge
push dateMon, 13 May 2019 09:36:57 +0000
treeherdermozilla-release@455c1065dcbe [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersleplatrem, pascalc
bugs1535962
milestone67.0
Bug 1535962: Introduce a sample rate for reporting uptake telemetry events r=leplatrem a=pascalc Differential Revision: https://phabricator.services.mozilla.com/D23814
modules/libpref/init/all.js
services/common/tests/unit/test_uptake_telemetry.js
services/common/uptake-telemetry.js
services/settings/RemoteSettingsClient.jsm
services/settings/remote-settings.js
toolkit/components/normandy/lib/ActionsManager.jsm
toolkit/components/normandy/lib/RecipeRunner.jsm
toolkit/components/normandy/lib/Uptake.jsm
toolkit/components/normandy/test/browser/browser_Uptake.js
toolkit/components/telemetry/docs/collection/uptake.rst
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -2755,16 +2755,21 @@ pref("security.strict_security_checks.en
 
 // Remote settings preferences
 pref("services.settings.poll_interval", 86400); // 24H
 pref("services.settings.server", "https://firefox.settings.services.mozilla.com/v1");
 pref("services.settings.changes.path", "/buckets/monitor/collections/changes/records");
 pref("services.settings.default_bucket", "main");
 pref("services.settings.default_signer", "remote-settings.content-signature.mozilla.org");
 
+// The percentage of clients who will report uptake telemetry as
+// events instead of just a histogram. This only applies on Release;
+// other channels always report events.
+pref("services.common.uptake.sampleRate", 1);   // 1%
+
 // Blocklist preferences
 pref("extensions.blocklist.enabled", true);
 // OneCRL freshness checking depends on this value, so if you change it,
 // please also update security.onecrl.maximum_staleness_in_seconds.
 pref("extensions.blocklist.interval", 86400);
 // Required blocklist freshness for OneCRL OCSP bypass
 // (default is 1.25x extensions.blocklist.interval, or 30 hours)
 pref("security.onecrl.maximum_staleness_in_seconds", 108000);
--- a/services/common/tests/unit/test_uptake_telemetry.js
+++ b/services/common/tests/unit/test_uptake_telemetry.js
@@ -1,31 +1,102 @@
+const { ClientID } = ChromeUtils.import("resource://gre/modules/ClientID.jsm");
 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
 const { UptakeTelemetry } = ChromeUtils.import("resource://services-common/uptake-telemetry.js");
 
 const COMPONENT = "remotesettings";
 
+async function withFakeClientID(uuid, f) {
+  const module = ChromeUtils.import("resource://services-common/uptake-telemetry.js", null);
+  const oldPolicy = module.Policy;
+  module.Policy = {
+    ...oldPolicy,
+    _clientIDHash: null,
+    getClientID: () => Promise.resolve(uuid),
+  };
+  try {
+    return await f();
+  } finally {
+    module.Policy = oldPolicy;
+  }
+}
+
+async function withFakeChannel(channel, f) {
+  const module = ChromeUtils.import("resource://services-common/uptake-telemetry.js", null);
+  const oldPolicy = module.Policy;
+  module.Policy = {
+    ...oldPolicy,
+    getChannel: () => channel,
+  };
+  try {
+    return await f();
+  } finally {
+    module.Policy = oldPolicy;
+  }
+}
 
 add_task(async function test_unknown_status_is_not_reported() {
   const source = "update-source";
   const startHistogram = getUptakeTelemetrySnapshot(source);
 
-  UptakeTelemetry.report(COMPONENT, "unknown-status", { source });
+  await UptakeTelemetry.report(COMPONENT, "unknown-status", { source });
 
   const endHistogram = getUptakeTelemetrySnapshot(source);
   const expectedIncrements = {};
   checkUptakeTelemetry(startHistogram, endHistogram, expectedIncrements);
 });
 
 add_task(async function test_each_status_can_be_caught_in_snapshot() {
   const source = "some-source";
   const startHistogram = getUptakeTelemetrySnapshot(source);
 
   const expectedIncrements = {};
   for (const label of Object.keys(UptakeTelemetry.STATUS)) {
     const status = UptakeTelemetry.STATUS[label];
-    UptakeTelemetry.report(COMPONENT, status, { source });
+    await UptakeTelemetry.report(COMPONENT, status, { source });
     expectedIncrements[status] = 1;
   }
 
   const endHistogram = getUptakeTelemetrySnapshot(source);
   checkUptakeTelemetry(startHistogram, endHistogram, expectedIncrements);
 });
+
+add_task(async function test_events_are_sent_when_hash_is_mod_0() {
+  const source = "some-source";
+  const startSnapshot = getUptakeTelemetrySnapshot(source);
+  const startSuccess = startSnapshot.events.success || 0;
+  const uuid = "d81bbfad-d741-41f5-a7e6-29f6bde4972a"; // hash % 100 = 0
+  await withFakeClientID(uuid, async () => {
+    await withFakeChannel("release", async () => {
+      await UptakeTelemetry.report(COMPONENT, UptakeTelemetry.STATUS.SUCCESS, { source });
+    });
+  });
+  const endSnapshot = getUptakeTelemetrySnapshot(source);
+  Assert.equal(endSnapshot.events.success, startSuccess + 1);
+});
+
+add_task(async function test_events_are_not_sent_when_hash_is_greater_than_pref() {
+  const source = "some-source";
+  const startSnapshot = getUptakeTelemetrySnapshot(source);
+  const startSuccess = startSnapshot.events.success || 0;
+  const uuid = "d81bbfad-d741-41f5-a7e6-29f6bde49721"; // hash % 100 = 1
+  await withFakeClientID(uuid, async () => {
+    await withFakeChannel("release", async () => {
+      await UptakeTelemetry.report(COMPONENT, UptakeTelemetry.STATUS.SUCCESS, { source });
+    });
+  });
+  const endSnapshot = getUptakeTelemetrySnapshot(source);
+  Assert.equal(endSnapshot.events.success || 0, startSuccess);
+});
+
+add_task(async function test_events_are_sent_when_nightly() {
+  const source = "some-source";
+  const startSnapshot = getUptakeTelemetrySnapshot(source);
+  const startSuccess = startSnapshot.events.success || 0;
+  const uuid = "d81bbfad-d741-41f5-a7e6-29f6bde49721"; // hash % 100 = 1
+  await withFakeClientID(uuid, async () => {
+    await withFakeChannel("nightly", async () => {
+      await UptakeTelemetry.report(COMPONENT, UptakeTelemetry.STATUS.SUCCESS, { source });
+    });
+  });
+  const endSnapshot = getUptakeTelemetrySnapshot(source);
+  Assert.equal(endSnapshot.events.success, startSuccess + 1);
+});
--- a/services/common/uptake-telemetry.js
+++ b/services/common/uptake-telemetry.js
@@ -2,25 +2,77 @@
  * 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 = ["UptakeTelemetry"];
 
-const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { XPCOMUtils } = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+ChromeUtils.defineModuleGetter(this, "AppConstants",
+                               "resource://gre/modules/AppConstants.jsm");
+ChromeUtils.defineModuleGetter(this, "ClientID",
+                               "resource://gre/modules/ClientID.jsm");
+ChromeUtils.defineModuleGetter(this, "Services",
+                               "resource://gre/modules/Services.jsm");
 
+XPCOMUtils.defineLazyGetter(this, "CryptoHash", () => {
+  return Components.Constructor("@mozilla.org/security/hash;1", "nsICryptoHash", "initWithString");
+});
+
+XPCOMUtils.defineLazyPreferenceGetter(this, "gSampleRate", "services.common.uptake.sampleRate");
 
 // Telemetry histogram id (see Histograms.json).
 const TELEMETRY_HISTOGRAM_ID = "UPTAKE_REMOTE_CONTENT_RESULT_1";
 
 // Telemetry events id (see Events.yaml).
 const TELEMETRY_EVENTS_ID = "uptake.remotecontent.result";
 
+/**
+ * A wrapper around certain low-level operations that can be substituted for testing.
+ */
+var Policy = {
+  _clientIDHash: null,
+
+  getClientID() {
+    return ClientID.getClientID();
+  },
+
+  /**
+   * Compute an integer in the range [0, 100) using a hash of the
+   * client ID.
+   *
+   * This is useful for sampling clients when trying to report
+   * telemetry only for a sample of clients.
+   */
+  async getClientIDHash() {
+    if (this._clientIDHash === null) {
+      this._clientIDHash = this._doComputeClientIDHash();
+    }
+    return this._clientIDHash;
+  },
+
+  async _doComputeClientIDHash() {
+    const clientID = await this.getClientID();
+    let byteArr = new TextEncoder().encode(clientID);
+    let hash = new CryptoHash("sha256");
+    hash.update(byteArr, byteArr.length);
+    const bytes = hash.finish(false);
+    let rem = 0;
+    for (let i = 0, len = bytes.length; i < len; i++) {
+      rem = ((rem << 8) + (bytes[i].charCodeAt(0) & 0xff)) % 100;
+    }
+    return rem;
+  },
+
+  getChannel() {
+    return AppConstants.MOZ_UPDATE_CHANNEL;
+  },
+};
 
 /**
  * A Telemetry helper to report uptake of remote content.
  */
 class UptakeTelemetry {
   /**
    * Supported uptake statuses:
    *
@@ -85,32 +137,38 @@ class UptakeTelemetry {
    *
    * @param {string} component     the component reporting the uptake (eg. "normandy").
    * @param {string} status        the uptake status (eg. "network_error")
    * @param {Object} extra         extra values to report
    * @param {string} extra.source  the update source (eg. "recipe-42").
    * @param {string} extra.trigger what triggered the polling/fetching (eg. "broadcast", "timer").
    * @param {int}    extra.age     age of pulled data in seconds
    */
-  static report(component, status, extra = {}) {
+  static async report(component, status, extra = {}) {
     const { source } = extra;
 
     if (!source) {
       throw new Error("`source` value is mandatory.");
     }
 
     // Report event for real-time monitoring. See Events.yaml for registration.
     // Contrary to histograms, Telemetry Events are not enabled by default.
     // Enable them on first call to `report()`.
     if (!this._eventsEnabled) {
       Services.telemetry.setEventRecordingEnabled(TELEMETRY_EVENTS_ID, true);
       this._eventsEnabled = true;
     }
-    Services.telemetry
-      .recordEvent(TELEMETRY_EVENTS_ID, "uptake", component, status, extra);
+
+    const hash = await Policy.getClientIDHash();
+    const channel = Policy.getChannel();
+    const shouldSendEvent = !["release", "esr"].includes(channel) || hash < gSampleRate;
+    if (shouldSendEvent) {
+      Services.telemetry
+        .recordEvent(TELEMETRY_EVENTS_ID, "uptake", component, status, extra);
+    }
 
     // Report via histogram in main ping.
     // Note: this is the legacy equivalent of the above event. We keep it for continuity.
     Services.telemetry
       .getKeyedHistogramById(TELEMETRY_HISTOGRAM_ID)
       .add(source, status);
   }
 }
--- a/services/settings/RemoteSettingsClient.jsm
+++ b/services/settings/RemoteSettingsClient.jsm
@@ -393,17 +393,17 @@ class RemoteSettingsClient extends Event
       }
       throw e;
     } finally {
       // No error was reported, this is a success!
       if (reportStatus === null) {
         reportStatus = UptakeTelemetry.STATUS.SUCCESS;
       }
       // Report success/error status to Telemetry.
-      UptakeTelemetry.report(TELEMETRY_COMPONENT, reportStatus, { source: this.identifier, trigger });
+      await UptakeTelemetry.report(TELEMETRY_COMPONENT, reportStatus, { source: this.identifier, trigger });
     }
   }
 
   /**
    * Fetch the signature info from the collection metadata and verifies that the
    * local set of records has the same.
    *
    * @param {Array<Object>} remoteRecords   The list of changes to apply to the local database.
--- a/services/settings/remote-settings.js
+++ b/services/settings/remote-settings.js
@@ -163,17 +163,17 @@ function remoteSettingsFunction() {
     };
 
     // Check if the server backoff time is elapsed.
     if (gPrefs.prefHasUserValue(PREF_SETTINGS_SERVER_BACKOFF)) {
       const backoffReleaseTime = gPrefs.getCharPref(PREF_SETTINGS_SERVER_BACKOFF);
       const remainingMilliseconds = parseInt(backoffReleaseTime, 10) - Date.now();
       if (remainingMilliseconds > 0) {
         // Backoff time has not elapsed yet.
-        UptakeTelemetry.report(TELEMETRY_COMPONENT, UptakeTelemetry.STATUS.BACKOFF, telemetryArgs);
+        await UptakeTelemetry.report(TELEMETRY_COMPONENT, UptakeTelemetry.STATUS.BACKOFF, telemetryArgs);
         throw new Error(`Server is asking clients to back off; retry in ${Math.ceil(remainingMilliseconds / 1000)}s.`);
       } else {
         gPrefs.clearUserPref(PREF_SETTINGS_SERVER_BACKOFF);
       }
     }
 
     Services.obs.notifyObservers(null, "remote-settings:changes-poll-start", JSON.stringify({ expectedTimestamp }));
 
@@ -193,30 +193,30 @@ function remoteSettingsFunction() {
         reportStatus = UptakeTelemetry.STATUS.SERVER_ERROR;
       } else if (/Timeout/.test(e.message)) {
         reportStatus = UptakeTelemetry.STATUS.TIMEOUT_ERROR;
       } else if (/NetworkError/.test(e.message)) {
         reportStatus = UptakeTelemetry.STATUS.NETWORK_ERROR;
       } else {
         reportStatus = UptakeTelemetry.STATUS.UNKNOWN_ERROR;
       }
-      UptakeTelemetry.report(TELEMETRY_COMPONENT, reportStatus, telemetryArgs);
+      await UptakeTelemetry.report(TELEMETRY_COMPONENT, reportStatus, telemetryArgs);
       // No need to go further.
       throw new Error(`Polling for changes failed: ${e.message}.`);
     }
 
     const { serverTimeMillis, changes, currentEtag, backoffSeconds, ageSeconds } = pollResult;
 
     // Report age of server data in Telemetry.
     telemetryArgs = { age: ageSeconds, ...telemetryArgs };
 
     // Report polling success to Uptake Telemetry.
     const reportStatus = changes.length === 0 ? UptakeTelemetry.STATUS.UP_TO_DATE
                                               : UptakeTelemetry.STATUS.SUCCESS;
-    UptakeTelemetry.report(TELEMETRY_COMPONENT, reportStatus, telemetryArgs);
+    await UptakeTelemetry.report(TELEMETRY_COMPONENT, reportStatus, telemetryArgs);
 
     // Check if the server asked the clients to back off (for next poll).
     if (backoffSeconds) {
       const backoffReleaseTime = Date.now() + backoffSeconds * 1000;
       gPrefs.setCharPref(PREF_SETTINGS_SERVER_BACKOFF, backoffReleaseTime);
     }
 
     // Record new update time and the difference between local and server time.
--- a/toolkit/components/normandy/lib/ActionsManager.jsm
+++ b/toolkit/components/normandy/lib/ActionsManager.jsm
@@ -62,17 +62,17 @@ class ActionsManager {
         log.warn(`Could not fetch implementation for ${action.name}: ${err}`);
 
         let status;
         if (/NetworkError/.test(err)) {
           status = Uptake.ACTION_NETWORK_ERROR;
         } else {
           status = Uptake.ACTION_SERVER_ERROR;
         }
-        Uptake.reportAction(action.name, status);
+        await Uptake.reportAction(action.name, status);
       }
     }
 
     const actionNames = Object.keys(this.remoteActionSandboxes);
     log.debug(`Fetched ${actionNames.length} actions from the server: ${actionNames.join(", ")}`);
   }
 
   async preExecution() {
@@ -80,17 +80,17 @@ class ActionsManager {
 
     for (const [actionName, manager] of Object.entries(this.remoteActionSandboxes)) {
       try {
         await manager.runAsyncCallback("preExecution");
         manager.disabled = false;
       } catch (err) {
         log.error(`Could not run pre-execution hook for ${actionName}:`, err.message);
         manager.disabled = true;
-        Uptake.reportAction(actionName, Uptake.ACTION_PRE_EXECUTION_ERROR);
+        await Uptake.reportAction(actionName, Uptake.ACTION_PRE_EXECUTION_ERROR);
       }
     }
   }
 
   async runRecipe(recipe) {
     let actionName = recipe.action;
 
     if (actionName in this.localActions) {
@@ -112,23 +112,23 @@ class ActionsManager {
           await manager.runAsyncCallback("action", recipe);
           status = Uptake.RECIPE_SUCCESS;
         } catch (e) {
           e.message = `Could not execute recipe ${recipe.name}: ${e.message}`;
           Cu.reportError(e);
           status = Uptake.RECIPE_EXECUTION_ERROR;
         }
       }
-      Uptake.reportRecipe(recipe, status);
+      await Uptake.reportRecipe(recipe, status);
     } else {
       log.error(
         `Could not execute recipe ${recipe.name}:`,
         `Action ${recipe.action} is either missing or invalid.`
       );
-      Uptake.reportRecipe(recipe, Uptake.RECIPE_INVALID_ACTION);
+      await Uptake.reportRecipe(recipe, Uptake.RECIPE_INVALID_ACTION);
     }
   }
 
   async finalize() {
     if (this.finalized) {
       throw new Error("ActionsManager has already been finalized");
     }
     this.finalized = true;
@@ -143,20 +143,20 @@ class ActionsManager {
       // Skip if pre-execution failed.
       if (manager.disabled) {
         log.info(`Skipping post-execution hook for ${actionName} due to earlier failure.`);
         continue;
       }
 
       try {
         await manager.runAsyncCallback("postExecution");
-        Uptake.reportAction(actionName, Uptake.ACTION_SUCCESS);
+        await Uptake.reportAction(actionName, Uptake.ACTION_SUCCESS);
       } catch (err) {
         log.info(`Could not run post-execution hook for ${actionName}:`, err.message);
-        Uptake.reportAction(actionName, Uptake.ACTION_POST_EXECUTION_ERROR);
+        await Uptake.reportAction(actionName, Uptake.ACTION_POST_EXECUTION_ERROR);
       }
     }
 
     // Nuke sandboxes
     Object.values(this.remoteActionSandboxes)
       .forEach(manager => manager.removeHold("ActionsManager"));
   }
 }
--- a/toolkit/components/normandy/lib/RecipeRunner.jsm
+++ b/toolkit/components/normandy/lib/RecipeRunner.jsm
@@ -229,17 +229,17 @@ var RecipeRunner = {
     } else {
       for (const recipe of recipesToRun) {
         await actions.runRecipe(recipe);
       }
     }
 
     await actions.finalize();
 
-    Uptake.reportRunner(Uptake.RUNNER_SUCCESS);
+    await Uptake.reportRunner(Uptake.RUNNER_SUCCESS);
   },
 
   /**
    * Return the list of recipes to run, filtered for the current environment.
    */
   async loadRecipes() {
     // If RemoteSettings is enabled, we read the list of recipes from there.
     // The JEXL filtering is done via the provided callback.
@@ -259,17 +259,17 @@ var RecipeRunner = {
       log.error(`Could not fetch recipes from ${apiUrl}: "${e}"`);
 
       let status = Uptake.RUNNER_SERVER_ERROR;
       if (/NetworkError/.test(e)) {
         status = Uptake.RUNNER_NETWORK_ERROR;
       } else if (e instanceof NormandyApi.InvalidSignatureError) {
         status = Uptake.RUNNER_INVALID_SIGNATURE;
       }
-      Uptake.reportRunner(status);
+      await Uptake.reportRunner(status);
       throw e;
     }
     // Evaluate recipe filters
     const recipesToRun = [];
     for (const recipe of recipes) {
       if (await this.checkFilter(recipe)) {
         recipesToRun.push(recipe);
       }
@@ -299,25 +299,25 @@ var RecipeRunner = {
    */
   async checkFilter(recipe) {
     const context = this.getFilterContext(recipe);
     let result;
     try {
       result = await FilterExpressions.eval(recipe.filter_expression, context);
     } catch (err) {
       log.error(`Error checking filter for "${recipe.name}". Filter: [${recipe.filter_expression}]. Error: "${err}"`);
-      Uptake.reportRecipe(recipe, Uptake.RECIPE_FILTER_BROKEN);
+      await Uptake.reportRecipe(recipe, Uptake.RECIPE_FILTER_BROKEN);
       return false;
     }
 
     if (!result) {
       // This represents a terminal state for the given recipe, so
       // report its outcome. Others are reported when executed in
       // ActionsManager.
-      Uptake.reportRecipe(recipe, Uptake.RECIPE_DIDNT_MATCH_FILTER);
+      await Uptake.reportRecipe(recipe, Uptake.RECIPE_DIDNT_MATCH_FILTER);
       return false;
     }
 
     return true;
   },
 
   /**
    * Clear all caches of systems used by RecipeRunner, in preparation
--- a/toolkit/components/normandy/lib/Uptake.jsm
+++ b/toolkit/components/normandy/lib/Uptake.jsm
@@ -30,22 +30,22 @@ var Uptake = {
   RECIPE_SUCCESS: UptakeTelemetry.STATUS.SUCCESS,
 
   // Uptake for the runner as a whole
   RUNNER_INVALID_SIGNATURE: UptakeTelemetry.STATUS.SIGNATURE_ERROR,
   RUNNER_NETWORK_ERROR: UptakeTelemetry.STATUS.NETWORK_ERROR,
   RUNNER_SERVER_ERROR: UptakeTelemetry.STATUS.SERVER_ERROR,
   RUNNER_SUCCESS: UptakeTelemetry.STATUS.SUCCESS,
 
-  reportRunner(status) {
-    UptakeTelemetry.report(COMPONENT, status, { source: `${COMPONENT}/runner` });
+  async reportRunner(status) {
+    await UptakeTelemetry.report(COMPONENT, status, { source: `${COMPONENT}/runner` });
   },
 
-  reportRecipe(recipe, status) {
-    UptakeTelemetry.report(COMPONENT, status, { source: `${COMPONENT}/recipe/${recipe.id}` });
+  async reportRecipe(recipe, status) {
+    await UptakeTelemetry.report(COMPONENT, status, { source: `${COMPONENT}/recipe/${recipe.id}` });
     const revisionId = parseInt(recipe.revision_id, 10);
     Services.telemetry.keyedScalarSet("normandy.recipe_freshness", recipe.id, revisionId);
   },
 
-  reportAction(actionName, status) {
-    UptakeTelemetry.report(COMPONENT, status, { source: `${COMPONENT}/action/${actionName}` });
+  async reportAction(actionName, status) {
+    await UptakeTelemetry.report(COMPONENT, status, { source: `${COMPONENT}/action/${actionName}` });
   },
 };
--- a/toolkit/components/normandy/test/browser/browser_Uptake.js
+++ b/toolkit/components/normandy/test/browser/browser_Uptake.js
@@ -3,12 +3,12 @@
 ChromeUtils.import("resource://gre/modules/Services.jsm", this);
 ChromeUtils.import("resource://normandy/lib/Uptake.jsm", this);
 
 const Telemetry = Services.telemetry;
 
 add_task(async function reportRecipeSubmitsFreshness() {
   Telemetry.clearScalars();
   const recipe = {id: 17, revision_id: "12"};
-  Uptake.reportRecipe(recipe, Uptake.RECIPE_SUCCESS);
+  await Uptake.reportRecipe(recipe, Uptake.RECIPE_SUCCESS);
   const scalars = Telemetry.getSnapshotForKeyedScalars("main", true);
   Assert.deepEqual(scalars.parent["normandy.recipe_freshness"], {17: 12});
 });
--- a/toolkit/components/telemetry/docs/collection/uptake.rst
+++ b/toolkit/components/telemetry/docs/collection/uptake.rst
@@ -13,16 +13,19 @@ The helper — described below — reports predefined update status, which eventually gives a unified way to obtain:
 * the distribution of error causes.
 
 .. note::
 
    Examples of update sources: *remote settings, add-ons update, add-ons, gfx, and plugins blocklists, certificate revocation, certificate pinning, system add-ons delivery…*
 
    Examples of update status: *up-to-date, success, network error, server error, signature error, server backoff, unknown error…*
 
+Every call to the UptakeTelemetry helper registers a point in a single :ref:`keyed histogram <histogram-type-keyed>` whose id is ``UPTAKE_REMOTE_CONTENT_RESULT_1`` with the specified update ``source`` as the key.
+
+Additionally, to provide real-time insight into uptake, a :ref:`Telemetry Event <eventtelemetry>` may be sent. Because telemetry events are more expensive to process than histograms, we take some measures to avoid overwhelming Mozilla systems with the flood of data that this produces. We always send events when not on release channel. On release channel, we only send events from 1% of clients.
 
 Usage
 -----
 
 .. code-block:: js
 
    const { UptakeTelemetry } = ChromeUtils.import("resource://services-common/uptake-telemetry.js", {});
 
@@ -53,18 +56,16 @@ Usage
   - ``UptakeTelemetry.STATUS.UNKNOWN_ERROR``: Uncategorized error.
   - ``UptakeTelemetry.STATUS.CUSTOM_1_ERROR``: Error #1 specific to this update source.
   - ``UptakeTelemetry.STATUS.CUSTOM_2_ERROR``: Error #2 specific to this update source.
   - ``UptakeTelemetry.STATUS.CUSTOM_3_ERROR``: Error #3 specific to this update source.
   - ``UptakeTelemetry.STATUS.CUSTOM_4_ERROR``: Error #4 specific to this update source.
   - ``UptakeTelemetry.STATUS.CUSTOM_5_ERROR``: Error #5 specific to this update source.
 
 
-The data is submitted as a :ref:`Telemetry Event <eventtelemetry>`, and to a single :ref:`keyed histogram <histogram-type-keyed>` whose id is ``UPTAKE_REMOTE_CONTENT_RESULT_1`` and the specified update ``source`` as the key.
-
 Example:
 
 .. code-block:: js
 
    const COMPONENT = "normandy";
    const UPDATE_SOURCE = "update-monitoring";
 
    let status;
@@ -77,25 +78,27 @@ Example:
                  UptakeTelemetry.STATUS.SERVER_ERROR ;
    }
    UptakeTelemetry.report(COMPONENT, status, { source: UPDATE_SOURCE });
 
 
 Additional Event Info
 '''''''''''''''''''''
 
-The Event API allows to report additional information. We support the following optional fields:
+Events sent using the telemetry events API can contain additional information. Uptake Telemetry allows you to add the following extra fields to events by adding them to the ``options`` argument:
 
 - ``trigger``: A label to distinguish what triggered the polling/fetching of remote content (eg. ``"broadcast"``, ``"timer"``, ``"forced"``, ``"manual"``)
 - ``age``: The age of pulled data in seconds (ie. difference between publication time and fetch time).
 
 .. code-block:: js
 
    UptakeTelemetry.report(component, status, { source, trigger: "timer", age: 138 });
 
+Remember that events are sampled on release channel. Those calls to uptake telemetry that do not produce events will ignore these extra fields.
+
 
 Use-cases
 ---------
 
 The following remote data sources are already using this unified histogram.
 
 * remote settings changes monitoring
 * add-ons blocklist