@elastic/synthetics 1.24.4 → 1.26.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 (119) hide show
  1. package/README.md +67 -0
  2. package/dist/cli.js +3 -1
  3. package/dist/cli.js.map +1 -1
  4. package/dist/common_types.d.ts +25 -0
  5. package/dist/common_types.d.ts.map +1 -1
  6. package/dist/core/certs.d.ts +55 -0
  7. package/dist/core/certs.d.ts.map +1 -0
  8. package/dist/core/certs.js +111 -0
  9. package/dist/core/certs.js.map +1 -0
  10. package/dist/core/gatherer.d.ts +4 -4
  11. package/dist/core/gatherer.d.ts.map +1 -1
  12. package/dist/core/gatherer.js +44 -7
  13. package/dist/core/gatherer.js.map +1 -1
  14. package/dist/core/globals.js +1 -1
  15. package/dist/core/index.d.ts +2 -1
  16. package/dist/core/index.d.ts.map +1 -1
  17. package/dist/core/index.js +16 -1
  18. package/dist/core/index.js.map +1 -1
  19. package/dist/core/runner.d.ts +1 -1
  20. package/dist/core/runner.d.ts.map +1 -1
  21. package/dist/core/runner.js +87 -58
  22. package/dist/core/runner.js.map +1 -1
  23. package/dist/dsl/api-journey.d.ts +62 -0
  24. package/dist/dsl/api-journey.d.ts.map +1 -0
  25. package/dist/dsl/api-journey.js +55 -0
  26. package/dist/dsl/api-journey.js.map +1 -0
  27. package/dist/dsl/index.d.ts +1 -0
  28. package/dist/dsl/index.d.ts.map +1 -1
  29. package/dist/dsl/index.js +1 -0
  30. package/dist/dsl/index.js.map +1 -1
  31. package/dist/dsl/journey.d.ts +10 -2
  32. package/dist/dsl/journey.d.ts.map +1 -1
  33. package/dist/dsl/journey.js +11 -2
  34. package/dist/dsl/journey.js.map +1 -1
  35. package/dist/dsl/monitor.d.ts +2 -1
  36. package/dist/dsl/monitor.d.ts.map +1 -1
  37. package/dist/dsl/monitor.js.map +1 -1
  38. package/dist/index.d.ts +2 -2
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +2 -1
  41. package/dist/index.js.map +1 -1
  42. package/dist/loader.d.ts.map +1 -1
  43. package/dist/loader.js +66 -1
  44. package/dist/loader.js.map +1 -1
  45. package/dist/network-timings.d.ts +55 -0
  46. package/dist/network-timings.d.ts.map +1 -0
  47. package/dist/network-timings.js +107 -0
  48. package/dist/network-timings.js.map +1 -0
  49. package/dist/options.d.ts +1 -0
  50. package/dist/options.d.ts.map +1 -1
  51. package/dist/options.js +16 -0
  52. package/dist/options.js.map +1 -1
  53. package/dist/plugins/api-network.d.ts +39 -0
  54. package/dist/plugins/api-network.d.ts.map +1 -0
  55. package/dist/plugins/api-network.js +235 -0
  56. package/dist/plugins/api-network.js.map +1 -0
  57. package/dist/plugins/network.d.ts +0 -6
  58. package/dist/plugins/network.d.ts.map +1 -1
  59. package/dist/plugins/network.js +5 -70
  60. package/dist/plugins/network.js.map +1 -1
  61. package/dist/plugins/plugin-manager.d.ts +11 -4
  62. package/dist/plugins/plugin-manager.d.ts.map +1 -1
  63. package/dist/plugins/plugin-manager.js +36 -16
  64. package/dist/plugins/plugin-manager.js.map +1 -1
  65. package/dist/push/index.d.ts.map +1 -1
  66. package/dist/push/index.js +4 -0
  67. package/dist/push/index.js.map +1 -1
  68. package/dist/push/monitor.d.ts.map +1 -1
  69. package/dist/push/monitor.js +1 -1
  70. package/dist/push/monitor.js.map +1 -1
  71. package/dist/push/run-local.d.ts.map +1 -1
  72. package/dist/push/run-local.js +15 -3
  73. package/dist/push/run-local.js.map +1 -1
  74. package/dist/push/utils.d.ts +7 -0
  75. package/dist/push/utils.d.ts.map +1 -1
  76. package/dist/push/utils.js +15 -1
  77. package/dist/push/utils.js.map +1 -1
  78. package/dist/reporters/base.d.ts +2 -2
  79. package/dist/reporters/base.d.ts.map +1 -1
  80. package/dist/reporters/base.js.map +1 -1
  81. package/dist/reporters/index.d.ts +2 -2
  82. package/dist/reporters/index.d.ts.map +1 -1
  83. package/dist/reporters/index.js.map +1 -1
  84. package/dist/reporters/json.d.ts +6 -2
  85. package/dist/reporters/json.d.ts.map +1 -1
  86. package/dist/reporters/json.js +68 -33
  87. package/dist/reporters/json.js.map +1 -1
  88. package/package.json +11 -21
  89. package/src/cli.ts +3 -0
  90. package/src/common_types.ts +34 -0
  91. package/src/core/certs.ts +122 -0
  92. package/src/core/gatherer.ts +63 -10
  93. package/src/core/globals.ts +1 -1
  94. package/src/core/index.ts +28 -0
  95. package/src/core/runner.ts +94 -67
  96. package/src/dsl/api-journey.ts +93 -0
  97. package/src/dsl/index.ts +1 -0
  98. package/src/dsl/journey.ts +21 -9
  99. package/src/dsl/monitor.ts +2 -0
  100. package/src/index.ts +6 -0
  101. package/src/loader.ts +78 -2
  102. package/src/network-timings.ts +130 -0
  103. package/src/options.ts +25 -0
  104. package/src/plugins/api-network.ts +252 -0
  105. package/src/plugins/network.ts +5 -87
  106. package/src/plugins/plugin-manager.ts +60 -19
  107. package/src/push/index.ts +11 -0
  108. package/src/push/monitor.ts +6 -2
  109. package/src/push/run-local.ts +25 -15
  110. package/src/push/utils.ts +15 -0
  111. package/src/reporters/base.ts +2 -1
  112. package/src/reporters/index.ts +2 -1
  113. package/src/reporters/json.ts +74 -41
  114. package/templates/README.md +3 -1
  115. package/templates/journeys/advanced-api-example-helpers.ts +79 -0
  116. package/templates/journeys/advanced-api-example.journey.ts +52 -0
  117. package/templates/journeys/api-example.journey.ts +35 -0
  118. package/templates/synthetics.config.ts +2 -0
  119. package/.husky/install.mjs +0 -7
