Repository: GoogleChrome/lighthouse
Pull request #5887 (2018-08-21 22:07:00)

core(tsc): tighten traceOfTab timing types
#5873 needed fixes to `traceOfTab`'s `timings` and `timestamps` creation, which is a good opportunity to take care of long-standing TODOs to fix the types for those objects. Basically most of their properties may not have been found in the trace but we were pretending they were all there to make dealing with them simpler.

We now throw on `NO_FCP` in so many places that maybe we should just call it required in a trace and throw in `traceOfTab` like we do for no `navigationStart`?

brendankenny: > Sprinkling NO_FCP everywhere seems worse than just updating the tests that need a fake FCP event

SG and @paulirish seemed 👍 

Updated.

Patch:
diff --git a/lighthouse-core/audits/metrics.js b/lighthouse-core/audits/metrics.js
index 7f7a53107..9113cc5be 100644
--- a/lighthouse-core/audits/metrics.js
+++ b/lighthouse-core/audits/metrics.js
@@ -115,18 +115,18 @@ class Metrics extends Audit {
  * @property {number=} estimatedInputLatencyTs
  * @property {number} observedNavigationStart
  * @property {number} observedNavigationStartTs
- * @property {number} observedFirstPaint
- * @property {number} observedFirstPaintTs
+ * @property {number=} observedFirstPaint
+ * @property {number=} observedFirstPaintTs
  * @property {number} observedFirstContentfulPaint
  * @property {number} observedFirstContentfulPaintTs
- * @property {number} observedFirstMeaningfulPaint
- * @property {number} observedFirstMeaningfulPaintTs
- * @property {number} observedTraceEnd
- * @property {number} observedTraceEndTs
- * @property {number} observedLoad
- * @property {number} observedLoadTs
- * @property {number} observedDomContentLoaded
- * @property {number} observedDomContentLoadedTs
+ * @property {number=} observedFirstMeaningfulPaint
+ * @property {number=} observedFirstMeaningfulPaintTs
+ * @property {number=} observedTraceEnd
+ * @property {number=} observedTraceEndTs
+ * @property {number=} observedLoad
+ * @property {number=} observedLoadTs
+ * @property {number=} observedDomContentLoaded
+ * @property {number=} observedDomContentLoadedTs
  * @property {number} observedFirstVisualChange
  * @property {number} observedFirstVisualChangeTs
  * @property {number} observedLastVisualChange
diff --git a/lighthouse-core/audits/webapp-install-banner.js b/lighthouse-core/audits/webapp-install-banner.js
index be4f7a114..b747ed6bd 100644
--- a/lighthouse-core/audits/webapp-install-banner.js
+++ b/lighthouse-core/audits/webapp-install-banner.js
@@ -104,14 +104,13 @@ class WebappInstallBanner extends MultiCheckAudit {
 
     if (!hasOfflineStartUrl) {
       failures.push('Service worker does not successfully serve the manifest\'s start_url');
-      // TODO(phulce): align gatherer `debugString` with `explanation`
-      if (artifacts.StartUrl.debugString) {
-        failures.push(artifacts.StartUrl.debugString);
+      if (artifacts.StartUrl.explanation) {
+        failures.push(artifacts.StartUrl.explanation);
       }
     }
 
-    if (artifacts.StartUrl.debugString) {
-      warnings.push(artifacts.StartUrl.debugString);
+    if (artifacts.StartUrl.explanation) {
+      warnings.push(artifacts.StartUrl.explanation);
     }
 
     return {failures, warnings};
diff --git a/lighthouse-core/gather/computed/metrics/first-contentful-paint.js b/lighthouse-core/gather/computed/metrics/first-contentful-paint.js
index e84c45b00..d82bf4fb6 100644
--- a/lighthouse-core/gather/computed/metrics/first-contentful-paint.js
+++ b/lighthouse-core/gather/computed/metrics/first-contentful-paint.js
@@ -6,7 +6,6 @@
 'use strict';
 
 const MetricArtifact = require('./metric');
-const LHError = require('../../../lib/errors');
 
 class FirstContentfulPaint extends MetricArtifact {
   get name() {
@@ -17,16 +16,13 @@ class FirstContentfulPaint extends MetricArtifact {
    * @param {LH.Artifacts.MetricComputationData} data
    * @return {Promise<LH.Artifacts.Metric>}
    */
-  computeObservedMetric(data) {
+  async computeObservedMetric(data) {
     const {traceOfTab} = data;
-    if (!traceOfTab.timestamps.firstContentfulPaint) {
-      throw new LHError(LHError.errors.NO_FCP);
-    }
 
-    return Promise.resolve({
+    return {
       timing: traceOfTab.timings.firstContentfulPaint,
       timestamp: traceOfTab.timestamps.firstContentfulPaint,
-    });
+    };
   }
 }
 
diff --git a/lighthouse-core/gather/computed/metrics/first-cpu-idle.js b/lighthouse-core/gather/computed/metrics/first-cpu-idle.js
index 95f485b2b..2ce2beac6 100644
--- a/lighthouse-core/gather/computed/metrics/first-cpu-idle.js
+++ b/lighthouse-core/gather/computed/metrics/first-cpu-idle.js
@@ -177,14 +177,14 @@ class FirstCPUIdle extends MetricArtifact {
     const DCL = traceOfTab.timings.domContentLoaded;
     const traceEnd = traceOfTab.timings.traceEnd;
 
-    if (traceEnd - FMP < MAX_QUIET_WINDOW_SIZE) {
-      throw new LHError(LHError.errors.FMP_TOO_LATE_FOR_FCPUI);
-    }
-
     if (!FMP || !DCL) {
       throw new LHError(FMP ? LHError.errors.NO_DCL : LHError.errors.NO_FMP);
     }
 
+    if (traceEnd - FMP < MAX_QUIET_WINDOW_SIZE) {
+      throw new LHError(LHError.errors.FMP_TOO_LATE_FOR_FCPUI);
+    }
+
     const longTasksAfterFMP = TracingProcessor.getMainThreadTopLevelEvents(traceOfTab, FMP)
         .filter(evt => evt.duration >= LONG_TASK_THRESHOLD);
     const firstInteractive = FirstCPUIdle.findQuietWindow(FMP, traceEnd, longTasksAfterFMP);
diff --git a/lighthouse-core/gather/computed/metrics/first-meaningful-paint.js b/lighthouse-core/gather/computed/metrics/first-meaningful-paint.js
index 20b7da4f8..ca9bdc44d 100644
--- a/lighthouse-core/gather/computed/metrics/first-meaningful-paint.js
+++ b/lighthouse-core/gather/computed/metrics/first-meaningful-paint.js
@@ -17,16 +17,17 @@ class FirstMeaningfulPaint extends MetricArtifact {
    * @param {LH.Artifacts.MetricComputationData} data
    * @return {Promise<LH.Artifacts.Metric>}
    */
-  computeObservedMetric(data) {
+  async computeObservedMetric(data) {
     const {traceOfTab} = data;
     if (!traceOfTab.timestamps.firstMeaningfulPaint) {
       throw new LHError(LHError.errors.NO_FMP);
     }
 
-    return Promise.resolve({
-      timing: traceOfTab.timings.firstMeaningfulPaint,
+    return {
+      // FMP established as existing, so cast
+      timing: /** @type {number} */ (traceOfTab.timings.firstMeaningfulPaint),
       timestamp: traceOfTab.timestamps.firstMeaningfulPaint,
-    });
+    };
   }
 }
 
diff --git a/lighthouse-core/gather/computed/metrics/interactive.js b/lighthouse-core/gather/computed/metrics/interactive.js
index bb4bd7bcc..53ec137f1 100644
--- a/lighthouse-core/gather/computed/metrics/interactive.js
+++ b/lighthouse-core/gather/computed/metrics/interactive.js
@@ -148,9 +148,6 @@ class Interactive extends MetricArtifact {
    */
   computeObservedMetric(data) {
     const {traceOfTab, networkRecords} = data;
-    if (!traceOfTab.timestamps.firstContentfulPaint) {
-      throw new LHError(LHError.errors.NO_FCP);
-    }
 
     if (!traceOfTab.timestamps.domContentLoaded) {
       throw new LHError(LHError.errors.NO_DCL);
diff --git a/lighthouse-core/gather/computed/metrics/lantern-first-meaningful-paint.js b/lighthouse-core/gather/computed/metrics/lantern-first-meaningful-paint.js
index 9bbcbe262..726b592fc 100644
--- a/lighthouse-core/gather/computed/metrics/lantern-first-meaningful-paint.js
+++ b/lighthouse-core/gather/computed/metrics/lantern-first-meaningful-paint.js
@@ -7,6 +7,7 @@
 
 const MetricArtifact = require('./lantern-metric');
 const BaseNode = require('../../../lib/dependency-graph/base-node');
+const LHError = require('../../../lib/errors');
 
 /** @typedef {BaseNode.Node} Node */
 
@@ -33,6 +34,10 @@ class FirstMeaningfulPaint extends MetricArtifact {
    */
   getOptimisticGraph(dependencyGraph, traceOfTab) {
     const fmp = traceOfTab.timestamps.firstMeaningfulPaint;
+    if (!fmp) {
+      throw new LHError(LHError.errors.NO_FMP);
+    }
+
     const blockingScriptUrls = MetricArtifact.getScriptUrls(dependencyGraph, node => {
       return (
         node.endTime <= fmp && node.hasRenderBlockingPriority() && node.initiatorType !== 'script'
@@ -58,6 +63,10 @@ class FirstMeaningfulPaint extends MetricArtifact {
    */
   getPessimisticGraph(dependencyGraph, traceOfTab) {
     const fmp = traceOfTab.timestamps.firstMeaningfulPaint;
+    if (!fmp) {
+      throw new LHError(LHError.errors.NO_FMP);
+    }
+
     const requiredScriptUrls = MetricArtifact.getScriptUrls(dependencyGraph, node => {
       return node.endTime <= fmp && node.hasRenderBlockingPriority();
     });
diff --git a/lighthouse-core/gather/computed/trace-of-tab.js b/lighthouse-core/gather/computed/trace-of-tab.js
index 696c6fbe4..a3addf08e 100644
--- a/lighthouse-core/gather/computed/trace-of-tab.js
+++ b/lighthouse-core/gather/computed/trace-of-tab.js
@@ -22,11 +22,6 @@ const TracingProcessor = require('../../lib/traces/tracing-processor');
 const LHError = require('../../lib/errors');
 const Sentry = require('../../lib/sentry');
 
-// Bring in web-inspector for side effect of adding [].stableSort
-// See https://github.com/GoogleChrome/lighthouse/pull/2415
-// eslint-disable-next-line no-unused-vars
-const NetworkRequest = require('../../lib/network-request');
-
 class TraceOfTab extends ComputedArtifact {
   get name() {
     return 'TraceOfTab';
@@ -90,10 +85,11 @@ class TraceOfTab extends ComputedArtifact {
     // Find our first paint of this frame
     const firstPaint = frameEvents.find(e => e.name === 'firstPaint' && e.ts > navigationStart.ts);
 
-    // FCP will follow at/after the FP
+    // FCP will follow at/after the FP. Used in so many places we require it.
     const firstContentfulPaint = frameEvents.find(
       e => e.name === 'firstContentfulPaint' && e.ts > navigationStart.ts
     );
+    if (!firstContentfulPaint) throw new LHError(LHError.errors.NO_FCP);
 
     // fMP will follow at/after the FP
     let firstMeaningfulPaint = frameEvents.find(
@@ -133,34 +129,44 @@ class TraceOfTab extends ComputedArtifact {
     const mainThreadEvents = processEvents
       .filter(e => e.tid === startedInPageEvt.tid);
 
+    // traceEnd must exist since at least navigationStart event was verified as existing.
     const traceEnd = trace.traceEvents.reduce((max, evt) => {
       return max.ts > evt.ts ? max : evt;
     });
-
-    const metrics = {
-      navigationStart,
-      firstPaint,
-      firstContentfulPaint,
-      firstMeaningfulPaint,
-      traceEnd: {ts: traceEnd.ts + (traceEnd.dur || 0)},
-      load,
-      domContentLoaded,
+    const fakeEndOfTraceEvt = {ts: traceEnd.ts + (traceEnd.dur || 0)};
+
+    /** @param {{ts: number}=} event */
+    const getTimestamp = (event) => event && event.ts;
+    /** @type {LH.Artifacts.TraceTimes} */
+    const timestamps = {
+      navigationStart: navigationStart.ts,
+      firstPaint: getTimestamp(firstPaint),
+      firstContentfulPaint: firstContentfulPaint.ts,
+      firstMeaningfulPaint: getTimestamp(firstMeaningfulPaint),
+      traceEnd: fakeEndOfTraceEvt.ts,
+      load: getTimestamp(load),
+      domContentLoaded: getTimestamp(domContentLoaded),
     };
 
-    const timings = {};
-    const timestamps = {};
 
-    Object.keys(metrics).forEach(metric => {
-      timestamps[metric] = metrics[metric] && metrics[metric].ts;
-      timings[metric] = (timestamps[metric] - navigationStart.ts) / 1000;
-    });
+    /** @param {number} ts */
+    const getTiming = (ts) => (ts - navigationStart.ts) / 1000;
+    /** @param {number=} ts */
+    const maybeGetTiming = (ts) => ts === undefined ? undefined : getTiming(ts);
+    /** @type {LH.Artifacts.TraceTimes} */
+    const timings = {
+      navigationStart: 0,
+      firstPaint: maybeGetTiming(timestamps.firstPaint),
+      firstContentfulPaint: getTiming(timestamps.firstContentfulPaint),
+      firstMeaningfulPaint: maybeGetTiming(timestamps.firstMeaningfulPaint),
+      traceEnd: getTiming(timestamps.traceEnd),
+      load: maybeGetTiming(timestamps.load),
+      domContentLoaded: maybeGetTiming(timestamps.domContentLoaded),
+    };
 
-    // @ts-ignore - TODO(bckenny): many of these are actually `|undefined`, but
-    // undefined case needs to be handled throughout codebase. See also note for
-    // LH.Artifacts.TraceOfTab.
     return {
-      timings: /** @type {LH.Artifacts.TraceTimes} */ (timings),
-      timestamps: /** @type {LH.Artifacts.TraceTimes} */ (timestamps),
+      timings,
+      timestamps,
       processEvents,
       mainThreadEvents,
       startedInPageEvt,
diff --git a/lighthouse-core/gather/gatherers/manifest.js b/lighthouse-core/gather/gatherers/manifest.js
index 0cdbf4fc8..ff4268aff 100644
--- a/lighthouse-core/gather/gatherers/manifest.js
+++ b/lighthouse-core/gather/gatherers/manifest.js
@@ -19,7 +19,7 @@ const BOM_FIRSTCHAR = 65279;
 class Manifest extends Gatherer {
   /**
    * Returns the parsed manifest or null if the page had no manifest. If the manifest
-   * was unparseable as JSON, manifest.value will be undefined and manifest.debugString
+   * was unparseable as JSON, manifest.value will be undefined and manifest.warning
    * will have the reason. See manifest-parser.js for more information.
    * @param {LH.Gatherer.PassContext} passContext
    * @return {Promise<LH.Artifacts['Manifest']>}
diff --git a/lighthouse-core/gather/gatherers/start-url.js b/lighthouse-core/gather/gatherers/start-url.js
index c859b23a4..9d7c59ec7 100644
--- a/lighthouse-core/gather/gatherers/start-url.js
+++ b/lighthouse-core/gather/gatherers/start-url.js
@@ -24,23 +24,23 @@ class StartUrl extends Gatherer {
       .then(manifest => {
         const startUrlInfo = this._readManifestStartUrl(manifest);
         if (startUrlInfo.isReadFailure) {
-          return {statusCode: -1, debugString: startUrlInfo.reason};
+          return {statusCode: -1, explanation: startUrlInfo.reason};
         }
 
         return this._attemptManifestFetch(passContext.driver, startUrlInfo.startUrl);
       }).catch(() => {
-        return {statusCode: -1, debugString: 'Unable to fetch start URL via service worker'};
+        return {statusCode: -1, explanation: 'Unable to fetch start URL via service worker'};
       });
   }
 
   /**
    * Read the parsed manifest and return failure reasons or the startUrl
-   * @param {?{value?: {start_url: {value?: string, debugString?: string}}, debugString?: string}} manifest
+   * @param {?{value?: {start_url: {value?: string, warning?: string}}, warning?: string}} manifest
    * @return {{isReadFailure: true, reason: string}|{isReadFailure: false, startUrl: string}}
    */
   _readManifestStartUrl(manifest) {
     if (!manifest || !manifest.value) {
-      const detailedMsg = manifest && manifest.debugString;
+      const detailedMsg = manifest && manifest.warning;
 
       if (detailedMsg) {
         return {isReadFailure: true, reason: `Error fetching web app manifest: ${detailedMsg}`};
@@ -50,12 +50,12 @@ class StartUrl extends Gatherer {
     }
 
     // Even if the start URL had an error, the browser will still supply a fallback URL.
-    // Therefore, we only set the debugString here and continue with the fetch.
-    if (manifest.value.start_url.debugString) {
-      return {isReadFailure: true, reason: manifest.value.start_url.debugString};
+    // Therefore, we only set the warning here and continue with the fetch.
+    if (manifest.value.start_url.warning) {
+      return {isReadFailure: true, reason: manifest.value.start_url.warning};
     }
 
-    // @ts-ignore - TODO(bckenny): should actually be testing value above, not debugString
+    // @ts-ignore - TODO(bckenny): should actually be testing value above, not warning
     return {isReadFailure: false, startUrl: manifest.value.start_url.value};
   }
 
@@ -64,13 +64,13 @@ class StartUrl extends Gatherer {
    * Resolves when we have a matched network request
    * @param {Driver} driver
    * @param {string} startUrl
-   * @return {Promise<{statusCode: number, debugString: string}>}
+   * @return {Promise<{statusCode: number, explanation: string}>}
    */
   _attemptManifestFetch(driver, startUrl) {
     // Wait up to 3s to get a matched network request from the fetch() to work
     const timeoutPromise = new Promise(resolve =>
       setTimeout(
-        () => resolve({statusCode: -1, debugString: 'Timed out waiting for fetched start_url'}),
+        () => resolve({statusCode: -1, explanation: 'Timed out waiting for fetched start_url'}),
         3000
       )
     );
@@ -88,7 +88,7 @@ class StartUrl extends Gatherer {
         if (!response.fromServiceWorker) {
           return resolve({
             statusCode: -1,
-            debugString: 'Unable to fetch start URL via service worker',
+            explanation: 'Unable to fetch start URL via service worker',
           });
         }
         // Successful SW-served fetch of the start_URL
diff --git a/lighthouse-core/lib/manifest-parser.js b/lighthouse-core/lib/manifest-parser.js
index 909781828..17b208af8 100644
--- a/lighthouse-core/lib/manifest-parser.js
+++ b/lighthouse-core/lib/manifest-parser.js
@@ -37,13 +37,13 @@ const ALLOWED_ORIENTATION_VALUES = [
  */
 function parseString(raw, trim) {
   let value;
-  let debugString;
+  let warning;
 
   if (typeof raw === 'string') {
     value = trim ? raw.trim() : raw;
   } else {
     if (raw !== undefined) {
-      debugString = 'ERROR: expected a string.';
+      warning = 'ERROR: expected a string.';
     }
     value = undefined;
   }
@@ -51,7 +51,7 @@ function parseString(raw, trim) {
   return {
     raw,
     value,
-    debugString,
+    warning,
   };
 }
 
@@ -70,7 +70,7 @@ function parseColor(raw) {
   const validatedColor = validateColor(color.raw);
   if (!validatedColor) {
     color.value = undefined;
-    color.debugString = 'ERROR: color parsing failed.';
+    color.warning = 'ERROR: color parsing failed.';
   }
 
   return color;
@@ -117,7 +117,7 @@ function parseStartUrl(jsonInput, manifestUrl, documentUrl) {
     return {
       raw,
       value: documentUrl,
-      debugString: 'ERROR: start_url string empty',
+      warning: 'ERROR: start_url string empty',
     };
   }
   const parsedAsString = parseString(raw);
@@ -135,7 +135,7 @@ function parseStartUrl(jsonInput, manifestUrl, documentUrl) {
     return {
       raw,
       value: documentUrl,
-      debugString: 'ERROR: invalid start_url relative to ${manifestUrl}',
+      warning: 'ERROR: invalid start_url relative to ${manifestUrl}',
     };
   }
 
@@ -144,7 +144,7 @@ function parseStartUrl(jsonInput, manifestUrl, documentUrl) {
     return {
       raw,
       value: documentUrl,
-      debugString: 'ERROR: start_url must be same-origin as document',
+      warning: 'ERROR: start_url must be same-origin as document',
     };
   }
 
@@ -165,7 +165,7 @@ function parseDisplay(jsonInput) {
     return {
       raw: jsonInput,
       value: DEFAULT_DISPLAY_MODE,
-      debugString: parsedString.debugString,
+      warning: parsedString.warning,
     };
   }
 
@@ -174,7 +174,7 @@ function parseDisplay(jsonInput) {
     return {
       raw: jsonInput,
       value: DEFAULT_DISPLAY_MODE,
-      debugString: 'ERROR: \'display\' has invalid value ' + displayValue +
+      warning: 'ERROR: \'display\' has invalid value ' + displayValue +
         `. will fall back to ${DEFAULT_DISPLAY_MODE}.`,
     };
   }
@@ -182,7 +182,7 @@ function parseDisplay(jsonInput) {
   return {
     raw: jsonInput,
     value: displayValue,
-    debugString: undefined,
+    warning: undefined,
   };
 }
 
@@ -195,7 +195,7 @@ function parseOrientation(jsonInput) {
   if (orientation.value &&
       !ALLOWED_ORIENTATION_VALUES.includes(orientation.value.toLowerCase())) {
     orientation.value = undefined;
-    orientation.debugString = 'ERROR: \'orientation\' has an invalid value, will be ignored.';
+    orientation.warning = 'ERROR: \'orientation\' has an invalid value, will be ignored.';
   }
 
   return orientation;
@@ -223,13 +223,13 @@ function parseIcon(raw, manifestUrl) {
     raw: raw.density,
     value: 1,
     /** @type {string|undefined} */
-    debugString: undefined,
+    warning: undefined,
   };
   if (density.raw !== undefined) {
     density.value = parseFloat(density.raw);
     if (isNaN(density.value) || !isFinite(density.value) || density.value <= 0) {
       density.value = 1;
-      density.debugString = 'ERROR: icon density cannot be NaN, +∞, or less than or equal to +0.';
+      density.warning = 'ERROR: icon density cannot be NaN, +∞, or less than or equal to +0.';
     }
   }
 
@@ -242,7 +242,7 @@ function parseIcon(raw, manifestUrl) {
     sizes = {
       raw: raw.sizes,
       value: set.size > 0 ? Array.from(set) : undefined,
-      debugString: undefined,
+      warning: undefined,
     };
   } else {
     sizes = {...parsedSizes, value: undefined};
@@ -256,7 +256,7 @@ function parseIcon(raw, manifestUrl) {
       density,
       sizes,
     },
-    debugString: undefined,
+    warning: undefined,
   };
 }
 
@@ -272,7 +272,7 @@ function parseIcons(jsonInput, manifestUrl) {
       raw,
       /** @type {Array<ReturnType<typeof parseIcon>>} */
       value: [],
-      debugString: undefined,
+      warning: undefined,
     };
   }
 
@@ -281,7 +281,7 @@ function parseIcons(jsonInput, manifestUrl) {
       raw,
       /** @type {Array<ReturnType<typeof parseIcon>>} */
       value: [],
-      debugString: 'ERROR: \'icons\' expected to be an array but is not.',
+      warning: 'ERROR: \'icons\' expected to be an array but is not.',
     };
   }
 
@@ -298,7 +298,7 @@ function parseIcons(jsonInput, manifestUrl) {
   return {
     raw,
     value,
-    debugString: undefined,
+    warning: undefined,
   };
 }
 
@@ -317,7 +317,7 @@ function parseApplication(raw) {
       appUrl.value = new URL(appUrl.value).href;
     } catch (e) {
       appUrl.value = undefined;
-      appUrl.debugString = 'ERROR: invalid application URL ${raw.url}';
+      appUrl.warning = 'ERROR: invalid application URL ${raw.url}';
     }
   }
 
@@ -328,7 +328,7 @@ function parseApplication(raw) {
       id,
       url: appUrl,
     },
-    debugString: undefined,
+    warning: undefined,
   };
 }
 
@@ -342,7 +342,7 @@ function parseRelatedApplications(jsonInput) {
     return {
       raw,
       value: undefined,
-      debugString: undefined,
+      warning: undefined,
     };
   }
 
@@ -350,7 +350,7 @@ function parseRelatedApplications(jsonInput) {
     return {
       raw,
       value: undefined,
-      debugString: 'ERROR: \'related_applications\' expected to be an array but is not.',
+      warning: 'ERROR: \'related_applications\' expected to be an array but is not.',
     };
   }
 
@@ -364,7 +364,7 @@ function parseRelatedApplications(jsonInput) {
   return {
     raw,
     value,
-    debugString: undefined,
+    warning: undefined,
   };
 }
 
@@ -374,13 +374,13 @@ function parseRelatedApplications(jsonInput) {
 function parsePreferRelatedApplications(jsonInput) {
   const raw = jsonInput.prefer_related_applications;
   let value;
-  let debugString;
+  let warning;
 
   if (typeof raw === 'boolean') {
     value = raw;
   } else {
     if (raw !== undefined) {
-      debugString = 'ERROR: \'prefer_related_applications\' expected to be a boolean.';
+      warning = 'ERROR: \'prefer_related_applications\' expected to be a boolean.';
     }
     value = undefined;
   }
@@ -388,7 +388,7 @@ function parsePreferRelatedApplications(jsonInput) {
   return {
     raw,
     value,
-    debugString,
+    warning,
   };
 }
 
@@ -425,7 +425,7 @@ function parse(string, manifestUrl, documentUrl) {
     return {
       raw: string,
       value: undefined,
-      debugString: 'ERROR: file isn\'t valid JSON: ' + e,
+      warning: 'ERROR: file isn\'t valid JSON: ' + e,
     };
   }
 
@@ -447,7 +447,7 @@ function parse(string, manifestUrl, documentUrl) {
   return {
     raw: string,
     value: manifest,
-    debugString: undefined,
+    warning: undefined,
   };
 }
 
diff --git a/typings/artifacts.d.ts b/typings/artifacts.d.ts
index 9f3103892..742aaa6dc 100644
--- a/typings/artifacts.d.ts
+++ b/typings/artifacts.d.ts
@@ -103,8 +103,8 @@ declare global {
       Scripts: Record<string, string>;
       /** Version information for all ServiceWorkers active after the first page load. */
       ServiceWorker: {versions: Crdp.ServiceWorker.ServiceWorkerVersion[]};
-      /** The status of an offline fetch of the page's start_url. -1 and a debugString if missing or there was an error. */
-      StartUrl: {statusCode: number, debugString?: string};
+      /** The status of an offline fetch of the page's start_url. -1 and a explanation if missing or there was an error. */
+      StartUrl: {statusCode: number, explanation?: string};
       /** Information on <script> and <link> tags blocking first paint. */
       TagsBlockingFirstPaint: Artifacts.TagBlockingFirstPaint[];
       /** The value of the <meta name="theme=color">'s content attribute, or null. */
@@ -365,30 +365,43 @@ declare global {
 
       export type Speedline = speedline.Output<'speedIndex'>;
 
-      // TODO(bckenny): all but navigationStart could actually be undefined.
       export interface TraceTimes {
         navigationStart: number;
-        firstPaint: number;
+        firstPaint?: number;
         firstContentfulPaint: number;
-        firstMeaningfulPaint: number;
+        firstMeaningfulPaint?: number;
         traceEnd: number;
-        load: number;
-        domContentLoaded: number;
+        load?: number;
+        domContentLoaded?: number;
       }
 
-      // TODO(bckenny): events other than started and navStart could be undefined.
       export interface TraceOfTab {
-        timings: TraceTimes;
+        /** The raw timestamps of key metric events, in microseconds. */
         timestamps: TraceTimes;
+        /** The relative times from navigationStart to key metric events, in milliseconds. */
+        timings: TraceTimes;
+        /** The subset of trace events from the page's process, sorted by timestamp. */
         processEvents: Array<TraceEvent>;
+        /** The subset of trace events from the page's main thread, sorted by timestamp. */
         mainThreadEvents: Array<TraceEvent>;
+        /** The event marking the start of tracing in the target browser. */
         startedInPageEvt: TraceEvent;
+        /** The trace event marking navigationStart. */
         navigationStartEvt: TraceEvent;
-        firstPaintEvt: TraceEvent;
+        /** The trace event marking firstPaint, if it was found. */
+        firstPaintEvt?: TraceEvent;
+        /** The trace event marking firstContentfulPaint, if it was found. */
         firstContentfulPaintEvt: TraceEvent;
-        firstMeaningfulPaintEvt: TraceEvent;
-        loadEvt: TraceEvent;
-        domContentLoadedEvt: TraceEvent;
+        /** The trace event marking firstMeaningfulPaint, if it was found. */
+        firstMeaningfulPaintEvt?: TraceEvent;
+        /** The trace event marking loadEventEnd, if it was found. */
+        loadEvt?: TraceEvent;
+        /** The trace event marking domContentLoadedEventEnd, if it was found. */
+        domContentLoadedEvt?: TraceEvent;
+        /**
+         * Whether the firstMeaningfulPaintEvt was the definitive event or a fallback to
+         * firstMeaningfulPaintCandidate events had to be attempted.
+         */
         fmpFellBack: boolean;
       }
     }


Test patch:
diff --git a/lighthouse-core/test/audits/bootup-time-test.js b/lighthouse-core/test/audits/bootup-time-test.js
index 3a3117af0..420a9afd7 100644
--- a/lighthouse-core/test/audits/bootup-time-test.js
+++ b/lighthouse-core/test/audits/bootup-time-test.js
@@ -12,7 +12,7 @@ const assert = require('assert');
 
 const acceptableTrace = require('../fixtures/traces/progressive-app-m60.json');
 const acceptableDevtoolsLogs = require('../fixtures/traces/progressive-app-m60.devtools.log.json');
-const errorTrace = require('../fixtures/traces/airhorner_no_fcp.json');
+const errorTrace = require('../fixtures/traces/no_fmp_event.json');
 
 describe('Performance: bootup-time audit', () => {
   const auditOptions = Object.assign({}, BootupTime.defaultOptions, {thresholdInMs: 10});
diff --git a/lighthouse-core/test/audits/mainthread-work-breakdown-test.js b/lighthouse-core/test/audits/mainthread-work-breakdown-test.js
index 7061f9881..5890bab7c 100644
--- a/lighthouse-core/test/audits/mainthread-work-breakdown-test.js
+++ b/lighthouse-core/test/audits/mainthread-work-breakdown-test.js
@@ -14,7 +14,7 @@ const options = PageExecutionTimings.defaultOptions;
 const acceptableTrace = require('../fixtures/traces/progressive-app-m60.json');
 const siteWithRedirectTrace = require('../fixtures/traces/site-with-redirect.json');
 const loadTrace = require('../fixtures/traces/load.json');
-const errorTrace = require('../fixtures/traces/airhorner_no_fcp.json');
+const errorTrace = require('../fixtures/traces/no_fmp_event.json');
 
 const acceptableTraceExpectations = {
   parseHTML: 14,
diff --git a/lighthouse-core/test/audits/webapp-install-banner-test.js b/lighthouse-core/test/audits/webapp-install-banner-test.js
index 6043dd569..48b3ce501 100644
--- a/lighthouse-core/test/audits/webapp-install-banner-test.js
+++ b/lighthouse-core/test/audits/webapp-install-banner-test.js
@@ -152,7 +152,7 @@ describe('PWA: webapp install banner audit', () => {
 
   it('includes warning from start_url', () => {
     const artifacts = generateMockArtifacts();
-    artifacts.StartUrl = {statusCode: 200, debugString: 'Warning!'};
+    artifacts.StartUrl = {statusCode: 200, explanation: 'Warning!'};
 
     return WebappInstallBannerAudit.audit(artifacts).then(result => {
       assert.strictEqual(result.rawValue, true);
diff --git a/lighthouse-core/test/fixtures/traces/backgrounded-tab-missing-paints.json b/lighthouse-core/test/fixtures/traces/backgrounded-tab-missing-paints.json
index 49557d5a2..d903ac00a 100644
--- a/lighthouse-core/test/fixtures/traces/backgrounded-tab-missing-paints.json
+++ b/lighthouse-core/test/fixtures/traces/backgrounded-tab-missing-paints.json
@@ -91,7 +91,7 @@
       "cat": "blink.user_timing,rail",
       "name": "firstContentfulPaint",
       "args": {
-        "frame": "0x1aff390e1e30"
+        "frame": "0x53965941e30"
       },
       "tts": 239094,
       "s": "p"
diff --git a/lighthouse-core/test/fixtures/traces/load.json b/lighthouse-core/test/fixtures/traces/load.json
index 9106ffe97..0ff31c49d 100644
--- a/lighthouse-core/test/fixtures/traces/load.json
+++ b/lighthouse-core/test/fixtures/traces/load.json
@@ -509,6 +509,7 @@
 {"pid":442,"tid":461,"ts":73609802087,"ph":"X","cat":"toplevel","name":"TaskQueueManager::ProcessTaskFromWorkQueue","args":{"src_file":"../../ipc/ipc_channel_proxy.cc","src_func":"OnMessageReceivedNoFilter"},"dur":5851,"tdur":666,"tts":20275155},
 {"pid":442,"tid":461,"ts":73609804410,"ph":"I","cat":"disabled-by-default-devtools.timeline","name":"TracingStartedInPage","args":{"data":{"sessionId":"442.138","page":"0xffffffffa1a94000"}},"tts":20275707,"s":"t"},
 {"pid":442,"tid":461,"ts":73609804410,"ph":"R","cat":"blink.user_timing","name":"navigationStart","args":{"data":{"sessionId":"442.138"},"frame":"0xffffffffa1a94000"},"tts":20275707,"s":"t"},
+{"pid":442,"tid":461,"ts":73609804428,"ph": "R","cat":"loading,rail,devtools.timeline","name": "firstContentfulPaint","args":{"frame":"0xffffffffa1a94000"},"tts":20275720},
 {"pid":442,"tid":461,"ts":73609804428,"ph":"I","cat":"disabled-by-default-devtools.timeline","name":"SetLayerTreeId","args":{"data":{"sessionId":"442.138","layerTreeId":1}},"tts":20275720,"s":"t"},
 {"pid":442,"tid":461,"ts":73609807946,"ph":"X","cat":"toplevel","name":"TaskQueueManager::ProcessTaskFromWorkQueue","args":{"src_file":"../../media/audio/fake_audio_worker.cc","src_func":"DoRead"},"dur":88,"tdur":85,"tts":20275831},
 {"pid":442,"tid":461,"ts":73609808041,"ph":"X","cat":"toplevel","name":"TaskQueueManager::ProcessTaskFromWorkQueue","args":{"src_file":"../../ipc/ipc_channel_proxy.cc","src_func":"OnMessageReceivedNoFilter"},"dur":193,"tdur":116,"tts":20275927},
diff --git a/lighthouse-core/test/fixtures/traces/no_navstart_event.json b/lighthouse-core/test/fixtures/traces/no_navstart_event.json
new file mode 100644
index 000000000..b1865c461
--- /dev/null
+++ b/lighthouse-core/test/fixtures/traces/no_navstart_event.json
@@ -0,0 +1,77 @@
+{
+  "traceEvents": [
+    {
+      "pid": 77472,
+      "tid": 775,
+      "ts": 2146735802456,
+      "ph": "I",
+      "cat": "disabled-by-default-devtools.timeline",
+      "name": "TracingStartedInPage",
+      "args": {
+        "data": {
+          "frames": [
+            {
+              "frame": "0x150343381dd0",
+              "name": "",
+              "url": "about:blank"
+            }
+          ],
+          "page": "0x150343381dd0",
+          "sessionId": "77472.1"
+        }
+      },
+      "tts": 341067,
+      "s": "t"
+    },
+    {
+      "pid": 77472,
+      "tid": 775,
+      "ts": 2146737301957,
+      "ph": "I",
+      "cat": "blink.user_timing,rail",
+      "name": "firstPaint",
+      "args": {
+        "frame": "0x150343381dd0"
+      },
+      "tts": 549087,
+      "s": "p"
+    },
+    {
+      "pid": 77472,
+      "tid": 775,
+      "ts": 2146737302468,
+      "ph": "R",
+      "cat": "blink.user_timing,rail",
+      "name": "firstImagePaint",
+      "args": {
+        "frame": "0x150343381dd0"
+      },
+      "tts": 549603
+    },
+    {
+      "pid": 77472,
+      "tid": 775,
+      "ts": 2146737302468,
+      "ph": "I",
+      "cat": "blink.user_timing,rail",
+      "name": "firstContentfulPaint",
+      "args": {
+        "frame": "0x150343381dd0"
+      },
+      "tts": 549599,
+      "s": "p"
+    },
+    {
+      "pid": 77472,
+      "tid": 775,
+      "ts": 2146740268666,
+      "ph": "R",
+      "cat": "loading",
+      "name": "firstMeaningfulPaintCandidate",
+      "args": {
+        "frame": "0x150343381dd0"
+      },
+      "tts": 1009771
+    }
+  ]
+}
\ No newline at end of file
diff --git a/lighthouse-core/test/fixtures/traces/threeframes-blank_content_more.json b/lighthouse-core/test/fixtures/traces/threeframes-blank_content_more.json
index ab528381c..a58b3c2db 100644
--- a/lighthouse-core/test/fixtures/traces/threeframes-blank_content_more.json
+++ b/lighthouse-core/test/fixtures/traces/threeframes-blank_content_more.json
@@ -45,6 +45,18 @@
 				"tdur": 186,
 				"tts": 4516530
 			},
+			{
+				"pid": 93449,
+				"tid": 1295,
+				"ts": 900001000000,
+				"ph": "R",
+				"cat": "loading,rail,devtools.timeline",
+				"name": "firstContentfulPaint",
+				"args": {
+					"frame": "dummy"
+				},
+				"tts": 63259760
+			},
 			{
 				"pid": 93267,
 				"tid": 1295,
diff --git a/lighthouse-core/test/gather/computed/main-thread-tasks-test.js b/lighthouse-core/test/gather/computed/main-thread-tasks-test.js
index 2f2ac83fd..79518a7b3 100644
--- a/lighthouse-core/test/gather/computed/main-thread-tasks-test.js
+++ b/lighthouse-core/test/gather/computed/main-thread-tasks-test.js
@@ -23,6 +23,7 @@ describe('MainResource computed artifact', () => {
     boilerplateTrace = [
       {ph: 'I', name: 'TracingStartedInPage', ts: baseTs, args},
       {ph: 'I', name: 'navigationStart', ts: baseTs, args},
+      {ph: 'R', name: 'firstContentfulPaint', ts: baseTs + 1, args},
     ];
     computedArtifacts = Runner.instantiateComputedArtifacts();
   });
@@ -79,7 +80,7 @@ describe('MainResource computed artifact', () => {
       attributableURLs: [],
 
       children: [taskB],
-      event: traceEvents[2],
+      event: traceEvents[3],
       startTime: 0,
       endTime: 100,
       duration: 100,
@@ -92,7 +93,7 @@ describe('MainResource computed artifact', () => {
       attributableURLs: [],
 
       children: [taskC],
-      event: traceEvents[3],
+      event: traceEvents[4],
       startTime: 5,
       endTime: 55,
       duration: 50,
diff --git a/lighthouse-core/test/gather/computed/metrics/first-cpu-idle-test.js b/lighthouse-core/test/gather/computed/metrics/first-cpu-idle-test.js
index 4d0602972..f5ff6b61c 100644
--- a/lighthouse-core/test/gather/computed/metrics/first-cpu-idle-test.js
+++ b/lighthouse-core/test/gather/computed/metrics/first-cpu-idle-test.js
@@ -95,6 +95,7 @@ describe('FirstInteractive computed artifact:', () => {
         computeObservedMetric({
           timings: {
             firstMeaningfulPaint: 3400,
+            domContentLoaded: 2000,
             traceEnd: 4500,
           },
           timestamps: {
diff --git a/lighthouse-core/test/gather/computed/metrics/first-meaningful-paint-test.js b/lighthouse-core/test/gather/computed/metrics/first-meaningful-paint-test.js
index 278a1bf29..dd57e63f3 100644
--- a/lighthouse-core/test/gather/computed/metrics/first-meaningful-paint-test.js
+++ b/lighthouse-core/test/gather/computed/metrics/first-meaningful-paint-test.js
@@ -16,7 +16,6 @@ const badNavStartTrace = require(`${TRACE_FIXTURES}/bad-nav-start-ts.json`);
 const lateTracingStartedTrace = require(`${TRACE_FIXTURES}/tracingstarted-after-navstart.json`);
 const preactTrace = require(`${TRACE_FIXTURES}/preactjs.com_ts_of_undefined.json`);
 const noFMPtrace = require(`${TRACE_FIXTURES}/no_fmp_event.json`);
-const noFCPtrace = require(`${TRACE_FIXTURES}/airhorner_no_fcp.json`);
 
 /* eslint-env jest */
 
@@ -98,12 +97,4 @@ describe('Metrics: FMP', () => {
     assert.equal(Math.round(result.timing), 4461);
     assert.equal(result.timestamp, 2146740268666);
   });
-
-  it('handles cases when no FCP exists', async () => {
-    trace = noFCPtrace;
-    addEmptyTask();
-    const result = await artifacts.requestFirstMeaningfulPaint({trace, devtoolsLog, settings});
-    assert.equal(Math.round(result.timing), 482);
-    assert.equal(result.timestamp, 2149509604903);
-  });
 });
diff --git a/lighthouse-core/test/gather/computed/trace-of-tab-test.js b/lighthouse-core/test/gather/computed/trace-of-tab-test.js
index f1b2f4687..ea5e624b5 100644
--- a/lighthouse-core/test/gather/computed/trace-of-tab-test.js
+++ b/lighthouse-core/test/gather/computed/trace-of-tab-test.js
@@ -15,6 +15,7 @@ const lateTracingStartedTrace = require('../../fixtures/traces/tracingstarted-af
 const preactTrace = require('../../fixtures/traces/preactjs.com_ts_of_undefined.json');
 const noFMPtrace = require('../../fixtures/traces/no_fmp_event.json');
 const noFCPtrace = require('../../fixtures/traces/airhorner_no_fcp');
+const noNavStartTrace = require('../../fixtures/traces/no_navstart_event');
 const backgroundTabTrace = require('../../fixtures/traces/backgrounded-tab-missing-paints');
 
 /* eslint-env jest */
@@ -89,15 +90,6 @@ describe('Trace of Tab computed artifact:', () => {
     });
   });
 
-  it('handles traces missing an FCP', async () => {
-    const trace = await traceOfTab.compute_(noFCPtrace);
-    assert.equal(trace.startedInPageEvt.ts, 2149509117532, 'bad tracingstartedInPage');
-    assert.equal(trace.navigationStartEvt.ts, 2149509122585, 'bad navStart');
-    assert.equal(trace.firstContentfulPaintEvt, undefined, 'bad fcp');
-    assert.equal(trace.firstMeaningfulPaintEvt.ts, 2149509604903, 'bad fmp');
-    assert.ok(trace.fmpFellBack);
-  });
-
   it('handles traces missing a paints (captured in background tab)', async () => {
     const trace = await traceOfTab.compute_(backgroundTabTrace);
     assert.equal(trace.startedInPageEvt.ts, 1966813248134);
@@ -108,7 +100,6 @@ describe('Trace of Tab computed artifact:', () => {
       1966813258737,
       'didnt select navStart event with same timestamp as usertiming measure'
     );
-    assert.equal(trace.firstContentfulPaintEvt, undefined, 'bad fcp');
     assert.equal(trace.firstMeaningfulPaintEvt, undefined, 'bad fmp');
   });
 
@@ -147,6 +138,17 @@ describe('Trace of Tab computed artifact:', () => {
           },
         },
         'tts': 141371,
+      }, {
+        'pid': 69920,
+        'tid': 1,
+        'ts': 2193564790060,
+        'ph': 'R',
+        'cat': 'loading,rail,devtools.timeline',
+        'name': 'firstContentfulPaint',
+        'args': {
+          'frame': 'B192D1F3355A6F961EC8F0B01623C1FB',
+        },
+        'tts': 141372,
       }, {
         'pid': 69920,
         'tid': 1,
@@ -190,4 +192,16 @@ describe('Trace of Tab computed artifact:', () => {
       assert.deepStrictEqual(sortedEvents, tsGroup);
     }
   });
+
+  it('throws on traces missing a navigationStart', () => {
+    return traceOfTab.compute_(noNavStartTrace)
+      .then(_ => assert(false, 'NO_NAVSTART error not throw'),
+        err => assert.equal(err.message, 'NO_NAVSTART'));
+  });
+
+  it('throws on traces missing an FCP', () => {
+    return traceOfTab.compute_(noFCPtrace)
+      .then(_ => assert(false, 'NO_FCP error not throw'),
+        err => assert.equal(err.message, 'NO_FCP'));
+  });
 });
diff --git a/lighthouse-core/test/gather/gatherers/manifest-test.js b/lighthouse-core/test/gather/gatherers/manifest-test.js
index 3fb709ad9..a22813ff9 100644
--- a/lighthouse-core/test/gather/gatherers/manifest-test.js
+++ b/lighthouse-core/test/gather/gatherers/manifest-test.js
@@ -122,7 +122,7 @@ describe('Manifest gatherer', () => {
       url: EXAMPLE_DOC_URL,
     }).then(artifact => {
       assert.ok(typeof artifact.value === 'object');
-      assert.strictEqual(artifact.debugString, undefined);
+      assert.strictEqual(artifact.explanation, undefined);
     });
   });
 });
diff --git a/lighthouse-core/test/gather/gatherers/start-url-test.js b/lighthouse-core/test/gather/gatherers/start-url-test.js
index 18ae8871b..6924a3033 100644
--- a/lighthouse-core/test/gather/gatherers/start-url-test.js
+++ b/lighthouse-core/test/gather/gatherers/start-url-test.js
@@ -77,11 +77,11 @@ describe('Start-url gatherer', () => {
       startUrlGathererWithResponseNotFromSW.afterPass(optionsWithResponseNotFromSW),
     ]).then(([artifact, artifactWithQueryString, artifactWithResponseNotFromSW]) => {
       assert.equal(artifact.statusCode, -1);
-      assert.ok(artifact.debugString, 'did not set debug string');
+      assert.ok(artifact.explanation, 'did not set debug string');
       assert.equal(artifactWithQueryString.statusCode, -1);
-      assert.ok(artifactWithQueryString.debugString, 'did not set debug string');
+      assert.ok(artifactWithQueryString.explanation, 'did not set debug string');
       assert.equal(artifactWithResponseNotFromSW.statusCode, -1);
-      assert.equal(artifactWithResponseNotFromSW.debugString,
+      assert.equal(artifactWithResponseNotFromSW.explanation,
           'Unable to fetch start URL via service worker');
     });
   });
@@ -107,7 +107,7 @@ describe('Start-url gatherer', () => {
     });
   });
 
-  it('returns a debugString when manifest cannot be found', () => {
+  it('returns a explanation when manifest cannot be found', () => {
     const driver = Object.assign({}, mockDriver);
     const startUrlGatherer = new StartUrlGatherer();
     const options = {
@@ -119,12 +119,12 @@ describe('Start-url gatherer', () => {
 
     return startUrlGatherer.afterPass(options, tracingData)
       .then(artifact => {
-        assert.equal(artifact.debugString,
+        assert.equal(artifact.explanation,
           `No usable web app manifest found on page`);
       });
   });
 
-  it('returns a debugString when manifest cannot be parsed', () => {
+  it('returns a explanation when manifest cannot be parsed', () => {
     const driver = Object.assign({}, mockDriver);
     const startUrlGatherer = new StartUrlGatherer();
     const options = {
@@ -139,13 +139,13 @@ describe('Start-url gatherer', () => {
 
     return startUrlGatherer.afterPass(options, tracingData)
       .then(artifact => {
-        assert.equal(artifact.debugString,
+        assert.equal(artifact.explanation,
           `Error fetching web app manifest: ERROR: file isn't valid JSON: ` +
           `SyntaxError: Unexpected token h in JSON at position 1`);
       });
   });
 
-  it('returns a debugString when start_url cannot be found', () => {
+  it('returns a explanation when start_url cannot be found', () => {
     const startUrlGatherer = new StartUrlGatherer();
     const options = {
       url: 'https://ifixit-pwa.appspot.com/',
@@ -154,7 +154,7 @@ describe('Start-url gatherer', () => {
 
     return startUrlGatherer.afterPass(options, tracingData)
       .then(artifact => {
-        assert.equal(artifact.debugString, 'ERROR: start_url string empty');
+        assert.equal(artifact.explanation, 'ERROR: start_url string empty');
       });
   });
 
@@ -167,7 +167,7 @@ describe('Start-url gatherer', () => {
 
     return startUrlGatherer.afterPass(options, tracingData)
       .then(artifact => {
-        assert.equal(artifact.debugString, 'ERROR: start_url must be same-origin as document');
+        assert.equal(artifact.explanation, 'ERROR: start_url must be same-origin as document');
       });
   });
 });
diff --git a/lighthouse-core/test/lib/manifest-parser-test.js b/lighthouse-core/test/lib/manifest-parser-test.js
index 9ea6001af..5a0b37c0b 100644
--- a/lighthouse-core/test/lib/manifest-parser-test.js
+++ b/lighthouse-core/test/lib/manifest-parser-test.js
@@ -27,12 +27,12 @@ function noUrlManifestParser(manifestSrc) {
 describe('Manifest Parser', function() {
   it('should not parse empty string input', function() {
     const parsedManifest = noUrlManifestParser('');
-    assert.ok(parsedManifest.debugString);
+    assert.ok(parsedManifest.warning);
   });
 
   it('accepts empty dictionary', function() {
     const parsedManifest = noUrlManifestParser('{}');
-    assert(!parsedManifest.debugString);
+    assert(!parsedManifest.warning);
     assert.equal(parsedManifest.value.name.value, undefined);
     assert.equal(parsedManifest.value.short_name.value, undefined);
     assert.equal(parsedManifest.value.start_url.value, EXAMPLE_DOC_URL);
@@ -52,38 +52,38 @@ describe('Manifest Parser', function() {
     it('gives an empty array and an error for erroneous icons entry', () => {
       const parsedManifest = manifestParser('{"icons": {"16": "img/icons/icon16.png"}}',
           EXAMPLE_MANIFEST_URL, EXAMPLE_DOC_URL);
-      assert.ok(!parsedManifest.debugString);
+      assert.ok(!parsedManifest.warning);
       const icons = parsedManifest.value.icons;
       assert.ok(Array.isArray(icons.value));
       assert.equal(icons.value.length, 0);
-      assert.ok(icons.debugString);
+      assert.ok(icons.warning);
     });
 
     it('gives an empty array and no error for missing icons entry', () => {
       const parsedManifest = manifestParser('{}',
           EXAMPLE_MANIFEST_URL, EXAMPLE_DOC_URL);
-      assert.ok(!parsedManifest.debugString);
+      assert.ok(!parsedManifest.warning);
       const icons = parsedManifest.value.icons;
       assert.ok(Array.isArray(icons.value));
       assert.equal(icons.value.length, 0);
-      assert.ok(!icons.debugString);
+      assert.ok(!icons.warning);
     });
 
     it('parses basic string', function() {
       const parsedManifest = manifestParser('{"icons": [{"src": "192.png", "sizes": "192x192"}]}',
           EXAMPLE_MANIFEST_URL, EXAMPLE_DOC_URL);
-      assert(!parsedManifest.debugString);
+      assert(!parsedManifest.warning);
       const icons = parsedManifest.value.icons;
-      assert(!icons.debugString);
+      assert(!icons.warning);
       const icon192 = icons.value[0];
-      assert(!icon192.value.sizes.debugString);
+      assert(!icon192.value.sizes.warning);
       assert.equal(icons.value.length, 1);
     });
 
     it('finds four icons in the stub manifest', function() {
       const parsedManifest = manifestParser(JSON.stringify(manifestStub), EXAMPLE_MANIFEST_URL,
           EXAMPLE_DOC_URL);
-      assert(!parsedManifest.debugString);
+      assert(!parsedManifest.warning);
       assert.equal(parsedManifest.value.icons.value.length, 4);
     });
 
@@ -139,23 +139,23 @@ describe('Manifest Parser', function() {
   describe('name parsing', function() {
     it('parses basic string', function() {
       const parsedManifest = noUrlManifestParser('{"name":"foo"}');
-      assert(!parsedManifest.debugString);
+      assert(!parsedManifest.warning);
       assert.equal(parsedManifest.value.name.value, 'foo');
     });
 
     it('trims whitespaces', function() {
       const parsedManifest = noUrlManifestParser('{"name":" foo "}');
-      assert(!parsedManifest.debugString);
+      assert(!parsedManifest.warning);
       assert.equal(parsedManifest.value.name.value, 'foo');
     });
 
     it('doesn\'t parse non-string', function() {
       let parsedManifest = noUrlManifestParser('{"name": {} }');
-      assert(!parsedManifest.debugString);
+      assert(!parsedManifest.warning);
       assert.equal(parsedManifest.value.name.value, undefined);
 
       parsedManifest = noUrlManifestParser('{"name": 42 }');
-      assert(!parsedManifest.debugString);
+      assert(!parsedManifest.warning);
       assert.equal(parsedManifest.value.name.value, undefined);
     });
   });
@@ -163,23 +163,23 @@ describe('Manifest Parser', function() {
   describe('short_name parsing', function() {
     it('parses basic string', function() {
       const parsedManifest = noUrlManifestParser('{"short_name":"foo"}');
-      assert(!parsedManifest.debugString);
+      assert(!parsedManifest.warning);
       assert.equal(parsedManifest.value.short_name.value, 'foo');
     });
 
     it('trims whitespaces', function() {
       const parsedManifest = noUrlManifestParser('{"short_name":" foo "}');
-      assert(!parsedManifest.debugString);
+      assert(!parsedManifest.warning);
       assert.equal(parsedManifest.value.short_name.value, 'foo');
     });
 
     it('doesn\'t parse non-string', function() {
       let parsedManifest = noUrlManifestParser('{"short_name": {} }');
-      assert(!parsedManifest.debugString);
+      assert(!parsedManifest.warning);
       assert.equal(parsedManifest.value.short_name.value, undefined);
 
       parsedManifest = noUrlManifestParser('{"short_name": 42 }');
-      assert(!parsedManifest.debugString);
+      assert(!parsedManifest.warning);
       assert.equal(parsedManifest.value.short_name.value, undefined);
     });
   });
@@ -199,7 +199,7 @@ describe('Manifest Parser', function() {
 
       const parsedManifest = manifestParser(manifestSrc, manifestUrl, docUrl);
       const parsedUrl = parsedManifest.value.start_url;
-      assert.ok(parsedUrl.debugString);
+      assert.ok(parsedUrl.warning);
       assert.equal(parsedUrl.value, docUrl);
     });
 
@@ -212,7 +212,7 @@ describe('Manifest Parser', function() {
 
       const parsedManifest = manifestParser(manifestSrc, manifestUrl, docUrl);
       const parsedUrl = parsedManifest.value.start_url;
-      assert.ok(parsedUrl.debugString);
+      assert.ok(parsedUrl.warning);
       assert.equal(parsedUrl.value, docUrl);
     });
 
@@ -225,7 +225,7 @@ describe('Manifest Parser', function() {
 
       const parsedManifest = manifestParser(manifestSrc, manifestUrl, docUrl);
       const parsedUrl = parsedManifest.value.start_url;
-      assert.ok(parsedUrl.debugString);
+      assert.ok(parsedUrl.warning);
       assert.equal(parsedUrl.value, docUrl);
     });
 
@@ -236,7 +236,7 @@ describe('Manifest Parser', function() {
 
       const parsedManifest = manifestParser(manifestSrc, manifestUrl, docUrl);
       const parsedUrl = parsedManifest.value.start_url;
-      assert.ok(!parsedUrl.debugString);
+      assert.ok(!parsedUrl.warning);
       assert.equal(parsedUrl.value, docUrl);
     });
 
@@ -251,7 +251,7 @@ describe('Manifest Parser', function() {
 
       const parsedManifest = manifestParser(manifestSrc, manifestUrl, docUrl);
       const parsedUrl = parsedManifest.value.start_url;
-      assert.ok(parsedUrl.debugString);
+      assert.ok(parsedUrl.warning);
       assert.equal(parsedUrl.value, docUrl);
     });
 
@@ -265,7 +265,7 @@ describe('Manifest Parser', function() {
 
       const parsedManifest = manifestParser(manifestSrc, manifestUrl, docUrl);
       const parsedUrl = parsedManifest.value.start_url;
-      assert.ok(parsedUrl.debugString);
+      assert.ok(parsedUrl.warning);
       assert.equal(parsedUrl.value, docUrl);
     });
 
@@ -278,7 +278,7 @@ describe('Manifest Parser', function() {
 
       const parsedManifest = manifestParser(manifestSrc, manifestUrl, docUrl);
       const parsedUrl = parsedManifest.value.start_url;
-      assert.ok(parsedUrl.debugString);
+      assert.ok(parsedUrl.warning);
       assert.equal(parsedUrl.value, docUrl);
     });
 
@@ -291,7 +291,7 @@ describe('Manifest Parser', function() {
 
       const parsedManifest = manifestParser(manifestSrc, manifestUrl, docUrl);
       const parsedUrl = parsedManifest.value.start_url;
-      assert.ok(parsedUrl.debugString);
+      assert.ok(parsedUrl.warning);
       assert.equal(parsedUrl.value, docUrl);
     });
 
@@ -304,7 +304,7 @@ describe('Manifest Parser', function() {
 
       const parsedManifest = manifestParser(manifestSrc, manifestUrl, docUrl);
       const parsedUrl = parsedManifest.value.start_url;
-      assert.ok(!parsedUrl.debugString);
+      assert.ok(!parsedUrl.warning);
       assert.equal(parsedUrl.value, 'https://example.com/');
     });
 
@@ -318,7 +318,7 @@ describe('Manifest Parser', function() {
 
       const parsedManifest = manifestParser(manifestSrc, manifestUrl, docUrl);
       const parsedUrl = parsedManifest.value.start_url;
-      assert.ok(!parsedUrl.debugString);
+      assert.ok(!parsedUrl.warning);
       assert.equal(parsedUrl.value, 'https://example.com/start_point.html');
     });
     /* eslint-enable camelcase */
@@ -331,21 +331,21 @@ describe('Manifest Parser', function() {
     it('falls back to \'browser\' and issues a warning for an invalid value', () => {
       const parsedManifest = noUrlManifestParser('{"display": {} }');
       const display = parsedManifest.value.display;
-      assert.ok(display.debugString);
+      assert.ok(display.warning);
       assert.equal(display.value, 'browser');
     });
 
     it('falls back to \'browser\' and issues a warning for an invalid value', () => {
       const parsedManifest = noUrlManifestParser('{"display": 5 }');
       const display = parsedManifest.value.display;
-      assert.ok(display.debugString);
+      assert.ok(display.warning);
       assert.equal(display.value, 'browser');
     });
 
     it('falls back to \'browser\' and issues no warning when undefined', () => {
       const parsedManifest = noUrlManifestParser('{}');
       const display = parsedManifest.value.display;
-      assert.ok(!display.debugString);
+      assert.ok(!display.warning);
       assert.equal(display.value, 'browser');
       assert.equal(display.rawValue, undefined);
     });
@@ -354,7 +354,7 @@ describe('Manifest Parser', function() {
       const displayValue = ' fullscreen     ';
       const parsedManifest = noUrlManifestParser(`{"display": "${displayValue}" }`);
       const display = parsedManifest.value.display;
-      assert.ok(!display.debugString);
+      assert.ok(!display.warning);
       assert.equal(display.value, 'fullscreen');
     });
 
@@ -362,42 +362,42 @@ describe('Manifest Parser', function() {
       const displayValue = 'fUlLScrEEn';
       const parsedManifest = noUrlManifestParser(`{"display": "${displayValue}" }`);
       const display = parsedManifest.value.display;
-      assert.ok(!display.debugString);
+      assert.ok(!display.warning);
       assert.equal(display.value, 'fullscreen');
     });
 
     it('falls back to \'browser\' and issues a warning when a non-existent mode', () => {
       const parsedManifest = noUrlManifestParser('{"display": "fullestscreen" }');
       const display = parsedManifest.value.display;
-      assert.ok(display.debugString);
+      assert.ok(display.warning);
       assert.equal(display.value, 'browser');
     });
 
     it('correctly parses `fullscreen` display mode', () => {
       const parsedManifest = noUrlManifestParser('{"display": "fullscreen" }');
       const display = parsedManifest.value.display;
-      assert.ok(!display.debugString);
+      assert.ok(!display.warning);
       assert.equal(display.value, 'fullscreen');
     });
 
     it('correctly parses `standalone` display mode', () => {
       const parsedManifest = noUrlManifestParser('{"display": "standalone" }');
       const display = parsedManifest.value.display;
-      assert.ok(!display.debugString);
+      assert.ok(!display.warning);
       assert.equal(display.value, 'standalone');
     });
 
     it('correctly parses `minimal-ui` display mode', () => {
       const parsedManifest = noUrlManifestParser('{"display": "minimal-ui" }');
       const display = parsedManifest.value.display;
-      assert.ok(!display.debugString);
+      assert.ok(!display.warning);
       assert.equal(display.value, 'minimal-ui');
     });
 
     it('correctly parses `browser` display mode', () => {
       const parsedManifest = noUrlManifestParser('{"display": "browser" }');
       const display = parsedManifest.value.display;
-      assert.ok(!display.debugString);
+      assert.ok(!display.warning);
       assert.equal(display.value, 'browser');
     });
   });
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 a9004146f..8650cbf77 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
@@ -167,7 +167,7 @@ describe('PerfCategoryRenderer', () => {
         group: 'load-opportunities',
         result: {
           error: true, score: 0,
-          rawValue: 100, debugString: 'Yikes!!', title: 'Bug #2',
+          rawValue: 100, explanation: 'Yikes!!', title: 'Bug #2',
         },
       };
       const wastedMs = renderer._getWastedMs(auditWithDebug);
diff --git a/lighthouse-extension/test/extension-test.js b/lighthouse-extension/test/extension-test.js
index 1b794692c..f25ef29e7 100644
--- a/lighthouse-extension/test/extension-test.js
+++ b/lighthouse-extension/test/extension-test.js
@@ -154,21 +154,20 @@ describe('Lighthouse chrome extension', function() {
   });
 
   it('should not have any audit errors', async () => {
-    // TODO(phulce): rework these to look at the tooltips
-    function getDebugStrings(elems, selectors) {
+    function getErrors(elems, selectors) {
       return elems.map(el => {
         const audit = el.closest(selectors.audits);
         const auditTitle = audit && audit.querySelector(selectors.titles);
         return {
-          debugString: el.textContent,
+          explanation: el.textContent,
           title: auditTitle ? auditTitle.textContent : 'Audit title unvailable',
         };
       });
     }
 
-    const errorSelectors = '.lh-audit-explanation, .tooltip-error';
-    const auditErrors = await extensionPage.$$eval(errorSelectors, getDebugStrings, selectors);
-    const errors = auditErrors.filter(item => item.debugString.includes('Audit error:'));
+    const errorSelectors = '.lh-audit-explanation, .tooltip--error';
+    const auditErrors = await extensionPage.$$eval(errorSelectors, getErrors, selectors);
+    const errors = auditErrors.filter(item => item.explanation.includes('Audit error:'));
     assert.deepStrictEqual(errors, [], 'Audit errors found within the report');
   });
 
diff --git a/lighthouse-viewer/test/viewer-test-pptr.js b/lighthouse-viewer/test/viewer-test-pptr.js
index 8546e91a9..1a42831bf 100644
--- a/lighthouse-viewer/test/viewer-test-pptr.js
+++ b/lighthouse-viewer/test/viewer-test-pptr.js
@@ -119,23 +119,22 @@ describe('Lighthouse Viewer', function() {
   });
 
   it('should not have any unexpected audit errors', async () => {
-    // TODO(phulce): rework these to look at the tooltips
-    function getDebugStrings(elems, selectors) {
+    function getErrors(elems, selectors) {
       return elems.map(el => {
         const audit = el.closest(selectors.audits);
         const auditTitle = audit && audit.querySelector(selectors.titles);
         return {
-          debugString: el.textContent,
+          explanation: el.textContent,
           title: auditTitle ? auditTitle.textContent : 'Audit title unvailable',
         };
       });
     }
 
-    const errorSelectors = '.lh-audit-explanation, .tooltip-error';
-    const auditErrors = await viewerPage.$$eval(errorSelectors, getDebugStrings, selectors);
-    const errors = auditErrors.filter(item => item.debugString.includes('Audit error:'));
+    const errorSelectors = '.lh-audit-explanation, .tooltip--error';
+    const auditErrors = await viewerPage.$$eval(errorSelectors, getErrors, selectors);
+    const errors = auditErrors.filter(item => item.explanation.includes('Audit error:'));
     const unexpectedErrrors = errors.filter(item => {
-      return !item.debugString.includes('Required RobotsTxt gatherer did not run');
+      return !item.explanation.includes('Required RobotsTxt gatherer did not run');
     });
     assert.deepStrictEqual(unexpectedErrrors, [], 'Audit errors found within the report');
   });