Repository navigation
fix: recognize bsstag hosts and localize session REST host on named staging envs (SDK-6948) #275
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
c7a5eba
832f42c
b01c4dc
5aa7589
1b89b02
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| --- | ||
| "@wdio/browserstack-service": patch | ||
| --- | ||
|
|
||
| - SDK-6948: with `BROWSERSTACK_STAGING_ENV` set to a bare env name (e.g. `k8s`), `*.bsstag.com` hubs are now treated as BrowserStack, so session status marking and Automate <-> TRA session linking work on internal staging envs. Production behaviour is unchanged. | ||
| - SDK-6948: the session REST base URL in `service.ts` now uses the binary-supplied API host (`APIUtils`) instead of a hardcoded production host. For GRR-enabled accounts this is the regional api host the binary already uses to mark sessions. The classic (non-CLI) flow on a named staging env is not supported; `_printSessionURL` logs a non-session reply at debug instead of throwing. | ||
| - SDK-6948: `BROWSERSTACK_STAGING_ENV` is trimmed (blank means unset), and `bsstag` hosts are matched case-insensitively. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -37,6 +37,7 @@ import { EVENTS } from './instrumentation/performance/constants.js' | |
| import { BrowserstackCLI } from './cli/index.js' | ||
| import { drainSkipReports, markTestStarted, reportSuiteSkipped } from './cli/skipReporter.js' | ||
| import { CLIUtils } from './cli/cliUtils.js' | ||
| import APIUtils from './cli/apiUtils.js' | ||
|
|
||
| import { _fetch as fetch } from './fetchWrapper.js' | ||
| import AutomationFramework from './cli/frameworks/automationFramework.js' | ||
|
|
@@ -246,12 +247,18 @@ export default class BrowserstackService implements Services.ServiceInstance { | |
|
|
||
| // Ensure capabilities are not null in case of multiremote | ||
|
|
||
| // Resolved here, after beforeSession's CLI bootstrap has applied the binary-supplied | ||
| // GRR/staging hosts (APIUtils.updateURLSForGRR). Without the CLI, or when the config server | ||
| // returns the default grr_urls, these are the production hosts; for a GRR-enabled account | ||
| // they follow the regional api host the binary already uses to mark the session. | ||
| this._sessionBaseUrl = `${APIUtils.BROWSERSTACK_AUTOMATE_API_URL}/automate/sessions` | ||
|
|
||
| if (this._isAppAutomate()) { | ||
| this._sessionBaseUrl = 'https://api-cloud.browserstack.com/app-automate/sessions' | ||
| this._sessionBaseUrl = `${APIUtils.BROWSERSTACK_AA_API_CLOUD_URL}/app-automate/sessions` | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| } | ||
|
|
||
| if (this._turboScale) { | ||
| this._sessionBaseUrl = 'https://api.browserstack.com/automate-turboscale/v1/sessions' | ||
| this._sessionBaseUrl = `${APIUtils.BROWSERSTACK_AUTOMATE_API_URL}/automate-turboscale/v1/sessions` | ||
| } | ||
|
|
||
| this._scenariosThatRan = [] | ||
|
|
@@ -1340,7 +1347,14 @@ export default class BrowserstackService implements Services.ServiceInstance { | |
| headers | ||
| }) | ||
| const res = response.clone() | ||
| browserUrl = (await res.json()).automation_session.browser_url | ||
| const session = (await res.json())?.automation_session | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Suggestion: [ROBUSTNESS] The guard only covers JSON replies
if (!response.ok) {
BStackLogger.debug(`Could not fetch Browserstack session URL (${response.status}) at ${sessionUrl}`)
return
}A test with |
||
| if (!session) { | ||
| // e.g. a 401 when the classic (non-CLI) flow targets a named staging env, whose | ||
| // session REST host is only known to the CLI; don't throw out of before(). | ||
| BStackLogger.debug(`Could not fetch Browserstack session URL (${response.status}) at ${sessionUrl}`) | ||
| return | ||
| } | ||
| browserUrl = session.browser_url | ||
| } | ||
|
|
||
| if (!this._browser) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1142,6 +1142,28 @@ export function getLtsSessionId(): string { | |
| return process.env[BROWSERSTACK_LTS_SESSION_ID] || '' | ||
| } | ||
|
|
||
| /** | ||
| * SDK-6948: a named internal staging env (`BROWSERSTACK_STAGING_ENV` set to a bare token such as | ||
| * `k8s`) serves its hub/cdp from `*.bsstag.com` (e.g. `hub-k8s.bsstag.com`). Treat those hosts as | ||
| * BrowserStack so session marking and Automate <-> TRA linking work there. A dotted value (full | ||
| * host) or an unset/blank variable leaves the production-only host checks unchanged. The env value is | ||
| * trimmed and the hostname compared case-insensitively (shared rule across SDKs). | ||
| * `BROWSERSTACK_ENV` (trimmed, lowercased) of staging/stag/preprod/pre-prod also selects the bsstag | ||
| * hosts, matching node-agent constants and the binary's getEnvironmentUrl. | ||
| */ | ||
| const BSTAG_BROWSERSTACK_ENVS = ['staging', 'stag', 'preprod', 'pre-prod'] | ||
|
|
||
| export function isStagingEnvBsstagHost(hostname: string): boolean { | ||
| const stagingEnv = process.env.BROWSERSTACK_STAGING_ENV?.trim() | ||
| const namedStagingEnv = Boolean(stagingEnv) && !stagingEnv!.includes('.') | ||
| const browserstackEnv = process.env.BROWSERSTACK_ENV?.trim().toLowerCase() || '' | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Warning: [CORRECTNESS/DOCS]
|
||
| if (!namedStagingEnv && !BSTAG_BROWSERSTACK_ENVS.includes(browserstackEnv)) { | ||
| return false | ||
| } | ||
| const host = hostname.toLowerCase() | ||
| return host === 'bsstag.com' || host.endsWith('.bsstag.com') | ||
| } | ||
|
|
||
| export function getCloudProvider(browser: WebdriverIO.Browser | WebdriverIO.MultiRemoteBrowser): string { | ||
| // NOTE: do NOT branch on isLoadTestingSession() here. getCloudProvider is | ||
| // shared with Automate-side guards (automateModule onBefore/onAfterTest, | ||
|
|
@@ -1157,11 +1179,11 @@ export function getCloudProvider(browser: WebdriverIO.Browser | WebdriverIO.Mult | |
| // Loop through all instances | ||
| for (const instanceName of browser.instances) { | ||
| const instance = (browser as any)[instanceName] as WebdriverIO.Browser | ||
| if (instance.options && instance.options.hostname && instance.options.hostname.includes('browserstack')) { | ||
| if (instance.options && instance.options.hostname && (instance.options.hostname.includes('browserstack') || isStagingEnvBsstagHost(instance.options.hostname))) { | ||
| return 'browserstack' | ||
| } | ||
| } | ||
| } else if (browser.options && browser.options.hostname && browser.options.hostname.includes('browserstack')) { // Single browser instance | ||
| } else if (browser.options && browser.options.hostname && (browser.options.hostname.includes('browserstack') || isStagingEnvBsstagHost(browser.options.hostname))) { // Single browser instance | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Suggestion — [PROCESS] #56 / #65 are still OPEN (not on main), edit the same three lines, and would reintroduce unconditional
|
||
| return 'browserstack' | ||
| } | ||
| return 'unknown_grid' | ||
|
|
@@ -1270,7 +1292,7 @@ export function isBrowserstackInfra(config: BrowserstackConfig & Options.Testrun | |
| // In case hostname is not present anywhere in the config, it returns true by default as hostname is not a mandatory parameter in the config | ||
|
|
||
| const isBrowserstack = (str: string ): boolean => { | ||
| return str === 'browserstack.com' || str.endsWith('.browserstack.com') | ||
| return str === 'browserstack.com' || str.endsWith('.browserstack.com') || isStagingEnvBsstagHost(str) | ||
| } | ||
|
|
||
| if ((config.hostname) && !isBrowserstack(config.hostname)) { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Problem
APIUtilsis localized in only one place:BrowserstackCLI.loadModules()→APIUtils.updateURLSForGRR(config.apis)(src/cli/index.ts:156). On the classic flow, that call never runs. The classic flow covers multiremote (BROWSERSTACK_IS_MULTIREMOTE=true), frameworks outsideCLISupportedFrameworks, and a CLI bootstrap that errors and is logged atcli/index.ts:104. On that flowAPIUtils.BROWSERSTACK_AUTOMATE_API_URLstayshttps://api.browserstack.com, so line 252 still produces the production host.Before this PR that did not matter on staging.
getCloudProviderreturnedunknown_gridforhub-k8s.bsstag.com, soisBrowserstackSession()was false and_printSessionURL,_updateand_updateJoball returned early. Fix (B) now makesisBrowserstackSession()true on staging, including multiremote. Because the multiremote branch ofgetCloudProviderwas also changed, the classic flow now actively calls these:GET https://api.browserstack.com/automate/sessions/<id>.jsoninbefore()(_printSessionURL)PUT …/<id>.jsoninafter()(_updateJob, gated!isRunning()) and inonReload(_update)All three carry the staging
user:key. That is exactly what the PR body says (C) fixes ("On staging it sent staging creds to the production API"). Two things follow. Session status is never marked on staging for these runs._printSessionURLalso dereferences(await res.json()).automation_session.browser_urlwithout a guard, so a 401 JSON reply throws a TypeError out ofbefore(). WDIO logs it rather than failing the test, but it is new noise. This is anti-pattern #23 (one path fixed, the parallel path not). Production is unaffected, because with the variable unsetisStagingEnvBsstagHostreturns false.Suggested Fix
When the CLI is not running and
BROWSERSTACK_STAGING_ENVis a bare token, derive the staging API host locally, mirroring binary #1889'sstagingApiHost. Do it in one helper next toisStagingEnvBsstagHostso both checks share the "named env" definition:If that is out of scope, document the classic flow as unsupported on staging in the PR body and the changeset. Add a unit test that pins the classic + staging host either way.
Confidence: 🟢 Objectively traceable.
updateURLSForGRRhas a single caller, in CLIloadModules, and the gates onisBrowserstackSession/!isRunning()are in the file. Matches shared anti-pattern #23 (parallel-path drift).There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Valid, thanks. Following the cross-SDK decision for this ticket, I did not add staging host-name derivation to the SDK. Instead, b01c4dc: (1) The PR body now documents that the classic (non-CLI) flow is not supported on named staging envs. It covers multiremote, frameworks outside
CLISupportedFrameworks, and a failed CLI bootstrap; in all of themAPIUtilskeeps the prod hosts, so marking there gets a 401. (2)_printSessionURLno longer dereferences.automation_session.browser_urlunguarded. A reply withoutautomation_session(e.g. a 401) logsCould not fetch Browserstack session URL (<status>)at debug and returns, instead of throwing aTypeErrorout ofbefore(). Test:logs at debug instead of throwing when the session REST reply has no automation_session, which fails without the guard. Production is unaffected, becauseisStagingEnvBsstagHostis false with the variable unset.