Bug 1121013 part C - report a new crash ping type on main-process crashes, and record submission rate/failure information in telemetry. Plugin/content crashes are already recorded via SUBPROCESS_ABNORMAL_ABORT and SUBPROCESS_CRASHES_WITH_DUMP and this patch leaves that unchanged. r=gfritzsche
authorBenjamin Smedberg <benjamin@smedbergs.us>
Mon, 30 Mar 2015 17:48:11 -0400
changeset 248036 6034dc30409f9da00973f116b9c0f6ae14fa14b8
parent 248035 01a6f68c4e53ed187a4bb059a48b324d2260ee50
child 248037 986f641d87e4115fe9a0a909c7aa698d3b2ec9e0
push id60888
push userkwierso@gmail.com
push dateThu, 11 Jun 2015 01:38:38 +0000
treeherdermozilla-inbound@39e638ed06bf [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgfritzsche
bugs1121013
milestone41.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 1121013 part C - report a new crash ping type on main-process crashes, and record submission rate/failure information in telemetry. Plugin/content crashes are already recorded via SUBPROCESS_ABNORMAL_ABORT and SUBPROCESS_CRASHES_WITH_DUMP and this patch leaves that unchanged. r=gfritzsche
toolkit/components/crashes/CrashManager.jsm
toolkit/components/crashes/tests/xpcshell/test_crash_manager.js
toolkit/components/crashes/tests/xpcshell/test_crash_store.js
toolkit/components/telemetry/Histograms.json
toolkit/components/telemetry/TelemetryEnvironment.jsm
toolkit/components/telemetry/docs/crash-ping.rst
toolkit/components/telemetry/docs/pings.rst
toolkit/components/telemetry/moz.build
toolkit/components/telemetry/tests/unit/TelemetryArchiveTesting.jsm
--- a/toolkit/components/crashes/CrashManager.jsm
+++ b/toolkit/components/crashes/CrashManager.jsm
@@ -1,24 +1,26 @@
 /* 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";
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+const myScope = this;
 
 Cu.import("resource://gre/modules/Log.jsm", this);
-Cu.import("resource://gre/modules/osfile.jsm", this)
+Cu.import("resource://gre/modules/osfile.jsm", this);
 Cu.import("resource://gre/modules/Promise.jsm", this);
 Cu.import("resource://gre/modules/Services.jsm", this);
 Cu.import("resource://gre/modules/Task.jsm", this);
 Cu.import("resource://gre/modules/Timer.jsm", this);
 Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
 Cu.import("resource://services-common/utils.js", this);
+Cu.import("resource://gre/modules/TelemetryController.jsm");
 
 this.EXPORTED_SYMBOLS = [
   "CrashManager",
 ];
 
 /**
  * How long to wait after application startup before crash event files are
  * automatically aggregated.
@@ -524,16 +526,42 @@ this.CrashManager.prototype = Object.fre
           let crashID = lines[0];
           let metadata = {};
           for (let i = 1; i < lines.length; i++) {
             let [key, val] = lines[i].split("=");
             metadata[key] = val;
           }
           store.addCrash(this.PROCESS_TYPE_MAIN, this.CRASH_TYPE_CRASH,
                          crashID, date, metadata);
+
+          // If we have a saved environment, use it. Otherwise report
+          // the current environment.
+          let crashEnvironment = null;
+          let reportMeta = Cu.cloneInto(metadata, myScope);
+          if ('TelemetryEnvironment' in reportMeta) {
+            try {
+              crashEnvironment = JSON.parse(reportMeta.TelemetryEnvironment);
+            } catch(e) {
+              Cu.reportError(e);
+            }
+            delete reportMeta.TelemetryEnvironment;
+          }
+          TelemetryController.submitExternalPing("crash",
+            {
+              version: 1,
+              crashDate: date.toISOString().slice(0, 10), // YYYY-MM-DD
+              metadata: reportMeta,
+              hasCrashEnvironment: (crashEnvironment !== null),
+            },
+            {
+              retentionDays: 180,
+              addClientId: true,
+              addEnvironment: true,
+              overrideEnvironment: crashEnvironment,
+            });
           break;
 
         case "crash.submission.1":
           if (lines.length == 3) {
             let [crashID, result, remoteID] = lines;
             store.addCrash(this.PROCESS_TYPE_MAIN, this.CRASH_TYPE_CRASH,
                            crashID, date);
 
@@ -768,24 +796,20 @@ CrashStore.prototype = Object.freeze({
         // days stored in the payload matches up to actual data.
         let actualCounts = new Map();
 
         // In the past, submissions were stored as separate crash records
         // with an id of e.g. "someID-submission". If we find IDs ending
         // with "-submission", we will need to convert the data to be stored
         // as actual submissions.
         //
-        // TODO: The old way of storing submissions was used from FF33 - FF34.
-        // We should drop the conversion code after a few releases. See bug
-        // 1056157.
-        let hasSubmissionsStoredAsCrashes = false;
-
+        // The old way of storing submissions was used from FF33 - FF34. We
+        // drop this old data on the floor.
         for (let id in data.crashes) {
           if (id.endsWith("-submission")) {
-            hasSubmissionsStoredAsCrashes = true;
             continue;
           }
 
           let crash = data.crashes[id];
           let denormalized = this._denormalize(crash);
 
           denormalized.submissions = new Map();
           if (crash.submissions) {
@@ -806,42 +830,16 @@ CrashStore.prototype = Object.freeze({
           if (denormalized.metadata && 
               denormalized.metadata.OOMAllocationSize) {
             let oomKey = key + "-oom";
             actualCounts.set(oomKey, (actualCounts.get(oomKey) || 0) + 1);
           }
 
         }
 
-        if (hasSubmissionsStoredAsCrashes) {
-          for (let id in data.crashes) {
-            if (!id.endsWith("-submission")) {
-              continue;
-            }
-
-            // This type of record will contain e.g.:
-            // {
-            //   "id": "crash1-submission",
-            //   "type": "main-crash-submission-succeeded",
-            //   "crashDate": "...",
-            // }
-            let submissionData = this._denormalize(data.crashes[id]);
-
-            let crashID = id.replace(/-submission$/, "");
-            let result = submissionData.type.endsWith("-succeeded") ?
-              CrashManager.prototype.SUBMISSION_RESULT_OK :
-              CrashManager.prototype.SUBMISSION_RESULT_FAILED;
-
-            this.addSubmissionAttempt(crashID, "converted",
-                                      submissionData.crashDate);
-            this.addSubmissionResult(crashID, "converted",
-                                     submissionData.crashDate, result);
-          }
-        }
-
         // The validation in this loop is arguably not necessary. We perform
         // it as a defense against unknown bugs.
         for (let dayKey in data.countsByDay) {
           let day = parseInt(dayKey, 10);
           for (let type in data.countsByDay[day]) {
             this._ensureCountsForDay(day);
 
             let count = data.countsByDay[day][type];
@@ -1155,73 +1153,69 @@ CrashStore.prototype = Object.freeze({
         crashes.push(crash);
       }
     }
 
     return crashes;
   },
 
   /**
-   * Obtain a particular crash submission from its ID.
-   *
-   * @return undefined | submission object
-   */
-  getSubmission: function (crashID, submissionID) {
-    let crash = this._data.crashes.get(crashID);
-    if (!crash || !submissionID) {
-      return undefined;
-    }
-
-    return crash.submissions.get(submissionID);
-  },
-
-  /**
    * Ensure the submission record is present in storage.
+   * @returns [submission, crash]
    */
   _ensureSubmissionRecord: function (crashID, submissionID) {
     let crash = this._data.crashes.get(crashID);
     if (!crash || !submissionID) {
       return null;
     }
 
     if (!crash.submissions.has(submissionID)) {
       crash.submissions.set(submissionID, {
         requestDate: null,
         responseDate: null,
         result: null,
       });
     }
 
-    return crash.submissions.get(submissionID);
+    return [crash.submissions.get(submissionID), crash];
   },
 
   /**
    * @return boolean True if the attempt was recorded.
    */
   addSubmissionAttempt: function (crashID, submissionID, date) {
-    let submission = this._ensureSubmissionRecord(crashID, submissionID);
+    let [submission, crash] =
+      this._ensureSubmissionRecord(crashID, submissionID);
     if (!submission) {
       return false;
     }
 
     submission.requestDate = date;
+    Services.telemetry.getKeyedHistogramById("PROCESS_CRASH_SUBMIT_ATTEMPT")
+      .add(crash.type, 1);
     return true;
   },
 
   /**
    * @return boolean True if the response was recorded.
    */
   addSubmissionResult: function (crashID, submissionID, date, result) {
-    let submission = this.getSubmission(crashID, submissionID);
+    let crash = this._data.crashes.get(crashID);
+    if (!crash || !submissionID) {
+      return false;
+    }
+    let submission = crash.submissions.get(submissionID);
     if (!submission) {
       return false;
     }
 
     submission.responseDate = date;
     submission.result = result;
+    Services.telemetry.getKeyedHistogramById("PROCESS_CRASH_SUBMIT_SUCCESS")
+      .add(crash.type, result == "ok");
     return true;
   },
 
   /**
    * @return boolean True if the classifications were set.
    */
   setCrashClassifications: function (crashID, classifications) {
     let crash = this._data.crashes.get(crashID);
--- a/toolkit/components/crashes/tests/xpcshell/test_crash_manager.js
+++ b/toolkit/components/crashes/tests/xpcshell/test_crash_manager.js
@@ -4,28 +4,31 @@
 "use strict";
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
 let bsp = Cu.import("resource://gre/modules/CrashManager.jsm", this);
 Cu.import("resource://gre/modules/Promise.jsm", this);
 Cu.import("resource://gre/modules/Task.jsm", this);
 Cu.import("resource://gre/modules/osfile.jsm", this);
+Cu.import("resource://gre/modules/TelemetryEnvironment.jsm", this);
 
 Cu.import("resource://testing-common/CrashManagerTest.jsm", this);
+Cu.import("resource://testing-common/TelemetryArchiveTesting.jsm", this);
 
 const DUMMY_DATE = new Date(Date.now() - 10 * 24 * 60 * 60 * 1000);
 DUMMY_DATE.setMilliseconds(0);
 
 const DUMMY_DATE_2 = new Date(Date.now() - 20 * 24 * 60 * 60 * 1000);
 DUMMY_DATE_2.setMilliseconds(0);
 
 function run_test() {
   do_get_profile();
   configureLogging();
+  TelemetryArchiveTesting.setup();
   run_next_test();
 }
 
 add_task(function* test_constructor_ok() {
   let m = new CrashManager({
     pendingDumpsDir: "/foo",
     submittedDumpsDir: "/bar",
     eventsDirs: [],
@@ -202,28 +205,69 @@ add_task(function* test_schedule_mainten
 
   yield m.scheduleMaintenance(25);
   let crashes = yield m.getCrashes();
   Assert.equal(crashes.length, 1);
   Assert.equal(crashes[0].id, "id1");
 });
 
 add_task(function* test_main_crash_event_file() {
+  let ac = new TelemetryArchiveTesting.Checker();
+  yield ac.promiseInit();
+  let theEnvironment = TelemetryEnvironment.currentEnvironment;
+
   let m = yield getManager();
-  yield m.createEventsFile("1", "crash.main.2", DUMMY_DATE, "id1\nk1=v1\nk2=v2");
+  yield m.createEventsFile("1", "crash.main.2", DUMMY_DATE, "id1\nk1=v1\nk2=v2\nTelemetryEnvironment=" + JSON.stringify(theEnvironment));
   let count = yield m.aggregateEventsFiles();
   Assert.equal(count, 1);
 
   let crashes = yield m.getCrashes();
   Assert.equal(crashes.length, 1);
   Assert.equal(crashes[0].id, "id1");
   Assert.equal(crashes[0].type, "main-crash");
-  Assert.deepEqual(crashes[0].metadata, { k1: "v1", k2: "v2"});
+  Assert.equal(crashes[0].metadata.k1, "v1");
+  Assert.equal(crashes[0].metadata.k2, "v2");
+  Assert.ok(crashes[0].metadata.TelemetryEnvironment);
+  Assert.equal(Object.getOwnPropertyNames(crashes[0].metadata).length, 3);
   Assert.deepEqual(crashes[0].crashDate, DUMMY_DATE);
 
+  let found = yield ac.promiseFindPing("crash", [
+    [["payload", "hasCrashEnvironment"], true],
+    [["payload", "metadata", "k1"], "v1"],
+  ]);
+  Assert.ok(found, "Telemetry ping submitted for found crash");
+  Assert.deepEqual(found.environment, theEnvironment, "The saved environment should be present");
+
+  count = yield m.aggregateEventsFiles();
+  Assert.equal(count, 0);
+});
+
+add_task(function* test_main_crash_event_file_noenv() {
+  let ac = new TelemetryArchiveTesting.Checker();
+  yield ac.promiseInit();
+
+  let m = yield getManager();
+  yield m.createEventsFile("1", "crash.main.2", DUMMY_DATE, "id1\nk1=v3\nk2=v2");
+  let count = yield m.aggregateEventsFiles();
+  Assert.equal(count, 1);
+
+  let crashes = yield m.getCrashes();
+  Assert.equal(crashes.length, 1);
+  Assert.equal(crashes[0].id, "id1");
+  Assert.equal(crashes[0].type, "main-crash");
+  Assert.deepEqual(crashes[0].metadata, { k1: "v3", k2: "v2"});
+  Assert.deepEqual(crashes[0].crashDate, DUMMY_DATE);
+
+  let found = yield ac.promiseFindPing("crash", [
+    [["payload", "hasCrashEnvironment"], false],
+    [["payload", "metadata", "k1"], "v3"],
+  ]);
+  Assert.ok(found, "Telemetry ping submitted for found crash");
+  Assert.ok(found.environment, "There is an environment");
+
   count = yield m.aggregateEventsFiles();
   Assert.equal(count, 0);
 });
 
 add_task(function* test_crash_submission_event_file() {
   let m = yield getManager();
   yield m.createEventsFile("1", "crash.main.2", DUMMY_DATE, "crash1");
   yield m.createEventsFile("1-submission", "crash.submission.1", DUMMY_DATE_2,
--- a/toolkit/components/crashes/tests/xpcshell/test_crash_store.js
+++ b/toolkit/components/crashes/tests/xpcshell/test_crash_store.js
@@ -487,97 +487,52 @@ add_task(function* test_high_water() {
   Assert.equal(s._countsByDay.get(day1).
                  get(PROCESS_TYPE_PLUGIN + "-" + CRASH_TYPE_CRASH),
                s.HIGH_WATER_DAILY_THRESHOLD + 1);
   Assert.equal(s._countsByDay.get(day1).
                  get(PROCESS_TYPE_PLUGIN + "-" + CRASH_TYPE_HANG),
                s.HIGH_WATER_DAILY_THRESHOLD + 1);
 });
 
-add_task(function* test_getSubmission() {
-  let s = yield getStore();
-
-  Assert.equal(s.getSubmission("crash1", "sub1"), undefined);
-  Assert.ok(s.addCrash(PROCESS_TYPE_MAIN, CRASH_TYPE_CRASH, "crash1",
-                       DUMMY_DATE));
-  Assert.equal(s.getSubmission("crash1", "sub1"), undefined);
-  Assert.ok(s.addSubmissionAttempt("crash1", "sub1", DUMMY_DATE));
-  Assert.ok(s.getSubmission("crash1", "sub1"));
-});
-
 add_task(function* test_addSubmission() {
   let s = yield getStore();
 
   Assert.ok(s.addCrash(PROCESS_TYPE_MAIN, CRASH_TYPE_CRASH, "crash1",
                        DUMMY_DATE));
 
   Assert.ok(s.addSubmissionAttempt("crash1", "sub1", DUMMY_DATE));
 
-  let submission = s.getSubmission("crash1", "sub1");
+  let crash = s.getCrash("crash1");
+  let submission = crash.submissions.get("sub1");
   Assert.ok(!!submission);
   Assert.equal(submission.requestDate.getTime(), DUMMY_DATE.getTime());
   Assert.equal(submission.responseDate, null);
   Assert.equal(submission.result, null);
 
   Assert.ok(s.addSubmissionResult("crash1", "sub1", DUMMY_DATE_2,
                                   SUBMISSION_RESULT_FAILED));
 
-  submission = s.getSubmission("crash1", "sub1");
+  crash = s.getCrash("crash1");
+  Assert.equal(crash.submissions.size, 1);
+  submission = crash.submissions.get("sub1");
   Assert.ok(!!submission);
   Assert.equal(submission.requestDate.getTime(), DUMMY_DATE.getTime());
   Assert.equal(submission.responseDate.getTime(), DUMMY_DATE_2.getTime());
   Assert.equal(submission.result, SUBMISSION_RESULT_FAILED);
 
   Assert.ok(s.addSubmissionAttempt("crash1", "sub2", DUMMY_DATE));
   Assert.ok(s.addSubmissionResult("crash1", "sub2", DUMMY_DATE_2,
                                   SUBMISSION_RESULT_OK));
 
-  submission = s.getSubmission("crash1", "sub2");
+  Assert.equal(crash.submissions.size, 2);
+  submission = crash.submissions.get("sub2");
   Assert.ok(!!submission);
   Assert.equal(submission.result, SUBMISSION_RESULT_OK);
 });
 
-add_task(function* test_convertSubmissionsStoredAsCrashes() {
-  let s = yield getStore();
-
-  let addSubmissionAsCrash = (processType, crashType, succeeded, id, date) => {
-    id = id + "-submission";
-    let process = processType + "-" + crashType + "-submission";
-    let submissionType = succeeded ? "succeeded" : "failed";
-    return s.addCrash(process, submissionType, id, date);
-  };
-
-  Assert.ok(s.addCrash(PROCESS_TYPE_MAIN, CRASH_TYPE_CRASH, "crash1",
-                       new Date()));
-  Assert.ok(addSubmissionAsCrash(PROCESS_TYPE_MAIN, CRASH_TYPE_CRASH, true,
-                                 "crash1", DUMMY_DATE));
-
-  Assert.ok(s.addCrash(PROCESS_TYPE_PLUGIN, CRASH_TYPE_HANG, "hang1",
-                       new Date()));
-  Assert.ok(addSubmissionAsCrash(PROCESS_TYPE_PLUGIN, CRASH_TYPE_HANG, false,
-                                 "hang1", DUMMY_DATE_2));
-
-  Assert.equal(s.crashes.length, 4);
-  yield s.save();
-  yield s.load();
-  Assert.equal(s.crashes.length, 2);
-
-  let submission = s.getSubmission("crash1", "converted");
-  Assert.ok(!!submission);
-  Assert.equal(submission.result, SUBMISSION_RESULT_OK);
-  Assert.equal(submission.requestDate.getTime(), DUMMY_DATE.getTime());
-  Assert.equal(submission.responseDate.getTime(), DUMMY_DATE.getTime());
-
-  submission = s.getSubmission("hang1", "converted");
-  Assert.ok(!!submission);
-  Assert.equal(submission.result, SUBMISSION_RESULT_FAILED);
-  Assert.equal(submission.requestDate.getTime(), DUMMY_DATE_2.getTime());
-  Assert.equal(submission.responseDate.getTime(), DUMMY_DATE_2.getTime());
-});
-
 add_task(function* test_setCrashClassification() {
   let s = yield getStore();
 
   Assert.ok(s.addCrash(PROCESS_TYPE_MAIN, CRASH_TYPE_CRASH, "crash1",
                        new Date()));
   let classifications = s.crashes[0].classifications;
   Assert.ok(!!classifications);
   Assert.equal(classifications.length, 0);
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -7620,16 +7620,30 @@
     "description": "Counts of plugin/content process abnormal shutdown, whether or not a crash report was available."
   },
   "SUBPROCESS_CRASHES_WITH_DUMP": {
     "expires_in_version": "never",
     "kind": "count",
     "keyed": true,
     "description": "Counts of plugin and content process crashes which are reported with a crash dump."
   },
+  "PROCESS_CRASH_SUBMIT_ATTEMPT": {
+    "expires_in_version": "never",
+    "kind": "count",
+    "keyed": true,
+    "releaseChannelCollection": "opt-out",
+    "description": "An attempt to submit a crash. Keyed on the CrashManager Crash.type."
+  },
+  "PROCESS_CRASH_SUBMIT_SUCCESS": {
+    "expires_in_version": "never",
+    "kind": "boolean",
+    "keyed": true,
+    "releaseChannelCollection": "opt-out",
+    "description": "The submission status when main/plugin/content crashes are submitted. 1 is success, 0 is failure. Keyed on the CrashManager Crash.type."
+  },
   "STUMBLER_TIME_BETWEEN_UPLOADS_SEC": {
     "expires_in_version": "45",
     "kind": "exponential",
     "n_buckets": 50,
     "high": 259200,
     "description": "Stumbler: The time in seconds between uploads."
   },
   "STUMBLER_VOLUME_BYTES_UPLOADED_PER_SEC": {
--- a/toolkit/components/telemetry/TelemetryEnvironment.jsm
+++ b/toolkit/components/telemetry/TelemetryEnvironment.jsm
@@ -895,23 +895,23 @@ EnvironmentCache.prototype = {
   },
 
   /**
    * Get the build data in object form.
    * @return Object containing the build data.
    */
   _getBuild: function () {
     let buildData = {
-      applicationId: Services.appinfo.ID,
-      applicationName: Services.appinfo.name,
+      applicationId: Services.appinfo.ID || null,
+      applicationName: Services.appinfo.name || null,
       architecture: Services.sysinfo.get("arch"),
-      buildId: Services.appinfo.appBuildID,
-      version: Services.appinfo.version,
-      vendor: Services.appinfo.vendor,
-      platformVersion: Services.appinfo.platformVersion,
+      buildId: Services.appinfo.appBuildID || null,
+      version: Services.appinfo.version || null,
+      vendor: Services.appinfo.vendor || null,
+      platformVersion: Services.appinfo.platformVersion || null,
       xpcomAbi: Services.appinfo.XPCOMABI,
       hotfixVersion: Preferences.get(PREF_HOTFIX_LASTVERSION, null),
     };
 
     // Add |architecturesInBinary| only for Mac Universal builds.
     if ("@mozilla.org/xpcom/mac-utils;1" in Cc) {
       let macUtils = Cc["@mozilla.org/xpcom/mac-utils;1"].getService(Ci.nsIMacUtils);
       if (macUtils && macUtils.isUniversalBinary) {
new file mode 100644
--- /dev/null
+++ b/toolkit/components/telemetry/docs/crash-ping.rst
@@ -0,0 +1,24 @@
+
+"crash" ping
+============
+
+This ping is captured after the main Firefox process crashes, whether or not the crash report is submitted to crash-stats.mozilla.org. It includes non-identifying metadata about the crash.
+
+The environment block that is sent with this ping varies: if Firefox was running long enough to record the environment block before the crash, then the environment at the time of the crash will be recorded and ``hasCrashEnvironment`` will be true. If Firefox crashed before the environment was recorded, ``hasCrashEnvironment`` will be false and the recorded environment will be the environment at time of submission.
+
+The client ID is submitted with this ping.
+
+Structure::
+
+    {
+      version: 1,
+      type: "crash",
+      ... common ping data
+      clientId: <UUID>,
+      environment: { ... },
+      payload: {
+        crashDate: "YYYY-MM-DD",
+        metadata: {...}, // Annotations saved while Firefox was running. See nsExceptionHandler.cpp for more information
+        hasCrashEnvironment: bool
+      }
+    }
--- a/toolkit/components/telemetry/docs/pings.rst
+++ b/toolkit/components/telemetry/docs/pings.rst
@@ -24,17 +24,18 @@ The telemetry server team is working tow
 * `2XX` - success, don't resubmit
 * `4XX` - there was some problem with the request - the client should not try to resubmit as it would just receive the same response
 * `5XX` - there was a server-side error, the client should try to resubmit later
 
 Ping types
 ==========
 
 * :doc:`main <main-ping>` - contains the information collected by Telemetry (Histograms, hang stacks, ...)
-* :doc:`saved-session <main-ping>` - has the same format as a main ping, but it contains the *"classic"* Telemetry payload with measurements covering the whole browser session. This is only a separate type to make storage of saved-session easier server-side.
+* :doc:`saved-session <main-ping>` - has the same format as a main ping, but it contains the *"classic"* Telemetry payload with measurements covering the whole browser session. This is only a separate type to make storage of saved-session easier server-side. This is temporary and will be removed soon.
+* :doc:`crash <crash-ping>` - a ping that is captured and sent after Firefox crashes.
 * ``activation`` - *planned* - sent right after installation or profile creation
 * ``upgrade`` - *planned* - sent right after an upgrade
 * ``deletion`` - *planned* - on opt-out we may have to tell the server to delete user data
 
 Archiving
 =========
 
 When archiving is enabled through the relative preference, pings submitted to ``TelemetryController`` are also stored locally in the user profile directory, in `<profile-dir>/datareporting/archived`.
--- a/toolkit/components/telemetry/moz.build
+++ b/toolkit/components/telemetry/moz.build
@@ -41,16 +41,20 @@ EXTRA_JS_MODULES += [
 ]
 
 EXTRA_PP_JS_MODULES += [
     'TelemetryController.jsm',
     'TelemetryEnvironment.jsm',
     'TelemetrySession.jsm',
 ]
 
+TESTING_JS_MODULES += [
+  'tests/unit/TelemetryArchiveTesting.jsm',
+]
+
 FAIL_ON_WARNINGS = True
 
 include('/ipc/chromium/chromium-config.mozbuild')
 
 FINAL_LIBRARY = 'xul'
 
 GENERATED_FILES = [
     'TelemetryHistogramData.inc',
new file mode 100644
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/TelemetryArchiveTesting.jsm
@@ -0,0 +1,86 @@
+const {utils: Cu} = Components;
+Cu.import("resource://gre/modules/TelemetryArchive.jsm");
+Cu.import("resource://testing-common/Assert.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/TelemetryController.jsm");
+
+this.EXPORTED_SYMBOLS = [
+  "TelemetryArchiveTesting",
+];
+
+function checkForProperties(ping, expected) {
+  for (let [props, val] of expected) {
+    let test = ping;
+    for (let prop of props) {
+      test = test[prop];
+      if (test === undefined) {
+        return false;
+      }
+    }
+    if (test !== val) {
+      return false;
+    }
+  }
+  return true;
+}
+
+/**
+ * A helper object that allows test code to check whether a telemetry ping
+ * was properly saved. To use, first initialize to collect the starting pings
+ * and then check for new ping data.
+ */
+function Checker() {
+}
+Checker.prototype = {
+  promiseInit: function() {
+    this._pingMap = new Map();
+    return TelemetryArchive.promiseArchivedPingList().then((plist) => {
+      for (let ping of plist) {
+        this._pingMap.set(ping.id, ping);
+      }
+    });
+  },
+
+  /**
+   * Find and return a new ping with certain properties.
+   *
+   * @param expected: an array of [['prop'...], 'value'] to check
+   * For example:
+   * [
+   *   [['environment', 'build', 'applicationId'], '20150101010101'],
+   *   [['version'], 1],
+   *   [['metadata', 'OOMAllocationSize'], 123456789],
+   * ]
+   * @returns a matching ping if found, or null
+   */
+  promiseFindPing: Task.async(function*(type, expected) {
+    let candidates = [];
+    let plist = yield TelemetryArchive.promiseArchivedPingList();
+    for (let ping of plist) {
+      if (this._pingMap.has(ping.id)) {
+        continue;
+      }
+      if (ping.type == type) {
+        candidates.push(ping);
+      }
+    }
+
+    for (let candidate of candidates) {
+      let ping = yield TelemetryArchive.promiseArchivedPingById(candidate.id);
+      if (checkForProperties(ping, expected)) {
+        return ping;
+      }
+    }
+    return null;
+  }),
+};
+
+const TelemetryArchiveTesting = {
+  setup: function() {
+    Services.prefs.setCharPref("toolkit.telemetry.log.level", "Trace");
+    Services.prefs.setBoolPref("toolkit.telemetry.archive.enabled", true);
+  },
+
+  Checker: Checker,
+};