Skip to content

Commit 14f2cba

Browse files
mschileclaude
andauthored
fix: resolve the cloud env when the urql client is created (#34536)
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent 15b4f2d commit 14f2cba

11 files changed

Lines changed: 91 additions & 16 deletions

File tree

cli/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
**Bugfixes:**
55

6+
- Fixed a regression in [15.20.0](#15-20-0) where the Cypress app sent all Cypress Cloud requests to `http://localhost:3000` instead of Cypress Cloud. Logging in could not complete, and the Runs and Debug pages reported no data. Addressed in [#34536](https://github.com/cypress-io/cypress/pull/34536).
67
- Fixed a regression in [15.18.0](#15-18-0) where pinning a command in the Command Log could leave the AUT snapshot permanently blank, with the pin stuck on. Stopping a run in open mode also no longer clears the AUT. Addressed in [#34502](https://github.com/cypress-io/cypress/pull/34502).
78

89
**Misc:**

packages/data-context/src/actions/EventCollectorActions.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,18 @@ type EventInputs = {
2424
* Defaults to staging when doing development. To override to production for development,
2525
* explicitly set process.env.CYPRESS_INTERNAL_ENV to 'production`
2626
*/
27-
const cloudEnv = (process.env.CYPRESS_INTERNAL_EVENT_COLLECTOR_ENV || 'production') as 'development' | 'staging' | 'production'
27+
function resolveEventCollectorEnv () {
28+
return (process.env.CYPRESS_INTERNAL_EVENT_COLLECTOR_ENV || 'production') as 'development' | 'staging' | 'production'
29+
}
2830

2931
export class EventCollectorActions {
3032
constructor (private ctx: DataContext) {
31-
debug('Using %s environment for Event Collection', cloudEnv)
33+
debug('Using %s environment for Event Collection', resolveEventCollectorEnv())
3234
}
3335

3436
async recordEvent (event: CollectibleEvent, includeMachineId: boolean): Promise<boolean> {
3537
try {
36-
const cloudUrl = this.ctx.cloud.getCloudUrl(cloudEnv)
38+
const cloudUrl = this.ctx.cloud.getCloudUrl(resolveEventCollectorEnv())
3739
const eventUrl = includeMachineId ? `${cloudUrl}/machine-collect` : `${cloudUrl}/anon-collect`
3840
const headers = {
3941
'Content-Type': 'application/json',

packages/data-context/src/sources/CloudDataSource.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ import { pathToArray } from 'graphql/jsutils/Path'
3131
export type CloudDataResponse<T = any> = ExecutionResult<T> & Partial<OperationResult<T | null>> & { executing?: Promise<ExecutionResult<T> & Partial<OperationResult<T | null>>> }
3232

3333
const debug = debugLib('cypress:data-context:sources:CloudDataSource')
34-
const cloudEnv = resolveCloudEnv()
3534

3635
// eslint-disable-next-line @typescript-eslint/no-unused-vars
3736
type StartsWith<T, Prefix extends string> = T extends `${Prefix}${infer _U}` ? T : never
@@ -117,7 +116,7 @@ export class CloudDataSource {
117116

118117
reset () {
119118
return this.#cloudUrqlClient = createClient({
120-
url: `${this.getCloudUrl(cloudEnv)}/test-runner-graphql`,
119+
url: `${this.getCloudUrl(resolveCloudEnv())}/test-runner-graphql`,
121120
exchanges: [
122121
dedupExchange,
123122
cacheExchange({

packages/data-context/test/unit/actions/EventCollectorActions.spec.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,5 +65,23 @@ describe('EventCollectorActions', () => {
6565

6666
expect(result).toBe(false)
6767
})
68+
69+
it('resolves the environment when the event is recorded, not when the module loads', async () => {
70+
const original = process.env.CYPRESS_INTERNAL_EVENT_COLLECTOR_ENV
71+
72+
process.env.CYPRESS_INTERNAL_EVENT_COLLECTOR_ENV = 'staging'
73+
74+
try {
75+
await actions.recordEvent({ campaign: '', medium: '', messageId: '', cohort: '' }, false)
76+
} finally {
77+
if (original === undefined) {
78+
delete process.env.CYPRESS_INTERNAL_EVENT_COLLECTOR_ENV
79+
} else {
80+
process.env.CYPRESS_INTERNAL_EVENT_COLLECTOR_ENV = original
81+
}
82+
}
83+
84+
expect(ctx.util.fetch).toHaveBeenNthCalledWith(1, 'https://cloud-staging.cypress.io/anon-collect', expect.anything())
85+
})
6886
})
6987
})

packages/data-context/test/unit/sources/CloudDataSource.spec.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,4 +348,36 @@ describe('CloudDataSource', () => {
348348
expect(fetchStub).toHaveBeenCalledTimes(1)
349349
})
350350
})
351+
352+
describe('cloud url', () => {
353+
const originalEnv = process.env.CYPRESS_INTERNAL_ENV
354+
355+
afterEach(() => {
356+
if (originalEnv === undefined) {
357+
delete process.env.CYPRESS_INTERNAL_ENV
358+
} else {
359+
process.env.CYPRESS_INTERNAL_ENV = originalEnv
360+
}
361+
})
362+
363+
it('does not freeze the environment at module load', async () => {
364+
process.env.CYPRESS_INTERNAL_ENV = 'production'
365+
366+
const source = new CloudDataSource({
367+
fetch: fetchStub,
368+
getUser: getUserStub,
369+
logout: logoutStub,
370+
invalidateClientUrqlCache: invalidateCacheStub,
371+
})
372+
373+
await source.executeRemoteGraphQL({
374+
fieldName: 'cloudViewer',
375+
operationDoc: FAKE_USER_QUERY,
376+
operationVariables: {},
377+
operationType: 'query',
378+
})
379+
380+
expect(fetchStub).toHaveBeenCalledWith('https://cloud.cypress.io/test-runner-graphql', expect.anything())
381+
})
382+
})
351383
})

packages/server/lib/cloud/api/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import type { AfterSpecDurations } from '@packages/types'
1717
import { agent } from '@packages/network'
1818
import type { CombinedAgent } from '@packages/network'
1919

20-
import { apiUrl, apiRoutes, makeRoutes } from '../routes'
20+
import { getApiUrl, apiRoutes, makeRoutes } from '../routes'
2121
import { getText } from '../../util/status_code'
2222
import * as enc from '../encryption'
2323
import getEnvInformationForProjectRoot from '../environment'
@@ -465,7 +465,7 @@ export default {
465465
projectId: options.projectId,
466466
testingType: options.testingType,
467467
cloudApi: {
468-
url: apiUrl,
468+
url: getApiUrl(),
469469
retryWithBackoff: this.retryWithBackoff,
470470
requestPromise: this.rp,
471471
},
@@ -634,6 +634,7 @@ export default {
634634
return retryWithBackoff(async (attemptIndex) => {
635635
const { projectRoot, timeout, ...preflightRequestBody } = preflightInfo
636636

637+
const apiUrl = getApiUrl()
637638
const preflightBaseProxy = apiUrl.replace('api', 'api-proxy')
638639

639640
const envInformation = await getEnvInformationForProjectRoot(projectRoot, process.pid.toString())

packages/server/lib/cloud/routes.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ import UrlParse from 'url-parse'
33

44
const app_config = require('../../config/app.json')
55

6-
export const apiUrl = app_config[process.env.CYPRESS_CONFIG_ENV || process.env.CYPRESS_INTERNAL_ENV || 'development'].api_url
6+
export const getApiUrl = (): string => {
7+
return app_config[process.env.CYPRESS_CONFIG_ENV || process.env.CYPRESS_INTERNAL_ENV || 'development'].api_url
8+
}
79

810
const CLOUD_ENDPOINTS = {
911
api: '',
@@ -42,10 +44,10 @@ const parseArgs = function (url, args: any[] = []) {
4244
return url
4345
}
4446

45-
const _makeRoutes = (baseUrl: string, routes: typeof CLOUD_ENDPOINTS) => {
47+
const _makeRoutes = (baseUrl: string | (() => string), routes: typeof CLOUD_ENDPOINTS) => {
4648
return _.reduce(routes, (memo, value, key) => {
4749
memo[key] = function (...args: any[]) {
48-
let url = new UrlParse(baseUrl, true)
50+
let url = new UrlParse(typeof baseUrl === 'function' ? baseUrl() : baseUrl, true)
4951

5052
if (value) {
5153
url.set('pathname', value)
@@ -62,6 +64,6 @@ const _makeRoutes = (baseUrl: string, routes: typeof CLOUD_ENDPOINTS) => {
6264
}, {} as Record<keyof typeof CLOUD_ENDPOINTS, (...args: any[]) => string>)
6365
}
6466

65-
export const apiRoutes = _makeRoutes(apiUrl, CLOUD_ENDPOINTS)
67+
export const apiRoutes = _makeRoutes(getApiUrl, CLOUD_ENDPOINTS)
6668

6769
export const makeRoutes = (baseUrl) => _makeRoutes(baseUrl, CLOUD_ENDPOINTS)

packages/server/lib/cloud/studio/StudioLifecycleManager.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,7 @@ export class StudioLifecycleManager {
293293
projectId: currentProjectOptions.projectSlug,
294294
testingType: cfg.testingType,
295295
cloudApi: {
296-
url: routes.apiUrl,
296+
url: routes.getApiUrl(),
297297
retryWithBackoff: api.retryWithBackoff,
298298
requestPromise: api.rp,
299299
},

packages/server/test/unit/cloud/api/api_spec.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -419,18 +419,25 @@ describe('lib/cloud/api', () => {
419419

420420
describe('errors', () => {
421421
it('[F1] POST /preflight TimeoutError', () => {
422-
preflightNock(API_BASEURL)
423-
.times(2)
422+
const scopeProxy = preflightNock(API_PROD_PROXY_BASEURL)
423+
.delayConnection(5000)
424+
.reply(200, {})
425+
426+
const scopeApi = preflightNock(API_PROD_BASEURL)
424427
.delayConnection(5000)
425428
.reply(200, {})
426429

427-
return api.sendPreflight({
430+
return prodApi.sendPreflight({
431+
projectId: 'abc123',
428432
timeout: 100,
429433
})
430434
.then(() => {
431435
throw new Error('should have thrown here')
432436
})
433437
.catch((err) => {
438+
scopeProxy.done()
439+
scopeApi.done()
440+
434441
expect(err.message).to.eq('Error: ESOCKETTIMEDOUT')
435442
})
436443
})

packages/server/test/unit/cloud/routes_spec.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,5 +96,18 @@ describe('lib/cloud/routes', () => {
9696

9797
expect(routes().apiRoutes.api()).to.eq('https://api-staging.cypress.io/')
9898
})
99+
100+
it('resolves per call rather than when the module is evaluated', () => {
101+
process.env.CYPRESS_INTERNAL_ENV = 'staging'
102+
103+
const loaded = routes()
104+
105+
expect(loaded.apiRoutes.api()).to.eq('https://api-staging.cypress.io/')
106+
107+
process.env.CYPRESS_INTERNAL_ENV = 'production'
108+
109+
expect(loaded.apiRoutes.api()).to.eq('https://api.cypress.io/')
110+
expect(loaded.getApiUrl()).to.eq('https://api.cypress.io/')
111+
})
99112
})
100113
})

0 commit comments

Comments
 (0)