@elastic/synthetics 1.0.0-beta.31 → 1.0.0-beta.32

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 (72) hide show
  1. package/dist/cli.js +51 -26
  2. package/dist/cli.js.map +1 -1
  3. package/dist/common_types.d.ts +2 -0
  4. package/dist/common_types.d.ts.map +1 -1
  5. package/dist/core/runner.d.ts.map +1 -1
  6. package/dist/core/runner.js +4 -3
  7. package/dist/core/runner.js.map +1 -1
  8. package/dist/dsl/monitor.d.ts +2 -1
  9. package/dist/dsl/monitor.d.ts.map +1 -1
  10. package/dist/dsl/monitor.js +2 -2
  11. package/dist/dsl/monitor.js.map +1 -1
  12. package/dist/generator/index.d.ts +11 -3
  13. package/dist/generator/index.d.ts.map +1 -1
  14. package/dist/generator/index.js +98 -27
  15. package/dist/generator/index.js.map +1 -1
  16. package/dist/generator/utils.d.ts +2 -1
  17. package/dist/generator/utils.d.ts.map +1 -1
  18. package/dist/generator/utils.js +27 -4
  19. package/dist/generator/utils.js.map +1 -1
  20. package/dist/helpers.d.ts +9 -5
  21. package/dist/helpers.d.ts.map +1 -1
  22. package/dist/helpers.js +37 -10
  23. package/dist/helpers.js.map +1 -1
  24. package/dist/locations/index.d.ts +45 -0
  25. package/dist/locations/index.d.ts.map +1 -0
  26. package/dist/locations/index.js +90 -0
  27. package/dist/locations/index.js.map +1 -0
  28. package/dist/{dsl/locations.d.ts → locations/public-locations.d.ts} +1 -1
  29. package/dist/locations/public-locations.d.ts.map +1 -0
  30. package/dist/{dsl/locations.js → locations/public-locations.js} +1 -1
  31. package/dist/locations/public-locations.js.map +1 -0
  32. package/dist/options.d.ts +7 -0
  33. package/dist/options.d.ts.map +1 -1
  34. package/dist/options.js +21 -1
  35. package/dist/options.js.map +1 -1
  36. package/dist/push/bundler.d.ts.map +1 -1
  37. package/dist/push/bundler.js +7 -2
  38. package/dist/push/bundler.js.map +1 -1
  39. package/dist/push/index.d.ts +6 -1
  40. package/dist/push/index.d.ts.map +1 -1
  41. package/dist/push/index.js +124 -5
  42. package/dist/push/index.js.map +1 -1
  43. package/dist/push/monitor.d.ts +2 -0
  44. package/dist/push/monitor.d.ts.map +1 -1
  45. package/dist/push/monitor.js +23 -7
  46. package/dist/push/monitor.js.map +1 -1
  47. package/dist/push/request.d.ts +8 -7
  48. package/dist/push/request.d.ts.map +1 -1
  49. package/dist/push/request.js +21 -22
  50. package/dist/push/request.js.map +1 -1
  51. package/dist/tsconfig.tsbuildinfo +1 -1
  52. package/package.json +1 -1
  53. package/src/cli.ts +79 -59
  54. package/src/common_types.ts +2 -0
  55. package/src/core/runner.ts +5 -6
  56. package/src/dsl/monitor.ts +2 -1
  57. package/src/generator/index.ts +138 -34
  58. package/src/generator/utils.ts +29 -3
  59. package/src/helpers.ts +35 -11
  60. package/src/locations/index.ts +108 -0
  61. package/src/{dsl/locations.ts → locations/public-locations.ts} +0 -0
  62. package/src/options.ts +38 -0
  63. package/src/push/bundler.ts +7 -2
  64. package/src/push/index.ts +150 -7
  65. package/src/push/monitor.ts +37 -9
  66. package/src/push/request.ts +26 -34
  67. package/templates/journeys/advanced-example-helpers.ts +9 -9
  68. package/templates/journeys/advanced-example.journey.ts +6 -6
  69. package/templates/journeys/example.journey.ts +1 -1
  70. package/templates/synthetics.config.ts +2 -1
  71. package/dist/dsl/locations.d.ts.map +0 -1
  72. package/dist/dsl/locations.js.map +0 -1
package/src/helpers.ts CHANGED
@@ -27,7 +27,7 @@ import { red, green, yellow, cyan, bold } from 'kleur/colors';
27
27
  import os from 'os';
