dd-trace 5.85.0 → 5.87.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/index.d.ts +38 -4
- package/package.json +1 -1
- package/packages/datadog-core/src/storage.js +30 -12
- package/packages/datadog-instrumentations/src/cucumber.js +14 -0
- package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
- package/packages/datadog-instrumentations/src/http/client.js +119 -1
- package/packages/datadog-instrumentations/src/jest.js +135 -10
- package/packages/datadog-instrumentations/src/mocha/main.js +9 -0
- package/packages/datadog-instrumentations/src/mocha/utils.js +6 -0
- package/packages/datadog-instrumentations/src/mysql2.js +131 -64
- package/packages/datadog-instrumentations/src/playwright.js +8 -0
- package/packages/datadog-instrumentations/src/prisma.js +225 -30
- package/packages/datadog-instrumentations/src/stripe.js +92 -0
- package/packages/datadog-instrumentations/src/vitest.js +11 -0
- package/packages/datadog-instrumentations/src/ws.js +22 -0
- package/packages/datadog-plugin-azure-functions/src/index.js +53 -37
- package/packages/datadog-plugin-cucumber/src/index.js +4 -10
- package/packages/datadog-plugin-cypress/src/cypress-plugin.js +12 -1
- package/packages/datadog-plugin-google-cloud-pubsub/src/consumer.js +2 -4
- package/packages/datadog-plugin-http/src/server.js +23 -8
- package/packages/datadog-plugin-jest/src/index.js +29 -10
- package/packages/datadog-plugin-jest/src/util.js +7 -1
- package/packages/datadog-plugin-mocha/src/index.js +5 -17
- package/packages/datadog-plugin-playwright/src/index.js +3 -0
- package/packages/datadog-plugin-prisma/src/datadog-tracing-helper.js +37 -14
- package/packages/datadog-plugin-prisma/src/index.js +8 -5
- package/packages/datadog-plugin-router/src/index.js +28 -19
- package/packages/datadog-plugin-vitest/src/index.js +6 -10
- package/packages/datadog-plugin-ws/src/server.js +8 -0
- package/packages/dd-trace/src/appsec/addresses.js +11 -0
- package/packages/dd-trace/src/appsec/channels.js +5 -1
- package/packages/dd-trace/src/appsec/downstream_requests.js +302 -0
- package/packages/dd-trace/src/appsec/iast/path-line.js +1 -0
- package/packages/dd-trace/src/appsec/index.js +103 -0
- package/packages/dd-trace/src/appsec/rasp/ssrf.js +66 -4
- package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +14 -1
- package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +1 -1
- package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +19 -0
- package/packages/dd-trace/src/ci-visibility/requests/upload-coverage-report.js +15 -0
- package/packages/dd-trace/src/ci-visibility/telemetry.js +36 -0
- package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +44 -1
- package/packages/dd-trace/src/config/defaults.js +2 -0
- package/packages/dd-trace/src/config/index.js +6 -0
- package/packages/dd-trace/src/config/supported-configurations.json +2 -0
- package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +47 -2
- package/packages/dd-trace/src/debugger/devtools_client/index.js +75 -23
- package/packages/dd-trace/src/debugger/devtools_client/remote_config.js +23 -1
- package/packages/dd-trace/src/debugger/devtools_client/snapshot/collector.js +3 -3
- package/packages/dd-trace/src/debugger/devtools_client/snapshot/index.js +168 -36
- package/packages/dd-trace/src/debugger/devtools_client/snapshot/processor.js +18 -0
- package/packages/dd-trace/src/exporters/common/agents.js +1 -1
- package/packages/dd-trace/src/exporters/common/request.js +35 -35
- package/packages/dd-trace/src/id.js +1 -1
- package/packages/dd-trace/src/lambda/context.js +27 -0
- package/packages/dd-trace/src/lambda/handler.js +5 -18
- package/packages/dd-trace/src/llmobs/constants/writers.js +1 -1
- package/packages/dd-trace/src/llmobs/sdk.js +34 -5
- package/packages/dd-trace/src/log/writer.js +1 -5
- package/packages/dd-trace/src/plugins/ci_plugin.js +63 -1
- package/packages/dd-trace/src/plugins/database.js +42 -43
- package/packages/dd-trace/src/plugins/outbound.js +27 -2
- package/packages/dd-trace/src/plugins/tracing.js +39 -4
- package/packages/dd-trace/src/plugins/util/git.js +27 -30
- package/packages/dd-trace/src/plugins/util/inferred_proxy.js +7 -0
- package/packages/dd-trace/src/plugins/util/test.js +3 -1
- package/packages/dd-trace/src/plugins/util/web.js +9 -7
- package/packages/dd-trace/src/profiling/config.js +6 -14
- package/packages/dd-trace/src/profiling/exporters/agent.js +23 -24
- package/packages/dd-trace/src/profiling/profiler.js +2 -0
- package/packages/dd-trace/src/startup-log.js +3 -2
- package/packages/dd-trace/src/plugins/util/serverless.js +0 -8
package/index.d.ts
CHANGED
|
@@ -295,7 +295,18 @@ interface Plugins {
|
|
|
295
295
|
}
|
|
296
296
|
|
|
297
297
|
declare namespace tracer {
|
|
298
|
-
export type SpanOptions = opentracing.SpanOptions
|
|
298
|
+
export type SpanOptions = Omit<opentracing.SpanOptions, 'childOf'> & {
|
|
299
|
+
/**
|
|
300
|
+
* Set childOf to 'null' to create a root span without a parent, even when a parent span
|
|
301
|
+
* exists in the current async context. If 'undefined' the parent will be inferred from the
|
|
302
|
+
* existing async context.
|
|
303
|
+
*/
|
|
304
|
+
childOf?: opentracing.Span | opentracing.SpanContext | null;
|
|
305
|
+
/**
|
|
306
|
+
* Optional name of the integration that crated this span.
|
|
307
|
+
*/
|
|
308
|
+
integrationName?: string;
|
|
309
|
+
};
|
|
299
310
|
export { Tracer };
|
|
300
311
|
|
|
301
312
|
export interface TraceOptions extends Analyzable {
|
|
@@ -749,6 +760,14 @@ declare namespace tracer {
|
|
|
749
760
|
*/
|
|
750
761
|
dbmPropagationMode?: 'disabled' | 'service' | 'full'
|
|
751
762
|
|
|
763
|
+
/**
|
|
764
|
+
* Whether to enable Data Streams Monitoring.
|
|
765
|
+
* Can also be enabled via the DD_DATA_STREAMS_ENABLED environment variable.
|
|
766
|
+
* When not provided, the value of DD_DATA_STREAMS_ENABLED is used.
|
|
767
|
+
* @default false
|
|
768
|
+
*/
|
|
769
|
+
dsmEnabled?: boolean
|
|
770
|
+
|
|
752
771
|
/**
|
|
753
772
|
* Configuration of the AppSec protection. Can be a boolean as an alias to `appsec.enabled`.
|
|
754
773
|
*/
|
|
@@ -3224,13 +3243,13 @@ declare namespace tracer {
|
|
|
3224
3243
|
/**
|
|
3225
3244
|
* The type of evaluation metric, one of 'categorical', 'score', or 'boolean'
|
|
3226
3245
|
*/
|
|
3227
|
-
metricType: 'categorical' | 'score' | 'boolean',
|
|
3246
|
+
metricType: 'categorical' | 'score' | 'boolean' | 'json',
|
|
3228
3247
|
|
|
3229
3248
|
/**
|
|
3230
3249
|
* The value of the evaluation metric.
|
|
3231
|
-
* Must be string for 'categorical' metrics, number for 'score' metrics,
|
|
3250
|
+
* Must be string for 'categorical' metrics, number for 'score' metrics, boolean for 'boolean' metrics and a JSON object for 'json' metrics.
|
|
3232
3251
|
*/
|
|
3233
|
-
value: string | number | boolean,
|
|
3252
|
+
value: string | number | boolean | { [key: string]: any },
|
|
3234
3253
|
|
|
3235
3254
|
/**
|
|
3236
3255
|
* An object of string key-value pairs to tag the evaluation metric with.
|
|
@@ -3246,6 +3265,21 @@ declare namespace tracer {
|
|
|
3246
3265
|
* The timestamp in milliseconds when the evaluation metric result was generated.
|
|
3247
3266
|
*/
|
|
3248
3267
|
timestampMs?: number
|
|
3268
|
+
|
|
3269
|
+
/**
|
|
3270
|
+
* Reasoning for the evaluation result.
|
|
3271
|
+
*/
|
|
3272
|
+
reasoning?: string,
|
|
3273
|
+
|
|
3274
|
+
/**
|
|
3275
|
+
* Whether the evaluation passed or failed. Valid values are pass and fail.
|
|
3276
|
+
*/
|
|
3277
|
+
assessment?: 'pass' | 'fail',
|
|
3278
|
+
|
|
3279
|
+
/**
|
|
3280
|
+
* Arbitrary JSON data associated with the evaluation.
|
|
3281
|
+
*/
|
|
3282
|
+
metadata?: { [key: string]: any }
|
|
3249
3283
|
}
|
|
3250
3284
|
|
|
3251
3285
|
interface Document {
|
package/package.json
CHANGED
|
@@ -59,28 +59,46 @@ class DatadogStorage extends AsyncLocalStorage {
|
|
|
59
59
|
return stores.get(handle)
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// To handle all versions always correct, feature detect AsyncContextFrame and
|
|
65
|
+
// fallback to manual approach if not active. With ACF `run` delegates to
|
|
66
|
+
// `enterWith`, without ACF `run` does not.
|
|
67
|
+
const isACFActive = (() => {
|
|
68
|
+
let active = false
|
|
69
|
+
const als = new AsyncLocalStorage()
|
|
70
|
+
als.enterWith = () => { active = true }
|
|
71
|
+
als.run(1, () => {})
|
|
72
|
+
als.disable()
|
|
73
|
+
return active
|
|
74
|
+
})()
|
|
75
|
+
|
|
76
|
+
if (!isACFActive) {
|
|
77
|
+
const superGetStore = AsyncLocalStorage.prototype.getStore
|
|
78
|
+
const superEnterWith = AsyncLocalStorage.prototype.enterWith
|
|
62
79
|
|
|
63
80
|
/**
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
81
|
+
* Override the `run` method to manually call `enterWith` and `getStore`
|
|
82
|
+
* when not using AsyncContextFrame.
|
|
83
|
+
*
|
|
84
|
+
* Without ACF, super.run() won't call this.enterWith(), so the WeakMap handle
|
|
85
|
+
* is never created and getStore() would fail.
|
|
86
|
+
*
|
|
69
87
|
* @template R
|
|
70
|
-
* @template
|
|
88
|
+
* @template {unknown[]} TArgs
|
|
71
89
|
* @param {Store<unknown>} store
|
|
72
|
-
* @param {() => R} fn
|
|
73
|
-
* @param {
|
|
90
|
+
* @param {(...args: TArgs) => R} fn
|
|
91
|
+
* @param {TArgs} args
|
|
74
92
|
* @returns {R}
|
|
75
93
|
* @override
|
|
76
94
|
*/
|
|
77
|
-
run (store, fn, ...args) {
|
|
78
|
-
const prior =
|
|
95
|
+
DatadogStorage.prototype.run = function run (store, fn, ...args) {
|
|
96
|
+
const prior = superGetStore.call(this)
|
|
79
97
|
this.enterWith(store)
|
|
80
98
|
try {
|
|
81
99
|
return Reflect.apply(fn, null, args)
|
|
82
100
|
} finally {
|
|
83
|
-
|
|
101
|
+
superEnterWith.call(this, prior)
|
|
84
102
|
}
|
|
85
103
|
}
|
|
86
104
|
}
|
|
@@ -111,4 +129,4 @@ function storage (namespace) {
|
|
|
111
129
|
return storages[namespace]
|
|
112
130
|
}
|
|
113
131
|
|
|
114
|
-
module.exports = { storage }
|
|
132
|
+
module.exports = { storage, isACFActive }
|
|
@@ -353,6 +353,20 @@ function wrapRun (pl, isLatestVersion, version) {
|
|
|
353
353
|
isEfdRetry = numRetries > 0
|
|
354
354
|
}
|
|
355
355
|
|
|
356
|
+
// Check if all EFD retries failed
|
|
357
|
+
if (isEfdRetry && (isNew || isModified)) {
|
|
358
|
+
const statuses = lastStatusByPickleId.get(this.pickle.id)
|
|
359
|
+
if (statuses.length === earlyFlakeDetectionNumRetries + 1) {
|
|
360
|
+
const { fail } = statuses.reduce((acc, status) => {
|
|
361
|
+
acc[status]++
|
|
362
|
+
return acc
|
|
363
|
+
}, { pass: 0, fail: 0 })
|
|
364
|
+
if (fail === earlyFlakeDetectionNumRetries + 1) {
|
|
365
|
+
hasFailedAllRetries = true
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
356
370
|
const attemptCtx = numAttemptToCtx.get(numAttempt)
|
|
357
371
|
|
|
358
372
|
const error = getErrorFromCucumberResult(result)
|
|
@@ -136,6 +136,7 @@ module.exports = {
|
|
|
136
136
|
'selenium-webdriver': () => require('../selenium'),
|
|
137
137
|
sequelize: () => require('../sequelize'),
|
|
138
138
|
sharedb: () => require('../sharedb'),
|
|
139
|
+
stripe: () => require('../stripe'),
|
|
139
140
|
tedious: () => require('../tedious'),
|
|
140
141
|
tinypool: { esmFirst: true, fn: () => require('../vitest') },
|
|
141
142
|
undici: () => require('../undici'),
|
|
@@ -14,6 +14,7 @@ const finishChannel = channel('apm:http:client:request:finish')
|
|
|
14
14
|
const endChannel = channel('apm:http:client:request:end')
|
|
15
15
|
const asyncStartChannel = channel('apm:http:client:request:asyncStart')
|
|
16
16
|
const errorChannel = channel('apm:http:client:request:error')
|
|
17
|
+
const responseFinishChannel = channel('apm:http:client:response:finish')
|
|
17
18
|
|
|
18
19
|
const names = ['http', 'https', 'node:http', 'node:https']
|
|
19
20
|
|
|
@@ -39,6 +40,112 @@ function normalizeCallback (inputOptions, callback, inputURL) {
|
|
|
39
40
|
return typeof inputOptions === 'function' ? [inputOptions, inputURL || {}] : [callback, inputOptions]
|
|
40
41
|
}
|
|
41
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Wires the downstream response so we can observe when the customer consumes
|
|
45
|
+
* the body and when the stream finishes
|
|
46
|
+
*
|
|
47
|
+
* @param {object} ctx - Instrumentation context
|
|
48
|
+
* @param {import('http').IncomingMessage} res - The downstream response object.
|
|
49
|
+
* @returns {{ finalizeIfNeeded: () => void }|null} Cleanup helper used for drain.
|
|
50
|
+
*/
|
|
51
|
+
function setupResponseInstrumentation (ctx, res) {
|
|
52
|
+
const shouldInstrumentFinish = responseFinishChannel.hasSubscribers
|
|
53
|
+
|
|
54
|
+
if (!shouldInstrumentFinish) {
|
|
55
|
+
return null
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let bodyConsumed = false
|
|
59
|
+
let finishCalled = false
|
|
60
|
+
let originalRead = null
|
|
61
|
+
let dataListenerAdded = false
|
|
62
|
+
let dataReadStarted = false
|
|
63
|
+
|
|
64
|
+
const { shouldCollectBody } = ctx
|
|
65
|
+
const bodyChunks = shouldCollectBody ? [] : null
|
|
66
|
+
|
|
67
|
+
const collectChunk = chunk => {
|
|
68
|
+
if (!shouldCollectBody || !chunk) return
|
|
69
|
+
|
|
70
|
+
if (typeof chunk === 'string') {
|
|
71
|
+
bodyChunks.push(chunk)
|
|
72
|
+
} else if (Buffer.isBuffer(chunk)) {
|
|
73
|
+
bodyChunks.push(chunk)
|
|
74
|
+
} else {
|
|
75
|
+
// Handle Uint8Array or other array-like types
|
|
76
|
+
bodyChunks.push(Buffer.from(chunk))
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Listen for body consumption
|
|
81
|
+
const onNewListener = (eventName) => {
|
|
82
|
+
if (eventName === 'data' || eventName === 'readable') {
|
|
83
|
+
bodyConsumed = true
|
|
84
|
+
|
|
85
|
+
// For 'data' events, add our own listener to collect chunks
|
|
86
|
+
if (eventName === 'data' && !dataListenerAdded && !dataReadStarted) {
|
|
87
|
+
dataListenerAdded = true
|
|
88
|
+
res.on('data', collectChunk)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// For 'readable' events, wrap the read() method
|
|
92
|
+
if (eventName === 'readable' && !originalRead && !dataListenerAdded && typeof res.read === 'function') {
|
|
93
|
+
originalRead = res.read
|
|
94
|
+
res.read = function () {
|
|
95
|
+
const chunk = originalRead.apply(this, arguments)
|
|
96
|
+
if (!dataListenerAdded) {
|
|
97
|
+
dataReadStarted = true
|
|
98
|
+
collectChunk(chunk)
|
|
99
|
+
}
|
|
100
|
+
return chunk
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
res.on('newListener', onNewListener)
|
|
107
|
+
|
|
108
|
+
// Cleanup function to restore original behavior
|
|
109
|
+
const cleanup = () => {
|
|
110
|
+
res.off('newListener', onNewListener)
|
|
111
|
+
res.off('data', collectChunk)
|
|
112
|
+
|
|
113
|
+
if (originalRead) {
|
|
114
|
+
res.read = originalRead
|
|
115
|
+
originalRead = null
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const notifyFinish = () => {
|
|
120
|
+
if (finishCalled) return
|
|
121
|
+
finishCalled = true
|
|
122
|
+
|
|
123
|
+
// Combine collected chunks into a single body
|
|
124
|
+
let body = null
|
|
125
|
+
if (bodyChunks?.length) {
|
|
126
|
+
const firstChunk = bodyChunks[0]
|
|
127
|
+
body = typeof firstChunk === 'string'
|
|
128
|
+
? bodyChunks.join('')
|
|
129
|
+
: Buffer.concat(bodyChunks)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
responseFinishChannel.publish({ ctx, res, body })
|
|
133
|
+
cleanup()
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
res.once('end', notifyFinish)
|
|
137
|
+
res.once('close', notifyFinish)
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
finalizeIfNeeded () {
|
|
141
|
+
if (!bodyConsumed) {
|
|
142
|
+
// Body not consumed, resume to complete the response
|
|
143
|
+
notifyFinish()
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
42
149
|
function patch (http, methodName) {
|
|
43
150
|
shimmer.wrap(http, methodName, instrumentRequest)
|
|
44
151
|
|
|
@@ -103,7 +210,18 @@ function patch (http, methodName) {
|
|
|
103
210
|
ctx.res = res
|
|
104
211
|
res.once('end', finish)
|
|
105
212
|
res.once(errorMonitor, finish)
|
|
106
|
-
|
|
213
|
+
|
|
214
|
+
const instrumentation = setupResponseInstrumentation(ctx, res)
|
|
215
|
+
|
|
216
|
+
if (!instrumentation) {
|
|
217
|
+
break
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const result = emit.apply(this, arguments)
|
|
221
|
+
|
|
222
|
+
instrumentation.finalizeIfNeeded()
|
|
223
|
+
|
|
224
|
+
return result
|
|
107
225
|
}
|
|
108
226
|
case 'connect':
|
|
109
227
|
case 'upgrade':
|
|
@@ -7,6 +7,7 @@ const {
|
|
|
7
7
|
getCoveredFilenamesFromCoverage,
|
|
8
8
|
JEST_WORKER_TRACE_PAYLOAD_CODE,
|
|
9
9
|
JEST_WORKER_COVERAGE_PAYLOAD_CODE,
|
|
10
|
+
JEST_WORKER_TELEMETRY_PAYLOAD_CODE,
|
|
10
11
|
getTestLineStart,
|
|
11
12
|
getTestSuitePath,
|
|
12
13
|
getTestParametersString,
|
|
@@ -16,6 +17,7 @@ const {
|
|
|
16
17
|
isModifiedTest,
|
|
17
18
|
} = require('../../dd-trace/src/plugins/util/test')
|
|
18
19
|
const {
|
|
20
|
+
SEED_SUFFIX_RE,
|
|
19
21
|
getFormattedJestTestParameters,
|
|
20
22
|
getJestTestName,
|
|
21
23
|
getJestSuitesToRun,
|
|
@@ -35,6 +37,7 @@ const testSuiteErrorCh = channel('ci:jest:test-suite:error')
|
|
|
35
37
|
const workerReportTraceCh = channel('ci:jest:worker-report:trace')
|
|
36
38
|
const workerReportCoverageCh = channel('ci:jest:worker-report:coverage')
|
|
37
39
|
const workerReportLogsCh = channel('ci:jest:worker-report:logs')
|
|
40
|
+
const workerReportTelemetryCh = channel('ci:jest:worker-report:telemetry')
|
|
38
41
|
|
|
39
42
|
const testSuiteCodeCoverageCh = channel('ci:jest:test-suite:code-coverage')
|
|
40
43
|
|
|
@@ -55,6 +58,7 @@ const itrSkippedSuitesCh = channel('ci:jest:itr:skipped-suites')
|
|
|
55
58
|
// Message sent by jest's main process to workers to run a test suite (=test file)
|
|
56
59
|
// https://github.com/jestjs/jest/blob/1d682f21c7a35da4d3ab3a1436a357b980ebd0fa/packages/jest-worker/src/types.ts#L37
|
|
57
60
|
const CHILD_MESSAGE_CALL = 1
|
|
61
|
+
|
|
58
62
|
// Maximum time we'll wait for the tracer to flush
|
|
59
63
|
const FLUSH_TIMEOUT = 10_000
|
|
60
64
|
|
|
@@ -125,6 +129,8 @@ function getTestEnvironmentOptions (config) {
|
|
|
125
129
|
return {}
|
|
126
130
|
}
|
|
127
131
|
|
|
132
|
+
const MAX_IGNORED_TEST_NAMES = 10
|
|
133
|
+
|
|
128
134
|
function getTestStats (testStatuses) {
|
|
129
135
|
return testStatuses.reduce((acc, testStatus) => {
|
|
130
136
|
acc[testStatus]++
|
|
@@ -132,6 +138,32 @@ function getTestStats (testStatuses) {
|
|
|
132
138
|
}, { pass: 0, fail: 0 })
|
|
133
139
|
}
|
|
134
140
|
|
|
141
|
+
/**
|
|
142
|
+
* @param {string[]} efdNames
|
|
143
|
+
* @param {string[]} quarantineNames
|
|
144
|
+
* @param {number} totalCount
|
|
145
|
+
*/
|
|
146
|
+
function logIgnoredFailuresSummary (efdNames, quarantineNames, totalCount) {
|
|
147
|
+
const names = []
|
|
148
|
+
for (const n of efdNames) {
|
|
149
|
+
names.push({ name: n, reason: 'Early Flake Detection' })
|
|
150
|
+
}
|
|
151
|
+
for (const n of quarantineNames) {
|
|
152
|
+
names.push({ name: n, reason: 'Quarantine' })
|
|
153
|
+
}
|
|
154
|
+
const shown = names.slice(0, MAX_IGNORED_TEST_NAMES)
|
|
155
|
+
const more = names.length - shown.length
|
|
156
|
+
const moreSuffix = more > 0 ? `\n ... and ${more} more` : ''
|
|
157
|
+
const list = shown.map(({ name, reason }) => ` • ${name} (${reason})`).join('\n')
|
|
158
|
+
const line = '-'.repeat(50)
|
|
159
|
+
// eslint-disable-next-line no-console -- Intentional user-facing message when exit code is flipped
|
|
160
|
+
console.warn(
|
|
161
|
+
`\n${line}\nDatadog Test Optimization\n${line}\n` +
|
|
162
|
+
`${totalCount} test failure(s) were ignored. Exit code set to 0.\n\n` +
|
|
163
|
+
`${list}${moreSuffix}\n`
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
|
|
135
167
|
function getWrappedEnvironment (BaseEnvironment, jestVersion) {
|
|
136
168
|
return class DatadogEnvironment extends BaseEnvironment {
|
|
137
169
|
constructor (config, context) {
|
|
@@ -515,6 +547,7 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
|
|
|
515
547
|
}
|
|
516
548
|
}
|
|
517
549
|
if (event.name === 'test_done') {
|
|
550
|
+
const originalError = event.test?.errors?.[0]
|
|
518
551
|
let status = 'pass'
|
|
519
552
|
if (event.test.errors && event.test.errors.length) {
|
|
520
553
|
status = 'fail'
|
|
@@ -571,6 +604,13 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
|
|
|
571
604
|
} else {
|
|
572
605
|
newTestsTestStatuses.set(testName, [status])
|
|
573
606
|
}
|
|
607
|
+
const testStatuses = newTestsTestStatuses.get(testName)
|
|
608
|
+
// Check if this is the last EFD retry.
|
|
609
|
+
// If it is, we'll set the failedAllTests flag to true if all the tests failed
|
|
610
|
+
if (testStatuses.length === earlyFlakeDetectionNumRetries + 1 &&
|
|
611
|
+
testStatuses.every(status => status === 'fail')) {
|
|
612
|
+
failedAllTests = true
|
|
613
|
+
}
|
|
574
614
|
}
|
|
575
615
|
}
|
|
576
616
|
|
|
@@ -594,7 +634,7 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
|
|
|
594
634
|
const shouldSetProbe = this.isDiEnabled && willBeRetriedByFailedTestReplay && numTestExecutions === 1
|
|
595
635
|
testErrCh.publish({
|
|
596
636
|
...ctx.currentStore,
|
|
597
|
-
error: formatJestError(
|
|
637
|
+
error: formatJestError(originalError),
|
|
598
638
|
shouldSetProbe,
|
|
599
639
|
promises,
|
|
600
640
|
finalStatus,
|
|
@@ -954,7 +994,7 @@ function getCliWrapper (isNewJestVersion) {
|
|
|
954
994
|
isTestManagementTestsEnabled = false
|
|
955
995
|
testManagementTests = {}
|
|
956
996
|
} else {
|
|
957
|
-
testManagementTests = receivedTestManagementTests
|
|
997
|
+
testManagementTests = receivedTestManagementTests || {}
|
|
958
998
|
}
|
|
959
999
|
} catch (err) {
|
|
960
1000
|
log.error('Jest test management tests error', err)
|
|
@@ -989,11 +1029,18 @@ function getCliWrapper (isNewJestVersion) {
|
|
|
989
1029
|
coverageMap,
|
|
990
1030
|
numFailedTestSuites,
|
|
991
1031
|
numFailedTests,
|
|
1032
|
+
numRuntimeErrorTestSuites = 0,
|
|
992
1033
|
numTotalTests,
|
|
993
1034
|
numTotalTestSuites,
|
|
1035
|
+
runExecError,
|
|
1036
|
+
wasInterrupted,
|
|
994
1037
|
},
|
|
995
1038
|
} = result
|
|
996
1039
|
|
|
1040
|
+
const hasSuiteLevelFailures = numRuntimeErrorTestSuites > 0
|
|
1041
|
+
const hasRunLevelFailure = runExecError != null || wasInterrupted === true
|
|
1042
|
+
const mustNotFlipSuccess = hasSuiteLevelFailures || hasRunLevelFailure
|
|
1043
|
+
|
|
997
1044
|
let testCodeCoverageLinesTotal
|
|
998
1045
|
|
|
999
1046
|
if (isUserCodeCoverageEnabled) {
|
|
@@ -1012,33 +1059,67 @@ function getCliWrapper (isNewJestVersion) {
|
|
|
1012
1059
|
* The rationale behind is the following: you may still be able to block your CI pipeline by gating
|
|
1013
1060
|
* on flakiness (the test will be considered flaky), but you may choose to unblock the pipeline too.
|
|
1014
1061
|
*/
|
|
1062
|
+
let numEfdFailedTestsToIgnore = 0
|
|
1063
|
+
const efdIgnoredNames = []
|
|
1064
|
+
const quarantineIgnoredNames = []
|
|
1065
|
+
|
|
1066
|
+
// Build fullName -> suite map from results (for EFD display)
|
|
1067
|
+
const fullNameToSuite = new Map()
|
|
1068
|
+
for (const { testResults, testFilePath } of result.results.testResults) {
|
|
1069
|
+
const suite = getTestSuitePath(testFilePath, result.globalConfig.rootDir)
|
|
1070
|
+
for (const { fullName } of testResults) {
|
|
1071
|
+
const name = testSuiteAbsolutePathsWithFastCheck.has(testFilePath)
|
|
1072
|
+
? fullName.replace(SEED_SUFFIX_RE, '')
|
|
1073
|
+
: fullName
|
|
1074
|
+
fullNameToSuite.set(name, suite)
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
/** @type {{ efdNames: string[], quarantineNames: string[], totalCount: number } | undefined} */
|
|
1079
|
+
let ignoredFailuresSummary
|
|
1015
1080
|
if (isEarlyFlakeDetectionEnabled) {
|
|
1016
|
-
|
|
1017
|
-
for (const testStatuses of newTestsTestStatuses.values()) {
|
|
1081
|
+
for (const [testName, testStatuses] of newTestsTestStatuses) {
|
|
1018
1082
|
const { pass, fail } = getTestStats(testStatuses)
|
|
1019
1083
|
if (pass > 0) { // as long as one passes, we'll consider the test passed
|
|
1020
|
-
|
|
1084
|
+
numEfdFailedTestsToIgnore += fail
|
|
1085
|
+
const suite = fullNameToSuite.get(testName)
|
|
1086
|
+
efdIgnoredNames.push(suite ? `${suite} › ${testName}` : testName)
|
|
1021
1087
|
}
|
|
1022
1088
|
}
|
|
1023
1089
|
// If every test that failed was an EFD retry, we'll consider the suite passed
|
|
1024
|
-
if (
|
|
1090
|
+
if (
|
|
1091
|
+
!mustNotFlipSuccess &&
|
|
1092
|
+
numEfdFailedTestsToIgnore !== 0 &&
|
|
1093
|
+
result.results.numFailedTests === numEfdFailedTestsToIgnore
|
|
1094
|
+
) {
|
|
1025
1095
|
result.results.success = true
|
|
1096
|
+
ignoredFailuresSummary = {
|
|
1097
|
+
efdNames: efdIgnoredNames,
|
|
1098
|
+
quarantineNames: [],
|
|
1099
|
+
totalCount: numEfdFailedTestsToIgnore,
|
|
1100
|
+
}
|
|
1026
1101
|
}
|
|
1027
1102
|
}
|
|
1028
1103
|
|
|
1104
|
+
let numFailedQuarantinedTests = 0
|
|
1105
|
+
let numFailedQuarantinedOrDisabledAttemptedToFixTests = 0
|
|
1029
1106
|
if (isTestManagementTestsEnabled) {
|
|
1030
1107
|
const failedTests = result
|
|
1031
1108
|
.results
|
|
1032
1109
|
.testResults.flatMap(({ testResults, testFilePath: testSuiteAbsolutePath }) => (
|
|
1033
1110
|
testResults.map(({ fullName: testName, status }) => (
|
|
1034
|
-
{
|
|
1111
|
+
{
|
|
1112
|
+
// Strip @fast-check/jest seed suffix so the name matches what was reported via TEST_NAME
|
|
1113
|
+
testName: testSuiteAbsolutePathsWithFastCheck.has(testSuiteAbsolutePath)
|
|
1114
|
+
? testName.replace(SEED_SUFFIX_RE, '')
|
|
1115
|
+
: testName,
|
|
1116
|
+
testSuiteAbsolutePath,
|
|
1117
|
+
status,
|
|
1118
|
+
}
|
|
1035
1119
|
))
|
|
1036
1120
|
))
|
|
1037
1121
|
.filter(({ status }) => status === 'failed')
|
|
1038
1122
|
|
|
1039
|
-
let numFailedQuarantinedTests = 0
|
|
1040
|
-
let numFailedQuarantinedOrDisabledAttemptedToFixTests = 0
|
|
1041
|
-
|
|
1042
1123
|
for (const { testName, testSuiteAbsolutePath } of failedTests) {
|
|
1043
1124
|
const testSuite = getTestSuitePath(testSuiteAbsolutePath, result.globalConfig.rootDir)
|
|
1044
1125
|
const testManagementTest = testManagementTests
|
|
@@ -1051,8 +1132,10 @@ function getCliWrapper (isNewJestVersion) {
|
|
|
1051
1132
|
// This uses `attempt_to_fix` because this is always the main process and it's not formatted in camelCase
|
|
1052
1133
|
if (testManagementTest?.attempt_to_fix && (testManagementTest?.quarantined || testManagementTest?.disabled)) {
|
|
1053
1134
|
numFailedQuarantinedOrDisabledAttemptedToFixTests++
|
|
1135
|
+
quarantineIgnoredNames.push(`${testSuite} › ${testName}`)
|
|
1054
1136
|
} else if (testManagementTest?.quarantined) {
|
|
1055
1137
|
numFailedQuarantinedTests++
|
|
1138
|
+
quarantineIgnoredNames.push(`${testSuite} › ${testName}`)
|
|
1056
1139
|
}
|
|
1057
1140
|
}
|
|
1058
1141
|
|
|
@@ -1060,12 +1143,42 @@ function getCliWrapper (isNewJestVersion) {
|
|
|
1060
1143
|
// Note that if a test is attempted to fix,
|
|
1061
1144
|
// it's considered quarantined both if it's disabled and if it's quarantined
|
|
1062
1145
|
// (it'll run but its status is ignored)
|
|
1146
|
+
// Skip if EFD block already flipped (to avoid logging twice)
|
|
1063
1147
|
if (
|
|
1148
|
+
!result.results.success &&
|
|
1149
|
+
!mustNotFlipSuccess &&
|
|
1064
1150
|
(numFailedQuarantinedOrDisabledAttemptedToFixTests !== 0 || numFailedQuarantinedTests !== 0) &&
|
|
1065
1151
|
result.results.numFailedTests ===
|
|
1066
1152
|
numFailedQuarantinedTests + numFailedQuarantinedOrDisabledAttemptedToFixTests
|
|
1067
1153
|
) {
|
|
1068
1154
|
result.results.success = true
|
|
1155
|
+
ignoredFailuresSummary = {
|
|
1156
|
+
efdNames: [],
|
|
1157
|
+
quarantineNames: quarantineIgnoredNames,
|
|
1158
|
+
totalCount: numFailedQuarantinedTests + numFailedQuarantinedOrDisabledAttemptedToFixTests,
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
// Combined check: if all failed tests are accounted for by EFD (flaky retries) and/or quarantine,
|
|
1164
|
+
// we should consider the suite passed even when neither check alone covers all failures.
|
|
1165
|
+
if (
|
|
1166
|
+
!result.results.success &&
|
|
1167
|
+
!mustNotFlipSuccess &&
|
|
1168
|
+
(isEarlyFlakeDetectionEnabled || isTestManagementTestsEnabled)
|
|
1169
|
+
) {
|
|
1170
|
+
const totalIgnoredFailures =
|
|
1171
|
+
numEfdFailedTestsToIgnore + numFailedQuarantinedTests + numFailedQuarantinedOrDisabledAttemptedToFixTests
|
|
1172
|
+
if (
|
|
1173
|
+
totalIgnoredFailures !== 0 &&
|
|
1174
|
+
result.results.numFailedTests === totalIgnoredFailures
|
|
1175
|
+
) {
|
|
1176
|
+
result.results.success = true
|
|
1177
|
+
ignoredFailuresSummary = {
|
|
1178
|
+
efdNames: efdIgnoredNames,
|
|
1179
|
+
quarantineNames: quarantineIgnoredNames,
|
|
1180
|
+
totalCount: totalIgnoredFailures,
|
|
1181
|
+
}
|
|
1069
1182
|
}
|
|
1070
1183
|
}
|
|
1071
1184
|
|
|
@@ -1123,6 +1236,14 @@ function getCliWrapper (isNewJestVersion) {
|
|
|
1123
1236
|
})
|
|
1124
1237
|
}
|
|
1125
1238
|
|
|
1239
|
+
if (ignoredFailuresSummary) {
|
|
1240
|
+
logIgnoredFailuresSummary(
|
|
1241
|
+
ignoredFailuresSummary.efdNames,
|
|
1242
|
+
ignoredFailuresSummary.quarantineNames,
|
|
1243
|
+
ignoredFailuresSummary.totalCount
|
|
1244
|
+
)
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1126
1247
|
numSkippedSuites = 0
|
|
1127
1248
|
|
|
1128
1249
|
return result
|
|
@@ -1534,6 +1655,10 @@ function onMessageWrapper (onMessage) {
|
|
|
1534
1655
|
workerReportLogsCh.publish(data)
|
|
1535
1656
|
return
|
|
1536
1657
|
}
|
|
1658
|
+
if (code === JEST_WORKER_TELEMETRY_PAYLOAD_CODE) { // datadog telemetry payload
|
|
1659
|
+
workerReportTelemetryCh.publish(data)
|
|
1660
|
+
return
|
|
1661
|
+
}
|
|
1537
1662
|
return onMessage.apply(this, arguments)
|
|
1538
1663
|
}
|
|
1539
1664
|
}
|
|
@@ -165,6 +165,15 @@ function getOnEndHandler (isParallel) {
|
|
|
165
165
|
this.failures -= numFailedQuarantinedTests + numFailedRetriedQuarantinedOrDisabledTests
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
+
// Recompute status after EFD and quarantine adjustments have reduced failure counts
|
|
169
|
+
if (status === 'fail') {
|
|
170
|
+
if (this.stats) {
|
|
171
|
+
status = this.stats.failures === 0 ? 'pass' : 'fail'
|
|
172
|
+
} else {
|
|
173
|
+
status = this.failures === 0 ? 'pass' : 'fail'
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
168
177
|
if (status === 'fail') {
|
|
169
178
|
error = new Error(`Failed tests: ${this.failures}.`)
|
|
170
179
|
}
|
|
@@ -271,6 +271,7 @@ function getOnTestEndHandler (config) {
|
|
|
271
271
|
const testStatuses = testsStatuses.get(testName)
|
|
272
272
|
|
|
273
273
|
const isLastAttempt = testStatuses.length === config.testManagementAttemptToFixRetries + 1
|
|
274
|
+
const isLastEfdRetry = testStatuses.length === config.earlyFlakeDetectionNumRetries + 1
|
|
274
275
|
|
|
275
276
|
if (test._ddIsAttemptToFix && isLastAttempt) {
|
|
276
277
|
if (testStatuses.includes('fail')) {
|
|
@@ -283,6 +284,11 @@ function getOnTestEndHandler (config) {
|
|
|
283
284
|
}
|
|
284
285
|
}
|
|
285
286
|
|
|
287
|
+
if (test._ddIsEfdRetry && isLastEfdRetry &&
|
|
288
|
+
testStatuses.every(status => status === 'fail')) {
|
|
289
|
+
hasFailedAllRetries = true
|
|
290
|
+
}
|
|
291
|
+
|
|
286
292
|
const isAttemptToFixRetry = test._ddIsAttemptToFix && testStatuses.length > 1
|
|
287
293
|
const isAtrRetry = config.isFlakyTestRetriesEnabled &&
|
|
288
294
|
!test._ddIsAttemptToFix &&
|