dd-trace 6.4.0 → 6.5.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 (178) 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 +10 -7
  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 +20 -7
  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/hook.js +27 -6
  65. package/packages/datadog-instrumentations/src/helpers/optional-peer-bundler.js +8 -8
  66. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/playwright.js +28 -2
  67. package/packages/datadog-instrumentations/src/jest.js +26 -0
  68. package/packages/datadog-instrumentations/src/mariadb.js +1 -1
  69. package/packages/datadog-instrumentations/src/mocha/main.js +0 -2
  70. package/packages/datadog-instrumentations/src/mocha/utils.js +0 -4
  71. package/packages/datadog-instrumentations/src/mongodb.js +3 -3
  72. package/packages/datadog-instrumentations/src/mongoose.js +3 -3
  73. package/packages/datadog-instrumentations/src/mquery.js +2 -2
  74. package/packages/datadog-instrumentations/src/mysql.js +1 -1
  75. package/packages/datadog-instrumentations/src/next.js +24 -0
  76. package/packages/datadog-instrumentations/src/nyc.js +0 -2
  77. package/packages/datadog-instrumentations/src/oracledb.js +2 -2
  78. package/packages/datadog-instrumentations/src/pg.js +2 -2
  79. package/packages/datadog-instrumentations/src/playwright.js +159 -22
  80. package/packages/datadog-instrumentations/src/process.js +3 -0
  81. package/packages/datadog-instrumentations/src/router.js +18 -8
  82. package/packages/datadog-instrumentations/src/selenium.js +52 -26
  83. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +2 -2
  84. package/packages/datadog-instrumentations/src/vitest-main.js +0 -2
  85. package/packages/datadog-instrumentations/src/ws.js +2 -1
  86. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/context.js +2 -2
  87. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/util.js +2 -1
  88. package/packages/datadog-plugin-aws-sdk/src/util.js +2 -2
  89. package/packages/datadog-plugin-cucumber/src/index.js +1 -1
  90. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +19 -57
  91. package/packages/datadog-plugin-cypress/src/index.js +6 -1
  92. package/packages/datadog-plugin-cypress/src/support.js +92 -26
  93. package/packages/datadog-plugin-electron/src/ipc.js +1 -1
  94. package/packages/datadog-plugin-graphql/src/execute.js +87 -45
  95. package/packages/datadog-plugin-graphql/src/validate.js +2 -2
  96. package/packages/datadog-plugin-http2/src/client.js +1 -1
  97. package/packages/datadog-plugin-langchain/src/handlers/embedding.js +2 -1
  98. package/packages/datadog-plugin-langchain/src/handlers/language_models.js +2 -1
  99. package/packages/datadog-plugin-langchain/src/tokens.js +2 -2
  100. package/packages/datadog-plugin-playwright/src/index.js +177 -64
  101. package/packages/datadog-plugin-router/src/index.js +11 -2
  102. package/packages/datadog-plugin-selenium/src/index.js +5 -36
  103. package/packages/datadog-plugin-ws/src/close.js +2 -1
  104. package/packages/datadog-plugin-ws/src/producer.js +2 -1
  105. package/packages/datadog-plugin-ws/src/receiver.js +2 -1
  106. package/packages/datadog-plugin-ws/src/server.js +2 -1
  107. package/packages/dd-trace/src/aiguard/sdk.js +1 -1
  108. package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +2 -2
  109. package/packages/dd-trace/src/appsec/iast/telemetry/span-tags.js +3 -1
  110. package/packages/dd-trace/src/azure_metadata.js +2 -1
  111. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js +93 -28
  112. package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +1 -1
  113. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/index.js +160 -0
  114. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/msgpack-to-json.js +288 -0
  115. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/payload-projection.js +84 -0
  116. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/sink.js +369 -0
  117. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/writer.js +60 -0
  118. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +41 -3
  119. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +7 -2
  120. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +1 -1
  121. package/packages/dd-trace/src/ci-visibility/requests/upload-coverage-report.js +6 -2
  122. package/packages/dd-trace/src/ci-visibility/rum.js +7 -0
  123. package/packages/dd-trace/src/ci-visibility/test-api-manual/test-api-manual-plugin.js +4 -0
  124. package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +1 -1
  125. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +137 -9
  126. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache.js +168 -8
  127. package/packages/dd-trace/src/ci-visibility/test-screenshot.js +90 -0
  128. package/packages/dd-trace/src/config/generated-config-types.d.ts +16 -0
  129. package/packages/dd-trace/src/config/helper.js +1 -0
  130. package/packages/dd-trace/src/config/index.js +10 -0
  131. package/packages/dd-trace/src/config/supported-configurations.json +66 -0
  132. package/packages/dd-trace/src/exporter.js +2 -0
  133. package/packages/dd-trace/src/exporters/common/client-library-headers.js +21 -0
  134. package/packages/dd-trace/src/exporters/common/request.js +59 -30
  135. package/packages/dd-trace/src/exporters/common/url.js +15 -1
  136. package/packages/dd-trace/src/feature-registry.js +10 -3
  137. package/packages/dd-trace/src/llmobs/experiments/client.js +113 -0
  138. package/packages/dd-trace/src/llmobs/experiments/dataset.js +154 -0
  139. package/packages/dd-trace/src/llmobs/experiments/experiment.js +283 -0
  140. package/packages/dd-trace/src/llmobs/experiments/index.js +152 -0
  141. package/packages/dd-trace/src/llmobs/experiments/noop.js +30 -0
  142. package/packages/dd-trace/src/llmobs/experiments/result.js +34 -0
  143. package/packages/dd-trace/src/llmobs/noop.js +6 -0
  144. package/packages/dd-trace/src/llmobs/plugins/ai/vercelTelemetry.js +20 -3
  145. package/packages/dd-trace/src/llmobs/plugins/anthropic/index.js +2 -2
  146. package/packages/dd-trace/src/llmobs/plugins/langgraph/index.js +1 -1
  147. package/packages/dd-trace/src/llmobs/plugins/openai/constants.js +8 -0
  148. package/packages/dd-trace/src/llmobs/plugins/openai/index.js +48 -14
  149. package/packages/dd-trace/src/llmobs/plugins/openai/utils.js +43 -0
  150. package/packages/dd-trace/src/llmobs/sdk.js +17 -0
  151. package/packages/dd-trace/src/llmobs/tagger.js +68 -0
  152. package/packages/dd-trace/src/llmobs/telemetry.js +2 -1
  153. package/packages/dd-trace/src/llmobs/util.js +32 -0
  154. package/packages/dd-trace/src/openfeature/agentless_configuration_source.js +322 -0
  155. package/packages/dd-trace/src/openfeature/configuration_source.js +90 -0
  156. package/packages/dd-trace/src/openfeature/flagging_provider.js +14 -23
  157. package/packages/dd-trace/src/openfeature/index.js +0 -2
  158. package/packages/dd-trace/src/openfeature/noop.js +1 -28
  159. package/packages/dd-trace/src/openfeature/register.js +16 -27
  160. package/packages/dd-trace/src/openfeature/remote_config.js +15 -18
  161. package/packages/dd-trace/src/openfeature/require-provider.js +18 -0
  162. package/packages/dd-trace/src/opentelemetry/span-ending-hook.js +10 -0
  163. package/packages/dd-trace/src/opentelemetry/span.js +5 -0
  164. package/packages/dd-trace/src/plugins/ci_plugin.js +20 -6
  165. package/packages/dd-trace/src/plugins/util/git.js +3 -2
  166. package/packages/dd-trace/src/plugins/util/inferred_proxy.js +29 -5
  167. package/packages/dd-trace/src/plugins/util/test.js +56 -4
  168. package/packages/dd-trace/src/proxy.js +84 -7
  169. package/packages/dd-trace/src/span_processor.js +5 -0
  170. package/packages/dd-trace/src/standalone/index.js +0 -14
  171. package/packages/dd-trace/src/telemetry/send-data.js +2 -2
  172. package/packages/dd-trace/src/util.js +26 -0
  173. package/vendor/dist/@apm-js-collab/code-transformer/index.js +1 -1
  174. package/vendor/dist/@datadog/sketches-js/index.js +1 -1
  175. package/vendor/dist/protobufjs/index.js +1 -1
  176. package/vendor/dist/protobufjs/minimal/index.js +1 -1
  177. package/vendor/dist/shell-quote/index.js +1 -1
  178. package/packages/datadog-instrumentations/src/helpers/require-optional-peer.js +0 -17
