Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/port-v8-missing-monorepo-commits.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@wdio/browserstack-service": patch
---

Port missing upstream monorepo (webdriverio/webdriverio) v8 commits to keep the standalone v8 line at parity:

- webdriverio/webdriverio#15231 — Test Management: add `testManagementOptions.testPlanId` support (env `BROWSERSTACK_TEST_PLAN_ID` / `--browserstack.testManagementOptions.testPlanId` CLI arg / config), forward `test_management.test_plan_id` in the build-start request, strip CLI-only caps (`NOT_ALLOWED_KEYS_IN_CAPS`, incl. `testManagementOptions`) before hitting the hub, and surface build-start errors.
- webdriverio/webdriverio#15217 — gRPC: raise the send/receive message size limit to 20 MB to accommodate large extension payloads.
- webdriverio/webdriverio#15146 — Exit handling: terminate the CLI process with `SIGINT` instead of `SIGKILL` on Unix.
- webdriverio/webdriverio#15200 — Logging: redact credentials (username/accesskey/user/key) from CLI log output before writing to file and console.
36 changes: 24 additions & 12 deletions packages/browserstack-service/src/cli/cliLogger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,21 @@ export class BStackLogger {
public static logFolderPath = path.join(process.cwd(), 'logs')
private static logFileStream: fs.WriteStream | null

private static redactCredentials(logMessage: string): string {
return logMessage
.replace(/([\\'"]*(?:username|accesskey|user|key)[\\'"]*\s*[:=]\s*[\\'"]*)([^\\'"\s,}]+)/gi, '$1')
.replace(/([?&](?:username|access_key|accesskey|user|key)=)([^&#\s]+)/gi, '$1')
}

static logToFile(logMessage: string, logLevel: string) {
try {
const redactedMessage = this.redactCredentials(logMessage)
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))
this.logFileStream.write(this.formatLog(redactedMessage, logLevel))
}
} catch (error) {
log.debug(`Failed to log to file. Error ${error}`)
Expand All @@ -33,32 +40,37 @@ export class BStackLogger {
}

public static info(message: string) {
this.logToFile(message, 'info')
log.info(message)
const redactedMessage = this.redactCredentials(message)
this.logToFile(redactedMessage, 'info')
log.info(redactedMessage)
}

public static error(message: string) {
this.logToFile(message, 'error')
log.error(message)
const redactedMessage = this.redactCredentials(message)
this.logToFile(redactedMessage, 'error')
log.error(redactedMessage)
}

public static debug(message: string, param?: unknown) {
this.logToFile(message, 'debug')
const redactedMessage = this.redactCredentials(message)
this.logToFile(redactedMessage, 'debug')
if (param) {
log.debug(message, param)
log.debug(redactedMessage, param)
} else {
log.debug(message)
log.debug(redactedMessage)
}
}

public static warn(message: string) {
this.logToFile(message, 'warn')
log.warn(message)
const redactedMessage = this.redactCredentials(message)
this.logToFile(redactedMessage, 'warn')
log.warn(redactedMessage)
}

public static trace(message: string) {
this.logToFile(message, 'trace')
log.trace(message)
const redactedMessage = this.redactCredentials(message)
this.logToFile(redactedMessage, 'trace')
log.trace(redactedMessage)
}

public static clearLogger() {
Expand Down
12 changes: 11 additions & 1 deletion packages/browserstack-service/src/cli/cliUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import { EVENTS as PerformanceEvents } from '../instrumentation/performance/cons
import { BStackLogger as logger } from './cliLogger.js'
import { UPDATED_CLI_ENDPOINT } from '../constants.js'
import type { Options, Capabilities } from '@wdio/types'
import type { BrowserstackConfig, BrowserstackOptions, TestObservabilityOptions } from '../types.js'
import type { BrowserstackConfig, BrowserstackOptions, TestManagementOptions, TestObservabilityOptions } from '../types.js'
import { TestFrameworkConstants } from './frameworks/constants/testFrameworkConstants.js'
import APIUtils from './apiUtils.js'

Expand Down Expand Up @@ -63,6 +63,7 @@ export class CLIUtils {
modifiedOpts.browserStackLocalOptions = modifiedOpts.opts
delete modifiedOpts.opts
}
delete modifiedOpts.testManagementOptions

modifiedOpts.testContextOptions = {
skipSessionName: isFalse(modifiedOpts.setSessionName),
Expand All @@ -88,6 +89,10 @@ export class CLIUtils {

const isNonBstackA11y = isTurboScale(options) || !shouldAddServiceVersion(config as Options.Testrunner, options.testObservability)
const observabilityOptions: TestObservabilityOptions = options.testObservabilityOptions || {}
const testManagementOptions: TestManagementOptions = options.testManagementOptions || {}
const testPlanId = typeof testManagementOptions.testPlanId === 'string'
? testManagementOptions.testPlanId.trim()
: ''
const binconfig: Record<string, unknown> = {
userName: observabilityOptions.user || config.user,
accessKey: observabilityOptions.key || config.key,
Expand All @@ -100,6 +105,11 @@ export class CLIUtils {
binconfig.buildName = observabilityOptions.buildName || binconfig.buildName
binconfig.projectName = observabilityOptions.projectName || binconfig.projectName
binconfig.buildTag = this.getObservabilityBuildTags(observabilityOptions, buildTag) || []
if (testPlanId.length > 0) {
binconfig.testManagementOptions = {
testPlanId
}
}

let caps = capabilities
if (capabilities && !Array.isArray(capabilities)) {
Expand Down
15 changes: 11 additions & 4 deletions packages/browserstack-service/src/cli/grpcClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ import PerformanceTester from '../instrumentation/performance/performance-tester
import * as PERFORMANCE_SDK_EVENTS from '../instrumentation/performance/constants.js'
import { BStackLogger } from './cliLogger.js'

// Increased from default 4 MB to accommodate large extension payloads
const GRPC_MESSAGE_LIMIT = 20 * 1024 * 1024 // 20 MB in bytes

/**
* GrpcClient - Singleton class for managing gRPC client connections
*
Expand Down Expand Up @@ -122,20 +125,24 @@ export class GrpcClient {
throw new Error('Unable to determine gRPC server listen address')
}

const channelOptions = {
'grpc.keepalive_time_ms': 10000,
'grpc.max_send_message_length': GRPC_MESSAGE_LIMIT,
'grpc.max_receive_message_length': GRPC_MESSAGE_LIMIT,
}

// Create a channel
this.channel = new grpcChannel(
listenAddress,
grpcCredentials.createInsecure(),
{
'grpc.keepalive_time_ms': 10000
}
channelOptions
)

// Create a client using the channel
this.client = new SDKClient(
listenAddress,
grpcCredentials.createInsecure(),
{}
channelOptions
)

this.logger.info(`Connected to gRPC server at ${listenAddress}`)
Expand Down
58 changes: 58 additions & 0 deletions packages/browserstack-service/src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ export class BrowserstackCLI {
this.modules[AutomateModule.MODULE_NAME] = new AutomateModule(this.browserstackConfig as Options.Testrunner)

if (startBinResponse.testhub) {
this.logBuildStartErrors(startBinResponse.testhub)
process.env[TESTOPS_BUILD_COMPLETED_ENV] = 'true'
if (startBinResponse.testhub.jwt) {
process.env[BROWSERSTACK_TESTHUB_JWT] = startBinResponse.testhub.jwt
Expand Down Expand Up @@ -170,6 +171,63 @@ export class BrowserstackCLI {
this.configureModules()
}

private logBuildStartErrors(testhub: unknown) {
if (process.env.WDIO_WORKER_ID) {
return
}

try {
const rawErrors = (testhub as { errors?: unknown } | null)?.errors
const parsedErrors = this.parseBuildStartErrors(rawErrors)

for (const [errorKey, errorDetails] of Object.entries(parsedErrors)) {
const errorMessage = errorDetails?.message
if (!errorMessage) {
continue
}

const formattedMessage = `[Build] ${errorKey}: ${errorMessage}`
if (errorDetails.type === 'info') {
BStackLogger.info(formattedMessage)
} else if (errorDetails.type === 'warn') {
BStackLogger.warn(formattedMessage)
} else {
BStackLogger.error(formattedMessage)
}
}
} catch (error) {
BStackLogger.debug(`Failed to log build-start errors: ${util.format(error)}`)
}
}

private parseBuildStartErrors(rawErrors: unknown): Record<string, { message?: string, type?: string }> {
if (!rawErrors) {
return {}
}

try {
if (typeof rawErrors === 'string') {
return JSON.parse(rawErrors) as Record<string, { message?: string, type?: string }>
}

if (Buffer.isBuffer(rawErrors)) {
return JSON.parse(rawErrors.toString('utf8')) as Record<string, { message?: string, type?: string }>
}

if (rawErrors instanceof Uint8Array) {
return JSON.parse(Buffer.from(rawErrors).toString('utf8')) as Record<string, { message?: string, type?: string }>
}

if (typeof rawErrors === 'object') {
return rawErrors as Record<string, { message?: string, type?: string }>
}
} catch (error) {
BStackLogger.debug(`Failed to parse build-start errors from CLI response: ${util.format(error)}`)
}

return {}
}

/**
* Configure modules
* @returns {Promise<void>}
Expand Down
3 changes: 2 additions & 1 deletion packages/browserstack-service/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ export const BSTACK_SERVICE_VERSION = bstackServiceVersion
export const UPDATED_CLI_ENDPOINT = 'sdk/v1/update_cli'
export const CLI_STOP_TIMEOUT = 3000

export const NOT_ALLOWED_KEYS_IN_CAPS = ['includeTagsInTestingScope', 'excludeTagsInTestingScope']
export const NOT_ALLOWED_KEYS_IN_CAPS = ['includeTagsInTestingScope', 'excludeTagsInTestingScope', 'testManagementOptions']
export const BROWSERSTACK_TEST_PLAN_ID = 'BROWSERSTACK_TEST_PLAN_ID'

export const LOGS_FILE = 'logs/bstack-wdio-service.log'
export const CLI_DEBUG_LOGS_FILE = 'log/sdk-cli-debug.log'
Expand Down
4 changes: 2 additions & 2 deletions packages/browserstack-service/src/exitHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ export function setupExitHandlers() {
cliProcess.kill('SIGTERM')
BStackLogger.debug('CLI process terminated successfully with SIGTERM (Windows)')
} else {
cliProcess.kill('SIGKILL')
BStackLogger.debug('CLI process terminated successfully with SIGKILL (Unix)')
cliProcess.kill('SIGINT')
BStackLogger.debug('CLI process terminated successfully with SIGINT (Unix)')
}
} catch (processError) {
BStackLogger.debug(`CLI process termination error: ${processError}`)
Expand Down
37 changes: 37 additions & 0 deletions packages/browserstack-service/src/launcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,8 @@ export default class BrowserstackLauncherService implements Services.ServiceInst
this._updateObjectTypeCaps(capabilities, 'accessibilityOptions', {})
}

this._removeCliOnlyCapabilityOptions(capabilities)

if (shouldSetupPercy) {
try {
const bestPlatformPercyCaps = getBestPlatformForPercySnapshot(capabilities)
Expand Down Expand Up @@ -752,6 +754,41 @@ export default class BrowserstackLauncherService implements Services.ServiceInst
})()
}

private _removeCliOnlyCapabilityOptions(capabilities?: Capabilities.RemoteCapabilities) {
if (!capabilities || typeof capabilities !== 'object') {
return
}

const strip = (capability: WebdriverIO.Capabilities) => {
const capabilityRecord = capability as Record<string, unknown>
const bstackOptions = capabilityRecord['bstack:options'] as Record<string, unknown> | undefined
if (bstackOptions && typeof bstackOptions === 'object') {
NOT_ALLOWED_KEYS_IN_CAPS.forEach(key => delete bstackOptions[key])
}
NOT_ALLOWED_KEYS_IN_CAPS.forEach(key => delete capabilityRecord[`browserstack.${key}`])

const alwaysMatch = (capability as WebdriverIO.Capabilities & { alwaysMatch?: WebdriverIO.Capabilities }).alwaysMatch
if (alwaysMatch && typeof alwaysMatch === 'object') {
strip(alwaysMatch)
}
}

if (Array.isArray(capabilities)) {
capabilities
.flatMap((c) => {
if (Object.values(c).length > 0 && Object.values(c).every(c => typeof c === 'object' && c.capabilities)) {
return Object.values(c).map((o) => o.capabilities) as WebdriverIO.Capabilities[]
}
return c as WebdriverIO.Capabilities
})
.forEach(strip)
} else {
Object.entries(capabilities as Capabilities.MultiRemoteCapabilities).forEach(([, caps]) => {
strip(caps.capabilities as WebdriverIO.Capabilities)
})
}
}

_updateObjectTypeCaps(capabilities?: Capabilities.RemoteCapabilities, capType?: string, value?: { [key: string]: any }) {
try {
if (Array.isArray(capabilities)) {
Expand Down
9 changes: 8 additions & 1 deletion packages/browserstack-service/src/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import type { BrowserstackConfig, BrowserstackOptions, MultiRemoteAction, Sessio
import type { Pickle, Feature, ITestCaseHookParameter, CucumberHook } from './cucumber-types.js'
import InsightsHandler from './insights-handler.js'
import TestReporter from './reporter.js'
import { DEFAULT_OPTIONS, PERF_MEASUREMENT_ENV } from './constants.js'
import { DEFAULT_OPTIONS, NOT_ALLOWED_KEYS_IN_CAPS, PERF_MEASUREMENT_ENV } from './constants.js'
import CrashReporter from './crash-reporter.js'
import AccessibilityHandler from './accessibility-handler.js'
import { BStackLogger } from './bstackLogger.js'
Expand Down Expand Up @@ -156,6 +156,13 @@ export default class BrowserstackService implements Services.ServiceInstance {
const instance = AutomationFramework.getTrackedInstance() as AutomationFrameworkInstance
const caps = AutomationFramework.getState(instance, AutomationFrameworkConstants.KEY_CAPABILITIES)
Object.assign(capabilities, caps)

// Strip CLI-only options that BrowserStack hub doesn't accept
const bstackOptions = (capabilities as Record<string, unknown>)['bstack:options'] as Record<string, unknown> | undefined
if (bstackOptions && typeof bstackOptions === 'object') {
NOT_ALLOWED_KEYS_IN_CAPS.forEach(key => delete bstackOptions[key])
}
NOT_ALLOWED_KEYS_IN_CAPS.forEach(key => delete (capabilities as Record<string, unknown>)[`browserstack.${key}`])
}
} catch (err) {
BStackLogger.error(`Error while connecting to Browserstack CLI: ${err}`)
Expand Down
9 changes: 9 additions & 0 deletions packages/browserstack-service/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ export interface TestReportingOptions {
key?: string
}

export interface TestManagementOptions {
testPlanId?: string,
}

export interface RunSmartSelectionOptions {
enabled?: boolean,
mode?: string,
Expand Down Expand Up @@ -105,6 +109,11 @@ export interface BrowserstackConfig {
* For e.g. buildName, projectName, BrowserStack access credentials, etc.
*/
testReportingOptions?: TestReportingOptions;
/**
* Set the Test Management related config options under this key.
* Currently supports testPlanId.
*/
testManagementOptions?: TestManagementOptions;
/**
* Set this to true to enable BrowserStack Percy which will take screenshots
* and snapshots for your tests run on Browserstack
Expand Down
Loading
Loading