Skip to content

feat(cypress): upload failure screenshots to the v2 media endpoint#8981

Merged
juan-fernandez merged 25 commits into
masterfrom
xinye.ji/cypress-failure-media-v2
Jul 6, 2026
Merged

feat(cypress): upload failure screenshots to the v2 media endpoint#8981
juan-fernandez merged 25 commits into
masterfrom
xinye.ji/cypress-failure-media-v2

Conversation

@xinyeji

@xinyeji xinyeji commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Adds opt-in upload of Cypress test-failure screenshots to the production v2 Test Optimization media endpoint (POST /api/unstable/ci/test-runs/<trace_id>/media), so failure-moment screenshots show up on the test run in the Datadog UI.

Builds on the innovation-day PoC #8574 (authored by @juan-fernandez, whose authorship is preserved on the first commits) and brings it up to the real bucket-backed v2 media contract. The remaining work was scoped and refined together with @juan-fernandez (#9154).

What's wired in:

  • New config flag DD_TEST_FAILURE_SCREENSHOTS_ENABLED (config.testOptimization.DD_TEST_FAILURE_SCREENSHOTS_ENABLED), default unset (off). Customer opt-in only; remote (library-settings) control is a follow-up. Naming follows the DD_TEST_* Test Optimization family (sibling of DD_TEST_FAILED_TEST_REPLAY_ENABLED).
  • Upload request module (packages/dd-trace/src/ci-visibility/requests/upload-test-screenshot.js): POSTs the raw image bytes with two headers the media service requires:
    • X-Dd-Idempotency-Key = <trace_id>:<filename> — stable per artifact and reused on retry, so a retried upload overwrites the same stored object instead of duplicating. The filename portion is hex-encoded so a non-ASCII test title can't throw ERR_INVALID_CHAR in http.request and abort the run (the transform is deterministic, so the backend's UUIDv5 overwrite-on-retry still holds).
    • X-Dd-Media-Captured-At = capture time in epoch ms (from the screenshot's takenAt, falling back to file mtime, then now). Stamped once and resent unchanged on retry, as it is part of the stored object key.
    • Validates trace_id (non-zero decimal uint64), DD_API_KEY, idempotency key, captured-at, and a non-empty file before sending; content type inferred from the extension (png/jpg/gif/webp).
  • Exporter gate (ci-visibility-exporter.js): new canUploadTestScreenshots() (true only when the upload URL is configured and the flag is on) and uploadTestScreenshot(). Agentless only for now — the agentless exporter points the upload at api.<site> with DD-API-KEY; the agent-proxy exporter leaves the URL undefined because the Datadog Agent's evp_proxy does not yet forward the media route (tracked as a follow-up).
  • Cypress plugin (cypress-plugin.js, index.js, plugin.js, instrumentation cypress-config.js):
    • Uploads on after:screenshot as soon as Cypress writes a failure screenshot, with an after:spec fallback that scans the spec's screenshots per failed test/attempt; uploads are deduped by file path.
    • Only failure screenshots are uploaded: requires an explicit failure signal (testFailure === true, or the (failed) filename marker), so manual cy.screenshot() captures are not uploaded (privacy).
    • User after:screenshot handlers are preserved: the plugin chains the user handler(s) first and merges their (possibly partial) return into the running details rather than replacing them, so Cypress metadata such as testFailure / takenAt survives a handler that only returns { path, size, dimensions }. Same chaining as the existing after:spec / after:run wrapping.
    • warnIfMisconfiguredTestFailureScreenshots() logs when the flag is on but Cypress screenshotOnRunFailure is off, or when not in agentless mode.

Motivation

Cypress already captures a screenshot at the moment a test fails. Surfacing it on the test run in Datadog makes triaging CI failures much faster. The v2 media backend requires a stable idempotency key and a real capture timestamp from the tracer; this wires those in against the real endpoint (no PoC-only host/header). Backend: DataDog/dd-source#443668. Frontend (Media tab): DataDog/web-ui#320875.

Additional Notes

  • Scope: screenshots only. Per-spec video upload was dropped during scoping (fix(cypress): scope failure media uploads to screenshots #9154): a Cypress video records the entire spec run (every test, passing ones included), so its privacy/cost profile is different and it warrants its own decision. No DD_TEST_FAILURE_VIDEO_ENABLED flag and no video code path ships here.
  • Agent (evp_proxy) mode is intentionally disabled. Agentless works today; agent forwarding of the media route is a follow-up.
  • Remote enablement is a follow-up — this PR wires the local env var only.
  • Semver: semver-minor (new opt-in feature).

Tests

  • Config (test/config/index.spec.js): default-off plus parse of true/false.
  • Exporter (ci-visibility-exporter.spec.js): canUploadTestScreenshots() gate matrix (URL × flag).
  • Request unit (requests/upload-test-screenshot.spec.js): header shape, validation errors, content-type, non-ASCII filename hex-encoding.
  • Integration (cypress/cypress-reporting.spec.js, ci-visibility-intake.js media handler with a configurable response status): a failing spec POSTs each failure screenshot to /api/unstable/ci/test-runs/<traceId>/media with the correct X-Dd-Idempotency-Key shape and a positive-integer X-Dd-Media-Captured-At; the flag-on/off matrix; a manual cy.screenshot() is not uploaded as failure media; and the cypress run still completes and reports the failed test when the media endpoint returns 500. ESM and CommonJS configs both gate capture on CYPRESS_ENABLE_FAILURE_SCREENSHOTS so the upload tests capture screenshots.

🤖 Generated with Claude Code

juan-fernandez and others added 4 commits June 18, 2026 20:01
Updates the Cypress failure-screenshot upload (from #8574) to the production v2
Test Optimization media contract:

- Send the two now-required headers:
  - X-Dd-Idempotency-Key = `{trace_id}:{filename}` — stable per artifact and
    reused on retry, so a retried upload overwrites the same stored object
    instead of creating a duplicate (the Cypress filename encodes test + attempt).
  - X-Dd-Media-Captured-At = capture time in epoch ms, taken from the screenshot's
    `takenAt` (mtime / now fallback), stamped once and resent unchanged on retry
    (it is part of the stored object key).
- Drop the temporary PoC bits: the `test-drive-test-failure-media-bucket` header
  and the `DD_POC_SITE` host override.
- Agentless uploads go to the normal api.<site> host with DD-API-KEY.

Agent (evp_proxy) mode upload stays disabled until the Datadog Agent allow-lists
POST /api/unstable/ci/test-runs/<trace_id>/media (follow-up).

Backend: DataDog/dd-source#443668. Builds on Juan's PoC #8574.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@dd-octo-sts

dd-octo-sts Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Overall package size

Self size: 6.53 MB
Deduped: 7.59 MB
No deduping: 7.59 MB

Dependency sizes | name | version | self size | total size | |------|---------|-----------|------------| | import-in-the-middle | 3.2.0 | 104.26 kB | 843.44 kB | | opentracing | 0.14.7 | 194.81 kB | 194.81 kB | | dc-polyfill | 0.1.11 | 25.74 kB | 25.74 kB |

🤖 This report was automatically generated by heaviest-objects-in-the-universe

@datadog-prod-us1-3

datadog-prod-us1-3 Bot commented Jun 18, 2026

Copy link
Copy Markdown

Tests

🎉 All green!

🧪 All tests passed
❄️ No new flaky tests detected

🎯 Code Coverage (details)
Patch Coverage: 67.00%
Overall Coverage: 87.77% (-0.27%)

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 577dde6 | Docs | Datadog PR Page | Give us feedback!

@pr-commenter

pr-commenter Bot commented Jun 18, 2026

Copy link
Copy Markdown

Benchmarks

Benchmark execution time: 2026-07-02 14:39:49

Comparing candidate commit 577dde6 in PR branch xinye.ji/cypress-failure-media-v2 with baseline commit 038b5ba in branch master.

📊 Benchmarking dashboard

Found 0 performance improvements and 0 performance regressions! Performance is the same for 2259 metrics, 27 unstable metrics.

Explanation

This is an A/B test comparing a candidate commit's performance against that of a baseline commit. Performance changes are noted in the tables below as:

  • 🟩 = significantly better candidate vs. baseline
  • 🟥 = significantly worse candidate vs. baseline

We compute a confidence interval (CI) over the relative difference of means between metrics from the candidate and baseline commits, considering the baseline as the reference.

If the CI is entirely outside the configured SIGNIFICANT_IMPACT_THRESHOLD (or the deprecated UNCONFIDENCE_THRESHOLD), the change is considered significant.

Feel free to reach out to #apm-benchmarking-platform on Slack if you have any questions.

More details about the CI and significant changes

You can imagine this CI as a range of values that is likely to contain the true difference of means between the candidate and baseline commits.

CIs of the difference of means are often centered around 0%, because often changes are not that big:

---------------------------------(------|---^--------)-------------------------------->
                              -0.6%    0%  0.3%     +1.2%
                                 |          |        |
         lower bound of the CI --'          |        |
sample mean (center of the CI) -------------'        |
         upper bound of the CI ----------------------'

As described above, a change is considered significant if the CI is entirely outside the configured SIGNIFICANT_IMPACT_THRESHOLD (or the deprecated UNCONFIDENCE_THRESHOLD).

For instance, for an execution time metric, this confidence interval indicates a significantly worse performance:

----------------------------------------|---------|---(---------^---------)---------->
                                       0%        1%  1.3%      2.2%      3.1%
                                                  |   |         |         |
       significant impact threshold --------------'   |         |         |
                      lower bound of CI --------------'         |         |
       sample mean (center of the CI) --------------------------'         |
                      upper bound of CI ----------------------------------'

Unstable benchmarks

These benchmarks have a confidence interval too wide to call a change; treat them as noise rather than signal.

scenario:appsec-appsec-enabled-24

  • unstable execution_time [-181.077ms; +178.106ms] or [-6.896%; +6.783%]

scenario:appsec-appsec-enabled-26

  • unstable execution_time [-204608.615µs; +205893.882µs] or [-8.158%; +8.210%]

scenario:appsec-appsec-enabled-with-attacks-26

  • unstable execution_time [-153.006ms; +156.027ms] or [-5.237%; +5.340%]

scenario:appsec-control-20

  • unstable execution_time [-153006.512µs; +154756.612µs] or [-8.741%; +8.841%]

scenario:appsec-control-24

  • unstable execution_time [-102311.176µs; +100721.509µs] or [-8.431%; +8.300%]

scenario:appsec-control-26

  • unstable execution_time [-112262.345µs; +114062.378µs] or [-9.288%; +9.437%]

scenario:debugger-line-probe-with-snapshot-default-24

  • unstable cpu_user_time [-1620.835ms; +514.977ms] or [-20.538%; +6.525%]
  • unstable execution_time [-1643.812ms; +536.573ms] or [-19.146%; +6.250%]
  • unstable instructions [-13.8G instructions; +4.5G instructions] or [-21.538%; +7.000%]
  • unstable throughput [-161.523op/s; +489.658op/s] or [-4.204%; +12.745%]

scenario:debugger-line-probe-with-snapshot-minimal-24

  • unstable cpu_user_time [-1743.789ms; +548.305ms] or [-21.990%; +6.914%]
  • unstable execution_time [-1752.940ms; +559.470ms] or [-20.357%; +6.497%]
  • unstable instructions [-14.7G instructions; +4.8G instructions] or [-22.742%; +7.390%]
  • unstable throughput [-159.944op/s; +507.900op/s] or [-4.164%; +13.222%]

scenario:debugger-line-probe-with-snapshot-minimal-26

  • unstable cpu_user_time [-2.880s; +0.414s] or [-29.209%; +4.196%]
  • unstable execution_time [-2.913s; +0.419s] or [-27.584%; +3.969%]
  • unstable instructions [-25.9G instructions; +3.4G instructions] or [-31.298%; +4.131%]
  • unstable throughput [-100.139op/s; +646.364op/s] or [-3.153%; +20.350%]

scenario:debugger-line-probe-without-snapshot-24

  • unstable cpu_user_time [-1720.414ms; +565.495ms] or [-21.729%; +7.142%]
  • unstable execution_time [-1753.091ms; +593.566ms] or [-20.371%; +6.897%]
  • unstable instructions [-14.7G instructions; +4.8G instructions] or [-22.736%; +7.384%]
  • unstable throughput [-175.605op/s; +502.173op/s] or [-4.567%; +13.059%]

scenario:dogstatsd-with-tags-20

  • unstable cpu_user_time [-170.129ms; +494.253ms] or [-3.487%; +10.131%]
  • unstable execution_time [-173.879ms; +492.335ms] or [-3.510%; +9.940%]
  • unstable throughput [-168660.652op/s; +59709.302op/s] or [-9.952%; +3.523%]

scenario:plugin-graphql-long-with-depth-and-collapse-off-20

  • unstable max_rss_usage [-28.118MB; +17.409MB] or [-7.441%; +4.607%]

scenario:spans-finish-later-26

  • unstable max_rss_usage [-5.736MB; +9.227MB] or [-3.895%; +6.265%]

@codecov

codecov Bot commented Jun 18, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 83.00000% with 34 lines in your changes missing coverage. Please review.
✅ Project coverage is 93.58%. Comparing base (038b5ba) to head (577dde6).
⚠️ Report is 40 commits behind head on master.

Files with missing lines Patch % Lines
...kages/datadog-plugin-cypress/src/cypress-plugin.js 86.11% 15 Missing ⚠️
...c/ci-visibility/requests/upload-test-screenshot.js 81.96% 11 Missing ⚠️
packages/datadog-plugin-cypress/src/index.js 57.14% 6 Missing ⚠️
...ace/src/ci-visibility/exporters/agentless/index.js 50.00% 1 Missing ⚠️
.../ci-visibility/exporters/ci-visibility-exporter.js 87.50% 1 Missing ⚠️

❌ Your patch check has failed because the patch coverage (83.00%) is below the target coverage (95.00%). You can increase the patch coverage or adjust the target coverage.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #8981      +/-   ##
==========================================
- Coverage   93.73%   93.58%   -0.15%     
==========================================
  Files         892      897       +5     
  Lines       51236    52416    +1180     
  Branches    11924    12317     +393     
==========================================
+ Hits        48026    49056    +1030     
- Misses       3210     3360     +150     
Flag Coverage Δ
aiguard 34.87% <ø> (-0.08%) ⬇️
aiguard-integration 41.95% <ø> (+0.17%) ⬆️
apm-bucket-0 34.83% <ø> (-0.08%) ⬇️
apm-bucket-1 40.50% <ø> (+0.10%) ⬆️
apm-bucket-2 37.49% <ø> (+0.08%) ⬆️
apm-capabilities-tracing 48.82% <62.79%> (+0.14%) ⬆️
apm-integrations-aerospike 33.12% <ø> (-0.08%) ⬇️
apm-integrations-confluentinc-kafka-javascript 40.02% <ø> (-0.08%) ⬇️
apm-integrations-couchbase 33.24% <ø> (-0.08%) ⬇️
apm-integrations-http 42.05% <ø> (+0.03%) ⬆️
apm-integrations-kafkajs 40.26% <ø> (-0.12%) ⬇️
apm-integrations-next 29.61% <ø> (+0.08%) ⬆️
apm-integrations-prisma 35.17% <ø> (+0.07%) ⬆️
apm-integrations-tedious 34.04% <ø> (+0.07%) ⬆️
appsec 57.89% <ø> (+0.14%) ⬆️
appsec-express_fastify_graphql 53.86% <ø> (+0.13%) ⬆️
appsec-integration 35.66% <0.00%> (-0.56%) ⬇️
appsec-kafka_ldapjs_lodash 43.75% <ø> (+0.10%) ⬆️
appsec-mongodb-core_mongoose_mysql 48.91% <ø> (+0.11%) ⬆️
appsec-next 28.10% <ø> (+0.03%) ⬆️
appsec-node-serialize_passport_postgres 48.08% <ø> (+0.11%) ⬆️
appsec-sourcing_stripe_template 45.64% <ø> (+0.10%) ⬆️
debugger 44.63% <ø> (+0.22%) ⬆️
instrumentations-bucket-0 28.15% <ø> (-0.08%) ⬇️
instrumentations-bucket-1 37.56% <ø> (+0.09%) ⬆️
instrumentations-bucket-10 40.51% <ø> (+0.09%) ⬆️
instrumentations-bucket-11 27.95% <ø> (-0.08%) ⬇️
instrumentations-bucket-12 28.78% <ø> (+0.04%) ⬆️
instrumentations-bucket-13 27.78% <ø> (-0.08%) ⬇️
instrumentations-bucket-2 30.35% <ø> (+0.05%) ⬆️
instrumentations-bucket-3 36.05% <ø> (+0.08%) ⬆️
instrumentations-bucket-4 28.56% <ø> (+0.42%) ⬆️
instrumentations-bucket-5 36.41% <ø> (+0.07%) ⬆️
instrumentations-bucket-6 38.42% <ø> (+0.09%) ⬆️
instrumentations-bucket-7 36.15% <ø> (+0.07%) ⬆️
instrumentations-bucket-8 37.10% <ø> (+0.08%) ⬆️
instrumentations-bucket-9 39.61% <ø> (+0.09%) ⬆️
instrumentations-instrumentation-couchbase 46.48% <ø> (-0.07%) ⬇️
instrumentations-integration-esbuild 24.48% <0.00%> (-0.38%) ⬇️
llmobs-ai_anthropic_bedrock 39.68% <ø> (+0.08%) ⬆️
llmobs-google-genai_langchain_vertex-ai 37.09% <ø> (+0.06%) ⬆️
llmobs-openai 39.74% <ø> (+0.09%) ⬆️
llmobs-sdk 43.59% <ø> (-0.08%) ⬇️
master-coverage 93.58% <83.00%> (?)
openfeature 37.92% <ø> (+0.17%) ⬆️
openfeature-unit 50.39% <ø> (ø)
platform-core_esbuild_instrumentations-misc 22.93% <0.00%> (-0.43%) ⬇️
platform-integration 47.62% <ø> (+0.20%) ⬆️
platform-shimmer_unit-guardrails_webpack 18.47% <0.00%> (-0.43%) ⬇️
plugins-bucket-0 36.51% <ø> (+0.12%) ⬆️
plugins-bucket-1 39.77% <ø> (+0.17%) ⬆️
plugins-bucket-11 38.62% <ø> (+0.09%) ⬆️
plugins-bucket-17 39.32% <ø> (+0.09%) ⬆️
plugins-bucket-18 42.18% <ø> (+0.10%) ⬆️
plugins-bucket-19 39.73% <ø> (+0.10%) ⬆️
plugins-bucket-20 43.44% <ø> (+0.12%) ⬆️
plugins-bucket-4 37.87% <ø> (+0.09%) ⬆️
plugins-bullmq_cassandra_cookie 39.93% <ø> (+0.10%) ⬆️
plugins-cookie-parser_crypto_dd-trace-api 33.21% <ø> (-0.08%) ⬇️
plugins-fetch_fs_generic-pool 36.20% <ø> (+0.08%) ⬆️
plugins-google-cloud-pubsub_grpc_handlebars 43.10% <ø> (-0.07%) ⬇️
plugins-hapi_hono_ioredis 37.94% <ø> (+0.09%) ⬆️
plugins-jest_knex_langgraph 32.60% <ø> (-0.08%) ⬇️
plugins-ldapjs_light-my-request_limitd-client 27.85% <ø> (-0.08%) ⬇️
plugins-lodash_mariadb_memcached 35.17% <ø> (-0.08%) ⬇️
plugins-mongodb_mongodb-core_mongoose 36.46% <ø> (+0.07%) ⬆️
plugins-multer_mysql_mysql2 35.14% <ø> (-0.08%) ⬇️
plugins-nats_node-serialize_opensearch 37.30% <ø> (+0.14%) ⬆️
plugins-passport-http_pino_postgres 35.52% <ø> (-0.08%) ⬇️
plugins-process_pug_redis 34.26% <ø> (-0.09%) ⬇️
plugins-undici_url_valkey 36.03% <ø> (+0.08%) ⬆️
plugins-vm_winston_ws 37.71% <ø> (+0.09%) ⬆️
profiling 43.80% <ø> (+0.12%) ⬆️
serverless-aws-sdk-aws-sdk 33.41% <ø> (+0.05%) ⬆️
serverless-aws-sdk-bedrockruntime 32.28% <ø> (+0.05%) ⬆️
serverless-aws-sdk-client 37.16% <ø> (-0.05%) ⬇️
serverless-aws-sdk-dynamodb 34.27% <ø> (+0.06%) ⬆️
serverless-aws-sdk-eventbridge 27.34% <ø> (+0.03%) ⬆️
serverless-aws-sdk-kinesis 37.55% <ø> (+0.08%) ⬆️
serverless-aws-sdk-lambda 34.72% <ø> (+0.06%) ⬆️
serverless-aws-sdk-s3 32.72% <ø> (+0.05%) ⬆️
serverless-aws-sdk-serverless-peer-service 39.65% <ø> (+0.10%) ⬆️
serverless-aws-sdk-sns 38.40% <ø> (+0.08%) ⬆️
serverless-aws-sdk-sqs 38.14% <ø> (+0.08%) ⬆️
serverless-aws-sdk-stepfunctions 33.30% <ø> (+0.06%) ⬆️
serverless-aws-sdk-util 47.95% <ø> (ø)
serverless-bucket-0 39.62% <ø> (+0.16%) ⬆️
serverless-lambda 34.33% <ø> (-0.09%) ⬇️
test-optimization-cucumber 52.40% <19.44%> (+0.21%) ⬆️
test-optimization-cypress 50.05% <82.00%> (+0.60%) ⬆️
test-optimization-jest 55.37% <19.44%> (+0.10%) ⬆️
test-optimization-mocha 53.48% <19.44%> (+0.17%) ⬆️
test-optimization-playwright-playwright-atr 43.42% <19.44%> (+0.18%) ⬆️
test-optimization-playwright-playwright-efd 43.70% <18.30%> (+0.17%) ⬆️
test-optimization-playwright-playwright-final-status 43.82% <18.30%> (+0.17%) ⬆️
test-optimization-playwright-playwright-impacted-tests 43.24% <18.30%> (+0.09%) ⬆️
test-optimization-playwright-playwright-reporting 43.36% <19.44%> (+0.18%) ⬆️
test-optimization-playwright-playwright-test-management 44.81% <18.30%> (+0.18%) ⬆️
test-optimization-playwright-playwright-test-span 44.61% <19.44%> (+0.18%) ⬆️
test-optimization-selenium 45.34% <19.44%> (+0.12%) ⬆️
test-optimization-testopt 46.73% <19.44%> (-1.22%) ⬇️
test-optimization-vitest 52.58% <19.44%> (+2.03%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

xinyeji and others added 10 commits June 18, 2026 20:30
Extends the media upload to Cypress's per-spec video:
- getContentType maps .mp4 -> video/mp4 and .webm -> video/webm.
- afterSpec uploads results.video to each failed test run's trace_id (Cypress
  records one video per spec, so it is attached to every failed test in the
  spec). Capture time falls back to the video file mtime (no per-test takenAt).
- New uploadTestVideo() reuses the same media request + idempotency key
  (`{trace_id}:{filename}`), so an upload retry overwrites instead of duplicating.

Backend + UI already support video (mp4/webm); this closes the tracer side for
Cypress. Image (screenshot) and video now both flow end-to-end.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
The debug line concatenated url.toString() (trailing '/') with the leading-slash
path, logging https://api.<site>//api/unstable/... The actual request was always
correct (request helper uses url.hostname + options.path separately); this just
makes the log resolve to a single, clean URL.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…RL import

- getScreenshotCapturedAtMs: add explicit screenshot !== null guard (typeof null
  is 'object'; required by eslint-rules/eslint-safe-typeof-object).
- agent-proxy: remove the now-unused 'url'.URL import (its only user was the
  DD_POC_SITE helper removed in the v2 change).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Recovers the failure-media upload assertion dropped when the v2 branch was
rebased onto master, adapted to the v2 contract.

The intake's /media handler now surfaces the content-type, idempotency key
and captured-at header on the emitted message so the test can assert them.

The test runs the basic-fail fixture with screenshots and video enabled and
asserts the tracer POSTs each artifact to
/api/unstable/ci/test-runs/<traceId>/media with the X-Dd-Idempotency-Key
(<traceId>:<filename> shape) and a positive-integer X-Dd-Media-Captured-At,
sends image/png for the screenshot and video/mp4 for the spec video, and no
longer sends the v1 test-drive-test-failure-media-bucket header.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…es it

The harness cypress.config.js disabled screenshots+video and a CLI --config
override does not win over the file values, so the v2 media test captured
nothing (Screenshots: 0). Drive both from CYPRESS_ENABLE_FAILURE_MEDIA (off by
default; the test sets it), skip 6.7.0 (flat JSON config can't read env), and
fix the lint nit (assert.match over assert.ok(startsWith)).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…oggles

Add DD_CIVISIBILITY_TEST_FAILURE_SCREENSHOTS_ENABLED and DD_CIVISIBILITY_TEST_FAILURE_VIDEO_ENABLED (both default off) so failure screenshots and the per-spec video can be uploaded independently. Gate screenshot uploads on canUploadTestScreenshots() and video uploads on the new canUploadTestVideo(); the v2 media integration test opts both in. Adds exporter gate unit tests + config tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
The esm module type runs Cypress via cypress-esm-config.mjs (programmatic cypress.run), not cypress.config.js, so the env-driven media gating must be mirrored there. It had video hardcoded false and screenshotOnRunFailure keyed off an unset var, so the esm cypress-reporting media test captured nothing. Both now gate on CYPRESS_ENABLE_FAILURE_MEDIA, matching the commonJS config.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
… Optimization convention)

Rename the two not-yet-released config flags from DD_CIVISIBILITY_TEST_FAILURE_{SCREENSHOTS,VIDEO}_ENABLED to DD_TEST_FAILURE_{SCREENSHOTS,VIDEO}_ENABLED, matching the newer DD_TEST_* family used by other test-optimization features (notably DD_TEST_FAILED_TEST_REPLAY_ENABLED, a sibling failure-capture feature) and the product naming. Internal property names (isTestFailure{Screenshots,Video}Enabled) are unchanged, so the exporter gate is untouched; regenerated config types + updated config and integration tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@xinyeji
xinyeji marked this pull request as ready for review June 26, 2026 09:32
@xinyeji
xinyeji requested review from a team as code owners June 26, 2026 09:33
@xinyeji
xinyeji requested review from khanayan123 and removed request for a team June 26, 2026 09:33

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f64f92c0cb

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread packages/datadog-plugin-cypress/src/cypress-plugin.js
@juan-fernandez

Copy link
Copy Markdown
Collaborator

E2E Test Report: SUCCESS WITH LOCAL CONTRACT VERIFICATION

Tested by: Shepherd Agent (autonomous QA for Datadog Test Optimization)

Test Environment

  • Method: Local Shepherd E2E testing with the mermaid Cypress playground
  • Dependency tested: dd-trace-js=xinye.ji/cypress-failure-media-v2
  • Revision tested: 946f150a28973cb511063e2ac01cb9e573e5edc4
  • Runtime: Node.js v22.14.0, Cypress 14.5.4, pnpm 10.30.3
  • Spec: cypress/integration/shepherd-atf.cy.js (intentional one-pass/one-fail Cypress spec)

Results

Check Status Evidence
PR branch installed into real Cypress playground PASS dd-trace injected from DataDog/dd-trace-js#xinye.ji/cypress-failure-media-v2
CI Visibility test telemetry emitted PASS Mockdog run 20260629-112201: 1 Cypress session, 2 tests, 1 pass, 1 intentional fail
Failure screenshot capture triggered PASS Cypress reported Screenshots: 1; tracer uploaded an image/png body
Spec video capture triggered PASS Cypress reported Video: true; tracer uploaded a video/mp4 body
Media endpoint path PASS Both uploads used /api/unstable/ci/test-runs/<trace_id>/media
X-Dd-Idempotency-Key header PASS Screenshot key: <trace_id>:shepherd hooks test -- should always fail (failed).png; video key: <trace_id>:shepherd-atf.cy.js.mp4
X-Dd-Media-Captured-At header PASS Both uploads sent positive epoch-ms timestamps
Raw media body PASS Screenshot body was non-empty PNG bytes; video body was non-empty MP4 bytes
Deprecated PoC bucket header PASS Capture server received no test-drive-test-failure-media-bucket header
Real Datadog backend / UI verification NOT RUN Local config/secrets.yaml is not configured, so staging/EU validation could not be performed from this machine

Test Methodology

  1. Fetched PR metadata and inspected the diff for the Cypress media upload change.
  2. Refreshed local tracer clones and checked whether other Test Optimization SDKs or Shepherd mockdog already model the media endpoint/headers. No existing implementation or mock route was found outside this PR.
  3. Ran the focused Mermaid Cypress playground against mockdog:
    • ./bin/crook run mermaid --command single --file cypress/integration/shepherd-atf.cy.js --dep dd-trace-js=xinye.ji/cypress-failure-media-v2 --debug --env DD_TEST_FAILURE_SCREENSHOTS_ENABLED=true --env DD_TEST_FAILURE_VIDEO_ENABLED=true --env CYPRESS_VIDEO=true --env CYPRESS_SCREENSHOT_ON_RUN_FAILURE=true
    • This confirmed normal CI Visibility telemetry and also showed mockdog returns 404 for the new media endpoint because Shepherd does not currently register /api/unstable/ci/test-runs/<trace_id>/media.
  4. Ran the same focused Cypress playground against a local capture server that implements the media route and returns 201.
  5. Verified the captured media requests included the expected endpoint, content types, required v2 headers, non-empty raw bodies, and no deprecated PoC bucket header.

Issues Found

  • No product bug found in this PR during local E2E.
  • Test infrastructure gap: Shepherd mockdog does not yet model POST /api/unstable/ci/test-runs/<trace_id>/media, so mockdog-only validation returns 404 for the new media uploads. I used a local capture server to verify the request contract.
  • Real backend validation was not possible locally because config/secrets.yaml is not configured.

This E2E test was performed by Shepherd - autonomous QA agent for Datadog Test Optimization.

Comment on lines +919 to +926
it('uploads failure screenshots and the spec video to the v2 media endpoint', async function () {
const envVars = getCiVisAgentlessConfig(receiver.port)
const specToRun = 'cypress/e2e/basic-fail.js'
// 6.7.0 uses a flat JSON config that can't read env to enable media; skip it.
if (version === '6.7.0') {
this.skip()
return
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can use over10It instead of it instead of doing the version check here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 34371da — switched to over10It and dropped the manual version === '6.7.0' check (kept a one-line comment for the why). Thanks!

class AgentProxyCiVisibilityExporter extends CiVisibilityExporter {
constructor (config) {
super(config)
// Agent-mode media upload is not wired yet: the Datadog Agent's evp_proxy must

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this sucks. Would this work if the endpoint were /api/v2/ci/test-runs/<trace_id>/media ? If so, there is any plan to move away from unstable?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update: we're on /api/unstable because libraries only carry an API key (ValidReportingAPIUser), and lambo only accepts that auth method on /api/intake/ and /api/unstable/ by default. To move to /api/v2 we have to register the route in lambo's API-key allow-list (ApiKeyOnlyAuthRoutes) — I've put that up in https://github.com/ddoghq/dd-source/pull/5434 and I'm checking with the CoreAuthN/aaa-authn team that it fits the use case. Once it lands we can flip the prefix to /api/v2 (which also unblocks agent mode, since evp_proxy forwards /api/v2/* but not /api/unstable/).

}

on('before:run', cypressPlugin.beforeRun.bind(cypressPlugin))
on('after:screenshot', cypressPlugin.afterScreenshot.bind(cypressPlugin))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Chain the user after:screenshot handler instead of replacing it

Cypress stores one callback per node event, except for task merging. In the auto path we collect user after:spec/after:run handlers but let user after:screenshot go straight through, then this registration replaces it. That drops existing screenshot hooks that move or redact screenshots, even when media upload is disabled. Please capture user after:screenshot handlers in the wrapper and chain them here, preserving any returned { path, size, dimensions } before uploading.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 34371da — we now capture the user's after:screenshot handler in the setupNodeEvents wrapper (same pattern we already use for after:spec/after:run) and chain it: the user handler runs first, we thread its returned { path, size, dimensions } forward (honoring Cypress's "undefined return ⇒ keep original"), then run our upload on the final details and return them to Cypress. It runs regardless of whether media upload is enabled, so screenshot move/redact hooks are preserved. Also covered the manual-plugin + auto-instrumentation path so the upload isn't dropped when a user handler is present.

}

function isFailureScreenshot (screenshot) {
return !!getScreenshotFilePath(screenshot) && screenshot?.testFailure !== false

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Do not treat screenshots without testFailure as failure screenshots

The screenshots in Cypress RunResult only expose name/takenAt/path/height/width, so the after:spec fallback usually cannot see testFailure. With this predicate, any manual cy.screenshot() taken by a test that later fails can be uploaded under the failure-screenshot flag, which is broader than the intended privacy contract. Please make this fallback use the stricter failure-screenshot check, for example requiring testFailure === true or the Cypress failure filename marker, and add a failed-test-with-manual-screenshot case.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 34371da — tightened the after:spec fallback predicate (isFailureScreenshot) to require an explicit failure signal: testFailure === true || filename includes '(failed)' (matching isFailureScreenshotForUpload). So a manual cy.screenshot() in a test that later fails is no longer swept into the failure-media upload. Added an integration test: a failing test that also takes cy.screenshot('before-failure'), asserting only the auto (failed) frame is uploaded and the manual capture never reaches the media endpoint.

- Hex-encode the idempotency key's filename portion before the
  X-Dd-Idempotency-Key header so a non-ASCII test title can't throw
  ERR_INVALID_CHAR and abort the run (deterministic; backend UUIDv5 unchanged).
- Tighten the after:spec failure-screenshot predicate to require an explicit
  failure signal (testFailure === true || the '(failed)' marker), so manual
  cy.screenshot() captures aren't uploaded as failure media.
- Chain the user's after:screenshot handler instead of replacing it, mirroring
  the after:spec/after:run capture, so user screenshot hooks are preserved.
- Use over10It for the media-endpoint test instead of a manual version skip.

Adds unit + integration coverage (non-ASCII filename; failing test with a
manual screenshot uploads only the auto failure frame).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@xinyeji
xinyeji requested a review from a team as a code owner June 29, 2026 14:56
@xinyeji
xinyeji requested review from wconti27 and removed request for a team June 29, 2026 14:56
…ilure-media-v2

# Conflicts:
#	packages/dd-trace/src/config/generated-config-types.d.ts

@juan-fernandez juan-fernandez left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looking good! Left some integration test feedback

Comment thread integration-tests/cypress/cypress-reporting.spec.js Outdated
Comment thread integration-tests/cypress/cypress-reporting.spec.js Outdated
Comment thread integration-tests/cypress/cypress-reporting.spec.js Outdated
Comment thread integration-tests/cypress/cypress-reporting.spec.js
Comment thread integration-tests/ci-visibility-intake.js Outdated

@juan-fernandez juan-fernandez left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One inline comment from the review follow-up.

Comment thread packages/datadog-plugin-cypress/src/index.js Outdated
xinyeji and others added 7 commits June 30, 2026 15:27
A user-supplied after:screenshot handler that returns only { path, size, dimensions } previously replaced the whole details object, dropping Cypress metadata (testFailure, takenAt) the plugin relies on to decide which screenshots to upload. Merge the handler's return into the running details instead of replacing them.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…ilience

Add agentless tests for screenshots-only and video-only flag combinations, a resilience test that the cypress run still completes and reports the failed test when the media endpoint returns 500, and a configurable media response status code on the intake mock. onlyAgentlessIt documents that media upload currently only goes through the agentless transport.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
A config test forbids internalPropertyName in supported-configurations.json. Switch the two DD_TEST_FAILURE_*_ENABLED entries to configurationNames with the same camelCase property name (the established pattern, e.g. DD_ENV -> env), so config.isTestFailureScreenshotsEnabled / isTestFailureVideoEnabled are unchanged and the generated types regenerate identically.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
juan-fernandez
juan-fernandez previously approved these changes Jul 2, 2026
…oxy) upload (#9186)

* fix(cypress): send failure-media metadata as query params + enable agent (evp_proxy) upload

The Agent's evp_proxy strips X-Dd-* headers, so agent-mode failure-media uploads
reached the backend with an empty idempotency key. Move idempotency_key and
captured_at_ms to query params (survive the proxy) and enable the agent-mode upload
path via evp_proxy (X-Datadog-EVP-Subdomain: api). Pairs with the dd-source backend
change reading these from the query string.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* fix(cypress): hex-encode failure-media idempotency key filename for evp_proxy

The Agent's evp_proxy validates the forwarded query string against a restrictive
charset and rejects a raw Cypress screenshot filename (spaces, parens), so the
query-param upload 502'd in agent mode. Restore the reviewed encoding
(<traceId>:<hex(filename)>) applied to the query value: trace id stays readable,
filename is hex-safe. Verified 201 through evp_proxy end-to-end.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* test(cypress): assert failure-media metadata on the query string, not headers

The mock intake and the integration assertions still expected the old header-borne
metadata, so the cypress media tests failed after the query-param move. Align them:
the intake parses idempotency_key/captured_at_ms from the query, the path assertion
ignores the query string, and a guard confirms the X-Dd-* metadata headers are gone.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
@juan-fernandez

Copy link
Copy Markdown
Collaborator

great job! 👏

@juan-fernandez
juan-fernandez merged commit 6d46086 into master Jul 6, 2026
651 of 653 checks passed
@juan-fernandez
juan-fernandez deleted the xinye.ji/cypress-failure-media-v2 branch July 6, 2026 14:24
This was referenced Jul 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants