dd-trace 5.99.0 → 5.100.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 (74) hide show
  1. package/LICENSE-3rdparty.csv +0 -1
  2. package/package.json +24 -5
  3. package/packages/datadog-instrumentations/src/cucumber.js +69 -5
  4. package/packages/datadog-instrumentations/src/express.js +3 -2
  5. package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
  6. package/packages/datadog-instrumentations/src/hono.js +15 -4
  7. package/packages/datadog-instrumentations/src/jest.js +89 -63
  8. package/packages/datadog-instrumentations/src/mocha/main.js +18 -22
  9. package/packages/datadog-instrumentations/src/mocha/utils.js +114 -96
  10. package/packages/datadog-instrumentations/src/mocha/worker.js +2 -2
  11. package/packages/datadog-instrumentations/src/path-to-regexp.js +44 -0
  12. package/packages/datadog-instrumentations/src/playwright.js +108 -18
  13. package/packages/datadog-instrumentations/src/router.js +53 -33
  14. package/packages/datadog-instrumentations/src/vitest.js +76 -30
  15. package/packages/datadog-plugin-aws-sdk/src/base.js +1 -1
  16. package/packages/datadog-plugin-aws-sdk/src/services/dynamodb.js +1 -1
  17. package/packages/datadog-plugin-bullmq/src/consumer.js +3 -2
  18. package/packages/datadog-plugin-bullmq/src/producer.js +25 -11
  19. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +32 -9
  20. package/packages/datadog-plugin-cypress/src/support.js +22 -21
  21. package/packages/datadog-plugin-dd-trace-api/src/index.js +1 -1
  22. package/packages/datadog-plugin-graphql/src/utils.js +2 -2
  23. package/packages/datadog-plugin-grpc/src/client.js +1 -1
  24. package/packages/datadog-plugin-grpc/src/server.js +1 -1
  25. package/packages/datadog-plugin-memcached/src/index.js +1 -1
  26. package/packages/datadog-plugin-mongodb-core/src/index.js +2 -3
  27. package/packages/datadog-plugin-playwright/src/index.js +6 -0
  28. package/packages/datadog-plugin-router/src/index.js +13 -0
  29. package/packages/dd-trace/index.js +4 -3
  30. package/packages/dd-trace/src/aiguard/sdk.js +2 -2
  31. package/packages/dd-trace/src/appsec/blocking.js +18 -6
  32. package/packages/dd-trace/src/appsec/graphql.js +1 -1
  33. package/packages/dd-trace/src/baggage.js +26 -13
  34. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +1 -1
  35. package/packages/dd-trace/src/config/generated-config-types.d.ts +45 -69
  36. package/packages/dd-trace/src/config/index.js +13 -12
  37. package/packages/dd-trace/src/config/normalize-service.js +31 -0
  38. package/packages/dd-trace/src/config/supported-configurations.json +31 -76
  39. package/packages/dd-trace/src/debugger/config.js +1 -1
  40. package/packages/dd-trace/src/dogstatsd.js +5 -8
  41. package/packages/dd-trace/src/encode/0.4.js +1 -1
  42. package/packages/dd-trace/src/encode/tags-processors.js +3 -3
  43. package/packages/dd-trace/src/exporter.js +1 -1
  44. package/packages/dd-trace/src/git_metadata_tagger.js +1 -1
  45. package/packages/dd-trace/src/heap_snapshots.js +4 -4
  46. package/packages/dd-trace/src/llmobs/constants/tags.js +3 -0
  47. package/packages/dd-trace/src/llmobs/sdk.js +21 -1
  48. package/packages/dd-trace/src/llmobs/span_processor.js +14 -1
  49. package/packages/dd-trace/src/llmobs/writers/base.js +7 -1
  50. package/packages/dd-trace/src/llmobs/writers/spans.js +1 -1
  51. package/packages/dd-trace/src/openfeature/eval-metrics-hook.js +2 -2
  52. package/packages/dd-trace/src/opentelemetry/context_manager.js +11 -8
  53. package/packages/dd-trace/src/opentelemetry/logs/index.js +5 -5
  54. package/packages/dd-trace/src/opentelemetry/metrics/index.js +6 -6
  55. package/packages/dd-trace/src/opentelemetry/span-helpers.js +170 -0
  56. package/packages/dd-trace/src/opentelemetry/span.js +14 -42
  57. package/packages/dd-trace/src/opentelemetry/trace/otlp_http_trace_exporter.js +1 -1
  58. package/packages/dd-trace/src/opentelemetry/tracer.js +11 -36
  59. package/packages/dd-trace/src/opentracing/propagation/text_map.js +44 -23
  60. package/packages/dd-trace/src/opentracing/propagation/tracestate.js +42 -12
  61. package/packages/dd-trace/src/opentracing/span.js +4 -3
  62. package/packages/dd-trace/src/plugin_manager.js +6 -6
  63. package/packages/dd-trace/src/plugins/log_plugin.js +1 -1
  64. package/packages/dd-trace/src/plugins/util/ci.js +119 -32
  65. package/packages/dd-trace/src/plugins/util/test.js +295 -29
  66. package/packages/dd-trace/src/profiling/ssi-heuristics.js +2 -2
  67. package/packages/dd-trace/src/propagation-hash/index.js +1 -1
  68. package/packages/dd-trace/src/proxy.js +9 -9
  69. package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +1 -1
  70. package/packages/dd-trace/src/span_processor.js +1 -1
  71. package/packages/dd-trace/src/telemetry/telemetry.js +7 -5
  72. package/packages/dd-trace/src/tracer_metadata.js +1 -1
  73. package/vendor/dist/path-to-regexp/LICENSE +0 -21
  74. package/vendor/dist/path-to-regexp/index.js +0 -1
