dd-trace 6.4.0 → 6.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/README.electron.md +7 -0
  2. package/README.md +17 -0
  3. package/ci/diagnose.js +2100 -0
  4. package/ci/init.js +23 -2
  5. package/ci/runbook.md +198 -0
  6. package/ci/test-optimization-validation/approval-artifacts.js +143 -0
  7. package/ci/test-optimization-validation/approval.js +299 -0
  8. package/ci/test-optimization-validation/artifact-id.js +20 -0
  9. package/ci/test-optimization-validation/bounded-json.js +121 -0
  10. package/ci/test-optimization-validation/ci-command-candidate.js +83 -0
  11. package/ci/test-optimization-validation/ci-discovery.js +181 -0
  12. package/ci/test-optimization-validation/ci-remediation.js +210 -0
  13. package/ci/test-optimization-validation/cli.js +903 -0
  14. package/ci/test-optimization-validation/command-blocker.js +81 -0
  15. package/ci/test-optimization-validation/command-output-policy.js +206 -0
  16. package/ci/test-optimization-validation/command-runner.js +707 -0
  17. package/ci/test-optimization-validation/command-suitability.js +471 -0
  18. package/ci/test-optimization-validation/executable-approval.js +48 -0
  19. package/ci/test-optimization-validation/executable.js +480 -0
  20. package/ci/test-optimization-validation/generated-file-policy.js +63 -0
  21. package/ci/test-optimization-validation/generated-files.js +351 -0
  22. package/ci/test-optimization-validation/generated-verifier.js +196 -0
  23. package/ci/test-optimization-validation/init-probe-preload.js +163 -0
  24. package/ci/test-optimization-validation/init-probe.js +246 -0
  25. package/ci/test-optimization-validation/late-initialization.js +63 -0
  26. package/ci/test-optimization-validation/local-command.js +163 -0
  27. package/ci/test-optimization-validation/manifest-loader.js +133 -0
  28. package/ci/test-optimization-validation/manifest-scaffold.js +738 -0
  29. package/ci/test-optimization-validation/manifest-schema.js +862 -0
  30. package/ci/test-optimization-validation/offline-fixtures.js +327 -0
  31. package/ci/test-optimization-validation/offline-output.js +406 -0
  32. package/ci/test-optimization-validation/payload-normalizer.js +72 -0
  33. package/ci/test-optimization-validation/plan-writer.js +1120 -0
  34. package/ci/test-optimization-validation/preflight-runner.js +114 -0
  35. package/ci/test-optimization-validation/redaction.js +331 -0
  36. package/ci/test-optimization-validation/report-writer.js +1508 -0
  37. package/ci/test-optimization-validation/safe-files.js +203 -0
  38. package/ci/test-optimization-validation/scenarios/auto-test-retries.js +159 -0
  39. package/ci/test-optimization-validation/scenarios/basic-reporting.js +657 -0
  40. package/ci/test-optimization-validation/scenarios/ci-wiring.js +780 -0
  41. package/ci/test-optimization-validation/scenarios/early-flake-detection.js +141 -0
  42. package/ci/test-optimization-validation/scenarios/helpers.js +539 -0
  43. package/ci/test-optimization-validation/scenarios/test-management.js +218 -0
  44. package/ci/test-optimization-validation/setup-runner.js +97 -0
  45. package/ci/test-optimization-validation/static-diagnosis.js +159 -0
  46. package/ci/test-optimization-validation/test-output.js +129 -0
  47. package/ci/test-optimization-validation-manifest.schema.json +1 -0
  48. package/ci/validate-test-optimization.js +3 -0
  49. package/ext/exporters.js +1 -0
  50. package/index.d.ts +121 -8
  51. package/init.js +18 -0
  52. package/initialize.mjs +1 -1
  53. package/loader-hook.mjs +28 -18
  54. package/openfeature.d.ts +1 -0
  55. package/openfeature.js +4 -0
  56. package/package.json +11 -8
  57. package/packages/datadog-instrumentations/src/ai.js +8 -2
  58. package/packages/datadog-instrumentations/src/child_process.js +1 -1
  59. package/packages/datadog-instrumentations/src/claude-agent-sdk.js +1 -1
  60. package/packages/datadog-instrumentations/src/cucumber-worker-threads.js +9 -3
  61. package/packages/datadog-instrumentations/src/cucumber.js +31 -19
  62. package/packages/datadog-instrumentations/src/cypress-config.js +398 -52
  63. package/packages/datadog-instrumentations/src/fastify.js +7 -11
  64. package/packages/datadog-instrumentations/src/helpers/channel.js +74 -0
  65. package/packages/datadog-instrumentations/src/helpers/hook.js +27 -6
  66. package/packages/datadog-instrumentations/src/helpers/optional-peer-bundler.js +8 -8
  67. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/playwright.js +28 -2
  68. package/packages/datadog-instrumentations/src/jest.js +44 -17
  69. package/packages/datadog-instrumentations/src/mariadb.js +1 -1
  70. package/packages/datadog-instrumentations/src/mocha/main.js +79 -32
  71. package/packages/datadog-instrumentations/src/mocha/utils.js +0 -4
  72. package/packages/datadog-instrumentations/src/mongodb.js +3 -3
  73. package/packages/datadog-instrumentations/src/mongoose.js +3 -3
  74. package/packages/datadog-instrumentations/src/mquery.js +2 -2
  75. package/packages/datadog-instrumentations/src/mysql.js +1 -1
  76. package/packages/datadog-instrumentations/src/next.js +24 -0
  77. package/packages/datadog-instrumentations/src/nyc.js +0 -2
  78. package/packages/datadog-instrumentations/src/oracledb.js +2 -2
  79. package/packages/datadog-instrumentations/src/pg.js +2 -2
  80. package/packages/datadog-instrumentations/src/playwright.js +162 -46
  81. package/packages/datadog-instrumentations/src/process.js +3 -0
  82. package/packages/datadog-instrumentations/src/router.js +18 -8
  83. package/packages/datadog-instrumentations/src/selenium.js +52 -26
  84. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +3 -3
  85. package/packages/datadog-instrumentations/src/vitest-main.js +14 -28
  86. package/packages/datadog-instrumentations/src/vitest-util.js +0 -7
  87. package/packages/datadog-instrumentations/src/vitest-worker.js +5 -8
  88. package/packages/datadog-instrumentations/src/ws.js +2 -1
  89. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/context.js +2 -2
  90. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/util.js +2 -1
  91. package/packages/datadog-plugin-aws-sdk/src/util.js +2 -2
  92. package/packages/datadog-plugin-cucumber/src/index.js +3 -2
  93. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +19 -57
  94. package/packages/datadog-plugin-cypress/src/index.js +6 -1
  95. package/packages/datadog-plugin-cypress/src/support.js +92 -26
  96. package/packages/datadog-plugin-electron/src/ipc.js +1 -1
  97. package/packages/datadog-plugin-graphql/src/execute.js +87 -45
  98. package/packages/datadog-plugin-graphql/src/validate.js +2 -2
  99. package/packages/datadog-plugin-http2/src/client.js +1 -1
  100. package/packages/datadog-plugin-langchain/src/handlers/embedding.js +2 -1
  101. package/packages/datadog-plugin-langchain/src/handlers/language_models.js +2 -1
  102. package/packages/datadog-plugin-langchain/src/tokens.js +2 -2
  103. package/packages/datadog-plugin-mocha/src/index.js +2 -1
  104. package/packages/datadog-plugin-playwright/src/index.js +181 -65
  105. package/packages/datadog-plugin-router/src/index.js +11 -2
  106. package/packages/datadog-plugin-selenium/src/index.js +5 -36
  107. package/packages/datadog-plugin-vitest/src/index.js +5 -5
  108. package/packages/datadog-plugin-ws/src/close.js +2 -1
  109. package/packages/datadog-plugin-ws/src/producer.js +2 -1
  110. package/packages/datadog-plugin-ws/src/receiver.js +2 -1
  111. package/packages/datadog-plugin-ws/src/server.js +2 -1
  112. package/packages/dd-trace/src/aiguard/sdk.js +1 -1
  113. package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +2 -2
  114. package/packages/dd-trace/src/appsec/iast/telemetry/span-tags.js +3 -1
  115. package/packages/dd-trace/src/azure_metadata.js +2 -1
  116. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js +93 -28
  117. package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +1 -1
  118. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/index.js +160 -0
  119. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/msgpack-to-json.js +288 -0
  120. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/payload-projection.js +84 -0
  121. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/sink.js +369 -0
  122. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/writer.js +60 -0
  123. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +41 -3
  124. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +7 -2
  125. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +1 -1
  126. package/packages/dd-trace/src/ci-visibility/requests/upload-coverage-report.js +6 -2
  127. package/packages/dd-trace/src/ci-visibility/rum.js +7 -0
  128. package/packages/dd-trace/src/ci-visibility/test-api-manual/test-api-manual-plugin.js +4 -0
  129. package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +1 -1
  130. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +137 -9
  131. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache.js +168 -8
  132. package/packages/dd-trace/src/ci-visibility/test-screenshot.js +90 -0
  133. package/packages/dd-trace/src/config/generated-config-types.d.ts +16 -0
  134. package/packages/dd-trace/src/config/helper.js +1 -0
  135. package/packages/dd-trace/src/config/index.js +10 -0
  136. package/packages/dd-trace/src/config/supported-configurations.json +66 -0
  137. package/packages/dd-trace/src/exporter.js +2 -0
  138. package/packages/dd-trace/src/exporters/common/client-library-headers.js +21 -0
  139. package/packages/dd-trace/src/exporters/common/request.js +59 -30
  140. package/packages/dd-trace/src/exporters/common/url.js +15 -1
  141. package/packages/dd-trace/src/feature-registry.js +10 -3
  142. package/packages/dd-trace/src/llmobs/experiments/client.js +113 -0
  143. package/packages/dd-trace/src/llmobs/experiments/dataset.js +154 -0
  144. package/packages/dd-trace/src/llmobs/experiments/experiment.js +283 -0
  145. package/packages/dd-trace/src/llmobs/experiments/index.js +152 -0
  146. package/packages/dd-trace/src/llmobs/experiments/noop.js +30 -0
  147. package/packages/dd-trace/src/llmobs/experiments/result.js +34 -0
  148. package/packages/dd-trace/src/llmobs/noop.js +6 -0
  149. package/packages/dd-trace/src/llmobs/plugins/ai/vercelTelemetry.js +20 -3
  150. package/packages/dd-trace/src/llmobs/plugins/anthropic/index.js +2 -2
  151. package/packages/dd-trace/src/llmobs/plugins/langgraph/index.js +1 -1
  152. package/packages/dd-trace/src/llmobs/plugins/openai/constants.js +8 -0
  153. package/packages/dd-trace/src/llmobs/plugins/openai/index.js +48 -14
  154. package/packages/dd-trace/src/llmobs/plugins/openai/utils.js +43 -0
  155. package/packages/dd-trace/src/llmobs/sdk.js +17 -0
  156. package/packages/dd-trace/src/llmobs/tagger.js +68 -0
  157. package/packages/dd-trace/src/llmobs/telemetry.js +2 -1
  158. package/packages/dd-trace/src/llmobs/util.js +32 -0
  159. package/packages/dd-trace/src/openfeature/agentless_configuration_source.js +322 -0
  160. package/packages/dd-trace/src/openfeature/configuration_source.js +90 -0
  161. package/packages/dd-trace/src/openfeature/flagging_provider.js +14 -23
  162. package/packages/dd-trace/src/openfeature/index.js +0 -2
  163. package/packages/dd-trace/src/openfeature/noop.js +1 -28
  164. package/packages/dd-trace/src/openfeature/register.js +16 -27
  165. package/packages/dd-trace/src/openfeature/remote_config.js +15 -18
  166. package/packages/dd-trace/src/openfeature/require-provider.js +18 -0
  167. package/packages/dd-trace/src/opentelemetry/span-ending-hook.js +10 -0
  168. package/packages/dd-trace/src/opentelemetry/span.js +5 -0
  169. package/packages/dd-trace/src/plugins/ci_plugin.js +20 -6
  170. package/packages/dd-trace/src/plugins/util/git.js +3 -2
  171. package/packages/dd-trace/src/plugins/util/inferred_proxy.js +29 -5
  172. package/packages/dd-trace/src/plugins/util/test.js +56 -4
  173. package/packages/dd-trace/src/profiling/profilers/space.js +1 -1
  174. package/packages/dd-trace/src/profiling/profilers/wall.js +1 -0
  175. package/packages/dd-trace/src/proxy.js +84 -7
  176. package/packages/dd-trace/src/span_processor.js +5 -0
  177. package/packages/dd-trace/src/standalone/index.js +0 -14
  178. package/packages/dd-trace/src/telemetry/send-data.js +2 -2
  179. package/packages/dd-trace/src/util.js +26 -0
  180. package/vendor/dist/@apm-js-collab/code-transformer/index.js +1 -1
  181. package/vendor/dist/@datadog/sketches-js/index.js +1 -1
  182. package/vendor/dist/protobufjs/index.js +1 -1
  183. package/vendor/dist/protobufjs/minimal/index.js +1 -1
  184. package/vendor/dist/shell-quote/index.js +1 -1
  185. package/packages/datadog-instrumentations/src/helpers/require-optional-peer.js +0 -17
