Repository: GoogleChrome/lighthouse
Pull request #5964 (2018-09-05 07:06:16)

deps(snyk): update snyk snapshot
## Why this PR?
a weekly update of the vulnerabilities snapshot for lighthouse

googlebot: 
Thanks for your pull request. It looks like this may be your first contribution to a Google open source project (if not, look below for help). Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

:memo: **Please visit <https://cla.developers.google.com/> to sign.**

Once you've signed (or fixed any issues), please reply here (e.g. `I signed it!`) and we'll verify it.

----

#### What to do if you already signed the CLA

##### Individual signers

*   It's possible we don't have your GitHub username or you're using a different email address on your commit. Check [your existing CLA data](https://cla.developers.google.com/clas) and verify that your [email is set on your git commits](https://help.github.com/articles/setting-your-email-in-git/).

##### Corporate signers

*   Your company has a Point of Contact who decides which employees are authorized to participate. Ask your POC to be added to the group of authorized contributors. If you don't know who your Point of Contact is, direct the Google project maintainer to [go/cla#troubleshoot](http://go/cla#troubleshoot) ([Public version](https://opensource.google.com/docs/cla/#troubleshoot)).
*   The email used to register you as an authorized contributor must be the email used for the Git commit. Check [your existing CLA data](https://cla.developers.google.com/clas) and verify that your [email is set on your git commits](https://help.github.com/articles/setting-your-email-in-git/).
*   The email used to register you as an authorized contributor must also be [attached to your GitHub account](https://github.com/settings/emails).
		

<!-- need_sender_cla -->

Patch:
diff --git a/docs/headless-chrome.md b/docs/headless-chrome.md
index f04f176e7..1c6ed4f75 100644
--- a/docs/headless-chrome.md
+++ b/docs/headless-chrome.md
@@ -5,12 +5,12 @@
 Setup:
 
 ```sh
-# get node 6
-curl -sL https://deb.nodesource.com/setup_6.x | sudo -E bash - &&\
+# Lighthouse requires Node 8 LTS (8.9) or later.
+curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash - &&\
 sudo apt-get install -y nodejs npm
 
 # get chromium (stable)
-apt-get install chromium-browser
+apt-get install chromium
 
 # install lighthouse
 npm i -g lighthouse
@@ -55,6 +55,30 @@ xvfb-run --server-args='-screen 0, 1024x768x16' \
 lighthouse --port=9222 https://github.com
 ```
 
+## Posting Lighthouse reports to GitHub Gists
+
+Be sure to replace `${GITHUB_OWNER}` and `${GITHUB_TOKEN}` with your own credentials. The code below is tested on Ubuntu. 
+
+```sh
+apt-get install -y nodejs npm chromium jq
+npm install -g lighthouse
+
+# Run lighthouse as JSON, pipe it to jq to wrangle and send it to GitHub Gist via curl 
+# so Lighthouse Viewer can grab it. 
+lighthouse "http://localhost" --chrome-flags="--no-sandbox --headless" \
+  --output json \
+| jq -r "{ description: \"YOUR TITLE HERE\", public: \"false\", files: {\"$(date "+%Y%m%d").lighthouse.report.json\": {content: (. | tostring) }}}" \
+| curl -sS -X POST -H 'Content-Type: application/json' \
+    -u ${GITHUB_OWNER}:${GITHUB_TOKEN} \
+    -d @- https://api.github.com/gists > results.gist
+
+# Let's be nice and add the Lighthouse Viewer link in the Gist description.
+GID=$(cat results.gist | jq -r '.id') && \
+curl -sS -X POST -H 'Content-Type: application/json' \
+  -u ${GITHUB_OWNER}:${GITHUB_TOKEN} \
+  -d "{ \"description\": \"YOUR TITLE HERE - Lighthouse: https://googlechrome.github.io/lighthouse/viewer/?gist=${GID}\" }" "https://api.github.com/gists/${GID}" > updated.gist
+```
+
 ## Node module
 
 Install:
diff --git a/lighthouse-core/audits/final-screenshot.js b/lighthouse-core/audits/final-screenshot.js
index c59b7ee56..11f852ad8 100644
--- a/lighthouse-core/audits/final-screenshot.js
+++ b/lighthouse-core/audits/final-screenshot.js
@@ -6,7 +6,7 @@
 'use strict';
 
 const Audit = require('./audit');
-const LHError = require('../lib/errors');
+const LHError = require('../lib/lh-error');
 
 class FinalScreenshot extends Audit {
   /**
diff --git a/lighthouse-core/audits/screenshot-thumbnails.js b/lighthouse-core/audits/screenshot-thumbnails.js
index 149c38130..6facb795a 100644
--- a/lighthouse-core/audits/screenshot-thumbnails.js
+++ b/lighthouse-core/audits/screenshot-thumbnails.js
@@ -6,7 +6,7 @@
 'use strict';
 
 const Audit = require('./audit');
-const LHError = require('../lib/errors');
+const LHError = require('../lib/lh-error');
 const jpeg = require('jpeg-js');
 
 const NUMBER_OF_THUMBNAILS = 10;
@@ -114,14 +114,15 @@ class ScreenshotThumbnails extends Audit {
           }
         });
       }
-
-      const imageData = frameForTimestamp.getParsedImage();
-      const thumbnailImageData = ScreenshotThumbnails.scaleImageToThumbnail(imageData);
-      const base64Data =
-        cachedThumbnails.get(frameForTimestamp) ||
-        jpeg.encode(thumbnailImageData, 90).data.toString('base64');
-
-      cachedThumbnails.set(frameForTimestamp, base64Data);
+      let base64Data;
+      if (cachedThumbnails.has(frameForTimestamp)) {
+        base64Data = cachedThumbnails.get(frameForTimestamp);
+      } else {
+        const imageData = frameForTimestamp.getParsedImage();
+        const thumbnailImageData = ScreenshotThumbnails.scaleImageToThumbnail(imageData);
+        base64Data = jpeg.encode(thumbnailImageData, 90).data.toString('base64');
+        cachedThumbnails.set(frameForTimestamp, base64Data);
+      }
       thumbnails.push({
         timing: Math.round(targetTimestamp - speedline.beginning),
         timestamp: targetTimestamp * 1000,
diff --git a/lighthouse-core/gather/computed/metrics/estimated-input-latency.js b/lighthouse-core/gather/computed/metrics/estimated-input-latency.js
index 268edba40..46394d742 100644
--- a/lighthouse-core/gather/computed/metrics/estimated-input-latency.js
+++ b/lighthouse-core/gather/computed/metrics/estimated-input-latency.js
@@ -6,7 +6,7 @@
 'use strict';
 
 const MetricArtifact = require('./metric');
-const LHError = require('../../../lib/errors');
+const LHError = require('../../../lib/lh-error');
 const TracingProcessor = require('../../../lib/traces/tracing-processor');
 
 const ROLLING_WINDOW_SIZE = 5000;
diff --git a/lighthouse-core/gather/computed/metrics/first-cpu-idle.js b/lighthouse-core/gather/computed/metrics/first-cpu-idle.js
index 2ce2beac6..db7ac6d02 100644
--- a/lighthouse-core/gather/computed/metrics/first-cpu-idle.js
+++ b/lighthouse-core/gather/computed/metrics/first-cpu-idle.js
@@ -6,7 +6,7 @@
 'use strict';
 const MetricArtifact = require('./metric');
 const TracingProcessor = require('../../../lib/traces/tracing-processor');
-const LHError = require('../../../lib/errors');
+const LHError = require('../../../lib/lh-error');
 
 const LONG_TASK_THRESHOLD = 50;
 
diff --git a/lighthouse-core/gather/computed/metrics/first-meaningful-paint.js b/lighthouse-core/gather/computed/metrics/first-meaningful-paint.js
index ca9bdc44d..8a6086ad0 100644
--- a/lighthouse-core/gather/computed/metrics/first-meaningful-paint.js
+++ b/lighthouse-core/gather/computed/metrics/first-meaningful-paint.js
@@ -6,7 +6,7 @@
 'use strict';
 
 const MetricArtifact = require('./metric');
-const LHError = require('../../../lib/errors');
+const LHError = require('../../../lib/lh-error');
 
 class FirstMeaningfulPaint extends MetricArtifact {
   get name() {
diff --git a/lighthouse-core/gather/computed/metrics/interactive.js b/lighthouse-core/gather/computed/metrics/interactive.js
index 53ec137f1..b07544172 100644
--- a/lighthouse-core/gather/computed/metrics/interactive.js
+++ b/lighthouse-core/gather/computed/metrics/interactive.js
@@ -9,7 +9,7 @@ const MetricArtifact = require('./metric');
 
 const NetworkRecorder = require('../../../lib/network-recorder');
 const TracingProcessor = require('../../../lib/traces/tracing-processor');
-const LHError = require('../../../lib/errors');
+const LHError = require('../../../lib/lh-error');
 
 const REQUIRED_QUIET_WINDOW = 5000;
 const ALLOWED_CONCURRENT_REQUESTS = 2;
diff --git a/lighthouse-core/gather/computed/metrics/lantern-first-meaningful-paint.js b/lighthouse-core/gather/computed/metrics/lantern-first-meaningful-paint.js
index 726b592fc..710df6400 100644
--- a/lighthouse-core/gather/computed/metrics/lantern-first-meaningful-paint.js
+++ b/lighthouse-core/gather/computed/metrics/lantern-first-meaningful-paint.js
@@ -7,7 +7,7 @@
 
 const MetricArtifact = require('./lantern-metric');
 const BaseNode = require('../../../lib/dependency-graph/base-node');
-const LHError = require('../../../lib/errors');
+const LHError = require('../../../lib/lh-error');
 
 /** @typedef {BaseNode.Node} Node */
 
diff --git a/lighthouse-core/gather/computed/speedline.js b/lighthouse-core/gather/computed/speedline.js
index 899f85c20..70c85e9b4 100644
--- a/lighthouse-core/gather/computed/speedline.js
+++ b/lighthouse-core/gather/computed/speedline.js
@@ -7,7 +7,7 @@
 
 const ComputedArtifact = require('./computed-artifact');
 const speedline = require('speedline-core');
-const LHError = require('../../lib/errors');
+const LHError = require('../../lib/lh-error');
 
 class Speedline extends ComputedArtifact {
   get name() {
diff --git a/lighthouse-core/gather/computed/trace-of-tab.js b/lighthouse-core/gather/computed/trace-of-tab.js
index ecc1afc91..23686c233 100644
--- a/lighthouse-core/gather/computed/trace-of-tab.js
+++ b/lighthouse-core/gather/computed/trace-of-tab.js
@@ -19,7 +19,7 @@
 const ComputedArtifact = require('./computed-artifact');
 const log = require('lighthouse-logger');
 const TracingProcessor = require('../../lib/traces/tracing-processor');
-const LHError = require('../../lib/errors');
+const LHError = require('../../lib/lh-error');
 const Sentry = require('../../lib/sentry');
 
 const ACCEPTABLE_NAVIGATION_URL_REGEX = /^(chrome|https?):/;
diff --git a/lighthouse-core/gather/connections/connection.js b/lighthouse-core/gather/connections/connection.js
index 17e868db8..df2040077 100644
--- a/lighthouse-core/gather/connections/connection.js
+++ b/lighthouse-core/gather/connections/connection.js
@@ -7,7 +7,7 @@
 
 const EventEmitter = require('events').EventEmitter;
 const log = require('lighthouse-logger');
-const LHError = require('../../lib/errors');
+const LHError = require('../../lib/lh-error');
 
 // TODO(bckenny): CommandCallback properties should be tied by command type after
 // https://github.com/Microsoft/TypeScript/pull/22348. See driver.js TODO.
diff --git a/lighthouse-core/gather/driver.js b/lighthouse-core/gather/driver.js
index a48327b02..6065a6e14 100644
--- a/lighthouse-core/gather/driver.js
+++ b/lighthouse-core/gather/driver.js
@@ -8,7 +8,7 @@
 const NetworkRecorder = require('../lib/network-recorder');
 const emulation = require('../lib/emulation');
 const Element = require('../lib/element');
-const LHError = require('../lib/errors');
+const LHError = require('../lib/lh-error');
 const NetworkRequest = require('../lib/network-request');
 const EventEmitter = require('events').EventEmitter;
 const URL = require('../lib/url-shim');
diff --git a/lighthouse-core/gather/gather-runner.js b/lighthouse-core/gather/gather-runner.js
index e4d77795f..86ff24d03 100644
--- a/lighthouse-core/gather/gather-runner.js
+++ b/lighthouse-core/gather/gather-runner.js
@@ -6,7 +6,7 @@
 'use strict';
 
 const log = require('lighthouse-logger');
-const LHError = require('../lib/errors');
+const LHError = require('../lib/lh-error');
 const URL = require('../lib/url-shim');
 const NetworkRecorder = require('../lib/network-recorder.js');
 const constants = require('../config/constants');
@@ -356,7 +356,7 @@ class GatherRunner {
 
       // Fail the run if more than 50% of all artifacts failed due to page load failure.
       if (pageLoadFailures.length > Object.keys(gathererArtifacts).length * 0.5) {
-        throw pageLoadFailures[0];
+        throw LHError.fromLighthouseError(pageLoadFailures[0]);
       }
     }
 
diff --git a/lighthouse-core/lib/errors.js b/lighthouse-core/lib/lh-error.js
similarity index 92%
rename from lighthouse-core/lib/errors.js
rename to lighthouse-core/lib/lh-error.js
index 303a5c49f..9409f0db7 100644
--- a/lighthouse-core/lib/errors.js
+++ b/lighthouse-core/lib/lh-error.js
@@ -31,12 +31,23 @@ class LighthouseError extends Error {
 
   /**
    * @param {{code?: string}} err
+   * @return {err is LighthouseError}
    */
   static isPageLoadError(err) {
     return err.code === ERRORS.NO_DOCUMENT_REQUEST.code ||
       err.code === ERRORS.FAILED_DOCUMENT_REQUEST.code;
   }
 
+  /**
+   * @param {LighthouseError} err
+   * @return {LighthouseError}
+   */
+  static fromLighthouseError(err) {
+    const {code, friendlyMessage: message, ...rest} = err;
+    // Note: {...rest} convinces tsc 3.1 that it's assignable to a Record.
+    return new LighthouseError({code, message}, {...rest});
+  }
+
   /**
    * @param {string} method
    * @param {{message: string, data?: string|undefined}} protocolError
diff --git a/lighthouse-core/lib/locales/en-US.json b/lighthouse-core/lib/locales/en-US.json
index c28ad3bf1..0ce2f3a6c 100644
--- a/lighthouse-core/lib/locales/en-US.json
+++ b/lighthouse-core/lib/locales/en-US.json
@@ -431,6 +431,14 @@
     "message": "Report error: no audit information",
     "description": "An error string displayed next to a particular audit when it has errored, but not provided any specific error message."
   },
+  "lighthouse-core/report/html/renderer/util.js | labDataTitle": {
+    "message": "Lab Data",
+    "description": "Title of the lab data section of the Performance category. Within this section are various speed metrics which quantify the pageload performance into values presented in seconds and milliseconds. \"Lab\" is an abbreviated form of \"laboratory\", and refers to the fact that the data is from a controlled test of a website, not measurements from real users visiting that site."
+  },
+  "lighthouse-core/report/html/renderer/util.js | lsPerformanceCategoryDescription": {
+    "message": "[Lighthouse](https://developers.google.com/web/tools/lighthouse/) analysis of the current page on emulated 3G. Values are estimated and may vary.",
+    "description": "Explanation shown to users below performance results to inform them that the test was done with a 3G network connection and to warn them that the numbers they see will likely change slightly the next time they run Lighthouse. 'Lighthouse' becomes link text to additional documentation."
+  },
   "lighthouse-core/report/html/renderer/util.js | manualAuditsGroupTitle": {
     "message": "Additional items to manually check",
     "description": "Section heading shown above a list of audits that were not computed by Lighthouse. They serve as a list of suggestions for the user to go and manually check. For example, Lighthouse can't automate testing cross-browser compatibility, so that is listed within this section, so the user is reminded to test it themselves. This section is collapsed by default, as the user should be focusing on the failed audits instead. Users can click this heading to reveal the list."
diff --git a/lighthouse-core/lib/traces/tracing-processor.js b/lighthouse-core/lib/traces/tracing-processor.js
index 05ff24d57..399d7b127 100644
--- a/lighthouse-core/lib/traces/tracing-processor.js
+++ b/lighthouse-core/lib/traces/tracing-processor.js
@@ -14,7 +14,7 @@ const SCHEDULABLE_TASK_TITLE = 'TaskQueueManager::ProcessTaskFromWorkQueue';
 const SCHEDULABLE_TASK_TITLE_ALT1 = 'ThreadControllerImpl::DoWork';
 // m69+ DoWork is different and we now need RunTask, see https://bugs.chromium.org/p/chromium/issues/detail?id=871204#c11
 const SCHEDULABLE_TASK_TITLE_ALT2 = 'ThreadControllerImpl::RunTask';
-const LHError = require('../errors');
+const LHError = require('../lh-error');
 
 class TraceProcessor {
   /**
diff --git a/lighthouse-core/report/html/renderer/performance-category-renderer.js b/lighthouse-core/report/html/renderer/performance-category-renderer.js
index 379013d88..e9d54f409 100644
--- a/lighthouse-core/report/html/renderer/performance-category-renderer.js
+++ b/lighthouse-core/report/html/renderer/performance-category-renderer.js
@@ -129,7 +129,7 @@ class PerformanceCategoryRenderer extends CategoryRenderer {
 
     // Metrics
     const metricAudits = category.auditRefs.filter(audit => audit.group === 'metrics');
-    const metricAuditsEl = this.renderAuditGroup(groups['metrics'], {expandable: false});
+    const metricAuditsEl = this.renderAuditGroup(groups.metrics, {expandable: false});
 
     const keyMetrics = metricAudits.filter(a => a.weight >= 3);
     const otherMetrics = metricAudits.filter(a => a.weight < 3);
diff --git a/lighthouse-core/report/html/renderer/psi.js b/lighthouse-core/report/html/renderer/psi.js
index 93d81503f..a97bb963f 100644
--- a/lighthouse-core/report/html/renderer/psi.js
+++ b/lighthouse-core/report/html/renderer/psi.js
@@ -34,7 +34,7 @@
  * @return {{scoreGaugeEl: Element, perfCategoryEl: Element, finalScreenshotDataUri: string|null}}
  */
 function prepareLabData(LHResultJsonString, document) {
-  const lhResult = /** @type {LH.Result} */ JSON.parse(LHResultJsonString);
+  const lhResult = /** @type {LH.Result} */ (JSON.parse(LHResultJsonString));
   const dom = new DOM(document);
 
   // Assume fresh styles needed on every call, so mark all template styles as unused.
@@ -45,6 +45,11 @@ function prepareLabData(LHResultJsonString, document) {
   if (!perfCategory) throw new Error(`No performance category. Can't make lab data section`);
   if (!reportLHR.categoryGroups) throw new Error(`No category groups found.`);
 
+  // Use custom title and description.
+  reportLHR.categoryGroups.metrics.title = lhResult.i18n.rendererFormattedStrings.labDataTitle;
+  reportLHR.categoryGroups.metrics.description =
+      lhResult.i18n.rendererFormattedStrings.lsPerformanceCategoryDescription;
+
   const perfRenderer = new PerformanceCategoryRenderer(dom, new DetailsRenderer(dom));
   // PSI environment string will ensure the categoryHeader and permalink elements are excluded
   const perfCategoryEl = perfRenderer.render(perfCategory, reportLHR.categoryGroups, 'PSI');
diff --git a/lighthouse-core/report/html/renderer/util.js b/lighthouse-core/report/html/renderer/util.js
index b059a2c75..856f4cc20 100644
--- a/lighthouse-core/report/html/renderer/util.js
+++ b/lighthouse-core/report/html/renderer/util.js
@@ -481,6 +481,11 @@ Util.UIStrings = {
   crcInitialNavigation: 'Initial Navigation',
   /** Label of value shown in the summary of critical request chains. Refers to the total amount of time (milliseconds) of the longest critical path chain/sequence of network requests. Example value: 2310 ms */
   crcLongestDurationLabel: 'Maximum critical path latency:',
+
+  /** Explanation shown to users below performance results to inform them that the test was done with a 3G network connection and to warn them that the numbers they see will likely change slightly the next time they run Lighthouse. 'Lighthouse' becomes link text to additional documentation. */
+  lsPerformanceCategoryDescription: '[Lighthouse](https://developers.google.com/web/tools/lighthouse/) analysis of the current page on emulated 3G. Values are estimated and may vary.',
+  /** Title of the lab data section of the Performance category. Within this section are various speed metrics which quantify the pageload performance into values presented in seconds and milliseconds. "Lab" is an abbreviated form of "laboratory", and refers to the fact that the data is from a controlled test of a website, not measurements from real users visiting that site.  */
+  labDataTitle: 'Lab Data',
 };
 
 if (typeof module !== 'undefined' && module.exports) {
diff --git a/lighthouse-core/runner.js b/lighthouse-core/runner.js
index 37b8d7654..3ccb31173 100644
--- a/lighthouse-core/runner.js
+++ b/lighthouse-core/runner.js
@@ -138,12 +138,14 @@ class Runner {
         categories,
         categoryGroups: runOpts.config.groups || undefined,
         timing: {total: Date.now() - startTime},
+        i18n: {
+          rendererFormattedStrings: i18n.getRendererFormattedStrings(settings.locale),
+          icuMessagePaths: {},
+        },
       };
 
-      lhr.i18n = {
-        rendererFormattedStrings: i18n.getRendererFormattedStrings(settings.locale),
-        icuMessagePaths: i18n.replaceIcuMessageInstanceIds(lhr, settings.locale),
-      };
+      // Replace ICU message references with localized strings; save replaced paths in lhr.
+      lhr.i18n.icuMessagePaths = i18n.replaceIcuMessageInstanceIds(lhr, settings.locale);
 
       const report = generateReport(lhr, settings.output);
       return {lhr, artifacts, report};
diff --git a/third-party/snyk/snapshot.json b/third-party/snyk/snapshot.json
index 51ffd65f1..a9cb056ce 100644
--- a/third-party/snyk/snapshot.json
+++ b/third-party/snyk/snapshot.json
@@ -1,91 +1,92 @@
 {
   "npm":{
     "angular":[
-      {"id":"npm:angular:20130621","severity":"medium","semver":{"vulnerable":["<=1.1.5"]}},
-      {"id":"npm:angular:20130622","severity":"medium","semver":{"vulnerable":["<1.2.0 >=1.0.0"]}},
-      {"id":"npm:angular:20130625","severity":"high","semver":{"vulnerable":["<1.1.5"]}},
-      {"id":"npm:angular:20131113","severity":"high","semver":{"vulnerable":["<1.2.2"]}},
+      {"id":"npm:angular:20180202","severity":"medium","semver":{"vulnerable":["<1.6.9"]}},
+      {"id":"npm:angular:20171018","severity":"medium","semver":{"vulnerable":["<1.6.7"]}},
+      {"id":"npm:angular:20160527","severity":"medium","semver":{"vulnerable":["<1.2.30 >=1.0.0"]}},
+      {"id":"npm:angular:20160122","severity":"medium","semver":{"vulnerable":["<1.5.0-rc.2 >=1.3.0"]}},
       {"id":"npm:angular:20140608","severity":"low","semver":{"vulnerable":["<1.3.0"]}},
+      {"id":"npm:angular:20131113","severity":"high","semver":{"vulnerable":["<1.2.2"]}},
       {"id":"npm:angular:20140908","severity":"medium","semver":{"vulnerable":["<1.3.0-rc.4"]}},
-      {"id":"npm:angular:20140909","severity":"high","semver":{"vulnerable":["<1.2.24 >=1.2.19"]}},
-      {"id":"npm:angular:20141104","severity":"medium","semver":{"vulnerable":["<1.3.2"]}},
-      {"id":"npm:angular:20150310","severity":"high","semver":{"vulnerable":["<1.5.0-beta.2"]}},
-      {"id":"npm:angular:20150315","severity":"medium","semver":{"vulnerable":["<1.6.1"]}},
-      {"id":"npm:angular:20150807","severity":"high","semver":{"vulnerable":["<1.5.0-beta.0 >=1.0.0"]}},
-      {"id":"npm:angular:20150807-1","severity":"medium","semver":{"vulnerable":["<1.5.0-beta.0 >=1.3.1"]}},
+      {"id":"npm:angular:20161101","severity":"medium","semver":{"vulnerable":["<1.5.9 >=1.5.0"]}},
       {"id":"npm:angular:20150909","severity":"high","semver":{"vulnerable":["<1.5.0-beta.2"]}},
-      {"id":"npm:angular:20151130","severity":"medium","semver":{"vulnerable":["<1.4.10"]}},
       {"id":"npm:angular:20151205","severity":"medium","semver":{"vulnerable":["<1.5.0-rc.0"]}},
-      {"id":"npm:angular:20160122","severity":"medium","semver":{"vulnerable":["<1.5.0-rc.2 >=1.3.0"]}},
-      {"id":"npm:angular:20160527","severity":"medium","semver":{"vulnerable":["<1.2.30 >=1.0.0"]}},
-      {"id":"npm:angular:20161101","severity":"medium","semver":{"vulnerable":["<1.5.9 >=1.5.0"]}},
-      {"id":"npm:angular:20171018","severity":"medium","semver":{"vulnerable":["<1.6.7"]}},
-      {"id":"npm:angular:20180202","severity":"medium","semver":{"vulnerable":["<1.6.9"]}}
+      {"id":"npm:angular:20151130","severity":"medium","semver":{"vulnerable":["<1.4.10"]}},
+      {"id":"npm:angular:20130622","severity":"medium","semver":{"vulnerable":["<1.2.0 >=1.0.0"]}},
+      {"id":"npm:angular:20150807-1","severity":"medium","semver":{"vulnerable":["<1.5.0-beta.0 >=1.3.1"]}},
+      {"id":"npm:angular:20150807","severity":"high","semver":{"vulnerable":["<1.5.0-beta.0 >=1.0.0"]}},
+      {"id":"npm:angular:20150315","severity":"medium","semver":{"vulnerable":["<1.6.1"]}},
+      {"id":"npm:angular:20150310","severity":"high","semver":{"vulnerable":["<1.5.0-beta.2"]}},
+      {"id":"npm:angular:20141104","severity":"medium","semver":{"vulnerable":["<1.3.2"]}},
+      {"id":"npm:angular:20130621","severity":"medium","semver":{"vulnerable":["<=1.1.5"]}},
+      {"id":"npm:angular:20140909","severity":"high","semver":{"vulnerable":["<1.2.24 >=1.2.19"]}},
+      {"id":"npm:angular:20130625","severity":"high","semver":{"vulnerable":["<1.1.5"]}}
     ],
     "backbone":[
-      {"id":"npm:backbone:20110701","severity":"medium","semver":{"vulnerable":["<0.5.0"]}},
-      {"id":"npm:backbone:20160523","severity":"medium","semver":{"vulnerable":["<= 0.3.3"]}}
+      {"id":"npm:backbone:20160523","severity":"medium","semver":{"vulnerable":["<= 0.3.3"]}},
+      {"id":"npm:backbone:20110701","severity":"medium","semver":{"vulnerable":["<0.5.0"]}}
     ],
     "bootstrap":[
-      {"id":"npm:bootstrap:20120510","severity":"medium","semver":{"vulnerable":["<2.1.0"]}},
+      {"id":"npm:bootstrap:20180529","severity":"medium","semver":{"vulnerable":[">=4.0.0 <4.1.2"]}},
       {"id":"npm:bootstrap:20160627","severity":"medium","semver":{"vulnerable":["<3.4.0 || >=4.0.0-alpha <4.0.0-beta.2"]}},
-      {"id":"npm:bootstrap:20180529","severity":"medium","semver":{"vulnerable":[">=4.0.0 <4.1.2"]}}
+      {"id":"npm:bootstrap:20120510","severity":"medium","semver":{"vulnerable":["<2.1.0"]}}
     ],
     "dojo":[
-      {"id":"npm:dojo:20090409","severity":"medium","semver":{"vulnerable":["<1.1"]}},
-      {"id":"npm:dojo:20100614","severity":"high","semver":{"vulnerable":[">=0.4 <0.4.4 || >=1.0 <1.0.3 || >=1.1 <1.1.2 || >=1.2 <1.2.4 || >=1.3 <1.3.3 || >=1.4 <1.4.2"]}},
+      {"id":"npm:dojo:20180818","severity":"medium","semver":{"vulnerable":["<1.10.10 || >=1.11.0 <1.11.6 || >=1.12.0 <1.12.4 || >=1.13.0 <1.13.1"]}},
+      {"id":"npm:dojo:20160523","severity":"medium","semver":{"vulnerable":["<= 1.0.0"]}},
       {"id":"npm:dojo:20100614-6","severity":"medium","semver":{"vulnerable":["<1.4.2"]}},
-      {"id":"npm:dojo:20160523","severity":"medium","semver":{"vulnerable":["<= 1.0.0"]}}
+      {"id":"npm:dojo:20100614-1","severity":"high","semver":{"vulnerable":[">=0.4 <0.4.4 || >=1.0 <1.0.3 || >=1.1 <1.1.2 || >=1.2 <1.2.4 || >=1.3 <1.3.3 || >=1.4 <1.4.2"]}},
+      {"id":"npm:dojo:20090409","severity":"medium","semver":{"vulnerable":["<1.1"]}}
     ],
     "foundation-sites":[
-      {"id":"npm:foundation-sites:20120717","severity":"medium","semver":{"vulnerable":["<3.0.6 >=3.0.0"]}},
+      {"id":"npm:foundation-sites:20170802","severity":"medium","semver":{"vulnerable":["<6.0.0"]}},
       {"id":"npm:foundation-sites:20150619","severity":"medium","semver":{"vulnerable":["<5.5.3"]}},
-      {"id":"npm:foundation-sites:20170802","severity":"medium","semver":{"vulnerable":["<6.0.0"]}}
+      {"id":"npm:foundation-sites:20120717","severity":"medium","semver":{"vulnerable":["<3.0.6 >=3.0.0"]}}
     ],
     "handlebars":[
-      {"id":"npm:handlebars:20110425","severity":"medium","semver":{"vulnerable":["<=1.0.0-beta.3"]}},
-      {"id":"npm:handlebars:20151207","severity":"medium","semver":{"vulnerable":["<4.0.0"]}}
+      {"id":"npm:handlebars:20151207","severity":"medium","semver":{"vulnerable":["<4.0.0"]}},
+      {"id":"npm:handlebars:20110425","severity":"medium","semver":{"vulnerable":["<=1.0.0-beta.3"]}}
     ],
     "highcharts":[
       {"id":"npm:highcharts:20180225","severity":"low","semver":{"vulnerable":["<6.1.0"]}}
     ],
     "jquery":[
-      {"id":"npm:jquery:20110606","severity":"medium","semver":{"vulnerable":["<1.6.3"]}},
-      {"id":"npm:jquery:20120206","severity":"medium","semver":{"vulnerable":["<1.9.0 >=1.7.1"]}},
-      {"id":"npm:jquery:20140902","severity":"medium","semver":{"vulnerable":[">=1.4.2 <1.6.2"]}},
+      {"id":"npm:jquery:20160529","severity":"low","semver":{"vulnerable":["=3.0.0-rc.1"]}},
       {"id":"npm:jquery:20150627","severity":"medium","semver":{"vulnerable":["<3.0.0-beta1 >1.12.3 || <1.12.0 >=1.4.0"]}},
-      {"id":"npm:jquery:20160529","severity":"low","semver":{"vulnerable":["=3.0.0-rc.1"]}}
+      {"id":"npm:jquery:20140902","severity":"medium","semver":{"vulnerable":[">=1.4.2 <1.6.2"]}},
+      {"id":"npm:jquery:20120206","severity":"medium","semver":{"vulnerable":["<1.9.0 >=1.7.1"]}},
+      {"id":"npm:jquery:20110606","severity":"medium","semver":{"vulnerable":["<1.6.3"]}}
     ],
     "jquery-mobile":[
       {"id":"npm:jquery-mobile:20120802","severity":"medium","semver":{"vulnerable":["<1.2.0"]}}
     ],
     "jquery-ui":[
-      {"id":"npm:jquery-ui:20100903","severity":"medium","semver":{"vulnerable":["<1.10.0"]}},
       {"id":"npm:jquery-ui:20121127","severity":"medium","semver":{"vulnerable":["<1.10.0"]}},
+      {"id":"npm:jquery-ui:20100903","severity":"medium","semver":{"vulnerable":["<1.10.0"]}},
       {"id":"npm:jquery-ui:20160721","severity":"high","semver":{"vulnerable":["<=1.11.4"]}}
     ],
     "knockout":[
-      {"id":"npm:knockout:20130701","severity":"medium","semver":{"vulnerable":["<3.0.0 >=2.1.0-pre"]}},
-      {"id":"npm:knockout:20180213","severity":"medium","semver":{"vulnerable":["<3.5.0-beta"]}}
+      {"id":"npm:knockout:20180213","severity":"medium","semver":{"vulnerable":["<3.5.0-beta"]}},
+      {"id":"npm:knockout:20130701","severity":"medium","semver":{"vulnerable":["<3.0.0 >=2.1.0-pre"]}}
     ],
     "lodash":[
       {"id":"npm:lodash:20180130","severity":"low","semver":{"vulnerable":["<4.17.5"]}}
     ],
     "moment":[
-      {"id":"npm:moment:20160126","severity":"low","semver":{"vulnerable":["<=2.11.1"]}},
+      {"id":"npm:moment:20170905","severity":"low","semver":{"vulnerable":["<2.19.3"]}},
       {"id":"npm:moment:20161019","severity":"medium","semver":{"vulnerable":["<2.15.2"]}},
-      {"id":"npm:moment:20170905","severity":"low","semver":{"vulnerable":["<2.19.3"]}}
+      {"id":"npm:moment:20160126","severity":"low","semver":{"vulnerable":["<=2.11.1"]}}
     ],
     "mustache":[
-      {"id":"npm:mustache:20110814","severity":"medium","semver":{"vulnerable":["< 0.3.1"]}},
-      {"id":"npm:mustache:20151207","severity":"medium","semver":{"vulnerable":["<2.2.1"]}}
+      {"id":"npm:mustache:20151207","severity":"medium","semver":{"vulnerable":["<2.2.1"]}},
+      {"id":"npm:mustache:20110814","severity":"medium","semver":{"vulnerable":["< 0.3.1"]}}
     ],
     "preact-render-to-string":[
       {"id":"npm:preact-render-to-string:20180802","severity":"medium","semver":{"vulnerable":["<3.7.2"]}}
     ],
     "react":[
-      {"id":"npm:react:20131217","severity":"medium","semver":{"vulnerable":[">=0.5.0 <0.5.2 || >=0.4.0 <0.4.2"]}},
-      {"id":"npm:react:20150318","severity":"high","semver":{"vulnerable":["<0.14.0"]}}
+      {"id":"npm:react:20150318","severity":"high","semver":{"vulnerable":["<0.14.0"]}},
+      {"id":"npm:react:20131217","severity":"medium","semver":{"vulnerable":[">=0.5.0 <0.5.2 || >=0.4.0 <0.4.2"]}}
     ],
     "react-dom":[
       {"id":"npm:react-dom:20180802","severity":"medium","semver":{"vulnerable":["<16.0.1 || >=16.1.0 <16.1.2 || >=16.2.0 <16.2.1 || >=16.3.0 <16.3.3 || >=16.4.0 <16.4.2"]}}
@@ -94,21 +95,21 @@
       {"id":"npm:riot:20131114","severity":"medium","semver":{"vulnerable":["<0.9.6"]}}
     ],
     "socket.io":[
-      {"id":"npm:socket.io:20120323","severity":"medium","semver":{"vulnerable":["<0.9.7"]}},
-      {"id":"npm:socket.io:20120417","severity":"medium","semver":{"vulnerable":["<0.9.6"]}}
+      {"id":"npm:socket.io:20120417","severity":"medium","semver":{"vulnerable":["<0.9.6"]}},
+      {"id":"npm:socket.io:20120323","severity":"medium","semver":{"vulnerable":["<0.9.7"]}}
     ],
     "vue":[
-      {"id":"npm:vue:20170401","severity":"medium","semver":{"vulnerable":["<2.3.0-beta.1"]}},
       {"id":"npm:vue:20170829","severity":"medium","semver":{"vulnerable":["<2.4.3"]}},
-      {"id":"npm:vue:20180222","severity":"low","semver":{"vulnerable":["<=2.5.14"]}},
-      {"id":"npm:vue:20180802","severity":"medium","semver":{"vulnerable":["<2.5.17"]}}
+      {"id":"npm:vue:20170401","severity":"medium","semver":{"vulnerable":["<2.3.0-beta.1"]}},
+      {"id":"npm:vue:20180802","severity":"medium","semver":{"vulnerable":["<2.5.17"]}},
+      {"id":"npm:vue:20180222","severity":"low","semver":{"vulnerable":["<=2.5.14"]}}
     ],
     "yui":[
-      {"id":"npm:yui:20101025","severity":"medium","semver":{"vulnerable":["<2.8.2 >=2.4.0"]}},
-      {"id":"npm:yui:20120428","severity":"medium","semver":{"vulnerable":["<3.5.1 >=3.5.0-PR1"]}},
-      {"id":"npm:yui:20121030","severity":"medium","semver":{"vulnerable":["<3.0.0 >=2.4.0"]}},
+      {"id":"npm:yui:20130604","severity":"medium","semver":{"vulnerable":[">=3.0.0 <3.10.1 || =3.10.2"]}},
       {"id":"npm:yui:20130515","severity":"medium","semver":{"vulnerable":["<3.10.0 >=3.0.0"]}},
-      {"id":"npm:yui:20130604","severity":"medium","semver":{"vulnerable":[">=3.0.0 <3.10.1 || =3.10.2"]}}
+      {"id":"npm:yui:20121030","severity":"medium","semver":{"vulnerable":["<3.0.0 >=2.4.0"]}},
+      {"id":"npm:yui:20120428","severity":"medium","semver":{"vulnerable":["<3.5.1 >=3.5.0-PR1"]}},
+      {"id":"npm:yui:20101025","severity":"medium","semver":{"vulnerable":["<2.8.2 >=2.4.0"]}}
     ]
   }
 }
\ No newline at end of file
diff --git a/typings/lhr.d.ts b/typings/lhr.d.ts
index ce2c4b460..09cf635bf 100644
--- a/typings/lhr.d.ts
+++ b/typings/lhr.d.ts
@@ -57,7 +57,7 @@ declare global {
       /** Execution timings for the Lighthouse run */
       timing: {total: number, [t: string]: number};
       /** The record of all formatted string locations in the LHR and their corresponding source values. */
-      i18n?: {rendererFormattedStrings: I18NRendererStrings, icuMessagePaths: I18NMessages};
+      i18n: {rendererFormattedStrings: I18NRendererStrings, icuMessagePaths: I18NMessages};
     }
 
     // Result namespace


Test patch:
diff --git a/lighthouse-core/test/report/html/renderer/psi-test.js b/lighthouse-core/test/report/html/renderer/psi-test.js
index 5fed4e78e..6c17433fe 100644
--- a/lighthouse-core/test/report/html/renderer/psi-test.js
+++ b/lighthouse-core/test/report/html/renderer/psi-test.js
@@ -94,6 +94,21 @@ describe('DOM', () => {
           prepareLabData(lhrWithoutGroupsStr, document);
         }, /no category groups/i);
       });
+
+      it('includes custom title and description', () => {
+        const {perfCategoryEl} = prepareLabData(sampleResultsStr, document);
+        const metricsGroupEl = perfCategoryEl.querySelector('.lh-audit-group--metrics');
+
+        // Assume using default locale.
+        const titleEl = metricsGroupEl.querySelector('.lh-audit-group__header');
+        assert.equal(titleEl.textContent, Util.UIStrings.labDataTitle);
+
+        // Description supports markdown links, so take everything after the last link.
+        const descriptionEnd = /[^)]+$/.exec(Util.UIStrings.lsPerformanceCategoryDescription)[0];
+        assert.ok(descriptionEnd.length > 6); // If this gets too short, pick a different comparison :)
+        const descriptionEl = metricsGroupEl.querySelector('.lh-audit-group__description');
+        assert.ok(descriptionEl.textContent.endsWith(descriptionEnd));
+      });
     });
   });
 
diff --git a/lighthouse-core/test/results/sample_v2.json b/lighthouse-core/test/results/sample_v2.json
index 2d59b639d..0daaeb6e1 100644
--- a/lighthouse-core/test/results/sample_v2.json
+++ b/lighthouse-core/test/results/sample_v2.json
@@ -3412,6 +3412,8 @@
       "crcLongestDurationLabel": "Maximum critical path latency:",
       "errorLabel": "Error!",
       "errorMissingAuditInfo": "Report error: no audit information",
+      "labDataTitle": "Lab Data",
+      "lsPerformanceCategoryDescription": "[Lighthouse](https://developers.google.com/web/tools/lighthouse/) analysis of the current page on emulated 3G. Values are estimated and may vary.",
       "manualAuditsGroupTitle": "Additional items to manually check",
       "notApplicableAuditsGroupTitle": "Not applicable",
       "opportunityResourceColumnLabel": "Opportunity",