@elastic/synthetics 1.17.2 → 1.18.0

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 (70) hide show
  1. package/dist/cli.js.map +1 -1
  2. package/dist/common_types.d.ts.map +1 -1
  3. package/dist/core/globals.d.ts +1 -0
  4. package/dist/core/globals.d.ts.map +1 -1
  5. package/dist/core/globals.js +12 -1
  6. package/dist/core/globals.js.map +1 -1
  7. package/dist/core/logger.d.ts.map +1 -1
  8. package/dist/core/logger.js +2 -7
  9. package/dist/core/logger.js.map +1 -1
  10. package/dist/core/mfa.d.ts.map +1 -1
  11. package/dist/core/mfa.js +1 -1
  12. package/dist/dsl/monitor.d.ts +4 -0
  13. package/dist/dsl/monitor.d.ts.map +1 -1
  14. package/dist/dsl/monitor.js +6 -0
  15. package/dist/dsl/monitor.js.map +1 -1
  16. package/dist/formatter/javascript.d.ts.map +1 -1
  17. package/dist/formatter/javascript.js.map +1 -1
  18. package/dist/generator/utils.d.ts +1 -1
  19. package/dist/helpers.d.ts.map +1 -1
  20. package/dist/helpers.js.map +1 -1
  21. package/dist/plugins/tracing.js.map +1 -1
  22. package/dist/push/bundler.d.ts +2 -5
  23. package/dist/push/bundler.d.ts.map +1 -1
  24. package/dist/push/bundler.js +15 -30
  25. package/dist/push/bundler.js.map +1 -1
  26. package/dist/push/index.d.ts.map +1 -1
  27. package/dist/push/index.js +21 -6
  28. package/dist/push/index.js.map +1 -1
  29. package/dist/push/monitor.d.ts +4 -1
  30. package/dist/push/monitor.d.ts.map +1 -1
  31. package/dist/push/monitor.js +13 -1
  32. package/dist/push/monitor.js.map +1 -1
  33. package/dist/push/run-local.d.ts +27 -0
  34. package/dist/push/run-local.d.ts.map +1 -0
  35. package/dist/push/run-local.js +132 -0
  36. package/dist/push/run-local.js.map +1 -0
  37. package/dist/push/utils.d.ts +2 -0
  38. package/dist/push/utils.d.ts.map +1 -1
  39. package/dist/push/utils.js +33 -1
  40. package/dist/push/utils.js.map +1 -1
  41. package/dist/reporters/base.d.ts.map +1 -1
  42. package/dist/reporters/base.js.map +1 -1
  43. package/dist/reporters/build_kite_cli.d.ts.map +1 -1
  44. package/dist/reporters/build_kite_cli.js.map +1 -1
  45. package/dist/reporters/json.d.ts +1 -1
  46. package/dist/reporters/json.d.ts.map +1 -1
  47. package/dist/reporters/json.js +6 -2
  48. package/dist/reporters/json.js.map +1 -1
  49. package/dist/reporters/junit.d.ts.map +1 -1
  50. package/dist/reporters/junit.js.map +1 -1
  51. package/package.json +8 -5
  52. package/src/cli.ts +8 -3
  53. package/src/common_types.ts +1 -1
  54. package/src/core/globals.ts +12 -0
  55. package/src/core/logger.ts +2 -8
  56. package/src/core/mfa.ts +11 -11
  57. package/src/dsl/monitor.ts +7 -0
  58. package/src/formatter/javascript.ts +3 -1
  59. package/src/helpers.ts +3 -3
  60. package/src/loader.ts +1 -1
  61. package/src/plugins/tracing.ts +1 -1
  62. package/src/push/bundler.ts +16 -36
  63. package/src/push/index.ts +28 -10
  64. package/src/push/monitor.ts +16 -4
  65. package/src/push/run-local.ts +155 -0
  66. package/src/push/utils.ts +51 -5
  67. package/src/reporters/base.ts +8 -4
  68. package/src/reporters/build_kite_cli.ts +7 -6
  69. package/src/reporters/json.ts +10 -14
  70. package/src/reporters/junit.ts +4 -10
package/src/cli.ts CHANGED
@@ -278,12 +278,17 @@ program
278
278
  // TOTP command
279
279
  program
280
280
  .command('totp <secret>')
