Bug 978158 - Setup FHR provider for Translation project r=felipe,rnewman
authorSteven MacLeod <smacleod@mozilla.com>
Tue, 03 Jun 2014 18:08:35 +0200
changeset 205707 5dc75955fb8ff8bcd7ad472ff40902b8d322acda
parent 205706 bee311ff4f697f393c36c5dcf8f8cd88a8f971bd
child 205708 b05b978bdaf6bf0dceafde7d11240c86a71546cb
push id3741
push userasasaki@mozilla.com
push dateMon, 21 Jul 2014 20:25:18 +0000
treeherdermozilla-beta@4d6f46f5af68 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersfelipe, rnewman
bugs978158
milestone32.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 978158 - Setup FHR provider for Translation project r=felipe,rnewman
browser/components/translation/Translation.jsm
browser/components/translation/moz.build
browser/components/translation/test/browser_translation_exceptions.js
browser/components/translation/test/browser_translation_infobar.js
browser/components/translation/test/test_cld2.js
browser/components/translation/test/unit/test_cld2.js
browser/components/translation/test/unit/test_healthreport.js
browser/components/translation/test/unit/xpcshell.ini
browser/components/translation/test/xpcshell.ini
browser/components/translation/translation.manifest
services/healthreport/docs/dataformat.rst
--- a/browser/components/translation/Translation.jsm
+++ b/browser/components/translation/Translation.jsm
@@ -1,21 +1,31 @@
 /* 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/. */
+ * 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";
 
-this.EXPORTED_SYMBOLS = ["Translation"];
+this.EXPORTED_SYMBOLS = [
+  "Translation",
+  "TranslationProvider",
+];
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
 const TRANSLATION_PREF_SHOWUI = "browser.translation.ui.show";
 
 Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/Metrics.jsm", this);
