@elastic/synthetics 1.0.0-beta.30 → 1.0.0-beta.33

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 (99) hide show
  1. package/dist/cli.js +51 -26
  2. package/dist/cli.js.map +1 -1
  3. package/dist/common_types.d.ts +4 -1
  4. package/dist/common_types.d.ts.map +1 -1
  5. package/dist/core/expect.d.ts +25 -25
  6. package/dist/core/expect.d.ts.map +1 -1
  7. package/dist/core/expect.js.map +1 -1
  8. package/dist/core/gatherer.d.ts.map +1 -1
  9. package/dist/core/gatherer.js +3 -1
  10. package/dist/core/gatherer.js.map +1 -1
  11. package/dist/core/runner.d.ts.map +1 -1
  12. package/dist/core/runner.js +4 -3
  13. package/dist/core/runner.js.map +1 -1
  14. package/dist/dsl/index.js +5 -1
  15. package/dist/dsl/index.js.map +1 -1
  16. package/dist/dsl/journey.d.ts +2 -1
  17. package/dist/dsl/journey.d.ts.map +1 -1
  18. package/dist/dsl/journey.js +5 -1
  19. package/dist/dsl/journey.js.map +1 -1
  20. package/dist/dsl/monitor.d.ts +2 -1
  21. package/dist/dsl/monitor.d.ts.map +1 -1
  22. package/dist/dsl/monitor.js +2 -2
  23. package/dist/dsl/monitor.js.map +1 -1
  24. package/dist/generator/index.d.ts +11 -3
  25. package/dist/generator/index.d.ts.map +1 -1
  26. package/dist/generator/index.js +98 -27
  27. package/dist/generator/index.js.map +1 -1
  28. package/dist/generator/utils.d.ts +2 -1
  29. package/dist/generator/utils.d.ts.map +1 -1
  30. package/dist/generator/utils.js +27 -4
  31. package/dist/generator/utils.js.map +1 -1
  32. package/dist/helpers.d.ts +10 -5
  33. package/dist/helpers.d.ts.map +1 -1
  34. package/dist/helpers.js +41 -10
  35. package/dist/helpers.js.map +1 -1
  36. package/dist/index.d.ts +1 -1
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/loader.js +11 -3
  39. package/dist/loader.js.map +1 -1
  40. package/dist/locations/index.d.ts +45 -0
  41. package/dist/locations/index.d.ts.map +1 -0
  42. package/dist/locations/index.js +90 -0
  43. package/dist/locations/index.js.map +1 -0
  44. package/dist/{dsl/locations.d.ts → locations/public-locations.d.ts} +1 -1
  45. package/dist/locations/public-locations.d.ts.map +1 -0
  46. package/dist/{dsl/locations.js → locations/public-locations.js} +1 -1
  47. package/dist/locations/public-locations.js.map +1 -0
  48. package/dist/options.d.ts +7 -0
  49. package/dist/options.d.ts.map +1 -1
  50. package/dist/options.js +21 -1
  51. package/dist/options.js.map +1 -1
  52. package/dist/plugins/index.js +5 -1
  53. package/dist/plugins/index.js.map +1 -1
  54. package/dist/push/bundler.d.ts.map +1 -1
  55. package/dist/push/bundler.js +12 -3
  56. package/dist/push/bundler.js.map +1 -1
  57. package/dist/push/index.d.ts +12 -1
  58. package/dist/push/index.d.ts.map +1 -1
  59. package/dist/push/index.js +137 -8
  60. package/dist/push/index.js.map +1 -1
  61. package/dist/push/monitor.d.ts +7 -0
  62. package/dist/push/monitor.d.ts.map +1 -1
  63. package/dist/push/monitor.js +23 -7
  64. package/dist/push/monitor.js.map +1 -1
  65. package/dist/push/plugin.d.ts.map +1 -1
  66. package/dist/push/plugin.js +11 -2
  67. package/dist/push/plugin.js.map +1 -1
  68. package/dist/push/request.d.ts +8 -7
  69. package/dist/push/request.d.ts.map +1 -1
  70. package/dist/push/request.js +22 -22
  71. package/dist/push/request.js.map +1 -1
  72. package/dist/tsconfig.tsbuildinfo +1 -1
  73. package/package.json +9 -9
  74. package/src/cli.ts +79 -59
  75. package/src/common_types.ts +4 -0
  76. package/src/core/expect.ts +58 -24
  77. package/src/core/gatherer.ts +9 -2
  78. package/src/core/runner.ts +5 -6
  79. package/src/dsl/journey.ts +8 -1
  80. package/src/dsl/monitor.ts +2 -1
  81. package/src/generator/index.ts +138 -34
  82. package/src/generator/utils.ts +29 -3
  83. package/src/helpers.ts +40 -12
  84. package/src/index.ts +1 -0
  85. package/src/loader.ts +11 -2
  86. package/src/locations/index.ts +108 -0
  87. package/src/{dsl/locations.ts → locations/public-locations.ts} +0 -0
  88. package/src/options.ts +38 -0
  89. package/src/push/bundler.ts +7 -2
  90. package/src/push/index.ts +177 -10
  91. package/src/push/monitor.ts +38 -9
  92. package/src/push/plugin.ts +6 -1
  93. package/src/push/request.ts +27 -34
  94. package/templates/journeys/advanced-example-helpers.ts +9 -9
  95. package/templates/journeys/advanced-example.journey.ts +6 -6
  96. package/templates/journeys/example.journey.ts +1 -1
  97. package/templates/synthetics.config.ts +2 -1
  98. package/dist/dsl/locations.d.ts.map +0 -1
  99. package/dist/dsl/locations.js.map +0 -1