281
- .description('Generate a Time-based One-Time token using the provided secret.')
282
- .option('--issuer <issuer>', 'Provider or Service the secret is associated with.')
281
+ .description(
282
+ 'Generate a Time-based One-Time token using the provided secret.'
283
+ )
284
+ .option(
285
+ '--issuer <issuer>',
286
+ 'Provider or Service the secret is associated with.'
287
+ )
283
288
  .option('--label <label>', 'Account Identifier (default: SyntheticsTOTP)')
284
289
  .action((secret, cmdOpts: TOTPCmdOptions) => {
285
290
  try {
286
- const token = totp(secret, cmdOpts)
291
+ const token = totp(secret, cmdOpts);
287
292
  write(bold(`OTP Token: ${token}`));
288
293
  } catch (e) {
289
294
  error(e);
@@ -324,4 +324,4 @@ export type JourneyEndResult = JourneyStartResult &
324
324
  options: RunOptions;
325
325
  };
326
326
 
327
- export type StepEndResult = StepResult
327
+ export type StepEndResult = StepResult;
@@ -34,4 +34,16 @@ if (!global[SYNTHETICS_RUNNER]) {
34
34
  global[SYNTHETICS_RUNNER] = new Runner();
35
35
  }
36
36
 
37
+ /**
38
+ * Set debug based on DEBUG ENV and namespace - synthetics
39
+ */
40
+ if (process.env.DEBUG && process.env.DEBUG.includes('synthetics')) {
41
+ process.env['__SYNTHETICS__DEBUG__'] = '1';
42
+ }
43
+
37
44
  export const runner: Runner = global[SYNTHETICS_RUNNER];
45
+
46
+ // is Debug mode enabled
47
+ export function inDebugMode() {
48
+ return !!process.env['__SYNTHETICS__DEBUG__'];
49
+ }
@@ -25,16 +25,10 @@
25
25
 
26
26
  import { grey, cyan, dim, italic } from 'kleur/colors';
27
27
  import { now } from '../helpers';
28
-
29
- /**
30
- * Set debug based on DEBUG ENV and namespace - synthetics
31
- */
32
- if (process.env.DEBUG && process.env.DEBUG.includes('synthetics')) {
33
- process.env['__SYNTHETICS__DEBUG__'] = '1';
34
- }
28
+ import { inDebugMode } from './globals';
35
29
 
36
30
  export function log(msg) {
37
- if (!process.env['__SYNTHETICS__DEBUG__'] || !msg) {
31
+ if (!inDebugMode() || !msg) {
38
32
  return;
39
33
  }
40
34
  if (typeof msg === 'object') {
package/src/core/mfa.ts CHANGED
@@ -23,48 +23,48 @@
23
23
  *
24
24
  */
25
25
 
26
- import { TOTP } from "otpauth";
26
+ import { TOTP } from 'otpauth';
27
27
 
28
28
  type TOTPOptions = {
29
29
  /**
30
30
  * Provider or Service the secret is associated with
31
31
  */
32
- issuer?: string
32
+ issuer?: string;
33
33
  /**
34
34
  * Account Identifier.
35
35
  * @default 'SyntheticsTOTP'
36
36
  */
37
- label?: string
37
+ label?: string;
38
38
  /**
39
39
  * Include issuer prefix in label.
40
40
  */
41
- issuerInLabel?: boolean
41
+ issuerInLabel?: boolean;
42
42
  /**
43
43
  * The encoded secret key used to generate the TOTP.
44
44
  */
45
- secret?: string
45
+ secret?: string;
46
46
  /**
47
47
  * The algorithm used to generate the TOTP.
48
48
  * @default 'SHA1'
49
49
  */
50
- algorithm?: string
50
+ algorithm?: string;
51
51
  /**
52
52
  * Number of digits in the generated token.
53
53
  * @default 6
54
54
  */
55
- digits?: number
55
+ digits?: number;
56
56
  /**
57
57
  * Validity period in seconds for the token.
58
58
  * @default 30
59
59
  */
60
- period?: number
60
+ period?: number;
61
61
  };
62
62
 
63
63
  export type TOTPCmdOptions = {
64
- issuer?: string
65
- label?: string
64
+ issuer?: string;
65
+ label?: string;
66
66
  };
67
67
 
68
68
  export function totp(secret?: string, options: TOTPOptions = {}) {
69
- return new TOTP({ label: "SyntheticsTOTP", secret, ...options }).generate();
69
+ return new TOTP({ label: 'SyntheticsTOTP', secret, ...options }).generate();
70
70
  }
@@ -158,6 +158,13 @@ export class Monitor {
158
158
  .digest('base64');
159
159
  }
160
160
 
161
+ /**
162
+ * Returns the size of the monitor in bytes which is sent as payload to Kibana
163
+ */
164
+ size() {
165
+ return JSON.stringify(this).length;
166
+ }
167
+
161
168
  validate() {
162
169
  const schedule = this.config.schedule;
163
170
  if (ALLOWED_SCHEDULES.includes(schedule)) {
@@ -199,7 +199,9 @@ export class SyntheticsGenerator extends JavaScriptLanguageGenerator {
199
199
  const offset = this.isProject ? 2 + stepIndent : 0 + stepIndent;
200
200
  const formatter = new JavaScriptFormatter(offset);
201
201
 
202
- const locators = actionInContext.frame.framePath.map(selector => `.${super._asLocator(selector)}.contentFrame()`);
202
+ const locators = actionInContext.frame.framePath.map(
203
+ selector => `.${super._asLocator(selector)}.contentFrame()`
204
+ );
203
205
  const subject = `${pageAlias}${locators.join('')}`;
204
206
  const signals = toSignalMap(action);
205
207
 
package/src/helpers.ts CHANGED
@@ -41,7 +41,7 @@ import micromatch from 'micromatch';
41
41
 
42
42
  const SEPARATOR = '\n';
43
43
 
44
- export function noop() { }
44
+ export function noop() {}
45
45
 
46
46
  export function indent(lines: string, tab = ' ') {
47
47
  return lines.replace(/^/gm, tab);
@@ -152,8 +152,8 @@ export function findPkgJsonByTraversing(resolvePath, cwd) {
152
152
  if (resolvePath === parentDirectory) {
153
153
  throw red(
154
154
  `Could not find package.json file in: "${cwd}"\n` +
155
- `It is recommended to run the agent in an NPM project.\n` +
156
- `You can create one by running "npm init -y" in the project folder.`
155
+ `It is recommended to run the agent in an NPM project.\n` +
156
+ `You can create one by running "npm init -y" in the project folder.`
157
157
  );
158
158
  }
159
159
  return findPkgJsonByTraversing(parentDirectory, cwd);
package/src/loader.ts CHANGED
@@ -29,7 +29,7 @@ import { CliArgs } from './common_types';
29
29
  import { step, journey } from './core';
30
30
  import { log } from './core/logger';
31
31
  import { expect } from './core/expect';
32
- import * as mfa from "./core/mfa";
32
+ import * as mfa from './core/mfa';
33
33
  import {
34
34
  isDepInstalled,
35
35
  isDirectory,
@@ -38,7 +38,7 @@ export type TraceOptions = {
38
38
  * https://chromedevtools.github.io/devtools-protocol/tot/Tracing/
39
39
  */
40
40
  export class Tracing {
41
- constructor(private driver: Driver, private options: TraceOptions) { }
41
+ constructor(private driver: Driver, private options: TraceOptions) {}
42
42
 
43
43
  async start() {
44
44
  log(`Plugins: started collecting trace events`);
@@ -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,
@@ -57,8 +57,11 @@ import {
57
57
  isBulkAPISupported,
58
58
  isLightweightMonitorSupported,
59
59
  logDiff,
60
+ logGroups,
61
+ printBytes,
60
62
  } from './utils';
61
- import { log } from '../core/logger';
63
+ import { runLocal } from './run-local';
64
+ import { inDebugMode } from '../core/globals';
62
65
 
63
66
  export async function push(monitors: Monitor[], options: PushOptions) {
64
67
  if (parseInt(process.env.CHUNK_SIZE) > 250) {
@@ -84,7 +87,7 @@ export async function push(monitors: Monitor[], options: PushOptions) {
84
87
  const { monitors: remote } = await bulkGetMonitors(options);
85
88
 
86
89
  progress(`preparing ${monitors.length} monitors`);
87
- const schemas = await buildMonitorSchema(monitors, true);
90
+ const { schemas, sizes } = await buildMonitorSchema(monitors, true);
88
91
  const local = getLocalMonitors(schemas);
89
92
 
90
93
  const { newIDs, changedIDs, removedIDs, unchangedIDs } = diffMonitorHashIDs(
@@ -92,6 +95,22 @@ export async function push(monitors: Monitor[], options: PushOptions) {
92
95
  remote
93
96
  );
94
97
  logDiff(newIDs, changedIDs, removedIDs, unchangedIDs);
98
+ if (inDebugMode()) {
99
+ logGroups(sizes, newIDs, changedIDs, removedIDs, unchangedIDs);
100
+ // show bundle size for the whole project
101
+ let totalSize = 0;
102
+ for (const value of sizes.values()) {
103
+ totalSize += value;
104
+ }
105
+ progress('total size of the monitors payload is ' + printBytes(totalSize));
106
+ }
107
+
108
+ if (options.dryRun) {
109
+ progress('Running browser monitors in dry run mode');
110
+ await runLocal(schemas);
111
+ progress('Dry run completed');
112
+ return;
113
+ }
95
114
 
96
115
  const updatedMonitors = new Set<string>([...changedIDs, ...newIDs]);
97
116
  if (updatedMonitors.size > 0) {
@@ -106,7 +125,6 @@ export async function push(monitors: Monitor[], options: PushOptions) {
106
125
  }
107
126
 
108
127
  if (removedIDs.size > 0) {
109
- log(`deleting monitor ids: ${Array.from(removedIDs.keys()).join(', ')}`);
110
128
  if (updatedMonitors.size === 0 && unchangedIDs.size === 0) {
111
129
  await confirmDelete(
112
130
  `Pushing without any monitors will delete all monitors associated with the project.\n Do you want to continue?`,
@@ -126,8 +144,7 @@ export async function push(monitors: Monitor[], options: PushOptions) {
126
144
  );
127
145
  }
128
146
  }
129
-
130
- done(`Pushed: ${grey(getMonitorManagementURL(options.url))}`);
147
+ done(`Pushed: ${underline(getMonitorManagementURL(options.url))} `);
131
148
  }
132
149
 
133
150
  async function confirmDelete(message: string, skip: boolean) {
@@ -223,8 +240,9 @@ export function validateSettings(opts: PushOptions) {
223
240
  - CLI '--schedule <mins>'
224
241
  - Config file 'monitors.schedule' field`;
225
242
  } 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(',')}`;
243
+ reason = `Set default schedule(${
244
+ opts.schedule
245
+ }) to one of the allowed values - ${ALLOWED_SCHEDULES.join(',')}`;
228
246
  } else if (
229
247
  (opts.locations ?? []).length > 0 &&
230
248
  (opts?.playwrightOptions?.clientCertificates ?? []).filter(cert => {
@@ -301,7 +319,7 @@ export async function pushLegacy(monitors: Monitor[], options: PushOptions) {
301
319
  let schemas: MonitorSchema[] = [];
302
320
  if (monitors.length > 0) {
303
321
  progress(`preparing ${monitors.length} monitors`);
304
- schemas = await buildMonitorSchema(monitors, false);
322
+ ({ schemas } = await buildMonitorSchema(monitors, false));
305
323
  const chunks = getChunks(schemas, 10);
306
324
  for (const chunk of chunks) {
307
325
  await liveProgress(
@@ -320,7 +338,7 @@ export async function pushLegacy(monitors: Monitor[], options: PushOptions) {
320
338
  `deleting all stale monitors`
321
339
  );
322
340
 
323
- done(`Pushed: ${grey(getMonitorManagementURL(options.url))}`);
341
+ done(`Pushed: ${underline(getMonitorManagementURL(options.url))}`);
324
342
  }
325
343
 
326
344
  // prints warning if any of the monitors has throttling settings enabled during push
@@ -42,6 +42,8 @@ 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[];
@@ -131,6 +133,7 @@ export async function buildMonitorSchema(monitors: Monitor[], isV2: boolean) {
131
133
  await mkdir(bundlePath, { recursive: true });
132
134
  const bundler = new Bundler();
133
135
  const schemas: MonitorSchema[] = [];
136
+ const sizes: Map<string, number> = new Map();
134
137
 
135
138
  for (const monitor of monitors) {
136
139
  const { source, config, filter, type } = monitor;
@@ -148,6 +151,17 @@ export async function buildMonitorSchema(monitors: Monitor[], isV2: boolean) {
148
151
  monitor.setContent(content);
149
152
  Object.assign(schema, { content, filter });
150
153
  }
154
+ const size = monitor.size();
155
+ const sizeKB = Math.round(size / 1000);
156
+ if (sizeKB > SIZE_LIMIT_KB) {
157
+ let outer = bold(
158
+ `Aborted: Bundled code ${sizeKB}kB exceeds the recommended ${SIZE_LIMIT_KB}kB limit. Please check the dependencies imported.\n`
159
+ );
160
+ const inner = `* ${config.id} - ${source.file}:${source.line}:${source.column}\n`;
161
+ outer += indent(inner);
162
+ throw red(outer);
163
+ }
164
+ sizes.set(config.id, size);
151
165
  /**
152
166
  * Generate hash only after the bundled content is created
153
167
  * to capture code changes in imported files
@@ -159,7 +173,7 @@ export async function buildMonitorSchema(monitors: Monitor[], isV2: boolean) {
159
173
  }
160
174
 
161
175
  await rm(bundlePath, { recursive: true });
162
- return schemas;
176
+ return { schemas, sizes };
163
177
  }
164
178
 
165
179
  export async function createLightweightMonitors(
@@ -225,9 +239,7 @@ export async function createLightweightMonitors(
225
239
  const monitor = mergedConfig[i];
226
240
  // Skip browser monitors from the YML files
227
241
  if (monitor['type'] === 'browser') {
228
- warn(
229
- `Browser monitors from ${file} are skipped.`
230
- );
242
+ warn(`Browser monitors from ${file} are skipped.`);
231
243
  continue;
232
244
  }
233
245
  const { line, col } = lineCounter.linePos(offsets[i]);
@@ -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
+ }
package/src/push/utils.ts CHANGED
@@ -25,7 +25,7 @@
25
25
 
26
26
  import semver from 'semver';
27
27
  import { progress, removeTrailingSlash } from '../helpers';
28
- import { green, red, grey, yellow } from 'kleur/colors';
28
+ import { green, red, grey, yellow, Colorize, bold } from 'kleur/colors';
29
29
  import { PushOptions } from '../common_types';
30
30
  import { Monitor } from '../dsl/monitor';
31
31
 
@@ -37,13 +37,59 @@ export function logDiff<T extends Set<string>>(
37
37
  ) {
38
38
  progress(
39
39
  'Monitor Diff: ' +
40
- green(`Added(${newIDs.size}) `) +
41
- yellow(`Updated(${changedIDs.size}) `) +
42
- red(`Removed(${removedIDs.size}) `) +
43
- grey(`Unchanged(${unchangedIDs.size})`)
40
+ green(`Added(${newIDs.size}) `) +
41
+ yellow(`Updated(${changedIDs.size}) `) +
42
+ red(`Removed(${removedIDs.size}) `) +
43
+ grey(`Unchanged(${unchangedIDs.size})`)
44
44
  );
45
45
  }
46
46
 
47
+ export function logGroups<T extends Set<string>>(
48
+ sizes: Map<string, number>,
49
+ newIDs: T,
50
+ changedIDs: T,
51
+ removedIDs: T,
52
+ unchangedIDs: T
53
+ ) {
54
+ console.groupCollapsed();
55
+ logGroup(sizes, 'Added', newIDs, green);
56
+ logGroup(sizes, 'Updated', changedIDs, yellow);
57
+ logGroup(sizes, 'Removed', removedIDs, red);
58
+ logGroup(sizes, 'Unchanged', unchangedIDs, grey);
59
+ console.groupEnd();
60
+ }
61
+
62
+ function logGroup(
63
+ sizes: Map<string, number>,
64
+ name: string,
65
+ ids: Set<string>,
66
+ color: Colorize
67
+ ) {
68
+ if (ids.size === 0) return;
69
+ // under collapsed group, so giving 2 space for padding
70
+ printLine(process.stdout.columns - 2);
71
+ console.groupCollapsed(color(bold(name)));
72
+ [...ids].forEach(id => {
73
+ console.log(grey(`- ${id} (${printBytes(sizes.get(id))})`));
74
+ });
75
+ console.groupEnd();
76
+ }
77
+
78
+ function printLine(length: number = process.stdout.columns) {
79
+ console.log(grey('-').repeat(length));
80
+ }
81
+
82
+ const BYTE_UNITS = ['B', 'kB', 'MB', 'GB', 'TB', 'PB'];
83
+ export function printBytes(bytes: number) {
84
+ if (bytes <= 0) return '0 B';
85
+ const exponent = Math.min(
86
+ Math.floor(Math.log10(bytes) / 3),
87
+ BYTE_UNITS.length - 1
88
+ );
89
+ bytes /= 1000 ** exponent;
90
+ return `${bytes.toFixed(1)} ${BYTE_UNITS[exponent]}`;
91
+ }
92
+
47
93
  export function getChunks<T>(arr: Array<T>, size: number): Array<T[]> {
48
94
  const chunks = [];
49
95
  for (let i = 0; i < arr.length; i += size) {