UNPKG

@wdio/browserstack-service

Version:
1,441 lines (1,424 loc) 256 kB
var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; 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; }; // src/launcher.ts import fs11 from "node:fs"; import { readFile } from "node:fs/promises"; import path12 from "node:path"; import { promisify as promisify2, format as format3 } from "node:util"; import { performance as performance3, PerformanceObserver as PerformanceObserver2 } from "node:perf_hooks"; import os5 from "node:os"; import { SevereServiceError } from "webdriverio"; import * as BrowserstackLocalLauncher from "browserstack-local"; // package.json var package_default = { name: "@wdio/browserstack-service", version: "9.19.1", 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", "@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", 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/yauzl": "^2.10.3", "@wdio/globals": "workspace:*" }, publishConfig: { access: "public" } }; // src/constants.ts var bstackServiceVersion = package_default.version; var BROWSER_DESCRIPTION = [ "device", "os", "osVersion", "os_version", "browserName", "browser", "browserVersion", "browser_version" ]; var VALID_APP_EXTENSION = [ ".apk", ".aab", ".ipa" ]; var DEFAULT_OPTIONS = { setSessionName: true, setSessionStatus: true, testObservability: true, accessibility: false }; var consoleHolder = Object.assign({}, console); var DATA_ENDPOINT = "https://collector-observability.browserstack.com"; var APP_ALLY_ENDPOINT = "https://app-accessibility.browserstack.com/automate"; var APP_ALLY_ISSUES_ENDPOINT = "api/v1/issues"; var APP_ALLY_ISSUES_SUMMARY_ENDPOINT = "api/v1/issues-summary"; var DATA_EVENT_ENDPOINT = "api/v1/event"; var DATA_BATCH_ENDPOINT = "api/v1/batch"; var DATA_SCREENSHOT_ENDPOINT = "api/v1/screenshots"; var DATA_BATCH_SIZE = 1e3; var DATA_BATCH_INTERVAL = 2e3; var DEFAULT_WAIT_TIMEOUT_FOR_PENDING_UPLOADS = 5e3; var DEFAULT_WAIT_INTERVAL_FOR_PENDING_UPLOADS = 100; var BSTACK_SERVICE_VERSION = bstackServiceVersion; var NOT_ALLOWED_KEYS_IN_CAPS = ["includeTagsInTestingScope", "excludeTagsInTestingScope"]; var LOGS_FILE = "logs/bstack-wdio-service.log"; var UPLOAD_LOGS_ADDRESS = "https://upload-observability.browserstack.com"; var UPLOAD_LOGS_ENDPOINT = "client-logs/upload"; var PERCY_LOGS_FILE = "logs/percy.log"; var 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" ]; var CAPTURE_MODES = ["click", "auto", "screenshot", "manual", "testcase"]; var LOG_KIND_USAGE_MAP = { "TEST_LOG": "log", "TEST_SCREENSHOT": "screenshot", "TEST_STEP": "step", "HTTP": "http" }; var FUNNEL_INSTRUMENTATION_URL = "https://api.browserstack.com/sdk/v1/event"; var EDS_URL = "https://eds.browserstack.com"; var SUPPORTED_BROWSERS_FOR_AI = ["chrome", "microsoftedge", "firefox"]; var TCG_URL = "https://tcg.browserstack.com"; var TCG_INFO = { tcgRegion: "use", tcgUrl: TCG_URL }; var BROWSERSTACK_TESTHUB_JWT = "BROWSERSTACK_TESTHUB_JWT"; var BSTACK_TCG_AUTH_RESULT = "BSTACK_TCG_AUTH_RESULT"; var TESTOPS_SCREENSHOT_ENV = "BS_TESTOPS_ALLOW_SCREENSHOTS"; var BROWSERSTACK_TESTHUB_UUID = "BROWSERSTACK_TESTHUB_UUID"; var TEST_ANALYTICS_ID = "TEST_ANALYTICS_ID"; var PERF_MEASUREMENT_ENV = "BROWSERSTACK_O11Y_PERF_MEASUREMENT"; var RERUN_TESTS_ENV = "BROWSERSTACK_RERUN_TESTS"; var RERUN_ENV = "BROWSERSTACK_RERUN"; var TESTOPS_BUILD_COMPLETED_ENV = "BS_TESTOPS_BUILD_COMPLETED"; var BROWSERSTACK_PERCY = "BROWSERSTACK_PERCY"; var BROWSERSTACK_ACCESSIBILITY = "BROWSERSTACK_ACCESSIBILITY"; var BROWSERSTACK_OBSERVABILITY = "BROWSERSTACK_OBSERVABILITY"; var MAX_GIT_META_DATA_SIZE_IN_BYTES = 64 * 1024; var GIT_META_DATA_TRUNCATED = "...[TRUNCATED]"; // src/bstackLogger.ts import path4 from "node:path"; import fs4 from "node:fs"; import chalk from "chalk"; import logger from "@wdio/logger"; // src/util.ts import { hostname as hostname2, platform as platform2, type as type2, version as version2, arch as arch2 } from "node:os"; import crypto from "node:crypto"; import fs3 from "node:fs"; import zlib from "node:zlib"; import { format, promisify } from "node:util"; import path3 from "node:path"; import util2 from "node:util"; import gitRepoInfo from "git-repo-info"; import gitconfig from "gitconfiglocal"; import { FormData as FormData2 } from "formdata-node"; import { performance as performance2 } from "node:perf_hooks"; // src/logPatcher.ts import Transport from "winston-transport"; var LOG_LEVELS = { INFO: "INFO", ERROR: "ERROR", DEBUG: "DEBUG", TRACE: "TRACE", WARN: "WARN" }; var 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); }; }; var logPatcher_default = logPatcher; // src/instrumentation/performance/performance-tester.ts import { createObjectCsvWriter } from "csv-writer"; import fs from "node:fs"; import fsPromise from "node:fs/promises"; import { performance, PerformanceObserver } from "node:perf_hooks"; import util from "node:util"; import worker from "node:worker_threads"; import path from "node:path"; import { arch, hostname, platform, type, version } from "node:os"; // src/fetchWrapper.ts import { fetch as undiciFetch, ProxyAgent } from "undici"; var ResponseError = class extends Error { response; constructor(message, res) { super(message); this.response = res; } }; 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); } // src/instrumentation/performance/performance-tester.ts var PerformanceTester = class _PerformanceTester { static _observer; static _csvWriter; static _events = []; static _measuredEvents = []; static started = false; static details = {}; static eventsMap = {}; static browser; static scenarioThatRan; static jsonReportDirName = "performance-report"; static jsonReportDirPath = path.join(process.cwd(), "logs", this.jsonReportDirName); static jsonReportFileName = `${this.jsonReportDirPath}/performance-report-${_PerformanceTester.getProcessId()}.json`; static startMonitoring(csvName = "performance-report.csv") { if (!fs.existsSync(this.jsonReportDirPath)) { fs.mkdirSync(this.jsonReportDirPath, { recursive: true }); } this._observer = new PerformanceObserver((list) => { list.getEntries().filter((entry) => entry.entryType === "measure").forEach( (entry) => { let finalEntry = entry; finalEntry = entry.toJSON(); if (this.details[entry.name]) { finalEntry = Object.assign(finalEntry, this.details[entry.name]); } delete this.details[entry.name]; this._measuredEvents.push(finalEntry); } ); if (process.env[PERF_MEASUREMENT_ENV]) { list.getEntries().forEach((entry) => this._events.push(entry)); } }); const entryTypes = ["measure"]; if (process.env[PERF_MEASUREMENT_ENV]) { entryTypes.push("function"); } this._observer.observe({ buffered: true, entryTypes }); this.started = true; if (process.env[PERF_MEASUREMENT_ENV]) { this._csvWriter = createObjectCsvWriter({ path: csvName, header: [ { id: "name", title: "Function Name" }, { id: "time", title: "Execution Time (ms)" } ] }); } } static calculateTimes(methods) { const times = {}; this._events.map((entry) => { if (!times[entry.name]) { times[entry.name] = 0; } times[entry.name] += entry.duration; }); const timeTaken = methods.reduce((a, c) => { return times[c] + (a || 0); }, 0); BStackLogger.debug(`Time for ${methods} is ${timeTaken}`); return timeTaken; } static async stopAndGenerate(filename = "performance-own.html") { if (!this.started) { return; } try { const eventsJson = JSON.stringify(this._measuredEvents); const finalJSONStr = eventsJson.slice(1, -1) + ","; await fsPromise.appendFile(this.jsonReportFileName, finalJSONStr); } catch (er) { BStackLogger.debug(`Failed to write events of the worker to ${this.jsonReportFileName}: ${util.format(er)}`); } this._observer.disconnect(); if (!process.env[PERF_MEASUREMENT_ENV]) { return; } await _PerformanceTester.sleep(2e3); this.started = false; this.generateCSV(this._events); const content = this.generateReport(this._events); const dir = path.join(process.cwd(), filename); try { await fsPromise.writeFile(dir, content); BStackLogger.info(`Performance report is at ${path}`); } catch (err) { BStackLogger.error(`Error in writing html ${util.format(err)}`); } } static generateReport(entries) { let html = "<!DOCTYPE html><html><head><title>Performance Report</title></head><body>"; html += "<h1>Performance Report</h1>"; html += "<table><thead><tr><th>Function Name</th><th>Duration (ms)</th></tr></thead><tbody>"; entries.forEach((entry) => { html += `<tr><td>${entry.name}</td><td>${entry.duration}</td></tr>`; }); html += "</tbody></table></body></html>"; return html; } static generateCSV(entries) { const times = {}; entries.map((entry) => { if (!times[entry.name]) { times[entry.name] = 0; } times[entry.name] += entry.duration; return { name: entry.name, time: entry.duration }; }); const dat = Object.entries(times).map(([key, value]) => { return { name: key, time: value }; }); this._csvWriter.writeRecords(dat).then(() => BStackLogger.info("Performance CSV report generated successfully")).catch((error) => console.error(error)); } static Measure(label, details = {}) { const self = this; return (target, key, descriptor) => { const originalMethod = descriptor.value; if (descriptor.value) { descriptor.value = function(...args) { return _PerformanceTester.measure.apply(self, [label, originalMethod, { methodName: key.toString(), ...details }, args, this]); }; } }; } static measureWrapper(name, fn, details = {}) { const self = this; details.worker = _PerformanceTester.getProcessId(); details.testName = _PerformanceTester.scenarioThatRan && _PerformanceTester.scenarioThatRan[_PerformanceTester.scenarioThatRan.length - 1]; details.platform = _PerformanceTester.browser?.sessionId; return function(...args) { return self.measure(name, fn, details, args); }; } static isEnabled() { return !(process.env.BROWSERSTACK_SDK_INSTRUMENTATION === "false"); } static measure(label, fn, details = {}, args, thisArg = null) { if (!this.started || !this.isEnabled()) { return fn.apply(thisArg, args); } _PerformanceTester.start(label); if (this.details) { this.details[label] = details; } try { const returnVal = fn.apply(thisArg, args); if (returnVal instanceof Promise) { return new Promise((resolve, reject) => { returnVal.then((v) => { _PerformanceTester.end(label); resolve(v); }).catch((e) => { _PerformanceTester.end(label, false, util.format(e)); reject(e); }); }); } _PerformanceTester.end(label); return returnVal; } catch (er) { _PerformanceTester.end(label, false, util.format(er)); throw er; } } static start(event) { const finalEvent = event + "-start"; if (this.eventsMap[finalEvent]) { return; } performance.mark(finalEvent); this.eventsMap[finalEvent] = 1; } static end(event, success = true, failure, details = {}) { performance.mark(event + "-end"); performance.measure(event, event + "-start", event + "-end"); this.details[event] = Object.assign({ success, failure: util.format(failure) }, Object.assign(Object.assign({ worker: _PerformanceTester.getProcessId(), platform: _PerformanceTester.browser?.sessionId, testName: _PerformanceTester.scenarioThatRan && _PerformanceTester.scenarioThatRan[_PerformanceTester.scenarioThatRan.length - 1] }, details), this.details[event] || {})); } static getProcessId() { return `${process.pid}-${worker.threadId}`; } static sleep = (ms = 100) => new Promise((resolve) => setTimeout(resolve, ms)); static async uploadEventsData() { try { let measures = []; if (await fsPromise.access(this.jsonReportDirPath).then(() => true).catch(() => false)) { const files = (await fsPromise.readdir(this.jsonReportDirPath)).map((file) => path.resolve(this.jsonReportDirPath, file)); measures = (await Promise.all(files.map((file) => fsPromise.readFile(file, "utf-8")))).map((el) => `[${el.slice(0, -1)}]`).map((el) => JSON.parse(el)).flat(); } if (this._measuredEvents.length > 0) { measures = measures.concat(this._measuredEvents); } const date = /* @__PURE__ */ new Date(); const options = { timeZone: "UTC", year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit", fractionalSecondDigits: 3, // To include microseconds hour12: false }; const formattedDate = new Intl.DateTimeFormat("en-GB", options).formatToParts(date).map(({ type: type3, value }) => type3 === "timeZoneName" ? "Z" : value).join("").replace(",", "T"); const payload = { event_type: "sdk_events", data: { testhub_uuid: process.env.PERF_TESTHUB_UUID || process.env.SDK_RUN_ID, created_day: formattedDate, event_name: "SDKFeaturePerformance", user_data: process.env.PERF_USER_NAME, host_info: JSON.stringify({ hostname: hostname(), platform: platform(), type: type(), version: version(), arch: arch() }), event_json: { measures, sdkRunId: process.env.SDK_RUN_ID } } }; const result = await fetchWrap(`${EDS_URL}/send_sdk_events`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(payload) }); BStackLogger.debug(`Successfully uploaded performance events ${util.format(await result.text())}`); } catch (er) { BStackLogger.debug(`Failed to upload performance events ${util.format(er)}`); } try { if (await fsPromise.access(this.jsonReportDirPath).then(() => true, () => false)) { const files = await fsPromise.readdir(this.jsonReportDirPath); for (const file of files) { await fsPromise.unlink(path.join(this.jsonReportDirPath, file)); } } } catch (er) { BStackLogger.debug(`Failed to delete performance related files ${util.format(er)}`); } } }; // src/instrumentation/performance/constants.ts var 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" }; var TESTHUB_EVENTS = { START: `${EVENTS.SDK_TESTHUB}:start`, STOP: `${EVENTS.SDK_TESTHUB}:stop` }; var 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` }; var 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` }; var 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` }; var O11Y_EVENTS = { SYNC: `${EVENTS.SDK_O11Y}:sync`, TAKE_SCREENSHOT: `${EVENTS.SDK_O11Y}:driver-takeScreenShot`, PRINT_BUILDLINK: `${EVENTS.SDK_O11Y}:print-buildlink` }; var 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` }; var TURBOSCALE_EVENTS = { HUB_MANAGEMENT: `${EVENTS.SDK_TURBOSCALE}:hub-management`, PRINT_BUILDLINK: `${EVENTS.SDK_TURBOSCALE}:print-buildlink` }; var APP_AUTOMATE_EVENTS = { APP_UPLOAD: `${EVENTS.SDK_APP_AUTOMATE}:app-upload` }; var 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` }; // src/crash-reporter.ts var CrashReporter = class { /* User test config for build run minus PII */ static userConfigForReporting = {}; /* User credentials used for reporting crashes in browserstack service */ static credentialsForCrashReportUpload = {}; static setCredentialsForCrashReportUpload(options, config) { this.credentialsForCrashReportUpload = { username: getObservabilityUser(options, config), password: getObservabilityKey(options, config) }; process.env.CREDENTIALS_FOR_CRASH_REPORTING = JSON.stringify(this.credentialsForCrashReportUpload); } static setConfigDetails(userConfig, capabilities, options) { const configWithoutPII = this.filterPII(userConfig); const filteredCapabilities = this.filterCapabilities(capabilities); this.userConfigForReporting = { framework: userConfig.framework, services: configWithoutPII.services, capabilities: filteredCapabilities, env: { "BROWSERSTACK_BUILD": process.env.BROWSERSTACK_BUILD, "BROWSERSTACK_BUILD_NAME": process.env.BROWSERSTACK_BUILD_NAME, "BUILD_TAG": process.env.BUILD_TAG } }; process.env.USER_CONFIG_FOR_REPORTING = JSON.stringify(this.userConfigForReporting); this.setCredentialsForCrashReportUpload(options, userConfig); } static async uploadCrashReport(exception, stackTrace) { try { if (!this.credentialsForCrashReportUpload.username || !this.credentialsForCrashReportUpload.password) { this.credentialsForCrashReportUpload = process.env.CREDENTIALS_FOR_CRASH_REPORTING !== void 0 ? JSON.parse(process.env.CREDENTIALS_FOR_CRASH_REPORTING) : this.credentialsForCrashReportUpload; } } catch (error) { return BStackLogger.error(`[Crash_Report_Upload] Failed to parse user credentials while reporting crash due to ${error}`); } if (!this.credentialsForCrashReportUpload.username || !this.credentialsForCrashReportUpload.password) { return BStackLogger.error("[Crash_Report_Upload] Failed to parse user credentials while reporting crash"); } try { if (Object.keys(this.userConfigForReporting).length === 0) { this.userConfigForReporting = process.env.USER_CONFIG_FOR_REPORTING !== void 0 ? JSON.parse(process.env.USER_CONFIG_FOR_REPORTING) : {}; } } catch (error) { BStackLogger.error(`[Crash_Report_Upload] Failed to parse user config while reporting crash due to ${error}`); this.userConfigForReporting = {}; } const data = { hashed_id: process.env[BROWSERSTACK_TESTHUB_UUID], observability_version: { frameworkName: "WebdriverIO-" + (this.userConfigForReporting.framework || "null"), sdkVersion: BSTACK_SERVICE_VERSION }, exception: { error: exception.toString(), stackTrace }, config: this.userConfigForReporting }; const url3 = `${DATA_ENDPOINT}/api/v1/analytics`; const encodedAuth = Buffer.from(`${this.credentialsForCrashReportUpload.username}:${this.credentialsForCrashReportUpload.password}`, "utf8").toString("base64"); const headers = { ...DEFAULT_REQUEST_CONFIG.headers, Authorization: `Basic ${encodedAuth}` }; const response = await _fetch(url3, { method: "POST", body: JSON.stringify(data), headers }); if (response.ok) { BStackLogger.debug(`[Crash_Report_Upload] Success response: ${JSON.stringify(await response.json())}`); } else { BStackLogger.error(`[Crash_Report_Upload] Failed due to ${response.body}`); } } static recursivelyRedactKeysFromObject(obj, keys) { if (!obj) { return; } if (Array.isArray(obj)) { obj.map((ele) => this.recursivelyRedactKeysFromObject(ele, keys)); } else { for (const prop in obj) { if (keys.includes(prop.toLowerCase())) { obj[prop] = "[REDACTED]"; } else if (typeof obj[prop] === "object") { this.recursivelyRedactKeysFromObject(obj[prop], keys); } } } } static deletePIIKeysFromObject(obj) { if (!obj) { return; } ["user", "username", "key", "accessKey"].forEach((key) => delete obj[key]); } static filterCapabilities(capabilities) { const capsCopy = JSON.parse(JSON.stringify(capabilities)); this.recursivelyRedactKeysFromObject(capsCopy, ["extensions"]); return capsCopy; } static filterPII(userConfig) { const configWithoutPII = JSON.parse(JSON.stringify(userConfig)); this.deletePIIKeysFromObject(configWithoutPII); const finalServices = []; const initialServices = configWithoutPII.services; delete configWithoutPII.services; try { for (const serviceArray of initialServices) { if (Array.isArray(serviceArray) && serviceArray.length >= 2 && serviceArray[0] === "browserstack") { for (let idx = 1; idx < serviceArray.length; idx++) { this.deletePIIKeysFromObject(serviceArray[idx]); if (serviceArray[idx]) { this.deletePIIKeysFromObject(serviceArray[idx].testObservabilityOptions); } } finalServices.push(serviceArray); break; } } } catch (err) { BStackLogger.error(`Error in parsing user config PII with error ${err ? err.stack || err : err}`); return configWithoutPII; } configWithoutPII.services = finalServices; return configWithoutPII; } }; // src/testOps/featureStats.ts var FeatureStats = class _FeatureStats { triggeredCount = 0; sentCount = 0; failedCount = 0; groups = {}; mark(status, groupId) { switch (status) { case "triggered": this.triggered(groupId); break; case "success": case "sent": this.sent(groupId); break; case "failed": this.failed(groupId); break; default: BStackLogger.debug("Request to mark usage for unknown status - " + status); break; } } triggered(groupId) { this.triggeredCount += 1; if (groupId) { this.createGroup(groupId).triggered(); } } sent(groupId) { this.sentCount += 1; if (groupId) { this.createGroup(groupId).sent(); } } failed(groupId) { this.failedCount += 1; if (groupId) { this.createGroup(groupId).failed(); } } success(groupId) { this.sent(groupId); } createGroup(groupId) { if (!this.groups[groupId]) { this.groups[groupId] = new _FeatureStats(); } return this.groups[groupId]; } getTriggeredCount() { return this.triggeredCount; } getSentCount() { return this.sentCount; } getFailedCount() { return this.failedCount; } getUsageForGroup(groupId) { return this.groups[groupId] || new _FeatureStats(); } getOverview() { return { triggeredCount: this.triggeredCount, sentCount: this.sentCount, failedCount: this.failedCount }; } getGroups() { return this.groups; } add(featureStats) { this.triggeredCount += featureStats.getTriggeredCount(); this.sentCount += featureStats.getSentCount(); this.failedCount += featureStats.getFailedCount(); Object.entries(featureStats.getGroups()).forEach(([groupId, group]) => { this.createGroup(groupId).add(group); }); } // omitGroups: true/false -> Include groups or not // onlyGroups: true/false -> data includes only groups // nestedGroups: true/false -> groups will be nested in groups if true toJSON(config = {}) { const overviewData = !config.onlyGroups ? { triggeredCount: this.triggeredCount, sentCount: this.sentCount, failedCount: this.failedCount } : {}; const groupsData = {}; if (!config.omitGroups) { Object.entries(this.groups).forEach(([groupId, group2]) => { groupsData[groupId] = group2.toJSON(); }); } const group = config.nestedGroups ? { groups: groupsData } : groupsData; return { ...overviewData, ...group }; } static fromJSON(json) { const stats = new _FeatureStats(); if (!json || isObjectEmpty(json)) { return stats; } stats.triggeredCount = json.triggeredCount; stats.sentCount = json.sentCount; stats.failedCount = json.failedCount; if (!json.groups) { return stats; } Object.entries(json.groups).forEach(([groupId, group]) => { stats.groups[groupId] = _FeatureStats.fromJSON(group); }); return stats; } }; var featureStats_default = FeatureStats; // src/testOps/featureUsage.ts var FeatureUsage = class { isTriggered; status; error; constructor(isTriggered) { if (isTriggered !== void 0) { this.isTriggered = isTriggered; } } getTriggered() { return this.isTriggered; } setTriggered(triggered) { this.isTriggered = triggered; } setStatus(status) { this.status = status; } setError(error) { this.error = error; } triggered() { this.isTriggered = true; } failed(e) { this.status = "failed"; this.error = getErrorString(e); } success() { this.status = "success"; } getStatus() { return this.status; } getError() { return this.error; } toJSON() { return { isTriggered: this.isTriggered, status: this.status, error: this.error }; } }; var featureUsage_default = FeatureUsage; // src/testOps/testOpsConfig.ts var TestOpsConfig = class _TestOpsConfig { constructor(enabled = true, manuallySet = false) { this.enabled = enabled; this.manuallySet = manuallySet; _TestOpsConfig._instance = this; } static _instance; buildStopped = false; buildHashedId; static getInstance(...args) { if (!this._instance) { this._instance = new _TestOpsConfig(...args); } return this._instance; } }; var testOpsConfig_default = TestOpsConfig; // src/testOps/usageStats.ts var UsageStats = class _UsageStats { static instance; testStartedStats; testFinishedStats; hookStartedStats; hookFinishedStats; cbtSessionStats; logStats; launchBuildUsage; stopBuildUsage; static getInstance() { if (!_UsageStats.instance) { _UsageStats.instance = new _UsageStats(); } return _UsageStats.instance; } constructor() { this.testStartedStats = new featureStats_default(); this.testFinishedStats = new featureStats_default(); this.hookStartedStats = new featureStats_default(); this.hookFinishedStats = new featureStats_default(); this.cbtSessionStats = new featureStats_default(); this.logStats = new featureStats_default(); this.launchBuildUsage = new featureUsage_default(); this.stopBuildUsage = new featureUsage_default(); } add(usageStats) { this.testStartedStats.add(usageStats.testStartedStats); this.testFinishedStats.add(usageStats.testFinishedStats); this.hookStartedStats.add(usageStats.hookStartedStats); this.hookFinishedStats.add(usageStats.hookFinishedStats); this.cbtSessionStats.add(usageStats.cbtSessionStats); this.logStats.add(usageStats.logStats); } getFormattedData(workersData) { this.addDataFromWorkers(workersData); const testOpsConfig = testOpsConfig_default.getInstance(); const usage = { enabled: testOpsConfig.enabled, manuallySet: testOpsConfig.manuallySet, buildHashedId: testOpsConfig.buildHashedId }; if (!usage.enabled) { return usage; } try { usage.events = this.getEventsData(); } catch (e) { BStackLogger.debug("exception in getFormattedData: " + e); } return usage; } addDataFromWorkers(workersData) { workersData.map((workerData) => { try { const usageStatsForWorker = _UsageStats.fromJSON(workerData.usageStats); this.add(usageStatsForWorker); } catch (e) { BStackLogger.debug("Exception in adding workerData: " + e); } }); } getEventsData() { return { buildEvents: { started: this.launchBuildUsage.toJSON(), finished: this.stopBuildUsage.toJSON() }, testEvents: { started: this.testStartedStats.toJSON(), finished: this.testFinishedStats.toJSON({ omitGroups: true }), ...this.testFinishedStats.toJSON({ onlyGroups: true }) }, hookEvents: { started: this.hookStartedStats.toJSON(), finished: this.hookFinishedStats.toJSON({ omitGroups: true }), ...this.hookFinishedStats.toJSON({ onlyGroups: true }) }, logEvents: this.logStats.toJSON(), cbtSessionEvents: this.cbtSessionStats.toJSON() }; } getDataToSave() { return { testEvents: { started: this.testStartedStats.toJSON(), finished: this.testFinishedStats.toJSON({ nestedGroups: true }) }, hookEvents: { started: this.hookStartedStats.toJSON(), finished: this.hookFinishedStats.toJSON({ nestedGroups: true }) }, logEvents: this.logStats.toJSON({ nestedGroups: true }), cbtSessionEvents: this.cbtSessionStats.toJSON() }; } static fromJSON(data) { const usageStats = new _UsageStats(); usageStats.testStartedStats = featureStats_default.fromJSON(data.testEvents.started); usageStats.testFinishedStats = featureStats_default.fromJSON(data.testEvents.finished); usageStats.hookStartedStats = featureStats_default.fromJSON(data.hookEvents.started); usageStats.hookFinishedStats = featureStats_default.fromJSON(data.hookEvents.finished); usageStats.logStats = featureStats_default.fromJSON(data.logEvents); usageStats.cbtSessionStats = featureStats_default.fromJSON(data.cbtSessionStats); return usageStats; } }; var usageStats_default = UsageStats; // src/scripts/accessibility-scripts.ts import path2 from "node:path"; import fs2 from "node:fs"; import os from "node:os"; var AccessibilityScripts = class _AccessibilityScripts { static instance = null; performScan = null; getResults = null; getResultsSummary = null; saveTestResults = null; commandsToWrap = null; ChromeExtension = {}; browserstackFolderPath = ""; commandsPath = ""; // don't allow to create instances from it other than through `checkAndGetInstance` constructor() { this.browserstackFolderPath = this.getWritableDir(); this.commandsPath = path2.join(this.browserstackFolderPath, "commands.json"); } static checkAndGetInstance() { if (!_AccessibilityScripts.instance) { _AccessibilityScripts.instance = new _AccessibilityScripts(); _AccessibilityScripts.instance.readFromExistingFile(); } return _AccessibilityScripts.instance; } /* eslint-disable @typescript-eslint/no-unused-vars */ getWritableDir() { const orderedPaths = [ path2.join(os.homedir(), ".browserstack"), process.cwd(), os.tmpdir() ]; for (const orderedPath of orderedPaths) { try { if (fs2.existsSync(orderedPath)) { fs2.accessSync(orderedPath); return orderedPath; } fs2.mkdirSync(orderedPath, { recursive: true }); return orderedPath; } catch (error) { } } return ""; } readFromExistingFile() { try { if (fs2.existsSync(this.commandsPath)) { const data = fs2.readFileSync(this.commandsPath, "utf8"); if (data) { this.update(JSON.parse(data)); } } } catch { } } update(data) { if (data.scripts) { this.performScan = data.scripts.scan; this.getResults = data.scripts.getResults; this.getResultsSummary = data.scripts.getResultsSummary; this.saveTestResults = data.scripts.saveResults; } if (data.commands && data.commands.length) { this.commandsToWrap = data.commands; } if (data.nonBStackInfraA11yChromeOptions) { this.ChromeExtension = data.nonBStackInfraA11yChromeOptions; } } store() { if (!fs2.existsSync(this.browserstackFolderPath)) { fs2.mkdirSync(this.browserstackFolderPath); } fs2.writeFileSync(this.commandsPath, JSON.stringify({ commands: this.commandsToWrap, scripts: { scan: this.performScan, getResults: this.getResults, getResultsSummary: this.getResultsSummary, saveResults: this.saveTestResults }, nonBStackInfraA11yChromeOptions: this.ChromeExtension })); } }; var accessibility_scripts_default = AccessibilityScripts.checkAndGetInstance(); // src/util.ts var pGitconfig = promisify(gitconfig); var DEFAULT_REQUEST_CONFIG = { headers: { "Content-Type": "application/json", "X-BSTACK-OBS": "true" } }; var COLORS = { error: "red", warn: "yellow", info: "cyanBright", debug: "green", trace: "cyan", progress: "magenta" }; function getBrowserDescription(cap) { cap = cap || {}; if (cap["bstack:options"]) { cap = { ...cap, ...cap["bstack:options"] }; } return BROWSER_DESCRIPTION.map((k) => cap[k]).filter(Boolean).join(" "); } function getBrowserCapabilities(browser, caps, browserName) { if (!browser.isMultiremote) { return { ...browser.capabilities, ...caps }; } const multiCaps = caps; const globalCap = browserName && browser.getInstance(browserName) ? browser.getInstance(browserName).capabilities : {}; const cap = browserName && multiCaps[browserName] ? multiCaps[browserName].capabilities : {}; return { ...globalCap, ...cap }; } function isBrowserstackCapability(cap) { return Boolean( cap && cap["bstack:options"] && // return false if the only cap in bstack:options is wdioService, // as that is added by the service and not present in user passed caps !(Object.keys(cap["bstack:options"]).length === 1 && cap["bstack:options"].wdioService) ); } function getParentSuiteName(fullTitle, testSuiteTitle) { const fullTitleWords = fullTitle.split(" "); const testSuiteTitleWords = testSuiteTitle.split(" "); const shortestLength = Math.min(fullTitleWords.length, testSuiteTitleWords.length); let c = 0; let parentSuiteName = ""; while (c < shortestLength && fullTitleWords[c] === testSuiteTitleWords[c]) { parentSuiteName += fullTitleWords[c++] + " "; } return parentSuiteName.trim(); } function processError(error, fn, args) { BStackLogger.error(`Error in executing ${fn.name} with args ${args}: ${error}`); let argsString; try { argsString = JSON.stringify(args); } catch { argsString = util2.inspect(args, { depth: 2 }); } CrashReporter.uploadCrashReport(`Error in executing ${fn.name} with args ${argsString} : ${error}`, error && error.stack || "unknown error"); } function o11yErrorHandler(fn) { return function(...args) { try { let functionToHandle = fn; if (process.env[PERF_MEASUREMENT_ENV]) { functionToHandle = performance2.timerify(functionToHandle); } const result = functionToHandle(...args); if (result instanceof Promise) { return result.catch((error) => processError(error, fn, args)); } return result; } catch (error) { processError(error, fn, args); } }; } async function nodeRequest(requestType, apiEndpoint, options, apiUrl, timeout = 12e4) { try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeout); const response = await _fetch(`${apiUrl}/${apiEndpoint}`, { method: requestType, signal: controller.signal, ...options }); clearTimeout(timeoutId); return await response.json(); } catch (error) { BStackLogger.debug(`Error in firing request ${apiUrl}/${apiEndpoint}: ${format(error)}`); const isLogUpload = apiEndpoint === UPLOAD_LOGS_ENDPOINT; if (error && error.response) { const errorMessageJson = error.response.body ? JSON.parse(error.response.body.toString()) : null; const errorMessage = errorMessageJson ? errorMessageJson.message : null; if (errorMessage) { const message = `${errorMessage} - ${error.stack}`; if (isLogUpload) { BStackLogger.debug(message); } else { BStackLogger.error(message); } } if (isLogUpload) { return; } throw error; } else { if (isLogUpload) { BStackLogger.debug(`Failed to fire api request due to ${error} - ${error.stack}`); return; } BStackLogger.debug(`Failed to fire api request due to ${error} - ${error.stack}`); throw error; } } } function o11yClassErrorHandler(errorClass) { const prototype = errorClass.prototype; if (Object.getOwnPropertyNames(prototype).length < 2) { return errorClass; } Object.getOwnPropertyNames(prototype).forEach((methodName) => { const method = prototype[methodName]; if (typeof method === "function" && methodName !== "constructor" && methodName !== "commandWrapper") { Object.defineProperty(prototype, methodName, { writable: true, value: function(...args) { try { const result = (process.env[PERF_MEASUREMENT_ENV] ? performance2.timerify(method) : method).call(this, ...args); if (result instanceof Promise) { return result.catch((error) => processError(error, method, args)); } return result; } catch (err) { processError(err, method, args); } } }); } }); return errorClass; } var processTestObservabilityResponse = (response) => { if (!response.observability) { handleErrorForObservability(null); return; } if (!response.observability.success) { handleErrorForObservability(response.observability); return; } process.env[BROWSERSTACK_OBSERVABILITY] = "true"; if (response.observability.options.allow_screenshots) { process.env[TESTOPS_SCREENSHOT_ENV] = response.observability.options.allow_screenshots.toString(); } }; var jsonifyAccessibilityArray = (dataArray, keyName, valueName) => { const result = {}; dataArray.forEach((element) => { result[element[keyName]] = element[valueName]; }); return result; }; var processAccessibilityResponse = (response, options) => { if (!response.accessibility) { if (options.accessibility === true) { handleErrorForAccessibility(null); } return; } if (!response.accessibility.success) { handleErrorForAccessibility(response.accessibility); return; } if (response.accessibility.options) { const { accessibilityToken, pollingTimeout, scannerVersion } = jsonifyAccessibilityArray(response.accessibility.options.capabilities, "name", "value"); const result = jsonifyAccessibilityArray(response.accessibility.options.capabilities, "name", "value"); const scriptsJson = { "scripts": jsonifyAccessibilityArray(response.accessibility.options.scripts, "name", "command"), "commands": response.accessibility.options.commandsToWrap.commands, "nonBStackInfraA11yChromeOptions": result["goog:chromeOptions"] }; if (scannerVersion) { process.env.BSTACK_A11Y_SCANNER_VERSION = scannerVersion; BStackLogger.debug(`Accessibility scannerVersion ${scannerVersion}`); } if (accessibilityToken) { process.env.BSTACK_A11Y_JWT = accessibilityToken; process.env[BROWSERSTACK_ACCESSIBILITY] = "true"; } if (pollingTimeout) { process.env.BSTACK_A11Y_POLLING_TIMEOUT = pollingTimeout; } if (scriptsJson) { accessibility_scripts_default.update(scriptsJson); accessibility_scripts_default.store(); } } }; var processLaunchBuildResponse = (response, options) => { if (options.testObservability) { processTestObservabilityResponse(response); } processAccessibilityResponse(response, options); }; var launchTestSession = PerformanceTester.measureWrapper(TESTHUB_EVENTS.START, o11yErrorHandler(async function launchTestSession2(options, config, bsConfig, bStackConfig, accessibilityAutomation) { const launchBuildUsage = usageStats_default.getInstance().launchBuildUsage; launchBuildUsage.triggered(); const data = { format: "json", project_name: getObservabilityProject(options, bsConfig.projectName), name: getObservabilityBuild(options, bsConfig.buildName), build_identifier: bsConfig.buildIdentifier, started_at: (/* @__PURE__ */ new Date()).toISOString(), tags: getObservabilityBuildTags(options, bsConfig.buildTag), host_info: { hostname: hostname2(), platform: platform2(), type: type2(), version: version2(), arch: arch2() }, ci_info: getCiInfo(), build_run_identifier: process.env.BROWSERSTACK_BUILD_RUN_IDENTIFIER, failed_tests_rerun: process.env[RERUN_ENV] || false, version_control: await getGitMetaData(), accessibility: { settings: options.accessibilityOptions }, browserstackAutomation: shouldAddServiceVersion(config, options.testObservability), framework_details: { frameworkName: "WebdriverIO-" + config.framework, frameworkVersion: bsConfig.bstackServiceVersion, sdkVersion: bsConfig.bstackServiceVersion, language: "ECMAScript", testFramework: { name: "WebdriverIO", version: bsConfig.bstackServiceVersion } }, product_map: getProductMapForBuildStartCall(bStackConfig, accessibilityAutomation), config: {} }; if (accessibilityAutomation && (isTurboScale(options) || data.browserstackAutomation === false)) { data.accessibility.settings ??= {}; data.accessibility.settings["includeEncodedExtension"] = true; } try { if (Object.keys(CrashReporter.userConfigForReporting).length === 0) { CrashReporter.userConfigForReporting = process.env.USER_CONFIG_FOR_REPORTING !== void 0 ? JSON.parse(process.env.USER_CONFIG_FOR_REPORTING) : {}; } } catch (error) { return BStackLogger.error(`[Crash_Report_Upload] Failed to parse user config while sending build start event due to ${error}`); } data.config = CrashReporter.userConfigForReporting; try { const url3 = `${DATA_ENDPOINT}/api/v2/builds`; const encodedAuth = Buffer.from(`${getObservabilityUser(options, config)}:${getObservabilityKey(options, config)}`, "utf8").toString("base64"); const headers = { ...DEFAULT_REQUEST_CONFIG.headers, Authorization: `Basic ${encodedAuth}` }; const response = await _fetch(url3, { method: "POST", headers, body: JSON.stringify(data) }); const jsonResponse = await response.json(); delete data?.accessibility?.settings?.includeEncodedExtension; BStackLogger.debug(`[Start_Build] Success response: ${JSON.stringify(jsonResponse)}`); process.env[TESTOPS_BUILD_COMPLETED_ENV] = "true"; if (jsonResponse.jwt) { process.env[BROWSERSTACK_TESTHUB_JWT] = jsonResponse.jwt; } if (jsonResponse.build_hashed_id) { process.env[BROWSERSTACK_TESTHUB_UUID] = jsonResponse.build_hashed_id; testOpsConfig_default.getInstance().buildHashedId = jsonResponse.build_hashed_id; BStackLogger.info(`Testhub started with id: ${testOpsConfig_default.getInstance()?.buildHashedId}`); } processLaunchBuildResponse(jsonResponse, options); launchBuildUsage.success(); return jsonResponse; } catch (error) { BStackLogger.debug(`TestHub build start failed: ${format(error)}`); if (!error.success) { launchBuildUsage.failed(error); logBuildError(error); return null; } } })); var validateCapsWithAppA11y = (platformMeta) => { if (platformMeta?.platform_name && String(platformMeta?.platform_name).toLowerCase() === "android" && (platformMeta?.platform_version && parseInt(platformMeta?.platform_version?.toString()) < 11)) { BStackLogger.warn("App Accessibility Automation tests are supported on OS version 11 and above for Android devices."); return false; } return true; }; var validateCapsWithA11y = (deviceName, platformMeta, chromeOptions) => { try { if (deviceName) { BStackLogger.warn("Accessibility Automation will run only on Desktop browsers."); return false; } if (platformMeta?.browser_name?.toLowerCase() !== "chrome") { BStackLogger.warn("Accessibility Automation will run only on Chrome browsers."); return false; } const browserVersion = platformMeta?.browser_version; if (!isUndefined(browserVersion) && !(browserVersion === "latest" || parseFloat(browserVersion + "") > 94)) { BStackLogger.warn("Accessibility Automation will run only on Chrome browser version greater than