Repository: GoogleChrome/lighthouse
Pull request #8294 (2019-04-15 20:56:46)

extension: remove github link in favor of copy to clipboard
<!-- Thank you for submitting a pull request! -->

**Summary**
Remove the github issue creation link and move to a clipboard button instead.

![lighthouse-copy](https://user-images.githubusercontent.com/1120926/56164757-aacee480-5fd1-11e9-9a15-67614bb52926.gif)


<details>
<summary>The following text is copied into the clipboard:</summary>

**Lighthouse Version**: 4.3.0
**Lighthouse Commit**: f46bcd78779ca5ce6052d6655a15a22d5f264478
**Chrome Version**: 73.0.3683.103
**Initial URL**: https://www.paulirish.com/
**Error Message**: Attempted to add event listener after connection disposed.
**Stack Trace**:
```
Error: Attempted to add event listener after connection disposed.
    at ExtensionConnection.on (chrome-extension://kolnikflpalakcdkahhcfkiahbhfjfod/scripts/lighthouse-ext-bundle.js:20743:7)
    at new Driver (chrome-extension://kolnikflpalakcdkahhcfkiahbhfjfod/scripts/lighthouse-ext-bundle.js:21189:12)
    at Function._gatherArtifactsFromBrowser (chrome-extension://kolnikflpalakcdkahhcfkiahbhfjfod/scripts/lighthouse-ext-bundle.js:32187:37)
    at Function.run (chrome-extension://kolnikflpalakcdkahhcfkiahbhfjfod/scripts/lighthouse-ext-bundle.js:32062:24)
    at lighthouse (chrome-extension://kolnikflpalakcdkahhcfkiahbhfjfod/scripts/lighthouse-ext-bundle.js:23321:15)
    at runLighthouseInExtension (chrome-extension://kolnikflpalakcdkahhcfkiahbhfjfod/scripts/lighthouse-ext-bundle.js:14595:20)
``` 
</details>


wardpeet: It now looks like
![image](https://user-images.githubusercontent.com/1120926/56534706-81c2cc80-655a-11e9-8c68-cd16feb41a7e.png)


brendankenny: @patrickhulce if you want to look again as well
googlebot: So there's good news and bad news.

:thumbsup: The good news is that everyone that needs to sign a CLA (the pull request submitter and all commit authors) have done so.  Everything is all good there.

:confused: The bad news is that it appears that one or more commits were authored or co-authored by someone other than the pull request submitter.  We need to confirm that all authors are ok with their commits being contributed to this project.  Please have them confirm that here in the pull request.

*Note to project maintainer: This is a terminal state, meaning the `cla/google` commit status will not change from this state. It's up to you to confirm consent of all the commit author(s), set the `cla` label to `yes` (if enabled on your project), and then merge this pull request when appropriate.*

ℹ️ **Googlers: [Go here](https://goto.google.com/prinfo/https%3A%2F%2Fgithub.com%2FGoogleChrome%2Flighthouse%2Fpull%2F8294) for more info**.

<!-- need_author_consent -->
patrickhulce: Gah, get out of here googlebot we both have CLAs! 😛 

Patch:
diff --git a/.travis.yml b/.travis.yml
index a69b1aa6d..eb9afebd5 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -26,6 +26,7 @@ before_script:
   # see comment above about puppeteer
   - export CHROME_PATH="$(which google-chrome-stable)"
   - sh -e /etc/init.d/xvfb start
+  - google-chrome-stable --version
   - yarn build-all
 script:
   - yarn bundlesize
diff --git a/build/build-dt-report-resources.js b/build/build-dt-report-resources.js
index 3abfc03c3..c0526aeed 100644
--- a/build/build-dt-report-resources.js
+++ b/build/build-dt-report-resources.js
@@ -23,8 +23,21 @@ const htmlReportAssets = require('../lighthouse-core/report/html/html-report-ass
  */
 function convertToAsciiAndWriteFile(name, content) {
   assert(content);
-  // eslint-disable-next-line no-control-regex
-  const escaped = content.replace(/[^\x00-\x7F]/g, c => '\\\\u' + c.charCodeAt(0).toString(16));
+
+  let unicodeEscapePrefix = '\\\\u'; // js
+  if (name.endsWith('.html')) {
+    // Can't support unicode characters in inline stylesheets or js, would have to parse
+    // and apply context-specific escapes. Instead, since no ascii is used in html yet,
+    // punt and throw if any ascii is found.
+    // eslint-disable-next-line no-control-regex
+    assert(!content.match(/[^\x00-\x7F]/));
+  } else if (name.endsWith('.css')) {
+    unicodeEscapePrefix = '\\';
+  }
+
+  const escaped =
+    // eslint-disable-next-line no-control-regex
+    content.replace(/[^\x00-\x7F]/g, c => unicodeEscapePrefix + c.charCodeAt(0).toString(16));
   fs.writeFileSync(`${distDir}/${name}`, escaped);
 }
 
diff --git a/clients/extension/scripts/popup.js b/clients/extension/scripts/popup.js
index 9676a8f4a..bf33fe7c2 100644
--- a/clients/extension/scripts/popup.js
+++ b/clients/extension/scripts/popup.js
@@ -6,13 +6,25 @@
 'use strict';
 
 /** @typedef {typeof import('./extension-entry.js') & {console: typeof console}} BackgroundPage */
+/** @typedef {import('../../../lighthouse-core/lib/lh-error.js')} LighthouseError */
 
 /**
  * Error strings that indicate a problem in how Lighthouse was run, not in
  * Lighthouse itself, mapped to more useful strings to report to the user.
  */
 const NON_BUG_ERROR_MESSAGES = {
-  'Another debugger': 'You probably have DevTools open. Close DevTools to use Lighthouse',
+  // The user tries to review an error page or has network issues
+  'ERRORED_DOCUMENT_REQUEST': 'Unable to load the page. Please verify the url you ' +
+      'are trying to review.',
+  'FAILED_DOCUMENT_REQUEST': 'Unable to load the page. Please verify the url you ' +
+      'are trying to review.',
+  'DNS_FAILURE': 'DNS servers could not resolve the provided domain.',
+  'INSECURE_DOCUMENT_REQUEST': 'The URL you have provided does not have a valid' +
+      ' SSL certificate.',
+  'INVALID_URL': 'Lighthouse can only audit URLs that start' +
+      ' with http:// or https://.',
+
+  // chrome extension API errors
   'multiple tabs': 'You probably have multiple tabs open to the same origin. ' +
       'Close the other tabs to use Lighthouse.',
   // The extension debugger API is forbidden from attaching to the web store.
@@ -21,19 +33,10 @@ const NON_BUG_ERROR_MESSAGES = {
       'Chrome Web Store. If necessary, use the Lighthouse CLI to do so.',
   'Cannot access a chrome': 'The Lighthouse extension cannot audit ' +
       'Chrome-specific urls. If necessary, use the Lighthouse CLI to do so.',
-  // The user tries to review an error page or has network issues
-  'Unable to load the page': 'Unable to load the page. Please verify the url you ' +
-      'are trying to review.',
   'Cannot access contents of the page': 'Lighthouse can only audit URLs that start' +
       ' with http:// or https://.',
-  'INSECURE_DOCUMENT_REQUEST': 'The URL you have provided does not have valid' +
-      ' security credentials.',
-  'INVALID_URL': 'Lighthouse can only audit URLs that start' +
-      ' with http:// or https://.',
 };
 
-const MAX_ISSUE_ERROR_LENGTH = 60;
-
 const subpageVisibleClass = 'subpage--visible';
 
 /** @type {?URL} */
@@ -79,35 +82,40 @@ function find(query, context = document) {
 }
 
 /**
+ * @param {string} message
  * @param {Error} err
- * @return {HTMLAnchorElement}
+ * @return {HTMLButtonElement}
  */
-function buildReportErrorLink(err) {
+function buildErrorCopyButton(message, err) {
   const issueBody = `
 **Lighthouse Version**: ${getLighthouseVersion()}
 **Lighthouse Commit**: ${getLighthouseCommitHash()}
 **Chrome Version**: ${getChromeVersion()}
 **Initial URL**: ${siteURL}
-**Error Message**: ${err.message}
+**Error Message**: ${message}
 **Stack Trace**:
 \`\`\`
 ${err.stack}
 \`\`\`
     `;
 
-  const url = new URL('https://github.com/GoogleChrome/lighthouse/issues/new');
+  const errorButtonDefaultText = 'Copy details to clipboard 📋';
+  const errorButtonEl = document.createElement('button');
+  errorButtonEl.className = 'button button--report-error';
+  errorButtonEl.textContent = errorButtonDefaultText;
 
-  const errorTitle = err.message.substring(0, MAX_ISSUE_ERROR_LENGTH);
-  url.searchParams.append('title', `Extension Error: ${errorTitle}`);
-  url.searchParams.append('body', issueBody.trim());
+  errorButtonEl.addEventListener('click', async () => {
+    // @ts-ignore - tsc doesn't include `clipboard` on `navigator`
+    await navigator.clipboard.writeText(issueBody);
+    errorButtonEl.textContent = 'Copied to clipboard 📋';
 
-  const reportErrorEl = document.createElement('a');
-  reportErrorEl.className = 'button button--report-error';
-  reportErrorEl.href = url.href;
-  reportErrorEl.textContent = 'Report Error';
-  reportErrorEl.target = '_blank';
+    // Return button to inviting state after timeout.
+    setTimeout(() => {
+      errorButtonEl.textContent = errorButtonDefaultText;
+    }, 1000);
+  });
 
-  return reportErrorEl;
+  return errorButtonEl;
 }
 
 /**
@@ -176,13 +184,14 @@ async function onGenerateReportButtonClick(background, settings) {
     // Close popup once report is opened in a new tab
     window.close();
   } catch (err) {
-    let message = err.message;
+    let message = err.friendlyMessage || err.message;
     let includeReportLink = true;
 
     // Check for errors in how the user ran Lighthouse and replace with a more
     // helpful message (and remove 'Report Error' link).
     for (const [test, replacement] of Object.entries(NON_BUG_ERROR_MESSAGES)) {
-      if (message.includes(test)) {
+      if (err.message.includes(test) ||
+          (err.friendlyMessage && err.friendlyMessage.includes(test))) {
         message = replacement;
         includeReportLink = false;
         break;
@@ -193,7 +202,7 @@ async function onGenerateReportButtonClick(background, settings) {
 
     if (includeReportLink) {
       feedbackEl.className = 'feedback feedback-error';
-      feedbackEl.appendChild(buildReportErrorLink(err));
+      feedbackEl.appendChild(buildErrorCopyButton(message, err));
     }
 
     hideRunningSubpage();
diff --git a/clients/extension/styles/lighthouse.css b/clients/extension/styles/lighthouse.css
index 01d62639a..cb54d65ef 100644
--- a/clients/extension/styles/lighthouse.css
+++ b/clients/extension/styles/lighthouse.css
@@ -132,8 +132,8 @@ html, body {
   padding: 3px 5px;
   font-size: .8em;
   text-align: center;
-  display: inline;
-  margin-left: 5px;
+  display: block;
+  margin-top: 5px;
 }
 
 .subpage {
diff --git a/docs/recipes/custom-audit/searchable-audit.js b/docs/recipes/custom-audit/searchable-audit.js
index 86ecdd2cf..18fe22656 100644
--- a/docs/recipes/custom-audit/searchable-audit.js
+++ b/docs/recipes/custom-audit/searchable-audit.js
@@ -36,7 +36,7 @@ class LoadAudit extends Audit {
     const belowThreshold = loadMetrics.searchableTime <= MAX_SEARCHABLE_TIME;
 
     return {
-      rawValue: loadMetrics.searchableTime,
+      numericValue: loadMetrics.searchableTime,
       // Cast true/false to 1/0
       score: Number(belowThreshold),
     };
diff --git a/docs/recipes/lighthouse-plugin-example/audits/preload-as.js b/docs/recipes/lighthouse-plugin-example/audits/preload-as.js
index 48d8385ed..892aba6c3 100644
--- a/docs/recipes/lighthouse-plugin-example/audits/preload-as.js
+++ b/docs/recipes/lighthouse-plugin-example/audits/preload-as.js
@@ -37,7 +37,7 @@ class PreloadAsAudit extends Audit {
     const passed = noAsLinks.length === 0;
 
     return {
-      rawValue: passed,
+      score: passed ? 1 : 0,
       displayValue: `Found ${noAsLinks.length} preload requests with missing \`as\` attributes`,
     };
   }
diff --git a/docs/releasing.md b/docs/releasing.md
index f61541bde..a1a026f0e 100644
--- a/docs/releasing.md
+++ b/docs/releasing.md
@@ -31,73 +31,22 @@ We follow [semver](https://semver.org/) versioning semantics (`vMajor.Minor.Patc
 ## Release Process
 
 ```sh
-# use a custom lighthouse-pristine checkout to make sure your dev files aren't involved.
-
-# * Install the latest.*
-yarn
-
-# * Bump it *
-yarn version --no-git-tag-version
-# manually bump extension v in clients/extension/manifest.json
-yarn update:sample-json
-
-# * Build it. This also builds the cli, extension, and viewer. *
-yarn build-all
-
-# * Test err'thing *
-echo "Test the CLI."
-yarn start "https://example.com" --view
-yarn smoke
-
-echo "Test the extension"
-# ...
-
-echo "Test a fresh local install"
-# (starting from lighthouse-pristine root...)
-npm pack
-cd ..; rm -rf tmp; mkdir tmp; cd tmp
-npm init -y
-npm install ../lighthouse-pristine/lighthouse-*.tgz
-npm explore lighthouse -- npm run smoke
-npm explore lighthouse -- npm run chrome # try the manual launcher
-npm explore lighthouse -- npm run fast -- http://example.com
-cd ..; rm -rf ./tmp;
-
-cd ../lighthouse-pristine; command rm -f lighthouse-*.tgz
-
-echo "Test the lighthouse-viewer build"
-# Manual test for now:
-# Start a server in dist/viewer/ and open the page in a tab. You should see the viewer.
-# Drop in a results.json or paste an existing gist url (e.g. https://gist.github.com/ebidel/b9fd478b5f40bf5fab174439dc18f83a).
-# Check for errors!
-cd dist/viewer ; python -m SimpleHTTPServer
-# go to http://localhost:8000/
-
-# * Update changelog *
-git fetch --tags
-yarn changelog
-# add new contributors, e.g. from git shortlog -s -e -n v2.3.0..HEAD
-#    and https://github.com/GoogleChrome/lighthouse/graphs/contributors
-echo "Edit the changelog for readability and brevity"
-
-# * Put up the PR *
-echo "Branch and commit the version bump."
-git checkout -b bumpv240
-git commit -am "2.4.0"
-echo "Generate a PR and get it merged."
-
-echo "Once it's merged, pull master and tag the (squashed) commit"
-git tag -a v2.4.0 -m "v2.4.0"
-git push --tags
-
-
-# * Deploy-time *
-echo "Rebuild extension and viewer to get the latest, tagged master commit"
-yarn build-all;
-
-# zip the extension files
+# Run the tests
+bash ./lighthouse-core/scripts/release/release-test.sh
+# Prepare the commit, replace x.x.x with the desired version
+bash ./lighthouse-core/scripts/release/release-prepare-commit.sh x.x.x
+
+# Open the PR and await merge...
+echo "It's been merged! 🎉"
+
+# Run the tests again :)
+bash ./lighthouse-core/scripts/release/release-test.sh
+# Package everything for publishing
+bash ./lighthouse-core/scripts/release/release-prepare-package.sh
+
+# Upload the extension
 node build/build-extension.js package; cd dist/extension-package/
-echo "Go here: https://chrome.google.com/webstore/developer/edit/blipmdconlkpinefehnmjammfjpmpbjk "
+open https://chrome.google.com/webstore/developer/edit/blipmdconlkpinefehnmjammfjpmpbjk
 echo "Upload the package zip to CWS dev dashboard"
 # Be in lighthouse-extension-owners group
 # Open <https://chrome.google.com/webstore/developer/dashboard>
@@ -106,12 +55,11 @@ echo "Upload the package zip to CWS dev dashboard"
 # Select `lighthouse-4.X.X.zip`
 # _Publish_ at the bottom
 
-echo "Verify the npm package won't include unncessary files"
-npm pack --dry-run
-npx pkgfiles
-
-echo "ship it"
+# Make sure you're in the Lighthouse pristine repo we just tested.
+cd ../lighthouse-pristine
+# Publish to NPM
 npm publish
+# Publish viewer
 yarn deploy-viewer
 
 # * Tell the world!!! *
diff --git a/lighthouse-cli/bin.js b/lighthouse-cli/bin.js
index b442e8549..510a766e4 100644
--- a/lighthouse-cli/bin.js
+++ b/lighthouse-cli/bin.js
@@ -94,6 +94,12 @@ async function begin() {
     cliFlags.outputPath = 'stdout';
   }
 
+  // @ts-ignore - deprecation message for removed disableDeviceEmulation; can remove warning in v6.
+  if (cliFlags.disableDeviceEmulation) {
+    log.warn('config', 'The "--disable-device-emulation" has been removed in v5.' +
+        ' Please use "--emulated-form-factor=none" instead.');
+  }
+
   if (cliFlags.extraHeaders) {
     // TODO: LH.Flags.extraHeaders is actually a string at this point, but needs to be
     // copied over to LH.Settings.extraHeaders, which is LH.Crdp.Network.Headers. Force
diff --git a/lighthouse-core/audits/accessibility/listitem.js b/lighthouse-core/audits/accessibility/listitem.js
index 07b6422c3..2fbea6117 100644
--- a/lighthouse-core/audits/accessibility/listitem.js
+++ b/lighthouse-core/audits/accessibility/listitem.js
@@ -6,7 +6,7 @@
 'use strict';
 
 /**
- * @fileoverview Ensures every HTML document has a `lang` attribute.
+ * @fileoverview Ensures every list item is contained within a parent list
  * See base class in axe-audit.js for audit() implementation.
  */
 
diff --git a/lighthouse-core/audits/audit.js b/lighthouse-core/audits/audit.js
index 3c9230a44..5daa69c5c 100644
--- a/lighthouse-core/audits/audit.js
+++ b/lighthouse-core/audits/audit.js
@@ -270,7 +270,7 @@ class Audit {
 
       score,
       scoreDisplayMode,
-      numericValue: product.rawValue,
+      numericValue: product.numericValue,
 
       displayValue: product.displayValue,
       explanation: product.explanation,
diff --git a/lighthouse-core/audits/bootup-time.js b/lighthouse-core/audits/bootup-time.js
index c2cc645b6..c5e178fef 100644
--- a/lighthouse-core/audits/bootup-time.js
+++ b/lighthouse-core/audits/bootup-time.js
@@ -179,7 +179,7 @@ class BootupTime extends Audit {
 
     return {
       score,
-      rawValue: totalBootupTime,
+      numericValue: totalBootupTime,
       displayValue: totalBootupTime > 0 ?
         str_(i18n.UIStrings.seconds, {timeInMs: totalBootupTime}) : '',
       details,
diff --git a/lighthouse-core/audits/byte-efficiency/byte-efficiency-audit.js b/lighthouse-core/audits/byte-efficiency/byte-efficiency-audit.js
index 9c99a78fc..634f44851 100644
--- a/lighthouse-core/audits/byte-efficiency/byte-efficiency-audit.js
+++ b/lighthouse-core/audits/byte-efficiency/byte-efficiency-audit.js
@@ -208,7 +208,7 @@ class UnusedBytes extends Audit {
       explanation: result.explanation,
       warnings: result.warnings,
       displayValue,
-      rawValue: wastedMs,
+      numericValue: wastedMs,
       score: UnusedBytes.scoreForWastedMs(wastedMs),
       extendedInfo: {
         value: {
diff --git a/lighthouse-core/audits/byte-efficiency/render-blocking-resources.js b/lighthouse-core/audits/byte-efficiency/render-blocking-resources.js
index 5e51c98a0..bcd9de011 100644
--- a/lighthouse-core/audits/byte-efficiency/render-blocking-resources.js
+++ b/lighthouse-core/audits/byte-efficiency/render-blocking-resources.js
@@ -224,7 +224,7 @@ class RenderBlockingResources extends Audit {
     return {
       displayValue,
       score: ByteEfficiencyAudit.scoreForWastedMs(wastedMs),
-      rawValue: wastedMs,
+      numericValue: wastedMs,
       details,
     };
   }
diff --git a/lighthouse-core/audits/byte-efficiency/total-byte-weight.js b/lighthouse-core/audits/byte-efficiency/total-byte-weight.js
index 542b726e8..615f6ed99 100644
--- a/lighthouse-core/audits/byte-efficiency/total-byte-weight.js
+++ b/lighthouse-core/audits/byte-efficiency/total-byte-weight.js
@@ -99,7 +99,7 @@ class TotalByteWeight extends ByteEfficiencyAudit {
 
     return {
       score,
-      rawValue: totalBytes,
+      numericValue: totalBytes,
       displayValue: str_(UIStrings.displayValue, {totalBytes}),
       extendedInfo: {
         value: {
diff --git a/lighthouse-core/audits/byte-efficiency/uses-long-cache-ttl.js b/lighthouse-core/audits/byte-efficiency/uses-long-cache-ttl.js
index 692fdc384..0d4738055 100644
--- a/lighthouse-core/audits/byte-efficiency/uses-long-cache-ttl.js
+++ b/lighthouse-core/audits/byte-efficiency/uses-long-cache-ttl.js
@@ -284,7 +284,7 @@ class CacheHeaders extends Audit {
 
       return {
         score,
-        rawValue: totalWastedBytes,
+        numericValue: totalWastedBytes,
         displayValue: str_(UIStrings.displayValue, {itemCount: results.length}),
         extendedInfo: {
           value: {
diff --git a/lighthouse-core/audits/diagnostics.js b/lighthouse-core/audits/diagnostics.js
index 2a1fb54d4..ac52685d5 100644
--- a/lighthouse-core/audits/diagnostics.js
+++ b/lighthouse-core/audits/diagnostics.js
@@ -66,7 +66,6 @@ class Diagnostics extends Audit {
 
     return {
       score: 1,
-      rawValue: 1,
       details: {
         type: 'debugdata',
         // TODO: Consider not nesting diagnostics under `items`.
diff --git a/lighthouse-core/audits/dobetterweb/dom-size.js b/lighthouse-core/audits/dobetterweb/dom-size.js
index e02e9869f..c3cffd61f 100644
--- a/lighthouse-core/audits/dobetterweb/dom-size.js
+++ b/lighthouse-core/audits/dobetterweb/dom-size.js
@@ -135,7 +135,7 @@ class DOMSize extends Audit {
 
     return {
       score,
-      rawValue: stats.totalBodyElements,
+      numericValue: stats.totalBodyElements,
       displayValue: str_(UIStrings.displayValue, {itemCount: stats.totalBodyElements}),
       extendedInfo: {
         value: items,
diff --git a/lighthouse-core/audits/errors-in-console.js b/lighthouse-core/audits/errors-in-console.js
index 1df3399a4..079c6f898 100644
--- a/lighthouse-core/audits/errors-in-console.js
+++ b/lighthouse-core/audits/errors-in-console.js
@@ -71,7 +71,7 @@ class ErrorLogs extends Audit {
 
     return {
       score: Number(numErrors === 0),
-      rawValue: numErrors,
+      numericValue: numErrors,
       details,
     };
   }
diff --git a/lighthouse-core/audits/load-fast-enough-for-pwa.js b/lighthouse-core/audits/load-fast-enough-for-pwa.js
index 102fd9796..cbc226b74 100644
--- a/lighthouse-core/audits/load-fast-enough-for-pwa.js
+++ b/lighthouse-core/audits/load-fast-enough-for-pwa.js
@@ -95,7 +95,7 @@ class LoadFastEnough4Pwa extends Audit {
       score,
       displayValue,
       explanation,
-      rawValue: tti.timing,
+      numericValue: tti.timing,
     };
   }
 }
diff --git a/lighthouse-core/audits/main-thread-tasks.js b/lighthouse-core/audits/main-thread-tasks.js
index 1399e6248..e3265f8bb 100644
--- a/lighthouse-core/audits/main-thread-tasks.js
+++ b/lighthouse-core/audits/main-thread-tasks.js
@@ -51,7 +51,7 @@ class MainThreadTasks extends Audit {
 
     return {
       score: 1,
-      rawValue: results.length,
+      numericValue: results.length,
       details: tableDetails,
     };
   }
diff --git a/lighthouse-core/audits/mainthread-work-breakdown.js b/lighthouse-core/audits/mainthread-work-breakdown.js
index 9325693ae..1865613c3 100644
--- a/lighthouse-core/audits/mainthread-work-breakdown.js
+++ b/lighthouse-core/audits/mainthread-work-breakdown.js
@@ -122,7 +122,7 @@ class MainThreadWorkBreakdown extends Audit {
 
     return {
       score,
-      rawValue: totalExecutionTime,
+      numericValue: totalExecutionTime,
       displayValue: str_(i18n.UIStrings.seconds, {timeInMs: totalExecutionTime}),
       details: tableDetails,
     };
diff --git a/lighthouse-core/audits/metrics.js b/lighthouse-core/audits/metrics.js
index 8818812c8..2a7db3068 100644
--- a/lighthouse-core/audits/metrics.js
+++ b/lighthouse-core/audits/metrics.js
@@ -117,7 +117,7 @@ class Metrics extends Audit {
 
     return {
       score: 1,
-      rawValue: (interactive && interactive.timing) || 0,
+      numericValue: (interactive && interactive.timing) || 0,
       details,
     };
   }
diff --git a/lighthouse-core/audits/metrics/estimated-input-latency.js b/lighthouse-core/audits/metrics/estimated-input-latency.js
index 5c9128814..38efde2b3 100644
--- a/lighthouse-core/audits/metrics/estimated-input-latency.js
+++ b/lighthouse-core/audits/metrics/estimated-input-latency.js
@@ -66,7 +66,7 @@ class EstimatedInputLatency extends Audit {
         context.options.scorePODR,
         context.options.scoreMedian
       ),
-      rawValue: metricResult.timing,
+      numericValue: metricResult.timing,
       displayValue: str_(i18n.UIStrings.ms, {timeInMs: metricResult.timing}),
     };
   }
diff --git a/lighthouse-core/audits/metrics/first-contentful-paint-3g.js b/lighthouse-core/audits/metrics/first-contentful-paint-3g.js
index 1e9413fb6..8742c2752 100644
--- a/lighthouse-core/audits/metrics/first-contentful-paint-3g.js
+++ b/lighthouse-core/audits/metrics/first-contentful-paint-3g.js
@@ -55,7 +55,7 @@ class FirstContentfulPaint3G extends Audit {
         context.options.scorePODR,
         context.options.scoreMedian
       ),
-      rawValue: metricResult.timing,
+      numericValue: metricResult.timing,
       displayValue: `${metricResult.timing}\xa0ms`,
     };
   }
diff --git a/lighthouse-core/audits/metrics/first-contentful-paint.js b/lighthouse-core/audits/metrics/first-contentful-paint.js
index 5da25f0d9..aa8753738 100644
--- a/lighthouse-core/audits/metrics/first-contentful-paint.js
+++ b/lighthouse-core/audits/metrics/first-contentful-paint.js
@@ -63,7 +63,7 @@ class FirstContentfulPaint extends Audit {
         context.options.scorePODR,
         context.options.scoreMedian
       ),
-      rawValue: metricResult.timing,
+      numericValue: metricResult.timing,
       displayValue: str_(i18n.UIStrings.seconds, {timeInMs: metricResult.timing}),
     };
   }
diff --git a/lighthouse-core/audits/metrics/first-cpu-idle.js b/lighthouse-core/audits/metrics/first-cpu-idle.js
index fe2e3d586..6557ba152 100644
--- a/lighthouse-core/audits/metrics/first-cpu-idle.js
+++ b/lighthouse-core/audits/metrics/first-cpu-idle.js
@@ -67,7 +67,7 @@ class FirstCPUIdle extends Audit {
         context.options.scorePODR,
         context.options.scoreMedian
       ),
-      rawValue: metricResult.timing,
+      numericValue: metricResult.timing,
       displayValue: str_(i18n.UIStrings.seconds, {timeInMs: metricResult.timing}),
     };
   }
diff --git a/lighthouse-core/audits/metrics/first-meaningful-paint.js b/lighthouse-core/audits/metrics/first-meaningful-paint.js
index 6272884ae..fe83fc0f7 100644
--- a/lighthouse-core/audits/metrics/first-meaningful-paint.js
+++ b/lighthouse-core/audits/metrics/first-meaningful-paint.js
@@ -66,7 +66,7 @@ class FirstMeaningfulPaint extends Audit {
         context.options.scorePODR,
         context.options.scoreMedian
       ),
-      rawValue: metricResult.timing,
+      numericValue: metricResult.timing,
       displayValue: str_(i18n.UIStrings.seconds, {timeInMs: metricResult.timing}),
     };
   }
diff --git a/lighthouse-core/audits/metrics/interactive.js b/lighthouse-core/audits/metrics/interactive.js
index df3891628..d14628444 100644
--- a/lighthouse-core/audits/metrics/interactive.js
+++ b/lighthouse-core/audits/metrics/interactive.js
@@ -78,7 +78,7 @@ class InteractiveMetric extends Audit {
         context.options.scorePODR,
         context.options.scoreMedian
       ),
-      rawValue: timeInMs,
+      numericValue: timeInMs,
       displayValue: str_(i18n.UIStrings.seconds, {timeInMs}),
       extendedInfo: {
         value: extendedInfo,
diff --git a/lighthouse-core/audits/metrics/max-potential-fid.js b/lighthouse-core/audits/metrics/max-potential-fid.js
index 29bbbfc43..3a81771bf 100644
--- a/lighthouse-core/audits/metrics/max-potential-fid.js
+++ b/lighthouse-core/audits/metrics/max-potential-fid.js
@@ -66,7 +66,7 @@ class MaxPotentialFID extends Audit {
         context.options.scorePODR,
         context.options.scoreMedian
       ),
-      rawValue: metricResult.timing,
+      numericValue: metricResult.timing,
       displayValue: str_(i18n.UIStrings.ms, {timeInMs: metricResult.timing}),
     };
   }
diff --git a/lighthouse-core/audits/metrics/speed-index.js b/lighthouse-core/audits/metrics/speed-index.js
index 54ad2de7b..d8cb2f193 100644
--- a/lighthouse-core/audits/metrics/speed-index.js
+++ b/lighthouse-core/audits/metrics/speed-index.js
@@ -65,7 +65,7 @@ class SpeedIndex extends Audit {
         context.options.scorePODR,
         context.options.scoreMedian
       ),
-      rawValue: metricResult.timing,
+      numericValue: metricResult.timing,
       displayValue: str_(i18n.UIStrings.seconds, {timeInMs: metricResult.timing}),
     };
   }
diff --git a/lighthouse-core/audits/network-requests.js b/lighthouse-core/audits/network-requests.js
index 2a8235220..d15f8e28b 100644
--- a/lighthouse-core/audits/network-requests.js
+++ b/lighthouse-core/audits/network-requests.js
@@ -90,7 +90,7 @@ class NetworkRequests extends Audit {
 
       return {
         score: 1,
-        rawValue: results.length,
+        numericValue: results.length,
         details: tableDetails,
       };
     });
diff --git a/lighthouse-core/audits/network-rtt.js b/lighthouse-core/audits/network-rtt.js
index 49986ca85..13f074fe2 100644
--- a/lighthouse-core/audits/network-rtt.js
+++ b/lighthouse-core/audits/network-rtt.js
@@ -69,7 +69,7 @@ class NetworkRTT extends Audit {
 
     return {
       score: 1,
-      rawValue: maxRtt,
+      numericValue: maxRtt,
       displayValue: str_(i18n.UIStrings.ms, {timeInMs: maxRtt}),
       details: tableDetails,
     };
diff --git a/lighthouse-core/audits/network-server-latency.js b/lighthouse-core/audits/network-server-latency.js
index 1f3b37853..a3067a58d 100644
--- a/lighthouse-core/audits/network-server-latency.js
+++ b/lighthouse-core/audits/network-server-latency.js
@@ -68,7 +68,7 @@ class NetworkServerLatency extends Audit {
 
     return {
       score: Math.max(1 - (maxLatency / 500), 0),
-      rawValue: maxLatency,
+      numericValue: maxLatency,
       displayValue: str_(i18n.UIStrings.ms, {timeInMs: maxLatency}),
       details: tableDetails,
     };
diff --git a/lighthouse-core/audits/predictive-perf.js b/lighthouse-core/audits/predictive-perf.js
index 369c1c07e..0c892df96 100644
--- a/lighthouse-core/audits/predictive-perf.js
+++ b/lighthouse-core/audits/predictive-perf.js
@@ -88,7 +88,7 @@ class PredictivePerf extends Audit {
 
     return {
       score,
-      rawValue: values.roughEstimateOfTTI,
+      numericValue: values.roughEstimateOfTTI,
       displayValue: Util.formatMilliseconds(values.roughEstimateOfTTI),
       details: {
         type: 'debugdata',
diff --git a/lighthouse-core/audits/redirects.js b/lighthouse-core/audits/redirects.js
index a2b1c7456..9e40a18a1 100644
--- a/lighthouse-core/audits/redirects.js
+++ b/lighthouse-core/audits/redirects.js
@@ -108,7 +108,7 @@ class Redirects extends Audit {
     return {
       // We award a passing grade if you only have 1 redirect
       score: redirectRequests.length <= 2 ? 1 : UnusedBytes.scoreForWastedMs(totalWastedMs),
-      rawValue: totalWastedMs,
+      numericValue: totalWastedMs,
       displayValue: totalWastedMs ?
         str_(i18n.UIStrings.displayValueMsSavings, {wastedMs: totalWastedMs}) :
         '',
diff --git a/lighthouse-core/audits/time-to-first-byte.js b/lighthouse-core/audits/time-to-first-byte.js
index ec6610ff2..37982b6e4 100644
--- a/lighthouse-core/audits/time-to-first-byte.js
+++ b/lighthouse-core/audits/time-to-first-byte.js
@@ -69,7 +69,7 @@ class TTFBMetric extends Audit {
     };
 
     return {
-      rawValue: ttfb,
+      numericValue: ttfb,
       score: Number(passed),
       displayValue,
       details,
diff --git a/lighthouse-core/audits/uses-rel-preconnect.js b/lighthouse-core/audits/uses-rel-preconnect.js
index e08620b25..8e80cda44 100644
--- a/lighthouse-core/audits/uses-rel-preconnect.js
+++ b/lighthouse-core/audits/uses-rel-preconnect.js
@@ -188,7 +188,7 @@ class UsesRelPreconnectAudit extends Audit {
 
     return {
       score: UnusedBytes.scoreForWastedMs(maxWasted),
-      rawValue: maxWasted,
+      numericValue: maxWasted,
       displayValue: maxWasted ?
         str_(i18n.UIStrings.displayValueMsSavings, {wastedMs: maxWasted}) :
         '',
diff --git a/lighthouse-core/audits/uses-rel-preload.js b/lighthouse-core/audits/uses-rel-preload.js
index 671e2ffea..240a6a9ba 100644
--- a/lighthouse-core/audits/uses-rel-preload.js
+++ b/lighthouse-core/audits/uses-rel-preload.js
@@ -222,7 +222,7 @@ class UsesRelPreloadAudit extends Audit {
 
     return {
       score: UnusedBytes.scoreForWastedMs(wastedMs),
-      rawValue: wastedMs,
+      numericValue: wastedMs,
       displayValue: wastedMs ?
         str_(i18n.UIStrings.displayValueMsSavings, {wastedMs}) :
         '',
diff --git a/lighthouse-core/gather/connections/cri.js b/lighthouse-core/gather/connections/cri.js
index 8b47e11b9..8fae2b3c4 100644
--- a/lighthouse-core/gather/connections/cri.js
+++ b/lighthouse-core/gather/connections/cri.js
@@ -108,21 +108,17 @@ class CriConnection extends Connection {
         });
       });
 
-      request.setTimeout(CONNECT_TIMEOUT, () => {
-        request.abort();
-
-        // After aborting, we expect an ECONNRESET error. Ignore.
-        request.on('error', err => {
-          // @ts-ignore - not in @types yet. See https://nodejs.org/api/errors.html#errors_class_systemerror.
-          if (err.code !== 'ECONNRESET') {
-            throw err;
-          }
-        });
+      // This error handler is critical to ensuring Lighthouse exits cleanly even when Chrome crashes.
+      // See https://github.com/GoogleChrome/lighthouse/pull/8583.
+      request.on('error', reject);
 
+      request.setTimeout(CONNECT_TIMEOUT, () => {
         // Reject on error with code specifically indicating timeout in connection setup.
         const err = new LighthouseError(LighthouseError.errors.CRI_TIMEOUT);
         log.error('CriConnection', err.friendlyMessage);
         reject(err);
+
+        request.abort();
       });
     });
   }
diff --git a/lighthouse-core/lib/i18n/en-US.json b/lighthouse-core/lib/i18n/en-US.json
index edda6fb8d..3c37fd85c 100644
--- a/lighthouse-core/lib/i18n/en-US.json
+++ b/lighthouse-core/lib/i18n/en-US.json
@@ -1260,8 +1260,8 @@
     "description": "Error message explaining that Lighthouse couldn't complete because the page has stopped responding to its instructions."
   },
   "lighthouse-core/lib/lh-error.js | pageLoadFailedInsecure": {
-    "message": "The URL you have provided does not have valid security credentials. {securityMessages}",
-    "description": "Error message explaining that the credentials included in the Lighthouse run were invalid, so the URL cannot be accessed. securityMessages will be replaced with one or more strings from the browser explaining what was insecure about the page load."
+    "message": "The URL you have provided does not have a valid security certificate. {securityMessages}",
+    "description": "Error message explaining that the security certificate of the page Lighthouse observed was invalid, so the URL cannot be accessed. securityMessages will be replaced with one or more strings from the browser explaining what was insecure about the page load."
   },
   "lighthouse-core/lib/lh-error.js | pageLoadFailedWithDetails": {
     "message": "Lighthouse was unable to reliably load the page you requested. Make sure you are testing the correct URL and that the server is properly responding to all requests. (Details: {errorDetails})",
diff --git a/lighthouse-core/lib/lh-error.js b/lighthouse-core/lib/lh-error.js
index a349667fb..fdbf06d7a 100644
--- a/lighthouse-core/lib/lh-error.js
+++ b/lighthouse-core/lib/lh-error.js
@@ -21,8 +21,8 @@ const UIStrings = {
   pageLoadFailedWithStatusCode: 'Lighthouse was unable to reliably load the page you requested. Make sure you are testing the correct URL and that the server is properly responding to all requests. (Status code: {statusCode})',
   /** Error message explaining that Lighthouse could not load the requested URL and the steps that might be taken to fix the unreliability. */
   pageLoadFailedWithDetails: 'Lighthouse was unable to reliably load the page you requested. Make sure you are testing the correct URL and that the server is properly responding to all requests. (Details: {errorDetails})',
-  /** Error message explaining that the credentials included in the Lighthouse run were invalid, so the URL cannot be accessed. securityMessages will be replaced with one or more strings from the browser explaining what was insecure about the page load. */
-  pageLoadFailedInsecure: 'The URL you have provided does not have valid security credentials. {securityMessages}',
+  /** Error message explaining that the security certificate of the page Lighthouse observed was invalid, so the URL cannot be accessed. securityMessages will be replaced with one or more strings from the browser explaining what was insecure about the page load. */
+  pageLoadFailedInsecure: 'The URL you have provided does not have a valid security certificate. {securityMessages}',
   /** Error message explaining that Chrome has encountered an error during the Lighthouse run, and that Chrome should be restarted. */
   internalChromeError: 'An internal Chrome error occurred. Please restart Chrome and try re-running Lighthouse.',
   /** Error message explaining that fetching the resources of the webpage has taken longer than the maximum time. */
diff --git a/lighthouse-core/report/html/renderer/report-renderer.js b/lighthouse-core/report/html/renderer/report-renderer.js
index aaf2e54b2..8b5dedf66 100644
--- a/lighthouse-core/report/html/renderer/report-renderer.js
+++ b/lighthouse-core/report/html/renderer/report-renderer.js
@@ -157,6 +157,39 @@ class ReportRenderer {
     return container;
   }
 
+  /**
+   * @param {LH.ReportResult} report
+   * @param {CategoryRenderer} categoryRenderer
+   * @param {Record<string, CategoryRenderer>} specificCategoryRenderers
+   * @return {DocumentFragment[]}
+   */
+  _renderScoreGauges(report, categoryRenderer, specificCategoryRenderers) {
+    // Group gauges in this order: default, pwa, plugins.
+    const defaultGauges = [];
+    const customGauges = []; // PWA.
+    const pluginGauges = [];
+
+    for (const category of report.reportCategories) {
+      const renderer = specificCategoryRenderers[category.id] || categoryRenderer;
+      const categoryGauge = renderer.renderScoreGauge(category, report.categoryGroups || {});
+
+      if (Util.isPluginCategory(category.id)) {
+        pluginGauges.push(categoryGauge);
+      } else if (renderer.renderScoreGauge === categoryRenderer.renderScoreGauge) {
+        // The renderer for default categories is just the default CategoryRenderer.
+        // If the functions are equal, then renderer is an instance of CategoryRenderer.
+        // For example, the PWA category uses PwaCategoryRenderer, which overrides
+        // CategoryRenderer.renderScoreGauge, so it would fail this check and be placed
+        // in the customGauges bucket.
+        defaultGauges.push(categoryGauge);
+      } else {
+        customGauges.push(categoryGauge);
+      }
+    }
+
+    return [...defaultGauges, ...customGauges, ...pluginGauges];
+  }
+
   /**
    * @param {LH.ReportResult} report
    * @return {DocumentFragment}
@@ -223,21 +256,9 @@ class ReportRenderer {
     // }
 
     if (scoreHeader) {
-      const defaultGauges = [];
-      const customGauges = [];
-      for (const category of report.reportCategories) {
-        const renderer = specificCategoryRenderers[category.id] || categoryRenderer;
-        const categoryGauge = renderer.renderScoreGauge(category, report.categoryGroups || {});
-
-        // Group gauges that aren't default at the end of the header
-        if (renderer.renderScoreGauge === categoryRenderer.renderScoreGauge) {
-          defaultGauges.push(categoryGauge);
-        } else {
-          customGauges.push(categoryGauge);
-        }
-      }
-      scoreHeader.append(...defaultGauges, ...customGauges);
-
+      const scoreGauges =
+        this._renderScoreGauges(report, categoryRenderer, specificCategoryRenderers);
+      scoreHeader.append(...scoreGauges);
       const scoreScale = this._dom.cloneTemplate('#tmpl-lh-scorescale', this._templateContext);
       const scoresContainer = this._dom.find('.lh-scores-container', headerContainer);
       scoresContainer.appendChild(scoreHeader);
@@ -247,10 +268,23 @@ class ReportRenderer {
     reportSection.appendChild(this._renderReportFooter(report));
 
     const reportFragment = this._dom.createFragment();
+
     if (!this._dom.isDevTools()) {
       const topbarDocumentFragment = this._renderReportTopbar(report);
       reportFragment.appendChild(topbarDocumentFragment);
     }
+
+    if (scoreHeader && !this._dom.isDevTools()) {
+      const stickyHeader = this._dom.createElement('div', 'lh-sticky-header');
+      this._dom.createChildOf(stickyHeader, 'div', 'lh-highlighter');
+
+      const scoreGauges =
+        this._renderScoreGauges(report, categoryRenderer, specificCategoryRenderers);
+      stickyHeader.append(...scoreGauges);
+
+      reportFragment.appendChild(stickyHeader);
+    }
+
     reportFragment.appendChild(headerContainer);
     reportFragment.appendChild(container);
 
diff --git a/lighthouse-core/report/html/renderer/report-ui-features.js b/lighthouse-core/report/html/renderer/report-ui-features.js
index 4d8bdd6a4..62e5e2e8d 100644
--- a/lighthouse-core/report/html/renderer/report-ui-features.js
+++ b/lighthouse-core/report/html/renderer/report-ui-features.js
@@ -40,6 +40,14 @@ class ReportUIFeatures {
     this._copyAttempt = false;
     /** @type {HTMLElement} */
     this.exportButton; // eslint-disable-line no-unused-expressions
+    /** @type {HTMLElement} */
+    this.topbarEl; // eslint-disable-line no-unused-expressions
+    /** @type {HTMLElement} */
+    this.scoreScaleEl; // eslint-disable-line no-unused-expressions
+    /** @type {HTMLElement} */
+    this.stickyHeaderEl; // eslint-disable-line no-unused-expressions
+    /** @type {HTMLElement} */
+    this.highlightEl; // eslint-disable-line no-unused-expressions
 
     this.onMediaQueryChange = this.onMediaQueryChange.bind(this);
     this.onCopy = this.onCopy.bind(this);
@@ -48,6 +56,7 @@ class ReportUIFeatures {
     this.onKeyDown = this.onKeyDown.bind(this);
     this.printShortCutDetect = this.printShortCutDetect.bind(this);
     this.onChevronClick = this.onChevronClick.bind(this);
+    this._updateStickyHeaderOnScroll = this._updateStickyHeaderOnScroll.bind(this);
   }
 
   /**
@@ -61,10 +70,13 @@ class ReportUIFeatures {
     this.json = report;
     this._setupMediaQueryListeners();
     this._setupExportButton();
+    this._setupStickyHeaderElements();
     this._setUpCollapseDetailsAfterPrinting();
     this._resetUIState();
     this._document.addEventListener('keydown', this.printShortCutDetect);
     this._document.addEventListener('copy', this.onCopy);
+    this._document.addEventListener('scroll', this._updateStickyHeaderOnScroll);
+    window.addEventListener('resize', this._updateStickyHeaderOnScroll);
   }
 
   /**
@@ -102,6 +114,13 @@ class ReportUIFeatures {
     dropdown.addEventListener('click', this.onExport);
   }
 
+  _setupStickyHeaderElements() {
+    this.topbarEl = this._dom.find('.lh-topbar', this._document);
+    this.scoreScaleEl = this._dom.find('.lh-scorescale', this._document);
+    this.stickyHeaderEl = this._dom.find('.lh-sticky-header', this._document);
+    this.highlightEl = this._dom.find('.lh-highlighter', this._document);
+  }
+
   /**
    * Handle copy events.
    * @param {ClipboardEvent} e
@@ -380,6 +399,30 @@ class ReportUIFeatures {
     this._document.body.removeChild(a);
     setTimeout(_ => URL.revokeObjectURL(href), 500);
   }
+
+  _updateStickyHeaderOnScroll() {
+    // Show sticky header when the score scale begins to go underneath the topbar.
+    const topbarBottom = this.topbarEl.getBoundingClientRect().bottom;
+    const scoreScaleTop = this.scoreScaleEl.getBoundingClientRect().top;
+    const showStickyHeader = topbarBottom >= scoreScaleTop;
+
+    // Highlight mini gauge when section is in view.
+    // In view = the last category that starts above the middle of the window.
+    const categoryEls = Array.from(this._document.querySelectorAll('.lh-category'));
+    const categoriesAboveTheMiddle =
+      categoryEls.filter(el => el.getBoundingClientRect().top - window.innerHeight / 2 < 0);
+    const highlightIndex =
+      categoriesAboveTheMiddle.length > 0 ? categoriesAboveTheMiddle.length - 1 : 0;
+
+    // Category order matches gauge order in sticky header.
+    const gaugeWrapperEls = this.stickyHeaderEl.querySelectorAll('.lh-gauge__wrapper');
+    const gaugeToHighlight = gaugeWrapperEls[highlightIndex];
+    const offset = gaugeToHighlight.getBoundingClientRect().left + 'px';
+
+    // Mutate at end to avoid layout thrashing.
+    this.stickyHeaderEl.classList.toggle('lh-sticky-header--visible', showStickyHeader);
+    this.highlightEl.style.left = offset;
+  }
 }
 
 if (typeof module !== 'undefined' && module.exports) {
diff --git a/lighthouse-core/report/html/report-styles.css b/lighthouse-core/report/html/report-styles.css
index 57d1fc61c..0a5a5d345 100644
--- a/lighthouse-core/report/html/report-styles.css
+++ b/lighthouse-core/report/html/report-styles.css
@@ -106,6 +106,8 @@
   --plugin-badge-size-big: calc(var(--gauge-circle-size-big) / 2.7);
   --plugin-badge-size: calc(var(--gauge-circle-size) / 2.7);
   --plugin-icon-size: 65%;
+  --pwa-icon-margin: 0 6px 0 -2px;
+  --pwa-icon-size: var(--topbar-icon-size);
   --score-container-padding: 12px;
   --score-container-width: 160px;
   --score-number-font-size-big: 42px;
@@ -116,6 +118,8 @@
   --score-title-font-size: 20px;
   --score-title-line-height-big: 36px;
   --score-title-line-height: 26px;
+  --scorescale-height: 6px;
+  --scorescale-width: 18px;
   --section-padding: 40px;
   --topbar-bg: var(--color-black-100);
   --topbar-height: 36px;
@@ -128,6 +132,8 @@
   --color-fail: var(--color-red);
   --color-pass-secondary: var(--color-green-700);
   --color-pass: var(--color-green);
+  --color-sticky-header-bg: var(--color-white);
+  --color-highlighter-bg: var(--color-black);
 
   --plugin-icon-url: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24px" height="24px" viewBox="0 0 24 24" fill="%23757575"><path d="M0 0h24v24H0z" fill="none"/><path d="M20.5 11H19V7c0-1.1-.9-2-2-2h-4V3.5C13 2.12 11.88 1 10.5 1S8 2.12 8 3.5V5H4c-1.1 0-1.99.9-1.99 2v3.8H3.5c1.49 0 2.7 1.21 2.7 2.7s-1.21 2.7-2.7 2.7H2V20c0 1.1.9 2 2 2h3.8v-1.5c0-1.49 1.21-2.7 2.7-2.7 1.49 0 2.7 1.21 2.7 2.7V22H17c1.1 0 2-.9 2-2v-4h1.5c1.38 0 2.5-1.12 2.5-2.5S21.88 11 20.5 11z"/></svg>');
   --logo-url: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="192" height="192"><g fill="none" fill-rule="evenodd"><path d="M0 0h192v192H0z"/><path d="M67.705 179.352l2.603-20.82 49.335-16.39 4.652 37.21A87.893 87.893 0 0 1 96 184a87.893 87.893 0 0 1-28.295-4.648zM52.44 172.48C25.894 157.328 8 128.754 8 96 8 47.399 47.399 8 96 8s88 39.399 88 88c0 32.754-17.894 61.328-44.44 76.48L130 96h6V80h-8V48L96 28 64 48v32h-8v16h6l-9.56 76.48zM113.875 96l2.882 23.05-43.318 14.433L78.125 96h35.75zM80 80V56.868l16-10 16 10V80H80z" fill="%23000000"/></g></svg>');
@@ -140,9 +146,9 @@
   --pwa-installable-gray-url: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="nonzero"><circle fill="%23DAE0E3" cx="12" cy="12" r="12"/><path d="M12 5a7 7 0 1 0 0 14 7 7 0 0 0 0-14zm3.5 7.7h-2.8v2.8h-1.4v-2.8H8.5v-1.4h2.8V8.5h1.4v2.8h2.8v1.4z" fill="%23FFF"/></g></svg>');
   --pwa-optimized-gray-url: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><rect fill="%23DAE0E3" width="24" height="24" rx="12"/><path fill="%23FFF" d="M12 15.07l3.6 2.18-.95-4.1 3.18-2.76-4.2-.36L12 6.17l-1.64 3.86-4.2.36 3.2 2.76-.96 4.1z"/><path d="M5 5h14v14H5z"/></g></svg>');
 
-  --pwa-fast-reliable-color-url: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg"><g fill-rule="nonzero" fill="none"><circle fill="%23CCE3F6" cx="12" cy="12" r="12"/><path d="M12 4.3l6.3 2.8v4.2c0 3.88-2.69 7.52-6.3 8.4-3.61-.88-6.3-4.51-6.3-8.4V7.1L12 4.3zm-.56 12.88l3.3-5.79.04-.08c.05-.1.01-.29-.26-.29h-1.96l.56-3.92h-.56L9.3 12.82c0 .03.07-.12-.03.07-.11.2-.12.37.2.37h1.97l-.56 3.92h.56z" fill="%23304FFE"/></g></svg>');
-  --pwa-installable-color-url: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg"><g fill-rule="nonzero" fill="none"><circle fill="%23D4ECD5" cx="12" cy="12" r="12"/><path d="M12 5a7 7 0 1 0 0 14 7 7 0 0 0 0-14zm3.5 7.7h-2.8v2.8h-1.4v-2.8H8.5v-1.4h2.8V8.5h1.4v2.8h2.8v1.4z" fill="%23009688"/></g></svg>');
-  --pwa-optimized-color-url: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><rect fill="%23FCE4EC" width="24" height="24" rx="12"/><path d="M5 5h14v14H5z"/><path fill="%23EC3F7A" d="M12 15.07l3.6 2.18-.95-4.1 3.18-2.76-4.2-.36L12 6.17l-1.64 3.86-4.2.36 3.2 2.76-.96 4.1z"/></g></svg>');
+  --pwa-fast-reliable-color-url: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg"><g fill-rule="nonzero" fill="none"><circle fill="%230CCE6B" cx="12" cy="12" r="12"/><path d="M12 4.3l6.3 2.8v4.2c0 3.88-2.69 7.52-6.3 8.4-3.61-.88-6.3-4.51-6.3-8.4V7.1L12 4.3zm-.56 12.88l3.3-5.79.04-.08c.05-.1.01-.29-.26-.29h-1.96l.56-3.92h-.56L9.3 12.82c0 .03.07-.12-.03.07-.11.2-.12.37.2.37h1.97l-.56 3.92h.56z" fill="%23FFF"/></g></svg>');
+  --pwa-installable-color-url: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg"><g fill-rule="nonzero" fill="none"><circle fill="%230CCE6B" cx="12" cy="12" r="12"/><path d="M12 5a7 7 0 1 0 0 14 7 7 0 0 0 0-14zm3.5 7.7h-2.8v2.8h-1.4v-2.8H8.5v-1.4h2.8V8.5h1.4v2.8h2.8v1.4z" fill="%23FFF"/></g></svg>');
+  --pwa-optimized-color-url: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><rect fill="%230CCE6B" width="24" height="24" rx="12"/><path d="M5 5h14v14H5z"/><path fill="%23FFF" d="M12 15.07l3.6 2.18-.95-4.1 3.18-2.76-4.2-.36L12 6.17l-1.64 3.86-4.2.36 3.2 2.76-.96 4.1z"/></g></svg>');
 }
 
 @media only screen and (max-width: 480px) {
@@ -155,6 +161,7 @@
     --gauge-circle-size: 72px;
     --header-padding: 16px 0 16px 0;
     --plugin-icon-size: 75%;
+    --pwa-icon-margin: 0 7px 0 -3px;
     --score-container-padding: 8px;
     --score-container-width: 112px;
     --score-number-font-size-big: 34px;
@@ -656,29 +663,24 @@
 .lh-audit-group__header::before {
   /* By default, groups don't get an icon */
   content: none;
-  width: calc(var(--subheader-font-size) / 14 * 24);
-  height: calc(var(--subheader-font-size) / 14 * 24);
-  margin-right: calc(var(--subheader-font-size) / 2);
-  background: var(--medium-100-gray) none no-repeat center / 16px;
+  width: var(--pwa-icon-size);
+  height: var(--pwa-icon-size);
+  margin: var(--pwa-icon-margin);
   display: inline-block;
-  border-radius: 50%;
   vertical-align: middle;
 }
 
 .lh-audit-group--pwa-fast-reliable .lh-audit-group__header::before {
   content: '';
   background-image: var(--pwa-fast-reliable-gray-url);
-  background-size: var(--lh-group-icon-background-size);
 }
 .lh-audit-group--pwa-installable .lh-audit-group__header::before {
   content: '';
   background-image: var(--pwa-installable-gray-url);
-  background-size: var(--lh-group-icon-background-size);
 }
 .lh-audit-group--pwa-optimized .lh-audit-group__header::before {
   content: '';
   background-image: var(--pwa-optimized-gray-url);
-  background-size: var(--lh-group-icon-background-size);
 }
 .lh-audit-group--pwa-fast-reliable.lh-badged .lh-audit-group__header::before {
   background-image: var(--pwa-fast-reliable-color-url);
@@ -830,7 +832,6 @@
   display: flex;
   flex-wrap: wrap;
   justify-content: center;
-  padding: var(--section-indent) calc(var(--section-indent) / 2) calc(var(--section-indent) * 2);
 }
 .lh-scores-header__solo {
   padding: 0;
@@ -838,7 +839,9 @@
 }
 
 .lh-scores-header .lh-gauge__wrapper,
-.lh-scores-header .lh-gauge--pwa__wrapper {
+.lh-scores-header .lh-gauge--pwa__wrapper,
+.lh-sticky-header .lh-gauge__wrapper,
+.lh-sticky-header .lh-gauge--pwa__wrapper {
   width: var(--score-container-width);
 }
 
@@ -852,11 +855,12 @@
   margin: 12px auto 0 auto;
   border: 1px solid var(--color-black-200);
   border-radius: 20px;
-  color: var(--medium-75-gray);
   padding: 8px 8px;
 }
 
 .lh-scorescale-range {
+  display: flex;
+  align-items: center;
   margin: 0 12px;
   font-family: var(--monospace-font-family);
   white-space: nowrap;
@@ -864,11 +868,11 @@
 
 .lh-scorescale-range::before {
   content: '';
-  width: var(--body-font-size);
-  height: calc(var(--body-font-size) * .50);
+  width: var(--scorescale-width);
+  height: var(--scorescale-height);
   border-radius: 10px;
-  display: inline-block;
-  margin: 0 5px;
+  display: block;
+  margin-right: 10px;
 }
 
 .lh-scorescale-range--pass::before {
diff --git a/lighthouse-core/report/html/templates.html b/lighthouse-core/report/html/templates.html
index fbe6d58f6..dbe7fb8fc 100644
--- a/lighthouse-core/report/html/templates.html
+++ b/lighthouse-core/report/html/templates.html
@@ -26,9 +26,9 @@ limitations under the License.
 <!-- Lighthouse score scale -->
 <template id="tmpl-lh-scorescale">
   <div class="lh-scorescale">
-      <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>
+      <span class="lh-scorescale-range lh-scorescale-range--fail">0&ndash;49</span>
+      <span class="lh-scorescale-range lh-scorescale-range--average">50&ndash;89</span>
+      <span class="lh-scorescale-range lh-scorescale-range--pass">90&ndash;100</span>
   </div>
 </template>
 
@@ -144,10 +144,49 @@ limitations under the License.
       position: relative;
       width: 100%;
     }
+
+    .lh-sticky-header {
+      --gauge-circle-size: 36px;
+      --plugin-badge-size: 18px;
+      --plugin-icon-size: 75%;
+      --score-container-width: 60px;
+      --score-number-font-size: 13px;
+      position: fixed;
+      left: 0;
+      right: 0;
+      top: var(--topbar-height);
+      font-weight: 700;
+      display: flex;
+      justify-content: center;
+      background-color: var(--color-sticky-header-bg);
+      border-bottom: 1px solid var(--color-black-200);
+      padding-top: var(--score-container-padding);
+      padding-bottom: 4px;
+      z-index: 1000;
+      pointer-events: none;
+      opacity: 0;
+    }
+
+    .lh-sticky-header--visible {
+      pointer-events: auto;
+      opacity: 1;
+    }
+
+    .lh-sticky-header .lh-gauge__label {
+      display: none;
+    }
+
+    .lh-highlighter {
+      width: var(--score-container-width);
+      height: 1px;
+      background: var(--color-highlighter-bg);
+      position: absolute;
+      bottom: -1px;
+      left: 0;
+    }
   </style>
   <div class="lh-scores-wrapper">
-    <div class="lh-scores-container">
-    </div>
+    <div class="lh-scores-container"></div>
   </div>
 </template>
 
@@ -513,7 +552,7 @@ limitations under the License.
       content: "";
       position: absolute;
       right: -6px;
-      bottom: -2px;
+      bottom: 5px;
       display: block;
       z-index: 100;
       box-shadow: 0 0 4px rgba(0,0,0,.2);
@@ -562,14 +601,16 @@ limitations under the License.
     .lh-gauge__label {
       font-size: var(--score-title-font-size);
       line-height: var(--score-title-line-height);
-      margin: calc(0.5 * var(--score-title-line-height));
+      margin-bottom: calc(0.5 * var(--score-title-line-height));
       text-align: center;
       color: black;
     }
 
+    /* TODO(#8185) use more BEM (.lh-gauge__label--big) instead of relying on descendent selector */
     .lh-category .lh-gauge__label {
       --score-title-font-size: var(--score-title-font-size-big);
       --score-title-line-height: var(--score-title-line-height-big);
+      margin: calc(0.5 * var(--score-title-line-height));
     }
 
   </style>
diff --git a/lighthouse-core/scripts/release/publish-clean-pristine.sh b/lighthouse-core/scripts/release/publish-clean-pristine.sh
new file mode 100755
index 000000000..f4bf04a26
--- /dev/null
+++ b/lighthouse-core/scripts/release/publish-clean-pristine.sh
@@ -0,0 +1,17 @@
+#!/usr/bin/env bash
+
+DIRNAME="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+LH_ROOT="$DIRNAME/../../.."
+cd $LH_ROOT
+
+set -euxo pipefail
+
+# Setup a pristine git environment
+cd ../lighthouse-pristine
+
+if [[ -z "$(git status --porcelain)" ]]; then
+  echo "Pristine repo already clean!"
+  exit 0
+fi
+
+git clean -fx
diff --git a/lighthouse-core/scripts/release/publish-prepare-commit.sh b/lighthouse-core/scripts/release/publish-prepare-commit.sh
new file mode 100755
index 000000000..1f7bd65c6
--- /dev/null
+++ b/lighthouse-core/scripts/release/publish-prepare-commit.sh
@@ -0,0 +1,76 @@
+#!/usr/bin/env bash
+
+TXT_BOLD=$(tput bold)
+TXT_DIM=$(tput setaf 245)
+TXT_RESET=$(tput sgr0)
+
+DIRNAME="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+LH_ROOT="$DIRNAME/../../.."
+cd "$LH_ROOT"
+
+set -euxo pipefail
+
+if [[ "$#" -ne 1 ]]; then
+  echo "You must specify the version to prepare a commit for!"
+  exit 1
+fi
+
+OLD_VERSION=$(node -e "console.log(require('./package.json').version)")
+NEW_VERSION=$1
+BRANCH_NAME="bump_$NEW_VERSION"
+SEMVER_PATTERN="[0-9]*\.[0-9]*\.[0-9]*"
+
+if [[ $(echo "$NEW_VERSION" | sed 's/[0-9]*\.[0-9]*\.[0-9]*/SECRET_REPLACE/g') != "SECRET_REPLACE" ]]; then
+ echo "Incorrect version format. Must be x.x.x"
+ exit 1
+fi
+
+if [[ -n "$(git status --porcelain)" ]]; then
+  echo "Repo has changes to the files! Commit or stash the changes to continue."
+  exit 1
+fi
+
+# Checkout a new branch for the version commit
+git fetch origin master
+git checkout origin/master
+git log -n 1
+git branch -D "$BRANCH_NAME" || true
+git checkout -b "$BRANCH_NAME"
+
+# Install the dependencies.
+yarn install
+
+# Bump the version in package.json and clients/extension/manifest.json
+NEEDLE="^  \"version\": \"$SEMVER_PATTERN\""
+REPLACEMENT="  \"version\": \"$NEW_VERSION\""
+
+sed -i '' "s/$NEEDLE/$REPLACEMENT/g" package.json clients/extension/manifest.json
+
+# Update the fixtures with the new version
+yarn update:sample-json
+
+# Create the changelog entry
+yarn changelog
+
+# Add new contributors to changelog
+git --no-pager shortlog -s -e -n "v2.3.0..v${OLD_VERSION}" | cut -f 2 | sort > auto_contribs_prior_to_last
+git --no-pager shortlog -s -e -n "v${OLD_VERSION}..HEAD" | cut -f 2 | sort > auto_contribs_since_last
+NEW_CONTRIBUTORS=$(comm -13 auto_contribs_prior_to_last auto_contribs_since_last)
+rm auto_contribs_prior_to_last auto_contribs_since_last
+
+if [[ $(echo "$NEW_CONTRIBUTORS" | wc -l) -gt 1 ]]; then
+  printf "Thanks to our new contributors 👽🐷🐰🐯🐻! \n$NEW_CONTRIBUTORS\n" | cat - changelog.md > tmp-changelog
+  mv tmp-changelog changelog.md
+fi
+
+git add changelog.md lighthouse-core/test/results/ proto/
+git commit -m "$NEW_VERSION"
+
+echo "Version bump commit ready on the ${TXT_BOLD}$BRANCH_NAME${TXT_RESET} branch!"
+
+echo "${TXT_DIM}Press any key to see the git diff, CTRL+C to exit...${TXT_RESET}"
+read -n 1 -r unused_variable
+git --no-pager diff HEAD^
+echo "${TXT_DIM}Press any key to push to GitHub, CTRL+C to exit...${TXT_RESET}"
+read -n 1 -r unused_variable
+git push -u origin "$BRANCH_NAME"
diff --git a/lighthouse-core/scripts/release/publish-prepare-package.sh b/lighthouse-core/scripts/release/publish-prepare-package.sh
new file mode 100755
index 000000000..c5398c5d7
--- /dev/null
+++ b/lighthouse-core/scripts/release/publish-prepare-package.sh
@@ -0,0 +1,39 @@
+#!/usr/bin/env bash
+
+DIRNAME="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+LH_PRISTINE_ROOT="$DIRNAME/../../../../lighthouse-pristine"
+
+set -euxo pipefail
+
+bash "$DIRNAME/release-prepare-pristine.sh"
+
+cd "$LH_PRISTINE_ROOT"
+
+VERSION=$(node -e "console.log(require('./package.json').version)")
+
+if ! git rev-parse "v$VERSION" ; then
+  if ! git --no-pager log -n 1 --oneline | grep "v$VERSION" ; then
+    echo "Cannot tag a commit other than the version bump!";
+    exit 1;
+  fi
+
+  git tag -a "v$VERSION" -m "v$VERSION"
+fi
+
+git checkout -f "v$VERSION"
+
+# Install the dependencies.
+yarn install
+
+# Build everything
+yarn build-all
+
+# Package the extension
+node build/build-extension.js package
+
+# Verify the npm package won't include unncessary files
+npm pack --dry-run
+npx pkgfiles
+
+echo "Make sure the files above look good!"
+
diff --git a/lighthouse-core/scripts/release/publish-prepare-pristine.sh b/lighthouse-core/scripts/release/publish-prepare-pristine.sh
new file mode 100755
index 000000000..960136ed0
--- /dev/null
+++ b/lighthouse-core/scripts/release/publish-prepare-pristine.sh
@@ -0,0 +1,27 @@
+#!/usr/bin/env bash
+
+DIRNAME="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+LH_ROOT="$DIRNAME/../../.."
+cd $LH_ROOT
+
+set -euxo pipefail
+
+# Setup a pristine git environment
+cd ../
+
+if [[ ! -e lighthouse-pristine/ ]]; then
+  git clone git@github.com:GoogleChrome/lighthouse.git lighthouse-pristine
+fi
+
+cd lighthouse-pristine/
+
+if [[ -n "$(git status --porcelain)" ]]; then
+  echo "Pristine repo has changes to the files! Commit or stash the changes to continue."
+  exit 1
+fi
+
+git fetch origin
+git fetch --tags
+git checkout -f master
+git reset --hard origin/master
+git clean -fdx # Forcibly clean all untracked files and directories, including `.gitignore`d ones.
diff --git a/proto/sample_v2_round_trip.json b/proto/sample_v2_round_trip.json
index 215f93705..3b4f77214 100644
--- a/proto/sample_v2_round_trip.json
+++ b/proto/sample_v2_round_trip.json
@@ -461,7 +461,6 @@
                 "type": "debugdata"
             }, 
             "id": "diagnostics", 
-            "numericValue": 1.0, 
             "score": null, 
             "scoreDisplayMode": "informative", 
             "title": "Diagnostics"
diff --git a/types/audit.d.ts b/types/audit.d.ts
index 9be3f533d..05ecf3d85 100644
--- a/types/audit.d.ts
+++ b/types/audit.d.ts
@@ -81,7 +81,7 @@ declare global {
       /** Overrides scoreDisplayMode with notApplicable if set to true */
       notApplicable?: boolean;
       /** A numeric value that has a meaning specific to the audit, e.g. the number of nodes in the DOM or the timestamp of a specific load event. More information can be found in the audit details, if present. */
-      rawValue?: number;
+      numericValue?: number;
       /** Extra information about the page provided by some types of audits, in one of several possible forms that can be rendered in the HTML report. */
       details?: Audit.Details;
     }


Test patch:
diff --git a/lighthouse-core/scripts/release/publish-test.sh b/lighthouse-core/scripts/release/publish-test.sh
new file mode 100755
index 000000000..2a5b52765
--- /dev/null
+++ b/lighthouse-core/scripts/release/publish-test.sh
@@ -0,0 +1,59 @@
+#!/usr/bin/env bash
+
+TXT_BOLD=$(tput bold)
+TXT_DIM=$(tput setaf 245)
+TXT_RESET=$(tput sgr0)
+
+DIRNAME="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+LH_PRISTINE_ROOT="$DIRNAME/../../../../lighthouse-pristine"
+
+set -euxo pipefail
+
+bash "$DIRNAME/release-prepare-pristine.sh"
+
+cd "$LH_PRISTINE_ROOT"
+
+# Install deps
+yarn --check-files
+
+# Test err'thing
+echo "${TXT_BOLD}Building all the clients..."
+yarn build-all
+
+echo "Running the standard test suite..."
+yarn test
+
+echo "Running the smoke tests...."
+yarn smoke
+
+echo "Testing the CLI..."
+yarn start "https://example.com" --view
+
+echo "Testing a fresh local install..."
+VERSION=$(node -e "console.log(require('./package.json').version)")
+npm pack
+
+rm -rf /tmp/lighthouse-local-test || true
+mkdir -p /tmp/lighthouse-local-test
+cd /tmp/lighthouse-local-test
+
+npm init -y
+npm install "$LH_PRISTINE_ROOT/lighthouse-$VERSION.tgz"
+npm explore lighthouse -- npm run smoke
+npm explore lighthouse -- npm run chrome # try the manual launcher
+npm explore lighthouse -- npm run fast -- http://example.com
+
+cd "$LH_PRISTINE_ROOT"
+rm -rf /tmp/lighthouse-local-test
+rm "lighthouse-$VERSION.tgz"
+
+echo "${TXT_BOLD}Now manually...${TXT_RESET}"
+echo "✅   Test the extension. Open chrome://extensions"
+echo "${TXT_DIM}Press any key to continue...${TXT_RESET}"
+read -n 1 -r unused_variable
+
+
+echo "✅   Test the viewer. Open http://localhost:8000"
+echo "    - Works with v4 report? https://gist.github.com/patrickhulce/7251f9eba409f385e4c0424515fe8009"
+echo "${TXT_DIM}Press any key to complete the test script...${TXT_RESET}"
+read -n 1 -r unused_variable
diff --git a/lighthouse-core/test/audits/bootup-time-test.js b/lighthouse-core/test/audits/bootup-time-test.js
index a91cb3be5..5365626bc 100644
--- a/lighthouse-core/test/audits/bootup-time-test.js
+++ b/lighthouse-core/test/audits/bootup-time-test.js
@@ -37,7 +37,7 @@ describe('Performance: bootup-time audit', () => {
       assert.deepEqual(roundedValueOf(output, 'https://www.google-analytics.com/analytics.js'), {scripting: 40.6, scriptParseCompile: 9.6, total: 53.4});
       assert.deepEqual(roundedValueOf(output, 'Other'), {scripting: 11.7, scriptParseCompile: 0, total: 1123.8}); // eslint-disable-line max-len
 
-      assert.equal(Math.round(output.rawValue), 225);
+      assert.equal(Math.round(output.numericValue), 225);
       assert.equal(output.details.items.length, 5);
       assert.equal(output.score, 1);
     });
@@ -58,7 +58,7 @@ describe('Performance: bootup-time audit', () => {
 
     assert.equal(output.details.items.length, 8);
     assert.equal(output.score, 0.98);
-    assert.equal(Math.round(output.rawValue), 720);
+    assert.equal(Math.round(output.numericValue), 720);
   });
 
   it('should get no data when no events are present', () => {
@@ -72,7 +72,7 @@ describe('Performance: bootup-time audit', () => {
       .then(output => {
         assert.equal(output.details.items.length, 0);
         assert.equal(output.score, 1);
-        assert.equal(Math.round(output.rawValue), 0);
+        assert.equal(Math.round(output.numericValue), 0);
       });
   });
 });
diff --git a/lighthouse-core/test/audits/byte-efficiency/byte-efficiency-audit-test.js b/lighthouse-core/test/audits/byte-efficiency/byte-efficiency-audit-test.js
index d000649f6..1dd1369af 100644
--- a/lighthouse-core/test/audits/byte-efficiency/byte-efficiency-audit-test.js
+++ b/lighthouse-core/test/audits/byte-efficiency/byte-efficiency-audit-test.js
@@ -88,7 +88,7 @@ describe('Byte efficiency base audit', () => {
     assert.deepEqual(result.details.items, []);
   });
 
-  it('should set the rawValue', () => {
+  it('should set the numericValue', () => {
     const result = ByteEfficiencyAudit.createAuditProduct(
       {
         headings: baseHeadings,
@@ -101,7 +101,7 @@ describe('Byte efficiency base audit', () => {
     );
 
     // 900ms savings comes from the graph calculation
-    assert.equal(result.rawValue, 900);
+    assert.equal(result.numericValue, 900);
   });
 
   it('should score the wastedMs', () => {
@@ -200,7 +200,7 @@ describe('Byte efficiency base audit', () => {
       simulator
     );
 
-    assert.equal(result.rawValue, 300);
+    assert.equal(result.numericValue, 300);
   });
 
   it('should create load simulator with the specified settings', async () => {
@@ -224,14 +224,14 @@ describe('Byte efficiency base audit', () => {
     let settings = {throttlingMethod: 'simulate', throttling: modestThrottling};
     let result = await MockAudit.audit(artifacts, {settings, computedCache});
     // expect modest savings
-    expect(result.rawValue).toBeLessThan(5000);
-    expect(result.rawValue).toMatchSnapshot();
+    expect(result.numericValue).toBeLessThan(5000);
+    expect(result.numericValue).toMatchSnapshot();
 
     settings = {throttlingMethod: 'simulate', throttling: ultraSlowThrottling};
     result = await MockAudit.audit(artifacts, {settings, computedCache});
     // expect lots of savings
-    expect(result.rawValue).not.toBeLessThan(5000);
-    expect(result.rawValue).toMatchSnapshot();
+    expect(result.numericValue).not.toBeLessThan(5000);
+    expect(result.numericValue).toMatchSnapshot();
   });
 
   it('should allow overriding of computeWasteWithTTIGraph', async () => {
@@ -262,7 +262,7 @@ describe('Byte efficiency base audit', () => {
     const result = await MockAudit.audit(artifacts, {settings, computedCache});
     const resultTti = await MockJustTTIAudit.audit(artifacts, {settings, computedCache});
     // expect less savings with just TTI
-    expect(resultTti.rawValue).toBeLessThan(result.rawValue);
-    expect({default: result.rawValue, justTTI: resultTti.rawValue}).toMatchSnapshot();
+    expect(resultTti.numericValue).toBeLessThan(result.numericValue);
+    expect({default: result.numericValue, justTTI: resultTti.numericValue}).toMatchSnapshot();
   });
 });
diff --git a/lighthouse-core/test/audits/byte-efficiency/render-blocking-resources-test.js b/lighthouse-core/test/audits/byte-efficiency/render-blocking-resources-test.js
index d0b256e7b..d1fd48b27 100644
--- a/lighthouse-core/test/audits/byte-efficiency/render-blocking-resources-test.js
+++ b/lighthouse-core/test/audits/byte-efficiency/render-blocking-resources-test.js
@@ -36,7 +36,7 @@ describe('Render blocking resources audit', () => {
     const computedCache = new Map();
     const result = await RenderBlockingResourcesAudit.audit(artifacts, {settings, computedCache});
     assert.equal(result.score, 1);
-    assert.equal(result.rawValue, 0);
+    assert.equal(result.numericValue, 0);
   });
 
   describe('#estimateSavingsWithGraphs', () => {
diff --git a/lighthouse-core/test/audits/byte-efficiency/total-byte-weight-test.js b/lighthouse-core/test/audits/byte-efficiency/total-byte-weight-test.js
index 90afd4e46..2fe5e7e91 100644
--- a/lighthouse-core/test/audits/byte-efficiency/total-byte-weight-test.js
+++ b/lighthouse-core/test/audits/byte-efficiency/total-byte-weight-test.js
@@ -47,7 +47,7 @@ describe('Total byte weight audit', () => {
     ]);
 
     return TotalByteWeight.audit(artifacts, {options, computedCache: new Map()}).then(result => {
-      assert.strictEqual(result.rawValue, 150 * 1024);
+      assert.strictEqual(result.numericValue, 150 * 1024);
       assert.strictEqual(result.score, 1);
       const results = result.details.items;
       assert.strictEqual(results.length, 3);
@@ -73,7 +73,7 @@ describe('Total byte weight audit', () => {
 
     return TotalByteWeight.audit(artifacts, {options, computedCache: new Map()}).then(result => {
       assert.ok(0.40 < result.score && result.score < 0.6, 'score is around 0.5');
-      assert.strictEqual(result.rawValue, 4180 * 1024);
+      assert.strictEqual(result.numericValue, 4180 * 1024);
       const results = result.details.items;
       assert.strictEqual(results.length, 10, 'results are clipped at top 10');
       assert.strictEqual(result.extendedInfo.value.totalCompletedRequests, 11);
@@ -88,7 +88,7 @@ describe('Total byte weight audit', () => {
     ]);
 
     return TotalByteWeight.audit(artifacts, {options, computedCache: new Map()}).then(result => {
-      assert.strictEqual(result.rawValue, 15000 * 1024);
+      assert.strictEqual(result.numericValue, 15000 * 1024);
       assert.strictEqual(result.score, 0);
     });
   });
diff --git a/lighthouse-core/test/audits/dobetterweb/dom-size-test.js b/lighthouse-core/test/audits/dobetterweb/dom-size-test.js
index 152a6eb41..509568c44 100644
--- a/lighthouse-core/test/audits/dobetterweb/dom-size-test.js
+++ b/lighthouse-core/test/audits/dobetterweb/dom-size-test.js
@@ -24,7 +24,7 @@ describe('DOMSize audit', () => {
   it('calculates score hitting mid distribution', () => {
     const auditResult = DOMSize.audit(artifact, {options});
     assert.equal(auditResult.score, 0.43);
-    assert.equal(auditResult.rawValue, numElements);
+    assert.equal(auditResult.numericValue, numElements);
     expect(auditResult.displayValue).toBeDisplayString('1,500 elements');
     assert.equal(auditResult.details.items[0].value, numElements.toLocaleString());
     assert.equal(auditResult.details.items[1].value, '1');
diff --git a/lighthouse-core/test/audits/errors-in-console-test.js b/lighthouse-core/test/audits/errors-in-console-test.js
index 6382a9d46..482d52a0d 100644
--- a/lighthouse-core/test/audits/errors-in-console-test.js
+++ b/lighthouse-core/test/audits/errors-in-console-test.js
@@ -16,7 +16,7 @@ describe('Console error logs audit', () => {
       ConsoleMessages: [],
       RuntimeExceptions: [],
     });
-    assert.equal(auditResult.rawValue, 0);
+    assert.equal(auditResult.numericValue, 0);
     assert.equal(auditResult.score, 1);
     assert.ok(!auditResult.displayValue, 0);
     assert.equal(auditResult.details.items.length, 0);
@@ -35,7 +35,7 @@ describe('Console error logs audit', () => {
       ],
       RuntimeExceptions: [],
     });
-    assert.equal(auditResult.rawValue, 0);
+    assert.equal(auditResult.numericValue, 0);
     assert.equal(auditResult.score, 1);
     assert.equal(auditResult.details.items.length, 0);
   });
@@ -81,7 +81,7 @@ describe('Console error logs audit', () => {
       }],
     });
 
-    assert.equal(auditResult.rawValue, 3);
+    assert.equal(auditResult.numericValue, 3);
     assert.equal(auditResult.score, 0);
     assert.equal(auditResult.details.items.length, 3);
     assert.equal(auditResult.details.items[0].url, 'http://www.example.com/favicon.ico');
@@ -107,7 +107,7 @@ describe('Console error logs audit', () => {
       ],
       RuntimeExceptions: [],
     });
-    assert.equal(auditResult.rawValue, 1);
+    assert.equal(auditResult.numericValue, 1);
     assert.equal(auditResult.score, 0);
     assert.equal(auditResult.details.items.length, 1);
     // url is undefined
@@ -138,7 +138,7 @@ describe('Console error logs audit', () => {
         },
       }],
     });
-    assert.equal(auditResult.rawValue, 1);
+    assert.equal(auditResult.numericValue, 1);
     assert.equal(auditResult.score, 0);
     assert.equal(auditResult.details.items.length, 1);
     assert.strictEqual(auditResult.details.items[0].url, 'http://example.com/fancybox.js');
diff --git a/lighthouse-core/test/audits/load-fast-enough-for-pwa-test.js b/lighthouse-core/test/audits/load-fast-enough-for-pwa-test.js
index 90d2eceab..4b2ac5211 100644
--- a/lighthouse-core/test/audits/load-fast-enough-for-pwa-test.js
+++ b/lighthouse-core/test/audits/load-fast-enough-for-pwa-test.js
@@ -26,7 +26,7 @@ describe('PWA: load-fast-enough-for-pwa audit', () => {
     const settings = {throttlingMethod: 'devtools', throttling: mobileSlow4GThrottling};
     return FastPWAAudit.audit(artifacts, {settings, computedCache: new Map()}).then(result => {
       assert.equal(result.score, true, 'fixture trace is not passing audit');
-      assert.equal(result.rawValue, 1582.189);
+      assert.equal(result.numericValue, 1582.189);
     });
   });
 
@@ -50,7 +50,7 @@ describe('PWA: load-fast-enough-for-pwa audit', () => {
     const settings = {throttlingMethod: 'devtools', throttling: mobileSlow4GThrottling};
     return FastPWAAudit.audit(artifacts, {settings, computedCache: new Map()}).then(result => {
       assert.equal(result.score, false, 'not failing a long TTI value');
-      assert.equal(result.rawValue, 15000);
+      assert.equal(result.numericValue, 15000);
       expect(result.displayValue).toBeDisplayString('Interactive at 15.0\xa0s');
       assert.ok(result.explanation);
     });
@@ -64,7 +64,7 @@ describe('PWA: load-fast-enough-for-pwa audit', () => {
 
     const settings = {throttlingMethod: 'devtools', throttling: mobileSlow4GThrottling};
     const result = await FastPWAAudit.audit(artifacts, {settings, computedCache: new Map()});
-    assert.equal(Math.round(result.rawValue), 1582);
+    assert.equal(Math.round(result.numericValue), 1582);
   });
 
   it('overrides with simulated result when throttling is modified', async () => {
@@ -75,8 +75,8 @@ describe('PWA: load-fast-enough-for-pwa audit', () => {
 
     const settings = {throttlingMethod: 'provided', throttling: {rttMs: 40, throughput: 100000}};
     const result = await FastPWAAudit.audit(artifacts, {settings, computedCache: new Map()});
-    expect(result.rawValue).toBeGreaterThan(2000); // If not overridden this would be 1582
-    expect(Math.round(result.rawValue)).toMatchSnapshot();
+    expect(result.numericValue).toBeGreaterThan(2000); // If not overridden this would be 1582
+    expect(Math.round(result.numericValue)).toMatchSnapshot();
   });
 
   it('overrides when throttling is modified but method is not "provided"', async () => {
@@ -87,7 +87,7 @@ describe('PWA: load-fast-enough-for-pwa audit', () => {
 
     const settings = {throttlingMethod: 'devtools', throttling: {rttMs: 40, throughput: 100000}};
     const result = await FastPWAAudit.audit(artifacts, {settings, computedCache: new Map()});
-    expect(result.rawValue).toBeGreaterThan(2000); // If not overridden this would be 1582
+    expect(result.numericValue).toBeGreaterThan(2000); // If not overridden this would be 1582
   });
 
   it('overrides when throttling is "provided" and fails the simulated TTI value', async () => {
@@ -110,6 +110,6 @@ describe('PWA: load-fast-enough-for-pwa audit', () => {
     const result = await FastPWAAudit.audit(artifacts, {settings, computedCache: new Map()});
     expect(result.displayValue)
       .toBeDisplayString('Interactive on simulated mobile network at 24.9\xa0s');
-    expect(result.rawValue).toBeGreaterThan(10000);
+    expect(result.numericValue).toBeGreaterThan(10000);
   });
 });
diff --git a/lighthouse-core/test/audits/mainthread-work-breakdown-test.js b/lighthouse-core/test/audits/mainthread-work-breakdown-test.js
index 4710b4ac1..e3f5e473d 100644
--- a/lighthouse-core/test/audits/mainthread-work-breakdown-test.js
+++ b/lighthouse-core/test/audits/mainthread-work-breakdown-test.js
@@ -56,7 +56,7 @@ describe('Performance: page execution timings audit', () => {
 
     const output = await PageExecutionTimings.audit(artifacts, {options, computedCache: new Map()});
     assert.deepStrictEqual(keyOutput(output), acceptableTraceExpectations);
-    assert.equal(Math.round(output.rawValue), 1360);
+    assert.equal(Math.round(output.numericValue), 1360);
     assert.equal(output.details.items.length, 7);
     assert.equal(output.score, 0.98);
   });
@@ -75,7 +75,7 @@ describe('Performance: page execution timings audit', () => {
       assert.ok(Math.abs(actual - expected) <= 2, `expected ${expected} got ${actual}`);
     }
 
-    assert.equal(Math.round(output.rawValue), 4081);
+    assert.equal(Math.round(output.numericValue), 4081);
     assert.equal(output.details.items.length, 7);
     assert.equal(output.score, 0.49);
   });
@@ -85,7 +85,7 @@ describe('Performance: page execution timings audit', () => {
 
     const output = await PageExecutionTimings.audit(artifacts, {options, computedCache: new Map()});
     assert.deepStrictEqual(keyOutput(output), siteWithRedirectTraceExpectations);
-    assert.equal(Math.round(output.rawValue), 784);
+    assert.equal(Math.round(output.numericValue), 784);
     assert.equal(output.details.items.length, 7);
     assert.equal(output.score, 1);
   });
@@ -95,7 +95,7 @@ describe('Performance: page execution timings audit', () => {
 
     const output = await PageExecutionTimings.audit(artifacts, {options, computedCache: new Map()});
     assert.deepStrictEqual(keyOutput(output), loadTraceExpectations);
-    assert.equal(Math.round(output.rawValue), 933);
+    assert.equal(Math.round(output.numericValue), 933);
     assert.equal(output.details.items.length, 6);
     assert.equal(output.score, 1);
   });
@@ -107,7 +107,7 @@ describe('Performance: page execution timings audit', () => {
     return PageExecutionTimings.audit(artifacts, context).then(output => {
       assert.equal(output.details.items.length, 0);
       assert.equal(output.score, 1);
-      assert.equal(Math.round(output.rawValue), 0);
+      assert.equal(Math.round(output.numericValue), 0);
     });
   });
 });
diff --git a/lighthouse-core/test/audits/metrics/__snapshots__/first-meaningful-paint-test.js.snap b/lighthouse-core/test/audits/metrics/__snapshots__/first-meaningful-paint-test.js.snap
index 4ca3dd6f7..ab837653f 100644
--- a/lighthouse-core/test/audits/metrics/__snapshots__/first-meaningful-paint-test.js.snap
+++ b/lighthouse-core/test/audits/metrics/__snapshots__/first-meaningful-paint-test.js.snap
@@ -2,7 +2,7 @@
 
 exports[`Performance: first-meaningful-paint audit computes FMP correctly for simulated 1`] = `
 Object {
-  "rawValue": 1510.6100000208241,
+  "numericValue": 1510.6100000208241,
   "score": 0.99,
 }
 `;
diff --git a/lighthouse-core/test/audits/metrics/estimated-input-latency-test.js b/lighthouse-core/test/audits/metrics/estimated-input-latency-test.js
index f52772738..d51d9e043 100644
--- a/lighthouse-core/test/audits/metrics/estimated-input-latency-test.js
+++ b/lighthouse-core/test/audits/metrics/estimated-input-latency-test.js
@@ -25,7 +25,7 @@ describe('Performance: estimated-input-latency audit', () => {
     const settings = {throttlingMethod: 'provided'};
     const context = {options, settings, computedCache: new Map()};
     return Audit.audit(artifacts, context).then(output => {
-      assert.equal(Math.round(output.rawValue * 10) / 10, 17.1);
+      assert.equal(Math.round(output.numericValue * 10) / 10, 17.1);
       assert.equal(output.score, 1);
       expect(output.displayValue).toBeDisplayString('20\xa0ms');
     });
diff --git a/lighthouse-core/test/audits/metrics/first-contentful-paint-3g-test.js b/lighthouse-core/test/audits/metrics/first-contentful-paint-3g-test.js
index b231a304d..120d29287 100644
--- a/lighthouse-core/test/audits/metrics/first-contentful-paint-3g-test.js
+++ b/lighthouse-core/test/audits/metrics/first-contentful-paint-3g-test.js
@@ -26,7 +26,7 @@ describe('Performance: first-contentful-paint-3g audit', () => {
 
     const result = await FCP3G.audit(artifacts, {settings: {}, options, computedCache: new Map()});
     // Use InlineSnapshot here so changes to Lantern coefficients can be easily updated en masse
-    expect({score: result.score, value: Math.round(result.rawValue)}).toMatchInlineSnapshot(`
+    expect({score: result.score, value: Math.round(result.numericValue)}).toMatchInlineSnapshot(`
 Object {
   "score": 0.99,
   "value": 2057,
diff --git a/lighthouse-core/test/audits/metrics/first-contentful-paint-test.js b/lighthouse-core/test/audits/metrics/first-contentful-paint-test.js
index ee3ee168c..7be3496bb 100644
--- a/lighthouse-core/test/audits/metrics/first-contentful-paint-test.js
+++ b/lighthouse-core/test/audits/metrics/first-contentful-paint-test.js
@@ -27,6 +27,6 @@ describe('Performance: first-contentful-paint audit', () => {
     const settings = {throttlingMethod: 'provided'};
     const result = await Audit.audit(artifacts, {settings, options, computedCache: new Map()});
     assert.equal(result.score, 1);
-    assert.equal(result.rawValue, 498.87);
+    assert.equal(result.numericValue, 498.87);
   });
 });
diff --git a/lighthouse-core/test/audits/metrics/first-meaningful-paint-test.js b/lighthouse-core/test/audits/metrics/first-meaningful-paint-test.js
index b3862b65c..61a416e72 100644
--- a/lighthouse-core/test/audits/metrics/first-meaningful-paint-test.js
+++ b/lighthouse-core/test/audits/metrics/first-meaningful-paint-test.js
@@ -23,7 +23,7 @@ describe('Performance: first-meaningful-paint audit', () => {
     const fmpResult = await FMPAudit.audit(artifacts, context);
 
     assert.equal(fmpResult.score, 1);
-    assert.equal(fmpResult.rawValue, 783.328);
+    assert.equal(fmpResult.numericValue, 783.328);
     expect(fmpResult.displayValue).toBeDisplayString('0.8\xa0s');
   });
 
@@ -37,7 +37,7 @@ describe('Performance: first-meaningful-paint audit', () => {
 
     expect({
       score: fmpResult.score,
-      rawValue: fmpResult.rawValue,
+      numericValue: fmpResult.numericValue,
     }).toMatchSnapshot();
   });
 });
diff --git a/lighthouse-core/test/audits/metrics/interactive-test.js b/lighthouse-core/test/audits/metrics/interactive-test.js
index 20fcef213..40efb636e 100644
--- a/lighthouse-core/test/audits/metrics/interactive-test.js
+++ b/lighthouse-core/test/audits/metrics/interactive-test.js
@@ -32,7 +32,7 @@ describe('Performance: interactive audit', () => {
     const context = {options, settings: {throttlingMethod: 'provided'}, computedCache: new Map()};
     return Interactive.audit(artifacts, context).then(output => {
       assert.equal(output.score, 1);
-      assert.equal(Math.round(output.rawValue), 1582);
+      assert.equal(Math.round(output.numericValue), 1582);
       expect(output.displayValue).toBeDisplayString('1.6\xa0s');
     });
   });
@@ -50,7 +50,7 @@ describe('Performance: interactive audit', () => {
     const context = {options, settings: {throttlingMethod: 'provided'}, computedCache: new Map()};
     return Interactive.audit(artifacts, context).then(output => {
       assert.equal(output.score, 0.97);
-      assert.equal(Math.round(output.rawValue), 2712);
+      assert.equal(Math.round(output.numericValue), 2712);
       expect(output.displayValue).toBeDisplayString('2.7\xa0s');
     });
   });
diff --git a/lighthouse-core/test/audits/metrics/speed-index-test.js b/lighthouse-core/test/audits/metrics/speed-index-test.js
index 199485036..48d6e7a91 100644
--- a/lighthouse-core/test/audits/metrics/speed-index-test.js
+++ b/lighthouse-core/test/audits/metrics/speed-index-test.js
@@ -24,7 +24,7 @@ describe('Performance: speed-index audit', () => {
     const settings = {throttlingMethod: 'provided'};
     return Audit.audit(artifacts, {options, settings, computedCache: new Map()}).then(result => {
       assert.equal(result.score, 1);
-      assert.equal(result.rawValue, 605);
+      assert.equal(result.numericValue, 605);
     });
   }, 10000);
 });
diff --git a/lighthouse-core/test/audits/network-requests-test.js b/lighthouse-core/test/audits/network-requests-test.js
index 6703d78e7..da967a2f5 100644
--- a/lighthouse-core/test/audits/network-requests-test.js
+++ b/lighthouse-core/test/audits/network-requests-test.js
@@ -22,7 +22,7 @@ describe('Network requests audit', () => {
 
     return NetworkRequests.audit(artifacts, {computedCache: new Map()}).then(output => {
       assert.equal(output.score, 1);
-      assert.equal(output.rawValue, 66);
+      assert.equal(output.numericValue, 66);
       assert.equal(output.details.items.length, 66);
       assert.equal(output.details.items[0].url, 'https://pwa.rocks/');
       assert.equal(output.details.items[0].startTime, 0);
diff --git a/lighthouse-core/test/audits/redirects-test.js b/lighthouse-core/test/audits/redirects-test.js
index 1bc6ded0e..94dfec03e 100644
--- a/lighthouse-core/test/audits/redirects-test.js
+++ b/lighthouse-core/test/audits/redirects-test.js
@@ -97,7 +97,7 @@ describe('Performance: Redirects audit', () => {
     return RedirectsAudit.audit(artifacts, context).then(output => {
       assert.equal(Math.round(output.score * 100) / 100, 0.37);
       assert.equal(output.details.items.length, 4);
-      assert.equal(output.rawValue, 1890);
+      assert.equal(output.numericValue, 1890);
     });
   });
 
@@ -107,7 +107,7 @@ describe('Performance: Redirects audit', () => {
     return RedirectsAudit.audit(artifacts, context).then(output => {
       assert.equal(Math.round(output.score * 100) / 100, 0.46);
       assert.equal(output.details.items.length, 3);
-      assert.equal(Math.round(output.rawValue), 1110);
+      assert.equal(Math.round(output.numericValue), 1110);
     });
   });
 
@@ -119,7 +119,7 @@ describe('Performance: Redirects audit', () => {
       assert.equal(output.score, 1);
       // We will still generate a table and show wasted time
       assert.equal(output.details.items.length, 2);
-      assert.equal(Math.round(output.rawValue), 780);
+      assert.equal(Math.round(output.numericValue), 780);
     });
   });
 
@@ -129,7 +129,7 @@ describe('Performance: Redirects audit', () => {
     return RedirectsAudit.audit(artifacts, context).then(output => {
       assert.equal(output.score, 1);
       assert.equal(output.details.items.length, 0);
-      assert.equal(output.rawValue, 0);
+      assert.equal(output.numericValue, 0);
     });
   });
 });
diff --git a/lighthouse-core/test/audits/time-to-first-byte-test.js b/lighthouse-core/test/audits/time-to-first-byte-test.js
index c0c8abd73..97b50ac17 100644
--- a/lighthouse-core/test/audits/time-to-first-byte-test.js
+++ b/lighthouse-core/test/audits/time-to-first-byte-test.js
@@ -25,7 +25,7 @@ describe('Performance: time-to-first-byte audit', () => {
     };
 
     return TimeToFirstByte.audit(artifacts, {computedCache: new Map()}).then(result => {
-      assert.strictEqual(result.rawValue, 630);
+      assert.strictEqual(result.numericValue, 630);
       assert.strictEqual(result.score, 0);
     });
   });
@@ -44,7 +44,7 @@ describe('Performance: time-to-first-byte audit', () => {
     };
 
     return TimeToFirstByte.audit(artifacts, {computedCache: new Map()}).then(result => {
-      assert.strictEqual(result.rawValue, 200);
+      assert.strictEqual(result.numericValue, 200);
       assert.strictEqual(result.score, 1);
     });
   });
diff --git a/lighthouse-core/test/audits/uses-rel-preconnect-test.js b/lighthouse-core/test/audits/uses-rel-preconnect-test.js
index 957e52508..07322ce44 100644
--- a/lighthouse-core/test/audits/uses-rel-preconnect-test.js
+++ b/lighthouse-core/test/audits/uses-rel-preconnect-test.js
@@ -36,9 +36,9 @@ describe('Performance: uses-rel-preconnect audit', () => {
     };
 
     const context = {settings: {}, computedCache: new Map()};
-    const {score, rawValue, details} = await UsesRelPreconnect.audit(artifacts, context);
+    const {score, numericValue, details} = await UsesRelPreconnect.audit(artifacts, context);
     assert.equal(score, 1);
-    assert.equal(rawValue, 0);
+    assert.equal(numericValue, 0);
     assert.equal(details.items.length, 0);
   });
 
@@ -61,9 +61,9 @@ describe('Performance: uses-rel-preconnect audit', () => {
     };
 
     const context = {settings: {}, computedCache: new Map()};
-    const {score, rawValue, details} = await UsesRelPreconnect.audit(artifacts, context);
+    const {score, numericValue, details} = await UsesRelPreconnect.audit(artifacts, context);
     assert.equal(score, 1);
-    assert.equal(rawValue, 0);
+    assert.equal(numericValue, 0);
     assert.equal(details.items.length, 0);
   });
 
@@ -83,9 +83,9 @@ describe('Performance: uses-rel-preconnect audit', () => {
     };
 
     const context = {settings: {}, computedCache: new Map()};
-    const {score, rawValue, details} = await UsesRelPreconnect.audit(artifacts, context);
+    const {score, numericValue, details} = await UsesRelPreconnect.audit(artifacts, context);
     assert.equal(score, 1);
-    assert.equal(rawValue, 0);
+    assert.equal(numericValue, 0);
     assert.equal(details.items.length, 0);
   });
 
@@ -111,9 +111,9 @@ describe('Performance: uses-rel-preconnect audit', () => {
     };
 
     const context = {settings: {}, computedCache: new Map()};
-    const {score, rawValue, details} = await UsesRelPreconnect.audit(artifacts, context);
+    const {score, numericValue, details} = await UsesRelPreconnect.audit(artifacts, context);
     assert.equal(score, 1);
-    assert.equal(rawValue, 0);
+    assert.equal(numericValue, 0);
     assert.equal(details.items.length, 0);
   });
 
@@ -134,9 +134,9 @@ describe('Performance: uses-rel-preconnect audit', () => {
     };
 
     const context = {settings: {}, computedCache: new Map()};
-    const {score, rawValue, details} = await UsesRelPreconnect.audit(artifacts, context);
+    const {score, numericValue, details} = await UsesRelPreconnect.audit(artifacts, context);
     assert.equal(score, 1);
-    assert.equal(rawValue, 0);
+    assert.equal(numericValue, 0);
     assert.equal(details.items.length, 0);
   });
 
@@ -201,8 +201,8 @@ describe('Performance: uses-rel-preconnect audit', () => {
     };
 
     const context = {settings: {}, computedCache: new Map()};
-    const {rawValue, extendedInfo} = await UsesRelPreconnect.audit(artifacts, context);
-    assert.equal(rawValue, 300);
+    const {numericValue, extendedInfo} = await UsesRelPreconnect.audit(artifacts, context);
+    assert.equal(numericValue, 300);
     assert.equal(extendedInfo.value.length, 1);
     assert.deepStrictEqual(extendedInfo.value, [
       {url: 'https://cdn.example.com', wastedMs: 300},
@@ -242,8 +242,8 @@ describe('Performance: uses-rel-preconnect audit', () => {
     };
 
     const context = {settings: {}, computedCache: new Map()};
-    const {rawValue, extendedInfo} = await UsesRelPreconnect.audit(artifacts, context);
-    assert.equal(rawValue, 300);
+    const {numericValue, extendedInfo} = await UsesRelPreconnect.audit(artifacts, context);
+    assert.equal(numericValue, 300);
     assert.equal(extendedInfo.value.length, 2);
     assert.deepStrictEqual(extendedInfo.value, [
       {url: 'https://othercdn.example.com', wastedMs: 300},
diff --git a/lighthouse-core/test/audits/uses-rel-preload-test.js b/lighthouse-core/test/audits/uses-rel-preload-test.js
index 6765a7370..c97fb5a78 100644
--- a/lighthouse-core/test/audits/uses-rel-preload-test.js
+++ b/lighthouse-core/test/audits/uses-rel-preload-test.js
@@ -205,7 +205,7 @@ describe('Performance: uses-rel-preload audit', () => {
     const artifacts = mockArtifacts(networkRecords, defaultMainResourceUrl);
     const context = {settings: {}, computedCache: new Map()};
     return UsesRelPreload.audit(artifacts, context).then(output => {
-      assert.equal(output.rawValue, 0);
+      assert.equal(output.numericValue, 0);
       assert.equal(output.details.items.length, 0);
     });
   });
@@ -217,7 +217,7 @@ describe('Performance: uses-rel-preload audit', () => {
     const artifacts = mockArtifacts(networkRecords, defaultMainResourceUrl);
     const context = {settings: {}, computedCache: new Map()};
     return UsesRelPreload.audit(artifacts, context).then(output => {
-      assert.equal(output.rawValue, 0);
+      assert.equal(output.numericValue, 0);
       assert.equal(output.details.items.length, 0);
     });
   });
@@ -237,6 +237,6 @@ describe('Performance: uses-rel-preload audit', () => {
     const context = {settings, computedCache: new Map()};
     const result = await UsesRelPreload.audit(artifacts, context);
     assert.equal(result.score, 1);
-    assert.equal(result.rawValue, 0);
+    assert.equal(result.numericValue, 0);
   });
 });
diff --git a/lighthouse-core/test/gather/driver-test.js b/lighthouse-core/test/gather/driver-test.js
index e3afa14fa..cdc961b2a 100644
--- a/lighthouse-core/test/gather/driver-test.js
+++ b/lighthouse-core/test/gather/driver-test.js
@@ -772,7 +772,8 @@ describe('.gotoURL', () => {
       } catch (err) {
         expect(err).toHaveProperty('code', 'INSECURE_DOCUMENT_REQUEST');
         expect(err.friendlyMessage).toBeDisplayString(
-          'The URL you have provided does not have valid security credentials. reason 1. reason 2.'
+          'The URL you have provided does not have a valid security certificate. ' +
+          'reason 1. reason 2.'
         );
       }
     });
diff --git a/lighthouse-core/test/report/html/renderer/report-renderer-test.js b/lighthouse-core/test/report/html/renderer/report-renderer-test.js
index 0121fd116..155fd19d3 100644
--- a/lighthouse-core/test/report/html/renderer/report-renderer-test.js
+++ b/lighthouse-core/test/report/html/renderer/report-renderer-test.js
@@ -79,8 +79,9 @@ describe('ReportRenderer', () => {
       const output = renderer.renderReport(sampleResults, container);
       assert.ok(output.querySelector('.lh-header-sticky'), 'has a header');
       assert.ok(output.querySelector('.lh-report'), 'has report body');
+      // 3 sets of gauges - one in sticky header, one in scores header, and one in each section.
       assert.equal(output.querySelectorAll('.lh-gauge__wrapper, .lh-gauge--pwa__wrapper').length,
-          sampleResults.reportCategories.length * 2, 'renders category gauges');
+          sampleResults.reportCategories.length * 3, 'renders category gauges');
       // no fireworks
       assert.ok(output.querySelector('.score100') === null, 'has no fireworks treatment');
     });
@@ -104,21 +105,39 @@ describe('ReportRenderer', () => {
       assert.ok(header.querySelector('.lh-scores-container'), 'contains score container');
     });
 
-    it('renders special score gauges after the mainstream ones', () => {
+    it('renders score gauges in this order: default, pwa, plugins', () => {
+      const sampleResultsCopy = JSON.parse(JSON.stringify(sampleResults));
+      sampleResultsCopy.categories['lighthouse-plugin-someplugin'] = {
+        id: 'lighthouse-plugin-someplugin',
+        title: 'Some Plugin',
+        auditRefs: [],
+      };
+
       const container = renderer._dom._document.body;
-      const output = renderer.renderReport(sampleResults, container);
+      const output = renderer.renderReport(sampleResultsCopy, container);
 
-      const indexOfFirstIrregularGauge = Array.from(output
+      const indexOfPwaGauge = Array.from(output
         .querySelectorAll('.lh-scores-header > a[class*="lh-gauge"]')).findIndex(el => {
         return el.matches('.lh-gauge--pwa__wrapper');
       });
 
+      const indexOfPluginGauge = Array.from(output
+        .querySelectorAll('.lh-scores-header > a[class*="lh-gauge"]')).findIndex(el => {
+        return el.matches('.lh-gauge__wrapper--plugin');
+      });
+
       const scoresHeaderElem = output.querySelector('.lh-scores-header');
+      assert.equal(scoresHeaderElem.children.length - 2, indexOfPwaGauge);
+      assert.equal(scoresHeaderElem.children.length - 1, indexOfPluginGauge);
+      assert(indexOfPluginGauge > indexOfPwaGauge);
+
       for (let i = 0; i < scoresHeaderElem.children.length; i++) {
         const gauge = scoresHeaderElem.children[i];
 
         assert.ok(gauge.classList.contains('lh-gauge__wrapper'));
-        if (i >= indexOfFirstIrregularGauge) {
+        if (i >= indexOfPluginGauge) {
+          assert.ok(gauge.classList.contains('lh-gauge__wrapper--plugin'));
+        } else if (i >= indexOfPwaGauge) {
           assert.ok(gauge.classList.contains('lh-gauge--pwa__wrapper'));
         }
       }
diff --git a/lighthouse-core/test/report/html/renderer/report-ui-features-test.js b/lighthouse-core/test/report/html/renderer/report-ui-features-test.js
index b9d4b8d40..4db93baeb 100644
--- a/lighthouse-core/test/report/html/renderer/report-ui-features-test.js
+++ b/lighthouse-core/test/report/html/renderer/report-ui-features-test.js
@@ -60,7 +60,7 @@ describe('ReportUIFeatures', () => {
       };
     };
 
-    global.window = {};
+    global.window = document.window;
     global.window.getComputedStyle = function() {
       return {
         marginTop: '10px',
diff --git a/lighthouse-core/test/results/sample_v2.json b/lighthouse-core/test/results/sample_v2.json
index a7548037f..fe569c181 100644
--- a/lighthouse-core/test/results/sample_v2.json
+++ b/lighthouse-core/test/results/sample_v2.json
@@ -791,7 +791,6 @@
       "description": "Collection of useful page vitals.",
       "score": null,
       "scoreDisplayMode": "informative",
-      "numericValue": 1,
       "details": {
         "type": "debugdata",
         "items": [