@@ -85,7 +85,6 @@
85
85
  "node-gyp-build","https://github.com/prebuild/node-gyp-build","['MIT']","['Mathias Buus']"
86
86
  "opentracing","https://github.com/opentracing/opentracing-javascript","['Apache-2.0']","['opentracing']"
87
87
  "oxc-parser","https://github.com/oxc-project/oxc","['MIT']","['Boshen and oxc contributors']"
88
- "path-to-regexp","https://github.com/pillarjs/path-to-regexp","['MIT']","['pillarjs']"
89
88
  "pprof-format","https://github.com/DataDog/pprof-format","['MIT']","['Datadog Inc.']"
90
89
  "protobufjs","https://github.com/protobufjs/protobuf.js","['BSD-3-Clause']","['Daniel Wirtz']"
91
90
  "queue-tick","https://github.com/mafintosh/queue-tick","['MIT']","['Mathias Buus']"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dd-trace",
3
- "version": "5.99.0",
3
+ "version": "5.100.0",
4
4
  "description": "Datadog APM tracing client for JavaScript",
5
5
  "main": "index.js",
6
6
  "typings": "index.d.ts",
@@ -22,7 +22,7 @@
22
22
  "lint:fix": "node scripts/check_licenses.js && node scripts/check-no-coverage-artifacts.js && eslint . --concurrency=auto --max-warnings 0 --fix",
23
23
  "lint:inspect": "npx @eslint/config-inspector@latest",
24
24
  "lint:codeowners": "codeowners-audit",
25
- "lint:codeowners:ci": "codeowners-audit --glob='**/*.spec.js'",
25
+ "lint:codeowners:ci": "codeowners-audit --glob='**/*.spec.js' --glob='benchmark/sirun/**'",
26
26
  "release:proposal": "node scripts/release/proposal",
27
27
  "services": "node ./scripts/install_plugin_modules && node packages/dd-trace/test/setup/services",
28
28
  "test": "echo '\nError: The root \"npm test\" command is intentionally disabled.\n\nInstead, run specific test suites:\n - npm run test:trace:core\n - npm run test:appsec\n - etc.\n\nOr run individual test files:\n npx mocha path/to/test.spec.js\n\nSee CONTRIBUTING.md (Testing section) for more details.\n' && exit 1",
@@ -66,23 +66,39 @@
66
66
  "test:profiler": "node scripts/mocha-parallel-files.js --expose-gc --timeout 30000 -- \"packages/dd-trace/test/profiling/**/*.spec.js\"",
67
67
  "test:profiler:ci": "nyc --silent node init && nyc -- npm run test:profiler",
68
68
  "test:integration": "mocha --timeout 60000 \"integration-tests/*.spec.js\"",
69
+ "test:integration:coverage": "node ./integration-tests/coverage/run-suite.js --timeout 60000 \"integration-tests/*.spec.js\"",
69
70
  "test:integration:aiguard": "mocha --timeout 60000 \"integration-tests/aiguard/*.spec.js\"",
71
+ "test:integration:aiguard:coverage": "node ./integration-tests/coverage/run-suite.js --timeout 60000 \"integration-tests/aiguard/*.spec.js\"",
70
72
  "test:integration:appsec": "mocha --timeout 60000 \"integration-tests/appsec/*.spec.js\"",
73
+ "test:integration:appsec:coverage": "node ./integration-tests/coverage/run-suite.js --timeout 60000 \"integration-tests/appsec/*.spec.js\"",
71
74
  "test:integration:bun": "mocha --timeout 60000 \"integration-tests/bun/*.spec.js\"",
72
75
  "test:integration:cucumber": "mocha --timeout 60000 \"integration-tests/cucumber/*.spec.js\"",
76
+ "test:integration:cucumber:coverage": "node ./integration-tests/coverage/run-suite.js --timeout 60000 \"integration-tests/cucumber/*.spec.js\"",
73
77
  "test:integration:cypress": "mocha --timeout 60000 \"integration-tests/cypress/${SPEC:-cypress-*}.spec.js\"",
78
+ "test:integration:cypress:coverage": "node ./integration-tests/coverage/run-suite.js --timeout 60000 \"integration-tests/cypress/${SPEC:-cypress-*}.spec.js\"",
74
79
  "test:integration:debugger": "mocha --timeout 60000 \"integration-tests/debugger/*.spec.js\"",
80
+ "test:integration:debugger:coverage": "node ./integration-tests/coverage/run-suite.js --timeout 60000 \"integration-tests/debugger/*.spec.js\"",
75
81
  "test:integration:esbuild": "mocha --timeout 60000 \"integration-tests/esbuild/*.spec.js\"",
82
+ "test:integration:esbuild:coverage": "node ./integration-tests/coverage/run-suite.js --timeout 60000 \"integration-tests/esbuild/*.spec.js\"",
76
83
  "test:integration:webpack": "mocha --timeout 60000 \"integration-tests/webpack/*.spec.js\"",
77
84
  "test:integration:openfeature": "mocha --timeout 60000 \"integration-tests/openfeature/*.spec.js\"",
85
+ "test:integration:openfeature:coverage": "node ./integration-tests/coverage/run-suite.js --timeout 60000 \"integration-tests/openfeature/*.spec.js\"",
78
86
  "test:integration:jest": "mocha --timeout 60000 \"integration-tests/jest/*.spec.js\"",
