@elastic/synthetics 1.24.3 → 1.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/README.md +67 -0
  2. package/dist/common_types.d.ts +17 -0
  3. package/dist/common_types.d.ts.map +1 -1
  4. package/dist/core/gatherer.d.ts +4 -4
  5. package/dist/core/gatherer.d.ts.map +1 -1
  6. package/dist/core/gatherer.js +28 -7
  7. package/dist/core/gatherer.js.map +1 -1
  8. package/dist/core/globals.js +1 -1
  9. package/dist/core/index.d.ts +2 -1
  10. package/dist/core/index.d.ts.map +1 -1
  11. package/dist/core/index.js +16 -1
  12. package/dist/core/index.js.map +1 -1
  13. package/dist/core/runner.d.ts +1 -1
  14. package/dist/core/runner.d.ts.map +1 -1
  15. package/dist/core/runner.js +86 -58
  16. package/dist/core/runner.js.map +1 -1
  17. package/dist/dsl/api-journey.d.ts +62 -0
  18. package/dist/dsl/api-journey.d.ts.map +1 -0
  19. package/dist/dsl/api-journey.js +55 -0
  20. package/dist/dsl/api-journey.js.map +1 -0
  21. package/dist/dsl/index.d.ts +1 -0
  22. package/dist/dsl/index.d.ts.map +1 -1
  23. package/dist/dsl/index.js +1 -0
  24. package/dist/dsl/index.js.map +1 -1
  25. package/dist/dsl/journey.d.ts +10 -2
  26. package/dist/dsl/journey.d.ts.map +1 -1
  27. package/dist/dsl/journey.js +11 -2
  28. package/dist/dsl/journey.js.map +1 -1
  29. package/dist/formatter/codegen.d.ts +69 -0
  30. package/dist/formatter/codegen.d.ts.map +1 -0
  31. package/dist/formatter/codegen.js +210 -0
  32. package/dist/formatter/codegen.js.map +1 -0
  33. package/dist/formatter/javascript.d.ts +6 -6
  34. package/dist/formatter/javascript.d.ts.map +1 -1
  35. package/dist/formatter/javascript.js +7 -7
  36. package/dist/formatter/javascript.js.map +1 -1
  37. package/dist/index.d.ts +2 -2
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +2 -1
  40. package/dist/index.js.map +1 -1
  41. package/dist/loader.d.ts.map +1 -1
  42. package/dist/loader.js +66 -1
  43. package/dist/loader.js.map +1 -1
  44. package/dist/network-timings.d.ts +55 -0
  45. package/dist/network-timings.d.ts.map +1 -0
  46. package/dist/network-timings.js +107 -0
  47. package/dist/network-timings.js.map +1 -0
  48. package/dist/plugins/api-network.d.ts +39 -0
  49. package/dist/plugins/api-network.d.ts.map +1 -0
  50. package/dist/plugins/api-network.js +235 -0
  51. package/dist/plugins/api-network.js.map +1 -0
  52. package/dist/plugins/network.d.ts +0 -6
  53. package/dist/plugins/network.d.ts.map +1 -1
  54. package/dist/plugins/network.js +5 -70
  55. package/dist/plugins/network.js.map +1 -1
  56. package/dist/plugins/plugin-manager.d.ts +11 -4
  57. package/dist/plugins/plugin-manager.d.ts.map +1 -1
  58. package/dist/plugins/plugin-manager.js +36 -16
  59. package/dist/plugins/plugin-manager.js.map +1 -1
  60. package/dist/push/index.d.ts.map +1 -1
  61. package/dist/push/index.js +4 -0
  62. package/dist/push/index.js.map +1 -1
  63. package/dist/push/monitor.d.ts.map +1 -1
  64. package/dist/push/monitor.js +1 -1
  65. package/dist/push/monitor.js.map +1 -1
  66. package/dist/push/run-local.d.ts.map +1 -1
  67. package/dist/push/run-local.js +2 -1
  68. package/dist/push/run-local.js.map +1 -1
  69. package/dist/push/utils.d.ts +7 -0
  70. package/dist/push/utils.d.ts.map +1 -1
  71. package/dist/push/utils.js +15 -1
  72. package/dist/push/utils.js.map +1 -1
  73. package/dist/reporters/base.d.ts +2 -2
  74. package/dist/reporters/base.d.ts.map +1 -1
  75. package/dist/reporters/base.js.map +1 -1
  76. package/dist/reporters/index.d.ts +2 -2
  77. package/dist/reporters/index.d.ts.map +1 -1
  78. package/dist/reporters/index.js.map +1 -1
  79. package/dist/reporters/json.d.ts +6 -2
  80. package/dist/reporters/json.d.ts.map +1 -1
  81. package/dist/reporters/json.js +68 -33
  82. package/dist/reporters/json.js.map +1 -1
  83. package/package.json +7 -7
  84. package/src/common_types.ts +22 -0
  85. package/src/core/gatherer.ts +41 -10
  86. package/src/core/globals.ts +1 -1
  87. package/src/core/index.ts +28 -0
  88. package/src/core/runner.ts +93 -67
  89. package/src/dsl/api-journey.ts +93 -0
  90. package/src/dsl/index.ts +1 -0
  91. package/src/dsl/journey.ts +21 -9
  92. package/src/formatter/codegen.ts +299 -0
  93. package/src/formatter/javascript.ts +1 -4
  94. package/src/index.ts +6 -0
  95. package/src/loader.ts +78 -2
  96. package/src/network-timings.ts +130 -0
  97. package/src/plugins/api-network.ts +252 -0
  98. package/src/plugins/network.ts +5 -87
  99. package/src/plugins/plugin-manager.ts +60 -19
  100. package/src/push/index.ts +11 -0
  101. package/src/push/monitor.ts +6 -2
  102. package/src/push/run-local.ts +2 -1
  103. package/src/push/utils.ts +15 -0
  104. package/src/reporters/base.ts +2 -1
  105. package/src/reporters/index.ts +2 -1
  106. package/src/reporters/json.ts +74 -41
  107. package/templates/README.md +3 -1
  108. package/templates/journeys/advanced-api-example-helpers.ts +79 -0
  109. package/templates/journeys/advanced-api-example.journey.ts +52 -0
  110. package/templates/journeys/api-example.journey.ts +35 -0
  111. package/templates/synthetics.config.ts +2 -0
