Bug 765285 - Include last 3 days of crash IDs in about:support. r=Mossop,dolske
☠☠ backed out by 0add31e02ee5 ☠ ☠
authorSebastian Hengst <archaeopteryx@coole-files.de>
Sat, 26 Oct 2013 20:10:55 +0200
changeset 152368 f78e6b91ad8d5fe20eac78651f29c109aa14e7fe
parent 152367 c63dfd127858906bc5d4ce0d9cac175d89864880
child 152369 6a3066431da8061dd94ab4010ba5ac811f3cf104
push idunknown
push userunknown
push dateunknown
reviewersMossop, dolske
bugs765285
milestone27.0a1
Bug 765285 - Include last 3 days of crash IDs in about:support. r=Mossop,dolske
toolkit/content/aboutSupport.js
toolkit/content/aboutSupport.xhtml
toolkit/crashreporter/CrashReports.jsm
toolkit/crashreporter/content/crashes.js
toolkit/crashreporter/moz.build
toolkit/locales/en-US/chrome/global/aboutSupport.dtd
toolkit/locales/en-US/chrome/global/aboutSupport.properties
toolkit/modules/Troubleshoot.jsm
toolkit/modules/tests/browser/browser_Troubleshoot.js
--- a/toolkit/content/aboutSupport.js
+++ b/toolkit/content/aboutSupport.js
@@ -1,16 +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/. */
 
 const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
 
 Components.utils.import("resource://gre/modules/Services.jsm");
 Components.utils.import("resource://gre/modules/Troubleshoot.jsm");