@@ -1,6 +1,9 @@
1
1
  'use strict'
2
2
 
3
3
  const shimmer = require('../../datadog-shimmer')
4
+ const nomenclature = require('../../dd-trace/src/service-naming')
5
+ const spanEndingHook = require('../../dd-trace/src/opentelemetry/span-ending-hook')
6
+ const { RESOURCE_NAME } = require('../../../ext/tags')
4
7
  const { channel, addHook } = require('./helpers/instrument')
5
8
 
6
9
  const startChannel = channel('apm:next:request:start')
@@ -21,6 +24,27 @@ const NEXT_REQUEST_META = Symbol.for('NextInternalRequestMeta')
21
24
  const META_IS_MIDDLEWARE = 'middlewareInvoke'
22
25
  const encounteredMiddleware = new WeakSet()
23
26
 
27
+ // `next.span_type` value Next.js sets on its own OTel root request span; the whole detection surface.
28
+ const NEXT_BASE_SERVER_HANDLE_REQUEST = 'BaseServer.handleRequest'
29
+
30
+ // In OTel-bridge mode (`plugins: false` + `new tracer.TracerProvider().register()`) Next emits its
31
+ // own OTel spans and renames the root request span to `${method} ${route}` at finish, which the
32
+ // bridge routes into the DD operation name and leaves the resource as the bare method — the reverse
33
+ // of Datadog's contract. Correct it via the bridge's pre-finish hook. See span-ending-hook.js.
34
+ spanEndingHook.hook = (ddSpan) => {
35
+ const tags = ddSpan.context().getTags()
36
+ if (tags['next.span_type'] !== NEXT_BASE_SERVER_HANDLE_REQUEST) return
37
+
38
+ const method = tags['http.method']
39
+ const route = tags['next.route'] ?? tags['http.route']
40
+ // Next already wrote the RSC-aware `${method} ${route}` into `next.span_name`; prefer it so we
41
+ // mirror Next's own naming, and only construct the resource when it is absent.
42
+ const resource = tags['next.span_name'] ?? (route ? `${method} ${route}` : method)
43
+
44
+ ddSpan.setOperationName(nomenclature.opName('web', 'server', 'next'))
45
+ ddSpan.setTag(RESOURCE_NAME, resource)
46
+ }
47
+
24
48
  function wrapHandleRequest (handleRequest) {
25
49
  return function (req, res, pathname, query) {
26
50
  return instrument(req, res, () => handleRequest.apply(this, arguments))
@@ -84,8 +84,6 @@ addHook({
84
84
  onDone: resolve,
85
85
  })
86
86
  })
