@elastic/synthetics 1.17.2 → 1.18.1

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 (88) hide show
  1. package/dist/cli.js.map +1 -1
  2. package/dist/common_types.d.ts.map +1 -1
  3. package/dist/config.d.ts.map +1 -1
  4. package/dist/config.js +1 -1
  5. package/dist/config.js.map +1 -1
  6. package/dist/core/globals.d.ts +1 -0
  7. package/dist/core/globals.d.ts.map +1 -1
  8. package/dist/core/globals.js +12 -1
  9. package/dist/core/globals.js.map +1 -1
  10. package/dist/core/logger.d.ts.map +1 -1
  11. package/dist/core/logger.js +2 -7
  12. package/dist/core/logger.js.map +1 -1
  13. package/dist/core/mfa.d.ts.map +1 -1
  14. package/dist/core/mfa.js +1 -1
  15. package/dist/core/runner.d.ts.map +1 -1
  16. package/dist/core/runner.js +1 -0
  17. package/dist/core/runner.js.map +1 -1
  18. package/dist/dsl/monitor.d.ts +4 -0
  19. package/dist/dsl/monitor.d.ts.map +1 -1
  20. package/dist/dsl/monitor.js +6 -0
  21. package/dist/dsl/monitor.js.map +1 -1
  22. package/dist/formatter/javascript.d.ts.map +1 -1
  23. package/dist/formatter/javascript.js.map +1 -1
  24. package/dist/generator/utils.d.ts +1 -1
  25. package/dist/helpers.d.ts.map +1 -1
  26. package/dist/helpers.js.map +1 -1
  27. package/dist/plugins/tracing.js.map +1 -1
  28. package/dist/push/bundler.d.ts +2 -5
  29. package/dist/push/bundler.d.ts.map +1 -1
  30. package/dist/push/bundler.js +15 -30
  31. package/dist/push/bundler.js.map +1 -1
  32. package/dist/push/index.d.ts.map +1 -1
  33. package/dist/push/index.js +38 -15
  34. package/dist/push/index.js.map +1 -1
  35. package/dist/push/kibana_api.d.ts +2 -1
  36. package/dist/push/kibana_api.d.ts.map +1 -1
  37. package/dist/push/kibana_api.js +6 -4
  38. package/dist/push/kibana_api.js.map +1 -1
  39. package/dist/push/monitor.d.ts +5 -1
  40. package/dist/push/monitor.d.ts.map +1 -1
  41. package/dist/push/monitor.js +14 -2
  42. package/dist/push/monitor.js.map +1 -1
  43. package/dist/push/request.d.ts +2 -2
  44. package/dist/push/request.d.ts.map +1 -1
  45. package/dist/push/request.js +7 -6
  46. package/dist/push/request.js.map +1 -1
  47. package/dist/push/run-local.d.ts +27 -0
  48. package/dist/push/run-local.d.ts.map +1 -0
  49. package/dist/push/run-local.js +132 -0
  50. package/dist/push/run-local.js.map +1 -0
  51. package/dist/push/utils.d.ts +6 -1
  52. package/dist/push/utils.d.ts.map +1 -1
  53. package/dist/push/utils.js +61 -6
  54. package/dist/push/utils.js.map +1 -1
  55. package/dist/reporters/base.d.ts.map +1 -1
  56. package/dist/reporters/base.js.map +1 -1
  57. package/dist/reporters/build_kite_cli.d.ts.map +1 -1
  58. package/dist/reporters/build_kite_cli.js.map +1 -1
  59. package/dist/reporters/json.d.ts +1 -1
  60. package/dist/reporters/json.d.ts.map +1 -1
  61. package/dist/reporters/json.js +6 -2
  62. package/dist/reporters/json.js.map +1 -1
  63. package/dist/reporters/junit.d.ts.map +1 -1
  64. package/dist/reporters/junit.js.map +1 -1
  65. package/package.json +13 -10
  66. package/src/cli.ts +8 -3
  67. package/src/common_types.ts +1 -1
  68. package/src/config.ts +3 -1
  69. package/src/core/globals.ts +12 -0
  70. package/src/core/logger.ts +2 -8
  71. package/src/core/mfa.ts +11 -11
  72. package/src/core/runner.ts +1 -0
  73. package/src/dsl/monitor.ts +7 -0
  74. package/src/formatter/javascript.ts +3 -1
  75. package/src/helpers.ts +3 -3
  76. package/src/loader.ts +1 -1
  77. package/src/plugins/tracing.ts +1 -1
  78. package/src/push/bundler.ts +16 -36
  79. package/src/push/index.ts +59 -24
  80. package/src/push/kibana_api.ts +7 -3
  81. package/src/push/monitor.ts +18 -5
  82. package/src/push/request.ts +26 -7
  83. package/src/push/run-local.ts +155 -0
  84. package/src/push/utils.ts +87 -9
  85. package/src/reporters/base.ts +8 -4
  86. package/src/reporters/build_kite_cli.ts +7 -6
  87. package/src/reporters/json.ts +10 -14
  88. package/src/reporters/junit.ts +4 -10
