dd-trace 5.17.0 → 5.19.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 (93) hide show
  1. package/LICENSE-3rdparty.csv +1 -2
  2. package/ext/exporters.d.ts +1 -1
  3. package/index.d.ts +105 -37
  4. package/init.js +40 -1
  5. package/initialize.mjs +8 -5
  6. package/package.json +29 -29
  7. package/packages/datadog-core/src/storage/index.js +1 -10
  8. package/packages/datadog-esbuild/index.js +5 -1
  9. package/packages/datadog-instrumentations/src/aws-sdk.js +2 -1
  10. package/packages/datadog-instrumentations/src/child_process.js +2 -2
  11. package/packages/datadog-instrumentations/src/cucumber.js +76 -34
  12. package/packages/datadog-instrumentations/src/fs.js +1 -1
  13. package/packages/datadog-instrumentations/src/hapi.js +1 -1
  14. package/packages/datadog-instrumentations/src/helpers/hook.js +8 -3
  15. package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -0
  16. package/packages/datadog-instrumentations/src/helpers/instrument.js +4 -3
  17. package/packages/datadog-instrumentations/src/helpers/register.js +56 -5
  18. package/packages/datadog-instrumentations/src/http/client.js +1 -1
  19. package/packages/datadog-instrumentations/src/jest.js +17 -2
  20. package/packages/datadog-instrumentations/src/kafkajs.js +1 -1
  21. package/packages/datadog-instrumentations/src/ldapjs.js +2 -2
  22. package/packages/datadog-instrumentations/src/mocha/main.js +12 -1
  23. package/packages/datadog-instrumentations/src/mocha/utils.js +58 -14
  24. package/packages/datadog-instrumentations/src/mocha/worker.js +1 -0
  25. package/packages/datadog-instrumentations/src/mquery.js +2 -2
  26. package/packages/datadog-instrumentations/src/next.js +1 -1
  27. package/packages/datadog-instrumentations/src/pg.js +2 -2
  28. package/packages/datadog-instrumentations/src/playwright.js +47 -33
  29. package/packages/datadog-instrumentations/src/restify.js +1 -1
  30. package/packages/datadog-instrumentations/src/vitest.js +349 -0
  31. package/packages/datadog-plugin-aws-sdk/src/base.js +8 -1
  32. package/packages/datadog-plugin-aws-sdk/src/services/dynamodb.js +1 -1
  33. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +9 -3
  34. package/packages/datadog-plugin-aws-sdk/src/services/sns.js +6 -1
  35. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +23 -5
  36. package/packages/datadog-plugin-aws-sdk/src/services/stepfunctions.js +1 -1
  37. package/packages/datadog-plugin-child_process/src/index.js +1 -1
  38. package/packages/datadog-plugin-child_process/src/scrub-cmd-params.js +6 -4
  39. package/packages/datadog-plugin-cucumber/src/index.js +24 -1
  40. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +79 -42
  41. package/packages/datadog-plugin-cypress/src/plugin.js +4 -3
  42. package/packages/datadog-plugin-fs/src/index.js +1 -1
  43. package/packages/datadog-plugin-jest/src/index.js +7 -1
  44. package/packages/datadog-plugin-kafkajs/src/producer.js +1 -1
  45. package/packages/datadog-plugin-mocha/src/index.js +25 -4
  46. package/packages/datadog-plugin-mongodb-core/src/index.js +1 -1
  47. package/packages/datadog-plugin-openai/src/index.js +57 -35
  48. package/packages/datadog-plugin-openai/src/token-estimator.js +20 -0
  49. package/packages/datadog-plugin-playwright/src/index.js +4 -1
  50. package/packages/datadog-plugin-sharedb/src/index.js +1 -1
  51. package/packages/datadog-plugin-vitest/src/index.js +167 -0
  52. package/packages/dd-trace/src/analytics_sampler.js +1 -1
  53. package/packages/dd-trace/src/appsec/iast/analyzers/nosql-injection-mongodb-analyzer.js +1 -1
  54. package/packages/dd-trace/src/appsec/iast/path-line.js +2 -19
  55. package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +2 -2
  56. package/packages/dd-trace/src/appsec/iast/taint-tracking/plugins/kafka.js +2 -2
  57. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/index.js +3 -1
  58. package/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js +4 -0
  59. package/packages/dd-trace/src/appsec/index.js +4 -4
  60. package/packages/dd-trace/src/appsec/passport.js +1 -1
  61. package/packages/dd-trace/src/appsec/rasp.js +32 -5
  62. package/packages/dd-trace/src/appsec/recommended.json +208 -3
  63. package/packages/dd-trace/src/appsec/reporter.js +60 -20
  64. package/packages/dd-trace/src/appsec/sdk/track_event.js +3 -0
  65. package/packages/dd-trace/src/appsec/stack_trace.js +90 -0
  66. package/packages/dd-trace/src/appsec/standalone.js +130 -0
  67. package/packages/dd-trace/src/appsec/telemetry.js +33 -1
  68. package/packages/dd-trace/src/appsec/waf/index.js +2 -2
  69. package/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +3 -3
  70. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +4 -2
  71. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +4 -2
  72. package/packages/dd-trace/src/config.js +136 -63
  73. package/packages/dd-trace/src/constants.js +3 -1
  74. package/packages/dd-trace/src/datastreams/processor.js +3 -2
  75. package/packages/dd-trace/src/exporters/agent/index.js +2 -2
  76. package/packages/dd-trace/src/format.js +1 -0
  77. package/packages/dd-trace/src/opentelemetry/span.js +1 -1
  78. package/packages/dd-trace/src/opentelemetry/tracer.js +6 -0
  79. package/packages/dd-trace/src/opentracing/propagation/text_map.js +12 -0
  80. package/packages/dd-trace/src/opentracing/span.js +4 -1
  81. package/packages/dd-trace/src/opentracing/tracer.js +2 -2
  82. package/packages/dd-trace/src/plugins/ci_plugin.js +7 -0
  83. package/packages/dd-trace/src/plugins/index.js +2 -0
  84. package/packages/dd-trace/src/plugins/util/test.js +5 -1
  85. package/packages/dd-trace/src/priority_sampler.js +2 -5
  86. package/packages/dd-trace/src/profiling/profiler.js +1 -1
  87. package/packages/dd-trace/src/proxy.js +3 -1
  88. package/packages/dd-trace/src/rate_limiter.js +2 -2
  89. package/packages/dd-trace/src/span_stats.js +4 -3
  90. package/packages/dd-trace/src/telemetry/init-telemetry.js +75 -0
  91. package/packages/dd-trace/src/tracer.js +2 -2
  92. package/packages/dd-trace/src/util.js +6 -1
  93. package/packages/datadog-core/src/storage/async_hooks.js +0 -49