@@ -27,34 +27,56 @@ import { existsSync } from 'fs';
27
27
  import { mkdir, readFile, writeFile } from 'fs/promises';
28
28
  import { bold, cyan, yellow } from 'kleur/colors';
29
29
  import { join, relative, dirname, basename } from 'path';
30
- import { prompt } from 'enquirer';
31
- import { SyntheticsLocations } from '../dsl/monitor';
32
- import { progress, write as stdWrite } from '../helpers';
33
- import { getPackageManager, replaceTemplates, runCommand } from './utils';
30
+ // @ts-ignore-next-line: has no exported member 'Input'
31
+ import { prompt, Input } from 'enquirer';
32
+ import { progress, removeTrailingSlash, write as stdWrite } from '../helpers';
33
+ import {
34
+ getPackageManager,
35
+ replaceTemplates,
36
+ runCommand,
37
+ cloudIDToKibanaURL,
38
+ } from './utils';
39
+ import { formatLocations, getLocations, groupLocations } from '../locations';
34
40
 
35
- // Templates that are required for setting up new
36
- // synthetics project
41
+ // Templates that are required for setting up new synthetics project
37
42
  const templateDir = join(__dirname, '..', '..', 'templates');
38
43
 
39
- type PromptOptions = {
40
- locations: string;
44
+ export type ProjectSettings = {
45
+ url: string;
46
+ space: string;
47
+ project: string;
48
+ };
49
+
50
+ type PromptOptions = ProjectSettings & {
51
+ locations: Array<string>;
52
+ privateLocations: Array<string>;
41
53
  schedule: number;
42
54
  };
43
55
 
44
56
  // exported for testing
45
- export const regularFiles = [
46
- '.github/workflows/run-synthetics.yml',
47
- 'README.md',
57
+ export const REGULAR_FILES_PATH = [
48
58
  'journeys/example.journey.ts',
49
- 'journeys/advanced-example.journey.ts',
50
59
  'journeys/advanced-example-helpers.ts',
60
+ 'journeys/advanced-example.journey.ts',
61
+ '.github/workflows/run-synthetics.yml',
62
+ 'README.md',
51
63
  ];
64
+ export const SETTINGS_PATH = '.synthetics/project.json';
65
+ export const CONFIG_PATH = 'synthetics.config.ts';
66
+
67
+ // Files to be overriden by default if the project is initialized multiple times
68
+ const DEFAULT_OVERRIDES = [SETTINGS_PATH, CONFIG_PATH];
52
69
 
53
70
  export class Generator {
54
71
  pkgManager = 'npm';
55
72
  constructor(public projectDir: string) {}
56
73
 
57
74
  async directory() {
75
+ progress(
76
+ `Initializing Synthetics project in '${
77
+ relative(process.cwd(), this.projectDir) || '.'
78
+ }'`
79
+ );
58
80
  if (!existsSync(this.projectDir)) {
59
81
  await mkdir(this.projectDir);
60
82
  }
@@ -64,12 +86,58 @@ export class Generator {
64
86
  if (process.env.TEST_QUESTIONS) {
65
87
  return JSON.parse(process.env.TEST_QUESTIONS);
66
88
  }
67
- const question = [
89
+
90
+ const { onCloud } = await prompt<{ onCloud: string }>({
91
+ type: 'confirm',
92
+ name: 'onCloud',
93
+ initial: 'y',
94
+ message: 'Do you use Elastic Cloud',
95
+ });
96
+ const url = await new Input({
97
+ header: onCloud
98
+ ? yellow(
99
+ 'Get cloud.id from your deployment https://www.elastic.co/guide/en/cloud/current/ec-cloud-id.html'
100
+ )
101
+ : '',
102
+ message: onCloud
103
+ ? 'What is your cloud.id'
104
+ : 'What is the url of your Kibana instance',
105
+ name: 'url',
106
+ required: true,
107
+ result(value) {
108
+ return onCloud ? cloudIDToKibanaURL(value) : value;
109
+ },
110
+ }).run();
111
+
112
+ const auth = await new Input({
113
+ name: 'auth',
114
+ header: yellow(
115
+ `Generate API key from Kibana ${removeTrailingSlash(
116
+ url
117
+ )}/app/uptime/manage-monitors/all`
118
+ ),
119
+ required: true,
120
+ message: 'What is your API key',
121
+ }).run();
122
+
123
+ const allLocations = await getLocations({ url, auth });
124
+ const locChoices = formatLocations(allLocations);
125
+ if (locChoices.length === 0) {
126
+ // TODO - Add link to docs
127
+ throw 'Follow the docs to set up your first private locations - <link>';
128
+ }
129
+
130
+ const monitorQues = [
68
131
  {
69
132
  type: 'select',
70
133
  name: 'locations',
71
- message: 'Select the default location where you want to run monitors.',
72
- choices: SyntheticsLocations,
134
+ hint: '(Use <space> to select, <return> to submit)',
135
+ message: 'Select the locations where you want to run monitors',
136
+ choices: locChoices,
137
+ multiple: true,
138
+ validate(value) {
139
+ return value.length === 0 ? `Select at least one option.` : true;
140
+ },
73
141
  },
74
142
  {
75
143
  type: 'numeral',
@@ -77,26 +145,50 @@ export class Generator {
77
145
  message: 'Set default schedule in minutes for all monitors',
78
146
  initial: 10,
79
147
  },
148
+ {
149
+ type: 'input',
150
+ name: 'project',
151
+ message: 'Choose project id to logically group monitors',
152
+ initial: basename(this.projectDir),
153
+ },
154
+ {
155
+ type: 'input',
156
+ name: 'space',
157
+ message: 'Choose the target Kibana space',
158
+ initial: 'default',
159
+ },
80
160
  ];
81
- return await prompt<PromptOptions>(question);
161
+
162
+ // Split and group private and public locations from the answered list.
163
+ const answers = await prompt<PromptOptions>(monitorQues);
164
+ const { locations, privateLocations } = groupLocations(answers.locations);
165
+ return { ...answers, url, locations, privateLocations };
82
166
  }
83
167
 
84
168
  async files(answers: PromptOptions) {
85
169
  const fileMap = new Map<string, string>();
170
+
171
+ // Setup project.json
172
+ const projectJSON = {
173
+ url: answers.url,
174
+ space: answers.space,
175
+ project: answers.project,
176
+ };
177
+ fileMap.set(SETTINGS_PATH, JSON.stringify(projectJSON, null, 2) + '\n');
178
+
86
179
  // Setup Synthetics config file
87
- const configFile = 'synthetics.config.ts';
88
180
  fileMap.set(
89
- configFile,
181
+ CONFIG_PATH,
90
182
  replaceTemplates(
91
- await readFile(join(templateDir, configFile), 'utf-8'),
183
+ await readFile(join(templateDir, CONFIG_PATH), 'utf-8'),
92
184
  answers
93
185
  )
94
186
  );
95
187
 
96
188
  // Setup non-templated files
97
189
  Promise.all(
98
- regularFiles.map(async fn => {
99
- fileMap.set(fn, await readFile(join(templateDir, fn), 'utf-8'));
190
+ REGULAR_FILES_PATH.map(async file => {
191
+ fileMap.set(file, await readFile(join(templateDir, file), 'utf-8'));
100
192
  })
101
193
  ).catch(e => {
102
194
  throw e;
@@ -110,20 +202,31 @@ export class Generator {
110
202
 
111
203
  async createFile(relativePath: string, content: string, override = false) {
112
204
  const absolutePath = join(this.projectDir, relativePath);
113
- if (override || !existsSync(absolutePath)) {
114
- progress(`Writing ${relative(process.cwd(), absolutePath)}.`);
115
- await mkdir(dirname(absolutePath), { recursive: true });
116
- await writeFile(absolutePath, content, 'utf-8');
205
+
206
+ if (
207
+ !override &&
208
+ !DEFAULT_OVERRIDES.includes(relativePath) &&
209
+ existsSync(absolutePath)
210
+ ) {
211
+ const { override } = await prompt<{ override: boolean }>({
212
+ type: 'confirm',
213
+ name: 'override',
214
+ message: `File ${relativePath} already exists. Override it?`,
215
+ initial: false,
216
+ });
217
+ if (!override) return;
117
218
  }
219
+
220
+ progress(`Writing ${relative(process.cwd(), absolutePath)}.`);
221
+ await mkdir(dirname(absolutePath), { recursive: true });
222
+ await writeFile(absolutePath, content, 'utf-8');
118
223
  }
119
224
 
120
225
  async package() {
121
226
  this.pkgManager = await getPackageManager(this.projectDir);
122
227
  const commands = new Map<string, string>();
123
228
  commands.set(
124
- `Initializing Synthetics project using ${
125
- this.pkgManager == 'yarn' ? 'Yarn' : 'NPM'
126
- }`,
229
+ `Setting up project using ${this.pkgManager == 'yarn' ? 'Yarn' : 'NPM'}`,
127
230
  this.pkgManager == 'yarn' ? 'yarn init -y' : 'npm init -y'
128
231
  );
129
232
 
@@ -131,8 +234,8 @@ export class Generator {
131
234
  commands.set(
132
235
  `Installing @elastic/synthetics library`,
133
236
  this.pkgManager == 'yarn'
134
- ? `yarn add -dev ${pkgName}`
135
- : `npm i -d ${pkgName}`
237
+ ? `yarn add -dev ${pkgName} --silent`
238
+ : `npm i -d ${pkgName} --quiet`
136
239
  );
137
240
 
138
241
  // Execute commands
@@ -158,8 +261,7 @@ export class Generator {
158
261
  pkgJSON.scripts.test = 'npx @elastic/synthetics journeys';
159
262
 
160
263
  // Add push command
161
- const project = basename(this.projectDir);
162
- pkgJSON.scripts.push = `npx @elastic/synthetics push journeys --project ${project} --url http://localhost:5601 --auth apiKey`;
264
+ pkgJSON.scripts.push = `npx @elastic/synthetics push --auth $SYNTHETICS_API_KEY`;
163
265
 
164
266
  await this.createFile(
165
267
  filename,
@@ -189,10 +291,12 @@ All set, you can run below commands inside: ${this.projectDir}:
189
291
 
190
292
  Run synthetic tests: ${cyan(runCommand(this.pkgManager, 'test'))}
191
293
 
192
- Push monitors to Kibana: ${cyan(runCommand(this.pkgManager, 'push'))}
294
+ Push monitors to Kibana: ${cyan(
295
+ 'SYNTHETICS_API_KEY=<value> ' + runCommand(this.pkgManager, 'push')
296
+ )}
193
297
 
194
298
  ${yellow(
195
- 'Make sure to update the Kibana url and api keys before pushing monitors to Kibana.'
299
+ 'Make sure to configure the SYNTHETICS_API_KEY before pushing monitors to Kibana.'
196
300
  )}
197
301
 
198
302
  Visit https://www.elastic.co/guide/en/observability/current/synthetic-run-tests.html to learn more.
@@ -40,10 +40,15 @@ export function runCommand(pkgManager: string, command: string) {
40
40
  return `npm run ${command}`;
41
41
  }
42
42
 
43
- export function replaceTemplates(input: string, literals: Record<string, any>) {
44
- for (const key in literals) {
45
- const finalValue = literals[key];
43
+ export function replaceTemplates(input: string, values: Record<string, any>) {
44
+ for (const key in values) {
45
+ const finalValue = values[key];
46
46
  input = input.replace(new RegExp(`'{{` + key + `}}'`, 'g'), () => {
47
+ if (Array.isArray(finalValue)) {
48
+ return String(finalValue)
49
+ .split(',')
50
+ .map(f => `'${f}'`);
51
+ }
47
52
  if (typeof finalValue == 'number') {
48
53
  return Number(finalValue);
49
54
  }
@@ -52,3 +57,24 @@ export function replaceTemplates(input: string, literals: Record<string, any>) {
52
57
  }
53
58
  return input;
54
59
  }
60
+
61
+ export function cloudIDToKibanaURL(id: string) {
62
+ const ID_SEPARATOR = ':';
63
+ const VALUE_SEPARATOR = '$';
64
+ // Ignore values before the separator
65
+ const index = id.lastIndexOf(ID_SEPARATOR);
66
+ if (index >= 0) {
67
+ id = id.substring(index + 1);
68
+ }
69
+
70
+ // Decode the base64 string
71
+ const decoded = Buffer.from(id, 'base64').toString();
72
+ const words = decoded.split(VALUE_SEPARATOR);
73
+
74
+ // construct the kibana and ES urls from decoded value
75
+ const [host, port = '443'] = words[0].split(ID_SEPARATOR);
76
+ const [kibanaId, kibanaPort = port] = words[2].split(ID_SEPARATOR);
77
+ const kibanaURL = `https://${kibanaId}.${host}:${kibanaPort}`;
78
+
79
+ return kibanaURL;
80
+ }
package/src/helpers.ts CHANGED
@@ -23,11 +23,11 @@
23
23
  *
24
24
  */
25
25
 
26
- import { red, green, yellow, cyan, bold } from 'kleur/colors';
26
+ import { red, green, yellow, cyan, bold, grey } 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');
@@ -331,6 +347,10 @@ export function progress(message: string) {
331
347
  write(cyan(bold(`${symbols.progress} ${message}`)));
332
348
  }
333
349
 
350
+ export function apiProgress(message: string) {
351
+ write(grey(`> ${message}`));
352
+ }
353
+
334
354
  export function error(message: string) {
335
355
  write(red(message));
336
356
  }
@@ -338,3 +358,11 @@ export function error(message: string) {
338
358
  export function done(message: string) {
339
359
  write(bold(green(`${symbols['succeeded']} ${message}`)));
340
360
  }
361
+
362
+ export function aborted(message: string) {
363
+ write(bold(yellow(`${symbols['warning']} ${message}`)));
364
+ }
365
+
366
+ export function removeTrailingSlash(url: string) {
367
+ return url.replace(/\/+$/, '');
368
+ }
package/src/index.ts CHANGED
@@ -52,6 +52,7 @@ export type {
52
52
  ChromiumBrowser,
53
53
  ChromiumBrowserContext,
54
54
  CDPSession,
55
+ APIRequestContext,
55
56
  } from 'playwright-chromium';
56
57
 
57
58
  /**
package/src/loader.ts CHANGED
@@ -93,10 +93,19 @@ const loadInlineScript = source => {
93
93
  'browser',
94
94
  'params',
95
95
  'expect',
96
+ 'request',
96
97
  source
97
98
  );
98
- journey('inline', ({ page, context, browser, params }) => {
99
- scriptFn.apply(null, [step, page, context, browser, params, expect]);
99
+ journey('inline', ({ page, context, browser, params, request }) => {
100
+ scriptFn.apply(null, [
101
+ step,
102
+ page,
103
+ context,
104
+ browser,
105
+ params,
106
+ expect,
107
+ request,
108
+ ]);
100
109
  });
101
110
  };
102
111
 
@@ -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
  });