@elastic/synthetics 1.0.0-beta.15 → 1.0.0-beta.19

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 (52) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/NOTICE.txt +29 -0
  3. package/README.md +1 -1
  4. package/dist/cli.js +34 -21
  5. package/dist/cli.js.map +1 -1
  6. package/dist/common_types.d.ts +10 -40
  7. package/dist/common_types.d.ts.map +1 -1
  8. package/dist/core/gatherer.d.ts.map +1 -1
  9. package/dist/core/gatherer.js +9 -1
  10. package/dist/core/gatherer.js.map +1 -1
  11. package/dist/core/logger.d.ts +0 -2
  12. package/dist/core/logger.d.ts.map +1 -1
  13. package/dist/core/logger.js +2 -15
  14. package/dist/core/logger.js.map +1 -1
  15. package/dist/core/runner.d.ts +19 -20
  16. package/dist/core/runner.d.ts.map +1 -1
  17. package/dist/core/runner.js +14 -5
  18. package/dist/core/runner.js.map +1 -1
  19. package/dist/formatter/javascript.d.ts +2 -0
  20. package/dist/formatter/javascript.d.ts.map +1 -1
  21. package/dist/formatter/javascript.js.map +1 -1
  22. package/dist/index.d.ts +1 -0
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +0 -6
  25. package/dist/index.js.map +1 -1
  26. package/dist/parse_args.d.ts.map +1 -1
  27. package/dist/parse_args.js +1 -1
  28. package/dist/parse_args.js.map +1 -1
  29. package/dist/plugins/network.d.ts +30 -11
  30. package/dist/plugins/network.d.ts.map +1 -1
  31. package/dist/plugins/network.js +234 -175
  32. package/dist/plugins/network.js.map +1 -1
  33. package/dist/reporters/base.d.ts.map +1 -1
  34. package/dist/reporters/base.js +6 -1
  35. package/dist/reporters/base.js.map +1 -1
  36. package/dist/reporters/json.d.ts +55 -5
  37. package/dist/reporters/json.d.ts.map +1 -1
  38. package/dist/reporters/json.js +45 -14
  39. package/dist/reporters/json.js.map +1 -1
  40. package/dist/tsconfig.tsbuildinfo +1 -1
  41. package/package.json +7 -7
  42. package/src/cli.ts +37 -25
  43. package/src/common_types.ts +10 -46
  44. package/src/core/gatherer.ts +8 -1
  45. package/src/core/logger.ts +1 -12
  46. package/src/core/runner.ts +42 -28
  47. package/src/formatter/javascript.ts +2 -0
  48. package/src/index.ts +1 -6
  49. package/src/parse_args.ts +1 -5
  50. package/src/plugins/network.ts +280 -213
  51. package/src/reporters/base.ts +6 -1
  52. package/src/reporters/json.ts +52 -16
@@ -23,269 +23,336 @@
23
23
  *
24
24
  */
25
25
 
26
- import { Page, Request, Response } from 'playwright-chromium';
26
+ import { Protocol } from 'playwright-chromium/types/protocol';
27
27
  import { NetworkInfo, BrowserInfo, Driver } from '../common_types';
28
28
  import { Step } from '../dsl';
29
29
  import { getTimestamp } from '../helpers';
30
30
 