87
- }).catch(() => {
88
- // Ignore errors - report generation failed
89
87
  })
90
88
  }
91
89
 
@@ -144,8 +144,8 @@ addHook({ name: 'oracledb', versions: ['>=5'], file: 'lib/oracledb.js' }, oracle
144
144
  shimmer.wrap(oracledb.Pool.prototype, 'getConnection', getConnection => {
145
145
  return function wrappedGetConnection (...args) {
146
146
  let callback
147
- if (typeof args[args.length - 1] === 'function') {
148
- callback = args[args.length - 1]
147
+ if (typeof args.at(-1) === 'function') {
148
+ callback = args.at(-1)
149
149
  }
150
150
  if (callback) {
151
151
  args[args.length - 1] = shimmer.wrapFunction(callback, callback => (err, connection) => {
@@ -97,7 +97,7 @@ function wrapQuery (query) {
97
97
 
98
98
  // Based on: https://github.com/brianc/node-postgres/blob/54eb0fa216aaccd727765641e7d1cf5da2bc483d/packages/pg/lib/client.js#L510
99
99
  const reusingQuery = typeof pgQuery.submit === 'function'
100
- const callback = args[args.length - 1]
100
+ const callback = args.at(-1)
101
101
 
102
102
  finish(error)
103
103
 
@@ -197,7 +197,7 @@ function wrapPoolQuery (query) {
197
197
  const ctx = { query: pgQuery, abortController }
198
198
 
199
199
  return startPoolQueryCh.runStores(ctx, () => {
200
- const cb = args[args.length - 1]
200
+ const cb = args.at(-1)
201
201
 
202
202
  if (abortController.signal.aborted) {
203
203
  const error = abortController.signal.reason || new Error('Aborted')
@@ -25,7 +25,11 @@ const log = require('../../dd-trace/src/log')
25
25
  const {
26
26
  getValueFromEnvSources,
27
27
  } = require('../../dd-trace/src/config/helper')
28
+ const {
29
+ RUM_TEST_EXECUTION_ID_COOKIE_NAME: RUM_COOKIE_NAME,
30
+ } = require('../../dd-trace/src/ci-visibility/rum')
28
31
  const { DD_MAJOR } = require('../../../version')
32
+ const { getChannelPromise } = require('./helpers/channel')
29
33
  const { addHook, channel, tracingChannel } = require('./helpers/instrument')
30
34
 
31
35
  const testStartCh = channel('ci:playwright:test:start')
@@ -33,6 +37,7 @@ const testFinishCh = channel('ci:playwright:test:finish')
33
37
  const testSkipCh = channel('ci:playwright:test:skip')
34
38
 
35
39
  const testSessionStartCh = channel('ci:playwright:session:start')
40
+ const testSessionConfigurationCh = channel('ci:playwright:session:configuration')
36
41
  const testSessionFinishCh = channel('ci:playwright:session:finish')
37
42
 
38
43
  const libraryConfigurationCh = channel('ci:playwright:library-configuration')
@@ -51,6 +56,9 @@ const dispatcherRunCh = tracingChannel('orchestrion:playwright:Dispatcher_run')
51
56
  const dispatcherCreateWorkerCh = tracingChannel('orchestrion:playwright:Dispatcher_createWorker')
52
57
  const processHostStartRunnerCh = tracingChannel('orchestrion:playwright:ProcessHost_startRunner')
53
58
  const createRootSuiteCh = tracingChannel('orchestrion:playwright:createRootSuite')
59
+ const artifactsRecorderScreenshotPathCh =
60
+ tracingChannel('orchestrion:playwright:ArtifactsRecorder_createScreenshotAttachmentPath')
61
+ const snapshotRecorderScreenshotPathCh = tracingChannel('orchestrion:playwright:SnapshotRecorder_createAttachmentPath')
54
62
  const pageGotoCh = tracingChannel('orchestrion:playwright-core:Page_goto')
55
63
 
56
64
  const testToCtx = new WeakMap()
@@ -61,6 +69,8 @@ const testsToTestStatuses = new Map()
61
69
 
62
70
  const RUM_FLUSH_WAIT_TIME = getValueFromEnvSources('DD_CIVISIBILITY_RUM_FLUSH_WAIT_MILLIS')
63
71
  const DD_PROPERTIES_TIMEOUT = 5000
72
+ const isFailureScreenshotUploadEnabled =
73
+ getValueFromEnvSources('DD_TEST_FAILURE_SCREENSHOTS_ENABLED') === true
64
74
 
65
75
  let applyRepeatEachIndex = null
66
76
 
@@ -115,8 +125,21 @@ const EFD_RETRY_COUNT_RESPONSE = 'ddEfdRetryCountResponse'
115
125
  const DD_PROPERTIES_REQUEST = 'ddPropertiesRequest'
116
126
  const DD_PROPERTIES_RESPONSE = 'ddProperties'
117
127
  const kDdPlaywrightDisabledTestIds = Symbol('ddPlaywrightDisabledTestIds')
128
+ const kDdPlaywrightFailureScreenshots = Symbol('ddPlaywrightFailureScreenshots')
118
129
  const kDdPlaywrightWorkerHostInstrumented = Symbol('ddPlaywrightWorkerHostInstrumented')
119
130
  const kDdPlaywrightWorkerInstrumented = Symbol('ddPlaywrightWorkerInstrumented')
131
+ const PLAYWRIGHT_FAILURE_SCREENSHOT_PATH_RE = /(?:^|[\\/])test-failed-\d+\.png$/
132
+ const automaticFailureScreenshotPaths = new Set()
133
+
134
+ /**
135
+ * Returns whether Playwright's internal screenshot recorder created an attachment.
136
+ *
137
+ * @param {object} attachment - Playwright attachment payload
138
+ * @returns {boolean}
139
+ */
140
+ function isAutomaticFailureScreenshotAttachment (attachment) {
141
+ return typeof attachment?.path === 'string' && automaticFailureScreenshotPaths.delete(attachment.path)
142
+ }
120
143
 
121
144
  function isValidKnownTests (receivedKnownTests) {
122
145
  return !!receivedKnownTests.playwright
@@ -501,6 +524,23 @@ function getProjectsFromRunner (runner, configArg) {
501
524
  })
502
525
  }
503
526
 
527
+ /**
528
+ * Returns whether at least one Playwright project captures automatic screenshots for failed tests.
529
+ *
530
+ * @param {Array<object>} projects - Playwright projects with resolved use options
531
+ * @returns {boolean} Whether failure screenshot capture is enabled
532
+ */
533
+ function isFailureScreenshotCaptureEnabled (projects) {
534
+ for (const project of projects) {
535
+ const screenshot = project.use?.screenshot
536
+ const mode = typeof screenshot === 'object' && screenshot !== null ? screenshot.mode : screenshot
537
+ if (mode === 'on' || mode === 'only-on-failure' || mode === 'on-first-failure') {
538
+ return true
539
+ }
540
+ }
541
+ return false
542
+ }
543
+
504
544
  function getProjectsFromDispatcher (dispatcher) {
505
545
  const bundledConfig = dispatcher._testRun?.config?.config?.projects
506
546
  if (bundledConfig) {
@@ -575,12 +615,6 @@ function getTestByTestId (dispatcher, testId) {
575
615
  }
576
616
  }
577
617
 
578
- function getChannelPromise (channelToPublishTo, params) {
579
- return new Promise(resolve => {
580
- channelToPublishTo.publish({ onDone: resolve, ...params })
581
- })
582
- }
583
-
584
618
  // Inspired by https://github.com/microsoft/playwright/blob/2b77ed4d7aafa85a600caa0b0d101b72c8437eeb/packages/playwright/src/reporters/base.ts#L293
585
619
  // We can't use test.outcome() directly because it's set on follow up handlers:
586
620
  // our `testEndHandler` is called before the outcome is set.
@@ -993,6 +1027,7 @@ function onDispatcherCreateWorker (dispatcher, worker) {
993
1027
 
994
1028
  const projects = getProjectsFromDispatcher(dispatcher)
995
1029
  sessionProjects = projects
1030
+ const automaticFailureScreenshotPathsByTestId = new Map()
996
1031
 
997
1032
  if (disabledTestIds.size && !worker[kDdPlaywrightWorkerHostInstrumented] &&
998
1033
  typeof worker.runTestGroup === 'function') {
@@ -1021,6 +1056,16 @@ function onDispatcherCreateWorker (dispatcher, worker) {
1021
1056
  const shouldCreateTestSpan = test.expectedStatus === 'skipped'
1022
1057
  testBeginHandler(test, browser, shouldCreateTestSpan)
1023
1058
  })
1059
+ worker.on('attach', ({ testId, path, _ddIsAutomaticFailureScreenshot }) => {
1060
+ if (!_ddIsAutomaticFailureScreenshot) return
1061
+
1062
+ let screenshotPaths = automaticFailureScreenshotPathsByTestId.get(testId)
1063
+ if (!screenshotPaths) {
1064
+ screenshotPaths = new Set()
1065
+ automaticFailureScreenshotPathsByTestId.set(testId, screenshotPaths)
1066
+ }
1067
+ screenshotPaths.add(path)
1068
+ })
1024
1069
  worker.on('testEnd', ({ testId, status, errors, annotations }) => {
1025
1070
  const test = getTestByTestId(dispatcher, testId)
1026
1071
  if (!test) return
@@ -1043,6 +1088,20 @@ function onDispatcherCreateWorker (dispatcher, worker) {
1043
1088
  }
1044
1089
  )
1045
1090
  const testResult = test.results.at(-1)
1091
+ const automaticFailureScreenshotPaths = automaticFailureScreenshotPathsByTestId.get(testId)
1092
+ automaticFailureScreenshotPathsByTestId.delete(testId)
1093
+ if (testStatus === 'fail' && automaticFailureScreenshotPaths?.size && testResult?.attachments?.length) {
1094
+ const screenshots = []
1095
+ for (const attachment of testResult.attachments) {
1096
+ if (automaticFailureScreenshotPaths.has(attachment.path)) {
1097
+ screenshots.push(attachment)
1098
+ }
1099
+ }
1100
+ if (screenshots.length) {
1101
+ worker[kDdPlaywrightFailureScreenshots] ??= []
1102
+ worker[kDdPlaywrightFailureScreenshots].push(screenshots)
1103
+ }
1104
+ }
1046
1105
  const isAtrRetry = testResult?.retry > 0 &&
1047
1106
  isFlakyTestRetriesEnabled &&
1048
1107
  !test._ddIsAttemptToFix &&
@@ -1142,12 +1201,17 @@ function dispatcherHookNew (dispatcherExport, runWrapper) {
1142
1201
  function runAllTestsWrapper (runAllTests, playwrightVersion) {
1143
1202
  // Config parameter is only available from >=1.55.0
1144
1203
  return async function (config) {
1145
- let onDone
1146
-
1147
1204
  rootDir = getRootDir(this, config)
1205
+ const projects = getProjectsFromRunner(this, config)
1206
+ const isFailureScreenshotEnabled = isFailureScreenshotCaptureEnabled(projects)
1148
1207
  const processArgv = process.argv.slice(2).join(' ')
1149
1208
  const command = `playwright ${processArgv}`
1150
- testSessionStartCh.publish({ command, frameworkVersion: playwrightVersion, rootDir })
1209
+ testSessionStartCh.publish({
1210
+ command,
1211
+ frameworkVersion: playwrightVersion,
1212
+ rootDir,
1213
+ isFailureScreenshotEnabled,
1214
+ })
1151
1215
 
1152
1216
  try {
1153
1217
  const { err, libraryConfig } = await getChannelPromise(
@@ -1174,6 +1238,8 @@ function runAllTestsWrapper (runAllTests, playwrightVersion) {
1174
1238
  log.error('Playwright session start error', e)
1175
1239
  }
1176
1240
 
1241
+ testSessionConfigurationCh.publish({ isFailureScreenshotEnabled })
1242
+
1177
1243
  const isTestOptimizationSupported = satisfies(playwrightVersion, MINIMUM_SUPPORTED_VERSION_RANGE_EFD)
1178
1244
  const shouldGetKnownTests = isKnownTestsEnabled && isTestOptimizationSupported
1179
1245
  const shouldGetTestManagementTests = isTestManagementTestsEnabled && isTestOptimizationSupported
@@ -1239,8 +1305,6 @@ function runAllTestsWrapper (runAllTests, playwrightVersion) {
1239
1305
  }
1240
1306
  }
1241
1307
 
1242
- const projects = getProjectsFromRunner(this, config)
1243
-
1244
1308
  // ATR and `--retries` are now compatible with Test Management.
1245
1309
  // Test Management tests have their retries set to 0 at the test level,
1246
1310
  // preventing them from being retried by ATR or `--retries`.
@@ -1288,7 +1352,7 @@ function runAllTestsWrapper (runAllTests, playwrightVersion) {
1288
1352
 
1289
1353
  for (const [fqn, testStatuses] of testsToTestStatuses.entries()) {
1290
1354
  // Only count as failed if the final status (after retries) is 'fail'
1291
- const lastStatus = testStatuses[testStatuses.length - 1]
1355
+ const lastStatus = testStatuses.at(-1)
1292
1356
  if (lastStatus === 'fail') {
1293
1357
  totalFailedTestCount += 1
1294
1358
  if (quarantinedButNotAttemptToFixFqns.has(fqn)) {
@@ -1308,17 +1372,12 @@ function runAllTestsWrapper (runAllTests, playwrightVersion) {
1308
1372
  logTestOptimizationSummary({ attemptToFixExecutions, newTestsWithDynamicNames })
1309
1373
  loggedAttemptToFixTests.clear()
1310
1374
 
1311
- const flushWait = new Promise(resolve => {
1312
- onDone = resolve
1313
- })
1314
- testSessionFinishCh.publish({
1375
+ await getChannelPromise(testSessionFinishCh, {
1315
1376
  status: preventedToFail ? 'pass' : STATUS_TO_TEST_STATUS[sessionStatus],
1316
1377
  isEarlyFlakeDetectionEnabled,
1317
1378
  isEarlyFlakeDetectionFaulty,
1318
1379
  isTestManagementTestsEnabled,
1319
- onDone,
1320
1380
  })
1321
- await flushWait
1322
1381
 
1323
1382
  startedSuites = []
1324
1383
  remainingTestsByFile = {}
@@ -1439,6 +1498,23 @@ pageGotoCh.subscribe({
1439
1498
  },
1440
1499
  })
1441
1500
 
1501
+ /**
1502
+ * Records a path created by Playwright's automatic screenshot recorder.
1503
+ *
1504
+ * @param {object} ctx - Orchestrion context
1505
+ * @returns {void}
1506
+ */
1507
+ function recordAutomaticFailureScreenshotPath (ctx) {
1508
+ if (isFailureScreenshotUploadEnabled &&
1509
+ typeof ctx.result === 'string' &&
1510
+ PLAYWRIGHT_FAILURE_SCREENSHOT_PATH_RE.test(ctx.result)) {
1511
+ automaticFailureScreenshotPaths.add(ctx.result)
1512
+ }
1513
+ }
1514
+
1515
+ artifactsRecorderScreenshotPathCh.subscribe({ end: recordAutomaticFailureScreenshotPath })
1516
+ snapshotRecorderScreenshotPathCh.subscribe({ end: recordAutomaticFailureScreenshotPath })
1517
+
1442
1518
  if (DD_MAJOR < 6) { // <1.38.0 is only supported up to version 5
1443
1519
  addHook({
1444
1520
  name: '@playwright/test',
@@ -1750,7 +1826,10 @@ function finishProcessHostStartRunner (processHost) {
1750
1826
  }
1751
1827
  // These messages are [code, payload]. The payload is test data
1752
1828
  if (Array.isArray(message) && message[0] === PLAYWRIGHT_WORKER_TRACE_PAYLOAD_CODE) {
1753
- workerReportCh.publish(message[1])
1829
+ workerReportCh.publish({
1830
+ serializedTraces: message[1],
1831
+ screenshots: processHost[kDdPlaywrightFailureScreenshots]?.shift(),
1832
+ })
1754
1833
  }
1755
1834
  })
1756
1835
  }
@@ -1774,23 +1853,64 @@ addHook({
1774
1853
  })
1775
1854
 
1776
1855
  async function handlePageGoto (page) {
1856
+ if (!testPageGotoCh.hasSubscribers || !page || typeof page.evaluate !== 'function') {
1857
+ return
1858
+ }
1859
+
1860
+ let rumState
1777
1861
  try {
1778
- if (page && typeof page.evaluate === 'function') {
1779
- const { isRumInstrumented, isRumActive, rumSamplingRate } = await page.evaluate(detectRum)
1780
- if (isRumInstrumented && rumSamplingRate < 100 && !isRumActive) {
1781
- log.debug("RUM was detected on the page, but it isn't active because the sampling rate is below 100%")
1782
- }
1862
+ rumState = await page.evaluate(detectRum)
1863
+ } catch (error) {
1864
+ // Redirects and closed contexts can make page evaluation fail after a successful navigation.
1865
+ log.error('Playwright RUM detection error', error)
1866
+ return
1867
+ }
1783
1868
 
1784
- if (isRumActive) {
1785
- testPageGotoCh.publish({
1786
- isRumActive,
1787
- page,
1788
- })
1789
- }
1790
- }
1791
- } catch (e) {
1792
- // ignore errors such as redirects, context destroyed, etc
1793
- log.error('goto hook error', e)
1869
+ if (!rumState) {
1870
+ return
1871
+ }
1872
+
1873
+ const { isRumInstrumented, isRumActive, rumSamplingRate } = rumState
1874
+ if (isRumInstrumented && rumSamplingRate < 100 && !isRumActive) {
1875
+ log.debug("RUM was detected on the page, but it isn't active because the sampling rate is below 100%")
1876
+ }
1877
+ if (!isRumActive) {
1878
+ return
1879
+ }
1880
+
1881
+ let browserVersion
1882
+ try {
1883
+ browserVersion = page.context().browser()?.version()
1884
+ } catch (error) {
1885
+ log.error('Playwright browser metadata error', error)
1886
+ }
1887
+
1888
+ const context = {
1889
+ isRumActive,
1890
+ browserVersion,
1891
+ testExecutionId: undefined,
1892
+ }
1893
+ try {
1894
+ testPageGotoCh.publish(context)
1895
+ } catch (error) {
1896
+ log.error('Playwright RUM correlation channel error', error)
1897
+ return
1898
+ }
1899
+
1900
+ if (!context.testExecutionId) {
1901
+ return
1902
+ }
1903
+
1904
+ try {
1905
+ const domain = new URL(page.url()).hostname
1906
+ await page.context().addCookies([{
1907
+ name: RUM_COOKIE_NAME,
1908
+ value: context.testExecutionId,
1909
+ domain,
1910
+ path: '/',
1911
+ }])
1912
+ } catch (error) {
1913
+ log.error('Playwright RUM correlation cookie error', error)
1794
1914
  }
1795
1915
  }
1796
1916
 
@@ -1918,10 +2038,11 @@ function instrumentWorkerMainMethods (workerMain) {
1918
2038
  if (url) {
1919
2039
  const domain = new URL(url).hostname
1920
2040
  await page.context().addCookies([{
1921
- name: 'datadog-ci-visibility-test-execution-id',
2041
+ name: RUM_COOKIE_NAME,
1922
2042
  value: '',
1923
2043
  domain,
1924
2044
  path: '/',
2045
+ expires: 0,
1925
2046
  }])
1926
2047
  } else {
1927
2048
  log.error('RUM is active but page.url() is not available')
@@ -1979,12 +2100,6 @@ function instrumentWorkerMainMethods (workerMain) {
1979
2100
  annotationTags = parseAnnotations(annotations)
1980
2101
  }
1981
2102
 
1982
- let onDone
1983
-
1984
- const flushPromise = new Promise(resolve => {
1985
- onDone = resolve
1986
- })
1987
-
1988
2103
  // Wait for the properties to be received, but do not block the worker forever if IPC fails.
1989
2104
  const ddPropertiesTimeoutPromise = new Promise(resolve => {
1990
2105
  const ddPropertiesTimeout = realSetTimeout(() => {
@@ -2010,7 +2125,7 @@ function instrumentWorkerMainMethods (workerMain) {
2010
2125
  testStatus: STATUS_TO_TEST_STATUS[status],
2011
2126
  })
2012
2127
 
2013
- testFinishCh.publish({
2128
+ await getChannelPromise(testFinishCh, {
2014
2129
  testStatus: STATUS_TO_TEST_STATUS[status],
2015
2130
  steps: steps.filter(step => step.testId === testId),
2016
2131
  error,
@@ -2028,21 +2143,20 @@ function instrumentWorkerMainMethods (workerMain) {
2028
2143
  hasFailedAttemptToFixRetries: test._ddHasFailedAttemptToFixRetries,
2029
2144
  isAtrRetry: test._ddIsAtrRetry,
2030
2145
  isModified: test._ddIsModified,
2031
- onDone,
2032
2146
  finalStatus,
2033
2147
  earlyFlakeAbortReason: test._ddEarlyFlakeAbortReason,
2034
2148
  ...testCtx.currentStore,
2035
2149
  })
2036
2150
 
2037
- await flushPromise
2038
-
2039
2151
  return res
2040
2152
  })
2041
2153
 
2042
2154
  // We reproduce what happens in `Dispatcher#_onStepBegin` and `Dispatcher#_onStepEnd`,
2043
2155
  // since `startTime` and `duration` are not available directly in the worker process
2044
2156
  shimmer.wrap(workerMain, 'dispatchEvent', dispatchEvent => function (event, payload) {
2045
- if (event === 'stepBegin') {
2157
+ if (event === 'testBegin' || event === 'testEnd') {
2158
+ automaticFailureScreenshotPaths.clear()
2159
+ } else if (event === 'stepBegin') {
2046
2160
  stepInfoByStepId[payload.stepId] = {
2047
2161
  startTime: payload.wallTime,
2048
2162
  title: payload.title,
@@ -2058,6 +2172,8 @@ function instrumentWorkerMainMethods (workerMain) {
2058
2172
  duration: payload.wallTime - stepInfo.startTime,
2059
2173
  error: payload.error,
2060
2174
  })
2175
+ } else if (event === 'attach' && isAutomaticFailureScreenshotAttachment(payload)) {
2176
+ payload._ddIsAutomaticFailureScreenshot = true
2061
2177
  }
2062
2178
  return dispatchEvent.apply(this, arguments)
2063
2179
  })
@@ -1,5 +1,7 @@
1
1
  'use strict'
2
2
 
3
+ const { syncBuiltinESMExports } = require('node:module')
4
+
3
5
  const { channel } = require('dc-polyfill')
4
6
  const shimmer = require('../../datadog-shimmer')
5
7
 
@@ -26,4 +28,5 @@ if (process.setUncaughtExceptionCaptureCallback) {
26
28
  return result
27
29
  }
28
30
  })
31
+ syncBuiltinESMExports()
29
32
  }
@@ -86,26 +86,36 @@ function createLayerDispatchWrappers (name) {
86
86
  const finishChannel = channel(`apm:${name}:middleware:finish`)
87
87
  const errorChannel = channel(`apm:${name}:middleware:error`)
88
88
  const nextChannel = channel(`apm:${name}:middleware:next`)
89
+ const repeatChannel = channel(`apm:${name}:middleware:repeat`)
89
90
  // Bound per name so express and a bare router keep independent guards.
90
91
  const publishError = createErrorPublisher(errorChannel)
91
92
 
92
- function wrapNext (req, originalNext) {
93
+ /**
94
+ * @param {import('node:http').IncomingMessage} req
95
+ * @param {string | undefined} layerName
96
+ * @param {(error?: unknown) => void} originalNext
97
+ */
98
+ function wrapNext (req, layerName, originalNext) {
93
99
  // Per layer dispatch, N per request. Named `next`/arity-1 mirrors the
94
100
  // router continuation so wrapCallback skips its name/length rewrite.
95
- let published = false
101
+ let calls = 0
96
102
  return shimmer.wrapCallback(originalNext, original => function next (error) {
97
103
  // A handler that calls `next()` and then rejects (`next(); await bg()`)
98
104
  // makes the host call this continuation twice. Publish once so the second
99
105
  // pass cannot tag the already-finished span's parent with a late error.
100
- if (!published) {
101
- published = true
102
-
106
+ calls++
107
+ if (calls === 1) {
103
108
  if (error && error !== 'route' && error !== 'router') {
104
109
  publishError({ req, error })
105
110
  }
106
111
 
107
112
  nextChannel.publish({ req })
108
113
  finishChannel.publish({ req })
114
+ } else if (calls === 2) {
115
+ // Surface the repeat as a diagnostic on the still-live request span. The
116
+ // host cannot tell a legitimate `next(); await bg()` from a buggy double
117
+ // `next()`, so this only records that it happened, not that it is wrong.
118
+ repeatChannel.publish({ req, name: layerName, error })
109
119
  }
110
120
 
111
121
  original.apply(this, arguments)
@@ -125,7 +135,7 @@ function createLayerDispatchWrappers (name) {
125
135
  const meta = getLayerMeta(this)
126
136
  if (meta === undefined || this.handle.length > 3) return originalRequest.call(this, req, res, next)
127
137
 
128
- const wrappedNext = typeof next === 'function' ? wrapNext(req, next) : next
138
+ const wrappedNext = typeof next === 'function' ? wrapNext(req, meta.name, next) : next
129
139
  enterChannel.publish({ name: meta.name, req, route: resolveLayerRoute(meta, this), layer: this })
130
140
 
131
141
  try {
@@ -143,7 +153,7 @@ function createLayerDispatchWrappers (name) {
143
153
  const meta = getLayerMeta(this)
144
154
  if (meta === undefined || this.handle.length !== 4) return originalError.call(this, error, req, res, next)
145
155
 
146
- const wrappedNext = typeof next === 'function' ? wrapNext(req, next) : next
156
+ const wrappedNext = typeof next === 'function' ? wrapNext(req, meta.name, next) : next
147
157
  enterChannel.publish({ name: meta.name, req, route: resolveLayerRoute(meta, this), layer: this })
148
158
 
149
159
  try {
@@ -170,7 +180,7 @@ function createLayerDispatchWrappers (name) {
170
180
  const isErrorHandler = original.length === 4
171
181
  const req = args[isErrorHandler ? 1 : 0]
172
182
  const nextIndex = isErrorHandler ? 3 : 2
173
- if (typeof args[nextIndex] === 'function') args[nextIndex] = wrapNext(req, args[nextIndex])
183
+ if (typeof args[nextIndex] === 'function') args[nextIndex] = wrapNext(req, meta.name, args[nextIndex])
174
184
 
175
185
  enterChannel.publish({ name: meta.name, req, route: resolveLayerRoute(meta, layer), layer })
176
186
 
@@ -5,6 +5,10 @@ const realSetTimeout = setTimeout
5
5
 
6
6
  const shimmer = require('../../datadog-shimmer')
7
7
  const { getValueFromEnvSources } = require('../../dd-trace/src/config/helper')
8
+ const {
9
+ RUM_TEST_EXECUTION_ID_COOKIE_NAME: DD_CIVISIBILITY_TEST_EXECUTION_ID_COOKIE_NAME,
10
+ } = require('../../dd-trace/src/ci-visibility/rum')
11
+ const log = require('../../dd-trace/src/log')
8
12
  const { addHook, channel } = require('./helpers/instrument')
9
13
 
10
14
  const ciSeleniumDriverGetStartCh = channel('ci:selenium:driver:get')
@@ -20,7 +24,6 @@ if (window.DD_RUM && window.DD_RUM.stopSession) {
20
24
  const IS_RUM_ACTIVE_SCRIPT = 'return !!window.DD_RUM'
21
25
 
22
26
  const DD_CIVISIBILITY_RUM_FLUSH_WAIT_MILLIS = getValueFromEnvSources('DD_CIVISIBILITY_RUM_FLUSH_WAIT_MILLIS')
23
- const DD_CIVISIBILITY_TEST_EXECUTION_ID_COOKIE_NAME = 'datadog-ci-visibility-test-execution-id'
24
27
 
25
28
  // TODO: can we increase the supported version range?
26
29
  addHook({
@@ -32,28 +35,47 @@ addHook({
32
35
  if (!ciSeleniumDriverGetStartCh.hasSubscribers) {
33
36
  return get.apply(this, arguments)
34
37
  }
35
- let traceId
36
- const setTraceId = (inputTraceId) => {
37
- traceId = inputTraceId
38
- }
39
38
  const getResult = await get.apply(this, arguments)
40
39
 
41
- const isRumActive = await this.executeScript(IS_RUM_ACTIVE_SCRIPT)
42
- const capabilities = await this.getCapabilities()
40
+ let isRumActive
41
+ try {
42
+ isRumActive = await this.executeScript(IS_RUM_ACTIVE_SCRIPT)
43
+ } catch (error) {
44
+ log.error('Selenium RUM detection error', error)
45
+ }
46
+
47
+ let browserName
48
+ let browserVersion
49
+ try {
50
+ const capabilities = await this.getCapabilities()
51
+ browserName = capabilities.getBrowserName()
52
+ browserVersion = capabilities.getBrowserVersion()
53
+ } catch (error) {
54
+ log.error('Selenium browser metadata error', error)
55
+ }
43
56
 
44
- ciSeleniumDriverGetStartCh.publish({
45
- setTraceId,
57
+ const context = {
46
58
  seleniumVersion,
47
- browserName: capabilities.getBrowserName(),
48
- browserVersion: capabilities.getBrowserVersion(),
59
+ browserName,
60
+ browserVersion,
49
61
  isRumActive,
50
- })
62
+ testExecutionId: undefined,
63
+ }
64
+ try {
65
+ ciSeleniumDriverGetStartCh.publish(context)
66
+ } catch (error) {
67
+ log.error('Selenium RUM correlation channel error', error)
68
+ }
51
69
 
52
- if (traceId && isRumActive) {
53
- await this.manage().addCookie({
54
- name: DD_CIVISIBILITY_TEST_EXECUTION_ID_COOKIE_NAME,
55
- value: traceId,
56
- })
70
+ if (context.testExecutionId && isRumActive) {
71
+ try {
72
+ await this.manage().addCookie({
73
+ name: DD_CIVISIBILITY_TEST_EXECUTION_ID_COOKIE_NAME,
74
+ value: context.testExecutionId,
75
+ })
76
+ } catch (error) {
77
+ log.error('Selenium RUM correlation cookie error', error)
78
+ }
57
79
  }
58
80
 
59
81
  return getResult
@@ -63,16 +85,20 @@ addHook({
63
85
  if (!ciSeleniumDriverGetStartCh.hasSubscribers) {
64
86
  return quit.apply(this, arguments)
65
87
  }
66
- const isRumActive = await this.executeScript(RUM_STOP_SESSION_SCRIPT)
88
+ try {
89
+ const isRumActive = await this.executeScript(RUM_STOP_SESSION_SCRIPT)
67
90
 
68
- if (isRumActive) {
69
- // We'll have time for RUM to flush the events (there's no callback to know when it's done)
70
- await new Promise(resolve => {
71
- realSetTimeout(() => {
72
- resolve()
73
- }, DD_CIVISIBILITY_RUM_FLUSH_WAIT_MILLIS)
74
- })
75
- await this.manage().deleteCookie(DD_CIVISIBILITY_TEST_EXECUTION_ID_COOKIE_NAME)
91
+ if (isRumActive) {
92
+ // We'll have time for RUM to flush the events (there's no callback to know when it's done)
93
+ await new Promise(resolve => {
94
+ realSetTimeout(() => {
95
+ resolve()
96
+ }, DD_CIVISIBILITY_RUM_FLUSH_WAIT_MILLIS)
97
+ })
98
+ await this.manage().deleteCookie(DD_CIVISIBILITY_TEST_EXECUTION_ID_COOKIE_NAME)
99
+ }
100
+ } catch (error) {
101
+ log.error('Selenium RUM cleanup error', error)
76
102
  }
77
103
 
78
104
  return quit.apply(this, arguments)