@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.
- package/README.md +67 -0
- package/dist/cli.js +3 -1
- package/dist/cli.js.map +1 -1
- package/dist/common_types.d.ts +25 -0
- package/dist/common_types.d.ts.map +1 -1
- package/dist/core/certs.d.ts +55 -0
- package/dist/core/certs.d.ts.map +1 -0
- package/dist/core/certs.js +111 -0
- package/dist/core/certs.js.map +1 -0
- package/dist/core/gatherer.d.ts +4 -4
- package/dist/core/gatherer.d.ts.map +1 -1
- package/dist/core/gatherer.js +44 -7
- package/dist/core/gatherer.js.map +1 -1
- package/dist/core/globals.js +1 -1
- package/dist/core/index.d.ts +2 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +16 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/runner.d.ts +1 -1
- package/dist/core/runner.d.ts.map +1 -1
- package/dist/core/runner.js +87 -58
- package/dist/core/runner.js.map +1 -1
- package/dist/dsl/api-journey.d.ts +62 -0
- package/dist/dsl/api-journey.d.ts.map +1 -0
- package/dist/dsl/api-journey.js +55 -0
- package/dist/dsl/api-journey.js.map +1 -0
- package/dist/dsl/index.d.ts +1 -0
- package/dist/dsl/index.d.ts.map +1 -1
- package/dist/dsl/index.js +1 -0
- package/dist/dsl/index.js.map +1 -1
- package/dist/dsl/journey.d.ts +10 -2
- package/dist/dsl/journey.d.ts.map +1 -1
- package/dist/dsl/journey.js +11 -2
- package/dist/dsl/journey.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.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +66 -1
- package/dist/loader.js.map +1 -1
- package/dist/network-timings.d.ts +55 -0
- package/dist/network-timings.d.ts.map +1 -0
- package/dist/network-timings.js +107 -0
- package/dist/network-timings.js.map +1 -0
- package/dist/options.d.ts +1 -0
- package/dist/options.d.ts.map +1 -1
- package/dist/options.js +16 -0
- package/dist/options.js.map +1 -1
- package/dist/plugins/api-network.d.ts +39 -0
- package/dist/plugins/api-network.d.ts.map +1 -0
- package/dist/plugins/api-network.js +235 -0
- package/dist/plugins/api-network.js.map +1 -0
- package/dist/plugins/network.d.ts +0 -6
- package/dist/plugins/network.d.ts.map +1 -1
- package/dist/plugins/network.js +5 -70
- package/dist/plugins/network.js.map +1 -1
- package/dist/plugins/plugin-manager.d.ts +11 -4
- package/dist/plugins/plugin-manager.d.ts.map +1 -1
- package/dist/plugins/plugin-manager.js +36 -16
- package/dist/plugins/plugin-manager.js.map +1 -1
- package/dist/push/index.d.ts.map +1 -1
- package/dist/push/index.js +4 -0
- package/dist/push/index.js.map +1 -1
- package/dist/push/monitor.d.ts.map +1 -1
- package/dist/push/monitor.js +1 -1
- package/dist/push/monitor.js.map +1 -1
- package/dist/push/run-local.d.ts.map +1 -1
- package/dist/push/run-local.js +15 -3
- package/dist/push/run-local.js.map +1 -1
- package/dist/push/utils.d.ts +7 -0
- package/dist/push/utils.d.ts.map +1 -1
- package/dist/push/utils.js +15 -1
- package/dist/push/utils.js.map +1 -1
- package/dist/reporters/base.d.ts +2 -2
- package/dist/reporters/base.d.ts.map +1 -1
- package/dist/reporters/base.js.map +1 -1
- package/dist/reporters/index.d.ts +2 -2
- package/dist/reporters/index.d.ts.map +1 -1
- package/dist/reporters/index.js.map +1 -1
- package/dist/reporters/json.d.ts +6 -2
- package/dist/reporters/json.d.ts.map +1 -1
- package/dist/reporters/json.js +68 -33
- package/dist/reporters/json.js.map +1 -1
- package/package.json +11 -21
- package/src/cli.ts +3 -0
- package/src/common_types.ts +34 -0
- package/src/core/certs.ts +122 -0
- package/src/core/gatherer.ts +63 -10
- package/src/core/globals.ts +1 -1
- package/src/core/index.ts +28 -0
- package/src/core/runner.ts +94 -67
- package/src/dsl/api-journey.ts +93 -0
- package/src/dsl/index.ts +1 -0
- package/src/dsl/journey.ts +21 -9
- package/src/dsl/monitor.ts +2 -0
- package/src/index.ts +6 -0
- package/src/loader.ts +78 -2
- package/src/network-timings.ts +130 -0
- package/src/options.ts +25 -0
- package/src/plugins/api-network.ts +252 -0
- package/src/plugins/network.ts +5 -87
- package/src/plugins/plugin-manager.ts +60 -19
- package/src/push/index.ts +11 -0
- package/src/push/monitor.ts +6 -2
- package/src/push/run-local.ts +25 -15
- package/src/push/utils.ts +15 -0
- package/src/reporters/base.ts +2 -1
- package/src/reporters/index.ts +2 -1
- package/src/reporters/json.ts +74 -41
- package/templates/README.md +3 -1
- package/templates/journeys/advanced-api-example-helpers.ts +79 -0
- package/templates/journeys/advanced-api-example.journey.ts +52 -0
- package/templates/journeys/api-example.journey.ts +35 -0
- package/templates/synthetics.config.ts +2 -0
- package/.husky/install.mjs +0 -7
package/src/cli.ts
CHANGED
|
@@ -74,6 +74,7 @@ const {
|
|
|
74
74
|
match,
|
|
75
75
|
fields,
|
|
76
76
|
maintenanceWindows,
|
|
77
|
+
certificateErrorSpkiAllowlist,
|
|
77
78
|
} = getCommonCommandOpts();
|
|
78
79
|
|
|
79
80
|
program
|
|
@@ -128,6 +129,7 @@ program
|
|
|
128
129
|
'--ignore-https-errors',
|
|
129
130
|
'ignores any HTTPS errors in sites being tested, including ones related to unrecognized certs or signatures. This can be insecure!'
|
|
130
131
|
)
|
|
132
|
+
.addOption(certificateErrorSpkiAllowlist)
|
|
131
133
|
.option(
|
|
132
134
|
'--quiet-exit-code',
|
|
133
135
|
'always return 0 as an exit code status, regardless of test pass / fail. Only return > 0 exit codes on internal errors where the suite could not be run'
|
|
@@ -232,6 +234,7 @@ program
|
|
|
232
234
|
.addOption(match)
|
|
233
235
|
.addOption(params)
|
|
234
236
|
.addOption(playwrightOpts)
|
|
237
|
+
.addOption(certificateErrorSpkiAllowlist)
|
|
235
238
|
.addOption(configOpt)
|
|
236
239
|
.addOption(maintenanceWindows)
|
|
237
240
|
.action(async cmdOpts => {
|
package/src/common_types.ts
CHANGED
|
@@ -64,6 +64,10 @@ export type NetworkConditions = {
|
|
|
64
64
|
latency: number;
|
|
65
65
|
};
|
|
66
66
|
|
|
67
|
+
export type APIDriver = {
|
|
68
|
+
request: APIRequestContext;
|
|
69
|
+
};
|
|
70
|
+
|
|
67
71
|
export type Driver = {
|
|
68
72
|
browser: ChromiumBrowser;
|
|
69
73
|
context: ChromiumBrowserContext;
|
|
@@ -208,6 +212,16 @@ type GrepOptions = {
|
|
|
208
212
|
match?: string;
|
|
209
213
|
};
|
|
210
214
|
|
|
215
|
+
/**
|
|
216
|
+
* PEM certificates (or Buffers) used to build Chromium's SPKI certificate-error
|
|
217
|
+
* allowlist. This does not add a CA to Chromium's trust store: matching
|
|
218
|
+
* certificates bypass certificate validation errors.
|
|
219
|
+
*/
|
|
220
|
+
export type CertificateErrorSpkiAllowlist =
|
|
221
|
+
| string
|
|
222
|
+
| Buffer
|
|
223
|
+
| Array<string | Buffer>;
|
|
224
|
+
|
|
211
225
|
type BaseArgs = {
|
|
212
226
|
params?: Params;
|
|
213
227
|
screenshots?: ScreenshotOptions;
|
|
@@ -217,6 +231,7 @@ type BaseArgs = {
|
|
|
217
231
|
outfd?: number;
|
|
218
232
|
wsEndpoint?: string;
|
|
219
233
|
pauseOnError?: boolean;
|
|
234
|
+
certificateErrorSpkiAllowlist?: CertificateErrorSpkiAllowlist;
|
|
220
235
|
playwrightOptions?: PlaywrightOptions;
|
|
221
236
|
quietExitCode?: boolean;
|
|
222
237
|
throttling?: MonitorConfig['throttling'];
|
|
@@ -256,6 +271,13 @@ export type RunOptions = BaseArgs & {
|
|
|
256
271
|
grepOpts?: GrepOptions;
|
|
257
272
|
};
|
|
258
273
|
|
|
274
|
+
export type APIRunOptions = BaseArgs & {
|
|
275
|
+
network?: boolean;
|
|
276
|
+
environment?: string;
|
|
277
|
+
reporter?: BuiltInReporterName | ReporterInstance;
|
|
278
|
+
grepOpts?: GrepOptions;
|
|
279
|
+
};
|
|
280
|
+
|
|
259
281
|
export type PushOptions = Partial<ProjectSettings> &
|
|
260
282
|
Partial<BaseArgs> & {
|
|
261
283
|
auth: string;
|
|
@@ -289,6 +311,13 @@ export type SyntheticsConfig = {
|
|
|
289
311
|
monitor?: MonitorConfig;
|
|
290
312
|
project?: ProjectSettings;
|
|
291
313
|
proxy?: ProxySettings;
|
|
314
|
+
certificateErrorSpkiAllowlist?: CertificateErrorSpkiAllowlist;
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
/** Runner Payload types */
|
|
318
|
+
export type APIJourneyResult = Partial<Journey> & {
|
|
319
|
+
networkinfo?: PluginOutput['networkinfo'];
|
|
320
|
+
stepsresults?: Array<StepResult>;
|
|
292
321
|
};
|
|
293
322
|
|
|
294
323
|
/** Runner Payload types */
|
|
@@ -330,6 +359,11 @@ export type JourneyEndResult = JourneyStartResult &
|
|
|
330
359
|
options: RunOptions;
|
|
331
360
|
};
|
|
332
361
|
|
|
362
|
+
export type APIJourneyEndResult = JourneyStartResult &
|
|
363
|
+
APIJourneyResult & {
|
|
364
|
+
options: RunOptions;
|
|
365
|
+
};
|
|
366
|
+
|
|
333
367
|
export type StepEndResult = StepResult;
|
|
334
368
|
|
|
335
369
|
export type ProxySettings = {
|
|
@@ -0,0 +1,122 @@
|
|
|
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 { X509Certificate, createHash } from 'crypto';
|
|
27
|
+
import { CertificateErrorSpkiAllowlist } from '../common_types';
|
|
28
|
+
import { warn } from '../helpers';
|
|
29
|
+
|
|
30
|
+
const PEM_CERTIFICATE_RE =
|
|
31
|
+
/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Normalize user-provided PEM certificates (string, Buffer, or an array of
|
|
35
|
+
* either) into a flat list of PEM strings. Each entry may itself be a bundle
|
|
36
|
+
* that contains more than one certificate.
|
|
37
|
+
*/
|
|
38
|
+
export function normalizeCertificateErrorSpkiAllowlist(
|
|
39
|
+
certificates?: CertificateErrorSpkiAllowlist
|
|
40
|
+
): string[] {
|
|
41
|
+
if (certificates == null) {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
const entries = Array.isArray(certificates) ? certificates : [certificates];
|
|
45
|
+
return entries
|
|
46
|
+
.map(entry => (Buffer.isBuffer(entry) ? entry.toString('utf-8') : entry))
|
|
47
|
+
.filter((entry): entry is string => Boolean(entry && entry.trim()));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Split a PEM bundle into the individual certificates it contains. Node's
|
|
52
|
+
* `X509Certificate` only parses the first certificate of a bundle, so we have
|
|
53
|
+
* to slice them apart ourselves to support full chains / multiple CAs.
|
|
54
|
+
*/
|
|
55
|
+
export function splitPemCertificates(pem: string): string[] {
|
|
56
|
+
return pem.match(PEM_CERTIFICATE_RE) ?? [];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Compute the base64 encoded SHA-256 fingerprint of a certificate's
|
|
61
|
+
* SubjectPublicKeyInfo (SPKI). This is the exact value Chromium expects in the
|
|
62
|
+
* `--ignore-certificate-errors-spki-list` switch.
|
|
63
|
+
*/
|
|
64
|
+
export function getSpkiFingerprint(pem: string): string {
|
|
65
|
+
const der = new X509Certificate(pem).publicKey.export({
|
|
66
|
+
type: 'spki',
|
|
67
|
+
format: 'der',
|
|
68
|
+
});
|
|
69
|
+
return createHash('sha256').update(der).digest('base64');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Describe an entry that failed to parse as a certificate for use in a
|
|
74
|
+
* warning message. A raw file path (e.g. from a typo'd
|
|
75
|
+
* `certificateErrorSpkiAllowlist` entry that didn't match an existing file) is
|
|
76
|
+
* shown as-is since that's the actionable detail; inline PEM content is not,
|
|
77
|
+
* to avoid dumping key material to the console.
|
|
78
|
+
*/
|
|
79
|
+
function describeInvalidCertificateEntry(candidate: string): string {
|
|
80
|
+
if (candidate.includes('-----BEGIN CERTIFICATE-----')) {
|
|
81
|
+
return 'an inline PEM certificate';
|
|
82
|
+
}
|
|
83
|
+
const preview =
|
|
84
|
+
candidate.length > 80 ? `${candidate.slice(0, 80)}...` : candidate;
|
|
85
|
+
return `"${preview}"`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Build the list of SPKI fingerprints for all supplied certificates. Chromium
|
|
90
|
+
* uses this list to bypass certificate errors for matching presented
|
|
91
|
+
* certificates; it does not establish CA trust. Invalid certificates are
|
|
92
|
+
* skipped, with a warning, so a single bad entry never aborts the whole run
|
|
93
|
+
* but also never fails silently - a `certificateErrorSpkiAllowlist` entry that
|
|
94
|
+
* doesn't resolve to a real file and isn't valid PEM (e.g. a typo'd path)
|
|
95
|
+
* would otherwise never be allowlisted, and matching endpoints would keep
|
|
96
|
+
* failing certificate validation with no indication why.
|
|
97
|
+
*/
|
|
98
|
+
export function getSpkiFingerprints(
|
|
99
|
+
certificates?: CertificateErrorSpkiAllowlist
|
|
100
|
+
): string[] {
|
|
101
|
+
const fingerprints = new Set<string>();
|
|
102
|
+
for (const entry of normalizeCertificateErrorSpkiAllowlist(certificates)) {
|
|
103
|
+
const pemCertificates = splitPemCertificates(entry);
|
|
104
|
+
// Fall back to treating the whole entry as a single certificate when no
|
|
105
|
+
// PEM boundary markers are present.
|
|
106
|
+
const candidates = pemCertificates.length > 0 ? pemCertificates : [entry];
|
|
107
|
+
for (const certificate of candidates) {
|
|
108
|
+
try {
|
|
109
|
+
fingerprints.add(getSpkiFingerprint(certificate));
|
|
110
|
+
} catch (e) {
|
|
111
|
+
warn(
|
|
112
|
+
`certificateErrorSpkiAllowlist: could not parse ${describeInvalidCertificateEntry(
|
|
113
|
+
certificate
|
|
114
|
+
)} as a certificate - it will not be allowlisted and matching endpoints may still fail certificate validation (${
|
|
115
|
+
e.message
|
|
116
|
+
})`
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return [...fingerprints];
|
|
122
|
+
}
|
package/src/core/gatherer.ts
CHANGED
|
@@ -32,7 +32,13 @@ import {
|
|
|
32
32
|
} from 'playwright-core';
|
|
33
33
|
import { PluginManager } from '../plugins';
|
|
34
34
|
import { log } from './logger';
|
|
35
|
-
import {
|
|
35
|
+
import { getSpkiFingerprints } from './certs';
|
|
36
|
+
import {
|
|
37
|
+
APIDriver,
|
|
38
|
+
Driver,
|
|
39
|
+
NetworkConditions,
|
|
40
|
+
RunOptions,
|
|
41
|
+
} from '../common_types';
|
|
36
42
|
|
|
37
43
|
// Default timeout for Playwright actions and Navigations
|
|
38
44
|
const DEFAULT_TIMEOUT = 50000;
|
|
@@ -56,10 +62,31 @@ export class Gatherer {
|
|
|
56
62
|
log(`Gatherer: connecting to WS endpoint: ${wsEndpoint}`);
|
|
57
63
|
Gatherer.browser = await chromium.connect(wsEndpoint);
|
|
58
64
|
} else {
|
|
65
|
+
/**
|
|
66
|
+
* Chromium on Linux has its own NSS trust store. This flag does not add
|
|
67
|
+
* a CA to that store: it bypasses certificate errors only when a
|
|
68
|
+
* presented certificate's SPKI matches one of the supplied fingerprints.
|
|
69
|
+
* It is narrower than `ignoreHTTPSErrors`, but is not CA trust.
|
|
70
|
+
*/
|
|
71
|
+
const spkiFingerprints = getSpkiFingerprints(
|
|
72
|
+
options.certificateErrorSpkiAllowlist
|
|
73
|
+
);
|
|
74
|
+
if (spkiFingerprints.length > 0) {
|
|
75
|
+
log(
|
|
76
|
+
`Gatherer: bypassing certificate errors for ${spkiFingerprints.length} allowlisted SPKI fingerprint(s)`
|
|
77
|
+
);
|
|
78
|
+
}
|
|
59
79
|
Gatherer.browser = await chromium.launch({
|
|
60
80
|
...playwrightOptions,
|
|
61
81
|
args: [
|
|
62
82
|
...(playwrightOptions?.headless ? ['--disable-gpu'] : []),
|
|
83
|
+
...(spkiFingerprints.length > 0
|
|
84
|
+
? [
|
|
85
|
+
`--ignore-certificate-errors-spki-list=${spkiFingerprints.join(
|
|
86
|
+
','
|
|
87
|
+
)}`,
|
|
88
|
+
]
|
|
89
|
+
: []),
|
|
63
90
|
...(playwrightOptions?.args ?? []),
|
|
64
91
|
],
|
|
65
92
|
});
|
|
@@ -71,9 +98,19 @@ export class Gatherer {
|
|
|
71
98
|
});
|
|
72
99
|
}
|
|
73
100
|
|
|
74
|
-
static async setupDriver
|
|
75
|
-
|
|
101
|
+
static async setupDriver<T extends 'browser' | 'api' = 'browser'>(
|
|
102
|
+
options: RunOptions,
|
|
103
|
+
journeyType: T = 'browser' as T
|
|
104
|
+
): Promise<T extends 'api' ? APIDriver : Driver> {
|
|
76
105
|
const { playwrightOptions } = options;
|
|
106
|
+
|
|
107
|
+
if (journeyType === 'api') {
|
|
108
|
+
const request = await apiRequest.newContext({ ...playwrightOptions });
|
|
109
|
+
return { request } as T extends 'api' ? APIDriver : Driver;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
await Gatherer.launchBrowser(options);
|
|
113
|
+
|
|
77
114
|
const context = await Gatherer.browser.newContext({
|
|
78
115
|
...playwrightOptions,
|
|
79
116
|
userAgent: await Gatherer.getUserAgent(playwrightOptions?.userAgent),
|
|
@@ -96,7 +133,13 @@ export class Gatherer {
|
|
|
96
133
|
const page = await context.newPage();
|
|
97
134
|
const client = await context.newCDPSession(page);
|
|
98
135
|
const request = await apiRequest.newContext({ ...playwrightOptions });
|
|
99
|
-
return {
|
|
136
|
+
return {
|
|
137
|
+
browser: Gatherer.browser,
|
|
138
|
+
context,
|
|
139
|
+
page,
|
|
140
|
+
client,
|
|
141
|
+
request,
|
|
142
|
+
} as T extends 'api' ? APIDriver : Driver;
|
|
100
143
|
}
|
|
101
144
|
|
|
102
145
|
static async getUserAgent(userAgent?: string) {
|
|
@@ -136,14 +179,22 @@ export class Gatherer {
|
|
|
136
179
|
* Starts recording all events related to the v8 devtools protocol
|
|
137
180
|
* https://chromedevtools.github.io/devtools-protocol/v8/
|
|
138
181
|
*/
|
|
139
|
-
static async beginRecording(driver: Driver, options: RunOptions) {
|
|
182
|
+
static async beginRecording(driver: Driver | APIDriver, options: RunOptions) {
|
|
140
183
|
log('Gatherer: started recording');
|
|
141
184
|
const { network, metrics } = options;
|
|
142
185
|
Gatherer.pluginManager = new PluginManager(driver);
|
|
143
186
|
Gatherer.pluginManager.registerAll(options);
|
|
144
|
-
const plugins = [
|
|
145
|
-
|
|
146
|
-
|
|
187
|
+
const plugins = [];
|
|
188
|
+
if ('browser' in driver) {
|
|
189
|
+
plugins.push(await Gatherer.pluginManager.start('browserconsole'));
|
|
190
|
+
network && plugins.push(await Gatherer.pluginManager.start('network'));
|
|
191
|
+
metrics &&
|
|
192
|
+
plugins.push(await Gatherer.pluginManager.start('performance'));
|
|
193
|
+
} else {
|
|
194
|
+
// Network is the only data source for API journeys, so always capture
|
|
195
|
+
// it regardless of `options.network`.
|
|
196
|
+
plugins.push(await Gatherer.pluginManager.start('network'));
|
|
197
|
+
}
|
|
147
198
|
await Promise.all(plugins);
|
|
148
199
|
return Gatherer.pluginManager;
|
|
149
200
|
}
|
|
@@ -153,10 +204,12 @@ export class Gatherer {
|
|
|
153
204
|
await Gatherer.pluginManager.unregisterAll();
|
|
154
205
|
}
|
|
155
206
|
|
|
156
|
-
static async dispose(driver: Driver) {
|
|
207
|
+
static async dispose(driver: Driver | APIDriver) {
|
|
157
208
|
log(`Gatherer: closing all contexts`);
|
|
158
209
|
await driver.request.dispose();
|
|
159
|
-
|
|
210
|
+
if ('context' in driver) {
|
|
211
|
+
await driver.context.close();
|
|
212
|
+
}
|
|
160
213
|
}
|
|
161
214
|
|
|
162
215
|
static async stop() {
|
package/src/core/globals.ts
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
import Runner from './runner';
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
|
-
* Use a
|
|
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) => {
|
package/src/core/runner.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
-
*
|
|
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
|
|
339
|
-
|
|
340
|
-
journey.status
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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
|
-
|
|
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
|
}
|
|
@@ -438,6 +457,7 @@ export default class Runner implements RunnerInfo {
|
|
|
438
457
|
privateLocations: options.privateLocations,
|
|
439
458
|
params: options.params,
|
|
440
459
|
playwrightOptions: options.playwrightOptions,
|
|
460
|
+
certificateErrorSpkiAllowlist: options.certificateErrorSpkiAllowlist,
|
|
441
461
|
screenshot: options.screenshots,
|
|
442
462
|
tags: options.tags,
|
|
443
463
|
alert: options.alert,
|
|
@@ -516,9 +536,9 @@ export default class Runner implements RunnerInfo {
|
|
|
516
536
|
const { dryRun, grepOpts } = options;
|
|
517
537
|
|
|
518
538
|
// collect all journeys with `.only` annotation and skip the rest
|
|
519
|
-
const
|
|
520
|
-
if (
|
|
521
|
-
this.#journeys =
|
|
539
|
+
const onlyJourneys = this.#journeys.filter(j => j.only);
|
|
540
|
+
if (onlyJourneys.length > 0) {
|
|
541
|
+
this.#journeys = onlyJourneys;
|
|
522
542
|
} else {
|
|
523
543
|
// filter journeys based on tags and skip annotations
|
|
524
544
|
this.#journeys = this.#journeys.filter(
|
|
@@ -551,9 +571,16 @@ export default class Runner implements RunnerInfo {
|
|
|
551
571
|
|
|
552
572
|
async _runJourneys(options: RunOptions) {
|
|
553
573
|
const result: RunResult = {};
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
574
|
+
/**
|
|
575
|
+
* Only spin up a Chromium browser if at least one browser journey is
|
|
576
|
+
* scheduled to run. Pure API-journey suites skip launch entirely.
|
|
577
|
+
*/
|
|
578
|
+
const hasBrowserJourneys = this.#journeys.some(j => j.type === 'browser');
|
|
579
|
+
if (hasBrowserJourneys) {
|
|
580
|
+
const browserStart = monotonicTimeInSeconds();
|
|
581
|
+
await Gatherer.launchBrowser(options);
|
|
582
|
+
this.#browserDelay = monotonicTimeInSeconds() - browserStart;
|
|
583
|
+
}
|
|
557
584
|
|
|
558
585
|
for (const journey of this.#journeys) {
|
|
559
586
|
const journeyResult: JourneyResult = this.#hookError
|
|
@@ -578,7 +605,7 @@ export default class Runner implements RunnerInfo {
|
|
|
578
605
|
this.#journeys = [];
|
|
579
606
|
this.#active = false;
|
|
580
607
|
/**
|
|
581
|
-
* Clear all cache data stored for post
|
|
608
|
+
* Clear all cache data stored for post-processing by
|
|
582
609
|
* the current synthetic agent run
|
|
583
610
|
*/
|
|
584
611
|
await rm(CACHE_PATH, { recursive: true, force: true });
|