@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.
- package/dist/cli.js +51 -26
- package/dist/cli.js.map +1 -1
- package/dist/common_types.d.ts +2 -0
- package/dist/common_types.d.ts.map +1 -1
- package/dist/core/runner.d.ts.map +1 -1
- package/dist/core/runner.js +4 -3
- package/dist/core/runner.js.map +1 -1
- package/dist/dsl/monitor.d.ts +2 -1
- package/dist/dsl/monitor.d.ts.map +1 -1
- package/dist/dsl/monitor.js +2 -2
- package/dist/dsl/monitor.js.map +1 -1
- package/dist/generator/index.d.ts +11 -3
- package/dist/generator/index.d.ts.map +1 -1
- package/dist/generator/index.js +98 -27
- package/dist/generator/index.js.map +1 -1
- package/dist/generator/utils.d.ts +2 -1
- package/dist/generator/utils.d.ts.map +1 -1
- package/dist/generator/utils.js +27 -4
- package/dist/generator/utils.js.map +1 -1
- package/dist/helpers.d.ts +9 -5
- package/dist/helpers.d.ts.map +1 -1
- package/dist/helpers.js +37 -10
- package/dist/helpers.js.map +1 -1
- package/dist/locations/index.d.ts +45 -0
- package/dist/locations/index.d.ts.map +1 -0
- package/dist/locations/index.js +90 -0
- package/dist/locations/index.js.map +1 -0
- package/dist/{dsl/locations.d.ts → locations/public-locations.d.ts} +1 -1
- package/dist/locations/public-locations.d.ts.map +1 -0
- package/dist/{dsl/locations.js → locations/public-locations.js} +1 -1
- package/dist/locations/public-locations.js.map +1 -0
- package/dist/options.d.ts +7 -0
- package/dist/options.d.ts.map +1 -1
- package/dist/options.js +21 -1
- package/dist/options.js.map +1 -1
- package/dist/push/bundler.d.ts.map +1 -1
- package/dist/push/bundler.js +7 -2
- package/dist/push/bundler.js.map +1 -1
- package/dist/push/index.d.ts +6 -1
- package/dist/push/index.d.ts.map +1 -1
- package/dist/push/index.js +124 -5
- package/dist/push/index.js.map +1 -1
- package/dist/push/monitor.d.ts +2 -0
- package/dist/push/monitor.d.ts.map +1 -1
- package/dist/push/monitor.js +23 -7
- package/dist/push/monitor.js.map +1 -1
- package/dist/push/request.d.ts +8 -7
- package/dist/push/request.d.ts.map +1 -1
- package/dist/push/request.js +21 -22
- package/dist/push/request.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/cli.ts +79 -59
- package/src/common_types.ts +2 -0
- package/src/core/runner.ts +5 -6
- package/src/dsl/monitor.ts +2 -1
- package/src/generator/index.ts +138 -34
- package/src/generator/utils.ts +29 -3
- package/src/helpers.ts +35 -11
- package/src/locations/index.ts +108 -0
- package/src/{dsl/locations.ts → locations/public-locations.ts} +0 -0
- package/src/options.ts +38 -0
- package/src/push/bundler.ts +7 -2
- package/src/push/index.ts +150 -7
- package/src/push/monitor.ts +37 -9
- package/src/push/request.ts +26 -34
- package/templates/journeys/advanced-example-helpers.ts +9 -9
- package/templates/journeys/advanced-example.journey.ts +6 -6
- package/templates/journeys/example.journey.ts +1 -1
- package/templates/synthetics.config.ts +2 -1
- package/dist/dsl/locations.d.ts.map +0 -1
- 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 {
|
|
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
|
|
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
|
|
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(
|
|
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
|
+
}
|
|
File without changes
|
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
|
+
}
|
package/src/push/bundler.ts
CHANGED
|
@@ -65,12 +65,17 @@ export class Bundler {
|
|
|
65
65
|
const archive = archiver('zip', {
|
|
66
66
|
zlib: { level: 9 },
|
|
67
67
|
});
|
|
68
|
-
archive.on('error',
|
|
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
|
-
|
|
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 {
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
+
}
|
package/src/push/monitor.ts
CHANGED
|
@@ -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 {
|
|
30
|
-
import {
|
|
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
|
-
|
|
40
|
-
|
|
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(
|
|
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
|
+
}
|
package/src/push/request.ts
CHANGED
|
@@ -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
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
33
|
+
export type APIRequestOptions = {
|
|
34
|
+
url: string;
|
|
35
|
+
method: Dispatcher.HttpMethod;
|
|
36
|
+
auth: string;
|
|
37
|
+
body?: string;
|
|
39
38
|
};
|
|
40
39
|
|
|
41
|
-
function
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
options.
|
|
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
|
-
|
|
102
|
-
let outer =
|
|
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
|
|
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.
|
|
24
|
+
return await page.locator(`${todosSelector} >> text="${task}"`);
|
|
25
25
|
};
|
|
26
26
|
|
|
27
|
-
export const
|
|
28
|
-
step(`check that task list has exactly ${
|
|
29
|
-
|
|
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
|
|
44
|
-
const destroyButton = await li
|
|
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
|
});
|