@elastic/synthetics 1.0.0-beta.2 → 1.0.0-beta.22

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 (122) hide show
  1. package/CHANGELOG.md +235 -0
  2. package/NOTICE.txt +250 -0
  3. package/README.md +2 -1
  4. package/dist/cli.js +87 -56
  5. package/dist/cli.js.map +1 -1
  6. package/dist/common_types.d.ts +116 -14
  7. package/dist/common_types.d.ts.map +1 -1
  8. package/dist/config.d.ts +2 -5
  9. package/dist/config.d.ts.map +1 -1
  10. package/dist/config.js +5 -5
  11. package/dist/config.js.map +1 -1
  12. package/dist/core/browser-service.js +5 -5
  13. package/dist/core/browser-service.js.map +1 -1
  14. package/dist/core/expect.d.ts +177 -0
  15. package/dist/core/expect.d.ts.map +1 -0
  16. package/dist/core/expect.js +33 -0
  17. package/dist/core/expect.js.map +1 -0
  18. package/dist/core/gatherer.d.ts +4 -7
  19. package/dist/core/gatherer.d.ts.map +1 -1
  20. package/dist/core/gatherer.js +39 -12
  21. package/dist/core/gatherer.js.map +1 -1
  22. package/dist/core/index.d.ts +5 -5
  23. package/dist/core/index.d.ts.map +1 -1
  24. package/dist/core/index.js +2 -2
  25. package/dist/core/index.js.map +1 -1
  26. package/dist/core/logger.d.ts +0 -1
  27. package/dist/core/logger.d.ts.map +1 -1
  28. package/dist/core/logger.js +3 -16
  29. package/dist/core/logger.js.map +1 -1
  30. package/dist/core/runner.d.ts +54 -45
  31. package/dist/core/runner.d.ts.map +1 -1
  32. package/dist/core/runner.js +181 -71
  33. package/dist/core/runner.js.map +1 -1
  34. package/dist/dsl/journey.d.ts +13 -4
  35. package/dist/dsl/journey.d.ts.map +1 -1
  36. package/dist/dsl/journey.js +40 -1
  37. package/dist/dsl/journey.js.map +1 -1
  38. package/dist/formatter/javascript.d.ts +73 -0
  39. package/dist/formatter/javascript.d.ts.map +1 -0
  40. package/dist/formatter/javascript.js +274 -0
  41. package/dist/formatter/javascript.js.map +1 -0
  42. package/dist/helpers.d.ts +33 -12
  43. package/dist/helpers.d.ts.map +1 -1
  44. package/dist/helpers.js +108 -54
  45. package/dist/helpers.js.map +1 -1
  46. package/dist/index.d.ts +8 -0
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +6 -7
  49. package/dist/index.js.map +1 -1
  50. package/dist/parse_args.d.ts +5 -2
  51. package/dist/parse_args.d.ts.map +1 -1
  52. package/dist/parse_args.js +59 -16
  53. package/dist/parse_args.js.map +1 -1
  54. package/dist/plugins/browser-console.d.ts +5 -8
  55. package/dist/plugins/browser-console.d.ts.map +1 -1
  56. package/dist/plugins/browser-console.js +27 -8
  57. package/dist/plugins/browser-console.js.map +1 -1
  58. package/dist/plugins/network.d.ts +23 -20
  59. package/dist/plugins/network.d.ts.map +1 -1
  60. package/dist/plugins/network.js +149 -124
  61. package/dist/plugins/network.js.map +1 -1
  62. package/dist/plugins/performance.d.ts +5 -5
  63. package/dist/plugins/performance.d.ts.map +1 -1
  64. package/dist/plugins/performance.js +5 -5
  65. package/dist/plugins/performance.js.map +1 -1
  66. package/dist/plugins/plugin-manager.d.ts +11 -11
  67. package/dist/plugins/plugin-manager.d.ts.map +1 -1
  68. package/dist/plugins/plugin-manager.js +40 -24
  69. package/dist/plugins/plugin-manager.js.map +1 -1
  70. package/dist/plugins/tracing.d.ts +10 -25
  71. package/dist/plugins/tracing.d.ts.map +1 -1
  72. package/dist/plugins/tracing.js +40 -26
  73. package/dist/plugins/tracing.js.map +1 -1
  74. package/dist/reporters/base.d.ts +0 -1
  75. package/dist/reporters/base.d.ts.map +1 -1
  76. package/dist/reporters/base.js +34 -32
  77. package/dist/reporters/base.js.map +1 -1
  78. package/dist/reporters/json.d.ts +46 -59
  79. package/dist/reporters/json.d.ts.map +1 -1
  80. package/dist/reporters/json.js +188 -70
  81. package/dist/reporters/json.js.map +1 -1
  82. package/dist/reporters/junit.d.ts.map +1 -1
  83. package/dist/reporters/junit.js +7 -7
  84. package/dist/reporters/junit.js.map +1 -1
  85. package/dist/sdk/lh-trace-processor.d.ts +304 -0
  86. package/dist/sdk/lh-trace-processor.d.ts.map +1 -0
  87. package/dist/sdk/lh-trace-processor.js +832 -0
  88. package/dist/sdk/lh-trace-processor.js.map +1 -0
  89. package/dist/sdk/trace-metrics.d.ts +61 -0
  90. package/dist/sdk/trace-metrics.d.ts.map +1 -0
  91. package/dist/sdk/trace-metrics.js +252 -0
  92. package/dist/sdk/trace-metrics.js.map +1 -0
  93. package/dist/sdk/trace-processor.d.ts +106 -0
  94. package/dist/sdk/trace-processor.d.ts.map +1 -0
  95. package/dist/sdk/trace-processor.js +74 -0
  96. package/dist/sdk/trace-processor.js.map +1 -0
  97. package/dist/tsconfig.tsbuildinfo +1 -1575
  98. package/package.json +27 -16
  99. package/src/cli.ts +68 -45
  100. package/src/common_types.ts +138 -14
  101. package/src/config.ts +2 -5
  102. package/src/core/expect.ts +187 -0
  103. package/src/core/gatherer.ts +48 -24
  104. package/src/core/index.ts +6 -6
  105. package/src/core/logger.ts +1 -12
  106. package/src/core/runner.ts +259 -112
  107. package/src/dsl/journey.ts +28 -6
  108. package/src/formatter/javascript.ts +330 -0
  109. package/src/helpers.ts +114 -24
  110. package/src/index.ts +7 -6
  111. package/src/parse_args.ts +97 -22
  112. package/src/plugins/browser-console.ts +30 -13
  113. package/src/plugins/network.ts +175 -159
  114. package/src/plugins/performance.ts +7 -7
  115. package/src/plugins/plugin-manager.ts +47 -36
  116. package/src/plugins/tracing.ts +43 -54
  117. package/src/reporters/base.ts +29 -23
  118. package/src/reporters/json.ts +259 -80
  119. package/src/reporters/junit.ts +1 -1
  120. package/src/sdk/lh-trace-processor.ts +1033 -0
  121. package/src/sdk/trace-metrics.ts +287 -0
  122. package/src/sdk/trace-processor.ts +147 -0
