Repository: GoogleChrome/lighthouse
Pull request #8307 (2019-04-16 04:31:03)

report: plugin badge for plugin score gauges
#8185

![image](https://user-images.githubusercontent.com/6392995/56184148-fed7ca00-5fcd-11e9-8942-28083fb81072.png)

Hoten: @exterkamp made the badges more responsive
googlebot: We found a Contributor License Agreement for you (the sender of this pull request), but were unable to find agreements for all the commit author(s) or Co-authors.  If you authored these, maybe you used a different email address in the git commits than was used to sign the CLA ([login here](https://cla.developers.google.com/) to double check)?  If these were authored by someone else, then they will need to sign a CLA as well, and confirm that they're okay with these being contributed to Google.
In order to pass this check, please resolve this problem and have the pull request author add another comment and the bot will run again. If the bot doesn't comment, it means it doesn't think anything has changed.

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

<!-- need_author_cla -->

Patch:
diff --git a/clients/lightrider/lightrider-entry.js b/clients/lightrider/lightrider-entry.js
index d6b86d107..3d33c4749 100644
--- a/clients/lightrider/lightrider-entry.js
+++ b/clients/lightrider/lightrider-entry.js
@@ -26,11 +26,11 @@ const LR_PRESETS = {
  * @param {Connection} connection
  * @param {string} url
  * @param {LH.Flags} flags Lighthouse flags, including `output`
- * @param {{lrDevice?: 'desktop'|'mobile', categoryIDs?: Array<string>, logAssets: boolean, keepRawValues: boolean, configOverride?: LH.Config.Json}} lrOpts Options coming from Lightrider
+ * @param {{lrDevice?: 'desktop'|'mobile', categoryIDs?: Array<string>, logAssets: boolean, configOverride?: LH.Config.Json}} lrOpts Options coming from Lightrider
  * @return {Promise<string|Array<string>|void>}
  */
 async function runLighthouseInLR(connection, url, flags, lrOpts) {
-  const {lrDevice, categoryIDs, logAssets, keepRawValues, configOverride} = lrOpts;
+  const {lrDevice, categoryIDs, logAssets, configOverride} = lrOpts;
 
   // Certain fixes need to kick in under LR, see https://github.com/GoogleChrome/lighthouse/issues/5839
   global.isLightrider = true;
@@ -61,7 +61,7 @@ async function runLighthouseInLR(connection, url, flags, lrOpts) {
 
     // pre process the LHR for proto
     if (flags.output === 'json' && typeof results.report === 'string') {
-      return preprocessor.processForProto(results.report, {keepRawValues});
+      return preprocessor.processForProto(results.report);
     }
 
     return results.report;
diff --git a/docs/recipes/gulp/gulpfile.js b/docs/recipes/gulp/gulpfile.js
index 265e7cf9b..6cdeb8f46 100644
--- a/docs/recipes/gulp/gulpfile.js
+++ b/docs/recipes/gulp/gulpfile.js
@@ -50,7 +50,7 @@ const handleOk = function(results) {
   console.log(results); // eslint-disable-line no-console
   // TODO: use lighthouse results for checking your performance expectations.
   // e.g. process.exit(1) or throw Error if score falls below a certain threshold.
-  // if (results.audits['first-meaningful-paint'].rawValue > 3000) {
+  // if (results.audits['first-meaningful-paint'].numericValue > 3000) {
   //   console.log(`Warning: Time to first meaningful paint ${results.audits['first-meaningful-paint'].displayValue}`);
   //   process.exit(1);
   // }
diff --git a/docs/understanding-results.md b/docs/understanding-results.md
index c90e0c6d9..a98ec06f7 100644
--- a/docs/understanding-results.md
+++ b/docs/understanding-results.md
@@ -55,7 +55,7 @@ An object containing the results of the audits, keyed by their name.
 | explanation | <code>string&#124;undefined</code> | A string indicating the reason for audit failure. |
 | warnings | <code>string[]&#124;undefined</code> | Messages identifying potentially invalid cases |
 | errorMessage | <code>string&#124;undefined</code> | A message set |
-| rawValue | <code>boolean&#124;number</code> | The unscored value determined by the audit. Typically this will match the score if there's no additional information to impart. For performance audits, this value is typically a number indicating the metric value. |
+| numericValue | <code>number&#124;undefined</code> | The unscored value determined by the audit. Typically this will match the score if there's no additional information to impart. For performance audits, this value is typically a number indicating the metric value. |
 | displayValue | `string` | The string to display in the report alongside audit results. If empty, nothing additional is shown. This is typically used to explain additional information such as the number and nature of failing items. |
 | score | <code>number</code> | The scored value determined by the audit as a number `0-1`, representing displayed scores of 0-100. |
 | scoreDisplayMode | <code>"binary"&#124;"numeric"&#124;"error"&#124;"manual"&#124;"notApplicable"&#124;"informative"</code> | A string identifying how the score should be interpreted for display i.e. is the audit pass/fail (score of 1 or 0), did it fail, should it be ignored, or are there shades of gray (scores between 0-1 inclusive). |
diff --git a/lighthouse-core/audits/audit.js b/lighthouse-core/audits/audit.js
index c7184b624..3c9230a44 100644
--- a/lighthouse-core/audits/audit.js
+++ b/lighthouse-core/audits/audit.js
@@ -270,7 +270,7 @@ class Audit {
 
       score,
       scoreDisplayMode,
-      rawValue: product.rawValue,
+      numericValue: product.rawValue,
 
       displayValue: product.displayValue,
       explanation: product.explanation,
diff --git a/lighthouse-core/audits/deprecations.js b/lighthouse-core/audits/deprecations.js
index acea4fd00..d7890c4b4 100644
--- a/lighthouse-core/audits/deprecations.js
+++ b/lighthouse-core/audits/deprecations.js
@@ -25,7 +25,7 @@ class Deprecations extends Audit {
       failureTitle: 'Uses deprecated APIs',
       description: 'Deprecated APIs will eventually be removed from the browser. ' +
           '[Learn more](https://www.chromestatus.com/features#deprecated).',
-      requiredArtifacts: ['ChromeConsoleMessages'],
+      requiredArtifacts: ['ConsoleMessages'],
     };
   }
 
@@ -34,7 +34,7 @@ class Deprecations extends Audit {
    * @return {LH.Audit.Product}
    */
   static audit(artifacts) {
-    const entries = artifacts.ChromeConsoleMessages;
+    const entries = artifacts.ConsoleMessages;
 
     const deprecations = entries.filter(log => log.entry.source === 'deprecation').map(log => {
       return {
diff --git a/lighthouse-core/audits/dobetterweb/geolocation-on-start.js b/lighthouse-core/audits/dobetterweb/geolocation-on-start.js
index a5cdfa328..bd2cfc312 100644
--- a/lighthouse-core/audits/dobetterweb/geolocation-on-start.js
+++ b/lighthouse-core/audits/dobetterweb/geolocation-on-start.js
@@ -25,7 +25,7 @@ class GeolocationOnStart extends ViolationAudit {
       description: 'Users are mistrustful of or confused by sites that request their ' +
           'location without context. Consider tying the request to user gestures instead. ' +
           '[Learn more](https://developers.google.com/web/tools/lighthouse/audits/geolocation-on-load).',
-      requiredArtifacts: ['ChromeConsoleMessages'],
+      requiredArtifacts: ['ConsoleMessages'],
     };
   }
 
diff --git a/lighthouse-core/audits/dobetterweb/no-document-write.js b/lighthouse-core/audits/dobetterweb/no-document-write.js
index e5c42eeed..61edf1a72 100644
--- a/lighthouse-core/audits/dobetterweb/no-document-write.js
+++ b/lighthouse-core/audits/dobetterweb/no-document-write.js
@@ -24,7 +24,7 @@ class NoDocWriteAudit extends ViolationAudit {
       description: 'For users on slow connections, external scripts dynamically injected via ' +
           '`document.write()` can delay page load by tens of seconds. ' +
           '[Learn more](https://developers.google.com/web/tools/lighthouse/audits/document-write).',
-      requiredArtifacts: ['ChromeConsoleMessages'],
+      requiredArtifacts: ['ConsoleMessages'],
     };
   }
 
diff --git a/lighthouse-core/audits/dobetterweb/notification-on-start.js b/lighthouse-core/audits/dobetterweb/notification-on-start.js
index 92e243470..d928c7007 100644
--- a/lighthouse-core/audits/dobetterweb/notification-on-start.js
+++ b/lighthouse-core/audits/dobetterweb/notification-on-start.js
@@ -25,7 +25,7 @@ class NotificationOnStart extends ViolationAudit {
       description: 'Users are mistrustful of or confused by sites that request to send ' +
           'notifications without context. Consider tying the request to user gestures ' +
           'instead. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/notifications-on-load).',
-      requiredArtifacts: ['ChromeConsoleMessages'],
+      requiredArtifacts: ['ConsoleMessages'],
     };
   }
 
diff --git a/lighthouse-core/audits/dobetterweb/uses-passive-event-listeners.js b/lighthouse-core/audits/dobetterweb/uses-passive-event-listeners.js
index 2bf4e5be0..036dd798a 100644
--- a/lighthouse-core/audits/dobetterweb/uses-passive-event-listeners.js
+++ b/lighthouse-core/audits/dobetterweb/uses-passive-event-listeners.js
@@ -25,7 +25,7 @@ class PassiveEventsAudit extends ViolationAudit {
       description: 'Consider marking your touch and wheel event listeners as `passive` ' +
           'to improve your page\'s scroll performance. ' +
           '[Learn more](https://developers.google.com/web/tools/lighthouse/audits/passive-event-listeners).',
-      requiredArtifacts: ['ChromeConsoleMessages'],
+      requiredArtifacts: ['ConsoleMessages'],
     };
   }
 
diff --git a/lighthouse-core/audits/errors-in-console.js b/lighthouse-core/audits/errors-in-console.js
index 74195a8e6..1df3399a4 100644
--- a/lighthouse-core/audits/errors-in-console.js
+++ b/lighthouse-core/audits/errors-in-console.js
@@ -23,7 +23,7 @@ class ErrorLogs extends Audit {
       description: 'Errors logged to the console indicate unresolved problems. ' +
         'They can come from network request failures and other browser concerns.',
       failureTitle: 'Browser errors were logged to the console',
-      requiredArtifacts: ['ChromeConsoleMessages', 'RuntimeExceptions'],
+      requiredArtifacts: ['ConsoleMessages', 'RuntimeExceptions'],
     };
   }
 
@@ -32,7 +32,7 @@ class ErrorLogs extends Audit {
    * @return {LH.Audit.Product}
    */
   static audit(artifacts) {
-    const consoleEntries = artifacts.ChromeConsoleMessages;
+    const consoleEntries = artifacts.ConsoleMessages;
     const runtimeExceptions = artifacts.RuntimeExceptions;
     /** @type {Array<{source: string, description: string|undefined, url: string|undefined}>} */
     const consoleRows =
diff --git a/lighthouse-core/audits/violation-audit.js b/lighthouse-core/audits/violation-audit.js
index b259537ee..92f434930 100644
--- a/lighthouse-core/audits/violation-audit.js
+++ b/lighthouse-core/audits/violation-audit.js
@@ -15,7 +15,7 @@ class ViolationAudit extends Audit {
    */
   static getViolationResults(artifacts, pattern) {
     const seen = new Set();
-    return artifacts.ChromeConsoleMessages
+    return artifacts.ConsoleMessages
         .map(message => message.entry)
         .filter(entry => entry.url && entry.source === 'violation' && pattern.test(entry.text))
         .map(entry => ({label: `line: ${entry.lineNumber}`, url: entry.url}))
diff --git a/lighthouse-core/computed/page-dependency-graph.js b/lighthouse-core/computed/page-dependency-graph.js
index 2a9a93442..292eafbd8 100644
--- a/lighthouse-core/computed/page-dependency-graph.js
+++ b/lighthouse-core/computed/page-dependency-graph.js
@@ -30,9 +30,23 @@ class PageDependencyGraph {
   static getNetworkInitiators(record) {
     if (!record.initiator) return [];
     if (record.initiator.url) return [record.initiator.url];
-    if (record.initiator.type === 'script' && record.initiator.stack) {
-      const frames = record.initiator.stack.callFrames;
-      return Array.from(new Set(frames.map(frame => frame.url))).filter(Boolean);
+    if (record.initiator.type === 'script') {
+      // Script initiators have the stack of callFrames from all functions that led to this request.
+      // If async stacks are enabled, then the stack will also have the parent functions that asynchronously
+      // led to this request chained in the `parent` property.
+      /** @type {Set<string>} */
+      const scriptURLs = new Set();
+      let stack = record.initiator.stack;
+      while (stack) {
+        const callFrames = stack.callFrames || [];
+        for (const frame of callFrames) {
+          if (frame.url) scriptURLs.add(frame.url);
+        }
+
+        stack = stack.parent;
+      }
+
+      return Array.from(scriptURLs);
     }
 
     return [];
diff --git a/lighthouse-core/config/default-config.js b/lighthouse-core/config/default-config.js
index 86dc25925..48951810b 100644
--- a/lighthouse-core/config/default-config.js
+++ b/lighthouse-core/config/default-config.js
@@ -114,7 +114,7 @@ const defaultConfig = {
       'css-usage',
       'viewport-dimensions',
       'runtime-exceptions',
-      'chrome-console-messages',
+      'console-messages',
       'anchor-elements',
       'image-elements',
       'link-elements',
@@ -475,6 +475,7 @@ const defaultConfig = {
         {id: 'link-text', weight: 1, group: 'seo-content'},
         {id: 'is-crawlable', weight: 1, group: 'seo-crawl'},
         {id: 'robots-txt', weight: 1, group: 'seo-crawl'},
+        {id: 'image-alt', weight: 1, group: 'seo-content'},
         {id: 'hreflang', weight: 1, group: 'seo-content'},
         {id: 'canonical', weight: 1, group: 'seo-content'},
         {id: 'font-size', weight: 1, group: 'seo-mobile'},
diff --git a/lighthouse-core/gather/driver.js b/lighthouse-core/gather/driver.js
index 61e97ff8e..38fe38320 100644
--- a/lighthouse-core/gather/driver.js
+++ b/lighthouse-core/gather/driver.js
@@ -525,7 +525,14 @@ class Driver {
    * @return {Promise<{url: string, data: string}|null>}
    */
   async getAppManifest() {
-    this.setNextProtocolTimeout(3000);
+    // In all environments but LR, Page.getAppManifest finishes very quickly.
+    // In LR, there is a bug that causes this command to hang until outgoing
+    // requests finish. This has been seen in long polling (where it will never
+    // return) and when other requests take a long time to finish. We allow 10 seconds
+    // for outgoing requests to finish. Anything more, and we continue the run without
+    // a manifest.
+    // Googlers, see: http://b/124008171
+    this.setNextProtocolTimeout(10000);
     let response;
     try {
       response = await this.sendCommand('Page.getAppManifest');
@@ -1337,9 +1344,6 @@ class Driver {
     const uniqueCategories = Array.from(new Set(traceCategories));
 
     // Check any domains that could interfere with or add overhead to the trace.
-    if (this.isDomainEnabled('Debugger')) {
-      throw new Error('Debugger domain enabled when starting trace');
-    }
     if (this.isDomainEnabled('CSS')) {
       throw new Error('CSS domain enabled when starting trace');
     }
@@ -1405,6 +1409,18 @@ class Driver {
     return this.sendCommand('Runtime.enable');
   }
 
+  /**
+   * Enables `Debugger` domain to receive async stacktrace information on network request initiators.
+   * This is critical for tracing certain performance simulation situations.
+   *
+   * @return {Promise<void>}
+   */
+  async enableAsyncStacks() {
+    await this.sendCommand('Debugger.enable');
+    await this.sendCommand('Debugger.setSkipAllPauses', {skip: true});
+    await this.sendCommand('Debugger.setAsyncCallStackDepth', {maxDepth: 8});
+  }
+
   /**
    * @param {LH.Config.Settings} settings
    * @return {Promise<void>}
diff --git a/lighthouse-core/gather/gather-runner.js b/lighthouse-core/gather/gather-runner.js
index 033a9a8bf..f7efa385f 100644
--- a/lighthouse-core/gather/gather-runner.js
+++ b/lighthouse-core/gather/gather-runner.js
@@ -33,7 +33,7 @@ const Driver = require('../gather/driver.js'); // eslint-disable-line no-unused-
  *     i. assertNoSameOriginServiceWorkerClients
  *     ii. retrieve and save userAgent
  *     iii. beginEmulation
- *     iv. enableRuntimeEvents
+ *     iv. enableRuntimeEvents/enableAsyncStacks
  *     v. evaluateScriptOnLoad rescue native Promise from potential polyfill
  *     vi. register a performance observer
  *     vii. register dialog dismisser
@@ -109,6 +109,7 @@ class GatherRunner {
     await driver.assertNoSameOriginServiceWorkerClients(options.requestedUrl);
     await driver.beginEmulation(options.settings);
     await driver.enableRuntimeEvents();
+    await driver.enableAsyncStacks();
     await driver.cacheNatives();
     await driver.registerPerformanceObserver();
     await driver.dismissJavaScriptDialogs();
diff --git a/lighthouse-core/gather/gatherers/chrome-console-messages.js b/lighthouse-core/gather/gatherers/console-messages.js
similarity index 92%
rename from lighthouse-core/gather/gatherers/chrome-console-messages.js
rename to lighthouse-core/gather/gatherers/console-messages.js
index 2444e6c7f..3093c9746 100644
--- a/lighthouse-core/gather/gatherers/chrome-console-messages.js
+++ b/lighthouse-core/gather/gatherers/console-messages.js
@@ -12,7 +12,7 @@
 
 const Gatherer = require('./gatherer');
 
-class ChromeConsoleMessages extends Gatherer {
+class ConsoleMessages extends Gatherer {
   constructor() {
     super();
     /** @type {Array<LH.Crdp.Log.EntryAddedEvent>} */
@@ -41,7 +41,7 @@ class ChromeConsoleMessages extends Gatherer {
 
   /**
    * @param {LH.Gatherer.PassContext} passContext
-   * @return {Promise<LH.Artifacts['ChromeConsoleMessages']>}
+   * @return {Promise<LH.Artifacts['ConsoleMessages']>}
    */
   async afterPass(passContext) {
     await passContext.driver.sendCommand('Log.stopViolationsReport');
@@ -51,4 +51,4 @@ class ChromeConsoleMessages extends Gatherer {
   }
 }
 
-module.exports = ChromeConsoleMessages;
+module.exports = ConsoleMessages;
diff --git a/lighthouse-core/lib/proto-preprocessor.js b/lighthouse-core/lib/proto-preprocessor.js
index 77b40feda..0db83e1fe 100644
--- a/lighthouse-core/lib/proto-preprocessor.js
+++ b/lighthouse-core/lib/proto-preprocessor.js
@@ -17,11 +17,8 @@ const fs = require('fs');
 
 /**
   * @param {string} result
-  * @param {{keepRawValues?: boolean}} opts
   */
-function processForProto(result, opts = {}) {
-  const {keepRawValues = false} = opts;
-
+function processForProto(result) {
   /** @type {LH.Result} */
   const reportJson = JSON.parse(result);
 
@@ -56,12 +53,8 @@ function processForProto(result, opts = {}) {
           audit.scoreDisplayMode = 'notApplicable';
         }
       }
-      // Drop raw values. https://github.com/GoogleChrome/lighthouse/issues/6199
-      if (!keepRawValues && 'rawValue' in audit) {
-        delete audit.rawValue;
-      }
-      // Normalize displayValue to always be a string, not an array. #6200
 
+      // Normalize displayValue to always be a string, not an array. #6200
       if (Array.isArray(audit.displayValue)) {
         /** @type {Array<any>}*/
         const values = [];
diff --git a/lighthouse-core/report/html/renderer/category-renderer.js b/lighthouse-core/report/html/renderer/category-renderer.js
index 8194a0f08..dcfc9b1fb 100644
--- a/lighthouse-core/report/html/renderer/category-renderer.js
+++ b/lighthouse-core/report/html/renderer/category-renderer.js
@@ -54,22 +54,20 @@ class CategoryRenderer {
 
   /**
    * @param {LH.ReportResult.AuditRef} audit
-   * @param {number} index
    * @return {Element}
    */
-  renderAudit(audit, index) {
+  renderAudit(audit) {
     const tmpl = this.dom.cloneTemplate('#tmpl-lh-audit', this.templateContext);
-    return this.populateAuditValues(audit, index, tmpl);
+    return this.populateAuditValues(audit, tmpl);
   }
 
   /**
    * Populate an DOM tree with audit details. Used by renderAudit and renderOpportunity
    * @param {LH.ReportResult.AuditRef} audit
-   * @param {number} index
    * @param {DocumentFragment} tmpl
    * @return {Element}
    */
-  populateAuditValues(audit, index, tmpl) {
+  populateAuditValues(audit, tmpl) {
     const auditEl = this.dom.find('.lh-audit', tmpl);
     auditEl.id = audit.result.id;
     const scoreDisplayMode = audit.result.scoreDisplayMode;
@@ -109,7 +107,6 @@ class CategoryRenderer {
         header.appendChild(elem);
       }
     }
-    this.dom.find('.lh-audit__index', auditEl).textContent = `${index + 1}`;
 
     // Add chevron SVG to the end of the summary
     this.dom.find('.lh-chevron-container', auditEl).appendChild(this._createChevron());
@@ -193,16 +190,17 @@ class CategoryRenderer {
    */
   renderAuditGroup(group) {
     const groupEl = this.dom.createElement('div', 'lh-audit-group');
-    const summaryEl = this.dom.createChildOf(groupEl, 'div');
-    const summaryInnerEl = this.dom.createChildOf(summaryEl, 'div', 'lh-audit-group__summary');
-    const headerEl = this.dom.createChildOf(summaryInnerEl, 'div', 'lh-audit-group__header');
 
+    const auditGroupHeader = this.dom.createElement('div', 'lh-audit-group__header');
+
+    this.dom.createChildOf(auditGroupHeader, 'span', 'lh-audit-group__title')
+      .textContent = group.title;
     if (group.description) {
-      const auditGroupDescription = this.dom.createElement('div', 'lh-audit-group__description');
-      auditGroupDescription.appendChild(this.dom.convertMarkdownLinkSnippets(group.description));
-      groupEl.appendChild(auditGroupDescription);
+      const descriptionEl = this.dom.convertMarkdownLinkSnippets(group.description);
+      descriptionEl.classList.add('lh-audit-group__description');
+      auditGroupHeader.appendChild(descriptionEl);
     }
-    headerEl.textContent = group.title;
+    groupEl.appendChild(auditGroupHeader);
 
     return groupEl;
   }
@@ -232,14 +230,12 @@ class CategoryRenderer {
 
     /** @type {Array<Element>} */
     const auditElements = [];
-    // Continuous numbering across all groups.
-    let index = 0;
 
     for (const [groupId, groupAuditRefs] of grouped) {
       if (groupId === notAGroup) {
         // Push not-grouped audits individually.
         for (const auditRef of groupAuditRefs) {
-          auditElements.push(this.renderAudit(auditRef, index++));
+          auditElements.push(this.renderAudit(auditRef));
         }
         continue;
       }
@@ -248,7 +244,7 @@ class CategoryRenderer {
       const groupDef = groupDefinitions[groupId];
       const auditGroupElem = this.renderAuditGroup(groupDef);
       for (const auditRef of groupAuditRefs) {
-        auditGroupElem.appendChild(this.renderAudit(auditRef, index++));
+        auditGroupElem.appendChild(this.renderAudit(auditRef));
       }
       auditGroupElem.classList.add(`lh-audit-group--${groupId}`);
       auditElements.push(auditGroupElem);
@@ -292,17 +288,15 @@ class CategoryRenderer {
 
     const headerEl = this.dom.find('.lh-audit-group__header', clumpElement);
     const title = this._clumpTitles[clumpId];
-    headerEl.textContent = title;
+    this.dom.find('.lh-audit-group__title', headerEl).textContent = title;
     if (description) {
-      const markdownDescriptionEl = this.dom.convertMarkdownLinkSnippets(description);
-      const auditGroupDescription = this.dom.createElement('div', 'lh-audit-group__description');
-      auditGroupDescription.appendChild(markdownDescriptionEl);
-      clumpElement.appendChild(auditGroupDescription);
+      const descriptionEl = this.dom.convertMarkdownLinkSnippets(description);
+      descriptionEl.classList.add('lh-audit-group__description');
+      headerEl.appendChild(descriptionEl);
     }
 
     const itemCountEl = this.dom.find('.lh-audit-group__itemcount', clumpElement);
-    // TODO(i18n): support multiple locales here
-    itemCountEl.textContent = `${auditRefs.length} audits`;
+    itemCountEl.textContent = `(${auditRefs.length})`;
 
     // Add all audit results to the clump.
     const auditElements = auditRefs.map(this.renderAudit.bind(this));
@@ -331,6 +325,10 @@ class CategoryRenderer {
     wrapper.href = `#${category.id}`;
     wrapper.classList.add(`lh-gauge__wrapper--${Util.calculateRating(category.score)}`);
 
+    if (Util.isPluginCategory(category.id)) {
+      wrapper.classList.add('lh-gauge__wrapper--plugin');
+    }
+
     // Cast `null` to 0
     const numericScore = Number(category.score);
     const gauge = this.dom.find('.lh-gauge', tmpl);
diff --git a/lighthouse-core/report/html/renderer/performance-category-renderer.js b/lighthouse-core/report/html/renderer/performance-category-renderer.js
index b3be31a80..55673fbbe 100644
--- a/lighthouse-core/report/html/renderer/performance-category-renderer.js
+++ b/lighthouse-core/report/html/renderer/performance-category-renderer.js
@@ -53,13 +53,12 @@ class PerformanceCategoryRenderer extends CategoryRenderer {
 
   /**
    * @param {LH.ReportResult.AuditRef} audit
-   * @param {number} index
    * @param {number} scale
    * @return {Element}
    */
-  _renderOpportunity(audit, index, scale) {
+  _renderOpportunity(audit, scale) {
     const oppTmpl = this.dom.cloneTemplate('#tmpl-lh-opportunity', this.templateContext);
-    const element = this.populateAuditValues(audit, index, oppTmpl);
+    const element = this.populateAuditValues(audit, oppTmpl);
     element.id = audit.result.id;
 
     if (!audit.result.details || audit.result.scoreDisplayMode === 'error') {
@@ -182,8 +181,7 @@ class PerformanceCategoryRenderer extends CategoryRenderer {
 
       const headerEl = this.dom.find('.lh-load-opportunity__header', tmpl);
       groupEl.appendChild(headerEl);
-      opportunityAudits.forEach((item, i) =>
-          groupEl.appendChild(this._renderOpportunity(item, i, scale)));
+      opportunityAudits.forEach(item => groupEl.appendChild(this._renderOpportunity(item, scale)));
       groupEl.classList.add('lh-audit-group--load-opportunities');
       element.appendChild(groupEl);
     }
@@ -199,7 +197,7 @@ class PerformanceCategoryRenderer extends CategoryRenderer {
 
     if (diagnosticAudits.length) {
       const groupEl = this.renderAuditGroup(groups['diagnostics']);
-      diagnosticAudits.forEach((item, i) => groupEl.appendChild(this.renderAudit(item, i)));
+      diagnosticAudits.forEach(item => groupEl.appendChild(this.renderAudit(item)));
       groupEl.classList.add('lh-audit-group--diagnostics');
       element.appendChild(groupEl);
     }
diff --git a/lighthouse-core/report/html/renderer/util.js b/lighthouse-core/report/html/renderer/util.js
index b09ed34c4..8b1ba2fae 100644
--- a/lighthouse-core/report/html/renderer/util.js
+++ b/lighthouse-core/report/html/renderer/util.js
@@ -463,6 +463,13 @@ class Util {
 
     return lines.filter(line => lineNumbersToKeep.has(line.lineNumber));
   }
+
+  /**
+   * @param {string} categoryId
+   */
+  static isPluginCategory(categoryId) {
+    return categoryId.startsWith('lighthouse-plugin-');
+  }
 }
 
 /**
diff --git a/lighthouse-core/report/html/report-styles.css b/lighthouse-core/report/html/report-styles.css
index 18225b2ae..ce533560e 100644
--- a/lighthouse-core/report/html/report-styles.css
+++ b/lighthouse-core/report/html/report-styles.css
@@ -41,15 +41,11 @@
   --expandable-indent: 20px;
   --secondary-text-color: #565656;
   /*--accent-color: #3879d9;*/
-  --fail-color: hsl(1, 73%, 45%);
-  --average-color: hsl(31, 100%, 45%); /* md orange 800 */
-  --pass-color: hsl(139, 70%, 30%);
   --informative-color: #0c50c7;
   --medium-75-gray: #757575;
   --medium-50-gray: hsl(210, 17%, 98%);
   --medium-100-gray: hsl(200, 12%, 95%);
   --warning-color: #ffab00; /* md amber a700 */
-  --report-border-color: #ccc;
   --report-secondary-border-color: #ebebeb;
   --metric-timeline-rule-color: #b3b3b3;
   --display-value-gray: hsl(216, 5%, 39%);
@@ -72,7 +68,6 @@
   --lh-sparkline-height: 5px;
   --lh-sparkline-thin-height: 3px;
   --lh-filmstrip-thumbnail-width: 60px;
-  --lh-score-icon-width: calc(var(--body-font-size) / 14 * 16);
   --lh-category-score-width: calc(5 * var(--body-font-size));
   --lh-audit-vpadding: 8px;
   --lh-audit-index-width: 18px;
@@ -81,10 +76,7 @@
   --lh-section-vpadding: 12px;
   --chevron-size: 12px;
 
-  /* TODO(cjamcl) clean up unused variables. */
-  --audits-margin-bottom: 40px;
-  --circle-size-big: 120px;
-  --circle-size: 96px;
+  /* Palette. */
   --color-black-100: #F5F5F5;
   --color-black-200: #E0E0E0;
   --color-black-400: #BDBDBD;
@@ -100,14 +92,26 @@
   --color-red-700: #EB0F00;
   --color-red: #FF4E42;
   --color-white: #FFFFFF;
+
+  /* TODO(cjamcl) clean up unused variables. */
+  --audits-margin-bottom: 40px;
   --env-item-bg: var(--color-black-100);
   --env-name-min-width: 220px;
   --env-tem-padding: 10px 0px;
+  --gauge-circle-size-big: 120px;
+  --gauge-circle-size: 96px;
   --header-padding: 20px 0 20px 0;
+  --icon-square-size: calc(var(--score-shape-size) * 0.88);
+  --plugin-badge-bg: var(--color-white);
+  --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%;
   --score-container-padding: 12px;
   --score-container-width: 160px;
   --score-number-font-size-big: 42px;
   --score-number-font-size: 34px;
+  --score-shape-margin: 7px 12px 0 4px;
+  --score-shape-size: 12px;
   --score-title-font-size-big: 28px;
   --score-title-font-size: 20px;
   --score-title-line-height-big: 36px;
@@ -118,9 +122,16 @@
   --topbar-icon-size: 24px;
   --topbar-padding: 0 8px;
 
+  --color-average-secondary: var(--color-orange-700);
+  --color-average: var(--color-orange);
+  --color-fail-secondary: var(--color-red-700);
+  --color-fail: var(--color-red);
+  --color-pass-secondary: var(--color-green-700);
+  --color-pass: var(--color-green);
+  
+  --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>');
 
-
   --pass-icon-url: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><title>check</title><path fill="%23178239" d="M24 4C12.95 4 4 12.95 4 24c0 11.04 8.95 20 20 20 11.04 0 20-8.96 20-20 0-11.05-8.96-20-20-20zm-4 30L10 24l2.83-2.83L20 28.34l15.17-15.17L38 16 20 34z"/></svg>');
   --average-icon-url: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><title>info</title><path fill="%23E67700" d="M24 4C12.95 4 4 12.95 4 24s8.95 20 20 20 20-8.95 20-20S35.05 4 24 4zm2 30h-4V22h4v12zm0-16h-4v-4h4v4z"/></svg>');
   --fail-icon-url: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><title>warn</title><path fill="%23C7221F" d="M2 42h44L24 4 2 42zm24-6h-4v-4h4v4zm0-8h-4v-8h4v8z"/></svg>');
@@ -139,14 +150,17 @@
     --audits-margin-bottom: 20px;
     --body-font-size: 14px;
     --body-line-height: 20px;
-    --circle-size-big: 96px;
-    --circle-size: 72px;
     --env-name-min-width: 120px;
+    --gauge-circle-size-big: 96px;
+    --gauge-circle-size: 72px;
     --header-padding: 16px 0 16px 0;
+    --plugin-icon-size: 75%;
     --score-container-padding: 8px;
     --score-container-width: 112px;
     --score-number-font-size-big: 34px;
     --score-number-font-size: 26px;
+    --score-shape-margin: 5px 12px 0 2px;
+    --score-shape-size: 10px;
     --score-title-font-size-big: 22px;
     --score-title-font-size: 14px;
     --score-title-line-height-big: 26px;
@@ -190,6 +204,7 @@
 
 .lh-root * {
   box-sizing: border-box;
+  -webkit-font-smoothing: antialiased;
 }
 
 .lh-root {
@@ -302,41 +317,47 @@
 /* Score */
 
 .lh-audit__score-icon {
-  margin-left: var(--lh-score-margin);
-  width: var(--lh-score-icon-width);
-  height: var(--lh-score-icon-width);
-  background: none no-repeat center center / contain;
+  width: var(--score-shape-size);
+  height: var(--score-shape-size);
+  margin: var(--score-shape-margin);
 }
 
 .lh-audit--pass .lh-audit__display-text {
-  color: var(--pass-color);
+  color: var(--color-pass-secondary);
 }
 .lh-audit--pass .lh-audit__score-icon {
-  background-image: var(--pass-icon-url);
+  border-radius: 100%;
+  background: var(--color-pass);
 }
 
 .lh-audit--average .lh-audit__display-text {
-  color: var(--average-color);
+  color: var(--color-average-secondary);
 }
 .lh-audit--average .lh-audit__score-icon {
-  background-image: var(--average-icon-url);
+  background: var(--color-average);
+  width: var(--icon-square-size);
+  height: var(--icon-square-size);
 }
 
 .lh-audit--fail .lh-audit__display-text {
-  color: var(--fail-color);
+  color: var(--color-fail-secondary);
 }
 .lh-audit--fail .lh-audit__score-icon {
-  background-image: var(--fail-icon-url);
+  border-left: calc(var(--score-shape-size) / 2) solid transparent;
+  border-right: calc(var(--score-shape-size) / 2) solid transparent;
+  border-bottom: var(--score-shape-size) solid var(--color-fail);
 }
 
-.lh-audit--informative .lh-audit__display-text {
-  color: var(--display-value-gray);
+.lh-audit--manual .lh-audit__display-text,
+.lh-audit--notapplicable .lh-audit__display-text {
+  color: var(--color-black-600);
 }
-
-.lh-audit--informative .lh-audit__score-icon,
-.lh-audit--manual .lh-audit__score-icon {
-  visibility: hidden;
+.lh-audit--manual .lh-audit__score-icon,
+.lh-audit--notapplicable .lh-audit__score-icon {
+  border-radius: 100%;
+  background: var(--color-black-400);
 }
+
 .lh-audit--error .lh-audit__score-icon {
   display: none;
 }
@@ -353,26 +374,16 @@
 }
 
 
-.lh-audit__index,
 .lh-audit__title,
 .lh-audit__display-text,
-.lh-audit__score-icon,
 .lh-load-opportunity__sparkline,
 .lh-chevron-container {
   margin: 0 var(--audit-item-gap);
 }
-.lh-audit__index {
-  margin-left: 0;
-}
 .lh-chevron-container {
   margin-right: 0;
 }
 
-
-.lh-audit__header .lh-audit__index {
-  width: var(--lh-audit-index-width);
-}
-
 .lh-audit__title {
   flex: 1;
 }
@@ -449,6 +460,7 @@
 .lh-metric__innerwrap {
   display: flex;
   justify-content: space-between;
+  font-weight: 500;
   padding: 8px var(--text-indent);
 }
 
@@ -457,10 +469,7 @@
 }
 
 .lh-metric__title {
-  font-size: var(--body-font-size);
-  line-height: var(--body-line-height);
-  display: flex;
-  white-space: nowrap;
+  flex: 1;
 }
 
 .lh-metric__name {
@@ -483,47 +492,47 @@
 }
 
 
-.lh-metric .lh-metric__value::after {
+.lh-metric .lh-metric__innerwrap::before {
   content: '';
-  width: var(--lh-score-icon-width);
-  height: var(--lh-score-icon-width);
-  background-size: contain;
+  width: var(--score-shape-size);
+  height: var(--score-shape-size);
   display: inline-block;
-  vertical-align: text-bottom;
-  margin-left: calc(var(--body-font-size) / 2);
+  margin: var(--score-shape-margin);
 }
 
 .lh-metric--pass .lh-metric__value {
-  color: var(--pass-color);
+  color: var(--color-pass-secondary);
 }
-.lh-metric--pass .lh-metric__value::after {
-  background: var(--pass-icon-url) no-repeat 50% 50%;
+.lh-metric--pass .lh-metric__innerwrap::before {
+  border-radius: 100%;
+  background: var(--color-pass);
 }
 
-
 .lh-metric--average .lh-metric__value {
-  color: var(--average-color);
-  padding-left: 16px;
+  color: var(--color-average-secondary);
 }
-.lh-metric--average .lh-metric__value::after {
-  background: var(--average-icon-url) no-repeat 50% 50%;
+.lh-metric--average .lh-metric__innerwrap::before {
+  background: var(--color-average);
+  width: var(--icon-square-size);
+  height: var(--icon-square-size);
 }
 
-
 .lh-metric--fail .lh-metric__value {
-  color: var(--fail-color);
+  color: var(--color-fail-secondary);
 }
-.lh-metric--fail .lh-metric__value::after {
-  background: var(--fail-icon-url) no-repeat 50% 50%;
+.lh-metric--fail .lh-metric__innerwrap::before {
+  border-left: calc(var(--score-shape-size) / 2) solid transparent;
+  border-right: calc(var(--score-shape-size) / 2) solid transparent;
+  border-bottom: var(--score-shape-size) solid var(--color-fail);
 }
 
 .lh-metric--error .lh-metric__value,
 .lh-metric--error .lh-metric__description {
-  color: var(--fail-color);
+  color: var(--color-fail-secondary);
 }
 
 /* Hide icon if there was an error */
-.lh-metric--error .lh-metric__value::after {
+.lh-metric--error .lh-metric__innerwrap::before {
   display: none;
 }
 
@@ -579,15 +588,15 @@
 }
 
 .lh-audit--pass .lh-sparkline__bar {
-  background: var(--pass-color);
+  background: var(--color-pass);
 }
 
 .lh-audit--average .lh-sparkline__bar {
-  background: var(--average-color);
+  background: var(--color-average);
 }
 
 .lh-audit--fail .lh-sparkline__bar {
-  background: var(--fail-color);
+  background: var(--color-fail);
 }
 
 
@@ -635,7 +644,7 @@
 }
 
 .lh-audit--error .lh-audit__display-text {
-  color: var(--fail-color);
+  color: var(--color-fail);
 }
 
 /* Audit Group */
@@ -644,14 +653,6 @@
   margin: var(--lh-audit-group-vpadding) 0;
 }
 
-.lh-audit-group__header {
-  font-size: var(--subheader-font-size);
-  line-height: var(--subheader-line-height);
-  color: var(--subheader-color);
-  flex: 1;
-  font-weight: bold;
-}
-
 .lh-audit-group__header::before {
   /* By default, groups don't get an icon */
   content: none;
@@ -691,7 +692,7 @@
 
 /* Removing too much whitespace */
 .lh-audit-group--metrics {
-  margin-top: calc(var(--circle-size)/2 * -1);
+  margin-top: calc(var(--gauge-circle-size)/2 * -1);
   border-bottom: none;
 }
 .lh-audit-group--metrics .lh-audit-group__summary {
@@ -708,26 +709,42 @@
 }
 
 .lh-audit-group__itemcount {
-  color: var(--display-value-gray);
-  margin: 3px 10px 0;
+  color: var(--color-black-600);
+  font-weight: bold;
 }
-.lh-audit-group__summary .lh-chevron {
+.lh-audit-group__header .lh-chevron {
   margin-top: calc((var(--body-line-height) - 5px) / 2);
 }
 
-.lh-audit-group__description {
+.lh-audit-group__header {
   font-size: var(--body-font-size);
-  color: var(--medium-75-gray);
   margin: 0 0 var(--lh-audit-group-vpadding);
 }
 
-.lh-clump > .lh-audit-group__description,
-.lh-audit-group--diagnostics .lh-audit-group__description,
-.lh-audit-group--load-opportunities .lh-audit-group__description,
-.lh-audit-group--metrics .lh-audit-group__description,
-.lh-audit-group--pwa-fast-reliable .lh-audit-group__description,
-.lh-audit-group--pwa-installable .lh-audit-group__description,
-.lh-audit-group--pwa-optimized .lh-audit-group__description {
+.lh-audit-group__header span.lh-audit-group__title {
+  font-weight: bold;
+}
+
+.lh-audit-group__header span.lh-audit-group__itemcount {
+  font-weight: bold;
+  color: var(--color-black-600);
+}
+
+.lh-audit-group__header span.lh-audit-group__description {
+  font-weight: 500;
+  color: var(--color-black-600);
+}
+.lh-audit-group__header span.lh-audit-group__description::before {
+  content: '—';
+}
+
+.lh-clump > .lh-audit-group__header,
+.lh-audit-group--diagnostics .lh-audit-group__header,
+.lh-audit-group--load-opportunities .lh-audit-group__header,
+.lh-audit-group--metrics .lh-audit-group__header,
+.lh-audit-group--pwa-fast-reliable .lh-audit-group__header,
+.lh-audit-group--pwa-installable .lh-audit-group__header,
+.lh-audit-group--pwa-optimized .lh-audit-group__header {
   margin-top: var(--lh-audit-group-vpadding);
 }
 
@@ -737,7 +754,7 @@
 }
 
 .lh-audit--fail .lh-audit-explanation {
-  color: var(--fail-color);
+  color: var(--color-fail);
 }
 
 /* Report */
@@ -786,7 +803,7 @@
 
 .lh-warnings {
   --item-margin: calc(var(--body-line-height) / 6);
-  border: 1px solid var(--average-color);
+  border: 1px solid var(--color-average);
   border-radius: 4px;
   margin: var(--lh-audit-vpadding) 0;
   padding: calc(var(--lh-audit-vpadding) / 2) var(--lh-audit-vpadding);
@@ -855,15 +872,15 @@
 }
 
 .lh-scorescale-range--pass::before {
-  background-color: var(--color-green);
+  background-color: var(--color-pass);
 }
 
 .lh-scorescale-range--average::before {
-  background-color: var(--color-orange);
+  background-color: var(--color-average);
 }
 
 .lh-scorescale-range--fail::before {
-  background-color: var(--color-red);
+  background-color: var(--color-fail);
 }
 
 /* Hide category score gauages if it's a single category report */
@@ -903,7 +920,7 @@
 
 .lh-category-header {
   font-size: var(--header-font-size);
-  min-height: var(--circle-size);
+  min-height: var(--gauge-circle-size);
   margin-bottom: var(--lh-section-vpadding);
 }
 
@@ -933,7 +950,6 @@
   transition: transform 0.3s, opacity 0.3s;
   transform: translateY(100px);
   opacity: 0;
-  -webkit-font-smoothing: antialiased;
   bottom: 0;
   left: 0;
   z-index: 3;
diff --git a/lighthouse-core/report/html/templates.html b/lighthouse-core/report/html/templates.html
index e4eee66cf..fbe6d58f6 100644
--- a/lighthouse-core/report/html/templates.html
+++ b/lighthouse-core/report/html/templates.html
@@ -56,8 +56,11 @@ limitations under the License.
   <details class="lh-clump lh-audit-group">
     <summary>
       <div class="lh-audit-group__summary">
-        <div class="lh-audit-group__header"></div>
-        <div class="lh-audit-group__itemcount"></div>
+        <div class="lh-audit-group__header">
+          <span class="lh-audit-group__title"></span>
+          <span class="lh-audit-group__itemcount"></span>
+          <!-- .lh-audit-group__description will be added here -->
+        </div>
         <div class=""></div>
       </div>
     </summary>
@@ -70,10 +73,9 @@ limitations under the License.
     <details class="lh-expandable-details">
       <summary>
         <div class="lh-audit__header lh-expandable-details__summary">
-          <span class="lh-audit__index"></span>
+          <span class="lh-audit__score-icon"></span>
           <span class="lh-audit__title"></span>
           <span class="lh-audit__display-text"></span>
-          <div class="lh-audit__score-icon"></div>
           <div class="lh-chevron-container"></div>
         </div>
       </summary>
@@ -102,7 +104,7 @@ limitations under the License.
           <div class="lh-audit__header lh-expandable-details__summary">
             <div class="lh-load-opportunity__cols">
               <div class="lh-load-opportunity__col lh-load-opportunity__col--one">
-                <span class="lh-audit__index"></span>
+                <span class="lh-audit__score-icon"></span>
                 <div class="lh-audit__title"></div>
               </div>
               <div class="lh-load-opportunity__col lh-load-opportunity__col--two">
@@ -455,32 +457,31 @@ limitations under the License.
 <template id="tmpl-lh-gauge">
   <style>
     .lh-gauge__wrapper--pass {
-      color: var(--color-green);
-      fill: var(--color-green);
-      stroke: var(--color-green);
+      color: var(--color-pass);
+      fill: var(--color-pass);
+      stroke: var(--color-pass);
     }
 
     .lh-gauge__wrapper--average {
-      color: var(--color-orange);
-      fill: var(--color-orange);
-      stroke: var(--color-orange);
+      color: var(--color-average);
+      fill: var(--color-average);
+      stroke: var(--color-average);
     }
 
     .lh-gauge__wrapper--fail {
-      color: var(--color-red);
-      fill: var(--color-red);
-      stroke: var(--color-red);
+      color: var(--color-fail);
+      fill: var(--color-fail);
+      stroke: var(--color-fail);
     }
 
     .lh-gauge {
       stroke-linecap: round;
-      width: var(--circle-size);
-      height: var(--circle-size);
+      width: var(--gauge-circle-size);
+      height: var(--gauge-circle-size);
     }
 
     .lh-category .lh-gauge {
-      width: var(--circle-size-big);
-      height: var(--circle-size-big);
+      --gauge-circle-size: var(--gauge-circle-size-big);
     }
 
     .lh-gauge-base {
@@ -497,6 +498,32 @@ limitations under the License.
         animation-delay: 250ms;
     }
 
+    .lh-gauge__svg-wrapper {
+      position: relative;
+    }
+    /* The plugin badge overlay */
+    .lh-gauge__wrapper--plugin .lh-gauge__svg-wrapper::before {
+      width: var(--plugin-badge-size);
+      height: var(--plugin-badge-size);
+      background-color: var(--plugin-badge-bg);
+      background-image: var(--plugin-icon-url);
+      background-repeat: no-repeat;
+      background-size: var(--plugin-icon-size);
+      background-position: 58% 50%;
+      content: "";
+      position: absolute;
+      right: -6px;
+      bottom: -2px;
+      display: block;
+      z-index: 100;
+      box-shadow: 0 0 4px rgba(0,0,0,.2);
+      border-radius: 25%;
+    }
+    .lh-category .lh-gauge__wrapper--plugin .lh-gauge__svg-wrapper::before {
+      width: var(--plugin-badge-size-big);
+      height: var(--plugin-badge-size-big);
+    }
+
     @keyframes load-gauge {
       from { stroke-dasharray: 0 352; }
     }
@@ -510,13 +537,12 @@ limitations under the License.
       font-size: var(--score-number-font-size);
       line-height: var(--score-number-font-size);
       text-align: center;
-      top: calc(var(--circle-size) / 3);
+      top: calc(var(--gauge-circle-size) / 3);
     }
 
     .lh-category .lh-gauge__percentage {
-      font-size: var(--score-number-font-size-big);
-      line-height: var(--score-number-font-size-big);
-      top: calc(var(--circle-size-big) / 3);
+      --gauge-circle-size: var(--gauge-circle-size-big);
+      --score-number-fontsize: var(--score-number-fontsize-big);
     }
 
     .lh-gauge__wrapper {
@@ -542,17 +568,19 @@ limitations under the License.
     }
 
     .lh-category .lh-gauge__label {
-      font-size: var(--score-title-font-size-big);
-      line-height: var(--score-title-line-height-big);
-      margin: calc(0.5 * var(--score-title-line-height-big));
+      --score-title-font-size: var(--score-title-font-size-big);
+      --score-title-line-height: var(--score-title-line-height-big);
     }
 
   </style>
   <a href="#" class="lh-gauge__wrapper">
-    <svg viewBox="0 0 120 120" class="lh-gauge">
-      <circle class="lh-gauge-base" r="56" cx="60" cy="60"></circle>
-      <circle class="lh-gauge-arc" transform="rotate(-90 60 60)" r="56" cx="60" cy="60"></circle>
-    </svg>
+    <!-- Wrapper exists for the ::before plugin icon. Cannot create pseudo-elements on svgs. -->
+    <div class="lh-gauge__svg-wrapper">
+      <svg viewBox="0 0 120 120" class="lh-gauge">
+        <circle class="lh-gauge-base" r="56" cx="60" cy="60"></circle>
+        <circle class="lh-gauge-arc" transform="rotate(-90 60 60)" r="56" cx="60" cy="60"></circle>
+      </svg>
+    </div>
     <div class="lh-gauge__percentage"></div>
     <!-- TODO: should likely be an h2  -->
     <div class="lh-gauge__label"></div>
@@ -836,7 +864,7 @@ limitations under the License.
         .lh-snippet__line--message code {
           padding: 10px;
           padding-left: 5px;
-          color: var(--fail-color);
+          color: var(--color-fail);
           font-family: var(--text-font-family);
         }
         .lh-snippet__line--message code {
@@ -854,8 +882,8 @@ limitations under the License.
           display: inline-block;
           vertical-align: middle;
           margin-right: 4px;
-          width: var(--lh-score-icon-width);
-          height: var(--lh-score-icon-width);
+          width: var(--score-shape-size);
+          height: var(--score-shape-size);
           background-image: var(--fail-icon-url);
         }
         .lh-snippet__line-number {
diff --git a/lighthouse-core/scripts/build-report-for-autodeployment.js b/lighthouse-core/scripts/build-report-for-autodeployment.js
index 65c2d7b05..f4534436e 100644
--- a/lighthouse-core/scripts/build-report-for-autodeployment.js
+++ b/lighthouse-core/scripts/build-report-for-autodeployment.js
@@ -14,6 +14,14 @@ const path = require('path');
 const ReportGenerator = require('../../lighthouse-core/report/report-generator.js');
 const lhr = /** @type {LH.Result} */ (require('../../lighthouse-core/test/results/sample_v2.json'));
 
+// Add a plugin to demo plugin rendering.
+lhr.categories['lighthouse-plugin-someplugin'] = {
+  id: 'lighthouse-plugin-someplugin',
+  title: 'Plugin',
+  score: 0.5,
+  auditRefs: [],
+};
+
 console.log('🕒 Generating report for sample_v2.json...');
 const html = ReportGenerator.generateReport(lhr, 'html');
 const filename = path.join(__dirname, '../../dist/index.html');
diff --git a/proto/lighthouse-result.proto b/proto/lighthouse-result.proto
index df9a044ea..76e9dfc90 100644
--- a/proto/lighthouse-result.proto
+++ b/proto/lighthouse-result.proto
@@ -283,6 +283,11 @@ message AuditResult {
   // List of warnings associated with this audit
   // type of `Value` since this can be null
   google.protobuf.Value warnings = 10;
+
+  // 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.
+  google.protobuf.DoubleValue numeric_value = 11;
 }
 
 // Message containing the i18n data for the LHR - Version 1
diff --git a/proto/sample_v2_round_trip.json b/proto/sample_v2_round_trip.json
index e9a1122bf..215f93705 100644
--- a/proto/sample_v2_round_trip.json
+++ b/proto/sample_v2_round_trip.json
@@ -137,6 +137,7 @@
             }, 
             "displayValue": "1.2\u00a0s", 
             "id": "bootup-time", 
+            "numericValue": 1150.573, 
             "score": 0.92, 
             "scoreDisplayMode": "numeric", 
             "title": "JavaScript execution time"
@@ -460,6 +461,7 @@
                 "type": "debugdata"
             }, 
             "id": "diagnostics", 
+            "numericValue": 1.0, 
             "score": null, 
             "scoreDisplayMode": "informative", 
             "title": "Diagnostics"
@@ -536,6 +538,7 @@
             }, 
             "displayValue": "31 elements", 
             "id": "dom-size", 
+            "numericValue": 31.0, 
             "score": 1.0, 
             "scoreDisplayMode": "numeric", 
             "title": "Avoids an excessive DOM size"
@@ -562,6 +565,7 @@
                 "type": "opportunity"
             }, 
             "id": "efficient-animated-content", 
+            "numericValue": 0.0, 
             "score": 1.0, 
             "scoreDisplayMode": "numeric", 
             "title": "Use video formats for animated content"
@@ -611,6 +615,7 @@
                 "type": "table"
             }, 
             "id": "errors-in-console", 
+            "numericValue": 5.0, 
             "score": 0.0, 
             "scoreDisplayMode": "binary", 
             "title": "Browser errors were logged to the console"
@@ -619,6 +624,7 @@
             "description": "Estimated Input Latency is an estimate of how long your app takes to respond to user input, in milliseconds, during the busiest 5s window of page load. If your latency is higher than 50 ms, users may perceive your app as laggy. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/estimated-input-latency).", 
             "displayValue": "20\u00a0ms", 
             "id": "estimated-input-latency", 
+            "numericValue": 16.0, 
             "score": 1.0, 
             "scoreDisplayMode": "numeric", 
             "title": "Estimated Input Latency"
@@ -688,6 +694,7 @@
             "description": "First Contentful Paint marks the time at which the first text or image is painted. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/first-contentful-paint).", 
             "displayValue": "4.0\u00a0s", 
             "id": "first-contentful-paint", 
+            "numericValue": 3969.135, 
             "score": 0.51, 
             "scoreDisplayMode": "numeric", 
             "title": "First Contentful Paint"
@@ -696,6 +703,7 @@
             "description": "First CPU Idle marks the first time at which the page's main thread is quiet enough to handle input. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/first-interactive).", 
             "displayValue": "4.9\u00a0s", 
             "id": "first-cpu-idle", 
+            "numericValue": 4927.278, 
             "score": 0.72, 
             "scoreDisplayMode": "numeric", 
             "title": "First CPU Idle"
@@ -704,6 +712,7 @@
             "description": "First Meaningful Paint measures when the primary content of a page is visible. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/first-meaningful-paint).", 
             "displayValue": "4.0\u00a0s", 
             "id": "first-meaningful-paint", 
+            "numericValue": 3969.136, 
             "score": 0.51, 
             "scoreDisplayMode": "numeric", 
             "title": "First Meaningful Paint"
@@ -1020,6 +1029,7 @@
             "description": "Time to interactive is the amount of time it takes for the page to become fully interactive. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/consistently-interactive).", 
             "displayValue": "4.9\u00a0s", 
             "id": "interactive", 
+            "numericValue": 4927.278, 
             "score": 0.78, 
             "scoreDisplayMode": "numeric", 
             "title": "Time to Interactive"
@@ -1240,6 +1250,7 @@
         "load-fast-enough-for-pwa": {
             "description": "A fast page load over a cellular network ensures a good mobile user experience. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/fast-3g).", 
             "id": "load-fast-enough-for-pwa", 
+            "numericValue": 4927.278, 
             "score": 1.0, 
             "scoreDisplayMode": "binary", 
             "title": "Page load is fast enough on mobile networks"
@@ -1337,6 +1348,7 @@
                 "type": "table"
             }, 
             "id": "main-thread-tasks", 
+            "numericValue": 16.0, 
             "score": null, 
             "scoreDisplayMode": "informative", 
             "title": "Tasks"
@@ -1398,6 +1410,7 @@
             }, 
             "displayValue": "1.5\u00a0s", 
             "id": "mainthread-work-breakdown", 
+            "numericValue": 1548.5690000000002, 
             "score": 0.96, 
             "scoreDisplayMode": "numeric", 
             "title": "Minimizes main-thread work"
@@ -1413,6 +1426,7 @@
             "description": "The potential First Input Delay that your users could experience is the duration, in milliseconds, of the longest task.", 
             "displayValue": "120\u00a0ms", 
             "id": "max-potential-fid", 
+            "numericValue": 122.537, 
             "score": 0.92, 
             "scoreDisplayMode": "numeric", 
             "title": "Max Potential FID"
@@ -1484,6 +1498,7 @@
                 "type": "debugdata"
             }, 
             "id": "metrics", 
+            "numericValue": 4927.278, 
             "score": null, 
             "scoreDisplayMode": "informative", 
             "title": "Metrics"
@@ -1724,6 +1739,7 @@
                 "type": "table"
             }, 
             "id": "network-requests", 
+            "numericValue": 18.0, 
             "score": null, 
             "scoreDisplayMode": "informative", 
             "title": "Network Requests"
@@ -1758,6 +1774,7 @@
             }, 
             "displayValue": "0\u00a0ms", 
             "id": "network-rtt", 
+            "numericValue": 1.7249999999999943, 
             "score": null, 
             "scoreDisplayMode": "informative", 
             "title": "Network Round Trip Times"
@@ -1792,6 +1809,7 @@
             }, 
             "displayValue": "570\u00a0ms", 
             "id": "network-server-latency", 
+            "numericValue": 572.829, 
             "score": null, 
             "scoreDisplayMode": "informative", 
             "title": "Server Backend Latencies"
@@ -1974,6 +1992,7 @@
                 "type": "opportunity"
             }, 
             "id": "offscreen-images", 
+            "numericValue": 0.0, 
             "score": 1.0, 
             "scoreDisplayMode": "numeric", 
             "title": "Defer offscreen images", 
@@ -2052,6 +2071,7 @@
                 "type": "opportunity"
             }, 
             "id": "redirects", 
+            "numericValue": 0.0, 
             "score": 1.0, 
             "scoreDisplayMode": "numeric", 
             "title": "Avoid multiple page redirects"
@@ -2115,6 +2135,7 @@
             }, 
             "displayValue": "Potential savings of 1,130\u00a0ms", 
             "id": "render-blocking-resources", 
+            "numericValue": 1129.0, 
             "score": 0.46, 
             "scoreDisplayMode": "numeric", 
             "title": "Eliminate render-blocking resources"
@@ -2200,6 +2221,7 @@
             "description": "Speed Index shows how quickly the contents of a page are visibly populated. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/speed-index).", 
             "displayValue": "4.4\u00a0s", 
             "id": "speed-index", 
+            "numericValue": 4417.0, 
             "score": 0.74, 
             "scoreDisplayMode": "numeric", 
             "title": "Speed Index"
@@ -2297,6 +2319,7 @@
             }, 
             "displayValue": "Root document took 570\u00a0ms", 
             "id": "time-to-first-byte", 
+            "numericValue": 570.5630000000001, 
             "score": 1.0, 
             "scoreDisplayMode": "binary", 
             "title": "Server response times are low (TTFB)"
@@ -2362,6 +2385,7 @@
             }, 
             "displayValue": "Total size was 157\u00a0KB", 
             "id": "total-byte-weight", 
+            "numericValue": 160738.0, 
             "score": 1.0, 
             "scoreDisplayMode": "numeric", 
             "title": "Avoids enormous network payloads"
@@ -2376,6 +2400,7 @@
                 "type": "opportunity"
             }, 
             "id": "unminified-css", 
+            "numericValue": 0.0, 
             "score": 1.0, 
             "scoreDisplayMode": "numeric", 
             "title": "Minify CSS"
@@ -2414,6 +2439,7 @@
             }, 
             "displayValue": "Potential savings of 30\u00a0KB", 
             "id": "unminified-javascript", 
+            "numericValue": 150.0, 
             "score": 0.88, 
             "scoreDisplayMode": "numeric", 
             "title": "Minify JavaScript", 
@@ -2429,6 +2455,7 @@
                 "type": "opportunity"
             }, 
             "id": "unused-css-rules", 
+            "numericValue": 0.0, 
             "score": 1.0, 
             "scoreDisplayMode": "numeric", 
             "title": "Remove unused CSS"
@@ -2646,6 +2673,7 @@
             }, 
             "displayValue": "11 resources found", 
             "id": "uses-long-cache-ttl", 
+            "numericValue": 103455.0, 
             "score": 0.58, 
             "scoreDisplayMode": "numeric", 
             "title": "Serve static assets with an efficient cache policy"
@@ -2660,6 +2688,7 @@
                 "type": "opportunity"
             }, 
             "id": "uses-optimized-images", 
+            "numericValue": 0.0, 
             "score": 1.0, 
             "scoreDisplayMode": "numeric", 
             "title": "Efficiently encode images", 
@@ -2710,6 +2739,7 @@
                 "type": "opportunity"
             }, 
             "id": "uses-rel-preconnect", 
+            "numericValue": 0.0, 
             "score": 1.0, 
             "scoreDisplayMode": "numeric", 
             "title": "Preconnect to required origins", 
@@ -2724,6 +2754,7 @@
                 "type": "opportunity"
             }, 
             "id": "uses-rel-preload", 
+            "numericValue": 0.0, 
             "score": 1.0, 
             "scoreDisplayMode": "numeric", 
             "title": "Preload key requests"
@@ -2738,6 +2769,7 @@
                 "type": "opportunity"
             }, 
             "id": "uses-responsive-images", 
+            "numericValue": 0.0, 
             "score": 1.0, 
             "scoreDisplayMode": "numeric", 
             "title": "Properly size images", 
@@ -2781,6 +2813,7 @@
             }, 
             "displayValue": "Potential savings of 63\u00a0KB", 
             "id": "uses-text-compression", 
+            "numericValue": 300.0, 
             "score": 0.75, 
             "scoreDisplayMode": "numeric", 
             "title": "Enable text compression"
@@ -2824,6 +2857,7 @@
             }, 
             "displayValue": "Potential savings of 9\u00a0KB", 
             "id": "uses-webp-images", 
+            "numericValue": 0.0, 
             "score": 1.0, 
             "scoreDisplayMode": "numeric", 
             "title": "Serve images in next-gen formats", 
@@ -3478,6 +3512,11 @@
                     "id": "robots-txt", 
                     "weight": 0.0
                 }, 
+                {
+                    "group": "seo-content", 
+                    "id": "image-alt", 
+                    "weight": 1.0
+                }, 
                 {
                     "group": "seo-content", 
                     "id": "hreflang", 
@@ -3511,7 +3550,7 @@
             "description": "These checks ensure that your page is optimized for search engine results ranking. There are additional factors Lighthouse does not check that may affect your search ranking. [Learn more](https://support.google.com/webmasters/answer/35769).", 
             "id": "seo", 
             "manualDescription": "Run these additional validators on your site to check additional SEO best practices.", 
-            "score": 0.9, 
+            "score": 0.82, 
             "title": "SEO"
         }
     }, 
diff --git a/types/artifacts.d.ts b/types/artifacts.d.ts
index db5e250f4..a563f5648 100644
--- a/types/artifacts.d.ts
+++ b/types/artifacts.d.ts
@@ -47,11 +47,33 @@ declare global {
       Timing: Artifacts.MeasureEntry[];
     }
 
+    /**
+     * Artifacts provided by the default gatherers that are exposed to plugins with a hardended API.
+     * NOTE: any breaking changes here are considered breaking Lighthouse changes that must be done
+     * on a major version bump.
+     */
+    export interface PublicGathererArtifacts {
+      /** Console deprecation and intervention warnings logged by Chrome during page load. */
+      ConsoleMessages: Crdp.Log.EntryAddedEvent[];
+      /** Information on size and loading for all the images in the page. Natural size information for `picture` and CSS images is only available if the image was one of the largest 50 images. */
+      ImageElements: Artifacts.ImageElement[];
+      /** All the link elements on the page or equivalently declared in `Link` headers. @see https://html.spec.whatwg.org/multipage/links.html */
+      LinkElements: Artifacts.LinkElement[];
+      /** The values of the <meta> elements in the head. */
+      MetaElements: Array<{name: string, content?: string}>;
+      /** Set of exceptions thrown during page load. */
+      RuntimeExceptions: Crdp.Runtime.ExceptionThrownEvent[];
+      /** Information on all script elements in the page. Also contains the content of all requested scripts and the networkRecord requestId that contained their content. Note, HTML documents will have one entry per script tag, all with the same requestId. */
+      ScriptElements: Array<Artifacts.ScriptElement>;
+      /** The dimensions and devicePixelRatio of the loaded viewport. */
+      ViewportDimensions: Artifacts.ViewportDimensions;
+    }
+
     /**
      * Artifacts provided by the default gatherers. Augment this interface when adding additional
-     * gatherers.
+     * gatherers. Changes to these artifacts are not considered a breaking Lighthouse change.
      */
-    export interface GathererArtifacts {
+    export interface GathererArtifacts extends PublicGathererArtifacts {
       /** The results of running the aXe accessibility tests on the page. */
       Accessibility: Artifacts.Accessibility;
       /** Array of all anchors on the page. */
@@ -60,8 +82,6 @@ declare global {
       AppCacheManifest: string | null;
       /** Array of all URLs cached in CacheStorage. */
       CacheContents: string[];
-      /** Console deprecation and intervention warnings logged by Chrome during page load. */
-      ChromeConsoleMessages: Crdp.Log.EntryAddedEvent[];
       /** CSS coverage information for styles used by page's final state. */
       CSSUsage: {rules: Crdp.CSS.RuleUsage[], stylesheets: Artifacts.CSSStyleSheetInfo[]};
       /** Information on the document's doctype(or null if not present), specifically the name, publicId, and systemId.
@@ -71,26 +91,18 @@ declare global {
       DOMStats: Artifacts.DOMStats;
       /** Relevant attributes and child properties of all <object>s, <embed>s and <applet>s in the page. */
       EmbeddedContent: Artifacts.EmbeddedContentInfo[];
-      /** All the link elements on the page or equivalently declared in `Link` headers. @see https://html.spec.whatwg.org/multipage/links.html */
-      LinkElements: Artifacts.LinkElement[];
       /** Information for font faces used in the page. */
       Fonts: Artifacts.Font[];
       /** Information on poorly sized font usage and the text affected by it. */
       FontSize: Artifacts.FontSize;
-      /** The hreflang and href values of all link[rel=alternate] nodes found in HEAD. */
-      Hreflang: {href: string, hreflang: string}[];
       /** The page's document body innerText if loaded with JavaScript disabled. */
       HTMLWithoutJavaScript: {bodyText: string, hasNoScript: boolean};
       /** Whether the page ended up on an HTTPS page after attempting to load the HTTP version. */
       HTTPRedirect: {value: boolean};
-      /** Information on size and loading for all the images in the page. Natural size information for `picture` and CSS images is only available if the image was one of the largest 50 images. */
-      ImageElements: Artifacts.ImageElement[];
       /** JS coverage information for code used during page load. */
       JsUsage: Crdp.Profiler.ScriptCoverage[];
       /** Parsed version of the page's Web App Manifest, or null if none found. */
       Manifest: Artifacts.Manifest | null;
-      /** The values of the <meta> elements in the head. */
-      MetaElements: Array<{name: string, content?: string}>;
       /** The URL loaded with interception */
       MixedContent: {url: string};
       /** The status code of the attempted load of the page while network access is disabled. */
@@ -103,10 +115,6 @@ declare global {
       ResponseCompression: {requestId: string, url: string, mimeType: string, transferSize: number, resourceSize: number, gzipSize?: number}[];
       /** Information on fetching and the content of the /robots.txt file. */
       RobotsTxt: {status: number|null, content: string|null};
-      /** Set of exceptions thrown during page load. */
-      RuntimeExceptions: Crdp.Runtime.ExceptionThrownEvent[];
-      /** Information on all script elements in the page. Also contains the content of all requested scripts and the networkRecord requestId that contained their content. Note, HTML documents will have one entry per script tag, all with the same requestId. */
-      ScriptElements: Array<Artifacts.ScriptElement>;
       /** Version information for all ServiceWorkers active after the first page load. */
       ServiceWorker: {versions: Crdp.ServiceWorker.ServiceWorkerVersion[], registrations: Crdp.ServiceWorker.ServiceWorkerRegistration[]};
       /** The status of an offline fetch of the page's start_url. -1 and a explanation if missing or there was an error. */
@@ -115,8 +123,6 @@ declare global {
       TagsBlockingFirstPaint: Artifacts.TagBlockingFirstPaint[];
       /** Information about tap targets including their position and size. */
       TapTargets: Artifacts.TapTarget[];
-      /** The dimensions and devicePixelRatio of the loaded viewport. */
-      ViewportDimensions: Artifacts.ViewportDimensions;
     }
 
     module Artifacts {
diff --git a/types/audit.d.ts b/types/audit.d.ts
index 6a2cc25cd..9be3f533d 100644
--- a/types/audit.d.ts
+++ b/types/audit.d.ts
@@ -113,7 +113,7 @@ declare global {
       /** A more detailed description that describes why the audit is important and links to Lighthouse documentation on the audit; markdown links supported. */
       description: string;
       /** 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-cli/test/cli/__snapshots__/index-test.js.snap b/lighthouse-cli/test/cli/__snapshots__/index-test.js.snap
index aeaddcff2..593c3c48d 100644
--- a/lighthouse-cli/test/cli/__snapshots__/index-test.js.snap
+++ b/lighthouse-cli/test/cli/__snapshots__/index-test.js.snap
@@ -962,6 +962,11 @@ Object {
           "id": "robots-txt",
           "weight": 1,
         },
+        Object {
+          "group": "seo-content",
+          "id": "image-alt",
+          "weight": 1,
+        },
         Object {
           "group": "seo-content",
           "id": "hreflang",
@@ -1079,7 +1084,7 @@ Object {
           "path": "runtime-exceptions",
         },
         Object {
-          "path": "chrome-console-messages",
+          "path": "console-messages",
         },
         Object {
           "path": "anchor-elements",
diff --git a/lighthouse-cli/test/cli/run-test.js b/lighthouse-cli/test/cli/run-test.js
index ea233fb75..7c00cf961 100644
--- a/lighthouse-cli/test/cli/run-test.js
+++ b/lighthouse-cli/test/cli/run-test.js
@@ -65,7 +65,7 @@ describe('CLI run', function() {
       assert.strictEqual(fileResults.fetchTime, lhr.fetchTime);
       assert.strictEqual(fileResults.requestedUrl, lhr.requestedUrl);
       assert.strictEqual(fileResults.finalUrl, lhr.finalUrl);
-      assert.strictEqual(fileResults.audits.viewport.rawValue, lhr.audits.viewport.rawValue);
+      assert.strictEqual(fileResults.audits.viewport.score, lhr.audits.viewport.score);
       assert.strictEqual(
           Object.keys(fileResults.audits).length,
           Object.keys(lhr.audits).length);
diff --git a/lighthouse-cli/test/smokehouse/dobetterweb/dbw-expectations.js b/lighthouse-cli/test/smokehouse/dobetterweb/dbw-expectations.js
index 360775167..5824d6487 100644
--- a/lighthouse-cli/test/smokehouse/dobetterweb/dbw-expectations.js
+++ b/lighthouse-cli/test/smokehouse/dobetterweb/dbw-expectations.js
@@ -75,7 +75,7 @@ module.exports = [
         },
         'render-blocking-resources': {
           score: '<1',
-          rawValue: '>100',
+          numericValue: '>100',
           details: {
             items: {
               length: 7,
@@ -145,7 +145,7 @@ module.exports = [
         },
         'dom-size': {
           score: 1,
-          rawValue: 31,
+          numericValue: 31,
           details: {
             items: [
               {statistic: 'Total DOM Elements', value: '31'},
diff --git a/lighthouse-cli/test/smokehouse/perf/expectations.js b/lighthouse-cli/test/smokehouse/perf/expectations.js
index 18b0ff7df..3fcebd574 100644
--- a/lighthouse-cli/test/smokehouse/perf/expectations.js
+++ b/lighthouse-cli/test/smokehouse/perf/expectations.js
@@ -27,8 +27,8 @@ module.exports = [
           score: '>=0.90',
         },
         'time-to-first-byte': {
-        // Can be flaky, so test float rawValue instead of boolean score
-          rawValue: '<1000',
+          // Can be flaky, so test float numericValue instead of boolean score
+          numericValue: '<1000',
         },
         'network-requests': {
           details: {
@@ -39,7 +39,7 @@ module.exports = [
         },
         'uses-rel-preload': {
           score: '<1',
-          rawValue: '>500',
+          numericValue: '>500',
           warnings: {
             0: /level-2.*warning/,
             length: 1,
diff --git a/lighthouse-cli/test/smokehouse/perf/lantern-expectations.js b/lighthouse-cli/test/smokehouse/perf/lantern-expectations.js
index 373fb6f4d..5186beb4b 100644
--- a/lighthouse-cli/test/smokehouse/perf/lantern-expectations.js
+++ b/lighthouse-cli/test/smokehouse/perf/lantern-expectations.js
@@ -15,13 +15,13 @@ module.exports = [
       finalUrl: 'http://localhost:10200/online-only.html',
       audits: {
         'first-contentful-paint': {
-          rawValue: '>2000',
+          numericValue: '>2000',
         },
         'first-cpu-idle': {
-          rawValue: '>2000',
+          numericValue: '>2000',
         },
         'interactive': {
-          rawValue: '>2000',
+          numericValue: '>2000',
         },
       },
     },
diff --git a/lighthouse-cli/test/smokehouse/redirects/expectations.js b/lighthouse-cli/test/smokehouse/redirects/expectations.js
index f3cef7150..6abd8c746 100644
--- a/lighthouse-cli/test/smokehouse/redirects/expectations.js
+++ b/lighthouse-cli/test/smokehouse/redirects/expectations.js
@@ -18,7 +18,7 @@ module.exports = [
       audits: {
         'redirects': {
           score: '<1',
-          rawValue: '>=500',
+          numericValue: '>=500',
           details: {
             items: {
               length: 3,
@@ -35,7 +35,7 @@ module.exports = [
       audits: {
         'redirects': {
           score: 1,
-          rawValue: '>=250',
+          numericValue: '>=250',
           details: {
             items: {
               length: 2,
diff --git a/lighthouse-cli/test/smokehouse/tricky-metrics/expectations.js b/lighthouse-cli/test/smokehouse/tricky-metrics/expectations.js
index 189610ad0..b7050ef4a 100644
--- a/lighthouse-cli/test/smokehouse/tricky-metrics/expectations.js
+++ b/lighthouse-cli/test/smokehouse/tricky-metrics/expectations.js
@@ -16,11 +16,11 @@ module.exports = [
       audits: {
         'first-cpu-idle': {
           score: '<75',
-          rawValue: '>9000',
+          numericValue: '>9000',
         },
         'interactive': {
           score: '<75',
-          rawValue: '>9000',
+          numericValue: '>9000',
         },
       },
     },
@@ -31,7 +31,7 @@ module.exports = [
       finalUrl: 'http://localhost:10200/delayed-fcp.html',
       audits: {
         'first-contentful-paint': {
-          rawValue: '>1', // We just want to check that it doesn't error
+          numericValue: '>1', // We just want to check that it doesn't error
         },
       },
     },
diff --git a/lighthouse-core/test/audits/deprecations-test.js b/lighthouse-core/test/audits/deprecations-test.js
index 0aff362a9..2fbb0b8b5 100644
--- a/lighthouse-core/test/audits/deprecations-test.js
+++ b/lighthouse-core/test/audits/deprecations-test.js
@@ -13,7 +13,7 @@ const assert = require('assert');
 describe('Console deprecations audit', () => {
   it('passes when no console messages were found', () => {
     const auditResult = DeprecationsAudit.audit({
-      ChromeConsoleMessages: [],
+      ConsoleMessages: [],
     });
     assert.equal(auditResult.score, 1);
     assert.equal(auditResult.details.items.length, 0);
@@ -21,7 +21,7 @@ describe('Console deprecations audit', () => {
 
   it('handles deprecations that do not have url or line numbers', () => {
     const auditResult = DeprecationsAudit.audit({
-      ChromeConsoleMessages: [
+      ConsoleMessages: [
         {
           entry: {
             source: 'deprecation',
@@ -41,7 +41,7 @@ describe('Console deprecations audit', () => {
     const URL = 'http://example.com';
 
     const auditResult = DeprecationsAudit.audit({
-      ChromeConsoleMessages: [
+      ConsoleMessages: [
         {
           entry: {
             source: 'deprecation',
diff --git a/lighthouse-core/test/audits/dobetterweb/geolocation-on-start-test.js b/lighthouse-core/test/audits/dobetterweb/geolocation-on-start-test.js
index e6681ab9c..b0e5c3fc3 100644
--- a/lighthouse-core/test/audits/dobetterweb/geolocation-on-start-test.js
+++ b/lighthouse-core/test/audits/dobetterweb/geolocation-on-start-test.js
@@ -15,7 +15,7 @@ describe('UX: geolocation audit', () => {
     const text = 'Do not request geolocation permission without a user action.';
 
     const auditResult = GeolocationOnStartAudit.audit({
-      ChromeConsoleMessages: [
+      ConsoleMessages: [
         {entry: {source: 'violation', url: 'https://example.com/', text}},
         {entry: {source: 'violation', url: 'https://example2.com/two', text}},
         {entry: {source: 'violation', url: 'http://abc.com/', text: 'No document.write'}},
@@ -28,7 +28,7 @@ describe('UX: geolocation audit', () => {
 
   it('passes when geolocation has not been automatically requested', () => {
     const auditResult = GeolocationOnStartAudit.audit({
-      ChromeConsoleMessages: [],
+      ConsoleMessages: [],
     });
     assert.equal(auditResult.score, 1);
     assert.equal(auditResult.details.items.length, 0);
diff --git a/lighthouse-core/test/audits/dobetterweb/no-document-write-test.js b/lighthouse-core/test/audits/dobetterweb/no-document-write-test.js
index 6a3172b72..7c49f428c 100644
--- a/lighthouse-core/test/audits/dobetterweb/no-document-write-test.js
+++ b/lighthouse-core/test/audits/dobetterweb/no-document-write-test.js
@@ -15,7 +15,7 @@ const URL = 'https://example.com';
 describe('Page does not use document.write()', () => {
   it('passes when document.write() is not used', () => {
     const auditResult = DocWriteUseAudit.audit({
-      ChromeConsoleMessages: [],
+      ConsoleMessages: [],
       URL: {finalUrl: URL},
     });
     assert.equal(auditResult.score, 1);
@@ -26,7 +26,7 @@ describe('Page does not use document.write()', () => {
     const text = 'Do not use document.write';
     const auditResult = DocWriteUseAudit.audit({
       URL: {finalUrl: URL},
-      ChromeConsoleMessages: [
+      ConsoleMessages: [
         {entry: {source: 'violation', url: 'https://example.com/', text}},
         {entry: {source: 'violation', url: 'https://example2.com/two', text}},
         {entry: {source: 'violation', url: 'http://abc.com/', text: 'Long event handler!'}},
diff --git a/lighthouse-core/test/audits/dobetterweb/notification-on-start-test.js b/lighthouse-core/test/audits/dobetterweb/notification-on-start-test.js
index 8a50e5f33..94dfe8915 100644
--- a/lighthouse-core/test/audits/dobetterweb/notification-on-start-test.js
+++ b/lighthouse-core/test/audits/dobetterweb/notification-on-start-test.js
@@ -14,7 +14,7 @@ describe('UX: notification audit', () => {
   it('fails when notification has been automatically requested', () => {
     const text = 'Do not request notification permission without a user action.';
     const auditResult = NotificationOnStart.audit({
-      ChromeConsoleMessages: [
+      ConsoleMessages: [
         {entry: {source: 'violation', url: 'https://example.com/', text}},
         {entry: {source: 'violation', url: 'https://example2.com/two', text}},
         {entry: {source: 'violation', url: 'http://abc.com/', text: 'No document.write'}},
@@ -27,7 +27,7 @@ describe('UX: notification audit', () => {
 
   it('passes when notification has not been automatically requested', () => {
     const auditResult = NotificationOnStart.audit({
-      ChromeConsoleMessages: [],
+      ConsoleMessages: [],
     });
     assert.equal(auditResult.score, 1);
     assert.equal(auditResult.details.items.length, 0);
diff --git a/lighthouse-core/test/audits/dobetterweb/uses-passive-event-listeners-test.js b/lighthouse-core/test/audits/dobetterweb/uses-passive-event-listeners-test.js
index 505fb6021..8077cac54 100644
--- a/lighthouse-core/test/audits/dobetterweb/uses-passive-event-listeners-test.js
+++ b/lighthouse-core/test/audits/dobetterweb/uses-passive-event-listeners-test.js
@@ -15,7 +15,7 @@ describe('Page uses passive events listeners where applicable', () => {
     const text = 'Use passive event listeners when you do not use preventDefault';
 
     const auditResult = PassiveEventsAudit.audit({
-      ChromeConsoleMessages: [
+      ConsoleMessages: [
         {entry: {source: 'violation', url: 'https://example.com/', text}},
         {entry: {source: 'violation', url: 'https://example2.com/two', text}},
         {entry: {source: 'violation', url: 'https://example2.com/two', text}}, // duplicate
@@ -30,7 +30,7 @@ describe('Page uses passive events listeners where applicable', () => {
 
   it('passes scroll blocking listeners should be passive', () => {
     const auditResult = PassiveEventsAudit.audit({
-      ChromeConsoleMessages: [],
+      ConsoleMessages: [],
     });
     assert.equal(auditResult.score, 1);
     assert.equal(auditResult.extendedInfo.value.length, 0);
diff --git a/lighthouse-core/test/audits/errors-in-console-test.js b/lighthouse-core/test/audits/errors-in-console-test.js
index 3238927e6..6382a9d46 100644
--- a/lighthouse-core/test/audits/errors-in-console-test.js
+++ b/lighthouse-core/test/audits/errors-in-console-test.js
@@ -13,7 +13,7 @@ const assert = require('assert');
 describe('Console error logs audit', () => {
   it('passes when no console messages were found', () => {
     const auditResult = ErrorLogsAudit.audit({
-      ChromeConsoleMessages: [],
+      ConsoleMessages: [],
       RuntimeExceptions: [],
     });
     assert.equal(auditResult.rawValue, 0);
@@ -24,7 +24,7 @@ describe('Console error logs audit', () => {
 
   it('filter out the non error logs', () => {
     const auditResult = ErrorLogsAudit.audit({
-      ChromeConsoleMessages: [
+      ConsoleMessages: [
         {
           entry: {
             level: 'info',
@@ -42,7 +42,7 @@ describe('Console error logs audit', () => {
 
   it('fails when error logs are found ', () => {
     const auditResult = ErrorLogsAudit.audit({
-      ChromeConsoleMessages: [
+      ConsoleMessages: [
         {
           entry: {
             level: 'error',
@@ -98,7 +98,7 @@ describe('Console error logs audit', () => {
 
   it('handle the case when some logs fields are undefined', () => {
     const auditResult = ErrorLogsAudit.audit({
-      ChromeConsoleMessages: [
+      ConsoleMessages: [
         {
           entry: {
             level: 'error',
@@ -119,7 +119,7 @@ describe('Console error logs audit', () => {
   // Checks bug #4188
   it('handle the case when exception info is not present', () => {
     const auditResult = ErrorLogsAudit.audit({
-      ChromeConsoleMessages: [],
+      ConsoleMessages: [],
       RuntimeExceptions: [{
         'timestamp': 1506535813608.003,
         'exceptionDetails': {
diff --git a/lighthouse-core/test/computed/page-dependency-graph-test.js b/lighthouse-core/test/computed/page-dependency-graph-test.js
index 814378405..97712ed01 100644
--- a/lighthouse-core/test/computed/page-dependency-graph-test.js
+++ b/lighthouse-core/test/computed/page-dependency-graph-test.js
@@ -281,6 +281,31 @@ describe('PageDependencyGraph computed artifact:', () => {
       assert.equal(nodes[2].isMainDocument(), false);
     });
 
+    it('should link up script initiators', () => {
+      const request1 = createRequest(1, '1', 0);
+      const request2 = createRequest(2, '2', 5);
+      const request3 = createRequest(3, '3', 5);
+      const request4 = createRequest(4, '4', 20);
+      request4.initiator = {
+        type: 'script',
+        stack: {callFrames: [{url: '2'}], parent: {parent: {callFrames: [{url: '3'}]}}},
+      };
+      const networkRecords = [request1, request2, request3, request4];
+
+      addTaskEvents(0, 0, []);
+
+      const graph = PageDependencyGraph.createGraph(traceOfTab, networkRecords);
+      const nodes = [];
+      graph.traverse(node => nodes.push(node));
+
+      assert.equal(nodes.length, 4);
+      assert.deepEqual(nodes.map(node => node.id), [1, 2, 3, 4]);
+      assert.deepEqual(nodes[0].getDependencies(), []);
+      assert.deepEqual(nodes[1].getDependencies(), [nodes[0]]);
+      assert.deepEqual(nodes[2].getDependencies(), [nodes[0]]);
+      assert.deepEqual(nodes[3].getDependencies(), [nodes[1], nodes[2]]);
+    });
+
     it('should throw when root node is not related to main document', () => {
       const request1 = createRequest(1, '1', 0, null, NetworkRequest.TYPES.Other);
       const request2 = createRequest(2, '2', 5, null, NetworkRequest.TYPES.Document);
diff --git a/lighthouse-core/test/gather/fake-driver.js b/lighthouse-core/test/gather/fake-driver.js
index 73e0782e0..1db517772 100644
--- a/lighthouse-core/test/gather/fake-driver.js
+++ b/lighthouse-core/test/gather/fake-driver.js
@@ -43,6 +43,9 @@ function makeFakeDriver({protocolGetVersionResponse}) {
     enableRuntimeEvents() {
       return Promise.resolve();
     },
+    enableAsyncStacks() {
+      return Promise.resolve();
+    },
     evaluateScriptOnLoad() {
       return Promise.resolve();
     },
diff --git a/lighthouse-core/test/gather/gather-runner-test.js b/lighthouse-core/test/gather/gather-runner-test.js
index 22c5762e4..36ab4f962 100644
--- a/lighthouse-core/test/gather/gather-runner-test.js
+++ b/lighthouse-core/test/gather/gather-runner-test.js
@@ -45,6 +45,9 @@ function getMockedEmulationDriver(emulationFn, netThrottleFn, cpuThrottleFn,
     enableRuntimeEvents() {
       return Promise.resolve();
     }
+    enableAsyncStacks() {
+      return Promise.resolve();
+    }
     assertNoSameOriginServiceWorkerClients() {
       return Promise.resolve();
     }
@@ -333,6 +336,7 @@ describe('GatherRunner', function() {
       setThrottling: asyncFunc,
       dismissJavaScriptDialogs: asyncFunc,
       enableRuntimeEvents: asyncFunc,
+      enableAsyncStacks: asyncFunc,
       cacheNatives: asyncFunc,
       gotoURL: asyncFunc,
       registerPerformanceObserver: asyncFunc,
@@ -392,6 +396,7 @@ describe('GatherRunner', function() {
       setThrottling: asyncFunc,
       dismissJavaScriptDialogs: asyncFunc,
       enableRuntimeEvents: asyncFunc,
+      enableAsyncStacks: asyncFunc,
       cacheNatives: asyncFunc,
       gotoURL: asyncFunc,
       registerPerformanceObserver: asyncFunc,
diff --git a/lighthouse-core/test/lib/manifest-parser-test.js b/lighthouse-core/test/lib/manifest-parser-test.js
index 20b56ad56..5e8bd6b85 100644
--- a/lighthouse-core/test/lib/manifest-parser-test.js
+++ b/lighthouse-core/test/lib/manifest-parser-test.js
@@ -347,7 +347,6 @@ describe('Manifest Parser', function() {
       const display = parsedManifest.value.display;
       assert.ok(!display.warning);
       assert.equal(display.value, 'browser');
-      assert.equal(display.rawValue, undefined);
     });
 
     it('trims whitespace', () => {
diff --git a/lighthouse-core/test/lib/proto-preprocessor-test.js b/lighthouse-core/test/lib/proto-preprocessor-test.js
index 73b452054..d92093308 100644
--- a/lighthouse-core/test/lib/proto-preprocessor-test.js
+++ b/lighthouse-core/test/lib/proto-preprocessor-test.js
@@ -78,7 +78,7 @@ describe('processing for proto', () => {
       'audits': {
         'critical-request-chains': {
           'scoreDisplayMode': 'not-applicable',
-          'rawValue': 14.3,
+          'numericValue': 14.3,
           'displayValue': ['hello %d', 123],
         },
       },
diff --git a/lighthouse-core/test/report/html/renderer/category-renderer-test.js b/lighthouse-core/test/report/html/renderer/category-renderer-test.js
index 30f60c445..28086f1bb 100644
--- a/lighthouse-core/test/report/html/renderer/category-renderer-test.js
+++ b/lighthouse-core/test/report/html/renderer/category-renderer-test.js
@@ -144,6 +144,21 @@ describe('CategoryRenderer', () => {
 
     const audits = categoryDOM.querySelectorAll('.lh-audit');
     assert.equal(audits.length, category.auditRefs.length, 'renders correct number of audits');
+
+    // No plugin categories in sampleResults.
+    assert.equal(
+      categoryDOM.querySelector('.lh-gauge__wrapper--plugin'), null, 'renders no plugin badges');
+  });
+
+  it('plugin category has plugin badge', () => {
+    const category = JSON.parse(
+      JSON.stringify(sampleResults.reportCategories.find(c => c.id === 'seo')));
+    category.id = 'lighthouse-plugin-someplugin';
+    category.title = 'Some Plugin';
+    const categoryDOM = renderer.render(category, sampleResults.categoryGroups);
+    assert.ok(categoryDOM.querySelector('.lh-gauge__wrapper--plugin'));
+    const label = categoryDOM.querySelector('.lh-gauge__label').textContent;
+    assert.equal(category.title, label);
   });
 
   it('handles markdown in category descriptions a category', () => {
@@ -265,32 +280,6 @@ describe('CategoryRenderer', () => {
       assert.equal(auditsElements.length, category.auditRefs.length);
     });
 
-    it('increments the audit index across groups', () => {
-      const elem = renderer.render(category, sampleResults.categoryGroups);
-
-      const passedAudits = elem.querySelectorAll('.lh-clump--passed .lh-audit__index');
-      const failedAudits = elem.querySelectorAll('.lh-clump--failed .lh-audit__index');
-      const manualAudits = elem.querySelectorAll('.lh-clump--manual .lh-audit__index');
-      const notApplicableAudits =
-        elem.querySelectorAll('.lh-clump--notapplicable .lh-audit__index');
-
-      const assertAllTheIndices = (nodeList) => {
-        // Must be at least one for a decent test.
-        assert.ok(nodeList.length > 0);
-
-        // Assert indices are continuous, starting at 1.
-        nodeList.forEach((node, i) => {
-          const auditIndex = Number.parseInt(node.textContent);
-          assert.strictEqual(auditIndex, i + 1);
-        });
-      };
-
-      assertAllTheIndices(passedAudits);
-      assertAllTheIndices(failedAudits);
-      assertAllTheIndices(manualAudits);
-      assertAllTheIndices(notApplicableAudits);
-    });
-
     it('renders audits without a group before grouped ones', () => {
       const categoryClone = JSON.parse(JSON.stringify(category));
 
diff --git a/lighthouse-core/test/report/html/renderer/performance-category-renderer-test.js b/lighthouse-core/test/report/html/renderer/performance-category-renderer-test.js
index b3c2ea12e..2d03ab6d8 100644
--- a/lighthouse-core/test/report/html/renderer/performance-category-renderer-test.js
+++ b/lighthouse-core/test/report/html/renderer/performance-category-renderer-test.js
@@ -122,7 +122,7 @@ describe('PerfCategoryRenderer', () => {
       group: 'load-opportunities',
       result: {
         score: 0, scoreDisplayMode: 'numeric',
-        rawValue: 100, explanation: 'Yikes!!', title: 'Bug #2', description: '',
+        numericValue: 100, explanation: 'Yikes!!', title: 'Bug #2', description: '',
       },
     };
 
@@ -166,7 +166,7 @@ describe('PerfCategoryRenderer', () => {
         group: 'load-opportunities',
         result: {
           error: true, score: 0,
-          rawValue: 100, explanation: 'Yikes!!', title: 'Bug #2',
+          numericValue: 100, explanation: 'Yikes!!', title: 'Bug #2',
         },
       };
       const wastedMs = renderer._getWastedMs(auditWithDebug);
diff --git a/lighthouse-core/test/report/html/renderer/psi-test.js b/lighthouse-core/test/report/html/renderer/psi-test.js
index f89e4b3f3..57823bcad 100644
--- a/lighthouse-core/test/report/html/renderer/psi-test.js
+++ b/lighthouse-core/test/report/html/renderer/psi-test.js
@@ -73,9 +73,9 @@ describe('DOM', () => {
         assert.ok(!result.perfCategoryEl.outerHTML.includes('lh-permalink'),
             'PSI\'s perfCategory HTML doesn\'t include a lh-permalink element');
         // Assume using default locale.
-        const titleEl = result.perfCategoryEl.querySelector('.lh-audit-group--metrics')
-          .querySelector('.lh-audit-group__header');
-        assert.equal(titleEl.textContent, Util.UIStrings.labDataTitle);
+        const title = result.perfCategoryEl.querySelector('.lh-audit-group--metrics')
+          .querySelector('.lh-audit-group__title').textContent;
+        assert.equal(title, Util.UIStrings.labDataTitle);
       });
 
       it('succeeds with stringified LHResult input', () => {
@@ -119,14 +119,16 @@ describe('DOM', () => {
         const metricsGroupEl = perfCategoryEl.querySelector('.lh-audit-group--metrics');
 
         // Assume using default locale.
-        const titleEl = metricsGroupEl.querySelector('.lh-audit-group__header');
-        assert.equal(titleEl.textContent, Util.UIStrings.labDataTitle);
-
-        // Description supports markdown links, so take everything after the last link.
-        const descriptionEnd = /[^)]+$/.exec(Util.UIStrings.lsPerformanceCategoryDescription)[0];
-        assert.ok(descriptionEnd.length > 6); // If this gets too short, pick a different comparison :)
-        const descriptionEl = metricsGroupEl.querySelector('.lh-audit-group__description');
-        assert.ok(descriptionEl.textContent.endsWith(descriptionEnd));
+        // Replacing markdown because ".textContent" will be post-markdown.
+        const expectedDescription = Util.UIStrings.lsPerformanceCategoryDescription
+          .replace('[Lighthouse](https://developers.google.com/web/tools/lighthouse/)', 'Lighthouse');
+
+        // Assume using default locale.
+        const title = metricsGroupEl.querySelector('.lh-audit-group__title').textContent;
+        const description =
+          metricsGroupEl.querySelector('.lh-audit-group__description').textContent;
+        assert.equal(title, Util.UIStrings.labDataTitle);
+        assert.equal(description, expectedDescription);
       });
     });
   });
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 840a37c94..0121fd116 100644
--- a/lighthouse-core/test/report/html/renderer/report-renderer-test.js
+++ b/lighthouse-core/test/report/html/renderer/report-renderer-test.js
@@ -124,6 +124,26 @@ describe('ReportRenderer', () => {
       }
     });
 
+    it('renders plugin score gauge', () => {
+      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(sampleResultsCopy, container);
+      const scoresHeaderElem = output.querySelector('.lh-scores-header');
+
+      const gaugeCount = scoresHeaderElem.querySelectorAll('.lh-gauge').length;
+      const pluginGaugeCount =
+        scoresHeaderElem.querySelectorAll('.lh-gauge__wrapper--plugin').length;
+
+      // 5 core categories + the 1 plugin.
+      assert.equal(6, gaugeCount);
+      assert.equal(1, pluginGaugeCount);
+    });
+
     it('should not mutate a report object', () => {
       const container = renderer._dom._document.body;
       const originalResults = JSON.parse(JSON.stringify(sampleResults));
diff --git a/lighthouse-core/test/report/report-generator-test.js b/lighthouse-core/test/report/report-generator-test.js
index 1214f0ade..c33b54c1b 100644
--- a/lighthouse-core/test/report/report-generator-test.js
+++ b/lighthouse-core/test/report/report-generator-test.js
@@ -63,7 +63,7 @@ describe('ReportGenerator', () => {
     it('should inject the report CSS', () => {
       const result = ReportGenerator.generateReportHtml({});
       assert.ok(!result.includes('/*%%LIGHTHOUSE_CSS%%*/'));
-      assert.ok(result.includes('--pass-color'));
+      assert.ok(result.includes('--color-green'));
     });
 
     it('should inject the report renderer javascript', () => {
diff --git a/lighthouse-core/test/results/artifacts/artifacts.json b/lighthouse-core/test/results/artifacts/artifacts.json
index d91391fc8..0aef7b229 100644
--- a/lighthouse-core/test/results/artifacts/artifacts.json
+++ b/lighthouse-core/test/results/artifacts/artifacts.json
@@ -406,7 +406,7 @@
       }
     }
   ],
-  "ChromeConsoleMessages": [
+  "ConsoleMessages": [
     {
       "entry": {
         "source": "other",
diff --git a/lighthouse-core/test/results/sample_v2.json b/lighthouse-core/test/results/sample_v2.json
index c7ed7e658..a7548037f 100644
--- a/lighthouse-core/test/results/sample_v2.json
+++ b/lighthouse-core/test/results/sample_v2.json
@@ -77,7 +77,7 @@
       "description": "First Contentful Paint marks the time at which the first text or image is painted. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/first-contentful-paint).",
       "score": 0.51,
       "scoreDisplayMode": "numeric",
-      "rawValue": 3969.135,
+      "numericValue": 3969.135,
       "displayValue": "4.0 s"
     },
     "first-meaningful-paint": {
@@ -86,7 +86,7 @@
       "description": "First Meaningful Paint measures when the primary content of a page is visible. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/first-meaningful-paint).",
       "score": 0.51,
       "scoreDisplayMode": "numeric",
-      "rawValue": 3969.136,
+      "numericValue": 3969.136,
       "displayValue": "4.0 s"
     },
     "load-fast-enough-for-pwa": {
@@ -95,7 +95,7 @@
       "description": "A fast page load over a cellular network ensures a good mobile user experience. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/fast-3g).",
       "score": 1,
       "scoreDisplayMode": "binary",
-      "rawValue": 4927.278
+      "numericValue": 4927.278
     },
     "speed-index": {
       "id": "speed-index",
@@ -103,7 +103,7 @@
       "description": "Speed Index shows how quickly the contents of a page are visibly populated. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/speed-index).",
       "score": 0.74,
       "scoreDisplayMode": "numeric",
-      "rawValue": 4417,
+      "numericValue": 4417,
       "displayValue": "4.4 s"
     },
     "screenshot-thumbnails": {
@@ -188,7 +188,7 @@
       "description": "Estimated Input Latency is an estimate of how long your app takes to respond to user input, in milliseconds, during the busiest 5s window of page load. If your latency is higher than 50 ms, users may perceive your app as laggy. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/estimated-input-latency).",
       "score": 1,
       "scoreDisplayMode": "numeric",
-      "rawValue": 16,
+      "numericValue": 16,
       "displayValue": "20 ms"
     },
     "max-potential-fid": {
@@ -197,7 +197,7 @@
       "description": "The potential First Input Delay that your users could experience is the duration, in milliseconds, of the longest task.",
       "score": 0.92,
       "scoreDisplayMode": "numeric",
-      "rawValue": 122.537,
+      "numericValue": 122.537,
       "displayValue": "120 ms"
     },
     "errors-in-console": {
@@ -206,7 +206,7 @@
       "description": "Errors logged to the console indicate unresolved problems. They can come from network request failures and other browser concerns.",
       "score": 0,
       "scoreDisplayMode": "binary",
-      "rawValue": 5,
+      "numericValue": 5,
       "details": {
         "type": "table",
         "headings": [
@@ -256,7 +256,7 @@
       "description": "Time To First Byte identifies the time at which your server sends a response. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/ttfb).",
       "score": 1,
       "scoreDisplayMode": "binary",
-      "rawValue": 570.5630000000001,
+      "numericValue": 570.5630000000001,
       "displayValue": "Root document took 570 ms",
       "details": {
         "type": "opportunity",
@@ -271,7 +271,7 @@
       "description": "First CPU Idle marks the first time at which the page's main thread is quiet enough to handle input. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/first-interactive).",
       "score": 0.72,
       "scoreDisplayMode": "numeric",
-      "rawValue": 4927.278,
+      "numericValue": 4927.278,
       "displayValue": "4.9 s"
     },
     "interactive": {
@@ -280,7 +280,7 @@
       "description": "Time to interactive is the amount of time it takes for the page to become fully interactive. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/consistently-interactive).",
       "score": 0.78,
       "scoreDisplayMode": "numeric",
-      "rawValue": 4927.278,
+      "numericValue": 4927.278,
       "displayValue": "4.9 s"
     },
     "user-timings": {
@@ -447,7 +447,7 @@
       "description": "Redirects introduce additional delays before the page can be loaded. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/redirects).",
       "score": 1,
       "scoreDisplayMode": "numeric",
-      "rawValue": 0,
+      "numericValue": 0,
       "displayValue": "",
       "details": {
         "type": "opportunity",
@@ -620,7 +620,7 @@
       "description": "Consider reducing the time spent parsing, compiling and executing JS. You may find delivering smaller JS payloads helps with this.",
       "score": 0.96,
       "scoreDisplayMode": "numeric",
-      "rawValue": 1548.5690000000002,
+      "numericValue": 1548.5690000000002,
       "displayValue": "1.5 s",
       "details": {
         "type": "table",
@@ -682,7 +682,7 @@
       "description": "Consider reducing the time spent parsing, compiling, and executing JS. You may find delivering smaller JS payloads helps with this. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/bootup).",
       "score": 0.92,
       "scoreDisplayMode": "numeric",
-      "rawValue": 1150.573,
+      "numericValue": 1150.573,
       "displayValue": "1.2 s",
       "details": {
         "type": "table",
@@ -748,7 +748,7 @@
       "description": "Consider using <link rel=preload> to prioritize fetching resources that are currently requested later in page load. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/preload).",
       "score": 1,
       "scoreDisplayMode": "numeric",
-      "rawValue": 0,
+      "numericValue": 0,
       "displayValue": "",
       "details": {
         "type": "opportunity",
@@ -763,7 +763,7 @@
       "description": "Consider adding preconnect or dns-prefetch resource hints to establish early connections to important third-party origins. [Learn more](https://developers.google.com/web/fundamentals/performance/resource-prioritization#preconnect).",
       "score": 1,
       "scoreDisplayMode": "numeric",
-      "rawValue": 0,
+      "numericValue": 0,
       "displayValue": "",
       "warnings": [],
       "details": {
@@ -791,7 +791,7 @@
       "description": "Collection of useful page vitals.",
       "score": null,
       "scoreDisplayMode": "informative",
-      "rawValue": 1,
+      "numericValue": 1,
       "details": {
         "type": "debugdata",
         "items": [
@@ -823,7 +823,7 @@
       "description": "Lists the network requests that were made during page load.",
       "score": null,
       "scoreDisplayMode": "informative",
-      "rawValue": 18,
+      "numericValue": 18,
       "details": {
         "type": "table",
         "headings": [
@@ -1064,7 +1064,7 @@
       "description": "Network round trip times (RTT) have a large impact on performance. If the RTT to an origin is high, it's an indication that servers closer to the user could improve performance. [Learn more](https://hpbn.co/primer-on-latency-and-bandwidth/).",
       "score": null,
       "scoreDisplayMode": "informative",
-      "rawValue": 1.7249999999999943,
+      "numericValue": 1.7249999999999943,
       "displayValue": "0 ms",
       "details": {
         "type": "table",
@@ -1099,7 +1099,7 @@
       "description": "Server latencies can impact web performance. If the server latency of an origin is high, it's an indication the server is overloaded or has poor backend performance. [Learn more](https://hpbn.co/primer-on-web-performance/#analyzing-the-resource-waterfall).",
       "score": null,
       "scoreDisplayMode": "informative",
-      "rawValue": 572.829,
+      "numericValue": 572.829,
       "displayValue": "570 ms",
       "details": {
         "type": "table",
@@ -1134,7 +1134,7 @@
       "description": "Lists the toplevel main thread tasks that executed during page load.",
       "score": null,
       "scoreDisplayMode": "informative",
-      "rawValue": 16,
+      "numericValue": 16,
       "details": {
         "type": "table",
         "headings": [
@@ -1225,7 +1225,7 @@
       "description": "Collects all available metrics.",
       "score": null,
       "scoreDisplayMode": "informative",
-      "rawValue": 4927.278,
+      "numericValue": 4927.278,
       "details": {
         "type": "debugdata",
         "items": [
@@ -1910,7 +1910,7 @@
       "description": "A long cache lifetime can speed up repeat visits to your page. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/cache-policy).",
       "score": 0.58,
       "scoreDisplayMode": "numeric",
-      "rawValue": 103455,
+      "numericValue": 103455,
       "displayValue": "11 resources found",
       "details": {
         "type": "table",
@@ -2024,7 +2024,7 @@
       "description": "Large network payloads cost users real money and are highly correlated with long load times. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/network-payloads).",
       "score": 1,
       "scoreDisplayMode": "numeric",
-      "rawValue": 160738,
+      "numericValue": 160738,
       "displayValue": "Total size was 157 KB",
       "details": {
         "type": "table",
@@ -2090,7 +2090,7 @@
       "description": "Consider lazy-loading offscreen and hidden images after all critical resources have finished loading to lower time to interactive. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/offscreen-images).",
       "score": 1,
       "scoreDisplayMode": "numeric",
-      "rawValue": 0,
+      "numericValue": 0,
       "displayValue": "",
       "warnings": [],
       "details": {
@@ -2107,7 +2107,7 @@
       "description": "Resources are blocking the first paint of your page. Consider delivering critical JS/CSS inline and deferring all non-critical JS/styles. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/blocking-resources).",
       "score": 0.46,
       "scoreDisplayMode": "numeric",
-      "rawValue": 1129,
+      "numericValue": 1129,
       "displayValue": "Potential savings of 1,130 ms",
       "details": {
         "type": "opportunity",
@@ -2164,7 +2164,7 @@
       "description": "Minifying CSS files can reduce network payload sizes. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/minify-css).",
       "score": 1,
       "scoreDisplayMode": "numeric",
-      "rawValue": 0,
+      "numericValue": 0,
       "displayValue": "",
       "details": {
         "type": "opportunity",
@@ -2180,7 +2180,7 @@
       "description": "Minifying JavaScript files can reduce payload sizes and script parse time. [Learn more](https://developers.google.com/speed/docs/insights/MinifyResources).",
       "score": 0.88,
       "scoreDisplayMode": "numeric",
-      "rawValue": 150,
+      "numericValue": 150,
       "displayValue": "Potential savings of 30 KB",
       "warnings": [],
       "details": {
@@ -2220,7 +2220,7 @@
       "description": "Remove dead rules from stylesheets and defer the loading of CSS not used for above-the-fold content to reduce unnecessary bytes consumed by network activity. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/unused-css).",
       "score": 1,
       "scoreDisplayMode": "numeric",
-      "rawValue": 0,
+      "numericValue": 0,
       "displayValue": "",
       "details": {
         "type": "opportunity",
@@ -2236,7 +2236,7 @@
       "description": "Image formats like JPEG 2000, JPEG XR, and WebP often provide better compression than PNG or JPEG, which means faster downloads and less data consumption. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/webp).",
       "score": 1,
       "scoreDisplayMode": "numeric",
-      "rawValue": 0,
+      "numericValue": 0,
       "displayValue": "Potential savings of 9 KB",
       "warnings": [],
       "details": {
@@ -2282,7 +2282,7 @@
       "description": "Optimized images load faster and consume less cellular data. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/optimize-images).",
       "score": 1,
       "scoreDisplayMode": "numeric",
-      "rawValue": 0,
+      "numericValue": 0,
       "displayValue": "",
       "warnings": [],
       "details": {
@@ -2299,7 +2299,7 @@
       "description": "Text-based resources should be served with compression (gzip, deflate or brotli) to minimize total network bytes. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/text-compression).",
       "score": 0.75,
       "scoreDisplayMode": "numeric",
-      "rawValue": 300,
+      "numericValue": 300,
       "displayValue": "Potential savings of 63 KB",
       "details": {
         "type": "opportunity",
@@ -2342,7 +2342,7 @@
       "description": "Serve images that are appropriately-sized to save cellular data and improve load time. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/oversized-images).",
       "score": 1,
       "scoreDisplayMode": "numeric",
-      "rawValue": 0,
+      "numericValue": 0,
       "displayValue": "",
       "warnings": [],
       "details": {
@@ -2359,7 +2359,7 @@
       "description": "Large GIFs are inefficient for delivering animated content. Consider using MPEG4/WebM videos for animations and PNG/WebP for static images instead of GIF to save network bytes. [Learn more](https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/replace-animated-gifs-with-video/)",
       "score": 1,
       "scoreDisplayMode": "numeric",
-      "rawValue": 0,
+      "numericValue": 0,
       "displayValue": "",
       "details": {
         "type": "opportunity",
@@ -2390,7 +2390,7 @@
       "description": "Browser engineers recommend pages contain fewer than ~1,500 DOM elements. The sweet spot is a tree depth < 32 elements and fewer than 60 children/parent element. A large DOM can increase memory usage, cause longer [style calculations](https://developers.google.com/web/fundamentals/performance/rendering/reduce-the-scope-and-complexity-of-style-calculations), and produce costly [layout reflows](https://developers.google.com/speed/articles/reflow). [Learn more](https://developers.google.com/web/tools/lighthouse/audits/dom-size).",
       "score": 1,
       "scoreDisplayMode": "numeric",
-      "rawValue": 31,
+      "numericValue": 31,
       "displayValue": "31 elements",
       "details": {
         "type": "table",
@@ -3491,6 +3491,11 @@
           "weight": 0,
           "group": "seo-crawl"
         },
+        {
+          "id": "image-alt",
+          "weight": 1,
+          "group": "seo-content"
+        },
         {
           "id": "hreflang",
           "weight": 1,
@@ -3522,7 +3527,7 @@
         }
       ],
       "id": "seo",
-      "score": 0.9
+      "score": 0.82
     },
     "pwa": {
       "title": "Progressive Web App",
@@ -5438,4 +5443,4 @@
     }
   },
   "stackPacks": []
-}
+}
\ No newline at end of file
diff --git a/lighthouse-core/test/runner-test.js b/lighthouse-core/test/runner-test.js
index ee3a55b45..720c85dd7 100644
--- a/lighthouse-core/test/runner-test.js
+++ b/lighthouse-core/test/runner-test.js
@@ -561,7 +561,8 @@ describe('Runner', () => {
           static audit(artifacts, context) {
             context.LighthouseRunWarnings.push(warningString);
             return {
-              rawValue: 5,
+              numericValue: 5,
+              score: 1,
             };
           }
         },
diff --git a/lighthouse-core/test/scoring-test.js b/lighthouse-core/test/scoring-test.js
index 723efdf6b..35ed8b020 100644
--- a/lighthouse-core/test/scoring-test.js
+++ b/lighthouse-core/test/scoring-test.js
@@ -35,7 +35,7 @@ describe('ReportScoring', () => {
   describe('#scoreAllCategories', () => {
     it('should score the categories', () => {
       const resultsByAuditId = {
-        'my-audit': {rawValue: 'you passed', score: 0},
+        'my-audit': {score: 0},
         'my-boolean-audit': {score: 1, extendedInfo: {}},
         'my-scored-audit': {score: 1},
         'my-failed-audit': {score: 0.2},