31
- /**
32
- * Kibana UI expects the requestStartTime and loadEndTime to be baseline
33
- * in seconds as they have the logic to convert it to milliseconds before
34
- * using for offset calculation
35
- */
36
- function epochTimeInSeconds() {
37
- return getTimestamp() / 1e6;
38
- }
39
-
40
- /**
41
- * Used as a key in each Network Request to identify the
42
- * associated request across distinct lifecycle events
43
- */
44
- export const NETWORK_ENTRY_SUMBOL = Symbol.for('NetworkEntry');
45
-
46
- type RequestWithEntry = Request & {
47
- NETWORK_ENTRY_SUMBOL?: symbol;
48
- };
49
-
50
31
  export class NetworkManager {
51
32
  private _browser: BrowserInfo;
52
- private _barrierPromises = new Set<Promise<void>>();
33
+ private _InflightRequestMap = new Map<string, NetworkInfo>();
53
34
  results: Array<NetworkInfo> = [];
54
35
  _currentStep: Partial<Step> = null;
55
36
 
56
37
  constructor(private driver: Driver) {}
57
38
 
58
- private _addBarrier(page: Page, promise: Promise<void>) {
59
- const race = Promise.race([
60
- new Promise<void>(resolve =>
61
- page.on('close', () => {
62
- this._barrierPromises.delete(race);
63
- resolve();
64
- })
65
- ),
66
- promise,
67
- ]) as Promise<void>;
68
- this._barrierPromises.add(race);
69
- }
70
-
71
39
  async start() {
72
- const { client, context } = this.driver;
40
+ const { client } = this.driver;
73
41
  const { product } = await client.send('Browser.getVersion');
74
42
  const [name, version] = product.split('/');
75
43
  this._browser = { name, version };
76
- context.on('request', this._onRequest.bind(this));
77
- context.on('response', this._onResponse.bind(this));
78
- context.on('requestfinished', this._onRequestCompleted.bind(this));
79
- context.on('requestfailed', this._onRequestCompleted.bind(this));
44
+ await client.send('Network.enable');
45
+ /**
46
+ * Listen for all network events
47
+ */
48
+ client.on(
49
+ 'Network.requestWillBeSent',
50
+ this._onRequestWillBeSent.bind(this)
51
+ );
52
+ client.on(
53
+ 'Network.requestWillBeSentExtraInfo',
54
+ this._onRequestWillBeSentExtraInfo.bind(this)
55
+ );
56
+ client.on('Network.dataReceived', this._onDataReceived.bind(this));
57
+ client.on('Network.responseReceived', this._onResponseReceived.bind(this));
58
+ client.on('Network.loadingFinished', this._onLoadingFinished.bind(this));
59
+ client.on('Network.loadingFailed', this._onLoadingFailed.bind(this));
80
60
  }
81
61
 
82
- private _findNetworkEntry(
83
- request: RequestWithEntry
84
- ): NetworkInfo | undefined {
85
- return request[NETWORK_ENTRY_SUMBOL];
62
+ _findNetworkRecord(requestId: string) {
63
+ return this._InflightRequestMap.get(requestId);
86
64
  }
87
65
 
88
- private _onRequest(request: Request) {
89
- const url = request.url();
66
+ _onRequestWillBeSent(event: Protocol.Network.requestWillBeSentPayload) {
67
+ const { requestId, request, timestamp, type, loaderId } = event;
68
+ const { url, method } = request;
90
69
  /**
91
70
  * Data URI should not show up as network requests
92
71
  */
93
72
  if (url.startsWith('data:')) {
94
73
  return;
95
74
  }
75
+ const isNavigationRequest = requestId == loaderId && type === 'Document';
76
+ const record = this._findNetworkRecord(requestId);
77
+ /**
78
+ * On redirects, another `requestWillBeSent` event will be fired for the
79
+ * same requestId. We calculate the timings for the redirect request using
80
+ * the `redirectedResponse` from the redirected request.
81
+ */
82
+ if (record) {
83
+ if (event.redirectResponse) {
84
+ const response = event.redirectResponse;
85
+ const data = Object.assign(event, {
86
+ type: event.type,
87
+ response,
88
+ encodedDataLength: response.encodedDataLength,
89
+ });
90
+ this._onResponseReceived(data);
91
+ this._onLoadingFinished(data);
92
+ } else {
93
+ /**
94
+ * Edge case, we handle it to proceed with next navigation
95
+ */
96
+ this._onLoadingFailed(
97
+ Object.assign(event, {
98
+ type: event.type,
99
+ errorText: 'redirectResponse data is not available',
100
+ })
101
+ );
102
+ }
103
+ const redirectedRecord = this._findNetworkRecord(requestId);
104
+ /**
105
+ * Rewrite the map with new redirect id to not reset
106
+ * the redirect request with original request
107
+ */
108
+ this._InflightRequestMap.delete(requestId);
109
+ this._InflightRequestMap.set(
110
+ `redirect:${timestamp}:${requestId}`,
111
+ redirectedRecord
112
+ );
113
+ }
96
114
 
97
- const timestamp = getTimestamp();
98
- const networkEntry: NetworkInfo = {
115
+ this._InflightRequestMap.set(requestId, {
99
116
  browser: this._browser,
100
117
  step: this._currentStep,
101
- timestamp,
118
+ timestamp: getTimestamp(),
102
119
  url,
103
- type: request.resourceType(),
104
- method: request.method(),
105
- requestSentTime: epochTimeInSeconds(),
106
- request: {
107
- url,
108
- method: request.method(),
109
- headers: {},
110
- },
111
- response: {
112
- statusCode: -1,
113
- headers: {},
114
- redirectURL: '',
115
- },
116
- isNavigationRequest: request.isNavigationRequest(),
117
- status: -1,
120
+ request,
121
+ type,
122
+ method,
123
+ requestSentTime: timestamp,
124
+ isNavigationRequest,
125
+ status: 0,
118
126
  loadEndTime: -1,
119
127
  responseReceivedTime: -1,
128
+ response: null,
120
129
  resourceSize: 0,
121
130
  transferSize: 0,
122
131
  timings: null,
132
+ });
133
+ }
134
+
135
+ _onRequestWillBeSentExtraInfo(
136
+ event: Protocol.Network.requestWillBeSentExtraInfoPayload
137
+ ) {
138
+ const { requestId, headers } = event;
139
+ const record = this._findNetworkRecord(requestId);
140
+ if (!record) {
141
+ return;
142
+ }
143
+ /**
144
+ * Enhance request headers with additional information
145
+ */
146
+ record.request.headers = {
147
+ ...record.request.headers,
148
+ ...headers,
123
149
  };
150
+ }
124
151
 
125
- if (request.redirectedFrom()) {
126
- const fromEntry = this._findNetworkEntry(request.redirectedFrom());
127
- if (fromEntry) fromEntry.response.redirectURL = request.url();
152
+ _onDataReceived(event: Protocol.Network.dataReceivedPayload) {
153
+ const { requestId, dataLength, encodedDataLength } = event;
154
+ const record = this._findNetworkRecord(requestId);
155
+ if (!record) {
156
+ return;
128
157
  }
129
- request[NETWORK_ENTRY_SUMBOL] = networkEntry;
130
- this.results.push(networkEntry);
158
+ record.resourceSize += dataLength;
159
+ if (encodedDataLength >= 0) record.transferSize += encodedDataLength;
131
160
  }
132
161
 
133
- private async _onResponse(response: Response) {
134
- const request = response.request();
135
- const networkEntry = this._findNetworkEntry(request);
136
- if (!networkEntry) return;
162
+ _onResponseReceived(event: Protocol.Network.responseReceivedPayload) {
163
+ const { requestId, response, timestamp } = event;
164
+ const record = this._findNetworkRecord(requestId);
165
+ if (!record) {
166
+ return;
167
+ }
168
+ Object.assign(record, {
169
+ status: response.status,
170
+ response,
171
+ responseReceivedTime: timestamp,
172
+ transferSize: response.encodedDataLength,
173
+ });
174
+ }
137
175
 
138
- networkEntry.status = response.status();
139
- networkEntry.responseReceivedTime = epochTimeInSeconds();
140
- networkEntry.response = {
141
- url: response.url(),
142
- statusCode: response.status(),
143
- statusText: response.statusText(),
144
- headers: {},
145
- redirectURL: networkEntry.response.redirectURL,
146
- };
176
+ _onLoadingFinished(event: Protocol.Network.loadingFinishedPayload) {
177
+ const { requestId, timestamp, encodedDataLength } = event;
178
+ this._requestCompleted(requestId, timestamp, encodedDataLength);
179
+ }
147
180
 
148
- const page = request.frame().page();
149
- this._addBarrier(
150
- page,
151
- request.allHeaders().then(reqHeaders => {
152
- networkEntry.request.headers = reqHeaders;
153
- networkEntry.request.referrer = reqHeaders?.referer;
154
- })
155
- );
156
- this._addBarrier(
157
- page,
158
- response.serverAddr().then(server => {
159
- networkEntry.response.remoteIPAddress = server?.ipAddress;
160
- networkEntry.response.remotePort = server?.port;
161
- })
162
- );
163
- this._addBarrier(
164
- page,
165
- response.securityDetails().then(details => {
166
- networkEntry.response.securityDetails = details;
167
- })
168
- );
169
- this._addBarrier(
170
- page,
171
- response.allHeaders().then(resHeaders => {
172
- networkEntry.response.headers = resHeaders;
181
+ _onLoadingFailed(event: Protocol.Network.loadingFailedPayload) {
182
+ const { requestId, timestamp } = event;
183
+ this._requestCompleted(requestId, timestamp);
184
+ }
173
185
 
174
- const mimeType = resHeaders['content-type']
175
- ? resHeaders['content-type'].split(';')[0]
176
- : 'unknown';
177
- networkEntry.response.mimeType = mimeType;
178
- })
179
- );
186
+ _requestCompleted(
187
+ requestId: string,
188
+ endTime: number,
189
+ encodedDataLength?: number
190
+ ) {
191
+ const record = this._findNetworkRecord(requestId);
192
+ if (!record) {
193
+ return;
194
+ }
195
+ if (encodedDataLength >= 0) {
196
+ record.transferSize = encodedDataLength;
197
+ }
198
+ record.loadEndTime = endTime;
199
+ record.timings = calculateTimings(record);
200
+ this.results.push(record);
201
+ /**
202
+ * When there is DNS failure, browsers keeps retrying requests which
203
+ * might result in incorrect timings so we get remove them to measure only once
204
+ */
205
+ this._InflightRequestMap.delete(requestId);
180
206
  }
181
207
 
182
- private async _onRequestCompleted(request: Request) {
183
- const networkEntry = this._findNetworkEntry(request);
184
- if (!networkEntry) return;
185
- networkEntry.loadEndTime = epochTimeInSeconds();
208
+ /**
209
+ * Account for missing response received event and also adjust the
210
+ * response received event based on when first byte event was recorded
211
+ */
186
212
 
187
- const timing = request.timing();
188
- const { loadEndTime, requestSentTime } = networkEntry;
189
- networkEntry.timings = {
190
- blocked: -1,
191
- dns: -1,
192
- ssl: -1,
193
- connect: -1,
194
- send: -1,
195
- wait: -1,
196
- receive: -1,
197
- total: -1,
198
- };
213
+ stop() {
214
+ return this.results;
215
+ }
216
+ }
199
217
 
200
- const firstPositive = (numbers: number[]) => {
201
- for (let i = 0; i < numbers.length; ++i) {
202
- if (numbers[i] > 0) {
203
- return numbers[i];
204
- }
205
- }
206
- return null;
207
- };
208
- const roundMilliSecs = (value: number): number => {
209
- return Math.floor(value * 1000) / 1000;
210
- };
218
+ function getResponseReceivedTime(
219
+ timing: Protocol.Network.Response['timing'],
220
+ responseReceivedTime: number
221
+ ) {
222
+ if (timing == null) {
223
+ return responseReceivedTime;
224
+ }
225
+ const startTime = timing.requestTime;
226
+ const headersReceivedTime = startTime + timing.receiveHeadersEnd / 1000;
227
+ if (responseReceivedTime < 0 || responseReceivedTime > headersReceivedTime) {
228
+ responseReceivedTime = headersReceivedTime;
229
+ }
230
+ if (startTime > responseReceivedTime) {
231
+ responseReceivedTime = startTime;
232
+ }
233
+ return responseReceivedTime;
234
+ }
211
235
 
212
- if (timing.startTime === 0) {
213
- // Convert to milliseconds before round off
214
- const total = roundMilliSecs((loadEndTime - requestSentTime) * 1000);
215
- networkEntry.timings.total = total;
216
- return;
217
- }
236
+ /**
237
+ * The timing calculations are based on the chrome devtools frontend
238
+ * https://github.com/ChromeDevTools/devtools-frontend/blob/7f5478d8ceb7586f23f4073ab5c2085dac1ec26a/front_end/network/RequestTimingView.js#L98-L193
239
+ */
240
+ export function calculateTimings(record: NetworkInfo) {
241
+ const result: NetworkInfo['timings'] = {
242
+ blocked: -1,
243
+ queueing: -1,
244
+ proxy: -1,
245
+ dns: -1,
246
+ ssl: -1,
247
+ connect: -1,
248
+ send: -1,
249
+ wait: -1,
250
+ receive: -1,
251
+ total: -1,
252
+ };
253
+ const { requestSentTime, loadEndTime, responseReceivedTime } = record;
218
254
 
219
- const blocked =
220
- roundMilliSecs(
221
- firstPositive([
222
- timing.domainLookupStart,
223
- timing.connectStart,
224
- timing.requestStart,
225
- ])
226
- ) || -1;
227
- const dns =
228
- timing.domainLookupEnd !== -1
229
- ? roundMilliSecs(timing.domainLookupEnd - timing.domainLookupStart)
230
- : -1;
231
- const connect =
232
- timing.connectEnd !== -1
233
- ? roundMilliSecs(timing.connectEnd - timing.connectStart)
234
- : -1;
235
- const ssl =
236
- timing.secureConnectionStart !== -1
237
- ? roundMilliSecs(timing.connectEnd - timing.secureConnectionStart)
238
- : -1;
239
- const wait =
240
- timing.responseStart !== -1
241
- ? roundMilliSecs(timing.responseStart - timing.requestStart)
242
- : -1;
243
- const receive =
244
- timing.responseEnd !== -1
245
- ? roundMilliSecs(timing.responseEnd - timing.responseStart)
246
- : -1;
247
- const total = [blocked, dns, connect, wait, receive].reduce(
248
- (pre, cur) => (cur > 0 ? cur + pre : pre),
249
- 0
250
- );
251
- networkEntry.timings = {
252
- blocked,
253
- dns,
254
- connect,
255
- ssl,
256
- wait,
257
- send: 0, // not exposed via RT api
258
- receive,
259
- total: roundMilliSecs(total),
260
- };
255
+ /**
256
+ * Handle when request failed and no responseReceived event was
257
+ * fired for that particular request
258
+ * Eg: connection refused by remote host
259
+ */
260
+ const response = record.response || {
261
+ timing: null,
262
+ fromServiceWorker: false,
263
+ };
261
264
 
262
- const page = request.frame().page();
263
- // For aborted/failed requests sizes does not exist
264
- this._addBarrier(
265
- page,
266
- request.sizes().then(sizes => {
267
- networkEntry.request.bytes =
268
- sizes.requestHeadersSize + sizes.requestBodySize;
269
- networkEntry.request.body = {
270
- bytes: sizes.requestBodySize,
271
- };
272
- networkEntry.response.bytes =
273
- sizes.responseHeadersSize + sizes.responseBodySize;
274
- networkEntry.response.body = {
275
- bytes: sizes.responseBodySize,
276
- };
277
- networkEntry.transferSize = sizes.responseBodySize;
278
- })
279
- );
265
+ const toMilliseconds = (time: number) => (time === -1 ? -1 : time * 1000);
266
+ const calculateDiffInMs = (
267
+ name: keyof NetworkInfo['timings'],
268
+ start: number,
269
+ end: number
270
+ ) => {
271
+ if (start < Number.MAX_VALUE && start <= end) {
272
+ result[name] = toMilliseconds(end - start);
273
+ }
274
+ };
275
+ const firstPositive = (numbers: number[]) => {
276
+ for (let i = 0; i < numbers.length; ++i) {
277
+ if (numbers[i] > 0) {
278
+ return numbers[i];
279
+ }
280
+ }
281
+ return null;
282
+ };
283
+ /**
284
+ * requestTime is baseline in seconds, rest of the timing data are ticks in milliseconds
285
+ * from the requestTime, so we calculate the offset from that
286
+ */
287
+ const addOffsetRange = (
288
+ name: keyof NetworkInfo['timings'],
289
+ start: number,
290
+ end: number
291
+ ) => {
292
+ if (start >= 0 && end >= 0) {
293
+ calculateDiffInMs(name, startTime + start / 1000, startTime + end / 1000);
294
+ }
295
+ };
296
+ const timing = response.timing;
297
+ const actResRcvdTime = getResponseReceivedTime(timing, responseReceivedTime);
298
+ const issueTime = requestSentTime;
299
+ const startTime = timing == null ? -1 : timing.requestTime;
300
+ const endTime = firstPositive([loadEndTime, actResRcvdTime]) || startTime;
301
+
302
+ if (timing == null) {
303
+ const start =
304
+ issueTime !== -1 ? issueTime : startTime !== -1 ? startTime : 0;
305
+ const middle = actResRcvdTime === -1 ? Number.MAX_VALUE : actResRcvdTime;
306
+ const end = endTime === -1 ? Number.MAX_VALUE : endTime;
307
+ calculateDiffInMs('total', start, end);
308
+ calculateDiffInMs('blocked', start, middle);
309
+ calculateDiffInMs('receive', middle, end);
310
+ // Check if the request is blocked/stalled for the whole timeframe and
311
+ // calculate timings appropriately for that request
312
+ if (!isFinite(result['blocked'])) {
313
+ result['blocked'] = result['total'];
314
+ }
315
+ return result;
316
+ }
317
+ calculateDiffInMs(
318
+ 'total',
319
+ issueTime < startTime ? issueTime : startTime,
320
+ endTime
321
+ );
322
+ if (issueTime < startTime) {
323
+ calculateDiffInMs('queueing', issueTime, startTime);
280
324
  }
325
+ const responseReceived = toMilliseconds(actResRcvdTime - startTime);
281
326
 
282
- stop() {
283
- const context = this.driver.context;
284
- context.on('request', this._onRequest.bind(this));
285
- context.on('response', this._onResponse.bind(this));
286
- context.on('requestfinished', this._onRequestCompleted.bind(this));
287
- context.on('requestfailed', this._onRequestCompleted.bind(this));
288
- this._barrierPromises.clear();
289
- return this.results;
327
+ if (response.fromServiceWorker) {
328
+ addOffsetRange('blocked', 0, timing.workerStart);
329
+ addOffsetRange('wait', timing.sendEnd, responseReceived);
330
+ } else if (!timing.pushStart) {
331
+ const blockingEnd =
332
+ firstPositive([
333
+ timing.dnsStart,
334
+ timing.connectStart,
335
+ timing.sendStart,
336
+ responseReceived,
337
+ ]) || 0;
338
+ addOffsetRange('blocked', 0, blockingEnd);
339
+ addOffsetRange('proxy', timing.proxyStart, timing.proxyEnd);
340
+ addOffsetRange('dns', timing.dnsStart, timing.dnsEnd);
341
+ addOffsetRange('connect', timing.connectStart, timing.connectEnd);
342
+ addOffsetRange('ssl', timing.sslStart, timing.sslEnd);
343
+ addOffsetRange('send', timing.sendStart, timing.sendEnd);
344
+ addOffsetRange(
345
+ 'wait',
346
+ Math.max(
347
+ timing.sendEnd,
348
+ timing.connectEnd,
349
+ timing.dnsEnd,
350
+ timing.proxyEnd,
351
+ blockingEnd
352
+ ),
353
+ responseReceived
354
+ );
290
355
  }
356
+ calculateDiffInMs('receive', actResRcvdTime, endTime);
357
+ return result;
291
358
  }
@@ -64,7 +64,12 @@ export default class BaseReporter {
64
64
 
65
65
  constructor(public runner: Runner, options: ReporterOptions = {}) {
66
66
  this.fd = options.fd || process.stdout.fd;
67
- this.stream = new SonicBoom({ fd: this.fd, sync: true });
67
+ /**
68
+ * minLength is set to 1 byte to make sure we flush the
69
+ * content even if its the last byte on the stream buffer
70
+ * before destroying the pipe with underlying file descriptor
71
+ */
72
+ this.stream = new SonicBoom({ fd: this.fd, sync: true, minLength: 1 });
68
73
  this._registerListeners();
69
74
  this.runner.on('end', () => this.stream.flush());
70
75
  }