87
+ "test:integration:jest:coverage": "node ./integration-tests/coverage/run-suite.js --timeout 60000 \"integration-tests/jest/*.spec.js\"",
79
88
  "test:integration:mocha": "mocha --timeout 60000 \"integration-tests/mocha/*.spec.js\"",
89
+ "test:integration:mocha:coverage": "node ./integration-tests/coverage/run-suite.js --timeout 60000 \"integration-tests/mocha/*.spec.js\"",
80
90
  "test:integration:playwright": "mocha --timeout 60000 \"integration-tests/playwright/${SPEC:-playwright-*}.spec.js\"",
91
+ "test:integration:playwright:coverage": "node ./integration-tests/coverage/run-suite.js --timeout 60000 \"integration-tests/playwright/${SPEC:-playwright-*}.spec.js\"",
81
92
  "test:integration:selenium": "mocha --timeout 60000 \"integration-tests/selenium/*.spec.js\"",
93
+ "test:integration:selenium:coverage": "node ./integration-tests/coverage/run-suite.js --timeout 60000 \"integration-tests/selenium/*.spec.js\"",
82
94
  "test:integration:vitest": "mocha --timeout 60000 \"integration-tests/vitest/*.spec.js\"",
95
+ "test:integration:vitest:coverage": "node ./integration-tests/coverage/run-suite.js --timeout 60000 \"integration-tests/vitest/*.spec.js\"",
83
96
  "test:integration:testopt": "mocha --timeout 120000 \"integration-tests/ci-visibility/*.spec.js\"",
97
+ "test:integration:testopt:coverage": "node ./integration-tests/coverage/run-suite.js --timeout 120000 \"integration-tests/ci-visibility/*.spec.js\"",
84
98
  "test:integration:profiler": "mocha --timeout 180000 \"integration-tests/profiler/*.spec.js\"",
85
- "test:integration:plugins": "mocha \"packages/datadog-plugin-@(${PLUGINS})/test/integration-test/**/*.spec.js\"",
99
+ "test:integration:profiler:coverage": "node ./integration-tests/coverage/run-suite.js --timeout 180000 \"integration-tests/profiler/*.spec.js\"",
100
+ "test:integration:plugins": "yarn services && mocha \"packages/datadog-plugin-@(${PLUGINS})/test/integration-test/**/${SPEC:-*}*.spec.js\"",
101
+ "test:integration:plugins:coverage": "yarn services && node ./integration-tests/coverage/run-suite.js \"packages/datadog-plugin-@(${PLUGINS})/test/integration-test/**/${SPEC:-*}*.spec.js\"",
86
102
  "test:unit:plugins": "mocha \"packages/datadog-instrumentations/test/@(${PLUGINS}).spec.js\" \"packages/datadog-plugin-@(${PLUGINS})/test/**/*.spec.js\" --exclude \"packages/datadog-plugin-@(${PLUGINS})/test/integration-test/**/*.spec.js\"",
87
103
  "test:shimmer": "mocha \"packages/datadog-shimmer/test/**/*.spec.js\"",
88
104
  "test:shimmer:ci": "nyc --silent node init && nyc -- npm run test:shimmer",
@@ -176,24 +192,27 @@
176
192
  "bun": "1.3.13",
177
193
  "codeowners-audit": "^2.9.0",
178
194
  "eslint": "^9.39.2",
179
- "eslint-plugin-cypress": "^6.3.1",
195
+ "eslint-plugin-cypress": "^6.4.0",
180
196
  "eslint-plugin-import": "^2.32.0",
181
197
  "eslint-plugin-jsdoc": "^62.9.0",
182
198
  "eslint-plugin-mocha": "^11.2.0",
183
199
  "eslint-plugin-n": "^17.23.2",
184
- "eslint-plugin-promise": "^7.2.1",
200
+ "eslint-plugin-promise": "^7.3.0",
185
201
  "eslint-plugin-unicorn": "^64.0.0",
186
202
  "express": "^5.1.0",
187
203
  "glob": "^10.4.5",
188
204
  "globals": "^17.2.0",
189
205
  "graphql": "*",
190
206
  "husky": "^9.1.7",
207
+ "istanbul-lib-report": "^3.0.0",
208
+ "istanbul-reports": "^3.0.2",
191
209
  "jszip": "^3.10.1",
192
210
  "mocha": "^11.6.0",
193
211
  "mocha-junit-reporter": "^2.2.1",
194
212
  "mocha-multi-reporters": "^1.5.1",
195
213
  "multer": "^2.1.1",
196
214
  "nock": "^13.5.6",
215
+ "node-preload": "^0.2.1",
197
216
  "nyc": "^18.0.0",
198
217
  "octokit": "^5.0.3",
199
218
  "opentracing": ">=0.14.7",
@@ -12,6 +12,10 @@ const {
12
12
  getTestSuitePath,
13
13
  CUCUMBER_WORKER_TRACE_PAYLOAD_CODE,
14
14
  getIsFaultyEarlyFlakeDetection,
15
+ recordAttemptToFixExecution,
16
+ collectAttemptToFixExecutionsFromTraces,
17
+ logAttemptToFixTestExecution,
18
+ logTestOptimizationSummary,
15
19
  } = require('../../dd-trace/src/plugins/util/test')
16
20
  const satisfies = require('../../../vendor/dist/semifies')
17
21
  const { addHook, channel } = require('./helpers/instrument')
@@ -60,9 +64,12 @@ const atrStatusesByScenarioKey = new Map()
60
64
  const numRetriesByPickleId = new Map()
61
65
  const numAttemptToCtx = new Map()
62
66
  const newTestsByTestFullname = new Map()