@@ -0,0 +1,252 @@
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, APIResponse } from 'playwright-core';
27
+ import { NetworkInfo, APIDriver } from '../common_types';
28
+ import { log } from '../core/logger';
29
+ import { Step } from '../dsl';
30
+ import { getTimestamp, now } from '../helpers';
31
+ import {
32
+ calcTotalTime,
33
+ getResourceTimings,
34
+ roundMilliSecs,
35
+ } from '../network-timings';
36
+
37
+ /**
38
+ * Kibana UI expects the requestStartTime and loadEndTime to be baseline
39
+ * in seconds as they have the logic to convert it to milliseconds before
40
+ * using for offset calculation
41
+ */
42
+ function epochTimeInSeconds() {
43
+ return getTimestamp() / 1e6;
44
+ }
45
+
46
+ // All `APIRequestContext` verbs funnel into `fetch`, so patching `fetch`
47
+ // alone captures every request exactly once.
48
+ type RequestLike = string | { url(): string };
49
+
50
+ function normalizeUrl(urlOrRequest: RequestLike): string {
51
+ if (typeof urlOrRequest === 'string') return urlOrRequest;
52
+ try {
53
+ return urlOrRequest.url();
54
+ } catch {
55
+ return String(urlOrRequest);
56
+ }
57
+ }
58
+
59
+ // Best-effort request body size; returns 0 for shapes we can't measure.
60
+ function bodyBytes(body: unknown): number {
61
+ if (body == null) return 0;
62
+ if (typeof body === 'string') return Buffer.byteLength(body);
63
+ if (Buffer.isBuffer(body)) return body.byteLength;
64
+ if (body instanceof Uint8Array) return body.byteLength;
65
+ if (typeof body === 'object') {
66
+ try {
67
+ return Buffer.byteLength(JSON.stringify(body));
68
+ } catch {
69
+ return 0;
70
+ }
71
+ }
72
+ return 0;
73
+ }
74
+
75
+ // Wire size of headers (`name: value\r\n`) for ECS `*.bytes`.
76
+ function headersBytes(headers: Record<string, string>): number {
77
+ let total = 0;
78
+ for (const [k, v] of Object.entries(headers ?? {})) {
79
+ total += Buffer.byteLength(k) + Buffer.byteLength(String(v)) + 4; // ": " + CRLF
80
+ }
81
+ return total;
82
+ }
83
+
84
+ export class APINetworkManager {
85
+ results: Array<NetworkInfo> = [];
86
+ _currentStep: Partial<Step> | null = null;
87
+ private _originalFetch?: APIRequestContext['fetch'];
88
+ private _patched = false;
89
+
90
+ constructor(private driver: APIDriver) {}
91
+
92
+ async start() {
93
+ if (this._patched) return;
94
+ log(`Plugins: started collecting API network events`);
95
+ const request = this.driver.request;
96
+ this._originalFetch = request.fetch.bind(request);
97
+ (request as any).fetch = (urlOrRequest: RequestLike, options?: any) =>
98
+ this._interceptRequest(urlOrRequest, options);
99
+ this._patched = true;
100
+ }
101
+
102
+ private async _interceptRequest(
103
+ urlOrRequest: RequestLike,
104
+ options?: any
105
+ ): Promise<APIResponse> {
106
+ const url = normalizeUrl(urlOrRequest);
107
+ const timestamp = getTimestamp();
108
+ const requestSentTime = epochTimeInSeconds();
109
+ const httpMethod = (options?.method ?? 'GET').toUpperCase();
110
+
111
+ log(`API network: ${httpMethod} ${url}`);
112
+
113
+ const requestBody = options?.postData ?? options?.data;
114
+ const requestBodyBytes = bodyBytes(requestBody);
115
+ const requestHeaders = options?.headers ?? {};
116
+
117
+ const entry: NetworkInfo = {
118
+ step: this._currentStep,
119
+ timestamp,
120
+ url,
121
+ type: 'fetch',
122
+ isNavigationRequest: false,
123
+ browser: { name: 'api', version: '' },
124
+ request: {
125
+ url,
126
+ method: httpMethod,
127
+ headers: requestHeaders,
128
+ bytes: headersBytes(requestHeaders) + requestBodyBytes,
129
+ body: requestBodyBytes > 0 ? { bytes: requestBodyBytes } : undefined,
130
+ },
131
+ response: {
132
+ status: -1,
133
+ headers: {},
134
+ mimeType: 'x-unknown',
135
+ },
136
+ requestSentTime,
137
+ loadEndTime: -1,
138
+ responseReceivedTime: -1,
139
+ resourceSize: 0,
140
+ transferSize: 0,
141
+ timings: {
142
+ blocked: -1,
143
+ dns: -1,
144
+ ssl: -1,
145
+ connect: -1,
146
+ send: -1,
147
+ wait: -1,
148
+ receive: -1,
149
+ total: -1,
150
+ },
151
+ };
152
+ this.results.push(entry);
153
+
154
+ const startTime = now();
155
+ try {
156
+ const response = (await this._originalFetch(
157
+ urlOrRequest as any,
158
+ options
159
+ )) as APIResponse;
160
+ const headers = response.headers();
161
+
162
+ // Prefer `Content-Length`; fall back to the body buffer (chunked
163
+ // responses omit the header).
164
+ const contentLength = parseContentLength(headers['content-length']);
165
+ let responseBodyBytes = contentLength;
166
+ if (responseBodyBytes < 0) {
167
+ try {
168
+ const buf = await response.body();
169
+ responseBodyBytes = buf?.byteLength ?? 0;
170
+ } catch {
171
+ responseBodyBytes = 0;
172
+ }
173
+ }
174
+ const responseHeaderBytes = headersBytes(headers);
175
+ const transferBytes = responseHeaderBytes + responseBodyBytes;
176
+ entry.response = {
177
+ url: response.url(),
178
+ status: response.status(),
179
+ statusText: response.statusText(),
180
+ headers,
181
+ mimeType: headers['content-type'] ?? 'x-unknown',
182
+ bytes: transferBytes,
183
+ body: { bytes: responseBodyBytes },
184
+ };
185
+ entry.transferSize = transferBytes;
186
+ entry.resourceSize = responseBodyBytes;
187
+ entry.responseReceivedTime = epochTimeInSeconds();
188
+ entry.loadEndTime = entry.responseReceivedTime;
189
+
190
+ // Per-phase Resource Timing (Playwright >= 1.62). Phases that did not
191
+ // happen (reused keep-alive socket) or are unknown (HAR replay) come
192
+ // back as `-1`; `calcTotalTime` then falls back to the wall-clock span.
193
+ const timing = response.timing();
194
+ entry.timings = getResourceTimings(timing);
195
+ entry.timings.total = calcTotalTime(entry, timing);
196
+
197
+ // Native (Playwright >= 1.61) TLS/socket info for the final hop;
198
+ // both resolve to `null` for non-HTTPS or unknown addresses.
199
+ const [serverAddr, securityDetails] = await Promise.all([
200
+ response.serverAddr(),
201
+ response.securityDetails(),
202
+ ]);
203
+ if (serverAddr) {
204
+ entry.response.remoteIPAddress = serverAddr.ipAddress;
205
+ entry.response.remotePort = serverAddr.port;
206
+ }
207
+ if (securityDetails) {
208
+ entry.response.securityDetails = {
209
+ ...securityDetails,
210
+ protocol: normalizeTLSProtocol(securityDetails.protocol),
211
+ };
212
+ }
213
+ return response;
214
+ } catch (error) {
215
+ entry.responseReceivedTime = epochTimeInSeconds();
216
+ entry.loadEndTime = entry.responseReceivedTime;
217
+ entry.timings.total = roundMilliSecs(now() - startTime);
218
+ throw error;
219
+ }
220
+ }
221
+
222
+ // Drop the own-property `fetch` so the prototype method is reachable
223
+ // again, keeping the context usable after the plugin stops.
224
+ private _restore() {
225
+ if (!this._patched) return;
226
+ delete (this.driver.request as any).fetch;
227
+ this._originalFetch = undefined;
228
+ this._patched = false;
229
+ }
230
+
231
+ async stop() {
232
+ this._restore();
233
+ log(`Plugins: stopped collecting API network events`);
234
+ return this.results;
235
+ }
236
+ }
237
+
238
+ function parseContentLength(raw: string | undefined): number {
239
+ if (!raw) return -1;
240
+ const n = Number(raw);
241
+ return Number.isFinite(n) && n >= 0 ? n : -1;
242
+ }
243
+
244
+ /**
245
+ * `securityDetails()` reports `"TLSv1.3"` but the reporter splits on a
246
+ * space to derive `tls.version_protocol`/`tls.version`. Normalize to the
247
+ * spaced `"TLS 1.3"` shape so both journey types emit consistent fields.
248
+ */
249
+ function normalizeTLSProtocol(raw?: string): string | undefined {
250
+ if (!raw) return undefined;
251
+ return raw.replace(/\s*v(?=\d)/i, ' ');
252
+ }
@@ -28,6 +28,7 @@ import { NetworkInfo, BrowserInfo, Driver } from '../common_types';
28
28
  import { log } from '../core/logger';
