dd-trace 5.96.0 → 5.97.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 (114) hide show
  1. package/index.d.ts +34 -0
  2. package/package.json +9 -7
  3. package/packages/datadog-esbuild/index.js +20 -9
  4. package/packages/datadog-instrumentations/src/child_process.js +7 -17
  5. package/packages/datadog-instrumentations/src/crypto.js +1 -2
  6. package/packages/datadog-instrumentations/src/cucumber.js +4 -1
  7. package/packages/datadog-instrumentations/src/cypress-config.js +324 -0
  8. package/packages/datadog-instrumentations/src/cypress.js +86 -4
  9. package/packages/datadog-instrumentations/src/dns.js +1 -2
  10. package/packages/datadog-instrumentations/src/express.js +4 -4
  11. package/packages/datadog-instrumentations/src/fs.js +27 -29
  12. package/packages/datadog-instrumentations/src/graphql.js +1 -1
  13. package/packages/datadog-instrumentations/src/helpers/bundler-register.js +41 -13
  14. package/packages/datadog-instrumentations/src/helpers/hook.js +31 -6
  15. package/packages/datadog-instrumentations/src/helpers/hooks.js +12 -19
  16. package/packages/datadog-instrumentations/src/helpers/instrument.js +27 -13
  17. package/packages/datadog-instrumentations/src/helpers/register.js +103 -142
  18. package/packages/datadog-instrumentations/src/http/client.js +2 -3
  19. package/packages/datadog-instrumentations/src/http/server.js +2 -5
  20. package/packages/datadog-instrumentations/src/http2/client.js +1 -3
  21. package/packages/datadog-instrumentations/src/http2/server.js +1 -3
  22. package/packages/datadog-instrumentations/src/jest.js +13 -4
  23. package/packages/datadog-instrumentations/src/limitd-client.js +1 -1
  24. package/packages/datadog-instrumentations/src/mocha/utils.js +4 -1
  25. package/packages/datadog-instrumentations/src/net.js +2 -8
  26. package/packages/datadog-instrumentations/src/pino.js +1 -1
  27. package/packages/datadog-instrumentations/src/playwright.js +4 -1
  28. package/packages/datadog-instrumentations/src/prisma.js +1 -2
  29. package/packages/datadog-instrumentations/src/selenium.js +4 -1
  30. package/packages/datadog-instrumentations/src/sequelize.js +1 -1
  31. package/packages/datadog-instrumentations/src/url.js +1 -3
  32. package/packages/datadog-instrumentations/src/vitest.js +5 -1
  33. package/packages/datadog-instrumentations/src/vm.js +1 -3
  34. package/packages/datadog-plugin-aws-sdk/src/base.js +4 -3
  35. package/packages/datadog-plugin-cucumber/src/index.js +7 -3
  36. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +57 -5
  37. package/packages/datadog-plugin-graphql/src/resolve.js +1 -1
  38. package/packages/datadog-plugin-jest/src/index.js +4 -2
  39. package/packages/datadog-plugin-kafkajs/src/batch-consumer.js +31 -4
  40. package/packages/datadog-plugin-mocha/src/index.js +5 -2
  41. package/packages/datadog-plugin-next/src/index.js +2 -14
  42. package/packages/datadog-plugin-openai/src/services.js +1 -0
  43. package/packages/datadog-webpack/index.js +3 -3
  44. package/packages/dd-trace/index.js +12 -10
  45. package/packages/dd-trace/src/agent/url.js +2 -2
  46. package/packages/dd-trace/src/aiguard/sdk.js +4 -0
  47. package/packages/dd-trace/src/appsec/blocking.js +3 -0
  48. package/packages/dd-trace/src/appsec/iast/iast-plugin.js +1 -1
  49. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +1 -1
  50. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +1 -1
  51. package/packages/dd-trace/src/appsec/remote_config.js +1 -0
  52. package/packages/dd-trace/src/appsec/sdk/index.js +4 -0
  53. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js +6 -1
  54. package/packages/dd-trace/src/ci-visibility/test-api-manual/test-api-manual-plugin.js +4 -0
  55. package/packages/dd-trace/src/config/defaults.js +316 -146
  56. package/packages/dd-trace/src/config/generated-config-types.d.ts +4 -1
  57. package/packages/dd-trace/src/config/helper.js +59 -10
  58. package/packages/dd-trace/src/config/index.js +569 -1505
  59. package/packages/dd-trace/src/config/parsers.js +256 -0
  60. package/packages/dd-trace/src/config/remote_config.js +59 -2
  61. package/packages/dd-trace/src/config/supported-configurations.json +350 -433
  62. package/packages/dd-trace/src/crashtracking/crashtracker.js +7 -1
  63. package/packages/dd-trace/src/crashtracking/index.js +1 -7
  64. package/packages/dd-trace/src/debugger/index.js +1 -1
  65. package/packages/dd-trace/src/dogstatsd.js +12 -9
  66. package/packages/dd-trace/src/encode/0.4.js +1 -1
  67. package/packages/dd-trace/src/exporters/agent/writer.js +7 -1
  68. package/packages/dd-trace/src/exporters/common/request.js +9 -0
  69. package/packages/dd-trace/src/exporters/common/writer.js +12 -2
  70. package/packages/dd-trace/src/heap_snapshots.js +3 -0
  71. package/packages/dd-trace/src/index.js +5 -2
  72. package/packages/dd-trace/src/lambda/runtime/ritm.js +6 -6
  73. package/packages/dd-trace/src/llmobs/index.js +4 -1
  74. package/packages/dd-trace/src/llmobs/plugins/ai/index.js +5 -1
  75. package/packages/dd-trace/src/llmobs/plugins/ai/util.js +60 -12
  76. package/packages/dd-trace/src/llmobs/plugins/bedrockruntime.js +4 -2
  77. package/packages/dd-trace/src/llmobs/sdk.js +12 -8
  78. package/packages/dd-trace/src/llmobs/span_processor.js +1 -1
  79. package/packages/dd-trace/src/llmobs/tagger.js +9 -6
  80. package/packages/dd-trace/src/llmobs/writers/base.js +2 -0
  81. package/packages/dd-trace/src/llmobs/writers/util.js +3 -0
  82. package/packages/dd-trace/src/log/index.js +26 -55
  83. package/packages/dd-trace/src/log/writer.js +7 -19
  84. package/packages/dd-trace/src/noop/proxy.js +8 -0
  85. package/packages/dd-trace/src/opentelemetry/logs/index.js +1 -1
  86. package/packages/dd-trace/src/opentelemetry/metrics/index.js +1 -1
  87. package/packages/dd-trace/src/opentracing/propagation/text_map.js +9 -4
  88. package/packages/dd-trace/src/payload-tagging/config/index.js +6 -5
  89. package/packages/dd-trace/src/plugin_manager.js +8 -6
  90. package/packages/dd-trace/src/plugins/ci_plugin.js +4 -0
  91. package/packages/dd-trace/src/plugins/plugin.js +7 -4
  92. package/packages/dd-trace/src/process-tags/index.js +3 -0
  93. package/packages/dd-trace/src/profiler.js +27 -2
  94. package/packages/dd-trace/src/profiling/config.js +73 -241
  95. package/packages/dd-trace/src/profiling/exporter_cli.js +1 -4
  96. package/packages/dd-trace/src/profiling/exporters/event_serializer.js +6 -2
  97. package/packages/dd-trace/src/profiling/profiler.js +56 -44
  98. package/packages/dd-trace/src/profiling/profilers/events.js +2 -3
  99. package/packages/dd-trace/src/profiling/profilers/wall.js +89 -6
  100. package/packages/dd-trace/src/profiling/ssi-heuristics.js +4 -1
  101. package/packages/dd-trace/src/propagation-hash/index.js +2 -1
  102. package/packages/dd-trace/src/proxy.js +32 -3
  103. package/packages/dd-trace/src/remote_config/index.js +3 -0
  104. package/packages/dd-trace/src/require-package-json.js +8 -4
  105. package/packages/dd-trace/src/ritm.js +58 -26
  106. package/packages/dd-trace/src/runtime_metrics/index.js +3 -0
  107. package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +3 -0
  108. package/packages/dd-trace/src/sampler.js +1 -1
  109. package/packages/dd-trace/src/standalone/index.js +3 -0
  110. package/packages/dd-trace/src/telemetry/index.js +2 -3
  111. package/packages/dd-trace/src/telemetry/send-data.js +5 -19
  112. package/packages/dd-trace/src/telemetry/session-propagation.js +19 -44
  113. package/packages/dd-trace/src/telemetry/telemetry.js +28 -171
  114. package/packages/dd-trace/src/util.js +0 -9