@@ -1,12 +1,21 @@
1
1
  'use strict'
2
2
 
3
+ const { randomUUID } = require('crypto')
3
4
  const fs = require('fs')
4
- const os = require('os')
5
5
  const path = require('path')
6
6
  const { pathToFileURL } = require('url')
7
+
8
+ const log = require('../../dd-trace/src/log')
9
+ const { getSegment } = require('../../dd-trace/src/util')
7
10
  const { channel } = require('./helpers/instrument')
8
11
 
9
- const DD_CONFIG_WRAPPED = Symbol('dd-trace.cypress.config.wrapped')
12
+ const DD_CONFIG_WRAPPED = Symbol.for('dd-trace.cypress.config.wrapped')
13
+ const BROWSER_INSTRUMENTATION_NOT_INSTALLED =
14
+ 'Browser-side Cypress Test Optimization instrumentation was not installed.'
15
+ const CONFIG_INSTRUMENTATION_NOT_INSTALLED =
16
+ 'Cypress configurations that cannot be intercepted through cypress.defineConfig were not auto-instrumented.'
17
+ const generatedFilesForExitCleanup = new Set()
18
+ let exitCleanupRegistered = false
10
19
 
11
20
  const setupNodeEventsCh = channel('ci:cypress:setup-node-events')
12
21
 
@@ -27,6 +36,154 @@ const noopTask = {
27
36
  'dd:log': () => null,
28
37
  }
29
38
 
39
+ /** @typedef {Error & { code?: string, path?: string, syscall?: string }} FileSystemError */
40
+ /** @typedef {{ directory: string, error: FileSystemError }} FileCreationFailure */
41
+
42
+ /**
43
+ * @param {FileSystemError} error filesystem error
44
+ * @param {string} fallbackPath path used when the error does not include one
45
+ * @returns {string} concise error description
46
+ */
47
+ function formatFileSystemError (error, fallbackPath) {
48
+ const code = error?.code || error?.name || 'UNKNOWN'
49
+ const syscall = error?.syscall ? ` during ${error.syscall}` : ''
50
+ return `${code}${syscall} at ${error?.path || fallbackPath}`
51
+ }
52
+
53
+ /**
54
+ * @param {string} artifact artifact that could not be created
55
+ * @param {FileCreationFailure[]} failures failed directory attempts
56
+ * @param {string} consequence effect on Cypress instrumentation
57
+ * @param {boolean} [customerVisible] whether to report the failure without requiring debug logging
58
+ * @returns {void}
59
+ */
60
+ function warnFileCreationFailures (artifact, failures, consequence, customerVisible = false) {
61
+ const details = failures.map(({ directory, error }) => formatFileSystemError(error, directory)).join('; ')
62
+ const message = 'Datadog could not create %s. Attempts failed: %s. %s'
63
+
64
+ if (customerVisible) {
65
+ // eslint-disable-next-line no-console
66
+ console.error('ERROR: ' + message, artifact, details, consequence)
67
+ } else {
68
+ log.warn(message, artifact, details, consequence)
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Reports a definitive browser-instrumentation failure even when dd-trace debug logging is disabled.
74
+ *
75
+ * @param {string} message printf-style error message
76
+ * @param {...unknown} args message arguments
77
+ * @returns {void}
78
+ */
79
+ function logBrowserInstrumentationError (message, ...args) {
80
+ // eslint-disable-next-line no-console
81
+ console.error('ERROR: ' + message, ...args)
82
+ }
83
+
84
+ /**
85
+ * @param {string} filePath generated file to remove
86
+ * @returns {void}
87
+ */
88
+ function removeGeneratedFile (filePath) {
89
+ try {
90
+ fs.unlinkSync(filePath)
91
+ } catch (error) {
92
+ if (error?.code !== 'ENOENT') {
93
+ log.warn(
94
+ 'Datadog could not remove generated Cypress file %s: %s.',
95
+ filePath,
96
+ formatFileSystemError(error, filePath)
97
+ )
98
+ }
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Removes project-local support files when Cypress exits without firing
104
+ * after:run, as happens in open mode without experimental run events.
105
+ *
106
+ * @returns {void}
107
+ */
108
+ function removeGeneratedFilesAtExit () {
109
+ exitCleanupRegistered = false
110
+ for (const filePath of generatedFilesForExitCleanup) removeGeneratedFile(filePath)
111
+ generatedFilesForExitCleanup.clear()
112
+ }
113
+
114
+ /**
115
+ * @param {string[]} filePaths generated support files
116
+ * @returns {void}
117
+ */
118
+ function registerGeneratedFilesForExitCleanup (filePaths) {
119
+ for (const filePath of filePaths) generatedFilesForExitCleanup.add(filePath)
120
+ if (exitCleanupRegistered) return
121
+
122
+ exitCleanupRegistered = true
123
+ process.once('exit', removeGeneratedFilesAtExit)
124
+ }
125
+
126
+ /**
127
+ * @param {string[]} filePaths generated support files
128
+ * @returns {void}
129
+ */
130
+ function cleanupGeneratedFiles (filePaths) {
131
+ for (const filePath of filePaths) {
132
+ generatedFilesForExitCleanup.delete(filePath)
133
+ removeGeneratedFile(filePath)
134
+ }
135
+
136
+ if (generatedFilesForExitCleanup.size === 0 && exitCleanupRegistered) {
137
+ process.removeListener('exit', removeGeneratedFilesAtExit)
138
+ exitCleanupRegistered = false
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Writes a new file without following or overwriting an existing path. If the
144
+ * write fails after creation, removes the partial file before rethrowing.
145
+ *
146
+ * @param {string} filePath generated file path
147
+ * @param {string} content generated file content
148
+ * @returns {void}
149
+ */
150
+ function writeExclusiveFile (filePath, content) {
151
+ let descriptor
152
+ let operationError
153
+
154
+ try {
155
+ descriptor = fs.openSync(filePath, 'wx')
156
+ fs.writeFileSync(descriptor, content)
157
+ } catch (error) {
158
+ operationError = error
159
+ }
160
+
161
+ if (descriptor !== undefined) {
162
+ try {
163
+ fs.closeSync(descriptor)
164
+ } catch (error) {
165
+ if (!operationError) operationError = error
166
+ }
167
+ }
168
+
169
+ if (operationError) {
170
+ if (descriptor !== undefined) removeGeneratedFile(filePath)
171
+ throw operationError
172
+ }
173
+ }
174
+
175
+ /**
176
+ * @param {unknown} handler Cypress task registration
177
+ * @returns {boolean}
178
+ */
179
+ function isDatadogTaskRegistration (handler) {
180
+ return !!handler && typeof handler === 'object' &&
181
+ typeof handler['dd:testSuiteStart'] === 'function' &&
182
+ typeof handler['dd:beforeEach'] === 'function' &&
183
+ typeof handler['dd:afterEach'] === 'function' &&
184
+ typeof handler['dd:addTags'] === 'function'
185
+ }
186
+
30
187
  /**
31
188
  * @param {unknown} value
32
189
  * @returns {boolean}
@@ -63,46 +220,190 @@ function mergeReturnedConfig (config, updatedConfig) {
63
220
  }
64
221
 
65
222
  /**
66
- * Creates a temporary wrapper support file under os.tmpdir() that loads
67
- * dd-trace's browser-side hooks before the user's original support file.
68
- * Returns the wrapper path (for cleanup) or undefined if injection was skipped.
223
+ * @param {string} rootPath parent path
224
+ * @param {string} candidatePath path that should be inside rootPath
225
+ * @returns {boolean}
226
+ */
227
+ function isPathInside (rootPath, candidatePath) {
228
+ const relativePath = path.relative(path.resolve(rootPath), path.resolve(candidatePath))
229
+ return relativePath === '' || (!relativePath.startsWith(`..${path.sep}`) && relativePath !== '..')
230
+ }
231
+
232
+ /**
233
+ * @param {string} fromDirectory directory containing the importing file
234
+ * @param {string} importedFile file to import
235
+ * @returns {string}
236
+ */
237
+ function getRelativeImportPath (fromDirectory, importedFile) {
238
+ let relativePath = path.relative(fromDirectory, importedFile).split(path.sep).join('/')
239
+ if (!relativePath.startsWith('./') && !relativePath.startsWith('../')) {
240
+ relativePath = `./${relativePath}`
241
+ }
242
+ return relativePath
243
+ }
244
+
245
+ /**
246
+ * Creates project-local support files that Cypress's E2E and component
247
+ * bundlers can both serve. The browser hook is copied because an action-style
248
+ * NODE_OPTIONS preload can live outside Vite's allowed filesystem roots.
249
+ *
250
+ * @param {string} directory writable directory inside the Cypress project
251
+ * @param {string|false|undefined} originalSupportFile user's support file
252
+ * @param {string} browserHooksSource Datadog browser-side support hooks
253
+ * @returns {string[]} generated files
254
+ */
255
+ function createSupportWrapper (directory, originalSupportFile, browserHooksSource) {
256
+ const suffix = `${process.pid}-${randomUUID()}`
257
+ const browserHooksFile = path.join(directory, `dd-cypress-support-hooks-${suffix}.mjs`)
258
+ const wrapperFile = path.join(directory, `dd-cypress-support-${suffix}.mjs`)
259
+ const wrapperImports = [
260
+ `import ${JSON.stringify(getRelativeImportPath(directory, browserHooksFile))}`,
261
+ ]
262
+
263
+ if (originalSupportFile) {
264
+ wrapperImports.push(`import ${JSON.stringify(getRelativeImportPath(directory, originalSupportFile))}`)
265
+ }
266
+
267
+ const generatedFiles = []
268
+ try {
269
+ writeExclusiveFile(browserHooksFile, browserHooksSource)
270
+ generatedFiles.push(browserHooksFile)
271
+ writeExclusiveFile(wrapperFile, `${wrapperImports.join('\n')}\n`)
272
+ generatedFiles.push(wrapperFile)
273
+ } catch (error) {
274
+ for (const generatedFile of generatedFiles) removeGeneratedFile(generatedFile)
275
+ throw error
276
+ }
277
+
278
+ return generatedFiles
279
+ }
280
+
281
+ /**
282
+ * Creates temporary project-local support files that load dd-trace's
283
+ * browser-side hooks before the user's original support file. Returns the
284
+ * generated paths for cleanup, or undefined if injection was skipped.
69
285
  *
70
286
  * @param {object} config Cypress resolved config object
71
- * @returns {string|undefined} wrapper file path, or undefined if skipped
287
+ * @returns {string[]|undefined} generated file paths, or undefined if skipped
72
288
  */
73
289
  function injectSupportFile (config) {
74
290
  const originalSupportFile = config.supportFile
75
- if (!originalSupportFile || originalSupportFile === false) return
76
291
 
292
+ if (originalSupportFile) {
293
+ try {
294
+ const content = fs.readFileSync(originalSupportFile, 'utf8')
295
+ // Naive check: skip lines starting with // or * to avoid matching commented-out imports.
296
+ const hasActiveDdTraceImport = content.split('\n').some(line => {
297
+ const trimmed = line.trim()
298
+ return trimmed.includes('dd-trace/ci/cypress/support') &&
299
+ !trimmed.startsWith('//') && !trimmed.startsWith('*')
300
+ })
301
+ if (hasActiveDdTraceImport) return
302
+ } catch (error) {
303
+ logBrowserInstrumentationError(
304
+ 'Datadog could not read the Cypress support file %s: %s. %s',
305
+ originalSupportFile,
306
+ formatFileSystemError(error, originalSupportFile),
307
+ BROWSER_INSTRUMENTATION_NOT_INSTALLED
308
+ )
309
+ return
310
+ }
311
+ }
312
+
313
+ let browserHooksSource
314
+ let browserHooksPath
77
315
  try {
78
- const content = fs.readFileSync(originalSupportFile, 'utf8')
79
- // Naive check: skip lines starting with // or * to avoid matching commented-out imports.
80
- const hasActiveDdTraceImport = content.split('\n').some(line => {
81
- const trimmed = line.trim()
82
- return trimmed.includes('dd-trace/ci/cypress/support') &&
83
- !trimmed.startsWith('//') && !trimmed.startsWith('*')
84
- })
85
- if (hasActiveDdTraceImport) return
86
- } catch {
316
+ browserHooksPath = require.resolve('../../datadog-plugin-cypress/src/support')
317
+ browserHooksSource = fs.readFileSync(browserHooksPath, 'utf8')
318
+ } catch (error) {
319
+ logBrowserInstrumentationError(
320
+ 'Datadog could not read its Cypress browser support hooks: %s. %s',
321
+ formatFileSystemError(error, browserHooksPath || 'dd-trace Cypress browser support hooks'),
322
+ BROWSER_INSTRUMENTATION_NOT_INSTALLED
323
+ )
87
324
  return
88
325
  }
89
326
 
90
- const ddSupportFile = require.resolve('../../../ci/cypress/support')
91
- const wrapperFile = path.join(os.tmpdir(), `dd-cypress-support-${process.pid}.mjs`)
327
+ const projectRoot = config.projectRoot
328
+ const candidateDirectories = []
329
+ if (originalSupportFile) candidateDirectories.push(path.dirname(originalSupportFile))
330
+ if (projectRoot) candidateDirectories.push(projectRoot)
331
+ const failures = []
92
332
 
93
- // Always use ESM: it can import both CJS and ESM support files.
94
- const wrapperContent =
95
- `import ${JSON.stringify(ddSupportFile)}\nimport ${JSON.stringify(originalSupportFile)}\n`
333
+ for (const directory of new Set(candidateDirectories)) {
334
+ if (projectRoot && !isPathInside(projectRoot, directory)) continue
335
+ try {
336
+ const generatedFiles = createSupportWrapper(directory, originalSupportFile, browserHooksSource)
337
+ config.supportFile = generatedFiles[1]
338
+ return generatedFiles
339
+ } catch (error) {
340
+ failures.push({ directory, error })
341
+ // Try the next directory inside the project.
342
+ }
343
+ }
96
344
 
97
- try {
98
- fs.writeFileSync(wrapperFile, wrapperContent)
99
- config.supportFile = wrapperFile
100
- return wrapperFile
101
- } catch {
102
- // Can't write wrapper - skip injection
345
+ if (failures.length > 0) {
346
+ warnFileCreationFailures(
347
+ 'the Cypress support wrapper',
348
+ failures,
349
+ BROWSER_INSTRUMENTATION_NOT_INSTALLED,
350
+ true
351
+ )
352
+ } else {
353
+ logBrowserInstrumentationError(
354
+ 'Datadog could not create the Cypress support wrapper because no project directory was available. %s',
355
+ BROWSER_INSTRUMENTATION_NOT_INSTALLED
356
+ )
103
357
  }
104
358
  }
105
359
 
360
+ /**
361
+ * Registers screenshot handlers collected from a manual plugin call. User
362
+ * handlers run first so a renamed screenshot path reaches the Datadog handler.
363
+ *
364
+ * @param {Function} on Cypress event registration function
365
+ * @param {Function[]} handlers collected after:screenshot handlers
366
+ * @param {Function|undefined} datadogHandler manual Datadog screenshot handler
367
+ * @returns {void}
368
+ */
369
+ function registerManualAfterScreenshotHandlers (on, handlers, datadogHandler) {
370
+ const userHandlers = handlers.filter(handler => handler !== datadogHandler)
371
+ if (userHandlers.length === 0) {
372
+ if (datadogHandler) on('after:screenshot', datadogHandler)
373
+ return
374
+ }
375
+
376
+ on('after:screenshot', (details) => {
377
+ const chain = userHandlers.reduce(
378
+ (promise, handler) => promise.then((latestDetails) => Promise.resolve(handler(latestDetails)).then(
379
+ returned => (returned == null ? latestDetails : { ...latestDetails, ...returned })
380
+ )),
381
+ Promise.resolve(details)
382
+ )
383
+ return chain.then((finalDetails) => {
384
+ if (!datadogHandler) return finalDetails
385
+ return Promise.resolve(datadogHandler(finalDetails)).then(() => finalDetails)
386
+ })
387
+ })
388
+ }
389
+
390
+ /**
391
+ * Registers one Cypress after:spec handler that runs every collected handler
392
+ * in registration order. Cypress 10+ otherwise keeps only the last handler.
393
+ *
394
+ * @param {Function} on Cypress event registration function
395
+ * @param {Function[]} handlers collected after:spec handlers
396
+ * @returns {void}
397
+ */
398
+ function registerAfterSpecHandlers (on, handlers) {
399
+ if (handlers.length === 0) return
400
+
401
+ on('after:spec', (spec, results) => handlers.reduce(
402
+ (chain, handler) => chain.then(() => handler(spec, results)),
403
+ Promise.resolve()
404
+ ))
405
+ }
406
+
106
407
  /**
107
408
  * Registers dd-trace's Cypress hooks (before:run, after:spec, after:run, tasks)
108
409
  * and injects the support file. Communicates with the plugin layer via
@@ -114,15 +415,22 @@ function injectSupportFile (config) {
114
415
  * @param {Function[]} userAfterSpecHandlers user's after:spec handlers collected from wrappedOn
115
416
  * @param {Function[]} userAfterRunHandlers user's after:run handlers collected from wrappedOn
116
417
  * @param {Function[]} userAfterScreenshotHandlers user's after:screenshot handlers collected from wrappedOn
418
+ * @param {object} manualPlugin manual plugin registration state
117
419
  * @returns {object} the config object (possibly modified)
118
420
  */
119
- function registerDdTraceHooks (on, config, userAfterSpecHandlers, userAfterRunHandlers, userAfterScreenshotHandlers) {
120
- const wrapperFile = injectSupportFile(config)
421
+ function registerDdTraceHooks (
422
+ on,
423
+ config,
424
+ userAfterSpecHandlers,
425
+ userAfterRunHandlers,
426
+ userAfterScreenshotHandlers,
427
+ manualPlugin
428
+ ) {
429
+ const generatedSupportFiles = injectSupportFile(config)
430
+ if (generatedSupportFiles) registerGeneratedFilesForExitCleanup(generatedSupportFiles)
121
431
 
122
432
  const cleanupWrapper = () => {
123
- if (wrapperFile) {
124
- try { fs.unlinkSync(wrapperFile) } catch { /* best effort */ }
125
- }
433
+ if (generatedSupportFiles) cleanupGeneratedFiles(generatedSupportFiles)
126
434
  }
127
435
 
128
436
  const registerAfterRunWithCleanup = () => {
@@ -136,12 +444,19 @@ function registerDdTraceHooks (on, config, userAfterSpecHandlers, userAfterRunHa
136
444
  }
137
445
 
138
446
  const registerNoopHandlers = () => {
139
- for (const h of userAfterSpecHandlers) on('after:spec', h)
447
+ registerAfterSpecHandlers(on, userAfterSpecHandlers)
140
448
  for (const h of userAfterScreenshotHandlers) on('after:screenshot', h)
141
449
  registerAfterRunWithCleanup()
142
450
  on('task', noopTask)
143
451
  }
144
452
 
453
+ if (manualPlugin.detected) {
454
+ registerAfterSpecHandlers(on, userAfterSpecHandlers)
455
+ registerManualAfterScreenshotHandlers(on, userAfterScreenshotHandlers, manualPlugin.afterScreenshotHandler)
456
+ registerAfterRunWithCleanup()
457
+ return config
458
+ }
459
+
145
460
  if (!setupNodeEventsCh.hasSubscribers) {
146
461
  registerNoopHandlers()
147
462
  return config
@@ -180,6 +495,10 @@ function wrapSetupNodeEvents (originalSetupNodeEvents) {
180
495
  const userAfterSpecHandlers = []
181
496
  const userAfterRunHandlers = []
182
497
  const userAfterScreenshotHandlers = []
498
+ const manualPlugin = {
499
+ detected: false,
500
+ afterScreenshotHandler: undefined,
501
+ }
183
502
 
184
503
  const wrappedOn = (event, handler) => {
185
504
  if (event === 'after:spec') {
@@ -189,6 +508,10 @@ function wrapSetupNodeEvents (originalSetupNodeEvents) {
189
508
  } else if (event === 'after:screenshot') {
190
509
  userAfterScreenshotHandlers.push(handler)
191
510
  } else {
511
+ if (event === 'task' && isDatadogTaskRegistration(handler)) {
512
+ manualPlugin.detected = true
513
+ manualPlugin.afterScreenshotHandler = userAfterScreenshotHandlers.at(-1)
514
+ }
192
515
  on(event, handler)
193
516
  }
194
517
  }
@@ -204,7 +527,8 @@ function wrapSetupNodeEvents (originalSetupNodeEvents) {
204
527
  mergeReturnedConfig(config, result),
205
528
  userAfterSpecHandlers,
206
529
  userAfterRunHandlers,
207
- userAfterScreenshotHandlers
530
+ userAfterScreenshotHandlers,
531
+ manualPlugin
208
532
  )
209
533
  })
210
534
  }
@@ -214,7 +538,8 @@ function wrapSetupNodeEvents (originalSetupNodeEvents) {
214
538
  mergeReturnedConfig(config, maybePromise),
215
539
  userAfterSpecHandlers,
216
540
  userAfterRunHandlers,
217
- userAfterScreenshotHandlers
541
+ userAfterScreenshotHandlers,
542
+ manualPlugin
218
543
  )
219
544
  }
220
545
  }
@@ -261,30 +586,32 @@ function isUnderEsmPackage (filePath) {
261
586
 
262
587
  /**
263
588
  * @param {string} originalConfigFile absolute path to the original config file
589
+ * @param {string} wrapperDirectory directory for the generated wrapper
264
590
  * @returns {string} path to the generated wrapper file
265
591
  */
266
- function createConfigWrapper (originalConfigFile) {
267
- // Decide the wrapper's module mode (ESM vs CJS). It must match how
268
- // Cypress would interpret the user's original config so that (1) Cypress
269
- // keeps the loader it would have used (notably the ts-node registration
270
- // for `.ts` configs), and (2) the wrapper body parses in that mode.
592
+ function createConfigWrapper (originalConfigFile, wrapperDirectory) {
593
+ // Match the module mode Cypress would use for the user's original config
594
+ // so the generated wrapper body parses and imports it correctly.
271
595
  const originalExt = path.extname(originalConfigFile)
272
596
  const isEsm = originalExt === '.mjs' || originalExt === '.mts' ||
273
597
  (originalExt !== '.cjs' && originalExt !== '.cts' && isUnderEsmPackage(originalConfigFile))
274
598
 
275
- // Preserve `.ts`/`.cts`/`.mts` so Cypress keeps ts-node registered for
276
- // the wrapper. For plain JS originals, pick the extension that encodes
277
- // the chosen module mode directly.
599
+ // Preserve explicit TypeScript extensions. If an ambiguous `.ts` wrapper
600
+ // falls back across package scopes, make its original module mode explicit
601
+ // instead of inheriting the fallback scope.
278
602
  let wrapperExt
279
- if (originalExt === '.ts' || originalExt === '.cts' || originalExt === '.mts') {
603
+ if (originalExt === '.ts') {
604
+ const wrapperIsEsm = isUnderEsmPackage(path.join(wrapperDirectory, 'wrapper.ts'))
605
+ wrapperExt = wrapperIsEsm === isEsm ? originalExt : (isEsm ? '.mts' : '.cts')
606
+ } else if (originalExt === '.cts' || originalExt === '.mts') {
280
607
  wrapperExt = originalExt
281
608
  } else {
282
609
  wrapperExt = isEsm ? '.mjs' : '.cjs'
283
610
  }
284
611
 
285
612
  const wrapperFile = path.join(
286
- path.dirname(originalConfigFile),
287
- `.dd-cypress-config-${process.pid}${wrapperExt}`
613
+ wrapperDirectory,
614
+ `.dd-cypress-config-${process.pid}-${randomUUID()}${wrapperExt}`
288
615
  )
289
616
 
290
617
  const cypressConfigPath = require.resolve('./cypress-config')
@@ -314,7 +641,7 @@ function createConfigWrapper (originalConfigFile) {
314
641
  '',
315
642
  ].join('\n')
316
643
 
317
- fs.writeFileSync(wrapperFile, body)
644
+ writeExclusiveFile(wrapperFile, body)
318
645
  return wrapperFile
319
646
  }
320
647
 
@@ -327,7 +654,7 @@ function isTypeScript6OrNewer (projectRoot) {
327
654
  // eslint-disable-next-line n/no-unpublished-require
328
655
  const packageJsonPath = require.resolve('typescript/package.json', { paths: [projectRoot] })
329
656
  const { version } = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
330
- const major = Number(String(version).split('.')[0])
657
+ const major = Number(getSegment(String(version), '.', 0))
331
658
  return major >= 6
332
659
  } catch {
333
660
  return false
@@ -408,20 +735,39 @@ function wrapCliConfigFileOptions (options) {
408
735
 
409
736
  if (!configFilePath || !fs.existsSync(configFilePath)) return noop
410
737
 
738
+ let wrapperFile
739
+ const failures = []
740
+ for (const wrapperDirectory of new Set([path.dirname(configFilePath), projectRoot])) {
741
+ try {
742
+ wrapperFile = createConfigWrapper(configFilePath, wrapperDirectory)
743
+ break
744
+ } catch (error) {
745
+ failures.push({ directory: wrapperDirectory, error })
746
+ // Try the project root as the fallback location.
747
+ }
748
+ }
749
+
750
+ if (!wrapperFile) {
751
+ warnFileCreationFailures(
752
+ 'the Cypress configuration wrapper',
753
+ failures,
754
+ CONFIG_INSTRUMENTATION_NOT_INSTALLED
755
+ )
756
+ return noop
757
+ }
758
+
411
759
  try {
412
- const wrapperFile = createConfigWrapper(configFilePath)
413
760
  const restoreTsNodeCompilerOptions = configureTsNodeForTypeScript6(projectRoot, configFilePath)
414
761
 
415
762
  return {
416
763
  options: { ...options, configFile: wrapperFile },
417
764
  cleanup: () => {
418
765
  restoreTsNodeCompilerOptions()
419
- try { fs.unlinkSync(wrapperFile) } catch { /* best effort */ }
766
+ removeGeneratedFile(wrapperFile)
420
767
  },
421
768
  }
422
769
  } catch {
423
- // Config directory may be read-only — fall back to no wrapping.
424
- // The defineConfig shimmer will still handle configs that use defineConfig.
770
+ removeGeneratedFile(wrapperFile)
425
771
  return noop
426
772
  }
427
773
  }
@@ -58,17 +58,11 @@ function wrapAddHook (addHook) {
58
58
  if (typeof fn !== 'function') return addHook.apply(this, arguments)
59
59
 
60
60
  arguments[arguments.length - 1] = shimmer.wrapFunction(fn, fn => function wrappedHook () {
61
- // Fast path: every fastify request invokes each addHook'd handler, so the wrap
62
- // runs in the user's hot path. The only side effects this wrapper carries are
63
- // the three channels below; when none of them have a subscriber (the default
64
- // plugin config, and the steady state once appsec / cookie subscribers detach),
65
- // the wrap has nothing to do, and a `fn.apply(this, arguments)` forward keeps
66
- // V8's CallApplyArguments fast path intact.
67
- //
68
- // The previous shape mutated `arguments[arguments.length - 1]` to swap `done`.
69
- // That mutation materialises the magical arguments object and disables V8
70
- // inlining of the enclosing function. The slow path below builds a fresh args
71
- // array instead so the hot fast path keeps a clean forward.
61
+ // Every fastify request invokes each addHook'd handler, so this wrapper runs in the
62
+ // user's hot path. When none of the three channels below has a subscriber (the default
63
+ // plugin config, and the steady state once appsec / cookie subscribers detach), forward
64
+ // `arguments` untouched: no args array is materialised and V8's CallApplyArguments fast
65
+ // path stays intact. The slow path copies/indexes the args only when it has work to do.
72
66
  if (errorChannel.hasSubscribers || cookieParserReadCh.hasSubscribers || callbackFinishCh.hasSubscribers) {
73
67
  return invokeHookWithContext(name, fn, this, arguments)
74
68
  }
@@ -98,6 +92,8 @@ function invokeHookWithContext (name, fn, thisArg, args) {
98
92
  const ctx = { req }
99
93
 
100
94
  try {
95
+ // `args` is the wrapper's `arguments` object, which has no `Array#at`.
96
+ // eslint-disable-next-line unicorn/prefer-at
101
97
  const lastArg = args[args.length - 1]
102
98
 
103
99
  if (typeof lastArg === 'function') {
@@ -49,10 +49,19 @@ function Hook (modules, hookOptions, onrequire) {
49
49
  this._patched = Object.create(null)
50
50
  const patched = new WeakMap()
51
51
 
52
+ /**
53
+ * @param {object|Function|undefined} moduleExports
54
+ * @param {string} moduleName
55
+ * @param {string|undefined} moduleBaseDir
56
+ * @param {string|undefined} moduleVersion
57
+ * @param {boolean|undefined} isIitm
58
+ */
52
59
  const safeHook = (moduleExports, moduleName, moduleBaseDir, moduleVersion, isIitm) => {
53
60
  const parts = [moduleBaseDir, moduleName].filter(Boolean)
54
61
  const filename = path.join(...parts)
55
62
 
63
+ const defaultExport = isIitm && moduleExports.default
64
+ let defaultExportAliases
56
65
  let defaultWrapResult
57
66
 
58
67
  const wrappedOnrequire = (moduleExports, ...args) => {
@@ -77,17 +86,29 @@ function Hook (modules, hookOptions, onrequire) {
77
86
  }
78
87
 
79
88
  if (
80
- isIitm &&
81
- moduleExports.default &&
82
- (typeof moduleExports.default === 'object' ||
83
- typeof moduleExports.default === 'function')
89
+ defaultExport &&
90
+ (typeof defaultExport === 'object' ||
91
+ typeof defaultExport === 'function')
84
92
  ) {
85
- defaultWrapResult = wrappedOnrequire(moduleExports.default, moduleName, moduleBaseDir, moduleVersion, isIitm)
93
+ defaultWrapResult = wrappedOnrequire(defaultExport, moduleName, moduleBaseDir, moduleVersion, isIitm)
94
+ if (defaultWrapResult && defaultWrapResult !== defaultExport) {
95
+ defaultExportAliases = []
96
+ for (const exportName of Object.keys(moduleExports)) {
97
+ if (exportName !== 'default' && moduleExports[exportName] === defaultExport) {
98
+ defaultExportAliases.push(exportName)
99
+ }
100
+ }
101
+ }
86
102
  }
87
103
 
88
104
  const newExports = wrappedOnrequire(moduleExports, moduleName, moduleBaseDir, moduleVersion, isIitm)
89
105
 
90
- if (defaultWrapResult) newExports.default = defaultWrapResult
106
+ if (defaultWrapResult && defaultExportAliases) {
107
+ newExports.default = defaultWrapResult
108
+ for (const exportName of defaultExportAliases) {
109
+ moduleExports[exportName] = defaultWrapResult
110
+ }
111
+ }
91
112
 
92
113
  this._patched[filename] = true
93
114