@elastic/synthetics 1.8.0 → 1.9.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 (43) hide show
  1. package/dist/cli.js +11 -9
  2. package/dist/cli.js.map +1 -1
  3. package/dist/common_types.d.ts +12 -3
  4. package/dist/common_types.d.ts.map +1 -1
  5. package/dist/core/expect.d.ts +1 -1
  6. package/dist/core/expect.d.ts.map +1 -1
  7. package/dist/core/expect.js +10 -1
  8. package/dist/core/expect.js.map +1 -1
  9. package/dist/core/runner.d.ts.map +1 -1
  10. package/dist/core/runner.js +12 -6
  11. package/dist/core/runner.js.map +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/generator/index.d.ts.map +1 -1
  17. package/dist/generator/index.js +22 -16
  18. package/dist/generator/index.js.map +1 -1
  19. package/dist/options.d.ts +11 -2
  20. package/dist/options.d.ts.map +1 -1
  21. package/dist/options.js +66 -59
  22. package/dist/options.js.map +1 -1
  23. package/dist/push/index.d.ts.map +1 -1
  24. package/dist/push/index.js +14 -9
  25. package/dist/push/index.js.map +1 -1
  26. package/dist/push/monitor.d.ts.map +1 -1
  27. package/dist/push/monitor.js +9 -2
  28. package/dist/push/monitor.js.map +1 -1
  29. package/dist/push/request.d.ts.map +1 -1
  30. package/dist/push/request.js +3 -2
  31. package/dist/push/request.js.map +1 -1
  32. package/package.json +16 -6
  33. package/src/cli.ts +24 -13
  34. package/src/common_types.ts +13 -3
  35. package/src/core/expect.ts +13 -3
  36. package/src/core/runner.ts +17 -6
  37. package/src/dsl/monitor.ts +13 -1
  38. package/src/generator/index.ts +24 -20
  39. package/src/options.ts +81 -66
  40. package/src/push/index.ts +22 -9
  41. package/src/push/monitor.ts +12 -3
  42. package/src/push/request.ts +3 -6
  43. package/dist/bundles/lib/index.js +0 -430
@@ -23,8 +23,19 @@
23
23
  *
24
24
  */
25
25
 
26
+ import { join } from 'path';
27
+
28
+ /**
29
+ * This file is a workaround to extend the expect functionality from Playwright package
30
+ * with few extensions that we don't support in Synthetics.
31
+ *
32
+ * We are requiring the package using absolute path to workaround the Module
33
+ * resolution export issues.
34
+ */
35
+
26
36
  /* eslint-disable @typescript-eslint/no-var-requires */
27
- const expectLib = require('../../dist/bundles/lib/index').expect;
37
+ const PW_PATH = require.resolve('playwright').replace('index.js', '');
38
+ const expectLib = require(join(PW_PATH, 'lib/matchers/expect')).expect;
28
39
 
