@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
@@ -23,13 +23,32 @@
23
23
  *
24
24
  */
25
25
 
26
+ import { readFileSync } from 'fs';
27
+ import { join } from 'path';
28
+ import sharp from 'sharp';
29
+ import { createHash } from 'crypto';
26
30
  import BaseReporter from './base';
27
- import { formatError, getTimestamp } from '../helpers';
31
+ import {
32
+ formatError,
33
+ getTimestamp,
34
+ CACHE_PATH,
35
+ totalist,
36
+ isDirectory,
37
+ getDurationInUs,
38
+ } from '../helpers';
28
39
  import { Journey, Step } from '../dsl';
29
40
  import snakeCaseKeys from 'snakecase-keys';
30
- import { NetworkInfo, StatusValue } from '../common_types';
31
- import { Protocol } from 'playwright-chromium/types/protocol';
32
- import { Metrics } from '../plugins';
41
+ import {
42
+ NetworkInfo,
43
+ SecurityDetails,
44
+ NetworkConditions,
45
+ TraceOutput,
46
+ StatusValue,
47
+ PerfMetrics,
48
+ Params,
49
+ Screenshot,
50
+ } from '../common_types';
51
+ import { PageMetrics } from '../plugins';
33
52
 
34
53
  /* eslint-disable @typescript-eslint/no-var-requires */
35
54
  const { version, name } = require('../../package.json');
@@ -38,10 +57,13 @@ type OutputType =
38
57
  | 'synthetics/metadata'
39
58
  | 'journey/register'
40
59
  | 'journey/start'
60
+ | 'screenshot/block'
61
+ | 'step/screenshot_ref'
41
62
  | 'step/screenshot'
63
+ | 'step/metrics'
64
+ | 'step/filmstrips'
42
65
  | 'step/end'
43
66
  | 'journey/network_info'
44
- | 'journey/filmstrips'
45
67
  | 'journey/browserconsole'
46
68
  | 'journey/end';
47
69
 
@@ -50,27 +72,50 @@ type Payload = {
50
72
  start?: number;
51
73
  end?: number;
52
74
  url?: string;
53
- status?: StatusValue | number;
54
- metrics?: Metrics;
55
- params?: Record<string, unknown>;
75
+ status?: StatusValue;
76
+ pagemetrics?: PageMetrics;
77
+ params?: Params;
56
78
  type?: OutputType;
57
79
  text?: string;
58
80
  index?: number;
81
+ network_conditions?: NetworkConditions;
59
82
  };
60
83
 
61
84
  type OutputFields = {
62
85
  type: OutputType;
86
+ _id?: string;
63
87
  journey?: Journey;
64
88
  timestamp?: number;
65
89
  url?: string;
66
- step?: Partial<Step>;
90
+ step?: Partial<Step> & {
91
+ duration?: {
92
+ us: number;
93
+ };
94
+ };
67
95
  error?: Error;
68
96
  root_fields?: Record<string, unknown>;
69
- payload?: Payload | NetworkInfo;
97
+ payload?: Payload;
70
98
  blob?: string;
71
99
  blob_mime?: string;
72
100
  };
73
101
 
