UNPKG

@elastic/synthetics

Version:

Elastic synthetic monitoring agent

394 lines 15.1 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. * */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.setGlobalProxy = exports.tagsMatch = exports.isMatch = exports.getProjectApiKeyURL = exports.getMonitorManagementURL = exports.removeTrailingSlash = exports.warn = exports.done = exports.error = exports.apiProgress = exports.liveProgress = exports.progress = exports.write = exports.safeNDJSONParse = exports.wrapFnWithLocation = exports.THROTTLING_WARNING_MSG = exports.getNetworkConditions = exports.DEFAULT_THROTTLING_OPTIONS = exports.megabitsToBytes = exports.getDurationInUs = exports.CACHE_PATH = exports.SYNTHETICS_PATH = exports.rewriteErrorStack = exports.rewriteErrorMessage = exports.findPWLogsIndexes = exports.totalist = exports.findPkgJsonByTraversing = exports.isFile = exports.isDirectory = exports.isDepInstalled = exports.runParallel = exports.now = exports.getTimestamp = exports.processStart = exports.microSecsToSeconds = exports.monotonicTimeInSeconds = exports.generateTempPath = exports.generateUniqueId = exports.symbols = exports.indent = exports.noop = void 0; const colors_1 = require("kleur/colors"); const os_1 = __importDefault(require("os")); const path_1 = require("path"); const fs_1 = __importDefault(require("fs")); const promises_1 = require("fs/promises"); const perf_hooks_1 = require("perf_hooks"); const source_map_support_1 = __importDefault(require("source-map-support")); const micromatch_1 = __importDefault(require("micromatch")); const undici_1 = require("undici"); const undici_2 = require("undici"); const SEPARATOR = '\n'; function noop() { } exports.noop = noop; function indent(lines, tab = ' ') { return lines.replace(/^/gm, tab); } exports.indent = indent; /** * Disable unicode symbols for windows, the underlying * FS stream has a known issue in windows */ const NO_UTF8_SUPPORT = process.platform === 'win32'; exports.symbols = { warning: (0, colors_1.yellow)(NO_UTF8_SUPPORT ? '!' : '⚠'), pending: (0, colors_1.yellow)(NO_UTF8_SUPPORT ? '!' : '⚠'), skipped: (0, colors_1.cyan)('-'), progress: (0, colors_1.cyan)('>'), succeeded: (0, colors_1.green)(NO_UTF8_SUPPORT ? 'ok' : '✓'), failed: (0, colors_1.red)(NO_UTF8_SUPPORT ? 'x' : '✖'), }; function generateUniqueId() { return `${Date.now() + Math.floor(Math.random() * 1e13)}`; } exports.generateUniqueId = generateUniqueId; function generateTempPath() { return (0, path_1.join)(os_1.default.tmpdir(), `synthetics-${generateUniqueId()}`); } exports.generateTempPath = generateTempPath; /** * Get Monotonically increasing time in seconds since * an arbitrary point in the past. * * We internally use the monotonically increasing clock timing * similar to the chrome devtools protocol network events for * journey,step start/end fields to make querying in the UI easier */ function monotonicTimeInSeconds() { const hrTime = process.hrtime(); // [seconds, nanoseconds] return hrTime[0] * 1 + hrTime[1] / 1e9; } exports.monotonicTimeInSeconds = monotonicTimeInSeconds; /** * Converts the trace events timestamp field from microsecond * resolution to monotonic seconds timestamp similar to other event types (journey, step, etc) * Reference - https://github.com/samccone/chrome-trace-event/blob/d45bc8af3b5c53a3adfa2c5fc107b4fae054f579/lib/trace-event.ts#L21-L22 * * Tested and verified on both Darwin and Linux */ function microSecsToSeconds(ts) { return ts / 1e6; } exports.microSecsToSeconds = microSecsToSeconds; /** * Timestamp at which the current node process began. */ exports.processStart = perf_hooks_1.performance.timeOrigin; function getTimestamp() { return (exports.processStart + now()) * 1000; } exports.getTimestamp = getTimestamp; /** * Relative current time from the start of the current node process */ function now() { return perf_hooks_1.performance.now(); } exports.now = now; /** * Execute all the hooks callbacks in parallel using Promise.all */ async function runParallel(callbacks, args) { const promises = callbacks.map(cb => cb(args)); return await Promise.all(promises); } exports.runParallel = runParallel; function isDepInstalled(dep) { try { return require.resolve(dep); } catch (e) { return false; } } exports.isDepInstalled = isDepInstalled; function isDirectory(path) { return fs_1.default.existsSync(path) && fs_1.default.statSync(path).isDirectory(); } exports.isDirectory = isDirectory; function isFile(filePath) { return fs_1.default.existsSync(filePath) && fs_1.default.statSync(filePath).isFile(); } exports.isFile = isFile; /** * Traverse the directory tree up from the cwd until we find * package.json file to check if the user is invoking our script * from an NPM project. */ function findPkgJsonByTraversing(resolvePath, cwd) { const packageJSON = (0, path_1.resolve)(resolvePath, 'package.json'); if (isFile(packageJSON)) { return packageJSON; } const parentDirectory = (0, path_1.dirname)(resolvePath); /** * We are in the system root and package.json does not exist */ if (resolvePath === parentDirectory) { throw (0, colors_1.red)(`Could not find package.json file in: "${cwd}"\n` + `It is recommended to run the agent in an NPM project.\n` + `You can create one by running "npm init -y" in the project folder.`); } return findPkgJsonByTraversing(parentDirectory, cwd); } exports.findPkgJsonByTraversing = findPkgJsonByTraversing; /** * Modified version of `totalist` package that handles the symlink issue * and avoids infinite recursion * * Based on code from totalist! * https://github.com/lukeed/totalist/blob/44379974e535afe9c38e8d643dd64c59101a14b9/src/async.js#L8 */ async function totalist(dir, callback, pre = '') { dir = (0, path_1.resolve)('.', dir); await (0, promises_1.readdir)(dir).then(arr => { return Promise.all(arr.map(str => { const abs = (0, path_1.join)(dir, str); return (0, promises_1.lstat)(abs).then(stats => stats.isDirectory() ? totalist(abs, callback, (0, path_1.join)(pre, str)) : callback((0, path_1.join)(pre, str), abs)); })); }); } exports.totalist = totalist; /** * Find index of Playwright specific Error logs that is thrown * as part of the custom error message/stack */ function findPWLogsIndexes(msgOrStack) { let startIndex = 0; let endIndex = 0; if (!msgOrStack) { return [startIndex, endIndex]; } const lines = String(msgOrStack).split(SEPARATOR); const logStart = /[=]{3,} logs [=]{3,}/; const logEnd = /[=]{10,}/; lines.forEach((line, index) => { if (logStart.test(line)) { startIndex = index; } else if (logEnd.test(line)) { endIndex = index; } }); return [startIndex, endIndex]; } exports.findPWLogsIndexes = findPWLogsIndexes; function rewriteErrorMessage(message, start) { if (start === 0) { return message; } return message.split(SEPARATOR).slice(0, start).join(SEPARATOR); } exports.rewriteErrorMessage = rewriteErrorMessage; function rewriteErrorStack(stack, indexes) { const [start, end] = indexes; /** * Do not rewrite if its not a playwright error */ if (start === 0 && end === 0) { return stack; } const linesToKeep = start + 3; if (start > 0 && linesToKeep < end) { const lines = stack.split(SEPARATOR); return lines .slice(0, linesToKeep) .concat(...lines.slice(end)) .join(SEPARATOR); } return stack; } exports.rewriteErrorStack = rewriteErrorStack; const cwd = process.cwd(); /** * All the settings that are related to the Synthetics is stored * under this directory * Examples: Screenshots, Project setup */ exports.SYNTHETICS_PATH = (0, path_1.join)(cwd, '.synthetics'); /** * Synthetics cache path that is based on the process id to make sure * each process does not modify the caching layer used by other process * once we move to executing journeys in parallel */ exports.CACHE_PATH = (0, path_1.join)(exports.SYNTHETICS_PATH, process.pid.toString()); function getDurationInUs(duration) { return Math.trunc(duration * 1e6); } exports.getDurationInUs = getDurationInUs; function megabitsToBytes(megabytes) { return (megabytes * 1024 * 1024) / 8; } exports.megabitsToBytes = megabitsToBytes; exports.DEFAULT_THROTTLING_OPTIONS = { download: 5, upload: 3, latency: 20, }; /** * Transforms the CLI throttling arguments in to format * expected by Chrome devtools protocol NetworkConditions */ function getNetworkConditions(throttlingOpts) { return { downloadThroughput: megabitsToBytes(throttlingOpts.download), uploadThroughput: megabitsToBytes(throttlingOpts.upload), latency: throttlingOpts.latency, offline: false, }; } exports.getNetworkConditions = getNetworkConditions; exports.THROTTLING_WARNING_MSG = `Throttling may not be active when the tests run - see https://github.com/elastic/synthetics/blob/main/docs/throttling.md for more details`; const dstackTraceLimit = 10; // Uses the V8 Stacktrace API to get the function location // information - https://v8.dev/docs/stack-trace-api#customizing-stack-traces function wrapFnWithLocation(func) { return (...args) => { const _prepareStackTrace = Error.prepareStackTrace; Error.prepareStackTrace = (_, stackFrames) => { // Deafult CallSite would not map to the original transpiled source // correctly, So we use source-map-support to map the CallSite to the // original source from our cached source map const frame = source_map_support_1.default.wrapCallSite(stackFrames[1]); return { file: frame.getFileName(), line: frame.getLineNumber(), column: frame.getColumnNumber(), }; }; Error.stackTraceLimit = 2; const obj = {}; Error.captureStackTrace(obj); const location = obj.stack; Error.stackTraceLimit = dstackTraceLimit; Error.prepareStackTrace = _prepareStackTrace; return func(location, ...args); }; } exports.wrapFnWithLocation = wrapFnWithLocation; // Safely parse ND JSON (Newline delimited JSON) chunks function safeNDJSONParse(data) { // data may not be at proper newline boundaries, so we make sure everything is split // on proper newlines const chunks = Array.isArray(data) ? data : [data]; const lines = chunks.join('\n').split(/\r?\n/); return lines .filter(l => l.match(/\S/)) // remove blank lines .map(line => { try { return JSON.parse(line); } catch (e) { throw `Error ${e} could not parse data '${line}'`; } }); } exports.safeNDJSONParse = safeNDJSONParse; // Console helpers function write(message, live) { process.stderr.write(message + (live ? '\r' : '\n')); } exports.write = write; function progress(message) { write((0, colors_1.cyan)((0, colors_1.bold)(`${exports.symbols.progress} ${message}`))); } exports.progress = progress; async function liveProgress(promise, message) { const start = now(); const interval = setInterval(() => { apiProgress(`${message} (${Math.trunc(now() - start)}ms)`, true); }, 500); promise.finally(() => clearInterval(interval)); const result = await promise; apiProgress(`${message} (${Math.trunc(now() - start)}ms)`); return result; } exports.liveProgress = liveProgress; function apiProgress(message, live = false) { write((0, colors_1.grey)(`> ${message}`), live); } exports.apiProgress = apiProgress; function error(message) { write((0, colors_1.red)(message)); } exports.error = error; function done(message) { write((0, colors_1.bold)((0, colors_1.green)(`${exports.symbols['succeeded']} ${message}`))); } exports.done = done; function warn(message) { write((0, colors_1.bold)((0, colors_1.yellow)(`${exports.symbols['warning']} ${message}`))); } exports.warn = warn; function removeTrailingSlash(url = '') { return url.replace(/\/+$/, ''); } exports.removeTrailingSlash = removeTrailingSlash; function getMonitorManagementURL(url) { return removeTrailingSlash(url) + '/app/synthetics/monitors'; } exports.getMonitorManagementURL = getMonitorManagementURL; function getProjectApiKeyURL(url) { return removeTrailingSlash(url) + '/app/synthetics/settings/api-keys'; } exports.getProjectApiKeyURL = getProjectApiKeyURL; /** * Matches tests based on the provided args. Proitize tags over match * - tags pattern that matches only tags * - match pattern that matches both name and tags */ function isMatch(tags, name, tagsPattern, matchPattern) { if (tagsPattern) { return tagsMatch(tags, tagsPattern); } if (matchPattern) { return (micromatch_1.default.isMatch(name, matchPattern) || tagsMatch(tags, matchPattern)); } return true; } exports.isMatch = isMatch; function tagsMatch(tags, pattern) { const matchess = (0, micromatch_1.default)(tags || ['*'], pattern); return matchess.length > 0; } exports.tagsMatch = tagsMatch; function setGlobalProxy(opts) { // Create proxy agent if options exist, if not attempt to load env proxy const proxyAgent = opts.uri ? new undici_1.ProxyAgent({ uri: opts.uri, token: opts.token, proxyTls: { ca: opts.ca ?? null, cert: opts.cert ?? null, rejectUnauthorized: !opts.noVerify, }, }) : new undici_2.EnvHttpProxyAgent(); (0, undici_1.setGlobalDispatcher)(proxyAgent); } exports.setGlobalProxy = setGlobalProxy; //# sourceMappingURL=helpers.js.map