Skip to content

Commit eebce9b

Browse files
authored
Merge branch 'develop' into chore/types-for-modes
2 parents a108da0 + ead3b08 commit eebce9b

File tree

12 files changed

+297
-22
lines changed

12 files changed

+297
-22
lines changed

cli/CHANGELOG.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
<!-- See the ../guides/writing-the-cypress-changelog.md for details on writing the changelog. -->
2+
## 15.13.1
3+
4+
_Released 03/31/2026 (PENDING)_
5+
6+
**Performance:**
7+
8+
- When recording to Cypress Cloud, the App now sends a smaller snapshot of your project config, which reduces payload size and can make Cloud recording faster. Addressed in [#33517](https://github.com/cypress-io/cypress/pull/33517).
9+
210
## 15.13.0
311

4-
_Released 03/24/2026 (PENDING)_
12+
_Released 03/24/2026_
513

614
**Features:**
715

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "cypress",
3-
"version": "15.12.0",
3+
"version": "15.13.0",
44
"description": "Cypress is a next generation front end testing tool built for the modern web",
55
"private": true,
66
"scripts": {

packages/app/cypress/e2e/support/e2eSupport.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,23 @@ beforeEach(() => {
2323
cy.mockNodeCloudRequest({ url: '/studio/metrics', method: 'post', body: {}, persist: true })
2424
})
2525

26+
afterEach(() => {
27+
const specPath = Cypress.spec.relative.replace(/\\/g, '/')
28+
29+
if (!specPath.includes('e2e/studio/')) {
30+
return
31+
}
32+
33+
// reset studio after each test to avoid triggering the browser's unsaved changes dialog in between tests
34+
cy.get('body').then(($body) => {
35+
const $btn = $body.find('[data-cy="studio-reset-button"]')
36+
37+
if ($btn.length) {
38+
cy.wrap($btn).click({ force: true })
39+
}
40+
})
41+
})
42+
2643
function e2eTestingTypeIsSelected () {
2744
cy.findByTestId('specs-testing-type-header').within(() => {
2845
cy.findByTestId('testing-type-switch').contains('button', 'E2E').should('have.attr', 'aria-selected', 'true')

packages/config/src/browser.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,23 @@ export const getPublicConfigKeys = () => {
134134
return publicConfigKeys
135135
}
136136

137+
/**
138+
* Keys allowed on Cypress Cloud recording payloads: public config options (same basis as
139+
* config.resolved) plus flattened component-testing-only fields not listed as top-level
140+
* `options[].name` entries (see mergeDefaults in project/utils).
141+
*/
142+
const cloudRecordingConfigExtraKeys = ['devServer', 'devServerConfig', 'indexHtmlFile'] as const
143+
144+
let cloudRecordingConfigKeysCache: string[] | undefined
145+
146+
export const getCloudRecordingConfigKeys = (): string[] => {
147+
if (!cloudRecordingConfigKeysCache) {
148+
cloudRecordingConfigKeysCache = [...publicConfigKeys, ...cloudRecordingConfigExtraKeys]
149+
}
150+
151+
return cloudRecordingConfigKeysCache
152+
}
153+
137154
export const matchesConfigKey = (key: string) => {
138155
if (_.has(defaultValues, key)) {
139156
return key

packages/config/test/index.spec.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,25 @@ describe('config/src/index', () => {
8888
})
8989
})
9090

91+
describe('.getCloudRecordingConfigKeys', () => {
92+
it('includes every public config key plus component recording extras', () => {
93+
const cloudKeys = configUtil.getCloudRecordingConfigKeys()
94+
const publicKeys = configUtil.getPublicConfigKeys()
95+
96+
for (const key of publicKeys) {
97+
expect(cloudKeys).toContain(key)
98+
}
99+
100+
expect(cloudKeys).toContain('devServer')
101+
expect(cloudKeys).toContain('devServerConfig')
102+
expect(cloudKeys).toContain('indexHtmlFile')
103+
})
104+
105+
it('returns the same array instance on each call', () => {
106+
expect(configUtil.getCloudRecordingConfigKeys()).toBe(configUtil.getCloudRecordingConfigKeys())
107+
})
108+
})
109+
91110
describe('.matchesConfigKey', () => {
92111
it('returns normalized key when config key has a default value', () => {
93112
let normalizedKey = configUtil.matchesConfigKey('EXEC_TIMEOUT')

packages/driver/cypress/e2e/commands/files.cy.js

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -92,20 +92,19 @@ describe('src/cy/commands/files', () => {
9292

9393
err.code = 'ENOENT'
9494

95-
let retries = 0
96-
97-
cy.on('command:retry', () => {
98-
retries += 1
99-
})
100-
10195
Cypress.backend.withArgs('run:privileged')
10296
.onFirstCall()
10397
.rejects(err)
10498
.onSecondCall()
10599
.resolves(okResponse)
106100

107101
cy.readFile('foo.json').then(() => {
108-
expect(retries).to.eq(2)
102+
// Verify two calls were indeed made: the first one to fail, and the second one to succeed.
103+
const readFilePrivilegedCalls = Cypress.backend.getCalls().filter(
104+
(c) => c.args[0] === 'run:privileged' && c.args[1]?.commandName === 'readFile',
105+
)
106+
107+
expect(readFilePrivilegedCalls.length).to.eq(2)
109108
})
110109
})
111110

packages/server/lib/config.ts

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,45 @@
11
import _ from 'lodash'
2-
import * as configUtils from '@packages/config'
2+
import { getCloudRecordingConfigKeys, setUrls } from '@packages/config'
33

4-
export const setUrls = configUtils.setUrls
4+
export { setUrls }
5+
6+
const devServerConfigRecordingPreservedKeys = ['bundler', 'framework'] as const
7+
8+
function sanitizeDevServerConfigForRecording (devServerConfig: Record<string, unknown>) {
9+
const preserved = _.pick(devServerConfig, devServerConfigRecordingPreservedKeys)
10+
const rest = _.omit(devServerConfig, devServerConfigRecordingPreservedKeys)
11+
12+
return {
13+
...preserved,
14+
..._.mapValues(rest, (val) => `omitted: ${typeof val}`),
15+
}
16+
}
17+
18+
function sanitizeEnvLikeForRecording (obj: Record<string, unknown>) {
19+
return _.mapValues(obj ?? {}, (val) => `omitted: ${typeof val}`)
20+
}
521

622
// Strips out values that can be aribitrarily sized / are duplicated from config
7-
// payload sent for recording
23+
// payload sent for recording (env and expose values replaced with typeof placeholders)
824
export function filterRuntimeConfigForRecording (config) {
9-
const { rawJson, devServer, env, resolved, ...configRest } = config
25+
const { rawJson, devServer, devServerConfig, env, expose, resolved, ...configRest } = config
1026
const { webpackConfig, viteConfig, ...devServerRest } = devServer ?? {}
1127
const resultConfig = { ...configRest }
1228

1329
if (env) {
14-
resultConfig.env = _.mapValues(env ?? {}, (val, key) => `omitted: ${typeof val}`)
30+
resultConfig.env = sanitizeEnvLikeForRecording(env)
31+
}
32+
33+
if (expose) {
34+
resultConfig.expose = sanitizeEnvLikeForRecording(expose)
35+
}
36+
37+
if (devServerConfig !== undefined) {
38+
if (_.isPlainObject(devServerConfig)) {
39+
resultConfig.devServerConfig = sanitizeDevServerConfigForRecording(devServerConfig)
40+
} else {
41+
resultConfig.devServerConfig = `omitted: ${typeof devServerConfig}`
42+
}
1543
}
1644

1745
if (devServer) {
@@ -25,5 +53,5 @@ export function filterRuntimeConfigForRecording (config) {
2553
}
2654
}
2755

28-
return resultConfig
56+
return _.pick(resultConfig, getCloudRecordingConfigKeys())
2957
}

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

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1007,6 +1007,8 @@ describe('lib/cloud/api', () => {
10071007
it('POSTs /instances/:id/tests strips arbitrarily large config values', function () {
10081008
this.props.config = {
10091009
projectId: 'abcd1234',
1010+
customBloat: { nested: 'x'.repeat(5000) },
1011+
_myPluginState: { foo: 'bar' },
10101012
devServer: {
10111013
bundler: 'webpack',
10121014
framework: 'react',
@@ -1053,6 +1055,46 @@ describe('lib/cloud/api', () => {
10531055
expect(expectedConfig.resolved).to.be.undefined
10541056
expect(expectedConfig.devServer.webpackConfig).to.equal('omitted')
10551057
expect(expectedConfig.devServer.viteConfig).to.equal('omitted')
1058+
expect(expectedConfig.customBloat).to.be.undefined
1059+
expect(expectedConfig._myPluginState).to.be.undefined
1060+
1061+
return api.postInstanceTests(this.props)
1062+
})
1063+
1064+
it('POSTs /instances/:id/tests keeps allowlisted component config keys', function () {
1065+
this.props.config = {
1066+
projectId: 'abcd1234',
1067+
indexHtmlFile: 'cypress/support/component-index.html',
1068+
devServerConfig: {
1069+
framework: 'react',
1070+
bundler: 'webpack',
1071+
mode: 'development',
1072+
webpackConfig: { entry: 'app' },
1073+
},
1074+
}
1075+
1076+
const expectedConfig = filterRuntimeConfigForRecording(this.props.config)
1077+
1078+
expect(expectedConfig.projectId).to.eq('abcd1234')
1079+
expect(expectedConfig.indexHtmlFile).to.eq('cypress/support/component-index.html')
1080+
expect(expectedConfig.devServerConfig).to.eql({
1081+
framework: 'react',
1082+
bundler: 'webpack',
1083+
mode: 'omitted: string',
1084+
webpackConfig: 'omitted: object',
1085+
})
1086+
1087+
nock(API_BASEURL)
1088+
.matchHeader('x-route-version', '1')
1089+
.matchHeader('x-cypress-run-id', this.props.runId)
1090+
.matchHeader('x-cypress-request-attempt', '0')
1091+
.matchHeader('x-os-name', OS_PLATFORM)
1092+
.matchHeader('x-cypress-version', pkg.version)
1093+
.post('/instances/instance-id-123/tests', {
1094+
...this.bodyProps,
1095+
config: expectedConfig,
1096+
})
1097+
.reply(200)
10561098

10571099
return api.postInstanceTests(this.props)
10581100
})
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
require('../spec_helper')
2+
3+
const _ = require('lodash')
4+
const { filterRuntimeConfigForRecording } = require('../../lib/config')
5+
const { getCloudRecordingConfigKeys } = require('@packages/config')
6+
7+
describe('lib/config filterRuntimeConfigForRecording', () => {
8+
it('returns an empty object for an empty config', () => {
9+
expect(filterRuntimeConfigForRecording({})).to.eql({})
10+
})
11+
12+
it('removes rawJson, resolved, and keys not in the cloud recording allowlist', () => {
13+
const filtered = filterRuntimeConfigForRecording({
14+
projectId: 'abc',
15+
baseUrl: 'http://localhost',
16+
rawJson: { huge: 'x'.repeat(1000) },
17+
resolved: { baseUrl: { value: 'x', from: 'config' } },
18+
socketId: 'sock-1',
19+
clientRoute: '/__/',
20+
arbitraryUserKey: { nested: true },
21+
})
22+
23+
expect(filtered.rawJson).to.be.undefined
24+
expect(filtered.resolved).to.be.undefined
25+
expect(filtered.socketId).to.be.undefined
26+
expect(filtered.clientRoute).to.be.undefined
27+
expect(filtered.arbitraryUserKey).to.be.undefined
28+
expect(filtered.projectId).to.eq('abc')
29+
expect(filtered.baseUrl).to.eq('http://localhost')
30+
})
31+
32+
it('replaces env values with type placeholders', () => {
33+
const filtered = filterRuntimeConfigForRecording({
34+
env: {
35+
STR: 'secret',
36+
NUM: 42,
37+
BOOL: false,
38+
OBJ: { a: 1 },
39+
},
40+
})
41+
42+
expect(filtered.env).to.eql({
43+
STR: 'omitted: string',
44+
NUM: 'omitted: number',
45+
BOOL: 'omitted: boolean',
46+
OBJ: 'omitted: object',
47+
})
48+
})
49+
50+
it('replaces expose values with type placeholders like env', () => {
51+
const filtered = filterRuntimeConfigForRecording({
52+
expose: {
53+
API_URL: 'https://secret.example',
54+
FLAG: true,
55+
},
56+
})
57+
58+
expect(filtered.expose).to.eql({
59+
API_URL: 'omitted: string',
60+
FLAG: 'omitted: boolean',
61+
})
62+
})
63+
64+
it('omits devServer webpackConfig and viteConfig but keeps other devServer fields', () => {
65+
const filtered = filterRuntimeConfigForRecording({
66+
devServer: {
67+
bundler: 'webpack',
68+
framework: 'react',
69+
webpackConfig: { entry: 'app' },
70+
viteConfig: { root: '/tmp' },
71+
},
72+
})
73+
74+
expect(filtered.devServer).to.eql({
75+
bundler: 'webpack',
76+
framework: 'react',
77+
webpackConfig: 'omitted',
78+
viteConfig: 'omitted',
79+
})
80+
})
81+
82+
it('preserves bundler and framework on devServerConfig and redacts other fields', () => {
83+
const filtered = filterRuntimeConfigForRecording({
84+
devServerConfig: {
85+
bundler: 'vite',
86+
framework: 'vue',
87+
viteConfig: { build: { target: 'esnext' } },
88+
mode: 'development',
89+
},
90+
})
91+
92+
expect(filtered.devServerConfig).to.eql({
93+
bundler: 'vite',
94+
framework: 'vue',
95+
viteConfig: 'omitted: object',
96+
mode: 'omitted: string',
97+
})
98+
})
99+
100+
it('redacts non-object devServerConfig with a type placeholder string', () => {
101+
expect(filterRuntimeConfigForRecording({ devServerConfig: null }).devServerConfig)
102+
.to.eq('omitted: object')
103+
104+
expect(filterRuntimeConfigForRecording({ devServerConfig: 'oops' }).devServerConfig)
105+
.to.eq('omitted: string')
106+
107+
expect(filterRuntimeConfigForRecording({ devServerConfig: [] }).devServerConfig)
108+
.to.eq('omitted: object')
109+
})
110+
111+
it('does not set devServerConfig when undefined', () => {
112+
const filtered = filterRuntimeConfigForRecording({ projectId: 'x' })
113+
114+
expect(filtered).not.to.have.property('devServerConfig')
115+
})
116+
117+
it('keeps indexHtmlFile and allowlisted public keys only', () => {
118+
const filtered = filterRuntimeConfigForRecording({
119+
indexHtmlFile: 'cypress/support/component-index.html',
120+
specPattern: '**/*.cy.ts',
121+
video: true,
122+
notARealOption: 'drop-me',
123+
})
124+
125+
expect(filtered.indexHtmlFile).to.eq('cypress/support/component-index.html')
126+
expect(filtered.specPattern).to.eq('**/*.cy.ts')
127+
expect(filtered.video).to.eq(true)
128+
expect(filtered.notARealOption).to.be.undefined
129+
})
130+
131+
it('output keys are a subset of getCloudRecordingConfigKeys()', () => {
132+
const allow = new Set(getCloudRecordingConfigKeys())
133+
const filtered = filterRuntimeConfigForRecording({
134+
projectId: 'p',
135+
devServer: { bundler: 'webpack', framework: 'react' },
136+
devServerConfig: { bundler: 'webpack', framework: 'react' },
137+
env: { K: 1 },
138+
expose: { X: 'y' },
139+
indexHtmlFile: 'index.html',
140+
extra: 'should-not-appear',
141+
})
142+
143+
_.each(_.keys(filtered), (key) => {
144+
expect(allow.has(key), `unexpected key on filtered config: ${key}`).to.equal(true)
145+
})
146+
})
147+
})

tooling/v8-snapshot/cache/darwin/snapshot-meta.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3972,5 +3972,5 @@
39723972
"./tooling/v8-snapshot/cache/darwin/snapshot-entry.js"
39733973
],
39743974
"deferredHashFile": "yarn.lock",
3975-
"deferredHash": "167855a3a47bb6b78d1809230b134a06937e08056d60d6a2bad5bebd148984f6"
3975+
"deferredHash": "b7f83437955752f0fb659b48d056320865210cc3da63c8ea88571be6b2d14a10"
39763976
}

0 commit comments

Comments
 (0)