lighthouse 8.4.0 → 8.5.0-dev.20210921

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/changelog.md +79 -0
  2. package/flow-report/assets/styles.css +85 -61
  3. package/flow-report/src/app.tsx +6 -2
  4. package/flow-report/src/common.tsx +0 -41
  5. package/flow-report/src/icons.tsx +11 -0
  6. package/flow-report/src/summary/summary.tsx +32 -25
  7. package/flow-report/src/topbar.tsx +60 -0
  8. package/flow-report/src/util.ts +0 -1
  9. package/flow-report/src/wrappers/{gauge.tsx → category-score.tsx} +11 -5
  10. package/flow-report/test/summary/summary-test.tsx +3 -3
  11. package/flow-report/tsconfig.json +6 -22
  12. package/{lighthouse-core/gather/gatherers/gather-context.js → lighthouse-cli/.eslintrc.cjs} +20 -18
  13. package/lighthouse-cli/bin.js +31 -21
  14. package/lighthouse-cli/cli-flags.js +12 -7
  15. package/lighthouse-cli/commands/commands.js +2 -7
  16. package/lighthouse-cli/commands/list-audits.js +2 -2
  17. package/lighthouse-cli/commands/list-trace-categories.js +2 -2
  18. package/lighthouse-cli/index.js +3 -1
  19. package/lighthouse-cli/package.json +4 -0
  20. package/lighthouse-cli/printer.js +4 -3
  21. package/lighthouse-cli/run.js +14 -15
  22. package/lighthouse-cli/sentry-prompt.js +5 -6
  23. package/lighthouse-cli/test/smokehouse/frontends/back-compat-util.js +1 -1
  24. package/lighthouse-cli/test/smokehouse/frontends/lib.js +5 -4
  25. package/lighthouse-cli/test/smokehouse/frontends/node.js +1 -1
  26. package/lighthouse-cli/test/smokehouse/frontends/smokehouse-bin.js +93 -14
  27. package/lighthouse-cli/test/smokehouse/lib/child-process-error.js +1 -1
  28. package/lighthouse-cli/test/smokehouse/lib/concurrent-mapper.js +1 -1
  29. package/lighthouse-cli/test/smokehouse/lib/local-console.js +1 -1
  30. package/lighthouse-cli/test/smokehouse/lighthouse-runners/bundle.js +20 -16
  31. package/lighthouse-cli/test/smokehouse/lighthouse-runners/cli.js +15 -12
  32. package/lighthouse-cli/test/smokehouse/report-assert.js +23 -13
  33. package/lighthouse-cli/test/smokehouse/smokehouse.js +32 -11
  34. package/lighthouse-core/audits/byte-efficiency/byte-efficiency-audit.js +0 -2
  35. package/lighthouse-core/audits/byte-efficiency/uses-responsive-images-snapshot.js +6 -1
  36. package/lighthouse-core/audits/dobetterweb/js-libraries.js +5 -0
  37. package/lighthouse-core/audits/dobetterweb/uses-http2.js +1 -0
  38. package/lighthouse-core/audits/lcp-lazy-loaded.js +1 -0
  39. package/lighthouse-core/audits/oopif-iframe-test-audit.js +30 -0
  40. package/lighthouse-core/computed/metrics/metric.js +2 -1
  41. package/lighthouse-core/config/config-helpers.js +47 -3
  42. package/lighthouse-core/config/config.js +3 -46
  43. package/lighthouse-core/config/default-config.js +0 -1
  44. package/lighthouse-core/fraggle-rock/config/config.js +22 -4
  45. package/lighthouse-core/fraggle-rock/config/default-config.js +0 -9
  46. package/lighthouse-core/fraggle-rock/config/filters.js +3 -0
  47. package/lighthouse-core/fraggle-rock/config/validation.js +19 -1
  48. package/lighthouse-core/fraggle-rock/gather/base-artifacts.js +8 -3
  49. package/lighthouse-core/fraggle-rock/gather/driver.js +10 -0
  50. package/lighthouse-core/fraggle-rock/gather/navigation-runner.js +40 -19
  51. package/lighthouse-core/fraggle-rock/gather/runner-helpers.js +3 -0
  52. package/lighthouse-core/fraggle-rock/gather/session.js +44 -1
  53. package/lighthouse-core/fraggle-rock/gather/snapshot-runner.js +4 -1
  54. package/lighthouse-core/fraggle-rock/gather/timespan-runner.js +5 -1
  55. package/lighthouse-core/gather/devtools-log.js +9 -3
  56. package/lighthouse-core/gather/driver/network-monitor.js +57 -13
  57. package/lighthouse-core/gather/driver/prepare.js +43 -17
  58. package/lighthouse-core/gather/driver/target-manager.js +125 -0
  59. package/lighthouse-core/gather/driver.js +20 -0
  60. package/lighthouse-core/gather/fetcher.js +2 -2
  61. package/lighthouse-core/gather/gather-runner.js +1 -1
  62. package/lighthouse-core/gather/gatherers/css-usage.js +0 -2
  63. package/lighthouse-core/gather/gatherers/devtools-log.js +11 -9
  64. package/lighthouse-core/gather/gatherers/js-usage.js +0 -1
  65. package/lighthouse-core/gather/gatherers/script-elements.js +4 -5
  66. package/lighthouse-core/gather/gatherers/seo/robots-txt.js +3 -3
  67. package/lighthouse-core/gather/gatherers/trace.js +4 -5
  68. package/lighthouse-core/index.js +4 -0
  69. package/lighthouse-core/lib/emulation.js +0 -1
  70. package/lighthouse-core/lib/i18n/locales/en-US.json +6 -0
  71. package/lighthouse-core/lib/i18n/locales/en-XL.json +6 -0
  72. package/lighthouse-core/lib/network-recorder.js +8 -0
  73. package/lighthouse-core/lib/proto-preprocessor.js +5 -10
  74. package/lighthouse-core/runner.js +1 -3
  75. package/package.json +12 -9
  76. package/readme.md +2 -0
  77. package/report/assets/styles.css +120 -12
  78. package/report/assets/templates.html +13 -0
  79. package/report/clients/psi.js +5 -1
  80. package/report/generator/tsconfig.json +5 -16
  81. package/report/renderer/category-renderer.js +58 -5
  82. package/report/renderer/components.js +25 -5
  83. package/report/renderer/performance-category-renderer.js +5 -5
  84. package/report/renderer/pwa-category-renderer.js +10 -0
  85. package/report/renderer/report-renderer.js +12 -3
  86. package/report/renderer/report-ui-features.js +1 -4
  87. package/report/test/renderer/category-renderer-test.js +19 -0
  88. package/report/tsconfig.json +6 -21
  89. package/report/types/report-result.d.ts +1 -1
  90. package/root.js +3 -1
  91. package/third-party/snyk/snapshot.json +2 -2
  92. package/tsconfig-all.json +15 -0
  93. package/tsconfig-base.json +24 -0
  94. package/tsconfig.json +8 -18
  95. package/types/artifacts.d.ts +7 -7
  96. package/types/config.d.ts +1 -1
  97. package/types/gatherer.d.ts +7 -1
  98. package/types/lhr/flow.d.ts +0 -1
  99. package/types/lhr/lhr.d.ts +5 -1
  100. package/types/lhr/tsconfig.json +4 -14
  101. package/types/protocol.d.ts +2 -0
  102. package/types/smokehouse.d.ts +2 -1
  103. package/dist/report/flow.js +0 -149
  104. package/dist/report/standalone.js +0 -228
  105. package/flow-report/test/common-test.tsx +0 -131
  106. package/lighthouse-core/gather/gatherers/host-form-factor.js +0 -31
  107. package/lighthouse-core/gather/gatherers/host-user-agent.js +0 -29
