From 2a17efac74111b0a723af0e5c186e73d18c688bd Mon Sep 17 00:00:00 2001 From: Brian Mann Date: Fri, 24 Feb 2023 17:32:36 -0500 Subject: [PATCH] feat: improve stability when recording (#25837) Co-authored-by: Ryan Manuel Co-authored-by: Tim Griesser --- .circleci/workflows.yml | 61 +- cli/CHANGELOG.md | 2 + guides/error-handling.md | 2 +- .../CANNOT_REMOVE_OLD_BROWSER_PROFILES.html | 2 +- .../CANNOT_TRASH_ASSETS.html | 2 +- ...PI_RESPONSE_FAILED_RETRYING - lastTry.html | 9 +- .../CLOUD_API_RESPONSE_FAILED_RETRYING.html | 9 +- .../CLOUD_CANNOT_CREATE_RUN_OR_INSTANCE.html | 6 +- .../CLOUD_CANNOT_PROCEED_IN_PARALLEL.html | 10 +- .../CLOUD_CANNOT_PROCEED_IN_SERIAL.html | 10 +- .../CLOUD_CANNOT_UPLOAD_RESULTS.html | 2 +- .../CLOUD_INVALID_RUN_REQUEST.html | 2 +- .../CLOUD_PROJECT_NOT_FOUND.html | 4 +- .../CLOUD_UNKNOWN_INVALID_REQUEST.html | 10 +- .../DEV_SERVER_CONFIG_FILE_NOT_FOUND.html | 2 +- .../EXPERIMENTAL_STUDIO_REMOVED.html | 2 +- ...MIGRATION_MISMATCHED_CYPRESS_VERSIONS.html | 2 +- .../__snapshot-html__/NOT_LOGGED_IN.html | 2 +- .../RECORDING_FROM_FORK_PR.html | 2 +- .../UNEXPECTED_INTERNAL_ERROR.html | 7 +- .../VIDEO_POST_PROCESSING_FAILED.html | 2 +- .../VIDEO_RECORDING_FAILED.html | 2 +- packages/errors/src/errors.ts | 112 ++-- .../test/unit/visualSnapshotErrors_spec.ts | 4 +- packages/server/lib/cloud/api.ts | 147 +++-- packages/server/lib/cloud/environment.ts | 152 +++++ packages/server/lib/modes/record.js | 9 +- packages/server/package.json | 1 + .../server/test/integration/cypress_spec.js | 2 +- .../fixtures/cloud/environment/.gitignore | 1 + .../node_modules/bar/package.json | 6 + .../node_modules/bar/src/index.js | 0 .../node_modules/foo/index.js | 0 .../node_modules/foo/package.json | 6 + .../all-tracked-dependencies/package.json | 8 + .../node_modules/foo/index.js | 0 .../node_modules/foo/package.json | 6 + .../package.json | 7 + .../node_modules/bar/package.json | 6 + .../node_modules/bar/src/index.js | 0 .../package.json | 7 + .../cloud/environment/test-project/child.js | 26 + .../environment/test-project/grandchild.js | 10 + .../cloud/environment/test-project/index.js | 26 + .../environment/test-project/package.json | 5 + packages/server/test/unit/cloud/api_spec.js | 413 +++++++++++++- .../test/unit/cloud/environment_spec.ts | 182 ++++++ .../server/test/unit/modes/record_spec.js | 3 +- scripts/after-pack-hook.js | 26 +- scripts/binary/binary-sources.js | 48 +- system-tests/__snapshots__/record_spec.js | 282 ++++++---- .../__snapshots__/web_security_spec.js | 2 +- system-tests/lib/serverStub.ts | 2 +- system-tests/lib/system-tests.ts | 2 +- system-tests/test/record_spec.js | 527 ++++++++++++------ yarn.lock | 7 + 56 files changed, 1723 insertions(+), 464 deletions(-) create mode 100644 packages/server/lib/cloud/environment.ts create mode 100644 packages/server/test/support/fixtures/cloud/environment/.gitignore create mode 100644 packages/server/test/support/fixtures/cloud/environment/all-tracked-dependencies/node_modules/bar/package.json create mode 100644 packages/server/test/support/fixtures/cloud/environment/all-tracked-dependencies/node_modules/bar/src/index.js create mode 100644 packages/server/test/support/fixtures/cloud/environment/all-tracked-dependencies/node_modules/foo/index.js create mode 100644 packages/server/test/support/fixtures/cloud/environment/all-tracked-dependencies/node_modules/foo/package.json create mode 100644 packages/server/test/support/fixtures/cloud/environment/all-tracked-dependencies/package.json create mode 100644 packages/server/test/support/fixtures/cloud/environment/partial-dependencies-matching/node_modules/foo/index.js create mode 100644 packages/server/test/support/fixtures/cloud/environment/partial-dependencies-matching/node_modules/foo/package.json create mode 100644 packages/server/test/support/fixtures/cloud/environment/partial-dependencies-matching/package.json create mode 100644 packages/server/test/support/fixtures/cloud/environment/partial-dependencies-not-matching/node_modules/bar/package.json create mode 100644 packages/server/test/support/fixtures/cloud/environment/partial-dependencies-not-matching/node_modules/bar/src/index.js create mode 100644 packages/server/test/support/fixtures/cloud/environment/partial-dependencies-not-matching/package.json create mode 100644 packages/server/test/support/fixtures/cloud/environment/test-project/child.js create mode 100644 packages/server/test/support/fixtures/cloud/environment/test-project/grandchild.js create mode 100644 packages/server/test/support/fixtures/cloud/environment/test-project/index.js create mode 100644 packages/server/test/support/fixtures/cloud/environment/test-project/package.json create mode 100644 packages/server/test/unit/cloud/environment_spec.ts diff --git a/.circleci/workflows.yml b/.circleci/workflows.yml index 778537e0bfcb..fc6e335d4d8c 100644 --- a/.circleci/workflows.yml +++ b/.circleci/workflows.yml @@ -30,7 +30,7 @@ mainBuildFilters: &mainBuildFilters - /^release\/\d+\.\d+\.\d+$/ # use the following branch as well to ensure that v8 snapshot cache updates are fully tested - 'update-v8-snapshot-cache-on-develop' - - 'lmiller/fixing-vite-windows' + - 'fix/preflight' # usually we don't build Mac app - it takes a long time # but sometimes we want to really confirm we are doing the right thing @@ -41,6 +41,7 @@ macWorkflowFilters: &darwin-workflow-filters - equal: [ develop, << pipeline.git.branch >> ] # use the following branch as well to ensure that v8 snapshot cache updates are fully tested - equal: [ 'update-v8-snapshot-cache-on-develop', << pipeline.git.branch >> ] + - equal: [ 'fix/preflight', << pipeline.git.branch >> ] - matches: pattern: /^release\/\d+\.\d+\.\d+$/ value: << pipeline.git.branch >> @@ -51,6 +52,7 @@ linuxArm64WorkflowFilters: &linux-arm64-workflow-filters - equal: [ develop, << pipeline.git.branch >> ] # use the following branch as well to ensure that v8 snapshot cache updates are fully tested - equal: [ 'update-v8-snapshot-cache-on-develop', << pipeline.git.branch >> ] + - equal: [ 'fix/preflight', << pipeline.git.branch >> ] - matches: pattern: /^release\/\d+\.\d+\.\d+$/ value: << pipeline.git.branch >> @@ -71,6 +73,7 @@ windowsWorkflowFilters: &windows-workflow-filters # use the following branch as well to ensure that v8 snapshot cache updates are fully tested - equal: [ 'lmiller/fixing-vite-windows', << pipeline.git.branch >> ] - equal: [ 'update-v8-snapshot-cache-on-develop', << pipeline.git.branch >> ] + - equal: [ 'fix/preflight', << pipeline.git.branch >> ] - matches: pattern: /^release\/\d+\.\d+\.\d+$/ value: << pipeline.git.branch >> @@ -136,7 +139,7 @@ commands: - run: name: Check current branch to persist artifacts command: | - if [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "release/"* && "$CIRCLE_BRANCH" != "lmiller/fixing-vite-windows" ]]; then + if [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "release/"* && "$CIRCLE_BRANCH" != "fix/preflight" && "$CIRCLE_BRANCH" != "update-v8-snapshot-cache-on-develop" ]]; then echo "Not uploading artifacts or posting install comment for this branch." circleci-agent step halt fi @@ -1485,6 +1488,30 @@ jobs: path: /tmp/cypress - store-npm-logs + server-unit-tests-cloud-environment: + <<: *defaults + parameters: + <<: *defaultsParameters + resource_class: + type: string + default: medium + resource_class: << parameters.resource_class >> + parallelism: 1 + steps: + - restore_cached_workspace + # TODO: Remove this once we switch off self-hosted M1 runners + - when: + condition: + equal: [ *darwin-arm64-executor, << parameters.executor >> ] + steps: + - run: rm -f /tmp/cypress/junit/* + - run: yarn workspace @packages/server test-unit cloud/environment_spec.ts + - verify-mocha-results: + expectedResultCount: 1 + - store_test_results: + path: /tmp/cypress + - store-npm-logs + server-integration-tests: <<: *defaults parallelism: 1 @@ -2577,6 +2604,7 @@ linux-x64-workflow: &linux-x64-workflow context: - test-runner:upload - test-runner:commit-status-checks + - test-runner:build-binary requires: - build # various testing scenarios, like building full binary @@ -2677,6 +2705,7 @@ linux-arm64-workflow: &linux-arm64-workflow context: - test-runner:upload - test-runner:commit-status-checks + - test-runner:build-binary executor: linux-arm64 resource_class: arm.medium requires: @@ -2694,6 +2723,12 @@ linux-arm64-workflow: &linux-arm64-workflow resource_class: arm.medium requires: - linux-arm64-build + - server-unit-tests-cloud-environment: + name: linux-arm64-server-unit-tests-cloud-environment + executor: linux-arm64 + resource_class: arm.medium + requires: + - linux-arm64-build darwin-x64-workflow: &darwin-x64-workflow jobs: @@ -2717,6 +2752,7 @@ darwin-x64-workflow: &darwin-x64-workflow - test-runner:sign-mac-binary - test-runner:upload - test-runner:commit-status-checks + - test-runner:build-binary executor: mac resource_class: macos.x86.medium.gen2 requires: @@ -2740,6 +2776,12 @@ darwin-x64-workflow: &darwin-x64-workflow resource_class: macos.x86.medium.gen2 requires: - darwin-x64-build + - server-unit-tests-cloud-environment: + name: darwin-x64-driver-server-unit-tests-cloud-environment + executor: mac + resource_class: macos.x86.medium.gen2 + requires: + - darwin-x64-build darwin-arm64-workflow: &darwin-arm64-workflow jobs: @@ -2762,6 +2804,7 @@ darwin-arm64-workflow: &darwin-arm64-workflow - test-runner:sign-mac-binary - test-runner:upload - test-runner:commit-status-checks + - test-runner:build-binary executor: darwin-arm64 resource_class: cypress-io/latest_m1 requires: @@ -2779,6 +2822,12 @@ darwin-arm64-workflow: &darwin-arm64-workflow resource_class: cypress-io/latest_m1 requires: - darwin-arm64-build + - server-unit-tests-cloud-environment: + name: darwin-arm64-server-unit-tests-cloud-environment + executor: darwin-arm64 + resource_class: cypress-io/latest_m1 + requires: + - darwin-arm64-build windows-workflow: &windows-workflow jobs: @@ -2819,6 +2868,13 @@ windows-workflow: &windows-workflow requires: - windows-build + - server-unit-tests-cloud-environment: + name: windows-server-unit-tests-cloud-environment + executor: windows + resource_class: windows.medium + requires: + - windows-build + - create-build-artifacts: name: windows-create-build-artifacts executor: windows @@ -2827,6 +2883,7 @@ windows-workflow: &windows-workflow - test-runner:sign-windows-binary - test-runner:upload - test-runner:commit-status-checks + - test-runner:build-binary requires: - windows-build - test-binary-against-kitchensink-chrome: diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index b10418d3b102..e35628a66291 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -8,10 +8,12 @@ _Released 02/28/2023 (PENDING)_ - It is now possible to set `hostOnly` cookies with [`cy.setCookie()`](https://docs.cypress.io/api/commands/setcookie) for a given domain. Addresses [#16856](https://github.com/cypress-io/cypress/issues/16856) and [#17527](https://github.com/cypress-io/cypress/issues/17527). - Added a Public API for third party component libraries to define a Framework Definition, embedding their library into the Cypress onboarding workflow. Learn more [here](https://docs.cypress.io/guides/component-testing/third-party-definitions). Implemented in [#25780](https://github.com/cypress-io/cypress/pull/25780) and closes [#25638](https://github.com/cypress-io/cypress/issues/25638). - Added a Debug Page tutorial slideshow for projects that are not connected to Cypress Cloud. Addresses [#25768](https://github.com/cypress-io/cypress/issues/25768). +- Improved various error message around interactions with the Cypress cloud. Implemented in [#25837](https://github.com/cypress-io/cypress/pull/25837) - Updated the "new" status badge for the Debug page navigation link to be less noticeable when the navigation is collapsed. Addresses [#25739](https://github.com/cypress-io/cypress/issues/25739). **Bugfixes:** +- Fixed various bugs when recording to the cloud. Fixed in [#25837](https://github.com/cypress-io/cypress/pull/25837) - Fixed an issue where cookies were being duplicated with the same hostname, but a prepended dot. Fixed an issue where cookies may not be expiring correctly. Fixes [#25174](https://github.com/cypress-io/cypress/issues/25174), [#25205](https://github.com/cypress-io/cypress/issues/25205) and [#25495](https://github.com/cypress-io/cypress/issues/25495). - Fixed an issue where cookies weren't being synced when the application was stable. Fixed in [#25855](https://github.com/cypress-io/cypress/pull/25855). Fixes [#25835](https://github.com/cypress-io/cypress/issues/25835). - Added missing TypeScript type definitions for the [`cy.reload()`](https://docs.cypress.io/api/commands/reload) command. Addressed in [#25779](https://github.com/cypress-io/cypress/pull/25779). diff --git a/guides/error-handling.md b/guides/error-handling.md index 3035e33c3259..4ae4daf3d021 100644 --- a/guides/error-handling.md +++ b/guides/error-handling.md @@ -68,7 +68,7 @@ CANNOT_TRASH_ASSETS: (arg1: string) => { return errTemplate`\ Warning: We failed to trash the existing run results. - This error will not alter the exit code. + This error will not affect or change the exit code. ${details(arg1)}` }, diff --git a/packages/errors/__snapshot-html__/CANNOT_REMOVE_OLD_BROWSER_PROFILES.html b/packages/errors/__snapshot-html__/CANNOT_REMOVE_OLD_BROWSER_PROFILES.html index 2763657e162e..8beb31f7cf4f 100644 --- a/packages/errors/__snapshot-html__/CANNOT_REMOVE_OLD_BROWSER_PROFILES.html +++ b/packages/errors/__snapshot-html__/CANNOT_REMOVE_OLD_BROWSER_PROFILES.html @@ -36,7 +36,7 @@
Warning: We failed to remove old browser profiles from previous runs.
 
-This error will not alter the exit code.
+This error will not affect or change the exit code.
 
 Error: fail whale
     at makeErr (cypress/packages/errors/test/unit/visualSnapshotErrors_spec.ts)
diff --git a/packages/errors/__snapshot-html__/CANNOT_TRASH_ASSETS.html b/packages/errors/__snapshot-html__/CANNOT_TRASH_ASSETS.html
index 3ab26c3ef702..6f2d58c3049e 100644
--- a/packages/errors/__snapshot-html__/CANNOT_TRASH_ASSETS.html
+++ b/packages/errors/__snapshot-html__/CANNOT_TRASH_ASSETS.html
@@ -36,7 +36,7 @@
     
     
Warning: We failed to trash the existing run results.
 
-This error will not alter the exit code.
+This error will not affect or change the exit code.
 
 Error: fail whale
     at makeErr (cypress/packages/errors/test/unit/visualSnapshotErrors_spec.ts)
diff --git a/packages/errors/__snapshot-html__/CLOUD_API_RESPONSE_FAILED_RETRYING - lastTry.html b/packages/errors/__snapshot-html__/CLOUD_API_RESPONSE_FAILED_RETRYING - lastTry.html
index 3851fc91ffdb..c93032f73e0a 100644
--- a/packages/errors/__snapshot-html__/CLOUD_API_RESPONSE_FAILED_RETRYING - lastTry.html	
+++ b/packages/errors/__snapshot-html__/CLOUD_API_RESPONSE_FAILED_RETRYING - lastTry.html	
@@ -34,11 +34,10 @@
     
   
     
-    
We encountered an unexpected error talking to our servers.
+    
We encountered an unexpected error communicating with our servers.
 
-We will retry 1 more time in 5 seconds...
-
-The server's response was:
+StatusCodeError: 500 - "Internal Server Error"
 
-StatusCodeError: 500 - "Internal Server Error"
+We will retry 1 more time in 5 seconds...
+
 
\ No newline at end of file diff --git a/packages/errors/__snapshot-html__/CLOUD_API_RESPONSE_FAILED_RETRYING.html b/packages/errors/__snapshot-html__/CLOUD_API_RESPONSE_FAILED_RETRYING.html index 8d56bdf9d80d..a8acc2a35f07 100644 --- a/packages/errors/__snapshot-html__/CLOUD_API_RESPONSE_FAILED_RETRYING.html +++ b/packages/errors/__snapshot-html__/CLOUD_API_RESPONSE_FAILED_RETRYING.html @@ -34,11 +34,10 @@ -
We encountered an unexpected error talking to our servers.
+    
We encountered an unexpected error communicating with our servers.
 
-We will retry 3 more times in 5 seconds...
-
-The server's response was:
+StatusCodeError: 500 - "Internal Server Error"
 
-StatusCodeError: 500 - "Internal Server Error"
+We will retry 3 more times in 5 seconds...
+
 
\ No newline at end of file diff --git a/packages/errors/__snapshot-html__/CLOUD_CANNOT_CREATE_RUN_OR_INSTANCE.html b/packages/errors/__snapshot-html__/CLOUD_CANNOT_CREATE_RUN_OR_INSTANCE.html index 9f34f7c4ad88..e5cabd1708a6 100644 --- a/packages/errors/__snapshot-html__/CLOUD_CANNOT_CREATE_RUN_OR_INSTANCE.html +++ b/packages/errors/__snapshot-html__/CLOUD_CANNOT_CREATE_RUN_OR_INSTANCE.html @@ -34,11 +34,11 @@ -
Warning: We encountered an error talking to our servers.
+    
Warning: We encountered an error communicating with our servers.
 
-This run will not be recorded.
+This run will proceed, but will not be recorded.
 
-This error will not alter the exit code.
+This error will not affect or change the exit code.
 
 StatusCodeError: 500 - "Internal Server Error"
 
\ No newline at end of file diff --git a/packages/errors/__snapshot-html__/CLOUD_CANNOT_PROCEED_IN_PARALLEL.html b/packages/errors/__snapshot-html__/CLOUD_CANNOT_PROCEED_IN_PARALLEL.html index 61a5436cc1c3..4e5ee6d22d0d 100644 --- a/packages/errors/__snapshot-html__/CLOUD_CANNOT_PROCEED_IN_PARALLEL.html +++ b/packages/errors/__snapshot-html__/CLOUD_CANNOT_PROCEED_IN_PARALLEL.html @@ -34,14 +34,12 @@ -
We encountered an unexpected error talking to our servers.
+    
We encountered an unexpected error communicating with our servers.
+
+StatusCodeError: 500 - "Internal Server Error"
 
 Because you passed the --parallel flag, this run cannot proceed because it requires a valid response from our servers.
 
 The --group flag you passed was: foo
-The --ciBuildId flag you passed was: invalid
-
-The server's response was:
-
-StatusCodeError: 500 - "Internal Server Error"
+The --ciBuildId flag you passed was: invalid
 
\ No newline at end of file diff --git a/packages/errors/__snapshot-html__/CLOUD_CANNOT_PROCEED_IN_SERIAL.html b/packages/errors/__snapshot-html__/CLOUD_CANNOT_PROCEED_IN_SERIAL.html index ad85fcf206d3..a2843bf81720 100644 --- a/packages/errors/__snapshot-html__/CLOUD_CANNOT_PROCEED_IN_SERIAL.html +++ b/packages/errors/__snapshot-html__/CLOUD_CANNOT_PROCEED_IN_SERIAL.html @@ -34,12 +34,10 @@ -
We encountered an unexpected error talking to our servers.
+    
We encountered an unexpected error communicating with our servers.
 
-The --group flag you passed was: foo
-The --ciBuildId flag you passed was: invalid
-
-The server's response was:
+StatusCodeError: 500 - "Internal Server Error"
 
-StatusCodeError: 500 - "Internal Server Error"
+The --group flag you passed was: foo
+The --ciBuildId flag you passed was: invalid
 
\ No newline at end of file diff --git a/packages/errors/__snapshot-html__/CLOUD_CANNOT_UPLOAD_RESULTS.html b/packages/errors/__snapshot-html__/CLOUD_CANNOT_UPLOAD_RESULTS.html index d179c0ff16ea..3068e21d2321 100644 --- a/packages/errors/__snapshot-html__/CLOUD_CANNOT_UPLOAD_RESULTS.html +++ b/packages/errors/__snapshot-html__/CLOUD_CANNOT_UPLOAD_RESULTS.html @@ -38,7 +38,7 @@ These results will not be recorded. -This error will not alter the exit code. +This error will not affect or change the exit code. StatusCodeError: 500 - "Internal Server Error"
\ No newline at end of file diff --git a/packages/errors/__snapshot-html__/CLOUD_INVALID_RUN_REQUEST.html b/packages/errors/__snapshot-html__/CLOUD_INVALID_RUN_REQUEST.html index d231b57b949f..59221ce2fdb9 100644 --- a/packages/errors/__snapshot-html__/CLOUD_INVALID_RUN_REQUEST.html +++ b/packages/errors/__snapshot-html__/CLOUD_INVALID_RUN_REQUEST.html @@ -34,7 +34,7 @@ -
Recording this run failed because the request was invalid.
+    
Recording this run failed. The request was invalid.
 
 request should follow postRunRequest@2.0.0 schema
 
diff --git a/packages/errors/__snapshot-html__/CLOUD_PROJECT_NOT_FOUND.html b/packages/errors/__snapshot-html__/CLOUD_PROJECT_NOT_FOUND.html
index 9d611931f02d..aa52d1cbda9d 100644
--- a/packages/errors/__snapshot-html__/CLOUD_PROJECT_NOT_FOUND.html
+++ b/packages/errors/__snapshot-html__/CLOUD_PROJECT_NOT_FOUND.html
@@ -42,7 +42,7 @@
 
 We will list the correct projectId in the 'Settings' tab.
 
-Alternatively, you can create a new project using the Desktop Application.
+Alternatively, you can create a new project directly from within the Cypress app.
 
-https://on.cypress.io/dashboard
+https://on.cypress.io/cloud
 
\ No newline at end of file diff --git a/packages/errors/__snapshot-html__/CLOUD_UNKNOWN_INVALID_REQUEST.html b/packages/errors/__snapshot-html__/CLOUD_UNKNOWN_INVALID_REQUEST.html index efd9ce64c704..4a8f63595df4 100644 --- a/packages/errors/__snapshot-html__/CLOUD_UNKNOWN_INVALID_REQUEST.html +++ b/packages/errors/__snapshot-html__/CLOUD_UNKNOWN_INVALID_REQUEST.html @@ -34,14 +34,12 @@ -
We encountered an unexpected error talking to our servers.
+    
We encountered an unexpected error communicating with our servers.
+
+StatusCodeError: 500 - "Internal Server Error"
 
 There is likely something wrong with the request.
 
 The --group flag you passed was: foo
-The --ciBuildId flag you passed was: invalid
-
-The server's response was:
-
-StatusCodeError: 500 - "Internal Server Error"
+The --ciBuildId flag you passed was: invalid
 
\ No newline at end of file diff --git a/packages/errors/__snapshot-html__/DEV_SERVER_CONFIG_FILE_NOT_FOUND.html b/packages/errors/__snapshot-html__/DEV_SERVER_CONFIG_FILE_NOT_FOUND.html index d74ab2966357..c1be73f2b254 100644 --- a/packages/errors/__snapshot-html__/DEV_SERVER_CONFIG_FILE_NOT_FOUND.html +++ b/packages/errors/__snapshot-html__/DEV_SERVER_CONFIG_FILE_NOT_FOUND.html @@ -43,7 +43,7 @@ - vite.config.js - vite.config.ts -Add your vite config at one of the above paths, or import your configuration file and provide it to +Add your vite config at one of the above paths, or import your configuration file and provide it to the devServer config as a viteConfig option.
\ No newline at end of file diff --git a/packages/errors/__snapshot-html__/EXPERIMENTAL_STUDIO_REMOVED.html b/packages/errors/__snapshot-html__/EXPERIMENTAL_STUDIO_REMOVED.html index 7e7d050d4507..cfc2257dc362 100644 --- a/packages/errors/__snapshot-html__/EXPERIMENTAL_STUDIO_REMOVED.html +++ b/packages/errors/__snapshot-html__/EXPERIMENTAL_STUDIO_REMOVED.html @@ -34,7 +34,7 @@ -
We're ending the experimental phase of Cypress Studio in Cypress version 10.0.0. 
+    
We're ending the experimental phase of Cypress Studio in Cypress version 10.0.0.
 
 If you don't think you can live without Studio or you'd like to learn about how to work around its removal, please join the discussion here: http://on.cypress.io/studio-removal
 
diff --git a/packages/errors/__snapshot-html__/MIGRATION_MISMATCHED_CYPRESS_VERSIONS.html b/packages/errors/__snapshot-html__/MIGRATION_MISMATCHED_CYPRESS_VERSIONS.html
index 138960871add..ce18942189c2 100644
--- a/packages/errors/__snapshot-html__/MIGRATION_MISMATCHED_CYPRESS_VERSIONS.html
+++ b/packages/errors/__snapshot-html__/MIGRATION_MISMATCHED_CYPRESS_VERSIONS.html
@@ -34,7 +34,7 @@
     
   
     
-    
You are running Cypress version 10.0.0 in global mode, but you are attempting to migrate a project where Cypress version 9.6.0 is installed. 
+    
You are running Cypress version 10.0.0 in global mode, but you are attempting to migrate a project where Cypress version 9.6.0 is installed.
 
 Ensure the project you are migrating has Cypress version Cypress version 10.0.0 installed.
 
diff --git a/packages/errors/__snapshot-html__/NOT_LOGGED_IN.html b/packages/errors/__snapshot-html__/NOT_LOGGED_IN.html
index ac98f99b559a..d2b4c94281e8 100644
--- a/packages/errors/__snapshot-html__/NOT_LOGGED_IN.html
+++ b/packages/errors/__snapshot-html__/NOT_LOGGED_IN.html
@@ -36,5 +36,5 @@
     
     
You're not logged in.
 
-Run cypress open to open the Desktop App and log in.
+Run cypress open to open Cypress and log in.
 
\ No newline at end of file diff --git a/packages/errors/__snapshot-html__/RECORDING_FROM_FORK_PR.html b/packages/errors/__snapshot-html__/RECORDING_FROM_FORK_PR.html index 9b5793382784..57ad8f4c20db 100644 --- a/packages/errors/__snapshot-html__/RECORDING_FROM_FORK_PR.html +++ b/packages/errors/__snapshot-html__/RECORDING_FROM_FORK_PR.html @@ -40,5 +40,5 @@ These results will not be recorded. -This error will not alter the exit code.
+This error will not affect or change the exit code.
\ No newline at end of file diff --git a/packages/errors/__snapshot-html__/UNEXPECTED_INTERNAL_ERROR.html b/packages/errors/__snapshot-html__/UNEXPECTED_INTERNAL_ERROR.html index 280a2afc4dc3..58a0474b021a 100644 --- a/packages/errors/__snapshot-html__/UNEXPECTED_INTERNAL_ERROR.html +++ b/packages/errors/__snapshot-html__/UNEXPECTED_INTERNAL_ERROR.html @@ -34,10 +34,11 @@ -
We encountered an unexpected internal error. Please check GitHub or open a new issue 
-if you don't see one already with the details below:
+    
We encountered an unexpected internal error.
+
+Please check GitHub or open a new issue if you don't see one already with the details below:
 
 Error: fail whale
     at makeErr (cypress/packages/errors/test/unit/visualSnapshotErrors_spec.ts)
-    at UNEXPECTED_INTERNAL_ERROR (cypress/packages/errors/test/unit/visualSnapshotErrors_spec.ts)
+    at UNEXPECTED_INTERNAL_ERROR (cypress/packages/errors/test/unit/visualSnapshotErrors_spec.ts)
 
\ No newline at end of file diff --git a/packages/errors/__snapshot-html__/VIDEO_POST_PROCESSING_FAILED.html b/packages/errors/__snapshot-html__/VIDEO_POST_PROCESSING_FAILED.html index d825c4409dc5..5f0017b8e6fb 100644 --- a/packages/errors/__snapshot-html__/VIDEO_POST_PROCESSING_FAILED.html +++ b/packages/errors/__snapshot-html__/VIDEO_POST_PROCESSING_FAILED.html @@ -36,7 +36,7 @@
Warning: We failed processing this video.
 
-This error will not alter the exit code.
+This error will not affect or change the exit code.
 
 Error: fail whale
     at makeErr (cypress/packages/errors/test/unit/visualSnapshotErrors_spec.ts)
diff --git a/packages/errors/__snapshot-html__/VIDEO_RECORDING_FAILED.html b/packages/errors/__snapshot-html__/VIDEO_RECORDING_FAILED.html
index b227d04f3322..08df4fedd5b7 100644
--- a/packages/errors/__snapshot-html__/VIDEO_RECORDING_FAILED.html
+++ b/packages/errors/__snapshot-html__/VIDEO_RECORDING_FAILED.html
@@ -36,7 +36,7 @@
     
     
Warning: We failed to record the video.
 
-This error will not alter the exit code.
+This error will not affect or change the exit code.
 
 Error: fail whale
     at makeErr (cypress/packages/errors/test/unit/visualSnapshotErrors_spec.ts)
diff --git a/packages/errors/src/errors.ts b/packages/errors/src/errors.ts
index e5f9d6f212c2..91d724bab67f 100644
--- a/packages/errors/src/errors.ts
+++ b/packages/errors/src/errors.ts
@@ -55,7 +55,7 @@ export const AllCypressErrors = {
     return errTemplate`\
         Warning: We failed to trash the existing run results.
 
-        This error will not alter the exit code.
+        This error will not affect or change the exit code.
 
         ${fmt.stackTrace(arg1)}`
   },
@@ -63,7 +63,7 @@ export const AllCypressErrors = {
     return errTemplate`\
         Warning: We failed to remove old browser profiles from previous runs.
 
-        This error will not alter the exit code.
+        This error will not affect or change the exit code.
 
         ${fmt.stackTrace(arg1)}`
   },
@@ -71,7 +71,7 @@ export const AllCypressErrors = {
     return errTemplate`\
         Warning: We failed to record the video.
 
-        This error will not alter the exit code.
+        This error will not affect or change the exit code.
 
         ${fmt.stackTrace(arg1)}`
   },
@@ -79,7 +79,7 @@ export const AllCypressErrors = {
     return errTemplate`\
         Warning: We failed processing this video.
 
-        This error will not alter the exit code.
+        This error will not affect or change the exit code.
 
         ${fmt.stackTrace(arg1)}`
   },
@@ -133,7 +133,7 @@ export const AllCypressErrors = {
     return errTemplate`\
         You're not logged in.
 
-        Run ${fmt.highlight(`cypress open`)} to open the Desktop App and log in.`
+        Run ${fmt.highlight(`cypress open`)} to open Cypress and log in.`
   },
   TESTS_DID_NOT_START_RETRYING: (arg1: string) => {
     return errTemplate`Timed out waiting for the browser to connect. ${fmt.off(arg1)}`
@@ -144,52 +144,49 @@ export const AllCypressErrors = {
   CLOUD_CANCEL_SKIPPED_SPEC: () => {
     return errTemplate`${fmt.off(`\n  `)}This spec and its tests were skipped because the run has been canceled.`
   },
-  CLOUD_API_RESPONSE_FAILED_RETRYING: (arg1: {tries: number, delay: number, response: Error}) => {
+  CLOUD_API_RESPONSE_FAILED_RETRYING: (arg1: {tries: number, delayMs: number, response: Error}) => {
     const time = pluralize('time', arg1.tries)
-    const delay = humanTime.long(arg1.delay, false)
+    const delay = humanTime.long(arg1.delayMs, false)
 
     return errTemplate`\
-        We encountered an unexpected error talking to our servers.
+        We encountered an unexpected error communicating with our servers.
 
-        We will retry ${fmt.off(arg1.tries)} more ${fmt.off(time)} in ${fmt.off(delay)}...
-
-        The server's response was:
+        ${fmt.highlightSecondary(arg1.response)}
 
-        ${fmt.highlightSecondary(arg1.response)}`
+        We will retry ${fmt.off(arg1.tries)} more ${fmt.off(time)} in ${fmt.off(delay)}...
+        `
     /* Because of fmt.listFlags() and fmt.listItems() */
     /* eslint-disable indent */
   },
   CLOUD_CANNOT_PROCEED_IN_PARALLEL: (arg1: {flags: any, response: Error}) => {
     return errTemplate`\
-        We encountered an unexpected error talking to our servers.
+        We encountered an unexpected error communicating with our servers.
+
+        ${fmt.highlightSecondary(arg1.response)}
 
         Because you passed the ${fmt.flag(`--parallel`)} flag, this run cannot proceed because it requires a valid response from our servers.
 
         ${fmt.listFlags(arg1.flags, {
       group: '--group',
       ciBuildId: '--ciBuildId',
-    })}
-
-        The server's response was:
-
-        ${fmt.highlightSecondary(arg1.response)}`
+    })}`
   },
   CLOUD_CANNOT_PROCEED_IN_SERIAL: (arg1: {flags: any, response: Error}) => {
     return errTemplate`\