@@ -11,8 +11,13 @@ const ritm = require('../../../dd-trace/src/ritm')
11
11
  * @param {string[]} modules list of modules to hook into
12
12
  * @param {Function} onrequire callback to be executed upon encountering module
13
13
  */
14
- function Hook (modules, onrequire) {
15
- if (!(this instanceof Hook)) return new Hook(modules, onrequire)
14
+ function Hook (modules, hookOptions, onrequire) {
15
+ if (!(this instanceof Hook)) return new Hook(modules, hookOptions, onrequire)
16
+
17
+ if (typeof hookOptions === 'function') {
18
+ onrequire = hookOptions
19
+ hookOptions = {}
20
+ }
16
21
 
17
22
  this._patched = Object.create(null)
18
23
 
@@ -28,7 +33,7 @@ function Hook (modules, onrequire) {
28
33
  }
29
34
 
30
35
  this._ritmHook = ritm(modules, {}, safeHook)
31
- this._iitmHook = iitm(modules, {}, (moduleExports, moduleName, moduleBaseDir) => {
36
+ this._iitmHook = iitm(modules, hookOptions, (moduleExports, moduleName, moduleBaseDir) => {
32
37
  // TODO: Move this logic to import-in-the-middle and only do it for CommonJS
33
38
  // modules and not ESM. In the meantime, all the modules we instrument are
34
39
  // CommonJS modules for which the default export is always moved to
@@ -23,6 +23,7 @@ module.exports = {
23
23
  '@opentelemetry/sdk-trace-node': () => require('../otel-sdk-trace'),
24
24
  '@redis/client': () => require('../redis'),
25
25
  '@smithy/smithy-client': () => require('../aws-sdk'),
26
+ '@vitest/runner': { esmFirst: true, fn: () => require('../vitest') },
26
27
  aerospike: () => require('../aerospike'),
27
28
  amqp10: () => require('../amqp10'),
28
29
  amqplib: () => require('../amqplib'),
@@ -110,6 +111,7 @@ module.exports = {
110
111
  sharedb: () => require('../sharedb'),
111
112
  tedious: () => require('../tedious'),
112
113
  undici: () => require('../undici'),
114
+ vitest: { esmFirst: true, fn: () => require('../vitest') },
113
115
  when: () => require('../when'),
114
116
  winston: () => require('../winston')
115
117
  }
@@ -17,10 +17,11 @@ exports.channel = function (name) {
17
17
  /**
18
18
  * @param {string} args.name module name
19
19
  * @param {string[]} args.versions array of semver range strings
20
- * @param {string} args.file path to file within package to instrument?
20
+ * @param {string} args.file path to file within package to instrument
21
+ * @param {string} args.filePattern pattern to match files within package to instrument
21
22
  * @param Function hook
22
23
  */
23
- exports.addHook = function addHook ({ name, versions, file }, hook) {
24
+ exports.addHook = function addHook ({ name, versions, file, filePattern }, hook) {
24
25
  if (typeof name === 'string') {
25
26
  name = [name]
26
27
  }
@@ -29,7 +30,7 @@ exports.addHook = function addHook ({ name, versions, file }, hook) {
29
30
  if (!instrumentations[val]) {
30
31
  instrumentations[val] = []
31
32
  }
32
- instrumentations[val].push({ name: val, versions, file, hook })
33
+ instrumentations[val].push({ name: val, versions, file, filePattern, hook })
33
34
  }
34
35
  }
35
36
 
@@ -7,6 +7,7 @@ const Hook = require('./hook')
7
7
  const requirePackageJson = require('../../../dd-trace/src/require-package-json')
8
8
  const log = require('../../../dd-trace/src/log')
9
9
  const checkRequireCache = require('../check_require_cache')
10
+ const telemetry = require('../../../dd-trace/src/telemetry/init-telemetry')
10
11
 
11
12
  const {
12
13
  DD_TRACE_DISABLED_INSTRUMENTATIONS = '',
@@ -35,22 +36,38 @@ if (DD_TRACE_DEBUG && DD_TRACE_DEBUG.toLowerCase() !== 'false') {
35
36
  setImmediate(checkRequireCache.checkForPotentialConflicts)
36
37
  }
37
38
 
39
+ const seenCombo = new Set()
40
+
38
41
  // TODO: make this more efficient
39
42
  for (const packageName of names) {
40
43
  if (disabledInstrumentations.has(packageName)) continue
41
44
 
42
- Hook([packageName], (moduleExports, moduleName, moduleBaseDir, moduleVersion) => {
45
+ const hookOptions = {}
46
+
47
+ let hook = hooks[packageName]
48
+
49
+ if (typeof hook === 'object') {
50
+ hookOptions.internals = hook.esmFirst
51
+ hook = hook.fn
52
+ }
53
+
54
+ Hook([packageName], hookOptions, (moduleExports, moduleName, moduleBaseDir, moduleVersion) => {
43
55
  moduleName = moduleName.replace(pathSepExpr, '/')
44
56
 
45
57
  // This executes the integration file thus adding its entries to `instrumentations`
46
- hooks[packageName]()
58
+ hook()
47
59
 
48
60
  if (!instrumentations[packageName]) {
49
61
  return moduleExports
50
62
  }
51
63
 
52
- for (const { name, file, versions, hook } of instrumentations[packageName]) {
64
+ const namesAndSuccesses = {}
65
+ for (const { name, file, versions, hook, filePattern } of instrumentations[packageName]) {
66
+ let fullFilePattern = filePattern
53
67
  const fullFilename = filename(name, file)
68
+ if (fullFilePattern) {
69
+ fullFilePattern = filename(name, fullFilePattern)
70
+ }
54
71
 
55
72
  // Create a WeakMap associated with the hook function so that patches on the same moduleExport only happens once
56
73
  // for example by instrumenting both dns and node:dns double the spans would be created
@@ -58,13 +75,29 @@ for (const packageName of names) {
58
75
  if (!hook[HOOK_SYMBOL]) {
59
76
  hook[HOOK_SYMBOL] = new WeakMap()
60
77
  }
78
+ let matchesFile = false
79
+
80
+ matchesFile = moduleName === fullFilename
61
81
 
62
- if (moduleName === fullFilename) {
82
+ if (fullFilePattern) {
83
+ // Some libraries include a hash in their filenames when installed,
84
+ // so our instrumentation has to include a '.*' to match them for more than a single version.
85
+ matchesFile = matchesFile || new RegExp(fullFilePattern).test(moduleName)
86
+ }
87
+
88
+ if (matchesFile) {
63
89
  const version = moduleVersion || getVersion(moduleBaseDir)
90
+ if (!Object.hasOwnProperty(namesAndSuccesses, name)) {
91
+ namesAndSuccesses[name] = {
92
+ success: false,
93
+ version
94
+ }
95
+ }
64
96
 
65
97
  if (matchVersion(version, versions)) {
66
98
  // Check if the hook already has a set moduleExport
67
99
  if (hook[HOOK_SYMBOL].has(moduleExports)) {
100
+ namesAndSuccesses[name].success = true
68
101
  return moduleExports
69
102
  }
70
103
 
@@ -76,11 +109,29 @@ for (const packageName of names) {
76
109
  // Set the moduleExports in the hooks weakmap
77
110
  hook[HOOK_SYMBOL].set(moduleExports, name)
78
111
  } catch (e) {
79
- log.error(e)
112
+ log.info('Error during ddtrace instrumentation of application, aborting.')
113
+ log.info(e)
114
+ telemetry('error', [
115
+ `error_type:${e.constructor.name}`,
116
+ `integration:${name}`,
117
+ `integration_version:${version}`
118
+ ])
80
119
  }
120
+ namesAndSuccesses[name].success = true
81
121
  }
82
122
  }
83
123
  }
124
+ for (const name of Object.keys(namesAndSuccesses)) {
125
+ const { success, version } = namesAndSuccesses[name]
126
+ if (!success && !seenCombo.has(`${name}@${version}`)) {
127
+ telemetry('abort.integration', [
128
+ `integration:${name}`,
129
+ `integration_version:${version}`
130
+ ])
131
+ log.info(`Found incompatible integration version: ${name}@${version}`)
132
+ seenCombo.add(`${name}@${version}`)
133
+ }
134
+ }
84
135
 
85
136
  return moduleExports
86
137
  })
@@ -132,7 +132,7 @@ function patch (http, methodName) {
132
132
  }
133
133
 
134
134
  function combineOptions (inputURL, inputOptions) {
135
- if (typeof inputOptions === 'object') {
135
+ if (inputOptions !== null && typeof inputOptions === 'object') {
136
136
  return Object.assign(inputURL || {}, inputOptions)
137
137
  } else {
138
138
  return inputURL
@@ -12,7 +12,8 @@ const {
12
12
  getTestParametersString,
13
13
  addEfdStringToTestName,
14
14
  removeEfdStringFromTestName,
15
- getIsFaultyEarlyFlakeDetection
15
+ getIsFaultyEarlyFlakeDetection,
16
+ NUM_FAILED_TEST_RETRIES
16
17
  } = require('../../dd-trace/src/plugins/util/test')
17
18
  const {
18
19
  getFormattedJestTestParameters,
@@ -49,6 +50,9 @@ const itrSkippedSuitesCh = channel('ci:jest:itr:skipped-suites')
49
50
  const CHILD_MESSAGE_CALL = 1
50
51
  // Maximum time we'll wait for the tracer to flush
51
52
  const FLUSH_TIMEOUT = 10000
53
+ // eslint-disable-next-line
54
+ // https://github.com/jestjs/jest/blob/41f842a46bb2691f828c3a5f27fc1d6290495b82/packages/jest-circus/src/types.ts#L9C8-L9C54
55
+ const RETRY_TIMES = Symbol.for('RETRY_TIMES')
52
56
 
53
57
  let skippableSuites = []
54
58
  let knownTests = {}
@@ -127,6 +131,7 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
127
131
  }
128
132
 
129
133
  this.isEarlyFlakeDetectionEnabled = this.testEnvironmentOptions._ddIsEarlyFlakeDetectionEnabled
134
+ this.isFlakyTestRetriesEnabled = this.testEnvironmentOptions._ddIsFlakyTestRetriesEnabled
130
135
 
131
136
  if (this.isEarlyFlakeDetectionEnabled) {
132
137
  const hasKnownTests = !!knownTests.jest
@@ -140,6 +145,13 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
140
145
  this.isEarlyFlakeDetectionEnabled = false
141
146
  }
142
147
  }
148
+
149
+ if (this.isFlakyTestRetriesEnabled) {
150
+ const currentNumRetries = this.global[RETRY_TIMES]
151
+ if (!currentNumRetries) {
152
+ this.global[RETRY_TIMES] = NUM_FAILED_TEST_RETRIES
153
+ }
154
+ }
143
155
  }
144
156
 
145
157
  getHasSnapshotTests () {
@@ -218,6 +230,7 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
218
230
  retriedTestsToNumAttempts.set(originalTestName, numEfdRetry + 1)
219
231
  }
220
232
  }
233
+ const isJestRetry = event.test?.invocations > 1
221
234
  asyncResource.runInAsyncScope(() => {
222
235
  testStartCh.publish({
223
236
  name: removeEfdStringFromTestName(testName),
@@ -228,7 +241,8 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
228
241
  testParameters,
229
242
  frameworkVersion: jestVersion,
230
243
  isNew: isNewTest,
231
- isEfdRetry: numEfdRetry > 0
244
+ isEfdRetry: numEfdRetry > 0,
245
+ isJestRetry
232
246
  })
233
247
  originalTestFns.set(event.test, event.test.fn)
234
248
  event.test.fn = asyncResource.bind(event.test.fn)
@@ -758,6 +772,7 @@ addHook({
758
772
  _ddIsEarlyFlakeDetectionEnabled,
759
773
  _ddEarlyFlakeDetectionNumRetries,
760
774
  _ddRepositoryRoot,
775
+ _ddIsFlakyTestRetriesEnabled,
761
776
  ...restOfTestEnvironmentOptions
762
777
  } = testEnvironmentOptions
763
778
 
@@ -59,7 +59,7 @@ addHook({ name: 'kafkajs', file: 'src/index.js', versions: ['>=1.4'] }, (BaseKaf
59
59
  try {
60
60
  const { topic, messages = [] } = arguments[0]
61
61
  for (const message of messages) {
62
- if (typeof message === 'object') {
62
+ if (message !== null && typeof message === 'object') {
63
63
  message.headers = message.headers || {}
64
64
  }
65
65
  }
@@ -61,7 +61,7 @@ addHook({ name: 'ldapjs', versions: ['>=2'] }, ldapjs => {
61
61
  let filter
62
62
  if (isString(options)) {
63
63
  filter = options
64
- } else if (typeof options === 'object' && options.filter) {
64
+ } else if (options !== null && typeof options === 'object' && options.filter) {
65
65
  if (isString(options.filter)) {
66
66
  filter = options.filter
67
67
  }
@@ -78,7 +78,7 @@ addHook({ name: 'ldapjs', versions: ['>=2'] }, ldapjs => {
78
78
  const callback = arguments[callbackIndex]
79
79
  // eslint-disable-next-line n/handle-callback-err
80
80
  arguments[callbackIndex] = shimmer.wrap(callback, function (err, corkedEmitter) {
81
- if (typeof corkedEmitter === 'object' && typeof corkedEmitter.on === 'function') {
81
+ if (corkedEmitter !== null && typeof corkedEmitter === 'object' && typeof corkedEmitter.on === 'function') {
82
82
  wrapEmitter(corkedEmitter)
83
83
  }
84
84
  callback.apply(this, arguments)
@@ -21,6 +21,7 @@ const {
21
21
  runnableWrapper,
22
22
  getOnTestHandler,
23
23
  getOnTestEndHandler,
24
+ getOnTestRetryHandler,
24
25
  getOnHookEndHandler,
25
26
  getOnFailHandler,
26
27
  getOnPendingHandler,
@@ -37,10 +38,12 @@ let isSuitesSkipped = false
37
38
  let skippedSuites = []
38
39
  let isEarlyFlakeDetectionEnabled = false
39
40
  let isSuitesSkippingEnabled = false
41
+ let isFlakyTestRetriesEnabled = false
40
42
  let earlyFlakeDetectionNumRetries = 0
41
43
  let knownTests = []
42
44
  let itrCorrelationId = ''
43
45
  let isForcedToRun = false
46
+ const config = {}
44
47
 
45
48
  // We'll preserve the original coverage here
46
49
  const originalCoverageMap = createCoverageMap()
@@ -227,6 +230,12 @@ addHook({
227
230
  isEarlyFlakeDetectionEnabled = libraryConfig.isEarlyFlakeDetectionEnabled
228
231
  isSuitesSkippingEnabled = libraryConfig.isSuitesSkippingEnabled
229
232
  earlyFlakeDetectionNumRetries = libraryConfig.earlyFlakeDetectionNumRetries
233
+ isFlakyTestRetriesEnabled = libraryConfig.isFlakyTestRetriesEnabled
234
+
235
+ config.isEarlyFlakeDetectionEnabled = isEarlyFlakeDetectionEnabled
236
+ config.isSuitesSkippingEnabled = isSuitesSkippingEnabled
237
+ config.earlyFlakeDetectionNumRetries = earlyFlakeDetectionNumRetries
238
+ config.isFlakyTestRetriesEnabled = isFlakyTestRetriesEnabled
230
239
 
231
240
  if (isEarlyFlakeDetectionEnabled) {
232
241
  knownTestsCh.publish({
@@ -317,6 +326,8 @@ addHook({
317
326
 
318
327
  this.on('test end', getOnTestEndHandler())
319
328
 
329
+ this.on('retry', getOnTestRetryHandler())
330
+
320
331
  // If the hook passes, 'hook end' will be emitted. Otherwise, 'fail' will be emitted
321
332
  this.on('hook end', getOnHookEndHandler())
322
333
 
@@ -401,7 +412,7 @@ addHook({
401
412
  name: 'mocha',
402
413
  versions: ['>=5.2.0'],
403
414
  file: 'lib/runnable.js'
404
- }, runnableWrapper)
415
+ }, (runnablePackage) => runnableWrapper(runnablePackage, config))
405
416
 
406
417
  // Only used in parallel mode (--parallel flag is passed)
407
418
  // Used to generate suite events and receive test payloads from workers
@@ -3,7 +3,8 @@
3
3
  const {
4
4
  getTestSuitePath,
5
5
  removeEfdStringFromTestName,
6
- addEfdStringToTestName
6
+ addEfdStringToTestName,
7
+ NUM_FAILED_TEST_RETRIES
7
8
  } = require('../../../dd-trace/src/plugins/util/test')
8
9
  const { channel, AsyncResource } = require('../helpers/instrument')
9
10
  const shimmer = require('../../../datadog-shimmer')
@@ -11,6 +12,8 @@ const shimmer = require('../../../datadog-shimmer')
11
12
  // test channels
12
13
  const testStartCh = channel('ci:mocha:test:start')
13
14
  const testFinishCh = channel('ci:mocha:test:finish')
15
+ // after a test has failed, we'll publish to this channel
16
+ const testRetryCh = channel('ci:mocha:test:retry')
14
17
  const errorCh = channel('ci:mocha:test:error')
15
18
  const skipCh = channel('ci:mocha:test:skip')
16
19
 
@@ -70,6 +73,10 @@ function isMochaRetry (test) {
70
73
  return test._currentRetry !== undefined && test._currentRetry !== 0
71
74
  }
72
75
 
76
+ function isLastRetry (test) {
77
+ return test._currentRetry === test._retries
78
+ }
79
+
73
80
  function getTestFullName (test) {
74
81
  return `mocha.${getTestSuitePath(test.file, process.cwd())}.${removeEfdStringFromTestName(test.fullTitle())}`
75
82
  }
@@ -84,22 +91,34 @@ function getTestStatus (test) {
84
91
  return 'pass'
85
92
  }
86
93
 
87
- function getTestAsyncResource (test) {
94
+ function getTestToArKey (test) {
88
95
  if (!test.fn) {
89
- return testToAr.get(test)
96
+ return test
90
97
  }
91
98
  if (!wrappedFunctions.has(test.fn)) {
92
- return testToAr.get(test.fn)
99
+ return test.fn
93
100
  }
94
101
  const originalFn = originalFns.get(test.fn)
95
- return testToAr.get(originalFn)
102
+ return originalFn
103
+ }
104
+
105
+ function getTestAsyncResource (test) {
106
+ const key = getTestToArKey(test)
107
+ return testToAr.get(key)
96
108
  }
97
109
 
98
- function runnableWrapper (RunnablePackage) {
110
+ function runnableWrapper (RunnablePackage, libraryConfig) {
99
111
  shimmer.wrap(RunnablePackage.prototype, 'run', run => function () {
100
112
  if (!testStartCh.hasSubscribers) {
101
113
  return run.apply(this, arguments)
102
114
  }
115
+ // Flaky test retries does not work in parallel mode
116
+ if (libraryConfig?.isFlakyTestRetriesEnabled) {
117
+ this.retries(NUM_FAILED_TEST_RETRIES)
118
+ }
119
+ // The reason why the wrapping logic is here is because we need to cover
120
+ // `afterEach` and `beforeEach` hooks as well.
121
+ // It can't be done in `getOnTestHandler` because it's only called for tests.
103
122
  const isBeforeEach = this.parent._beforeEach.includes(this)
104
123
  const isAfterEach = this.parent._afterEach.includes(this)
105
124
 
@@ -135,11 +154,16 @@ function runnableWrapper (RunnablePackage) {
135
154
 
136
155
  function getOnTestHandler (isMain, newTests) {
137
156
  return function (test) {
138
- if (isMochaRetry(test)) {
139
- return
140
- }
141
157
  const testStartLine = testToStartLine.get(test)
142
158
  const asyncResource = new AsyncResource('bound-anonymous-fn')
159
+
160
+ // This may be a retry. If this is the case, `test.fn` is already wrapped,
161
+ // so we need to restore it.
162
+ if (wrappedFunctions.has(test.fn)) {
163
+ const originalFn = originalFns.get(test.fn)
164
+ test.fn = originalFn
165
+ wrappedFunctions.delete(test.fn)
166
+ }
143
167
  testToAr.set(test.fn, asyncResource)
144
168
 
145
169
  const {
@@ -186,7 +210,7 @@ function getOnTestEndHandler () {
186
210
  // if there are afterEach to be run, we don't finish the test yet
187
211
  if (asyncResource && !test.parent._afterEach.length) {
188
212
  asyncResource.runInAsyncScope(() => {
189
- testFinishCh.publish(status)
213
+ testFinishCh.publish({ status, hasBeenRetried: isMochaRetry(test) })
190
214
  })
191
215
  }
192
216
  }
@@ -197,12 +221,17 @@ function getOnHookEndHandler () {
197
221
  const test = hook.ctx.currentTest
198
222
  if (test && hook.parent._afterEach.includes(hook)) { // only if it's an afterEach
199
223
  const isLastAfterEach = hook.parent._afterEach.indexOf(hook) === hook.parent._afterEach.length - 1
224
+ if (test._retries > 0 && !isLastRetry(test)) {
225
+ return
226
+ }
200
227
  if (isLastAfterEach) {
201
228
  const status = getTestStatus(test)
202
229
  const asyncResource = getTestAsyncResource(test)
203
- asyncResource.runInAsyncScope(() => {
204
- testFinishCh.publish(status)
205
- })
230
+ if (asyncResource) {
231
+ asyncResource.runInAsyncScope(() => {
232
+ testFinishCh.publish({ status, hasBeenRetried: isMochaRetry(test) })
233
+ })
234
+ }
206
235
  }
207
236
  }
208
237
  }
@@ -226,7 +255,7 @@ function getOnFailHandler (isMain) {
226
255
  err.message = `${testOrHook.fullTitle()}: ${err.message}`
227
256
  errorCh.publish(err)
228
257
  // if it's a hook and it has failed, 'test end' will not be called
229
- testFinishCh.publish('fail')
258
+ testFinishCh.publish({ status: 'fail', hasBeenRetried: isMochaRetry(test) })
230
259
  } else {
231
260
  errorCh.publish(err)
232
261
  }
@@ -250,6 +279,20 @@ function getOnFailHandler (isMain) {
250
279
  }
251
280
  }
252
281
 
282
+ function getOnTestRetryHandler () {
283
+ return function (test) {
284
+ const asyncResource = getTestAsyncResource(test)
285
+ if (asyncResource) {
286
+ const isFirstAttempt = test._currentRetry === 0
287
+ asyncResource.runInAsyncScope(() => {
288
+ testRetryCh.publish(isFirstAttempt)
289
+ })
290
+ }
291
+ const key = getTestToArKey(test)
292
+ testToAr.delete(key)
293
+ }
294
+ }
295
+
253
296
  function getOnPendingHandler () {
254
297
  return function (test) {
255
298
  const testStartLine = testToStartLine.get(test)
@@ -299,6 +342,7 @@ module.exports = {
299
342
  testToStartLine,
300
343
  getOnTestHandler,
301
344
  getOnTestEndHandler,
345
+ getOnTestRetryHandler,
302
346
  getOnHookEndHandler,
303
347
  getOnFailHandler,
304
348
  getOnPendingHandler,
@@ -49,3 +49,4 @@ addHook({
49
49
  versions: ['>=5.2.0'],
50
50
  file: 'lib/runnable.js'
51
51
  }, runnableWrapper)
52
+ // TODO: parallel mode does not support flaky test retries, so no library config is passed.
@@ -25,9 +25,9 @@ const methodsOptionalArgs = ['findOneAndUpdate']
25
25
  function getFilters (args, methodName) {
26
26
  const [arg0, arg1] = args
27
27
 
28
- const filters = arg0 && typeof arg0 === 'object' ? [arg0] : []
28
+ const filters = arg0 !== null && typeof arg0 === 'object' ? [arg0] : []
29
29
 
30
- if (arg1 && typeof arg1 === 'object' && methodsOptionalArgs.includes(methodName)) {
30
+ if (arg1 !== null && typeof arg1 === 'object' && methodsOptionalArgs.includes(methodName)) {
31
31
  filters.push(arg1)
32
32
  }
33
33
 
@@ -46,7 +46,7 @@ function wrapHandleApiRequest (handleApiRequest) {
46
46
  function wrapHandleApiRequestWithMatch (handleApiRequest) {
47
47
  return function (req, res, query, match) {
48
48
  return instrument(req, res, () => {
49
- const page = (typeof match === 'object' && typeof match.definition === 'object')
49
+ const page = (match !== null && typeof match === 'object' && typeof match.definition === 'object')
50
50
  ? match.definition.pathname
51
51
  : undefined
52
52
 
@@ -35,7 +35,7 @@ function wrapQuery (query) {
35
35
  const asyncResource = new AsyncResource('bound-anonymous-fn')
36
36
  const processId = this.processID
37
37
 
38
- const pgQuery = arguments[0] && typeof arguments[0] === 'object'
38
+ const pgQuery = arguments[0] !== null && typeof arguments[0] === 'object'
39
39
  ? arguments[0]
40
40
  : { text: arguments[0] }
41
41
 
@@ -109,7 +109,7 @@ function wrapPoolQuery (query) {
109
109
 
110
110
  const asyncResource = new AsyncResource('bound-anonymous-fn')
111
111
 
112
- const pgQuery = arguments[0] && typeof arguments[0] === 'object' ? arguments[0] : { text: arguments[0] }
112
+ const pgQuery = arguments[0] !== null && typeof arguments[0] === 'object' ? arguments[0] : { text: arguments[0] }
113
113
 
114
114
  return asyncResource.runInAsyncScope(() => {
115
115
  startPoolQueryCh.publish({