@elastic/synthetics 1.0.0-beta.24 → 1.0.0-beta.25

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.
@@ -23,360 +23,284 @@
23
23
  *
24
24
  */
25
25
 
26
- import { Protocol } from 'playwright-core/types/protocol';
26
+ import { Page, Request, Response } from 'playwright-chromium';
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
+
31
50
  export class NetworkManager {
32
51
  private _browser: BrowserInfo;
33
- private _InflightRequestMap = new Map<string, NetworkInfo>();
52
+ private _barrierPromises = new Set<Promise<void>>();
34
53
  results: Array<NetworkInfo> = [];
35
54
  _currentStep: Partial<Step> = null;
36
55
 
37
56
  constructor(private driver: Driver) {}
38
57
 
58
+ /**
59
+ * Adds a protection barrier aganist all asynchronous extract operations from
60
+ * request/response object that are happening during page lifecycle, If the
61
+ * page is closed during the extraction, the barrier enforces those operations
62
+ * to not result in exception
63
+ */
64
+ private _addBarrier(page: Page, promise: Promise<void>) {
65
+ if (!page) return;
66
+ const race = Promise.race([
67
+ new Promise<void>(resolve =>
68
+ page.on('close', () => {
69
+ this._barrierPromises.delete(race);
70
+ resolve();
71
+ })
72
+ ),
73
+ promise,
74
+ ]);
75
+ this._barrierPromises.add(race);
76
+ race.then(() => this._barrierPromises.delete(race));
77
+ }
78
+
39
79
  async start() {
40
- const { client } = this.driver;
80
+ const { client, context } = this.driver;
41
81
  const { product } = await client.send('Browser.getVersion');
42
82
  const [name, version] = product.split('/');
43
83
  this._browser = { name, version };
44
84
  await client.send('Network.enable');
45
85
  /**
46
- * Listen for all network events
86
+ * Listen for all network events from PW context
47
87
  */
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));
88
+ context.on('request', this._onRequest.bind(this));
89
+ context.on('response', this._onResponse.bind(this));
90
+ context.on('requestfinished', this._onRequestCompleted.bind(this));
91
+ context.on('requestfailed', this._onRequestCompleted.bind(this));
60
92
  }
61
93
 