package/index.d.ts CHANGED
@@ -130,6 +130,11 @@ interface Tracer extends opentracing.Tracer {
130
130
 
131
131
  appsec: tracer.Appsec;
132
132
 
133
+ /**
134
+ * Profiling API for attaching custom labels to profiler samples.
135
+ */
136
+ profiling: tracer.Profiling;
137
+
133
138
  TracerProvider: tracer.opentelemetry.TracerProvider;
134
139
 
135
140
  dogstatsd: tracer.DogStatsD;
@@ -1570,6 +1575,35 @@ declare namespace tracer {
1570
1575
  trackUserLoginFailure(login: string, metadata?: any): void;
1571
1576
  }
1572
1577
 
1578
+ export interface Profiling {
1579
+ /**
1580
+ * Declares the set of custom label keys that will be used with
1581
+ * {@link runWithLabels}. This is used for profile upload metadata
1582
+ * (so the Datadog UI knows which keys to index for filtering) and
1583
+ * for pprof serialization optimization.
1584
+ *
1585
+ * @param keys Custom label key names.
1586
+ */
1587
+ setCustomLabelKeys(keys: Iterable<string>): void;
1588
+
1589
+ /**
1590
+ * Runs a function with custom profiling labels attached to all wall profiler
1591
+ * samples taken during its execution. Labels are key-value pairs that appear
1592
+ * in the pprof output and can be used to filter flame graphs in the Datadog UI.
1593
+ *
1594
+ * Requires AsyncContextFrame (ACF) to be enabled. Supports nesting: inner
1595
+ * calls merge labels with outer calls, with inner values taking precedence.
1596
+ *
1597
+ * When profiling is not enabled or ACF is not active, the function is still
1598
+ * called but labels are silently dropped.
1599
+ *
1600
+ * @param labels Custom labels to attach to profiler samples.
1601
+ * @param fn Function to execute with the labels.
1602
+ * @returns The return value of fn.
1603
+ */
1604
+ runWithLabels<T>(labels: Record<string, string | number>, fn: () => T): T;
1605
+ }
1606
+
1573
1607
  export interface Appsec {
1574
1608
  /**
1575
1609
  * Links a successful login event to the current trace. Will link the passed user to the current trace with Appsec.setUser() internally.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dd-trace",
3
- "version": "5.96.0",
3
+ "version": "5.97.0",
4
4
  "description": "Datadog APM tracing client for JavaScript",
5
5
  "main": "index.js",
6
6
  "typings": "index.d.ts",
@@ -11,6 +11,8 @@
11
11
  "bench": "node benchmark/index.js",
12
12
  "bench:e2e:test-optimization": "node benchmark/e2e-test-optimization/benchmark-run.js",
13
13
  "dependencies:dedupe": "yarn-deduplicate yarn.lock",
14
+ "generate:config:types": "node scripts/generate-config-types.js",
15
+ "verify:config:types": "node scripts/generate-config-types.js --check",
14
16
  "type:check": "tsc --noEmit -p tsconfig.dev.json",
15
17
  "type:doc:build": "cd docs && yarn && yarn build",
16
18
  "type:doc:test": "cd docs && yarn && yarn test",
@@ -142,7 +144,7 @@
142
144
  "import-in-the-middle": "^3.0.1"
143
145
  },
144
146
  "optionalDependencies": {
145
- "@datadog/libdatadog": "0.9.2",
147
+ "@datadog/libdatadog": "0.9.3",
146
148
  "@datadog/native-appsec": "11.0.1",
147
149
  "@datadog/native-iast-taint-tracking": "4.1.0",
148
150
  "@datadog/native-metrics": "3.1.1",
@@ -166,19 +168,19 @@
166
168
  "@types/mocha": "^10.0.10",
167
169
  "@types/node": "^18.19.106",
168
170
  "@types/sinon": "^21.0.0",
169
- "axios": "^1.13.4",
171
+ "axios": "^1.15.0",
170
172
  "benchmark": "^2.1.4",
171
173
  "body-parser": "^2.2.2",
172
174
  "bun": "1.3.11",
173
175
  "codeowners-audit": "^2.9.0",
174
176
  "eslint": "^9.39.2",
175
- "eslint-plugin-cypress": "^6.2.1",
177
+ "eslint-plugin-cypress": "^6.2.2",
176
178
  "eslint-plugin-import": "^2.32.0",
177
- "eslint-plugin-jsdoc": "^62.8.1",
179
+ "eslint-plugin-jsdoc": "^62.9.0",
178
180
  "eslint-plugin-mocha": "^11.2.0",
179
181
  "eslint-plugin-n": "^17.23.2",
180
182
  "eslint-plugin-promise": "^7.2.1",
181
- "eslint-plugin-unicorn": "^63.0.0",
183
+ "eslint-plugin-unicorn": "^64.0.0",
182
184
  "express": "^5.1.0",
183
185
  "glob": "^10.4.5",
184
186
  "globals": "^17.2.0",
@@ -200,7 +202,7 @@
200
202
  "semver": "^7.7.2",
201
203
  "sinon": "^21.0.3",
202
204
  "tiktoken": "^1.0.21",
203
- "typescript": "^5.9.2",
205
+ "typescript": "^6.0.2",
204
206
  "workerpool": "^10.0.0",
205
207
  "yaml": "^2.8.3",
206
208
  "yarn-deduplicate": "^6.0.2"
@@ -2,7 +2,6 @@
2
2
 
3
3
  const { execSync } = require('node:child_process')
4
4
  const fs = require('node:fs')
5
- const RAW_BUILTINS = require('node:module').builtinModules
6
5
  const path = require('node:path')
7
6
  const { pathToFileURL, fileURLToPath } = require('node:url')
8
7
 
@@ -25,15 +24,27 @@ for (const hook of Object.values(hooks)) {
25
24
  }
26
25
  }
27
26
 
27
+ function moduleOfInterestKey (name, file) {
28
+ return file ? `${name}/${file}` : name
29
+ }
30
+
31
+ const builtinModules = new Set(require('module').builtinModules)
32
+
33
+ function addModuleOfInterest (name, file) {
34
+ if (!name) return
35
+
36
+ modulesOfInterest.add(moduleOfInterestKey(name, file))
37
+
38
+ if (builtinModules.has(name)) {
39
+ modulesOfInterest.add(moduleOfInterestKey(`node:${name}`, file))
40
+ }
41
+ }
42
+
28
43
  const modulesOfInterest = new Set()
29
44
 
30
- for (const instrumentation of Object.values(instrumentations)) {
45
+ for (const [name, instrumentation] of Object.entries(instrumentations)) {
31
46
  for (const entry of instrumentation) {
32
- if (entry.file) {
33
- modulesOfInterest.add(`${entry.name}/${entry.file}`) // e.g. "redis/my/file.js"
34
- } else {
35
- modulesOfInterest.add(entry.name) // e.g. "redis"
36
- }
47
+ addModuleOfInterest(name, entry.file)
37
48
  }
38
49
  }
39
50
 
@@ -41,7 +52,7 @@ const CHANNEL = 'dd-trace:bundler:load'
41
52
 
42
53
  const builtins = new Set()
43
54
 
44
- for (const builtin of RAW_BUILTINS) {
55
+ for (const builtin of builtinModules) {
45
56
  builtins.add(builtin)
46
57
  builtins.add(`node:${builtin}`)
47
58
  }
@@ -247,7 +258,7 @@ ${build.initialOptions.banner.js}`
247
258
  }
248
259
 
249
260
  try {
250
- const packageJson = JSON.parse(fs.readFileSync(/** @type {string} */ (pathToPackageJson)).toString())
261
+ const packageJson = JSON.parse(fs.readFileSync(/** @type {string} */(pathToPackageJson)).toString())
251
262
 