28
28
  import { resolve, join, dirname } from 'path';
29
29
  import fs from 'fs';
30
- import { promisify } from 'util';
30
+ import { lstat, readdir } from 'fs/promises';
31
31
  import { performance } from 'perf_hooks';
32
32
  import sourceMapSupport from '@cspotcode/source-map-support';
33
33
  import {
@@ -38,13 +38,6 @@ import {
38
38
  ThrottlingOptions,
39
39
  } from './common_types';
40
40
 
41
- const lstatAsync = promisify(fs.lstat);
42
- const readdirAsync = promisify(fs.readdir);
43
-
44
- export const readFileAsync = promisify(fs.readFile);
45
- export const writeFileAsync = promisify(fs.writeFile);
46
- export const mkdirAsync = promisify(fs.mkdir);
47
-
48
41
  const SEPARATOR = '\n';
49
42
 
50
43
  export function noop() {}
@@ -177,11 +170,11 @@ export async function totalist(
177
170
  pre = ''
178
171
  ) {
179
172
  dir = resolve('.', dir);
180
- await readdirAsync(dir).then(arr => {
173
+ await readdir(dir).then(arr => {
181
174
  return Promise.all(
182
175
  arr.map(str => {
183
176
  const abs = join(dir, str);
184
- return lstatAsync(abs).then(stats =>
177
+ return lstat(abs).then(stats =>
185
178
  stats.isDirectory()
186
179
  ? totalist(abs, callback, join(pre, str))
187
180
  : callback(join(pre, str), abs)
@@ -254,12 +247,18 @@ export function formatError(error: Error) {
254
247
  }
255
248
 
256
249
  const cwd = process.cwd();
250
+ /**
251
+ * All the settings that are related to the Synthetics is stored
252
+ * under this directory
253
+ * Examples: Screenshots, Project setup
254
+ */
255
+ export const SYNTHETICS_PATH = join(cwd, '.synthetics');
257
256
  /**
258
257
  * Synthetics cache path that is based on the process id to make sure
259
258
  * each process does not modify the caching layer used by other process
260
259
  * once we move to executing journeys in parallel
261
260
  */
262
- export const CACHE_PATH = join(cwd, '.synthetics', process.pid.toString());
261
+ export const CACHE_PATH = join(SYNTHETICS_PATH, process.pid.toString());
263
262
 
264
263
  export function getDurationInUs(duration: number) {
265
264
  return Math.trunc(duration * 1e6);
@@ -322,6 +321,23 @@ export function wrapFnWithLocation<A extends unknown[], R>(
322
321
  };
323
322
  }
324
323
 
324
+ // Safely parse ND JSON (Newline delimitted JSON) chunks
325
+ export function safeNDJSONParse(chunks: string[]) {
326
+ // chunks may not be at proper newline boundaries, so we make sure everything is split
327
+ // on proper newlines
328
+ chunks = Array.isArray(chunks) ? chunks : [chunks];
329
+ const lines = chunks.join('\n').split(/\r?\n/);
330
+ return lines
331
+ .filter(l => l.match(/\S/)) // remove blank lines
332
+ .map(line => {
333
+ try {
334
+ return JSON.parse(line);
335
+ } catch (e) {
336
+ throw `Error ${e} could not parse data '${line}'`;
337
+ }
338
+ });
339
+ }
340
+
325
341
  // Console helpers
326
342
  export function write(message: string) {
327
343
  process.stderr.write(message + '\n');
@@ -338,3 +354,11 @@ export function error(message: string) {
338
354
  export function done(message: string) {
339
355
  write(bold(green(`${symbols['succeeded']} ${message}`)));
340
356
  }
357
+
358
+ export function aborted(message: string) {
359
+ write(bold(yellow(`${symbols['warning']} ${message}`)));
360
+ }
361
+
362
+ export function removeTrailingSlash(url: string) {
363
+ return url.replace(/\/+$/, '');
364
+ }
@@ -0,0 +1,108 @@
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 {
27
+ formatAPIError,
28
+ formatNotFoundError,
29
+ sendRequest,
30
+ ok,
31
+ } from '../push/request';
32
+ import { indent, removeTrailingSlash, write } from '../helpers';
33
+
34
+ export type LocationCmdOptions = {
35
+ auth: string;
36
+ url: string;
37
+ };
38
+
39
+ type LocationMetadata = {
40
+ id: string;
41
+ label: string;
42
+ isServiceManaged: boolean;
43
+ };
44
+
45
+ export type LocationAPIResponse = {
46
+ locations: Array<LocationMetadata>;
47
+ };
48
+
49
+ const PRIVATE_KEYWORD = '(private)';
50
+
51
+ export async function getLocations(options: LocationCmdOptions) {
52
+ const url =
53
+ removeTrailingSlash(options.url) + '/internal/uptime/service/locations';
54
+ const { body, statusCode } = await sendRequest({
55
+ url,
56
+ method: 'GET',
57
+ auth: options.auth,
58
+ });
59
+ if (statusCode === 404) {
60
+ throw formatNotFoundError(await body.text());
61
+ }
62
+ if (!ok(statusCode)) {
63
+ const { error, message } = await body.json();
64
+ throw formatAPIError(statusCode, error, message);
65
+ }
66
+ return ((await body.json()) as LocationAPIResponse).locations;
67
+ }
68
+
69
+ export function formatLocations(locations: Array<LocationMetadata>) {
70
+ const formatted: Array<string> = [];
71
+ for (const location of locations) {
72
+ let name = location.label;
73
+ if (location.isServiceManaged) {
74
+ [, name] = name.includes('-') ? name.split('-') : [, name];
75
+ name = name.toLowerCase().trim().split(' ').join('_');
76
+ } else {
77
+ name = `${name}${PRIVATE_KEYWORD}`;
78
+ }
79
+ formatted.push(name);
80
+ }
81
+ return formatted;
82
+ }
83
+
84
+ export function groupLocations(locations: Array<string>) {
85
+ const grouped = { locations: [], privateLocations: [] };
86
+ for (const loc of locations) {
87
+ if (loc.includes(PRIVATE_KEYWORD)) {
88
+ grouped.privateLocations.push(loc.replace(PRIVATE_KEYWORD, ''));
89
+ } else {
90
+ grouped.locations.push(loc);
91
+ }
92
+ }
93
+ return grouped;
94
+ }
95
+
96
+ export function renderLocations(locations: Array<string>) {
97
+ let outer = 'Available locations: \n';
98
+ let inner = '';
99
+ for (const location of locations) {
100
+ inner += `* ${location}\n`;
101
+ }
102
+ outer += indent(inner);
103
+ outer += `\nSet default location for monitors via
104
+ - Synthetics config file 'monitors.locations' | 'monitors.privateLocations' field
105
+ - Monitor API 'monitor.use({ locations: ["japan"], privateLocations: ["custom-location"] }')`;
106
+
107
+ write(outer);
108
+ }
package/src/options.ts CHANGED
@@ -24,6 +24,7 @@
24
24
  */
25
25
 
26
26
  import merge from 'deepmerge';
27
+ import { createOption } from 'commander';
27
28
  import { readConfig } from './config';
28
29
  import { getNetworkConditions, DEFAULT_THROTTLING_OPTIONS } from './helpers';
29
30
  import type { CliArgs, RunOptions, ThrottlingOptions } from './common_types';
@@ -136,6 +137,8 @@ export function normalizeOptions(cliArgs: CliArgs): RunOptions {
136
137
 
137
138
  options.schedule = cliArgs.schedule ?? monitor?.schedule;
138
139
  options.locations = cliArgs.locations ?? monitor?.locations;
140
+ options.privateLocations =
141
+ cliArgs.privateLocations ?? monitor?.privateLocations;
139
142
 
140
143
  return options;
141
144
  }
@@ -171,3 +174,38 @@ export function parseThrottling(value: string, prev?: string) {
171
174
  }
172
175
  return JSON.parse(value || prev);
173
176
  }
177
+
178
+ export function getCommonCommandOpts() {
179
+ const params = createOption(
180
+ '-p, --params <jsonstring>',
181
+ 'JSON object that gets injected to all journeys'
182
+ );
183
+ params.argParser(JSON.parse);
184
+
185
+ const playwrightOpts = createOption(
186
+ '--playwright-options <jsonstring>',
187
+ '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.'
188
+ );
189
+ playwrightOpts.argParser(JSON.parse);
190
+
191
+ const pattern = createOption(
192
+ '--pattern <pattern>',
193
+ 'RegExp file patterns to search inside directory'
194
+ );
195
+ const tags = createOption(
196
+ '--tags <name...>',
197
+ 'run only journeys with a tag that matches the glob'
198
+ );
199
+ const match = createOption(
200
+ '--match <name>',
201
+ 'run only journeys with a name or tag that matches the glob'
202
+ );
203
+
204
+ return {
205
+ params,
206
+ playwrightOpts,
207
+ pattern,
208
+ tags,
209
+ match,
210
+ };
211
+ }
@@ -65,12 +65,17 @@ export class Bundler {
65
65
  const archive = archiver('zip', {
66
66
  zlib: { level: 9 },
67
67
  });
68
- archive.on('error', err => reject(err));
68
+ archive.on('error', reject);
69
69
  output.on('close', fulfill);
70
70
  archive.pipe(output);
71
71
  for (const [path, content] of this.moduleMap.entries()) {
72
72
  const relativePath = relativeToCwd(path);
73
- archive.append(content, { name: relativePath });
73
+ // Date is fixed to Unix epoch so the file metadata is
74
+ // not modified everytime when files are bundled
75
+ archive.append(content, {
76
+ name: relativePath,
77
+ date: new Date('1970-01-01'),
78
+ });
74
79
  }
75
80
  archive.finalize();
76
81
  });
package/src/push/index.ts CHANGED
@@ -23,19 +23,43 @@
23
23
  *
24
24
  */
25
25
 
26
+ import { join } from 'path';
27
+ import { existsSync } from 'fs';
28
+ import { readFile, writeFile } from 'fs/promises';
29
+ import { prompt } from 'enquirer';
30
+ import { bold, yellow } from 'kleur/colors';
26
31
  import {
27
- createMonitors,
28
32
  ok,
29
33
  formatAPIError,
30
34
  formatFailedMonitors,
31
35
  formatNotFoundError,
36
+ formatStaleMonitors,
32
37
  } from './request';
38
+ import { buildMonitorSchema, createMonitors } from './monitor';
39
+ import { ProjectSettings } from '../generator';
33
40
  import { Monitor } from '../dsl/monitor';
34
- import { PushOptions } from '../common_types';
35
- import { buildMonitorSchema } from './monitor';
36
- import { progress, error, done } from '../helpers';
41
+ import {
42
+ progress,
43
+ write,
44
+ error,
45
+ SYNTHETICS_PATH,
46
+ aborted,
47
+ indent,
48
+ safeNDJSONParse,
49
+ done,
50
+ } from '../helpers';
51
+ import type { PushOptions } from '../common_types';
37
52
 
38
53
  export async function push(monitors: Monitor[], options: PushOptions) {
54
+ if (monitors.length === 0) {
55
+ throw 'No Monitors found';
56
+ }
57
+
58
+ const duplicates = trackDuplicates(monitors);
59
+ if (duplicates.size > 0) {
60
+ throw error(formatDuplicateError(duplicates));
61
+ }
62
+
39
63
  progress(`preparing all monitors`);
40
64
  const schemas = await buildMonitorSchema(monitors);
41
65
  try {
@@ -48,12 +72,131 @@ export async function push(monitors: Monitor[], options: PushOptions) {
48
72
  const { error, message } = await body.json();
49
73
  throw formatAPIError(statusCode, error, message);
50
74
  }
51
- const { failedMonitors } = await body.json();
52
- if (failedMonitors.length > 0) {
53
- throw formatFailedMonitors(failedMonitors);
75
+ body.setEncoding('utf-8');
76
+ for await (const data of body) {
77
+ // Its kind of hacky for now where Kibana streams the response by
78
+ // writing the data as NDJSON events (data can be interleaved), we
79
+ // distinguish the final data by checking if the event was a progress vs complete event
80
+ const chunks = safeNDJSONParse(data);
81
+ for (const chunk of chunks) {
82
+ if (typeof chunk === 'string') {
83
+ progress(chunk);
84
+ continue;
85
+ }
86
+ const { failedMonitors, failedStaleMonitors } = chunk;
87
+ if (failedMonitors && failedMonitors.length > 0) {
88
+ throw formatFailedMonitors(failedMonitors);
89
+ }
90
+ if (failedStaleMonitors.length > 0) {
91
+ write(yellow(formatStaleMonitors(failedStaleMonitors)));
92
+ }
93
+ }
54
94
  }
55
95
  done('Pushed');
56
96
  } catch (e) {
57
97
  error(e);
58
98
  }
59
99
  }
100
+
101
+ function trackDuplicates(monitors: Monitor[]) {
102
+ const monitorMap: Map<string, Monitor> = new Map();
103
+ const duplicates: Set<Monitor> = new Set();
104
+ for (const monitor of monitors) {
105
+ const id = monitor.config.id;
106
+ if (monitorMap.has(id)) {
107
+ duplicates.add(monitorMap.get(id));
108
+ duplicates.add(monitor);
109
+ }
110
+ monitorMap.set(id, monitor);
111
+ }
112
+ return duplicates;
113
+ }
114
+
115
+ export function formatDuplicateError(monitors: Set<Monitor>) {
116
+ let outer = bold(`Aborted: Duplicate monitors found\n`);
117
+
118
+ let inner = '';
119
+ for (const monitor of monitors) {
120
+ const { config, source } = monitor;
121
+ inner += `* ${config.id} - ${source.file}:${source.line}:${source.column}\n`;
122
+ }
123
+ outer += indent(inner);
124
+ return outer;
125
+ }
126
+
127
+ const PROJECT_SETTINGS_PATH = join(SYNTHETICS_PATH, 'project.json');
128
+ const INSTALLATION_HELP = `Run 'npx @elastic/synthetics init' to create project with default settings.`;
129
+
130
+ export async function loadSettings() {
131
+ if (!existsSync(PROJECT_SETTINGS_PATH)) {
132
+ throw error(`Aborted. Synthetics project not set up.
133
+
134
+ ${INSTALLATION_HELP}`);
135
+ }
136
+
137
+ const contents = await readFile(PROJECT_SETTINGS_PATH, 'utf-8');
138
+ return JSON.parse(contents) as ProjectSettings;
139
+ }
140
+
141
+ export function validateSettings(opts: PushOptions) {
142
+ const INVALID = 'Aborted. Invalid synthetics project settings.';
143
+
144
+ let reason = '';
145
+ if (!opts.project) {
146
+ reason = 'Set project id via CLI as `--project <id>`';
147
+ } else if (!opts.locations && !opts.privateLocations) {
148
+ reason = `Set default location for all monitors via
149
+ - CLI as '--locations <values...> or --privateLocations <values...>'
150
+ - Synthetics config file 'monitors.locations' | 'monitors.privateLocations' field`;
151
+ } else if (!opts.schedule) {
152
+ reason = `Set default schedule in minutes for all monitors via
153
+ - CLI as '--schedule <mins>'
154
+ - Synthetics config file 'monitors.schedule' field`;
155
+ }
156
+
157
+ if (!reason) return;
158
+
159
+ throw error(`${INVALID}
160
+
161
+ ${reason}
162
+
163
+ ${INSTALLATION_HELP}`);
164
+ }
165
+
166
+ async function overrideSettings(project: string) {
167
+ const settings = JSON.parse(await readFile(PROJECT_SETTINGS_PATH, 'utf-8'));
168
+ settings.project = project;
169
+
170
+ await writeFile(
171
+ PROJECT_SETTINGS_PATH,
172
+ JSON.stringify(settings, null, 2),
173
+ 'utf-8'
174
+ );
175
+ }
176
+
177
+ export async function catchIncorrectSettings(
178
+ settings: ProjectSettings,
179
+ options: PushOptions
180
+ ) {
181
+ let override = !settings.project;
182
+ if (settings.project && settings.project !== options.project) {
183
+ if (typeof process.env.TEST_OVERRIDE != null) {
184
+ override = Boolean(process.env.TEST_OVERRIDE);
185
+ } else {
186
+ // Add an extra line to make it easier to read the prompt
187
+ write('');
188
+ ({ override } = await prompt<{ override: boolean }>({
189
+ type: 'confirm',
190
+ name: 'override',
191
+ message: `Monitors were pushed under the '${settings.project}' project. Are you sure you want to push them under the new '${options.project}' (note that this will duplicate the monitors, the old ones being orphaned)`,
192
+ initial: false,
193
+ }));
194
+ }
195
+ if (!override) {
196
+ throw aborted('Push command Aborted');
197
+ }
198
+ }
199
+ if (override) {
200
+ await overrideSettings(options.project);
201
+ }
202
+ }
@@ -23,12 +23,14 @@
23
23
  *
24
24
  */
25
25
 
26
- import { mkdir } from 'fs/promises';
26
+ import { mkdir, rm } from 'fs/promises';
27
27
  import { join } from 'path';
28
28
  import { Bundler } from './bundler';
29
- import { CACHE_PATH } from '../helpers';
30
- import { LocationsMap } from '../dsl/locations';
29
+ import { sendRequest } from './request';
30
+ import { removeTrailingSlash, SYNTHETICS_PATH } from '../helpers';
31
+ import { LocationsMap } from '../locations/public-locations';
31
32
  import { Monitor, MonitorConfig } from '../dsl/monitor';
33
+ import { PushOptions } from '../common_types';
32
34
 
33
35
  export type MonitorSchema = Omit<MonitorConfig, 'locations'> & {
34
36
  content: string;
@@ -36,19 +38,23 @@ export type MonitorSchema = Omit<MonitorConfig, 'locations'> & {
36
38
  filter: Monitor['filter'];
37
39
  };
38
40
 
39
- function translateLocation(locations: MonitorConfig['locations']) {
40
- return locations.map(loc => LocationsMap[loc]).filter(Boolean);
41
+ type APISchema = {
42
+ project: string;
43
+ keep_stale: boolean;
44
+ monitors: MonitorSchema[];
45
+ };
46
+
47
+ function translateLocation(locations?: MonitorConfig['locations']) {
48
+ if (!locations) return [];
49
+ return locations.map(loc => LocationsMap[loc] || loc).filter(Boolean);
41
50
  }
42
51
 
43
52
  export async function buildMonitorSchema(monitors: Monitor[]) {
44
- if (monitors.length == 0) {
45
- throw new Error('No Monitors found');
46
- }
47
53
  /**
48
54
  * Set up the bundle artifacts path which can be used to
49
55
  * create the bundles required for uploading journeys
50
56
  */
51
- const bundlePath = join(CACHE_PATH, 'bundles');
57
+ const bundlePath = join(SYNTHETICS_PATH, 'bundles');
52
58
  await mkdir(bundlePath, { recursive: true });
53
59
  const bundler = new Bundler();
54
60
  const schemas: MonitorSchema[] = [];
@@ -64,5 +70,27 @@ export async function buildMonitorSchema(monitors: Monitor[]) {
64
70
  filter: filter,
65
71
  });
66
72
  }
73
+
74
+ await rm(bundlePath, { recursive: true });
67
75
  return schemas;
68
76
  }
77
+
78
+ export async function createMonitors(
79
+ monitors: MonitorSchema[],
80
+ options: PushOptions
81
+ ) {
82
+ const schema: APISchema = {
83
+ project: options.project,
84
+ keep_stale: false,
85
+ monitors,
86
+ };
87
+
88
+ return await sendRequest({
89
+ url:
90
+ removeTrailingSlash(options.url) +
91
+ `/s/${options.space}/api/synthetics/service/project/monitors`,
92
+ method: 'PUT',
93
+ auth: options.auth,
94
+ body: JSON.stringify(schema),
95
+ });
96
+ }
@@ -24,45 +24,23 @@
24
24
  */
25
25
 
26
26
  import { bold } from 'kleur/colors';
27
- import { request } from 'undici';
28
- import { PushOptions } from '../common_types';
27
+ import { Dispatcher, request } from 'undici';
29
28
  import { indent, symbols } from '../helpers';
30
- import { MonitorSchema } from './monitor';
31
29
 
32
30
  /* eslint-disable @typescript-eslint/no-var-requires */
33
31
  const { version } = require('../../package.json');
34
32
 
35
- export type APISchema = {
36
- project: string;
37
- keep_stale: boolean;
38
- monitors: MonitorSchema[];
33
+ export type APIRequestOptions = {
34
+ url: string;
35
+ method: Dispatcher.HttpMethod;
36
+ auth: string;
37
+ body?: string;
39
38
  };
40
39
 
41
- function getAPIUrl(options: PushOptions) {
42
- // remove trialing / as well
43
- return (
44
- options.url.replace(/\/+$/, '') +
45
- `/s/${options.space}/api/synthetics/service/project/monitors`
46
- );
47
- }
48
-
49
- export function ok(statusCode: number) {
50
- return statusCode >= 200 && statusCode <= 299;
51
- }
52
-
53
- export async function createMonitors(
54
- monitors: MonitorSchema[],
55
- options: PushOptions
56
- ) {
57
- const schema: APISchema = {
58
- project: options.project,
59
- keep_stale: false,
60
- monitors,
61
- };
62
-
63
- return await request(getAPIUrl(options), {
64
- method: 'PUT',
65
- body: JSON.stringify(schema),
40
+ export async function sendRequest(options: APIRequestOptions) {
41
+ return await request(options.url, {
42
+ method: options.method,
43
+ body: options.body,
66
44
  headers: {
67
45
  authorization: `ApiKey ${options.auth}`,
68
46
  'content-type': 'application/json',
@@ -72,6 +50,10 @@ export async function createMonitors(
72
50
  });
73
51
  }
74
52
 
53
+ export function ok(statusCode: number) {
54
+ return statusCode >= 200 && statusCode <= 299;
55
+ }
56
+
75
57
  export type APIMonitorError = {
76
58
  id?: string;
77
59
  reason: string;
@@ -98,8 +80,8 @@ export function formatAPIError(
98
80
  return outer;
99
81
  }
100
82
 
101
- export function formatFailedMonitors(errors: APIMonitorError[]) {
102
- let outer = bold(`${symbols['failed']} Error\n`);
83
+ function formatMonitorError(errors: APIMonitorError[]) {
84
+ let outer = '';
103
85
  for (const error of errors) {
104
86
  const monitorId = error.id ? `: monitor(${error.id})` : '';
105
87
  let inner = bold(`> ${error.reason}${monitorId}\n`);
@@ -109,3 +91,13 @@ export function formatFailedMonitors(errors: APIMonitorError[]) {
109
91
  }
110
92
  return outer;
111
93
  }
94
+
95
+ export function formatFailedMonitors(errors: APIMonitorError[]) {
96
+ const heading = bold(`${symbols['failed']} Error\n`);
97
+ return heading + formatMonitorError(errors);
98
+ }
99
+
100
+ export function formatStaleMonitors(errors: APIMonitorError[]) {
101
+ const heading = bold(`${symbols['warning']} Warnings\n`);
102
+ return heading + formatMonitorError(errors);
103
+ }
@@ -12,7 +12,7 @@ export const loadAppStep = (page: Page, url: string) => {
12
12
 
13
13
  export const addTaskStep = (page: Page, task: string) => {
14
14
  step(`add task ${task}`, async () => {
15
- const input = await page.$('input.new-todo');
15
+ const input = await page.locator('input.new-todo');
16
16
  await input.type(task);
17
17
  await input.press('Enter');
18
18
  });
@@ -21,12 +21,13 @@ export const addTaskStep = (page: Page, task: string) => {
21
21
  const todosSelector = 'ul.todo-list li.todo';
22
22
 
23
23
  export const findTask = async (page: Page, task: string) => {
24
- return await page.waitForSelector(`${todosSelector} >> text="${task}"`);
24
+ return await page.locator(`${todosSelector} >> text="${task}"`);
25
25
  };
26
26
 
27
- export const assertTaskListSizeStep = async (page: Page, size: number) => {
28
- step(`check that task list has exactly ${size} elements`, async () => {
29
- expect((await page.$$(todosSelector)).length).toBe(size);
27
+ export const assertTaskListCountStep = async (page: Page, count: number) => {
28
+ step(`check that task list has exactly ${count} elements`, async () => {
29
+ const tasks = await page.locator(todosSelector);
30
+ expect(await tasks.count()).toBe(count);
30
31
  });
31
32
  };
32
33
 
@@ -40,14 +41,13 @@ export const destroyTaskStep = async (page: Page, task: string) => {
40
41
  step(`destroy task '${task}'`, async () => {
41
42
  const label = await findTask(page, task);
42
43
  // xpath indexes arrays starting at 1!!! Easy to forget!
43
- const li = await label.$('xpath=ancestor::li[1]');
44
- const destroyButton = await li.$('button');
45
-
44
+ const li = await label.locator('xpath=ancestor::li[1]');
45
+ const destroyButton = await li.locator('button');
46
46
  // The destroy button is not visible until hovered. Setup a click test which
47
47
  // will wait up to 30s for the button to be visible.
48
48
  const clickFuture = destroyButton.click();
49
49
  // now hover, making the destroy button clickable
50
- li.hover();
50
+ await li.hover();
51
51
  // now we are done
52
52
  await clickFuture;
53
53
  });