UNPKG

@elastic/synthetics

Version:

Elastic synthetic monitoring agent

235 lines 8.96 kB
"use strict"; /** * 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