+Components.utils.import("resource://gre/modules/PluralForm.jsm");
 Components.utils.import("resource://gre/modules/ResetProfile.jsm");
 
 window.addEventListener("load", function onload(event) {
   window.removeEventListener("load", onload, false);
   Troubleshoot.snapshot(function (snapshot) {
     for (let prop in snapshotFormatters)
       snapshotFormatters[prop](snapshot[prop]);
   });
@@ -27,16 +28,83 @@ let snapshotFormatters = {
     $("useragent-box").textContent = data.userAgent;
     $("supportLink").href = data.supportURL;
     let version = data.version;
     if (data.vendor)
       version += " (" + data.vendor + ")";
     $("version-box").textContent = version;
   },
 
+#ifdef MOZ_CRASHREPORTER
+  crashes: function crashes(data) {
+    let strings = stringBundle();
+    let daysRange = Troubleshoot.kMaxCrashAge / (24 * 60 * 60 * 1000);
+    $("crashes-title").textContent =
+      PluralForm.get(daysRange, strings.GetStringFromName("crashesTitle"))
+                .replace("#1", daysRange);
+    let reportURL;
+    try {
+      reportURL = Services.prefs.getCharPref("breakpad.reportURL");
+      // Ignore any non http/https urls
+      if (!/^https?:/i.test(reportURL))
+        reportURL = null;
+    }
+    catch (e) { }
+    if (!reportURL) {
+      $("crashes-noConfig").style.display = "block";
+      $("crashes-noConfig").classList.remove("no-copy");
+      return;
+    }
+    else {
+      $("crashes-allReports").style.display = "block";
+      $("crashes-allReports").classList.remove("no-copy");
+    }
+
+    if (data.pending > 0) {
+      $("crashes-allReportsWithPending").textContent =
+        PluralForm.get(data.pending, strings.GetStringFromName("pendingReports"))
+                  .replace("#1", data.pending);
+    }
+
+    let dateNow = new Date();
+    $.append($("crashes-tbody"), data.submitted.map(function (crash) {
+      let date = new Date(crash.date);
+      let timePassed = dateNow - date;
+      let formattedDate;
+      if (timePassed >= 24 * 60 * 60 * 1000)
+      {
+        let daysPassed = Math.round(timePassed / (24 * 60 * 60 * 1000));
+        let daysPassedString = strings.GetStringFromName("crashesTimeDays");
+        formattedDate = PluralForm.get(daysPassed, daysPassedString)
+                                  .replace("#1", daysPassed);
+      }
+      else if (timePassed >= 60 * 60 * 1000)
+      {
+        let hoursPassed = Math.round(timePassed / (60 * 60 * 1000));
+        let hoursPassedString = strings.GetStringFromName("crashesTimeHours");
+        formattedDate = PluralForm.get(hoursPassed, hoursPassedString)
+                                  .replace("#1", hoursPassed);
+      }
+      else
+      {
+        let minutesPassed = Math.max(Math.round(timePassed / (60 * 1000)), 1);
+        let minutesPassedString = strings.GetStringFromName("crashesTimeMinutes");
+        formattedDate = PluralForm.get(minutesPassed, minutesPassedString)
+                                  .replace("#1", minutesPassed);
+      }
+      return $.new("tr", [
+        $.new("td", [
+          $.new("a", crash.id, null, {href : reportURL + crash.id})
+        ]),
+        $.new("td", formattedDate)
+      ]);
+    }));
+  },
+#endif
+
   extensions: function extensions(data) {
     $.append($("extensions-tbody"), data.map(function (extension) {
       return $.new("tr", [
         $.new("td", extension.name),
         $.new("td", extension.version),
         $.new("td", extension.isActive),
         $.new("td", extension.id),
       ]);
@@ -207,20 +275,24 @@ let snapshotFormatters = {
     $("prefs-user-js-section").style.display = "";
     // Clear the no-copy class
     $("prefs-user-js-section").className = "";
   },
 };
 
 let $ = document.getElementById.bind(document);
 
-$.new = function $_new(tag, textContentOrChildren, className) {
+$.new = function $_new(tag, textContentOrChildren, className, attributes) {
   let elt = document.createElement(tag);
   if (className)
     elt.className = className;
+  if (attributes) {
+    for (let attrName in attributes)
+      elt.setAttribute(attrName, attributes[attrName]);
+  }
   if (Array.isArray(textContentOrChildren))
     this.append(elt, textContentOrChildren);
   else
     elt.textContent = String(textContentOrChildren);
   return elt;
 };
 
 $.append = function $_append(parent, children) {
--- a/toolkit/content/aboutSupport.xhtml
+++ b/toolkit/content/aboutSupport.xhtml
@@ -147,42 +147,57 @@
               &aboutSupport.appBasicsBuildConfig;
             </th>
 
             <td>
               <a href="about:buildconfig">about:buildconfig</a>
             </td>
           </tr>
 
-#ifdef MOZ_CRASHREPORTER
-          <tr class="no-copy">
-            <th class="column">
-              &aboutSupport.appBasicsCrashIDs;
-            </th>
-
-            <td>
-              <a href="about:crashes">about:crashes</a>
-            </td>
-          </tr>
-#endif
-
           <tr class="no-copy">
             <th class="column">
               &aboutSupport.appBasicsMemoryUse;
             </th>
 
             <td>
               <a href="about:memory">about:memory</a>
             </td>
           </tr>
 
         </tbody>
       </table>
 
       <!-- - - - - - - - - - - - - - - - - - - - - -->
+#ifdef MOZ_CRASHREPORTER
+
+      <h2 class="major-section" id="crashes-title">
+        &aboutSupport.crashes.title;
+      </h2>
+
+      <table id="crashes-table">
+        <thead>
+          <tr>
+            <th>
+              &aboutSupport.crashes.id;
+            </th>
+            <th>
+              &aboutSupport.crashes.sendDate;
+            </th>
+          </tr>
+        </thead>
+        <tbody id="crashes-tbody">
+        </tbody>
+      </table>
+      <p id="crashes-allReports" class="no-copy" style="display: none">
+        <a href="about:crashes" id="crashes-allReportsWithPending" style="display: block">&aboutSupport.crashes.allReports;</a>
+      </p>
+      <p id="crashes-noConfig" class="no-copy" style="display: none">&aboutSupport.crashes.noConfig;</p>
+
+#endif
+      <!-- - - - - - - - - - - - - - - - - - - - - -->
 
       <h2 class="major-section">
         &aboutSupport.extensionsTitle;
       </h2>
 
       <table>
         <thead>
           <tr>
new file mode 100644
--- /dev/null
+++ b/toolkit/crashreporter/CrashReports.jsm
@@ -0,0 +1,91 @@
+/* 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/. */
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+this.EXPORTED_SYMBOLS = [
+  "CrashReports"
+];
+
+this.CrashReports = {
+  pendingDir: null,
+  reportsDir: null,
+  submittedDir: null,
+  getReports: function CrashReports_getReports()
+  {
+    let reports = [];
+
+    try {
+      // Ignore any non http/https urls
+      if (!/^https?:/i.test(Services.prefs.getCharPref("breakpad.reportURL")))
+        return reports;
+    }
+    catch (e) { }
+
+    if (this.submittedDir.exists() && this.submittedDir.isDirectory()) {
+      let entries = this.submittedDir.directoryEntries;
+      while (entries.hasMoreElements()) {
+        let file = entries.getNext().QueryInterface(Components.interfaces.nsIFile);
+        let leaf = file.leafName;
+        if (leaf.startsWith("bp-") &&
+            leaf.endsWith(".txt")) {
+          let entry = {
+            id: leaf.slice(0, -4),
+            date: file.lastModifiedTime,
+            pending: false
+          };
+          reports.push(entry);
+        }
+      }
+    }
+
+    if (this.pendingDir.exists() && this.pendingDir.isDirectory()) {
+      let uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
+      let entries = this.pendingDir.directoryEntries;
+      while (entries.hasMoreElements()) {
+        let file = entries.getNext().QueryInterface(Components.interfaces.nsIFile);
+        let leaf = file.leafName;
+        let id = leaf.slice(0, -4);
+        if (leaf.endsWith(".dmp") && uuidRegex.test(id)) {
+          let entry = {
+            id: id,
+            date: file.lastModifiedTime,
+            pending: true
+          };
+          reports.push(entry);
+        }
+      }
+    }
+
+    // Sort reports descending by date
+    return reports.sort( (a, b) => b.date - a.date);
+  }
+}
+
+function CrashReports_pendingDir()
+{
+  let pendingDir = Services.dirsvc.get("UAppData", Components.interfaces.nsIFile);
+  pendingDir.append("Crash Reports");
+  pendingDir.append("pending");
+  return pendingDir;
+}
+
+function CrashReports_reportsDir()
+{
+  let reportsDir = Services.dirsvc.get("UAppData", Components.interfaces.nsIFile);
+  reportsDir.append("Crash Reports");
+  return reportsDir;
+}
+
+function CrashReports_submittedDir()
+{
+  let submittedDir = Services.dirsvc.get("UAppData", Components.interfaces.nsIFile);
+  submittedDir.append("Crash Reports");
+  submittedDir.append("submitted");
+  return submittedDir;
+}
+
+this.CrashReports.pendingDir = CrashReports_pendingDir();
+this.CrashReports.reportsDir = CrashReports_reportsDir();
+this.CrashReports.submittedDir = CrashReports_submittedDir();
--- a/toolkit/crashreporter/content/crashes.js
+++ b/toolkit/crashreporter/content/crashes.js
@@ -1,18 +1,18 @@
 /* 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/. */
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 
-var reportsDir, submittedDir, pendingDir;
 var reportURL;
 
+Components.utils.import("resource://gre/modules/CrashReports.jsm");
 Components.utils.import("resource://gre/modules/CrashSubmit.jsm");
 Components.utils.import("resource://gre/modules/Services.jsm");
 
 const buildID = Services.appinfo.appBuildID;
 
 function submitSuccess(dumpid, ret) {
   let link = document.getElementById(dumpid);
   if (link) {
@@ -50,36 +50,16 @@ function submitPendingReport(event) {
                                submitError: submitError,
                                noThrottle: true })) {
     link.className = "submitting";
   }
   event.preventDefault();
   return false;
 }
 
-function findInsertionPoint(reports, date) {
-  if (reports.length == 0)
-    return 0;
-
-  var min = 0;
-  var max = reports.length - 1;
-  while (min < max) {
-    var mid = parseInt((min + max) / 2);
-    if (reports[mid].date < date)
-      max = mid - 1;
-    else if (reports[mid].date > date)
-      min = mid + 1;
-    else
-      return mid;
-  }
-  if (reports[min].date <= date)
-    return min;
-  return min+1;
-}
-
 function populateReportList() {
   var prefService = Cc["@mozilla.org/preferences-service;1"].
                     getService(Ci.nsIPrefBranch);
 
   try {
     reportURL = prefService.getCharPref("breakpad.reportURL");
     // Ignore any non http/https urls
     if (!/^https?:/i.test(reportURL))
@@ -87,67 +67,17 @@ function populateReportList() {
   }
   catch (e) { }
   if (!reportURL) {
     document.getElementById("clear-reports").style.display = "none";
     document.getElementById("reportList").style.display = "none";
     document.getElementById("noConfig").style.display = "block";
     return;
   }
-  var directoryService = Cc["@mozilla.org/file/directory_service;1"].
-                         getService(Ci.nsIProperties);
-
-  reportsDir = directoryService.get("UAppData", Ci.nsIFile);
-  reportsDir.append("Crash Reports");
-
-  submittedDir = directoryService.get("UAppData", Ci.nsIFile);
-  submittedDir.append("Crash Reports");
-  submittedDir.append("submitted");
-
-  var reports = [];
-  if (submittedDir.exists() && submittedDir.isDirectory()) {
-    var entries = submittedDir.directoryEntries;
-    while (entries.hasMoreElements()) {
-      var file = entries.getNext().QueryInterface(Ci.nsIFile);
-      var leaf = file.leafName;
-      if (leaf.substr(0, 3) == "bp-" &&
-          leaf.substr(-4) == ".txt") {
-        var entry = {
-          id: leaf.slice(0, -4),
-          date: file.lastModifiedTime,
-          pending: false
-        };
-        var pos = findInsertionPoint(reports, entry.date);
-        reports.splice(pos, 0, entry);
-      }
-    }
-  }
-
-  pendingDir = directoryService.get("UAppData", Ci.nsIFile);
-  pendingDir.append("Crash Reports");
-  pendingDir.append("pending");
-
-  if (pendingDir.exists() && pendingDir.isDirectory()) {
-    var uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
-    var entries = pendingDir.directoryEntries;
-    while (entries.hasMoreElements()) {
-      var file = entries.getNext().QueryInterface(Ci.nsIFile);
-      var leaf = file.leafName;
-      var id = leaf.slice(0, -4);
-      if (leaf.substr(-4) == ".dmp" && uuidRegex.test(id)) {
-        var entry = {
-          id: id,
-          date: file.lastModifiedTime,
-          pending: true
-        };
-        var pos = findInsertionPoint(reports, entry.date);
-        reports.splice(pos, 0, entry);
-      }
-    }
-  }
+  let reports = CrashReports.getReports();
 
   if (reports.length == 0) {
     document.getElementById("clear-reports").style.display = "none";
     document.getElementById("reportList").style.display = "none";
     document.getElementById("noReports").style.display = "block";
     return;
   }
 
@@ -203,37 +133,37 @@ function clearReports() {
   var bundle = bundles.createBundle("chrome://global/locale/crashes.properties");
   var prompts = Cc["@mozilla.org/embedcomp/prompt-service;1"].
                 getService(Ci.nsIPromptService);
   if (!prompts.confirm(window,
                        bundle.GetStringFromName("deleteconfirm.title"),
                        bundle.GetStringFromName("deleteconfirm.description")))
     return;
 
-  var entries = submittedDir.directoryEntries;
+  var entries = CrashReports.submittedDir.directoryEntries;
   while (entries.hasMoreElements()) {
     var file = entries.getNext().QueryInterface(Ci.nsIFile);
     var leaf = file.leafName;
     if (leaf.substr(0, 3) == "bp-" &&
         leaf.substr(-4) == ".txt") {
       file.remove(false);
     }
   }
-  entries = reportsDir.directoryEntries;
+  entries = CrashReports.reportsDir.directoryEntries;
   var oneYearAgo = Date.now() - 31586000000;
   while (entries.hasMoreElements()) {
     var file = entries.getNext().QueryInterface(Ci.nsIFile);
     var leaf = file.leafName;
     if (leaf.substr(0, 11) == "InstallTime" &&
         file.lastModifiedTime < oneYearAgo &&
         leaf != "InstallTime" + buildID) {
       file.remove(false);
     }
   }
-  entries = pendingDir.directoryEntries;
+  entries = CrashReports.pendingDir.directoryEntries;
   while (entries.hasMoreElements()) {
     entries.getNext().QueryInterface(Ci.nsIFile).remove(false);
   }
   document.getElementById("clear-reports").style.display = "none";
   document.getElementById("reportList").style.display = "none";
   document.getElementById("noReports").style.display = "block";
 }
 
--- a/toolkit/crashreporter/moz.build
+++ b/toolkit/crashreporter/moz.build
@@ -68,16 +68,17 @@ SOURCES += [
 ]
 
 if CONFIG['OS_ARCH'] == 'Darwin':
     SOURCES += [
         'mac_utils.mm',
     ]
 
 EXTRA_JS_MODULES += [
+    'CrashReports.jsm',
     'CrashSubmit.jsm',
     'KeyValueParser.jsm',
 ]
 
 LIBXUL_LIBRARY = True
 
 MSVC_ENABLE_PGO = True
 
--- a/toolkit/locales/en-US/chrome/global/aboutSupport.dtd
+++ b/toolkit/locales/en-US/chrome/global/aboutSupport.dtd
@@ -6,16 +6,26 @@
 
 <!-- LOCALIZATION NOTE (aboutSupport.pageSubtitle): don't change the 'supportLink' id. -->
 <!ENTITY aboutSupport.pageSubtitle "
   This page contains technical information that might be useful when you're
   trying to solve a problem. If you are looking for answers to common questions
   about &brandShortName;, check out our <a id='supportLink'>support website</a>.
 ">
 
+<!ENTITY aboutSupport.crashes.title "Crash Reports">
+<!-- LOCALIZATION NOTE (aboutSupport.crashes.id):
+This is likely the same like id.heading in crashes.dtd. -->
+<!ENTITY aboutSupport.crashes.id "Report ID">
+<!-- LOCALIZATION NOTE (aboutSupport.crashes.id):
+This is likely the same like date.heading in crashes.dtd. -->
+<!ENTITY aboutSupport.crashes.sendDate "Submitted">
+<!ENTITY aboutSupport.crashes.allReports "All Crash Reports">
+<!ENTITY aboutSupport.crashes.noConfig "This application has not been configured to display crash reports.">
+
 <!ENTITY aboutSupport.extensionsTitle "Extensions">
 <!ENTITY aboutSupport.extensionName "Name">
 <!ENTITY aboutSupport.extensionEnabled "Enabled">
 <!ENTITY aboutSupport.extensionVersion "Version">
 <!ENTITY aboutSupport.extensionId "ID">
 
 <!ENTITY aboutSupport.appBasicsTitle "Application Basics">
 <!ENTITY aboutSupport.appBasicsName "Name">
@@ -30,17 +40,16 @@
 <!-- LOCALIZATION NOTE (aboutSupport.appBasicsProfileDirWinMac):
 This is the Windows- and Mac-specific variant of aboutSupport.appBasicsProfileDir.
 Windows/Mac use the term "Folder" instead of "Directory" -->
 <!ENTITY aboutSupport.appBasicsProfileDirWinMac "Profile Folder">
 
 <!ENTITY aboutSupport.appBasicsEnabledPlugins "Enabled Plugins">
 <!ENTITY aboutSupport.appBasicsBuildConfig "Build Configuration">
 <!ENTITY aboutSupport.appBasicsUserAgent "User Agent">
-<!ENTITY aboutSupport.appBasicsCrashIDs "Crash Reports">
 <!ENTITY aboutSupport.appBasicsMemoryUse "Memory Use">
 
 <!ENTITY aboutSupport.showDir.label "Open Directory">
 <!-- LOCALIZATION NOTE (aboutSupport.showMac.label): This is the Mac-specific
 variant of aboutSupport.showDir.label.  This allows us to use the preferred
 "Finder" terminology on Mac. -->
 <!ENTITY aboutSupport.showMac.label "Show in Finder">
 <!-- LOCALIZATION NOTE (aboutSupport.showWin.label): This is the Windows-specific
--- a/toolkit/locales/en-US/chrome/global/aboutSupport.properties
+++ b/toolkit/locales/en-US/chrome/global/aboutSupport.properties
@@ -1,12 +1,37 @@
 # 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/.
 
+# LOCALIZATION NOTE (downloadsTitleFiles): Semi-colon list of plural forms.
+# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
+# #1 number of days relevant with relevant crash reports
+crashesTitle=Crash Reports for the Last #1 Day;Crash Reports for the Last #1 Days
+
+# LOCALIZATION NOTE (crashesTimeMinutes): Semi-colon list of plural forms.
+# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
+# #1 number of minutes (between 1 and 59) which have passed since the crash
+crashesTimeMinutes=#1 minute ago;#1 minutes ago
+
+# LOCALIZATION NOTE (crashesTimeHours): Semi-colon list of plural forms.
+# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
+# #1 number of hours (between 1 and 23) which have passed since the crash
+crashesTimeHours=#1 hour ago;#1 hours ago
+
+# LOCALIZATION NOTE (crashesTimeDays): Semi-colon list of plural forms.
+# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
+# #1 number of days (1 or more) which have passed since the crash
+crashesTimeDays=#1 day ago;#1 days ago
+
+# LOCALIZATION NOTE (downloadsTitleFiles): Semi-colon list of plural forms.
+# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
+# #1 number of pending crash reports
+pendingReports=All Crash Reports (including #1 pending crash in the given time range);All Crash Reports (including #1 pending crashes in the given time range)
+
 # LOCALIZATION NOTE In the following strings, "Direct2D", "DirectWrite" and "ClearType" 
 # are proper nouns and should not be translated. Feel free to leave english strings if 
 # there are no good translations, these are only used in about:support
 
 # LOCALIZATION NOTE: This can be localized with a more generic term, like
 # "Graphics-accelerated Windows". It describes a number of windows, e.g.:
 # "GPU Accelerated Windows: 2/2 (Direct3D 9)"
 # "GPU Accelerated Windows: 0/2"
--- a/toolkit/modules/Troubleshoot.jsm
+++ b/toolkit/modules/Troubleshoot.jsm
@@ -3,18 +3,19 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 this.EXPORTED_SYMBOLS = [
   "Troubleshoot",
 ];
 
 const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
 
+Cu.import("resource://gre/modules/AddonManager.jsm");
+Cu.import("resource://gre/modules/CrashReports.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/AddonManager.jsm");
 
 // We use a preferences whitelist to make sure we only show preferences that
 // are useful for support and won't compromise the user's privacy.  Note that
 // entries are *prefixes*: for example, "accessibility." applies to all prefs
 // under the "accessibility.*" branch.
 const PREFS_WHITELIST = [
   "accessibility.",
   "browser.cache.",
@@ -99,16 +100,18 @@ this.Troubleshoot = {
       }
       catch (err) {
         let msg = "Troubleshoot data provider failed: " + name + "\n" + err;
         Cu.reportError(msg);
         providerDone(name, msg);
       }
     }
   },
+
+  kMaxCrashAge: 3 * 24 * 60 * 60 * 1000, // 3 days
 };
 
 // Each data provider is a name => function mapping.  When a snapshot is
 // captured, each provider's function is called, and it's the function's job to
 // generate the provider's data.  The function is passed a "done" callback, and
 // when done, it must pass its data to the callback.  The resulting snapshot
 // object will contain a name => data entry for each provider.
 let dataProviders = {
@@ -129,16 +132,28 @@ let dataProviders = {
                        getService(Ci.nsIURLFormatter);
     try {
       data.supportURL = urlFormatter.formatURLPref("app.support.baseURL");
     }
     catch (e) {}
     done(data);
   },
 
+#ifdef MOZ_CRASHREPORTER
+  crashes: function crashes(done) {
+    let reports = CrashReports.getReports();
+    let now = new Date();
+    let reportsNew = reports.filter(report => (now - report.date < Troubleshoot.kMaxCrashAge));
+    let reportsSubmitted = reportsNew.filter(report => (!report.pending));
+    let reportsPendingCount = reportsNew.length - reportsSubmitted.length;
+    let data = {submitted : reportsSubmitted, pending : reportsPendingCount};
+    done(data);
+  },
+#endif
+
   extensions: function extensions(done) {
     AddonManager.getAddonsByTypes(["extension"], function (extensions) {
       extensions.sort(function (a, b) {
         if (a.isActive != b.isActive)
           return b.isActive ? 1 : -1;
         let lc = a.name.localeCompare(b.name);
         if (lc != 0)
           return lc;
--- a/toolkit/modules/tests/browser/browser_Troubleshoot.js
+++ b/toolkit/modules/tests/browser/browser_Troubleshoot.js
@@ -95,16 +95,47 @@ const SNAPSHOT_SCHEMA = {
         vendor: {
           type: "string",
         },
         supportURL: {
           type: "string",
         },
       },
     },
+    crashes: {
+      required: true,
+      type: "object",
+      properties: {
+        pending: {
+          required: true,
+          type: "number",
+        },
+        submitted: {
+          required: true,
+          type: "array",
+          items: {
+            type: "object",
+            properties: {
+              id: {
+                required: true,
+                type: "string",
+              },
+              date: {
+                required: true,
+                type: "number",
+              },
+              pending: {
+                required: true,
+                type: "boolean",
+              },
+            },
+          },
+        },
+      },
+    },
     extensions: {
       required: true,
       type: "array",
       items: {
         type: "object",
         properties: {
           name: {
             required: true,