Repository: GoogleChrome/lighthouse
Pull request #5977 (2018-09-06 10:59:31)

core(report): fix cursor behaviour
<!-- Thank you for submitting a pull request! -->

**Summary**
<!-- What kind of change does this PR introduce? -->
Right now there is some inconsistency in our report on (mainly `<detail>`) elements who can be collapsed.  Mouse `cursor: pointer` should be set in CSS to indicate whether element is clickable

<!-- Is this a bugfix, feature, refactoring, build related change, etc? -->
A minor bugfix. More testing is highly appreciated as I cannot be sure to cover every relevant element
<!-- Describe the need for this change -->
Improve general user experience

<!-- Link any documentation or information that would help understand this change -->

**Related Issues/PRs**
<!-- Provide any additional information we might need to understand the pull request -->
https://github.com/GoogleChrome/lighthouse/issues/5958



Patch:
diff --git a/changelog.md b/changelog.md
index e8c15d914..d5418b7de 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,3 +1,159 @@
+ <a name="3.1.1"></a>
+# 3.1.1 (2018-09-07)
+[Full Changelog](https://github.com/googlechrome/lighthouse/compare/v3.1.0...v3.1.1)
+
+* update report score legend to match updated color buckets ([#5985](https://github.com/googlechrome/lighthouse/pull/5985))
+
+<a name="3.1.0"></a>
+# 3.1.0 (2018-09-06)
+[Full Changelog](https://github.com/googlechrome/lighthouse/compare/v3.0.3...v3.1.0)
+
+## New Contributors!
+Huge thanks to @midzer and @justinribeiro for being so darn helpful. Great stuff. :D
+
+And thanks to @mettamatt for some helpful documentation. Who doesn't love better docs?!
+
+## Notable changes
+
+* red/orange/green color buckets for scores are tougher now. ([#5831](https://github.com/googlechrome/lighthouse/pull/5831))
+
+| | Previously | Now |
+|-|-|-|
+| Red (bad) | 0-44 | 0-49 |
+| Orange (average)| 45-74 | 50-89 |
+| Green (good)| 75-100 | 90-100 |
+
+Scoring itself hasn't changed; only what color is used to decorate a score.
+
+* trace-processor: fix bug where top-level events weren't being found in the trace ([#5841](https://github.com/googlechrome/lighthouse/pull/5841))
+
+## Core
+
+* normalize URL before checking equality ([#5960](https://github.com/googlechrome/lighthouse/pull/5960))
+* expose LighthouseRunWarnings on audit context ([#5684](https://github.com/googlechrome/lighthouse/pull/5684))
+* adjust audit titles for consistency ([#5717](https://github.com/googlechrome/lighthouse/pull/5717))
+* export any page-functions as string ([#5902](https://github.com/googlechrome/lighthouse/pull/5902))
+* all ByteEfficiency audits require a trace ([#5840](https://github.com/googlechrome/lighthouse/pull/5840))
+* enhanced noopener output ([#5857](https://github.com/googlechrome/lighthouse/pull/5857))
+* errors: class rename and re-source page load errors ([#5972](https://github.com/googlechrome/lighthouse/pull/5972))
+* fix defaultConfig UIStrings and exit code for test ([#5884](https://github.com/googlechrome/lighthouse/pull/5884))
+* remove last debugString references ([#5856](https://github.com/googlechrome/lighthouse/pull/5856))
+* redirects: update redirects to use OpportunityDetails ([#5791](https://github.com/googlechrome/lighthouse/pull/5791))
+* warn when extensions affected perf ([#5666](https://github.com/googlechrome/lighthouse/pull/5666))
+* driver: handle when page redefines global.URL ([#5748](https://github.com/googlechrome/lighthouse/pull/5748))
+* gather-runner: call clearDataForOrigin on teardown ([#5933](https://github.com/googlechrome/lighthouse/pull/5933))
+* lhr: expose environment info ([#5871](https://github.com/googlechrome/lighthouse/pull/5871))
+* lr: add LR presets for desktop and mobile ([#5886](https://github.com/googlechrome/lighthouse/pull/5886))
+* lr: drop maxWaitForLoad 45 -> 35 ([#5938](https://github.com/googlechrome/lighthouse/pull/5938))
+* network: handle LR transferSize ([#5895](https://github.com/googlechrome/lighthouse/pull/5895))
+* network-analyzer: infer RTT from receiveHeadersEnd ([#5694](https://github.com/googlechrome/lighthouse/pull/5694))
+* optimized-images: support non-standard mime types ([#5688](https://github.com/googlechrome/lighthouse/pull/5688))
+* preload: remove CRC dependency ([#5901](https://github.com/googlechrome/lighthouse/pull/5901))
+* screenshot-thumbnails: improve caching ([#5976](https://github.com/googlechrome/lighthouse/pull/5976))
+* ttfb: reuse requestMainResource ([#5657](https://github.com/googlechrome/lighthouse/pull/5657))
+* trace-of-tab: only use navstart of chrome/http documents ([#5917](https://github.com/googlechrome/lighthouse/pull/5917))
+* tracing-processor: throw on no top level events ([#5878](https://github.com/googlechrome/lighthouse/pull/5878))
+* uses-long-cache-ttl: handle multiple cache-control headers ([#5745](https://github.com/googlechrome/lighthouse/pull/5745))
+
+## Extension
+
+* access core through module instead of Runner ([#5855](https://github.com/googlechrome/lighthouse/pull/5855))
+* tweak runLighthouseAsInCLI jsdoc ([#5812](https://github.com/googlechrome/lighthouse/pull/5812))
+* refactor option/flag passing (breaking change) ([#5769](https://github.com/googlechrome/lighthouse/pull/5769))
+* remove X-UA-Compatible meta ([#5739](https://github.com/googlechrome/lighthouse/pull/5739))
+* popup: keep feedback class ([#5872](https://github.com/googlechrome/lighthouse/pull/5872))
+
+## i18n
+
+* add i18n 'type' for prlint ([#5880](https://github.com/googlechrome/lighthouse/pull/5880))
+* minor message and description changes based on translator feedback ([#5829](https://github.com/googlechrome/lighthouse/pull/5829))
+* reframe the ICU message descriptions with placeholders ([#5737](https://github.com/googlechrome/lighthouse/pull/5737))
+* export rendererFormattedStrings ([#5713](https://github.com/googlechrome/lighthouse/pull/5713))
+* extract performance category strings to UIStrings ([#5716](https://github.com/googlechrome/lighthouse/pull/5716))
+* always use english for status logs ([#5727](https://github.com/googlechrome/lighthouse/pull/5727))
+* add 'ln' locale which maps to 'fr' ([#5879](https://github.com/googlechrome/lighthouse/pull/5879))
+* support descriptions ([#5718](https://github.com/googlechrome/lighthouse/pull/5718))
+* add [ICU Syntax] label to relevant message descriptions ([#5736](https://github.com/googlechrome/lighthouse/pull/5736))
+* import psuedo-locale json from TC ([#5726](https://github.com/googlechrome/lighthouse/pull/5726))
+* incorporate 52 languages into messages pipeline ([#5781](https://github.com/googlechrome/lighthouse/pull/5781))
+* add descriptions to UI strings ([#5743](https://github.com/googlechrome/lighthouse/pull/5743))
+* add strings of the opportunity group split (first paint, overall) ([#5744](https://github.com/googlechrome/lighthouse/pull/5744))
+* add locale fallback when language not supported ([#5746](https://github.com/googlechrome/lighthouse/pull/5746))
+* localize strings at end of run ([#5655](https://github.com/googlechrome/lighthouse/pull/5655))
+* always use formatted strings for extension popup ([#5761](https://github.com/googlechrome/lighthouse/pull/5761))
+* add strings for user timing entries table ([#5806](https://github.com/googlechrome/lighthouse/pull/5806))
+* sort the ICU messages when saved ([#5771](https://github.com/googlechrome/lighthouse/pull/5771))
+* roll latest TC messages ([#5802](https://github.com/googlechrome/lighthouse/pull/5802))
+* add localized messages for 52 locales ([#5780](https://github.com/googlechrome/lighthouse/pull/5780))
+* add assertion script ([#5686](https://github.com/googlechrome/lighthouse/pull/5686))
+* report: use LHR locale for toLocaleString ([#5734](https://github.com/googlechrome/lighthouse/pull/5734))
+* report: localize CRC renderer strings ([#5730](https://github.com/googlechrome/lighthouse/pull/5730))
+
+## Types
+
+* don't use index signature in LHErrors ([#5896](https://github.com/googlechrome/lighthouse/pull/5896))
+* type check config files ([#5858](https://github.com/googlechrome/lighthouse/pull/5858))
+* don't use index signature in simulator.js ([#5897](https://github.com/googlechrome/lighthouse/pull/5897))
+* update debugger protocol type checking ([#5836](https://github.com/googlechrome/lighthouse/pull/5836))
+* remove more reliance on implicit index signatures ([#5874](https://github.com/googlechrome/lighthouse/pull/5874))
+* update types in emulation.js ([#5830](https://github.com/googlechrome/lighthouse/pull/5830))
+* tighten traceOfTab timing types ([#5887](https://github.com/googlechrome/lighthouse/pull/5887))
+* update to ts 3.1 to support new protocol defs ([#5942](https://github.com/googlechrome/lighthouse/pull/5942))
+* update to latest tsc ([#5581](https://github.com/googlechrome/lighthouse/pull/5581))
+* NetworkRequest.RESOURCE_TYPES type fix ([#5851](https://github.com/googlechrome/lighthouse/pull/5851))
+* add explicit index signature in mainthread-work-breakdown ([#5859](https://github.com/googlechrome/lighthouse/pull/5859))
+* cli: make LH.Flags type correct and consistent ([#5849](https://github.com/googlechrome/lighthouse/pull/5849))
+
+## Report
+
+* link to our own "unused css" reference doc ([#5698](https://github.com/googlechrome/lighthouse/pull/5698))
+* expose prepareLabData directly as a fn ([#5818](https://github.com/googlechrome/lighthouse/pull/5818))
+* responsive styles ([#5706](https://github.com/googlechrome/lighthouse/pull/5706))
+* add PSI.prepareLabData() ([#5804](https://github.com/googlechrome/lighthouse/pull/5804))
+* adopt 80-char line-length license ([#5757](https://github.com/googlechrome/lighthouse/pull/5757))
+* add method to get the final screenshot ([#5673](https://github.com/googlechrome/lighthouse/pull/5673))
+* metrics: display metrics in seconds ([#5914](https://github.com/googlechrome/lighthouse/pull/5914))
+* psi: add Util.prepareReportResult method ([#5766](https://github.com/googlechrome/lighthouse/pull/5766))
+* psi: reset template styles on every call of prepareLabData ([#5877](https://github.com/googlechrome/lighthouse/pull/5877))
+* psi: add lab data summary sentence ([#5961](https://github.com/googlechrome/lighthouse/pull/5961))
+
+## New Audits
+
+* a11y: add manual audit for interactive element affordance ([#5764](https://github.com/googlechrome/lighthouse/pull/5764))
+
+## Tests
+
+* quiet down collect-scripts during CI ([#5801](https://github.com/googlechrome/lighthouse/pull/5801))
+* better display value tests with i18n ([#5720](https://github.com/googlechrome/lighthouse/pull/5720))
+* dbw: remove createShadowRoot call ([#5906](https://github.com/googlechrome/lighthouse/pull/5906))
+* jest: disable coverage collection by default ([#5772](https://github.com/googlechrome/lighthouse/pull/5772))
+* smokehouse: update expectations for HTML Imports deprecation ([#5943](https://github.com/googlechrome/lighthouse/pull/5943))
+* smokehouse: update passive listener expectations ([#5899](https://github.com/googlechrome/lighthouse/pull/5899))
+
+## Deps
+
+* snyk: update snyk snapshot ([#5774](https://github.com/googlechrome/lighthouse/pull/5774)), ([#5773](https://github.com/googlechrome/lighthouse/pull/5773)), ([#5762](https://github.com/googlechrome/lighthouse/pull/5762)), ([#5691](https://github.com/googlechrome/lighthouse/pull/5691))
+* depend on speedline-core, rather than the cli ([#5800](https://github.com/googlechrome/lighthouse/pull/5800))
+
+
+## Docs
+
+* headless: Require Node 8 and apt install chromium ([#5974](https://github.com/googlechrome/lighthouse/pull/5974))
+* headless: example script to post report as gist ([#5975](https://github.com/googlechrome/lighthouse/pull/5975))\
+* integrations: Add SpeedCurve to "Lighthouse Integrations" ([#5732](https://github.com/googlechrome/lighthouse/pull/5732))
+* link FCP audit description to lighthouse docs ([#5850](https://github.com/googlechrome/lighthouse/pull/5850))
+* puppeteer: remove appMode mention ([#5923](https://github.com/googlechrome/lighthouse/pull/5923))
+* readme: mention chrome-flags troubleshooting in getting started ([#5916](https://github.com/googlechrome/lighthouse/pull/5916))
+* report: add a renderer readme ([#5725](https://github.com/googlechrome/lighthouse/pull/5725))
+* scoring: http2 redirect PWA score issue ([#5929](https://github.com/googlechrome/lighthouse/pull/5929))
+
+## Misc
+
+* cli: enableErrorReporting must be undefined by default ([#5854](https://github.com/googlechrome/lighthouse/pull/5854))
+* asset-saver: tweak output format of logAssets ([#5696](https://github.com/googlechrome/lighthouse/pull/5696))
+* logger: add time/timeEnd methods ([#5905](https://github.com/googlechrome/lighthouse/pull/5905))
+* fix grocers apostrophe typo (API's v APIs) ([#5948](https://github.com/googlechrome/lighthouse/pull/5948))
+
  <a name="3.0.3"></a>
 # 3.0.3 (2018-07-17)
 [Full Changelog](https://github.com/googlechrome/lighthouse/compare/v3.0.2...v3.0.3)
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/docs/scoring.md b/docs/scoring.md
index f75b16534..93128a277 100644
--- a/docs/scoring.md
+++ b/docs/scoring.md
@@ -44,9 +44,9 @@ These weights are heuristics, and the Lighthouse team is working on formalizing
 Once Lighthouse is done gathering the raw performance metrics for your website (metrics reported in miliseconds), it converts them into a score by mapping the raw performance number to a number between 0-100 by looking where your raw performance metric falls on the Lighthouse scoring distribution. The Lighthouse scoring distribution is a log normal distribution that is derived from the performance metrics of real website performance data (see sample distribution [here](https://www.desmos.com/calculator/zrjq6v1ihi)).
 
 Once we finish computing the percentile equivalent of your raw performance score, we take the weighted average of all the performance metrics (per the weighting above). Finally, we apply a coloring to the score (green, orange, and red) depending on what "bucket" your score falls in. This maps to:
-- Red (poor score): 0-44.
-- Orange (average): 45-74
-- Green (good): 75-100.
+- Red (poor score): 0-49
+- Orange (average): 50-89
+- Green (good): 90-100
 
 ### What can developers do to improve their performance score?
 *Note: we've built [a little calculator](https://docs.google.com/spreadsheets/d/1Cxzhy5ecqJCucdf1M0iOzM8mIxNc7mmx107o5nj38Eo/edit#gid=283330180) that can help you understand what thresholds you should be aiming for achieving a certain Lighthouse performance score. *
diff --git a/lighthouse-cli/bin.js b/lighthouse-cli/bin.js
index 4baa04b15..d02844d89 100644
--- a/lighthouse-cli/bin.js
+++ b/lighthouse-cli/bin.js
@@ -107,7 +107,6 @@ function run() {
       }
     })
     .then(_ => {
-      // @ts-ignore TODO(bckenny): Sentry type checking
       Sentry.init({
         url,
         flags: cliFlags,
diff --git a/lighthouse-core/audits/byte-efficiency/offscreen-images.js b/lighthouse-core/audits/byte-efficiency/offscreen-images.js
index dff8f991e..66c6f5f92 100644
--- a/lighthouse-core/audits/byte-efficiency/offscreen-images.js
+++ b/lighthouse-core/audits/byte-efficiency/offscreen-images.js
@@ -181,7 +181,6 @@ class OffscreenImages extends ByteEfficiencyAudit {
 
       if (processed instanceof Error) {
         warnings.push(processed.message);
-        // @ts-ignore TODO(bckenny): Sentry type checking
         Sentry.captureException(processed, {tags: {audit: this.meta.id}, level: 'warning'});
         return results;
       }
diff --git a/lighthouse-core/audits/byte-efficiency/uses-responsive-images.js b/lighthouse-core/audits/byte-efficiency/uses-responsive-images.js
index ffca2e41a..e2817755b 100644
--- a/lighthouse-core/audits/byte-efficiency/uses-responsive-images.js
+++ b/lighthouse-core/audits/byte-efficiency/uses-responsive-images.js
@@ -105,7 +105,6 @@ class UsesResponsiveImages extends ByteEfficiencyAudit {
 
       if (processed instanceof Error) {
         warnings.push(processed.message);
-        // @ts-ignore TODO(bckenny): Sentry type checking
         Sentry.captureException(processed, {tags: {audit: this.meta.id}, level: 'warning'});
         return;
       }
diff --git a/lighthouse-core/audits/dobetterweb/no-vulnerable-libraries.js b/lighthouse-core/audits/dobetterweb/no-vulnerable-libraries.js
index b4a3cb54f..013a503be 100644
--- a/lighthouse-core/audits/dobetterweb/no-vulnerable-libraries.js
+++ b/lighthouse-core/audits/dobetterweb/no-vulnerable-libraries.js
@@ -15,7 +15,6 @@
 const Audit = require('../audit');
 const Sentry = require('../../lib/sentry');
 const semver = require('semver');
-// @ts-ignore - json require
 const snykDatabase = require('../../../third-party/snyk/snapshot.json');
 
 const SEMVER_REGEX = /^(\d+\.\d+\.\d+)[^-0-9]+/;
@@ -93,7 +92,6 @@ class NoVulnerableLibrariesAudit extends Audit {
     } catch (err) {
       err.pkgName = lib.npmPkgName;
       // Report the failure and skip this library if the version was ill-specified
-      // @ts-ignore TODO(bckenny): Sentry type checking
       Sentry.captureException(err, {level: 'warning'});
       return [];
     }
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/audits/uses-rel-preconnect.js b/lighthouse-core/audits/uses-rel-preconnect.js
index 9e84d00c4..e25ddd618 100644
--- a/lighthouse-core/audits/uses-rel-preconnect.js
+++ b/lighthouse-core/audits/uses-rel-preconnect.js
@@ -172,7 +172,9 @@ class UsesRelPreconnectAudit extends Audit {
     return {
       score: UnusedBytes.scoreForWastedMs(maxWasted),
       rawValue: maxWasted,
-      displayValue: str_(i18n.UIStrings.displayValueMsSavings, {wastedMs: maxWasted}),
+      displayValue: maxWasted ?
+        str_(i18n.UIStrings.displayValueMsSavings, {wastedMs: maxWasted}) :
+        '',
       extendedInfo: {
         value: results,
       },
diff --git a/lighthouse-core/audits/uses-rel-preload.js b/lighthouse-core/audits/uses-rel-preload.js
index 2ec6cb374..b11064563 100644
--- a/lighthouse-core/audits/uses-rel-preload.js
+++ b/lighthouse-core/audits/uses-rel-preload.js
@@ -190,7 +190,9 @@ class UsesRelPreloadAudit extends Audit {
     return {
       score: UnusedBytes.scoreForWastedMs(wastedMs),
       rawValue: wastedMs,
-      displayValue: str_(i18n.UIStrings.displayValueMsSavings, {wastedMs}),
+      displayValue: wastedMs ?
+        str_(i18n.UIStrings.displayValueMsSavings, {wastedMs}) :
+        '',
       extendedInfo: {
         value: results,
       },
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..1201251c0 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?):/;
@@ -116,7 +116,6 @@ class TraceOfTab extends ComputedArtifact {
     // However, if no candidates were found (a bogus trace, likely), we fail.
     if (!firstMeaningfulPaint) {
       // Track this with Sentry since it's likely a bug we should investigate.
-      // @ts-ignore TODO(bckenny): Sentry type checking
       Sentry.captureMessage('No firstMeaningfulPaint found, using fallback', {level: 'warning'});
 
       const fmpCand = 'firstMeaningfulPaintCandidate';
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/gather/gatherers/dobetterweb/optimized-images.js b/lighthouse-core/gather/gatherers/dobetterweb/optimized-images.js
index 80ee69be4..67484a2fd 100644
--- a/lighthouse-core/gather/gatherers/dobetterweb/optimized-images.js
+++ b/lighthouse-core/gather/gatherers/dobetterweb/optimized-images.js
@@ -194,7 +194,6 @@ class OptimizedImages extends Gatherer {
       } catch (err) {
         // Track this with Sentry since these errors aren't surfaced anywhere else, but we don't
         // want to tank the entire run due to a single image.
-        // @ts-ignore TODO(bckenny): Sentry type checking
         Sentry.captureException(err, {
           tags: {gatherer: 'OptimizedImages'},
           extra: {imageUrl: URL.elideDataURI(record.url)},
diff --git a/lighthouse-core/gather/gatherers/dobetterweb/response-compression.js b/lighthouse-core/gather/gatherers/dobetterweb/response-compression.js
index 254993401..235845617 100644
--- a/lighthouse-core/gather/gatherers/dobetterweb/response-compression.js
+++ b/lighthouse-core/gather/gatherers/dobetterweb/response-compression.js
@@ -103,7 +103,6 @@ class ResponseCompression extends Gatherer {
           });
         });
       }).catch(err => {
-        // @ts-ignore TODO(bckenny): Sentry type checking
         Sentry.captureException(err, {
           tags: {gatherer: 'ResponseCompression'},
           extra: {url: URL.elideDataURI(record.url)},
diff --git a/lighthouse-core/gather/gatherers/fonts.js b/lighthouse-core/gather/gatherers/fonts.js
index f580e476c..dc0ae4cb7 100644
--- a/lighthouse-core/gather/gatherers/fonts.js
+++ b/lighthouse-core/gather/gatherers/fonts.js
@@ -211,18 +211,18 @@ class Fonts extends Gatherer {
     return fontData.then(([loadedFonts, fontsAndErrors]) => {
       // Filter out errors from retrieving data on font faces.
       const fontFaces = /** @type {Array<LH.Artifacts.Font>} */ (fontsAndErrors.filter(
-        fontOrError => {
-          // Abuse the type system a bit since `err` property isn't common between types.
-          const dataError = /** @type {FontGatherError} */ (fontOrError);
-          if (dataError.err) {
-            const err = new Error(dataError.err.message);
-            err.stack = dataError.err.stack || err.stack;
-            // @ts-ignore TODO(bckenny): Sentry type checking
-            Sentry.captureException(err, {tags: {gatherer: 'Fonts'}, level: 'warning'});
-            return false;
-          }
-          return true;
-        }));
+        fontOrError => !('err' in fontOrError)));
+
+      const firstFontError = fontsAndErrors.find(fontOrError => 'err' in fontOrError);
+      if (firstFontError) {
+        // Abuse the type system a bit since `err` property isn't common between types.
+        const dataError = /** @type {FontGatherError} */ (firstFontError);
+        if (dataError.err) {
+          const err = new Error(dataError.err.message);
+          err.stack = dataError.err.stack || err.stack;
+          Sentry.captureException(err, {tags: {gatherer: 'Fonts'}, level: 'warning'});
+        }
+      }
 
       return loadedFonts.map(loadedFont => {
         const fontFaceItem = this._findSameFontFamily(loadedFont, fontFaces);
diff --git a/lighthouse-core/lib/arbitrary-equality-map.js b/lighthouse-core/lib/arbitrary-equality-map.js
index 7332944ea..89901f07f 100644
--- a/lighthouse-core/lib/arbitrary-equality-map.js
+++ b/lighthouse-core/lib/arbitrary-equality-map.js
@@ -5,7 +5,6 @@
  */
 'use strict';
 
-// @ts-ignore
 const isEqual = require('lodash.isequal');
 
 /**
diff --git a/lighthouse-core/lib/i18n.js b/lighthouse-core/lib/i18n.js
index f1b2ac8bf..5b33d5365 100644
--- a/lighthouse-core/lib/i18n.js
+++ b/lighthouse-core/lib/i18n.js
@@ -25,9 +25,7 @@ const MESSAGE_INSTANCE_ID_REGEX = /(.* \| .*) # (\d+)$/;
     // In browser environments where we don't need the polyfill, this won't exist
     if (!IntlPolyfill.NumberFormat) return;
 
-    // @ts-ignore
     Intl.NumberFormat = IntlPolyfill.NumberFormat;
-    // @ts-ignore
     Intl.DateTimeFormat = IntlPolyfill.DateTimeFormat;
   } catch (_) {
     log.warn('i18n', 'Failed to install `intl` polyfill');
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/index.js b/lighthouse-core/lib/locales/index.js
index 26c4cc7ce..7ea013682 100644
--- a/lighthouse-core/lib/locales/index.js
+++ b/lighthouse-core/lib/locales/index.js
@@ -18,7 +18,7 @@
 /** @type {Record<LH.Locale, LocaleMessages>} */
 const locales = {
   'en-US': require('./en-US.json'), // The 'source' strings, with descriptions
-  // @ts-ignore - tsc bug, something about en/en-US pointing to same file
+  // @ts-ignore - tsc bug, something about en/en-US pointing to same file. https://github.com/Microsoft/TypeScript/issues/26307
   'en': require('./en-US.json'), // According to CLDR/ICU, 'en' == 'en-US' dates/numbers (Why?!)
 
   // TODO: en-GB has just ~10 messages that are different from en-US. We should only ship those.
diff --git a/lighthouse-core/lib/sentry.js b/lighthouse-core/lib/sentry.js
index 558687c39..c2d61b7ba 100644
--- a/lighthouse-core/lib/sentry.js
+++ b/lighthouse-core/lib/sentry.js
@@ -3,72 +3,85 @@
  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
  * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
  */
-// @ts-nocheck
 'use strict';
 
 const log = require('lighthouse-logger');
 
-// eslint-disable-next-line max-len
+/** @typedef {import('raven').CaptureOptions} CaptureOptions */
+/** @typedef {import('raven').ConstructorOptions} ConstructorOptions */
+
 const SENTRY_URL = 'https://a6bb0da87ee048cc9ae2a345fc09ab2e:63a7029f46f74265981b7e005e0f69f8@sentry.io/174697';
 
-const noop = () => Promise.resolve();
-const sentryApi = {
-  captureMessage: noop,
-  captureException: noop,
-  captureBreadcrumb: noop,
-  mergeContext: noop,
-  getContext: noop,
-};
+// Per-run chance of capturing errors (if enabled).
+const SAMPLE_RATE = 0.01;
 
+/** @type {Array<{pattern: RegExp, rate: number}>} */
 const SAMPLED_ERRORS = [
-  // Error code based sampling
-  {pattern: /DOCUMENT_REQUEST$/, rate: 0.1},
-  {pattern: /(IDLE_PERIOD|FMP_TOO_LATE)/, rate: 0.1},
-  {pattern: /^NO_.*/, rate: 0.1},
-  // Message based sampling
-  {pattern: /Could not load stylesheet/, rate: 0.01},
-  {pattern: /Failed to decode/, rate: 0.1},
-  {pattern: /All image optimizations failed/, rate: 0.1},
-  {pattern: /No.*resource with given/, rate: 0.01},
-  {pattern: /No.*node with given id/, rate: 0.01},
+  // Error code based sampling. Delete if still unused after 2019-01-01.
+  // e.g.: {pattern: /No.*node with given id/, rate: 0.01},
 ];
 
+const noop = () => {};
+
 /**
- * We'll create a delegate for sentry so that environments without error reporting enabled will use
+ * A delegate for sentry so that environments without error reporting enabled will use
  * noop functions and environments with error reporting will call the actual Sentry methods.
  */
-const sentryDelegate = Object.assign({}, sentryApi);
-sentryDelegate.init = function init(opts) {
+const sentryDelegate = {
+  init,
+  /** @type {(message: string, options?: CaptureOptions) => void} */
+  captureMessage: noop,
+  /** @type {(breadcrumb: any) => void} */
+  captureBreadcrumb: noop,
+  /** @type {() => any} */
+  getContext: noop,
+  /** @type {(error: Error, options?: CaptureOptions) => Promise<void>} */
+  captureException: async () => {},
+};
+
+/**
+ * When called, replaces noops with actual Sentry implementation.
+ * @param {{url: string, flags: LH.CliFlags, environmentData: ConstructorOptions}} opts
+ */
+function init(opts) {
+  // If error reporting is disabled, leave the functions as a noop
   if (!opts.flags.enableErrorReporting) {
-    // If error reporting is disabled, leave the functions as a noop
     return;
   }
 
-  const environmentData = opts.environmentData || {};
-  const sentryConfig = Object.assign({}, environmentData, {captureUnhandledRejections: true});
+  // If not selected for samping, leave the functions as a noop.
+  if (SAMPLE_RATE <= Math.random()) {
+    return;
+  }
 
   try {
     const Sentry = require('raven');
+    const sentryConfig = Object.assign({}, opts.environmentData,
+      {captureUnhandledRejections: true});
     Sentry.config(SENTRY_URL, sentryConfig).install();
-    Object.keys(sentryApi).forEach(functionName => {
-      // Have each delegate function call the corresponding sentry function by default
-      sentryDelegate[functionName] = (...args) => Sentry[functionName](...args);
-    });
 
-    // Special case captureException to return a Promise so we don't process.exit too early
-    sentryDelegate.captureException = (err, opts) => {
-      opts = opts || {};
+    // Have each delegate function call the corresponding sentry function by default
+    sentryDelegate.captureMessage = (...args) => Sentry.captureMessage(...args);
+    sentryDelegate.captureBreadcrumb = (...args) => Sentry.captureBreadcrumb(...args);
+    sentryDelegate.getContext = () => Sentry.getContext();
 
-      const empty = Promise.resolve();
+    // Special case captureException to return a Promise so we don't process.exit too early
+    sentryDelegate.captureException = async (err, opts = {}) => {
       // Ignore if there wasn't an error
-      if (!err) return empty;
+      if (!err) return;
+
       // Ignore expected errors
-      if (err.expected) return empty;
-      // Sample known errors that occur at a high frequency
+      // @ts-ignore Non-standard property added to flag error as not needing capturing.
+      if (err.expected) return;
+
+      // Sample known errors that occur at a high frequency.
       const sampledErrorMatch = SAMPLED_ERRORS.find(sample => sample.pattern.test(err.message));
-      if (sampledErrorMatch && sampledErrorMatch.rate <= Math.random()) return empty;
+      if (sampledErrorMatch && sampledErrorMatch.rate <= Math.random()) return;
+
       // Protocol errors all share same stack trace, so add more to fingerprint
+      // @ts-ignore - properties added to protocol method LHErrors.
       if (err.protocolMethod) {
+        // @ts-ignore - properties added to protocol method LHErrors.
         opts.fingerprint = ['{{ default }}', err.protocolMethod, err.protocolError];
       }
 
@@ -76,21 +89,19 @@ sentryDelegate.init = function init(opts) {
         Sentry.captureException(err, opts, () => resolve());
       });
     };
+
+    const context = Object.assign({
+      url: opts.url,
+      deviceEmulation: !opts.flags.disableDeviceEmulation,
+      throttlingMethod: opts.flags.throttlingMethod,
+    }, opts.flags.throttling);
+    Sentry.mergeContext({extra: Object.assign({}, opts.environmentData.extra, context)});
   } catch (e) {
     log.warn(
       'sentry',
       'Could not load raven library, errors will not be reported.'
     );
   }
-
-  const context = Object.assign({
-    url: opts.url,
-    deviceEmulation: !opts.flags.disableDeviceEmulation,
-    throttlingMethod: opts.flags.throttlingMethod,
-  }, opts.flags.throttling);
-
-  sentryDelegate.mergeContext({extra: Object.assign({}, environmentData.extra, context)});
-  return context;
-};
+}
 
 module.exports = sentryDelegate;
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/report-ui-features.js b/lighthouse-core/report/html/renderer/report-ui-features.js
index 4ed83000d..3150a9bc2 100644
--- a/lighthouse-core/report/html/renderer/report-ui-features.js
+++ b/lighthouse-core/report/html/renderer/report-ui-features.js
@@ -130,10 +130,8 @@ class ReportUIFeatures {
 
   _setupHeaderAnimation() {
     const scoresWrapper = this._dom.find('.lh-scores-wrapper', this._document);
-    this.headerOverlap = /** @type {number} */
-      // @ts-ignore - TODO: move off CSSOM to support other browsers
-      (scoresWrapper.computedStyleMap().get('margin-top').value);
-
+    const computedMarginTop = window.getComputedStyle(scoresWrapper).marginTop;
+    this.headerOverlap = parseFloat(computedMarginTop || '0');
     this.headerSticky = this._dom.find('.lh-header-sticky', this._document);
     this.headerBackground = this._dom.find('.lh-header-bg', this._document);
     this.lighthouseIcon = this._dom.find('.lh-lighthouse', this._document);
@@ -141,9 +139,8 @@ class ReportUIFeatures {
     this.productInfo = this._dom.find('.lh-product-info', this._document);
     this.toolbar = this._dom.find('.lh-toolbar', this._document);
     this.toolbarMetadata = this._dom.find('.lh-toolbar__metadata', this._document);
-
-    // @ts-ignore - TODO: move off CSSOM to support other browsers
-    this.headerHeight = this.headerBackground.computedStyleMap().get('height').value;
+    const computedHeight = window.getComputedStyle(this.headerBackground).height;
+    this.headerHeight = parseFloat(computedHeight || '0');
 
     this._document.addEventListener('scroll', this.onScroll, {passive: true});
 
diff --git a/lighthouse-core/report/html/renderer/util.js b/lighthouse-core/report/html/renderer/util.js
index 856f4cc20..a11a6f6dc 100644
--- a/lighthouse-core/report/html/renderer/util.js
+++ b/lighthouse-core/report/html/renderer/util.js
@@ -142,10 +142,7 @@ class Util {
       case 'numeric':
       case 'binary':
       default:
-        // Numeric audits that are within PASS_THRESHOLD will still show up with failing.
-        // For opportunities, we want to have them show up with other failing for contrast.
-        // For diagnostics, we sort by score so they'll be lowest priority.
-        return Number(audit.score) === 1;
+        return Number(audit.score) >= RATINGS.PASS.minScore;
     }
   }
 
diff --git a/lighthouse-core/report/html/report-styles.css b/lighthouse-core/report/html/report-styles.css
index 0df1d2fca..7bba98095 100644
--- a/lighthouse-core/report/html/report-styles.css
+++ b/lighthouse-core/report/html/report-styles.css
@@ -150,12 +150,15 @@
   display: none !important;
 }
 
+.lh-root details > summary {
+  cursor: pointer;
+}
+
 .lh-audit-group a,
 .lh-category-header__description a {
   color: #0c50c7;
 }
 
-
 .lh-audit__description {
   --inner-audit-left-padding: calc(var(--text-indent) + var(--lh-audit-index-width) + 2 * var(--audit-item-gap));
   --inner-audit-right-padding: calc(var(--text-indent) + 2px);
@@ -299,7 +302,6 @@
 .lh-audit__header {
   display: flex;
   padding: var(--lh-audit-vpadding) var(--text-indent);
-  cursor: pointer;
 }
 
 .lh-audit--load-opportunity .lh-audit__header {
diff --git a/lighthouse-core/report/html/templates.html b/lighthouse-core/report/html/templates.html
index fb92d7ce9..29e93cb11 100644
--- a/lighthouse-core/report/html/templates.html
+++ b/lighthouse-core/report/html/templates.html
@@ -27,9 +27,9 @@ limitations under the License.
 <template id="tmpl-lh-scorescale">
   <div class="lh-scorescale">
     <span class="lh-scorescale-label"></span>
-    <span class="lh-scorescale-range lh-scorescale-range--fail">0-44</span>
-    <span class="lh-scorescale-range lh-scorescale-range--average">45-74</span>
-    <span class="lh-scorescale-range lh-scorescale-range--pass">75-100</span>
+    <span class="lh-scorescale-range lh-scorescale-range--fail">0-49</span>
+    <span class="lh-scorescale-range lh-scorescale-range--average">50-89</span>
+    <span class="lh-scorescale-range lh-scorescale-range--pass">90-100</span>
   </div>
 </template>
 
diff --git a/lighthouse-core/runner.js b/lighthouse-core/runner.js
index 3ccb31173..d1d7aa2bd 100644
--- a/lighthouse-core/runner.js
+++ b/lighthouse-core/runner.js
@@ -40,11 +40,9 @@ class Runner {
       const lighthouseRunWarnings = [];
 
       const sentryContext = Sentry.getContext();
-      // @ts-ignore TODO(bckenny): Sentry type checking
       Sentry.captureBreadcrumb({
         message: 'Run started',
         category: 'lifecycle',
-        // @ts-ignore TODO(bckenny): Sentry type checking
         data: sentryContext && sentryContext.extra,
       });
 
@@ -150,7 +148,6 @@ class Runner {
       const report = generateReport(lhr, settings.output);
       return {lhr, artifacts, report};
     } catch (err) {
-      // @ts-ignore TODO(bckenny): Sentry type checking
       await Sentry.captureException(err, {level: 'fatal'});
       throw err;
     }
@@ -249,7 +246,6 @@ class Runner {
           // @ts-ignore An artifact *could* be an Error, but caught here, so ignore elsewhere.
           const artifactError = artifacts[artifactName];
 
-          // @ts-ignore TODO(bckenny): Sentry type checking
           Sentry.captureException(artifactError, {
             tags: {gatherer: artifactName},
             level: 'error',
@@ -283,7 +279,6 @@ class Runner {
         throw err;
       }
 
-      // @ts-ignore TODO(bckenny): Sentry type checking
       Sentry.captureException(err, {tags: {audit: audit.meta.id}, level: 'error'});
       // Non-fatal error become error audit result.
       const errorMessage = err.friendlyMessage ?
diff --git a/lighthouse-extension/app/manifest.json b/lighthouse-extension/app/manifest.json
index 1b70c5101..bb29d4151 100644
--- a/lighthouse-extension/app/manifest.json
+++ b/lighthouse-extension/app/manifest.json
@@ -1,6 +1,6 @@
 {
   "name": "__MSG_appName__",
-  "version": "3.0.3",
+  "version": "3.1.1",
   "minimum_chrome_version": "66",
   "manifest_version": 2,
   "description": "__MSG_appDescription__",
diff --git a/lighthouse-extension/app/popup.html b/lighthouse-extension/app/popup.html
index faa9d2669..c363f4204 100644
--- a/lighthouse-extension/app/popup.html
+++ b/lighthouse-extension/app/popup.html
@@ -27,8 +27,7 @@ Unless required by applicable law or agreed to in writing, software distributed
             id="lantern-checkbox"
             type="checkbox"
             checked />
-          Simulate throttling for performance audits (faster)
-          <span class="header-titles__new-adornment">NEW!</span>
+          Simulated throttling (faster)
         </label>
       </div>
     </div>
diff --git a/lighthouse-extension/app/styles/lighthouse.css b/lighthouse-extension/app/styles/lighthouse.css
index bf3cef568..01d62639a 100644
--- a/lighthouse-extension/app/styles/lighthouse.css
+++ b/lighthouse-extension/app/styles/lighthouse.css
@@ -87,11 +87,6 @@ html, body {
   left: 0;
 }
 
-.header-titles__new-adornment {
-  color: coral;
-  font-weight: bold;
-}
-
 .main {
   height: 70px;
   display: flex;
diff --git a/lighthouse-viewer/app/src/lighthouse-report-viewer.js b/lighthouse-viewer/app/src/lighthouse-report-viewer.js
index 551142bfd..28847ac6a 100644
--- a/lighthouse-viewer/app/src/lighthouse-report-viewer.js
+++ b/lighthouse-viewer/app/src/lighthouse-report-viewer.js
@@ -56,7 +56,6 @@ class LighthouseReportViewer {
    * @private
    */
   _addEventListeners() {
-    // @ts-ignore - tsc thinks document can't listen for `paste`
     document.addEventListener('paste', this._onPaste);
 
     const gistUrlInput = find('.js-gist-url', document);
diff --git a/package.json b/package.json
index 407b11241..ce81cc26c 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "lighthouse",
-  "version": "3.0.3",
+  "version": "3.1.1",
   "description": "Lighthouse",
   "main": "./lighthouse-core/index.js",
   "bin": {
@@ -77,6 +77,7 @@
     "@types/lodash.isequal": "^4.5.2",
     "@types/node": "*",
     "@types/opn": "^3.0.28",
+    "@types/raven": "^2.5.1",
     "@types/semver": "^5.5.0",
     "@types/update-notifier": "^1.0.2",
     "@types/ws": "^4.0.1",
diff --git a/readme.md b/readme.md
index 2c3b1c66e..951f7a7b4 100644
--- a/readme.md
+++ b/readme.md
@@ -280,6 +280,7 @@ Other awesome open source projects that use Lighthouse.
 * **[lighthouse-hue](https://github.com/ebidel/lighthouse-hue)** - set the color of Philips Hue lights based on a Lighthouse score
    * **[lighthouse-magic-light](https://github.com/manekinekko/lighthouse-magic-light)** set the color of the MagicLight Bluetooth Smart Light Bulb based on Lighthouse score
 * **[lighthouse-batch](https://www.npmjs.com/package/lighthouse-batch)** - run Lighthouse over a number of sites and generate a summary of their metrics/scores.
+* **[lighthouse-badges](https://github.com/emazzotta/lighthouse-badges)** - Generate gh-badges (shields.io) based on Lighthouse performance.
 * **[lighthouse-cron](https://github.com/thearegee/lighthouse-cron)** - Cron multiple batch Lighthouse audits and emit results for sending to remote server.
 * **[lightcrawler](https://github.com/github/lightcrawler)** - Crawl a website and run each page found through Lighthouse.
 * **[lighthouse-lambda](https://github.com/joytocode/lighthouse-lambda)** - Run Lighthouse on AWS Lambda with prebuilt stable desktop Headless Chrome.
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/yarn.lock b/yarn.lock
index ae72338ab..0937311b3 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -99,6 +99,13 @@
   dependencies:
     "@types/node" "*"
 
+"@types/raven@^2.5.1":
+  version "2.5.1"
+  resolved "https://registry.yarnpkg.com/@types/raven/-/raven-2.5.1.tgz#62ef0a59e29691945e1f295b62ed199619cbd9b6"
+  dependencies:
+    "@types/events" "*"
+    "@types/node" "*"
+
 "@types/rimraf@^0.0.28":
   version "0.0.28"
   resolved "https://registry.yarnpkg.com/@types/rimraf/-/rimraf-0.0.28.tgz#5562519bc7963caca8abf7f128cae3b594d41d06"


Test patch:
diff --git a/lighthouse-core/test/report/html/renderer/performance-category-renderer-test.js b/lighthouse-core/test/report/html/renderer/performance-category-renderer-test.js
index 8650cbf77..15781d576 100644
--- a/lighthouse-core/test/report/html/renderer/performance-category-renderer-test.js
+++ b/lighthouse-core/test/report/html/renderer/performance-category-renderer-test.js
@@ -140,7 +140,7 @@ describe('PerfCategoryRenderer', () => {
     const diagnosticSection = categoryDOM.querySelectorAll('.lh-category > .lh-audit-group')[2];
 
     const diagnosticAudits = category.auditRefs.filter(audit => audit.group === 'diagnostics' &&
-        audit.result.score !== 1 && audit.result.scoreDisplayMode !== 'not-applicable');
+        !Util.showAsPassed(audit.result));
     const diagnosticElements = diagnosticSection.querySelectorAll('.lh-audit');
     assert.equal(diagnosticElements.length, diagnosticAudits.length);
   });
@@ -150,8 +150,7 @@ describe('PerfCategoryRenderer', () => {
     const passedSection = categoryDOM.querySelector('.lh-category > .lh-passed-audits');
 
     const passedAudits = category.auditRefs.filter(audit =>
-        audit.group && audit.group !== 'metrics' &&
-        (audit.result.score === 1 || audit.result.scoreDisplayMode === 'not-applicable'));
+        audit.group && audit.group !== 'metrics' && Util.showAsPassed(audit.result));
     const passedElements = passedSection.querySelectorAll('.lh-audit');
     assert.equal(passedElements.length, passedAudits.length);
   });
diff --git a/lighthouse-core/test/results/sample_v2.json b/lighthouse-core/test/results/sample_v2.json
index 0daaeb6e1..0a93fa9f7 100644
--- a/lighthouse-core/test/results/sample_v2.json
+++ b/lighthouse-core/test/results/sample_v2.json
@@ -5,7 +5,7 @@
     "hostUserAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3358.0 Safari/537.36",
     "benchmarkIndex": 1000
   },
-  "lighthouseVersion": "3.0.3",
+  "lighthouseVersion": "3.1.1",
   "fetchTime": "2018-03-13T00:55:45.840Z",
   "requestedUrl": "http://localhost:10200/dobetterweb/dbw_tester.html",
   "finalUrl": "http://localhost:10200/dobetterweb/dbw_tester.html",
@@ -768,7 +768,7 @@
       "score": 1,
       "scoreDisplayMode": "numeric",
       "rawValue": 0,
-      "displayValue": "Potential savings of 0 ms",
+      "displayValue": "",
       "details": {
         "type": "opportunity",
         "headings": [],
@@ -783,7 +783,7 @@
       "score": 1,
       "scoreDisplayMode": "numeric",
       "rawValue": 0,
-      "displayValue": "Potential savings of 0 ms",
+      "displayValue": "",
       "details": {
         "type": "opportunity",
         "headings": [],
@@ -3595,26 +3595,6 @@
       "lighthouse-core/audits/uses-rel-preload.js | description": [
         "audits[uses-rel-preload].description"
       ],
-      "lighthouse-core/lib/i18n.js | displayValueMsSavings": [
-        {
-          "values": {
-            "wastedMs": 0
-          },
-          "path": "audits[uses-rel-preload].displayValue"
-        },
-        {
-          "values": {
-            "wastedMs": 0
-          },
-          "path": "audits[uses-rel-preconnect].displayValue"
-        },
-        {
-          "values": {
-            "wastedMs": 1129
-          },
-          "path": "audits[render-blocking-resources].displayValue"
-        }
-      ],
       "lighthouse-core/audits/uses-rel-preconnect.js | title": [
         "audits[uses-rel-preconnect].title"
       ],
@@ -3678,6 +3658,14 @@
       "lighthouse-core/audits/byte-efficiency/render-blocking-resources.js | description": [
         "audits[render-blocking-resources].description"
       ],
+      "lighthouse-core/lib/i18n.js | displayValueMsSavings": [
+        {
+          "values": {
+            "wastedMs": 1129
+          },
+          "path": "audits[render-blocking-resources].displayValue"
+        }
+      ],
       "lighthouse-core/lib/i18n.js | columnWastedMs": [
         "audits[render-blocking-resources].details.headings[2].label"
       ],
@@ -3797,4 +3785,4 @@
       ]
     }
   }
-}
+}
\ No newline at end of file