@@ -32,7 +32,12 @@ import {
32
32
  } from 'playwright-core';
33
33
  import { PluginManager } from '../plugins';
34
34
  import { log } from './logger';
35
- import { Driver, NetworkConditions, RunOptions } from '../common_types';
35
+ import {
36
+ APIDriver,
37
+ Driver,
38
+ NetworkConditions,
39
+ RunOptions,
40
+ } from '../common_types';
36
41
 
37
42
  // Default timeout for Playwright actions and Navigations
38
43
  const DEFAULT_TIMEOUT = 50000;
@@ -71,9 +76,19 @@ export class Gatherer {
71
76
  });
72
77
  }
73
78
 
74
- static async setupDriver(options: RunOptions): Promise<Driver> {
75
- await Gatherer.launchBrowser(options);
79
+ static async setupDriver<T extends 'browser' | 'api' = 'browser'>(
80
+ options: RunOptions,
81
+ journeyType: T = 'browser' as T
82
+ ): Promise<T extends 'api' ? APIDriver : Driver> {
76
83
  const { playwrightOptions } = options;
84
+
85
+ if (journeyType === 'api') {
86
+ const request = await apiRequest.newContext({ ...playwrightOptions });
87
+ return { request } as T extends 'api' ? APIDriver : Driver;
88
+ }
89
+
90
+ await Gatherer.launchBrowser(options);
91
+
77
92
  const context = await Gatherer.browser.newContext({
78
93
  ...playwrightOptions,
79
94
  userAgent: await Gatherer.getUserAgent(playwrightOptions?.userAgent),
@@ -96,7 +111,13 @@ export class Gatherer {
96
111
  const page = await context.newPage();
97
112
  const client = await context.newCDPSession(page);
98
113
  const request = await apiRequest.newContext({ ...playwrightOptions });
99
- return { browser: Gatherer.browser, context, page, client, request };
114
+ return {
115
+ browser: Gatherer.browser,
116
+ context,
117
+ page,
118
+ client,
119
+ request,
120
+ } as T extends 'api' ? APIDriver : Driver;
100
121
  }
101
122
 
102
123
  static async getUserAgent(userAgent?: string) {
@@ -136,14 +157,22 @@ export class Gatherer {
136
157
  * Starts recording all events related to the v8 devtools protocol
137
158
  * https://chromedevtools.github.io/devtools-protocol/v8/
138
159
  */
139
- static async beginRecording(driver: Driver, options: RunOptions) {
160
+ static async beginRecording(driver: Driver | APIDriver, options: RunOptions) {
140
161
  log('Gatherer: started recording');
141
162
  const { network, metrics } = options;
142
163
  Gatherer.pluginManager = new PluginManager(driver);
143
164
  Gatherer.pluginManager.registerAll(options);
144
- const plugins = [await Gatherer.pluginManager.start('browserconsole')];
145
- network && plugins.push(await Gatherer.pluginManager.start('network'));
146
- metrics && plugins.push(await Gatherer.pluginManager.start('performance'));
165
+ const plugins = [];
166
+ if ('browser' in driver) {
167
+ plugins.push(await Gatherer.pluginManager.start('browserconsole'));
168
+ network && plugins.push(await Gatherer.pluginManager.start('network'));
169
+ metrics &&
170
+ plugins.push(await Gatherer.pluginManager.start('performance'));
171
+ } else {
172
+ // Network is the only data source for API journeys, so always capture
173
+ // it regardless of `options.network`.
174
+ plugins.push(await Gatherer.pluginManager.start('network'));
175
+ }
147
176
  await Promise.all(plugins);
148
177
  return Gatherer.pluginManager;
149
178
  }
@@ -153,10 +182,12 @@ export class Gatherer {
153
182
  await Gatherer.pluginManager.unregisterAll();
154
183
  }
155
184
 
156
- static async dispose(driver: Driver) {
185
+ static async dispose(driver: Driver | APIDriver) {
157
186
  log(`Gatherer: closing all contexts`);
158
187
  await driver.request.dispose();
159
- await driver.context.close();
188
+ if ('context' in driver) {
189
+ await driver.context.close();
190
+ }
160
191
  }
161
192
 
162
193
  static async stop() {
@@ -26,7 +26,7 @@
26
26
  import Runner from './runner';
27
27
 
28
28
  /**
29
- * Use a gloabl Runner which would be accessed by the runtime and
29
+ * Use a global Runner which would be accessed by the runtime and
30
30
  * required to handle the local vs global invocation through CLI
31
31
  */
32
32
  const SYNTHETICS_RUNNER = Symbol.for('SYNTHETICS_RUNNER');
package/src/core/index.ts CHANGED
@@ -24,6 +24,10 @@
24
24
  */
25
25
 
26
26
  import {
27
+ APIJourney,
28
+ APIJourneyCallback,
29
+ APIJourneyOptions,
30
+ APIJourneyWithAnnotations,
27
31
  Journey,
28
32
  JourneyCallback,
29
33
  JourneyOptions,
@@ -60,6 +64,30 @@ export const journey = createJourney() as JourneyWithAnnotations;
60
64
  journey.skip = createJourney('skip');
61
65
  journey.only = createJourney('only');
62
66
 
67
+ const createAPIJourney = (type?: 'skip' | 'only') =>
68
+ wrapFnWithLocation(
69
+ (
70
+ location: Location,
71
+ options: APIJourneyOptions | string,
72
+ callback: APIJourneyCallback
73
+ ) => {
74
+ log(`API Journey register: ${JSON.stringify(options)}`);
75
+ if (typeof options === 'string') {
76
+ options = { name: options, id: options };
77
+ }
78
+ const j = new APIJourney(options, callback, location);
79
+ if (type) {
80
+ j[type] = true;
81
+ }
82
+ runner._addJourney(j);
83
+ return j;
84
+ }
85
+ );
86
+
87
+ export const apiJourney = createAPIJourney() as APIJourneyWithAnnotations;
88
+ apiJourney.skip = createAPIJourney('skip');
89
+ apiJourney.only = createAPIJourney('only');
90
+
63
91
  const createStep = (type?: 'skip' | 'soft' | 'only') =>
64
92
  wrapFnWithLocation(
65
93
  (location: Location, name: string, callback: VoidCallback) => {
@@ -25,7 +25,7 @@
25
25
 
26
26
  import { join } from 'path';
27
27
  import { mkdir, rm, writeFile } from 'fs/promises';
28
- import { Journey } from '../dsl/journey';
28
+ import { Journey, JourneyCallbackOpts } from '../dsl/journey';
29
29
  import { Step } from '../dsl/step';
30
30
  import { reporters, Reporter } from '../reporters';
31
31
  import {
@@ -44,6 +44,7 @@ import {
44
44
  JourneyResult,
45
45
  StepResult,
46
46
  PushOptions,
47
+ APIDriver,
47
48
  } from '../common_types';
48
49
  import { PerformanceManager, filterBrowserMessages } from '../plugins';
49
50
  import { Gatherer } from './gatherer';
@@ -66,7 +67,7 @@ export interface RunnerInfo {
66
67
  */
67
68
  readonly currentJourney: Journey | undefined;
68
69
  /**
69
- * All registerd journeys
70
+ * All registered journeys
70
71
  */
71
72
  readonly journeys: Journey[];
72
73
  }
@@ -78,7 +79,7 @@ export default class Runner implements RunnerInfo {
78
79
  #journeys: Journey[] = [];
79
80
  #hooks: SuiteHooks = { beforeAll: [], afterAll: [] };
80
81
  #screenshotPath = join(CACHE_PATH, 'screenshots');
81
- #driver?: Driver;
82
+ #driver?: Driver | APIDriver;
82
83
  #browserDelay = -1;
83
84
  #hookError: Error | undefined;
84
85
  #monitor?: Monitor;
@@ -210,18 +211,22 @@ export default class Runner implements RunnerInfo {
210
211
  async #runStep(step: Step, options: RunOptions): Promise<StepResult> {
211
212
  log(`Runner: start step (${step.name})`);
212
213
  const { metrics, screenshots, filmstrips, trace } = options;
213
- /**
214
- * URL needs to be the first navigation request of any step
215
- * Listening for request solves the case where `about:blank` would be
216
- * reported for failed navigations
217
- */
218
- const captureUrl = req => {
219
- if (!step.url && req.isNavigationRequest()) {
220
- step.url = req.url();
221
- }
222
- this.#driver.context.off('request', captureUrl);
223
- };
224
- this.#driver.context.on('request', captureUrl);
214
+
215
+ if ('context' in this.#driver) {
216
+ /**
217
+ * URL needs to be the first navigation request of any step
218
+ * Listening for request solves the case where `about:blank` would be
219
+ * reported for failed navigations
220
+ */
221
+ const captureUrl = req => {
222
+ if (!step.url && req.isNavigationRequest()) {
223
+ step.url = req.url();
224
+ }
225
+ 'context' in this.#driver &&
226
+ this.#driver.context.off('request', captureUrl);
227
+ };
228
+ this.#driver.context.on('request', captureUrl);
229
+ }
225
230
 
226
231
  const data: StepResult = {};
227
232
  const traceEnabled = trace || filmstrips;
@@ -240,30 +245,32 @@ export default class Runner implements RunnerInfo {
240
245
  step.status = 'failed';
241
246
  step.error = error;
242
247
  } finally {
243
- /**
244
- * Collect all step level metrics and trace events
245
- */
246
- if (metrics) {
247
- data.pagemetrics = await (
248
- Gatherer.pluginManager.get('performance') as PerformanceManager
249
- ).getMetrics();
250
- }
251
- if (traceEnabled) {
252
- const traceOutput = await Gatherer.pluginManager.stop('trace');
253
- Object.assign(data, traceOutput);
254
- }
255
- /**
256
- * Capture screenshot for the newly created pages
257
- * via popup or new windows/tabs
258
- *
259
- * Last open page will get us the correct screenshot
260
- */
261
- const pages = this.#driver.context.pages();
262
- const page = pages[pages.length - 1];
263
- if (page) {
264
- step.url ??= page.url();
265
- if (screenshots && screenshots !== 'off') {
266
- await this.captureScreenshot(page, step);
248
+ if ('context' in this.#driver) {
249
+ /**
250
+ * Collect all step level metrics and trace events
251
+ */
252
+ if (metrics) {
253
+ data.pagemetrics = await (
254
+ Gatherer.pluginManager.get('performance') as PerformanceManager
255
+ ).getMetrics();
256
+ }
257
+ if (traceEnabled) {
258
+ const traceOutput = await Gatherer.pluginManager.stop('trace');
259
+ Object.assign(data, traceOutput);
260
+ }
261
+ /**
262
+ * Capture screenshot for the newly created pages
263
+ * via popup or new windows/tabs
264
+ *
265
+ * Last open page will get us the correct screenshot
266
+ */
267
+ const pages = this.#driver.context.pages();
268
+ const page = pages[pages.length - 1];
269
+ if (page) {
270
+ step.url ??= page.url();
271
+ if (screenshots && screenshots !== 'off') {
272
+ await this.captureScreenshot(page, step);
273
+ }
267
274
  }
268
275
  }
269
276
  }
@@ -310,22 +317,24 @@ export default class Runner implements RunnerInfo {
310
317
 
311
318
  async #startJourney(journey: Journey, options: RunOptions) {
312
319
  journey._startTime = monotonicTimeInSeconds();
313
- this.#driver = await Gatherer.setupDriver(options);
320
+ this.#driver = await Gatherer.setupDriver(options, journey.type);
314
321
  await Gatherer.beginRecording(this.#driver, options);
315
- /**
316
- * For each journey we create the screenshots folder for
317
- * caching all screenshots and clear them at end of each journey
318
- */
319
- await mkdir(this.#screenshotPath, { recursive: true });
322
+ if (journey.type === 'browser') {
323
+ /**
324
+ * For each journey we create the screenshots folder for
325
+ * caching all screenshots and clear them at end of each journey
326
+ */
327
+ await mkdir(this.#screenshotPath, { recursive: true });
328
+ }
320
329
  const params = options.params;
321
330
  this.#reporter?.onJourneyStart?.(journey, {
322
331
  timestamp: getTimestamp(),
323
332
  params,
324
333
  });
325
334
  /**
326
- * Exeucute the journey callback which registers the steps for current journey
335
+ * Execute the journey callback which registers the steps for current journey
327
336
  */
328
- journey.cb({ ...this.#driver, params, info: this });
337
+ journey.cb({ ...this.#driver, params, info: this } as JourneyCallbackOpts);
329
338
  }
330
339
 
331
340
  async #endJourney(
@@ -335,24 +344,34 @@ export default class Runner implements RunnerInfo {
335
344
  ) {
336
345
  // Enhance the journey results
337
346
  const pOutput = await Gatherer.pluginManager.output();
338
- const bConsole = filterBrowserMessages(
339
- pOutput.browserconsole,
340
- journey.status
341
- );
342
- await this.#reporter?.onJourneyEnd?.(journey, {
343
- browserDelay: this.#browserDelay,
344
- timestamp: getTimestamp(),
345
- options,
346
- networkinfo: pOutput.networkinfo,
347
- browserconsole: bConsole,
348
- });
347
+ const isBrowserJourney = journey.type === 'browser';
348
+ const bConsole = isBrowserJourney
349
+ ? filterBrowserMessages(pOutput.browserconsole, journey.status)
350
+ : undefined;
351
+ if (isBrowserJourney) {
352
+ await this.#reporter?.onJourneyEnd?.(journey, {
353
+ browserDelay: this.#browserDelay,
354
+ timestamp: getTimestamp(),
355
+ options,
356
+ networkinfo: pOutput.networkinfo,
357
+ browserconsole: bConsole,
358
+ });
359
+ } else {
360
+ await this.#reporter?.onJourneyEnd?.(journey, {
361
+ timestamp: getTimestamp(),
362
+ options,
363
+ networkinfo: pOutput.networkinfo,
364
+ });
365
+ }
349
366
  await Gatherer.endRecording();
350
367
  await Gatherer.dispose(this.#driver);
351
368
  // clear screenshots cache after each journey
352
- await rm(this.#screenshotPath, { recursive: true, force: true });
369
+ if (isBrowserJourney) {
370
+ await rm(this.#screenshotPath, { recursive: true, force: true });
371
+ }
353
372
  return Object.assign(result, {
354
373
  networkinfo: pOutput.networkinfo,
355
- browserconsole: bConsole,
374
+ ...(isBrowserJourney ? { browserconsole: bConsole } : {}),
356
375
  ...journey,
357
376
  });
358
377
  }
@@ -516,9 +535,9 @@ export default class Runner implements RunnerInfo {
516
535
  const { dryRun, grepOpts } = options;
517
536
 
518
537
  // collect all journeys with `.only` annotation and skip the rest
519
- const onlyJournerys = this.#journeys.filter(j => j.only);
520
- if (onlyJournerys.length > 0) {
521
- this.#journeys = onlyJournerys;
538
+ const onlyJourneys = this.#journeys.filter(j => j.only);
539
+ if (onlyJourneys.length > 0) {
540
+ this.#journeys = onlyJourneys;
522
541
  } else {
523
542
  // filter journeys based on tags and skip annotations
524
543
  this.#journeys = this.#journeys.filter(
@@ -551,9 +570,16 @@ export default class Runner implements RunnerInfo {
551
570
 
552
571
  async _runJourneys(options: RunOptions) {
553
572
  const result: RunResult = {};
554
- const browserStart = monotonicTimeInSeconds();
555
- await Gatherer.launchBrowser(options);
556
- this.#browserDelay = monotonicTimeInSeconds() - browserStart;
573
+ /**
574
+ * Only spin up a Chromium browser if at least one browser journey is
575
+ * scheduled to run. Pure API-journey suites skip launch entirely.
576
+ */
577
+ const hasBrowserJourneys = this.#journeys.some(j => j.type === 'browser');
578
+ if (hasBrowserJourneys) {
579
+ const browserStart = monotonicTimeInSeconds();
580
+ await Gatherer.launchBrowser(options);
581
+ this.#browserDelay = monotonicTimeInSeconds() - browserStart;
582
+ }
557
583
 
558
584
  for (const journey of this.#journeys) {
559
585
  const journeyResult: JourneyResult = this.#hookError
@@ -578,7 +604,7 @@ export default class Runner implements RunnerInfo {
578
604
  this.#journeys = [];
579
605
  this.#active = false;
580
606
  /**
581
- * Clear all cache data stored for post processing by
607
+ * Clear all cache data stored for post-processing by
582
608
  * the current synthetic agent run
583
609
  */
584
610
  await rm(CACHE_PATH, { recursive: true, force: true });
@@ -0,0 +1,93 @@
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 { APIRequestContext } from 'playwright-core';
27
+ import { Location, Params } from '../common_types';
28
+
29
+ import { Journey } from './journey';
30
+ import { Monitor, MonitorConfig } from './monitor';
31
+ import { RunnerInfo } from '../core/runner';
32
+
33
+ export type APIJourneyOptions = {
34
+ name: string;
35
+ id?: string;
36
+ tags?: string[];
37
+ };
38
+
39
+ export type APIJourneyCallbackOpts = {
40
+ params: Params;
41
+ request: APIRequestContext;
42
+ info: RunnerInfo;
43
+ };
44
+ export type APIJourneyCallback = (options: APIJourneyCallbackOpts) => void;
45
+
46
+ type APIJourneyType = (
47
+ options: string | APIJourneyOptions,
48
+ callback: APIJourneyCallback
49
+ ) => APIJourney;
50
+
51
+ /**
52
+ * Same step-based model as a browser `Journey` but without a Chromium
53
+ * context. `type === 'api'` lets the runner/gatherer/plugin-manager skip
54
+ * browser-only setup.
55
+ */
56
+ export class APIJourney extends Journey {
57
+ constructor(
58
+ options: APIJourneyOptions | string,
59
+ cb: APIJourneyCallback,
60
+ location?: Location
61
+ ) {
62
+ const opts =
63
+ typeof options === 'string'
64
+ ? { name: options, id: options }
65
+ : { ...options };
66
+ // Runner narrows callback args by `journey.type`, so the cast is sound.
67
+ super({ ...opts, type: 'api' }, cb as any, location);
68
+ }
69
+
70
+ // Register as an `api` project monitor on `synthetics push`.
71
+ override _updateMonitor(config: MonitorConfig) {
72
+ this._setMonitor(
73
+ new Monitor({
74
+ name: this.name,
75
+ id: this.id,
76
+ type: 'api',
77
+ tags: this.tags ?? [],
78
+ ...config,
79
+ })
80
+ );
81
+ }
82
+ }
83
+
84
+ export type APIJourneyWithAnnotations = APIJourneyType & {
85
+ /**
86
+ * Skip this journey and all its steps
87
+ */
88
+ skip: APIJourneyType;
89
+ /**
90
+ * Run only this journey and skip rest of the journeys
91
+ */
92
+ only: APIJourneyType;
93
+ };
package/src/dsl/index.ts CHANGED
@@ -24,4 +24,5 @@
24
24
  */
25
25
 
26
26
  export * from './journey';
27
+ export * from './api-journey';
27
28
  export * from './step';
@@ -50,7 +50,7 @@ export type JourneyOptions = {
50
50
 
51
51
  type HookType = 'before' | 'after';
52
52
  export type Hooks = Record<HookType, Array<HooksCallback>>;
53
- type JourneyCallbackOpts = {
53
+ export type JourneyCallbackOpts = {
54
54
  page: Page;
55
55
  context: BrowserContext;
56
56
  browser: Browser;
@@ -62,6 +62,7 @@ type JourneyCallbackOpts = {
62
62
  export type JourneyCallback = (options: JourneyCallbackOpts) => void;
63
63
 
64
64
  export class Journey {
65
+ readonly type: 'browser' | 'api' = 'browser';
65
66
  readonly name: string;
66
67
  readonly id?: string;
67
68
  readonly tags?: string[];
@@ -78,10 +79,11 @@ export class Journey {
78
79
  error?: Error;
79
80
 
80
81
  constructor(
81
- options: JourneyOptions,
82
+ options: JourneyOptions & { type?: 'browser' | 'api' },
82
83
  cb: JourneyCallback,
83
84
  location?: Location
84
85
  ) {
86
+ this.type = options.type ?? 'browser';
85
87
  this.name = options.name;
86
88
  this.id = options.id || options.name;
87
89
  this.tags = options.tags;
@@ -144,13 +146,23 @@ export class Journey {
144
146
  /**
145
147
  * Use defaults values from journey for monitor object (id, name and tags)
146
148
  */
147
- this.#monitor = new Monitor({
148
- name: this.name,
149
- id: this.id,
150
- type: 'browser',
151
- tags: this.tags ?? [],
152
- ...config,
153
- });
149
+ this._setMonitor(
150
+ new Monitor({
151
+ name: this.name,
152
+ id: this.id,
153
+ type: 'browser',
154
+ tags: this.tags ?? [],
155
+ ...config,
156
+ })
157
+ );
158
+ }
159
+
160
+ /**
161
+ * Subclasses use this to swap in a monitor with a different default
162
+ * `type` while reusing the source-location and name-filter wiring.
163
+ */
164
+ protected _setMonitor(monitor: Monitor) {
165
+ this.#monitor = monitor;
154
166
  this.#monitor.setSource(this.location);
155
167
  this.#monitor.setFilter({ match: this.name });
156
168
  }