62
- _findNetworkRecord(requestId: string) {
63
- return this._InflightRequestMap.get(requestId);
94
+ private _findNetworkEntry(
95
+ request: RequestWithEntry
96
+ ): NetworkInfo | undefined {
97
+ return request[NETWORK_ENTRY_SUMBOL];
64
98
  }
65
99
 
66
- _onRequestWillBeSent(event: Protocol.Network.requestWillBeSentPayload) {
67
- const { url, method, postData, headers } = event.request;
100
+ private _onRequest(request: Request) {
101
+ const url = request.url();
68
102
  /**
69
103
  * Data URI should not show up as network requests
70
104
  */
71
105
  if (url.startsWith('data:')) {
72
106
  return;
73
107
  }
74
- const { requestId, timestamp, type, loaderId } = event;
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
- record.response.redirectURL = url;
85
- const response = event.redirectResponse;
86
- const data = Object.assign(event, {
87
- type: event.type,
88
- response,
89
- encodedDataLength: response.encodedDataLength,
90
- hasExtraInfo: false,
91
- });
92
- this._onResponseReceived(data);
93
- this._onLoadingFinished(data);
94
- } else {
95
- /**
96
- * Edge case, we handle it to proceed with next navigation
97
- */
98
- this._onLoadingFailed(
99
- Object.assign(event, {
100
- type: event.type,
101
- errorText: 'redirectResponse data is not available',
102
- })
103
- );
104
- }
105
- const redirectedRecord = this._findNetworkRecord(requestId);
106
- /**
107
- * Rewrite the map with new redirect id to not reset
108
- * the redirect request with original request
109
- */
110
- this._InflightRequestMap.delete(requestId);
111
- this._InflightRequestMap.set(
112
- `redirect:${timestamp}:${requestId}`,
113
- redirectedRecord
114
- );
115
- }
116
108
 
117
- this._InflightRequestMap.set(requestId, {
109
+ const timestamp = getTimestamp();
110
+ const networkEntry: NetworkInfo = {
118
111
  browser: this._browser,
119
112
  step: this._currentStep,
120
- timestamp: getTimestamp(),
113
+ timestamp,
121
114
  url,
122
- type,
115
+ type: request.resourceType(),
123
116
  request: {
124
117
  url,
125
- method,
126
- headers,
127
- bytes: postData?.length || 0,
118
+ method: request.method(),
119
+ headers: {},
120
+ body: {
121
+ bytes: request.postDataBuffer()?.length || 0,
122
+ },
128
123
  },
129
124
  response: {
130
125
  status: -1,
131
- mimeType: 'x-unknown',
132
126
  headers: {},
133
- timing: null,
127
+ mimeType: 'x-unknown',
128
+ redirectURL: '',
134
129
  },
135
- isNavigationRequest,
136
- requestSentTime: timestamp,
137
- responseReceivedTime: -1,
130
+ isNavigationRequest: request.isNavigationRequest(),
131
+ requestSentTime: epochTimeInSeconds(),
138
132
  loadEndTime: -1,
133
+ responseReceivedTime: -1,
139
134
  resourceSize: 0,
140
135
  transferSize: 0,
141
136
  timings: null,
142
- });
143
- }
144
-
145
- _onRequestWillBeSentExtraInfo(
146
- event: Protocol.Network.requestWillBeSentExtraInfoPayload
147
- ) {
148
- const { requestId, headers } = event;
149
- const record = this._findNetworkRecord(requestId);
150
- if (!record) {
151
- return;
152
- }
153
- /**
154
- * Enhance request headers with additional information
155
- */
156
- record.request.headers = {
157
- ...record.request.headers,
158
- ...headers,
159
137
  };
160
- record.request.referrer = record.request.headers?.Referer;
161
- }
162
-
163
- _onDataReceived(event: Protocol.Network.dataReceivedPayload) {
164
- const { requestId, dataLength, encodedDataLength } = event;
165
- const record = this._findNetworkRecord(requestId);
166
- if (!record) {
167
- return;
168
- }
169
- record.resourceSize += dataLength;
170
- if (encodedDataLength >= 0) record.transferSize += encodedDataLength;
171
- }
172
138
 
173
- _onResponseReceived(event: Protocol.Network.responseReceivedPayload) {
174
- const { requestId, response, timestamp } = event;
175
- const record = this._findNetworkRecord(requestId);
176
- if (!record) {
177
- return;
139
+ if (request.redirectedFrom()) {
140
+ const fromEntry = this._findNetworkEntry(request.redirectedFrom());
141
+ if (fromEntry) fromEntry.response.redirectURL = request.url();
178
142
  }
179
143
 
180
- record.responseReceivedTime = timestamp;
181
- record.transferSize = response.encodedDataLength;
182
- record.response = {
183
- url: response.url,
184
- status: response.status,
185
- protocol: response.protocol,
186
- statusText: response.statusText,
187
- headers: response.headers,
188
- mimeType: response.mimeType,
189
- remoteIPAddress: response.remoteIPAddress,
190
- remotePort: response.remotePort,
191
- fromServiceWorker: response.fromServiceWorker,
192
- redirectURL: record.response.redirectURL,
193
- timing: response.timing,
194
- };
195
- if (response.securityDetails) {
196
- record.response.securityDetails = {
197
- protocol: response.securityDetails.protocol,
198
- subjectName: response.securityDetails.subjectName,
199
- issuer: response.securityDetails.issuer,
200
- validFrom: response.securityDetails.validFrom,
201
- validTo: response.securityDetails.validTo,
202
- };
203
- }
204
- }
205
-
206
- _onLoadingFinished(event: Protocol.Network.loadingFinishedPayload) {
207
- const { requestId, timestamp, encodedDataLength } = event;
208
- this._requestCompleted(requestId, timestamp, encodedDataLength);
144
+ request[NETWORK_ENTRY_SUMBOL] = networkEntry;
145
+ this.results.push(networkEntry);
209
146
  }
210
147
 
211
- _onLoadingFailed(event: Protocol.Network.loadingFailedPayload) {
212
- const { requestId, timestamp } = event;
213
- this._requestCompleted(requestId, timestamp);
214
- }
148
+ private async _onResponse(response: Response) {
149
+ const request = response.request();
150
+ const networkEntry = this._findNetworkEntry(request);
151
+ if (!networkEntry) return;
215
152
 
216
- _requestCompleted(
217
- requestId: string,
218
- endTime: number,
219
- encodedDataLength?: number
220
- ) {
221
- const record = this._findNetworkRecord(requestId);
222
- if (!record) {
223
- return;
224
- }
225
- if (encodedDataLength >= 0) {
226
- record.transferSize = encodedDataLength;
227
- record.response.body = {
228
- bytes: encodedDataLength,
229
- };
230
- }
231
- record.loadEndTime = endTime;
232
- record.timings = calculateTimings(record);
233
- this.results.push(record);
234
- /**
235
- * When there is DNS failure, browsers keeps retrying requests which
236
- * might result in incorrect timings so we get remove them to measure only once
237
- */
238
- this._InflightRequestMap.delete(requestId);
239
- }
153
+ networkEntry.responseReceivedTime = epochTimeInSeconds();
154
+ networkEntry.response = {
155
+ url: response.url(),
156
+ status: response.status(),
157
+ statusText: response.statusText(),
158
+ headers: {},
159
+ mimeType: 'x-unknown',
160
+ redirectURL: networkEntry.response.redirectURL,
161
+ };
240
162
 
241
- /**
242
- * Account for missing response received event and also adjust the
243
- * response received event based on when first byte event was recorded
244
- */
163
+ const page = request.frame().page();
164
+ this._addBarrier(
165
+ page,
166
+ request.allHeaders().then(reqHeaders => {
167
+ networkEntry.request.headers = reqHeaders;
168
+ networkEntry.request.referrer = reqHeaders?.referer;
169
+ })
170
+ );
171
+ this._addBarrier(
172
+ page,
173
+ response.serverAddr().then(server => {
174
+ networkEntry.response.remoteIPAddress = server?.ipAddress;
175
+ networkEntry.response.remotePort = server?.port;
176
+ })
177
+ );
178
+ this._addBarrier(
179
+ page,
180
+ response.securityDetails().then(details => {
181
+ if (details) networkEntry.response.securityDetails = details;
182
+ })
183
+ );
184
+ this._addBarrier(
185
+ page,
186
+ response.allHeaders().then(resHeaders => {
187
+ networkEntry.response.headers = resHeaders;
245
188
 
246
- stop() {
247
- return this.results;
189
+ const contentType = resHeaders['content-type'];
190
+ if (contentType)
191
+ networkEntry.response.mimeType = contentType.split(';')[0];
192
+ })
193
+ );
248
194
  }
249
- }
250
195
 
251
- function getResponseReceivedTime(
252
- timing: Protocol.Network.Response['timing'],
253
- responseReceivedTime: number
254
- ) {
255
- if (timing == null) {
256
- return responseReceivedTime;
257
- }
258
- const startTime = timing.requestTime;
259
- const headersReceivedTime = startTime + timing.receiveHeadersEnd / 1000;
260
- if (responseReceivedTime < 0 || responseReceivedTime > headersReceivedTime) {
261
- responseReceivedTime = headersReceivedTime;
262
- }
263
- if (startTime > responseReceivedTime) {
264
- responseReceivedTime = startTime;
265
- }
266
- return responseReceivedTime;
267
- }
196
+ private async _onRequestCompleted(request: Request) {
197
+ const networkEntry = this._findNetworkEntry(request);
198
+ if (!networkEntry) return;
268
199
 
269
- /**
270
- * The timing calculations are based on the chrome devtools frontend
271
- * https://github.com/ChromeDevTools/devtools-frontend/blob/7f5478d8ceb7586f23f4073ab5c2085dac1ec26a/front_end/network/RequestTimingView.js#L98-L193
272
- */
273
- export function calculateTimings(record: NetworkInfo) {
274
- const result: NetworkInfo['timings'] = {
275
- blocked: -1,
276
- queueing: -1,
277
- proxy: -1,
278
- dns: -1,
279
- ssl: -1,
280
- connect: -1,
281
- send: -1,
282
- wait: -1,
283
- receive: -1,
284
- total: -1,
285
- };
200
+ networkEntry.loadEndTime = epochTimeInSeconds();
201
+ const timing = request.timing();
202
+ const { loadEndTime, requestSentTime } = networkEntry;
203
+ networkEntry.timings = {
204
+ blocked: -1,
205
+ dns: -1,
206
+ ssl: -1,
207
+ connect: -1,
208
+ send: -1,
209
+ wait: -1,
210
+ receive: -1,
211
+ total: -1,
212
+ };
286
213
 
287
- const toMilliseconds = (time: number) => (time === -1 ? -1 : time * 1000);
288
- const calculateDiffInMs = (
289
- name: keyof NetworkInfo['timings'],
290
- start: number,
291
- end: number
292
- ) => {
293
- if (start < Number.MAX_VALUE && start <= end) {
294
- result[name] = toMilliseconds(end - start);
295
- }
296
- };
297
- const firstPositive = (numbers: number[]) => {
298
- for (let i = 0; i < numbers.length; ++i) {
299
- if (numbers[i] > 0) {
300
- return numbers[i];
214
+ const firstPositive = (numbers: number[]) => {
215
+ for (let i = 0; i < numbers.length; ++i) {
216
+ if (numbers[i] > 0) {
217
+ return numbers[i];
218
+ }
301
219
  }
302
- }
303
- return null;
304
- };
305
- /**
306
- * requestTime is baseline in seconds, rest of the timing data are ticks in milliseconds
307
- * from the requestTime, so we calculate the offset from that
308
- */
309
- const addOffsetRange = (
310
- name: keyof NetworkInfo['timings'],
311
- start: number,
312
- end: number
313
- ) => {
314
- if (start >= 0 && end >= 0) {
315
- calculateDiffInMs(name, startTime + start / 1000, startTime + end / 1000);
316
- }
317
- };
318
- const { requestSentTime, loadEndTime, responseReceivedTime, response } =
319
- record;
320
- const timing = response.timing;
321
- const actResRcvdTime = getResponseReceivedTime(timing, responseReceivedTime);
322
- const issueTime = requestSentTime;
323
- const startTime = timing == null ? -1 : timing.requestTime;
324
- const endTime = firstPositive([loadEndTime, actResRcvdTime]) || startTime;
220
+ return null;
221
+ };
222
+ const roundMilliSecs = (value: number): number => {
223
+ return Math.floor(value * 1000) / 1000;
224
+ };
325
225
 
326
- if (timing == null) {
327
- const start =
328
- issueTime !== -1 ? issueTime : startTime !== -1 ? startTime : 0;
329
- const middle = actResRcvdTime === -1 ? Number.MAX_VALUE : actResRcvdTime;
330
- const end = endTime === -1 ? Number.MAX_VALUE : endTime;
331
- calculateDiffInMs('total', start, end);
332
- calculateDiffInMs('blocked', start, middle);
333
- calculateDiffInMs('receive', middle, end);
334
- // Check if the request is blocked/stalled for the whole timeframe and
335
- // calculate timings appropriately for that request
336
- if (!isFinite(result['blocked'])) {
337
- result['blocked'] = result['total'];
226
+ if (timing.startTime === 0) {
227
+ // Convert to milliseconds before round off
228
+ const total = roundMilliSecs((loadEndTime - requestSentTime) * 1000);
229
+ networkEntry.timings.total = total;
230
+ return;
338
231
  }
339
- return result;
340
- }
341
- calculateDiffInMs(
342
- 'total',
343
- issueTime < startTime ? issueTime : startTime,
344
- endTime
345
- );
346
- if (issueTime < startTime) {
347
- calculateDiffInMs('queueing', issueTime, startTime);
348
- }
349
- const responseReceived = toMilliseconds(actResRcvdTime - startTime);
350
232
 
351
- if (response.fromServiceWorker) {
352
- addOffsetRange('blocked', 0, timing.workerStart);
353
- addOffsetRange('wait', timing.sendEnd, responseReceived);
354
- } else if (!timing.pushStart) {
355
- const blockingEnd =
356
- firstPositive([
357
- timing.dnsStart,
358
- timing.connectStart,
359
- timing.sendStart,
360
- responseReceived,
361
- ]) || 0;
362
- addOffsetRange('blocked', 0, blockingEnd);
363
- addOffsetRange('proxy', timing.proxyStart, timing.proxyEnd);
364
- addOffsetRange('dns', timing.dnsStart, timing.dnsEnd);
365
- addOffsetRange('connect', timing.connectStart, timing.connectEnd);
366
- addOffsetRange('ssl', timing.sslStart, timing.sslEnd);
367
- addOffsetRange('send', timing.sendStart, timing.sendEnd);
368
- addOffsetRange(
369
- 'wait',
370
- Math.max(
371
- timing.sendEnd,
372
- timing.connectEnd,
373
- timing.dnsEnd,
374
- timing.proxyEnd,
375
- blockingEnd
376
- ),
377
- responseReceived
233
+ const blocked =
234
+ roundMilliSecs(
235
+ firstPositive([
236
+ timing.domainLookupStart,
237
+ timing.connectStart,
238
+ timing.requestStart,
239
+ ])
240
+ ) || -1;
241
+ const dns =
242
+ timing.domainLookupEnd !== -1
243
+ ? roundMilliSecs(timing.domainLookupEnd - timing.domainLookupStart)
244
+ : -1;
245
+ const connect =
246
+ timing.connectEnd !== -1
247
+ ? roundMilliSecs(timing.connectEnd - timing.connectStart)
248
+ : -1;
249
+ const ssl =
250
+ timing.secureConnectionStart !== -1
251
+ ? roundMilliSecs(timing.connectEnd - timing.secureConnectionStart)
252
+ : -1;
253
+ const wait =
254
+ timing.responseStart !== -1
255
+ ? roundMilliSecs(timing.responseStart - timing.requestStart)
256
+ : -1;
257
+ const receive =
258
+ timing.responseEnd !== -1
259
+ ? roundMilliSecs(timing.responseEnd - timing.responseStart)
260
+ : -1;
261
+ const total = [blocked, dns, connect, wait, receive].reduce(
262
+ (pre, cur) => (cur > 0 ? cur + pre : pre),
263
+ 0
264
+ );
265
+ networkEntry.timings = {
266
+ blocked,
267
+ dns,
268
+ connect,
269
+ ssl,
270
+ wait,
271
+ send: 0, // not exposed via RT api
272
+ receive,
273
+ total,
274
+ };
275
+
276
+ const page = request.frame().page();
277
+ // For aborted/failed requests sizes will not be present
278
+ this._addBarrier(
279
+ page,
280
+ request.sizes().then(sizes => {
281
+ networkEntry.request.bytes =
282
+ sizes.requestHeadersSize + sizes.requestBodySize;
283
+ networkEntry.request.body = {
284
+ bytes: sizes.requestBodySize,
285
+ };
286
+ networkEntry.response.bytes =
287
+ sizes.responseHeadersSize + sizes.responseBodySize;
288
+ networkEntry.response.body = {
289
+ bytes: sizes.responseBodySize,
290
+ };
291
+ networkEntry.transferSize = sizes.responseBodySize;
292
+ })
378
293
  );
379
294
  }
380
- calculateDiffInMs('receive', actResRcvdTime, endTime);
381
- return result;
295
+
296
+ async stop() {
297
+ await Promise.all(this._barrierPromises);
298
+ const context = this.driver.context;
299
+ context.off('request', this._onRequest.bind(this));
300
+ context.off('response', this._onResponse.bind(this));
301
+ context.off('requestfinished', this._onRequestCompleted.bind(this));
302
+ context.off('requestfailed', this._onRequestCompleted.bind(this));
303
+ this._barrierPromises.clear();
304
+ return this.results;
305
+ }
382
306
  }
@@ -102,11 +102,11 @@ export class PluginManager {
102
102
  (this.get('network') as NetworkManager)._currentStep = step;
103
103
  }
104
104
 
105
- output() {
105
+ async output() {
106
106
  const data: PluginOutput = {};
107
107
  for (const [, plugin] of this.plugins) {
108
108
  if (plugin instanceof NetworkManager) {
109
- data.networkinfo = plugin.stop();
109
+ data.networkinfo = await plugin.stop();
110
110
  } else if (plugin instanceof BrowserConsole) {
111
111
  data.browserconsole = plugin.stop();
112
112
  }
@@ -128,23 +128,8 @@ function getMetadata() {
128
128
  };
129
129
  }
130
130
 
131
- function formatVersion(protocol: string | undefined) {
132
- if (!protocol) {
133
- return;
134
- }
135
- if (protocol === 'h2') {
136
- return 2;
137
- } else if (protocol === 'http/1.1') {
138
- return 1.1;
139
- } else if (protocol === 'http/1.0') {
140
- return 1.0;
141
- } else if (protocol.startsWith('h3')) {
142
- return 3;
143
- }
144
- }
145
-
146
131
  function formatTLS(tls: SecurityDetails) {
147
- if (!tls) {
132
+ if (!tls || !tls.protocol) {
148
133
  return;
149
134
  }
150
135
  const [name, version] = tls.protocol.toLowerCase().split(' ');
@@ -177,7 +162,6 @@ export function formatNetworkFields(network: NetworkInfo) {
177
162
  original: request.headers?.['User-Agent'],
178
163
  },
179
164
  http: {
180
- version: formatVersion(response?.protocol),
181
165
  request,
182
166
  response,
183
167
  },