diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index 60efc3ade78..c0829a33e15 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -12,6 +12,8 @@ _Released 11/18/2025 (PENDING)_ - Fixed an issue where [`cy.wrap()`](https://docs.cypress.io/api/commands/wrap) would cause infinite recursion and freeze the Cypress App when called with objects containing circular references. Fixes [#24715](https://github.com/cypress-io/cypress/issues/24715). Addressed in [#32917](https://github.com/cypress-io/cypress/pull/32917). - Fixed an issue where top changes on test retries could cause attempt numbers to show up more than one time in the reporter and cause attempts to be lost in Test Replay. Addressed in [#32888](https://github.com/cypress-io/cypress/pull/32888). - Fixed an issue where stack traces that are used to determine a test's invocation details are sometimes incorrect. Addressed in [#32699](https://github.com/cypress-io/cypress/pull/32699) +- Fixed an issue where larger than expected config values were causing issues in certain cases when recording to the Cypress Cloud. Addressed in [#32957](https://github.com/cypress-io/cypress/pull/32957) + **Misc:** diff --git a/packages/server/lib/cloud/api/index.ts b/packages/server/lib/cloud/api/index.ts index 41389431c99..90c0c2b246b 100644 --- a/packages/server/lib/cloud/api/index.ts +++ b/packages/server/lib/cloud/api/index.ts @@ -37,6 +37,7 @@ import type { CreateInstanceRequestBody, CreateInstanceResponse } from './create import { transformError } from './axios_middleware/transform_error' import { DecryptionError } from './cloud_request_errors' import { isNonRetriableCertErrorCode } from '../network/non_retriable_cert_error_codes' +import { filterRuntimeConfigForRecording } from '../../config' const debug = debugModule('cypress:server:cloud:api') const debugProtocol = debugModule('cypress:server:protocol') @@ -506,7 +507,7 @@ export default { }, postInstanceTests (options) { - const { instanceId, runId, timeout, ...body } = options + const { instanceId, runId, timeout, config, ...body } = options return retryWithBackoff((attemptIndex) => { return rp.post({ @@ -519,7 +520,10 @@ export default { 'x-cypress-run-id': runId, 'x-cypress-request-attempt': attemptIndex, }, - body, + body: { + ...body, + config: filterRuntimeConfigForRecording(config ?? {}), + }, }) .catch(RequestErrors.StatusCodeError, transformError) .catch(tagError) diff --git a/packages/server/lib/config.ts b/packages/server/lib/config.ts index 5d22ab1d689..d4e3bd1c3c0 100644 --- a/packages/server/lib/config.ts +++ b/packages/server/lib/config.ts @@ -1,15 +1,29 @@ import _ from 'lodash' -import type { ResolvedFromConfig } from '@packages/types' import * as configUtils from '@packages/config' export const setUrls = configUtils.setUrls -export function getResolvedRuntimeConfig (config, runtimeConfig) { - const resolvedRuntimeFields = _.mapValues(runtimeConfig, (v): ResolvedFromConfig => ({ value: v, from: 'runtime' })) +// Strips out values that can be aribitrarily sized / are duplicated from config +// payload sent for recording +export function filterRuntimeConfigForRecording (config) { + const { rawJson, devServer, env, resolved, ...configRest } = config + const { webpackConfig, viteConfig, ...devServerRest } = devServer ?? {} + const resultConfig = { ...configRest } - return { - ...config, - ...runtimeConfig, - resolved: { ...config.resolved, ...resolvedRuntimeFields }, + if (env) { + resultConfig.env = _.mapValues(env ?? {}, (val, key) => `omitted: ${typeof val}`) } + + if (devServer) { + resultConfig.devServer = { ...devServerRest } + if (typeof webpackConfig !== 'undefined') { + resultConfig.devServer.webpackConfig = `omitted` + } + + if (typeof viteConfig !== 'undefined') { + resultConfig.devServer.viteConfig = `omitted` + } + } + + return resultConfig } diff --git a/packages/server/lib/modes/record.ts b/packages/server/lib/modes/record.ts index f7cbd3bed4e..7fa24070c07 100644 --- a/packages/server/lib/modes/record.ts +++ b/packages/server/lib/modes/record.ts @@ -15,7 +15,6 @@ import { getError } from '@packages/errors' import type { AllCypressErrorNames } from '@packages/errors' import { get as getErrors, warning as errorsWarning, throwErr } from '../errors' import * as capture from '../capture' -import { getResolvedRuntimeConfig } from '../config' import * as env from '../util/env' import ciProvider from '../util/ci_provider' import { flattenSuiteIntoRunnables } from '../util/tests_utils' @@ -754,7 +753,7 @@ const createRunAndRecordSpecs = (options: any = {}) => { const r = flattenSuiteIntoRunnables(runnables) const runtimeConfig = runnables.runtimeConfig - const resolvedRuntimeConfig = getResolvedRuntimeConfig(config, runtimeConfig) + const resolvedRuntimeConfig = { ...config, ...runtimeConfig } const tests = _.chain(r[0]) .uniqBy('id') diff --git a/packages/server/test/unit/cloud/api/api_spec.js b/packages/server/test/unit/cloud/api/api_spec.js index 83691b3df1a..bdbf7593ca5 100644 --- a/packages/server/test/unit/cloud/api/api_spec.js +++ b/packages/server/test/unit/cloud/api/api_spec.js @@ -8,6 +8,7 @@ require('../../../spec_helper') const _ = require('lodash') const os = require('os') const encryption = require('../../../../lib/cloud/encryption') +const { filterRuntimeConfigForRecording } = require('../../../../lib/config') const { agent, @@ -987,7 +988,7 @@ describe('lib/cloud/api', () => { this.bodyProps = _.omit(this.props, 'instanceId', 'runId') }) - it('POSTs /instances/:id/results', function () { + it('POSTs /instances/:id/tests', function () { nock(API_BASEURL) .matchHeader('x-route-version', '1') .matchHeader('x-cypress-run-id', this.props.runId) @@ -1000,6 +1001,59 @@ describe('lib/cloud/api', () => { return api.postInstanceTests(this.props) }) + it('POSTs /instances/:id/tests strips arbitrarily large config values', function () { + this.props.config = { + projectId: 'abcd1234', + devServer: { + bundler: 'webpack', + framework: 'react', + webpackConfig: 'a'.repeat(10000), + viteConfig: 'a'.repeat(10000), + }, + env: { + NUMERIC_VALUE: 1, + TRUTHY_VALUE: true, + SOME_REALLY_LONG_VALUE: 'a'.repeat(10000), + }, + resolved: { + env: { + 'NUMERIC_VALUE': { 'value': 1, 'from': 'env' }, + 'TRUTHY_VALUE': { 'value': true, 'from': 'env' }, + 'SOME_REALLY_LONG_VALUE': { 'value': 'a'.repeat(10000), 'from': 'env' }, + }, + }, + } + + this.props.config.rawJson = _.cloneDeep(this.props.config) + + const expectedConfig = filterRuntimeConfigForRecording(this.props.config) + + nock(API_BASEURL) + .matchHeader('x-route-version', '1') + .matchHeader('x-cypress-run-id', this.props.runId) + .matchHeader('x-cypress-request-attempt', '0') + .matchHeader('x-os-name', OS_PLATFORM) + .matchHeader('x-cypress-version', pkg.version) + .post('/instances/instance-id-123/tests', { + ...this.bodyProps, + config: expectedConfig, + }) + .reply(200) + + expect(expectedConfig.projectId).to.eq('abcd1234') + expect(expectedConfig.env).to.eql({ + NUMERIC_VALUE: `omitted: number`, + TRUTHY_VALUE: `omitted: boolean`, + SOME_REALLY_LONG_VALUE: `omitted: string`, + }) + + expect(expectedConfig.resolved).to.be.undefined + expect(expectedConfig.devServer.webpackConfig).to.equal('omitted') + expect(expectedConfig.devServer.viteConfig).to.equal('omitted') + + return api.postInstanceTests(this.props) + }) + it('PUT /instances/:id failure formatting', () => { nock(API_BASEURL) .matchHeader('x-route-version', '1') diff --git a/system-tests/test/record_spec.js b/system-tests/test/record_spec.js index 0df5f6828e1..8294f0cd7ea 100644 --- a/system-tests/test/record_spec.js +++ b/system-tests/test/record_spec.js @@ -602,16 +602,7 @@ describe('e2e record', () => { const requests = getRequests() expect(requests[2].body.config.defaultCommandTimeout).eq(1111) - expect(requests[2].body.config.resolved.defaultCommandTimeout).deep.eq({ - value: 1111, - from: 'runtime', - }) - expect(requests[2].body.config.pageLoadTimeout).eq(3333) - expect(requests[2].body.config.resolved.pageLoadTimeout).deep.eq({ - value: 3333, - from: 'runtime', - }) expect(requests[2].body.tests[0].config).deep.eq({ defaultCommandTimeout: 1234,