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.
- package/LICENSE-3rdparty.csv +1 -2
- package/ext/exporters.d.ts +1 -1
- package/index.d.ts +105 -37
- package/init.js +40 -1
- package/initialize.mjs +8 -5
- package/package.json +29 -29
- package/packages/datadog-core/src/storage/index.js +1 -10
- package/packages/datadog-esbuild/index.js +5 -1
- package/packages/datadog-instrumentations/src/aws-sdk.js +2 -1
- package/packages/datadog-instrumentations/src/child_process.js +2 -2
- package/packages/datadog-instrumentations/src/cucumber.js +76 -34
- package/packages/datadog-instrumentations/src/fs.js +1 -1
- package/packages/datadog-instrumentations/src/hapi.js +1 -1
- package/packages/datadog-instrumentations/src/helpers/hook.js +8 -3
- package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -0
- package/packages/datadog-instrumentations/src/helpers/instrument.js +4 -3
- package/packages/datadog-instrumentations/src/helpers/register.js +56 -5
- package/packages/datadog-instrumentations/src/http/client.js +1 -1
- package/packages/datadog-instrumentations/src/jest.js +17 -2
- package/packages/datadog-instrumentations/src/kafkajs.js +1 -1
- package/packages/datadog-instrumentations/src/ldapjs.js +2 -2
- package/packages/datadog-instrumentations/src/mocha/main.js +12 -1
- package/packages/datadog-instrumentations/src/mocha/utils.js +58 -14
- package/packages/datadog-instrumentations/src/mocha/worker.js +1 -0
- package/packages/datadog-instrumentations/src/mquery.js +2 -2
- package/packages/datadog-instrumentations/src/next.js +1 -1
- package/packages/datadog-instrumentations/src/pg.js +2 -2
- package/packages/datadog-instrumentations/src/playwright.js +47 -33
- package/packages/datadog-instrumentations/src/restify.js +1 -1
- package/packages/datadog-instrumentations/src/vitest.js +349 -0
- package/packages/datadog-plugin-aws-sdk/src/base.js +8 -1
- package/packages/datadog-plugin-aws-sdk/src/services/dynamodb.js +1 -1
- package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +9 -3
- package/packages/datadog-plugin-aws-sdk/src/services/sns.js +6 -1
- package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +23 -5
- package/packages/datadog-plugin-aws-sdk/src/services/stepfunctions.js +1 -1
- package/packages/datadog-plugin-child_process/src/index.js +1 -1
- package/packages/datadog-plugin-child_process/src/scrub-cmd-params.js +6 -4
- package/packages/datadog-plugin-cucumber/src/index.js +24 -1
- package/packages/datadog-plugin-cypress/src/cypress-plugin.js +79 -42
- package/packages/datadog-plugin-cypress/src/plugin.js +4 -3
- package/packages/datadog-plugin-fs/src/index.js +1 -1
- package/packages/datadog-plugin-jest/src/index.js +7 -1
- package/packages/datadog-plugin-kafkajs/src/producer.js +1 -1
- package/packages/datadog-plugin-mocha/src/index.js +25 -4
- package/packages/datadog-plugin-mongodb-core/src/index.js +1 -1
- package/packages/datadog-plugin-openai/src/index.js +57 -35
- package/packages/datadog-plugin-openai/src/token-estimator.js +20 -0
- package/packages/datadog-plugin-playwright/src/index.js +4 -1
- package/packages/datadog-plugin-sharedb/src/index.js +1 -1
- package/packages/datadog-plugin-vitest/src/index.js +167 -0
- package/packages/dd-trace/src/analytics_sampler.js +1 -1
- package/packages/dd-trace/src/appsec/iast/analyzers/nosql-injection-mongodb-analyzer.js +1 -1
- package/packages/dd-trace/src/appsec/iast/path-line.js +2 -19
- package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +2 -2
- package/packages/dd-trace/src/appsec/iast/taint-tracking/plugins/kafka.js +2 -2
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/index.js +3 -1
- package/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js +4 -0
- package/packages/dd-trace/src/appsec/index.js +4 -4
- package/packages/dd-trace/src/appsec/passport.js +1 -1
- package/packages/dd-trace/src/appsec/rasp.js +32 -5
- package/packages/dd-trace/src/appsec/recommended.json +208 -3
- package/packages/dd-trace/src/appsec/reporter.js +60 -20
- package/packages/dd-trace/src/appsec/sdk/track_event.js +3 -0
- package/packages/dd-trace/src/appsec/stack_trace.js +90 -0
- package/packages/dd-trace/src/appsec/standalone.js +130 -0
- package/packages/dd-trace/src/appsec/telemetry.js +33 -1
- package/packages/dd-trace/src/appsec/waf/index.js +2 -2
- package/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +3 -3
- package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +4 -2
- package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +4 -2
- package/packages/dd-trace/src/config.js +136 -63
- package/packages/dd-trace/src/constants.js +3 -1
- package/packages/dd-trace/src/datastreams/processor.js +3 -2
- package/packages/dd-trace/src/exporters/agent/index.js +2 -2
- package/packages/dd-trace/src/format.js +1 -0
- package/packages/dd-trace/src/opentelemetry/span.js +1 -1
- package/packages/dd-trace/src/opentelemetry/tracer.js +6 -0
- package/packages/dd-trace/src/opentracing/propagation/text_map.js +12 -0
- package/packages/dd-trace/src/opentracing/span.js +4 -1
- package/packages/dd-trace/src/opentracing/tracer.js +2 -2
- package/packages/dd-trace/src/plugins/ci_plugin.js +7 -0
- package/packages/dd-trace/src/plugins/index.js +2 -0
- package/packages/dd-trace/src/plugins/util/test.js +5 -1
- package/packages/dd-trace/src/priority_sampler.js +2 -5
- package/packages/dd-trace/src/profiling/profiler.js +1 -1
- package/packages/dd-trace/src/proxy.js +3 -1
- package/packages/dd-trace/src/rate_limiter.js +2 -2
- package/packages/dd-trace/src/span_stats.js +4 -3
- package/packages/dd-trace/src/telemetry/init-telemetry.js +75 -0
- package/packages/dd-trace/src/tracer.js +2 -2
- package/packages/dd-trace/src/util.js +6 -1
- 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,
|
|
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
|
-
|
|
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
|
-
|
|
58
|
+
hook()
|
|
47
59
|
|
|
48
60
|
if (!instrumentations[packageName]) {
|
|
49
61
|
return moduleExports
|
|
50
62
|
}
|
|
51
63
|
|
|
52
|
-
|
|
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 (
|
|
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.
|
|
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
|
|
94
|
+
function getTestToArKey (test) {
|
|
88
95
|
if (!test.fn) {
|
|
89
|
-
return
|
|
96
|
+
return test
|
|
90
97
|
}
|
|
91
98
|
if (!wrappedFunctions.has(test.fn)) {
|
|
92
|
-
return
|
|
99
|
+
return test.fn
|
|
93
100
|
}
|
|
94
101
|
const originalFn = originalFns.get(test.fn)
|
|
95
|
-
return
|
|
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
|
|
204
|
-
|
|
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,
|
|
@@ -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({
|