Repository: GoogleChrome/lighthouse
Pull request #7156 (2019-02-05 16:56:00)

core(networkanalysis): set additional RTT to 0 not NaN
**Summary**
This has no impact on anything other than the new diagnostic audit. In network analysis there could be partial data missing. Before we would set the value as `NaN` and simulator would fallback to using `0`. Instead of letting the consumer fallback, this just explicit falls back so our `maxRtt` doesn't wind up to be `null` in the diagnostic audit.

**Related Issues/PRs**
#7052 



Patch:
diff --git a/lighthouse-core/audits/audit.js b/lighthouse-core/audits/audit.js
index 9eff46f94..98b44ba30 100644
--- a/lighthouse-core/audits/audit.js
+++ b/lighthouse-core/audits/audit.js
@@ -126,11 +126,11 @@ class Audit {
   }
 
   /**
-   * @param {Array<LH.ResultLite.Audit.ColumnHeading>} headings
-   * @param {Array<LH.ResultLite.Audit.WastedBytesDetailsItem>|Array<LH.ResultLite.Audit.WastedTimeDetailsItem>} items
+   * @param {Array<LH.Audit.Details.OpportunityColumnHeading>} headings
+   * @param {Array<LH.Audit.Details.OpportunityItem>} items
    * @param {number} overallSavingsMs
    * @param {number=} overallSavingsBytes
-   * @return {LH.Result.Audit.OpportunityDetails}
+   * @return {LH.Audit.Details.Opportunity}
    */
   static makeOpportunityDetails(headings, items, overallSavingsMs, overallSavingsBytes) {
     return {
diff --git a/lighthouse-core/audits/byte-efficiency/byte-efficiency-audit.js b/lighthouse-core/audits/byte-efficiency/byte-efficiency-audit.js
index 4d90e2e24..792cd8c84 100644
--- a/lighthouse-core/audits/byte-efficiency/byte-efficiency-audit.js
+++ b/lighthouse-core/audits/byte-efficiency/byte-efficiency-audit.js
@@ -27,7 +27,7 @@ const WASTED_MS_FOR_SCORE_OF_ZERO = 5000;
 /**
  * @typedef {object} ByteEfficiencyProduct
  * @property {Array<LH.Audit.ByteEfficiencyItem>} items
- * @property {LH.Result.Audit.OpportunityDetails['headings']} headings
+ * @property {LH.Audit.Details.Opportunity['headings']} headings
  * @property {string} [displayValue]
  * @property {string} [explanation]
  * @property {Array<string>} [warnings]
diff --git a/lighthouse-core/audits/byte-efficiency/efficient-animated-content.js b/lighthouse-core/audits/byte-efficiency/efficient-animated-content.js
index 27483d87f..bd4c938dc 100644
--- a/lighthouse-core/audits/byte-efficiency/efficient-animated-content.js
+++ b/lighthouse-core/audits/byte-efficiency/efficient-animated-content.js
@@ -74,7 +74,7 @@ class EfficientAnimatedContent extends ByteEfficiencyAudit {
       };
     });
 
-    /** @type {LH.Result.Audit.OpportunityDetails['headings']} */
+    /** @type {LH.Audit.Details.Opportunity['headings']} */
     const headings = [
       {key: 'url', valueType: 'url', label: str_(i18n.UIStrings.columnURL)},
       {key: 'totalBytes', valueType: 'bytes', label: str_(i18n.UIStrings.columnSize)},
diff --git a/lighthouse-core/audits/byte-efficiency/offscreen-images.js b/lighthouse-core/audits/byte-efficiency/offscreen-images.js
index 79472b92d..b4f40ac0e 100644
--- a/lighthouse-core/audits/byte-efficiency/offscreen-images.js
+++ b/lighthouse-core/audits/byte-efficiency/offscreen-images.js
@@ -223,7 +223,7 @@ class OffscreenImages extends ByteEfficiencyAudit {
         await TraceOfTab.request(trace, context).then(tot => tot.timestamps.traceEnd));
     }
 
-    /** @type {LH.Result.Audit.OpportunityDetails['headings']} */
+    /** @type {LH.Audit.Details.Opportunity['headings']} */
     const headings = [
       {key: 'url', valueType: 'thumbnail', label: ''},
       {key: 'url', valueType: 'url', label: str_(i18n.UIStrings.columnURL)},
diff --git a/lighthouse-core/audits/byte-efficiency/render-blocking-resources.js b/lighthouse-core/audits/byte-efficiency/render-blocking-resources.js
index 5e1e17e65..8400d3aff 100644
--- a/lighthouse-core/audits/byte-efficiency/render-blocking-resources.js
+++ b/lighthouse-core/audits/byte-efficiency/render-blocking-resources.js
@@ -211,7 +211,7 @@ class RenderBlockingResources extends Audit {
       displayValue = str_(i18n.UIStrings.displayValueMsSavings, {wastedMs});
     }
 
-    /** @type {LH.Result.Audit.OpportunityDetails['headings']} */
+    /** @type {LH.Audit.Details.Opportunity['headings']} */
     const headings = [
       {key: 'url', valueType: 'url', label: str_(i18n.UIStrings.columnURL)},
       {key: 'totalBytes', valueType: 'bytes', label: str_(i18n.UIStrings.columnSize)},
diff --git a/lighthouse-core/audits/byte-efficiency/unminified-css.js b/lighthouse-core/audits/byte-efficiency/unminified-css.js
index c5ec4a954..87604f5de 100644
--- a/lighthouse-core/audits/byte-efficiency/unminified-css.js
+++ b/lighthouse-core/audits/byte-efficiency/unminified-css.js
@@ -102,7 +102,7 @@ class UnminifiedCSS extends ByteEfficiencyAudit {
       items.push(result);
     }
 
-    /** @type {LH.Result.Audit.OpportunityDetails['headings']} */
+    /** @type {LH.Audit.Details.Opportunity['headings']} */
     const headings = [
       {key: 'url', valueType: 'url', label: str_(i18n.UIStrings.columnURL)},
       {key: 'totalBytes', valueType: 'bytes', label: str_(i18n.UIStrings.columnSize)},
diff --git a/lighthouse-core/audits/byte-efficiency/unminified-javascript.js b/lighthouse-core/audits/byte-efficiency/unminified-javascript.js
index 441a29733..364af573f 100644
--- a/lighthouse-core/audits/byte-efficiency/unminified-javascript.js
+++ b/lighthouse-core/audits/byte-efficiency/unminified-javascript.js
@@ -95,7 +95,7 @@ class UnminifiedJavaScript extends ByteEfficiencyAudit {
       }
     }
 
-    /** @type {LH.Result.Audit.OpportunityDetails['headings']} */
+    /** @type {LH.Audit.Details.Opportunity['headings']} */
     const headings = [
       {key: 'url', valueType: 'url', label: str_(i18n.UIStrings.columnURL)},
       {key: 'totalBytes', valueType: 'bytes', label: str_(i18n.UIStrings.columnSize)},
diff --git a/lighthouse-core/audits/byte-efficiency/unused-css-rules.js b/lighthouse-core/audits/byte-efficiency/unused-css-rules.js
index 1c2adbb99..475d8657a 100644
--- a/lighthouse-core/audits/byte-efficiency/unused-css-rules.js
+++ b/lighthouse-core/audits/byte-efficiency/unused-css-rules.js
@@ -173,7 +173,7 @@ class UnusedCSSRules extends ByteEfficiencyAudit {
           .map(sheetId => UnusedCSSRules.mapSheetToResult(indexedSheets[sheetId], pageUrl))
           .filter(sheet => sheet && sheet.wastedBytes > IGNORE_THRESHOLD_IN_BYTES);
 
-      /** @type {LH.Result.Audit.OpportunityDetails['headings']} */
+      /** @type {LH.Audit.Details.Opportunity['headings']} */
       const headings = [
         {key: 'url', valueType: 'url', label: str_(i18n.UIStrings.columnURL)},
         {key: 'totalBytes', valueType: 'bytes', label: str_(i18n.UIStrings.columnSize)},
diff --git a/lighthouse-core/audits/byte-efficiency/uses-optimized-images.js b/lighthouse-core/audits/byte-efficiency/uses-optimized-images.js
index 0ffd9a7e8..2d4b08516 100644
--- a/lighthouse-core/audits/byte-efficiency/uses-optimized-images.js
+++ b/lighthouse-core/audits/byte-efficiency/uses-optimized-images.js
@@ -80,7 +80,7 @@ class UsesOptimizedImages extends ByteEfficiencyAudit {
       });
     }
 
-    /** @type {LH.Result.Audit.OpportunityDetails['headings']} */
+    /** @type {LH.Audit.Details.Opportunity['headings']} */
     const headings = [
       {key: 'url', valueType: 'thumbnail', label: ''},
       {key: 'url', valueType: 'url', label: str_(i18n.UIStrings.columnURL)},
diff --git a/lighthouse-core/audits/byte-efficiency/uses-responsive-images.js b/lighthouse-core/audits/byte-efficiency/uses-responsive-images.js
index 9769ca6ca..139f26c73 100644
--- a/lighthouse-core/audits/byte-efficiency/uses-responsive-images.js
+++ b/lighthouse-core/audits/byte-efficiency/uses-responsive-images.js
@@ -119,7 +119,7 @@ class UsesResponsiveImages extends ByteEfficiencyAudit {
     const items = Array.from(resultsMap.values())
         .filter(item => item.wastedBytes > IGNORE_THRESHOLD_IN_BYTES);
 
-    /** @type {LH.Result.Audit.OpportunityDetails['headings']} */
+    /** @type {LH.Audit.Details.Opportunity['headings']} */
     const headings = [
       {key: 'url', valueType: 'thumbnail', label: ''},
       {key: 'url', valueType: 'url', label: str_(i18n.UIStrings.columnURL)},
diff --git a/lighthouse-core/audits/byte-efficiency/uses-text-compression.js b/lighthouse-core/audits/byte-efficiency/uses-text-compression.js
index aa706f66f..a400dca3c 100644
--- a/lighthouse-core/audits/byte-efficiency/uses-text-compression.js
+++ b/lighthouse-core/audits/byte-efficiency/uses-text-compression.js
@@ -82,7 +82,7 @@ class ResponsesAreCompressed extends ByteEfficiencyAudit {
       });
     });
 
-    /** @type {LH.Result.Audit.OpportunityDetails['headings']} */
+    /** @type {LH.Audit.Details.Opportunity['headings']} */
     const headings = [
       {key: 'url', valueType: 'url', label: str_(i18n.UIStrings.columnURL)},
       {key: 'totalBytes', valueType: 'bytes', label: str_(i18n.UIStrings.columnSize)},
diff --git a/lighthouse-core/audits/byte-efficiency/uses-webp-images.js b/lighthouse-core/audits/byte-efficiency/uses-webp-images.js
index 56e58a2a6..fb681dd3a 100644
--- a/lighthouse-core/audits/byte-efficiency/uses-webp-images.js
+++ b/lighthouse-core/audits/byte-efficiency/uses-webp-images.js
@@ -79,7 +79,7 @@ class UsesWebPImages extends ByteEfficiencyAudit {
       });
     }
 
-    /** @type {LH.Result.Audit.OpportunityDetails['headings']} */
+    /** @type {LH.Audit.Details.Opportunity['headings']} */
     const headings = [
       {key: 'url', valueType: 'thumbnail', label: ''},
       {key: 'url', valueType: 'url', label: str_(i18n.UIStrings.columnURL)},
diff --git a/lighthouse-core/audits/redirects.js b/lighthouse-core/audits/redirects.js
index fd1a4bcd1..a2b1c7456 100644
--- a/lighthouse-core/audits/redirects.js
+++ b/lighthouse-core/audits/redirects.js
@@ -98,7 +98,7 @@ class Redirects extends Audit {
       });
     }
 
-    /** @type {LH.Result.Audit.OpportunityDetails['headings']} */
+    /** @type {LH.Audit.Details.Opportunity['headings']} */
     const headings = [
       {key: 'url', valueType: 'url', label: str_(i18n.UIStrings.columnURL)},
       {key: 'wastedMs', valueType: 'timespanMs', label: str_(i18n.UIStrings.columnTimeSpent)},
diff --git a/lighthouse-core/audits/time-to-first-byte.js b/lighthouse-core/audits/time-to-first-byte.js
index 928e17dd9..ec6610ff2 100644
--- a/lighthouse-core/audits/time-to-first-byte.js
+++ b/lighthouse-core/audits/time-to-first-byte.js
@@ -60,7 +60,7 @@ class TTFBMetric extends Audit {
     const passed = ttfb < TTFB_THRESHOLD;
     const displayValue = str_(UIStrings.displayValue, {timeInMs: ttfb});
 
-    /** @type {LH.Result.Audit.OpportunityDetails} */
+    /** @type {LH.Audit.Details.Opportunity} */
     const details = {
       type: 'opportunity',
       overallSavingsMs: ttfb - TTFB_THRESHOLD,
diff --git a/lighthouse-core/audits/uses-rel-preconnect.js b/lighthouse-core/audits/uses-rel-preconnect.js
index 0f09b12d9..e08620b25 100644
--- a/lighthouse-core/audits/uses-rel-preconnect.js
+++ b/lighthouse-core/audits/uses-rel-preconnect.js
@@ -178,7 +178,7 @@ class UsesRelPreconnectAudit extends Audit {
     results = results
       .sort((a, b) => b.wastedMs - a.wastedMs);
 
-    /** @type {LH.Result.Audit.OpportunityDetails['headings']} */
+    /** @type {LH.Audit.Details.Opportunity['headings']} */
     const headings = [
       {key: 'url', valueType: 'url', label: str_(i18n.UIStrings.columnURL)},
       {key: 'wastedMs', valueType: 'timespanMs', label: str_(i18n.UIStrings.columnWastedMs)},
diff --git a/lighthouse-core/audits/uses-rel-preload.js b/lighthouse-core/audits/uses-rel-preload.js
index e79680c55..671e2ffea 100644
--- a/lighthouse-core/audits/uses-rel-preload.js
+++ b/lighthouse-core/audits/uses-rel-preload.js
@@ -213,7 +213,7 @@ class UsesRelPreloadAudit extends Audit {
         .map(preloadURL => str_(UIStrings.crossoriginWarning, {preloadURL}));
     }
 
-    /** @type {LH.Result.Audit.OpportunityDetails['headings']} */
+    /** @type {LH.Audit.Details.Opportunity['headings']} */
     const headings = [
       {key: 'url', valueType: 'url', label: str_(i18n.UIStrings.columnURL)},
       {key: 'wastedMs', valueType: 'timespanMs', label: str_(i18n.UIStrings.columnWastedMs)},
diff --git a/lighthouse-core/computed/network-analysis.js b/lighthouse-core/computed/network-analysis.js
index 267afe235..07784eb78 100644
--- a/lighthouse-core/computed/network-analysis.js
+++ b/lighthouse-core/computed/network-analysis.js
@@ -35,9 +35,8 @@ class NetworkAnalysis {
     /** @type {Map<string, number>} */
     const serverResponseTimeByOrigin = new Map();
     for (const [origin, summary] of responseTimeSummaries.entries()) {
-      /** @type {number} */
-      // @ts-ignore - satisfy the type checker that entry exists.
-      const rttForOrigin = rttByOrigin.get(origin);
+      // Not all origins have usable timing data, we'll default to using no additional latency.
+      const rttForOrigin = rttByOrigin.get(origin) || minimumRtt;
       additionalRttByOrigin.set(origin, rttForOrigin - minimumRtt);
       serverResponseTimeByOrigin.set(origin, summary.median);
     }
diff --git a/lighthouse-core/report/html/renderer/crc-details-renderer.js b/lighthouse-core/report/html/renderer/crc-details-renderer.js
index 9ffde2295..150064d47 100644
--- a/lighthouse-core/report/html/renderer/crc-details-renderer.js
+++ b/lighthouse-core/report/html/renderer/crc-details-renderer.js
@@ -145,7 +145,7 @@ class CriticalRequestChainRenderer {
    * @param {DocumentFragment} tmpl
    * @param {CRCSegment} segment
    * @param {Element} elem Parent element.
-   * @param {CRCDetailsJSON} details
+   * @param {LH.Audit.Details.CriticalRequestChain} details
    */
   static buildTree(dom, tmpl, segment, elem, details) {
     elem.appendChild(CriticalRequestChainRenderer.createChainNode(dom, tmpl, segment));
@@ -161,7 +161,7 @@ class CriticalRequestChainRenderer {
   /**
    * @param {DOM} dom
    * @param {ParentNode} templateContext
-   * @param {CRCDetailsJSON} details
+   * @param {LH.Audit.Details.CriticalRequestChain} details
    * @return {Element}
    */
   static render(dom, templateContext, details) {
@@ -194,14 +194,6 @@ if (typeof module !== 'undefined' && module.exports) {
   self.CriticalRequestChainRenderer = CriticalRequestChainRenderer;
 }
 
-/** @typedef {{
-      type: string,
-      header: {text: string},
-      longestChain: {duration: number, length: number, transferSize: number},
-      chains: LH.Audit.SimpleCriticalRequestNode
-  }} CRCDetailsJSON
- */
-
 /** @typedef {{
       node: LH.Audit.SimpleCriticalRequestNode[string],
       isLastChild: boolean,
diff --git a/lighthouse-core/report/html/renderer/details-renderer.js b/lighthouse-core/report/html/renderer/details-renderer.js
index b440662f3..9526be130 100644
--- a/lighthouse-core/report/html/renderer/details-renderer.js
+++ b/lighthouse-core/report/html/renderer/details-renderer.js
@@ -19,8 +19,7 @@
 /* globals self CriticalRequestChainRenderer Util URL */
 
 /** @typedef {import('./dom.js')} DOM */
-/** @typedef {import('./crc-details-renderer.js')} CRCDetailsJSON */
-/** @typedef {LH.Result.Audit.OpportunityDetails} OpportunityDetails */
+/** @typedef {LH.Audit.Details.Opportunity} OpportunityDetails */
 
 /** @type {Array<string>} */
 const URL_PREFIXES = ['http://', 'https://', 'data:'];
@@ -76,7 +75,7 @@ class DetailsRenderer {
       case 'criticalrequestchain':
         return CriticalRequestChainRenderer.render(this._dom, this._templateContext,
           // @ts-ignore - TODO(bckenny): Fix type hierarchy
-          /** @type {CRCDetailsJSON} */ (details));
+          /** @type {LH.Audit.Details.CriticalRequestChain} */ (details));
       case 'opportunity':
         // @ts-ignore - TODO(bckenny): Fix type hierarchy
         return this._renderOpportunityTable(details);
@@ -289,7 +288,7 @@ class DetailsRenderer {
     for (const row of details.items) {
       const rowElem = this._dom.createChildOf(tbodyElem, 'tr');
       for (const heading of details.headings) {
-        const key = /** @type {keyof LH.Result.Audit.OpportunityDetailsItem} */ (heading.key);
+        const key = /** @type {keyof LH.Audit.Details.OpportunityItem} */ (heading.key);
         const value = row[key];
 
         if (typeof value === 'undefined' || value === null) {
@@ -360,7 +359,7 @@ class DetailsRenderer {
   }
 
   /**
-   * @param {FilmstripDetails} details
+   * @param {LH.Audit.Details.Filmstrip} details
    * @return {Element}
    */
   _renderFilmstrip(details) {
@@ -460,9 +459,3 @@ if (typeof module !== 'undefined' && module.exports) {
   }} LinkDetailsJSON
  */
 
-/** @typedef {{
-      type: string,
-      scale: number,
-      items: Array<{timing: number, timestamp: number, data: string}>,
-  }} FilmstripDetails
- */
diff --git a/lighthouse-core/report/html/renderer/performance-category-renderer.js b/lighthouse-core/report/html/renderer/performance-category-renderer.js
index 870bdfc4e..8d63094bc 100644
--- a/lighthouse-core/report/html/renderer/performance-category-renderer.js
+++ b/lighthouse-core/report/html/renderer/performance-category-renderer.js
@@ -19,8 +19,7 @@
 /* globals self, Util, CategoryRenderer */
 
 /** @typedef {import('./dom.js')} DOM */
-/** @typedef {import('./details-renderer.js').FilmstripDetails} FilmstripDetails */
-/** @typedef {LH.Result.Audit.OpportunityDetails} OpportunityDetails */
+/** @typedef {LH.Audit.Details.Opportunity} OpportunityDetails */
 
 class PerformanceCategoryRenderer extends CategoryRenderer {
   /**
diff --git a/types/audit-details.d.ts b/types/audit-details.d.ts
new file mode 100644
index 000000000..dda07549b
--- /dev/null
+++ b/types/audit-details.d.ts
@@ -0,0 +1,69 @@
+/**
+ * @license Copyright 2019 Google Inc. All Rights Reserved.
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
+ */
+
+declare global {
+  module LH.Audit.Details {
+    export interface Filmstrip {
+      type: 'filmstrip';
+      scale: number;
+      items: {
+        /** The relative time from navigationStart to this frame, in milliseconds. */
+        timing: number;
+        /** The raw timestamp of this frame, in microseconds. */
+        timestamp: number;
+        /** The data URL encoding of this frame. */
+        data: string;
+      }[];
+    }
+
+    export interface Screenshot {
+      type: 'screenshot';
+      timestamp: number;
+      data: string;
+    }
+
+    export interface Opportunity {
+      type: 'opportunity';
+      overallSavingsMs: number;
+      overallSavingsBytes?: number;
+      headings: OpportunityColumnHeading[];
+      items: OpportunityItem[];
+    }
+
+    export interface CriticalRequestChain {
+      type: 'criticalrequestchain';
+      longestChain: {
+        duration: number;
+        length: number;
+        transferSize: number;
+      };
+      chains: Audit.SimpleCriticalRequestNode;
+    }
+
+    // Contents of details below here
+
+    export interface OpportunityColumnHeading {
+      /** The name of the property within items being described. */
+      key: string;
+      /** Readable text label of the field. */
+      label: string;
+      /** The data format of the column of values being described. */
+      valueType: string;
+    }
+    
+    export interface OpportunityItem {
+      url: string;
+      wastedBytes?: number;
+      totalBytes?: number;
+      wastedMs?: number;
+      [p: string]: number | boolean | string | undefined;
+    }
+
+  }
+}
+
+// empty export to keep file a module
+export {}
diff --git a/types/audit.d.ts b/types/audit.d.ts
index 1fb2b36ff..b57bb71a9 100644
--- a/types/audit.d.ts
+++ b/types/audit.d.ts
@@ -63,7 +63,7 @@ declare global {
       granularity?: number;
     }
 
-    export interface ByteEfficiencyItem extends Result.Audit.OpportunityDetailsItem {
+    export interface ByteEfficiencyItem extends Audit.Details.OpportunityItem {
       url: string;
       wastedBytes: number;
       totalBytes: number;
diff --git a/types/lhr-lite.d.ts b/types/lhr-lite.d.ts
index 8395a6b9c..a8c1251d9 100644
--- a/types/lhr-lite.d.ts
+++ b/types/lhr-lite.d.ts
@@ -70,49 +70,10 @@ declare global {
         /** An explanation of audit-related issues encountered on the test page. */
         explanation?: string;
         /** Extra information provided by some types of audits. */
-        details?: Audit.MetricDetails | Audit.OpportunityDetails;
+        details?: never;
         /** Error message from any exception thrown while running this audit. */
         errorMessage?: string;
       }
-
-      export module Audit {
-        export interface MetricDetails {
-          type: 'metric';
-          /** The value of the metric expressed in milliseconds. */
-          timespanMs?: number;
-        }
-
-        export interface OpportunityDetails {
-          type: 'opportunity';
-          overallSavingsMs: number
-          overallSavingsBytes?: number
-          headings: ColumnHeading[];
-          items: (WastedBytesDetailsItem | WastedTimeDetailsItem)[];
-        }
-
-        export interface ColumnHeading {
-          /** The property key name within DetailsItem being described. */
-          key: string;
-          /** Readable text label of the field. */
-          label: string;
-          // TODO(bckenny): should be just string and let lhr be more specific?
-          valueType: 'url' | 'timespanMs' | 'bytes' | 'thumbnail';
-        }
-
-        export interface WastedBytesDetailsItem {
-          url: string;
-          wastedBytes?: number;
-          totalBytes?: number;
-          [p: string]: number | boolean | string | undefined;
-        }
-
-        export interface WastedTimeDetailsItem {
-          url: string;
-          wastedMs: number;
-          totalBytes?: number;
-          [p: string]: number | boolean | string | undefined;
-        }
-      }
     }
   }
 }
diff --git a/types/lhr.d.ts b/types/lhr.d.ts
index 8c1403f40..d1b25612d 100644
--- a/types/lhr.d.ts
+++ b/types/lhr.d.ts
@@ -113,20 +113,6 @@ declare global {
         blockedUrlPatterns: string[];
         extraHeaders: Crdp.Network.Headers;
       }
-
-      export module Audit {
-        export interface OpportunityDetailsItem {
-          url: string;
-          wastedBytes?: number;
-          totalBytes?: number;
-          wastedMs?: number;
-          [p: string]: number | boolean | string | undefined;
-        }
-
-        export interface OpportunityDetails extends ResultLite.Audit.OpportunityDetails {
-          items: OpportunityDetailsItem[];
-        }
-      }
     }
   }
 }


Test patch:
diff --git a/lighthouse-core/test/computed/network-analysis-test.js b/lighthouse-core/test/computed/network-analysis-test.js
new file mode 100644
index 000000000..ee88ddcd6
--- /dev/null
+++ b/lighthouse-core/test/computed/network-analysis-test.js
@@ -0,0 +1,53 @@
+/**
+ * @license Copyright 2019 Google Inc. All Rights Reserved.
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
+ */
+'use strict';
+
+const NetworkAnalysis = require('../../computed/network-analysis.js');
+
+const acceptableDevToolsLog = require('../fixtures/traces/progressive-app-m60.devtools.log.json');
+
+/* eslint-env jest */
+
+describe('Network analysis computed', () => {
+  it('should return network analysis', async () => {
+    const result = await NetworkAnalysis.request(acceptableDevToolsLog, {computedCache: new Map()});
+
+    expect(Math.round(result.rtt)).toEqual(3);
+    expect(Math.round(result.throughput)).toEqual(1628070);
+    expect(result.additionalRttByOrigin).toMatchInlineSnapshot(`
+Map {
+  "https://pwa.rocks" => 0.3960000176447025,
+  "https://www.googletagmanager.com" => 0,
+  "https://www.google-analytics.com" => 1.0450000117997007,
+  "__SUMMARY__" => 0,
+}
+`);
+    expect(result.serverResponseTimeByOrigin).toMatchInlineSnapshot(`
+Map {
+  "https://pwa.rocks" => 159.42199996789026,
+  "https://www.googletagmanager.com" => 153.03200000198592,
+  "https://www.google-analytics.com" => 159.5549999910874,
+  "__SUMMARY__" => 159.42199996789026,
+}
+`);
+  });
+
+  it('should be robust enough to handle missing data', async () => {
+    const mutatedLog = acceptableDevToolsLog.map(entry => {
+      if (entry.method !== 'Network.responseReceived') return entry;
+      if (!entry.params.response.url.includes('google-analytics')) return entry;
+
+      const clonedEntry = JSON.parse(JSON.stringify(entry));
+      const invalidTimings = {sslStart: -1, sslEnd: -1, connectStart: -1, connectEnd: -1};
+      Object.assign(clonedEntry.params.response.timing, invalidTimings);
+
+      return clonedEntry;
+    });
+
+    const result = await NetworkAnalysis.request(mutatedLog, {computedCache: new Map()});
+    expect(result.additionalRttByOrigin.get('https://www.google-analytics.com')).toEqual(0);
+  });
+});
diff --git a/lighthouse-core/test/report/html/renderer/crc-details-renderer-test.js b/lighthouse-core/test/report/html/renderer/crc-details-renderer-test.js
index 1d5c1cb64..1cf2564a6 100644
--- a/lighthouse-core/test/report/html/renderer/crc-details-renderer-test.js
+++ b/lighthouse-core/test/report/html/renderer/crc-details-renderer-test.js
@@ -23,7 +23,6 @@ const superLongURL =
     'https://example.com/thisIsASuperLongURLThatWillTriggerFilenameTruncationWhichWeWantToTest.js';
 const DETAILS = {
   type: 'criticalrequestchain',
-  header: {type: 'text', text: 'CRC Header'},
   chains: {
     0: {
       request: {