@elastic/synthetics
Version:
Elastic synthetic monitoring agent
223 lines (205 loc) • 7.45 kB
text/typescript
/**
* MIT License
*
* Copyright (c) 2020-present, Elastic NV
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
*/
import {
chromium,
ChromiumBrowser,
BrowserContext,
request as apiRequest,
selectors,
} from 'playwright-core';
import { PluginManager } from '../plugins';
import { log } from './logger';
import { getSpkiFingerprints } from './certs';
import {
APIDriver,
Driver,
NetworkConditions,
RunOptions,
} from '../common_types';
// Default timeout for Playwright actions and Navigations
const DEFAULT_TIMEOUT = 50000;
/**
* Purpose of the Gatherer is to set up the necessary browser driver
* related capabilities for the runner to run all journeys
*/
export class Gatherer {
static browser: ChromiumBrowser;
static pluginManager: PluginManager;
static async launchBrowser(options: RunOptions) {
if (Gatherer.browser != null) {
return;
}
log('Gatherer: setup browser');
const { wsEndpoint, playwrightOptions } = options;
if (wsEndpoint) {
log(`Gatherer: connecting to WS endpoint: ${wsEndpoint}`);
Gatherer.browser = await chromium.connect(wsEndpoint);
} else {
/**
* Chromium on Linux has its own NSS trust store. This flag does not add
* a CA to that store: it bypasses certificate errors only when a
* presented certificate's SPKI matches one of the supplied fingerprints.
* It is narrower than `ignoreHTTPSErrors`, but is not CA trust.
*/
const spkiFingerprints = getSpkiFingerprints(
options.certificateErrorSpkiAllowlist
);
if (spkiFingerprints.length > 0) {
log(
`Gatherer: bypassing certificate errors for ${spkiFingerprints.length} allowlisted SPKI fingerprint(s)`
);
}
Gatherer.browser = await chromium.launch({
...playwrightOptions,
args: [
...(playwrightOptions?.headless ? ['--disable-gpu'] : []),
...(spkiFingerprints.length > 0
? [
`--ignore-certificate-errors-spki-list=${spkiFingerprints.join(
','
)}`,
]
: []),
...(playwrightOptions?.args ?? []),
],
});
}
// Register sig int handler to close the browser
process.on('SIGINT', async () => {
await Gatherer.stop();
process.exit(130);
});
}
static async setupDriver<T extends 'browser' | 'api' = 'browser'>(
options: RunOptions,
journeyType: T = 'browser' as T
): Promise<T extends 'api' ? APIDriver : Driver> {
const { playwrightOptions } = options;
if (journeyType === 'api') {
const request = await apiRequest.newContext({ ...playwrightOptions });
return { request } as T extends 'api' ? APIDriver : Driver;
}
await Gatherer.launchBrowser(options);
const context = await Gatherer.browser.newContext({
...playwrightOptions,
userAgent: await Gatherer.getUserAgent(playwrightOptions?.userAgent),
});
// Set timeouts for actions and navigations
context.setDefaultTimeout(
playwrightOptions?.actionTimeout ?? DEFAULT_TIMEOUT
);
context.setDefaultNavigationTimeout(
playwrightOptions?.navigationTimeout ?? DEFAULT_TIMEOUT
);
// TODO: Network throttling via chrome devtools emulation is disabled for now.
// See docs/throttling.md for more details.
// Gatherer.setNetworkConditions(context, networkConditions);
if (playwrightOptions?.testIdAttribute) {
selectors.setTestIdAttribute(playwrightOptions.testIdAttribute);
}
const page = await context.newPage();
const client = await context.newCDPSession(page);
const request = await apiRequest.newContext({ ...playwrightOptions });
return {
browser: Gatherer.browser,
context,
page,
client,
request,
} as T extends 'api' ? APIDriver : Driver;
}
static async getUserAgent(userAgent?: string) {
if (!userAgent) {
const session = await Gatherer.browser.newBrowserCDPSession();
({ userAgent } = await session.send('Browser.getVersion'));
return userAgent + ' Elastic/Synthetics';
}
return userAgent;
}
static setNetworkConditions(
context: BrowserContext,
networkConditions: NetworkConditions
) {
if (networkConditions) {
context.on('page', page => {
const context = page.context();
const emulatePromise = context
.newCDPSession(page)
.then(client =>
client.send('Network.emulateNetworkConditions', networkConditions)
);
/**
* Guard against pages that gets closed before the emulation kicks to capture
* unhandled rejections from accessing the CDP session of closed page
*/
Promise.race([
new Promise<void>(resolve => page.on('close', () => resolve())),
emulatePromise,
]);
});
}
}
/**
* Starts recording all events related to the v8 devtools protocol
* https://chromedevtools.github.io/devtools-protocol/v8/
*/
static async beginRecording(driver: Driver | APIDriver, options: RunOptions) {
log('Gatherer: started recording');
const { network, metrics } = options;
Gatherer.pluginManager = new PluginManager(driver);
Gatherer.pluginManager.registerAll(options);
const plugins = [];
if ('browser' in driver) {
plugins.push(await Gatherer.pluginManager.start('browserconsole'));
network && plugins.push(await Gatherer.pluginManager.start('network'));
metrics &&
plugins.push(await Gatherer.pluginManager.start('performance'));
} else {
// Network is the only data source for API journeys, so always capture
// it regardless of `options.network`.
plugins.push(await Gatherer.pluginManager.start('network'));
}
await Promise.all(plugins);
return Gatherer.pluginManager;
}
static async endRecording() {
log('Gatherer: ended recording');
await Gatherer.pluginManager.unregisterAll();
}
static async dispose(driver: Driver | APIDriver) {
log(`Gatherer: closing all contexts`);
await driver.request.dispose();
if ('context' in driver) {
await driver.context.close();
}
}
static async stop() {
log(`Gatherer: closing browser`);
if (Gatherer.browser) {
await Gatherer.browser.close();
Gatherer.browser = null;
}
}
}