UNPKG

@wdio/browserstack-service

Version:
1,310 lines (1,300 loc) 520 kB
var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __esm = (fn, res) => function __init() { return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res; }; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __decorateClass = (decorators, target, key, kind) => { var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target; for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result; if (kind && result) __defProp(target, key, result); return result; }; // package.json var package_default; var init_package = __esm({ "package.json"() { package_default = { name: "@wdio/browserstack-service", version: "9.23.3", description: "WebdriverIO service for better Browserstack integration", author: "Adam Bjerstedt <abjerstedt@gmail.com>", homepage: "https://github.com/webdriverio/webdriverio/tree/main/packages/wdio-browserstack-service", license: "MIT", engines: { node: ">=18.20.0" }, repository: { type: "git", url: "git+https://github.com/webdriverio/webdriverio.git", directory: "packages/wdio-browserstack-service" }, keywords: [ "webdriverio", "wdio", "browserstack", "wdio-service" ], bugs: { url: "https://github.com/webdriverio/webdriverio/issues" }, type: "module", types: "./build/index.d.ts", exports: { ".": { types: "./build/index.d.ts", import: "./build/index.js" }, "./cleanup": { import: "./build/cleanup.js", source: "./src/cleanup.ts" } }, typeScriptVersion: "3.8.3", dependencies: { "@browserstack/ai-sdk-node": "1.5.17", "@browserstack/wdio-browserstack-service": "^2.0.2", "@percy/appium-app": "^2.0.9", "@percy/selenium-webdriver": "^2.2.2", "@types/gitconfiglocal": "^2.0.1", "@wdio/logger": "workspace:*", "@wdio/reporter": "workspace:*", "@wdio/types": "workspace:*", "browserstack-local": "^1.5.1", chalk: "^5.3.0", "csv-writer": "^1.6.0", "formdata-node": "5.0.1", "git-repo-info": "^2.1.1", gitconfiglocal: "^2.1.0", glob: "^11.0.0", tar: "^7.5.7", undici: "^6.21.3", uuid: "^11.1.0", webdriverio: "workspace:*", "winston-transport": "^4.5.0", yauzl: "^3.0.0" }, peerDependencies: { "@wdio/cli": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0" }, devDependencies: { "@types/node": "^20.1.0", "@types/tar": "^6.1.13", "@types/yauzl": "^2.10.3", "@wdio/globals": "workspace:*" }, publishConfig: { access: "public" } }; } }); // src/constants.ts var bstackServiceVersion, BROWSER_DESCRIPTION, VALID_APP_EXTENSION, DEFAULT_OPTIONS, consoleHolder, APP_ALLY_ISSUES_ENDPOINT, APP_ALLY_ISSUES_SUMMARY_ENDPOINT, DATA_EVENT_ENDPOINT, DATA_BATCH_ENDPOINT, DATA_SCREENSHOT_ENDPOINT, DATA_BATCH_SIZE, DATA_BATCH_INTERVAL, DEFAULT_WAIT_TIMEOUT_FOR_PENDING_UPLOADS, DEFAULT_WAIT_INTERVAL_FOR_PENDING_UPLOADS, BSTACK_SERVICE_VERSION, NOT_ALLOWED_KEYS_IN_CAPS, LOGS_FILE, CLI_DEBUG_LOGS_FILE, UPLOAD_LOGS_ENDPOINT, PERCY_LOGS_FILE, PERCY_DOM_CHANGING_COMMANDS_ENDPOINTS, CAPTURE_MODES, LOG_KIND_USAGE_MAP, SUPPORTED_BROWSERS_FOR_AI, TCG_URL, TCG_INFO, SMART_SELECTION_MODE_RELEVANT_FIRST, SMART_SELECTION_MODE_RELEVANT_ONLY, BROWSERSTACK_TESTHUB_JWT, BSTACK_TCG_AUTH_RESULT, TESTOPS_SCREENSHOT_ENV, BROWSERSTACK_TESTHUB_UUID, TEST_ANALYTICS_ID, PERF_MEASUREMENT_ENV, RERUN_TESTS_ENV, RERUN_ENV, TESTOPS_BUILD_COMPLETED_ENV, BROWSERSTACK_PERCY, BROWSERSTACK_ACCESSIBILITY, BROWSERSTACK_OBSERVABILITY, BROWSERSTACK_TEST_REPORTING, TEST_REPORTING_PROJECT_NAME, MAX_GIT_META_DATA_SIZE_IN_BYTES, GIT_META_DATA_TRUNCATED, CLI_STOP_TIMEOUT, WDIO_NAMING_PREFIX, UPDATED_CLI_ENDPOINT; var init_constants = __esm({ "src/constants.ts"() { "use strict"; init_package(); bstackServiceVersion = package_default.version; BROWSER_DESCRIPTION = [ "device", "os", "osVersion", "os_version", "browserName", "browser", "browserVersion", "browser_version" ]; VALID_APP_EXTENSION = [ ".apk", ".aab", ".ipa" ]; DEFAULT_OPTIONS = { setSessionName: true, setSessionStatus: true, testObservability: true, accessibility: false }; consoleHolder = Object.assign({}, console); APP_ALLY_ISSUES_ENDPOINT = "api/v1/issues"; APP_ALLY_ISSUES_SUMMARY_ENDPOINT = "api/v1/issues-summary"; DATA_EVENT_ENDPOINT = "api/v1/event"; DATA_BATCH_ENDPOINT = "api/v1/batch"; DATA_SCREENSHOT_ENDPOINT = "api/v1/screenshots"; DATA_BATCH_SIZE = 1e3; DATA_BATCH_INTERVAL = 2e3; DEFAULT_WAIT_TIMEOUT_FOR_PENDING_UPLOADS = 5e3; DEFAULT_WAIT_INTERVAL_FOR_PENDING_UPLOADS = 100; BSTACK_SERVICE_VERSION = bstackServiceVersion; NOT_ALLOWED_KEYS_IN_CAPS = ["includeTagsInTestingScope", "excludeTagsInTestingScope"]; LOGS_FILE = "logs/bstack-wdio-service.log"; CLI_DEBUG_LOGS_FILE = "log/sdk-cli-debug.log"; UPLOAD_LOGS_ENDPOINT = "client-logs/upload"; PERCY_LOGS_FILE = "logs/percy.log"; PERCY_DOM_CHANGING_COMMANDS_ENDPOINTS = [ "/session/:sessionId/url", "/session/:sessionId/forward", "/session/:sessionId/back", "/session/:sessionId/refresh", "/session/:sessionId/screenshot", "/session/:sessionId/actions", "/session/:sessionId/appium/device/shake" ]; CAPTURE_MODES = ["click", "auto", "screenshot", "manual", "testcase"]; LOG_KIND_USAGE_MAP = { "TEST_LOG": "log", "TEST_SCREENSHOT": "screenshot", "TEST_STEP": "step", "HTTP": "http" }; SUPPORTED_BROWSERS_FOR_AI = ["chrome", "microsoftedge", "firefox"]; TCG_URL = "https://tcg.browserstack.com"; TCG_INFO = { tcgRegion: "use", tcgUrl: TCG_URL }; SMART_SELECTION_MODE_RELEVANT_FIRST = "relevantFirst"; SMART_SELECTION_MODE_RELEVANT_ONLY = "relevantOnly"; BROWSERSTACK_TESTHUB_JWT = "BROWSERSTACK_TESTHUB_JWT"; BSTACK_TCG_AUTH_RESULT = "BSTACK_TCG_AUTH_RESULT"; TESTOPS_SCREENSHOT_ENV = "BS_TESTOPS_ALLOW_SCREENSHOTS"; BROWSERSTACK_TESTHUB_UUID = "BROWSERSTACK_TESTHUB_UUID"; TEST_ANALYTICS_ID = "TEST_ANALYTICS_ID"; PERF_MEASUREMENT_ENV = "BROWSERSTACK_O11Y_PERF_MEASUREMENT"; RERUN_TESTS_ENV = "BROWSERSTACK_RERUN_TESTS"; RERUN_ENV = "BROWSERSTACK_RERUN"; TESTOPS_BUILD_COMPLETED_ENV = "BS_TESTOPS_BUILD_COMPLETED"; BROWSERSTACK_PERCY = "BROWSERSTACK_PERCY"; BROWSERSTACK_ACCESSIBILITY = "BROWSERSTACK_ACCESSIBILITY"; BROWSERSTACK_OBSERVABILITY = "BROWSERSTACK_OBSERVABILITY"; BROWSERSTACK_TEST_REPORTING = "BROWSERSTACK_TEST_REPORTING"; TEST_REPORTING_PROJECT_NAME = "TEST_REPORTING_PROJECT_NAME"; MAX_GIT_META_DATA_SIZE_IN_BYTES = 64 * 1024; GIT_META_DATA_TRUNCATED = "...[TRUNCATED]"; CLI_STOP_TIMEOUT = 5e3; WDIO_NAMING_PREFIX = "WebdriverIO-"; UPDATED_CLI_ENDPOINT = "sdk/v1/update_cli"; } }); // src/logPatcher.ts import Transport from "winston-transport"; var LOG_LEVELS, logPatcher, logPatcher_default; var init_logPatcher = __esm({ "src/logPatcher.ts"() { "use strict"; LOG_LEVELS = { INFO: "INFO", ERROR: "ERROR", DEBUG: "DEBUG", TRACE: "TRACE", WARN: "WARN" }; logPatcher = class extends Transport { logToTestOps = (level = LOG_LEVELS.INFO, message = [""]) => { process.emit(`bs:addLog:${process.pid}`, { timestamp: (/* @__PURE__ */ new Date()).toISOString(), level: level.toUpperCase(), message: `"${message.join(", ")}"`, kind: "TEST_LOG", http_response: {} }); }; /* Patching this would show user an extended trace on their cli */ trace = (...message) => { this.logToTestOps(LOG_LEVELS.TRACE, message); }; debug = (...message) => { this.logToTestOps(LOG_LEVELS.DEBUG, message); }; info = (...message) => { this.logToTestOps(LOG_LEVELS.INFO, message); }; warn = (...message) => { this.logToTestOps(LOG_LEVELS.WARN, message); }; error = (...message) => { this.logToTestOps(LOG_LEVELS.ERROR, message); }; log = (...message) => { this.logToTestOps(LOG_LEVELS.INFO, message); }; }; logPatcher_default = logPatcher; } }); // src/cli/apiUtils.ts var APIUtils; var init_apiUtils = __esm({ "src/cli/apiUtils.ts"() { "use strict"; APIUtils = class { static FUNNEL_INSTRUMENTATION_URL = "https://api.browserstack.com/sdk/v1/event"; static BROWSERSTACK_AUTOMATE_API_URL = "https://api.browserstack.com"; static BROWSERSTACK_AA_API_URL = "https://api.browserstack.com"; static BROWSERSTACK_PERCY_API_URL = "https://api.browserstack.com"; static BROWSERSTACK_AUTOMATE_API_CLOUD_URL = "https://api-cloud.browserstack.com"; static BROWSERSTACK_AA_API_CLOUD_URL = "https://api-cloud.browserstack.com"; static APP_ALLY_ENDPOINT = "https://app-accessibility.browserstack.com/automate"; static DATA_ENDPOINT = "https://collector-observability.browserstack.com"; static UPLOAD_LOGS_ADDRESS = "https://upload-observability.browserstack.com"; static EDS_URL = "https://eds.browserstack.com"; static updateURLSForGRR(apis) { this.FUNNEL_INSTRUMENTATION_URL = `${apis.automate.api}/sdk/v1/event`; this.BROWSERSTACK_AUTOMATE_API_URL = apis.automate.api; this.BROWSERSTACK_AA_API_URL = apis.appAutomate.api; this.BROWSERSTACK_PERCY_API_URL = apis.percy.api; this.BROWSERSTACK_AUTOMATE_API_CLOUD_URL = apis.automate.upload; this.BROWSERSTACK_AA_API_CLOUD_URL = apis.appAutomate.upload; this.APP_ALLY_ENDPOINT = `${apis.appAccessibility.api}/automate`; this.DATA_ENDPOINT = apis.observability.api; this.UPLOAD_LOGS_ADDRESS = apis.observability.upload; this.EDS_URL = apis.edsInstrumentation.api; } }; } }); // src/fetchWrapper.ts import { fetch as undiciFetch, ProxyAgent } from "undici"; async function fetchWrap(input, init) { const res = await _fetch(input, init); if (!res.ok) { throw new ResponseError(`Error response from server ${res.status}: ${await res.text()}`, res); } return res; } function _fetch(input, init) { const proxyUrl = process.env.HTTP_PROXY || process.env.HTTPS_PROXY; if (proxyUrl) { const noProxy = process.env.NO_PROXY && process.env.NO_PROXY.trim() ? process.env.NO_PROXY.trim().split(/[\s,;]+/) : []; const request = new Request(input); const url3 = new URL(request.url); if (!noProxy.some((str) => url3.hostname.endsWith(str))) { return undiciFetch( request.url, { ...init, dispatcher: new ProxyAgent(proxyUrl) } ); } } return fetch(input, init); } var ResponseError; var init_fetchWrapper = __esm({ "src/fetchWrapper.ts"() { "use strict"; ResponseError = class extends Error { response; constructor(message, res) { super(message); this.response = res; } }; } }); // src/instrumentation/performance/constants.ts var EVENTS, TESTHUB_EVENTS, AUTOMATE_EVENTS, A11Y_EVENTS, PERCY_EVENTS, O11Y_EVENTS, HOOK_EVENTS, TURBOSCALE_EVENTS, APP_AUTOMATE_EVENTS, DRIVER_EVENT, FRAMEWORK_EVENTS, CONFIG_EVENTS, AI_EVENTS, DISPATCHER_EVENTS; var init_constants2 = __esm({ "src/instrumentation/performance/constants.ts"() { "use strict"; EVENTS = { SDK_SETUP: "sdk:setup", SDK_CLEANUP: "sdk:cleanup", SDK_PRE_TEST: "sdk:pre-test", SDK_TEST: "sdk:test", SDK_POST_TEST: "sdk:post-test", SDK_HOOK: "sdk:hook", SDK_DRIVER: "sdk:driver", SDK_A11Y: "sdk:a11y", SDK_O11Y: "sdk:o11y", SDK_AUTO_CAPTURE: "sdk:auto-capture", SDK_PROXY_SETUP: "sdk:proxy-setup", SDK_TESTHUB: "sdk:testhub", SDK_AUTOMATE: "sdk:automate", SDK_APP_AUTOMATE: "sdk:app-automate", SDK_TURBOSCALE: "sdk:turboscale", SDK_PERCY: "sdk:percy", SDK_PRE_INITIALIZE: "sdk:driver:pre-initialization", SDK_POST_INITIALIZE: "sdk:driver:post-initialization", SDK_CLI_CHECK_UPDATE: "sdk:cli:check-update", SDK_CLI_DOWNLOAD: "sdk:cli:download", SDK_CLI_ON_BOOTSTRAP: "sdk:cli:on-bootstrap", SDK_CLI_ON_CONNECT: "sdk:cli:on-connect", SDK_CLI_START: "sdk:cli:start", SDK_CLI_ON_STOP: "sdk:cli:on-stop", SDK_CONNECT_BIN_SESSION: "sdk:connectBinSession", SDK_START_BIN_SESSION: "sdk:startBinSession", // New events from Python SDK SDK_DRIVER_INIT: "sdk:driverInit", SDK_FIND_NEAREST_HUB: "sdk:findNearestHub", SDK_AUTOMATION_FRAMEWORK_INIT: "sdk:automationFrameworkInit", SDK_AUTOMATION_FRAMEWORK_START: "sdk:automationFrameworkStart", SDK_AUTOMATION_FRAMEWORK_STOP: "sdk:automationFrameworkStop", SDK_ACCESSIBILITY_CONFIG: "sdk:accessibilityConfig", SDK_OBSERVABILITY_CONFIG: "sdk:observabilityConfig", SDK_AI_SELF_HEAL_STEP: "sdk:aiSelfHealStep", SDK_AI_SELF_HEAL_GET_RESULT: "sdk:aiSelfHealGetResult", SDK_TEST_FRAMEWORK_EVENT: "sdk:testFrameworkEvent", SDK_TEST_SESSION_EVENT: "sdk:testSessionEvent", SDK_CLI_LOG_CREATED_EVENT: "sdk:cli:logCreatedEvent", SDK_CLI_ENQUEUE_TEST_EVENT: "sdk:cli:enqueueTestEvent", SDK_ON_STOP: "sdk:onStop", SDK_SEND_LOGS: "sdk:sendlogs", // Funnel Events SDK_FUNNEL_TEST_ATTEMPTED: "sdk:funnel:test-attempted", SDK_FUNNEL_TEST_SUCCESSFUL: "sdk:funnel:test-successful", // Log Upload Events SDK_UPLOAD_LOGS: "sdk:upload-logs", // Key Metrics Events SDK_SEND_KEY_METRICS: "sdk:send-key-metrics", SDK_KEY_METRICS_PREPARATION: "sdk:key-metrics:preparation", SDK_KEY_METRICS_UPLOAD: "sdk:key-metrics:upload", // CLI Binary Events SDK_CLI_DOWNLOAD_BINARY: "sdk:cli:download-binary", SDK_CLI_BINARY_VERIFICATION: "sdk:cli:binary-verification", // Cleanup & Shutdown Events (tracking gap between driver:quit and funnel:test-successful) SDK_LISTENER_WORKER_END: "sdk:listener:worker-end", SDK_PERCY_TEARDOWN: "sdk:percy:teardown", SDK_WORKER_SAVE_DATA: "sdk:worker:save-data", SDK_PERFORMANCE_REPORT_GEN: "sdk:performance:report-generation", SDK_PERFORMANCE_JSON_WRITE: "sdk:performance:json-write", SDK_PERFORMANCE_HTML_GEN: "sdk:performance:html-generation", // Device Allocation Event (tracking gap between beforeSession and before hooks) SDK_DEVICE_ALLOCATION: "sdk:device-allocation" }; TESTHUB_EVENTS = { START: `${EVENTS.SDK_TESTHUB}:start`, STOP: `${EVENTS.SDK_TESTHUB}:stop` }; AUTOMATE_EVENTS = { KEEP_ALIVE: `${EVENTS.SDK_AUTOMATE}:keep-alive`, HUB_MANAGEMENT: `${EVENTS.SDK_AUTOMATE}:hub-management`, LOCAL_START: `${EVENTS.SDK_AUTOMATE}:local-start`, LOCAL_STOP: `${EVENTS.SDK_AUTOMATE}:local-stop`, DRIVER_MANAGE: `${EVENTS.SDK_AUTOMATE}:driver-manage`, SESSION_NAME: `${EVENTS.SDK_AUTOMATE}:session-name`, SESSION_STATUS: `${EVENTS.SDK_AUTOMATE}:session-status`, SESSION_ANNOTATION: `${EVENTS.SDK_AUTOMATE}:session-annotation`, IDLE_TIMEOUT: `${EVENTS.SDK_AUTOMATE}:idle-timeout`, GENERATE_CI_ARTIFACT: `${EVENTS.SDK_AUTOMATE}:ci-artifacts`, PRINT_BUILDLINK: `${EVENTS.SDK_AUTOMATE}:print-buildlink` }; A11Y_EVENTS = { PERFORM_SCAN: `${EVENTS.SDK_A11Y}:driver-performscan`, SAVE_RESULTS: `${EVENTS.SDK_A11Y}:save-results`, GET_RESULTS: `${EVENTS.SDK_A11Y}:get-accessibility-results`, GET_RESULTS_SUMMARY: `${EVENTS.SDK_A11Y}:get-accessibility-results-summary` }; PERCY_EVENTS = { DOWNLOAD: `${EVENTS.SDK_PERCY}:download`, SCREENSHOT: `${EVENTS.SDK_PERCY}:screenshot`, START: `${EVENTS.SDK_PERCY}:start`, STOP: `${EVENTS.SDK_PERCY}:stop`, AUTO_CAPTURE: `${EVENTS.SDK_PERCY}:auto-capture`, SNAPSHOT: `${EVENTS.SDK_PERCY}:snapshot`, SCREENSHOT_APP: `${EVENTS.SDK_PERCY}:screenshot-app` }; O11Y_EVENTS = { SYNC: `${EVENTS.SDK_O11Y}:sync`, TAKE_SCREENSHOT: `${EVENTS.SDK_O11Y}:driver-takeScreenShot`, PRINT_BUILDLINK: `${EVENTS.SDK_O11Y}:print-buildlink` }; HOOK_EVENTS = { BEFORE_EACH: `${EVENTS.SDK_HOOK}:before-each`, AFTER_EACH: `${EVENTS.SDK_HOOK}:after-each`, AFTER_ALL: `${EVENTS.SDK_HOOK}:after-all`, BEFORE_ALL: `${EVENTS.SDK_HOOK}:before-all`, BEFORE: `${EVENTS.SDK_HOOK}:before`, AFTER: `${EVENTS.SDK_HOOK}:after` }; TURBOSCALE_EVENTS = { HUB_MANAGEMENT: `${EVENTS.SDK_TURBOSCALE}:hub-management`, PRINT_BUILDLINK: `${EVENTS.SDK_TURBOSCALE}:print-buildlink` }; APP_AUTOMATE_EVENTS = { APP_UPLOAD: `${EVENTS.SDK_APP_AUTOMATE}:app-upload` }; DRIVER_EVENT = { QUIT: `${EVENTS.SDK_DRIVER}:quit`, GET: `${EVENTS.SDK_DRIVER}:get`, PRE_EXECUTE: `${EVENTS.SDK_DRIVER}:pre-execute`, POST_EXECUTE: `${EVENTS.SDK_DRIVER}:post-execute`, INIT: EVENTS.SDK_DRIVER_INIT, PRE_INITIALIZE: EVENTS.SDK_PRE_INITIALIZE, POST_INITIALIZE: EVENTS.SDK_POST_INITIALIZE }; FRAMEWORK_EVENTS = { INIT: EVENTS.SDK_AUTOMATION_FRAMEWORK_INIT, START: EVENTS.SDK_AUTOMATION_FRAMEWORK_START, STOP: EVENTS.SDK_AUTOMATION_FRAMEWORK_STOP }; CONFIG_EVENTS = { ACCESSIBILITY: EVENTS.SDK_ACCESSIBILITY_CONFIG, OBSERVABILITY: EVENTS.SDK_OBSERVABILITY_CONFIG }; AI_EVENTS = { SELF_HEAL_STEP: EVENTS.SDK_AI_SELF_HEAL_STEP, SELF_HEAL_GET_RESULT: EVENTS.SDK_AI_SELF_HEAL_GET_RESULT }; DISPATCHER_EVENTS = { TEST_FRAMEWORK: EVENTS.SDK_TEST_FRAMEWORK_EVENT, TEST_SESSION: EVENTS.SDK_TEST_SESSION_EVENT, LOG_CREATED: EVENTS.SDK_CLI_LOG_CREATED_EVENT, ENQUEUE_TEST: EVENTS.SDK_CLI_ENQUEUE_TEST_EVENT }; } }); // src/cli/cliLogger.ts import path from "node:path"; import fs from "node:fs"; import chalk from "chalk"; import logger from "@wdio/logger"; var log, BStackLogger; var init_cliLogger = __esm({ "src/cli/cliLogger.ts"() { "use strict"; init_constants(); init_util(); log = logger("@wdio/browserstack-service/cli"); BStackLogger = class { static logFilePath = path.join(process.cwd(), LOGS_FILE); static logFolderPath = path.join(process.cwd(), "logs"); static logFileStream; static logToFile(logMessage, logLevel) { try { if (!this.logFileStream) { this.ensureLogsFolder(); this.logFileStream = fs.createWriteStream(this.logFilePath, { flags: "a" }); } if (this.logFileStream && this.logFileStream.writable) { this.logFileStream.write(this.formatLog(logMessage, logLevel)); } } catch (error) { log.debug(`Failed to log to file. Error ${error}`); } } static formatLog(logMessage, level) { return `${chalk.gray((/* @__PURE__ */ new Date()).toISOString())} ${chalk[COLORS[level]](level.toUpperCase())} ${chalk.whiteBright("@wdio/browserstack-service")} ${logMessage} `; } static info(message) { this.logToFile(message, "info"); log.info(message); } static error(message) { this.logToFile(message, "error"); log.error(message); } static debug(message, param) { this.logToFile(message, "debug"); if (param) { log.debug(message, param); } else { log.debug(message); } } static warn(message) { this.logToFile(message, "warn"); log.warn(message); } static trace(message) { this.logToFile(message, "trace"); log.trace(message); } static clearLogger() { if (this.logFileStream) { this.logFileStream.end(); } this.logFileStream = null; } static clearLogFile() { if (fs.existsSync(this.logFilePath)) { fs.truncateSync(this.logFilePath); } } static ensureLogsFolder() { if (!fs.existsSync(this.logFolderPath)) { fs.mkdirSync(this.logFolderPath); } } }; } }); // src/cli/frameworks/constants/testFrameworkConstants.ts var TestFrameworkConstants; var init_testFrameworkConstants = __esm({ "src/cli/frameworks/constants/testFrameworkConstants.ts"() { "use strict"; TestFrameworkConstants = { KEY_TEST_UUID: "test_uuid", KEY_TEST_ID: "test_id", KEY_TEST_NAME: "test_name", KEY_TEST_FILE_PATH: "test_file_path", KEY_TEST_TAGS: "test_tags", KEY_TEST_RESULT: "test_result", KEY_TEST_RESULT_AT: "test_result_at", KEY_TEST_STARTED_AT: "test_started_at", KEY_TEST_ENDED_AT: "test_ended_at", KEY_TEST_LOCATION: "test_location", KEY_TEST_SCOPE: "test_scope", KEY_TEST_SCOPES: "test_scopes", KEY_TEST_FRAMEWORK_NAME: "test_framework_name", KEY_TEST_FRAMEWORK_VERSION: "test_framework_version", KEY_TEST_CODE: "test_code", KEY_TEST_RERUN_NAME: "test_rerun_name", KEY_PLATFORM_INDEX: "platform_index", KEY_TEST_FAILURE: "test_failure", KEY_TEST_FAILURE_TYPE: "test_failure_type", KEY_TEST_FAILURE_REASON: "test_failure_reason", KEY_TEST_LOGS: "test_logs", KEY_TEST_META: "test_meta", KEY_TEST_DEFERRED: "test_deferred", KEY_SESSION_NAME: "test_session_name", KEY_AUTOMATE_SESSION_NAME: "automate_session_name", KEY_AUTOMATE_SESSION_STATUS: "automate_session_status", KEY_AUTOMATE_SESSION_REASON: "automate_session_reason", KEY_EVENT_STARTED_AT: "event_started_at", KEY_EVENT_ENDED_AT: "event_ended_at", KEY_HOOK_ID: "hook_id", KEY_HOOK_RESULT: "hook_result", KEY_HOOK_LOGS: "hook_logs", KEY_HOOK_NAME: "hook_name", KEY_HOOKS_STARTED: "test_hooks_started", KEY_HOOKS_FINISHED: "test_hooks_finished", DEFAULT_TEST_RESULT: "pending", DEFAULT_HOOK_RESULT: "pending", KIND_SCREENSHOT: "TEST_SCREENSHOT", KIND_LOG: "TEST_LOG", HOOK_REGEX: "^(BEFORE_|AFTER_)" }; } }); // src/cli/cliUtils.ts import fs2 from "node:fs"; import fsp from "node:fs/promises"; import { platform, arch, homedir } from "node:os"; import path2 from "node:path"; import util, { promisify } from "node:util"; import { exec } from "node:child_process"; import { Readable } from "node:stream"; import yauzl from "yauzl"; import { threadId } from "node:worker_threads"; var CLI_LOCK_TIMEOUT_MS, CLI_LOCK_POLL_MS, CLI_DOWNLOAD_TIMEOUT_MS, CLI_DOWNLOAD_TMP_PREFIX, CLI_DOWNLOAD_TMP_SUFFIX, CLIUtils; var init_cliUtils = __esm({ "src/cli/cliUtils.ts"() { "use strict"; init_fetchWrapper(); init_util(); init_performance_tester(); init_constants2(); init_cliLogger(); init_constants(); init_testFrameworkConstants(); init_apiUtils(); CLI_LOCK_TIMEOUT_MS = 5 * 60 * 1e3; CLI_LOCK_POLL_MS = 1e3; CLI_DOWNLOAD_TIMEOUT_MS = 5 * 60 * 1e3; CLI_DOWNLOAD_TMP_PREFIX = "downloaded_file_"; CLI_DOWNLOAD_TMP_SUFFIX = ".zip"; CLIUtils = class _CLIUtils { static automationFrameworkDetail = {}; static testFrameworkDetail = {}; static CLISupportedFrameworks = ["mocha"]; static isDevelopmentEnv() { return process.env.BROWSERSTACK_CLI_ENV === "development"; } static getCLIParamsForDevEnv() { return { id: process.env.BROWSERSTACK_CLI_ENV || "", listen: `unix:/tmp/sdk-platform-${process.env.BROWSERSTACK_CLI_ENV}.sock` }; } /** * Build config object for binary session request * @returns {string} * @throws {Error} */ static getBinConfig(config, capabilities, options, buildTag) { const modifiedOpts = { ...options }; if (modifiedOpts.opts) { modifiedOpts.browserStackLocalOptions = modifiedOpts.opts; delete modifiedOpts.opts; } modifiedOpts.testContextOptions = { skipSessionName: isFalse(modifiedOpts.setSessionName), skipSessionStatus: isFalse(modifiedOpts.setSessionStatus), sessionNameOmitTestTitle: modifiedOpts.sessionNameOmitTestTitle || false, sessionNamePrependTopLevelSuiteTitle: modifiedOpts.sessionNamePrependTopLevelSuiteTitle || false, sessionNameFormat: modifiedOpts.sessionNameFormat || "" }; const commonBstackOptions = (() => { if (capabilities && !Array.isArray(capabilities) && typeof capabilities === "object" && "bstack:options" in capabilities) { return capabilities["bstack:options"] || {}; } return {}; })(); const isNonBstackA11y = isTurboScale(options) || !shouldAddServiceVersion( config, options.testObservability ); const observabilityOptions = options.testObservabilityOptions || {}; const binconfig = { userName: observabilityOptions.user || config.user, accessKey: observabilityOptions.key || config.key, platforms: [], isNonBstackA11yWDIO: isNonBstackA11y, ...modifiedOpts, ...commonBstackOptions }; binconfig.buildName = observabilityOptions.buildName || binconfig.buildName; binconfig.projectName = observabilityOptions.projectName || binconfig.projectName; binconfig.buildTag = this.getObservabilityBuildTags(observabilityOptions, buildTag) || []; let caps = capabilities; if (capabilities && !Array.isArray(capabilities)) { caps = [capabilities]; } if (Array.isArray(caps)) { for (const cap of caps) { const platform4 = {}; const capability = cap; Object.keys(capability).filter((key) => key !== "bstack:options").forEach((key) => { platform4[key] = capability[key]; }); if (capability["bstack:options"]) { Object.keys( capability["bstack:options"] ).forEach((key) => { platform4[key] = capability["bstack:options"][key]; }); } binconfig.platforms.push(platform4); } } return JSON.stringify(binconfig); } static getSdkVersion() { return BSTACK_SERVICE_VERSION; } static getSdkLanguage() { return "ECMAScript"; } static async setupCliPath(config) { BStackLogger.debug("Configuring Cli path."); const developmentBinaryPath = process.env.SDK_CLI_BIN_PATH || null; if (!isNullOrEmpty(developmentBinaryPath)) { BStackLogger.debug(`Development Cli Path: ${developmentBinaryPath}`); return developmentBinaryPath; } try { const cliDir = this.getCliDir(); if (isNullOrEmpty(cliDir)) { throw new Error("No writable directory available for the CLI"); } const existingCliPath = this.getExistingCliPath(cliDir); const finalBinaryPath = await this.checkAndUpdateCli( existingCliPath, cliDir, config ); BStackLogger.debug(`Resolved binary path: ${finalBinaryPath}`); return finalBinaryPath; } catch (err) { BStackLogger.debug( `Error in setting up cli path directory, Exception: ${util.format(err)}` ); } return null; } static async checkAndUpdateCli(existingCliPath, cliDir, config) { if (process.env.BROWSERSTACK_TESTHUB_JWT) { BStackLogger.debug( `Worker process detected, skipping CLI update. Using existing: ${existingCliPath}` ); if (existingCliPath && fs2.existsSync(existingCliPath)) { return existingCliPath; } BStackLogger.warn( "Worker process has no existing CLI binary, attempting download as fallback." ); } PerformanceTester.start(EVENTS.SDK_CLI_CHECK_UPDATE); BStackLogger.info(`Current CLI Path Found: ${existingCliPath}`); const queryParams = { sdk_version: _CLIUtils.getSdkVersion(), os: platform(), os_arch: arch(), cli_version: "0", sdk_language: this.getSdkLanguage() }; if (!isNullOrEmpty(existingCliPath)) { queryParams.cli_version = await this.runShellCommand( `${existingCliPath} version` ); } const response = await this.requestToUpdateCLI(queryParams, config); if (nestedKeyValue(response, ["updated_cli_version"])) { BStackLogger.debug( `Need to update binary, current binary version: ${queryParams.cli_version}` ); const browserStackBinaryUrl = process.env.BROWSERSTACK_BINARY_URL || null; if (!isNullOrEmpty(browserStackBinaryUrl)) { BStackLogger.debug( `Using BROWSERSTACK_BINARY_URL: ${browserStackBinaryUrl}` ); response.url = browserStackBinaryUrl; } const finalBinaryPath = await this.downloadLatestBinary( nestedKeyValue(response, ["url"]), cliDir ); PerformanceTester.end(EVENTS.SDK_CLI_CHECK_UPDATE); return finalBinaryPath; } PerformanceTester.end(EVENTS.SDK_CLI_CHECK_UPDATE); return existingCliPath; } static getCliDir() { const writableDir = this.getWritableDir(); try { if (isNullOrEmpty(writableDir)) { throw new Error("No writable directory available for the CLI"); } const cliDirPath = path2.join(writableDir, "cli"); if (!fs2.existsSync(cliDirPath)) { createDir(cliDirPath); } return cliDirPath; } catch (err) { BStackLogger.error( `Error in getting writable directory, writableDir=${util.format(err)}` ); return ""; } } static getWritableDir() { const writableDirOptions = [ process.env.BROWSERSTACK_FILES_DIR, path2.join(homedir(), ".browserstack"), path2.join("tmp", ".browserstack") ]; for (const path21 of writableDirOptions) { if (isNullOrEmpty(path21)) { continue; } try { if (fs2.existsSync(path21)) { BStackLogger.debug(`File ${path21} already exist`); if (!isWritable(path21)) { BStackLogger.debug(`Giving write permission to ${path21}`); const success = setReadWriteAccess(path21); if (!isTrue(success)) { BStackLogger.warn( `Unable to provide write permission to ${path21}` ); } } } else { BStackLogger.debug(`File does not exist: ${path21}`); createDir(path21); BStackLogger.debug(`Giving write permission to ${path21}`); const success = setReadWriteAccess(path21); if (!isTrue(success)) { BStackLogger.warn( `Unable to provide write permission to ${path21}` ); } } return path21; } catch (err) { BStackLogger.error( `Unable to get writable directory, exception ${util.format(err)}` ); } } return null; } static getExistingCliPath(cliDir) { try { if (!fs2.existsSync(cliDir) || !fs2.statSync(cliDir).isDirectory()) { return ""; } const allBinaries = fs2.readdirSync(cliDir).map((file) => path2.join(cliDir, file)).filter( (filePath) => fs2.statSync(filePath).isFile() && path2.basename(filePath).startsWith("binary-") ); if (allBinaries.length > 0) { const latestBinary = allBinaries.map((filePath) => ({ filePath, mtime: fs2.statSync(filePath).mtime })).reduce( (latest, current) => { if (!latest || !latest.mtime) { return current; } if (current.mtime > latest.mtime) { return current; } return latest; }, null ); return latestBinary ? latestBinary.filePath : ""; } return ""; } catch (err) { BStackLogger.error(`Error while reading CLI path: ${util.format(err)}`); return ""; } } static requestToUpdateCLI = async (queryParams, config) => { const params = new URLSearchParams(queryParams); const requestInit = { method: "GET", headers: { Authorization: `Basic ${Buffer.from(`${getBrowserStackUser(config)}:${getBrowserStackKey(config)}`).toString("base64")}` } }; const response = await _fetch( `${APIUtils.BROWSERSTACK_AUTOMATE_API_URL}/${UPDATED_CLI_ENDPOINT}?${params.toString()}`, requestInit ); const jsonResponse = await response.json(); BStackLogger.debug(`response ${JSON.stringify(jsonResponse)}`); return jsonResponse; }; static runShellCommand(cmdCommand, workingDir = "") { return new Promise((resolve) => { const process2 = exec( cmdCommand, { cwd: workingDir, timeout: 5e3 }, (error, stdout, stderr) => { if (error) { resolve(stderr.trim() || "SHELL_EXECUTE_ERROR"); } else { resolve(stdout.trim()); } } ); process2.on("error", () => { resolve("SHELL_EXECUTE_ERROR"); }); }); } static downloadLatestBinary = async (binDownloadUrl, cliDir) => { const lockPath = path2.join(cliDir, "download.lock"); const sleep2 = (ms) => new Promise((r) => setTimeout(r, ms)); const parseLockFile = () => { try { const content = fs2.readFileSync(lockPath, "utf8").trim(); const [pidLine, timestampLine] = content.split("\n"); const pid = Number.parseInt(pidLine, 10); const timestamp = Number.parseInt(timestampLine, 10); if (!Number.isFinite(pid) || !Number.isFinite(timestamp)) { return null; } return { pid, timestamp }; } catch { return null; } }; const isProcessRunning = (pid) => { try { process.kill(pid, 0); return true; } catch { return false; } }; const acquireLock = async (timeoutMs = CLI_LOCK_TIMEOUT_MS, pollMs = CLI_LOCK_POLL_MS) => { const start = Date.now(); while (true) { try { const fd = fs2.openSync(lockPath, "wx"); try { fs2.writeFileSync(fd, `${process.pid} ${Date.now()} `); } catch { } return () => { try { fs2.closeSync(fd); } catch { } try { fs2.unlinkSync(lockPath); } catch { } }; } catch (e) { const error = e; if (error.code === "EEXIST") { const lockMeta = parseLockFile(); if (lockMeta) { const lockAge = Date.now() - lockMeta.timestamp; const running = isProcessRunning(lockMeta.pid); if (!running || lockAge > timeoutMs) { BStackLogger.warn( `Stale CLI download lock detected (pid=${lockMeta.pid}, age=${lockAge}ms). Removing lock.` ); try { fs2.unlinkSync(lockPath); } catch { } continue; } } const existingBinary = _CLIUtils.getExistingCliPath(cliDir); if (existingBinary && fs2.existsSync(existingBinary) && fs2.statSync(existingBinary).size > 0) { BStackLogger.debug( `Binary appeared while waiting for lock: ${existingBinary}` ); return { alreadyExists: existingBinary }; } if (Date.now() - start > timeoutMs) { throw new Error( `Timeout waiting for lock: ${lockPath}` ); } await sleep2(pollMs); continue; } throw e; } } }; const cleanupTemporaryDownloads = (maxAgeMs = CLI_LOCK_TIMEOUT_MS) => { try { const now2 = Date.now(); for (const entry of fs2.readdirSync(cliDir)) { if (!entry.startsWith(CLI_DOWNLOAD_TMP_PREFIX) || !entry.endsWith(CLI_DOWNLOAD_TMP_SUFFIX)) { continue; } const filePath = path2.join(cliDir, entry); let stats; try { stats = fs2.statSync(filePath); } catch { continue; } if (now2 - stats.mtimeMs < maxAgeMs) { continue; } try { fs2.unlinkSync(filePath); } catch (err) { BStackLogger.debug( `Failed to delete temp CLI zip file ${filePath}: ${util.format(err)}` ); } } } catch (err) { BStackLogger.debug( `Failed to scan temp CLI downloads in ${cliDir}: ${util.format(err)}` ); } }; PerformanceTester.start(EVENTS.SDK_CLI_DOWNLOAD); BStackLogger.debug(`Downloading SDK binary from: ${binDownloadUrl}`); let downloadEnded = false; const endDownload = (success = true, errMsg) => { if (downloadEnded) { return; } downloadEnded = true; if (success) { PerformanceTester.end(EVENTS.SDK_CLI_DOWNLOAD); return; } PerformanceTester.end( EVENTS.SDK_CLI_DOWNLOAD, false, errMsg ); }; let releaseLock; try { const lockResult = await acquireLock(); if (typeof lockResult !== "function") { endDownload(); return lockResult.alreadyExists; } releaseLock = lockResult; const existingBinary = _CLIUtils.getExistingCliPath(cliDir); if (existingBinary && fs2.existsSync(existingBinary) && fs2.statSync(existingBinary).size > 0) { BStackLogger.debug( `Binary already exists after acquiring lock: ${existingBinary}` ); endDownload(); releaseLock(); return existingBinary; } cleanupTemporaryDownloads(); const zipFilePath = path2.join( cliDir, `${CLI_DOWNLOAD_TMP_PREFIX}${process.pid}_${Date.now()}${CLI_DOWNLOAD_TMP_SUFFIX}` ); const downloadedFileStream = fs2.createWriteStream(zipFilePath); return new Promise((resolve, reject) => { const binaryName = null; const processDownload = async () => { const abortController = new AbortController(); const timeout = setTimeout( () => abortController.abort(), CLI_DOWNLOAD_TIMEOUT_MS ); let response; try { response = await _fetch(binDownloadUrl, { signal: abortController.signal }); } finally { clearTimeout(timeout); } if (!response.body) { throw new Error("No response body received"); } downloadedFileStream.on("error", function(err) { BStackLogger.error( `Got Error while downloading cli binary file: ${err}` ); endDownload(false, util.format(err)); releaseLock?.(); reject(err); }); try { const arrayBuffer = await response.arrayBuffer(); const nodeStream = Readable.from([ new Uint8Array(arrayBuffer) ]); nodeStream.pipe(downloadedFileStream); _CLIUtils.downloadFileStream( downloadedFileStream, binaryName, zipFilePath, cliDir, (result) => { endDownload(); releaseLock?.(); resolve(result); }, (err) => { endDownload(false, util.format(err)); releaseLock?.(); reject(err); } ); } catch (err) { BStackLogger.error( `Got Error in cli binary downloading request ${util.format(err)}` ); endDownload(false, util.format(err)); releaseLock?.(); reject(err); } }; processDownload(); }); } catch (err) { releaseLock?.(); endDownload(false, util.format(err)); BStackLogger.debug( `Failed to download binary, Exception: ${util.format(err)}` ); return null; } }; static downloadFileStream(downloadedFileStream, binaryName, zipFilePath, cliDir, resolve, reject) { downloadedFileStream.on("close", async function() { const yauzlOpenPromise = promisify(yauzl.open); try { const zipfile = await yauzlOpenPromise(zipFilePath, { lazyEntries: true }); zipfile.readEntry(); zipfile.on("entry", async (entry) => { if (!binaryName) { binaryName = entry.fileName; } if (/\/$/.test(entry.fileName)) { zipfile.readEntry(); } else { const writeStream = fs2.createWriteStream( path2.join(cliDir, entry.fileName) ); const openReadStreamPromise = promisify( zipfile.openReadStream ).bind(zipfile); try { const readStream = await openReadStreamPromise(entry); readStream.on("end", function() { writeStream.close(); zipfile.readEntry(); }); readStream.pipe(writeStream); } catch (zipErr) { reject(zipErr); } if (entry.fileName === binaryName) { zipfile.close(); } } }); zipfile.on("error", (zipErr) => { reject(zipErr); }); zipfile.once("end", () => { fsp.unlink(zipFilePath).catch(() => { BStackLogger.warn( `Failed to delete zip file: ${zipFilePath}` ); }); fsp.chmod(`${cliDir}/${binaryName}`, "0755").then(() => { resolve(`${cliDir}/${binaryName}`); }).catch((err) => { reject(err); }); zipfile.close(); }); } catch (err) { reject(err); } }); } static getTestFrameworkDetail() { if (process.env.BROWSERSTACK_TEST_FRAMEWORK_DETAIL) { return JSON.parse(process.env.BROWSERSTACK_TEST_FRAMEWORK_DETAIL); } return this.testFrameworkDetail; } static getAutomationFrameworkDetail() { if (process.env.BROWSERSTACK_AUTOMATION_FRAMEWORK_DETAIL) { return JSON.parse( process.env.BROWSERSTACK_AUTOMATION_FRAMEWORK_DETAIL ); } return this.automationFrameworkDetail; } static setFrameworkDetail(testFramework, automationFramework) { if (!testFramework || !automationFramework) { BStackLogger.debug( `Test or Automation framework not provided testFramework=${testFramework}, automationFramework=${automationFramework}` ); } this.testFrameworkDetail = { name: testFramework, version: { [testFramework]: _CLIUtils.getSdkVersion() } }; this.automationFrameworkDetail = { name: automationFramework, version: { [automationFramework]: _CLIUtils.getSdkVersion() } }; process.env.BROWSERSTACK_AUTOMATION_FRAMEWORK_DETAIL = JSON.stringify( this.automationFrameworkDetail ); process.env.BROWSERSTACK_TEST_FRAMEWORK_DETAIL = JSON.stringify( this.testFrameworkDetail ); } /** * Get the current instance name using thread id and processId * @returns {string} */ static getCurrentInstanceName() { return `${process.pid}:${threadId}`; } /** * Generate a unique client worker identifier combining thread ID and process ID. * This identifier is used to track worker-specific events and performance metrics * across distributed test execution. Format matches the Python SDK implementation * for consistency across SDKs. * * Format: "threadId-processId" * * @param context - Optional execution context with threadId and processId * @returns Worker ID string in format "threadId-processId" * @example * const workerId = CLIUtils.getClientWorkerId() // Returns "1-12345" * const workerId = CLIUtils.getClientWorkerId({ threadId: 123, processId: 456 }) // Returns "123-456" */ static getClientWorkerId(context) { const workerThreadId = context?.threadId?.toString() || threadId.toString(); const workerProcessId = context?.processId?.toString() || process.pid.toString(); return `${workerThreadId}-${workerProcessId}`; } /** * * @param {TestFrameworkState | AutomationFrameworkState} frameworkState * @param {HookState} hookState * @returns {string} */ static getHookRegistryKey(frameworkState, hookState) { return `${frameworkState}:${hookState}`; } static matchHookRegex(hookState) { const pattern = new RegExp(TestFrameworkConstants.HOOK_REGEX); return pattern.test(hookState); } static getObservabilityBuildTags(observabilityOptions, bstackBuildTag) { if (process.env.TEST_OBSERVABILITY_BUILD_TAG) { return process.env.TEST_OBSERVABILITY_BUILD_TAG.split(","); } if (observabilityOptions.buildTag) { return observabilityOptions.buildTag; } if (bstackBuildTag) { return [bstackBuildTag]; } return []; } static checkCLISupportedFrameworks(framework) { if (framework === void 0) { return false; } return this.CLISupportedFrameworks.includes(framework); } }; } }); // src/instrumentation/performance/performance-tester.ts import { createObjectCsvWriter } from "csv-writer"; import fs3 from "node:fs"; import fsPromise from "node:fs/promises"; import { performance, PerformanceObserver } from "node:perf_hooks"; import util2 from "node:util"; import worker from "node:worker_threads"; import path3 from "node:path"; import { arch as arch2, hostname, platform as platform2, type, version } from "node:os"; var PerformanceTester; var init_performance_tester = __esm({ "src/instrumentation/performance/performance-tester.ts"() { "use strict"; init_bstackLogger(); init_constants(); init_apiUtils(); init_cliUtils(); init_constants2(); init_fetchWrapper(); PerformanceTester = class _PerformanceTester { static _observer; static _csvWriter; static _events = []; static _measuredEvents = []; static _hasStoppedGeneration = false; static _stopGenerateCallCount = 0; static started = false; static details = {}; static eventsMap = {}; static browser; static scenarioThatRan; static jsonReportDirName = "performance-report"; static jsonReportDirPath = path3.join(process.cwd(), "logs", this.jsonReportDirName); static jsonReportFileName = `${this.jsonReportDirPath}/performance-report-${_PerformanceTester.getProcessId()}.json`; static startMonitoring(csvName = "performance-report.csv") { if (!fs3.existsSync(this.jsonReportDirPath)) { fs3.mkdirSync(this.jsonReportDirPath, {