package/src/parse_args.ts CHANGED
@@ -23,15 +23,14 @@
23
23
  *
24
24
  */
25
25
 
26
- import { program } from 'commander';
26
+ import { program, Option } from 'commander';
27
+ import { CliArgs } from './common_types';
27
28
  import { reporters } from './reporters';
28
-
29
- const availableReporters = Object.keys(reporters)
30
- .map(r => String(r))
31
- .join();
29
+ import { DEFAULT_NETWORK_CONDITIONS_ARG } from './helpers';
32
30
 
33
31
  /* eslint-disable-next-line @typescript-eslint/no-var-requires */
34
32
  const { name, version } = require('../package.json');
33
+
35
34
  program
36
35
  .name(`npx ${name}`)
37
36
  .usage('[options] [dir] [files] file')
@@ -39,12 +38,15 @@ program
39
38
  '-c, --config <path>',
40
39
  'configuration path (default: synthetics.config.js)'
41
40
  )
42
- .option('-s, --suite-params <jsonstring>', 'suite variables', '{}')
43
- .option('-e, --environment <envname>', 'e.g. production', 'development')
44
- .option('-j, --json', 'output newline delimited JSON')
45
41
  .option(
46
- '--reporter <reporter>',
47
- `output repoter format, can be one of ${availableReporters}`
42
+ '-p, --params <jsonstring>',
43
+ 'JSON object that gets injected to all journeys',
44
+ JSON.parse
45
+ )
46
+ .addOption(
47
+ new Option('--reporter <value>', `output repoter format`).choices(
48
+ Object.keys(reporters)
49
+ )
48
50
  )