252
263
  const isESM = isESMFile(fullPathToModule, pathToPackageJson, packageJson)
253
264
  if (isESM && !interceptedESMModules.has(fullPathToModule)) {
@@ -14,11 +14,6 @@ const childProcessChannel = dc.tracingChannel('datadog:child_process:execution')
14
14
  // ignored exec method because it calls to execFile directly
15
15
  const execAsyncMethods = ['execFile', 'spawn', 'fork']
16
16
 
17
- const names = ['child_process', 'node:child_process']
18
-
19
- // child_process and node:child_process returns the same object instance, we only want to add hooks once
20
- let patched = false
21
-
22
17
  function throwSyncError (error) {
23
18
  throw error
24
19
  }
@@ -37,19 +32,14 @@ function returnSpawnSyncError (error, context) {
37
32
  return context.result
38
33
  }
39
34
 
40
- for (const name of names) {
41
- addHook({ name }, childProcess => {
42
- if (!patched) {
43
- patched = true
44
- shimmer.massWrap(childProcess, execAsyncMethods, wrapChildProcessAsyncMethod(childProcess.ChildProcess))
45
- shimmer.wrap(childProcess, 'execSync', wrapChildProcessSyncMethod(throwSyncError, true))
46
- shimmer.wrap(childProcess, 'execFileSync', wrapChildProcessSyncMethod(throwSyncError))
47
- shimmer.wrap(childProcess, 'spawnSync', wrapChildProcessSyncMethod(returnSpawnSyncError))
48
- }
35
+ addHook({ name: 'child_process' }, childProcess => {
36
+ shimmer.massWrap(childProcess, execAsyncMethods, wrapChildProcessAsyncMethod(childProcess.ChildProcess))
37
+ shimmer.wrap(childProcess, 'execSync', wrapChildProcessSyncMethod(throwSyncError, true))
38
+ shimmer.wrap(childProcess, 'execFileSync', wrapChildProcessSyncMethod(throwSyncError))
39
+ shimmer.wrap(childProcess, 'spawnSync', wrapChildProcessSyncMethod(returnSpawnSyncError))
49
40
 
50
- return childProcess
51
- })
52
- }
41
+ return childProcess
42
+ })
53
43
 
54
44
  function normalizeArgs (args, shell) {
55
45
  const childProcessInfo = {
@@ -11,9 +11,8 @@ const cryptoCipherCh = channel('datadog:crypto:cipher:start')
11
11
 
12
12
  const hashMethods = ['createHash', 'createHmac', 'createSign', 'createVerify', 'sign', 'verify']
13
13
  const cipherMethods = ['createCipheriv', 'createDecipheriv']
14
- const names = ['crypto', 'node:crypto']
15
14
 
16
- addHook({ name: names }, crypto => {
15
+ addHook({ name: 'crypto' }, crypto => {
17
16
  shimmer.massWrap(crypto, hashMethods, wrapCryptoMethod(cryptoHashCh))
18
17
  shimmer.massWrap(crypto, cipherMethods, wrapCryptoMethod(cryptoCipherCh))
19
18
  return crypto
@@ -61,6 +61,8 @@ const numRetriesByPickleId = new Map()
61
61
  const numAttemptToCtx = new Map()
62
62
  const newTestsByTestFullname = new Map()
63
63
  const modifiedTestsByPickleId = new Map()
64
+ // Pickle IDs for tests that are genuinely new (not in known tests list).
65
+ const newTestPickleIds = new Set()
64
66
 
65
67
  let eventDataCollector = null
66
68
  let pickleByFile = {}
@@ -359,7 +361,7 @@ function wrapRun (pl, isLatestVersion, version) {
359
361
  }
360
362
 
361
363
  if (isKnownTestsEnabled && status !== 'skip') {
362
- isNew = numRetries !== undefined
364
+ isNew = newTestPickleIds.has(this.pickle.id)
363
365
  }
364
366
 
365
367
  if (isNew || isModified) {
@@ -714,6 +716,7 @@ function getWrappedRunTestCase (runTestCaseFunction, isNewerCucumberVersion = fa
714
716
  if (isKnownTestsEnabled && !isAttemptToFix) {
715
717
  isNew = isNewTest(testSuitePath, pickle.name)
716
718
  if (isNew) {
719
+ newTestPickleIds.add(pickle.id)
717
720
  numRetriesByPickleId.set(pickle.id, 0)
718
721
  }
719
722
  }
@@ -0,0 +1,324 @@
1
+ 'use strict'
2
+
3
+ const fs = require('fs')
4
+ const os = require('os')
5
+ const path = require('path')
6
+ const { pathToFileURL } = require('url')
7
+
8
+ const DD_CONFIG_WRAPPED = Symbol('dd-trace.cypress.config.wrapped')
9
+
10
+ const noopTask = {
11
+ 'dd:testSuiteStart': () => null,
12
+ 'dd:beforeEach': () => ({}),
13
+ 'dd:afterEach': () => null,
14
+ 'dd:addTags': () => null,
15
+ 'dd:log': () => null,
16
+ }
17
+
18
+ /**
19
+ * @param {unknown} value
20
+ * @returns {boolean}
21
+ */
22
+ function isPlainObject (value) {
23
+ if (!value || typeof value !== 'object') return false
24
+ const prototype = Object.getPrototypeOf(value)
25
+ return prototype === Object.prototype || prototype === null
26
+ }
27
+
28
+ /**
29
+ * Cypress allows setupNodeEvents to return partial config fragments that it
30
+ * diffs and merges into the resolved config. Preserve that behavior here so
31
+ * the wrapper does not drop user-provided config updates.
32
+ *
33
+ * @param {object} config Cypress resolved config object
34
+ * @param {unknown} updatedConfig value returned from setupNodeEvents
35
+ * @returns {object} resolved config with returned overrides applied
36
+ */
37
+ function mergeReturnedConfig (config, updatedConfig) {
38
+ if (!isPlainObject(updatedConfig) || updatedConfig === config) {
39
+ return config
40
+ }
41
+
42
+ const mergedConfig = { ...config }
43
+
44
+ for (const [key, value] of Object.entries(updatedConfig)) {
45
+ mergedConfig[key] = isPlainObject(value) && isPlainObject(mergedConfig[key])
46
+ ? mergeReturnedConfig(mergedConfig[key], value)
47
+ : value
48
+ }
49
+
50
+ return mergedConfig
51
+ }
52
+
53
+ /**
54
+ * Creates a temporary wrapper support file under os.tmpdir() that loads
55
+ * dd-trace's browser-side hooks before the user's original support file.
56
+ * Returns the wrapper path (for cleanup) or undefined if injection was skipped.
57
+ *
58
+ * @param {object} config Cypress resolved config object
59
+ * @returns {string|undefined} wrapper file path, or undefined if skipped
60
+ */
61
+ function injectSupportFile (config) {
62
+ const originalSupportFile = config.supportFile
63
+ if (!originalSupportFile || originalSupportFile === false) return
64
+
65
+ try {
66
+ const content = fs.readFileSync(originalSupportFile, 'utf8')
67
+ // Naive check: skip lines starting with // or * to avoid matching commented-out imports.
68
+ const hasActiveDdTraceImport = content.split('\n').some(line => {
69
+ const trimmed = line.trim()
70
+ return trimmed.includes('dd-trace/ci/cypress/support') &&
71
+ !trimmed.startsWith('//') && !trimmed.startsWith('*')
72
+ })
73
+ if (hasActiveDdTraceImport) return
74
+ } catch {
75
+ return
76
+ }
77
+
78
+ const ddSupportFile = require.resolve('../../../ci/cypress/support')
79
+ const wrapperFile = path.join(os.tmpdir(), `dd-cypress-support-${process.pid}.mjs`)
80
+
81
+ // Always use ESM: it can import both CJS and ESM support files.
82
+ const wrapperContent =
83
+ `import ${JSON.stringify(ddSupportFile)}\nimport ${JSON.stringify(originalSupportFile)}\n`
84
+
85
+ try {
86
+ fs.writeFileSync(wrapperFile, wrapperContent)
87
+ config.supportFile = wrapperFile
88
+ return wrapperFile
89
+ } catch {
90
+ // Can't write wrapper - skip injection
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Registers dd-trace's Cypress hooks (before:run, after:spec, after:run, tasks)
96
+ * and injects the support file. Handles chaining with user-registered handlers
97
+ * for after:spec/after:run so both the user's code and dd-trace's run in sequence.
98
+ *
99
+ * @param {Function} on Cypress event registration function
100
+ * @param {object} config Cypress resolved config object
101
+ * @param {Function[]} userAfterSpecHandlers user's after:spec handlers collected from wrappedOn
102
+ * @param {Function[]} userAfterRunHandlers user's after:run handlers collected from wrappedOn
103
+ * @returns {object} the config object (possibly modified)
104
+ */
105
+ function registerDdTraceHooks (on, config, userAfterSpecHandlers, userAfterRunHandlers) {
106
+ const wrapperFile = injectSupportFile(config)
107
+
108
+ const cleanupWrapper = () => {
109
+ if (wrapperFile) {
110
+ try { fs.unlinkSync(wrapperFile) } catch { /* best effort */ }
111
+ }
112
+ }
113
+
114
+ const tracer = global._ddtrace
115
+
116
+ const registerAfterRunWithCleanup = () => {
117
+ on('after:run', (results) => {
118
+ const chain = userAfterRunHandlers.reduce(
119
+ (p, h) => p.then(() => h(results)),
120
+ Promise.resolve()
121
+ )
122
+ return chain.finally(cleanupWrapper)
123
+ })
124
+ }
125
+
126
+ const registerNoopHandlers = () => {
127
+ for (const h of userAfterSpecHandlers) on('after:spec', h)
128
+ registerAfterRunWithCleanup()
129
+ on('task', noopTask)
130
+ }
131
+
132
+ if (!tracer || !tracer._initialized) {
133
+ registerNoopHandlers()
134
+ return config
135
+ }
136
+
137
+ const NoopTracer = require('../../../packages/dd-trace/src/noop/tracer')
138
+
139
+ if (tracer._tracer instanceof NoopTracer) {
140
+ registerNoopHandlers()
141
+ return config
142
+ }
143
+
144
+ const cypressPlugin = require('../../../packages/datadog-plugin-cypress/src/cypress-plugin')
145
+
146
+ if (cypressPlugin._isInit) {
147
+ for (const h of userAfterSpecHandlers) on('after:spec', h)
148
+ registerAfterRunWithCleanup()
149
+ return config
150
+ }
151
+
152
+ on('before:run', cypressPlugin.beforeRun.bind(cypressPlugin))
153
+
154
+ on('after:spec', (spec, results) => {
155
+ const chain = userAfterSpecHandlers.reduce(
156
+ (p, h) => p.then(() => h(spec, results)),
157
+ Promise.resolve()
158
+ )
159
+ return chain.then(() => cypressPlugin.afterSpec(spec, results))
160
+ })
161
+
162
+ on('after:run', (results) => {
163
+ const chain = userAfterRunHandlers.reduce(
164
+ (p, h) => p.then(() => h(results)),
165
+ Promise.resolve()
166
+ )
167
+ return chain
168
+ .then(() => cypressPlugin.afterRun(results))
169
+ .finally(cleanupWrapper)
170
+ })
171
+
172
+ on('task', cypressPlugin.getTasks())
173
+
174
+ return Promise.resolve(cypressPlugin.init(tracer, config)).then(() => config)
175
+ }
176
+
177
+ /**
178
+ * @param {Function|undefined} originalSetupNodeEvents
179
+ * @returns {Function}
180
+ */
181
+ function wrapSetupNodeEvents (originalSetupNodeEvents) {
182
+ return function ddSetupNodeEvents (on, config) {
183
+ const userAfterSpecHandlers = []
184
+ const userAfterRunHandlers = []
185
+
186
+ const wrappedOn = (event, handler) => {
187
+ if (event === 'after:spec') {
188
+ userAfterSpecHandlers.push(handler)
189
+ } else if (event === 'after:run') {
190
+ userAfterRunHandlers.push(handler)
191
+ } else {
192
+ on(event, handler)
193
+ }
194
+ }
195
+
196
+ const maybePromise = originalSetupNodeEvents
197
+ ? originalSetupNodeEvents.call(this, wrappedOn, config)
198
+ : undefined
199
+
200
+ if (maybePromise && typeof maybePromise.then === 'function') {
201
+ return maybePromise.then((result) => {
202
+ return registerDdTraceHooks(
203
+ on,
204
+ mergeReturnedConfig(config, result),
205
+ userAfterSpecHandlers,
206
+ userAfterRunHandlers
207
+ )
208
+ })
209
+ }
210
+
211
+ return registerDdTraceHooks(
212
+ on,
213
+ mergeReturnedConfig(config, maybePromise),
214
+ userAfterSpecHandlers,
215
+ userAfterRunHandlers
216
+ )
217
+ }
218
+ }
219
+
220
+ /**
221
+ * @param {object} config
222
+ * @returns {object}
223
+ */
224
+ function wrapConfig (config) {
225
+ if (!config || config[DD_CONFIG_WRAPPED]) return config
226
+ config[DD_CONFIG_WRAPPED] = true
227
+
228
+ if (config.e2e) {
229
+ config.e2e.setupNodeEvents = wrapSetupNodeEvents(config.e2e.setupNodeEvents)
230
+ }
231
+ if (config.component) {
232
+ config.component.setupNodeEvents = wrapSetupNodeEvents(config.component.setupNodeEvents)
233
+ }
234
+
235
+ return config
236
+ }
237
+
238
+ /**
239
+ * @param {string} originalConfigFile absolute path to the original config file
240
+ * @returns {string} path to the generated wrapper file
241
+ */
242
+ function createConfigWrapper (originalConfigFile) {
243
+ const wrapperFile = path.join(
244
+ path.dirname(originalConfigFile),
245
+ `.dd-cypress-config-${process.pid}.mjs`
246
+ )
247
+
248
+ const cypressConfigPath = require.resolve('./cypress-config')
249
+
250
+ // Always use ESM: it can import both CJS and ESM configs, so it works
251
+ // regardless of the original file's extension or "type": "module" in package.json.
252
+ // Import cypress-config.js directly (CJS default = module.exports object).
253
+ fs.writeFileSync(wrapperFile, [
254
+ `import originalConfig from ${JSON.stringify(pathToFileURL(originalConfigFile).href)}`,
255
+ `import cypressConfig from ${JSON.stringify(pathToFileURL(cypressConfigPath).href)}`,
256
+ '',
257
+ 'export default cypressConfig.wrapConfig(originalConfig)',
258
+ '',
259
+ ].join('\n'))
260
+
261
+ return wrapperFile
262
+ }
263
+
264
+ /**
265
+ * Wraps the Cypress config file for a CLI start() call. When an explicit
266
+ * configFile is provided, creates a temp wrapper that imports the original
267
+ * and passes it through wrapConfig. This handles ESM configs (.mjs) and
268
+ * plain-object configs (without defineConfig) that can't be intercepted
269
+ * via the defineConfig shimmer.
270
+ *
271
+ * @param {object|undefined} options
272
+ * @returns {{ options: object|undefined, cleanup: Function }}
273
+ */
274
+ function wrapCliConfigFileOptions (options) {
275
+ const noop = { options, cleanup: () => {} }
276
+
277
+ if (!options) return noop
278
+
279
+ const projectRoot = typeof options.project === 'string' ? options.project : process.cwd()
280
+ let configFilePath
281
+
282
+ if (options.configFile === false) {
283
+ // configFile: false means "no config file" — respect Cypress's semantics
284
+ return noop
285
+ } else if (typeof options.configFile === 'string') {
286
+ configFilePath = path.isAbsolute(options.configFile)
287
+ ? options.configFile
288
+ : path.resolve(projectRoot, options.configFile)
289
+ } else {
290
+ // No explicit --config-file: resolve the default cypress.config.{js,ts,cjs,mjs}
291
+ for (const ext of ['.js', '.ts', '.cjs', '.mjs']) {
292
+ const candidate = path.join(projectRoot, `cypress.config${ext}`)
293
+ if (fs.existsSync(candidate)) {
294
+ configFilePath = candidate
295
+ break
296
+ }
297
+ }
298
+ }
299
+
300
+ // Skip .ts files — Cypress transpiles them internally via its own loader.
301
+ // The ESM wrapper can't import .ts directly. The defineConfig shimmer
302
+ // handles .ts configs since they're transpiled to CJS by Cypress.
303
+ if (!configFilePath || !fs.existsSync(configFilePath) || path.extname(configFilePath) === '.ts') return noop
304
+
305
+ try {
306
+ const wrapperFile = createConfigWrapper(configFilePath)
307
+
308
+ return {
309
+ options: { ...options, configFile: wrapperFile },
310
+ cleanup: () => {
311
+ try { fs.unlinkSync(wrapperFile) } catch { /* best effort */ }
312
+ },
313
+ }
314
+ } catch {
315
+ // Config directory may be read-only — fall back to no wrapping.
316
+ // The defineConfig shimmer will still handle configs that use defineConfig.
317
+ return noop
318
+ }
319
+ }
320
+
321
+ module.exports = {
322
+ wrapCliConfigFileOptions,
323
+ wrapConfig,
324
+ }
@@ -1,11 +1,93 @@
1
1
  'use strict'
2
2
 
3
+ const shimmer = require('../../datadog-shimmer')
3
4
  const { DD_MAJOR } = require('../../../version')
4
5
  const { addHook } = require('./helpers/instrument')
6
+ const {
7
+ wrapCliConfigFileOptions,
8
+ wrapConfig,
9
+ } = require('./cypress-config')
5
10
 
6
- // No handler because this is only useful for testing.
7
- // Cypress plugin does not patch any library.
11
+ // Wrap defineConfig() so configs are instrumented when loaded in Cypress's
12
+ // config child process. This covers both CLI and programmatic usage with CJS configs.
8
13
  addHook({
9
14
  name: 'cypress',
10
- versions: DD_MAJOR >= 6 ? ['>=10.2.0'] : ['>=6.7.0'],
11
- }, lib => lib)
15
+ versions: ['>=10.2.0'],
16
+ }, (cypress) => {
17
+ if (typeof cypress.defineConfig === 'function') {
18
+ shimmer.wrap(cypress, 'defineConfig', (defineConfig) => function (config) {
19
+ wrapConfig(config)
20
+ return defineConfig(config)
21
+ })
22
+ }
23
+ return cypress
24
+ })
25
+
26
+ // Wrap the CLI entry points (cypress run / cypress open) to handle config files
27
+ // that can't be intercepted via the defineConfig shimmer: ESM configs (.mjs)
28
+ // and plain-object configs (without defineConfig).
29
+ function getCliStartWrapper (start) {
30
+ return function ddTraceCliStart (options) {
31
+ const { options: wrappedOptions, cleanup } = wrapCliConfigFileOptions(options)
32
+ const result = start.call(this, wrappedOptions)
33
+
34
+ if (result && typeof result.then === 'function') {
35
+ return result.finally(cleanup)
36
+ }
37
+
38
+ cleanup()
39
+ return result
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Wraps `start` on an object (or its `.default`) if present.
45
+ *
46
+ * @param {object} mod module exports
47
+ * @returns {object} mod
48
+ */
49
+ function wrapStartOnModule (mod) {
50
+ const target = mod.default || mod
51
+ if (typeof target.start === 'function') {
52
+ shimmer.wrap(target, 'start', getCliStartWrapper)
53
+ }
54
+ return mod
55
+ }
56
+
57
+ // Hook the CLI entry points where Cypress resolves and executes `run`/`open`.
58
+ // Cypress 10-14: lib/exec/{run,open}.js as separate files.
59
+ // Cypress 15-15.10: dist/exec/{run,open}.js as separate files.
60
+ // Cypress >=15.11: bundled into dist/cli-<hash>.js exporting runModule/openModule.
61
+ for (const file of ['lib/exec/run.js', 'lib/exec/open.js', 'dist/exec/run.js', 'dist/exec/open.js']) {
62
+ addHook({
63
+ name: 'cypress',
64
+ versions: ['>=10.2.0'],
65
+ file,
66
+ }, wrapStartOnModule)
67
+ }
68
+
69
+ // Cypress >=15.11 bundles run/open into a single CLI chunk (dist/cli-<hash>.js).
70
+ // The chunk exports runModule and openModule, each with a start() method.
71
+ addHook({
72
+ name: 'cypress',
73
+ versions: ['>=10.2.0'],
74
+ filePattern: 'dist/cli.*',
75
+ }, (cliChunk) => {
76
+ if (cliChunk.runModule?.start) {
77
+ shimmer.wrap(cliChunk.runModule, 'start', getCliStartWrapper)
78
+ }
79
+ if (cliChunk.openModule?.start) {
80
+ shimmer.wrap(cliChunk.openModule, 'start', getCliStartWrapper)
81
+ }
82
+ return cliChunk
83
+ })
84
+
85
+ // Cypress <10 uses the old pluginsFile approach. No auto-instrumentation;
86
+ // users must use the manual dd-trace/ci/cypress/plugin setup.
87
+ // This hook is kept so the plugin system registers Cypress for version tracking.
88
+ if (DD_MAJOR < 6) {
89
+ addHook({
90
+ name: 'cypress',
91
+ versions: ['>=6.7.0 <10.2.0'],
92
+ }, lib => lib)
93
+ }
@@ -18,9 +18,8 @@ const rrtypes = {
18
18
  }
19
19
 
20
20
  const rrtypeMap = new WeakMap()
21
- const names = ['dns', 'node:dns']
22
21
 
23
- addHook({ name: names }, dns => {
22
+ addHook({ name: 'dns' }, dns => {
24
23
  shimmer.wrap(dns, 'lookup', fn => wrap('apm:dns:lookup', fn, 2))
25
24
  shimmer.wrap(dns, 'lookupService', fn => wrap('apm:dns:lookup_service', fn, 2))
26
25
  shimmer.wrap(dns, 'resolve', fn => wrap('apm:dns:resolve', fn, 2))