67
+ const attemptToFixTestsByTestFullname = new Map()
63
68
  const modifiedTestsByPickleId = new Map()
64
69
  // Pickle IDs for tests that are genuinely new (not in known tests list).
65
70
  const newTestPickleIds = new Set()
71
+ const attemptToFixExecutions = new Map()
72
+ const loggedAttemptToFixTests = new Set()
66
73
 
67
74
  let eventDataCollector = null
68
75
  let pickleByFile = {}
@@ -154,6 +161,16 @@ function getTestStatusFromRetries (testStatuses) {
154
161
  return 'pass'
155
162
  }
156
163
 
164
+ function getTestStatusFromAttemptToFixExecutions (testStatuses) {
165
+ if (testStatuses.every(status => status === 'pass')) {
166
+ return 'pass'
167
+ }
168
+ if (testStatuses.every(status => status === 'skip')) {
169
+ return 'skip'
170
+ }
171
+ return 'fail'
172
+ }
173
+
157
174
  function getErrorFromCucumberResult (cucumberResult) {
158
175
  if (!cucumberResult.message) {
159
176
  return
@@ -247,9 +264,8 @@ function getFinalStatus ({
247
264
  }) {
248
265
  // Note that intermediate executions DO NOT report a final status tag
249
266
 
250
- // If the test is quarantined or disabled, regardless of its actual execution result or active retry features,
251
- // the final status of its last execution should be reported as 'skip'.
252
- if (isQuarantined || isDisabled || status === 'skip') {
267
+ // If the test is quarantined or disabled, its final status is skip unless attempt-to-fix takes precedence.
268
+ if (status === 'skip' || (!isLastAttemptToFix && (isQuarantined || isDisabled))) {
253
269
  return 'skip'
254
270
  }
255
271
 
@@ -282,6 +298,7 @@ function wrapRun (pl, isLatestVersion, version) {
282
298
  let numAttempt = 0
283
299
 
284
300
  const testFileAbsolutePath = this.pickle.uri
301
+ const testSuitePath = getTestSuitePath(testFileAbsolutePath, process.cwd())
285
302
 
286
303
  const testSourceLine = this.gherkinDocument?.feature?.location?.line
287
304
 
@@ -293,6 +310,9 @@ function wrapRun (pl, isLatestVersion, version) {
293
310
  }
294
311
  const ctx = testStartPayload
295
312
  numAttemptToCtx.set(numAttempt, ctx)
313
+ if (isTestManagementTestsEnabled && getTestProperties(testSuitePath, this.pickle.name).attemptToFix) {
314
+ logAttemptToFixTestExecution(testSuitePath, this.pickle.name, loggedAttemptToFixTests)
315
+ }
296
316
  testStartCh.runStores(ctx, () => {})
297
317
  const promises = {}
298
318
  try {
@@ -441,6 +461,16 @@ function wrapRun (pl, isLatestVersion, version) {
441
461
 
442
462
  const error = getErrorFromCucumberResult(result)
443
463
 
464
+ if (isAttemptToFix) {
465
+ recordAttemptToFixExecution(attemptToFixExecutions, {
466
+ testSuite: testSuitePath,
467
+ testName: this.pickle.name,
468
+ status,
469
+ isDisabled,
470
+ isQuarantined,
471
+ })
472
+ }
473
+
444
474
  if (promises.hitBreakpointPromise) {
445
475
  await promises.hitBreakpointPromise
446
476
  }
@@ -661,6 +691,7 @@ function getWrappedStart (start, frameworkVersion, isParallel = false, isCoordin
661
691
  }
662
692
 
663
693
  atrStatusesByScenarioKey.clear()
694
+ attemptToFixTestsByTestFullname.clear()
664
695
  sessionStartCh.publish({ command, frameworkVersion })
665
696
 
666
697
  if (!errorSkippableRequest && skippedSuites.length) {
@@ -701,6 +732,8 @@ function getWrappedStart (start, frameworkVersion, isParallel = false, isCoordin
701
732
  isTestManagementTestsEnabled,
702
733
  isParallel,
703
734
  })
735
+ logTestOptimizationSummary({ attemptToFixExecutions })
736
+ loggedAttemptToFixTests.clear()
704
737
  eventDataCollector = null
705
738
  return success
706
739
  }
@@ -811,6 +844,7 @@ function getWrappedRunTestCase (runTestCaseFunction, isNewerCucumberVersion = fa
811
844
  let testStatus = lastTestStatus
812
845
  let shouldBePassedByEFD = false
813
846
  let shouldBePassedByTestManagement = false
847
+ let shouldBeFailedByAttemptToFix = false
814
848
  if ((isNew || isModified) && isEarlyFlakeDetectionEnabled) {
815
849
  /**
816
850
  * If Early Flake Detection (EFD) is enabled the logic is as follows:
@@ -827,7 +861,15 @@ function getWrappedRunTestCase (runTestCaseFunction, isNewerCucumberVersion = fa
827
861
  }
828
862
  }
829
863
 
830
- if (isTestManagementTestsEnabled && (isDisabled || isQuarantined)) {
864
+ if (isAttemptToFix && testStatuses.length === testManagementAttemptToFixRetries + 1) {
865
+ testStatus = getTestStatusFromAttemptToFixExecutions(testStatuses)
866
+ if (testStatus === 'fail') {
867
+ this.success = false
868
+ shouldBeFailedByAttemptToFix = true
869
+ }
870
+ }
871
+
872
+ if (isTestManagementTestsEnabled && !isAttemptToFix && (isDisabled || isQuarantined)) {
831
873
  this.success = true
832
874
  shouldBePassedByTestManagement = true
833
875
  }
@@ -863,10 +905,14 @@ function getWrappedRunTestCase (runTestCaseFunction, isNewerCucumberVersion = fa
863
905
  return shouldBePassedByEFD
864
906
  }
865
907
 
866
- if (isNewerCucumberVersion && isTestManagementTestsEnabled && (isQuarantined || isDisabled)) {
908
+ if (isNewerCucumberVersion && isTestManagementTestsEnabled && !isAttemptToFix && (isQuarantined || isDisabled)) {
867
909
  return shouldBePassedByTestManagement
868
910
  }
869
911
 
912
+ if (isNewerCucumberVersion && isAttemptToFix && shouldBeFailedByAttemptToFix) {
913
+ return false
914
+ }
915
+
870
916
  return runTestCaseResult
871
917
  }
872
918
  }
@@ -882,6 +928,7 @@ function getWrappedParseWorkerMessage (parseWorkerMessageFunction, isNewVersion)
882
928
  if (Array.isArray(message)) {
883
929
  const [messageCode, payload] = message
884
930
  if (messageCode === CUCUMBER_WORKER_TRACE_PAYLOAD_CODE) {
931
+ collectAttemptToFixExecutionsFromTraces(payload, attemptToFixExecutions)
885
932
  workerReportTraceCh.publish(payload)
886
933
  return
887
934
  }
@@ -962,6 +1009,23 @@ function getWrappedParseWorkerMessage (parseWorkerMessageFunction, isNewVersion)
962
1009
  // we only push to `finished` if the retries have finished
963
1010
  finished.push(newTestFinalStatus)
964
1011
  }
1012
+ } else if (
1013
+ isTestManagementTestsEnabled &&
1014
+ getTestProperties(getTestSuitePath(testFileAbsolutePath, process.cwd()), pickle.name).attemptToFix
1015
+ ) {
1016
+ const testFullname = `${pickle.uri}:${pickle.name}`
1017
+ let testStatuses = attemptToFixTestsByTestFullname.get(testFullname)
1018
+ if (testStatuses) {
1019
+ testStatuses.push(status)
1020
+ } else {
1021
+ testStatuses = [status]
1022
+ attemptToFixTestsByTestFullname.set(testFullname, testStatuses)
1023
+ }
1024
+
1025
+ if (status === 'skip' || testStatuses.length === testManagementAttemptToFixRetries + 1) {
1026
+ finished.push(getTestStatusFromAttemptToFixExecutions(testStatuses))
1027
+ attemptToFixTestsByTestFullname.delete(testFullname)
1028
+ }
965
1029
  } else {
966
1030
  // TODO: can we get error message?
967
1031
  const finished = pickleResultByFile[testFileAbsolutePath]
@@ -3,6 +3,7 @@
3
3
  const shimmer = require('../../datadog-shimmer')
4
4
  const { createWrapRouterMethod } = require('./router')
5
5
  const { addHook, channel, tracingChannel } = require('./helpers/instrument')
6
+ const { getCompileToRegexp } = require('./path-to-regexp')
6
7
  const {
7
8
  setRouterMountPath,
8
9
  markAppMounted,
@@ -26,8 +27,6 @@ function wrapHandle (handle) {
26
27
  }
27
28
  }
28
29
 
29
- const wrapRouterMethod = createWrapRouterMethod('express')
30
-
31
30
  const responseJsonChannel = channel('datadog:express:response:json:start')
32
31
 
33
32
  function wrapResponseJson (json) {
@@ -163,6 +162,8 @@ addHook({ name: 'express', versions: ['>=4'], file: 'lib/express.js' }, express
163
162
  // It would otherwise produce spans for router and express, and so duplicating them.
164
163
  // We now fall back to router instrumentation
165
164
  addHook({ name: 'express', versions: ['4'], file: 'lib/express.js' }, express => {
165
+ const wrapRouterMethod = createWrapRouterMethod('express', getCompileToRegexp())
166
+
166
167
  shimmer.wrap(express.Router, 'use', wrapRouterMethod)
167
168
  shimmer.wrap(express.Router, 'route', wrapRouterMethod)
168
169
 
@@ -118,6 +118,7 @@ module.exports = {
118
118
  passport: () => require('../passport'),
119
119
  'passport-http': () => require('../passport-http'),
120
120
  'passport-local': () => require('../passport-local'),
121
+ 'path-to-regexp': () => require('../path-to-regexp'),
121
122
  pg: () => require('../pg'),
122
123
  pino: () => require('../pino'),
123
124
  'pino-pretty': () => require('../pino'),
@@ -14,9 +14,15 @@ const enterChannel = channel('apm:hono:middleware:enter')
14
14
  const exitChannel = channel('apm:hono:middleware:exit')
15
15
  const finishChannel = channel('apm:hono:middleware:finish')
16
16
 
17
+ // `app.request()` and non-node adapters call `app.fetch` without an `incoming`
18
+ // IncomingMessage; the APM `web` helpers depend on one, so the wrappers below
19
+ // skip publishing whenever it is missing.
17
20
  function wrapFetch (fetch) {
18
21
  return function (request, env, executionCtx) {
19
- handleChannel.publish({ req: env.incoming })
22
+ const req = env?.incoming
23
+ if (req) {
24
+ handleChannel.publish({ req })
25
+ }
20
26
  return fetch.apply(this, arguments)
21
27
  }
22
28
  }
@@ -31,8 +37,10 @@ function wrapCompose (compose) {
31
37
 
32
38
  const instrumentedOnError = (...args) => {
33
39
  const [error, context] = args
34
- const req = context.env.incoming
35
- errorChannel.publish({ req, error })
40
+ const req = context.env?.incoming
41
+ if (req) {
42
+ errorChannel.publish({ req, error })
43
+ }
36
44
  return onError(...args)
37
45
  }
38
46
 
@@ -62,7 +70,10 @@ function wrapMiddleware (middleware, route) {
62
70
  middleware,
63
71
  (middleware) =>
64
72
  function (context, next) {
65
- const req = context.env.incoming
73
+ const req = context.env?.incoming
74
+ if (!req) {
75
+ return middleware.apply(this, arguments)
76
+ }
66
77
  routeChannel.publish({ req, route })
67
78
  enterChannel.publish({ req, name, route })
68
79
  if (typeof next === 'function') {
@@ -21,6 +21,9 @@ const {
21
21
  isModifiedTest,
22
22
  DYNAMIC_NAME_RE,
23
23
  collectDynamicNamesFromTraces,
24
+ recordAttemptToFixExecution,
25
+ logAttemptToFixTestExecution,
26
+ logTestOptimizationSummary,
24
27
  } = require('../../dd-trace/src/plugins/util/test')
25
28
  const {
26
29
  SEED_SUFFIX_RE,
@@ -78,7 +81,7 @@ let knownTests = {}
78
81
  let isCodeCoverageEnabled = false
79
82
  let isCodeCoverageEnabledBecauseOfUs = false
80
83
  let isSuitesSkippingEnabled = false
81
- let isKeepingCoverageConfiguration = false
84
+ let DD_TEST_TIA_KEEP_COV_CONFIG = false
82
85
  let isUserCodeCoverageEnabled = false
83
86
  let isSuitesSkipped = false
84
87
  let numSkippedSuites = 0
@@ -107,6 +110,7 @@ const wrappedWorkerChannels = new WeakMap()
107
110
  // New tests whose names contain likely dynamic data (timestamps, UUIDs, etc.)
108
111
  // Populated in-process for runInBand, and via worker-report:trace for parallel mode.
109
112
  const newTestsWithDynamicNames = new Set()
113
+ const loggedAttemptToFixTests = new Set()
110
114
  const testSuiteMockedFiles = new Map()
111
115
  const testsToBeRetried = new Set()
112
116
  // Per-test: how many EFD retries were determined after the first execution.
@@ -193,64 +197,88 @@ function getTestStats (testStatuses) {
193
197
  }
194
198
 
195
199
  /**
196
- * @param {string[]} efdNames
197
- * @param {string[]} quarantineNames
198
- * @param {number} totalCount
199
- */
200
- /**
201
- * Renders a truncated bullet list from an array of items.
200
+ * Formats the ignored-failure section for the Test Optimization summary.
202
201
  *
203
- * @param {Array<{ text: string, suffix?: string }>} items
202
+ * @param {{ efdNames: string[], quarantineNames: string[], totalCount: number } | undefined} ignoredFailures
204
203
  * @returns {string}
205
204
  */
206
- function formatList (items) {
205
+ function formatIgnoredFailuresSummary (ignoredFailures) {
206
+ if (!ignoredFailures) return ''
207
+
208
+ const items = []
209
+
210
+ for (const n of ignoredFailures.efdNames) {
211
+ items.push({ text: n, suffix: 'Early Flake Detection' })
212
+ }
213
+ for (const n of ignoredFailures.quarantineNames) {
214
+ items.push({ text: n, suffix: 'Quarantine' })
215
+ }
216
+
217
+ if (items.length === 0 || ignoredFailures.totalCount <= 0) return ''
218
+
207
219
  const shown = items.slice(0, MAX_IGNORED_TEST_NAMES)
208
220
  const more = items.length - shown.length
209
221
  const moreSuffix = more > 0 ? `\n ... and ${more} more` : ''
210
- return shown.map(({ text, suffix }) => ` • ${text}${suffix ? ` (${suffix})` : ''}`).join('\n') + moreSuffix
222
+ const formattedItems = shown
223
+ .map(({ text, suffix }) => ` • ${text}${suffix ? ` (${suffix})` : ''}`)
224
+ .join('\n') + moreSuffix
225
+
226
+ return `${ignoredFailures.totalCount} test failure(s) were ignored. Exit code set to 0.\n\n${formattedItems}`
211
227
  }
212
228
 
213
229
  /**
214
- * Logs a single "Datadog Test Optimization" summary at session end,
215
- * combining all relevant sections (ignored failures, dynamic names).
230
+ * Logs a single "Datadog Test Optimization" summary at session end.
216
231
  *
217
232
  * @param {{ efdNames: string[], quarantineNames: string[], totalCount: number } | undefined} ignoredFailures
218
233
  */
219
- function logSessionSummary (ignoredFailures) {
220
- const sections = []
234
+ function logSessionSummary (ignoredFailures, attemptToFixExecutions) {
235
+ logTestOptimizationSummary({
236
+ attemptToFixExecutions,
237
+ extraSections: [formatIgnoredFailuresSummary(ignoredFailures)],
238
+ newTestsWithDynamicNames,
239
+ })
240
+ loggedAttemptToFixTests.clear()
241
+ }
221
242
 
222
- if (ignoredFailures) {
223
- const items = []
224
- for (const n of ignoredFailures.efdNames) {
225
- items.push({ text: n, suffix: 'Early Flake Detection' })
226
- }
227
- for (const n of ignoredFailures.quarantineNames) {
228
- items.push({ text: n, suffix: 'Quarantine' })
229
- }
230
- sections.push(
231
- `${ignoredFailures.totalCount} test failure(s) were ignored. Exit code set to 0.\n\n` +
232
- formatList(items)
233
- )
234
- }
243
+ function getTestStatusFromJestResult (status) {
244
+ if (status === 'failed') return 'fail'
245
+ if (status === 'passed') return 'pass'
246
+ }
235
247
 
236
- if (newTestsWithDynamicNames.size > 0) {
237
- const items = [...newTestsWithDynamicNames].map(name => ({ text: name }))
238
- sections.push(
239
- `${newTestsWithDynamicNames.size} test(s) detected as new but their names contain ` +
240
- 'dynamic data (timestamps, UUIDs, etc.).\n' +
241
- 'Tests with changing names are always treated as new on every run, ' +
242
- 'causing unnecessary Early Flake Detection retries and preventing correct new test detection.\n' +
243
- 'Consider using stable, deterministic test names.\n\n' +
244
- formatList(items)
245
- )
246
- newTestsWithDynamicNames.clear()
248
+ function getAttemptToFixExecutionsFromJestResults (result) {
249
+ const executions = new Map()
250
+ const rootDir = result.globalConfig?.rootDir || process.cwd()
251
+
252
+ for (const { testResults, testFilePath } of result.results.testResults) {
253
+ const testSuite = getTestSuitePath(testFilePath, rootDir)
254
+ const testManagementTestsForSuite = testManagementTests
255
+ ?.jest
256
+ ?.suites
257
+ ?.[testSuite]
258
+ ?.tests
259
+ if (!testManagementTestsForSuite) continue
260
+
261
+ for (const { fullName, status } of testResults) {
262
+ const testName = testSuiteAbsolutePathsWithFastCheck.has(testFilePath)
263
+ ? fullName.replace(SEED_SUFFIX_RE, '')
264
+ : fullName
265
+ const testStatus = getTestStatusFromJestResult(status)
266
+ if (!testStatus) continue
267
+
268
+ const testManagementTest = testManagementTestsForSuite[testName]?.properties
269
+ if (!testManagementTest?.attempt_to_fix) continue
270
+
271
+ recordAttemptToFixExecution(executions, {
272
+ testSuite,
273
+ testName,
274
+ status: testStatus,
275
+ isDisabled: testManagementTest.disabled,
276
+ isQuarantined: testManagementTest.quarantined,
277
+ })
278
+ }
247
279
  }
248
280
 
249
- if (sections.length === 0) return
250
-
251
- const line = '-'.repeat(50)
252
- // eslint-disable-next-line no-console -- Intentional user-facing session summary
253
- console.warn(`\n${line}\nDatadog Test Optimization\n${line}\n${sections.join('\n\n')}\n`)
281
+ return executions
254
282
  }
255
283
 
256
284
  function getWrappedEnvironment (BaseEnvironment, jestVersion) {
@@ -556,6 +584,10 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
556
584
  }
557
585
  testContexts.set(event.test, ctx)
558
586
 
587
+ if (isAttemptToFix) {
588
+ logAttemptToFixTestExecution(this.testSuite, testName, loggedAttemptToFixTests)
589
+ }
590
+
559
591
  testStartCh.runStores(ctx, () => {
560
592
  let p = event.test.parent
561
593
  const hooks = []
@@ -795,7 +827,7 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
795
827
  // Only suppress on the final execution — not when ATR/EFD/ATF will retry the test.
796
828
  if (!event.test?.[ATR_RETRY_SUPPRESSION_FLAG] && !willBeRetriedByFailedTestReplay) {
797
829
  const quarantineCtx = testContexts.get(event.test)
798
- if (quarantineCtx?.isQuarantined && event.test.errors?.length) {
830
+ if (quarantineCtx?.isQuarantined && !quarantineCtx.isAttemptToFix && event.test.errors?.length) {
799
831
  quarantinedFailingTests.add(`${quarantineCtx.suite} › ${quarantineCtx.name}`)
800
832
  event.test.errors = []
801
833
  }
@@ -858,10 +890,10 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
858
890
  }
859
891
  if (event.name === 'run_finish') {
860
892
  for (const [test, errors] of atrSuppressedErrors) {
861
- // Do not restore errors for quarantined tests — they should stay suppressed
893
+ // Do not restore errors for non-ATF quarantined tests — they should stay suppressed
862
894
  // so Jest doesn't see the failure (prevents --bail from stopping the run).
863
895
  const ctx = testContexts.get(test)
864
- if (ctx?.isQuarantined) {
896
+ if (ctx?.isQuarantined && !ctx.isAttemptToFix) {
865
897
  const testName = getJestTestName(test, this.getShouldStripSeedFromTestName())
866
898
  quarantinedFailingTests.add(`${ctx.suite} › ${testName}`)
867
899
  } else {
@@ -984,7 +1016,8 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
984
1016
 
985
1017
  // If the test is quarantined, regardless of its actual execution result,
986
1018
  // the final status of its last execution should be reported as 'skip'.
987
- if (this.isTestManagementTestsEnabled &&
1019
+ if (!attemptToFixResult.isAttemptToFixEnabled &&
1020
+ this.isTestManagementTestsEnabled &&
988
1021
  this.testManagementTestsForThisSuite?.quarantined?.includes(testName)) {
989
1022
  return 'skip'
990
1023
  }
@@ -1141,8 +1174,8 @@ function getCliWrapper (isNewJestVersion) {
1141
1174
  if (!err) {
1142
1175
  isCodeCoverageEnabled = libraryConfig.isCodeCoverageEnabled
1143
1176
  isSuitesSkippingEnabled = libraryConfig.isSuitesSkippingEnabled
1144
- isKeepingCoverageConfiguration =
1145
- libraryConfig.isKeepingCoverageConfiguration ?? isKeepingCoverageConfiguration
1177
+ DD_TEST_TIA_KEEP_COV_CONFIG =
1178
+ libraryConfig.DD_TEST_TIA_KEEP_COV_CONFIG ?? DD_TEST_TIA_KEEP_COV_CONFIG
1146
1179
  isEarlyFlakeDetectionEnabled = libraryConfig.isEarlyFlakeDetectionEnabled
1147
1180
  earlyFlakeDetectionNumRetries = libraryConfig.earlyFlakeDetectionNumRetries
1148
1181
  earlyFlakeDetectionSlowTestRetries = libraryConfig.earlyFlakeDetectionSlowTestRetries ?? {}
@@ -1315,7 +1348,6 @@ function getCliWrapper (isNewJestVersion) {
1315
1348
  }
1316
1349
 
1317
1350
  let numFailedQuarantinedTests = 0
1318
- let numFailedQuarantinedOrDisabledAttemptedToFixTests = 0
1319
1351
  let numSuppressedQuarantinedTests = 0
1320
1352
  if (isTestManagementTestsEnabled) {
1321
1353
  const failedTests = result
@@ -1343,11 +1375,7 @@ function getCliWrapper (isNewJestVersion) {
1343
1375
  ?.tests
1344
1376
  ?.[testName]
1345
1377
  ?.properties
1346
- // This uses `attempt_to_fix` because this is always the main process and it's not formatted in camelCase
1347
- if (testManagementTest?.attempt_to_fix && (testManagementTest?.quarantined || testManagementTest?.disabled)) {
1348
- numFailedQuarantinedOrDisabledAttemptedToFixTests++
1349
- quarantineIgnoredNames.push(`${testSuite} › ${testName}`)
1350
- } else if (testManagementTest?.quarantined) {
1378
+ if (testManagementTest?.quarantined && !testManagementTest?.attempt_to_fix) {
1351
1379
  numFailedQuarantinedTests++
1352
1380
  quarantineIgnoredNames.push(`${testSuite} › ${testName}`)
1353
1381
  }
@@ -1365,13 +1393,11 @@ function getCliWrapper (isNewJestVersion) {
1365
1393
  quarantinedFailingTests.clear()
1366
1394
 
1367
1395
  // If every test that failed was quarantined, we'll consider the suite passed
1368
- // Note that if a test is attempted to fix,
1369
- // it's considered quarantined both if it's disabled and if it's quarantined
1370
- // (it'll run but its status is ignored)
1396
+ // Attempt-to-fix tests ignore quarantine/disabled suppression and keep their framework result.
1371
1397
  // Skip if EFD block already flipped (to avoid logging twice)
1372
1398
  // Only use visible failures (from Jest results) for the flip check.
1373
1399
  // Suppressed quarantine failures are not in numFailedTests.
1374
- const visibleQuarantineFailures = numFailedQuarantinedTests + numFailedQuarantinedOrDisabledAttemptedToFixTests
1400
+ const visibleQuarantineFailures = numFailedQuarantinedTests
1375
1401
  if (
1376
1402
  !result.results.success &&
1377
1403
  !mustNotFlipSuccess &&
@@ -1406,7 +1432,7 @@ function getCliWrapper (isNewJestVersion) {
1406
1432
  (isEarlyFlakeDetectionEnabled || isTestManagementTestsEnabled)
1407
1433
  ) {
1408
1434
  const visibleIgnoredFailures =
1409
- numEfdFailedTestsToIgnore + numFailedQuarantinedTests + numFailedQuarantinedOrDisabledAttemptedToFixTests
1435
+ numEfdFailedTestsToIgnore + numFailedQuarantinedTests
1410
1436
  if (
1411
1437
  visibleIgnoredFailures !== 0 &&
1412
1438
  result.results.numFailedTests === visibleIgnoredFailures
@@ -1474,7 +1500,7 @@ function getCliWrapper (isNewJestVersion) {
1474
1500
  })
1475
1501
  }
1476
1502
 
1477
- logSessionSummary(ignoredFailuresSummary)
1503
+ logSessionSummary(ignoredFailuresSummary, getAttemptToFixExecutionsFromJestResults(result))
1478
1504
 
1479
1505
  numSkippedSuites = 0
1480
1506
 
@@ -1495,7 +1521,7 @@ function coverageReporterWrapper (coverageReporter) {
1495
1521
  */
1496
1522
  // `_addUntestedFiles` is an async function
1497
1523
  shimmer.wrap(CoverageReporter.prototype, '_addUntestedFiles', addUntestedFiles => function () {
1498
- if (isKeepingCoverageConfiguration) {
1524
+ if (DD_TEST_TIA_KEEP_COV_CONFIG) {
1499
1525
  return addUntestedFiles.apply(this, arguments)
1500
1526
  }
1501
1527
  if (isCodeCoverageEnabledBecauseOfUs) {
@@ -1697,7 +1723,7 @@ function configureTestEnvironment (readConfigsResult) {
1697
1723
  ...readConfigsResult.globalConfig,
1698
1724
  passWithNoTests: true,
1699
1725
  }
1700
- if (isCodeCoverageEnabledBecauseOfUs && !isKeepingCoverageConfiguration) {
1726
+ if (isCodeCoverageEnabledBecauseOfUs && !DD_TEST_TIA_KEEP_COV_CONFIG) {
1701
1727
  globalConfig.coverageReporters = ['none']
1702
1728
  readConfigsResult.configs = configs.map(config => ({
1703
1729
  ...config,