49
51
  .option('-d, --debug', 'print debug logs info')
50
52
  .option(
@@ -54,30 +56,103 @@ program
54
56
  .option('--inline', 'Run inline journeys from heartbeat')
55
57
  .option('-r, --require <modules...>', 'module(s) to preload')
56
58
  .option('--no-headless', 'run browser in headful mode')
57
- .option('--sandbox', 'enable chromium sandboxing')
59
+ .option('--sandbox', 'enable chromium sandboxing', false)
60
+ .option('--rich-events', 'Mimics a heartbeat run')
58
61
  .option(
59
- '--ws-endpoint <endpoint>',
60
- 'Browser WebSocket endpoint to connect to'
62
+ '--capability <features...>',
63
+ 'Enable capabilities through feature flags'
61
64
  )
62
- .option(
63
- '--pause-on-error',
64
- 'pause on error until a keypress is made in the console. Useful during development'
65
+ .addOption(
66
+ new Option('--screenshots [flag]', 'take screenshots at end of each step')
67
+ .choices(['on', 'off', 'only-on-failure'])
68
+ .default('on')
65
69
  )
66
- .option('--screenshots', 'take screenshot for each step')
67
- .option('--filmstrips', 'record detailed filmstrip info for all journeys')
68
- .option('--network', 'capture network information for all journeys')
69
- .option('--metrics', 'capture performance metrics for each step')
70
70
  .option(
71
71
  '--dry-run',
72
72
  "don't actually execute anything, report only registered journeys"
73
73
  )
74
- .option('--journey-name <name>', 'only run the journey with the given name')
74
+ .option(
75
+ '--match <name>',
76
+ 'run only journeys with a name or tags that matches the glob'
77
+ )
78
+ .option(
79
+ '--tags <name...>',
80
+ 'run only journeys with the given tag(s), or globs'
81
+ )
75
82
  .option(
76
83
  '--outfd <fd>',
77
84
  'specify a file descriptor for logs. Default is stdout',
78
85
  parseInt
79
86
  )
87
+ .option(
88
+ '--ws-endpoint <endpoint>',
89
+ 'Browser WebSocket endpoint to connect to'
90
+ )
91
+ .option(
92
+ '--pause-on-error',
93
+ 'pause on error until a keypress is made in the console. Useful during development'
94
+ )
95
+ .option(
96
+ '--ignore-https-errors',
97
+ 'ignores any HTTPS errors in sites being tested, including ones related to unrecognized certs or signatures. This can be insecure!'
98
+ )
99
+ .option(
100
+ '--quiet-exit-code',
101
+ 'always return 0 as an exit code status, regardless of test pass / fail. Only return > 0 exit codes on internal errors where the suite could not be run'
102
+ )
103
+ .addOption(
104
+ new Option(
105
+ '--throttling <d/u/l>',
106
+ 'List of options to throttle network conditions for download throughput (d) in megabits/second, upload throughput (u) in megabits/second and latency (l) in milliseconds.'
107
+ ).default(DEFAULT_NETWORK_CONDITIONS_ARG)
108
+ )
109
+ .option('--no-throttling', 'Turns off default network throttling.')
110
+ .option('--playwright-options <jsonstring>', 'JSON object to pass in custom Playwright options for the agent. Options passed will be merged with Playwright options defined in your synthetics.config.js file. Options defined via --playwright-options take precedence.', JSON.parse)
80
111
  .version(version)
81
112
  .description('Run synthetic tests');
82
113
 
83
- export default program.parse(process.argv);
114
+ const command = program.parse(process.argv);
115
+ const options = command.opts() as CliArgs;
116
+
117
+ /**
118
+ * Group all events that can be consumed by heartbeat and
119
+ * eventually by the Synthetics UI.
120
+ */
121
+ if (options.richEvents) {
122
+ options.reporter = options.reporter ?? 'json';
123
+ options.ssblocks = true;
124
+ options.network = true;
125
+ options.quietExitCode = true;
126
+ options.trace = true;
127
+ }
128
+
129
+ if (options.capability) {
130
+ const supportedCapabilities = [
131
+ 'trace',
132
+ 'network',
133
+ 'filmstrips',
134
+ 'metrics',
135
+ 'ssblocks',
136
+ ];
137
+ /**
138
+ * trace - record chrome trace events(LCP, FCP, CLS, etc.) for all journeys
139
+ * network - capture network information for all journeys
140
+ * filmstrips - record detailed filmstrips for all journeys
141
+ * metrics - capture performance metrics (DOM Nodes, Heap size, etc.) for each step
142
+ * ssblocks - Dedupes the screenshots in to blocks to save storage space
143
+ */
144
+ for (const flag of options.capability) {
145
+ if (supportedCapabilities.includes(flag)) {
146
+ options[flag] = true;
147
+ } else {
148
+ console.warn(
149
+ `Missing capability "${flag}", current supported capabilities are ${supportedCapabilities.join(
150
+ ', '
151
+ )}`
152
+ );
153
+ }
154
+ }
155
+ }
156
+
157
+ export { options };
158
+ export default command;
@@ -23,22 +23,18 @@
23
23
  *
24
24
  */
25
25
 
26
- import { Page } from 'playwright-chromium';
27
- import { DefaultPluginOutput } from '../common_types';
26
+ import { BrowserMessage, Driver } from '../common_types';
28
27
  import { Step } from '../dsl';
29
28
  import { getTimestamp } from '../helpers';
30
29
 
31
- export type BrowserMessage = {
32
- text: string;
33
- type: string;
34
- } & DefaultPluginOutput;
35
-
36
30
  const defaultMessageLimit = 1000;
37
31
 
38
32
  export class BrowserConsole {
39
33
  private messages: BrowserMessage[] = [];
40
34
  _currentStep: Partial<Step> = null;
41
35
 
36
+ constructor(private driver: Driver) {}
37
+
42
38
  private consoleEventListener = msg => {
43
39
  if (!this._currentStep) {
44
40
  return;
@@ -52,20 +48,41 @@ export class BrowserConsole {
52
48
  type,
53
49
  step: { name, index },
54
50
  });
55
- if (this.messages.length > defaultMessageLimit) {
56
- this.messages.splice(0, 1);
57
- }
51
+
52
+ this.enforceMessagesLimit();
58
53
  }
59
54
  };
60
55
 
61
- constructor(private page: Page) {}
56
+ private pageErrorEventListener = (error: Error) => {
57
+ if (!this._currentStep) {
58
+ return;
59
+ }
60
+ const { name, index } = this._currentStep;
61
+ this.messages.push({
62
+ timestamp: getTimestamp(),
63
+ text: error.message,
64
+ type: 'error',
65
+ step: { name, index },
66
+ error,
67
+ });
68
+
69
+ this.enforceMessagesLimit();
70
+ };
71
+
72
+ private enforceMessagesLimit() {
73
+ if (this.messages.length > defaultMessageLimit) {
74
+ this.messages.splice(0, 1);
75
+ }
76
+ }
62
77
 
63
78
  start() {
64
- this.page.on('console', this.consoleEventListener);
79
+ this.driver.page.on('console', this.consoleEventListener);
80
+ this.driver.page.on('pageerror', this.pageErrorEventListener);
65
81
  }
66
82
 
67
83
  stop() {
68
- this.page.off('console', this.consoleEventListener);
84
+ this.driver.page.off('console', this.consoleEventListener);
85
+ this.driver.page.off('pageerror', this.pageErrorEventListener);
69
86
  return this.messages;
70
87
  }
71
88
  }
@@ -23,9 +23,8 @@
23
23
  *
24
24
  */
25
25
 
26
- import { CDPSession } from 'playwright-chromium';
27
26
  import { Protocol } from 'playwright-chromium/types/protocol';
28
- import { NetworkInfo, BrowserInfo } from '../common_types';
27
+ import { NetworkInfo, BrowserInfo, Driver } from '../common_types';
29
28
  import { Step } from '../dsl';
30
29
  import { getTimestamp } from '../helpers';
31
30
 
@@ -35,7 +34,10 @@ export class NetworkManager {
35
34
  results: Array<NetworkInfo> = [];
36
35
  _currentStep: Partial<Step> = null;
37
36
 
38
- async start(client: CDPSession) {
37
+ constructor(private driver: Driver) {}
38
+
39
+ async start() {
40
+ const { client } = this.driver;
39
41
  const { product } = await client.send('Browser.getVersion');
40
42
  const [name, version] = product.split('/');
41
43
  this._browser = { name, version };
@@ -62,14 +64,14 @@ export class NetworkManager {
62
64
  }
63
65
 
64
66
  _onRequestWillBeSent(event: Protocol.Network.requestWillBeSentPayload) {
65
- const { requestId, request, timestamp, type, loaderId } = event;
66
- const { url, method } = request;
67
+ const { url, method, postData, headers } = event.request;
67
68
  /**
68
69
  * Data URI should not show up as network requests
69
70
  */
70
71
  if (url.startsWith('data:')) {
71
72
  return;
72
73
  }
74
+ const { requestId, timestamp, type, loaderId } = event;
73
75
  const isNavigationRequest = requestId == loaderId && type === 'Document';
74
76
  const record = this._findNetworkRecord(requestId);
75
77
  /**
@@ -79,6 +81,7 @@ export class NetworkManager {
79
81
  */
80
82
  if (record) {
81
83
  if (event.redirectResponse) {
84
+ record.response.redirectURL = url;
82
85
  const response = event.redirectResponse;
83
86
  const data = Object.assign(event, {
84
87
  type: event.type,
@@ -115,15 +118,23 @@ export class NetworkManager {
115
118
  step: this._currentStep,
116
119
  timestamp: getTimestamp(),
117
120
  url,
118
- request,
119
121
  type,
120
- method,
121
- requestSentTime: timestamp,
122
+ request: {
123
+ url,
124
+ method,
125
+ headers,
126
+ bytes: postData?.length || 0,
127
+ },
128
+ response: {
129
+ status: -1,
130
+ mimeType: 'x-unknown',
131
+ headers: {},
132
+ timing: null,
133
+ },
122
134
  isNavigationRequest,
123
- status: 0,
124
- loadEndTime: -1,
135
+ requestSentTime: timestamp,
125
136
  responseReceivedTime: -1,
126
- response: null,
137
+ loadEndTime: -1,
127
138
  resourceSize: 0,
128
139
  transferSize: 0,
129
140
  timings: null,
@@ -145,6 +156,7 @@ export class NetworkManager {
145
156
  ...record.request.headers,
146
157
  ...headers,
147
158
  };
159
+ record.request.referrer = record.request.headers?.Referer;
148
160
  }
149
161
 
150
162
  _onDataReceived(event: Protocol.Network.dataReceivedPayload) {
@@ -163,12 +175,31 @@ export class NetworkManager {
163
175
  if (!record) {
164
176
  return;
165
177
  }
166
- Object.assign(record, {
178
+
179
+ record.responseReceivedTime = timestamp;
180
+ record.transferSize = response.encodedDataLength;
181
+ record.response = {
182
+ url: response.url,
167
183
  status: response.status,
168
- response,
169
- responseReceivedTime: timestamp,
170
- transferSize: response.encodedDataLength,
171
- });
184
+ protocol: response.protocol,
185
+ statusText: response.statusText,
186
+ headers: response.headers,
187
+ mimeType: response.mimeType,
188
+ remoteIPAddress: response.remoteIPAddress,
189
+ remotePort: response.remotePort,
190
+ fromServiceWorker: response.fromServiceWorker,
191
+ redirectURL: record.response.redirectURL,
192
+ timing: response.timing,
193
+ };
194
+ if (response.securityDetails) {
195
+ record.response.securityDetails = {
196
+ protocol: response.securityDetails.protocol,
197
+ subjectName: response.securityDetails.subjectName,
198
+ issuer: response.securityDetails.issuer,
199
+ validFrom: response.securityDetails.validFrom,
200
+ validTo: response.securityDetails.validTo,
201
+ };
202
+ }
172
203
  }
173
204
 
174
205
  _onLoadingFinished(event: Protocol.Network.loadingFinishedPayload) {
@@ -192,9 +223,12 @@ export class NetworkManager {
192
223
  }
193
224
  if (encodedDataLength >= 0) {
194
225
  record.transferSize = encodedDataLength;
226
+ record.response.body = {
227
+ bytes: encodedDataLength,
228
+ };
195
229
  }
196
230
  record.loadEndTime = endTime;
197
- record.timings = this.calculateTimings(record);
231
+ record.timings = calculateTimings(record);
198
232
  this.results.push(record);
199
233
  /**
200
234
  * When there is DNS failure, browsers keeps retrying requests which
@@ -207,159 +241,141 @@ export class NetworkManager {
207
241
  * Account for missing response received event and also adjust the
208
242
  * response received event based on when first byte event was recorded
209
243
  */
210
- getResponseReceivedTime(
211
- timing: Protocol.Network.Response['timing'],
212
- responseReceivedTime: number
213
- ) {
214
- if (timing == null) {
215
- return responseReceivedTime;
216
- }
217
- const startTime = timing.requestTime;
218
- const headersReceivedTime = startTime + timing.receiveHeadersEnd / 1000;
219
- if (
220
- responseReceivedTime < 0 ||
221
- responseReceivedTime > headersReceivedTime
222
- ) {
223
- responseReceivedTime = headersReceivedTime;
224
- }
225
- if (startTime > responseReceivedTime) {
226
- responseReceivedTime = startTime;
227
- }
228
- return responseReceivedTime;
229
- }
230
244
 
231
- /**
232
- * The timing calculations are based on the chrome devtools frontend
233
- * https://github.com/ChromeDevTools/devtools-frontend/blob/7f5478d8ceb7586f23f4073ab5c2085dac1ec26a/front_end/network/RequestTimingView.js#L98-L193
234
- */
235
- calculateTimings(record: NetworkInfo) {
236
- const result: NetworkInfo['timings'] = {
237
- blocked: -1,
238
- queueing: -1,
239
- proxy: -1,
240
- dns: -1,
241
- ssl: -1,
242
- connect: -1,
243
- send: -1,
244
- wait: -1,
245
- receive: -1,
246
- total: -1,
247
- };
248
- const { requestSentTime, loadEndTime, responseReceivedTime } = record;
245
+ stop() {
246
+ return this.results;
247
+ }
248
+ }
249
249
 
250
- /**
251
- * Handle when request failed and no responseReceived event was
252
- * fired for that particular request
253
- * Eg: connection refused by remote host
254
- */
255
- const response = record.response || {
256
- timing: null,
257
- fromServiceWorker: false,
258
- };
250
+ function getResponseReceivedTime(
251
+ timing: Protocol.Network.Response['timing'],
252
+ responseReceivedTime: number
253
+ ) {
254
+ if (timing == null) {
255
+ return responseReceivedTime;
256
+ }
257
+ const startTime = timing.requestTime;
258
+ const headersReceivedTime = startTime + timing.receiveHeadersEnd / 1000;
259
+ if (responseReceivedTime < 0 || responseReceivedTime > headersReceivedTime) {
260
+ responseReceivedTime = headersReceivedTime;
261
+ }
262
+ if (startTime > responseReceivedTime) {
263
+ responseReceivedTime = startTime;
264
+ }
265
+ return responseReceivedTime;
266
+ }
259
267
 
260
- const toMilliseconds = (time: number) => (time === -1 ? -1 : time * 1000);
261
- const calculateDiffInMs = (
262
- name: keyof NetworkInfo['timings'],
263
- start: number,
264
- end: number
265
- ) => {
266
- if (start < Number.MAX_VALUE && start <= end) {
267
- result[name] = toMilliseconds(end - start);
268
- }
269
- };
270
- const firstPositive = (numbers: number[]) => {
271
- for (let i = 0; i < numbers.length; ++i) {
272
- if (numbers[i] > 0) {
273
- return numbers[i];
274
- }
275
- }
276
- return null;
277
- };
278
- /**
279
- * requestTime is baseline in seconds, rest of the timing data are ticks in milliseconds
280
- * from the requestTime, so we calculate the offset from that
281
- */
282
- const addOffsetRange = (
283
- name: keyof NetworkInfo['timings'],
284
- start: number,
285
- end: number
286
- ) => {
287
- if (start >= 0 && end >= 0) {
288
- calculateDiffInMs(
289
- name,
290
- startTime + start / 1000,
291
- startTime + end / 1000
292
- );
293
- }
294
- };
295
- const timing = response.timing;
296
- const actResRcvdTime = this.getResponseReceivedTime(
297
- timing,
298
- responseReceivedTime
299
- );
300
- const issueTime = requestSentTime;
301
- const startTime = timing == null ? -1 : timing.requestTime;
302
- const endTime = firstPositive([loadEndTime, actResRcvdTime]) || startTime;
268
+ /**
269
+ * The timing calculations are based on the chrome devtools frontend
270
+ * https://github.com/ChromeDevTools/devtools-frontend/blob/7f5478d8ceb7586f23f4073ab5c2085dac1ec26a/front_end/network/RequestTimingView.js#L98-L193
271
+ */
272
+ export function calculateTimings(record: NetworkInfo) {
273
+ const result: NetworkInfo['timings'] = {
274
+ blocked: -1,
275
+ queueing: -1,
276
+ proxy: -1,
277
+ dns: -1,
278
+ ssl: -1,
279
+ connect: -1,
280
+ send: -1,
281
+ wait: -1,
282
+ receive: -1,
283
+ total: -1,
284
+ };
303
285
 
304
- if (timing == null) {
305
- const start =
306
- issueTime !== -1 ? issueTime : startTime !== -1 ? startTime : 0;
307
- const middle = actResRcvdTime === -1 ? Number.MAX_VALUE : actResRcvdTime;
308
- const end = endTime === -1 ? Number.MAX_VALUE : endTime;
309
- calculateDiffInMs('total', start, end);
310
- calculateDiffInMs('blocked', start, middle);
311
- calculateDiffInMs('receive', middle, end);
312
- // Check if the request is blocked/stalled for the whole timeframe and
313
- // calculate timings appropriately for that request
314
- if (!isFinite(result['blocked'])) {
315
- result['blocked'] = result['total'];
286
+ const toMilliseconds = (time: number) => (time === -1 ? -1 : time * 1000);
287
+ const calculateDiffInMs = (
288
+ name: keyof NetworkInfo['timings'],
289
+ start: number,
290
+ end: number
291
+ ) => {
292
+ if (start < Number.MAX_VALUE && start <= end) {
293
+ result[name] = toMilliseconds(end - start);
294
+ }
295
+ };
296
+ const firstPositive = (numbers: number[]) => {
297
+ for (let i = 0; i < numbers.length; ++i) {
298
+ if (numbers[i] > 0) {
299
+ return numbers[i];
316
300
  }
317
- return result;
318
301
  }
319
- calculateDiffInMs(
320
- 'total',
321
- issueTime < startTime ? issueTime : startTime,
322
- endTime
323
- );
324
- if (issueTime < startTime) {
325
- calculateDiffInMs('queueing', issueTime, startTime);
302
+ return null;
303
+ };
304
+ /**
305
+ * requestTime is baseline in seconds, rest of the timing data are ticks in milliseconds
306
+ * from the requestTime, so we calculate the offset from that
307
+ */
308
+ const addOffsetRange = (
309
+ name: keyof NetworkInfo['timings'],
310
+ start: number,
311
+ end: number
312
+ ) => {
313
+ if (start >= 0 && end >= 0) {
314
+ calculateDiffInMs(name, startTime + start / 1000, startTime + end / 1000);
326
315
  }
327
- const responseReceived = toMilliseconds(actResRcvdTime - startTime);
316
+ };
317
+ const { requestSentTime, loadEndTime, responseReceivedTime, response } =
318
+ record;
319
+ const timing = response.timing;
320
+ const actResRcvdTime = getResponseReceivedTime(timing, responseReceivedTime);
321
+ const issueTime = requestSentTime;
322
+ const startTime = timing == null ? -1 : timing.requestTime;
323
+ const endTime = firstPositive([loadEndTime, actResRcvdTime]) || startTime;
328
324
 
329
- if (response.fromServiceWorker) {
330
- addOffsetRange('blocked', 0, timing.workerStart);
331
- addOffsetRange('wait', timing.sendEnd, responseReceived);
332
- } else if (!timing.pushStart) {
333
- const blockingEnd =
334
- firstPositive([
335
- timing.dnsStart,
336
- timing.connectStart,
337
- timing.sendStart,
338
- responseReceived,
339
- ]) || 0;
340
- addOffsetRange('blocked', 0, blockingEnd);
341
- addOffsetRange('proxy', timing.proxyStart, timing.proxyEnd);
342
- addOffsetRange('dns', timing.dnsStart, timing.dnsEnd);
343
- addOffsetRange('connect', timing.connectStart, timing.connectEnd);
344
- addOffsetRange('ssl', timing.sslStart, timing.sslEnd);
345
- addOffsetRange('send', timing.sendStart, timing.sendEnd);
346
- addOffsetRange(
347
- 'wait',
348
- Math.max(
349
- timing.sendEnd,
350
- timing.connectEnd,
351
- timing.dnsEnd,
352
- timing.proxyEnd,
353
- blockingEnd
354
- ),
355
- responseReceived
356
- );
325
+ if (timing == null) {
326
+ const start =
327
+ issueTime !== -1 ? issueTime : startTime !== -1 ? startTime : 0;
328
+ const middle = actResRcvdTime === -1 ? Number.MAX_VALUE : actResRcvdTime;
329
+ const end = endTime === -1 ? Number.MAX_VALUE : endTime;
330
+ calculateDiffInMs('total', start, end);
331
+ calculateDiffInMs('blocked', start, middle);
332
+ calculateDiffInMs('receive', middle, end);
333
+ // Check if the request is blocked/stalled for the whole timeframe and
334
+ // calculate timings appropriately for that request
335
+ if (!isFinite(result['blocked'])) {
336
+ result['blocked'] = result['total'];
357
337
  }
358
- calculateDiffInMs('receive', actResRcvdTime, endTime);
359
338
  return result;
360
339
  }
340
+ calculateDiffInMs(
341
+ 'total',
342
+ issueTime < startTime ? issueTime : startTime,
343
+ endTime
344
+ );
345
+ if (issueTime < startTime) {
346
+ calculateDiffInMs('queueing', issueTime, startTime);
347
+ }
348
+ const responseReceived = toMilliseconds(actResRcvdTime - startTime);
361
349
 
362
- stop() {
363
- return this.results;
350
+ if (response.fromServiceWorker) {
351
+ addOffsetRange('blocked', 0, timing.workerStart);
352
+ addOffsetRange('wait', timing.sendEnd, responseReceived);
353
+ } else if (!timing.pushStart) {
354
+ const blockingEnd =
355
+ firstPositive([
356
+ timing.dnsStart,
357
+ timing.connectStart,
358
+ timing.sendStart,
359
+ responseReceived,
360
+ ]) || 0;
361
+ addOffsetRange('blocked', 0, blockingEnd);
362
+ addOffsetRange('proxy', timing.proxyStart, timing.proxyEnd);
363
+ addOffsetRange('dns', timing.dnsStart, timing.dnsEnd);
364
+ addOffsetRange('connect', timing.connectStart, timing.connectEnd);
365
+ addOffsetRange('ssl', timing.sslStart, timing.sslEnd);
366
+ addOffsetRange('send', timing.sendStart, timing.sendEnd);
367
+ addOffsetRange(
368
+ 'wait',
369
+ Math.max(
370
+ timing.sendEnd,
371
+ timing.connectEnd,
372
+ timing.dnsEnd,
373
+ timing.proxyEnd,
374
+ blockingEnd
375
+ ),
376
+ responseReceived
377
+ );
364
378
  }
379
+ calculateDiffInMs('receive', actResRcvdTime, endTime);
380
+ return result;
365
381
  }