+Cu.import("resource://gre/modules/Task.jsm", this);
+
+const DAILY_COUNTER_FIELD = {type: Metrics.Storage.FIELD_DAILY_COUNTER};
+const DAILY_LAST_TEXT_FIELD = {type: Metrics.Storage.FIELD_DAILY_LAST_TEXT};
+
 
 this.Translation = {
   supportedSourceLanguages: ["en", "zh", "ja", "es", "de", "fr", "ru", "ar", "ko", "pt"],
   supportedTargetLanguages: ["en", "pl", "tr", "vi"],
 
   _defaultTargetLanguage: "",
   get defaultTargetLanguage() {
     if (!this._defaultTargetLanguage) {
@@ -23,26 +33,30 @@ this.Translation = {
                                       .getService(Ci.nsIXULChromeRegistry)
                                       .getSelectedLocale("global")
                                       .split("-")[0];
     }
     return this._defaultTargetLanguage;
   },
 
   languageDetected: function(aBrowser, aDetectedLanguage) {
+    if (this.supportedSourceLanguages.indexOf(aDetectedLanguage) == -1 ||
+        aDetectedLanguage == this.defaultTargetLanguage)
+      return;
+
+    TranslationHealthReport.recordTranslationOpportunity(aDetectedLanguage);
+
     if (!Services.prefs.getBoolPref(TRANSLATION_PREF_SHOWUI))
       return;
 
-    if (this.supportedSourceLanguages.indexOf(aDetectedLanguage) != -1 &&
-        aDetectedLanguage != this.defaultTargetLanguage) {
-      if (!aBrowser.translationUI)
-        aBrowser.translationUI = new TranslationUI(aBrowser);
+    if (!aBrowser.translationUI)
+      aBrowser.translationUI = new TranslationUI(aBrowser);
 
-      aBrowser.translationUI.showTranslationUI(aDetectedLanguage);
-    }
+
+    aBrowser.translationUI.showTranslationUI(aDetectedLanguage);
   }
 };
 
 /* TranslationUI objects keep the information related to translation for
  * a specific browser.  This object is passed to the translation
  * infobar so that it can initialize itself.  The properties exposed to
  * the infobar are:
  * - detectedLanguage, code of the language detected on the web page.
@@ -182,8 +196,233 @@ TranslationUI.prototype = {
           this.originalShown = false;
         } else {
           this.state = this.STATE_ERROR;
         }
         break;
     }
   }
 };
+
+/**
+ * Helper methods for recording translation data in FHR.
+ */
+let TranslationHealthReport = {
+  /**
+   * Record a translation opportunity in the health report.
+   * @param language
+   *        The language of the page.
+   */
+  recordTranslationOpportunity: function (language) {
+    this._withProvider(provider => provider.recordTranslationOpportunity(language));
+   },
+
+   /**
+   * Record a translation in the health report.
+   * @param langFrom
+   *        The language of the page.
+   * @param langTo
+   *        The language translated to
+   * @param numCharacters
+   *        The number of characters that were translated
+   */
+  recordTranslation: function (langFrom, langTo, numCharacters) {
+    this._withProvider(provider => provider.recordTranslation(langFrom, langTo, numCharacters));
+  },
+
+  /**
+   * Record a change of the detected language in the health report. This should
+   * only be called when actually executing a translation not every time the
+   * user changes in the language in the UI.
+   *
+   * @param beforeFirstTranslation
+   *        A boolean indicating if we are recording a change of detected
+   *        language before translating the page for the first time. If we
+   *        have already translated the page from the detected language and
+   *        the user has manually adjusted the detected language false should
+   *        be passed.
+   */
+  recordLanguageChange: function (beforeFirstTranslation) {
+    this._withProvider(provider => provider.recordLanguageChange(beforeFirstTranslation));
+  },
+
+  /**
+   * Retrieve the translation provider and pass it to the given function.
+   *
+   * @param callback
+   *        The function that will be passed the translation provider.
+   */
+  _withProvider: function (callback) {
+    try {
+      let reporter = Cc["@mozilla.org/datareporting/service;1"]
+                        .getService().wrappedJSObject.healthReporter;
+
+      if (reporter) {
+        reporter.onInit().then(function () {
+          callback(reporter.getProvider("org.mozilla.translation"));
+        }, Cu.reportError);
+      } else {
+        callback(null);
+      }
+    } catch (ex) {
+      Cu.reportError(ex);
+    }
+  }
+};
+
+/**
+ * Holds usage data about the Translation feature.
+ *
+ * This is a special telemetry measurement that is transmitted in the FHR
+ * payload. Data will only be recorded/transmitted when both telemetry and
+ * FHR are enabled. Additionally, if telemetry was previously enabled but
+ * is currently disabled, old recorded data will not be transmitted.
+ */
+function TranslationMeasurement1() {
+  Metrics.Measurement.call(this);
+
+  this._serializers[this.SERIALIZE_JSON].singular =
+    this._wrapJSONSerializer(this._serializers[this.SERIALIZE_JSON].singular);
+
+  this._serializers[this.SERIALIZE_JSON].daily =
+    this._wrapJSONSerializer(this._serializers[this.SERIALIZE_JSON].daily);
+}
+
+TranslationMeasurement1.prototype = Object.freeze({
+  __proto__: Metrics.Measurement.prototype,
+
+  name: "translation",
+  version: 1,
+
+  fields: {
+    translationOpportunityCount: DAILY_COUNTER_FIELD,
+    pageTranslatedCount: DAILY_COUNTER_FIELD,
+    charactersTranslatedCount: DAILY_COUNTER_FIELD,
+    translationOpportunityCountsByLanguage: DAILY_LAST_TEXT_FIELD,
+    pageTranslatedCountsByLanguage: DAILY_LAST_TEXT_FIELD,
+    detectedLanguageChangedBefore: DAILY_COUNTER_FIELD,
+    detectedLanguageChangedAfter: DAILY_COUNTER_FIELD,
+  },
+
+  shouldIncludeField: function (field) {
+    if (!Services.prefs.getBoolPref("toolkit.telemetry.enabled")) {
+      // This measurement should only be included when telemetry is
+      // enabled, so we will not include any fields.
+      return false;
+    }
+
+    return field in this._fields;
+  },
+
+  _getDailyLastTextFieldAsJSON: function(name, date) {
+    let id = this.fieldID(name);
+
+    return this.storage.getDailyLastTextFromFieldID(id, date).then((data) => {
+      if (data.hasDay(date)) {
+        data = JSON.parse(data.getDay(date));
+      } else {
+        data = {};
+      }
+
+      return data;
+    });
+},
+
+  _wrapJSONSerializer: function (serializer) {
+    let _parseInPlace = function(o, k) {
+      if (k in o) {
+        o[k] = JSON.parse(o[k]);
+      }
+    };
+
+    return function (data) {
+      let result = serializer(data);
+
+      // Special case the serialization of these fields so that
+      // they are sent as objects, not stringified objects.
+      _parseInPlace(result, "translationOpportunityCountsByLanguage");
+      _parseInPlace(result, "pageTranslatedCountsByLanguage");
+
+      return result;
+    }
+  }
+});
+
+this.TranslationProvider = function () {
+  Metrics.Provider.call(this);
+}
+
+TranslationProvider.prototype = Object.freeze({
+  __proto__: Metrics.Provider.prototype,
+
+  name: "org.mozilla.translation",
+
+  measurementTypes: [
+    TranslationMeasurement1,
+  ],
+
+  recordTranslationOpportunity: function (language, date=new Date()) {
+    let m = this.getMeasurement(TranslationMeasurement1.prototype.name,
+                                TranslationMeasurement1.prototype.version);
+
+    return this._enqueueTelemetryStorageTask(function* recordTask() {
+      yield m.incrementDailyCounter("translationOpportunityCount", date);
+
+      let langCounts = yield m._getDailyLastTextFieldAsJSON(
+        "translationOpportunityCountsByLanguage", date);
+
+      langCounts[language] = (langCounts[language] || 0) + 1;
+      langCounts = JSON.stringify(langCounts);
+
+      yield m.setDailyLastText("translationOpportunityCountsByLanguage",
+                               langCounts, date);
+
+    }.bind(this));
+  },
+
+  recordTranslation: function (langFrom, langTo, numCharacters, date=new Date()) {
+    let m = this.getMeasurement(TranslationMeasurement1.prototype.name,
+                                TranslationMeasurement1.prototype.version);
+
+    return this._enqueueTelemetryStorageTask(function* recordTask() {
+      yield m.incrementDailyCounter("pageTranslatedCount", date);
+      yield m.incrementDailyCounter("charactersTranslatedCount", date,
+                                    numCharacters);
+
+      let langCounts = yield m._getDailyLastTextFieldAsJSON(
+        "pageTranslatedCountsByLanguage", date);
+
+      let counts = langCounts[langFrom] || {};
+      counts["total"] = (counts["total"] || 0) + 1;
+      counts[langTo] = (counts[langTo] || 0) + 1;
+      langCounts[langFrom] = counts;
+      langCounts = JSON.stringify(langCounts);
+
+      yield m.setDailyLastText("pageTranslatedCountsByLanguage",
+                               langCounts, date);
+    }.bind(this));
+  },
+
+  recordLanguageChange: function (beforeFirstTranslation) {
+    let m = this.getMeasurement(TranslationMeasurement1.prototype.name,
+                                TranslationMeasurement1.prototype.version);
+
+    return this._enqueueTelemetryStorageTask(function* recordTask() {
+      if (beforeFirstTranslation) {
+          yield m.incrementDailyCounter("detectedLanguageChangedBefore");
+        } else {
+          yield m.incrementDailyCounter("detectedLanguageChangedAfter");
+        }
+    }.bind(this));
+  },
+
+  _enqueueTelemetryStorageTask: function (task) {
+    if (!Services.prefs.getBoolPref("toolkit.telemetry.enabled")) {
+      // This measurement should only be included when telemetry is
+      // enabled, so don't record any data.
+      return Promise.resolve(null);
+    }
+
+    return this.enqueueStorageOperation(() => {
+      return Task.spawn(task);
+    });
+  }
+});
--- a/browser/components/translation/moz.build
+++ b/browser/components/translation/moz.build
@@ -16,10 +16,14 @@ EXTRA_JS_MODULES = [
 
 JAR_MANIFESTS += ['jar.mn']
 
 BROWSER_CHROME_MANIFESTS += [
     'test/browser.ini'
 ]
 
 XPCSHELL_TESTS_MANIFESTS += [
-    'test/xpcshell.ini'
+    'test/unit/xpcshell.ini'
 ]
+
+EXTRA_PP_COMPONENTS += [
+    'translation.manifest',
+]
--- a/browser/components/translation/test/browser_translation_exceptions.js
+++ b/browser/components/translation/test/browser_translation_exceptions.js
@@ -1,15 +1,17 @@
 /* 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/. */
 
 // tests the translation infobar, using a fake 'Translation' implementation.
 
-Components.utils.import("resource:///modules/translation/Translation.jsm");
+let tmp = {};
+Cu.import("resource:///modules/translation/Translation.jsm", tmp);
+let {Translation} = tmp;
 
 const kLanguagesPref = "browser.translation.neverForLanguages";
 const kShowUIPref = "browser.translation.ui.show";
 
 function test() {
   waitForExplicitFinish();
 
   Services.prefs.setBoolPref(kShowUIPref, true);
--- a/browser/components/translation/test/browser_translation_infobar.js
+++ b/browser/components/translation/test/browser_translation_infobar.js
@@ -1,15 +1,17 @@
 /* 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/. */
 
 // tests the translation infobar, using a fake 'Translation' implementation.
 
-Components.utils.import("resource:///modules/translation/Translation.jsm");
+let tmp = {};
+Cu.import("resource:///modules/translation/Translation.jsm", tmp);
+let {Translation} = tmp;
 
 const kShowUIPref = "browser.translation.ui.show";
 
 function waitForCondition(condition, nextTest, errorMsg) {
   var tries = 0;
   var interval = setInterval(function() {
     if (tries >= 30) {
       ok(false, errorMsg);
rename from browser/components/translation/test/test_cld2.js
rename to browser/components/translation/test/unit/test_cld2.js
new file mode 100644
--- /dev/null
+++ b/browser/components/translation/test/unit/test_healthreport.js
@@ -0,0 +1,269 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/Services.jsm", this);
+Cu.import("resource://gre/modules/Metrics.jsm", this);
+Cu.import("resource:///modules/translation/Translation.jsm", this);
+Cu.import("resource://testing-common/services/healthreport/utils.jsm", this);
+
+// At end of test, restore original state.
+const ORIGINAL_TELEMETRY_ENABLED = Services.prefs.getBoolPref("toolkit.telemetry.enabled");
+
+function run_test() {
+  run_next_test();
+}
+
+add_test(function setup() {
+  do_get_profile();
+  Services.prefs.setBoolPref("toolkit.telemetry.enabled", true);
+
+  run_next_test();
+});
+
+do_register_cleanup(function() {
+  Services.prefs.setBoolPref("toolkit.telemetry.enabled",
+                             ORIGINAL_TELEMETRY_ENABLED);
+});
+
+add_task(function test_constructor() {
+  let provider = new TranslationProvider();
+});
+
+// Provider can initialize and de-initialize properly.
+add_task(function* test_init() {
+  let storage = yield Metrics.Storage("init");
+  let provider = new TranslationProvider();
+  yield provider.init(storage);
+  yield provider.shutdown();
+  yield storage.close();
+});
+
+// Test recording translation opportunities.
+add_task(function* test_translation_opportunity() {
+  let storage = yield Metrics.Storage("opportunity");
+  let provider = new TranslationProvider();
+  yield provider.init(storage);
+
+  // Initially nothing should be configured.
+  let now = new Date();
+  let m = provider.getMeasurement("translation", 1);
+  let values = yield m.getValues();
+  Assert.equal(values.days.size, 0);
+  Assert.ok(!values.days.hasDay(now));
+
+  // Record an opportunity.
+  yield provider.recordTranslationOpportunity("fr", now);
+
+  values = yield m.getValues();
+  Assert.equal(values.days.size, 1);
+  Assert.ok(values.days.hasDay(now));
+  let day = values.days.getDay(now);
+  Assert.ok(day.has("translationOpportunityCount"));
+  Assert.equal(day.get("translationOpportunityCount"), 1);
+
+  Assert.ok(day.has("translationOpportunityCountsByLanguage"));
+  let countsByLanguage = JSON.parse(day.get("translationOpportunityCountsByLanguage"));
+  Assert.equal(countsByLanguage["fr"], 1);
+
+  // Record more opportunities.
+  yield provider.recordTranslationOpportunity("fr", now);
+  yield provider.recordTranslationOpportunity("fr", now);
+  yield provider.recordTranslationOpportunity("es", now);
+
+  values = yield m.getValues();
+  let day = values.days.getDay(now);
+  Assert.ok(day.has("translationOpportunityCount"));
+  Assert.equal(day.get("translationOpportunityCount"), 4);
+
+  Assert.ok(day.has("translationOpportunityCountsByLanguage"));
+  countsByLanguage = JSON.parse(day.get("translationOpportunityCountsByLanguage"));
+  Assert.equal(countsByLanguage["fr"], 3);
+  Assert.equal(countsByLanguage["es"], 1);
+
+  yield provider.shutdown();
+  yield storage.close();
+});
+
+// Test recording a translation.
+add_task(function* test_record_translation() {
+  let storage = yield Metrics.Storage("translation");
+  let provider = new TranslationProvider();
+  yield provider.init(storage);
+  let now = new Date();
+
+  // Record a translation.
+  yield provider.recordTranslation("fr", "es", 1000, now);
+
+  let m = provider.getMeasurement("translation", 1);
+  let values = yield m.getValues();
+  Assert.equal(values.days.size, 1);
+  Assert.ok(values.days.hasDay(now));
+  let day = values.days.getDay(now);
+  Assert.ok(day.has("pageTranslatedCount"));
+  Assert.equal(day.get("pageTranslatedCount"), 1);
+  Assert.ok(day.has("charactersTranslatedCount"));
+  Assert.equal(day.get("charactersTranslatedCount"), 1000);
+
+  Assert.ok(day.has("pageTranslatedCountsByLanguage"));
+  let countsByLanguage = JSON.parse(day.get("pageTranslatedCountsByLanguage"));
+  Assert.ok("fr" in countsByLanguage);
+  Assert.equal(countsByLanguage["fr"]["total"], 1);
+  Assert.equal(countsByLanguage["fr"]["es"], 1);
+
+  // Record more translations.
+  yield provider.recordTranslation("fr", "es", 1, now);
+  yield provider.recordTranslation("fr", "en", 2, now);
+  yield provider.recordTranslation("es", "en", 4, now);
+
+  values = yield m.getValues();
+  let day = values.days.getDay(now);
+  Assert.ok(day.has("pageTranslatedCount"));
+  Assert.equal(day.get("pageTranslatedCount"), 4);
+  Assert.ok(day.has("charactersTranslatedCount"));
+  Assert.equal(day.get("charactersTranslatedCount"), 1007);
+
+  Assert.ok(day.has("pageTranslatedCountsByLanguage"));
+  let countsByLanguage = JSON.parse(day.get("pageTranslatedCountsByLanguage"));
+  Assert.ok("fr" in countsByLanguage);
+  Assert.equal(countsByLanguage["fr"]["total"], 3);
+  Assert.equal(countsByLanguage["fr"]["es"], 2);
+  Assert.equal(countsByLanguage["fr"]["en"], 1);
+  Assert.ok("es" in countsByLanguage);
+  Assert.equal(countsByLanguage["es"]["total"], 1);
+  Assert.equal(countsByLanguage["es"]["en"], 1);
+
+  yield provider.shutdown();
+  yield storage.close();
+});
+
+// Test recording changing languages.
+add_task(function* test_record_translation() {
+  let storage = yield Metrics.Storage("translation");
+  let provider = new TranslationProvider();
+  yield provider.init(storage);
+  let now = new Date();
+
+  // Record a language change before translation.
+  yield provider.recordLanguageChange(true);
+
+  // Record two language changes after translation.
+  yield provider.recordLanguageChange(false);
+  yield provider.recordLanguageChange(false);
+
+
+  let m = provider.getMeasurement("translation", 1);
+  let values = yield m.getValues();
+  Assert.equal(values.days.size, 1);
+  Assert.ok(values.days.hasDay(now));
+  let day = values.days.getDay(now);
+
+  Assert.ok(day.has("detectedLanguageChangedBefore"));
+  Assert.equal(day.get("detectedLanguageChangedBefore"), 1);
+  Assert.ok(day.has("detectedLanguageChangedAfter"));
+  Assert.equal(day.get("detectedLanguageChangedAfter"), 2);
+
+  yield provider.shutdown();
+  yield storage.close();
+});
+
+// Test the payload after recording with telemetry enabled.
+add_task(function* test_healthreporter_json() {
+  Services.prefs.setBoolPref("toolkit.telemetry.enabled", true);
+
+  let reporter = yield getHealthReporter("healthreporter_json");
+  yield reporter.init();
+  try {
+    let now = new Date();
+    let provider = new TranslationProvider();
+    yield reporter._providerManager.registerProvider(provider);
+
+    yield provider.recordTranslationOpportunity("fr", now);
+    yield provider.recordLanguageChange(true);
+    yield provider.recordTranslation("fr", "en", 1000, now);
+    yield provider.recordLanguageChange(false);
+
+    yield provider.recordTranslationOpportunity("es", now);
+    yield provider.recordTranslation("es", "en", 1000, now);
+
+    yield reporter.collectMeasurements();
+    let payload = yield reporter.getJSONPayload(true);
+    let today = reporter._formatDate(now);
+
+    Assert.ok(today in payload.data.days);
+    let day = payload.data.days[today];
+
+    Assert.ok("org.mozilla.translation.translation" in day);
+
+    let translations = day["org.mozilla.translation.translation"];
+
+    Assert.equal(translations["translationOpportunityCount"], 2);
+    Assert.equal(translations["pageTranslatedCount"], 2);
+    Assert.equal(translations["charactersTranslatedCount"], 2000);
+
+    Assert.ok("translationOpportunityCountsByLanguage" in translations);
+    Assert.equal(translations["translationOpportunityCountsByLanguage"]["fr"], 1);
+    Assert.equal(translations["translationOpportunityCountsByLanguage"]["es"], 1);
+
+    Assert.ok("pageTranslatedCountsByLanguage" in translations);
+    Assert.ok("fr" in translations["pageTranslatedCountsByLanguage"]);
+    Assert.equal(translations["pageTranslatedCountsByLanguage"]["fr"]["total"], 1);
+    Assert.equal(translations["pageTranslatedCountsByLanguage"]["fr"]["en"], 1);
+
+    Assert.ok("es" in translations["pageTranslatedCountsByLanguage"]);
+    Assert.equal(translations["pageTranslatedCountsByLanguage"]["es"]["total"], 1);
+    Assert.equal(translations["pageTranslatedCountsByLanguage"]["es"]["en"], 1);
+
+    Assert.ok("detectedLanguageChangedBefore" in translations);
+    Assert.equal(translations["detectedLanguageChangedBefore"], 1);
+    Assert.ok("detectedLanguageChangedAfter" in translations);
+    Assert.equal(translations["detectedLanguageChangedAfter"], 1);
+  } finally {
+    reporter._shutdown();
+  }
+});
+
+// Test the payload after recording with telemetry disabled.
+add_task(function* test_healthreporter_json() {
+  Services.prefs.setBoolPref("toolkit.telemetry.enabled", false);
+
+  let reporter = yield getHealthReporter("healthreporter_json");
+  yield reporter.init();
+  try {
+    let now = new Date();
+    let provider = new TranslationProvider();
+    yield reporter._providerManager.registerProvider(provider);
+
+    yield provider.recordTranslationOpportunity("fr", now);
+    yield provider.recordLanguageChange(true);
+    yield provider.recordTranslation("fr", "en", 1000, now);
+    yield provider.recordLanguageChange(false);
+
+    yield provider.recordTranslationOpportunity("es", now);
+    yield provider.recordTranslation("es", "en", 1000, now);
+
+    yield reporter.collectMeasurements();
+    let payload = yield reporter.getJSONPayload(true);
+    let today = reporter._formatDate(now);
+
+    Assert.ok(today in payload.data.days);
+    let day = payload.data.days[today];
+
+    Assert.ok("org.mozilla.translation.translation" in day);
+
+    let translations = day["org.mozilla.translation.translation"];
+
+    Assert.ok(!("translationOpportunityCount" in translations));
+    Assert.ok(!("pageTranslatedCount" in translations));
+    Assert.ok(!("charactersTranslatedCount" in translations));
+    Assert.ok(!("translationOpportunityCountsByLanguage" in translations));
+    Assert.ok(!("pageTranslatedCountsByLanguage" in translations));
+    Assert.ok(!("detectedLanguageChangedBefore" in translations));
+    Assert.ok(!("detectedLanguageChangedAfter" in translations));
+  } finally {
+    reporter._shutdown();
+  }
+});
rename from browser/components/translation/test/xpcshell.ini
rename to browser/components/translation/test/unit/xpcshell.ini
--- a/browser/components/translation/test/xpcshell.ini
+++ b/browser/components/translation/test/unit/xpcshell.ini
@@ -1,6 +1,7 @@
 [DEFAULT]
 head = 
 tail = 
 firefox-appdir = browser
 
 [test_cld2.js]
+[test_healthreport.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/translation/translation.manifest
@@ -0,0 +1,3 @@
+#ifdef MOZ_SERVICES_HEALTHREPORT
+category healthreport-js-provider-default TranslationProvider resource:///modules/translation/Translation.jsm
+#endif
--- a/services/healthreport/docs/dataformat.rst
+++ b/services/healthreport/docs/dataformat.rst
@@ -1523,16 +1523,77 @@ Example
       "cpuCount": 8,
       "memoryMB": 8192,
       "architecture": "x86-64",
       "name": "Darwin",
       "version": "12.2.0"
     }
 
 
+org.mozilla.translation.translation
+-----------------------------------
+
+This daily measurement contains information about the usage of the translation
+feature. It is a special telemetry measurement which will only be recorded in
+FHR if telemetry is enabled.
+
+Version 1
+^^^^^^^^^
+
+Daily counts are reported in the following properties:
+
+translationOpportunityCount
+    Integer count of the number of opportunities there were to translate a page.
+pageTranslatedCount
+    Integer count of the number of pages translated.
+charactersTranslatedCount
+    Integer count of the number of characters translated.
+detectedLanguageChangedBefore
+    Integer count of the number of times the user manually adjusted the detected
+    language before translating.
+detectedLanguageChangedAfter
+    Integer count of the number of times the user manually adjusted the detected
+    language after having first translated the page.
+
+Additional daily counts broken down by language are reported in the following
+properties:
+
+translationOpportunityCountsByLanguage
+    A mapping from language to count of opportunities to translate that
+    language.
+pageTranslatedCountsByLanguage
+    A mapping from language to the counts of pages translated from that
+    language. Each language entry will be an object containing a "total" member
+    along with individual counts for each language translated to.
+
+Example
+^^^^^^^
+
+::
+
+    "org.mozilla.translation.translation": {
+      "_v": 1,
+      "translationOpportunityCount": 134,
+      "pageTranslatedCount": 6,
+      "charactersTranslatedCount": "1126",
+      "detectedLanguageChangedBefore": 1,
+      "detectedLanguageChangedAfter": 2,
+      "translationOpportunityCountsByLanguage": {
+        "fr": 100,
+        "es": 34
+      },
+      "pageTranslatedCountsByLanguage": {
+        "fr": {
+          "total": 6,
+          "es": 5,
+          "en": 1
+        }
+      }
+    }
+
 
 org.mozilla.experiments.info
 ----------------------------------
 
 Daily measurement reporting information about the Telemetry Experiments service.
 
 Version 1
 ^^^^^^^^^