-        We encountered an unexpected error talking to our servers.
+        We encountered an unexpected error communicating with our servers.
+
+        ${fmt.highlightSecondary(arg1.response)}
 
         ${fmt.listFlags(arg1.flags, {
       group: '--group',
       ciBuildId: '--ciBuildId',
-    })}
-
-        The server's response was:
-
-        ${fmt.highlightSecondary(arg1.response)}`
+    })}`
   },
   CLOUD_UNKNOWN_INVALID_REQUEST: (arg1: {flags: any, response: Error}) => {
     return errTemplate`\
-        We encountered an unexpected error talking to our servers.
+        We encountered an unexpected error communicating with our servers.
+
+        ${fmt.highlightSecondary(arg1.response)}
 
         There is likely something wrong with the request.
 
@@ -198,11 +195,7 @@ export const AllCypressErrors = {
       group: '--group',
       parallel: '--parallel',
       ciBuildId: '--ciBuildId',
-    })}
-
-        The server's response was:
-
-        ${fmt.highlightSecondary(arg1.response)}`
+    })}`
   },
   CLOUD_UNKNOWN_CREATE_RUN_WARNING: (arg1: {props?: any, message: string}) => {
     if (!Object.keys(arg1.props).length) {
@@ -361,15 +354,15 @@ export const AllCypressErrors = {
       ${fmt.highlightSecondary(`Auto Cancellation`)} is not included under your current billing plan.
 
       To enable this service, please visit your billing and upgrade to another plan with Auto Cancellation.
-      
+
       ${fmt.off(arg1.link)}`
   },
   CLOUD_AUTO_CANCEL_MISMATCH: (arg1: {runUrl: string}) => {
     return errTemplate`\
         You passed the ${fmt.flag(`--auto-cancel-after-failures`)} flag, but this run originally started with a different value for the ${fmt.flag(`--auto-cancel-after-failures`)} flag.
-      
+
         The existing run is: ${fmt.url(arg1.runUrl)}
-        
+
         ${fmt.listFlags(arg1, {
       tags: '--tag',
       group: '--group',
@@ -379,7 +372,7 @@ export const AllCypressErrors = {
     })}
 
         The first setting of --auto-cancel-after-failures for any given run takes precedent.
-        
+
         https://on.cypress.io/auto-cancellation-mismatch`
   },
   DEPRECATED_BEFORE_BROWSER_LAUNCH_ARGS: () => {
@@ -497,7 +490,7 @@ export const AllCypressErrors = {
   },
   CLOUD_INVALID_RUN_REQUEST: (arg1: {message: string, errors: string[], object: object}) => {
     return errTemplate`\
-        Recording this run failed because the request was invalid.
+        Recording this run failed. The request was invalid.
 
         ${fmt.highlight(arg1.message)}
 
@@ -517,7 +510,7 @@ export const AllCypressErrors = {
 
         These results will not be recorded.
 
-        This error will not alter the exit code.`
+        This error will not affect or change the exit code.`
   },
   CLOUD_CANNOT_UPLOAD_RESULTS: (apiErr: Error) => {
     return errTemplate`\
@@ -525,17 +518,17 @@ export const AllCypressErrors = {
 
         These results will not be recorded.
 
-        This error will not alter the exit code.
+        This error will not affect or change the exit code.
 
         ${fmt.highlightSecondary(apiErr)}`
   },
   CLOUD_CANNOT_CREATE_RUN_OR_INSTANCE: (apiErr: Error) => {
     return errTemplate`\
-        Warning: We encountered an error talking to our servers.
+        Warning: We encountered an error communicating with our servers.
 
-        This run will not be recorded.
+        This run will proceed, but will not be recorded.
 
-        This error will not alter the exit code.
+        This error will not affect or change the exit code.
 
         ${fmt.highlightSecondary(apiErr)}`
   },
@@ -559,9 +552,9 @@ export const AllCypressErrors = {
 
         We will list the correct projectId in the 'Settings' tab.
 
-        Alternatively, you can create a new project using the Desktop Application.
+        Alternatively, you can create a new project directly from within the Cypress app.
 
-        https://on.cypress.io/dashboard`
+        https://on.cypress.io/cloud`
   },
   // TODO: make this relative path, not absolute
   NO_PROJECT_ID: (configFilePath: string) => {
@@ -879,7 +872,7 @@ export const AllCypressErrors = {
   CONFIG_FILES_LANGUAGE_CONFLICT: (projectRoot: string, filesFound: string[]) => {
     return errTemplate`
       Could not load a Cypress configuration file because there are multiple matches.
-      
+
       We've found ${fmt.highlight(filesFound.length)} Cypress configuration files named
       ${fmt.highlight(filesFound.join(', '))} at the location below:
 
@@ -1141,7 +1134,7 @@ export const AllCypressErrors = {
         The ${fmt.highlight(`experimentalSessionSupport`)} configuration option was removed in ${fmt.cypressVersion(`9.6.0`)}.
 
         You can safely remove this option from your config.
-        
+
         https://on.cypress.io/session`
   },
   EXPERIMENTAL_SESSION_AND_ORIGIN_REMOVED: () => {
@@ -1149,7 +1142,7 @@ export const AllCypressErrors = {
         The ${fmt.highlight(`experimentalSessionAndOrigin`)} configuration option was removed in ${fmt.cypressVersion(`12.0.0`)}.
 
         You can safely remove this option from your config.
-        
+
         https://on.cypress.io/session
         https://on.cypress.io/origin`
   },
@@ -1173,10 +1166,10 @@ export const AllCypressErrors = {
   },
   EXPERIMENTAL_STUDIO_REMOVED: () => {
     return errTemplate`\
-        We're ending the experimental phase of Cypress Studio in ${fmt.cypressVersion(`10.0.0`)}. 
-        
+        We're ending the experimental phase of Cypress Studio in ${fmt.cypressVersion(`10.0.0`)}.
+
         If you don't think you can live without Studio or you'd like to learn about how to work around its removal, please join the discussion here: http://on.cypress.io/studio-removal
-        
+
         Your feedback will help us factor in product decisions that may see Studio return in a future release.
 
         You can safely remove the ${fmt.highlight(`experimentalStudio`)} configuration option from your config.`
@@ -1400,11 +1393,11 @@ export const AllCypressErrors = {
 
     return errTemplate`\
         The ${fmt.highlight('pluginsFile')} configuration option you have supplied has been replaced with ${fmt.highlightSecondary('setupNodeEvents')}.
-        
+
         This new option is not a one-to-one correlation and it must be configured separately as a testing type property: ${fmt.highlightSecondary('e2e.setupNodeEvents')} and ${fmt.highlightSecondary('component.setupNodeEvents')}
-        
+
         ${fmt.code(code)}
-        
+
         https://on.cypress.io/migration-guide`
   },
 
@@ -1565,8 +1558,9 @@ export const AllCypressErrors = {
 
   UNEXPECTED_INTERNAL_ERROR: (err: Error) => {
     return errTemplate`
-      We encountered an unexpected internal error. Please check GitHub or open a new issue 
-      if you don't see one already with the details below:
+      We encountered an unexpected internal error.
+
+      Please check GitHub or open a new issue if you don't see one already with the details below:
 
       ${fmt.stackTrace(err)}
     `
@@ -1617,7 +1611,7 @@ export const AllCypressErrors = {
       ${fmt.code(code)}
 
       https://on.cypress.io/migration-guide
-      
+
       ${stackTrace}
       `
   },
@@ -1661,14 +1655,14 @@ export const AllCypressErrors = {
       ${fmt.code(code)}
 
       https://on.cypress.io/migration-guide
-      
+
       ${stackTrace}
       `
   },
 
   MIGRATION_MISMATCHED_CYPRESS_VERSIONS: (version: string, currentVersion: string) => {
     return errTemplate`
-      You are running ${fmt.cypressVersion(currentVersion)} in global mode, but you are attempting to migrate a project where ${fmt.cypressVersion(version)} is installed. 
+      You are running ${fmt.cypressVersion(currentVersion)} in global mode, but you are attempting to migrate a project where ${fmt.cypressVersion(version)} is installed.
 
       Ensure the project you are migrating has Cypress version ${fmt.cypressVersion(currentVersion)} installed.
 
@@ -1691,14 +1685,14 @@ export const AllCypressErrors = {
 
     return errTemplate`\
       You are using ${fmt.highlight(devServer)} for your dev server, but a configuration file was not found. We traversed upwards from:
-      
+
       ${fmt.highlightSecondary(root)}
-      
+
       looking for a file named:
 
       ${fmt.listItems(searchedFor, { prefix: ' - ' })}
 
-      Add your ${fmt.highlight(devServer)} config at one of the above paths, or import your configuration file and provide it to 
+      Add your ${fmt.highlight(devServer)} config at one of the above paths, or import your configuration file and provide it to
       the devServer config as a ${fmt.highlight(devServerConfigFile)} option.
     `
   },
diff --git a/packages/errors/test/unit/visualSnapshotErrors_spec.ts b/packages/errors/test/unit/visualSnapshotErrors_spec.ts
index 8dd2e3bfeed7..2f078cfba3fa 100644
--- a/packages/errors/test/unit/visualSnapshotErrors_spec.ts
+++ b/packages/errors/test/unit/visualSnapshotErrors_spec.ts
@@ -375,12 +375,12 @@ describe('visual error templates', () => {
       return {
         default: [{
           tries: 3,
-          delay: 5000,
+          delayMs: 5000,
           response: makeApiErr(),
         }],
         lastTry: [{
           tries: 1,
-          delay: 5000,
+          delayMs: 5000,
           response: makeApiErr(),
         }],
       }
diff --git a/packages/server/lib/cloud/api.ts b/packages/server/lib/cloud/api.ts
index 179ae5cbdfa2..0afda703c4b4 100644
--- a/packages/server/lib/cloud/api.ts
+++ b/packages/server/lib/cloud/api.ts
@@ -13,18 +13,18 @@ const errors = require('../errors')
 const { apiUrl, apiRoutes, makeRoutes } = require('./routes')
 
 import Bluebird from 'bluebird'
-import type { OptionsWithUrl } from 'request-promise'
+import { getText } from '../util/status_code'
 import * as enc from './encryption'
+import getEnvInformationForProjectRoot from './environment'
 
+import type { OptionsWithUrl } from 'request-promise'
 const THIRTY_SECONDS = humanInterval('30 seconds')
 const SIXTY_SECONDS = humanInterval('60 seconds')
 const TWO_MINUTES = humanInterval('2 minutes')
 
-const DELAYS: number[] = process.env.API_RETRY_INTERVALS ? process.env.API_RETRY_INTERVALS.split(',').map(_.toNumber) : [
-  THIRTY_SECONDS,
-  SIXTY_SECONDS,
-  TWO_MINUTES,
-]
+const DELAYS: number[] = process.env.API_RETRY_INTERVALS
+  ? process.env.API_RETRY_INTERVALS.split(',').map(_.toNumber)
+  : [THIRTY_SECONDS, SIXTY_SECONDS, TWO_MINUTES]
 
 const runnerCapabilities = {
   'dynamicSpecsInSerialMode': true,
@@ -35,6 +35,7 @@ let responseCache = {}
 
 class DecryptionError extends Error {
   isDecryptionError = true
+
   constructor (message: string) {
     super(message)
     this.name = 'DecryptionError'
@@ -51,7 +52,7 @@ const rp = request.defaults((params: CypressRequestOptions, callback) => {
   let resp
 
   if (params.cacheable && (resp = getCachedResponse(params))) {
-    debug('resolving with cached response for ', params.url)
+    debug('resolving with cached response for %o', { url: params.url })
 
     return Bluebird.resolve(resp)
   }
@@ -65,7 +66,7 @@ const rp = request.defaults((params: CypressRequestOptions, callback) => {
     rejectUnauthorized: true,
   })
 
-  const headers = params.headers != null ? params.headers : (params.headers = {})
+  const headers = params.headers ??= {}
 
   _.defaults(headers, {
     'x-os-name': os.platform(),
@@ -89,19 +90,35 @@ const rp = request.defaults((params: CypressRequestOptions, callback) => {
       const { secretKey, jwe } = await enc.encryptRequest(params)
 
       params.transform = async function (body, response) {
-        if (response.headers['x-cypress-encrypted'] || params.encrypt === 'always' && response.statusCode < 500) {
+        const { statusCode } = response
+        const options = this // request promise options
+
+        const throwStatusCodeErrWithResp = (message, responseBody) => {
+          throw new RequestErrors.StatusCodeError(response.statusCode, message, options, responseBody)
+        }
+
+        // response is valid and we are encrypting
+        if (response.headers['x-cypress-encrypted'] || params.encrypt === 'always') {
           let decryptedBody
 
           try {
             decryptedBody = await enc.decryptResponse(body, secretKey)
           } catch (e) {
+            // we failed decrypting the response...
+
+            // if status code is >=500 or 404 remove body
+            if (statusCode >= 500 || statusCode === 404) {
+              // remove server responses and replace with basic status code text
+              throwStatusCodeErrWithResp(getText(statusCode), body)
+            }
+
             throw new DecryptionError(e.message)
           }
 
           // If we've hit an encrypted payload error case, we need to re-constitute the error
           // as it would happen normally, with the body as an error property
           if (response.statusCode > 400) {
-            throw new RequestErrors.StatusCodeError(response.statusCode, decryptedBody, {}, decryptedBody)
+            throwStatusCodeErrWithResp(decryptedBody, decryptedBody)
           }
 
           return decryptedBody
@@ -136,28 +153,29 @@ const getCachedResponse = (params) => {
 }
 
 const retryWithBackoff = (fn) => {
-  // for e2e testing purposes
-  let attempt
-
   if (process.env.DISABLE_API_RETRIES) {
     debug('api retries disabled')
 
     return Bluebird.try(() => fn(0))
   }
 
-  return (attempt = (retryIndex) => {
+  const attempt = (retryIndex) => {
     return Bluebird
     .try(() => fn(retryIndex))
+    .catch(RequestErrors.TransformError, (err) => {
+      // Unroll the error thrown from within the transform
+      throw err.cause
+    })
     .catch(isRetriableError, (err) => {
-      if (retryIndex > DELAYS.length) {
+      if (retryIndex >= DELAYS.length) {
         throw err
       }
 
-      const delay = DELAYS[retryIndex]
+      const delayMs = DELAYS[retryIndex]
 
       errors.warning(
         'CLOUD_API_RESPONSE_FAILED_RETRYING', {
-          delay,
+          delayMs,
           tries: DELAYS.length - retryIndex,
           response: err,
         },
@@ -166,17 +184,16 @@ const retryWithBackoff = (fn) => {
       retryIndex++
 
       return Bluebird
-      .delay(delay)
+      .delay(delayMs)
       .then(() => {
-        debug(`retry #${retryIndex} after ${delay}ms`)
+        debug(`retry #${retryIndex} after ${delayMs}ms`)
 
         return attempt(retryIndex)
       })
     })
-    .catch(RequestErrors.TransformError, (err) => {
-      throw err.cause
-    })
-  })(0)
+  }
+
+  return attempt(0)
 }
 
 const formatResponseBody = function (err) {
@@ -198,10 +215,9 @@ const tagError = function (err) {
 }
 
 // retry on timeouts, 5xx errors, or any error without a status code
-// do not retry on decryption errors
+// including decryption errors
 const isRetriableError = (err) => {
-  // TransformError means something failed in decryption handling
-  if (err instanceof RequestErrors.TransformError) {
+  if (err instanceof DecryptionError) {
     return false
   }
 
@@ -211,6 +227,7 @@ const isRetriableError = (err) => {
 }
 
 export type CreateRunOptions = {
+  projectRoot: string
   ci: string
   ciBuildId: string
   projectId: string
@@ -228,7 +245,6 @@ export type CreateRunOptions = {
 
 let preflightResult = {
   encrypt: true,
-  apiUrl,
 }
 
 let recordRoutes = apiRoutes
@@ -248,7 +264,6 @@ module.exports = {
     recordRoutes = apiRoutes
     preflightResult = {
       encrypt: true,
-      apiUrl,
     }
   },
 
@@ -270,9 +285,10 @@ module.exports = {
   },
 
   createRun (options: CreateRunOptions) {
-    const preflightOptions = _.pick(options, ['projectId', 'ciBuildId', 'browser', 'testingType', 'parallel'])
+    const preflightOptions = _.pick(options, ['projectId', 'projectRoot', 'ciBuildId', 'browser', 'testingType', 'parallel', 'timeout'])
 
-    return this.preflight(preflightOptions).then((result) => {
+    return this.sendPreflight(preflightOptions)
+    .then((result) => {
       const { warnings } = result
 
       return retryWithBackoff((attemptIndex) => {
@@ -300,7 +316,7 @@ module.exports = {
           url: recordRoutes.runs(),
           json: true,
           encrypt: preflightResult.encrypt,
-          timeout: options.timeout != null ? options.timeout : SIXTY_SECONDS,
+          timeout: options.timeout ?? SIXTY_SECONDS,
           headers: {
             'x-route-version': '4',
             'x-cypress-request-attempt': attemptIndex,
@@ -334,7 +350,7 @@ module.exports = {
         url: recordRoutes.instances(runId),
         json: true,
         encrypt: preflightResult.encrypt,
-        timeout: timeout != null ? timeout : SIXTY_SECONDS,
+        timeout: timeout ?? SIXTY_SECONDS,
         headers: {
           'x-route-version': '5',
           'x-cypress-run-id': runId,
@@ -354,7 +370,7 @@ module.exports = {
         url: recordRoutes.instanceTests(instanceId),
         json: true,
         encrypt: preflightResult.encrypt,
-        timeout: timeout || SIXTY_SECONDS,
+        timeout: timeout ?? SIXTY_SECONDS,
         headers: {
           'x-route-version': '1',
           'x-cypress-run-id': runId,
@@ -372,7 +388,7 @@ module.exports = {
       return rp.put({
         url: recordRoutes.instanceStdout(options.instanceId),
         json: true,
-        timeout: options.timeout != null ? options.timeout : SIXTY_SECONDS,
+        timeout: options.timeout ?? SIXTY_SECONDS,
         body: {
           stdout: options.stdout,
         },
@@ -393,7 +409,7 @@ module.exports = {
         url: recordRoutes.instanceResults(options.instanceId),
         json: true,
         encrypt: preflightResult.encrypt,
-        timeout: options.timeout != null ? options.timeout : SIXTY_SECONDS,
+        timeout: options.timeout ?? SIXTY_SECONDS,
         headers: {
           'x-route-version': '1',
           'x-cypress-run-id': options.runId,
@@ -452,23 +468,52 @@ module.exports = {
     responseCache = {}
   },
 
-  preflight (preflightInfo) {
+  sendPreflight (preflightInfo) {
     return retryWithBackoff(async (attemptIndex) => {
-      const preflightBase = process.env.CYPRESS_API_URL ? apiUrl.replace('api', 'api-proxy') : apiUrl
-      const result = await rp.post({
-        url: `${preflightBase}preflight`,
-        body: {
-          apiUrl,
-          envUrl: process.env.CYPRESS_API_URL,
-          ...preflightInfo,
-        },
-        headers: {
-          'x-route-version': '1',
-          'x-cypress-request-attempt': attemptIndex,
-        },
-        json: true,
-        encrypt: 'always',
-      })
+      const { timeout, projectRoot } = preflightInfo
+
+      preflightInfo = _.omit(preflightInfo, 'timeout', 'projectRoot')
+
+      const preflightBaseProxy = apiUrl.replace('api', 'api-proxy')
+
+      const envInformation = await getEnvInformationForProjectRoot(projectRoot, process.pid.toString())
+      const makeReq = ({ baseUrl, agent }) => {
+        return rp.post({
+          url: `${baseUrl}preflight`,
+          body: {
+            apiUrl,
+            envUrl: envInformation.envUrl,
+            dependencies: envInformation.dependencies,
+            errors: envInformation.errors,
+            ...preflightInfo,
+          },
+          headers: {
+            'x-route-version': '1',
+            'x-cypress-request-attempt': attemptIndex,
+          },
+          timeout: timeout ?? SIXTY_SECONDS,
+          json: true,
+          encrypt: 'always',
+          agent,
+        })
+        .catch(RequestErrors.TransformError, (err) => {
+          // Unroll the error thrown from within the transform
+          throw err.cause
+        })
+      }
+
+      const postReqs = async () => {
+        return makeReq({ baseUrl: preflightBaseProxy, agent: null })
+        .catch((err) => {
+          if (err.statusCode === 412) {
+            throw err
+          }
+
+          return makeReq({ baseUrl: apiUrl, agent })
+        })
+      }
+
+      const result = await postReqs()
 
       preflightResult = result // { encrypt: boolean, apiUrl: string }
       recordRoutes = makeRoutes(result.apiUrl)
diff --git a/packages/server/lib/cloud/environment.ts b/packages/server/lib/cloud/environment.ts
new file mode 100644
index 000000000000..f86a2609b0d8
--- /dev/null
+++ b/packages/server/lib/cloud/environment.ts
@@ -0,0 +1,152 @@
+import { exec } from 'child_process'
+import { promisify } from 'util'
+import base64Url from 'base64url'
+import fs from 'fs-extra'
+import resolvePackagePath from 'resolve-package-path'
+
+const execAsync = promisify(exec)
+
+// See https://whimsical.com/encryption-logic-BtJJkN7TxacK8kaHDgH1zM for more information on what this is doing
+const getProcessBranchForPid = async (pid: string) => {
+  const { stdout } = await execAsync('ps -eo pid=,ppid=')
+  const processTree = stdout.split('\n').reduce((acc, line) => {
+    const [pid, ppid] = line.trim().split(/\s+/)
+
+    acc.set(pid, ppid)
+
+    return acc
+  }, new Map())
+
+  const currentProcessBranch: string[] = []
+
+  while (pid && pid !== '0') {
+    currentProcessBranch.push(pid)
+    pid = processTree.get(pid)
+  }
+
+  return currentProcessBranch
+}
+
+interface GetCypressEnvUrlFromProcessBranch {
+  envUrl?: string
+  error?: {
+    dependency?: string
+    name: string
+    message: string
+    stack: string
+  }
+}
+
+// See https://whimsical.com/encryption-logic-BtJJkN7TxacK8kaHDgH1zM for more information on what this is doing
+const getCypressEnvUrlFromProcessBranch = async (pid: string): Promise => {
+  let error: { name: string, message: string, stack: string } | undefined
+  let envUrl: string | undefined
+
+  if (process.platform !== 'win32') {
+    try {
+      const processBranch = await getProcessBranchForPid(pid)
+      const { stdout } = await execAsync(`ps eww -p ${processBranch.join(',')} -o pid=,command=`)
+
+      const pidEnvUrlMapping = stdout.split('\n').reduce((acc, line) => {
+        const cypressEnvUrl = line.trim().match(/(\d+)\s.*CYPRESS_API_URL=(\S+)\s/)
+
+        if (cypressEnvUrl) {
+          acc.set(cypressEnvUrl[1], cypressEnvUrl[2])
+        }
+
+        return acc
+      }, new Map())
+
+      const foundPid = processBranch.find((pid) => pidEnvUrlMapping.get(pid))
+
+      if (foundPid) {
+        envUrl = pidEnvUrlMapping.get(foundPid)
+      }
+    } catch (err) {
+      error = err
+    }
+  }
+
+  return {
+    envUrl,
+    error,
+  }
+}
+
+interface DependencyInformation {
+  maybeCheckProcessTreeIfPresent: string[]
+  neverCheckProcessTreeIfPresent: string[]
+}
+
+// See https://whimsical.com/encryption-logic-BtJJkN7TxacK8kaHDgH1zM for more information on what this is doing
+const getEnvInformationForProjectRoot = async (projectRoot: string, pid: string) => {
+  let dependencies = {}
+  let errors: { dependency?: string, name: string, message: string, stack: string }[] = []
+  let envDependencies = process.env.CYPRESS_ENV_DEPENDENCIES
+  let envUrl = process.env.CYPRESS_API_URL
+  let checkProcessTree
+
+  if (envDependencies) {
+    const envDependenciesInformation = JSON.parse(base64Url.decode(envDependencies)) as DependencyInformation
+
+    const packageToJsonMapping: Record = {}
+
+    const processDependency = ({ checkOnFound }) => {
+      return (dependency) => {
+        try {
+          const packageJsonPath = resolvePackagePath(dependency, projectRoot)
+
+          if (packageJsonPath) {
+            packageToJsonMapping[dependency] = packageJsonPath
+            checkProcessTree = checkOnFound
+          }
+        } catch (error) {
+          errors.push({
+            dependency,
+            name: error.name,
+            message: error.message,
+            stack: error.stack,
+          })
+        }
+      }
+    }
+
+    envDependenciesInformation.maybeCheckProcessTreeIfPresent.forEach(processDependency({ checkOnFound: true }))
+    envDependenciesInformation.neverCheckProcessTreeIfPresent.forEach(processDependency({ checkOnFound: false }))
+
+    const [{ envUrl: processTreeEnvUrl, error: processTreeError }] = await Promise.all([
+      checkProcessTree ? getCypressEnvUrlFromProcessBranch(pid) : { envUrl: undefined, error: undefined },
+      ...Object.entries(packageToJsonMapping).map(async ([dependency, packageJsonPath]) => {
+        try {
+          const packageVersion = (await fs.readJSON(packageJsonPath)).version
+
+          dependencies[dependency] = {
+            version: packageVersion,
+          }
+        } catch (error) {
+          errors.push({
+            dependency,
+            name: error.name,
+            message: error.message,
+            stack: error.stack,
+          })
+        }
+      }),
+    ])
+
+    if (processTreeEnvUrl || processTreeError) {
+      envUrl = processTreeEnvUrl
+      if (processTreeError) {
+        errors.push(processTreeError)
+      }
+    }
+  }
+
+  return {
+    envUrl,
+    errors,
+    dependencies,
+  }
+}
+
+export default getEnvInformationForProjectRoot
diff --git a/packages/server/lib/modes/record.js b/packages/server/lib/modes/record.js
index 0097820aa678..0d52e25c7b08 100644
--- a/packages/server/lib/modes/record.js
+++ b/packages/server/lib/modes/record.js
@@ -267,7 +267,7 @@ const createRun = Promise.method((options = {}) => {
     ciBuildId: null,
   })
 
-  let { projectId, recordKey, platform, git, specPattern, specs, parallel, ciBuildId, group, tags, testingType, autoCancelAfterFailures } = options
+  let { projectRoot, projectId, recordKey, platform, git, specPattern, specs, parallel, ciBuildId, group, tags, testingType, autoCancelAfterFailures } = options
 
   if (recordKey == null) {
     recordKey = env.get('CYPRESS_RECORD_KEY')
@@ -310,6 +310,7 @@ const createRun = Promise.method((options = {}) => {
   debugCiInfo('CI provider information %o', ci)
 
   return api.createRun({
+    projectRoot,
     specs,
     group,
     tags,
@@ -384,9 +385,8 @@ const createRun = Promise.method((options = {}) => {
       }
     })
   }).catch((err) => {
-    debug('failed creating run with status %d %o', err.statusCode, {
-      stack: err.stack,
-    })
+    debug('failed creating run with status %o',
+      _.pick(err, ['name', 'message', 'statusCode', 'stack']))
 
     switch (err.statusCode) {
       case 401:
@@ -618,6 +618,7 @@ const createRunAndRecordSpecs = (options = {}) => {
     }
 
     return createRun({
+      projectRoot,
       git,
       specs,
       group,
diff --git a/packages/server/package.json b/packages/server/package.json
index 59d1c07826f2..a2192e40a130 100644
--- a/packages/server/package.json
+++ b/packages/server/package.json
@@ -107,6 +107,7 @@
     "randomstring": "1.1.5",
     "recast": "0.20.4",
     "resolve": "1.17.0",
+    "resolve-package-path": "4.0.3",
     "sanitize-filename": "1.6.3",
     "semver": "7.3.2",
     "send": "0.17.1",
diff --git a/packages/server/test/integration/cypress_spec.js b/packages/server/test/integration/cypress_spec.js
index 360c5b960528..581c912b512d 100644
--- a/packages/server/test/integration/cypress_spec.js
+++ b/packages/server/test/integration/cypress_spec.js
@@ -1196,7 +1196,7 @@ describe('lib/cypress', () => {
     beforeEach(async function () {
       await clearCtx()
 
-      sinon.stub(api, 'preflight').resolves()
+      sinon.stub(api, 'sendPreflight').resolves()
       sinon.stub(api, 'createRun').resolves()
       const createInstanceStub = sinon.stub(api, 'createInstance')
 
diff --git a/packages/server/test/support/fixtures/cloud/environment/.gitignore b/packages/server/test/support/fixtures/cloud/environment/.gitignore
new file mode 100644
index 000000000000..cf4bab9ddde9
--- /dev/null
+++ b/packages/server/test/support/fixtures/cloud/environment/.gitignore
@@ -0,0 +1 @@
+!node_modules
diff --git a/packages/server/test/support/fixtures/cloud/environment/all-tracked-dependencies/node_modules/bar/package.json b/packages/server/test/support/fixtures/cloud/environment/all-tracked-dependencies/node_modules/bar/package.json
new file mode 100644
index 000000000000..55f760d8d406
--- /dev/null
+++ b/packages/server/test/support/fixtures/cloud/environment/all-tracked-dependencies/node_modules/bar/package.json
@@ -0,0 +1,6 @@
+{
+  "name": "bar",
+  "version": "2.0.0",
+  "main": "src/index.js",
+  "type": "module"
+}
diff --git a/packages/server/test/support/fixtures/cloud/environment/all-tracked-dependencies/node_modules/bar/src/index.js b/packages/server/test/support/fixtures/cloud/environment/all-tracked-dependencies/node_modules/bar/src/index.js
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/packages/server/test/support/fixtures/cloud/environment/all-tracked-dependencies/node_modules/foo/index.js b/packages/server/test/support/fixtures/cloud/environment/all-tracked-dependencies/node_modules/foo/index.js
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/packages/server/test/support/fixtures/cloud/environment/all-tracked-dependencies/node_modules/foo/package.json b/packages/server/test/support/fixtures/cloud/environment/all-tracked-dependencies/node_modules/foo/package.json
new file mode 100644
index 000000000000..44290b9c0cbd
--- /dev/null
+++ b/packages/server/test/support/fixtures/cloud/environment/all-tracked-dependencies/node_modules/foo/package.json
@@ -0,0 +1,6 @@
+{
+  "name": "foo",
+  "version": "1.0.0",
+  "main": "index.js",
+  "type": "module"
+}
diff --git a/packages/server/test/support/fixtures/cloud/environment/all-tracked-dependencies/package.json b/packages/server/test/support/fixtures/cloud/environment/all-tracked-dependencies/package.json
new file mode 100644
index 000000000000..18a31aa405f5
--- /dev/null
+++ b/packages/server/test/support/fixtures/cloud/environment/all-tracked-dependencies/package.json
@@ -0,0 +1,8 @@
+{
+  "name": "all-tracked-dependencies",
+  "version": "1.0.0",
+  "dependencies": {
+    "bar": "2.0.0",
+    "foo": "1.0.0"
+  }
+}
diff --git a/packages/server/test/support/fixtures/cloud/environment/partial-dependencies-matching/node_modules/foo/index.js b/packages/server/test/support/fixtures/cloud/environment/partial-dependencies-matching/node_modules/foo/index.js
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/packages/server/test/support/fixtures/cloud/environment/partial-dependencies-matching/node_modules/foo/package.json b/packages/server/test/support/fixtures/cloud/environment/partial-dependencies-matching/node_modules/foo/package.json
new file mode 100644
index 000000000000..44290b9c0cbd
--- /dev/null
+++ b/packages/server/test/support/fixtures/cloud/environment/partial-dependencies-matching/node_modules/foo/package.json
@@ -0,0 +1,6 @@
+{
+  "name": "foo",
+  "version": "1.0.0",
+  "main": "index.js",
+  "type": "module"
+}
diff --git a/packages/server/test/support/fixtures/cloud/environment/partial-dependencies-matching/package.json b/packages/server/test/support/fixtures/cloud/environment/partial-dependencies-matching/package.json
new file mode 100644
index 000000000000..902097307b95
--- /dev/null
+++ b/packages/server/test/support/fixtures/cloud/environment/partial-dependencies-matching/package.json
@@ -0,0 +1,7 @@
+{
+  "name": "all-tracked-dependencies",
+  "version": "1.0.0",
+  "dependencies": {
+    "foo": "1.0.0"
+  }
+}
diff --git a/packages/server/test/support/fixtures/cloud/environment/partial-dependencies-not-matching/node_modules/bar/package.json b/packages/server/test/support/fixtures/cloud/environment/partial-dependencies-not-matching/node_modules/bar/package.json
new file mode 100644
index 000000000000..55f760d8d406
--- /dev/null
+++ b/packages/server/test/support/fixtures/cloud/environment/partial-dependencies-not-matching/node_modules/bar/package.json
@@ -0,0 +1,6 @@
+{
+  "name": "bar",
+  "version": "2.0.0",
+  "main": "src/index.js",
+  "type": "module"
+}
diff --git a/packages/server/test/support/fixtures/cloud/environment/partial-dependencies-not-matching/node_modules/bar/src/index.js b/packages/server/test/support/fixtures/cloud/environment/partial-dependencies-not-matching/node_modules/bar/src/index.js
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/packages/server/test/support/fixtures/cloud/environment/partial-dependencies-not-matching/package.json b/packages/server/test/support/fixtures/cloud/environment/partial-dependencies-not-matching/package.json
new file mode 100644
index 000000000000..ddbbee04ac36
--- /dev/null
+++ b/packages/server/test/support/fixtures/cloud/environment/partial-dependencies-not-matching/package.json
@@ -0,0 +1,7 @@
+{
+  "name": "all-tracked-dependencies",
+  "version": "1.0.0",
+  "dependencies": {
+    "bar": "2.0.0"
+  }
+}
diff --git a/packages/server/test/support/fixtures/cloud/environment/test-project/child.js b/packages/server/test/support/fixtures/cloud/environment/test-project/child.js
new file mode 100644
index 000000000000..ae77d3f731e0
--- /dev/null
+++ b/packages/server/test/support/fixtures/cloud/environment/test-project/child.js
@@ -0,0 +1,26 @@
+import { spawn } from 'child_process'
+import path from 'path'
+import * as url from 'url'
+
+// eslint-disable-next-line no-console
+console.log('child', process.pid, process.ppid, process.env.CHILD_CYPRESS_API_URL)
+
+const __dirname = url.fileURLToPath(new URL('.', import.meta.url))
+
+const proc = spawn('node', ['grandchild.js'], {
+  cwd: path.join(__dirname),
+  stdio: 'inherit',
+  env: {
+    ...process.env,
+    CYPRESS_API_URL: process.env.PARENT_CYPRESS_API_URL,
+  },
+})
+
+const timeout = setTimeout(() => {
+
+}, 1e9)
+
+process.on('SIGTERM', () => {
+  clearTimeout(timeout)
+  proc.kill()
+})
diff --git a/packages/server/test/support/fixtures/cloud/environment/test-project/grandchild.js b/packages/server/test/support/fixtures/cloud/environment/test-project/grandchild.js
new file mode 100644
index 000000000000..8a96cb8fbc16
--- /dev/null
+++ b/packages/server/test/support/fixtures/cloud/environment/test-project/grandchild.js
@@ -0,0 +1,10 @@
+// eslint-disable-next-line no-console
+console.log('grandchild', process.pid, process.ppid, process.env.GRANDCHILD_CYPRESS_API_URL)
+
+const timeout = setTimeout(() => {
+
+}, 1e9)
+
+process.on('SIGTERM', () => {
+  clearTimeout(timeout)
+})
diff --git a/packages/server/test/support/fixtures/cloud/environment/test-project/index.js b/packages/server/test/support/fixtures/cloud/environment/test-project/index.js
new file mode 100644
index 000000000000..abba1c052854
--- /dev/null
+++ b/packages/server/test/support/fixtures/cloud/environment/test-project/index.js
@@ -0,0 +1,26 @@
+import { spawn } from 'child_process'
+import path from 'path'
+import * as url from 'url'
+
+// eslint-disable-next-line no-console
+console.log('parent', process.pid, process.ppid, process.env.PARENT_CYPRESS_API_URL)
+
+const __dirname = url.fileURLToPath(new URL('.', import.meta.url))
+
+const proc = spawn('node', ['child.js'], {
+  cwd: path.join(__dirname),
+  stdio: 'inherit',
+  env: {
+    ...process.env,
+    CYPRESS_API_URL: process.env.PARENT_CYPRESS_API_URL,
+  },
+})
+
+const timeout = setTimeout(() => {
+
+}, 1e9)
+
+process.on('SIGTERM', () => {
+  clearTimeout(timeout)
+  proc.kill()
+})
diff --git a/packages/server/test/support/fixtures/cloud/environment/test-project/package.json b/packages/server/test/support/fixtures/cloud/environment/test-project/package.json
new file mode 100644
index 000000000000..8ca713b3fa7e
--- /dev/null
+++ b/packages/server/test/support/fixtures/cloud/environment/test-project/package.json
@@ -0,0 +1,5 @@
+{
+  "name": "test-project",
+  "version": "1.0.0",
+  "type": "module"
+}
diff --git a/packages/server/test/unit/cloud/api_spec.js b/packages/server/test/unit/cloud/api_spec.js
index 8bb4347075dd..e9bdfbf0cb17 100644
--- a/packages/server/test/unit/cloud/api_spec.js
+++ b/packages/server/test/unit/cloud/api_spec.js
@@ -1,6 +1,7 @@
 const crypto = require('crypto')
 const jose = require('jose')
 const base64Url = require('base64url')
+const stealthyRequire = require('stealthy-require')
 
 require('../../spec_helper')
 
@@ -19,6 +20,8 @@ const machineId = require('../../../lib/cloud/machine_id')
 const Promise = require('bluebird')
 
 const API_BASEURL = 'http://localhost:1234'
+const API_PROD_BASEURL = 'https://api.cypress.io'
+const API_PROD_PROXY_BASEURL = 'https://api-proxy.cypress.io'
 const CLOUD_BASEURL = 'http://localhost:3000'
 const AUTH_URLS = {
   'dashboardAuthUrl': 'http://localhost:3000/test-runner.html',
@@ -29,11 +32,12 @@ const makeError = (details = {}) => {
   return _.extend(new Error(details.message || 'Some error'), details)
 }
 
-const preflightNock = (encrypted = false) => {
+const encryptRequest = encryption.encryptRequest
+
+const decryptReqBodyAndRespond = ({ reqBody, resBody }, fn) => {
   const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', {
     modulusLength: 2048,
   })
-  const encryptRequest = encryption.encryptRequest
 
   /**
    * @type {crypto.KeyObject}
@@ -41,46 +45,64 @@ const preflightNock = (encrypted = false) => {
   let _secretKey
 
   sinon.stub(encryption, 'encryptRequest').callsFake(async (params) => {
+    if (reqBody) {
+      expect(params.body).to.deep.eq(reqBody)
+    }
+
     const { secretKey, jwe } = await encryptRequest(params, publicKey)
 
+    if (fn) {
+      encryption.encryptRequest.restore()
+    }
+
     _secretKey = secretKey
 
     return { secretKey, jwe }
   })
 
-  nock(API_BASEURL)
-  .defaultReplyHeaders({ 'x-cypress-encrypted': 'true' })
-  .matchHeader('x-route-version', '1')
-  .matchHeader('x-os-name', 'linux')
-  .matchHeader('x-cypress-version', pkg.version)
-  .post('/preflight', () => true)
-  .reply(200, async (uri, requestBody) => {
+  return async (uri, encReqBody) => {
     const decryptedSecretKey = crypto.createSecretKey(
       crypto.privateDecrypt(
         privateKey,
-        Buffer.from(base64Url.toBase64(requestBody.recipients[0].encrypted_key), 'base64'),
+        Buffer.from(base64Url.toBase64(encReqBody.recipients[0].encrypted_key), 'base64'),
       ),
     )
-    const decrypted = await encryption.decryptResponse(requestBody, privateKey)
 
     expect(_secretKey.export().toString('utf8')).to.eq(decryptedSecretKey.export().toString('utf8'))
 
     const enc = new jose.GeneralEncrypt(
-      Buffer.from(JSON.stringify({ encrypted, apiUrl: decrypted.apiUrl })),
+      Buffer.from(JSON.stringify(resBody)),
     )
 
     enc.setProtectedHeader({ alg: 'A256GCMKW', enc: 'A256GCM', zip: 'DEF' }).addRecipient(decryptedSecretKey)
 
     const jweResponse = await enc.encrypt()
 
+    fn && fn()
+
     return jweResponse
-  })
+  }
+}
+
+const preflightNock = (baseUrl) => {
+  return nock(baseUrl)
+  .matchHeader('x-route-version', '1')
+  .matchHeader('x-os-name', 'linux')
+  .matchHeader('x-cypress-version', pkg.version)
+  .post('/preflight')
 }
 
 describe('lib/cloud/api', () => {
   beforeEach(() => {
     api.setPreflightResult({ encrypt: false })
-    preflightNock(false)
+
+    preflightNock(API_BASEURL)
+    .reply(200, decryptReqBodyAndRespond({
+      resBody: {
+        encrypt: false,
+        apiUrl: `${API_BASEURL}/`,
+      },
+    }))
 
     nock(API_BASEURL)
     .matchHeader('x-route-version', '2')
@@ -196,17 +218,307 @@ describe('lib/cloud/api', () => {
     })
   })
 
-  context('.preflight', () => {
-    it('POST /preflight + returns encryption', function () {
+  context('.sendPreflight', () => {
+    let prodApi
+
+    beforeEach(function () {
+      this.timeout(30000)
+
       nock.cleanAll()
       sinon.restore()
-
       sinon.stub(os, 'platform').returns('linux')
-      preflightNock(true)
 
-      return api.preflight({ projectId: 'abc123' })
+      process.env.CYPRESS_CONFIG_ENV = 'production'
+      process.env.CYPRESS_API_URL = 'https://some.server.com'
+
+      if (!prodApi) {
+        prodApi = stealthyRequire(require.cache, () => {
+          return require('../../../lib/cloud/api')
+        }, () => {
+          require('../../../lib/cloud/encryption')
+        }, module)
+      }
+    })
+
+    it('POST /preflight to proxy. returns encryption', () => {
+      preflightNock(API_PROD_PROXY_BASEURL)
+      .reply(200, decryptReqBodyAndRespond({
+        reqBody: {
+          envUrl: 'https://some.server.com',
+          dependencies: {},
+          errors: [],
+          apiUrl: 'https://api.cypress.io/',
+          projectId: 'abc123',
+        },
+        resBody: {
+          encrypt: true,
+          apiUrl: `${API_PROD_BASEURL}/`,
+        },
+      }))
+
+      return prodApi.sendPreflight({ projectId: 'abc123' })
+      .then((ret) => {
+        expect(ret).to.deep.eq({ encrypt: true, apiUrl: `${API_PROD_BASEURL}/` })
+      })
+    })
+
+    it('POST /preflight to proxy, and then api on response status code failure. returns encryption', () => {
+      const scopeProxy = preflightNock(API_PROD_PROXY_BASEURL)
+      .reply(500)
+
+      const scopeApi = preflightNock(API_PROD_BASEURL)
+      .reply(200, decryptReqBodyAndRespond({
+        reqBody: {
+          envUrl: 'https://some.server.com',
+          dependencies: {},
+          errors: [],
+          apiUrl: 'https://api.cypress.io/',
+          projectId: 'abc123',
+        },
+        resBody: {
+          encrypt: true,
+          apiUrl: `${API_PROD_BASEURL}/`,
+        },
+      }))
+
+      return prodApi.sendPreflight({ projectId: 'abc123' })
       .then((ret) => {
-        expect(ret).to.deep.eq({ encrypted: true, apiUrl: `${API_BASEURL}/` })
+        scopeProxy.done()
+        scopeApi.done()
+        expect(ret).to.deep.eq({ encrypt: true, apiUrl: `${API_PROD_BASEURL}/` })
+      })
+    })
+
+    it('POST /preflight to proxy, and then api on network failure. returns encryption', () => {
+      const scopeProxy = preflightNock(API_PROD_PROXY_BASEURL)
+      .replyWithError('some request error')
+
+      const scopeApi = preflightNock(API_PROD_BASEURL)
+      .reply(200, decryptReqBodyAndRespond({
+        reqBody: {
+          envUrl: 'https://some.server.com',
+          dependencies: {},
+          errors: [],
+          apiUrl: 'https://api.cypress.io/',
+          projectId: 'abc123',
+        },
+        resBody: {
+          encrypt: true,
+          apiUrl: `${API_PROD_BASEURL}/`,
+        },
+      }))
+
+      return prodApi.sendPreflight({ projectId: 'abc123' })
+      .then((ret) => {
+        scopeProxy.done()
+        scopeApi.done()
+        expect(ret).to.deep.eq({ encrypt: true, apiUrl: `${API_PROD_BASEURL}/` })
+      })
+    })
+
+    it('sets timeout to 60 seconds', () => {
+      sinon.stub(api.rp, 'post').resolves({})
+
+      return api.sendPreflight({})
+      .then(() => {
+        expect(api.rp.post).to.be.calledWithMatch({ timeout: 60000 })
+      })
+    })
+
+    describe('errors', () => {
+      it('[F1] POST /preflight TimeoutError', () => {
+        preflightNock(API_BASEURL)
+        .times(2)
+        .delayConnection(5000)
+        .reply(200, {})
+
+        return api.sendPreflight({
+          timeout: 100,
+        })
+        .then(() => {
+          throw new Error('should have thrown here')
+        })
+        .catch((err) => {
+          expect(err.message).to.eq('Error: ESOCKETTIMEDOUT')
+        })
+      })
+
+      it('[F1] POST /preflight RequestError', () => {
+        const scopeProxy = preflightNock(API_PROD_PROXY_BASEURL)
+        .replyWithError('first request error')
+
+        const scopeApi = preflightNock(API_PROD_BASEURL)
+        .replyWithError('2nd request error')
+
+        return prodApi.sendPreflight({ projectId: 'abc123' })
+        .then(() => {
+          throw new Error('should have thrown here')
+        })
+        .catch((err) => {
+          scopeProxy.done()
+          scopeApi.done()
+
+          expect(err).not.to.have.property('statusCode')
+          expect(err).to.contain({
+            name: 'RequestError',
+            message: 'Error: 2nd request error',
+          })
+        })
+      })
+
+      it('[F1] POST /preflight statusCode >= 500', () => {
+        const scopeProxy = preflightNock(API_PROD_PROXY_BASEURL)
+        .reply(500)
+
+        const scopeApi = preflightNock(API_PROD_BASEURL)
+        .reply(500)
+
+        return prodApi.sendPreflight({ projectId: 'abc123' })
+        .then(() => {
+          throw new Error('should have thrown here')
+        })
+        .catch((err) => {
+          scopeProxy.done()
+          scopeApi.done()
+
+          expect(err).to.contain({
+            name: 'StatusCodeError',
+            statusCode: 500,
+          })
+        })
+      })
+
+      it('[F2] POST /preflight statusCode = 404', () => {
+        const scopeProxy = preflightNock(API_PROD_PROXY_BASEURL)
+        .reply(404)
+
+        const scopeApi = preflightNock(API_PROD_BASEURL)
+        .reply(404, '404 not found', {
+          'Content-Type': 'text/html',
+        })
+
+        return prodApi.sendPreflight({ projectId: 'abc123' })
+        .then(() => {
+          throw new Error('should have thrown here')
+        })
+        .catch((err) => {
+          scopeProxy.done()
+          scopeApi.done()
+
+          expect(err).to.contain({
+            name: 'StatusCodeError',
+            statusCode: 404,
+          })
+        })
+      })
+
+      it('[F3] POST /preflight statusCode = 422 but decrypt error', () => {
+        const scopeProxy = preflightNock(API_PROD_PROXY_BASEURL)
+        .reply(422, { data: 'very encrypted and secure string' })
+
+        const scopeApi = preflightNock(API_PROD_BASEURL)
+        .reply(422, { data: 'very encrypted and secure string' })
+
+        return prodApi.sendPreflight({ projectId: 'abc123' })
+        .then(() => {
+          throw new Error('should have thrown here')
+        })
+        .catch((err) => {
+          scopeProxy.done()
+          scopeApi.done()
+
+          expect(err).not.to.have.property('statusCode')
+          expect(err).to.contain({
+            name: 'DecryptionError',
+            message: 'JWE Recipients missing or incorrect type',
+          })
+        })
+      })
+
+      it('[F3] POST /preflight statusCode = 200 but decrypt error', () => {
+        const scopeProxy = preflightNock(API_PROD_PROXY_BASEURL)
+        .reply(200, { data: 'very encrypted and secure string' })
+
+        const scopeApi = preflightNock(API_PROD_BASEURL)
+        .reply(201, 'very encrypted and secure string')
+
+        return prodApi.sendPreflight({ projectId: 'abc123' })
+        .then(() => {
+          throw new Error('should have thrown here')
+        })
+        .catch((err) => {
+          scopeProxy.done()
+          scopeApi.done()
+
+          expect(err).not.to.have.property('statusCode')
+          expect(err).to.contain({
+            name: 'DecryptionError',
+            message: 'General JWE must be an object',
+          })
+        })
+      })
+
+      it('[F3] POST /preflight statusCode = 201 but no body', () => {
+        const scopeProxy = preflightNock(API_PROD_PROXY_BASEURL)
+        .reply(200)
+
+        const scopeApi = preflightNock(API_PROD_BASEURL)
+        .reply(201)
+
+        return prodApi.sendPreflight({ projectId: 'abc123' })
+        .then(() => {
+          throw new Error('should have thrown here')
+        })
+        .catch((err) => {
+          scopeProxy.done()
+          scopeApi.done()
+
+          expect(err).not.to.have.property('statusCode')
+          expect(err).to.contain({
+            name: 'DecryptionError',
+            message: 'General JWE must be an object',
+          })
+        })
+      })
+
+      it('[F4] POST /preflight statusCode = 412 valid decryption', () => {
+        const scopeProxy = preflightNock(API_PROD_PROXY_BASEURL)
+        .reply(412, decryptReqBodyAndRespond({
+          reqBody: {
+            envUrl: 'https://some.server.com',
+            dependencies: {},
+            errors: [],
+            apiUrl: 'https://api.cypress.io/',
+            projectId: 'abc123',
+          },
+          resBody: {
+            message: 'Recording is not working',
+            errors: [
+              'attempted to send invalid data',
+            ],
+            object: {
+              projectId: 'cy12345',
+            },
+          },
+        }))
+
+        const scopeApi = preflightNock(API_PROD_BASEURL)
+        .reply(200)
+
+        return prodApi.sendPreflight({ projectId: 'abc123' })
+        .then(() => {
+          throw new Error('should have thrown here')
+        })
+        .catch((err) => {
+          scopeProxy.done()
+          expect(scopeApi.isDone()).to.be.false
+
+          expect(err).to.contain({
+            name: 'StatusCodeError',
+            message: '412 - {"message":"Recording is not working","errors":["attempted to send invalid data"],"object":{"projectId":"cy12345"}}',
+            statusCode: 412,
+          })
+        })
       })
     })
   })
@@ -257,6 +569,38 @@ describe('lib/cloud/api', () => {
       })
     })
 
+    it('POST /runs + returns runId with encryption', function () {
+      nock.cleanAll()
+      sinon.restore()
+      sinon.stub(os, 'platform').returns('linux')
+
+      preflightNock(API_BASEURL)
+      .reply(200, decryptReqBodyAndRespond({
+        resBody: {
+          encrypt: true,
+          apiUrl: `${API_BASEURL}/`,
+        },
+      }, () => {
+        nock(API_BASEURL)
+        .defaultReplyHeaders({ 'x-cypress-encrypted': 'true' })
+        .matchHeader('x-route-version', '4')
+        .matchHeader('x-os-name', 'linux')
+        .matchHeader('x-cypress-version', pkg.version)
+        .post('/runs')
+        .reply(200, decryptReqBodyAndRespond({
+          reqBody: this.buildProps,
+          resBody: {
+            runId: 'new-run-id-123',
+          },
+        }))
+      }))
+
+      return api.createRun(this.buildProps)
+      .then((ret) => {
+        expect(ret).to.deep.eq({ runId: 'new-run-id-123' })
+      })
+    })
+
     it('POST /runs failure formatting', function () {
       nock(API_BASEURL)
       .matchHeader('x-route-version', '4')
@@ -330,6 +674,20 @@ describe('lib/cloud/api', () => {
         expect(err.isApiError).to.be.true
       })
     })
+
+    it('tags errors on /preflight', function () {
+      preflightNock(API_BASEURL)
+      .times(2)
+      .reply(500, {})
+
+      return api.createRun({})
+      .then(() => {
+        throw new Error('should have thrown here')
+      })
+      .catch((err) => {
+        expect(err.isApiError).to.be.true
+      })
+    })
   })
 
   context('.createInstance', () => {
@@ -952,13 +1310,16 @@ describe('lib/cloud/api', () => {
       return api.retryWithBackoff(fn1)
       .then(() => {
         throw new Error('Should not resolve 499 error')
-      }).catch((err) => {
+      })
+      .catch((err) => {
         expect(err.message).to.equal('499 error')
 
         return api.retryWithBackoff(fn2)
-      }).then(() => {
+      })
+      .then(() => {
         throw new Error('Should not resolve 600 error')
-      }).catch((err) => {
+      })
+      .catch((err) => {
         expect(err.message).to.equal('600 error')
       })
     })
@@ -1000,19 +1361,19 @@ describe('lib/cloud/api', () => {
         expect(errors.warning).to.be.calledThrice
         expect(errors.warning.firstCall.args[0]).to.eql('CLOUD_API_RESPONSE_FAILED_RETRYING')
         expect(errors.warning.firstCall.args[1]).to.eql({
-          delay: 30000,
+          delayMs: 30000,
           tries: 3,
           response: err,
         })
 
         expect(errors.warning.secondCall.args[1]).to.eql({
-          delay: 60000,
+          delayMs: 60000,
           tries: 2,
           response: err,
         })
 
         expect(errors.warning.thirdCall.args[1]).to.eql({
-          delay: 120000,
+          delayMs: 120000,
           tries: 1,
           response: err,
         })
diff --git a/packages/server/test/unit/cloud/environment_spec.ts b/packages/server/test/unit/cloud/environment_spec.ts
new file mode 100644
index 000000000000..302f36ce8c71
--- /dev/null
+++ b/packages/server/test/unit/cloud/environment_spec.ts
@@ -0,0 +1,182 @@
+import '../../spec_helper'
+import getEnvInformationForProjectRoot from '../../../lib/cloud/environment'
+import path from 'path'
+import base64url from 'base64url'
+import { exec } from 'child_process'
+import originalResolvePackagePath from 'resolve-package-path'
+import proxyquire from 'proxyquire'
+
+describe('lib/cloud/api', () => {
+  beforeEach(() => {
+    delete process.env.CYPRESS_API_URL
+    process.env.CYPRESS_ENV_DEPENDENCIES = base64url.encode(JSON.stringify({
+      maybeCheckProcessTreeIfPresent: ['foo'],
+      neverCheckProcessTreeIfPresent: ['bar'],
+    }))
+  })
+
+  let proc
+  const spawnProcessTree = async ({
+    grandParentUrl,
+    parentUrl,
+    url,
+  }: {
+    grandParentUrl?: string
+    parentUrl?: string
+    url?: string
+  }) => {
+    return new Promise((resolve) => {
+      proc = exec(`node ${path.join(__dirname, '..', '..', 'support', 'fixtures', 'cloud', 'environment', 'test-project', 'index.js')}`, {
+        cwd: path.join(__dirname, '..', '..', 'support', 'fixtures', 'cloud', 'environment', 'test-project'),
+        env: {
+          ...process.env,
+          CYPRESS_API_URL: grandParentUrl,
+          CHILD_CYPRESS_API_URL: parentUrl,
+          GRANDCHILD_CYPRESS_API_URL: url,
+        },
+      })
+
+      proc.stdout.on('data', (data) => {
+        const match = data.toString().match(/grandchild (\d+)/)
+
+        if (match) {
+          resolve(match[1])
+        }
+      })
+    })
+  }
+
+  afterEach(() => {
+    if (proc) {
+      proc.kill()
+    }
+  })
+
+  it('should be able to get the environment for: present CYPRESS_API_URL and all tracked dependencies', async () => {
+    process.env.CYPRESS_API_URL = 'https://example.com'
+
+    const information = await getEnvInformationForProjectRoot(path.join(__dirname, '..', '..', 'support', 'fixtures', 'cloud', 'environment', 'all-tracked-dependencies'), process.pid.toString())
+
+    expect(information).to.deep.eq({
+      envUrl: 'https://example.com',
+      dependencies: { bar: { version: '2.0.0' }, foo: { version: '1.0.0' } },
+      errors: [],
+    })
+  })
+
+  it('should be able to get the environment for: present CYPRESS_API_URL and a thrown error when tracking dependencies', async () => {
+    process.env.CYPRESS_API_URL = 'https://example.com'
+
+    const resolvePackagePath = sinon.stub()
+
+    resolvePackagePath.withArgs('foo', sinon.match.any).throws(new Error('some error'))
+    resolvePackagePath.withArgs('bar', sinon.match.any).callsFake(originalResolvePackagePath)
+    const { default: getEnvInfo } = proxyquire('../../../lib/cloud/environment', {
+      'resolve-package-path': resolvePackagePath,
+    })
+
+    const { errors, ...information } = await getEnvInfo(path.join(__dirname, '..', '..', 'support', 'fixtures', 'cloud', 'environment', 'all-tracked-dependencies'), process.pid.toString())
+
+    expect(information).to.deep.eq({
+      envUrl: 'https://example.com',
+      dependencies: { bar: { version: '2.0.0' } },
+    })
+
+    expect(errors).to.have.length(1)
+    expect(errors[0].dependency).to.equal('foo')
+    expect(errors[0].message).to.equal('some error')
+    expect(errors[0].name).to.equal('Error')
+    expect(errors[0].stack).to.include('Error: some error')
+  })
+
+  it('should be able to get the environment for: absent CYPRESS_API_URL and all tracked dependencies', async () => {
+    const information = await getEnvInformationForProjectRoot(path.join(__dirname, '..', '..', 'support', 'fixtures', 'cloud', 'environment', 'all-tracked-dependencies'), process.pid.toString())
+
+    expect(information).to.deep.eq({
+      envUrl: undefined,
+      dependencies: { bar: { version: '2.0.0' }, foo: { version: '1.0.0' } },
+      errors: [],
+    })
+  })
+
+  it('should be able to get the environment for: absent CYPRESS_API_URL and partial dependencies not matching criteria', async () => {
+    const information = await getEnvInformationForProjectRoot(path.join(__dirname, '..', '..', 'support', 'fixtures', 'cloud', 'environment', 'partial-dependencies-not-matching'), process.pid.toString())
+
+    expect(information).to.deep.eq({
+      envUrl: undefined,
+      dependencies: { bar: { version: '2.0.0' } },
+      errors: [],
+    })
+  })
+
+  context('absent CYPRESS_API_URL and partial dependencies matching criteria', () => {
+    it('should be able to get the environment for CYPRESS_API_URL defined in grandparent process', async () => {
+      const pid = await spawnProcessTree({
+        grandParentUrl: 'https://grandparent.com',
+      })
+
+      const information = await getEnvInformationForProjectRoot(path.join(__dirname, '..', '..', 'support', 'fixtures', 'cloud', 'environment', 'partial-dependencies-matching'), pid.toString())
+
+      expect(information).to.deep.eq({
+        envUrl: process.platform !== 'win32' ? 'https://grandparent.com' : undefined,
+        dependencies: { foo: { version: '1.0.0' } },
+        errors: [],
+      })
+    })
+
+    it('should be able to get the environment for CYPRESS_API_URL defined in parent process', async () => {
+      const pid = await spawnProcessTree({
+        parentUrl: 'https://parent.com',
+      })
+
+      const information = await getEnvInformationForProjectRoot(path.join(__dirname, '..', '..', 'support', 'fixtures', 'cloud', 'environment', 'partial-dependencies-matching'), pid.toString())
+
+      expect(information).to.deep.eq({
+        envUrl: process.platform !== 'win32' ? 'https://parent.com' : undefined,
+        dependencies: { foo: { version: '1.0.0' } },
+        errors: [],
+      })
+    })
+
+    it('should be able to get the environment for CYPRESS_API_URL defined in current process', async () => {
+      const pid = await spawnProcessTree({
+        url: 'https://url.com',
+      })
+
+      const information = await getEnvInformationForProjectRoot(path.join(__dirname, '..', '..', 'support', 'fixtures', 'cloud', 'environment', 'partial-dependencies-matching'), pid.toString())
+
+      expect(information).to.deep.eq({
+        envUrl: process.platform !== 'win32' ? 'https://url.com' : undefined,
+        dependencies: { foo: { version: '1.0.0' } },
+        errors: [],
+      })
+    })
+
+    it('should be able to get the environment for CYPRESS_API_URL defined in parent process overriding grandparent process', async () => {
+      const pid = await spawnProcessTree({
+        grandParentUrl: 'https://grandparent.com',
+        parentUrl: 'https://parent.com',
+      })
+
+      const information = await getEnvInformationForProjectRoot(path.join(__dirname, '..', '..', 'support', 'fixtures', 'cloud', 'environment', 'partial-dependencies-matching'), pid.toString())
+
+      expect(information).to.deep.eq({
+        envUrl: process.platform !== 'win32' ? 'https://parent.com' : undefined,
+        dependencies: { foo: { version: '1.0.0' } },
+        errors: [],
+      })
+    })
+
+    it('should return no envUrl when CYPRESS_API_URL is not defined in any parent process', async () => {
+      const pid = await spawnProcessTree({})
+
+      const information = await getEnvInformationForProjectRoot(path.join(__dirname, '..', '..', 'support', 'fixtures', 'cloud', 'environment', 'partial-dependencies-matching'), pid.toString())
+
+      expect(information).to.deep.eq({
+        envUrl: undefined,
+        dependencies: { foo: { version: '1.0.0' } },
+        errors: [],
+      })
+    })
+  })
+})
diff --git a/packages/server/test/unit/modes/record_spec.js b/packages/server/test/unit/modes/record_spec.js
index 9f8b040bfb4e..dd08db98a9a5 100644
--- a/packages/server/test/unit/modes/record_spec.js
+++ b/packages/server/test/unit/modes/record_spec.js
@@ -17,7 +17,7 @@ const initialEnv = _.clone(process.env)
 // tested as an e2e/record_spec
 describe('lib/modes/record', () => {
   beforeEach(() => {
-    sinon.stub(api, 'preflight').callsFake(async () => {
+    sinon.stub(api, 'sendPreflight').callsFake(async () => {
       api.setPreflightResult({ encrypt: false })
     })
   })
@@ -306,6 +306,7 @@ describe('lib/modes/record', () => {
           expect(commitInfo.commitInfo).to.be.calledWith(projectRoot)
 
           expect(api.createRun).to.be.calledWith({
+            projectRoot,
             group,
             parallel,
             projectId,
diff --git a/scripts/after-pack-hook.js b/scripts/after-pack-hook.js
index cba2426c8f17..fa28fe6d289e 100644
--- a/scripts/after-pack-hook.js
+++ b/scripts/after-pack-hook.js
@@ -7,7 +7,9 @@ const path = require('path')
 const { setupV8Snapshots } = require('@tooling/v8-snapshot')
 const { flipFuses, FuseVersion, FuseV1Options } = require('@electron/fuses')
 const { buildEntryPointAndCleanup } = require('./binary/binary-cleanup')
-const { getIntegrityCheckSource, getBinaryEntryPointSource } = require('./binary/binary-sources')
+const { getIntegrityCheckSource, getBinaryEntryPointSource, getEncryptionFileSource, getCloudApiFileSource, validateEncryptionFile } = require('./binary/binary-sources')
+
+const CY_ROOT_DIR = path.join(__dirname, '..')
 
 module.exports = async function (params) {
   try {
@@ -58,23 +60,21 @@ module.exports = async function (params) {
 
     if (!['1', 'true'].includes(process.env.DISABLE_SNAPSHOT_REQUIRE)) {
       const binaryEntryPointSource = await getBinaryEntryPointSource()
-      const encryptionFile = path.join(outputFolder, 'packages/server/lib/cloud/encryption.js')
-      const fileContents = await fs.readFile(encryptionFile, 'utf8')
-
-      if (!fileContents.includes(`test: CY_TEST,`)) {
-        throw new Error(`Expected to find test key in cloud encryption file`)
-      }
+      const encryptionFilePath = path.join(CY_ROOT_DIR, 'packages/server/lib/cloud/encryption.ts')
+      const encryptionFileSource = await getEncryptionFileSource(encryptionFilePath)
+      const cloudApiFilePath = path.join(CY_ROOT_DIR, 'packages/server/lib/cloud/environment.ts')
+      const cloudApiFileSource = await getCloudApiFileSource(cloudApiFilePath)
 
       await Promise.all([
-        fs.writeFile(encryptionFile, fileContents.replace(`test: CY_TEST,`, '').replace(/const CY_TEST = `(.*?)`/, '')),
+        fs.writeFile(encryptionFilePath, encryptionFileSource),
+        fs.writeFile(cloudApiFilePath, cloudApiFileSource),
         fs.writeFile(path.join(outputFolder, 'index.js'), binaryEntryPointSource),
       ])
 
-      const afterReplace = await fs.readFile(encryptionFile, 'utf8')
-
-      if (afterReplace.includes('CY_TEST')) {
-        throw new Error(`Expected test key to be stripped from cloud encryption file`)
-      }
+      await Promise.all([
+        validateEncryptionFile(encryptionFilePath),
+        validateEncryptionFile(cloudApiFilePath),
+      ])
 
       await flipFuses(
         exePathPerPlatform[os.platform()],
diff --git a/scripts/binary/binary-sources.js b/scripts/binary/binary-sources.js
index e754ca050922..dcf3ff1fd302 100644
--- a/scripts/binary/binary-sources.js
+++ b/scripts/binary/binary-sources.js
@@ -1,4 +1,4 @@
-const fs = require('fs')
+const fs = require('fs-extra')
 const crypto = require('crypto')
 const path = require('path')
 const esbuild = require('esbuild')
@@ -40,7 +40,53 @@ const getIntegrityCheckSource = (baseDirectory) => {
   .replaceAll('CRYPTO_HMAC_DIGEST_TO_STRING', escapeString(crypto.Hmac.prototype.digest.toString()))
 }
 
+const getEncryptionFileSource = async (encryptionFilePath) => {
+  const fileContents = await fs.readFile(encryptionFilePath, 'utf8')
+
+  if (!fileContents.includes(`test: CY_TEST,`)) {
+    throw new Error(`Expected to find test key in cloud encryption file`)
+  }
+
+  return fileContents.replace(`test: CY_TEST,`, '').replace(/const CY_TEST = `(.*?)`/, '')
+}
+
+const validateEncryptionFile = async (encryptionFilePath) => {
+  const afterReplaceEncryption = await fs.readFile(encryptionFilePath, 'utf8')
+
+  if (afterReplaceEncryption.includes('CY_TEST')) {
+    throw new Error(`Expected test key to be stripped from cloud encryption file`)
+  }
+}
+
+const getCloudApiFileSource = async (cloudApiFilePath) => {
+  const fileContents = await fs.readFile(cloudApiFilePath, 'utf8')
+
+  if (!fileContents.includes('process.env.CYPRESS_ENV_DEPENDENCIES')) {
+    throw new Error(`Expected to find CYPRESS_ENV_DEPENDENCIES in cloud api file`)
+  }
+
+  if (process.env.CYPRESS_ENV_DEPENDENCIES) {
+    return fileContents.replace('process.env.CYPRESS_ENV_DEPENDENCIES', `'${process.env.CYPRESS_ENV_DEPENDENCIES}'`)
+  }
+
+  return fileContents
+}
+
+const validateCloudApiFile = async (cloudApiFilePath) => {
+  if (process.env.CYPRESS_ENV_DEPENDENCIES) {
+    const afterReplaceCloudApi = await fs.readFile(cloudApiFilePath, 'utf8')
+
+    if (afterReplaceCloudApi.includes('process.env.CYPRESS_ENV_DEPENDENCIES')) {
+      throw new Error(`Expected process.env.CYPRESS_ENV_DEPENDENCIES to be stripped from cloud api file`)
+    }
+  }
+}
+
 module.exports = {
   getBinaryEntryPointSource,
   getIntegrityCheckSource,
+  getEncryptionFileSource,
+  getCloudApiFileSource,
+  validateCloudApiFile,
+  validateEncryptionFile,
 }
diff --git a/system-tests/__snapshots__/record_spec.js b/system-tests/__snapshots__/record_spec.js
index 0571033efc59..b2ee918e72a1 100644
--- a/system-tests/__snapshots__/record_spec.js
+++ b/system-tests/__snapshots__/record_spec.js
@@ -271,9 +271,9 @@ Please log into Cypress Cloud and find your project.
 
 We will list the correct projectId in the 'Settings' tab.
 
-Alternatively, you can create a new project using the Desktop Application.
+Alternatively, you can create a new project directly from within the Cypress app.
 
-https://on.cypress.io/dashboard
+https://on.cypress.io/cloud
 
 `
 
@@ -332,11 +332,11 @@ exports['e2e record api interaction errors update instance stdout warns but proc
   (Uploading Results)
 
   - Done Uploading (1/1) /foo/bar/.projects/e2e/cypress/screenshots/record_pass.cy.js/yay it passes.png
-Warning: We encountered an error talking to our servers.
+Warning: We encountered an error communicating with our servers.
 
-This run will not be recorded.
+This run will proceed, but will not be recorded.
 
-This error will not alter the exit code.
+This error will not affect or change the exit code.
 
 StatusCodeError: 500 - "Internal Server Error"
 
@@ -498,7 +498,7 @@ The Record Key is missing. Your CI provider is likely not passing private enviro
 
 These results will not be recorded.
 
-This error will not alter the exit code.
+This error will not affect or change the exit code.
 
 ====================================================================================================
 
@@ -574,16 +574,7 @@ https://on.cypress.io/run-group-name-not-unique
 `
 
 exports['e2e record api interaction errors create run unknown 422 errors and exits when there is an unknown 422 response 1'] = `
-We encountered an unexpected error talking to our servers.
-
-There is likely something wrong with the request.
-
-The --tag flag you passed was: nightly
-The --group flag you passed was: e2e-tests
-The --parallel flag you passed was: true
-The --ciBuildId flag you passed was: ciBuildId123
-
-The server's response was:
+We encountered an unexpected error communicating with our servers.
 
 StatusCodeError: 422
 
@@ -592,20 +583,25 @@ StatusCodeError: 422
   "message": "An unknown message here from the server."
 }
 
+There is likely something wrong with the request.
+
+The --tag flag you passed was: nightly
+The --group flag you passed was: e2e-tests
+The --parallel flag you passed was: true
+The --ciBuildId flag you passed was: ciBuildId123
+
 `
 
 exports['e2e record api interaction errors create run 500 does not proceed and exits with error when parallelizing 1'] = `
-We encountered an unexpected error talking to our servers.
+We encountered an unexpected error communicating with our servers.
+
+StatusCodeError: 500 - "Internal Server Error"
 
 Because you passed the --parallel flag, this run cannot proceed because it requires a valid response from our servers.
 
 The --group flag you passed was: foo
 The --ciBuildId flag you passed was: ciBuildId123
 
-The server's response was:
-
-StatusCodeError: 500 - "Internal Server Error"
-
 `
 
 exports['e2e record api interaction errors create instance 500 does not proceed and exits with error when parallelizing and creating instance 1'] = `
@@ -623,17 +619,15 @@ exports['e2e record api interaction errors create instance 500 does not proceed
   │ Run URL:    https://dashboard.cypress.io/projects/cjvoj7/runs/12                               │
   └────────────────────────────────────────────────────────────────────────────────────────────────┘
 
-We encountered an unexpected error talking to our servers.
+We encountered an unexpected error communicating with our servers.
+
+StatusCodeError: 500 - "Internal Server Error"
 
 Because you passed the --parallel flag, this run cannot proceed because it requires a valid response from our servers.
 
 The --group flag you passed was: foo
 The --ciBuildId flag you passed was: ciBuildId123
 
-The server's response was:
-
-StatusCodeError: 500 - "Internal Server Error"
-
 `
 
 exports['e2e record api interaction errors update instance 500 does not proceed and exits with error when parallelizing and updating instance 1'] = `
@@ -690,41 +684,36 @@ exports['e2e record api interaction errors update instance 500 does not proceed
 
   (Uploading Results)
 
-We encountered an unexpected error talking to our servers.
+We encountered an unexpected error communicating with our servers.
+
+StatusCodeError: 500 - "Internal Server Error"
 
 Because you passed the --parallel flag, this run cannot proceed because it requires a valid response from our servers.
 
 The --group flag you passed was: foo
 The --ciBuildId flag you passed was: ciBuildId123
 
-The server's response was:
-
-StatusCodeError: 500 - "Internal Server Error"
-
 `
 
 exports['e2e record api interaction errors api retries on error warns and does not create or update instances 1'] = `
-We encountered an unexpected error talking to our servers.
+We encountered an unexpected error communicating with our servers.
+
+StatusCodeError: 500 - "Internal Server Error"
 
 We will retry 3 more times in X second(s)...
 
-The server's response was:
+We encountered an unexpected error communicating with our servers.
 
 StatusCodeError: 500 - "Internal Server Error"
-We encountered an unexpected error talking to our servers.
 
 We will retry 2 more times in X second(s)...
 
-The server's response was:
+We encountered an unexpected error communicating with our servers.
 
 StatusCodeError: 500 - "Internal Server Error"
-We encountered an unexpected error talking to our servers.
 
 We will retry 1 more time in X second(s)...
 
-The server's response was:
-
-StatusCodeError: 500 - "Internal Server Error"
 
 ====================================================================================================
 
@@ -739,13 +728,12 @@ StatusCodeError: 500 - "Internal Server Error"
   │ Run URL:    https://dashboard.cypress.io/projects/cjvoj7/runs/12                               │
   └────────────────────────────────────────────────────────────────────────────────────────────────┘
 
-We encountered an unexpected error talking to our servers.
+We encountered an unexpected error communicating with our servers.
 
-We will retry 3 more times in X second(s)...
+StatusCodeError: 500 - "Internal Server Error"
 
-The server's response was:
+We will retry 3 more times in X second(s)...
 
-StatusCodeError: 500 - "Internal Server Error"
 
 ────────────────────────────────────────────────────────────────────────────────────────────────────
                                                                                                     
@@ -813,7 +801,7 @@ The Record Key is missing. Your CI provider is likely not passing private enviro
 
 These results will not be recorded.
 
-This error will not alter the exit code.
+This error will not affect or change the exit code.
 
 ====================================================================================================
 
@@ -894,13 +882,7 @@ https://on.cypress.io/dashboard/organizations/org-id-1234/billing
 `
 
 exports['e2e record api interaction errors create run 402 - unknown error errors and exits when there\'s an unknown 402 error 1'] = `
-We encountered an unexpected error talking to our servers.
-
-There is likely something wrong with the request.
-
-The --tag flag you passed was: 
-
-The server's response was:
+We encountered an unexpected error communicating with our servers.
 
 StatusCodeError: 402
 
@@ -908,6 +890,10 @@ StatusCodeError: 402
   "error": "Something went wrong"
 }
 
+There is likely something wrong with the request.
+
+The --tag flag you passed was: 
+
 `
 
 exports['e2e record api interaction errors create run 402 - free plan exceeds monthly tests errors and exits when on free plan and over recorded tests limit 1'] = `
@@ -1832,24 +1818,20 @@ https://on.cypress.io/dashboard/organizations/org-id-1234/billing
 `
 
 exports['e2e record api interaction errors create run 500 errors and exits 1'] = `
-We encountered an unexpected error talking to our servers.
-
-The server's response was:
+We encountered an unexpected error communicating with our servers.
 
 StatusCodeError: 500 - "Internal Server Error"
 
 `
 
 exports['e2e record api interaction errors create run 500 when grouping without parallelization errors and exits 1'] = `
-We encountered an unexpected error talking to our servers.
+We encountered an unexpected error communicating with our servers.
+
+StatusCodeError: 500 - "Internal Server Error"
 
 The --group flag you passed was: foo
 The --ciBuildId flag you passed was: ciBuildId123
 
-The server's response was:
-
-StatusCodeError: 500 - "Internal Server Error"
-
 `
 
 exports['e2e record api interaction errors create instance 500 without parallelization - does not proceed 1'] = `
@@ -1867,9 +1849,7 @@ exports['e2e record api interaction errors create instance 500 without paralleli
   │ Run URL:    https://dashboard.cypress.io/projects/cjvoj7/runs/12                               │
   └────────────────────────────────────────────────────────────────────────────────────────────────┘
 
-We encountered an unexpected error talking to our servers.
-
-The server's response was:
+We encountered an unexpected error communicating with our servers.
 
 StatusCodeError: 500 - "Internal Server Error"
 
@@ -1890,9 +1870,7 @@ exports['e2e record api interaction errors create instance errors and exits on c
   │ Run URL:    https://dashboard.cypress.io/projects/cjvoj7/runs/12                               │
   └────────────────────────────────────────────────────────────────────────────────────────────────┘
 
-We encountered an unexpected error talking to our servers.
-
-The server's response was:
+We encountered an unexpected error communicating with our servers.
 
 StatusCodeError: 500 - "Internal Server Error"
 
@@ -1918,15 +1896,13 @@ exports['e2e record api interaction errors postInstanceTests without paralleliza
                                                                                                     
   Running:  a_record.cy.js                                                                  (1 of 2)
   Estimated: X second(s)
-We encountered an unexpected error talking to our servers.
+We encountered an unexpected error communicating with our servers.
+
+StatusCodeError: 500 - "Internal Server Error"
 
 The --group flag you passed was: foo
 The --ciBuildId flag you passed was: 1
 
-The server's response was:
-
-StatusCodeError: 500 - "Internal Server Error"
-
 `
 
 exports['e2e record api interaction errors postInstanceTests with parallelization errors and exits 1'] = `
@@ -1949,17 +1925,15 @@ exports['e2e record api interaction errors postInstanceTests with parallelizatio
                                                                                                     
   Running:  a_record.cy.js                                                                  (1 of 2)
   Estimated: X second(s)
-We encountered an unexpected error talking to our servers.
+We encountered an unexpected error communicating with our servers.
+
+StatusCodeError: 500 - "Internal Server Error"
 
 Because you passed the --parallel flag, this run cannot proceed because it requires a valid response from our servers.
 
 The --group flag you passed was: foo
 The --ciBuildId flag you passed was: ciBuildId123
 
-The server's response was:
-
-StatusCodeError: 500 - "Internal Server Error"
-
 `
 
 exports['e2e record api interaction errors postInstanceResults errors and exits in serial 1'] = `
@@ -2016,9 +1990,7 @@ exports['e2e record api interaction errors postInstanceResults errors and exits
 
   (Uploading Results)
 
-We encountered an unexpected error talking to our servers.
-
-The server's response was:
+We encountered an unexpected error communicating with our servers.
 
 StatusCodeError: 500 - "Internal Server Error"
 
@@ -2292,7 +2264,7 @@ exports['e2e record quiet mode respects quiet mode 1'] = `
 `
 
 exports['e2e record api interaction errors create run 412 errors and exits when request schema is invalid 1'] = `
-Recording this run failed because the request was invalid.
+Recording this run failed. The request was invalid.
 
 request should follow postRunRequest@2.0.0 schema
 
@@ -2572,41 +2544,98 @@ Available browsers found on your system are:
 - browser3
 `
 
-exports['e2e record /preflight preflight failure renders error messages properly 1'] = `
-Recording this run failed because the request was invalid.
+exports['e2e record api interaction errors sendPreflight [F1] 500 status code errors with empty body fails after retrying 1'] = `
+We encountered an unexpected error communicating with our servers.
 
-Recording this way is no longer supported
+StatusCodeError: 500 - "Internal Server Error"
 
-Errors:
+We will retry 1 more time in X second(s)...
 
-[
-  "attempted to send envUrl foo.bar.baz"
-]
+We encountered an unexpected error communicating with our servers.
 
-Request Sent:
+StatusCodeError: 500 - "Internal Server Error"
 
-{
-  "ciBuildId": "ciBuildId123",
-  "projectId": "cy12345"
-}
+Because you passed the --parallel flag, this run cannot proceed because it requires a valid response from our servers.
+
+The --group flag you passed was: foo
+The --ciBuildId flag you passed was: ciBuildId123
 
 `
 
-exports['e2e record /preflight preflight failure: unencrypted fails on an unencrypted preflight response 1'] = `
-We encountered an unexpected error talking to our servers.
+exports['e2e record api interaction errors sendPreflight [F2] 404 status code with JSON body fails without retrying 1'] = `
+We could not find a Cypress Cloud project with the projectId: pid123
+
+This projectId came from your cypress-with-project-id.config.js file or an environment variable.
+
+Please log into Cypress Cloud and find your project.
+
+We will list the correct projectId in the 'Settings' tab.
+
+Alternatively, you can create a new project directly from within the Cypress app.
+
+https://on.cypress.io/cloud
+
+`
+
+exports['e2e record api interaction errors sendPreflight [F2] 404 status code with empty body fails without retrying 1'] = `
+We could not find a Cypress Cloud project with the projectId: pid123
+
+This projectId came from your cypress-with-project-id.config.js file or an environment variable.
+
+Please log into Cypress Cloud and find your project.
+
+We will list the correct projectId in the 'Settings' tab.
+
+Alternatively, you can create a new project directly from within the Cypress app.
+
+https://on.cypress.io/cloud
+
+`
+
+exports['e2e record api interaction errors sendPreflight [F3] 201 status code with invalid decryption fails without retrying 1'] = `
+We encountered an unexpected error communicating with our servers.
+
+DecryptionError: JWE Recipients missing or incorrect type
 
 Because you passed the --parallel flag, this run cannot proceed because it requires a valid response from our servers.
 
 The --group flag you passed was: foo
 The --ciBuildId flag you passed was: ciBuildId123
 
-The server's response was:
+`
 
-DecryptionError: JWE Recipients missing or incorrect type
+exports['e2e record api interaction errors sendPreflight [F3] 200 status code with empty body fails without retrying 1'] = `
+We encountered an unexpected error communicating with our servers.
+
+DecryptionError: General JWE must be an object
+
+Because you passed the --parallel flag, this run cannot proceed because it requires a valid response from our servers.
+
+The --group flag you passed was: foo
+The --ciBuildId flag you passed was: ciBuildId123
+
+`
+
+exports['e2e record api interaction errors sendPreflight [F4] 412 status code with valid decryption fails without retrying 1'] = `
+Recording this run failed. The request was invalid.
+
+Recording is not working
+
+Errors:
+
+[
+  "attempted to send invalid data"
+]
+
+Request Sent:
+
+{
+  "projectId": "cy12345"
+}
 
 `
 
-exports['e2e record /preflight preflight failure: warning message renders preflight warning messages prior to run warnings 1'] = `
+exports['e2e record api interaction errors sendPreflight [W1] warning message renders preflight warning messages prior to run warnings 1'] = `
 Warning from Cypress Cloud: 
 
 ----------------------------------------------------------------------
@@ -2691,3 +2720,64 @@ https://on.cypress.io/dashboard/organizations/org-id-1234/billing
 
 
 `
+
+exports['e2e record api interaction errors sendPreflight [F1] socket errors fails after retrying 1'] = `
+We encountered an unexpected error communicating with our servers.
+
+RequestError: Error: socket hang up
+
+We will retry 1 more time in X second(s)...
+
+We encountered an unexpected error communicating with our servers.
+
+RequestError: Error: socket hang up
+
+Because you passed the --parallel flag, this run cannot proceed because it requires a valid response from our servers.
+
+The --group flag you passed was: foo
+The --ciBuildId flag you passed was: ciBuildId123
+
+`
+
+exports['e2e record api interaction errors sendPreflight [F3] 422 status code with invalid decryption fails without retrying 1'] = `
+We encountered an unexpected error communicating with our servers.
+
+DecryptionError: JWE Recipients missing or incorrect type
+
+Because you passed the --parallel flag, this run cannot proceed because it requires a valid response from our servers.
+
+The --group flag you passed was: foo
+The --ciBuildId flag you passed was: ciBuildId123
+
+`
+
+exports['e2e record api interaction errors sendPreflight [F1] 500 status code errors with body fails after retrying 1'] = `
+We encountered an unexpected error communicating with our servers.
+
+StatusCodeError: 500 - "Internal Server Error"
+
+We will retry 1 more time in X second(s)...
+
+We encountered an unexpected error communicating with our servers.
+
+StatusCodeError: 500 - "Internal Server Error"
+
+Because you passed the --parallel flag, this run cannot proceed because it requires a valid response from our servers.
+
+The --group flag you passed was: foo
+The --ciBuildId flag you passed was: ciBuildId123
+
+`
+
+exports['e2e record api interaction errors sendPreflight [F5] 422 status code with valid decryption on createRun errors and exits when group name is in use 1'] = `
+You passed the --group flag, but this group name has already been used for this run.
+
+The existing run is: https://cloud.cypress.io/runs/12345
+
+The --group flag you passed was: e2e-tests
+
+If you are trying to parallelize this run, then also pass the --parallel flag, else pass a different group name.
+
+https://on.cypress.io/run-group-name-not-unique
+
+`
diff --git a/system-tests/__snapshots__/web_security_spec.js b/system-tests/__snapshots__/web_security_spec.js
index 80b65be410f2..4aacbbcd74f9 100644
--- a/system-tests/__snapshots__/web_security_spec.js
+++ b/system-tests/__snapshots__/web_security_spec.js
@@ -213,7 +213,7 @@ This option will not have an effect in Firefox. Tests that rely on web security
 
 Warning: We failed processing this video.
 
-This error will not alter the exit code.
+This error will not affect or change the exit code.
 
 TimeoutError: operation timed out
       [stack trace lines]
diff --git a/system-tests/lib/serverStub.ts b/system-tests/lib/serverStub.ts
index bad85e367d3d..8d333e6b4a96 100644
--- a/system-tests/lib/serverStub.ts
+++ b/system-tests/lib/serverStub.ts
@@ -70,7 +70,7 @@ export const encryptBody = async (req, res, body) => {
 }
 
 export const routeHandlers = {
-  preflight: {
+  sendPreflight: {
     method: 'post',
     url: '/preflight',
     res: async (req, res) => {
diff --git a/system-tests/lib/system-tests.ts b/system-tests/lib/system-tests.ts
index fa5ae7162b34..ada6c36d09d6 100644
--- a/system-tests/lib/system-tests.ts
+++ b/system-tests/lib/system-tests.ts
@@ -308,7 +308,7 @@ Bluebird.config({
 const diffRe = /Difference\n-{10}\n([\s\S]*)\n-{19}\nSaved snapshot text/m
 const expectedAddedVideoSnapshotLines = [
   'Warning: We failed processing this video.',
-  'This error will not alter the exit code.',
+  'This error will not affect or change the exit code.',
   'TimeoutError: operation timed out',
   '[stack trace lines]',
 ]
diff --git a/system-tests/test/record_spec.js b/system-tests/test/record_spec.js
index 5a5399f24d56..c0139be58768 100644
--- a/system-tests/test/record_spec.js
+++ b/system-tests/test/record_spec.js
@@ -1267,7 +1267,7 @@ describe('e2e record', () => {
         },
       } }))
 
-      it('errors and exits when there\'s an unknown 402 error', function () {
+      it(`errors and exits when there's an unknown 402 error`, function () {
         return systemTests.exec(this, {
           key: 'f858a2bc-b469-4e48-be67-0876339ee7e1',
           configFile: 'cypress-with-project-id.config.js',
@@ -1613,6 +1613,375 @@ describe('e2e record', () => {
         })
       })
     })
+
+    describe('sendPreflight', () => {
+      describe('[F1] socket errors', () => {
+        setupStubbedServer(createRoutes({
+          sendPreflight: {
+            res (req, res) {
+              return req.socket.destroy(new Error('killed'))
+            },
+          },
+        }))
+
+        it('fails after retrying', function () {
+          process.env.API_RETRY_INTERVALS = '1000'
+
+          return systemTests.exec(this, {
+            key: 'f858a2bc-b469-4e48-be67-0876339ee7e1',
+            configFile: 'cypress-with-project-id.config.js',
+            spec: 'record_pass*',
+            group: 'foo',
+            tag: 'nightly',
+            record: true,
+            parallel: true,
+            snapshot: true,
+            ciBuildId: 'ciBuildId123',
+            expectedExitCode: 1,
+          })
+        })
+      })
+
+      describe('[F1] 500 status code errors with empty body', () => {
+        setupStubbedServer(createRoutes({
+          sendPreflight: {
+            res (req, res) {
+              return res.sendStatus(500)
+            },
+          },
+        }))
+
+        it('fails after retrying', function () {
+          process.env.API_RETRY_INTERVALS = '1000'
+
+          return systemTests.exec(this, {
+            key: 'f858a2bc-b469-4e48-be67-0876339ee7e1',
+            configFile: 'cypress-with-project-id.config.js',
+            spec: 'record_pass*',
+            group: 'foo',
+            tag: 'nightly',
+            record: true,
+            parallel: true,
+            snapshot: true,
+            ciBuildId: 'ciBuildId123',
+            expectedExitCode: 1,
+          })
+        })
+      })
+
+      describe('[F1] 500 status code errors with body', () => {
+        setupStubbedServer(createRoutes({
+          sendPreflight: {
+            res (req, res) {
+              return res
+              .status(500)
+              .json({ message: 'an error message' })
+            },
+          },
+        }))
+
+        it('fails after retrying', function () {
+          process.env.API_RETRY_INTERVALS = '1000'
+
+          return systemTests.exec(this, {
+            key: 'f858a2bc-b469-4e48-be67-0876339ee7e1',
+            configFile: 'cypress-with-project-id.config.js',
+            spec: 'record_pass*',
+            group: 'foo',
+            tag: 'nightly',
+            record: true,
+            parallel: true,
+            snapshot: true,
+            ciBuildId: 'ciBuildId123',
+            expectedExitCode: 1,
+          })
+        })
+      })
+
+      describe('[F2] 404 status code with JSON body', () => {
+        setupStubbedServer(createRoutes({
+          sendPreflight: {
+            res (req, res) {
+              return res
+              .status(404)
+              .json({ message: 'not found' })
+            },
+          },
+        }))
+
+        it('fails without retrying', function () {
+          process.env.API_RETRY_INTERVALS = '1000'
+
+          return systemTests.exec(this, {
+            key: 'f858a2bc-b469-4e48-be67-0876339ee7e1',
+            configFile: 'cypress-with-project-id.config.js',
+            spec: 'record_pass*',
+            group: 'foo',
+            tag: 'nightly',
+            record: true,
+            parallel: true,
+            snapshot: true,
+            ciBuildId: 'ciBuildId123',
+            expectedExitCode: 1,
+          })
+        })
+      })
+
+      describe('[F2] 404 status code with empty body', () => {
+        setupStubbedServer(createRoutes({
+          sendPreflight: {
+            res (req, res) {
+              return res.sendStatus(404)
+            },
+          },
+        }))
+
+        it('fails without retrying', function () {
+          process.env.API_RETRY_INTERVALS = '1000'
+
+          return systemTests.exec(this, {
+            key: 'f858a2bc-b469-4e48-be67-0876339ee7e1',
+            configFile: 'cypress-with-project-id.config.js',
+            spec: 'record_pass*',
+            group: 'foo',
+            tag: 'nightly',
+            record: true,
+            parallel: true,
+            snapshot: true,
+            ciBuildId: 'ciBuildId123',
+            expectedExitCode: 1,
+          })
+        })
+      })
+
+      describe('[F3] 422 status code with invalid decryption', () => {
+        setupStubbedServer(createRoutes({
+          sendPreflight: {
+            res: async (req, res) => {
+              return res.status(422).json({
+                message: 'something broke',
+              })
+            },
+          },
+        }))
+
+        it('fails without retrying', function () {
+          process.env.API_RETRY_INTERVALS = '1000'
+
+          return systemTests.exec(this, {
+            key: 'f858a2bc-b469-4e48-be67-0876339ee7e1',
+            configFile: 'cypress-with-project-id.config.js',
+            spec: 'record_pass*',
+            group: 'foo',
+            tag: 'nightly',
+            record: true,
+            parallel: true,
+            snapshot: true,
+            ciBuildId: 'ciBuildId123',
+            expectedExitCode: 1,
+          })
+        })
+      })
+
+      describe('[F3] 201 status code with invalid decryption', () => {
+        setupStubbedServer(createRoutes({
+          sendPreflight: {
+            res (req, res) {
+              return res
+              .status(201)
+              .json({ data: 'very encrypted and secure string' })
+            },
+          },
+        }))
+
+        it('fails without retrying', function () {
+          process.env.API_RETRY_INTERVALS = '1000'
+
+          return systemTests.exec(this, {
+            key: 'f858a2bc-b469-4e48-be67-0876339ee7e1',
+            configFile: 'cypress-with-project-id.config.js',
+            spec: 'record_pass*',
+            group: 'foo',
+            tag: 'nightly',
+            record: true,
+            parallel: true,
+            snapshot: true,
+            ciBuildId: 'ciBuildId123',
+            expectedExitCode: 1,
+          })
+        })
+      })
+
+      describe('[F3] 200 status code with empty body', () => {
+        setupStubbedServer(createRoutes({
+          sendPreflight: {
+            res (req, res) {
+              return res.sendStatus(200)
+            },
+          },
+        }))
+
+        it('fails without retrying', function () {
+          process.env.API_RETRY_INTERVALS = '1000'
+
+          return systemTests.exec(this, {
+            key: 'f858a2bc-b469-4e48-be67-0876339ee7e1',
+            configFile: 'cypress-with-project-id.config.js',
+            spec: 'record_pass*',
+            group: 'foo',
+            tag: 'nightly',
+            record: true,
+            parallel: true,
+            snapshot: true,
+            ciBuildId: 'ciBuildId123',
+            expectedExitCode: 1,
+          })
+        })
+      })
+
+      describe('[F4] 412 status code with valid decryption', () => {
+        setupStubbedServer(createRoutes({
+          sendPreflight: {
+            res: async (req, res) => {
+              return res.status(412).json(await encryptBody(req, res, {
+                message: 'Recording is not working',
+                errors: [
+                  'attempted to send invalid data',
+                ],
+                object: {
+                  projectId: 'cy12345',
+                },
+              }))
+            },
+          },
+        }))
+
+        it('fails without retrying', function () {
+          process.env.API_RETRY_INTERVALS = '1000'
+
+          return systemTests.exec(this, {
+            key: 'f858a2bc-b469-4e48-be67-0876339ee7e1',
+            configFile: 'cypress-with-project-id.config.js',
+            spec: 'record_pass*',
+            group: 'foo',
+            tag: 'nightly',
+            record: true,
+            parallel: true,
+            snapshot: true,
+            ciBuildId: 'ciBuildId123',
+            expectedExitCode: 1,
+          })
+        })
+      })
+
+      describe('[F5] 422 status code with valid decryption on createRun', async () => {
+        const mockServer = setupStubbedServer(createRoutes({
+          sendPreflight: {
+            res: async (req, res) => {
+              return res.json(await encryptBody(req, res, {
+                encrypt: true,
+                apiUrl: req.body.apiUrl,
+              }))
+            },
+          },
+          postRun: {
+            res: async (req, res) => {
+              mockServer.setSpecs(req)
+
+              return res
+              .set({ 'x-cypress-encrypted': true })
+              .status(422)
+              .json(await encryptBody(req, res, {
+                code: 'RUN_GROUP_NAME_NOT_UNIQUE',
+                message: 'Run group name cannot be used again without passing the parallel flag.',
+                payload: {
+                  runUrl: 'https://cloud.cypress.io/runs/12345',
+                },
+              }))
+            },
+          },
+        }))
+
+        // the other 422 tests for this are in integration/cypress_spec
+        it('errors and exits when group name is in use', function () {
+          process.env.CIRCLECI = '1'
+
+          return systemTests.exec(this, {
+            key: 'f858a2bc-b469-4e48-be67-0876339ee7e1',
+            configFile: 'cypress-with-project-id.config.js',
+            spec: 'record_pass*',
+            group: 'e2e-tests',
+            record: true,
+            snapshot: true,
+            expectedExitCode: 1,
+          })
+          .then(() => {
+            const urls = getRequestUrls()
+
+            expect(urls).to.deep.eq([
+              'POST /runs',
+            ])
+          })
+        })
+      })
+
+      describe('[W1] warning message', () => {
+        const mockServer = setupStubbedServer(createRoutes({
+          sendPreflight: {
+            res: async (req, res) => {
+              return res.json(await encryptBody(req, res, {
+                encrypt: true,
+                apiUrl: req.body.apiUrl,
+                warnings: [
+                  {
+                    message: dedent`
+                    ----------------------------------------------------------------------
+                    This feature will not be supported soon, please check with Cypress to learn more: https://on.cypress.io/
+                    ----------------------------------------------------------------------
+                  `,
+                  },
+                ],
+              }))
+            },
+          },
+          postRun: {
+            res (req, res) {
+              mockServer.setSpecs(req)
+
+              return res.status(200).json({
+                runId,
+                groupId,
+                machineId,
+                runUrl,
+                tags,
+                warnings: [{
+                  name: 'foo',
+                  message: 'foo',
+                  code: 'FREE_PLAN_IN_GRACE_PERIOD_EXCEEDS_MONTHLY_PRIVATE_TESTS',
+                  limit: 500,
+                  gracePeriodEnds: '2999-12-31',
+                  orgId: 'org-id-1234',
+                }],
+              })
+            },
+          },
+        }))
+
+        it('renders preflight warning messages prior to run warnings', async function () {
+          return await systemTests.exec(this, {
+            key: 'f858a2bc-b469-4e48-be67-0876339ee7e1',
+            configFile: 'cypress-with-project-id.config.js',
+            spec: 'record_pass*',
+            group: 'foo',
+            tag: 'nightly',
+            record: true,
+            parallel: true,
+            snapshot: true,
+            ciBuildId: 'ciBuildId123',
+          })
+        })
+      })
+    })
   })
 
   describe('api interaction warnings', () => {
@@ -1890,160 +2259,4 @@ describe('e2e record', () => {
       })
     })
   })
-
-  describe('/preflight', () => {
-    describe('preflight failure: unencrypted', () => {
-      setupStubbedServer(createRoutes({
-        preflight: {
-          res (req, res) {
-            return res.json({ apiUrl: 'http://localhost:1234' })
-          },
-        },
-      }))
-
-      it('fails on an unencrypted preflight response', async function () {
-        return systemTests.exec(this, {
-          key: 'f858a2bc-b469-4e48-be67-0876339ee7e1',
-          configFile: 'cypress-with-project-id.config.js',
-          spec: 'record_pass*',
-          group: 'foo',
-          tag: 'nightly',
-          record: true,
-          parallel: true,
-          snapshot: true,
-          ciBuildId: 'ciBuildId123',
-          expectedExitCode: 1,
-        })
-      })
-    })
-
-    describe('preflight failure 500 server error', () => {
-      setupStubbedServer(createRoutes({
-        preflight: {
-          res (req, res) {
-            return res.sendStatus(500)
-          },
-        },
-      }))
-
-      it('retries on a preflight server error', async function () {
-        await new Promise((resolve, reject) => {
-          let sp
-
-          systemTests.exec(this, {
-            key: 'f858a2bc-b469-4e48-be67-0876339ee7e1',
-            configFile: 'cypress-with-project-id.config.js',
-            spec: 'record_pass*',
-            group: 'foo',
-            tag: 'nightly',
-            record: true,
-            parallel: true,
-            ciBuildId: 'ciBuildId123',
-            onSpawn (spawnResult) {
-              sp = spawnResult
-              sp.stdout.on('data', (chunk) => {
-                const msg = String(chunk)
-
-                if (msg.includes('We will retry')) {
-                  resolve()
-                  sp.kill()
-                }
-              })
-            },
-          }).catch(reject)
-        })
-      })
-    })
-
-    describe('preflight failure', () => {
-      setupStubbedServer(createRoutes({
-        preflight: {
-          res: async (req, res) => {
-            return res.status(412).json(await encryptBody(req, res, {
-              message: 'Recording this way is no longer supported',
-              errors: [
-                'attempted to send envUrl foo.bar.baz',
-              ],
-              object: {
-                ciBuildId: 'ciBuildId123',
-                projectId: 'cy12345',
-              },
-            }))
-          },
-        },
-      }))
-
-      it('renders error messages properly', async function () {
-        return systemTests.exec(this, {
-          key: 'f858a2bc-b469-4e48-be67-0876339ee7e1',
-          configFile: 'cypress-with-project-id.config.js',
-          spec: 'record_pass*',
-          group: 'foo',
-          tag: 'nightly',
-          record: true,
-          parallel: true,
-          snapshot: true,
-          ciBuildId: 'ciBuildId123',
-          expectedExitCode: 1,
-        })
-      })
-    })
-
-    describe('preflight failure: warning message', () => {
-      const mockServer = setupStubbedServer(createRoutes({
-        preflight: {
-          res: async (req, res) => {
-            return res.json(await encryptBody(req, res, {
-              encrypt: true,
-              apiUrl: req.body.apiUrl,
-              warnings: [
-                {
-                  message: dedent`
-                    ----------------------------------------------------------------------
-                    This feature will not be supported soon, please check with Cypress to learn more: https://on.cypress.io/
-                    ----------------------------------------------------------------------
-                  `,
-                },
-              ],
-            }))
-          },
-        },
-        postRun: {
-          res (req, res) {
-            mockServer.setSpecs(req)
-
-            return res.status(200).json({
-              runId,
-              groupId,
-              machineId,
-              runUrl,
-              tags,
-              warnings: [{
-                name: 'foo',
-                message: 'foo',
-                code: 'FREE_PLAN_IN_GRACE_PERIOD_EXCEEDS_MONTHLY_PRIVATE_TESTS',
-                limit: 500,
-                gracePeriodEnds: '2999-12-31',
-                orgId: 'org-id-1234',
-              }],
-            })
-          },
-        },
-      }))
-
-      it('renders preflight warning messages prior to run warnings', async function () {
-        return await systemTests.exec(this, {
-          key: 'f858a2bc-b469-4e48-be67-0876339ee7e1',
-          configFile: 'cypress-with-project-id.config.js',
-          spec: 'record_pass*',
-          group: 'foo',
-          tag: 'nightly',
-          record: true,
-          parallel: true,
-          snapshot: true,
-          ciBuildId: 'ciBuildId123',
-        })
-      })
-    })
-  })
 })
diff --git a/yarn.lock b/yarn.lock
index 461d15133e96..9fb9d86418e0 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -25677,6 +25677,13 @@ resolve-options@^1.1.0:
   dependencies:
     value-or-function "^3.0.0"
 
+resolve-package-path@4.0.3:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/resolve-package-path/-/resolve-package-path-4.0.3.tgz#31dab6897236ea6613c72b83658d88898a9040aa"
+  integrity sha512-SRpNAPW4kewOaNUt8VPqhJ0UMxawMwzJD8V7m1cJfdSTK9ieZwS6K7Dabsm4bmLFM96Z5Y/UznrpG5kt1im8yA==
+  dependencies:
+    path-root "^0.1.1"
+
 resolve-pkg@2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/resolve-pkg/-/resolve-pkg-2.0.0.tgz#ac06991418a7623edc119084edc98b0e6bf05a41"