Skip to content

Commit 1d84532

Browse files
feat: Improve UX when Test Replay upload fails due to slow upload (#30235)
* new error message for stream stall upload failures * new unknown error msg, extract printProtocolUploadError for testing, consolidatd args for stream stall msg * rename env more appropriately, add sampling interval param to putProtocolArtifact * default to 5000, override with app capture protocol value, override that with env var * update snapshots * fix math in upload stall error msg * changelog * update gql schema w/ new error msg, fix protocol tscheck * update test to use fn for app capture supplied interval * increase default stream stall interval to 10 seconds * typos * snapshots * rename env var * do not use user-supplied interval if it parses to NaN * use the more standard makeErr for unknown/uncategorized upload error in visual snapshots * rearrange upload stall error message * fix typo * changelog --------- Co-authored-by: Jennifer Shehane <[email protected]>
1 parent fbe51fc commit 1d84532

20 files changed

+455
-66
lines changed

cli/CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
<!-- See the ../guides/writing-the-cypress-changelog.md for details on writing the changelog. -->
2-
## 13.14.3
2+
## 13.15.0
33

44
_Released 9/24/2024 (PENDING)_
55

6+
**Features:**
7+
8+
- Cypress now displays more actionable errors when a Test Replay upload takes too long, and more verbose messages when uncategorized errors occur during the upload process. Addressed in [#30235](https://github.com/cypress-io/cypress/pull/30235).
9+
610
**Bugfixes:**
711

812
- Fixed an issue where Firefox was incorrectly mutating the state of click events on checkboxes after Firefox version `129` and up. Addressed in [#30245](https://github.com/cypress-io/cypress/pull/30245).

packages/errors/__snapshot-html__/CLOUD_PROTOCOL_UPLOAD_AGGREGATE_ERROR - withSystemError.html

Lines changed: 46 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/errors/__snapshot-html__/CLOUD_PROTOCOL_UPLOAD_STREAM_STALL_FAILURE.html

Lines changed: 47 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/errors/__snapshot-html__/CLOUD_PROTOCOL_UPLOAD_UNKNOWN_ERROR.html

Lines changed: 45 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/errors/src/errors.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -574,6 +574,17 @@ export const AllCypressErrors = {
574574
This error will not affect or change the exit code.
575575
`
576576
},
577+
CLOUD_PROTOCOL_UPLOAD_UNKNOWN_ERROR: (error: Error) => {
578+
return errTemplate`\
579+
Warning: We encountered an error while uploading the Test Replay recording of this spec.
580+
581+
These results will not display Test Replay recordings.
582+
583+
This error will not affect or change the exit code.
584+
585+
${fmt.highlightSecondary(error)}
586+
`
587+
},
577588
CLOUD_PROTOCOL_UPLOAD_HTTP_FAILURE: (error: Error & { url: string, status: number, statusText: string, responseBody: string }) => {
578589
return errTemplate`\
579590
Warning: We encountered an HTTP error while uploading the Test Replay recording for this spec.
@@ -586,7 +597,7 @@ export const AllCypressErrors = {
586597
587598
${fmt.highlightTertiary(error.responseBody)}`
588599
},
589-
CLOUD_PROTOCOL_UPLOAD_NEWORK_FAILURE: (error: Error & { url: string }) => {
600+
CLOUD_PROTOCOL_UPLOAD_NETWORK_FAILURE: (error: Error & { url: string }) => {
590601
return errTemplate`\
591602
Warning: We encountered a network error while uploading the Test Replay recording for this spec.
592603
@@ -598,14 +609,29 @@ export const AllCypressErrors = {
598609
599610
${fmt.highlightSecondary(error)}`
600611
},
612+
CLOUD_PROTOCOL_UPLOAD_STREAM_STALL_FAILURE: (error: Error & { chunkSizeKB: number, maxActivityDwellTime: number }) => {
613+
const kbpsThreshold = (error.chunkSizeKB * 8) / (error.maxActivityDwellTime / 1000)
614+
615+
return errTemplate`\
616+
Warning: We encountered slow network conditions while uploading the Test Replay recording for this spec.
617+
618+
The upload transfer rate fell below ${fmt.highlightSecondary(`${kbpsThreshold}kbps`)} over a sampling period of ${fmt.highlightSecondary(`${error.maxActivityDwellTime}ms`)}.
619+
620+
To prevent long CI execution durations, this Test Replay recording will not be uploaded.
621+
622+
The results for this spec will not display Test Replay recordings.
623+
624+
If this error occurs often, the sampling period may be configured by setting the ${fmt.highlightSecondary('CYPRESS_TEST_REPLAY_UPLOAD_SAMPLING_INTERVAL')} environment variable to a higher value than ${fmt.stringify(error.maxActivityDwellTime)}.
625+
`
626+
},
601627
CLOUD_PROTOCOL_UPLOAD_AGGREGATE_ERROR: (error: {
602628
errors: (Error & { kind?: 'SystemError', url: string } | Error & { kind: 'HttpError', url: string, status?: string, statusText?: string, responseBody?: string })[]
603629
}) => {
604630
if (error.errors.length === 1) {
605631
const firstError = error.errors[0]
606632

607633
if (firstError?.kind === 'SystemError') {
608-
return AllCypressErrors.CLOUD_PROTOCOL_UPLOAD_NEWORK_FAILURE(firstError as Error & { url: string })
634+
return AllCypressErrors.CLOUD_PROTOCOL_UPLOAD_NETWORK_FAILURE(firstError as Error & { url: string })
609635
}
610636

611637
return AllCypressErrors.CLOUD_PROTOCOL_UPLOAD_HTTP_FAILURE(error.errors[0] as Error & { url: string, status: number, statusText: string, responseBody: string})

packages/errors/test/unit/visualSnapshotErrors_spec.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -685,7 +685,7 @@ describe('visual error templates', () => {
685685
default: [err],
686686
}
687687
},
688-
CLOUD_PROTOCOL_UPLOAD_NEWORK_FAILURE: () => {
688+
CLOUD_PROTOCOL_UPLOAD_NETWORK_FAILURE: () => {
689689
// @ts-expect-error
690690
const err: Error & { url: string } = makeErr()
691691

@@ -695,6 +695,17 @@ describe('visual error templates', () => {
695695
default: [err],
696696
}
697697
},
698+
CLOUD_PROTOCOL_UPLOAD_STREAM_STALL_FAILURE: () => {
699+
// @ts-expect-error
700+
const err: Error & { chunkSizeKB: number, maxActivityDwellTime: number } = new Error('stream stall')
701+
702+
err.chunkSizeKB = 64
703+
err.maxActivityDwellTime = 5000
704+
705+
return {
706+
default: [err],
707+
}
708+
},
698709
CLOUD_PROTOCOL_UPLOAD_AGGREGATE_ERROR: () => {
699710
// @ts-expect-error
700711
const aggregateError: Error & { errors: any[] } = makeErr()
@@ -719,6 +730,13 @@ describe('visual error templates', () => {
719730
withSystemError: [aggregateErrorWithSystemError],
720731
}
721732
},
733+
CLOUD_PROTOCOL_UPLOAD_UNKNOWN_ERROR: () => {
734+
const error = makeErr()
735+
736+
return {
737+
default: [error],
738+
}
739+
},
722740
CLOUD_RECORD_KEY_NOT_VALID: () => {
723741
return {
724742
default: ['record-key-123', 'project-id-123'],

packages/graphql/schemas/schema.graphql

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1153,7 +1153,9 @@ enum ErrorTypeEnum {
11531153
CLOUD_PROTOCOL_INITIALIZATION_FAILURE
11541154
CLOUD_PROTOCOL_UPLOAD_AGGREGATE_ERROR
11551155
CLOUD_PROTOCOL_UPLOAD_HTTP_FAILURE
1156-
CLOUD_PROTOCOL_UPLOAD_NEWORK_FAILURE
1156+
CLOUD_PROTOCOL_UPLOAD_NETWORK_FAILURE
1157+
CLOUD_PROTOCOL_UPLOAD_STREAM_STALL_FAILURE
1158+
CLOUD_PROTOCOL_UPLOAD_UNKNOWN_ERROR
11571159
CLOUD_RECORD_KEY_NOT_VALID
11581160
CLOUD_RUN_GROUP_NAME_NOT_UNIQUE
11591161
CLOUD_STALE_RUN

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

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,18 @@ import { putFetch, ParseKinds } from '../network/put_fetch'
77
import { isRetryableError } from '../network/is_retryable_error'
88
const debug = Debug('cypress:server:cloud:api:protocol-artifact')
99

10-
// the upload will get canceled if the stream pipeline
11-
// stalls (does not push data to the `fetch` sink) for more
12-
// than 5 seconds
13-
const MAX_ACTIVITY_DWELL_TIME = 5000
14-
1510
export const _delay = linearDelay(500)
1611

1712
export const putProtocolArtifact = asyncRetry(
18-
async (artifactPath: string, maxFileSize: number, destinationUrl: string) => {
13+
async (artifactPath: string, maxFileSize: number, destinationUrl: string, uploadMonitorSamplingRate: number) => {
1914
debug(`Atttempting to upload Test Replay archive from ${artifactPath} to ${destinationUrl})`)
2015
const { size } = await fsAsync.stat(artifactPath)
2116

2217
if (size > maxFileSize) {
2318
throw new Error(`Spec recording too large: artifact is ${size} bytes, limit is ${maxFileSize} bytes`)
2419
}
2520

26-
const activityMonitor = new StreamActivityMonitor(MAX_ACTIVITY_DWELL_TIME)
21+
const activityMonitor = new StreamActivityMonitor(uploadMonitorSamplingRate)
2722
const fileStream = fs.createReadStream(artifactPath)
2823
const controller = activityMonitor.getController()
2924

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { HttpError } from '../network/http_error'
2+
import { SystemError } from '../network/system_error'
3+
import { StreamStalledError } from '../upload/stream_stalled_error'
4+
import Debug from 'debug'
5+
import * as errors from '../../errors'
6+
7+
const debug = Debug('cypress:server:cloud:artifacts')
8+
9+
export const printProtocolUploadError = (error: Error) => {
10+
debug('protocol error: %O', error)
11+
// eslint-disable-next-line no-console
12+
console.log('')
13+
if ((error as AggregateError).errors) {
14+
errors.warning('CLOUD_PROTOCOL_UPLOAD_AGGREGATE_ERROR', error as AggregateError)
15+
} else if (HttpError.isHttpError(error)) {
16+
errors.warning('CLOUD_PROTOCOL_UPLOAD_HTTP_FAILURE', error)
17+
} else if (SystemError.isSystemError(error)) {
18+
errors.warning('CLOUD_PROTOCOL_UPLOAD_NETWORK_FAILURE', error)
19+
} else if (StreamStalledError.isStreamStalledError(error)) {
20+
errors.warning('CLOUD_PROTOCOL_UPLOAD_STREAM_STALL_FAILURE', error)
21+
} else {
22+
errors.warning('CLOUD_PROTOCOL_UPLOAD_UNKNOWN_ERROR', error)
23+
}
24+
}

0 commit comments

Comments
 (0)