@@ -24,25 +24,19 @@
24
24
  */
25
25
 
26
26
  import path from 'path';
27
- import { stat, unlink, readFile } from 'fs/promises';
27
+ import { unlink, readFile } from 'fs/promises';
28
28
  import { createWriteStream } from 'fs';
29
29
  import * as esbuild from 'esbuild';
30
30
  import archiver from 'archiver';
31
31
  import { commonOptions } from '../core/transform';
32
32
  import { SyntheticsBundlePlugin } from './plugin';
33
33
 
34
- // 1500KB Max Gzipped limit for bundled code to be pushed as Kibana project monitors.
35
- const SIZE_LIMIT_KB = 1500;
36
-
37
34
  function relativeToCwd(entry: string) {
38
35
  return path.relative(process.cwd(), entry);
39
36
  }
40
37
 
41
38
  export class Bundler {
42
- moduleMap = new Map<string, string>();
43
- constructor() {}
44
-
45
- async prepare(absPath: string) {
39
+ async bundle(absPath: string) {
46
40
  const options: esbuild.BuildOptions = {
47
41
  ...commonOptions(),
48
42
  ...{
@@ -59,56 +53,42 @@ export class Bundler {
59
53
  if (result.errors.length > 0) {
60
54
  throw result.errors;
61
55
  }
62
- this.moduleMap.set(absPath, result.outputFiles[0].text);
56
+ return result.outputFiles[0].text;
63
57
  }
64
58
 
65
- async zip(outputPath: string) {
59
+ async zip(source: string, code: string, dest: string) {
66
60
  return new Promise((fulfill, reject) => {
67
- const output = createWriteStream(outputPath);
61
+ const output = createWriteStream(dest);
68
62
  const archive = archiver('zip', {
69
63
  zlib: { level: 9 },
70
64
  });
71
65
  archive.on('error', reject);
72
66
  output.on('close', fulfill);
73
67
  archive.pipe(output);
74
- for (const [path, content] of this.moduleMap.entries()) {
75
- const relativePath = relativeToCwd(path);
76
- // Date is fixed to Unix epoch so the file metadata is
77
- // not modified everytime when files are bundled
78
- archive.append(content, {
79
- name: relativePath,
80
- date: new Date('1970-01-01'),
81
- });
82
- }
68
+ const relativePath = relativeToCwd(source);
69
+ // Date is fixed to Unix epoch so the file metadata is
70
+ // not modified everytime when files are bundled
71
+ archive.append(code, {
72
+ name: relativePath,
73
+ date: new Date('1970-01-01'),
74
+ });
83
75
  archive.finalize();
84
76
  });
85
77
  }
86
78
 
87
79
  async build(entry: string, output: string) {
88
- await this.prepare(entry);
89
- await this.zip(output);
90
- const data = await this.encode(output);
91
- await this.checkSize(output);
80
+ const code = await this.bundle(entry);
81
+ await this.zip(entry, code, output);
82
+ const content = await this.encode(output);
92
83
  await this.cleanup(output);
93
- return data;
84
+ return content;
94
85
  }
95
86
 
96
87
  async encode(outputPath: string) {
97
88
  return await readFile(outputPath, 'base64');
98
89
  }
99
90
 
100
- async checkSize(outputPath: string) {
101
- const { size } = await stat(outputPath);
102
- const sizeKb = size / 1024;
103
- if (sizeKb > SIZE_LIMIT_KB) {
104
- throw new Error(
105
- `Bundled monitor code exceeds the recommended ${SIZE_LIMIT_KB}KB limit. Please check your dependencies and try again.`
106
- );
107
- }
108
- }
109
-
110
91
  async cleanup(outputPath: string) {
111
- this.moduleMap = new Map<string, string>();
112
92
  await unlink(outputPath);
113
93
  }
114
94
  }
package/src/push/index.ts CHANGED
@@ -24,7 +24,7 @@
24
24
  */
25
25
  import { readFile, writeFile } from 'fs/promises';
26
26
  import { prompt } from 'enquirer';
27
- import { bold, grey } from 'kleur/colors';
27
+ import { bold, underline } from 'kleur/colors';
28
28
  import {
29
29
  getLocalMonitors,
30
30
  buildMonitorSchema,
@@ -50,15 +50,20 @@ import {
50
50
  bulkGetMonitors,
51
51
  bulkPutMonitors,
52
52
  createMonitorsLegacy,
53
- CHUNK_SIZE,
53
+ BATCH_SIZE,
54
+ MAX_PAYLOAD_SIZE_KB,
54
55
  } from './kibana_api';
55
56
  import {
56
- getChunks,
57
+ getBatches,
58
+ getSizedBatches,
57
59
  isBulkAPISupported,
58
60
  isLightweightMonitorSupported,
59
61
  logDiff,
62
+ logGroups,
63
+ printBytes,
60
64
  } from './utils';
61
- import { log } from '../core/logger';
65
+ import { runLocal } from './run-local';
66
+ import { inDebugMode } from '../core/globals';
62
67
 
63
68
  export async function push(monitors: Monitor[], options: PushOptions) {
64
69
  if (parseInt(process.env.CHUNK_SIZE) > 250) {
@@ -84,7 +89,7 @@ export async function push(monitors: Monitor[], options: PushOptions) {
84
89
  const { monitors: remote } = await bulkGetMonitors(options);
85
90
 
86
91
  progress(`preparing ${monitors.length} monitors`);
87
- const schemas = await buildMonitorSchema(monitors, true);
92
+ const { schemas, sizes } = await buildMonitorSchema(monitors, true);
88
93
  const local = getLocalMonitors(schemas);
89
94
 
90
95
  const { newIDs, changedIDs, removedIDs, unchangedIDs } = diffMonitorHashIDs(
@@ -92,21 +97,47 @@ export async function push(monitors: Monitor[], options: PushOptions) {
92
97
  remote
93
98
  );
94
99
  logDiff(newIDs, changedIDs, removedIDs, unchangedIDs);
100
+ if (inDebugMode()) {
101
+ logGroups(sizes, newIDs, changedIDs, removedIDs, unchangedIDs);
102
+ // show bundle size for the whole project
103
+ let totalSize = 0;
104
+ for (const value of sizes.values()) {
105
+ totalSize += value;
106
+ }
107
+ progress(
108
+ `total size of the ${sizes.size} monitors payload is ` +
109
+ printBytes(totalSize)
110
+ );
111
+ }
112
+
113
+ if (options.dryRun) {
114
+ progress('Running browser monitors in dry run mode');
115
+ await runLocal(schemas);
116
+ progress('Dry run completed');
117
+ return;
118
+ }
95
119
 
96
120
  const updatedMonitors = new Set<string>([...changedIDs, ...newIDs]);
97
121
  if (updatedMonitors.size > 0) {
98
122
  const updatedMonSchemas = schemas.filter(s => updatedMonitors.has(s.id));
99
- const chunks = getChunks(updatedMonSchemas, CHUNK_SIZE);
100
- for (const chunk of chunks) {
123
+ const batches = getSizedBatches(
124
+ updatedMonSchemas,
125
+ sizes,
126
+ MAX_PAYLOAD_SIZE_KB,
127
+ BATCH_SIZE
128
+ );
129
+ if (batches.length > 1) {
130
+ progress(`Monitors will be pushed as ${batches.length} batches.`);
131
+ }
132
+ for (const batch of batches) {
101
133
  await liveProgress(
102
- bulkPutMonitors(options, chunk),
103
- `creating or updating ${chunk.length} monitors`
134
+ bulkPutMonitors(options, batch),
135
+ `creating or updating ${batch.length} monitors`
104
136
  );
105
137
  }
106
138
  }
107
139
 
108
140
  if (removedIDs.size > 0) {
109
- log(`deleting monitor ids: ${Array.from(removedIDs.keys()).join(', ')}`);
110
141
  if (updatedMonitors.size === 0 && unchangedIDs.size === 0) {
111
142
  await confirmDelete(
112
143
  `Pushing without any monitors will delete all monitors associated with the project.\n Do you want to continue?`,
@@ -118,16 +149,15 @@ export async function push(monitors: Monitor[], options: PushOptions) {
118
149
  options.yes
119
150
  );
120
151
  }
121
- const chunks = getChunks(Array.from(removedIDs), CHUNK_SIZE);
122
- for (const chunk of chunks) {
152
+ const batches = getBatches(Array.from(removedIDs), BATCH_SIZE);
153
+ for (const batch of batches) {
123
154
  await liveProgress(
124
- bulkDeleteMonitors(options, chunk),
125
- `deleting ${chunk.length} monitors`
155
+ bulkDeleteMonitors(options, batch),
156
+ `deleting ${batch.length} monitors`
126
157
  );
127
158
  }
128
159
  }
129
-
130
- done(`Pushed: ${grey(getMonitorManagementURL(options.url))}`);
160
+ done(`Pushed: ${underline(getMonitorManagementURL(options.url))} `);
131
161
  }
132
162
 
133
163
  async function confirmDelete(message: string, skip: boolean) {
@@ -223,8 +253,9 @@ export function validateSettings(opts: PushOptions) {
223
253
  - CLI '--schedule <mins>'
224
254
  - Config file 'monitors.schedule' field`;
225
255
  } else if (opts.schedule && !ALLOWED_SCHEDULES.includes(opts.schedule)) {
226
- reason = `Set default schedule(${opts.schedule
227
- }) to one of the allowed values - ${ALLOWED_SCHEDULES.join(',')}`;
256
+ reason = `Set default schedule(${
257
+ opts.schedule
258
+ }) to one of the allowed values - ${ALLOWED_SCHEDULES.join(',')}`;
228
259
  } else if (
229
260
  (opts.locations ?? []).length > 0 &&
230
261
  (opts?.playwrightOptions?.clientCertificates ?? []).filter(cert => {
@@ -299,14 +330,18 @@ export async function pushLegacy(monitors: Monitor[], options: PushOptions) {
299
330
  }
300
331
 
301
332
  let schemas: MonitorSchema[] = [];
333
+ let sizes: Map<string, number> = new Map();
302
334
  if (monitors.length > 0) {
303
335
  progress(`preparing ${monitors.length} monitors`);
304
- schemas = await buildMonitorSchema(monitors, false);
305
- const chunks = getChunks(schemas, 10);
306
- for (const chunk of chunks) {
336
+ ({ schemas, sizes } = await buildMonitorSchema(monitors, false));
337
+ const batches = getSizedBatches(schemas, sizes, MAX_PAYLOAD_SIZE_KB, 10);
338
+ if (batches.length > 1) {
339
+ progress(`Monitors will be pushed as ${batches.length} batches.`);
340
+ }
341
+ for (const batch of batches) {
307
342
  await liveProgress(
308
- createMonitorsLegacy({ schemas: chunk, keepStale: true, options }),
309
- `creating or updating ${chunk.length} monitors`
343
+ createMonitorsLegacy({ schemas: batch, keepStale: true, options }),
344
+ `creating or updating ${batch.length} monitors`
310
345
  );
311
346
  }
312
347
  } else {
@@ -320,7 +355,7 @@ export async function pushLegacy(monitors: Monitor[], options: PushOptions) {
320
355
  `deleting all stale monitors`
321
356
  );
322
357
 
323
- done(`Pushed: ${grey(getMonitorManagementURL(options.url))}`);
358
+ done(`Pushed: ${underline(getMonitorManagementURL(options.url))}`);
324
359
  }
325
360
 
326
361
  // prints warning if any of the monitors has throttling settings enabled during push
@@ -36,8 +36,9 @@ import {
36
36
  } from './request';
37
37
  import { generateURL } from './utils';
38
38
 
39
- // Default chunk size for bulk put / delete
40
- export const CHUNK_SIZE = parseInt(process.env.CHUNK_SIZE) || 100;
39
+ // Default batch size for bulk put / delete
40
+ export const BATCH_SIZE = parseInt(process.env.CHUNK_SIZE) || 250;
41
+ export const MAX_PAYLOAD_SIZE_KB = 50 * 1000; // 50 MB in KB
41
42
 
42
43
  export type PutResponse = {
43
44
  createdMonitors: string[];
@@ -49,12 +50,15 @@ export async function bulkPutMonitors(
49
50
  options: PushOptions,
50
51
  schemas: MonitorSchema[]
51
52
  ) {
53
+ const url = generateURL(options, 'bulk_update') + '/_bulk_update';
54
+
52
55
  const resp = await sendReqAndHandleError<PutResponse>({
53
- url: generateURL(options, 'bulk_update') + '/_bulk_update',
56
+ url,
54
57
  method: 'PUT',
55
58
  auth: options.auth,
56
59
  body: JSON.stringify({ monitors: schemas }),
57
60
  });
61
+
58
62
  const { failedMonitors } = resp;
59
63
  if (failedMonitors && failedMonitors.length > 0) {
60
64
  throw formatFailedMonitors(failedMonitors);
@@ -42,12 +42,15 @@ import { isParamOptionSupported, normalizeMonitorName } from './utils';
42
42
 
43
43
  // Allowed extensions for lightweight monitor files
44
44
  const ALLOWED_LW_EXTENSIONS = ['.yml', '.yaml'];
45
+ // 1500kB Max Gzipped limit for bundled monitor code to be pushed as Kibana project monitors.
46
+ const SIZE_LIMIT_KB = 1500;
45
47
 
46
48
  export type MonitorSchema = Omit<MonitorConfig, 'locations'> & {
47
49
  locations: string[];
48
50
  content?: string;
49
51
  filter?: Monitor['filter'];
50
52
  hash?: string;
53
+ size?: number;
51
54
  };
52
55
 
53
56
  // Abbreviated monitor info, as often returned by the API,
@@ -131,6 +134,7 @@ export async function buildMonitorSchema(monitors: Monitor[], isV2: boolean) {
131
134
  await mkdir(bundlePath, { recursive: true });
132
135
  const bundler = new Bundler();
133
136
  const schemas: MonitorSchema[] = [];
137
+ const sizes: Map<string, number> = new Map();
134
138
 
135
139
  for (const monitor of monitors) {
136
140
  const { source, config, filter, type } = monitor;
@@ -148,6 +152,17 @@ export async function buildMonitorSchema(monitors: Monitor[], isV2: boolean) {
148
152
  monitor.setContent(content);
149
153
  Object.assign(schema, { content, filter });
150
154
  }
155
+ const size = monitor.size();
156
+ const sizeKB = Math.round(size / 1000);
157
+ if (sizeKB > SIZE_LIMIT_KB) {
158
+ let outer = bold(
159
+ `Aborted: Bundled code ${sizeKB}kB exceeds the recommended ${SIZE_LIMIT_KB}kB limit. Please check the dependencies imported.\n`
160
+ );
161
+ const inner = `* ${config.id} - ${source.file}:${source.line}:${source.column}\n`;
162
+ outer += indent(inner);
163
+ throw red(outer);
164
+ }
165
+ sizes.set(config.id, size);
151
166
  /**
152
167
  * Generate hash only after the bundled content is created
153
168
  * to capture code changes in imported files
@@ -159,7 +174,7 @@ export async function buildMonitorSchema(monitors: Monitor[], isV2: boolean) {
159
174
  }
160
175
 
161
176
  await rm(bundlePath, { recursive: true });
162
- return schemas;
177
+ return { schemas, sizes };
163
178
  }
164
179
 
165
180
  export async function createLightweightMonitors(
@@ -225,9 +240,7 @@ export async function createLightweightMonitors(
225
240
  const monitor = mergedConfig[i];
226
241
  // Skip browser monitors from the YML files
227
242
  if (monitor['type'] === 'browser') {
228
- warn(
229
- `Browser monitors from ${file} are skipped.`
230
- );
243
+ warn(`Browser monitors from ${file} are skipped.`);
231
244
  continue;
232
245
  }
233
246
  const { line, col } = lineCounter.linePos(offsets[i]);
@@ -288,7 +301,7 @@ export function buildMonitorFromYaml(
288
301
  });
289
302
 
290
303
  /**
291
- * Params support is only available for lighweight monitors
304
+ * Params support is only available for lightweight monitors
292
305
  * post 8.7.2 stack
293
306
  */
294
307
  if (isParamOptionSupported(options.kibanaVersion)) {
@@ -58,8 +58,14 @@ export async function sendReqAndHandleError<T>(
58
58
  options: APIRequestOptions
59
59
  ): Promise<T> {
60
60
  const { statusCode, body } = await sendRequest(options);
61
+
61
62
  return (
62
- await handleError(statusCode, options.url, body)
63
+ await handleError(
64
+ statusCode,
65
+ options.url,
66
+ body,
67
+ statusCode === 413 ? `${options.body?.length} bytes sent` : ''
68
+ )
63
69
  ).json() as Promise<T>;
64
70
  }
65
71
 
@@ -74,7 +80,8 @@ type APIError = {
74
80
  export async function handleError(
75
81
  statusCode: number,
76
82
  url: string,
77
- body: Dispatcher.ResponseData['body']
83
+ body: Dispatcher.ResponseData['body'],
84
+ extraMessage?: string
78
85
  ): Promise<Dispatcher.ResponseData['body']> {
79
86
  if (statusCode === 404) {
80
87
  throw formatNotFoundError(url, await body.text());
@@ -84,9 +91,19 @@ export async function handleError(
84
91
  const resp = await body.text();
85
92
  parsed = JSON.parse(resp) as APIError;
86
93
  } catch (e) {
87
- throw formatAPIError(statusCode, 'unexpected error', e.message);
94
+ throw formatAPIError(
95
+ statusCode,
96
+ 'unexpected error',
97
+ e.message,
98
+ extraMessage
99
+ );
88
100
  }
89
- throw formatAPIError(statusCode, parsed.error, parsed.message);
101
+ throw formatAPIError(
102
+ statusCode,
103
+ parsed.error,
104
+ parsed.message,
105
+ extraMessage
106
+ );
90
107
  }
91
108
 
92
109
  return body;
@@ -111,16 +128,18 @@ export function formatNotFoundError(url: string, message: string) {
111
128
  }
112
129
 
113
130
  export function formatAPIError(
114
- statuCode: number,
131
+ statusCode: number,
115
132
  error: string,
116
- message: string
133
+ message: string,
134
+ extraMessage = ''
117
135
  ) {
118
136
  let outer = bold(`${symbols['failed']} Error\n`);
119
137
  let inner = bold(
120
- `${symbols['failed']} monitor creation failed - ${statuCode}:${error}\n`
138
+ `${symbols['failed']} monitor creation failed - ${statusCode}:${error}\n`
121
139
  );
122
140
  inner += indent(message, ' ');
123
141
  outer += indent(inner);
142
+ outer += extraMessage ? indent(extraMessage, ' ') : '';
124
143
  return red(outer);
125
144
  }
126
145
 
@@ -0,0 +1,155 @@
1
+ /**
2
+ * MIT License
3
+ *
4
+ * Copyright (c) 2020-present, Elastic NV
5
+ *
6
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ * of this software and associated documentation files (the "Software"), to deal
8
+ * in the Software without restriction, including without limitation the rights
9
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ * copies of the Software, and to permit persons to whom the Software is
11
+ * furnished to do so, subject to the following conditions:
12
+ *
13
+ * The above copyright notice and this permission notice shall be included in
14
+ * all copies or substantial portions of the Software.
15
+ *
16
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22
+ * THE SOFTWARE.
23
+ *
24
+ */
25
+
26
+ import { execFileSync, spawn } from 'child_process';
27
+ import { rm, writeFile } from 'fs/promises';
28
+ import { createReadStream } from 'fs';
29
+ import { tmpdir } from 'os';
30
+ import { Extract } from 'unzip-stream';
31
+ import { red } from 'kleur/colors';
32
+ import { join } from 'path';
33
+ import { pathToFileURL } from 'url';
34
+ import { MonitorSchema } from './monitor';
35
+
36
+ async function unzipFile(zipPath, destination) {
37
+ return new Promise<void>((resolve, reject) => {
38
+ createReadStream(zipPath)
39
+ .pipe(Extract({ path: destination }))
40
+ .on('close', resolve)
41
+ .on('error', err =>
42
+ reject(new Error(`failed to extract zip ${zipPath} : ${err.message}`))
43
+ );
44
+ });
45
+ }
46
+
47
+ async function runNpmInstall(directory) {
48
+ return new Promise<void>((resolve, reject) => {
49
+ const flags = [
50
+ '--no-audit', // Prevent audit checks
51
+ '--no-update-notifier', // Prevent update checks
52
+ '--no-fund', // No need for package funding messages here
53
+ '--package-lock=false', // no need to write package lock here
54
+ '--progress=false', // no need to display progress
55
+ ];
56
+
57
+ const npmInstall = spawn('npm', ['install', ...flags], {
58
+ cwd: directory,
59
+ stdio: 'ignore',
60
+ });
61
+ npmInstall.on('close', code => {
62
+ if (code === 0) {
63
+ resolve();
64
+ } else {
65
+ reject(new Error(`npm install failed with exit code ${code}`));
66
+ }
67
+ });
68
+ npmInstall.on('error', err =>
69
+ reject(new Error(`failed to setup: ${err.message}`))
70
+ );
71
+ });
72
+ }
73
+
74
+ async function runTest(directory, schema: MonitorSchema) {
75
+ return new Promise<void>((resolve, reject) => {
76
+ const runTest = spawn(
77
+ 'npx',
78
+ [
79
+ '@elastic/synthetics',
80
+ '.',
81
+ '--playwright-options',
82
+ JSON.stringify(schema.playwrightOptions),
83
+ '--params',
84
+ JSON.stringify(schema.params),
85
+ ],
86
+ {
87
+ cwd: directory,
88
+ stdio: 'inherit',
89
+ }
90
+ );
91
+
92
+ runTest.on('close', resolve);
93
+ runTest.on('error', err => {
94
+ reject(
95
+ new Error(`Failed to execute @elastic/synthetics : ${err.message}`)
96
+ );
97
+ });
98
+ });
99
+ }
100
+
101
+ async function writePkgJSON(dir: string, synthPath: string) {
102
+ const packageJsonContent = {
103
+ name: 'project-journey',
104
+ private: 'true',
105
+ dependencies: {
106
+ '@elastic/synthetics': pathToFileURL(synthPath),
107
+ },
108
+ };
109
+ await writeFile(
110
+ join(dir, 'package.json'),
111
+ JSON.stringify(packageJsonContent, null, 2),
112
+ 'utf-8'
113
+ );
114
+ }
115
+
116
+ async function extract(
117
+ schema: MonitorSchema,
118
+ zipPath: string,
119
+ unzipPath: string
120
+ ) {
121
+ if (schema.type !== 'browser') {
122
+ return;
123
+ }
124
+ const content = schema.content;
125
+ await writeFile(zipPath, content, 'base64');
126
+ await unzipFile(zipPath, unzipPath);
127
+ }
128
+
129
+ export async function runLocal(schemas: MonitorSchema[]) {
130
+ // lookup installed bin path of a node module
131
+ const resolvedPath = execFileSync('which', ['elastic-synthetics'], {
132
+ encoding: 'utf8',
133
+ }).trim();
134
+ const synthPath = resolvedPath.replace(
135
+ join('bin', 'elastic-synthetics'),
136
+ join('lib', 'node_modules', '@elastic/synthetics')
137
+ );
138
+ const rand = Date.now();
139
+ const zipPath = join(tmpdir(), `synthetics-zip-${rand}.zip`);
140
+ const unzipPath = join(tmpdir(), `synthetics-unzip-${rand}`);
141
+ try {
142
+ for (const schema of schemas) {
143
+ await extract(schema, zipPath, unzipPath);
144
+ }
145
+ await writePkgJSON(unzipPath, synthPath);
146
+ await runNpmInstall(unzipPath);
147
+ // TODO: figure out a way to collect all params and Playwright options
148
+ await runTest(unzipPath, schemas[0]);
149
+ } catch (e) {
150
+ throw red(`Aborted: ${e.message}`);
151
+ } finally {
152
+ await rm(zipPath, { recursive: true, force: true });
153
+ await rm(unzipPath, { recursive: true, force: true });
154
+ }
155
+ }