@elastic/synthetics
Version:
Elastic synthetic monitoring agent
235 lines • 8.96 kB
JavaScript
;
/**
* 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.
*
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.APINetworkManager = void 0;
const logger_1 = require("../core/logger");
const helpers_1 = require("../helpers");
const network_timings_1 = require("../network-timings");
/**
* Kibana UI expects the requestStartTime and loadEndTime to be baseline
* in seconds as they have the logic to convert it to milliseconds before
* using for offset calculation
*/
function epochTimeInSeconds() {
return (0, helpers_1.getTimestamp)() / 1e6;
}
function normalizeUrl(urlOrRequest) {
if (typeof urlOrRequest === 'string')
return urlOrRequest;
try {
return urlOrRequest.url();
}
catch {
return String(urlOrRequest);
}
}
// Best-effort request body size; returns 0 for shapes we can't measure.
function bodyBytes(body) {
if (body == null)
return 0;
if (typeof body === 'string')
return Buffer.byteLength(body);
if (Buffer.isBuffer(body))
return body.byteLength;
if (body instanceof Uint8Array)
return body.byteLength;
if (typeof body === 'object') {
try {
return Buffer.byteLength(JSON.stringify(body));
}
catch {
return 0;
}
}
return 0;
}
// Wire size of headers (`name: value\r\n`) for ECS `*.bytes`.
function headersBytes(headers) {
let total = 0;
for (const [k, v] of Object.entries(headers ?? {})) {
total += Buffer.byteLength(k) + Buffer.byteLength(String(v)) + 4; // ": " + CRLF
}
return total;
}
class APINetworkManager {
driver;
results = [];
_currentStep = null;
_originalFetch;
_patched = false;
constructor(driver) {
this.driver = driver;
}
async start() {
if (this._patched)
return;
(0, logger_1.log)(`Plugins: started collecting API network events`);
const request = this.driver.request;
this._originalFetch = request.fetch.bind(request);
request.fetch = (urlOrRequest, options) => this._interceptRequest(urlOrRequest, options);
this._patched = true;
}
async _interceptRequest(urlOrRequest, options) {
const url = normalizeUrl(urlOrRequest);
const timestamp = (0, helpers_1.getTimestamp)();
const requestSentTime = epochTimeInSeconds();
const httpMethod = (options?.method ?? 'GET').toUpperCase();
(0, logger_1.log)(`API network: ${httpMethod} ${url}`);
const requestBody = options?.postData ?? options?.data;
const requestBodyBytes = bodyBytes(requestBody);
const requestHeaders = options?.headers ?? {};
const entry = {
step: this._currentStep,
timestamp,
url,
type: 'fetch',
isNavigationRequest: false,
browser: { name: 'api', version: '' },
request: {
url,
method: httpMethod,
headers: requestHeaders,
bytes: headersBytes(requestHeaders) + requestBodyBytes,
body: requestBodyBytes > 0 ? { bytes: requestBodyBytes } : undefined,
},
response: {
status: -1,
headers: {},
mimeType: 'x-unknown',
},
requestSentTime,
loadEndTime: -1,
responseReceivedTime: -1,
resourceSize: 0,
transferSize: 0,
timings: {
blocked: -1,
dns: -1,
ssl: -1,
connect: -1,
send: -1,
wait: -1,
receive: -1,
total: -1,
},
};
this.results.push(entry);
const startTime = (0, helpers_1.now)();
try {
const response = (await this._originalFetch(urlOrRequest, options));
const headers = response.headers();
// Prefer `Content-Length`; fall back to the body buffer (chunked
// responses omit the header).
const contentLength = parseContentLength(headers['content-length']);
let responseBodyBytes = contentLength;
if (responseBodyBytes < 0) {
try {
const buf = await response.body();
responseBodyBytes = buf?.byteLength ?? 0;
}
catch {
responseBodyBytes = 0;
}
}
const responseHeaderBytes = headersBytes(headers);
const transferBytes = responseHeaderBytes + responseBodyBytes;
entry.response = {
url: response.url(),
status: response.status(),
statusText: response.statusText(),
headers,
mimeType: headers['content-type'] ?? 'x-unknown',
bytes: transferBytes,
body: { bytes: responseBodyBytes },
};
entry.transferSize = transferBytes;
entry.resourceSize = responseBodyBytes;
entry.responseReceivedTime = epochTimeInSeconds();
entry.loadEndTime = entry.responseReceivedTime;
// Per-phase Resource Timing (Playwright >= 1.62). Phases that did not
// happen (reused keep-alive socket) or are unknown (HAR replay) come
// back as `-1`; `calcTotalTime` then falls back to the wall-clock span.
const timing = response.timing();
entry.timings = (0, network_timings_1.getResourceTimings)(timing);
entry.timings.total = (0, network_timings_1.calcTotalTime)(entry, timing);
// Native (Playwright >= 1.61) TLS/socket info for the final hop;
// both resolve to `null` for non-HTTPS or unknown addresses.
const [serverAddr, securityDetails] = await Promise.all([
response.serverAddr(),
response.securityDetails(),
]);
if (serverAddr) {
entry.response.remoteIPAddress = serverAddr.ipAddress;
entry.response.remotePort = serverAddr.port;
}
if (securityDetails) {
entry.response.securityDetails = {
...securityDetails,
protocol: normalizeTLSProtocol(securityDetails.protocol),
};
}
return response;
}
catch (error) {
entry.responseReceivedTime = epochTimeInSeconds();
entry.loadEndTime = entry.responseReceivedTime;
entry.timings.total = (0, network_timings_1.roundMilliSecs)((0, helpers_1.now)() - startTime);
throw error;
}
}
// Drop the own-property `fetch` so the prototype method is reachable
// again, keeping the context usable after the plugin stops.
_restore() {
if (!this._patched)
return;
delete this.driver.request.fetch;
this._originalFetch = undefined;
this._patched = false;
}
async stop() {
this._restore();
(0, logger_1.log)(`Plugins: stopped collecting API network events`);
return this.results;
}
}
exports.APINetworkManager = APINetworkManager;
function parseContentLength(raw) {
if (!raw)
return -1;
const n = Number(raw);
return Number.isFinite(n) && n >= 0 ? n : -1;
}
/**
* `securityDetails()` reports `"TLSv1.3"` but the reporter splits on a
* space to derive `tls.version_protocol`/`tls.version`. Normalize to the
* spaced `"TLS 1.3"` shape so both journey types emit consistent fields.
*/
function normalizeTLSProtocol(raw) {
if (!raw)
return undefined;
return raw.replace(/\s*v(?=\d)/i, ' ');
}
//# sourceMappingURL=api-network.js.map