102
+ type ScreenshotBlob = {
103
+ blob: string;
104
+ id: string;
105
+ };
106
+
107
+ type ScreenshotReference = {
108
+ width: number;
109
+ height: number;
110
+ blocks: Array<{
111
+ hash: string;
112
+ top: number;
113
+ left: number;
114
+ width: number;
115
+ height: number;
116
+ }>;
117
+ };
118
+
74
119
  function getMetadata() {
75
120
  return {
76
121
  os: {
@@ -98,41 +143,12 @@ function formatVersion(protocol: string | undefined) {
98
143
  }
99
144
  }
100
145
 
101
- function formatRequest(request: Protocol.Network.Request) {
102
- const postData = request.postData ? request.postData : '';
103
- return {
104
- ...request,
105
- body: {
106
- bytes: postData.length,
107
- content: postData,
108
- },
109
- referrer: request.headers?.Referer,
110
- };
111
- }
112
-
113
- function formatResponse(response: Protocol.Network.Response) {
114
- if (!response) {
115
- return;
116
- }
117
- return {
118
- ...response,
119
- body: {
120
- bytes: response.encodedDataLength,
121
- },
122
- status_code: response.status,
123
- };
124
- }
125
-
126
- function formatTLS(tls: Protocol.Network.SecurityDetails) {
146
+ function formatTLS(tls: SecurityDetails) {
127
147
  if (!tls) {
128
148
  return;
129
149
  }
130
- const cipher = `${tls.keyExchange ? tls.keyExchange + '_' : ''}${
131
- tls.cipher
132
- }_${tls.keyExchangeGroup}`;
133
150
  const [name, version] = tls.protocol.toLowerCase().split(' ');
134
151
  return {
135
- cipher,
136
152
  server: {
137
153
  x509: {
138
154
  issuer: {
@@ -146,7 +162,7 @@ function formatTLS(tls: Protocol.Network.SecurityDetails) {
146
162
  },
147
163
  },
148
164
  version_protocol: name,
149
- version: version,
165
+ version,
150
166
  };
151
167
  }
152
168
 
@@ -162,16 +178,14 @@ export function formatNetworkFields(network: NetworkInfo) {
162
178
  },
163
179
  http: {
164
180
  version: formatVersion(response?.protocol),
165
- request: formatRequest(request),
166
- response: formatResponse(response),
181
+ request,
182
+ response,
167
183
  },
168
184
  tls: formatTLS(response?.securityDetails),
169
185
  };
170
186
 
171
187
  const pickItems: Array<keyof NetworkInfo> = [
172
188
  'browser',
173
- 'status',
174
- 'method',
175
189
  'type',
176
190
  'isNavigationRequest',
177
191
  'requestSentTime',
@@ -200,6 +214,7 @@ function journeyInfo(
200
214
  return {
201
215
  name: journey.name,
202
216
  id: journey.id,
217
+ tags: journey.tags,
203
218
  status: type === 'journey/end' ? status : undefined,
204
219
  };
205
220
  }
@@ -216,21 +231,103 @@ function stepInfo(
216
231
  name: step.name,
217
232
  index: step.index,
218
233
  status: type === 'step/end' ? status : undefined,
234
+ duration: step.duration,
235
+ };
236
+ }
237
+
238
+ export async function getScreenshotBlocks(screenshot: Buffer) {
239
+ const img = sharp(screenshot, { sequentialRead: true });
240
+ const { width, height } = await img.metadata();
241
+ /**
242
+ * Chop the screenshot image (1280*720) which is the default
243
+ * viewport size in to 64 equal blocks for a given image
244
+ * which can be acheived by keeping the division to 8
245
+ *
246
+ * Changing division to 16 would yield us 256 smaller blocks, but it is not
247
+ * optimal for caching in the ES and proved out to be bad, so we stick to 8
248
+ */
249
+ const divisions = 8;
250
+ const blockWidth = Math.floor(width / divisions);
251
+ const blockHeight = Math.floor(height / divisions);
252
+ const reference: ScreenshotReference = {
253
+ width,
254
+ height,
255
+ blocks: [],
219
256
  };
257
+ const blocks: Array<ScreenshotBlob> = [];
258
+
259
+ for (let row = 0; row < divisions; row++) {
260
+ const top = row * blockHeight;
261
+ for (let col = 0; col < divisions; col++) {
262
+ const left = col * blockWidth;
263
+ const buf = await img
264
+ .extract({ top, left, width: blockWidth, height: blockHeight })
265
+ .jpeg()
266
+ .toBuffer();
267
+
268
+ const hash = createHash('sha1').update(buf).digest('hex');
269
+ blocks.push({
270
+ blob: buf.toString('base64'),
271
+ id: hash,
272
+ });
273
+ /**
274
+ * We dont write the width, height of individual blocks on the
275
+ * reference as we use similar sized blocks for each extraction,
276
+ * we would need to send the width and height here if we decide to
277
+ * go with dynamic block extraction.
278
+ */
279
+ reference.blocks.push({
280
+ hash,
281
+ top,
282
+ left,
283
+ width: blockWidth,
284
+ height: blockHeight,
285
+ });
286
+ }
287
+ }
288
+
289
+ return { blocks, reference, blob_mime: 'image/jpeg' };
290
+ }
291
+
292
+ /**
293
+ * Get all the screenshots from the cached screenshot location
294
+ * at the end of each journey and construct equally sized blocks out
295
+ * of the individual screenshot image.
296
+ */
297
+ export async function gatherScreenshots(
298
+ screenshotsPath: string,
299
+ callback: (data: Screenshot) => Promise<void>
300
+ ) {
301
+ if (isDirectory(screenshotsPath)) {
302
+ await totalist(screenshotsPath, async (_, absPath) => {
303
+ try {
304
+ const content = readFileSync(absPath, 'utf8');
305
+ const screenshot: Screenshot = JSON.parse(content);
306
+ await callback(screenshot);
307
+ } catch (_) {
308
+ // TODO: capture progarammatic synthetic errors under different type
309
+ }
310
+ });
311
+ }
220
312
  }
221
313
 
222
314
  export default class JSONReporter extends BaseReporter {
223
- _registerListeners() {
315
+ override _registerListeners() {
224
316
  /**
225
317
  * report the number of journeys that exists on a suite which
226
318
  * could be used for better sharding
227
319
  */
228
- this.runner.on('start', ({ numJourneys }) => {
320
+ this.runner.on('start', ({ numJourneys, networkConditions }) => {
229
321
  this.writeJSON({
230
322
  type: 'synthetics/metadata',
231
323
  root_fields: {
232
324
  num_journeys: numJourneys,
233
325
  },
326
+ payload: networkConditions
327
+ ? {
328
+ network_conditions: networkConditions,
329
+ }
330
+ : undefined,
234
331
  });
235
332
  });
236
333
 
@@ -258,33 +355,47 @@ export default class JSONReporter extends BaseReporter {
258
355
  start,
259
356
  end,
260
357
  error,
261
- screenshot,
262
358
  url,
263
359
  status,
360
+ pagemetrics,
361
+ traces,
264
362
  metrics,
363
+ filmstrips,
265
364
  }) => {
266
- if (screenshot) {
267
- this.writeJSON({
268
- type: 'step/screenshot',
269
- journey,
270
- step,
271
- blob: screenshot,
272
- blob_mime: 'image/jpeg',
365
+ this.writeMetrics(journey, step, 'relative_trace', traces);
366
+ this.writeMetrics(journey, step, 'experience', metrics);
367
+ if (filmstrips) {
368
+ // Write each filmstrip separately so that we don't get documents that are too large
369
+ filmstrips.forEach((strip, index) => {
370
+ this.writeJSON({
371
+ type: 'step/filmstrips',
372
+ journey,
373
+ step,
374
+ payload: { index },
375
+ root_fields: {
376
+ browser: { relative_trace: { start: strip.start } },
377
+ },
378
+ blob: strip.blob,
379
+ blob_mime: strip.mime,
380
+ });
273
381
  });
274
382
  }
275
383
  this.writeJSON({
276
384
  type: 'step/end',
277
385
  journey,
278
- step,
386
+ step: {
387
+ ...step,
388
+ duration: {
389
+ us: getDurationInUs(end - start),
390
+ },
391
+ },
279
392
  url,
280
393
  error,
281
394
  payload: {
282
395
  source: step.callback.toString(),
283
- start,
284
- end,
285
396
  url,
286
397
  status,
287
- metrics,
398
+ pagemetrics,
288
399
  },
289
400
  });
290
401
  }
@@ -292,16 +403,45 @@ export default class JSONReporter extends BaseReporter {
292
403
 
293
404
  this.runner.on(
294
405
  'journey:end',
295
- ({
406
+ async ({
296
407
  journey,
408
+ timestamp,
297
409
  start,
298
410
  end,
299
- filmstrips,
300
411
  networkinfo,
301
412
  browserconsole,
302
413
  status,
303
414
  error,
415
+ options,
304
416
  }) => {
417
+ const { ssblocks, screenshots } = options;
418
+ const writeScreenshots =
419
+ screenshots === 'on' ||
420
+ (screenshots === 'only-on-failure' && status === 'failed');
421
+ if (writeScreenshots) {
422
+ await gatherScreenshots(
423
+ join(CACHE_PATH, 'screenshots'),
424
+ async screenshot => {
425
+ const { data, timestamp, step } = screenshot;
426
+ if (!data) {
427
+ return;
428
+ }
429
+ if (ssblocks) {
430
+ await this.writeScreenshotBlocks(journey, screenshot);
431
+ } else {
432
+ this.writeJSON({
433
+ type: 'step/screenshot',
434
+ timestamp,
435
+ journey,
436
+ step,
437
+ blob: data,
438
+ blob_mime: 'image/jpeg',
439
+ });
440
+ }
441
+ }
442
+ );
443
+ }
444
+
305
445
  if (networkinfo) {
306
446
  networkinfo.forEach(ni => {
307
447
  const { ecs, payload } = formatNetworkFields(ni);
@@ -315,37 +455,26 @@ export default class JSONReporter extends BaseReporter {
315
455
  });
316
456
  });
317
457
  }
318
- if (filmstrips) {
319
- // Write each filmstrip separately so that we don't get documents that are too large
320
- filmstrips.forEach((strip, index) => {
321
- this.writeJSON({
322
- type: 'journey/filmstrips',
323
- journey,
324
- payload: {
325
- index,
326
- ...{
327
- startTime: strip.startTime,
328
- ts: strip.ts,
329
- },
330
- },
331
- blob: strip.snapshot,
332
- });
333
- });
334
- }
335
458
  if (browserconsole) {
336
- browserconsole.forEach(({ timestamp, text, type, step }) => {
459
+ browserconsole.forEach(({ timestamp, text, type, step, error }) => {
337
460
  this.writeJSON({
338
461
  type: 'journey/browserconsole',
339
462
  journey,
340
463
  timestamp,
341
464
  step,
342
- payload: { text, type } as Payload,
465
+ error,
466
+ payload: {
467
+ text,
468
+ type,
469
+ } as Payload,
343
470
  });
344
471
  });
345
472
  }
473
+
346
474
  this.writeJSON({
347
475
  type: 'journey/end',
348
476
  journey,
477
+ timestamp,
349
478
  error,
350
479
  payload: {
351
480
  start,
@@ -353,16 +482,65 @@ export default class JSONReporter extends BaseReporter {
353
482
  status,
354
483
  },
355
484
  });
485
+ process.nextTick(() => this.runner.emit('journey:end:reported', {}));
356
486
  }
357
487
  );
358
488
  }
359
489
 
490
+ async writeScreenshotBlocks(journey: Journey, screenshot: Screenshot) {
491
+ const { blob_mime, blocks, reference } = await getScreenshotBlocks(
492
+ Buffer.from(screenshot.data, 'base64')
493
+ );
494
+ for (let i = 0; i < blocks.length; i++) {
495
+ const block = blocks[i];
496
+ this.writeJSON({
497
+ type: 'screenshot/block',
498
+ timestamp: screenshot.timestamp,
499
+ _id: block.id,
500
+ blob: block.blob,
501
+ blob_mime,
502
+ });
503
+ }
504
+ this.writeJSON({
505
+ type: 'step/screenshot_ref',
506
+ timestamp: screenshot.timestamp,
507
+ journey,
508
+ step: screenshot.step,
509
+ root_fields: {
510
+ screenshot_ref: reference,
511
+ },
512
+ });
513
+ }
514
+
515
+ writeMetrics(
516
+ journey: Journey,
517
+ step: Step,
518
+ type: string,
519
+ events: Array<TraceOutput> | PerfMetrics
520
+ ) {
521
+ const metrics = Array.isArray(events) ? events : [events];
522
+ metrics.forEach(event => {
523
+ event &&
524
+ this.writeJSON({
525
+ type: 'step/metrics',
526
+ journey,
527
+ step,
528
+ root_fields: {
529
+ browser: {
530
+ [type]: event,
531
+ },
532
+ },
533
+ });
534
+ });
535
+ }
536
+
360
537
  // Writes a structured synthetics event
361
538
  // Note that blob is ultimately stored in ES as a base64 encoded string. You must base 64 encode
362
539
  // it before passing it into this function!
363
540
  // The payload field is an un-indexed field with no ES mapping, so users can put arbitary structured
364
541
  // stuff in there
365
542
  writeJSON({
543
+ _id,
366
544
  journey,
367
545
  type,
368
546
  timestamp,
@@ -376,6 +554,7 @@ export default class JSONReporter extends BaseReporter {
376
554
  }: OutputFields) {
377
555
  this.write({
378
556
  type,
557
+ _id,
379
558
  '@timestamp': timestamp || getTimestamp(),
380
559
  journey: journeyInfo(journey, type, payload?.status),
381
560
  step: stepInfo(step, type, payload?.status),
@@ -52,7 +52,7 @@ export default class JUnitReporter extends BaseReporter {
52
52
  private totalFailures = 0;
53
53
  private totalSkipped = 0;
54
54
 
55
- _registerListeners() {
55
+ override _registerListeners() {
56
56
  const journeyMap = new Map<string, XMLEntry>();
57
57
 
58
58
  this.runner.on('journey:start', ({ journey }) => {