29
29
  import { Step } from '../dsl';
30
30
  import { getTimestamp } from '../helpers';
31
+ import { calcTotalTime, getResourceTimings } from '../network-timings';
31
32
 
32
33
  /**
33
34
  * Kibana UI expects the requestStartTime and loadEndTime to be baseline
@@ -38,22 +39,6 @@ function epochTimeInSeconds() {
38
39
  return getTimestamp() / 1e6;
39
40
  }
40
41
 
41
- /**
42
- * Find the first positive number in an array for Resource timing data
43
- */
44
- const firstPositive = (numbers: number[]) => {
45
- for (let i = 0; i < numbers.length; ++i) {
46
- if (numbers[i] > 0) {
47
- return numbers[i];
48
- }
49
- }
50
- return null;
51
- };
52
-
53
- const roundMilliSecs = (value: number): number => {
54
- return Math.floor(value * 1000) / 1000;
55
- };
56
-
57
42
  /**
58
43
  * Used as a key in each Network Request to identify the
59
44
  * associated request across distinct lifecycle events
@@ -200,42 +185,8 @@ export class NetworkManager {
200
185
  // Gather all resource timing information up until the
201
186
  // TTFB(Time to first byte) is received
202
187
  const timing = request.timing();
203
- const blocked =
204
- roundMilliSecs(
205
- firstPositive([
206
- timing.domainLookupStart,
207
- timing.connectStart,
208
- timing.requestStart,
209
- ])
210
- ) || -1;
211
- const dns =
212
- timing.domainLookupEnd !== -1
213
- ? roundMilliSecs(timing.domainLookupEnd - timing.domainLookupStart)
214
- : -1;
215
- const connect =
216
- timing.connectEnd !== -1
217
- ? roundMilliSecs(timing.connectEnd - timing.connectStart)
218
- : -1;
219
- const ssl =
220
- timing.secureConnectionStart !== -1
221
- ? roundMilliSecs(timing.connectEnd - timing.secureConnectionStart)
222
- : -1;
223
- const wait =
224
- timing.responseStart !== -1
225
- ? roundMilliSecs(timing.responseStart - timing.requestStart)
226
- : -1;
227
-
228
- networkEntry.timings = {
229
- blocked,
230
- dns,
231
- ssl,
232
- connect,
233
- send: 0, // not exposed via RT api
234
- wait,
235
- receive: -1, // will be available after full response is received
236
- total: -1,
237
- };
238
- this._calcTotalTime(networkEntry, timing);
188
+ networkEntry.timings = getResourceTimings(timing);
189
+ networkEntry.timings.total = calcTotalTime(networkEntry, timing);
239
190
 
240
191
  const frame = this._nullableFrameBarrier(request);
241
192
  const page = frame?.page();
@@ -278,12 +229,8 @@ export class NetworkManager {
278
229
  networkEntry.loadEndTime = epochTimeInSeconds();
279
230
  // responseEnd is fired after the last byte of the response is received.
280
231
  const timing = request.timing();
281
- const receive =
282
- timing.responseEnd !== -1
283
- ? roundMilliSecs(timing.responseEnd - timing.responseStart)
284
- : -1;
285
- networkEntry.timings.receive = receive;
286
- this._calcTotalTime(networkEntry, timing);
232
+ networkEntry.timings = getResourceTimings(timing);
233
+ networkEntry.timings.total = calcTotalTime(networkEntry, timing);
287
234
 
288
235
  // For aborted/failed requests sizes will not be present
289
236
  if (timing.startTime <= 0) {
@@ -310,35 +257,6 @@ export class NetworkManager {
310
257
  );
311
258
  }
312
259
 
313
- /**
314
- * Calculates the total time for the network request based on the ResourceTiming
315
- * data from Playwright. Fallbacks to the event timings if ResourceTiming data
316
- * is not available.
317
- */
318
- private _calcTotalTime(
319
- entry: NetworkInfo,
320
- rtiming: ReturnType<Request['timing']>
321
- ) {
322
- const timings = entry.timings;
323
- entry.timings.total = [
324
- timings.blocked,
325
- timings.dns,
326
- timings.connect,
327
- timings.wait,
328
- timings.receive,
329
- ].reduce((pre, cur) => ((cur || -1) > 0 ? cur + pre : pre), 0);
330
-
331
- // fallback when ResourceTiming data is not available
332
- if (rtiming.startTime <= 0) {
333
- const end =
334
- entry.loadEndTime ||
335
- entry.responseReceivedTime ||
336
- entry.requestSentTime;
337
- const total = roundMilliSecs((end - entry.requestSentTime) * 1000);
338
- entry.timings.total = total <= 0 ? -1 : total;
339
- }
340
- }
341
-
342
260
  async stop() {
343
261
  /**
344
262
  * Waiting for all network events is error prone and might hang the tests
@@ -23,7 +23,7 @@
23
23
  *
24
24
  */
25
25
 
26
- import { PluginOutput, Driver } from '../common_types';
26
+ import { PluginOutput, Driver, APIDriver, NetworkInfo } from '../common_types';
27
27
  import {
28
28
  BrowserConsole,
29
29
  NetworkManager,
@@ -32,11 +32,39 @@ import {
32
32
  TraceOptions,
33
33
  } from './';
34
34
  import { Step } from '../dsl';
35
+ import { APINetworkManager } from './api-network';
35
36
 
36
37
  type PluginType = 'network' | 'trace' | 'performance' | 'browserconsole';
37
- type Plugin = NetworkManager | Tracing | PerformanceManager | BrowserConsole;
38
+
39
+ // Shared shape for `NetworkManager` and `APINetworkManager` so the manager
40
+ // stays agnostic to the driver type.
41
+ export type NetworkPlugin = {
42
+ results: Array<NetworkInfo>;
43
+ _currentStep: Partial<Step> | null;
44
+ start(): Promise<void>;
45
+ stop(): Promise<Array<NetworkInfo>>;
46
+ };
47
+
48
+ type Plugin =
49
+ | NetworkManager
50
+ | Tracing
51
+ | PerformanceManager
52
+ | BrowserConsole
53
+ | APINetworkManager;
38
54
  type PluginOptions = TraceOptions;
39
55
 
56
+ function isBrowserDriver(driver: Driver | APIDriver): driver is Driver {
57
+ return 'context' in driver;
58
+ }
59
+
60
+ function isNetworkPlugin(
61
+ plugin: Plugin
62
+ ): plugin is NetworkManager | APINetworkManager {
63
+ return (
64
+ plugin instanceof NetworkManager || plugin instanceof APINetworkManager
65
+ );
66
+ }
67
+
40
68
  export class PluginManager {
41
69
  protected plugins = new Map<PluginType, Plugin>();
42
70
  public PLUGIN_TYPES: Array<PluginType> = [
@@ -45,23 +73,31 @@ export class PluginManager {
45
73
  'performance',
46
74
  'browserconsole',
47
75
  ];
48
- constructor(private driver: Driver) {}
76
+ constructor(private driver: Driver | APIDriver) {}
49
77
 
50
78
  register(type: PluginType, options: PluginOptions) {
51
79
  let instance: Plugin;
52
- switch (type) {
53
- case 'network':
54
- instance = new NetworkManager(this.driver);
55
- break;
56
- case 'trace':
57
- instance = new Tracing(this.driver, options);
58
- break;
59
- case 'performance':
60
- instance = new PerformanceManager(this.driver);
61
- break;
62
- case 'browserconsole':
63
- instance = new BrowserConsole(this.driver);
64
- break;
80
+
81
+ if (isBrowserDriver(this.driver)) {
82
+ switch (type) {
83
+ case 'network':
84
+ instance = new NetworkManager(this.driver);
85
+ break;
86
+ case 'trace':
87
+ instance = new Tracing(this.driver, options);
88
+ break;
89
+ case 'performance':
90
+ instance = new PerformanceManager(this.driver);
91
+ break;
92
+ case 'browserconsole':
93
+ instance = new BrowserConsole(this.driver);
94
+ break;
95
+ }
96
+ } else {
97
+ // API journeys only record network; skip browser-only plugins.
98
+ if (type === 'network') {
99
+ instance = new APINetworkManager(this.driver);
100
+ }
65
101
  }
66
102
  instance && this.plugins.set(type, instance);
67
103
  return instance;
@@ -97,15 +133,20 @@ export class PluginManager {
97
133
  return this.plugins.get(type);
98
134
  }
99
135
 
136
+ // Propagate the active step to step-scoped plugins (no-op when absent).
100
137
  onStep(step: Step) {
101
- (this.get('browserconsole') as BrowserConsole)._currentStep = step;
102
- (this.get('network') as NetworkManager)._currentStep = step;
138
+ const browserConsole = this.plugins.get('browserconsole') as
139
+ | BrowserConsole
140
+ | undefined;
141
+ if (browserConsole) browserConsole._currentStep = step;
142
+ const network = this.plugins.get('network');
143
+ if (network && isNetworkPlugin(network)) network._currentStep = step;
103
144
  }
104
145
 
105
146
  async output() {
106
147
  const data: PluginOutput = {};
107
148
  for (const [, plugin] of this.plugins) {
108
- if (plugin instanceof NetworkManager) {
149
+ if (isNetworkPlugin(plugin)) {
109
150
  data.networkinfo = await plugin.stop();
110
151
  } else if (plugin instanceof BrowserConsole) {
111
152
  data.browserconsole = plugin.stop();
package/src/push/index.ts CHANGED
@@ -56,10 +56,12 @@ import {
56
56
  import {
57
57
  getBatches,
58
58
  getSizedBatches,
59
+ isAPIMonitorSupported,
59
60
  isBulkAPISupported,
60
61
  isLightweightMonitorSupported,
61
62
  logDiff,
62
63
  logGroups,
64
+ MIN_API_MONITOR_VERSION,
63
65
  printBytes,
64
66
  } from './utils';
65
67
  import { runLocal } from './run-local';
@@ -76,6 +78,15 @@ export async function push(monitors: Monitor[], options: PushOptions) {
76
78
  throw error(formatDuplicateError(duplicates));
77
79
  }
78
80
 
81
+ if (
82
+ monitors.some(m => m.type === 'api') &&
83
+ !isAPIMonitorSupported(options.kibanaVersion)
84
+ ) {
85
+ throw error(
86
+ `Aborted: API journey monitors are only supported in Kibana ${MIN_API_MONITOR_VERSION} or above (current: ${options.kibanaVersion}). Please upgrade Kibana or remove API journeys before pushing.`
87
+ );
88
+ }
89
+
79
90
  progress(
80
91
  `Pushing monitors for '${options.id}' project in kibana '${options.space}' space`
81
92
  );
@@ -38,7 +38,11 @@ import {
38
38
  MonitorConfig,
39
39
  } from '../dsl/monitor';
40
40
  import { PushOptions } from '../common_types';
41
- import { isParamOptionSupported, normalizeMonitorName } from './utils';
41
+ import {
42
+ isBundledMonitorType,
43
+ isParamOptionSupported,
44
+ normalizeMonitorName,
45
+ } from './utils';
42
46
 
43
47
  // Allowed extensions for lightweight monitor files
44
48
  const ALLOWED_LW_EXTENSIONS = ['.yml', '.yaml'];
@@ -143,7 +147,7 @@ export async function buildMonitorSchema(monitors: Monitor[], isV2: boolean) {
143
147
  locations: translateLocation(config.locations),
144
148
  };
145
149
 
146
- if (type === 'browser') {
150
+ if (isBundledMonitorType(type)) {
147
151
  const outPath = join(
148
152
  bundlePath,
149
153
  normalizeMonitorName(config.name) + '.zip'
@@ -32,6 +32,7 @@ import { red } from 'kleur/colors';
32
32
  import { join } from 'path';
33
33
  import { pathToFileURL } from 'url';
34
34
  import { MonitorSchema } from './monitor';
35
+ import { isBundledMonitorType } from './utils';
35
36
 
36
37
  async function unzipFile(zipPath, destination) {
37
38
  return new Promise<void>((resolve, reject) => {
@@ -73,21 +74,30 @@ async function runNpmInstall(directory) {
73
74
 
74
75
  async function runTest(directory, schema: MonitorSchema) {
75
76
  return new Promise<void>((resolve, reject) => {
76
- const runTest = spawn(
77
- 'npx',
78
- [
79
- '@elastic/synthetics',
80
- '.',
81
- '--playwright-options',
82
- JSON.stringify(schema.playwrightOptions),
83
- '--params',
84
- JSON.stringify(schema.params),
85
- ],
86
- {
87
- cwd: directory,
88
- stdio: 'inherit',
77
+ const args = [
78
+ '@elastic/synthetics',
79
+ '.',
80
+ '--playwright-options',
81
+ JSON.stringify(schema.playwrightOptions),
82
+ '--params',
83
+ JSON.stringify(schema.params),
84
+ ];
85
+ const allowlist = schema.certificateErrorSpkiAllowlist;
86
+ if (allowlist != null) {
87
+ const entries = Array.isArray(allowlist) ? allowlist : [allowlist];
88
+ const pemEntries = entries
89
+ .map(entry =>
90
+ Buffer.isBuffer(entry) ? entry.toString('utf-8') : entry
91
+ )
92
+ .filter((entry): entry is string => Boolean(entry));
93
+ if (pemEntries.length > 0) {
94
+ args.push('--certificate-error-spki-allowlist', ...pemEntries);
89
95
  }
90
- );
96
+ }
97
+ const runTest = spawn('npx', args, {
98
+ cwd: directory,
99
+ stdio: 'inherit',
100
+ });
91
101
 
92
102
  runTest.on('close', resolve);
93
103
  runTest.on('error', err => {
@@ -118,7 +128,7 @@ async function extract(
118
128
  zipPath: string,
119
129
  unzipPath: string
120
130
  ) {
121
- if (schema.type !== 'browser') {
131
+ if (!isBundledMonitorType(schema.type)) {
122
132
  return;
123
133
  }
124
134
  const content = schema.content;
package/src/push/utils.ts CHANGED
@@ -185,6 +185,21 @@ export function isParamOptionSupported(version: string) {
185
185
  return semver.satisfies(version, '>=8.7.2');
186
186
  }
187
187
 
188
+ // API journey monitors are supported only from 9.6.0 and above.
189
+ // Coerce so pre-release/SNAPSHOT builds (e.g. `9.6.0-SNAPSHOT`) count as 9.6.0.
190
+ export const MIN_API_MONITOR_VERSION = '9.6.0';
191
+ export function isAPIMonitorSupported(version: string) {
192
+ const coerced = semver.coerce(version);
193
+ return coerced ? semver.gte(coerced, MIN_API_MONITOR_VERSION) : false;
194
+ }
195
+
196
+ /**
197
+ * Project monitors ship bundled journey code (browser + api), unlike
198
+ * lightweight YAML monitors (http/tcp/icmp).
199
+ */
200
+ export const isBundledMonitorType = (type?: string) =>
201
+ type === 'browser' || type === 'api';
202
+
188
203
  /**
189
204
  * Helper that replaces url paths traversal issues when bundling
190
205
  */
@@ -30,6 +30,7 @@ import { symbols, indent, now } from '../helpers';
30
30
  import { Reporter, ReporterOptions } from '.';
31
31
  import { Journey, Step } from '../dsl';
32
32
  import {
33
+ APIJourneyEndResult,
33
34
  JourneyEndResult,
34
35
  JourneyStartResult,
35
36
  StepResult,
@@ -84,7 +85,7 @@ export default class BaseReporter implements Reporter {
84
85
  }
85
86
 
86
87
  /* eslint-disable @typescript-eslint/no-unused-vars */
87
- onJourneyEnd(journey: Journey, {}: JourneyEndResult) {
88
+ onJourneyEnd(journey: Journey, {}: JourneyEndResult | APIJourneyEndResult) {
88
89
  const { failed, succeeded, skipped } = this.metrics;
89
90
  const total = failed + succeeded + skipped;
90
91
  /**
@@ -32,6 +32,7 @@ import {
32
32
  JourneyEndResult,
33
33
  JourneyStartResult,
34
34
  StepEndResult,
35
+ APIJourneyEndResult,
35
36
  } from '../common_types';
36
37
  import BuildKiteCLIReporter from './build_kite_cli';
37
38
 
@@ -63,7 +64,7 @@ export interface Reporter {
63
64
  onStepEnd?(journey: Journey, step: Step, result: StepEndResult): void;
64
65
  onJourneyEnd?(
65
66
  journey: Journey,
66
- result: JourneyEndResult
67
+ result: JourneyEndResult | APIJourneyEndResult
67
68
  ): void | Promise<void>;
68
69
  onEnd?(): void | Promise<void>;
69
70
  }