@@ -47,10 +47,7 @@ const artifacts = {
47
47
  FontSize: '',
48
48
  FormElements: '',
49
49
  FullPageScreenshot: '',
50
- GatherContext: '',
51
50
  GlobalListeners: '',
52
- HostFormFactor: '',
53
- HostUserAgent: '',
54
51
  IFrameElements: '',
55
52
  ImageElements: '',
56
53
  InstallabilityErrors: '',
@@ -85,8 +82,6 @@ for (const key of Object.keys(artifacts)) {
85
82
  const defaultConfig = {
86
83
  artifacts: [
87
84
  // Artifacts which can be depended on come first.
88
- {id: artifacts.HostUserAgent, gatherer: 'host-user-agent'},
89
- {id: artifacts.HostFormFactor, gatherer: 'host-form-factor'},
90
85
  {id: artifacts.DevtoolsLog, gatherer: 'devtools-log'},
91
86
  {id: artifacts.Trace, gatherer: 'trace'},
92
87
 
@@ -103,7 +98,6 @@ const defaultConfig = {
103
98
  {id: artifacts.FontSize, gatherer: 'seo/font-size'},
104
99
  {id: artifacts.FormElements, gatherer: 'form-elements'},
105
100
  {id: artifacts.FullPageScreenshot, gatherer: 'full-page-screenshot'},
106
- {id: artifacts.GatherContext, gatherer: 'gather-context'},
107
101
  {id: artifacts.GlobalListeners, gatherer: 'global-listeners'},
108
102
  {id: artifacts.IFrameElements, gatherer: 'iframe-elements'},
109
103
  {id: artifacts.ImageElements, gatherer: 'image-elements'},
@@ -142,8 +136,6 @@ const defaultConfig = {
142
136
  cpuQuietThresholdMs: 1000,
143
137
  artifacts: [
144
138
  // Artifacts which can be depended on come first.
145
- artifacts.HostUserAgent,
146
- artifacts.HostFormFactor,
147
139
  artifacts.DevtoolsLog,
148
140
  artifacts.Trace,
149
141
 
@@ -158,7 +150,6 @@ const defaultConfig = {
158
150
  artifacts.EmbeddedContent,
159
151
  artifacts.FontSize,
160
152
  artifacts.FormElements,
161
- artifacts.GatherContext,
162
153
  artifacts.GlobalListeners,
163
154
  artifacts.IFrameElements,
164
155
  artifacts.ImageElements,
@@ -16,6 +16,9 @@ const baseArtifactKeySource = {
16
16
  Timing: '',
17
17
  URL: '',
18
18
  PageLoadError: '',
19
+ HostFormFactor: '',
20
+ HostUserAgent: '',
21
+ GatherContext: '',
19
22
  };
20
23
 
21
24
  const baseArtifactKeys = Object.keys(baseArtifactKeySource);
@@ -38,6 +38,22 @@ function isValidArtifactDependency(dependent, dependency) {
38
38
  return true;
39
39
  }
40
40
 
41
+ /**
42
+ * Throws if pluginName is invalid or (somehow) collides with a category in the
43
+ * configJSON being added to.
44
+ * @param {LH.Config.Json} configJSON
45
+ * @param {string} pluginName
46
+ */
47
+ function assertValidPluginName(configJSON, pluginName) {
48
+ if (!pluginName.startsWith('lighthouse-plugin-')) {
49
+ throw new Error(`plugin name '${pluginName}' does not start with 'lighthouse-plugin-'`);
50
+ }
51
+
52
+ if (configJSON.categories && configJSON.categories[pluginName]) {
53
+ throw new Error(`plugin name '${pluginName}' not allowed because it is the id of a category already found in config`); // eslint-disable-line max-len
54
+ }
55
+ }
56
+
41
57
  /**
42
58
  * Throws an error if the provided object does not implement the required Fraggle Rock gatherer interface.
43
59
  * @param {LH.Config.AnyFRGathererDefn} gathererDefn
@@ -123,9 +139,10 @@ function assertValidAudit(auditDefinition) {
123
139
  }
124
140
 
125
141
  // If it'll have a ✔ or ✖ displayed alongside the result, it should have failureTitle
142
+ const scoreDisplayMode = implementation.meta.scoreDisplayMode || Audit.SCORING_MODES.BINARY;
126
143
  if (
127
144
  !i18n.isStringOrIcuMessage(implementation.meta.failureTitle) &&
128
- implementation.meta.scoreDisplayMode === Audit.SCORING_MODES.BINARY
145
+ scoreDisplayMode === Audit.SCORING_MODES.BINARY
129
146
  ) {
130
147
  throw new Error(`${auditName} has no meta.failureTitle and should.`);
131
148
  }
@@ -283,6 +300,7 @@ function throwInvalidArtifactDependency(artifactId, dependencyKey) {
283
300
  module.exports = {
284
301
  isFRGathererDefn,
285
302
  isValidArtifactDependency,
303
+ assertValidPluginName,
286
304
  assertValidFRGatherer,
287
305
  assertValidFRNavigations,
288
306
  assertValidAudit,
@@ -8,6 +8,7 @@
8
8
  const log = require('lighthouse-logger');
9
9
  const isEqual = require('lodash.isequal');
10
10
  const {
11
+ getBrowserVersion,
11
12
  getBenchmarkIndex,
12
13
  getEnvironmentWarnings,
13
14
  } = require('../../gather/driver/environment.js');
@@ -15,10 +16,12 @@ const {
15
16
  /**
16
17
  * @param {LH.Config.FRConfig} config
17
18
  * @param {LH.Gatherer.FRTransitionalDriver} driver
19
+ * @param {{gatherMode: LH.Gatherer.GatherMode}} context
18
20
  * @return {Promise<LH.BaseArtifacts>}
19
21
  */
20
- async function getBaseArtifacts(config, driver) {
22
+ async function getBaseArtifacts(config, driver, context) {
21
23
  const BenchmarkIndex = await getBenchmarkIndex(driver.executionContext);
24
+ const {userAgent} = await getBrowserVersion(driver.defaultSession);
22
25
 
23
26
  return {
24
27
  // Meta artifacts.
@@ -28,12 +31,14 @@ async function getBaseArtifacts(config, driver) {
28
31
  settings: config.settings,
29
32
  // Environment artifacts that can always be computed.
30
33
  BenchmarkIndex,
34
+ HostUserAgent: userAgent,
35
+ HostFormFactor: userAgent.includes('Android') || userAgent.includes('Mobile') ?
36
+ 'mobile' : 'desktop',
31
37
  // Contextual artifacts whose collection changes based on gather mode.
32
38
  URL: {requestedUrl: '', finalUrl: ''},
33
39
  PageLoadError: null,
40
+ GatherContext: context,
34
41
  // Artifacts that have been replaced by regular gatherers in Fraggle Rock.
35
- HostFormFactor: 'mobile',
36
- HostUserAgent: '',
37
42
  Stacks: [],
38
43
  NetworkUserAgent: '',
39
44
  WebAppManifest: null,
@@ -16,6 +16,7 @@ const throwNotConnectedFn = () => {
16
16
 
17
17
  /** @type {LH.Gatherer.FRProtocolSession} */
18
18
  const defaultSession = {
19
+ setTargetInfo: throwNotConnectedFn,
19
20
  hasNextProtocolTimeout: throwNotConnectedFn,
20
21
  getNextProtocolTimeout: throwNotConnectedFn,
21
22
  setNextProtocolTimeout: throwNotConnectedFn,
@@ -24,7 +25,10 @@ const defaultSession = {
24
25
  off: throwNotConnectedFn,
25
26
  addProtocolMessageListener: throwNotConnectedFn,
26
27
  removeProtocolMessageListener: throwNotConnectedFn,
28
+ addSessionAttachedListener: throwNotConnectedFn,
29
+ removeSessionAttachedListener: throwNotConnectedFn,
27
30
  sendCommand: throwNotConnectedFn,
31
+ dispose: throwNotConnectedFn,
28
32
  };
29
33
 
30
34
  /** @implements {LH.Gatherer.FRTransitionalDriver} */
@@ -69,6 +73,12 @@ class Driver {
69
73
  this._executionContext = new ExecutionContext(this._session);
70
74
  this._fetcher = new Fetcher(this._session, this._executionContext);
71
75
  }
76
+
77
+ /** @return {Promise<void>} */
78
+ async disconnect() {
79
+ if (!this._session) return;
80
+ await this._session.dispose();
81
+ }
72
82
  }
73
83
 
74
84
  module.exports = Driver;
@@ -23,6 +23,7 @@ const {getBaseArtifacts, finalizeArtifacts} = require('./base-artifacts.js');
23
23
  const i18n = require('../../lib/i18n/i18n.js');
24
24
  const LighthouseError = require('../../lib/lh-error.js');
25
25
  const {getPageLoadError} = require('../../lib/navigation-error.js');
26
+ const Trace = require('../../gather/gatherers/trace.js');
26
27
  const DevtoolsLog = require('../../gather/gatherers/devtools-log.js');
27
28
  const NetworkRecords = require('../../computed/network-records.js');
28
29
 
@@ -32,6 +33,7 @@ const NetworkRecords = require('../../computed/network-records.js');
32
33
  * @property {LH.Config.FRConfig} config
33
34
  * @property {LH.Config.NavigationDefn} navigation
34
35
  * @property {string} requestedUrl
36
+ * @property {LH.FRBaseArtifacts} baseArtifacts
35
37
  * @property {Map<string, LH.ArbitraryEqualityMap>} computedCache
36
38
  */
37
39
 
@@ -45,7 +47,7 @@ async function _setup({driver, config, requestedUrl}) {
45
47
  await driver.connect();
46
48
  await gotoURL(driver, defaultNavigationConfig.blankPage, {waitUntil: ['navigated']});
47
49
 
48
- const baseArtifacts = await getBaseArtifacts(config, driver);
50
+ const baseArtifacts = await getBaseArtifacts(config, driver, {gatherMode: 'navigation'});
49
51
  baseArtifacts.URL.requestedUrl = requestedUrl;
50
52
 
51
53
  await prepare.prepareTargetForNavigationMode(driver, config.settings);
@@ -105,21 +107,36 @@ async function _navigate(navigationContext) {
105
107
  /**
106
108
  * @param {NavigationContext} navigationContext
107
109
  * @param {PhaseState} phaseState
108
- * @return {Promise<{devtoolsLog: LH.DevtoolsLog, records: Array<LH.Artifacts.NetworkRequest>} | undefined>}
110
+ * @return {Promise<{devtoolsLog?: LH.DevtoolsLog, records?: Array<LH.Artifacts.NetworkRequest>, trace?: LH.Trace}>}
109
111
  */
110
- async function _collectNetworkData(navigationContext, phaseState) {
112
+ async function _collectDebugData(navigationContext, phaseState) {
111
113
  const devtoolsLogArtifactDefn = phaseState.artifactDefinitions.find(
112
114
  definition => definition.gatherer.instance.meta.symbol === DevtoolsLog.symbol
113
115
  );
114
- if (!devtoolsLogArtifactDefn) return undefined;
116
+ const traceArtifactDefn = phaseState.artifactDefinitions.find(
117
+ definition => definition.gatherer.instance.meta.symbol === Trace.symbol
118
+ );
119
+
120
+ const artifactDefinitions = [devtoolsLogArtifactDefn, traceArtifactDefn].filter(
121
+ /**
122
+ * @param {LH.Config.AnyArtifactDefn | undefined} defn
123
+ * @return {defn is LH.Config.AnyArtifactDefn}
124
+ */
125
+ defn => Boolean(defn)
126
+ );
127
+ if (!artifactDefinitions.length) return {};
115
128
 
116
- const devtoolsLogArtifactId = devtoolsLogArtifactDefn.id;
117
- const artifactDefinitions = [devtoolsLogArtifactDefn];
118
129
  await collectPhaseArtifacts({...phaseState, phase: 'getArtifact', artifactDefinitions});
130
+ const getArtifactState = phaseState.artifactState.getArtifact;
131
+
132
+ const devtoolsLogArtifactId = devtoolsLogArtifactDefn && devtoolsLogArtifactDefn.id;
133
+ const devtoolsLog = devtoolsLogArtifactId && await getArtifactState[devtoolsLogArtifactId];
134
+ const records = devtoolsLog && await NetworkRecords.request(devtoolsLog, navigationContext);
135
+
136
+ const traceArtifactId = traceArtifactDefn && traceArtifactDefn.id;
137
+ const trace = traceArtifactId && await getArtifactState[traceArtifactId];
119
138
 
120
- const devtoolsLog = await phaseState.artifactState.getArtifact[devtoolsLogArtifactId];
121
- const records = await NetworkRecords.request(devtoolsLog, navigationContext);
122
- return {devtoolsLog, records};
139
+ return {devtoolsLog, records, trace};
123
140
  }
124
141
 
125
142
  /**
@@ -137,12 +154,12 @@ async function _computeNavigationResult(
137
154
  ) {
138
155
  const {navigationError, finalUrl} = navigateResult;
139
156
  const warnings = [...setupResult.warnings, ...navigateResult.warnings];
140
- const networkData = await _collectNetworkData(navigationContext, phaseState);
141
- const pageLoadError = networkData
157
+ const debugData = await _collectDebugData(navigationContext, phaseState);
158
+ const pageLoadError = debugData.records
142
159
  ? getPageLoadError(navigationError, {
143
160
  url: finalUrl,
144
161
  loadFailureMode: navigationContext.navigation.loadFailureMode,
145
- networkRecords: networkData.records,
162
+ networkRecords: debugData.records,
146
163
  })
147
164
  : navigationError;
148
165
 
@@ -154,7 +171,8 @@ async function _computeNavigationResult(
154
171
  /** @type {Partial<LH.GathererArtifacts>} */
155
172
  const artifacts = {};
156
173
  const pageLoadErrorId = `pageLoadError-${navigationContext.navigation.id}`;
157
- if (networkData) artifacts.devtoolsLogs = {[pageLoadErrorId]: networkData.devtoolsLog};
174
+ if (debugData.devtoolsLog) artifacts.devtoolsLogs = {[pageLoadErrorId]: debugData.devtoolsLog};
175
+ if (debugData.trace) artifacts.traces = {[pageLoadErrorId]: debugData.trace};
158
176
 
159
177
  return {
160
178
  finalUrl,
@@ -182,6 +200,7 @@ async function _navigation(navigationContext) {
182
200
  computedCache: navigationContext.computedCache,
183
201
  artifactDefinitions: navigationContext.navigation.artifacts,
184
202
  artifactState,
203
+ baseArtifacts: navigationContext.baseArtifacts,
185
204
  settings: navigationContext.config.settings,
186
205
  };
187
206
 
@@ -197,10 +216,10 @@ async function _navigation(navigationContext) {
197
216
  }
198
217
 
199
218
  /**
200
- * @param {{driver: Driver, config: LH.Config.FRConfig, requestedUrl: string; computedCache: NavigationContext['computedCache']}} args
219
+ * @param {{driver: Driver, config: LH.Config.FRConfig, requestedUrl: string; baseArtifacts: LH.FRBaseArtifacts, computedCache: NavigationContext['computedCache']}} args
201
220
  * @return {Promise<{artifacts: Partial<LH.FRArtifacts & LH.FRBaseArtifacts>}>}
202
221
  */
203
- async function _navigations({driver, config, requestedUrl, computedCache}) {
222
+ async function _navigations({driver, config, requestedUrl, baseArtifacts, computedCache}) {
204
223
  if (!config.navigations) throw new Error('No navigations configured');
205
224
 
206
225
  /** @type {Partial<LH.FRArtifacts & LH.FRBaseArtifacts>} */
@@ -214,6 +233,7 @@ async function _navigations({driver, config, requestedUrl, computedCache}) {
214
233
  navigation,
215
234
  requestedUrl,
216
235
  config,
236
+ baseArtifacts,
217
237
  computedCache,
218
238
  };
219
239
 
@@ -243,7 +263,7 @@ async function _cleanup({requestedUrl, driver, config}) {
243
263
  const didResetStorage = !config.settings.disableStorageReset;
244
264
  if (didResetStorage) await storage.clearDataForOrigin(driver.defaultSession, requestedUrl);
245
265
 
246
- // TODO(FR-COMPAT): add driver.disconnect session tracking
266
+ await driver.disconnect();
247
267
  }
248
268
 
249
269
  /**
@@ -258,9 +278,10 @@ async function navigation(options) {
258
278
  return Runner.run(
259
279
  async () => {
260
280
  const driver = new Driver(page);
261
- const {baseArtifacts} = await _setup({driver, config, requestedUrl});
262
- const {artifacts} = await _navigations({driver, config, requestedUrl, computedCache});
263
- await _cleanup({driver, config, requestedUrl});
281
+ const context = {driver, config, requestedUrl};
282
+ const {baseArtifacts} = await _setup(context);
283
+ const {artifacts} = await _navigations({...context, baseArtifacts, computedCache});
284
+ await _cleanup(context);
264
285
 
265
286
  return finalizeArtifacts(baseArtifacts, artifacts);
266
287
  },
@@ -10,6 +10,7 @@
10
10
  * @property {import('./driver.js')} driver
11
11
  * @property {Array<LH.Config.AnyArtifactDefn>} artifactDefinitions
12
12
  * @property {ArtifactState} artifactState
13
+ * @property {LH.FRBaseArtifacts} baseArtifacts
13
14
  * @property {LH.Gatherer.FRGatherPhase} phase
14
15
  * @property {LH.Gatherer.GatherMode} gatherMode
15
16
  * @property {Map<string, LH.ArbitraryEqualityMap>} computedCache
@@ -68,6 +69,7 @@ async function collectPhaseArtifacts(options) {
68
69
  driver,
69
70
  artifactDefinitions,
70
71
  artifactState,
72
+ baseArtifacts,
71
73
  phase,
72
74
  gatherMode,
73
75
  computedCache,
@@ -91,6 +93,7 @@ async function collectPhaseArtifacts(options) {
91
93
  url: await driver.url(),
92
94
  gatherMode,
93
95
  driver,
96
+ baseArtifacts,
94
97
  dependencies,
95
98
  computedCache,
96
99
  settings,
@@ -18,8 +18,12 @@ class ProtocolSession {
18
18
  */
19
19
  constructor(session) {
20
20
  this._session = session;
21
+ /** @type {LH.Crdp.Target.TargetInfo|undefined} */
22
+ this._targetInfo = undefined;
21
23
  /** @type {number|undefined} */
22
24
  this._nextProtocolTimeout = undefined;
25
+ /** @type {WeakMap<any, any>} */
26
+ this._callbackMap = new WeakMap();
23
27
 
24
28
  // FIXME: Monkeypatch puppeteer to be able to listen to *all* protocol events.
25
29
  // This patched method will now emit a copy of every event on `*`.
@@ -27,13 +31,22 @@ class ProtocolSession {
27
31
  // @ts-expect-error - Test for the monkeypatch.
28
32
  if (originalEmit[SessionEmitMonkeypatch]) return;
29
33
  session.emit = (method, ...args) => {
30
- originalEmit.call(session, '*', {method, params: args[0]});
34
+ // OOPIF sessions need to emit their sessionId so downstream processors can recognize
35
+ // the target the event came from.
36
+ const sessionId = this._targetInfo && this._targetInfo.type === 'iframe' ?
37
+ this._targetInfo.targetId : undefined;
38
+ originalEmit.call(session, '*', {method, params: args[0], sessionId});
31
39
  return originalEmit.call(session, method, ...args);
32
40
  };
33
41
  // @ts-expect-error - It's monkeypatching 🤷‍♂️.
34
42
  session.emit[SessionEmitMonkeypatch] = true;
35
43
  }
36
44
 
45
+ /** @param {LH.Crdp.Target.TargetInfo} targetInfo */
46
+ setTargetInfo(targetInfo) {
47
+ this._targetInfo = targetInfo;
48
+ }
49
+
37
50
  /**
38
51
  * @return {boolean}
39
52
  */
@@ -75,6 +88,27 @@ class ProtocolSession {
75
88
  this._session.once(eventName, /** @type {*} */ (callback));
76
89
  }
77
90
 
91
+ /**
92
+ * Bind to the puppeteer `sessionattached` listener and return an LH ProtocolSession.
93
+ * @param {(session: ProtocolSession) => void} callback
94
+ */
95
+ addSessionAttachedListener(callback) {
96
+ /** @param {import('puppeteer').CDPSession} session */
97
+ const listener = session => callback(new ProtocolSession(session));
98
+ this._callbackMap.set(callback, listener);
99
+ this._session.connection().on('sessionattached', listener);
100
+ }
101
+
102
+ /**
103
+ * Unbind to the puppeteer `sessionattached` listener.
104
+ * @param {(session: ProtocolSession) => void} callback
105
+ */
106
+ removeSessionAttachedListener(callback) {
107
+ const listener = this._callbackMap.get(callback);
108
+ if (!listener) return;
109
+ this._session.connection().off('sessionattached', listener);
110
+ }
111
+
78
112
  /**
79
113
  * Bind to our custom event that fires for *any* protocol event.
80
114
  * @param {(payload: LH.Protocol.RawEventMessage) => void} callback
@@ -129,6 +163,15 @@ class ProtocolSession {
129
163
  if (timeout) clearTimeout(timeout);
130
164
  });
131
165
  }
166
+
167
+ /**
168
+ * Disposes of a session so that it can no longer talk to Chrome.
169
+ * @return {Promise<void>}
170
+ */
171
+ async dispose() {
172
+ this._session.removeAllListeners();
173
+ await this._session.detach();
174
+ }
132
175
  }
133
176
 
134
177
  module.exports = ProtocolSession;
@@ -28,7 +28,7 @@ async function snapshot(options) {
28
28
 
29
29
  return Runner.run(
30
30
  async () => {
31
- const baseArtifacts = await getBaseArtifacts(config, driver);
31
+ const baseArtifacts = await getBaseArtifacts(config, driver, {gatherMode: 'snapshot'});
32
32
  baseArtifacts.URL.requestedUrl = url;
33
33
  baseArtifacts.URL.finalUrl = url;
34
34
 
@@ -38,12 +38,15 @@ async function snapshot(options) {
38
38
  phase: 'getArtifact',
39
39
  gatherMode: 'snapshot',
40
40
  driver,
41
+ baseArtifacts,
41
42
  artifactDefinitions,
42
43
  artifactState,
43
44
  computedCache,
44
45
  settings: config.settings,
45
46
  });
46
47
 
48
+ await driver.disconnect();
49
+
47
50
  const artifacts = await awaitArtifacts(artifactState);
48
51
  return finalizeArtifacts(baseArtifacts, artifacts);
49
52
  },
@@ -12,6 +12,7 @@ const {
12
12
  collectPhaseArtifacts,
13
13
  awaitArtifacts,
14
14
  } = require('./runner-helpers.js');
15
+ const {prepareTargetForTimespanMode} = require('../../gather/driver/prepare.js');
15
16
  const {initializeConfig} = require('../config/config.js');
16
17
  const {getBaseArtifacts, finalizeArtifacts} = require('./base-artifacts.js');
17
18
 
@@ -29,17 +30,20 @@ async function startTimespan(options) {
29
30
  const computedCache = new Map();
30
31
  const artifactDefinitions = config.artifacts || [];
31
32
  const requestedUrl = await options.page.url();
33
+ const baseArtifacts = await getBaseArtifacts(config, driver, {gatherMode: 'timespan'});
32
34
  const artifactState = getEmptyArtifactState();
33
35
  /** @type {Omit<import('./runner-helpers.js').CollectPhaseArtifactOptions, 'phase'>} */
34
36
  const phaseOptions = {
35
37
  driver,
36
38
  artifactDefinitions,
37
39
  artifactState,
40
+ baseArtifacts,
38
41
  computedCache,
39
42
  gatherMode: 'timespan',
40
43
  settings: config.settings,
41
44
  };
42
45
 
46
+ await prepareTargetForTimespanMode(driver, config.settings);
43
47
  await collectPhaseArtifacts({phase: 'startInstrumentation', ...phaseOptions});
44
48
  await collectPhaseArtifacts({phase: 'startSensitiveInstrumentation', ...phaseOptions});
45
49
 
@@ -48,13 +52,13 @@ async function startTimespan(options) {
48
52
  const finalUrl = await options.page.url();
49
53
  return Runner.run(
50
54
  async () => {
51
- const baseArtifacts = await getBaseArtifacts(config, driver);
52
55
  baseArtifacts.URL.requestedUrl = requestedUrl;
53
56
  baseArtifacts.URL.finalUrl = finalUrl;
54
57
 
55
58
  await collectPhaseArtifacts({phase: 'stopSensitiveInstrumentation', ...phaseOptions});
56
59
  await collectPhaseArtifacts({phase: 'stopInstrumentation', ...phaseOptions});
57
60
  await collectPhaseArtifacts({phase: 'getArtifact', ...phaseOptions});
61
+ await driver.disconnect();
58
62
 
59
63
  const artifacts = await awaitArtifacts(artifactState);
60
64
  return finalizeArtifacts(baseArtifacts, artifacts);
@@ -46,9 +46,15 @@ class DevtoolsLog {
46
46
  * @param {LH.Protocol.RawEventMessage} message
47
47
  */
48
48
  record(message) {
49
- if (this._isRecording && (!this._filter || this._filter.test(message.method))) {
50
- this._messages.push(message);
51
- }
49
+ // We're not recording, skip the rest of the checks.
50
+ if (!this._isRecording) return;
51
+ // The event was likely an internal puppeteer method that uses Symbols.
52
+ if (typeof message.method !== 'string') return;
53
+ // The event didn't pass our filter, do not record it.
54
+ if (this._filter && !this._filter.test(message.method)) return;
55
+
56
+ // We passed all the checks, record the message.
57
+ this._messages.push(message);
52
58
  }
53
59
  }
54
60
 
@@ -15,48 +15,82 @@ const {EventEmitter} = require('events');
15
15
  const NetworkRecorder = require('../../lib/network-recorder.js');
16
16
  const NetworkRequest = require('../../lib/network-request.js');
17
17
  const URL = require('../../lib/url-shim.js');
18
+ const TargetManager = require('./target-manager.js');
18
19
 
19
20
  /** @typedef {import('../../lib/network-recorder.js').NetworkRecorderEvent} NetworkRecorderEvent */
20
21
  /** @typedef {'network-2-idle'|'network-critical-idle'|'networkidle'|'networkbusy'|'network-critical-busy'|'network-2-busy'} NetworkMonitorEvent_ */
21
22
  /** @typedef {NetworkRecorderEvent|NetworkMonitorEvent_} NetworkMonitorEvent */
23
+ /** @typedef {Record<NetworkMonitorEvent_, []> & Record<NetworkRecorderEvent, [NetworkRequest]> & {protocolmessage: [LH.Protocol.RawEventMessage]}} NetworkMonitorEventMap */
24
+ /** @typedef {LH.Protocol.StrictEventEmitter<NetworkMonitorEventMap>} NetworkMonitorEmitter */
22
25
 
23
- class NetworkMonitor extends EventEmitter {
26
+ /** @implements {NetworkMonitorEmitter} */
27
+ class NetworkMonitor {
24
28
  /** @type {NetworkRecorder|undefined} */
25
29
  _networkRecorder = undefined;
30
+ /** @type {TargetManager|undefined} */
31
+ _targetManager = undefined;
26
32
  /** @type {Array<LH.Crdp.Page.Frame>} */
27
33
  _frameNavigations = [];
28
34
 
29
35
  /** @param {LH.Gatherer.FRProtocolSession} session */
30
36
  constructor(session) {
31
- super();
32
37
  this._session = session;
33
38
 
39
+ this._onTargetAttached = this._onTargetAttached.bind(this);
40
+
41
+ /** @type {Map<string, LH.Gatherer.FRProtocolSession>} */
42
+ this._sessions = new Map();
43
+
34
44
  /** @param {LH.Crdp.Page.FrameNavigatedEvent} event */
35
45
  this._onFrameNavigated = event => this._frameNavigations.push(event.frame);
36
46
 
37
47
  /** @param {LH.Protocol.RawEventMessage} event */
38
48
  this._onProtocolMessage = event => {
49
+ this.emit('protocolmessage', event);
39
50
  if (!this._networkRecorder) return;
40
51
  this._networkRecorder.dispatch(event);
41
52
  };
42
53
 
43
- // Redefine the event emitter types with a narrower type signature.
44
- /** @param {NetworkMonitorEvent} event @param {*} listener */
45
- this.on = (event, listener) => super.on(event, listener);
46
- /** @param {NetworkMonitorEvent} event @param {*} listener */
47
- this.once = (event, listener) => super.once(event, listener);
48
- /** @param {NetworkMonitorEvent} event @param {*} listener */
49
- this.off = (event, listener) => super.off(event, listener);
54
+ // Attach the event emitter types to this class.
55
+ const emitter = /** @type {NetworkMonitorEmitter} */ (new EventEmitter());
56
+ /** @type {typeof emitter['emit']} */
57
+ this.emit = emitter.emit.bind(emitter);
58
+ /** @type {typeof emitter['on']} */
59
+ this.on = emitter.on.bind(emitter);
60
+ /** @type {typeof emitter['once']} */
61
+ this.once = emitter.once.bind(emitter);
62
+ /** @type {typeof emitter['off']} */
63
+ this.off = emitter.off.bind(emitter);
64
+ /** @type {typeof emitter['addListener']} */
65
+ this.addListener = emitter.addListener.bind(emitter);
66
+ /** @type {typeof emitter['removeListener']} */
67
+ this.removeListener = emitter.removeListener.bind(emitter);
68
+ /** @type {typeof emitter['removeAllListeners']} */
69
+ this.removeAllListeners = emitter.removeAllListeners.bind(emitter);
70
+ }
71
+
72
+ /**
73
+ * @param {{target: {targetId: string}, session: LH.Gatherer.FRProtocolSession}} session
74
+ */
75
+ async _onTargetAttached({session, target}) {
76
+ const targetId = target.targetId;
77
+
78
+ this._sessions.set(targetId, session);
79
+ session.addProtocolMessageListener(this._onProtocolMessage);
80
+
81
+ await session.sendCommand('Network.enable');
50
82
  }
51
83
 
52
84
  /**
53
85
  * @return {Promise<void>}
54
86
  */
55
87
  async enable() {
56
- if (this._networkRecorder) return;
88
+ if (this._targetManager) return;
57
89
 
58
90
  this._frameNavigations = [];
91
+ this._sessions = new Map();
59
92
  this._networkRecorder = new NetworkRecorder();
93
+ this._targetManager = new TargetManager(this._session);
60
94
 
61
95
  /**
62
96
  * Reemit the same network recorder events.
@@ -72,21 +106,31 @@ class NetworkMonitor extends EventEmitter {
72
106
  this._networkRecorder.on('requestloaded', reEmit('requestloaded'));
73
107
 
74
108
  this._session.on('Page.frameNavigated', this._onFrameNavigated);
75
- this._session.addProtocolMessageListener(this._onProtocolMessage);
109
+ this._targetManager.addTargetAttachedListener(this._onTargetAttached);
76
110
 
77
111
  await this._session.sendCommand('Page.enable');
78
- await this._session.sendCommand('Network.enable');
112
+ await this._targetManager.enable();
79
113
  }
80
114
 
81
115
  /**
82
116
  * @return {Promise<void>}
83
117
  */
84
118
  async disable() {
119
+ if (!this._targetManager) return;
120
+
85
121
  this._session.off('Page.frameNavigated', this._onFrameNavigated);
86
- this._session.removeProtocolMessageListener(this._onProtocolMessage);
122
+ this._targetManager.removeTargetAttachedListener(this._onTargetAttached);
123
+
124
+ for (const session of this._sessions.values()) {
125
+ session.removeProtocolMessageListener(this._onProtocolMessage);
126
+ }
127
+
128
+ await this._targetManager.disable();
87
129
 
88
130
  this._frameNavigations = [];
89
131
  this._networkRecorder = undefined;
132
+ this._targetManager = undefined;
133
+ this._sessions = new Map();
90
134
  }
91
135
 
92
136
  /** @return {Promise<string | undefined>} */