29
40
  function notSupported(name: string) {
30
41
  throw new Error(`expect.${name} is not supported in @elastic/synthetics.`);
@@ -39,5 +50,4 @@ expectLib.extend({
39
50
  toMatchSnapshot: () => notSupported('toMatchSnapshot'),
40
51
  });
41
52
 
42
- export const expect: typeof import('../../bundles/node_modules/playwright/types/test').expect =
43
- expectLib;
53
+ export const expect: typeof import('playwright/types/test').expect = expectLib;
@@ -404,8 +404,7 @@ export default class Runner {
404
404
 
405
405
  buildMonitors(options: PushOptions) {
406
406
  /**
407
- * Update the global monitor configuration required for
408
- * setting defaults
407
+ * Update the global monitor configuration required for setting defaults
409
408
  */
410
409
  this.updateMonitor({
411
410
  throttling: options.throttling,
@@ -430,11 +429,23 @@ export default class Runner {
430
429
  );
431
430
  }
432
431
  /**
433
- * Execute dummy callback to get all monitor specific
434
- * configurations for the current journey
432
+ * Before pushing a browser monitor, three things need to be done:
433
+ *
434
+ * - execute callback `monitor.use` in particular to get monitor configurations
435
+ * - update the monitor config with global configuration
436
+ * - filter out monitors based on matched tags and name after applying both
437
+ * global and local monitor configurations
435
438
  */
436
439
  journey.callback({ params: options.params } as any);
437
440
  journey.monitor.update(this.monitor?.config);
441
+ if (
442
+ !journey.monitor.isMatch(
443
+ options.grepOpts?.match,
444
+ options.grepOpts?.tags
445
+ )
446
+ ) {
447
+ continue;
448
+ }
438
449
  journey.monitor.validate();
439
450
  monitors.push(journey.monitor);
440
451
  }
@@ -454,7 +465,7 @@ export default class Runner {
454
465
  params: options.params,
455
466
  }).catch(e => (this.hookError = e));
456
467
 
457
- const { dryRun, match, tags } = options;
468
+ const { dryRun, grepOpts } = options;
458
469
  /**
459
470
  * Skip other journeys when using `.only`
460
471
  */
@@ -471,7 +482,7 @@ export default class Runner {
471
482
  this.#reporter.onJourneyRegister?.(journey);
472
483
  continue;
473
484
  }
474
- if (!journey.isMatch(match, tags) || journey.skip) {
485
+ if (!journey.isMatch(grepOpts?.match, grepOpts?.tags) || journey.skip) {
475
486
  continue;
476
487
  }
477
488
  const journeyResult: JourneyResult = this.hookError
@@ -33,7 +33,7 @@ import {
33
33
  Params,
34
34
  PlaywrightOptions,
35
35
  } from '../common_types';
36
- import { indent } from '../helpers';
36
+ import { indent, isMatch } from '../helpers';
37
37
  import { LocationsMap } from '../locations/public-locations';
38
38
 
39
39
  export type SyntheticsLocationsType = keyof typeof LocationsMap;
@@ -129,6 +129,18 @@ export class Monitor {
129
129
  this.filter = filter;
130
130
  }
131
131
 
132
+ /**
133
+ * Matches monitors based on the provided args. Proitize tags over match
134
+ */
135
+ isMatch(matchPattern: string, tagsPattern: Array<string>) {
136
+ return isMatch(
137
+ this.config.tags,
138
+ this.config.name,
139
+ tagsPattern,
140
+ matchPattern
141
+ );
142
+ }
143
+
132
144
  /**
133
145
  * Hash is used to identify if the monitor has changed since the last time
134
146
  * it was pushed to Kibana. Change is based on three factors:
@@ -22,13 +22,14 @@
22
22
  * THE SOFTWARE.
23
23
  *
24
24
  */
25
+ import { URL } from 'url';
25
26
  import { execSync } from 'child_process';
26
27
  import { existsSync } from 'fs';
27
28
  import { mkdir, readFile, writeFile } from 'fs/promises';
28
29
  import { bold, cyan, yellow } from 'kleur/colors';
29
30
  import { join, relative, dirname, basename } from 'path';
30
31
  // @ts-ignore-next-line: has no exported member 'Input'
31
- import { prompt, Input } from 'enquirer';
32
+ import { prompt, Input, Password } from 'enquirer';
32
33
  import { getProjectApiKeyURL, progress, write as stdWrite } from '../helpers';
33
34
  import {
34
35
  getPackageManager,
@@ -60,6 +61,8 @@ export const REGULAR_FILES_PATH = [
60
61
  ];
61
62
  export const CONFIG_PATH = 'synthetics.config.ts';
62
63
 
64
+ const IS_URL = new RegExp('^(https?:\\/\\/)');
65
+
63
66
  export class Generator {
64
67
  pkgManager = 'npm';
65
68
  constructor(public projectDir: string) {}
@@ -80,29 +83,30 @@ export class Generator {
80
83
  return JSON.parse(process.env.TEST_QUESTIONS);
81
84
  }
82
85
 
83
- const { onCloud } = await prompt<{ onCloud: string }>({
84
- type: 'confirm',
85
- name: 'onCloud',
86
- initial: 'y',
87
- message: 'Do you use Elastic Cloud',
88
- });
89
86
  const url = await new Input({
90
- header: onCloud
91
- ? yellow(
92
- 'Get cloud.id from your deployment https://www.elastic.co/guide/en/cloud/current/ec-cloud-id.html'
93
- )
94
- : '',
95
- message: onCloud
96
- ? 'What is your cloud.id'
97
- : 'What is the url of your Kibana instance',
98
87
  name: 'url',
88
+ message: 'Enter Elastic Kibana URL or Cloud ID',
99
89
  required: true,
100
- result(value) {
101
- return onCloud ? cloudIDToKibanaURL(value) : value;
90
+ validate(value) {
91
+ try {
92
+ if (!IS_URL.test(value)) {
93
+ value = cloudIDToKibanaURL(value);
94
+ }
95
+ new URL(value);
96
+ return true;
97
+ } catch (e) {
98
+ return 'Invalid URL or Cloud ID';
99
+ }
100
+ },
101
+ result(value: string) {
102
+ if (!IS_URL.test(value)) {
103
+ value = cloudIDToKibanaURL(value);
104
+ }
105
+ return value;
102
106
  },
103
107
  }).run();
104
108
 
105
- const auth = await new Input({
109
+ const auth = await new Password({
106
110
  name: 'auth',
107
111
  header: yellow(
108
112
  `Generate API key from Kibana ${getProjectApiKeyURL(url)}`
@@ -249,7 +253,7 @@ export class Generator {
249
253
  }
250
254
  // Add push command
251
255
  if (!pkgJSON.scripts.push) {
252
- pkgJSON.scripts.push = 'npx @elastic/synthetics push';
256
+ pkgJSON.scripts.push = `npx @elastic/synthetics push`;
253
257
  }
254
258
 
255
259
  await this.createFile(
@@ -287,7 +291,7 @@ All set, you can run below commands inside: ${this.projectDir}:
287
291
  )}
288
292
 
289
293
  ${yellow(
290
- 'Make sure to configure the SYNTHETICS_API_KEY before pushing monitors to Kibana.'
294
+ 'Configure API Key via `SYNTHETICS_API_KEY` env variable or --auth CLI flag.'
291
295
  )}
292
296
 
293
297
  Visit https://www.elastic.co/guide/en/observability/current/synthetic-run-tests.html to learn more.
package/src/options.ts CHANGED
@@ -26,74 +26,49 @@
26
26
  import merge from 'deepmerge';
27
27
  import { createOption } from 'commander';
28
28
  import { readConfig } from './config';
29
- import type { CliArgs, PushOptions, RunOptions } from './common_types';
30
- import { THROTTLING_WARNING_MSG, error, warn } from './helpers';
29
+ import type { CliArgs, RunOptions } from './common_types';
30
+ import { THROTTLING_WARNING_MSG, warn } from './helpers';
31
31
 
32
32
  type Mode = 'run' | 'push';
33
33
 
34
+ /**
35
+ * Normalize the options passed via CLI and Synthetics config file
36
+ *
37
+ * Order of preference for options:
38
+ * 1. Local options configured via Runner API
39
+ * 2. CLI flags
40
+ * 3. Configuration file
41
+ */
34
42
  export async function normalizeOptions(
35
43
  cliArgs: CliArgs,
36
44
  mode: Mode = 'run'
37
45
  ): Promise<RunOptions> {
46
+ /**
47
+ * Move filtering flags from the top level to filter object
48
+ * and delete the old keys
49
+ */
50
+ const grepOpts = {
51
+ pattern: cliArgs.pattern,
52
+ tags: cliArgs.tags,
53
+ match: cliArgs.match,
54
+ };
55
+ delete cliArgs.pattern;
56
+ delete cliArgs.tags;
57
+ delete cliArgs.match;
58
+
38
59
  const options: RunOptions = {
39
60
  ...cliArgs,
61
+ grepOpts,
40
62
  environment: process.env['NODE_ENV'] || 'development',
41
63
  };
42
64
  /**
43
- * Group all events that can be consumed by heartbeat and
44
- * eventually by the Synthetics UI.
45
- */
46
- if (cliArgs.richEvents) {
47
- options.reporter = cliArgs.reporter ?? 'json';
48
- options.ssblocks = true;
49
- options.network = true;
50
- options.trace = true;
51
- options.quietExitCode = true;
52
- }
53
-
54
- if (cliArgs.capability) {
55
- const supportedCapabilities = [
56
- 'trace',
57
- 'network',
58
- 'filmstrips',
59
- 'metrics',
60
- 'ssblocks',
61
- ];
62
- /**
63
- * trace - record chrome trace events(LCP, FCP, CLS, etc.) for all journeys
64
- * network - capture network information for all journeys
65
- * filmstrips - record detailed filmstrips for all journeys
66
- * metrics - capture performance metrics (DOM Nodes, Heap size, etc.) for each step
67
- * ssblocks - Dedupes the screenshots in to blocks to save storage space
68
- */
69
- for (const flag of cliArgs.capability) {
70
- if (supportedCapabilities.includes(flag)) {
71
- options[flag] = true;
72
- } else {
73
- console.warn(
74
- `Missing capability "${flag}", current supported capabilities are ${supportedCapabilities.join(
75
- ', '
76
- )}`
77
- );
78
- }
79
- }
80
- }
81
-
82
- /**
83
- * Validate and read synthetics config file
84
- * based on the environment
65
+ * Validate and read synthetics config file based on the environment
85
66
  */
86
67
  const config =
87
68
  cliArgs.config || !cliArgs.inline
88
69
  ? await readConfig(options.environment, cliArgs.config)
89
70
  : {};
90
71
 
91
- /**
92
- * Order of preference for options that are used while running are
93
- * 1. Local options configured via Runner API
94
- * 2. CLI flags
95
- * 3. Configuration file
96
- */
97
72
  options.params = Object.freeze(merge(config.params, cliArgs.params || {}));
98
73
 
99
74
  /**
@@ -118,10 +93,49 @@ export async function normalizeOptions(
118
93
  */
119
94
  switch (mode) {
120
95
  case 'run':
96
+ if (cliArgs.capability) {
97
+ const supportedCapabilities = [
98
+ 'trace',
99
+ 'network',
100
+ 'filmstrips',
101
+ 'metrics',
102
+ 'ssblocks',
103
+ ];
104
+ /**
105
+ * trace - record chrome trace events(LCP, FCP, CLS, etc.) for all journeys
106
+ * network - capture network information for all journeys
107
+ * filmstrips - record detailed filmstrips for all journeys
108
+ * metrics - capture performance metrics (DOM Nodes, Heap size, etc.) for each step
109
+ * ssblocks - Dedupes the screenshots in to blocks to save storage space
110
+ */
111
+ for (const flag of cliArgs.capability) {
112
+ if (supportedCapabilities.includes(flag)) {
113
+ options[flag] = true;
114
+ } else {
115
+ console.warn(
116
+ `Missing capability "${flag}", current supported capabilities are ${supportedCapabilities.join(
117
+ ', '
118
+ )}`
119
+ );
120
+ }
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Group all events that can be consumed by heartbeat and
126
+ * eventually by the Synthetics UI.
127
+ */
128
+ if (cliArgs.richEvents) {
129
+ options.reporter = cliArgs.reporter ?? 'json';
130
+ options.ssblocks = true;
131
+ options.network = true;
132
+ options.trace = true;
133
+ options.quietExitCode = true;
134
+ }
135
+
121
136
  options.screenshots = cliArgs.screenshots ?? 'on';
122
137
  break;
123
138
  case 'push':
124
- validatePushOptions(options as PushOptions);
125
139
  /**
126
140
  * Merge the default monitor config from synthetics.config.ts file
127
141
  * with the CLI options passed via push command
@@ -152,14 +166,6 @@ export function getHeadlessFlag(
152
166
  return configHeadless ?? true;
153
167
  }
154
168
 
155
- export function validatePushOptions(opts: PushOptions) {
156
- if (opts.tags || opts.match) {
157
- throw error(`Aborted. Invalid CLI flags.
158
-
159
- Tags and Match are not supported in push command.`);
160
- }
161
- }
162
-
163
169
  /* eslint-disable-next-line @typescript-eslint/no-unused-vars */
164
170
  function toObject(value: boolean | Record<string, any>): Record<string, any> {
165
171
  const defaulVal = {};
@@ -183,19 +189,17 @@ export function parseThrottling() {
183
189
  export function getCommonCommandOpts() {
184
190
  const params = createOption(
185
191
  '-p, --params <jsonstring>',
186
- 'JSON object that gets injected to all journeys'
187
- );
188
- params.argParser(JSON.parse);
192
+ 'JSON object that defines any variables your tests require.'
193
+ ).argParser(JSON.parse);
189
194
 
190
195
  const playwrightOpts = createOption(
191
196
  '--playwright-options <jsonstring>',
192
197
  'JSON object to pass in custom Playwright options for the agent. Options passed will be merged with Playwright options defined in your synthetics.config.js file.'
193
- );
194
- playwrightOpts.argParser(JSON.parse);
198
+ ).argParser(JSON.parse);
195
199
 
196
200
  const pattern = createOption(
197
201
  '--pattern <pattern>',
198
- 'RegExp pattern to match journey/monitor files that are different from the default (ex: /*.journey.(ts|js)$/)'
202
+ 'RegExp pattern to match journey files in the current working directory (default: /*.journey.(ts|js)$/)'
199
203
  );
200
204
 
201
205
  const apiDocsLink =
@@ -210,7 +214,16 @@ export function getCommonCommandOpts() {
210
214
 
211
215
  const configOpt = createOption(
212
216
  '-c, --config <path>',
213
- 'configuration path (default: synthetics.config.js)'
217
+ 'path to the configuration file (default: synthetics.config.(js|ts))'
218
+ );
219
+
220
+ const tags = createOption(
221
+ '--tags <name...>',
222
+ 'run/push tests with the tag(s) matching a pattern'
223
+ );
224
+ const match = createOption(
225
+ '--match <name>',
226
+ 'run/push tests with a name or tags that matches a pattern'
214
227
  );
215
228
 
216
229
  return {
@@ -220,5 +233,7 @@ export function getCommonCommandOpts() {
220
233
  playwrightOpts,
221
234
  pattern,
222
235
  configOpt,
236
+ tags,
237
+ match,
223
238
  };
224
239
  }
package/src/push/index.ts CHANGED
@@ -58,6 +58,7 @@ import {
58
58
  isLightweightMonitorSupported,
59
59
  logDiff,
60
60
  } from './utils';
61
+ import { log } from '../core/logger';
61
62
 
62
63
  export async function push(monitors: Monitor[], options: PushOptions) {
63
64
  const duplicates = trackDuplicates(monitors);
@@ -99,8 +100,17 @@ export async function push(monitors: Monitor[], options: PushOptions) {
99
100
  }
100
101
 
101
102
  if (removedIDs.size > 0) {
103
+ log(`deleting monitor ids: ${Array.from(removedIDs.keys()).join(', ')}`);
102
104
  if (updatedMonitors.size === 0 && unchangedIDs.size === 0) {
103
- await promptConfirmDeleteAll(options);
105
+ await confirmDelete(
106
+ `Pushing without any monitors will delete all monitors associated with the project.\n Do you want to continue?`,
107
+ options.yes
108
+ );
109
+ } else {
110
+ await confirmDelete(
111
+ `Deleting ${removedIDs.size} monitors. Do you want to continue?`,
112
+ options.yes
113
+ );
104
114
  }
105
115
  const chunks = getChunks(Array.from(removedIDs), CHUNK_SIZE);
106
116
  for (const chunk of chunks) {
@@ -114,22 +124,22 @@ export async function push(monitors: Monitor[], options: PushOptions) {
114
124
  done(`Pushed: ${grey(getMonitorManagementURL(options.url))}`);
115
125
  }
116
126
 
117
- async function promptConfirmDeleteAll(options: PushOptions) {
118
- write('');
119
- const { deleteAll } = await prompt<{ deleteAll: boolean }>({
127
+ async function confirmDelete(message: string, skip: boolean) {
128
+ const { deleteMonitors } = await prompt<{ deleteMonitors: boolean }>({
120
129
  type: 'confirm',
121
130
  skip() {
122
- if (options.yes) {
131
+ write('');
132
+ if (skip || !process.stdout.isTTY) {
123
133
  this.initial = process.env.TEST_OVERRIDE ?? true;
124
134
  return true;
125
135
  }
126
136
  return false;
127
137
  },
128
- name: 'deleteAll',
129
- message: `Pushing without any monitors will delete all monitors associated with the project.\n Do you want to continue?`,
138
+ name: 'deleteMonitors',
139
+ message,
130
140
  initial: false,
131
141
  });
132
- if (!deleteAll) {
142
+ if (!deleteMonitors) {
133
143
  throw warn('Push command Aborted');
134
144
  }
135
145
  }
@@ -288,7 +298,10 @@ export async function pushLegacy(monitors: Monitor[], options: PushOptions) {
288
298
  );
289
299
  }
290
300
  } else {
291
- await promptConfirmDeleteAll(options);
301
+ await confirmDelete(
302
+ `Pushing without any monitors will delete all monitors associated with the project.\n Do you want to continue?`,
303
+ options.yes
304
+ );
292
305
  }
293
306
  await liveProgress(
294
307
  createMonitorsLegacy({ schemas, keepStale: false, options }),
@@ -167,8 +167,8 @@ export async function createLightweightMonitors(
167
167
  ) {
168
168
  const lwFiles = new Set<string>();
169
169
  // Filter monitor files based on the provided pattern
170
- const pattern = options.pattern
171
- ? new RegExp(options.pattern, 'i')
170
+ const pattern = options.grepOpts?.pattern
171
+ ? new RegExp(options.grepOpts?.pattern, 'i')
172
172
  : /.(yml|yaml)$/;
173
173
  const ignore = /(node_modules|.github)/;
174
174
  await totalist(workDir, (rel, abs) => {
@@ -211,7 +211,9 @@ export async function createLightweightMonitors(
211
211
  offsets.push(monNode.srcToken.offset);
212
212
  }
213
213
 
214
- const mergedConfig = parsedDoc.toJS()['heartbeat.monitors'];
214
+ const mergedConfig = parsedDoc.toJS()[
215
+ 'heartbeat.monitors'
216
+ ] as Array<MonitorConfig>;
215
217
  for (let i = 0; i < mergedConfig.length; i++) {
216
218
  const monitor = mergedConfig[i];
217
219
  // Skip browser monitors from the YML files
@@ -220,7 +222,14 @@ export async function createLightweightMonitors(
220
222
  }
221
223
  const { line, col } = lineCounter.linePos(offsets[i]);
222
224
  try {
225
+ /**
226
+ * Build the monitor object from the yaml config along with global configuration
227
+ * and perform the match based on the provided filters
228
+ */
223
229
  const mon = buildMonitorFromYaml(monitor, options);
230
+ if (!mon.isMatch(options.grepOpts?.match, options.grepOpts?.tags)) {
231
+ continue;
232
+ }
224
233
  mon.setSource({ file, line, column: col });
225
234
  monitors.push(mon);
226
235
  } catch (e) {
@@ -81,13 +81,10 @@ export async function handleError(
81
81
  } else if (!ok(statusCode)) {
82
82
  let parsed: APIError;
83
83
  try {
84
- parsed = (await body.json()) as APIError;
84
+ const resp = await body.text();
85
+ parsed = JSON.parse(resp) as APIError;
85
86
  } catch (e) {
86
- throw formatAPIError(
87
- statusCode,
88
- 'unexpected non-JSON error',
89
- await body.text()
90
- );
87
+ throw formatAPIError(statusCode, 'unexpected error', e.message);
91
88
  }
92
89
  throw formatAPIError(statusCode, parsed.error, parsed.message);
93
90
  }